1use thiserror::Error;
10
11#[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 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 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
184pub type Result<T> = std::result::Result<T, TranscriptionError>;