Earily work on converting speech to Perl
[clearscm.git] / bin / simple_google_tts
1 #!/bin/bash
2
3 # NAME:         Simple Google TTS
4 # VERSION:      0.1
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
9 #
10 # LICENSE:      GNU GPLv3 (http://www.gnu.de/documents/gpl-3.0.en.html)
11 #
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.
19 #
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.
28 #
29 # USAGE:        simple_google_tts [-p|-g|-h] languagecode ['strings'|'file.txt']
30 #
31 #               please consult the README or the help output (-h) for more information
32
33 ############# GLOBVAR/PREP ###############
34
35 ScriptPath="$(readlink -f "$0")"
36 ScriptBase="$(basename "$0")"
37 ParentPath="${ScriptPath%/*}"
38 speakpl="$ParentPath/speak.pl"
39
40 TOP_PID="$$"
41 PidFile="/tmp/${0##*/}.pid"
42
43
44 ############### SETTINGS #################
45
46 Player="play"
47
48 ##############  DIALOGS  #################
49
50 Usage="\
51 $(basename "$0") [-p|-g|-h] languagecode ['strings'|'file.txt']
52
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
56
57     Selection of valid language codes: en, es, de...
58     Check speak.pl for a list of all valid codes
59
60     Warning: offline TTS only supports en, de, es, fr, it
61
62     If an instance of the script is already running it will be terminated.
63
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)\
66 "
67
68 GuiIcon="orca"
69 GuiTitle="Google TTS script"
70
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."
89
90 ############## FUNCTIONS #################
91
92 check_deps () {
93     for i in "$@"; do
94       type "$i" > /dev/null 2>&1 
95       if [[ "$?" != "0" ]]; then
96         MissingDeps+=" $i"
97       fi
98     done
99 }
100
101 check_environment () {
102     if [[ ! -f "$speakpl" && "$OptOffline" != "1" ]]; then
103       notify "$MsgErrNoSpeakpl"
104       OptOffline="1"
105     fi
106     check_deps sox perl
107     if [[ -n "$MissingDeps" ]]; then
108       notify "${MsgErrDeps}${MissingDeps}"
109       exit 1
110     fi
111 }
112
113 check_existing_instance(){
114   ExistingPID="$(cat "$PidFile" 2> /dev/null)"
115   if [[ -n "$ExistingPID" ]]; then
116     rm "$PidFile"
117     notify "$MsgInfoExistInstance"
118     kill -s TERM "$ExistingPID"
119     wait "$ExistingPID"
120   fi
121 }
122
123 arg_evaluate_options(){
124     # grab options if present
125     while getopts "gph" Options; do
126       case $Options in
127         g ) OptNotify="1"
128             ;;
129         p ) OptOffline="1"
130             ;;
131         h ) echo "$Usage"
132             exit 0
133             ;;
134        \? ) echo "$Usage"
135             exit 1
136             ;;
137       esac
138     done
139 }
140
141 arg_check_input(){
142   if [[ $# -eq 0 ]]; then
143     echo "$MsgErrNoLang"
144     echo "$Usage"
145     exit 1
146   elif [[ $# -eq 1 ]]; then
147     echo "$MsgInfoInpXsel"
148     InputMode="xsel"
149   elif [[ $# -eq 2 ]]; then
150     if [[ -f "$2" && -n "$(file --mime-type -b "$2" | grep text)" ]]; then
151       echo "$MsgInfoInpFile"
152       InputMode="file"
153     elif [[ ! -f "$2" ]]; then
154       echo "$MsgInfoInpString"
155       InputMode="string"
156     else
157       echo "$MsgErrInvalidInput"
158       echo "$Usage"
159       exit 1
160     fi
161   fi
162   LangCode="$1"
163   Input="$2"
164 }
165
166 notify(){
167   echo "$1"
168   if [[ "$OptNotify" = "1" ]]; then
169     notify-send -i "$GuiIcon" "$GuiTitle" "$1"
170   fi
171 }
172
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"
177     OptOffline="1"
178   fi
179 }
180
181 set_tts_mode(){
182   if [[ "$OptOffline" = "1" ]]; then
183     echo "$MsgInfoModePico"
184     tts_engine="tts_pico"
185     OutFile="out.wav"
186   else
187     echo "$MsgInfoModeGoogle"
188     tts_engine="tts_google"
189     OutFile="out.mp3"
190   fi
191 }
192
193 set_input_mode(){
194   if [[ "$InputMode" = "xsel" ]]; then
195     InputText="$(xsel)"
196   elif [[ "$InputMode" = "string" ]]; then
197     InputText="$Input"
198   elif [[ "$InputMode" = "file" ]]; then
199     InputText="$(cat "$Input")"
200   fi
201
202   # check if input is empty or only consists of whitespace
203   if [[ -z "${InputText//[[:space:]]/}" ]]; then
204     notify "$MsgErrInputEmpty"
205     exit 1
206   fi
207 }
208
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
214   
215   InputTextModded="$(echo "$InputText" | \
216     sed 's/\.$/|/g' | sed 's/^\s*$/|/g' | tr '\n' ' ' | tr '|' '\n')"
217
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
225
226   # insert trailing newline to allow for short text fragments
227   readarray TextSections < <(echo -e "$InputTextModded\n")
228
229   # subtract one section because of trailing newline
230   Sections="$((${#TextSections[@]} - 1))"
231
232   # TODO: find a more elegant way to handle short inputs
233 }
234
235 pico_synth(){
236   pico2wave --wave="$OutFile" --lang="$LangCode" "$1"
237 }
238
239 speakpl_synth(){
240   "$speakpl" "$LangCode" <(echo "$1") "$OutFile" > /dev/null 2>&1
241 }
242
243 tts_google(){
244   split_into_paragraphs
245   for i in "${!TextSections[@]}"; do
246     if [[ "$i" = "$Sections" ]]; then
247       echo "$MsgInfoDone"
248       [[ -n "$PlayerPID" ]] && wait "$PlayerPID"
249       break
250     else
251       echo "Processing $((i+1)) out of $Sections paragraphs"
252     fi
253     OutFile="out_$i.mp3"
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 &
262       $Player "$OutFile"
263       PlayerPID="$!"
264     else
265       echo "$MsgInfoSectionEmpty"
266       continue
267     fi
268   done
269 }
270
271 tts_pico(){
272   if [[ "$LangCode" = "en" ]]; then
273     LangCode="en-GB"
274   elif [[ "$LangCode" = "de" ]]; then
275     LangCode="de-DE"
276   elif [[ "$LangCode" = "es" ]]; then
277     LangCode="es-ES"
278   elif [[ "$LangCode" = "fr" ]]; then
279     LangCode="fr-FR"
280   elif [[ "$LangCode" = "it" ]]; then
281     LangCode="it-IT"
282   else 
283     echo "$MsgErrInvalidLang"
284     exit 1
285   fi
286   OutFile="out.wav"
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
293 }
294
295 cleanup(){
296   pkill -P "$TOP_PID"
297   [[ -n "$TmpDir" && -d "$TmpDir" ]] && rm -r "$TmpDir"
298   [[ -n "$PidFile" && -f "$PidFile" ]] && rm "$PidFile"
299 }
300
301 ############# INSTANCECHECK ##############
302
303 check_existing_instance
304
305 ############## USGCHECKS #################
306
307 arg_evaluate_options "$@"
308 shift $((OPTIND-1))
309 check_environment
310 arg_check_input "$@"
311 check_connectivity
312
313 ############### PREPWORK ##################
314
315 echo "$TOP_PID" > "$PidFile"
316
317 TmpDir="$(mktemp -d "/tmp/${0##*/}.XXXXXX")"
318 cd "$TmpDir"
319
320 trap "cleanup; exit" EXIT
321
322 ################ MAIN ####################
323
324 set_tts_mode
325 set_input_mode
326 notify "$MsgInfoSynthesize"
327 "$tts_engine"