#!/usr/bin/env perl # # AGI script that uses iSpeech text to speech engine. # http://www.ispeech.org/ # # Copyright (C) 2011 - 2014, Lefteris Zafiris # # This program is free software, distributed under the terms of # the GNU General Public License Version 2. See the LICENSE file # at the top of the source tree. # # The Asterisk ispeech plugin developmend was funded by ifbyphone http://ifbyphone.com/ # # ----- # Usage # ----- # agi(ispeech-tts.agi,"text",[voice],[intkey],[speed]): This will invoke the iSpeech TTS # engine, render the text string to speech and play it back to the user. # If 'intkey' is set the script will wait for user input. Any given interrupt keys will # cause the playback to immediately terminate and the dialplan to proceed to the # matching extension (this is mainly for use in IVR, see README for examples). # If 'speed' is set the speech rate is altered by that factor. # # # Parameters like default voice, max wold number, speed, caching, cache dir and # use of SSL can be set up by altering the following variables: # Default voice: $voice # Max word count: $word_limit # Speed factor: $speed # Chace: $usecache # SSL: $use_ssl # # An API key is a password that is required for access. To obtain an API key please visit: # http://www.ispeech.org/developers and register for a developer account. use warnings; use strict; use Encode qw(decode encode); use Digest::MD5 qw(md5_hex); use File::Path qw(mkpath); use URI::Escape; use LWP::UserAgent; use LWP::ConnCache; $| = 1; # ----------------------------- # # User defined parameters: # # ----------------------------- # # Ispeech API key my $key = "developerdemokeydeveloperdemokey"; # Default voice # my $voice = "usenglishfemale"; # API word limit my $word_limit = 50; # Output speed factor # my $speed = 1; # Use of cache mechanism # my $usecache = 0; # Cache directory path # my $cachedir = "/tmp"; # Use SSL # my $use_ssl = 1; # Verbose debugging messages # my $debug = 1; # ----------------------------- # my %AGI; my @text; my $ua; my $filename; my @result; my $name; my $url; my $frequency = 8000; my $format = "wav"; my $intkey = ""; my $timeout = 10; my $maxlen = 4096; my $host = "api.ispeech.org/api/rest"; # Store AGI input # # $text $voice $intkey $speed $token # ($AGI{arg_1}, $AGI{arg_2}, $AGI{arg_3}, $AGI{arg_4}, $AGI{arg_5}) = @ARGV; while () { chomp; last if (!length); $AGI{$1} = $2 if (/^agi_(\w+)\:\s+(.*)$/); } $name = " -- $AGI{request}:"; # Sanitising input # $AGI{arg_1} = uri_unescape($AGI{arg_1}); $AGI{arg_1} = decode('utf8', $AGI{arg_1}); for ($AGI{arg_1}) { s/[\\|*~<>^\(\)\[\]\{\}[:cntrl:]]/ /g; s/\s+/ /g; s/^\s|\s$//g; die "$name No text passed for synthesis.\n" if (!length); # Split input to comply with word limits # $_ .= "." unless (/^.+[.,?!:;]$/); @text = /(?:\W*\w+){1,$word_limit}[.,?!:;]+|(?:\W*\w+){1,$word_limit}\s+/g; } # Setting voice, interrupt keys and speed rate # if (length($AGI{arg_2})) { if ($AGI{arg_2} =~ /^[a-z]+$/) { $voice = $AGI{arg_2}; } else { warn "$name Invalid voice setting. Using default.\n"; } } if (length($AGI{arg_3})) { $intkey = "0123456789#*" if ($AGI{arg_3} eq "any"); $intkey = $AGI{arg_3} if ($AGI{arg_3} =~ /^[0-9*#]+$/); } if (length($AGI{arg_4})) { $speed = $AGI{arg_4} if ($AGI{arg_4} =~ /^\d+(\.\d+)?$/ and ($AGI{arg_4} <= 10 and $AGI{arg_4} >= 0)); } if (length($AGI{arg_5})) { $key = $AGI{arg_5}; } # Check cache path size: dir length + md5 + file extension # if ($usecache && ((length($cachedir) + 32 + 6) < $maxlen)) { mkpath("$cachedir") unless (-d "$cachedir"); } else { warn "$name Cache path size exceeds limit. Disabling cache.\n"; $usecache = 0; } # Answer channel if not already answered # warn "$name Checking channel status.\n" if ($debug); print "CHANNEL STATUS\n"; @result = checkresponse(); if ($result[0] == 4) { warn "$name Answering channel.\n" if ($debug); print "ANSWER\n"; @result = checkresponse(); if ($result[0] != 0) { die "$name Failed to answer channel.\n"; } } # Initialise User angent # if ($use_ssl) { $url = "https://" . $host; $ua = LWP::UserAgent->new(ssl_opts => {verify_hostname => 1}); } else { $url = "http://" . $host; $ua = LWP::UserAgent->new; } $ua->agent("Asterisk iSpeech TTS module"); $ua->env_proxy; $ua->conn_cache(LWP::ConnCache->new()); $ua->timeout($timeout); foreach my $line (@text) { $line = encode('utf8', $line); $line =~ s/^\s+|\s+$//g; next if (length($line) == 0); if ($debug) { warn "$name Text passed for synthesis: $line\n", "$name Voice: $voice, Interrupt keys: $intkey, Speed: $speed\n", "$name Caching: $usecache, Cache dir: $cachedir\n"; } $filename = md5_hex("$line.$speed.$voice"); if ($usecache) { # Stream file from cache if it exists # if (-r "$cachedir/$filename.$format") { warn "$name File already in cache.\n" if ($debug); my $res = playback("$cachedir/$filename", $intkey); last if ($res > 0); die if ($res < 0); next; } } $line = uri_escape($line); die "$name No API key found. Aborting.\n" if (!$key); warn "$name URL passed: $url?apikey=$key&action=convert&text=$line&voice=$voice&format=$format&frequency=$frequency&speed=$speed\n" if ($debug); open(my $fh, "+>", "$cachedir/$filename.$format") or die "$name Unable to open file for writing: $cachedir/$filename.$format\n"; my $ua_response = $ua->get( "$url?apikey=$key&action=convert&text=$line&voice=$voice&format=$format&frequency=$frequency&speed=$speed", ':content_file' => "$cachedir/$filename.$format", ); warn "$name The response was: ", $ua_response->decoded_content if ($debug); die "$name Failed to fetch voice data.\n" unless ($ua_response->is_success); if ($ua_response->code != 200) { my $error = <$fh>; close($fh); unlink "$cachedir/$filename.$format"; $error = uri_unescape($error); die "$name An iSpeech API error occured: $error\n"; } close($fh); # Playback and delete file if no caching is set # my $res = playback("$cachedir/$filename", $intkey); unlink "$cachedir/$filename.$format" if (!$usecache); last if ($res > 0); die if ($res < 0); } exit; sub checkresponse { my $input = ; my @values; chomp $input; if ($input =~ /^200 result=(-?\d+)\s?(.*)$/) { warn "$name Command returned: $input\n" if ($debug); @values = ("$1", "$2"); } else { $input .= if ($input =~ /^520-Invalid/); warn "$name Unexpected result: $input\n"; @values = (-1, -1); } return @values; } sub playback { my ($file, $keys) = @_; my @response; print "STREAM FILE $file \"$keys\"\n"; @response = checkresponse(); if ($response[0] >= 32 && chr($response[0]) =~ /[\w*#]/) { warn "$name Got digit ", chr($response[0]), "\n" if ($debug); print "SET EXTENSION ", chr($response[0]), "\n"; checkresponse(); print "SET PRIORITY 1\n"; checkresponse(); } elsif ($response[0] == -1) { warn "$name Failed to play $file\n"; } return $response[0]; }