Skip to main content

crispasr_sys/
lib.rs

1//! Raw FFI bindings to CrispASR.
2//! Mirrors the public C API in include/whisper.h.
3
4use std::ffi::{c_char, c_float, c_int, c_void};
5
6/// Opaque context handle.
7#[repr(C)]
8pub struct WhisperContext(c_void);
9
10/// Opaque state handle.
11#[repr(C)]
12pub struct WhisperState(c_void);
13
14/// Opaque params handle (allocated by whisper_full_default_params_by_ref).
15#[repr(C)]
16pub struct WhisperFullParams(c_void);
17
18/// Opaque context params handle.
19#[repr(C)]
20pub struct WhisperContextParams(c_void);
21
22/// Sampling strategy.
23pub const CRISPASR_SAMPLING_GREEDY: c_int = 0;
24pub const CRISPASR_SAMPLING_BEAM_SEARCH: c_int = 1;
25
26/// Progress callback for long-form (chunked) transcription (issue #208).
27/// `Option<...>` so a null pointer clears the callback (C `NULL`).
28pub type CrispasrProgressCallback =
29    Option<unsafe extern "C" fn(processed: c_int, total: c_int, user_data: *mut c_void)>;
30
31extern "C" {
32    // --- Lifecycle ---
33    pub fn whisper_init_from_file_with_params(
34        path: *const c_char,
35        params: *const WhisperContextParams,
36    ) -> *mut WhisperContext;
37
38    pub fn whisper_context_default_params_by_ref() -> *mut WhisperContextParams;
39    pub fn whisper_free(ctx: *mut WhisperContext);
40    pub fn whisper_free_params(params: *mut WhisperFullParams);
41    pub fn whisper_free_context_params(params: *mut WhisperContextParams);
42
43    // --- Inference ---
44    pub fn whisper_full(
45        ctx: *mut WhisperContext,
46        params: *const WhisperFullParams,
47        samples: *const c_float,
48        n_samples: c_int,
49    ) -> c_int;
50
51    pub fn whisper_full_default_params_by_ref(strategy: c_int) -> *mut WhisperFullParams;
52
53    // --- Results ---
54    pub fn whisper_full_n_segments(ctx: *mut WhisperContext) -> c_int;
55
56    pub fn whisper_full_get_segment_text(
57        ctx: *mut WhisperContext,
58        i_segment: c_int,
59    ) -> *const c_char;
60
61    pub fn whisper_full_get_segment_t0(ctx: *mut WhisperContext, i_segment: c_int) -> i64;
62
63    pub fn whisper_full_get_segment_t1(ctx: *mut WhisperContext, i_segment: c_int) -> i64;
64
65    pub fn whisper_full_get_segment_no_speech_prob(
66        ctx: *mut WhisperContext,
67        i_segment: c_int,
68    ) -> c_float;
69
70    // --- Language ---
71    pub fn whisper_full_lang_id(ctx: *mut WhisperContext) -> c_int;
72    pub fn whisper_lang_str(id: c_int) -> *const c_char;
73    pub fn whisper_lang_id(lang: *const c_char) -> c_int;
74
75    // --- 0.4.2: VAD + tdrz setters on whisper_full_params ---
76    pub fn crispasr_params_set_vad(p: *mut WhisperFullParams, v: c_int);
77    pub fn crispasr_params_set_vad_model_path(p: *mut WhisperFullParams, path: *const c_char);
78    pub fn crispasr_params_set_vad_threshold(p: *mut WhisperFullParams, threshold: c_float);
79    pub fn crispasr_params_set_vad_min_speech_ms(p: *mut WhisperFullParams, ms: c_int);
80    pub fn crispasr_params_set_vad_min_silence_ms(p: *mut WhisperFullParams, ms: c_int);
81    pub fn crispasr_params_set_tdrz(p: *mut WhisperFullParams, v: c_int);
82}
83
84// =========================================================================
85// Unified session FFI (CrispASR 0.4.0+) — multi-backend dispatch
86// =========================================================================
87//
88// Open any CrispASR-supported GGUF (Whisper, Parakeet, Canary, Cohere,
89// Qwen3-ASR, Granite Speech, FastConformer-CTC, Canary-CTC, Voxtral,
90// Voxtral4B, Wav2Vec2) through one handle. Backend auto-detected from
91// `general.architecture` metadata unless overridden.
92
93/// Opaque handle returned by `crispasr_session_open`.
94#[repr(C)]
95pub struct CrispasrSession(c_void);
96
97/// Opaque result handle returned by `crispasr_session_transcribe`.
98/// Must be freed with `crispasr_session_result_free`.
99#[repr(C)]
100pub struct CrispasrSessionResult(c_void);
101
102/// Opaque streaming-decoder handle returned by
103/// `crispasr_session_stream_open`. Must be freed with
104/// `crispasr_stream_close`. (PLAN #62)
105#[repr(C)]
106pub struct CrispasrStream(c_void);
107
108/// Opaque microphone handle returned by `crispasr_mic_open`.
109/// Must be freed with `crispasr_mic_close`. (PLAN #62d)
110#[repr(C)]
111pub struct CrispasrMic(c_void);
112
113/// Opaque result handle for `crispasr_align_words_abi`. Must be freed
114/// with `crispasr_align_result_free`.
115#[repr(C)]
116pub struct CrispasrAlignResult(c_void);
117
118/// Tunables for [`crispasr_session_transcribe_vad`]. Mirrors crispasr's
119/// `whisper_vad_params` plus the max-chunk fallback used to bound encoder
120/// cost on long audio. Pass a null pointer to use defaults.
121#[repr(C)]
122#[derive(Clone, Copy, Debug)]
123pub struct CrispasrVadAbiOpts {
124    pub threshold: c_float,
125    pub min_speech_duration_ms: c_int,
126    pub min_silence_duration_ms: c_int,
127    pub speech_pad_ms: c_int,
128    pub chunk_seconds: c_int,
129    pub n_threads: c_int,
130}
131
132impl Default for CrispasrVadAbiOpts {
133    fn default() -> Self {
134        Self {
135            threshold: 0.5,
136            min_speech_duration_ms: 250,
137            min_silence_duration_ms: 100,
138            speech_pad_ms: 30,
139            chunk_seconds: 30,
140            n_threads: 4,
141        }
142    }
143}
144
145/// ABI segment for [`crispasr_diarize_segments_abi`]. Caller fills
146/// `t0_cs` / `t1_cs`; the diarizer writes `speaker` (-1 if unassigned).
147#[repr(C)]
148#[derive(Clone, Copy, Debug)]
149pub struct CrispasrDiarizeSegAbi {
150    pub t0_cs: i64,
151    pub t1_cs: i64,
152    pub speaker: c_int,
153    pub _pad: c_int,
154}
155
156/// ABI options for [`crispasr_diarize_segments_abi`]. `method` is a
157/// value in 0..3: 0 = Energy, 1 = Xcorr, 2 = VadTurns, 3 = Pyannote.
158/// `pyannote_model_path` is required for Pyannote, ignored otherwise.
159#[repr(C)]
160#[derive(Clone, Copy, Debug)]
161pub struct CrispasrDiarizeOptsAbi {
162    pub method: c_int,
163    pub n_threads: c_int,
164    pub slice_t0_cs: i64,
165    pub pyannote_model_path: *const c_char,
166}
167
168extern "C" {
169    pub fn crispasr_session_open(
170        model_path: *const c_char,
171        n_threads: c_int,
172    ) -> *mut CrispasrSession;
173
174    pub fn crispasr_session_open_explicit(
175        model_path: *const c_char,
176        backend_name: *const c_char,
177        n_threads: c_int,
178    ) -> *mut CrispasrSession;
179
180    pub fn crispasr_session_backend(s: *mut CrispasrSession) -> *const c_char;
181
182    // CTC vocabulary access (Omni CTC backend). `n_vocab` is the number of
183    // SentencePiece pieces (0 for backends without an exposed CTC vocab);
184    // `token_text` maps an id in `[0, n_vocab)` to its raw piece (U+2581 marker
185    // intact), or "" when out of range / unsupported. Pairs with the result
186    // logits accessor to detokenize a greedy CTC decode.
187    pub fn crispasr_session_n_vocab(s: *mut CrispasrSession) -> c_int;
188    pub fn crispasr_session_token_text(s: *mut CrispasrSession, id: c_int) -> *const c_char;
189
190    // Acoustic language detected by the last transcribe, written into `out_buf`
191    // as an ISO-639-1 code (whisper only; other backends fall back to the
192    // source-language hint, then "unknown"). Returns the code length in bytes
193    // (not counting NUL) or -1 on bad args. Distinct from the text-LID pass.
194    pub fn crispasr_session_detected_language(
195        s: *mut CrispasrSession,
196        out_buf: *mut c_char,
197        out_cap: c_int,
198    ) -> c_int;
199
200    /// Write a comma-separated list of backend names the loaded dylib
201    /// was built with. Returns the number of bytes written (not counting
202    /// NUL) or a negative error.
203    pub fn crispasr_session_available_backends(out_csv: *mut c_char, out_cap: c_int) -> c_int;
204
205    pub fn crispasr_session_transcribe(
206        s: *mut CrispasrSession,
207        pcm: *const c_float,
208        n_samples: c_int,
209    ) -> *mut CrispasrSessionResult;
210
211    /// 0.4.9+: language-aware session transcribe. `language` is an
212    /// ISO 639-1 code or null/empty to keep the backend's historical
213    /// default. Backends that accept a source-language hint (whisper,
214    /// canary, cohere, voxtral, voxtral4b) honour it; others ignore
215    /// silently.
216    pub fn crispasr_session_transcribe_lang(
217        s: *mut CrispasrSession,
218        pcm: *const c_float,
219        n_samples: c_int,
220        language: *const c_char,
221    ) -> *mut CrispasrSessionResult;
222
223    /// 0.8.7+: chunked-encode transcribe (issue #208). Forces the
224    /// Parakeet backend through its bounded long-form path (overlapping
225    /// short-window transcribe-and-merge for non-JA models, streamed
226    /// encoder for the JA-only model) regardless of audio length, so long
227    /// files transcribe in bounded time AND recover the sections a single
228    /// full-length pass drops. `chunk_seconds <= 0` keeps the per-model
229    /// defaults; otherwise it sets the non-JA window length / the JA
230    /// streamed window. `overlap_seconds < 0` uses the default. For
231    /// non-Parakeet backends the chunk params are inert and this matches
232    /// `crispasr_session_transcribe_lang`.
233    pub fn crispasr_session_transcribe_chunked_lang(
234        s: *mut CrispasrSession,
235        pcm: *const c_float,
236        n_samples: c_int,
237        chunk_seconds: c_int,
238        overlap_seconds: c_int,
239        language: *const c_char,
240    ) -> *mut CrispasrSessionResult;
241
242    pub fn crispasr_session_transcribe_chunked(
243        s: *mut CrispasrSession,
244        pcm: *const c_float,
245        n_samples: c_int,
246        chunk_seconds: c_int,
247        overlap_seconds: c_int,
248    ) -> *mut CrispasrSessionResult;
249
250    /// 0.10.3+ (issue #208): register a per-session progress callback for
251    /// long-form (chunked) transcription. Fired once per finished window
252    /// with `(processed_samples, total_samples, user_data)`; `processed`
253    /// is monotonic and reaches `total` on the last window. Invoked on the
254    /// transcribe thread. Pass `None`/null `cb` to clear.
255    pub fn crispasr_session_set_progress_callback(
256        s: *mut CrispasrSession,
257        cb: CrispasrProgressCallback,
258        user_data: *mut c_void,
259    );
260
261    /// VAD-driven session transcribe. Runs Silero VAD on the PCM buffer,
262    /// merges short / overlong speech slices, stitches them into one
263    /// contiguous buffer with 0.1s silence gaps, calls the backend once,
264    /// then remaps segment + word timestamps back to original-audio
265    /// positions.
266    ///
267    /// `vad_model_path` must point to a Silero GGUF on disk. Pass a null
268    /// or empty `opts` pointer to use defaults (mirrors crispasr's
269    /// `whisper_vad_default_params`).
270    pub fn crispasr_session_transcribe_vad(
271        s: *mut CrispasrSession,
272        pcm: *const c_float,
273        n_samples: c_int,
274        sample_rate: c_int,
275        vad_model_path: *const c_char,
276        opts: *const CrispasrVadAbiOpts,
277    ) -> *mut CrispasrSessionResult;
278
279    /// 0.4.9+: language-aware VAD transcribe (same semantics as the
280    /// language kwarg on `crispasr_session_transcribe_lang`).
281    pub fn crispasr_session_transcribe_vad_lang(
282        s: *mut CrispasrSession,
283        pcm: *const c_float,
284        n_samples: c_int,
285        sample_rate: c_int,
286        vad_model_path: *const c_char,
287        opts: *const CrispasrVadAbiOpts,
288        language: *const c_char,
289    ) -> *mut CrispasrSessionResult;
290
291    /// Shared speaker diarization (0.4.5+). Writes a zero-based speaker
292    /// index into each `segs[i].speaker`. Returns 0 on success, 1 on
293    /// Pyannote model load failure, -1 on invalid args.
294    pub fn crispasr_diarize_segments_abi(
295        left_pcm: *const c_float,
296        right_pcm: *const c_float,
297        n_samples: c_int,
298        is_stereo: c_int,
299        segs: *mut CrispasrDiarizeSegAbi,
300        n_segs: c_int,
301        opts: *const CrispasrDiarizeOptsAbi,
302    ) -> c_int;
303
304    /// Shared language identification (0.4.6+). `method` is 0 for
305    /// whisper, 1 for silero. `model_path` is required. Fills
306    /// `out_lang_buf` with a null-terminated ISO 639-1 code. Returns 0
307    /// on success, -1 on invalid args, 1 on model / detect failure,
308    /// 2 when the output buffer is too small.
309    pub fn crispasr_detect_language_pcm(
310        samples: *const c_float,
311        n_samples: c_int,
312        method: c_int,
313        model_path: *const c_char,
314        n_threads: c_int,
315        use_gpu: c_int,
316        gpu_device: c_int,
317        flash_attn: c_int,
318        out_lang_buf: *mut c_char,
319        out_lang_cap: c_int,
320        out_confidence: *mut c_float,
321    ) -> c_int;
322
323    /// Shared CTC / forced-aligner word timings (0.4.7+).
324    /// Pass any `aligner_model` path — filenames containing
325    /// "forced-aligner" / "qwen3-fa" / "qwen3-forced" go through the
326    /// Qwen3-ForcedAligner path; everything else uses canary-ctc.
327    /// Returns a handle the caller must free with
328    /// [`crispasr_align_result_free`]. Returns null on failure.
329    pub fn crispasr_align_words_abi(
330        aligner_model: *const c_char,
331        transcript: *const c_char,
332        samples: *const c_float,
333        n_samples: c_int,
334        t_offset_cs: i64,
335        n_threads: c_int,
336    ) -> *mut CrispasrAlignResult;
337
338    pub fn crispasr_align_result_n_words(r: *mut CrispasrAlignResult) -> c_int;
339    pub fn crispasr_align_result_word_text(r: *mut CrispasrAlignResult, i: c_int) -> *const c_char;
340    pub fn crispasr_align_result_word_t0(r: *mut CrispasrAlignResult, i: c_int) -> i64;
341    pub fn crispasr_align_result_word_t1(r: *mut CrispasrAlignResult, i: c_int) -> i64;
342    pub fn crispasr_align_result_free(r: *mut CrispasrAlignResult);
343
344    /// Shared HF download + cache (0.4.8+). Writes the resolved path
345    /// into `out_buf`. Returns 0 on success, -1 on invalid args, 1 on
346    /// download failure, 2 when the output buffer is too small.
347    pub fn crispasr_cache_ensure_file_abi(
348        filename: *const c_char,
349        url: *const c_char,
350        quiet: c_int,
351        cache_dir_override: *const c_char,
352        out_buf: *mut c_char,
353        out_cap: c_int,
354    ) -> c_int;
355
356    /// Return the CrispASR cache directory (creating it if missing).
357    pub fn crispasr_cache_dir_abi(
358        cache_dir_override: *const c_char,
359        out_buf: *mut c_char,
360        out_cap: c_int,
361    ) -> c_int;
362
363    /// Shared known-model registry lookup by backend. 0 = hit, 1 = miss.
364    pub fn crispasr_registry_lookup_abi(
365        backend: *const c_char,
366        out_filename: *mut c_char,
367        filename_cap: c_int,
368        out_url: *mut c_char,
369        url_cap: c_int,
370        out_size: *mut c_char,
371        size_cap: c_int,
372    ) -> c_int;
373
374    /// Shared known-model registry lookup by filename (exact then fuzzy).
375    pub fn crispasr_registry_list_backends_abi(out_csv: *mut c_char, out_cap: c_int) -> c_int;
376
377    /// Describe the exact canonical artifact bundle downloaded by `-m auto`.
378    /// Returns its artifact count, 0 on miss, or a negative argument/buffer error.
379    pub fn crispasr_registry_default_bundle_info_abi(
380        backend: *const c_char,
381        out_backend: *mut c_char,
382        backend_cap: c_int,
383        out_license: *mut c_char,
384        license_cap: c_int,
385        out_requires_acceptance: *mut c_int,
386    ) -> c_int;
387
388    /// Read one default-bundle artifact by index. 0 = success.
389    pub fn crispasr_registry_default_bundle_artifact_abi(
390        backend: *const c_char,
391        index: c_int,
392        out_kind: *mut c_int,
393        out_filename: *mut c_char,
394        filename_cap: c_int,
395        out_url: *mut c_char,
396        url_cap: c_int,
397        out_size: *mut c_char,
398        size_cap: c_int,
399    ) -> c_int;
400
401    // --- Streaming (PLAN #62) — rolling-window decoder for whisper today ---
402    pub fn crispasr_session_stream_open(
403        s: *mut CrispasrSession,
404        n_threads: c_int,
405        step_ms: c_int,
406        length_ms: c_int,
407        keep_ms: c_int,
408        language: *const c_char,
409        translate: c_int,
410    ) -> *mut CrispasrStream;
411    pub fn crispasr_stream_feed(
412        s: *mut CrispasrStream,
413        pcm: *const c_float,
414        n_samples: c_int,
415    ) -> c_int;
416    pub fn crispasr_stream_get_text(
417        s: *mut CrispasrStream,
418        out_text: *mut c_char,
419        out_cap: c_int,
420        out_t0_s: *mut f64,
421        out_t1_s: *mut f64,
422        out_counter: *mut i64,
423    ) -> c_int;
424    pub fn crispasr_stream_flush(s: *mut CrispasrStream) -> c_int;
425    pub fn crispasr_stream_close(s: *mut CrispasrStream);
426
427    /// Toggle voxtral4b live-captions decode-during-feed (PLAN #7 phase 3).
428    /// No-op for backends that don't have audio-injection prompt decode.
429    /// Set BEFORE the first feed for clean semantics.
430    pub fn crispasr_stream_set_live_decode(s: *mut CrispasrStream, enabled: c_int);
431
432    // --- Mic capture (PLAN #62d) — miniaudio ma_device wrapper ---
433    pub fn crispasr_mic_open(
434        sample_rate: c_int,
435        channels: c_int,
436        cb: extern "C" fn(pcm: *const c_float, n_samples: c_int, userdata: *mut c_void),
437        userdata: *mut c_void,
438    ) -> *mut CrispasrMic;
439    pub fn crispasr_mic_start(m: *mut CrispasrMic) -> c_int;
440    pub fn crispasr_mic_stop(m: *mut CrispasrMic) -> c_int;
441    pub fn crispasr_mic_close(m: *mut CrispasrMic);
442    pub fn crispasr_mic_default_device_name() -> *const c_char;
443    pub fn crispasr_registry_lookup_by_filename_abi(
444        filename: *const c_char,
445        out_filename: *mut c_char,
446        filename_cap: c_int,
447        out_url: *mut c_char,
448        url_cap: c_int,
449        out_size: *mut c_char,
450        size_cap: c_int,
451    ) -> c_int;
452
453    pub fn crispasr_session_result_n_segments(r: *mut CrispasrSessionResult) -> c_int;
454    pub fn crispasr_session_result_segment_text(
455        r: *mut CrispasrSessionResult,
456        i: c_int,
457    ) -> *const c_char;
458    pub fn crispasr_session_result_segment_t0(r: *mut CrispasrSessionResult, i: c_int) -> i64;
459    pub fn crispasr_session_result_segment_t1(r: *mut CrispasrSessionResult, i: c_int) -> i64;
460
461    pub fn crispasr_session_result_n_words(r: *mut CrispasrSessionResult, i_seg: c_int) -> c_int;
462    pub fn crispasr_session_result_word_text(
463        r: *mut CrispasrSessionResult,
464        i_seg: c_int,
465        i_word: c_int,
466    ) -> *const c_char;
467    pub fn crispasr_session_result_word_t0(
468        r: *mut CrispasrSessionResult,
469        i_seg: c_int,
470        i_word: c_int,
471    ) -> i64;
472    pub fn crispasr_session_result_word_t1(
473        r: *mut CrispasrSessionResult,
474        i_seg: c_int,
475        i_word: c_int,
476    ) -> i64;
477    pub fn crispasr_session_result_word_p(
478        r: *mut CrispasrSessionResult,
479        i_seg: c_int,
480        i_word: c_int,
481    ) -> f32;
482    // Whisper's per-segment no-speech probability (the <|nospeech|> token
483    // posterior) in [0, 1]. Only the whisper backend populates it; other
484    // backends and out-of-range indices return the -1.0 sentinel ("no data").
485    pub fn crispasr_session_result_segment_no_speech_prob(
486        r: *mut CrispasrSessionResult,
487        i_seg: c_int,
488    ) -> f32;
489
490    // Raw per-frame CTC logits (Omni CTC backend, opted in via
491    // `crispasr_session_set_return_logits`). Frame-major, pre-softmax:
492    // `logits[t * n_logit_vocab + v]`; the pointer is NULL when none captured.
493    pub fn crispasr_session_result_n_logit_frames(r: *mut CrispasrSessionResult) -> c_int;
494    pub fn crispasr_session_result_n_logit_vocab(r: *mut CrispasrSessionResult) -> c_int;
495    pub fn crispasr_session_result_logits(r: *mut CrispasrSessionResult) -> *const c_float;
496
497    pub fn crispasr_session_result_free(r: *mut CrispasrSessionResult);
498    pub fn crispasr_session_close(s: *mut CrispasrSession);
499
500    // --- TTS synthesis (vibevoice, qwen3-tts, kokoro, orpheus) ---
501    pub fn crispasr_session_set_codec_path(s: *mut CrispasrSession, path: *const c_char) -> c_int;
502    pub fn crispasr_session_set_voice(
503        s: *mut CrispasrSession,
504        path: *const c_char,
505        ref_text_or_null: *const c_char,
506    ) -> c_int;
507    pub fn crispasr_session_set_speaker_name(s: *mut CrispasrSession, name: *const c_char)
508        -> c_int;
509    pub fn crispasr_session_n_speakers(s: *mut CrispasrSession) -> c_int;
510    pub fn crispasr_session_get_speaker_name(s: *mut CrispasrSession, i: c_int) -> *const c_char;
511    // qwen3-tts VoiceDesign: natural-language voice description.
512    pub fn crispasr_session_set_instruct(s: *mut CrispasrSession, instruct: *const c_char)
513        -> c_int;
514    // #316: synthesize these phonemes verbatim, skipping the G2P. Empty clears.
515    // -2 = the active backend has no phonemes-in call (kokoro and piper do).
516    pub fn crispasr_session_set_tts_phonemes(s: *mut CrispasrSession, phonemes: *const c_char) -> c_int;
517    // qwen3-tts variant detection (returns 0/1; 0 also covers "not qwen3-tts").
518    pub fn crispasr_session_is_custom_voice(s: *mut CrispasrSession) -> c_int;
519    pub fn crispasr_session_is_voice_design(s: *mut CrispasrSession) -> c_int;
520    pub fn crispasr_session_synthesize(
521        s: *mut CrispasrSession,
522        text: *const c_char,
523        out_n_samples: *mut c_int,
524    ) -> *mut f32;
525    pub fn crispasr_pcm_free(pcm: *mut f32);
526    // Drop the kokoro per-session phoneme cache. No-op for non-kokoro
527    // backends. Returns 0 on success, -1 if `s` is null. (PLAN #56 #5)
528    pub fn crispasr_session_kokoro_clear_phoneme_cache(s: *mut CrispasrSession) -> c_int;
529
530    // --- Sticky session-state setters (PLAN #59 partial unblock) ---
531    pub fn crispasr_session_set_source_language(
532        s: *mut CrispasrSession,
533        lang: *const c_char,
534    ) -> c_int;
535    pub fn crispasr_session_set_target_language(
536        s: *mut CrispasrSession,
537        lang: *const c_char,
538    ) -> c_int;
539    pub fn crispasr_session_set_punctuation(s: *mut CrispasrSession, enable: c_int) -> c_int;
540    pub fn crispasr_session_set_punc_model(
541        s: *mut CrispasrSession,
542        punc_model: *const c_char,
543    ) -> c_int;
544    pub fn crispasr_session_set_hotwords(
545        s: *mut CrispasrSession,
546        hotwords: *const c_char,
547        boost: c_float,
548    ) -> c_int;
549    pub fn crispasr_session_set_g2p_dict(s: *mut CrispasrSession, source: *const c_char) -> c_int;
550    pub fn crispasr_session_set_speaker_id(s: *mut CrispasrSession, id: c_int) -> c_int;
551    pub fn crispasr_session_set_translate(s: *mut CrispasrSession, enable: c_int) -> c_int;
552    // --- Text-to-text translation (m2m100 / m2m100-wmt21 / madlad / gemma4-e2b) ---
553    //
554    // Distinct from `crispasr_session_set_translate` above, which is the
555    // *audio-side* Whisper sticky flag (PCM input → English text out).
556    // This one translates an already-extracted Rust string between
557    // arbitrary language pairs via whichever MT-capable backend the
558    // session loaded.  Returns a malloc'd UTF-8 buffer that the caller
559    // MUST release via `crispasr_session_translate_text_free` (mirrors
560    // the punc-side ownership pattern).  Returns nullptr on:
561    //   * any input pointer being null,
562    //   * the session not having a CAP_TRANSLATE backend loaded,
563    //   * the backend's internal translate routine erroring out.
564    //
565    // `max_tokens` caps the decoder output length.  Pass `<= 0` to
566    // fall back to the C++ default (200 for m2m100).
567    pub fn crispasr_session_translate_text(
568        s: *mut CrispasrSession,
569        text: *const c_char,
570        src_lang: *const c_char,
571        tgt_lang: *const c_char,
572        max_tokens: c_int,
573    ) -> *mut c_char;
574    // Free a buffer previously returned by `crispasr_session_translate_text`.
575    // No-op when `text` is null.  Calling `libc::free` directly also works
576    // (the C++ side just delegates to `free()`), but routing through this
577    // symbol keeps ownership symmetric and protects callers if the C++
578    // side ever switches allocators.
579    pub fn crispasr_session_translate_text_free(text: *mut c_char);
580    pub fn crispasr_session_set_temperature(
581        s: *mut CrispasrSession,
582        temperature: c_float,
583        seed: u64,
584    ) -> c_int;
585    pub fn crispasr_session_set_tts_seed(s: *mut CrispasrSession, seed: u64) -> c_int;
586    pub fn crispasr_session_set_max_new_tokens(
587        s: *mut CrispasrSession,
588        max_new_tokens: c_int,
589    ) -> c_int;
590    pub fn crispasr_session_set_frequency_penalty(
591        s: *mut CrispasrSession,
592        penalty: c_float,
593    ) -> c_int;
594    pub fn crispasr_session_set_tts_steps(s: *mut CrispasrSession, steps: c_int) -> c_int;
595    pub fn crispasr_session_set_tts_num_candidates(s: *mut CrispasrSession, n: c_int) -> c_int;
596    pub fn crispasr_session_set_top_p(s: *mut CrispasrSession, top_p: c_float) -> c_int;
597    pub fn crispasr_session_set_top_k(s: *mut CrispasrSession, top_k: c_int) -> c_int;
598    pub fn crispasr_session_set_do_sample(s: *mut CrispasrSession, enable: c_int) -> c_int;
599    pub fn crispasr_session_set_min_p(s: *mut CrispasrSession, min_p: c_float) -> c_int;
600    pub fn crispasr_session_set_repetition_penalty(s: *mut CrispasrSession, r: c_float) -> c_int;
601    pub fn crispasr_session_set_cfg_weight(s: *mut CrispasrSession, cfg_weight: c_float) -> c_int;
602    pub fn crispasr_session_set_tts_noise_temp(
603        s: *mut CrispasrSession,
604        noise_temp: c_float,
605    ) -> c_int;
606    pub fn crispasr_session_set_exaggeration(
607        s: *mut CrispasrSession,
608        exaggeration: c_float,
609    ) -> c_int;
610    pub fn crispasr_session_set_max_speech_tokens(s: *mut CrispasrSession, n: c_int) -> c_int;
611    pub fn crispasr_session_set_length_scale(s: *mut CrispasrSession, scale: c_float) -> c_int;
612    pub fn crispasr_session_set_best_of(s: *mut CrispasrSession, n: c_int) -> c_int;
613    pub fn crispasr_session_set_beam_size(s: *mut CrispasrSession, n: c_int) -> c_int;
614    pub fn crispasr_session_set_return_logits(s: *mut CrispasrSession, enable: c_int) -> c_int;
615    pub fn crispasr_session_set_grammar_text(
616        s: *mut CrispasrSession,
617        gbnf_text: *const c_char,
618        root_rule: *const c_char,
619        penalty: c_float,
620    ) -> c_int;
621    pub fn crispasr_session_set_fallback_thresholds(
622        s: *mut CrispasrSession,
623        entropy_thold: c_float,
624        logprob_thold: c_float,
625        no_speech_thold: c_float,
626        temperature_inc: c_float,
627    ) -> c_int;
628    pub fn crispasr_session_set_alt_n(s: *mut CrispasrSession, n: c_int) -> c_int;
629    pub fn crispasr_session_set_whisper_decode_extras(
630        s: *mut CrispasrSession,
631        suppress_nst: c_int,
632        suppress_regex: *const c_char,
633        carry_initial_prompt: c_int,
634    ) -> c_int;
635    pub fn crispasr_session_set_ask(s: *mut CrispasrSession, prompt: *const c_char) -> c_int;
636    pub fn crispasr_session_detect_language(
637        s: *mut CrispasrSession,
638        pcm: *const c_float,
639        n_samples: c_int,
640        lid_model_path: *const c_char,
641        method: c_int,
642        out_lang: *mut c_char,
643        out_lang_cap: c_int,
644        out_prob: *mut c_float,
645    ) -> c_int;
646
647    // --- Text-LID (P13.5 Phase 7) ---
648    //
649    // Detect the language of a UTF-8 text string via the internal
650    // `text_lid_dispatch` façade — routes to CLD3 (ISO 639-1, 109
651    // labels) or GlotLID-V3 / LID-176 fastText (ISO 639-3 + script,
652    // 2102 or 176 labels) based on the GGUF's architecture key.
653    // Label format follows whichever backend the GGUF loads as —
654    // see the C-API doc-comment for normalisation guidance.
655    //
656    // Returns:
657    //   *  0 — success; `out_label_buf` + `out_confidence` populated.
658    //   * -1 — invalid args (null pointer or out_label_cap <= 0).
659    //   *  1 — dispatcher init / predict failure.
660    //   *  2 — output buffer too small for the predicted label.
661    pub fn crispasr_text_detect_language(
662        text: *const c_char,
663        model_path: *const c_char,
664        n_threads: c_int,
665        out_label_buf: *mut c_char,
666        out_label_cap: c_int,
667        out_confidence: *mut c_float,
668    ) -> c_int;
669
670    pub fn crispasr_detect_backend_from_gguf(
671        path: *const c_char,
672        out_name: *mut c_char,
673        out_cap: c_int,
674    ) -> c_int;
675
676    // --- FireRedPunc punctuation restoration ---
677    pub fn crispasr_punc_init(model_path: *const c_char) -> *mut c_void;
678    pub fn crispasr_punc_process(ctx: *mut c_void, text: *const c_char) -> *mut c_char;
679    pub fn crispasr_punc_free_text(text: *mut c_char);
680    pub fn crispasr_punc_free(ctx: *mut c_void);
681
682    pub fn crispasr_c_api_version() -> *const c_char;
683
684    // --- Kokoro per-language model + voice routing (PLAN #56 opt 2b) ---
685    // See `src/kokoro.h` for full semantics.
686    pub fn crispasr_kokoro_lang_is_german_abi(lang: *const c_char) -> bool;
687    pub fn crispasr_kokoro_lang_has_native_voice_abi(lang: *const c_char) -> bool;
688    pub fn crispasr_kokoro_resolve_model_for_lang_abi(
689        model_path: *const c_char,
690        lang: *const c_char,
691        out_path: *mut c_char,
692        out_path_len: c_int,
693    ) -> c_int;
694    pub fn crispasr_kokoro_resolve_fallback_voice_abi(
695        model_path: *const c_char,
696        lang: *const c_char,
697        out_path: *mut c_char,
698        out_path_len: c_int,
699        out_picked: *mut c_char,
700        out_picked_len: c_int,
701    ) -> c_int;
702
703    // TitaNet speaker verification
704    pub fn crispasr_titanet_init(model_path: *const c_char, n_threads: i32) -> *mut c_void;
705    pub fn crispasr_titanet_free(ctx: *mut c_void);
706    pub fn crispasr_titanet_embed(
707        ctx: *mut c_void,
708        pcm_16k: *const c_float,
709        n_samples: i32,
710        out: *mut c_float,
711    ) -> i32;
712    pub fn crispasr_titanet_cosine_sim(a: *const c_float, b: *const c_float, dim: i32) -> c_float;
713
714    // Speaker profile database
715    pub fn crispasr_speaker_db_load(dir_path: *const c_char) -> *mut c_void;
716    pub fn crispasr_speaker_db_free(db: *mut c_void);
717    pub fn crispasr_speaker_db_count(db: *const c_void) -> i32;
718    pub fn crispasr_speaker_db_match(
719        db: *const c_void,
720        embedding: *const c_float,
721        dim: i32,
722        threshold: c_float,
723        out_name: *mut c_char,
724        out_cap: i32,
725    ) -> c_float;
726    pub fn crispasr_speaker_db_enroll(
727        dir_path: *const c_char,
728        name: *const c_char,
729        embedding: *const c_float,
730        dim: i32,
731    ) -> i32;
732
733    // Pluggable speaker embedder + agglomerative clustering + pyannote
734    // cache (issue #107 P6). Same building blocks as the CLI's
735    // --diarize-embedder path; expose them so Rust callers can compose
736    // the diarize pipeline without round-tripping through the CLI.
737
738    /// Build a pluggable speaker embedder. `model_spec` is one of
739    /// `"auto"`, `"titanet"`, `"indextts"`, `"indextts-bigvgan"`,
740    /// `"ecapa"`, or a `.gguf` path. Returns null on failure.
741    pub fn crispasr_speaker_embedder_make_abi(
742        model_spec: *const c_char,
743        n_threads: i32,
744        cache_dir: *const c_char,
745    ) -> *mut c_void;
746
747    pub fn crispasr_speaker_embedder_free_abi(embedder: *mut c_void);
748
749    /// Output embedding dimension (e.g. 192 for TitaNet, 512 for
750    /// IndexTTS-BigVGAN).
751    pub fn crispasr_speaker_embedder_dim_abi(embedder: *const c_void) -> i32;
752
753    /// Extract one embedding. `out` must hold at least `dim()` floats.
754    /// Returns 1 on success, 0 if the model rejected the input.
755    pub fn crispasr_speaker_embedder_embed_abi(
756        embedder: *mut c_void,
757        pcm_16k: *const c_float,
758        n_samples: i32,
759        out: *mut c_float,
760    ) -> i32;
761
762    pub fn crispasr_speaker_embedder_name_abi(embedder: *const c_void) -> *const c_char;
763
764    /// Agglomerative single-linkage cosine clustering. `embeddings` is
765    /// a row-major `n × dim` buffer of (ideally L2-normalized) vectors.
766    /// `labels_out` receives one cluster ID per input in `[0, k)`.
767    /// Returns the cluster count `k`, or -1 on invalid arguments.
768    pub fn crispasr_speaker_cluster_abi(
769        embeddings: *const c_float,
770        n: i32,
771        dim: i32,
772        merge_threshold: c_float,
773        max_speakers: i32,
774        labels_out: *mut i32,
775    ) -> i32;
776
777    /// Pre-compute pyannote-seg posteriors over a full audio buffer.
778    /// Returns an opaque cache or null on failure. Free with
779    /// `crispasr_pyannote_cache_free_abi`.
780    pub fn crispasr_pyannote_cache_compute_abi(
781        full_audio: *const c_float,
782        n_samples: i32,
783        model_path: *const c_char,
784        n_threads: i32,
785    ) -> *mut c_void;
786
787    pub fn crispasr_pyannote_cache_free_abi(cache: *mut c_void);
788
789    /// Score `segs` against the cached posteriors. `slice_t0_cs` is the
790    /// absolute centisecond at which the cache buffer starts (typically
791    /// 0 — the cache covers the whole input audio).
792    pub fn crispasr_pyannote_cache_apply_abi(
793        cache: *const c_void,
794        slice_t0_cs: i64,
795        segs: *mut CrispasrDiarizeSegAbi,
796        n_segs: i32,
797    ) -> i32;
798
799    // --- params_set_* on whisper_full_params (full C-ABI parity) ---
800    pub fn crispasr_params_set_language(p: *mut WhisperFullParams, lang: *const c_char);
801    pub fn crispasr_params_set_translate(p: *mut WhisperFullParams, v: c_int);
802    pub fn crispasr_params_set_detect_language(p: *mut WhisperFullParams, v: c_int);
803    pub fn crispasr_params_set_token_timestamps(p: *mut WhisperFullParams, v: c_int);
804    pub fn crispasr_params_set_n_threads(p: *mut WhisperFullParams, n: c_int);
805    pub fn crispasr_params_set_max_len(p: *mut WhisperFullParams, n: c_int);
806    pub fn crispasr_params_set_best_of(p: *mut WhisperFullParams, n: c_int);
807    pub fn crispasr_params_set_split_on_word(p: *mut WhisperFullParams, v: c_int);
808    pub fn crispasr_params_set_no_context(p: *mut WhisperFullParams, v: c_int);
809    pub fn crispasr_params_set_single_segment(p: *mut WhisperFullParams, v: c_int);
810    pub fn crispasr_params_set_print_realtime(p: *mut WhisperFullParams, v: c_int);
811    pub fn crispasr_params_set_print_progress(p: *mut WhisperFullParams, v: c_int);
812    pub fn crispasr_params_set_print_timestamps(p: *mut WhisperFullParams, v: c_int);
813    pub fn crispasr_params_set_print_special(p: *mut WhisperFullParams, v: c_int);
814    pub fn crispasr_params_set_suppress_blank(p: *mut WhisperFullParams, v: c_int);
815    pub fn crispasr_params_set_temperature(p: *mut WhisperFullParams, t: c_float);
816    pub fn crispasr_params_set_max_tokens(p: *mut WhisperFullParams, n: c_int);
817    pub fn crispasr_params_set_initial_prompt(p: *mut WhisperFullParams, prompt: *const c_char);
818    pub fn crispasr_params_set_alt_n(p: *mut WhisperFullParams, n: c_int);
819
820    // --- Token-level accessors ---
821    pub fn crispasr_token_t0(ctx: *mut WhisperContext, i_seg: c_int, i_tok: c_int) -> i64;
822    pub fn crispasr_token_t1(ctx: *mut WhisperContext, i_seg: c_int, i_tok: c_int) -> i64;
823    pub fn crispasr_token_p(ctx: *mut WhisperContext, i_seg: c_int, i_tok: c_int) -> c_float;
824    pub fn crispasr_token_n_alts(ctx: *mut WhisperContext, i_seg: c_int, i_tok: c_int) -> c_int;
825    pub fn crispasr_token_alt_id(
826        ctx: *mut WhisperContext,
827        i_seg: c_int,
828        i_tok: c_int,
829        i_alt: c_int,
830    ) -> i32;
831    pub fn crispasr_token_alt_p(
832        ctx: *mut WhisperContext,
833        i_seg: c_int,
834        i_tok: c_int,
835        i_alt: c_int,
836    ) -> c_float;
837    pub fn crispasr_token_alt_text(
838        ctx: *mut WhisperContext,
839        i_seg: c_int,
840        i_tok: c_int,
841        i_alt: c_int,
842        out: *mut c_char,
843        out_cap: c_int,
844    ) -> c_int;
845
846    // --- Language detection (whisper context) ---
847    pub fn crispasr_detect_language(
848        ctx: *mut WhisperContext,
849        pcm: *const c_float,
850        n_samples: c_int,
851        n_threads: c_int,
852        out_code: *mut c_char,
853        out_cap: c_int,
854    ) -> c_float;
855
856    // --- VAD ---
857    pub fn crispasr_vad_segments(
858        vad_model_path: *const c_char,
859        pcm: *const c_float,
860        n_samples: c_int,
861        sample_rate: c_int,
862        threshold: c_float,
863        min_speech_ms: c_int,
864        min_silence_ms: c_int,
865        n_threads: c_int,
866        use_gpu: c_int,
867        out_spans: *mut *mut c_float,
868    ) -> c_int;
869    pub fn crispasr_vad_slices(
870        vad_model_path: *const c_char,
871        pcm: *const c_float,
872        n_samples: c_int,
873        sample_rate: c_int,
874        threshold: c_float,
875        min_speech_ms: c_int,
876        min_silence_ms: c_int,
877        speech_pad_ms: c_int,
878        max_chunk_duration_s: c_float,
879        n_threads: c_int,
880        out_spans: *mut *mut c_float,
881    ) -> c_int;
882    pub fn crispasr_vad_free(spans: *mut c_float);
883
884    // --- LCS dedup ---
885    pub fn crispasr_lcs_dedup_prefix_count(
886        prev_tail_tokens: *const i32,
887        n_prev: c_int,
888        curr_tokens: *const i32,
889        n_curr: c_int,
890        min_lcs_length: c_int,
891    ) -> c_int;
892
893    // --- Streaming (whisper context) ---
894    pub fn crispasr_stream_open(
895        ctx: *mut WhisperContext,
896        n_threads: c_int,
897        step_ms: c_int,
898        length_ms: c_int,
899        keep_ms: c_int,
900        language: *const c_char,
901        translate: c_int,
902    ) -> *mut CrispasrStream;
903
904    // --- Direct Parakeet API ---
905    pub fn crispasr_parakeet_init(
906        model_path: *const c_char,
907        n_threads: c_int,
908        use_flash: c_int,
909    ) -> *mut c_void;
910    pub fn crispasr_parakeet_free(ctx: *mut c_void);
911    pub fn crispasr_parakeet_transcribe(
912        ctx: *mut c_void,
913        pcm: *const c_float,
914        n_samples: c_int,
915        language: *const c_char,
916    ) -> *mut c_void;
917    pub fn crispasr_parakeet_result_text(r: *mut c_void) -> *const c_char;
918    pub fn crispasr_parakeet_result_n_words(r: *mut c_void) -> c_int;
919    pub fn crispasr_parakeet_result_word_text(r: *mut c_void, i: c_int) -> *const c_char;
920    pub fn crispasr_parakeet_result_word_t0(r: *mut c_void, i: c_int) -> i64;
921    pub fn crispasr_parakeet_result_word_t1(r: *mut c_void, i: c_int) -> i64;
922    pub fn crispasr_parakeet_result_n_tokens(r: *mut c_void) -> c_int;
923    pub fn crispasr_parakeet_result_token_text(r: *mut c_void, i: c_int) -> *const c_char;
924    pub fn crispasr_parakeet_result_token_t0(r: *mut c_void, i: c_int) -> i64;
925    pub fn crispasr_parakeet_result_token_t1(r: *mut c_void, i: c_int) -> i64;
926    pub fn crispasr_parakeet_result_token_p(r: *mut c_void, i: c_int) -> c_float;
927    pub fn crispasr_parakeet_result_free(r: *mut c_void);
928
929    // --- RNNoise audio enhancement ---
930    pub fn crispasr_enhance_audio_rnnoise(
931        in_pcm: *const c_float,
932        n_samples: i32,
933        out_pcm: *mut c_float,
934        out_cap: i32,
935    ) -> c_int;
936
937    // --- Session open with params ---
938    pub fn crispasr_session_open_with_params(
939        model_path: *const c_char,
940        backend_name: *const c_char,
941        params: *const c_void,
942    ) -> *mut CrispasrSession;
943
944    // --- Session result word alts ---
945    pub fn crispasr_session_result_word_n_alts(
946        r: *mut CrispasrSessionResult,
947        i_seg: c_int,
948        i_word: c_int,
949    ) -> c_int;
950    pub fn crispasr_session_result_word_alt_text(
951        r: *mut CrispasrSessionResult,
952        i_seg: c_int,
953        i_word: c_int,
954        i_alt: c_int,
955    ) -> *const c_char;
956    pub fn crispasr_session_result_word_alt_p(
957        r: *mut CrispasrSessionResult,
958        i_seg: c_int,
959        i_word: c_int,
960        i_alt: c_int,
961    ) -> c_float;
962}