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