This commit is contained in:
mike
2026-06-18 23:26:30 +02:00
parent 1dead1c666
commit 045b9b6458
21 changed files with 1477 additions and 979 deletions

152
optimize_clips.py Normal file
View File

@@ -0,0 +1,152 @@
import os
import glob
import shutil
import subprocess
import tempfile
import threading
import time
CLIPS_DIR = "/data/events/clips"
ARCHIVE_DIR = "/mnt/tour-big/clips"
SIZE_THRESHOLD_MB = 200
SIZE_THRESHOLD_BYTES = SIZE_THRESHOLD_MB * 1024 * 1024
MAX_PER_RUN = 20
def has_nvenc():
r = subprocess.run(["ffmpeg", "-hide_banner", "-encoders"],
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
return b"hevc_nvenc" in r.stdout
def _ffmpeg(cmd):
return subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
def _proxy_cmd(src, dst, hw):
"""10fps H.265 proxy for quick local browsing."""
if hw:
return ["ffmpeg", "-y",
"-hwaccel", "cuda", "-hwaccel_output_format", "cuda",
"-i", src,
"-vcodec", "hevc_nvenc", "-rc", "constqp", "-qp", "28", "-preset", "p4",
"-r", "10", "-tag:v", "hvc1", "-acodec", "copy", dst]
return ["ffmpeg", "-y", "-i", src,
"-vcodec", "libx265", "-crf", "28", "-preset", "medium",
"-r", "10", "-tag:v", "hvc1", "-acodec", "copy", dst]
def _archive_cmd(src, dst, hw):
"""Full-fps high-quality H.265 for archive — small enough to pull back fast over slow SMB."""
if hw:
return ["ffmpeg", "-y",
"-hwaccel", "cuda", "-hwaccel_output_format", "cuda",
"-i", src,
"-vcodec", "hevc_nvenc", "-rc", "vbr", "-cq", "19", "-b:v", "0",
"-preset", "p6", "-tune", "hq",
"-tag:v", "hvc1", "-acodec", "copy", dst]
return ["ffmpeg", "-y", "-i", src,
"-vcodec", "libx265", "-crf", "19", "-preset", "slow",
"-tag:v", "hvc1", "-acodec", "copy", dst]
def _encode_worker(label, cmd, hw, fallback_cmd, out):
proc = _ffmpeg(cmd)
if proc.returncode != 0 and hw:
print(f" [{label}] NVENC failed, retrying in software...")
proc = _ffmpeg(fallback_cmd)
out[label] = proc
def process(src, orig_size, hw):
name = os.path.basename(src)
archive_dst = os.path.join(ARCHIVE_DIR, name)
if os.path.exists(archive_dst):
print(f" Skip {name} — already archived")
return 0
proxy_tmp = src + ".proxy.mp4"
# Encode archive to local /tmp first — never write directly to a slow SMB share
archive_tmp = os.path.join(tempfile.gettempdir(), name + ".archive.mp4")
print(f"[{name}] {orig_size/(1024*1024):.0f} MB ({'NVENC' if hw else 'software'})")
t0 = time.time()
results = {}
threads = [
threading.Thread(target=_encode_worker, args=(
"proxy",
_proxy_cmd(src, proxy_tmp, hw), hw,
_proxy_cmd(src, proxy_tmp, False),
results,
)),
threading.Thread(target=_encode_worker, args=(
"archive",
_archive_cmd(src, archive_tmp, hw), hw,
_archive_cmd(src, archive_tmp, False),
results,
)),
]
for t in threads: t.start()
for t in threads: t.join()
failed = {k: v for k, v in results.items() if v.returncode != 0}
if failed:
for label, proc in failed.items():
print(f" [{label}] error: {proc.stderr.decode()[-300:]}")
for f in (proxy_tmp, archive_tmp):
if os.path.exists(f): os.remove(f)
return 0
proxy_mb = os.path.getsize(proxy_tmp) / (1024*1024)
archive_mb = os.path.getsize(archive_tmp) / (1024*1024)
orig_mb = orig_size / (1024*1024)
print(f" proxy {orig_mb:.0f}->{proxy_mb:.0f} MB "
f"archive {orig_mb:.0f}->{archive_mb:.0f} MB "
f"({time.time()-t0:.1f}s) — pushing archive to share...")
try:
shutil.move(archive_tmp, archive_dst)
except Exception as e:
print(f" Archive move failed: {e} — aborting, original intact")
for f in (proxy_tmp, archive_tmp):
if os.path.exists(f): os.remove(f)
return 0
# Atomic swap: original -> proxy (same filesystem)
os.replace(proxy_tmp, src)
freed = orig_size - os.path.getsize(src)
print(f" Done freed locally: {freed/(1024*1024):.0f} MB")
return freed
if __name__ == "__main__":
hw = has_nvenc()
print(f"Encoder: {'hevc_nvenc (A6000, 2x parallel)' if hw else 'libx265 (software)'}")
print(f"Clips: {CLIPS_DIR}")
print(f"Archive: {ARCHIVE_DIR}\n")
if not os.path.isdir(ARCHIVE_DIR):
print(f"ERROR: archive not reachable: {ARCHIVE_DIR}")
raise SystemExit(1)
files = sorted(
[(fp, os.path.getsize(fp))
for fp in glob.glob(os.path.join(CLIPS_DIR, "*.mp4"))
if os.path.isfile(fp) and os.path.getsize(fp) > SIZE_THRESHOLD_BYTES],
key=lambda x: x[1], reverse=True,
)
print(f"Found {len(files)} files > {SIZE_THRESHOLD_MB} MB\n")
total_freed = 0
done = 0
for path, size in files:
freed = process(path, size, hw)
total_freed += freed
if freed:
done += 1
if done >= MAX_PER_RUN:
print(f"Reached {MAX_PER_RUN}-file limit.")
break
print(f"\nDone. Processed: {done} Freed locally: {total_freed/(1024*1024):.0f} MB")