4 # AGI script that uses Google's translate text to speech engine.
6 # Copyright (C) 2011 - 2015, Lefteris Zafiris <zaf.000@gmail.com>
8 # This program is free software, distributed under the terms of
9 # the GNU General Public License Version 2. See the COPYING file
10 # at the top of the source tree.
15 # agi(googletts.agi,"text",[language],[intkey],[speed]): This will invoke the Google TTS
16 # engine, render the text string to speech and play it back to the user.
17 # If 'intkey' is set the script will wait for user input. Any given interrupt keys will
18 # cause the playback to immediately terminate and the dialplan to proceed to the
19 # matching extension (this is mainly for use in IVR, see README for examples).
20 # If 'speed' is set the speech rate is altered by that factor (defaults to 1.2).
22 # The script contacts google's TTS service in order to get the voice data
23 # which then stores in a local cache (by default /tmp/) for future use.
25 # Parameters like default language, sample rate, caching and cache dir
26 # can be set up by altering the following variables:
27 # Default langeuage: $lang
28 # Sample rate: $samplerate
29 # Speed factor: $speed
31 # Chache directory: $cachedir
32 # SoX Version: $sox_ver
38 use Encode qw(decode encode);
39 use File::Temp qw(tempfile);
40 use File::Copy qw(move);
41 use File::Path qw(mkpath);
42 use Digest::MD5 qw(md5_hex);
48 # ----------------------------- #
49 # User defined parameters: #
50 # ----------------------------- #
54 # Output speed factor #
57 # Use of cache mechanism #
60 # Cache directory path #
61 my $cachedir = "/tmp";
63 # Output audio sample rate #
64 # Leave blank to auto-detect #
68 # Leave blank to auto-detect #
71 # Verbose debugging messages #
74 # ----------------------------- #
85 my $url = "https://translate.google.com";
86 my $sox = `/usr/bin/which sox`;
87 my $mpg123 = `/usr/bin/which mpg123`;
90 ($AGI{arg_1}, $AGI{arg_2}, $AGI{arg_3}, $AGI{arg_4}) = @ARGV;
94 $AGI{$1} = $2 if (/^agi_(\w+)\:\s+(.*)$/);
96 my $name = " -- $AGI{request}:";
98 # Abort if required programs not found. #
99 fatal_log("sox or mpg123 is missing. Aborting.") if (!$sox || !$mpg123);
100 chomp($sox, $mpg123);
103 $AGI{arg_1} = uri_unescape($AGI{arg_1}); # Added by XCALLY team #
104 $AGI{arg_1} = decode('utf8', $AGI{arg_1});
106 s/[\\|*~<>^\(\)\[\]\{\}[:cntrl:]]/ /g;
109 fatal_log("No text passed for synthesis.") if (!length);
110 # Split input to comply with google tts requirements #
111 $_ .= "." unless (/^.+[.,?!:;]$/);
112 @text = /.{1,150}[.,?!:;]|.{1,150}\s/g;
116 # Setting language, interrupt keys and speed rate #
117 if (length($AGI{arg_2})) {
118 if ($AGI{arg_2} =~ /^[a-zA-Z]{2}(-[a-zA-Z]{2,6})?$/) {
121 console_log("Invalid language setting. Using default.");
125 if (length($AGI{arg_3})) {
126 $intkey = "0123456789#*" if ($AGI{arg_3} eq "any");
127 $intkey = $AGI{arg_3} if ($AGI{arg_3} =~ /^[0-9*#]+$/);
130 if (length($AGI{arg_4})) {
131 $speed = $AGI{arg_4} if ($AGI{arg_4} =~ /^\d+(\.\d+)?$/);
134 # Check cache path size: dir length + md5 + file extension #
136 if ((length($cachedir) + 32 + 6) < $maxlen) {
137 mkpath("$cachedir") unless (-d "$cachedir");
139 console_log("Cache path size exceeds limit. Disabling cache.");
144 # Answer channel if not already answered #
145 print "CHANNEL STATUS\n";
146 my @result = checkresponse();
147 if ($result[0] == 4) {
149 @result = checkresponse();
150 if ($result[0] != 0) {
151 fatal_log("Failed to answer channel.");
155 # Setting filename extension according to sample rate. #
156 if (!$samplerate) { ($fexten, $samplerate) = detect_format(); }
157 elsif ($samplerate == 12000) { $fexten = "sln12"; }
158 elsif ($samplerate == 16000) { $fexten = "sln16"; }
159 elsif ($samplerate == 32000) { $fexten = "sln32"; }
160 elsif ($samplerate == 44100) { $fexten = "sln44"; }
161 elsif ($samplerate == 48000) { $fexten = "sln48"; }
162 else { ($fexten, $samplerate) = ("sln", 8000); }
164 # Initialise User angent #
165 my $ua = LWP::UserAgent->new(ssl_opts => { verify_hostname => 1 });
166 $ua->agent("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.101 Safari/537.36");
168 $ua->conn_cache(LWP::ConnCache->new());
169 $ua->timeout($timeout);
171 for (my $i = 0; $i < $lines; $i++) {
174 my $len = length($text[$i]);
175 my $line = encode('utf8', $text[$i]);
176 $line =~ s/^\s+|\s+$//g;
177 next if (length($line) == 0);
179 console_log("Text passed for synthesis: $line",
180 "Language: $lang, Interrupt keys: $intkey, Sample rate: $samplerate",
181 "Speed: $speed, Caching: $usecache, Cache dir: $cachedir"
185 $filename = md5_hex("$line.$lang.$speed");
187 # Stream file from cache if it exists #
188 if (-r "$cachedir/$filename.$fexten") {
189 console_log("File already in cache.") if ($debug);
190 $res = playback("$cachedir/$filename", $intkey);
197 # Hnadle interrupts #
198 $SIG{'INT'} = \&int_handler;
199 $SIG{'HUP'} = \&int_handler;
201 ($fh, $tmpname) = tempfile("ggl_XXXXXXXX", DIR => $tmpdir, UNLINK => 1);
202 my $token = make_token($line);
203 $line = uri_escape($line);
204 my $req = "$url/translate_tts?ie=UTF-8&q=$line&tl=$lang&total=$lines&idx=$i&textlen=$len&client=tw-ob&tk=$token&prev=input";
205 console_log("URL passed: $req") if ($debug);
207 my $ua_request = HTTP::Request->new('GET' => $req);
210 'Accept-Encoding' => 'identity;q=1, *;q=0',
211 'Accept-Language' => 'en-US,en;q=0.5',
213 'Range' => 'bytes=0-',
214 'Referer' => 'https://translate.google.com/',
216 my $ua_response = $ua->request($ua_request, $tmpname);
217 fatal_log("Failed to fetch file: ", $ua_response->code, $ua_response->message) unless ($ua_response->is_success);
219 # Convert mp3 file to 16bit 8Khz or 16kHz mono raw #
220 system($mpg123, "-q", "-w", "$tmpname.wav", $tmpname) == 0
221 or fatal_log("$mpg123 failed: $?");
223 # Detect sox version #
225 $sox_ver = (system("$sox --version > /dev/null 2>&1") == 0) ? 14 : 12;
226 console_log("Found sox version $sox_ver in: $sox, mpg123 in: $mpg123") if ($debug);
228 my @soxargs = get_sox_args("$tmpname.wav", "$tmpname.$fexten");
229 system(@soxargs) == 0 or fatal_log("$sox failed: $?");
230 unlink "$tmpname.wav";
232 # Playback and save file in cache #
233 $res = playback($tmpname, $intkey);
236 console_log("Saving file $filename to cache") if ($debug);
237 move("$tmpname.$fexten", "$cachedir/$filename.$fexten");
239 unlink "$tmpname.$fexten";
250 if ($input =~ /^200 result=(-?\d+)\s?(.*)$/) {
251 @values = ("$1", "$2");
253 $input .= <STDIN> if ($input =~ /^520-Invalid/);
254 warn "$name Unexpected result: $input\n";
261 my ($file, $keys) = @_;
264 print "STREAM FILE $file \"$keys\"\n";
265 @response = checkresponse();
266 if ($response[0] >= 32 && chr($response[0]) =~ /[\w*#]/) {
267 console_log("Got digit chr($response[0])") if ($debug);
268 print "SET EXTENSION ", chr($response[0]), "\n";
270 print "SET PRIORITY 1\n";
272 } elsif ($response[0] == -1) {
273 console_log("Failed to play $file.");
279 # Detect the sound format used #
281 print "GET FULL VARIABLE \${CHANNEL(audionativeformat)}\n";
282 my @reply = checkresponse();
284 if (/(silk|sln)12/) { @format = ("sln12", 12000); }
285 elsif (/(speex|slin|silk)16|g722|siren7/) { @format = ("sln16", 16000); }
286 elsif (/(speex|slin|celt)32|siren14/) { @format = ("sln32", 32000); }
287 elsif (/(celt|slin)44/) { @format = ("sln44", 44100); }
288 elsif (/(celt|slin)48/) { @format = ("sln48", 48000); }
289 else { @format = ("sln", 8000); }
295 # Set the appropiate sox cli arguments #
296 my ($source_file, $dest_file) = @_;
298 my @soxargs = ($sox, $source_file, "-q", "-r", $samplerate, "-t", "raw", $dest_file);
300 if ($sox_ver >= 14) {
301 push(@soxargs, ("tempo", "-s", $speed));
303 push(@soxargs, ("stretch", 1 / $speed, "80"));
309 # Obfuscated crap straight from Google:
310 # https://translate.google.com/translate/releases/twsfe_w_20151214_RC03/r/js/desktop_module_main.js
313 my $time = int(time() / 3600);
314 my @chars = unpack('U*', $text);
318 $stamp = make_rl($stamp + $_, '+-a^+6');
320 $stamp = make_rl($stamp, '+-3^+b+-f');
322 $stamp = ($stamp & 2147483647) + 2147483648;
325 return ($stamp . '.' . ($stamp ^ $time));
329 my ($num, $str) = @_;
331 for (my $i = 0; $i < length($str) - 2 ; $i += 3) {
332 my $d = substr($str, $i+2, 1);
333 if (ord($d) >= ord('a')) {
338 if (substr($str, $i+1, 1) eq '+') {
343 if (substr($str, $i, 1) eq '+') {
344 $num = $num + $d & 4294967295;
353 foreach my $message (@_) {
354 warn "$name $message\n";
355 print "NOOP \"$name $message\"\n";
366 die "$name Interrupt signal received, terminating...\n";
371 warn "$name Cleaning temp files.\n" if ($debug);
372 unlink glob "$tmpname*";