#!/usr/bin/env bash debug="1" padding="10" piper_bin="/opt/bin/piper/piper/piper" # The location of the text to speech engine piper_voice="/opt/bin/piper/piper/piper-voices/en/en_US/lessac/medium/en_US-lessac-medium.onnx" silence_mp3="silence.mp3" source_volume="0" srt_volume="0" if [ "${debug}" -ne "1" ] then ffmpeg_debug="-hide_banner -loglevel error" else ffmpeg_debug="" fi ################################################# function echo_error() { echo -e "ERROR: $@" 1>&2 display_help_and_exit exit 1 } ################################################# function echo_debug() { if [ "${debug}" == "2" ] then echo -e "INFO: $@" 1>&2 fi } ################################################# function echo_status() { if [ "${debug}" -le "1" ] then echo -ne "\r$@" 1>&2 else echo -e "INFO: $@" 1>&2 fi } ################################################# function echo_info() { if [ "${debug}" != "0" ] then echo "INFO: $@" 1>&2 fi } ################################################# function display_help_and_exit() { echo_debug "For more information see " exit 1 } ################################################# function check_variable() { for variable in "$@" do if [[ -z ${!variable+x} ]] then # indirect expansion here echo_error "The variable \"${variable}\" is missing." else echo_debug "The variable \"${variable}\" is set to \"${!variable}\".~" fi done } ################################################# function timestamp_to_sec() { this_timestamp="${1}" this_in_sec="$( echo "${this_timestamp}" | sed 's/,/./g' | awk -F ":" '{ printf ($1 * 3600) + ($2 * 60) + $3, $0}' )" echo "${this_in_sec}" } ################################################# function timestamp_to_millisec() { printf '%.*f\n' 0 $( echo "$( timestamp_to_sec "${1}" ) * 1000" | bc --mathlib ) } ################################################# function update_playhead() { play_head="$( mediainfo --full --Output=JSON "${this_srt_wav_file}" | jq --raw-output '.media.track | .[] | select(."@type"=="Audio") | .Duration_String3' )" echo_status "Play head: ${play_head} .. ${movie_duration}" play_head_seconds="$( timestamp_to_sec "${play_head}" )" play_head_milliseconds="$( timestamp_to_millisec "${play_head}" )" } ################################################# function get_movie_duration() { movie_duration="$( mediainfo --full --Output=JSON "${this_movie_file}" | jq --raw-output '.media.track | .[] | select(."@type"=="Audio") | .Duration_String3' )" movie_duration_seconds="$( timestamp_to_sec "${movie_duration}" )" movie_duration_milliseconds="$( timestamp_to_millisec "${movie_duration}" )" } ################################################# function check_movie() { check_variable this_movie_file if [ ! -s "${this_movie_file}" ] then echo_error "The \"this_movie_file\" file is missing." fi if [ "$( file --brief --mime-type "${this_movie_file}" | grep --count --perl-regexp 'video/mp4|video/x-matroska' )" -ne "1" ] then echo_error "The \"this_movie_file\" variable has not a valid \"video/mp4\" or \"video/x-matroska\" mime type." fi get_movie_duration check_variable movie_duration } ################################################# function check_srt() { check_variable this_srt_file movie_duration if [ ! -s "${this_srt_file}" ] then echo_debug "No srt found trying: ffmpeg -y -hide_banner -loglevel error -txt_format text -i \"${this_movie_file}\" \"${this_srt_file}\"" ffmpeg -y -hide_banner -loglevel error -txt_format text -i "${this_movie_file}" "${this_srt_file}" check_srt this_srt_file fi if [ "$( file --brief --mime-type "${this_srt_file}" | grep --count 'application/x-subrip' )" -ne "1" ] then echo_error "The \"this_srt_file\" variable has not a valid \"application/json\" mime type." fi cp "${this_srt_file}" "${this_srt_temp_file}" 1>&2 if [ ! -s "${this_srt_temp_file}" ] then echo_error "The temp file is missing \"${this_srt_temp_file}\".~" fi if [ "$( file "${this_srt_temp_file}" | grep --count 'CRLF' )" -eq "1" ] then dos2unix "${this_srt_temp_file}" 1>&2 fi python3 -c "from bs4 import BeautifulSoup; print(BeautifulSoup(open('${this_srt_temp_file}', 'r').read(), 'html.parser').get_text())" | sponge "${this_srt_temp_file}" if [ "$( grep -c "⋄⋄⋄ Fin ⋄⋄⋄" "${this_srt_temp_file}" )" -eq "0" ] then last_srt_index="$( tail "${this_srt_temp_file}" | grep --perl-regexp '^[0-9]+$' | tail -1 )" echo "" >> "${this_srt_temp_file}" echo $(( ${last_srt_index} + 1 )) >> "${this_srt_temp_file}" echo "$( \date --date="${movie_duration} today - 2 seconds" +%H:%M:%S,000 ) --> $( \date --date="${movie_duration} today - 1 seconds" +%H:%M:%S,000 )" >> "${this_srt_temp_file}" echo "The End." >> "${this_srt_temp_file}" echo "" >> "${this_srt_temp_file}" echo $(( ${last_srt_index} + 2 )) >> "${this_srt_temp_file}" echo "$( \date --date="${movie_duration} today - 1 seconds" +%H:%M:%S,000 ) --> $( \date --date="${movie_duration} today" +%H:%M:%S,000 )" >> "${this_srt_temp_file}" echo "⋄⋄⋄ Fin ⋄⋄⋄" >> "${this_srt_temp_file}" fi } ################################################# function check_mp3() { if [ ! -s "${1}" ] then echo_error "The audio file \"${1}\" is missing." fi if [ "$( file --brief --mime-type "${1}" | grep --count 'audio/mpeg' )" -ne "1" ] then echo_error "The file \"${1}\" has not a valid \"audio/mpeg\" mime type." fi } ################################################# function check_wav() { if [ ! -s "${1}" ] then echo_error "The audio file \"${1}\" is missing." fi if [ "$( file --brief --mime-type "${1}" | grep --count 'audio/x-wav' )" -ne "1" ] then echo_error "The file \"${1}\" has not a valid \"audio/x-wav\" mime type." fi } ################################################# function fill_silence() { # ffmpeg -y -hide_banner -loglevel erroroptions # -f fmt (input/output) Force input or output file format. # -i url (input) input file url # -t duration When used as an output option (before an output url), stop writing the output after its duration reaches duration. # -q:a Set the audio quality (codec-specific, VBR). This is an alias for subtitle_start="${1}" if [ ! -s "${this_srt_wav_file}" ] then echo_debug "Creating the audio track \"${this_srt_wav_file}\".~" ffmpeg -y -hide_banner -loglevel error -f lavfi -i anullsrc=r=22050:cl=mono -t ${subtitle_start} -q:a 9 -acodec pcm_s16le "${this_srt_wav_file}" 1>&2 else subtitle_start_millisec="$( timestamp_to_millisec "${subtitle_start}" )" subtitle_start_sec="$( timestamp_to_sec "${subtitle_start}" )" if [ "${play_head_milliseconds}" -lt "${subtitle_start_millisec}" ] then silence_to_add="$( echo "${subtitle_start_sec} ${play_head_seconds}" | awk '{ printf ($1 - $2), $0}' )" echo_debug "Adding \"${silence_to_add}\" seconds to the audio track \"${this_srt_wav_file}\".~" ffmpeg -y -hide_banner -loglevel error -f lavfi -i anullsrc=r=22050:cl=mono -t "${silence_to_add}" -q:a 9 -acodec pcm_s16le "${silence_wav}" 1>&2 if [ ${?} -ne "0" ] then echo_debug "ffmpeg -y -hide_banner -loglevel error -f lavfi -i anullsrc=r=22050:cl=mono -t \"${silence_to_add}\" -q:a 9 -acodec pcm_s16le \"${silence_wav}\"" 1>&2 fi sox -V2 "${this_srt_wav_file}" "${silence_wav}" "${this_srt_wav_file%.*}_combined.wav" mv "${this_srt_wav_file%.*}_combined.wav" "${this_srt_wav_file}" 1>&2 rm "${silence_wav}" 1>&2 else echo_debug "There is a overlap at \"${subtitle_start}\".~" fi fi check_wav "${this_srt_wav_file}" } ################################################# function segment_to_speech() { this_index="${1}" this_in="${2}" this_out="${3}" this_segment_text="${4}" this_in_sec="$( timestamp_to_sec "${this_in}" )" this_out_sec="$( timestamp_to_sec "${this_out}" )" this_in_millisec="$( timestamp_to_millisec "${this_in}" )" this_out_millisec="$( timestamp_to_millisec "${this_out}" )" if [ "${this_out_millisec}" -lt "${this_in_millisec}" ] then echo_error "The timing on segment ${this_index} is incorrect, as the end \"${this_out}\" cannot be before the start \"${this_in}\".~" fi if [ "${play_head_milliseconds}" -lt "${this_in_millisec}" ] then echo_debug "Adding space as the playhead \"${play_head_milliseconds}\" is behind the subtitles \"${this_in_millisec}\"" fill_silence "${this_in}" #TODO calculate difference, Pass to new function which will create that lenght of silence and add to it. fi # this_segment_duration_sec="$( awk "BEGIN{print ${this_out_sec}-${this_in_sec}}" )" # this_segment_duration_millisec="$( printf '%.*f\n' 0 $( echo "${this_segment_duration_sec} * 1000" | bc --mathlib ) )" echo "${this_segment_text}" | "${piper_bin}" --model "${piper_voice}" --sentence_silence 0 --output_file "${this_srt_wav_file_next_line}" > /dev/null 2>&1 # check_wav "${this_srt_wav_file_next_line}" sox -V2 "${this_srt_wav_file}" "${this_srt_wav_file_next_line}" "${this_srt_wav_file%.*}_combined.wav" # check_wav "${this_srt_wav_file%.*}_combined.wav" mv "${this_srt_wav_file%.*}_combined.wav" "${this_srt_wav_file}" 1>&2 update_playhead rm "${this_srt_wav_file_next_line}" } ################################################# function create_audio() { first_subtitle_time_stamp="$( head "${this_srt_temp_file}" | grep --perl-regexp ' --> ' | head -1 | awk -F ' --> ' '{print $1}' | sed 's/,/./g' )" check_variable first_subtitle_time_stamp fill_silence "${first_subtitle_time_stamp}" padding_zero="$( echo $(( $( tail "${this_srt_temp_file}" | grep --perl-regexp '^[0-9]+$' | tail -1 ) + 1 )) | wc --chars )" cat "${this_srt_temp_file}" | while read this_line do if [ "$( echo "${this_line}" | grep --count --perl-regexp '^$' )" -eq "1" ] then continue # Ignore Blank Lines fi if [ "$( echo "${this_line}" | grep --count --perl-regexp '^[0-9]+$' )" -eq "1" ] then # Entering a new segment # If there are no variables set then this is the first segment if [ ! -z "${segment_start}" ] then segment_index="$(printf %0${padding_zero}d $(( ${this_line} - 1 )) )" echo_debug "Generating speech for segment ${segment_index}: \"${segment_text}\"" segment_to_speech "${segment_index}" "${segment_start}" "${segment_end}" "${segment_text}" fi # Now reset the variables segment_start="" segment_end="" segment_text="" update_playhead continue # Index line fi if [ "$( echo "${this_line}" | grep --count ' --> ' )" -eq "1" ] then segment_start="$( echo "${this_line}" | awk -F ' --> ' '{print $1}' | sed 's/,/./g' )" segment_end="$( echo "${this_line}" | awk -F ' --> ' '{print $2}' | sed 's/,/./g' )" continue fi # Combine multi line segment_text="${segment_text} ${this_line}" done } ################################################# # Main for this_movie_file in $@ do echo "Processing \"${this_movie_file}\"" check_movie this_movie_file this_movie_file_voice_over="${this_movie_file%.*}_voice_over.mkv" this_movie_original_mp3_file=".~${this_movie_file%.*}_original.mp3" this_movie_new_mp3_file=".~${this_movie_file%.*}_mixed.mp3" this_srt_file="${this_movie_file%.*}.srt" this_srt_temp_file=".~${this_srt_file%.*}_temp.srt" check_srt this_srt_file this_srt_wav_file=".~${this_srt_file%.*}_srt.wav" this_srt_wav_file_next_line="${this_srt_temp_file%.*}_next_line.wav" silence_wav=".~silence.wav" play_head="00:00:00.000" play_head_seconds="0" play_head_milliseconds="0" if [ ! -e "${this_srt_wav_file}" ] then echo_info "Creating the new media" create_audio fi echo "" echo_info "Extracting the original sound track from \"${this_movie_file}\" to \"${this_movie_original_mp3_file}\"" ffmpeg -y -hide_banner -loglevel error -txt_format text -i "${this_movie_file}" "${this_movie_original_mp3_file}" if [ "${source_volume}" != "0" ] then echo_info "Changing volume of movie \"${this_movie_file}\" to \"${this_movie_original_mp3_file}\"" sox -v ${source_volume} "${this_movie_original_mp3_file}" ".~${this_movie_original_mp3_file}" mv -v ".~${this_movie_original_mp3_file}" "${this_movie_original_mp3_file}" fi if [ "${srt_volume}" != "0" ] then echo_info "Changing volume of subtitle audio \"${this_movie_file}\" to \"${this_movie_original_mp3_file}\"" sox -v ${srt_volume} "${this_srt_wav_file}" ".~${this_srt_wav_file}" mv -v ".~${this_srt_wav_file}" "${this_srt_wav_file}" fi echo_info "Combining \"${this_movie_original_mp3_file}\" and \"${this_srt_wav_file}\" into a new track \"${this_movie_new_mp3_file}\"" ffmpeg -y -hide_banner -loglevel error -txt_format text -i "${this_movie_original_mp3_file}" -i "${this_srt_wav_file}" -filter_complex "[0:a][1:a]amerge=inputs=2[a]" -map "[a]" -ac 2 "${this_movie_new_mp3_file}" echo_info "Adding \"${this_movie_new_mp3_file}\" to \"${this_movie_file}\" to give \"${this_movie_file_voice_over}\"" ffmpeg -y -hide_banner -loglevel error -txt_format text -i "${this_movie_file}" -i "${this_movie_new_mp3_file}" -map 0 -map 1:a -c:v copy "${this_movie_file_voice_over}" if [ "${debug}" -eq "0" ] then rm "${this_movie_original_mp3_file}" 1>&2 rm "${this_srt_temp_file}" 1>&2 rm "${this_srt_wav_file}" 1>&2 rm "${this_movie_new_mp3_file}" 1>&2 fi cp -v "${this_srt_file}" "${this_srt_file%.*}_voice_over.srt" echo_info "Finished generating \"${this_movie_file_voice_over}\"" done