mod_rayo: add support for speech recognizers other than pocketsphinx, fixed some input component bugs, allow simultaneous dtmf and voice input.

This commit is contained in:
Chris Rienzo 2013-08-14 09:41:11 -04:00
parent 8859df6789
commit 96eba0d6a8
13 changed files with 408 additions and 157 deletions

View File

@ -11,6 +11,11 @@
<param name="record-file-prefix" value="$${recordings_dir}/"/>
</record>
<!-- input component params -->
<input>
<param name="default-recognizer" value="pocketsphinx"/>
</input>
<!-- XMPP server domain -->
<domain name="$${rayo_domain_name}" shared-secret="ClueCon">
<!-- use this instead if you want secure XMPP client to server connections. Put .crt and .key file in freeswitch/certs -->

View File

@ -11,6 +11,11 @@
<param name="record-file-prefix" value="$${recordings_dir}/"/>
</record>
<!-- input component params -->
<input>
<param name="default-recognizer" value="pocketsphinx"/>
</input>
<!-- XMPP server domain -->
<domain name="$${rayo_domain_name}" shared-secret="ClueCon">
<!-- use this instead if you want secure XMPP client to server connections. Put .crt and .key file in freeswitch/certs -->

View File

@ -216,6 +216,17 @@ double iks_find_decimal_attrib(iks *xml, const char *attrib)
return atof(iks_find_attrib_soft(xml, attrib));
}
/**
* Get attribute character value of node
* @param xml the XML node to search
* @param attrib the Attribute name
* @return the attribute value
*/
char iks_find_char_attrib(iks *xml, const char *attrib)
{
return iks_find_attrib_soft(xml, attrib)[0];
}
/**
* Convert iksemel XML node type to string
* @param type the XML node type
@ -392,6 +403,54 @@ int iks_attrib_is_decimal_between_zero_and_one(const char *value)
return SWITCH_FALSE;
}
/**
* Validate dtmf digit
* @param value
* @return SWITCH_TRUE if 0-9,a,b,c,d,A,B,C,D,*,#
*/
int iks_attrib_is_dtmf_digit(const char *value)
{
if (value && *value && strlen(value) == 1) {
switch (*value) {
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
case 'A':
case 'a':
case 'B':
case 'b':
case 'C':
case 'c':
case 'D':
case 'd':
case '*':
case '#':
return SWITCH_TRUE;
}
}
return SWITCH_FALSE;
}
/**
* @param fn to evaluate attribute
* @param attrib to evaluate
* @return true if not set or is valid
*/
int validate_optional_attrib(iks_attrib_validation_function fn, const char *attrib)
{
if (!attrib || !*attrib) {
return SWITCH_TRUE;
}
return fn(attrib);
}
#define IKS_SHA256_HEX_DIGEST_LENGTH ((SHA256_DIGEST_LENGTH * 2) + 1)
/**

View File

@ -63,6 +63,7 @@ extern const char *iks_find_attrib_soft(iks *xml, const char *attrib);
extern const char *iks_find_attrib_default(iks *xml, const char *attrib, const char *def);
extern int iks_find_bool_attrib(iks *xml, const char *attrib);
extern int iks_find_int_attrib(iks *xml, const char *attrib);
extern char iks_find_char_attrib(iks *xml, const char *attrib);
extern double iks_find_decimal_attrib(iks *xml, const char *attrib);
extern const char *iks_node_type_to_string(int type);
extern const char *iks_net_error_to_string(int err);
@ -73,9 +74,12 @@ extern char *iks_server_dialback_key(const char *secret, const char *receiving_s
/** A function to validate attribute value */
typedef int (*iks_attrib_validation_function)(const char *);
extern int validate_optional_attrib(iks_attrib_validation_function fn, const char *attrib);
#define ELEMENT_DECL(name) extern int VALIDATE_##name(iks *node);
#define ELEMENT(name) int VALIDATE_##name(iks *node) { int result = 1; if (!node) return 0;
#define ATTRIB(name, def, rule) result &= iks_attrib_is_##rule(iks_find_attrib_default(node, #name, #def));
#define OPTIONAL_ATTRIB(name, def, rule) result &= validate_optional_attrib(iks_attrib_is_##rule, iks_find_attrib_default(node, #name, #def));
#define STRING_ATTRIB(name, def, rule) result &= value_matches(iks_find_attrib_default(node, #name, #def), rule);
#define ELEMENT_END return result; }
@ -87,6 +91,7 @@ extern int iks_attrib_is_positive(const char *value);
extern int iks_attrib_is_positive_or_neg_one(const char *value);
extern int iks_attrib_is_any(const char *value);
extern int iks_attrib_is_decimal_between_zero_and_one(const char *value);
extern int iks_attrib_is_dtmf_digit(const char *value);
#endif

View File

@ -3737,6 +3737,14 @@ SWITCH_MODULE_LOAD_FUNCTION(mod_rayo_load)
"</grammar></input>"
"</prompt>");
rayo_add_cmd_alias("prompt_barge_mrcp", "<prompt xmlns=\""RAYO_PROMPT_NS"\" barge-in=\"true\">"
"<output xmlns=\""RAYO_OUTPUT_NS"\" repeat-times=\"5\"><document content-type=\"application/ssml+xml\"><![CDATA[<speak><p>Please press a digit.</p></speak>]]></document></output>"
"<input xmlns=\""RAYO_INPUT_NS"\" mode=\"any\" initial-timeout=\"5000\" inter-digit-timeout=\"3000\">"
"<grammar content-type=\"application/srgs+xml\">"
"<![CDATA[<grammar mode=\"dtmf\"><rule id=\"digit\" scope=\"public\"><one-of><item>0</item><item>1</item><item>2</item><item>3</item><item>4</item><item>5</item><item>6</item><item>7</item><item>8</item><item>9</item></one-of></rule></grammar>]]>"
"</grammar></input>"
"</prompt>");
rayo_add_cmd_alias("prompt_no_barge", "<prompt xmlns=\""RAYO_PROMPT_NS"\" barge-in=\"false\">"
"<output xmlns=\""RAYO_OUTPUT_NS"\" repeat-times=\"2\"><document content-type=\"application/ssml+xml\"><![CDATA[<speak><p>Please press a digit.</p></speak>]]></document></output>"
"<input xmlns=\""RAYO_INPUT_NS"\" mode=\"dtmf\" initial-timeout=\"5000\" inter-digit-timeout=\"3000\">"
@ -3800,6 +3808,34 @@ SWITCH_MODULE_LOAD_FUNCTION(mod_rayo_load)
"<unjoin xmlns=\""RAYO_NS"\" mixer-name=\"test\"/>");
rayo_add_cmd_alias("unjoin",
"<unjoin xmlns=\""RAYO_NS"\"/>");
rayo_add_cmd_alias("input_voice_yesno_unimrcp",
"<input xmlns=\""RAYO_INPUT_NS"\" mode=\"voice\" recognizer=\"unimrcp\"><grammar content-type=\"application/srgs+xml\">"
"<![CDATA[<grammar xmlns=\"http://www.w3.org/2001/06/grammar\" "
"xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" "
"xsi:schemaLocation=\"http://www.w3.org/2001/06/grammar http://www.w3.org/TR/speech-grammar/grammar.xsd\" "
"xml:lang=\"en-US\" version=\"1.0\">"
"<rule id=\"yesno\"><one-of><item>yes</item><item>no</item></one-of></rule></grammar>]]></grammar></input>");
rayo_add_cmd_alias("input_voice_yesno_unimrcp_timeout",
"<input xmlns=\""RAYO_INPUT_NS"\" mode=\"voice\" recognizer=\"unimrcp\" max-silence=\"5\" initial-timeout=\"5000\"><grammar content-type=\"application/srgs+xml\">"
"<![CDATA[<grammar xmlns=\"http://www.w3.org/2001/06/grammar\" "
"xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" "
"xsi:schemaLocation=\"http://www.w3.org/2001/06/grammar http://www.w3.org/TR/speech-grammar/grammar.xsd\" "
"xml:lang=\"en-US\" version=\"1.0\">"
"<rule id=\"yesno\"><one-of><item>yes</item><item>no</item></one-of></rule></grammar>]]></grammar></input>");
rayo_add_cmd_alias("input_voice_yesno_pocketsphinx",
"<input xmlns=\""RAYO_INPUT_NS"\" mode=\"voice\" recognizer=\"pocketsphinx\" max-silence=\"5000\" initial-timeout=\"5000\"><grammar content-type=\"application/srgs+xml\">"
"<![CDATA[<grammar xmlns=\"http://www.w3.org/2001/06/grammar\" "
"xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" "
"xsi:schemaLocation=\"http://www.w3.org/2001/06/grammar http://www.w3.org/TR/speech-grammar/grammar.xsd\" "
"xml:lang=\"en-US\" version=\"1.0\">"
"<rule id=\"yesno\"><one-of><item>yes</item><item>no</item></one-of></rule></grammar>]]></grammar></input>");
rayo_add_cmd_alias("input_voice_yesno_default",
"<input xmlns=\""RAYO_INPUT_NS"\" mode=\"voice\"><grammar content-type=\"application/srgs+xml\">"
"<![CDATA[<grammar xmlns=\"http://www.w3.org/2001/06/grammar\" "
"xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" "
"xsi:schemaLocation=\"http://www.w3.org/2001/06/grammar http://www.w3.org/TR/speech-grammar/grammar.xsd\" "
"xml:lang=\"en-US\" version=\"1.0\">"
"<rule id=\"yesno\"><one-of><item>yes</item><item>no</item></one-of></rule></grammar>]]></grammar></input>");
return SWITCH_STATUS_SUCCESS;
}

View File

@ -227,15 +227,10 @@ void rayo_component_api_execute_async(struct rayo_component *component, const ch
*/
switch_status_t rayo_components_load(switch_loadable_module_interface_t **module_interface, switch_memory_pool_t *pool, const char *config_file)
{
rayo_input_component_load();
rayo_output_component_load(module_interface, pool);
rayo_prompt_component_load();
rayo_record_component_load(pool, config_file);
if (rayo_input_component_load() != SWITCH_STATUS_SUCCESS ||
rayo_output_component_load(module_interface, pool) != SWITCH_STATUS_SUCCESS ||
rayo_prompt_component_load() != SWITCH_STATUS_SUCCESS ||
rayo_record_component_load(pool, config_file) != SWITCH_STATUS_SUCCESS) {
if (rayo_input_component_load(module_interface, pool, config_file) != SWITCH_STATUS_SUCCESS ||
rayo_output_component_load(module_interface, pool, config_file) != SWITCH_STATUS_SUCCESS ||
rayo_prompt_component_load(module_interface, pool, config_file) != SWITCH_STATUS_SUCCESS ||
rayo_record_component_load(module_interface, pool, config_file) != SWITCH_STATUS_SUCCESS) {
return SWITCH_STATUS_TERM;
}
return SWITCH_STATUS_SUCCESS;

View File

@ -54,10 +54,10 @@
#define COMPONENT_COMPLETE_HANGUP "hangup", RAYO_EXT_COMPLETE_NS
extern switch_status_t rayo_components_load(switch_loadable_module_interface_t **module_interface, switch_memory_pool_t *pool, const char *config_file);
extern switch_status_t rayo_input_component_load(void);
extern switch_status_t rayo_output_component_load(switch_loadable_module_interface_t **module_interface, switch_memory_pool_t *pool);
extern switch_status_t rayo_prompt_component_load(void);
extern switch_status_t rayo_record_component_load(switch_memory_pool_t *pool, const char *config_file);
extern switch_status_t rayo_input_component_load(switch_loadable_module_interface_t **module_interface, switch_memory_pool_t *pool, const char *config_file);
extern switch_status_t rayo_output_component_load(switch_loadable_module_interface_t **module_interface, switch_memory_pool_t *pool, const char *config_file);
extern switch_status_t rayo_prompt_component_load(switch_loadable_module_interface_t **module_interface, switch_memory_pool_t *pool, const char *config_file);
extern switch_status_t rayo_record_component_load(switch_loadable_module_interface_t **module_interface, switch_memory_pool_t *pool, const char *config_file);
extern switch_status_t rayo_components_shutdown(void);
extern switch_status_t rayo_input_component_shutdown(void);

View File

@ -33,7 +33,7 @@
*/
ELEMENT(RAYO_INPUT)
STRING_ATTRIB(mode, any, "any,dtmf,voice")
ATTRIB(terminator,, any)
OPTIONAL_ATTRIB(terminator,, dtmf_digit)
ATTRIB(recognizer,, any)
ATTRIB(language, en-US, any)
ATTRIB(initial-timeout, -1, positive_or_neg_one)

View File

@ -45,6 +45,8 @@ struct input_handler;
static struct {
/** grammar parser */
struct srgs_parser *parser;
/** default recognizer to use if none specified */
const char *default_recognizer;
} globals;
/**
@ -57,8 +59,8 @@ struct input_component {
int speech_mode;
/** Number of collected digits */
int num_digits;
/** Terminating digits */
int term_digit_mask;
/** Terminating digit */
char term_digit;
/** The collected digits */
char digits[MAX_DTMF + 1];
/** grammar to match */
@ -70,7 +72,9 @@ struct input_component {
/** maximum silence allowed */
int max_silence;
/** minimum speech detection confidence */
int min_confidence;
double min_confidence;
/** sensitivity to background noise */
double sensitivity;
/** timeout after first digit is received */
int inter_digit_timeout;
/** stop flag */
@ -79,6 +83,10 @@ struct input_component {
int start_timers;
/** true if event fired for first digit / start of speech */
int barge_event;
/** optional language to use */
const char *language;
/** optional recognizer to use */
const char *recognizer;
/** global data */
struct input_handler *handler;
};
@ -91,77 +99,24 @@ struct input_component {
struct input_handler {
/** media bug to monitor frames / control input lifecycle */
switch_media_bug_t *bug;
/** active input component - TODO multiple inputs */
struct input_component *component;
/** active voice input component */
struct input_component *voice_component;
/** active dtmf input component */
struct input_component *dtmf_component;
/** synchronizes media bug and dtmf callbacks */
switch_mutex_t *mutex;
/** last recognizer used */
const char *last_recognizer;
};
/**
* @return digit mask
* @param digit1 to match
* @param digit2 to match
* @return true if matching
*/
static int get_digit_mask(char digit)
static int digit_test(char digit1, char digit2)
{
switch(digit) {
case '0': return 1;
case '1': return 1 << 1;
case '2': return 1 << 2;
case '3': return 1 << 3;
case '4': return 1 << 4;
case '5': return 1 << 5;
case '6': return 1 << 6;
case '7': return 1 << 7;
case '8': return 1 << 8;
case '9': return 1 << 9;
case 'A':
case 'a': return 1 << 10;
case 'B':
case 'b': return 1 << 11;
case 'C':
case 'c': return 1 << 12;
case 'D':
case 'd': return 1 << 13;
case '#': return 1 << 14;
case '*': return 1 << 15;
}
return 0;
}
/**
* @param digit_mask to check
* @param digit to look for
* @return true if set
*/
static int digit_mask_test(int digit_mask, char digit)
{
return digit_mask & get_digit_mask(digit);
}
/**
* @param digit_mask to set digit in
* @param digit to set
* @return the digit mask with the set digit
*/
static int digit_mask_set(int digit_mask, char digit)
{
return digit_mask | get_digit_mask(digit);
}
/**
* @param digit_mask to set digits in
* @param digits to add to mask
* @return the digit mask with the set digits
*/
static int digit_mask_set_from_digits(int digit_mask, const char *digits)
{
if (!zstr(digits)) {
int digits_len = strlen(digits);
int i;
for (i = 0; i < digits_len; i++) {
digit_mask = digit_mask_set(digit_mask, digits[i]);
}
}
return digit_mask;
return digit1 && digit2 && tolower(digit1) == tolower(digit2);
}
/**
@ -205,15 +160,14 @@ static switch_status_t input_component_on_dtmf(switch_core_session_t *session, c
switch_mutex_lock(handler->mutex);
component = handler->component;
component = handler->dtmf_component;
/* additional paranoia check */
if (!component) {
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_INFO, "Received DTMF without active input component\n");
switch_mutex_unlock(handler->mutex);
return SWITCH_STATUS_SUCCESS;
}
is_term_digit = digit_mask_test(component->term_digit_mask, dtmf->digit);
is_term_digit = digit_test(component->term_digit, dtmf->digit);
if (!is_term_digit) {
component->digits[component->num_digits] = dtmf->digit;
@ -247,7 +201,7 @@ static switch_status_t input_component_on_dtmf(switch_core_session_t *session, c
}
case SMT_NO_MATCH: {
/* notify of no-match and remove input component */
handler->component = NULL;
handler->dtmf_component = NULL;
switch_core_media_bug_remove(session, &handler->bug);
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_DEBUG, "NO MATCH = %s\n", component->digits);
rayo_component_send_complete(RAYO_COMPONENT(component), INPUT_NOMATCH);
@ -256,7 +210,7 @@ static switch_status_t input_component_on_dtmf(switch_core_session_t *session, c
case SMT_MATCH_END: {
iks *result = nlsml_create_dtmf_match(component->digits);
/* notify of match and remove input component */
handler->component = NULL;
handler->dtmf_component = NULL;
switch_core_media_bug_remove(session, &handler->bug);
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_DEBUG, "MATCH = %s\n", component->digits);
send_match_event(RAYO_COMPONENT(component), result);
@ -279,7 +233,7 @@ static switch_bool_t input_component_bug_callback(switch_media_bug_t *bug, void
struct input_component *component;
switch_mutex_lock(handler->mutex);
component = handler->component;
component = handler->dtmf_component;
switch(type) {
case SWITCH_ABC_TYPE_INIT: {
@ -294,7 +248,7 @@ static switch_bool_t input_component_bug_callback(switch_media_bug_t *bug, void
int elapsed_ms = (switch_micro_time_now() - component->last_digit_time) / 1000;
if (component->num_digits && component->inter_digit_timeout > 0 && elapsed_ms > component->inter_digit_timeout) {
enum srgs_match_type match;
handler->component = NULL;
handler->dtmf_component = NULL;
switch_core_media_bug_set_flag(bug, SMBF_PRUNE);
/* we got some input, check for match */
@ -310,7 +264,7 @@ static switch_bool_t input_component_bug_callback(switch_media_bug_t *bug, void
rayo_component_send_complete(RAYO_COMPONENT(component), INPUT_NOMATCH);
}
} else if (!component->num_digits && component->initial_timeout > 0 && elapsed_ms > component->initial_timeout) {
handler->component = NULL;
handler->dtmf_component = NULL;
switch_core_media_bug_set_flag(bug, SMBF_PRUNE);
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_DEBUG, "initial-timeout\n");
rayo_component_send_complete(RAYO_COMPONENT(component), INPUT_NOINPUT);
@ -323,10 +277,10 @@ static switch_bool_t input_component_bug_callback(switch_media_bug_t *bug, void
/* check for hangup */
if (component) {
if (component->stop) {
handler->component = NULL;
handler->dtmf_component = NULL;
rayo_component_send_complete(RAYO_COMPONENT(component), COMPONENT_COMPLETE_STOP);
} else {
handler->component = NULL;
handler->dtmf_component = NULL;
rayo_component_send_complete(RAYO_COMPONENT(component), COMPONENT_COMPLETE_HANGUP);
}
}
@ -396,34 +350,53 @@ static iks *start_call_input(struct input_component *component, switch_core_sess
handler = switch_core_session_alloc(session, sizeof(*handler));
switch_mutex_init(&handler->mutex, SWITCH_MUTEX_NESTED, switch_core_session_get_pool(session));
switch_channel_set_private(switch_core_session_get_channel(session), RAYO_INPUT_COMPONENT_PRIVATE_VAR, handler);
handler->last_recognizer = "";
}
handler->component = component;
/* TODO break up this function by mode... dtmf/voice/fax/etc */
component->speech_mode = strcmp(iks_find_attrib_soft(input, "mode"), "dtmf");
if (component->speech_mode && handler->voice_component) {
/* don't allow multi voice input */
return iks_new_error_detailed(iq, STANZA_ERROR_CONFLICT, "Multiple voice input is not allowed");
}
if (!component->speech_mode && handler->dtmf_component) {
/* don't allow multi dtmf input */
return iks_new_error_detailed(iq, STANZA_ERROR_CONFLICT, "Multiple dtmf input is not allowed");
}
if (component->speech_mode) {
handler->voice_component = component;
} else {
handler->dtmf_component = component;
}
component->grammar = NULL;
component->num_digits = 0;
component->digits[0] = '\0';
component->stop = 0;
component->speech_mode = 0;
component->initial_timeout = iks_find_int_attrib(input, "initial-timeout");
component->inter_digit_timeout = iks_find_int_attrib(input, "inter-digit-timeout");
component->max_silence = iks_find_int_attrib(input, "max-silence");
component->min_confidence = (int)ceil(iks_find_decimal_attrib(input, "min-confidence") * 100.0);
component->min_confidence = iks_find_decimal_attrib(input, "min-confidence");
component->sensitivity = iks_find_decimal_attrib(input, "sensitivity");
component->barge_event = iks_find_bool_attrib(input, "barge-event");
component->start_timers = iks_find_bool_attrib(input, "start-timers");
/* TODO this should just be a single digit terminator? */
component->term_digit_mask = digit_mask_set_from_digits(0, iks_find_attrib_soft(input, "terminator"));
/* TODO recognizer ignored */
/* TODO language ignored */
component->term_digit = iks_find_char_attrib(input, "terminator");
component->recognizer = iks_find_attrib(input, "recognizer");
component->language = iks_find_attrib(input, "language");
component->handler = handler;
/* parse the grammar */
if (!(component->grammar = srgs_parse(globals.parser, iks_find_cdata(input, "grammar")))) {
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_DEBUG, "Failed to parse grammar body\n");
RAYO_UNLOCK(component);
RAYO_DESTROY(component);
return iks_new_error_detailed(iq, STANZA_ERROR_BAD_REQUEST, "Failed to parse grammar body");
}
/* is this voice or dtmf srgs grammar? */
if (!strcasecmp("dtmf", iks_find_attrib_soft(input, "mode"))) {
if (!component->speech_mode) {
/* parse the grammar */
if (!(component->grammar = srgs_parse(globals.parser, iks_find_cdata(input, "grammar")))) {
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_DEBUG, "Failed to parse grammar body\n");
RAYO_UNLOCK(component);
RAYO_DESTROY(component);
return iks_new_error_detailed(iq, STANZA_ERROR_BAD_REQUEST, "Failed to parse grammar body");
}
component->last_digit_time = switch_micro_time_now();
/* acknowledge command */
@ -431,38 +404,124 @@ static iks *start_call_input(struct input_component *component, switch_core_sess
/* start dtmf input detection */
if (switch_core_media_bug_add(session, "rayo_input_component", NULL, input_component_bug_callback, handler, 0, SMBF_READ_REPLACE, &handler->bug) != SWITCH_STATUS_SUCCESS) {
handler->dtmf_component = NULL;
rayo_component_send_complete(RAYO_COMPONENT(component), COMPONENT_COMPLETE_ERROR);
}
} else {
char *grammar = NULL;
const char *jsgf_path;
component->speech_mode = 1;
jsgf_path = srgs_grammar_to_jsgf_file(component->grammar, SWITCH_GLOBAL_dirs.grammar_dir, "gram");
if (!jsgf_path) {
switch_stream_handle_t grammar = { 0 };
SWITCH_STANDARD_STREAM(grammar);
if (zstr(component->recognizer)) {
component->recognizer = globals.default_recognizer;
}
/* if recognition engine is different, we can't handle this request */
if (!zstr(handler->last_recognizer) && strcmp(component->recognizer, handler->last_recognizer)) {
handler->voice_component = NULL;
RAYO_UNLOCK(component);
RAYO_DESTROY(component);
return iks_new_error_detailed(iq, STANZA_ERROR_INTERNAL_SERVER_ERROR, "Grammar error");
return iks_new_error_detailed(iq, STANZA_ERROR_BAD_REQUEST, "Must use the same recognizer for the entire call");
}
handler->last_recognizer = switch_core_session_strdup(session, component->recognizer);
if (!strcmp(component->recognizer, "pocketsphinx")) {
const char *jsgf_path;
/* transform SRGS grammar to JSGF */
if (!(component->grammar = srgs_parse(globals.parser, iks_find_cdata(input, "grammar")))) {
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_DEBUG, "Failed to parse grammar body\n");
handler->voice_component = NULL;
RAYO_UNLOCK(component);
RAYO_DESTROY(component);
return iks_new_error_detailed(iq, STANZA_ERROR_BAD_REQUEST, "Failed to parse grammar body");
}
jsgf_path = srgs_grammar_to_jsgf_file(component->grammar, SWITCH_GLOBAL_dirs.grammar_dir, "gram");
if (!jsgf_path) {
handler->voice_component = NULL;
RAYO_UNLOCK(component);
RAYO_DESTROY(component);
return iks_new_error_detailed(iq, STANZA_ERROR_INTERNAL_SERVER_ERROR, "Grammar conversion to JSGF error");
}
/* build pocketsphinx grammar string */
grammar.write_function(&grammar,
"{start-input-timers=%s,no-input-timeout=%d,speech-timeout=%d,confidence-threshold=%d}%s",
component->start_timers ? "true" : "false",
component->initial_timeout,
component->max_silence,
(int)ceil(component->min_confidence * 100.0),
jsgf_path);
} else if (!strncmp(component->recognizer, "unimrcp", strlen("unimrcp"))) {
/* send inline grammar to unimrcp */
grammar.write_function(&grammar, "{start-input-timers=%s,confidence-threshold=%f,sensitivity-level=%f",
component->start_timers ? "true" : "false",
component->min_confidence,
component->sensitivity);
if (component->initial_timeout > 0) {
grammar.write_function(&grammar, ",no-input-timeout=%d",
component->initial_timeout);
}
if (component->max_silence > 0) {
grammar.write_function(&grammar, ",speech-complete-timeout=%d,speech-incomplete-timeout=%d",
component->max_silence,
component->max_silence);
}
if (!zstr(component->language)) {
grammar.write_function(&grammar, ",speech-language=%s", component->language);
}
if (!strcmp(iks_find_attrib_soft(input, "mode"), "any")) {
/* set dtmf params */
if (component->inter_digit_timeout > 0) {
grammar.write_function(&grammar, ",dtmf-interdigit-timeout=%d", component->inter_digit_timeout);
}
if (component->term_digit) {
grammar.write_function(&grammar, ",dtmf-term-char=%c", component->term_digit);
}
}
grammar.write_function(&grammar, "}inline:%s", iks_find_cdata(input, "grammar"));
} else {
/* passthrough to unknown ASR module */
grammar.write_function(&grammar, "%s", iks_find_cdata(input, "grammar"));
}
/* acknowledge command */
rayo_component_send_start(RAYO_COMPONENT(component), iq);
/* TODO configurable speech detection - different engines, grammar passthrough, dtmf handled by recognizer */
grammar = switch_mprintf("{no-input-timeout=%s,speech-timeout=%s,start-input-timers=%s,confidence-threshold=%d}%s",
component->initial_timeout, component->max_silence,
component->start_timers ? "true" : "false",
component->min_confidence, jsgf_path);
/* start speech detection */
switch_channel_set_variable(switch_core_session_get_channel(session), "fire_asr_events", "true");
if (switch_ivr_detect_speech(session, "pocketsphinx", grammar, "mod_rayo_grammar", "", NULL) != SWITCH_STATUS_SUCCESS) {
if (switch_ivr_detect_speech(session, component->recognizer, grammar.data, "mod_rayo_grammar", "", NULL) != SWITCH_STATUS_SUCCESS) {
handler->voice_component = NULL;
rayo_component_send_complete(RAYO_COMPONENT(component), COMPONENT_COMPLETE_ERROR);
}
switch_safe_free(grammar);
switch_safe_free(grammar.data);
}
return NULL;
}
/**
* Create input component id for session.
* @param session requesting component
* @param input request
* @return the ID
*/
static char *create_input_component_id(switch_core_session_t *session, iks *input)
{
const char *mode = "unk";
if (input) {
mode = iks_find_attrib_soft(input, "mode");
if (!strcmp(mode, "any")) {
mode = "voice";
}
}
return switch_core_session_sprintf(session, "%s-input-%s", switch_core_session_get_uuid(session), mode);
}
/**
* Start execution of input component
*/
@ -470,10 +529,10 @@ static iks *start_call_input_component(struct rayo_actor *call, struct rayo_mess
{
iks *iq = msg->payload;
switch_core_session_t *session = (switch_core_session_t *)session_data;
char *component_id = switch_mprintf("%s-input", switch_core_session_get_uuid(session));
iks *input = iks_find(iq, "input");
char *component_id = create_input_component_id(session, input);
switch_memory_pool_t *pool = NULL;
struct input_component *input_component = NULL;
iks *input = iks_find(iq, "input");
const char *error = NULL;
if (!validate_call_input(input, &error)) {
@ -484,7 +543,6 @@ static iks *start_call_input_component(struct rayo_actor *call, struct rayo_mess
switch_core_new_memory_pool(&pool);
input_component = switch_core_alloc(pool, sizeof(*input_component));
rayo_component_init(RAYO_COMPONENT(input_component), pool, RAT_CALL_COMPONENT, "input", component_id, call, iks_find_attrib(iq, "from"));
switch_safe_free(component_id);
/* start input */
return start_call_input(input_component, session, iks_find(iq, "input"), iq, NULL, 0);
@ -530,7 +588,6 @@ static iks *start_timers_call_input_component(struct rayo_actor *component, stru
switch_mutex_lock(input_component->handler->mutex);
if (input_component->speech_mode) {
switch_ivr_detect_speech_start_input_timers(session);
rayo_component_send_complete(RAYO_COMPONENT(component), COMPONENT_COMPLETE_STOP);
} else {
input_component->last_digit_time = switch_micro_time_now();
input_component->start_timers = 1;
@ -549,53 +606,63 @@ static void on_detected_speech_event(switch_event_t *event)
{
const char *speech_type = switch_event_get_header(event, "Speech-Type");
char *event_str = NULL;
const char *uuid = switch_event_get_header(event, "Unique-ID");
switch_event_serialize(event, &event_str, SWITCH_FALSE);
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "%s\n", event_str);
if (!speech_type) {
if (!speech_type || !uuid) {
return;
}
if (!strcasecmp("detected-speech", speech_type)) {
const char *uuid = switch_event_get_header(event, "Unique-ID");
char *component_id = switch_mprintf("%s-input", uuid);
char *component_id = switch_mprintf("%s-input-voice", uuid);
struct rayo_component *component = RAYO_COMPONENT_LOCATE(component_id);
switch_safe_free(component_id);
if (component) {
const char *result = switch_event_get_body(event);
switch_mutex_lock(INPUT_COMPONENT(component)->handler->mutex);
INPUT_COMPONENT(component)->handler->component = NULL;
INPUT_COMPONENT(component)->handler->voice_component = NULL;
switch_mutex_unlock(INPUT_COMPONENT(component)->handler->mutex);
if (zstr(result)) {
rayo_component_send_complete(component, INPUT_NOMATCH);
} else {
enum nlsml_match_type match_type = nlsml_parse(result, uuid);
switch (match_type) {
case NMT_NOINPUT:
if (strchr(result, '<')) {
/* got an XML result */
enum nlsml_match_type match_type = nlsml_parse(result, uuid);
switch (match_type) {
case NMT_NOINPUT:
rayo_component_send_complete(component, INPUT_NOINPUT);
break;
case NMT_MATCH: {
iks *result_xml = nlsml_normalize(result);
send_match_event(RAYO_COMPONENT(component), result_xml);
iks_delete(result_xml);
break;
}
case NMT_BAD_XML:
switch_log_printf(SWITCH_CHANNEL_UUID_LOG(uuid), SWITCH_LOG_WARNING, "Failed to parse NLSML result: %s!\n", result);
rayo_component_send_complete(component, INPUT_NOMATCH);
break;
case NMT_NOMATCH:
rayo_component_send_complete(component, INPUT_NOMATCH);
break;
default:
switch_log_printf(SWITCH_CHANNEL_UUID_LOG(uuid), SWITCH_LOG_CRIT, "Unknown NLSML match type: %i, %s!\n", match_type, result);
rayo_component_send_complete(component, INPUT_NOMATCH);
break;
}
} else if (strstr(result, "002")) {
/* Completion-Cause: 002 no-input-timeout */
rayo_component_send_complete(component, INPUT_NOINPUT);
break;
case NMT_MATCH: {
iks *result_xml = nlsml_normalize(result);
send_match_event(RAYO_COMPONENT(component), result_xml);
iks_delete(result_xml);
break;
}
case NMT_BAD_XML:
switch_log_printf(SWITCH_CHANNEL_UUID_LOG(uuid), SWITCH_LOG_WARNING, "Failed to parse NLSML result: %s!\n", result);
} else {
/* assume no match */
rayo_component_send_complete(component, INPUT_NOMATCH);
break;
case NMT_NOMATCH:
rayo_component_send_complete(component, INPUT_NOMATCH);
break;
default:
switch_log_printf(SWITCH_CHANNEL_UUID_LOG(uuid), SWITCH_LOG_CRIT, "Unknown NLSML match type: %i, %s!\n", match_type, result);
rayo_component_send_complete(component, INPUT_NOMATCH);
break;
}
}
RAYO_UNLOCK(component);
}
} else if (!strcasecmp("begin-speaking", speech_type)) {
const char *uuid = switch_event_get_header(event, "Unique-ID");
char *component_id = switch_mprintf("%s-input", uuid);
char *component_id = switch_mprintf("%s-input-voice", uuid);
struct rayo_component *component = RAYO_COMPONENT_LOCATE(component_id);
switch_safe_free(component_id);
if (component && INPUT_COMPONENT(component)->barge_event) {
@ -603,14 +670,13 @@ static void on_detected_speech_event(switch_event_t *event)
}
RAYO_UNLOCK(component);
} else if (!strcasecmp("closed", speech_type)) {
const char *uuid = switch_event_get_header(event, "Unique-ID");
char *component_id = switch_mprintf("%s-input", uuid);
char *component_id = switch_mprintf("%s-input-voice", uuid);
struct rayo_component *component = RAYO_COMPONENT_LOCATE(component_id);
switch_safe_free(component_id);
if (component) {
char *channel_state = switch_event_get_header(event, "Channel-State");
switch_mutex_lock(INPUT_COMPONENT(component)->handler->mutex);
INPUT_COMPONENT(component)->handler->component = NULL;
INPUT_COMPONENT(component)->handler->voice_component = NULL;
switch_mutex_unlock(INPUT_COMPONENT(component)->handler->mutex);
switch_log_printf(SWITCH_CHANNEL_UUID_LOG(uuid), SWITCH_LOG_DEBUG, "Recognizer closed\n");
if (channel_state && !strcmp("CS_HANGUP", channel_state)) {
@ -625,12 +691,63 @@ static void on_detected_speech_event(switch_event_t *event)
switch_safe_free(event_str);
}
/**
* Process module XML configuration
* @param pool memory pool to allocate from
* @param config_file to use
* @return SWITCH_STATUS_SUCCESS on successful configuration
*/
static switch_status_t do_config(switch_memory_pool_t *pool, const char *config_file)
{
switch_xml_t cfg, xml;
/* set defaults */
globals.default_recognizer = "pocketsphinx";
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "Configuring module\n");
if (!(xml = switch_xml_open_cfg(config_file, &cfg, NULL))) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "open of %s failed\n", config_file);
return SWITCH_STATUS_TERM;
}
/* get params */
{
switch_xml_t settings = switch_xml_child(cfg, "input");
if (settings) {
switch_xml_t param;
for (param = switch_xml_child(settings, "param"); param; param = param->next) {
const char *var = switch_xml_attr_soft(param, "name");
const char *val = switch_xml_attr_soft(param, "value");
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "param: %s = %s\n", var, val);
if (!strcasecmp(var, "default-recognizer")) {
if (!zstr(val)) {
globals.default_recognizer = switch_core_strdup(pool, val);
}
} else {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_WARNING, "Unsupported param: %s\n", var);
}
}
}
}
switch_xml_free(xml);
return SWITCH_STATUS_SUCCESS;
}
/**
* Initialize input component
* @param module_interface
* @param pool memory pool to allocate from
* @param config_file to use
* @return SWITCH_STATUS_SUCCESS if successful
*/
switch_status_t rayo_input_component_load(void)
switch_status_t rayo_input_component_load(switch_loadable_module_interface_t **module_interface, switch_memory_pool_t *pool, const char *config_file)
{
if (do_config(pool, config_file) != SWITCH_STATUS_SUCCESS) {
return SWITCH_STATUS_TERM;
}
srgs_init();
nlsml_init();

View File

@ -1092,9 +1092,12 @@ static char *fileman_supported_formats[] = { "fileman", NULL };
/**
* Initialize output component
* @param module_interface
* @param pool memory pool to allocate from
* @param config_file to use
* @return SWITCH_STATUS_SUCCESS if successful
*/
switch_status_t rayo_output_component_load(switch_loadable_module_interface_t **module_interface, switch_memory_pool_t *pool)
switch_status_t rayo_output_component_load(switch_loadable_module_interface_t **module_interface, switch_memory_pool_t *pool, const char *config_file)
{
switch_api_interface_t *api_interface;
switch_file_interface_t *file_interface;

View File

@ -620,9 +620,12 @@ static iks *forward_output_component_request(struct rayo_actor *prompt, struct r
/**
* Initialize prompt component
* @param module_interface
* @param pool memory pool to allocate from
* @param config_file to use
* @return SWITCH_STATUS_SUCCESS if successful
*/
switch_status_t rayo_prompt_component_load(void)
switch_status_t rayo_prompt_component_load(switch_loadable_module_interface_t **module_interface, switch_memory_pool_t *pool, const char *config_file)
{
/* Prompt is a convenience component that wraps <input> and <output> */
rayo_actor_command_handler_add(RAT_CALL, "", "set:"RAYO_PROMPT_NS":prompt", start_call_prompt_component);

View File

@ -479,11 +479,12 @@ static switch_status_t do_config(switch_memory_pool_t *pool, const char *config_
/**
* Initialize record component
* @param module_interface
* @param pool memory pool to allocate from
* @param config_file to use
* @return SWITCH_STATUS_SUCCESS if successful
*/
switch_status_t rayo_record_component_load(switch_memory_pool_t *pool, const char *config_file)
switch_status_t rayo_record_component_load(switch_loadable_module_interface_t **module_interface, switch_memory_pool_t *pool, const char *config_file)
{
if (do_config(pool, config_file) != SWITCH_STATUS_SUCCESS) {
return SWITCH_STATUS_TERM;

View File

@ -145,6 +145,27 @@ static void test_dialback_key(void)
ASSERT_NULL(iks_server_dialback_key("s3cr3tf0rd14lb4ck", "xmpp.example.com", "example.org", NULL));
}
static void test_validate_dtmf(void)
{
ASSERT_EQUALS(SWITCH_TRUE, iks_attrib_is_dtmf_digit("1"));
ASSERT_EQUALS(SWITCH_TRUE, iks_attrib_is_dtmf_digit("A"));
ASSERT_EQUALS(SWITCH_TRUE, iks_attrib_is_dtmf_digit("a"));
ASSERT_EQUALS(SWITCH_TRUE, iks_attrib_is_dtmf_digit("D"));
ASSERT_EQUALS(SWITCH_TRUE, iks_attrib_is_dtmf_digit("d"));
ASSERT_EQUALS(SWITCH_TRUE, iks_attrib_is_dtmf_digit("*"));
ASSERT_EQUALS(SWITCH_TRUE, iks_attrib_is_dtmf_digit("#"));
ASSERT_EQUALS(SWITCH_FALSE, iks_attrib_is_dtmf_digit("E"));
ASSERT_EQUALS(SWITCH_FALSE, iks_attrib_is_dtmf_digit(NULL));
ASSERT_EQUALS(SWITCH_FALSE, iks_attrib_is_dtmf_digit(""));
ASSERT_EQUALS(SWITCH_FALSE, iks_attrib_is_dtmf_digit("11"));
ASSERT_EQUALS(SWITCH_TRUE, validate_optional_attrib(iks_attrib_is_dtmf_digit, "A"));
ASSERT_EQUALS(SWITCH_TRUE, validate_optional_attrib(iks_attrib_is_dtmf_digit, "1"));
ASSERT_EQUALS(SWITCH_FALSE, validate_optional_attrib(iks_attrib_is_dtmf_digit, "Z"));
ASSERT_EQUALS(SWITCH_FALSE, validate_optional_attrib(iks_attrib_is_dtmf_digit, "11"));
ASSERT_EQUALS(SWITCH_TRUE, validate_optional_attrib(iks_attrib_is_dtmf_digit, NULL));
ASSERT_EQUALS(SWITCH_TRUE, validate_optional_attrib(iks_attrib_is_dtmf_digit, ""));
}
/**
* main program
*/
@ -159,5 +180,6 @@ int main(int argc, char **argv)
TEST(test_rayo_test_srgs);
TEST(test_iks_helper_value_matches);
TEST(test_dialback_key);
TEST(test_validate_dtmf);
return 0;
}