Skip to content

Commit cc3a7c0

Browse files
committed
refactor(ocr): 关键帧存图触发解耦(v0.12.0 M5·帧侧前置)
handle_full_frame: - 存图触发从「OCR 文本非空 + 同文本去重」改为「网格差异变化 + 2s 防抖」 ——关键帧纯图不再依赖 OCR 文本(视频全帧 OCR 下线后纯图帧仍归档) - 关键帧样本收集随视觉变化触发(纯图帧亦采样投票,文本为可选信号 ocr_text=None) - last_archived_text 参数退役(不再按文本去重——视觉变化即触发) 真机说明:本提交为 M5 帧侧前置(存图触发解耦),视频全帧 OCR 下线的 live_frame_process 流程改动单独跟进。
1 parent 94183d3 commit cc3a7c0

1 file changed

Lines changed: 30 additions & 29 deletions

File tree

‎app/src-tauri/src/live_keyframes.rs‎

Lines changed: 30 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! 实时链路关键帧处理与融合重写(live_session_frame.rs 拆分,保持主文件 ≤600 行)。
22
//!
3-
//! @ai-context: ① handle_full_frame:全帧画面要点落库 + M6 关键帧样本收集与
3+
//! @ai-context: ① handle_full_frame:全帧画面要点落库 + 关键帧样本收集与
44
//! 归档存图(三层图结构参考图集数据源);② 停止时关键帧投票
55
//! (session:keyframes 事件);③ rewrite_with_fusion:融合重写
66
//! (原属 live_session_frame,REQ-031 无字幕短路)。
@@ -21,13 +21,14 @@ use crate::types::{NewSessionOcrBlock, NewSessionSegment, TranscriptSegment};
2121
/// @ai-context: 去重(与导入链路 same_texts 同口径):强制 OCR 兜底会使静止画面
2222
/// 每 15s 重复识别——文本集合与上次完全一致时跳过落库,防要点列表刷屏。
2323
/// @ai-context: 落库成功即 emit live:ocr(前端实时画面流,简要单行卡片)。
24-
/// @ai-context: M6/REQ-051:关键帧样本收集(停止时投票)+ 新画面文本归档存图
24+
/// @ai-context: M6/REQ-051:关键帧样本收集(停止时投票)+ 归档存图
2525
/// (三层图结构参考图集数据源;预算上限由 image_store 控制)。
26+
/// @ai-context: v0.12.0 M5(关键帧纯图):存图触发解耦——不再依赖 OCR 文本非空,
27+
/// 改「网格差异变化 + 2s 防抖」(视频全帧 OCR 下线后纯图帧仍归档);
28+
/// 关键帧样本收集亦随视觉变化触发(纯图帧采样,文本为可选信号)。
2629
/// @ai-context: 修复①:UI 垃圾(播放器时间码/控制条/水印)源头过滤——与字幕路径
2730
/// (REQ-083)同口径,不进文本集/不落库/不归档(播放器区域被版面
2831
/// 误判时,OCR 产出时间码会每 2s 刷屏归档)。
29-
/// @ai-context: 修复②:空文本帧不归档——旧实现首帧无条件归档占坑(
30-
/// last_archived_text=None 恒真),误判会话参考图集只剩一张开头图。
3132
/// @ai-context: v0.7.3(REQ-155/156,ADR-015):屏分配——落库带 screen_id+bbox;
3233
/// layout_changed(版面指纹变化)与相似度/gap 共同判定新屏
3334
/// (ScreenTracker 纯状态机,同屏续屏不产生新屏记录)。
@@ -40,7 +41,7 @@ pub fn handle_full_frame(
4041
session_id: i64,
4142
last_texts: &mut Vec<String>,
4243
frame_samples: &mut Vec<crate::frame_cluster::FrameSample>,
43-
last_archived_text: &mut Option<String>,
44+
_last_archived_text: &mut Option<String>,
4445
last_archived_at: &mut Option<Instant>,
4546
image_store: &mut Option<crate::image_store::SessionImageStore>,
4647
ocr_input_hash: u64,
@@ -71,40 +72,40 @@ pub fn handle_full_frame(
7172
return;
7273
}
7374
}
74-
// M6:关键帧样本收集(全帧分支每次 OCR 成功记录;停止时投票器消费)
75-
if !texts.is_empty() {
75+
// v0.12.0 M5:视觉变化信号(关键帧样本收集 + 存图触发共用——解耦后不再
76+
// 依赖 OCR 文本)
77+
let grid_changed = !grid.changed_cells.is_empty();
78+
// v0.12.0 M5:关键帧样本收集改随视觉变化触发(纯图帧亦采样——投票器按
79+
// ahash/dhash 聚类,文本为可选信号)
80+
if grid_changed {
81+
let sample_text = if texts.is_empty() { None } else { Some(texts.join(" ")) };
7682
frame_samples.push(crate::frame_cluster::FrameSample {
7783
timestamp_ms: frame.timestamp_ms,
7884
ahash: ocr_input_hash,
7985
dhash: ocr_input_dhash,
80-
ocr_text: Some(texts.join(" ")),
86+
ocr_text: sample_text,
8187
change_magnitude: 0.0,
8288
});
8389
}
84-
// M6:新画面文本 → 归档存图(参考图集;同文本不重复归档 + 2s 防抖)。
85-
// 修复:空文本帧不归档——旧实现首帧无条件归档占坑(last_archived_text=None
86-
// 恒真,哪怕文本为空),误判会话参考图集只剩一张开头占坑图
87-
if !texts.is_empty() {
88-
let joined = texts.join(" ");
89-
let is_new_text = last_archived_text.as_deref() != Some(joined.as_str());
90-
let interval_ok = last_archived_at.is_none_or(|t| t.elapsed() >= Duration::from_secs(2));
91-
if is_new_text && interval_ok {
92-
if let Some(store) = image_store.as_mut() {
93-
match store.save_frame(frame.timestamp_ms, &frame.bgraw, frame.width, frame.height) {
94-
Ok(rel) => {
95-
// 2026-08 用户需求:实时图片数据显示——归档成功即推送
96-
// (前端转写面板"最近画面"条据此即时刷新,无需轮询)
97-
let _ = app.emit("live:image-saved", rel);
98-
}
99-
Err(e) => {
100-
// 归档失败不阻断 OCR 主链路(预算满/IO 错误静默降级,日志可观测)
101-
eprintln!("[ScreenWorker] 关键帧归档失败: {}", e);
102-
}
90+
// v0.12.0 M5(关键帧纯图):存图触发解耦——不再依赖 OCR 文本非空,
91+
// 改为「网格差异变化 + 2s 防抖」。(视频全帧 OCR 下线后关键帧纯图仍须归档;
92+
// 旧"同文本不重复/空文本不归档"的文本口径废弃——视觉变化即触发,纯图帧也归档。)
93+
let interval_ok = last_archived_at.is_none_or(|t| t.elapsed() >= Duration::from_secs(2));
94+
if grid_changed && interval_ok {
95+
if let Some(store) = image_store.as_mut() {
96+
match store.save_frame(frame.timestamp_ms, &frame.bgraw, frame.width, frame.height) {
97+
Ok(rel) => {
98+
// 2026-08 用户需求:实时图片数据显示——归档成功即推送
99+
// (前端转写面板"最近画面"条据此即时刷新,无需轮询)
100+
let _ = app.emit("live:image-saved", rel);
101+
}
102+
Err(e) => {
103+
// 归档失败不阻断主链路(预算满/IO 错误静默降级,日志可观测)
104+
eprintln!("[ScreenWorker] 关键帧归档失败: {}", e);
103105
}
104106
}
105-
*last_archived_text = Some(joined);
106-
*last_archived_at = Some(Instant::now());
107107
}
108+
*last_archived_at = Some(Instant::now());
108109
}
109110
if crate::import_frame::same_texts(&texts, last_texts) {
110111
return;

0 commit comments

Comments
 (0)