1pub mod local;
4pub mod openrouter;
5
6use crate::audio::AudioInput;
7use crate::error::Result;
8use async_trait::async_trait;
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "snake_case")]
14pub enum BackendKind {
15 Asr,
17 LlmAssisted,
19}
20
21#[derive(Debug, Clone)]
23pub struct TranscriptionOptions {
24 pub model: String,
26 pub language: String,
28 pub timestamps: bool,
30 pub cancel: Option<crate::cancel::CancelFlag>,
32}
33
34impl Default for TranscriptionOptions {
35 fn default() -> Self {
36 Self {
37 model: crate::config::DEFAULT_LOCAL_MODEL.to_string(),
38 language: crate::config::DEFAULT_LANGUAGE.to_string(),
39 timestamps: false,
40 cancel: None,
41 }
42 }
43}
44
45impl TranscriptionOptions {
46 pub fn with_cancel(mut self, flag: crate::cancel::CancelFlag) -> Self {
47 self.cancel = Some(flag);
48 self
49 }
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
57pub struct Segment {
58 pub start: f64,
60 pub end: f64,
62 pub text: String,
63}
64
65impl Segment {
66 pub fn try_new(start: f64, end: f64, text: impl Into<String>) -> Result<Self> {
68 let s = Self {
69 start,
70 end,
71 text: text.into(),
72 };
73 s.validate()?;
74 Ok(s)
75 }
76
77 pub fn validate(&self) -> Result<()> {
79 if !self.start.is_finite() || !self.end.is_finite() {
80 return Err(crate::error::UserError::Other {
81 message: format!(
82 "segment timestamps must be finite (start={}, end={})",
83 self.start, self.end
84 ),
85 }
86 .into());
87 }
88 if self.start < 0.0 || self.end < 0.0 {
89 return Err(crate::error::UserError::Other {
90 message: format!(
91 "segment timestamps must be non-negative (start={}, end={})",
92 self.start, self.end
93 ),
94 }
95 .into());
96 }
97 if self.end < self.start {
98 return Err(crate::error::UserError::Other {
99 message: format!(
100 "segment end before start (start={}, end={})",
101 self.start, self.end
102 ),
103 }
104 .into());
105 }
106 Ok(())
107 }
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct TranscriptionResult {
116 pub text: String,
117 pub segments: Vec<Segment>,
118 pub language: Option<String>,
119 pub model: String,
120 pub provider: String,
121 pub duration_secs: f64,
122 #[serde(default = "default_backend_kind")]
124 pub backend_kind: BackendKind,
125 #[serde(default = "default_true")]
127 pub timestamps_reliable: bool,
128 #[serde(default)]
130 pub cleanup_style: crate::cleanup::CleanupStyle,
131 #[serde(default, skip_serializing_if = "Option::is_none")]
133 pub cleanup_provider: Option<crate::cleanup::CleanupProviderKind>,
134 #[serde(default, skip_serializing_if = "Option::is_none")]
136 pub original_text: Option<String>,
137 #[serde(default, skip_serializing_if = "Option::is_none")]
139 pub original_segments: Option<Vec<Segment>>,
140 #[serde(default, skip_serializing_if = "Option::is_none")]
142 pub cleanup_segment_policy: Option<crate::cleanup::SegmentCleanupPolicy>,
143}
144
145fn default_backend_kind() -> BackendKind {
146 BackendKind::Asr
147}
148fn default_true() -> bool {
149 true
150}
151
152impl TranscriptionResult {
153 pub fn validate_segments(&self) -> Result<()> {
155 for (i, seg) in self.segments.iter().enumerate() {
156 if let Err(e) = seg.validate() {
157 return Err(crate::error::UserError::Other {
158 message: format!("segment[{i}]: {e}"),
159 }
160 .into());
161 }
162 }
163 if !self.duration_secs.is_finite() || self.duration_secs < 0.0 {
164 return Err(crate::error::UserError::Other {
165 message: format!(
166 "duration_secs must be finite and non-negative (got {})",
167 self.duration_secs
168 ),
169 }
170 .into());
171 }
172 Ok(())
173 }
174
175 pub fn local(
176 text: String,
177 segments: Vec<Segment>,
178 language: Option<String>,
179 model: String,
180 duration_secs: f64,
181 ) -> Self {
182 Self {
183 text,
184 segments,
185 language,
186 model,
187 provider: "local".into(),
188 duration_secs,
189 backend_kind: BackendKind::Asr,
190 timestamps_reliable: true,
191 cleanup_style: crate::cleanup::CleanupStyle::Raw,
192 cleanup_provider: None,
193 original_text: None,
194 original_segments: None,
195 cleanup_segment_policy: None,
196 }
197 }
198
199 pub fn openrouter(
200 text: String,
201 segments: Vec<Segment>,
202 language: Option<String>,
203 model: String,
204 duration_secs: f64,
205 _timestamps_requested: bool,
206 ) -> Self {
207 Self {
208 text,
209 segments,
210 language,
211 model,
212 provider: "openrouter".into(),
213 duration_secs,
214 backend_kind: BackendKind::LlmAssisted,
215 timestamps_reliable: false,
217 cleanup_style: crate::cleanup::CleanupStyle::Raw,
218 cleanup_provider: None,
219 original_text: None,
220 original_segments: None,
221 cleanup_segment_policy: None,
222 }
223 }
224}
225
226#[async_trait]
228pub trait TranscriptionProvider: Send + Sync {
229 fn name(&self) -> &'static str;
231
232 fn backend_kind(&self) -> BackendKind;
234
235 fn timestamps_reliable(&self) -> bool {
237 matches!(self.backend_kind(), BackendKind::Asr)
238 }
239
240 async fn transcribe(
242 &self,
243 input: &AudioInput,
244 options: &TranscriptionOptions,
245 ) -> Result<TranscriptionResult>;
246}
247
248pub use local::LocalWhisperProvider;
249pub use openrouter::{OpenRouterProvider, OpenRouterSttMode, SttPath};
250
251#[cfg(test)]
252mod tests {
253 use super::*;
254
255 #[test]
256 fn segment_try_new_accepts_valid() {
257 let s = Segment::try_new(0.0, 1.5, "hello").unwrap();
258 assert_eq!(s.start, 0.0);
259 assert_eq!(s.end, 1.5);
260 assert_eq!(s.text, "hello");
261 }
262
263 #[test]
264 fn segment_try_new_rejects_nan() {
265 assert!(Segment::try_new(f64::NAN, 1.0, "x").is_err());
266 assert!(Segment::try_new(0.0, f64::INFINITY, "x").is_err());
267 }
268
269 #[test]
270 fn segment_try_new_rejects_negative_and_inverted() {
271 assert!(Segment::try_new(-0.1, 1.0, "x").is_err());
272 assert!(Segment::try_new(2.0, 1.0, "x").is_err());
273 }
274
275 #[test]
276 fn segment_validate_ok_on_zero_length() {
277 Segment::try_new(1.0, 1.0, "").unwrap();
279 }
280}