Skip to content

Latest commit

 

History

History
213 lines (178 loc) · 12.8 KB

File metadata and controls

213 lines (178 loc) · 12.8 KB

Audio Input Limits

How transcribe.cpp bounds audio input length, what a caller observes when a limit is reached, and how to discover the limit before calling. This is both the design reference for the library and the contract a consumer can rely on.

The one-paragraph version

There is no single global length limit. Each model family falls into one of three buckets. Some families chunk long audio internally and have no practical limit; some have a hard context-window limit and reject over-length input before running; some accept any length but lose accuracy past the window they were trained on and warn when you cross it. Whatever the bucket, the library never truncates silently: an over-length input is rejected up front with TRANSCRIBE_ERR_INPUT_TOO_LONG; a transcript that runs into the context or generation budget mid-decode returns the hard status TRANSCRIBE_ERR_OUTPUT_TRUNCATED (with the partial transcript still readable and transcribe_was_truncated() set); and a soft-window family logs a WARN and proceeds. Every model reports its usable limit through transcribe_capabilities::max_audio_ms (or, per-session, transcribe_session_get_limits()) so you can check before you call. (Streaming is the one exception to the non-OK truncation rule — see below.)

Discovering the limit (consumer-facing)

Read transcribe_capabilities::max_audio_ms after loading a model:

struct transcribe_capabilities caps;
transcribe_capabilities_init(&caps);
transcribe_model_get_capabilities(model, &caps);

if (caps.max_audio_ms == 0) {
    /* No practical limit. See the family notes for any absolute safety cap. */
} else {
    /* Usable ceiling in milliseconds of 16 kHz audio. Longer input is
       either rejected (hard-cap families) or warned-and-degraded
       (soft-window families) — see the family's bucket below. */
}

max_audio_ms is a single honest number derived from the model's real metadata, not a hardcoded guess. For hard-cap families it is computed from the decoder context window minus a representative prompt and a generation reserve — the same shape as the upfront gate, but the gate uses the exact prompt for each call (which shifts a little with the language hint and other options), so max_audio_ms is advisory and slightly conservative, not an exact per-call bound. For soft-window families it is the advisory window the model was trained on. 0 means "no practical limit." It is a model-level value at the default context; for the effective limit under a lowered n_ctx, use transcribe_session_get_limits().

The three buckets

1. Chunked / unbounded — "just works"

Families Limit Behavior
whisper, parakeet (all variants), voxtral_realtime none (max_audio_ms = 0) Long audio is windowed internally and stitched (whisper), processed by an unbounded/streaming encoder (parakeet, voxtral_realtime), or padded if short. No practical limit; all three ignore n_ctx (it cannot lower a limit they don't have). whisper and parakeet never error on length. voxtral_realtime has one exception — its absolute dec_max_position cap (~2.9 h, see below): a clip past it returns INPUT_TOO_LONG (one-shot/batch), and a stream that reaches it flags was_truncated.

Whisper slices audio into 30 s windows with prev-context stitching; parakeet's conformer is effectively unbounded (the encoder positional table is recomputed per run, not a fixed wall). This holds for every parakeet variant, including the cache-aware streaming RNN-T member (nemotron-3.5-asr-streaming-0.6b): the RNN-T transducer has no decoder context window, and the cache-aware path carries constant-memory caches (cache_last_channel / cache_last_time + the decoder LSTM state) rather than a growing KV, so it is unbounded. These families do not need and do not have a length gate.

2. Hard context cap — reject up front

Families Limit source Behavior
qwen3_asr, canary_qwen, funasr_nano, granite, granite_nar, voxtral, cohere, canary decoder context window (dec_max_position_embeddings / dec_max_seq), or the encoder positional table (enc_pos_emb_max_len, for cohere/canary) — all from GGUF KV cache grows to fit, clamped to the model's true max. Over-length input is rejected before the decode (or before the encoder, where the encoder table is the binding limit) with TRANSCRIBE_ERR_INPUT_TOO_LONG.

These families wrap an LLM-style decoder whose context window (audio_tokens + prompt + generation) is the binding constraint. The number of tokens a clip consumes is a deterministic function of its sample count (n_samples → mel frames → fixed subsampling → audio tokens), so the library computes the prefill size before running the encoder and rejects an over-length clip immediately — the caller never pays for a compute pass that cannot fit. The rejection goes through the log callback, not raw stderr.

The one case that cannot be predicted up front is the transcript itself running long enough to exhaust the remaining budget mid-decode (rare — the output would have to be very large for the audio length). There, the run returns the hard status TRANSCRIBE_ERR_OUTPUT_TRUNCATED while keeping the partial transcript readable (exactly like an aborted run); transcribe_was_truncated(session) is also set, and a WARN is logged. A truncated transcript is never returned as TRANSCRIBE_OK — a caller cannot mistake it for complete — and the partial output is never discarded. In transcribe_run_batch this is a per-utterance status (the whole-batch call still returns TRANSCRIBE_OK).

3. Soft window — warn and proceed

Families Window Behavior
gigaam (~25 s), sensevoice (~30 s), medasr (~400 s), moonshine (output-bound, ~48 s), moonshine_streaming (output-bound, ~17 min) training / positional window Any length is accepted; past the window the library emits a WARN (degraded accuracy is possible) and proceeds. max_audio_ms reports the window as advisory.

These families have no hard architectural wall but were trained on a bounded window; beyond it, accuracy degrades rather than failing. The library does not reject (a caller may knowingly accept degradation) but it does warn so the degradation is never silent. Upstream gigaam rejects over-length audio outright; transcribe.cpp deliberately warns-and-proceeds instead so callers keep control.

Moonshine is the honest edge case in this bucket: its cap is on output (max_length = 194 decode tokens, ≈ 48 s), not input, so it cannot be gated on audio length — a dense short clip can hit it too. It is reported via transcribe_was_truncated() and a WARN (and, offline, the hard TRANSCRIBE_ERR_OUTPUT_TRUNCATED status) when the cap is reached. moonshine_streaming has the same output-bound shape with a much larger window (dec_max_position_embeddings = 4096, ≈ 17 min); because it also streams, its truncation follows the streaming rule below — stream_finalize still returns TRANSCRIBE_OK and the truncation surfaces only through transcribe_was_truncated().

Context sizing and the n_ctx knob

For bucket-2 families the decoder KV cache grows to fit the actual input, bucketed to avoid reallocation churn, and is clamped to the model's true maximum read from GGUF metadata. This replaced earlier per-family hardcoded walls (e.g. a fixed 2048 that ignored a much larger real context window) so the GGUF metadata is the single source of truth for the ceiling.

transcribe_session_params::n_ctx lets a caller lower the ceiling to bound memory:

  • 0 (default): use the model's true maximum from GGUF.
  • > 0: cap the context at this many tokens. Values above the model maximum are clamped down to it — the knob can only narrow, never extend past what the model supports.

For decoder-context-bound families, lowering n_ctx lowers the effective audio limit and the upfront gate enforces the lowered value. Note that max_audio_ms is a model-level capability, queried before any session exists, so it always reports the model's default-context ceiling (n_ctx == 0); it is not re-derived for a session that narrows n_ctx. A session that lowers n_ctx may therefore reject audio shorter than the advertised max_audio_ms.

Encoder-bound families are different. For cohere and canary, the input-audio limit is the encoder positional table, while n_ctx only bounds the decoder self-KV / output budget. In those families transcribe_session_get_limits() reports a smaller effective_n_ctx and max_kv_bytes when n_ctx is lowered, but effective_max_audio_ms stays pinned to the encoder input bound.

Chunked / unbounded families (bucket 1) ignore n_ctx entirely. whisper, parakeet, and voxtral_realtime have no lowerable context ceiling, so a non-zero n_ctx is a documented no-op and transcribe_session_get_limits() keeps reporting them unbounded. voxtral_realtime is the subtle case: it does have an absolute decoder position cap (dec_max_position, ~2.9 h of audio), but that is the model's true RoPE wall — not a memory ceiling a caller can lower — so n_ctx does not narrow it (its decoder KV is a constant-memory sliding ring; there is nothing for n_ctx to bound). A clip past the absolute cap is rejected with TRANSCRIBE_ERR_INPUT_TOO_LONG (one-shot and batch) or surfaced via transcribe_was_truncated() (streaming) regardless of n_ctx.

What a caller observes — summary

Situation Status Log Result
Input within limit TRANSCRIBE_OK full transcript
Over-length, hard-cap family TRANSCRIBE_ERR_INPUT_TOO_LONG ERROR via callback no transcript (rejected before the decode)
Generation ran long mid-decode TRANSCRIBE_ERR_OUTPUT_TRUNCATED WARN via callback partial transcript readable; transcribe_was_truncated() == true
Over-window, soft-window family TRANSCRIBE_OK WARN via callback full transcript (accuracy may be degraded)
Chunked / unbounded family TRANSCRIBE_OK full transcript
Cache/graph allocation failed TRANSCRIBE_ERR_OOM ERROR via callback no transcript (no silent context shrink)

In transcribe_run_batch, INPUT_TOO_LONG and OUTPUT_TRUNCATED are per-utterance statuses (transcribe_batch_status(session, i)); the whole-batch call returns TRANSCRIBE_OK.

transcribe_was_truncated(session) is reset at the top of every transcribe_run, transcribe_run_batch, and transcribe_stream_begin (the same lifecycle as transcribe_was_aborted).

Streaming is the exception

TRANSCRIBE_ERR_OUTPUT_TRUNCATED is an offline-only status (transcribe_run / transcribe_run_batch). An active stream is incremental and has its own terminal-state machine (transcribe_stream_*, IDLE/ACTIVE/FINISHED/FAILED), and stream_feed / stream_finalize return the status of that step, not a verdict on the whole transcript. So when a streaming decode reaches its context cap (e.g. voxtral_realtime at its absolute position limit — hours of continuous audio, or moonshine_streaming at its output window), the stream does not fail and stream_finalize returns TRANSCRIBE_OK; the truncation is surfaced through transcribe_was_truncated(session) and a WARN. This is deliberate: forcing a stream into a failed terminal state on truncation would discard the committed text the caller has been consuming. A streaming caller that needs to detect truncation should check transcribe_was_truncated() after finalize.

Design notes (for maintainers)

  • The upfront gate and max_audio_ms share a shape for decoder-context-bound families but differ in precision: max_audio_ms ≈ (ceiling − representative_prompt − generation_reserve) / tokens_per_ms, where ceiling is the GGUF max. The gate uses the exact per-call prompt, so max_audio_ms is advisory/slightly-conservative, not the exact bound. The per-model rate constants live in transcribe_model::LimitsBasis, which transcribe_session_get_limits() reads to recompute the effective limit at a lowered n_ctx. For encoder-bound families, the session query keeps the audio bound from max_audio_ms while still reporting the lowered decoder KV budget.
  • n_ctx is a session context/KV cap, never a way to extend the model: effective_n_ctx = n_ctx == 0 ? model_max : min(n_ctx, model_max).
  • Allocation failures (KV cache, compute graph) return TRANSCRIBE_ERR_OOM with a logged diagnostic, never GGUF/BACKEND, and the library does not silently shrink the context and retry.
  • Length/context messages must go through the shared transcribe_log helpers, never fprintf(stderr) — a consumer that installed a log sink must see them.
  • New families pick a bucket at port time and fill max_audio_ms accordingly (0 for chunked/unbounded). The model-card template carries an "Input limits" block that must be populated with the real number.