Génération unique remplacée par un cycle créatif:
- Sélecteur "1x/2x/4x" en haut du panneau actions
- Quand >1, l'app génère N variantes en parallèle avec
variations légères du prompt (composition, lighting, colors, mood...)
- Pour 2x: prompt original + alternative composition
- Pour 4x: 4 explorations distinctes (composition, lighting, colors, mood)
- Toutes les variantes apparaissent dans la galerie M1
- La dernière devient active par défaut, user peut switcher
Prompt history:
- Endpoint frameVariants.promptHistory(frameId, type?) retourne
les 20 derniers prompts uniques utilisés sur cette frame
- Dropdown "↺ Réutiliser un prompt..." sous chaque textarea
- Cliquer un prompt l'injecte dans la textarea
Iterate from variant:
- Bouton GitBranch (icône) au hover de chaque variante
- Click = copie le prompt de la variante dans le textarea
- Permet itération "je raffine cette idée" en un clic
Backend:
- regenerateBackgroundBatch / regenerateCharacterBatch
- Promise.allSettled pour exécution parallèle robuste (échecs partiels OK)
- Chaque résultat crée une variante (M1)
- Retourne {generated, failed, variants, errors}
- Sync legacy field avec la dernière variante générée
- Validation max 8 variantes pour éviter l'abus
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
L'utilisateur peut désormais déplacer, redimensionner, rotater et
flipper le personnage généré sans devoir tout régénérer.
DB:
- Nouveau champ transform JSON sur frameVariants
- Format: {x, y, scale, rotation, flipH, flipV} avec coords relatives
Backend (sharp):
- compositeLayers applique transform avant le blend:
* scale: resize layer (peut être >100% ou <100%)
* rotation: sharp.rotate avec fond transparent
* flipH/flipV: flop/flip
* x/y: offset en pourcentage de la base (centré + delta)
- Gestion intelligente des layers qui dépassent: extract crop
(sharp interdit top/left négatifs et inputs plus grands que la base)
- compositing.composeFrame récupère le transform de la variant
character active automatiquement
- Nouveau endpoint frameVariants.updateTransform
Frontend (LayerManipulator):
- Composant overlay avec bounding box pointillée + 8 handles
- Handles coins = scale, handle haut = rotation, area centrale = move
- CSS transform live (translate/scale/rotate/scaleX(-1) pour flip)
- Toolbar flottante: flip H/V, position/scale/rotation affichés en live
- Reset button quand transformé
- Bouton "Recomposer" déclenche composeFrame avec le nouveau transform
- Save backend automatique au release de souris
ViewportPanel:
- Bouton "Manipuler" dans toolbar (visible uniquement mode composite)
- Active LayerManipulator overlay, mutuellement exclusif avec Annoter/Loupe
- Désactivé si pas de variant character actif (toast warn)
Workflow:
1. Mode composite dans viewport
2. Click "Manipuler" → handles apparaissent
3. Drag pour déplacer / corners pour scale / handle haut pour rotation
4. Sauvegarde auto au release (en DB)
5. Click "Recomposer" → sharp regenère avec transform appliqué
6. Nouvelle variante composite créée (Module 1)
7. La galerie M1 montre l'avant/après
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
L'outil d'annotation existant ne servait qu'à créer un masque global.
Maintenant chaque masque peut déclencher un inpainting IA qui
modifie UNIQUEMENT la zone sélectionnée.
Backend:
- convertMaskForOpenAI(): convertit notre format (blanc=édit/noir=préserve)
vers format OpenAI (alpha=0=édit/opaque=préserve)
- Auto-redimensionne le mask aux dims de l'image source
- generateImage() accepte maintenant un paramètre maskUrl
- OpenAI images.edits utilise le param "mask" + champ "image" (singulier)
pour le mode inpainting
- Nouveau endpoint generation.inpaintZone(frameId, maskUrl, prompt, sourceType)
- sourceType: original / bg (regen actif) / fg (perso actif) / composite
- Crée une nouvelle variante du type approprié (Module 1)
- Synchronise les champs legacy
Frontend (AnnotationCanvas):
- Nouveau bouton "Inpainter zone" dans la toolbar
- Form dropdown avec sélecteur de source (original/composite/bg/fg)
et prompt textarea
- handleInpaint: upload du masque + appel inpaintZone + new variant
- Sauve masque (bouton existant renommé "Sauver masque") séparé de l'inpainting
- AnnotationCanvas reçoit projectId + frameIndex pour pouvoir appeler les routes
Workflow utilisateur:
1. Mode "Annoter" dans le viewport (sur frame originale)
2. Dessine au pinceau/rectangle/lasso la zone à modifier
3. Click "Inpainter zone"
4. Choisit source (original/composite/etc.) + écrit le prompt
5. Click "Lancer inpainting"
6. OpenAI génère uniquement la zone masquée
7. Nouvelle variante créée et visible dans la galerie M1
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Le pipeline de génération écrasait silencieusement les versions
précédentes. Maintenant chaque génération crée une variante
sans perdre l'historique.
Backend:
- Nouvelle table frameVariants (id, frameId, type, url, prompt,
provider, generationTimeMs, isActive, isPinned, label, metadata)
- 5 helpers DB: createFrameVariant, listFrameVariants,
setActiveVariant, deleteFrameVariant, toggleVariantPin, renameVariant
- Routeur tRPC frameVariants: list, setActive, delete, togglePin, rename
- regenerateBackground/regenerateCharacter/composeFrame créent
désormais une variante + désactivent les anciennes du même type
- Les champs legacy (regeneratedBgUrl, etc.) restent synchronisés
avec la variante active pour backward-compat
- Tracking des metadata (style, characterId, sourceLayers, etc.)
- Mesure du generationTimeMs par variante
Frontend:
- Nouveau composant VersionsGallery (thumbnails horizontaux avec
active radio, pin star, delete, tooltip prompt+meta+time)
- Intégré dans GenerationPanel sous chaque bloc (fond, perso, composite)
- Click sur une variante = la désactive les autres et l'active
- Hover = actions (épingler, supprimer)
- Tooltip détaillé (prompt, provider, durée, date)
- Pinned en premier, puis chronologique desc
Migration:
- Script migre les regen URLs existantes en variants
(12 variantes migrées sur 8 frames du projet 90001)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Format ratio (60):
- OpenAI gpt-image-1: choix automatique de size (1024x1024 / 1536x1024 /
1024x1536) selon ratio cible le plus proche
- Crop sharp post-génération pour matcher EXACTEMENT le ratio source
- segmentationService passe les dimensions du projet (width/height)
à regenerateBackground et regenerateCharacter
- Routes generation.* récupèrent project.width/height depuis DB
- Testé: frame 25 (854x480) -> bg généré en 854x480 exact
Redirect bug (61):
- DashboardLayout: navigate("/login") déplacé du render vers useEffect
(anti-pattern React fix)
- main.tsx redirectToLoginIfUnauthorized: préserve l'URL d'origine
dans ?return=...
- Login.tsx onSuccess: redirige vers ?return URL au lieu de "/"
- Plus de retour à l'accueil après refresh ou opération
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- OPENAI_API_KEY ajouté dans .env.docker
- imageGeneration.ts: tente Gemini d'abord, fallback OpenAI sur erreur
- Support images.edits (multipart avec ref image) pour gpt-image-1
- Support images.generations (text-only) si pas de ref
- Provider trace dans GenerateImageResponse pour debug
- Testé: pipeline complet frame 25
* Fond Ghibli: 21s via OpenAI (Gemini 429) -> 1.6MB PNG
* Personnage moderne: 26s via OpenAI -> 1.6MB PNG RGBA
* Composite final: 1s via sharp -> 2.6MB PNG
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Modèle corrigé: gemini-2.0-flash-exp (supprimé) -> gemini-2.5-flash-image
- Pipeline de génération testé et fonctionnel
(bloqué temporairement par quota API gratuit)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Chat IA:
- Scroll natif avec auto-scroll vers le dernier message
- Input toujours visible en bas du panneau
- Remplace ScrollArea (ref cassé) par div overflow-y-auto
Panneaux redimensionnables:
- Bottom panel (timeline): drag vertical pour ajuster la hauteur (120-600px)
- Right panel (assistant): drag horizontal pour ajuster la largeur (280-700px)
- Handles visuels avec feedback hover
Timeline:
- Molette = zoom (sans Ctrl), Shift+molette = scroll horizontal
- Zoom max augmenté à 20x
- Clic droit = menu contextuel (aller à frame, set IN/OUT, effacer)
- data-seq-id sur les blocs de séquence pour identification
Génération IA:
- Meilleur error handling avec message dans le toast (8s durée)
- Console.error pour debug navigateur
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Cache utilisateur en mémoire (60s TTL) : élimine la query
getUserByOpenId (~300ms) de chaque requête authentifiée
- Suppression du upsertUser(lastSignedIn) à chaque requête
- staleTime sur toutes les queries (auth.me: 60s, workspace: 30s,
frames: 120s, home: 15s)
- refetchOnWindowFocus: false partout
Résultat: auth.me 300ms -> 70ms, workspace load 2.5s -> 0.8s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
L'authenticateRequest appelait getUserInfoWithJwt sur l'API Manus
à chaque requête, même pour les utilisateurs déjà en base.
Maintenant: si l'utilisateur existe en DB, on le retourne directement
sans appel réseau externe. L'appel OAuth ne se fait que pour les
nouveaux utilisateurs non-locaux.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- listAllProjects() pour les admins (sans filtre userId)
- Les users normaux voient toujours uniquement leurs projets
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Nouvel onglet "Génération IA" dans le workspace avec 2 boutons:
* Regénérer l'arrière-plan (prompt + style)
* Redessiner le personnage (prompt + sélecteur de character sheet)
- 3 endpoints tRPC: generation.regenerateBackground,
generation.regenerateCharacter, generation.inpaintBackground
- Fix URLs relatives -> signed URLs S3 absolues pour l'API Forge
- Résultat affiché en preview dans le panneau
- Testé: génération cyberpunk sur frame 200 -> PNG 1344x768 OK
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Chargement bulk des URLs de frames (1 query au lieu de 1/frame)
- Preload des 30 prochaines frames pendant la lecture
- Cache navigateur activé sur le proxy S3 (max-age=3600, immutable)
- Fallback query tRPC uniquement si la map n'a pas la frame
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>