mirror of
https://github.com/jambonz/jambonz-feature-server.git
synced 2026-02-11 00:39:56 +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);
|
return this._lccDub(opts.dub, callSid);
|
||||||
}
|
}
|
||||||
else if (opts.boostAudioSignal) {
|
else if (opts.boostAudioSignal) {
|
||||||
return this._lccBoostAudioSignal(opts.boostAudioSignal, callSid);
|
return this._lccBoostAudioSignal(opts, callSid);
|
||||||
}
|
}
|
||||||
else if (opts.media_path) {
|
else if (opts.media_path) {
|
||||||
return this._lccMediaPath(opts.media_path, callSid);
|
return this._lccMediaPath(opts.media_path, callSid);
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ class TaskConfig extends Task {
|
|||||||
'vad',
|
'vad',
|
||||||
'ttsStream',
|
'ttsStream',
|
||||||
'autoStreamTts',
|
'autoStreamTts',
|
||||||
'disableTtsCache'
|
'disableTtsCache',
|
||||||
|
'noiseIsolation'
|
||||||
].forEach((k) => this[k] = this.data[k] || {});
|
].forEach((k) => this[k] = this.data[k] || {});
|
||||||
|
|
||||||
if ('notifyEvents' in this.data) {
|
if ('notifyEvents' in this.data) {
|
||||||
@@ -90,6 +91,7 @@ class TaskConfig extends Task {
|
|||||||
get hasNotifySttLatency() { return Object.keys(this.data).includes('notifySttLatency'); }
|
get hasNotifySttLatency() { return Object.keys(this.data).includes('notifySttLatency'); }
|
||||||
get hasTtsStream() { return Object.keys(this.ttsStream).length; }
|
get hasTtsStream() { return Object.keys(this.ttsStream).length; }
|
||||||
get hasDisableTtsCache() { return Object.keys(this.data).includes('disableTtsCache'); }
|
get hasDisableTtsCache() { return Object.keys(this.data).includes('disableTtsCache'); }
|
||||||
|
get hasNoiseIsolation() { return Object.keys(this.data).includes('noiseIsolation'); }
|
||||||
|
|
||||||
get summary() {
|
get summary() {
|
||||||
const phrase = [];
|
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 ('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.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(',')}}`;
|
return `${this.name}{${phrase.join(',')}}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,6 +368,17 @@ class TaskConfig extends Task {
|
|||||||
this.logger.info(`set disableTtsCache = ${this.disableTtsCache}`);
|
this.logger.info(`set disableTtsCache = ${this.disableTtsCache}`);
|
||||||
cs.disableTtsCache = this.data.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) {
|
async kill(cs) {
|
||||||
|
|||||||
@@ -195,9 +195,6 @@ class TaskDial extends Task {
|
|||||||
async exec(cs) {
|
async exec(cs) {
|
||||||
await super.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) {
|
if (this.data.anchorMedia && this.data.exitMediaPath) {
|
||||||
this.logger.info('Dial:exec - incompatible anchorMedia and exitMediaPath are both set, will obey anchorMedia');
|
this.logger.info('Dial:exec - incompatible anchorMedia and exitMediaPath are both set, will obey anchorMedia');
|
||||||
delete this.data.exitMediaPath;
|
delete this.data.exitMediaPath;
|
||||||
@@ -553,7 +550,7 @@ class TaskDial extends Task {
|
|||||||
let sbcAddress = this.proxy || getSBC();
|
let sbcAddress = this.proxy || getSBC();
|
||||||
const teamsInfo = {};
|
const teamsInfo = {};
|
||||||
let fqdn;
|
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');
|
this.logger.debug(forwardPAI, 'forwardPAI value');
|
||||||
if (!sbcAddress) throw new Error('no SBC found for outbound call');
|
if (!sbcAddress) throw new Error('no SBC found for outbound call');
|
||||||
this.headers = {
|
this.headers = {
|
||||||
@@ -875,12 +872,8 @@ class TaskDial extends Task {
|
|||||||
this.sd = sd;
|
this.sd = sd;
|
||||||
this.callSid = sd.callSid;
|
this.callSid = sd.callSid;
|
||||||
if (this.earlyMedia) {
|
if (this.earlyMedia) {
|
||||||
if (this._aLegAlreadyAnswered) {
|
debug('Dial:_selectSingleDial propagating answer supervision on A leg now that B is connected');
|
||||||
debug('Dial:_selectSingleDial A leg was already answered, skipping propagateAnswer');
|
await cs.propagateAnswer();
|
||||||
} else {
|
|
||||||
debug('Dial:_selectSingleDial propagating answer supervision on A leg now that B is connected');
|
|
||||||
await cs.propagateAnswer();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (this.timeLimit) {
|
if (this.timeLimit) {
|
||||||
this.timerMaxCallDuration = setTimeout(this._onMaxCallDuration.bind(this, cs), this.timeLimit * 1000);
|
this.timerMaxCallDuration = setTimeout(this._onMaxCallDuration.bind(this, cs), this.timeLimit * 1000);
|
||||||
|
|||||||
@@ -152,17 +152,9 @@ class TaskListen extends Task {
|
|||||||
|
|
||||||
async _startListening(cs, ep) {
|
async _startListening(cs, ep) {
|
||||||
this._initListeners(ep);
|
this._initListeners(ep);
|
||||||
const tempci = this.nested ? this.parentTask.sd.callInfo : cs.callInfo.toJSON();
|
const ci = this.nested ? this.parentTask.sd.callInfo : cs.callInfo.toJSON();
|
||||||
const ci = structuredClone(tempci);
|
|
||||||
if (this._ignoreCustomerData) {
|
if (this._ignoreCustomerData) {
|
||||||
delete ci.customerData;
|
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(
|
const metadata = Object.assign(
|
||||||
{sampleRate: this.sampleRate, mixType: this.mixType},
|
{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.model = this.parent.model || 'models/gemini-2.0-flash-live-001';
|
||||||
this.auth = this.parent.auth;
|
this.auth = this.parent.auth;
|
||||||
this.connectionOptions = this.parent.connectOptions;
|
this.connectionOptions = this.parent.connectOptions;
|
||||||
const {host, version} = this.connectionOptions || {};
|
|
||||||
this.host = host;
|
|
||||||
this.version = version;
|
|
||||||
|
|
||||||
const {apiKey} = this.auth || {};
|
const {apiKey} = this.auth || {};
|
||||||
if (!apiKey) throw new Error('auth.apiKey is required for Google S2S');
|
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.eventHook = this.data.eventHook;
|
||||||
this.toolHook = this.data.toolHook;
|
this.toolHook = this.data.toolHook;
|
||||||
|
|
||||||
const {setup, sessionResumption} = this.data.llmOptions;
|
const {setup} = this.data.llmOptions;
|
||||||
|
|
||||||
if (typeof setup !== 'object') {
|
if (typeof setup !== 'object') {
|
||||||
throw new Error('llmOptions with an initial setup is required for Google S2S');
|
throw new Error('llmOptions with an initial setup is required for Google S2S');
|
||||||
@@ -57,7 +54,6 @@ class TaskLlmGoogle_S2S extends Task {
|
|||||||
this.setup = {
|
this.setup = {
|
||||||
...setup,
|
...setup,
|
||||||
model: this.model,
|
model: this.model,
|
||||||
...(sessionResumption && {sessionResumption}),
|
|
||||||
// make sure output is always audio
|
// make sure output is always audio
|
||||||
generationConfig: {
|
generationConfig: {
|
||||||
...(setup.generationConfig || {}),
|
...(setup.generationConfig || {}),
|
||||||
@@ -142,10 +138,6 @@ class TaskLlmGoogle_S2S extends Task {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const args = [ep.uuid, 'session.create', this.apiKey];
|
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);
|
await this._api(ep, args);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.error({err}, 'TaskLlmGoogle_S2S:_startListening');
|
this.logger.error({err}, 'TaskLlmGoogle_S2S:_startListening');
|
||||||
|
|||||||
@@ -99,6 +99,9 @@ function makeTask(logger, obj, parent) {
|
|||||||
case TaskName.Alert:
|
case TaskName.Alert:
|
||||||
const TaskAlert = require('./alert');
|
const TaskAlert = require('./alert');
|
||||||
return new TaskAlert(logger, data, parent);
|
return new TaskAlert(logger, data, parent);
|
||||||
|
case TaskName.NoiseIsolation:
|
||||||
|
const TaskNoiseIsolation = require('./noise-isolation');
|
||||||
|
return new TaskNoiseIsolation(logger, data, parent);
|
||||||
}
|
}
|
||||||
|
|
||||||
// should never reach
|
// 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})
|
...(reduceLatency && {RIMELABS_TTS_STREAMING_REDUCE_LATENCY: reduceLatency})
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
case 'google':
|
|
||||||
obj = {
|
|
||||||
GOOGLE_TTS_LANGUAGE_CODE: language,
|
|
||||||
GOOGLE_TTS_VOICE_NAME: voice,
|
|
||||||
GOOGLE_APPLICATION_CREDENTIALS: JSON.stringify(credentials.credentials)
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
if (vendor.startsWith('custom:')) {
|
if (vendor.startsWith('custom:')) {
|
||||||
const use_tls = custom_tts_streaming_url.startsWith('wss://');
|
const use_tls = custom_tts_streaming_url.startsWith('wss://');
|
||||||
|
|||||||
@@ -49,6 +49,9 @@ class BackgroundTaskManager extends Emitter {
|
|||||||
case 'ttsStream':
|
case 'ttsStream':
|
||||||
task = await this._initTtsStream(opts);
|
task = await this._initTtsStream(opts);
|
||||||
break;
|
break;
|
||||||
|
case 'noiseIsolation':
|
||||||
|
task = await this._initNoiseIsolation(opts);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -194,6 +197,25 @@ class BackgroundTaskManager extends Emitter {
|
|||||||
return task;
|
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) {
|
_taskCompleted(type, task) {
|
||||||
this.logger.debug({type, task}, `BackgroundTaskManager:_taskCompleted: task completed, sticky: ${task.sticky}`);
|
this.logger.debug({type, task}, `BackgroundTaskManager:_taskCompleted: task completed, sticky: ${task.sticky}`);
|
||||||
task.removeAllListeners();
|
task.removeAllListeners();
|
||||||
|
|||||||
@@ -31,7 +31,8 @@
|
|||||||
"SayLegacy": "say:legacy",
|
"SayLegacy": "say:legacy",
|
||||||
"Stream": "stream",
|
"Stream": "stream",
|
||||||
"Tag": "tag",
|
"Tag": "tag",
|
||||||
"Transcribe": "transcribe"
|
"Transcribe": "transcribe",
|
||||||
|
"NoiseIsolation": "noiseIsolation"
|
||||||
},
|
},
|
||||||
"AllowedSipRecVerbs": ["answer", "config", "gather", "transcribe", "listen", "tag", "hangup", "sip:decline"],
|
"AllowedSipRecVerbs": ["answer", "config", "gather", "transcribe", "listen", "tag", "hangup", "sip:decline"],
|
||||||
"AllowedConfirmSessionVerbs": ["config", "gather", "plays", "say", "tag"],
|
"AllowedConfirmSessionVerbs": ["config", "gather", "plays", "say", "tag"],
|
||||||
@@ -311,11 +312,6 @@
|
|||||||
"ConnectFailure": "deepgram_tts_streaming::connect_failed",
|
"ConnectFailure": "deepgram_tts_streaming::connect_failed",
|
||||||
"Connect": "deepgram_tts_streaming::connect"
|
"Connect": "deepgram_tts_streaming::connect"
|
||||||
},
|
},
|
||||||
"GoogleTtsStreamingEvents": {
|
|
||||||
"Empty": "google_tts_streaming::empty",
|
|
||||||
"ConnectFailure": "google_tts_streaming::connect_failed",
|
|
||||||
"Connect": "google_tts_streaming::connect"
|
|
||||||
},
|
|
||||||
"CartesiaTtsStreamingEvents": {
|
"CartesiaTtsStreamingEvents": {
|
||||||
"Empty": "cartesia_tts_streaming::empty",
|
"Empty": "cartesia_tts_streaming::empty",
|
||||||
"ConnectFailure": "cartesia_tts_streaming::connect_failed",
|
"ConnectFailure": "cartesia_tts_streaming::connect_failed",
|
||||||
|
|||||||
@@ -1310,9 +1310,6 @@ module.exports = (logger) => {
|
|||||||
...(openaiOptions.turn_detection.silence_duration_ms && {
|
...(openaiOptions.turn_detection.silence_duration_ms && {
|
||||||
OPENAI_TURN_DETECTION_SILENCE_DURATION_MS: 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',
|
'cartesia',
|
||||||
'elevenlabs',
|
'elevenlabs',
|
||||||
'rimelabs',
|
'rimelabs',
|
||||||
'google',
|
|
||||||
'custom'
|
'custom'
|
||||||
].forEach((vendor) => {
|
].forEach((vendor) => {
|
||||||
const eventClassName = `${vendor.charAt(0).toUpperCase() + vendor.slice(1)}TtsStreamingEvents`;
|
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",
|
"debug": "^4.3.4",
|
||||||
"deepcopy": "^2.1.0",
|
"deepcopy": "^2.1.0",
|
||||||
"drachtio-fsmrf": "^4.1.2",
|
"drachtio-fsmrf": "^4.1.2",
|
||||||
"drachtio-srf": "^5.0.18",
|
"drachtio-srf": "^5.0.14",
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
"express-validator": "^7.0.1",
|
"express-validator": "^7.0.1",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
|
|||||||
Reference in New Issue
Block a user