diff --git a/.gitignore b/.gitignore index 7f60ec04..46c1374a 100644 --- a/.gitignore +++ b/.gitignore @@ -141,3 +141,21 @@ tools/.messkopf.txt # Das Arbeitsschloss gehoert dem Rechner, nicht dem Verlauf: Es sagt, # wer GERADE arbeitet. Committet waere es eine Behauptung von gestern. .arbeitsschloss + +# --------------------------------------------------------------------- +# AUSNAHME: server/pruefdaten/ IST VERSIONIERT (08.10.2026) +# +# Dort liegen PRUEFDATEN, keine erzeugten Bilder: gespeicherte Seiten, +# ein Erkennungstext und das Bild, aus dem er stammt. Ihre Pruefsummen +# stehen in den Pruefdateien -- ohne sie laeuft pruef-kampagne-lesen +# und pruef-kampagne-bild gar nicht erst an. +# +# Noetig wurde die Zeile, weil `bild-*.png` weiter oben die ERZEUGTEN +# Bildschirmfotos ausschliesst (bild-kampagne-1280.png und +# Geschwister) -- und `bild-agentur-nachbau.png` zufaellig genauso +# heisst. Zwei verschiedene Dinge, ein Muster. +# +# `.gitattributes` haelt diese Dateien ausserdem byteweise fest +# (`server/pruefdaten/** -text`), sonst wuerde Git die Zeilenenden +# umschreiben und die Pruefsummen brechen. +!server/pruefdaten/** diff --git a/server/kampagne-bild-lesen.mjs b/server/kampagne-bild-lesen.mjs new file mode 100644 index 00000000..408d1f9a --- /dev/null +++ b/server/kampagne-bild-lesen.mjs @@ -0,0 +1,599 @@ +/* ===================================================================== + EINE KAMPAGNE AUS EINEM BILDSCHIRMFOTO LESEN (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." + + WARUM ES DIESE DATEI UEBERHAUPT GIBT. Gemessen am 07.10.2026 an + seinem echten Agenturlink: `vm.tiktok.com/ZSbACwAnK/` fuehrt auf + `tiktok.com/tcn/activity?...` -- das Creator Network. Diese Seiten + baut TikTok erst im Browser auf, angemeldet; von aussen stehen dort + 233 KB leere Huelle. Dieselbe Kampagnennummer auf dem Weg der + oeffentlichen Kampagnenseite gibt 42 KB ohne Datenpaket, waehrend + eine echte Kampagne dort 967 KB MIT Datenpaket liefert. Die Kampagne + existiert also schlicht nicht als oeffentliche Seite. Kein Programm + kann sie von aussen lesen -- auch keines, das jemand anders baut. + + Was Filipe sieht, sieht nur sein Browser. Also wird genau das + gelesen: das Bild davon. + + --------------------------------------------------------------------- + HIER PASSIERT NICHTS AUSSER LESEN + + Keine Netzanfrage, keine Datenbank, kein Express, keine Datei, keine + Uhr, und auch KEINE Texterkennung. Text hinein, Daten heraus -- + dieselbe Regel wie bei kampagne-lesen.mjs, und aus demselben Grund: + So laesst sich das Auslesen gegen GESPEICHERTE Erkennungstexte + pruefen, ohne Tesseract, ohne Bild, ohne Server. Eine Pruefung, die + dafuer ein Bild durch die Texterkennung schicken muss, misst die + Tagesform von Tesseract statt unseren Code. + + Die Texterkennung selbst steht in workspace-kampagne.js -- an der + Stelle, an der es ohnehin einen Prozess und ein Dateisystem gibt. + + --------------------------------------------------------------------- + DIESELBE FORM WIE DER LINK-LESER + + `leseBild()` gibt `{ pflicht, kuer, hinweise, auszug }` zurueck -- + Feld fuer Feld dasselbe wie `leseKampagne()`. Das ist die wichtigste + Entscheidung in dieser Datei: Dadurch greifen Gliederung, + Geschenkplaketten, Aufklappen, Teilen, Steckbrief und Spaltenwahl + unveraendert weiter. Haette das Bild eine eigene Form, gaebe es zwei + Wege durch das Haus, und der zweite waere der, der beim naechsten + Umbau vergessen wird. + + --------------------------------------------------------------------- + DER ZEITRAUM WIRD ZWEIMAL GELESEN -- UND MUSS ZWEIMAL DASSELBE SAGEN + + Auf der Seite steht er an zwei Stellen, in zwei Schreibweisen: + + 10/01/2026 00:00:00 ~ 10/31/2026 22:59:59 (UTC+02:00) (oben) + Daten 01.10.2026 00:00 ~ 31.10.2026 22:59(UTC+02:00) (im Raster) + + Die obere ist amerikanisch (Monat zuerst), die untere deutsch (Tag + zuerst). Beide meinen den 1. bis 31. Oktober. Genommen wird die + DEUTSCHE -- sie ist eindeutig --, und die amerikanische dient als + Gegenprobe. Stimmen sie nicht ueberein, entsteht KEINE Kachel: + + Eine Kachel ohne Zeitraum ist unvollstaendig und sagt das selbst. + Eine Kachel mit dem FALSCHEN Zeitraum ist eine Falschauskunft -- sie + behauptet einen Termin, den sie nicht kennt, und man sieht es ihr + nicht an. Bei einer Texterkennung ist das keine theoretische Gefahr: + Aus einer 3 kann eine 8 werden, und „bis 08.10." statt „bis 03.10." + liest sich genauso plausibel. + ===================================================================== */ + +/** Was beim Lesen schiefgehen kann. Dieselbe Bauart wie + * `KampagneUnlesbar` in kampagne-lesen.mjs -- zwei Arten, damit die + * Route unterscheiden kann, was sie meldet. + * + * "pflicht" Titel oder Zeitraum fehlen / widersprechen sich + * "aufbau" das sieht nicht nach dieser Seite aus + */ +export class BildUnlesbar extends Error { + constructor(grund, art = "aufbau") { + super(grund); + this.name = "BildUnlesbar"; + this.art = art; + } +} + +/* --------------------------------------------------------------------- + MUSTER + + ALLE MIT TOLERANZ GEGEN DIE TEXTERKENNUNG. Gemessen an einer + nachgebauten Seite (server/pruefdaten/bild-agentur-*.txt): + + „Sichtbar für" kam als „Sichtbar für" durch -- gut + „22:59(UTC+02:00)" kam als „22:5XUTC+02:00)" durch -- die + Klammer ging verloren, die Ziffer auch + „Erstellt von" kam als „Erstellt von." durch -- ein Punkt + zu viel + „Creator*innen" kam als „Creator‘innen" und „Creatorinnen" + durch -- der Stern wandert + + Deshalb wird nirgends auf ein Satzzeichen vertraut, und hinter jedem + Schild steht `[\s.:]*` statt eines festen Trenners. Was dagegen NICHT + toleriert wird, sind die Ziffern des Zeitraums -- dafuer gibt es die + Gegenprobe oben. */ +const Z_DEUTSCH = new RegExp( + "Daten[\\s.:]*" + + "([0-9]{1,2})[.\\-/]([0-9]{1,2})[.\\-/]([0-9]{4})" + + "[^0-9]{0,12}(?:[0-9]{1,2}[:.][0-9]{2})?" + + "[\\s]*[~\\-–—][\\s]*" + + "([0-9]{1,2})[.\\-/]([0-9]{1,2})[.\\-/]([0-9]{4})", "i"); + +const Z_AMERIKANISCH = new RegExp( + "([0-9]{1,2})/([0-9]{1,2})/([0-9]{4})[\\s]+[0-9]{1,2}:[0-9]{2}:[0-9]{2}" + + "[\\s]*[~\\-–—][\\s]*" + + "([0-9]{1,2})/([0-9]{1,2})/([0-9]{4})", "i"); + +/* Eine Zonenangabe ist „Erdteil/Ort" -- und genau so steht sie da. + Ohne den Schraegstrich waere „Europe" allein keine Zone, die + `Intl.DateTimeFormat` kennt, und ein stillschweigendes Ausweichen + auf UTC waere ein anderer Tag (siehe tagInZone in + kampagne-lesen.mjs). */ +const ZONE = /Zeitzone[\s.:]*([A-Za-z]+\/[A-Za-z_+\-0-9]+)/i; + +/* DIE SCHILDER DES RASTERS -- mit ihrem GANZEN Namen. + + Die Texterkennung zieht die zwei Rasterspalten in EINE Zeile + zusammen: „Gruppenname X Zeitzone Y“. Gelesen wird deshalb + zeilenweise: Erst werden alle Schilder in der Zeile gesucht, + dann ist der Wert eines Schildes das, was bis zum NAECHSTEN + Schild steht. + + WARUM DER GANZE NAME (gemessen 08.10.2026, erster Lauf): + Mit dem Stueck „Sichtbar“ kam „Sichtbar für: für Privat“ heraus + -- das „für“ gehoert zum Schild, nicht zum Wert. Mit + „Anmeldung“ dasselbe: „Anmeldung erforderlich: erforderlich + Nein“. Ein halbes Schild laesst seine zweite Haelfte im Wert + stehen. + + UND WARUM „Creator“ KEIN SCHILD IST: Es steht mitten IM Wert + des Nachbarn („1 Creator Network, 55 Creator*innen“). Als + Schild gefuehrt schnitt es genau dort ab und machte aus der + Teilnehmerangabe „innen der. 1“. Das Schild heisst + „Creator*innen“ mit Stern -- und der Stern wandert bei der + Texterkennung, deshalb `Creator.{0,3}innen` und ein + Wortende dahinter. + + `muster` findet das Schild, `name` steht spaeter auf der + Kachel. Beides getrennt, weil das eine tolerant sein muss und + das andere lesbar. */ +const RASTER_SCHILDER = [ + { schluessel: "gruppenname", muster: /Gruppenname/i, name: "Gruppenname" }, + { schluessel: "zeitzone", muster: /Zeitzone/i, name: "Zeitzone" }, + { schluessel: "daten", muster: /Daten/i, name: "Daten" }, + { schluessel: "sichtbar", muster: /Sichtbar\s*f[uü]r/i, name: "Sichtbar für" }, + { schluessel: "erstellt", muster: /Erstellt\s*von/i, name: "Erstellt von" }, + /* „Teilnehmer*innen der ...“ -- auf dem Bild abgeschnitten, und + der Stern wandert. Der Punktrest am Ende gehoert zum Schild. */ + { schluessel: "teilnehmer", muster: /Teilnehmer.{0,3}innen(?:\s*der)?\s*\.{0,3}/i, + name: "Teilnehmer*innen" }, + { schluessel: "anmeldung", muster: /Anmeldung\s*erforderlich/i, + name: "Anmeldung erforderlich" }, + /* NUR AM ZEILENANFANG -- und das ist die wichtigste Zeile + dieser Liste (gemessen 08.10.2026, zweiter Lauf). + + „Creator*innen“ ist ein Schild UND steht zugleich mitten im + Wert des Nachbarn: „Teilnehmer*innen der ... 1 Creator + Network, 55 Creator*innen. Einzelheiten anzeigen“. Ohne diese + Einschraenkung schnitt es dort ab, und heraus kamen zwei + falsche Felder: „Teilnehmer*innen: 1 Creator Network, 55“ und + „Creator*innen: Einzelheiten anzeigen“. + + Im Raster steht dieses Schild in der LINKEN Spalte, also am + Zeilenanfang. Zwei Zeichen Spielraum fuer das, was die + Texterkennung links manchmal dazuerfindet. */ + { schluessel: "creator", muster: /Creator.{0,3}innen\b(?!\s*der)/i, + name: "Creator*innen", nurZeilenanfang: true }, +]; + +/* Aufzaehlungszeichen, wie die Texterkennung sie liefert. Gemessen: + Aus demselben runden Punkt wurden « (U+00AB) und ® (U+00AE) -- je + nach Zeile. Wer hier nur „•" erwartet, findet keine einzige + Aufgabe. */ +const AUFZAEHLUNG = /^[\s]*[«»®©@*•·▪◦‣>+\-–—]{1,3}[\s]*/u; + +/* DIE STATUSWORTE -- mit Toleranz gegen EINEN verlesenen + Anfangsbuchstaben (gemessen 08.10.2026): Aus „In Bearbeitung“ + wurde „in aearbeitung“. Das B vor „earbeitung“ ist genau das + Zeichen, das auf dunklem Grund am haeufigsten kippt. + + `name` ist, was auf der Kachel steht; `muster`, woran es + erkannt wird. Nie das Erkannte anzeigen -- sonst stuende + „in aearbeitung“ auf der Kachel. */ +const STATUSWORTE = [ + { name: "In Bearbeitung", muster: /\bin\s*\S?earbeitung\b/i }, + { name: "Nicht gestartet", muster: /\bnicht\s*\S?estartet\b/i }, + { name: "Beendet", muster: /\b\S?eendet\b/i }, + { name: "Abgeschlossen", muster: /\b\S?bgeschlossen\b/i }, + { name: "Entwurf", muster: /\b\S?ntwurf\b/i }, + { name: "Laufend", muster: /\b\S?aufend\b/i }, + { name: "Geplant", muster: /\b\S?eplant\b/i }, +]; + +/* --------------------------------------------------------------------- + KLEINE HELFER (alle rein) */ + +const zeilen = (text) => String(text || "") + .split(/\r?\n/).map((z) => z.trim()).filter(Boolean); + +const iso = (t, m, j) => `${j}-${String(m).padStart(2, "0")}-${String(t).padStart(2, "0")}`; + +/** Ist das ein moegliches Datum? Keine Uhr, kein Kalender -- nur die + * Frage, ob die Zahlen ueberhaupt einen Tag ergeben koennen. */ +function tagMoeglich(t, m, j) { + const tag = Number(t); + const monat = Number(m); + const jahr = Number(j); + if (!(monat >= 1 && monat <= 12)) return false; + if (!(jahr >= 2000 && jahr <= 2100)) return false; + const lang = [31, (jahr % 4 === 0 && jahr % 100 !== 0) || jahr % 400 === 0 ? 29 : 28, + 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][monat - 1]; + return tag >= 1 && tag <= lang; +} + +/** Den Wert hinter einem Schild holen -- bis zum naechsten Schild. + * + * WARUM NICHT „bis zum Zeilenende": Die Texterkennung zieht die zwei + * Rasterspalten in EINE Zeile zusammen. „Gruppenname Star Light + * Zeitzone Europe/Amsterdam" wuerde sonst den halben Nachbarn + * mitnehmen. */ +/** Alle Rasterfelder einer Zeile -- zerlegt an ihren Schildern. + * + * Erst werden alle Schilder gesucht und nach ihrer Stelle + * sortiert; der Wert eines Schildes ist dann genau das, was bis + * zum naechsten steht. Das ist der Unterschied zu „bis zum + * Zeilenende“ (nimmt den Nachbarn mit) und zu „bis zum ersten + * Wort, das wie ein Schild aussieht“ (schneidet mitten im Wert, + * gemessen am 08.10.2026 bei „1 Creator Network“). + * + * JEDES SCHILD NUR EINMAL JE ZEILE: `exec` ohne `g` liefert den + * ERSTEN Treffer. Stuende eines zweimal, gewaenne das erste -- + * und das ist auf dieser Seite auch das richtige. */ +/* Was die Seite neben einen Wert stellt und was die Texterkennung + daraus macht -- beides gehoert nicht in die Kachel. + + DIE FRAGEZEICHEN-KREISE der Seite (ⓘ) werden je nach Zeile als + `®`, `©`, `@` oder `0` gelesen. Gemessen: „Nein ⓘ“ kam als + „Nein ®“ durch und „1 ⓘ“ als „10“ -- das erste laesst sich + entfernen, das zweite nicht (aus „10“ ist nicht mehr zu sehen, + dass die 0 ein Kreis war). Deshalb wird nur abgeraeumt, was + eindeutig keine Angabe ist: ein EINZELNES Sonderzeichen am Ende. + + DIE KNOPFBESCHRIFTUNGEN („Einzelheiten anzeigen“) stehen im + selben Rasterfeld wie der Wert. Auf einer Kachel waeren sie ein + Hinweis auf etwas, das man dort nicht anklicken kann. */ +const WERT_ZIERDE = /[\s]*[®©@*^~|]+[\s]*$/u; +const WERT_KNOEPFE = /(Einzelheiten|Mehr|Weniger|Details)\s+anzeigen\s*\.?\s*$/i; + +function wertSaeubern(roh) { + let w = String(roh || ""); + w = w.replace(/^[\s.:,]+/, "").replace(WERT_KNOEPFE, ""); + w = w.replace(WERT_ZIERDE, "").replace(/[\s.:,]+$/, ""); + return w.trim(); +} + +function rasterZeile(zeile) { + const stellen = []; + for (const s of RASTER_SCHILDER) { + const m = s.muster.exec(zeile); + if (!m || m.index < 0) continue; + if (s.nurZeilenanfang && m.index > 2) continue; + stellen.push({ ...s, von: m.index, bis: m.index + m[0].length }); + } + stellen.sort((a, b) => a.von - b.von); + const aus = {}; + for (let i = 0; i < stellen.length; i += 1) { + const ende = i + 1 < stellen.length ? stellen[i + 1].von : zeile.length; + const wert = wertSaeubern(zeile.slice(stellen[i].bis, ende)); + if (wert) aus[stellen[i].schluessel] = wert; + } + return aus; +} + +/** Alle Rasterfelder des ganzen Textes. Spaetere Zeilen + * ueberschreiben nicht: Das erste Vorkommen gewinnt, weil das + * Raster oben steht und weiter unten dieselben Woerter in + * Fliesstext auftauchen koennen. */ +function rasterFelder(alle) { + const aus = {}; + for (const z of alle) { + for (const [k, v] of Object.entries(rasterZeile(z))) { + if (aus[k] === undefined) aus[k] = v; + } + } + return aus; +} + +/** Den Titel aus der Kopfzeile schaelen. + * + * Rechts daneben stehen Bedienelemente („Kampagne duplizieren", + * „Weniger anzeigen") und links ein Statusabzeichen. Beides gehoert + * nicht in den Titel einer Kachel. */ +function titelSaeubern(roh) { + let t = String(roh || "").trim(); + for (const wort of ["Kampagne duplizieren", "Weniger anzeigen", "Mehr anzeigen", + "Kampagne bearbeiten", "Einzelheiten anzeigen"]) { + const i = t.search(new RegExp(wort.replace(/ /g, "[\\s]+"), "i")); + if (i > 0) t = t.slice(0, i); + } + for (const s of STATUSWORTE) t = t.replace(s.muster, " "); + /* Das Pfeilzeichen des Aufklappers und uebrig gebliebene Zierde. */ + t = t.replace(/[\s^~*_|]+$/u, "").replace(/^[\s*_|]+/u, ""); + return t.trim(); +} + +/** Welches Statuswort steht oben? Nur zur Auskunft, nichts haengt daran. */ +function statusAus(text) { + for (const s of STATUSWORTE) if (s.muster.test(String(text || ""))) return s.name; + return null; +} + +/* ===================================================================== + LESEN + ===================================================================== */ + +/** Liest den Erkennungstext eines Kampagnen-Bildschirmfotos. + * + * @param {string} text was die Texterkennung geliefert hat + * @returns {{pflicht: object, kuer: object, hinweise: string[], auszug: object}} + * @throws {BildUnlesbar} + */ +export function leseBild(text) { + const roh = String(text || ""); + const alle = zeilen(roh); + const hinweise = []; + + if (alle.length < 3) { + throw new BildUnlesbar( + "auf dem Bild war fast kein Text zu erkennen -- " + + `${alle.length} Zeile(n)`, "aufbau"); + } + + /* EINMAL ZERLEGEN, DANN BENUTZEN. Vorher wurde fuer jedes Feld + einzeln im ganzen Text gesucht -- und dabei schnitt die Suche + mitten in den Werten der Nachbarn ab (gemessen 08.10.2026: + aus „1 Creator Network, 55 Creator*innen“ wurde „innen der. + 1“). Jetzt wird das Raster zeilenweise zerlegt, einmal. */ + const feld = rasterFelder(alle); + + /* ---- PFLICHT: der Zeitraum ------------------------------------ */ + const d = Z_DEUTSCH.exec(roh); + const a = Z_AMERIKANISCH.exec(roh); + + let start = null; + let ende = null; + let quelle = null; + + if (d && tagMoeglich(d[1], d[2], d[3]) && tagMoeglich(d[4], d[5], d[6])) { + start = iso(d[1], d[2], d[3]); + ende = iso(d[4], d[5], d[6]); + quelle = "deutsch"; + } + + let gegenStart = null; + let gegenEnde = null; + if (a && tagMoeglich(a[2], a[1], a[3]) && tagMoeglich(a[5], a[4], a[6])) { + /* Monat zuerst -- deshalb a[2] als Tag und a[1] als Monat. */ + gegenStart = iso(a[2], a[1], a[3]); + gegenEnde = iso(a[5], a[4], a[6]); + if (!start) { start = gegenStart; ende = gegenEnde; quelle = "amerikanisch"; } + } + + if (!start || !ende) { + throw new BildUnlesbar( + /* ANFUEHRUNGSZEICHEN: oeffnend „ (U+201E), schliessend “ + (U+201C) -- NIE das gerade ". Das gerade beendet die + Zeichenkette, und der Uebersetzer meldet einen Fehler zwanzig + Zeilen weiter unten. Dieses Haus hat das schon zweimal einen + Abend gekostet. */ + "auf dem Bild war kein Zeitraum zu lesen -- gesucht wurde " + + "„Daten 01.10.2026 ... ~ 31.10.2026 ...“ und " + + "„10/01/2026 00:00:00 ~ 10/31/2026 22:59:59“", "pflicht"); + } + + /* DIE GEGENPROBE. Stehen beide Schreibweisen da und sagen + Verschiedenes, hat die Texterkennung sich bei einer Ziffer vertan + -- und welche die richtige ist, weiss niemand. Dann lieber keine + Kachel als eine mit einem erfundenen Termin. */ + if (quelle === "deutsch" && gegenStart + && (gegenStart !== start || gegenEnde !== ende)) { + throw new BildUnlesbar( + `der Zeitraum steht auf dem Bild zweimal und beide Male anders ` + + `(${start} bis ${ende} gegen ${gegenStart} bis ${gegenEnde}) -- ` + + "bitte ein schärferes Bild einfügen", "pflicht"); + } + if (!gegenStart) { + hinweise.push("Der Zeitraum konnte nur an einer Stelle des Bildes " + + "geprüft werden — bitte Anfang und Ende kurz nachsehen."); + } + + if (ende < start) { + throw new BildUnlesbar( + `das Ende liegt vor dem Anfang (${start} bis ${ende}) -- ` + + "da hat die Texterkennung sich vertan", "pflicht"); + } + + /* ---- PFLICHT: der Titel --------------------------------------- */ + /* ERST „Gruppenname", DANN die Kopfzeile. Im Raster steht der Titel + ohne Abzeichen und ohne Bedienelemente daneben -- also sauberer. + Die Kopfzeile ist die Ausweiche. */ + let titel = titelSaeubern(feld.gruppenname || ""); + if (!titel || titel.length < 2) titel = titelSaeubern(alle[0]); + if (!titel || titel.length < 2) { + throw new BildUnlesbar( + "auf dem Bild war kein Titel zu lesen", "pflicht"); + } + titel = titel.slice(0, 160); + + /* ---- Zeitzone -------------------------------------------------- */ + const zoneTreffer = ZONE.exec(roh); + const zone = zoneTreffer ? zoneTreffer[1] : null; + if (!zone) { + hinweise.push("Die Zeitzone war auf dem Bild nicht zu lesen — " + + "die Tage stehen so da, wie sie auf dem Bild standen."); + } + + /* ---- Die Aufgaben ---------------------------------------------- */ + /* WO SIE ANFANGEN: nach der Phasenueberschrift. Steht die nicht da, + werden alle Aufzaehlungszeilen genommen -- auf dieser Seite gibt + es oberhalb keine. */ + const phaseAb = alle.findIndex((z) => + /Ziele erreichen|Phasendetails|Endet in|Endetin/i.test(z)); + /* AB DER ZEILE DANACH, nicht ab ihr selbst (berichtigt + 08.10.2026). + + Die Phasenueberschrift traegt auf der Seite ein Zielscheiben- + Zeichen, und die Texterkennung macht daraus ein `@`. Damit + sah sie aus wie eine Aufzaehlungszeile, und die erste + „Aufgabe“ hiess „Ziele erreichen Endetin 23 Tage 22 : 50 : + 44“ -- ein Haken, den niemand abhaken kann. + + Die Zeile selbst ist die Ueberschrift; was zaehlt, steht + darunter. */ + const ab = phaseAb >= 0 ? phaseAb + 1 : 0; + const aufgaben = []; + const gesehen = new Set(); + /* WAS NIE EINE AUFGABE IST (berichtigt 08.10.2026, zweiter + Lauf). `ab` reicht nicht: Gesucht wird die erste Zeile, die + nach der Phase aussieht, und das war „Phasendetails + Creator*innen-Leistung“ -- eine Zeile VOR der eigentlichen + Ueberschrift. Die rutschte dadurch wieder hinein, mitsamt + ihrem Countdown: „Ziele erreichen Endetin 23 Tage 22 : 50 : + 44“. + + Statt die Startzeile noch genauer zu suchen, wird gesagt, was + KEINE Aufgabe ist. Das haelt auch, wenn die Seite die Bloecke + einmal anders anordnet. */ + const KEINE_AUFGABE = /Endet\s*in|Endetin|Phasendetails|Leistung|Ziele erreichen/i; + for (const z of alle.slice(ab)) { + if (!AUFZAEHLUNG.test(z)) continue; + if (KEINE_AUFGABE.test(z)) continue; + /* EIN PUNKT AM ENDE IST FAST IMMER ZUFALL. Gemessen kam + „Geh LIVE für 900 Minuten.“ heraus, auf der Seite steht es + ohne Punkt. Eine Aufgabe ist eine Beschriftung, kein Satz; + und der Haken haengt an ihrem Text, also soll der stimmen. + Entfernt wird NUR ein einzelnes Satzzeichen am Ende -- ein + „...“ oder ein Fragezeichen bleibt stehen. */ + const nur = z.replace(AUFZAEHLUNG, "").trim().replace(/(? /Gruppenbeschreibung/i.test(z)); + if (beschrAb >= 0) { + for (const z of alle.slice(beschrAb + 1, beschrAb + 4)) { + if (/Phasendetails|Creator.{0,3}innen-Leistung|Ziele erreichen/i.test(z)) break; + if ((z.match(/\p{L}/gu) || []).length < 3) continue; + einleitung = z; + break; + } + } + + /* ---- Der Steckbrief -------------------------------------------- */ + /* Dieselben Felder wie beim Link-Leser, soweit es sie gibt. Was + leer ist, faellt weg statt als „—" zu erscheinen. */ + const steckbrief = { + nummer: null, + kurznummer: null, + region: null, + sprache: null, + verbund: feld.erstellt || null, + zeitzone: zone, + anmeldung_ab: null, + ergebnis_bis: null, + }; + + const kuer = { + einleitung, + einleitungTitel: null, + aufgaben, + aufgabenTitel: aufgaben.length ? "Ziele erreichen" : null, + aufgabenGruppen: [], + weitereRegeln: [], + geschenke: [], + geschenkTitel: null, + geschenkHinweis: null, + preise: [], + preisTitel: null, + klapptexte: [], + freitext: null, + rangliste: null, + steckbrief, + laender: [], + bannerUrl: null, + }; + + /* ---- Was sonst noch dastand ------------------------------------ */ + /* Als eigener Abschnitt im Regeltext -- mit „## " davor, damit die + Gliederung ihn als Ueberschrift erkennt (siehe gliederung() in + kampagne-lesen.mjs). */ + /* DIE NAMEN KOMMEN AUS DER SCHILDERLISTE, nicht aus einer + zweiten hier. Zwei Listen derselben Woerter laufen + auseinander, und dann heisst dasselbe Feld an zwei Stellen + verschieden. */ + const nebenbei = []; + /* „creator“ STEHT HIER ABSICHTLICH NICHT (08.10.2026). + Auf der Seite steht „1 ⓘ“ -- und der Fragezeichen-Kreis wird als + Null gelesen. Heraus kam „Creator*innen: 10“, also eine falsche + Zahl, und aus dem Erkennungstext ist nicht mehr zu sehen, ob die + 0 ein Kreis war oder eine Ziffer. Eine Zahl, der man nicht trauen + kann, gehoert nicht auf eine Kachel: Keine Angabe ist richtig, + eine falsche Angabe ist falsch. Die nuetzliche Zahl steht ohnehin + daneben („55 Creator*innen“ in der Teilnehmerzeile). */ + for (const schluessel of ["sichtbar", "teilnehmer", "anmeldung", "erstellt"]) { + const w = feld[schluessel]; + if (!w) continue; + const s = RASTER_SCHILDER.find((x) => x.schluessel === schluessel); + nebenbei.push(`${s ? s.name : schluessel}: ${w}`); + } + if (nebenbei.length) { + kuer.weitereRegeln.push({ titel: "Angaben der Kampagne", zeilen: nebenbei }); + } + + const status = statusAus(alle[0] || "") || statusAus(roh); + + return { + pflicht: { titel, start, ende, id: null, zone: zone || "" }, + kuer, + hinweise, + auszug: { + gelesen_aus: "bildschirmfoto", + sprache: "de-DE", + start_ms: null, + ende_ms: null, + zeitzone: zone, + zeitzone_versatz: null, + status: null, + status_anzeige: status, + kurznummer: null, + laender: [], + bausteine: [], + einleitung, + einleitungTitel: null, + aufgaben, + aufgabenTitel: kuer.aufgabenTitel, + aufgabenGruppen: [], + geschenke: [], + geschenkTitel: null, + geschenkHinweis: null, + preise: [], + preisTitel: null, + klapptexte: [], + rangliste: null, + steckbrief, + banner_url: null, + weitere_bilder: [], + woerterbuch_eintraege: 0, + platzhalter_ohne_eintrag: 0, + /* DER ERKANNTE TEXT KOMMT MIT. Geht spaeter etwas schief -- + ein Feld falsch, ein Datum daneben --, steht hier, WAS die + Texterkennung wirklich gesehen hat. Ohne ihn liesse sich ein + Fehlgriff nur nachstellen, indem man dasselbe Bild noch + einmal schickt, und das Bild hat vielleicht niemand mehr. + Gekuerzt, damit der Auszug nicht aufgeblaeht wird. */ + erkannter_text: roh.slice(0, 8000), + erkannte_zeilen: alle.length, + zeitraum_quelle: quelle, + zeitraum_gegenprobe: gegenStart ? `${gegenStart}..${gegenEnde}` : null, + }, + }; +} diff --git a/server/pruef-kampagne-bild.mjs b/server/pruef-kampagne-bild.mjs new file mode 100644 index 00000000..804d7686 --- /dev/null +++ b/server/pruef-kampagne-bild.mjs @@ -0,0 +1,403 @@ +/* ===================================================================== + 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", "« 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); diff --git a/server/pruefdaten/bild-agentur-erkannt.txt b/server/pruefdaten/bild-agentur-erkannt.txt new file mode 100644 index 00000000..e464ad16 --- /dev/null +++ b/server/pruefdaten/bild-agentur-erkannt.txt @@ -0,0 +1,22 @@ +[Star Light] Refined visuals Goal 1.0 in aearbeitung Kampagne duplizieren Weniger anzeigen * +10/01/2026 00:00:00 - 10/31/2026 22:59:59 (UTC+02:00) + +Grundlegende Informationen +Gruppenname [Star Light] Refined visuals Goal 1.0 Zeitzone Europe/Amsterdam + +Daten 01.10.2026 00:00 - 31.10.2026 22:5XUTC+02:00) Sichtbar für Privat. + +Erstellt von. Spicy Media Teilnehmerinnen der. 1 Creator Network, 55 Creator*innen. Einzelheiten anzeigen +Creator*innen 10 Anmeldung erforderlich Nein ® + +Gruppenbeschreibung + +Zusätzliche Prämien für das Team im Oktober. + +Phasendetails _Creator‘innen-Leistung + +@ Ziele erreichen Endetin 23 Tage 22 : 50 : 44 + +«Geh LIVE für 900 Minuten. +« 15 gültige LIVE-Gehen-Tage haben +® An 20 Tagen Ziele für optimierte Bildqualität erreichen diff --git a/server/pruefdaten/bild-agentur-nachbau.html b/server/pruefdaten/bild-agentur-nachbau.html new file mode 100644 index 00000000..3614a62c --- /dev/null +++ b/server/pruefdaten/bild-agentur-nachbau.html @@ -0,0 +1,128 @@ + + +
+ + + + + + +10/01/2026 00:00:00 ~ 10/31/2026 22:59:59 (UTC+02:00)
+ +Zusätzliche Prämien für das Team im Oktober.
+Titel, Zeitraum, Banner, Aufgaben und Preise kommen von selbst.
+Aus dem Link kommen Titel, Zeitraum, Banner, Aufgaben und Preise von selbst — aus einem Bildschirmfoto Titel, Zeitraum und Ziele.