Earily work on converting speech to Perl
[clearscm.git] / bin / speak.pl
1 #!/usr/bin/perl
2
3 #--------------------------------------------------
4 #
5 # Copyright 2012 Michal Fapso (https://github.com/michalfapso)
6
7 # Modified by Glutanimate (https://github.com/glutanimate)
8 #
9 # Usage:
10 # ./speak.pl en input.txt output.mp3
11 #
12 # Prerequisites:
13 # sudo apt-get install libwww-perl libwww-mechanize-perl libhtml-tree-perl sox libsox-fmt-mp3
14 #
15 # Compiling sox:
16 # Older versions of sox package might not have the support for mp3 codec,
17 # so just download sox from http://sox.sourceforge.net/
18 # install packages libmp3lame-dev libmad0-dev
19 # and compile sox
20 #
21 # List of language code names for Google TTS:
22 #  af  Afrikaans
23 #  sq  Albanian
24 #  am  Amharic
25 #  ar  Arabic
26 #  hy  Armenian
27 #  az  Azerbaijani
28 #  eu  Basque
29 #  be  Belarusian
30 #  bn  Bengali
31 #  bh  Bihari
32 #  bs  Bosnian
33 #  br  Breton
34 #  bg  Bulgarian
35 #  km  Cambodian
36 #  ca  Catalan
37 #  zh-CN  Chinese (Simplified)
38 #  zh-TW  Chinese (Traditional)
39 #  co  Corsican
40 #  hr  Croatian
41 #  cs  Czech
42 #  da  Danish
43 #  nl  Dutch
44 #  en  English
45 #  eo  Esperanto
46 #  et  Estonian
47 #  fo  Faroese
48 #  tl  Filipino
49 #  fi  Finnish
50 #  fr  French
51 #  fy  Frisian
52 #  gl  Galician
53 #  ka  Georgian
54 #  de  German
55 #  el  Greek
56 #  gn  Guarani
57 #  gu  Gujarati
58 #  ha  Hausa
59 #  iw  Hebrew
60 #  hi  Hindi
61 #  hu  Hungarian
62 #  is  Icelandic
63 #  id  Indonesian
64 #  ia  Interlingua
65 #  ga  Irish
66 #  it  Italian
67 #  ja  Japanese
68 #  jw  Javanese
69 #  kn  Kannada
70 #  kk  Kazakh
71 #  rw  Kinyarwanda
72 #  rn  Kirundi
73 #  ko  Korean
74 #  ku  Kurdish
75 #  ky  Kyrgyz
76 #  lo  Laothian
77 #  la  Latin
78 #  lv  Latvian
79 #  ln  Lingala
80 #  lt  Lithuanian
81 #  mk  Macedonian
82 #  mg  Malagasy
83 #  ms  Malay
84 #  ml  Malayalam
85 #  mt  Maltese
86 #  mi  Maori
87 #  mr  Marathi
88 #  mo  Moldavian
89 #  mn  Mongolian
90 #  sr-ME  Montenegrin
91 #  ne  Nepali
92 #  no  Norwegian
93 #  nn  Norwegian (Nynorsk)
94 #  oc  Occitan
95 #  or  Oriya
96 #  om  Oromo
97 #  ps  Pashto
98 #  fa  Persian
99 #  pl  Polish
100 #  pt-BR  Portuguese (Brazil)
101 #  pt-PT  Portuguese (Portugal)
102 #  pa  Punjabi
103 #  qu  Quechua
104 #  ro  Romanian
105 #  rm  Romansh
106 #  ru  Russian
107 #  gd  Scots Gaelic
108 #  sr  Serbian
109 #  sh  Serbo-Croatian
110 #  st  Sesotho
111 #  sn  Shona
112 #  sd  Sindhi
113 #  si  Sinhalese
114 #  sk  Slovak
115 #  sl  Slovenian
116 #  so  Somali
117 #  es  Spanish
118 #  su  Sundanese
119 #  sw  Swahili
120 #  sv  Swedish
121 #  tg  Tajik
122 #  ta  Tamil
123 #  tt  Tatar
124 #  te  Telugu
125 #  th  Thai
126 #  ti  Tigrinya
127 #  to  Tonga
128 #  tr  Turkish
129 #  tk  Turkmen
130 #  tw  Twi
131 #  ug  Uighur
132 #  uk  Ukrainian
133 #  ur  Urdu
134 #  uz  Uzbek
135 #  vi  Vietnamese
136 #  cy  Welsh
137 #  xh  Xhosa
138 #  yi  Yiddish
139 #  yo  Yoruba
140 #  zu  Zulu 
141 #--------------------------------------------------
142
143 use strict;
144
145 use File::Path qw( rmtree );
146 use HTTP::Cookies;
147 use WWW::Mechanize;
148 use LWP;
149 use HTML::TreeBuilder;
150 use Data::Dumper;
151 $Data::Dumper::Maxdepth = 2;
152
153 if (scalar(@ARGV) != 3) {
154   print STDERR "Usage: $0 LANGUAGE IN.txt OUT.mp3\n";
155   print STDERR "\n";
156   print STDERR "Examples: \n";
157   print STDERR "    echo \"Hello world\" | ./speak.pl en speech.mp3\n";
158   print STDERR "    cat file.txt       | ./speak.pl en speech.mp3\n";
159   exit;
160 }
161
162 my $language = $ARGV[0]; # sk | en | cs | ...
163 my $textfile_in = $ARGV[1];
164 my $all_mp3_out = $ARGV[2];
165
166 my $SENTENCE_MAX_CHARACTERS = 100; # limit for google tts
167 my $TMP_DIR = "$all_mp3_out.tmp";
168 my $RECAPTCHA_URL = "http://www.google.com/sorry/?continue=http%3A%2F%2Ftranslate.google.com%2Ftranslate_tts%3Ftl=en%26q=Your+identity+was+successfuly+confirmed.";
169 my $RECAPTCHA_SLEEP_SECONDS = 60;
170 my $SYSTEM_WEBBROWSER = "firefox";
171 my $MAX_OPENED_FILES = 1000;
172 mkdir $TMP_DIR;
173
174 my $silence_duration_paragraphs = 0.8;
175 my $silence_duration_sentences  = 0.2;
176 my $silence_duration_comma      = 0.1;
177 my $silence_duration_brace      = 0.1;
178 my $silence_duration_semicolon  = 0.2;
179 my $silence_duration_words      = 0.05;
180
181 my @headers = (
182 'Host' => 'translate.google.com',
183 'User-Agent' => 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.71 Safari/537.36',
184 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
185 'Accept-Language' => 'en-us,en;q=0.5',
186 'Accept-Encoding' => 'gzip,deflate',
187 'Accept-Charset' => 'ISO-8859-1,utf-8;q=0.7,*;q=0.7',
188 'Keep-Alive' => '300',
189 'Connection' => 'keep-alive',
190 );
191
192 my $cookie_jar = HTTP::Cookies->new(hide_cookie2 => 1);
193
194 my $mech = WWW::Mechanize->new(autocheck => 0, cookie_jar => $cookie_jar);
195 $mech->agent_alias( 'Windows IE 6' );
196 $mech->add_header( "Connection" => "keep-alive" );
197 $mech->add_header( "Accept" => "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
198 $mech->add_header( "Accept-Language" => "en-us;q=0.5,en;q=0.3");
199
200 my $browser = LWP::UserAgent->new;
201
202 my $referer = "";
203
204 my @all_mp3s = ();
205 my $sentence_idx = 0;
206 my $tts_requests_counter = 0;
207 my $sample_rate = 0;
208 # For each input line
209 open(IN, $textfile_in) or die("ERROR: Can not open file '$textfile_in'");
210 while (my $line = <IN>)
211 {
212   chomp($line);
213   print "line: $line\n";
214   # Check for empty lines - paragraphs separator
215   if ($line =~ /^\s*$/) {
216     if ($sample_rate != 0) {
217       push @all_mp3s, SilenceToMp3($sentence_idx++, $silence_duration_paragraphs, $sample_rate);
218     }
219   } else {
220     my @words = split(/\s+/, $line);
221     my $sentence = "";
222     # For each word
223     for (my $i=0; $i<scalar(@words); $i++) 
224     {
225       my $word = $words[$i];
226       $sentence .= " $word"; # add another word to the sentence
227       my $say = 0;
228       my $silence_duration = 0.0;
229       if (length($sentence) >= $SENTENCE_MAX_CHARACTERS) {
230         # Remove the last word;
231         $sentence = substr($sentence, 0, length($sentence)-length($word)-1); 
232         $say = 1;
233         $silence_duration = $silence_duration_words;
234         $i --; # one word back
235       }
236       # If a separator was found
237       elsif (substr($word, length($word)-1, 1) =~ /[.!?]/ ) {
238         $say = 1;
239         $silence_duration = $silence_duration_sentences;
240       }
241       elsif (substr($word, length($word)-1, 1) eq ",") {
242         $say = 1;
243         $silence_duration = $silence_duration_comma;
244       }
245       elsif (substr($word, length($word)-1, 1) eq ";") {
246         $say = 1;
247         $silence_duration = $silence_duration_semicolon;
248       }
249       elsif (substr($word, length($word)-1, 1) eq ")") {
250         $say = 1;
251         $silence_duration = $silence_duration_brace;
252       }
253       # If there are no more words
254       elsif ($i == scalar(@words)-1) {
255         $say = 1;
256         $silence_duration = $silence_duration_words;
257       }
258
259       if ($say) {
260         print "sentence[$tts_requests_counter]: $sentence\n";
261         my $trimmed_mp3 = TrimSilence( SentenceToMp3($sentence, $sentence_idx++) );
262         my $trimmed_mp3_sample_rate = `soxi -r $trimmed_mp3`;
263         chomp($trimmed_mp3_sample_rate);
264         if ($sample_rate == 0) {
265           $sample_rate = $trimmed_mp3_sample_rate;
266         }
267         if ($sample_rate != $trimmed_mp3_sample_rate) {
268           die("Error: sample rate of '$trimmed_mp3' differs from the sample rate of previous files.");
269         }
270         #print "trimmed_mp3_sample_rate: $trimmed_mp3_sample_rate\n";
271         push @all_mp3s, $trimmed_mp3;
272         push @all_mp3s, SilenceToMp3($sentence_idx++, $silence_duration, $sample_rate);
273         $tts_requests_counter ++;
274         $sentence = ""; # start a new sentence
275       }
276     }
277   }
278 }
279
280 print "Concatenate: @all_mp3s\n";
281 print "Writing output to $all_mp3_out...";
282 JoinMp3s(\@all_mp3s, $all_mp3_out);
283 print "done\n";
284 rmtree( $TMP_DIR );
285
286 sub JoinMp3s() {
287   my $mp3s_ref = shift;
288   my $mp3_out = shift;
289   my $depth = shift || 0;
290
291 #  print "JoinMp3s(".join(" ",@{$mp3s_ref}).", $mp3_out, $depth)\n";
292
293   #--------------------------------------------------
294   # Problem if the number of mp3s exceeds the max number of opened files per process
295   # The audio files should be concatenated by smaller chunks 
296   #--------------------------------------------------
297   if (scalar(@{$mp3s_ref}) < $MAX_OPENED_FILES) {
298     Exec("sox @{$mp3s_ref} $mp3_out");
299   } else {
300     my @subset_mp3s_out = ();
301     my @subset_mp3s = ();
302     my $sub_idx = 0;
303     for (my $i = 0; $i < scalar(@{$mp3s_ref}); $i++) {
304       push (@subset_mp3s, $mp3s_ref->[$i]);
305       if (scalar(@subset_mp3s) >= $MAX_OPENED_FILES-1 || $i == scalar(@{$mp3s_ref})-1) {
306         my $sub_mp3_out = "$TMP_DIR/subjoin_".$depth."_$sub_idx.mp3"; $sub_idx++;
307         JoinMp3s(\@subset_mp3s, $sub_mp3_out, $depth+1);
308         push (@subset_mp3s_out, $sub_mp3_out);
309         @subset_mp3s = ();
310       }
311     }
312     JoinMp3s(\@subset_mp3s_out, $mp3_out, $depth+1);
313   }
314 }
315
316 sub SilenceToMp3() {
317   my $idx = shift;
318   my $duration = shift;
319   my $sample_rate = shift;
320
321   my $mp3_out = sprintf("$TMP_DIR/%04d_sil.mp3", $sentence_idx);
322   Exec("sox -n -r $sample_rate $mp3_out trim 0.0 $duration");
323   return $mp3_out;
324 }
325
326 sub SentenceToMp3() {
327   my $sentence     = shift;
328   my $sentence_idx = shift;
329
330   $sentence =~ s/ /+/g;
331   if (length($sentence) > $SENTENCE_MAX_CHARACTERS) {
332     die ("ERROR: sentence has more than $SENTENCE_MAX_CHARACTERS characters: '$sentence'");
333   }
334
335   my $mp3_out = sprintf("$TMP_DIR/%04d.mp3", $sentence_idx);
336
337   my $resp = GetSentenceResponse_CaptchaAware($sentence); # NOT WORKING YET
338
339   if (length($resp) == 0) {
340     print "EMPTY SENTENCE: '$sentence'\n";
341     return "";
342   }
343   open(FILE,">$mp3_out");
344   print FILE $resp;
345   close(FILE);
346   return $mp3_out;
347 }
348
349 sub GetSentenceResponse() {
350   my $sentence = shift;
351   my $amptk = int(rand(1000000)) . '|' . int(rand(1000000));
352   my $resp = $browser->get("https://translate.google.com/translate_tts?ie=UTF-8&tl=$language&q=$sentence&total=1&idx=0&client=tw-ob&tk=$amptk");
353
354   if ($resp->content =~ "^<!DOCTYPE" ||
355     $resp->content =~ "^<html>") 
356   {
357     die("ERROR: expecting MP3 data, but got a HTML page!");
358   }
359   return $resp->content;
360 }
361
362 sub GetSentenceResponse_CaptchaAware() {
363   my $sentence = shift;
364
365   my $recaptcha_waiting = 0;
366   print "URL: https://translate.google.com/translate_tts?ie=UTF-8&tl=$language&q=$sentence&total=1&idx=0&client=tw-ob\n";
367   while (1) {
368     my $amptk = int(rand(1000000)) . '|' . int(rand(1000000));
369     my $url = "https://translate.google.com/translate_tts?ie=UTF-8&tl=$language&q=$sentence&total=1&idx=0&client=tw-ob&tk=$amptk";
370     $mech->get($url); $mech->add_header( Referer => "$referer" ); $referer = $url;
371     if ($mech->response()->content() =~ /^<!DOCTYPE/ || 
372       $mech->response()->content() =~ /^<html>/) 
373     {
374       my $tree = HTML::TreeBuilder->new();
375       $tree->parse_content($mech->response()->content());
376       print "HTML response: ".$tree->as_text()."\n";
377
378       if (!$recaptcha_waiting) {
379         $recaptcha_waiting = 1; 
380         print "We have to wait\n";
381       }
382       print ".";
383       sleep($RECAPTCHA_SLEEP_SECONDS);
384       next;
385
386       my $captcha_img_url = "http://translate.google.com".$tree->look_down("_tag", "img")->attr("src");
387       print "img: ".$captcha_img_url;
388       my $mech2 = $mech->clone();
389       $referer = "http://www.google.com/sorry/?continue=$url";
390       $mech2->add_header( Referer => "$referer" );
391       $mech2->get($captcha_img_url, ':content_file' => 'captcha.jpg'); 
392       
393 #      print "\n\n".$mech->response()->content()."\n\n";
394   
395       print "enter captcha here: ";
396       my $val = <STDIN>;
397       print "val: $val\n";
398
399       # TODO: THIS DOES NOT WORK! MAYBE WAITING FOR HALF AN HOUR WOULD BE BETTER
400       $mech->add_header( Referer => "$referer" );
401       my $res = $mech->submit_form(with_fields => {captcha => "$val"});
402       print "response: ".$res->content."\n";
403     } else {
404 #      print "MP3 response\n";
405       last;
406     }
407     sleep($RECAPTCHA_SLEEP_SECONDS);
408     PrintWaitingDot();
409   }
410   if ($recaptcha_waiting) { print "\n"; }
411   return $mech->response()->content();
412 }
413
414 sub PrintWaitingDot() {
415   select STDOUT;
416   print ".";
417   $|=1;
418 }
419
420 sub TrimSilence() {
421   my $mp3 = shift;
422
423   if ($mp3 eq "") {
424     return "";
425   }
426
427   my $mp3_out = $mp3;
428   $mp3_out =~ s/\.mp3$/_trim.mp3/;
429   Exec("
430   sox $mp3 -p silence 1 0.1 -60d \\
431   | sox -p -p reverse \\
432   | sox -p -p silence 1 0.1 -60d \\
433   | sox -p $mp3_out reverse
434   ");
435   return $mp3_out;
436 }
437
438 sub Exec() {
439   my $cmd = shift;
440 #  print "exec $cmd\n";
441   system $cmd;
442   return;
443 }