Use include/transcribe/extensions.h as the default header for generated
bindings. It includes transcribe.h plus every family extension header shipped
by the install, so generators see the full public surface in one translation
unit.
Use include/transcribe.h directly only for bindings that intentionally expose
the stable generic ABI and omit family-specific extension structs and telemetry.
The header split is source organization, not a separate library boundary:
family headers include transcribe.h, do not depend on each other, and are
flattened normally by C preprocessors, bindgen, CFFI API-mode builds, cgo
preambles, and Swift/ObjC module maps.
Three binding-neutral artifacts exist so no binding depends on another binding's generated files:
include/transcribe.abihash— the public-ABI digest (sha256/16 over the normalized FFI surface: structs, enums, macros, layout, prototypes). Emitted bybindings/python/_generate/generate.py(the hash oracle) and drift-gated in CI alongside_generated.py. Every first-class binding pins this value: when the header's ABI changes, the hash moves and the binding's CI goes red until its FFI layer is regenerated or consciously reviewed. Comment-only header edits do not move it.contract.json— stamped into every native artifact directory (_native/in provider wheels; the root of extractedtranscribe-native-<tuple>bundles):version,header_hash,backends,lane. A binding validatesversion(pre-1.0: exact base match) andheader_hash(must equal the hash its FFI layer was generated against) BEFORE dlopen. This is the same contract the Python provider enforces via_contract.py.lib/transcribe-link.json— installed bycmake --install(theTRANSCRIBE_INSTALLrules): the machine-readable link interface for non-CMake consumers building from source (the Rust-syscrate'sbuild.rs). Archive order, system libs, frameworks, flags, and — forGGML_BACKEND_DLinstalls —module_dir, the directory to hand totranscribe_init_backends(). Proven per push by the link-smoke CI lane, which compiles a toy C consumer from nothing but this manifest in both static and shared postures. The Rust-syscrate consumes it from two sources: its own vendored source build (the default), or — with theTRANSCRIBE_DIRenv var pointing at an installed prefix — a prebuilt tree, skipping the source build entirely.
Every accessor that returns a const char * (transcribe_full_text,
transcribe_detected_language, and the text field of the segment / word /
token row structs) returns a borrowed pointer into session-owned storage. See
the "Result text-pointer lifetime" block at the top of include/transcribe.h
for the full contract. The binding-relevant rule:
- Offline path: the pointer is valid until the next
transcribe_run/transcribe_stream_begin/transcribe_stream_reset/transcribe_session_freeon the same session. - Streaming path: raw result pointers, including
transcribe_full_textand rowtextfields, may be replaced by everytranscribe_stream_feed/transcribe_stream_finalizecall. Bindings that need UI-stable streaming text should usetranscribe_stream_get_text()and expose owned copies ofcommitted_textandtentative_text.
The simplest safe rule for a binding is to copy the bytes at the FFI
boundary, which the marshal-and-copy idioms each language already uses make
automatic: Python ctypes.c_char_p / .decode(), Go C.GoString, Rust
CStr::to_str().to_owned(), Swift String(cString:). A binding that instead
hands out a zero-copy view must scope it to the current callback/update turn
and document that it dies at the next stream mutation.
First-class bindings expose both streaming result shapes as owned values:
- the UI-facing
transcribe_stream_get_text()view (full, committed, and tentative text), and - a full structured snapshot built from the ordinary current-result accessors (clean/raw text, detected language, timestamp kind, segments, speaker turns, words, tokens, and timings).
The structured snapshot must be copied completely before the next feed or finalize call; bindings must not return the session-owned pointers directly.
First-class bindings expose the generic diarization surface rather than only the generated FFI:
- a run option with
default/off/onvalues (defaultis globally off), - the
diarizationfeature probe, speaker_id/speakerIdon every segment (1-based, 0 = unattributed), and- speaker-turn rows with start/end milliseconds, speaker id, and confidence.
Speaker-turn times may both be zero when a model attributes text but does not
produce timing (Granite SAA). Timestamp selection is an independent axis:
explicit unsupported combinations return UNSUPPORTED_TIMESTAMPS; AUTO
chooses the richest granularity compatible with the selected task. Result
objects own copies of every row and remain valid after the next run.
Every first-class binding exposes raw_text / rawText on the materialized
result alongside the clean text: the model's decoded output before family
post-processing (diarization markers, timestamp/special tokens, tag filtering,
whitespace trims). It equals the clean text modulo whitespace for families
that emit clean text natively, and is the recommended replacement for
keep_special_tags when the goal is recovering what the model emitted —
unlike the flag it works for every family, covers plain-text markers, and does
not give up the clean transcript. It is present on single, batch, and full
structured stream snapshots (and may be empty before a stream has produced a
successful hypothesis).
First-class high-level bindings expose every field of the generic model-load,
session, run, and stream parameter structs. Generated/raw FFI coverage is not
sufficient: an application must not need private binding internals to set a
public generic option. In particular, transcribe_run_params::pnc and itn
are typed three-state controls (default, off, on) and must flow through
single-run, batch, streaming, and one-shot convenience surfaces wherever those
surfaces exist.
Every generic option needs model-free enum/type coverage (plus direct materialization coverage where the binding architecture permits it) and, when a supporting model exists, a model-gated behavior test. A new field added to a generic parameter struct must update all first-class bindings and this conformance coverage in the same change.
When adding a new family extension, update:
include/transcribe/<family>.hwith the typed struct, kind constant, andtranscribe_<family>_<name>_ext_init()function declaration.include/transcribe/extensions.hwith one new include.docs/extension-kinds.mdwith the registered FourCC value and the slot (RUNorSTREAM) the kind is legal on.