GitHub: https://github.com/iwalton3/media-scripts/tree/master/subtitle-audio-transfer
Sometimes, the media you want to watch is available in a higher video or audio quality without the desired subtitles or appropriate audio tracks. Merging the videos to contain the desired tracks is usually error-prone, as the tracks are not usually synced and even a sync error of less than a second can render the video unwatchable.
These scripts automatically process video files and sync the subtitle or audio tracks, using the sync parameters in the mkv video format. They are great for adding subtitles to raw anime releases when there are not comparable subtitled versions. The subtitle sync uses the audio sync script as a fallback, so you should install both even if you do not plan to transfer audio tracks.
You’ll need a copy of mkvtoolnix and python3 for these scripts to work. I suggest using cygwin or WSL to run the scripts if you are not running Linux. The sync work is done by subsync and align-videos-by-sound, which should be installed with the following commands:
sudo pip3 install git+https://github.com/smacke/subsync
sudo pip3 install git+https://github.com/jeorgen/align-videos-by-sound
The subtitle sync script processes files in the subtitle and video folder. The files in the subtitle folder can be videos with the desired subtitles, or raw subtitle files. Note that the fallback mode for subtitle sync using the audio tracks will not work with raw subtitle files. The script is fairly fast at processing, and has error handling that will by default not tolerate sync errors over 20 seconds. (You can change that in the script if you have something with a higher margin of error.)
subtitle-sync.sh:
#!/bin/bash
i=0
paste <(ls subtitle) <(ls video) | column -ts $'\t'
read -p "Please confirm the numbering is correct. [y/N] " verify
if [[ ! "$verify" =~ [yY] ]]
then
echo "Aborted!"
exit 1
fi
paste <(ls subtitle) <(ls video) | while IFS=$'\t' read -r s v
do
((i++))
out="$(sed -e 's/\.[^.]\+$/.mkv/g' <<< "$s")"
if [[ ! -e "$out" ]]
then
if [[ -e "$i.txt" ]]
then
rm "CHECKME_$out"
offset=$(cat "$i.txt")
else
ffmpeg -loglevel quiet -nostdin -y -i subtitle/"$s" subtmp.srt < /dev/null
offset=$(subsync video/"$v" -i subtmp.srt 2>&1 1>/dev/null | grep 'offset seconds' | sed 's/.*: /1000*/g' | bc | sed 's/\..*//g')
echo "Sync: $offset"
rm subtmp.srt
if [[ "${offset#-}" -gt "20000" ]]
then
echo "Sync using subtitles failed. Trying audio sync..."
offset=$(audio-transfer.py -print-offset-only video/"$v" subtitle/"$s")
echo "Sync: $offset"
if [[ "${offset#-}" -gt "20000" ]] || [[ "$offset" == "" ]]
then
echo "Sync failure!"
echo "Please write $i.txt"
echo "$i" >> failed.txt
out="CHECKME_$out"
offset=0
fi
fi
fi
mkvmerge -o "$out" -A -D -B -T -y "-1:$offset" subtitle/"$s" video/"$v"
fi
done
The audio subtitle sync script is much slower, but it is precise enough to sync audio files in most cases. Checking the result before replacing your files is highly encouraged. The script does not have a fallback mode, so I suggest checking to make sure the sync settings are reasonable on all the videos. The usage of this script is more complicated, as audio tracks do use more space, and removal of unwanted audio tracks may be desired. The usage is: audio-transfer.py [-copy-subtitles] [+lang,] [-all|-lang,] main_src audio_src destination (Eg: +eng,jpn adds audio from audio_src. -jpn removes audio from main_src. -all removes all audio from main_src.) An example usage would be to copy all English audio tracks from audio folder into tracks from video folder, using names from audio folder:
paste <(ls audio) <(ls video) | while IFS=$'\t' read -r a v; do audio-transfer.py +eng video/"$v" audio/"$a" "$(sed 's/\.[^.]\+$/.mkv/g' <<< "$a")" < /dev/null; done
audio-transfer.py:
#!/usr/bin/env python3
import sys
import subprocess
from align_videos_by_soundtrack.align import SyncDetector, cli_common
remove = []
remove_specified = False
remove_all = False
add = []
add_specified = False
copy_subtitles = False
files = []
print_offset_only = False
for argument in sys.argv[1:]:
if argument == "-copy-subtitles":
copy_subtitles = True
elif argument == "-print-offset-only":
print_offset_only = True
elif argument.startswith("-"):
if argument == "-all":
remove_all = True
else:
remove_specified = True
remove = argument[1:].split(",")
elif argument.startswith("+"):
add_specified = True
add = argument[1:].split(",")
else:
files.append(argument)
if print_offset_only:
main_src, audio_src = files
destination = None
else:
main_src, audio_src, destination = files
cli_common.logger_config()
with SyncDetector() as det:
result = det.align([audio_src, main_src])
offset = 0
if result[0]["trim"] > 0:
offset = int(-result[0]["trim"]*1000)
else:
offset = int(result[0]["pad"]*1000)
arguments = ["mkvmerge","-o",destination,"-D","-B","-T"]
if not copy_subtitles:
arguments.extend(["-S","-M"])
if add_specified:
arguments.extend(["-a",",".join(add)])
arguments.extend(["-y", "-1:" + str(offset), audio_src])
if remove_specified:
arguments.extend(["-a","!"+(",".join(remove))])
elif remove_all:
arguments.append("-A")
arguments.append(main_src)
if print_offset_only:
print(offset)
else:
print("Sync: {0}".format(offset))
subprocess.check_call(arguments)