From 087e8386f50d1312e6d43057992de76058704206 Mon Sep 17 00:00:00 2001 From: avi Date: Tue, 15 Sep 2026 07:24:57 -0500 Subject: [PATCH] Transcription: dwindle model choice to Whisper Base + Whisper Large v3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Registry now lists exactly two models instead of five: - 'Whisper Base' — names the actual bundled model (was 'Base (default)'). - 'Whisper Large v3' — kept; honest copy on size/speed/RAM. tiny/small/medium removed from the registry (validate_model_name now rejects them; existing rows keep their saved model — all current data is 'base', still valid). Detail screen shows the model that WILL run by real display name ('Whisper Base (default)') instead of 'Use default (base)'; the picker lists the two models with the default marked inline. Backend 78 passed/1 skipped; app 34/34; engine hot-reloaded clean. --- .../main/kotlin/com/shonar/desktop/Screens.kt | 18 +++---- backend/shonar/services/ai/model_registry.py | 34 +++---------- backend/tests/test_inline_queue.py | 4 +- backend/tests/test_models.py | 51 ++++++++++--------- 4 files changed, 43 insertions(+), 64 deletions(-) diff --git a/app/src/main/kotlin/com/shonar/desktop/Screens.kt b/app/src/main/kotlin/com/shonar/desktop/Screens.kt index 04d6381..6dbba1b 100644 --- a/app/src/main/kotlin/com/shonar/desktop/Screens.kt +++ b/app/src/main/kotlin/com/shonar/desktop/Screens.kt @@ -601,24 +601,24 @@ fun DetailScreen(state: DesktopState) { } } } - // Model override: "Use default" or a specific size. + // Model override: shows the model that WILL run, by its real name. Row(verticalAlignment = Alignment.CenterVertically) { Text("Model: ", style = MaterialTheme.typography.bodyMedium) - val current = detail.overrideModel - ?: models?.defaultModel - ?: "base" + val current = detail.overrideModel ?: models?.defaultModel ?: "base" + val currentLabel = models?.byName(current)?.displayName ?: current TextButton({ showModels = !showModels }) { - Text(if (detail.overrideModel == null) "Use default ($current)" else current) + Text(if (detail.overrideModel == null) + "$currentLabel (default)" else currentLabel) } } if (showModels && models != null) { Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { - ModelRow(state, detail, null, models.defaultModel, - "Use default (${models.defaultModel})", "") models.models.forEach { m -> ModelRow(state, detail, m.name, models.defaultModel, - m.displayName, m.description + - (if (!m.available) " — not downloaded" else "")) + m.displayName + (if (m.name == models.defaultModel) + " (default)" else ""), + m.description + + (if (!m.available) " — downloads ~once from the app" else "")) } } Spacer(Modifier.height(4.dp)) diff --git a/backend/shonar/services/ai/model_registry.py b/backend/shonar/services/ai/model_registry.py index a578115..f12ca7a 100644 --- a/backend/shonar/services/ai/model_registry.py +++ b/backend/shonar/services/ai/model_registry.py @@ -37,42 +37,20 @@ class TranscriptionModelInfo: SUPPORTED_TRANSCRIPTION_MODELS: dict[str, TranscriptionModelInfo] = { - "tiny": TranscriptionModelInfo( - name="tiny", - display_name="Tiny", - description="Fastest and lightest. Good for quick drafts and slow machines.", - params="~39M", - approx_memory="~1 GB RAM", - relative_speed="~10x real-time (CPU)", - ), "base": TranscriptionModelInfo( name="base", - display_name="Base (default)", - description="Balanced default. Works reasonably well on ordinary computers.", + display_name="Whisper Base", + description="The bundled model — works offline out of the box. " + "Good for clear speech on ordinary computers.", params="~74M", approx_memory="~1 GB RAM", relative_speed="~7x real-time (CPU)", ), - "small": TranscriptionModelInfo( - name="small", - display_name="Small", - description="Better accuracy with higher resource usage.", - params="~244M", - approx_memory="~2 GB RAM", - relative_speed="~4x real-time (CPU)", - ), - "medium": TranscriptionModelInfo( - name="medium", - display_name="Medium", - description="Higher accuracy and slower performance.", - params="~769M", - approx_memory="~5 GB RAM", - relative_speed="~2x real-time (CPU)", - ), "large-v3": TranscriptionModelInfo( name="large-v3", - display_name="Large v3", - description="Highest accuracy and greatest resource requirements.", + display_name="Whisper Large v3", + description="Best accuracy (accents, names, messy audio). Downloads " + "~3 GB once; slow on CPU — best on machines with 16 GB+ RAM.", params="~1.5B", approx_memory="~10 GB RAM", relative_speed="~1x real-time (CPU)", diff --git a/backend/tests/test_inline_queue.py b/backend/tests/test_inline_queue.py index d71c277..cd2b6ff 100644 --- a/backend/tests/test_inline_queue.py +++ b/backend/tests/test_inline_queue.py @@ -116,7 +116,7 @@ async def test_reprocess_reruns_in_place_with_model(client, monkeypatch): calls_before = stt.calls r = await client.post( - f"/api/v1/recordings/{rec['id']}/reprocess?job=transcribe&model=small", + f"/api/v1/recordings/{rec['id']}/reprocess?job=transcribe&model=large-v3", headers=h, ) assert r.status_code == 200, r.text @@ -127,7 +127,7 @@ async def test_reprocess_reruns_in_place_with_model(client, monkeypatch): # 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" + assert r.json()["transcription_model"] == "large-v3" finally: await inline_queue.stop() diff --git a/backend/tests/test_models.py b/backend/tests/test_models.py index 44a96a4..8501e49 100644 --- a/backend/tests/test_models.py +++ b/backend/tests/test_models.py @@ -59,16 +59,16 @@ async def upload_recording(client, token, client_id, session_model=None, finaliz return r -async def test_models_endpoint_lists_five(client): +async def test_models_endpoint_lists_two(client): token = await user_tokens(client) r = await client.get("/api/v1/models", headers={"Authorization": f"Bearer {token}"}) assert r.status_code == 200, r.text body = r.json() assert body["default_model"] == "base" - assert [m["name"] for m in body["models"]] == ["tiny", "base", "small", "medium", "large-v3"] + assert [m["name"] for m in body["models"]] == ["base", "large-v3"] base = next(m for m in body["models"] if m["name"] == "base") assert base["is_default"] is True - assert "ordinary computers" in base["description"] + assert "bundled model" in base["description"] for m in body["models"]: assert isinstance(m["downloaded"], bool) assert isinstance(m["available"], bool) @@ -79,32 +79,32 @@ async def test_default_get_put_validation(client): h = {"Authorization": f"Bearer {token}"} r = await client.get("/api/v1/models/default", headers=h) assert r.json() == {"default_model": "base"} - r = await client.put("/api/v1/models/default", json={"model": "small"}, headers=h) + r = await client.put("/api/v1/models/default", json={"model": "large-v3"}, headers=h) assert r.status_code == 200, r.text - assert r.json() == {"default_model": "small"} + assert r.json() == {"default_model": "large-v3"} r = await client.put("/api/v1/models/default", json={"model": "xxl-turbo"}, headers=h) assert r.status_code == 422 assert "Supported models" in r.text # rejected change did not stick r = await client.get("/api/v1/models/default", headers=h) - assert r.json() == {"default_model": "small"} + assert r.json() == {"default_model": "large-v3"} async def test_finalize_override_and_default_history(client): token = await user_tokens(client) h = {"Authorization": f"Bearer {token}"} - r = await upload_recording(client, token, "m-override-1", finalize_model="small") + r = await upload_recording(client, token, "m-override-1", finalize_model="large-v3") assert r.status_code == 201, r.text - assert r.json()["transcription_model"] == "small" + assert r.json()["transcription_model"] == "large-v3" r = await upload_recording(client, token, "m-default-1") assert r.json()["transcription_model"] == "base" # Changing the default affects future rows only. - r = await client.put("/api/v1/models/default", json={"model": "tiny"}, headers=h) + r = await client.put("/api/v1/models/default", json={"model": "large-v3"}, headers=h) assert r.status_code == 200 r = await upload_recording(client, token, "m-default-2") - assert r.json()["transcription_model"] == "tiny" + assert r.json()["transcription_model"] == "large-v3" async with db_session._session_factory() as s: got = dict((await s.execute( @@ -112,19 +112,20 @@ async def test_finalize_override_and_default_history(client): .where(Recording.client_recording_id.in_( ["m-override-1", "m-default-1", "m-default-2"]))) ).all()) - assert got == {"m-override-1": "small", "m-default-1": "base", "m-default-2": "tiny"} + assert got == {"m-override-1": "large-v3", "m-default-1": "base", + "m-default-2": "large-v3"} async def test_session_override_and_finalize_wins(client): token = await user_tokens(client) - r = await upload_recording(client, token, "m-sess-1", session_model="small") + r = await upload_recording(client, token, "m-sess-1", session_model="large-v3") assert r.status_code == 201, r.text - assert r.json()["transcription_model"] == "small" + assert r.json()["transcription_model"] == "large-v3" - r = await upload_recording(client, token, "m-sess-2", session_model="small", - finalize_model="tiny") + r = await upload_recording(client, token, "m-sess-2", session_model="large-v3", + finalize_model="base") assert r.status_code == 201, r.text - assert r.json()["transcription_model"] == "tiny" + assert r.json()["transcription_model"] == "base" async def test_finalize_invalid_override_rejected(client): @@ -175,15 +176,15 @@ async def _run_with_spy(monkeypatch, recording_id): async def test_worker_uses_saved_model(client, monkeypatch): token = await user_tokens(client) - rec = (await upload_recording(client, token, "m-work-1", finalize_model="small")).json() + rec = (await upload_recording(client, token, "m-work-1", finalize_model="large-v3")).json() await _run_with_spy(monkeypatch, rec["id"]) # Last construction is the worker rebuild from the saved model. - assert SpyTranscriber.seen_models[-1] == "small" + assert SpyTranscriber.seen_models[-1] == "large-v3" h = {"Authorization": f"Bearer {token}"} r = await client.get(f"/api/v1/recordings/{rec['id']}/transcript", headers=h) assert r.status_code == 200 - assert r.json()["model"] == "small" + assert r.json()["model"] == "large-v3" r = await client.get(f"/api/v1/recordings/{rec['id']}/jobs", headers=h) job = next(j for j in r.json() if j["job_type"] == "transcribe") @@ -197,7 +198,7 @@ async def test_worker_keeps_history_after_default_change(client, monkeypatch): h = {"Authorization": f"Bearer {token}"} rec = (await upload_recording(client, token, "m-work-2")).json() assert rec["transcription_model"] == "base" - r = await client.put("/api/v1/models/default", json={"model": "tiny"}, headers=h) + r = await client.put("/api/v1/models/default", json={"model": "large-v3"}, headers=h) assert r.status_code == 200 await _run_with_spy(monkeypatch, rec["id"]) assert SpyTranscriber.seen_models[-1] == "base" @@ -229,15 +230,15 @@ async def test_unavailable_model_fails_with_instructions(client, monkeypatch): async def test_reupload_explicit_override_updates_model(client): token = await user_tokens(client) - r = await upload_recording(client, token, "m-reup-1", finalize_model="small") - assert r.json()["transcription_model"] == "small" + r = await upload_recording(client, token, "m-reup-1", finalize_model="large-v3") + assert r.json()["transcription_model"] == "large-v3" # Same client id, new explicit choice: model history moves with it. - r = await upload_recording(client, token, "m-reup-1", finalize_model="tiny") + r = await upload_recording(client, token, "m-reup-1", finalize_model="base") assert r.status_code == 201, r.text - assert r.json()["transcription_model"] == "tiny" + assert r.json()["transcription_model"] == "base" # Same client id, no choice: history untouched. r = await upload_recording(client, token, "m-reup-1") - assert r.json()["transcription_model"] == "tiny" + assert r.json()["transcription_model"] == "base" async def test_models_need_auth(client):