mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
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:
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user