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:
wuwiandClaude Opus 5.5 committed 2026-10-05 10:56:50 +02:00
1 parent d388004eac
commit 5fba837ead
5 files changed
+65 -36

No files matched your search

+1 -1
View File
@@ -1140,6 +1140,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
</div> </div>
</div> </div>
<span id="recLamp" hidden>REC</span> <span id="recLamp" hidden>REC</span>
<script type="module" src="src/main.js?v=356"></script> <script type="module" src="src/main.js?v=357"></script>
</body> </body>
</html> </html>
+7
View File
@@ -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 - 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 timestamps for music (they drift up to 1.5x) and not on transcripts of non-speech audio
(it invents words from footsteps). (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
View File
@@ -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 # 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 # 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) # 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 DLG = -20.0
QUIET = 2.3 # seconds into s_amb where "please" ends and the music is cut 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 = [ EDL = [
('gates', 'gates_v1', 0.0, 7.0, dict(fi=1.2, gain=-2, afi=1.0)), ('gates', 'gates_v1', 0.0, 5.6, dict(fi=1.0, gain=-2, afi=0.8)),
('court', 'court_v1', 0.0, 5.0, dict(gain=-4)), ('court', 'court_v1', 0.0, 4.0, dict(gain=-4)),
('board', 'board_v1', 0.0, 5.5, dict(fo=0.5, gain=-3, afo=0.5)), ('board', 'board_v1', 0.0, 4.6, 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)])), ('trad', None, 0, 4.4, dict(ov=[('t_a', 0.25, 4.0), ('t_b', 1.8, 4.0)])),
('title', None, 0, 4.0, dict(ov=[('title', 0.2, 3.5)])), ('title', None, 0, 3.4, dict(ov=[('title', 0.15, 3.0)])),
('q1', None, 0, 2.3, dict(ov=[('q_start', 0.15, 2.0)])), ('q1', None, 0, 1.9, dict(ov=[('q_start', 0.1, 1.7)])),
('reg', 'reg_v1.keep', 0.0, 7.3, dict(ov=[('lt_reg', *L3)], dlg=True)), ('reg', 'reg_v1.keep', 0.0, 7.1, dict(ov=[('lt_reg', *L3)], dlg=True)),
('tin', 'tin_v1', 0.0, 3.0, dict(gain=-6)), ('tin', 'tin_v1', 0.0, 2.2, dict(gain=-6)),
('doris', 'doris_v1', 0.0, 8.0, dict(ov=[('lt_doris', *L3)], dlg=True)), ('doris', 'doris_v1', 0.0, 7.6, dict(ov=[('lt_doris', *L3)], dlg=True)),
('cream', 'cream_v1', 0.0, 3.2, dict(gain=-6)), ('cream', 'cream_v1', 0.4, 2.2, dict(gain=-6)),
('clive', 'clive_v1', 0.0, 6.3, dict(ov=[('lt_clive', *L3)], dlg=True)), ('clive', 'clive_v1', 0.0, 6.1, dict(ov=[('lt_clive', *L3)], dlg=True)),
('bylaws', 'bylaws_v1', 0.0, 3.0, dict(gain=-6)), ('bylaws', 'bylaws_v1', 0.0, 2.2, dict(gain=-6)),
('ambrose', 'ambrose_v1', 0.0, 7.0, dict(ov=[('lt_ambrose', *L3)], dlg=True)), ('ambrose', 'ambrose_v1', 0.0, 7.9, dict(ov=[('lt_ambrose', *L3)], dlg=True, afo=0.5)),
('archive', 'archive_v1', 0.0, 5.0, dict(fo=0.5, gain=-4, afo=0.5)), ('archive', 'archive_v1', 0.0, 3.8, dict(fo=0.5, gain=-4, afo=0.5)),
('q2', None, 0, 2.3, dict(ov=[('q_chall', 0.15, 2.0)])), ('q2', None, 0, 1.9, dict(ov=[('q_chall', 0.1, 1.7)])),
('s_doris', 'stare_doris_v1', 0.6, 3.4, dict(dlg=True)), ('s_doris', 'stare_doris_v1', 0.7, 2.7, dict(dlg=True)),
('s_clive', 'stare_clive_v2', 0.8, 3.2, dict(gain=-6)), ('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) # "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)), ('s_amb', 'stare_ambrose_v1', 0.0, 3.0, dict(dlg=True, afo=0.8)),
('stands', 'stands_close_v2', 0.9, 3.5, dict(gain=-17)), # the heads creak, barely ('stands', 'stands_close_v2', 0.9, 3.0, dict(gain=-17)), # the heads creak, barely
('play', 'ambrose_night_v2', 0.0, 2.0, dict(dlg=True)), ('play', 'ambrose_night_v2', 0.0, 1.9, dict(dlg=True)),
('splat', None, 0, 1.6, dict()), ('splat', None, 0, 1.2, dict()),
('logo', None, 0, 4.6, dict(ov=[('logo', 0.25, 4.6)], fo=0.6)), ('logo', None, 0, 3.8, dict(ov=[('logo', 0.2, 3.8)], fo=0.5)),
('stinger', 'reg_dawn_v1', 0.0, 5.4, dict(fi=0.4, afi=0.35, dlg=(0.0, 2.6), lift=2, eq=True)), ('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, 4.2, dict(ov=[('end', 0.5, 4.2)], fo=1.0)), ('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 # 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 TOTAL = t
# music/effects: (file, src_in, dur, at, gain dB, fade_in, fade_out) # 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 = [ MUSIC = [
(aud('m_open.mp3'), 0.0, 30.5, start['gates'] + 1.5, -11, 2.0, 2.5), (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, 38.6, start['reg'], -20, 1.5, 2.0), (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 # 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, (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"), 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 # 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,' 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]') '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]', 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 # 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) # postable everywhere; uncapped crf 16 came out at 36 Mb/s, 480 MB for 2 minutes)
+7 -7
View File
@@ -13,8 +13,8 @@ section:target { display: block; }
.c { position: absolute; left: 0; right: 0; text-align: center; } .c { position: absolute; left: 0; right: 0; text-align: center; }
/* black cards: one line of quiet Baskerville, the documentary's voice */ /* 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; } .say { font: italic 400 88px/1.18 Baskerville, serif; letter-spacing: .005em; padding: 0 80px; text-wrap: balance; }
.q { font: italic 400 66px/1.2 Baskerville, serif; } .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); .q::before, .q::after { content: ''; display: block; width: 46px; height: 2px; background: var(--gold);
margin: 0 auto 34px; opacity: .8; } margin: 0 auto 34px; opacity: .8; }
.q::after { margin: 34px auto 0; } .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; } .px { font: 400 34px/1.5 Silkscreen, monospace; letter-spacing: .18em; margin-left: .18em; }
</style></head><body> </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_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:960px">Some of them refuse to die.</div></section> <section id="t_b"><div class="c say" style="top:1010px">Some of them refuse to die.</div></section>
<section id="title"> <section id="title">
<svg class="crest" width="150" height="190" viewBox="0 0 150 190" fill="none" stroke="#c9a45a" stroke-width="3.2"> <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> <div class="c est">EST. 1877</div>
</section> </section>
<section id="q_start"><div class="c q" style="top:880px">How did it start?</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:880px">And the challengers?</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_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 &amp; Cream</span><em>1971 –</em></div></section> <section id="lt_doris"><div class="lt"><i></i><b>Doris Penny</b><span>Strawberries &amp; Cream</span><em>1971 –</em></div></section>
@@ -72,6 +72,6 @@ section:target { display: block; }
<section id="end"> <section id="end">
<img class="logo" src="../../assets/logo-v2.png" style="width:700px; top:700px"> <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> </section>
</body></html> </body></html>
+13
View File
@@ -3,6 +3,7 @@
the soundtrack (an audio model hears it so the cut does not rest on guesswork). 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 <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 import base64, json, os, subprocess, sys, tempfile
@@ -11,6 +12,18 @@ import requests
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from gen import API, H, OUT 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] clip = sys.argv[1]
n = int(sys.argv[2]) if len(sys.argv) > 2 else 6 n = int(sys.argv[2]) if len(sys.argv) > 2 else 6
name = os.path.splitext(os.path.basename(clip))[0] name = os.path.splitext(os.path.basename(clip))[0]