-
-
Save michalfapso/3110049 to your computer and use it in GitHub Desktop.
#!/usr/bin/perl | |
#-------------------------------------------------- | |
# Usage: | |
# ./speak.pl en input.txt output.mp3 | |
# | |
# Prerequisites: | |
# sudo apt-get install libwww-perl libhtml-tree-perl sox libsox-fmt-mp3 | |
# | |
# Compiling sox: | |
# Older versions of sox package might not have the support for mp3 codec, | |
# so just download sox from http://sox.sourceforge.net/ | |
# install packages libmp3lame-dev libmad0-dev | |
# and compile sox | |
# | |
# List of language code names for Google TTS: | |
# af Afrikaans | |
# sq Albanian | |
# am Amharic | |
# ar Arabic | |
# hy Armenian | |
# az Azerbaijani | |
# eu Basque | |
# be Belarusian | |
# bn Bengali | |
# bh Bihari | |
# bs Bosnian | |
# br Breton | |
# bg Bulgarian | |
# km Cambodian | |
# ca Catalan | |
# zh-CN Chinese (Simplified) | |
# zh-TW Chinese (Traditional) | |
# co Corsican | |
# hr Croatian | |
# cs Czech | |
# da Danish | |
# nl Dutch | |
# en English | |
# eo Esperanto | |
# et Estonian | |
# fo Faroese | |
# tl Filipino | |
# fi Finnish | |
# fr French | |
# fy Frisian | |
# gl Galician | |
# ka Georgian | |
# de German | |
# el Greek | |
# gn Guarani | |
# gu Gujarati | |
# ha Hausa | |
# iw Hebrew | |
# hi Hindi | |
# hu Hungarian | |
# is Icelandic | |
# id Indonesian | |
# ia Interlingua | |
# ga Irish | |
# it Italian | |
# ja Japanese | |
# jw Javanese | |
# kn Kannada | |
# kk Kazakh | |
# rw Kinyarwanda | |
# rn Kirundi | |
# ko Korean | |
# ku Kurdish | |
# ky Kyrgyz | |
# lo Laothian | |
# la Latin | |
# lv Latvian | |
# ln Lingala | |
# lt Lithuanian | |
# mk Macedonian | |
# mg Malagasy | |
# ms Malay | |
# ml Malayalam | |
# mt Maltese | |
# mi Maori | |
# mr Marathi | |
# mo Moldavian | |
# mn Mongolian | |
# sr-ME Montenegrin | |
# ne Nepali | |
# no Norwegian | |
# nn Norwegian (Nynorsk) | |
# oc Occitan | |
# or Oriya | |
# om Oromo | |
# ps Pashto | |
# fa Persian | |
# pl Polish | |
# pt-BR Portuguese (Brazil) | |
# pt-PT Portuguese (Portugal) | |
# pa Punjabi | |
# qu Quechua | |
# ro Romanian | |
# rm Romansh | |
# ru Russian | |
# gd Scots Gaelic | |
# sr Serbian | |
# sh Serbo-Croatian | |
# st Sesotho | |
# sn Shona | |
# sd Sindhi | |
# si Sinhalese | |
# sk Slovak | |
# sl Slovenian | |
# so Somali | |
# es Spanish | |
# su Sundanese | |
# sw Swahili | |
# sv Swedish | |
# tg Tajik | |
# ta Tamil | |
# tt Tatar | |
# te Telugu | |
# th Thai | |
# ti Tigrinya | |
# to Tonga | |
# tr Turkish | |
# tk Turkmen | |
# tw Twi | |
# ug Uighur | |
# uk Ukrainian | |
# ur Urdu | |
# uz Uzbek | |
# vi Vietnamese | |
# cy Welsh | |
# xh Xhosa | |
# yi Yiddish | |
# yo Yoruba | |
# zu Zulu | |
#-------------------------------------------------- | |
use strict; | |
use HTTP::Cookies; | |
use WWW::Mechanize; | |
use LWP; | |
use HTML::TreeBuilder; | |
use Data::Dumper; | |
$Data::Dumper::Maxdepth = 2; | |
if (scalar(@ARGV) != 3) { | |
print STDERR "Usage: $0 LANGUAGE IN.txt OUT.mp3\n"; | |
print STDERR "\n"; | |
print STDERR "Examples: \n"; | |
print STDERR " echo \"Hello world\" | ./speak.pl en speech.mp3\n"; | |
print STDERR " cat file.txt | ./speak.pl en speech.mp3\n"; | |
exit; | |
} | |
my $language = $ARGV[0]; # sk | en | cs | ... | |
my $textfile_in = $ARGV[1]; | |
my $all_mp3_out = $ARGV[2]; | |
my $SENTENCE_MAX_CHARACTERS = 100; # limit for google tts | |
my $TMP_DIR = "$all_mp3_out.tmp"; | |
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."; | |
my $RECAPTCHA_SLEEP_SECONDS = 60; | |
my $SYSTEM_WEBBROWSER = "firefox"; | |
my $MAX_OPENED_FILES = 1000; | |
mkdir $TMP_DIR; | |
my $silence_duration_paragraphs = 0.8; | |
my $silence_duration_sentences = 0.2; | |
my $silence_duration_comma = 0.1; | |
my $silence_duration_brace = 0.1; | |
my $silence_duration_semicolon = 0.2; | |
my $silence_duration_words = 0.05; | |
my @headers = ( | |
'Host' => 'translate.google.com', | |
'User-Agent' => 'Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.1.5) Gecko/20091109 Ubuntu/9.10 (karmic) Firefox/3.5.5', | |
'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', | |
'Accept-Language' => 'en-us,en;q=0.5', | |
'Accept-Encoding' => 'gzip,deflate', | |
'Accept-Charset' => 'ISO-8859-1,utf-8;q=0.7,*;q=0.7', | |
'Keep-Alive' => '300', | |
'Connection' => 'keep-alive', | |
); | |
my $cookie_jar = HTTP::Cookies->new(hide_cookie2 => 1); | |
#$cookie_jar->clear(); | |
#$cookie_jar->set_cookie(undef, "SESSIONID", $sessionid, "/", $domain, undef, 1, 0, undef, 1); | |
my $mech = WWW::Mechanize->new(autocheck => 0, cookie_jar => $cookie_jar); | |
$mech->agent_alias( 'Windows IE 6' ); | |
$mech->add_header( "Connection" => "keep-alive" ); | |
$mech->add_header( "Accept" => "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"); | |
$mech->add_header( "Accept-Language" => "sk,cs;q=0.8,en-us;q=0.5,en;q=0.3"); | |
my $browser = LWP::UserAgent->new; | |
my $referer = ""; | |
my @all_mp3s = (); | |
my $sentence_idx = 0; | |
my $tts_requests_counter = 0; | |
my $sample_rate = 0; | |
# For each input line | |
open(IN, $textfile_in) or die("ERROR: Can not open file '$textfile_in'"); | |
while (my $line = <IN>) | |
{ | |
chomp($line); | |
print "line: $line\n"; | |
# Check for empty lines - paragraphs separator | |
if ($line =~ /^\s*$/) { | |
if ($sample_rate != 0) { | |
push @all_mp3s, SilenceToMp3($sentence_idx++, $silence_duration_paragraphs, $sample_rate); | |
} | |
} else { | |
my @words = split(/\s+/, $line); | |
my $sentence = ""; | |
# For each word | |
for (my $i=0; $i<scalar(@words); $i++) | |
{ | |
my $word = $words[$i]; | |
$sentence .= " $word"; # add another word to the sentence | |
my $say = 0; | |
my $silence_duration = 0.0; | |
if (length($sentence) >= $SENTENCE_MAX_CHARACTERS) { | |
# Remove the last word; | |
$sentence = substr($sentence, 0, length($sentence)-length($word)-1); | |
$say = 1; | |
$silence_duration = $silence_duration_words; | |
$i --; # one word back | |
} | |
# If a separator was found | |
elsif (substr($word, length($word)-1, 1) =~ /[.!?]/ ) { | |
$say = 1; | |
$silence_duration = $silence_duration_sentences; | |
} | |
elsif (substr($word, length($word)-1, 1) eq ",") { | |
$say = 1; | |
$silence_duration = $silence_duration_comma; | |
} | |
elsif (substr($word, length($word)-1, 1) eq ";") { | |
$say = 1; | |
$silence_duration = $silence_duration_semicolon; | |
} | |
elsif (substr($word, length($word)-1, 1) eq ")") { | |
$say = 1; | |
$silence_duration = $silence_duration_brace; | |
} | |
# If there are no more words | |
elsif ($i == scalar(@words)-1) { | |
$say = 1; | |
$silence_duration = $silence_duration_words; | |
} | |
if ($say) { | |
print "sentence[$tts_requests_counter]: $sentence\n"; | |
my $trimmed_mp3 = TrimSilence( SentenceToMp3($sentence, $sentence_idx++) ); | |
my $trimmed_mp3_sample_rate = `soxi -r $trimmed_mp3`; | |
chomp($trimmed_mp3_sample_rate); | |
if ($sample_rate == 0) { | |
$sample_rate = $trimmed_mp3_sample_rate; | |
} | |
if ($sample_rate != $trimmed_mp3_sample_rate) { | |
die("Error: sample rate of '$trimmed_mp3' differs from the sample rate of previous files."); | |
} | |
#print "trimmed_mp3_sample_rate: $trimmed_mp3_sample_rate\n"; | |
push @all_mp3s, $trimmed_mp3; | |
push @all_mp3s, SilenceToMp3($sentence_idx++, $silence_duration, $sample_rate); | |
$tts_requests_counter ++; | |
$sentence = ""; # start a new sentence | |
} | |
} | |
} | |
} | |
print "Concatenate: @all_mp3s\n"; | |
print "Writing output to $all_mp3_out..."; | |
JoinMp3s(\@all_mp3s, $all_mp3_out); | |
print "done\n"; | |
sub JoinMp3s() { | |
my $mp3s_ref = shift; | |
my $mp3_out = shift; | |
my $depth = shift || 0; | |
# print "JoinMp3s(".join(" ",@{$mp3s_ref}).", $mp3_out, $depth)\n"; | |
#-------------------------------------------------- | |
# Problem if the number of mp3s exceeds the max number of opened files per process | |
# The audio files should be concatenated by smaller chunks | |
#-------------------------------------------------- | |
if (scalar(@{$mp3s_ref}) < $MAX_OPENED_FILES) { | |
Exec("sox @{$mp3s_ref} $mp3_out"); | |
} else { | |
my @subset_mp3s_out = (); | |
my @subset_mp3s = (); | |
my $sub_idx = 0; | |
for (my $i = 0; $i < scalar(@{$mp3s_ref}); $i++) { | |
push (@subset_mp3s, $mp3s_ref->[$i]); | |
if (scalar(@subset_mp3s) >= $MAX_OPENED_FILES-1 || $i == scalar(@{$mp3s_ref})-1) { | |
my $sub_mp3_out = "$TMP_DIR/subjoin_".$depth."_$sub_idx.mp3"; $sub_idx++; | |
JoinMp3s(\@subset_mp3s, $sub_mp3_out, $depth+1); | |
push (@subset_mp3s_out, $sub_mp3_out); | |
@subset_mp3s = (); | |
} | |
} | |
JoinMp3s(\@subset_mp3s_out, $mp3_out, $depth+1); | |
} | |
} | |
sub SilenceToMp3() { | |
my $idx = shift; | |
my $duration = shift; | |
my $sample_rate = shift; | |
my $mp3_out = sprintf("$TMP_DIR/%04d_sil.mp3", $sentence_idx); | |
Exec("sox -n -r $sample_rate $mp3_out trim 0.0 $duration"); | |
return $mp3_out; | |
} | |
sub SentenceToMp3() { | |
my $sentence = shift; | |
my $sentence_idx = shift; | |
$sentence =~ s/ /+/g; | |
if (length($sentence) > $SENTENCE_MAX_CHARACTERS) { | |
die ("ERROR: sentence has more than $SENTENCE_MAX_CHARACTERS characters: '$sentence'"); | |
} | |
my $mp3_out = sprintf("$TMP_DIR/%04d.mp3", $sentence_idx); | |
#print "mp3_out: $mp3_out\n"; | |
#print "http://translate.google.com/translate_tts?q=$sentence\n"; | |
# my $resp = GetSentenceResponse($sentence); | |
my $resp = GetSentenceResponse_CaptchaAware($sentence); # NOT WORKING YET | |
if (length($resp) == 0) { | |
print "EMPTY SENTENCE: '$sentence'\n"; | |
return ""; | |
} | |
open(FILE,">$mp3_out"); | |
print FILE $resp; | |
close(FILE); | |
return $mp3_out; | |
} | |
sub GetSentenceResponse() { | |
my $sentence = shift; | |
#my $resp = $browser->get("http://translate.google.com/translate_tts?tl=$language&q=$sentence", @headers); | |
my $resp = $browser->get("http://translate.google.com/translate_tts?tl=$language&q=$sentence"); | |
if ($resp->content =~ "^<!DOCTYPE" || | |
$resp->content =~ "^<html>") | |
{ | |
die("ERROR: expecting MP3 data, but got a HTML page!"); | |
} | |
return $resp->content; | |
} | |
sub GetSentenceResponse_CaptchaAware() { | |
my $sentence = shift; | |
my $recaptcha_waiting = 0; | |
print "URL: http://translate.google.com/translate_tts?tl=$language&q=$sentence\n"; | |
while (1) { | |
#$resp = $browser->get("http://translate.google.com/translate_tts?tl=$language&q=$sentence", @headers); | |
#print $resp->content; | |
#$mech->get("http://translate.google.com/translate_tts?tl=$language&q=$sentence", @headers); | |
my $url = "http://translate.google.com/translate_tts?tl=$language&q=$sentence"; | |
$mech->get($url); $mech->add_header( Referer => "$referer" ); $referer = $url; | |
# print "Headers:\n".Dumper($mech->dump_headers()); | |
# open my $fh, '<', "recaptcha_response.html" or die "error opening file: $!"; | |
# $resp = do { local $/; <$fh> }; | |
if ($mech->response()->content() =~ /^<!DOCTYPE/ || | |
$mech->response()->content() =~ /^<html>/) | |
{ | |
my $tree = HTML::TreeBuilder->new(); | |
$tree->parse_content($mech->response()->content()); | |
print "HTML response: ".$tree->as_text()."\n"; | |
if (!$recaptcha_waiting) { | |
$recaptcha_waiting = 1; | |
print "We have to wait\n"; | |
} | |
print "."; | |
sleep($RECAPTCHA_SLEEP_SECONDS); | |
next; | |
my $captcha_img_url = "http://translate.google.com".$tree->look_down("_tag", "img")->attr("src"); | |
print "img: ".$captcha_img_url; | |
my $mech2 = $mech->clone(); | |
$referer = "http://www.google.com/sorry/?continue=$url"; | |
$mech2->add_header( Referer => "$referer" ); | |
$mech2->get($captcha_img_url, ':content_file' => 'captcha.jpg'); | |
# print "\n\n".$mech->response()->content()."\n\n"; | |
print "enter captcha here: "; | |
my $val = <STDIN>; | |
print "val: $val\n"; | |
# TODO: THIS DOES NOT WORK! MAYBE WAITING FOR HALF AN HOUR WOULD BE BETTER | |
$mech->add_header( Referer => "$referer" ); | |
my $res = $mech->submit_form(with_fields => {captcha => "$val"}); | |
print "response: ".$res->content."\n"; | |
} else { | |
# print "MP3 response\n"; | |
last; | |
} | |
sleep($RECAPTCHA_SLEEP_SECONDS); | |
PrintWaitingDot(); | |
} | |
if ($recaptcha_waiting) { print "\n"; } | |
return $mech->response()->content(); | |
} | |
sub PrintWaitingDot() { | |
select STDOUT; | |
print "."; | |
$|=1; | |
} | |
sub TrimSilence() { | |
my $mp3 = shift; | |
if ($mp3 eq "") { | |
return ""; | |
} | |
my $mp3_out = $mp3; | |
$mp3_out =~ s/\.mp3$/_trim.mp3/; | |
Exec(" | |
sox $mp3 -p silence 1 0.1 -60d \\ | |
| sox -p -p reverse \\ | |
| sox -p -p silence 1 0.1 -60d \\ | |
| sox -p $mp3_out reverse | |
"); | |
return $mp3_out; | |
} | |
sub Exec() { | |
my $cmd = shift; | |
# print "exec $cmd\n"; | |
system $cmd; | |
return; | |
} |
how can i use it in asp.net ? could you give me a clue
@michalfapso , thanks a lot for sharing this gist! I was thinking of using your script in a project of mine (a simple bash wrapper to speak.pl that would read from the X selection and add a few other tweaks). But, in order to use it, I would have to modify your script slightly.
Right now speak.pl
generates a lot of temporary files that aren't cleaned up after executing. The following modification takes care of that:
--- speak.pl 2013-10-02 11:26:07.000000000 +0200
+++ speak.pl 2014-08-07 11:05:27.000000000 +0200
@@ -137,13 +137,14 @@
use strict;
+use File::Path qw( rmtree );
use HTTP::Cookies;
use WWW::Mechanize;
use LWP;
use HTML::TreeBuilder;
use Data::Dumper;
$Data::Dumper::Maxdepth = 2;
-
+
if (scalar(@ARGV) != 3) {
print STDERR "Usage: $0 LANGUAGE IN.txt OUT.mp3\n";
print STDERR "\n";
@@ -277,6 +278,7 @@
print "Writing output to $all_mp3_out...";
JoinMp3s(\@all_mp3s, $all_mp3_out);
print "done\n";
+rmtree( $TMP_DIR );
sub JoinMp3s() {
my $mp3s_ref = shift;
(Note: I don't know the slightest thing about actual perl programming. I just found this method by googling. Please tell me if this is completely stupid and/or might result in unforeseen problems)
Would it be fine with you if hosted the modified speak.pl
script in my repository? What license should I assume for speak.pl
?
Thanks a lot in advance.
I just published my bash script wrapper. I'd be very happy if you gave it a try.
Cheers, Glutanimate
I wonder if it's possible to persuade it to generate subtitles at the same time?
Michal,
Thank you for sharing this awesome piece of perl code. Works like a champ! Again, thank you so much.
-Arul