feat: add video xcorr lab with gray proxy support

This commit is contained in:
Honney OC committed 2026-09-16 15:51:34 +00:00
1 parent 068f059cc1
commit 87a7e9347b
4 files changed
+1199 -1

No files matched your search

+58
View File
@@ -9,6 +9,9 @@ Requires: ffmpeg installed and on PATH.
Usage:
python extract.py # normal extraction
python extract.py loud # loud-only mode: gate out quiet audio, keep only transients
python extract.py proxy # proxy-only mode: build lossless gray analysis proxies
# from ALREADY extracted master videos (local only,
# never touches the original sources)
"""
import subprocess
@@ -22,6 +25,11 @@ LOUD_MODE = "loud" in sys.argv
if LOUD_MODE:
sys.argv.remove("loud")
# ─── Proxy-only mode: build proxies from already extracted masters (no source access) ───
PROXY_ONLY = "proxy" in sys.argv
if PROXY_ONLY:
sys.argv.remove("proxy")
# ─── Input / config (config.toml is base config, fallback to hardcoded) ───
CONFIG_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "config.toml")
_cfg_inputs = None
@@ -35,6 +43,8 @@ _cfg_target_lufs = -14
_cfg_target_tp = -1.0
_cfg_target_lra = 11
_cfg_audio_stream = None # None = prompt, int = index, "skip" = no audio extraction
_cfg_proxy = True # build lossless gray analysis proxy from extracted master
_cfg_proxy_width = 192
try:
try:
import tomllib # py3.11+
@@ -56,6 +66,13 @@ try:
_cfg_eps = float(_ex["target_fps_eps"])
except Exception:
pass
if _ex.get("proxy") is not None:
_cfg_proxy = bool(_ex["proxy"])
if _ex.get("proxy_width") is not None:
try:
_cfg_proxy_width = int(_ex["proxy_width"])
except Exception:
pass
_au = _cfg.get("audio", {})
if _au.get("normalize") is not None:
_cfg_normalize = bool(_au["normalize"])
@@ -114,6 +131,7 @@ OUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), _cfg_out_d
# GPU only makes sense for preview proxies, not for measurement. CPU encode
# of 90 s clips is fast enough and bit-exact.
VIDEO_CODEC_BASE = ["-c:v", "ffv1", "-level", "3", "-pix_fmt", "yuv444p"]
PROXY_CODEC = ["-c:v", "ffv1", "-level", "3", "-pix_fmt", "gray8"]
AUDIO_CODEC = ["-c:a", "pcm_f32le", "-ar", "48000", "-ac", "1"]
# target fps may be overridden via config.toml
TARGET_FPS = _cfg_target_fps # default "24000/1001"
@@ -166,6 +184,31 @@ def probe_fps(path: str) -> float | None:
def fps_matches(a: float | None, b: float, eps: float = FPS_EPS) -> bool:
return a is not None and abs(a - b) < eps
def make_proxy(master_path: str, proxy_out: str, width: int | None = None) -> bool:
"""Build a lossless gray analysis proxy from an ALREADY extracted master.
Reads only the local master mkv (never the original source), downscales to
`width` px gray8 FFV1. Used by sync_video_test.py for fast decode."""
if not os.path.exists(master_path):
print(f"[proxy] SKIP: master not found: {master_path}", file=sys.stderr)
return False
w = int(width) if width else _cfg_proxy_width
cmd = [
"ffmpeg", "-y",
"-i", master_path,
"-an",
"-vf", f"format=gray,scale={w}:-2:flags=neighbor",
*PROXY_CODEC,
proxy_out,
]
print(f"[proxy] {os.path.basename(master_path)} → {os.path.basename(proxy_out)} ({w}px gray FFV1)…")
r = subprocess.run(cmd, capture_output=True, text=True)
if r.returncode != 0:
print(f"[proxy] FAILED:\n{r.stderr}", file=sys.stderr)
return False
print(f"[proxy] done ✔")
return True
def probe_audio_streams(path: str) -> list[dict]:
"""Probe all audio streams in a file. Returns list of dicts matching ~/.bin/ff style."""
if shutil.which("ffprobe") is None:
@@ -439,6 +482,17 @@ _stems = _derive_stems(INPUT_FILES)
# ─── Prepare output folder ───
os.makedirs(OUT_DIR, exist_ok=True)
# ─── Proxy-only mode: build proxies from already extracted masters, never open sources ───
if PROXY_ONLY:
_ok = 0
for _stem in _stems:
_master = os.path.join(OUT_DIR, f"{_stem}_video.mkv")
_proxy = os.path.join(OUT_DIR, f"{_stem}_proxy.mkv")
if make_proxy(_master, _proxy):
_ok += 1
print(f"[proxy] {_ok}/{len(_stems)} proxies built in {OUT_DIR}")
sys.exit(0 if _ok == len(_stems) else 1)
# ─── Validate start offset against file durations ───
for _check_idx, _check_file in enumerate(INPUT_FILES):
try:
@@ -512,6 +566,10 @@ for _file_idx, INPUT_FILE in enumerate(INPUT_FILES):
sys.exit(f"ffmpeg (video) failed:\n{r.stderr}")
print(f"[video: {stem}] done ✔")
# ─── Analysis proxy: derived from the LOCAL master just written (never the source) ───
if _cfg_proxy:
make_proxy(video_out, os.path.join(OUT_DIR, f"{stem}_proxy.mkv"))
# ─── Extract audio-only (duration s, no video, CPU PCM re-encode) ───
if audio_stream is None:
print(f"[audio: {stem}] skipped (no stream selected)")