use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ErrorCategory {
InvalidInput,
UnsupportedCapability,
Cancelled,
DeadlineExceeded,
BusyOverloaded,
ModelUnavailable,
ArtifactIntegrity,
Network,
Auth,
RateLimit,
Quota,
Filesystem,
DiskFull,
Subprocess,
NativeInference,
Internal,
User,
Environment,
Provider,
}
impl ErrorCategory {
pub fn as_str(self) -> &'static str {
match self {
Self::InvalidInput => "invalid_input",
Self::UnsupportedCapability => "unsupported_capability",
Self::Cancelled => "cancelled",
Self::DeadlineExceeded => "deadline_exceeded",
Self::BusyOverloaded => "busy_overloaded",
Self::ModelUnavailable => "model_unavailable",
Self::ArtifactIntegrity => "artifact_integrity",
Self::Network => "network",
Self::Auth => "auth",
Self::RateLimit => "rate_limit",
Self::Quota => "quota",
Self::Filesystem => "filesystem",
Self::DiskFull => "disk_full",
Self::Subprocess => "subprocess",
Self::NativeInference => "native_inference",
Self::Internal => "internal",
Self::User => "user",
Self::Environment => "environment",
Self::Provider => "provider",
}
}
}
#[derive(Debug, Error)]
pub enum TranscriptionError {
#[error("{0}")]
User(#[from] UserError),
#[error("{0}")]
Environment(#[from] EnvironmentError),
#[error("{0}")]
Provider(#[from] ProviderError),
#[error("internal error: {0}")]
Internal(String),
}
pub type AurumError = TranscriptionError;
#[derive(Debug, Error)]
pub enum UserError {
#[error("audio file not found: {path}\n Hint: check the path and try again.")]
FileNotFound { path: String },
#[error("invalid audio file: {reason}\n Hint: ensure the file is a supported audio format (mp3, m4a, wav, flac, ogg, …).")]
InvalidAudio { reason: String },
#[error(
"audio is too long ({duration_secs:.1}s); maximum accepted is {max_secs:.0}s.\n \
Hint: split the file, or raise the limit once longer-form support lands."
)]
AudioTooLong { duration_secs: f64, max_secs: f64 },
#[error(
"decoded audio is too large ({decoded_bytes} bytes); maximum is {max_bytes} bytes.\n \
Hint: split the file into shorter clips."
)]
AudioTooLarge {
decoded_bytes: usize,
max_bytes: usize,
},
#[error("unsupported output format: {format}\n Hint: use one of: txt, srt, json.")]
InvalidOutputFormat { format: String },
#[error("unknown provider: {provider}\n Hint: use one of: local, openrouter.")]
InvalidProvider { provider: String },
#[error("unknown local model: {model}\n Hint: available models: {available}")]
InvalidModel { model: String, available: String },
#[error(
"model '{model}' is not cached and downloads are disabled (local_only).\n \
Hint: download it once while online, or pass local_only=false."
)]
ModelNotCached { model: String },
#[error(
"unsupported PCM sample rate {got} Hz (need {need}).\n \
Hint: resample to {need} Hz mono f32 before calling from_pcm."
)]
UnsupportedSampleRate { got: u32, need: u32 },
#[error(
"OpenRouter API key is missing.\n \
Set OPENROUTER_API_KEY in your environment, or add api_key under [openrouter] in the config file.\n \
Get a key at https://openrouter.ai/keys"
)]
MissingApiKey,
#[error("invalid configuration: {reason}")]
InvalidConfig { reason: String },
#[error("unsupported capability for {provider}/{model}: {reason}\n Hint: {hint}")]
UnsupportedCapability {
provider: String,
model: String,
reason: String,
hint: String,
},
#[error("{message}")]
Other { message: String },
}
#[derive(Debug, Error)]
pub enum EnvironmentError {
#[error(
"ffmpeg is required but was not found on PATH.\n \
Install it, then retry:\n \
• macOS: brew install ffmpeg\n \
• Ubuntu: sudo apt install ffmpeg\n \
• Windows: winget install ffmpeg\n \
• Or see: https://ffmpeg.org/download.html"
)]
FfmpegMissing,
#[error("ffmpeg failed: {reason}")]
FfmpegFailed { reason: String },
#[error("insufficient disk space while writing {path}: {reason}")]
DiskSpace { path: String, reason: String },
#[error("failed to access cache/config directory {path}: {reason}")]
DirectoryAccess { path: String, reason: String },
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("{message}")]
Other { message: String },
}
#[derive(Debug, Error)]
pub enum ProviderError {
#[error("failed to load model '{model}': {reason}")]
ModelLoad { model: String, reason: String },
#[error("failed to download model '{model}': {reason}")]
ModelDownload { model: String, reason: String },
#[error("transcription failed: {reason}")]
TranscriptionFailed { reason: String },
#[error("transcription cancelled")]
Cancelled,
#[error("operation deadline exceeded")]
DeadlineExceeded,
#[error("resource overload: {reason}")]
Overload { reason: String },
#[error("network error talking to {provider}: {reason}")]
Network { provider: String, reason: String },
#[error(
"rate limited by {provider}.\n \
Wait a moment and retry. If this persists, check your plan/quota."
)]
RateLimited { provider: String },
#[error(
"quota or billing issue with {provider}: {reason}\n \
Check your account balance and plan limits."
)]
QuotaExceeded { provider: String, reason: String },
#[error("authentication failed for {provider}: {reason}")]
Auth { provider: String, reason: String },
#[error("{provider} returned an error: {reason}")]
Remote { provider: String, reason: String },
#[error("{provider} response too large: {reason}")]
ResponseTooLarge { provider: String, reason: String },
#[error("{provider} returned an invalid payload: {reason}")]
InvalidProviderPayload { provider: String, reason: String },
#[error("limit exceeded: {reason}")]
LimitExceeded { reason: String },
#[error("{message}")]
Other { message: String },
}
impl TranscriptionError {
pub fn category(&self) -> &'static str {
match self {
Self::User(_) => "user",
Self::Environment(_) => "environment",
Self::Provider(_) => "provider",
Self::Internal(_) => "internal",
}
}
pub fn error_category(&self) -> ErrorCategory {
match self {
Self::User(u) => match u {
UserError::UnsupportedCapability { .. } => ErrorCategory::UnsupportedCapability,
UserError::ModelNotCached { .. } | UserError::InvalidModel { .. } => {
ErrorCategory::ModelUnavailable
}
UserError::MissingApiKey => ErrorCategory::Auth,
UserError::InvalidConfig { .. }
| UserError::InvalidProvider { .. }
| UserError::InvalidOutputFormat { .. }
| UserError::FileNotFound { .. }
| UserError::InvalidAudio { .. }
| UserError::AudioTooLong { .. }
| UserError::AudioTooLarge { .. }
| UserError::UnsupportedSampleRate { .. }
| UserError::Other { .. } => ErrorCategory::InvalidInput,
},
Self::Environment(e) => match e {
EnvironmentError::DiskSpace { .. } => ErrorCategory::DiskFull,
EnvironmentError::FfmpegMissing | EnvironmentError::FfmpegFailed { .. } => {
ErrorCategory::Subprocess
}
EnvironmentError::DirectoryAccess { .. } | EnvironmentError::Io(_) => {
ErrorCategory::Filesystem
}
EnvironmentError::Other { .. } => ErrorCategory::Environment,
},
Self::Provider(p) => match p {
ProviderError::Cancelled => ErrorCategory::Cancelled,
ProviderError::DeadlineExceeded => ErrorCategory::DeadlineExceeded,
ProviderError::Overload { .. } => ErrorCategory::BusyOverloaded,
ProviderError::Network { .. } => ErrorCategory::Network,
ProviderError::Auth { .. } => ErrorCategory::Auth,
ProviderError::RateLimited { .. } => ErrorCategory::RateLimit,
ProviderError::QuotaExceeded { .. } => ErrorCategory::Quota,
ProviderError::ModelLoad { .. } | ProviderError::ModelDownload { .. } => {
ErrorCategory::ModelUnavailable
}
ProviderError::TranscriptionFailed { .. } => ErrorCategory::NativeInference,
ProviderError::InvalidProviderPayload { .. }
| ProviderError::ResponseTooLarge { .. }
| ProviderError::LimitExceeded { .. }
| ProviderError::Remote { .. }
| ProviderError::Other { .. } => ErrorCategory::Provider,
},
Self::Internal(_) => ErrorCategory::Internal,
}
}
pub fn retryable(&self) -> bool {
matches!(
self.error_category(),
ErrorCategory::Network
| ErrorCategory::RateLimit
| ErrorCategory::BusyOverloaded
| ErrorCategory::DeadlineExceeded
)
}
pub fn exit_code(&self) -> i32 {
match self.error_category() {
ErrorCategory::Internal => 1,
ErrorCategory::Cancelled => 5,
ErrorCategory::DeadlineExceeded => 6,
ErrorCategory::BusyOverloaded => 7,
ErrorCategory::User
| ErrorCategory::InvalidInput
| ErrorCategory::UnsupportedCapability
| ErrorCategory::Auth
| ErrorCategory::ModelUnavailable
| ErrorCategory::ArtifactIntegrity => 2,
ErrorCategory::Environment
| ErrorCategory::Filesystem
| ErrorCategory::DiskFull
| ErrorCategory::Subprocess => 3,
ErrorCategory::Provider
| ErrorCategory::Network
| ErrorCategory::RateLimit
| ErrorCategory::Quota
| ErrorCategory::NativeInference => 4,
}
}
pub fn internal(msg: impl Into<String>) -> Self {
Self::Internal(msg.into())
}
}
impl From<std::io::Error> for TranscriptionError {
fn from(value: std::io::Error) -> Self {
Self::Environment(EnvironmentError::Io(value))
}
}
pub type Result<T> = std::result::Result<T, TranscriptionError>;