Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
c683647
prompting prereqs: drop voxtral INITIAL_PROMPT bit, IBM granite trans…
cjpais Sep 27, 2026
e20948a
prompting core ABI: vocabulary/prompt/prefix fields, INSTRUCT task, f…
cjpais Sep 27, 2026
685a0d7
prompting: log why a prompting request is rejected
cjpais Sep 27, 2026
3be96ed
prompting bindings (python, rust, swift, ts) and CLI flags
cjpais Sep 27, 2026
7f2aa4d
whisper: vocabulary and context prompt in the startofprev slot
cjpais Sep 27, 2026
4ed172e
qwen3_asr: vocabulary and context prompt in the system message; share…
cjpais Sep 27, 2026
80c4ab5
granite: vocabulary as keyword-list biasing (4.0, 4.1, 4.1-plus)
cjpais Sep 27, 2026
459c981
voxtral: TRANSCRIBE_TASK_INSTRUCT through the chat instruct path
cjpais Sep 27, 2026
8e9a9b0
funasr_nano: vocabulary as the upstream hotword list
cjpais Sep 27, 2026
9c11c2d
whisper: transcript prefix after the SOT sequence, first window only
cjpais Sep 27, 2026
2355363
prompting: document AUTO timestamps under a prefix
cjpais Sep 27, 2026
0afce1a
canary2: transcript prefix via user_prefix; SentencePiece BPE encoder…
cjpais Sep 27, 2026
84df795
granite: transcript prefix on 4.1-plus via prefix_text
cjpais Sep 27, 2026
9a71694
granite: simplify prefix trim
cjpais Sep 27, 2026
975985b
moss: vocabulary as the upstream hotword hint; converter splits the i…
cjpais Sep 27, 2026
ce7c494
docs: prompting line per model doc
cjpais Sep 27, 2026
ceb033d
python: model-gated prompting tests
cjpais Sep 27, 2026
265f12d
prompting: reject bad prompting text before clearing the result; whis…
cjpais Sep 27, 2026
bbd5b57
prompting acceptance tolerances
cjpais Sep 27, 2026
9f76384
granite: decode budget for the word-timestamps task
cjpais Sep 27, 2026
7ccfac6
granite: timestamps AUTO selects word timestamps on 4.1-plus unless v…
cjpais Sep 27, 2026
078648b
docs: granite-plus timestamps AUTO
cjpais Sep 27, 2026
14ca031
prompting tolerances: voxtral, granite-plus AUTO
cjpais Sep 27, 2026
04b34fd
prompting tolerances: fun-asr mlt-nano
cjpais Sep 27, 2026
a949cce
Revert "docs: granite-plus timestamps AUTO"
cjpais Sep 27, 2026
cab3cc0
Revert "granite: timestamps AUTO selects word timestamps on 4.1-plus …
cjpais Sep 27, 2026
202eec4
prompting tolerances: granite-plus word-timestamps note
cjpais Sep 27, 2026
b7fcdd7
review
cjpais Sep 27, 2026
5cb2659
bindings: prompting fixes, tests and docs
cjpais Sep 28, 2026
1b18582
cli: reject --prefix with --batch; strip a UTF-8 BOM from --vocabular…
cjpais Sep 28, 2026
bcf9d26
voxtral: INSTRUCT decodes until EOS, growing the KV cache on demand; …
cjpais Sep 28, 2026
ddf6f81
docs
cjpais Sep 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions bindings/python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,17 @@ and the one-shot `transcribe()` helper.
result = session.run(pcm, pnc="off", itn="on")
```

### Prompting

`vocabulary` (custom terms), `prompt` (context, or the instruction under
`task="instruct"`) and `prefix` (text the model continues from) take effect
where `model.supports()` reports `"vocabulary"`, `"context_prompt"`,
`"instruct"` or `"transcript_prefix"`.

```python
result = session.run(pcm, vocabulary=["Kubernetes", "gRPC"])
```

Streaming models expose incremental transcription with committed/tentative
text views — see `examples/stream_wav.py`:

Expand Down
92 changes: 75 additions & 17 deletions bindings/python/src/transcribe_cpp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
# String-enum types, exported so callers (and type checkers) can name them.
Backend = Literal["auto", "cpu", "metal", "vulkan", "cpu_accel", "cuda", "rocm"]
KVType = Literal["auto", "f32", "f16"]
Task = Literal["transcribe", "translate"]
Task = Literal["transcribe", "translate", "instruct"]
Timestamps = Literal["none", "auto", "segment", "word", "token"]
Pnc = Literal["default", "off", "on"]
Itn = Literal["default", "off", "on"]
Expand All @@ -62,6 +62,7 @@
Feature = Literal[
"initial_prompt", "temperature_fallback", "long_form",
"cancellation", "pnc", "itn", "diarization",
"vocabulary", "context_prompt", "instruct", "transcript_prefix",
]

__all__ = [
Expand Down Expand Up @@ -203,6 +204,7 @@
_TASKS = {
"transcribe": _generated.TRANSCRIBE_TASK_TRANSCRIBE,
"translate": _generated.TRANSCRIBE_TASK_TRANSLATE,
"instruct": _generated.TRANSCRIBE_TASK_INSTRUCT,
}
_TIMESTAMPS = {
"none": _generated.TRANSCRIBE_TIMESTAMPS_NONE,
Expand Down Expand Up @@ -250,6 +252,10 @@
"pnc": _generated.TRANSCRIBE_FEATURE_PNC,
"itn": _generated.TRANSCRIBE_FEATURE_ITN,
"diarization": _generated.TRANSCRIBE_FEATURE_DIARIZATION,
"vocabulary": _generated.TRANSCRIBE_FEATURE_VOCABULARY,
"context_prompt": _generated.TRANSCRIBE_FEATURE_CONTEXT_PROMPT,
"instruct": _generated.TRANSCRIBE_FEATURE_INSTRUCT,
"transcript_prefix": _generated.TRANSCRIBE_FEATURE_TRANSCRIPT_PREFIX,
}


Expand Down Expand Up @@ -653,9 +659,17 @@ def _stream_update_from(u) -> StreamUpdate:
)


def _cstr(value: str, name: str) -> bytes:
"""UTF-8 bytes for a C string option; a NUL would silently cut it in C."""
if "\x00" in value:
raise InvalidArgument(f"{name} contains a NUL character")
return value.encode("utf-8")


def _build_run_params(task, language, target_language, timestamps,
keep_special_tags, spec_k_drafts, diarize="default",
pnc="default", itn="default"):
pnc="default", itn="default", vocabulary=None,
prompt=None, prefix=None):
if not isinstance(spec_k_drafts, int) or spec_k_drafts < -1:
raise InvalidArgument(
f"spec_k_drafts must be -1 (family default), 0 (disabled), or a "
Expand All @@ -668,10 +682,27 @@ def _build_run_params(task, language, target_language, timestamps,
params.pnc = _enum(_PNC, pnc, "pnc")
params.itn = _enum(_ITN, itn, "itn")
params.diarize = _enum(_DIARIZE, diarize, "diarize")
params.language = language.encode("utf-8") if language else None
params.target_language = target_language.encode("utf-8") if target_language else None
params.language = _cstr(language, "language") if language else None
params.target_language = _cstr(target_language, "target_language") if target_language else None
params.keep_special_tags = keep_special_tags
params.spec_k_drafts = spec_k_drafts
if vocabulary is not None:
if isinstance(vocabulary, (str, bytes)):
raise InvalidArgument("vocabulary must be a sequence of terms, not a single string")
if isinstance(vocabulary, (set, frozenset, dict)):
raise InvalidArgument("vocabulary is in priority order; pass a list, not a set or dict")
vocabulary = list(vocabulary) # an iterator would be used up by the check below
if not all(isinstance(t, str) for t in vocabulary):
raise InvalidArgument("vocabulary terms must be strings")
terms = [_cstr(t, "vocabulary") for t in vocabulary]
if terms:
arr = (ctypes.c_char_p * len(terms))(*terms)
params.vocabulary = ctypes.cast(arr, ctypes.POINTER(ctypes.c_char_p))
params.n_vocabulary = len(terms)
# The C struct holds raw pointers; keep the buffers alive with it.
params._prompting_keepalive = (arr, terms)
params.prompt = _cstr(prompt, "prompt") if prompt else None
params.prefix = _cstr(prefix, "prefix") if prefix else None
return params


Expand Down Expand Up @@ -740,7 +771,7 @@ def __init__(self, *, initial_prompt: str | None = None,

def _apply(self, ext) -> None:
if self.initial_prompt is not None:
ext.initial_prompt = self.initial_prompt.encode("utf-8")
ext.initial_prompt = _cstr(self.initial_prompt, "initial_prompt")
if self.condition_on_prev_tokens is not None:
ext.condition_on_prev_tokens = self.condition_on_prev_tokens
if self.temperature is not None:
Expand Down Expand Up @@ -981,8 +1012,7 @@ def capabilities(self) -> Capabilities:
)

def supports(self, feature: Feature) -> bool:
"""Whether the model exposes a behavioral feature (initial prompt,
temperature fallback, long-form, cancellation, pnc, itn)."""
"""Whether the model exposes a behavioral feature (see ``Feature``)."""
return bool(_lib.transcribe_model_supports(
self._h, _enum(_FEATURES, feature, "feature")))

Expand Down Expand Up @@ -1103,7 +1133,10 @@ def run(self, pcm: PCMLike, *, task: Task = "transcribe",
diarize: Diarize = "default",
keep_special_tags: bool = False,
spec_k_drafts: int = -1,
family: FamilyExtension | None = None) -> Result:
family: FamilyExtension | None = None,
vocabulary: Sequence[str] | None = None,
prompt: str | None = None,
prefix: str | None = None) -> Result:
"""Transcribe 16 kHz mono float32 PCM and return a materialized Result.

``pnc`` controls punctuation/capitalization and ``itn`` controls
Expand All @@ -1113,14 +1146,23 @@ def run(self, pcm: PCMLike, *, task: Task = "transcribe",
``spec_k_drafts`` tunes speculative decoding on models whose
capabilities advertise ``supports_spec_decode`` (-1 = family default,
0 = disabled, >0 = draft length; silently ignored elsewhere).
``vocabulary`` (terms, priority order), ``prompt`` and ``prefix`` are
the generic prompting inputs; probe ``model.supports()`` for
``"vocabulary"``, ``"context_prompt"``, ``"instruct"`` and
``"transcript_prefix"``. With ``task="instruct"`` the ``prompt`` is the
required instruction and the output is free text. Unsupported
vocabulary/context is ignored with a warning; an unsupported prefix
or instruct task raises. With ``prefix``, ``text`` holds only the
continuation and ``raw_text`` leads with the prefix.

On ``Aborted`` (via :meth:`cancel`) and ``OutputTruncated`` (including
its ``OutputRepetition`` subclass) the partial transcript is preserved
and attached to the exception as ``partial_result``."""
self._cancel.clear()
array, n_samples = _pcm_to_carray(pcm)
params = _build_run_params(task, language, target_language, timestamps,
keep_special_tags, spec_k_drafts, diarize, pnc, itn)
keep_special_tags, spec_k_drafts, diarize, pnc, itn,
vocabulary, prompt, prefix)
ext = self._resolve_family(family, "run") if family is not None else None
if ext is not None:
params.family = ctypes.cast(
Expand All @@ -1146,7 +1188,9 @@ def run_batch(self, pcms: Sequence[PCMLike], *, task: Task = "transcribe",
keep_special_tags: bool = False,
spec_k_drafts: int = -1,
family: FamilyExtension | None = None,
return_exceptions: bool = False) -> list[Result | TranscribeError]:
return_exceptions: bool = False,
vocabulary: Sequence[str] | None = None,
prompt: str | None = None) -> list[Result | TranscribeError]:
"""Transcribe several utterances in one dispatch — one Result each.

Families with a batched compute path process every utterance in a single
Expand All @@ -1163,7 +1207,10 @@ def run_batch(self, pcms: Sequence[PCMLike], *, task: Task = "transcribe",
view (``Result`` or ``TranscribeError`` each) so completed work is
never discarded. With ``return_exceptions=True`` no exception is
raised for utterance failures and that mixed list is returned
directly (the ``asyncio.gather`` convention)."""
directly (the ``asyncio.gather`` convention).

``vocabulary`` / ``prompt`` apply to every utterance (see :meth:`run`);
a transcript prefix is per-utterance and so is not accepted here."""
self._cancel.clear()
pcms = list(pcms)
if not pcms:
Expand All @@ -1179,7 +1226,8 @@ def run_batch(self, pcms: Sequence[PCMLike], *, task: Task = "transcribe",
counts[k] = n

params = _build_run_params(task, language, target_language, timestamps,
keep_special_tags, spec_k_drafts, diarize, pnc, itn)
keep_special_tags, spec_k_drafts, diarize, pnc, itn,
vocabulary, prompt)
ext = self._resolve_family(family, "run") if family is not None else None
if ext is not None:
params.family = ctypes.cast(
Expand Down Expand Up @@ -1234,19 +1282,24 @@ def stream(self, *, task: Task = "transcribe", language: str | None = None,
diarize: Diarize = "default",
keep_special_tags: bool = False, commit_policy: CommitPolicy = "auto",
stable_prefix_agreement_n: int = 0,
family: FamilyExtension | None = None) -> Stream:
family: FamilyExtension | None = None,
vocabulary: Sequence[str] | None = None,
prompt: str | None = None) -> Stream:
"""Begin streaming on this session and return a Stream to feed audio to.

Requires a model whose capabilities advertise ``supports_streaming``;
otherwise raises NotImplementedByModel. ``family`` is an optional
family-specific stream extension (e.g. MoonshineStreamingOptions). The
session is single-threaded and runs at most one stream at a time. Use
the Stream as a context manager so it is reset when you are done."""
the Stream as a context manager so it is reset when you are done.
``vocabulary`` and ``prompt`` are as in :meth:`run`; ``task="instruct"``
raises."""
self._cancel.clear()
# spec_k_drafts is an offline-decode knob; streaming always uses the
# family default (-1).
run_params = _build_run_params(task, language, target_language, timestamps,
keep_special_tags, -1, diarize, pnc, itn)
keep_special_tags, -1, diarize, pnc, itn,
vocabulary, prompt)
sp = _StreamParams()
_lib.transcribe_stream_params_init(_byref(sp))
sp.commit_policy = _enum(_COMMIT_POLICIES, commit_policy, "commit_policy")
Expand Down Expand Up @@ -1499,6 +1552,9 @@ def transcribe(
keep_special_tags: bool = False,
spec_k_drafts: int = -1,
family: FamilyExtension | None = None,
vocabulary: Sequence[str] | None = None,
prompt: str | None = None,
prefix: str | None = None,
) -> Result:
"""Transcribe *pcm* in one call and return a materialized Result.

Expand All @@ -1507,13 +1563,15 @@ def transcribe(
many clips keep a Model and call ``model.session().run(...)`` yourself; this
helper is for the one-shot case. ``backend`` / ``device`` apply only when
*model* is a path — they are ignored when an already-loaded Model is passed.
``family`` / ``spec_k_drafts`` pass through to :meth:`Session.run`.
``family`` / ``spec_k_drafts`` and the prompting inputs (``vocabulary``,
``prompt``, ``prefix``) pass through to :meth:`Session.run`.
"""
session_opts = dict(n_threads=n_threads, kv_type=kv_type, n_ctx=n_ctx)
run_opts = dict(task=task, language=language, target_language=target_language,
timestamps=timestamps, pnc=pnc, itn=itn, diarize=diarize,
keep_special_tags=keep_special_tags,
spec_k_drafts=spec_k_drafts, family=family)
spec_k_drafts=spec_k_drafts, family=family,
vocabulary=vocabulary, prompt=prompt, prefix=prefix)

if isinstance(model, Model):
with model.session(**session_opts) as session:
Expand Down
11 changes: 8 additions & 3 deletions bindings/python/src/transcribe_cpp/_generated.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
# Stable digest of the ABI surface below (structs, enums, macros, layout,
# prototypes). A native provider package echoes this back so the API
# package can reject an ABI-mismatched provider before dlopen.
PUBLIC_HEADER_HASH = "9866413f80138057"
PUBLIC_HEADER_HASH = "59b9a92b47074666"

# === enum constants ===
TRANSCRIBE_OK = 0
Expand Down Expand Up @@ -59,6 +59,7 @@
TRANSCRIBE_LOG_LEVEL_CONT = 5
TRANSCRIBE_TASK_TRANSCRIBE = 0
TRANSCRIBE_TASK_TRANSLATE = 1
TRANSCRIBE_TASK_INSTRUCT = 2
TRANSCRIBE_TIMESTAMPS_NONE = 0
TRANSCRIBE_TIMESTAMPS_AUTO = 1
TRANSCRIBE_TIMESTAMPS_SEGMENT = 2
Expand Down Expand Up @@ -96,6 +97,10 @@
TRANSCRIBE_FEATURE_PNC = 4
TRANSCRIBE_FEATURE_ITN = 5
TRANSCRIBE_FEATURE_DIARIZATION = 6
TRANSCRIBE_FEATURE_VOCABULARY = 7
TRANSCRIBE_FEATURE_CONTEXT_PROMPT = 8
TRANSCRIBE_FEATURE_INSTRUCT = 9
TRANSCRIBE_FEATURE_TRANSCRIPT_PREFIX = 10
TRANSCRIBE_STREAM_IDLE = 0
TRANSCRIBE_STREAM_ACTIVE = 1
TRANSCRIBE_STREAM_FINISHED = 2
Expand Down Expand Up @@ -168,7 +173,7 @@ class transcribe_whisper_chunk_trace(_c.Structure):
transcribe_device_info._fields_ = [("struct_size", _c.c_uint64), ("name", _c.c_char_p), ("description", _c.c_char_p), ("kind", _c.c_char_p), ("device_id", _c.c_char_p), ("memory_total", _c.c_uint64), ("memory_free", _c.c_uint64), ("device_type", _c.c_int)]
transcribe_model_load_params._fields_ = [("struct_size", _c.c_uint64), ("backend", _c.c_int), ("device", _c.c_void_p)]
transcribe_session_params._fields_ = [("struct_size", _c.c_uint64), ("n_threads", _c.c_int), ("kv_type", _c.c_int), ("n_ctx", _c.c_int32)]
transcribe_run_params._fields_ = [("struct_size", _c.c_uint64), ("task", _c.c_int), ("timestamps", _c.c_int), ("pnc", _c.c_int), ("itn", _c.c_int), ("diarize", _c.c_int), ("language", _c.c_char_p), ("target_language", _c.c_char_p), ("keep_special_tags", _c.c_bool), ("family", _c.POINTER(transcribe_ext)), ("spec_k_drafts", _c.c_int32)]
transcribe_run_params._fields_ = [("struct_size", _c.c_uint64), ("task", _c.c_int), ("timestamps", _c.c_int), ("pnc", _c.c_int), ("itn", _c.c_int), ("diarize", _c.c_int), ("language", _c.c_char_p), ("target_language", _c.c_char_p), ("keep_special_tags", _c.c_bool), ("family", _c.POINTER(transcribe_ext)), ("spec_k_drafts", _c.c_int32), ("vocabulary", _c.POINTER(_c.c_char_p)), ("n_vocabulary", _c.c_int32), ("prompt", _c.c_char_p), ("prefix", _c.c_char_p)]
transcribe_capabilities._fields_ = [("struct_size", _c.c_uint64), ("native_sample_rate", _c.c_int32), ("n_languages", _c.c_int), ("languages", _c.POINTER(_c.c_char_p)), ("max_timestamp_kind", _c.c_int), ("supports_language_detect", _c.c_bool), ("supports_translate", _c.c_bool), ("supports_streaming", _c.c_bool), ("supports_spec_decode", _c.c_bool), ("max_audio_ms", _c.c_int64), ("n_translate_target_languages", _c.c_int), ("translate_target_languages", _c.POINTER(_c.c_char_p))]
transcribe_session_limits._fields_ = [("struct_size", _c.c_uint64), ("effective_n_ctx", _c.c_int32), ("effective_max_audio_ms", _c.c_int64), ("max_kv_bytes", _c.c_int64)]
transcribe_stream_params._fields_ = [("struct_size", _c.c_uint64), ("family", _c.POINTER(transcribe_ext)), ("commit_policy", _c.c_int), ("stable_prefix_agreement_n", _c.c_uint32)]
Expand Down Expand Up @@ -213,7 +218,7 @@ class transcribe_whisper_chunk_trace(_c.Structure):
'transcribe_device_info': {'size': 64, 'align': 8, 'offsets': {'struct_size': 0, 'name': 8, 'description': 16, 'kind': 24, 'device_id': 32, 'memory_total': 40, 'memory_free': 48, 'device_type': 56}},
'transcribe_model_load_params': {'size': 24, 'align': 8, 'offsets': {'struct_size': 0, 'backend': 8, 'device': 16}},
'transcribe_session_params': {'size': 24, 'align': 8, 'offsets': {'struct_size': 0, 'n_threads': 8, 'kv_type': 12, 'n_ctx': 16}},
'transcribe_run_params': {'size': 72, 'align': 8, 'offsets': {'struct_size': 0, 'task': 8, 'timestamps': 12, 'pnc': 16, 'itn': 20, 'diarize': 24, 'language': 32, 'target_language': 40, 'keep_special_tags': 48, 'family': 56, 'spec_k_drafts': 64}},
'transcribe_run_params': {'size': 104, 'align': 8, 'offsets': {'struct_size': 0, 'task': 8, 'timestamps': 12, 'pnc': 16, 'itn': 20, 'diarize': 24, 'language': 32, 'target_language': 40, 'keep_special_tags': 48, 'family': 56, 'spec_k_drafts': 64, 'vocabulary': 72, 'n_vocabulary': 80, 'prompt': 88, 'prefix': 96}},
'transcribe_capabilities': {'size': 56, 'align': 8, 'offsets': {'struct_size': 0, 'native_sample_rate': 8, 'n_languages': 12, 'languages': 16, 'max_timestamp_kind': 24, 'supports_language_detect': 28, 'supports_translate': 29, 'supports_streaming': 30, 'supports_spec_decode': 31, 'max_audio_ms': 32, 'n_translate_target_languages': 40, 'translate_target_languages': 48}},
'transcribe_session_limits': {'size': 32, 'align': 8, 'offsets': {'struct_size': 0, 'effective_n_ctx': 8, 'effective_max_audio_ms': 16, 'max_kv_bytes': 24}},
'transcribe_stream_params': {'size': 24, 'align': 8, 'offsets': {'struct_size': 0, 'family': 8, 'commit_policy': 16, 'stable_prefix_agreement_n': 20}},
Expand Down
Loading
Loading