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.
126 lines
5.1 KiB
JavaScript
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' };
|
|
}
|