html_translation_lib/pipeline/
filter.rs

1//! 文本过滤器模块
2//!
3//! 智能过滤需要翻译的文本内容
4
5#![allow(dead_code)] // 暂时允许未使用代码
6
7use crate::config::TranslationConfig;
8use regex::Regex;
9use std::collections::HashSet;
10
11/// 文本过滤器
12pub struct TextFilter {
13    /// 最小文本长度
14    min_length: usize,
15    
16    /// 跳过链接文本
17    skip_links: bool,
18    
19    /// 跳过代码块
20    skip_code_blocks: bool,
21    
22    /// 自定义过滤规则
23    custom_filters: Vec<Regex>,
24    
25    /// 数字模式正则
26    number_pattern: Regex,
27    
28    /// URL模式正则
29    url_pattern: Regex,
30    
31    /// 邮箱模式正则
32    email_pattern: Regex,
33    
34    /// 代码模式正则
35    code_pattern: Regex,
36    
37    /// 常见的不需要翻译的词汇
38    skip_words: HashSet<String>,
39}
40
41impl TextFilter {
42    /// 创建新的文本过滤器
43    pub fn new(config: &TranslationConfig) -> Self {
44        let custom_filters: Vec<Regex> = config
45            .custom_filters
46            .iter()
47            .filter_map(|pattern| Regex::new(pattern).ok())
48            .collect();
49        
50        let mut skip_words = HashSet::new();
51        // 添加常见的技术术语和不需要翻译的词汇
52        skip_words.insert("HTML".to_string());
53        skip_words.insert("CSS".to_string());
54        skip_words.insert("JavaScript".to_string());
55        skip_words.insert("JSON".to_string());
56        skip_words.insert("XML".to_string());
57        skip_words.insert("API".to_string());
58        skip_words.insert("HTTP".to_string());
59        skip_words.insert("HTTPS".to_string());
60        skip_words.insert("URL".to_string());
61        skip_words.insert("URI".to_string());
62        skip_words.insert("UUID".to_string());
63        skip_words.insert("ID".to_string());
64        
65        Self {
66            min_length: config.min_text_length,
67            skip_links: config.skip_links,
68            skip_code_blocks: config.skip_code_blocks,
69            custom_filters,
70            number_pattern: Regex::new(r"^\d+(\.\d+)?$").unwrap(),
71            url_pattern: Regex::new(r"^https?://[^\s]+$").unwrap(),
72            email_pattern: Regex::new(r"^[^\s@]+@[^\s@]+\.[^\s@]+$").unwrap(),
73            code_pattern: Regex::new(r"^[a-zA-Z_][a-zA-Z0-9_]*\(\)$|^[a-zA-Z_][a-zA-Z0-9_.]*$").unwrap(),
74            skip_words,
75        }
76    }
77    
78    /// 判断文本是否应该翻译
79    pub fn should_translate(&self, text: &str) -> bool {
80        let trimmed = text.trim();
81        
82        // 检查长度
83        if trimmed.len() < self.min_length {
84            return false;
85        }
86        
87        // 检查是否为纯数字
88        if self.number_pattern.is_match(trimmed) {
89            return false;
90        }
91        
92        // 检查是否为URL
93        if self.url_pattern.is_match(trimmed) {
94            return false;
95        }
96        
97        // 检查是否为邮箱
98        if self.email_pattern.is_match(trimmed) {
99            return false;
100        }
101        
102        // 检查是否为代码模式
103        if self.skip_code_blocks && self.code_pattern.is_match(trimmed) {
104            return false;
105        }
106        
107        // 检查是否在跳过词汇列表中
108        if self.skip_words.contains(&trimmed.to_uppercase()) {
109            return false;
110        }
111        
112        // 基于字母比例调整分数
113        let alpha_count = trimmed.chars().filter(|c| c.is_alphabetic()).count();
114        let total_count = trimmed.chars().count();
115        if total_count > 0 {
116            let alpha_ratio = alpha_count as f32 / total_count as f32;
117            if alpha_ratio < 0.3 {
118                return false;
119            }
120        }
121        
122        // 应用自定义过滤规则
123        for filter in &self.custom_filters {
124            if filter.is_match(trimmed) {
125                return false;
126            }
127        }
128        
129        // 检查是否主要由数字和符号组成(如版本号)
130        if self.is_version_like(trimmed) {
131            return false;
132        }
133        
134        // 检查是否为常见的HTML实体
135        if self.is_html_entity(trimmed) {
136            return false;
137        }
138        
139        true
140    }
141    
142    /// 分析文本的可翻译性
143    pub fn analyze_text(&self, text: &str) -> TextAnalysis {
144        let should_translate = self.should_translate(text);
145        let confidence = self.calculate_confidence(text);
146        let language_hint = self.detect_language_hint(text);
147        
148        TextAnalysis {
149            should_translate,
150            confidence,
151            language_hint,
152            reasons: self.get_skip_reasons(text),
153        }
154    }
155    
156    /// 检查是否类似版本号
157    fn is_version_like(&self, text: &str) -> bool {
158        // 匹配类似 "v1.2.3", "1.0", "2.1.4-beta" 的模式
159        let version_pattern = Regex::new(r"^v?\d+(\.\d+)*(-[a-zA-Z0-9]+)?$").unwrap();
160        version_pattern.is_match(text)
161    }
162    
163    /// 检查是否为HTML实体
164    fn is_html_entity(&self, text: &str) -> bool {
165        // 匹配 &lt; &gt; &amp; 等HTML实体
166        let entity_pattern = Regex::new(r"^&[a-zA-Z0-9#]+;$").unwrap();
167        entity_pattern.is_match(text)
168    }
169    
170    /// 计算翻译置信度
171    fn calculate_confidence(&self, text: &str) -> f32 {
172        let trimmed = text.trim();
173        let mut score = 0.5; // 基础分数
174        
175        // 基于长度调整分数
176        match trimmed.len() {
177            0..=2 => score -= 0.3,
178            3..=5 => score -= 0.1,
179            6..=20 => score += 0.1,
180            21..=100 => score += 0.2,
181            _ => score += 0.1,
182        }
183        
184        // 基于字母比例调整分数
185        let alpha_count = trimmed.chars().filter(|c| c.is_alphabetic()).count();
186        let total_count = trimmed.chars().count();
187        let alpha_ratio = alpha_count as f32 / total_count as f32;
188        score += (alpha_ratio - 0.5) * 0.4;
189        
190        // 基于常见模式调整分数
191        if self.number_pattern.is_match(trimmed) {
192            score -= 0.4;
193        }
194        
195        if self.url_pattern.is_match(trimmed) {
196            score -= 0.5;
197        }
198        
199        // 限制在0-1范围内
200        score.clamp(0.0, 1.0)
201    }
202    
203    /// 检测语言提示
204    fn detect_language_hint(&self, text: &str) -> LanguageHint {
205        let trimmed = text.trim();
206        
207        // 简单的语言检测逻辑
208        let has_chinese = trimmed.chars().any(|c| {
209            ('\u{4E00}'..='\u{9FFF}').contains(&c) // 中文字符范围
210        });
211        
212        let has_japanese = trimmed.chars().any(|c| {
213            ('\u{3040}'..='\u{309F}').contains(&c) || // 平假名
214            ('\u{30A0}'..='\u{30FF}').contains(&c)    // 片假名
215        });
216        
217        let has_korean = trimmed.chars().any(|c| {
218            ('\u{AC00}'..='\u{D7AF}').contains(&c) // 韩文字符范围
219        });
220        
221        let has_cyrillic = trimmed.chars().any(|c| {
222            ('\u{0400}'..='\u{04FF}').contains(&c) // 西里尔字符范围
223        });
224        
225        if has_chinese {
226            LanguageHint::Chinese
227        } else if has_japanese {
228            LanguageHint::Japanese
229        } else if has_korean {
230            LanguageHint::Korean
231        } else if has_cyrillic {
232            LanguageHint::Russian
233        } else if trimmed.is_ascii() {
234            LanguageHint::English
235        } else {
236            LanguageHint::Unknown
237        }
238    }
239    
240    /// 获取跳过原因
241    fn get_skip_reasons(&self, text: &str) -> Vec<String> {
242        let mut reasons = Vec::new();
243        let trimmed = text.trim();
244        
245        if trimmed.len() < self.min_length {
246            reasons.push(format!("文本长度过短 ({})", trimmed.len()));
247        }
248        
249        if self.number_pattern.is_match(trimmed) {
250            reasons.push("纯数字".to_string());
251        }
252        
253        if self.url_pattern.is_match(trimmed) {
254            reasons.push("URL地址".to_string());
255        }
256        
257        if self.email_pattern.is_match(trimmed) {
258            reasons.push("邮箱地址".to_string());
259        }
260        
261        if self.skip_words.contains(&trimmed.to_uppercase()) {
262            reasons.push("技术术语".to_string());
263        }
264        
265        if self.is_version_like(trimmed) {
266            reasons.push("版本号格式".to_string());
267        }
268        
269        if self.is_html_entity(trimmed) {
270            reasons.push("HTML实体".to_string());
271        }
272        
273        reasons
274    }
275}
276
277/// 过滤规则
278pub enum FilterRule {
279    /// 最小长度规则
280    MinLength(usize),
281    
282    /// 正则表达式规则
283    Regex(Regex),
284    
285    /// 跳过特定类型
286    SkipType(String),
287    
288    /// 自定义函数规则
289    Custom(Box<dyn Fn(&str) -> bool>),
290}
291
292/// 文本分析结果
293#[derive(Debug, Clone)]
294pub struct TextAnalysis {
295    /// 是否应该翻译
296    pub should_translate: bool,
297    
298    /// 翻译置信度 (0.0 - 1.0)
299    pub confidence: f32,
300    
301    /// 语言提示
302    pub language_hint: LanguageHint,
303    
304    /// 跳过原因
305    pub reasons: Vec<String>,
306}
307
308/// 语言提示枚举
309#[derive(Debug, Clone, PartialEq)]
310pub enum LanguageHint {
311    /// 未知语言
312    Unknown,
313    
314    /// 英语
315    English,
316    
317    /// 中文
318    Chinese,
319    
320    /// 日语
321    Japanese,
322    
323    /// 韩语
324    Korean,
325    
326    /// 俄语
327    Russian,
328}