#!/bin/bash # piperread # txt to speech using local # txt2speach called piper # based on awsread # needs piper, aplay, xclip?, md5sum, awk # usage: # piperread # will read from clipboard # piperread /path/to/file.txt # will read from file # config piperpath="$HOME/piper" # where is the piper exe if [ -d "$piperpath" ] ; then PATH="$piperpath:$PATH" fi voicespath="$HOME/piper" # where are the onnx and json files resume="please" # non-empty is true #keepAudio="" # non-empty is true < disabled # split text at (max:3000) bytes="600" # text display width in chars (50-80) width="50" # center text block centertext="mkay" # non-empty is true # storeroot storeroot="$HOME/tmp/recPIPERREAD" mkdir -p "$storeroot" || exit # debug 1 is true debug="0" # end config # checks command -v piper >/dev/null 2>&1 || { echo "I need piper on path, exiting." >&2; exit 1; } command -v awk >/dev/null 2>&1 || { echo "I need awk, exiting." >&2; exit 1; } command -v mpv >/dev/null 2>&1 || { echo "I need mpv, exiting." >&2; exit 1; } command -v ffmpeg >/dev/null 2>&1 || { echo "I need ffmpeg, exiting." >&2; exit 1; } command -v md5sum >/dev/null 2>&1 || { echo "I need md5sum, exiting." >&2; exit 1; } command -v jq >/dev/null 2>&1 || { echo "I need jq for json stuff, exiting." >&2; exit 1; } # loop over onnx files and populate array # Populate the array with .onnx files populateVoicesArray () { pushd "$voicespath" >/dev/null || { echo "$voicespath does not exist."; exit 1;} voices=(*.onnx) # populate array (( ${#voices[@]} )) || { echo "No voices (*.onnx) found in $voicespath"; exit 1; } popd >/dev/null || exit } storeInit () { # hash based on file path hash="$(readlink -f "$input" | md5sum | awk '{print $1}')" (( debug )) && echo "$hash" store="$storeroot/${hash}/" mkdir -p "$store" || exit touch "$store" || exit } resumeSave () { if [[ "$base" != "clipboard" ]]; then echo "$part" > "$store/resume" echo "$base" > "$store/name" echo "$fullpath" >> "$store/name" fi } # padding to center calcPad() { if [[ -n $centertext ]]; then columns="$(tput cols)" pad="$((( columns - width )/2))" else pad="0" fi } # calc percentage (will need perc2 script to actually display that?) perc () { local tmp tmp="$(bc -l <<< "$1/$2*100")" awk 'BEGIN{printf "%."'"2"'"f\n", "'"$tmp"'"}' } help () { cat << EOF ╭────────────────────────────────────────────────────────────────────────────────────────────────╮ │ │ │ ███ ███ ███ ███ ███ ███ ███ █ ██ │ │ ███ █ ███ ██ █ █ ██ ███ █ █ │ │ █ ███ █ ███ █ █ ███ █ █ ██ │ │ │ │ piperread, txt to speech │ │ needs piper, mpv, xclip?, md5sum, awk, jq │ │ │ │ usage: │ │ piperread # will read from clipboard │ │ piperread file.txt # will read from file │ │ piperread --goto 100 file.txt # or -g goto page 100, will set resume to this new entry │ │ piperread --list # or -l list over store │ │ piperread --hash ab # or -x will read first file that includes those letters in hash │ │ piperread --random # or -r read random page │ │ │ │ interactive keys: │ │ n # skips to next section (quits curently playing mpv) │ │ ctrl+c # press for exit and to get the normal cursor back │ │ ctrl+z # suspend/stop playback, return with fg │ │ │ │ config note: │ │ 'resume' is enabled by default and should work for files only, not for clipboard. │ ╰────────────────────────────────────────────────────────────────────────────────────────────────╯ EOF } # switches # help && exit if [ "$1" == "--help" ] || [ "$1" == "-h" ]; then # print help help exit fi # goto if [ "$1" == "--goto" ] || [ "$1" == "-g" ]; then # goto chunk shift jumpto="$1" # assuming --goto 120 shift fi # list && exit if [ "$1" == "--list" ] || [ "$1" == "-l" ]; then # list stored resumes/names ... cd "$storeroot" || exit # sort by date horror find . -type f -iname "name" -printf "%T@\t%Tc %h\n" | sort -n | cut -d " " -f 8- |\ while read -r path; do name="$(tail -1 "$path/name")" from="$(cat "$path/resume")" printf -v from "% 4d" "$from" # pad with spaces path="$(basename "$path")" #&& path="${path:0:8}..." # clean and shorten path/hash echo -e "$path\t$from\t$name" done exit fi # random if [ "$1" == "--random" ] || [ "$1" == "-r" ]; then # goto random page random="1" # 1 is true (( debug )) && echo "Random page requested" shift # assuming --random file.txt fi # hash if [ "$1" == "--hash" ] || [ "$1" == "-x" ]; then # find file by hash shift search="$1" cd "$storeroot" || exit cd -- "$(find . -name "*$search*" -type d | tail -1)" || exit input="$(cat name | tail -1)" || exit fi # else just path to file if [ -f "$1" ]; then input="$1" fi (( debug )) && echo "input 1st time: $input" # start # assuming we have a valid $input file path here, lets check again if [ -f "$input" ]; then echo "$input" fullpath="$(readlink -f "$input")" (( debug )) && echo "$fullpath" # else its probably clipboard, checked again later in the code ˇ fi # storeInit storeInit # gets done in any case # hide cursor tput civis cleanup () { (( debug )) && echo "$?" [ -n "$tmp" ] && rm -fr "$tmp" [ -f "$storeroot/mpv_keybindings.conf" ] && rm -fr "$storeroot/mpv_keybindings.conf" tput cnorm exit 0 } # tmp dir tmp="/tmp/$RANDOM-$$-piperread" trap cleanup EXIT SIGTERM SIGINT # https://www.gnu.org/software/libc/manual/html_node/Termination-Signals.html mkdir -m 700 "$tmp" || { echo '!! unable to create a tmp dir' >&2; tmp=; exit 1; } # named pipe # pipe="$tmp/pipe" # if [[ ! -p $pipe ]]; then # mkfifo $pipe # fi # play # play (){ # if [[ -n $keepAudio ]]; then # name="$(($(date +%s%N)/1000000))" # lets name parts by epoch miliseconds # mpv --no-resume-playback --msg-level=all=no --no-video --record-file="${store}${name}.wav" "$1" # else # mpv --no-resume-playback --msg-level=all=no --no-video "$1" # fi # } # read clipboard or file (assuming text) if [ "$#" -eq "0" ] then # store clipboard to tmp (assuming text) xclip -selection clipboard -o > "$tmp/some.txt" 2>/dev/null || powershell.exe Get-clipboard > "$tmp/some.txt" || exit 1 base="clipboard" else # file (( debug )) && echo "input: $input" [[ -f "$input" ]] || { echo "no input found"; exit 1; } cp "$input" "$tmp/some.txt" # base baseext=${input##*/} # file.ext base="${baseext%.*}" # file # resume if [[ -n $resume ]] && ((! random )); then if [[ -n $jumpto ]]; then # jumpto is set, assuming --goto was used echo "Goto $jumpto" elif [[ -f "$store/resume" ]]; then jumpto="$(cat "$store/resume")" else jumpto="1" fi (( jumpto > 1 )) && echo "Resume from part $jumpto" fi fi # tr remove newlines, sed add newlines where punctuations are, # sed remove double spaces, split by some bytes, keep lines cd "$tmp" || exit cat "some.txt" | tr '\r\n' ' ' | sed 's/[.!?] */&\n/g' | sed 's/ \{1,\}/ /g' | split --line-bytes="${bytes}" || exit 1 # count generated files all="$(find . -type f -name "x*" | wc -l)" (( random )) && randomPage="$(( RANDOM % all ))" (( debug )) && echo "all $all, randomPage $randomPage" # mpv custom keybindings, idea is to disable all but 'q' mpvCustomKeys () { cat << EOF > "$storeroot/mpv_keybindings.conf" n quit EOF } # read part="0" for file in x*; do populateVoicesArray # so the *.onnx voices can be add or deleted/renamed during playback ((part=part+1)) # random if (( random )); then (( part < randomPage )) && continue resume="" # just to skip next section fi # resume if [[ -n $resume ]]; then (( part < jumpto )) && continue fi # exit if there is no such file [[ -f $file ]] || exit (( random )) || resumeSave # store position if random is false calcPad # How much to pad to center block # get random voice rand="(($RANDOM % ${#voices[@]}))" # get random speaker (some models have more than 1 spekaer) num_speakers="$(jq '.num_speakers' "${voicespath}/${voices[$rand]}".json)" || { echo "no voice json found"; exit 1; } speaker="$(($RANDOM % num_speakers))" # head is around here i guess echoBlock (){ echo # print header and text part (( all > 1 )) && echo "(${part}/${all})" cat "$file" | fmt -w ${width} | sed 's/ \{1,\}/ /g' echo "─── (voice ${voices[$rand]}/$speaker) ─── ${perc}%" } perc="$( perc "$part" "$all" )" echoBlock | pr -T -o "$pad" # perc2 "$perc" # < to much stuff on screen # replace text that is ofen read wrong, like 'dr.' sed -i 's/Dr\./Doctor/' "$file" sed -i 's/Mr\./Mister/' "$file" sed -i 's/Ms\./Miss/' "$file" # synth # working aplay line #cat "$file" | piper -m "${voicespath}/${voices[$rand]}" --output-raw 2>/dev/null | aplay -i -r 22050 -f S16_LE -t raw - || exit # working mpv line ! (with trouble when mpv is paused, sometimes it can't continue and all hell ...) #cat "$file" | piper -s "$speaker" -m "${voicespath}/${voices[$rand]}" --output-raw 2>/dev/null | mpv --demuxer=rawaudio --demuxer-rawaudio-format=s16le --demuxer-rawaudio-rate=22050 --audio-samplerate=22050 --demuxer-rawaudio-channels=1 --no-resume-playback --msg-level=all=no --no-video - || exit 1 # working mpv line ! (with disabled mpv keybindings, replaced with custom ones in function mpvCustomKeys) mpvCustomKeys # lets just keep the n to quit mpv (skip to next segment) piper -s "$speaker" -m "${voicespath}/${voices[$rand]}" --output-raw < "$file" 2>/dev/null | mpv --demuxer=rawaudio --demuxer-rawaudio-format=s16le --demuxer-rawaudio-rate=22050 --audio-samplerate=22050 --demuxer-rawaudio-channels=1 --no-resume-playback --no-video --no-input-default-bindings --input-conf="$storeroot/mpv_keybindings.conf" --msg-level=all=no - 2>/dev/null || exit 1 # working ffmpeg/mpv line (with trouble when mpv is paused) #cat "$file" | piper -m "${voicespath}/${voices[$rand]}" --output-raw 2>/dev/null | ffmpeg -vn -f s16le -ar 22050 -ac 1 -i - -f wav - 2>/dev/null | mpv --no-resume-playback --msg-level=all=no --no-video - 2>/dev/null || exit 1 # logging #cat "$file" | piper -m "${voicespath}/${voices[$rand]}" --output-raw 2>> $HOME/pipererr.txt | ffmpeg -vn -f s16le -ar 22050 -ac 1 -i - -f wav - 2>> $HOME/ffmpegerr.txt | mpv --no-resume-playback --msg-level=all=no --no-input-default-bindings --no-video - 2>> $HOME/mpverr.txt || exit 1 # testing with --no-input-default-bindings ^ done