Add continuity report with per-junction gaps and copy-pasteable runs

When a clip boundary's audio doesn't match, print a full table of every
junction (capture-time gap, confidence, verdict) plus the input clips
grouped into continuous runs, each formatted as a ready-to-paste clip
list -- so a discontinuous batch can be immediately re-run on just the
subset that's actually one sequence, without manually diffing timestamps.
This commit is contained in:
2026-07-26 12:53:54 -04:00
parent ac3bdbcfe9
commit ffafb46d78
2 changed files with 104 additions and 12 deletions
+82 -12
View File
@@ -32,6 +32,7 @@ import re
import subprocess
import sys
import tempfile
from datetime import datetime
from pathlib import Path
try:
@@ -104,6 +105,61 @@ def best_overlap(a_tail, b_head, sr=SR, kmin_s=0.05, coarse_step_s=0.002, refine
return best_k / sr, best_score, rms
def fetch_creation_time(clip):
"""Best-effort capture timestamp for a clip, preferring the
local-time Apple tag (matches what Photos/Finder show) and falling
back to the UTC container tag. Returns None if neither is present or
parseable."""
out = subprocess.run(
["ffprobe", "-v", "error", "-show_entries",
"format_tags=com.apple.quicktime.creationdate,creation_time",
"-of", "default=noprint_wrappers=1:nokey=0", str(clip)],
check=True, capture_output=True, text=True,
).stdout
tags = dict(line.split("=", 1) for line in out.strip().splitlines() if "=" in line)
raw = tags.get("TAG:com.apple.quicktime.creationdate") or tags.get("TAG:creation_time")
if not raw:
return None
raw = raw.strip().replace("Z", "+0000")
for fmt in ("%Y-%m-%dT%H:%M:%S%z", "%Y-%m-%dT%H:%M:%S.%f%z"):
try:
return datetime.strptime(raw, fmt)
except ValueError:
continue
return None
def format_gap(gap):
return f"{gap:.0f}s" if gap is not None else "unknown"
def print_continuity_report(clips, results):
"""results: list of (gap_seconds_or_None, score, rms, verdict) per
junction, one entry per consecutive clip pair."""
print("\nContinuity report:", file=sys.stderr)
header = f" {'junction':<40} {'gap':>8} {'confidence':>11} {'verdict':>10}"
print(header, file=sys.stderr)
for i, (gap, score, rms, verdict) in enumerate(results):
junction = f"{clips[i].name} -> {clips[i + 1].name}"
label = {"match": "match", "quiet": "inconclusive", "no match": "NO MATCH"}[verdict]
print(f" {junction:<40} {format_gap(gap):>8} {score:>11.3f} {label:>10}", file=sys.stderr)
runs = [[clips[0]]]
for i, (_, _, _, verdict) in enumerate(results):
if verdict == "no match":
runs.append([clips[i + 1]])
else:
runs[-1].append(clips[i + 1])
print("\nContinuous runs (copy-paste to re-run on just that subset):", file=sys.stderr)
for n, run in enumerate(runs, 1):
if len(run) >= 2:
print(f" Run {n} ({len(run)} clips): {' '.join(c.name for c in run)}", file=sys.stderr)
else:
print(f" Run {n} (1 clip): {run[0].name} -- nothing to concatenate on its own", file=sys.stderr)
print(file=sys.stderr)
def detect_overlaps(clips, confidence_threshold, min_signal_rms, allow_discontinuous):
"""Measure the audio overlap at each clip boundary. Junctions with a
confident match are trimmed accordingly. Junctions with a poor match
@@ -120,33 +176,47 @@ def detect_overlaps(clips, confidence_threshold, min_signal_rms, allow_discontin
raws.append(raw)
audios = [load_audio(r) for r in raws]
creation_times = [fetch_creation_time(c) for c in clips]
overlaps = []
discontinuities = []
results = []
has_discontinuity = False
for i in range(len(audios) - 1):
overlap_s, score, rms = best_overlap(audios[i], audios[i + 1])
pair = f"{clips[i].name} -> {clips[i + 1].name}"
gap = None
if creation_times[i] is not None and creation_times[i + 1] is not None:
gap = (creation_times[i + 1] - creation_times[i]).total_seconds()
if score >= confidence_threshold:
verdict = "match"
print(f" {clips[i + 1].name}: trimming {overlap_s:.3f}s of duplicated "
f"start (confidence {score:.3f})", file=sys.stderr)
elif rms < min_signal_rms:
verdict = "quiet"
print(f"warning: {pair} is too quiet (rms={rms:.0f}) to verify overlap "
f"via audio; assuming continuous with 0 overlap", file=sys.stderr)
overlap_s = 0.0
else:
verdict = "no match"
print(f"warning: {pair} audio does not match (confidence {score:.3f}, "
f"rms={rms:.0f}) -- these clips may not be a continuous sequence",
file=sys.stderr)
discontinuities.append(pair)
f"rms={rms:.0f}, gap {format_gap(gap)}) -- these clips may not be "
f"a continuous sequence", file=sys.stderr)
has_discontinuity = True
overlap_s = 0.0
overlaps.append(overlap_s)
if discontinuities and not allow_discontinuous:
sys.exit(
"error: audio doesn't match at " + ", ".join(discontinuities) + " -- "
"these clips don't look like a continuous sequence (wrong order, "
"missing clip, or unrelated files?). Re-check the input, or pass "
"--allow-discontinuous to concatenate them anyway."
)
overlaps.append(overlap_s)
results.append((gap, score, rms, verdict))
if has_discontinuity:
print_continuity_report(clips, results)
if not allow_discontinuous:
sys.exit(
"error: these clips don't look like a single continuous sequence "
"(wrong order, a missing clip, or unrelated files?). See the "
"continuity report above -- re-run on one of the listed runs, or "
"pass --allow-discontinuous to concatenate everything anyway."
)
return overlaps