@@ -30,6 +30,38 @@ pub fn normalize_source_title(raw: &str) -> String {
3030 trimmed. to_string ( )
3131}
3232
33+ /// 标题 → 热词候选(REQ-266 注入源扩展,纯函数)。
34+ ///
35+ /// @ai-context: 采集目标窗口标题携带课程/主题专名("项目从立项到交付全流程|落地
36+ /// 式项目全流程 - 抖音")——按分隔符切 token、剔除平台品牌与纯数字、
37+ /// 长度 2..=14 字符,去重限量(≤10),供 preheat 注入;tokens 表外字
38+ /// 风险由 use-time 过滤兜底(streaming_asr filter_hotwords_by_tokens)。
39+ pub fn title_hotword_candidates ( raw : & str ) -> Vec < String > {
40+ const SEPARATORS : & [ char ] = & [ '|' , '|' , '-' , '_' , ' ' , '/' , '·' , ':' , ':' ] ;
41+ let normalized = normalize_source_title ( raw) ;
42+ let mut out: Vec < String > = Vec :: new ( ) ;
43+ for tok in normalized. split ( SEPARATORS ) {
44+ let tok = tok. trim ( ) ;
45+ let n = tok. chars ( ) . count ( ) ;
46+ if !( 2 ..=14 ) . contains ( & n) {
47+ continue ;
48+ }
49+ if tok. chars ( ) . all ( |c| c. is_ascii_digit ( ) ) {
50+ continue ; // 纯数字(年份/计数)无术语价值
51+ }
52+ if BRAND_SUFFIXES . iter ( ) . any ( |b| tok == * b) {
53+ continue ; // 平台品牌名不入词表
54+ }
55+ if !out. iter ( ) . any ( |t| t == tok) {
56+ out. push ( tok. to_string ( ) ) ;
57+ }
58+ if out. len ( ) >= 10 {
59+ break ;
60+ }
61+ }
62+ out
63+ }
64+
3365/// 同源去重:existing 含 base → 追加「 #N」(N=现有最大后缀 +1,首个为 #2)。
3466///
3567/// @ai-context: 纯比较不猜测——只处理「恰好等于 base」与「base #N」两类;
@@ -163,4 +195,32 @@ mod tests {
163195 ) ;
164196 assert_eq ! ( first_line_title( std:: iter:: empty:: <& str >( ) ) , None ) ;
165197 }
198+
199+ // ── title_hotword_candidates(REQ-266)──
200+
201+ #[ test]
202+ fn title_candidates_split_and_strip_brand ( ) {
203+ // "项目从立项到交付全流程|落地式项目全流程 - 抖音" → 两段主题词;品牌剔除
204+ let cs = title_hotword_candidates ( "项目从立项到交付全流程|落地式项目全流程 - 抖音" ) ;
205+ assert ! ( cs. contains( & "项目从立项到交付全流程" . to_string( ) ) ) ;
206+ assert ! ( cs. contains( & "落地式项目全流程" . to_string( ) ) ) ;
207+ assert ! ( !cs. iter( ) . any( |t| t == "抖音" ) ) ;
208+ }
209+
210+ #[ test]
211+ fn title_candidates_bare_brand_and_tiny_empty ( ) {
212+ assert ! ( title_hotword_candidates( "抖音" ) . is_empty( ) ) ;
213+ assert ! ( title_hotword_candidates( "" ) . is_empty( ) ) ;
214+ assert ! ( title_hotword_candidates( "a" ) . is_empty( ) ) ;
215+ assert ! ( title_hotword_candidates( "2024" ) . is_empty( ) ) ;
216+ }
217+
218+ #[ test]
219+ fn title_candidates_dedupe_and_cap ( ) {
220+ // 重复 token 只取一次;超出 10 个截断不 panic
221+ let many = "词一 词二 词三 词四 词五 词六 词七 词八 词九 词十 词十一 词一" ;
222+ let cs = title_hotword_candidates ( many) ;
223+ assert ! ( cs. len( ) <= 10 ) ;
224+ assert_eq ! ( cs. iter( ) . filter( |t| * t == "词一" ) . count( ) , 1 ) ;
225+ }
166226}
0 commit comments