Three files hardcoded /media/reala-misaki/BDROM -- extract.py, 07_motion_survey.py and check.sh -- which made the pipeline unrunnable for anyone whose disc mounts anywhere else. DLX_BDROM now overrides it everywhere, defaulting to /media/$USER/BDROM, so this box is unchanged and another one works. Verified by running extract.py against a symlinked mount at a different path. check.sh's failure message names the path it looked at and the variable to set, instead of assuming udisks put it where this machine puts it. README gains a "Reproducing this" section: no media ships here and none of it is redistributable, so it says what you have to bring (the disc) and what is already packaged (vasm is vendored as a binary with its source tarball; the k-means is hand-rolled, so numpy and Pillow are the whole Python dependency). It also names the two gates that SKIP rather than fail -- the px68k second-core pass and the IPL ROM DMAC gate -- because both live outside this repo and a silent skip is worth reading as a skip. One trap called out rather than left to bite: scene selection is a hard-coded stream number, not a search. A different pressing that numbers its .m2ts files differently will extract the wrong footage and the green light will PASS on it. check.sh ALL GREEN. Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
53 lines
2.4 KiB
Python
53 lines
2.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Extract decimated frames from a Blu-ray .m2ts into 256x192 PNGs.
|
|
|
|
Source is 1920x1080 (16:9). The arcade original is 4:3, so we CENTER-CROP to
|
|
1440x1080 by default -- see docs/STATUS.md open question on framing.
|
|
"""
|
|
import subprocess, sys, os, shutil, getpass
|
|
|
|
# Where the Dragon's Lair Blu-ray is mounted. Nothing in this repo ships the
|
|
# media -- bring your own disc, loop-mount it read-only, and point DLX_BDROM at
|
|
# the mount if it is not where udisks puts it:
|
|
# udisksctl loop-setup -r -f DRAGONS_LAIR.iso
|
|
BDROM = os.environ.get("DLX_BDROM") or f"/media/{getpass.getuser()}/BDROM"
|
|
STREAM_DIR = f"{BDROM}/BDMV/STREAM"
|
|
W, H = 256, 192
|
|
|
|
def duration(path):
|
|
out = subprocess.check_output(["ffprobe","-v","error","-show_entries",
|
|
"format=duration","-of","csv=p=0",path], text=True)
|
|
return float(out.strip())
|
|
|
|
def extract(stream, outdir, fps=12, mode="crop", start=None, dur=None):
|
|
src = f"{STREAM_DIR}/{stream}.m2ts"
|
|
total = duration(src)
|
|
if start is None: start = 0.0
|
|
if dur is None: dur = total - start
|
|
shutil.rmtree(outdir, ignore_errors=True); os.makedirs(outdir)
|
|
if mode == "crop": # 4:3 centre crop, arcade framing
|
|
vf = f"crop=1440:1080:240:0,hqdn3d=4:3:0:0,scale={W}:{H}:flags=lanczos"
|
|
elif mode == "squash": # full 16:9 squeezed into 4:3
|
|
vf = f"hqdn3d=4:3:0:0,scale={W}:{H}:flags=lanczos"
|
|
elif mode == "wide": # 16:9 preserved, letterboxed later
|
|
vf = f"hqdn3d=4:3:0:0,scale={W}:144:flags=lanczos"
|
|
else: raise ValueError(mode)
|
|
vf = f"fps={fps}," + vf
|
|
subprocess.check_call(["ffmpeg","-v","error","-ss",str(start),"-t",str(dur),
|
|
"-i",src,"-vf",vf,"-vsync","0",f"{outdir}/f%04d.png","-y"])
|
|
n = len(os.listdir(outdir))
|
|
print(f"{stream}: dur={total:.2f}s -> {n} frames @{fps}fps ({mode})")
|
|
return n
|
|
|
|
if __name__ == "__main__":
|
|
# extract.py <stream> <outdir> [fps] [mode] [start_s] [dur_s]
|
|
# start/dur matter for the long streams: 00223 is 9.4 min and the parts
|
|
# that stress the codec are a few seconds each (tools/analysis/07 finds
|
|
# them). Without them a survey silently averages action into idle scenes.
|
|
stream, outdir = sys.argv[1], sys.argv[2]
|
|
fps = int(sys.argv[3]) if len(sys.argv) > 3 else 12
|
|
mode = sys.argv[4] if len(sys.argv) > 4 else "crop"
|
|
start = float(sys.argv[5]) if len(sys.argv) > 5 else None
|
|
dur = float(sys.argv[6]) if len(sys.argv) > 6 else None
|
|
extract(stream, outdir, fps, mode, start, dur)
|