Add continuity check: abort if clip audio doesn't actually overlap

Distinguishes a genuine audio mismatch at a clip boundary (evidence
the inputs aren't really a continuous sequence) from a boundary that's
simply too quiet to judge either way, using an RMS floor alongside the
existing cross-correlation confidence score. A confident mismatch now
aborts before encoding instead of silently splicing unrelated clips
together; --allow-discontinuous opts back into the old behavior.
This commit is contained in:
2026-07-26 12:49:18 -04:00
parent 302afee062
commit ac3bdbcfe9
2 changed files with 66 additions and 15 deletions
+21 -1
View File
@@ -21,7 +21,11 @@ This script:
needed here).
2. Cross-correlates the audio at each clip boundary to measure the *actual*
overlap duration from the content itself (not just filename order or
whole-second creation timestamps, which aren't precise enough).
whole-second creation timestamps, which aren't precise enough). If a
boundary's audio simply doesn't match — a sign the inputs aren't actually
a continuous sequence (wrong order, a missing clip, unrelated files) — the
run aborts before encoding anything, rather than silently splicing
together clips that don't belong together.
3. Trims the duplicated span off the start of each subsequent clip.
4. Re-encodes the video across the joins (trimming mid-GOP HEVC can't be done
with a plain stream copy) and concatenates everything into one file.
@@ -75,6 +79,22 @@ python .\concat_live_clips.py (2441..2445 | ForEach-Object { "IMG_$_.MOV" })
| `--preset` | x265 preset (default: `medium`) |
| `--metadata-from N` | Take container metadata from the Nth input clip, 0-indexed (default: `0`, the first clip) |
| `--confidence-threshold N` | Minimum audio cross-correlation confidence required to trust an overlap detection before falling back to 0 (default: `0.9`) |
| `--min-signal-rms N` | Below this audio RMS level (int16 scale), a boundary is considered too quiet to judge continuity, so a low-confidence match there won't trigger an abort (default: `25`) |
| `--allow-discontinuous` | Proceed even if audio at a clip boundary doesn't match, instead of aborting |
`--crf`, `--bitrate`, and `--lossless` are mutually exclusive; the default
(no flag) auto-computes a bitrate target from the source clips.
### Continuity check
Before encoding, each clip boundary's audio is checked for a real match.
Three outcomes:
- **Confident match** (confidence ≥ `--confidence-threshold`) — the overlap
is measured and trimmed as normal.
- **Too quiet to tell** (signal below `--min-signal-rms`) — treated as 0
overlap and the run proceeds, since there's no reliable signal either way.
- **Confident mismatch** (enough signal, but it doesn't correlate) — treated
as evidence the clips aren't actually a continuous sequence, and the run
aborts with an error before any encoding happens. Pass
`--allow-discontinuous` to concatenate them anyway.
+44 -13
View File
@@ -65,10 +65,13 @@ def load_audio(path):
def best_overlap(a_tail, b_head, sr=SR, kmin_s=0.05, coarse_step_s=0.002, refine_range_s=0.01):
"""Find the overlap (seconds) that best aligns the tail of clip A with
the head of clip B, via normalized cross-correlation, plus a confidence
score in [0, 1]."""
score in [0, 1] and the RMS signal level (on the original int16 scale)
of the compared windows, so a caller can tell a genuine mismatch apart
from both sides just being too quiet to correlate reliably."""
n = min(len(a_tail), len(b_head))
a_tail = a_tail[-n:]
b_head = b_head[:n]
rms = float(min(np.sqrt(np.mean(a_tail ** 2)), np.sqrt(np.mean(b_head ** 2))))
kmin = max(1, int(kmin_s * sr))
coarse_step = max(1, int(coarse_step_s * sr))
@@ -83,7 +86,7 @@ def best_overlap(a_tail, b_head, sr=SR, kmin_s=0.05, coarse_step_s=0.002, refine
ks = list(range(kmin, n, coarse_step))
if not ks:
return 0.0, 0.0
return 0.0, 0.0, rms
scores = [score(k) for k in ks]
best_i = int(np.argmax(scores))
best_k = ks[best_i]
@@ -98,10 +101,17 @@ def best_overlap(a_tail, b_head, sr=SR, kmin_s=0.05, coarse_step_s=0.002, refine
best_score = s
best_k = k
return best_k / sr, best_score
return best_k / sr, best_score, rms
def detect_overlaps(clips, confidence_threshold):
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
AND enough signal to trust that result are treated as evidence the
clips aren't actually a continuous sequence, and abort the run (unless
allow_discontinuous is set) rather than silently stitching together
what might be unrelated clips. Junctions too quiet to judge either way
fall back to 0 overlap, same as a confident 0."""
with tempfile.TemporaryDirectory() as tmp:
raws = []
for c in clips:
@@ -111,18 +121,33 @@ def detect_overlaps(clips, confidence_threshold):
audios = [load_audio(r) for r in raws]
overlaps = []
discontinuities = []
for i in range(len(audios) - 1):
overlap_s, score = best_overlap(audios[i], audios[i + 1])
if score < confidence_threshold:
print(
f"warning: low-confidence overlap ({score:.3f}) between "
f"{clips[i].name} and {clips[i + 1].name}; treating as 0 overlap",
file=sys.stderr,
)
overlap_s = 0.0
overlap_s, score, rms = best_overlap(audios[i], audios[i + 1])
pair = f"{clips[i].name} -> {clips[i + 1].name}"
if score >= confidence_threshold:
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:
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:
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)
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."
)
return overlaps
@@ -210,6 +235,12 @@ def main():
help="Take container metadata from the Nth clip, 0-indexed (default: 0, the first clip)")
parser.add_argument("--confidence-threshold", type=float, default=0.9,
help="Minimum audio cross-correlation confidence to trust an overlap detection (default: 0.9)")
parser.add_argument("--min-signal-rms", type=float, default=25.0,
help="Below this audio RMS level (int16 scale), a clip boundary is considered too quiet "
"to judge continuity and a low-confidence match there won't trigger an error (default: 25)")
parser.add_argument("--allow-discontinuous", action="store_true",
help="Proceed even if audio at a clip boundary doesn't match (normally treated as a "
"sign these aren't actually a continuous sequence, and aborts)")
args = parser.parse_args()
clips = args.clips
@@ -222,7 +253,7 @@ def main():
output = args.output or default_output_name(clips)
print(f"Detecting real overlap between {len(clips)} clips via audio cross-correlation...", file=sys.stderr)
overlaps = detect_overlaps(clips, args.confidence_threshold)
overlaps = detect_overlaps(clips, args.confidence_threshold, args.min_signal_rms, args.allow_discontinuous)
filter_complex = build_filter(clips, overlaps)