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..4: 0 = Energy, 1 = Xcorr, 2 = VadTurns, 3 = Pyannote,
158/// 4 = FoxNose. `pyannote_model_path` is required for Pyannote,
159/// `foxnose_embedder_path` for FoxNose; each is ignored otherwise.
160///
161/// This layout is hand-maintained and MUST match
162/// `crispasr_diarize_opts_abi` in `src/crispasr_c_api.cpp`, which is
163/// APPEND-ONLY: the C side reads every field unconditionally, so a
164/// short struct here is an out-of-bounds read even for methods 0..3.
165#[repr(C)]
166#[derive(Clone, Copy, Debug)]
167pub struct CrispasrDiarizeOptsAbi {
168    pub method: c_int,
169    pub n_threads: c_int,
170    pub slice_t0_cs: i64,
171    pub pyannote_model_path: *const c_char,
172    // #324 FoxNose (method 4). Ignored by the other methods.
173    pub foxnose_embedder_path: *const c_char,
174    /// 0 -> 1
175    pub min_speakers: c_int,
176    /// 0 -> 8
177    pub max_speakers: c_int,
178    /// >0 pins the speaker count and skips estimation
179    pub num_speakers: c_int,
180    pub _pad2: c_int,
181}
182
183extern "C" {
184    pub fn crispasr_session_open(
185        model_path: *const c_char,
186        n_threads: c_int,
187    ) -> *mut CrispasrSession;
188
189    pub fn crispasr_session_open_explicit(
190        model_path: *const c_char,
191        backend_name: *const c_char,
192        n_threads: c_int,
193    ) -> *mut CrispasrSession;
194
195    pub fn crispasr_session_backend(s: *mut CrispasrSession) -> *const c_char;
196
197    // CTC vocabulary access (Omni CTC backend). `n_vocab` is the number of
198    // SentencePiece pieces (0 for backends without an exposed CTC vocab);
199    // `token_text` maps an id in `[0, n_vocab)` to its raw piece (U+2581 marker
200    // intact), or "" when out of range / unsupported. Pairs with the result
201    // logits accessor to detokenize a greedy CTC decode.
202    pub fn crispasr_session_n_vocab(s: *mut CrispasrSession) -> c_int;
203    pub fn crispasr_session_token_text(s: *mut CrispasrSession, id: c_int) -> *const c_char;
204
205    // Acoustic language detected by the last transcribe, written into `out_buf`
206    // as an ISO-639-1 code (whisper only; other backends fall back to the
207    // source-language hint, then "unknown"). Returns the code length in bytes
208    // (not counting NUL) or -1 on bad args. Distinct from the text-LID pass.
209    pub fn crispasr_session_detected_language(
210        s: *mut CrispasrSession,
211        out_buf: *mut c_char,
212        out_cap: c_int,
213    ) -> c_int;
214
215    /// Write a comma-separated list of backend names the loaded dylib
216    /// was built with. Returns the number of bytes written (not counting
217    /// NUL) or a negative error.
218    pub fn crispasr_session_available_backends(out_csv: *mut c_char, out_cap: c_int) -> c_int;
219
220    pub fn crispasr_session_transcribe(
221        s: *mut CrispasrSession,
222        pcm: *const c_float,
223        n_samples: c_int,
224    ) -> *mut CrispasrSessionResult;
225
226    /// 0.4.9+: language-aware session transcribe. `language` is an
227    /// ISO 639-1 code or null/empty to keep the backend's historical
228    /// default. Backends that accept a source-language hint (whisper,
229    /// canary, cohere, voxtral, voxtral4b) honour it; others ignore
230    /// silently.
231    pub fn crispasr_session_transcribe_lang(
232        s: *mut CrispasrSession,
233        pcm: *const c_float,
234        n_samples: c_int,
235        language: *const c_char,
236    ) -> *mut CrispasrSessionResult;
237
238    /// 0.8.7+: chunked-encode transcribe (issue #208). Forces the
239    /// Parakeet backend through its bounded long-form path (overlapping
240    /// short-window transcribe-and-merge for non-JA models, streamed
241    /// encoder for the JA-only model) regardless of audio length, so long
242    /// files transcribe in bounded time AND recover the sections a single
243    /// full-length pass drops. `chunk_seconds <= 0` keeps the per-model
244    /// defaults; otherwise it sets the non-JA window length / the JA
245    /// streamed window. `overlap_seconds < 0` uses the default. For
246    /// non-Parakeet backends the chunk params are inert and this matches
247    /// `crispasr_session_transcribe_lang`.
248    pub fn crispasr_session_transcribe_chunked_lang(
249        s: *mut CrispasrSession,
250        pcm: *const c_float,
251        n_samples: c_int,
252        chunk_seconds: c_int,
253        overlap_seconds: c_int,
254        language: *const c_char,
255    ) -> *mut CrispasrSessionResult;
256
257    pub fn crispasr_session_transcribe_chunked(
258        s: *mut CrispasrSession,
259        pcm: *const c_float,
260        n_samples: c_int,
261        chunk_seconds: c_int,
262        overlap_seconds: c_int,
263    ) -> *mut CrispasrSessionResult;
264
265    /// 0.10.3+ (issue #208): register a per-session progress callback for
266    /// long-form (chunked) transcription. Fired once per finished window
267    /// with `(processed_samples, total_samples, user_data)`; `processed`
268    /// is monotonic and reaches `total` on the last window. Invoked on the
269    /// transcribe thread. Pass `None`/null `cb` to clear.
270    pub fn crispasr_session_set_progress_callback(
271        s: *mut CrispasrSession,
272        cb: CrispasrProgressCallback,
273        user_data: *mut c_void,
274    );
275
276    /// VAD-driven session transcribe. Runs Silero VAD on the PCM buffer,
277    /// merges short / overlong speech slices, stitches them into one
278    /// contiguous buffer with 0.1s silence gaps, calls the backend once,
279    /// then remaps segment + word timestamps back to original-audio
280    /// positions.
281    ///
282    /// `vad_model_path` must point to a Silero GGUF on disk. Pass a null
283    /// or empty `opts` pointer to use defaults (mirrors crispasr's
284    /// `whisper_vad_default_params`).
285    pub fn crispasr_session_transcribe_vad(
286        s: *mut CrispasrSession,
287        pcm: *const c_float,
288        n_samples: c_int,
289        sample_rate: c_int,
290        vad_model_path: *const c_char,
291        opts: *const CrispasrVadAbiOpts,
292    ) -> *mut CrispasrSessionResult;
293
294    /// 0.4.9+: language-aware VAD transcribe (same semantics as the
295    /// language kwarg on `crispasr_session_transcribe_lang`).
296    pub fn crispasr_session_transcribe_vad_lang(
297        s: *mut CrispasrSession,
298        pcm: *const c_float,
299        n_samples: c_int,
300        sample_rate: c_int,
301        vad_model_path: *const c_char,
302        opts: *const CrispasrVadAbiOpts,
303        language: *const c_char,
304    ) -> *mut CrispasrSessionResult;
305
306    /// Shared speaker diarization (0.4.5+). Writes a zero-based speaker
307    /// index into each `segs[i].speaker`. Returns 0 on success, 1 on
308    /// Pyannote model load failure, -1 on invalid args.
309    pub fn crispasr_diarize_segments_abi(
310        left_pcm: *const c_float,
311        right_pcm: *const c_float,
312        n_samples: c_int,
313        is_stereo: c_int,
314        segs: *mut CrispasrDiarizeSegAbi,
315        n_segs: c_int,
316        opts: *const CrispasrDiarizeOptsAbi,
317    ) -> c_int;
318
319    /// Shared language identification (0.4.6+). `method` is 0 for
320    /// whisper, 1 for silero. `model_path` is required. Fills
321    /// `out_lang_buf` with a null-terminated ISO 639-1 code. Returns 0
322    /// on success, -1 on invalid args, 1 on model / detect failure,
323    /// 2 when the output buffer is too small.
324    pub fn crispasr_detect_language_pcm(
325        samples: *const c_float,
326        n_samples: c_int,
327        method: c_int,
328        model_path: *const c_char,
329        n_threads: c_int,
330        use_gpu: c_int,
331        gpu_device: c_int,
332        flash_attn: c_int,
333        out_lang_buf: *mut c_char,
334        out_lang_cap: c_int,
335        out_confidence: *mut c_float,
336    ) -> c_int;
337
338    /// Shared CTC / forced-aligner word timings (0.4.7+).
339    /// Pass any `aligner_model` path — filenames containing
340    /// "forced-aligner" / "qwen3-fa" / "qwen3-forced" go through the
341    /// Qwen3-ForcedAligner path; everything else uses canary-ctc.
342    /// Returns a handle the caller must free with
343    /// [`crispasr_align_result_free`]. Returns null on failure.
344    pub fn crispasr_align_words_abi(
345        aligner_model: *const c_char,
346        transcript: *const c_char,
347        samples: *const c_float,
348        n_samples: c_int,
349        t_offset_cs: i64,
350        n_threads: c_int,
351    ) -> *mut CrispasrAlignResult;
352
353    pub fn crispasr_align_result_n_words(r: *mut CrispasrAlignResult) -> c_int;
354    pub fn crispasr_align_result_word_text(r: *mut CrispasrAlignResult, i: c_int) -> *const c_char;
355    pub fn crispasr_align_result_word_t0(r: *mut CrispasrAlignResult, i: c_int) -> i64;
356    pub fn crispasr_align_result_word_t1(r: *mut CrispasrAlignResult, i: c_int) -> i64;
357    pub fn crispasr_align_result_free(r: *mut CrispasrAlignResult);
358
359    /// Shared HF download + cache (0.4.8+). Writes the resolved path
360    /// into `out_buf`. Returns 0 on success, -1 on invalid args, 1 on
361    /// download failure, 2 when the output buffer is too small.
362    pub fn crispasr_cache_ensure_file_abi(
363        filename: *const c_char,
364        url: *const c_char,
365        quiet: c_int,
366        cache_dir_override: *const c_char,
367        out_buf: *mut c_char,
368        out_cap: c_int,
369    ) -> c_int;
370
371    /// Return the CrispASR cache directory (creating it if missing).
372    pub fn crispasr_cache_dir_abi(
373        cache_dir_override: *const c_char,
374        out_buf: *mut c_char,
375        out_cap: c_int,
376    ) -> c_int;
377
378    /// Shared known-model registry lookup by backend. 0 = hit, 1 = miss.
379    pub fn crispasr_registry_lookup_abi(
380        backend: *const c_char,
381        out_filename: *mut c_char,
382        filename_cap: c_int,
383        out_url: *mut c_char,
384        url_cap: c_int,
385        out_size: *mut c_char,
386        size_cap: c_int,
387    ) -> c_int;
388
389    /// Shared known-model registry lookup by filename (exact then fuzzy).
390    pub fn crispasr_registry_list_backends_abi(out_csv: *mut c_char, out_cap: c_int) -> c_int;
391
392    /// Describe the exact canonical artifact bundle downloaded by `-m auto`.
393    /// Returns its artifact count, 0 on miss, or a negative argument/buffer error.
394    pub fn crispasr_registry_default_bundle_info_abi(
395        backend: *const c_char,
396        out_backend: *mut c_char,
397        backend_cap: c_int,
398        out_license: *mut c_char,
399        license_cap: c_int,
400        out_requires_acceptance: *mut c_int,
401    ) -> c_int;
402
403    /// Read one default-bundle artifact by index. 0 = success.
404    pub fn crispasr_registry_default_bundle_artifact_abi(
405        backend: *const c_char,
406        index: c_int,
407        out_kind: *mut c_int,
408        out_filename: *mut c_char,
409        filename_cap: c_int,
410        out_url: *mut c_char,
411        url_cap: c_int,
412        out_size: *mut c_char,
413        size_cap: c_int,
414    ) -> c_int;
415
416    // --- Streaming (PLAN #62) — rolling-window decoder for whisper today ---
417    pub fn crispasr_session_stream_open(
418        s: *mut CrispasrSession,
419        n_threads: c_int,
420        step_ms: c_int,
421        length_ms: c_int,
422        keep_ms: c_int,
423        language: *const c_char,
424        translate: c_int,
425    ) -> *mut CrispasrStream;
426    pub fn crispasr_stream_feed(
427        s: *mut CrispasrStream,
428        pcm: *const c_float,
429        n_samples: c_int,
430    ) -> c_int;
431    pub fn crispasr_stream_get_text(
432        s: *mut CrispasrStream,
433        out_text: *mut c_char,
434        out_cap: c_int,
435        out_t0_s: *mut f64,
436        out_t1_s: *mut f64,
437        out_counter: *mut i64,
438    ) -> c_int;
439    pub fn crispasr_stream_flush(s: *mut CrispasrStream) -> c_int;
440    pub fn crispasr_stream_close(s: *mut CrispasrStream);
441
442    /// Toggle voxtral4b live-captions decode-during-feed (PLAN #7 phase 3).
443    /// No-op for backends that don't have audio-injection prompt decode.
444    /// Set BEFORE the first feed for clean semantics.
445    pub fn crispasr_stream_set_live_decode(s: *mut CrispasrStream, enabled: c_int);
446
447    // --- Mic capture (PLAN #62d) — miniaudio ma_device wrapper ---
448    pub fn crispasr_mic_open(
449        sample_rate: c_int,
450        channels: c_int,
451        cb: extern "C" fn(pcm: *const c_float, n_samples: c_int, userdata: *mut c_void),
452        userdata: *mut c_void,
453    ) -> *mut CrispasrMic;
454    pub fn crispasr_mic_start(m: *mut CrispasrMic) -> c_int;
455    pub fn crispasr_mic_stop(m: *mut CrispasrMic) -> c_int;
456    pub fn crispasr_mic_close(m: *mut CrispasrMic);
457    pub fn crispasr_mic_default_device_name() -> *const c_char;
458    pub fn crispasr_registry_lookup_by_filename_abi(
459        filename: *const c_char,
460        out_filename: *mut c_char,
461        filename_cap: c_int,
462        out_url: *mut c_char,
463        url_cap: c_int,
464        out_size: *mut c_char,
465        size_cap: c_int,
466    ) -> c_int;
467
468    pub fn crispasr_session_result_n_segments(r: *mut CrispasrSessionResult) -> c_int;
469    pub fn crispasr_session_result_segment_text(
470        r: *mut CrispasrSessionResult,
471        i: c_int,
472    ) -> *const c_char;
473    pub fn crispasr_session_result_segment_t0(r: *mut CrispasrSessionResult, i: c_int) -> i64;
474    pub fn crispasr_session_result_segment_t1(r: *mut CrispasrSessionResult, i: c_int) -> i64;
475
476    pub fn crispasr_session_result_n_words(r: *mut CrispasrSessionResult, i_seg: c_int) -> c_int;
477    pub fn crispasr_session_result_word_text(
478        r: *mut CrispasrSessionResult,
479        i_seg: c_int,
480        i_word: c_int,
481    ) -> *const c_char;
482    pub fn crispasr_session_result_word_t0(
483        r: *mut CrispasrSessionResult,
484        i_seg: c_int,
485        i_word: c_int,
486    ) -> i64;
487    pub fn crispasr_session_result_word_t1(
488        r: *mut CrispasrSessionResult,
489        i_seg: c_int,
490        i_word: c_int,
491    ) -> i64;
492    pub fn crispasr_session_result_word_p(
493        r: *mut CrispasrSessionResult,
494        i_seg: c_int,
495        i_word: c_int,
496    ) -> f32;
497    // Whisper's per-segment no-speech probability (the <|nospeech|> token
498    // posterior) in [0, 1]. Only the whisper backend populates it; other
499    // backends and out-of-range indices return the -1.0 sentinel ("no data").
500    pub fn crispasr_session_result_segment_no_speech_prob(
501        r: *mut CrispasrSessionResult,
502        i_seg: c_int,
503    ) -> f32;
504
505    // Raw per-frame CTC logits (Omni CTC backend, opted in via
506    // `crispasr_session_set_return_logits`). Frame-major, pre-softmax:
507    // `logits[t * n_logit_vocab + v]`; the pointer is NULL when none captured.
508    pub fn crispasr_session_result_n_logit_frames(r: *mut CrispasrSessionResult) -> c_int;
509    pub fn crispasr_session_result_n_logit_vocab(r: *mut CrispasrSessionResult) -> c_int;
510    pub fn crispasr_session_result_logits(r: *mut CrispasrSessionResult) -> *const c_float;
511
512    pub fn crispasr_session_result_free(r: *mut CrispasrSessionResult);
513    pub fn crispasr_session_close(s: *mut CrispasrSession);
514
515    // --- TTS synthesis (vibevoice, qwen3-tts, kokoro, orpheus) ---
516    pub fn crispasr_session_set_codec_path(s: *mut CrispasrSession, path: *const c_char) -> c_int;
517    pub fn crispasr_session_set_voice(
518        s: *mut CrispasrSession,
519        path: *const c_char,
520        ref_text_or_null: *const c_char,
521    ) -> c_int;
522    pub fn crispasr_session_set_speaker_name(s: *mut CrispasrSession, name: *const c_char)
523        -> c_int;
524    pub fn crispasr_session_n_speakers(s: *mut CrispasrSession) -> c_int;
525    pub fn crispasr_session_get_speaker_name(s: *mut CrispasrSession, i: c_int) -> *const c_char;
526    // qwen3-tts VoiceDesign: natural-language voice description.
527    pub fn crispasr_session_set_instruct(s: *mut CrispasrSession, instruct: *const c_char)
528        -> c_int;
529    // #316: synthesize these phonemes verbatim, skipping the G2P. Empty clears.
530    // -2 = the active backend has no phonemes-in call (kokoro and piper do).
531    pub fn crispasr_session_set_tts_phonemes(
532        s: *mut CrispasrSession,
533        phonemes: *const c_char,
534    ) -> c_int;
535    // qwen3-tts variant detection (returns 0/1; 0 also covers "not qwen3-tts").
536    pub fn crispasr_session_is_custom_voice(s: *mut CrispasrSession) -> c_int;
537    pub fn crispasr_session_is_voice_design(s: *mut CrispasrSession) -> c_int;
538    pub fn crispasr_session_synthesize(
539        s: *mut CrispasrSession,
540        text: *const c_char,
541        out_n_samples: *mut c_int,
542    ) -> *mut f32;
543    // Speech-to-Speech — audio in -> audio out via a single model pass. Supported
544    // on S2S-capable backends (lfm2-audio, mini-omni2, sidon, voxcpm2-vae). Returns
545    // malloc'd f32 PCM (free with `crispasr_pcm_free`); `out_text`, if non-null,
546    // receives the malloc'd intermediate transcript (free with
547    // `crispasr_session_translate_text_free`). Returns null on failure / unsupported.
548    pub fn crispasr_session_speech_to_speech(
549        s: *mut CrispasrSession,
550        in_samples: *const f32,
551        n_in_samples: c_int,
552        out_text: *mut *mut c_char,
553        out_n_samples: *mut c_int,
554    ) -> *mut f32;
555    // Source separation (#359). Stereo interleaved PCM in at the model's own
556    // rate (44100 Hz for htdemucs / mel-band-roformer); returns the stem count.
557    // Stem buffers are owned by the session and valid only until the next
558    // separate() call or close, so a safe wrapper must copy them out.
559    pub fn crispasr_session_separate(
560        s: *mut CrispasrSession,
561        pcm_stereo: *const f32,
562        n_samples: c_int,
563    ) -> c_int;
564    pub fn crispasr_session_separate_n_stems(s: *mut CrispasrSession) -> c_int;
565    pub fn crispasr_session_separate_stem_name(
566        s: *mut CrispasrSession,
567        stem_idx: c_int,
568    ) -> *const c_char;
569    pub fn crispasr_session_separate_stem(
570        s: *mut CrispasrSession,
571        stem_idx: c_int,
572        out_n_samples: *mut c_int,
573    ) -> *const f32;
574    pub fn crispasr_session_separate_sample_rate(s: *mut CrispasrSession) -> c_int;
575    // UNMARKED synthesis (no watermark/disclosure). Hard-refused unless
576    // `crispasr_session_accept_marking_responsibility` was called first. Returns
577    // malloc'd f32 PCM (free with `crispasr_pcm_free`); null on refusal/failure.
578    pub fn crispasr_session_synthesize_raw(
579        s: *mut CrispasrSession,
580        text: *const c_char,
581        out_n_samples: *mut c_int,
582    ) -> *mut f32;
583    // Attest that the integrator accepts AI-content marking/disclosure
584    // responsibility (EU AI Act Art. 50). REQUIRED before `synthesize_raw`.
585    pub fn crispasr_session_accept_marking_responsibility(
586        s: *mut CrispasrSession,
587        attestation: *const c_char,
588    ) -> c_int;
589    // Declare whose voice a PRESET voice is: "real_person" | "synthetic" |
590    // "unknown". A preset can be an identifiable individual, which makes its
591    // output a deep fake under Art. 3(60) without any cloning. Returns 0, -1 on
592    // a bad session, -2 on an unrecognised value.
593    pub fn crispasr_session_set_speaker_identity(
594        s: *mut CrispasrSession,
595        identity: *const c_char,
596    ) -> c_int;
597    // Sample rate the backend expects for input PCM (16000 for Whisper-family,
598    // the model's native rate otherwise; 0 on error). Pair with s2s/synthesize to
599    // feed input at the right rate.
600    pub fn crispasr_session_input_sample_rate(s: *mut CrispasrSession) -> c_int;
601    // #332: output-side counterparts. output_sample_rate is the rate of the
602    // PCM synthesize / speech_to_speech return (0 = backend has no audio
603    // output); the channel getters are 1 (mono) for every current backend.
604    pub fn crispasr_session_output_sample_rate(s: *mut CrispasrSession) -> c_int;
605    pub fn crispasr_session_input_channels(s: *mut CrispasrSession) -> c_int;
606    pub fn crispasr_session_output_channels(s: *mut CrispasrSession) -> c_int;
607    pub fn crispasr_pcm_free(pcm: *mut f32);
608    // Embed the AI-content watermark into f32 mono PCM, in place. The other
609    // half of `synthesize_raw`: opting out of automatic marking obliges the
610    // caller to mark the result, and this is what they mark it with.
611    // `alpha <= 0` selects the robust, reliably detectable default.
612    pub fn crispasr_watermark_embed(pcm: *mut f32, n_samples: c_int, alpha: c_float);
613    // Confidence in [0, 1] that `pcm` carries the watermark. A weak diagnostic:
614    // the spread-spectrum null mean is 0.5, not 0 (see docs/eu-ai-act.md §6.7).
615    pub fn crispasr_watermark_detect(pcm: *const c_float, n_samples: c_int) -> c_float;
616    // Drop the kokoro per-session phoneme cache. No-op for non-kokoro
617    // backends. Returns 0 on success, -1 if `s` is null. (PLAN #56 #5)
618    pub fn crispasr_session_kokoro_clear_phoneme_cache(s: *mut CrispasrSession) -> c_int;
619
620    // --- Sticky session-state setters (PLAN #59 partial unblock) ---
621    pub fn crispasr_session_set_source_language(
622        s: *mut CrispasrSession,
623        lang: *const c_char,
624    ) -> c_int;
625    pub fn crispasr_session_set_target_language(
626        s: *mut CrispasrSession,
627        lang: *const c_char,
628    ) -> c_int;
629    pub fn crispasr_session_set_tts_reference_language(
630        s: *mut CrispasrSession,
631        lang: *const c_char,
632    ) -> c_int;
633    pub fn crispasr_session_set_punctuation(s: *mut CrispasrSession, enable: c_int) -> c_int;
634    pub fn crispasr_session_set_punc_model(
635        s: *mut CrispasrSession,
636        punc_model: *const c_char,
637    ) -> c_int;
638    pub fn crispasr_session_set_hotwords(
639        s: *mut CrispasrSession,
640        hotwords: *const c_char,
641        boost: c_float,
642    ) -> c_int;
643    pub fn crispasr_session_set_sensitivity(
644        s: *mut CrispasrSession,
645        preset: *const c_char,
646    ) -> c_int;
647    pub fn crispasr_session_set_g2p_dict(s: *mut CrispasrSession, source: *const c_char) -> c_int;
648    pub fn crispasr_session_set_speaker_id(s: *mut CrispasrSession, id: c_int) -> c_int;
649    pub fn crispasr_session_set_translate(s: *mut CrispasrSession, enable: c_int) -> c_int;
650    // --- Text-to-text translation (m2m100 / m2m100-wmt21 / madlad / gemma4-e2b) ---
651    //
652    // Distinct from `crispasr_session_set_translate` above, which is the
653    // *audio-side* Whisper sticky flag (PCM input → English text out).
654    // This one translates an already-extracted Rust string between
655    // arbitrary language pairs via whichever MT-capable backend the
656    // session loaded.  Returns a malloc'd UTF-8 buffer that the caller
657    // MUST release via `crispasr_session_translate_text_free` (mirrors
658    // the punc-side ownership pattern).  Returns nullptr on:
659    //   * any input pointer being null,
660    //   * the session not having a CAP_TRANSLATE backend loaded,
661    //   * the backend's internal translate routine erroring out.
662    //
663    // `max_tokens` caps the decoder output length.  Pass `<= 0` to
664    // fall back to the C++ default (200 for m2m100).
665    pub fn crispasr_session_translate_text(
666        s: *mut CrispasrSession,
667        text: *const c_char,
668        src_lang: *const c_char,
669        tgt_lang: *const c_char,
670        max_tokens: c_int,
671    ) -> *mut c_char;
672    // Free a buffer previously returned by `crispasr_session_translate_text`.
673    // No-op when `text` is null.  Calling `libc::free` directly also works
674    // (the C++ side just delegates to `free()`), but routing through this
675    // symbol keeps ownership symmetric and protects callers if the C++
676    // side ever switches allocators.
677    pub fn crispasr_session_translate_text_free(text: *mut c_char);
678    pub fn crispasr_session_set_temperature(
679        s: *mut CrispasrSession,
680        temperature: c_float,
681        seed: u64,
682    ) -> c_int;
683    pub fn crispasr_session_set_tts_seed(s: *mut CrispasrSession, seed: u64) -> c_int;
684    pub fn crispasr_session_set_max_new_tokens(
685        s: *mut CrispasrSession,
686        max_new_tokens: c_int,
687    ) -> c_int;
688    pub fn crispasr_session_set_frequency_penalty(
689        s: *mut CrispasrSession,
690        penalty: c_float,
691    ) -> c_int;
692    pub fn crispasr_session_set_tts_steps(s: *mut CrispasrSession, steps: c_int) -> c_int;
693    pub fn crispasr_session_set_tts_num_candidates(s: *mut CrispasrSession, n: c_int) -> c_int;
694    pub fn crispasr_session_set_top_p(s: *mut CrispasrSession, top_p: c_float) -> c_int;
695    pub fn crispasr_session_set_top_k(s: *mut CrispasrSession, top_k: c_int) -> c_int;
696    pub fn crispasr_session_set_do_sample(s: *mut CrispasrSession, enable: c_int) -> c_int;
697    pub fn crispasr_session_set_min_p(s: *mut CrispasrSession, min_p: c_float) -> c_int;
698    pub fn crispasr_session_set_repetition_penalty(s: *mut CrispasrSession, r: c_float) -> c_int;
699    pub fn crispasr_session_set_cfg_weight(s: *mut CrispasrSession, cfg_weight: c_float) -> c_int;
700    pub fn crispasr_session_set_tts_noise_temp(
701        s: *mut CrispasrSession,
702        noise_temp: c_float,
703    ) -> c_int;
704    pub fn crispasr_session_set_exaggeration(
705        s: *mut CrispasrSession,
706        exaggeration: c_float,
707    ) -> c_int;
708    pub fn crispasr_session_set_max_speech_tokens(s: *mut CrispasrSession, n: c_int) -> c_int;
709    pub fn crispasr_session_set_min_speech_tokens(s: *mut CrispasrSession, n: c_int) -> c_int;
710    pub fn crispasr_session_set_length_scale(s: *mut CrispasrSession, scale: c_float) -> c_int;
711    pub fn crispasr_session_set_best_of(s: *mut CrispasrSession, n: c_int) -> c_int;
712    pub fn crispasr_session_set_beam_size(s: *mut CrispasrSession, n: c_int) -> c_int;
713    pub fn crispasr_session_set_return_logits(s: *mut CrispasrSession, enable: c_int) -> c_int;
714    pub fn crispasr_session_set_grammar_text(
715        s: *mut CrispasrSession,
716        gbnf_text: *const c_char,
717        root_rule: *const c_char,
718        penalty: c_float,
719    ) -> c_int;
720    pub fn crispasr_session_set_fallback_thresholds(
721        s: *mut CrispasrSession,
722        entropy_thold: c_float,
723        logprob_thold: c_float,
724        no_speech_thold: c_float,
725        temperature_inc: c_float,
726    ) -> c_int;
727    pub fn crispasr_session_set_alt_n(s: *mut CrispasrSession, n: c_int) -> c_int;
728    pub fn crispasr_session_set_whisper_decode_extras(
729        s: *mut CrispasrSession,
730        suppress_nst: c_int,
731        suppress_regex: *const c_char,
732        carry_initial_prompt: c_int,
733    ) -> c_int;
734    pub fn crispasr_session_set_ask(s: *mut CrispasrSession, prompt: *const c_char) -> c_int;
735    pub fn crispasr_session_detect_language(
736        s: *mut CrispasrSession,
737        pcm: *const c_float,
738        n_samples: c_int,
739        lid_model_path: *const c_char,
740        method: c_int,
741        out_lang: *mut c_char,
742        out_lang_cap: c_int,
743        out_prob: *mut c_float,
744    ) -> c_int;
745
746    // --- Text-LID (P13.5 Phase 7) ---
747    //
748    // Detect the language of a UTF-8 text string via the internal
749    // `text_lid_dispatch` façade — routes to CLD3 (ISO 639-1, 109
750    // labels) or GlotLID-V3 / LID-176 fastText (ISO 639-3 + script,
751    // 2102 or 176 labels) based on the GGUF's architecture key.
752    // Label format follows whichever backend the GGUF loads as —
753    // see the C-API doc-comment for normalisation guidance.
754    //
755    // Returns:
756    //   *  0 — success; `out_label_buf` + `out_confidence` populated.
757    //   * -1 — invalid args (null pointer or out_label_cap <= 0).
758    //   *  1 — dispatcher init / predict failure.
759    //   *  2 — output buffer too small for the predicted label.
760    pub fn crispasr_text_detect_language(
761        text: *const c_char,
762        model_path: *const c_char,
763        n_threads: c_int,
764        out_label_buf: *mut c_char,
765        out_label_cap: c_int,
766        out_confidence: *mut c_float,
767    ) -> c_int;
768
769    pub fn crispasr_detect_backend_from_gguf(
770        path: *const c_char,
771        out_name: *mut c_char,
772        out_cap: c_int,
773    ) -> c_int;
774
775    // --- FireRedPunc punctuation restoration ---
776    pub fn crispasr_punc_init(model_path: *const c_char) -> *mut c_void;
777    pub fn crispasr_punc_process(ctx: *mut c_void, text: *const c_char) -> *mut c_char;
778    pub fn crispasr_punc_free_text(text: *mut c_char);
779    pub fn crispasr_punc_free(ctx: *mut c_void);
780
781    pub fn crispasr_c_api_version() -> *const c_char;
782
783    // --- Kokoro per-language model + voice routing (PLAN #56 opt 2b) ---
784    // See `src/kokoro.h` for full semantics.
785    pub fn crispasr_kokoro_lang_is_german_abi(lang: *const c_char) -> bool;
786    pub fn crispasr_kokoro_lang_has_native_voice_abi(lang: *const c_char) -> bool;
787    pub fn crispasr_kokoro_resolve_model_for_lang_abi(
788        model_path: *const c_char,
789        lang: *const c_char,
790        out_path: *mut c_char,
791        out_path_len: c_int,
792    ) -> c_int;
793    pub fn crispasr_kokoro_resolve_fallback_voice_abi(
794        model_path: *const c_char,
795        lang: *const c_char,
796        out_path: *mut c_char,
797        out_path_len: c_int,
798        out_picked: *mut c_char,
799        out_picked_len: c_int,
800    ) -> c_int;
801
802    // TitaNet speaker verification
803    pub fn crispasr_titanet_init(model_path: *const c_char, n_threads: i32) -> *mut c_void;
804    pub fn crispasr_titanet_free(ctx: *mut c_void);
805    pub fn crispasr_titanet_embed(
806        ctx: *mut c_void,
807        pcm_16k: *const c_float,
808        n_samples: i32,
809        out: *mut c_float,
810    ) -> i32;
811    pub fn crispasr_titanet_cosine_sim(a: *const c_float, b: *const c_float, dim: i32) -> c_float;
812
813    // Speaker profile database
814    pub fn crispasr_speaker_db_load(dir_path: *const c_char) -> *mut c_void;
815    pub fn crispasr_speaker_db_free(db: *mut c_void);
816    pub fn crispasr_speaker_db_count(db: *const c_void) -> i32;
817    pub fn crispasr_speaker_db_match(
818        db: *const c_void,
819        embedding: *const c_float,
820        dim: i32,
821        threshold: c_float,
822        out_name: *mut c_char,
823        out_cap: i32,
824    ) -> c_float;
825    pub fn crispasr_speaker_db_enroll(
826        dir_path: *const c_char,
827        name: *const c_char,
828        embedding: *const c_float,
829        dim: i32,
830    ) -> i32;
831
832    // Pluggable speaker embedder + agglomerative clustering + pyannote
833    // cache (issue #107 P6). Same building blocks as the CLI's
834    // --diarize-embedder path; expose them so Rust callers can compose
835    // the diarize pipeline without round-tripping through the CLI.
836
837    /// Build a pluggable speaker embedder. `model_spec` is one of
838    /// `"auto"`, `"titanet"`, `"indextts"`, `"indextts-bigvgan"`,
839    /// `"ecapa"`, or a `.gguf` path. Returns null on failure.
840    pub fn crispasr_speaker_embedder_make_abi(
841        model_spec: *const c_char,
842        n_threads: i32,
843        cache_dir: *const c_char,
844    ) -> *mut c_void;
845
846    pub fn crispasr_speaker_embedder_free_abi(embedder: *mut c_void);
847
848    /// Output embedding dimension (e.g. 192 for TitaNet, 512 for
849    /// IndexTTS-BigVGAN).
850    pub fn crispasr_speaker_embedder_dim_abi(embedder: *const c_void) -> i32;
851
852    /// Extract one embedding. `out` must hold at least `dim()` floats.
853    /// Returns 1 on success, 0 if the model rejected the input.
854    pub fn crispasr_speaker_embedder_embed_abi(
855        embedder: *mut c_void,
856        pcm_16k: *const c_float,
857        n_samples: i32,
858        out: *mut c_float,
859    ) -> i32;
860
861    pub fn crispasr_speaker_embedder_name_abi(embedder: *const c_void) -> *const c_char;
862
863    /// Agglomerative single-linkage cosine clustering. `embeddings` is
864    /// a row-major `n × dim` buffer of (ideally L2-normalized) vectors.
865    /// `labels_out` receives one cluster ID per input in `[0, k)`.
866    /// Returns the cluster count `k`, or -1 on invalid arguments.
867    pub fn crispasr_speaker_cluster_abi(
868        embeddings: *const c_float,
869        n: i32,
870        dim: i32,
871        merge_threshold: c_float,
872        max_speakers: i32,
873        labels_out: *mut i32,
874    ) -> i32;
875
876    /// Pre-compute pyannote-seg posteriors over a full audio buffer.
877    /// Returns an opaque cache or null on failure. Free with
878    /// `crispasr_pyannote_cache_free_abi`.
879    pub fn crispasr_pyannote_cache_compute_abi(
880        full_audio: *const c_float,
881        n_samples: i32,
882        model_path: *const c_char,
883        n_threads: i32,
884    ) -> *mut c_void;
885
886    pub fn crispasr_pyannote_cache_free_abi(cache: *mut c_void);
887
888    /// Score `segs` against the cached posteriors. `slice_t0_cs` is the
889    /// absolute centisecond at which the cache buffer starts (typically
890    /// 0 — the cache covers the whole input audio).
891    pub fn crispasr_pyannote_cache_apply_abi(
892        cache: *const c_void,
893        slice_t0_cs: i64,
894        segs: *mut CrispasrDiarizeSegAbi,
895        n_segs: i32,
896    ) -> i32;
897
898    // --- params_set_* on whisper_full_params (full C-ABI parity) ---
899    pub fn crispasr_params_set_language(p: *mut WhisperFullParams, lang: *const c_char);
900    pub fn crispasr_params_set_translate(p: *mut WhisperFullParams, v: c_int);
901    pub fn crispasr_params_set_detect_language(p: *mut WhisperFullParams, v: c_int);
902    pub fn crispasr_params_set_token_timestamps(p: *mut WhisperFullParams, v: c_int);
903    pub fn crispasr_params_set_n_threads(p: *mut WhisperFullParams, n: c_int);
904    pub fn crispasr_params_set_max_len(p: *mut WhisperFullParams, n: c_int);
905    pub fn crispasr_params_set_best_of(p: *mut WhisperFullParams, n: c_int);
906    pub fn crispasr_params_set_split_on_word(p: *mut WhisperFullParams, v: c_int);
907    pub fn crispasr_params_set_no_context(p: *mut WhisperFullParams, v: c_int);
908    pub fn crispasr_params_set_single_segment(p: *mut WhisperFullParams, v: c_int);
909    pub fn crispasr_params_set_print_realtime(p: *mut WhisperFullParams, v: c_int);
910    pub fn crispasr_params_set_print_progress(p: *mut WhisperFullParams, v: c_int);
911    pub fn crispasr_params_set_print_timestamps(p: *mut WhisperFullParams, v: c_int);
912    pub fn crispasr_params_set_print_special(p: *mut WhisperFullParams, v: c_int);
913    pub fn crispasr_params_set_suppress_blank(p: *mut WhisperFullParams, v: c_int);
914    pub fn crispasr_params_set_temperature(p: *mut WhisperFullParams, t: c_float);
915    pub fn crispasr_params_set_max_tokens(p: *mut WhisperFullParams, n: c_int);
916    pub fn crispasr_params_set_initial_prompt(p: *mut WhisperFullParams, prompt: *const c_char);
917    pub fn crispasr_params_set_alt_n(p: *mut WhisperFullParams, n: c_int);
918
919    // --- Token-level accessors ---
920    pub fn crispasr_token_t0(ctx: *mut WhisperContext, i_seg: c_int, i_tok: c_int) -> i64;
921    pub fn crispasr_token_t1(ctx: *mut WhisperContext, i_seg: c_int, i_tok: c_int) -> i64;
922    pub fn crispasr_token_p(ctx: *mut WhisperContext, i_seg: c_int, i_tok: c_int) -> c_float;
923    pub fn crispasr_token_n_alts(ctx: *mut WhisperContext, i_seg: c_int, i_tok: c_int) -> c_int;
924    pub fn crispasr_token_alt_id(
925        ctx: *mut WhisperContext,
926        i_seg: c_int,
927        i_tok: c_int,
928        i_alt: c_int,
929    ) -> i32;
930    pub fn crispasr_token_alt_p(
931        ctx: *mut WhisperContext,
932        i_seg: c_int,
933        i_tok: c_int,
934        i_alt: c_int,
935    ) -> c_float;
936    pub fn crispasr_token_alt_text(
937        ctx: *mut WhisperContext,
938        i_seg: c_int,
939        i_tok: c_int,
940        i_alt: c_int,
941        out: *mut c_char,
942        out_cap: c_int,
943    ) -> c_int;
944
945    // --- Language detection (whisper context) ---
946    pub fn crispasr_detect_language(
947        ctx: *mut WhisperContext,
948        pcm: *const c_float,
949        n_samples: c_int,
950        n_threads: c_int,
951        out_code: *mut c_char,
952        out_cap: c_int,
953    ) -> c_float;
954
955    // --- VAD ---
956    pub fn crispasr_vad_segments(
957        vad_model_path: *const c_char,
958        pcm: *const c_float,
959        n_samples: c_int,
960        sample_rate: c_int,
961        threshold: c_float,
962        min_speech_ms: c_int,
963        min_silence_ms: c_int,
964        n_threads: c_int,
965        use_gpu: c_int,
966        out_spans: *mut *mut c_float,
967    ) -> c_int;
968    pub fn crispasr_vad_slices(
969        vad_model_path: *const c_char,
970        pcm: *const c_float,
971        n_samples: c_int,
972        sample_rate: c_int,
973        threshold: c_float,
974        min_speech_ms: c_int,
975        min_silence_ms: c_int,
976        speech_pad_ms: c_int,
977        max_chunk_duration_s: c_float,
978        n_threads: c_int,
979        out_spans: *mut *mut c_float,
980    ) -> c_int;
981    pub fn crispasr_vad_free(spans: *mut c_float);
982
983    // --- LCS dedup ---
984    pub fn crispasr_lcs_dedup_prefix_count(
985        prev_tail_tokens: *const i32,
986        n_prev: c_int,
987        curr_tokens: *const i32,
988        n_curr: c_int,
989        min_lcs_length: c_int,
990    ) -> c_int;
991
992    // --- Streaming (whisper context) ---
993    pub fn crispasr_stream_open(
994        ctx: *mut WhisperContext,
995        n_threads: c_int,
996        step_ms: c_int,
997        length_ms: c_int,
998        keep_ms: c_int,
999        language: *const c_char,
1000        translate: c_int,
1001    ) -> *mut CrispasrStream;
1002
1003    // --- Direct Parakeet API ---
1004    pub fn crispasr_parakeet_init(
1005        model_path: *const c_char,
1006        n_threads: c_int,
1007        use_flash: c_int,
1008    ) -> *mut c_void;
1009    pub fn crispasr_parakeet_free(ctx: *mut c_void);
1010    pub fn crispasr_parakeet_transcribe(
1011        ctx: *mut c_void,
1012        pcm: *const c_float,
1013        n_samples: c_int,
1014        language: *const c_char,
1015    ) -> *mut c_void;
1016    pub fn crispasr_parakeet_result_text(r: *mut c_void) -> *const c_char;
1017    pub fn crispasr_parakeet_result_n_words(r: *mut c_void) -> c_int;
1018    pub fn crispasr_parakeet_result_word_text(r: *mut c_void, i: c_int) -> *const c_char;
1019    pub fn crispasr_parakeet_result_word_t0(r: *mut c_void, i: c_int) -> i64;
1020    pub fn crispasr_parakeet_result_word_t1(r: *mut c_void, i: c_int) -> i64;
1021    pub fn crispasr_parakeet_result_n_tokens(r: *mut c_void) -> c_int;
1022    pub fn crispasr_parakeet_result_token_text(r: *mut c_void, i: c_int) -> *const c_char;
1023    pub fn crispasr_parakeet_result_token_t0(r: *mut c_void, i: c_int) -> i64;
1024    pub fn crispasr_parakeet_result_token_t1(r: *mut c_void, i: c_int) -> i64;
1025    pub fn crispasr_parakeet_result_token_p(r: *mut c_void, i: c_int) -> c_float;
1026    pub fn crispasr_parakeet_result_free(r: *mut c_void);
1027
1028    // --- RNNoise audio enhancement ---
1029    pub fn crispasr_enhance_audio_rnnoise(
1030        in_pcm: *const c_float,
1031        n_samples: i32,
1032        out_pcm: *mut c_float,
1033        out_cap: i32,
1034    ) -> c_int;
1035
1036    // --- Session open with params ---
1037    pub fn crispasr_session_open_with_params(
1038        model_path: *const c_char,
1039        backend_name: *const c_char,
1040        params: *const c_void,
1041    ) -> *mut CrispasrSession;
1042
1043    // --- Session result word alts ---
1044    pub fn crispasr_session_result_word_n_alts(
1045        r: *mut CrispasrSessionResult,
1046        i_seg: c_int,
1047        i_word: c_int,
1048    ) -> c_int;
1049    pub fn crispasr_session_result_word_alt_text(
1050        r: *mut CrispasrSessionResult,
1051        i_seg: c_int,
1052        i_word: c_int,
1053        i_alt: c_int,
1054    ) -> *const c_char;
1055    pub fn crispasr_session_result_word_alt_p(
1056        r: *mut CrispasrSessionResult,
1057        i_seg: c_int,
1058        i_word: c_int,
1059        i_alt: c_int,
1060    ) -> c_float;
1061}
1062
1063// =========================================================================
1064// Chat / LLM FFI — mirrors include/crispasr_chat.h
1065// =========================================================================
1066//
1067// Text in, text out over the private `crispasr-llama-core` (vendored
1068// llama.cpp) built unconditionally into libcrispasr, so these symbols are
1069// always present — no build flag gates them. Only POD structs and one
1070// opaque handle cross the boundary.
1071
1072/// Opaque handle returned by `crispasr_chat_open`. Free with
1073/// `crispasr_chat_close`.
1074#[repr(C)]
1075pub struct CrispasrChatSession(c_void);
1076
1077/// The one error code on the chat ABI with a stable, documented meaning:
1078/// a registered abort callback stopped the run. Every other non-zero
1079/// `CrispasrChatError::code` is a diagnostic aid — read `message`.
1080pub const CRISPASR_CHAT_ERR_ABORTED: i32 = 40;
1081
1082/// Out-parameter for every chat entry point that can fail (may be null).
1083/// Left untouched on success; on failure `code` is non-zero and `message`
1084/// holds a NUL-terminated diagnostic.
1085#[repr(C)]
1086#[derive(Clone, Copy, Debug)]
1087pub struct CrispasrChatError {
1088    pub code: i32,
1089    pub message: [c_char; 256],
1090}
1091
1092impl Default for CrispasrChatError {
1093    fn default() -> Self {
1094        Self {
1095            code: 0,
1096            message: [0; 256],
1097        }
1098    }
1099}
1100
1101/// One turn of a conversation. `role` is "system", "user", "assistant" or
1102/// "tool"; both pointers must stay valid for the duration of the call.
1103#[repr(C)]
1104#[derive(Clone, Copy, Debug)]
1105pub struct CrispasrChatMessage {
1106    pub role: *const c_char,
1107    pub content: *const c_char,
1108}
1109
1110/// Per-session, model-level open params. Fill via
1111/// [`crispasr_chat_open_params_default`] before overriding fields — the C
1112/// side reads every one of them.
1113#[repr(C)]
1114#[derive(Clone, Copy, Debug)]
1115pub struct CrispasrChatOpenParams {
1116    pub n_threads: c_int,
1117    pub n_threads_batch: c_int,
1118    /// Context window in tokens; 0 = the model's own default.
1119    pub n_ctx: c_int,
1120    pub n_batch: c_int,
1121    pub n_ubatch: c_int,
1122    /// -1 = offload all layers, 0 = CPU only.
1123    pub n_gpu_layers: c_int,
1124    pub use_mmap: bool,
1125    pub use_mlock: bool,
1126    pub embeddings: bool,
1127    /// Overrides the template baked into the GGUF; null reads
1128    /// `tokenizer.chat_template` from the model. Copied by the callee.
1129    pub chat_template: *const c_char,
1130}
1131
1132impl Default for CrispasrChatOpenParams {
1133    fn default() -> Self {
1134        Self {
1135            n_threads: 0,
1136            n_threads_batch: 0,
1137            n_ctx: 0,
1138            n_batch: 0,
1139            n_ubatch: 0,
1140            n_gpu_layers: 0,
1141            use_mmap: false,
1142            use_mlock: false,
1143            embeddings: false,
1144            chat_template: std::ptr::null(),
1145        }
1146    }
1147}
1148
1149/// Per-call, sampler-level generate params. Fill via
1150/// [`crispasr_chat_generate_params_default`] before overriding fields.
1151#[repr(C)]
1152#[derive(Clone, Copy, Debug)]
1153pub struct CrispasrChatGenerateParams {
1154    pub max_tokens: c_int,
1155    /// 0.0 = greedy (short-circuits the rest of the sampler chain).
1156    pub temperature: c_float,
1157    pub top_k: c_int,
1158    pub top_p: c_float,
1159    pub min_p: c_float,
1160    pub repeat_penalty: c_float,
1161    pub repeat_last_n: c_int,
1162    /// 0xFFFFFFFF = random.
1163    pub seed: u32,
1164    /// Array of `n_stop` NUL-terminated stop strings; null = none.
1165    pub stop: *const *const c_char,
1166    pub n_stop: usize,
1167    /// Prefill the prompt but suppress assistant generation.
1168    pub prefill_only: bool,
1169}
1170
1171impl Default for CrispasrChatGenerateParams {
1172    fn default() -> Self {
1173        Self {
1174            max_tokens: 0,
1175            temperature: 0.0,
1176            top_k: 0,
1177            top_p: 0.0,
1178            min_p: 0.0,
1179            repeat_penalty: 0.0,
1180            repeat_last_n: 0,
1181            seed: 0,
1182            stop: std::ptr::null(),
1183            n_stop: 0,
1184            prefill_only: false,
1185        }
1186    }
1187}
1188
1189/// Fired once per detokenised UTF-8 chunk during a streaming generate. The
1190/// chunk pointer is valid only for the duration of the call.
1191/// `Option<...>` so a null pointer clears the callback (C `NULL`).
1192pub type CrispasrChatOnToken =
1193    Option<unsafe extern "C" fn(utf8_chunk: *const c_char, user: *mut c_void)>;
1194
1195/// Abort hook. Returns **true to continue**, false to abort — the
1196/// `whisper_encoder_begin_callback` convention on the ASR surface, and the
1197/// opposite of ggml's own. Called on the generating thread before each
1198/// prompt batch and each sampled token, and (CPU backend only) from inside
1199/// a running compute graph, so it must be cheap and non-blocking. It must
1200/// not re-enter the session that registered it — the session mutex is held.
1201pub type CrispasrChatAbortCallback = Option<unsafe extern "C" fn(user: *mut c_void) -> bool>;
1202
1203extern "C" {
1204    // --- Params ---
1205    pub fn crispasr_chat_open_params_default(out: *mut CrispasrChatOpenParams);
1206    pub fn crispasr_chat_generate_params_default(out: *mut CrispasrChatGenerateParams);
1207
1208    // --- Session lifecycle ---
1209    pub fn crispasr_chat_open(
1210        model_path: *const c_char,
1211        params: *const CrispasrChatOpenParams,
1212        err: *mut CrispasrChatError,
1213    ) -> *mut CrispasrChatSession;
1214    pub fn crispasr_chat_close(s: *mut CrispasrChatSession);
1215    pub fn crispasr_chat_reset(s: *mut CrispasrChatSession, err: *mut CrispasrChatError) -> i32;
1216
1217    // --- Generation ---
1218    /// Returns a malloc'd UTF-8 string (free with
1219    /// [`crispasr_chat_string_free`]) or null on failure / abort.
1220    pub fn crispasr_chat_generate(
1221        s: *mut CrispasrChatSession,
1222        messages: *const CrispasrChatMessage,
1223        n_messages: usize,
1224        params: *const CrispasrChatGenerateParams,
1225        err: *mut CrispasrChatError,
1226    ) -> *mut c_char;
1227
1228    /// 0 on clean completion (including stop-sequence / EOG termination),
1229    /// [`CRISPASR_CHAT_ERR_ABORTED`] when the abort callback stopped it,
1230    /// other non-zero on failure.
1231    pub fn crispasr_chat_generate_stream(
1232        s: *mut CrispasrChatSession,
1233        messages: *const CrispasrChatMessage,
1234        n_messages: usize,
1235        params: *const CrispasrChatGenerateParams,
1236        on_token: CrispasrChatOnToken,
1237        user: *mut c_void,
1238        err: *mut CrispasrChatError,
1239    ) -> i32;
1240
1241    /// Register `cb` on the session (null clears it). Takes the session
1242    /// lock, so calling it during a generation blocks rather than
1243    /// cancelling — register before starting one.
1244    pub fn crispasr_chat_set_abort_callback(
1245        s: *mut CrispasrChatSession,
1246        cb: CrispasrChatAbortCallback,
1247        user: *mut c_void,
1248    );
1249
1250    // --- Introspection ---
1251    pub fn crispasr_chat_template_name(s: *mut CrispasrChatSession) -> *const c_char;
1252    pub fn crispasr_chat_n_ctx(s: *mut CrispasrChatSession) -> i32;
1253
1254    /// Prompt tokens a FRESH session prefills for `messages` — chat
1255    /// template, BOS, and the trailing generation prompt included.
1256    /// Negative on failure, with `err` filled.
1257    pub fn crispasr_chat_count_tokens(
1258        s: *mut CrispasrChatSession,
1259        messages: *const CrispasrChatMessage,
1260        n_messages: usize,
1261        err: *mut CrispasrChatError,
1262    ) -> i32;
1263
1264    /// Approximate working set in bytes for a GGUF chat model on disk, or
1265    /// 0 when it could not be estimated (`err` filled).
1266    pub fn crispasr_chat_memory_estimate(
1267        model_path: *const c_char,
1268        params: *const CrispasrChatOpenParams,
1269        err: *mut CrispasrChatError,
1270    ) -> usize;
1271
1272    pub fn crispasr_chat_string_free(s: *mut c_char);
1273
1274    /// Canonical EU AI Act Art. 50(1) "you are talking to an AI" wording.
1275    /// Static string — never null, never freed.
1276    pub fn crispasr_chat_ai_disclosure_text() -> *const c_char;
1277}
1278
1279#[cfg(test)]
1280mod tests {
1281    use super::*;
1282
1283    // Guards the hand-maintained mirrors of the APPEND-ONLY structs in
1284    // src/crispasr_c_api.cpp. The C side reads every field unconditionally,
1285    // so a short layout here is an out-of-bounds read even for methods
1286    // that ignore the trailing fields (#332).
1287    #[test]
1288    fn diarize_abi_layout() {
1289        use std::mem::{offset_of, size_of};
1290        assert_eq!(size_of::<CrispasrDiarizeSegAbi>(), 24);
1291        assert_eq!(size_of::<CrispasrDiarizeOptsAbi>(), 48);
1292        assert_eq!(offset_of!(CrispasrDiarizeOptsAbi, slice_t0_cs), 8);
1293        assert_eq!(offset_of!(CrispasrDiarizeOptsAbi, pyannote_model_path), 16);
1294        assert_eq!(
1295            offset_of!(CrispasrDiarizeOptsAbi, foxnose_embedder_path),
1296            24
1297        );
1298        assert_eq!(offset_of!(CrispasrDiarizeOptsAbi, min_speakers), 32);
1299        assert_eq!(offset_of!(CrispasrDiarizeOptsAbi, num_speakers), 40);
1300    }
1301
1302    // Same guard for the hand-maintained mirrors of the POD structs in
1303    // include/crispasr_chat.h. Values are what the C compiler reports for
1304    // that header on a 64-bit target; the trailing `bool` in each params
1305    // struct is what makes the tail padding easy to get wrong.
1306    #[test]
1307    fn chat_abi_layout() {
1308        use std::mem::{offset_of, size_of};
1309
1310        assert_eq!(size_of::<CrispasrChatError>(), 260);
1311        assert_eq!(offset_of!(CrispasrChatError, message), 4);
1312
1313        assert_eq!(size_of::<CrispasrChatMessage>(), 16);
1314        assert_eq!(offset_of!(CrispasrChatMessage, content), 8);
1315
1316        assert_eq!(size_of::<CrispasrChatOpenParams>(), 40);
1317        assert_eq!(offset_of!(CrispasrChatOpenParams, n_gpu_layers), 20);
1318        assert_eq!(offset_of!(CrispasrChatOpenParams, use_mmap), 24);
1319        assert_eq!(offset_of!(CrispasrChatOpenParams, use_mlock), 25);
1320        assert_eq!(offset_of!(CrispasrChatOpenParams, embeddings), 26);
1321        assert_eq!(offset_of!(CrispasrChatOpenParams, chat_template), 32);
1322
1323        assert_eq!(size_of::<CrispasrChatGenerateParams>(), 56);
1324        assert_eq!(offset_of!(CrispasrChatGenerateParams, seed), 28);
1325        assert_eq!(offset_of!(CrispasrChatGenerateParams, stop), 32);
1326        assert_eq!(offset_of!(CrispasrChatGenerateParams, n_stop), 40);
1327        assert_eq!(offset_of!(CrispasrChatGenerateParams, prefill_only), 48);
1328    }
1329}