feat(music-bed): give each a-roll pause its own short cue
24cadf2 parent: ef15d3a modified
.claude/skills/music-bed/SKILL.md +7 -1 | @@ -33,12 +33,18 @@ the voice loudness (≈ −24.6 LUFS on this channel), pauses ≥ 0.9 s, inserte | ||
| 33 | 33 | titles, and a Whisper transcript **in timeline time** (`contexte.md` can be stale: don't trust its times). |
| 34 | 34 | |
| 35 | 35 | Cut the video into sequences: cold open, intro, each part (part titles are 1.2 s `transition` motions), |
| 36 | -conclusion. Inside a part, split again where the tone changes or where an inserted clip interrupts. | |
| 36 | +conclusion; each A-roll pause is a sequence of its own. Inside a part, split again where the tone changes or where an inserted clip interrupts. | |
| 37 | 37 | |
| 38 | 38 | ### 2. Decide what plays where |
| 39 | 39 | |
| 40 | 40 | - **No music under inserted clips** that carry their own sound (politician quotes, TV excerpts, memes). |
| 41 | 41 | The user likes it. Stop the bed right on the clip's first frame (0.3 s fade) and restart after. |
| 42 | +- **A-roll pauses get a short cue of their own.** A pause (A-roll clip with `pause`, `montage/pause.mp4`) | |
| 43 | + is a full-screen motion — usually a part title — during which the voice stops on purpose so the viewer | |
| 44 | + breathes. Don't leave it silent and don't run the talking bed through it: end the previous bed on the | |
| 45 | + pause's first frame (0.3 s fade), lay a short cue (a sting or a few bars, cut to the pause length + a tail | |
| 46 | + of ≤ 0.5 s) at the bed's level or a few dB above (no voice to duck under), then start the next part's | |
| 47 | + bed after it. The cue is the hinge between two parts: it closes one mood and opens the next. | |
| 42 | 48 | - **Every spoken passage gets music to its end.** A 15 s stretch of voice between two archives without |
| 43 | 49 | music feels like a hole; a short teaser before a part title can get its own dedicated track. |
| 44 | 50 | - **Silence is a choice:** keep it for a punchline right before an archive, not for whole passages. |
| @@ -33,12 +33,18 @@ the voice loudness (≈ −24.6 LUFS on this channel), pauses ≥ 0.9 s, inserte | |||
| 33 | titles, and a Whisper transcript **in timeline time** (`contexte.md` can be stale: don't trust its times). | 33 | titles, and a Whisper transcript **in timeline time** (`contexte.md` can be stale: don't trust its times). |
| 34 | 34 | ||
| 35 | Cut the video into sequences: cold open, intro, each part (part titles are 1.2 s `transition` motions), | 35 | Cut the video into sequences: cold open, intro, each part (part titles are 1.2 s `transition` motions), |
| 36 | -conclusion. Inside a part, split again where the tone changes or where an inserted clip interrupts. | 36 | +conclusion; each A-roll pause is a sequence of its own. Inside a part, split again where the tone changes or where an inserted clip interrupts. |
| 37 | 37 | ||
| 38 | ### 2. Decide what plays where | 38 | ### 2. Decide what plays where |
| 39 | 39 | ||
| 40 | - **No music under inserted clips** that carry their own sound (politician quotes, TV excerpts, memes). | 40 | - **No music under inserted clips** that carry their own sound (politician quotes, TV excerpts, memes). |
| 41 | The user likes it. Stop the bed right on the clip's first frame (0.3 s fade) and restart after. | 41 | The user likes it. Stop the bed right on the clip's first frame (0.3 s fade) and restart after. |
| 42 | +- **A-roll pauses get a short cue of their own.** A pause (A-roll clip with `pause`, `montage/pause.mp4`) | ||
| 43 | + is a full-screen motion — usually a part title — during which the voice stops on purpose so the viewer | ||
| 44 | + breathes. Don't leave it silent and don't run the talking bed through it: end the previous bed on the | ||
| 45 | + pause's first frame (0.3 s fade), lay a short cue (a sting or a few bars, cut to the pause length + a tail | ||
| 46 | + of ≤ 0.5 s) at the bed's level or a few dB above (no voice to duck under), then start the next part's | ||
| 47 | + bed after it. The cue is the hinge between two parts: it closes one mood and opens the next. | ||
| 42 | - **Every spoken passage gets music to its end.** A 15 s stretch of voice between two archives without | 48 | - **Every spoken passage gets music to its end.** A 15 s stretch of voice between two archives without |
| 43 | music feels like a hole; a short teaser before a part title can get its own dedicated track. | 49 | music feels like a hole; a short teaser before a part title can get its own dedicated track. |
| 44 | - **Silence is a choice:** keep it for a punchline right before an archive, not for whole passages. | 50 | - **Silence is a choice:** keep it for a punchline right before an archive, not for whole passages. |
modified
.claude/skills/music-bed/scripts/common.py +14 -0 | @@ -47,6 +47,20 @@ def inserted_clips(tl: dict) -> list[dict]: | ||
| 47 | 47 | return out |
| 48 | 48 | |
| 49 | 49 | |
| 50 | +def pause_clips(tl: dict) -> list[dict]: | |
| 51 | + """A-roll pauses: the voice stops (black, silence) under a full-screen motion (`pause` = its id), usually a | |
| 52 | + part title. Not an inserted clip: each one gets its own short musical cue.""" | |
| 53 | + out, t = [], 0.0 | |
| 54 | + titles = {m["id"]: m["title"] for m in tl["motion"]} | |
| 55 | + for c in tl["aroll"]: | |
| 56 | + d = c["out"] - c["in"] | |
| 57 | + if c.get("pause"): | |
| 58 | + out.append({"id": c["id"], "motion": c["pause"], "title": titles.get(c["pause"], ""), | |
| 59 | + "start": round(t, 3), "end": round(t + d, 3)}) | |
| 60 | + t += d | |
| 61 | + return out | |
| 62 | + | |
| 63 | + | |
| 50 | 64 | def _ctx() -> ssl.SSLContext: |
| 51 | 65 | ctx = ssl.create_default_context() |
| 52 | 66 | ctx.check_hostname = False |
| @@ -47,6 +47,20 @@ def inserted_clips(tl: dict) -> list[dict]: | |||
| 47 | return out | 47 | return out |
| 48 | 48 | ||
| 49 | 49 | ||
| 50 | +def pause_clips(tl: dict) -> list[dict]: | ||
| 51 | + """A-roll pauses: the voice stops (black, silence) under a full-screen motion (`pause` = its id), usually a | ||
| 52 | + part title. Not an inserted clip: each one gets its own short musical cue.""" | ||
| 53 | + out, t = [], 0.0 | ||
| 54 | + titles = {m["id"]: m["title"] for m in tl["motion"]} | ||
| 55 | + for c in tl["aroll"]: | ||
| 56 | + d = c["out"] - c["in"] | ||
| 57 | + if c.get("pause"): | ||
| 58 | + out.append({"id": c["id"], "motion": c["pause"], "title": titles.get(c["pause"], ""), | ||
| 59 | + "start": round(t, 3), "end": round(t + d, 3)}) | ||
| 60 | + t += d | ||
| 61 | + return out | ||
| 62 | + | ||
| 63 | + | ||
| 50 | def _ctx() -> ssl.SSLContext: | 64 | def _ctx() -> ssl.SSLContext: |
| 51 | ctx = ssl.create_default_context() | 65 | ctx = ssl.create_default_context() |
| 52 | ctx.check_hostname = False | 66 | ctx.check_hostname = False |
modified
.claude/skills/music-bed/scripts/voice.py +4 -1 | @@ -8,7 +8,7 @@ import re | ||
| 8 | 8 | import subprocess |
| 9 | 9 | import sys |
| 10 | 10 | |
| 11 | -from common import inserted_clips, lufs, project_dir, timeline, total, work_dir | |
| 11 | +from common import inserted_clips, lufs, pause_clips, project_dir, timeline, total, work_dir | |
| 12 | 12 | |
| 13 | 13 | |
| 14 | 14 | def mix_aroll(pid: str, tl: dict, dest) -> None: |
| @@ -59,6 +59,9 @@ def main() -> None: | ||
| 59 | 59 | print("inserted clips (no music under them):") |
| 60 | 60 | for c in inserted_clips(tl): |
| 61 | 61 | print(f" {c['start']:8.2f} -> {c['end']:8.2f} {c['src']}") |
| 62 | + print("A-roll pauses under a full-screen motion (a short cue each):") | |
| 63 | + for c in pause_clips(tl): | |
| 64 | + print(f" {c['start']:8.2f} -> {c['end']:8.2f} {c['motion']} {c['title'][:60]}") | |
| 62 | 65 | print("part titles (short motions):") |
| 63 | 66 | for m in tl["motion"]: |
| 64 | 67 | if m["kind"] == "transition" or m["dur"] <= 1.5: |
| @@ -8,7 +8,7 @@ import re | |||
| 8 | import subprocess | 8 | import subprocess |
| 9 | import sys | 9 | import sys |
| 10 | 10 | ||
| 11 | -from common import inserted_clips, lufs, project_dir, timeline, total, work_dir | 11 | +from common import inserted_clips, lufs, pause_clips, project_dir, timeline, total, work_dir |
| 12 | 12 | ||
| 13 | 13 | ||
| 14 | def mix_aroll(pid: str, tl: dict, dest) -> None: | 14 | def mix_aroll(pid: str, tl: dict, dest) -> None: |
| @@ -59,6 +59,9 @@ def main() -> None: | |||
| 59 | print("inserted clips (no music under them):") | 59 | print("inserted clips (no music under them):") |
| 60 | for c in inserted_clips(tl): | 60 | for c in inserted_clips(tl): |
| 61 | print(f" {c['start']:8.2f} -> {c['end']:8.2f} {c['src']}") | 61 | print(f" {c['start']:8.2f} -> {c['end']:8.2f} {c['src']}") |
| 62 | + print("A-roll pauses under a full-screen motion (a short cue each):") | ||
| 63 | + for c in pause_clips(tl): | ||
| 64 | + print(f" {c['start']:8.2f} -> {c['end']:8.2f} {c['motion']} {c['title'][:60]}") | ||
| 62 | print("part titles (short motions):") | 65 | print("part titles (short motions):") |
| 63 | for m in tl["motion"]: | 66 | for m in tl["motion"]: |
| 64 | if m["kind"] == "transition" or m["dur"] <= 1.5: | 67 | if m["kind"] == "transition" or m["dur"] <= 1.5: |