Bias Voice Input with Hotwords¶
Open-vocabulary speech-to-text engines guess every word from acoustic evidence alone, which means they often mangle game-specific proper nouns — the NPC name "Aeltharion" becomes "Al Therion", the spell "frostbolt" becomes "frost bolt". Sherpa-ONNX exposes a hotwords list that nudges the decoder toward a designer-authored set of phrases without retraining the model.
This recipe shows how to ship a phrase list with your game and apply
it to a VoiceInput.
Prerequisites¶
- A working
VoiceInputsession — see Use Voice Input for the full setup walkthrough. - An STT model that supports hotwords. Most transducer / CTC families do; encoder-decoder families (Whisper, SenseVoice, Moonshine, Canary) log a warning and ignore the list when the voice input is created. The matrix and the warning are in Which models support hotwords below.
1. Author a phrase list¶
Hotwords live in a StringStorage of kind List, shipped as a UTF-8
text file in your game's storage data folder — one phrase per line.
Blank lines and lines starting with # are ignored:
# data/voice-hotwords.txt
# fantasy NPC names
Aeltharion
Cor'than
Bjornthor
# spells / abilities
fireball
frostbolt
magic missile
The file path is relative to the process storage root (Project Settings
Storage Data Folder, default TryllStorage/). The
server loads it on demand the first time a VoiceInput references it —
no CreateStringStorage call is needed.
2. Reference the file when creating VoiceInput¶
C++
#include <tryll/TryllClient.h>
#include <tryll/VoiceInput.h>
Tryll::Client::VoiceInputConfig viCfg;
viCfg.modelName = "Parakeet TDT 0.6B v3 (int8)";
viCfg.inputFormat = micFormat;
viCfg.hotwordsStoragePath = "data/voice-hotwords.txt";
viCfg.hotwordsScore = 1.8f; // bonus per matched phrase; default 1.5
auto vi = client.CreateVoiceInput(viCfg);
Python
voice = client.create_voice_input(
model_name="Parakeet TDT 0.6B v3 (int8)",
sample_rate=16000, channels=1, bits_per_sample=16,
hotwords_storage_path="data/voice-hotwords.txt",
hotwords_score=1.8,
)
Unity (C#)
var cfg = new VoiceInputConfig
{
ModelName = "Parakeet TDT 0.6B v3 (int8)",
InputFormat = AudioFormat.Default,
VadThreshold = 0.5f,
VadMinSilenceMs = 1000,
VadSpeechPadMs = 250,
HotwordsStoragePath = "data/voice-hotwords.txt",
HotwordsScore = 1.8f,
};
var (voice, err) = await client.CreateVoiceInputAsync(cfg);
On TryllVoiceInputComponent, set the same values in the inspector:
Hotwords Storage Path (relative to Storage Data Folder; empty disables)
and Hotwords Score (default 1.5).
Unreal (C++)
FTryllVoiceInputConfig Config;
Config.ModelName = TEXT("Parakeet TDT 0.6B v3 (int8)");
Config.InputFormat.SampleRate = 16000;
Config.HotwordsStoragePath = TEXT("data/voice-hotwords.txt");
Config.HotwordsScore = 1.8f;
Subsystem->CreateVoiceInput(Config,
[](TSharedPtr<FTryllVoiceInput> Voice, FTryllError Err)
{
if (!Err.IsOk()) { /* handle error */ return; }
// Voice is ready — call BeginUtterance to start capturing.
// BeginUtterance options include auto_finish_on_silence and
// max_utterance_ms.
});
The referenced file must exist under the process storage root; a
missing file fails CreateVoiceInput with StorageFileNotFound.
Picking a score¶
The score is the bonus added to a matched phrase during beam search
(default 1.5). No score value turns biasing off: an empty
hotwordsStoragePath is what disables it.
| Score | Bias | Use when… |
|---|---|---|
| 1.0 | light | a smaller bonus than the default; still biases |
| 1.5 | gentle (default) | the lexicon is broad and you do not want false positives |
| 1.8 – 2.5 | moderate | proper nouns + spell names — typical game lexicon |
| 3.0 – 4.0 | aggressive | the recognizer keeps picking a phonetically-similar real word and you accept some false positives |
The score applies to every phrase in the list — unless a line sets its own.
Append :<number> to a lexicon line to bias that phrase alone:
Here Kaelthorne is biased at 3.0 and the rest at whatever the session's
hotwordsScore is. Use it for the one name a recognizer keeps mangling,
instead of raising the score for the whole list and paying for it everywhere.
The score you set is one of two¶
The server applies hotword bias in two places, and only one of them is yours to set per session:
| What it does | Where it is set | |
|---|---|---|
| Candidate boost | makes a hotword's tokens reachable by the recognizer at all | the server's model catalog (stt_hotwords_score), per model |
| Phrase weight | weights matched phrases against one another | hotwordsScore on the VoiceInput — the one in this guide |
The catalog value is what lets the recognizer consider Kaelthorne in the first
place; hotwordsScore then decides how strongly Kaelthorne competes with the
other phrases in your lexicon. You normally only touch the second.
Turning biasing on for a model¶
Biasing needs beam-search decoding, which the server chooses when it loads the model — so it is a per-model decision, not something a single voice input can switch on. The shipped catalog enables it for Parakeet TDT 0.6B v3 and no other model.
To change that for your project, use the Hotword biasing control in the Model Manager (beside Memory placement) — it is available on STT models:
| Default | follow the server's setting for this model |
| Off | never bias. Supplying a lexicon then fails with HotwordBiasDisabled (4106) instead of quietly doing nothing |
| On | always bias. Beam-search decoding is then in force for every voice input on this model, costing a few percent of decode time and changing its transcripts — on but unused is not free |
From code, the same choice is a per-model hint on the session: hotword_hints
on create_session in Python; in C++, a TryllClient::ModelPlacementHint with
hotwords set, passed in SessionConfig::placementHints:
using Tryll::Client::TryllClient;
TryllClient::SessionConfig cfg;
cfg.placementHints.push_back({
.modelName = "Parakeet TDT 0.6B v3 (int8)",
.hotwords = ::Tryll::HotwordBias_On,
});
client.CreateSession(cfg);
from tryll_client import HotwordBias
client.create_session(hotword_hints={"Parakeet TDT 0.6B v3 (int8)": HotwordBias.On})
If hotwords seem to do nothing at all on a model — no value of hotwordsScore
changes anything — that model is not enabled for biasing. Check the control above,
and the server log, which says hotwords=unavailable on the model's load line.
Raising the score is not free, and past a point it is actively destructive. Bias nudges the beam toward your phrases everywhere, including in speech that contains none of them, and short utterances have the least context to resist it. Measured on Parakeet TDT 0.6B v3 with a nine-phrase fantasy lexicon, at score 6.0:
| Spoken | Transcribed |
|---|---|
| "Fireball" | "Mord" |
| "Hello, how are you?" | "Mords soul. Mordrian souls souls soul" |
| a four-sentence passage | unaffected |
So tune against two sets, not one: utterances that contain your terms, and utterances that do not. The second set is what tells you when to stop. Start at 1.5, raise until the terms land, then check the second set before keeping the value.
Which models support hotwords¶
Two things have to be true: the decoder must have a beam for the bias to act on, and the model bundle must carry a sub-word vocabulary the server can encode your phrases with.
| Family | Hotwords |
|---|---|
| Zipformer transducer / Parakeet TDT (Transducer, NemoTransducer) | yes |
| Streaming Zipformer (online transducer) | yes |
| Zipformer-CTC, NeMo CTC, Paraformer, Dolphin, WeNet CTC | no — no beam to bias |
| Whisper | no |
| SenseVoice | no |
| Moonshine, Canary, FireRedASR, CohereTranscribe | no |
When you pass a hotwords list to a model that cannot use it, the server logs a warning when the voice input is created and ignores the phrases, rather than accepting the list silently:
[Sherpa-ONNX] Hotword biasing unavailable for STT family 'whisper'
(this decoder has no token-level bias hook); ignoring
9 supplied phrase(s) on this session.
The model's load banner also states it up front:
[Sherpa-ONNX] STT model loaded: … | family=nemo_transducer | … |
decoding=modified_beam_search | hotwords=available
The session is otherwise unaffected. Hotword-capable models load with beam-search decoding, which costs a few percent of decode time.
Caveats¶
- Write plain words. The server hands your phrases to the
recognizer's own tokenizer, so
Aeltharionis what you want in the file — not a pre-tokenized▁Ael th ar ion. Lexicons written in the older▁-piece form still work; the server folds them back to graphemes. There is no longer any reason to runtext2token.pyover a lexicon by hand. - A whole utterance biases better than a fragment. The recognizer decodes one speech segment at a time, and a phrase near a segment boundary has less context to be recognised from. If a term is being missed, check whether the pause structure of the speech is splitting the sentence before blaming the score.
- The score is the default for the whole voice input. A lexicon
line that ends with
:<number>overrides it for that phrase only. - The lexicon is loaded from the process storage root. The server reads the file on demand and caches it. Ship the file with your game data and every session on that server can use it.
- Out-of-vocabulary characters are dropped. If your lexicon contains characters the model's tokenizer cannot represent (e.g. CJK characters against a pure-English Zipformer), they are silently dropped from the bias.