Skip to main content

aurum_core/providers/
mod.rs

1//! Transcription provider abstraction.
2
3pub 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/// How a backend produces transcripts — affects timestamp trust and UX.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "snake_case")]
14pub enum BackendKind {
15    /// Dedicated ASR (e.g. whisper.cpp). Timestamps are engine-derived.
16    Asr,
17    /// Multimodal LLM asked to transcribe. Text may paraphrase; timestamps are unreliable.
18    LlmAssisted,
19}
20
21/// Options controlling a single transcription request.
22#[derive(Debug, Clone)]
23pub struct TranscriptionOptions {
24    /// Model name (local ggml name or remote model id).
25    pub model: String,
26    /// BCP-47 / ISO language code, or `"auto"`.
27    pub language: String,
28    /// Request segment-level timestamps when the provider supports them.
29    pub timestamps: bool,
30    /// Optional cooperative cancel flag (honoured by local whisper decode).
31    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/// A single timed segment of transcript text.
53#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
54pub struct Segment {
55    /// Start time in seconds.
56    pub start: f64,
57    /// End time in seconds.
58    pub end: f64,
59    pub text: String,
60}
61
62/// Normalized result returned by every provider.
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct TranscriptionResult {
65    pub text: String,
66    pub segments: Vec<Segment>,
67    pub language: Option<String>,
68    pub model: String,
69    pub provider: String,
70    pub duration_secs: f64,
71    /// Backend class — consumers should treat LLM timestamps as best-effort.
72    #[serde(default = "default_backend_kind")]
73    pub backend_kind: BackendKind,
74    /// Whether segment timestamps are considered reliable.
75    #[serde(default = "default_true")]
76    pub timestamps_reliable: bool,
77    /// Post-ASR cleanup style applied to [`Self::text`] (default: raw).
78    #[serde(default)]
79    pub cleanup_style: crate::cleanup::CleanupStyle,
80    /// Cleanup backend used, if any cleanup beyond raw was applied.
81    #[serde(default, skip_serializing_if = "Option::is_none")]
82    pub cleanup_provider: Option<crate::cleanup::CleanupProviderKind>,
83    /// Pre-cleanup ASR text when cleanup rewrote [`Self::text`].
84    #[serde(default, skip_serializing_if = "Option::is_none")]
85    pub original_text: Option<String>,
86    /// Pre-cleanup ASR segments when cleanup rewrote or cleared timings.
87    #[serde(default, skip_serializing_if = "Option::is_none")]
88    pub original_segments: Option<Vec<Segment>>,
89    /// Segment policy that was applied during cleanup (when not raw).
90    #[serde(default, skip_serializing_if = "Option::is_none")]
91    pub cleanup_segment_policy: Option<crate::cleanup::SegmentCleanupPolicy>,
92}
93
94fn default_backend_kind() -> BackendKind {
95    BackendKind::Asr
96}
97fn default_true() -> bool {
98    true
99}
100
101impl TranscriptionResult {
102    pub fn local(
103        text: String,
104        segments: Vec<Segment>,
105        language: Option<String>,
106        model: String,
107        duration_secs: f64,
108    ) -> Self {
109        Self {
110            text,
111            segments,
112            language,
113            model,
114            provider: "local".into(),
115            duration_secs,
116            backend_kind: BackendKind::Asr,
117            timestamps_reliable: true,
118            cleanup_style: crate::cleanup::CleanupStyle::Raw,
119            cleanup_provider: None,
120            original_text: None,
121            original_segments: None,
122            cleanup_segment_policy: None,
123        }
124    }
125
126    pub fn openrouter(
127        text: String,
128        segments: Vec<Segment>,
129        language: Option<String>,
130        model: String,
131        duration_secs: f64,
132        _timestamps_requested: bool,
133    ) -> Self {
134        Self {
135            text,
136            segments,
137            language,
138            model,
139            provider: "openrouter".into(),
140            duration_secs,
141            backend_kind: BackendKind::LlmAssisted,
142            // LLM timestamps are never treated as reliable ASR timing.
143            timestamps_reliable: false,
144            cleanup_style: crate::cleanup::CleanupStyle::Raw,
145            cleanup_provider: None,
146            original_text: None,
147            original_segments: None,
148            cleanup_segment_policy: None,
149        }
150    }
151}
152
153/// Provider trait — the foundation for local and remote backends.
154#[async_trait]
155pub trait TranscriptionProvider: Send + Sync {
156    /// Human-readable provider name (e.g. `"local"`, `"openrouter"`).
157    fn name(&self) -> &'static str;
158
159    /// Backend classification.
160    fn backend_kind(&self) -> BackendKind;
161
162    /// Whether this provider can emit trustworthy media timestamps.
163    fn timestamps_reliable(&self) -> bool {
164        matches!(self.backend_kind(), BackendKind::Asr)
165    }
166
167    /// Transcribe audio according to `options`.
168    async fn transcribe(
169        &self,
170        input: &AudioInput,
171        options: &TranscriptionOptions,
172    ) -> Result<TranscriptionResult>;
173}
174
175pub use local::LocalWhisperProvider;
176pub use openrouter::{OpenRouterProvider, OpenRouterSttMode, SttPath};