Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
28 changes: 20 additions & 8 deletions bindings/python/src/transcribe_cpp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
CommitPolicy = Literal["auto", "on_finalize", "stable_prefix"]
Feature = Literal[
"initial_prompt", "temperature_fallback", "long_form",
"cancellation", "pnc", "itn", "diarization",
"cancellation", "pnc", "itn", "diarization", "hotwords",
]

__all__ = [
Expand Down Expand Up @@ -233,6 +233,7 @@
"pnc": _generated.TRANSCRIBE_FEATURE_PNC,
"itn": _generated.TRANSCRIBE_FEATURE_ITN,
"diarization": _generated.TRANSCRIBE_FEATURE_DIARIZATION,
"hotwords": _generated.TRANSCRIBE_FEATURE_HOTWORDS,
}


Expand Down Expand Up @@ -621,7 +622,8 @@ def _stream_update_from(u) -> StreamUpdate:


def _build_run_params(task, language, target_language, timestamps,
keep_special_tags, spec_k_drafts, diarize="default"):
keep_special_tags, spec_k_drafts, diarize="default",
hotwords=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 @@ -636,6 +638,7 @@ def _build_run_params(task, language, target_language, timestamps,
params.target_language = target_language.encode("utf-8") if target_language else None
params.keep_special_tags = keep_special_tags
params.spec_k_drafts = spec_k_drafts
params.hotwords = hotwords.encode("utf-8") if hotwords else None
return params


Expand Down Expand Up @@ -1059,6 +1062,7 @@ def run(self, pcm: PCMLike, *, task: Task = "transcribe",
diarize: Diarize = "default",
keep_special_tags: bool = False,
spec_k_drafts: int = -1,
hotwords: str | None = None,
family: FamilyExtension | None = None) -> Result:
"""Transcribe 16 kHz mono float32 PCM and return a materialized Result.

Expand All @@ -1067,14 +1071,18 @@ 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).
``hotwords`` is an optional comma-joined keyword-biasing hint honored
by models whose ``supports("hotwords")`` is true (e.g. moss, granite
AR); silently ignored elsewhere. None/empty means no hint.

On ``Aborted`` (via :meth:`cancel`) and ``OutputTruncated`` 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)
keep_special_tags, spec_k_drafts, diarize,
hotwords)
ext = self._resolve_family(family, "run") if family is not None else None
if ext is not None:
params.family = ctypes.cast(
Expand All @@ -1096,6 +1104,7 @@ def run_batch(self, pcms: Sequence[PCMLike], *, task: Task = "transcribe",
diarize: Diarize = "default",
keep_special_tags: bool = False,
spec_k_drafts: int = -1,
hotwords: str | None = None,
family: FamilyExtension | None = None,
return_exceptions: bool = False) -> list[Result | TranscribeError]:
"""Transcribe several utterances in one dispatch — one Result each.
Expand Down Expand Up @@ -1130,7 +1139,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)
keep_special_tags, spec_k_drafts, diarize,
hotwords)
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 @@ -1193,8 +1203,8 @@ def stream(self, *, task: Task = "transcribe", language: str | None = None,
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."""
self._cancel.clear()
# spec_k_drafts is an offline-decode knob; streaming always uses the
# family default (-1).
# spec_k_drafts and hotwords are offline-decode knobs; streaming always
# uses the family defaults (-1 / no hint).
run_params = _build_run_params(task, language, target_language, timestamps,
keep_special_tags, -1, diarize)
sp = _StreamParams()
Expand Down Expand Up @@ -1441,6 +1451,7 @@ def transcribe(
diarize: Diarize = "default",
keep_special_tags: bool = False,
spec_k_drafts: int = -1,
hotwords: str | None = None,
family: FamilyExtension | None = None,
) -> Result:
"""Transcribe *pcm* in one call and return a materialized Result.
Expand All @@ -1450,13 +1461,14 @@ def transcribe(
many clips keep a Model and call ``model.session().run(...)`` yourself; this
helper is for the one-shot case. ``backend`` / ``gpu_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`` / ``hotwords`` 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, diarize=diarize,
keep_special_tags=keep_special_tags,
spec_k_drafts=spec_k_drafts, family=family)
spec_k_drafts=spec_k_drafts, hotwords=hotwords, family=family)

if isinstance(model, Model):
with model.session(**session_opts) as session:
Expand Down
7 changes: 4 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 = "fb2e64791dcbb70a"
PUBLIC_HEADER_HASH = "4faee4159facd940"

# === enum constants ===
TRANSCRIBE_OK = 0
Expand Down Expand Up @@ -94,6 +94,7 @@
TRANSCRIBE_FEATURE_PNC = 4
TRANSCRIBE_FEATURE_ITN = 5
TRANSCRIBE_FEATURE_DIARIZATION = 6
TRANSCRIBE_FEATURE_HOTWORDS = 7
TRANSCRIBE_STREAM_IDLE = 0
TRANSCRIBE_STREAM_ACTIVE = 1
TRANSCRIBE_STREAM_FINISHED = 2
Expand Down Expand Up @@ -166,7 +167,7 @@ class transcribe_whisper_chunk_trace(_c.Structure):
transcribe_backend_device._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), ("gpu_device", _c.c_int)]
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), ("hotwords", _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 @@ -211,7 +212,7 @@ class transcribe_whisper_chunk_trace(_c.Structure):
'transcribe_backend_device': {'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': 16, 'align': 8, 'offsets': {'struct_size': 0, 'backend': 8, 'gpu_device': 12}},
'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': 80, '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, 'hotwords': 72}},
'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
2 changes: 1 addition & 1 deletion bindings/python/tests/test_family_ext.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ def test_run_batch_accepts_family(model_path, audio_pcm):
def test_supports_probe_all_features(model_path):
with t.Model(model_path) as model:
for feature in ("initial_prompt", "temperature_fallback", "long_form",
"cancellation", "pnc", "itn", "diarization"):
"cancellation", "pnc", "itn", "diarization", "hotwords"):
assert model.supports(feature) in (True, False)
with pytest.raises(t.InvalidArgument, match="unknown feature"):
model.supports("levitation")
Expand Down
10 changes: 7 additions & 3 deletions bindings/rust/sys/src/transcribe_sys.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
// @generated by `cargo xtask bindgen` from include/transcribe/extensions.h
// DO NOT EDIT BY HAND. Regenerate: `cargo xtask bindgen`.
// Pinned to include/transcribe.abihash = fb2e64791dcbb70a
// Pinned to include/transcribe.abihash = 4faee4159facd940

/// The public-ABI digest these bindings were generated against
/// (sha256/16 over the normalized FFI surface). The load-time version
/// gate and the CI drift check both anchor on this value.
pub const PUBLIC_HEADER_HASH: &str = "fb2e64791dcbb70a";
pub const PUBLIC_HEADER_HASH: &str = "4faee4159facd940";

/* automatically generated by rust-bindgen 0.72.1 */

Expand Down Expand Up @@ -340,10 +340,11 @@ pub struct transcribe_run_params {
pub keep_special_tags: bool,
pub family: *const transcribe_ext,
pub spec_k_drafts: i32,
pub hotwords: *const ::std::os::raw::c_char,
}
#[allow(clippy::unnecessary_operation, clippy::identity_op)]
const _: () = {
["Size of transcribe_run_params"][::std::mem::size_of::<transcribe_run_params>() - 72usize];
["Size of transcribe_run_params"][::std::mem::size_of::<transcribe_run_params>() - 80usize];
["Alignment of transcribe_run_params"]
[::std::mem::align_of::<transcribe_run_params>() - 8usize];
["Offset of field: transcribe_run_params::struct_size"]
Expand All @@ -368,6 +369,8 @@ const _: () = {
[::std::mem::offset_of!(transcribe_run_params, family) - 56usize];
["Offset of field: transcribe_run_params::spec_k_drafts"]
[::std::mem::offset_of!(transcribe_run_params, spec_k_drafts) - 64usize];
["Offset of field: transcribe_run_params::hotwords"]
[::std::mem::offset_of!(transcribe_run_params, hotwords) - 72usize];
};
unsafe extern "C" {
pub fn transcribe_run_params_init(params: *mut transcribe_run_params);
Expand Down Expand Up @@ -435,6 +438,7 @@ impl transcribe_feature {
pub const TRANSCRIBE_FEATURE_PNC: transcribe_feature = transcribe_feature(4);
pub const TRANSCRIBE_FEATURE_ITN: transcribe_feature = transcribe_feature(5);
pub const TRANSCRIBE_FEATURE_DIARIZATION: transcribe_feature = transcribe_feature(6);
pub const TRANSCRIBE_FEATURE_HOTWORDS: transcribe_feature = transcribe_feature(7);
}
#[repr(transparent)]
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
Expand Down
18 changes: 13 additions & 5 deletions bindings/rust/transcribe-cpp/src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ pub struct RunOptions {
pub keep_special_tags: bool,
/// Speculative-decode draft length. `-1` = family default, `0` = disabled.
pub spec_k_drafts: i32,
/// Comma-joined keyword-biasing hint. `None` = no hint; ignored by
/// families without hotword support.
pub hotwords: Option<String>,
/// Optional family-specific run extension (e.g. whisper decode knobs).
pub family: Option<RunExtension>,
}
Expand All @@ -57,6 +60,7 @@ impl Default for RunOptions {
target_language: None,
keep_special_tags: false,
spec_k_drafts: -1,
hotwords: None,
family: None,
}
}
Expand Down Expand Up @@ -148,7 +152,7 @@ impl Session {
/// On an aborted or truncated decode the partial transcript is preserved
/// on the returned [`Error::Aborted`] / [`Error::OutputTruncated`].
pub fn run(&mut self, pcm: &[f32], options: &RunOptions) -> Result<Transcript> {
let (params, _lang, _target, _family) = build_run_params(options)?;
let (params, _lang, _target, _hotwords, _family) = build_run_params(options)?;
let n = clamp_len(pcm.len())?;

// The compute path is serialized per model; hold the lock for the native
Expand Down Expand Up @@ -191,7 +195,7 @@ impl Session {
pcms: &[&[f32]],
options: &RunOptions,
) -> Result<Vec<Result<Transcript>>> {
let (params, _lang, _target, _family) = build_run_params(options)?;
let (params, _lang, _target, _hotwords, _family) = build_run_params(options)?;
let ptrs: Vec<*const f32> = pcms.iter().map(|p| p.as_ptr()).collect();
let lens: Vec<i32> = pcms
.iter()
Expand Down Expand Up @@ -267,7 +271,7 @@ impl Session {
/// Dropping the returned `Stream` abandons it and returns the session to
/// idle.
pub fn stream(&mut self, run: &RunOptions, stream: &StreamOptions) -> Result<Stream<'_>> {
let (run_params, _lang, _target, _family) = build_run_params(run)?;
let (run_params, _lang, _target, _hotwords, _family) = build_run_params(run)?;
let (stream_params, _stream_family) = build_stream_params(stream);
{
// Claim the model's compute lease for the whole stream lifetime: a
Expand Down Expand Up @@ -417,11 +421,13 @@ impl Session {
}

/// Everything that must outlive a `transcribe_run` call: the params struct
/// plus the heap buffers its pointers borrow (language strings, family ext).
/// plus the heap buffers its pointers borrow (language strings, hotwords,
/// family ext).
type RunParamsBundle = (
sys::transcribe_run_params,
Option<CString>,
Option<CString>,
Option<CString>,
Option<RunExtRaw>,
);

Expand All @@ -442,8 +448,10 @@ fn build_run_params(o: &RunOptions) -> Result<RunParamsBundle> {

let lang = o.language.as_deref().map(CString::new).transpose()?;
let target = o.target_language.as_deref().map(CString::new).transpose()?;
let hotwords = o.hotwords.as_deref().map(CString::new).transpose()?;
params.language = lang.as_ref().map_or(std::ptr::null(), |c| c.as_ptr());
params.target_language = target.as_ref().map_or(std::ptr::null(), |c| c.as_ptr());
params.hotwords = hotwords.as_ref().map_or(std::ptr::null(), |c| c.as_ptr());

let family = o
.family
Expand All @@ -452,7 +460,7 @@ fn build_run_params(o: &RunOptions) -> Result<RunParamsBundle> {
.transpose()?;
params.family = family.as_ref().map_or(std::ptr::null(), |f| f.ext_ptr());

Ok((params, lang, target, family))
Ok((params, lang, target, hotwords, family))
}

/// PCM/utterance lengths cross the ABI as `int`; reject anything that overflows.
Expand Down
3 changes: 3 additions & 0 deletions bindings/rust/transcribe-cpp/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,8 @@ pub enum Feature {
Itn,
/// Produces structured speaker attribution.
Diarization,
/// Honors the keyword-biasing hotwords hint.
Hotwords,
}

impl Feature {
Expand All @@ -221,6 +223,7 @@ impl Feature {
Feature::Pnc => F::TRANSCRIBE_FEATURE_PNC,
Feature::Itn => F::TRANSCRIBE_FEATURE_ITN,
Feature::Diarization => F::TRANSCRIBE_FEATURE_DIARIZATION,
Feature::Hotwords => F::TRANSCRIBE_FEATURE_HOTWORDS,
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion bindings/swift/Sources/TranscribeCpp/ABIHash.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import CTranscribe
extension Transcribe {
/// sha256/16 of the normalized public FFI surface, pinned to the value in
/// include/transcribe.abihash at the time this binding was last reviewed.
public static let pinnedHeaderHash = "fb2e64791dcbb70a"
public static let pinnedHeaderHash = "4faee4159facd940"

/// The public-ABI digest this binding was reviewed against (16 hex chars).
public static func headerHash() -> String { pinnedHeaderHash }
Expand Down
Loading