anime_dub_sync
Transfer a dubbed audio track (e.g. German) from a low-quality source file onto a high-quality video file, with automatic A/V offset detection and verification.
Only use this with sources you legally own. All paths and titles in this repo are generic placeholders — do not commit real media filenames, absolute local paths, or release-group names.
Requirements
System tools (must be on PATH):
ffmpeg+ffprobemkvmerge(from MKVToolNix)
Python (>= 3.10):
opencv-python,scipy,numpy,soundfile,tqdn- see
requirements.txt
Install:
pip install -r requirements.txt
Repository layout
main.py # batch + single-episode orchestration
utils.py # subprocess helper
audio/
__init__.py
audio_utils.py # ffmpeg extraction, concat, shifting, mux helpers
audio_cross_correlation.py # intro/outro audio offset measurement
video/
__init__.py
video_utils.py # ffprobe wrapper
scene_change_cross_correlation.py # primary video offset detection
temporal_cross_correlation.py # alternative/legacy motion-signal method
mkv/
__init__.py
chapter.py # chapter parsing, intro/outro candidate search
intro_outro.py # final sync verification
check_intro_outro.py # CLI wrapper for verification
How it works
1. Video offset detection (video/)
video/scene_change_cross_correlation.py:70 — corelate_videos(vid1, vid2, fps):
get_scene_change_signaldecodes each video with OpenCV at 160x90 grayscale, computesmean(diff^2)per frame. Cuts produce large spikes.resample_signalresamples both signals to a common FPS withscipy.signal.resample.- Both signals are z-normalized, then
scipy.signal.correlate(mode='full')finds the lag with peak correlation. - Lag in samples / FPS = offset in seconds. Positive means
vid1starts aftervid2.
video/video_utils.py:14 — probe_video(path) returns
(fps, duration, codec, start_time) via ffprobe -show_streams -show_format.
video/temporal_cross_correlation.py:52 — find_temporal_offset is a similar
motion-signal correlator. It is exported but not used by main.py; the active
path is corelate_videos.
2. Audio splicing (audio/)
audio/audio_utils.py:
extract_audio_segment_file(video, start, end, out)— extracts0:a:0with ffmpeg to stereo AAC.mka.extract_audio_segment_tmp(file, (start, duration) | start, end, stream)— extracts a stream to a temp 44.1 kHz PCM WAV for analysis.get_audio_duration(file)— parsesDURATIONtag via ffprobe.stick_together_audio(left, right, out)— concatenates with the ffmpeg concat demuxer (-c copy, no re-encode).add_silence(file, offset)—adelay(offset > 0, silence at start) orapad(offset < 0, silence at end), re-encoded to Opus.replace_with_shifted_mka(mkv, mka, audioline, offset)— physically shifts withadelay/atrim+asetpts, re-encodes to Opus 160k, replaces the track withmkvmerge, tags itger/German (Opus Stereo).get_audio_streams,get_track_info,shift_audio— stream enumeration and in-place shift helpers.
audio/audio_cross_correlation.py:39 — get_audio_offset(file1, file2, time):
decodes to mono 44.1 kHz WAV, mean-normalizes, scipy.signal.correlate, returns
lag / sample-rate in seconds.
3. Intro/outro verification (mkv/)
mkv/chapter.py:12 — get_chapters(file) lists (start, duration) via
ffprobe -show_chapters.
mkv/chapter.py:35 — get_intro_outro(file, length=90.0) keeps chapters with
duration within [length/2, length*2]. Expects exactly 2 (intro + outro).
mkv/intro_outro.py:8 — check_intro_outro_sync(file, length, uncertainty, max_offset):
- Trims
uncertainty(default 10%) off each intro/outro edge. - Extracts first and last audio streams for intro and outro.
- Audio-correlates intro pair and outro pair.
- Returns
(True, mean_offset, last_stream)if|mean(outro_offsets) - mean(intro_offsets)| < max_offset(default 0.1 s), else(False, None, None).
4. Orchestration (main.py)
main.py:18 — mux_audio_into_output() runs mkvmerge to add the synced
.mka as a German track.
main.py:37 — process_one(German_Video, Quality_Video, output_dir, offset=None) runs 9 tqdm steps:
Probe video— probe both inputs.Compute offset—corelate_videos, or use suppliedoffset.Extract pre beginning— head of quality audio before dub starts (skipped if < 0.05 s).Extract german audio— main dubbed segment.Extract post end— tail of quality audio (skipped if < 0.5 s, appended only if >MIN_END_DUR = 2.5s).Combine audio— concat head + dub + tail; falls back toadd_silence.Mux— mkvmerge intooutput_dir/<quality_stem>.mkv.Cleanup— removes temp_original_audio*.mka.Final check—check_intro_outro_sync; sub-frame residual is fixed withreplace_with_shifted_mkaif|offset| > 1/fps. Failures are renamed to[FAILED]_*.mkv, chapterless files to[NO CHAPTER]_*.mkv.
Usage
Single episode (output dir as 3rd arg, else ../output_dir):
python main.py <dubbed_source.mkv> <high_quality_source.mkv> [output_dir]
python main.py dubbed_ep01.mkv quality_ep01.mkv ./output_dir
With explicit offset in seconds (bypass video correlation), pass it as 4th
argument handling inside process_one — currently CLI uses auto-detect; edit
or call the function directly for manual offsets.
Batch: edit main() in main.py:225 (output_dir, dubbed_dir,
quality_dir, episode range), then:
python main.py
Verify an existing file without re-muxing:
python mkv/check_intro_outro.py ./output_dir/episode.mkv
Configuration
main.py:16MIN_END_DUR = 2.5— minimum tail length to append.main.py:14DEBUG = False— enable ffmpeg segment logging.mkv/intro_outro.py:8length=90.0, uncertainty=0.1, max_offset=0.1— expected OP/ED length and tolerances.
Notes and limitations
- Needs chapter marks to run the final check; files without them are kept
with a
[NO CHAPTER]_prefix and returnFalse. - Scene-change correlation assumes both files are the same episode cut. Different cuts, missing scenes, or very dark/flat video degrade the peak.
utils.py:4run(cmd)prints combined stdout on failure, then re-raisesCalledProcessError.- Do not commit
.mkv/.mkamedia or absolute filesystem paths. Keepoutput_dir/and temp audio out of git.