mirror of
https://github.com/jambonz/jambonz-feature-server.git
synced 2026-03-29 11:28:32 +00:00
Compare commits
13 Commits
patch/em
...
test/test-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6e72d6d73 | ||
|
|
ee68575ea4 | ||
|
|
6d0aeff6e2 | ||
|
|
d2a5d483d0 | ||
|
|
d3eb106d5d | ||
|
|
689e55bdf0 | ||
|
|
ed7e036890 | ||
|
|
f90fcdf57b | ||
|
|
c2a1819cbb | ||
|
|
4259a24fa0 | ||
|
|
e4e37d5697 | ||
|
|
b7a3c2970a | ||
|
|
cc33ac1d51 |
5
.github/workflows/build.yml
vendored
5
.github/workflows/build.yml
vendored
@@ -12,6 +12,11 @@ jobs:
|
||||
node-version: 20
|
||||
- run: npm ci
|
||||
- run: npm run jslint
|
||||
- name: Install Docker Compose
|
||||
run: |
|
||||
sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||
sudo chmod +x /usr/local/bin/docker-compose
|
||||
docker-compose --version
|
||||
- run: docker pull drachtio/sipp
|
||||
- run: npm test
|
||||
env:
|
||||
|
||||
@@ -873,7 +873,8 @@ class CallSession extends Emitter {
|
||||
writeAlerts({
|
||||
alert_type: AlertType.TTS_FAILURE,
|
||||
account_sid: this.accountSid,
|
||||
vendor
|
||||
vendor,
|
||||
target_sid: this.callSid
|
||||
}).catch((err) => this.logger.error({err}, 'Error writing tts alert'));
|
||||
}
|
||||
}
|
||||
@@ -919,6 +920,7 @@ class CallSession extends Emitter {
|
||||
speech_credential_sid: credential.speech_credential_sid,
|
||||
api_key: credential.api_key,
|
||||
deepgram_stt_uri: credential.deepgram_stt_uri,
|
||||
deepgram_tts_uri: credential.deepgram_tts_uri,
|
||||
deepgram_stt_use_tls: credential.deepgram_stt_use_tls
|
||||
};
|
||||
}
|
||||
@@ -996,7 +998,8 @@ class CallSession extends Emitter {
|
||||
writeAlerts({
|
||||
alert_type: AlertType.STT_NOT_PROVISIONED,
|
||||
account_sid: this.accountSid,
|
||||
vendor
|
||||
vendor,
|
||||
target_sid: this.callSid
|
||||
}).catch((err) => this.logger.error({err}, 'Error writing tts alert'));
|
||||
}
|
||||
}
|
||||
@@ -1806,7 +1809,8 @@ Duration=${duration} `
|
||||
await writeAlerts({
|
||||
alert_type: AlertType.WEBHOOK_CONNECTION_FAILURE,
|
||||
account_sid: this.accountSid,
|
||||
detail: `Session:reconnect error ${err}`
|
||||
detail: `Session:reconnect error ${err}`,
|
||||
url: this.application.call_hook.url,
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error({error}, 'Error writing WEBHOOK_CONNECTION_FAILURE alert');
|
||||
@@ -2005,11 +2009,6 @@ Duration=${duration} `
|
||||
if (this.direction === CallDirection.Inbound) {
|
||||
if (task.earlyMedia && !this.req.finalResponseSent) {
|
||||
this.res.send(183, {body: ep.local.sdp});
|
||||
this._notifyCallStatusChange({
|
||||
callStatus: CallStatus.EarlyMedia,
|
||||
sipStatus: 183,
|
||||
sipReason: 'Early Media'
|
||||
});
|
||||
return {ep};
|
||||
}
|
||||
this.logger.debug('propogating answer');
|
||||
@@ -2072,6 +2071,9 @@ Duration=${duration} `
|
||||
this.logger.error('CallSession:replaceEndpoint cannot be called without stable dlg');
|
||||
return;
|
||||
}
|
||||
// When this call kicked out from conference, session need to replace endpoint
|
||||
// but this.ms might be undefined/null at this case.
|
||||
this.ms = this.ms || this.getMS();
|
||||
this.ep = await this.ms.createEndpoint({remoteSdp: this.dlg.remote.sdp});
|
||||
this._configMsEndpoint();
|
||||
|
||||
|
||||
@@ -35,21 +35,11 @@ class InboundCallSession extends CallSession {
|
||||
_onCancel() {
|
||||
this.rootSpan.setAttributes({'call.termination': 'caller abandoned'});
|
||||
this.callInfo.callTerminationBy = 'caller';
|
||||
const wasEarlyMedia = this.callInfo.callStatus === 'early-media';
|
||||
this._notifyCallStatusChange({
|
||||
callStatus: CallStatus.NoAnswer,
|
||||
sipStatus: 487,
|
||||
sipReason: 'Request Terminated'
|
||||
});
|
||||
if (wasEarlyMedia) {
|
||||
const duration = 0; // Set duration to 0 for early media termination, required param
|
||||
this._notifyCallStatusChange({
|
||||
callStatus: CallStatus.Completed,
|
||||
sipStatus: 487,
|
||||
sipReason: 'Call Terminated During Early Media',
|
||||
duration: duration
|
||||
});
|
||||
}
|
||||
this._callReleased();
|
||||
}
|
||||
|
||||
|
||||
@@ -135,15 +135,10 @@ class Conference extends Task {
|
||||
* @param {SipDialog} dlg
|
||||
*/
|
||||
async _init(cs, dlg) {
|
||||
const friendlyName = this.confName;
|
||||
const {createHash, retrieveHash} = cs.srf.locals.dbHelpers;
|
||||
this.friendlyName = this.confName;
|
||||
this.confName = `conf:${cs.accountSid}:${this.confName}`;
|
||||
|
||||
this.statusParams = Object.assign({
|
||||
conferenceSid: this.confName,
|
||||
friendlyName
|
||||
}, cs.callInfo);
|
||||
|
||||
// check if conference is in progress
|
||||
const obj = await retrieveHash(this.confName);
|
||||
if (obj) {
|
||||
@@ -493,7 +488,7 @@ class Conference extends Task {
|
||||
}
|
||||
|
||||
async doConferenceParticipantAction(cs, opts) {
|
||||
const {action, tag} = opts;
|
||||
const {action, tag, wait_hook } = opts;
|
||||
|
||||
switch (action) {
|
||||
case 'tag':
|
||||
@@ -509,7 +504,10 @@ class Conference extends Task {
|
||||
await this.clearCoachMode();
|
||||
break;
|
||||
case 'hold':
|
||||
this.doConferenceHold(cs, {conf_hold_status: 'hold'});
|
||||
this.doConferenceHold(cs, {
|
||||
conf_hold_status: 'hold',
|
||||
...(wait_hook && {wait_hook})
|
||||
});
|
||||
break;
|
||||
case 'unhold':
|
||||
this.doConferenceHold(cs, {conf_hold_status: 'unhold'});
|
||||
@@ -604,7 +602,7 @@ class Conference extends Task {
|
||||
* when we hang up as the last member, the current member count = 1
|
||||
* when we are kicked out of the call when the moderator leaves, the member count = 0
|
||||
*/
|
||||
if (this.participantCount === 0) {
|
||||
if (this.participantCount === 0 || this.endConferenceOnExit) {
|
||||
const {deleteKey} = cs.srf.locals.dbHelpers;
|
||||
try {
|
||||
this._notifyConferenceEvent(cs, 'end');
|
||||
@@ -612,7 +610,8 @@ class Conference extends Task {
|
||||
this.logger.info(`conf ${this.confName} deprovisioned: ${removed ? 'success' : 'failure'}`);
|
||||
}
|
||||
catch (err) {
|
||||
this.logger.error(err, `Error deprovisioning conference ${this.confName}`);
|
||||
this.logger.error(err, `Error deprovisioning conference ${this.confName},
|
||||
might be the conference already cleaned by another moderator`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -689,8 +688,24 @@ class Conference extends Task {
|
||||
if (!params.time) params.time = (new Date()).toISOString();
|
||||
if (!params.members && typeof this.participantCount === 'number') params.members = this.participantCount;
|
||||
cs.application.requestor
|
||||
.request('verb:hook', this.statusHook, Object.assign(params, this.statusParams, httpHeaders))
|
||||
.catch((err) => this.logger.info(err, 'Conference:notifyConferenceEvent - error'));
|
||||
.request(
|
||||
'verb:hook',
|
||||
this.statusHook,
|
||||
Object.assign(
|
||||
params,
|
||||
Object.assign(
|
||||
{
|
||||
conferenceSid: this.confName,
|
||||
friendlyName: this.friendlyName,
|
||||
},
|
||||
cs.callInfo.toJSON()
|
||||
),
|
||||
httpHeaders
|
||||
)
|
||||
)
|
||||
.catch((err) =>
|
||||
this.logger.info(err, 'Conference:notifyConferenceEvent - error')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -322,6 +322,9 @@ class TaskGather extends SttTask {
|
||||
clearTimeout(this.interDigitTimer);
|
||||
let resolved = false;
|
||||
if (this.dtmfBargein) {
|
||||
if (!this.playComplete) {
|
||||
this.notifyStatus({event: 'dtmf-bargein-detected', ...evt});
|
||||
}
|
||||
this._killAudio(cs);
|
||||
this.emit('dtmf', evt);
|
||||
}
|
||||
@@ -543,7 +546,8 @@ class TaskGather extends SttTask {
|
||||
account_sid: this.cs.accountSid,
|
||||
alert_type: AlertType.STT_FAILURE,
|
||||
vendor: this.vendor,
|
||||
detail: err.message
|
||||
detail: err.message,
|
||||
target_sid: this.cs.callSid
|
||||
});
|
||||
}).catch((err) => this.logger.info({err}, 'Error generating alert for tts failure'));
|
||||
}
|
||||
@@ -699,6 +703,7 @@ class TaskGather extends SttTask {
|
||||
this.playTask.kill(cs);
|
||||
this.playTask = null;
|
||||
}
|
||||
this.playComplete = true;
|
||||
}
|
||||
|
||||
_onTranscription(cs, ep, evt, fsEvent) {
|
||||
@@ -862,6 +867,7 @@ class TaskGather extends SttTask {
|
||||
if (!this.playComplete) {
|
||||
this.logger.debug({transcript: evt.alternatives[0].transcript}, 'killing audio due to speech');
|
||||
this.emit('vad');
|
||||
this.notifyStatus({event: 'speech-bargein-detected', ...evt});
|
||||
}
|
||||
this._killAudio(cs);
|
||||
}
|
||||
@@ -876,13 +882,15 @@ class TaskGather extends SttTask {
|
||||
this.cs.callInfo, httpHeaders));
|
||||
}
|
||||
if (this.vendor === 'soniox') {
|
||||
this._clearTimer();
|
||||
if (evt.vendor.finalWords.length) {
|
||||
this.logger.debug({evt}, 'TaskGather:_onTranscription - buffering soniox transcript');
|
||||
this._sonioxTranscripts.push(evt.vendor.finalWords);
|
||||
}
|
||||
}
|
||||
|
||||
// If transcription received, reset timeout timer.
|
||||
if (this._timeoutTimer) {
|
||||
this._startTimer();
|
||||
}
|
||||
/* restart asr timer if we get a partial transcript */
|
||||
if (this.isContinuousAsr) this._startAsrTimer();
|
||||
}
|
||||
@@ -966,6 +974,7 @@ class TaskGather extends SttTask {
|
||||
alert_type: AlertType.STT_FAILURE,
|
||||
message: `Custom speech vendor ${this.vendor} error: ${evt.error}`,
|
||||
vendor: this.vendor,
|
||||
target_sid: cs.callSid
|
||||
}).catch((err) => this.logger.info({err}, 'Error generating alert for jambonz custom connection failure'));
|
||||
if (!(await this._startFallback(cs, ep, evt))) {
|
||||
this.notifyTaskDone();
|
||||
|
||||
@@ -114,7 +114,8 @@ class TaskSay extends TtsTask {
|
||||
writeAlerts({
|
||||
account_sid,
|
||||
alert_type: AlertType.TTS_NOT_PROVISIONED,
|
||||
vendor
|
||||
vendor,
|
||||
target_sid: cs.callSid
|
||||
}).catch((err) => this.logger.info({err}, 'Error generating alert for no tts'));
|
||||
throw new Error('no provisioned speech credentials for TTS');
|
||||
}
|
||||
@@ -185,7 +186,8 @@ class TaskSay extends TtsTask {
|
||||
account_sid: cs.accountSid,
|
||||
alert_type: AlertType.TTS_FAILURE,
|
||||
vendor,
|
||||
detail: err.message
|
||||
detail: err.message,
|
||||
target_sid: cs.callSid
|
||||
}).catch((err) => this.logger.info({err}, 'Error generating alert for tts failure'));
|
||||
throw err;
|
||||
}
|
||||
@@ -201,7 +203,7 @@ class TaskSay extends TtsTask {
|
||||
}
|
||||
|
||||
async exec(cs, {ep}) {
|
||||
const {srf, accountSid:account_sid} = cs;
|
||||
const {srf, accountSid:account_sid, callSid:target_sid} = cs;
|
||||
const {writeAlerts, AlertType} = srf.locals;
|
||||
const {addFileToCache} = srf.locals.dbHelpers;
|
||||
const engine = this.synthesizer.engine || cs.synthesizer?.engine || 'neural';
|
||||
@@ -312,7 +314,8 @@ class TaskSay extends TtsTask {
|
||||
account_sid,
|
||||
alert_type: AlertType.TTS_FAILURE,
|
||||
vendor,
|
||||
detail: evt.variable_tts_error
|
||||
detail: evt.variable_tts_error,
|
||||
target_sid
|
||||
}).catch((err) => this.logger.info({err}, 'Error generating alert for no tts'));
|
||||
}
|
||||
if (evt.variable_tts_cache_filename && !this.killed) {
|
||||
|
||||
@@ -178,7 +178,8 @@ class SttTask extends Task {
|
||||
writeAlerts({
|
||||
account_sid: cs.accountSid,
|
||||
alert_type: AlertType.STT_NOT_PROVISIONED,
|
||||
vendor
|
||||
vendor,
|
||||
target_sid: cs.callSid
|
||||
}).catch((err) => this.logger.info({err}, 'Error generating alert for no stt'));
|
||||
this.notifyTaskDone();
|
||||
throw new Error(`No speech-to-text service credentials for ${vendor} have been configured`);
|
||||
@@ -309,6 +310,7 @@ class SttTask extends Task {
|
||||
message: 'STT failure reported by vendor',
|
||||
detail: evt.error,
|
||||
vendor: this.vendor,
|
||||
target_sid: cs.callSid
|
||||
}).catch((err) => this.logger.info({err}, `Error generating alert for ${this.vendor} connection failure`));
|
||||
}
|
||||
|
||||
@@ -321,6 +323,7 @@ class SttTask extends Task {
|
||||
alert_type: AlertType.STT_FAILURE,
|
||||
message: `Failed connecting to ${this.vendor} speech recognizer: ${reason}`,
|
||||
vendor: this.vendor,
|
||||
target_sid: cs.callSid
|
||||
}).catch((err) => this.logger.info({err}, `Error generating alert for ${this.vendor} connection failure`));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -605,6 +605,7 @@ class TaskTranscribe extends SttTask {
|
||||
alert_type: AlertType.STT_FAILURE,
|
||||
message: `Custom speech vendor ${this.vendor} error: ${evt.error}`,
|
||||
vendor: this.vendor,
|
||||
target_sid: cs.callSid
|
||||
}).catch((err) => this.logger.info({err}, 'Error generating alert for jambonz custom connection failure'));
|
||||
if (!(await this._startFallback(cs, _ep, evt))) {
|
||||
this.notifyTaskDone();
|
||||
|
||||
@@ -152,7 +152,8 @@ class TtsTask extends Task {
|
||||
account_sid: cs.accountSid,
|
||||
alert_type: AlertType.TTS_FAILURE,
|
||||
vendor,
|
||||
detail: err.message
|
||||
detail: err.message,
|
||||
target_sid: cs.callSid
|
||||
}).catch((err) => this.logger.info({err}, 'Error generating alert for tts failure'));
|
||||
this.notifyError({msg: 'TTS error', details: err.message || err});
|
||||
throw err;
|
||||
|
||||
@@ -210,7 +210,8 @@ module.exports = (logger) => {
|
||||
account_sid: cs.accountSid,
|
||||
alert_type: AlertType.STT_FAILURE,
|
||||
vendor: vendor,
|
||||
detail: err.message
|
||||
detail: err.message,
|
||||
target_sid: cs.callSid
|
||||
});
|
||||
}).catch((err) => logger.info({err}, 'Error generating alert for tts failure'));
|
||||
|
||||
|
||||
@@ -77,6 +77,7 @@ const speechMapper = (cred) => {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.api_key = o.api_key;
|
||||
obj.deepgram_stt_uri = o.deepgram_stt_uri;
|
||||
obj.deepgram_tts_uri = o.deepgram_tts_uri;
|
||||
obj.deepgram_stt_use_tls = o.deepgram_stt_use_tls;
|
||||
}
|
||||
else if ('soniox' === obj.vendor) {
|
||||
|
||||
@@ -441,7 +441,7 @@ class SingleDialer extends Emitter {
|
||||
});
|
||||
app.requestor.request('session:adulting', '/adulting', {
|
||||
...cs.callInfo.toJSON(),
|
||||
parentCallInfo: this.parentCallInfo
|
||||
parentCallInfo: this.parentCallInfo.toJSON()
|
||||
}).catch((err) => {
|
||||
newLogger.error({err}, 'doAdulting: error sending adulting request');
|
||||
});
|
||||
|
||||
@@ -45,7 +45,8 @@ const stickyVars = {
|
||||
'DEEPGRAM_SPEECH_ENDPOINTING',
|
||||
'DEEPGRAM_SPEECH_UTTERANCE_END_MS',
|
||||
'DEEPGRAM_SPEECH_VAD_TURNOFF',
|
||||
'DEEPGRAM_SPEECH_TAG'
|
||||
'DEEPGRAM_SPEECH_TAG',
|
||||
'DEEPGRAM_SPEECH_MODEL_VERSION'
|
||||
],
|
||||
aws: [
|
||||
'AWS_VOCABULARY_NAME',
|
||||
@@ -316,8 +317,10 @@ const normalizeIbm = (evt, channel, language) => {
|
||||
|
||||
const normalizeGoogle = (evt, channel, language) => {
|
||||
const copy = JSON.parse(JSON.stringify(evt));
|
||||
const language_code = evt.language_code || language;
|
||||
|
||||
return {
|
||||
language_code: language,
|
||||
language_code: language_code,
|
||||
channel_tag: channel,
|
||||
is_final: evt.is_final,
|
||||
alternatives: [evt.alternatives[0]],
|
||||
@@ -707,7 +710,9 @@ module.exports = (logger) => {
|
||||
...(deepgramOptions.vadTurnoff) &&
|
||||
{DEEPGRAM_SPEECH_VAD_TURNOFF: deepgramOptions.vadTurnoff},
|
||||
...(deepgramOptions.tag) &&
|
||||
{DEEPGRAM_SPEECH_TAG: deepgramOptions.tag}
|
||||
{DEEPGRAM_SPEECH_TAG: deepgramOptions.tag},
|
||||
...(deepgramOptions.version) &&
|
||||
{DEEPGRAM_SPEECH_MODEL_VERSION: deepgramOptions.version}
|
||||
};
|
||||
}
|
||||
else if ('soniox' === vendor) {
|
||||
@@ -835,6 +840,7 @@ module.exports = (logger) => {
|
||||
};
|
||||
} else if (vendor.startsWith('custom:')) {
|
||||
let {options = {}} = rOpts;
|
||||
const {sampleRate} = rOpts.customOptions || {};
|
||||
const {auth_token, custom_stt_url} = sttCredentials;
|
||||
options = {
|
||||
...options,
|
||||
@@ -842,14 +848,15 @@ module.exports = (logger) => {
|
||||
{hints: rOpts.hints}),
|
||||
...(rOpts.hints?.length > 0 && typeof rOpts.hints[0] === 'object' &&
|
||||
{hints: JSON.stringify(rOpts.hints)}),
|
||||
...(typeof rOpts.hintsBoost === 'number' && {hintsBoost: rOpts.hintsBoost})
|
||||
...(typeof rOpts.hintsBoost === 'number' && {hintsBoost: rOpts.hintsBoost}),
|
||||
...(task.cs?.callSid && {callSid: task.cs.callSid})
|
||||
};
|
||||
|
||||
opts = {
|
||||
...opts,
|
||||
...(auth_token && {JAMBONZ_STT_API_KEY: auth_token}),
|
||||
JAMBONZ_STT_URL: custom_stt_url,
|
||||
...(Object.keys(options).length > 0 && {JAMBONZ_STT_OPTIONS: JSON.stringify(options)}),
|
||||
...(sampleRate && {JAMBONZ_STT_SAMPLING: sampleRate})
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
4604
package-lock.json
generated
4604
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -31,10 +31,10 @@
|
||||
"@jambonz/http-health-check": "^0.0.1",
|
||||
"@jambonz/mw-registrar": "^0.2.7",
|
||||
"@jambonz/realtimedb-helpers": "^0.8.8",
|
||||
"@jambonz/speech-utils": "^0.1.11",
|
||||
"@jambonz/speech-utils": "^0.1.13",
|
||||
"@jambonz/stats-collector": "^0.1.10",
|
||||
"@jambonz/time-series": "^0.2.8",
|
||||
"@jambonz/verb-specifications": "^0.0.74",
|
||||
"@jambonz/time-series": "^0.2.9",
|
||||
"@jambonz/verb-specifications": "^0.0.76",
|
||||
"@opentelemetry/api": "^1.8.0",
|
||||
"@opentelemetry/exporter-jaeger": "^1.23.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.50.0",
|
||||
|
||||
Reference in New Issue
Block a user