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,
session: SessionDep,
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).
@ -345,7 +346,8 @@ async def reprocess_recording(
deliberate request. Running jobs are left alone (409 instead of a
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
rec = await _owned_recording(session, user, recording_id)
@ -378,6 +380,16 @@ async def reprocess_recording(
existing.started_at = None
existing.finished_at = None
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_error = None
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
finally:
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