Skip to main content

aurum_core/
error.rs

1//! Error taxonomy for Aurum.
2//!
3//! Errors are grouped so callers (and the CLI) can present actionable messages:
4//! - [`TranscriptionError::User`] — bad input, missing key, invalid model name
5//! - [`TranscriptionError::Environment`] — missing ffmpeg, disk full, cache issues
6//! - [`TranscriptionError::Provider`] — network, rate limit, model load failure
7//! - [`TranscriptionError::Internal`] — unexpected bugs
8
9use thiserror::Error;
10
11/// Top-level error type used across the core library and CLI.
12#[derive(Debug, Error)]
13pub enum TranscriptionError {
14    #[error("{0}")]
15    User(#[from] UserError),
16
17    #[error("{0}")]
18    Environment(#[from] EnvironmentError),
19
20    #[error("{0}")]
21    Provider(#[from] ProviderError),
22
23    #[error("internal error: {0}")]
24    Internal(String),
25}
26
27#[derive(Debug, Error)]
28pub enum UserError {
29    #[error("audio file not found: {path}\n  Hint: check the path and try again.")]
30    FileNotFound { path: String },
31
32    #[error("invalid audio file: {reason}\n  Hint: ensure the file is a supported audio format (mp3, m4a, wav, flac, ogg, …).")]
33    InvalidAudio { reason: String },
34
35    #[error(
36        "audio is too long ({duration_secs:.1}s); maximum accepted is {max_secs:.0}s.\n  \
37         Hint: split the file, or raise the limit once longer-form support lands."
38    )]
39    AudioTooLong { duration_secs: f64, max_secs: f64 },
40
41    #[error(
42        "decoded audio is too large ({decoded_bytes} bytes); maximum is {max_bytes} bytes.\n  \
43         Hint: split the file into shorter clips."
44    )]
45    AudioTooLarge {
46        decoded_bytes: usize,
47        max_bytes: usize,
48    },
49
50    #[error("unsupported output format: {format}\n  Hint: use one of: txt, srt, json.")]
51    InvalidOutputFormat { format: String },
52
53    #[error("unknown provider: {provider}\n  Hint: use one of: local, openrouter.")]
54    InvalidProvider { provider: String },
55
56    #[error("unknown local model: {model}\n  Hint: available models: {available}")]
57    InvalidModel { model: String, available: String },
58
59    #[error(
60        "model '{model}' is not cached and downloads are disabled (local_only).\n  \
61         Hint: download it once while online, or pass local_only=false."
62    )]
63    ModelNotCached { model: String },
64
65    #[error(
66        "unsupported PCM sample rate {got} Hz (need {need}).\n  \
67         Hint: resample to {need} Hz mono f32 before calling from_pcm."
68    )]
69    UnsupportedSampleRate { got: u32, need: u32 },
70
71    #[error(
72        "OpenRouter API key is missing.\n  \
73         Set OPENROUTER_API_KEY in your environment, or add api_key under [openrouter] in the config file.\n  \
74         Get a key at https://openrouter.ai/keys"
75    )]
76    MissingApiKey,
77
78    #[error("invalid configuration: {reason}")]
79    InvalidConfig { reason: String },
80
81    #[error("{message}")]
82    Other { message: String },
83}
84
85#[derive(Debug, Error)]
86pub enum EnvironmentError {
87    #[error(
88        "ffmpeg is required but was not found on PATH.\n  \
89         Install it, then retry:\n  \
90         • macOS:   brew install ffmpeg\n  \
91         • Ubuntu:  sudo apt install ffmpeg\n  \
92         • Windows: winget install ffmpeg\n  \
93         • Or see:  https://ffmpeg.org/download.html"
94    )]
95    FfmpegMissing,
96
97    #[error("ffmpeg failed: {reason}")]
98    FfmpegFailed { reason: String },
99
100    #[error("insufficient disk space while writing {path}: {reason}")]
101    DiskSpace { path: String, reason: String },
102
103    #[error("failed to access cache/config directory {path}: {reason}")]
104    DirectoryAccess { path: String, reason: String },
105
106    #[error("I/O error: {0}")]
107    Io(#[from] std::io::Error),
108
109    #[error("{message}")]
110    Other { message: String },
111}
112
113#[derive(Debug, Error)]
114pub enum ProviderError {
115    #[error("failed to load model '{model}': {reason}")]
116    ModelLoad { model: String, reason: String },
117
118    #[error("failed to download model '{model}': {reason}")]
119    ModelDownload { model: String, reason: String },
120
121    #[error("transcription failed: {reason}")]
122    TranscriptionFailed { reason: String },
123
124    #[error("transcription cancelled")]
125    Cancelled,
126
127    #[error("network error talking to {provider}: {reason}")]
128    Network { provider: String, reason: String },
129
130    #[error(
131        "rate limited by {provider}.\n  \
132         Wait a moment and retry. If this persists, check your plan/quota."
133    )]
134    RateLimited { provider: String },
135
136    #[error(
137        "quota or billing issue with {provider}: {reason}\n  \
138         Check your account balance and plan limits."
139    )]
140    QuotaExceeded { provider: String, reason: String },
141
142    #[error("authentication failed for {provider}: {reason}")]
143    Auth { provider: String, reason: String },
144
145    #[error("{provider} returned an error: {reason}")]
146    Remote { provider: String, reason: String },
147
148    #[error("{message}")]
149    Other { message: String },
150}
151
152impl TranscriptionError {
153    /// Short category label for logging / exit-code mapping.
154    pub fn category(&self) -> &'static str {
155        match self {
156            Self::User(_) => "user",
157            Self::Environment(_) => "environment",
158            Self::Provider(_) => "provider",
159            Self::Internal(_) => "internal",
160        }
161    }
162
163    /// Suggested process exit code.
164    pub fn exit_code(&self) -> i32 {
165        match self {
166            Self::User(_) => 2,
167            Self::Environment(_) => 3,
168            Self::Provider(_) => 4,
169            Self::Internal(_) => 1,
170        }
171    }
172
173    pub fn internal(msg: impl Into<String>) -> Self {
174        Self::Internal(msg.into())
175    }
176}
177
178impl From<std::io::Error> for TranscriptionError {
179    fn from(value: std::io::Error) -> Self {
180        Self::Environment(EnvironmentError::Io(value))
181    }
182}
183
184/// Result alias used throughout the crate.
185pub type Result<T> = std::result::Result<T, TranscriptionError>;