Skip to content

feat(huddle): make STT model selectable + add multilingual Parakeet v3 (#2478)#2520

Open
artile wants to merge 1 commit into
block:mainfrom
artile:feat/multilingual-huddle-stt
Open

feat(huddle): make STT model selectable + add multilingual Parakeet v3 (#2478)#2520
artile wants to merge 1 commit into
block:mainfrom
artile:feat/multilingual-huddle-stt

Conversation

@artile

@artile artile commented Jul 23, 2026

Copy link
Copy Markdown

Summary

Huddle transcription was hard-pinned to an English-only STT model, so non-English speech transcribed as garbage (#2478). There was no language setting, env override, or model-swap path — the loader knew exactly one model.

This makes the huddle STT model selectable, and ships a multilingual option alongside the English default.

Closes the "no model-swap path" root cause in #2478; see the coverage note below for what is and isn't covered by the bundled multilingual model.

What changed

  • huddle/models.rs — a small STT model registry. STT_MODELS is the single source of truth for each model: id, download URL, archive SHA-256, expected files, sherpa-onnx family, manifest version, license text, and language coverage.
  • select_stt_model(override, locale) — pure, unit-tested selection. Precedence (issue Huddle transcription is English-only: non-English speech (e.g. Korean) produces garbled transcripts #2478 options 2 + 3):
    1. BUZZ_STT_MODEL env override, when it names a known model (case-insensitive),
    2. else a non-English system locale → the multilingual model,
    3. else the English default.
  • Two models ship, both with a pinned SHA-256:
    id model family languages
    parakeet-en (default) Parakeet TDT-CTC 110M NeMo CTC English
    parakeet-v3 Parakeet TDT 0.6B v3 transducer 25 European (auto-detected)
  • huddle/stt.rs configures sherpa-onnx per family: NeMo CTC (single model.int8.onnx) or transducer (encoder/decoder/joiner.int8.onnx); tokens.txt is shared. Downloaded on demand via the existing flow.
  • Per-model checksum is now Option. The size cap, safe extraction, and expected-files verification always apply; the English default stays hash-pinned. Both shipped models are pinned today (the v3 hash was computed from the k2-fsa release archive, since upstream publishes none).

Model choice — why Parakeet v3, and the honest coverage note

The issue suggested NVIDIA Parakeet v3, and a sherpa-onnx export exists (sherpa-onnx-nemo-parakeet-tdt-0.6b-v3-int8), so it is what this PR bundles. It adds 25 European languages with automatic language-ID.

Parakeet v3 covers European languages only. The concrete case in #2478 is Korean, which v3 does not cover. This PR removes the architectural root cause (the English-only hard pin) and adds the multilingual model + per-family config path; a CJK model (e.g. SenseVoice-Small, KO/EN/JA/ZH/yue) is now a one-entry follow-up in the registry — the transducer/CTC plumbing it would reuse is already here. Happy to add it in a follow-up if you'd like Korean in this same PR instead.

Testing

  • cargo check -p buzz-desktop — clean.
  • cargo clippy -p buzz-desktop — clean, no warnings.
  • cargo fmt — clean.
  • cargo test -p buzz-desktop huddle::models8 passed, 0 failed. New tests cover: English default, non-English locale → multilingual, override precedence + case-insensitivity, unknown/empty override fallback, registry invariants (unique ids, English default hash-pinned, a multilingual model exists), license-sidecar readiness, and per-family (transducer) readiness.

I can't run a live non-English transcription in CI here (it needs the ~465 MB v3 model + a huddle), but the selection/config/readiness paths are unit-tested and the archive contents/subdir/files were verified against the k2-fsa release. Glad to help test Korean once a CJK model lands (ties back to #2478).

Notes

  • Zero unsafe; no panics/unwraps on the new paths.
  • No new dependencies (locale is read from standard POSIX env vars).
  • The English model stays the default and is unchanged for existing users.

Refs #2478

…block#2478)

Huddle transcription was hard-pinned to an English-only STT model, so
non-English speech transcribed as garbage (block#2478). There was no language
setting, env override, or model-swap path — the loader knew one model.

Make the huddle STT model selectable behind a small registry:

- `STT_MODELS` in huddle/models.rs is the single source of truth for each
  model: id, download URL, archive checksum, expected files, sherpa-onnx
  family, manifest version, license, and language coverage.
- `select_stt_model` picks a model from the `BUZZ_STT_MODEL` override, then
  the system locale (non-English -> multilingual), then the English default.
  The function is pure and unit-tested.
- Two models ship: the existing Parakeet TDT-CTC 110M (English, default) and
  Parakeet TDT 0.6B v3 (multilingual, 25 European languages). Both carry a
  pinned SHA-256; the v3 hash was computed from the k2-fsa release archive.
- huddle/stt.rs configures the sherpa-onnx recognizer per family: NeMo CTC
  (single model.int8.onnx) or transducer (encoder/decoder/joiner). tokens.txt
  is shared. The archive checksum is now optional per model (size cap, safe
  extraction, and expected-files verification always apply); the English
  default stays pinned.

Coverage note: Parakeet v3 covers European languages only. Korean/CJK — the
concrete case in block#2478 — is a follow-up: the registry makes adding a CJK
model (e.g. SenseVoice-Small) one data entry plus a family already modelled.

Zero unsafe, no panics/unwraps on the new paths. New model selection logic is
covered by unit tests in huddle::models.

Refs block#2478

Signed-off-by: Taras Kornichuk <dread9ko@gmail.com>
@artile
artile force-pushed the feat/multilingual-huddle-stt branch from d6c3482 to 9236cb8 Compare July 23, 2026 14:32

@LauraGPT LauraGPT 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.

Thanks for opening the model-selection path. I tested the exact language from #2478 and found one correctness gap: the current locale fallback still routes ko-KR to Parakeet v3, although v3 does not support Korean.

I verified the official sherpa-onnx SenseVoice export end to end:

  • archive: sherpa-onnx-sense-voice-zh-en-ja-ko-yue-int8-2024-07-17.tar.bz2
  • size: 163002883 bytes
  • SHA-256: 7d1efa2138a65b0b488df37f8b89e3d91a60676e416f515b952358d83dfd347e
  • required files: model.int8.onnx, tokens.txt
  • coverage: zh/en/ja/ko/yue
  • live CPU check with sherpa-onnx 1.13.4: the packaged 4.608 s Korean sample decoded in 0.211 s, detected <|ko|>, and produced normal Korean text. The packaged zh/ja/en/yue samples also decoded successfully.

The smallest complete fix is to add a SenseVoice family and registry entry, then select models by declared primary-language tags: zh/ja/ko/yue -> SenseVoice, Parakeet's supported European tags -> v3, en -> the existing English default. Unknown non-English locales should not silently select a model that does not support them.

One licensing detail: SenseVoice is under the FunASR Model Open Source License 1.1, not CC-BY-4.0. The archive includes a LICENSE pointer to the FunASR license, so the generic license_text comments should not assume every model is CC-BY.

.unwrap_or("")
.to_ascii_lowercase();
if !lang.is_empty() && lang != "en" {
if let Some(model) = STT_MODELS.iter().find(|m| m.multilingual) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This selects the first multilingual model for every non-English locale. In the reported case, ko-KR therefore selects parakeet-v3, but the registry itself documents that v3 has no Korean coverage. Please dispatch on supported primary-language tags rather than a boolean (and add a regression test for ko-KR). If SenseVoice is deferred, at minimum only auto-select v3 for its 25 declared language tags so this PR does not replace one unsupported model with another.

// Coverage note: Parakeet v3 covers European languages only. CJK/Korean
// (the concrete case in #2478) is not covered by this model; the registry
// makes adding a CJK model (e.g. SenseVoice-Small) a follow-up — one data
// entry, no engine change beyond a family already handled here.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SenseVoice is not one data entry with the currently handled families: sherpa-onnx 1.13.4 exposes it through cfg.model_config.sense_voice, so this needs a SttFamily::SenseVoice arm in stt.rs. The configuration is a single model.int8.onnx path plus language = "auto" and use_itn = true; it should not be configured as NemoCtc or Transducer.

Copy link
Copy Markdown

I implemented the complete SenseVoice follow-up and opened a collaboration PR directly against your current branch: artile#1

It is based exactly on 9236cb855d7f2b09d6508f35a9f412f14563ca71, so merging it will update this PR without a rebase or cherry-pick. The patch adds the SenseVoice sherpa-onnx family, routes zh/yue/ja/ko to the verified int8 model, limits Parakeet v3 auto-selection to its declared European languages, and uses the correct FunASR model license.

Validation completed on the exact patch:

  • official zh/en/ja/ko/yue samples decoded successfully on CPU with sherpa-onnx 1.13.4;
  • Korean sample: 4.608 s audio, 0.211 s decode, detected <|ko|>;
  • cargo test --manifest-path desktop/src-tauri/Cargo.toml huddle:: -- --nocapture: 102 passed, 0 failed;
  • Clippy with -D warnings, rustfmt check, and git diff --check: passed.

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.

2 participants