//! 翻译服务模块 //! //! 支持多种翻译提供商:阿里百炼、DeepL、Ollama use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone)] pub enum TranslationProvider { AliBailian, // 阿里百炼 DeepL, Ollama, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TranslationConfig { pub provider: String, pub api_key: Option, pub api_url: Option, pub model: Option, } impl Default for TranslationConfig { fn default() -> Self { Self { provider: "ali_bailian".to_string(), api_key: None, api_url: Some("https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation".to_string()), model: Some("qwen-turbo".to_string()), } } } #[derive(Debug, Clone)] pub struct TranslationResult { pub original: String, pub translated: String, pub source_lang: String, pub target_lang: String, } pub struct TranslationService { provider: TranslationProvider, config: TranslationConfig, } impl TranslationService { pub fn new(provider: TranslationProvider, config: TranslationConfig) -> Self { Self { provider, config } } pub fn with_default_config() -> Self { Self { provider: TranslationProvider::AliBailian, config: TranslationConfig::default(), } } /// 翻译文本 pub fn translate(&self, text: &str, from: &str, to: &str) -> Result { match self.provider { TranslationProvider::AliBailian => self.translate_with_bailian(text, from, to), TranslationProvider::DeepL => self.translate_with_deepl(text, from, to), TranslationProvider::Ollama => self.translate_with_ollama(text, from, to), } } /// 使用阿里百炼翻译 fn translate_with_bailian(&self, text: &str, from: &str, to: &str) -> Result { // 构建翻译请求 prompt let prompt = format!( "Translate the following text from {} to {}. Only output the translation, no explanations:\n\n{}", from, to, text ); // 调用阿里百炼 API let client = reqwest::blocking::Client::new(); let request_body = serde_json::json!({ "model": self.config.model.as_deref().unwrap_or("qwen-turbo"), "input": { "messages": [ { "role": "user", "content": prompt } ] }, "parameters": { "temperature": 0.1, "max_tokens": 2000 } }); let api_key = self.config.api_key.as_deref() .context("阿里百炼 API Key 未配置")?; let response = client .post(self.config.api_url.as_deref().unwrap_or("https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation")) .header("Authorization", format!("Bearer {}", api_key)) .header("Content-Type", "application/json") .json(&request_body) .send() .context("翻译请求失败")?; if !response.status().is_success() { anyhow::bail!("翻译 API 返回错误:{}", response.status()); } let result: serde_json::Value = response.json() .context("解析翻译响应失败")?; // 提取翻译结果 let translated = result["output"]["choices"][0]["message"]["content"] .as_str() .context("翻译结果为空")? .to_string(); Ok(translated.trim().to_string()) } /// 使用 DeepL 翻译 fn translate_with_deepl(&self, text: &str, from: &str, to: &str) -> Result { let api_key = self.config.api_key.as_deref() .context("DeepL API Key 未配置")?; let client = reqwest::blocking::Client::new(); let response = client .post("https://api-free.deepl.com/v2/translate") .header("Authorization", format!("DeepL-Auth-Key {}", api_key)) .header("Content-Type", "application/json") .json(&serde_json::json!({ "text": [text], "source_lang": from.to_uppercase(), "target_lang": to.to_uppercase() })) .send() .context("DeepL 翻译请求失败")?; let result: serde_json::Value = response.json()?; let translated = result["translations"][0]["text"] .as_str() .context("DeepL 翻译结果为空")? .to_string(); Ok(translated) } /// 使用 Ollama 本地模型翻译 fn translate_with_ollama(&self, text: &str, from: &str, to: &str) -> Result { let api_url = self.config.api_url.as_deref() .unwrap_or("http://localhost:11434/api/generate"); let prompt = format!( "Translate from {} to {}. Only output translation:\n{}", from, to, text ); let client = reqwest::blocking::Client::new(); let response = client .post(api_url) .header("Content-Type", "application/json") .json(&serde_json::json!({ "model": self.config.model.as_deref().unwrap_or("qwen2.5:7b"), "prompt": prompt, "stream": false })) .send() .context("Ollama 翻译请求失败")?; let result: serde_json::Value = response.json()?; let translated = result["response"] .as_str() .context("Ollama 翻译结果为空")? .to_string(); Ok(translated.trim().to_string()) } /// 检测语言 pub fn detect_language(&self, text: &str) -> Result { // 简单语言检测:基于字符特征 let has_chinese = text.chars().any(|c| c >= '\u{4e00}' && c <= '\u{9fff}'); let has_japanese = text.chars().any(|c| c >= '\u{3040}' && c <= '\u{309f}' || c >= '\u{30a0}' && c <= '\u{30ff}'); let has_korean = text.chars().any(|c| c >= '\u{ac00}' && c <= '\u{d7af}'); if has_chinese { Ok("zh".to_string()) } else if has_japanese { Ok("ja".to_string()) } else if has_korean { Ok("ko".to_string()) } else { Ok("en".to_string()) } } /// 批量翻译(用于文档段落) pub fn translate_batch(&self, texts: &[String], from: &str, to: &str) -> Result> { let mut results = Vec::new(); for text in texts { let translated = self.translate(text, from, to)?; results.push(TranslationResult { original: text.clone(), translated, source_lang: from.to_string(), target_lang: to.to_string(), }); } Ok(results) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_language_detection() { let service = TranslationService::with_default_config(); assert_eq!(service.detect_language("你好世界").unwrap(), "zh"); assert_eq!(service.detect_language("Hello World").unwrap(), "en"); } }