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:
+45
-14
@@ -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, 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
|
||||
print(f" {clips[i + 1].name}: trimming {overlap_s:.3f}s of duplicated "
|
||||
f"start (confidence {score:.3f})", file=sys.stderr)
|
||||
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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user