@@ -33,7 +33,6 @@ use crate::purify_config::PurifyConfig;
3333use crate :: symbol_normalize:: SymbolNormalizeConfig ;
3434use crate :: types:: { SessionOcrBlock , SessionScreen , SessionSegment } ;
3535use crate :: ui_junk:: UiJunkList ;
36- use crate :: verbal_normalize:: { NormalizeConfig , NormalizeStrength } ;
3736
3837// ────────────────────────────────────────────────────────────
3938// 拆分子模块(AGENTS.md §3:单文件 ≤300 行)——父模块**自己**用 #[path] 声明,
@@ -45,10 +44,19 @@ use crate::verbal_normalize::{NormalizeConfig, NormalizeStrength};
4544#[ path = "note_filter_render.rs" ]
4645mod render;
4746
47+ /// 净化纯函数族 + 口头禅词表 + CJK 判定(过滤链与 note_filter_ai 共用)。
48+ #[ path = "note_filter_purify.rs" ]
49+ mod purify;
50+
4851/// 再导出:可见性与项 **1:1**(`pub` 项用 `pub use`、`pub(crate)` 项用
4952/// `pub(crate) use`——后者写成 `pub use` 会触发 E0365,项本身不允许外泄)。
5053pub use self :: render:: { refresh_screen_points, render_screen_points} ;
5154pub ( crate ) use self :: render:: { apply_session_warning, rebuild_markdown} ;
55+ pub ( crate ) use self :: purify:: FILLER_WORDS ;
56+
57+ // 净化族(本步起 filter_note_transcript 仍在主文件 ⇒ 直接引入;S3 搬出后本行
58+ // 随之下线,否则触发 unused_imports)。
59+ use self :: purify:: { concat_transcript, is_filler_only, is_fragment, is_purified_empty, purify_segment} ;
5260
5361/// 净化环境(依赖注入聚合——净化配置 + 符号映射 + OCR 纠错表)。
5462///
@@ -67,18 +75,6 @@ pub struct PurifyEnv {
6775/// 生成";净化链每次规则变更递增;v0.7.6 结构渲染层接入 REQ-177~181)。
6876pub const RULE_VERSION : & str = "note-rules-0.7.6" ;
6977
70- /// 口头禅词集(REQ-163 删除判定 + REQ-085 AI Filler 候选共用)。
71- ///
72- /// @ai-context: v0.7.5 扩展(与 verbal_normalize 词表对齐——「大家知道吗/
73- /// 咱们/我们看」等口语高频词此前只在实时路径被清,笔记路径漏网);
74- /// 短段全由这些词组成 → 规则级删除;"对"单字不删(回应语义,
75- /// 且碎片规则已按 ≤2 字处理——验收口径)。
76- pub ( crate ) const FILLER_WORDS : & [ & str ] = & [
77- "嗯" , "啊" , "呃" , "哦" , "诶" , "哎" , "哈" , "嗯嗯" , "哈哈" , "好的" , "对" , "那个" , "这个" ,
78- "就是" , "然后" , "对吧" , "是吧" , "对不对" , "对不对啊" , "好不好" , "就是说" , "然后呢" ,
79- "你们知道吗" , "大家知道吗" , "大家注意" , "大家看" , "咱们" , "我们看" , "我们来看" , "接下来呢" ,
80- ] ;
81-
8278/// 被过滤原因。
8379#[ derive( Debug , Clone , Copy , PartialEq , Eq , serde:: Serialize , serde:: Deserialize ) ]
8480#[ serde( rename_all = "kebab-case" ) ]
@@ -413,104 +409,6 @@ fn filter_note_empty(title: &str) -> NoteFilterResult {
413409 }
414410}
415411
416- /// 单段口语净化(纯函数):结巴折叠 → 书面化(保守档 Light)→ 符号规范化 →
417- /// 术语替换;返回净化后文本(统计计数由调用方入参累加)。
418- ///
419- /// @ai-context: 顺序契约(会话31 实证驱动):**折叠必须先于书面化**——verbal
420- /// compress_repeats 会把"甲甲甲"先压成"甲甲"(2 连短语重复),
421- /// 折叠规则(≥3 连同字)随后不再命中,结巴残留(「甲甲甲」→「甲」
422- /// 验收不达标);折叠在前则 3 连先收拢、书面化不再误动。
423- fn purify_segment (
424- text : & str ,
425- config : & PurifyConfig ,
426- symbol_cfg : & SymbolNormalizeConfig ,
427- stats : & mut FilterStats ,
428- ) -> String {
429- let mut out = text. to_string ( ) ;
430- let changed = |before : & str , after : & str | before != after;
431- let before = out. clone ( ) ;
432- let mut fold_hit = false ;
433- if config. stutter_fold {
434- let folded = crate :: stutter_fold:: fold_stutter ( & out) ;
435- fold_hit = folded != out;
436- out = folded;
437- }
438- if config. verbal_normalize {
439- let vcfg = NormalizeConfig { strength : NormalizeStrength :: Light } ;
440- out = crate :: verbal_normalize:: normalize ( & out, & vcfg) ;
441- }
442- if config. symbol_normalize {
443- out = crate :: symbol_normalize:: normalize ( & out, symbol_cfg) ;
444- }
445- let mut term_hit = false ;
446- if config. term_replace {
447- let replaced = crate :: stutter_fold:: apply_term_replacements ( & out) ;
448- term_hit = replaced != out;
449- out = replaced;
450- }
451- if changed ( & before, & out) {
452- stats. verbal += 1 ;
453- }
454- if fold_hit {
455- stats. stutter += 1 ;
456- }
457- if term_hit {
458- stats. term_replace += 1 ;
459- }
460- out
461- }
462-
463- /// 净化残留判定(纯函数):空串 / 纯符号(无字母数字汉字)→ 无信息内容。
464- fn is_purified_empty ( text : & str ) -> bool {
465- let t = text. trim ( ) ;
466- t. is_empty ( ) || t. chars ( ) . all ( |c| !c. is_alphanumeric ( ) && !is_cjk ( c) )
467- }
468-
469- /// 口头禅短段判定(纯函数,REQ-163):去首尾标点后 ≤filler_max_chars 字且
470- /// 全部空白分隔 token ∈ 口头禅词表 → 删除候选。
471- ///
472- /// @ai-context: "对不对?"→去"?"→"对不对" ✓;"对"单字 <2 不删(回应语义);
473- /// "3.14"数字不删(非口头禅词);"你说得对"含"你说得"不删。
474- fn is_filler_only ( text : & str , config : & PurifyConfig ) -> bool {
475- let stripped: String = text
476- . trim ( )
477- . trim_matches ( |c : char | c. is_ascii_punctuation ( ) || "。!?,、;:…·" . contains ( c) )
478- . to_string ( ) ;
479- let chars = stripped. chars ( ) . count ( ) ;
480- if chars < 2 || chars > config. filler_max_chars {
481- return false ;
482- }
483- let tokens: Vec < & str > = stripped. split_whitespace ( ) . collect ( ) ;
484- !tokens. is_empty ( ) && tokens. iter ( ) . all ( |t| FILLER_WORDS . contains ( t) )
485- }
486-
487- /// 保留段转写文本拼接(供 OCR 共现校验/纠错——画面词与讲述词互证)。
488- fn concat_transcript ( kept : & [ SessionSegment ] ) -> String {
489- let mut out = String :: new ( ) ;
490- for s in kept {
491- out. push_str ( & s. text ) ;
492- out. push ( ' ' ) ;
493- }
494- out
495- }
496-
497- /// 碎片段判定(纯函数):≤2 字 / 时长 <500ms / 纯符号(阈值可配置 REQ-173)。
498- ///
499- /// @ai-context: 纯符号 = 无字母数字汉字("----/···");"3.14/2024" 含数字
500- /// 不算纯符号——误杀保护(数字内容不误删)。
501- fn is_fragment ( seg : & SessionSegment , config : & PurifyConfig ) -> bool {
502- let text = seg. text . trim ( ) ;
503- text. chars ( ) . count ( ) <= config. fragment_max_chars
504- || seg. end_ms . saturating_sub ( seg. start_ms ) < config. fragment_min_duration_ms
505- || text. chars ( ) . all ( |c| !c. is_alphanumeric ( ) && !is_cjk ( c) )
506- }
507-
508- /// CJK 统一表意文字区段(含扩展 A)。
509- fn is_cjk ( c : char ) -> bool {
510- let u = c as u32 ;
511- ( 0x4E00 ..=0x9FFF ) . contains ( & u) || ( 0x3400 ..=0x4DBF ) . contains ( & u)
512- }
513-
514412// ────────────────────────────────────────────────────────────
515413// REQ-085:AI 复核(已拆至 note_filter_ai.rs——登记拆分计划落地;
516414// 公共 API 再导出保持对外兼容)
0 commit comments