feat: Erkennungs-Pipeline mit Barcode-Vorrang und OCR-Rueckfall
This commit is contained in:
@@ -0,0 +1,44 @@
|
|||||||
|
import { emptySpec, isUsable } from './spec.js';
|
||||||
|
import { decodePartNumber } from './pn-decoder.js';
|
||||||
|
import { extractFields } from './ocr-extract.js';
|
||||||
|
|
||||||
|
async function safely(fn, fallback) {
|
||||||
|
try {
|
||||||
|
return await fn();
|
||||||
|
} catch {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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), []);
|
||||||
|
|
||||||
|
let best = emptySpec();
|
||||||
|
for (const code of codes) {
|
||||||
|
const decoded = decodePartNumber(code);
|
||||||
|
if (isUsable(decoded)) {
|
||||||
|
return { spec: decoded, source: 'barcode', confidence: 'green' };
|
||||||
|
}
|
||||||
|
// Teilenummer merken, auch wenn das Schema unbekannt ist.
|
||||||
|
if (best.partNumber === null) best = decoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = await safely(() => deps.runOcr(frame), '');
|
||||||
|
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' };
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { test } from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { recognize } from '../src/pipeline.js';
|
||||||
|
|
||||||
|
const OCR_TEXT = '64GB 4DRx4 PC4-2400T-LD1-11-MC0 M386A8K40BM1-CRC4Y 1908';
|
||||||
|
|
||||||
|
const deps = ({ codes = [], text = '' }) => ({
|
||||||
|
decodeBarcodes: async () => codes,
|
||||||
|
runOcr: async () => text,
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Barcode mit bekannter Teilenummer ergibt gruen und ueberspringt OCR', async () => {
|
||||||
|
let ocrAufgerufen = false;
|
||||||
|
const result = await recognize({}, {
|
||||||
|
decodeBarcodes: async () => ['M386A8K40BM1-CRC4Y'],
|
||||||
|
runOcr: async () => { ocrAufgerufen = true; return ''; },
|
||||||
|
});
|
||||||
|
assert.equal(result.source, 'barcode');
|
||||||
|
assert.equal(result.confidence, 'green');
|
||||||
|
assert.equal(result.spec.capacityGb, 64);
|
||||||
|
assert.equal(ocrAufgerufen, false, 'OCR darf bei gruenem Barcode nicht laufen');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Barcode ohne bekannte Teilenummer faellt auf OCR zurueck', async () => {
|
||||||
|
const result = await recognize({}, deps({ codes: ['7325773'], text: OCR_TEXT }));
|
||||||
|
assert.equal(result.source, 'ocr');
|
||||||
|
assert.equal(result.confidence, 'yellow');
|
||||||
|
assert.equal(result.spec.capacityGb, 64);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('OCR-Ergebnis behaelt die Teilenummer aus dem Barcode bei', async () => {
|
||||||
|
const result = await recognize({}, deps({ codes: ['7325773'], text: '64GB 4DRx4 PC4-2400' }));
|
||||||
|
assert.equal(result.spec.partNumber, '7325773');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ohne Barcode und ohne brauchbaren Text ergibt rot', async () => {
|
||||||
|
const result = await recognize({}, deps({ codes: [], text: 'Made in Philippines' }));
|
||||||
|
assert.equal(result.source, 'none');
|
||||||
|
assert.equal(result.confidence, 'red');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('OCR-Fehler fuehrt zu rot statt zu einem Absturz', async () => {
|
||||||
|
const result = await recognize({}, {
|
||||||
|
decodeBarcodes: async () => [],
|
||||||
|
runOcr: async () => { throw new Error('tesseract nicht geladen'); },
|
||||||
|
});
|
||||||
|
assert.equal(result.confidence, 'red');
|
||||||
|
assert.equal(result.source, 'none');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Barcode-Fehler fuehrt nicht zum Abbruch, OCR uebernimmt', async () => {
|
||||||
|
const result = await recognize({}, {
|
||||||
|
decodeBarcodes: async () => { throw new Error('zxing nicht geladen'); },
|
||||||
|
runOcr: async () => OCR_TEXT,
|
||||||
|
});
|
||||||
|
assert.equal(result.source, 'ocr');
|
||||||
|
assert.equal(result.spec.capacityGb, 64);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user