Skip to content

Commit 52f4958

Browse files
committed
refactor(video): 视频会话画面要点纯图落地——删两条残留 region OCR 路径
- live_frame_process: else 分支纯关键帧归档(handle_full_frame 空块),删 !is_subtitle 布局分析 + region_ocr_blocks 分支(0.12.1 仍产 OCR 文字的根因), 移除 layout_cache 参数 - import: 关键帧无条件纯图归档(rgb_to_bgra),字幕 OCR 独立门控;删中区域 OCR - screens: build_keyframe_screens + build_view_screens 按 kind 分派(photo 保持 OCR 文本屏,video 走纯图屏);get_session_detail/preview_session_note/ manual_capture_inner 三处接入 - 退役机制标 #[allow(dead_code)] + @ai-context(region_ocr/layout_cache/ grid_from_bgra/save_crop);SessionDetailPanel kind 感知标题 - 新增 3 单测:纯图屏逐图/无图空屏/按 kind 分派
1 parent 6a41e1d commit 52f4958

16 files changed

Lines changed: 236 additions & 158 deletions

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ pub async fn import_video(state: State<'_, AppState>, path: String) -> Result<i6
3737
let app = state.app.clone();
3838
// REQ-117(v0.7.0 M2):UI 垃圾黑名单(导入画面要点源头过滤——双入口同口径)
3939
let ui_junk = state.ui_junk.clone();
40+
// v0.12.0 M5 补完成:关键帧纯图归档根目录(应用数据目录)
41+
let data_dir = state.data_dir.clone();
4042
// 审查 P2 修复(TD-036):ffmpeg 探测注入生产捆绑路径(resource_dir/ffmpeg,
4143
// 安装包随 bundle.resources 携带;开发期捆绑目录 = crate 下 ffmpeg/)。
4244
// 解析顺序仍为:ENTROPY_FFMPEG_DIR → 注入目录 → PATH(ffmpeg.rs)。
@@ -46,7 +48,7 @@ pub async fn import_video(state: State<'_, AppState>, path: String) -> Result<i6
4648
}
4749
let resolver = FfmpegResolver::with_dirs(resolver_dirs);
4850
tauri::async_runtime::spawn_blocking(move || {
49-
run_video_import(&db, &engines, &resolver, &trimmed, &ui_junk, |p: &ImportProgress| {
51+
run_video_import(&db, &engines, &resolver, &trimmed, &ui_junk, &data_dir, |p: &ImportProgress| {
5052
let _ = app.emit("import:progress", p);
5153
})
5254
})

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,14 @@ pub async fn get_session_detail(state: State<'_, AppState>, id: i64) -> Result<S
109109
let events = state.db.list_events(id).map_err(|e| e.to_string())?;
110110
// v0.7.3(REQ-160,ADR-015):画面要点屏卡(新数据按 screen_id 分组、
111111
// 旧数据聚类兜底;图匹配失败/目录缺失不阻断——前端无缩略图降级)
112+
// v0.12.0 M5 补完成:按会话类型分派——photo=OCR 文本屏;video=关键帧纯图屏
112113
let images_dir = state.data_dir.join("session-images").join(id.to_string());
113-
let mut screens = crate::screens::build_screens(&ocr_blocks, Some(&images_dir));
114+
let mut screens = crate::screens::build_view_screens(
115+
session.kind.as_deref(),
116+
session.id,
117+
&ocr_blocks,
118+
Some(&images_dir),
119+
);
114120
// v0.7.3(REQ-159):屏内结构块渲染课后精修产物(表格 Markdown/公式 LaTeX;
115121
// 未精修 → 保留原始 OCR 文本,徽标降级)
116122
let artifact = state.db.get_artifact(id).map_err(|e| e.to_string())?;

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,14 @@ pub async fn preview_session_note(
358358
};
359359
let mut result = filter_note(&session.title, &segments, &ocr_blocks, &state.ui_junk, &env);
360360
let images_dir = state.data_dir.join("session-images").join(id.to_string());
361-
crate::screens::attach_images(&mut result.ocr_screens, &images_dir);
361+
// v0.12.0 M5 补完成:视频会话(kind≠photo)画面要点 = 关键帧纯图屏(无 OCR
362+
// 文字——ADR-023 视频会话不再识别画面要点,真实要点经 vision-exp 精修提取);
363+
// 图文会话保持过滤链 OCR 屏(attach_images,与落库同口径零变化)。
364+
if session.kind.as_deref() == Some("photo") {
365+
crate::screens::attach_images(&mut result.ocr_screens, &images_dir);
366+
} else {
367+
result.ocr_screens = crate::screens::build_keyframe_screens(session.id, Some(&images_dir));
368+
}
362369
// v0.7.5(REQ-170):预览与落库同口径——异常会话预览即带警示行
363370
crate::note_filter::apply_session_warning(&mut result, &session.status);
364371
crate::note_filter::refresh_screen_points(&mut result);

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,16 @@ fn manual_capture_inner(
9393
) -> std::result::Result<StructureImageRecord, String> {
9494
// ① 定位屏卡 image_ref(复用屏卡体系;按 first_seen 精确定位——聚类屏
9595
// 屏号不唯一;无图 → 明确错误,前端禁用按钮)
96+
// v0.12.0 M5 补完成:画面要点屏按会话类型分派(photo=OCR 屏;video=关键帧
97+
// 纯图屏)——框选截取入口与详情/预览同一分派,防屏卡定位错表
9698
let blocks = state.db.list_ocr_blocks(session_id).map_err(|e| e.to_string())?;
9799
let images_dir = session_images_dir(&state.data_dir, session_id);
98-
let screens = crate::screens::build_screens(&blocks, Some(&images_dir));
100+
let kind = state
101+
.db
102+
.get_session(session_id)
103+
.map_err(|e| e.to_string())?
104+
.and_then(|s| s.kind);
105+
let screens = crate::screens::build_view_screens(kind.as_deref(), session_id, &blocks, Some(&images_dir));
99106
let screen = screens
100107
.iter()
101108
.find(|s| s.first_seen_ms == first_seen_ms)

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,22 @@
99
use crate::layout_analyzer::FrameGrid;
1010

1111
/// 目标网格宽(格)。
12+
/// @ai-context: v0.12.0 M5 补完成:grid_from_bgra 无生产调用方(视频会话版面
13+
/// 分析随全帧 OCR 下线——ADR-023),仅 structure_capture 测试交叉
14+
/// 校验网格口径使用——dead_code 豁免(机制先行模式,image_caption 先例)。
15+
#[allow(dead_code)]
1216
const TARGET_COLS: u32 = 32;
1317
/// 目标网格高(格)。
18+
#[allow(dead_code)]
1419
const TARGET_ROWS: u32 = 18;
1520

1621
/// BGRA8 帧 → 版面分析网格(纯函数;尺寸/数据不匹配返回 None)。
1722
///
1823
/// @ai-context: 每格采样其中心像素的亮度(Rec.601:0.299R+0.587G+0.114B,
1924
/// BGRA 存储序 = [B,G,R,A]);网格尺寸不足时退化为 1 格。
25+
/// @ai-context: v0.12.0 M5 补完成:生产调用方随视频会话版面分析下线(ADR-023),
26+
/// 保留纯函数与测试(structure_capture 同口径交叉校验)。
27+
#[allow(dead_code)]
2028
pub fn grid_from_bgra(bgraw: &[u8], width: u32, height: u32) -> Option<FrameGrid> {
2129
if width == 0 || height == 0 || bgraw.len() != (width as usize) * (height as usize) * 4 {
2230
return None;

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,11 @@ impl SessionImageStore {
108108
/// @ai-context: 修复:双指纹去重与 save_frame 同口径——旧实现无去重,
109109
/// 视频进度条等静态误判区域每 tick 重复存图(会话 15 实测
110110
/// 49 张全同垃圾 crop,耗尽与 full 共享的 50 张预算)。
111+
/// @ai-context: v0.12.0 M5 补完成后无生产调用方(视频会话区域 OCR 下线——
112+
/// 唯一写入方 region_ocr_blocks 退役;crop/ 列表消费方
113+
/// commands_refine_inner 仍在),存储层保留(图文/结构场景
114+
/// 未来可复用;单测覆盖)——dead_code 豁免(机制先行模式)。
115+
#[allow(dead_code)]
111116
pub fn save_crop(
112117
&mut self,
113118
timestamp_ms: u64,

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

Lines changed: 61 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,10 @@ pub fn run_video_import<F: Fn(&ImportProgress)>(
8383
engines: &EnginePool,
8484
resolver: &FfmpegResolver,
8585
video_path: &str,
86-
// REQ-117:UI 垃圾黑名单(导入画面要点源头过滤——与实时链路同口径)
86+
// REQ-117:UI 垃圾黑名单(导入字幕源头过滤——与实时链路同口径)
8787
ui_junk: &crate::ui_junk::UiJunkList,
88+
// v0.12.0 M5 补完成:数据目录(session-images 图片库根)——关键帧纯图归档
89+
data_dir: &Path,
8890
progress: F,
8991
) -> Result<i64> {
9092
let video = Path::new(video_path);
@@ -105,11 +107,20 @@ pub fn run_video_import<F: Fn(&ImportProgress)>(
105107
kind: None,
106108
})?;
107109
let session_id = session.id;
110+
// v0.12.0 M5 补完成:视频导入关键帧纯图归档(存图不识别——与实时链路同口径;
111+
// 创建失败不阻断——降级为无图集会话,日志可观测)
112+
let mut image_store = crate::image_store::SessionImageStore::new(
113+
data_dir.join("session-images").join(session_id.to_string()),
114+
)
115+
.ok();
108116
// 中间文件限定系统临时目录(导入后即清理,不污染应用数据目录)
109117
let work_dir = std::env::temp_dir().join(format!("entropy-import-{}", session_id));
110118
let _ = std::fs::create_dir_all(&work_dir);
111119

112-
let result = run_import_inner(db, engines, resolver, video, session_id, &work_dir, ui_junk, &progress);
120+
let result = run_import_inner(
121+
db, engines, resolver, video, session_id, &work_dir, ui_junk,
122+
&mut image_store, &progress,
123+
);
113124
// 清理中间文件(无论成败);失败标记会话 failed(与实时链路同口径)
114125
let _ = std::fs::remove_dir_all(&work_dir);
115126
result.inspect_err(|_| {
@@ -119,7 +130,7 @@ pub fn run_video_import<F: Fn(&ImportProgress)>(
119130
}
120131

121132
/// 管线主体(与清理/失败标记分离,保证清理必然执行)。
122-
/// @ai-context: 参数为管线上下文传递(db/engines/resolver/路径/会话/黑名单),
133+
/// @ai-context: 参数为管线上下文传递(db/engines/resolver/路径/会话/黑名单/图片库),
123134
/// 聚合会破坏内聚——登记 clippy 豁免(与 persist_final 同模式)。
124135
#[allow(clippy::too_many_arguments)]
125136
fn run_import_inner<F: Fn(&ImportProgress)>(
@@ -130,6 +141,7 @@ fn run_import_inner<F: Fn(&ImportProgress)>(
130141
session_id: i64,
131142
work_dir: &Path,
132143
ui_junk: &crate::ui_junk::UiJunkList,
144+
image_store: &mut Option<crate::image_store::SessionImageStore>,
133145
progress: &F,
134146
) -> Result<()> {
135147
// 1) 字幕决策(L1 零依赖;L2 需 ffmpeg)
@@ -150,10 +162,12 @@ fn run_import_inner<F: Fn(&ImportProgress)>(
150162
crate::import_transcribe::transcribe_audio(db, engines, resolver, video, session_id, work_dir, progress)?;
151163
}
152164

153-
// 3) 关键帧 OCR(画面要点,独立于转写路径;ffmpeg 缺失跳过不阻断)
154-
// REQ-130(v0.7.0 M3):P4 无图短路——会话档案 disable_ocr 时跳过画面识别。
155-
// 导入路径当前 profile=None(默认档案,OCR 不跳过——零回归);播客类档案
156-
// 接入导入链路(前端传 profile)后自动生效(ASR-only 快速路径)。
165+
// 3) 关键帧处理(v0.12.0 M5 补完成:纯图归档 + 字幕 OCR;独立于转写路径;
166+
// ffmpeg 缺失跳过不阻断)
167+
// REQ-130(v0.7.0 M3):P4 无图短路——会话档案 disable_ocr 时跳过**字幕 OCR**
168+
// (画面要点 OCR 已随 ADR-023 下线,本门控只作用于字幕)。导入路径当前
169+
// profile=None(默认档案,OCR 不跳过——零回归);关键帧纯图归档恒执行
170+
// (存图不识别——与 disable_ocr 语义解耦)。
157171
let ocr_enabled = db
158172
.get_session(session_id)
159173
.ok()
@@ -164,17 +178,8 @@ fn run_import_inner<F: Fn(&ImportProgress)>(
164178
!crate::video_profile::profile_by_kind(kind).disable_ocr
165179
})
166180
.unwrap_or(true);
167-
if ocr_enabled {
168-
// REQ-117:导入画面要点过 UI 垃圾黑名单(与实时链路同口径)
169-
ocr_keyframes(engines, db, resolver, video, session_id, work_dir, ui_junk, progress);
170-
} else {
171-
progress(&ImportProgress {
172-
stage: "ocr".into(),
173-
message: "档案禁用画面识别(disable_ocr),跳过关键帧 OCR".into(),
174-
done: 1,
175-
total: 1,
176-
});
177-
}
181+
// REQ-117:字幕过 UI 垃圾黑名单(与实时链路同口径)
182+
ocr_keyframes(engines, db, resolver, video, session_id, work_dir, ui_junk, image_store, ocr_enabled, progress);
178183

179184
// 4) 完成
180185
db.finish_session(session_id)?;
@@ -202,8 +207,12 @@ fn write_subtitle_segments(db: &Db, session_id: i64, segments: &[crate::fusion::
202207
Ok(())
203208
}
204209

205-
/// 关键帧提取 + OCR(失败跳过不阻断;帧号按固定间隔换算时间戳)。
206-
/// @ai-context: 参数为管线上下文传递(引擎/DB/解析器/路径/会话/黑名单),
210+
/// 关键帧纯图归档 + 字幕 OCR(失败跳过不阻断;帧号按固定间隔换算时间戳)。
211+
/// @ai-context: v0.12.0 M5 补完成(ADR-023):关键帧**存图不识别**——不再做
212+
/// 画面要点 OCR(中部 region=full 下线);只做底部字幕带 OCR
213+
/// (region=subtitle,本地 OCR 只做字幕)。关键帧全量纯图归档
214+
/// 到 session-images/<id>(参考图集/精修图片理解同一数据源)。
215+
/// @ai-context: 参数为管线上下文传递(引擎/DB/解析器/路径/会话/黑名单/图片库),
207216
/// 登记 clippy 豁免(与 persist_final 同模式)。
208217
#[allow(clippy::too_many_arguments)]
209218
fn ocr_keyframes<F: Fn(&ImportProgress)>(
@@ -214,10 +223,13 @@ fn ocr_keyframes<F: Fn(&ImportProgress)>(
214223
session_id: i64,
215224
work_dir: &Path,
216225
ui_junk: &crate::ui_junk::UiJunkList,
226+
image_store: &mut Option<crate::image_store::SessionImageStore>,
227+
// REQ-130:disable_ocr 档案 → 跳过字幕 OCR(关键帧归档恒执行)
228+
ocr_enabled: bool,
217229
progress: &F,
218230
) {
219231
let Ok(paths) = resolver.resolve() else {
220-
progress(&ImportProgress { stage: "ocr".into(), message: "ffmpeg 缺失,跳过关键帧 OCR".into(), done: 1, total: 1 });
232+
progress(&ImportProgress { stage: "ocr".into(), message: "ffmpeg 缺失,跳过关键帧处理".into(), done: 1, total: 1 });
221233
return;
222234
};
223235
let frames_dir = work_dir.join("frames");
@@ -228,7 +240,7 @@ fn ocr_keyframes<F: Fn(&ImportProgress)>(
228240
&ffmpeg::extract_keyframes_args(video, &frames_dir, ffmpeg::KEYFRAME_FPS, ffmpeg::KEYFRAME_MAX_FRAMES),
229241
ffmpeg::default_timeout(),
230242
) {
231-
eprintln!("[Import] 关键帧提取失败(跳过画面识别): {}", e);
243+
eprintln!("[Import] 关键帧提取失败(跳过): {}", e);
232244
return;
233245
}
234246
let mut files: Vec<PathBuf> = std::fs::read_dir(&frames_dir)
@@ -245,23 +257,41 @@ fn ocr_keyframes<F: Fn(&ImportProgress)>(
245257
progress(&ImportProgress { stage: "ocr".into(), message: "无关键帧产出".into(), done: 1, total: 1 });
246258
return;
247259
}
248-
// TD-037:区域裁剪 → 识别 → 信息整合——中部(full)+ 底部(subtitle)两路,
249-
// 各区域帧间文本去重(静态画面不重复落库)
250-
let mut last_full: Vec<String> = Vec::new();
260+
// v0.12.0 M5 补完成:字幕区帧间文本去重(静态画面不重复落库);全帧文本
261+
// 去重随画面要点 OCR 一并下线(无 full 块再落库)
251262
let mut last_subtitle: Vec<String> = Vec::new();
252263
// v0.11.7:累计识别块数(进度消息「已识别 N 块文字」)
253264
let mut total_blocks = 0usize;
254265
for (i, path) in files.iter().enumerate() {
255266
let timestamp_ms = (i as u64) * FRAME_INTERVAL_MS;
256267
match image::open(path).map(|d| d.into_rgb8()) {
257-
Ok(img) => total_blocks += crate::import_frame::ocr_keyframe(
258-
db, engines, session_id, timestamp_ms, &img, &mut last_full, &mut last_subtitle, ui_junk,
259-
),
268+
Ok(img) => {
269+
// v0.12.0 M5 补完成:关键帧纯图归档(存图不识别——预算超限/去重
270+
// 由 image_store 管理;归档失败不阻断,日志可观测)
271+
if let Some(store) = image_store.as_mut() {
272+
let (w, h) = img.dimensions();
273+
let bgra = crate::structure_capture::rgb_to_bgra(&img);
274+
if let Err(e) = store.save_frame(timestamp_ms, &bgra, w, h) {
275+
eprintln!("[Import] 关键帧归档失败: {}", e);
276+
}
277+
}
278+
if ocr_enabled {
279+
total_blocks += crate::import_frame::ocr_keyframe_subtitles(
280+
db, engines, session_id, timestamp_ms, &img, &mut last_subtitle, ui_junk,
281+
);
282+
}
283+
}
260284
Err(e) => eprintln!("[Import] 关键帧解码失败(跳过): {}", e),
261285
}
262286
progress(&ImportProgress {
263287
stage: "ocr".into(),
264-
message: format!("画面识别 {}/{} · 已识别 {} 块文字", i + 1, total, total_blocks),
288+
message: format!(
289+
"关键帧归档 {}/{} · 已识别字幕 {} 块{}",
290+
i + 1,
291+
total,
292+
total_blocks,
293+
if ocr_enabled { "" } else { "(档案禁用字幕识别)" }
294+
),
265295
done: (i + 1) as u32,
266296
total,
267297
});
@@ -331,12 +361,14 @@ mod tests {
331361
// Arrange & Act:不存在的文件 → 可操作错误(不建会话)
332362
let db = crate::db::Db::open(":memory:").expect("mem db");
333363
let resolver = FfmpegResolver::dev();
364+
let tmp = std::env::temp_dir();
334365
let result = run_video_import(
335366
&db,
336367
&crate::engine::EnginePool::dummy(),
337368
&resolver,
338369
"不存在.mp4",
339370
&crate::ui_junk::UiJunkList::defaults(),
371+
&tmp,
340372
|_| {},
341373
);
342374
// Assert

0 commit comments

Comments
 (0)