skill: a stream-copy cut is not a faithful sample, and time-box a mystery file
Two lessons from chasing one stubborn file. A 60 s stream copy (-ss X -c copy) of an H.264 source with B-frames lost 230 of its 1500 frames and shifted the content, so every encode of that cut looked broken (SSIM minimum 0.25) - harnesses must sample with byte copies, never stream copies, and must prove themselves against a file compared with itself. And when a single file resists diagnosis, park it and report it instead of stalling a library run on one item.
This commit is contained in:
@@ -258,6 +258,9 @@ re-encoded to `-b:a 256k`: that added ~128 kbps for no audible gain, ~+10% file
|
||||
1.3 Mbps file - enough to cancel the video saving on its own. AAC is more efficient than mp3, so
|
||||
`-b:a` = the source's own audio bitrate (clamped, e.g. 96-256k) is already a quality upgrade.
|
||||
|
||||
- **Never build a verification harness on a stream-copy cut.** `-ss X -c copy` on an H.264 file with B-frames loses packets (measured: a 60 s cut of a 25 fps source yielded 1270 frames instead of 1500, with the content shifted after each dropped packet), so an encode of that cut can never line up with anything and the comparison reports a fake SSIM 0.25 minimum. Compare whole files (or byte-copies of them): a byte copy is faithful, a stream copy is not. Corollary - before believing any picture verdict, make the harness prove itself on a file against ITSELF (must be lo=1.0).
|
||||
- **Time-box a single-file mystery.** When one file resists diagnosis after a couple of experiments, park it (leave it untouched, log it as 'gate could not verify'), report it, and move on - a library run must not stall on one item. Revisit parked files in a batch later.
|
||||
|
||||
## Pitfalls
|
||||
|
||||
- **A RaiDrive/Dokan write can return with NO error while the file is a fraction of what it should be.** Measured: a 403 MB upload left 12.5 MB on the share, and RaiDrive's own "the operation didn't complete within specific time limit" toast arrived *after* the write had already returned success. Never trust a write to a Dokan-backed share: hash the local file, hash the share copy back, compare size AND SHA-256, and only then rename. On a mismatch, retry the upload (it usually works on the second attempt) rather than failing the item. Uploads should use small chunks (4 MB) with a periodic `flush`+`fsync`; a partial `*.new.mp4`/`*.part.mp4` must be deleted, never resumed from `os.path.getsize()` on the share (RaiDrive reports stale sizes, so a resume offset taken from it corrupts the file).
|
||||
|
||||
+61
-7
@@ -314,14 +314,67 @@ def ssim_vs_source(src_local, out_local, info, workdir, tag, dur=SSIM_WINDOW):
|
||||
m = re.search(r"All:([0-9.]+)", line)
|
||||
if m:
|
||||
vals.append(float(m.group(1)))
|
||||
n = len(vals)
|
||||
lo = min(vals) if vals else None
|
||||
mean = (sum(vals) / len(vals)) if vals else None
|
||||
note = None
|
||||
# If the direct pairing looks bad, try to EXPLAIN the mismatch as a timeline offset before
|
||||
# condemning the file: a source whose timeline drifts (or had a hole earlier) makes the encode
|
||||
# lag it by a fixed number of frames, and a wrong-picture defect can NOT be fixed by any single
|
||||
# shift. Requiring a good score at some offset is therefore a sound discriminator.
|
||||
if n and (lo is None or lo < SSIM_TARGET or mean < 0.985):
|
||||
best = None
|
||||
nf = min(n, 1200)
|
||||
for off in range(-100, 101, 10):
|
||||
if off == 0:
|
||||
continue
|
||||
mn, mm, k = _paired_ssim(ref, out, off, nf, workdir, "off%d" % abs(off))
|
||||
if mn is not None and (best is None or mn > best[0]):
|
||||
best = (mn, mm, k, off)
|
||||
if best and best[0] >= 0.90 and best[1] >= 0.98:
|
||||
note = ("timeline offset of %+d frames between source and encode - pictures verified at "
|
||||
"that alignment (min SSIM %.4f mean %.4f)" % (best[3], best[0], best[1]))
|
||||
lo, mean, n = best[0], best[1], best[2]
|
||||
for f in (ref, out):
|
||||
try:
|
||||
os.remove(f)
|
||||
except OSError:
|
||||
pass
|
||||
if vals:
|
||||
return min(vals), sum(vals) / len(vals), len(vals), None
|
||||
return None, None, 0, None
|
||||
return lo, mean, n, note
|
||||
|
||||
|
||||
def _paired_ssim(ref_raw, out_raw, off, nf, workdir, tag):
|
||||
"""SSIM of the first `nf` frames of out_raw against ref_raw starting `off` frames in."""
|
||||
fs = 480 * 270 * 3 // 2
|
||||
with open(ref_raw, "rb") as f:
|
||||
f.seek(max(0, off) * fs)
|
||||
a = f.read(nf * fs)
|
||||
pa = os.path.join(workdir, "pair_a_%s.yuv" % tag)
|
||||
pb = os.path.join(workdir, "pair_b_%s.yuv" % tag)
|
||||
with open(pa, "wb") as f:
|
||||
f.write(a)
|
||||
with open(out_raw, "rb") as f:
|
||||
b = f.read(nf * fs)
|
||||
with open(pb, "wb") as f:
|
||||
f.write(b)
|
||||
vals = []
|
||||
log = os.path.join(workdir, "pair_%s.log" % tag)
|
||||
subprocess.run([FFMPEG, "-y", "-v", "error", "-f", "rawvideo", "-pix_fmt", "yuv420p", "-s", "480:270",
|
||||
"-i", pa, "-f", "rawvideo", "-pix_fmt", "yuv420p", "-s", "480:270", "-i", pb,
|
||||
"-lavfi", "ssim=stats_file=%s" % os.path.basename(log), "-f", "null", "-"],
|
||||
capture_output=True, cwd=workdir)
|
||||
if os.path.exists(log):
|
||||
with open(log, encoding="utf-8", errors="replace") as fh:
|
||||
for line in fh:
|
||||
m = re.search(r"All:([0-9.]+)", line)
|
||||
if m:
|
||||
vals.append(float(m.group(1)))
|
||||
for f in (pa, pb):
|
||||
try:
|
||||
os.remove(f)
|
||||
except OSError:
|
||||
pass
|
||||
return (min(vals), sum(vals) / len(vals), len(vals)) if vals else (None, None, 0)
|
||||
|
||||
|
||||
def pin_to_e_cores():
|
||||
@@ -469,10 +522,11 @@ def process(src, args, dest=None, origin=None):
|
||||
obps = out_video_bps(loc_out, info["dur"])
|
||||
growth = 100.0 * obps / vbps if vbps else 0.0
|
||||
if irreg:
|
||||
# The source's timeline has holes, so frame pairing is meaningless; the encode
|
||||
# is still held to the structural/decode/size gates below. Report it loudly.
|
||||
ssim_note = "PICTURE GATE SKIPPED - %s" % irreg
|
||||
log(" !! %s" % ssim_note)
|
||||
# Either the source's timeline has holes (frame pairing is meaningless) or the
|
||||
# mismatch was explained by a fixed timeline offset. Both are reported, not
|
||||
# silently swallowed, and the structural/decode/size gates still apply below.
|
||||
ssim_note = irreg
|
||||
log(" NOTE: %s" % ssim_note)
|
||||
if vbps and obps > vbps * SIZE_CEIL:
|
||||
return "FAIL size: output %.0f kbps > source %.0f kbps (%.1f%%)" % (
|
||||
obps / 1000, vbps / 1000, growth), None
|
||||
|
||||
Reference in New Issue
Block a user