Skip to content

Commit 69d4fad

Browse files
committed
feat(classroom): P2-1 本地OCR骨架 + P2-2/3/5/6 集成文档
ocrService: PP-OCRv5 模型就绪检测 + local_ocr_recognize IPC + 降级契约(recognizeLocal 待模型环境联调) visionWorker: 本地OCR优先分支(可用时返回文本草稿,失败静默降级云端VLM零回归) 集成文档: 模型获取/推理联调清单/公式引擎/版面/VLM分类/区域监测安全评审路径
1 parent d29a79b commit 69d4fad

7 files changed

Lines changed: 201 additions & 0 deletions

File tree

‎client/electron/ai/index.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { registerTtsHandlers } from './handlers/ttsHandler.js';
1616
import { registerLocalAsrHandlers } from './local-asr/index.js';
1717
import { loadLocalAsrConfig } from './local-asr/config.js';
1818
import { registerVadHandlers } from './vad/sileroVadService.js';
19+
import { registerLocalOcrHandlers } from './local-ocr/ocrService.js';
1920

2021
// 导入所有 AI 功能模块
2122
import { feature as summarizeFeature } from './handlers/summarizeHandler.js';
@@ -102,6 +103,9 @@ export function registerAIHandlers(): void {
102103
// 注册本地 Silero VAD IPC handler(P0-2,主进程 onnxruntime 推理)
103104
registerVadHandlers();
104105

106+
// 注册本地 OCR IPC handler(P2-1,PP-OCRv5 onnxruntime 推理骨架)
107+
registerLocalOcrHandlers();
108+
105109
// 注册流式输出 IPC handler
106110
registerStreamHandler();
107111

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/**
2+
* 本地 OCR 服务(P2-1,主进程骨架)
3+
*
4+
* @ai-context: 用 onnxruntime-node 运行 PP-OCRv5 检测+识别模型(RapidOCR
5+
* ONNX 发行版),渲染进程经 IPC `local_ocr_recognize` 提交截图换取文本行。
6+
* 模型目录约定 userData/ocr-models/(det/rec 两个 ONNX),未放置/加载失败
7+
* 时 available=false,渲染进程回退云端 VLM(本地优先优雅降级)。
8+
* @ai-context EN: Local OCR service skeleton over onnxruntime-node with
9+
* PP-OCRv5 (RapidOCR ONNX release). Degrades to cloud VLM when the model
10+
* directory is absent.
11+
* @ai-context: ⚠️ 推理参数联调说明——det 输出阈值/rec 输入尺寸等参数依据
12+
* PP-OCRv5 官方推理代码编写,但模型文件未随仓库分发,本服务需在装有
13+
* 模型的开发环境联调验证后启用(见 docs/knowledge/solutions/
14+
* 2026-08-local-ocr-integration.md 的验证清单)。
15+
*/
16+
17+
import path from 'path';
18+
import fs from 'fs';
19+
import { app } from 'electron';
20+
import { safeHandle } from '../../ipcUtils.js';
21+
import { IPC_CHANNELS } from '../../ipc/channels.js';
22+
import { logger } from '../../logger.js';
23+
24+
// ================================================================
25+
// 模型路径与可用性
26+
// ================================================================
27+
28+
/** OCR 模型文件名(RapidOCR 官方 ONNX 发行版命名) */
29+
const OCR_MODEL_FILES = {
30+
det: 'ch_PP-OCRv5_det_infer.onnx',
31+
rec: 'ch_PP-OCRv5_rec_infer.onnx',
32+
dict: 'ppocr_keys_v1.txt',
33+
} as const;
34+
35+
/** 模型目录:userData/ocr-models/(用户手动放置,见集成文档) */
36+
function getOcrModelDir(): string {
37+
return path.join(app.getPath('userData'), 'ocr-models');
38+
}
39+
40+
/** 模型文件是否齐备 */
41+
export function isLocalOcrReady(): boolean {
42+
const dir = getOcrModelDir();
43+
return Object.values(OCR_MODEL_FILES).every((f) => fs.existsSync(path.join(dir, f)));
44+
}
45+
46+
// ================================================================
47+
// 推理(PP-OCRv5:det 文本框检测 → 裁剪 → rec 文本识别)
48+
// ================================================================
49+
50+
let _session: { det: unknown; rec: unknown } | null = null;
51+
let _loadFailed = false;
52+
53+
/** 懒加载 det/rec 会话(缺失/失败返回 false,渲染进程降级云端) */
54+
function ensureLoaded(): boolean {
55+
if (_session) return true;
56+
if (_loadFailed) return false;
57+
if (!isLocalOcrReady()) return false;
58+
try {
59+
// eslint-disable-next-line @typescript-eslint/no-require-imports
60+
const ort = require('onnxruntime-node') as typeof import('onnxruntime-node');
61+
const dir = getOcrModelDir();
62+
// 会话加载为异步,此处同步加载不可行——本骨架在首次 recognize 时
63+
// 异步初始化(见 recognizeLocal 内 initSessions)
64+
void ort;
65+
void dir;
66+
return true;
67+
} catch (err) {
68+
_loadFailed = true;
69+
logger.warn(`[LocalOCR] 加载失败: ${err}`);
70+
return false;
71+
}
72+
}
73+
74+
export interface OcrLine {
75+
text: string;
76+
/** 归一化文本框 [x0,y0,x1,y1](0-1 比例坐标) */
77+
box: [number, number, number, number];
78+
confidence: number;
79+
}
80+
81+
/**
82+
* 本地 OCR 识别入口。
83+
* ⚠️ 骨架状态:模型就绪检测与降级契约完整;det/rec 推理管线(图像预处理
84+
* → det 推理 → 框后处理 → 裁剪 → rec 推理 → CTC 字典解码)需在装有模型的
85+
* 开发环境按集成文档联调验证。当前返回 null(调用方走云端 VLM 降级)。
86+
*/
87+
export async function recognizeLocal(_imageBase64: string): Promise<OcrLine[] | null> {
88+
if (!ensureLoaded()) return null;
89+
// TODO(P2-1-联调):det/rec 推理管线(见文件头 @ai-context 与集成文档验证清单)
90+
return null;
91+
}
92+
93+
// ================================================================
94+
// IPC 注册
95+
// ================================================================
96+
97+
/** 注册本地 OCR IPC handler(ai/index.ts 统一调用) */
98+
export function registerLocalOcrHandlers(): void {
99+
safeHandle(
100+
IPC_CHANNELS.LOCAL_OCR_RECOGNIZE,
101+
async (_event, args: { imageBase64: string }) => {
102+
if (!args?.imageBase64) {
103+
return { available: false, lines: null };
104+
}
105+
const lines = await recognizeLocal(args.imageBase64);
106+
return { available: lines !== null, lines };
107+
},
108+
);
109+
logger.info('[LocalOCR] IPC handler registered (local_ocr_recognize)');
110+
}

‎client/electron/ipc/channels.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ export const IPC_CHANNELS = {
5252
// 本地 Silero VAD(主进程 onnxruntime 推理,P0-2)
5353
VAD_SILERO_PROCESS: 'vad_silero_process',
5454

55+
// 本地 OCR(PP-OCRv5 onnxruntime 推理,P2-1)
56+
LOCAL_OCR_RECOGNIZE: 'local_ocr_recognize',
57+
5558
// Path C 视频录制
5659
VIDEO_RECORD_START: 'video_record_start',
5760
VIDEO_RECORD_STOP: 'video_record_stop',

‎client/electron/preload.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,8 @@ const ALLOWED_CHANNELS = [
136136
'local_asr_stream_set_hotwords',
137137
// 本地 Silero VAD(主进程 onnxruntime 推理,P0-2)
138138
'vad_silero_process',
139+
// 本地 OCR(PP-OCRv5 onnxruntime 推理,P2-1)
140+
'local_ocr_recognize',
139141
// MCP 学习记忆服务器应用内授权开关
140142
'memory_server:get_consent',
141143
'memory_server:set_consent',

‎client/src/env.d.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ declare global {
102102
audio_capture_stop: () => Promise<{ success: boolean }>;
103103
/** P0-2: 本地 Silero VAD 推理(主进程 onnxruntime,16kHz Float32 PCM 块) */
104104
vad_silero_process: (args: { samples: ArrayBuffer; sampleRate?: number; reset?: boolean }) => Promise<{ probability: number | null; available: boolean }>;
105+
/** P2-1: 本地 OCR 识别(PP-OCRv5;模型未就绪时 available=false) */
106+
local_ocr_recognize: (args: { imageBase64: string }) => Promise<{ available: boolean; lines: Array<{ text: string; box: [number, number, number, number]; confidence: number }> | null }>;
105107
// ── A3 微进展叙述(新增) ──
106108
ai_progress_narrate: (args: { statsText: string; authToken?: string }) => Promise<{
107109
narrative: string;

‎client/src/lib/ai/visionWorker.ts‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,31 @@ export class VisionWorker implements PipelineWorker {
7373
// 将 ArrayBuffer 转为 base64
7474
const base64 = arrayBufferToBase64(screenshotData.imageBuffer);
7575

76+
// P2-1:本地 OCR 优先(PP-OCRv5,离线可用;模型未就绪/失败 → 云端 VLM 现状路径)。
77+
// 本地路径产出纯文本草稿(formulas/concepts 由 P2-2 公式引擎与 P2-4 VLM
78+
// 增强补齐),当前为骨架降级契约:recognizeLocal 未联调前恒返回 null
79+
if (window.electronAPI) {
80+
try {
81+
const ocr = await window.electronAPI.local_ocr_recognize({ imageBase64: base64 });
82+
if (ocr?.available && ocr.lines?.length) {
83+
return {
84+
text: ocr.lines.map((l) => l.text).join('\n'),
85+
confidence: 0.9,
86+
source: 'vision',
87+
model: 'local-ocr-ppocrv5',
88+
processingTimeMs: 0,
89+
capturedAt: screenshotData.timestamp ?? Date.now(),
90+
structured: {
91+
formulas: [], diagrams: [], keyPoints: [],
92+
codeBlocks: [], concepts: [], mode: 'local-ocr',
93+
},
94+
};
95+
}
96+
} catch {
97+
/* 本地 OCR 失败静默降级云端 */
98+
}
99+
}
100+
76101
// 从 metadata 中获取提取模式,或使用默认值
77102
const mode = (message.metadata?.visionMode as VisionExtractMode) ?? this.defaultMode;
78103

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# 本地识别引擎集成指南(P2-1 OCR / P2-2 公式 / P2-3 版面 / P2-5 VLM 分类 / P2-6 区域监测)
2+
3+
> 日期:2026-08-16
4+
> 状态:**代码骨架已就绪,推理管线待装有模型的环境联调验证**(模型文件未随仓库分发)
5+
> 原则:所有本地引擎均为可选增强——模型缺失/推理失败时自动降级云端 VLM(现状路径),零回归风险
6+
7+
---
8+
9+
## 一、P2-1 本地 OCR(PP-OCRv5 / RapidOCR ONNX)
10+
11+
### 已落地
12+
13+
- `client/electron/ai/local-ocr/ocrService.ts`:模型就绪检测(userData/ocr-models/ 目录约定)、IPC `local_ocr_recognize`、`recognizeLocal` 骨架(det/rec 推理管线标注 TODO(P2-1-联调))
14+
- `client/src/lib/ai/visionWorker.ts`:本地 OCR 优先分支(available+lines 时返回文本草稿,失败静默降级云端 VLM)
15+
- IPC 登记:`channels.ts` LOCAL_OCR_RECOGNIZE + preload 白名单 + env.d.ts 类型
16+
17+
### 待联调(验证清单)
18+
19+
1. 模型获取:RapidOCR 官方 ONNX 发行版(GitHub RapidAI/RapidOCR → 模型仓库 `ch_PP-OCRv5_det_infer.onnx` / `ch_PP-OCRv5_rec_infer.onnx` / `ppocr_keys_v1.txt`),放置到 `userData/ocr-models/`
20+
2. 推理管线:nativeImage 解码 PNG → det 预处理(等比缩放到 32 倍数、归一化)→ det 推理 → DB/框后处理(PP-OCRv5 det 输出格式以官方推理代码为准)→ 框排序裁剪 → rec 预处理(48 高等比宽)→ rec 推理 → CTC 字典解码(ppocr_keys_v1.txt)
21+
3. 验收:离线 fine 路径可用率 100%(模型就绪后);OCR 文本 CER 对比云端 VLM(P0-1 基线脚本)
22+
23+
---
24+
25+
## 二、P2-2 公式引擎 / P2-3 版面解析
26+
27+
### 集成路径(依赖 P2-1 完成)
28+
29+
- **公式(UniMERNet)**:onnx 导出(PyTorch → ONNX)→ `client/electron/ai/local-formula/` 服务(照 ocrService 模式)→ 版面定位公式框(P2-3)→ 识别 → 与 VLM 公式输出交叉校验
30+
- **版面(PP-StructureV3 layout)**:PicoDet layout ONNX → `client/electron/ai/local-layout/` 服务 → 分块结果注入 VisionWorker structured 字段
31+
- 市场选型依据:`docs/Foresight/ocr-vision-market-2025-2026.md`(§2/§3)
32+
33+
---
34+
35+
## 三、P2-5 VLM 内容分类
36+
37+
- 规则版分类已上线(P1-6 `contentClassifier.ts`,标题关键词 + 转写证据,分类驱动采样参数与步骤视图)
38+
- VLM 版:待本地 VLM(Ollama 多模态模型经既有 `callWithLocalFallback`)或网关 vision 端点就绪后,实现 `classifyContentByVision(frame)` 替换规则版(保留规则版为离线回退)
39+
- 验收:分类准确率 ≥90%(对比 P1-6 规则版 ≥80%)
40+
41+
---
42+
43+
## 四、P2-6 区域化监测 + 输入事件触发
44+
45+
- **区域化 OCR 监测**:依赖 P2-3 版面定位参数面板区域 → 定时 OCR 数值变化 → 参数变更记录(「曝光 +0.5」形态,注入 `LessonStep.paramChanges`,stepExtractor 已预留字段)
46+
- **输入事件触发截图**:系统级输入监听需单独安全评审(无感采集约束 `useSystemPicker: false`,隐私影响评估后默认关闭、用户授权启用);评审通过前不实施
47+
- 临时替代已上线:P1-7 指令句补帧(转写指令词触发强制补帧)+ P1-8 漏捕检测手动补截(C 键)覆盖「操作瞬间捕捉」的主路径
48+
49+
---
50+
51+
## 附:依赖与依据
52+
53+
- 选型报告:`docs/Foresight/ocr-vision-market-2025-2026.md`
54+
- 采集约束:`docs/Foresight/classroom-assistant-optimization-roadmap.md` §1.2(无感采集/本地优先)
55+
- 现有本地引擎模式参考:`client/electron/ai/vad/sileroVadService.ts`(P0-2,完整落地样例)

0 commit comments

Comments
 (0)