Fix pipeline: robust gegen unerwartete Adapter-Rueckgaben und mehrdeutige Barcodes

recognize() stuerzte ab, wenn decodeBarcodes() etwas anderes als eine Liste
zurueckgab (null/undefined/Objekt), da die for...of-Schleife nicht iterierbare
Werte nicht abfaengt. Nicht-Listen werden nun wie eine leere Liste behandelt,
ebenso wird eine nicht-string-wertige runOcr()-Rueckgabe wie leerer Text
behandelt.

Ausserdem gewann bei mehreren verwertbaren Barcodes im selben Bild bisher
ungeprueft der erste Treffer mit gruener Ampel. Jetzt werden alle verwertbaren
Codes gesammelt: genau einer bleibt gruen wie bisher, mehrere untereinander
vertraegliche werden zusammengefuehrt (spaeterer Treffer ueberschreibt kein
bereits gesetztes Feld) und bleiben gruen, mehrere unvertraegliche ergeben rot
mit leerem Spec statt eines geratenen Ergebnisses.

Korrigiert ausserdem den irrefuehrenden Warnkommentar in pn-tables.js: ein
falscher Tabelleneintrag faellt auf dem Barcode-Weg nicht automatisch auf, da
dort keine Texterkennung zum Abgleich laeuft.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
vchuser
2026-07-28 15:29:54 +02:00
co-authored by Claude Opus 5
parent 2d81f7b005
commit 4099687707
3 changed files with 126 additions and 6 deletions
+46 -4
View File
@@ -1,4 +1,4 @@
import { emptySpec, isUsable } from './spec.js';
import { emptySpec, isUsable, specsCompatible } from './spec.js';
import { decodePartNumber } from './pn-decoder.js';
import { extractFields } from './ocr-extract.js';
@@ -10,24 +10,66 @@ async function safely(fn, fallback) {
}
}
/**
* Fuehrt mehrere untereinander vertraegliche Specs zusammen: ein spaeterer
* Treffer fuellt nur Felder, die noch offen sind, ueberschreibt aber nie
* ein bereits gesetztes Feld.
*/
function mergeCompatibleSpecs(specs) {
const merged = { ...specs[0] };
for (let i = 1; i < specs.length; i++) {
for (const field of Object.keys(merged)) {
if (merged[field] === null && specs[i][field] !== null) {
merged[field] = specs[i][field];
}
}
}
return merged;
}
/** 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.
* @returns {Promise<{spec: object, source: 'barcode'|'ocr'|'none', confidence: 'green'|'yellow'|'red'}>}
*/
export async function recognize(frame, deps) {
const codes = await safely(() => deps.decodeBarcodes(frame), []);
const decodedCodes = await safely(() => deps.decodeBarcodes(frame), []);
const codes = Array.isArray(decodedCodes) ? decodedCodes : [];
let best = emptySpec();
const usable = [];
for (const code of codes) {
const decoded = decodePartNumber(code);
if (isUsable(decoded)) {
return { spec: decoded, source: 'barcode', confidence: 'green' };
usable.push(decoded);
continue;
}
// Teilenummer merken, auch wenn das Schema unbekannt ist.
if (best.partNumber === null) best = decoded;
}
const text = await safely(() => deps.runOcr(frame), '');
if (usable.length === 1) {
return { spec: usable[0], source: 'barcode', confidence: 'green' };
}
if (usable.length > 1) {
if (allCompatible(usable)) {
return { spec: mergeCompatibleSpecs(usable), 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), '');
const text = typeof ocrResult === 'string' ? ocrResult : '';
const fromOcr = extractFields(text);
// Der Barcode ist die exaktere Quelle: seine Teilenummer gewinnt.