Compare commits
4
Commits
3907204209
...
87ba439287
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87ba439287 | ||
|
|
b903b1c7aa | ||
|
|
1c10c68053 | ||
|
|
54b1447f21 |
@@ -80,9 +80,14 @@ keine Daten verlassen das Gerät.
|
|||||||
„QR-Code" sucht die App ohnehin laufend, ein Knopf ohne Texterkennung hätte
|
„QR-Code" sucht die App ohnehin laufend, ein Knopf ohne Texterkennung hätte
|
||||||
dort nichts zu tun. Der Zielrahmen hebt sich in „Strichcode" und „QR-Code"
|
dort nichts zu tun. Der Zielrahmen hebt sich in „Strichcode" und „QR-Code"
|
||||||
hervor, sobald ein passender Code im Bild ist; in „Text erkennen" bleibt er
|
hervor, sobald ein passender Code im Bild ist; in „Text erkennen" bleibt er
|
||||||
schlicht, weil dort nichts laufend erkannt wird. Ein Wechsel der Funktion
|
schlicht, weil dort nichts laufend erkannt wird. Trotzdem ist der Rahmen
|
||||||
während des Sortierens (z. B. bei gemischter Ware) gilt sofort: laufende
|
auch dort wirksam: „Modul scannen" liest genau den Bildausschnitt innerhalb
|
||||||
Suche startet oder stoppt, der Knopf erscheint oder verschwindet.
|
des Rahmens, in voller Kameraauflösung, nicht mehr das gesamte,
|
||||||
|
heruntergerechnete Kamerabild (`grabFrameRegion` in `src/camera.js` —
|
||||||
|
derselbe Ausschnitt, den auch die laufende Barcode-Suche benutzt). Ein
|
||||||
|
Wechsel der Funktion während des Sortierens (z. B. bei gemischter Ware)
|
||||||
|
gilt sofort: laufende Suche startet oder stoppt, der Knopf erscheint oder
|
||||||
|
verschwindet.
|
||||||
|
|
||||||
Liegt beim Start eine gesicherte Sitzung vor, erscheint zuerst die Frage
|
Liegt beim Start eine gesicherte Sitzung vor, erscheint zuerst die Frage
|
||||||
nach dem Fortsetzen (siehe Punkt 5 unten) und erst danach die Funktionswahl
|
nach dem Fortsetzen (siehe Punkt 5 unten) und erst danach die Funktionswahl
|
||||||
@@ -123,20 +128,41 @@ keine Daten verlassen das Gerät.
|
|||||||
3. **Texterkennung** (ausschließlich in „Text erkennen", ausgelöst durch
|
3. **Texterkennung** (ausschließlich in „Text erkennen", ausgelöst durch
|
||||||
„Modul scannen"). Anders als früher ist das kein automatischer Rückfall
|
„Modul scannen"). Anders als früher ist das kein automatischer Rückfall
|
||||||
vom Barcode-Weg mehr, sondern die einzige Quelle dieser Funktion — sie
|
vom Barcode-Weg mehr, sondern die einzige Quelle dieser Funktion — sie
|
||||||
liest ohnehin nie einen Barcode (siehe Punkt 0). Das Kamerabild wird dafür
|
liest ohnehin nie einen Barcode (siehe Punkt 0). Gelesen wird der
|
||||||
|
Bildausschnitt innerhalb des Zielrahmens, in voller Kameraauflösung
|
||||||
|
(`grabFrameRegion`, siehe Punkt 0) — nicht mehr das ganze, auf 1280 Pixel
|
||||||
|
heruntergerechnete Kamerabild. Das Bild wird dafür
|
||||||
in Graustufen gewandelt und mit einem **Otsu-Schwellwert** in Schwarz/Weiß
|
in Graustufen gewandelt und mit einem **Otsu-Schwellwert** in Schwarz/Weiß
|
||||||
aufbereitet: Der Schwellwert wird aus der Helligkeitsverteilung des
|
aufbereitet: Der Schwellwert wird aus der Helligkeitsverteilung des
|
||||||
gesamten Bildes bestimmt, nicht aus einer festen Kontrastspreizung
|
gesamten Bildes bestimmt, nicht aus einer festen Kontrastspreizung
|
||||||
zwischen hellstem und dunkelstem Pixel. Das macht die Aufbereitung robust
|
zwischen hellstem und dunkelstem Pixel. Das macht die Aufbereitung robust
|
||||||
gegen einzelne Lichtreflexe auf glänzenden Metalletiketten, die eine reine
|
gegen einzelne Lichtreflexe auf glänzenden Metalletiketten, die eine reine
|
||||||
Min/Max-Spreizung leicht kippen würden. Das OCR-Ergebnis wird anschließend
|
Min/Max-Spreizung leicht kippen würden. Die Zeichen-Whitelist von
|
||||||
gegen die bekannten Spec-Werte abgeglichen, was die typischen
|
Tesseract (`tessedit_char_whitelist` in `src/ocr.js`) lässt Groß- **und**
|
||||||
Verwechslungen (0/O, 1/I, 8/B, …) auflöst. Da in dieser Funktion nie eine
|
Kleinbuchstaben, Ziffern sowie die auf Etiketten üblichen Satz- und
|
||||||
per Barcode gelesene Teilenummer vorliegt, entscheiden hier ausschließlich
|
Sonderzeichen zu — nicht mehr nur Großbuchstaben. Das OCR-Ergebnis wird
|
||||||
die aus dem erkannten Text abgeleiteten technischen Angaben (Kapazität,
|
anschließend gegen die bekannten Spec-Werte abgeglichen, was die
|
||||||
Bauform, Rank, Geschwindigkeit) über die Stapelzuordnung
|
typischen Verwechslungen (0/O, 1/I, 8/B, …) auflöst; das kann durch die
|
||||||
(`specsCompatible` in `src/spec.js`) — anders als in „Strichcode"/
|
breitere Whitelist bei den Codeteilen (Kapazität, Rank, Geschwindigkeit,
|
||||||
„QR-Code", wo meist die exakte Teilenummer trägt.
|
Teilenummer) etwas an Genauigkeit kosten, weil Tesseract jetzt z. B.
|
||||||
|
zwischen „O“ und „o“ unterscheiden muss — eine bewusst in Kauf genommene
|
||||||
|
Abwägung, damit der vollständige Etikettentext lesbar wird. Da in dieser
|
||||||
|
Funktion nie eine per Barcode gelesene Teilenummer vorliegt, entscheiden
|
||||||
|
hier ausschließlich die aus dem erkannten Text abgeleiteten technischen
|
||||||
|
Angaben (Kapazität, Bauform, Rank, Geschwindigkeit) über die
|
||||||
|
Stapelzuordnung (`specsCompatible` in `src/spec.js`) — anders als in
|
||||||
|
„Strichcode"/„QR-Code", wo meist die exakte Teilenummer trägt.
|
||||||
|
|
||||||
|
Der vollständige, ungefilterte Erkennungstext (nicht nur die daraus
|
||||||
|
verstandenen Felder) wird zusätzlich mitgeführt (`recognize` in
|
||||||
|
`src/pipeline.js` liefert ihn als `rawText` zurück) und je Eintrag
|
||||||
|
gespeichert — rein additiv, ohne jeden Einfluss auf die Stapelzuordnung
|
||||||
|
(siehe „Grenzen" unten). In Funktion „Text erkennen" bleibt die
|
||||||
|
Treffer-Rückmeldung deshalb stehen, bis der Nutzer sie wegtippt, und zeigt
|
||||||
|
den vollen erkannten Text an, statt sich nach gut einer Sekunde von selbst
|
||||||
|
auszublenden wie in den beiden Barcode-Funktionen (`showResult` in
|
||||||
|
`src/ui/result-overlay.js`). Der Rohtext jedes Eintrags lässt sich später
|
||||||
|
auch in der Sitzungsliste nachlesen.
|
||||||
4. **Stapel-Zuweisung.** Grün (Barcode) und Gelb (OCR) laufen ohne Eingabe
|
4. **Stapel-Zuweisung.** Grün (Barcode) und Gelb (OCR) laufen ohne Eingabe
|
||||||
durch; nur bei roter Konfidenz (nichts Eindeutiges erkannt, widersprüchliche
|
durch; nur bei roter Konfidenz (nichts Eindeutiges erkannt, widersprüchliche
|
||||||
Barcodes oder widersprüchliche unbekannte Barcodes ohne jede OCR-Kapazität)
|
Barcodes oder widersprüchliche unbekannte Barcodes ohne jede OCR-Kapazität)
|
||||||
@@ -187,12 +213,15 @@ korrekt funktioniert.
|
|||||||
npm test
|
npm test
|
||||||
```
|
```
|
||||||
|
|
||||||
144 Tests, alle grün (Stand dieses Dokuments). Getestet werden ausschließlich
|
154 Tests, alle grün (Stand dieses Dokuments). Getestet werden ausschließlich
|
||||||
die reinen Module ohne Browser-Zugriff: Teilenummer-Decoder, Spec-Normalisierung
|
die reinen Module ohne Browser-Zugriff: Teilenummer-Decoder, Spec-Normalisierung
|
||||||
und toleranter Vergleich, OCR-Bildaufbereitung (Otsu-Schwellwert) und
|
und toleranter Vergleich, OCR-Bildaufbereitung (Otsu-Schwellwert) und
|
||||||
-Feldextraktion, Stapel-Zuweisung, die Erkennungs-Pipeline samt Zeitgrenzen
|
-Feldextraktion, Stapel-Zuweisung, die Erkennungs-Pipeline samt Zeitgrenzen,
|
||||||
und Mehrdeutigkeitsbehandlung, die Sitzungssicherung samt Prüfung eines
|
Mehrdeutigkeitsbehandlung und dem additiven OCR-Rohtext (`rawText` wird nur
|
||||||
wiederhergestellten Zustands, die drei Scan-Funktionen (`scan-modes.js` —
|
zurückgegeben, wenn die Texterkennung tatsächlich lief, und beeinflusst
|
||||||
|
nachweislich nie die Stapelzuordnung), die Sitzungssicherung samt Prüfung
|
||||||
|
eines wiederhergestellten Zustands (einschließlich `entry.rawText`), die drei
|
||||||
|
Scan-Funktionen (`scan-modes.js` —
|
||||||
Codearten je Funktion, DataMatrix/Aztec/PDF417 in „QR-Code") sowie die je Funktion
|
Codearten je Funktion, DataMatrix/Aztec/PDF417 in „QR-Code") sowie die je Funktion
|
||||||
tatsächlich aufgerufenen Adapter (`scan-recognition.js` — Texterkennung läuft
|
tatsächlich aufgerufenen Adapter (`scan-recognition.js` — Texterkennung läuft
|
||||||
nachweislich nie in „Strichcode"/„QR-Code", Barcode-Dekodierung nachweislich
|
nachweislich nie in „Strichcode"/„QR-Code", Barcode-Dekodierung nachweislich
|
||||||
@@ -263,19 +292,19 @@ worauf du bei der Veröffentlichung achten solltest.
|
|||||||
| `src/pn-decoder.js` | Teilenummer → Spec-Felder |
|
| `src/pn-decoder.js` | Teilenummer → Spec-Felder |
|
||||||
| `src/ocr-extract.js` | OCR-Rohtext → Spec-Felder |
|
| `src/ocr-extract.js` | OCR-Rohtext → Spec-Felder |
|
||||||
| `src/session.js` | Stapel halten, vorschlagen, buchen, umsortieren, zurücknehmen — inklusive der aus den Einträgen abgeleiteten, je Stapel bekannten Barcode-Inhalte (`stack.codes`), die vor dem Vergleich der technischen Angaben über die Zuordnung entscheiden, sofern diese Angaben nicht widersprechen |
|
| `src/session.js` | Stapel halten, vorschlagen, buchen, umsortieren, zurücknehmen — inklusive der aus den Einträgen abgeleiteten, je Stapel bekannten Barcode-Inhalte (`stack.codes`), die vor dem Vergleich der technischen Angaben über die Zuordnung entscheiden, sofern diese Angaben nicht widersprechen |
|
||||||
| `src/pipeline.js` | Barcode → Teilenummer → OCR → Ampelfarbe, inkl. Zeitgrenzen, Mehrdeutigkeitsbehandlung und Kurzschluss über einen bereits bekannten Barcode-Inhalt (`deps.isKnownCode`) — unverändert; kennt keine Scan-Funktionen |
|
| `src/pipeline.js` | Barcode → Teilenummer → OCR → Ampelfarbe, inkl. Zeitgrenzen, Mehrdeutigkeitsbehandlung und Kurzschluss über einen bereits bekannten Barcode-Inhalt (`deps.isKnownCode`) — kennt keine Scan-Funktionen; liefert zusätzlich (additiv) den ungefilterten OCR-Rohtext (`rawText`), leer, wenn keine Texterkennung lief |
|
||||||
| `src/scan-modes.js` | Die drei Scan-Funktionen (Strichcode, QR-Code, Text erkennen): Kennung, deutsche Beschriftung, gelesene Codearten, laufende Suche ja/nein, Texterkennung ja/nein — einzige Quelle, sowohl für die Auswahl-Oberfläche als auch für `main.js` |
|
| `src/scan-modes.js` | Die drei Scan-Funktionen (Strichcode, QR-Code, Text erkennen): Kennung, deutsche Beschriftung, gelesene Codearten, laufende Suche ja/nein, Texterkennung ja/nein — einzige Quelle, sowohl für die Auswahl-Oberfläche als auch für `main.js` |
|
||||||
| `src/scan-recognition.js` | Baut die an `recognize()` übergebenen Adapter anhand der gewählten Funktion: in „Strichcode"/„QR-Code" liefert die Texterkennung sofort leeren Text ohne Tesseract anzustoßen, in „Text erkennen" liefert die Barcode-Dekodierung sofort eine leere Liste ohne `zxing-wasm` anzustoßen |
|
| `src/scan-recognition.js` | Baut die an `recognize()` übergebenen Adapter anhand der gewählten Funktion: in „Strichcode"/„QR-Code" liefert die Texterkennung sofort leeren Text ohne Tesseract anzustoßen, in „Text erkennen" liefert die Barcode-Dekodierung sofort eine leere Liste ohne `zxing-wasm` anzustoßen |
|
||||||
| `src/storage.js` | Absturzschutz der laufenden Sitzung: sichert und lädt aus `localStorage`, verwirft beim Laden jeden in sich unstimmigen Zustand vollständig (siehe unten) |
|
| `src/storage.js` | Absturzschutz der laufenden Sitzung: sichert und lädt aus `localStorage`, verwirft beim Laden jeden in sich unstimmigen Zustand vollständig (siehe unten); `entry.rawText` ist wie `entry.codes` optional und wird bei Fehlen als leerer Rohtext wiederhergestellt |
|
||||||
| `src/camera.js` | Kamerastart, Einzelbildaufnahme, Bilddatei-Ersatzweg |
|
| `src/camera.js` | Kamerastart, Einzelbildaufnahme (herunterskaliert, für die Dateiauswahl), Rahmenausschnitt in voller Auflösung (`grabFrameRegion`, für die laufende Barcode-Suche **und** für „Modul scannen" in „Text erkennen"), Bilddatei-Ersatzweg |
|
||||||
| `src/barcode.js` | Adapter zu `zxing-wasm`; `decodeBarcodes(imageData, formats)` bekommt die Codearten übergeben, ohne Angabe wie bisher (Code128 + DataMatrix) |
|
| `src/barcode.js` | Adapter zu `zxing-wasm`; `decodeBarcodes(imageData, formats)` bekommt die Codearten übergeben, ohne Angabe wie bisher (Code128 + DataMatrix) |
|
||||||
| `src/ocr.js` | Bildaufbereitung (Otsu) und Adapter zu `tesseract.js` |
|
| `src/ocr.js` | Bildaufbereitung (Otsu) und Adapter zu `tesseract.js`; Zeichen-Whitelist deckt Groß- und Kleinbuchstaben, Ziffern sowie übliche Etiketten-Sonderzeichen ab |
|
||||||
| `src/ui/scan-view.js` | Scan-Ansicht: Kamera-Vorschau, antippbare Anzeige der gewählten Funktion (Wechsel), Scan-Knopf (nur in „Text erkennen" sichtbar), Stapel-Leiste, „Zuletzt"-Zeile, Ersatzweg-Schaltfläche bei fehlender Kamera |
|
| `src/ui/scan-view.js` | Scan-Ansicht: Kamera-Vorschau, antippbare Anzeige der gewählten Funktion (Wechsel), Scan-Knopf (nur in „Text erkennen" sichtbar), Stapel-Leiste, „Zuletzt"-Zeile, Ersatzweg-Schaltfläche bei fehlender Kamera |
|
||||||
| `src/ui/mode-dialog.js` | Vollbild-Auswahl der Scan-Funktion vor dem Start und beim Wechsel: drei große Flächen, eine je Funktion aus `src/scan-modes.js` |
|
| `src/ui/mode-dialog.js` | Vollbild-Auswahl der Scan-Funktion vor dem Start und beim Wechsel: drei große Flächen, eine je Funktion aus `src/scan-modes.js` |
|
||||||
| `src/ui/result-overlay.js` | Kurze Treffer-Rückmeldung (grün/gelb), blendet sich nach kurzer Zeit selbst wieder aus |
|
| `src/ui/result-overlay.js` | Treffer-Rückmeldung (grün/gelb): blendet sich in „Strichcode"/„QR-Code" nach kurzer Zeit selbst aus; in „Text erkennen" bleibt sie stehen, zeigt den vollen OCR-Rohtext und wartet auf eine Eingabe, bevor sie schließt |
|
||||||
| `src/ui/ambiguous-dialog.js` | Rot-Dialog bei roter Konfidenz oder mehrdeutiger Stapelzuordnung |
|
| `src/ui/ambiguous-dialog.js` | Rot-Dialog bei roter Konfidenz oder mehrdeutiger Stapelzuordnung |
|
||||||
| `src/ui/resume-dialog.js` | Dialog beim Start: gesicherte Sitzung fortsetzen oder verwerfen — läuft vor der Funktionswahl |
|
| `src/ui/resume-dialog.js` | Dialog beim Start: gesicherte Sitzung fortsetzen oder verwerfen — läuft vor der Funktionswahl |
|
||||||
| `src/ui/session-list.js` | Sitzungsliste: Stapel-Übersicht, Umsortieren, Entfernen, Sitzung beenden |
|
| `src/ui/session-list.js` | Sitzungsliste: Stapel-Übersicht, Umsortieren, Entfernen, Sitzung beenden, Anzeige des vollständigen OCR-Rohtexts je Eintrag (sofern vorhanden) |
|
||||||
| `src/ui/describe-spec.js` | Gemeinsame Kurzbeschreibung eines Specs (Kapazität, Rank, Geschwindigkeit, Bauform), wahlweise mit Platzhaltern für fehlende Felder; von Treffer-Rückmeldung, „Zuletzt"-Zeile und Sitzungsliste gemeinsam genutzt |
|
| `src/ui/describe-spec.js` | Gemeinsame Kurzbeschreibung eines Specs (Kapazität, Rank, Geschwindigkeit, Bauform), wahlweise mit Platzhaltern für fehlende Felder; von Treffer-Rückmeldung, „Zuletzt"-Zeile und Sitzungsliste gemeinsam genutzt |
|
||||||
| `src/main.js` | Verdrahtung aller Module zur lauffähigen App |
|
| `src/main.js` | Verdrahtung aller Module zur lauffähigen App |
|
||||||
| `src/styles.css` | Farbvariablen (Grün/Gelb/Rot der Ampel-Rückmeldung), Layout des Kamera-Vollbilds samt Zielrahmen, Stapel-Leiste und Aktionsknöpfe sowie die Overlays für Treffer-Rückmeldung, Rot-Dialog und Sitzungsliste |
|
| `src/styles.css` | Farbvariablen (Grün/Gelb/Rot der Ampel-Rückmeldung), Layout des Kamera-Vollbilds samt Zielrahmen, Stapel-Leiste und Aktionsknöpfe sowie die Overlays für Treffer-Rückmeldung, Rot-Dialog und Sitzungsliste |
|
||||||
@@ -309,6 +338,14 @@ Zeichenketten); ein gesicherter Stand aus einer Fassung vor dieser Erweiterung
|
|||||||
ohne diese Felder gilt dagegen nicht als unstimmig, sondern wird beim Laden
|
ohne diese Felder gilt dagegen nicht als unstimmig, sondern wird beim Laden
|
||||||
als Stapel bzw. Eintrag ohne bekannte Codes wiederhergestellt.
|
als Stapel bzw. Eintrag ohne bekannte Codes wiederhergestellt.
|
||||||
|
|
||||||
|
Nach demselben Muster ist `entry.rawText` (der volle OCR-Rohtext, siehe
|
||||||
|
„Funktionsweise" oben) optional: Ist er vorhanden, muss er eine Zeichenkette
|
||||||
|
sein; ein gesicherter Stand aus einer Fassung vor dieser Erweiterung ohne
|
||||||
|
dieses Feld gilt nicht als unstimmig und wird beim Laden als Eintrag ohne
|
||||||
|
Rohtext (leere Zeichenkette) wiederhergestellt. Anders als `stack.codes` wird
|
||||||
|
`entry.rawText` beim Laden nicht neu abgeleitet — er hängt an genau diesem
|
||||||
|
Eintrag und fließt in keine Stapel-Berechnung ein.
|
||||||
|
|
||||||
## Grenzen
|
## Grenzen
|
||||||
|
|
||||||
- Keine Bestandsführung über Sitzungen hinweg, kein Export. Die Sicherung in
|
- Keine Bestandsführung über Sitzungen hinweg, kein Export. Die Sicherung in
|
||||||
@@ -346,6 +383,13 @@ als Stapel bzw. Eintrag ohne bekannte Codes wiederhergestellt.
|
|||||||
Validierung braucht deshalb echte Module, keine Code-Prüfung.
|
Validierung braucht deshalb echte Module, keine Code-Prüfung.
|
||||||
- Farb- und Schriftgestaltung ist bewusst schlicht gehalten und kann nach dem
|
- Farb- und Schriftgestaltung ist bewusst schlicht gehalten und kann nach dem
|
||||||
ersten Einsatz am Tisch nachgezogen werden.
|
ersten Einsatz am Tisch nachgezogen werden.
|
||||||
|
- Der volle OCR-Rohtext (`rawText`) dient ausschließlich der Anzeige für den
|
||||||
|
Nutzer (Treffer-Rückmeldung, Sitzungsliste). Die Stapelzuordnung stützt
|
||||||
|
sich bewusst weiterhin ausschließlich auf die daraus abgeleiteten,
|
||||||
|
verstandenen Spec-Felder — roher Erkennungstext schwankt zwischen
|
||||||
|
Aufnahmen (Zeilenumbrüche, zusätzlich erkannte Wörter, Groß-/
|
||||||
|
Kleinschreibung, …) und würde als Gruppierungsmerkmal für praktisch jedes
|
||||||
|
Modul einen eigenen Stapel erzeugen.
|
||||||
|
|
||||||
## Herstellertabellen erweitern
|
## Herstellertabellen erweitern
|
||||||
|
|
||||||
@@ -404,7 +448,11 @@ werden:
|
|||||||
halten: ebenfalls Treffer.
|
halten: ebenfalls Treffer.
|
||||||
- „Text erkennen" wählen: kein automatisches Hervorheben des Rahmens (er
|
- „Text erkennen" wählen: kein automatisches Hervorheben des Rahmens (er
|
||||||
bleibt durchgehend schlicht), „Modul scannen"-Knopf sichtbar und löst die
|
bleibt durchgehend schlicht), „Modul scannen"-Knopf sichtbar und löst die
|
||||||
Texterkennung aus.
|
Texterkennung aus. Der Rahmen ist dort trotzdem wirksam: nur der
|
||||||
|
Bildausschnitt innerhalb des Rahmens wird gelesen (nicht das ganze
|
||||||
|
Kamerabild) — mit Text außerhalb des Rahmens auf demselben Etikett prüfen,
|
||||||
|
dass er **nicht** erkannt wird, während Text innerhalb des Rahmens
|
||||||
|
vollständig erscheint.
|
||||||
- Anzeige der gewählten Funktion in der Scan-Ansicht antippen: Auswahl
|
- Anzeige der gewählten Funktion in der Scan-Ansicht antippen: Auswahl
|
||||||
erscheint erneut, nach der Wahl gilt der Wechsel sofort — laufende Suche
|
erscheint erneut, nach der Wahl gilt der Wechsel sofort — laufende Suche
|
||||||
startet/stoppt, Knopf erscheint/verschwindet, **ohne** die Seite neu zu
|
startet/stoppt, Knopf erscheint/verschwindet, **ohne** die Seite neu zu
|
||||||
@@ -418,7 +466,21 @@ werden:
|
|||||||
- In Funktion „Text erkennen" ein Etikett fotografieren und „Modul scannen"
|
- In Funktion „Text erkennen" ein Etikett fotografieren und „Modul scannen"
|
||||||
antippen: die Otsu-Aufbereitung liefert auf einem echten, glänzenden
|
antippen: die Otsu-Aufbereitung liefert auf einem echten, glänzenden
|
||||||
Etikettenfoto tatsächlich brauchbaren Text (bisher nur an synthetischen
|
Etikettenfoto tatsächlich brauchbaren Text (bisher nur an synthetischen
|
||||||
Testbildern geprüft, nie an einem echten Foto).
|
Testbildern geprüft, nie an einem echten Foto). Prüfen, dass die
|
||||||
|
Rückmeldung dabei den **vollständigen** erkannten Text zeigt (Groß- und
|
||||||
|
Kleinbuchstaben, nicht nur die von der App verstandenen Felder wie
|
||||||
|
`32GB 2Rx4 PC4-2666`), stehen bleibt, bis sie weggetippt wird, und bei
|
||||||
|
langem Text lesbar bleibt (per Scrollen erreichbar, wächst nicht aus dem
|
||||||
|
Bildschirm heraus). Danach in der Sitzungsliste nachsehen, dass derselbe
|
||||||
|
Rohtext beim Eintrag zum Nachlesen erscheint.
|
||||||
|
- Da der Bildausschnitt für die Texterkennung jetzt deutlich größer ist als
|
||||||
|
das bisher übergebene, auf 1280 Pixel herunterskalierte Vollbild
|
||||||
|
(`grabFrameRegion` statt `grabFrame`, siehe „Funktionsweise"), dauert ein
|
||||||
|
einzelner Erkennungsversuch länger als vorher — beobachten, wie lange ein
|
||||||
|
normaler Scan auf einem durchschnittlichen Handy tatsächlich braucht, und
|
||||||
|
ob er dabei spürbar unter der 20-Sekunden-Zeitgrenze der Pipeline bleibt
|
||||||
|
(siehe dazu auch den folgenden Punkt zur Ladezeit des Tesseract-Arbeiters,
|
||||||
|
die zusätzlich in dieselbe Zeitgrenze fällt).
|
||||||
- Ladezeit des WASM-Barcode-Moduls (`zxing-wasm`) beim allerersten Scan einer
|
- Ladezeit des WASM-Barcode-Moduls (`zxing-wasm`) beim allerersten Scan einer
|
||||||
Sitzung in Funktion „Strichcode" oder „QR-Code" beobachten — bleibt sie
|
Sitzung in Funktion „Strichcode" oder „QR-Code" beobachten — bleibt sie
|
||||||
deutlich unter der 10-Sekunden-Zeitgrenze der Pipeline, und braucht ein
|
deutlich unter der 10-Sekunden-Zeitgrenze der Pipeline, und braucht ein
|
||||||
@@ -440,8 +502,11 @@ werden:
|
|||||||
Ladevorgang dauerhaft "verfügbar", springt nach einem erzwungenen
|
Ladevorgang dauerhaft "verfügbar", springt nach einem erzwungenen
|
||||||
Fehlschlag (z. B. Flugmodus beim ersten Laden) beim nächsten Erfolg wieder
|
Fehlschlag (z. B. Flugmodus beim ersten Laden) beim nächsten Erfolg wieder
|
||||||
darauf zurück?
|
darauf zurück?
|
||||||
- Stichprobe, ob die feste Zeichen-Whitelist der Texterkennung auf echten
|
- Stichprobe, ob die Zeichen-Whitelist der Texterkennung (Groß-/
|
||||||
Etiketten keine tatsächlich benötigten Zeichen ausschließt.
|
Kleinbuchstaben, Ziffern, übliche Satz-/Sonderzeichen) auf echten Etiketten
|
||||||
|
keine tatsächlich benötigten Zeichen ausschließt, und ob die Erkennung der
|
||||||
|
Codeteile (Kapazität, Rank, Geschwindigkeit, Teilenummer) durch die jetzt
|
||||||
|
mögliche Groß-/Kleinschreibung noch verlässlich genug bleibt.
|
||||||
- Funktion „Strichcode"/„QR-Code": kein passender Code im Bild führt zu
|
- Funktion „Strichcode"/„QR-Code": kein passender Code im Bild führt zu
|
||||||
Rot-Dialog (statt Fehler oder Absturz) — hier ohne jeden OCR-Versuch.
|
Rot-Dialog (statt Fehler oder Absturz) — hier ohne jeden OCR-Versuch.
|
||||||
Funktion „Text erkennen": kein verwertbarer OCR-Text führt ebenfalls zu
|
Funktion „Text erkennen": kein verwertbarer OCR-Text führt ebenfalls zu
|
||||||
@@ -461,9 +526,15 @@ werden:
|
|||||||
es passend.
|
es passend.
|
||||||
- Stapel-Leiste (`A: 12`, `B: 4`) aus rund einem Meter Entfernung mit einer
|
- Stapel-Leiste (`A: 12`, `B: 4`) aus rund einem Meter Entfernung mit einer
|
||||||
Hand lesbar.
|
Hand lesbar.
|
||||||
- Grüne/gelbe Rückmeldung erscheint nach dem Scan und verschwindet nach
|
- In „Strichcode"/„QR-Code": Grüne/gelbe Rückmeldung erscheint nach dem Scan
|
||||||
rund 1200 ms von selbst; bei zwei Scans deutlich unter 1200 ms Abstand
|
und verschwindet nach rund 1200 ms von selbst; bei zwei Scans deutlich
|
||||||
bleibt jeweils nur eine Rückmeldung sichtbar und die App bleibt bedienbar.
|
unter 1200 ms Abstand bleibt jeweils nur eine Rückmeldung sichtbar und die
|
||||||
|
App bleibt bedienbar.
|
||||||
|
- In „Text erkennen": Rückmeldung bleibt stehen, bis sie über die
|
||||||
|
„weiter"-Schaltfläche (mindestens 56px hoch) weggetippt wird — kein
|
||||||
|
automatisches Ausblenden. Ein weiterer Scan, bevor die vorherige
|
||||||
|
Rückmeldung weggetippt wurde, ersetzt sie sofort durch die neue, ohne die
|
||||||
|
App dauerhaft zu sperren.
|
||||||
- Alle Bedienflächen (Scan-, Rückgängig-, Stapel-Knöpfe) mit dem Daumen
|
- Alle Bedienflächen (Scan-, Rückgängig-, Stapel-Knöpfe) mit dem Daumen
|
||||||
erreichbar, während die andere Hand das Modul hält.
|
erreichbar, während die andere Hand das Modul hält.
|
||||||
- Rückgängig-Knopf: Screenreader liest "Letzten Scan zurücknehmen" vor.
|
- Rückgängig-Knopf: Screenreader liest "Letzten Scan zurücknehmen" vor.
|
||||||
|
|||||||
+22
-4
@@ -1,6 +1,6 @@
|
|||||||
// Vollstaendige Verdrahtung der App: Kamera, Barcode/OCR-Erkennung, Sitzung
|
// Vollstaendige Verdrahtung der App: Kamera, Barcode/OCR-Erkennung, Sitzung
|
||||||
// und Oberflaeche. Ersetzt das fruehere Sichtpruefungs-Geruest vollstaendig.
|
// und Oberflaeche. Ersetzt das fruehere Sichtpruefungs-Geruest vollstaendig.
|
||||||
import { startCamera, grabFrame, grabFrameRegion, imageDataFromFile } from './camera.js';
|
import { startCamera, grabFrameRegion, imageDataFromFile } from './camera.js';
|
||||||
import { decodeBarcodes } from './barcode.js';
|
import { decodeBarcodes } from './barcode.js';
|
||||||
import { runOcr, isOcrAvailable } from './ocr.js';
|
import { runOcr, isOcrAvailable } from './ocr.js';
|
||||||
import { recognize } from './pipeline.js';
|
import { recognize } from './pipeline.js';
|
||||||
@@ -50,7 +50,13 @@ let cameraUnavailableMessage = null;
|
|||||||
let currentModeId = null;
|
let currentModeId = null;
|
||||||
|
|
||||||
const view = renderScanView(app, {
|
const view = renderScanView(app, {
|
||||||
onCapture: () => processCapture(() => grabFrame(view.video)),
|
// "Modul scannen" ist nur in Funktion 3 ("Text erkennen") sichtbar (siehe
|
||||||
|
// applyMode()/setCaptureVisible) - der Zielrahmen suggeriert dem Nutzer dort
|
||||||
|
// einen Ausschnitt, den grabFrame() (Vollbild, herunterskaliert) bisher gar
|
||||||
|
// nicht einloeste. grabFrameRegion() liest genau den Rahmenausschnitt, in
|
||||||
|
// voller Aufloesung - derselbe Ausschnitt, den auch die Barcode-Dauersuche
|
||||||
|
// benutzt (siehe runSearchAttempt() weiter unten).
|
||||||
|
onCapture: () => processCapture(() => grabFrameRegion(view.video)),
|
||||||
onUndo: () => {
|
onUndo: () => {
|
||||||
// Waehrend eine Erkennung laeuft, darf kein Eintrag zurueckgenommen
|
// Waehrend eine Erkennung laeuft, darf kein Eintrag zurueckgenommen
|
||||||
// werden - sonst koennte die Rueckgaengig-Flaeche einen Eintrag treffen,
|
// werden - sonst koennte die Rueckgaengig-Flaeche einen Eintrag treffen,
|
||||||
@@ -150,7 +156,7 @@ async function processCapture(getFrame) {
|
|||||||
// selbst (pipeline.js) bleibt unveraendert und kennt keine Funktionen.
|
// selbst (pipeline.js) bleibt unveraendert und kennt keine Funktionen.
|
||||||
const { decodeBarcodes: decode, runOcr: recognizeText } =
|
const { decodeBarcodes: decode, runOcr: recognizeText } =
|
||||||
buildRecognitionAdapters(currentModeId, { decodeBarcodes, runOcr });
|
buildRecognitionAdapters(currentModeId, { decodeBarcodes, runOcr });
|
||||||
const { spec, source, confidence, codes } = await recognize(
|
const { spec, source, confidence, codes, rawText } = await recognize(
|
||||||
frame,
|
frame,
|
||||||
{ decodeBarcodes: decode, runOcr: recognizeText, isKnownCode },
|
{ decodeBarcodes: decode, runOcr: recognizeText, isKnownCode },
|
||||||
);
|
);
|
||||||
@@ -188,13 +194,25 @@ async function processCapture(getFrame) {
|
|||||||
if (targetSession !== session) return;
|
if (targetSession !== session) return;
|
||||||
|
|
||||||
const entry = commitAssignment(targetSession, spec, source, stackId, codes);
|
const entry = commitAssignment(targetSession, spec, source, stackId, codes);
|
||||||
|
// entry.rawText ist rein additiv (siehe recognize() in pipeline.js) -
|
||||||
|
// session.js kennt das Feld nicht und bleibt deshalb unangetastet; es wird
|
||||||
|
// hier von aussen an den bereits gebuchten Eintrag angehaengt, genau wie
|
||||||
|
// die Stapelzuordnung selbst nie vom Rohtext abhaengt.
|
||||||
|
entry.rawText = rawText;
|
||||||
view.setLast(describeEntry(spec, entry.stackId));
|
view.setLast(describeEntry(spec, entry.stackId));
|
||||||
syncView();
|
syncView();
|
||||||
|
|
||||||
// Nach einer manuellen Entscheidung im Rot-Dialog gibt es nichts mehr
|
// Nach einer manuellen Entscheidung im Rot-Dialog gibt es nichts mehr
|
||||||
// rueckzumelden - der Dialog selbst war die Rueckmeldung.
|
// rueckzumelden - der Dialog selbst war die Rueckmeldung.
|
||||||
if (confidence !== 'red') {
|
if (confidence !== 'red') {
|
||||||
await showResult(app, { stackId: entry.stackId, spec, confidence });
|
// Nur in Funktion 3 ("Text erkennen") ist der erkannte Text lang genug,
|
||||||
|
// dass er gelesen werden will - dort bleibt die Rueckmeldung deshalb
|
||||||
|
// stehen, bis der Nutzer sie wegtippt, statt sich nach kurzer Zeit von
|
||||||
|
// selbst auszublenden (siehe showResult()). In den beiden Barcode-
|
||||||
|
// Funktionen zaehlt Tempo, dort bleibt es beim automatischen Ausblenden.
|
||||||
|
await showResult(app, {
|
||||||
|
stackId: entry.stackId, spec, confidence, rawText, autoHide: currentModeId !== 'ocr',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
failed = true;
|
failed = true;
|
||||||
|
|||||||
+13
-2
@@ -117,8 +117,19 @@ function getWorker() {
|
|||||||
workerPromise = createWorker('eng')
|
workerPromise = createWorker('eng')
|
||||||
.then(async (worker) => {
|
.then(async (worker) => {
|
||||||
await worker.setParameters({
|
await worker.setParameters({
|
||||||
// Etiketten enthalten nur Grossbuchstaben, Ziffern und wenige Sonderzeichen.
|
// Der Nutzer will den vollstaendigen Etikettentext sehen, nicht nur
|
||||||
tessedit_char_whitelist: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-/. x',
|
// die von der App verstandenen Felder (die ohnehin nur Grossbuch-
|
||||||
|
// staben, Ziffern und wenige Zeichen brauchen) - deshalb Gross- UND
|
||||||
|
// Kleinbuchstaben, Ziffern sowie die auf Etiketten ueblichen Satz-
|
||||||
|
// und Sonderzeichen. Das kann die Genauigkeit bei den Codeteilen
|
||||||
|
// (Kapazitaet, Rank, Geschwindigkeit, Teilenummer) etwas verringern,
|
||||||
|
// weil Tesseract nun z.B. zwischen "O" und "o" unterscheiden muss -
|
||||||
|
// hingenommen, weil extractFields() ohnehin verwechslungstolerant
|
||||||
|
// gegen die bekannten Werte prueft (siehe spec.js/CONFUSIONS) und
|
||||||
|
// der Nutzer diesen Tausch ausdruecklich will.
|
||||||
|
tessedit_char_whitelist:
|
||||||
|
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
|
||||||
|
+ ' -/.,:;()+*_#%&',
|
||||||
});
|
});
|
||||||
return worker;
|
return worker;
|
||||||
})
|
})
|
||||||
|
|||||||
+14
-8
@@ -60,10 +60,16 @@ function allCompatible(specs) {
|
|||||||
* Stapel bekannt ist. Ist einer der gelesenen Codes bekannt, steht die
|
* Stapel bekannt ist. Ist einer der gelesenen Codes bekannt, steht die
|
||||||
* Zuordnung fest und die Texterkennung wird uebersprungen. Fehlt die
|
* Zuordnung fest und die Texterkennung wird uebersprungen. Fehlt die
|
||||||
* Funktion, verhaelt sich die Pipeline wie ohne diese Optimierung.
|
* Funktion, verhaelt sich die Pipeline wie ohne diese Optimierung.
|
||||||
* @returns {Promise<{spec: object, source: 'barcode'|'ocr'|'none', confidence: 'green'|'yellow'|'red', codes: string[]}>}
|
* @returns {Promise<{spec: object, source: 'barcode'|'ocr'|'none', confidence: 'green'|'yellow'|'red', codes: string[], rawText: string}>}
|
||||||
* codes sind die im Bild gelesenen rohen Barcode-Zeichenketten (koennen
|
* codes sind die im Bild gelesenen rohen Barcode-Zeichenketten (koennen
|
||||||
* neben der Teilenummer z.B. auch eine Seriennummer enthalten) - zur
|
* neben der Teilenummer z.B. auch eine Seriennummer enthalten) - zur
|
||||||
* Weitergabe an proposeAssignment/commitAssignment in session.js.
|
* Weitergabe an proposeAssignment/commitAssignment in session.js.
|
||||||
|
* rawText ist der ungefilterte Texterkennungs-Rohtext, rein additiv: er
|
||||||
|
* fliesst nirgends in die Stapelzuordnung ein (die stuetzt sich weiterhin
|
||||||
|
* ausschliesslich auf die aus ihm abgeleiteten, verstandenen Spec-Felder,
|
||||||
|
* siehe extractFields/ocr-extract.js) und dient allein der Anzeige fuer den
|
||||||
|
* Nutzer. Leer, wenn die Texterkennung in diesem Durchlauf nicht lief (z.B.
|
||||||
|
* weil ein gruener Barcode-Treffer sie ueberfluessig gemacht hat).
|
||||||
*/
|
*/
|
||||||
export async function recognize(frame, deps) {
|
export async function recognize(frame, deps) {
|
||||||
const barcodeTimeoutMs = deps.barcodeTimeoutMs ?? DEFAULT_BARCODE_TIMEOUT_MS;
|
const barcodeTimeoutMs = deps.barcodeTimeoutMs ?? DEFAULT_BARCODE_TIMEOUT_MS;
|
||||||
@@ -99,7 +105,7 @@ export async function recognize(frame, deps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (usable.length === 1) {
|
if (usable.length === 1) {
|
||||||
return { spec: usable[0], source: 'barcode', confidence: 'green', codes };
|
return { spec: usable[0], source: 'barcode', confidence: 'green', codes, rawText: '' };
|
||||||
}
|
}
|
||||||
if (usable.length > 1) {
|
if (usable.length > 1) {
|
||||||
if (allCompatible(usable)) {
|
if (allCompatible(usable)) {
|
||||||
@@ -109,10 +115,10 @@ export async function recognize(frame, deps) {
|
|||||||
// da alle uebrigen Felder aus derselben Teilenummer abgeleitet werden,
|
// da alle uebrigen Felder aus derselben Teilenummer abgeleitet werden,
|
||||||
// sind vertraegliche Treffer ohnehin gleich. Es gibt nichts aufzufuellen,
|
// sind vertraegliche Treffer ohnehin gleich. Es gibt nichts aufzufuellen,
|
||||||
// der erste Treffer genuegt.
|
// der erste Treffer genuegt.
|
||||||
return { spec: usable[0], source: 'barcode', confidence: 'green', codes };
|
return { spec: usable[0], source: 'barcode', confidence: 'green', codes, rawText: '' };
|
||||||
}
|
}
|
||||||
// Mehrdeutigkeit zwischen verwertbaren Barcodes: nicht raten, Nutzer entscheidet.
|
// Mehrdeutigkeit zwischen verwertbaren Barcodes: nicht raten, Nutzer entscheidet.
|
||||||
return { spec: emptySpec(), source: 'none', confidence: 'red', codes };
|
return { spec: emptySpec(), source: 'none', confidence: 'red', codes, rawText: '' };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ist einer der gelesenen Codes bereits einem Stapel bekannt, steht die
|
// Ist einer der gelesenen Codes bereits einem Stapel bekannt, steht die
|
||||||
@@ -121,7 +127,7 @@ export async function recognize(frame, deps) {
|
|||||||
// Bei einem tatsaechlich neuen Modul (kein Code bekannt) laeuft sie wie
|
// Bei einem tatsaechlich neuen Modul (kein Code bekannt) laeuft sie wie
|
||||||
// bisher, denn dort liefert sie die lesbare Beschriftung des neuen Stapels.
|
// bisher, denn dort liefert sie die lesbare Beschriftung des neuen Stapels.
|
||||||
if (typeof deps.isKnownCode === 'function' && codes.some((code) => deps.isKnownCode(code))) {
|
if (typeof deps.isKnownCode === 'function' && codes.some((code) => deps.isKnownCode(code))) {
|
||||||
return { spec: best, source: 'barcode', confidence: 'green', codes };
|
return { spec: best, source: 'barcode', confidence: 'green', codes, rawText: '' };
|
||||||
}
|
}
|
||||||
|
|
||||||
const ocrResult = await safely(() => deps.runOcr(frame), '', ocrTimeoutMs);
|
const ocrResult = await safely(() => deps.runOcr(frame), '', ocrTimeoutMs);
|
||||||
@@ -136,7 +142,7 @@ export async function recognize(frame, deps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isUsable(merged)) {
|
if (isUsable(merged)) {
|
||||||
return { spec: merged, source: 'ocr', confidence: 'yellow', codes };
|
return { spec: merged, source: 'ocr', confidence: 'yellow', codes, rawText: text };
|
||||||
}
|
}
|
||||||
// Zum Sortieren muss keine Kapazitaet bekannt sein - eine exakt gelesene
|
// Zum Sortieren muss keine Kapazitaet bekannt sein - eine exakt gelesene
|
||||||
// Teilenummer genuegt, um ein Modul wiederzuerkennen. Sie ueberlebt bis
|
// Teilenummer genuegt, um ein Modul wiederzuerkennen. Sie ueberlebt bis
|
||||||
@@ -144,7 +150,7 @@ export async function recognize(frame, deps) {
|
|||||||
// mehrere sich einig waren (siehe 'best' oben) - bei widersprechenden
|
// mehrere sich einig waren (siehe 'best' oben) - bei widersprechenden
|
||||||
// Codes bleibt merged.partNumber null und die Vorsicht damit erhalten.
|
// Codes bleibt merged.partNumber null und die Vorsicht damit erhalten.
|
||||||
if (merged.partNumber !== null) {
|
if (merged.partNumber !== null) {
|
||||||
return { spec: merged, source: 'barcode', confidence: 'green', codes };
|
return { spec: merged, source: 'barcode', confidence: 'green', codes, rawText: text };
|
||||||
}
|
}
|
||||||
return { spec: merged, source: 'none', confidence: 'red', codes };
|
return { spec: merged, source: 'none', confidence: 'red', codes, rawText: text };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,6 +52,9 @@ function isUsableStack(stack) {
|
|||||||
// dieser Erweiterung kennt das Feld nicht und gilt deshalb nicht als
|
// dieser Erweiterung kennt das Feld nicht und gilt deshalb nicht als
|
||||||
// unstimmig (siehe isUsableStack). Ist es vorhanden, muss es eine Liste von
|
// unstimmig (siehe isUsableStack). Ist es vorhanden, muss es eine Liste von
|
||||||
// Zeichenketten sein.
|
// Zeichenketten sein.
|
||||||
|
// entry.rawText (der OCR-Rohtext, siehe pipeline.js/recognize) ist nach
|
||||||
|
// demselben Muster optional: ein Stand aus einer Fassung ohne dieses Feld ist
|
||||||
|
// nicht unstimmig; ist es vorhanden, muss es eine Zeichenkette sein.
|
||||||
function isUsableEntry(entry) {
|
function isUsableEntry(entry) {
|
||||||
return (
|
return (
|
||||||
isPlainObject(entry)
|
isPlainObject(entry)
|
||||||
@@ -59,6 +62,7 @@ function isUsableEntry(entry) {
|
|||||||
&& typeof entry.stackId === 'string'
|
&& typeof entry.stackId === 'string'
|
||||||
&& isPlainObject(entry.spec)
|
&& isPlainObject(entry.spec)
|
||||||
&& (entry.codes === undefined || isStringArray(entry.codes))
|
&& (entry.codes === undefined || isStringArray(entry.codes))
|
||||||
|
&& (entry.rawText === undefined || typeof entry.rawText === 'string')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,6 +111,14 @@ export function loadSession(store) {
|
|||||||
|
|
||||||
if (entryIds.some((entryId) => entryId >= parsed.nextEntryId)) return null;
|
if (entryIds.some((entryId) => entryId >= parsed.nextEntryId)) return null;
|
||||||
|
|
||||||
|
// rawText fehlt bei einem Eintrag aus einer Fassung vor dieser Erweiterung
|
||||||
|
// (siehe isUsableEntry oben) - beim Wiederherstellen wird das einheitlich
|
||||||
|
// als leerer Rohtext behandelt, damit main.js/session-list.js sich nicht
|
||||||
|
// um "undefined vs. leer" kuemmern muessen.
|
||||||
|
for (const entry of parsed.entries) {
|
||||||
|
if (entry.rawText === undefined) entry.rawText = '';
|
||||||
|
}
|
||||||
|
|
||||||
for (const stack of parsed.stacks) {
|
for (const stack of parsed.stacks) {
|
||||||
const stackEntries = parsed.entries.filter((entry) => entry.stackId === stack.id);
|
const stackEntries = parsed.entries.filter((entry) => entry.stackId === stack.id);
|
||||||
stack.count = stackEntries.length;
|
stack.count = stackEntries.length;
|
||||||
|
|||||||
@@ -125,6 +125,21 @@ body {
|
|||||||
.overlay .detail { font-size: 20px; line-height: 1.5; }
|
.overlay .detail { font-size: 20px; line-height: 1.5; }
|
||||||
.overlay .pn { font-size: 15px; opacity: 0.85; word-break: break-all; }
|
.overlay .pn { font-size: 15px; opacity: 0.85; word-break: break-all; }
|
||||||
|
|
||||||
|
/* Voller OCR-Rohtext in der wartenden Rueckmeldung (Funktion "Text
|
||||||
|
erkennen", siehe result-overlay.js) - linksbuendig und mit Zeilenumbruch,
|
||||||
|
damit ein langer Etikettentext lesbar bleibt statt einer einzelnen,
|
||||||
|
abgeschnittenen Zeile. Waechst nicht aus dem Bildschirm heraus, weil
|
||||||
|
`.overlay` selbst scrollbar ist (overflow-y: auto, siehe oben). */
|
||||||
|
.overlay .raw-text {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.5;
|
||||||
|
text-align: left;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
max-width: 420px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.choices { display: grid; gap: 12px; width: 100%; max-width: 420px; }
|
.choices { display: grid; gap: 12px; width: 100%; max-width: 420px; }
|
||||||
.choices button { min-height: 64px; font-size: 20px; }
|
.choices button { min-height: 64px; font-size: 20px; }
|
||||||
|
|
||||||
|
|||||||
+82
-14
@@ -1,38 +1,106 @@
|
|||||||
import { describeSpec } from './describe-spec.js';
|
import { describeSpec } from './describe-spec.js';
|
||||||
|
|
||||||
/** Haelt pro Root die aktuell sichtbare Rueckmeldung samt Zeitgeber fest. */
|
/** Haelt pro Root die aktuell sichtbare Rueckmeldung samt Abschlussfunktion fest. */
|
||||||
const currentFeedback = new WeakMap();
|
const currentFeedback = new WeakMap();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Blendet die Treffer-Rueckmeldung ein und nach kurzer Zeit wieder aus.
|
* Blendet die Treffer-Rueckmeldung ein. Gruen bei Barcode, gelb bei OCR.
|
||||||
* Gruen bei Barcode, gelb bei OCR. Keine Eingabe noetig.
|
*
|
||||||
|
* In den Barcode-Funktionen (autoHide: true, Voreinstellung) blendet sie sich
|
||||||
|
* nach kurzer Zeit von selbst wieder aus - dort zaehlt Tempo, keine Eingabe
|
||||||
|
* noetig. In Funktion 3 ("Text erkennen", autoHide: false) ist der erkannte
|
||||||
|
* Text potenziell ein ganzer Etikettentext und damit zu lang fuer eine knappe
|
||||||
|
* automatische Anzeige: die Rueckmeldung bleibt dort stehen, bis der Nutzer
|
||||||
|
* sie ueber die Schaltflaeche wegtippt, und zeigt zusaetzlich den vollen
|
||||||
|
* Rohtext (rawText). Lang werdender Text bleibt lesbar ueber dasselbe Muster,
|
||||||
|
* mit dem auch die anderen Vollbild-Overlays lange Inhalte handhaben: die
|
||||||
|
* `.overlay`-Klasse ist selbst scrollbar (overflow-y: auto), statt aus dem
|
||||||
|
* Bildschirm herauszuwachsen (siehe styles.css).
|
||||||
|
*
|
||||||
|
* Das zurueckgegebene Versprechen wird in jedem Fall genau einmal eingeloest -
|
||||||
|
* beim automatischen Ausblenden, beim Wegtippen und auch dann, wenn eine neue
|
||||||
|
* Rueckmeldung (z.B. durch einen weiteren Scan) diese hier verdraengt, bevor
|
||||||
|
* sie selbst eingeloest wurde. Die App sperrt waehrend der laufenden
|
||||||
|
* Erkennung die Bedienung (siehe processCapture() in main.js) - ein nie
|
||||||
|
* eingeloestes Versprechen wuerde sie dauerhaft lahmlegen.
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} root
|
||||||
|
* @param {{stackId: string, spec: object, confidence: string, rawText?: string, autoHide?: boolean}} args
|
||||||
|
* @param {number} durationMs Anzeigedauer bei autoHide: true.
|
||||||
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
export function showResult(root, { stackId, spec, confidence }, durationMs = 1200) {
|
export function showResult(
|
||||||
|
root,
|
||||||
|
{ stackId, spec, confidence, rawText = '', autoHide = true },
|
||||||
|
durationMs = 1200,
|
||||||
|
) {
|
||||||
|
// Eine noch offene vorherige Rueckmeldung wird sofort abgeschlossen (nicht
|
||||||
|
// nur entfernt) - ihr Versprechen darf nicht offen bleiben, nur weil eine
|
||||||
|
// neue Rueckmeldung sie verdraengt.
|
||||||
const previous = currentFeedback.get(root);
|
const previous = currentFeedback.get(root);
|
||||||
if (previous) {
|
if (previous) previous.finish();
|
||||||
clearTimeout(previous.timer);
|
|
||||||
previous.overlay.remove();
|
|
||||||
previous.resolve();
|
|
||||||
currentFeedback.delete(root);
|
|
||||||
}
|
|
||||||
|
|
||||||
const overlay = document.createElement('div');
|
const overlay = document.createElement('div');
|
||||||
overlay.className = `overlay ${confidence}`;
|
overlay.className = `overlay ${confidence}`;
|
||||||
|
|
||||||
|
if (!autoHide) {
|
||||||
|
// Nur die wartende Variante ist eine echte Eingabeaufforderung - siehe
|
||||||
|
// ambiguous-dialog.js/mode-dialog.js fuer dasselbe Muster (role="dialog",
|
||||||
|
// aria-modal, Ueberschrift referenziert per aria-labelledby).
|
||||||
|
overlay.setAttribute('role', 'dialog');
|
||||||
|
overlay.setAttribute('aria-modal', 'true');
|
||||||
|
overlay.setAttribute('aria-labelledby', 'result-overlay-heading');
|
||||||
|
}
|
||||||
|
|
||||||
overlay.innerHTML = `
|
overlay.innerHTML = `
|
||||||
<div class="stack-id">STAPEL ${stackId}</div>
|
<div class="stack-id" id="result-overlay-heading">STAPEL ${stackId}</div>
|
||||||
<div class="detail"></div>
|
<div class="detail"></div>
|
||||||
<div class="pn"></div>
|
<div class="pn"></div>
|
||||||
|
<div class="raw-text" id="raw-text" hidden></div>
|
||||||
`;
|
`;
|
||||||
overlay.querySelector('.detail').textContent = describeSpec(spec);
|
overlay.querySelector('.detail').textContent = describeSpec(spec);
|
||||||
overlay.querySelector('.pn').textContent = spec.partNumber ?? '';
|
overlay.querySelector('.pn').textContent = spec.partNumber ?? '';
|
||||||
|
|
||||||
|
if (!autoHide) {
|
||||||
|
// Der vollstaendige erkannte Text - Fremdinhalt vom Etikett, deshalb als
|
||||||
|
// Text gesetzt, nicht als Auszeichnung (siehe ambiguous-dialog.js/#read).
|
||||||
|
const rawTextEl = overlay.querySelector('#raw-text');
|
||||||
|
rawTextEl.hidden = false;
|
||||||
|
rawTextEl.textContent = rawText;
|
||||||
|
}
|
||||||
|
|
||||||
root.appendChild(overlay);
|
root.appendChild(overlay);
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const timer = setTimeout(() => {
|
let settled = false;
|
||||||
|
let timer = null;
|
||||||
|
|
||||||
|
// Schuetzt vor Mehrfacheinloesung: gleichzeitig ablaufender Zeitgeber und
|
||||||
|
// Tippen auf "weiter", oder ein Abschluss durch eine verdraengende neue
|
||||||
|
// Rueckmeldung, nachdem der Zeitgeber schon gelaufen ist.
|
||||||
|
const finish = () => {
|
||||||
|
if (settled) return;
|
||||||
|
settled = true;
|
||||||
|
if (timer) clearTimeout(timer);
|
||||||
overlay.remove();
|
overlay.remove();
|
||||||
currentFeedback.delete(root);
|
currentFeedback.delete(root);
|
||||||
resolve();
|
resolve();
|
||||||
}, durationMs);
|
};
|
||||||
currentFeedback.set(root, { overlay, timer, resolve });
|
|
||||||
|
if (autoHide) {
|
||||||
|
timer = setTimeout(finish, durationMs);
|
||||||
|
} else {
|
||||||
|
// Bedienflaeche mindestens 56px hoch (siehe Vorgaben) - der einzige Weg,
|
||||||
|
// diese Rueckmeldung zu schliessen, solange sie nicht autoHide ist.
|
||||||
|
const dismissButton = document.createElement('button');
|
||||||
|
dismissButton.className = 'action';
|
||||||
|
dismissButton.style.minHeight = '56px';
|
||||||
|
dismissButton.textContent = 'weiter';
|
||||||
|
dismissButton.addEventListener('click', finish);
|
||||||
|
overlay.appendChild(dismissButton);
|
||||||
|
dismissButton.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
currentFeedback.set(root, { finish });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,6 +90,22 @@ export function renderSessionList(root, session, { onMove, onRemove, onEndSessio
|
|||||||
row.appendChild(removeButton);
|
row.appendChild(removeButton);
|
||||||
|
|
||||||
body.appendChild(row);
|
body.appendChild(row);
|
||||||
|
|
||||||
|
// entry.rawText ist additiv (siehe pipeline.js/recognize) und nur bei
|
||||||
|
// per Texterkennung erfassten Eintraegen nicht leer - zum Nachlesen des
|
||||||
|
// vollstaendigen Etikettentexts, unabhaengig von den daraus verstandenen
|
||||||
|
// Feldern oben in derselben Zeile.
|
||||||
|
if (entry.rawText) {
|
||||||
|
const rawTextRow = document.createElement('div');
|
||||||
|
rawTextRow.style.fontSize = '12px';
|
||||||
|
rawTextRow.style.opacity = '0.7';
|
||||||
|
rawTextRow.style.textAlign = 'left';
|
||||||
|
rawTextRow.style.whiteSpace = 'pre-wrap';
|
||||||
|
rawTextRow.style.overflowWrap = 'break-word';
|
||||||
|
rawTextRow.style.marginBottom = '6px';
|
||||||
|
rawTextRow.textContent = `Erkannter Text: ${entry.rawText}`;
|
||||||
|
body.appendChild(rawTextRow);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { test } from 'node:test';
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { recognize } from '../src/pipeline.js';
|
import { recognize } from '../src/pipeline.js';
|
||||||
import { emptySpec } from '../src/spec.js';
|
import { emptySpec } from '../src/spec.js';
|
||||||
|
import { createSession, proposeAssignment, commitAssignment } from '../src/session.js';
|
||||||
|
|
||||||
const OCR_TEXT = '64GB 4DRx4 PC4-2400T-LD1-11-MC0 M386A8K40BM1-CRC4Y 1908';
|
const OCR_TEXT = '64GB 4DRx4 PC4-2400T-LD1-11-MC0 M386A8K40BM1-CRC4Y 1908';
|
||||||
|
|
||||||
@@ -281,3 +282,93 @@ test('ohne isKnownCode-Abhaengigkeit bleibt die Pipeline ohne sie lauffaehig (Te
|
|||||||
assert.equal(ocrCalled, true);
|
assert.equal(ocrCalled, true);
|
||||||
assert.equal(result.confidence, 'green');
|
assert.equal(result.confidence, 'green');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- Erweiterung: Rohtext als additives Feld (Funktion "Text erkennen") ---
|
||||||
|
//
|
||||||
|
// Der Rohtext ist reine Anzeige-Information fuer den Nutzer - er verlaesst
|
||||||
|
// die Erkennung, wird aber nirgends zur Stapelzuordnung herangezogen (siehe
|
||||||
|
// den letzten Test unten, der recognize() mit session.js zusammenspielen
|
||||||
|
// laesst).
|
||||||
|
|
||||||
|
test('recognize liefert den Rohtext, wenn die Texterkennung gelaufen ist (gelbes Ergebnis)', async () => {
|
||||||
|
const result = await recognize({}, deps({ codes: [], text: OCR_TEXT }));
|
||||||
|
assert.equal(result.source, 'ocr');
|
||||||
|
assert.equal(result.rawText, OCR_TEXT);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('recognize liefert den Rohtext auch, wenn die Texterkennung lief, aber nichts Verwertbares fand (rot)', async () => {
|
||||||
|
const result = await recognize({}, deps({ codes: [], text: 'Made in Philippines' }));
|
||||||
|
assert.equal(result.confidence, 'red');
|
||||||
|
assert.equal(result.rawText, 'Made in Philippines');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('recognize liefert den Rohtext, wenn OCR keine Kapazitaet findet, aber eine unverwertbare Barcode-Teilenummer bestaetigt (gruen)', async () => {
|
||||||
|
// '7325773' hat kein bekanntes Nummernschema (decodePartNumber liefert
|
||||||
|
// keine Kapazitaet) - die Texterkennung laeuft trotzdem und liefert Text
|
||||||
|
// ohne jede Kapazitaetsangabe. Die Teilenummer aus dem Barcode traegt
|
||||||
|
// (siehe recognize()), das Ergebnis bleibt gruen - der gelaufene Rohtext
|
||||||
|
// wird trotzdem zurueckgegeben.
|
||||||
|
const result = await recognize({}, {
|
||||||
|
decodeBarcodes: async () => ['7325773'],
|
||||||
|
runOcr: async () => 'Made in Philippines',
|
||||||
|
});
|
||||||
|
assert.equal(result.source, 'barcode');
|
||||||
|
assert.equal(result.confidence, 'green');
|
||||||
|
assert.equal(result.spec.partNumber, '7325773');
|
||||||
|
assert.equal(result.rawText, 'Made in Philippines');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('recognize liefert eine leere Zeichenkette als Rohtext, wenn die Texterkennung nicht lief (gruener Barcode-Treffer)', async () => {
|
||||||
|
const result = await recognize({}, {
|
||||||
|
decodeBarcodes: async () => ['M386A8K40BM1-CRC4Y'],
|
||||||
|
runOcr: async () => { throw new Error('darf nicht aufgerufen werden'); },
|
||||||
|
});
|
||||||
|
assert.equal(result.source, 'barcode');
|
||||||
|
assert.equal(result.rawText, '');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('recognize liefert eine leere Zeichenkette als Rohtext, wenn ein bekannter Code die Texterkennung ueberfluessig macht', async () => {
|
||||||
|
const result = await recognize({}, {
|
||||||
|
decodeBarcodes: async () => ['HMA84GL7AFR4N-UH'],
|
||||||
|
runOcr: async () => { throw new Error('darf nicht aufgerufen werden'); },
|
||||||
|
isKnownCode: (code) => code === 'HMA84GL7AFR4N-UH',
|
||||||
|
});
|
||||||
|
assert.equal(result.rawText, '');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('recognize liefert eine leere Zeichenkette als Rohtext bei mehrdeutigen verwertbaren Barcodes (rot, ohne OCR)', async () => {
|
||||||
|
const result = await recognize({}, {
|
||||||
|
decodeBarcodes: async () => ['M386A8K40BM1-CRC4Y', 'M386A8K40BM1-CWE4Y'],
|
||||||
|
runOcr: async () => { throw new Error('darf nicht aufgerufen werden'); },
|
||||||
|
});
|
||||||
|
assert.equal(result.confidence, 'red');
|
||||||
|
assert.equal(result.rawText, '');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('der Rohtext beeinflusst die Stapelzuordnung nicht: zwei Aufnahmen mit unterschiedlichem Rohtext, aber gleichen verstandenen Feldern, landen auf demselben Stapel', async () => {
|
||||||
|
const firstText = '64GB 4DRx4 PC4-2400T-LD1-11-MC0 M386A8K40BM1-CRC4Y 1908';
|
||||||
|
// Anderer Rohtext (andere Reihenfolge, zusaetzliches "Made in Korea",
|
||||||
|
// andere Gross-/Kleinschreibung) - dieselben verstandenen Felder.
|
||||||
|
const secondText = 'made in korea m386a8k40bm1-crc4y 64gb 4drx4 PC4-2400 1908';
|
||||||
|
|
||||||
|
const first = await recognize({}, deps({ codes: [], text: firstText }));
|
||||||
|
const second = await recognize({}, deps({ codes: [], text: secondText }));
|
||||||
|
|
||||||
|
assert.notEqual(first.rawText, second.rawText, 'Vorbedingung: die Rohtexte unterscheiden sich tatsaechlich');
|
||||||
|
assert.deepEqual(first.spec, second.spec, 'Vorbedingung: dieselben verstandenen Felder');
|
||||||
|
|
||||||
|
const session = createSession();
|
||||||
|
const plan1 = proposeAssignment(session, first.spec, first.codes);
|
||||||
|
const entry1 = commitAssignment(session, first.spec, first.source, plan1.stackId, first.codes);
|
||||||
|
// Der Rohtext haengt (wie main.js es tut) rein additiv am Eintrag, ohne
|
||||||
|
// dass session.js davon weiss - siehe main.js/processCapture().
|
||||||
|
entry1.rawText = first.rawText;
|
||||||
|
|
||||||
|
const plan2 = proposeAssignment(session, second.spec, second.codes);
|
||||||
|
assert.equal(plan2.kind, 'match', 'das zweite Modul muss denselben Stapel treffen');
|
||||||
|
const entry2 = commitAssignment(session, second.spec, second.source, plan2.stackId, second.codes);
|
||||||
|
entry2.rawText = second.rawText;
|
||||||
|
|
||||||
|
assert.equal(entry1.stackId, entry2.stackId, 'unterschiedlicher Rohtext darf keinen eigenen Stapel erzeugen');
|
||||||
|
assert.notEqual(entry1.rawText, entry2.rawText, 'die Eintraege fuehren trotzdem je ihren eigenen Rohtext');
|
||||||
|
});
|
||||||
|
|||||||
@@ -308,3 +308,39 @@ test('Eintrag mit gebrochener entryId liefert null', () => {
|
|||||||
}));
|
}));
|
||||||
assert.equal(loadSession(store), null);
|
assert.equal(loadSession(store), null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- Erweiterung: der OCR-Rohtext eines Eintrags (entry.rawText) ---
|
||||||
|
|
||||||
|
test('Rohtext eines Eintrags ueberlebt Sichern und Laden', () => {
|
||||||
|
const store = fakeStore();
|
||||||
|
const session = createSession();
|
||||||
|
const entry = commitAssignment(session, { ...emptySpec(), capacityGb: 32 }, 'ocr', 'A');
|
||||||
|
entry.rawText = '32GB 2Rx4 PC4-2666 Vollständiges Etikett Made in Korea';
|
||||||
|
saveSession(session, store);
|
||||||
|
|
||||||
|
const reloaded = loadSession(store);
|
||||||
|
assert.notEqual(reloaded, null);
|
||||||
|
assert.equal(reloaded.entries[0].rawText, '32GB 2Rx4 PC4-2666 Vollständiges Etikett Made in Korea');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ein Stand ohne rawText-Feld (aeltere Fassung) wird nicht verworfen, der Eintrag gilt als ohne Rohtext', () => {
|
||||||
|
const store = fakeStore();
|
||||||
|
store.setItem('ram-sortierhilfe:session', JSON.stringify({
|
||||||
|
stacks: [{ id: 'A', count: 1, spec: { ...emptySpec(), capacityGb: 64 } }],
|
||||||
|
entries: [{ entryId: 1, stackId: 'A', spec: { ...emptySpec(), capacityGb: 64 }, source: 'barcode' }],
|
||||||
|
nextEntryId: 2,
|
||||||
|
}));
|
||||||
|
const reloaded = loadSession(store);
|
||||||
|
assert.notEqual(reloaded, null, 'ein Stand ohne rawText-Feld darf nicht verworfen werden');
|
||||||
|
assert.equal(reloaded.entries[0].rawText, '');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Eintrag mit rawText als Nicht-Zeichenkette liefert null', () => {
|
||||||
|
const store = fakeStore();
|
||||||
|
store.setItem('ram-sortierhilfe:session', JSON.stringify({
|
||||||
|
stacks: [{ id: 'A', count: 1, spec: emptySpec() }],
|
||||||
|
entries: [{ entryId: 1, stackId: 'A', spec: emptySpec(), source: 'barcode', rawText: 123 }],
|
||||||
|
nextEntryId: 2,
|
||||||
|
}));
|
||||||
|
assert.equal(loadSession(store), null);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user