managarten/docs/plans/me-images-and-reference-generation.md
Till JS 89258eb451 feat(profile,api): meImages foundation for AI reference generation (M1)
M1 of docs/plans/me-images-and-reference-generation.md — a user-owned
pool of reference images (face, fullbody, hands, …) that will back
image generation where the user appears as themselves (outfit try-on,
glasses, portraits) via OpenAI /v1/images/edits. Data layer only in
this commit; UI lands in M2, the edits endpoint in M3.

- Dexie v38: meImages table with id/kind/primaryFor/createdAt indices.
  Added to USER_LEVEL_TABLES so the hook stamps userId and skips the
  spaceId/authorId/visibility trio (one human = one face across every
  Space, not per-Space).
- Encryption registry: label + tags encrypted; kind/primaryFor/usage
  stay plaintext because they drive the indexed queries and the
  Reference picker's filtering. mediaId/URLs/dimensions are structural.
- Profile module store: createMeImage, updateMeImage,
  setAiReferenceEnabled (per-image KI opt-in — plan decision #5),
  setPrimary (transactional slot swap — only one row per primary slot),
  deleteMeImage. Emits MeImage* domain events.
- Queries: useAllMeImages, useMeImagesByKind, useReferenceImages
  (only the rows the user opted in for KI), useImageByPrimary.
- POST /api/v1/profile/me-images/upload: thin wrapper over mana-media
  with app='me' as the reference tag. No new MinIO bucket — plan
  decision #1 revised after verifying mana-media uses one bucket and
  only tags references by app.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 13:50:53 +02:00

20 KiB
Raw Blame History

Me-Images + Reference-basierte Bildgenerierung — Plan

Status (2026-04-23)

Greenfield. Keine Zeile Code, kein Schema, kein Endpunkt. Vorarbeit: Picture-Modul hat bereits ungenutzte sourceImageId + generationId Felder (Platzhalter), OpenAI gpt-image-2 ist für Text-zu-Bild produktiv über apps/api/src/modules/picture/routes.ts:65-96.

Ziel

Der Nutzer hinterlegt mehrere eigene Referenzbilder (Gesicht, Ganzkörper, weitere Posen/Outfits) in einem zentralen Pool. Diese Bilder werden explizit opt-in von KI-Bildgenerierung als Referenz verwendet, primär über OpenAI gpt-image-2 (der /v1/images/edits-Endpoint akzeptiert bis zu 16 Reference-Images pro Call) mit Replicate-Fallback und optional lokalem mana-image-gen (FLUX + IP-Adapter, später).

Kernfragen, die dieser Plan beantwortet:

  1. Wo leben die Referenzbilder? (Datenmodell, Scope, Verschlüsselung)
  2. Wie kommen sie in den Generator-Payload? (UI + API)
  3. Wie ruft der Server OpenAI mit Reference-Images? (Backend)
  4. Welche Use-Cases ergeben sich? (Konsumenten-Module)

Nicht im Scope:

  • Wardrobe/Outfit-Modul — bekommt einen eigenen Plan (wardrobe-module.md), konsumiert nur das hier entstehende Fundament.
  • Face-Swap in Video/Live-Streams — nur Still-Images.
  • Per-Space-Avatare — ein Nutzer hat eine Identität; falls später Bedarf, reicht ein spaceId-Zusatzfeld.
  • Gesichtsvalidierung / Liveness-Check — Vertrauensmodell: der Nutzer lädt nur Bilder seiner selbst hoch, wir erzwingen das nicht.

Abgrenzung

  • Kein photos: photos ist Album/Tag-orientiert für beliebige Fotos. meImages ist ein kuratierter, winziger Pool (typ. 210 Bilder) mit klarer KI-Opt-in-Semantik.
  • Kein body: body trackt Messungen/Workout. Progress-Fotos (Before/After) gehören dort hin, nicht in meImages — das hier ist für KI-Referenz, nicht für Fitness-Logging.
  • Kein picture.images: images sind KI-generierte oder importierte Assets für Boards. meImages ist der Input für Generierung, nicht das Ergebnis.
  • Cross-Link: picture.images.sourceImageId und picture.images.referenceImageIds[] zeigen auf meImages.mediaId (oder andere media-IDs). Das Picture-Modul bleibt der zentrale Ort, an dem das Ergebnis landet.

Entscheidungen

1. Eigene Dexie-Tabelle, nicht auth.users.image erweitern

Gründe:

  • auth.users.image ist eine einzelne Text-URL in Better Auth. Mehrere Bilder + Metadaten + KI-Flags passen nicht rein ohne das Auth-Schema zu verunstalten.
  • Dexie + mana-sync + Encryption-Registry sind das etablierte Pattern für per-User-Daten.
  • auth.users.image bleibt als abgeleitete Anzeige erhalten (Primary-Face → Avatar-URL), wird aber über einen Sync-Hook gepflegt, nicht direkt beschrieben.

2. Pro User, nicht pro Space

Ein Mensch hat eine Identität. Space-spezifische Avatare (Brand-Space vs. Personal-Space) sind ein 10%-Fall und können später über ein optionales spaceOverride: { [spaceId]: meImageId } Feld im profile-Singleton gelöst werden, ohne meImages selbst zu ändern.

3. Primär gpt-image-2 via /v1/images/edits, nicht Text-zu-Bild

Der Text-zu-Bild-Endpoint (/v1/images/generations) wird produktiv für freie Generierung genutzt und bleibt wie er ist. Für Reference-Workflows nutzen wir /v1/images/edits — derselbe Endpoint akzeptiert:

  • image (multipart) — eine oder mehrere Reference-Bilder (gpt-image-2: bis zu 16)
  • prompt — der Transformations-Wunsch
  • mask (optional) — für Inpainting
  • size, quality, n wie gehabt

Das ist der native OpenAI-Weg und erspart uns IP-Adapter-Engineering auf dem eigenen GPU-Server. Lokaler Fallback (FLUX + PuLID/InstantID auf RTX 3090) wird als M5 / später geplant, nicht in M1-M3.

4. Opt-in pro Bild, nicht global

Jedes meImage hat ein usage.aiReference: boolean Flag. Default beim Upload: false. Der Nutzer aktiviert gezielt, welche Bilder die KI verwenden darf. Global-Kill-Switch kommt aus dem Profile-Singleton (profile.aiUsesReferenceImages: boolean), Default true, damit einzelne Opt-ins direkt wirken.

Architektur-Überblick

┌─ Client (SvelteKit) ────────────────────────────────────┐
│  /settings/me-images (Upload + Toggles)                 │
│  picture/GeneratorForm (Reference-Picker)               │
│  Dexie: meImages (encrypted label/tags/kind)            │
└──────┬──────────────────────────────────────────────────┘
       │ mana-sync (encrypted rows)
       ▼
┌─ mana-sync → PostgreSQL (mana_sync.meImages) ───────────┐
└─────────────────────────────────────────────────────────┘

┌─ Generate-Flow (NEU) ───────────────────────────────────┐
│  POST /api/v1/picture/generate-with-reference           │
│    { prompt, referenceMediaIds: [...], mode, mask? }    │
│                                                          │
│  Backend:                                                │
│  1. Credits validieren (edits kostet wie generate)       │
│  2. Fetch reference buffers aus mana-media (via mediaId) │
│  3. multipart → OpenAI /v1/images/edits                  │
│     oder (Fallback) mana-image-gen /edit                 │
│  4. Response → uploadImageToMedia → return {images[]}    │
└─────────────────────────────────────────────────────────┘

┌─ Tool-Registry / MCP ───────────────────────────────────┐
│  me.listReferenceImages   (read-only, für Personas)     │
│  me.generateWithReference (triggert obigen Endpoint)    │
└─────────────────────────────────────────────────────────┘

Datenmodell

Neue Dexie-Tabelle: meImages

// apps/mana/apps/web/src/lib/modules/profile/types.ts
export type MeImageKind =
  | 'face'       // Kopf/Schulter, neutral
  | 'fullbody'   // Ganzkörper, stehend
  | 'halfbody'   // Hüfte aufwärts
  | 'hands'      // für Schmuck/Ring-Anproben
  | 'reference'; // sonstige (andere Pose, anderer Lichtkontext)

export interface LocalMeImage {
  id: string;
  kind: MeImageKind;
  label?: string;              // "Portrait neutral Studio", "Outfit Juni"
  mediaId: string;             // → mana-media CAS (quelle-of-truth fürs Bild)
  storagePath: string;         // cached vom mana-media-Response
  publicUrl: string;
  thumbnailUrl?: string;
  width: number;
  height: number;
  tags: string[];              // 'smiling', 'glasses-off', 'studio-light'
  usage: {
    aiReference: boolean;      // Opt-in: darf KI das nutzen?
    showInProfile: boolean;    // für Avatar-Fallback-Logik
  };
  primaryFor?: 'avatar' | 'face-ref' | 'body-ref' | null;
  createdAt: number;
  updatedAt: number;
  _pendingSync?: number;
}

Primary-Logik: Pro primaryFor-Wert existiert maximal ein meImage mit diesem Flag. Setzen eines neuen Primary räumt das alte auf (Store-Methode setPrimary(id, slot)).

Encryption-Registry-Eintrag

// apps/mana/apps/web/src/lib/data/crypto/registry.ts
meImages: {
  enabled: true,
  fields: ['label', 'tags', 'kind']
}

mediaId, storagePath, publicUrl, width, height, primaryFor, Timestamps → plaintext (konsistent mit images im Picture-Modul). Das Bild selbst liegt hinter mana-media-Auth — nicht verschlüsselt auf Dateiebene, aber nur für den Owner abrufbar. Für Zero-Knowledge-Modus-Nutzer: im M4 kommt optionale client-seitige Blob-Verschlüsselung dazu (out-of-scope für M1).

Kein neuer Sync-Endpoint nötig

mana-sync behandelt meImages wie jede andere per-User-Tabelle (userScoped, nicht spaceScoped). Nur Registrierung in der Sync-Schema-Liste.

Picture-Modul: bestehende Felder aktivieren + eins ergänzen

// apps/mana/apps/web/src/lib/modules/picture/types.ts
export interface LocalImage {
  // ... bestehend
  sourceImageId?: string | null;        // bereits vorhanden — jetzt genutzt
  referenceImageIds?: string[] | null;  // NEU: für multi-reference gpt-image-2
  generationMode?: 'text' | 'edit' | 'inpaint'; // NEU
  generationId?: string | null;         // bereits vorhanden
}

Encryption-Registry: referenceImageIds, generationMode → plaintext (IDs sind random, keine Leak-Gefahr).

Backend-Erweiterungen

Neuer Endpoint: POST /api/v1/picture/generate-with-reference

Datei: apps/api/src/modules/picture/routes.ts (erweitern, nicht neue Datei)

routes.post('/generate-with-reference', async (c) => {
  const userId = c.get('userId');
  const {
    prompt,
    model,                 // 'openai/gpt-image-2' | 'local/flux-pulid' | …
    referenceMediaIds,     // string[] (mana-media IDs; aus meImages oder picture.images)
    mode,                  // 'edit' | 'inpaint'
    maskMediaId,           // optional, nur für inpaint
    quality,
    width,
    height,
    n,
  } = await c.req.json();

  // 1. Credits — gleicher Tarif wie /generate (3/10/25 je quality × n)
  // 2. Reference-Buffers holen (parallel): for each id → fetchMediaBuffer(id, userId)
  //    — mana-media verifiziert, dass userId der Owner ist (keine fremden IDs)
  // 3. multipart/form-data bauen:
  //      model, prompt, size, quality, n
  //      image[] (als File-Parts; bei n>1 refs: image[]=ref1, image[]=ref2, …)
  //      mask (optional)
  // 4. POST https://api.openai.com/v1/images/edits
  // 5. b64_json → uploadImageToMedia → return { images: [...] }
});

Lib-Helper neu in apps/api/src/lib/media.ts: fetchMediaBuffer(mediaId, userId): Promise<ArrayBuffer> — lädt + verifiziert Ownership in einem Call.

Modell-Routing analog zum bestehenden /generate:

  • openai/gpt-image-2 (default) → OpenAI /v1/images/edits
  • local/* → mana-image-gen /edit (siehe M5)
  • Replicate hat keinen äquivalenten Multi-Reference-Endpoint → wir überspringen Replicate hier; fällt auf OpenAI zurück.

Fehler-Matrix:

  • 402 Insufficient credits
  • 404 Reference media not found or not owned
  • 413 Reference zu groß (OpenAI-Limit: 4MB pro PNG)
  • 502 OpenAI-Fehler (mit detail.slice(0,500) wie bisher)

mana-image-gen erweitern (M5, nicht M1)

Python/FastAPI-Seite bekommt einen POST /edit Endpoint, der IP-Adapter oder PuLID auf FLUX lädt und reference_images: list[bytes] + prompt annimmt. Weil Replicate/lokal nicht parallel zu OpenAI im selben Call laufen müssen, ist das ein reiner Fallback für Offline-/Zero-Knowledge-Szenarien und kann später dazukommen.

UI: zwei Touchpoints

1. /settings/me-images (neu)

  • 2 prominente Slots oben: Gesicht (quadratisch, 512×512 empfohlen) und Ganzkörper (portrait, min 1024 hoch)
  • Darunter Grid für zusätzliche Referenzen (Drag-and-Drop, Multi-Select-Upload — Pattern aus picture/ListView.svelte:165-217 klauen)
  • Pro Bild-Kachel:
    • Kind-Badge (Gesicht / Ganzkörper / Hände / …)
    • Toggle usage.aiReference (prominent, mit Tooltip "Wird an OpenAI gesendet wenn du ein Bild mit Referenz generierst")
    • Primary-Stern (nur einer pro Slot aktiv)
    • Tag-Editor
    • Löschen
  • Oben Globaler Kill-Switch: "KI darf meine Referenzbilder verwenden" (aus profile-Singleton)
  • Hinweis-Card zu Datenschutz: wo landen die Bilder, wer sieht sie, wie löschen

Zugriff: ⚙ im profile-Modul → "Meine Bilder" + direkte Route.

2. Picture-Generator: Reference-Picker

In apps/mana/apps/web/src/lib/modules/picture/components/GeneratorForm.svelte (oder Äquivalent):

  • Neuer "Referenz hinzufügen"-Button öffnet ein Popover
  • Popover listet:
    • Mich: alle meImages mit usage.aiReference === true (primary zuerst)
    • Aus diesem Modul: letzte N images (für Generation-Chaining)
  • Multi-Select bis zu 4 Referenzen (Client-Limit, OpenAI erlaubt 16)
  • Wenn mindestens eine Referenz gewählt: Endpoint switched auf /generate-with-reference, UI zeigt "gpt-image-2 Edit" statt "Generate"
  • Optional: Mask-Drawing für Inpainting (out-of-scope für M2, kommt in M3)

Tool-Registry + MCP

Nach M1+M2 bekommt packages/mana-tool-registry (siehe Memory, MCP M1+M1.5 shipped) zwei neue Tools:

  • me.listReferenceImages() — read-only, gibt { id, kind, label, primaryFor, thumbnailUrl }[] zurück, nur aiReference=true Einträge. Plaintext-Tier (label wird ent-verschlüsselt auf Server-Seite wie andere encrypted Tools).
  • me.generateWithReference({ prompt, referenceImageIds, mode }) — wrappt den neuen Endpoint, gibt { imageIds, mediaIds } zurück.

Damit können Personas (AI Workbench, Chat, ai-missions) und externe MCP-Clients (Claude Desktop) den Nutzer "visualisieren". Beispiel: Persona "Stylistin" bekommt me.listReferenceImages + me.generateWithReference als Tool-Subset und kann in Chat sagen "Probieren wir drei Brillen-Looks?".

Verschlüsselung + Datenschutz

  • Metadaten (label, tags, kind): client-seitig AES-GCM-256 vor Dexie-Write, wie im Standard-Pattern.
  • Bilddaten: bleiben in MinIO (mana-media Bucket) mit Owner-RLS. Für Zero-Knowledge-Mode-Nutzer kommt in M4 optionale Client-Blob-Verschlüsselung (Upload verschlüsselt → Server sieht Ciphertext → OpenAI bekommt nur Bilder, wenn der Nutzer den Key entsperrt und den Edit-Call triggert). Das ist ein eigener Workstream und kein Blocker für M1-M3.
  • OpenAI-Call: jeder /generate-with-reference-Call geht als HTTPS-Multipart raus. Bilder landen kurzzeitig auf OpenAI-Servern (Policy: 30 Tage). Das muss die Settings-UI explizit erwähnen.
  • Audit: jeder Edit-Call loggt {userId, referenceMediaIds, prompt, model, timestamp} in eine neue picture.generation_log-Tabelle (nicht encrypted, für Rechnungs-/Abuse-Prüfung — Memoro-seitig, nicht in Dexie).

Use-Cases + Modul-Zuordnung

M1M3 decken diese Use-Cases direkt ab:

Use Case Wo im UI Modul
"Zeig mir wie ich mit einer schwarzen Brille aussehe" Picture Generator → Reference: face → Prompt picture
"Generiere ein Profilbild im Studio-Look aus meinem Selfie" Picture Generator → Reference: face → Prompt picture
"Mach ein Titelbild für meine Präsentation mit meinem Portrait" Presi → Cover-Generator → Reference-Picker presi (M4 Konsument)
"Ich in mittelalterlicher Rüstung" / kreative Spielereien Picture Generator picture
Avatar automatisch aus primary face ableiten Profile-Settings profile

Eigener Folge-Plan wardrobe-module.md (nicht in diesem Plan):

Use Case Wo im UI Modul
Outfit-Katalog pflegen (T-Shirts, Hosen, Schuhe als einzelne Items) Wardrobe Gridview wardrobe (neu)
"Kombiniere diese Jacke mit meinem Outfit aus Foto X" Wardrobe → Outfit-Composer wardrobe
Virtual Try-On mit Ganzkörper-Referenz + Garment-Referenz Wardrobe → Try-On wardrobe
Jahreszeit-Vorschläge ("Was ziehe ich heute an") Wardrobe Daily-Card wardrobe

Weitere sinnvolle Konsumenten (eigene Tickets, nicht Teil dieses Plans):

  • website (Block-Tree CMS, in Planung): Portrait-Block kann primaryFor='avatar' automatisch ziehen.
  • presi: Cover-Slide-Template mit Nutzer-Portrait.
  • broadcast / social-relay: Avatar-Generierung für Posts.
  • dreams: "Ich im Traum" — Nutzer als Protagonist in KI-generierten Traum-Szenen.
  • wishes: "Wie würde mir das stehen" — Wishlist-Preview vor dem Kauf.

Migrationsplan

Soft-first/Hard-follow-up-Regel (siehe Memory):

  1. Soft: Dexie v27 führt meImages ein, Encryption-Registry um den Eintrag erweitern, sync-Schema registrieren. auth.users.image bleibt als-is. Neue Primary-Face-Uploads schreiben zusätzlich zur meImages-Tabelle.
  2. Hard (Folge-Commit, einige Tage später): One-shot-Migration im Client: existierendes auth.users.imagemeImages mit kind='face', primaryFor='avatar', usage.aiReference=false (Opt-in bleibt explizit). auth.users.image wird danach zum abgeleiteten Feld, das über einen Sync-Hook aus meImages(primaryFor='avatar').publicUrl gefüllt wird.

Milestones

  • M1 — meImages Foundation (~1 Tag)

    • Dexie v27: meImages-Tabelle
    • apps/mana/apps/web/src/lib/modules/profile/types.ts: Typen
    • Encryption-Registry-Eintrag
    • Store (stores/meImages.svelte.ts): CRUD + setPrimary
    • Queries (useMyImages, useReferenceImages, useImageByPrimary)
    • Sync-Schema registrieren
    • Upload-Wrapper nutzt bestehenden picture/upload-Endpoint mit app=me (neuer Bucket me-storage in MinIO)
  • M2 — UI Route /settings/me-images (~1 Tag)

    • Route + ModuleShell-Wrapping (wie andere Settings-Routen)
    • Slot-Komponenten für Face/Fullbody, Grid für Reste
    • Drag-and-Drop-Upload + Multi-File
    • Opt-in-Toggles pro Bild + global
    • Primary-Stern
    • Profile-Modul ⚙ → neuer Eintrag "Meine Bilder"
    • Hard-Migration auth.users.imagemeImages
  • M3 — Backend generate-with-reference (~1-2 Tage)

    • fetchMediaBuffer-Helper in apps/api/src/lib/media.ts
    • Neue Route POST /picture/generate-with-reference mit OpenAI /v1/images/edits
    • Credit-Validierung identisch zu /generate
    • Generation-Log-Tabelle
    • Fehler-Matrix
  • M4 — Picture-Generator UI (~1 Tag)

    • Reference-Picker-Popover in GeneratorForm
    • Payload-Switch /generate vs. /generate-with-reference
    • picture.images.referenceImageIds + generationMode persistieren
    • Detailansicht eines Bilds zeigt genutzte Referenzen
  • M5 — Tool-Registry + MCP-Exposure (~0.5 Tag)

    • me.listReferenceImages + me.generateWithReference in packages/mana-tool-registry
    • MCP-Server (Port 3069) exponiert die Tools
    • Persona-Runner (sobald M2 von Personas-Plan live) kann sie konsumieren
  • M6 — (optional, später) Lokaler Fallback via mana-image-gen (mehrere Tage)

    • FLUX + PuLID/InstantID auf GPU-Server
    • POST /edit in mana-image-gen
    • Routing über local/flux-pulid
  • M7 — (optional, später) Inpainting-Mask-Drawing (~2 Tage)

    • Canvas-Mask-Editor im Picture-Generator
    • Mask als zweites Medium hochladen + an /edit übergeben
  • M8 — (optional, später) Zero-Knowledge-Bilder

    • Client-seitige Verschlüsselung der Bild-Blobs in MinIO
    • Bei Generate-Call entschlüsselt der Client und sendet temp an Server → OpenAI → Result wieder verschlüsseln

Entschieden (2026-04-23)

  1. Bucket-Namensgebung: eigener me-storage-Bucket revidiert — mana-media nutzt heute einen einzelnen Bucket (mana-media); der app-String landet als Tag in media_references.app. Upload geht mit app='me', kein neuer Bucket nötig. Falls später Lifecycle-Rules pro App-Tag nötig werden, reicht eine mc-Regel mit --prefix 'me/' auf dem mana-media-Bucket.
  2. primaryFor='avatar'auth.users.image: Client-Dexie-Hook ruft PUT /api/v1/auth/profile. mana-sync bleibt außen vor.
  3. OpenAI Ref-Image-Format: Original-Format durchreichen (PNG/JPG/WEBP — OpenAI akzeptiert alle). Keine Server-Konvertierung.
  4. Credit-Kosten für Multi-Ref-Edits: identisch zu /generate, pro Output-Bild, unabhängig von Reference-Anzahl.
  5. profile.aiUsesReferenceImages-Default: true (globaler Panic-Kill-Switch; Pro-Bild-Opt-in ist die eigentliche Hürde).
  6. Alter Avatar-Upload-Pfad: bleibt in M1 unangetastet; M2 biegt EditProfileModal auf /settings/me-images um und räumt den toten Endpoint-Call weg.

Verweise

  • Bestehender Picture-Generate-Endpoint: apps/api/src/modules/picture/routes.ts:43-227
  • Picture Upload-Pattern (für UI-Klau): apps/mana/apps/web/src/lib/modules/picture/ListView.svelte:165-217
  • Encryption-Registry-Pattern: apps/mana/apps/web/src/lib/data/crypto/registry.ts
  • mana-media CAS: services/mana-media/CLAUDE.md
  • MCP-Gateway + Tool-Registry: services/mana-mcp/CLAUDE.md, packages/mana-tool-registry/
  • Spaces-Modul-Allowlist (falls neues wardrobe kommt): packages/shared-types/src/spaces.ts:63-184