Files
ocr_scanner/src/pipeline.js
T
vchuser 37df87a324 Behandle Mehrdeutigkeit auch bei unverwertbaren Barcodes
Mehrere Barcodes ohne bekanntes Nummernschema, die sich in der
Teilenummer widersprechen, durften bisher nicht mehr Vorrang vor der
per OCR gelesenen Teilenummer erhalten als ein einzelner Treffer -
der erste gewann ungeprueft. Jetzt gilt derselbe Massstab wie bei
verwertbaren Treffern: Vorrang nur bei Eindeutigkeit (ein Code, oder
mehrere mit derselben - via canonical() verwechslungstolerant
verglichenen - Teilenummer). Widersprechen sie sich, stuetzt sich das
Ergebnis allein auf die Texterkennung.

Zusaetzlich: ungueltige @param-Zeile fuer deps in recognize() korrigiert.
2026-07-28 15:49:39 +02:00

126 lines
5.1 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.
* @returns {Promise<{spec: object, source: 'barcode'|'ocr'|'none', confidence: 'green'|'yellow'|'red'}>}
*/
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' };
}
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' };
}
// Mehrdeutigkeit zwischen verwertbaren Barcodes: nicht raten, Nutzer entscheidet.
return { spec: emptySpec(), source: 'none', confidence: 'red' };
}
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' };
}
return { spec: merged, source: 'none', confidence: 'red' };
}