source: lliurex-tts/trunk/fuentes/simple-google-tts/simple_google_tts @ 4755

Last change on this file since 4755 was 4755, checked in by joamuran, 2 years ago

added festival capabilities

  • Property svn:executable set to *
File size: 9.8 KB
Line 
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
35ScriptPath="$(readlink -f "$0")"
36ScriptBase="$(basename "$0")"
37ParentPath="${ScriptPath%/*}"
38speakpl="$ParentPath/speak.pl"
39
40TOP_PID="$$"
41PidFile="/tmp/${0##*/}.pid"
42
43
44############### SETTINGS #################
45
46Player="play"
47
48##############  DIALOGS  #################
49
50Usage="\
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
68GuiIcon="orca"
69GuiTitle="Google TTS script"
70
71MsgErrNoSpeakpl="Error: speak.pl not found. Falling back to offline playback."
72MsgErrDeps="Error: missing dependencies. Couldn't find:"
73MsgInfoExistInstance="Aborting synthesis and playback of existing script instance"
74MsgErrNoLang="Error: No language code provided."
75MsgInfoInpXsel="Reading from X selection."
76MsgInfoInpFile="Reading from text file."
77MsgInfoInpString="Reading from string."
78MsgErrInvalidInput="Error: Invalid input (file might not be a text file)."
79MsgInfoConnOff="No internet connection."
80MsgInfoModePico="Using pico2wave for TTS synthesis."
81MsgInfoModeGoogle="Using Google for TTS synthesis."
82MsgErrInvalidLang="Error: Offline TTS via pico2wave only supports the .\
83following languages: en, de, es, fr, it."
84MsgErrInputEmpty="Error: Input empty."
85MsgInfoSynthesize="Synthesizing virtual speech."
86MsgInfoPlayback="Playing synthesized speech"
87MsgInfoSectionEmpty="Skipping empty paragraph"
88MsgInfoDone="All sections processed. Waiting for playback to finish."
89
90############## FUNCTIONS #################
91
92check_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
101check_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
113check_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
123arg_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
141arg_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
166notify(){
167  echo "$1"
168  if [[ "$OptNotify" = "1" ]]; then
169    notify-send -i "$GuiIcon" "$GuiTitle" "$1"
170  fi
171}
172
173check_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
181set_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
193set_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
209split_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# pico2wave --wave="eixida.wav" --lang="es-ES" "Hola"
236#
237
238fest_synth(){
239    echo "$1" | text2wave -o "$OutFile" -eval '(voice_upc_ca_ona_hts)'
240}
241
242pico_synth(){
243  pico2wave --wave="$OutFile" --lang="$LangCode" "$1"
244}
245
246speakpl_synth(){
247  "$speakpl" "$LangCode" <(echo "$1") "$OutFile" > /dev/null 2>&1
248}
249
250tts_google(){
251  split_into_paragraphs
252  for i in "${!TextSections[@]}"; do
253    if [[ "$i" = "$Sections" ]]; then
254      echo "$MsgInfoDone"
255      [[ -n "$PlayerPID" ]] && wait "$PlayerPID"
256      break
257    else
258      echo "Processing $((i+1)) out of $Sections paragraphs"
259    fi
260    OutFile="out_$i.mp3"
261    SectionText="${TextSections[$i]}"
262    if [[ -n "${SectionText//[[:space:]]/}" ]]; then
263      speakpl_synth "${TextSections[$i]}"
264      [[ -n "$PlayerPID" ]] && wait "$PlayerPID"
265      [[ -f "out_$((i-1)).mp3" ]] && rm "out_$((i-1)).mp3"
266      echo "$MsgInfoPlayback $((i+1))"
267      $Player "$OutFile" > /dev/null 2>&1 &
268      PlayerPID="$!"
269    else
270      echo "$MsgInfoSectionEmpty"
271      continue
272    fi
273  done
274}
275
276tts_pico(){
277  if [[ "$LangCode" = "en" ]]; then
278    LangCode="en-GB"
279  elif [[ "$LangCode" = "de" ]]; then
280    LangCode="de-DE"
281  elif [[ "$LangCode" = "es" ]]; then
282    LangCode="es-ES"
283  elif [[ "$LangCode" = "fr" ]]; then
284    LangCode="fr-FR"
285  elif [[ "$LangCode" = "it" ]]; then
286    LangCode="it-IT"
287  elif [[ "$LangCode" = "ca" ]]; then
288    LangCode="ca"
289   
290  else
291    echo "$MsgErrInvalidLang"
292    exit 1
293  fi
294  OutFile="out.wav"
295  # pico2wave handles long text inputs and
296  # fixed formatting line-breaks well enough on its own.
297  # no need to use split_into_paragraphs()
298 
299  if [[ "$LangCode" != "ca" ]]; then
300      pico_synth "$InputText"
301  else
302      fest_synth  "$InputText"
303  fi
304 
305  echo "$MsgInfoPlayback"
306  $Player "$OutFile" > /dev/null 2>&1
307}
308
309cleanup(){
310  pkill -P "$TOP_PID"
311  [[ -n "$TmpDir" && -d "$TmpDir" ]] && rm -r "$TmpDir"
312  [[ -n "$PidFile" && -f "$PidFile" ]] && rm "$PidFile"
313}
314
315############# INSTANCECHECK ##############
316
317check_existing_instance
318
319############## USGCHECKS #################
320
321arg_evaluate_options "$@"
322shift $((OPTIND-1))
323check_environment
324arg_check_input "$@"
325check_connectivity
326
327############### PREPWORK ##################
328
329echo "$TOP_PID" > "$PidFile"
330
331TmpDir="$(mktemp -d "/tmp/${0##*/}.XXXXXX")"
332cd "$TmpDir"
333
334trap "cleanup; exit" EXIT
335
336################ MAIN ####################
337
338set_tts_mode
339set_input_mode
340notify "$MsgInfoSynthesize"
341"$tts_engine"
Note: See TracBrowser for help on using the repository browser.