Files
dogfather-universe/server/pruef-kampagne-bild.mjs
T
DogFatherGitandClaude Opus 5 698c23e20d Agentur-Events: aus einem Bildschirmfoto wird eine Kampagnenkachel
Filipe: "bei den agentur events soll ich kein link sondern immer so ein
screen reinschicke und dan soll daraus alles genommen werden und daraus
die kampagne kachel gemacht werden."

WARUM EIN BILD UND NICHT DER LINK. Gemessen am 07./08.10.2026 an seinem
echten Agenturlink, mit Gegenprobe:
  vm.tiktok.com/ZSbACwAnK/ -> tiktok.com/tcn/activity?activity_id=...
    233 KB reine Huelle, kein Datenpaket
  dieselbe Nummer auf dem Weg der oeffentlichen Kampagnenseite
    42 KB, ebenfalls ohne Datenpaket
  eine ECHTE Kampagne auf demselben Weg
    967 KB MIT Datenpaket
Die Agentur-Kampagnen leben im Creator Network, und das rendert nichts
auf dem Server. Was Filipe sieht, sieht nur sein Browser -- also wird
genau das gelesen: das Bild davon.

DREI WEGE ZUM SELBEN KASTEN: einfuegen (Strg+V nach Win+Umschalt+S),
hereinziehen, auswaehlen (Handy). Derselbe Kasten, derselbe
Knopfzustand, dieselbe Meldezeile wie beim Link.

TESSERACT, NICHT EIN DIENST IM NETZ. Apache 2.0, auf dem eigenen
Server, 0 EUR, und das Bild verlaesst ihn nicht -- ein Backstage-Foto
zeigt Teilnehmerzahlen und Kampagnennamen. Version 5.5.0 mit deutschen
Sprachdaten war bereits vorhanden. `--psm 4` gemessen gegen `6` (24
statt 14 Zeilen); keine Bildnachbearbeitung, weil vier gemessene
Fassungen (unveraendert, invertiert, doppelt, beides) dieselben Ziele
wortgleich lesen.

DIESELBE FORM WIE DER LINK-LESER. `leseBild()` gibt Feld fuer Feld
dasselbe zurueck wie `leseKampagne()` -- dadurch greifen Gliederung,
Geschenkplaketten, Aufklappen, Teilen, Steckbrief und Spaltenwahl
unveraendert. Geprueft wird das ausdruecklich (17 gegen 17 Felder).

DER ZEITRAUM WIRD ZWEIMAL GELESEN UND MUSS ZWEIMAL DASSELBE SAGEN.
Auf der Seite steht er amerikanisch (10/01/2026) und deutsch
(01.10.2026). Genommen wird die deutsche, die amerikanische ist die
Gegenprobe. Widersprechen sie sich, entsteht KEINE Kachel: Eine Kachel
ohne Zeitraum ist unvollstaendig und sagt das selbst -- eine mit dem
falschen ist eine Falschauskunft, und bei Texterkennung ist aus einer 3
schnell eine 8.

Der Anlege-Block ist aus der Link-Route herausgezogen
(`kachelSchreiben`), damit beide Wege DIESELBE Stelle benutzen.
Abgeschrieben waere er die Dopplung, die hier am 11.09.2026 schon
einmal drei Spalten gekostet hat.

GEMESSEN UND BERICHTIGT (alles aus echten Laeufen, nicht geraten):
  - Die Phasenueberschrift trug ein Zielscheiben-Zeichen, das als `@`
    gelesen wurde -- die erste "Aufgabe" hiess "Ziele erreichen Endetin
    23 Tage 22 : 50 : 44".
  - Halbe Schilder liessen ihre zweite Haelfte im Wert stehen:
    "Sichtbar für: für Privat".
  - "Creator*innen" schnitt mitten im Nachbarwert ab und machte aus
    "1 Creator Network, 55 Creator*innen" ein "1 Creator Network, 55".
  - "In Bearbeitung" kam als "in aearbeitung" durch.
  - Die Creator-Zahl steht als "1 (i)" da, und der Kreis wird als Null
    gelesen. Sie wird deshalb GAR NICHT angezeigt: keine Angabe ist
    richtig, eine falsche ist falsch.
  - Das Wasserzeichen (seine Mailadresse, vielfach und schraeg) taucht
    im Erkennungstext kein einziges Mal auf -- festgehalten mit
    Gegenprobe, falls eine kuenftige Fassung es doch liest.
  - `.knopf--still` hiess zuerst wie ein allgemeiner Knopf-Zusatz;
    pruef-css-klassen hatte recht, ihn auf drei Seiten zu vermissen.
    Jetzt `.kampagneholen__waehlen`, im Namensraum seines Kastens.
  - Die Pruefung selbst meldete zweimal Falsches: Sie fand "execFile"
    und "Tesseract" in den KOMMENTAREN des Lesers und hielt
    `regex.exec(...)` fuer einen fremden Prozess.

GEPRUEFT: 43 neu (pruef-kampagne-bild, davon 1 nicht nachsehbar --
Tesseract fehlt auf dem Entwicklungsrechner; auf dem Server liefert es
den gespeicherten Erkennungstext zeichengleich), 169 (Route,
unveraendert nach dem Herausziehen), struktur, css-klassen, zeichen,
deutsche-texte.

PARALLELE ARBEIT: An derselben Arbeitskopie wird gleichzeitig an der
Crew-Seite gebaut (Reaction, Foyer). Dieser Commit fasst ausschliesslich
die zehn Dateien oben an -- einzeln vorgemerkt, kein `git add -A`. Der
globale Cache-Stempel wurde absichtlich NICHT gelaufen, nur die zwei
Verweise in bereich.html; gestempelt wird am Ende einmal fuer beide.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-08 10:04:57 +02:00

404 lines
18 KiB
JavaScript

/* =====================================================================
EINE KAMPAGNE AUS EINEM BILDSCHIRMFOTO — GEPRUEFT (08.10.2026)
Filipe: „bei den agentur events soll ich kein link sondern immer so
ein screen reinschicke und dan soll daraus alles genommen werden und
daraus die kampagne kachel gemacht werden."
ZWEI DINGE, ZWEI MESSUNGEN, UND SIE HAENGEN NICHT ANEINANDER:
1. DAS AUSLESEN (Abschnitte 1-4). Text hinein, Daten heraus. Laeuft
ueberall, auch ohne Tesseract -- deshalb ist der Leser ueberhaupt
von der Texterkennung getrennt. Geprueft wird gegen einen
GESPEICHERTEN Erkennungstext, dessen Pruefsumme hier steht.
2. DIE TEXTERKENNUNG (Abschnitt 5). Bild hinein, Text heraus. Dafuer
braucht es Tesseract. Ist es nicht da, ist das Ergebnis NICHT
„bestanden" und auch nicht „uebersprungen", sondern der dritte
Ausgang: „konnte nicht nachsehen", mit Grund und Anleitung
(CLAUDE.md, 03.09.2026).
WARUM DER ERKENNUNGSTEXT GESPEICHERT IST und nicht bei jedem Lauf
neu erzeugt wird: Sonst misst diese Pruefung die Tagesform von
Tesseract statt unseren Code. Faellt die Texterkennung einmal anders
aus, soll Abschnitt 5 rot werden -- und die Abschnitte 1-4 sollen
trotzdem weiter sagen, ob das Auslesen stimmt.
===================================================================== */
import { readFileSync, existsSync } from "node:fs";
import { createHash } from "node:crypto";
import { execFileSync } from "node:child_process";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { notbremse } from "./helfer-notbremse.mjs";
notbremse(90_000, "pruef-kampagne-bild");
const HIER = dirname(fileURLToPath(import.meta.url));
const DATEN = join(HIER, "pruefdaten");
let fehler = 0;
let geprueft = 0;
const melde = (t) => console.log(t);
const ok = (b, t) => {
geprueft++;
console.log((b ? " ok " : " FEHL ") + t);
if (!b) fehler++;
};
/* Der dritte Ausgang: nicht in Ordnung, aber auch nicht „bestanden". */
let offen = 0;
const nichtNachsehbar = (was, wie) => {
offen++;
melde(` -- NICHT NACHSEHBAR: ${was}`);
melde(` ${wie}`);
};
/* ---- Die gespeicherten Daten ---------------------------------- */
const DATEIEN = {
text: {
name: "bild-agentur-erkannt.txt",
sha: "49de95db94482ebd", gross: 768,
was: "was Tesseract am 08.10.2026 aus dem Nachbau gelesen hat",
},
bild: {
name: "bild-agentur-nachbau.png",
was: "der Nachbau von Filipes Bildschirmfoto, als Bild",
},
seite: {
name: "bild-agentur-nachbau.html",
was: "derselbe Nachbau als Seite -- daraus entsteht das Bild neu",
},
};
melde("=== 0. Sind die gespeicherten Daten da? ===");
for (const [, d] of Object.entries(DATEIEN)) {
const pfad = join(DATEN, d.name);
if (!existsSync(pfad)) {
melde(` FEHL ${d.name} fehlt in ${DATEN}`);
melde(" Sie gehoert ins Verzeichnis und ist in Gitea versioniert.");
process.exit(3);
}
const roh = readFileSync(pfad);
const sha = createHash("sha256").update(roh).digest("hex").slice(0, 16);
if (d.sha) {
ok(sha === d.sha && roh.length === d.gross,
`${d.name}: ${roh.length} Bytes, sha ${sha} — ${d.was}`
+ (sha === d.sha ? "" : ` (ERWARTET ${d.sha}/${d.gross})`));
} else {
ok(roh.length > 100, `${d.name}: ${roh.length} Bytes — ${d.was}`);
}
}
const { leseBild, BildUnlesbar } = await import("./kampagne-bild-lesen.mjs");
const ERKANNT = readFileSync(join(DATEN, DATEIEN.text.name), "utf8");
/* =====================================================================
1. WAS AUF DER KACHEL LANDET
===================================================================== */
melde("");
melde("=== 1. Aus dem Erkennungstext wird eine Kachel ===");
{
const g = leseBild(ERKANNT);
ok(g.pflicht.titel === "[Star Light] Refined visuals Goal 1.0",
`der Titel: ${JSON.stringify(g.pflicht.titel)}`);
/* OHNE DAS STATUSABZEICHEN und ohne die Knoepfe daneben. Auf dem
Bild steht rechts „Kampagne duplizieren" und links „In
Bearbeitung" -- beides gehoert nicht in einen Kacheltitel. */
ok(!/Bearbeitung|duplizieren|anzeigen/i.test(g.pflicht.titel),
" ohne Statusabzeichen und ohne die Knoepfe daneben");
ok(g.pflicht.start === "2026-10-01" && g.pflicht.ende === "2026-10-31",
`der Zeitraum: ${g.pflicht.start} bis ${g.pflicht.ende}`);
/* DIE DEUTSCHE SCHREIBWEISE GEWINNT -- sie ist eindeutig. Die
amerikanische oben (10/01/2026) ist die Gegenprobe. */
ok(g.auszug.zeitraum_quelle === "deutsch",
`gelesen aus der deutschen Angabe (${g.auszug.zeitraum_quelle})`);
ok(g.auszug.zeitraum_gegenprobe === "2026-10-01..2026-10-31",
`und die amerikanische sagt dasselbe (${g.auszug.zeitraum_gegenprobe})`);
ok(g.pflicht.zone === "Europe/Amsterdam", `die Zeitzone: ${g.pflicht.zone}`);
ok(g.auszug.status_anzeige === "In Bearbeitung",
`der Stand: ${JSON.stringify(g.auszug.status_anzeige)}`);
/* ---- Die Ziele --------------------------------------------- */
ok(g.kuer.aufgaben.length === 3,
`drei Ziele (${g.kuer.aufgaben.length}): ${JSON.stringify(g.kuer.aufgaben)}`);
ok(g.kuer.aufgaben[0] === "Geh LIVE für 900 Minuten",
" das erste wortgleich, ohne den Punkt, den die Texterkennung anhaengt");
ok(g.kuer.aufgaben[2] === "An 20 Tagen Ziele für optimierte Bildqualität erreichen",
" und das dritte mit Umlauten");
/* DIE PHASENUEBERSCHRIFT IST KEINE AUFGABE. Sie traegt auf der
Seite ein Zielscheiben-Zeichen, und daraus macht die
Texterkennung ein `@` -- sie sah damit aus wie eine
Aufzaehlungszeile. Beim ersten Lauf hiess die erste „Aufgabe"
wirklich „Ziele erreichen Endetin 23 Tage 22 : 50 : 44". */
ok(!g.kuer.aufgaben.some((a) => /Endet|Ziele erreichen|Leistung/i.test(a)),
" und die Phasenueberschrift mit ihrem Countdown ist keine");
ok(g.kuer.einleitung === "Zusätzliche Prämien für das Team im Oktober.",
`die Beschreibung: ${JSON.stringify(g.kuer.einleitung)}`);
ok(g.kuer.steckbrief.verbund === "Spicy Media",
`erstellt von: ${JSON.stringify(g.kuer.steckbrief.verbund)}`);
/* ---- Die Nebenangaben -------------------------------------- */
const zeilen = g.kuer.weitereRegeln[0]?.zeilen || [];
ok(zeilen.includes("Sichtbar für: Privat"),
`„Sichtbar für" steht sauber da: ${JSON.stringify(zeilen)}`);
/* HALBE SCHILDER WAREN DER FEHLER. Mit dem Stueck „Sichtbar" kam
„Sichtbar für: für Privat" heraus -- das zweite Wort gehoert zum
Schild, nicht zum Wert. */
ok(!zeilen.some((z) => /: (für|erforderlich|von|innen)/i.test(z)),
" und kein Schild steht doppelt im eigenen Wert");
/* UND „Creator*innen" SCHNEIDET NICHT MEHR IM NACHBARN. Es steht
mitten in „1 Creator Network, 55 Creator*innen" und machte daraus
„1 Creator Network, 55". */
ok(zeilen.some((z) => /Teilnehmer.*55 Creator/i.test(z)),
" und die Teilnehmerzahl ist vollstaendig");
/* DIE ZAHL, DER MAN NICHT TRAUEN KANN, FEHLT. Auf der Seite steht
„1 ⓘ" -- der Kreis wird als Null gelesen, heraus kaeme „10".
Keine Angabe ist richtig, eine falsche Angabe ist falsch. */
ok(!zeilen.some((z) => /^Creator\*innen:/i.test(z)),
" und die unsichere Creator-Zahl steht gar nicht erst da");
/* ---- Die Form ist dieselbe wie beim Link-Leser -------------- */
/* DAS IST DIE TRAGENDE ZUSICHERUNG DIESER DATEI. Weicht sie ab,
greifen Gliederung, Geschenke, Aufklappen und Teilen nicht mehr
-- und zwar still. */
const { leseKampagne } = await import("./kampagne-lesen.mjs");
const { brotliDecompressSync } = await import("node:zlib");
const vomLink = leseKampagne(brotliDecompressSync(
readFileSync(join(DATEN, "kampagne-gipfelstuermer.html.br"))).toString("utf8"));
const felderBild = Object.keys(g.kuer).sort();
const felderLink = Object.keys(vomLink.kuer).sort();
ok(felderBild.join(",") === felderLink.join(","),
"kuer hat dieselben Felder wie beim Link-Leser "
+ `(${felderBild.length} gegen ${felderLink.length})`);
const aBild = Object.keys(g.auszug).sort();
const aLink = Object.keys(vomLink.auszug).sort();
const fehltImBild = aLink.filter((f) => !aBild.includes(f));
ok(fehltImBild.length === 0,
`und der Auszug laesst kein Feld des Link-Lesers aus`
+ (fehltImBild.length ? `: ${fehltImBild.join(", ")}` : ""));
ok(["pflicht", "kuer", "hinweise", "auszug"].every((f) => f in g),
"und die vier Teile heissen gleich");
/* ---- Die Kachel sagt, woher sie kommt ---------------------- */
ok(g.auszug.gelesen_aus === "bildschirmfoto",
`der Auszug nennt die Quelle (${g.auszug.gelesen_aus})`);
ok(typeof g.auszug.erkannter_text === "string"
&& g.auszug.erkannter_text.includes("Star Light"),
`und traegt den Erkennungstext mit (${g.auszug.erkannter_text.length} Zeichen)`);
}
/* =====================================================================
2. WAS NICHT GEHT — UND WARUM DAS RICHTIG IST
===================================================================== */
melde("");
melde("=== 2. Lieber keine Kachel als eine mit erfundenem Termin ===");
{
const wirft = (text, art, was) => {
let f = null;
try { leseBild(text); } catch (x) { f = x; }
ok(f instanceof BildUnlesbar && f.art === art,
`${was} -> ${f ? `${f.name}/${f.art}` : "KEIN Fehler"}`);
return f;
};
wirft("", "aufbau", "gar kein Text");
wirft("nur\nzwei", "aufbau", "fast kein Text");
wirft("Gruppenname Test\nZeitzone Europe/Berlin\nnoch eine Zeile",
"pflicht", "ein Bild ohne Zeitraum");
/* DIE GEGENPROBE ZWISCHEN DEN ZWEI SCHREIBWEISEN -- der Kern
dieser Datei. Eine Texterkennung macht aus einer 3 eine 8, und
„bis 08.10." liest sich genauso plausibel wie „bis 03.10.". */
const widerspruch = [
"Kampagne X",
"10/01/2026 00:00:00 ~ 10/31/2026 22:59:59 (UTC+02:00)",
"Gruppenname Kampagne X Zeitzone Europe/Berlin",
"Daten 01.10.2026 00:00 ~ 28.10.2026 22:59",
].join("\n");
const f = wirft(widerspruch, "pflicht", "zwei Zeitraeume, die sich widersprechen");
ok(/zweimal und beide Male anders/.test(f?.message || ""),
` und die Meldung sagt, was los ist: „${f?.message?.slice(0, 70)}…"`);
/* GEGENPROBE ZUR GEGENPROBE: Stimmen beide ueberein, entsteht sehr
wohl eine Kachel. Ohne diese Zeile waere „es wirft" auch dann
gruen, wenn es IMMER wirft. */
const einig = widerspruch.replace("28.10.2026", "31.10.2026");
let g = null;
try { g = leseBild(einig); } catch { g = null; }
ok(g !== null && g.pflicht.ende === "2026-10-31",
`Gegenprobe: stimmen beide ueberein, entsteht die Kachel `
+ `(${g ? g.pflicht.ende : "KEINE"})`);
/* Steht nur EINE Schreibweise da, geht es -- mit Hinweis. Das ist
der mittlere Fall: nicht falsch, aber auch nicht doppelt
geprueft. */
const nurDeutsch = "Kampagne Y\nGruppenname Kampagne Y Zeitzone Europe/Berlin\n"
+ "Daten 05.11.2026 00:00 ~ 20.11.2026 22:59";
const h = leseBild(nurDeutsch);
ok(h.pflicht.start === "2026-11-05" && h.pflicht.ende === "2026-11-20",
`eine Schreibweise allein reicht (${h.pflicht.start}..${h.pflicht.ende})`);
ok(h.hinweise.some((x) => /nur an einer Stelle/i.test(x)),
` und die Kachel sagt, dass nicht gegengeprueft werden konnte`);
/* Ein Ende vor dem Anfang ist ein Zahlendreher, kein Zeitraum. */
wirft("Kampagne Z\nGruppenname Kampagne Z\nDaten 20.11.2026 ~ 05.11.2026",
"pflicht", "Ende vor Anfang");
/* Und ein Datum, das es nicht gibt. */
wirft("Kampagne Q\nGruppenname Kampagne Q\nDaten 31.02.2026 ~ 05.03.2026",
"pflicht", "der 31. Februar");
}
/* =====================================================================
3. DAS WASSERZEICHEN
===================================================================== */
melde("");
melde("=== 3. Filipes Mailadresse liegt ueber dem ganzen Bild ===");
{
/* Auf seinen Bildschirmfotos steht sie vielfach und schraeg ueber
der Seite. Gemessen: Sie taucht im Erkennungstext kein einziges
Mal auf -- zu wenig Kontrast. Das ist Glueck, kein Verdienst, und
deshalb wird es festgehalten: Wuerde eine kuenftige
Tesseract-Fassung sie lesen, stuende sie mitten in den Zielen. */
ok(!/dogfather1608|gmail/i.test(ERKANNT),
"das Wasserzeichen steht nicht im Erkennungstext");
const g = leseBild(ERKANNT);
const alles = JSON.stringify(g);
ok(!/dogfather1608|gmail/i.test(alles),
"und damit auch nirgends in der Kachel");
/* GEGENPROBE: Stuende es doch im Text, wuerde es auch in den Zielen
landen -- also waere die Zeile oben nicht nur Zierde. */
const mitWasser = ERKANNT.replace("« 15 gültige", "« [email protected] 15 gültige");
const g2 = leseBild(mitWasser);
ok(g2.kuer.aufgaben.some((a) => /dogfather1608/.test(a)),
" Gegenprobe: stuende es im Text, stuende es auch in den Zielen");
}
/* =====================================================================
4. KEINE UHR, KEIN NETZ, KEINE DATENBANK
===================================================================== */
melde("");
melde("=== 4. Der Leser haengt an nichts ===");
{
/* OHNE DIE KOMMENTARE (berichtigt 08.10.2026, erster Lauf).
Die Datei ERKLAERT, warum sie die Texterkennung nicht aufruft --
und nannte sie dabei beim Namen. Die Muster unten fanden
„execFile" und „Tesseract" in genau diesen Saetzen und meldeten
einen Schaden, den es nicht gab.
Das ist derselbe Fehlgriff wie am 07.10.2026 beim Teilen, wo das
Wort „agentur" im eingebetteten Stilblatt fuer ein Leck gehalten
wurde: Eine Pruefung, die eine Erklaerung fuer die Sache haelt,
meldet jeden Tag einen Schaden -- und wird nach dem zweiten Mal
weggeklickt.
Gemessen wird der CODE. Der Kommentarraeumer ist absichtlich
schlicht; er muss nur Kommentare entfernen, nicht JavaScript
verstehen. */
const mitAllem = readFileSync(join(HIER, "kampagne-bild-lesen.mjs"), "utf8");
const quelle = mitAllem
.replace(/\/\*[\s\S]*?\*\//g, " ")
.replace(/(^|[^:])\/\/.*/g, "$1 ");
const verboten = [
[/\bimport\s/, "ein import"],
[/\bfetch\s*\(/, "fetch"],
[/\bDate\.now\s*\(/, "Date.now"],
[/new\s+Date\s*\(\s*\)/, "new Date() ohne Angabe"],
[/\bdb\s*\(/, "die Datenbank"],
[/readFileSync|writeFileSync/, "das Dateisystem"],
/* `(?<![.\w])` SCHLIESST `regex.exec(...)` AUS (berichtigt
08.10.2026). Das Muster traf `Z_DEUTSCH.exec(roh)` -- eine
Mustersuche, kein fremder Prozess. Ein Fehlalarm, der jeden Tag
kommt, wird nach dem zweiten Mal weggeklickt, zusammen mit dem
echten Befund darin (CLAUDE.md, 26.09.2026). */
[/\b(execFile|spawnSync|execSync|spawn)\s*\(|(?<![.\w])exec\s*\(/,
"ein fremder Prozess"],
[/tesseract/i, "die Texterkennung selbst"],
];
const drin = verboten.filter(([m]) => m.test(quelle));
ok(drin.length === 0,
`die Datei fuehrt NICHTS ein und fasst nichts an`
+ (drin.length ? ` — gefunden: ${drin.map((x) => x[1]).join(", ")}` : ""));
/* GEGENPROBE: Die Muster finden sehr wohl etwas, wenn es dasteht.
Ohne sie waere die Zeile oben auch bei kaputten Mustern gruen. */
ok(verboten.filter(([m]) => m.test('import x from "y"; fetch(); db(); tesseract')).length >= 4,
" Gegenprobe: die Muster schlagen an, wenn es wirklich dasteht");
/* UND DER KOMMENTARRAEUMER WIRFT NICHT ALLES WEG. Ohne diese Zeile
waere die Pruefung oben auch dann gruen, wenn `quelle` leer ist --
ein gruener Haken ueber einem leeren Feld. */
ok(quelle.length > 1500 && quelle.includes("export function leseBild"),
` und der Code selbst steht noch da (${quelle.length} von `
+ `${mitAllem.length} Zeichen, Rest sind Kommentare)`);
ok(/execFile|tesseract/i.test(mitAllem),
" Gegenprobe: in den KOMMENTAREN kommen die Woerter sehr wohl vor");
/* GLEICHE EINGABE, GLEICHES ERGEBNIS. Eine Lesefunktion mit einer
Uhr darin liefert zu verschiedenen Zeiten Verschiedenes -- und
eine Pruefung darauf ist eine Zeitbombe. */
const a = JSON.stringify(leseBild(ERKANNT));
const b = JSON.stringify(leseBild(ERKANNT));
ok(a.length > 500 && a === b,
`zwei Laeufe, ${a.length} Zeichen, Zeichen fuer Zeichen gleich`);
}
/* =====================================================================
5. DIE TEXTERKENNUNG SELBST
===================================================================== */
melde("");
melde("=== 5. Vom Bild zum Text ===");
{
let da = false;
try {
execFileSync("tesseract", ["--version"], { stdio: "ignore", timeout: 10_000 });
da = true;
} catch { da = false; }
if (!da) {
nichtNachsehbar(
"Tesseract ist auf dieser Maschine nicht eingerichtet — "
+ "der Schritt vom BILD zum TEXT wurde nicht gemessen.",
"Auf dem Server ist es vorhanden (5.5.0 mit deutschen Sprachdaten, "
+ "gemessen 08.10.2026); dort misst dieser Abschnitt wirklich. "
+ "Lokal nachruesten: winget install UB-Mannheim.TesseractOCR — "
+ "die Abschnitte 1 bis 4 laufen auch ohne.");
} else {
const bild = join(DATEN, DATEIEN.bild.name);
let text = "";
try {
text = execFileSync("tesseract",
[bild, "stdout", "-l", "deu", "--psm", "4"],
{ encoding: "utf8", timeout: 30_000, stdio: ["ignore", "pipe", "ignore"] });
} catch (f) {
text = "";
melde(` -- Tesseract brach ab: ${f?.message?.slice(0, 80)}`);
}
ok(text.length > 200, `aus dem Bild kommen ${text.length} Zeichen Text`);
/* NICHT „Zeichen fuer Zeichen gleich wie gespeichert": Eine
andere Tesseract-Fassung liest ein Komma anders, und dann waere
diese Pruefung rot, ohne dass etwas kaputt ist. Gemessen wird,
was zaehlt -- dass die KACHEL dieselbe wird. */
let g = null;
try { g = leseBild(text); } catch (f) { void f; g = null; }
ok(g !== null, "und der Leser macht daraus eine Kachel");
ok(g?.pflicht.titel === "[Star Light] Refined visuals Goal 1.0",
`mit demselben Titel: ${JSON.stringify(g?.pflicht.titel)}`);
ok(g?.pflicht.start === "2026-10-01" && g?.pflicht.ende === "2026-10-31",
`demselben Zeitraum: ${g?.pflicht.start}..${g?.pflicht.ende}`);
ok(g?.kuer.aufgaben.length === 3,
`und denselben drei Zielen (${g?.kuer.aufgaben.length})`);
}
}
melde("");
melde(`${geprueft} Pruefungen, ${fehler} Fehler`
+ (offen ? `, ${offen} nicht nachsehbar` : ""));
if (!geprueft) {
melde("ABBRUCH: nichts geprueft -- das ist ein Fehler, kein Erfolg.");
process.exit(3);
}
melde(fehler ? "NICHT IN ORDNUNG" : (offen ? "IN ORDNUNG, mit Luecke" : "ALLES IN ORDNUNG"));
process.exit(fehler ? 1 : 0);