use anyhow::Result;
use async_trait::async_trait;
use futures::Stream;
use serde::{Deserialize, Serialize};
use std::pin::Pin;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TranscriptionConfig {
pub duration_secs: Option<u64>,
pub chunk_duration_secs: f64,
pub model: String,
pub out_file: Option<String>,
pub language: Option<String>,
pub endpoint: Option<String>,
}
impl Default for TranscriptionConfig {
fn default() -> Self {
Self {
duration_secs: Some(30),
chunk_duration_secs: 5.0,
model: "whisper-1".to_string(),
out_file: None,
language: None,
endpoint: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum TranscriptionEvent {
Transcription {
chunk_id: usize,
text: String,
timestamp: std::time::SystemTime,
},
Error {
chunk_id: usize,
message: String,
},
Started {
timestamp: std::time::SystemTime,
},
Completed {
timestamp: std::time::SystemTime,
total_chunks: usize,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TranscriptionStats {
pub duration_secs: f64,
pub total_chunks: usize,
pub successful_chunks: usize,
pub error_count: usize,
pub total_chars: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TranscriptionProviderMetadata {
pub name: String,
pub supported_models: Vec<String>,
pub supports_streaming: bool,
pub supported_languages: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TranscriptionProviderKind {
Mock,
#[cfg(feature = "vttrs")]
VttRs,
}
impl TranscriptionProviderKind {
#[allow(clippy::should_implement_trait)]
pub fn from_str(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"mock" => Some(TranscriptionProviderKind::Mock),
#[cfg(feature = "vttrs")]
"vttrs" | "vtt-rs" => Some(TranscriptionProviderKind::VttRs),
_ => None,
}
}
pub fn as_str(&self) -> &'static str {
match self {
TranscriptionProviderKind::Mock => "mock",
#[cfg(feature = "vttrs")]
TranscriptionProviderKind::VttRs => "vttrs",
}
}
}
#[async_trait]
pub trait TranscriptionProvider: Send + Sync {
async fn start_transcription(
&self,
config: &TranscriptionConfig,
) -> Result<Pin<Box<dyn Stream<Item = Result<TranscriptionEvent>> + Send>>>;
fn metadata(&self) -> TranscriptionProviderMetadata;
fn kind(&self) -> TranscriptionProviderKind;
async fn health_check(&self) -> Result<bool> {
Ok(true)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_provider_kind_from_str() {
assert_eq!(
TranscriptionProviderKind::from_str("mock"),
Some(TranscriptionProviderKind::Mock)
);
assert_eq!(
TranscriptionProviderKind::from_str("Mock"),
Some(TranscriptionProviderKind::Mock)
);
assert_eq!(
TranscriptionProviderKind::from_str("MOCK"),
Some(TranscriptionProviderKind::Mock)
);
assert_eq!(TranscriptionProviderKind::from_str("invalid"), None);
}
#[test]
fn test_provider_kind_as_str() {
assert_eq!(TranscriptionProviderKind::Mock.as_str(), "mock");
}
#[test]
fn test_transcription_config_default() {
let config = TranscriptionConfig::default();
assert_eq!(config.duration_secs, Some(30));
assert_eq!(config.chunk_duration_secs, 5.0);
assert_eq!(config.model, "whisper-1");
}
#[test]
fn test_transcription_config_serialization() {
let config = TranscriptionConfig {
duration_secs: Some(60),
chunk_duration_secs: 3.0,
model: "whisper-large".to_string(),
out_file: Some("/tmp/transcript.txt".to_string()),
language: Some("en".to_string()),
endpoint: None,
};
let json = serde_json::to_string(&config).unwrap();
let deserialized: TranscriptionConfig = serde_json::from_str(&json).unwrap();
assert_eq!(config.duration_secs, deserialized.duration_secs);
assert_eq!(config.model, deserialized.model);
}
}