feat(storyboard): lay part transitions as a-roll pauses
ef15d3a parent: 6fab937 modified
backend/studio/modules/projects/storyboard.py +78 -7 | @@ -79,8 +79,9 @@ ZOOM_SLOW_DEFAULT = 1.1 | ||
| 79 | 79 | ZOOM_DUR = (1.5, 8.0) |
| 80 | 80 | ZOOM_SLOW_DUR = (4.0, 15.0) |
| 81 | 81 | ZOOM_LEAD = 0.1 |
| 82 | -# transition : courte, l'écran couvert au milieu, juste avant les premiers mots de la nouvelle partie | |
| 83 | -TRANSITION_DUR = 1.2 | |
| 82 | +# transition : juste avant les premiers mots de la nouvelle partie ; posée en pause (la voix s'arrête dessous), elle | |
| 83 | +# dure assez pour lire son titre et voir son animation (à 1,2 s, on ne lit rien) | |
| 84 | +TRANSITION_DUR = 3.0 | |
| 84 | 85 | TRANSITION_LEAD = 0.9 |
| 85 | 86 | # un beat posé à la main : sa durée par défaut, et sa durée minimale |
| 86 | 87 | HAND_DUR = 3.0 |
| @@ -316,9 +317,16 @@ une image est mise en scène, en plein écran (`place` full) ou sur un côté co | ||
| 316 | 317 | `right`) : dans `motion`, des gestes nets calés sur ses mots (ce qui s'isole, se surligne, se détache, \ |
| 317 | 318 | au mot qui le nomme), pas « fondu + lent zoom ». Sinon `media` vide, `search` = la recherche à faire pour le trouver (mots-clés concrets, en \ |
| 318 | 319 | {lang}) et `show` = le plan idéal ; l'auteur le trouvera ou le tournera. |
| 319 | -- transition : un enchaînement animé plein écran, court, au changement de partie ou de chapitre, \ | |
| 320 | -une par changement au plus. `anchor` = les premiers mots de la nouvelle partie ; `show` = ce qu'elle \ | |
| 321 | -affiche (le titre de la partie, en quelques mots) ; `until` = les mêmes mots que `anchor`. | |
| 320 | +- transition : un carton animé plein écran au changement de partie ou de chapitre, une par \ | |
| 321 | +changement au plus. La vidéo s'arrête dessous (image noire, voix coupée) environ 3 secondes, le temps \ | |
| 322 | +de lire le titre, puis la voix reprend sur `anchor`. `anchor` = les premiers mots de la PREMIÈRE \ | |
| 323 | +PHRASE de la nouvelle partie, au début de cette phrase ; `show` = ce qu'elle affiche (le titre de la \ | |
| 324 | +partie, en quelques mots) ; `until` = les mêmes mots que `anchor`. Ce qui précède l'ancre doit être \ | |
| 325 | +une pensée finie : la phrase d'avant se termine (point, point d'interrogation) et clôt la partie \ | |
| 326 | +précédente. Lis les deux ou trois phrases autour : une chute, un commentaire ou une conclusion \ | |
| 327 | +courte qui suit (« Et ça, c'est pas bon. », « Voilà. », « Bref, ça marchait. ») appartient encore à \ | |
| 328 | +la partie précédente, la transition vient APRÈS elle. Jamais au milieu d'une phrase ni entre une \ | |
| 329 | +question et sa réponse, une idée et son explication ; sans frontière nette, pas de transition. | |
| 322 | 330 | `kind`, `anim` et `motion` ne servent qu'aux motions (et `place` aux motions et aux images) ; `sfx` \ |
| 323 | 331 | aux motions, B-roll et transitions. |
| 324 | 332 | |
| @@ -1187,6 +1195,8 @@ def compose(pid: str, only: list[str] | None = None) -> dict: | ||
| 1187 | 1195 | |
| 1188 | 1196 | out = {"motions": [], "placed": [], "skipped": [], "missing": []} |
| 1189 | 1197 | changed = False |
| 1198 | + # les transitions posées : l'A-roll s'arrête sous chacune (une pause), une fois tout posé | |
| 1199 | + pauses: list[tuple[str, dict]] = [] | |
| 1190 | 1200 | for b in board["beats"]: |
| 1191 | 1201 | if not b.get("on") or (only is not None and b["id"] not in only) or b["id"] in written: |
| 1192 | 1202 | continue |
| @@ -1237,6 +1247,8 @@ def compose(pid: str, only: list[str] | None = None) -> dict: | ||
| 1237 | 1247 | "kind": "image" if kind == "broll" else "transition" if kind == "transition" |
| 1238 | 1248 | else b["kind"], "idea": b["show"], **_ends(b), "beat": b["id"]}) |
| 1239 | 1249 | out["motions"].append(mid) |
| 1250 | + if kind == "transition": | |
| 1251 | + pauses.append((mid, b)) | |
| 1240 | 1252 | sound = sfx.get(b.get("sfx") or "") |
| 1241 | 1253 | if sound: |
| 1242 | 1254 | timeline["audio"].append({"id": next_id("audio"), "src": sound["src"], |
| @@ -1247,13 +1259,64 @@ def compose(pid: str, only: list[str] | None = None) -> dict: | ||
| 1247 | 1259 | **({"motion": mid} if mid else {})}) |
| 1248 | 1260 | out["placed"].append(b["id"]) |
| 1249 | 1261 | changed = True |
| 1262 | + shifts = _pause_transitions(pid, timeline, pauses) | |
| 1250 | 1263 | if changed: |
| 1251 | 1264 | for track in ("zooms", "broll", "motion", "audio"): |
| 1252 | 1265 | timeline[track].sort(key=lambda x: x["at"]) |
| 1253 | - montage.save(pid, timeline) | |
| 1266 | + montage.save(pid, timeline, shifts=shifts) | |
| 1254 | 1267 | return out |
| 1255 | 1268 | |
| 1256 | 1269 | |
| 1270 | +def _gap_cut(words: list[cues.Word], aroll: list[dict], at: float, anchor: str) -> float: | |
| 1271 | + """ | |
| 1272 | + Où couper l'A-roll pour la transition posée à `at` : dans le silence juste avant le premier mot de la nouvelle | |
| 1273 | + partie (`anchor`, retrouvé près de `at`), sur une coupe du dérush s'il y en a une dans ce silence, sinon en son | |
| 1274 | + milieu. Jamais dans le plus long silence des environs : c'est souvent celui d'avant une chute qui clôt la partie | |
| 1275 | + précédente (« …les compromis qui sont faits. ‖ Et ça, c'est pas bon. »), que la pause couperait de sa phrase. | |
| 1276 | + Sans ancre retrouvée : `at` + TRANSITION_LEAD (là où make_beat a vu le premier mot). | |
| 1277 | + """ | |
| 1278 | + toks, owner = autoderush._token_index(words) | |
| 1279 | + near = next((i for i, o in enumerate(owner) if words[o].start >= at - 1.0), len(toks)) | |
| 1280 | + i = _find(toks, autoderush._tokens(anchor), near - 5, near + 40, near) | |
| 1281 | + k = owner[i] if i is not None and abs(words[owner[i]].start - at - TRANSITION_LEAD) < 3.0 else None | |
| 1282 | + if k is None: | |
| 1283 | + k = next((n for n, w in enumerate(words) if w.start >= at + TRANSITION_LEAD - 0.3), None) | |
| 1284 | + if k is None: | |
| 1285 | + return at | |
| 1286 | + lo = words[k - 1].end if k else 0.0 | |
| 1287 | + hi = words[k].start | |
| 1288 | + cuts, cursor = [], 0.0 | |
| 1289 | + for c in aroll: | |
| 1290 | + cuts.append(cursor) | |
| 1291 | + cursor += c["out"] - c["in"] | |
| 1292 | + inside = [c for c in cuts if lo - 0.02 <= c <= hi + 0.02] | |
| 1293 | + return round(min(inside, key=lambda c: abs(c - (lo + hi) / 2)) if inside else (lo + hi) / 2, 3) | |
| 1294 | + | |
| 1295 | + | |
| 1296 | +def _pause_transitions(pid: str, timeline: dict, pauses: list[tuple[str, dict]]) -> list[dict]: | |
| 1297 | + """ | |
| 1298 | + Les transitions de partie qui viennent d'être posées deviennent des plans de l'A-roll : chacune est calée au milieu | |
| 1299 | + du silence entre les deux parties, et l'A-roll s'arrête sous elle (montage.pause_motion) ; la voix reprend après. | |
| 1300 | + Modifie `timeline` ; rend les décalages à reporter sur les beats du storyboard. | |
| 1301 | + """ | |
| 1302 | + if not pauses or not (src := montage.ensure_pause(pid)): | |
| 1303 | + return [] | |
| 1304 | + words = timeline_words(service.load(pid), timeline) | |
| 1305 | + shifts: list[dict] = [] | |
| 1306 | + # de la dernière à la première : une pause ne décale pas celles d'avant | |
| 1307 | + for mid, b in sorted(pauses, key=lambda p: -p[1]["at"]): | |
| 1308 | + m = next(x for x in timeline["motion"] if x["id"] == mid) | |
| 1309 | + delta = _gap_cut(words, timeline["aroll"], m["at"], b.get("anchor") or "") - m["at"] | |
| 1310 | + m["at"] = round(m["at"] + delta, 3) | |
| 1311 | + for s in timeline["audio"]: | |
| 1312 | + if s.get("motion") == mid: | |
| 1313 | + s["at"] = round(max(0.0, s["at"] + delta), 3) | |
| 1314 | + done = montage.pause_motion(timeline, mid, src) or [] | |
| 1315 | + # le beat de la transition reste où il est ; ceux d'après suivent la pause | |
| 1316 | + shifts += [{**s, "at": max(s["at"], round(b["at"] + 0.01, 3))} for s in done] | |
| 1317 | + return shifts | |
| 1318 | + | |
| 1319 | + | |
| 1257 | 1320 | # au-delà, une image agrandie se voit floue à l'écran |
| 1258 | 1321 | MAX_UPSCALE = 1.15 |
| 1259 | 1322 | |
| @@ -1351,7 +1414,15 @@ def brief(pid: str, ids: list[str]) -> str: | ||
| 1351 | 1414 | lines = [f"### {m['id']} · {m['at']:.2f} → {m['at'] + m['dur']:.2f} s ({m['dur']:.1f} s)", |
| 1352 | 1415 | f"- Il dit : « {b['says']} »", |
| 1353 | 1416 | f"- On montre : {b['show']}"] |
| 1354 | - if kind == "transition": | |
| 1417 | + if kind == "transition" and any(c.get("pause") == m["id"] for c in timeline["aroll"]): | |
| 1418 | + lines.append("- Transition plein écran entre deux parties de la vidéo, en PAUSE : la voix s'est tue et " | |
| 1419 | + "l'A-roll est arrêté dessous (image noire, silence) tout le temps du motion, qui est un plan à " | |
| 1420 | + "part entière. Couvre tout le cadre de la première à la dernière image (pas de marge, le fond " | |
| 1421 | + "opaque est là dès le début : sinon on voit du noir) ; ce qu'elle affiche se lit d'un coup " | |
| 1422 | + "d'œil et a le temps de respirer : le spectateur marque un temps avant la partie suivante. " | |
| 1423 | + "Le titre est en place et lisible tôt (avant le premier tiers) puis reste fixe au moins " | |
| 1424 | + "1,5 s avant de sortir : pas d'animation qui court jusqu'à la dernière image.") | |
| 1425 | + elif kind == "transition": | |
| 1355 | 1426 | lines.append(f"- Transition plein écran entre deux parties de la vidéo : l'écran est entièrement couvert " |
| 1356 | 1427 | f"au milieu (vers {m['dur'] / 2:.2f} s), entrée et sortie rapides et franches ; ce qu'elle " |
| 1357 | 1428 | "affiche se lit d'un coup d'œil. Ici pas de marge : elle couvre tout le cadre.") |
| @@ -79,8 +79,9 @@ ZOOM_SLOW_DEFAULT = 1.1 | |||
| 79 | ZOOM_DUR = (1.5, 8.0) | 79 | ZOOM_DUR = (1.5, 8.0) |
| 80 | ZOOM_SLOW_DUR = (4.0, 15.0) | 80 | ZOOM_SLOW_DUR = (4.0, 15.0) |
| 81 | ZOOM_LEAD = 0.1 | 81 | ZOOM_LEAD = 0.1 |
| 82 | -# transition : courte, l'écran couvert au milieu, juste avant les premiers mots de la nouvelle partie | 82 | +# transition : juste avant les premiers mots de la nouvelle partie ; posée en pause (la voix s'arrête dessous), elle |
| 83 | -TRANSITION_DUR = 1.2 | 83 | +# dure assez pour lire son titre et voir son animation (à 1,2 s, on ne lit rien) |
| 84 | +TRANSITION_DUR = 3.0 | ||
| 84 | TRANSITION_LEAD = 0.9 | 85 | TRANSITION_LEAD = 0.9 |
| 85 | # un beat posé à la main : sa durée par défaut, et sa durée minimale | 86 | # un beat posé à la main : sa durée par défaut, et sa durée minimale |
| 86 | HAND_DUR = 3.0 | 87 | HAND_DUR = 3.0 |
| @@ -316,9 +317,16 @@ une image est mise en scène, en plein écran (`place` full) ou sur un côté co | |||
| 316 | `right`) : dans `motion`, des gestes nets calés sur ses mots (ce qui s'isole, se surligne, se détache, \ | 317 | `right`) : dans `motion`, des gestes nets calés sur ses mots (ce qui s'isole, se surligne, se détache, \ |
| 317 | au mot qui le nomme), pas « fondu + lent zoom ». Sinon `media` vide, `search` = la recherche à faire pour le trouver (mots-clés concrets, en \ | 318 | au mot qui le nomme), pas « fondu + lent zoom ». Sinon `media` vide, `search` = la recherche à faire pour le trouver (mots-clés concrets, en \ |
| 318 | {lang}) et `show` = le plan idéal ; l'auteur le trouvera ou le tournera. | 319 | {lang}) et `show` = le plan idéal ; l'auteur le trouvera ou le tournera. |
| 319 | -- transition : un enchaînement animé plein écran, court, au changement de partie ou de chapitre, \ | 320 | +- transition : un carton animé plein écran au changement de partie ou de chapitre, une par \ |
| 320 | -une par changement au plus. `anchor` = les premiers mots de la nouvelle partie ; `show` = ce qu'elle \ | 321 | +changement au plus. La vidéo s'arrête dessous (image noire, voix coupée) environ 3 secondes, le temps \ |
| 321 | -affiche (le titre de la partie, en quelques mots) ; `until` = les mêmes mots que `anchor`. | 322 | +de lire le titre, puis la voix reprend sur `anchor`. `anchor` = les premiers mots de la PREMIÈRE \ |
| 323 | +PHRASE de la nouvelle partie, au début de cette phrase ; `show` = ce qu'elle affiche (le titre de la \ | ||
| 324 | +partie, en quelques mots) ; `until` = les mêmes mots que `anchor`. Ce qui précède l'ancre doit être \ | ||
| 325 | +une pensée finie : la phrase d'avant se termine (point, point d'interrogation) et clôt la partie \ | ||
| 326 | +précédente. Lis les deux ou trois phrases autour : une chute, un commentaire ou une conclusion \ | ||
| 327 | +courte qui suit (« Et ça, c'est pas bon. », « Voilà. », « Bref, ça marchait. ») appartient encore à \ | ||
| 328 | +la partie précédente, la transition vient APRÈS elle. Jamais au milieu d'une phrase ni entre une \ | ||
| 329 | +question et sa réponse, une idée et son explication ; sans frontière nette, pas de transition. | ||
| 322 | `kind`, `anim` et `motion` ne servent qu'aux motions (et `place` aux motions et aux images) ; `sfx` \ | 330 | `kind`, `anim` et `motion` ne servent qu'aux motions (et `place` aux motions et aux images) ; `sfx` \ |
| 323 | aux motions, B-roll et transitions. | 331 | aux motions, B-roll et transitions. |
| 324 | 332 | ||
| @@ -1187,6 +1195,8 @@ def compose(pid: str, only: list[str] | None = None) -> dict: | |||
| 1187 | 1195 | ||
| 1188 | out = {"motions": [], "placed": [], "skipped": [], "missing": []} | 1196 | out = {"motions": [], "placed": [], "skipped": [], "missing": []} |
| 1189 | changed = False | 1197 | changed = False |
| 1198 | + # les transitions posées : l'A-roll s'arrête sous chacune (une pause), une fois tout posé | ||
| 1199 | + pauses: list[tuple[str, dict]] = [] | ||
| 1190 | for b in board["beats"]: | 1200 | for b in board["beats"]: |
| 1191 | if not b.get("on") or (only is not None and b["id"] not in only) or b["id"] in written: | 1201 | if not b.get("on") or (only is not None and b["id"] not in only) or b["id"] in written: |
| 1192 | continue | 1202 | continue |
| @@ -1237,6 +1247,8 @@ def compose(pid: str, only: list[str] | None = None) -> dict: | |||
| 1237 | "kind": "image" if kind == "broll" else "transition" if kind == "transition" | 1247 | "kind": "image" if kind == "broll" else "transition" if kind == "transition" |
| 1238 | else b["kind"], "idea": b["show"], **_ends(b), "beat": b["id"]}) | 1248 | else b["kind"], "idea": b["show"], **_ends(b), "beat": b["id"]}) |
| 1239 | out["motions"].append(mid) | 1249 | out["motions"].append(mid) |
| 1250 | + if kind == "transition": | ||
| 1251 | + pauses.append((mid, b)) | ||
| 1240 | sound = sfx.get(b.get("sfx") or "") | 1252 | sound = sfx.get(b.get("sfx") or "") |
| 1241 | if sound: | 1253 | if sound: |
| 1242 | timeline["audio"].append({"id": next_id("audio"), "src": sound["src"], | 1254 | timeline["audio"].append({"id": next_id("audio"), "src": sound["src"], |
| @@ -1247,13 +1259,64 @@ def compose(pid: str, only: list[str] | None = None) -> dict: | |||
| 1247 | **({"motion": mid} if mid else {})}) | 1259 | **({"motion": mid} if mid else {})}) |
| 1248 | out["placed"].append(b["id"]) | 1260 | out["placed"].append(b["id"]) |
| 1249 | changed = True | 1261 | changed = True |
| 1262 | + shifts = _pause_transitions(pid, timeline, pauses) | ||
| 1250 | if changed: | 1263 | if changed: |
| 1251 | for track in ("zooms", "broll", "motion", "audio"): | 1264 | for track in ("zooms", "broll", "motion", "audio"): |
| 1252 | timeline[track].sort(key=lambda x: x["at"]) | 1265 | timeline[track].sort(key=lambda x: x["at"]) |
| 1253 | - montage.save(pid, timeline) | 1266 | + montage.save(pid, timeline, shifts=shifts) |
| 1254 | return out | 1267 | return out |
| 1255 | 1268 | ||
| 1256 | 1269 | ||
| 1270 | +def _gap_cut(words: list[cues.Word], aroll: list[dict], at: float, anchor: str) -> float: | ||
| 1271 | + """ | ||
| 1272 | + Où couper l'A-roll pour la transition posée à `at` : dans le silence juste avant le premier mot de la nouvelle | ||
| 1273 | + partie (`anchor`, retrouvé près de `at`), sur une coupe du dérush s'il y en a une dans ce silence, sinon en son | ||
| 1274 | + milieu. Jamais dans le plus long silence des environs : c'est souvent celui d'avant une chute qui clôt la partie | ||
| 1275 | + précédente (« …les compromis qui sont faits. ‖ Et ça, c'est pas bon. »), que la pause couperait de sa phrase. | ||
| 1276 | + Sans ancre retrouvée : `at` + TRANSITION_LEAD (là où make_beat a vu le premier mot). | ||
| 1277 | + """ | ||
| 1278 | + toks, owner = autoderush._token_index(words) | ||
| 1279 | + near = next((i for i, o in enumerate(owner) if words[o].start >= at - 1.0), len(toks)) | ||
| 1280 | + i = _find(toks, autoderush._tokens(anchor), near - 5, near + 40, near) | ||
| 1281 | + k = owner[i] if i is not None and abs(words[owner[i]].start - at - TRANSITION_LEAD) < 3.0 else None | ||
| 1282 | + if k is None: | ||
| 1283 | + k = next((n for n, w in enumerate(words) if w.start >= at + TRANSITION_LEAD - 0.3), None) | ||
| 1284 | + if k is None: | ||
| 1285 | + return at | ||
| 1286 | + lo = words[k - 1].end if k else 0.0 | ||
| 1287 | + hi = words[k].start | ||
| 1288 | + cuts, cursor = [], 0.0 | ||
| 1289 | + for c in aroll: | ||
| 1290 | + cuts.append(cursor) | ||
| 1291 | + cursor += c["out"] - c["in"] | ||
| 1292 | + inside = [c for c in cuts if lo - 0.02 <= c <= hi + 0.02] | ||
| 1293 | + return round(min(inside, key=lambda c: abs(c - (lo + hi) / 2)) if inside else (lo + hi) / 2, 3) | ||
| 1294 | + | ||
| 1295 | + | ||
| 1296 | +def _pause_transitions(pid: str, timeline: dict, pauses: list[tuple[str, dict]]) -> list[dict]: | ||
| 1297 | + """ | ||
| 1298 | + Les transitions de partie qui viennent d'être posées deviennent des plans de l'A-roll : chacune est calée au milieu | ||
| 1299 | + du silence entre les deux parties, et l'A-roll s'arrête sous elle (montage.pause_motion) ; la voix reprend après. | ||
| 1300 | + Modifie `timeline` ; rend les décalages à reporter sur les beats du storyboard. | ||
| 1301 | + """ | ||
| 1302 | + if not pauses or not (src := montage.ensure_pause(pid)): | ||
| 1303 | + return [] | ||
| 1304 | + words = timeline_words(service.load(pid), timeline) | ||
| 1305 | + shifts: list[dict] = [] | ||
| 1306 | + # de la dernière à la première : une pause ne décale pas celles d'avant | ||
| 1307 | + for mid, b in sorted(pauses, key=lambda p: -p[1]["at"]): | ||
| 1308 | + m = next(x for x in timeline["motion"] if x["id"] == mid) | ||
| 1309 | + delta = _gap_cut(words, timeline["aroll"], m["at"], b.get("anchor") or "") - m["at"] | ||
| 1310 | + m["at"] = round(m["at"] + delta, 3) | ||
| 1311 | + for s in timeline["audio"]: | ||
| 1312 | + if s.get("motion") == mid: | ||
| 1313 | + s["at"] = round(max(0.0, s["at"] + delta), 3) | ||
| 1314 | + done = montage.pause_motion(timeline, mid, src) or [] | ||
| 1315 | + # le beat de la transition reste où il est ; ceux d'après suivent la pause | ||
| 1316 | + shifts += [{**s, "at": max(s["at"], round(b["at"] + 0.01, 3))} for s in done] | ||
| 1317 | + return shifts | ||
| 1318 | + | ||
| 1319 | + | ||
| 1257 | # au-delà, une image agrandie se voit floue à l'écran | 1320 | # au-delà, une image agrandie se voit floue à l'écran |
| 1258 | MAX_UPSCALE = 1.15 | 1321 | MAX_UPSCALE = 1.15 |
| 1259 | 1322 | ||
| @@ -1351,7 +1414,15 @@ def brief(pid: str, ids: list[str]) -> str: | |||
| 1351 | lines = [f"### {m['id']} · {m['at']:.2f} → {m['at'] + m['dur']:.2f} s ({m['dur']:.1f} s)", | 1414 | lines = [f"### {m['id']} · {m['at']:.2f} → {m['at'] + m['dur']:.2f} s ({m['dur']:.1f} s)", |
| 1352 | f"- Il dit : « {b['says']} »", | 1415 | f"- Il dit : « {b['says']} »", |
| 1353 | f"- On montre : {b['show']}"] | 1416 | f"- On montre : {b['show']}"] |
| 1354 | - if kind == "transition": | 1417 | + if kind == "transition" and any(c.get("pause") == m["id"] for c in timeline["aroll"]): |
| 1418 | + lines.append("- Transition plein écran entre deux parties de la vidéo, en PAUSE : la voix s'est tue et " | ||
| 1419 | + "l'A-roll est arrêté dessous (image noire, silence) tout le temps du motion, qui est un plan à " | ||
| 1420 | + "part entière. Couvre tout le cadre de la première à la dernière image (pas de marge, le fond " | ||
| 1421 | + "opaque est là dès le début : sinon on voit du noir) ; ce qu'elle affiche se lit d'un coup " | ||
| 1422 | + "d'œil et a le temps de respirer : le spectateur marque un temps avant la partie suivante. " | ||
| 1423 | + "Le titre est en place et lisible tôt (avant le premier tiers) puis reste fixe au moins " | ||
| 1424 | + "1,5 s avant de sortir : pas d'animation qui court jusqu'à la dernière image.") | ||
| 1425 | + elif kind == "transition": | ||
| 1355 | lines.append(f"- Transition plein écran entre deux parties de la vidéo : l'écran est entièrement couvert " | 1426 | lines.append(f"- Transition plein écran entre deux parties de la vidéo : l'écran est entièrement couvert " |
| 1356 | f"au milieu (vers {m['dur'] / 2:.2f} s), entrée et sortie rapides et franches ; ce qu'elle " | 1427 | f"au milieu (vers {m['dur'] / 2:.2f} s), entrée et sortie rapides et franches ; ce qu'elle " |
| 1357 | "affiche se lit d'un coup d'œil. Ici pas de marge : elle couvre tout le cadre.") | 1428 | "affiche se lit d'un coup d'œil. Ici pas de marge : elle couvre tout le cadre.") |
modified
backend/studio/modules/projects/storyboard_chat.py +6 -2 | @@ -75,8 +75,12 @@ cadre), 2 à 6 s (slow : 5 à 15 s) ; jamais sous un visuel plein écran ; | ||
| 75 | 75 | passe en plein écran ; une image est animée en plein écran ou sur un côté, comme un portrait, selon \ |
| 76 | 76 | `place`) ; si aucun ne convient, `media` vide et `search` = la recherche à faire pour le trouver ; pour une vidéo, \ |
| 77 | 77 | `start` = la seconde du média où le plan commence, s'il désigne un passage précis ; |
| 78 | -- transition : un enchaînement animé plein écran, court, au changement de partie ; `anchor` = les \ | |
| 79 | -premiers mots de la nouvelle partie, `show` = ce qu'elle affiche. | |
| 78 | +- transition : un carton animé plein écran au changement de partie ; la vidéo s'arrête dessous \ | |
| 79 | +(image noire, voix coupée) environ 3 secondes, le temps de lire le titre, puis la voix reprend sur \ | |
| 80 | +`anchor`. `anchor` = les premiers mots de la PREMIÈRE PHRASE de la nouvelle partie, `show` = ce \ | |
| 81 | +qu'elle affiche. Ce qui précède doit être une pensée finie : une chute ou une courte conclusion qui \ | |
| 82 | +suit (« Et ça, c'est pas bon. ») appartient encore à la partie précédente, la transition vient après \ | |
| 83 | +elle ; jamais au milieu d'une phrase, ni entre une question et sa réponse. | |
| 80 | 84 | |
| 81 | 85 | L'auteur pose aussi des beats à la main sur la timeline (marqués « posé par l'auteur ») : un passage \ |
| 82 | 86 | et sa consigne, souvent courte (« une animation ici », « un B-roll »). Quand il te le demande, précise-la \ |
| @@ -75,8 +75,12 @@ cadre), 2 à 6 s (slow : 5 à 15 s) ; jamais sous un visuel plein écran ; | |||
| 75 | passe en plein écran ; une image est animée en plein écran ou sur un côté, comme un portrait, selon \ | 75 | passe en plein écran ; une image est animée en plein écran ou sur un côté, comme un portrait, selon \ |
| 76 | `place`) ; si aucun ne convient, `media` vide et `search` = la recherche à faire pour le trouver ; pour une vidéo, \ | 76 | `place`) ; si aucun ne convient, `media` vide et `search` = la recherche à faire pour le trouver ; pour une vidéo, \ |
| 77 | `start` = la seconde du média où le plan commence, s'il désigne un passage précis ; | 77 | `start` = la seconde du média où le plan commence, s'il désigne un passage précis ; |
| 78 | -- transition : un enchaînement animé plein écran, court, au changement de partie ; `anchor` = les \ | 78 | +- transition : un carton animé plein écran au changement de partie ; la vidéo s'arrête dessous \ |
| 79 | -premiers mots de la nouvelle partie, `show` = ce qu'elle affiche. | 79 | +(image noire, voix coupée) environ 3 secondes, le temps de lire le titre, puis la voix reprend sur \ |
| 80 | +`anchor`. `anchor` = les premiers mots de la PREMIÈRE PHRASE de la nouvelle partie, `show` = ce \ | ||
| 81 | +qu'elle affiche. Ce qui précède doit être une pensée finie : une chute ou une courte conclusion qui \ | ||
| 82 | +suit (« Et ça, c'est pas bon. ») appartient encore à la partie précédente, la transition vient après \ | ||
| 83 | +elle ; jamais au milieu d'une phrase, ni entre une question et sa réponse. | ||
| 80 | 84 | ||
| 81 | L'auteur pose aussi des beats à la main sur la timeline (marqués « posé par l'auteur ») : un passage \ | 85 | L'auteur pose aussi des beats à la main sur la timeline (marqués « posé par l'auteur ») : un passage \ |
| 82 | et sa consigne, souvent courte (« une animation ici », « un B-roll »). Quand il te le demande, précise-la \ | 86 | et sa consigne, souvent courte (« une animation ici », « un B-roll »). Quand il te le demande, précise-la \ |