Add the LIFETIME MEMBERS trailer pipeline

A vertical live-action mockumentary trailer: the dead staff of St. Mordred's
(the game's bosses) each tell a different origin story, and the film ends on
"Quiet, please." / "Play." / splat. tools/trailer/ holds the whole pipeline:
stills and takes through the OpenRouter key (gen.py, stills.sh, clips.sh),
the frame graphics in HTML/CSS (frame.html), a QA listener (qa.py), the
music sectioner (segment.py) and the edit (cut.py). Media lands in the
gitignored promo/trailer/. SCRIPT.md is the screenplay and the cut notes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5.5 committed 2026-10-03 20:27:13 +02:00
1 parent 74aa209753
commit d388004eac
10 files changed
+631 -1

No files matched your search

+39
View File
@@ -0,0 +1,39 @@
#!/usr/bin/env python3
"""Cut the Lyria cues into their sections. Lyria writes medleys (three moods per cue, joined by
hard cuts), so the score is EDITED from sections rather than used whole. Novelty on a spectral
self-similarity curve finds the joins; each section is then written out as its own wav for audition.
uv run --with numpy python tools/trailer/segment.py promo/trailer/audio/cue_a.mp3 ...
"""
import os, subprocess, sys
import numpy as np
SR, HOP = 22050, 11025 # half-second frames
for f in sys.argv[1:]:
raw = subprocess.run(['ffmpeg', '-v', 'error', '-i', f, '-ac', '1', '-ar', str(SR), '-f', 's16le', '-'],
capture_output=True).stdout
x = np.frombuffer(raw, np.int16).astype(float) / 32768
n = len(x) // HOP
fr = x[:n * HOP].reshape(n, HOP) * np.hanning(HOP)
spec = np.abs(np.fft.rfft(fr, axis=1))
edges = np.geomspace(40, 10000, 25)
bins = np.fft.rfftfreq(HOP, 1 / SR)
bands = np.stack([spec[:, (bins >= a) & (bins < b)].mean(1) for a, b in zip(edges, edges[1:])], 1)
feat = np.log(bands + 1e-6)
feat = (feat - feat.mean(0)) / (feat.std(0) + 1e-6)
k = 4 # 2 s either side
nov = np.zeros(n)
for i in range(k, n - k):
nov[i] = np.linalg.norm(feat[i - k:i].mean(0) - feat[i:i + k].mean(0))
cuts = [0]
for i in np.argsort(-nov):
if nov[i] < 2.2: break
if all(abs(i - c) >= 12 for c in cuts) and n - i >= 8:
cuts.append(i)
cuts = sorted(cuts) + [n]
rms = 20 * np.log10(np.sqrt((fr ** 2).mean(1)) + 1e-9)
base = os.path.splitext(f)[0]
for j, (a, b) in enumerate(zip(cuts, cuts[1:])):
out = f'{base}.s{j}.wav'
subprocess.run(['ffmpeg', '-v', 'error', '-y', '-ss', str(a / 2), '-t', str((b - a) / 2), '-i', f, out], check=True)
print(f'{os.path.basename(out)} {a/2:6.1f}-{b/2:6.1f}s rms {rms[a:b].mean():5.1f} dB')