mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:01:09 +02:00
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 (M189258eb45, M2a64a7e39c, M2.5e2b5ac38c, M3 in38dc80654, M4 ind087b4744, M5fc635f983) 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:
parent
9589feb296
commit
638f9c34d6
2 changed files with 424 additions and 41 deletions
|
|
@ -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:<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)
|
||||
|
||||
|
|
|
|||
337
docs/plans/wardrobe-module.md
Normal file
337
docs/plans/wardrobe-module.md
Normal 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** (~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`
|
||||
Loading…
Add table
Add a link
Reference in a new issue