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

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

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

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

27 KiB
Raw Permalink Blame History

Wardrobe — Module Plan

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

M1M5 SHIPPED — Feature ist end-to-end benutzbar. Nutzer pflegt Garments + Outfits pro Space (alle sechs Space-Typen), komponiert über den Composer, rendert Try-On-Vorschauen via OpenAI gpt-image-2-Edits, und kann dasselbe via MCP-Tools an Personas/Agents delegieren. Solo-Garment-Try-On ("nur diese Brille anprobieren") als Follow-up in M4.1.

Milestone Commit Inhalt
M1 Datenschicht 4fc9d6c59 Dexie v41 wardrobeGarments + wardrobeOutfits (space-scoped), Types/Collections/Queries/Stores, module-registry, space-allowlist in allen 6 Typen, /api/v1/wardrobe/garments/upload, MAX_REFERENCE_IMAGES Cap 4→8, picture.images.wardrobeOutfitId Back-Ref
M2 Garments-UI 5a49bcbf0 /wardrobe Route, CategoryTabs, GarmentCard, GarmentForm, /wardrobe/garment/[id], Drag-Drop-Upload, edit/archive/delete flows, Active-Space-Badge
M3 Outfits-Composer 2b89bf795 /wardrobe/compose/[[outfitId]] Composer (click-to-add, garment-library left, editor right), OutfitsView-Tab, OutfitCard (try-on cover → garment collage fallback), /wardrobe/outfit/[id] Detail
M4 Try-On d56ad396d runOutfitTryOn + TryOnButton auf DetailOutfitView, Accessoire-Modus-Detection, Empty-State bei fehlenden Referenzen, non-personal-Space-Hinweis; verifyMediaOwnership erweitert auf ['me','wardrobe']
M5 MCP-Tools 7e3f53f8a (+ 66b7e08df für types/index) wardrobe.listGarments / .listOutfits / .createOutfit / .tryOn in packages/mana-tool-registry/src/modules/wardrobe.ts, registered in registerAllModules

Fundament konsumiert: me-images M1-M5 (siehe docs/plans/me-images-and-reference-generation.md) — Space-scoped meImages (v40), /api/v1/picture/generate-with-reference (gpt-image-2 via /v1/images/edits), useImageByPrimary('face-ref'|'body-ref').

Offen (nach M5)

  • M4.1 Solo-Garment-Try-OnrunGarmentTryOn() + GarmentTryOnButton auf DetailGarmentView. Render eines einzelnen Kleidungsstücks ohne Outfit-Kontext (z.B. Brille an mir ausprobieren). Ergebnis landet in picture.images ohne wardrobeOutfitId-Back-Ref. Implementiert als Plan-Follow-up.
  • M6 Persona-Template "Stil-Coach" (~0.5 Tag, optional) — neuer Eintrag unter /agents/templates mit 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."
  • M7 "Heute trage ich…"-Tiefe (~0.5 Tag, optional) — "heute getragen"-Button auf OutfitCard + Stats-Widget ("am häufigsten getragen", "lange nicht mehr angehabt"). Quick-Log-Button ist bereits in DetailGarmentView drin; fehlt nur Card-Ebene + Stats.
  • M8 Context-basierte Outfit-Mission (mehrere Tage, optional) — mana-ai Mission-Template "Outfit des Tages": liest calendar + wetter + wardrobe, erzeugt 3 Vorschläge als Proposals. Workbench-Widget "Heute anziehen" als Card.
  • Multi-Variant-Rendering (n=2/4) im TryOnButton — Picker-UI "Zeig mir 3 Looks" statt 1-Klick-1-Bild.
  • Multi-Foto pro GarmentmediaIds: string[] ist vorbereitet (Primary [0]); UI rendert aktuell nur Primary. Detail-Strip für alternate Views (front/back/detail) wäre die nächste Ausbau-Stufe.

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).

6. Space-scoped Katalog, user-scoped Try-On-Subject

Der Kleiderschrank selbst (wardrobeGarments + wardrobeOutfits) ist space-scoped: derselbe Mechanismus den tags/scenes/agents/missions/kontextDoc nach Phase 2c nutzen (spaceId, authorId, visibility per Hook gestempelt, Queries über scopedForModule<>). Das deckt sämtliche realen Use-Cases ab:

  • personal: der eigene Kleiderschrank
  • brand: Merchandise (T-Shirts, Caps, Zip-Hoodies) einer Marke — der Brand-Space ist gemeinsamer Pflegeort für alle Team-Mitglieder
  • club: Trikots, Vereinsbekleidung
  • family: Kinder-Kleiderschrank, gemeinsam von beiden Elternteilen gepflegt
  • team: Bühnenkostüme, Uniformen, Produktions-Wardrobe
  • practice: Praxis-Kittel, Dresscode-Items

Alle sechs Space-Typen bekommen wardrobe in die Allowlist.

Aber: Try-On-Referenzen (meImages) bleiben user-scoped — ein Mensch hat eine Identität, die er in jeden Space mitbringt. Konsequenz: wer in einem Brand-Space ein Merch-Hemd "anprobiert", sieht sich selbst im Hemd, nicht die Marke oder einen Avatar der Marke. Ein Vereins-Mitglied das in einem Club-Space auf "Trikot anprobieren" klickt, sieht sich selbst im Trikot — auch wenn der Katalog dem Verein gehört. Das deckt den intuitiven Fall ab ("wie sehe ich in dem Vereinstrikot aus") ohne dass wir ein zweites Subject-Konzept pro Space aufmachen.

Der einzige nicht-offensichtliche Fall ist family: Eltern pflegen den Kleiderschrank des Kindes, aber meImages eines Kindes existiert nicht (das Kind hat keinen eigenen Account). Try-On würde das Elternteil ins Kinder-Shirt rendern — absurd und unbrauchbar. Für family-Wardrobe machen wir zwei Dinge:

  1. Der Katalog-Teil (Garments + Outfits ansehen, komponieren, neu-eintragen) funktioniert ohne Einschränkung — das ist der Hauptwert für Familien.
  2. Der Try-On-Button bekommt einen Hinweis "Try-On in Familien-Spaces ist auf deine eigenen Bilder angewiesen — ein Bild wie 'so sähe das an meinem Kind aus' gibt es hier nicht."

Falls später konkreter Bedarf für "Try-On auf Familienmitglied" aufkommt, ist das ein separater Plan (neues Konzept spaceMembers[].faceMediaId oder ähnliches). Heute nicht spekulieren.

Membership-Gating: fällt automatisch aus dem Space-Foundation-Stack — scopedForModule<> filtert bereits auf aktive Space-Membership, mana-sync RLS cross-checked auf PostgreSQL-Ebene. Kein extra Code in Wardrobe.

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

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

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:

// 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 SHIPPED 4fc9d6c59

    • Dexie v41 (nicht v39 — me-images-space-migration hat v40 belegt): wardrobeGarments + wardrobeOutfits mit Indices (space-scoped, kein Compound-Index nötig — scopedTable filtert in-memory)
    • Types + Encryption-Registry (name/brand/color/size/material/tags/notes für Garments, name/description/tags für Outfits) + Collections + Queries via scopedForModule<>, nicht in USER_LEVEL_TABLES
    • Stores (garments, outfits) mit Domain-Events (WardrobeGarmentAdded, WardrobeOutfitCreated, WardrobeOutfitTryOn, etc.)
    • module.config.ts registriert appId='wardrobe'
    • wardrobe in alle sechs Space-Typen der Allowlist
    • MAX_REFERENCE_IMAGES Cap auf 8 (apps/api/src/modules/picture/routes.ts) + Client-Default in ReferenceImagePicker.svelte
    • POST /api/v1/wardrobe/garments/upload-Endpoint + Route-Registrierung
    • wardrobeOutfitId-Feld auf LocalImage + toImage-Converter
  • M2 — Garments-Grundlayer SHIPPED 5a49bcbf0

    • Route /wardrobe mit RoutePage
    • CategoryTabs, GarmentCard, GarmentForm; Upload-Zone reuses MeImageUploadZone (cross-module import, purely presentational)
    • Multi-File-Upload, aktive Kategorie bestimmt den default-Kind für neue Drops
    • Detailseite /wardrobe/garment/[id] — Foto, Metadaten, "heute getragen"-Button
    • Archive / Delete / Edit flows
    • Active-Space-Badge im Intro-Card
  • M3 — Outfits-Composer SHIPPED 2b89bf795

    • Route /wardrobe/compose/[[outfitId]]
    • Zwei-Spalten-Composer mit Garment-Library (nach Kategorie gruppiert) + Outfit-Editor. Click-to-Add statt Drag-Drop (keyboard-accessible, 100% workflow)
    • Outfit-Preview-Chips mit Hover-× zum Entfernen
    • Create/Edit an dieselbe Route, [[outfitId]] optional; {#key outfitId ?? 'new'} für sauberen Re-Mount
    • Detailseite /wardrobe/outfit/[id] mit Metadata-Card + Komposition-Grid + Try-On-Verlauf-Strip
    • OutfitsView als zweiter Tab in ListView mit "+ Neues Outfit"-CTA
  • M4 — Try-On-Integration SHIPPED d56ad396d

    • runOutfitTryOn in api/try-on.ts composed die reference-Liste aus aktivem Space's face-ref + body-ref + garment-mediaIds, ruft /generate-with-reference
    • accessoryOnly-Modus auto-detectiert aus FACE_ONLY_CATEGORIES — nur face-ref, 1024×1024 Format
    • TryOnButton auf DetailOutfitView (DetailGarmentView folgt in M4.1)
    • Nach Erfolg: picture.images.wardrobeOutfitId + lastTryOn-Snapshot aufs Outfit
    • Empty-State bei fehlenden Referenzen → Link zu /profile/me-images
    • Non-Personal-Space-Hinweis ("Try-On nutzt deine Referenzbilder aus diesem Space"); Family-Space-Sonderhinweis
    • Try-On-Verlauf-Strip via useOutfitTryOns (bereits in M3 angelegt, füllt sich nach erstem Render auto)
    • Server-side verifyMediaOwnership auf ['me','wardrobe'] erweitert
  • M4.1 — Solo-Garment-Try-On SHIPPED (folgender Commit)

    • runGarmentTryOn in api/try-on.ts — Single-Garment als "impliziter Solo-Outfit"; wardrobeOutfitId=null auf der erzeugten picture.images-Row
    • GarmentTryOnButton auf DetailGarmentView mit Inline-Preview des zuletzt erzeugten Bildes
    • Gemeinsamer callGenerateWithReference-Helper refactored aus runOutfitTryOn
    • isAccessoryGarment(garment) Helper für face-only Detection
  • M5 — MCP-Tools SHIPPED 7e3f53f8a (+ 66b7e08df für types/index)

    • packages/mana-tool-registry/src/modules/wardrobe.ts mit 4 Tools: listGarments, listOutfits, createOutfit, tryOn
    • 'wardrobe' im ModuleId-Union
    • registerWardrobeTools() in registerAllModules() — MCP exponiert automatisch
  • 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 alle sechs Space-Typen der Allowlist: personal, brand, club, family, team, practice (Entscheidung #6).

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-Scopeentschieden: alle sechs Space-Typen (siehe Entscheidung #6). Brand hat Merch, Clubs haben Trikots, Families gemeinsame Kleiderschränke, Teams Kostüme, Practices Dresscode. Try-On-Subject bleibt user-global — das deckt auch non-personal-Spaces sauber ab, mit Ausnahme von family (Kinder-Shirts "auf Kind rendern" ist out-of-scope; Katalog-Pflege funktioniert trotzdem).
  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