//! `TranscriptionProvider` is the async-native trait every transcription //! backend implements, regardless of whether the work happens locally //! (Whisper, Parakeet, Moonshine via the `LocalProviderAdapter` in //! `magnotia-transcription`) or remotely (OpenAI Whisper, Groq, //! Deepgram, etc.). //! //! Living in `magnotia-cloud-providers` is deliberate: the AGPL OEM //! exception (≥£2k/yr) requires a clean trait surface that an OEM //! licensee can implement without depending on Wyrdnote's transcription //! internals. The trait crate stays small; provider implementations //! sit alongside. //! //! Object-safety discipline: no generic methods, no `Self`-returning //! methods. The compile-time witness in `tests` enforces this. use std::fmt; use async_trait::async_trait; use lumotia_core::error::Result; use lumotia_core::types::{AudioSamples, ModelId, Transcript, TranscriptionOptions}; use serde::{Deserialize, Serialize}; /// Stable, lower-kebab-case identifier for a provider. Used in user /// profiles, settings storage, and logs. Examples: `local-whisper`, /// `local-parakeet`, `openai-whisper`, `groq-whisper-v3`. #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct ProviderId(String); impl ProviderId { pub fn new(id: impl Into) -> Self { Self(id.into()) } pub fn as_str(&self) -> &str { &self.0 } } impl fmt::Display for ProviderId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(&self.0) } } /// Whether a provider runs locally or over the network. The orchestrator /// inspects this to decide whether to honour an offline-only profile. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum ProviderKind { Local, Cloud(NetworkRequirement), } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum NetworkRequirement { /// Provider requires a live network connection on every call. Online, /// Provider can fall back to a cached or queued path when offline. OnlineWithFallback, } /// Indicative cost class for UI surfacing. Not a billing source of truth. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum CostClass { /// No marginal cost beyond the user's local hardware. Free, /// Per-call cost; user supplies their own API key (BYOK). PaidByok, /// Per-call cost; provider billed by CORBEL on the user's behalf. PaidManaged, } /// Capabilities a provider advertises to the orchestrator and the UI. /// Superset of `lumotia_transcription::TranscriberCapabilities` for /// local providers, with extra fields cloud providers populate. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub struct ProviderCapabilities { pub sample_rate: u32, pub channels: u16, pub initial_prompt_supported: bool, pub language_hint_supported: bool, pub streaming_supported: bool, pub cost_class: CostClass, } /// User-selectable engine configuration. The orchestrator resolves /// `engine_id` against the `EngineRegistry`, then derives /// `TranscriptionOptions` from the remaining fields. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct EngineProfile { pub engine_id: ProviderId, pub model_id: Option, pub language: Option, pub initial_prompt: Option, } impl EngineProfile { pub fn new(engine_id: ProviderId) -> Self { Self { engine_id, model_id: None, language: None, initial_prompt: None, } } pub fn to_options(&self) -> TranscriptionOptions { TranscriptionOptions { language: self.language.clone(), initial_prompt: self.initial_prompt.clone(), } } } /// Result returned by `TranscriptionProvider::transcribe`. Carries the /// transcript plus inference timing so the orchestrator can surface /// latency without losing it across the trait boundary. #[derive(Debug, Clone)] pub struct ProviderTranscript { pub transcript: Transcript, pub inference_ms: u64, } /// Async-native unified interface for transcription providers. /// /// `Send + Sync` supertraits: providers live in an `Arc` shared across tokio tasks. `async_trait` /// macro converts the async methods into boxed futures so the trait /// stays object-safe. #[async_trait] pub trait TranscriptionProvider: Send + Sync { fn provider_id(&self) -> ProviderId; fn kind(&self) -> ProviderKind; fn capabilities(&self) -> ProviderCapabilities; /// Idempotent pre-flight. Local providers verify the model is /// loaded into memory; cloud providers validate credentials and /// reach the endpoint. Called before the first `transcribe` of a /// session, and again after a profile or settings change that /// invalidates the previous prepare. async fn prepare(&self, profile: &EngineProfile) -> Result<()>; /// Transcribe a chunk of audio. The orchestrator passes raw audio /// already resampled to the provider's `capabilities().sample_rate`. async fn transcribe( &self, audio: AudioSamples, options: TranscriptionOptions, ) -> Result; } #[cfg(test)] mod tests { use super::*; #[test] fn provider_trait_is_object_safe() { // Compile-time witness: if the trait stops being object-safe // (generic method, Self-returning method, missing async_trait // attribute) this declaration fails to build. No runtime work. let _: Option> = None; } #[test] fn provider_id_round_trips_display_and_str() { let id = ProviderId::new("local-whisper"); assert_eq!(id.as_str(), "local-whisper"); assert_eq!(id.to_string(), "local-whisper"); } #[test] fn engine_profile_derives_options() { let profile = EngineProfile { engine_id: ProviderId::new("local-whisper"), model_id: None, language: Some("en".to_string()), initial_prompt: Some("Wyrdnote".to_string()), }; let opts = profile.to_options(); assert_eq!(opts.language, Some("en".to_string())); assert_eq!(opts.initial_prompt, Some("Wyrdnote".to_string())); } }