3 # NAME: Simple Google TTS
5 # AUTHOR: (c) 2014 - 2016 Glutanimate <https://github.com/Glutanimate/>
6 # DESCRIPTION: Wrapper script for Michal Fapso's speak.pl Google TTS script
7 # DEPENDENCIES: - wrapper: xsel libttspico0 libttspico-utils libttspico-data libnotify-bin
8 # - speak.pl: libwww-perl libwww-mechanize-perl libhtml-tree-perl sox libsox-fmt-mp3
10 # LICENSE: GNU GPLv3 (http://www.gnu.de/documents/gpl-3.0.en.html)
12 # NOTICE: THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
13 # EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
14 # PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR
15 # IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
16 # AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND
17 # PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE,
18 # YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
20 # IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY
21 # COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS
22 # PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL,
23 # INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE
24 # THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED
25 # INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE
26 # PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER
27 # PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
29 # USAGE: simple_google_tts [-p|-g|-h] languagecode ['strings'|'file.txt']
31 # please consult the README or the help output (-h) for more information
33 ############# GLOBVAR/PREP ###############
35 ScriptPath="$(readlink -f "$0")"
36 ScriptBase="$(basename "$0")"
37 ParentPath="${ScriptPath%/*}"
38 speakpl="$ParentPath/speak.pl"
41 PidFile="/tmp/${0##*/}.pid"
44 ############### SETTINGS #################
48 ############## DIALOGS #################
51 $(basename "$0") [-p|-g|-h] languagecode ['strings'|'file.txt']
53 -p: use offline TTS (pico2wave) instead of Google's TTS system
54 -g: activate gui notifications (via notify-send)
55 -h: display this help section
57 Selection of valid language codes: en, es, de...
58 Check speak.pl for a list of all valid codes
60 Warning: offline TTS only supports en, de, es, fr, it
62 If an instance of the script is already running it will be terminated.
64 If you don't provide an input string or input file, $(basename "$0")
65 will read from the X selection (current/last highlighted text)\
69 GuiTitle="Google TTS script"
71 MsgErrNoSpeakpl="Error: speak.pl not found. Falling back to offline playback."
72 MsgErrDeps="Error: missing dependencies. Couldn't find:"
73 MsgInfoExistInstance="Aborting synthesis and playback of existing script instance"
74 MsgErrNoLang="Error: No language code provided."
75 MsgInfoInpXsel="Reading from X selection."
76 MsgInfoInpFile="Reading from text file."
77 MsgInfoInpString="Reading from string."
78 MsgErrInvalidInput="Error: Invalid input (file might not be a text file)."
79 MsgInfoConnOff="No internet connection."
80 MsgInfoModePico="Using pico2wave for TTS synthesis."
81 MsgInfoModeGoogle="Using Google for TTS synthesis."
82 MsgErrInvalidLang="Error: Offline TTS via pico2wave only supports the .\
83 following languages: en, de, es, fr, it."
84 MsgErrInputEmpty="Error: Input empty."
85 MsgInfoSynthesize="Synthesizing virtual speech."
86 MsgInfoPlayback="Playing synthesized speech"
87 MsgInfoSectionEmpty="Skipping empty paragraph"
88 MsgInfoDone="All sections processed. Waiting for playback to finish."
90 ############## FUNCTIONS #################
94 type "$i" > /dev/null 2>&1
95 if [[ "$?" != "0" ]]; then
101 check_environment () {
102 if [[ ! -f "$speakpl" && "$OptOffline" != "1" ]]; then
103 notify "$MsgErrNoSpeakpl"
107 if [[ -n "$MissingDeps" ]]; then
108 notify "${MsgErrDeps}${MissingDeps}"
113 check_existing_instance(){
114 ExistingPID="$(cat "$PidFile" 2> /dev/null)"
115 if [[ -n "$ExistingPID" ]]; then
117 notify "$MsgInfoExistInstance"
118 kill -s TERM "$ExistingPID"
123 arg_evaluate_options(){
124 # grab options if present
125 while getopts "gph" Options; do
142 if [[ $# -eq 0 ]]; then
146 elif [[ $# -eq 1 ]]; then
147 echo "$MsgInfoInpXsel"
149 elif [[ $# -eq 2 ]]; then
150 if [[ -f "$2" && -n "$(file --mime-type -b "$2" | grep text)" ]]; then
151 echo "$MsgInfoInpFile"
153 elif [[ ! -f "$2" ]]; then
154 echo "$MsgInfoInpString"
157 echo "$MsgErrInvalidInput"
168 if [[ "$OptNotify" = "1" ]]; then
169 notify-send -i "$GuiIcon" "$GuiTitle" "$1"
173 check_connectivity(){
174 if ! ping -q -w 1 -c 1 \
175 "$(ip r | grep default | cut -d ' ' -f 3)" > /dev/null; then
176 echo "$MsgInfoConnOff"
182 if [[ "$OptOffline" = "1" ]]; then
183 echo "$MsgInfoModePico"
184 tts_engine="tts_pico"
187 echo "$MsgInfoModeGoogle"
188 tts_engine="tts_google"
194 if [[ "$InputMode" = "xsel" ]]; then
196 elif [[ "$InputMode" = "string" ]]; then
198 elif [[ "$InputMode" = "file" ]]; then
199 InputText="$(cat "$Input")"
202 # check if input is empty or only consists of whitespace
203 if [[ -z "${InputText//[[:space:]]/}" ]]; then
204 notify "$MsgErrInputEmpty"
209 split_into_paragraphs(){
210 # Newlines aren't reliable indicators of paragraph breaks
211 # (e.g.: PDF files where each line ends with a newline).
212 # Instead we look for lines ending with a full stop and divide
213 # our text input into sections based on that
215 InputTextModded="$(echo "$InputText" | \
216 sed 's/\.$/|/g' | sed 's/^\s*$/|/g' | tr '\n' ' ' | tr '|' '\n')"
218 # - first sed command: replace end-of-line full stops with '|' delimiter
219 # - second sed command: replace empty lines with same delimiter (e.g.
220 # to separate text headings from text)
221 # - subsequent tr commands: remove existing newlines; replace delimiter with
222 # newlines to prepare for readarray
223 # TODO: find a more elegant and secure way to split the text by
224 # multi-character/regex patterns
226 # insert trailing newline to allow for short text fragments
227 readarray TextSections < <(echo -e "$InputTextModded\n")
229 # subtract one section because of trailing newline
230 Sections="$((${#TextSections[@]} - 1))"
232 # TODO: find a more elegant way to handle short inputs
236 pico2wave --wave="$OutFile" --lang="$LangCode" "$1"
240 "$speakpl" "$LangCode" <(echo "$1") "$OutFile" > /dev/null 2>&1
244 split_into_paragraphs
245 for i in "${!TextSections[@]}"; do
246 if [[ "$i" = "$Sections" ]]; then
248 [[ -n "$PlayerPID" ]] && wait "$PlayerPID"
251 echo "Processing $((i+1)) out of $Sections paragraphs"
254 SectionText="${TextSections[$i]}"
255 if [[ -n "${SectionText//[[:space:]]/}" ]]; then
256 speakpl_synth "${TextSections[$i]}"
257 [[ -n "$PlayerPID" ]] && wait "$PlayerPID"
258 [[ -f "out_$((i-1)).mp3" ]] && rm "out_$((i-1)).mp3"
259 echo "$MsgInfoPlayback $((i+1))"
260 echo "Playing $OutFile"
261 #$Player "$OutFile" > /dev/null 2>&1 &
265 echo "$MsgInfoSectionEmpty"
272 if [[ "$LangCode" = "en" ]]; then
274 elif [[ "$LangCode" = "de" ]]; then
276 elif [[ "$LangCode" = "es" ]]; then
278 elif [[ "$LangCode" = "fr" ]]; then
280 elif [[ "$LangCode" = "it" ]]; then
283 echo "$MsgErrInvalidLang"
287 # pico2wave handles long text inputs and
288 # fixed formatting line-breaks well enough on its own.
289 # no need to use split_into_paragraphs()
290 pico_synth "$InputText"
291 echo "$MsgInfoPlayback"
292 $Player "$OutFile" > /dev/null 2>&1
297 [[ -n "$TmpDir" && -d "$TmpDir" ]] && rm -r "$TmpDir"
298 [[ -n "$PidFile" && -f "$PidFile" ]] && rm "$PidFile"
301 ############# INSTANCECHECK ##############
303 check_existing_instance
305 ############## USGCHECKS #################
307 arg_evaluate_options "$@"
313 ############### PREPWORK ##################
315 echo "$TOP_PID" > "$PidFile"
317 TmpDir="$(mktemp -d "/tmp/${0##*/}.XXXXXX")"
320 trap "cleanup; exit" EXIT
322 ################ MAIN ####################
326 notify "$MsgInfoSynthesize"