Stand up NVIDIA Nemotron 3.5 ASR (streaming, CPU ONNX) as the shared speech-to-text backend for every Hermes agent on this machine, replacing the broken faster-whisper/libcublas path. Use when voice messages arrive untranscribed or when asked to enable local speech recognition for the agent fleet.
Resources
1Install
npx skillscat add joecastelino/jay-skill-pack/local-nemotron-stt-fleet Install via the SkillsCat registry.
Local Nemotron 3.5 ASR for the whole agent fleet
Make every Hermes agent transcribe voice messages locally on CPU. The box has no GPU,
and the default stt.provider: local (faster-whisper) dies on a missing libcublas.so.12
CUDA library. The fix uses Hermes' built-in local_command STT provider pointed at a
CPU ONNX runner — no edits to the hermes-agent code.
The full working deployment is already on disk at /home/itadmin/nemotron-stt/ (model/, venv/,
bin/ffmpeg, transcribe.py, nemotron-stt.sh) — copy from there. A reference copy of the inference
script also lives at references/transcribe.py in this skill. Literal copy-paste setup commands
(hf download, pip, systemd drop-ins, config edits) are recoverable from the 2026-06-18 session
transcript via session_search if needed.
Hard-won lessons (read first — these cost the most time)
- Model id: the real model is
nvidia/nemotron-3.5-asr-streaming-0.6b(note-asr-).
Its official repo is a.nemoneeding NeMo+CUDA — wrong for CPU. Use the community CPU buildonnx-community/nemotron-3.5-asr-streaming-0.6b-onnx-int4(encoder/decoder/joint.onnx+
silero_vad + tokenizer; ~757 MB on disk despite the "int4" label). - Shared-path trap: Jay's
~resolves to.../profiles/jay/home, but each agent runs with a
different HOME. Install fleet assets to the REAL path/home/itadmin/nemotron-stt/so all agents
resolve them identically. Never park shared assets under a profile~. - onnxruntime-genai does NOT drive this model even though the model card says so — the
genai_config.jsonis a customnemotron_speechdescriptor it can't execute. Drive the three
ONNX graphs directly with plainonnxruntime(seereferences/transcribe.py). - ffmpeg: not installed, and sudo is password-gated. The Playwright-bundled ffmpeg is
--disable-everything(no audio codecs — useless). Use a johnvansickle static build dropped
at/home/itadmin/nemotron-stt/bin/ffmpeg, and put that dir first on each gateway's PATH —
Hermes pre-converts non-WAV audio (voice msgs are.ogg/Opus) and needs ffmpeg discoverable. - Provider must be explicit: if
stt.provideris absent, Hermes auto-detects and picks the
brokenlocal(faster-whisper present). Setlocal_commandexplicitly in the base config AND
every profile config. - Restart safety: restarting the gateways bounces all agents. Restart the OTHER agents first,
verify healthy, then base + your own last. (A live one-shot session usually survives its own
gateway bounce, but do yourself last to be safe — and finish all other work first.) - Performance: ~2x realtime, ~1.1 GB peak RAM (fits a ~3 GB-free box).
Procedure
- Confirm the real model id via the HF API; pick the
onnx-community ...-onnx-int4build. - Create
/home/itadmin/nemotron-stt/{model,bin}; download the model intomodel/. - Place a static ffmpeg at
bin/ffmpeg(chmod +x). - Make a dedicated venv in
nemotron-stt/; installonnxruntime numpy librosa soundfile. - Drop in
transcribe.py(copy fromreferences/transcribe.py) and a one-line wrappernemotron-stt.shthat execs the venv python + transcribe.py "$@". - Per gateway service, add a systemd drop-in
*.service.d/stt-nemotron.confthat setsHERMES_LOCAL_STT_COMMANDto the wrapper template
(... {input_path} --output_dir {output_dir} --model {model} --language {language}) and a PATH
that prepends/home/itadmin/nemotron-stt/bin. - Set
stt.provider: local_commandin the base config and in every profile config. - Reload the user systemd manager and restart the gateways (others first, base+self last).
- Confirm the env var is live in a running gateway, then run the end-to-end check.
Verify
- All gateways report active.
- From the hermes-agent venv with the env set:
from tools.transcription_tools import transcribe_audio; transcribe_audio('<some .ogg>')
returnssuccess True,provider local_command, correct text, and NO libcublas error. - A real voice message on Slack/Telegram now transcribes instead of arriving blank.
Model I/O (for rebuilding transcribe.py)
Encoder in: audio_signal[1,65,128] (65 log-mel frames x 128 mels), length[1], cache_last_channel
[1,24,56,1024], cache_last_time[1,24,1024,8], cache_last_channel_len[1], lang_id[1] (0=English).
Encoder out: outputs[1,7,1024] + the three cache_*next + encoded_lengths. Streaming: front-pad 9
zero frames, slide a 65-frame window advancing 56 per chunk, carry cache*_next back in.
Decoder in: targets[b,L], h_in/c_in[2,b,640]; out decoder_output[b,640,L] (transpose to [b,L,640]
before the joint), h_out/c_out. Joint(encoder_output[b,T,1024], decoder_output[b,L,640]) ->
logits[b,T,L,13088]. RNNT greedy: blank_id 13087, SOS=blank, update LSTM state only on non-blank
emits, max 10 symbols/step. Detokenize from tokenizer.json unigram vocab; replace U+2581 with space.
Mel: n_fft512 hop160 win400 hann center, 128 mels 0-8000Hz slaney, preemph0.97, log(x+1e-10).