72 lines
3.0 KiB
TypeScript
72 lines
3.0 KiB
TypeScript
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 }
|
||
|
||
function requireImage(args: Record<string, unknown>, nodeId: string): string {
|
||
for (const key of ["image_path", "imagePath", "file_path", "filePath", "path", "image", "pictureSource"]) {
|
||
const v = String(args[key] ?? "").trim();
|
||
if (v) return v;
|
||
}
|
||
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();
|
||
}
|
||
}
|
||
|
||
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;
|
||
});
|
||
|
||
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;
|
||
});
|
||
|
||
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"));
|
||
ctx.log("OCR 表格识别完成:" + rows.length + " 行", "info", node.id);
|
||
return rows;
|
||
}); |