mirror of
https://github.com/jambonz/jambonz-feature-server.git
synced 2026-02-12 09:19:34 +00:00
Compare commits
3 Commits
v0.8.0-rc7
...
v0.8.0-rc9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ef80dafd2 | ||
|
|
c37fba541f | ||
|
|
f9921cf4e9 |
@@ -270,6 +270,10 @@ class CallSession extends Emitter {
|
||||
return this.backgroundGatherTask;
|
||||
}
|
||||
|
||||
get isListenEnabled() {
|
||||
return this.backgroundListenTask;
|
||||
}
|
||||
|
||||
get b3() {
|
||||
return this.rootSpan?.getTracingPropagation();
|
||||
}
|
||||
@@ -461,6 +465,50 @@ class CallSession extends Emitter {
|
||||
}
|
||||
}
|
||||
|
||||
async startBackgroundListen(opts) {
|
||||
if (this.isListenEnabled) {
|
||||
this.logger.info('CallSession:startBackgroundListen - listen is already enabled, ignoring request');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.logger.debug({opts}, 'CallSession:startBackgroundListen');
|
||||
const t = normalizeJambones(this.logger, [opts]);
|
||||
this.backgroundListenTask = makeTask(this.logger, t[0]);
|
||||
const resources = await this._evaluatePreconditions(this.backgroundListenTask);
|
||||
const {span, ctx} = this.rootSpan.startChildSpan(`background-gather:${this.backgroundListenTask.summary}`);
|
||||
this.backgroundListenTask.span = span;
|
||||
this.backgroundListenTask.ctx = ctx;
|
||||
this.backgroundListenTask.exec(this, resources)
|
||||
.then(() => {
|
||||
this.logger.info('CallSession:startBackgroundListen: listen completed');
|
||||
this.backgroundListenTask && this.backgroundListenTask.removeAllListeners();
|
||||
this.backgroundListenTask && this.backgroundListenTask.span.end();
|
||||
this.backgroundListenTask = null;
|
||||
return;
|
||||
})
|
||||
.catch((err) => {
|
||||
this.logger.info({err}, 'CallSession:startBackgroundListen: listen threw error');
|
||||
this.backgroundListenTask && this.backgroundListenTask.removeAllListeners();
|
||||
this.backgroundListenTask && this.backgroundListenTask.span.end();
|
||||
this.backgroundListenTask = null;
|
||||
});
|
||||
} catch (err) {
|
||||
this.logger.info({err, opts}, 'CallSession:startBackgroundListen - Error creating listen task');
|
||||
}
|
||||
}
|
||||
|
||||
async stopBackgroundListen() {
|
||||
try {
|
||||
if (this.backgroundListenTask) {
|
||||
this.backgroundListenTask.removeAllListeners();
|
||||
this.backgroundListenTask.kill().catch(() => {});
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.info({err}, 'CallSession:stopBackgroundListen - Error stopping listen task');
|
||||
}
|
||||
this.backgroundListenTask = null;
|
||||
}
|
||||
|
||||
async enableBotMode(gather, autoEnable) {
|
||||
try {
|
||||
if (this.backgroundGatherTask) {
|
||||
|
||||
@@ -8,7 +8,8 @@ class TaskConfig extends Task {
|
||||
'synthesizer',
|
||||
'recognizer',
|
||||
'bargeIn',
|
||||
'record'
|
||||
'record',
|
||||
'listen'
|
||||
].forEach((k) => this[k] = this.data[k] || {});
|
||||
|
||||
if ('notifyEvents' in this.data) {
|
||||
@@ -30,7 +31,7 @@ class TaskConfig extends Task {
|
||||
});
|
||||
}
|
||||
if (this.bargeIn.sticky) this.autoEnable = true;
|
||||
this.preconditions = (this.bargeIn.enable || this.record?.action || this.data.amd) ?
|
||||
this.preconditions = (this.bargeIn.enable || this.record?.action || this.listen?.url || this.data.amd) ?
|
||||
TaskPreconditions.Endpoint :
|
||||
TaskPreconditions.None;
|
||||
}
|
||||
@@ -38,8 +39,9 @@ class TaskConfig extends Task {
|
||||
get name() { return TaskName.Config; }
|
||||
|
||||
get hasSynthesizer() { return Object.keys(this.synthesizer).length; }
|
||||
|
||||
get hasRecognizer() { return Object.keys(this.recognizer).length; }
|
||||
get hasRecording() { return Object.keys(this.record).length; }
|
||||
get hasListen() { return Object.keys(this.listen).length; }
|
||||
|
||||
get summary() {
|
||||
const phrase = [];
|
||||
@@ -54,6 +56,10 @@ class TaskConfig extends Task {
|
||||
const s = `{${v},${l}}`;
|
||||
phrase.push(`set recognizer${s}`);
|
||||
}
|
||||
if (this.hasRecording) phrase.push(this.record.action);
|
||||
if (this.hasListen) {
|
||||
phrase.push(this.listen.enable ? `listen ${this.listen.url}` : 'stop listen');
|
||||
}
|
||||
if (this.data.amd) phrase.push('enable amd');
|
||||
if (this.notifyEvents) phrase.push(`event notification ${this.notifyEvents ? 'on' : 'off'}`);
|
||||
return `${this.name}{${phrase.join(',')}`;
|
||||
@@ -64,8 +70,7 @@ class TaskConfig extends Task {
|
||||
|
||||
if (this.notifyEvents) {
|
||||
this.logger.debug(`turning event notification ${this.notifyEvents ? 'on' : 'off'}`);
|
||||
cs.notifyEvents = !!this.data.notifEvents;
|
||||
|
||||
cs.notifyEvents = !!this.data.notifyEvents;
|
||||
}
|
||||
|
||||
if (this.data.amd) {
|
||||
@@ -147,11 +152,21 @@ class TaskConfig extends Task {
|
||||
this.logger.info({err}, 'Config: error starting recording');
|
||||
}
|
||||
}
|
||||
if (this.hasListen) {
|
||||
const {enable, ...opts} = this.listen;
|
||||
if (enable) {
|
||||
this.logger.debug({opts}, 'Config: enabling listen');
|
||||
cs.startBackgroundListen({verb: 'listen', ...opts});
|
||||
} else {
|
||||
this.logger.info('Config: disabling listen');
|
||||
cs.stopBackgroundListen();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async kill(cs) {
|
||||
super.kill(cs);
|
||||
if (this.ep && this.stopAmd) this.stopAmd(this.ep, this);
|
||||
//if (this.ep && this.stopAmd) this.stopAmd(this.ep, this);
|
||||
}
|
||||
|
||||
_onAmdEvent(cs, evt) {
|
||||
|
||||
@@ -10,7 +10,7 @@ class TaskListen extends Task {
|
||||
|
||||
[
|
||||
'action', 'auth', 'method', 'url', 'finishOnKey', 'maxLength', 'metadata', 'mixType', 'passDtmf', 'playBeep',
|
||||
'sampleRate', 'timeout', 'transcribe', 'wsAuth'
|
||||
'sampleRate', 'timeout', 'transcribe', 'wsAuth', 'disableBidirectionalAudio'
|
||||
].forEach((k) => this[k] = this.data[k]);
|
||||
|
||||
this.mixType = this.mixType || 'mono';
|
||||
@@ -136,7 +136,9 @@ class TaskListen extends Task {
|
||||
}
|
||||
|
||||
/* support bi-directional audio */
|
||||
ep.addCustomEventListener(ListenEvents.PlayAudio, this._onPlayAudio.bind(this, ep));
|
||||
if (!this.disableBiDirectionalAudio) {
|
||||
ep.addCustomEventListener(ListenEvents.PlayAudio, this._onPlayAudio.bind(this, ep));
|
||||
}
|
||||
ep.addCustomEventListener(ListenEvents.KillAudio, this._onKillAudio.bind(this, ep));
|
||||
ep.addCustomEventListener(ListenEvents.Disconnect, this._onDisconnect.bind(this, ep));
|
||||
}
|
||||
|
||||
@@ -42,11 +42,24 @@
|
||||
"recognizer": "#recognizer",
|
||||
"bargeIn": "#bargeIn",
|
||||
"record": "#recordOptions",
|
||||
"listen": "#listenOptions",
|
||||
"amd": "#amd",
|
||||
"notifyEvents": "boolean"
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"listenOptions": {
|
||||
"properties": {
|
||||
"enable": "boolean",
|
||||
"url": "string",
|
||||
"sampleRate": "number",
|
||||
"wsAuth": "#auth",
|
||||
"metadata": "object"
|
||||
},
|
||||
"required": [
|
||||
"enable"
|
||||
]
|
||||
},
|
||||
"bargeIn": {
|
||||
"properties": {
|
||||
"enable": "boolean",
|
||||
@@ -275,6 +288,7 @@
|
||||
},
|
||||
"passDtmf": "boolean",
|
||||
"playBeep": "boolean",
|
||||
"disableBidirectionalAudio": "boolean",
|
||||
"sampleRate": "number",
|
||||
"timeout": "number",
|
||||
"transcribe": "#transcribe",
|
||||
|
||||
@@ -7,6 +7,86 @@ const {
|
||||
DeepgramTranscriptionEvents,
|
||||
} = require('./constants');
|
||||
|
||||
const stickyVars = {
|
||||
google: [
|
||||
'GOOGLE_SPEECH_HINTS',
|
||||
'GOOGLE_SPEECH_SEPARATE_RECOGNITION_PER_CHANNEL',
|
||||
'GOOGLE_SPEECH_PROFANITY_FILTER',
|
||||
'GOOGLE_SPEECH_ENABLE_AUTOMATIC_PUNCTUATION',
|
||||
'GOOGLE_SPEECH_ENABLE_WORD_TIME_OFFSETS',
|
||||
'GOOGLE_SPEECH_SINGLE_UTTERANCE',
|
||||
'GOOGLE_SPEECH_SPEAKER_DIARIZATION',
|
||||
'GOOGLE_SPEECH_USE_ENHANCED',
|
||||
'GOOGLE_SPEECH_ALTERNATIVE_LANGUAGE_CODES',
|
||||
'GOOGLE_SPEECH_METADATA_INTERACTION_TYPE',
|
||||
'GOOGLE_SPEECH_METADATA_INDUSTRY_NAICS_CODE'
|
||||
],
|
||||
microsoft: [
|
||||
'AZURE_SPEECH_HINTS',
|
||||
'AZURE_SERVICE_ENDPOINT_ID',
|
||||
'AZURE_REQUEST_SNR',
|
||||
'AZURE_PROFANITY_OPTION',
|
||||
'AZURE_SERVICE_ENDPOINT',
|
||||
'AZURE_INITIAL_SPEECH_TIMEOUT_MS',
|
||||
'AZURE_USE_OUTPUT_FORMAT_DETAILED',
|
||||
],
|
||||
deepgram: [
|
||||
'DEEPGRAM_SPEECH_KEYWORDS',
|
||||
'DEEPGRAM_API_KEY',
|
||||
'DEEPGRAM_SPEECH_TIER',
|
||||
'DEEPGRAM_SPEECH_MODEL',
|
||||
'DEEPGRAM_SPEECH_ENABLE_AUTOMATIC_PUNCTUATION',
|
||||
'DEEPGRAM_SPEECH_PROFANITY_FILTER',
|
||||
'DEEPGRAM_SPEECH_REDACT',
|
||||
'DEEPGRAM_SPEECH_DIARIZE',
|
||||
'DEEPGRAM_SPEECH_NER',
|
||||
'DEEPGRAM_SPEECH_ALTERNATIVES',
|
||||
'DEEPGRAM_SPEECH_NUMERALS',
|
||||
'DEEPGRAM_SPEECH_SEARCH',
|
||||
'DEEPGRAM_SPEECH_REPLACE',
|
||||
'DEEPGRAM_SPEECH_ENDPOINTING',
|
||||
'DEEPGRAM_SPEECH_VAD_TURNOFF',
|
||||
'DEEPGRAM_SPEECH_TAG'
|
||||
],
|
||||
aws: [
|
||||
'AWS_VOCABULARY_NAME',
|
||||
'AWS_VOCABULARY_FILTER_METHOD',
|
||||
'AWS_VOCABULARY_FILTER_NAME'
|
||||
],
|
||||
nuance: [
|
||||
'NUANCE_ACCESS_TOKEN',
|
||||
'NUANCE_KRYPTON_ENDPOINT',
|
||||
'NUANCE_TOPIC',
|
||||
'NUANCE_UTTERANCE_DETECTION_MODE',
|
||||
'NUANCE_FILTER_PROFANITY',
|
||||
'NUANCE_INCLUDE_TOKENIZATION',
|
||||
'NUANCE_DISCARD_SPEAKER_ADAPTATION',
|
||||
'NUANCE_SUPPRESS_CALL_RECORDING',
|
||||
'NUANCE_MASK_LOAD_FAILURES',
|
||||
'NUANCE_SUPPRESS_INITIAL_CAPITALIZATION',
|
||||
'NUANCE_ALLOW_ZERO_BASE_LM_WEIGHT',
|
||||
'NUANCE_FILTER_WAKEUP_WORD',
|
||||
'NUANCE_NO_INPUT_TIMEOUT_MS',
|
||||
'NUANCE_RECOGNITION_TIMEOUT_MS',
|
||||
'NUANCE_UTTERANCE_END_SILENCE_MS',
|
||||
'NUANCE_MAX_HYPOTHESES',
|
||||
'NUANCE_SPEECH_DOMAIN',
|
||||
'NUANCE_FORMATTING',
|
||||
'NUANCE_RESOURCES'
|
||||
],
|
||||
ibm: [
|
||||
'IBM_ACCESS_TOKEN',
|
||||
'IBM_SPEECH_REGION',
|
||||
'IBM_SPEECH_INSTANCE_ID',
|
||||
'IBM_SPEECH_MODEL',
|
||||
'IBM_SPEECH_LANGUAGE_CUSTOMIZATION_ID',
|
||||
'IBM_SPEECH_ACOUSTIC_CUSTOMIZATION_ID',
|
||||
'IBM_SPEECH_BASE_MODEL_VERSION',
|
||||
'IBM_SPEECH_WATSON_METADATA',
|
||||
'IBM_SPEECH_WATSON_LEARNING_OPT_OUT'
|
||||
]
|
||||
};
|
||||
|
||||
const normalizeDeepgram = (evt, channel, language) => {
|
||||
const copy = JSON.parse(JSON.stringify(evt));
|
||||
const alternatives = (evt.channel?.alternatives || [])
|
||||
@@ -19,7 +99,7 @@ const normalizeDeepgram = (evt, channel, language) => {
|
||||
language_code: language,
|
||||
channel_tag: channel,
|
||||
is_final: evt.is_final,
|
||||
alternatives,
|
||||
alternatives: [alternatives[0]],
|
||||
vendor: {
|
||||
name: 'deepgram',
|
||||
evt: copy
|
||||
@@ -50,7 +130,7 @@ const normalizeGoogle = (evt, channel, language) => {
|
||||
language_code: language,
|
||||
channel_tag: channel,
|
||||
is_final: evt.is_final,
|
||||
alternatives: evt.alternatives,
|
||||
alternatives: [evt.alternatives[0]],
|
||||
vendor: {
|
||||
name: 'google',
|
||||
evt: copy
|
||||
@@ -64,7 +144,7 @@ const normalizeNuance = (evt, channel, language) => {
|
||||
language_code: language,
|
||||
channel_tag: channel,
|
||||
is_final: evt.is_final,
|
||||
alternatives: evt.alternatives,
|
||||
alternatives: [evt.alternatives[0]],
|
||||
vendor: {
|
||||
name: 'nuance',
|
||||
evt: copy
|
||||
@@ -92,7 +172,7 @@ const normalizeMicrosoft = (evt, channel, language) => {
|
||||
language_code,
|
||||
channel_tag: channel,
|
||||
is_final: evt.RecognitionStatus === 'Success',
|
||||
alternatives,
|
||||
alternatives: [alternatives[0]],
|
||||
vendor: {
|
||||
name: 'microsoft',
|
||||
evt: copy
|
||||
@@ -198,7 +278,7 @@ module.exports = (logger) => {
|
||||
{GOOGLE_SPEECH_ALTERNATIVE_LANGUAGE_CODES: rOpts.altLanguages.join(',')}),
|
||||
...(rOpts.interactionType &&
|
||||
{GOOGLE_SPEECH_METADATA_INTERACTION_TYPE: rOpts.interactionType}),
|
||||
...{GOOGLE_SPEECH_MODEL: rOpts.model || (task.name === TaskName.Gather ? 'command_and_search' : 'phone_call')},
|
||||
...{GOOGLE_SPEECH_MODEL: rOpts.model || (task.name === TaskName.Gather ? 'latest_short' : 'phone_call')},
|
||||
...(rOpts.naicsCode > 0 &&
|
||||
{GOOGLE_SPEECH_METADATA_INDUSTRY_NAICS_CODE: rOpts.naicsCode}),
|
||||
};
|
||||
@@ -333,7 +413,7 @@ module.exports = (logger) => {
|
||||
...(deepgramOptions.vadTurnoff) &&
|
||||
{DEEPGRAM_SPEECH_VAD_TURNOFF: deepgramOptions.vadTurnoff},
|
||||
...(deepgramOptions.tag) &&
|
||||
{DEEPGRAM_SPEECH_VAD_TURNOFF: deepgramOptions.tag}
|
||||
{DEEPGRAM_SPEECH_TAG: deepgramOptions.tag}
|
||||
};
|
||||
}
|
||||
else if ('ibm' === rOpts.vendor) {
|
||||
@@ -360,7 +440,9 @@ module.exports = (logger) => {
|
||||
{IBM_SPEECH_WATSON_LEARNING_OPT_OUT: ibmOptions.watsonLearningOptOut}
|
||||
};
|
||||
}
|
||||
|
||||
stickyVars[rOpts.vendor].forEach((key) => {
|
||||
if (!opts[key]) opts[key] = '';
|
||||
});
|
||||
logger.debug({opts}, 'recognizer channel vars');
|
||||
return opts;
|
||||
};
|
||||
|
||||
102
test/config-test.js
Normal file
102
test/config-test.js
Normal file
@@ -0,0 +1,102 @@
|
||||
const test = require('tape');
|
||||
const { sippUac } = require('./sipp')('test_fs');
|
||||
const bent = require('bent');
|
||||
const getJSON = bent('json')
|
||||
const clearModule = require('clear-module');
|
||||
const {provisionCallHook} = require('./utils')
|
||||
|
||||
process.on('unhandledRejection', (reason, p) => {
|
||||
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
|
||||
});
|
||||
|
||||
function connect(connectable) {
|
||||
return new Promise((resolve, reject) => {
|
||||
connectable.on('connect', () => {
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
test('\'config: listen\'', async(t) => {
|
||||
clearModule.all();
|
||||
const {srf, disconnect} = require('../app');
|
||||
try {
|
||||
await connect(srf);
|
||||
|
||||
// GIVEN
|
||||
const from = "config_listen_success";
|
||||
let verbs = [
|
||||
{
|
||||
"verb": "config",
|
||||
"listen": {
|
||||
"enable": true,
|
||||
"url": `ws://172.38.0.60:3000/${from}`
|
||||
}
|
||||
},
|
||||
{
|
||||
"verb": "pause",
|
||||
"length": 5
|
||||
}
|
||||
];
|
||||
|
||||
provisionCallHook(from, verbs);
|
||||
|
||||
// THEN
|
||||
await sippUac('uac-gather-account-creds-success-send-bye.xml', '172.38.0.10', from);
|
||||
let obj = await getJSON(`http://127.0.0.1:3100/ws_packet_count/${from}`);
|
||||
t.pass('config: successfully started background listen');
|
||||
|
||||
disconnect();
|
||||
} catch (err) {
|
||||
console.log(`error received: ${err}`);
|
||||
disconnect();
|
||||
t.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
test('\'config: listen - stop\'', async(t) => {
|
||||
clearModule.all();
|
||||
const {srf, disconnect} = require('../app');
|
||||
try {
|
||||
await connect(srf);
|
||||
|
||||
// GIVEN
|
||||
const from = "config_listen_success";
|
||||
let verbs = [
|
||||
{
|
||||
"verb": "config",
|
||||
"listen": {
|
||||
"enable": true,
|
||||
"url": `ws://172.38.0.60:3000/${from}`
|
||||
}
|
||||
},
|
||||
{
|
||||
"verb": "pause",
|
||||
"length": 1
|
||||
},
|
||||
{
|
||||
"verb": "config",
|
||||
"listen": {
|
||||
"enable": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"verb": "pause",
|
||||
"length": 3
|
||||
}
|
||||
];
|
||||
|
||||
provisionCallHook(from, verbs);
|
||||
|
||||
// THEN
|
||||
await sippUac('uac-gather-account-creds-success-send-bye.xml', '172.38.0.10', from);
|
||||
let obj = await getJSON(`http://127.0.0.1:3100/ws_packet_count/${from}`);
|
||||
t.pass('config: successfully started then stopped background listen');
|
||||
|
||||
disconnect();
|
||||
} catch (err) {
|
||||
console.log(`error received: ${err}`);
|
||||
disconnect();
|
||||
t.error(err);
|
||||
}
|
||||
});
|
||||
@@ -44,7 +44,7 @@ services:
|
||||
drachtio:
|
||||
image: drachtio/drachtio-server:latest
|
||||
restart: always
|
||||
command: drachtio --contact "sip:*;transport=udp,tcp" --address 0.0.0.0 --port 9022
|
||||
command: drachtio --contact "sip:*;transport=udp" --mtu 4096 --address 0.0.0.0 --port 9022
|
||||
ports:
|
||||
- "9060:9022/tcp"
|
||||
networks:
|
||||
|
||||
@@ -12,5 +12,6 @@ require('./create-call-test');
|
||||
require('./play-tests');
|
||||
require('./sip-refer-tests');
|
||||
require('./listen-tests');
|
||||
require('./config-test');
|
||||
require('./remove-test-db');
|
||||
require('./docker_stop');
|
||||
@@ -40,7 +40,7 @@ test('\'listen-success\'', async(t) => {
|
||||
// THEN
|
||||
await sippUac('uac-gather-account-creds-success-send-bye.xml', '172.38.0.10', from);
|
||||
let obj = await getJSON(`http://127.0.0.1:3100/ws_packet_count/${from}`);
|
||||
t.ok(38000 <= obj.count, 'listen: success incomming call audio');
|
||||
t.ok(38000 <= obj.count, 'listen: success incoming call audio');
|
||||
|
||||
obj = await getJSON(`http://127.0.0.1:3100/ws_metadata/${from}`);
|
||||
t.ok(obj.metadata.from === from && obj.metadata.sampleRate === 8000, 'listen: success metadata');
|
||||
|
||||
Reference in New Issue
Block a user