use crate::result::Transcript;
use transcribe_cpp_sys as sys;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error("invalid argument: {0}")]
InvalidArgument(String),
#[error("not implemented by this model: {0}")]
NotImplemented(String),
#[error("model file not found: {0}")]
ModelFileNotFound(String),
#[error("model load failed: {0}")]
ModelLoad(String),
#[error("out of memory: {0}")]
OutOfMemory(String),
#[error("backend error: {0}")]
Backend(String),
#[error("unsupported request: {0}")]
Unsupported(String),
#[error("ABI struct-size mismatch: {0}")]
BadStructSize(String),
#[error("input too long: {0}")]
InputTooLong(String),
#[error("operation aborted: {message}")]
Aborted {
message: String,
partial: Option<Box<Transcript>>,
},
#[error("output truncated before end-of-stream: {message}")]
OutputTruncated {
message: String,
partial: Option<Box<Transcript>>,
},
#[error("output began repeating before end-of-stream: {message}")]
OutputRepetition {
message: String,
partial: Option<Box<Transcript>>,
},
#[error("unsupported role: {0}")]
UnsupportedRole(String),
#[error("input too short: {0}")]
InputTooShort(String),
#[error("native library version mismatch: {0}")]
VersionMismatch(String),
#[error("string contains an interior NUL byte: {0}")]
Nul(#[from] std::ffi::NulError),
#[error("model busy: {0}")]
Busy(String),
#[error("{0}")]
Other(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(rename_all = "snake_case")
)]
pub enum ErrorKind {
InvalidArgument,
NotImplemented,
ModelFileNotFound,
ModelLoad,
OutOfMemory,
Backend,
Unsupported,
BadStructSize,
InputTooLong,
Aborted,
OutputTruncated,
OutputRepetition,
UnsupportedRole,
VersionMismatch,
Nul,
Busy,
InputTooShort,
#[cfg_attr(feature = "serde", serde(other))]
Other,
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ErrorReport {
pub kind: ErrorKind,
pub message: String,
#[cfg_attr(feature = "serde", serde(default))]
pub status: i32,
#[cfg_attr(feature = "serde", serde(default))]
pub partial: Option<Transcript>,
}
impl From<&Error> for ErrorReport {
fn from(err: &Error) -> Self {
ErrorReport {
kind: err.kind(),
message: err.to_string(),
status: err.raw_status(),
partial: err.partial().cloned(),
}
}
}
impl From<Error> for ErrorReport {
fn from(err: Error) -> Self {
let (kind, message, status) = (err.kind(), err.to_string(), err.raw_status());
let partial = match err {
Error::Aborted { partial, .. }
| Error::OutputTruncated { partial, .. }
| Error::OutputRepetition { partial, .. } => partial.map(|p| *p),
_ => None,
};
ErrorReport {
kind,
message,
status,
partial,
}
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for Error {
fn serialize<S: serde::Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
#[derive(serde::Serialize)]
struct Report<'a> {
kind: ErrorKind,
message: String,
status: i32,
partial: Option<&'a Transcript>,
}
Report {
kind: self.kind(),
message: self.to_string(),
status: self.raw_status(),
partial: self.partial(),
}
.serialize(s)
}
}
impl Error {
pub fn kind(&self) -> ErrorKind {
match self {
Error::InvalidArgument(_) => ErrorKind::InvalidArgument,
Error::NotImplemented(_) => ErrorKind::NotImplemented,
Error::ModelFileNotFound(_) => ErrorKind::ModelFileNotFound,
Error::ModelLoad(_) => ErrorKind::ModelLoad,
Error::OutOfMemory(_) => ErrorKind::OutOfMemory,
Error::Backend(_) => ErrorKind::Backend,
Error::Unsupported(_) => ErrorKind::Unsupported,
Error::BadStructSize(_) => ErrorKind::BadStructSize,
Error::InputTooLong(_) => ErrorKind::InputTooLong,
Error::Aborted { .. } => ErrorKind::Aborted,
Error::OutputTruncated { .. } => ErrorKind::OutputTruncated,
Error::OutputRepetition { .. } => ErrorKind::OutputRepetition,
Error::UnsupportedRole(_) => ErrorKind::UnsupportedRole,
Error::InputTooShort(_) => ErrorKind::InputTooShort,
Error::VersionMismatch(_) => ErrorKind::VersionMismatch,
Error::Nul(_) => ErrorKind::Nul,
Error::Busy(_) => ErrorKind::Busy,
Error::Other(_) => ErrorKind::Other,
}
}
pub fn raw_status(&self) -> i32 {
use sys::transcribe_status as S;
let s = match self {
Error::InvalidArgument(_) => S::TRANSCRIBE_ERR_INVALID_ARG,
Error::NotImplemented(_) => S::TRANSCRIBE_ERR_NOT_IMPLEMENTED,
Error::ModelFileNotFound(_) => S::TRANSCRIBE_ERR_FILE_NOT_FOUND,
Error::ModelLoad(_) => S::TRANSCRIBE_ERR_GGUF,
Error::OutOfMemory(_) => S::TRANSCRIBE_ERR_OOM,
Error::Backend(_) => S::TRANSCRIBE_ERR_BACKEND,
Error::Unsupported(_) => S::TRANSCRIBE_ERR_UNSUPPORTED_TASK,
Error::BadStructSize(_) => S::TRANSCRIBE_ERR_BAD_STRUCT_SIZE,
Error::InputTooLong(_) => S::TRANSCRIBE_ERR_INPUT_TOO_LONG,
Error::Aborted { .. } => S::TRANSCRIBE_ERR_ABORTED,
Error::OutputTruncated { .. } => S::TRANSCRIBE_ERR_OUTPUT_TRUNCATED,
Error::OutputRepetition { .. } => S::TRANSCRIBE_ERR_OUTPUT_REPETITION,
Error::UnsupportedRole(_) => S::TRANSCRIBE_ERR_UNSUPPORTED_ROLE,
Error::InputTooShort(_) => S::TRANSCRIBE_ERR_INPUT_TOO_SHORT,
_ => S::TRANSCRIBE_OK,
};
s.0 as i32
}
pub fn partial(&self) -> Option<&Transcript> {
match self {
Error::Aborted { partial, .. }
| Error::OutputTruncated { partial, .. }
| Error::OutputRepetition { partial, .. } => partial.as_deref(),
_ => None,
}
}
}
pub(crate) fn status_string(status: i32) -> String {
let ptr = unsafe { sys::transcribe_status_string(status) };
if ptr.is_null() {
return format!("status {status}");
}
unsafe { std::ffi::CStr::from_ptr(ptr) }
.to_string_lossy()
.into_owned()
}
pub(crate) fn error_for_status(status: sys::transcribe_status, context: &str) -> Error {
use sys::transcribe_status as S;
let code = status.0 as i32;
let msg = {
let s = status_string(code);
if context.is_empty() {
format!("{s} (status {code})")
} else {
format!("{context}: {s} (status {code})")
}
};
match status {
S::TRANSCRIBE_ERR_INVALID_ARG | S::TRANSCRIBE_ERR_SAMPLE_RATE => {
Error::InvalidArgument(msg)
}
S::TRANSCRIBE_ERR_NOT_IMPLEMENTED => Error::NotImplemented(msg),
S::TRANSCRIBE_ERR_FILE_NOT_FOUND => Error::ModelFileNotFound(msg),
S::TRANSCRIBE_ERR_GGUF
| S::TRANSCRIBE_ERR_UNSUPPORTED_ARCH
| S::TRANSCRIBE_ERR_UNSUPPORTED_VARIANT => Error::ModelLoad(msg),
S::TRANSCRIBE_ERR_OOM => Error::OutOfMemory(msg),
S::TRANSCRIBE_ERR_BACKEND => Error::Backend(msg),
S::TRANSCRIBE_ERR_UNSUPPORTED_LANGUAGE
| S::TRANSCRIBE_ERR_UNSUPPORTED_TASK
| S::TRANSCRIBE_ERR_UNSUPPORTED_TIMESTAMPS
| S::TRANSCRIBE_ERR_UNSUPPORTED_PNC
| S::TRANSCRIBE_ERR_UNSUPPORTED_ITN => Error::Unsupported(msg),
S::TRANSCRIBE_ERR_BAD_STRUCT_SIZE => Error::BadStructSize(msg),
S::TRANSCRIBE_ERR_INPUT_TOO_LONG => Error::InputTooLong(msg),
S::TRANSCRIBE_ERR_ABORTED => Error::Aborted {
message: msg,
partial: None,
},
S::TRANSCRIBE_ERR_OUTPUT_TRUNCATED => Error::OutputTruncated {
message: msg,
partial: None,
},
S::TRANSCRIBE_ERR_OUTPUT_REPETITION => Error::OutputRepetition {
message: msg,
partial: None,
},
S::TRANSCRIBE_ERR_UNSUPPORTED_ROLE => Error::UnsupportedRole(msg),
S::TRANSCRIBE_ERR_INPUT_TOO_SHORT => Error::InputTooShort(msg),
_ => Error::Other(msg),
}
}
pub(crate) fn check(status: sys::transcribe_status, context: &str) -> Result<()> {
if status == sys::transcribe_status::TRANSCRIBE_OK {
Ok(())
} else {
Err(error_for_status(status, context))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::mem::discriminant;
use sys::transcribe_status as S;
#[test]
fn every_status_maps_to_a_typed_variant() {
let unknown = status_string(-1);
let codes: Vec<S> = (1..256u32)
.take_while(|&c| status_string(c as i32) != unknown)
.map(S)
.collect();
assert!(
codes.len() >= S::TRANSCRIBE_ERR_UNSUPPORTED_ROLE.0 as usize,
"status walk stopped early at {} codes",
codes.len()
);
for status in codes {
let code = status.0;
let err = error_for_status(status, "ctx");
assert!(
!matches!(err, Error::Other(_)),
"status {code} falls through to Error::Other: {err:?}"
);
let raw = err.raw_status();
assert_ne!(raw, 0, "status {code} reports raw_status 0: {err:?}");
let back = error_for_status(S(raw as u32), "ctx");
assert_eq!(
discriminant(&back),
discriminant(&err),
"status {code} -> {err:?} -> raw {raw} -> {back:?}"
);
assert_eq!(back.kind(), err.kind(), "status {code}");
assert_ne!(err.kind(), ErrorKind::Other, "status {code}");
}
let role = error_for_status(S::TRANSCRIBE_ERR_UNSUPPORTED_ROLE, "ctx");
assert!(matches!(role, Error::UnsupportedRole(_)), "{role:?}");
}
}