Files
ocr_scanner/src/pipeline.js
T
TanerUslu b903b1c7aa OCR-Rohtext additiv zurückgeben, je Eintrag speichern und anzeigen
recognize() (pipeline.js) liefert zusätzlich den ungefilterten OCR-Rohtext
(rawText) zurück - leer, wenn keine Texterkennung lief. Rein additiv: die
Stapelzuordnung (session.js/spec.js, unangetastet) stützt sich weiterhin
ausschließlich auf die daraus abgeleiteten, verstandenen Spec-Felder.

main.js hängt den Rohtext nach dem Buchen als zusätzliche Eigenschaft an den
Eintrag (analog zu den bereits vorhandenen Barcode-Inhalten je Eintrag).
storage.js sichert/prüft/stellt entry.rawText nach demselben optionalen
Muster wie entry.codes wieder her - ein gesicherter Stand ohne dieses Feld
wird nicht verworfen.

In Funktion "Text erkennen" bleibt die Treffer-Rückmeldung jetzt stehen, bis
der Nutzer sie wegtippt, und zeigt den vollen erkannten Text (lesbar über
dasselbe Scroll-Muster wie die übrigen Vollbild-Overlays); in den beiden
Barcode-Funktionen bleibt es beim automatischen Ausblenden. Das
zurückgegebene Versprechen von showResult löst in jedem Fall - Zeitgeber,
Wegtippen, verdrängende neue Rückmeldung - genau einmal ein, damit die
Bedienung nie dauerhaft gesperrt bleibt. Die Sitzungsliste zeigt den Rohtext
zusätzlich je Eintrag zum Nachlesen.

Tests (pipeline.test.js, storage.test.js): rawText nur bei tatsächlich
gelaufener Texterkennung, unterschiedlicher Rohtext beeinflusst die
Stapelzuordnung nachweislich nicht, Rohtext übersteht Sichern/Laden inkl.
Abwärtskompatibilität zu einem Stand ohne dieses Feld.
2026-07-29 15:19:13 +02:00

157 lines
7.3 KiB
JavaScript

import { canonical, emptySpec, isUsable, specsCompatible } from './spec.js';
import { decodePartNumber } from './pn-decoder.js';
import { extractFields } from './ocr-extract.js';
// Die Barcode-Dekodierung selbst ist normalerweise schnell. Diese Grenze ist
// keine Leistungserwartung, sondern nur eine Absicherung gegen einen dauerhaft
// haengenden Adapter. Sie muss auch das einmalige Laden des WebAssembly-Moduls
// beim ersten Scan einer Sitzung abdecken, das auf einem Handy mehrere Sekunden
// dauern kann.
const DEFAULT_BARCODE_TIMEOUT_MS = 10000;
// Texterkennung kann auf einem Handy durchaus einige Sekunden dauern - die
// Obergrenze muss so grosszuegig sein, dass ein normaler Ablauf auf einem
// langsamen Handy sie nie erreicht.
const DEFAULT_OCR_TIMEOUT_MS = 20000;
const TIMED_OUT = Symbol('timed-out');
/**
* Wettlauf zwischen dem uebergebenen Versprechen und einem Zeitgeber. Ist der
* Zeitgeber schneller, wird TIMED_OUT geliefert, ohne dass der urspruengliche
* Aufruf abgebrochen wird - er darf spaeter noch einloesen, wir warten nur
* nicht mehr darauf. Der Zeitgeber wird in jedem Fall wieder aufgeraeumt,
* damit kein offener Timer den Node-Prozess am Beenden hindert.
*/
function withTimeout(promise, ms) {
let timer;
const timeout = new Promise((resolve) => {
timer = setTimeout(() => resolve(TIMED_OUT), ms);
});
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
}
async function safely(fn, fallback, timeoutMs) {
try {
const result = await withTimeout(fn(), timeoutMs);
return result === TIMED_OUT ? fallback : result;
} catch {
return fallback;
}
}
/** Prueft, ob alle Specs paarweise vertraeglich sind. */
function allCompatible(specs) {
for (let i = 0; i < specs.length; i++) {
for (let j = i + 1; j < specs.length; j++) {
if (!specsCompatible(specs[i], specs[j])) return false;
}
}
return true;
}
/**
* Barcode zuerst, OCR nur als Rueckfallebene.
* @param {object} deps Adapter- und Zeitgrenzen-Bundel: deps.decodeBarcodes und
* deps.runOcr sind erforderlich. deps.barcodeTimeoutMs und deps.ocrTimeoutMs
* sind optional und ueberschreiben die eingebauten Obergrenzen - fuer Tests,
* die einen haengenden Adapter simulieren, ohne tatsaechlich zu warten.
* deps.isKnownCode ist optional: eine Funktion (code: string) => boolean,
* mit der der Aufrufer prueft, ob ein roher Barcode-Inhalt bereits einem
* Stapel bekannt ist. Ist einer der gelesenen Codes bekannt, steht die
* Zuordnung fest und die Texterkennung wird uebersprungen. Fehlt die
* Funktion, verhaelt sich die Pipeline wie ohne diese Optimierung.
* @returns {Promise<{spec: object, source: 'barcode'|'ocr'|'none', confidence: 'green'|'yellow'|'red', codes: string[], rawText: string}>}
* codes sind die im Bild gelesenen rohen Barcode-Zeichenketten (koennen
* neben der Teilenummer z.B. auch eine Seriennummer enthalten) - zur
* Weitergabe an proposeAssignment/commitAssignment in session.js.
* rawText ist der ungefilterte Texterkennungs-Rohtext, rein additiv: er
* fliesst nirgends in die Stapelzuordnung ein (die stuetzt sich weiterhin
* ausschliesslich auf die aus ihm abgeleiteten, verstandenen Spec-Felder,
* siehe extractFields/ocr-extract.js) und dient allein der Anzeige fuer den
* Nutzer. Leer, wenn die Texterkennung in diesem Durchlauf nicht lief (z.B.
* weil ein gruener Barcode-Treffer sie ueberfluessig gemacht hat).
*/
export async function recognize(frame, deps) {
const barcodeTimeoutMs = deps.barcodeTimeoutMs ?? DEFAULT_BARCODE_TIMEOUT_MS;
const ocrTimeoutMs = deps.ocrTimeoutMs ?? DEFAULT_OCR_TIMEOUT_MS;
const decodedCodes = await safely(() => deps.decodeBarcodes(frame), [], barcodeTimeoutMs);
const codes = Array.isArray(decodedCodes) ? decodedCodes : [];
const usable = [];
const unusable = [];
for (const code of codes) {
const decoded = decodePartNumber(code);
if (isUsable(decoded)) {
usable.push(decoded);
} else {
unusable.push(decoded);
}
}
// Auch unter den nicht verwertbaren Barcodes (unbekanntes Nummernschema)
// gilt: Vorrang vor der gelesenen Teilenummer nur, wenn er eindeutig ist.
// Genau ein Code - oder mehrere mit uebereinstimmender Teilenummer - behaelt
// seinen Vorrang samt abgeleiteter Felder. Widersprechen sich mehrere
// untereinander, erhaelt keiner Vorrang; es wird nichts aus ihnen uebernommen.
let best = emptySpec();
const withPartNumber = unusable.filter((decoded) => decoded.partNumber !== null);
if (withPartNumber.length > 0) {
const first = withPartNumber[0];
const allSameCode = withPartNumber.every(
(decoded) => canonical(decoded.partNumber) === canonical(first.partNumber)
);
if (allSameCode) best = first;
}
if (usable.length === 1) {
return { spec: usable[0], source: 'barcode', confidence: 'green', codes, rawText: '' };
}
if (usable.length > 1) {
if (allCompatible(usable)) {
// Vertraeglichkeit unter verwertbaren Barcode-Treffern setzt eine
// uebereinstimmende Teilenummer voraus (decodePartNumber setzt sie fuer
// jede nicht-leere Eingabe, specsCompatible vergleicht sie mit) - und
// da alle uebrigen Felder aus derselben Teilenummer abgeleitet werden,
// sind vertraegliche Treffer ohnehin gleich. Es gibt nichts aufzufuellen,
// der erste Treffer genuegt.
return { spec: usable[0], source: 'barcode', confidence: 'green', codes, rawText: '' };
}
// Mehrdeutigkeit zwischen verwertbaren Barcodes: nicht raten, Nutzer entscheidet.
return { spec: emptySpec(), source: 'none', confidence: 'red', codes, rawText: '' };
}
// Ist einer der gelesenen Codes bereits einem Stapel bekannt, steht die
// Zuordnung fest (siehe proposeAssignment in session.js) - die bis zu
// zwanzig Sekunden dauernde Texterkennung waere reine Zeitverschwendung.
// Bei einem tatsaechlich neuen Modul (kein Code bekannt) laeuft sie wie
// bisher, denn dort liefert sie die lesbare Beschriftung des neuen Stapels.
if (typeof deps.isKnownCode === 'function' && codes.some((code) => deps.isKnownCode(code))) {
return { spec: best, source: 'barcode', confidence: 'green', codes, rawText: '' };
}
const ocrResult = await safely(() => deps.runOcr(frame), '', ocrTimeoutMs);
const text = typeof ocrResult === 'string' ? ocrResult : '';
const fromOcr = extractFields(text);
// Der Barcode ist die exaktere Quelle: seine Teilenummer gewinnt.
const merged = { ...fromOcr };
if (best.partNumber !== null) merged.partNumber = best.partNumber;
for (const field of ['capacityGb', 'formFactor', 'rank', 'speed']) {
if (merged[field] === null && best[field] !== null) merged[field] = best[field];
}
if (isUsable(merged)) {
return { spec: merged, source: 'ocr', confidence: 'yellow', codes, rawText: text };
}
// Zum Sortieren muss keine Kapazitaet bekannt sein - eine exakt gelesene
// Teilenummer genuegt, um ein Modul wiederzuerkennen. Sie ueberlebt bis
// hierher nur, wenn genau ein unverwertbarer Code sie lieferte oder
// mehrere sich einig waren (siehe 'best' oben) - bei widersprechenden
// Codes bleibt merged.partNumber null und die Vorsicht damit erhalten.
if (merged.partNumber !== null) {
return { spec: merged, source: 'barcode', confidence: 'green', codes, rawText: text };
}
return { spec: merged, source: 'none', confidence: 'red', codes, rawText: text };
}