managarten/docs/plans/me-images-and-reference-generation.md
Till JS e0820331b0 feat(wardrobe): solo-garment try-on + plan-doc status updates (M4.1)
Closes the one checklist item M4 left for later — "TryOnButton auf
DetailGarmentView (mit impliziten 'Solo-Outfit')". A user can now open
a single garment's detail page, see "An mir anprobieren · 10 Credits",
and get an inline preview of themselves wearing just that one item
(or just that accessory, for glasses/jewelry/hat/accessory).

Client:
- api/try-on.ts: extracts a shared callGenerateWithReference() helper
  and a dimsForSize() utility from runOutfitTryOn so the new
  runGarmentTryOn can share the HTTP-error matrix + picture.images
  row shape without a refactor of the outfit path.
- runGarmentTryOn({ garment, faceRefMediaId, bodyRefMediaId?, prompt?,
  quality? }): auto-detects accessoryOnly from the garment's category
  (FACE_ONLY_CATEGORIES), composes the DE default prompt ("im/in
  <Name>", "mit <Name>" für Accessoires), writes a picture.images row
  with wardrobeOutfitId=null so it doesn't pollute any outfit's
  try-on history. Does NOT update any outfit.lastTryOn — it's a
  standalone preview, on purpose.
- GarmentTryOnButton.svelte: thinner sibling of TryOnButton. Same
  three states (ready / missing-refs / loading), same non-personal-
  space disclaimer. Extra: inline preview panel showing the last
  rendered result, with a link to the Picture gallery ("Gefunden in
  der Picture-Galerie als normale Generierung.").
- DetailGarmentView now puts the try-on action above the existing
  wear-tracking button. Try-on is the more engaging action for this
  page; demoting "heute getragen" to a secondary-styled button
  respects that without removing it.

Plan docs:
- docs/plans/wardrobe-module.md — rewrites the Status block to M1-M5
  with actual commit hashes, and checks off the per-milestone task
  lists. Adds a new M4.1 block for solo-garment try-on.
- docs/plans/me-images-and-reference-generation.md — adds the v40
  space-scope migration (cb9a9bb42) as its own row in the commit
  table, with a pointer to the sub-plan.

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

27 KiB
Raw Blame History

Me-Images + Reference-basierte Bildgenerierung — Plan

Status (2026-04-23, Stand nach M5)

M1M5 + M2.5 SHIPPED — das Feature ist end-to-end lieferbar. Nutzer legt unter /profile/me-images Gesicht + Ganzkörper (+ optional weitere Referenzen) ab, toggled pro Bild "KI darf nutzen", geht in den Picture-Generator, wählt Referenzen, triggert eine OpenAI gpt-image-2-Edit. Ergebnis landet in der Picture-Galerie. MCP-Tools (me.listReferenceImages, me.generateWithReference) sind registriert — Claude Desktop / Persona Runner können dasselbe automatisiert.

Commits (teils durch parallele Sessions in Commits mit anderer Attribution gelandet — Code korrekt, nur Message irreführend):

Milestone Commit Inhalt
M1 Foundation 89258eb45 Dexie v38 meImages-Table, Encryption-Registry (label+tags), Store + Queries, POST /api/v1/profile/me-images/upload
M2 Settings-UI a64a7e39c Route /profile/me-images, Face/Fullbody-Slots, Grid, Drag-Drop, pro-Bild opt-in Toggle
M3 Edits-Endpoint in 38dc80654 POST /picture/generate-with-reference → OpenAI /v1/images/edits; getMediaBuffer, verifyMediaOwnership in apps/api/lib/media
M4 Reference-Picker in d087b4744 ReferenceImagePicker.svelte, Model-Auto-Switch, Endpoint-Routing, generationMode+referenceImageIds auf LocalImage
M2.5 Avatar-Migration e2b5ac38c One-shot migration/legacy-avatar.ts, Autosync face-ref→avatar→auth.users.image, EditProfileModal-Cleanup
M5 MCP-Tools fc635f983 packages/mana-tool-registry/src/modules/me.ts — zwei Tools, auto-registriert
Space-Scope-Migration cb9a9bb42 meImages aus USER_LEVEL_TABLES ausgetragen, Dexie v40 retro-stampt spaceId = _personal:<uid>-Sentinel + authorId + visibility, drops userId. Queries/Store/MCP-Tool filtern jetzt auf aktiven Space. auth.users.image bleibt an Personal-Space primary-avatar gekoppelt — andere Spaces haben ihren eigenen lokalen Avatar. Sub-Plan: docs/plans/me-images-space-scope-migration.md

Offen (noch nicht angefangen)

  • M6 — Lokaler Fallback via mana-image-gen (mehrere Tage, optional). FLUX + PuLID/InstantID auf dem GPU-Server (Windows, RTX 3090), POST /edit-Endpoint in mana-image-gen, Routing über local/flux-pulid im apps/api-Endpoint. Lohnt sich erst, wenn Zero-Knowledge-Mode-User das brauchen oder OpenAI-Limits zum Problem werden.

  • M7 — Inpainting Mask Drawing (~2 Tage, optional). Canvas-basiertes Mask-Editor im Picture-Generator (Brush-Size, Clear, Invert), Mask als zweites Multipart-Part an /generate-with-reference. OpenAI /v1/images/edits akzeptiert mask bereits — nur der Client-Editor fehlt. Nice-to-have für "ersetze nur das Outfit, Gesicht bleibt".

  • M8 — Zero-Knowledge-Bild-Blobs (größerer Workstream). Client-seitige AES-Verschlüsselung der Bild-Blobs bevor sie zu mana-media gehen; beim Generate-Call lokal entschlüsseln, temporär an den Server durchreichen, Ergebnis wieder client-seitig verschlüsseln. Dann sieht selbst der Server nichts ausser Ciphertext. Braucht eine Architektur-Skizze eigener Güte — nicht Teil dieses Plans.

  • Global Kill-Switch profile.aiUsesReferenceImages — im Plan als Feld auf dem profile-Singleton vorgesehen (Panic-Switch für "alle Referenzen temporär aus"). In M2 als "Pro-Bild reicht für jetzt" deferred, noch nicht gebaut. ~30 Minuten: Feld auf LocalUserContext + Toggle im Intro-Block von MeImagesView + bei Empty-Set auf dem Generator die Referenzen ausblenden.

  • Kind-Editor pro Tile — der kind eines uploadeten Bilds (face/fullbody/halfbody/hands/reference) ist beim Upload fix. Ein späteres "Kind ändern"-Kontrollelement im Tile ist eine Stunde Arbeit, aber keiner hat's bis jetzt vermisst.

  • Detailansicht eines generierten Bilds zeigt Referenzen — die Felder (generationMode, referenceImageIds) sind auf LocalImage gespeichert, aber ListView.svelte im Picture-Modul rendert sie noch nicht. Im Detail-Modal wäre ein "Erstellt mit Referenzen: [Thumbnail ×3]"-Block der sinnvolle Schritt. ~1 Stunde.

  • Re-Upload-Pfad für Legacy-Avatar — die M2.5-Migration setzt mediaId = 'legacy-avatar:<uid>' und lässt den Legacy-Avatar bewusst nicht durch mana-media laufen. Wenn der Nutzer diesen Avatar als KI-Referenz nutzen will, müsste er das Bild nochmal hochladen. Heute bounced verifyMediaOwnership — das ist das korrekte Sicherheitsverhalten, aber die UI sagt das dem Nutzer nicht. Ein Hint "Dieses Bild stammt noch aus dem alten Profil — für KI-Nutzung bitte neu hochladen" im Avatar-Tile würde reichen. ~30 Minuten.

Vorläufer

Picture-Modul hatte bereits ungenutzte sourceImageId + generationId Felder (Platzhalter), OpenAI gpt-image-2 war 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) ────────────────────────────────────┐
│  /profile/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. /profile/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 SHIPPED 89258eb45

    • Dexie v38 (nicht v27 — v26 war library, v37 website-builder): meImages-Tabelle
    • apps/mana/apps/web/src/lib/modules/profile/types.ts: MeImageKind, MeImagePrimarySlot, MeImageUsage, LocalMeImage, MeImage, toMeImage
    • Encryption-Registry-Eintrag — label + tags encrypted; kind, primaryFor, usage plaintext
    • Store stores/me-images.svelte.tscreateMeImage, updateMeImage, setPrimary (transactional), setAiReferenceEnabled, deleteMeImage + Domain-Events
    • Queries — useAllMeImages, useMeImagesByKind, useReferenceImages, useImageByPrimary
    • Sync-Schema registriert (module.config.ts) + meImages in USER_LEVEL_TABLES (user-scoped, kein spaceId-Stamping)
    • Upload-Endpoint POST /api/v1/profile/me-images/upload wrappt uploadImageToMedia({ app: 'me' })
    • eigener me-storage-Bucket: mana-media nutzt einen Bucket für alle Apps; app='me' als Tag in media_references reicht
  • M2 — UI Route /profile/me-images SHIPPED a64a7e39c

    • Route + RoutePage-Wrapping (nicht /settings/me-images — Repo-Konvention: pro-Modul-Subrouten)
    • MeImageSlotCard für Face/Fullbody, MeImageTile für Grid, MeImageUploadZone (reusable)
    • Drag-and-Drop + Multi-File via File Picker
    • Opt-in-Toggle pro Bild (aiReference)
    • Primary-Stern (für kinds mit zugewiesenem Slot)
    • Profile-ListView → "Meine Bilder"-Eintrag im Konto-Tab mit Sub-Hint
    • (Hard-Migration wurde nach M2.5 ausgelagert — siehe unten)
    • Global Kill-Switch profile.aiUsesReferenceImagesoffen (siehe Offenes-Liste)
  • M2.5 — Legacy-Avatar-Migration + Autosync SHIPPED e2b5ac38c

    • migration/legacy-avatar.ts — idempotenter One-Shot beim Öffnen der Route
    • setPrimary(id, 'face-ref') claimt silent auch 'avatar' auf derselben Zeile (Kopplung)
    • syncAvatarToAuth() nach jeder primary/delete-Änderung — schreibt auth.users.image
    • EditProfileModal Inline-Upload → "In Meine Bilder verwalten"-Link
    • profileService.uploadAvatar + AvatarUploadResponse + Test gelöscht (dead code)
  • M3 — Backend generate-with-reference SHIPPED in 38dc80654

    • getMediaBuffer + verifyMediaOwnership in apps/api/src/lib/media.ts
    • POST /api/v1/picture/generate-with-reference mit OpenAI /v1/images/edits multipart
    • Credit-Validierung identisch zu /generate (3/10/25 × n)
    • Fehler-Matrix: 400 (prompt/refs), 402 (credits), 404 (ownership), 502 (OpenAI), 503 (keine Config)
    • Degraded-Fallback: wenn mana-media nach OpenAI-Success failed → inline base64 in Response (Generierung nicht verloren)
    • Generation-Log-Tabelle: verworfen — Credit-Audit-Trail reicht, kein Postgres-Schema-Change in M3 nötig
  • M4 — Picture-Generator UI SHIPPED in d087b4744

    • ReferenceImagePicker.svelte — Multi-Select bis 4, leerer Zustand linkt zu /profile/me-images
    • Payload-Switch /generate/generate-with-reference via isReferenceMode
    • Auto-Model-Switch auf openai/gpt-image-2 wenn Referenzen gewählt; Flux Schnell im Dropdown disabled
    • negativePrompt wird im Referenz-Modus disabled + als "wird ignoriert" markiert
    • generationMode + referenceImageIds auf LocalImage persistiert (und in toImage propagiert)
    • Detailansicht eines Bilds zeigt genutzte Referenzen — offen, ~1h
  • M5 — Tool-Registry + MCP-Exposure SHIPPED fc635f983

    • packages/mana-tool-registry/src/modules/me.ts
    • me.listReferenceImages({kind?}) — pullt via mana-sync (app=profile), decryptet label+tags, filtert auf usage.aiReference=true
    • me.generateWithReference({prompt, referenceMediaIds, quality, size, n}) — Proxy über M3-Endpoint
    • MCP-Server exponiert beide automatisch (iteriert Registry in createMcpServerForUser)
    • Persona-Runner kann sie sobald ANTHROPIC_API_KEY gesetzt + Persona ihnen erlaubt ist konsumieren
  • M6 — Lokaler Fallback via mana-image-gen (mehrere Tage) — OFFEN

    • FLUX + PuLID/InstantID auf GPU-Server (Windows, RTX 3090)
    • POST /edit in mana-image-gen
    • Routing über local/flux-pulid im apps/api-Endpoint
  • M7 — Inpainting Mask Drawing (~2 Tage) — OFFEN

    • Canvas-Mask-Editor im Picture-Generator
    • Mask als zweites Multipart-Part an /v1/images/edits
  • M8 — Zero-Knowledge-Bild-Blobs — OFFEN, großer Workstream

    • Client-seitige AES-Verschlüsselung vor Upload zu mana-media
    • Generate-Call entschlüsselt client-seitig, sendet temp an Server → OpenAI → Ergebnis wieder verschlüsseln
    • Braucht eigene Architektur-Skizze; das hier ist nur ein Hinweis dass der Bedarf existiert

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 /profile/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