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.
157 lines
7.3 KiB
JavaScript
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 };
|
|
}
|