Compare commits

..

3 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
Hoan Luu Huu
325af42946 speechmatics support end_of_utterance_silence_trigger (#1499)
* speechmatics support end_of_utterance_silence_trigger

* wip
2026-01-23 10:11:58 -05:00
8 changed files with 182 additions and 33 deletions

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

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

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

View File

@@ -1375,7 +1375,9 @@ module.exports = (logger) => {
speechmaticsOptions.transcription_config.audio_filtering_config.volume_threshold}),
...(speechmaticsOptions.transcription_config?.transcript_filtering_config?.remove_disfluencies &&
{SPEECHMATICS_REMOVE_DISFLUENCIES:
speechmaticsOptions.transcription_config.transcript_filtering_config.remove_disfluencies})
speechmaticsOptions.transcription_config.transcript_filtering_config.remove_disfluencies}),
SPEECHMATICS_END_OF_UTTERANCE_SILENCE_TRIGGER:
speechmaticsOptions.transcription_config?.conversation_config?.end_of_utterance_silence_trigger || 0.5
};
}
else if (vendor.startsWith('custom:')) {

75
package-lock.json generated
View File

@@ -42,7 +42,7 @@
"polly-ssml-split": "^0.1.0",
"sdp-transform": "^2.15.0",
"short-uuid": "^5.1.0",
"sinon": "^21.0.1",
"sinon": "^17.0.1",
"to-snake-case": "^1.0.0",
"undici": "^7.5.0",
"verify-aws-sns-signature": "^0.1.0",
@@ -2721,9 +2721,9 @@
}
},
"node_modules/@sinonjs/fake-timers": {
"version": "15.1.0",
"resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.1.0.tgz",
"integrity": "sha512-cqfapCxwTGsrR80FEgOoPsTonoefMBY7dnUEbQ+GRcved0jvkJLzvX6F4WtN+HBqbPX/SiFsIRUp+IrCW/2I2w==",
"version": "11.3.1",
"resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.3.1.tgz",
"integrity": "sha512-EVJO7nW5M/F5Tur0Rf2z/QoMo+1Ia963RiMtapiQrEWvY0iBUvADo8Beegwjpnle5BHkyHuoxSTW3jF43H1XRA==",
"license": "BSD-3-Clause",
"dependencies": {
"@sinonjs/commons": "^3.0.1"
@@ -2739,6 +2739,12 @@
"type-detect": "^4.1.0"
}
},
"node_modules/@sinonjs/text-encoding": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz",
"integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==",
"license": "(Unlicense OR Apache-2.0)"
},
"node_modules/@smithy/abort-controller": {
"version": "4.2.5",
"resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.5.tgz",
@@ -4606,9 +4612,9 @@
}
},
"node_modules/diff": {
"version": "8.0.3",
"resolved": "https://registry.npmjs.org/diff/-/diff-8.0.3.tgz",
"integrity": "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==",
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz",
"integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.3.1"
@@ -7742,6 +7748,12 @@
"verror": "1.10.0"
}
},
"node_modules/just-extend": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz",
"integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==",
"license": "MIT"
},
"node_modules/jwa": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
@@ -8036,21 +8048,6 @@
"node": ">= 6.0.0"
}
},
"node_modules/microsoft-cognitiveservices-speech-sdk/node_modules/utf-8-validate": {
"version": "5.0.10",
"resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz",
"integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"node-gyp-build": "^4.3.0"
},
"engines": {
"node": ">=6.14.2"
}
},
"node_modules/microsoft-cognitiveservices-speech-sdk/node_modules/uuid": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
@@ -8276,6 +8273,25 @@
"integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==",
"license": "ISC"
},
"node_modules/nise": {
"version": "5.1.9",
"resolved": "https://registry.npmjs.org/nise/-/nise-5.1.9.tgz",
"integrity": "sha512-qOnoujW4SV6e40dYxJOb3uvuoPHtmLzIk4TFo+j0jPJoC+5Z9xja5qH5JZobEPsa8+YYphMrOSwnrshEhG2qww==",
"license": "BSD-3-Clause",
"dependencies": {
"@sinonjs/commons": "^3.0.0",
"@sinonjs/fake-timers": "^11.2.2",
"@sinonjs/text-encoding": "^0.7.2",
"just-extend": "^6.2.0",
"path-to-regexp": "^6.2.1"
}
},
"node_modules/nise/node_modules/path-to-regexp": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz",
"integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==",
"license": "MIT"
},
"node_modules/no-case": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz",
@@ -9975,15 +9991,16 @@
"license": "ISC"
},
"node_modules/sinon": {
"version": "21.0.1",
"resolved": "https://registry.npmjs.org/sinon/-/sinon-21.0.1.tgz",
"integrity": "sha512-Z0NVCW45W8Mg5oC/27/+fCqIHFnW8kpkFOq0j9XJIev4Ld0mKmERaZv5DMLAb9fGCevjKwaEeIQz5+MBXfZcDw==",
"version": "17.0.1",
"resolved": "https://registry.npmjs.org/sinon/-/sinon-17.0.1.tgz",
"integrity": "sha512-wmwE19Lie0MLT+ZYNpDymasPHUKTaZHUH/pKEubRXIzySv9Atnlw+BUMGCzWgV7b7wO+Hw6f1TEOr0IUnmU8/g==",
"license": "BSD-3-Clause",
"dependencies": {
"@sinonjs/commons": "^3.0.1",
"@sinonjs/fake-timers": "^15.1.0",
"@sinonjs/samsam": "^8.0.3",
"diff": "^8.0.2",
"@sinonjs/commons": "^3.0.0",
"@sinonjs/fake-timers": "^11.2.2",
"@sinonjs/samsam": "^8.0.0",
"diff": "^5.1.0",
"nise": "^5.1.5",
"supports-color": "^7.2.0"
},
"funding": {

View File

@@ -58,7 +58,7 @@
"polly-ssml-split": "^0.1.0",
"sdp-transform": "^2.15.0",
"short-uuid": "^5.1.0",
"sinon": "^21.0.1",
"sinon": "^17.0.1",
"to-snake-case": "^1.0.0",
"undici": "^7.5.0",
"verify-aws-sns-signature": "^0.1.0",