Files
wuwiandClaude Opus 5.5 d388004eac 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>
2026-10-03 20:27:13 +02:00

40 lines
1.9 KiB
Python

#!/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')