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(
517        s: *mut CrispasrSession,
518        phonemes: *const c_char,
519    ) -> c_int;
520    // qwen3-tts variant detection (returns 0/1; 0 also covers "not qwen3-tts").
521    pub fn crispasr_session_is_custom_voice(s: *mut CrispasrSession) -> c_int;
522    pub fn crispasr_session_is_voice_design(s: *mut CrispasrSession) -> c_int;
523    pub fn crispasr_session_synthesize(
524        s: *mut CrispasrSession,
525        text: *const c_char,
526        out_n_samples: *mut c_int,
527    ) -> *mut f32;
528    // Speech-to-Speech — audio in -> audio out via a single model pass. Supported
529    // on S2S-capable backends (lfm2-audio, mini-omni2, sidon, voxcpm2-vae). Returns
530    // malloc'd f32 PCM (free with `crispasr_pcm_free`); `out_text`, if non-null,
531    // receives the malloc'd intermediate transcript (free with
532    // `crispasr_session_translate_text_free`). Returns null on failure / unsupported.
533    pub fn crispasr_session_speech_to_speech(
534        s: *mut CrispasrSession,
535        in_samples: *const f32,
536        n_in_samples: c_int,
537        out_text: *mut *mut c_char,
538        out_n_samples: *mut c_int,
539    ) -> *mut f32;
540    // UNMARKED synthesis (no watermark/disclosure). Hard-refused unless
541    // `crispasr_session_accept_marking_responsibility` was called first. Returns
542    // malloc'd f32 PCM (free with `crispasr_pcm_free`); null on refusal/failure.
543    pub fn crispasr_session_synthesize_raw(
544        s: *mut CrispasrSession,
545        text: *const c_char,
546        out_n_samples: *mut c_int,
547    ) -> *mut f32;
548    // Attest that the integrator accepts AI-content marking/disclosure
549    // responsibility (EU AI Act Art. 50). REQUIRED before `synthesize_raw`.
550    pub fn crispasr_session_accept_marking_responsibility(
551        s: *mut CrispasrSession,
552        attestation: *const c_char,
553    ) -> c_int;
554    // Sample rate the backend expects for input PCM (16000 for Whisper-family,
555    // the model's native rate otherwise; 0 on error). Pair with s2s/synthesize to
556    // feed input at the right rate.
557    pub fn crispasr_session_input_sample_rate(s: *mut CrispasrSession) -> c_int;
558    pub fn crispasr_pcm_free(pcm: *mut f32);
559    // Drop the kokoro per-session phoneme cache. No-op for non-kokoro
560    // backends. Returns 0 on success, -1 if `s` is null. (PLAN #56 #5)
561    pub fn crispasr_session_kokoro_clear_phoneme_cache(s: *mut CrispasrSession) -> c_int;
562
563    // --- Sticky session-state setters (PLAN #59 partial unblock) ---
564    pub fn crispasr_session_set_source_language(
565        s: *mut CrispasrSession,
566        lang: *const c_char,
567    ) -> c_int;
568    pub fn crispasr_session_set_target_language(
569        s: *mut CrispasrSession,
570        lang: *const c_char,
571    ) -> c_int;
572    pub fn crispasr_session_set_punctuation(s: *mut CrispasrSession, enable: c_int) -> c_int;
573    pub fn crispasr_session_set_punc_model(
574        s: *mut CrispasrSession,
575        punc_model: *const c_char,
576    ) -> c_int;
577    pub fn crispasr_session_set_hotwords(
578        s: *mut CrispasrSession,
579        hotwords: *const c_char,
580        boost: c_float,
581    ) -> c_int;
582    pub fn crispasr_session_set_g2p_dict(s: *mut CrispasrSession, source: *const c_char) -> c_int;
583    pub fn crispasr_session_set_speaker_id(s: *mut CrispasrSession, id: c_int) -> c_int;
584    pub fn crispasr_session_set_translate(s: *mut CrispasrSession, enable: c_int) -> c_int;
585    // --- Text-to-text translation (m2m100 / m2m100-wmt21 / madlad / gemma4-e2b) ---
586    //
587    // Distinct from `crispasr_session_set_translate` above, which is the
588    // *audio-side* Whisper sticky flag (PCM input → English text out).
589    // This one translates an already-extracted Rust string between
590    // arbitrary language pairs via whichever MT-capable backend the
591    // session loaded.  Returns a malloc'd UTF-8 buffer that the caller
592    // MUST release via `crispasr_session_translate_text_free` (mirrors
593    // the punc-side ownership pattern).  Returns nullptr on:
594    //   * any input pointer being null,
595    //   * the session not having a CAP_TRANSLATE backend loaded,
596    //   * the backend's internal translate routine erroring out.
597    //
598    // `max_tokens` caps the decoder output length.  Pass `<= 0` to
599    // fall back to the C++ default (200 for m2m100).
600    pub fn crispasr_session_translate_text(
601        s: *mut CrispasrSession,
602        text: *const c_char,
603        src_lang: *const c_char,
604        tgt_lang: *const c_char,
605        max_tokens: c_int,
606    ) -> *mut c_char;
607    // Free a buffer previously returned by `crispasr_session_translate_text`.
608    // No-op when `text` is null.  Calling `libc::free` directly also works
609    // (the C++ side just delegates to `free()`), but routing through this
610    // symbol keeps ownership symmetric and protects callers if the C++
611    // side ever switches allocators.
612    pub fn crispasr_session_translate_text_free(text: *mut c_char);
613    pub fn crispasr_session_set_temperature(
614        s: *mut CrispasrSession,
615        temperature: c_float,
616        seed: u64,
617    ) -> c_int;
618    pub fn crispasr_session_set_tts_seed(s: *mut CrispasrSession, seed: u64) -> c_int;
619    pub fn crispasr_session_set_max_new_tokens(
620        s: *mut CrispasrSession,
621        max_new_tokens: c_int,
622    ) -> c_int;
623    pub fn crispasr_session_set_frequency_penalty(
624        s: *mut CrispasrSession,
625        penalty: c_float,
626    ) -> c_int;
627    pub fn crispasr_session_set_tts_steps(s: *mut CrispasrSession, steps: c_int) -> c_int;
628    pub fn crispasr_session_set_tts_num_candidates(s: *mut CrispasrSession, n: c_int) -> c_int;
629    pub fn crispasr_session_set_top_p(s: *mut CrispasrSession, top_p: c_float) -> c_int;
630    pub fn crispasr_session_set_top_k(s: *mut CrispasrSession, top_k: c_int) -> c_int;
631    pub fn crispasr_session_set_do_sample(s: *mut CrispasrSession, enable: c_int) -> c_int;
632    pub fn crispasr_session_set_min_p(s: *mut CrispasrSession, min_p: c_float) -> c_int;
633    pub fn crispasr_session_set_repetition_penalty(s: *mut CrispasrSession, r: c_float) -> c_int;
634    pub fn crispasr_session_set_cfg_weight(s: *mut CrispasrSession, cfg_weight: c_float) -> c_int;
635    pub fn crispasr_session_set_tts_noise_temp(
636        s: *mut CrispasrSession,
637        noise_temp: c_float,
638    ) -> c_int;
639    pub fn crispasr_session_set_exaggeration(
640        s: *mut CrispasrSession,
641        exaggeration: c_float,
642    ) -> c_int;
643    pub fn crispasr_session_set_max_speech_tokens(s: *mut CrispasrSession, n: c_int) -> c_int;
644    pub fn crispasr_session_set_length_scale(s: *mut CrispasrSession, scale: c_float) -> c_int;
645    pub fn crispasr_session_set_best_of(s: *mut CrispasrSession, n: c_int) -> c_int;
646    pub fn crispasr_session_set_beam_size(s: *mut CrispasrSession, n: c_int) -> c_int;
647    pub fn crispasr_session_set_return_logits(s: *mut CrispasrSession, enable: c_int) -> c_int;
648    pub fn crispasr_session_set_grammar_text(
649        s: *mut CrispasrSession,
650        gbnf_text: *const c_char,
651        root_rule: *const c_char,
652        penalty: c_float,
653    ) -> c_int;
654    pub fn crispasr_session_set_fallback_thresholds(
655        s: *mut CrispasrSession,
656        entropy_thold: c_float,
657        logprob_thold: c_float,
658        no_speech_thold: c_float,
659        temperature_inc: c_float,
660    ) -> c_int;
661    pub fn crispasr_session_set_alt_n(s: *mut CrispasrSession, n: c_int) -> c_int;
662    pub fn crispasr_session_set_whisper_decode_extras(
663        s: *mut CrispasrSession,
664        suppress_nst: c_int,
665        suppress_regex: *const c_char,
666        carry_initial_prompt: c_int,
667    ) -> c_int;
668    pub fn crispasr_session_set_ask(s: *mut CrispasrSession, prompt: *const c_char) -> c_int;
669    pub fn crispasr_session_detect_language(
670        s: *mut CrispasrSession,
671        pcm: *const c_float,
672        n_samples: c_int,
673        lid_model_path: *const c_char,
674        method: c_int,
675        out_lang: *mut c_char,
676        out_lang_cap: c_int,
677        out_prob: *mut c_float,
678    ) -> c_int;
679
680    // --- Text-LID (P13.5 Phase 7) ---
681    //
682    // Detect the language of a UTF-8 text string via the internal
683    // `text_lid_dispatch` façade — routes to CLD3 (ISO 639-1, 109
684    // labels) or GlotLID-V3 / LID-176 fastText (ISO 639-3 + script,
685    // 2102 or 176 labels) based on the GGUF's architecture key.
686    // Label format follows whichever backend the GGUF loads as —
687    // see the C-API doc-comment for normalisation guidance.
688    //
689    // Returns:
690    //   *  0 — success; `out_label_buf` + `out_confidence` populated.
691    //   * -1 — invalid args (null pointer or out_label_cap <= 0).
692    //   *  1 — dispatcher init / predict failure.
693    //   *  2 — output buffer too small for the predicted label.
694    pub fn crispasr_text_detect_language(
695        text: *const c_char,
696        model_path: *const c_char,
697        n_threads: c_int,
698        out_label_buf: *mut c_char,
699        out_label_cap: c_int,
700        out_confidence: *mut c_float,
701    ) -> c_int;
702
703    pub fn crispasr_detect_backend_from_gguf(
704        path: *const c_char,
705        out_name: *mut c_char,
706        out_cap: c_int,
707    ) -> c_int;
708
709    // --- FireRedPunc punctuation restoration ---
710    pub fn crispasr_punc_init(model_path: *const c_char) -> *mut c_void;
711    pub fn crispasr_punc_process(ctx: *mut c_void, text: *const c_char) -> *mut c_char;
712    pub fn crispasr_punc_free_text(text: *mut c_char);
713    pub fn crispasr_punc_free(ctx: *mut c_void);
714
715    pub fn crispasr_c_api_version() -> *const c_char;
716
717    // --- Kokoro per-language model + voice routing (PLAN #56 opt 2b) ---
718    // See `src/kokoro.h` for full semantics.
719    pub fn crispasr_kokoro_lang_is_german_abi(lang: *const c_char) -> bool;
720    pub fn crispasr_kokoro_lang_has_native_voice_abi(lang: *const c_char) -> bool;
721    pub fn crispasr_kokoro_resolve_model_for_lang_abi(
722        model_path: *const c_char,
723        lang: *const c_char,
724        out_path: *mut c_char,
725        out_path_len: c_int,
726    ) -> c_int;
727    pub fn crispasr_kokoro_resolve_fallback_voice_abi(
728        model_path: *const c_char,
729        lang: *const c_char,
730        out_path: *mut c_char,
731        out_path_len: c_int,
732        out_picked: *mut c_char,
733        out_picked_len: c_int,
734    ) -> c_int;
735
736    // TitaNet speaker verification
737    pub fn crispasr_titanet_init(model_path: *const c_char, n_threads: i32) -> *mut c_void;
738    pub fn crispasr_titanet_free(ctx: *mut c_void);
739    pub fn crispasr_titanet_embed(
740        ctx: *mut c_void,
741        pcm_16k: *const c_float,
742        n_samples: i32,
743        out: *mut c_float,
744    ) -> i32;
745    pub fn crispasr_titanet_cosine_sim(a: *const c_float, b: *const c_float, dim: i32) -> c_float;
746
747    // Speaker profile database
748    pub fn crispasr_speaker_db_load(dir_path: *const c_char) -> *mut c_void;
749    pub fn crispasr_speaker_db_free(db: *mut c_void);
750    pub fn crispasr_speaker_db_count(db: *const c_void) -> i32;
751    pub fn crispasr_speaker_db_match(
752        db: *const c_void,
753        embedding: *const c_float,
754        dim: i32,
755        threshold: c_float,
756        out_name: *mut c_char,
757        out_cap: i32,
758    ) -> c_float;
759    pub fn crispasr_speaker_db_enroll(
760        dir_path: *const c_char,
761        name: *const c_char,
762        embedding: *const c_float,
763        dim: i32,
764    ) -> i32;
765
766    // Pluggable speaker embedder + agglomerative clustering + pyannote
767    // cache (issue #107 P6). Same building blocks as the CLI's
768    // --diarize-embedder path; expose them so Rust callers can compose
769    // the diarize pipeline without round-tripping through the CLI.
770
771    /// Build a pluggable speaker embedder. `model_spec` is one of
772    /// `"auto"`, `"titanet"`, `"indextts"`, `"indextts-bigvgan"`,
773    /// `"ecapa"`, or a `.gguf` path. Returns null on failure.
774    pub fn crispasr_speaker_embedder_make_abi(
775        model_spec: *const c_char,
776        n_threads: i32,
777        cache_dir: *const c_char,
778    ) -> *mut c_void;
779
780    pub fn crispasr_speaker_embedder_free_abi(embedder: *mut c_void);
781
782    /// Output embedding dimension (e.g. 192 for TitaNet, 512 for
783    /// IndexTTS-BigVGAN).
784    pub fn crispasr_speaker_embedder_dim_abi(embedder: *const c_void) -> i32;
785
786    /// Extract one embedding. `out` must hold at least `dim()` floats.
787    /// Returns 1 on success, 0 if the model rejected the input.
788    pub fn crispasr_speaker_embedder_embed_abi(
789        embedder: *mut c_void,
790        pcm_16k: *const c_float,
791        n_samples: i32,
792        out: *mut c_float,
793    ) -> i32;
794
795    pub fn crispasr_speaker_embedder_name_abi(embedder: *const c_void) -> *const c_char;
796
797    /// Agglomerative single-linkage cosine clustering. `embeddings` is
798    /// a row-major `n × dim` buffer of (ideally L2-normalized) vectors.
799    /// `labels_out` receives one cluster ID per input in `[0, k)`.
800    /// Returns the cluster count `k`, or -1 on invalid arguments.
801    pub fn crispasr_speaker_cluster_abi(
802        embeddings: *const c_float,
803        n: i32,
804        dim: i32,
805        merge_threshold: c_float,
806        max_speakers: i32,
807        labels_out: *mut i32,
808    ) -> i32;
809
810    /// Pre-compute pyannote-seg posteriors over a full audio buffer.
811    /// Returns an opaque cache or null on failure. Free with
812    /// `crispasr_pyannote_cache_free_abi`.
813    pub fn crispasr_pyannote_cache_compute_abi(
814        full_audio: *const c_float,
815        n_samples: i32,
816        model_path: *const c_char,
817        n_threads: i32,
818    ) -> *mut c_void;
819
820    pub fn crispasr_pyannote_cache_free_abi(cache: *mut c_void);
821
822    /// Score `segs` against the cached posteriors. `slice_t0_cs` is the
823    /// absolute centisecond at which the cache buffer starts (typically
824    /// 0 — the cache covers the whole input audio).
825    pub fn crispasr_pyannote_cache_apply_abi(
826        cache: *const c_void,
827        slice_t0_cs: i64,
828        segs: *mut CrispasrDiarizeSegAbi,
829        n_segs: i32,
830    ) -> i32;
831
832    // --- params_set_* on whisper_full_params (full C-ABI parity) ---
833    pub fn crispasr_params_set_language(p: *mut WhisperFullParams, lang: *const c_char);
834    pub fn crispasr_params_set_translate(p: *mut WhisperFullParams, v: c_int);
835    pub fn crispasr_params_set_detect_language(p: *mut WhisperFullParams, v: c_int);
836    pub fn crispasr_params_set_token_timestamps(p: *mut WhisperFullParams, v: c_int);
837    pub fn crispasr_params_set_n_threads(p: *mut WhisperFullParams, n: c_int);
838    pub fn crispasr_params_set_max_len(p: *mut WhisperFullParams, n: c_int);
839    pub fn crispasr_params_set_best_of(p: *mut WhisperFullParams, n: c_int);
840    pub fn crispasr_params_set_split_on_word(p: *mut WhisperFullParams, v: c_int);
841    pub fn crispasr_params_set_no_context(p: *mut WhisperFullParams, v: c_int);
842    pub fn crispasr_params_set_single_segment(p: *mut WhisperFullParams, v: c_int);
843    pub fn crispasr_params_set_print_realtime(p: *mut WhisperFullParams, v: c_int);
844    pub fn crispasr_params_set_print_progress(p: *mut WhisperFullParams, v: c_int);
845    pub fn crispasr_params_set_print_timestamps(p: *mut WhisperFullParams, v: c_int);
846    pub fn crispasr_params_set_print_special(p: *mut WhisperFullParams, v: c_int);
847    pub fn crispasr_params_set_suppress_blank(p: *mut WhisperFullParams, v: c_int);
848    pub fn crispasr_params_set_temperature(p: *mut WhisperFullParams, t: c_float);
849    pub fn crispasr_params_set_max_tokens(p: *mut WhisperFullParams, n: c_int);
850    pub fn crispasr_params_set_initial_prompt(p: *mut WhisperFullParams, prompt: *const c_char);
851    pub fn crispasr_params_set_alt_n(p: *mut WhisperFullParams, n: c_int);
852
853    // --- Token-level accessors ---
854    pub fn crispasr_token_t0(ctx: *mut WhisperContext, i_seg: c_int, i_tok: c_int) -> i64;
855    pub fn crispasr_token_t1(ctx: *mut WhisperContext, i_seg: c_int, i_tok: c_int) -> i64;
856    pub fn crispasr_token_p(ctx: *mut WhisperContext, i_seg: c_int, i_tok: c_int) -> c_float;
857    pub fn crispasr_token_n_alts(ctx: *mut WhisperContext, i_seg: c_int, i_tok: c_int) -> c_int;
858    pub fn crispasr_token_alt_id(
859        ctx: *mut WhisperContext,
860        i_seg: c_int,
861        i_tok: c_int,
862        i_alt: c_int,
863    ) -> i32;
864    pub fn crispasr_token_alt_p(
865        ctx: *mut WhisperContext,
866        i_seg: c_int,
867        i_tok: c_int,
868        i_alt: c_int,
869    ) -> c_float;
870    pub fn crispasr_token_alt_text(
871        ctx: *mut WhisperContext,
872        i_seg: c_int,
873        i_tok: c_int,
874        i_alt: c_int,
875        out: *mut c_char,
876        out_cap: c_int,
877    ) -> c_int;
878
879    // --- Language detection (whisper context) ---
880    pub fn crispasr_detect_language(
881        ctx: *mut WhisperContext,
882        pcm: *const c_float,
883        n_samples: c_int,
884        n_threads: c_int,
885        out_code: *mut c_char,
886        out_cap: c_int,
887    ) -> c_float;
888
889    // --- VAD ---
890    pub fn crispasr_vad_segments(
891        vad_model_path: *const c_char,
892        pcm: *const c_float,
893        n_samples: c_int,
894        sample_rate: c_int,
895        threshold: c_float,
896        min_speech_ms: c_int,
897        min_silence_ms: c_int,
898        n_threads: c_int,
899        use_gpu: c_int,
900        out_spans: *mut *mut c_float,
901    ) -> c_int;
902    pub fn crispasr_vad_slices(
903        vad_model_path: *const c_char,
904        pcm: *const c_float,
905        n_samples: c_int,
906        sample_rate: c_int,
907        threshold: c_float,
908        min_speech_ms: c_int,
909        min_silence_ms: c_int,
910        speech_pad_ms: c_int,
911        max_chunk_duration_s: c_float,
912        n_threads: c_int,
913        out_spans: *mut *mut c_float,
914    ) -> c_int;
915    pub fn crispasr_vad_free(spans: *mut c_float);
916
917    // --- LCS dedup ---
918    pub fn crispasr_lcs_dedup_prefix_count(
919        prev_tail_tokens: *const i32,
920        n_prev: c_int,
921        curr_tokens: *const i32,
922        n_curr: c_int,
923        min_lcs_length: c_int,
924    ) -> c_int;
925
926    // --- Streaming (whisper context) ---
927    pub fn crispasr_stream_open(
928        ctx: *mut WhisperContext,
929        n_threads: c_int,
930        step_ms: c_int,
931        length_ms: c_int,
932        keep_ms: c_int,
933        language: *const c_char,
934        translate: c_int,
935    ) -> *mut CrispasrStream;
936
937    // --- Direct Parakeet API ---
938    pub fn crispasr_parakeet_init(
939        model_path: *const c_char,
940        n_threads: c_int,
941        use_flash: c_int,
942    ) -> *mut c_void;
943    pub fn crispasr_parakeet_free(ctx: *mut c_void);
944    pub fn crispasr_parakeet_transcribe(
945        ctx: *mut c_void,
946        pcm: *const c_float,
947        n_samples: c_int,
948        language: *const c_char,
949    ) -> *mut c_void;
950    pub fn crispasr_parakeet_result_text(r: *mut c_void) -> *const c_char;
951    pub fn crispasr_parakeet_result_n_words(r: *mut c_void) -> c_int;
952    pub fn crispasr_parakeet_result_word_text(r: *mut c_void, i: c_int) -> *const c_char;
953    pub fn crispasr_parakeet_result_word_t0(r: *mut c_void, i: c_int) -> i64;
954    pub fn crispasr_parakeet_result_word_t1(r: *mut c_void, i: c_int) -> i64;
955    pub fn crispasr_parakeet_result_n_tokens(r: *mut c_void) -> c_int;
956    pub fn crispasr_parakeet_result_token_text(r: *mut c_void, i: c_int) -> *const c_char;
957    pub fn crispasr_parakeet_result_token_t0(r: *mut c_void, i: c_int) -> i64;
958    pub fn crispasr_parakeet_result_token_t1(r: *mut c_void, i: c_int) -> i64;
959    pub fn crispasr_parakeet_result_token_p(r: *mut c_void, i: c_int) -> c_float;
960    pub fn crispasr_parakeet_result_free(r: *mut c_void);
961
962    // --- RNNoise audio enhancement ---
963    pub fn crispasr_enhance_audio_rnnoise(
964        in_pcm: *const c_float,
965        n_samples: i32,
966        out_pcm: *mut c_float,
967        out_cap: i32,
968    ) -> c_int;
969
970    // --- Session open with params ---
971    pub fn crispasr_session_open_with_params(
972        model_path: *const c_char,
973        backend_name: *const c_char,
974        params: *const c_void,
975    ) -> *mut CrispasrSession;
976
977    // --- Session result word alts ---
978    pub fn crispasr_session_result_word_n_alts(
979        r: *mut CrispasrSessionResult,
980        i_seg: c_int,
981        i_word: c_int,
982    ) -> c_int;
983    pub fn crispasr_session_result_word_alt_text(
984        r: *mut CrispasrSessionResult,
985        i_seg: c_int,
986        i_word: c_int,
987        i_alt: c_int,
988    ) -> *const c_char;
989    pub fn crispasr_session_result_word_alt_p(
990        r: *mut CrispasrSessionResult,
991        i_seg: c_int,
992        i_word: c_int,
993        i_alt: c_int,
994    ) -> c_float;
995}