feat: OCR 后端切换飞桨——RapidOCR ONNX(@gutenye/ocr-node,PaddleOCR v4 模型全离线)

This commit is contained in:
yu committed 2026-10-29 14:13:50 +08:00
1 parent 06886c5b31
commit 3b0883c80f
3 files changed
+716 -47

No files matched your search

+41 -44
View File
@@ -1,9 +1,18 @@
import Tesseract from "tesseract.js";
import fs from "node:fs";
import { register } from "../engine/registry";
import { RuntimeError } from "../engine/types";
interface OcrWord { text: string; x: number; y: number; width: number; height: number; confidence: number }
interface RapidOcrLine { text: string; mean: number; box?: number[][] }
let ocrInstance: unknown = null;
async function getOcr(): Promise<{ detect: (image: string) => Promise<{ texts: RapidOcrLine[] }> }> {
if (ocrInstance) return ocrInstance as { detect: (image: string) => Promise<{ texts: RapidOcrLine[] }> };
const mod = await import("@gutenye/ocr-node");
const instance = await mod.default.create();
ocrInstance = instance;
return instance;
}
function requireImage(args: Record<string, unknown>, nodeId: string): string {
for (const key of ["image_path", "imagePath", "file_path", "filePath", "path", "image", "pictureSource"]) {
@@ -13,60 +22,48 @@ function requireImage(args: Record<string, unknown>, nodeId: string): string {
throw new RuntimeError("validation_error", "图片路径为空", { nodeId, componentId: nodeId, recommendation: "填写本地图片路径(png/jpg/bmp)" });
}
async function recognizeWithLayout(imagePath: string, nodeId: string): Promise<{ text: string; words: OcrWord[]; textLines: string[] }> {
if (!fs.existsSync(imagePath)) {
throw new RuntimeError("file_not_found", "图片不存在:" + imagePath, { nodeId, componentId: nodeId });
}
const worker = await Tesseract.createWorker("chi_sim+eng");
try {
const result = await worker.recognize(imagePath, {}, { text: true, blocks: true });
const data = result.data as {
text?: string;
blocks?: Array<{
paragraphs?: Array<{
lines?: Array<{
words?: Array<{ text: string; confidence: number; bbox: { x0: number; y0: number; x1: number; y1: number } }>;
}>;
}>;
}>;
};
const words: OcrWord[] = [];
const textLines: string[] = [];
for (const block of data.blocks ?? []) {
for (const para of block.paragraphs ?? []) {
for (const line of para.lines ?? []) {
const lineWords = line.words ?? [];
textLines.push(lineWords.map(w => w.text).join(" "));
for (const w of lineWords) {
words.push({ text: w.text, x: w.bbox.x0, y: w.bbox.y0, width: w.bbox.x1 - w.bbox.x0, height: w.bbox.y1 - w.bbox.y0, confidence: w.confidence });
}
}
}
}
return { text: (data.text ?? "").trim(), words, textLines };
} finally {
await worker.terminate();
}
function boxToRect(box: number[][] | undefined): { x: number; y: number; width: number; height: number } {
const points = box ?? [];
const xs = points.map(p => p[0]);
const ys = points.map(p => p[1]);
const minX = Math.min(...(xs.length ? xs : [0]));
const minY = Math.min(...(ys.length ? ys : [0]));
const maxX = Math.max(...(xs.length ? xs : [0]));
const maxY = Math.max(...(ys.length ? ys : [0]));
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
}
register("OcrOffline_GetOcrTextNew", async (ctx, node, args) => {
const imagePath = requireImage(args, node.id);
const result = await recognizeWithLayout(imagePath, node.id);
ctx.log("OCR 识别完成:" + result.text.length + " 字符 / " + result.words.length + " 词", "info", node.id);
return result.text;
const ocr = await getOcr();
const result = await ocr.detect(imagePath);
const text = result.texts.map(t => t.text).join("\n");
ctx.log("OCR 识别完成:" + text.length + " 字符 / " + result.texts.length + " 行", "info", node.id);
return text;
});
register("OcrOffline_GetOcrTextPosition", async (ctx, node, args) => {
const imagePath = requireImage(args, node.id);
const result = await recognizeWithLayout(imagePath, node.id);
ctx.log("OCR 定位完成:" + result.words.length + " 个词", "info", node.id);
return result.words;
const ocr = await getOcr();
const result = await ocr.detect(imagePath);
const words = result.texts.map(t => {
const rect = boxToRect(t.box);
return { text: t.text, x: rect.x, y: rect.y, width: rect.width, height: rect.height, confidence: t.mean };
});
ctx.log("OCR 定位完成:" + words.length + " 行", "info", node.id);
return words;
});
register("OcrOffline_GetOcrTable", async (ctx, node, args) => {
const imagePath = requireImage(args, node.id);
const result = await recognizeWithLayout(imagePath, node.id);
const rows = result.textLines.map(line => line.split(/ {2,}/).filter(Boolean).join("\t"));
const ocr = await getOcr();
const result = await ocr.detect(imagePath);
const sorted = [...result.texts].sort((a, b) => {
const ay = a.box ? Math.min(...a.box.map(p => p[1])) : 0;
const by = b.box ? Math.min(...b.box.map(p => p[1])) : 0;
return ay - by;
});
const rows = sorted.map(t => t.text);
ctx.log("OCR 表格识别完成:" + rows.length + " 行", "info", node.id);
return rows;
});