Skip to main content

transcribe_cpp/
session.rs

1//! [`Session`] — a transcription context bound to a [`Model`].
2//!
3//! A session is single-threaded (`Send` but not `Sync`): `run` takes `&mut
4//! self`, so the type system enforces "one call at a time" on a given session.
5//! Across *different* sessions of one model the per-model compute lock
6//! ([`ModelInner::compute_lock`](crate::model::ModelInner)) enforces the C
7//! library's "one in-flight compute per model" rule: one-shot runs/batches
8//! serialize on it, and while a stream is in flight any overlapping
9//! run/batch/stream is refused with [`Error::Busy`].
10
11use std::ffi::CString;
12use std::os::raw::c_void;
13use std::sync::atomic::AtomicBool;
14use std::sync::Arc;
15
16use transcribe_cpp_sys as sys;
17
18use crate::cancel::{abort_trampoline, CancelToken};
19use crate::error::{check, error_for_status, Error, Result};
20use crate::family::{RunExtRaw, RunExtension};
21use crate::model::{Model, ModelInner, SessionLimits, SessionOptions};
22use crate::result::{owned_opt_str, owned_str, Segment, Timings, Token, Transcript, Word};
23use crate::streaming::{build_stream_params, StreamOptions, StreamText, StreamUpdate};
24use crate::types::{Itn, Pnc, StreamState, Task, TimestampKind};
25
26/// Per-run parameters.
27#[derive(Debug, Clone, PartialEq)]
28pub struct RunOptions {
29    pub task: Task,
30    pub timestamps: TimestampKind,
31    pub pnc: Pnc,
32    pub itn: Itn,
33    /// Source language hint (ISO code), or `None` to autodetect.
34    pub language: Option<String>,
35    /// Target language for translation, or `None`.
36    pub target_language: Option<String>,
37    /// Keep special vocab tags (e.g. `<|...|>`) in the returned text.
38    pub keep_special_tags: bool,
39    /// Speculative-decode draft length. `-1` = family default, `0` = disabled.
40    pub spec_k_drafts: i32,
41    /// Optional family-specific run extension (e.g. whisper decode knobs).
42    pub family: Option<RunExtension>,
43}
44
45impl Default for RunOptions {
46    fn default() -> Self {
47        RunOptions {
48            task: Task::Transcribe,
49            timestamps: TimestampKind::None,
50            pnc: Pnc::Default,
51            itn: Itn::Default,
52            language: None,
53            target_language: None,
54            keep_special_tags: false,
55            spec_k_drafts: -1,
56            family: None,
57        }
58    }
59}
60
61/// A transcription session.
62pub struct Session {
63    ptr: *mut sys::transcribe_session,
64    // Keeps the native model alive (and carries the per-model run lock). The
65    // model is freed only after this Arc — and every other handle — is dropped.
66    model: Arc<ModelInner>,
67    // Retained so the abort callback's userdata pointer stays valid while
68    // installed. `None` when no cancel token is set.
69    cancel: Option<Arc<AtomicBool>>,
70}
71
72impl std::fmt::Debug for Session {
73    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74        f.debug_struct("Session").finish_non_exhaustive()
75    }
76}
77
78// SAFETY: a session may be moved between threads as long as it is used by at
79// most one at a time, which `&mut self` on the mutating calls enforces. It is
80// deliberately NOT Sync.
81unsafe impl Send for Session {}
82
83impl Drop for Session {
84    fn drop(&mut self) {
85        unsafe { sys::transcribe_session_free(self.ptr) };
86        // self.model Arc is dropped here, possibly freeing the model.
87    }
88}
89
90impl Session {
91    pub(crate) fn new(model: &Model, options: &SessionOptions) -> Result<Session> {
92        let mut params: sys::transcribe_session_params = unsafe { std::mem::zeroed() };
93        unsafe { sys::transcribe_session_params_init(&mut params) };
94        params.n_threads = options.n_threads;
95        params.kv_type = options.kv_type.to_raw();
96        params.n_ctx = options.n_ctx;
97
98        let mut out: *mut sys::transcribe_session = std::ptr::null_mut();
99        let status = unsafe { sys::transcribe_session_init(model.inner.ptr, &params, &mut out) };
100        check(status, "session init")?;
101        debug_assert!(!out.is_null());
102
103        Ok(Session {
104            ptr: out,
105            model: Arc::clone(&model.inner),
106            cancel: None,
107        })
108    }
109
110    /// Install a [`CancelToken`] so an in-flight run/stream can be aborted from
111    /// another thread. Replaces any previously installed token. The token's
112    /// flag is shared, so cancel via any clone of it. (Cancellation only takes
113    /// effect on models whose family honors the abort callback —
114    /// [`Feature::Cancellation`](crate::Feature::Cancellation).)
115    pub fn set_cancel_token(&mut self, token: &CancelToken) {
116        let flag = Arc::clone(&token.flag);
117        let userdata = Arc::as_ptr(&flag) as *mut c_void;
118        // SAFETY: single-threaded session contract — no run is in flight when
119        // this is called. The new callback is installed before the old retained
120        // Arc is dropped, so the previous userdata is never dangling-referenced.
121        unsafe { sys::transcribe_set_abort_callback(self.ptr, Some(abort_trampoline), userdata) };
122        self.cancel = Some(flag);
123    }
124
125    /// Remove any installed cancel token.
126    pub fn clear_cancel_token(&mut self) {
127        unsafe { sys::transcribe_set_abort_callback(self.ptr, None, std::ptr::null_mut()) };
128        self.cancel = None;
129    }
130
131    /// Whether the most recent run/stream was ended by cancellation.
132    pub fn was_aborted(&self) -> bool {
133        unsafe { sys::transcribe_was_aborted(self.ptr) }
134    }
135
136    /// Whether the most recent decode stopped at the generation budget before
137    /// end-of-stream (the transcript is incomplete).
138    pub fn was_truncated(&self) -> bool {
139        unsafe { sys::transcribe_was_truncated(self.ptr) }
140    }
141
142    /// Transcribe one buffer of 16 kHz mono float32 PCM in `[-1, 1]`.
143    ///
144    /// On an aborted or truncated decode the partial transcript is preserved
145    /// on the returned [`Error::Aborted`] / [`Error::OutputTruncated`].
146    pub fn run(&mut self, pcm: &[f32], options: &RunOptions) -> Result<Transcript> {
147        let (params, _lang, _target, _family) = build_run_params(options)?;
148        let n = clamp_len(pcm.len())?;
149
150        // The compute path is serialized per model; hold the lock for the native
151        // call, then materialize this session's own result storage. Refuse a run
152        // that would overlap an active stream on another session (the C contract).
153        let status = {
154            let guard = self
155                .model
156                .compute_lock
157                .lock()
158                .unwrap_or_else(|e| e.into_inner());
159            if *guard {
160                return Err(Error::Busy(
161                    "a stream is active on this model; finish or drop it before run()".into(),
162                ));
163            }
164            unsafe { sys::transcribe_run(self.ptr, pcm.as_ptr(), n, &params) }
165        };
166
167        match status {
168            s if s == sys::transcribe_status::TRANSCRIBE_OK => Ok(self.materialize_run()),
169            s if s == sys::transcribe_status::TRANSCRIBE_ERR_ABORTED
170                || s == sys::transcribe_status::TRANSCRIBE_ERR_OUTPUT_TRUNCATED =>
171            {
172                // Partial transcript is preserved by the C API for both.
173                let partial = Box::new(self.materialize_run());
174                Err(attach_partial(error_for_status(s, "run"), partial))
175            }
176            s => Err(error_for_status(s, "run")),
177        }
178    }
179
180    /// Transcribe several buffers in one call (throughput path).
181    ///
182    /// Returns the whole-batch outcome as an outer `Result`; each utterance's
183    /// individual outcome is an inner `Result` in the returned vector (one per
184    /// input, in order). A malformed single utterance fails only its own slot.
185    pub fn run_batch(
186        &mut self,
187        pcms: &[&[f32]],
188        options: &RunOptions,
189    ) -> Result<Vec<Result<Transcript>>> {
190        let (params, _lang, _target, _family) = build_run_params(options)?;
191        let ptrs: Vec<*const f32> = pcms.iter().map(|p| p.as_ptr()).collect();
192        let lens: Vec<i32> = pcms
193            .iter()
194            .map(|p| clamp_len(p.len()))
195            .collect::<Result<_>>()?;
196        let n = clamp_len(pcms.len())?;
197
198        let status = {
199            let guard = self
200                .model
201                .compute_lock
202                .lock()
203                .unwrap_or_else(|e| e.into_inner());
204            if *guard {
205                return Err(Error::Busy(
206                    "a stream is active on this model; finish or drop it before run_batch()".into(),
207                ));
208            }
209            unsafe { sys::transcribe_run_batch(self.ptr, ptrs.as_ptr(), lens.as_ptr(), n, &params) }
210        };
211
212        // OK and ABORTED both populate one per-utterance slot per input; fold
213        // the batch-level abort into the per-utterance view (the C contract).
214        let ok = status == sys::transcribe_status::TRANSCRIBE_OK
215            || status == sys::transcribe_status::TRANSCRIBE_ERR_ABORTED;
216        if !ok {
217            return Err(error_for_status(status, "run_batch"));
218        }
219
220        let count = unsafe { sys::transcribe_batch_n_results(self.ptr) };
221        let mut results = Vec::with_capacity(count.max(0) as usize);
222        for i in 0..count {
223            let st = unsafe { sys::transcribe_batch_status(self.ptr, i) };
224            if st == sys::transcribe_status::TRANSCRIBE_OK {
225                results.push(Ok(self.materialize_batch(i)));
226            } else if st == sys::transcribe_status::TRANSCRIBE_ERR_ABORTED
227                || st == sys::transcribe_status::TRANSCRIBE_ERR_OUTPUT_TRUNCATED
228            {
229                let partial = Box::new(self.materialize_batch(i));
230                let err = attach_partial(error_for_status(st, "run_batch utterance"), partial);
231                results.push(Err(err));
232            } else {
233                results.push(Err(error_for_status(st, "run_batch utterance")));
234            }
235        }
236        Ok(results)
237    }
238
239    /// The effective per-session limits.
240    pub fn limits(&self) -> Result<SessionLimits> {
241        let mut out: sys::transcribe_session_limits = unsafe { std::mem::zeroed() };
242        unsafe { sys::transcribe_session_limits_init(&mut out) };
243        let status = unsafe { sys::transcribe_session_get_limits(self.ptr, &mut out) };
244        check(status, "session limits")?;
245        Ok(SessionLimits {
246            effective_n_ctx: out.effective_n_ctx,
247            effective_max_audio_ms: out.effective_max_audio_ms,
248            max_kv_bytes: out.max_kv_bytes,
249        })
250    }
251
252    /// The model this session is bound to.
253    pub fn model(&self) -> Model {
254        Model {
255            inner: Arc::clone(&self.model),
256        }
257    }
258
259    /// Begin a streaming run, returning a [`Stream`] that borrows this session
260    /// for its lifetime (so the session can't be used for an offline `run`
261    /// while a stream is active). `run` supplies task / language / timestamps;
262    /// `stream` supplies the commit policy and any stream-slot family extension.
263    /// Dropping the returned `Stream` abandons it and returns the session to
264    /// idle.
265    pub fn stream(&mut self, run: &RunOptions, stream: &StreamOptions) -> Result<Stream<'_>> {
266        let (run_params, _lang, _target, _family) = build_run_params(run)?;
267        let (stream_params, _stream_family) = build_stream_params(stream);
268        {
269            // Claim the model's compute lease for the whole stream lifetime: a
270            // stream spans begin..drop, so per-call locking alone would let a
271            // second stream (or a run) on another session race it.
272            let mut guard = self
273                .model
274                .compute_lock
275                .lock()
276                .unwrap_or_else(|e| e.into_inner());
277            if *guard {
278                return Err(Error::Busy(
279                    "a stream is already active on this model".into(),
280                ));
281            }
282            check(
283                unsafe { sys::transcribe_stream_begin(self.ptr, &run_params, &stream_params) },
284                "stream begin",
285            )?;
286            *guard = true; // released at finalize/reset, or by Stream::drop
287        }
288        Ok(Stream {
289            session: self,
290            holds_lease: true,
291        })
292    }
293
294    // --- result materialization (top-level / single accessors) ---------------
295
296    fn materialize_run(&self) -> Transcript {
297        let n_seg = unsafe { sys::transcribe_n_segments(self.ptr) };
298        let n_word = unsafe { sys::transcribe_n_words(self.ptr) };
299        let n_tok = unsafe { sys::transcribe_n_tokens(self.ptr) };
300
301        Transcript {
302            text: owned_str(unsafe { sys::transcribe_full_text(self.ptr) }),
303            language: owned_opt_str(unsafe { sys::transcribe_detected_language(self.ptr) }),
304            timestamp_kind: TimestampKind::from_raw(unsafe {
305                sys::transcribe_returned_timestamp_kind(self.ptr)
306            }),
307            segments: (0..n_seg).map(|i| self.segment(i)).collect(),
308            words: (0..n_word).map(|i| self.word(i)).collect(),
309            tokens: (0..n_tok).map(|i| self.token(i)).collect(),
310            timings: self.timings(),
311        }
312    }
313
314    fn segment(&self, i: i32) -> Segment {
315        let mut raw: sys::transcribe_segment = unsafe { std::mem::zeroed() };
316        unsafe { sys::transcribe_segment_init(&mut raw) };
317        let _ = unsafe { sys::transcribe_get_segment(self.ptr, i, &mut raw) };
318        Segment::from_raw(&raw)
319    }
320
321    fn word(&self, i: i32) -> Word {
322        let mut raw: sys::transcribe_word = unsafe { std::mem::zeroed() };
323        unsafe { sys::transcribe_word_init(&mut raw) };
324        let _ = unsafe { sys::transcribe_get_word(self.ptr, i, &mut raw) };
325        Word::from_raw(&raw)
326    }
327
328    fn token(&self, i: i32) -> Token {
329        let mut raw: sys::transcribe_token = unsafe { std::mem::zeroed() };
330        unsafe { sys::transcribe_token_init(&mut raw) };
331        let _ = unsafe { sys::transcribe_get_token(self.ptr, i, &mut raw) };
332        Token::from_raw(&raw)
333    }
334
335    fn timings(&self) -> Timings {
336        let mut raw: sys::transcribe_timings = unsafe { std::mem::zeroed() };
337        unsafe { sys::transcribe_timings_init(&mut raw) };
338        let _ = unsafe { sys::transcribe_get_timings(self.ptr, &mut raw) };
339        Timings::from_raw(&raw)
340    }
341
342    // --- result materialization (batch accessors, utterance i) ---------------
343
344    fn materialize_batch(&self, i: i32) -> Transcript {
345        let n_seg = unsafe { sys::transcribe_batch_n_segments(self.ptr, i) };
346        let n_word = unsafe { sys::transcribe_batch_n_words(self.ptr, i) };
347        let n_tok = unsafe { sys::transcribe_batch_n_tokens(self.ptr, i) };
348
349        Transcript {
350            text: owned_str(unsafe { sys::transcribe_batch_full_text(self.ptr, i) }),
351            language: owned_opt_str(unsafe {
352                sys::transcribe_batch_detected_language(self.ptr, i)
353            }),
354            timestamp_kind: TimestampKind::from_raw(unsafe {
355                sys::transcribe_batch_returned_timestamp_kind(self.ptr, i)
356            }),
357            segments: (0..n_seg).map(|j| self.batch_segment(i, j)).collect(),
358            words: (0..n_word).map(|j| self.batch_word(i, j)).collect(),
359            tokens: (0..n_tok).map(|j| self.batch_token(i, j)).collect(),
360            timings: self.batch_timings(i),
361        }
362    }
363
364    fn batch_segment(&self, i: i32, j: i32) -> Segment {
365        let mut raw: sys::transcribe_segment = unsafe { std::mem::zeroed() };
366        unsafe { sys::transcribe_segment_init(&mut raw) };
367        let _ = unsafe { sys::transcribe_batch_get_segment(self.ptr, i, j, &mut raw) };
368        Segment::from_raw(&raw)
369    }
370
371    fn batch_word(&self, i: i32, j: i32) -> Word {
372        let mut raw: sys::transcribe_word = unsafe { std::mem::zeroed() };
373        unsafe { sys::transcribe_word_init(&mut raw) };
374        let _ = unsafe { sys::transcribe_batch_get_word(self.ptr, i, j, &mut raw) };
375        Word::from_raw(&raw)
376    }
377
378    fn batch_token(&self, i: i32, j: i32) -> Token {
379        let mut raw: sys::transcribe_token = unsafe { std::mem::zeroed() };
380        unsafe { sys::transcribe_token_init(&mut raw) };
381        let _ = unsafe { sys::transcribe_batch_get_token(self.ptr, i, j, &mut raw) };
382        Token::from_raw(&raw)
383    }
384
385    fn batch_timings(&self, i: i32) -> Timings {
386        let mut raw: sys::transcribe_timings = unsafe { std::mem::zeroed() };
387        unsafe { sys::transcribe_timings_init(&mut raw) };
388        let _ = unsafe { sys::transcribe_batch_get_timings(self.ptr, i, &mut raw) };
389        Timings::from_raw(&raw)
390    }
391}
392
393/// Everything that must outlive a `transcribe_run` call: the params struct
394/// plus the heap buffers its pointers borrow (language strings, family ext).
395type RunParamsBundle = (
396    sys::transcribe_run_params,
397    Option<CString>,
398    Option<CString>,
399    Option<RunExtRaw>,
400);
401
402/// Build `transcribe_run_params` from options. The returned keepalives own the
403/// buffers the params' pointers borrow, so the caller must hold them for the
404/// duration of the native call.
405fn build_run_params(o: &RunOptions) -> Result<RunParamsBundle> {
406    let mut params: sys::transcribe_run_params = unsafe { std::mem::zeroed() };
407    unsafe { sys::transcribe_run_params_init(&mut params) };
408
409    params.task = o.task.to_raw();
410    params.timestamps = o.timestamps.to_raw();
411    params.pnc = o.pnc.to_raw();
412    params.itn = o.itn.to_raw();
413    params.keep_special_tags = o.keep_special_tags;
414    params.spec_k_drafts = o.spec_k_drafts;
415
416    let lang = o.language.as_deref().map(CString::new).transpose()?;
417    let target = o.target_language.as_deref().map(CString::new).transpose()?;
418    params.language = lang.as_ref().map_or(std::ptr::null(), |c| c.as_ptr());
419    params.target_language = target.as_ref().map_or(std::ptr::null(), |c| c.as_ptr());
420
421    let family = o
422        .family
423        .as_ref()
424        .map(RunExtension::materialize)
425        .transpose()?;
426    params.family = family.as_ref().map_or(std::ptr::null(), |f| f.ext_ptr());
427
428    Ok((params, lang, target, family))
429}
430
431/// PCM/utterance lengths cross the ABI as `int`; reject anything that overflows.
432fn clamp_len(len: usize) -> Result<i32> {
433    i32::try_from(len).map_err(|_| Error::InvalidArgument(format!("length {len} exceeds i32::MAX")))
434}
435
436/// Replace the (partial-less) Aborted/OutputTruncated error with one carrying
437/// the materialized partial transcript.
438fn attach_partial(err: Error, partial: Box<Transcript>) -> Error {
439    match err {
440        Error::Aborted { message, .. } => Error::Aborted {
441            message,
442            partial: Some(partial),
443        },
444        Error::OutputTruncated { message, .. } => Error::OutputTruncated {
445            message,
446            partial: Some(partial),
447        },
448        other => other,
449    }
450}
451
452/// An active streaming run, borrowed from a [`Session`].
453///
454/// Feed PCM with [`Stream::feed`], read the UI-stable text with
455/// [`Stream::text`], and end input with [`Stream::finalize`]. Dropping the
456/// `Stream` (without finalizing) abandons it and returns the session to idle.
457/// `Send` but not `Sync`, like the session it borrows.
458pub struct Stream<'a> {
459    session: &'a mut Session,
460    // True while this stream holds the model's compute lease. Set at begin,
461    // cleared the moment the stream stops being active (finalize/reset) or on
462    // drop — whichever comes first. Tracked per-stream so drop never releases a
463    // lease a *different* session has since acquired.
464    holds_lease: bool,
465}
466
467impl std::fmt::Debug for Stream<'_> {
468    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
469        f.debug_struct("Stream")
470            .field("state", &self.state())
471            .field("revision", &self.revision())
472            .finish()
473    }
474}
475
476impl Drop for Stream<'_> {
477    fn drop(&mut self) {
478        // Abandon any unfinalized stream (reset is idempotent and safe from any
479        // state) and release the lease IF this stream still holds it — under the
480        // lock. After finalize/reset the lease is already gone (and may now be
481        // another session's), so only clear it when holds_lease is still true.
482        let mut guard = self
483            .session
484            .model
485            .compute_lock
486            .lock()
487            .unwrap_or_else(|e| e.into_inner());
488        unsafe { sys::transcribe_stream_reset(self.session.ptr) };
489        if self.holds_lease {
490            *guard = false;
491        }
492    }
493}
494
495impl Stream<'_> {
496    /// Feed a chunk of 16 kHz mono float32 PCM and return the change metadata.
497    pub fn feed(&mut self, pcm: &[f32]) -> Result<StreamUpdate> {
498        let n = clamp_len(pcm.len())?;
499        let mut update: sys::transcribe_stream_update = unsafe { std::mem::zeroed() };
500        unsafe { sys::transcribe_stream_update_init(&mut update) };
501        let status = {
502            // This stream already holds the model's compute lease (it is in
503            // flight); the lock here just serializes the native call.
504            let _guard = self
505                .session
506                .model
507                .compute_lock
508                .lock()
509                .unwrap_or_else(|e| e.into_inner());
510            unsafe { sys::transcribe_stream_feed(self.session.ptr, pcm.as_ptr(), n, &mut update) }
511        };
512        check(status, "stream feed")?;
513        Ok(StreamUpdate::from_raw(&update))
514    }
515
516    /// Signal end of input: flush buffered audio and emit the final text. The
517    /// session transitions to finished.
518    pub fn finalize(&mut self) -> Result<StreamUpdate> {
519        let mut update: sys::transcribe_stream_update = unsafe { std::mem::zeroed() };
520        unsafe { sys::transcribe_stream_update_init(&mut update) };
521        let status = {
522            let mut guard = self
523                .session
524                .model
525                .compute_lock
526                .lock()
527                .unwrap_or_else(|e| e.into_inner());
528            let st = unsafe { sys::transcribe_stream_finalize(self.session.ptr, &mut update) };
529            // Finalize ends the active stream (Finished, or Failed on error);
530            // either way it is no longer active, so free the model's compute
531            // lease now rather than holding other sessions off until drop.
532            if self.holds_lease {
533                *guard = false;
534            }
535            st
536        };
537        self.holds_lease = false;
538        check(status, "stream finalize")?;
539        Ok(StreamUpdate::from_raw(&update))
540    }
541
542    /// Abandon the stream and return the session to idle (clears all snapshot
543    /// state). Releases the model's compute lease (the stream is no longer
544    /// active), so other sessions of the model can proceed without waiting for
545    /// this `Stream` to drop.
546    pub fn reset(&mut self) {
547        let mut guard = self
548            .session
549            .model
550            .compute_lock
551            .lock()
552            .unwrap_or_else(|e| e.into_inner());
553        unsafe { sys::transcribe_stream_reset(self.session.ptr) };
554        if self.holds_lease {
555            *guard = false;
556        }
557        self.holds_lease = false;
558    }
559
560    /// The current UI-facing text snapshot (owned copies).
561    pub fn text(&self) -> StreamText {
562        let mut raw: sys::transcribe_stream_text = unsafe { std::mem::zeroed() };
563        unsafe { sys::transcribe_stream_text_init(&mut raw) };
564        let _ = unsafe { sys::transcribe_stream_get_text(self.session.ptr, &mut raw) };
565        StreamText::from_raw(&raw)
566    }
567
568    /// A full structured snapshot (segments/words/tokens) of the current
569    /// hypothesis. For most UIs [`Stream::text`] is the better choice.
570    pub fn snapshot(&self) -> Transcript {
571        self.session.materialize_run()
572    }
573
574    /// The stream's lifecycle state.
575    pub fn state(&self) -> StreamState {
576        StreamState::from_raw(unsafe { sys::transcribe_stream_get_state(self.session.ptr) })
577    }
578
579    /// The monotonic snapshot revision; diff against the previous value.
580    pub fn revision(&self) -> i32 {
581        unsafe { sys::transcribe_stream_revision(self.session.ptr) }
582    }
583
584    /// The stream's recorded terminal failure, or `None` while it is healthy.
585    ///
586    /// Set after a [`feed`](Self::feed) / [`finalize`](Self::finalize)
587    /// transitioned the stream to [`StreamState::Failed`]; reset by a new
588    /// stream. Inspect it when [`state`](Self::state) is `Failed` to learn why.
589    pub fn last_status(&self) -> Option<Error> {
590        let status = unsafe { sys::transcribe_stream_last_status(self.session.ptr) };
591        if status == sys::transcribe_status::TRANSCRIBE_OK {
592            None
593        } else {
594            Some(error_for_status(status, "stream"))
595        }
596    }
597
598    /// Whether the stream was ended by cancellation.
599    pub fn was_aborted(&self) -> bool {
600        self.session.was_aborted()
601    }
602}