feat: OCR-Feldextraktion mit Abgleich gegen bekannte Werte

This commit is contained in:
vchuser
2026-07-28 14:46:52 +02:00
parent 4201643fc5
commit 9098a45aec
2 changed files with 111 additions and 0 deletions
+57
View File
@@ -0,0 +1,57 @@
import { emptySpec, KNOWN, matchKnown, normalizeToken } from './spec.js';
// Die Muster sind bewusst grosszuegig: Was sie einsammeln, wird
// anschliessend gegen die bekannten Werte abgeglichen. Ein Treffer,
// der dort nicht besteht, wird verworfen statt geraten.
const CAPACITY_PATTERN = /\b([0-9OQBSIL]{1,3})\s?GB\b/g;
// Kein \b am Ende: auf Etiketten folgt der Geschwindigkeit oft direkt
// ein Buchstabe (PC4-2400T), und dort gibt es keine Wortgrenze.
const SPEED_PATTERN = /\bPC4[-\s]?([0-9OQ]{4})/g;
const RANK_PATTERN = /\b([0-9OQ][DS]?R[Xx][0-9OQ])\b/g;
const PART_NUMBER_PATTERN = /\b([A-Z]{1,3}[0-9]{2,4}[A-Z0-9]{4,}(?:-[A-Z0-9]{2,6})?)\b/g;
const DATE_CODE_PATTERN = /(?:^|\s)([0-9]{4})(?=\s|$)/g;
function firstMatch(text, pattern, transform) {
pattern.lastIndex = 0;
let match;
while ((match = pattern.exec(text)) !== null) {
const value = transform(match[1]);
if (value !== null) return value;
}
return null;
}
/**
* Zerlegt den OCR-Rohtext eines Etiketts in Spec-Felder.
* Jeder Kandidat wird gegen die bekannten Werte geprueft; besteht er
* die Pruefung nicht, bleibt das Feld offen.
*/
export function extractFields(rawText) {
const spec = emptySpec();
const text = normalizeToken(rawText).replace(/\s+/g, ' ');
if (text === '') return spec;
spec.capacityGb = firstMatch(text, CAPACITY_PATTERN, (raw) =>
matchKnown(raw, KNOWN.capacityGb),
);
spec.speed = firstMatch(text, SPEED_PATTERN, (raw) =>
matchKnown(`PC4-${raw}`, KNOWN.speed),
);
spec.rank = firstMatch(text, RANK_PATTERN, (raw) => matchKnown(raw, KNOWN.rank));
spec.formFactor = firstMatch(text, /\b(U?L?R?S?O?DIMM)\b/g, (raw) =>
matchKnown(raw, KNOWN.formFactor),
);
// Teilenummer: der laengste Kandidat, der nicht der Seriennummer entspricht.
PART_NUMBER_PATTERN.lastIndex = 0;
const candidates = [...text.matchAll(PART_NUMBER_PATTERN)].map((m) => m[1]);
const withDash = candidates.filter((c) => c.includes('-'));
spec.partNumber = withDash[0] ?? null;
// Datumscode: vierstellige Zahl, die fuer sich allein steht.
DATE_CODE_PATTERN.lastIndex = 0;
const dateMatch = [...text.matchAll(DATE_CODE_PATTERN)].map((m) => m[1]);
spec.dateCode = dateMatch[0] ?? null;
return spec;
}
+54
View File
@@ -0,0 +1,54 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { extractFields } from '../src/ocr-extract.js';
const REFERENZ = `SAMSUNG
Made in Philippines
K136000908252DF287
64GB 4DRx4 PC4-2400T-LD1-11-MC0
M386A8K40BM1-CRC4Y S
1908`;
test('liest alle Felder aus dem Referenz-Etikett', () => {
const spec = extractFields(REFERENZ);
assert.equal(spec.capacityGb, 64);
assert.equal(spec.rank, '4DRx4');
assert.equal(spec.speed, 'PC4-2400');
assert.equal(spec.partNumber, 'M386A8K40BM1-CRC4Y');
assert.equal(spec.dateCode, '1908');
});
test('repariert eine Verwechslung in der Geschwindigkeit', () => {
const spec = extractFields('64GB 4DRx4 PC4-24O0T-LD1-11-MC0');
assert.equal(spec.speed, 'PC4-2400');
});
test('repariert eine Verwechslung in der Kapazitaet', () => {
const spec = extractFields('BGB 1Rx8 PC4-2400');
assert.equal(spec.capacityGb, 8, 'B wird als 8 gelesen');
assert.equal(spec.rank, '1Rx8');
});
test('erkennt die Bauform aus dem Klartext, wenn vorhanden', () => {
const spec = extractFields('32GB 2Rx4 PC4-2666 RDIMM');
assert.equal(spec.formFactor, 'RDIMM');
});
test('laesst Felder offen, die nicht im Text stehen', () => {
const spec = extractFields('Oracle PN: 7325773');
assert.equal(spec.capacityGb, null);
assert.equal(spec.speed, null);
assert.equal(spec.rank, null);
});
test('unbekannte Geschwindigkeit bleibt null statt falsch geraten', () => {
const spec = extractFields('64GB 4DRx4 PC4-9999');
assert.equal(spec.speed, null);
assert.equal(spec.capacityGb, 64);
});
test('leerer Text liefert ein leeres Spec', () => {
const spec = extractFields('');
assert.equal(spec.capacityGb, null);
assert.equal(spec.partNumber, null);
});