Skip to content

Feature: contextual hotword biasing for Qwen3-ASR #219

Description

@ivan-digital

Summary

Add user-provided hotword biasing to Qwen3-ASR so callers can pass a list of phrases (proper nouns, product names, domain jargon) that the decoder should prefer during token generation. No retraining, no new model — a decode-time intervention that adds a score bonus to tokens that advance a match against a precomputed trie.

Motivation

Qwen3-ASR picks the most likely transcription, which is often not the most correct one for niche vocabulary:

Baseline Desired
"off clarer" "Aufklärer"
"cocoro" "Kokoro"
"fast confirmer" "FastConformer"
"fire red V A D" "FireRedVAD"

Callers currently post-process with fuzzy matching, which fixes the word but not the word-boundary / capitalization artifacts the misrecognition induces. Biasing moves the fix into the decoder where it belongs.

Proposed API

```swift
let asr = try await Qwen3ASRModel.fromPretrained()

let hotwords = HotwordSet(phrases: [
.init("Aufklärer", score: 2.0),
.init("speech-swift", score: 2.5),
.init("Kokoro", score: 2.0),
.init("FastConformer", score: 2.0),
])

let text = asr.transcribe(
audio: audio,
sampleRate: 16000,
hotwords: hotwords
)
```

  • `HotwordSet` owns a token-id trie built from the tokenizer once; reused across calls.
  • `score` is an optional per-phrase bonus added to the logits when the decoder is "inside" that phrase in the trie.
  • Existing `transcribe(audio:sampleRate:language:context:)` signature stays; `hotwords` is a new optional parameter.

Implementation

Code reuse

`Sources/SpeechWakeWord/ContextGraph.swift` already implements an Aho-Corasick BPE-token trie for the keyword-spotting decoder. The same data structure applies here — the only difference is the score-application point. Promote `ContextGraph` to `AudioCommon` and use it from both modules.

Integration point

In the Qwen3-ASR autoregressive token step (MLX path):

  1. Maintain a per-stream trie cursor (starts at root, advances on each emitted token).
  2. Before argmax/sampling, add `score` to the logit of every token that would advance the cursor from its current node.
  3. On match completion, reset cursor to root (phrase committed).
  4. On mismatch, fall back through Aho-Corasick failure links (preserves partial matches across phrase overlap).

Purely decode-time — no change to model weights or forward pass.

Modules touched

  • `Sources/AudioCommon/` — promote `ContextGraph` here (rename namespace).
  • `Sources/Qwen3ASR/` — add `HotwordSet`, wire into text decoder token loop.
  • `Sources/SpeechWakeWord/` — import `ContextGraph` from `AudioCommon`.

CLI

```bash
audio transcribe recording.wav --hotwords "Kokoro,Aufklärer,FastConformer"
audio transcribe recording.wav --hotwords-file vocab.txt # one phrase per line, optional ":score" suffix
```

Tests

  • Unit: `HotwordSet` trie build from tokenizer; score lookup on a token path; failure-link traversal on mismatch.
  • E2E: transcribe a clip with a non-standard term (e.g., a TTS-generated phrase containing "Aufklärer"). Assert:
    • without hotwords → model produces the misrecognized spelling
    • with hotwords → model produces the expected spelling

Benchmarking

  • Relative WER on a domain set (a small hand-curated set with proper nouns).
  • Inference RTF with/without hotwords — should be negligible since it's a single extra vector add per step.
  • Scaling: how does WER change as hotword count grows (1, 10, 100, 1000)?

Out of scope

  • Language-model (external n-gram / neural LM) rescoring.
  • Automatic hotword discovery from transcript context.
  • Extending to Parakeet / Omnilingual / Nemotron (separate follow-ups once the abstraction is proven on Qwen3-ASR).

References

  • `Sources/SpeechWakeWord/ContextGraph.swift` — existing Aho-Corasick BPE trie.
  • `Sources/Qwen3ASR/QuantizedTextDecoder.swift` — token generation loop (integration point).

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions