Transcription: dwindle model choice to Whisper Base + Whisper Large v3
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.
This commit is contained in:
parent
7234a62eb5
commit
087e8386f5
4 changed files with 43 additions and 64 deletions
|
|
@ -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) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Text("Model: ", style = MaterialTheme.typography.bodyMedium)
|
Text("Model: ", style = MaterialTheme.typography.bodyMedium)
|
||||||
val current = detail.overrideModel
|
val current = detail.overrideModel ?: models?.defaultModel ?: "base"
|
||||||
?: models?.defaultModel
|
val currentLabel = models?.byName(current)?.displayName ?: current
|
||||||
?: "base"
|
|
||||||
TextButton({ showModels = !showModels }) {
|
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) {
|
if (showModels && models != null) {
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||||
ModelRow(state, detail, null, models.defaultModel,
|
|
||||||
"Use default (${models.defaultModel})", "")
|
|
||||||
models.models.forEach { m ->
|
models.models.forEach { m ->
|
||||||
ModelRow(state, detail, m.name, models.defaultModel,
|
ModelRow(state, detail, m.name, models.defaultModel,
|
||||||
m.displayName, m.description +
|
m.displayName + (if (m.name == models.defaultModel)
|
||||||
(if (!m.available) " — not downloaded" else ""))
|
" (default)" else ""),
|
||||||
|
m.description +
|
||||||
|
(if (!m.available) " — downloads ~once from the app" else ""))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer(Modifier.height(4.dp))
|
Spacer(Modifier.height(4.dp))
|
||||||
|
|
|
||||||
|
|
@ -37,42 +37,20 @@ class TranscriptionModelInfo:
|
||||||
|
|
||||||
|
|
||||||
SUPPORTED_TRANSCRIPTION_MODELS: dict[str, 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(
|
"base": TranscriptionModelInfo(
|
||||||
name="base",
|
name="base",
|
||||||
display_name="Base (default)",
|
display_name="Whisper Base",
|
||||||
description="Balanced default. Works reasonably well on ordinary computers.",
|
description="The bundled model — works offline out of the box. "
|
||||||
|
"Good for clear speech on ordinary computers.",
|
||||||
params="~74M",
|
params="~74M",
|
||||||
approx_memory="~1 GB RAM",
|
approx_memory="~1 GB RAM",
|
||||||
relative_speed="~7x real-time (CPU)",
|
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(
|
"large-v3": TranscriptionModelInfo(
|
||||||
name="large-v3",
|
name="large-v3",
|
||||||
display_name="Large v3",
|
display_name="Whisper Large v3",
|
||||||
description="Highest accuracy and greatest resource requirements.",
|
description="Best accuracy (accents, names, messy audio). Downloads "
|
||||||
|
"~3 GB once; slow on CPU — best on machines with 16 GB+ RAM.",
|
||||||
params="~1.5B",
|
params="~1.5B",
|
||||||
approx_memory="~10 GB RAM",
|
approx_memory="~10 GB RAM",
|
||||||
relative_speed="~1x real-time (CPU)",
|
relative_speed="~1x real-time (CPU)",
|
||||||
|
|
|
||||||
|
|
@ -116,7 +116,7 @@ async def test_reprocess_reruns_in_place_with_model(client, monkeypatch):
|
||||||
calls_before = stt.calls
|
calls_before = stt.calls
|
||||||
|
|
||||||
r = await client.post(
|
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,
|
headers=h,
|
||||||
)
|
)
|
||||||
assert r.status_code == 200, r.text
|
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.
|
# Same recording id, model override persisted on the row.
|
||||||
r = await client.get(f"/api/v1/recordings/{rec['id']}", headers=h)
|
r = await client.get(f"/api/v1/recordings/{rec['id']}", headers=h)
|
||||||
assert r.json()["id"] == rec["id"]
|
assert r.json()["id"] == rec["id"]
|
||||||
assert r.json()["transcription_model"] == "small"
|
assert r.json()["transcription_model"] == "large-v3"
|
||||||
finally:
|
finally:
|
||||||
await inline_queue.stop()
|
await inline_queue.stop()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -59,16 +59,16 @@ async def upload_recording(client, token, client_id, session_model=None, finaliz
|
||||||
return r
|
return r
|
||||||
|
|
||||||
|
|
||||||
async def test_models_endpoint_lists_five(client):
|
async def test_models_endpoint_lists_two(client):
|
||||||
token = await user_tokens(client)
|
token = await user_tokens(client)
|
||||||
r = await client.get("/api/v1/models", headers={"Authorization": f"Bearer {token}"})
|
r = await client.get("/api/v1/models", headers={"Authorization": f"Bearer {token}"})
|
||||||
assert r.status_code == 200, r.text
|
assert r.status_code == 200, r.text
|
||||||
body = r.json()
|
body = r.json()
|
||||||
assert body["default_model"] == "base"
|
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")
|
base = next(m for m in body["models"] if m["name"] == "base")
|
||||||
assert base["is_default"] is True
|
assert base["is_default"] is True
|
||||||
assert "ordinary computers" in base["description"]
|
assert "bundled model" in base["description"]
|
||||||
for m in body["models"]:
|
for m in body["models"]:
|
||||||
assert isinstance(m["downloaded"], bool)
|
assert isinstance(m["downloaded"], bool)
|
||||||
assert isinstance(m["available"], bool)
|
assert isinstance(m["available"], bool)
|
||||||
|
|
@ -79,32 +79,32 @@ async def test_default_get_put_validation(client):
|
||||||
h = {"Authorization": f"Bearer {token}"}
|
h = {"Authorization": f"Bearer {token}"}
|
||||||
r = await client.get("/api/v1/models/default", headers=h)
|
r = await client.get("/api/v1/models/default", headers=h)
|
||||||
assert r.json() == {"default_model": "base"}
|
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.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)
|
r = await client.put("/api/v1/models/default", json={"model": "xxl-turbo"}, headers=h)
|
||||||
assert r.status_code == 422
|
assert r.status_code == 422
|
||||||
assert "Supported models" in r.text
|
assert "Supported models" in r.text
|
||||||
# rejected change did not stick
|
# rejected change did not stick
|
||||||
r = await client.get("/api/v1/models/default", headers=h)
|
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):
|
async def test_finalize_override_and_default_history(client):
|
||||||
token = await user_tokens(client)
|
token = await user_tokens(client)
|
||||||
h = {"Authorization": f"Bearer {token}"}
|
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.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")
|
r = await upload_recording(client, token, "m-default-1")
|
||||||
assert r.json()["transcription_model"] == "base"
|
assert r.json()["transcription_model"] == "base"
|
||||||
|
|
||||||
# Changing the default affects future rows only.
|
# 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
|
assert r.status_code == 200
|
||||||
r = await upload_recording(client, token, "m-default-2")
|
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:
|
async with db_session._session_factory() as s:
|
||||||
got = dict((await s.execute(
|
got = dict((await s.execute(
|
||||||
|
|
@ -112,19 +112,20 @@ async def test_finalize_override_and_default_history(client):
|
||||||
.where(Recording.client_recording_id.in_(
|
.where(Recording.client_recording_id.in_(
|
||||||
["m-override-1", "m-default-1", "m-default-2"])))
|
["m-override-1", "m-default-1", "m-default-2"])))
|
||||||
).all())
|
).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):
|
async def test_session_override_and_finalize_wins(client):
|
||||||
token = await user_tokens(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.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",
|
r = await upload_recording(client, token, "m-sess-2", session_model="large-v3",
|
||||||
finalize_model="tiny")
|
finalize_model="base")
|
||||||
assert r.status_code == 201, r.text
|
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):
|
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):
|
async def test_worker_uses_saved_model(client, monkeypatch):
|
||||||
token = await user_tokens(client)
|
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"])
|
await _run_with_spy(monkeypatch, rec["id"])
|
||||||
# Last construction is the worker rebuild from the saved model.
|
# 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}"}
|
h = {"Authorization": f"Bearer {token}"}
|
||||||
r = await client.get(f"/api/v1/recordings/{rec['id']}/transcript", headers=h)
|
r = await client.get(f"/api/v1/recordings/{rec['id']}/transcript", headers=h)
|
||||||
assert r.status_code == 200
|
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)
|
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")
|
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}"}
|
h = {"Authorization": f"Bearer {token}"}
|
||||||
rec = (await upload_recording(client, token, "m-work-2")).json()
|
rec = (await upload_recording(client, token, "m-work-2")).json()
|
||||||
assert rec["transcription_model"] == "base"
|
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
|
assert r.status_code == 200
|
||||||
await _run_with_spy(monkeypatch, rec["id"])
|
await _run_with_spy(monkeypatch, rec["id"])
|
||||||
assert SpyTranscriber.seen_models[-1] == "base"
|
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):
|
async def test_reupload_explicit_override_updates_model(client):
|
||||||
token = await user_tokens(client)
|
token = await user_tokens(client)
|
||||||
r = await upload_recording(client, token, "m-reup-1", finalize_model="small")
|
r = await upload_recording(client, token, "m-reup-1", finalize_model="large-v3")
|
||||||
assert r.json()["transcription_model"] == "small"
|
assert r.json()["transcription_model"] == "large-v3"
|
||||||
# Same client id, new explicit choice: model history moves with it.
|
# 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.status_code == 201, r.text
|
||||||
assert r.json()["transcription_model"] == "tiny"
|
assert r.json()["transcription_model"] == "base"
|
||||||
# Same client id, no choice: history untouched.
|
# Same client id, no choice: history untouched.
|
||||||
r = await upload_recording(client, token, "m-reup-1")
|
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):
|
async def test_models_need_auth(client):
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue