Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BNhLunz586g6DeYMa7Qsd1
199 lines
14 KiB
TypeScript
199 lines
14 KiB
TypeScript
import type { APIRoute } from 'astro';
|
||
|
||
export const prerender = false;
|
||
|
||
/**
|
||
* Maschinen-/KI-lesbare Kurzdoku unter /llms.txt (öffentlich, ohne Geheimnisse).
|
||
* Beschreibt Zweck, Fähigkeiten und die HTTP-/MCP-Schnittstelle von Klarbild,
|
||
* damit ein KI-Agent das Tool ohne Weboberfläche bedienen kann.
|
||
*/
|
||
const BODY = `# Klarbild
|
||
|
||
> Selbst gehostetes KI-Bildwerkzeug: verwandelt Screenshots und Fundstücke in
|
||
> saubere, druckfertige Bilder (bereinigen, freistellen, exakte Fotoformate,
|
||
> The-Frame-Querformat, Sticker mit Kontur), liefert an Picdrop (SFTP/FTPS)
|
||
> aus und sichert auf beliebige Backup-Ziele (z. B. NAS). Zusätzlich ein
|
||
> KI-freies Druckmodul (/drucken): Bilder exakt auf physische Maße bringen und mehrere
|
||
> davon in 100-%-Größe mit Schnittmarken auf einen Druckbogen setzen (PDF).
|
||
> Bedienung über Web, Telegram-Bot und HTTP-API/MCP.
|
||
|
||
Basis-URL: https://klarbild.heidrich-digital.de
|
||
Öffentliche Vorstellung des Druckmoduls (ohne Login, reine Beschreibung): /vorstellung
|
||
Öffentliche Teilen-Links: /s/<slug> (ohne Login, nur was der Link freigibt)
|
||
Sprache der Oberfläche: Deutsch. Stack: Astro 5 (SSR) · Postgres · sharp · pg-boss.
|
||
|
||
## Authentifizierung (für Agenten)
|
||
- Header: \`Authorization: Bearer <API-Token>\` (Admin → „Automatisierung / MCP-Zugriff").
|
||
- Der Token gilt für \`/api/*\` — NICHT für \`/api/admin/*\` (nur per Login-Session).
|
||
- Ohne Token/Session antworten API-Routen mit 401.
|
||
|
||
## Kernbegriffe
|
||
- Modus (mode): \`each\` (Screenshot bereinigen/freistellen/formatieren),
|
||
\`compose\` (ein Bild umwandeln ODER mehrere kombinieren, mit Textbeschreibung),
|
||
\`generate\` (neues Bild allein aus Text).
|
||
- Aufgaben (tasks, nur bei each): \`clean\`, \`cutout\`, \`format\`, \`contour\`.
|
||
- Rezept (recipe): gespeicherte Voreinstellung. Auftrag (job): ein Stapel; Position (item): ein Bild.
|
||
- Auftrags-Status: queued, running, paused, done, failed, cancelled. \`failed\` = alle Positionen fehlgeschlagen.
|
||
|
||
## Formate (output_format)
|
||
- Feste Schlüssel: 9x13,10x15,13x18,15x20,20x30,30x40,30x45,40x50,40x60,50x70,60x90,
|
||
A4,A3,A2,20x20,30x30, theframe (3840x2160), hochformat (2160x3840), keep (Original behalten).
|
||
- Freie Formate ohne Vorgabe: \`BREITExHOEHE\` in cm (z. B. \`25x35\`) oder \`sticker<N>\`
|
||
für N×N cm (z. B. \`sticker5\` = 5×5 cm). Grenze 300 cm. Exakte Pixel: round(cm/2.54*dpi).
|
||
- Dateiformat (output_ext): \`png\` (verlustfrei, Transparenz) oder \`jpg\` (klein, Picdrop-freundlich).
|
||
JPG nur ohne Transparenz; freigestellte Motive bleiben immer PNG. Global (Admin) oder pro Rezept.
|
||
|
||
## HTTP-API (Auswahl, JSON)
|
||
- GET /api/health — Status (öffentlich).
|
||
- GET /api/me — aktueller Nutzer/Rechte.
|
||
- GET /api/models — verfügbare Bildmodelle (Qualitätsstufen).
|
||
- GET /api/recipes — Rezepte/Presets auflisten.
|
||
- POST /api/recipes — Rezept anlegen. Body u. a.: name, mode, tasks[], output_format,
|
||
orientation(portrait|landscape), crop_mode(crop|extend), dpi, contour_mm, output_ext(png|jpg|null),
|
||
delivery(library|picdrop|both), picdrop_gallery, delivery_target_id.
|
||
- DELETE /api/recipes/:id — Rezept löschen.
|
||
- GET /api/delivery-targets — Auslieferungs-/Backup-Ziele.
|
||
- POST /api/jobs — Auftrag starten. Body: { recipe, mode, delivery, private,
|
||
prompt_text?(compose/generate), sources:[{source_path, filename}] }. Antwort: { jobId }.
|
||
- GET /api/jobs — letzte Aufträge. GET /api/jobs/:id — Auftrag + Positionen (inkl. error_message, finished_at).
|
||
- POST /api/jobs/:id/{pause|resume|cancel|retry-failed|delete}.
|
||
- POST /api/items/:id/{retry|reuse|alternative}.
|
||
- GET /api/items/:id/file — Ergebnisbild. Query: ?thumb=1 (Vorschau), ?preview=1 (kleines JPG fürs Vollbild),
|
||
?download=1 (als Datei), ?src=1 (Quelle). Content-Type wird aus den Magic Bytes bestimmt (PNG/JPG/WEBP).
|
||
|
||
## Drucken — exakte Maße & Druckbogen (ohne KI, /drucken)
|
||
Rein lokale Geometrie: Zuschnitt (sharp) + PDF (pdf-lib). Kein Modell, keine Kosten.
|
||
- Maßangaben: \`12x15\` = 12×15 cm · \`35x45mm\` · \`5\` = 5×5 cm · \`4:3/15\` = Verhältnis 4:3, längere Kante 15 cm.
|
||
- Papierformate (id): A6,A5,A4,A3,A3plus(329×483 mm),A2, F9x13,F10x15,F13x18,F15x20,F20x30, Letter, Legal — oder freies Maß.
|
||
- Bildformate: 86 Vorgaben in 10 Gruppen (Passbild, Klein, Foto, Zoll (US), Sofortbild,
|
||
Poster & Rahmen, Quadrat, DIN, Karten, Seitenverhältnis) — oder freies Maß. Auswahl der ids:
|
||
P35x45 (biometrisch), P50x50 · K20x30,K30x40,K40x50,K45x60,K60x80,K60x90 ·
|
||
S9x13,S10x15,S11x15,S12x15,S13x18,S15x20,S18x24,S20x25,S20x30,S24x30 ·
|
||
R30x40,R30x45,R40x50,R40x60,R50x70,R60x80,R60x90,R70x100 (Poster & Rahmen) ·
|
||
Q10x10,Q13x13,Q20x20,Q30x30 · DA6,DA5,DA4,DA3,DA2 ·
|
||
W16x9,W9x16,W3x2,W4x3,W5x4 (Seitenverhältnisse als Druckmaß, u. a. The Frame).
|
||
Damit sind alle Formate der KI-Generierung (src/lib/format.ts) auch hier druckbar.
|
||
- Passt das Bild nicht zum Format (fit je Zelle): \`cover\` = zuschneiden bis ausgefüllt (Standard,
|
||
nichts bleibt leer, außen fehlt etwas) oder \`contain\` = ganzes Bild einpassen, außen bleibt die
|
||
Randfarbe \`bg\` (#rrggbb, Standard #ffffff). Verzerrt wird nie.
|
||
- Schnitthilfen (marks.mode): \`none\` · \`corner\` (Eckmarken außerhalb des Endformats, Standard 4 mm lang,
|
||
3 mm Versatz, 0,25 pt Haarlinie) · \`grid\` (durchgehende Linien über den Bogen, laufen nie durch ein Motiv).
|
||
- Beschnittzugabe (bleedMm): das Bild ragt je Seite darüber hinaus; die Trimmbox bleibt exakt der gewählte Ausschnitt.
|
||
Der Abstand zwischen zwei Bildern ist immer ≥ 2 × Beschnittzugabe.
|
||
- Platzierung: gleich große Bilder → exaktes Raster; gemischte Größen → MaxRects-Packer mit fester
|
||
Ausrichtung je Format (probiert alle Ausrichtungs-Kombinationen und nimmt die mit den wenigsten Bogen).
|
||
- Die PDF-Seite hat exakt das Bogenmaß. Beim Drucken „Tatsächliche Größe / 100 %" wählen — nicht „an Seite anpassen".
|
||
- **Größer als der Bogen** (z. B. 20 × 30 cm auf DIN-A4-Fotopapier) — je Zelle über \`oversize\`:
|
||
\`fit\` = melden, dass es nicht passt (Standard) · \`overflow\` = mittig aufs ganze Blatt legen,
|
||
der Überstand wird beim Drucken abgeschnitten (20 × 30 auf A4: genau 3 mm in der Höhe) ·
|
||
\`tile\` = Posterdruck über mehrere Blätter mit Klebefalz (\`overlapMm\`, Standard 10 mm, gestrichelt markiert;
|
||
jedes Blatt trägt Nummer, Reihe und Spalte). 30 × 40 cm auf A4 ergibt 2 × 2 Blätter.
|
||
- **Beschriftung** (\`footer\`): \`false\` = aus · \`true\` = automatisch (Maße, dpi, Druckhinweis) ·
|
||
\`{ text, position, sizePt }\` = eigener Text. Position: \`unten-links|unten-mitte|unten-rechts|
|
||
oben-links|oben-mitte|oben-rechts\`. Die Zeile entfällt automatisch, wenn an der gewählten
|
||
Kante kein Platz frei ist — sie wird nie über ein Bild gedruckt.
|
||
- Dasselbe Bild darf mehrfach im \`cells\`-Feld stehen (verschiedene \`id\`, gleiche \`src\`), um es
|
||
in mehreren Maßen auf einen Bogen zu setzen. In der Weboberfläche macht das der Knopf „Duplizieren".
|
||
|
||
- POST /api/print/single — ein Bild exakt auf Maß. Body: { src:{kind:'upload',path}|{kind:'item',id},
|
||
crop?:{x,y,w,h} (relativ 0..1; ohne Angabe mittiger Ausschnitt), size|wMm+hMm, landscape?, dpi?, ext?(jpg|png), bleedMm? }.
|
||
Antwort: die Bilddatei. Header: X-Klarbild-Px, X-Klarbild-Real-Dpi, X-Klarbild-Dpi-Ok.
|
||
- GET /api/print/single?size=12x15&dpi=300 — nur rechnen: mm → Pixel.
|
||
- POST /api/print/sheet — Druckbogen als PDF. Body: { paper:{id}|{size}|{wMm,hMm}, landscape?, marginMm?,
|
||
gapMm?, bleedMm?, center?, dpi?, ext?, footer?, marks:{mode,lengthMm?,offsetMm?},
|
||
cells:[{ id, src, crop?, size|wMm+hMm, landscape?, count?, allowRotate?, fit?, bg?,
|
||
oversize?(fit|overflow|tile), overlapMm? }] }.
|
||
Antwort: application/pdf. Header: X-Klarbild-Pages, X-Klarbild-Per-Sheet.
|
||
Grenzen: 40 Zellen, 200 Exemplare je Zelle, 500 Einzelbilder und 60 Bogen je Auftrag.
|
||
- GET/POST/DELETE /api/print/presets — Bogen-Vorlagen (Papier, Marken, Formate, Stückzahlen).
|
||
Mitgeseedet: „Kita-Satz (A4)", „Schulsatz klein (A4)", „Passbildbogen 35×45 (A4)",
|
||
„2 × 10×7,5 auf Fotopapier 10×15". Eine Vorlage mit mehreren Formaten und nur einem Bild
|
||
klont das Bild in alle Formate.
|
||
- Bogen ausliefern: \`deliver: { target: ''|'nas'|<uuid>, gallery? }\` in /api/print/sheet legt das PDF
|
||
zusätzlich ins Ziel. Antwort bleibt das PDF; Header X-Klarbild-Delivered (0|1) und
|
||
X-Klarbild-Delivery-Msg (URI-kodiert).
|
||
|
||
## MCP-Server (für Assistenten)
|
||
Werkzeug \`klarbild\` (mcp/klarbild-mcp.mjs), nutzt denselben API-Token:
|
||
- list_recipes — verfügbare Rezepte.
|
||
- process_images — Bilder (lokaler Ordner / URLs) mit einem Rezept verarbeiten lassen.
|
||
- job_status — Fortschritt/Ergebnis eines Auftrags abfragen.
|
||
- exact_size — OHNE KI: ein Bild exakt auf ein Maß bringen, Datei lokal speichern.
|
||
- print_sheet — OHNE KI: Druckbogen (mehrere Bilder × Stückzahl, Schnittmarken) als PDF lokal speichern.
|
||
|
||
## Teilen-Links (öffentlich, ohne Konto)
|
||
Ein Bild oder eine Sammlung unter \`/s/<slug>\` zugänglich machen — der Empfänger braucht kein Konto.
|
||
- Voreinstellungen sind bewusst vorsichtig (auf den Bildern sind Kinder): **Ablauf** (Standard 30 Tage,
|
||
\`settings.share_default_days\`), jederzeit **widerrufbar** und wieder freigebbar, optionales
|
||
**Zugangswort** (argon2), **Herunterladen abschaltbar**, Seiten und Dateien tragen \`X-Robots-Tag: noindex\`.
|
||
- Der Zugriff hängt am **Link**, nicht am Bild: Eine fremde \`item\`-Kennung an einem gültigen Link
|
||
liefert 404. Bilder aus privaten Sitzungen lassen sich gar nicht teilen.
|
||
- Zustände: 200 (gültig) · 401 (Zugangswort nötig oder falsch) · 404 (unbekannt) · 410 (abgelaufen oder
|
||
widerrufen). Das Zugangswort geht per **POST**, danach quittiert ein kurzlebiges Cookie je Link.
|
||
- \`GET /api/s/<slug>/file?item=<uuid>\` — Ansichtsfassung (auf 2000 px verkleinert).
|
||
\`&download=1\` liefert das Original; bei gesperrtem Download 403. \`&thumb=1\` für die Übersicht.
|
||
- \`GET /api/s/<slug>/zip\` — **alle** Bilder des Links als ZIP (voller Auflösung).
|
||
Bewusst GET, damit es ein schlichter Link ohne JavaScript ist. Achtet auf Download-Sperre
|
||
(403) und Zugangswort (401). Grenzen: 200 Dateien, 500 MB entpackt (danach schließt das
|
||
Archiv mit einer \`HINWEIS.txt\`), 5 Pakete je Link und Adresse in 10 Minuten. Wird als
|
||
**ein** Download gezählt. Es wird eine Datei nach der anderen gelesen und weitergereicht,
|
||
nicht alles auf einmal in den Speicher.
|
||
- \`GET|POST /api/shares\` — eigene Links auflisten bzw. anlegen. Body: \`{ itemIds[], title?, note?,
|
||
days?, allowDownload?, password? }\`. \`days\` muss eine Zahl von 0–3650 sein (0 = unbegrenzt);
|
||
alles andere wird **abgelehnt**, nicht als „unbegrenzt" gedeutet.
|
||
- \`GET|PATCH|DELETE /api/shares/<id>\` — Zugriffe ansehen, ändern (\`action: 'revoke'|'unrevoke'\`,
|
||
\`title\`, \`note\`, \`days\`, \`allowDownload\`, \`password\`), löschen.
|
||
- \`POST /api/shares/send\` — Link per Mail verschicken: \`{ shareId, to, message? }\`, höchstens 10
|
||
Empfänger je Anfrage, 50 Mails je Stunde und Absender. **Kein Anhang** — ein Link lässt sich
|
||
zurückziehen, ein Anhang nicht.
|
||
- WhatsApp läuft über einen \`wa.me\`-Link im Browser (kein Server-Versand; die WhatsApp-Business-API
|
||
bräuchte Meta-Konto, genehmigte Vorlagen und Gebühren).
|
||
- Gezählt wird **wann** und mit welcher Art Gerät geöffnet wurde, nicht **wer**: von der IP nur ein
|
||
Hash, gesalzen mit Server-Geheimnis **und** Link-Kennung (also nicht über Links hinweg verknüpfbar),
|
||
höchstens eine Zeile je Besucher und Stunde, Löschung nach 90 Tagen.
|
||
|
||
## E-Mail
|
||
Versand über das eigene Postfach (SMTP, Zugang verschlüsselt in \`settings\`; Admin → E-Mail).
|
||
Gebraucht für: Passwort zurücksetzen, Hinweis bei fertigen Aufträgen (je Nutzer schaltbar unter
|
||
\`/konto\`), Teilen-Links verschicken. Ohne eingerichteten Versand melden die betroffenen Routen 503.
|
||
- \`POST /api/auth/reset { email }\` — Rücksetz-Link anfordern. Antwortet **immer gleich**, egal ob es
|
||
die Adresse gibt. \`POST /api/auth/reset { token, password }\` — neues Passwort setzen; Token gilt
|
||
60 Minuten, genau einmal, und entwertet alle offenen Sitzungen des Kontos.
|
||
- \`GET|PATCH /api/me\` — eigenes Konto: \`email\`, \`notify_jobs\`.
|
||
|
||
## KI-Kennzeichnung
|
||
Ergebnisse tragen IPTC \`DigitalSourceType\` als XMP in der Datei:
|
||
\`trainedAlgorithmicMedia\` (aus Text erzeugt) bzw. \`compositeWithTrainedAlgorithmicMedia\`
|
||
(vorhandenes Foto verändert), dazu \`xmp:CreatorTool\` mit dem verwendeten Modell.
|
||
Auslesbar mit exiftool. Das Druckmodul erzeugt nichts mit KI und kennzeichnet folglich nichts.
|
||
|
||
## Dateinamen der Ergebnisse
|
||
Schema: \`JJJJ-MM-TT_HHMMSS_<motiv>_<format>_<kürzel>.<ext>\` — z. B. \`2026-08-20_143207_lea-badge_the-frame_a3f9.jpg\`.
|
||
- Der Zeitstempel steht vorn, deshalb sortiert jede Dateiliste (Picdrop, NAS, Finder) von allein chronologisch.
|
||
- Das vierstellige Kürzel stammt aus der Item-ID. Dadurch sind zwei Bilder **nie** namensgleich —
|
||
auch nicht bei gleichem Motiv, gleichem Format und gleichem Tag. (Vor dem 21.08.2026 fehlten Uhrzeit
|
||
und Kürzel; gleichartige Bilder desselben Tages überschrieben sich beim Ausliefern.)
|
||
- Zeitzone: \`KLARBILD_TZ\`, Standard \`Europe/Berlin\`.
|
||
- Motiv wird aus dem Ursprungsnamen geschält (Datum, UUIDs und der eigene Stempel fallen weg);
|
||
generische Namen wie \`IMG\`, \`foto\`, \`telegram\` werden zu \`klarbild\`.
|
||
- Nachbenennung der ganzen Bibliothek: Admin → „Dateinamen" bzw.
|
||
\`POST /api/admin/storage { action: 'rename_preview' | 'rename_apply' | 'redeliver_all' }\` (nur per Login-Session,
|
||
nicht per API-Token). Idempotent — Stempel aus \`created_at\`, Kürzel aus der Item-ID.
|
||
|
||
## Auslieferung
|
||
- Picdrop = SFTP/FTPS-Ziel; Galerie = Unterordner unter dem Basispfad. Voller Pfad = Basisordner + Galerie
|
||
(wird erst beim Ausliefern zusammengesetzt). Standard-Galerie „POSTER LEA"; „The Frame" → „TheFrame-Backgrounds".
|
||
- Ausgeliefert wird das Ergebnis im gewählten Dateiformat (output_ext, global oder pro Auftrag) — PNG oder JPG. Freigestellte Motive bleiben PNG.
|
||
- Optionaler .md-Metadaten-Beileger je Ziel schaltbar (Picdrop/NAS/Zusatzziel).
|
||
|
||
## Datenschutz
|
||
- Private Session (job.private=true): kein Bibliothekseintrag, keine Auslieferung/Sicherung,
|
||
kein gespeicherter Prompt; Ergebnis wird nach kurzer Zeit gelöscht.
|
||
- Sichtbarkeit global steuerbar (nur eigene / alle sehen alles); Admin sieht alles.
|
||
|
||
Weitere Doku (menschlich): In-App /anleitung und /changelog.
|
||
`;
|
||
|
||
export const GET: APIRoute = async () =>
|
||
new Response(BODY, { headers: { 'Content-Type': 'text/plain; charset=utf-8', 'Cache-Control': 'public, max-age=3600' } });
|