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>
40 lines
1.9 KiB
Python
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')
|