Compare commits

..

9 Commits

Author SHA1 Message Date
Quan HL
9b600178ff fix nuance onprem stt does not have data 2023-12-19 17:36:26 +07:00
Dave Horton
7d355f2fac enable azure stt through a proxy (#275) 2023-12-18 12:37:10 -05:00
Hoan Luu Huu
c6b8ec1b28 fix upload recording (#274) 2023-12-18 08:36:25 -05:00
Hoan Luu Huu
10159a0ba6 fix azure tts test from speech-utils (#272)
* fix azure tts test from speech-utils

* wip
2023-12-06 22:10:55 -05:00
Hoan Luu Huu
7a558c7349 update speech utils version (#270) 2023-12-05 20:48:19 -05:00
Hoan Luu Huu
4dbe7af9db fix list registered user does not have registered_status field (#268) 2023-12-05 07:44:43 -05:00
Hoan Luu Huu
4ec34faa29 support inband dtmf (#261)
* support inband dtmf

* support inband dtmf

* wip
2023-12-02 11:31:07 -05:00
Hoan Luu Huu
e2d6086f9f elevenlabs new model and options (#267) 2023-11-30 14:01:50 -05:00
Hoan Luu Huu
0e056ad296 support getting registered user details (#265)
* support getting registered user details

* add swager

* fix to use new registrar api

* assert sip_realm should be available for registered sip user query

* update mw registra version
2023-11-28 08:44:45 -05:00
10 changed files with 2458 additions and 57 deletions

2
app.js
View File

@@ -53,7 +53,7 @@ const {
getTtsSize,
purgeTtsCache,
synthAudio
} = require('@jambonz/speech-utils')({redis_client: client}, logger);
} = require('@jambonz/speech-utils')({}, logger);
const {
lookupAppBySid,
lookupAccountBySid,

View File

@@ -11,7 +11,7 @@ class S3MultipartUploadStream extends Writable {
super(opts);
this.logger = logger;
this.bucketName = opts.bucketName;
this.objectKey = opts.objectKey;
this.objectKey = opts.Key;
this.uploadId = null;
this.partNumber = 1;
this.multipartETags = [];

View File

@@ -5,7 +5,7 @@ const S3MultipartUploadStream = require('./s3-multipart-upload-stream');
const getUploader = (key, metadata, bucket_credential, logger) => {
const uploaderOpts = {
bucketName: bucket_credential.name,
objectKey: key,
Key: key,
metadata
};
try {

View File

@@ -155,6 +155,9 @@ router.get('/:sid/RegisteredSipUsers', async(req, res) => {
if (!result || result.length === 0) {
throw new DbErrorBadRequest(`account not found for sid ${account_sid}`);
}
if (!result[0].sip_realm) {
throw new DbErrorBadRequest('account does not have sip_realm configuration');
}
const users = await registrar.getRegisteredUsersForRealm(result[0].sip_realm);
res.status(200).json(users.map((u) => `${u}@${result[0].sip_realm}`));
} catch (err) {
@@ -162,6 +165,42 @@ router.get('/:sid/RegisteredSipUsers', async(req, res) => {
}
});
router.post('/:sid/RegisteredSipUsers', async(req, res) => {
const {logger, registrar} = req.app.locals;
const users = req.body;
try {
const account_sid = parseAccountSid(req);
await validateRequest(req, account_sid);
const result = await Account.retrieve(account_sid);
if (!result || result.length === 0) {
throw new DbErrorBadRequest(`account not found for sid ${account_sid}`);
}
if (!result[0].sip_realm) {
throw new DbErrorBadRequest('account does not have sip_realm configuration');
}
if (!users || !Array.isArray(users) || users.length === 0) {
return res.status(200).json(await registrar.getRegisteredUsersDetailsForRealm(result[0].sip_realm));
}
const ret = [];
for (const u of users) {
const user = await registrar.query(`${u}@${result[0].sip_realm}`) || {
name: u,
contact: null,
expiryTime: 0,
protocol: null
};
ret.push({
name: u,
...user,
registered_status: user.expiryTime > 0 ? 'active' : 'inactive',
});
}
res.status(200).json(ret);
} catch (err) {
sysError(logger, res, err);
}
});
router.get('/:sid/RegisteredSipUsers/:client', async(req, res) => {
const {logger, registrar, lookupClientByAccountAndUsername} = req.app.locals;
const client = req.params.client;
@@ -225,7 +264,8 @@ function validateUpdateCall(opts) {
'mute_status',
'sip_request',
'record',
'tag'
'tag',
'dtmf'
]
.reduce((acc, prop) => (opts[prop] ? ++acc : acc), 0);
@@ -267,6 +307,9 @@ function validateUpdateCall(opts) {
if (opts.record && !opts.record.action) {
throw new DbErrorBadRequest('record requires action property');
}
if (opts.dtmf && !opts.dtmf.digit) {
throw new DbErrorBadRequest('invalid dtmf');
}
if ('startCallRecording' === opts.record?.action && !opts.record.siprecServerURL) {
throw new DbErrorBadRequest('record requires siprecServerURL property when starting recording');
}

View File

@@ -132,7 +132,8 @@ const encryptCredential = (obj) => {
custom_tts_url,
auth_token = '',
cobalt_server_uri,
model_id
model_id,
options
} = obj;
switch (vendor) {
@@ -210,7 +211,7 @@ const encryptCredential = (obj) => {
case 'elevenlabs':
assert(api_key, 'invalid elevenLabs speech credential: api_key is required');
assert(model_id, 'invalid elevenLabs speech credential: model_id is required');
const elevenlabsData = JSON.stringify({api_key, model_id});
const elevenlabsData = JSON.stringify({api_key, model_id, options});
return encrypt(elevenlabsData);
case 'assemblyai':
@@ -411,7 +412,8 @@ router.put('/:sid', async(req, res) => {
custom_stt_url,
custom_tts_url,
cobalt_server_uri,
model_id
model_id,
options
} = req.body;
const newCred = {
@@ -433,7 +435,8 @@ router.put('/:sid', async(req, res) => {
custom_stt_url,
custom_tts_url,
cobalt_server_uri,
model_id
model_id,
options
};
logger.info({o, newCred}, 'updating speech credential with this new credential');
obj.credential = encryptCredential(newCred);
@@ -553,7 +556,7 @@ router.get('/:sid/test', async(req, res) => {
} = credential;
if (cred.use_for_tts) {
try {
await testMicrosoftTts(logger, {
await testMicrosoftTts(logger, synthAudio, {
api_key,
region,
use_custom_tts,

View File

@@ -4192,6 +4192,27 @@ paths:
type: array
items:
type: string
post:
tags:
- Accounts
summary: retrieve online sip users for an account by list of sip username
operationId: listRegisteredSipUsers
requestBody:
content:
application/json:
schema:
type: array
items:
type: string
responses:
200:
description: retrieve online sip users for an account
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/RegisteredClient'
/Accounts/{AccountSid}/RegisteredSipUsers/{Client}:
parameters:
- name: Client

View File

@@ -95,6 +95,13 @@ const testMicrosoftStt = async(logger, credentials) => {
const speechConfig = sdk.SpeechConfig.fromSubscription(api_key, region);
const audioConfig = sdk.AudioConfig.fromWavFileInput(fs.readFileSync(`${__dirname}/../../data/test_audio.wav`));
speechConfig.speechRecognitionLanguage = 'en-US';
if (process.env.JAMBONES_HTTP_PROXY_IP && process.env.JAMBONES_HTTP_PROXY_PORT) {
logger.debug(
`testMicrosoftStt: using proxy ${process.env.JAMBONES_HTTP_PROXY_IP}:${process.env.JAMBONES_HTTP_PROXY_PORT}`);
speechConfig.setProxy(process.env.JAMBONES_HTTP_PROXY_IP, process.env.JAMBONES_HTTP_PROXY_PORT);
}
const speechRecognizer = new sdk.SpeechRecognizer(speechConfig, audioConfig);
return new Promise((resolve, reject) => {
@@ -150,38 +157,19 @@ const testAwsStt = async(logger, credentials) => {
}
};
const testMicrosoftTts = async(logger, credentials) => {
const {
api_key,
region,
// eslint-disable-next-line no-unused-vars
use_custom_tts,
// eslint-disable-next-line no-unused-vars
custom_tts_endpoint,
// eslint-disable-next-line no-unused-vars
use_custom_stt,
// eslint-disable-next-line no-unused-vars
custom_stt_endpoint
} = credentials;
logger.info({
api_key,
region,
use_custom_tts,
custom_tts_endpoint,
use_custom_stt,
custom_stt_endpoint
}, 'testing microsoft tts');
if (!api_key) throw new Error('testMicrosoftTts: credentials are missing api_key');
if (!region) throw new Error('testMicrosoftTts: credentials are missing region');
const testMicrosoftTts = async(logger, synthAudio, credentials) => {
try {
const getJSON = bent('json', {
'Ocp-Apim-Subscription-Key': api_key
});
const response = await getJSON(`https://${region}.tts.speech.microsoft.com/cognitiveservices/voices/list`);
return response;
await synthAudio({increment: () => {}, histogram: () => {}},
{
vendor: 'microsoft',
credentials,
language: 'en-US',
voice: 'en-US-JennyMultilingualNeural',
text: 'Hi there and welcome to jambones!'
}
);
} catch (err) {
logger.info({err}, `testMicrosoftTts - failed to list voices for region ${region}`);
logger.info({err}, 'testMicrosoftTts returned error');
throw err;
}
};
@@ -370,6 +358,8 @@ function decryptCredential(obj, credential, logger, isObscureKey = true) {
const o = JSON.parse(decrypt(credential));
obj.client_id = o.client_id;
obj.secret = o.secret ? (isObscureKey ? obscureKey(o.secret) : o.secret) : null;
obj.nuance_tts_uri = o.nuance_tts_uri;
obj.nuance_stt_uri = o.nuance_stt_uri;
}
else if ('deepgram' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
@@ -395,6 +385,7 @@ function decryptCredential(obj, credential, logger, isObscureKey = true) {
const o = JSON.parse(decrypt(credential));
obj.api_key = isObscureKey ? obscureKey(o.api_key) : o.api_key;
obj.model_id = o.model_id;
obj.options = o.options;
} else if (obj.vendor.startsWith('custom:')) {
const o = JSON.parse(decrypt(credential));
obj.auth_token = isObscureKey ? obscureKey(o.auth_token) : o.auth_token;

2357
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -27,9 +27,9 @@
"@google-cloud/storage": "^6.12.0",
"@jambonz/db-helpers": "^0.9.1",
"@jambonz/lamejs": "^1.2.2",
"@jambonz/mw-registrar": "^0.2.5",
"@jambonz/mw-registrar": "^0.2.7",
"@jambonz/realtimedb-helpers": "^0.8.7",
"@jambonz/speech-utils": "^0.0.26",
"@jambonz/speech-utils": "^0.0.32",
"@jambonz/time-series": "^0.2.8",
"@jambonz/verb-specifications": "^0.0.45",
"@soniox/soniox-node": "^1.1.1",

View File

@@ -85,6 +85,24 @@ test('client test', async(t) => {
t.ok(result.length === 1 && result[0] === 'dhorton@drachtio.org',
'successfully queried all registered clients');
result = await request.post(`/Accounts/${account_sid}/RegisteredSipUsers`, {
resolveWithFullResponse: true,
auth: authAdmin,
json: true,
body: ['dhorton']
});
t.ok(result.body.length === 1 && result.body[0].name === 'dhorton',
'successfully queried all registered clients');
result = await request.post(`/Accounts/${account_sid}/RegisteredSipUsers`, {
resolveWithFullResponse: true,
auth: authAdmin,
json: true,
body: []
});
t.ok(result.body.length === 1 && result.body[0].name === 'dhorton',
'successfully queried all registered clients');
/* query all entity */
result = await request.get('/Clients', {
auth: authAdmin,