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]>
This commit is contained in:
@@ -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(/(?<![.!?])[.,;]$/u, "").trim();
|
||||
/* Mindestens vier Buchstaben: Ein einzelnes Zeichen, das die
|
||||
Texterkennung fuer einen Punkt gehalten hat, ist keine
|
||||
Aufgabe. */
|
||||
if ((nur.match(/\p{L}/gu) || []).length < 4) continue;
|
||||
if (gesehen.has(nur)) continue;
|
||||
gesehen.add(nur);
|
||||
aufgaben.push(nur);
|
||||
}
|
||||
if (!aufgaben.length) {
|
||||
hinweise.push("Auf dem Bild war keine Aufgabenliste zu erkennen — "
|
||||
+ "bitte nachtragen.");
|
||||
}
|
||||
|
||||
/* ---- Die Beschreibung ------------------------------------------ */
|
||||
/* Sie steht UNTER ihrer Ueberschrift, nicht dahinter -- anders als
|
||||
die Rasterfelder. Genommen wird die naechste Zeile, die kein
|
||||
Schild ist. */
|
||||
let einleitung = null;
|
||||
const beschrAb = alle.findIndex((z) => /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,
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user