fix: normalize video fps to 24000/1001 with ffprobe check
This commit is contained in:
1 parent
c7ca45696e
commit
fe0c3af9a4
1 file changed
+77
-2
+77
-2
@@ -37,12 +37,75 @@ OUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "extracted
|
||||
# inter-frame only, and FFV1/PCM/ProRes/DNxHR have no GPU implementation.
|
||||
# GPU only makes sense for preview proxies, not for measurement. CPU encode
|
||||
# of 90 s clips is fast enough and bit-exact.
|
||||
VIDEO_CODEC = ["-c:v", "ffv1", "-level", "3", "-pix_fmt", "yuv444p"]
|
||||
VIDEO_CODEC_BASE = ["-c:v", "ffv1", "-level", "3", "-pix_fmt", "yuv444p"]
|
||||
AUDIO_CODEC = ["-c:a", "pcm_f32le", "-ar", "48000", "-ac", "1"]
|
||||
TARGET_FPS = "24000/1001" # Blu-ray film = 23.976023... (shown as 23.98)
|
||||
TARGET_FPS_FLOAT = 24000 / 1001
|
||||
FPS_EPS = 0.02 # tolerance for float comparison
|
||||
|
||||
def _parse_rate(s: str) -> float | None:
|
||||
s = s.strip()
|
||||
if not s or s in ("0/0", "0"):
|
||||
return None
|
||||
if "/" in s:
|
||||
try:
|
||||
n, d = s.split("/")
|
||||
return float(n) / float(d) if float(d) != 0 else None
|
||||
except ValueError:
|
||||
return None
|
||||
try:
|
||||
return float(s)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
def probe_fps(path: str) -> float | None:
|
||||
"""Return probed video fps via ffprobe, or None if probe fails."""
|
||||
if shutil.which("ffprobe") is None:
|
||||
return None
|
||||
# avg_frame_rate is usually canonical; fall back to r_frame_rate
|
||||
for entry in ("avg_frame_rate", "r_frame_rate"):
|
||||
try:
|
||||
r = subprocess.run(
|
||||
["ffprobe", "-v", "error", "-select_streams", "v:0",
|
||||
"-show_entries", f"stream={entry}", "-of", "default=nw=1:nk=1", path],
|
||||
capture_output=True, text=True, timeout=10
|
||||
)
|
||||
for line in r.stdout.strip().splitlines():
|
||||
fps = _parse_rate(line)
|
||||
if fps and fps > 0 and fps < 1000:
|
||||
return fps
|
||||
except Exception:
|
||||
continue
|
||||
return None
|
||||
|
||||
def fps_matches(a: float | None, b: float, eps: float = FPS_EPS) -> bool:
|
||||
return a is not None and abs(a - b) < eps
|
||||
|
||||
# ─── Check ffmpeg ───
|
||||
if shutil.which(f"ffmpeg") is None:
|
||||
sys.exit(f"ERROR: 'ffmpeg' not found on PATH. Install it and try again.")
|
||||
|
||||
# ─── Pre-probe all inputs to decide fps handling ───
|
||||
probed = {}
|
||||
for p in INPUT_FILES:
|
||||
fps = probe_fps(p) if os.path.exists(p) else None
|
||||
probed[p] = fps
|
||||
tag = f"{fps:.3f}" if fps else "unknown (ffprobe missing or file not found)"
|
||||
print(f"[probe] {os.path.basename(p)}: {tag} fps" + (f" {'≈ target ' + TARGET_FPS if fps_matches(fps, TARGET_FPS_FLOAT) else '≠ target ' + TARGET_FPS}" if fps else ""))
|
||||
|
||||
# sanity check: both mismatch target and differ from each other → warn (neither is canonical)
|
||||
valid_fps = [v for v in probed.values() if v is not None]
|
||||
if len(valid_fps) >= 2:
|
||||
all_match = all(fps_matches(v, TARGET_FPS_FLOAT) for v in valid_fps)
|
||||
any_mismatch = any(not fps_matches(v, TARGET_FPS_FLOAT) for v in valid_fps)
|
||||
if any_mismatch:
|
||||
distinct = len({round(v, 2) for v in valid_fps}) > 1
|
||||
if not all_match and distinct:
|
||||
print(f"WARNING: inputs have different non-target fps {valid_fps} — both will be normalized to {TARGET_FPS} ({TARGET_FPS_FLOAT:.6f} fps). "
|
||||
f"If this is PAL speedup (25) vs NTSC film (24000/1001), consider also compensating audio tempo.")
|
||||
elif not all_match:
|
||||
print(f"NOTE: inputs share fps {valid_fps[0]:.3f} but differ from target {TARGET_FPS} — both will be normalized to {TARGET_FPS}.")
|
||||
|
||||
# ─── Prepare output folder ───
|
||||
os.makedirs(OUT_DIR, exist_ok=True)
|
||||
|
||||
@@ -59,13 +122,25 @@ for INPUT_FILE in INPUT_FILES:
|
||||
print(f"-" * 50)
|
||||
|
||||
# ─── Extract video-only (first 90s, no audio, CPU FFV1 re-encode) ───
|
||||
# Only apply fps filter if probed fps differs from target (avoid needless re-timing)
|
||||
fps = probed.get(INPUT_FILE)
|
||||
needs_fps = not fps_matches(fps, TARGET_FPS_FLOAT)
|
||||
video_codec = VIDEO_CODEC_BASE + (["-vf", f"fps={TARGET_FPS}:round=near", "-vsync", "cfr"] if needs_fps else [])
|
||||
if fps is not None:
|
||||
print(f"[fps] {stem}: probed {fps:.3f} → {'convert to ' + TARGET_FPS if needs_fps else 'already ' + TARGET_FPS + ', keeping native timing'}")
|
||||
else:
|
||||
print(f"[fps] {stem}: unknown fps → forcing {TARGET_FPS} (safe fallback)")
|
||||
# ensure fallback forces target if we couldn't probe
|
||||
if not needs_fps:
|
||||
video_codec = VIDEO_CODEC_BASE + ["-vf", f"fps={TARGET_FPS}:round=near", "-vsync", "cfr"]
|
||||
|
||||
cmd_video = [
|
||||
"ffmpeg",
|
||||
"-y", # overwrite output
|
||||
"-i", INPUT_FILE,
|
||||
"-t", str(DURATION),
|
||||
"-an", # no audio
|
||||
*VIDEO_CODEC, # ffv1 lossless intra-only, CPU (no GPU equiv.)
|
||||
*video_codec, # ffv1 lossless intra-only, CPU (no GPU equiv.)
|
||||
video_out
|
||||
]
|
||||
print(f"[video: {stem}] running ffmpeg…")
|
||||
|
||||
Reference in new issue
Block a user