Skip to content

fix: make Cohere Transcribe usable and stop buffered streaming from crawling - #54

Merged
Mr-Sunglasses merged 1 commit into
mainfrom
fix/cohere-language-and-buffered-streaming-throughput
Sep 6, 2026
Merged

fix: make Cohere Transcribe usable and stop buffered streaming from crawling#54
Mr-Sunglasses merged 1 commit into
mainfrom
fix/cohere-language-and-buffered-streaming-throughput

Conversation

@Mr-Sunglasses

Copy link
Copy Markdown
Member

Two reported problems, both reproduced against the real models on disk.

1. Cohere Transcribe never transcribed anything

Not a download or model problem — the model works fine once told a language:

cohere-transcribe-14-lang-int8  lang="en"    OK  infer=2304ms
  "Ask not what your country can do for you, ask what you can do for your country."
cohere-transcribe-14-lang-int8  lang="auto"  FAIL LanguageUnsupportedError

Its decoder refuses to run without an explicit language, and passing auto through
yields an empty transcript rather than a fallback:

Invalid language: 'auto'. Supported values: ar, de, el, en, es, fr, it, ja, ko, nl, pl, pt, vi, zh
lang='auto' en.wav: ''
lang=''     en.wav: ''   ("Please specify a language for Cohere Transcribe")

But every client defaults that field to autoCreateSessionRequest.language,
the test-transcription query parameter, and the test panel's first <option>. So
the model failed with language_unsupported on any default-configured client.

Fix. CatalogModel.requires_explicit_language, the mirror of
detects_language_automatically, is set on Cohere and reported by /health so mobile
clients can drop "Detect language" too. syncTestLanguages() in the web UI reads it,
hides that option and selects the first supported language instead. It also greys out
languages outside a model's coverage when the model does not self-detect, which heads
off the same class of failure (picking Hindi on Cohere). An empty languages list
still means "unknown", so nothing is restricted there. The engine-side guard stays as
the backstop.

2. "Streaming 560 ms" was ~20× slower than the batch export, not faster

Same audio, same machine, same 8 threads:

model inference
parakeet-unified-en-0.6b-int8 (non-streaming) 373 ms
parakeet-unified-en-0.6b-560ms-int8 (streaming) 7398 ms

The encoder's ONNX metadata explains it:

buffered_streaming     = 1
left_feature_frames    = 560
chunk_feature_frames   = 16
right_feature_frames   = 40
inputs: audio_signal, length      <- 2 inputs, no cache tensors

No cache state: this is a sliding-window export, not a cache-aware streaming
Conformer. Every step re-encodes 560+16+40 = 616 feature frames (~6.2 s of audio) to
emit 16 new frames (160 ms). Measured: 47 decode steps for 7.4 s of audio
(7.435 / 0.160 ≈ 46.5 ✔), ~152 ms each — about 290 s of audio pushed through the
encoder to transcribe 7.4 s. Thread scaling confirms it is compute, not overhead:

threads=1  decode=20.15s  429ms/chunk  rtf=2.71
threads=2  decode=12.17s  259ms/chunk  rtf=1.64
threads=4  decode= 8.33s  177ms/chunk  rtf=1.12
threads=8  decode= 7.14s  152ms/chunk  rtf=0.96

So "560 ms" is the latency before a word appears, not throughput. The model barely
keeps up with live audio and costs ~20× for a finished file, where there are no
partials to deliver early. There is no config knob for this.

Fix. Whole-file work goes to the batch export of the same weights when the user
has it. Models name a batch_twin_id; SherpaOnnxEngine holds one
_ResidentRecognizer per export, transcribe() uses the batch one and
create_stream() keeps the streaming one. When the twin is not installed both point
at the same holder, so behaviour is unchanged. Measured through the gateway:
7398 ms → 212 ms, identical transcript, streaming_supported still true.

Two bugs found reviewing that fix, and fixed

Resolving the twin once in __init__ was wrong, because nothing on the download or
delete paths rebuilds the engine
— only select_model, engine configure, and
deleting the active model do.

  • A twin downloaded later was never used. Reproduced live: install the twin while
    the streaming model is selected and transcription stays at RTF 1.005. Since the
    model description now tells users to install it, this would have silently done
    nothing.
  • A twin deleted later broke transcription. forget_if_active(twin_id) returns
    false for a non-active model, so the engine kept pointing at a removed directory.
    After the next unload it raised an unwrapped RuntimeError — an HTTP 500 — while
    /health still reported ready:
    twin present  -> infer=279ms
    twin deleted  -> RuntimeError: Load model from .../twin/encoder.int8.onnx
    

The twin is now resolved on each use, and a twin that cannot load falls back to the
selection, so an opportunistic speed-up can never fail a request the selection could
have served. Verified live, with no engine rebuild between steps:

streaming, no twin:        infer=7781ms rtf=1.047
twin appears:              infer= 212ms rtf=0.029
twin removed:              infer=7406ms rtf=0.996

unload() and model_is_resident still reach a twin that has since been uninstalled,
so idle offload can reclaim its weights.

Also

Relabelled the streaming model from "Fast · streaming" to "Low latency · high CPU"
and rewrote both Unified descriptions — the non-streaming entry's had been copy-pasted
and described the streaming variant.

Checks

592 tests pass (28 new), plus ruff check, ruff format --check,
flake8 --select=WPS,E999, and mypy — the full CI quality job. New tests cover the
twin wiring through EngineManager.select_model, a twin appearing and disappearing
without a rebuild, load-failure fallback, residency/unload across both exports, the
/health flag, and an assertion that Cohere is the only model flagged.

One judgement call worth flagging: warmup() now loads both exports when a twin is
in play (~1.3 GB instead of 663 MB), since an engine that reports itself warm and still
pays a cold load on the first dictation has warmed the wrong one. Easy to change if
you would rather warm only the batch export.

🤖 Generated with Claude Code

…rawling

Cohere Transcribe never transcribed anything from a default-configured
client. Its decoder refuses to run without an explicit language --
sherpa-onnx answers "Invalid language: 'auto'" and returns an empty
result -- but every client defaults that field to auto: the session
schema, the test-transcription query parameter, and the test panel's
first <option>. So the model failed with `language_unsupported` unless
the operator happened to change the dropdown first.

The catalog now carries `requires_explicit_language`, the mirror of
`detects_language_automatically`, and /health reports it so mobile
clients can drop "Detect language" too. The web test panel reads it and
hides that option, selecting the first supported language instead; it
also greys out languages outside a model's coverage when the model does
not detect its own, which heads off the same class of failure. The
engine-side guard stays as the backstop.

Parakeet Unified English INT8 Streaming 560 ms was ~20x slower than the
non-streaming export of the same weights, not faster. Its encoder ONNX
declares buffered_streaming=1 with a 560-frame left context, a 16-frame
chunk and 40 frames of right context, and takes no cache tensors: it
re-encodes a 6.2-second window every 160 ms step. 560 ms is the latency
before a word appears, not throughput. Measured on a 10-core M-series at
8 threads it decodes 7.4 s of audio in 7.4 s (RTF ~1.0) against 373 ms
for the batch export -- so it only just keeps up with live audio, and
costs ~20x for a finished file where there are no partials to deliver
early.

Nothing can make that export fast, so whole-file work now goes to the
batch export when the user has it. Models name a `batch_twin_id`;
SherpaOnnxEngine holds one recognizer per export, transcribe() uses the
batch one and create_stream() keeps the streaming one. Measured through
the gateway: 7398 ms -> 212 ms, identical transcript.

The twin is resolved on each use rather than snapshotted in __init__,
because nothing on the download or delete paths rebuilds the engine --
only select-model, engine config, and deleting the *active* model do.
Snapshotting meant a twin downloaded afterwards was never used, and one
deleted afterwards left the engine pointing at a missing directory,
raising an unwrapped RuntimeError as a 500 while /health still reported
ready. A twin that cannot load now falls back to the selection as well,
so an opportunistic speed-up can never fail a request the selection
could have served.

Also relabels the streaming model from "Fast · streaming" to "Low
latency · high CPU" and rewrites both Unified descriptions -- the
non-streaming entry's had been copy-pasted and described the wrong
model.
@netlify

netlify Bot commented Sep 6, 2026

Copy link
Copy Markdown

Deploy Preview for voca-gateway canceled.

Name Link
🔨 Latest commit bca8d92
🔍 Latest deploy log https://app.netlify.com/projects/voca-gateway/deploys/6a9cdf9656fd050008ec7ead

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mr-Sunglasses has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@Mr-Sunglasses
Mr-Sunglasses merged commit f34c887 into main Sep 6, 2026
8 checks passed
@Mr-Sunglasses
Mr-Sunglasses deleted the fix/cohere-language-and-buffered-streaming-throughput branch September 6, 2026 03:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant