Skip to content

Commit 08e8acc

Browse files
Merge pull request #2020 from cgwire/refactoring-players
[players] Refactor preview players into composables and shared bars
2 parents 2745f64 + e0228a9 commit 08e8acc

109 files changed

Lines changed: 24860 additions & 16467 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CLAUDE.md

Lines changed: 79 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,14 @@ src/
2525
lists/ # List/table components (StudioList, etc.)
2626
modals/ # Modal dialogs (BaseModal, EditStudiosModal, etc.)
2727
pages/ # Page-level components (Studios, etc.)
28-
previews/ # Preview/player components
28+
players/ # Preview/playlist player components (annotations, bars, viewers, ...)
2929
sides/ # Sidebar components
3030
tops/ # Topbar components
3131
widgets/ # Reusable UI widgets (Combobox*, DateField, etc.)
32-
mixins/ # Legacy Options API mixins (annotation.js, etc.)
33-
composables/ # Composition API composables (modal.js, combobox.js, etc.)
32+
mixins/ # Legacy Options API mixins still being migrated to composables
33+
composables/
34+
players/ # Player-specific composables (annotation, comparison, playlistComparison, ...)
35+
# Generic ones (modal, combobox, ...) stay at the root
3436
lib/ # Utility libraries (csv, string, sorting, etc.)
3537
locales/ # i18n translation files (en.js, fr.json, etc.)
3638
router/ # Vue Router configuration
@@ -301,7 +303,80 @@ The `modalMixin` in `src/components/modals/base_modal.js` is being replaced by t
301303

302304
## Key dependencies
303305

304-
- **fabric.js** v5.1.0 (cgwire fork) — annotation canvas, used only in `src/components/mixins/annotation.js` and `src/components/previews/PreviewPlayer.vue`
306+
- **fabric.js** v5.1.0 (cgwire fork) — annotation canvas, wrapped by `src/composables/players/annotation.js` and `src/components/players/annotations/AnnotationCanvas.vue`
305307
- **socket.io-client** — real-time events via `vue-websocket-next`
306308
- **moment / moment-timezone** — date handling (used throughout schedule and timesheet components)
307309
- **vue-multiselect** — people/entity selection dropdowns
310+
311+
## Intégration des fonctionnalités IA dans Kitsu
312+
313+
Cette section définit les règles à appliquer pour toute fonctionnalité IA ajoutée à Kitsu (cloud et self-hosted). Elle s'inspire de l'enquête [*Le forcing de l'IA*](https://limitesnumeriques.fr/travaux-productions/ai-forcing) (Limites Numériques, février 2025), qui documente les patterns par lesquels les éditeurs imposent l'IA via le design, au détriment des usages réels.
314+
315+
**Principe directeur** : Kitsu intègre l'IA sans forcing. L'IA est une fonctionnalité comme une autre, justifiée par un usage validé, et non un produit poussé pour lui-même.
316+
317+
### 1. Design d'interface
318+
319+
- Pas de couleur dédiée à l'IA (pas de violet, mauve, dégradé bleu-rose, gradient shimmer).
320+
- Pas d'icône évoquant la magie (pas de ✨, pas d'étoile, pas de baguette).
321+
- Pas d'animation spécifique aux boutons ou zones IA quand les autres fonctions sont statiques.
322+
- Les fonctionnalités IA utilisent les mêmes composants visuels (couleurs, typographie, iconographie Material) que le reste de Kitsu.
323+
- Pas de placement privilégié : l'IA ne prend pas le bouton principal d'une vue (Tâches, Casting, Breakdown, Playlists). Elle se loge dans le contexte métier où elle a un sens.
324+
- Pas de répétition de la même fonction IA à plusieurs endroits de l'interface.
325+
326+
### 2. Activation et contrôle
327+
328+
- Activation explicite. Pas de déclenchement par raccourci clavier fréquent ou par clic accidentel sur la zone de saisie principale.
329+
- Opt-in par défaut, pas opt-out, pour toute fonctionnalité IA significative.
330+
- En self-hosted, IA désactivée par défaut, activable explicitement par l'administrateur du studio.
331+
- Bouton clair de désactivation par projet et par studio.
332+
333+
### 3. Nommage et vocabulaire
334+
335+
- Pas de prénom d'assistant ("Kit", "Kitty", "Kitsubot", etc.).
336+
- Pas de visage, d'avatar ou de mascotte associé à l'IA.
337+
- Pas de métaphore de l'"assistant" qui aide sans remplacer.
338+
- Pas de vocabulaire magique ("magie", "boost", "wow", "intelligent").
339+
- Décrire ce que la fonction fait, factuellement. Exemple : "Générer un résumé des notes de review", pas "Résumer ✨".
340+
- Indiquer aussi ce que la fonction ne fait pas et ses limites connues (taux d'erreur, hallucinations possibles, types de contenus mal traités).
341+
342+
### 4. Transparence
343+
344+
- Pour chaque appel IA, le studio peut savoir : quel modèle est utilisé, où il tourne (local ou cloud), quelles données sortent du studio, vers quel fournisseur.
345+
- Cette information est accessible dans les paramètres et dans la documentation, pas cachée derrière plusieurs clics.
346+
- Quand pertinent, exposer le coût (tokens, latence, ordre de grandeur énergétique) à l'admin du studio.
347+
- Privilégier les modèles locaux ou auto-hébergeables quand c'est viable, surtout pour le self-hosted.
348+
349+
### 5. Choix de fonctionnalités
350+
351+
- Une fonctionnalité IA n'est développée que pour répondre à un point de douleur identifié et validé avec 2 ou 3 studios pilotes.
352+
- Pas de fonctionnalité IA "parce qu'il en faut" ou "parce que les concurrents en ont".
353+
- Pas de "tâtonnement" public : les expérimentations restent en feature flag ou en bêta privée tant que l'usage n'est pas démontré.
354+
- Mesure de l'usage réel (pas le premier clic d'essai). Une fonction non utilisée au bout de 3 mois est retirée, pas conservée par inertie.
355+
356+
### 6. Communication produit
357+
358+
- Pas de pop-up d'annonce, de modal d'onboarding ou de tour guidé poussant à essayer l'IA.
359+
- Pas de badge "Nouveau" ou "AI" clignotant sur le bouton.
360+
- Une page de documentation dédiée suffit. Le changelog mentionne la fonctionnalité au même niveau que les autres.
361+
- Sur le blog et la roadmap, expliquer **pourquoi** la fonction arrive (quel problème studio elle résout), pas seulement qu'elle arrive.
362+
363+
### 7. Cadrage éditorial
364+
365+
- L'IA est cadrée en empowerment des équipes (artistes, supes, prod), pas en remplacement.
366+
- Le ton évite l'emphase sur la performance brute du modèle, et insiste sur l'intégration dans le workflow.
367+
- Cohérent avec la sensibilité de la communauté open source VFX/animation : transparence, respect des métiers, attention à l'empreinte.
368+
369+
### 8. Positionnement
370+
371+
Cette charte n'est pas seulement défensive. Elle ouvre un angle de différenciation : Kitsu peut se positionner comme **le production tracker qui intègre l'IA sans forcing**, à rebours des patterns documentés par Limites Numériques. Cet angle peut être assumé publiquement (article de blog dédié, mention dans la doc, communication communauté).
372+
373+
### Checklist de revue avant merge d'une feature IA
374+
375+
- [ ] Aucune couleur, icône ou animation spécifique IA
376+
- [ ] Activation explicite, pas de raccourci ambigu
377+
- [ ] Opt-in (et désactivé par défaut en self-hosted)
378+
- [ ] Pas d'anthropomorphisation
379+
- [ ] Modèle, hébergement et données documentés et visibles
380+
- [ ] Point de douleur validé avec au moins 2 studios pilotes
381+
- [ ] Métriques d'usage en place
382+
- [ ] Pas de pop-up ou onboarding poussant à l'essai
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
# `usePanzoomSync` — Design
2+
3+
**Date** : 2026-05-14
4+
**Branche** : `refactoring-review`
5+
**Scope** : extraire la logique de tracking de la transform panzoom partagée
6+
entre `SharedPlaylistPlayer` et (à terme) `PreviewPlayer`, et préparer le
7+
terrain pour l'objectif final : annoter en zoomant/panant dans
8+
`PreviewPlayer`.
9+
10+
## Contexte
11+
12+
Aujourd'hui le tracking de la transform panzoom existe sous deux formes
13+
incompatibles dans la base de code :
14+
15+
- **`SharedPlaylistPlayer`** : `panzoomTransform: Ref<{x,y,scale}>` mis à
16+
jour via `@panzoom-changed`, passé en prop à `SharedAnnotationOverlay`
17+
qui applique `fabric.setViewportTransform(...)` sur le canvas
18+
d'annotation. Les annotations suivent le zoom en temps réel.
19+
- **`PreviewPlayer`** : pas de tracking. Le toggle `isZoomPan` est modal :
20+
zoom ON cache le fabric canvas (`v-show="!isZoomPan && …"`), zoom OFF
21+
reset à scale 1. Annotations et zoom sont mutuellement exclusifs.
22+
23+
Cap visé : faire converger `PreviewPlayer` vers le pattern overlay-live
24+
(annotations toujours visibles, suivent la transform). Mais on y va
25+
progressivement.
26+
27+
## Approche choisie
28+
29+
Composable minimal, agnostique du viewer : `state + handlers`.
30+
31+
```js
32+
// src/composables/panzoom.js
33+
import { ref } from 'vue'
34+
35+
export function usePanzoomSync() {
36+
const transform = ref({ x: 0, y: 0, scale: 1 })
37+
38+
const onPanzoomChanged = ({ x, y, scale }) => {
39+
transform.value = { x, y, scale }
40+
}
41+
42+
const reset = () => {
43+
transform.value = { x: 0, y: 0, scale: 1 }
44+
}
45+
46+
const applyTo = fabricCanvas => {
47+
if (!fabricCanvas) return
48+
const { x, y, scale } = transform.value
49+
fabricCanvas.setViewportTransform([scale, 0, 0, scale, x, y])
50+
fabricCanvas.requestRenderAll()
51+
}
52+
53+
return { transform, onPanzoomChanged, reset, applyTo }
54+
}
55+
```
56+
57+
**Pourquoi pas un composable couplé au viewer (qui encapsulerait aussi
58+
`resumeZoom`/`pauseZoom`)** : la gestion impérative du panzoom underlying
59+
(via `PreviewViewer.resumeZoom`/`pauseZoom`/`resetZoom`) est corrélée à
60+
plusieurs états dans `PreviewPlayer` (`isAnnotationsDisplayed`,
61+
`isDrawing`, `isTyping`) qui n'ont rien à voir avec le tracking de la
62+
transform. Mélanger les deux responsabilités élargirait l'API du
63+
composable sans bénéfice.
64+
65+
## Mode comparison (PreviewPlayer)
66+
67+
Décision : zoom synchronisé sur les deux viewers. Une seule instance de
68+
`usePanzoomSync` par `PreviewPlayer`. Le viewer comparison adoptera la
69+
même transform que le viewer principal en phase 3 (en propageant
70+
impérativement via `PictureViewer.setPanZoom` qui existe déjà).
71+
72+
## Découpage en phases
73+
74+
### Phase 1 — Extraction + adoption dans `SharedPlaylistPlayer`
75+
76+
Remplace exactement le code dupliqué actuel
77+
(`SharedPlaylistPlayer.vue:265, 608-610, 744`) :
78+
79+
```js
80+
import { usePanzoomSync } from '@/composables/panzoom'
81+
82+
const { transform: panzoomTransform, onPanzoomChanged, reset: resetPanzoomTransform } =
83+
usePanzoomSync()
84+
85+
// dans watch(isZoomEnabled, …)
86+
resetPanzoomTransform()
87+
```
88+
89+
- L'alias `transform: panzoomTransform` préserve le nom utilisé dans le
90+
template (`:panzoom-transform="panzoomTransform"`).
91+
- `applyTo` n'est **pas** consommé par `SharedPlaylistPlayer` : la cible
92+
(fabric canvas dans `SharedAnnotationOverlay`) est dans un autre
93+
composant qui reçoit `panzoomTransform` en prop et applique lui-même
94+
(`SharedAnnotationOverlay.vue:217-223`). Ce flux prop-down reste tel
95+
quel pour cette PR.
96+
- Comportement strictement identique. Zéro changement visible.
97+
98+
### Phase 2 — Adoption dans `PreviewPlayer` (modal préservé) + fix bug comparison
99+
100+
**2a. Fix du bug "comparisonViewer figé en zoom-pan"**
101+
102+
`PreviewPlayer.vue:2095-2101` aujourd'hui :
103+
104+
```js
105+
watch(isZoomPan, () => {
106+
if (isZoomPan.value) {
107+
previewViewer.value.resumeZoom()
108+
} else {
109+
previewViewer.value.pauseZoom()
110+
}
111+
})
112+
```
113+
114+
→ Symétriser sur les deux viewers + reset :
115+
116+
```js
117+
watch(isZoomPan, enabled => {
118+
const viewers = [previewViewer.value, comparisonViewer.value]
119+
if (enabled) {
120+
viewers.forEach(v => v?.resumeZoom())
121+
} else {
122+
viewers.forEach(v => {
123+
v?.pauseZoom()
124+
v?.resetZoom()
125+
})
126+
resetPanzoomTransform()
127+
}
128+
})
129+
```
130+
131+
**2b. Brancher `usePanzoomSync` (uniquement `reset` pour l'instant)**
132+
133+
```js
134+
const { transform: panzoomTransform, onPanzoomChanged, reset: resetPanzoomTransform, applyTo: applyPanzoomTo } =
135+
usePanzoomSync()
136+
```
137+
138+
Phase 2 = on n'ajoute que ce qu'on utilise : `resetPanzoomTransform()`
139+
aux endroits où les viewers sont reset (`watch(isZoomPan)` ci-dessus,
140+
`onAnnotationDisplayedClicked` au `PreviewPlayer.vue:1450-1456`,
141+
`clearPreview` au `:2052`). `onPanzoomChanged`, `applyTo` et la ref
142+
`transform`/`panzoomTransform` ne sont pas branchés en phase 2 — ils
143+
arrivent en phase 3.
144+
145+
### Phase 3 — Annoter en zoomant dans `PreviewPlayer` (esquisse, hors de ce design doc)
146+
147+
Pour vérifier que l'API phase 1 est suffisante :
148+
149+
1. Retirer `v-show="!isZoomPan && …"` sur `canvas-wrapper` et
150+
`canvas-comparison-wrapper` → les fabric canvases sont toujours dans
151+
le DOM.
152+
2. Retirer le `resetZoom` automatique dans
153+
`onAnnotationDisplayedClicked` et `watch(isAnnotationsDisplayed)`.
154+
3. Dans `useAnnotation`, ajouter `applyPanzoomToCanvas(transform)` qui
155+
appelle `fabricCanvas.setViewportTransform(...)` sur les deux canvases
156+
(main + comparison). Soit injecté en paramètre du composable, soit
157+
exposé pour que le composant le wire avec `watch(transform,
158+
applyPanzoomToCanvas)`.
159+
4. Brancher `@panzoom-changed="onPanzoomChanged"` sur le main viewer.
160+
5. Synchroniser le panzoom underlying du comparison viewer sur celui du
161+
main via `PictureViewer.setPanZoom` (existe déjà,
162+
`PictureViewer.vue:333-351`). Ajouter l'équivalent côté video si
163+
besoin.
164+
165+
**Risque connu** : le canvas comparison du `PreviewPlayer` est shifté de
166+
`getDimensions().width / 2` dans `fixCanvasComparisonSize`. Suivre la
167+
transform live avec `setViewportTransform` va probablement nécessiter
168+
d'ajuster ce positionnement. À traiter en phase 3.
169+
170+
## Tests
171+
172+
### Unitaires (composable) — `tests/unit/composables/panzoom.spec.js`
173+
174+
Le folder `tests/unit/composables/` n'existe pas encore, à créer.
175+
176+
Cas :
177+
178+
- État initial : `transform.value` égal à `{x:0, y:0, scale:1}`.
179+
- `onPanzoomChanged({x:10, y:20, scale:2})``transform.value` reflète
180+
exactement.
181+
- `reset()` après modification → revient à `{0,0,1}`.
182+
- `applyTo(null)` / `applyTo(undefined)` → no-op, ne lève pas.
183+
- `applyTo(fakeCanvas)` avec transform non-identité → `setViewportTransform`
184+
appelé avec `[scale, 0, 0, scale, x, y]` + `requestRenderAll` appelé.
185+
Fake canvas = objet avec deux spies (pas de stub fabric global).
186+
- Réactivité : muter via `onPanzoomChanged` puis lire — la valeur
187+
retournée est la même ref que celle exposée.
188+
189+
### Consommateurs
190+
191+
Pas de nouveaux specs. Ni `SharedPlaylistPlayer` ni `PreviewPlayer` n'ont
192+
de couverture aujourd'hui — c'est un refactor sans changement observable
193+
côté UX en phase 1, et un fix isolé en phase 2.
194+
195+
### Vérif manuelle (phase 2)
196+
197+
- Task avec preview vidéo + une révision précédente → comparison mode →
198+
activer zoom-pan → **les deux** viewers zooment (2a).
199+
- Désactiver zoom-pan → les deux reviennent à scale 1, transform interne
200+
reset.
201+
- SharedPlaylistPlayer : lancer un playlist partagé, vérifier que les
202+
annotations suivent toujours le zoom (régression possible si l'alias
203+
`transform: panzoomTransform` est mal câblé).
204+
205+
## Risques et hors-scope
206+
207+
- **Hors-scope phase 1/2** : refonte du positionnement du canvas
208+
comparison dans `PreviewPlayer`.
209+
- **Risque transverse** : le bug fix 2a change un comportement observable
210+
(les deux viewers zooment au lieu d'un seul). Si un utilisateur s'en
211+
était inconsciemment accommodé, c'est un changement de feel — assumé
212+
car le comportement actuel est clairement un oubli, pas un design.
213+
- **Aucun risque côté SharedPlaylistPlayer** : refactor pur, même
214+
comportement, mêmes événements, mêmes consommateurs.

0 commit comments

Comments
 (0)