Skip to content

Commit 3ed0973

Browse files
committed
fix(ocr): 审查修复——AI复核配图口径+图匹配单次扫描
1 parent 765b62a commit 3ed0973

3 files changed

Lines changed: 56 additions & 4 deletions

File tree

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,9 @@ pub async fn review_text_filter(
190190
let db = state.db.clone();
191191
let ui_junk = state.ui_junk.clone();
192192
let guards = state.ai_guardrails.clone();
193+
// v0.7.3 审查修复:AI 复核预览与 preview_session_note/session_to_note 同口径——
194+
// 画面要点屏 attach 归档图 + 重建配图行(否则复核后预览配图消失,双出口不一致)
195+
let data_dir = state.data_dir.clone();
193196
let mock = std::env::var("AI_TEXT_FILTER_MOCK").map(|v| v == "1").unwrap_or(false);
194197
tauri::async_runtime::spawn_blocking(move || {
195198
let session = db
@@ -205,6 +208,11 @@ pub async fn review_text_filter(
205208
let ocr_blocks = db.list_ocr_blocks(session_id).map_err(|e| e.to_string())?;
206209
// ① 纯规则过滤(与 preview_session_note 同一管线)
207210
let mut result = filter_note(&session.title, &segments, &ocr_blocks, &ui_junk);
211+
// ①b v0.7.3 审查修复:画面要点配图(与 preview_session_note 同口径——
212+
// 归档图匹配 + 重建含图段落;目录缺失/无图 → 纯文本降级)
213+
let images_dir = data_dir.join("session-images").join(session_id.to_string());
214+
crate::screens::attach_images(&mut result.ocr_screens, &images_dir);
215+
crate::note_filter::refresh_screen_points(&mut result);
208216
// ② 边界候选(规则层判不了的段)
209217
let boundary = boundary_candidates(&result.kept);
210218
let cfg = AiTextFilterConfig::from_env();

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,8 @@ pub fn normalize(text: &str) -> String {
8888
.collect()
8989
}
9090

91-
/// CJK 统一表意文字(含扩展 A;与 note_filter 同口径)。
92-
pub(crate) fn is_cjk(c: char) -> bool {
91+
/// CJK 统一表意文字(含扩展 A;与 note_filter/outline 各自私有实现同口径)。
92+
fn is_cjk(c: char) -> bool {
9393
let u = c as u32;
9494
(0x4E00..=0x9FFF).contains(&u) || (0x3400..=0x4DBF).contains(&u)
9595
}
@@ -230,6 +230,8 @@ pub fn cluster_blocks_into_screens(
230230
/// 且 x 间隔 ≤ 字高×LINE_X_GAP_RATIO → 同行(文本直接拼接,无空格——
231231
/// 中文字符天然连续);否则换行。x 间隔为负(重叠)视为同行。
232232
/// @ai-context: 无 bbox 块降级:每块一行(按时间序,全 0 包围盒)。
233+
/// 混合场景(部分块 bbox 缺失)时无 bbox 块按 y=0 排最前——
234+
/// 顺序不完美但不丢内容(诚实降级,注释说明已知局限)。
233235
pub fn line_merge(blocks: &[ScreenBlockInput]) -> Vec<MergedLine> {
234236
let mut sorted: Vec<&ScreenBlockInput> = blocks.iter().collect();
235237
sorted.sort_by(|a, b| {

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

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,22 +72,64 @@ pub fn filter_usable_blocks(blocks: &[SessionOcrBlock], ui_junk: &UiJunkList) ->
7272
///
7373
/// @ai-context: 归档图按时间戳命名(full/{ts}.webp,image_store 约定);目录
7474
/// 缺失/无图 → 保持 None(前端无缩略图降级,不阻断)。
75+
/// @ai-context: 审查修复(2026-08-20):目录**一次扫描**建时间戳映射(原实现
76+
/// 每屏一次 read_dir——N 屏 = N 次目录遍历,会话多屏时浪费 IO)。
7577
pub fn attach_images(screens: &mut [SessionScreen], images_dir: &Path) {
78+
if screens.is_empty() {
79+
return;
80+
}
81+
let ts_list = list_full_image_timestamps(images_dir);
82+
if ts_list.is_empty() {
83+
return;
84+
}
7685
for s in screens.iter_mut() {
7786
if s.image_ref.is_none() {
78-
s.image_ref = match_image(images_dir, s.first_seen_ms);
87+
s.image_ref = match_timestamp(&ts_list, s.first_seen_ms);
7988
}
8089
}
8190
}
8291

92+
/// 归档 full 图时间戳列表(纯 IO,一次扫描)。
93+
fn list_full_image_timestamps(images_dir: &Path) -> Vec<u64> {
94+
let entries = std::fs::read_dir(images_dir.join("full")).ok();
95+
let mut ts: Vec<u64> = entries
96+
.into_iter()
97+
.flatten()
98+
.filter_map(|e| e.ok())
99+
.filter(|e| e.path().extension().is_some_and(|x| x == "webp"))
100+
.filter_map(|e| {
101+
e.file_name()
102+
.to_string_lossy()
103+
.trim_end_matches(".webp")
104+
.parse::<u64>()
105+
.ok()
106+
})
107+
.collect();
108+
ts.sort_unstable();
109+
ts
110+
}
111+
112+
/// 时间戳匹配(纯函数):取 ≤ 目标的最近者;无则取最早者(None=空列表)。
113+
fn match_timestamp(sorted: &[u64], target: u64) -> Option<String> {
114+
let ts = sorted
115+
.iter()
116+
.rev()
117+
.find(|&&t| t <= target)
118+
.copied()
119+
.or_else(|| sorted.first().copied())?;
120+
Some(format!("full/{}.webp", ts))
121+
}
122+
83123
/// 屏构建核心(纯编排,无 IO):块流 → 屏序列。
84124
fn build_screens_inner(session_id: i64, blocks: &[SessionOcrBlock]) -> Vec<SessionScreen> {
85125
let full: Vec<&SessionOcrBlock> =
86126
blocks.iter().filter(|b| b.region == "full" && !b.text.trim().is_empty()).collect();
87127
if full.is_empty() {
88128
return Vec::new();
89129
}
90-
// ① 有 screen_id 的新数据按屏号分组(同屏块时间连续落库——连续分组成立);
130+
// ① 有 screen_id 的新数据按屏号分组(同屏块时间连续落库——连续分组成立:
131+
// 在线 ScreenTracker 屏号单调递增,同一屏号绝不跨屏段出现;非连续同号
132+
// 仅可能来自外部写入(防御不变量注释,遇破坏时按独立屏处理不崩溃);
91133
// 无 screen_id 的旧数据单独聚类兜底
92134
let mut clusters: Vec<(Option<i64>, ScreenCluster)> = Vec::new();
93135
let mut grouped: Vec<(i64, Vec<&SessionOcrBlock>)> = Vec::new();

0 commit comments

Comments
 (0)