diff --git a/docs/plans/me-images-and-reference-generation.md b/docs/plans/me-images-and-reference-generation.md index d93c36045..d3b68a3d2 100644 --- a/docs/plans/me-images-and-reference-generation.md +++ b/docs/plans/me-images-and-reference-generation.md @@ -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`. +**M1–M5 + 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:'` 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) diff --git a/docs/plans/wardrobe-module.md b/docs/plans/wardrobe-module.md new file mode 100644 index 000000000..05a6d66bb --- /dev/null +++ b/docs/plans/wardrobe-module.md @@ -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** (~1–1.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** (~1–1.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** (~1–1.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`