mirror of
https://github.com/jambonz/jambonz-feature-server.git
synced 2026-02-10 08:21:33 +00:00
Compare commits
12 Commits
v0.9.3-rc5
...
v0.9.3-rc7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
edb7e21ff9 | ||
|
|
cafd9530a2 | ||
|
|
ca8cace284 | ||
|
|
499c800213 | ||
|
|
97952afb1d | ||
|
|
f4e68d0ea1 | ||
|
|
6bad1a22f3 | ||
|
|
fcefa1ff31 | ||
|
|
67cd53c930 | ||
|
|
a59784b8ab | ||
|
|
a2581eaeb4 | ||
|
|
3706aa4d98 |
@@ -1,4 +1,4 @@
|
||||
FROM --platform=linux/amd64 node:18.15-alpine3.16 as base
|
||||
FROM --platform=linux/amd64 node:20-alpine as base
|
||||
|
||||
RUN apk --update --no-cache add --virtual .builds-deps build-base python3
|
||||
|
||||
|
||||
@@ -866,6 +866,8 @@ class CallSession extends Emitter {
|
||||
}
|
||||
}
|
||||
clearTtsStream() {
|
||||
this.requestor?.request('tts:streaming-event', '/streaming-event', {event_type: 'user_interruption'})
|
||||
.catch((err) => this.logger.info({err}, 'CallSession:clearTtsStream - Error sending user_interruption'));
|
||||
this.ttsStreamingBuffer?.clear();
|
||||
}
|
||||
|
||||
@@ -1107,7 +1109,8 @@ class CallSession extends Emitter {
|
||||
speech_credential_sid: credential.speech_credential_sid,
|
||||
auth_token: credential.auth_token,
|
||||
custom_stt_url: credential.custom_stt_url,
|
||||
custom_tts_url: credential.custom_tts_url
|
||||
custom_tts_url: credential.custom_tts_url,
|
||||
custom_tts_streaming_url: credential.custom_tts_streaming_url
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1285,6 +1288,11 @@ class CallSession extends Emitter {
|
||||
this.wakeupResolver({reason: 'session ended'});
|
||||
this.wakeupResolver = null;
|
||||
}
|
||||
|
||||
if (this._maxCallDurationTimer) {
|
||||
clearTimeout(this._maxCallDurationTimer);
|
||||
this._maxCallDurationTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2952,6 +2960,27 @@ Duration=${duration} `
|
||||
}
|
||||
this.logger.info({vendor}, 'CallSession:_onTtsStreamingConnectFailure - tts streaming connect failure');
|
||||
}
|
||||
|
||||
async startMaxCallDurationTimer(timeLimit) {
|
||||
if (!this._maxCallDurationTimer && timeLimit > 0) {
|
||||
this.timeLimit = timeLimit;
|
||||
this._maxCallDurationTimer = setTimeout(this._onMaxCallDuration.bind(this), timeLimit * 1000);
|
||||
this.logger.debug(`CallSession:startMaxCallDurationTimer - started max call duration timer for ${timeLimit}s`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* _onMaxCallDuration - called when the call has reached the maximum duration
|
||||
*/
|
||||
_onMaxCallDuration() {
|
||||
this.logger.info(`callSession:_onMaxCallDuration tearing down call as it has reached ${this.timeLimit}s`);
|
||||
if (!this.dlg) {
|
||||
this.logger.debug('CallSession:_onMaxCallDuration - no dialog, call already gone');
|
||||
return;
|
||||
}
|
||||
this._jambonzHangup('Max Call Duration');
|
||||
this._maxCallDurationTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CallSession;
|
||||
|
||||
@@ -521,7 +521,7 @@ class TaskDial extends Task {
|
||||
const {req, callInfo, direction, srf} = cs;
|
||||
const {getSBC} = srf.locals;
|
||||
const {lookupTeamsByAccount, lookupAccountBySid} = srf.locals.dbHelpers;
|
||||
const {lookupCarrier, lookupCarrierByPhoneNumber} = dbUtils(this.logger, cs.srf);
|
||||
const {lookupCarrier, lookupCarrierByPhoneNumber, lookupVoipCarrierBySid} = dbUtils(this.logger, cs.srf);
|
||||
let sbcAddress = this.proxy || getSBC();
|
||||
const teamsInfo = {};
|
||||
let fqdn;
|
||||
@@ -540,6 +540,8 @@ class TaskDial extends Task {
|
||||
...this.headers
|
||||
};
|
||||
|
||||
// default to inband dtmf if not specified
|
||||
this.inbandDtmfEnabled = cs.inbandDtmfEnabled;
|
||||
// get calling user from From header
|
||||
const parsedFrom = req.getParsedHeader('from');
|
||||
const fromUri = parseUri(parsedFrom.uri);
|
||||
@@ -617,10 +619,17 @@ class TaskDial extends Task {
|
||||
const str = this.callerId || req.callingNumber || '';
|
||||
const callingNumber = str.startsWith('+') ? str.substring(1) : str;
|
||||
const voip_carrier_sid = await lookupCarrierByPhoneNumber(cs.accountSid, callingNumber);
|
||||
const req_voip_carrier_sid = req.has('X-Voip-Carrier-Sid') ? req.get('X-Voip-Carrier-Sid') : null;
|
||||
if (voip_carrier_sid) {
|
||||
this.logger.info(
|
||||
`Dial:_attemptCalls: selected voip_carrier_sid ${voip_carrier_sid} for callingNumber: ${callingNumber}`);
|
||||
opts.headers['X-Requested-Carrier-Sid'] = voip_carrier_sid;
|
||||
|
||||
// Checking if outbound carrier is different from inbound carrier and has dtmf type tones
|
||||
if (voip_carrier_sid !== req_voip_carrier_sid) {
|
||||
const [voipCarrier] = await lookupVoipCarrierBySid(voip_carrier_sid);
|
||||
this.inbandDtmfEnabled = voipCarrier?.dtmf_type === 'tones';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ const Task = require('../task');
|
||||
const {TaskPreconditions} = require('../../utils/constants');
|
||||
const TaskLlmOpenAI_S2S = require('./llms/openai_s2s');
|
||||
const TaskLlmVoiceAgent_S2S = require('./llms/voice_agent_s2s');
|
||||
const TaskLlmUltravox_S2S = require('./llms/ultravox_s2s');
|
||||
|
||||
class TaskLlm extends Task {
|
||||
constructor(logger, opts) {
|
||||
@@ -41,9 +42,7 @@ class TaskLlm extends Task {
|
||||
switch (this.vendor) {
|
||||
case 'openai':
|
||||
case 'microsoft':
|
||||
if (this.model.startsWith('gpt-4o-realtime')) {
|
||||
llm = new TaskLlmOpenAI_S2S(this.logger, this.data, this);
|
||||
}
|
||||
llm = new TaskLlmOpenAI_S2S(this.logger, this.data, this);
|
||||
break;
|
||||
|
||||
case 'voiceagent':
|
||||
@@ -51,6 +50,10 @@ class TaskLlm extends Task {
|
||||
llm = new TaskLlmVoiceAgent_S2S(this.logger, this.data, this);
|
||||
break;
|
||||
|
||||
case 'ultravox':
|
||||
llm = new TaskLlmUltravox_S2S(this.logger, this.data, this);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(`Unsupported vendor ${this.vendor} for LLM`);
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ class TaskLlmOpenAI_S2S extends Task {
|
||||
|
||||
switch (this.vendor) {
|
||||
case 'openai':
|
||||
return 'v1/realtime?model=${this.model}';
|
||||
return `v1/realtime?model=${this.model}`;
|
||||
case 'microsoft':
|
||||
return `openai/realtime?api-version=2024-10-01-preview&deployment=${this.model}`;
|
||||
}
|
||||
|
||||
245
lib/tasks/llm/llms/ultravox_s2s.js
Normal file
245
lib/tasks/llm/llms/ultravox_s2s.js
Normal file
@@ -0,0 +1,245 @@
|
||||
const Task = require('../../task');
|
||||
const TaskName = 'Llm_Ultravox_s2s';
|
||||
const {request} = require('undici');
|
||||
const {LlmEvents_Ultravox} = require('../../../utils/constants');
|
||||
|
||||
const ultravox_server_events = [
|
||||
'pong',
|
||||
'state',
|
||||
'transcript',
|
||||
'conversationText',
|
||||
'clientToolInvocation',
|
||||
'playbackClearBuffer',
|
||||
];
|
||||
|
||||
const ClientEvent = 'client.event';
|
||||
|
||||
const expandWildcards = (events) => {
|
||||
// no-op for deepgram
|
||||
return events;
|
||||
};
|
||||
|
||||
const SessionDelete = 'session.delete';
|
||||
|
||||
class TaskLlmUltravox_S2S extends Task {
|
||||
constructor(logger, opts, parentTask) {
|
||||
super(logger, opts, parentTask);
|
||||
this.parent = parentTask;
|
||||
|
||||
this.vendor = this.parent.vendor;
|
||||
this.model = this.parent.model || 'fixie-ai/ultravox';
|
||||
this.auth = this.parent.auth;
|
||||
this.connectionOptions = this.parent.connectOptions;
|
||||
|
||||
const {apiKey} = this.auth || {};
|
||||
if (!apiKey) throw new Error('auth.apiKey is required for Vendor: Ultravox');
|
||||
this.apiKey = apiKey;
|
||||
this.actionHook = this.data.actionHook;
|
||||
this.eventHook = this.data.eventHook;
|
||||
this.toolHook = this.data.toolHook;
|
||||
|
||||
/**
|
||||
* only one of these will have items,
|
||||
* if includeEvents, then these are the events to include
|
||||
* if excludeEvents, then these are the events to exclude
|
||||
*/
|
||||
this.includeEvents = [];
|
||||
this.excludeEvents = [];
|
||||
|
||||
/* default to all events if user did not specify */
|
||||
this._populateEvents(this.data.events || ultravox_server_events);
|
||||
|
||||
this.addCustomEventListener = parentTask.addCustomEventListener.bind(parentTask);
|
||||
this.removeCustomEventListeners = parentTask.removeCustomEventListeners.bind(parentTask);
|
||||
}
|
||||
|
||||
get name() { return TaskName; }
|
||||
|
||||
async _api(ep, args) {
|
||||
const res = await ep.api('uuid_ultravox_s2s', `^^|${args.join('|')}`);
|
||||
if (!res.body?.startsWith('+OK')) {
|
||||
throw new Error(`Error calling uuid_ultravox_s2s: ${JSON.stringify(res.body)}`);
|
||||
}
|
||||
}
|
||||
|
||||
async createCall() {
|
||||
const payload = {
|
||||
...this.data.llmOptions,
|
||||
model: this.model,
|
||||
medium: {
|
||||
...(this.data.llmOptions.medium || {}),
|
||||
serverWebSocket: {
|
||||
inputSampleRate: 8000,
|
||||
outputSampleRate: 8000,
|
||||
}
|
||||
}
|
||||
};
|
||||
const {statusCode, body} = await request('https://api.ultravox.ai/api/calls', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-API-Key': this.apiKey
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const data = await body.json();
|
||||
if (statusCode !== 201 || !data?.joinUrl) {
|
||||
this.logger.error({statusCode, data}, 'Ultravox Error registering call');
|
||||
throw new Error(`Ultravox Error registering call: ${data.message}`);
|
||||
}
|
||||
this.logger.info({joinUrl: data.joinUrl}, 'Ultravox Call registered');
|
||||
return data.joinUrl;
|
||||
}
|
||||
|
||||
_unregisterHandlers() {
|
||||
this.removeCustomEventListeners();
|
||||
}
|
||||
|
||||
_registerHandlers(ep) {
|
||||
this.addCustomEventListener(ep, LlmEvents_Ultravox.Connect, this._onConnect.bind(this, ep));
|
||||
this.addCustomEventListener(ep, LlmEvents_Ultravox.ConnectFailure, this._onConnectFailure.bind(this, ep));
|
||||
this.addCustomEventListener(ep, LlmEvents_Ultravox.Disconnect, this._onDisconnect.bind(this, ep));
|
||||
this.addCustomEventListener(ep, LlmEvents_Ultravox.ServerEvent, this._onServerEvent.bind(this, ep));
|
||||
}
|
||||
|
||||
async _startListening(cs, ep) {
|
||||
this._registerHandlers(ep);
|
||||
|
||||
const joinUrl = await this.createCall();
|
||||
// split the joinUrl into host and path
|
||||
const {host, pathname, search} = new URL(joinUrl);
|
||||
|
||||
try {
|
||||
const args = [ep.uuid, 'session.create', host, pathname + search];
|
||||
await this._api(ep, args);
|
||||
} catch (err) {
|
||||
this.logger.error({err}, 'TaskLlmUltraVox_S2S:_startListening');
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
}
|
||||
|
||||
async exec(cs, {ep}) {
|
||||
await super.exec(cs);
|
||||
|
||||
await this._startListening(cs, ep);
|
||||
|
||||
await this.awaitTaskDone();
|
||||
|
||||
/* note: the parent llm verb started the span, which is why this is necessary */
|
||||
await this.parent.performAction(this.results);
|
||||
|
||||
this._unregisterHandlers();
|
||||
}
|
||||
|
||||
async kill(cs) {
|
||||
super.kill(cs);
|
||||
|
||||
this._api(cs.ep, [cs.ep.uuid, SessionDelete])
|
||||
.catch((err) => this.logger.info({err}, 'TaskLlmUltravox_S2S:kill - error deleting session'));
|
||||
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
|
||||
_onConnect(ep) {
|
||||
this.logger.debug('TaskLlmUltravox_S2S:_onConnect');
|
||||
}
|
||||
_onConnectFailure(_ep, evt) {
|
||||
this.logger.info(evt, 'TaskLlmUltravox_S2S:_onConnectFailure');
|
||||
this.results = {completionReason: 'connection failure'};
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
_onDisconnect(_ep, evt) {
|
||||
this.logger.info(evt, 'TaskLlmUltravox_S2S:_onConnectFailure');
|
||||
this.results = {completionReason: 'disconnect from remote end'};
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
|
||||
async _onServerEvent(_ep, evt) {
|
||||
let endConversation = false;
|
||||
const type = evt.type;
|
||||
this.logger.info({evt}, 'TaskLlmUltravox_S2S:_onServerEvent');
|
||||
|
||||
/* server errors of some sort */
|
||||
if (type === 'error') {
|
||||
endConversation = true;
|
||||
this.results = {
|
||||
completionReason: 'server error',
|
||||
error: evt.error
|
||||
};
|
||||
}
|
||||
|
||||
/* tool calls */
|
||||
else if (type === 'client_tool_invocation') {
|
||||
this.logger.debug({evt}, 'TaskLlmUltravox_S2S:_onServerEvent - function_call');
|
||||
if (!this.toolHook) {
|
||||
this.logger.warn({evt}, 'TaskLlmUltravox_S2S:_onServerEvent - no toolHook defined!');
|
||||
}
|
||||
else {
|
||||
const {toolName: name, invocationId: call_id, parameters: args} = evt;
|
||||
|
||||
try {
|
||||
await this.parent.sendToolHook(call_id, {name, args});
|
||||
} catch (err) {
|
||||
this.logger.info({err, evt}, 'TaskLlmUltravox_S2S - error calling function');
|
||||
this.results = {
|
||||
completionReason: 'client error calling function',
|
||||
error: err
|
||||
};
|
||||
endConversation = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* check whether we should notify on this event */
|
||||
if (this.includeEvents.length > 0 ? this.includeEvents.includes(type) : !this.excludeEvents.includes(type)) {
|
||||
this.parent.sendEventHook(evt)
|
||||
.catch((err) => this.logger.info({err}, 'TaskLlmUltravox_S2S:_onServerEvent - error sending event hook'));
|
||||
}
|
||||
|
||||
if (endConversation) {
|
||||
this.logger.info({results: this.results},
|
||||
'TaskLlmUltravox_S2S:_onServerEvent - ending conversation due to error');
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
}
|
||||
|
||||
async processToolOutput(ep, tool_call_id, data) {
|
||||
try {
|
||||
this.logger.debug({tool_call_id, data}, 'TaskLlmUltravox_S2S:processToolOutput');
|
||||
|
||||
if (!data.type || data.type !== 'client_tool_result') {
|
||||
this.logger.info({data},
|
||||
'TaskLlmUltravox_S2S:processToolOutput - invalid tool output, must be client_tool_result');
|
||||
}
|
||||
else {
|
||||
await this._api(ep, [ep.uuid, ClientEvent, JSON.stringify(data)]);
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.info({err}, 'TaskLlmUltravox_S2S:processToolOutput');
|
||||
}
|
||||
}
|
||||
|
||||
_populateEvents(events) {
|
||||
if (events.includes('all')) {
|
||||
/* work by excluding specific events */
|
||||
const exclude = events
|
||||
.filter((evt) => evt.startsWith('-'))
|
||||
.map((evt) => evt.slice(1));
|
||||
if (exclude.length === 0) this.includeEvents = ultravox_server_events;
|
||||
else this.excludeEvents = expandWildcards(exclude);
|
||||
}
|
||||
else {
|
||||
/* work by including specific events */
|
||||
const include = events
|
||||
.filter((evt) => !evt.startsWith('-'));
|
||||
this.includeEvents = expandWildcards(include);
|
||||
}
|
||||
|
||||
this.logger.debug({
|
||||
includeEvents: this.includeEvents,
|
||||
excludeEvents: this.excludeEvents
|
||||
}, 'TaskLlmUltravox_S2S:_populateEvents');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TaskLlmUltravox_S2S;
|
||||
@@ -98,7 +98,7 @@ class TaskLlmVoiceAgent_S2S extends Task {
|
||||
async _api(ep, args) {
|
||||
const res = await ep.api('uuid_voice_agent_s2s', `^^|${args.join('|')}`);
|
||||
if (!res.body?.startsWith('+OK')) {
|
||||
throw new Error({args}, `Error calling uuid_voice_agent_s2s: ${JSON.stringify(res.body)}`);
|
||||
throw new Error(`Error calling uuid_voice_agent_s2s: ${JSON.stringify(res.body)}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const Task = require('./task');
|
||||
const {TaskName} = require('../utils/constants');
|
||||
const WsRequestor = require('../utils/ws-requestor');
|
||||
|
||||
/**
|
||||
* Redirects to a new application
|
||||
@@ -13,6 +14,17 @@ class TaskRedirect extends Task {
|
||||
|
||||
async exec(cs) {
|
||||
await super.exec(cs);
|
||||
|
||||
if (cs.requestor instanceof WsRequestor && cs.application.requestor._isAbsoluteUrl(this.actionHook)) {
|
||||
this.logger.info(`Task:performAction redirecting to ${this.actionHook}, requires new ws connection`);
|
||||
try {
|
||||
this.cs.requestor.close();
|
||||
const requestor = new WsRequestor(this.logger, cs.accountSid, {url: this.actionHook}, this.webhook_secret) ;
|
||||
this.cs.application.requestor = requestor;
|
||||
} catch (err) {
|
||||
this.logger.info(err, `Task:performAction error redirecting to ${this.actionHook}`);
|
||||
}
|
||||
}
|
||||
await this.performAction();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ class TaskRestDial extends Task {
|
||||
|
||||
this.from = this.data.from;
|
||||
this.callerName = this.data.callerName;
|
||||
this.timeLimit = this.data.timeLimit;
|
||||
this.fromHost = this.data.fromHost;
|
||||
this.to = this.data.to;
|
||||
this.call_hook = this.data.call_hook;
|
||||
@@ -66,6 +67,9 @@ class TaskRestDial extends Task {
|
||||
const cs = this.callSession;
|
||||
cs.setDialog(dlg);
|
||||
cs.referHook = this.referHook;
|
||||
if (this.timeLimit) {
|
||||
cs.startMaxCallDurationTimer(this.timeLimit);
|
||||
}
|
||||
this.logger.debug('TaskRestDial:_onConnect - call connected');
|
||||
if (this.sipRequestWithinDialogHook) this._initSipRequestWithinDialogHandler(cs, dlg);
|
||||
try {
|
||||
|
||||
@@ -59,7 +59,7 @@ class TtsTask extends Task {
|
||||
}
|
||||
|
||||
async setTtsStreamingChannelVars(vendor, language, voice, credentials, ep) {
|
||||
const {api_key, model_id} = credentials;
|
||||
const {api_key, model_id, custom_tts_streaming_url, auth_token} = credentials;
|
||||
const {stability, similarity_boost, use_speaker_boost, style} = this.options;
|
||||
let obj;
|
||||
|
||||
@@ -95,7 +95,18 @@ class TtsTask extends Task {
|
||||
};
|
||||
break;
|
||||
default:
|
||||
throw new Error(`vendor ${vendor} is not supported for tts streaming yet`);
|
||||
if (vendor.startsWith('custom:')) {
|
||||
const use_tls = custom_tts_streaming_url.startsWith('wss://');
|
||||
obj = {
|
||||
CUSTOM_TTS_STREAMING_HOST: custom_tts_streaming_url.replace(/^(ws|wss):\/\//, ''),
|
||||
CUSTOM_TTS_STREAMING_API_KEY: auth_token,
|
||||
CUSTOM_TTS_STREAMING_VOICE_ID: voice,
|
||||
CUSTOM_TTS_STREAMING_LANGUAGE: language || 'en',
|
||||
CUSTOM_TTS_STREAMING_USE_TLS: use_tls
|
||||
};
|
||||
} else {
|
||||
throw new Error(`vendor ${vendor} is not supported for tts streaming yet`);
|
||||
}
|
||||
}
|
||||
this.logger.info({vendor, credentials, obj}, 'setTtsStreamingChannelVars');
|
||||
|
||||
|
||||
@@ -103,6 +103,7 @@ class BackgroundTaskManager extends Emitter {
|
||||
async _initBargeIn(opts) {
|
||||
let task;
|
||||
try {
|
||||
const copy = JSON.parse(JSON.stringify(opts));
|
||||
const t = normalizeJambones(this.logger, [opts]);
|
||||
task = makeTask(this.logger, t[0]);
|
||||
task
|
||||
@@ -121,7 +122,7 @@ class BackgroundTaskManager extends Emitter {
|
||||
if (task.sticky && !this.cs.callGone && !this.cs._stopping) {
|
||||
this.logger.info('BackgroundTaskManager:_initBargeIn: restarting background bargeIn');
|
||||
this._bargeInHandled = false;
|
||||
this.newTask('bargeIn', opts, true);
|
||||
this.newTask('bargeIn', copy, true);
|
||||
}
|
||||
return;
|
||||
})
|
||||
|
||||
@@ -182,6 +182,13 @@
|
||||
"Disconnect": "voice_agent_s2s::disconnect",
|
||||
"ServerEvent": "voice_agent_s2s::server_event"
|
||||
},
|
||||
"LlmEvents_Ultravox": {
|
||||
"Error": "error",
|
||||
"Connect": "ultravox_s2s::connect",
|
||||
"ConnectFailure": "ultravox_s2s::connect_failed",
|
||||
"Disconnect": "ultravox_s2s::disconnect",
|
||||
"ServerEvent": "ultravox_s2s::server_event"
|
||||
},
|
||||
"QueueResults": {
|
||||
"Bridged": "bridged",
|
||||
"Error": "error",
|
||||
@@ -251,6 +258,11 @@
|
||||
"ConnectFailure": "elevenlabs_tts_streaming::connect_failed",
|
||||
"Connect": "elevenlabs_tts_streaming::connect"
|
||||
},
|
||||
"CustomTtsStreamingEvents": {
|
||||
"Empty": "custom_tts_streaming::empty",
|
||||
"ConnectFailure": "custom_tts_streaming::connect_failed",
|
||||
"Connect": "custom_tts_streaming::connect"
|
||||
},
|
||||
"TtsStreamingEvents": {
|
||||
"Empty": "tts_streaming::empty",
|
||||
"Pause": "tts_streaming::pause",
|
||||
|
||||
@@ -143,6 +143,7 @@ const speechMapper = (cred) => {
|
||||
obj.auth_token = o.auth_token;
|
||||
obj.custom_stt_url = o.custom_stt_url;
|
||||
obj.custom_tts_url = o.custom_tts_url;
|
||||
obj.custom_tts_streaming_url = o.custom_tts_streaming_url;
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
|
||||
@@ -349,6 +349,15 @@ class SingleDialer extends Emitter {
|
||||
if (Object.keys(opts).length > 0) {
|
||||
this.ep.set(opts);
|
||||
}
|
||||
if (this.dialTask?.inbandDtmfEnabled && !this.ep.inbandDtmfEnabled) {
|
||||
// https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Modules/mod-dptools/6587132/#0-about
|
||||
try {
|
||||
this.ep.execute('start_dtmf');
|
||||
this.ep.inbandDtmfEnabled = true;
|
||||
} catch (err) {
|
||||
this.logger.info(err, 'place-outdial:_configMsEndpoint - error enable inband DTMF');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -43,7 +43,8 @@ const stickyVars = {
|
||||
'DEEPGRAM_SPEECH_UTTERANCE_END_MS',
|
||||
'DEEPGRAM_SPEECH_VAD_TURNOFF',
|
||||
'DEEPGRAM_SPEECH_TAG',
|
||||
'DEEPGRAM_SPEECH_MODEL_VERSION'
|
||||
'DEEPGRAM_SPEECH_MODEL_VERSION',
|
||||
'DEEPGRAM_SPEECH_FILLER_WORDS'
|
||||
],
|
||||
aws: [
|
||||
'AWS_VOCABULARY_NAME',
|
||||
@@ -812,7 +813,9 @@ module.exports = (logger) => {
|
||||
...(deepgramOptions.tag) &&
|
||||
{DEEPGRAM_SPEECH_TAG: deepgramOptions.tag},
|
||||
...(deepgramOptions.version) &&
|
||||
{DEEPGRAM_SPEECH_MODEL_VERSION: deepgramOptions.version}
|
||||
{DEEPGRAM_SPEECH_MODEL_VERSION: deepgramOptions.version},
|
||||
...(deepgramOptions.fillerWords) &&
|
||||
{DEEPGRAM_SPEECH_FILLER_WORDS: deepgramOptions.fillerWords}
|
||||
};
|
||||
}
|
||||
else if ('soniox' === vendor) {
|
||||
|
||||
@@ -5,8 +5,8 @@ const {
|
||||
TtsStreamingConnectionStatus
|
||||
} = require('../utils/constants');
|
||||
const MAX_CHUNK_SIZE = 1800;
|
||||
const HIGH_WATER_BUFFER_SIZE = 5000;
|
||||
const LOW_WATER_BUFFER_SIZE = 1000;
|
||||
const HIGH_WATER_BUFFER_SIZE = 1000;
|
||||
const LOW_WATER_BUFFER_SIZE = 200;
|
||||
const TIMEOUT_RETRY_MSECS = 3000;
|
||||
|
||||
class TtsStreamingBuffer extends Emitter {
|
||||
@@ -90,7 +90,7 @@ class TtsStreamingBuffer extends Emitter {
|
||||
/* if we crossed the high water mark, reject the request */
|
||||
if (this.tokens.length + totalLength > HIGH_WATER_BUFFER_SIZE) {
|
||||
this.logger.info(
|
||||
`TtsStreamingBuffer:bufferTokensTTS buffer is full, rejecting request to buffer ${totalLength} tokens`);
|
||||
`TtsStreamingBuffer throttling: buffer is full, rejecting request to buffer ${totalLength} tokens`);
|
||||
|
||||
if (!this._isFull) {
|
||||
this._isFull = true;
|
||||
@@ -117,9 +117,14 @@ class TtsStreamingBuffer extends Emitter {
|
||||
return;
|
||||
}
|
||||
else if (this._connectionStatus === TtsStreamingConnectionStatus.Connected) {
|
||||
this._api(this.ep, [this.ep.uuid, 'flush'])
|
||||
.catch((err) => this.logger.info({err},
|
||||
`TtsStreamingBuffer:flush Error flushing TTS streaming: ${JSON.stringify(err)}`));
|
||||
|
||||
if (this.size === 0) {
|
||||
this._doFlush();
|
||||
}
|
||||
else {
|
||||
/* we have tokens queued, so flush after they have been sent */
|
||||
this._pendingFlush = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,8 +195,13 @@ class TtsStreamingBuffer extends Emitter {
|
||||
await this._api(this.ep, [this.ep.uuid, 'send', modifiedChunk]);
|
||||
this.logger.debug(`TtsStreamingBuffer:_feedTokens: sent ${chunk.length}, remaining: ${this.tokens.length}`);
|
||||
|
||||
if (this._pendingFlush) {
|
||||
this._doFlush();
|
||||
this._pendingFlush = false;
|
||||
}
|
||||
|
||||
if (this.isFull && this.tokens.length <= LOW_WATER_BUFFER_SIZE) {
|
||||
this.logger.info('TtsStreamingBuffer:_feedTokens TTS streaming buffer is no longer full');
|
||||
this.logger.info('TtsStreamingBuffer throttling: TTS streaming buffer is no longer full - resuming');
|
||||
this._isFull = false;
|
||||
this.emit(TtsStreamingEvents.Resume);
|
||||
}
|
||||
@@ -204,7 +214,7 @@ class TtsStreamingBuffer extends Emitter {
|
||||
}
|
||||
|
||||
async _api(ep, args) {
|
||||
const apiCmd = `uuid_${this.vendor}_tts_streaming`;
|
||||
const apiCmd = `uuid_${this.vendor.startsWith('custom:') ? 'custom' : this.vendor}_tts_streaming`;
|
||||
const res = await ep.api(apiCmd, `^^|${args.join('|')}`);
|
||||
if (!res.body?.startsWith('+OK')) {
|
||||
this.logger.info({args}, `Error calling ${apiCmd}: ${res.body}`);
|
||||
@@ -219,6 +229,12 @@ class TtsStreamingBuffer extends Emitter {
|
||||
this.emit(TtsStreamingEvents.ConnectFailure, {vendor});
|
||||
}
|
||||
|
||||
_doFlush() {
|
||||
this._api(this.ep, [this.ep.uuid, 'flush'])
|
||||
.catch((err) => this.logger.info({err},
|
||||
`TtsStreamingBuffer:_doFlush Error flushing TTS streaming: ${JSON.stringify(err)}`));
|
||||
}
|
||||
|
||||
async _onConnect(vendor) {
|
||||
this.logger.info(`streaming tts connection made to ${vendor}`);
|
||||
this._connectionStatus = TtsStreamingConnectionStatus.Connected;
|
||||
@@ -261,7 +277,8 @@ class TtsStreamingBuffer extends Emitter {
|
||||
// DH: add other vendors here as modules are added
|
||||
'deepgram',
|
||||
'cartesia',
|
||||
'elevenlabs'
|
||||
'elevenlabs',
|
||||
'custom'
|
||||
].forEach((vendor) => {
|
||||
const eventClassName = `${vendor.charAt(0).toUpperCase() + vendor.slice(1)}TtsStreamingEvents`;
|
||||
const eventClass = require('../utils/constants')[eventClassName];
|
||||
|
||||
86
package-lock.json
generated
86
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "jambonz-feature-server",
|
||||
"version": "0.9.2",
|
||||
"version": "0.9.3",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "jambonz-feature-server",
|
||||
"version": "0.9.2",
|
||||
"version": "0.9.3",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-auto-scaling": "^3.549.0",
|
||||
@@ -18,7 +18,7 @@
|
||||
"@jambonz/speech-utils": "^0.2.1",
|
||||
"@jambonz/stats-collector": "^0.1.10",
|
||||
"@jambonz/time-series": "^0.2.13",
|
||||
"@jambonz/verb-specifications": "^0.0.90",
|
||||
"@jambonz/verb-specifications": "^0.0.92",
|
||||
"@opentelemetry/api": "^1.8.0",
|
||||
"@opentelemetry/exporter-jaeger": "^1.23.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.50.0",
|
||||
@@ -31,8 +31,8 @@
|
||||
"bent": "^7.3.12",
|
||||
"debug": "^4.3.4",
|
||||
"deepcopy": "^2.1.0",
|
||||
"drachtio-fsmrf": "^3.0.46",
|
||||
"drachtio-srf": "^4.5.35",
|
||||
"drachtio-fsmrf": "^4.0.1",
|
||||
"drachtio-srf": "^5.0.1",
|
||||
"express": "^4.19.2",
|
||||
"express-validator": "^7.0.1",
|
||||
"moment": "^2.30.1",
|
||||
@@ -40,7 +40,7 @@
|
||||
"pino": "^8.20.0",
|
||||
"polly-ssml-split": "^0.1.0",
|
||||
"proxyquire": "^2.1.3",
|
||||
"sdp-transform": "^2.14.2",
|
||||
"sdp-transform": "^2.15.0",
|
||||
"short-uuid": "^5.1.0",
|
||||
"sinon": "^17.0.1",
|
||||
"to-snake-case": "^1.0.0",
|
||||
@@ -1671,9 +1671,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@jambonz/verb-specifications": {
|
||||
"version": "0.0.90",
|
||||
"resolved": "https://registry.npmjs.org/@jambonz/verb-specifications/-/verb-specifications-0.0.90.tgz",
|
||||
"integrity": "sha512-ix2XByDTSroWlEMMY26ba2mbON4Yh3911Agm44GeV3ygmJMGABwCv28KqItOVBW0Ro5uSM+cQ0vEacPVnmAldA==",
|
||||
"version": "0.0.92",
|
||||
"resolved": "https://registry.npmjs.org/@jambonz/verb-specifications/-/verb-specifications-0.0.92.tgz",
|
||||
"integrity": "sha512-zb1y5Hq+FqGYleYYKZafEIHyhhlH3VHapTJh3N0s+2xdy8I2Gf17zJpUc45mhV/4ficlT3SSwETsBbMt20Hwog==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.3.4",
|
||||
@@ -3969,18 +3969,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/drachtio-fsmrf": {
|
||||
"version": "3.0.46",
|
||||
"resolved": "https://registry.npmjs.org/drachtio-fsmrf/-/drachtio-fsmrf-3.0.46.tgz",
|
||||
"integrity": "sha512-ScgyOnsOL45feuKKquT2Gij/jDRdjVha2TnQ6/Me2/M/C+29c9W7cdYlt3UZB3GyodazBukwIy5RrZf1iuXBGw==",
|
||||
"license": "MIT",
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/drachtio-fsmrf/-/drachtio-fsmrf-4.0.1.tgz",
|
||||
"integrity": "sha512-QjXHe1d0D7KJTmXAyiKr07TNKPIT6BAPYHqBU8lp51Ms7MgN/nNUCVUPvL3RRUA6FfU7gxaNtYy0LIfdskHVJQ==",
|
||||
"dependencies": {
|
||||
"camel-case": "^4.1.2",
|
||||
"debug": "^2.6.9",
|
||||
"delegates": "^0.1.0",
|
||||
"drachtio-modesl": "^1.2.9",
|
||||
"drachtio-srf": "^4.5.38",
|
||||
"drachtio-srf": "^5.0.1",
|
||||
"only": "^0.0.2",
|
||||
"sdp-transform": "^2.14.1",
|
||||
"sdp-transform": "^2.15.0",
|
||||
"snake-case": "^3.0.4",
|
||||
"uuid-random": "^1.3.2"
|
||||
},
|
||||
@@ -4027,15 +4026,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/drachtio-srf": {
|
||||
"version": "4.5.40",
|
||||
"resolved": "https://registry.npmjs.org/drachtio-srf/-/drachtio-srf-4.5.40.tgz",
|
||||
"integrity": "sha512-rybIu6kcY8oyw98z9eYQ5lSOALdEINPVc82N3Updl78qgLJvo9qEQ/j+idawUfMlIuqJ0Ab38wkqNeg5Cn3jNA==",
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/drachtio-srf/-/drachtio-srf-5.0.1.tgz",
|
||||
"integrity": "sha512-HMPn+xLm2TCYY6cJoRpTlhCRakyKgPD93lM1gg6nNIlO9gPSKlUjX3mxL4iaFKuh+UFMMTAoB26fj892A6Lr4Q==",
|
||||
"dependencies": {
|
||||
"debug": "^3.2.7",
|
||||
"delegates": "^0.1.0",
|
||||
"node-noop": "^0.0.1",
|
||||
"only": "^0.0.2",
|
||||
"sdp-transform": "^2.14.1",
|
||||
"sdp-transform": "^2.15.0",
|
||||
"short-uuid": "^4.2.2",
|
||||
"sip-methods": "^0.3.0",
|
||||
"sip-status": "^0.1.0",
|
||||
@@ -8172,9 +8171,9 @@
|
||||
"integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA=="
|
||||
},
|
||||
"node_modules/sdp-transform": {
|
||||
"version": "2.14.2",
|
||||
"resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.14.2.tgz",
|
||||
"integrity": "sha512-icY6jVao7MfKCieyo1AyxFYm1baiM+fA00qW/KrNNVlkxHAd34riEKuEkUe4bBb3gJwLJZM+xT60Yj1QL8rHiA==",
|
||||
"version": "2.15.0",
|
||||
"resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.15.0.tgz",
|
||||
"integrity": "sha512-KrOH82c/W+GYQ0LHqtr3caRpM3ITglq3ljGUIb8LTki7ByacJZ9z+piSGiwZDsRyhQbYBOBJgr2k6X4BZXi3Kw==",
|
||||
"bin": {
|
||||
"sdp-verify": "checker.js"
|
||||
}
|
||||
@@ -9004,9 +9003,10 @@
|
||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
|
||||
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/type": {
|
||||
"version": "1.2.0",
|
||||
@@ -10939,9 +10939,9 @@
|
||||
}
|
||||
},
|
||||
"@jambonz/verb-specifications": {
|
||||
"version": "0.0.90",
|
||||
"resolved": "https://registry.npmjs.org/@jambonz/verb-specifications/-/verb-specifications-0.0.90.tgz",
|
||||
"integrity": "sha512-ix2XByDTSroWlEMMY26ba2mbON4Yh3911Agm44GeV3ygmJMGABwCv28KqItOVBW0Ro5uSM+cQ0vEacPVnmAldA==",
|
||||
"version": "0.0.92",
|
||||
"resolved": "https://registry.npmjs.org/@jambonz/verb-specifications/-/verb-specifications-0.0.92.tgz",
|
||||
"integrity": "sha512-zb1y5Hq+FqGYleYYKZafEIHyhhlH3VHapTJh3N0s+2xdy8I2Gf17zJpUc45mhV/4ficlT3SSwETsBbMt20Hwog==",
|
||||
"requires": {
|
||||
"debug": "^4.3.4",
|
||||
"pino": "^8.8.0"
|
||||
@@ -12652,17 +12652,17 @@
|
||||
}
|
||||
},
|
||||
"drachtio-fsmrf": {
|
||||
"version": "3.0.46",
|
||||
"resolved": "https://registry.npmjs.org/drachtio-fsmrf/-/drachtio-fsmrf-3.0.46.tgz",
|
||||
"integrity": "sha512-ScgyOnsOL45feuKKquT2Gij/jDRdjVha2TnQ6/Me2/M/C+29c9W7cdYlt3UZB3GyodazBukwIy5RrZf1iuXBGw==",
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/drachtio-fsmrf/-/drachtio-fsmrf-4.0.1.tgz",
|
||||
"integrity": "sha512-QjXHe1d0D7KJTmXAyiKr07TNKPIT6BAPYHqBU8lp51Ms7MgN/nNUCVUPvL3RRUA6FfU7gxaNtYy0LIfdskHVJQ==",
|
||||
"requires": {
|
||||
"camel-case": "^4.1.2",
|
||||
"debug": "^2.6.9",
|
||||
"delegates": "^0.1.0",
|
||||
"drachtio-modesl": "^1.2.9",
|
||||
"drachtio-srf": "^4.5.38",
|
||||
"drachtio-srf": "^5.0.1",
|
||||
"only": "^0.0.2",
|
||||
"sdp-transform": "^2.14.1",
|
||||
"sdp-transform": "^2.15.0",
|
||||
"snake-case": "^3.0.4",
|
||||
"uuid-random": "^1.3.2"
|
||||
},
|
||||
@@ -12704,15 +12704,15 @@
|
||||
}
|
||||
},
|
||||
"drachtio-srf": {
|
||||
"version": "4.5.40",
|
||||
"resolved": "https://registry.npmjs.org/drachtio-srf/-/drachtio-srf-4.5.40.tgz",
|
||||
"integrity": "sha512-rybIu6kcY8oyw98z9eYQ5lSOALdEINPVc82N3Updl78qgLJvo9qEQ/j+idawUfMlIuqJ0Ab38wkqNeg5Cn3jNA==",
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/drachtio-srf/-/drachtio-srf-5.0.1.tgz",
|
||||
"integrity": "sha512-HMPn+xLm2TCYY6cJoRpTlhCRakyKgPD93lM1gg6nNIlO9gPSKlUjX3mxL4iaFKuh+UFMMTAoB26fj892A6Lr4Q==",
|
||||
"requires": {
|
||||
"debug": "^3.2.7",
|
||||
"delegates": "^0.1.0",
|
||||
"node-noop": "^0.0.1",
|
||||
"only": "^0.0.2",
|
||||
"sdp-transform": "^2.14.1",
|
||||
"sdp-transform": "^2.15.0",
|
||||
"short-uuid": "^4.2.2",
|
||||
"sip-methods": "^0.3.0",
|
||||
"sip-status": "^0.1.0",
|
||||
@@ -15777,9 +15777,9 @@
|
||||
"integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA=="
|
||||
},
|
||||
"sdp-transform": {
|
||||
"version": "2.14.2",
|
||||
"resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.14.2.tgz",
|
||||
"integrity": "sha512-icY6jVao7MfKCieyo1AyxFYm1baiM+fA00qW/KrNNVlkxHAd34riEKuEkUe4bBb3gJwLJZM+xT60Yj1QL8rHiA=="
|
||||
"version": "2.15.0",
|
||||
"resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.15.0.tgz",
|
||||
"integrity": "sha512-KrOH82c/W+GYQ0LHqtr3caRpM3ITglq3ljGUIb8LTki7ByacJZ9z+piSGiwZDsRyhQbYBOBJgr2k6X4BZXi3Kw=="
|
||||
},
|
||||
"semver": {
|
||||
"version": "7.5.4",
|
||||
@@ -16409,9 +16409,9 @@
|
||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
|
||||
},
|
||||
"tslib": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
|
||||
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
|
||||
},
|
||||
"type": {
|
||||
"version": "1.2.0",
|
||||
|
||||
10
package.json
10
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jambonz-feature-server",
|
||||
"version": "0.9.2",
|
||||
"version": "0.9.3",
|
||||
"main": "app.js",
|
||||
"engines": {
|
||||
"node": ">= 18.x"
|
||||
@@ -33,7 +33,7 @@
|
||||
"@jambonz/realtimedb-helpers": "^0.8.8",
|
||||
"@jambonz/speech-utils": "^0.2.1",
|
||||
"@jambonz/stats-collector": "^0.1.10",
|
||||
"@jambonz/verb-specifications": "^0.0.90",
|
||||
"@jambonz/verb-specifications": "^0.0.92",
|
||||
"@jambonz/time-series": "^0.2.13",
|
||||
"@opentelemetry/api": "^1.8.0",
|
||||
"@opentelemetry/exporter-jaeger": "^1.23.0",
|
||||
@@ -47,8 +47,8 @@
|
||||
"bent": "^7.3.12",
|
||||
"debug": "^4.3.4",
|
||||
"deepcopy": "^2.1.0",
|
||||
"drachtio-fsmrf": "^3.0.46",
|
||||
"drachtio-srf": "^4.5.35",
|
||||
"drachtio-fsmrf": "^4.0.1",
|
||||
"drachtio-srf": "^5.0.1",
|
||||
"express": "^4.19.2",
|
||||
"express-validator": "^7.0.1",
|
||||
"moment": "^2.30.1",
|
||||
@@ -56,7 +56,7 @@
|
||||
"pino": "^8.20.0",
|
||||
"polly-ssml-split": "^0.1.0",
|
||||
"proxyquire": "^2.1.3",
|
||||
"sdp-transform": "^2.14.2",
|
||||
"sdp-transform": "^2.15.0",
|
||||
"short-uuid": "^5.1.0",
|
||||
"sinon": "^17.0.1",
|
||||
"to-snake-case": "^1.0.0",
|
||||
|
||||
@@ -222,3 +222,62 @@ test('test create-call app_json', async(t) => {
|
||||
t.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
test('test create-call timeLimit', async(t) => {
|
||||
clearModule.all();
|
||||
const {srf, disconnect} = require('../app');
|
||||
|
||||
try {
|
||||
await connect(srf);
|
||||
|
||||
|
||||
// GIVEN
|
||||
let from = 'create-call-app-json';
|
||||
let account_sid = 'bb845d4b-83a9-4cde-a6e9-50f3743bab3f';
|
||||
|
||||
// Give UAS app time to come up
|
||||
const p = sippUac('uas.xml', '172.38.0.10', from);
|
||||
await waitFor(1000);
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
const app_json = `[
|
||||
{
|
||||
"verb": "pause",
|
||||
"length": 7
|
||||
}
|
||||
]`;
|
||||
|
||||
const post = bent('http://127.0.0.1:3000/', 'POST', 'json', 201);
|
||||
post('v1/createCall', {
|
||||
'account_sid':account_sid,
|
||||
"call_hook": {
|
||||
"url": "http://127.0.0.1:3100/",
|
||||
"method": "POST",
|
||||
"username": "username",
|
||||
"password": "password"
|
||||
},
|
||||
app_json,
|
||||
"from": from,
|
||||
"to": {
|
||||
"type": "phone",
|
||||
"number": "15583084809"
|
||||
},
|
||||
"timeLimit": 1,
|
||||
"speech_recognizer_vendor": "google",
|
||||
"speech_recognizer_language": "en"
|
||||
});
|
||||
|
||||
//THEN
|
||||
await p;
|
||||
const endTime = Date.now();
|
||||
|
||||
t.ok(endTime - startTime < 2000, 'create-call: timeLimit is respected');
|
||||
|
||||
disconnect();
|
||||
} catch (err) {
|
||||
console.log(`error received: ${err}`);
|
||||
disconnect();
|
||||
t.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user