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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ __pycache__/

# WER evaluation data + generated working reports.
/samples/wer/
# Diarization eval corpora (AMI audio + fetched RTTMs); regenerable via
# scripts/diar/ingest_ami.py + fetch_ami_forced_alignment.py. The committed
# oracle case lives at samples/sortformer-2spk-mix.wav (outside samples/diar/).
/samples/diar/
/reports/*

# Reviewable porting evidence (intake, porting log) is intentionally committed.
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ C/C++ speech-to-text inference library. Runs diverse STT model families via [GGU
| Voxtral Realtime | `voxtral-mini-4b-realtime-2602` (streaming audio-LLM) | [docs/models/voxtral-realtime.md](docs/models/voxtral-realtime.md) |
| MedASR | `medasr` (Conformer + CTC, English medical-dictation, gated) | [docs/models/medasr.md](docs/models/medasr.md) |
| MOSS Transcribe-Diarize | `moss-transcribe-diarize` (audio-LLM; English + Chinese ASR with inline speaker diarization) | [docs/models/moss-transcribe-diarize.md](docs/models/moss-transcribe-diarize.md) |
| Sortformer | `diar_streaming_sortformer_4spk-v2.1` (streaming speaker diarizer, up to 4 speakers; no transcription) | [docs/models/diar_streaming_sortformer_4spk-v2.1.md](docs/models/diar_streaming_sortformer_4spk-v2.1.md) |

Per-variant model cards live under [`docs/models/`](docs/models/).

Expand Down
37 changes: 37 additions & 0 deletions bindings/python/src/transcribe_cpp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
Task = Literal["transcribe", "translate"]
Timestamps = Literal["none", "auto", "segment", "word", "token"]
Diarize = Literal["default", "off", "on"]
SortformerPreset = Literal["default", "very_high_latency", "high_latency", "low_latency"]
CommitPolicy = Literal["auto", "on_finalize", "stable_prefix"]
Feature = Literal[
"initial_prompt", "temperature_fallback", "long_form",
Expand Down Expand Up @@ -81,8 +82,10 @@
"MoonshineStreamingOptions",
"ParakeetStreamOptions",
"ParakeetBufferedStreamOptions",
"SortformerStreamOptions",
"VoxtralRealtimeStreamOptions",
"Backend",
"SortformerPreset",
"KVType",
"Task",
"Timestamps",
Expand Down Expand Up @@ -799,6 +802,40 @@ def _apply(self, ext) -> None:
ext.min_decode_interval_ms = self.min_decode_interval_ms


class SortformerStreamOptions(FamilyExtension):
"""Sortformer streaming operating-point options (run slot).

Sortformer is a diarizer: a run produces speaker segments, no text.
``preset`` selects the latency / accuracy trade-off from the model's
published menu; ``"default"`` keeps the GGUF-shipped checkpoint
configuration. ``"very_high_latency"`` (~30 s lookahead) is the
offline-file operating point; ``"low_latency"`` (~1 s) is the
real-time point and costs substantially more compute per audio
second."""

_slot = "run"
_kind = _generated.TRANSCRIBE_EXT_KIND_SORTFORMER_STREAM
_struct = _generated.transcribe_sortformer_stream_ext
_init = "transcribe_sortformer_stream_ext_init"

_presets = {
"default": _generated.TRANSCRIBE_SORTFORMER_PRESET_DEFAULT,
"very_high_latency": _generated.TRANSCRIBE_SORTFORMER_PRESET_VERY_HIGH_LATENCY,
"high_latency": _generated.TRANSCRIBE_SORTFORMER_PRESET_HIGH_LATENCY,
"low_latency": _generated.TRANSCRIBE_SORTFORMER_PRESET_LOW_LATENCY,
}

def __init__(self, *, preset: SortformerPreset | None = None):
if preset is not None and preset not in self._presets:
raise ValueError(f"unknown sortformer preset {preset!r}; "
f"expected one of {sorted(self._presets)}")
self.preset = preset

def _apply(self, ext) -> None:
if self.preset is not None:
ext.preset = self._presets[self.preset]


# --- high-level handles ---------------------------------------------------


Expand Down
13 changes: 12 additions & 1 deletion 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 = "d67a9bd78b964445"
PUBLIC_HEADER_HASH = "fb2e64791dcbb70a"

# === enum constants ===
TRANSCRIBE_OK = 0
Expand Down Expand Up @@ -101,13 +101,18 @@
TRANSCRIBE_STREAM_COMMIT_AUTO = 0
TRANSCRIBE_STREAM_COMMIT_ON_FINALIZE = 1
TRANSCRIBE_STREAM_COMMIT_STABLE_PREFIX = 2
TRANSCRIBE_SORTFORMER_PRESET_DEFAULT = 0
TRANSCRIBE_SORTFORMER_PRESET_VERY_HIGH_LATENCY = 1
TRANSCRIBE_SORTFORMER_PRESET_HIGH_LATENCY = 2
TRANSCRIBE_SORTFORMER_PRESET_LOW_LATENCY = 3
TRANSCRIBE_WHISPER_PROMPT_FIRST_SEGMENT = 0
TRANSCRIBE_WHISPER_PROMPT_ALL_SEGMENTS = 1

# === macro constants (integer object-like macros) ===
TRANSCRIBE_EXT_KIND_MOONSHINE_STREAMING_STREAM = 1414746957
TRANSCRIBE_EXT_KIND_PARAKEET_BUFFERED_STREAM = 1396853584
TRANSCRIBE_EXT_KIND_PARAKEET_STREAM = 1414744912
TRANSCRIBE_EXT_KIND_SORTFORMER_STREAM = 1414743635
TRANSCRIBE_EXT_KIND_VOXTRAL_REALTIME_STREAM = 1414746710
TRANSCRIBE_EXT_KIND_WHISPER_RUN = 1314015319

Expand Down Expand Up @@ -148,6 +153,8 @@ class transcribe_parakeet_stream_ext(_c.Structure):
pass
class transcribe_parakeet_buffered_stream_ext(_c.Structure):
pass
class transcribe_sortformer_stream_ext(_c.Structure):
pass
class transcribe_voxtral_realtime_stream_ext(_c.Structure):
pass
class transcribe_whisper_run_ext(_c.Structure):
Expand All @@ -173,6 +180,7 @@ class transcribe_whisper_chunk_trace(_c.Structure):
transcribe_moonshine_streaming_stream_ext._fields_ = [("ext", transcribe_ext), ("min_decode_interval_ms", _c.c_int32)]
transcribe_parakeet_stream_ext._fields_ = [("ext", transcribe_ext), ("att_context_right", _c.c_int32)]
transcribe_parakeet_buffered_stream_ext._fields_ = [("ext", transcribe_ext), ("left_ms", _c.c_int32), ("chunk_ms", _c.c_int32), ("right_ms", _c.c_int32)]
transcribe_sortformer_stream_ext._fields_ = [("ext", transcribe_ext), ("preset", _c.c_int)]
transcribe_voxtral_realtime_stream_ext._fields_ = [("ext", transcribe_ext), ("num_delay_tokens", _c.c_int32), ("min_decode_interval_ms", _c.c_int32)]
transcribe_whisper_run_ext._fields_ = [("ext", transcribe_ext), ("initial_prompt", _c.c_char_p), ("prompt_tokens", _c.POINTER(_c.c_int32)), ("n_prompt_tokens", _c.c_size_t), ("prompt_condition", _c.c_int), ("condition_on_prev_tokens", _c.c_bool), ("max_prev_context_tokens", _c.c_int32), ("temperature", _c.c_float), ("temperature_inc", _c.c_float), ("compression_ratio_thold", _c.c_float), ("logprob_thold", _c.c_float), ("no_speech_thold", _c.c_float), ("seed", _c.c_uint32), ("max_initial_timestamp", _c.c_float)]
transcribe_whisper_chunk_trace._fields_ = [("struct_size", _c.c_uint64), ("t0_ms", _c.c_int64), ("t1_ms", _c.c_int64), ("temperature_used", _c.c_float), ("compression_ratio", _c.c_float), ("avg_logprob", _c.c_float), ("no_speech_prob", _c.c_float), ("no_speech_triggered", _c.c_bool), ("n_fallbacks", _c.c_int32)]
Expand Down Expand Up @@ -217,6 +225,7 @@ class transcribe_whisper_chunk_trace(_c.Structure):
'transcribe_moonshine_streaming_stream_ext': {'size': 24, 'align': 8, 'offsets': {'ext': 0, 'min_decode_interval_ms': 16}},
'transcribe_parakeet_stream_ext': {'size': 24, 'align': 8, 'offsets': {'ext': 0, 'att_context_right': 16}},
'transcribe_parakeet_buffered_stream_ext': {'size': 32, 'align': 8, 'offsets': {'ext': 0, 'left_ms': 16, 'chunk_ms': 20, 'right_ms': 24}},
'transcribe_sortformer_stream_ext': {'size': 24, 'align': 8, 'offsets': {'ext': 0, 'preset': 16}},
'transcribe_voxtral_realtime_stream_ext': {'size': 24, 'align': 8, 'offsets': {'ext': 0, 'num_delay_tokens': 16, 'min_decode_interval_ms': 20}},
'transcribe_whisper_run_ext': {'size': 80, 'align': 8, 'offsets': {'ext': 0, 'initial_prompt': 16, 'prompt_tokens': 24, 'n_prompt_tokens': 32, 'prompt_condition': 40, 'condition_on_prev_tokens': 44, 'max_prev_context_tokens': 48, 'temperature': 52, 'temperature_inc': 56, 'compression_ratio_thold': 60, 'logprob_thold': 64, 'no_speech_thold': 68, 'seed': 72, 'max_initial_timestamp': 76}},
'transcribe_whisper_chunk_trace': {'size': 48, 'align': 8, 'offsets': {'struct_size': 0, 't0_ms': 8, 't1_ms': 16, 'temperature_used': 24, 'compression_ratio': 28, 'avg_logprob': 32, 'no_speech_prob': 36, 'no_speech_triggered': 40, 'n_fallbacks': 44}},
Expand Down Expand Up @@ -365,6 +374,8 @@ def configure(lib):
lib.transcribe_session_params_init.argtypes = [_c.POINTER(transcribe_session_params)]
lib.transcribe_set_abort_callback.restype = None
lib.transcribe_set_abort_callback.argtypes = [_c.c_void_p, _c.CFUNCTYPE(_c.c_bool, _c.c_void_p), _c.c_void_p]
lib.transcribe_sortformer_stream_ext_init.restype = None
lib.transcribe_sortformer_stream_ext_init.argtypes = [_c.POINTER(transcribe_sortformer_stream_ext)]
lib.transcribe_speaker_segment_init.restype = None
lib.transcribe_speaker_segment_init.argtypes = [_c.POINTER(transcribe_speaker_segment)]
lib.transcribe_status_string.restype = _c.c_char_p
Expand Down
14 changes: 14 additions & 0 deletions bindings/python/tests/test_family_ext.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
t.MoonshineStreamingOptions,
t.ParakeetStreamOptions,
t.ParakeetBufferedStreamOptions,
t.SortformerStreamOptions,
t.VoxtralRealtimeStreamOptions,
]

Expand Down Expand Up @@ -73,6 +74,19 @@ def test_parakeet_buffered_partial_overrides():
assert built.right_ms == fresh.right_ms


def test_sortformer_preset_maps_to_enum_value():
built = t.SortformerStreamOptions(preset="very_high_latency")._build()
assert built.preset == _generated.TRANSCRIBE_SORTFORMER_PRESET_VERY_HIGH_LATENCY
# None keeps the init default (DEFAULT = GGUF-shipped cfg).
default = t.SortformerStreamOptions()._build()
assert default.preset == _generated.TRANSCRIBE_SORTFORMER_PRESET_DEFAULT


def test_sortformer_unknown_preset_rejected():
with pytest.raises(ValueError, match="preset"):
t.SortformerStreamOptions(preset="ultra_low_latency") # type: ignore[arg-type]


# --- model-gated: resolve_family validation + a real extension run ----------


Expand Down
38 changes: 36 additions & 2 deletions bindings/rust/sys/src/transcribe_sys.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
// @generated by `cargo xtask bindgen` from include/transcribe/extensions.h
// DO NOT EDIT BY HAND. Regenerate: `cargo xtask bindgen`.
// Pinned to include/transcribe.abihash = d67a9bd78b964445
// Pinned to include/transcribe.abihash = fb2e64791dcbb70a

/// 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 = "d67a9bd78b964445";
pub const PUBLIC_HEADER_HASH: &str = "fb2e64791dcbb70a";

/* automatically generated by rust-bindgen 0.72.1 */

pub const TRANSCRIBE_EXT_KIND_MOONSHINE_STREAMING_STREAM: u32 = 1414746957;
pub const TRANSCRIBE_EXT_KIND_PARAKEET_STREAM: u32 = 1414744912;
pub const TRANSCRIBE_EXT_KIND_PARAKEET_BUFFERED_STREAM: u32 = 1396853584;
pub const TRANSCRIBE_EXT_KIND_SORTFORMER_STREAM: u32 = 1414743635;
pub const TRANSCRIBE_EXT_KIND_VOXTRAL_REALTIME_STREAM: u32 = 1414746710;
pub const TRANSCRIBE_EXT_KIND_WHISPER_RUN: u32 = 1314015319;
impl transcribe_status {
Expand Down Expand Up @@ -1156,6 +1157,39 @@ unsafe extern "C" {
ext: *mut transcribe_parakeet_buffered_stream_ext,
);
}
impl transcribe_sortformer_preset {
pub const TRANSCRIBE_SORTFORMER_PRESET_DEFAULT: transcribe_sortformer_preset =
transcribe_sortformer_preset(0);
pub const TRANSCRIBE_SORTFORMER_PRESET_VERY_HIGH_LATENCY: transcribe_sortformer_preset =
transcribe_sortformer_preset(1);
pub const TRANSCRIBE_SORTFORMER_PRESET_HIGH_LATENCY: transcribe_sortformer_preset =
transcribe_sortformer_preset(2);
pub const TRANSCRIBE_SORTFORMER_PRESET_LOW_LATENCY: transcribe_sortformer_preset =
transcribe_sortformer_preset(3);
}
#[repr(transparent)]
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
pub struct transcribe_sortformer_preset(pub ::std::os::raw::c_uint);
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct transcribe_sortformer_stream_ext {
pub ext: transcribe_ext,
pub preset: transcribe_sortformer_preset,
}
#[allow(clippy::unnecessary_operation, clippy::identity_op)]
const _: () = {
["Size of transcribe_sortformer_stream_ext"]
[::std::mem::size_of::<transcribe_sortformer_stream_ext>() - 24usize];
["Alignment of transcribe_sortformer_stream_ext"]
[::std::mem::align_of::<transcribe_sortformer_stream_ext>() - 8usize];
["Offset of field: transcribe_sortformer_stream_ext::ext"]
[::std::mem::offset_of!(transcribe_sortformer_stream_ext, ext) - 0usize];
["Offset of field: transcribe_sortformer_stream_ext::preset"]
[::std::mem::offset_of!(transcribe_sortformer_stream_ext, preset) - 16usize];
};
unsafe extern "C" {
pub fn transcribe_sortformer_stream_ext_init(ext: *mut transcribe_sortformer_stream_ext);
}
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct transcribe_voxtral_realtime_stream_ext {
Expand Down
54 changes: 54 additions & 0 deletions bindings/rust/transcribe-cpp/src/family.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,55 @@ pub struct VoxtralRealtimeStreamOptions {
pub min_decode_interval_ms: Option<i32>,
}

/// Sortformer streaming operating point (latency / accuracy trade-off).
/// The menu is discrete (jointly-tuned bundles), not a latency dial;
/// `Default` keeps the GGUF-shipped checkpoint configuration.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum SortformerPreset {
#[default]
Default,
/// ~30.4 s algorithmic lookahead; the offline-file operating point.
VeryHighLatency,
/// ~10.0 s lookahead.
HighLatency,
/// ~1.04 s lookahead; the real-time point (compute-heavy per audio
/// second — many small windows).
LowLatency,
}

impl SortformerPreset {
fn to_sys(self) -> sys::transcribe_sortformer_preset {
match self {
SortformerPreset::Default => {
sys::transcribe_sortformer_preset::TRANSCRIBE_SORTFORMER_PRESET_DEFAULT
}
SortformerPreset::VeryHighLatency => {
sys::transcribe_sortformer_preset::TRANSCRIBE_SORTFORMER_PRESET_VERY_HIGH_LATENCY
}
SortformerPreset::HighLatency => {
sys::transcribe_sortformer_preset::TRANSCRIBE_SORTFORMER_PRESET_HIGH_LATENCY
}
SortformerPreset::LowLatency => {
sys::transcribe_sortformer_preset::TRANSCRIBE_SORTFORMER_PRESET_LOW_LATENCY
}
}
}
}

/// Sortformer diarizer run-extension knobs (run slot). Sortformer produces
/// speaker segments, no text; read results via the speaker-segment
/// accessors. `None` keeps the family default (the GGUF-shipped cfg).
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SortformerStreamOptions {
pub preset: Option<SortformerPreset>,
}

/// A family extension for the run slot (offline `run`/`run_batch`).
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum RunExtension {
Whisper(WhisperRunOptions),
Sortformer(SortformerStreamOptions),
}

/// A family extension for the stream slot.
Expand All @@ -86,6 +130,7 @@ pub(crate) enum RunExtRaw {
ext: Box<sys::transcribe_whisper_run_ext>,
_prompt: Option<CString>,
},
Sortformer(Box<sys::transcribe_sortformer_stream_ext>),
}

impl RunExtRaw {
Expand All @@ -95,6 +140,9 @@ impl RunExtRaw {
RunExtRaw::Whisper { ext, .. } => {
(&**ext) as *const sys::transcribe_whisper_run_ext as *const sys::transcribe_ext
}
RunExtRaw::Sortformer(e) => {
(&**e) as *const sys::transcribe_sortformer_stream_ext as *const sys::transcribe_ext
}
}
}
}
Expand Down Expand Up @@ -128,6 +176,12 @@ impl RunExtension {
_prompt: prompt,
})
}
RunExtension::Sortformer(o) => {
let mut ext: sys::transcribe_sortformer_stream_ext = unsafe { std::mem::zeroed() };
unsafe { sys::transcribe_sortformer_stream_ext_init(&mut ext) };
set(&mut ext.preset, o.preset.map(SortformerPreset::to_sys));
Ok(RunExtRaw::Sortformer(Box::new(ext)))
}
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion bindings/rust/transcribe-cpp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ pub use cancel::CancelToken;
pub use error::{Error, Result};
pub use family::{
MoonshineStreamingOptions, ParakeetBufferedStreamOptions, ParakeetStreamOptions, RunExtension,
StreamExtension, VoxtralRealtimeStreamOptions, WhisperRunOptions,
SortformerPreset, SortformerStreamOptions, StreamExtension, VoxtralRealtimeStreamOptions,
WhisperRunOptions,
};
pub use logging::{disable_logging, init_logging};
pub use model::{Capabilities, Model, ModelOptions, SessionLimits, SessionOptions};
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 = "d67a9bd78b964445"
public static let pinnedHeaderHash = "fb2e64791dcbb70a"

/// The public-ABI digest this binding was reviewed against (16 hex chars).
public static func headerHash() -> String { pinnedHeaderHash }
Expand Down
37 changes: 36 additions & 1 deletion bindings/swift/Sources/TranscribeCpp/Family.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import CTranscribe
// `transcribe_ext` pointer is handed to the run/begin call; the library copies
// what it needs before returning.

// MARK: - Run-slot extensions (whisper)
// MARK: - Run-slot extensions (whisper, sortformer)

public struct WhisperRunOptions: Sendable {
public var initialPrompt: String?
Expand Down Expand Up @@ -46,12 +46,42 @@ public struct WhisperRunOptions: Sendable {
}
}

/// Sortformer streaming operating point (latency / accuracy trade-off).
/// The menu is discrete (jointly-tuned bundles), not a latency dial;
/// `.default` keeps the GGUF-shipped checkpoint configuration.
/// `.veryHighLatency` (~30 s lookahead) is the offline-file operating
/// point; `.lowLatency` (~1 s) is the real-time point.
public enum SortformerPreset: Sendable {
case `default`
case veryHighLatency
case highLatency
case lowLatency

var cValue: transcribe_sortformer_preset {
switch self {
case .default: return TRANSCRIBE_SORTFORMER_PRESET_DEFAULT
case .veryHighLatency: return TRANSCRIBE_SORTFORMER_PRESET_VERY_HIGH_LATENCY
case .highLatency: return TRANSCRIBE_SORTFORMER_PRESET_HIGH_LATENCY
case .lowLatency: return TRANSCRIBE_SORTFORMER_PRESET_LOW_LATENCY
}
}
}

/// Sortformer diarizer options (run slot). Sortformer produces speaker
/// segments, no text; read results via the speaker-segment accessors.
public struct SortformerStreamOptions: Sendable {
public var preset: SortformerPreset?
public init(preset: SortformerPreset? = nil) { self.preset = preset }
}

public enum RunExtension: Sendable {
case whisper(WhisperRunOptions)
case sortformer(SortformerStreamOptions)

var kind: UInt32 {
switch self {
case .whisper: return TRANSCRIBE_EXT_KIND_WHISPER_RUN
case .sortformer: return TRANSCRIBE_EXT_KIND_SORTFORMER_STREAM
}
}
}
Expand Down Expand Up @@ -79,6 +109,11 @@ func withRunExtension<R>(
c.initial_prompt = prompt
return try withUnsafePointer(to: &c.ext) { try body($0) }
}
case .sortformer(let o):
var c = transcribe_sortformer_stream_ext()
transcribe_sortformer_stream_ext_init(&c)
if let v = o.preset { c.preset = v.cValue }
return try withUnsafePointer(to: &c.ext) { try body($0) }
}
}

Expand Down
Loading
Loading