Skip to main content

transcribe_cpp/
diarize.rs

1//! [`DiarizeSession`] — the DIARIZE role (who spoke when), from [`Model::diarize_session`].
2//! Threading, lifetime, and compute-lock rules are those of [`Session`](crate::Session).
3
4use std::os::raw::c_void;
5use std::sync::atomic::AtomicBool;
6use std::sync::Arc;
7
8use transcribe_cpp_sys as sys;
9
10use crate::cancel::{abort_trampoline, CancelToken};
11use crate::error::{check, Result};
12use crate::family::DiarizeExtension;
13use crate::model::{Model, ModelInner};
14use crate::result::{SpeakerSegment, Timings};
15use crate::session::clamp_len;
16
17/// Static facts about a diarization model ([`Model::diarize_info`]).
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19#[non_exhaustive]
20#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
21pub struct DiarizeInfo {
22    /// Input PCM rate (16000).
23    pub sample_rate: i32,
24    /// `speaker_id` is in `[1, max_speakers]`.
25    pub max_speakers: i32,
26}
27
28/// Options for creating a diarization session.
29#[derive(Debug, Clone, Default, PartialEq, Eq)]
30#[cfg_attr(
31    feature = "serde",
32    derive(serde::Serialize, serde::Deserialize),
33    serde(default)
34)]
35pub struct DiarizeSessionOptions {
36    /// CPU threads for ops that run on CPU; 0 = library default.
37    pub n_threads: i32,
38}
39
40/// Per-run diarization parameters.
41#[derive(Debug, Clone, Default, PartialEq, Eq)]
42#[cfg_attr(
43    feature = "serde",
44    derive(serde::Serialize, serde::Deserialize),
45    serde(default)
46)]
47pub struct DiarizeOptions {
48    /// Optional family-specific extension (e.g. the Sortformer preset).
49    pub family: Option<DiarizeExtension>,
50}
51
52/// A diarization session.
53pub struct DiarizeSession {
54    ptr: *mut sys::transcribe_diarize_session,
55    // Keeps the native model alive and carries the per-model compute lock.
56    model: Arc<ModelInner>,
57    // Keeps the abort callback's userdata alive while installed.
58    cancel: Option<Arc<AtomicBool>>,
59}
60
61impl std::fmt::Debug for DiarizeSession {
62    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63        f.debug_struct("DiarizeSession").finish_non_exhaustive()
64    }
65}
66
67// SAFETY: as for `Session` — `&mut self` on the mutating calls keeps use to
68// one thread at a time; deliberately NOT Sync.
69unsafe impl Send for DiarizeSession {}
70
71impl Drop for DiarizeSession {
72    fn drop(&mut self) {
73        unsafe { sys::transcribe_diarize_session_free(self.ptr) };
74    }
75}
76
77impl DiarizeSession {
78    pub(crate) fn new(model: &Model, options: &DiarizeSessionOptions) -> Result<DiarizeSession> {
79        let mut params: sys::transcribe_diarize_session_params = unsafe { std::mem::zeroed() };
80        unsafe { sys::transcribe_diarize_session_params_init(&mut params) };
81        params.n_threads = options.n_threads;
82
83        let mut out: *mut sys::transcribe_diarize_session = std::ptr::null_mut();
84        let status =
85            unsafe { sys::transcribe_diarize_session_init(model.inner.ptr, &params, &mut out) };
86        check(status, "diarize session init")?;
87        debug_assert!(!out.is_null());
88
89        Ok(DiarizeSession {
90            ptr: out,
91            model: Arc::clone(&model.inner),
92            cancel: None,
93        })
94    }
95
96    /// Install a [`CancelToken`] so an in-flight run can be aborted from
97    /// another thread (the run then returns [`Error::Aborted`](crate::Error)).
98    /// Replaces any previously installed token.
99    pub fn set_cancel_token(&mut self, token: &CancelToken) {
100        let flag = Arc::clone(&token.flag);
101        let userdata = Arc::as_ptr(&flag) as *mut c_void;
102        unsafe {
103            sys::transcribe_diarize_set_abort_callback(self.ptr, Some(abort_trampoline), userdata)
104        };
105        self.cancel = Some(flag);
106    }
107
108    /// Remove any installed cancel token.
109    pub fn clear_cancel_token(&mut self) {
110        unsafe { sys::transcribe_diarize_set_abort_callback(self.ptr, None, std::ptr::null_mut()) };
111        self.cancel = None;
112    }
113
114    /// Diarize one recording of 16 kHz mono float32 PCM. Returns the speaker
115    /// segments, grouped by speaker and time-ordered within a speaker;
116    /// segments of different speakers may overlap.
117    pub fn run(&mut self, pcm: &[f32], options: &DiarizeOptions) -> Result<Vec<SpeakerSegment>> {
118        let n = clamp_len(pcm.len())?;
119        let family = options.family.as_ref().map(DiarizeExtension::materialize);
120        let mut params: sys::transcribe_diarize_params = unsafe { std::mem::zeroed() };
121        unsafe { sys::transcribe_diarize_params_init(&mut params) };
122        params.family = family.as_ref().map_or(std::ptr::null(), |f| f.ext_ptr());
123
124        let status = self.model.with_compute(
125            Some("a stream is active on this model; finish or drop it before diarize run()"),
126            |_| unsafe { sys::transcribe_diarize_run(self.ptr, pcm.as_ptr(), n, &params) },
127        )?;
128        check(status, "diarize run")?;
129
130        let count = unsafe { sys::transcribe_diarize_n_segments(self.ptr) };
131        Ok((0..count)
132            .map(|i| {
133                let mut raw: sys::transcribe_speaker_segment = unsafe { std::mem::zeroed() };
134                unsafe { sys::transcribe_speaker_segment_init(&mut raw) };
135                let _ = unsafe { sys::transcribe_diarize_get_segment(self.ptr, i, &mut raw) };
136                SpeakerSegment::from_raw(&raw)
137            })
138            .collect())
139    }
140
141    /// Model load time plus the last run's stage timings (`decode_ms` is 0).
142    pub fn timings(&self) -> Timings {
143        let mut raw: sys::transcribe_timings = unsafe { std::mem::zeroed() };
144        unsafe { sys::transcribe_timings_init(&mut raw) };
145        let _ = unsafe { sys::transcribe_diarize_get_timings(self.ptr, &mut raw) };
146        Timings::from_raw(&raw)
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153    use crate::Error;
154    use std::sync::Mutex;
155
156    #[test]
157    fn run_is_busy_while_a_stream_holds_the_lease() {
158        // Null native handles (both frees are NULL no-ops): the Busy check runs
159        // under the lock before any native call.
160        let mut session = DiarizeSession {
161            ptr: std::ptr::null_mut(),
162            model: Arc::new(ModelInner {
163                ptr: std::ptr::null_mut(),
164                compute_lock: Mutex::new(true),
165            }),
166            cancel: None,
167        };
168        let err = session
169            .run(&[0.0; 160], &DiarizeOptions::default())
170            .unwrap_err();
171        let Error::Busy(msg) = err else {
172            panic!("expected Busy, got {err:?}");
173        };
174        assert_eq!(
175            msg,
176            "a stream is active on this model; finish or drop it before diarize run()"
177        );
178    }
179}