docs(plans): me-images M1-M5 status + new wardrobe plan

Two plan updates as a set:

- me-images-and-reference-generation.md: rewrites the "Status" block
  to reflect what actually shipped (M1 89258eb45, M2 a64a7e39c, M2.5
  e2b5ac38c, M3 in 38dc80654, M4 in d087b4744, M5 fc635f983) and
  adds an "Offen" section listing the small follow-ups that didn't
  make the M1-M5 cut — global aiUsesReferenceImages kill-switch,
  kind-editor on existing tiles, reference-display in picture
  detail view, legacy-avatar re-upload hint — plus the three
  optional later tracks (M6 local FLUX+PuLID, M7 inpainting masks,
  M8 zero-knowledge blobs). Milestones checklist is now
  -annotated per shipped item with actual decisions (Dexie v38
  instead of v27, no me-storage bucket after all, generation_log
  deferred, etc.).

- wardrobe-module.md: new plan. Data layer sketch (two tables:
  wardrobeGarments + wardrobeOutfits, reuses me-images + picture
  as dependencies), UI breakdown (/wardrobe, /wardrobe/compose,
  garment + outfit detail routes), Try-On as a thin wrapper over
  the M3 endpoint (with the cap bumped from 4 → 8 references, so
  face + body + up-to-6 garments fits one call), four MCP tools
  in a new wardrobe.ts module, and two optional later tracks
  (Persona Stil-Coach template, context-driven outfit suggestion
  mission). The explicit non-goals block keeps the scope tight:
  no product DB, no replacement for inventory, no shopping, no
  style-coaching that feels judgmental.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-23 15:08:45 +02:00
parent 9589feb296
commit 638f9c34d6
2 changed files with 424 additions and 41 deletions

View file

@ -1,8 +1,39 @@
# Me-Images + Reference-basierte Bildgenerierung — Plan
## Status (2026-04-23)
## Status (2026-04-23, Stand nach M5)
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`.
**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 |
## 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
@ -286,54 +317,69 @@ Soft-first/Hard-follow-up-Regel (siehe Memory):
## 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)
- **M1 — `meImages` Foundation** ✅ SHIPPED `89258eb45`
- [x] Dexie v38 (nicht v27 — v26 war library, v37 website-builder): `meImages`-Tabelle
- [x] `apps/mana/apps/web/src/lib/modules/profile/types.ts`: `MeImageKind`, `MeImagePrimarySlot`, `MeImageUsage`, `LocalMeImage`, `MeImage`, `toMeImage`
- [x] Encryption-Registry-Eintrag — `label` + `tags` encrypted; `kind`, `primaryFor`, `usage` plaintext
- [x] Store `stores/me-images.svelte.ts``createMeImage`, `updateMeImage`, `setPrimary` (transactional), `setAiReferenceEnabled`, `deleteMeImage` + Domain-Events
- [x] Queries — `useAllMeImages`, `useMeImagesByKind`, `useReferenceImages`, `useImageByPrimary`
- [x] Sync-Schema registriert (`module.config.ts`) + `meImages` in `USER_LEVEL_TABLES` (user-scoped, kein spaceId-Stamping)
- [x] 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`** (~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`
- **M2 — UI Route `/profile/me-images`** ✅ SHIPPED `a64a7e39c`
- [x] Route + `RoutePage`-Wrapping (nicht `/settings/me-images` — Repo-Konvention: pro-Modul-Subrouten)
- [x] `MeImageSlotCard` für Face/Fullbody, `MeImageTile` für Grid, `MeImageUploadZone` (reusable)
- [x] Drag-and-Drop + Multi-File via File Picker
- [x] Opt-in-Toggle pro Bild (`aiReference`)
- [x] Primary-Stern (für kinds mit zugewiesenem Slot)
- [x] Profile-ListView → "Meine Bilder"-Eintrag im Konto-Tab mit Sub-Hint
- [x] *(Hard-Migration wurde nach M2.5 ausgelagert — siehe unten)*
- [ ] Global Kill-Switch `profile.aiUsesReferenceImages`*offen* (siehe Offenes-Liste)
- **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
- **M2.5 — Legacy-Avatar-Migration + Autosync** ✅ SHIPPED `e2b5ac38c`
- [x] `migration/legacy-avatar.ts` — idempotenter One-Shot beim Öffnen der Route
- [x] `setPrimary(id, 'face-ref')` claimt silent auch `'avatar'` auf derselben Zeile (Kopplung)
- [x] `syncAvatarToAuth()` nach jeder primary/delete-Änderung — schreibt `auth.users.image`
- [x] `EditProfileModal` Inline-Upload → "In Meine Bilder verwalten"-Link
- [x] `profileService.uploadAvatar` + `AvatarUploadResponse` + Test gelöscht (dead code)
- **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
- **M3 — Backend `generate-with-reference`** ✅ SHIPPED in `38dc80654`
- [x] `getMediaBuffer` + `verifyMediaOwnership` in `apps/api/src/lib/media.ts`
- [x] `POST /api/v1/picture/generate-with-reference` mit OpenAI `/v1/images/edits` multipart
- [x] Credit-Validierung identisch zu `/generate` (3/10/25 × n)
- [x] Fehler-Matrix: 400 (prompt/refs), 402 (credits), 404 (ownership), 502 (OpenAI), 503 (keine Config)
- [x] 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
- **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
- **M4 — Picture-Generator UI** ✅ SHIPPED in `d087b4744`
- [x] `ReferenceImagePicker.svelte` — Multi-Select bis 4, leerer Zustand linkt zu `/profile/me-images`
- [x] Payload-Switch `/generate``/generate-with-reference` via `isReferenceMode`
- [x] Auto-Model-Switch auf `openai/gpt-image-2` wenn Referenzen gewählt; Flux Schnell im Dropdown disabled
- [x] `negativePrompt` wird im Referenz-Modus disabled + als "wird ignoriert" markiert
- [x] `generationMode` + `referenceImageIds` auf `LocalImage` persistiert (und in `toImage` propagiert)
- [ ] Detailansicht eines Bilds zeigt genutzte Referenzen — *offen*, ~1h
- **M6 — (optional, später) Lokaler Fallback via mana-image-gen** (mehrere Tage)
- [ ] FLUX + PuLID/InstantID auf GPU-Server
- **M5 — Tool-Registry + MCP-Exposure** ✅ SHIPPED `fc635f983`
- [x] `packages/mana-tool-registry/src/modules/me.ts`
- [x] `me.listReferenceImages({kind?})` — pullt via mana-sync (`app=profile`), decryptet `label`+`tags`, filtert auf `usage.aiReference=true`
- [x] `me.generateWithReference({prompt, referenceMediaIds, quality, size, n})` — Proxy über M3-Endpoint
- [x] MCP-Server exponiert beide automatisch (iteriert Registry in `createMcpServerForUser`)
- [x] 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`
- [ ] Routing über `local/flux-pulid` im apps/api-Endpoint
- **M7 — (optional, später) Inpainting-Mask-Drawing** (~2 Tage)
- **M7 — Inpainting Mask Drawing** (~2 Tage) — OFFEN
- [ ] Canvas-Mask-Editor im Picture-Generator
- [ ] Mask als zweites Medium hochladen + an `/edit` übergeben
- [ ] Mask als zweites Multipart-Part an `/v1/images/edits`
- **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
- **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)

View file

@ -0,0 +1,337 @@
# Wardrobe — Module Plan
## Status (2026-04-23)
Greenfield. Das Fundament (meImages + reference-based image generation) ist komplett verfügbar — siehe `docs/plans/me-images-and-reference-generation.md` Stand M1-M5. Dieses Modul konsumiert es.
## Ziel
Ein Nutzer pflegt seinen **digitalen Kleiderschrank**: einzelne Kleidungsstücke und Accessoires (Hemd, Hose, Schuhe, Brille, Uhr) mit Foto + Metadaten. Er kombiniert sie zu **Outfits** und nutzt KI, um sich selbst in dem Outfit zu visualisieren — als Anprobe ohne Spiegel oder als Vorschau vor dem Kauf.
Kernfragen, die dieser Plan beantwortet:
1. Wie bilden wir Kleidungsstücke + Outfits im Datenmodell ab?
2. Wie fließen sie in die KI-Generierung (me-images plus garment-photos)?
3. Welche Mindest-UI braucht es, damit das Feature tragfähig ist?
4. Wie machen wir Wardrobe-AI (Outfit-Vorschläge auf Basis von Kontext) möglich, ohne das Modul zu überladen?
## Abgrenzung
- **Kein generisches Fashion-Catalog / Shopping**: Dieses Modul verwaltet, was dem Nutzer *gehört*, nicht was er kaufen könnte. Wishlist lebt weiter in `wishes`.
- **Kein Ersatz für `inventory`**: `inventory` ist für physische Gegenstände mit Seriennummer/Garantie (Elektronik, Möbel). Kleidung hat eigene Primitive (Größe, Farbe, Kategorie, Tragevorschriften) — und eigene Use-Cases (Outfit-Komposition, Try-On). Ein Nutzer könnte seine 300€-Jacke in beiden führen — Wardrobe für Outfit-Zwecke, Inventory für Versicherungs-Zwecke. Wir doppeln den Datensatz bewusst nicht zusammen.
- **Kein Stylist-Coaching / Body-Shaming**: Die AI schlägt Kombinationen basierend auf *des Nutzers eigenem Kleiderschrank* vor, nicht auf abstraktem Stil-Urteil. Kein "Du solltest…".
- **Kein Online-Kauf**: Wishlist→Affiliate ist ein separater Track. Wardrobe ist Besitz.
- **Cross-Link zu `picture`**: Try-On-Ergebnisse landen in der Picture-Galerie (`picture.images`) wie jede andere Generierung. `LocalOutfit.lastTryOnImageId` pointet dorthin.
- **Cross-Link zu `me-images`**: Wardrobe liest `useImageByPrimary('face-ref')` + `useImageByPrimary('body-ref')` um den Nutzer zu visualisieren. Ohne primary face+body → Try-On nicht verfügbar.
## Entscheidungen
### 1. Ein Modul, zwei Tabellen
Das Repo hat Präzedenz für ein einzelnes Modul mit mehreren Tabellen (siehe `library` mit kind-discriminator, `invoices` mit clients/settings). Wardrobe ist dasselbe Muster:
- **`wardrobeGarments`** — einzelne Kleidungsstücke / Accessoires
- **`wardrobeOutfits`** — Zusammenstellungen (referenzieren garmentIds)
Kein separates `wardrobeTryOns`-Table: Try-On-Ergebnisse sind normale `picture.images` mit `generationMode='reference'` und einer Rück-Referenz `outfitId`. Warum kein drittes Table? Ein Try-On ist ein "snapshot of an outfit at a moment" — die einzigen neuen Daten sind `outfitId`-Rückreferenz + Pose-/Prompt-Kontext. Das passt als optionale Felder auf `LocalImage` (pattern wie `referenceImageIds`).
### 2. Garment = Foto + Kategorie, nicht Produkt-DB
Ein Garment ist für uns im MVP:
- ein Foto (mana-media-Upload, wie alles andere)
- eine Kategorie aus geschlossener Liste (top/bottom/shoes/accessory/…)
- paar freie Text-Felder (Name, Marke, Farbe, Notizen)
Wir bauen **keine Produkt-Datenbank**, keine Barcode-Scanning, keinen Marken-Katalog, kein EAN-Lookup. Das sind alles interessante Features, die das Modul nicht *tragen* muss, um nützlich zu sein. Eine einfache handy-gemachte Foto + Name-Tipperei reicht für den 80%-Fall.
### 3. Try-On ist ein Picture-Modul-Ergebnis
Ein Try-On-Call geht über den vorhandenen `POST /api/v1/picture/generate-with-reference`-Endpoint (M3 des me-images-Plans), mit einer erweiterten Referenz-Liste:
```
referenceMediaIds = [primaryFaceMediaId, primaryBodyMediaId, ...outfit.garmentIds.map(g => g.primaryMediaId)]
prompt = "Porträt von mir in diesem Outfit, realistisch, freundliches Lächeln"
```
Problem: M3 cappt bei 4 Referenzen. Ein Outfit mit 3 Kleidungsstücken + Face + Body = 5 Refs. Wardrobe M1 muss den Cap im Endpoint anheben (→ 8). OpenAI erlaubt bis 16, 8 ist ein vernünftiger Kompromiss zwischen Cost-Schutz und tatsächlichem Bedarf.
### 4. Outfit-Vorschläge sind ein Mission-Flow, kein Modul-Primitiv
"Mana, schlag mir ein Outfit für die Hochzeit am Samstag vor" ist eine natürliche Agent-Aufgabe, keine vordefinierte Wardrobe-Funktion. Der Persona-Runner (siehe Memory: `mana-persona-runner` auf :3070) kann die neuen MCP-Tools konsumieren:
- `wardrobe.listGarments({category?, tags?})` — was besitze ich
- `wardrobe.listOutfits({occasion?})` — welche Kombinationen habe ich schon
- `wardrobe.tryOn({outfitId, prompt?})` — visualisiere mich drin
- `wardrobe.createOutfit({name, garmentIds, ...})` — speichere eine neue Kombi
Die Persona plant dann frei: "Schau Wetter + Calendar → scanne wardrobeGarments → schlage 3 Outfits vor → frag user welches ihm gefällt → tryOn → speichere das gewählte als namens 'Hochzeit April'". Das ist keine Wardrobe-Logik, das ist Agent-Composition.
Wir bauen in diesem Modul keinen eigenen "Suggester" — das wäre ein redundantes Regel-System neben dem Persona-Runner.
### 5. Brillen / Accessoires brauchen nur Face-Ref
Für Kategorien `accessory`, `glasses`, `hat`, `jewelry` (Hals/Ohren) ist `primaryFullbody` nicht nötig — der Try-On läuft nur mit `primaryFace` + Garment-Photo. Das spart OpenAI-Credits und schärft die Ergebnisse (kein Ganzkörper-Rendering, das die Brille verkleinert). Die UI bietet für diese Kategorien einen "Brillen-Try-On"-Preset an (face-only prompt, 1024×1024 statt Portrait).
## Architektur-Überblick
```
┌─ Client (SvelteKit) ────────────────────────────────────┐
│ /wardrobe │
│ Grid-View: alle Garments, filter by category │
│ Detail: Garment oder Outfit, Try-On-Button │
│ /wardrobe/compose/[outfitId] │
│ Drag-drop Outfit-Builder │
│ Dexie: wardrobeGarments, wardrobeOutfits │
└──────┬──────────────────────────────────────────────────┘
│ mana-sync (encrypted name/notes/description)
┌─ Try-On-Flow (reuses M3 endpoint) ──────────────────────┐
│ POST /api/v1/picture/generate-with-reference │
│ referenceMediaIds = [face, body, ...garments] │
│ prompt = composed from outfit + occasion hint │
│ Result → picture.images with outfitId back-ref │
└─────────────────────────────────────────────────────────┘
┌─ MCP / Agent tools ─────────────────────────────────────┐
│ wardrobe.listGarments (read) │
│ wardrobe.listOutfits (read) │
│ wardrobe.createOutfit (write) │
│ wardrobe.tryOn (write — consumes credits) │
│ wardrobe.addGarment (write, multipart upload) │
└─────────────────────────────────────────────────────────┘
```
## Datenmodell
### `LocalWardrobeGarment`
```typescript
export type GarmentCategory =
| 'top' // Hemd, T-Shirt, Bluse, Pullover
| 'bottom' // Hose, Rock, Shorts
| 'dress' // Kleid, Anzug-Einteiler
| 'outerwear' // Jacke, Mantel
| 'shoes'
| 'accessory' // Schal, Gürtel, Tuch
| 'glasses' // Brille, Sonnenbrille
| 'jewelry' // Kette, Ring, Uhr, Ohrring
| 'hat'
| 'bag'
| 'other';
export interface LocalWardrobeGarment extends BaseRecord {
id: string;
name: string; // "Blau-weiß gestreiftes Hemd"
category: GarmentCategory;
mediaIds: string[]; // ≥1, first entry is the primary photo
brand?: string | null;
color?: string | null; // freeform — "navy", "hellgrau", "#2a4d6e"
size?: string | null; // freeform — "M", "42", "US 10"
material?: string | null;
tags: string[]; // "formal", "summer", "favorite", "needs-ironing"
notes?: string | null;
purchasedAt?: string | null;
priceCents?: number | null;
currency?: string | null; // ISO
isArchived?: boolean; // nicht mehr tragbar / weggegeben
wearCount?: number; // optional — zählt beim Markieren als "heute getragen"
lastWornAt?: string | null;
}
```
**Encryption-Registry-Eintrag:** `['name', 'brand', 'color', 'size', 'material', 'tags', 'notes']`. Kategorie, IDs, Zähler, Dates bleiben plaintext.
### `LocalWardrobeOutfit`
```typescript
export interface OutfitTryOn {
imageId: string; // points at picture.images
createdAt: string;
prompt: string;
model: string;
}
export interface LocalWardrobeOutfit extends BaseRecord {
id: string;
name: string; // "Bürooutfit Juni"
description?: string | null;
garmentIds: string[]; // refs zu LocalWardrobeGarment
occasion?: string | null; // 'work', 'casual', 'formal', 'workout', 'date', 'sleep'
season?: string[]; // ['spring', 'summer']
tags: string[];
isFavorite?: boolean;
isArchived?: boolean;
/**
* Most recent try-on (snapshot pointer). Full history lives in picture.images
* filtered by outfitId, so the UI can show a chronological strip.
*/
lastTryOn?: OutfitTryOn | null;
lastWornAt?: string | null;
}
```
**Encryption:** `['name', 'description', 'tags', 'occasion']`. garmentIds, season-enum, booleans, lastTryOn-pointer plaintext.
### Erweiterung auf `picture.images`
Ein neues optionales Feld:
```typescript
// apps/mana/apps/web/src/lib/modules/picture/types.ts
interface LocalImage {
// ... bestehend
wardrobeOutfitId?: string | null; // Rück-Referenz für "alle Try-Ons dieses Outfits"
}
```
Plaintext (ID-Feld), kein Registry-Change.
### `picture/generate-with-reference`-Endpoint Cap anheben
`MAX_REFERENCE_IMAGES` in `apps/api/src/modules/picture/routes.ts` von 4 auf 8. Begründung + Cost-Kalkül im Kommentar, identische Validierung.
## Modul-Struktur
```
apps/mana/apps/web/src/lib/modules/wardrobe/
├── types.ts # GarmentCategory, LocalWardrobeGarment, LocalWardrobeOutfit, OutfitTryOn
├── collections.ts # wardrobeGarmentsTable + wardrobeOutfitsTable
├── queries.ts # useAllGarments, useGarmentsByCategory, useOutfitById, useTryOnsForOutfit
├── module.config.ts # { appId: 'wardrobe', tables: [...] }
├── stores/
│ ├── garments.svelte.ts # createGarment, updateGarment, bumpWear, archive, delete
│ └── outfits.svelte.ts # createOutfit, addGarment, removeGarment, setLastTryOn, delete
├── api/
│ ├── upload.ts # uploadGarmentPhoto → POST /api/v1/wardrobe/garments/upload (app='wardrobe')
│ └── try-on.ts # runTryOn({outfit, prompt}) — wraps /picture/generate-with-reference
├── components/
│ ├── GarmentCard.svelte # Grid tile
│ ├── GarmentForm.svelte # Create/edit Sheet
│ ├── GarmentUploadZone.svelte
│ ├── OutfitCard.svelte
│ ├── OutfitComposer.svelte # Drag-drop Auswahl + Preview
│ ├── CategoryTabs.svelte
│ └── TryOnButton.svelte # Triggert runTryOn; zeigt Credits
├── views/
│ ├── GridView.svelte # Default: Kategorien + Garments
│ ├── OutfitsView.svelte # Alle Outfits
│ ├── DetailGarmentView.svelte
│ └── DetailOutfitView.svelte # zeigt Try-On-History
├── ListView.svelte # Modul-Root mit Tabs Garments/Outfits
├── constants.ts # CATEGORY_LABELS, SEASON_LABELS, OCCASION_LABELS
└── index.ts
```
Route-Seiten:
```
apps/mana/apps/web/src/routes/(app)/wardrobe/
├── +page.svelte # → ListView
├── garment/[id]/+page.svelte # → DetailGarmentView
├── outfit/[id]/+page.svelte # → DetailOutfitView
└── compose/[[outfitId]]/+page.svelte # OutfitComposer (new or edit)
```
## Backend
Ein neuer thin Upload-Endpoint:
```
POST /api/v1/wardrobe/garments/upload
```
wrappt `uploadImageToMedia({ app: 'wardrobe', userId })`. Pattern 1:1 wie `/api/v1/profile/me-images/upload` — drei Zeilen Unterschied (nur der `app`-String).
Ein neues apps/api-Module-Verzeichnis `apps/api/src/modules/wardrobe/routes.ts` + Route-Registrierung in `apps/api/src/index.ts`.
**Keine neue Try-On-Route:** Die Client-seitige `runTryOn()` ruft direkt den existierenden `/api/v1/picture/generate-with-reference`-Endpoint — der kennt keinen Wardrobe-Kontext, bekommt nur die mediaIds + prompt. Nach Erfolg schreibt der Client die `wardrobeOutfitId`-Rück-Referenz auf die entstandene `picture.images`-Zeile.
**Cap auf 8 anheben**: trivialer Einzeiler in `picture/routes.ts`. Credit-Berechnung bleibt identisch (pro Output-Bild, nicht pro Referenz).
## MCP-Tools (`packages/mana-tool-registry/src/modules/wardrobe.ts`)
Vier Tools, alle user-space. Pattern ist 1:1 an `me.ts` aus M5 angelehnt:
- **`wardrobe.listGarments({category?, tags?, limit?})`** — read. Pullt via mana-sync `app='wardrobe'`, entschlüsselt `name`+`brand`+`notes`+`tags`. Filter client-side.
- **`wardrobe.listOutfits({occasion?, favoriteOnly?})`** — read. Gleicher Pull-Pattern für `wardrobeOutfits`.
- **`wardrobe.createOutfit({name, garmentIds, occasion?, tags?})`** — write. Validiert dass alle garmentIds existieren und dem User gehören. Schreibt via `pushInsert`.
- **`wardrobe.tryOn({outfitId, prompt?, accessoryOnly?})`** — write (kostet Credits). Liest das Outfit, holt primary face + body + garment mediaIds, composed default-prompt falls keiner mitkommt, ruft die apps/api-Generate-Route. Response propagiert zurück, inklusive Credit-Kosten.
## Milestones
- **M1 — Datenschicht & Backend-Cap** (~11.5 Tage)
- [ ] Dexie v39: `wardrobeGarments` + `wardrobeOutfits` mit Indices
- [ ] Types + Encryption-Registry + Collections + Queries
- [ ] Stores (garments, outfits)
- [ ] `module.config.ts` registriert `appId='wardrobe'`
- [ ] `wardrobe` in Space-Modul-Allowlist aufnehmen (personal space + welche anderen?)
- [ ] `MAX_REFERENCE_IMAGES` Cap auf 8 (`apps/api/src/modules/picture/routes.ts`) mit Comment + ClientCap im Generator
- [ ] Neuer `POST /api/v1/wardrobe/garments/upload`-Endpoint + Route-Registrierung
- [ ] `wardrobeOutfitId`-Feld auf `LocalImage` + `toImage`-Converter
- **M2 — Garments-Grundlayer** (~11.5 Tage)
- [ ] Route `/wardrobe` mit `RoutePage`
- [ ] `CategoryTabs`, `GarmentCard`, `GarmentUploadZone`, `GarmentForm`
- [ ] Multi-File-Upload pro Kategorie (wie me-images)
- [ ] Detailseite `/wardrobe/garment/[id]` — Foto, Metadaten, "heute getragen"-Button (incrementiert `wearCount`)
- [ ] Archive / Delete / Edit flows
- **M3 — Outfits-Composer** (~11.5 Tage)
- [ ] Route `/wardrobe/compose/[[outfitId]]`
- [ ] Drag-drop-Leiste mit Garments (nach Kategorie gruppiert)
- [ ] Outfit-Preview-Kachel rechts (Stapel der Garment-Thumbnails)
- [ ] Create/Edit an dieselbe Route, `[[outfitId]]` optional
- [ ] Detailseite `/wardrobe/outfit/[id]`
- [ ] `OutfitsView` als zweiter Tab im Root
- **M4 — Try-On-Integration** (~1 Tag)
- [ ] `runTryOn(outfit, prompt?)` in `api/try-on.ts` — composed die reference-Liste aus `useImageByPrimary('face-ref' | 'body-ref')` + garment-mediaIds, ruft `/generate-with-reference`
- [ ] `accessoryOnly`-Preset für `glasses`/`jewelry`/`hat` — nur face-ref, quadratisches Format
- [ ] `TryOnButton.svelte` auf DetailOutfitView + auf DetailGarmentView (mit impliziten "Solo-Outfit")
- [ ] Nach Erfolg: `picture.images.wardrobeOutfitId` setzen + `lastTryOn`-Snapshot aufs Outfit
- [ ] Empty-State wenn `primaryFace` oder `primaryFullbody` fehlen → Link zu `/profile/me-images`
- [ ] Try-On-History als horizontaler Strip in DetailOutfitView
- **M5 — MCP-Tools** (~0.5 Tag)
- [ ] `packages/mana-tool-registry/src/modules/wardrobe.ts` mit den 4 Tools
- [ ] `'wardrobe'` zum `ModuleId`-Union
- [ ] `registerWardrobeTools()` in `registerAllModules()`
- **M6 — Persona-Templates** (~0.5 Tag, optional)
- [ ] Persona-Template "Stil-Coach": auto-Policy für `wardrobe.list*` + `me.listReferenceImages`, propose-Policy für `wardrobe.createOutfit` + `wardrobe.tryOn`
- [ ] Seed-Prompt: "Du bist der persönliche Stil-Coach. Schlage Outfits aus dem vorhandenen Kleiderschrank vor, basierend auf Kontext (Kalender-Event, Wetter, Nutzer-Stimmung). Nie kritisch, nie body-urteilend."
- [ ] Template-Eintrag unter `/agents/templates`
- **M7 — "Heute trage ich…"-Logging** (~0.5 Tag, optional)
- [ ] In GarmentCard + OutfitCard ein schnelles "heute getragen"-Flag, setzt `lastWornAt = today` + bumpt `wearCount`
- [ ] Stats-Widget: "Am häufigsten getragen", "Lange nicht mehr angehabt"
- **M8 — Kontext-basierte Suggestion** (optional, mehrere Tage)
- [ ] mana-ai Mission-Template "Outfit des Tages": liest calendar + wetter + wardrobe, erzeugt 3 Vorschläge als Proposals
- [ ] Im Workbench als Widget "Heute anziehen" (Card)
## Verschlüsselung
Alle user-typed Felder verschlüsselt (siehe Registry-Einträge oben). Bild-Blobs selbst bleiben in mana-media mit Owner-RLS — exakt wie bei meImages und picture.
Für Zero-Knowledge-Nutzer gilt dasselbe wie bei me-images: `ctx.getMasterKey()` in MCP-Tools throwet, die Tools fallen stumm aus. Client-seitige Blob-Verschlüsselung ist Teil von M8 des me-images-Plans und greift dann auch für Wardrobe-Fotos, wenn sie über denselben `uploadImageToMedia`-Pfad gehen.
## Cross-Modul-Impact
| Modul | Impact |
|---|---|
| `picture` | Neues optionales Feld `wardrobeOutfitId`. Cap auf `generate-with-reference` von 4 → 8. |
| `me-images` | Nichts — Wardrobe konsumiert nur `useImageByPrimary`. |
| `profile` | Nichts. |
| `shared-branding` | Neuer App-Eintrag `wardrobe` (Icon, Farbe, Tier — vermutlich `beta`). |
| `shared-types/spaces.ts` | `wardrobe` in die personal-space Allowlist (vermutlich auch `family` / `team` erlauben, aber nicht `club` / `practice`). |
## Offene Fragen (vor M1 klären)
1. **Photo-Quality-Anforderung an Garments**: reicht ein handy-Snap "Shirt auf dem Bett liegend", oder müssen wir flat-lay-erzwingen? → Empfehlung: akzeptieren was der Nutzer liefert, die gpt-image-Modelle sind robust. Falls M4-Ergebnisse systematisch schlecht werden, später ein "Bild-Guide" in die UI schreiben.
2. **Outfit ohne Foto hochladen**: darf ein Nutzer ein Outfit komponieren, bei dem ein Garment-Bild fehlt? → Empfehlung: ja, aber Try-On ist deaktiviert, bis alle Garments mindestens ein Foto haben. UX-Hinweis im Composer.
3. **Multi-Foto pro Garment**: sinnvoll (front / back / Detail), aber ein Primary-Foto reicht für Try-On. → `mediaIds: string[]` mit `mediaIds[0]` als Primary. UI macht das in M2 nicht sichtbar, kommt in M7 als Erweiterung.
4. **Kategorie-Detection via AI beim Upload**: "wir laden dein Foto hoch und schlagen die Kategorie vor" — interessant, aber eine nicht-triviale extra Inferenz. → NICHT M1-Scope. Später als optional Enrichment-Step.
5. **Space-Scope**: Wardrobe gehört klar in personal-space, aber gibt es Use-Cases für family-Space ("unser gemeinsamer Kinder-Kleiderschrank")? → offen. Default: nur `personal` + `family` in der Allowlist; `brand` / `club` nein.
6. **Accessoire-Try-On-Prompt-Template**: vorformatierter Prompt für Brillen ("Portrait frontal, freundliche Mimik, studio-Licht, ohne Hintergrundstörung, brillen-fokus") vs. freier Prompt? → Default mit Preset + der Nutzer kann überschreiben. Preset-Variante als MVP.
## Verweise
- me-images Fundament: `docs/plans/me-images-and-reference-generation.md`
- bestehender Edit-Endpoint: `apps/api/src/modules/picture/routes.ts:248-...`
- Tool-Registry me-Modul als Pattern: `packages/mana-tool-registry/src/modules/me.ts`
- Library-Plan als Struktur-Analogon: `docs/plans/library-module.md`
- Spaces-Modul-Allowlist: `packages/shared-types/src/spaces.ts:63-184`