Setting the file. One moment.
Render Diff · Remotion To Hyperframes · heygen-com/hyperframes · Skills Docs
ContentsBack to the top of the page (opens in a new tab)
scripts/ render_diff.sh
Shell · 103 lines · 3 KB
# Exit codes:
16 # 0 — pass (mean SSIM >= threshold)
17 # 1 — fail (mean SSIM < threshold)
18 # 2 — usage / setup error
19 #
20 # Threshold defaults to 0.85 (loose; tier-specific thresholds are applied by
21 # the orchestrator). Override with R2HF_SSIM_THRESHOLD=0.95 in the environment.
22
23 set -euo pipefail
24
25 THRESHOLD = "${ R2HF_SSIM_THRESHOLD :- 0 . 85 }"
26
27 if [[ $# -lt 2 || $# -gt 3 ]]; then
28 echo "usage: $0 <baseline.mp4> <translated.mp4> [output-dir]" >&2
29 exit 2
30 fi
31
32 BASELINE = " $1 "
33 TRANSLATED = " $2 "
34 OUTDIR = " ${3 :- . / diff-out } "
35
36 if [[ ! -f " $BASELINE " ]]; then
37 echo "error: baseline not found: $BASELINE " >&2
38 exit 2
39 fi
40 if [[ ! -f " $TRANSLATED " ]]; then
41 echo "error: translated not found: $TRANSLATED " >&2
42 exit 2
43 fi
44 if ! command -v ffmpeg > /dev/null 2>&1 ; then
45 echo "error: ffmpeg not on PATH" >&2
46 exit 2
47 fi
48
49 mkdir -p " $OUTDIR "
50 SSIM_LOG = " $OUTDIR /ssim.log"
51 SUMMARY = " $OUTDIR /summary.json"
52
53 # ffmpeg's ssim filter writes one line per frame to stats_file and a single
54 # Mean SSIM line to stderr. We capture both — per-frame for distribution
55 # stats, and the mean for the headline number.
56 ffmpeg -hide_banner -nostats -loglevel info \
57 -i " $BASELINE " -i " $TRANSLATED " \
58 -lavfi "[0:v]scale=iw:ih[ref];[1:v]scale=iw:ih[main];[main][ref]ssim=stats_file= $SSIM_LOG " \
59 -f null - 2> " $OUTDIR /ffmpeg.stderr"
60
61 # Parse: each line in ssim.log looks like
62 # n:1 Y:0.987655 U:0.992345 V:0.991234 All:0.989012 (19.512345)
63 # We want the All:N column.
64 python3 - " $SSIM_LOG " " $SUMMARY " " $THRESHOLD " << 'PY'
65 import json, math, re, sys
66 from pathlib import Path
67
68 log_path = Path(sys.argv[1])
69 out_path = Path(sys.argv[2])
70 threshold = float(sys.argv[3])
71
72 values = []
73 pattern = re.compile(r"All:([\d.]+)")
74 for line in log_path.read_text().splitlines():
75 m = pattern.search(line)
76 if m:
77 try:
78 values.append(float(m.group(1)))
79 except ValueError:
80 pass
81
82 if not values:
83 print(f"error: no SSIM samples parsed from {log_path}", file=sys.stderr)
84 sys.exit(2)
85
86 values.sort()
87 n = len(values)
88 mean = sum(values) / n
89 p_idx = lambda p: min(n - 1, max(0, int(math.floor(p * n))))
90 summary = {
91 "frame_count": n,
92 "mean": round(mean, 6),
93 "min": round(values[0], 6),
94 "max": round(values[-1], 6),
95 "p05": round(values[p_idx(0.05)], 6),
96 "p95": round(values[p_idx(0.95)], 6),
97 "threshold": threshold,
98 "pass": bool(mean >= threshold),
99 }
100 out_path.write_text(json.dumps(summary, indent=2) + "\n")
101 print(json.dumps(summary, indent=2))
102 sys.exit(0 if summary["pass"] else 1)
103 PY