3 #--------------------------------------------------
5 # Copyright 2012 Michal Fapso (https://github.com/michalfapso)
7 # Modified by Glutanimate (https://github.com/glutanimate)
10 # ./speak.pl en input.txt output.mp3
13 # sudo apt-get install libwww-perl libwww-mechanize-perl libhtml-tree-perl sox libsox-fmt-mp3
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
21 # List of language code names for Google TTS:
37 # zh-CN Chinese (Simplified)
38 # zh-TW Chinese (Traditional)
93 # nn Norwegian (Nynorsk)
100 # pt-BR Portuguese (Brazil)
101 # pt-PT Portuguese (Portugal)
141 #--------------------------------------------------
145 use File::Path qw( rmtree );
149 use HTML::TreeBuilder;
151 $Data::Dumper::Maxdepth = 2;
153 if (scalar(@ARGV) != 3) {
154 print STDERR "Usage: $0 LANGUAGE IN.txt OUT.mp3\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";
162 my $language = $ARGV[0]; # sk | en | cs | ...
163 my $textfile_in = $ARGV[1];
164 my $all_mp3_out = $ARGV[2];
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;
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;
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',
192 my $cookie_jar = HTTP::Cookies->new(hide_cookie2 => 1);
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");
200 my $browser = LWP::UserAgent->new;
205 my $sentence_idx = 0;
206 my $tts_requests_counter = 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>)
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);
220 my @words = split(/\s+/, $line);
223 for (my $i=0; $i<scalar(@words); $i++)
225 my $word = $words[$i];
226 $sentence .= " $word"; # add another word to the sentence
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);
233 $silence_duration = $silence_duration_words;
234 $i --; # one word back
236 # If a separator was found
237 elsif (substr($word, length($word)-1, 1) =~ /[.!?]/ ) {
239 $silence_duration = $silence_duration_sentences;
241 elsif (substr($word, length($word)-1, 1) eq ",") {
243 $silence_duration = $silence_duration_comma;
245 elsif (substr($word, length($word)-1, 1) eq ";") {
247 $silence_duration = $silence_duration_semicolon;
249 elsif (substr($word, length($word)-1, 1) eq ")") {
251 $silence_duration = $silence_duration_brace;
253 # If there are no more words
254 elsif ($i == scalar(@words)-1) {
256 $silence_duration = $silence_duration_words;
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;
267 if ($sample_rate != $trimmed_mp3_sample_rate) {
268 die("Error: sample rate of '$trimmed_mp3' differs from the sample rate of previous files.");
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
280 print "Concatenate: @all_mp3s\n";
281 print "Writing output to $all_mp3_out...";
282 JoinMp3s(\@all_mp3s, $all_mp3_out);
287 my $mp3s_ref = shift;
289 my $depth = shift || 0;
291 # print "JoinMp3s(".join(" ",@{$mp3s_ref}).", $mp3_out, $depth)\n";
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");
300 my @subset_mp3s_out = ();
301 my @subset_mp3s = ();
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);
312 JoinMp3s(\@subset_mp3s_out, $mp3_out, $depth+1);
318 my $duration = shift;
319 my $sample_rate = shift;
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");
326 sub SentenceToMp3() {
327 my $sentence = shift;
328 my $sentence_idx = shift;
330 $sentence =~ s/ /+/g;
331 if (length($sentence) > $SENTENCE_MAX_CHARACTERS) {
332 die ("ERROR: sentence has more than $SENTENCE_MAX_CHARACTERS characters: '$sentence'");
335 my $mp3_out = sprintf("$TMP_DIR/%04d.mp3", $sentence_idx);
337 my $resp = GetSentenceResponse_CaptchaAware($sentence); # NOT WORKING YET
339 if (length($resp) == 0) {
340 print "EMPTY SENTENCE: '$sentence'\n";
343 open(FILE,">$mp3_out");
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");
354 if ($resp->content =~ "^<!DOCTYPE" ||
355 $resp->content =~ "^<html>")
357 die("ERROR: expecting MP3 data, but got a HTML page!");
359 return $resp->content;
362 sub GetSentenceResponse_CaptchaAware() {
363 my $sentence = shift;
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";
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>/)
374 my $tree = HTML::TreeBuilder->new();
375 $tree->parse_content($mech->response()->content());
376 print "HTML response: ".$tree->as_text()."\n";
378 if (!$recaptcha_waiting) {
379 $recaptcha_waiting = 1;
380 print "We have to wait\n";
383 sleep($RECAPTCHA_SLEEP_SECONDS);
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');
393 # print "\n\n".$mech->response()->content()."\n\n";
395 print "enter captcha here: ";
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";
404 # print "MP3 response\n";
407 sleep($RECAPTCHA_SLEEP_SECONDS);
410 if ($recaptcha_waiting) { print "\n"; }
411 return $mech->response()->content();
414 sub PrintWaitingDot() {
428 $mp3_out =~ s/\.mp3$/_trim.mp3/;
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
440 # print "exec $cmd\n";