//! AI 后端 trait + 本地命令行实现 //! //! 抽象 ASR/LLM/TTS 三层,支持本地命令行(LocalCliBackend)和未来云端后端。 //! V1 只实现 local;cloud 配置时返回"暂未支持"错误。 use anyhow::{bail, Result}; use std::path::{Path, PathBuf}; use std::process::Command; /// AI 后端抽象 trait pub trait AiBackend: Send { /// 语音转文字 (ASR) /// - audio_path: 16kHz mono wav 临时文件 /// - model_path: whisper.cpp 模型 (ggml-tiny.bin 等) fn asr(&self, audio_path: &Path, model_path: &Path) -> Result; /// LLM 对话 /// - model_path: llama.cpp GGUF 模型 /// - persona: 角色人设 system prompt /// - user_message: 本轮用户输入 /// - history: 历史轮次 (role, content) /// - max_tokens: 最大回复 token (0=默认 128) fn llm_chat( &self, model_path: &Path, persona: &str, user_message: &str, history: &[(String, String)], max_tokens: u16, ) -> Result; /// 文字转语音 (TTS) /// - text: 要合成的文本 /// - model_path: piper 模型目录/文件 /// - output_path: 输出 wav 文件路径 fn tts(&self, text: &str, model_path: &Path, output_path: &Path) -> Result<()>; /// 后端标识(local / cloud) fn backend_name(&self) -> &str; } /// 本地命令行后端 /// /// 通过子进程调用 whisper-cli / llama-cli / piper 二进制。 /// Spike (2026-07-03) 验证的命令行参数固化于此。 pub struct LocalCliBackend { /// 工具目录(含 whisper-cli, llama-cli, piper 二进制) tools_dir: PathBuf, /// whisper-cli 依赖的库路径(libggml*.so 所在) whisper_lib_dir: Option, /// piper 依赖的库路径(libespeak-ng.so / libpiper_phonemize.so 所在) piper_lib_dir: Option, /// piper 模型 config json 路径(.onnx.json) piper_config: Option, /// piper espeak-ng-data 目录路径 espeak_data_dir: Option, } impl LocalCliBackend { pub fn new(tools_dir: PathBuf) -> Self { Self { tools_dir, whisper_lib_dir: None, piper_lib_dir: None, piper_config: None, espeak_data_dir: None, } } /// 设置 whisper-cli 依赖库路径(部署时由 deploy_ai.sh 标定) pub fn with_whisper_lib_dir(mut self, lib_dir: PathBuf) -> Self { self.whisper_lib_dir = Some(lib_dir); self } /// 设置 piper 依赖(库路径、config、espeak-ng-data 目录) pub fn with_piper_deps(mut self, lib_dir: PathBuf, config: PathBuf, espeak_data: PathBuf) -> Self { self.piper_lib_dir = Some(lib_dir); self.piper_config = Some(config); self.espeak_data_dir = Some(espeak_data); self } fn whisper_cli(&self) -> PathBuf { self.tools_dir.join("whisper-cli") } fn llama_cli(&self) -> PathBuf { self.tools_dir.join("llama-cli") } fn piper(&self) -> PathBuf { self.tools_dir.join("piper") } /// 给命令设置 LD_LIBRARY_PATH(合并所有库路径) fn with_lib_path(&self, cmd: &mut Command, lib_dir: &Path) { let existing = std::env::var("LD_LIBRARY_PATH").unwrap_or_default(); let new_path = if existing.is_empty() { lib_dir.to_string_lossy().into_owned() } else { format!("{}:{}", lib_dir.to_string_lossy(), existing) }; cmd.env("LD_LIBRARY_PATH", new_path); } } impl AiBackend for LocalCliBackend { fn asr(&self, audio_path: &Path, model_path: &Path) -> Result { // whisper-cli -m -f