use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use std::path::PathBuf; use std::time::Duration; use utoipa::ToSchema; use crate::models::AudioFormat; /// Initial task submitted to the queue #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AsyncTranscriptionTask { pub task_id: String, pub audio_file_path: PathBuf, // Path to the audio file on disk pub original_filename: String, // Original filename from upload pub model: Option, pub response_format: Option, pub created_at: DateTime, pub priority: TaskPriority, } impl AsyncTranscriptionTask { pub fn new( task_id: String, audio_file_path: PathBuf, original_filename: String, model: Option, response_format: Option, ) -> Self { Self { task_id, audio_file_path, original_filename, model, response_format, created_at: Utc::now(), priority: TaskPriority::Normal, } } } /// After Step 1: Audio format processing completed #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AudioProcessedTask { pub task_id: String, pub processed_audio_path: PathBuf, pub original_format: AudioFormat, pub model: Option, pub response_format: Option, pub created_at: DateTime, pub audio_duration: Option, pub cleanup_files: Vec, // Files to cleanup after processing } /// After Step 2: Whisper transcription completed #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TranscriptionCompletedTask { pub task_id: String, pub transcription_result: SerializableTranscriptionResult, pub response_format: Option, pub created_at: DateTime, pub processing_stages: Vec, } /// Serializable version of voice_toolkit::stt::TranscriptionResult #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SerializableTranscriptionResult { pub text: String, pub segments: Vec, pub language: Option, pub audio_duration: u64, // milliseconds } /// Serializable version of voice_toolkit segment #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SerializableSegment { pub start_time: u64, // milliseconds pub end_time: u64, // milliseconds pub text: String, pub confidence: f32, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ProcessingStageInfo { pub stage: ProcessingStage, pub started_at: DateTime, pub completed_at: DateTime, pub duration: Duration, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ToSchema)] pub enum TaskPriority { Low = 1, Normal = 2, High = 3, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ToSchema)] pub enum ProcessingStage { AudioFormatDetection, AudioConversion, WhisperTranscription, ResultProcessing, } impl ProcessingStage { pub fn step_name(&self) -> &'static str { match self { ProcessingStage::AudioFormatDetection => "audio_format_step", ProcessingStage::AudioConversion => "audio_format_step", // Same step handles both ProcessingStage::WhisperTranscription => "whisper_transcription_step", ProcessingStage::ResultProcessing => "result_formatting_step", } } } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ToSchema)] pub enum TaskStatus { Pending { queued_at: DateTime, }, Processing { stage: ProcessingStage, started_at: DateTime, progress_details: Option, }, Completed { completed_at: DateTime, processing_time: Duration, result_summary: Option, }, Failed { error: TaskError, failed_at: DateTime, retry_count: u32, is_recoverable: bool, }, Cancelled { cancelled_at: DateTime, reason: Option, }, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ToSchema)] pub struct ProgressDetails { pub current_stage: ProcessingStage, pub stage_progress: Option, // 0.0 to 1.0 pub estimated_remaining: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ToSchema)] pub enum TaskError { AudioProcessingFailed { stage: ProcessingStage, message: String, is_recoverable: bool, }, TranscriptionFailed { model: String, message: String, is_recoverable: bool, }, StorageError { operation: String, message: String, }, TimeoutError { stage: ProcessingStage, timeout_duration: Duration, }, CancellationRequested, } impl TaskError { pub fn is_recoverable(&self) -> bool { match self { TaskError::AudioProcessingFailed { is_recoverable, .. } => *is_recoverable, TaskError::TranscriptionFailed { is_recoverable, .. } => *is_recoverable, TaskError::StorageError { .. } => true, // Storage errors are usually recoverable TaskError::TimeoutError { .. } => true, // Timeout errors are recoverable TaskError::CancellationRequested => false, // Cancellation is not recoverable } } } impl std::fmt::Display for TaskError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { TaskError::AudioProcessingFailed { stage, message, .. } => { write!(f, "音频处理失败 ({}): {}", stage.step_name(), message) } TaskError::TranscriptionFailed { model, message, .. } => { write!(f, "转录失败 ({}): {}", model, message) } TaskError::StorageError { operation, message } => { write!(f, "存储错误 ({}): {}", operation, message) } TaskError::TimeoutError { stage, timeout_duration, } => { write!( f, "超时错误 ({}): {} 秒", stage.step_name(), timeout_duration.as_secs() ) } TaskError::CancellationRequested => { write!(f, "任务已被取消") } } } } impl Default for TaskPriority { fn default() -> Self { TaskPriority::Normal } } impl AsyncTranscriptionTask { pub fn with_priority(mut self, priority: TaskPriority) -> Self { self.priority = priority; self } } impl AudioProcessedTask { pub fn from_async_task( task: AsyncTranscriptionTask, processed_audio_path: PathBuf, original_format: AudioFormat, audio_duration: Option, cleanup_files: Vec, ) -> Self { Self { task_id: task.task_id, processed_audio_path, original_format, model: task.model, response_format: task.response_format, created_at: task.created_at, audio_duration, cleanup_files, } } } impl TranscriptionCompletedTask { pub fn from_audio_processed_task( task: AudioProcessedTask, transcription_result: SerializableTranscriptionResult, processing_stages: Vec, ) -> Self { Self { task_id: task.task_id, transcription_result, response_format: task.response_format, created_at: task.created_at, processing_stages, } } } // Conversion functions between voice_toolkit types and serializable types impl From for SerializableTranscriptionResult { fn from(result: voice_toolkit::stt::TranscriptionResult) -> Self { Self { text: result.text, segments: result .segments .into_iter() .map(|s| SerializableSegment::from_voice_toolkit_segment(s)) .collect(), language: result.language, audio_duration: result.audio_duration, } } } impl From for SerializableSegment { fn from(segment: crate::models::Segment) -> Self { Self { start_time: (segment.start * 1000.0) as u64, // Convert seconds to milliseconds end_time: (segment.end * 1000.0) as u64, // Convert seconds to milliseconds text: segment.text, confidence: segment.confidence, } } } impl SerializableSegment { pub fn from_voice_toolkit_segment(segment: voice_toolkit::stt::TranscriptionSegment) -> Self { Self { start_time: segment.start_time * 1000, // Convert seconds to milliseconds (assuming start_time is in seconds as u64) end_time: segment.end_time * 1000, // Convert seconds to milliseconds (assuming end_time is in seconds as u64) text: segment.text, confidence: segment.confidence, } } } impl From for crate::models::TranscriptionResponse { fn from(result: SerializableTranscriptionResult) -> Self { Self { text: result.text, segments: result .segments .into_iter() .map(|s| crate::models::Segment { start: s.start_time as f32 / 1000.0, // Convert from ms to seconds end: s.end_time as f32 / 1000.0, text: s.text, confidence: s.confidence, }) .collect(), language: result.language, duration: Some(result.audio_duration as f32 / 1000.0), // Convert from ms to seconds processing_time: 0.0, // Will be set by the handler metadata: None, } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_task_priority_default() { assert_eq!(TaskPriority::default(), TaskPriority::Normal); } #[test] fn test_async_transcription_task_creation() { let task = AsyncTranscriptionTask::new( "test-task-1".to_string(), PathBuf::from("/tmp/test.mp3"), "test.mp3".to_string(), Some("base".to_string()), Some("json".to_string()), ); assert_eq!(task.task_id, "test-task-1"); assert_eq!(task.audio_file_path, PathBuf::from("/tmp/test.mp3")); assert_eq!(task.original_filename, "test.mp3"); assert_eq!(task.model, Some("base".to_string())); assert_eq!(task.response_format, Some("json".to_string())); assert_eq!(task.priority, TaskPriority::Normal); } #[test] fn test_task_with_priority() { let task = AsyncTranscriptionTask::new( "test-task-1".to_string(), PathBuf::from("/tmp/test.mp3"), "test.mp3".to_string(), None, None, ) .with_priority(TaskPriority::High); assert_eq!(task.priority, TaskPriority::High); } #[test] fn test_processing_stage_step_name() { assert_eq!( ProcessingStage::AudioFormatDetection.step_name(), "audio_format_step" ); assert_eq!( ProcessingStage::AudioConversion.step_name(), "audio_format_step" ); assert_eq!( ProcessingStage::WhisperTranscription.step_name(), "whisper_transcription_step" ); assert_eq!( ProcessingStage::ResultProcessing.step_name(), "result_formatting_step" ); } #[test] fn test_task_error_is_recoverable() { let recoverable_error = TaskError::AudioProcessingFailed { stage: ProcessingStage::AudioConversion, message: "Conversion failed".to_string(), is_recoverable: true, }; assert!(recoverable_error.is_recoverable()); let non_recoverable_error = TaskError::CancellationRequested; assert!(!non_recoverable_error.is_recoverable()); let timeout_error = TaskError::TimeoutError { stage: ProcessingStage::WhisperTranscription, timeout_duration: Duration::from_secs(3600), }; assert!(timeout_error.is_recoverable()); } #[test] fn test_serializable_transcription_result_conversion() { use crate::models::TranscriptionResponse; let serializable_result = SerializableTranscriptionResult { text: "Hello world".to_string(), segments: vec![SerializableSegment { start_time: 0, end_time: 2000, // 2 seconds in ms text: "Hello world".to_string(), confidence: 0.95, }], language: Some("en".to_string()), audio_duration: 2000, // 2 seconds in ms }; let response: TranscriptionResponse = serializable_result.into(); assert_eq!(response.text, "Hello world"); assert_eq!(response.segments.len(), 1); assert_eq!(response.segments[0].start, 0.0); assert_eq!(response.segments[0].end, 2.0); // Converted to seconds assert_eq!(response.language, Some("en".to_string())); assert_eq!(response.duration, Some(2.0)); // Converted to seconds } }