fix(ocr): fix PaddleOCR crashes, add multi-image and auto-detect language

- Pin PaddlePaddle to 3.0.0 on ARM64 to fix segfault in PIR inference
  engine (3.1+ crashes on aarch64 Debian Bookworm)
- Fix text extraction for PaddleOCR 3.4.x result format (rec_texts)
- Add Node.js-level fallback chain (best -> balanced -> fast) when
  Python subprocess crashes
- Add multi-image OCR: processes all uploaded files sequentially with
  per-file progress and filename headers in combined output
- Convert input images to PNG via Sharp before OCR so HEIC, AVIF, WebP,
  TIFF all work transparently
- Implement real auto-detect language using Tesseract multi-lang script
  detection (analyzes Unicode ranges for Hangul, CJK, Kana, Latin)
- Default enhance to off (hurts clean digital images)
This commit is contained in:
Siddharth Kumar Sah
2026-04-12 23:46:39 +08:00
parent f2e17d2d44
commit 29fafd0722
6 changed files with 259 additions and 120 deletions
+57 -21
View File
@@ -103,30 +103,66 @@ export function registerOcr(app: FastifyInstance) {
}
: undefined;
const result = await extractText(
fileBuffer,
workspacePath,
{
quality,
language: settings.language,
enhance: settings.enhance,
},
onProgress,
);
// Fallback chain: best -> balanced -> fast
// PaddleOCR can crash with segfault on some platforms, so we retry
// with a lower quality tier at the Node.js level.
const fallbackChain: Array<"fast" | "balanced" | "best"> =
quality === "best"
? ["best", "balanced", "fast"]
: quality === "balanced"
? ["balanced", "fast"]
: ["fast"];
if (clientJobId) {
updateSingleFileProgress({
jobId: clientJobId,
phase: "complete",
percent: 100,
});
let lastError: unknown;
for (const tier of fallbackChain) {
try {
const result = await extractText(
fileBuffer,
workspacePath,
{
quality: tier,
language: settings.language,
enhance: settings.enhance,
},
onProgress,
);
if (clientJobId) {
updateSingleFileProgress({
jobId: clientJobId,
phase: "complete",
percent: 100,
});
}
return reply.send({
jobId,
filename,
text: result.text,
});
} catch (err) {
lastError = err;
const msg = err instanceof Error ? err.message : String(err);
// If the Python process crashed (segfault, dispatcher exit), try next tier
if (
msg.includes("exited unexpectedly") ||
msg.includes("exited with code") ||
msg.includes("Segmentation fault")
) {
request.log.warn(
{ toolId: "ocr", quality: tier, err },
`OCR ${tier} crashed, falling back`,
);
if (onProgress) onProgress(15, "Retrying...");
continue;
}
// Non-crash errors (validation, timeout) should not retry
throw err;
}
}
return reply.send({
jobId,
filename,
text: result.text,
});
// All tiers failed
throw lastError;
} catch (err) {
request.log.error({ err, toolId: "ocr" }, "OCR failed");
return reply.status(422).send({