chore: stop tracking internal notes and personal scripts
a4c15bc parent: 9307277 modified
.gitignore +5 -0 | @@ -30,3 +30,8 @@ Thumbs.db | ||
| 30 | 30 | |
| 31 | 31 | # vidéos d'exemple du skill hyperframes-animation (37 Mo, gardées en local par panoplie) |
| 32 | 32 | /.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/*.mp4 | 32 | /.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. | ||