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    /// The loaded library's base version disagrees with the headers this crate
70    /// was generated against (the pre-1.0 version lock). Raised on first use.
71    #[error("native library version mismatch: {0}")]
72    VersionMismatch(String),
73    /// A caller-supplied string contained an interior NUL byte.
74    #[error("string contains an interior NUL byte: {0}")]
75    Nul(#[from] std::ffi::NulError),
76    /// Another session of the same model already has a compute operation in
77    /// flight. The C library serializes compute per model (the 0.x limitation):
78    /// at most one run / batch / active stream across ALL of a model's sessions
79    /// at a time. Finish or drop the in-flight stream (one-shot runs/batches
80    /// just queue) before starting another, or give each concurrent worker its
81    /// own model. Raised purely on the Rust side, so [`Error::raw_status`] is 0.
82    #[error("model busy: {0}")]
83    Busy(String),
84    /// A non-OK status with no more specific mapping.
85    #[error("{0}")]
86    Other(String),
87}
88
89impl Error {
90    /// The `transcribe_status` this error was mapped from, or `0` for errors
91    /// raised on the Rust side (version gate, NUL in a string, …).
92    pub fn raw_status(&self) -> i32 {
93        use sys::transcribe_status as S;
94        let s = match self {
95            Error::InvalidArgument(_) => S::TRANSCRIBE_ERR_INVALID_ARG,
96            Error::NotImplemented(_) => S::TRANSCRIBE_ERR_NOT_IMPLEMENTED,
97            Error::ModelFileNotFound(_) => S::TRANSCRIBE_ERR_FILE_NOT_FOUND,
98            Error::ModelLoad(_) => S::TRANSCRIBE_ERR_GGUF,
99            Error::OutOfMemory(_) => S::TRANSCRIBE_ERR_OOM,
100            Error::Backend(_) => S::TRANSCRIBE_ERR_BACKEND,
101            Error::Unsupported(_) => S::TRANSCRIBE_ERR_UNSUPPORTED_TASK,
102            Error::BadStructSize(_) => S::TRANSCRIBE_ERR_BAD_STRUCT_SIZE,
103            Error::InputTooLong(_) => S::TRANSCRIBE_ERR_INPUT_TOO_LONG,
104            Error::Aborted { .. } => S::TRANSCRIBE_ERR_ABORTED,
105            Error::OutputTruncated { .. } => S::TRANSCRIBE_ERR_OUTPUT_TRUNCATED,
106            _ => S::TRANSCRIBE_OK,
107        };
108        s.0 as i32
109    }
110
111    /// The partial transcript carried by [`Error::Aborted`] /
112    /// [`Error::OutputTruncated`], if any. `None` for every other variant.
113    pub fn partial(&self) -> Option<&Transcript> {
114        match self {
115            Error::Aborted { partial, .. } | Error::OutputTruncated { partial, .. } => {
116                partial.as_deref()
117            }
118            _ => None,
119        }
120    }
121}
122
123/// Human-readable text for a `transcribe_status` (from the C side).
124pub(crate) fn status_string(status: i32) -> String {
125    // Borrowed pointer into static storage; copy at the boundary.
126    let ptr = unsafe { sys::transcribe_status_string(status) };
127    if ptr.is_null() {
128        return format!("status {status}");
129    }
130    unsafe { std::ffi::CStr::from_ptr(ptr) }
131        .to_string_lossy()
132        .into_owned()
133}
134
135/// Build the mapped error for a non-OK status (without a partial transcript).
136/// The run path re-attaches partials for the two result-bearing statuses.
137pub(crate) fn error_for_status(status: sys::transcribe_status, context: &str) -> Error {
138    use sys::transcribe_status as S;
139    let code = status.0 as i32;
140    let msg = {
141        let s = status_string(code);
142        if context.is_empty() {
143            format!("{s} (status {code})")
144        } else {
145            format!("{context}: {s} (status {code})")
146        }
147    };
148    match status {
149        S::TRANSCRIBE_ERR_INVALID_ARG | S::TRANSCRIBE_ERR_SAMPLE_RATE => {
150            Error::InvalidArgument(msg)
151        }
152        S::TRANSCRIBE_ERR_NOT_IMPLEMENTED => Error::NotImplemented(msg),
153        S::TRANSCRIBE_ERR_FILE_NOT_FOUND => Error::ModelFileNotFound(msg),
154        S::TRANSCRIBE_ERR_GGUF
155        | S::TRANSCRIBE_ERR_UNSUPPORTED_ARCH
156        | S::TRANSCRIBE_ERR_UNSUPPORTED_VARIANT => Error::ModelLoad(msg),
157        S::TRANSCRIBE_ERR_OOM => Error::OutOfMemory(msg),
158        S::TRANSCRIBE_ERR_BACKEND => Error::Backend(msg),
159        S::TRANSCRIBE_ERR_UNSUPPORTED_LANGUAGE
160        | S::TRANSCRIBE_ERR_UNSUPPORTED_TASK
161        | S::TRANSCRIBE_ERR_UNSUPPORTED_TIMESTAMPS
162        | S::TRANSCRIBE_ERR_UNSUPPORTED_PNC
163        | S::TRANSCRIBE_ERR_UNSUPPORTED_ITN => Error::Unsupported(msg),
164        S::TRANSCRIBE_ERR_BAD_STRUCT_SIZE => Error::BadStructSize(msg),
165        S::TRANSCRIBE_ERR_INPUT_TOO_LONG => Error::InputTooLong(msg),
166        S::TRANSCRIBE_ERR_ABORTED => Error::Aborted {
167            message: msg,
168            partial: None,
169        },
170        S::TRANSCRIBE_ERR_OUTPUT_TRUNCATED => Error::OutputTruncated {
171            message: msg,
172            partial: None,
173        },
174        _ => Error::Other(msg),
175    }
176}
177
178/// `Ok(())` for `TRANSCRIBE_OK`, otherwise the mapped error.
179pub(crate) fn check(status: sys::transcribe_status, context: &str) -> Result<()> {
180    if status == sys::transcribe_status::TRANSCRIBE_OK {
181        Ok(())
182    } else {
183        Err(error_for_status(status, context))
184    }
185}