1use crate::result::Transcript;
10use transcribe_cpp_sys as sys;
11
12pub type Result<T> = std::result::Result<T, Error>;
14
15#[derive(Debug, thiserror::Error)]
21#[non_exhaustive]
22pub enum Error {
23 #[error("invalid argument: {0}")]
25 InvalidArgument(String),
26 #[error("not implemented by this model: {0}")]
28 NotImplemented(String),
29 #[error("model file not found: {0}")]
31 ModelFileNotFound(String),
32 #[error("model load failed: {0}")]
34 ModelLoad(String),
35 #[error("out of memory: {0}")]
37 OutOfMemory(String),
38 #[error("backend error: {0}")]
40 Backend(String),
41 #[error("unsupported request: {0}")]
44 Unsupported(String),
45 #[error("ABI struct-size mismatch: {0}")]
48 BadStructSize(String),
49 #[error("input too long: {0}")]
51 InputTooLong(String),
52 #[error("operation aborted: {message}")]
55 Aborted {
56 message: String,
57 partial: Option<Box<Transcript>>,
59 },
60 #[error("output truncated before end-of-stream: {message}")]
64 OutputTruncated {
65 message: String,
66 partial: Option<Box<Transcript>>,
68 },
69 #[error("output began repeating before end-of-stream: {message}")]
73 OutputRepetition {
74 message: String,
75 partial: Option<Box<Transcript>>,
77 },
78 #[error("unsupported role: {0}")]
80 UnsupportedRole(String),
81 #[error("input too short: {0}")]
84 InputTooShort(String),
85 #[error("native library version mismatch: {0}")]
88 VersionMismatch(String),
89 #[error("string contains an interior NUL byte: {0}")]
91 Nul(#[from] std::ffi::NulError),
92 #[error("model busy: {0}")]
99 Busy(String),
100 #[error("{0}")]
102 Other(String),
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
111#[non_exhaustive]
112#[cfg_attr(
113 feature = "serde",
114 derive(serde::Serialize, serde::Deserialize),
115 serde(rename_all = "snake_case")
116)]
117pub enum ErrorKind {
118 InvalidArgument,
119 NotImplemented,
120 ModelFileNotFound,
121 ModelLoad,
122 OutOfMemory,
123 Backend,
124 Unsupported,
125 BadStructSize,
126 InputTooLong,
127 Aborted,
128 OutputTruncated,
129 OutputRepetition,
130 UnsupportedRole,
131 VersionMismatch,
132 Nul,
133 Busy,
134 InputTooShort,
135 #[cfg_attr(feature = "serde", serde(other))]
136 Other,
137}
138
139#[derive(Debug, Clone, PartialEq)]
154#[non_exhaustive]
155#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
156pub struct ErrorReport {
157 pub kind: ErrorKind,
159 pub message: String,
161 #[cfg_attr(feature = "serde", serde(default))]
163 pub status: i32,
164 #[cfg_attr(feature = "serde", serde(default))]
166 pub partial: Option<Transcript>,
167}
168
169impl From<&Error> for ErrorReport {
170 fn from(err: &Error) -> Self {
171 ErrorReport {
172 kind: err.kind(),
173 message: err.to_string(),
174 status: err.raw_status(),
175 partial: err.partial().cloned(),
176 }
177 }
178}
179
180impl From<Error> for ErrorReport {
182 fn from(err: Error) -> Self {
183 let (kind, message, status) = (err.kind(), err.to_string(), err.raw_status());
184 let partial = match err {
185 Error::Aborted { partial, .. }
186 | Error::OutputTruncated { partial, .. }
187 | Error::OutputRepetition { partial, .. } => partial.map(|p| *p),
188 _ => None,
189 };
190 ErrorReport {
191 kind,
192 message,
193 status,
194 partial,
195 }
196 }
197}
198
199#[cfg(feature = "serde")]
202impl serde::Serialize for Error {
203 fn serialize<S: serde::Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
204 #[derive(serde::Serialize)]
205 struct Report<'a> {
206 kind: ErrorKind,
207 message: String,
208 status: i32,
209 partial: Option<&'a Transcript>,
210 }
211 Report {
212 kind: self.kind(),
213 message: self.to_string(),
214 status: self.raw_status(),
215 partial: self.partial(),
216 }
217 .serialize(s)
218 }
219}
220
221impl Error {
222 pub fn kind(&self) -> ErrorKind {
224 match self {
226 Error::InvalidArgument(_) => ErrorKind::InvalidArgument,
227 Error::NotImplemented(_) => ErrorKind::NotImplemented,
228 Error::ModelFileNotFound(_) => ErrorKind::ModelFileNotFound,
229 Error::ModelLoad(_) => ErrorKind::ModelLoad,
230 Error::OutOfMemory(_) => ErrorKind::OutOfMemory,
231 Error::Backend(_) => ErrorKind::Backend,
232 Error::Unsupported(_) => ErrorKind::Unsupported,
233 Error::BadStructSize(_) => ErrorKind::BadStructSize,
234 Error::InputTooLong(_) => ErrorKind::InputTooLong,
235 Error::Aborted { .. } => ErrorKind::Aborted,
236 Error::OutputTruncated { .. } => ErrorKind::OutputTruncated,
237 Error::OutputRepetition { .. } => ErrorKind::OutputRepetition,
238 Error::UnsupportedRole(_) => ErrorKind::UnsupportedRole,
239 Error::InputTooShort(_) => ErrorKind::InputTooShort,
240 Error::VersionMismatch(_) => ErrorKind::VersionMismatch,
241 Error::Nul(_) => ErrorKind::Nul,
242 Error::Busy(_) => ErrorKind::Busy,
243 Error::Other(_) => ErrorKind::Other,
244 }
245 }
246
247 pub fn raw_status(&self) -> i32 {
250 use sys::transcribe_status as S;
251 let s = match self {
252 Error::InvalidArgument(_) => S::TRANSCRIBE_ERR_INVALID_ARG,
253 Error::NotImplemented(_) => S::TRANSCRIBE_ERR_NOT_IMPLEMENTED,
254 Error::ModelFileNotFound(_) => S::TRANSCRIBE_ERR_FILE_NOT_FOUND,
255 Error::ModelLoad(_) => S::TRANSCRIBE_ERR_GGUF,
256 Error::OutOfMemory(_) => S::TRANSCRIBE_ERR_OOM,
257 Error::Backend(_) => S::TRANSCRIBE_ERR_BACKEND,
258 Error::Unsupported(_) => S::TRANSCRIBE_ERR_UNSUPPORTED_TASK,
259 Error::BadStructSize(_) => S::TRANSCRIBE_ERR_BAD_STRUCT_SIZE,
260 Error::InputTooLong(_) => S::TRANSCRIBE_ERR_INPUT_TOO_LONG,
261 Error::Aborted { .. } => S::TRANSCRIBE_ERR_ABORTED,
262 Error::OutputTruncated { .. } => S::TRANSCRIBE_ERR_OUTPUT_TRUNCATED,
263 Error::OutputRepetition { .. } => S::TRANSCRIBE_ERR_OUTPUT_REPETITION,
264 Error::UnsupportedRole(_) => S::TRANSCRIBE_ERR_UNSUPPORTED_ROLE,
265 Error::InputTooShort(_) => S::TRANSCRIBE_ERR_INPUT_TOO_SHORT,
266 _ => S::TRANSCRIBE_OK,
267 };
268 s.0 as i32
269 }
270
271 pub fn partial(&self) -> Option<&Transcript> {
275 match self {
276 Error::Aborted { partial, .. }
277 | Error::OutputTruncated { partial, .. }
278 | Error::OutputRepetition { partial, .. } => partial.as_deref(),
279 _ => None,
280 }
281 }
282}
283
284pub(crate) fn status_string(status: i32) -> String {
286 let ptr = unsafe { sys::transcribe_status_string(status) };
288 if ptr.is_null() {
289 return format!("status {status}");
290 }
291 unsafe { std::ffi::CStr::from_ptr(ptr) }
292 .to_string_lossy()
293 .into_owned()
294}
295
296pub(crate) fn error_for_status(status: sys::transcribe_status, context: &str) -> Error {
299 use sys::transcribe_status as S;
300 let code = status.0 as i32;
301 let msg = {
302 let s = status_string(code);
303 if context.is_empty() {
304 format!("{s} (status {code})")
305 } else {
306 format!("{context}: {s} (status {code})")
307 }
308 };
309 match status {
310 S::TRANSCRIBE_ERR_INVALID_ARG | S::TRANSCRIBE_ERR_SAMPLE_RATE => {
311 Error::InvalidArgument(msg)
312 }
313 S::TRANSCRIBE_ERR_NOT_IMPLEMENTED => Error::NotImplemented(msg),
314 S::TRANSCRIBE_ERR_FILE_NOT_FOUND => Error::ModelFileNotFound(msg),
315 S::TRANSCRIBE_ERR_GGUF
316 | S::TRANSCRIBE_ERR_UNSUPPORTED_ARCH
317 | S::TRANSCRIBE_ERR_UNSUPPORTED_VARIANT => Error::ModelLoad(msg),
318 S::TRANSCRIBE_ERR_OOM => Error::OutOfMemory(msg),
319 S::TRANSCRIBE_ERR_BACKEND => Error::Backend(msg),
320 S::TRANSCRIBE_ERR_UNSUPPORTED_LANGUAGE
321 | S::TRANSCRIBE_ERR_UNSUPPORTED_TASK
322 | S::TRANSCRIBE_ERR_UNSUPPORTED_TIMESTAMPS
323 | S::TRANSCRIBE_ERR_UNSUPPORTED_PNC
324 | S::TRANSCRIBE_ERR_UNSUPPORTED_ITN => Error::Unsupported(msg),
325 S::TRANSCRIBE_ERR_BAD_STRUCT_SIZE => Error::BadStructSize(msg),
326 S::TRANSCRIBE_ERR_INPUT_TOO_LONG => Error::InputTooLong(msg),
327 S::TRANSCRIBE_ERR_ABORTED => Error::Aborted {
328 message: msg,
329 partial: None,
330 },
331 S::TRANSCRIBE_ERR_OUTPUT_TRUNCATED => Error::OutputTruncated {
332 message: msg,
333 partial: None,
334 },
335 S::TRANSCRIBE_ERR_OUTPUT_REPETITION => Error::OutputRepetition {
336 message: msg,
337 partial: None,
338 },
339 S::TRANSCRIBE_ERR_UNSUPPORTED_ROLE => Error::UnsupportedRole(msg),
340 S::TRANSCRIBE_ERR_INPUT_TOO_SHORT => Error::InputTooShort(msg),
341 _ => Error::Other(msg),
342 }
343}
344
345pub(crate) fn check(status: sys::transcribe_status, context: &str) -> Result<()> {
347 if status == sys::transcribe_status::TRANSCRIBE_OK {
348 Ok(())
349 } else {
350 Err(error_for_status(status, context))
351 }
352}
353
354#[cfg(test)]
355mod tests {
356 use super::*;
357 use std::mem::discriminant;
358 use sys::transcribe_status as S;
359
360 #[test]
361 fn every_status_maps_to_a_typed_variant() {
362 let unknown = status_string(-1);
366 let codes: Vec<S> = (1..256u32)
367 .take_while(|&c| status_string(c as i32) != unknown)
368 .map(S)
369 .collect();
370 assert!(
371 codes.len() >= S::TRANSCRIBE_ERR_UNSUPPORTED_ROLE.0 as usize,
372 "status walk stopped early at {} codes",
373 codes.len()
374 );
375 for status in codes {
376 let code = status.0;
377 let err = error_for_status(status, "ctx");
378 assert!(
379 !matches!(err, Error::Other(_)),
380 "status {code} falls through to Error::Other: {err:?}"
381 );
382 let raw = err.raw_status();
386 assert_ne!(raw, 0, "status {code} reports raw_status 0: {err:?}");
387 let back = error_for_status(S(raw as u32), "ctx");
388 assert_eq!(
389 discriminant(&back),
390 discriminant(&err),
391 "status {code} -> {err:?} -> raw {raw} -> {back:?}"
392 );
393 assert_eq!(back.kind(), err.kind(), "status {code}");
394 assert_ne!(err.kind(), ErrorKind::Other, "status {code}");
395 }
396 let role = error_for_status(S::TRANSCRIBE_ERR_UNSUPPORTED_ROLE, "ctx");
397 assert!(matches!(role, Error::UnsupportedRole(_)), "{role:?}");
398 }
399}