Compare commits

..

2 Commits

Author SHA1 Message Date
xquanluu
ceb0339083 wip 2026-01-28 08:57:51 +07:00
xquanluu
f070f262db support noise isolation 2026-01-27 13:28:40 +07:00
14 changed files with 752 additions and 718 deletions

View File

@@ -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);

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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},

View File

@@ -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');

View File

@@ -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

View 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;

View File

@@ -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://');

View File

@@ -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();

View File

@@ -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",

View File

@@ -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
})
};
}
}

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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",