Add MEMBERSHIP, the club's own 1980s VHS advert
cut.py --cut ad: a plummy announcer (one gpt-audio read per line, ad.sh) sells St. Mordred's over the creepiest takes with yellow 80s supers, a PLAY/STOP VCR read-out, two testimonials from the real lines and the small print read at speed, until the game's umpire says "Quiet." and the jingle dies. A VHS grade (chroma smear, scanlines, tape wobble) is the cut's own. gen.py learns a voice command; one new silent Doris take. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
da09b55525
commit
806a739c6f
7 files changed
+170
-11
No files matched your search
+56
-5
@@ -6,6 +6,7 @@ segment, concatenates, mixes, grades, and writes promo/trailer/lifetime-members.
|
||||
python3 tools/trailer/cut.py # cut 3, the full film: cards + segments + mix
|
||||
python3 tools/trailer/cut.py --short # the ~35 s social cutdown, from the same takes
|
||||
python3 tools/trailer/cut.py --cut score # FIFTEEN–LOVE: the game's umpire scores a love game against you
|
||||
python3 tools/trailer/cut.py --cut ad # MEMBERSHIP: the club's own 1980s VHS recruitment advert
|
||||
python3 tools/trailer/cut.py --mix # reuse segments, redo concat/mix/grade only
|
||||
Every cut keeps its own build folder and output file; nothing overwrites an earlier version.
|
||||
"""
|
||||
@@ -97,8 +98,30 @@ SCORE = [
|
||||
('massacre', None, 0, 1.6, dict()),
|
||||
('logo', None, 0, 6.2, dict(ov=[('logo', 0.3, 6.2)], fo=0.7)),
|
||||
]
|
||||
# MEMBERSHIP: the club recruits YOU — a 1980s VHS advert. A plummy announcer and yellow supers sell
|
||||
# the club over the creepiest shots we have; two "testimonials" use the real lines; the small print is
|
||||
# read at speed; then the game's umpire says "Quiet." and the jingle dies on the spot.
|
||||
su = lambda c, a, dur: [(c, a, dur, 0.2)]
|
||||
AD = [
|
||||
('open', 'gates_v1', 0.0, 3.4, dict(fi=0.3, gain=-10, ov=[('osd_play', 0.0, 1.9, 0)])),
|
||||
('court', 'court_v1', 0.0, 5.4, dict(gain=-12, ov=su('su_grass', 0.6, 5.4))),
|
||||
('reg', 'stare_reg_v1', 0.0, 3.9, dict(gain=-12, ov=su('su_staff', 0.4, 3.9))),
|
||||
('tin', 'tin_v1', 0.0, 2.7, dict(gain=-12, ov=su('su_tonic', 0.3, 2.7))),
|
||||
('doris', 'doris_silent_v1', 0.6, 4.4, dict(gain=-14, ov=su('su_berries', 0.4, 4.4))),
|
||||
('cream', 'cream_v1', 0.3, 2.5, dict(gain=-12, ov=su('su_fresh', 0.3, 2.5))),
|
||||
('stands', 'stands_close_v2', 0.6, 4.6, dict(gain=-14, ov=su('su_welcome', 0.5, 4.6))),
|
||||
('clive', 'stare_clive_v2', 1.0, 3.0, dict(gain=-14, ov=su('su_officials', 0.3, 3.0))),
|
||||
('umpire', 'ambrose_night_v1', 2.1, 1.9, dict(gain=-14, ov=su('su_since', 0.3, 1.9))),
|
||||
('board', 'board_v1', 1.0, 3.3, dict(gain=-12, ov=[('su_since', 0, 3.3, 0)])),
|
||||
('archive', 'archive_v1', 0.0, 3.6, dict(gain=-12, ov=su('su_fixture', 0.3, 3.6))),
|
||||
('members', None, 0, 3.0, dict(ov=[('ad_members', 0, 3.0, 0)])),
|
||||
('doris_t', 'stare_doris_v1', 0.7, 2.8, dict(dlg=True, ov=[('tm_doris', 0.2, 2.8, 0.15)])),
|
||||
('reg_t', 'reg_dawn_v1', 1.4, 2.5, dict(dlg=(0.3, 1.9), eq=True, ov=[('tm_reg', 0.2, 2.5, 0.15)])),
|
||||
('club', None, 0, 12.6, dict(ov=[('ad_club', 0, 12.6, 0), ('ad_fine', 5.9, 12.6, 0.2)])),
|
||||
('logo', None, 0, 3.6, dict(ov=[('logo', 0, 3.6, 0), ('osd_stop', 2.3, 3.6, 0)])),
|
||||
]
|
||||
CUTS = {'full': (FULL, f'lifetime-members-cut{CUT}.mp4'), 'short': (SHORT_EDL, 'lifetime-members-short.mp4'),
|
||||
'score': (SCORE, 'fifteen-love.mp4')}
|
||||
'score': (SCORE, 'fifteen-love.mp4'), 'ad': (AD, 'membership.mp4')}
|
||||
EDL, OUT_NAME = CUTS[NAME]
|
||||
|
||||
# every segment is a whole number of frames, or the audio drifts a sub-frame per cut
|
||||
@@ -155,7 +178,24 @@ def score_sound(start):
|
||||
return music, sfx
|
||||
|
||||
|
||||
MUSIC, SFX = (score_sound if NAME == 'score' else doc_sound)(start)
|
||||
def ad_sound(start):
|
||||
vo = lambda n, at, src=0.0: (aud(n + '.wav'), src, 9.0, at, 'dlg', 0.0, 0.08)
|
||||
duck = f"'if(between(t,{start['doris_t']:.2f},{start['club']:.2f}),0.35,1)':eval=frame" # under the testimonials
|
||||
music = [
|
||||
(aud('ad_long.mp3'), 0.0, start['logo'] + 0.06, 0.0, -14, 0.25, 0.01, duck), # dies on "Quiet."
|
||||
]
|
||||
sfx = [
|
||||
vo('v01', start['open'] + 0.35), vo('v02', start['court'] + 0.1, 0.1), vo('v03', start['reg'] + 0.35),
|
||||
vo('v04', start['tin'] + 0.1), vo('v05', start['doris'] + 0.3), vo('v06', start['cream'] + 0.15, 0.45),
|
||||
vo('v07', start['stands'] + 0.3, 0.1), vo('v08', start['clive'] + 0.15, 1.1), vo('v09', start['umpire'] + 0.15),
|
||||
vo('v10', start['archive'] + 0.15), vo('v11', start['members'] + 0.3), vo('v12', start['club'] + 0.4),
|
||||
vo('v13', start['club'] + 6.1),
|
||||
(game('quiet_dark.m4a'), 0.0, 2.3, start['logo'] + 0.05, 'dlg', 0.0, 0.05),
|
||||
]
|
||||
return music, sfx
|
||||
|
||||
|
||||
MUSIC, SFX = {'score': score_sound, 'ad': ad_sound}.get(NAME, doc_sound)(start)
|
||||
|
||||
|
||||
def run(cmd):
|
||||
@@ -168,7 +208,7 @@ def cards():
|
||||
page = 'file://' + os.path.join(ROOT, 'tools', 'trailer', 'frame.html')
|
||||
for c in sorted({o[0] for _, _, _, _, op in EDL for o in op.get('ov', [])}):
|
||||
run([CHROME, '--headless=new', '--disable-gpu', '--hide-scrollbars', '--force-device-scale-factor=1',
|
||||
f'--window-size={W},{H}', '--default-background-color=00000000', '--virtual-time-budget=1500',
|
||||
f'--window-size={W},{H}', '--default-background-color=00000000', '--virtual-time-budget=6000',
|
||||
f'--screenshot={card(c)}', f'{page}#{c}'])
|
||||
|
||||
|
||||
@@ -268,8 +308,19 @@ def mix(parts):
|
||||
print('loudness in', m['input_i'], 'LUFS, LRA', m['input_lra'])
|
||||
# one grade over everything, so seventeen takes from two models read as one film:
|
||||
# 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]')
|
||||
if NAME == 'ad':
|
||||
# VHS, 1989: hotter colour, chroma bleeding sideways, soft, noisy, scanlines, a little tape wow
|
||||
lines = os.path.join(B, 'scanlines.png')
|
||||
run(['magick', '-size', '1x4', 'xc:none', '-fill', 'rgba(0,0,0,0.20)', '-draw', 'point 0,3', lines + '.pat.png'])
|
||||
run(['magick', '-size', f'{W}x{H}', f'tile:{lines}.pat.png', lines])
|
||||
cmd += ['-loop', '1', '-framerate', str(FPS), '-t', f'{TOTAL:.3f}', '-i', lines]
|
||||
f.append(f'[0:v]eq=contrast=1.07:saturation=1.22:gamma=1.03,chromashift=cbh=5:crh=-5:edge=smear,'
|
||||
f'boxblur=1:1:4:1,noise=c0s=9:c1s=7:c2s=7:c0f=t+u:c1f=t+u:c2f=t+u,vignette=angle=PI/5[vv];'
|
||||
f'[vv][{len(MUSIC + SFX) + 1}:v]overlay=0:0,format=yuv420p[v]')
|
||||
f[-2] = f[-2].replace('aresample=48000[a]', 'lowpass=f=12500,vibrato=f=0.6:d=0.012,aresample=48000[a]')
|
||||
else:
|
||||
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, OUT_NAME)
|
||||
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
|
||||
|
||||
Reference in new issue
Block a user