Skip to content

Commit f34c887

Browse files
fix: make Cohere Transcribe usable and stop buffered streaming from crawling (#54)
1 parent a110094 commit f34c887

10 files changed

Lines changed: 623 additions & 67 deletions

File tree

app/catalog.py

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,10 @@
115115
FASTER_WHISPER_MEDIUM_SIZE_BYTES = 1_530_575_217
116116
FASTER_WHISPER_MEDIUM_EN_SIZE_BYTES = 1_530_460_562
117117
ACCURATE_QUALITY = "Accurate"
118+
# Buffered streaming exports pay for their low latency in throughput: they
119+
# re-encode a whole context window per step, so they must not sit in a picker
120+
# under a label like "Fast" beside the batch export of the same weights.
121+
LOW_LATENCY_HIGH_CPU_QUALITY = "Low latency · high CPU"
118122
# Turbo is not the most accurate Whisper — the Open ASR Leaderboard puts it at
119123
# 6.36 average WER against full Large v3's 5.78 — so it must not claim to be in
120124
# a picker that also offers Large v3 and the Q5 build of those same weights.
@@ -219,6 +223,15 @@ class CatalogModel:
219223
decoder_prompt: str | None = None
220224
apple_silicon_only: bool = False
221225
detects_language_automatically: bool = False
226+
# True when the decoder refuses to run without being told the spoken
227+
# language. Clients must not offer "detect language" for these — the
228+
# request fails outright rather than falling back to a default.
229+
requires_explicit_language: bool = False
230+
# A non-streaming export of the same weights, for models whose streaming
231+
# export is buffered rather than cache-aware and so costs many times more
232+
# compute per second of audio. Used only for whole-file transcription,
233+
# where there is no latency to save; live streaming still uses this model.
234+
batch_twin_id: str | None = None
222235
retired: bool = False
223236
replacement_id: str | None = None
224237
retirement_reason: str | None = None
@@ -527,6 +540,8 @@ def sherpa_onnx(
527540
detects_language_automatically=bool(
528541
kwargs.get("detects_language_automatically", False)
529542
),
543+
requires_explicit_language=bool(kwargs.get("requires_explicit_language", False)),
544+
batch_twin_id=kwargs.get("batch_twin_id"),
530545
)
531546

532547
@classmethod
@@ -1356,8 +1371,9 @@ def _github_release_page(cls, archive_url: str | None) -> str | None:
13561371
family="Parakeet Unified",
13571372
model_type=NEMO_TRANSDUCER_TYPE,
13581373
description=(
1359-
"Unified FastConformer RNNT for English. INT8 CPU export. The streaming variant uses "
1360-
"560 ms model context; end-to-end latency depends on the host. "
1374+
"Unified FastConformer RNNT for English. INT8 CPU export. Encodes the whole "
1375+
"recording in one pass, which makes it roughly twenty times cheaper than the "
1376+
"streaming export of the same weights — prefer it unless you need live partials. "
13611377
),
13621378
language_codes=ENGLISH_CODES,
13631379
license_name="NVIDIA Open Model License",
@@ -1368,19 +1384,24 @@ def _github_release_page(cls, archive_url: str | None) -> str | None:
13681384
"Parakeet Unified English INT8 Streaming 560 ms",
13691385
PARAKEET_UNIFIED_STREAMING_SIZE_BYTES,
13701386
ENGLISH_ONLY,
1371-
"Fast · streaming",
1387+
LOW_LATENCY_HIGH_CPU_QUALITY,
13721388
4,
13731389
huggingface_repo="csukuangfj2/sherpa-onnx-nemo-parakeet-unified-en-0.6b-int8-streaming-560ms",
13741390
required_files=(ENCODER_INT8_FILE, DECODER_INT8_FILE, JOINER_INT8_FILE, TOKENS_FILE),
13751391
family="Parakeet Unified",
13761392
model_type=STREAMING_TRANSDUCER_TYPE,
13771393
description=(
1378-
"Unified FastConformer RNNT for English. INT8 CPU export. The streaming variant uses "
1379-
"560 ms model context; end-to-end latency depends on the host. "
1394+
"Unified FastConformer RNNT for English, exported for 560 ms streaming latency. "
1395+
"That 560 ms is how long after a word you see it, not how fast it runs: this is a "
1396+
"buffered export, so every 160 ms step re-encodes the whole 6.2-second context "
1397+
"window. It costs roughly twenty times the compute of the non-streaming export of "
1398+
"the same weights and only just keeps up with real time on a busy CPU. Install it "
1399+
"for live partials; install the non-streaming variant for recorded audio. "
13801400
),
13811401
language_codes=ENGLISH_CODES,
13821402
license_name="NVIDIA Open Model License",
13831403
supports_streaming=True,
1404+
batch_twin_id=f"{ENGINE_SHERPA_ONNX}:parakeet-unified-en-0.6b-int8",
13841405
),
13851406
_sherpa_onnx(
13861407
"cohere-transcribe-14-lang-int8",
@@ -1398,10 +1419,12 @@ def _github_release_page(cls, archive_url: str | None) -> str | None:
13981419
),
13991420
family="Cohere Transcribe",
14001421
model_type=COHERE_TRANSCRIBE_TYPE,
1422+
requires_explicit_language=True,
14011423
description=(
14021424
"Cohere's multilingual speech recognizer through a community INT8 CPU export. Select "
14031425
"the spoken language explicitly; automatic language detection and Hindi are not "
1404-
"supported. "
1426+
"supported. The decoder rejects the request outright rather than guessing, so "
1427+
'clients left on "detect language" get an error instead of a transcript. '
14051428
),
14061429
language_codes=(
14071430
ENGLISH_LANGUAGE_CODE,

app/engines.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,38 @@ def catalog_selection(
120120
chosen = min(installed, key=lambda model: model.size_bytes)
121121
return chosen.path, self.model_manager.catalog_model(chosen.id)
122122

123+
def sherpa_engine(
124+
self,
125+
selection: tuple[Path | None, catalog.CatalogModel | None],
126+
rc: runtime_config.RuntimeConfig,
127+
) -> sherpa_onnx.SherpaOnnxEngine:
128+
"""Build the sherpa engine, handing it a batch twin when one is installed."""
129+
root, model = selection
130+
twin_root, twin_model = self.batch_twin(model)
131+
return sherpa_onnx.SherpaOnnxEngine(
132+
root,
133+
model,
134+
cpu_threads=rc.cpu_threads,
135+
batch_root=twin_root,
136+
batch_model=twin_model,
137+
)
138+
139+
def batch_twin(
140+
self, model: catalog.CatalogModel | None
141+
) -> tuple[Path | None, catalog.CatalogModel | None]:
142+
"""Where the batch twin lives, whether or not it is installed yet.
143+
144+
The engine re-checks that directory on every use, so handing it the
145+
path the twin *would* occupy is what lets a twin downloaded after the
146+
engine was built start being used -- and one deleted afterwards stop --
147+
without a rebuild that nothing on those paths triggers.
148+
"""
149+
twin_id = model.batch_twin_id if model else None
150+
twin_model = self.model_manager.catalog_model(twin_id) if twin_id else None
151+
if twin_model is None:
152+
return None, None
153+
return self.model_manager.model_path(twin_model), twin_model
154+
123155
def catalog_model_for_path(self, path: Path | None) -> catalog.CatalogModel | None:
124156
if path is None:
125157
return None
@@ -196,7 +228,7 @@ def _resolve_fallback(self, rc: runtime_config.RuntimeConfig) -> base.Transcript
196228
return mlx_audio.MLXAudioEngine(mlx_sel[0], mlx_sel[1])
197229
shp_sel = self.catalog_selection(rc.sherpa_model, catalog.ENGINE_SHERPA_ONNX)
198230
if shp_sel[0] is not None:
199-
return sherpa_onnx.SherpaOnnxEngine(shp_sel[0], shp_sel[1], cpu_threads=rc.cpu_threads)
231+
return self.sherpa_engine(shp_sel, rc)
200232
fw_p = self.resolve_path(catalog.ENGINE_FASTER_WHISPER, rc)
201233
if fw_p is not None:
202234
return faster_whisper.FasterWhisperEngine(
@@ -286,7 +318,7 @@ def _build_named(
286318
model_id = rc.sherpa_model if sherpa else rc.mlx_audio_model
287319
sel = self.resolver.catalog_selection(model_id, engine)
288320
return (
289-
sherpa_onnx.SherpaOnnxEngine(sel[0], sel[1], cpu_threads=rc.cpu_threads)
321+
self.resolver.sherpa_engine(sel, rc)
290322
if sherpa
291323
else mlx_audio.MLXAudioEngine(sel[0], sel[1])
292324
)

0 commit comments

Comments
 (0)