use crate::asr::model::ModelConfig; use crate::asr::engine; use crate::config::{get_config, save_config as save_config_file, AppSettings}; use serde::{Deserialize, Serialize}; use tauri::{Emitter, State}; use tracing::{error, info}; use super::state::{AppState, AppTheme}; /// 录音响应 #[derive(Debug, Serialize, Deserialize)] pub struct RecordResponse { pub success: bool, pub message: String, pub audio_path: Option, pub duration_secs: Option, } /// 识别响应 #[derive(Debug, Serialize, Deserialize)] pub struct RecognizeResponse { pub success: bool, pub text: String, pub language: Option, pub confidence: Option, pub duration_ms: Option, } /// 开始录音(非阻塞,立即返回) #[tauri::command] pub fn start_recording( state: State<'_, AppState>, app: tauri::AppHandle, ) -> Result { info!("开始录音命令"); if state.is_recording() { return Ok(RecordResponse { success: false, message: "正在录音中".to_string(), audio_path: None, duration_secs: None, }); } let config = get_config().map_err(|e| e.to_string())?; match crate::audio::start_recording(config.audio.sample_rate, config.audio.channels) { Ok(handle) => { let duration_secs = handle.duration_secs(); state.set_recording_handle(handle); info!("录音已启动"); // 通知前端录音已开始 let _ = app.emit("recording-started", ()); Ok(RecordResponse { success: true, message: "录音已开始".to_string(), audio_path: None, duration_secs: Some(duration_secs), }) } Err(e) => { error!("启动录音失败: {}", e); Err(e.to_string()) } } } /// 停止录音(停止流并保存 WAV 文件) #[tauri::command] pub fn stop_recording( state: State<'_, AppState>, app: tauri::AppHandle, ) -> Result { info!("停止录音命令"); let handle = state.take_recording_handle(); let Some(handle) = handle else { return Ok(RecordResponse { success: false, message: "未在录音".to_string(), audio_path: None, duration_secs: None, }); }; let duration_secs = handle.duration_secs(); info!("录音时长: {:.2}s", duration_secs); // 保存 WAV 文件 let output_path = chrono::Local::now().format("recordings/rec_%Y%m%d_%H%M%S.wav"); let output_path = std::path::PathBuf::from(output_path.to_string()); if let Some(parent) = output_path.parent() { let _ = std::fs::create_dir_all(parent); } match handle.stop_and_save(&output_path) { Ok((path, actual_duration)) => { state.set_recording_path(path.clone()); info!("录音已保存: {}", path); // 通知前端录音已停止 let _ = app.emit("recording-stopped", serde_json::json!({ "path": path, "duration": actual_duration })); Ok(RecordResponse { success: true, message: "录音已保存".to_string(), audio_path: Some(path), duration_secs: Some(actual_duration), }) } Err(e) => { // 即使保存失败也返回样本数量信息 let samples = handle.get_samples(); error!("保存录音失败: {}", e); Ok(RecordResponse { success: false, message: format!("保存失败: {}", e), audio_path: None, duration_secs: Some(duration_secs), }) } } } /// 识别录音文件(停止录音后自动识别) #[tauri::command] pub async fn recognize_last_recording( state: State<'_, AppState>, ) -> Result { let path = state .get_recording_path() .ok_or_else(|| "没有最近的录音文件".to_string())?; recognize_file(path, state).await } /// 识别指定音频文件 #[tauri::command] pub async fn recognize_file( path: String, state: State<'_, AppState>, ) -> Result { info!("识别音频: {}", path); ensure_engine_initialized()?; match engine::recognize(&path).await { Ok(result) => { let history = crate::config::HistoryEntry::new( result.text.clone(), result.language.clone(), result.confidence, result.duration_ms as f32 / 1000.0, ); state.add_history(history.clone()); Ok(RecognizeResponse { success: true, text: result.text, language: Some(result.language), confidence: Some(result.confidence), duration_ms: Some(result.duration_ms), }) } Err(e) => { error!("识别失败: {}", e); Err(format!("识别失败: {}", e)) } } } /// 识别音频文件(兼容旧命令名) #[tauri::command] pub async fn recognize_audio( path: String, state: State<'_, AppState>, ) -> Result { recognize_file(path, state).await } /// 确保 ASR 引擎已初始化 fn ensure_engine_initialized() -> Result<(), String> { if engine::ensure_engine_initialized().is_ok() { return Ok(()); } let config = get_config().map_err(|e| e.to_string())?; if let Some(model_path) = &config.asr.model_path { if model_path.exists() { let model_config = ModelConfig::new(model_path, &config.asr.model); return engine::init_engine(model_config).map_err(|e| format!("ASR 引擎初始化失败: {}", e)); } } let default_config = ModelConfig::default(); if default_config.model_exists() { engine::init_engine(default_config).map_err(|e| format!("ASR 引擎初始化失败: {}", e)) } else { Err("模型文件不存在,请先下载模型".to_string()) } } /// 获取配置 #[tauri::command] pub fn get_config_cmd() -> Result { get_config().map_err(|e| e.to_string()) } /// 保存配置 #[tauri::command] pub fn save_config(settings: AppSettings) -> Result<(), String> { save_config_file(&settings).map_err(|e| e.to_string()) } /// 获取历史记录 #[tauri::command] pub fn get_history( state: State<'_, AppState>, limit: Option, ) -> Vec { state.get_history(limit.unwrap_or(20)) } /// 清空历史记录 #[tauri::command] pub fn clear_history(state: State<'_, AppState>) { state.clear_history(); } /// 获取当前主题 #[tauri::command] pub fn get_theme(state: State<'_, AppState>) -> String { state.get_theme().as_str().to_string() } /// 设置主题 #[tauri::command] pub fn set_theme(theme: String, state: State<'_, AppState>, app: tauri::AppHandle) { let app_theme = AppTheme::from_str(&theme); state.set_theme(app_theme); let _ = app.emit("theme-change", theme); } /// 选择模型文件 #[tauri::command] pub async fn select_model_file(app: tauri::AppHandle) -> Result { use tauri_plugin_dialog::DialogExt; let (tx, rx) = std::sync::mpsc::channel(); app.dialog() .file() .add_filter("ONNX Model", &["onnx"]) .pick_file(move |file_path| { let result = match file_path { Some(path) => path.into_path() .map(|p| p.to_string_lossy().to_string()) .map_err(|e| format!("转换路径失败: {}", e)), None => Err("用户取消选择".to_string()), }; let _ = tx.send(result); }); rx.recv() .map_err(|e| e.to_string()) .and_then(|r| r) }