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>
20 KiB
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:
- Wo leben die Referenzbilder? (Datenmodell, Scope, Verschlüsselung)
- Wie kommen sie in den Generator-Payload? (UI + API)
- Wie ruft der Server OpenAI mit Reference-Images? (Backend)
- 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:photosist Album/Tag-orientiert für beliebige Fotos.meImagesist ein kuratierter, winziger Pool (typ. 2–10 Bilder) mit klarer KI-Opt-in-Semantik. - Kein
body:bodytrackt Messungen/Workout. Progress-Fotos (Before/After) gehören dort hin, nicht inmeImages— das hier ist für KI-Referenz, nicht für Fitness-Logging. - Kein
picture.images:imagessind KI-generierte oder importierte Assets für Boards.meImagesist der Input für Generierung, nicht das Ergebnis. - Cross-Link:
picture.images.sourceImageIdundpicture.images.referenceImageIds[]zeigen aufmeImages.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.imageist 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.imagebleibt 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-Wunschmask(optional) — für Inpaintingsize,quality,nwie 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/editslocal/*→ 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-217klauen) - 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
meImagesmitusage.aiReference === true(primary zuerst) - Aus diesem Modul: letzte N
images(für Generation-Chaining)
- Mich: alle
- 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, nuraiReference=trueEinträ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 neuepicture.generation_log-Tabelle (nicht encrypted, für Rechnungs-/Abuse-Prüfung — Memoro-seitig, nicht in Dexie).
Use-Cases + Modul-Zuordnung
M1–M3 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 kannprimaryFor='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):
- Soft: Dexie v27 führt
meImagesein, Encryption-Registry um den Eintrag erweitern, sync-Schema registrieren.auth.users.imagebleibt als-is. Neue Primary-Face-Uploads schreiben zusätzlich zurmeImages-Tabelle. - Hard (Folge-Commit, einige Tage später): One-shot-Migration im Client: existierendes
auth.users.image→meImagesmitkind='face',primaryFor='avatar',usage.aiReference=false(Opt-in bleibt explizit).auth.users.imagewird danach zum abgeleiteten Feld, das über einen Sync-Hook ausmeImages(primaryFor='avatar').publicUrlgefüllt wird.
Milestones
-
M1 —
meImagesFoundation (~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 mitapp=me(neuer Bucketme-storagein MinIO)
- Dexie v27:
-
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.image→meImages
-
M3 — Backend
generate-with-reference(~1-2 Tage)fetchMediaBuffer-Helper inapps/api/src/lib/media.ts- Neue Route
POST /picture/generate-with-referencemit 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
/generatevs./generate-with-reference picture.images.referenceImageIds+generationModepersistieren- Detailansicht eines Bilds zeigt genutzte Referenzen
-
M5 — Tool-Registry + MCP-Exposure (~0.5 Tag)
me.listReferenceImages+me.generateWithReferenceinpackages/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 /editin 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)
- Bucket-Namensgebung:
eigenerrevidiert — mana-media nutzt heute einen einzelnen Bucket (me-storage-Bucketmana-media); derapp-String landet als Tag inmedia_references.app. Upload geht mitapp='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. primaryFor='avatar'→auth.users.image: Client-Dexie-Hook ruftPUT /api/v1/auth/profile. mana-sync bleibt außen vor.- OpenAI Ref-Image-Format: Original-Format durchreichen (PNG/JPG/WEBP — OpenAI akzeptiert alle). Keine Server-Konvertierung.
- Credit-Kosten für Multi-Ref-Edits: identisch zu
/generate, pro Output-Bild, unabhängig von Reference-Anzahl. profile.aiUsesReferenceImages-Default:true(globaler Panic-Kill-Switch; Pro-Bild-Opt-in ist die eigentliche Hürde).- Alter Avatar-Upload-Pfad: bleibt in M1 unangetastet; M2 biegt
EditProfileModalauf/settings/me-imagesum 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
wardrobekommt):packages/shared-types/src/spaces.ts:63-184