Skip to main content

transcribe_cpp/
result.rs

1//! Owned result types.
2//!
3//! Every result is fully materialized into these types — all text is copied at
4//! the FFI boundary (`CStr -> String`), so no borrowed session pointer ever
5//! escapes to user code (the "copy at the FFI boundary" rule). Holding a
6//! [`Transcript`] across the next `run`/`stream` call is therefore always safe.
7
8use std::ffi::CStr;
9use std::os::raw::c_char;
10
11use transcribe_cpp_sys as sys;
12
13use crate::types::TimestampKind;
14
15/// A fully-materialized transcription result.
16#[derive(Debug, Clone, Default, PartialEq)]
17#[cfg_attr(
18    feature = "serde",
19    derive(serde::Serialize, serde::Deserialize),
20    serde(default)
21)]
22pub struct Transcript {
23    /// The full transcript text.
24    pub text: String,
25    /// The model's decoded output before family post-processing (diarization
26    /// markers, timestamp/special tokens, tag filtering, whitespace trims).
27    /// Equal to `text` modulo whitespace for families that emit clean text.
28    pub raw_text: String,
29    /// The model-detected language (ISO code), if it predicted one. `None`
30    /// when a language hint was given, the model lacks LID, or none applies.
31    pub language: Option<String>,
32    /// The finest timestamp granularity actually populated below.
33    pub timestamp_kind: TimestampKind,
34    /// Transcript segments. Speaker-attributed models may return untimed rows
35    /// even when timestamps are disabled.
36    pub segments: Vec<Segment>,
37    /// Diarized speaker turns (empty unless diarization was enabled and found).
38    pub speaker_segments: Vec<SpeakerSegment>,
39    /// Word rows (empty unless word-or-finer timestamps were produced).
40    pub words: Vec<Word>,
41    /// Token rows (empty unless token timestamps were produced).
42    pub tokens: Vec<Token>,
43    /// Stage timings for the run.
44    pub timings: Timings,
45}
46
47/// One segment row. Times are milliseconds relative to the original audio.
48#[derive(Debug, Clone, Default, PartialEq)]
49#[cfg_attr(
50    feature = "serde",
51    derive(serde::Serialize, serde::Deserialize),
52    serde(default)
53)]
54pub struct Segment {
55    pub t0_ms: i64,
56    pub t1_ms: i64,
57    pub first_word: i32,
58    pub n_words: i32,
59    pub first_token: i32,
60    pub n_tokens: i32,
61    pub text: String,
62    /// 1-based speaker id; zero means no attribution.
63    pub speaker_id: i32,
64}
65
66/// One diarized speaker turn. Zero times mean the model attributed text but
67/// did not provide turn timing; `p` is NaN when unavailable.
68#[derive(Debug, Clone, Default, PartialEq)]
69#[cfg_attr(
70    feature = "serde",
71    derive(serde::Serialize, serde::Deserialize),
72    serde(default)
73)]
74pub struct SpeakerSegment {
75    pub t0_ms: i64,
76    pub t1_ms: i64,
77    pub speaker_id: i32,
78    #[cfg_attr(
79        feature = "serde",
80        serde(default = "nan", deserialize_with = "nan_from_null")
81    )]
82    pub p: f32,
83}
84
85/// One word row.
86#[derive(Debug, Clone, Default, PartialEq)]
87#[cfg_attr(
88    feature = "serde",
89    derive(serde::Serialize, serde::Deserialize),
90    serde(default)
91)]
92pub struct Word {
93    pub t0_ms: i64,
94    pub t1_ms: i64,
95    pub seg_index: i32,
96    pub first_token: i32,
97    pub n_tokens: i32,
98    pub text: String,
99}
100
101/// One token row. `p` is a per-token confidence hint (NaN when the family
102/// produces none); its exact meaning is family-specific.
103#[derive(Debug, Clone, Default, PartialEq)]
104#[cfg_attr(
105    feature = "serde",
106    derive(serde::Serialize, serde::Deserialize),
107    serde(default)
108)]
109pub struct Token {
110    pub id: i32,
111    #[cfg_attr(
112        feature = "serde",
113        serde(default = "nan", deserialize_with = "nan_from_null")
114    )]
115    pub p: f32,
116    pub t0_ms: i64,
117    pub t1_ms: i64,
118    pub seg_index: i32,
119    pub word_index: i32,
120    pub text: String,
121}
122
123/// Per-run stage timings, in milliseconds. Zero means "unknown / not measured".
124#[derive(Debug, Clone, Copy, Default, PartialEq)]
125#[cfg_attr(
126    feature = "serde",
127    derive(serde::Serialize, serde::Deserialize),
128    serde(default)
129)]
130pub struct Timings {
131    pub load_ms: f32,
132    pub mel_ms: f32,
133    pub encode_ms: f32,
134    pub decode_ms: f32,
135}
136
137/// A missing confidence means "none reported" (NaN), not 0.0.
138#[cfg(feature = "serde")]
139fn nan() -> f32 {
140    f32::NAN
141}
142
143/// JSON writes NaN as `null`; read it back as NaN. Binary formats carry the
144/// raw `f32`.
145#[cfg(feature = "serde")]
146fn nan_from_null<'de, D: serde::Deserializer<'de>>(d: D) -> Result<f32, D::Error> {
147    use serde::Deserialize;
148    if d.is_human_readable() {
149        Ok(Option::<f32>::deserialize(d)?.unwrap_or(f32::NAN))
150    } else {
151        f32::deserialize(d)
152    }
153}
154
155/// Copy a borrowed C string into an owned `String` (empty for NULL).
156pub(crate) fn owned_str(ptr: *const c_char) -> String {
157    if ptr.is_null() {
158        return String::new();
159    }
160    unsafe { CStr::from_ptr(ptr) }
161        .to_string_lossy()
162        .into_owned()
163}
164
165/// Copy a borrowed C string into `Some(String)`, or `None` for NULL/empty.
166pub(crate) fn owned_opt_str(ptr: *const c_char) -> Option<String> {
167    if ptr.is_null() {
168        return None;
169    }
170    let s = unsafe { CStr::from_ptr(ptr) }
171        .to_string_lossy()
172        .into_owned();
173    if s.is_empty() {
174        None
175    } else {
176        Some(s)
177    }
178}
179
180impl Segment {
181    pub(crate) fn from_raw(raw: &sys::transcribe_segment) -> Self {
182        Segment {
183            t0_ms: raw.t0_ms,
184            t1_ms: raw.t1_ms,
185            first_word: raw.first_word,
186            n_words: raw.n_words,
187            first_token: raw.first_token,
188            n_tokens: raw.n_tokens,
189            text: owned_str(raw.text),
190            speaker_id: raw.speaker_id,
191        }
192    }
193}
194
195impl SpeakerSegment {
196    pub(crate) fn from_raw(raw: &sys::transcribe_speaker_segment) -> Self {
197        SpeakerSegment {
198            t0_ms: raw.t0_ms,
199            t1_ms: raw.t1_ms,
200            speaker_id: raw.speaker_id,
201            p: raw.p,
202        }
203    }
204}
205
206impl Word {
207    pub(crate) fn from_raw(raw: &sys::transcribe_word) -> Self {
208        Word {
209            t0_ms: raw.t0_ms,
210            t1_ms: raw.t1_ms,
211            seg_index: raw.seg_index,
212            first_token: raw.first_token,
213            n_tokens: raw.n_tokens,
214            text: owned_str(raw.text),
215        }
216    }
217}
218
219impl Token {
220    pub(crate) fn from_raw(raw: &sys::transcribe_token) -> Self {
221        Token {
222            id: raw.id,
223            p: raw.p,
224            t0_ms: raw.t0_ms,
225            t1_ms: raw.t1_ms,
226            seg_index: raw.seg_index,
227            word_index: raw.word_index,
228            text: owned_str(raw.text),
229        }
230    }
231}
232
233impl Timings {
234    pub(crate) fn from_raw(raw: &sys::transcribe_timings) -> Self {
235        Timings {
236            load_ms: raw.load_ms,
237            mel_ms: raw.mel_ms,
238            encode_ms: raw.encode_ms,
239            decode_ms: raw.decode_ms,
240        }
241    }
242}