Für ein Projekt brauchten wir eine OCR-Lösung, die PDF-Rechnungen automatisch auslesen kann. Unsere erste Implementierung mit Tesseract.js auf einem Raspberry Pi 4 (4GB) war funktional, aber schmerzhaft langsam: 56 Sekunden pro Dokument. I
import Tesseract from "tesseract.js";
import sharp from "sharp";
export async function extractText(
imageBuffer,
lang = "deu",
logger = () => {},
) {
const processedBuffer = await sharp(imageBuffer)
.grayscale()
.normalize()
.toBuffer();
const {
data: { text },
} = await Tesseract.recognize(processedBuffer, lang, { logger });
return text;
}
Das war für ein produktives System völlig inakzeptabel. Zeit für Optimierungen!
Der erste logische Schritt war der Wechsel von der JavaScript-Implementierung zur nativen C++-Version von Tesseract. Wir kompilierten Tesseract 5.3.0 direkt auf dem Pi aus dem Source Code mit ARM-Optimierungen:
wget https://github.com/tesseract-ocr/tesseract/archive/refs/tags/5.3.0.tar.gz
tar -xzf 5.3.0.tar.gz
cd tesseract-5.3.0
./autogen.sh
./configure --enable-static --disable-shared CXXFLAGS="-O3 -march=armv7-a"
make -j4
sudo make install
Unsere neue Implementierung ruft Tesseract über spawn()
auf:
import { spawn } from "child_process";
import { promises as fs } from "fs";
export async function extractText(
imageBuffer,
lang = "deu",
logger = () => {},
) {
const tempDir = "/tmp/ocr";
const tempId = Date.now().toString(36);
const inputPath = `${tempDir}/ocr_${tempId}.png`;
const outputPath = `${tempDir}/ocr_${tempId}`;
try {
const processedBuffer = await preprocessImage(imageBuffer);
await fs.writeFile(inputPath, processedBuffer);
const args = [
inputPath,
outputPath,
"-l",
lang,
"--oem",
"1", // LSTM OCR Engine
"--psm",
"6", // Uniform block of text
];
const text = await new Promise((resolve, reject) => {
const tesseract = spawn("tesseract", args);
tesseract.on("close", async (code) => {
if (code !== 0) {
reject(new Error(`Tesseract failed with code ${code}`));
return;
}
const content = await fs.readFile(`${outputPath}.txt`, "utf8");
resolve(content.trim());
});
tesseract.on("error", reject);
});
return text;
} finally {
// Cleanup temp files
await cleanup([inputPath, `${outputPath}.txt`]);
}
}
Ergebnis: 20 Sekunden - eine deutliche Verbesserung um fast 3x, aber immer noch zu langsam.
Als nächstes versuchten wir verschiedene System-Level Optimierungen:
# CPU Governor auf Performance
echo performance | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor
# GPU Memory reduzieren für mehr RAM
echo "gpu_mem=16" | sudo tee -a /boot/config.txt
# RAM-Disk für temp files
echo "tmpfs /tmp/ocr tmpfs defaults,size=256M 0 0" | sudo tee -a /etc/fstab
sudo mount -a
# Node.js Memory optimieren
node --max-old-space-size=512 --optimize-for-size server.js
Ergebnis: Leider brachten diese Optimierungen exakt nichts - immer noch 20+ Sekunden.
Um herauszufinden, wo die Zeit wirklich draufgeht, bauten wir ein detailliertes Profiling ein:
export async function extractTextWithProfiling(imageBuffer, lang = "deu") {
const startTime = Date.now();
const profile = {};
// 1. Bildinfo analysieren
const imageInfo = await sharp(imageBuffer).metadata();
console.log(
`Original: ${imageInfo.width}x${imageInfo.height}, ${Math.round(imageBuffer.length / 1024)}KB`,
);
// 2. Preprocessing Zeit messen
const preprocessStart = Date.now();
const processedBuffer = await preprocessImage(imageBuffer);
profile.preprocessingTime = Date.now() - preprocessStart;
// 3. Tesseract Zeit messen
const tesseractStart = Date.now();
// ... Tesseract ausführen
profile.tesseractTime = Date.now() - tesseractStart;
profile.totalTime = Date.now() - startTime;
console.log(
`Preprocessing: ${profile.preprocessingTime}ms (${Math.round((profile.preprocessingTime / profile.totalTime) * 100)}%)`,
);
console.log(
`Tesseract: ${profile.tesseractTime}ms (${Math.round((profile.tesseractTime / profile.totalTime) * 100)}%)`,
);
console.log(`TOTAL: ${profile.totalTime}ms`);
}
Das Profiling enthüllte das eigentliche Problem: Wir verarbeiteten riesige Bilder (3472x4624 Pixel, 4+ MB) ohne angemessene Größenreduzierung.
Der Game-Changer war aggressives Resizing kombiniert mit optimierten Tesseract-Parametern:
export async function preprocessImage(imageBuffer) {
return await sharp(imageBuffer)
.resize({ width: 800, fit: "inside", withoutEnlargement: true }) // ⭐ GAME CHANGER
.grayscale()
.normalize()
.png({ compressionLevel: 0 }) // Keine Komprimierung für Speed
.toBuffer();
}
export async function extractTextFast(
imageBuffer,
lang = "deu",
logger = () => {},
) {
const args = [
inputPath,
outputPath,
"-l",
lang,
"--oem",
"1",
"--psm",
"6",
"-c",
"debug_file=/dev/null", // Keine Debug-Ausgaben
];
// ... Rest der Implementierung
}
📈 PERFORMANCE PROFILE:
========================
📊 Original Image: 3472x4624 (4220KB)
⚡ Preprocessing: 457ms (12%)
💾 File Write: 6ms (0%)
🔤 Tesseract: 3188ms (87%)
📖 File Read: 2ms (0%)
🧹 Cleanup: 1ms (0%)
⏱️ TOTAL: 3661ms (4s)
Für den produktiven Einsatz optimierten wir auch die PM2-Konfiguration:
// ecosystem.config.cjs
module.exports = {
apps: [
{
name: "invoice-manager",
script: "./app.js",
env: {
NODE_ENV: "production",
PORT: 3001,
OCR_CACHE_DIR: "/tmp/ocr",
UV_THREADPOOL_SIZE: "2",
OMP_THREAD_LIMIT: "2",
},
node_args: "--max-old-space-size=512 --optimize-for-size",
max_memory_restart: "700M",
kill_timeout: 15000,
log_file: "./logs/app.log",
cron_restart: "0 3 * * *", // Nightly restart
},
],
};
Wichtiger Hinweis: Achtet darauf, dass alle Environment-Pfade existieren. TESSDATA_PREFIX
kann meist weggelassen werden, da Tesseract seine Standard-Pfade kennt.
Die Optimierung war ein voller Erfolg:
Gesamtverbesserung: 14x schneller! 🚀
Mit Frontend-Cropping (Benutzer wählt relevanten Textbereich aus) erwarten wir weitere Verbesserungen auf unter 2 Sekunden.
Claude durfte einen bescheidenen Satz am Ende einfügen :-) (nein ich kriege dafür kein Geld):
Dieser Artikel entstand in Zusammenarbeit mit Claude (Anthropic), der bei der Optimierung und Problemlösung geholfen hat. Ohne die systematische Herangehensweise und das Performance-Profiling wären wir wahrscheinlich lange bei der 20-Sekunden-Marke hängengeblieben.
Für Feedback bin ich immer dankbar. Gerne an jacob@derkuba.de
Viele Grüße
Euer Kuba
PS: Dieser Artikel wurde mit Hilfe KI sprachlich aufgehübscht.