api: reprocess accepts ?model= to switch the saved override on re-run

Validates through validate_model_name (422, never silently substituted)
and persists rec.transcription_model — the worker reads the row, so the
switch survives into the queued job. 2 new inline-queue tests: in-place
re-run with model switch (same id, one extra transcribe call), and
validation/ownership errors (409 nothing-to-summarize, 422 bad model,
404 not owner).
This commit is contained in:
avi 2026-09-13 20:36:54 -05:00
commit 0cba721ada
2 changed files with 65 additions and 1 deletions

View file

@ -337,6 +337,7 @@ async def reprocess_recording(
user: CurrentUser, user: CurrentUser,
session: SessionDep, session: SessionDep,
job: str = Query(default="summarize", pattern="^(transcribe|summarize)$"), job: str = Query(default="summarize", pattern="^(transcribe|summarize)$"),
model: str | None = Query(default=None, max_length=64),
): ):
"""Force one pipeline stage to run again (Summarize / Re-transcribe). """Force one pipeline stage to run again (Summarize / Re-transcribe).
@ -345,7 +346,8 @@ async def reprocess_recording(
deliberate request. Running jobs are left alone (409 instead of a deliberate request. Running jobs are left alone (409 instead of a
duplicate). duplicate).
""" """
from shonar.db.models import JobStatus, JobType as JT, ProcessingStatus from shonar.db.models import JobStatus, ProcessingStatus
from shonar.db.models import JobType as JT
from shonar.services import processing as proc from shonar.services import processing as proc
rec = await _owned_recording(session, user, recording_id) rec = await _owned_recording(session, user, recording_id)
@ -378,6 +380,16 @@ async def reprocess_recording(
existing.started_at = None existing.started_at = None
existing.finished_at = None existing.finished_at = None
if job_type is JT.transcribe: if job_type is JT.transcribe:
if model is not None:
# A re-transcribe may switch models; the saved per-recording
# override is what the worker reads, so persist it here.
from shonar.services.ai import ProviderConfigError
from shonar.services.ai.model_registry import validate_model_name
try:
rec.transcription_model = validate_model_name(model)
except ProviderConfigError as e:
raise HTTPException(422, str(e)) from e
rec.processing_status = ProcessingStatus.processing rec.processing_status = ProcessingStatus.processing
rec.processing_error = None rec.processing_error = None
await session.flush() await session.flush()

View file

@ -99,3 +99,55 @@ async def test_inline_retries_transient_failure(client, monkeypatch):
assert state["tries"] >= 2 # the retry actually happened assert state["tries"] >= 2 # the retry actually happened
finally: finally:
await inline_queue.stop() await inline_queue.stop()
async def test_reprocess_reruns_in_place_with_model(client, monkeypatch):
"""POST /reprocess?job=transcribe&model= re-runs the pipeline on the
SAME recording (no re-upload) and persists the model override."""
stt = FakeTranscriber()
use_fakes(monkeypatch, stt, FakeLlm())
use_inline(monkeypatch)
await inline_queue.start()
try:
token = await user_tokens(client, email="reproc@shonar.dev")
h = {"Authorization": f"Bearer {token}"}
rec = await upload_recording(client, token, client_id="reproc-1")
await _wait_terminal(rec["id"], want=2)
calls_before = stt.calls
r = await client.post(
f"/api/v1/recordings/{rec['id']}/reprocess?job=transcribe&model=small",
headers=h,
)
assert r.status_code == 200, r.text
statuses = await _wait_terminal(rec["id"], want=2)
assert statuses[JobType.transcribe] == JobStatus.succeeded
assert stt.calls == calls_before + 1 # re-ran, no new recording
# Same recording id, model override persisted on the row.
r = await client.get(f"/api/v1/recordings/{rec['id']}", headers=h)
assert r.json()["id"] == rec["id"]
assert r.json()["transcription_model"] == "small"
finally:
await inline_queue.stop()
async def test_reprocess_validation(client, monkeypatch):
token = await user_tokens(client, email="reproc2@shonar.dev")
h = {"Authorization": f"Bearer {token}"}
rec = await upload_recording(client, token, client_id="reproc-2")
# Nothing transcribed yet -> summarize refuses.
r = await client.post(
f"/api/v1/recordings/{rec['id']}/reprocess?job=summarize", headers=h)
assert r.status_code == 409
# Bad model name -> 422 (never silently substituted).
r = await client.post(
f"/api/v1/recordings/{rec['id']}/reprocess?job=transcribe&model=not-a-model",
headers=h)
assert r.status_code == 422
# Not the owner -> 404.
other = await user_tokens(client, email="reproc3@shonar.dev")
r = await client.post(
f"/api/v1/recordings/{rec['id']}/reprocess?job=transcribe",
headers={"Authorization": f"Bearer {other}"})
assert r.status_code == 404