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}
87
88fn default_backend_kind() -> BackendKind {
89    BackendKind::Asr
90}
91fn default_true() -> bool {
92    true
93}
94
95impl TranscriptionResult {
96    pub fn local(
97        text: String,
98        segments: Vec<Segment>,
99        language: Option<String>,
100        model: String,
101        duration_secs: f64,
102    ) -> Self {
103        Self {
104            text,
105            segments,
106            language,
107            model,
108            provider: "local".into(),
109            duration_secs,
110            backend_kind: BackendKind::Asr,
111            timestamps_reliable: true,
112            cleanup_style: crate::cleanup::CleanupStyle::Raw,
113            cleanup_provider: None,
114            original_text: None,
115        }
116    }
117
118    pub fn openrouter(
119        text: String,
120        segments: Vec<Segment>,
121        language: Option<String>,
122        model: String,
123        duration_secs: f64,
124        _timestamps_requested: bool,
125    ) -> Self {
126        Self {
127            text,
128            segments,
129            language,
130            model,
131            provider: "openrouter".into(),
132            duration_secs,
133            backend_kind: BackendKind::LlmAssisted,
134            // LLM timestamps are never treated as reliable ASR timing.
135            timestamps_reliable: false,
136            cleanup_style: crate::cleanup::CleanupStyle::Raw,
137            cleanup_provider: None,
138            original_text: None,
139        }
140    }
141}
142
143/// Provider trait — the foundation for local and remote backends.
144#[async_trait]
145pub trait TranscriptionProvider: Send + Sync {
146    /// Human-readable provider name (e.g. `"local"`, `"openrouter"`).
147    fn name(&self) -> &'static str;
148
149    /// Backend classification.
150    fn backend_kind(&self) -> BackendKind;
151
152    /// Whether this provider can emit trustworthy media timestamps.
153    fn timestamps_reliable(&self) -> bool {
154        matches!(self.backend_kind(), BackendKind::Asr)
155    }
156
157    /// Transcribe audio according to `options`.
158    async fn transcribe(
159        &self,
160        input: &AudioInput,
161        options: &TranscriptionOptions,
162    ) -> Result<TranscriptionResult>;
163}
164
165pub use local::LocalWhisperProvider;
166pub use openrouter::OpenRouterProvider;