One Offset Is Not Enough
Independent audio and video clocks can start misaligned and keep separating, so a single timeline shift fixes the opening while the ending drifts anyway.
avsyncdoctor detects flash and click events, fits both an offset and a clock ratio, applies FFmpeg trim and tempo correction, and remeasures the output.
The Demo
Five anchors carry a known 120 ms offset and 5,000 ppm of clock drift. A real MP4 integration test detects the mismatch, repairs it with FFmpeg, and requires the remeasured skew to stay under 40 ms.
The event-based path suits claps, flashes, impacts, and test slates. Continuous speech and semantic mismatch need content-aware audio-visual features.
Bad Fixtures Broke the Fit, Not Just the Numbers
A least-squares fit is only as safe as its edge cases. The estimator divided by the variance of the video timestamps, and separately by the fitted slope, with no guard on either denominator reaching zero. A fixture with identical video timestamps, and another with constant audio timestamps, each crashed it with an unhandled division by zero. That exception type wasn't caught by the JSON API's error handling either, so posting either fixture to the live endpoint dropped the connection with no response at all. Both degenerate cases are now rejected with a clear error before the division runs.
More Anchors Made the Fit Better and the Verdict Worse
The fixture places its anchors exactly, which is not what flash and click detection gives you. So I drew random offsets and drifts and jittered every detected event by one frame at 24 fps.
With five anchors the fit lands a median 27.2 ms from the true offset and 3,398 ppm from the true drift. With twelve anchors it improves to 16.2 ms and 1,087 ppm. The shipped pass rate over the same runs goes the other way: 41% down to 4%.
The fit isn't getting worse. max_skew_after_model_ms is the largest residual across the anchors, and the largest of twelve jittered residuals is naturally bigger than the largest of five. It measures how noisy your event detection was, not how good the fit is. fit_rmse_ms stays near the noise level instead, 29.5 and 37.1 ms against 41.7 ms of jitter, so that's the number to gate on when anchors are detected rather than placed. Keep max residual for exact anchors like a test slate, where any residual really is a defect. A test holds both behaviors. 9 tests pass across Python 3.10 through 3.13.
Research Basis
AV-SyncBench separates temporal alignment from semantic correspondence when evaluating audio-video synchronization. Read AV-SyncBench.
Try It
Needs FFmpeg:
git clone https://github.com/ahmeddoghri/avsyncdoctor && cd avsyncdoctor
pip install -e .
avsyncdoctor demo # 120 ms offset, 5000 ppm drift, repaired
python -m avsyncdoctor.sweep # accuracy under one-frame anchor jitter
avsyncdoctor serve # workbench on :8090