00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032 #include "asterisk.h"
00033
00034 ASTERISK_FILE_VERSION(__FILE__, "$Revision: 411313 $");
00035
00036 #include "asterisk/file.h"
00037 #include "asterisk/channel.h"
00038 #include "asterisk/pbx.h"
00039 #include "asterisk/module.h"
00040 #include "asterisk/lock.h"
00041 #include "asterisk/app.h"
00042 #include "asterisk/speech.h"
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
00056
00057
00058
00059
00060
00061
00062
00063
00064
00065
00066
00067
00068
00069
00070
00071
00072
00073
00074
00075
00076
00077
00078
00079
00080
00081
00082
00083
00084
00085
00086
00087
00088
00089
00090
00091
00092
00093
00094
00095
00096
00097
00098
00099
00100
00101
00102
00103
00104
00105
00106
00107
00108
00109
00110
00111
00112
00113
00114
00115
00116
00117
00118
00119
00120
00121
00122
00123
00124
00125
00126
00127
00128
00129
00130
00131
00132
00133
00134
00135
00136
00137
00138
00139
00140
00141
00142
00143
00144
00145
00146
00147
00148
00149
00150
00151
00152
00153
00154
00155
00156
00157
00158
00159
00160
00161
00162
00163
00164
00165
00166
00167
00168
00169
00170
00171
00172
00173
00174
00175
00176
00177
00178
00179
00180
00181
00182
00183
00184
00185
00186
00187
00188
00189
00190
00191
00192
00193
00194
00195
00196
00197
00198
00199
00200
00201
00202
00203
00204
00205
00206
00207
00208
00209
00210
00211
00212
00213
00214
00215
00216
00217
00218
00219
00220
00221
00222
00223
00224
00225
00226
00227
00228
00229
00230
00231
00232
00233
00234
00235
00236
00237
00238
00239
00240
00241
00242
00243
00244
00245
00246
00247
00248
00249
00250
00251
00252
00253
00254
00255
00256
00257
00258
00259
00260
00261
00262 static void destroy_callback(void *data)
00263 {
00264 struct ast_speech *speech = (struct ast_speech*)data;
00265
00266 if (speech == NULL) {
00267 return;
00268 }
00269
00270
00271 ast_speech_destroy(speech);
00272
00273 return;
00274 }
00275
00276
00277 static const struct ast_datastore_info speech_datastore = {
00278 .type = "speech",
00279 .destroy = destroy_callback
00280 };
00281
00282
00283 static struct ast_speech *find_speech(struct ast_channel *chan)
00284 {
00285 struct ast_speech *speech = NULL;
00286 struct ast_datastore *datastore = NULL;
00287
00288 if (!chan) {
00289 return NULL;
00290 }
00291
00292 datastore = ast_channel_datastore_find(chan, &speech_datastore, NULL);
00293 if (datastore == NULL) {
00294 return NULL;
00295 }
00296 speech = datastore->data;
00297
00298 return speech;
00299 }
00300
00301
00302 static struct ast_speech_result *find_result(struct ast_speech_result *results, char *result_num)
00303 {
00304 struct ast_speech_result *result = results;
00305 char *tmp = NULL;
00306 int nbest_num = 0, wanted_num = 0, i = 0;
00307
00308 if (!result) {
00309 return NULL;
00310 }
00311
00312 if ((tmp = strchr(result_num, '/'))) {
00313 *tmp++ = '\0';
00314 nbest_num = atoi(result_num);
00315 wanted_num = atoi(tmp);
00316 } else {
00317 wanted_num = atoi(result_num);
00318 }
00319
00320 do {
00321 if (result->nbest_num != nbest_num)
00322 continue;
00323 if (i == wanted_num)
00324 break;
00325 i++;
00326 } while ((result = AST_LIST_NEXT(result, list)));
00327
00328 return result;
00329 }
00330
00331
00332 static int speech_score(struct ast_channel *chan, const char *cmd, char *data,
00333 char *buf, size_t len)
00334 {
00335 struct ast_speech_result *result = NULL;
00336 struct ast_speech *speech = find_speech(chan);
00337 char tmp[128] = "";
00338
00339 if (data == NULL || speech == NULL || !(result = find_result(speech->results, data))) {
00340 return -1;
00341 }
00342
00343 snprintf(tmp, sizeof(tmp), "%d", result->score);
00344
00345 ast_copy_string(buf, tmp, len);
00346
00347 return 0;
00348 }
00349
00350 static struct ast_custom_function speech_score_function = {
00351 .name = "SPEECH_SCORE",
00352 .read = speech_score,
00353 .write = NULL,
00354 };
00355
00356
00357 static int speech_text(struct ast_channel *chan, const char *cmd, char *data,
00358 char *buf, size_t len)
00359 {
00360 struct ast_speech_result *result = NULL;
00361 struct ast_speech *speech = find_speech(chan);
00362
00363 if (data == NULL || speech == NULL || !(result = find_result(speech->results, data))) {
00364 return -1;
00365 }
00366
00367 if (result->text != NULL) {
00368 ast_copy_string(buf, result->text, len);
00369 } else {
00370 buf[0] = '\0';
00371 }
00372
00373 return 0;
00374 }
00375
00376 static struct ast_custom_function speech_text_function = {
00377 .name = "SPEECH_TEXT",
00378 .read = speech_text,
00379 .write = NULL,
00380 };
00381
00382
00383 static int speech_grammar(struct ast_channel *chan, const char *cmd, char *data,
00384 char *buf, size_t len)
00385 {
00386 struct ast_speech_result *result = NULL;
00387 struct ast_speech *speech = find_speech(chan);
00388
00389 if (data == NULL || speech == NULL || !(result = find_result(speech->results, data))) {
00390 return -1;
00391 }
00392
00393 if (result->grammar != NULL) {
00394 ast_copy_string(buf, result->grammar, len);
00395 } else {
00396 buf[0] = '\0';
00397 }
00398
00399 return 0;
00400 }
00401
00402 static struct ast_custom_function speech_grammar_function = {
00403 .name = "SPEECH_GRAMMAR",
00404 .read = speech_grammar,
00405 .write = NULL,
00406 };
00407
00408
00409 static int speech_engine_write(struct ast_channel *chan, const char *cmd, char *data, const char *value)
00410 {
00411 struct ast_speech *speech = find_speech(chan);
00412
00413 if (data == NULL || speech == NULL) {
00414 return -1;
00415 }
00416
00417 ast_speech_change(speech, data, value);
00418
00419 return 0;
00420 }
00421
00422 static struct ast_custom_function speech_engine_function = {
00423 .name = "SPEECH_ENGINE",
00424 .read = NULL,
00425 .write = speech_engine_write,
00426 };
00427
00428
00429 static int speech_results_type_write(struct ast_channel *chan, const char *cmd, char *data, const char *value)
00430 {
00431 struct ast_speech *speech = find_speech(chan);
00432
00433 if (data == NULL || speech == NULL)
00434 return -1;
00435
00436 if (!strcasecmp(value, "normal"))
00437 ast_speech_change_results_type(speech, AST_SPEECH_RESULTS_TYPE_NORMAL);
00438 else if (!strcasecmp(value, "nbest"))
00439 ast_speech_change_results_type(speech, AST_SPEECH_RESULTS_TYPE_NBEST);
00440
00441 return 0;
00442 }
00443
00444 static struct ast_custom_function speech_results_type_function = {
00445 .name = "SPEECH_RESULTS_TYPE",
00446 .read = NULL,
00447 .write = speech_results_type_write,
00448 };
00449
00450
00451 static int speech_read(struct ast_channel *chan, const char *cmd, char *data,
00452 char *buf, size_t len)
00453 {
00454 int results = 0;
00455 struct ast_speech_result *result = NULL;
00456 struct ast_speech *speech = find_speech(chan);
00457 char tmp[128] = "";
00458
00459
00460 if (!strcasecmp(data, "status")) {
00461 if (speech != NULL)
00462 ast_copy_string(buf, "1", len);
00463 else
00464 ast_copy_string(buf, "0", len);
00465 return 0;
00466 }
00467
00468
00469 if (speech == NULL) {
00470 return -1;
00471 }
00472
00473
00474 if (!strcasecmp(data, "spoke")) {
00475 if (ast_test_flag(speech, AST_SPEECH_SPOKE))
00476 ast_copy_string(buf, "1", len);
00477 else
00478 ast_copy_string(buf, "0", len);
00479 } else if (!strcasecmp(data, "results")) {
00480
00481 for (result = speech->results; result; result = AST_LIST_NEXT(result, list))
00482 results++;
00483 snprintf(tmp, sizeof(tmp), "%d", results);
00484 ast_copy_string(buf, tmp, len);
00485 } else {
00486 buf[0] = '\0';
00487 }
00488
00489 return 0;
00490 }
00491
00492 static struct ast_custom_function speech_function = {
00493 .name = "SPEECH",
00494 .read = speech_read,
00495 .write = NULL,
00496 };
00497
00498
00499
00500
00501 static int speech_create(struct ast_channel *chan, const char *data)
00502 {
00503 struct ast_speech *speech = NULL;
00504 struct ast_datastore *datastore = NULL;
00505
00506
00507 speech = ast_speech_new(data, chan->nativeformats);
00508 if (speech == NULL) {
00509
00510 pbx_builtin_setvar_helper(chan, "ERROR", "1");
00511 return 0;
00512 }
00513
00514 datastore = ast_datastore_alloc(&speech_datastore, NULL);
00515 if (datastore == NULL) {
00516 ast_speech_destroy(speech);
00517 pbx_builtin_setvar_helper(chan, "ERROR", "1");
00518 return 0;
00519 }
00520 pbx_builtin_setvar_helper(chan, "ERROR", NULL);
00521 datastore->data = speech;
00522 ast_channel_datastore_add(chan, datastore);
00523
00524 return 0;
00525 }
00526
00527
00528 static int speech_load(struct ast_channel *chan, const char *vdata)
00529 {
00530 int res = 0;
00531 struct ast_speech *speech = find_speech(chan);
00532 char *data;
00533 AST_DECLARE_APP_ARGS(args,
00534 AST_APP_ARG(grammar);
00535 AST_APP_ARG(path);
00536 );
00537
00538 data = ast_strdupa(vdata);
00539 AST_STANDARD_APP_ARGS(args, data);
00540
00541 if (speech == NULL)
00542 return -1;
00543
00544 if (args.argc != 2)
00545 return -1;
00546
00547
00548 res = ast_speech_grammar_load(speech, args.grammar, args.path);
00549
00550 return res;
00551 }
00552
00553
00554 static int speech_unload(struct ast_channel *chan, const char *data)
00555 {
00556 int res = 0;
00557 struct ast_speech *speech = find_speech(chan);
00558
00559 if (speech == NULL)
00560 return -1;
00561
00562
00563 res = ast_speech_grammar_unload(speech, data);
00564
00565 return res;
00566 }
00567
00568
00569 static int speech_deactivate(struct ast_channel *chan, const char *data)
00570 {
00571 int res = 0;
00572 struct ast_speech *speech = find_speech(chan);
00573
00574 if (speech == NULL)
00575 return -1;
00576
00577
00578 res = ast_speech_grammar_deactivate(speech, data);
00579
00580 return res;
00581 }
00582
00583
00584 static int speech_activate(struct ast_channel *chan, const char *data)
00585 {
00586 int res = 0;
00587 struct ast_speech *speech = find_speech(chan);
00588
00589 if (speech == NULL)
00590 return -1;
00591
00592
00593 res = ast_speech_grammar_activate(speech, data);
00594
00595 return res;
00596 }
00597
00598
00599 static int speech_start(struct ast_channel *chan, const char *data)
00600 {
00601 int res = 0;
00602 struct ast_speech *speech = find_speech(chan);
00603
00604 if (speech == NULL)
00605 return -1;
00606
00607 ast_speech_start(speech);
00608
00609 return res;
00610 }
00611
00612
00613 static int speech_processing_sound(struct ast_channel *chan, const char *data)
00614 {
00615 int res = 0;
00616 struct ast_speech *speech = find_speech(chan);
00617
00618 if (speech == NULL)
00619 return -1;
00620
00621 if (speech->processing_sound != NULL) {
00622 ast_free(speech->processing_sound);
00623 speech->processing_sound = NULL;
00624 }
00625
00626 speech->processing_sound = ast_strdup(data);
00627
00628 return res;
00629 }
00630
00631
00632 static int speech_streamfile(struct ast_channel *chan, const char *filename, const char *preflang)
00633 {
00634 struct ast_filestream *fs = NULL;
00635
00636 if (!(fs = ast_openstream(chan, filename, preflang)))
00637 return -1;
00638
00639 if (ast_applystream(chan, fs))
00640 return -1;
00641
00642 ast_playstream(fs);
00643
00644 return 0;
00645 }
00646
00647 enum {
00648 SB_OPT_NOANSWER = (1 << 0),
00649 };
00650
00651 AST_APP_OPTIONS(speech_background_options, BEGIN_OPTIONS
00652 AST_APP_OPTION('n', SB_OPT_NOANSWER),
00653 END_OPTIONS );
00654
00655
00656 static int speech_background(struct ast_channel *chan, const char *data)
00657 {
00658 unsigned int timeout = 0;
00659 int res = 0, done = 0, started = 0, quieted = 0, max_dtmf_len = 0;
00660 struct ast_speech *speech = find_speech(chan);
00661 struct ast_frame *f = NULL;
00662 int oldreadformat = AST_FORMAT_SLINEAR;
00663 char dtmf[AST_MAX_EXTENSION] = "";
00664 struct timeval start = { 0, 0 }, current;
00665 struct ast_datastore *datastore = NULL;
00666 char *parse, *filename_tmp = NULL, *filename = NULL, tmp[2] = "", dtmf_terminator = '#';
00667 const char *tmp2 = NULL;
00668 struct ast_flags options = { 0 };
00669 AST_DECLARE_APP_ARGS(args,
00670 AST_APP_ARG(soundfile);
00671 AST_APP_ARG(timeout);
00672 AST_APP_ARG(options);
00673 );
00674
00675 parse = ast_strdupa(data);
00676 AST_STANDARD_APP_ARGS(args, parse);
00677
00678 if (speech == NULL)
00679 return -1;
00680
00681 if (!ast_strlen_zero(args.options)) {
00682 char *options_buf = ast_strdupa(args.options);
00683 ast_app_parse_options(speech_background_options, &options, NULL, options_buf);
00684 }
00685
00686
00687 if (chan->_state != AST_STATE_UP && !ast_test_flag(&options, SB_OPT_NOANSWER)
00688 && ast_answer(chan)) {
00689 return -1;
00690 }
00691
00692
00693 oldreadformat = chan->readformat;
00694
00695
00696 if (ast_set_read_format(chan, speech->format))
00697 return -1;
00698
00699 if (!ast_strlen_zero(args.soundfile)) {
00700
00701 filename_tmp = ast_strdupa(args.soundfile);
00702 if (!ast_strlen_zero(args.timeout)) {
00703 if ((timeout = atof(args.timeout) * 1000.0) == 0)
00704 timeout = -1;
00705 } else
00706 timeout = 0;
00707 }
00708
00709
00710 ast_channel_lock(chan);
00711 if ((tmp2 = pbx_builtin_getvar_helper(chan, "SPEECH_DTMF_MAXLEN")) && !ast_strlen_zero(tmp2)) {
00712 max_dtmf_len = atoi(tmp2);
00713 }
00714
00715
00716 if ((tmp2 = pbx_builtin_getvar_helper(chan, "SPEECH_DTMF_TERMINATOR"))) {
00717 if (ast_strlen_zero(tmp2))
00718 dtmf_terminator = '\0';
00719 else
00720 dtmf_terminator = tmp2[0];
00721 }
00722 ast_channel_unlock(chan);
00723
00724
00725 if (speech->state == AST_SPEECH_STATE_NOT_READY || speech->state == AST_SPEECH_STATE_DONE) {
00726 ast_speech_change_state(speech, AST_SPEECH_STATE_NOT_READY);
00727 ast_speech_start(speech);
00728 }
00729
00730
00731 ast_stopstream(chan);
00732
00733
00734 while (done == 0) {
00735
00736 if (!quieted && (chan->streamid == -1 && chan->timingfunc == NULL) && (filename = strsep(&filename_tmp, "&"))) {
00737
00738 ast_stopstream(chan);
00739
00740 speech_streamfile(chan, filename, chan->language);
00741 }
00742
00743
00744 ast_sched_runq(chan->sched);
00745
00746
00747 res = ast_sched_wait(chan->sched);
00748 if (res < 0)
00749 res = 1000;
00750
00751
00752 if (ast_waitfor(chan, res) > 0) {
00753 f = ast_read(chan);
00754 if (f == NULL) {
00755
00756 done = 3;
00757 break;
00758 }
00759 }
00760
00761
00762 if ((!quieted || strlen(dtmf)) && started == 1) {
00763 current = ast_tvnow();
00764 if ((ast_tvdiff_ms(current, start)) >= timeout) {
00765 done = 1;
00766 if (f)
00767 ast_frfree(f);
00768 break;
00769 }
00770 }
00771
00772
00773 ast_mutex_lock(&speech->lock);
00774 if (ast_test_flag(speech, AST_SPEECH_QUIET)) {
00775 if (chan->stream)
00776 ast_stopstream(chan);
00777 ast_clear_flag(speech, AST_SPEECH_QUIET);
00778 quieted = 1;
00779 }
00780
00781 switch (speech->state) {
00782 case AST_SPEECH_STATE_READY:
00783
00784 if (chan->streamid == -1 && chan->timingfunc == NULL)
00785 ast_stopstream(chan);
00786 if (!quieted && chan->stream == NULL && timeout && started == 0 && !filename_tmp) {
00787 if (timeout == -1) {
00788 done = 1;
00789 if (f)
00790 ast_frfree(f);
00791 break;
00792 }
00793 start = ast_tvnow();
00794 started = 1;
00795 }
00796
00797 if (!strlen(dtmf) && f != NULL && f->frametype == AST_FRAME_VOICE) {
00798 ast_speech_write(speech, f->data.ptr, f->datalen);
00799 }
00800 break;
00801 case AST_SPEECH_STATE_WAIT:
00802
00803 if (!strlen(dtmf)) {
00804 if (chan->stream == NULL) {
00805 if (speech->processing_sound != NULL) {
00806 if (strlen(speech->processing_sound) > 0 && strcasecmp(speech->processing_sound, "none")) {
00807 speech_streamfile(chan, speech->processing_sound, chan->language);
00808 }
00809 }
00810 } else if (chan->streamid == -1 && chan->timingfunc == NULL) {
00811 ast_stopstream(chan);
00812 if (speech->processing_sound != NULL) {
00813 if (strlen(speech->processing_sound) > 0 && strcasecmp(speech->processing_sound, "none")) {
00814 speech_streamfile(chan, speech->processing_sound, chan->language);
00815 }
00816 }
00817 }
00818 }
00819 break;
00820 case AST_SPEECH_STATE_DONE:
00821
00822 ast_speech_change_state(speech, AST_SPEECH_STATE_NOT_READY);
00823 if (!strlen(dtmf)) {
00824
00825 speech->results = ast_speech_results_get(speech);
00826
00827 done = 1;
00828
00829 if (chan->stream != NULL) {
00830 ast_stopstream(chan);
00831 }
00832 }
00833 break;
00834 default:
00835 break;
00836 }
00837 ast_mutex_unlock(&speech->lock);
00838
00839
00840 if (f != NULL) {
00841
00842 switch (f->frametype) {
00843 case AST_FRAME_DTMF:
00844 if (dtmf_terminator != '\0' && f->subclass.integer == dtmf_terminator) {
00845 done = 1;
00846 } else {
00847 quieted = 1;
00848 if (chan->stream != NULL) {
00849 ast_stopstream(chan);
00850 }
00851 if (!started) {
00852
00853 timeout = (chan->pbx && chan->pbx->dtimeoutms) ? chan->pbx->dtimeoutms : 5000;
00854 started = 1;
00855 }
00856 start = ast_tvnow();
00857 snprintf(tmp, sizeof(tmp), "%c", f->subclass.integer);
00858 strncat(dtmf, tmp, sizeof(dtmf) - strlen(dtmf) - 1);
00859
00860 if (max_dtmf_len && strlen(dtmf) == max_dtmf_len)
00861 done = 1;
00862 }
00863 break;
00864 case AST_FRAME_CONTROL:
00865 switch (f->subclass.integer) {
00866 case AST_CONTROL_HANGUP:
00867
00868 done = 3;
00869 default:
00870 break;
00871 }
00872 default:
00873 break;
00874 }
00875 ast_frfree(f);
00876 f = NULL;
00877 }
00878 }
00879
00880 if (!ast_strlen_zero(dtmf)) {
00881
00882 speech->results = ast_calloc(1, sizeof(*speech->results));
00883 if (speech->results != NULL) {
00884 ast_speech_dtmf(speech, dtmf);
00885 speech->results->score = 1000;
00886 speech->results->text = ast_strdup(dtmf);
00887 speech->results->grammar = ast_strdup("dtmf");
00888 }
00889 ast_speech_change_state(speech, AST_SPEECH_STATE_NOT_READY);
00890 }
00891
00892
00893 if (done == 3) {
00894
00895 ast_speech_destroy(speech);
00896 datastore = ast_channel_datastore_find(chan, &speech_datastore, NULL);
00897 if (datastore != NULL)
00898 ast_channel_datastore_remove(chan, datastore);
00899 } else {
00900
00901 ast_set_read_format(chan, oldreadformat);
00902 }
00903
00904 return 0;
00905 }
00906
00907
00908
00909 static int speech_destroy(struct ast_channel *chan, const char *data)
00910 {
00911 int res = 0;
00912 struct ast_speech *speech = find_speech(chan);
00913 struct ast_datastore *datastore = NULL;
00914
00915 if (speech == NULL)
00916 return -1;
00917
00918
00919 ast_speech_destroy(speech);
00920
00921 datastore = ast_channel_datastore_find(chan, &speech_datastore, NULL);
00922 if (datastore != NULL) {
00923 ast_channel_datastore_remove(chan, datastore);
00924 }
00925
00926 return res;
00927 }
00928
00929 static int unload_module(void)
00930 {
00931 int res = 0;
00932
00933 res = ast_unregister_application("SpeechCreate");
00934 res |= ast_unregister_application("SpeechLoadGrammar");
00935 res |= ast_unregister_application("SpeechUnloadGrammar");
00936 res |= ast_unregister_application("SpeechActivateGrammar");
00937 res |= ast_unregister_application("SpeechDeactivateGrammar");
00938 res |= ast_unregister_application("SpeechStart");
00939 res |= ast_unregister_application("SpeechBackground");
00940 res |= ast_unregister_application("SpeechDestroy");
00941 res |= ast_unregister_application("SpeechProcessingSound");
00942 res |= ast_custom_function_unregister(&speech_function);
00943 res |= ast_custom_function_unregister(&speech_score_function);
00944 res |= ast_custom_function_unregister(&speech_text_function);
00945 res |= ast_custom_function_unregister(&speech_grammar_function);
00946 res |= ast_custom_function_unregister(&speech_engine_function);
00947 res |= ast_custom_function_unregister(&speech_results_type_function);
00948
00949 return res;
00950 }
00951
00952 static int load_module(void)
00953 {
00954 int res = 0;
00955
00956 res = ast_register_application_xml("SpeechCreate", speech_create);
00957 res |= ast_register_application_xml("SpeechLoadGrammar", speech_load);
00958 res |= ast_register_application_xml("SpeechUnloadGrammar", speech_unload);
00959 res |= ast_register_application_xml("SpeechActivateGrammar", speech_activate);
00960 res |= ast_register_application_xml("SpeechDeactivateGrammar", speech_deactivate);
00961 res |= ast_register_application_xml("SpeechStart", speech_start);
00962 res |= ast_register_application_xml("SpeechBackground", speech_background);
00963 res |= ast_register_application_xml("SpeechDestroy", speech_destroy);
00964 res |= ast_register_application_xml("SpeechProcessingSound", speech_processing_sound);
00965 res |= ast_custom_function_register(&speech_function);
00966 res |= ast_custom_function_register(&speech_score_function);
00967 res |= ast_custom_function_register(&speech_text_function);
00968 res |= ast_custom_function_register(&speech_grammar_function);
00969 res |= ast_custom_function_register(&speech_engine_function);
00970 res |= ast_custom_function_register(&speech_results_type_function);
00971
00972 return res;
00973 }
00974
00975 AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_DEFAULT, "Dialplan Speech Applications",
00976 .load = load_module,
00977 .unload = unload_module,
00978 .nonoptreq = "res_speech",
00979 );