transcribe-cpp
Safe, idiomatic Rust bindings for transcribe.cpp, a C/C++ speech-to-text library built on ggml.
Status: in development (0.2.0). Core model, session, run, stream, cancellation, backend, and family-extension APIs are implemented and tested.
Upgrading from 0.1? See the
0.2 migration guide,
including the replacement of ModelOptions::gpu_device with exact Device
handles.
Install
The native library is compiled from source by the transcribe-cpp-sys crate, so
a first build needs a C++ toolchain and CMake. There is no external
compression dependency (the deflate codec is vendored), so no system zlib /
vcpkg setup is required on any platform. No prebuilt download and no environment
variables on the happy path.
Quickstart
use ;
let mut session = load?.session?;
// pcm: 16 kHz mono f32 in [-1, 1]
let result = session.run?;
println!;
# Ok::
Punctuation, capitalization, and text normalization
RunOptions::pnc and RunOptions::itn default to preserving each model
family's shipped behavior. Probe model.supports(Feature::Pnc) or
Feature::Itn before selecting Pnc::Off/On or Itn::Off/On. The same
RunOptions is used by single runs, batches, streams, and transcribe().
use ;
let options = RunOptions ;
let result = session.run?;
# Ok::
Prompting
RunOptions::vocabulary (custom terms), prompt (context, or the instruction
under Task::Instruct) and prefix (text the model continues from) take
effect where model.supports() reports Feature::Vocabulary,
ContextPrompt, Instruct or TranscriptPrefix.
use RunOptions;
let options = RunOptions ;
let result = session.run?;
# Ok::
Streaming exposes both UI-stable text and a fully materialized structured snapshot:
let mut stream = session.stream?;
stream.feed?;
println!;
stream.finalize?;
let transcript = stream.snapshot; // language, segments, words, tokens, timings
# Ok::
Diarization (who spoke when)
A model whose roles() contain Role::Diarize (e.g. Sortformer) opens a
DiarizeSession that returns speaker turns; calls for a role the model lacks
return Error::UnsupportedRole.
use ;
let model = load?;
assert!;
let mut diarize = model.diarize_session?;
for turn in diarize.run?
# Ok::
Language ID
A model whose roles() contain Role::LangId (VoxLingua107 ECAPA-TDNN)
opens a LangIdSession. Codes are the model's own labels; match them against
an ASR model's capabilities().languages yourself. allowed: None scores
every label, Some(vec![]) is Error::InvalidArgument, and clips under
langid_info()?.min_audio_ms are Error::InputTooShort. A low
allowed_mass means the speech is probably outside the allowed set.
use ;
let model = load?;
let mut lid = model.langid_session?;
let opts = LangIdOptions ;
let result = lid.run?;
println!;
# Ok::
Voice activity detection
A model whose roles() contain Role::Vad (Silero VAD; GGUF or the
whisper.cpp ggml-silero-v5.1.2.bin / v6.2.0.bin) opens a VadSession.
run scores a clip and returns per-frame probabilities
(vad_info()?.frame_samples samples each) plus speech segments in samples;
stream_feed / stream_flush return the probabilities of the frames each call
completed.
use ;
let model = load?;
let mut vad = model.vad_session?;
for s in vad.run?.segments
for chunk in pcm.chunks
vad.stream_flush?;
# Ok::
Runnable examples:
The raw FFI layer is
transcribe-cpp-sys; this crate
is the safe wrapper.
Backends
Backends are selected with cargo features forwarded to transcribe-cpp-sys:
metal (default on Apple), vulkan, cuda, rocm, and openmp.
On Windows, vulkan requires the Vulkan SDK. Deep Cargo output paths are
shortened automatically during the native build; see the
Windows Vulkan build notes
for prerequisites and the short CARGO_TARGET_DIR fallback.
The default link is static and self-contained. Advanced packaging modes are
available through shared and dynamic-backends; see the transcribe-cpp-sys
README if you need runtime-loaded backend modules or custom
TRANSCRIBE_CMAKE_ARGS.
Exact device selection
devices() returns process-local Device handles. Leave
ModelOptions::device as None for the backend's automatic policy, or pass
Some(device) to select that exact primary device with no fallback. Handles
are not stable across processes; persist device_id when it is Some,
otherwise kind + name + description (e.g. Metal), and later re-find the
device in devices() after backend initialization. In dynamic-backend builds,
finish init_backends() or init_backends_default() before any thread
enumerates devices, queries backend availability, or loads a model; native
registry mutation is a startup-only operation and must not race those calls.
Memory-mapped weights
Where the device can use host memory (the CPU, Metal on Apple silicon), model
weights are memory-mapped from the file and served from the page cache, which
the OS can reclaim, instead of being copied into process memory. Don't
truncate or rewrite a model file in place while it is loaded; write a new file
and rename it over the old one. Set ModelOptions::no_mmap (or the
TRANSCRIBE_NO_MMAP=1 environment variable) to copy the weights instead.
Packaging a distributable (shared / dynamic-backends)
With the default static build there is nothing to do — the native code is
baked into your binary. But if you enable shared or dynamic-backends, your
installer must ship the runtime libraries (and, for dynamic-backends, the
backend modules) next to your executable. Bundling for distribution (e.g. a
Tauri/Electron installer) happens at build time, so a runtime lookup is the
wrong tool — you need the artifact path while you build.
This crate forwards the native build's output directories to your build
script as DEP_TRANSCRIBE_CPP_* env vars. The one you usually want is
DEP_TRANSCRIBE_CPP_RUNTIME_DIR: a single directory holding the shared objects
to copy beside your exe (the DLLs on Windows, the .so/.dylib on Unix — no
per-OS logic needed). Also available: DEP_TRANSCRIBE_CPP_LIB_DIR,
_BIN_DIR, _MODULE_DIR (the dynamic-backends modules), and _INCLUDE_DIR.
The runtime keys are present only in a shared posture; in a static build they
are unset (nothing to bundle).
In your application's build.rs:
use ;
Threading
-
ModelisSend + Syncand cheap to clone (Arc-backed); the native model is freed only after the last handle and everySessiondrop. -
SessionisSendbut notSync; mutating calls take&mut self. -
In 0.x the C library allows at most one in-flight run across all sessions of a model; this crate enforces it with a per-model mutex, so concurrent calls queue rather than race.
DiarizeSessions share the same lock. For real parallelism, use oneModelper worker. -
License: MIT