提交qiming-mcp-proxy
This commit is contained in:
429
qiming-mcp-proxy/voice-cli/src/models/stepped_task.rs
Normal file
429
qiming-mcp-proxy/voice-cli/src/models/stepped_task.rs
Normal file
@@ -0,0 +1,429 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user