Files
qiming/qiming-mcp-proxy/voice-cli/src/models/stepped_task.rs
2026-06-01 13:03:20 +08:00

430 lines
13 KiB
Rust

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<String>,
pub response_format: Option<String>,
pub created_at: DateTime<Utc>,
pub priority: TaskPriority,
}
impl AsyncTranscriptionTask {
pub fn new(
task_id: String,
audio_file_path: PathBuf,
original_filename: String,
model: Option<String>,
response_format: Option<String>,
) -> 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<String>,
pub response_format: Option<String>,
pub created_at: DateTime<Utc>,
pub audio_duration: Option<f32>,
pub cleanup_files: Vec<PathBuf>, // 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<String>,
pub created_at: DateTime<Utc>,
pub processing_stages: Vec<ProcessingStageInfo>,
}
/// Serializable version of voice_toolkit::stt::TranscriptionResult
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SerializableTranscriptionResult {
pub text: String,
pub segments: Vec<SerializableSegment>,
pub language: Option<String>,
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<Utc>,
pub completed_at: DateTime<Utc>,
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<Utc>,
},
Processing {
stage: ProcessingStage,
started_at: DateTime<Utc>,
progress_details: Option<ProgressDetails>,
},
Completed {
completed_at: DateTime<Utc>,
processing_time: Duration,
result_summary: Option<String>,
},
Failed {
error: TaskError,
failed_at: DateTime<Utc>,
retry_count: u32,
is_recoverable: bool,
},
Cancelled {
cancelled_at: DateTime<Utc>,
reason: Option<String>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ToSchema)]
pub struct ProgressDetails {
pub current_stage: ProcessingStage,
pub stage_progress: Option<f32>, // 0.0 to 1.0
pub estimated_remaining: Option<Duration>,
}
#[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<f32>,
cleanup_files: Vec<PathBuf>,
) -> 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<ProcessingStageInfo>,
) -> 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<voice_toolkit::stt::TranscriptionResult> 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<crate::models::Segment> 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<SerializableTranscriptionResult> 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
}
}