1616//! 的短文本不是"纯符号"碎片);confidence=None(字幕段)跳过
1717//! 低置信规则。
1818
19- use std:: collections:: HashSet ;
20-
2119use crate :: ai_protocol:: { TextFilterAction , TextFilterDecision } ;
22- use crate :: types:: { SessionOcrBlock , SessionSegment , TranscriptSegment } ;
20+ use crate :: types:: { SessionOcrBlock , SessionScreen , SessionSegment , TranscriptSegment } ;
2321use crate :: ui_junk:: UiJunkList ;
24- use crate :: watermark_filter:: { detect_watermarks, WatermarkConfig , WatermarkInput } ;
2522
2623/// 碎片段最大字符数(≤2 字丢弃)。
2724pub const FRAGMENT_MAX_CHARS : usize = 2 ;
2825/// 碎片段最小时长(<500ms 丢弃)。
2926pub const FRAGMENT_MIN_DURATION_MS : u64 = 500 ;
3027/// 低置信丢弃阈值(confidence <0.6 丢弃)。
3128pub const LOW_CONFIDENCE_THRESHOLD : f32 = 0.6 ;
32- /// OCR 画面要点最低分数(沿用 concat 口径)。
33- const OCR_MIN_SCORE : f32 = 0.5 ;
3429
3530/// 被过滤原因。
3631#[ derive( Debug , Clone , Copy , PartialEq , Eq , serde:: Serialize , serde:: Deserialize ) ]
@@ -84,8 +79,12 @@ pub struct NoteFilterResult {
8479 pub markdown : String ,
8580 /// 保留段(合并段已延伸 end_ms;AI merge 已拼接文本——仅产物层)
8681 pub kept : Vec < SessionSegment > ,
87- /// 画面要点(水印/UI 垃圾/低分已排除)
82+ /// 画面要点(屏段落行:区间+标题+正文+标签+配图; 水印/UI 垃圾/低分已排除)
8883 pub ocr_points : Vec < String > ,
84+ /// v0.7.3(REQ-160):画面要点屏(结构化——前端预览渲染屏卡;
85+ /// 命令层 attach 图后 image_ref 填充,markdown 配图行随之可渲染)
86+ #[ serde( default ) ]
87+ pub ocr_screens : Vec < SessionScreen > ,
8988 pub stats : FilterStats ,
9089 pub filtered : Vec < FilteredItem > ,
9190 pub merged : Vec < MergedItem > ,
@@ -171,10 +170,13 @@ pub fn filter_note(
171170 }
172171 kept. push ( seg) ;
173172 }
174- // ② 画面要点(水印 + UI 垃圾 + 低分 + 去重)
175- let ocr_points = build_ocr_points ( ocr_blocks, ui_junk) ;
173+ // ② 画面要点(v0.7.3 REQ-160:可消费块过滤 → 屏构建 → 屏段落渲染;
174+ // 水印 + UI 垃圾 + 低分在 filter_usable_blocks 排除——与原料口径解耦)
175+ let usable = crate :: screens:: filter_usable_blocks ( ocr_blocks, ui_junk) ;
176+ let ocr_screens = crate :: screens:: build_screens ( & usable, None ) ;
177+ let ocr_points = render_screen_points ( & ocr_screens) ;
176178 let markdown = rebuild_markdown ( title, & kept, & ocr_points) ;
177- NoteFilterResult { title : title. to_string ( ) , markdown, kept, ocr_points, stats, filtered, merged }
179+ NoteFilterResult { title : title. to_string ( ) , markdown, kept, ocr_points, ocr_screens , stats, filtered, merged }
178180}
179181
180182/// 碎片段判定(纯函数):≤2 字 / 时长 <500ms / 纯符号。
@@ -188,39 +190,57 @@ fn is_fragment(seg: &SessionSegment) -> bool {
188190 || text. chars ( ) . all ( |c| !c. is_alphanumeric ( ) && !is_cjk ( c) )
189191}
190192
191- /// 画面要点构建(纯函数):水印排除 + UI 垃圾 + 低分过滤 + 精确去重。
192- fn build_ocr_points ( ocr_blocks : & [ SessionOcrBlock ] , ui_junk : & UiJunkList ) -> Vec < String > {
193- // 水印检测(REQ-059):全帧块(字幕区文本已在讲述内容中,不重复进要点)
194- let inputs: Vec < WatermarkInput > = ocr_blocks
195- . iter ( )
196- . filter ( |b| b. region == "full" )
197- . map ( |b| WatermarkInput {
198- text : b. text . clone ( ) ,
199- timestamp_ms : b. timestamp_ms ,
200- region_key : None ,
201- } )
202- . collect ( ) ;
203- let watermarks = detect_watermarks ( & inputs, & WatermarkConfig :: default ( ) ) ;
204- let mut sorted: Vec < & SessionOcrBlock > = ocr_blocks
193+ /// 画面要点屏段落渲染(纯函数):屏卡 → 笔记画面要点行(Markdown 列表项)。
194+ ///
195+ /// @ai-context: 每屏一段:`- **[MM:SS–MM:SS] 标题**` + 正文行(二级列表)+
196+ /// 标签行 + 配图行(image_ref 有值时;src 为相对 data_dir 路径
197+ /// `session-images/{sid}/full/{ts}.webp`——前端渲染拼 baseUrl)。
198+ /// @ai-context: 无标题屏(旧数据无 bbox 降级)用"画面 N"占位——不丢内容。
199+ pub fn render_screen_points ( screens : & [ SessionScreen ] ) -> Vec < String > {
200+ let mut lines = Vec :: new ( ) ;
201+ for ( i, s) in screens. iter ( ) . enumerate ( ) {
202+ let range = format ! (
203+ "[{}–{}]" ,
204+ crate :: concat:: format_timestamp( s. first_seen_ms) ,
205+ crate :: concat:: format_timestamp( s. last_seen_ms)
206+ ) ;
207+ let title = s. title . clone ( ) . unwrap_or_else ( || format ! ( "画面 {}" , i + 1 ) ) ;
208+ lines. push ( format ! ( "- **{} {}**" , range, title) ) ;
209+ for b in & s. body {
210+ lines. push ( format ! ( " - {}" , b) ) ;
211+ }
212+ if !s. labels . is_empty ( ) {
213+ lines. push ( format ! ( " - 标签:{}" , s. labels. join( " · " ) ) ) ;
214+ }
215+ if let Some ( rel) = & s. image_ref {
216+ lines. push ( format ! ( " -  ) ;
217+ }
218+ }
219+ lines
220+ }
221+
222+ /// 刷新画面要点段落(纯函数):ocr_screens 重新渲染 + 重建 markdown。
223+ ///
224+ /// @ai-context: 命令层 attach_images 填充 image_ref 后调用——配图行随
225+ /// image_ref 出现/消失,保持 markdown 与 ocr_screens 一致
226+ /// (单管线双出口原则的图版本)。
227+ pub fn refresh_screen_points ( result : & mut NoteFilterResult ) {
228+ result. ocr_points = render_screen_points ( & result. ocr_screens ) ;
229+ let transcript: Vec < TranscriptSegment > = result
230+ . kept
205231 . iter ( )
206- . filter ( |b| {
207- b. region == "full"
208- && b. score >= OCR_MIN_SCORE
209- && !b. text . trim ( ) . is_empty ( )
210- && !ui_junk. is_junk ( & b. text )
211- && !watermarks. texts . iter ( ) . any ( |w| b. text . trim ( ) == w)
232+ . map ( |s| TranscriptSegment {
233+ start_ms : s. start_ms ,
234+ end_ms : s. end_ms ,
235+ text : s. text . clone ( ) ,
236+ word_timestamps : None ,
237+ confidence : None ,
238+ volume : None ,
212239 } )
213240 . collect ( ) ;
214- sorted. sort_by_key ( |b| b. timestamp_ms ) ;
215- let mut seen = HashSet :: new ( ) ;
216- let mut points = Vec :: new ( ) ;
217- for b in sorted {
218- let text = b. text . trim ( ) . to_string ( ) ;
219- if seen. insert ( text. clone ( ) ) {
220- points. push ( format ! ( "[{}] {}" , crate :: concat:: format_timestamp( b. timestamp_ms) , text) ) ;
221- }
222- }
223- points
241+ let paragraphs = crate :: concat:: split_transcript_paragraphs ( & transcript) ;
242+ result. markdown =
243+ crate :: concat:: assemble_markdown ( & result. title , & paragraphs, & result. ocr_points ) ;
224244}
225245
226246/// 组装 Markdown(标题 + 讲述内容 + 画面要点;段落切分复用 concat 口径)。
0 commit comments