managarten/docs/plans/comic-module.md
Till JS 3d30e39ae7 feat(comic): Mc5 — Wardrobe-Hook "Als Comic-Character"
Brücke von Wardrobe nach Comic: User klickt auf einem Outfit oder
einem einzelnen Kleidungsstück „Als Comic-Character", landet im
Character-Builder mit pre-filltem Add-Prompt ("wearing the
Bühnenoutfit"), picked Stil und rendert die ersten 4 Varianten.

Wardrobe-Buttons:
- DetailOutfitView: unterhalb des TryOnButton ein outline-Link
  navigiert zu `/comic/character/new?title=…&prompt=wearing+the+
  OUTFITNAME+outfit`.
- DetailGarmentView: analog mit `prompt=wearing+GARMENTNAME` für
  ein einzelnes Kleidungsstück. Beide nur sichtbar wenn das
  Outfit/Garment nicht archiviert ist.
- Sparkle-Icon + dezent neutraler Border-Style (nicht primary —
  das ist die TryOn-CTA), hover schaltet auf primary/40.

Comic CharacterBuilder bekommt drei optionale Props:
`initialName?`, `initialAddPrompt?`, `initialStyle?`. Im
extend-Modus ignoriert (Source ist dann der existing-Character),
im create-Modus dienen sie als $state-Initialwerte. Routine read
ist intentional — Mounting passiert frisch pro Route-Visit, also
einmaliges Capture passt.

`/comic/character/new/+page.svelte` parsed jetzt
`page.url.searchParams` für `title`, `prompt`, `style` und reicht
sie als Props durch. style wird gegen die VALID_STYLES-Liste
validiert — defekte URL-Params fallen ohne Crash auf
"unset/default" zurück.

Bewusst NICHT gemacht: Try-On-Output direkt als sourceBodyMediaId
verwenden. Das Try-On-Bild ist im mana-media mit `app='picture'`
getaggt; `verifyMediaOwnership` auf
`/picture/generate-with-reference` akzeptiert nur
`['me','wardrobe','comic']` — der Comic-Generate würde mit
HTTP 404 abbrechen. Lösung wäre eine Server-Route die Picture-
Output als Comic-Asset re-tagged, das ist aber eigene Spec.
Aktueller Pfad ist sauberer: rohe meImages-Refs bleiben Source,
der Add-Prompt steuert den Outfit-Look.

Plan-Doc §11 Mc5 dokumentiert den Pfad + warum kein
Try-On-Reuse.

Comic-Files type-checken sauber.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 19:32:29 +02:00

36 KiB
Raw Blame History

Comic — Module Plan

Status (2026-04-24, vor M1)

Geplant, noch nichts geshipped. Dieses Dokument legt Datenmodell, UI und KI-Integration fest; die Meilensteine M1M5 bringen das Feature auf Produktions-Qualität, M6+ sind Ausbau.

Ziel

Ein Nutzer erzeugt aus sich selbst und beliebigen Text-Inputs (Tagebuch, Notizen, Writing-Drafts, Library-Einträge, Kalender-Events) einen Comic — eine geordnete Folge von Bild-Panels in konsistentem Stil. gpt-image-2 rendert jedes Panel aus einer Referenz-Komposition (Face-Ref + optionale Szene) und einem Panel-Prompt; Sprechblasen und Caption-Text werden direkt ins Bild reinrendered, kein separater Overlay-Layer.

Kernfragen, die dieser Plan beantwortet:

  1. Wie bilden wir eine Comic-Story im Datenmodell ab — als Liste geordneter Panel-Referenzen oder als eigenständige Entität?
  2. Wie fließt Input aus anderen Modulen (Journal-Eintrag, Notes, Library-Review, Writing-Draft) in die Panel-Generierung ein?
  3. Wie halten wir Character-Konsistenz über Panels hinweg, ohne ein separates Character-Management-System zu bauen?
  4. Wie integrieren wir gpt-image-2 mit den fünf unterschiedlichen Comic-Stilen (comic/manga/cartoon/graphic-novel/webtoon), ohne pro Stil einen eigenen Backend-Pfad zu bauen?

Abgrenzung

  • Kein eigener Image-Editor: Panels sind picture.images-Rows wie alle anderen generierten Bilder. Wer Panel X nachbearbeiten will, tut das im Picture-Modul (oder generiert neu). Comic verwaltet die Reihenfolge und den Story-Kontext, nicht die einzelnen Pixel.
  • Kein Storyboard-Canvas in M1M4: Panels leben in einer geordneten Liste mit optionaler Caption. Ein Comic-Strip-Canvas mit Drag-und-Drop-Positionierung (wie Picture-Boards) ist M6+.
  • Keine SVG-Speech-Bubble-Overlays: Sprechblasen/Captions werden gpt-image-2 über den Prompt reingekippt, nicht nachträglich über SVG aufs Bild gelegt. Weniger Kontrolle, einfacher Datenweg, ein Asset-Export pro Panel.
  • Keine eigene Character-DB: Character-Referenzen sind meImages-Einträge (Face-Ref, Body-Ref, plus optionale Costume-Referenzen aus wardrobe). Kein neues Konzept "Comic-Character" als eigene Table.
  • Kein Multi-Character-Crew in M1M5: Ein Comic hat einen Protagonisten (der Nutzer oder eine Kostüm-Variante von ihm). Crew mit mehreren Gesichtern ist M6+ — braucht Konsistenz-Tricks, die wir nicht auf den MVP-Weg packen wollen.
  • Cross-Link zu picture: Panel-Ergebnisse landen in picture.images wie jede andere Generierung. LocalImage bekommt einen comicStoryId-Back-Ref + optional comicPanelIndex.
  • Cross-Link zu me-images: Ohne useImageByPrimary('face-ref') kein Comic — identisch zu Wardrobe's Try-On-Flow.

Entscheidungen

1. Ein Modul, eine Tabelle

Im Gegensatz zu Wardrobe (Garments + Outfits) reicht für Comic eine Tabelle:

  • comicStories — eine Comic-Story mit Titel, Stil, Character-Refs, Story-Kontext, Panel-Liste (als panelImageIds: string[] in Plaintext)

Kein zweites Table comicPanels, weil ein Panel kein eigenständiges Primitiv ist — es ist ein picture.images-Eintrag mit Back-Ref. Das spart Sync-Volumen, vermeidet FK-Cleanup beim Löschen, und hält die Panel-Reihenfolge an einem Ort (im Story-Record als ID-Array, statt als orderIndex-Feld auf jedem Panel).

Die zusätzlichen Panel-Metadaten (Caption-Text, Dialogue-Vorschläge vom AI-Storyboard, Prompt-Varianten) wandern in einen nested-JSON-Feld auf der Story:

panelMeta: Record<string /* panelImageId */, {
  caption?: string;           // freitext, encrypted
  dialogue?: string;          // freitext, encrypted
  promptUsed?: string;        // encrypted — reproduce/regenerate
  sourceInput?: {             // ref auf Cross-Modul-Input für dieses Panel
    module: 'journal' | 'notes' | 'library' | 'writing' | 'calendar';
    entryId: string;
  };
}>

Das ist denormalisiert-aber-handhabbar: wer eine Story löscht, löscht automatisch die Meta; wer ein Panel löscht, muss aus panelImageIds

  • panelMeta den Eintrag rausnehmen. Trivialer Store-Helper.

2. Character-Konsistenz via fixe Referenz-Liste pro Story

Jede Story speichert bei Erstellung einmal characterMediaIds: string[] — Face-Ref + optional Body-Ref + optional Kostüm-Fotos aus Wardrobe. Alle Panel-Generierungen übergeben diese Referenz-Liste unverändert an /api/v1/picture/generate-with-reference. gpt-image-2 ist nicht deterministisch, aber identische Refs + identischer Stil-Preset-Prefix im Prompt ergeben über 48 Panels einen erkennbaren Character.

Kein Feinschliff-Tuning in M1M5. Wenn sich nach M3 zeigt, dass Panels auseinanderdriften, adressieren wir das mit einer zusätzlichen "Anchor-Panel"-Referenz (erstes erzeugtes Panel wird Referenz für alle folgenden) — das ist M6+.

2.1 Image-Modell als Picker, nicht hartcodiert (nachgezogen)

Comic nutzt die gleiche Model-Auswahl wie Wardrobe's Try-On:

  • openai/gpt-image-2 — Default, mittlerer Preis, fällt server-seitig auf gpt-image-1 zurück wenn die OpenAI-Org nicht verified ist.
  • google/gemini-3-pro-image-preview — Nano Banana Pro, hohe Charakter-Konsistenz, höherer Preis.
  • google/gemini-3.1-flash-image-preview — Nano Banana 2, neuestes, schnell, günstig.

PanelModelPicker (Analog zu TryOnModelPicker) sitzt als segmentierter Picker in PanelEditor / BatchPanelEditor / StoryboardSuggester. Die Wahl ist per-Editor-Mount lokal; keine Story-Level-Persistierung, weil ein Model-Flag auf der Row eine Migration bräuchte und die Wahl meistens eh ad-hoc ist.

MCP-Tool comic.generatePanel und Catalog-Tool generate_comic_panel akzeptieren beide einen optionalen model-Parameter mit demselben Enum. Default bleibt openai/gpt-image-2.

3. Fünf Stil-Presets, Mapping im Client

export type ComicStyle =
  | 'comic'          // US-Comic, Linework + Cell-Shading, kräftige Farben
  | 'manga'          // S/W, Screen-Tones, dynamische Perspektiven
  | 'cartoon'        // weicher, pastellig, Saturday-Morning-Cartoon
  | 'graphic-novel'  // realistischer, Aquarell/Painterly, stimmungsvoll
  | 'webtoon';       // vertikal-lesbar, moderne Farbpalette, Soft-Shading

Pro Stil ein Prompt-Prefix-Template im Client (lib/modules/comic/styles.ts), das in jede Panel-Generierung eingewoben wird. Das Backend kennt die Stile nicht — es sieht nur den finalen Prompt. Gleicher Ansatz wie Wardrobe's accessoryOnly-Prompt-Detection.

Stil wird bei Story-Erstellung gewählt und ist danach fix. Stil-Wechsel = neue Story (oder Panels einzeln neu generieren).

4. Sprechblasen & Captions direkt im Bild

gpt-image-2 kann Text ins Bild rendern — nicht perfekt, aber für Comic-Panels akzeptabel. Vorteil: ein einziger Asset-Export pro Panel, kein zweiter Overlay-Layer, kein extra Canvas-Render-Schritt beim Teilen/Drucken. Nachteil: Text-Korrekturen erfordern Neu-Generierung des Panels (= neuer Credit-Call).

Im Panel-Editor gibt's zwei Freitext-Felder neben dem Prompt: "Caption" (Off-Voice-Erzähltext) und "Dialog" (Sprechblasen- Inhalt). Beide werden in den Prompt eingewoben: …, caption reading "[caption]", character saying "[dialog]" in speech bubble, …. Deutsch-Text funktioniert; User-Erwartungshaltung aber auf Englisch-Text einstellen (die Modelle sind auf Englisch stabiler) und im UI-Hint vermerken.

Der Nutzer kann Caption und Dialog leer lassen → stummes Panel.

5. Panel-Generierung in drei Modi (evolvierend über M2M4)

  • M2 Single-Panel: User klickt "+ Panel", schreibt Prompt + optional Caption/Dialog, drückt "Generieren". Kosten: 1 gpt-image-2-Call (Default quality='medium', 10 Credits).
  • M3 Batch: User schreibt 24 Panel-Prompts im Voraus, drückt "Alle generieren". Backend bekommt n=1 pro Panel, aber UI startet die Calls parallel. Kosten: N × Credits.
  • M4 AI-Storyboard: User wählt einen Input (Journal-Eintrag, Notes, Writing-Draft, Library-Review, Calendar-Event), Claude liest den Text und schlägt 46 Panel-Beschreibungen vor (Text-Only, kein Bild). User bestätigt/editiert, dann läuft Batch-Gen. Claude-Call läuft client-side über bestehende @mana/shared-ai Helper (kein neuer Service-Pfad nötig).

6. Cross-Modul-Input: lesend, nicht schreibend

Das Comic-Modul liest aus den Stores anderer Module (journal, notes, library, writing, calendar), schreibt aber niemals dorthin zurück. Ein Journal-Eintrag bleibt im Journal, ein Library-Review bleibt in der Library — Comic merkt sich nur per panelMeta[id].sourceInput dass dieses Panel aus Input X entstanden ist. Das erlaubt später "zeig mir alle Comics, die aus diesem Journal-Eintrag entstanden sind" als einfache Query.

Das Decrypt läuft client-side via <module>Store.getEntry(id)decryptRecords(…) → übergeben an Claude. Keine Server-Side-Decrypts, keine Key-Grants, kein Mission-Flow nötig — weil der Nutzer selbst interaktiv am UI steht.

7. Space-scoped Katalog, user-scoped Protagonist

Wie bei Wardrobe: comicStories sind space-scoped (Brand kann Comics über sein Produkt machen, Club über Vereinsgeschichte, Family über Kinder-Abenteuer, Team über Bühnenproduktion, Practice als Patienten-Aufklärungs-Comic). Face-Refs bleiben user-global aus meImages — wer in einem Brand-Space einen Comic erstellt, ist selbst der Protagonist.

Family-Edge-Case: Kinder haben keinen eigenen Account, also auch keine meImages. Wer eine Kinder-Geschichte als Comic machen will, nutzt entweder ein eigenes Face-Ref ("Opa erzählt aus dem Krieg, gerendert als Opa") oder das Comic-Modul zeigt den Family-Space-Hinweis (analog zu Wardrobe): "Protagonist-Rendering nutzt deine eigenen Referenzbilder." Kein Multi-Subject-Konzept in M1M5.

Alle sechs Space-Typen bekommen comic in die Allowlist.

8. Visibility-System von Anfang an

Comics sind ein Format das Nutzer möglicherweise teilen wollen ("mein 4-Panel-Comic zum gestrigen Bug-Report"). Wir adoptieren das Visibility-System (shared-privacy) von M1 an — visibility, visibilityChangedAt/By, unlistedToken, <VisibilityPicker> im Detail-View. Comics mit visibility='public' können später via /embed/comic/:id auf Webseiten eingebettet werden (Plan-Punkt von visibility-system.md passt 1:1).

Architektur-Überblick

┌─ Client (SvelteKit) ────────────────────────────────────┐
│  /comic                                                 │
│    ListView: alle Stories (Cards mit erstem Panel)      │
│  /comic/[id]                                            │
│    Detail: Story-Meta + Panel-Strip (horizontal)        │
│    "+ Panel" CTA, pro Panel Caption/Dialog-Editor       │
│  /comic/new                                             │
│    CreateForm: Titel, Stil, Character-Picker, Kontext   │
│  Dexie: comicStories                                    │
└──────┬──────────────────────────────────────────────────┘
       │ mana-sync (encrypted title/description/panelMeta)
       ▼
┌─ Panel-Generierung (reuses M3 /picture endpoint) ───────┐
│  POST /api/v1/picture/generate-with-reference           │
│    referenceMediaIds = story.characterMediaIds          │
│    prompt = stylePrefix + panelPrompt + captionHint     │
│  Result → picture.images row                            │
│  Client writes: image.comicStoryId = story.id           │
│                 image.comicPanelIndex = N               │
│                 story.panelImageIds.push(imageId)       │
│                 story.panelMeta[imageId] = {...}        │
└─────────────────────────────────────────────────────────┘

┌─ AI-Storyboard (M4, client-side Claude) ────────────────┐
│  User selects input (journal entry / note / …)          │
│  decryptedText = moduleStore.getEntry(id).content       │
│  Claude.suggest({ style, text }) → Panel[]              │
│  User reviews/edits panels                              │
│  Batch-Gen via /picture endpoint                        │
└─────────────────────────────────────────────────────────┘

┌─ MCP / Agent tools ─────────────────────────────────────┐
│  comic.listStories      (read)                          │
│  comic.createStory      (write)                         │
│  comic.generatePanel    (write — consumes credits)      │
│  comic.reorderPanels    (write)                         │
└─────────────────────────────────────────────────────────┘

Datenmodell

LocalComicStory

export type ComicStyle =
  | 'comic'
  | 'manga'
  | 'cartoon'
  | 'graphic-novel'
  | 'webtoon';

export interface ComicPanelMeta {
  caption?: string;            // encrypted
  dialogue?: string;           // encrypted
  promptUsed?: string;         // encrypted
  sourceInput?: {              // plaintext refs
    module: 'journal' | 'notes' | 'library' | 'writing' | 'calendar';
    entryId: string;
  };
}

export interface LocalComicStory extends BaseRecord {
  id: string;
  title: string;                         // encrypted
  description?: string | null;           // encrypted
  style: ComicStyle;                     // plaintext enum
  /**
   * Referenz-Liste die für jedes Panel-Generate identisch übergeben wird.
   * Mindestens der primary face-ref aus meImages; optional body-ref +
   * bis zu 3 Wardrobe-Garment-Fotos für ein Kostüm-Setup. Cap 8 wie bei
   * Wardrobe (MAX_REFERENCE_IMAGES im /generate-with-reference endpoint).
   */
  characterMediaIds: string[];           // plaintext FKs
  /**
   * Kontext den Claude in M4 als Briefing für die Storyboard-Generierung
   * sieht. Freitext, typisch 13 Sätze ("Ich ärgere mich über einen Bug
   * in unserer Sync-Logik — mach daraus einen 4-Panel-Frust-Comic.").
   */
  storyContext?: string | null;          // encrypted
  /**
   * Geordnete Liste der Panel-picture.images-IDs. Reihenfolge = Lese-
   * reihenfolge. Reorder = neu schreiben.
   */
  panelImageIds: string[];               // plaintext FKs
  panelMeta: Record<string, ComicPanelMeta>;  // keyed by panel image id
  tags: string[];                        // encrypted
  isFavorite?: boolean;
  isArchived?: boolean;
  visibility?: VisibilityLevel;
  visibilityChangedAt?: string;
  visibilityChangedBy?: string;
  unlistedToken?: string;
}

Encryption-Registry-Eintrag: ['title', 'description', 'storyContext', 'tags', 'panelMeta']panelMeta komplett encrypted (JSON-Blob, der Freitext-Felder enthält). Style-Enum, IDs, Booleans, visibility bleiben plaintext.

Erweiterung auf picture.images

Zwei neue optionale Plaintext-Felder:

// apps/mana/apps/web/src/lib/modules/picture/types.ts
interface LocalImage {
  // ... bestehend
  wardrobeOutfitId?: string | null;
  wardrobeGarmentId?: string | null;
  comicStoryId?: string | null;          // NEU
  comicPanelIndex?: number | null;       // NEU — 0-basiert, Lese-Position
}

Das comicPanelIndex-Feld ist redundant mit story.panelImageIds, aber erlaubt der Picture-Galerie-Ansicht, direkt "Panel 3 von Story X" anzuzeigen ohne die Story zu laden. Plaintext-Zahl, kein Registry-Change.

verifyMediaOwnership erweitert

apps/api/src/modules/picture/routes.ts:299-318 — die erlaubten Apps um 'comic' erweitern, damit Wardrobe-Garments als Kostüm-Referenz in Comic-Panel-Generierungen verwendet werden können:

verifyMediaOwnership(userId, refIds, ['me', 'wardrobe', 'comic'])

('comic' für zukünftige comic-eigene Referenz-Uploads wie Panel-Anker-Bilder in M6+; aktuell leer, aber der Slot ist reserviert.)

Modul-Struktur

apps/mana/apps/web/src/lib/modules/comic/
├── types.ts                      # ComicStyle, LocalComicStory, ComicPanelMeta
├── collections.ts                # comicStoriesTable
├── queries.ts                    # useAllStories, useStoryById, useStoriesByInput
├── module.config.ts              # { appId: 'comic', tables: ['comicStories'] }
├── styles.ts                     # STYLE_PREFIXES: Record<ComicStyle, string>
├── stores/
│   └── stories.svelte.ts         # createStory, updateStory, appendPanel,
│                                 # reorderPanels, removePanel, updatePanelMeta,
│                                 # archive, delete
├── api/
│   ├── generate-panel.ts         # runPanelGenerate({story, prompt, caption, dialogue})
│   │                             #   — wraps /picture/generate-with-reference
│   └── storyboard.ts             # (M4) suggestPanels({style, sourceText, panelCount})
│                                 #   — client-side Claude-Call via @mana/shared-ai
├── components/
│   ├── StoryCard.svelte          # Grid tile (Cover = panelImageIds[0])
│   ├── StoryForm.svelte          # Create/edit Sheet (title, style, character, context)
│   ├── StylePicker.svelte        # 5 Presets als radio-tiles
│   ├── CharacterPicker.svelte    # meImages face-ref auto-select + optional garments
│   ├── PanelStrip.svelte         # horizontal scroll, panel thumbnails
│   ├── PanelCard.svelte          # einzelnes Panel mit Caption/Dialog-Anzeige
│   ├── PanelEditor.svelte        # Prompt + Caption + Dialog + "Generieren"-Button
│   ├── StoryboardSuggester.svelte # (M4) Input-Picker + Claude-Suggestion-Liste
│   └── ReferenceInputPicker.svelte # (M4) wählt Journal/Notes/Library/Writing/Calendar
├── views/
│   ├── ListView.svelte           # Grid aller Stories
│   └── DetailView.svelte         # Story-Meta + PanelStrip + "+ Panel" CTA
├── constants.ts                  # STYLE_LABELS, MAX_PANELS_PER_STORY (default 12)
└── index.ts

Route-Seiten:

apps/mana/apps/web/src/routes/(app)/comic/
├── +page.svelte                  # → ListView
├── [id]/+page.svelte             # → DetailView
└── new/+page.svelte              # → StoryForm (create)

Kein Composer-Route wie bei Wardrobe — Comic-Erstellung ist kurz (Titel + Stil + Character = 3 Felder), Panel-Editing läuft im Detail-View als inline-Sheet.

Backend

Neuer App-Slot 'comic' für zukünftige Uploads (Panel-Anker, Custom-Backgrounds in M6+). In M1 genügt die Registrierung des Slots in verifyMediaOwnership + der App-Allowlist; eigener Upload-Endpoint ist M1 nicht nötig, weil Panel-Bilder als picture.images über den bestehenden Generate-Flow entstehen.

Keine eigene Generate-Route: runPanelGenerate() ruft direkt /api/v1/picture/generate-with-reference, analog zu Wardrobe. Nach Erfolg schreibt der Client die comicStoryId + comicPanelIndex- Back-Refs auf die picture.images-Row und appendet die imageId auf story.panelImageIds + setzt story.panelMeta[imageId].

Cap-Prüfung: MAX_REFERENCE_IMAGES=8 (bereits in Wardrobe M1 gesetzt) deckt Comic ab — Face (1) + Body (1) + bis zu 3 Kostüm-Fotos = 5, mit Puffer für M6+ Anchor-Panel.

mana-apps.ts Eintrag: packages/shared-branding/src/mana-apps.ts bekommt einen neuen Eintrag:

{
  id: 'comic',
  name: 'Comic',
  description: 'Aus Text wird ein Comic',
  icon: 'BookImage' /* oder similar */,
  color: '#…' /* TBD, siehe design-ux.md für Palette */,
  requiredTier: 'beta',
  route: '/comic',
}

MCP-Tools (packages/mana-tool-registry/src/modules/comic.ts)

Vier Tools, Pattern 1:1 an wardrobe.ts angelehnt:

  • comic.listStories({style?, favoriteOnly?, limit?}) — read, auto. Pullt via mana-sync app='comic', entschlüsselt title+description+ tags+panelMeta. Filter client-side.
  • comic.createStory({title, style, characterMediaIds, description?, storyContext?}) — write, propose. Validiert dass alle characterMediaIds dem User gehören (app='me'|'wardrobe'). Schreibt via pushInsert.
  • comic.generatePanel({storyId, panelPrompt, caption?, dialogue?, sourceInput?}) — write (kostet Credits), propose. Liest die Story, composed den finalen Prompt (stylePrefix + panelPrompt + caption/dialog-Hints), ruft /picture/generate-with-reference, appendet das Ergebnis auf panelImageIds + panelMeta.
  • comic.reorderPanels({storyId, panelImageIds}) — write, propose. Validiert Set-Equality (keine neuen/fehlenden IDs), schreibt die neue Reihenfolge.

AI_TOOL_CATALOG in @mana/shared-ai/src/tools/schemas.ts bekommt die vier Tools, comic kommt in die ModuleId-Union.

Milestones

  • M1 — Datenschicht & Modul-Registrierung

    • Dexie v43: comicStories mit Indices [createdAt, style, isFavorite, isArchived] (space-scoped, kein Compound-Index)
    • types.ts: ComicStyle, LocalComicStory, ComicPanelMeta, toStory-Converter
    • Encryption-Registry-Eintrag für comicStories (title/description/storyContext/tags/panelMeta)
    • collections.ts, queries.ts (useAllStories, useStoryById) via scopedForModule<>
    • stores/stories.svelte.ts mit createStory + archive + delete (Panel-Methoden kommen in M2)
    • module.config.ts registriert appId='comic'
    • comic in alle sechs Space-Typen der Allowlist (packages/shared-types/src/spaces.ts)
    • mana-apps.ts Eintrag mit requiredTier: 'beta'
    • picture.images.comicStoryId + comicPanelIndex Felder + toImage-Converter
    • verifyMediaOwnership um 'comic' erweitern
    • Encryption-Roundtrip-Test für panelMeta-JSON (wie library M1 für kind-discriminator)
  • M2 — Story-CRUD + Single-Panel-Generierung

    • Route /comicListView, Story-Grid mit StoryCard (Cover = panelImageIds[0] → mana-media URL, Fallback Placeholder für Stories ohne Panels)
    • Route /comic/newStoryForm (Title, StylePicker mit 5 Presets, CharacterPicker bindet an useImageByPrimary('face-ref') + optional body-ref-Add + Wardrobe-Garment-Picker für bis zu 3 Kostüme, optional storyContext-Textarea)
    • Route /comic/[id]DetailView: Meta-Card + PanelStrip (horizontal scroll) + "+ Panel" CTA
    • PanelEditor inline-Sheet: Prompt-Textarea, Caption-Freitext, Dialog-Freitext, "Generieren"-Button
    • api/generate-panel.ts: runPanelGenerate({story, prompt, caption, dialogue}) composed den Prompt (styles.ts liefert stylePrefix) und ruft /picture/generate-with-reference
    • Nach Erfolg: picture.images.comicStoryId + comicPanelIndex setzen + story.panelImageIds.push() + panelMeta[imageId] = {…}
    • Panel-Lösch-Button (Dexie-Row der picture.images bleibt — nur aus panelImageIds und panelMeta entfernen; User kann im Picture-Modul final löschen)
    • Non-personal-Space-Hinweis + Empty-State bei fehlenden meImages (Link zu /profile/me-images)
    • Visibility-Felder setzbar via <VisibilityPicker> in DetailView
  • M3 — Batch-Panel-Generierung

    • PanelEditor unterstützt Multi-Panel-Modus: 24 Prompts im Formular, "Alle generieren"-Button
    • Client startet N parallele /picture/generate-with-reference-Calls, zeigt Progress-Bar pro Panel
    • Credit-Hinweis zeigt Gesamtkosten vorher (n × creditsForQuality(medium))
    • Retry-UI falls 1 von N fehlschlägt (nur der fehlgeschlagene wird erneut generiert)
    • comic.generatePanel MCP-Tool bekommt optional count?: 1..4-Parameter (default 1)
  • M4 — AI-Storyboard aus Cross-Modul-Input

    • ReferenceInputPicker-Komponente: Modul-Tabs (Journal / Notes / Library / Writing / Calendar), pro Tab Live-Query der letzten N Einträge mit Suche
    • Per ausgewähltem Entry: <module>Store.getEntry(id) → decrypt content → in Storyboard-Flow reichen
    • api/storyboard.ts: suggestPanels({style, sourceText, panelCount=4}) ruft Claude (via @mana/shared-ai, client-side, genau wie AI-Workbench-Planer — kein neuer Service-Pfad), erwartet Panel[] als strukturierte Antwort {prompt, caption, dialogue}
    • StoryboardSuggester-Komponente zeigt Claude-Vorschläge als editierbare Liste (Prompt + Caption + Dialog pro Panel), User kann editieren/löschen/Reihenfolge ändern
    • "Alle generieren"-Button übergibt die bestätigte Panel-Liste an den M3-Batch-Pfad
    • panelMeta[imageId].sourceInput = {module, entryId} beim Erzeugen gesetzt
    • useStoriesByInput({module, entryId}) Query für künftige Cross-Reference-UI ("Comics zu diesem Journal-Eintrag")
  • M5 — MCP-Tools + Visibility-Polish

    • packages/mana-tool-registry/src/modules/comic.ts mit 4 Tools: listStories, createStory, generatePanel, reorderPanels
    • 'comic' in ModuleId-Union
    • registerComicTools() in registerAllModules()
    • AI_TOOL_CATALOG in @mana/shared-ai/src/tools/schemas.ts erweitert
    • Propose-Policy für createStory/generatePanel/reorderPanels, auto-Policy für listStories
    • <VisibilityPicker> voll integriert inkl. unlistedToken-Generierung, canEmbedOnWebsite check für public Comics
    • Embed-Route /embed/comic/[id] (public + unlisted) mit Panel-Strip-Render (wie andere Visibility-adoptierte Module)
  • M6 — Persona-Template "Comic-Autor" (optional, ~0.5 Tag)

    • Persona-Template: auto-Policy für comic.listStories + journal.list* + notes.list*, propose-Policy für comic.createStory + comic.generatePanel
    • Seed-Prompt: "Du bist Comic-Autor. Wenn der User dir einen Moment, ein Erlebnis oder eine Idee gibt, schlag ihm einen kurzen Comic vor — Titel, Stil, 4 Panels mit Prompt + Caption + Dialog. Humor wenn der User es leicht nimmt, ernst wenn er es ernst nimmt."
  • M7 — Comic-Strip-Canvas (optional, mehrere Tage)

    • Picture-Boards-Pattern adaptieren für Comic: freie Panel-Positionierung, variable Panel-Größen, Gutter, Speech-Bubble-Overlay (dann doch SVG, opt-in pro Story)
    • Export als einzelnes PNG/PDF-Asset (Panel-Strip → Canvas → Blob)
    • Rechtfertigt sich nur, wenn Nutzer Feedback-Signal senden dass die lineare Liste nicht reicht
  • M8 — Multi-Character-Crew (optional, mehrere Tage)

    • Story bekommt characterCast: CharacterRef[] statt flaches characterMediaIds[]
    • Pro Panel kann der Autor einen oder mehrere Cast-Member auswählen; referenceMediaIds wird pro Panel zusammengesetzt
    • Namens-Mapping (Cast-Member bekommt Namen → Dialog kann "Alice sagt:" taggen)
    • Nur starten wenn Single-Character-Flow nach M5-Soak stabil

Verschlüsselung

Alle user-typed Felder verschlüsselt (siehe Registry-Einträge oben). panelMeta als ganzer JSON-Blob verschlüsselt (nicht per-Feld) — einfacher Roundtrip, gleiche Semantik wie bei Library's kind-spezifischen Metadaten.

Bild-Blobs selbst bleiben in mana-media mit Owner-RLS, identisch zu Picture/Wardrobe/Me-Images. Zero-Knowledge-Nutzer: MCP-Tools fallen stumm aus (kein MK → ctx.getMasterKey() throwt), UI-Flow bleibt funktional weil die Decrypts client-side passieren.

Cross-Modul-Impact

Modul Impact
picture Zwei neue optionale Felder auf LocalImage: comicStoryId, comicPanelIndex. Keine Registry-Änderung (beide plaintext). Galerie-View könnte optional ein "Teil von Comic X"-Chip zeigen (M5+ optional).
me-images Nichts — Comic konsumiert nur useImageByPrimary.
wardrobe Nichts — Comic liest Garments als referenzielle mediaIds, schreibt nicht zurück.
journal, notes, library, writing, calendar Nichts — nur lesende Cross-Module-Reads über die Module-Stores.
shared-branding Neuer App-Eintrag comic (Icon, Farbe, Tier=beta).
shared-types/spaces.ts comic in alle sechs Space-Typen der Allowlist (personal, brand, club, family, team, practice).
shared-ai/tools/schemas.ts 4 neue Einträge im AI_TOOL_CATALOG.
mana-tool-registry Neues Modul comic.ts + registerComicTools().
apps/api/picture/routes.ts verifyMediaOwnership um 'comic' erweitern.

Offene Fragen (vor M1 klären)

  1. Panel-Count-Limit pro Story: 8? 12? 20? → Empfehlung: hartes Client-Limit 12 in constants.ts, weicher Hinweis ab 8 ("lange Comics sind mit gpt-image-2 schwer konsistent zu halten"). Erhöhen nach M5-Soak möglich.
  2. Quality-Default für Panels: medium (10 Credits)? → Ja, wie Wardrobe. User kann pro Panel overriden (low/medium/high); Batch-Modus nutzt eine Story-weite Default-Setting.
  3. Stil-Wechsel nachträglich: erlaubt? → Nein, Stil ist fix nach Story-Create. Wer wechseln will, dupliziert die Story (M6+ Feature) oder erstellt neu.
  4. Dialog/Caption Sprache: User-Sprache oder Englisch? → Default User-Sprache (Deutsch in unserem primären Markt). UI-Hinweis dass Englisch stabiler rendert. Kein Auto-Translate in M1M5.
  5. AI-Storyboard-Panel-Count: Claude schlägt 46 Panels vor, der User kann mehr/weniger anfordern? → Default 4, Slider 28 im UI, Hard-Cap 8.
  6. Panel-Lösch-Semantik: beim Entfernen aus panelImageIds auch die picture.images-Row löschen? → Nein. Row bleibt, nur die Story-Referenz geht weg. User kann das Panel in der Picture-Galerie behalten oder dort final löschen. Symmetrisch zu Wardrobe (Try-On- Bilder überleben eine Outfit-Löschung).

§11 Character-System (Mc1Mc5)

Nachgezogen 2026-04-25, weil sich im Soak gezeigt hat: rohe meImages direkt als Story-Refs sind kein guter „Identity-Anchor". gpt-image-2 und Nano Banana variieren zwischen Calls — Panel 1 sieht anders aus als Panel 4. User hat zwischen den Panels keine Iteration, kein „nochmal probieren bis das Aussehen stimmt".

Lösung: ein Comic-Character als eigene Entität, die der Nutzer einmal aufbaut + iteriert + pinnt, und die dann als stabiler Story-Anchor dient.

Datenmodell

Eigenes Table comicCharacters (Sibling zu comicStories, space-scoped wie comicStories — Source-meImages sind ja auch space-scoped post-v40, sonst orphan-Refs nach Space-Wechsel).

interface LocalComicCharacter extends BaseRecord {
  id: string;
  name: string;                   // "Manga-Me", "Cartoon-Casual"
  description?: string | null;
  style: ComicStyle;              // mit welchem Stil generiert
  addPrompt?: string | null;      // user-typed Add-Prompt zum Stil

  sourceFaceMediaId: string;      // welche meImages dienten als Source
  sourceBodyMediaId?: string | null;

  variantMediaIds: string[];      // alle generierten Versuche (FK auf picture.images)
  pinnedVariantId?: string | null; // welcher Versuch IST der Charakter

  tags: string[];
  isFavorite?: boolean;
  isArchived?: boolean;
}

Encryption: name / description / addPrompt / tags. Style + IDs

  • Variant-Liste + Booleans bleiben plaintext.

picture.images bekommt einen comicCharacterId-Back-Ref (analog zu comicStoryId/wardrobeOutfitId/wardrobeGarmentId). Mutually exclusive mit comicStoryId — eine Image-Row ist entweder Panel ODER Variant, nie beides.

Snapshot-Semantik

Stories speichern mediaId at create time, nicht den characterId als Live-Lookup. Re-Pinning eines Characters ändert also keine bestehenden Stories — die haben den alten Variant weiter als Ref. Neue Stories nach dem Re-Pin nutzen den neuen.

UX-Flow

Mc1 — Datenschicht (3h): Dexie v49 + types + crypto-registry + collections + queries (useAllCharacters, useCharacter, useCharactersByStyle) + Store (createCharacter, appendVariant, pinVariant, removeVariant, updateCharacter, archive, delete). picture.images.comicCharacterId + Module-Registry-Tabellenliste + Encryption-Roundtrip-Test.

Mc2 — UI (5h):

  • Routes /comic/character, /comic/character/new, /comic/character/[id]
  • ListView-Root bekommt 2-Tab-UI: Stories | Characters
  • CharacterBuilder.svelte: Source picken (face Pflicht, body optional), Stil picken, Add-Prompt optional, „Generieren"-Button feuert 4 parallele Variant-Calls (n=4 in einem gpt-image-2-Call). Variant-Grid darunter, User pinnt eine, „Mehr Varianten" appendet weitere 4.
  • CharacterCard.svelte: Cover = pinned-variant (oder erste Variant als Fallback), Style-Badge, Favorit-Heart.
  • api/generate-character.ts: runCharacterGenerate({character, n=4}) ruft /picture/generate-with-reference mit [face, body?]-Refs + Stil-Prefix + Add-Prompt, schreibt N picture.images mit comicCharacterId-Back-Ref, ruft appendVariant für jeden.

Mc3 — Story-Create-Update (3h):

  • StoryForm wechselt von „face/body/garments-Picker" auf CharacterRefPicker.svelte:
    • Default-Modus: Grid existierender Characters (gefiltert nach Stil oder „Alle"). Pick = einzige Story-Character-Ref.
    • „+ Neuer Character" navigiert zu /comic/character/new mit Return-URL.
    • Toggle „Quick-Modus (kein Character)": fällt zurück auf altes Pattern (face + body + garments) — für „mal eben schnell aus dem Tagebuch ohne Setup".
  • Story-Type bekommt:
    • characterId?: string (FK auf comicCharacters, für Anzeige + Click-Through; null im Quick-Modus)
    • characterMediaId?: string (Snapshot der gepinnten Variant zum Story-Create-Zeitpunkt — was der Renderer nutzt)
    • Soft-Migration: bestehende Stories mit characterMediaIds[] bleiben kompatibel; runPanelGenerate prüft erst characterMediaId (Snapshot), dann fällt zurück auf characterMediaIds[0..n]. Hard-Migration in einem Folge-Commit wenn alle Stories migrert sind.
    • Optional costumeGarmentIds: string[] für Wardrobe-Refs zusätzlich zum Character (Kostüm über dem Character).

Mc4 — MCP + AI-Catalog (~2h, optional):

  • comic.listCharacters, comic.createCharacter, comic.generateVariant, comic.pinVariant in packages/mana-tool-registry.
  • list_comic_characters, create_comic_character, generate_character_variant in AI_TOOL_CATALOG.
  • Persona kann „mach mir einen Manga-Character für Story X" sagen.

Mc5 — Wardrobe-Hook shipped:

  • In Wardrobe-DetailOutfitView ein „Als Comic-Character"-Knopf unterhalb des TryOnButton, navigiert zu /comic/character/new?title=…&prompt=wearing+the+OUTFITNAME+outfit.
  • In DetailGarmentView analog mit prompt=wearing+GARMENTNAME.
  • CharacterBuilder akzeptiert initialName / initialAddPrompt / initialStyle-Props. Die /comic/character/new-Route liest URL-Params und reicht sie als initial state durch — der Builder startet mit dem prefillten Add-Prompt, User picked Stil + rendert die ersten 4 Varianten selbst.
  • Bewusst KEIN Try-On-Output als sourceBodyMediaId: das Try-On-Bild ist mit app='picture' getaggt, der verifyMediaOwnership-Check des Comic-Endpoints akzeptiert nur ['me', 'wardrobe', 'comic']. Re-Upload als 'comic' wäre eine zusätzliche Server-Route — Aufwand vs. Nutzen nicht klar. Workflow stattdessen: rohe meImages bleiben Source, der Add-Prompt steuert den Outfit-Look.

Tradeoffs

  • Variant-Count fix bei 4 statt Slider 1-4: 4 ist sweet-spot für Auswahl ohne Decision-Fatigue, in einem API-Call ausführbar, Credits ~10c × 4 = 40c pro Generate-Round (medium-Quality).
  • Quick-Modus behalten: nicht jede Story braucht Setup. Soft defaults: existieren Characters → Default-Modus „Pick", sonst Default „Quick".
  • Snapshot statt Live-Ref: Stories sind stabil. Trade-off: re-pinned Characters reflektieren nicht in alten Stories — User muss explizit „Story-Charakter aktualisieren"-Flow nutzen (M5+ Feature).
  • Space-scoped Characters: bewusst nicht user-global, weil Source-meImages space-scoped sind. Trade-off: man muss in jedem Space einen eigenen Manga-Me bauen. Akzeptabel weil Spaces unterschiedliche Settings sind (personal vs. brand).

Verweise

  • Fundament Picture-Generate-Reference: apps/api/src/modules/picture/routes.ts:250-430
  • Wardrobe als Modul-Blaupause: docs/plans/wardrobe-module.md
  • Library als Single-Table-Modul mit Discriminator-Pattern: docs/plans/library-module.md
  • Writing-Plan für Cross-Modul-Input-Pattern: docs/plans/writing-module.md
  • Visibility-System: docs/plans/visibility-system.md, packages/shared-privacy/
  • Spaces-Modul-Allowlist: packages/shared-types/src/spaces.ts
  • Tool-Registry-Pattern: packages/mana-tool-registry/src/modules/wardrobe.ts
  • Me-Images (Face/Body-Ref-Konzept): docs/plans/me-images-and-reference-generation.md