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>
Le panneau Génération IA était inutilisable : on lançait une régénération
mais on ne voyait jamais le résultat dans l'interface (juste un toast).
Le mode 'Composite' du viewport ne marchait pas car il dépendait de la
table layers qui était vide.
Refonte complète du GenerationPanel:
- Big preview central : affiche la version actuellement sélectionnée
(Original / Fond IA / Perso IA / Composite)
- 4 boutons en haut pour switcher entre versions, désactivés si pas
encore générées
- 4 thumbnails en bas pour preview rapide + zoom au clic
- Auto-switch vers la version qui vient d'être générée
- Bouton "Composer" qui lance compositing.composeFrame
- Badge "généré" sur chaque action quand l'URL existe
- Modal de zoom plein écran
CompositePreview amélioré pour fonctionner SANS layers DB:
- Si compositedUrl existe : l'affiche directement (chemin rapide)
- Sinon empile bg + fg via <img> avec mixBlendMode CSS
- Plus de dépendance à la table layers vide
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>
- Nginx proxy_read_timeout augmenté de 60s à 300s (la génération
d'image peut prendre 10-30s, et le timeout Nginx coupait la connexion)
- Auth: skip appel OAuth quand l'utilisateur existe déjà en DB
- tRPC: ajout maxURLLength pour éviter les problèmes de batching
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>
- Overlay <video> pendant la lecture : utilise la vidéo source pour un
rendu fluide au lieu de swapper des <img> frame par frame
- La vidéo démarre au timecode correspondant au frame sélectionné
- Badge LECTURE animé pendant le playback
- L'overlay disparaît quand la lecture est en pause
- Audio géré séparément par l'HTMLAudioElement existant
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>