diff --git a/src/pipeline.js b/src/pipeline.js new file mode 100644 index 0000000..0ed20dd --- /dev/null +++ b/src/pipeline.js @@ -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' }; +} diff --git a/test/pipeline.test.js b/test/pipeline.test.js new file mode 100644 index 0000000..c6aa516 --- /dev/null +++ b/test/pipeline.test.js @@ -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); +});