mirror of
https://github.com/jambonz/jambonz-feature-server.git
synced 2026-02-10 00:13:41 +00:00
Compare commits
2 Commits
v0.9.6-rc2
...
feat/noise
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ceb0339083 | ||
|
|
f070f262db |
@@ -2028,7 +2028,7 @@ Duration=${duration} `
|
||||
return this._lccDub(opts.dub, callSid);
|
||||
}
|
||||
else if (opts.boostAudioSignal) {
|
||||
return this._lccBoostAudioSignal(opts.boostAudioSignal, callSid);
|
||||
return this._lccBoostAudioSignal(opts, callSid);
|
||||
}
|
||||
else if (opts.media_path) {
|
||||
return this._lccMediaPath(opts.media_path, callSid);
|
||||
|
||||
@@ -19,7 +19,8 @@ class TaskConfig extends Task {
|
||||
'vad',
|
||||
'ttsStream',
|
||||
'autoStreamTts',
|
||||
'disableTtsCache'
|
||||
'disableTtsCache',
|
||||
'noiseIsolation'
|
||||
].forEach((k) => this[k] = this.data[k] || {});
|
||||
|
||||
if ('notifyEvents' in this.data) {
|
||||
@@ -90,6 +91,7 @@ class TaskConfig extends Task {
|
||||
get hasNotifySttLatency() { return Object.keys(this.data).includes('notifySttLatency'); }
|
||||
get hasTtsStream() { return Object.keys(this.ttsStream).length; }
|
||||
get hasDisableTtsCache() { return Object.keys(this.data).includes('disableTtsCache'); }
|
||||
get hasNoiseIsolation() { return Object.keys(this.data).includes('noiseIsolation'); }
|
||||
|
||||
get summary() {
|
||||
const phrase = [];
|
||||
@@ -128,6 +130,7 @@ class TaskConfig extends Task {
|
||||
}
|
||||
if ('autoStreamTts' in this.data) phrase.push(`enable Say.stream value ${this.data.autoStreamTts ? 'on' : 'off'}`);
|
||||
if (this.hasDisableTtsCache) phrase.push(`disableTtsCache ${this.data.disableTtsCache ? 'on' : 'off'}`);
|
||||
if (this.hasNoiseIsolation) phrase.push(`noiseIsolation ${this.noiseIsolation.enable ? 'on' : 'off'}`);
|
||||
return `${this.name}{${phrase.join(',')}}`;
|
||||
}
|
||||
|
||||
@@ -365,6 +368,17 @@ class TaskConfig extends Task {
|
||||
this.logger.info(`set disableTtsCache = ${this.disableTtsCache}`);
|
||||
cs.disableTtsCache = this.data.disableTtsCache;
|
||||
}
|
||||
|
||||
if (this.hasNoiseIsolation) {
|
||||
const {enable, ...opts} = this.noiseIsolation;
|
||||
if (enable) {
|
||||
this.logger.debug({opts}, 'Config: enabling noiseIsolation');
|
||||
cs.startBackgroundTask('noiseIsolation', {verb: 'noiseIsolation', ...opts});
|
||||
} else {
|
||||
this.logger.info('Config: disabling noiseIsolation');
|
||||
cs.stopBackgroundTask('noiseIsolation');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async kill(cs) {
|
||||
|
||||
@@ -195,9 +195,6 @@ class TaskDial extends Task {
|
||||
async exec(cs) {
|
||||
await super.exec(cs);
|
||||
|
||||
/* capture whether A leg was already answered before this dial task started */
|
||||
this._aLegAlreadyAnswered = !!cs.dlg;
|
||||
|
||||
if (this.data.anchorMedia && this.data.exitMediaPath) {
|
||||
this.logger.info('Dial:exec - incompatible anchorMedia and exitMediaPath are both set, will obey anchorMedia');
|
||||
delete this.data.exitMediaPath;
|
||||
@@ -553,7 +550,7 @@ class TaskDial extends Task {
|
||||
let sbcAddress = this.proxy || getSBC();
|
||||
const teamsInfo = {};
|
||||
let fqdn;
|
||||
const forwardPAI = this.forwardPAI ?? !JAMBONZ_DIAL_PAI_HEADER; // dial verb overides env var
|
||||
const forwardPAI = this.forwardPAI ?? JAMBONZ_DIAL_PAI_HEADER; // dial verb overides env var
|
||||
this.logger.debug(forwardPAI, 'forwardPAI value');
|
||||
if (!sbcAddress) throw new Error('no SBC found for outbound call');
|
||||
this.headers = {
|
||||
@@ -875,12 +872,8 @@ class TaskDial extends Task {
|
||||
this.sd = sd;
|
||||
this.callSid = sd.callSid;
|
||||
if (this.earlyMedia) {
|
||||
if (this._aLegAlreadyAnswered) {
|
||||
debug('Dial:_selectSingleDial A leg was already answered, skipping propagateAnswer');
|
||||
} else {
|
||||
debug('Dial:_selectSingleDial propagating answer supervision on A leg now that B is connected');
|
||||
await cs.propagateAnswer();
|
||||
}
|
||||
debug('Dial:_selectSingleDial propagating answer supervision on A leg now that B is connected');
|
||||
await cs.propagateAnswer();
|
||||
}
|
||||
if (this.timeLimit) {
|
||||
this.timerMaxCallDuration = setTimeout(this._onMaxCallDuration.bind(this, cs), this.timeLimit * 1000);
|
||||
|
||||
@@ -152,17 +152,9 @@ class TaskListen extends Task {
|
||||
|
||||
async _startListening(cs, ep) {
|
||||
this._initListeners(ep);
|
||||
const tempci = this.nested ? this.parentTask.sd.callInfo : cs.callInfo.toJSON();
|
||||
const ci = structuredClone(tempci);
|
||||
const ci = this.nested ? this.parentTask.sd.callInfo : cs.callInfo.toJSON();
|
||||
if (this._ignoreCustomerData) {
|
||||
delete ci.customerData;
|
||||
} else {
|
||||
for (const key in ci.customerData) {
|
||||
if (ci.customerData.hasOwnProperty(key)) {
|
||||
const value = ci.customerData[key];
|
||||
ci.customerData[key] = typeof value === 'string' ? escapeString(value) : value;
|
||||
}
|
||||
}
|
||||
}
|
||||
const metadata = Object.assign(
|
||||
{sampleRate: this.sampleRate, mixType: this.mixType},
|
||||
|
||||
@@ -36,9 +36,6 @@ class TaskLlmGoogle_S2S extends Task {
|
||||
this.model = this.parent.model || 'models/gemini-2.0-flash-live-001';
|
||||
this.auth = this.parent.auth;
|
||||
this.connectionOptions = this.parent.connectOptions;
|
||||
const {host, version} = this.connectionOptions || {};
|
||||
this.host = host;
|
||||
this.version = version;
|
||||
|
||||
const {apiKey} = this.auth || {};
|
||||
if (!apiKey) throw new Error('auth.apiKey is required for Google S2S');
|
||||
@@ -49,7 +46,7 @@ class TaskLlmGoogle_S2S extends Task {
|
||||
this.eventHook = this.data.eventHook;
|
||||
this.toolHook = this.data.toolHook;
|
||||
|
||||
const {setup, sessionResumption} = this.data.llmOptions;
|
||||
const {setup} = this.data.llmOptions;
|
||||
|
||||
if (typeof setup !== 'object') {
|
||||
throw new Error('llmOptions with an initial setup is required for Google S2S');
|
||||
@@ -57,7 +54,6 @@ class TaskLlmGoogle_S2S extends Task {
|
||||
this.setup = {
|
||||
...setup,
|
||||
model: this.model,
|
||||
...(sessionResumption && {sessionResumption}),
|
||||
// make sure output is always audio
|
||||
generationConfig: {
|
||||
...(setup.generationConfig || {}),
|
||||
@@ -142,10 +138,6 @@ class TaskLlmGoogle_S2S extends Task {
|
||||
|
||||
try {
|
||||
const args = [ep.uuid, 'session.create', this.apiKey];
|
||||
if (this.host) {
|
||||
args.push(this.host);
|
||||
if (this.version) args.push(this.version);
|
||||
}
|
||||
await this._api(ep, args);
|
||||
} catch (err) {
|
||||
this.logger.error({err}, 'TaskLlmGoogle_S2S:_startListening');
|
||||
|
||||
@@ -99,6 +99,9 @@ function makeTask(logger, obj, parent) {
|
||||
case TaskName.Alert:
|
||||
const TaskAlert = require('./alert');
|
||||
return new TaskAlert(logger, data, parent);
|
||||
case TaskName.NoiseIsolation:
|
||||
const TaskNoiseIsolation = require('./noise-isolation');
|
||||
return new TaskNoiseIsolation(logger, data, parent);
|
||||
}
|
||||
|
||||
// should never reach
|
||||
|
||||
90
lib/tasks/noise-isolation.js
Normal file
90
lib/tasks/noise-isolation.js
Normal file
@@ -0,0 +1,90 @@
|
||||
const Task = require('./task');
|
||||
const {TaskName, TaskPreconditions} = require('../utils/constants');
|
||||
|
||||
class TaskNoiseIsolation extends Task {
|
||||
constructor(logger, opts, parentTask) {
|
||||
super(logger, opts, parentTask);
|
||||
this.preconditions = TaskPreconditions.Endpoint;
|
||||
|
||||
this.vendor = this.data.vendor || 'krisp';
|
||||
this.direction = this.data.direction || 'read';
|
||||
this.level = typeof this.data.level === 'number' ? this.data.level : 100;
|
||||
this.model = this.data.model;
|
||||
}
|
||||
|
||||
get name() { return TaskName.NoiseIsolation; }
|
||||
|
||||
get apiCommand() {
|
||||
return `uuid_${this.vendor}_noise_isolation`;
|
||||
}
|
||||
|
||||
get summary() {
|
||||
return `${this.name}{vendor=${this.vendor},direction=${this.direction},level=${this.level}}`;
|
||||
}
|
||||
|
||||
async exec(cs, {ep}) {
|
||||
await super.exec(cs);
|
||||
this.ep = ep;
|
||||
|
||||
if (!ep?.connected) {
|
||||
this.logger.info('TaskNoiseIsolation:exec - no endpoint connected');
|
||||
this.notifyTaskDone();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this._startNoiseIsolation(ep);
|
||||
await this.awaitTaskDone();
|
||||
} catch (err) {
|
||||
this.logger.error({err}, 'TaskNoiseIsolation:exec - error');
|
||||
}
|
||||
}
|
||||
|
||||
async _startNoiseIsolation(ep) {
|
||||
// API format: uuid_${vendor}_noise_isolation <uuid> start <direction> [level] [model]
|
||||
// model is only added if level is set
|
||||
const args = [ep.uuid, 'start', this.direction];
|
||||
if (this.level !== 100) {
|
||||
args.push(this.level);
|
||||
if (this.model) {
|
||||
args.push(this.model);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.info({args, apiCommand: this.apiCommand}, 'TaskNoiseIsolation:_startNoiseIsolation');
|
||||
|
||||
try {
|
||||
const res = await ep.api(this.apiCommand, args.join(' '));
|
||||
if (!res.body?.startsWith('+OK')) {
|
||||
this.logger.error({res}, 'TaskNoiseIsolation:_startNoiseIsolation - error starting noise isolation');
|
||||
} else {
|
||||
this.logger.info('TaskNoiseIsolation:_startNoiseIsolation - noise isolation started');
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.error({err}, 'TaskNoiseIsolation:_startNoiseIsolation - error');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async _stopNoiseIsolation(ep) {
|
||||
if (!ep?.connected) return;
|
||||
|
||||
const args = [ep.uuid, 'stop'];
|
||||
this.logger.info({args, apiCommand: this.apiCommand}, 'TaskNoiseIsolation:_stopNoiseIsolation');
|
||||
|
||||
try {
|
||||
await ep.api(this.apiCommand, args.join(' '));
|
||||
this.logger.info('TaskNoiseIsolation:_stopNoiseIsolation - noise isolation stopped');
|
||||
} catch (err) {
|
||||
this.logger.info({err}, 'TaskNoiseIsolation:_stopNoiseIsolation - error stopping noise isolation');
|
||||
}
|
||||
}
|
||||
|
||||
async kill(cs) {
|
||||
super.kill(cs);
|
||||
await this._stopNoiseIsolation(this.ep);
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TaskNoiseIsolation;
|
||||
@@ -157,13 +157,6 @@ class TtsTask extends Task {
|
||||
...(reduceLatency && {RIMELABS_TTS_STREAMING_REDUCE_LATENCY: reduceLatency})
|
||||
};
|
||||
break;
|
||||
case 'google':
|
||||
obj = {
|
||||
GOOGLE_TTS_LANGUAGE_CODE: language,
|
||||
GOOGLE_TTS_VOICE_NAME: voice,
|
||||
GOOGLE_APPLICATION_CREDENTIALS: JSON.stringify(credentials.credentials)
|
||||
};
|
||||
break;
|
||||
default:
|
||||
if (vendor.startsWith('custom:')) {
|
||||
const use_tls = custom_tts_streaming_url.startsWith('wss://');
|
||||
|
||||
@@ -49,6 +49,9 @@ class BackgroundTaskManager extends Emitter {
|
||||
case 'ttsStream':
|
||||
task = await this._initTtsStream(opts);
|
||||
break;
|
||||
case 'noiseIsolation':
|
||||
task = await this._initNoiseIsolation(opts);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -194,6 +197,25 @@ class BackgroundTaskManager extends Emitter {
|
||||
return task;
|
||||
}
|
||||
|
||||
// Initiate Noise Isolation
|
||||
async _initNoiseIsolation(opts) {
|
||||
let task;
|
||||
try {
|
||||
const t = normalizeJambones(this.logger, [opts]);
|
||||
task = makeTask(this.logger, t[0]);
|
||||
const resources = await this.cs._evaluatePreconditions(task);
|
||||
const {span, ctx} = this.rootSpan.startChildSpan(`background-noiseIsolation:${task.summary}`);
|
||||
task.span = span;
|
||||
task.ctx = ctx;
|
||||
task.exec(this.cs, resources)
|
||||
.then(this._taskCompleted.bind(this, 'noiseIsolation', task))
|
||||
.catch(this._taskError.bind(this, 'noiseIsolation', task));
|
||||
} catch (err) {
|
||||
this.logger.info(err, 'BackgroundTaskManager:_initNoiseIsolation - Error creating noiseIsolation task');
|
||||
}
|
||||
return task;
|
||||
}
|
||||
|
||||
_taskCompleted(type, task) {
|
||||
this.logger.debug({type, task}, `BackgroundTaskManager:_taskCompleted: task completed, sticky: ${task.sticky}`);
|
||||
task.removeAllListeners();
|
||||
|
||||
@@ -31,7 +31,8 @@
|
||||
"SayLegacy": "say:legacy",
|
||||
"Stream": "stream",
|
||||
"Tag": "tag",
|
||||
"Transcribe": "transcribe"
|
||||
"Transcribe": "transcribe",
|
||||
"NoiseIsolation": "noiseIsolation"
|
||||
},
|
||||
"AllowedSipRecVerbs": ["answer", "config", "gather", "transcribe", "listen", "tag", "hangup", "sip:decline"],
|
||||
"AllowedConfirmSessionVerbs": ["config", "gather", "plays", "say", "tag"],
|
||||
@@ -311,11 +312,6 @@
|
||||
"ConnectFailure": "deepgram_tts_streaming::connect_failed",
|
||||
"Connect": "deepgram_tts_streaming::connect"
|
||||
},
|
||||
"GoogleTtsStreamingEvents": {
|
||||
"Empty": "google_tts_streaming::empty",
|
||||
"ConnectFailure": "google_tts_streaming::connect_failed",
|
||||
"Connect": "google_tts_streaming::connect"
|
||||
},
|
||||
"CartesiaTtsStreamingEvents": {
|
||||
"Empty": "cartesia_tts_streaming::empty",
|
||||
"ConnectFailure": "cartesia_tts_streaming::connect_failed",
|
||||
|
||||
@@ -1310,9 +1310,6 @@ module.exports = (logger) => {
|
||||
...(openaiOptions.turn_detection.silence_duration_ms && {
|
||||
OPENAI_TURN_DETECTION_SILENCE_DURATION_MS: openaiOptions.turn_detection.silence_duration_ms
|
||||
}),
|
||||
...(openaiOptions.turn_detection.eagerness && {
|
||||
OPENAI_TURN_DETECTION_EAGERNESS: openaiOptions.turn_detection.eagerness
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -421,7 +421,6 @@ class TtsStreamingBuffer extends Emitter {
|
||||
'cartesia',
|
||||
'elevenlabs',
|
||||
'rimelabs',
|
||||
'google',
|
||||
'custom'
|
||||
].forEach((vendor) => {
|
||||
const eventClassName = `${vendor.charAt(0).toUpperCase() + vendor.slice(1)}TtsStreamingEvents`;
|
||||
|
||||
1283
package-lock.json
generated
1283
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -49,7 +49,7 @@
|
||||
"debug": "^4.3.4",
|
||||
"deepcopy": "^2.1.0",
|
||||
"drachtio-fsmrf": "^4.1.2",
|
||||
"drachtio-srf": "^5.0.18",
|
||||
"drachtio-srf": "^5.0.14",
|
||||
"express": "^4.19.2",
|
||||
"express-validator": "^7.0.1",
|
||||
"moment": "^2.30.1",
|
||||
|
||||
Reference in New Issue
Block a user