Skip to content

Commit 9f71fdd

Browse files
committed
fix(ocr): 引擎就绪状态真实化——engine_ready 替代线程心跳判定
就绪清单/模型面板/设备设置/系统徽标四处改用 engine_ready(worker 加载成功/失败回写):此前 ocr_alive 线程心跳在引擎加载失败时仍为 true,OCR 全挂仍显示 ✓;未就绪时不再显示请求态后端(GPU (CUDA #0))冒充生效态。单测补充 engine_ready 缺省/往返/旧载荷兼容。
1 parent 208e7f3 commit 9f71fdd

8 files changed

Lines changed: 110 additions & 16 deletions

File tree

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ pub struct DiagSnapshot {
3232
pub ocr_failures: u64,
3333
pub ocr_backend: OcrBackend,
3434
pub ocr_fallback_reason: Option<String>,
35+
/// v0.12.1:OCR 引擎加载成功标志(false=加载中/失败——系统徽标据此告警)
36+
pub ocr_engine_ready: bool,
3537
}
3638

3739
/// 健康巡检(磁盘剩余 + 模型文件 + 引擎线程心跳)。
@@ -51,7 +53,9 @@ pub fn health_status(state: State<'_, AppState>) -> HealthSnapshot {
5153
};
5254
let (asr_alive, ocr_alive) = state.engines.liveness();
5355
// 模型完整性:流式四件套 + 离线 SenseVoice(审查修复——此前仅查流式,
54-
// sensevoice 缺失要到首次识别失败才能暴露;OCR 模型经 ModelScope 缓存路径不稳定,留日志侧)
56+
// sensevoice 缺失要到首次识别失败才能暴露)。OCR 模型由 oar-ocr 托管在
57+
// $OAR_HOME(默认 ~/.oar,非 app 模型目录)——就绪状态经 ocr_device_status
58+
// 的 engine_ready 暴露(加载成功才算就绪),不在此处查文件清单。
5559
// TD-2026-08-20-E/F 清偿:增查说话人(wespeaker)与标点恢复模型——
5660
// 此前就绪清单(ReadyCheckCard)不含这两项,缺失要到会话页才暴露
5761
let missing = {
@@ -103,6 +107,7 @@ pub fn diag_snapshot(state: State<'_, AppState>) -> DiagSnapshot {
103107
ocr_failures,
104108
ocr_backend: status.actual,
105109
ocr_fallback_reason: status.fallback_reason,
110+
ocr_engine_ready: status.engine_ready,
106111
}
107112
}
108113

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

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ pub struct OcrDeviceStatus {
5959
pub actual: OcrBackend,
6060
/// 回退原因(无回退为 None;如"运行时无 CUDA 支持")
6161
pub fallback_reason: Option<String>,
62+
/// 引擎是否加载成功(v0.12.1:true=加载成功可用;false=加载中或加载失败——
63+
/// 失败原因见 fallback_reason;线程心跳 alive ≠ 引擎就绪,前端不得混用)
64+
#[serde(default)]
65+
pub engine_ready: bool,
6266
/// 最近校准基准
6367
pub bench: Option<BenchResult>,
6468
/// 校准进行中标记(前端轮询用)
@@ -67,7 +71,15 @@ pub struct OcrDeviceStatus {
6771

6872
impl OcrDeviceStatus {
6973
pub fn new(mode: OcrDeviceMode, requested: OcrBackend, bench: Option<BenchResult>) -> Self {
70-
Self { mode, requested, actual: requested, fallback_reason: None, bench, calibrating: false }
74+
Self {
75+
mode,
76+
requested,
77+
actual: requested,
78+
fallback_reason: None,
79+
engine_ready: false,
80+
bench,
81+
calibrating: false,
82+
}
7183
}
7284
}
7385

@@ -241,5 +253,29 @@ mod tests {
241253
assert_eq!(s.requested, s.actual);
242254
assert!(!s.calibrating);
243255
assert!(s.fallback_reason.is_none());
256+
// v0.12.1:新建状态引擎未就绪(worker 加载成功后才置 true)
257+
assert!(!s.engine_ready);
258+
}
259+
260+
#[test]
261+
fn status_roundtrip_keeps_engine_ready() {
262+
// v0.12.1:engine_ready 缺省反序列化为 false(旧前端/旧 JSON 零回归);
263+
// 显式 false 往返一致
264+
let s = OcrDeviceStatus {
265+
mode: OcrDeviceMode::ForceCpu,
266+
requested: OcrBackend::Cpu,
267+
actual: OcrBackend::Cpu,
268+
fallback_reason: Some("请求后端 Cuda 构建失败".to_string()),
269+
engine_ready: false,
270+
bench: None,
271+
calibrating: false,
272+
};
273+
let json = serde_json::to_string(&s).unwrap();
274+
let back: OcrDeviceStatus = serde_json::from_str(&json).unwrap();
275+
assert_eq!(back, s);
276+
// 无 engine_ready 键的旧载荷 → false(不阻断反序列化)
277+
let legacy = r#"{"mode":"Auto","requested":{"Cuda":{"device_id":0}},"actual":{"Cuda":{"device_id":0}},"fallback_reason":null,"bench":null,"calibrating":false}"#;
278+
let from_legacy: OcrDeviceStatus = serde_json::from_str(legacy).unwrap();
279+
assert!(!from_legacy.engine_ready);
244280
}
245281
}

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,14 +149,22 @@ pub(crate) fn ocr_worker_loop(
149149
.unwrap_or_default()
150150
);
151151
if let Ok(mut s) = device_status.lock() {
152+
// v0.12.1:engine_ready 与 actual/fallback_reason 一起回写——
153+
// 前端就绪判定必须用 engine_ready,不得用线程心跳(活 ≠ 引擎就绪)
152154
s.actual = engine.backend;
155+
s.engine_ready = true;
153156
s.fallback_reason = engine.fallback_reason.clone();
154157
}
155158
}
156159
Err(e) => {
157160
eprintln!("[Engine] OCR 引擎加载失败: {}", e);
158161
if let Ok(mut s) = device_status.lock() {
159-
s.fallback_reason = Some(format!("OCR 引擎加载失败: {}", e));
162+
// v0.12.1:失败显式回写 engine_ready=false(加载中的初始值也是 false,
163+
// 重启场景仍显式置位,防未来复用旧状态);附可行动提示——
164+
// auto-download 已恢复,重启应用即重试 ModelScope 缓存命中/下载
165+
s.engine_ready = false;
166+
s.fallback_reason =
167+
Some(format!("OCR 引擎加载失败: {}(模型缺失或下载失败——重启应用自动重试)", e));
160168
}
161169
}
162170
}

‎app/src/components/ModelManagementPanel.tsx‎

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { invoke } from "@tauri-apps/api/core";
1616
import { listen } from "@tauri-apps/api/event";
1717
import StructureModelSetting from "./StructureModelSetting";
1818
// L11 去重:HealthSnapshot/StreamingModelStatus/DownloadProgress 单一定义源在 types/system.ts
19-
import type { DownloadProgress, HealthSnapshot, StreamingModelStatus } from "../types";
19+
import type { DownloadProgress, HealthSnapshot, OcrDeviceStatus, StreamingModelStatus } from "../types";
2020

2121
/** 说话人下载状态。 */
2222
interface SpeakerDownloadStatus {
@@ -31,6 +31,7 @@ const badge: React.CSSProperties = { fontSize: 11, padding: "1px 8px", borderRad
3131

3232
export default function ModelManagementPanel() {
3333
const [health, setHealth] = useState<HealthSnapshot | null>(null);
34+
const [ocr, setOcr] = useState<OcrDeviceStatus | null>(null);
3435
const [streaming, setStreaming] = useState<StreamingModelStatus | null>(null);
3536
const [streamProgress, setStreamProgress] = useState<DownloadProgress | null>(null);
3637
const [streamBusy, setStreamBusy] = useState(false);
@@ -41,12 +42,14 @@ export default function ModelManagementPanel() {
4142
const refresh = useCallback(async () => {
4243
setError("");
4344
try {
44-
const [h, s] = await Promise.all([
45+
const [h, s, o] = await Promise.all([
4546
invoke<HealthSnapshot>("health_status"),
4647
invoke<StreamingModelStatus>("asr_streaming_model_status"),
48+
invoke<OcrDeviceStatus>("ocr_device_status"),
4749
]);
4850
setHealth(h);
4951
setStreaming(s);
52+
setOcr(o);
5053
setSpeaker(await invoke<SpeakerDownloadStatus>("speaker_model_download_status"));
5154
} catch (e) {
5255
setError(`模型状态查询失败: ${e}`);
@@ -177,15 +180,19 @@ export default function ModelManagementPanel() {
177180
<span style={{ fontSize: 10.5, color: "#9ca3af" }}>脚本 scripts/download-punctuation.mjs 可安装</span>
178181
</div>
179182

180-
{/* OCR 模型(ModelScope 自动缓存) */}
183+
{/* OCR 模型(ModelScope 自动缓存)——v0.12.1:就绪判定用 engine_ready
184+
(引擎加载成功),不再用 ocr_alive 线程心跳(活 ≠ 就绪) */}
181185
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 9, flexWrap: "wrap" }}>
182186
<span style={{ fontSize: 11, width: 200 }}>OCR(PP-OCRv6)</span>
183-
{health?.ocr_alive ? (
187+
{ocr?.engine_ready ? (
184188
<span style={{ ...badge, background: "#f0fdfa", color: "#0d9488" }}>✓ 引擎运行中</span>
185189
) : (
186-
<span style={{ ...badge, background: "#fffbeb", color: "#b45309" }}>引擎未就绪</span>
190+
<span style={{ ...badge, background: "#fef2f2", color: "#dc2626" }}>✗ 引擎未就绪</span>
187191
)}
188192
<span style={{ fontSize: 10.5, color: "#9ca3af" }}>首次使用经 ModelScope 自动缓存</span>
193+
{!ocr?.engine_ready && (
194+
<span style={{ fontSize: 10.5, color: "#b45309" }}>重启应用自动重试;失败原因见设置页「OCR 推理设备」</span>
195+
)}
189196
</div>
190197

191198
{/* 结构模型(版面/表格/公式——原结构分析模型区段) */}

‎app/src/components/OcrDeviceSetting.tsx‎

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,23 @@ export function OcrDeviceSetting() {
137137
</div>
138138
{status && (
139139
<div style={{ fontSize: 11, lineHeight: 1.7, color: "#6b7280" }}>
140+
{!status.engine_ready && (
141+
<div
142+
style={{
143+
background: "#fef2f2",
144+
border: "1px solid #fecaca",
145+
borderRadius: 6,
146+
padding: "5px 8px",
147+
marginBottom: 6,
148+
color: "#b91c1c",
149+
}}
150+
>
151+
⚠ OCR 引擎未就绪:模型文件缺失或下载失败,重启应用自动重试(
152+
<span style={{ fontWeight: 600 }}>{backendLabel(status.actual)}</span> 请求未生效)
153+
</div>
154+
)}
140155
<div>
141-
当前后端:
156+
{status.engine_ready ? "当前后端:" : "请求后端:"}
142157
<b style={{ color: backendLabel(status.actual) === "CPU" ? "#6b7280" : "#0d9488" }}>
143158
{backendLabel(status.actual)}
144159
</b>
@@ -150,7 +165,10 @@ export function OcrDeviceSetting() {
150165
<div style={{ color: "#b45309" }}>⚠ {status.fallback_reason}</div>
151166
)}
152167
{status.bench && (
153-
<div>校准:CPU {status.bench.cpu_ms.toFixed(1)}ms / GPU {status.bench.gpu_ms.toFixed(1)}ms</div>
168+
<div style={{ color: status.engine_ready ? undefined : "#9ca3af" }}>
169+
校准:CPU {status.bench.cpu_ms.toFixed(1)}ms / GPU {status.bench.gpu_ms.toFixed(1)}ms
170+
{!status.engine_ready && "(历史基准,引擎恢复后重新检测)"}
171+
</div>
154172
)}
155173
</div>
156174
)}

‎app/src/components/ReadyCheckCard.tsx‎

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ interface OcrDeviceStatus {
2121
mode: string;
2222
actual: string | { Cuda: { device_id: number } };
2323
fallback_reason: string | null;
24+
/** v0.12.1:引擎加载成功标志(false=加载中/失败;就绪判定以此为准) */
25+
engine_ready: boolean;
2426
}
2527

2628
interface ReadyItem {
@@ -78,11 +80,15 @@ export default function ReadyCheckCard() {
7880
detail: missingPunct.length > 0 ? "未下载(无标点降级,不影响转写)" : undefined,
7981
},
8082
{
81-
label: `OCR 引擎(${ocrBackendLabel(ocr.actual)})`,
82-
ok: health.ocr_alive,
83-
warn: !health.ocr_alive ? false : !!ocr.fallback_reason,
84-
detail: !health.ocr_alive
85-
? "引擎未就绪"
83+
// v0.12.1:就绪判定由线程心跳改为 engine_ready(加载成功才算就绪;
84+
// 原 ocr_alive 在引擎加载失败时仍为 true——线程活着≠引擎可用,
85+
// 曾导致 OCR 全挂仍显示 ✓ 的误报);未就绪时标签不显示后端,
86+
// 避免"GPU (CUDA #0)"这类请求态冒充生效态
87+
label: `OCR 引擎(${ocr.engine_ready ? ocrBackendLabel(ocr.actual) : "未就绪"})`,
88+
ok: ocr.engine_ready,
89+
warn: !ocr.engine_ready ? false : !!ocr.fallback_reason,
90+
detail: !ocr.engine_ready
91+
? "引擎未就绪" + (ocr.fallback_reason ? `:${ocr.fallback_reason}` : "")
8692
: ocr.fallback_reason ?? undefined,
8793
},
8894
{

‎app/src/components/SystemStatusBadge.tsx‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ interface DiagSnapshot {
2020
ocr_failures: number;
2121
ocr_backend: string | { Cuda: { device_id: number } };
2222
ocr_fallback_reason: string | null;
23+
/** v0.12.1:OCR 引擎加载成功标志(false=加载中/失败——徽标告警依据) */
24+
ocr_engine_ready: boolean;
2325
}
2426

2527
function backendLabel(b: DiagSnapshot["ocr_backend"]): string {
@@ -53,7 +55,14 @@ export function SystemStatusBadge() {
5355
return () => clearInterval(timer);
5456
}, [refresh]);
5557

56-
const warn = health?.disk_warn || (health?.missing_models.length ?? 0) > 0 || health?.asr_alive === false || health?.ocr_alive === false;
58+
// v0.12.1:OCR 引擎加载失败(engine_ready=false)计入告警——此前仅看线程心跳,
59+
// 引擎挂了仍显示健康
60+
const warn =
61+
health?.disk_warn ||
62+
(health?.missing_models.length ?? 0) > 0 ||
63+
health?.asr_alive === false ||
64+
health?.ocr_alive === false ||
65+
diag?.ocr_engine_ready === false;
5766
const engineDown = health && (health.asr_alive === false || health.ocr_alive === false);
5867

5968
return (
@@ -103,6 +112,9 @@ export function SystemStatusBadge() {
103112
引擎心跳:ASR {health?.asr_alive ? "●" : "✗"} OCR {health?.ocr_alive ? "●" : "✗"}
104113
{engineDown && <b style={{ color: "#dc2626" }}>(引擎异常)</b>}
105114
</div>
115+
<div>
116+
OCR 引擎:{diag?.ocr_engine_ready ? "✓ 就绪" : <b style={{ color: "#dc2626" }}>未就绪(重启应用重试)</b>}
117+
</div>
106118
<hr style={{ border: "none", borderTop: "1px solid #f3f4f6", margin: "6px 0" }} />
107119
<div style={{ fontWeight: 600, marginBottom: 4 }}>诊断(开发期)</div>
108120
<div>OCR 缓存:命中 {diag?.ocr_cache_hits ?? 0} / 未中 {diag?.ocr_cache_misses ?? 0}({diag?.ocr_hit_rate.toFixed(0)}%)</div>

‎app/src/types/system.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ export interface OcrDeviceStatus {
5959
requested: OcrBackend;
6060
actual: OcrBackend;
6161
fallback_reason: string | null;
62+
/** v0.12.1:引擎加载成功标志(false=加载中/失败;就绪判定以此为准,非线程心跳) */
63+
engine_ready: boolean;
6264
bench: BenchResult | null;
6365
calibrating: boolean;
6466
}

0 commit comments

Comments
 (0)