/* ===================================================================== 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"); /* ---- EINE AUFGABE DARF HEISSEN, WIE SIE WILL (08.10.2026) --- GEFUNDEN IM ECHTEN BETRIEB, nicht beim Bauen: Filipes erster Versuch am 08.10.2026 kam laut Protokoll mit ZWEI Aufgaben an, waehrend auf seinem Bild DREI standen — und er hat die Kachel danach geloescht. Die Ursache war die Regel, die die Phasenueberschrift aussortiert. Sie warf jede Zeile weg, in der „Ziele erreichen“, „Leistung“ oder „Phasendetails“ IRGENDWO vorkam — also auch gewoehnliche Ziele, die zufaellig dieselben Woerter benutzen. Eine Ueberschriftenregel, die Aufgaben verschluckt. IM ZWEIFEL BLEIBT DIE ZEILE. Eine Ueberschrift zu viel in der Hakenliste loescht man in zwei Sekunden. Eine fehlende Aufgabe merkt niemand — bis jemand sie nicht erfuellt. */ { const mitFallen = [ "Kampagne mit Fallen", "Gruppenname Kampagne mit Fallen Zeitzone Europe/Berlin", "Daten 01.10.2026 ~ 31.10.2026", "@ Ziele erreichen Endetin 23 Tage 22 : 50 : 44", "· Ziele erreichen für mehr Reichweite", "· Deine Leistung steigern", "· An 20 Tagen Ziele für optimierte Bildqualität erreichen", "· Phasendetails im Kalender eintragen", ].join("\n"); const g2 = leseBild(mitFallen); ok(g2.kuer.aufgaben.length === 4, `vier Aufgaben, obwohl drei davon Ueberschriftwoerter tragen ` + `(${g2.kuer.aufgaben.length}): ${JSON.stringify(g2.kuer.aufgaben)}`); ok(g2.kuer.aufgaben.includes("Ziele erreichen für mehr Reichweite"), " „Ziele erreichen für mehr Reichweite“ ist eine Aufgabe"); ok(g2.kuer.aufgaben.includes("Deine Leistung steigern"), " „Deine Leistung steigern“ auch"); /* GEGENPROBE: Die ECHTE Ueberschrift mit ihrem Countdown faellt weiterhin heraus. Ohne sie waere „alles bleibt stehen“ auch dann gruen, wenn gar nichts mehr aussortiert wird. */ ok(!g2.kuer.aufgaben.some((a) => /Endetin|Endet in/i.test(a)), " die Zeile MIT Countdown bleibt draussen"); /* Und eine Zeile, die NUR aus dem Ueberschriftwort besteht, ebenfalls -- sonst stuende „Phasendetails“ als Haken da. */ const nurWort = leseBild([...mitFallen.split("\n"), "· Phasendetails"].join("\n")); ok(!nurWort.kuer.aufgaben.includes("Phasendetails"), " und eine Zeile, die nur das Wort ist, auch"); } 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`); /* ---- DER TITEL WIRD GEGENGELESEN (08.10.2026) -------------- ANLASS WAR EIN ECHTER FALL: Filipe hat den Bildweg benutzt, und aus „Refined visuals Goal 1.0“ wurde „Refined visvals Goal 1.0“ — ein u als v. Genau die Verwechslung, die man beim Korrekturlesen überliest, weil das Wort noch fast richtig aussieht. Auf der Kachel stand sie still da. NACHGEMESSEN, OB DIE SPRACHWAHL HILFT: Auf dem Server gegen die gespeicherte Prüfdatei mit `deu`, `eng`, `deu+eng` und `eng+deu` — alle vier lesen „visuals“ RICHTIG. Der Fehler ließ sich also nicht nachstellen, und eine Einstellung zu ändern, deren Wirkung man nicht messen kann, ist Raten. Es bleibt bei `deu`. WAS STATTDESSEN HILFT: Der Titel steht auf der Seite ZWEIMAL — oben als Überschrift (groß, fett) und im Raster hinter „Gruppenname“ (klein, normal). Zwei Schriftgrößen heißt zwei Gelegenheiten, sich zu verlesen, und zwar unabhängig voneinander. Gehen die beiden Lesungen auseinander, sagt die Kachel es. EIN HINWEIS, KEINE ABLEHNUNG — anders als beim Zeitraum. Ein falsches Datum ist eine Falschauskunft; ein verlesener Buchstabe im Titel ein Schönheitsfehler. Daran eine ganze Kachel scheitern zu lassen wäre unverhältnismäßig. */ { const zweierlei = [ "【Star Light】Refined visvals Goal 1.0 In Bearbeitung", "Gruppenname 【Star Light】Refined visuals Goal 1.0 Zeitzone Europe/Berlin", "Daten 01.10.2026 00:00 ~ 31.10.2026 22:59", ].join("\n"); const g = leseBild(zweierlei); /* ERST BELEGEN, DASS DIE KACHEL ÜBERHAUPT ENTSTEHT. Ohne diese Zeile wäre das Folgende auch bei einer Ablehnung grün. */ ok(g.pflicht.titel.includes("Refined"), `die Kachel entsteht trotzdem: ${JSON.stringify(g.pflicht.titel)}`); ok(g.hinweise.some((h) => /zweimal auf dem Bild/i.test(h)), `und sagt, dass der Titel zweimal verschieden gelesen wurde: ` + `${JSON.stringify(g.hinweise.find((h) => /zweimal/i.test(h)) || null)}`); /* BEIDE LESUNGEN KOMMEN MIT. Fällt später auf, dass die falsche gewonnen hat, steht die andere schon da. */ ok(g.auszug.titel_raster && g.auszug.titel_kopf && g.auszug.titel_raster !== g.auszug.titel_kopf, `und beide stehen im Auszug: ${JSON.stringify( [g.auszug.titel_raster, g.auszug.titel_kopf])}`); /* GEGENPROBE: Stimmen beide überein, gibt es KEINEN Hinweis. Ohne sie wäre „der Hinweis kommt“ auch dann grün, wenn er immer käme — und ein Hinweis, der immer kommt, wird überlesen (CLAUDE.md, 03.09.2026). */ const einig = zweierlei.replace("visvals", "visuals"); const h = leseBild(einig); ok(!h.hinweise.some((x) => /zweimal auf dem Bild/i.test(x)), `Gegenprobe: bei zwei gleichen Lesungen kommt kein Hinweis ` + `(${h.hinweise.length} Hinweise)`); /* UND DER ECHTE ERKENNUNGSTEXT LÖST IHN AUCH NICHT AUS. Sonst stünde er an jeder Kachel, und das wäre derselbe Fehler. */ const echt = leseBild(ERKANNT); ok(!echt.hinweise.some((x) => /zweimal auf dem Bild/i.test(x)), "und am gespeicherten Erkennungstext ebenso wenig"); } /* 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", "« dogfather1608@gmail.com 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"], /* `(? 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);