forked from zaf/asterisk-speech-recog
-
Notifications
You must be signed in to change notification settings - Fork 0
/
speech-recog.agi
executable file
·337 lines (298 loc) · 9.34 KB
/
speech-recog.agi
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
#!/usr/bin/env perl
#
# AGI script that renders speech to text using Google's Speech API.
#
# Copyright (C) 2011 - 2014, Lefteris Zafiris <[email protected]>
#
# This program is free software, distributed under the terms of
# the GNU General Public License Version 2. See the COPYING file
# at the top of the source tree.
#
# -----
# Usage
# -----
# agi(speech-recog.agi,[lang],[timeout],[intkey],[NOBEEP])
# Records from the current channel until 2 seconds of silence are detected
# (this can be set by the user by the 'timeout' argument, -1 for no timeout) or the
# interrupt key (# by default) is pressed. If NOBEEP is set, no beep sound is played
# back to the user to indicate the start of the recording.
# The recorded sound is send over to googles speech recognition service and the
# returned text string is assigned as the value of the channel variable 'utterance'.
# The scripts sets the following channel variables:
# utterance : The generated text string.
# confidence : A value between 0 and 1 indicating how 'confident' the recognition engine
# feels about the result. Values bigger than 0.95 usually mean that the
# resulted text is correct.
#
# User defined parameters:
# Speech API key from google:
# $key
#
# Default language:
# $language
#
# Default timeout:
# $timeout (value in seconds of silence before recording is stopped)
#
# Default interupt key:
# $intkey (can be any digit from 0 to 9 or # and *, or a combination of them)
#
# Sample rate:
# $samplerate (value in Hz. 0 for atuomatic detection per channel/call, 16000 for
# use with wideband codecs, 8000 for traditional codecs.
#
# Profanity filter:
# $pro_filter (0:disable, 1: remove profanities, 2: mask profanities)
#
# Encode voice using speex:
# $use_speex (0: disable, 1: enable) **_Highly experimental_**
# works only with patched speex encoder that supports MIME "x-speex-with-header-byte"
# https://github.com/zaf/Speex-with-header-bytes
#
# SSL:
# $use_ssl (0: disable, 1: enable)
#
use warnings;
use strict;
use URI::Escape;
use File::Copy qw(move);
use File::Temp qw(tempfile);
use LWP::UserAgent;
use JSON;
use Encode qw(encode);
$| = 1;
# ----------------------------- #
# User defined parameters: #
# ----------------------------- #
# Speech API key #
my $key = "";
# Default language #
my $language = "en-US";
# Default max silence timeout #
my $timeout = 2;
# Absolute Recording timeout #
my $abs_timeout = -1;
# Default interrupt key #
my $intkey = "#";
# Input audio sample rate #
# Leave blank to auto-detect #
my $samplerate = "";
# Profanity filter #
my $pro_filter = 0;
# Use speex #
my $use_speex = 0;
# Use SSL #
my $use_ssl = 1;
# Verbose debugging messages #
my $debug = 0;
# ----------------------------- #
my %AGI;
my $ua;
my $fh;
my $tmpname;
my $format;
my @result;
my $name;
my $audio;
my $uaresponse;
my %response;
my $endian;
my $url;
my $silence;
my $filetype;
my $flac;
my $speex;
my $results = 1;
my $grammar = "builtin:dictation"; #"builtin:search";
my $beep = "BEEP";
my $comp_level = -8;
my $ua_timeout = 10;
my $tmpdir = "/tmp";
my $host = "www.google.com/speech-api/v2/recognize";
# Store AGI input #
($AGI{arg_1}, $AGI{arg_2}, $AGI{arg_3}, $AGI{arg_4}) = @ARGV;
while (<STDIN>) {
chomp;
last if (!length);
$AGI{$1} = $2 if (/^agi_(\w+)\:\s+(.*)$/);
}
$name = " -- $AGI{request}:";
# Reset variables. #
%response = (
utterance => -1,
confidence => -1,
);
warn "$name Clearing channel variables.\n" if ($debug);
foreach (keys %response) {
print "SET VARIABLE \"$_\" \"$response{$_}\"\n";
checkresponse();
}
# Abort if key is missing or required programs not found. #
if (!$key) {
print "VERBOSE \"API key is missing. Aborting.\" 3\n";
checkresponse();
die "$name API key is missing. Aborting.\n";
}
if ($use_speex) {
$speex = `/usr/bin/which speexenc`;
die "$name speexenc is missing. Aborting.\n" if (!$speex);
chomp($speex);
warn "$name Found speexenc in: $speex\n" if ($debug);
} else {
$flac = `/usr/bin/which flac`;
die "$name flac is missing. Aborting.\n" if (!$flac);
chomp($flac);
warn "$name Found flac in: $flac\n" if ($debug);
}
# Setting language, timeout, interrupt keys and BEEP indication #
if (length($AGI{arg_1})) {
$language = $AGI{arg_1} if ($AGI{arg_1} =~ /^[a-z]{2}(-[a-zA-Z]{2,6})?$/);
}
if (length($AGI{arg_2})) {
if ($AGI{arg_2} == -1) {
$silence = "";
} elsif ($AGI{arg_2} =~ /^\d+$/) {
$silence = "s=$AGI{arg_2}";
} else {
$silence = "s=$timeout";
}
} else {
$silence = "s=$timeout";
}
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})) {
$beep = "" if ($AGI{arg_4} eq "NOBEEP");
}
# 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";
}
}
# Setting recording file format according to sample rate. #
if (!$samplerate) { ($format, $samplerate) = detect_format(); }
elsif ($samplerate == 12000) { $format = "sln12"; }
elsif ($samplerate == 16000) { $format = "sln16"; }
elsif ($samplerate == 32000) { $format = "sln32"; }
elsif ($samplerate == 44100) { $format = "sln44"; }
elsif ($samplerate == 48000) { $format = "sln48"; }
else { ($format, $samplerate) = ("sln", 8000); }
# 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;
}
$language = uri_escape($language);
$grammar = uri_escape($grammar);
$url .= "?key=$key&lang=$language&pfilter=$pro_filter&lm=$grammar&maxresults=$results";
$ua->agent("Asterisk AGI speeech recognition script");
$ua->env_proxy;
$ua->timeout($ua_timeout);
# Hnadle interrupts #
$SIG{'INT'} = \&int_handler;
$SIG{'HUP'} = \&int_handler;
# Record file #
($fh, $tmpname) = tempfile("stt_XXXXXX", DIR => $tmpdir, UNLINK => 1);
print "RECORD FILE $tmpname $format \"$intkey\" \"$abs_timeout\" $beep \"$silence\"\n";
@result = checkresponse();
die "$name Failed to record file, aborting...\n" if ($result[0] == -1);
if ($debug) {
warn "$name Recording Format: $format, Rate: $samplerate Hz, ",
"Encoding format: ", ($use_speex) ? "speex" : "flac", "\n",
"$name Languge: $language, SSL: ", ($use_ssl) ? "yes, " : "no, ",
"$silence, Interrupt keys: $intkey\n";
}
# Encode sound data #
if ($use_speex) {
$filetype = "x-speex-with-header-byte";
$endian = (unpack("h*", pack("s", 1)) =~ /01/) ? "--be" : "--le";
# Encode file to speex. #
system($speex, "--vbr", "--rate", $samplerate, "--headerbyte", "--quiet", $endian,
"$tmpname.$format", "$tmpname.spx") == 0 or die "$name $speex failed: $?\n";
open($fh, "<", "$tmpname.spx") or die "Can't read file: $!";
} else {
$filetype = "x-flac";
$endian = (unpack("h*", pack("s", 1)) =~ /01/) ? "big" : "little";
# Encode file to flac. #
system($flac, $comp_level, "--totally-silent", "--channels=1", "--endian=$endian",
"--sign=signed", "--bps=16", "--force-raw-format", "--sample-rate=$samplerate",
"$tmpname.$format") == 0 or die "$name $flac failed: $?\n";
open($fh, "<", "$tmpname.flac") or die "Can't read file: $!";
}
$audio = do { local $/; <$fh> };
close($fh);
# Send adio data for analysis #
$uaresponse = $ua->post(
"$url",
Content_Type => "audio/$filetype; rate=$samplerate",
Content => "$audio",
);
if (!$uaresponse->is_success) {
print "VERBOSE \"Unable to get recognition data.\" 3\n";
checkresponse();
die "$name Unable to get recognition data.\n";
}
foreach (split(/\n/,$uaresponse->content)) {
my $jdata = decode_json($_);
for ( $jdata->{result}[0]->{alternative}[0] ) {
$response{utterance} = encode('utf8', $_->{transcript});
$response{confidence} = $_->{confidence};
}
}
warn "$name The response was:\n", $uaresponse->content if ($debug);
foreach (keys %response) {
warn "$name Setting variable: $_ = $response{$_}\n" if ($debug);
print "SET VARIABLE \"$_\" \"$response{$_}\"\n";
checkresponse();
}
exit;
sub checkresponse {
my $input = <STDIN>;
my @values;
chomp $input;
if ($input =~ /^200 result=(-?\d+)\s?(.*)$/) {
warn "$name Command returned: $input\n" if ($debug);
@values = ("$1", "$2");
} else {
$input .= <STDIN> if ($input =~ /^520-Invalid/);
warn "$name Unexpected result: $input\n";
@values = (-1, -1);
}
return @values;
}
sub detect_format {
# Detect the sound format used #
my @format;
print "GET FULL VARIABLE \${CHANNEL(audionativeformat)}\n";
my @reply = checkresponse();
for ($reply[1]) {
if (/(silk|sln)12/) { @format = ("sln12", 12000); }
elsif (/(speex|slin|silk)16|g722|siren7/) { @format = ("sln16", 16000); }
elsif (/(speex|slin|celt)32|siren14/) { @format = ("sln32", 32000); }
elsif (/(celt|slin)44/) { @format = ("sln44", 44100); }
elsif (/(celt|slin)48/) { @format = ("sln48", 48000); }
else { @format = ("sln", 8000); }
}
return @format;
}
sub int_handler {
die "$name Interrupt signal received, terminating...\n";
}
END {
if ($tmpname) {
warn "$name Cleaning temp files.\n" if ($debug);
unlink glob "$tmpname.*";
}
}