Skip to main content

transcribe_cpp/
error.rs

1//! Error type mapped from `transcribe_status`.
2//!
3//! The C API reports failures as a `transcribe_status` int; this module maps
4//! each one to a distinct [`Error`] variant (requirements §3: "distinct
5//! failures stay distinct"). The grouping mirrors the Python binding's
6//! exception hierarchy — the conformance precedent — and the raw status is
7//! preserved via [`Error::raw_status`].
8
9use crate::result::Transcript;
10use transcribe_cpp_sys as sys;
11
12/// Convenience alias for results from this crate.
13pub type Result<T> = std::result::Result<T, Error>;
14
15/// Everything that can go wrong talking to the native library.
16///
17/// Match on the variant for typed handling; [`Error::raw_status`] recovers the
18/// underlying `transcribe_status` code (0 for errors raised purely on the Rust
19/// side, e.g. the load-time version gate or a `NUL` in a caller string).
20#[derive(Debug, thiserror::Error)]
21#[non_exhaustive]
22pub enum Error {
23    /// `TRANSCRIBE_ERR_INVALID_ARG` (or `_SAMPLE_RATE`).
24    #[error("invalid argument: {0}")]
25    InvalidArgument(String),
26    /// `TRANSCRIBE_ERR_NOT_IMPLEMENTED` — the model has no path for this call.
27    #[error("not implemented by this model: {0}")]
28    NotImplemented(String),
29    /// `TRANSCRIBE_ERR_FILE_NOT_FOUND` — the GGUF path does not exist.
30    #[error("model file not found: {0}")]
31    ModelFileNotFound(String),
32    /// `TRANSCRIBE_ERR_GGUF` / `_UNSUPPORTED_ARCH` / `_UNSUPPORTED_VARIANT`.
33    #[error("model load failed: {0}")]
34    ModelLoad(String),
35    /// `TRANSCRIBE_ERR_OOM`.
36    #[error("out of memory: {0}")]
37    OutOfMemory(String),
38    /// `TRANSCRIBE_ERR_BACKEND` — the requested backend could not be satisfied.
39    #[error("backend error: {0}")]
40    Backend(String),
41    /// `TRANSCRIBE_ERR_UNSUPPORTED_{TASK,LANGUAGE,TIMESTAMPS,PNC,ITN}` — the
42    /// model cannot satisfy this request shape.
43    #[error("unsupported request: {0}")]
44    Unsupported(String),
45    /// `TRANSCRIBE_ERR_BAD_STRUCT_SIZE` — an ABI/struct-size fault. With the
46    /// generated FFI this indicates a header/library skew, not caller error.
47    #[error("ABI struct-size mismatch: {0}")]
48    BadStructSize(String),
49    /// `TRANSCRIBE_ERR_INPUT_TOO_LONG` — audio longer than the model can decode.
50    #[error("input too long: {0}")]
51    InputTooLong(String),
52    /// `TRANSCRIBE_ERR_ABORTED` — the abort callback returned true. The
53    /// transcript of chunks that completed before the abort is preserved.
54    #[error("operation aborted: {message}")]
55    Aborted {
56        message: String,
57        /// Partial transcript from chunks that completed before the abort.
58        partial: Option<Box<Transcript>>,
59    },
60    /// `TRANSCRIBE_ERR_OUTPUT_TRUNCATED` — the decode hit the generation budget
61    /// before end-of-stream; the transcript is incomplete by contract. The
62    /// partial transcript is always preserved.
63    #[error("output truncated before end-of-stream: {message}")]
64    OutputTruncated {
65        message: String,
66        /// The (incomplete) transcript produced before truncation.
67        partial: Option<Box<Transcript>>,
68    },
69    /// `TRANSCRIBE_ERR_OUTPUT_REPETITION` — the decode was stopped because the
70    /// output began repeating itself; the transcript is incomplete by contract.
71    /// The partial transcript, with the repeats dropped, is always preserved.
72    #[error("output began repeating before end-of-stream: {message}")]
73    OutputRepetition {
74        message: String,
75        /// The (incomplete) transcript produced before the loop, one copy kept.
76        partial: Option<Box<Transcript>>,
77    },
78    /// The loaded library's base version disagrees with the headers this crate
79    /// was generated against (the pre-1.0 version lock). Raised on first use.
80    #[error("native library version mismatch: {0}")]
81    VersionMismatch(String),
82    /// A caller-supplied string contained an interior NUL byte.
83    #[error("string contains an interior NUL byte: {0}")]
84    Nul(#[from] std::ffi::NulError),
85    /// Another session of the same model already has a compute operation in
86    /// flight. The C library serializes compute per model (the 0.x limitation):
87    /// at most one run / batch / active stream across ALL of a model's sessions
88    /// at a time. Finish or drop the in-flight stream (one-shot runs/batches
89    /// just queue) before starting another, or give each concurrent worker its
90    /// own model. Raised purely on the Rust side, so [`Error::raw_status`] is 0.
91    #[error("model busy: {0}")]
92    Busy(String),
93    /// A non-OK status with no more specific mapping.
94    #[error("{0}")]
95    Other(String),
96}
97
98impl Error {
99    /// The `transcribe_status` this error was mapped from, or `0` for errors
100    /// raised on the Rust side (version gate, NUL in a string, …).
101    pub fn raw_status(&self) -> i32 {
102        use sys::transcribe_status as S;
103        let s = match self {
104            Error::InvalidArgument(_) => S::TRANSCRIBE_ERR_INVALID_ARG,
105            Error::NotImplemented(_) => S::TRANSCRIBE_ERR_NOT_IMPLEMENTED,
106            Error::ModelFileNotFound(_) => S::TRANSCRIBE_ERR_FILE_NOT_FOUND,
107            Error::ModelLoad(_) => S::TRANSCRIBE_ERR_GGUF,
108            Error::OutOfMemory(_) => S::TRANSCRIBE_ERR_OOM,
109            Error::Backend(_) => S::TRANSCRIBE_ERR_BACKEND,
110            Error::Unsupported(_) => S::TRANSCRIBE_ERR_UNSUPPORTED_TASK,
111            Error::BadStructSize(_) => S::TRANSCRIBE_ERR_BAD_STRUCT_SIZE,
112            Error::InputTooLong(_) => S::TRANSCRIBE_ERR_INPUT_TOO_LONG,
113            Error::Aborted { .. } => S::TRANSCRIBE_ERR_ABORTED,
114            Error::OutputTruncated { .. } => S::TRANSCRIBE_ERR_OUTPUT_TRUNCATED,
115            Error::OutputRepetition { .. } => S::TRANSCRIBE_ERR_OUTPUT_REPETITION,
116            _ => S::TRANSCRIBE_OK,
117        };
118        s.0 as i32
119    }
120
121    /// The partial transcript carried by [`Error::Aborted`] /
122    /// [`Error::OutputTruncated`] / [`Error::OutputRepetition`], if any. `None`
123    /// for every other variant.
124    pub fn partial(&self) -> Option<&Transcript> {
125        match self {
126            Error::Aborted { partial, .. }
127            | Error::OutputTruncated { partial, .. }
128            | Error::OutputRepetition { partial, .. } => partial.as_deref(),
129            _ => None,
130        }
131    }
132}
133
134/// Human-readable text for a `transcribe_status` (from the C side).
135pub(crate) fn status_string(status: i32) -> String {
136    // Borrowed pointer into static storage; copy at the boundary.
137    let ptr = unsafe { sys::transcribe_status_string(status) };
138    if ptr.is_null() {
139        return format!("status {status}");
140    }
141    unsafe { std::ffi::CStr::from_ptr(ptr) }
142        .to_string_lossy()
143        .into_owned()
144}
145
146/// Build the mapped error for a non-OK status (without a partial transcript).
147/// The run path re-attaches partials for the two result-bearing statuses.
148pub(crate) fn error_for_status(status: sys::transcribe_status, context: &str) -> Error {
149    use sys::transcribe_status as S;
150    let code = status.0 as i32;
151    let msg = {
152        let s = status_string(code);
153        if context.is_empty() {
154            format!("{s} (status {code})")
155        } else {
156            format!("{context}: {s} (status {code})")
157        }
158    };
159    match status {
160        S::TRANSCRIBE_ERR_INVALID_ARG | S::TRANSCRIBE_ERR_SAMPLE_RATE => {
161            Error::InvalidArgument(msg)
162        }
163        S::TRANSCRIBE_ERR_NOT_IMPLEMENTED => Error::NotImplemented(msg),
164        S::TRANSCRIBE_ERR_FILE_NOT_FOUND => Error::ModelFileNotFound(msg),
165        S::TRANSCRIBE_ERR_GGUF
166        | S::TRANSCRIBE_ERR_UNSUPPORTED_ARCH
167        | S::TRANSCRIBE_ERR_UNSUPPORTED_VARIANT => Error::ModelLoad(msg),
168        S::TRANSCRIBE_ERR_OOM => Error::OutOfMemory(msg),
169        S::TRANSCRIBE_ERR_BACKEND => Error::Backend(msg),
170        S::TRANSCRIBE_ERR_UNSUPPORTED_LANGUAGE
171        | S::TRANSCRIBE_ERR_UNSUPPORTED_TASK
172        | S::TRANSCRIBE_ERR_UNSUPPORTED_TIMESTAMPS
173        | S::TRANSCRIBE_ERR_UNSUPPORTED_PNC
174        | S::TRANSCRIBE_ERR_UNSUPPORTED_ITN => Error::Unsupported(msg),
175        S::TRANSCRIBE_ERR_BAD_STRUCT_SIZE => Error::BadStructSize(msg),
176        S::TRANSCRIBE_ERR_INPUT_TOO_LONG => Error::InputTooLong(msg),
177        S::TRANSCRIBE_ERR_ABORTED => Error::Aborted {
178            message: msg,
179            partial: None,
180        },
181        S::TRANSCRIBE_ERR_OUTPUT_TRUNCATED => Error::OutputTruncated {
182            message: msg,
183            partial: None,
184        },
185        S::TRANSCRIBE_ERR_OUTPUT_REPETITION => Error::OutputRepetition {
186            message: msg,
187            partial: None,
188        },
189        _ => Error::Other(msg),
190    }
191}
192
193/// `Ok(())` for `TRANSCRIBE_OK`, otherwise the mapped error.
194pub(crate) fn check(status: sys::transcribe_status, context: &str) -> Result<()> {
195    if status == sys::transcribe_status::TRANSCRIBE_OK {
196        Ok(())
197    } else {
198        Err(error_for_status(status, context))
199    }
200}