feat: add video xcorr lab with gray proxy support
This commit is contained in:
1 parent
068f059cc1
commit
87a7e9347b
4 files changed
+1199
-1
No files matched your search
+58
@@ -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)")
|
||||
|
||||
Reference in new issue
Block a user