Tighten the trailer to 1:30 and fix its two audio faults
Cut 3 of LIFETIME MEMBERS: the intro music no longer runs into Reg's first line, Ambrose finishes "...still waiting for a result" (line ends are now measured off each take instead of the QA transcript), every shot is tighter, and the black title cards are set larger. Cut 2 stays on disk beside it as the reference. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d388004eac
commit
5fba837ead
5 files changed
+65
-36
No files matched your search
@@ -69,3 +69,10 @@ still in promo/trailer/clips if a 15-second cutdown ever wants them.
|
||||
- QA listening (`qa.py`, Gemini): trust it on spoken lines and on mix complaints, NOT on its
|
||||
timestamps for music (they drift up to 1.5x) and not on transcripts of non-speech audio
|
||||
(it invents words from footsteps).
|
||||
|
||||
## Cut 2 → cut 3 (Oliver, 2026-10-05) — 1:45 → 1:30, cut 2 kept as the reference
|
||||
- The intro music ran 3 s into Reg's "Fertiliser": it now fades out under "How did it start?".
|
||||
- Ambrose lost the end of "...result": cut 2 trusted the QA transcript's 6.6 s; the take's speech
|
||||
envelope says 7.2. Every line end in the EDL is now measured off the take, not the transcript.
|
||||
- Tighter everywhere (B-roll 2.2 s, cold open 14 s, shorter cards and logo); the black cards'
|
||||
text is bigger (88/96 px Baskerville, balanced wrapping).
|
||||
+37
-28
@@ -23,35 +23,38 @@ game = lambda n: os.path.join(ROOT, 'assets', 'audio', n)
|
||||
# ov: [(card, t_in, t_out)] faded overlays · fi/fo: picture fade from/to black · gain: clip audio dB
|
||||
# mute: drop the clip's own audio · dlg: a spoken line — its gain is MEASURED so every voice in the
|
||||
# film sits at DLG LUFS (eq: lift a voice out of loud ambience first)
|
||||
L3 = (0.7, 4.8)
|
||||
L3 = (0.6, 4.6)
|
||||
DLG = -20.0
|
||||
QUIET = 2.3 # seconds into s_amb where "please" ends and the music is cut
|
||||
CUT = 3 # cut 2 (1:45) is kept beside it as lifetime-members-cut2.mp4, the reference
|
||||
# Line ends below are MEASURED off each take's speech-band envelope, not off the QA transcript
|
||||
# (its timestamps drift): cut 2 lost the end of Ambrose's "...result" by trusting 6.6 s; it is 7.2.
|
||||
EDL = [
|
||||
('gates', 'gates_v1', 0.0, 7.0, dict(fi=1.2, gain=-2, afi=1.0)),
|
||||
('court', 'court_v1', 0.0, 5.0, dict(gain=-4)),
|
||||
('board', 'board_v1', 0.0, 5.5, dict(fo=0.5, gain=-3, afo=0.5)),
|
||||
('trad', None, 0, 5.2, dict(ov=[('t_a', 0.3, 4.8), ('t_b', 2.3, 4.8)])),
|
||||
('title', None, 0, 4.0, dict(ov=[('title', 0.2, 3.5)])),
|
||||
('q1', None, 0, 2.3, dict(ov=[('q_start', 0.15, 2.0)])),
|
||||
('reg', 'reg_v1.keep', 0.0, 7.3, dict(ov=[('lt_reg', *L3)], dlg=True)),
|
||||
('tin', 'tin_v1', 0.0, 3.0, dict(gain=-6)),
|
||||
('doris', 'doris_v1', 0.0, 8.0, dict(ov=[('lt_doris', *L3)], dlg=True)),
|
||||
('cream', 'cream_v1', 0.0, 3.2, dict(gain=-6)),
|
||||
('clive', 'clive_v1', 0.0, 6.3, dict(ov=[('lt_clive', *L3)], dlg=True)),
|
||||
('bylaws', 'bylaws_v1', 0.0, 3.0, dict(gain=-6)),
|
||||
('ambrose', 'ambrose_v1', 0.0, 7.0, dict(ov=[('lt_ambrose', *L3)], dlg=True)),
|
||||
('archive', 'archive_v1', 0.0, 5.0, dict(fo=0.5, gain=-4, afo=0.5)),
|
||||
('q2', None, 0, 2.3, dict(ov=[('q_chall', 0.15, 2.0)])),
|
||||
('s_doris', 'stare_doris_v1', 0.6, 3.4, dict(dlg=True)),
|
||||
('s_clive', 'stare_clive_v2', 0.8, 3.2, dict(gain=-6)),
|
||||
('gates', 'gates_v1', 0.0, 5.6, dict(fi=1.0, gain=-2, afi=0.8)),
|
||||
('court', 'court_v1', 0.0, 4.0, dict(gain=-4)),
|
||||
('board', 'board_v1', 0.0, 4.6, dict(fo=0.5, gain=-3, afo=0.5)),
|
||||
('trad', None, 0, 4.4, dict(ov=[('t_a', 0.25, 4.0), ('t_b', 1.8, 4.0)])),
|
||||
('title', None, 0, 3.4, dict(ov=[('title', 0.15, 3.0)])),
|
||||
('q1', None, 0, 1.9, dict(ov=[('q_start', 0.1, 1.7)])),
|
||||
('reg', 'reg_v1.keep', 0.0, 7.1, dict(ov=[('lt_reg', *L3)], dlg=True)),
|
||||
('tin', 'tin_v1', 0.0, 2.2, dict(gain=-6)),
|
||||
('doris', 'doris_v1', 0.0, 7.6, dict(ov=[('lt_doris', *L3)], dlg=True)),
|
||||
('cream', 'cream_v1', 0.4, 2.2, dict(gain=-6)),
|
||||
('clive', 'clive_v1', 0.0, 6.1, dict(ov=[('lt_clive', *L3)], dlg=True)),
|
||||
('bylaws', 'bylaws_v1', 0.0, 2.2, dict(gain=-6)),
|
||||
('ambrose', 'ambrose_v1', 0.0, 7.9, dict(ov=[('lt_ambrose', *L3)], dlg=True, afo=0.5)),
|
||||
('archive', 'archive_v1', 0.0, 3.8, dict(fo=0.5, gain=-4, afo=0.5)),
|
||||
('q2', None, 0, 1.9, dict(ov=[('q_chall', 0.1, 1.7)])),
|
||||
('s_doris', 'stare_doris_v1', 0.7, 2.7, dict(dlg=True)),
|
||||
('s_clive', 'stare_clive_v2', 1.0, 2.8, dict(gain=-6)),
|
||||
# "Quiet, please." — and the score obeys: it stops dead on the end of "please" (see MUSIC)
|
||||
('s_amb', 'stare_ambrose_v1', 0.0, 3.2, dict(dlg=True, afo=0.9)),
|
||||
('stands', 'stands_close_v2', 0.9, 3.5, dict(gain=-17)), # the heads creak, barely
|
||||
('play', 'ambrose_night_v2', 0.0, 2.0, dict(dlg=True)),
|
||||
('splat', None, 0, 1.6, dict()),
|
||||
('logo', None, 0, 4.6, dict(ov=[('logo', 0.25, 4.6)], fo=0.6)),
|
||||
('stinger', 'reg_dawn_v1', 0.0, 5.4, dict(fi=0.4, afi=0.35, dlg=(0.0, 2.6), lift=2, eq=True)),
|
||||
('end', None, 0, 4.2, dict(ov=[('end', 0.5, 4.2)], fo=1.0)),
|
||||
('s_amb', 'stare_ambrose_v1', 0.0, 3.0, dict(dlg=True, afo=0.8)),
|
||||
('stands', 'stands_close_v2', 0.9, 3.0, dict(gain=-17)), # the heads creak, barely
|
||||
('play', 'ambrose_night_v2', 0.0, 1.9, dict(dlg=True)),
|
||||
('splat', None, 0, 1.2, dict()),
|
||||
('logo', None, 0, 3.8, dict(ov=[('logo', 0.2, 3.8)], fo=0.5)),
|
||||
('stinger', 'reg_dawn_v1', 0.6, 3.6, dict(fi=0.4, afi=0.35, dlg=(1.0, 2.7), lift=2, eq=True)),
|
||||
('end', None, 0, 3.6, dict(ov=[('end', 0.4, 3.6)], fo=0.9)),
|
||||
]
|
||||
|
||||
# every segment is a whole number of frames, or the audio drifts a sub-frame per cut
|
||||
@@ -63,9 +66,15 @@ for name, _, _, dur, _ in EDL:
|
||||
TOTAL = t
|
||||
|
||||
# music/effects: (file, src_in, dur, at, gain dB, fade_in, fade_out)
|
||||
# m_open turns a corner at 21.0 s (segment.py's join): that turn lands on the title card, and the
|
||||
# whole cue has faded out under the question card, before Reg opens his mouth (cut 2 ran it 3 s into
|
||||
# his line — Oliver: "the music from the intro is overlapping the voice")
|
||||
OPEN_TURN = 21.0
|
||||
o_in = max(0.0, OPEN_TURN - start['title'])
|
||||
o_at = max(0.0, start['title'] - OPEN_TURN)
|
||||
MUSIC = [
|
||||
(aud('m_open.mp3'), 0.0, 30.5, start['gates'] + 1.5, -11, 2.0, 2.5),
|
||||
(aud('cue_a.mp3'), 35.0, 38.6, start['reg'], -20, 1.5, 2.0),
|
||||
(aud('m_open.mp3'), o_in, start['q1'] + 1.2 - o_at, o_at, -11, 2.0, 3.4), # gone mid-question
|
||||
(aud('cue_a.mp3'), 35.0, start['q2'] - start['reg'], start['reg'], -20, 1.5, 2.0),
|
||||
# the drone sits low under Doris, then swells through Clive's eyes so the shush has something to stop
|
||||
(aud('cue_b.mp3'), 0.0, start['s_amb'] + QUIET - start['q2'], start['q2'], -17, 1.0, 0.01,
|
||||
f"'if(lt(t,{start['s_clive'] - start['q2']:.2f}),1,min(2.4,1+(t-{start['s_clive'] - start['q2']:.2f})*0.6))':eval=frame"),
|
||||
@@ -181,7 +190,7 @@ def mix(parts):
|
||||
# a touch of contrast, a little less saturation, a soft vignette, and live 35mm-ish grain
|
||||
f.append('[0:v]eq=contrast=1.04:saturation=0.9:gamma=0.98,vignette=angle=PI/5.5,'
|
||||
'noise=c0s=5:c0f=t+u,format=yuv420p[v]')
|
||||
out = os.path.join(T, 'lifetime-members.mp4')
|
||||
out = os.path.join(T, f'lifetime-members-cut{CUT}.mp4')
|
||||
cmd += ['-filter_complex', ';'.join(f), '-map', '[v]', '-map', '[a]',
|
||||
# grain is the costliest thing to encode: tune for it and cap the rate (~14 Mb/s keeps it
|
||||
# postable everywhere; uncapped crf 16 came out at 36 Mb/s, 480 MB for 2 minutes)
|
||||
|
||||
@@ -13,8 +13,8 @@ section:target { display: block; }
|
||||
.c { position: absolute; left: 0; right: 0; text-align: center; }
|
||||
|
||||
/* black cards: one line of quiet Baskerville, the documentary's voice */
|
||||
.say { font: italic 400 62px/1.25 Baskerville, serif; letter-spacing: .005em; padding: 0 110px; }
|
||||
.q { font: italic 400 66px/1.2 Baskerville, serif; }
|
||||
.say { font: italic 400 88px/1.18 Baskerville, serif; letter-spacing: .005em; padding: 0 80px; text-wrap: balance; }
|
||||
.q { font: italic 400 96px/1.15 Baskerville, serif; padding: 0 80px; text-wrap: balance; }
|
||||
.q::before, .q::after { content: ''; display: block; width: 46px; height: 2px; background: var(--gold);
|
||||
margin: 0 auto 34px; opacity: .8; }
|
||||
.q::after { margin: 34px auto 0; }
|
||||
@@ -39,8 +39,8 @@ section:target { display: block; }
|
||||
.px { font: 400 34px/1.5 Silkscreen, monospace; letter-spacing: .18em; margin-left: .18em; }
|
||||
</style></head><body>
|
||||
|
||||
<section id="t_a"><div class="c say" style="top:820px">Every old club has its traditions.</div></section>
|
||||
<section id="t_b"><div class="c say" style="top:960px">Some of them refuse to die.</div></section>
|
||||
<section id="t_a"><div class="c say" style="top:700px">Every old club has its traditions.</div></section>
|
||||
<section id="t_b"><div class="c say" style="top:1010px">Some of them refuse to die.</div></section>
|
||||
|
||||
<section id="title">
|
||||
<svg class="crest" width="150" height="190" viewBox="0 0 150 190" fill="none" stroke="#c9a45a" stroke-width="3.2">
|
||||
@@ -57,8 +57,8 @@ section:target { display: block; }
|
||||
<div class="c est">EST. 1877</div>
|
||||
</section>
|
||||
|
||||
<section id="q_start"><div class="c q" style="top:880px">How did it start?</div></section>
|
||||
<section id="q_chall"><div class="c q" style="top:880px">And the challengers?</div></section>
|
||||
<section id="q_start"><div class="c q" style="top:850px">How did it start?</div></section>
|
||||
<section id="q_chall"><div class="c q" style="top:790px">And the challengers?</div></section>
|
||||
|
||||
<section id="lt_reg"><div class="lt"><i></i><b>Reginald Moss</b><span>Head Groundskeeper</span><em>1962 –</em></div></section>
|
||||
<section id="lt_doris"><div class="lt"><i></i><b>Doris Penny</b><span>Strawberries & Cream</span><em>1971 –</em></div></section>
|
||||
@@ -72,6 +72,6 @@ section:target { display: block; }
|
||||
|
||||
<section id="end">
|
||||
<img class="logo" src="../../assets/logo-v2.png" style="width:700px; top:700px">
|
||||
<div class="c say" style="top:1170px; font-size:50px">Lifetime membership now open.</div>
|
||||
<div class="c say" style="top:1170px; font-size:62px">Lifetime membership now open.</div>
|
||||
</section>
|
||||
</body></html>
|
||||
@@ -3,6 +3,7 @@
|
||||
the soundtrack (an audio model hears it so the cut does not rest on guesswork).
|
||||
|
||||
uv run --with requests python tools/trailer/qa.py <clip.mp4> [frames=6]
|
||||
uv run --with requests python tools/trailer/qa.py --ask <media> "<question>" # any file, any question
|
||||
"""
|
||||
import base64, json, os, subprocess, sys, tempfile
|
||||
|
||||
@@ -11,6 +12,18 @@ import requests
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from gen import API, H, OUT
|
||||
|
||||
def ask(media, question, model=os.environ.get('QA_LISTENER', 'google/gemini-2.5-pro')):
|
||||
wav = tempfile.mktemp(suffix='.wav')
|
||||
subprocess.run(['ffmpeg', '-v', 'error', '-y', '-i', media, '-ac', '1', '-ar', '16000', wav], check=True)
|
||||
r = requests.post(f'{API}/chat/completions', headers=H(), json={'model': model, 'messages': [{'role': 'user', 'content': [
|
||||
{'type': 'text', 'text': question},
|
||||
{'type': 'input_audio', 'input_audio': {'data': base64.b64encode(open(wav, 'rb').read()).decode(), 'format': 'wav'}}]}]},
|
||||
timeout=600)
|
||||
return r.json()['choices'][0]['message']['content'] if r.ok else r.text[:500]
|
||||
|
||||
|
||||
if sys.argv[1] == '--ask':
|
||||
sys.exit(print(ask(sys.argv[2], sys.argv[3])))
|
||||
clip = sys.argv[1]
|
||||
n = int(sys.argv[2]) if len(sys.argv) > 2 else 6
|
||||
name = os.path.splitext(os.path.basename(clip))[0]
|
||||
|
||||
Reference in new issue
Block a user