hlassiege/darylpublic Fork
⑂ a4c15bc
Commits
⬇ Clone ▾
git clone https://git.rickub.com/hlassiege/daryl.git
git clone ssh://git@rickub.com/hlassiege/daryl.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP

chore: stop tracking internal notes and personal scripts

hlassiege committed 2026-11-12T13:45:51+01:00 Browse files
a4c15bc parent: 9307277
modified .gitignore +5 -0
@@ -30,3 +30,8 @@ Thumbs.db
3030
3131 # vidéos d'exemple du skill hyperframes-animation (37 Mo, gardées en local par panoplie)
3232 /.claude/skills/hyperframes-animation/examples/assets/*.mp4
33+
34+# notes internes, scripts personnels et kits tiers (gardés en local, non publiés)
35+/docs/
36+/backend/scripts/compare_stt.py
37+/editing-os-starter/
@@ -30,3 +30,8 @@ Thumbs.db
30 30
31 # vidéos d'exemple du skill hyperframes-animation (37 Mo, gardées en local par panoplie)31 # vidéos d'exemple du skill hyperframes-animation (37 Mo, gardées en local par panoplie)
32 /.claude/skills/hyperframes-animation/examples/assets/*.mp432 /.claude/skills/hyperframes-animation/examples/assets/*.mp4
33+
34+# notes internes, scripts personnels et kits tiers (gardés en local, non publiés)
35+/docs/
36+/backend/scripts/compare_stt.py
37+/editing-os-starter/
deleted CLAUDE.md +0 -0
deleted file mode 100644
deleted file mode 100644
deleted backend/scripts/compare_stt.py +0 -174
deleted file mode 100644
@@ -1,174 +0,0 @@
1-"""
2-Compare des moteurs de transcription sur un extrait de rush : Whisper local (celui du studio),
3-Voxtral (API Mistral), Gladia (Solaria) et ElevenLabs (Scribe v2).
4-
5- uv run python scripts/compare_stt.py <video> [--from 210] [--to 330] [--only gladia,elevenlabs]
6-
7-Clés lues dans l'environnement (ou le registre Windows, comme FAL_KEY) :
8-MISTRAL_API_KEY, GLADIA_API_KEY, ELEVENLABS_API_KEY.
9-
10-Sortie dans data/stt-compare/<horodatage>/ : par moteur, la réponse brute (.json), les phrases
11-(.txt) et les mots (.words.txt), avec les temps en secondes du rush.
12-"""
13-import argparse
14-import json
15-import os
16-import subprocess
17-import sys
18-import tempfile
19-import time
20-from datetime import datetime
21-from pathlib import Path
22-
23-import httpx
24-
25-BACKEND_DIR = Path(__file__).resolve().parent.parent
26-sys.path.insert(0, str(BACKEND_DIR))
27-
28-from studio.config import DATA_DIR # noqa: E402
29-from studio.core.system import tool # noqa: E402
30-
31-LANG = "fr"
32-# au-delà de cette pause entre deux mots, on commence une nouvelle phrase (moteurs sans segments)
33-GAP = 0.7
34-
35-
36-def key(name: str) -> str:
37- found = os.environ.get(name)
38- if not found and sys.platform == "win32":
39- import winreg
40- try:
41- with winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Environment") as reg:
42- found = winreg.QueryValueEx(reg, name)[0]
43- except OSError:
44- pass
45- if not found:
46- raise RuntimeError(f"clé absente : {name}")
47- return found
48-
49-
50-def extract(video: Path, start: float, end: float | None, out: Path) -> None:
51- cmd = [tool("ffmpeg"), "-v", "error", "-y", "-ss", str(start), "-i", str(video)]
52- if end is not None:
53- cmd += ["-t", str(end - start)]
54- cmd += ["-vn", "-ac", "1", "-ar", "16000", str(out)]
55- subprocess.run(cmd, check=True)
56-
57-
58-# --- moteurs : chacun renvoie (réponse brute, mots [(début, fin, texte)], phrases [(début, fin, texte)])
59-
60-def run_whisper(wav: Path):
61- from studio.modules.transcript.whisper import Engine
62- segs = []
63- Engine().transcribe(wav, LANG, lambda s: None, lambda s, d: segs.append(s), lambda: None)
64- words = [(w.start, w.end, w.text.strip()) for s in segs for w in s.words]
65- return [s.to_json() for s in segs], words, [(s.start, s.end, s.text) for s in segs]
66-
67-
68-def run_voxtral(wav: Path):
69- with wav.open("rb") as f:
70- r = httpx.post("https://api.mistral.ai/v1/audio/transcriptions",
71- headers={"Authorization": f"Bearer {key('MISTRAL_API_KEY')}"},
72- data={"model": "voxtral-mini-latest", "timestamp_granularities": "word"},
73- files={"file": (wav.name, f, "audio/wav")}, timeout=600)
74- if r.status_code >= 400:
75- raise RuntimeError(f"Mistral {r.status_code} : {r.text}")
76- raw = r.json()
77- # avec timestamp_granularities=word, les « segments » renvoyés sont les mots
78- words = [(w["start"], w["end"], w["text"].strip()) for w in raw.get("segments") or []]
79- return raw, words, group(words)
80-
81-
82-def run_gladia(wav: Path):
83- h = {"x-gladia-key": key("GLADIA_API_KEY")}
84- with wav.open("rb") as f:
85- up = httpx.post("https://api.gladia.io/v2/upload", headers=h,
86- files={"audio": (wav.name, f, "audio/wav")}, timeout=600)
87- if up.status_code >= 400:
88- raise RuntimeError(f"Gladia upload {up.status_code} : {up.text}")
89- job = httpx.post("https://api.gladia.io/v2/pre-recorded", headers=h, timeout=60,
90- json={"audio_url": up.json()["audio_url"], "language_config": {"languages": [LANG]}})
91- if job.status_code >= 400:
92- raise RuntimeError(f"Gladia {job.status_code} : {job.text}")
93- url = f"https://api.gladia.io/v2/pre-recorded/{job.json()['id']}"
94- while True:
95- raw = httpx.get(url, headers=h, timeout=60).json()
96- if raw.get("status") == "done":
97- break
98- if raw.get("status") == "error":
99- raise RuntimeError(f"Gladia : {json.dumps(raw)[:500]}")
100- time.sleep(2)
101- utts = (raw.get("result") or {}).get("transcription", {}).get("utterances") or []
102- words = [(w["start"], w["end"], w["word"].strip()) for u in utts for w in u.get("words") or []]
103- return raw, words, [(u["start"], u["end"], u["text"].strip()) for u in utts]
104-
105-
106-def run_elevenlabs(wav: Path):
107- with wav.open("rb") as f:
108- r = httpx.post("https://api.elevenlabs.io/v1/speech-to-text",
109- headers={"xi-api-key": key("ELEVENLABS_API_KEY")},
110- data={"model_id": "scribe_v2", "language_code": LANG, "timestamps_granularity": "word",
111- "tag_audio_events": "true", "diarize": "false"},
112- files={"file": (wav.name, f, "audio/wav")}, timeout=600)
113- if r.status_code >= 400:
114- raise RuntimeError(f"ElevenLabs {r.status_code} : {r.text}")
115- raw = r.json()
116- words = [(w["start"], w["end"], w["text"].strip()) for w in raw.get("words") or []
117- if w.get("type") != "spacing"]
118- return raw, words, group(words)
119-
120-
121-def group(words):
122- """Phrases à partir des mots seuls : coupure sur une pause, ou après une fin de phrase."""
123- out, cur = [], []
124- for w in words:
125- if cur and (w[0] - cur[-1][1] >= GAP or cur[-1][2][-1:] in ".?!"):
126- out.append((cur[0][0], cur[-1][1], " ".join(x[2] for x in cur)))
127- cur = []
128- cur.append(w)
129- if cur:
130- out.append((cur[0][0], cur[-1][1], " ".join(x[2] for x in cur)))
131- return out
132-
133-
134-ENGINES = {"whisper": run_whisper, "voxtral": run_voxtral, "gladia": run_gladia, "elevenlabs": run_elevenlabs}
135-
136-
137-def ts(t: float) -> str:
138- return f"{int(t // 60):02d}:{t % 60:05.2f}"
139-
140-
141-def main() -> None:
142- ap = argparse.ArgumentParser()
143- ap.add_argument("video", type=Path)
144- ap.add_argument("--from", dest="start", type=float, default=0.0)
145- ap.add_argument("--to", dest="end", type=float)
146- ap.add_argument("--only", default=",".join(ENGINES))
147- a = ap.parse_args()
148-
149- out_dir = DATA_DIR / "stt-compare" / datetime.now().strftime("%Y%m%d-%H%M%S")
150- out_dir.mkdir(parents=True)
151- with tempfile.TemporaryDirectory() as tmp:
152- wav = Path(tmp) / "extrait.wav"
153- extract(a.video, a.start, a.end, wav)
154- for name in a.only.split(","):
155- print(f"{name}…", flush=True)
156- t0 = time.time()
157- try:
158- raw, words, segs = ENGINES[name](wav)
159- except Exception as e: # un moteur en échec n'empêche pas les autres
160- print(f" échec : {e}")
161- (out_dir / f"{name}.error.txt").write_text(str(e), encoding="utf-8")
162- continue
163- o = a.start
164- (out_dir / f"{name}.json").write_text(json.dumps(raw, ensure_ascii=False, indent=1), encoding="utf-8")
165- (out_dir / f"{name}.txt").write_text(
166- "".join(f"[{ts(s + o)} → {ts(e + o)}] {t}\n" for s, e, t in segs), encoding="utf-8")
167- (out_dir / f"{name}.words.txt").write_text(
168- "".join(f"{ts(s + o)} {ts(e + o)} {t}\n" for s, e, t in words), encoding="utf-8")
169- print(f" {len(segs)} phrases, {len(words)} mots, {time.time() - t0:.0f} s")
170- print(f"résultats : {out_dir}")
171-
172-
173-if __name__ == "__main__":
174- main()
deleted file mode 100644
@@ -1,174 +0,0 @@
1-"""
2-Compare des moteurs de transcription sur un extrait de rush : Whisper local (celui du studio),
3-Voxtral (API Mistral), Gladia (Solaria) et ElevenLabs (Scribe v2).
4-
5- uv run python scripts/compare_stt.py <video> [--from 210] [--to 330] [--only gladia,elevenlabs]
6-
7-Clés lues dans l'environnement (ou le registre Windows, comme FAL_KEY) :
8-MISTRAL_API_KEY, GLADIA_API_KEY, ELEVENLABS_API_KEY.
9-
10-Sortie dans data/stt-compare/<horodatage>/ : par moteur, la réponse brute (.json), les phrases
11-(.txt) et les mots (.words.txt), avec les temps en secondes du rush.
12-"""
13-import argparse
14-import json
15-import os
16-import subprocess
17-import sys
18-import tempfile
19-import time
20-from datetime import datetime
21-from pathlib import Path
22-
23-import httpx
24-
25-BACKEND_DIR = Path(__file__).resolve().parent.parent
26-sys.path.insert(0, str(BACKEND_DIR))
27-
28-from studio.config import DATA_DIR # noqa: E402
29-from studio.core.system import tool # noqa: E402
30-
31-LANG = "fr"
32-# au-delà de cette pause entre deux mots, on commence une nouvelle phrase (moteurs sans segments)
33-GAP = 0.7
34-
35-
36-def key(name: str) -> str:
37- found = os.environ.get(name)
38- if not found and sys.platform == "win32":
39- import winreg
40- try:
41- with winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Environment") as reg:
42- found = winreg.QueryValueEx(reg, name)[0]
43- except OSError:
44- pass
45- if not found:
46- raise RuntimeError(f"clé absente : {name}")
47- return found
48-
49-
50-def extract(video: Path, start: float, end: float | None, out: Path) -> None:
51- cmd = [tool("ffmpeg"), "-v", "error", "-y", "-ss", str(start), "-i", str(video)]
52- if end is not None:
53- cmd += ["-t", str(end - start)]
54- cmd += ["-vn", "-ac", "1", "-ar", "16000", str(out)]
55- subprocess.run(cmd, check=True)
56-
57-
58-# --- moteurs : chacun renvoie (réponse brute, mots [(début, fin, texte)], phrases [(début, fin, texte)])
59-
60-def run_whisper(wav: Path):
61- from studio.modules.transcript.whisper import Engine
62- segs = []
63- Engine().transcribe(wav, LANG, lambda s: None, lambda s, d: segs.append(s), lambda: None)
64- words = [(w.start, w.end, w.text.strip()) for s in segs for w in s.words]
65- return [s.to_json() for s in segs], words, [(s.start, s.end, s.text) for s in segs]
66-
67-
68-def run_voxtral(wav: Path):
69- with wav.open("rb") as f:
70- r = httpx.post("https://api.mistral.ai/v1/audio/transcriptions",
71- headers={"Authorization": f"Bearer {key('MISTRAL_API_KEY')}"},
72- data={"model": "voxtral-mini-latest", "timestamp_granularities": "word"},
73- files={"file": (wav.name, f, "audio/wav")}, timeout=600)
74- if r.status_code >= 400:
75- raise RuntimeError(f"Mistral {r.status_code} : {r.text}")
76- raw = r.json()
77- # avec timestamp_granularities=word, les « segments » renvoyés sont les mots
78- words = [(w["start"], w["end"], w["text"].strip()) for w in raw.get("segments") or []]
79- return raw, words, group(words)
80-
81-
82-def run_gladia(wav: Path):
83- h = {"x-gladia-key": key("GLADIA_API_KEY")}
84- with wav.open("rb") as f:
85- up = httpx.post("https://api.gladia.io/v2/upload", headers=h,
86- files={"audio": (wav.name, f, "audio/wav")}, timeout=600)
87- if up.status_code >= 400:
88- raise RuntimeError(f"Gladia upload {up.status_code} : {up.text}")
89- job = httpx.post("https://api.gladia.io/v2/pre-recorded", headers=h, timeout=60,
90- json={"audio_url": up.json()["audio_url"], "language_config": {"languages": [LANG]}})
91- if job.status_code >= 400:
92- raise RuntimeError(f"Gladia {job.status_code} : {job.text}")
93- url = f"https://api.gladia.io/v2/pre-recorded/{job.json()['id']}"
94- while True:
95- raw = httpx.get(url, headers=h, timeout=60).json()
96- if raw.get("status") == "done":
97- break
98- if raw.get("status") == "error":
99- raise RuntimeError(f"Gladia : {json.dumps(raw)[:500]}")
100- time.sleep(2)
101- utts = (raw.get("result") or {}).get("transcription", {}).get("utterances") or []
102- words = [(w["start"], w["end"], w["word"].strip()) for u in utts for w in u.get("words") or []]
103- return raw, words, [(u["start"], u["end"], u["text"].strip()) for u in utts]
104-
105-
106-def run_elevenlabs(wav: Path):
107- with wav.open("rb") as f:
108- r = httpx.post("https://api.elevenlabs.io/v1/speech-to-text",
109- headers={"xi-api-key": key("ELEVENLABS_API_KEY")},
110- data={"model_id": "scribe_v2", "language_code": LANG, "timestamps_granularity": "word",
111- "tag_audio_events": "true", "diarize": "false"},
112- files={"file": (wav.name, f, "audio/wav")}, timeout=600)
113- if r.status_code >= 400:
114- raise RuntimeError(f"ElevenLabs {r.status_code} : {r.text}")
115- raw = r.json()
116- words = [(w["start"], w["end"], w["text"].strip()) for w in raw.get("words") or []
117- if w.get("type") != "spacing"]
118- return raw, words, group(words)
119-
120-
121-def group(words):
122- """Phrases à partir des mots seuls : coupure sur une pause, ou après une fin de phrase."""
123- out, cur = [], []
124- for w in words:
125- if cur and (w[0] - cur[-1][1] >= GAP or cur[-1][2][-1:] in ".?!"):
126- out.append((cur[0][0], cur[-1][1], " ".join(x[2] for x in cur)))
127- cur = []
128- cur.append(w)
129- if cur:
130- out.append((cur[0][0], cur[-1][1], " ".join(x[2] for x in cur)))
131- return out
132-
133-
134-ENGINES = {"whisper": run_whisper, "voxtral": run_voxtral, "gladia": run_gladia, "elevenlabs": run_elevenlabs}
135-
136-
137-def ts(t: float) -> str:
138- return f"{int(t // 60):02d}:{t % 60:05.2f}"
139-
140-
141-def main() -> None:
142- ap = argparse.ArgumentParser()
143- ap.add_argument("video", type=Path)
144- ap.add_argument("--from", dest="start", type=float, default=0.0)
145- ap.add_argument("--to", dest="end", type=float)
146- ap.add_argument("--only", default=",".join(ENGINES))
147- a = ap.parse_args()
148-
149- out_dir = DATA_DIR / "stt-compare" / datetime.now().strftime("%Y%m%d-%H%M%S")
150- out_dir.mkdir(parents=True)
151- with tempfile.TemporaryDirectory() as tmp:
152- wav = Path(tmp) / "extrait.wav"
153- extract(a.video, a.start, a.end, wav)
154- for name in a.only.split(","):
155- print(f"{name}…", flush=True)
156- t0 = time.time()
157- try:
158- raw, words, segs = ENGINES[name](wav)
159- except Exception as e: # un moteur en échec n'empêche pas les autres
160- print(f" échec : {e}")
161- (out_dir / f"{name}.error.txt").write_text(str(e), encoding="utf-8")
162- continue
163- o = a.start
164- (out_dir / f"{name}.json").write_text(json.dumps(raw, ensure_ascii=False, indent=1), encoding="utf-8")
165- (out_dir / f"{name}.txt").write_text(
166- "".join(f"[{ts(s + o)} → {ts(e + o)}] {t}\n" for s, e, t in segs), encoding="utf-8")
167- (out_dir / f"{name}.words.txt").write_text(
168- "".join(f"{ts(s + o)} {ts(e + o)} {t}\n" for s, e, t in words), encoding="utf-8")
169- print(f" {len(segs)} phrases, {len(words)} mots, {time.time() - t0:.0f} s")
170- print(f"résultats : {out_dir}")
171-
172-
173-if __name__ == "__main__":
174- main()
deleted docs/plan-motion-storyboard.md +0 -215
deleted file mode 100644
@@ -1,215 +0,0 @@
1-# Plan — storyboard de motion design et bibliothèque d'inspirations
2-
3-Reprise d'idées d'**editing-os** (dossier `editing-os-starter/`, kit Claude Code + HyperFrames d'un autre
4-créateur, licence MIT, analysé le 09/10/2026). On en reprend les méthodes, pas les fichiers : ses scripts
5-supposent macOS et ses cartes sont en anglais, calées sur ses styles.
6-
7-## Où on en est
8-
9-**Fait (10/10/2026)**
10-- Dérush automatique : onglet **Daryl** de l'étape Dérush (`backend/studio/modules/projects/autoderush.py`,
11- `web/src/project/steps/derush/AutoDerush.vue`). Recalage des mots sur le son, silences avec respiration
12- selon le contexte, bégaiements, lecture des prises par Claude (coupes désignées en texte + ancre
13- `before`, jamais en timecode), contrôles (phrase encore dite deux fois, contenu coupé sans équivalent,
14- coupe introuvable). Rien n'est coupé sans « Appliquer ». Testé sur « Lever l'anonymat ? » : 35 coupes
15- de prises toutes retrouvées, 1 avertissement, ~1 min.
16-- Les transcriptions enregistrent leur moteur (`state.engine`, `transcript/tasks.py`). Dans « Lever
17- l'anonymat ? », les rushes 0021, 0024, 0025, 0026 et 0028 sont encore en Whisper (à refaire en ElevenLabs).
18-
19-- Phase 1.1 + 1.2 : `backend/studio/modules/projects/storyboard.py` (job `project_storyboard`, file
20- `storyboard`), routes `GET/POST /{pid}/montage/storyboard` et `POST …/storyboard/stop`. Le transcript de
21- la timeline est reconstruit mot à mot (`timeline_words`) ; Claude donne `line` + `anchor` + `until`
22- (jamais de timecode) ; `at` = mot d'ancre − 0,2 s, fin = dernier mot couvert + 0,5 s (2,5 à 15 s) ;
23- `fit` raccourcit un beat qui déborde ; `check` (refait à chaque lecture) signale `anchor`, `late`
24- (hors −0,2 / +1,8 s), `overlap`, `busy` (motion déjà là), `density`. Testé sur « Lever l'anonymat ? »
25- (17 min, densité équilibrée) : 32 beats en 2 min, toutes les ancres retrouvées, aucun avertissement.
26-- Phase 1.3 : le storyboard est **une étape à part** (`storyboard`, phase « long », avant Montage) :
27- `web/src/project/steps/StoryboardStep.vue` + `montage/useStoryboard.ts`. Beats en cartes larges
28- (quand / il dit + on montre / place, type, base du kit, son), aperçu fixe à droite avec la place du
29- beat dessinée dessus, bande de rythme en haut, ↑/↓ d'un beat à l'autre. Retouches par
30- `PUT …/storyboard`. « Valider → Montage » termine l'étape, sans rien générer.
31-- Places d'un beat : `full`, `left`, `right`, `top`, `lower` (l'ancien `side` est lu comme `right`).
32- Choisies sur un petit écran 16:9 cliquable. À la génération, Daryl reçoit 3 images de la vidéo
33- (20 / 50 / 80 % de la durée) pour mettre le visuel du côté libre. Les zooms restent au Montage.
34-- Chat du storyboard, sous l'aperçu (`storyboard_chat.py`, job `{pid}-storyboard-chat`, file `storyboard` ;
35- `web/src/project/steps/storyboard/StoryboardChat.vue`) : demande sur toute la vidéo ou sur le moment
36- choisi (beat sélectionné, sinon tête de lecture), avec l'image de la vidéo source à ce moment
37- (ffmpeg → `montage/frames/frame-<ms>.jpg`, lue par Daryl avec Read). Réponse structurée
38- (`add` / `update` / `remove`) : un beat ajouté est placé par ses mots exacts (`locate`), une ancre
39- introuvable est signalée. Les retouches à la main sont bloquées pendant sa réponse. Session reprise
40- d'un tour à l'autre (le transcript n'est envoyé qu'au premier). Pas encore testé avec Claude.
41-- Phase 1.4, dans le Montage (c'est là qu'on teste le rendu) : les beats cochés sans animation sont en
42- pointillés sur la piste motion (clic = la lecture va sur le beat) et en slide sur l'aperçu, à leur place
43- (`Preview.vue`, props `beats` / `create` / `anims`), avec « Créer l'animation », et « Créer les n
44- animations » dans la barre d'outils. `POST …/storyboard/animate` (`beats` : ids, sinon tous) :
45- `storyboard.compose` pose un motion par beat (`beat` = id du beat, place et durée validées) et son son
46- d'entrée (0,15 s avant, −12 dB), puis **une tâche par beat** (`beat_anim.py`, job `{pid}-anim-{beat}`)
47- écrit `motion/<id>.html` avec une fiche courte (`storyboard.brief`, mots du beat avec leurs instants,
48- `MOTION_RULES`, charte, modèle du kit). 3 files (`beat_anim_0..2`) : 3 créations à la fois, les autres
49- en file. État par beat dans `view.anim_jobs` (file, en cours + activité, échec) : sur la slide et dans
50- la barre « Animations » du Montage, avec Arrêter / Relancer. Un beat compte comme animé quand son HTML
51- existe (`storyboard.animated`) : un motion posé mais jamais écrit se relance. Le chat du monteur ne
52- compose plus de beats ; `contexte.md` liste les motions en cours de création pour qu'il n'y touche pas.
53- Lois chiffrées d'editing-os ajoutées à `MOTION_RULES`, « Fais le travail » retiré.
54-- **Types de beats** (`type`) : `motion`, `zoom`, `broll`, `transition` (un ancien beat est un motion).
55- - zoom : `zoom {scale 1,05–1,5, x, y}`, le centre borné pour que le cadre reste dans l'image (`zoom_of`),
56- 2 à 8 s, 0,1 s avant le mot ; posé tel quel sur la piste `zooms` (champ `beat`).
57- - broll : `src` (média visuel du projet ou de la bibliothèque, `storyboard.visuals`) ou, vide, une
58- suggestion « à trouver » avec `search`. Une vidéo va sur la piste `broll` ; une image devient un motion
59- animé par Daryl (copiée dans `motion/media/<beat>-<nom>`, plein écran ou sur un côté comme un portrait).
60- Les plans Image / Écran / B-roll du script deviennent des broll avec leur média attaché.
61- - transition : plein écran, 1,2 s, 0,9 s avant les premiers mots de la nouvelle partie ; un motion animé
62- par Daryl avec une fiche dédiée.
63- - Contrôles : chevauchement entre ce qui occupe l'écran (motion, broll, transition) et entre zooms,
64- `hidden` (zoom sous un plein écran), `busy` par piste ; la densité ne compte que les motions.
65- - Génération : choix des types à proposer (`types`). Écran : filtres par type, couleurs, carte par type
66- (zoom : grossissement + cadre cliquable sur l'image de la vidéo ; broll : média, aperçu, recherche à
67- copier, lien Assets). Montage : « Créer les n animations » et « Poser n zooms et B-roll » ; un zoom ou
68- un B-roll vidéo pas encore posé se voit déjà dans l'aperçu. `POST …/animate` rend
69- `{queued, placed, skipped, missing}`. Pas encore testé avec une vraie génération de Claude.
70-- **Suite décidée** : retours image par image dans le Montage (temps local du motion + beat + HTML
71- envoyés à Daryl, sans rendu ; retours accumulés puis envoyés ; un retour généralisable devient une règle).
72-- Plans du script : les plans Animation / Texte à l'écran de `script.json` sont les beats de départ
73- (`origin: "script"`, `plan` = id du plan), repérés dans la timeline par vote de triplets de mots
74- (`find_passage`) ; Daryl les détaille sans les remplacer et n'ajoute des beats (`origin: "daryl"`) que
75- pour la densité ; un plan qu'il oublie est posé tel quel ; un plan introuvable est signalé. Les plans
76- Image / Écran / B-roll / Insert / Plan de coupe occupent l'écran : contrôle `cover`. Les plans sont
77- aussi listés dans `contexte.md` pour le chat du monteur. « Lever l'anonymat ? » : 12 plans repris sur 13
78- (« 35 millions de comptes » dit autrement), 21 beats ajoutés, aucun avertissement.
79-
80-- **Bibliothèque de transitions** (10/10/2026) : `backend/studio/modules/projects/transitions.py`, les 55
81- transitions `xfade` de ffmpeg 7.1 en 5 familles (fondus, balayages, glissements, formes, effets ; `squeezev`
82- retirée, elle fait planter ffmpeg). Timeline : `transition` de l'A-roll = `{fx, dur}` (un ancien nombre est lu
83- comme un fondu), `enter` / `exit` sur B-roll et motions. Rendu (`render.overlay_groups`) : entrée et sortie =
84- `xfade` contre une image transparente, un plan collé au précédent de sa piste avec une entrée s'enchaîne avec
85- lui (centré sur la coupe, vraies images du média autour). Galerie : démos rendues par ffmpeg
86- (`data/cache/transitions/`), onglet Transitions de la Bibliothèque, sélecteur dans l'inspecteur du Montage et
87- sur les cartes motion / B-roll du storyboard. Aperçu du Montage en CSS (`web/src/lib/transitions.ts`,
88- `layersAt`), approché pour lamelles, vent, pixels, dissolution. Daryl propose `enter_fx` / `exit_fx` au
89- storyboard et connaît le catalogue au chat du Montage. Resolve : fondu enchaîné à la place des autres
90- transitions de l'A-roll, coupes franches pour les entrées / sorties (signalé à l'export).
91-
92-**Pas encore fait, à garder en tête**
93-- Vérification après rendu (re-transcrire le MP4 exporté et vérifier que les coupes ont porté).
94-- Mastering -14 LUFS (loudnorm deux passes, puis `aresample=48000`, `alimiter`, `aformat=s16` avant l'AAC :
95- voir `editing-os-starter/scripts/master.py`).
96-- Export de la timeline du dérush : remis à plus tard par l'utilisateur.
97-
98-## Constat de départ (étape Montage)
99-
100-- Daryl crée les motions **directement** dans `montage/timeline.json` + `montage/motion/<id>.html`. Son
101- prompt dit « Fais le travail, ne propose pas seulement » (`montage_tasks.py`), ce qui contredit la règle
102- de l'utilisateur : l'IA propose, l'utilisateur décide.
103-- Une seule charte (`animations/kit/style.css`) et 2 exemples (`exemple-arbre`, `exemple-chronologie`).
104-- Les shorts ont leur propre flux de slides (`animations/service.py`), avec des densités sobre / équilibrée / dense.
105-
106-## Phase 1 — Storyboard dans l'étape Montage
107-
108-But : Daryl propose d'abord un storyboard, l'utilisateur le valide, et seulement ensuite il compose.
109-Méthode reprise de `editing-os-starter/.claude/skills/design-beats/SKILL.md`.
110-
111-### 1.1 Génération (job `project_storyboard`, sortie structurée)
112-- Entrées : transcript en temps timeline (comme `contexte.md`), chapitres, script, charte, densité choisie,
113- sélection d'inspirations (phase 2).
114-- La question pour chaque passage : « qu'est-ce qu'on **montre** pendant qu'il dit ça ? », jamais « quel
115- texte on affiche ». Grille de traduction adaptée à la vidéo longue éducative : chiffre → compteur qui se
116- construit ; processus → schéma qui s'assemble ; comparaison → A/B ; date → chronologie ; outil cité →
117- capture ou logo ; liste → éléments qui se cochent. Interdits : carte-label statique, texte qui répète la
118- parole, décor.
119-- Un beat : `{id, at, dur, anchor (mots exacts du départ), says, show, kind, place (plein écran | côté |
120- bas), motion (entrée / entretien / sortie), sfx, ref (inspiration, phase 2), why}`.
121-- Stockage : `montage/storyboard.json`.
122-
123-### 1.2 Contrôles automatiques
124-- L'ancre existe dans le transcript, et le beat démarre entre 0,2 s avant et 1,8 s après le mot ancré
125- (« Gate 0 » d'editing-os) : sinon on recale ou on signale.
126-- Pas de chevauchement ; densité réelle comparée à la densité demandée.
127-- Ancre introuvable : signalée, pas devinée (comme le dérush automatique).
128-
129-### 1.3 Écran (onglet Storyboard de l'étape Montage, à côté du chat)
130-- Une ligne par beat : timecode cliquable, « il dit », « on montre » modifiable, type, place, case à cocher,
131- supprimer, « refaire ce beat ».
132-- Les beats en fantôme sur la piste motion de la timeline, pour juger le rythme.
133-- Bouton « Composer les n beats validés ».
134-
135-### 1.4 Composition depuis le storyboard
136-- Daryl ne compose que les beats validés, en suivant « on montre » ; tout écart est re-proposé.
137-- Le chat garde l'édition directe pour les petites demandes explicites ; une création d'ensemble passe par
138- le storyboard. Retirer « Fais le travail, ne propose pas seulement ».
139-- Ajouter à `MOTION_RULES` les lois chiffrées d'editing-os (`MOTION_PHILOSOPHY.md`, skill `motion-doctrine`) :
140- une idée par beat ; visuel 0,2 s avant le mot ; sortie ≈ 75 % de la durée d'entrée ; pas d'oscillation
141- décorative (`repeat: -1`, yoyo) ; immobilité de 0,3 à 0,75 s avant un climax ; la timeline doit durer
142- toute la composition (`tl.to({}, {duration}, 0)`), sinon flash noir.
143-
144-## Phase 2 — Bibliothèque d'inspirations
145-
146-But : une galerie de références de motion (démo, mécanique, prompt) dans laquelle l'utilisateur choisit et
147-annote ce que Daryl doit suivre. Modèle : `editing-os-starter/motion-library/` et
148-`scripts/motion-library/` (import, catégories, planches), `editing-os/lib/selection.mjs`.
149-
150-### 2.1 Données
151-- Base : `editing-os-starter/motion-library/analyse.json` — 270 références analysées (54 pépites,
152- 159 solides, 57 faibles ; 15 usages : explicatif, data, typo, UI, produit…). Chaque fiche :
153- `titre, resume, usage, usages, ambiance, mecanique {transitions, rythme, texte, techniques}, niveau,
154- pourquoi`.
155-- Import porté en Python : démos, posters et prompts depuis julienpocinho.fr et prompt-motion.com
156- (décodage des morceaux RSC Next.js, voir `import.mjs`). 4 requêtes à la fois au plus.
157-- Planche de 12 images (4×3, 480×270) par démo avec ffmpeg, lue directement depuis l'URL ; vignette = le
158- JPEG le plus lourd entre 25 et 85 % de la durée (les images noires pèsent presque rien).
159-- Stockage local `data/library/inspirations/` : **contenu de tiers, jamais dans le dépôt** (on ne versionne
160- que l'importeur).
161-- Références de l'utilisateur : URL ou clip (téléchargement yt-dlp existant) ; Claude regarde la planche et
162- remplit la fiche « mécanique » au même format.
163-
164-### 2.2 Écran (onglet Inspirations de la page Bibliothèque)
165-- Grille de posters, démo au survol ; filtres usage / niveau / ambiance ; les faibles masqués par défaut.
166-- Fiche : démo, planche, mécanique, prompt d'origine. « Ajouter à ma sélection » avec une note.
167-
168-### 2.3 Lien avec le storyboard
169-- Daryl reçoit la sélection et ses notes (prioritaires), puis l'index par usage ; il cite une référence par
170- beat avancé, affichée en vignette sur la ligne du beat.
171-- On reprend la mécanique (mouvement, rythme, transitions), jamais l'habillage : la charte reste celle de
172- l'utilisateur.
173-
174-## Plus tard — styles (design system)
175-Plusieurs chartes au choix par projet (`tokens.css` + `DESIGN.md`), puis des cartes à emplacements de texte
176-(`data-slot` + `maxChars`), rangées par tier (t1 plein écran opaque, t2 transparent qui ne couvre pas le
177-visage) et par usage, avec une registry et un tirage reproductible sans répétition consécutive (voir
178-`editing-os-starter/style-library/GUIDE.md`, `.claude/skills/motion-graphics/`). Le champ `kind` / `place`
179-du storyboard deviendra le choix d'une carte.
180-
181-## Ordre
182-1. Phase 1.1 + 1.2, testées sur un projet réel sans interface.
183-2. Phase 1.3 + 1.4 : utilisable pour de vrai.
184-3. Phase 2.1.
185-4. Phase 2.2 + 2.3.
186-
187-## Décisions (10/10/2026)
188-1. **Vidéo longue uniquement** pour l'instant (les shorts plus tard).
189-2. Densité : reprendre les trois densités des slides (`animations/service.py`, `DENSITIES`) : sobre
190- (~1 motion / 1 min 30), équilibrée (~1 / 30-45 s), dense (~1 / 15-20 s). Le rythme des reels
191- d'editing-os (un événement toutes les 1 à 2 s) est trop dense pour une vidéo longue. **« Équilibrée »
192- par défaut, réglable à chaque génération** (validé).
193-3. **Import complet** des inspirations (démos, posters, prompts des deux sites, en local).
194-
195-## Notes d'outillage (réponses du 10/10/2026)
196-- **Analyser une animation** : editing-os ne décompose pas image par image. ffmpeg tire une planche de
197- 12 images réparties sur la démo, Claude la regarde (vision) avec le prompt d'origine et écrit la fiche
198- « mécanique ». Ça capte la composition et les états, pas les courbes ni les timings fins. Pour reproduire
199- une transition précise : une planche dense autour du moment (8 à 12 images par seconde sur 1 à 2 s),
200- repéré par détection de changement de plan (`select='gt(scene,0.15)'`).
201-- **Détourage** : CLI HyperFrames, `npx hyperframes remove-background` (modèle u2net_human_seg, ~168 Mo,
202- local, gratuit). Sortie ProRes 4444 ou WebM VP9 avec alpha. Lent : ~2 images/s en 1080p sur CPU, donc
203- à réserver aux passages qui en ont besoin. Re-encoder d'abord une source à cadence variable (sinon sujet
204- dédoublé), et appliquer les mêmes coupes à la vidéo et au détourage. Usages : texte derrière la personne
205- (on rend vidéo + texte, puis on recolle la personne détourée par-dessus), personne qui dépasse d'une carte.
206-- **Musique et sons** :
207- - editing-os : un pack de SFX maison nommés par rôle (`whoosh`, `pop`, `click`, `cash`, `impact`,
208- `riser`, `buzzer`…) avec une table son ↔ animation et un volume par son (`asset-library/sfx/README.md`) ;
209- la musique via `npx hyperframes media-use resolve --type bgm` (catalogue HeyGen, compte heygen
210- requis) ; le mixage (ducking, fondus) via le skill `hyperframes-audio`.
211- - Studio : bibliothèque `data/library/` (musique, sfx), Daryl ne place que des fichiers listés dans
212- `contexte.md`, gains fixes, pas de ducking.
213- - À reprendre : nommer les SFX par rôle et donner la table son ↔ animation à Daryl ; un `sfx` par beat
214- dans le storyboard ; ducking de la musique sous la voix au rendu (`sidechaincompress` ffmpeg) ; le
215- catalogue HeyGen seulement si l'utilisateur veut un compte de plus.
deleted file mode 100644
@@ -1,215 +0,0 @@
1-# Plan — storyboard de motion design et bibliothèque d'inspirations
2-
3-Reprise d'idées d'**editing-os** (dossier `editing-os-starter/`, kit Claude Code + HyperFrames d'un autre
4-créateur, licence MIT, analysé le 09/10/2026). On en reprend les méthodes, pas les fichiers : ses scripts
5-supposent macOS et ses cartes sont en anglais, calées sur ses styles.
6-
7-## Où on en est
8-
9-**Fait (10/10/2026)**
10-- Dérush automatique : onglet **Daryl** de l'étape Dérush (`backend/studio/modules/projects/autoderush.py`,
11- `web/src/project/steps/derush/AutoDerush.vue`). Recalage des mots sur le son, silences avec respiration
12- selon le contexte, bégaiements, lecture des prises par Claude (coupes désignées en texte + ancre
13- `before`, jamais en timecode), contrôles (phrase encore dite deux fois, contenu coupé sans équivalent,
14- coupe introuvable). Rien n'est coupé sans « Appliquer ». Testé sur « Lever l'anonymat ? » : 35 coupes
15- de prises toutes retrouvées, 1 avertissement, ~1 min.
16-- Les transcriptions enregistrent leur moteur (`state.engine`, `transcript/tasks.py`). Dans « Lever
17- l'anonymat ? », les rushes 0021, 0024, 0025, 0026 et 0028 sont encore en Whisper (à refaire en ElevenLabs).
18-
19-- Phase 1.1 + 1.2 : `backend/studio/modules/projects/storyboard.py` (job `project_storyboard`, file
20- `storyboard`), routes `GET/POST /{pid}/montage/storyboard` et `POST …/storyboard/stop`. Le transcript de
21- la timeline est reconstruit mot à mot (`timeline_words`) ; Claude donne `line` + `anchor` + `until`
22- (jamais de timecode) ; `at` = mot d'ancre − 0,2 s, fin = dernier mot couvert + 0,5 s (2,5 à 15 s) ;
23- `fit` raccourcit un beat qui déborde ; `check` (refait à chaque lecture) signale `anchor`, `late`
24- (hors −0,2 / +1,8 s), `overlap`, `busy` (motion déjà là), `density`. Testé sur « Lever l'anonymat ? »
25- (17 min, densité équilibrée) : 32 beats en 2 min, toutes les ancres retrouvées, aucun avertissement.
26-- Phase 1.3 : le storyboard est **une étape à part** (`storyboard`, phase « long », avant Montage) :
27- `web/src/project/steps/StoryboardStep.vue` + `montage/useStoryboard.ts`. Beats en cartes larges
28- (quand / il dit + on montre / place, type, base du kit, son), aperçu fixe à droite avec la place du
29- beat dessinée dessus, bande de rythme en haut, ↑/↓ d'un beat à l'autre. Retouches par
30- `PUT …/storyboard`. « Valider → Montage » termine l'étape, sans rien générer.
31-- Places d'un beat : `full`, `left`, `right`, `top`, `lower` (l'ancien `side` est lu comme `right`).
32- Choisies sur un petit écran 16:9 cliquable. À la génération, Daryl reçoit 3 images de la vidéo
33- (20 / 50 / 80 % de la durée) pour mettre le visuel du côté libre. Les zooms restent au Montage.
34-- Chat du storyboard, sous l'aperçu (`storyboard_chat.py`, job `{pid}-storyboard-chat`, file `storyboard` ;
35- `web/src/project/steps/storyboard/StoryboardChat.vue`) : demande sur toute la vidéo ou sur le moment
36- choisi (beat sélectionné, sinon tête de lecture), avec l'image de la vidéo source à ce moment
37- (ffmpeg → `montage/frames/frame-<ms>.jpg`, lue par Daryl avec Read). Réponse structurée
38- (`add` / `update` / `remove`) : un beat ajouté est placé par ses mots exacts (`locate`), une ancre
39- introuvable est signalée. Les retouches à la main sont bloquées pendant sa réponse. Session reprise
40- d'un tour à l'autre (le transcript n'est envoyé qu'au premier). Pas encore testé avec Claude.
41-- Phase 1.4, dans le Montage (c'est là qu'on teste le rendu) : les beats cochés sans animation sont en
42- pointillés sur la piste motion (clic = la lecture va sur le beat) et en slide sur l'aperçu, à leur place
43- (`Preview.vue`, props `beats` / `create` / `anims`), avec « Créer l'animation », et « Créer les n
44- animations » dans la barre d'outils. `POST …/storyboard/animate` (`beats` : ids, sinon tous) :
45- `storyboard.compose` pose un motion par beat (`beat` = id du beat, place et durée validées) et son son
46- d'entrée (0,15 s avant, −12 dB), puis **une tâche par beat** (`beat_anim.py`, job `{pid}-anim-{beat}`)
47- écrit `motion/<id>.html` avec une fiche courte (`storyboard.brief`, mots du beat avec leurs instants,
48- `MOTION_RULES`, charte, modèle du kit). 3 files (`beat_anim_0..2`) : 3 créations à la fois, les autres
49- en file. État par beat dans `view.anim_jobs` (file, en cours + activité, échec) : sur la slide et dans
50- la barre « Animations » du Montage, avec Arrêter / Relancer. Un beat compte comme animé quand son HTML
51- existe (`storyboard.animated`) : un motion posé mais jamais écrit se relance. Le chat du monteur ne
52- compose plus de beats ; `contexte.md` liste les motions en cours de création pour qu'il n'y touche pas.
53- Lois chiffrées d'editing-os ajoutées à `MOTION_RULES`, « Fais le travail » retiré.
54-- **Types de beats** (`type`) : `motion`, `zoom`, `broll`, `transition` (un ancien beat est un motion).
55- - zoom : `zoom {scale 1,05–1,5, x, y}`, le centre borné pour que le cadre reste dans l'image (`zoom_of`),
56- 2 à 8 s, 0,1 s avant le mot ; posé tel quel sur la piste `zooms` (champ `beat`).
57- - broll : `src` (média visuel du projet ou de la bibliothèque, `storyboard.visuals`) ou, vide, une
58- suggestion « à trouver » avec `search`. Une vidéo va sur la piste `broll` ; une image devient un motion
59- animé par Daryl (copiée dans `motion/media/<beat>-<nom>`, plein écran ou sur un côté comme un portrait).
60- Les plans Image / Écran / B-roll du script deviennent des broll avec leur média attaché.
61- - transition : plein écran, 1,2 s, 0,9 s avant les premiers mots de la nouvelle partie ; un motion animé
62- par Daryl avec une fiche dédiée.
63- - Contrôles : chevauchement entre ce qui occupe l'écran (motion, broll, transition) et entre zooms,
64- `hidden` (zoom sous un plein écran), `busy` par piste ; la densité ne compte que les motions.
65- - Génération : choix des types à proposer (`types`). Écran : filtres par type, couleurs, carte par type
66- (zoom : grossissement + cadre cliquable sur l'image de la vidéo ; broll : média, aperçu, recherche à
67- copier, lien Assets). Montage : « Créer les n animations » et « Poser n zooms et B-roll » ; un zoom ou
68- un B-roll vidéo pas encore posé se voit déjà dans l'aperçu. `POST …/animate` rend
69- `{queued, placed, skipped, missing}`. Pas encore testé avec une vraie génération de Claude.
70-- **Suite décidée** : retours image par image dans le Montage (temps local du motion + beat + HTML
71- envoyés à Daryl, sans rendu ; retours accumulés puis envoyés ; un retour généralisable devient une règle).
72-- Plans du script : les plans Animation / Texte à l'écran de `script.json` sont les beats de départ
73- (`origin: "script"`, `plan` = id du plan), repérés dans la timeline par vote de triplets de mots
74- (`find_passage`) ; Daryl les détaille sans les remplacer et n'ajoute des beats (`origin: "daryl"`) que
75- pour la densité ; un plan qu'il oublie est posé tel quel ; un plan introuvable est signalé. Les plans
76- Image / Écran / B-roll / Insert / Plan de coupe occupent l'écran : contrôle `cover`. Les plans sont
77- aussi listés dans `contexte.md` pour le chat du monteur. « Lever l'anonymat ? » : 12 plans repris sur 13
78- (« 35 millions de comptes » dit autrement), 21 beats ajoutés, aucun avertissement.
79-
80-- **Bibliothèque de transitions** (10/10/2026) : `backend/studio/modules/projects/transitions.py`, les 55
81- transitions `xfade` de ffmpeg 7.1 en 5 familles (fondus, balayages, glissements, formes, effets ; `squeezev`
82- retirée, elle fait planter ffmpeg). Timeline : `transition` de l'A-roll = `{fx, dur}` (un ancien nombre est lu
83- comme un fondu), `enter` / `exit` sur B-roll et motions. Rendu (`render.overlay_groups`) : entrée et sortie =
84- `xfade` contre une image transparente, un plan collé au précédent de sa piste avec une entrée s'enchaîne avec
85- lui (centré sur la coupe, vraies images du média autour). Galerie : démos rendues par ffmpeg
86- (`data/cache/transitions/`), onglet Transitions de la Bibliothèque, sélecteur dans l'inspecteur du Montage et
87- sur les cartes motion / B-roll du storyboard. Aperçu du Montage en CSS (`web/src/lib/transitions.ts`,
88- `layersAt`), approché pour lamelles, vent, pixels, dissolution. Daryl propose `enter_fx` / `exit_fx` au
89- storyboard et connaît le catalogue au chat du Montage. Resolve : fondu enchaîné à la place des autres
90- transitions de l'A-roll, coupes franches pour les entrées / sorties (signalé à l'export).
91-
92-**Pas encore fait, à garder en tête**
93-- Vérification après rendu (re-transcrire le MP4 exporté et vérifier que les coupes ont porté).
94-- Mastering -14 LUFS (loudnorm deux passes, puis `aresample=48000`, `alimiter`, `aformat=s16` avant l'AAC :
95- voir `editing-os-starter/scripts/master.py`).
96-- Export de la timeline du dérush : remis à plus tard par l'utilisateur.
97-
98-## Constat de départ (étape Montage)
99-
100-- Daryl crée les motions **directement** dans `montage/timeline.json` + `montage/motion/<id>.html`. Son
101- prompt dit « Fais le travail, ne propose pas seulement » (`montage_tasks.py`), ce qui contredit la règle
102- de l'utilisateur : l'IA propose, l'utilisateur décide.
103-- Une seule charte (`animations/kit/style.css`) et 2 exemples (`exemple-arbre`, `exemple-chronologie`).
104-- Les shorts ont leur propre flux de slides (`animations/service.py`), avec des densités sobre / équilibrée / dense.
105-
106-## Phase 1 — Storyboard dans l'étape Montage
107-
108-But : Daryl propose d'abord un storyboard, l'utilisateur le valide, et seulement ensuite il compose.
109-Méthode reprise de `editing-os-starter/.claude/skills/design-beats/SKILL.md`.
110-
111-### 1.1 Génération (job `project_storyboard`, sortie structurée)
112-- Entrées : transcript en temps timeline (comme `contexte.md`), chapitres, script, charte, densité choisie,
113- sélection d'inspirations (phase 2).
114-- La question pour chaque passage : « qu'est-ce qu'on **montre** pendant qu'il dit ça ? », jamais « quel
115- texte on affiche ». Grille de traduction adaptée à la vidéo longue éducative : chiffre → compteur qui se
116- construit ; processus → schéma qui s'assemble ; comparaison → A/B ; date → chronologie ; outil cité →
117- capture ou logo ; liste → éléments qui se cochent. Interdits : carte-label statique, texte qui répète la
118- parole, décor.
119-- Un beat : `{id, at, dur, anchor (mots exacts du départ), says, show, kind, place (plein écran | côté |
120- bas), motion (entrée / entretien / sortie), sfx, ref (inspiration, phase 2), why}`.
121-- Stockage : `montage/storyboard.json`.
122-
123-### 1.2 Contrôles automatiques
124-- L'ancre existe dans le transcript, et le beat démarre entre 0,2 s avant et 1,8 s après le mot ancré
125- (« Gate 0 » d'editing-os) : sinon on recale ou on signale.
126-- Pas de chevauchement ; densité réelle comparée à la densité demandée.
127-- Ancre introuvable : signalée, pas devinée (comme le dérush automatique).
128-
129-### 1.3 Écran (onglet Storyboard de l'étape Montage, à côté du chat)
130-- Une ligne par beat : timecode cliquable, « il dit », « on montre » modifiable, type, place, case à cocher,
131- supprimer, « refaire ce beat ».
132-- Les beats en fantôme sur la piste motion de la timeline, pour juger le rythme.
133-- Bouton « Composer les n beats validés ».
134-
135-### 1.4 Composition depuis le storyboard
136-- Daryl ne compose que les beats validés, en suivant « on montre » ; tout écart est re-proposé.
137-- Le chat garde l'édition directe pour les petites demandes explicites ; une création d'ensemble passe par
138- le storyboard. Retirer « Fais le travail, ne propose pas seulement ».
139-- Ajouter à `MOTION_RULES` les lois chiffrées d'editing-os (`MOTION_PHILOSOPHY.md`, skill `motion-doctrine`) :
140- une idée par beat ; visuel 0,2 s avant le mot ; sortie ≈ 75 % de la durée d'entrée ; pas d'oscillation
141- décorative (`repeat: -1`, yoyo) ; immobilité de 0,3 à 0,75 s avant un climax ; la timeline doit durer
142- toute la composition (`tl.to({}, {duration}, 0)`), sinon flash noir.
143-
144-## Phase 2 — Bibliothèque d'inspirations
145-
146-But : une galerie de références de motion (démo, mécanique, prompt) dans laquelle l'utilisateur choisit et
147-annote ce que Daryl doit suivre. Modèle : `editing-os-starter/motion-library/` et
148-`scripts/motion-library/` (import, catégories, planches), `editing-os/lib/selection.mjs`.
149-
150-### 2.1 Données
151-- Base : `editing-os-starter/motion-library/analyse.json` — 270 références analysées (54 pépites,
152- 159 solides, 57 faibles ; 15 usages : explicatif, data, typo, UI, produit…). Chaque fiche :
153- `titre, resume, usage, usages, ambiance, mecanique {transitions, rythme, texte, techniques}, niveau,
154- pourquoi`.
155-- Import porté en Python : démos, posters et prompts depuis julienpocinho.fr et prompt-motion.com
156- (décodage des morceaux RSC Next.js, voir `import.mjs`). 4 requêtes à la fois au plus.
157-- Planche de 12 images (4×3, 480×270) par démo avec ffmpeg, lue directement depuis l'URL ; vignette = le
158- JPEG le plus lourd entre 25 et 85 % de la durée (les images noires pèsent presque rien).
159-- Stockage local `data/library/inspirations/` : **contenu de tiers, jamais dans le dépôt** (on ne versionne
160- que l'importeur).
161-- Références de l'utilisateur : URL ou clip (téléchargement yt-dlp existant) ; Claude regarde la planche et
162- remplit la fiche « mécanique » au même format.
163-
164-### 2.2 Écran (onglet Inspirations de la page Bibliothèque)
165-- Grille de posters, démo au survol ; filtres usage / niveau / ambiance ; les faibles masqués par défaut.
166-- Fiche : démo, planche, mécanique, prompt d'origine. « Ajouter à ma sélection » avec une note.
167-
168-### 2.3 Lien avec le storyboard
169-- Daryl reçoit la sélection et ses notes (prioritaires), puis l'index par usage ; il cite une référence par
170- beat avancé, affichée en vignette sur la ligne du beat.
171-- On reprend la mécanique (mouvement, rythme, transitions), jamais l'habillage : la charte reste celle de
172- l'utilisateur.
173-
174-## Plus tard — styles (design system)
175-Plusieurs chartes au choix par projet (`tokens.css` + `DESIGN.md`), puis des cartes à emplacements de texte
176-(`data-slot` + `maxChars`), rangées par tier (t1 plein écran opaque, t2 transparent qui ne couvre pas le
177-visage) et par usage, avec une registry et un tirage reproductible sans répétition consécutive (voir
178-`editing-os-starter/style-library/GUIDE.md`, `.claude/skills/motion-graphics/`). Le champ `kind` / `place`
179-du storyboard deviendra le choix d'une carte.
180-
181-## Ordre
182-1. Phase 1.1 + 1.2, testées sur un projet réel sans interface.
183-2. Phase 1.3 + 1.4 : utilisable pour de vrai.
184-3. Phase 2.1.
185-4. Phase 2.2 + 2.3.
186-
187-## Décisions (10/10/2026)
188-1. **Vidéo longue uniquement** pour l'instant (les shorts plus tard).
189-2. Densité : reprendre les trois densités des slides (`animations/service.py`, `DENSITIES`) : sobre
190- (~1 motion / 1 min 30), équilibrée (~1 / 30-45 s), dense (~1 / 15-20 s). Le rythme des reels
191- d'editing-os (un événement toutes les 1 à 2 s) est trop dense pour une vidéo longue. **« Équilibrée »
192- par défaut, réglable à chaque génération** (validé).
193-3. **Import complet** des inspirations (démos, posters, prompts des deux sites, en local).
194-
195-## Notes d'outillage (réponses du 10/10/2026)
196-- **Analyser une animation** : editing-os ne décompose pas image par image. ffmpeg tire une planche de
197- 12 images réparties sur la démo, Claude la regarde (vision) avec le prompt d'origine et écrit la fiche
198- « mécanique ». Ça capte la composition et les états, pas les courbes ni les timings fins. Pour reproduire
199- une transition précise : une planche dense autour du moment (8 à 12 images par seconde sur 1 à 2 s),
200- repéré par détection de changement de plan (`select='gt(scene,0.15)'`).
201-- **Détourage** : CLI HyperFrames, `npx hyperframes remove-background` (modèle u2net_human_seg, ~168 Mo,
202- local, gratuit). Sortie ProRes 4444 ou WebM VP9 avec alpha. Lent : ~2 images/s en 1080p sur CPU, donc
203- à réserver aux passages qui en ont besoin. Re-encoder d'abord une source à cadence variable (sinon sujet
204- dédoublé), et appliquer les mêmes coupes à la vidéo et au détourage. Usages : texte derrière la personne
205- (on rend vidéo + texte, puis on recolle la personne détourée par-dessus), personne qui dépasse d'une carte.
206-- **Musique et sons** :
207- - editing-os : un pack de SFX maison nommés par rôle (`whoosh`, `pop`, `click`, `cash`, `impact`,
208- `riser`, `buzzer`…) avec une table son ↔ animation et un volume par son (`asset-library/sfx/README.md`) ;
209- la musique via `npx hyperframes media-use resolve --type bgm` (catalogue HeyGen, compte heygen
210- requis) ; le mixage (ducking, fondus) via le skill `hyperframes-audio`.
211- - Studio : bibliothèque `data/library/` (musique, sfx), Daryl ne place que des fichiers listés dans
212- `contexte.md`, gains fixes, pas de ducking.
213- - À reprendre : nommer les SFX par rôle et donner la table son ↔ animation à Daryl ; un `sfx` par beat
214- dans le storyboard ; ducking de la musique sous la voix au rendu (`sidechaincompress` ffmpeg) ; le
215- catalogue HeyGen seulement si l'utilisateur veut un compte de plus.