crispasr extends upstream whisper.cpp's argument set with a handful
of backend-dispatch flags. Every historical whisper flag still works —
when you don't pass --backend, whisper is the default.
- Quick reference — most-used flag patterns
- Core flags — model, backend, input, language, GPU backend
- Model resolution flags —
--model-quant,--auto-download,--cache-dir,--hf-repo - TTS-specific flags — voice, instruct, codec, steps, trim
- Model resolution flags —
- Output formats — txt / srt / vtt / json / csv / lrc
- Segmentation & chunking — VAD, fixed chunks
- Word-level timestamps via CTC alignment — incl. standalone
--align-only - Sampling / decoding — temperature, beam, grammar
- Language detection (LID)
- Diarization —
--diarize, pyannote, embedder-based clustering - LLM-backend specific — aligner, max-new-tokens
- Multi-language / translation
- Threading & processors
- Whisper-only flags
- Pitch / F0 estimation (
--pitch) — CREPE pitch track - Piano transcription (
--piano) — note events (onset/offset/pitch/velocity) - Chord recognition (
--chords) — BTC chord timeline (non-commercial weights) - Auto-download (
-m auto) — registry table - Audio formats — WAV / FLAC / MP3 / OGG / Opus / M4A
- Memory footprint — KV quant, mmap, recommended combos
The 10 patterns that cover most usage:
# Auto-download + transcribe
crispasr -m auto --backend parakeet -f audio.wav
# VAD + SRT + sentence-split (best subtitle output)
crispasr -m parakeet.gguf -f long.wav --vad -osrt --split-on-punct
# Word timestamps via CTC aligner (LLM backends)
crispasr --backend voxtral -m auto -f jfk.wav -am canary-ctc-aligner.gguf -osrt -ml 1
# Auto language detection
crispasr -m auto --backend cohere -f audio.wav -l auto
# Translate to English
crispasr -m auto --backend whisper -f de.wav --translate
# Live mic
crispasr --mic -m auto --backend parakeet
# Text LID — auto-routes by GGUF arch, auto-downloads on first use
crispasr-lid -m auto --text "Bonjour le monde" # cstr/cld3-GGUF (default)
crispasr-lid -m auto:glotlid --text "Bonjour le monde" # 2102 ISO 639-3 + script
# Post-ASR text LID
crispasr -m parakeet.gguf -f speech.wav --lid-on-transcript auto
# JSON with word + token detail
crispasr -m auto --backend parakeet -f audio.wav -ojf
# Force GPU pick
crispasr --gpu-backend vulkan -dev 1 -m auto -f audio.wav
# Half-VRAM voxtral4b
CRISPASR_KV_QUANT=q4_0 CRISPASR_GGUF_MMAP=1 crispasr --backend voxtral4b -m auto -f audio.wav
# TTS — synthesize speech from text
crispasr --backend kokoro -m auto --tts "Hello, how are you?" -o output.wav
# S2S — speech-to-speech (audio in → audio out)
crispasr --backend lfm2-audio -m auto -f input.wav --s2s -o reply.wav
# Speech restoration — 16 kHz input, restored 48 kHz output
crispasr -m sidon-v0.1-f16.gguf -f input.wav --s2s --s2s-output restored.wav
# VoxCPM2 AudioVAE upscaling — 16 kHz input, upscaled 48 kHz output
crispasr -m voxcpm2-vae-f32.gguf -f input.wav --s2s --s2s-output upscaled.wav
# List every backend + capabilities
crispasr --list-backends| Flag | Meaning |
|---|---|
-m FNAME, --model FNAME |
Path to a model file, or auto to download a default for the selected backend |
--backend NAME |
Force a specific backend. Default: auto-detected from GGUF metadata + filename heuristics |
-f FNAME, --file FNAME |
Input audio (can repeat; also accepts positional filenames) |
-t N, --threads N |
Thread count (default: min(4, nproc)) |
-l LANG, --language LANG |
ISO-639-1 code (default: en) |
--tts "TEXT" |
Synthesize speech from text (requires CAP_TTS backend). Output via --tts-output |
--tts-output FNAME |
Output path for TTS WAV (default: tts_output.wav) |
--tts-phonemes "IPA" |
Synthesize these phonemes verbatim, skipping the G2P — the seam for telling a G2P bug from a model bug (#316). kokoro and piper only; any other backend exits 2 rather than silently synthesizing --tts instead. See tts.md |
--tts-stream |
Stream s16le mono PCM to stdout per sentence (pipe to a player); logs stay on stderr. See streaming.md |
--s2s |
Speech-to-speech mode: audio in → audio out (requires CAP_S2S backend, e.g. lfm2-audio, mini-omni2, sidon, voxcpm2-vae) |
--s2s-output FNAME |
Output path for S2S WAV |
--voice PATH |
Voice reference for TTS: GGUF voice pack or reference WAV for cloning (--i-have-rights required for WAV cloning) |
--server |
Run as HTTP server with persistent model (see server.md) |
--server-workers N |
Server: N>1 loads N model instances so pure-ASR requests run concurrently (N× memory; env CRISPASR_SERVER_WORKERS overrides). See concurrency.md |
--ws-port N |
Server: real-time WebSocket ASR streaming port (-1 off, 0 = HTTP port + 1) |
--no-warmup |
Server: skip the startup warmup transcribe (workaround for GPU drivers that hang in warmup, #165) |
--list-backends |
Print the capability matrix and exit |
--gpu-backend NAME |
Force GPU backend: cuda, vulkan, metal, or cpu (default: auto) |
--no-gpu / --device N |
Disable GPU entirely, or pin to GPU index N |
--return-logits |
For dense CTC backends, write <audio>.ctc-logits.json containing the frame-major CTC grid (data[t * n_vocab + v]) plus shape metadata and any exposed vocab |
| Flag | Meaning |
|---|---|
-m auto |
Download the registry default for --backend on first use; subsequent runs are instant |
--model-quant Q |
Preferred quant for registry resolution; overrides the default. E.g. --model-quant q8_0 to get Q8_0 instead of the default Q4_K. Also changes any companion model (voice pack, codec). |
--auto-download |
Explicitly allow auto-download of missing registry models. Implied by -m auto and --hf-repo. |
--cache-dir DIR |
Override the auto-download cache directory. Precedence: this flag, $CRISPASR_CACHE_DIR, $CRISPASR_MODELS_DIR, then ~/.cache/crispasr/. |
-hfr REPO, --hf-repo OWNER/REPO[:FILE] |
Fetch model from an arbitrary HuggingFace repo. E.g. --hf-repo cstr/parakeet-tdt-0.6b-v3-GGUF:parakeet-tdt-0.6b-v3-q4_k.gguf. Implies --auto-download. |
-hff FNAME, --hf-file FNAME |
Filename within --hf-repo (alternative to the OWNER/REPO:FILE shorthand) |
--dry-run-resolve |
Print the resolved model path + companion paths and exit — does not load or synthesize |
--dry-run-ignore-cache |
As --dry-run-resolve but pretend the cache is empty (shows what would be downloaded) |
| Flag | Meaning |
|---|---|
--voice PATH |
GGUF voice pack or reference WAV. GGUF packs are used for VibeVoice/Qwen3-TTS/Orpheus style conditioning; WAV enables voice cloning (requires --i-have-rights) |
--voice-dir PATH |
Server: directory of <name>.gguf or <name>.wav voice profiles. Enables /v1/voices listing and name-based voice selection in /v1/audio/speech |
--ref-text "TEXT" |
Reference transcription for the ref audio (qwen3-tts, f5-tts). Auto-transcribed from --voice <wav> if omitted |
--ref-asr BACKEND |
ASR backend to auto-transcribe the ref audio (default: whisper) |
--instruct "TEXT" |
Natural-language voice/style description. For qwen3-tts: VoiceDesign mode (voice description) or CustomVoice mode (style control) |
--make-ref |
Create a TADA voice reference GGUF from --voice <audio.wav> + --ref-text "transcript". Pure C++, no Python. Auto-discovers tada-encoder-f16.gguf + tada-aligner-en.gguf next to the model. Output path via --make-ref-output (default: tada-ref-custom.gguf) |
--make-ref-output PATH |
Output path for --make-ref (default: tada-ref-custom.gguf) |
--make-ref-encoder PATH |
Explicit path to the TADA encoder GGUF (auto-discovered if omitted) |
--make-ref-aligner PATH |
Explicit path to the TADA aligner GGUF (auto-discovered if omitted) |
--codec-model FNAME |
Explicit path to the codec/companion GGUF (e.g. Qwen3-TTS codec encoder). Defaults to sibling / cache / registry auto-discovery |
--codec-quant Q |
Preferred quant for registry companion resolution (codec model) |
--tts-steps N |
DPM-Solver++ diffusion steps (VibeVoice only; default 20, valid range 10–20) |
--tts-trim-silence |
Trim leading silence from TTS output |
--tts-max-input-chars N |
Server: cap on /v1/audio/speech input length in characters (default 4096; 0 = no cap) |
| Flag | Output |
|---|---|
-otxt |
Plain text to <audio>.txt |
-osrt |
SubRip (SRT) to <audio>.srt |
-ovtt |
WebVTT to <audio>.vtt |
-ocsv |
CSV (start, end, text) |
-oj, -ojf |
JSON (compact or full with word/token arrays) |
-olrc |
LRC lyrics format |
-of FNAME |
Output file base (no extension) |
-np |
No prints (suppress stderr progress) |
-pc |
Color-code output by token confidence (where supported) |
--no-timestamps |
Plain text only, no timing |
-ml N |
Max chars per display segment. 0=unlimited, 1=per-word, N=split at word boundaries |
-sp, --split-on-punct |
Split subtitle lines at sentence-ending punctuation (. ! ?). Creates readable subtitles even for CTC models that produce long segments |
CrispASR writes outputs side-by-side with the input audio (e.g.
jfk.wav → jfk.srt, jfk.vtt, jfk.json):
{
"crispasr": {
"backend": "parakeet",
"model": "parakeet-tdt-0.6b-v3-q4_k.gguf",
"language":"en"
},
"transcription": [
{
"timestamps": { "from": "00:00:00,240", "to": "00:00:10,880" },
"offsets": { "from": 240, "to": 10880 },
"text": "And so, my fellow Americans, ask not what your country can do for you, ask what you can do for your country."
}
]
}Add -ojf (--output-json-full) to include per-word words[] and
per-token tokens[] arrays when the backend populates them:
"words": [
{ "text": "And", "t0": 24, "t1": 52, "offsets": { "from": 240, "to": 520 } }
]Time units. Segment offsets (and each word/token offsets) are in
milliseconds. The per-word / per-token t0/t1 fields are in
centiseconds — this is the library's internal timebase (see the C ABI's
t0_cs/t1_cs) and is retained for backwards compatibility. Prefer the
offsets object for a unit that is consistent across every level of the
document (issue #228).
| Flag | Meaning |
|---|---|
--vad |
Enable Silero VAD. Auto-downloads ggml-silero-v6.2.0.bin (~885 KB) to ~/.cache/crispasr/ on first use |
--vad-model FNAME |
Override the VAD model path (default: auto) |
-vt F |
VAD threshold (default 0.5) |
-vspd N |
VAD min speech duration (ms, default 250) |
-vsd N |
VAD min silence duration (ms, default 100) |
--vad-export FILE |
Compute VAD boundaries and write them to FILE as JSON, then exit. Implies --vad. No ASR model needed — standalone verb |
--vad-import FILE |
Read segment boundaries from FILE instead of running VAD — reuse boundaries across backends without recomputing VAD (issue #227). Implies --vad |
--vad-import-strict |
With --vad-import, refuse (rather than warn) if the file's chunk length differs from this run |
--vad-export-raw FILE |
Like --vad-export, but writes raw VAD speech segments (chunk-length-independent). Imports at any --chunk-seconds, re-chunked per run. Implies --vad |
-ck N, --chunk-seconds N |
Fallback chunk size when VAD is off (default: 30 s for whisper, disabled for other backends) |
--chunk-overlap F |
Overlap context (seconds) at chunk boundaries (default 3.0) |
--lcs-dedup auto|on|off |
NeMo-style sub-word LCS dedup across chunk boundaries (default auto — fires when chunking with overlap) |
--lcs-min-length N |
Minimum LCS length to act on (default 1; raise to 3-4 on long-silence audio where blank tokens dominate boundaries) |
--parakeet-decoder ctc|tdt|maes |
Select decode strategy: ctc (CTC head), tdt (TDT greedy/beam, default), maes (MAES beam search — requires -bs N with N>1) |
-bs N, --beam-size N |
Parakeet TDT/RNNT beam search width (default 1 = greedy). 2–4 recommended with hotwords or MAES. CTC decode is frame-synchronous and always greedy |
--vad-export is a standalone verb: it runs Silero VAD on the audio,
writes the boundaries, and exits — no ASR model download or load needed.
To compare several backends on the same audio without recomputing VAD:
# Step 1: export VAD boundaries (no model needed, ~300 ms).
crispasr -f talk.wav --vad-export talk.vad.json
# Step 2: transcribe with multiple backends using the same boundaries.
crispasr -m parakeet.gguf -f talk.wav --vad-import talk.vad.json
crispasr -m moonshine.gguf -f talk.wav --vad-import talk.vad.jsonBoth --vad-export and --vad-import imply --vad, so the separate
--vad flag is not required. The JSON records each segment's sample
offsets plus absolute centisecond timestamps; boundaries are clamped to
the imported audio and rescaled if the sample rate differs.
Two export forms. --vad-export writes chunk boundaries (a "kind": "chunks" file): the VAD segments already split to the run's --chunk-seconds.
These are only valid for that chunk length — importing them under a different
--chunk-seconds warns (and, with --vad-import-strict, refuses), because the
chunking would not match.
--vad-export-raw writes the raw VAD speech segments ("kind": "vad_segments"), before any chunking. One raw file imports cleanly at any
--chunk-seconds — it is re-chunked for each run exactly as a fresh VAD pass
would be — so it is the form to keep when the same audio will be transcribed by
backends that want different chunk sizes:
crispasr -f talk.wav --vad-export-raw talk.vad.json # once
crispasr -m parakeet.gguf -f talk.wav --vad-import talk.vad.json --chunk-seconds 12
crispasr -m whisper.gguf -f talk.wav --vad-import talk.vad.json --chunk-seconds 30Files written before "kind" existed are read as chunks (the historical
behaviour), so older exports keep working.
By default CrispASR degrades gracefully: a VAD, forced-aligner, or
punctuation model that fails to load is skipped with a stderr warning and the
command still exits 0. That is convenient interactively but hides failures
from automation — a zero exit and a valid -ojf do not prove the requested
stages ran. For integrations that treat those stages as required task
properties, opt into strict semantics:
| Flag | Effect |
|---|---|
--strict-pipeline |
Require every stage explicitly requested on this command line: VAD if --vad/-vm, word timestamps if -am/--force-aligner, punctuation if --punc-model. |
--require-vad |
Force the VAD requirement (needs --vad/-vm). |
--require-word-timestamps |
Every non-empty output segment must carry word timestamps (native or aligned). |
--require-punctuation |
Force the punctuation-model requirement (needs --punc-model). |
Under strict semantics a required stage that fails to load or produce its
output returns a non-zero exit (and the output file is not written), while
a stage that ran and legitimately found nothing (VAD detected no speech)
stays a success. Nothing depends on parsing stderr, and a direct local -m/
-vm/-am/--punc-model path is used as-is — strict mode never falls back to
auto-download.
Distinct exit codes let a caller tell which stage failed:
| Exit | Meaning |
|---|---|
2 |
Config error — a --require-* whose stage was never requested |
30 |
Required VAD model failed to load |
31 |
Required word timestamps missing (aligner failed to load, or no native/aligned words on a non-empty segment) |
32 |
Required punctuation model failed to load |
# The integration's contract: rc 0 ⟺ every required stage succeeded.
crispasr --backend parakeet -m asr.gguf -f in.wav -l zh \
--vad -vm vad.gguf -am aligner.gguf --force-aligner \
--punc-model punc.gguf --strict-pipeline -ojf -of result
echo "rc=$?" # 0 = VAD ran + words present + punctuation applied; 30/31/32 = that stage failedStrict requirements route through the unified backend dispatch, so pass an
explicit --backend (any backend, including --backend whisper) rather than
relying on the legacy whisper-only path.
Across surfaces. The CLI and the HTTP server both auto-run these stages
and both honour strict semantics (the server via strict_pipeline /
require_* form fields, returning HTTP 400 — see
server.md).
The decision logic is shared (crispasr_strict.h) so they can't drift. The
session C-ABI (crispasr_session_*, used by the language bindings) is a
lower-level, caller-driven primitive — it does not auto-orchestrate VAD +
alignment + punctuation, so there is no silent degradation to guard: a binding
caller invokes each stage explicitly (crispasr_session_transcribe_vad, the
aligner, the punc setter) and already sees each one's result directly. Strict
mode is therefore a property of the two orchestrating front-ends (CLI, server).
Restrict processing to a slice of the input instead of the whole file:
crispasr -m parakeet.gguf -f talk.wav --offset-t 60000 # skip the first 60 s
crispasr -m parakeet.gguf -f talk.wav --offset-t 60000 --duration 30000 # only 60 s → 90 s| Flag | Meaning |
|---|---|
-ot MS, --offset-t MS |
Start transcription MS milliseconds into the audio |
-d MS, --duration MS |
Process only MS milliseconds from the offset (0 = to end) |
The window is applied to the decoded audio before VAD/chunking, and reported
timestamps (segment, word, token) are shifted back so they stay in
original-audio time. These flags previously affected only the whisper
backend's internal seek; they now work for every backend. An offset past the
end of the file is reported and exits cleanly.
MAES (Modified Adaptive Expansion Search) is a transducer-specific beam search that processes one encoder frame at a time with adaptive expansion. More efficient than the label-looping beam for transducers.
# Via --parakeet-decoder maes:
crispasr -m parakeet-tdt-0.6b-v3.gguf -f audio.wav --parakeet-decoder maes --beam-size 4
# Via env var:
CRISPASR_PARAKEET_MAES=1 crispasr -m model.gguf -f audio.wav --beam-size 4Tuning env vars:
| Variable | Default | Description |
|---|---|---|
CRISPASR_PARAKEET_MAES |
0 | Set to 1 to enable MAES (alternative to --parakeet-decoder maes) |
CRISPASR_MAES_NUM_STEPS |
2 | Max non-blank expansions per frame |
CRISPASR_MAES_GAMMA |
2.3 | Pruning threshold (lower = more aggressive) |
CRISPASR_MAES_BETA |
2 | Extra candidates beyond beam_size |
Works with both TDT and RNNT parakeet models, and nemotron (via
CRISPASR_NEMOTRON_MAES=1). CTC beam search is separate (activated by
--beam-size N with CTC models, uses shared
core_ctc::prefix_beam_search).
Nemotron MAES (§167b): same algorithm ported to the nemotron RNNT
decoder. On JFK 11s, MAES removes spurious <en-US> language tags and
produces proper punctuation vs standard beam.
CRISPASR_NEMOTRON_MAES=1 crispasr -m nemotron-3.5-asr-streaming-0.6b-q4_k.gguf \
--backend nemotron --beam-size 4 -f audio.wavParakeet's long-audio path is model-dependent — the JA-only model and the multilingual / v3 / EN models behave very differently:
-
Non-JA (v3 / multilingual / EN, vocab > 4096): a single full-attention pass is byte-for-byte identical to upstream NeMo (verified 100 % word match vs
nvidia/parakeet-tdt-0.6b-v3, 30 s → 5 min). The backend advertises internal chunking so the dispatcher hands it the whole clip, then:- ≤ cap (default 300 s): one full-attention pass — NeMo-exact.
- > cap: split at silence into ≤cap single-pass pieces, each transcribed with ±2 s acoustic context and committed by word timestamp at a single shared cut — no overlap, no gap, no boundary duplicates, memory bounded by the cap. (Full attention is O(T²): ~5 min is safe on 16 GB, ~28 min OOMs, hence the cap + silence-split.)
This replaced the old forced
streamedpath, which collapsed on v3 (5 min → 75 words), and the dispatcher chunk-30 + LCS-merge fallback, which duplicated a phrase at every 30 s boundary (token-id-exact LCS can't cancel the divergent re-transcription of the overlap). -
JA-only (vocab ≤ 4096): the bidirectional encoder is numerically unstable when attention spans the whole utterance — codec-level perturbations as small as 0.3 % RMS flipped the encoder output std by ~14 % on the #89 clip, driving the TDT decoder into emit-blank-forever past ~20 s. (Upstream NeMo fails the same way on the same audio — plain, local-attention, and buffered TDT inference all score 1–51 % content recall vs a whisper-large-v3 reference.) JA therefore uses auto-VAD + a 12 s slice cap + single-pass per slice: VAD finds speech, slices longer than 12 s (continuous speech merges far past the encoder's ~12 s safe window) are re-split at energy minima, and each slice gets one NeMo-exact full-attention pass. A gap-fill second pass then re-transcribes any span ≥1 s the first pass left empty inside a slice (the encoder sometimes blanks an utterance whenever enough context follows it, even though the same span transcribes verbatim in isolation) and merges the recovered words back. Measured on the issue #89 reporter's clips (phonetic char-bigram recall vs whisper-large-v3-turbo): 60 s 97.2 %, 120 s 96.9 %, 300 s 95.9 % — up from 61–64 % with the previous streamed default; NeMo's best long-form mode scores ~51 % raw recall on the same audio, and an independent SenseVoice-small run tops out at the same ~97 % (the inter-model agreement ceiling — the rest is kana/kanji hearing variants, not missing content).
Env vars for tuning (all override the per-model defaults):
| Variable | Default | Meaning |
|---|---|---|
CRISPASR_PARAKEET_STREAM_THRESHOLD |
non-JA 300, JA 12 | Single-pass cap (seconds). Audio ≤ this gets one full-attention pass; 0 disables single-pass entirely (always streamed). |
CRISPASR_PARAKEET_VAD_SLICE_CAP |
non-JA 0, JA 12 | Max VAD slice duration (seconds); longer slices are re-split at energy minima before decoding. 0 = no cap. |
CRISPASR_PARAKEET_ATT_CONTEXT |
unset | "L,R" switches the encoder to rel_pos_local_attn with that window (encoder frames, 1 = 80 ms) — NeMo's change_attention_model equivalent. "-1,-1" forces full attention. |
CRISPASR_GAP_FILL |
1 (bounded-window backends only) | Second pass re-transcribing spans ≥1 s the first pass left empty inside a slice; recovered words are merged back. 0 disables. |
CRISPASR_GAP_FILL_MIN_CS |
100 | Gap-fill trigger threshold in centiseconds (100 = 1.0 s of missing speech). |
CRISPASR_PARAKEET_LONGFORM |
non-JA 1, JA 0 | 1 = silence-split single-pass above the cap; 0 = streamed fallback above the cap. |
CRISPASR_PARAKEET_INTERNAL_CHUNKING |
non-JA on, JA off | 0 = revert to the dispatcher's chunk-30 + overlap-save + LCS-merge path (A/B). |
CRISPASR_PARAKEET_STREAM_CHUNK |
0 (auto: 8 JA / 30 non-JA) | Streamed-path encoder chunk size (seconds). |
CRISPASR_PARAKEET_STREAM_OVERLAP |
2 | Streamed-path encoder overlap (seconds). |
CRISPASR_PARAKEET_VRAM_BUDGET_MB |
0 (off) | Proactive memory policy: if single-pass full attention's estimated O(T²) rel-pos bias exceeds this, use the streamed (bounded-window) encoder before allocating — avoids the OOM spike on small GPUs. 0 = disabled (single-pass as before; the reactive OOM fallback still backstops). |
CRISPASR_PARAKEET_MEM_POLICY |
auto |
auto honours the VRAM budget; single/streamed force that path; off disables the proactive check (reactive-only). |
CRISPASR_PARAKEET_MEM_COEFF |
8.0 | O(T²) estimate coefficient. Default calibrated so a ~4 min clip (T≈2800, 8 heads) estimates ~1.9 GiB, matching a measured CUDA allocation. |
CRISPASR_SESSION_UNIFIED_DISPATCH |
0 | 1 routes the session/bindings parakeet path through the shared CLI orchestration (improvements Phase 1) instead of the legacy inline copy. Default off until parity-flipped. |
CLI escape hatches (no env needed): --chunk-seconds N forces the dispatcher's
N-second chunk + merge; --vad forces the VAD path.
For parakeet (non-JA, CAP_INTERNAL_CHUNKING), --chunk-seconds N does not
go through the dispatcher's per-slice merge (which corrupts this full-attention
FastConformer). Instead it runs one coherent internal-streamed decode — encoded
at the model's quality window (30 s, bounded VRAM), so the full transcript is
preserved — and then groups the resulting words into ~N-second output
segments with per-segment offsets/words/tokens (issue #257). So
--chunk-seconds 7 yields ~7-second segments of the complete transcript, not a
truncated single blob. The encoder window can be overridden independently with
CRISPASR_PARAKEET_STREAM_CHUNK. (For bounded-VRAM single-pass long audio with
no segmentation, use --att-context L,R instead.)
Examples:
# Non-JA (v3): default is NeMo-exact single-pass / silence-split longform —
# no flags needed, any length:
crispasr -m parakeet-tdt-0.6b-v3.gguf -f long_de.wav -osrt
# JA model — auto-VAD + 12 s slice cap + per-slice single-pass by default
# (whole-clip single-pass collapses past ~12 s), no flags needed:
crispasr -m parakeet-tdt-0.6b-ja.gguf -f podcast_ja.wav -osrt
# Force the old dispatcher chunk+merge path for comparison:
CRISPASR_PARAKEET_INTERNAL_CHUNKING=0 \
crispasr -m parakeet-tdt-0.6b-v3.gguf -f long_de.wav -osrt
# Lower the single-pass cap on a memory-constrained box (splits sooner):
CRISPASR_PARAKEET_STREAM_THRESHOLD=120 \
crispasr -m parakeet-tdt-0.6b-v3.gguf -f long_de.wav -osrtEvery non-whisper backend uses the Silero VAD model to segment long
audio into speech regions, stitches them into a single contiguous
buffer (with 0.1 s silence gaps), transcribes in one pass, and
remaps timestamps back to original-audio positions. This preserves
cross-segment context and avoids boundary artifacts. Short VAD
segments (< 3 s) are auto-merged, and oversized segments are split at
--chunk-seconds boundaries. Whisper handles VAD internally via
wparams.vad.
# Just pass --vad — the model is auto-downloaded on first use
./build/bin/crispasr --backend parakeet -m parakeet.gguf -f long_audio.wav \
--vad -osrt
# Or point at an existing GGUF
./build/bin/crispasr --backend parakeet -m parakeet.gguf -f long_audio.wav \
--vad-model ~/models/ggml-silero-v6.2.0.bin -osrtThe cached model lives at ~/.cache/crispasr/ggml-silero-v6.2.0.bin
(~885 KB). If you don't pass --vad, whisper falls back to fixed
30-second chunking (-ck 30). Backends with CAP_UNBOUNDED_INPUT
(parakeet, canary, wav2vec2, firered-asr, fastconformer-ctc,
granite-nar) process the full audio in one encoder pass by default
because their non-autoregressive encoders lose context at fixed chunk
boundaries (#89). LLM-based backends (cohere, moonshine, voxtral,
granite, qwen3, etc.) still chunk at 30 s to avoid OOM from growing
KV caches. Pass --chunk-seconds N explicitly to force or override
chunking for any backend.
crispasr --backend parakeet -m parakeet.gguf -f long_audio.wav \
--vad -osrt --split-on-punct- Best timing quality: parakeet. Native TDT timestamps are more accurate and natural than the forced-aligner fallback used by LLM backends.
- Best default subtitle flags:
--vad --split-on-punct. VAD segments at natural speech pauses, then CrispASR stitches and remaps timestamps back to the original timeline. Avoids the mid-sentence boundary problems of fixed 30-second chunking. - For backends without native timestamps (
cohere,granite,voxtral,voxtral4b,qwen3): use a CTC aligner together with--vad. Without VAD, leading silence can throw off sentence starts, especially for the qwen3 forced aligner. - Any length (non-JA v3 / multilingual / EN): a single full-attention
pass is NeMo-exact and the default; clips over the 300 s cap are
silence-split into single-pass pieces with no boundary duplicates. No
manual
--chunk-secondsneeded. JA-only models use the streamed path instead. See the "Parakeet long-form encoding" section above. - If parakeet OOMs on very long audio: lower the single-pass cap with
CRISPASR_PARAKEET_STREAM_THRESHOLD=120so the silence-split longform kicks in sooner (each piece is then ≤120 s of full attention). - Hybrid TDT+CTC models (e.g.
parakeet-tdt_ctc-0.6b-ja): pass--parakeet-decoder ctcto use the CTC head. CTC decode is frame-synchronous and avoids TDT emission-frame-shift artifacts at chunk boundaries.
The LLM-based backends (qwen3, voxtral, voxtral4b, granite)
don't emit timestamps natively. CrispASR supports a second-pass
forced alignment via NVIDIA's canary-ctc-aligner — a 600 M-param
FastConformer + CTC head that works on any transcript + audio pair
in 25+ European languages.
# Auto-download the aligner — Q4_K (~442 MB) lives in the registry.
./build/bin/crispasr --backend voxtral -m auto -f samples/jfk.wav \
-am auto -osrt -ml 1
# [00:00:00.240 --> 00:00:00.640] And
# [00:00:00.640 --> 00:00:00.880] so,
# [00:00:00.880 --> 00:00:01.040] my
# …or grab it once manually (Q4_K / Q5_0 / Q8_0 / F16 all on the same repo):
curl -L -o canary-ctc-aligner.gguf \
https://huggingface.co/cstr/canary-ctc-aligner-GGUF/resolve/main/canary-ctc-aligner-q4_k.gguf
./build/bin/crispasr --backend voxtral -m auto -f samples/jfk.wav \
-am canary-ctc-aligner.gguf -osrt -ml 1Alignment granularity is one encoder frame (~80 ms).
For non-English audio, use a language-matched wav2vec2 CTC aligner instead of the multilingual canary-ctc model. These are the same models WhisperX uses for word alignment, converted to GGUF and available via auto-download:
| Alias | Language | Source model |
|---|---|---|
-am wav2vec2-aligner |
English (default) | wav2vec2-xlsr-en |
-am wav2vec2-aligner-de |
German | jonatasgrosman/xlsr-53-german |
-am wav2vec2-aligner-fr |
French | jonatasgrosman/xlsr-53-french |
-am wav2vec2-aligner-es |
Spanish | jonatasgrosman/xlsr-53-spanish |
-am wav2vec2-aligner-it |
Italian | jonatasgrosman/xlsr-53-italian |
-am wav2vec2-aligner-ja |
Japanese | jonatasgrosman/xlsr-53-japanese |
-am wav2vec2-aligner-zh |
Chinese | jonatasgrosman/xlsr-53-chinese-zh-cn |
-am wav2vec2-aligner-nl |
Dutch | jonatasgrosman/xlsr-53-dutch |
-am wav2vec2-aligner-pt |
Portuguese | jonatasgrosman/xlsr-53-portuguese |
-am wav2vec2-aligner-ar |
Arabic | jonatasgrosman/xlsr-53-arabic |
-am wav2vec2-aligner-uk |
Ukrainian | Yehor/xls-r-300m-uk-with-small-lm |
-am wav2vec2-aligner-cs |
Czech | comodoro/xls-r-300m-cs-250 |
# Japanese word timestamps with the JA-specific aligner:
crispasr --backend cohere -m cohere.gguf -f japanese.wav \
-am wav2vec2-aligner-ja --auto-download -osrt -ml 1
# French with Voxtral:
crispasr --backend voxtral -m auto -f french.wav \
-am wav2vec2-aligner-fr --auto-download -osrt -ml 1All models are Q4_K-quantized (~200 MB each) and auto-download on first
use. The canary-ctc aligner remains the default (-am auto) because
it covers 25+ languages in one model.
A second aligner family (NVIDIA FastConformer-Hybrid-CTC, CC-BY-4.0) covers languages the wav2vec2 set doesn't — Slavic, Caucasian, Central-Asian. Same usage, all auto-download:
-am fastconformer-aligner-<code> where <code> is one of: en /
en-pc (English, +punctuation/caps) · de · es · fr · it · nl ·
pl (Polish) · ru (Russian) · ua (Ukrainian) · hr (Croatian) ·
be (Belarusian) · ar (Arabic) · fa (Persian) · ka (Georgian) ·
hy (Armenian) · uz (Uzbek) · kk-ru (Kazakh+Russian). Bare
-am fastconformer-aligner = English.
# Polish word timestamps (no wav2vec2 PL aligner exists):
crispasr --backend voxtral -m auto -f polish.wav \
-am fastconformer-aligner-pl --auto-download -osrt -ml 1For LLM backends with a native timestamp head there is also
-am qwen3-forced-aligner (multilingual, ~500 MB) — note it is more
sensitive to leading silence than the CTC aligners.
For subtitle output, prefer adding --vad --split-on-punct:
./build/bin/crispasr --backend cohere -m cohere.gguf -f talk.wav \
-am canary-ctc-aligner.gguf --vad -osrt --split-on-punctBy default the CTC aligner is a fallback — it only runs when the
backend doesn't already produce word-level timestamps natively.
--force-aligner flips that: even when the backend has native
timing (whisper, parakeet, canary, cohere, kyutai-stt), the aligner
runs and replaces the native words with CTC-derived ones. Useful
when the user trusts the aligner's word-onset accuracy more than
the backend's native timing.
# Parakeet's native word ts replaced with canary-ctc-aligner output:
./build/bin/crispasr --backend parakeet -m auto -f samples/jfk.wav \
-am auto --force-aligner -ojfThe flag also lifts the CAP_TIMESTAMPS_CTC capability gate, so
backends that don't formally advertise CTC alignment compatibility
(whisper, parakeet, kyutai-stt — they only declare native timing)
can still use it once the user explicitly asks. Short alias is
-falign, NOT -fa (already taken by --flash-attn).
Notes:
- Without
--force-aligner, the aligner path is a fallback for backends that lack native timestamps. qwen3-forced-aligneris more sensitive to leading silence;--vadis strongly recommended with it.- Parakeet remains the better choice when timestamp quality is the top priority and you don't want to pay for a second forward pass.
Canary's native word timing is cross-attention DTW on the encoder–
decoder, which has a measured ~414 ms MAE on word boundaries
(src/canary.cpp:1377-1390). The official NVIDIA companion model
canary-ctc-aligner (separate 600 M FastConformer + CTC head,
shipped inside the same .nemo tarball as the main canary weights;
hf_readmes/canary-ctc-aligner-GGUF.md) gets ~78 ms MAE — 5.3×
tighter.
Because the gap is so large and the aligner is curated/registered,
--backend canary now defaults to -am auto --force-aligner whenever
the requested output benefits from word-level timestamps (-osrt,
-ovtt, -ojf, -owts, --max-len > 0, --split-on-punct,
--print-colors). This auto-downloads canary-ctc-aligner-q4_k.gguf
(~442 MB) into the crispasr cache the first time and reuses it
afterwards. Stream / mic / server / --text / --tts modes are
exempt.
# v0.7.0+: equivalent to passing `-am auto --force-aligner` automatically
./build/bin/crispasr --backend canary -m auto -f samples/jfk.wav \
--max-len 50 --split-on-punct -osrtTo opt out (e.g., to keep the old DTW path because you don't want
the ~442 MB download or the second forward pass), add
--no-auto-aligner:
./build/bin/crispasr --backend canary -m auto -f samples/jfk.wav \
--max-len 50 --split-on-punct -osrt --no-auto-alignerThe implicit-enable line goes to stderr (suppressed under
--no-prints) so it doesn't perturb stdout subtitle parsing in
upstream tools like SubtitleEdit.
Aligns pre-existing text against audio without running ASR first.
Accepts plain text (via --ref-text or --text-file file.txt) or an
unaligned .srt file. Works with all three aligner families:
canary-ctc, wav2vec2/hubert, qwen3-forced-aligner.
For .srt input the cue structure is preserved: the cue texts are
aligned as one transcript and each cue is re-emitted with corrected
timings (first/last aligned word of that cue). So a mistimed subtitle
file goes in, and the same subtitles come out re-timed.
# Re-time an unaligned/mistimed SRT (same cues, corrected timestamps):
crispasr --align-only -am auto --auto-download -f audio.wav \
--text-file subtitles.srt --align-output retimed.srt
# Align a transcript against audio, output word-level SRT:
crispasr --align-only -am auto --auto-download -f audio.wav \
--ref-text "And so my fellow Americans ask not what your country can do for you"
# Align a plain .txt (one subtitle line per text line) into cue-level SRT:
crispasr --align-only -am auto --auto-download -f audio.wav \
--text-file transcript.txt --align-granularity segment \
--align-output aligned.srt
# JSON with per-segment + nested per-word timings:
crispasr --align-only -am canary-ctc-aligner-q4_k.gguf -f audio.wav \
--text-file subtitles.srt --align-format jsonNo ASR model (-m) is required. Output formats: srt (default),
json (start/end in seconds), plain (tab-separated). Destination:
stdout (default) or --align-output <path>.
Granularity is controlled by --align-granularity:
| Value | Meaning |
|---|---|
auto (default) |
segment for .srt input, word otherwise |
segment |
one output entry per input SRT cue / non-empty .txt line, re-timed from the word alignment; JSON nests the per-word timings under each segment |
word |
one output entry per aligned word (the pre-0.8.9 behaviour, also for .srt input) |
Any of these resolves via -am <name> --auto-download. All are
permissively licensed (no NC restriction):
-am name |
Family | Size (q4_k) | Languages | Upstream license |
|---|---|---|---|---|
canary-ctc-aligner (= auto) |
FastConformer-CTC (canary-1b-v2 aux) | ~442 MB | 25 European | CC-BY-4.0 |
fastconformer-aligner[-en] / fastconformer-ctc |
FastConformer-CTC standalone | ~83 MB | en | CC-BY-4.0 |
fastconformer-{aligner,ctc}-{en-pc,es,fr,it,nl,pl,ru,ua,hr,be,ar,fa,ka,hy,uz,kk-ru,de} |
FastConformer hybrid CTC branch | ~82 MB | per-language (+punct/caps except fa, kk-ru) | CC-BY-4.0 |
parakeet-ctc-0.6b / parakeet-ctc-1.1b |
FastConformer-CTC | ~455 / ~795 MB | en | CC-BY-4.0 |
wav2vec2-aligner-{en,de,fr,es,it,ja,zh,nl,uk,…} |
wav2vec2/XLSR CTC | ~212–300 MB | per-language (incl. CJK) | Apache-2.0 |
qwen3-forced-aligner / qwen3-fa |
Qwen3 timestamp head | ~500 MB | multilingual | Apache-2.0 |
Everything with GGUF arch canary-ctc (the whole NeMo
stt_*_fastconformer_ctc_* standalone family, plus CTC branches
extracted from the stt_*_fastconformer_hybrid_large_pc hybrids via
models/convert-stt-fastconformer-ctc-to-gguf.py) loads through the
default aligner dispatch — the ~80 MB FastConformer models are the
smallest/fastest aligner option. The popular MMS-based aligners are
deliberately absent (weights CC-BY-NC-4.0), as is the Portuguese
hybrid stt_pt_fastconformer_hybrid_large_pc — the only NC release in
NVIDIA's per-language hybrid fleet.
--backend granite (Granite Speech 4.1 PLUS variant) now requests
word-level timestamps from the model whenever the output format needs
them: --max-len, -osrt, -ovtt, or --split-on-punct. Previously,
word timestamps were only generated for --output-wts and -ojf,
causing --max-len to silently have no effect.
./build/bin/crispasr --backend granite \
-m granite-speech-4.1-2b-plus-q4_k.gguf \
-f audio.wav --max-len 50 -osrtNote: Qwen3 does not support word-level timestamps, so
--max-lensegment splitting is not available with that backend.
| Flag | Meaning |
|---|---|
-tp F, --temperature F |
Sampling temperature. 0 = pure argmax (default, bit-identical). > 0 enables multinomial sampling for whisper, voxtral, voxtral4b, qwen3, granite |
--seed N |
RNG seed for sampling. 0 = non-deterministic. Used by temperature-sampling ASR backends and TTS backends that sample; CLI values override backend-specific env seeds |
-bo N, --best-of N |
Number of best candidates to keep when temperature > 0 (whisper + some AR backends) |
-bs N, --beam-size N |
Beam search width. Default 5 for whisper, 1 (greedy) for other backends. 23 backends: whisper, parakeet, nemotron, canary, canary-qwen, cohere, granite, qwen3, voxtral, voxtral4b, glm-asr, kyutai-stt, moonshine, moonshine-streaming, firered-asr, omniasr, gemma4-e2b, funasr, sensevoice, granite-nle, moss-audio, moss-transcribe, moss-diarize, higgs-stt, ark-asr, mimo-asr, m2m100, madlad/t5. Also lfm2-audio (stub). Not applicable to paraformer (NAR) |
-tpi F, --temperature-inc F |
Whisper temperature-fallback increment |
-nf, --no-fallback |
Disable temperature fallback (equivalent to --temperature-inc 0) |
--frequency-penalty F |
Opt-in repeated generated-token penalty for autoregressive ASR backends (0.0 disabled). Applied to generated output tokens before greedy/sampling selection. |
--grammar FNAME |
GBNF grammar file for constrained whisper decoding |
--grammar-rule NAME |
Top-level rule name in the grammar (default: root) |
--grammar-penalty F |
Scales down logits of tokens that violate the grammar (default: 100.0) |
--alt |
Show alternative token candidates with per-token probabilities (whisper + any backend that emits token alternatives) |
--alt-n N |
Number of alternative token candidates per step (default: 1) |
--print-confidence |
After the transcript, print each segment's tokens with an inline confidence annotation (word[NN%]). Works for any backend that emits per-token confidence (whisper, parakeet, moonshine, …); backends without token info print plain text |
--prompt STR |
Initial prompt for whisper |
Supply domain-specific vocabulary that the ASR should prefer when in doubt — names, jargon, product terms, place names. Works by boosting the log-probability of tokens that continue a matching hotword prefix during CTC/TDT decoding (shallow fusion via an Aho-Corasick trie).
| Flag | Meaning |
|---|---|
--hotwords "A,B,C" |
Comma-separated hotword list |
--hotwords-file FILE |
One hotword per line |
--hotwords-boost F |
Per-token log-prob boost (default: 2.0) |
Per-word boost suffix: "Berenz^5.0,NVIDIA^3.0,plain".
| Mechanism | Backends |
|---|---|
| CTC-WS trie (Phase A) — token-level logit boost during CTC/TDT decode | parakeet (CTC + TDT) |
| LLM prompt injection (Phase B) — hotwords appended to the system/instruction prompt | qwen3-asr, voxtral |
Free-form prompt injection — separate --context flag, not --hotwords (see below) |
vibevoice |
| Not applicable | voxtral4b (fixed streaming prompt), granite-nle (NAR, no text prompt), funasr (hardcoded prompt), whisper (use --prompt instead) |
VibeVoice-ASR uses its own flag, --context "TEXT", rather than
--hotwords — the model's prompt format accepts free-form prose/metadata
(names, organizations, topic context) instead of a structured hotword
list with per-word boost weights. The text is spliced directly into the
model's instruction prompt, matching the context_info parameter in
microsoft/VibeVoice's vibevoice_asr_processor.py. Empty or
whitespace-only --context behaves identically to omitting the flag
(same prompt, byte-for-byte).
crispasr --backend vibevoice -m auto -f meeting.wav \
--context "ACME Corp, John Smith, Q3 earnings"# Boost rare names during parakeet CTC decode
crispasr --backend parakeet -m auto -f meeting.wav \
--parakeet-decoder ctc \
--hotwords "Berenz,Acme Corp,GPU-PB"
# Same hotwords for qwen3-asr (injected into LLM system prompt)
crispasr --backend qwen3 -m auto -f meeting.wav \
--hotwords "Berenz,Acme Corp,GPU-PB"
# Boost from a file with custom boost values
crispasr --backend parakeet -m auto -f meeting.wav \
--hotwords-file names.txt --hotwords-boost 3.0With greedy decode, a boosted token that loses to blank or a common
word at one frame is gone forever. Beam search (-bs 2 or higher)
keeps alternative hypotheses alive, so a boosted rare term can survive
in a lower-ranked beam and win later when more confirming audio
arrives. The combination is especially effective for medical, legal,
or brand-name vocabulary where greedy hotword boosting alone is
insufficient.
# Beam search + hotwords for medical transcription
crispasr --backend parakeet -m auto -f consultation.wav \
--hotwords "metformin,lisinopril,atorvastatin" -bs 4The overhead is negligible: parakeet's TDT beam search only multiplies the cheap predictor+joint steps (~10 KB LSTM state per beam), while the encoder dominates wall time. Measured overhead: ~3 % at beam=2, ~12 % at beam=4 on 60 s audio.
An Aho-Corasick multi-pattern trie is built from the hotword strings by tokenizing each word through the backend's SentencePiece vocab. Before each frame's argmax (or each beam expansion step), the trie checks which tokens continue an active hotword prefix match and adds the boost to their logits. After token selection, the trie state advances based on the emitted token. Each beam hypothesis maintains its own trie position. The overhead is O(1) per frame — no measurable impact on throughput.
CrispASR has two distinct LID paths: audio-LID (decides what language the audio is in, before/during ASR) and text-LID (decides the language of a transcript or arbitrary UTF-8 string).
| Flag | Meaning |
|---|---|
-l auto, --detect-language |
Auto-detect the input language. Backends without native lang-detect (cohere, canary, granite, voxtral, voxtral4b) get it via the LID pre-step |
--lid-backend NAME |
Audio-LID provider: whisper (default), silero (95 langs, 16 MB), ecapa (107 or 45 langs, 40-43 MB), firered (120 langs, 544 MB), or off |
--lid-model FNAME |
Override the audio-LID model path (default: auto-downloads ggml-tiny.bin ~75 MB on first use) |
Runs on a transcript or any UTF-8 string. The dispatcher in
src/text_lid_dispatch.{h,cpp} peeks the GGUF's general.architecture
and picks fastText (GlotLID-V3 / LID-176) or Google CLD3 — same flag,
same binary, any text-LID GGUF.
| Flag | Meaning |
|---|---|
--lid-on-transcript FNAME |
After ASR, run a text-LID GGUF on the assembled transcript and emit lang=<code>\tconf=<score>\tbackend=<name> to stderr. Accepts a path or auto[:cld3|glotlid|lid-fasttext176] (default cld3, auto-downloaded). Errors are logged but never fail the run |
Standalone CLI: crispasr-lid (separate binary, ships with every build):
| Flag | Meaning |
|---|---|
-m, --model PATH|auto[:variant] |
Path or auto-download key. auto / auto:cld3 → cstr/cld3-GGUF (default). auto:glotlid → cstr/glotlid-GGUF. auto:lid-fasttext176 → cstr/fasttext-lid176-GGUF. A bare canonical filename (e.g. cld3-f16.gguf) is also looked up in the registry and downloaded if missing |
--text STR |
Input text (otherwise stdin) |
-k, --topk N |
Top-k predictions, one label\tscore per line (default 1) |
--quiet |
Suppress the trailing backend=… variant=… dim=… N labels summary |
First-use download lands in ~/.cache/crispasr/ (or
$CRISPASR_CACHE_DIR if set). Subsequent runs are instant.
Example — same input, three different label spaces:
$ crispasr-lid -m cld3-f16.gguf --text "Bonjour le monde, comment allez-vous?"
fr 0.999983
crispasr-lid: backend=lid-cld3 variant=Google CLD3 dim=80 109 labels
$ crispasr-lid -m lid-glotlid-f16.gguf --text "Bonjour le monde, comment allez-vous?"
fra_Latn 0.983436
crispasr-lid: backend=lid-fasttext variant=glotlid-v3 dim=256 2102 labels
$ crispasr-lid -m lid-fasttext176-f16.gguf --text "Bonjour le monde, comment allez-vous?"
fr 0.958174
crispasr-lid: backend=lid-fasttext variant=fasttext-lid176 dim=16 176 labelsCLD3 is the smallest+fastest option (440 KB F16, 109 langs, Apache-2.0)
but inherits CLD3's known short-input quirks ("Hello world" lands on
ky 0.72 consistently — too short to disambiguate; the C++ port
faithfully reproduces upstream's pycld3 behaviour). GlotLID-V3 covers
the most languages (2102 ISO 639-3 + script). LID-176 is CC-BY-SA-3.0
(viral) — pick CLD3 or GlotLID for non-SA distribution.
Diarization assigns a speaker label to every transcribed segment. Every method below works with every ASR backend.
Start with foxnose unless you have a reason not to: it is the most
accurate in-tree path, needs no Python and no external binary, and estimates
the number of speakers instead of requiring you to know it.
Measured on 8 VoxConverse dev files against human labels (whisper-tiny
segments, 0.25 s collar, tools/der_score.py):
| Method | Mean DER |
|---|---|
foxnose + WeSpeaker |
7.3 % |
pyannote + TitaNet |
7.8 % |
Scored the way the upstream reference scores itself — on diarization turns
rather than ASR segments — foxnose is at parity with it: 3.18 % against
3.07 %.
# Recommended: foxnose (auto-downloads the 24 MB WeSpeaker GGUF)
crispasr -m auto --backend cohere -f podcast.wav \
--diarize --diarize-method foxnose --diarize-embedder auto -ojf
# Pin the speaker count when you know it (skips estimation entirely)
crispasr -m auto --backend cohere -f podcast.wav \
--diarize --diarize-method foxnose --diarize-embedder auto \
--diarize-num-speakers 2 -ojf
# Native GGUF pyannote (no Python, no sherpa-onnx)
crispasr -m auto --backend cohere -f podcast.wav \
--diarize --diarize-method pyannote --sherpa-segment-model auto -ojf
# Same + embedder-based global speaker IDs (recommended for >2 speakers
# or long-form audio where pyannote local-track IDs drift)
crispasr -m auto --backend cohere -f podcast.wav \
--diarize --diarize-method pyannote --sherpa-segment-model auto \
--diarize-embedder auto -ojf| Method | Stereo / mono | What it does |
|---|---|---|
energy |
stereo | ` |
xcorr |
stereo | TDOA via cross-correlation, ±5 ms search window |
vad-turns |
mono | Alternates 0/1 every >600 ms gap (mono-friendly proxy) |
foxnose |
mono | Recommended. WeSpeaker ResNet34-LM embeddings over sliding windows -> PCA + full-covariance GMM/BIC speaker counting -> Ng-Jordan-Weiss spectral clustering -> Viterbi temporal smoothing. Estimates the speaker count; --diarize-num-speakers N pins it. Needs --diarize-embedder auto (or a WeSpeaker GGUF path). Weights are CC-BY-4.0 — see THIRD_PARTY_NOTICES.txt |
pyannote |
mono | Native GGUF pyannote-seg-3.0; runs once globally over the full audio, splits ASR segments at speaker-turn boundaries when per-word timestamps exist. Auto-downloads the GGUF via --sherpa-segment-model auto |
sherpa / ecapa |
mono | External sherpa-onnx subprocess with segmentation + speaker-embedding model. Since #110, runs once globally over the full audio (not per-slice), producing consistent speaker IDs across the whole file. Splits ASR segments at speaker-turn boundaries when per-word timestamps exist. Requires --sherpa-bin, --sherpa-segment-model, --sherpa-embedding-model |
Bare --diarize (no --diarize-method) defaults to energy for stereo
input and vad-turns for mono — the historical behaviour.
Note on global execution (issue #110). Both
pyannoteandsherpa/ecapanow run once on the full audio before any VAD/ASR slicing. The global speaker-turn timeline is then used to assign speakers to each per-slice ASR segment, ensuring speaker IDs are consistent across the entire file. Before #110,sherpa/ecaparan per-slice, producing local IDs that could reset between slices.
foxnose slides a 1.2 s window at a 0.6 s hop, so every sample goes through the
embedding network twice. Setting CRISPASR_DIARIZE_SPAN_EMBED=1 runs one
network pass per span of 32 windows and takes each window as a slice of it.
Measured on the VoxConverse dev shard: 1.78x less diarization CPU (66.0 s -> 37.0 s on an 85 s file), for +0.30 mean DER (7.32% -> 7.62%). Six of eight files come out identical; one borderline file loses a speaker, because the slightly different embeddings flip the speaker-count estimate from 4 to 3.
Off by default — accuracy is the better default for a diarizer. Turn it on when
you are throughput-bound and can accept that. Span size
(CRISPASR_DIARIZE_SPAN_WINDOWS, default 32) does not affect the accuracy
cost — it is identical from N=2 to N=32 — so there is nothing to tune: larger is
simply faster.
The pyannote method's per-pass local tracks (spk0 / spk1 / spk2) are
not anchored to physical speakers across an entire file. Pass
--diarize-embedder to extract a speaker embedding per segment and
cluster on cosine similarity, producing IDs that are consistent across
the whole audio.
| Alias | Backend | Dim |
|---|---|---|
auto, titanet |
TitaNet-Large | 192 |
indextts, indextts-bigvgan, ecapa |
IndexTTS-BigVGAN ECAPA-TDNN | 512 |
<path>.gguf |
Dispatched by filename (indextts substring -> IndexTTS, otherwise TitaNet) |
— |
The interface is pluggable: add a new adapter by subclassing
CrispasrSpeakerEmbedder in src/crispasr_speaker_embedder.cpp and
extending the factory's dispatch.
--diarize-max-speakers N (default 8) bounds the search. --diarize-num-speakers N
pins the count outright and skips estimation.
--diarize-cluster-thresholdonly applies if you pass it (#326). The default path estimates the speaker count with the same spectral clustererfoxnoseuses, and a cosine merge threshold means nothing to it. It used to be consulted always — single-linkage agglomerative at a fixed 0.5 — and because single linkage chains, the merge loop never reached the threshold and--diarize-max-speakerssilently became the answer rather than a ceiling: on 4 of 8 VoxConverse dev files it returned exactly 8 speakers against 4-7 real ones. Passing the flag explicitly still selects the old agglomerative path for anyone who tuned it; leaving it alone gets the estimator.
This clustering is session-scoped: embeddings are computed per
recording and discarded, labels are anonymous (speaker N), and nothing
is persisted — no voiceprint database, no names. It identifies no one; it
only makes the labels stable within one file.
--diarize-speakers is shorthand for --diarize --diarize-embedder auto.
Use it when you just want stable per-recording speaker labels without
remembering the flag combination:
crispasr -m auto --backend cohere -f meeting.wav --diarize-speakers -ojfNamed profiles are a separate, deliberately opt-in feature. The
--enroll-speaker/--speaker-dbflags build a persistent database of voiceprints linked to real names — biometric special-category data under GDPR Art. 9, with consent/transparency obligations. Matching is a closed-roster confirmation applied per global speaker cluster: it requires--speaker-db-consentAND--expect-speakers "NameA,NameB"(the enrolled participants you assert are present), only runs on recorded files (never streaming), and unmatched clusters keep their anonymous(speaker N)labels. An open "identify anyone in the database" 1:N scan is deliberately unsupported (EU AI Act, Annex III 1(a)). The session-scoped clustering above does not persist anything and needs no consent flag. See diarization-speakers.md for the full comparison and the legal/privacy posture.
Each segment carries the label as the string "(speaker N) " in
crispasr_segment.speaker. Output writers surface it as:
- txt / wts: prefixed inline:
(speaker 0) hello world - srt: prefixed inline
- vtt:
<v Speaker 0>markup - json: per-segment
"speaker": "0"(stripped of the(speaker )wrapper)
When the embedder is enabled the labels are global cluster IDs; otherwise they are pyannote-local track IDs.
What changed in 0.6.6+ (issue #107)
--diarize-method pyannoteis now actually correct end-to-end: pyannote-seg runs ONCE over the full audio (cross-slice consistent IDs), overlap classes contribute to both involved speakers, ASR segments split at speaker-turn boundaries when word timestamps exist.--diarize-method Xnow also works with the whisper backend (previously silently ignored — only the upstream stereo-energy diarize ran).- Output writers prefer the unified
segs[i].speakerover the legacy stereo-only energy estimator. Mono input now gets aspeakerfield in JSON when an explicit method is set.
| Flag | Meaning |
|---|---|
-am FNAME, --aligner-model FNAME |
CTC aligner GGUF for word-level timestamps |
-n N, --max-new-tokens N |
Max tokens the LLM may generate (default 512) |
--frequency-penalty F |
Penalize repeated generated token IDs on supported autoregressive backends. Useful with -n as a retry knob after cap-triggered degeneration. |
There are three distinct translation paths, each with its own flags. Pick by what you have for input and what you need out:
| You have | You want | Use |
|---|---|---|
| Audio in language X | Translated text in English | -tr / --translate (audio→EN-text on whisper, canary, granite, voxtral, qwen3) |
| Audio in language X | Translated text in language Y | -sl X -tl Y (audio AST on canary, granite-4.1, qwen3) |
| Plain text in language X | Translated text in language Y | --text "..." -sl X -tl Y --backend m2m100 (text→text only) |
| Flag | Meaning |
|---|---|
-sl LANG, --source-lang LANG |
Source language (canary AST source; explicit pin overrides LID) |
-tl LANG, --target-lang LANG |
Target language (canary AST; set different from -sl for X→Y translation) |
-tr, --translate |
Translate to English (whisper, canary) — boolean toggle, no string arg |
--no-punctuation |
Disable punctuation in the output. Native for cohere/canary, post-processed for everyone else |
Three text-to-text translation backends, all driven by --text "..." -sl <src> -tl <tgt>:
| Backend | Model | Languages | Status |
|---|---|---|---|
m2m100 |
facebook/m2m100_418M — 12L+12L transformer, ~502 MB Q8_0 |
100, any-to-any | ✓ production-ready (en→de exact match to Python ref) |
m2m100-wmt21 |
facebook/wmt21-dense-24-wide-en-x + facebook/wmt21-dense-24-wide-x-en — 24L+24L wider, ~2.5 GB Q4_K each |
English ↔ 7 languages (separate en-x / x-en checkpoints) |
✓ runs on m2m100 runtime; vocab fix in 7f48bad |
madlad (alias t5) |
google/madlad400-3b-mt — T5 12L+12L, ~1.9 GB Q4_K |
419 | ✓ tokens match Python SP bit-by-bit; outputs match HF reference |
# m2m100 base — production
./build/bin/crispasr --backend m2m100 -m auto \
--text "Hello world, how are you today?" \
-sl en -tl de
# → Hallo Welt, wie bist du heute?
# WMT21 (4.7B, English-to-X, auto-downloads ~2.5 GB)
./build/bin/crispasr --backend m2m100-wmt21 -m auto \
--text "The president said he would not attend." \
-sl en -tl de
# MADLAD-400 (419 languages — output matches Python SP)
./build/bin/crispasr --backend madlad -m auto \
--text "Hello world." \
-sl en -tl taFor MADLAD-400 the source-language tag is informational (T5 encoders
are language-agnostic); the adapter synthesises the <2xx> target-
language prefix from -tl automatically. m2m100 / WMT21 use both
-sl and -tl.
| Flag | Meaning |
|---|---|
--text "TEXT" |
Plain text to translate. Output goes to stdout. |
-sl LANG, -tl LANG |
Source / target — same flags as audio AST; ISO-639-1 codes. |
--translate-max-tokens N |
Max output tokens (default 256). |
--tr-sl LANG, --tr-tl LANG (long: --translate-source-lang / --translate-target-lang) |
Translator-stage source/target. Falls back to -sl/-tl. Only matters in 2-stage pipelines where the primary backend's -sl/-tl mean something else (the primary's AST source/target). 2-stage piping (ASR → m2m100) needs --translate-model PATH — that's a follow-up; the override flags are plumbed but the standalone path is what's exercised today. |
| Flag | Meaning |
|---|---|
-t N, --threads N |
Threads per inference call (default min(4, nproc)) |
-p N, --processors N |
Run N parallel decoder states (whisper only — uses whisper_full_parallel) |
--no-gpu / --device N |
Disable GPU or pin to GPU N |
These work both with the historical default whisper code path AND
with --backend whisper. The historical path retains a few extras
unique to it (-owts karaoke, full-mode JSON DTW tokens) — pass a
ggml-*.bin model without --backend to get them.
| Flag | Meaning |
|---|---|
--diarize |
Generic diarization post-step. Stereo defaults to energy, mono to vad-turns. Pair with --diarize-method for pyannote / sherpa / etc. — see Diarization. |
-tdrz, --tinydiarize |
TinyDiarize speaker turn detection (upstream whisper feature, separate from --diarize) |
--carry-initial-prompt |
Forward --prompt across audio chunks |
-dtw |
Output DTW token-level timing in -ojf JSON |
-fa, -nfa |
Force flash-attn on / off |
-suppress-regex |
Suppress tokens whose detokenized text matches the regex |
-suppress-nst |
Suppress non-speech tokens |
-owts |
Karaoke-style word-timestamp WTS output |
For the full list of upstream whisper flags see crispasr --help
when invoked with a ggml-*.bin model loaded.
Pitch tracking is its own task, like --separate: audio goes in, a pitch
track comes out — not transcript segments. --pitch therefore routes to its
own dispatcher before any ASR backend is constructed.
# Auto-download the default model (crepe tiny) and print a pitch track
crispasr --pitch -m auto --auto-download -f samples/jfk.wav
# Explicit model, JSON output, 20 ms hop
crispasr --pitch -m crepe-full-f16.gguf --pitch-format json --pitch-hop-ms 20 -f audio.wavThe backend (crepe) is auto-detected from the GGUF general.architecture,
so you never name it; input is decoded to CREPE's native 16 kHz mono
automatically.
| Flag | Meaning |
|---|---|
--pitch |
Enable the pitch task |
--pitch-format FMT |
text (default) or json |
--pitch-hop-ms MS |
Analysis hop in milliseconds (default 10, CREPE's reference) |
Default output is one tab-separated line per frame — time_ms, f0_hz,
voiced_prob — so it pipes straight into awk / cut:
0.0 123.456 0.9312
10.0 123.501 0.9370
--pitch-format json emits {file, model, n_frames, frames: [...]} with the
same three fields per frame.
Models (cstr/crepe-GGUF, MIT):
| Registry key | File | Size | Notes |
|---|---|---|---|
crepe / crepe-tiny |
crepe-tiny-f16.gguf |
~1.0 MB | default — RTF 0.28 on Metal |
crepe-full |
crepe-full-f16.gguf |
~44.5 MB | RTF 2.0 on Metal; offline use |
CREPE is compute-heavy per frame (282 GFLOP per second of audio for full,
7.3 for tiny), so tiny is the shipping default and the GPU path is not
optional — on CPU even tiny runs at roughly RTF 2.4.
A standalone task: audio in, a beat and downbeat grid out. Like --chords,
--pitch and --separate it routes to its own dispatcher before any ASR
backend is built.
No DBN, and that is the point. Nearly every published beat tracker post-processes its framewise logits with madmom's Dynamic Bayesian Network, which is Böck-patented and licensed non-commercially. Beat This! (CPJKU, ISMIR 2024) reaches state-of-the-art without one, and its postprocessing here is peak-picking only. Both the code and the published weights are MIT, and the dependency list contains no part of madmom — so unlike
--chordsthere is no licence gate on this task.
# Explicit model
crispasr --beats -m beat-this-f16.gguf -f song.wav
# JSON, including the median-interval tempo estimate
crispasr --beats -m beat-this-f16.gguf --beats-format json -f song.wavThe backend (beat-this) is auto-detected from the GGUF
general.architecture; input is decoded to the model's native 22.05 kHz mono
automatically, and long files are internally split into 1500-frame chunks with
a 6-frame border overlap, matching upstream so results do not drift at seams.
| Flag | Meaning |
|---|---|
--beats |
Enable the beat task |
--beats-format FMT |
text (default) or json |
Default output is one tab-separated line per beat — time_sec and either
beat or downbeat — which is the .beats layout the MIR beat datasets
(Ballroom, GTZAN-Rhythm) use, so it drops straight into mir_eval.beat:
0.000 downbeat
0.540 beat
1.020 beat
1.520 beat
2.020 downbeat
Every downbeat is also emitted as a beat. The postprocessor snaps each downbeat onto its nearest detected beat, so downbeats are a strict subset of beats and you never have to merge two lists to reconstruct the grid.
Another standalone task: audio in, note events out — onset, offset, MIDI
pitch, name and velocity. Routes to its own dispatcher before any ASR backend
is built, like --pitch / --chords / --separate.
crispasr --piano -m auto --auto-download -f piano.wav
crispasr --piano --piano-format json -m piano-transcription-f16.gguf -f piano.wavDefault output is one tab-separated line per note — onset_sec, offset_sec,
midi, name, velocity:
1.406 1.455 60 C4 32
2.597 4.990 42 F#2 74
Deliberately not the .lab layout --chords uses: a chord timeline is
contiguous non-overlapping spans, whereas piano notes overlap freely, so
reusing that shape would imply a structure the data does not have.
--piano-format json adds pedal events.
| Flag | Meaning |
|---|---|
--piano |
Enable the piano-transcription task |
--piano-format FMT |
text (default) or json |
Before this verb existed the model was reachable only as --backend piano-transcription through transcribe(), which rendered each note as a
segment whose text read "C4 v=80" — parsing that back is lossy, and it was
never the intended seam. The structured session API
(crispasr_session_piano_notes) already existed for bindings; --piano gives
the CLI the same fidelity. The old path still works.
Model (cstr/piano-transcription-GGUF, Apache-2.0): ByteDance/Kong
high-resolution piano transcription, 16 kHz mono, CRNN + BiGRU with four heads
(frame/onset/offset/velocity) plus onset- and frame-refinement GRUs.
Per-frame, per-string fret scores from guitar audio, via TabCNN
(Wiggins & Kim, ISMIR 2019). Backend key tabcnn; the architecture is
auto-detected from the GGUF, so -m <file> alone is enough.
crispasr --tab -m tabcnn-f16.gguf -f guitar.wav
crispasr --tab -m auto --auto-download -f guitar.wav # fetches cstr/tabcnn-GGUF
crispasr --tab -m tabcnn-f16.gguf -f guitar.wav --tab-format jsonText output is one line per frame, tab-separated: the frame time in seconds,
then one column per string from low E to high E, with - for a string that is
not played.
0.023 1 - - - - -
0.046 1 - - - - -
--tab-format json adds the per-string log-probability of each displayed fret,
plus frame_period_sec, n_strings, n_classes and silent_class.
The model has no decoder: six independent softmaxes per frame, no
inter-string coupling, no temporal model, no search. The frets the CLI prints
are a plain argmax — they exist so the CLI has something to show and they
ignore every playability constraint (one note per string, fret range, capo, hand
span). Because the strings are scored independently, the grid can contain
physically impossible combinations.
For real use, take the log-probabilities through the C ABI and run your own constrained Viterbi/DP:
int n = crispasr_session_tab(s, pcm, n_samples, sample_rate);
int frames, strings, classes;
const float* emissions =
crispasr_session_tab_emissions(s, &frames, &strings, &classes);
int silent = crispasr_session_tab_silent_class(s); // read it, never assume
float hop = crispasr_session_tab_frame_period(s);
int open0 = crispasr_session_tab_string_open_midi(s, 0); // for capo/transposeemissions is [frame][string][class] log-probabilities, frame-major, valid
until the next call or session close.
Weights are CC BY 4.0 from the EGSet12 record (https://zenodo.org/records/11406378) — commercial use permitted, attribution required. This is the GuitarProFX-augmented variant; the baseline collapses from tablature F1 0.748 to 0.447 on real electric guitar, while the augmented one recovers to 0.585 (DAFx-24). Cite:
Pedroza HE, Abreu W, Corey R, Roman IR. "Leveraging real electric guitar tones and effects to improve robustness in guitar tablature transcription modeling." DAFx, 2024.
| variant | size | tablature F1 |
|---|---|---|
tabcnn-f16.gguf |
1.78 MB | 0.7732 (default) |
tabcnn-q8_0.gguf |
1.10 MB | 0.7749 |
tabcnn-q4_k.gguf |
0.72 MB | 0.7749 |
Quantization preserves head.weight at full precision — quantizing the output
layer costs 5.8 F1 points at Q4_0, while dense0 costs nothing.
- Solo guitar; mixed music is out of domain.
- Not a streaming surface. The CQT is normalised against the whole clip's
maximum (
amplitude_to_db(ref=np.max)), so features cannot be computed chunked without changing them —--tabis two-pass by construction. - GuitarSet-reported numbers overstate real-world accuracy; EGSet12 is the honest reference point.
Another standalone task: audio in, a chord timeline out. Like --pitch and
--separate it routes to its own dispatcher before any ASR backend is built.
⚠ The weights are non-commercial. CrispASR is MIT and the upstream BTC code (jayg996/BTC-ISMIR19) is MIT, but the checkpoints are CC-BY-NC-SA: they were trained on the Isophonics / Robbie Williams / UsPop2002 chord annotations, whose licences forbid commercial use. The registry refuses to download them unless you explicitly accept that licence, and a commercial product must train or supply its own weights. See docs/music-transcription/PLAN.md.
# Accept the non-commercial licence and auto-download the default model
crispasr --chords -m auto --auto-download --accept-license cc-by-nc-sa-4.0 -f song.wav
# Explicit model, JSON output
crispasr --chords -m btc-chords-large-f32.gguf --chords-format json -f song.wav
# Collapse the 170-class output to plain maj/min
CRISPASR_BTC_MAJ_MIN=1 crispasr --chords -m btc-chords-large-f32.gguf -f song.wavThe backend (btc) is auto-detected from the GGUF general.architecture;
input is decoded to BTC's native 22.05 kHz mono automatically.
| Flag | Meaning |
|---|---|
--chords |
Enable the chord task |
--chords-format FMT |
text (default) or json |
--accept-license SPDX |
Required to download the CC-BY-NC-SA weights (or all) |
Default output is one tab-separated line per span — start_sec, end_sec,
chord — which is exactly the .lab layout the chord datasets (Isophonics et
al.) use, so it drops straight into mir_eval:
0.000 1.950 C
1.950 3.901 G
3.901 8.081 N
N means "no chord". --chords-format json emits
{file, vocabulary, n_spans, chords: [...]} with a confidence per span.
Models (cstr/btc-chords-GGUF, weights CC-BY-NC-SA, code MIT):
| Registry key | File | Size | Notes |
|---|---|---|---|
btc-chords, btc-chords-large |
btc-chords-large-f16.gguf |
5.6 MB | default — 170-class vocabulary |
btc-chords-majmin |
btc-chords-f16.gguf |
5.6 MB | 25-class (maj/min + N) |
f32 variants (11.7 MB) are also published. Both dtypes pass the per-stage diff harness at cos 1.000000 on all 13 stages, so f16 is the shipping default.
The 170-class model is the default deliberately: it reduces to maj/min with
CRISPASR_BTC_MAJ_MIN=1, whereas a 25-class model can never be expanded.
When you pass -m auto (or -m default), CrispASR downloads the
default quantized model for the selected backend into
~/.cache/crispasr/ on first use. The registry (kept in sync with
src/crispasr_model_registry.cpp):
ASR backends
| Backend | Default download | Approx size |
|---|---|---|
| whisper | ggerganov/whisper.cpp/ggml-base.en.bin |
~147 MB |
| parakeet | cstr/parakeet-tdt-0.6b-v3-GGUF |
~467 MB |
| canary | cstr/canary-1b-v2-GGUF |
~600 MB |
| voxtral | cstr/voxtral-mini-3b-2507-GGUF |
~2.5 GB |
| voxtral4b | cstr/voxtral-mini-4b-realtime-GGUF |
~3.3 GB |
| granite | cstr/granite-speech-4.0-1b-GGUF |
~2.94 GB |
| granite-4.1 | cstr/granite-speech-4.1-2b-GGUF |
~2.94 GB |
| granite-4.1-plus | cstr/granite-speech-4.1-2b-plus-GGUF |
~5.6 GB |
| granite-4.1-nar | cstr/granite-speech-4.1-2b-nar-GGUF |
~5.4 GB (F16) / ~3.2 GB (Q4_K) |
| qwen3 | cstr/qwen3-asr-0.6b-GGUF |
~500 MB |
| cohere | cstr/cohere-transcribe-03-2026-GGUF |
~550 MB |
| wav2vec2 | cstr/wav2vec2-large-xlsr-53-english-GGUF |
~212 MB |
| omniasr | cstr/omniASR-CTC-1B-GGUF |
~551 MB |
| omniasr-llm | cstr/omniasr-llm-300m-v2-GGUF |
~580 MB |
| hubert | cstr/hubert-large-ls960-ft-GGUF |
~200 MB |
| data2vec | cstr/data2vec-audio-960h-GGUF |
~60 MB |
TTS backends — all auto-download the model + a default voice pack:
| Backend | Default download | Approx size | Notes |
|---|---|---|---|
| vibevoice-tts | cstr/vibevoice-realtime-0.5b-GGUF (Q4_K) + vibevoice-voice-emma.gguf |
~636 MB + ~3 MB | --model-quant q8_0 → ~1.1 GB higher-quality variant |
| vibevoice | cstr/vibevoice-asr-GGUF (Q4_K) |
~4.5 GB | ASR + TTS combo model |
| vibevoice-1.5b | cstr/vibevoice-1.5b-GGUF (Q4_K) |
~1.6 GB | Base model, runs without a voice pack |
| kokoro | cstr/kokoro-v1-GGUF (Q8_0) |
~330 MB | German variant: --backend kokoro-de |
| qwen3-tts | cstr/qwen3-tts-0.6b-base-GGUF (Q8_0) + F16 codec |
~690 MB + ~346 MB | Streaming-capable; codec auto-discovered |
| qwen3-tts-1.7b-base | cstr/qwen3-tts-1.7b-base-GGUF (Q8_0) + F16 codec |
~1.9 GB + ~346 MB | Higher quality |
| orpheus | cstr/orpheus-3b-0.1-ft-GGUF (Q8_0) |
~3.7 GB | Llama-3 based; US-English |
| chatterbox | cstr/chatterbox-tts-GGUF (Q4_K) |
~2 GB | S3Gen + T3; multilingual |
| piper | cstr/piper-en-hfc-medium-GGUF |
~63 MB | Lightweight, many voices via --voice |
| tada-1b | cstr/tada-tts-1b-GGUF (Q4_K + codec) |
~2.7 GB | English-only; --voice tada-ref.gguf |
| tada / tada-3b-ml | cstr/tada-tts-3b-ml-GGUF (Q4_K + codec) |
~5 GB | 9 languages; -l fr auto-downloads tada-ref-fr.gguf — see tts.md §TADA |
Downloads go through curl (preferred) with a wget fallback — no
Python, no libcurl link dependency. Works identically on Linux,
macOS, and Windows 10+ where curl ships in the base system. Models
are cached by filename; re-running is a single stat() check. The
same registry + cache helpers are reachable from the language
bindings (see bindings.md) so Python/Rust/Dart callers
can drive -m auto-style resolution without re-implementing it.
CrispASR decodes most common formats natively — no ffmpeg required — and auto-resamples to 16 kHz mono. The decoders are permissive-licensed and either embedded or linked:
- miniaudio (MIT-0) — WAV (any bit depth: 16/24/32
PCM, IEEE float, A-law, μ-law, ADPCM), FLAC, MP3, and the WAV family AIFF /
W64 / RF64 (via its bundled
dr_wav) - stb_vorbis (public domain) — OGG Vorbis
- glint (MIT) — in-tree clean-room
decoder for raw ADTS
.aac(AAC-LC) and Ogg.opus(RFC-conformant: all 12 RFC 6716/8251 vectors, SILK byte-identical to libopus). Cross-platform, always available with no runtime library, so it is the default for both.aacand.opusvia the librarycrispasr_audio_loadAPI —.opusdecodes even in builds without libopus. Pin the previous decoder withCRISPASR_AAC_DECODER=fdk/coreaudioorCRISPASR_OPUS_DECODER=libopus(=glint/auto= default);CRISPASR_{AAC,OPUS}_DEBUG=1prints a one-line decode summary. Opus is glint by default everywhere it appears — bare.opus, the stereo loader, and WebM/Matroska Opus — so a build withCRISPASR_OPUS=OFF(no libopus at all) still decodes them. - libopus + opusfile (BSD-3) — optional fallback for Opus (
.opus+ WebM), selected viaCRISPASR_OPUS_DECODER=libopus. Built by default (CRISPASR_OPUS, on when systemopusfileis found;CRISPASR_OPUS_FETCH=ONbuilds it statically for platforms without system libs). No longer required for any input format. - AudioToolbox (Apple system framework) — container
.m4a/.alac/.caf(and.aac) on macOS/iOS, no extra dependency; fdk-aac viadlopenprovides the same container support on Linux/Windows when installed.
| Format | Linux/other | Apple (macOS/iOS) | CRISPASR_FFMPEG=ON |
|---|---|---|---|
| WAV / FLAC / MP3 / OGG Vorbis / AIFF / W64 / RF64 | ✔ | ✔ | ✔ |
.opus |
✔ | ✔ | ✔ |
.aac (raw ADTS, AAC-LC) |
✔ (glint) | ✔ (glint) | ✔ |
.m4a / .alac / .caf (container) |
✔¹ | ✔ (AudioToolbox) | ✔ |
.webm (Opus/Vorbis, Matroska) |
✔ (glint/stb_vorbis) | ✔ | ✔ |
.wma / .amr / raw PCM |
✗ | ✗ | ✔ / pre-convert |
¹ Container AAC (.m4a) needs libfdk-aac installed (loaded via dlopen) or
CRISPASR_FFMPEG=ON; only raw ADTS .aac is covered dependency-free by the
in-tree glint decoder. On Apple all AAC/ALAC/CAF is handled natively
(AudioToolbox).
The crispasr CLI decodes all of the above through the same library loader
(crispasr_audio_load), so -f in.opus / in.aac / in.m4a / in.webm work
with no ffmpeg subprocess — the ffmpeg fallback is only reached for formats
the native decoders can't handle. For those, build with CRISPASR_FFMPEG=ON (an
optional, dynamically-linked fallback — see install.md) or
pre-convert: ffmpeg -i in.X -ar 16000 -ac 1 -c:a pcm_s16le out.wav.
Three runtime knobs control how much RAM / VRAM the binary uses. All are env vars (no CLI flags — these are rarely-changed deployment settings, not per-invocation switches).
The default f16 KV cache is the highest-quality option but the
biggest VRAM consumer. q8_0 halves it; q4_0 quarters it. Quality
drift is <0.1 % WER on validated backends; for long-audio chunked
work on a VRAM-tight host, this is the cheapest knob you can turn.
CRISPASR_KV_QUANT=q8_0 ./build/bin/crispasr --backend voxtral4b -m auto -f audio.wavPer-backend coverage:
| Backend | Honors CRISPASR_KV_QUANT? |
|---|---|
| voxtral / voxtral4b | ✔ |
| qwen3-asr | ✔ |
| granite / granite-4.1 / granite-4.1-plus | ✔ |
| glm-asr | ✔ |
| mimo-asr | ✔ |
| omniasr-llm | ✔ |
| gemma4-e2b | ✔ |
| canary | ✔ (cast-on-read fallback for read path; flash_attn_ext on for self-attn since §73) |
| cohere | ✔ (same — §73 flash_attn_ext shipped; +11 % regression on JFK quant K/V vs cast, see PERFORMANCE.md) |
| kyutai-stt | ✔ (native flash_attn_ext, quant-safe by construction) |
| orpheus | ✔ |
| qwen3-tts | ✔ (talker only) |
| chatterbox / chatterbox-turbo / kartoffelbox-turbo / lahgtna-chatterbox | ✔ (T3 LM side; S3Gen Conformer attention is F32 by design) |
| vibevoice | F16-only — flag is read but the σ-VAE attention path uses ggml_cpy(K_perm, view) write that's incompatible with quant K/V. Migration recipe is the canary/cohere flash_attn_ext port (see PERFORMANCE.md "Where the gaps are"). |
| granite-4.1-nar | — (non-autoregressive variant, no LLM decode path) |
| whisper / parakeet / fc-ctc / wav2vec2 / hubert / data2vec / firered-asr / moonshine / moonshine-streaming / omniasr-CTC | — (no KV cache: CTC / transducer / encoder-only) |
| kokoro | — (single-shot StyleTTS2 / iSTFTNet, no KV cache) |
The flag is read once per session via
core_attn::kv_dtype_from_env(<backend_name>); subsequent
session_transcribe calls reuse the dtype from session open. Set
the env before launching crispasr (or before opening the session
in Python / Rust / Dart).
The two halves of the KV cache have very different sensitivity
profiles: V is forgiving (errors get averaged inside the
post-softmax weighted sum), K is fragile (errors distort which
positions get attended to). llama.cpp exposes --cache-type-k /
--cache-type-v for this; CrispASR does the same via two env
vars that override CRISPASR_KV_QUANT per half.
# Common llama.cpp recipe — ~40 % more KV memory savings vs symmetric
# Q8_0/Q8_0, with PPL barely moved on Llama-class models.
CRISPASR_KV_QUANT_K=q8_0 CRISPASR_KV_QUANT_V=q4_0 \
./build/bin/crispasr --backend voxtral4b -m auto -f audio.wavBoth halves fall through to CRISPASR_KV_QUANT when their
type-specific var is unset, so the legacy single-knob configuration
keeps working unchanged.
Same per-backend coverage as the table above — the asymmetric
plumbing was added to every backend that honored CRISPASR_KV_QUANT
(voxtral, voxtral4b, omniasr, qwen3_asr, granite_speech, orpheus,
glm_asr, gemma4_e2b, mimo_asr, qwen3_tts).
For users with very long context where even CRISPASR_KV_QUANT=q4_0 won't
fit in VRAM. Allocates the KV cache on the CPU backend instead of
the GPU backend, even when model weights are active on GPU.
# Long-context fallback when VRAM is exhausted
CRISPASR_KV_ON_CPU=1 ./build/bin/crispasr --backend voxtral4b -m auto -f long-audio.wav
# Stacks with CRISPASR_KV_QUANT_K/_V — minimum-memory KV path
CRISPASR_KV_ON_CPU=1 CRISPASR_KV_QUANT_K=q8_0 CRISPASR_KV_QUANT_V=q4_0 \
./build/bin/crispasr --backend voxtral4b -m auto -f long-audio.wavTry CRISPASR_KV_QUANT first. The expensive part isn't the alloc —
every attention step copies the KV slice GPU↔CPU↔GPU. The
PCIe / unified-memory traffic is typically slower than just running
with quantised KV in VRAM. Reach for CRISPASR_KV_ON_CPU only when
quantisation alone can't fit the context.
The verbose log line shows (on cpu) vs (on gpu) so you can
confirm where the cache landed:
voxtral4b: kv cache 169 MiB k=q8_0 v=q4_0 (on cpu, ...)
Same per-backend coverage as CRISPASR_KV_QUANT (voxtral, voxtral4b,
omniasr, qwen3_asr, granite_speech, orpheus, glm_asr, gemma4_e2b,
mimo_asr, qwen3_tts).
llama.cpp --n-gpu-layers parity. Default -1 keeps legacy
single-backend behaviour (everything on GPU, or CPU if -ng).
Setting N in [0, total_layers) puts the first N transformer
blocks on GPU and spills the rest to system RAM, so models larger
than VRAM can still run end-to-end.
# Voxtral4b has 26 transformer blocks. Half on GPU, half on CPU.
CRISPASR_N_GPU_LAYERS=13 \
./build/bin/crispasr --backend voxtral4b -m auto -f audio.wav
# Tight VRAM: only audio encoder + projection + embeddings on GPU,
# all 26 transformer blocks on CPU.
CRISPASR_N_GPU_LAYERS=0 \
./build/bin/crispasr --backend voxtral4b -m auto -f audio.wavVerbose log shows weight residency and the layer split:
voxtral4b: weight residency: gpu=1585 MiB (571 tensors), cpu=821 MiB (143 tensors)
voxtral4b: layer offload: gpu=[0,13), cpu=[13,26) (CRISPASR_N_GPU_LAYERS=13)
Coverage (10 LLM-decode backends): voxtral, voxtral4b, qwen3_asr,
granite_speech, glm_asr, orpheus, omniasr-llm, gemma4_e2b, mimo_asr,
vibevoice. Vibevoice is dual-mode — ASR-only files split the
28-layer lm.layers.<N>.* path; TTS-enabled files (tts_n_layers > 0)
split the dominant 20-layer tts_lm.layers.<N>.* path while the
4-layer base LM stays on GPU. Encoder-decoder ASR (canary, cohere,
kyutai-stt) is not yet covered — cross-attention layout has no
<prefix><N>.* block-tagged tensors and needs a bespoke predicate.
Stacks with CRISPASR_KV_ON_CPU and CRISPASR_KV_QUANT_K/_V — set all three for
the most aggressive memory footprint reduction. CRISPASR_KV_QUANT is
cheaper than layer offload; reach for CRISPASR_N_GPU_LAYERS only when the
model doesn't fit, not the cache.
Map the GGUF file directly into the model's backend buffer instead of read-and-copy. Saves one full copy of the GGUF on load: a 14.9 GB F16 model goes from "load + 14.9 GB peak RSS" to "mmap + ~working-set RSS." No quality impact; pure load-time + RAM win.
Default-on since 0.6.7 (issue #94 — chatterbox-turbo slow / failing
init on macOS, where the legacy alloc+copy path took 30-60 s for
the 658 MB T3 GGUF). Opt out with CRISPASR_GGUF_MMAP=0 if your
model files live on volumes that may disappear mid-run — mmap-backed
weights SIGBUS if the underlying file vanishes (network mounts,
removable disks).
# Default — mmap is on, no env var needed
./build/bin/crispasr --backend voxtral4b -m auto -f audio.wav
# Opt out for removable media
CRISPASR_GGUF_MMAP=0 ./build/bin/crispasr --backend voxtral4b -m auto -f audio.wavHonored by every backend that uses core_gguf::load_weights() —
all non-whisper backends. Whisper itself uses upstream's loader and
isn't affected.
When mmap is enabled, this triggers a one-byte read on every page to force the working set resident before returning. Trades cold- start load time for cold-start prefill time. Useful for servers that will do many short generations after one-time load and don't want the first request to pay the page-fault tax.
CRISPASR_GGUF_MMAP=1 CRISPASR_GGUF_PRELOAD=1 ./build/bin/crispasr ...In order of cost — try the cheapest first:
# 1. Cheapest — half the KV. ~0.05 % WER drift on validated suite.
CRISPASR_KV_QUANT=q8_0 \
./build/bin/crispasr --backend voxtral4b -m auto -f audio.wav
# 2. Aggressive — quarter the KV. ~0.2 % WER drift.
CRISPASR_KV_QUANT=q4_0 \
./build/bin/crispasr --backend voxtral4b -m auto -f audio.wav
# 3. Plus mmap so the load doesn't double-allocate the model weights.
# Useful when you're loading a multi-GB F16 model and the host has
# less RAM than 2× model size.
CRISPASR_KV_QUANT=q4_0 CRISPASR_GGUF_MMAP=1 \
./build/bin/crispasr --backend voxtral4b -m auto -f audio.wavSee the CRISPASR_N_GPU_LAYERS and CRISPASR_KV_ON_CPU sections above
for the full layer-offload and KV-spill knobs — both are supported.
All TTS output is watermarked by default. Additional flags control the neural watermark, C2PA signing, voice-cloning consent, and the opt-out:
| Flag | Description |
|---|---|
--watermark-model PATH |
Load AudioSeal GGUF for neural watermarking (upgrades built-in spread-spectrum) |
--no-watermark |
Disable the AI-content watermark on TTS output. Equivalent to the CRISPASR_NO_WATERMARK env var; both emit a one-time stderr warning and shift the AI-content marking responsibility onto the operator (see below) |
--detect-watermark PATH |
Read a WAV file, run watermark detection, print confidence + verdict (>0.65 = AI-GENERATED, 0.4–0.65 = UNCERTAIN, <0.4 = none), then exit |
--i-have-rights |
Required for voice cloning (--voice <file.wav>); attests speaker consent |
--speaker-identity VALUE |
Whose voice a preset voice is: real_person, synthetic or unknown (default). real_person adds the audible AI disclosure to non-cloned output — a stock voice can be an identifiable individual, which makes the output a deep fake under Art. 3(60) without any cloning. It does not require --i-have-rights. Backends default to unknown and warn once per model until someone answers; don't silence that with synthetic unless you've read the model card. See eu-ai-act.md §6.2a |
--no-spoken-disclaimer |
Skip the audible AI-disclosure prefix on voice-cloned output (watermark + C2PA still applied; caller assumes disclosure responsibility) |
--g2p-dict SOURCE |
G2P pronunciation dictionary: olaph (MIT, default), open-dict (CC-BY-SA), or path to a custom dict file. Auto-downloads on first use. See tts.md for details. |
--c2pa-cert PATH |
X.509 certificate for C2PA Content Credentials signing |
--c2pa-key PATH |
Private key for C2PA signing (generate both with scripts/generate-c2pa-cert.sh) |
Disabling the watermark. --no-watermark and CRISPASR_NO_WATERMARK=1 are
equal-status opt-outs (neither is more "official"). Either one turns the mark
off for the whole process and logs, once:
crispasr: warning: watermarking disabled. AI usage marking responsibility rests with the operator.
The message is deliberately jurisdiction-neutral — no statute is named at
runtime. Turning the mark off does not remove any legal AI-disclosure obligation
that may apply to the output; it transfers responsibility for meeting it to
whoever runs the binary. See tts.md for the full rationale.
Debug env vars:
CRISPASR_AUDIOSEAL_DEBUG=1— print AudioSeal tensor shapes during graph buildCRISPASR_AUDIOSEAL_DUMP_STAGES=1— dump per-stage binary tensors to/tmp/
See tts.md for full watermarking documentation.
For TTS-specific deployment knobs (codec backend selection, graph
reuse, etc.) see tts.md:
CRISPASR_QWEN3_TTS_CODEC_GPU— clean codec-on-GPU path (CUDA / Vulkan)CRISPASR_QWEN3_TTS_O15— code-predictor graph reuse (CPU/Metal opt-in)CRISPASR_KOKORO_GEN_GPU— generator on GPU (CUDA / Vulkan)CRISPASR_COSYVOICE3_FLOW_STEPS=N— CosyVoice3 flow Euler steps (1..100; model default10). Lower values reduce flow latency approximately linearly at a possible quality cost;5is a practical fast mode.CRISPASR_COSYVOICE3_BENCH=1— print CosyVoice3 per-stage timings.CRISPASR_COSYVOICE3_CFG_BATCH=0— compatibility fallback to two separate flow forwards per Euler step. The default batch-2 path matches upstream and is faster while producing identical output on validated CPU and Metal runs.CRISPASR_COSYVOICE3_KV_BUCKET=0— compatibility fallback that exposes the full KV allocation to every AR step instead of the default 256-token active buckets.CRISPASR_TADA_NUM_CANDIDATES=N— TADA flow-matching duration candidates per token, ranked by reconstruction likelihood (CLI default4). The duration head is noise-sensitive, so a single draw (N=1, fastest) can occasionally collapse timing into rushed/garbled speech;4–8make it robust. All candidates for a step solve in one batched forward, so higherNadds little wall-clock. Default4also applies through the C ABI / bindings / server; override there at runtime withset_tts_num_candidates(n). Seetts.md.CRISPASR_TADA_DO_SAMPLE,CRISPASR_TADA_TEMPERATURE,CRISPASR_TADA_TOP_P,CRISPASR_TADA_TOP_K,CRISPASR_TADA_REPETITION_PENALTY— TADA talker text-decoder sampling, matching upstreamInferenceOptionsdefaults (do_sample=1, temp=0.6, top_p=0.9, top_k=0, rep_penalty=1.1). The talker samples by default; greedy decoding (CRISPASR_TADA_DO_SAMPLE=0) has no repetition control and loops/cuts off words on harder or non-English text. Honoured by the CLI, C ABI, bindings and server;set_temperature/set_top_p/set_repetition_penaltyalso reach TADA at runtime.CRISPASR_VIBEVOICE_VAE_BACKEND={auto,cpu,gpu}— VAE decoder placementCRISPASR_VIBEVOICE_TTS_FLASH_ATTN={1,0}— TTS LM attention:1(default) uses fusedggml_flash_attn_ext;0uses an explicitsoftmax(QKᵀ)·Vpath. Set0if VibeVoice TTS garbles, mixes voices, or repeats on a GPU whose fused flash-attention shader is buggy — notably AMD RDNA4 (RX 9700 XT) on Vulkan, whose coopmat2 FA shader produces wrong hidden states (issue #171). The no-rebuild equivalent isGGML_VK_DISABLE_COOPMAT2=1. This knob andCRISPASR_VIBEVOICE_VAE_BACKENDbisect the TTS GPU graph (LM attention vs. the conv/col2im VAE) to localise a bad kernel.
CosyVoice3 performance notes:
- Keep GPU execution enabled. The 10-step DiT flow is substantially slower
on CPU;
--gpu-backend metalselects Metal explicitly on macOS. -n/--max-new-tokensis also the AR KV-cache sizing bound. A realistic cap reduces per-token work, but a value that is too low truncates speech.CRISPASR_COSYVOICE3_FLOW_STEPS=Nsets the CFM Euler step count (default10). Flow work is ~linear inNand flow is ~48 % of the wall. M1 sweep (--seed 42, log-mel-spectrogram corr vs the 10-step output — ASR roundtrip is verbatim at 8/6 and cannot distinguish steps):8→0.9948,6→0.9925,4→0.9895 with a one-word ASR slip.6is the perceptual sweet spot (~−40 % flow work, ~−19 % of the wall) and matches the chatterbox default;4starts to show audible artifacts.6was confirmed across short / numbers / long sentences (6-vs-10 mel-corr 0.9953 / 0.9930 / 0.9938).10is the conservative upstream default; drop to6for a large speedup at near-identical quality.- An external model directory affects cold startup, not steady-state synthesis. For repeated requests, use server mode so the ~1.2 GB model set remains resident.
- Baked voices do not load the S3 tokenizer or CAMPPlus encoder. Those
companions add roughly 475 MiB and are loaded lazily on the first
.wavcloning request, including a later request to a resident server. - On an Apple M1 test using the Q4_K LLM and Q8_0 flow, request-sized KV caching plus active KV buckets reduced a 17-token LM decode from about 6.8 s to 0.5 s. Batched CFG reduced the 10-step flow from about 4.6 s to 1.9–2.3 s. The final WAV remained byte-identical. Hardware and prompt length change results.
For users coming from llama.cpp, here's how the equivalent knobs
map:
| Concern | llama.cpp | CrispASR |
|---|---|---|
| KV cache dtype | --type-k q8_0 --type-v q8_0 (CLI flag, separate K/V) |
CRISPASR_KV_QUANT=q8_0 for symmetric, or CRISPASR_KV_QUANT_K / _V per half |
| mmap weights | --no-mmap (mmap is default on) |
CRISPASR_GGUF_MMAP=0 (mmap is default on since 0.6.7) |
| Lock pages in RAM | --mlock |
(not supported — mmap+preload is the closest analogue) |
| GPU layer count | --n-gpu-layers N / -ngl N (CLI flag) |
CRISPASR_N_GPU_LAYERS=N env var — 10 LLM backends |
| KV-on-CPU-only | --no-kv-offload |
CRISPASR_KV_ON_CPU=1 env var |
| Flash attention | --flash-attn / -fa |
always-on where the backend's capabilities() declares CAP_FLASH_ATTN |
| Threads | --threads N / -t N |
--threads N / -t N (matched) |
| Force CPU | --gpu-layers 0 |
--no-gpu / --gpu-backend cpu |
Differences worth flagging:
- mmap default. Both projects now default mmap on. CrispASR
flipped from opt-in to default-on in 0.6.7 after issue #94 (slow /
failing chatterbox-turbo init on macOS — the legacy alloc+copy
path took 30-60 s for the 658 MB T3 GGUF). Set
CRISPASR_GGUF_MMAP=0to opt out (matches llama.cpp's--no-mmap). - K/V dtype unified. llama.cpp lets you set
--type-kand--type-vindependently (rare scenario: quantize K but keep V at f16). CrispASR uses a singleCRISPASR_KV_QUANTfor both. The split would be a small change if anyone needs it; file an issue with a use case. - CLI flags vs env vars. llama.cpp surfaces every memory knob
as a CLI flag; CrispASR uses env vars for them on the assumption
that they're rarely-changed deployment settings. If you want flag
parity, see open issue / PR — converting the env vars to flags
is mechanical (
-DCRISPASR_KV_QUANT=valstyle) but adds CLI surface area. CRISPASR_N_GPU_LAYERS=N. Equivalent to--n-gpu-layers N. Supported on 10 LLM-decode backends (voxtral, voxtral4b, qwen3-asr, granite, glm-asr, orpheus, omniasr-llm, gemma4-e2b, mimo-asr, vibevoice). See the section above for details.
docs/streaming.md—--stream,--mic,--live, sliding-window flags, per-token confidencedocs/tts.md— Kokoro / Qwen3-TTS / VibeVoice / Orpheus / Chatterbox- every TTS-side env var (
CRISPASR_QWEN3_TTS_CODEC_GPU,CRISPASR_QWEN3_TTS_SKIP_REF_DECODE,CRISPASR_QWEN3_TTS_O15,CRISPASR_KOKORO_GEN_GPU,CRISPASR_VIBEVOICE_VAE_BACKEND, …)
- every TTS-side env var (
docs/server.md— HTTP/inference, OpenAI-compat/v1/audio/transcriptions,/v1/audio/speech(TTS),/v1/audio/speech-to-speech(S2S)docs/bindings.md— Python / Rust / Dart / Go / Java / Ruby — every CLI feature is reachable through the C-ABIdocs/install.md— full build options, GPU backends, ffmpeg ingestion, glibc compatibilitydocs/quantize.md—crispasr-quantizeper-backend recommended quantsdocs/architecture.md— internals:src/core/primitives, per-backend graph surveydocs/contributing.md— adding a new backend, PyTorch-vs-C++ stage diff workflowdocs/regression-matrix.md—tools/test-all-backends.pycapability tiers