mirror of
https://github.com/jambonz/jambonz-api-server.git
synced 2026-01-25 02:08:24 +00:00
Compare commits
25 Commits
v0.9.4-rc3
...
v0.9.5-rc6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
035458ad3c | ||
|
|
fd9dc77a58 | ||
|
|
2b66a121a0 | ||
|
|
3a6d10e725 | ||
|
|
6f87204d88 | ||
|
|
9854666d4f | ||
|
|
0d4b7e88ad | ||
|
|
819319dbe5 | ||
|
|
0ba69e872b | ||
|
|
9b4f1b67bf | ||
|
|
542ccfca79 | ||
|
|
5421f1421f | ||
|
|
0842793aea | ||
|
|
781179bf0e | ||
|
|
1532a4ab9c | ||
|
|
5fd89b1d65 | ||
|
|
e2fc0216e1 | ||
|
|
fcff3d4b32 | ||
|
|
2dd06df641 | ||
|
|
579a586a03 | ||
|
|
3e1b383284 | ||
|
|
c51b7bab82 | ||
|
|
bb5dba7c20 | ||
|
|
c7e279d0ee | ||
|
|
6700ff35be |
6
app.js
6
app.js
@@ -48,7 +48,8 @@ const {
|
||||
retrieveKey,
|
||||
deleteKey,
|
||||
incrKey,
|
||||
listConferences
|
||||
listConferences,
|
||||
getCallCount
|
||||
} = require('./lib/helpers/realtimedb-helpers');
|
||||
const {
|
||||
getTtsVoices,
|
||||
@@ -118,7 +119,8 @@ app.locals = {
|
||||
queryAlertsSP,
|
||||
writeCdrs,
|
||||
writeAlerts,
|
||||
AlertType
|
||||
AlertType,
|
||||
getCallCount
|
||||
};
|
||||
|
||||
const unless = (paths, middleware) => {
|
||||
|
||||
@@ -503,7 +503,7 @@ messaging_hook_sid CHAR(36) COMMENT 'webhook to call for inbound SMS/MMS ',
|
||||
app_json TEXT,
|
||||
speech_synthesis_vendor VARCHAR(64) NOT NULL DEFAULT 'google',
|
||||
speech_synthesis_language VARCHAR(12) NOT NULL DEFAULT 'en-US',
|
||||
speech_synthesis_voice VARCHAR(256),
|
||||
speech_synthesis_voice VARCHAR(256) DEFAULT 'en-US-Standard-C',
|
||||
speech_synthesis_label VARCHAR(64),
|
||||
speech_recognizer_vendor VARCHAR(64) NOT NULL DEFAULT 'google',
|
||||
speech_recognizer_language VARCHAR(64) NOT NULL DEFAULT 'en-US',
|
||||
|
||||
@@ -228,6 +228,10 @@ const sql = {
|
||||
],
|
||||
9004: [
|
||||
'ALTER TABLE applications ADD COLUMN env_vars TEXT',
|
||||
],
|
||||
9005: [
|
||||
'UPDATE applications SET speech_synthesis_voice = \'en-US-Standard-C\' WHERE speech_synthesis_voice IS NULL AND speech_synthesis_vendor = \'google\' AND speech_synthesis_language = \'en-US\'',
|
||||
'ALTER TABLE applications MODIFY COLUMN speech_synthesis_voice VARCHAR(255) DEFAULT \'en-US-Standard-C\''
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
@@ -13,7 +13,8 @@ const {
|
||||
deleteKey,
|
||||
incrKey,
|
||||
client: redisClient,
|
||||
listConferences
|
||||
listConferences,
|
||||
getCallCount
|
||||
} = require('@jambonz/realtimedb-helpers')({}, logger);
|
||||
|
||||
module.exports = {
|
||||
@@ -29,5 +30,6 @@ module.exports = {
|
||||
deleteKey,
|
||||
redisClient,
|
||||
incrKey,
|
||||
listConferences
|
||||
listConferences,
|
||||
getCallCount
|
||||
};
|
||||
|
||||
@@ -2,6 +2,6 @@ const opts = {
|
||||
level: process.env.JAMBONES_LOGLEVEL || 'info'
|
||||
};
|
||||
const pino = require('pino');
|
||||
const logger = pino(opts, pino.destination(1, {sync: false}));
|
||||
const logger = pino(opts);
|
||||
|
||||
module.exports = logger;
|
||||
|
||||
@@ -36,25 +36,98 @@ class Application extends Model {
|
||||
super();
|
||||
}
|
||||
|
||||
static _criteriaBuilder(obj, args) {
|
||||
let sql = '';
|
||||
if (obj.account_sid) {
|
||||
sql += ' AND app.account_sid = ?';
|
||||
args.push(obj.account_sid);
|
||||
}
|
||||
if (obj.service_provider_sid) {
|
||||
sql += ' AND app.account_sid in (SELECT account_sid from accounts WHERE service_provider_sid = ?)';
|
||||
args.push(obj.service_provider_sid);
|
||||
}
|
||||
if (obj.name) {
|
||||
sql += ' AND app.name LIKE ?';
|
||||
args.push(`%${obj.name}%`);
|
||||
}
|
||||
return sql;
|
||||
}
|
||||
|
||||
static countAll(obj) {
|
||||
let sql = 'SELECT COUNT(*) AS count FROM applications app WHERE 1 = 1';
|
||||
const args = [];
|
||||
sql += Application._criteriaBuilder(obj, args);
|
||||
return new Promise((resolve, reject) => {
|
||||
getMysqlConnection((err, conn) => {
|
||||
if (err) return reject(err);
|
||||
conn.query({sql}, args, (err, results) => {
|
||||
conn.release();
|
||||
if (err) return reject(err);
|
||||
resolve(results[0].count);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* list all applications - for all service providers, for one service provider, or for one account,
|
||||
* or by an optional name
|
||||
*/
|
||||
static retrieveAll(service_provider_sid, account_sid, name) {
|
||||
static retrieveAll(obj) {
|
||||
const { page, page_size = 50 } = obj || {};
|
||||
|
||||
// If pagination is requested, first get the application IDs
|
||||
if (page !== null && page !== undefined) {
|
||||
let idSql = 'SELECT application_sid, name FROM applications app WHERE 1 = 1';
|
||||
const idArgs = [];
|
||||
idSql += Application._criteriaBuilder(obj, idArgs);
|
||||
idSql += ' ORDER BY app.name';
|
||||
|
||||
const limit = Number(page_size);
|
||||
const offset = Number(page > 0 ? (page - 1) : page) * limit;
|
||||
idSql += ' LIMIT ? OFFSET ?';
|
||||
idArgs.push(limit);
|
||||
idArgs.push(offset);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
getMysqlConnection((err, conn) => {
|
||||
if (err) return reject(err);
|
||||
|
||||
// Get paginated application IDs
|
||||
conn.query(idSql, idArgs, (err, idResults) => {
|
||||
if (err) {
|
||||
conn.release();
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
if (idResults.length === 0) {
|
||||
conn.release();
|
||||
return resolve([]);
|
||||
}
|
||||
|
||||
// Get full data for these applications
|
||||
const appIds = idResults.map((row) => row.application_sid);
|
||||
const placeholders = appIds.map(() => '?').join(',');
|
||||
const fullSql = `${retrieveSql}
|
||||
WHERE app.application_sid IN (${placeholders}) ORDER BY app.name`;
|
||||
|
||||
conn.query({sql: fullSql, nestTables: true}, appIds, (err, results) => {
|
||||
conn.release();
|
||||
if (err) return reject(err);
|
||||
const r = transmogrifyResults(results);
|
||||
resolve(r);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// No pagination - use original query
|
||||
let sql = retrieveSql + ' WHERE 1 = 1';
|
||||
const args = [];
|
||||
if (account_sid) {
|
||||
sql = `${sql} AND app.account_sid = ?`;
|
||||
args.push(account_sid);
|
||||
}
|
||||
else if (service_provider_sid) {
|
||||
sql = `${sql} AND account_sid in (SELECT account_sid from accounts WHERE service_provider_sid = ?)`;
|
||||
args.push(service_provider_sid);
|
||||
}
|
||||
if (name) {
|
||||
sql = `${sql} AND app.name = ?`;
|
||||
args.push(name);
|
||||
}
|
||||
sql += Application._criteriaBuilder(obj, args);
|
||||
sql += ' ORDER BY app.application_sid';
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
getMysqlConnection((err, conn) => {
|
||||
if (err) return reject(err);
|
||||
|
||||
@@ -26,24 +26,45 @@ class PhoneNumber extends Model {
|
||||
return rows;
|
||||
}
|
||||
|
||||
static async retrieveAllByCriteria({
|
||||
service_provider_sid, account_sid, filter
|
||||
}) {
|
||||
static _criteriaBuilder(obj, params) {
|
||||
let sql = '';
|
||||
if (obj.service_provider_sid) {
|
||||
sql += ' AND account_sid IN (SELECT account_sid FROM accounts WHERE service_provider_sid = ?)';
|
||||
params.push(obj.service_provider_sid);
|
||||
}
|
||||
if (obj.account_sid) {
|
||||
sql += ' AND account_sid = ?';
|
||||
params.push(obj.account_sid);
|
||||
}
|
||||
if (obj.filter) {
|
||||
sql += ' AND number LIKE ?';
|
||||
params.push(`%${obj.filter}%`);
|
||||
}
|
||||
|
||||
return sql;
|
||||
}
|
||||
|
||||
static async countAll(obj) {
|
||||
let sql = 'SELECT COUNT(*) AS count FROM phone_numbers WHERE 1 = 1';
|
||||
const args = [];
|
||||
sql += PhoneNumber._criteriaBuilder(obj, args);
|
||||
const [rows] = await promisePool.query(sql, args);
|
||||
return rows[0].count;
|
||||
}
|
||||
|
||||
static async retrieveAllByCriteria(obj) {
|
||||
let sql = 'SELECT * FROM phone_numbers WHERE 1=1';
|
||||
const params = [];
|
||||
if (service_provider_sid) {
|
||||
sql += ' AND account_sid IN (SELECT account_sid FROM accounts WHERE service_provider_sid = ?)';
|
||||
params.push(service_provider_sid);
|
||||
}
|
||||
if (account_sid) {
|
||||
sql += ' AND account_sid = ?';
|
||||
params.push(account_sid);
|
||||
}
|
||||
if (filter) {
|
||||
sql += ' AND number LIKE ?';
|
||||
params.push(`%${filter}%`);
|
||||
}
|
||||
const { page, page_size = 50 } = obj || {};
|
||||
sql += PhoneNumber._criteriaBuilder(obj, params);
|
||||
sql += ' ORDER BY number';
|
||||
if (page !== null && page !== undefined) {
|
||||
const limit = Number(page_size);
|
||||
const offset = Number(page > 0 ? (page - 1) : page) * limit;
|
||||
sql += ' LIMIT ? OFFSET ?';
|
||||
params.push(limit);
|
||||
params.push(offset);
|
||||
}
|
||||
const [rows] = await promisePool.query(sql, params);
|
||||
return rows;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,57 @@ class VoipCarrier extends Model {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
static _criteriaBuilder(obj, args) {
|
||||
let sql = '';
|
||||
if (obj.account_sid) {
|
||||
// carrier belong to an account when
|
||||
// 1. account_sid is set
|
||||
// 2. account_sid is null and service_provider_sid matches the account's service_provider_sid
|
||||
sql += ` AND (vc.account_sid = ? OR
|
||||
(vc.account_sid IS NULL AND vc.service_provider_sid IN
|
||||
(SELECT service_provider_sid FROM accounts WHERE account_sid = ?))
|
||||
)`;
|
||||
args.push(obj.account_sid);
|
||||
args.push(obj.account_sid);
|
||||
}
|
||||
if (obj.service_provider_sid) {
|
||||
sql += ' AND vc.service_provider_sid = ?';
|
||||
args.push(obj.service_provider_sid);
|
||||
}
|
||||
if (obj.name) {
|
||||
sql += ' AND vc.name LIKE ?';
|
||||
args.push(`%${obj.name}%`);
|
||||
}
|
||||
|
||||
return sql;
|
||||
}
|
||||
|
||||
static async countAll(obj) {
|
||||
let sql = 'SELECT COUNT(*) AS count FROM voip_carriers vc WHERE 1 = 1';
|
||||
const args = [];
|
||||
sql += VoipCarrier._criteriaBuilder(obj, args);
|
||||
const [rows] = await promisePool.query(sql, args);
|
||||
return rows[0].count;
|
||||
}
|
||||
|
||||
static async retrieveByCriteria(obj) {
|
||||
let sql = 'SELECT * from voip_carriers vc WHERE 1 =1';
|
||||
const args = [];
|
||||
sql += VoipCarrier._criteriaBuilder(obj, args);
|
||||
if (obj.page !== null && obj.page !== undefined) {
|
||||
const limit = Number(obj.page_size || 50);
|
||||
const offset = (Number(obj.page) - 1) * limit;
|
||||
sql += ' LIMIT ? OFFSET ?';
|
||||
args.push(limit, offset);
|
||||
}
|
||||
const [rows] = await promisePool.query(sql, args);
|
||||
if (rows) {
|
||||
rows.map((r) => r.register_status = JSON.parse(r.register_status || '{}'));
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
static async retrieveAll(account_sid) {
|
||||
if (!account_sid) return super.retrieveAll();
|
||||
const [rows] = await promisePool.query(retrieveSql, account_sid);
|
||||
|
||||
@@ -26,7 +26,7 @@ const getUploader = (key, metadata, bucket_credential, logger) => {
|
||||
accessKeyId: bucket_credential.access_key_id,
|
||||
secretAccessKey: bucket_credential.secret_access_key,
|
||||
},
|
||||
region: 'us-east-1',
|
||||
region: bucket_credential.region || 'us-east-1',
|
||||
forcePathStyle: true
|
||||
};
|
||||
return new S3MultipartUploadStream(logger, uploaderOpts);
|
||||
|
||||
@@ -23,7 +23,8 @@ const {
|
||||
} = require('./utils');
|
||||
const short = require('short-uuid');
|
||||
const VoipCarrier = require('../../models/voip-carrier');
|
||||
const { encrypt, obscureBucketCredentialsSensitiveData, isObscureKey } = require('../../utils/encrypt-decrypt');
|
||||
const { encrypt, obscureBucketCredentialsSensitiveData,
|
||||
isObscureKey, decrypt } = require('../../utils/encrypt-decrypt');
|
||||
const { testS3Storage, testGoogleStorage, testAzureStorage } = require('../../utils/storage-utils');
|
||||
const translator = short();
|
||||
|
||||
@@ -92,8 +93,34 @@ router.get('/:sid/Applications', async(req, res) => {
|
||||
try {
|
||||
const account_sid = parseAccountSid(req);
|
||||
await validateRequest(req, account_sid);
|
||||
const results = await Application.retrieveAll(null, account_sid);
|
||||
res.status(200).json(results);
|
||||
const {page, page_size, name} = req.query || {};
|
||||
const isPaginationRequest = page !== null && page !== undefined;
|
||||
let results = [];
|
||||
let total = 0;
|
||||
if (isPaginationRequest) {
|
||||
total = await Application.countAll({account_sid, name});
|
||||
results = await Application.retrieveAll({
|
||||
account_sid, name, page, page_size
|
||||
});
|
||||
} else {
|
||||
results = await Application.retrieveAll({account_sid});
|
||||
}
|
||||
const ret = results.map((a) => {
|
||||
if (a.env_vars) {
|
||||
a.env_vars = JSON.parse(decrypt(a.env_vars));
|
||||
return a;
|
||||
} else {
|
||||
return a;
|
||||
}
|
||||
});
|
||||
|
||||
const body = isPaginationRequest ? {
|
||||
total,
|
||||
page: Number(page),
|
||||
page_size: Number(page_size),
|
||||
data: ret
|
||||
} : ret;
|
||||
res.status(200).json(body);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
@@ -221,7 +248,8 @@ router.get('/:sid/RegisteredSipUsers/:client', async(req, res) => {
|
||||
allow_direct_app_calling: clientDb ? clientDb.allow_direct_app_calling : 0,
|
||||
allow_direct_queue_calling: clientDb ? clientDb.allow_direct_queue_calling : 0,
|
||||
allow_direct_user_calling: clientDb ? clientDb.allow_direct_user_calling : 0,
|
||||
registered_status: user ? 'active' : 'inactive'
|
||||
registered_status: user ? 'active' : 'inactive',
|
||||
proxy: user ? user.proxy : null
|
||||
});
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
@@ -355,7 +383,10 @@ async function validateCreateCall(logger, sid, req) {
|
||||
const {lookupAppBySid} = req.app.locals;
|
||||
const obj = req.body;
|
||||
|
||||
if (req.user.account_sid !== sid) throw new DbErrorBadRequest(`unauthorized createCall request for account ${sid}`);
|
||||
if (req.user.hasServiceProviderAuth ||
|
||||
req.user.hasAccountAuth && req.user.account_sid !== sid) {
|
||||
throw new DbErrorBadRequest(`unauthorized createCall request for account ${sid}`);
|
||||
}
|
||||
|
||||
obj.account_sid = sid;
|
||||
if (!obj.from) throw new DbErrorBadRequest('missing from parameter');
|
||||
@@ -650,12 +681,12 @@ function encryptBucketCredential(obj, storedCredentials = {}) {
|
||||
name,
|
||||
access_key_id,
|
||||
tags,
|
||||
endpoint
|
||||
endpoint,
|
||||
} = obj.bucket_credential;
|
||||
let {
|
||||
secret_access_key,
|
||||
service_key,
|
||||
connection_string
|
||||
connection_string,
|
||||
} = obj.bucket_credential;
|
||||
|
||||
switch (vendor) {
|
||||
@@ -680,7 +711,9 @@ function encryptBucketCredential(obj, storedCredentials = {}) {
|
||||
secret_access_key = storedCredentials.secret_access_key;
|
||||
}
|
||||
const s3Data = JSON.stringify({vendor, endpoint, name, access_key_id,
|
||||
secret_access_key, tags});
|
||||
secret_access_key, tags,
|
||||
...(region && {region})
|
||||
});
|
||||
obj.bucket_credential = encrypt(s3Data);
|
||||
break;
|
||||
case 'google':
|
||||
@@ -1187,4 +1220,23 @@ router.get('/:sid/Conferences', async(req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* retrieve counts of calls under an account
|
||||
*/
|
||||
router.get('/:sid/CallCount', async(req, res) => {
|
||||
const {logger, getCallCount} = req.app.locals;
|
||||
try {
|
||||
const accountSid = parseAccountSid(req);
|
||||
await validateRequest(req, accountSid);
|
||||
const count = await getCallCount(accountSid);
|
||||
count.outbound = Number(count.outbound);
|
||||
count.inbound = Number(count.inbound);
|
||||
logger.debug(`retrieved, outbound: ${count.outbound}, inbound: ${count.inbound}, for account sid ${accountSid}`);
|
||||
res.status(200).json(count);
|
||||
updateLastUsed(logger, accountSid, req).catch((err) => {});
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -173,11 +173,19 @@ router.post('/', async(req, res) => {
|
||||
/* list */
|
||||
router.get('/', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
const {page, page_size, name} = req.query || {};
|
||||
const isPaginationRequest = page !== null && page !== undefined;
|
||||
try {
|
||||
const service_provider_sid = req.user.hasServiceProviderAuth ? req.user.service_provider_sid : null;
|
||||
const account_sid = req.user.hasAccountAuth ? req.user.account_sid : null;
|
||||
const name = req.query.name;
|
||||
const results = await Application.retrieveAll(service_provider_sid, account_sid, name);
|
||||
let results = [];
|
||||
let total = 0;
|
||||
if (isPaginationRequest) {
|
||||
total = await Application.countAll({service_provider_sid, account_sid, name});
|
||||
}
|
||||
results = await Application.retrieveAll({
|
||||
service_provider_sid, account_sid, name, page, page_size
|
||||
});
|
||||
const ret = results.map((a) => {
|
||||
if (a.env_vars) {
|
||||
a.env_vars = JSON.parse(decrypt(a.env_vars));
|
||||
@@ -186,7 +194,13 @@ router.get('/', async(req, res) => {
|
||||
return a;
|
||||
}
|
||||
});
|
||||
res.status(200).json(ret);
|
||||
const body = isPaginationRequest ? {
|
||||
total,
|
||||
page: Number(page),
|
||||
page_size: Number(page_size),
|
||||
data: ret
|
||||
} : ret;
|
||||
res.status(200).json(body);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
|
||||
@@ -52,20 +52,21 @@ router.post('/', async(req, res) => {
|
||||
let obj;
|
||||
try {
|
||||
if (!email || !validateEmail(email)) {
|
||||
logger.info({email}, 'Bad POST to /forgot-password is missing email or invalid email');
|
||||
return res.status(400).json({error: 'invalid or missing email'});
|
||||
}
|
||||
|
||||
const [r] = await promisePool.query({sql, nestTables: true}, email);
|
||||
if (0 === r.length) {
|
||||
logger.info('user not found');
|
||||
logger.info(`user not found: ${email}`);
|
||||
return res.status(400).json({error: 'failed to reset your password'});
|
||||
}
|
||||
obj = r[0];
|
||||
if (!obj.user.is_active) {
|
||||
logger.info(obj.user.name, 'user is inactive');
|
||||
logger.info({user: obj.user.name, obj}, 'user is inactive');
|
||||
return res.status(400).json({error: 'failed to reset your password'});
|
||||
} else if (obj.acc.account_sid !== null && !obj.acc.is_active) {
|
||||
logger.info(obj.acc.account_sid, 'account is inactive');
|
||||
logger.info({account_sid: obj.acc.account_sid, obj}, 'account is inactive');
|
||||
return res.status(400).json({error: 'failed to reset your password'});
|
||||
}
|
||||
res.sendStatus(204);
|
||||
|
||||
@@ -13,6 +13,7 @@ const preconditions = {
|
||||
};
|
||||
const sysError = require('../error');
|
||||
const { parsePhoneNumberSid } = require('./utils');
|
||||
const hasWhitespace = (str) => /\s/.test(str);
|
||||
|
||||
|
||||
/* check for required fields when adding */
|
||||
@@ -28,6 +29,7 @@ async function validateAdd(req) {
|
||||
}
|
||||
|
||||
if (!req.body.number) throw new DbErrorBadRequest('number is required');
|
||||
if (hasWhitespace(req.body.number)) throw new DbErrorBadRequest('number cannot contain whitespace');
|
||||
const formattedNumber = e164(req.body.number);
|
||||
req.body.number = formattedNumber;
|
||||
} catch (err) {
|
||||
@@ -97,28 +99,35 @@ decorate(router, PhoneNumber, ['add', 'update', 'delete'], preconditions);
|
||||
/* list */
|
||||
router.get('/', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
const {account_sid, filter} = req.query;
|
||||
const {service_provider_sid: query_service_provider_sid,
|
||||
account_sid: query_account_sid, filter, page, page_size} = req.query;
|
||||
const isPaginationRequest = page !== null && page !== undefined;
|
||||
let service_provider_sid = null, account_sid = query_account_sid;
|
||||
if (req.user.hasAccountAuth) {
|
||||
account_sid = req.user.account_sid;
|
||||
} else if (req.user.hasServiceProviderAuth) {
|
||||
service_provider_sid = req.user.service_provider_sid;
|
||||
} else {
|
||||
// admin user can query all phone numbers
|
||||
service_provider_sid = query_service_provider_sid;
|
||||
account_sid = query_account_sid;
|
||||
}
|
||||
try {
|
||||
let results = [];
|
||||
if (req.user.hasAccountAuth) {
|
||||
results = await PhoneNumber.retrieveAllByCriteria({
|
||||
account_sid: req.user.account_sid,
|
||||
filter
|
||||
});
|
||||
} else if (req.user.hasServiceProviderAuth) {
|
||||
results = await PhoneNumber.retrieveAllByCriteria({
|
||||
service_provider_sid: req.user.service_provider_sid,
|
||||
account_sid,
|
||||
filter
|
||||
});
|
||||
} else if (req.user.hasAdminAuth) {
|
||||
results = await PhoneNumber.retrieveAllByCriteria({
|
||||
account_sid,
|
||||
filter
|
||||
});
|
||||
let total = 0;
|
||||
if (isPaginationRequest) {
|
||||
total = await PhoneNumber.countAll({service_provider_sid, account_sid, filter});
|
||||
}
|
||||
const results = await PhoneNumber.retrieveAllByCriteria({
|
||||
service_provider_sid, account_sid, filter, page, page_size
|
||||
});
|
||||
const body = isPaginationRequest ? {
|
||||
total,
|
||||
page: Number(page),
|
||||
page_size: Number(page_size),
|
||||
data: results,
|
||||
} : results;
|
||||
|
||||
res.status(200).json(results);
|
||||
res.status(200).json(body);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
|
||||
@@ -149,13 +149,30 @@ router.get('/:sid/VoipCarriers', async(req, res) => {
|
||||
try {
|
||||
await validateRetrieve(req);
|
||||
const service_provider_sid = parseServiceProviderSid(req);
|
||||
const carriers = await VoipCarrier.retrieveAllForSP(service_provider_sid);
|
||||
|
||||
if (req.user.hasScope('account')) {
|
||||
return res.status(200).json(carriers.filter((c) => c.account_sid === req.user.account_sid || !c.account_sid));
|
||||
const {account_sid: query_account_sid, name, page, page_size} = req.query || {};
|
||||
const isPaginationRequest = page !== null && page !== undefined;
|
||||
const account_sid = req.user.hasAccountAuth ? req.user.account_sid : query_account_sid || null;
|
||||
let carriers = [];
|
||||
let total = 0;
|
||||
if (isPaginationRequest) {
|
||||
total = await VoipCarrier.countAll({service_provider_sid, account_sid, name});
|
||||
}
|
||||
carriers = await VoipCarrier.retrieveByCriteria({
|
||||
service_provider_sid,
|
||||
account_sid,
|
||||
name,
|
||||
page,
|
||||
page_size,
|
||||
});
|
||||
|
||||
res.status(200).json(carriers);
|
||||
const body = isPaginationRequest ? {
|
||||
total,
|
||||
page: Number(page),
|
||||
page_size: Number(page_size),
|
||||
data: carriers,
|
||||
} : carriers;
|
||||
|
||||
res.status(200).json(body);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ const decorate = require('./decorate');
|
||||
const sysError = require('../error');
|
||||
const net = require('net');
|
||||
|
||||
const hasWhitespace = (str) => /\s/.test(str);
|
||||
const checkUserScope = async(req, voip_carrier_sid) => {
|
||||
const {lookupCarrierBySid} = req.app.locals;
|
||||
if (!voip_carrier_sid) {
|
||||
@@ -60,6 +61,9 @@ const validate = async(req, sid) => {
|
||||
throw new DbErrorBadRequest(
|
||||
`netmask required to have value equal or greater than ${process.env.JAMBONZ_MIN_GATEWAY_NETMASK}`);
|
||||
}
|
||||
if (hasWhitespace(ipv4)) {
|
||||
throw new DbErrorBadRequest('Gateway must not contain whitespace');
|
||||
}
|
||||
if (inbound && !net.isIPv4(ipv4)) {
|
||||
throw new DbErrorBadRequest('Inbound gateway must be IPv4 address');
|
||||
}
|
||||
|
||||
@@ -14,7 +14,9 @@ const {decryptCredential, testWhisper, testDeepgramTTS,
|
||||
testSpeechmaticsStt,
|
||||
testCartesia,
|
||||
testVoxistStt,
|
||||
testOpenAiStt} = require('../../utils/speech-utils');
|
||||
testOpenAiStt,
|
||||
testInworld,
|
||||
testResembleTTS} = require('../../utils/speech-utils');
|
||||
const {DbErrorUnprocessableRequest, DbErrorForbidden, DbErrorBadRequest} = require('../../utils/errors');
|
||||
const {
|
||||
testGoogleTts,
|
||||
@@ -131,10 +133,14 @@ const encryptCredential = (obj) => {
|
||||
deepgram_stt_use_tls,
|
||||
deepgram_tts_uri,
|
||||
playht_tts_uri,
|
||||
resemble_tts_uri,
|
||||
resemble_tts_use_tls,
|
||||
use_custom_tts,
|
||||
custom_tts_endpoint,
|
||||
custom_tts_endpoint_url,
|
||||
use_custom_stt,
|
||||
use_for_stt,
|
||||
use_for_tts,
|
||||
custom_stt_endpoint,
|
||||
custom_stt_endpoint_url,
|
||||
tts_api_key,
|
||||
@@ -148,10 +154,14 @@ const encryptCredential = (obj) => {
|
||||
custom_tts_streaming_url,
|
||||
auth_token = '',
|
||||
cobalt_server_uri,
|
||||
// For most vendors, model_id is being used for both TTS and STT, or one of them.
|
||||
// for Cartesia, model_id is used for TTS only. introduce stt_model_id for STT
|
||||
model_id,
|
||||
stt_model_id,
|
||||
user_id,
|
||||
voice_engine,
|
||||
engine_version,
|
||||
service_version,
|
||||
options
|
||||
} = obj;
|
||||
|
||||
@@ -215,9 +225,24 @@ const encryptCredential = (obj) => {
|
||||
if (!deepgram_stt_uri || !deepgram_tts_uri) {
|
||||
assert(api_key, 'invalid deepgram speech credential: api_key is required');
|
||||
}
|
||||
const deepgramData = JSON.stringify({api_key, deepgram_stt_uri, deepgram_stt_use_tls, deepgram_tts_uri});
|
||||
const deepgramData = JSON.stringify({api_key, deepgram_stt_uri,
|
||||
deepgram_stt_use_tls, deepgram_tts_uri, model_id});
|
||||
return encrypt(deepgramData);
|
||||
|
||||
case 'resemble':
|
||||
assert(api_key, 'invalid resemble speech credential: api_key is required');
|
||||
const resembleData = JSON.stringify({
|
||||
api_key,
|
||||
...(resemble_tts_uri && {resemble_tts_uri}),
|
||||
...(resemble_tts_use_tls && {resemble_tts_use_tls})
|
||||
});
|
||||
return encrypt(resembleData);
|
||||
|
||||
case 'deepgramriver':
|
||||
assert(api_key, 'invalid deepgram river speech credential: api_key is required');
|
||||
const deepgramriverData = JSON.stringify({api_key});
|
||||
return encrypt(deepgramriverData);
|
||||
|
||||
case 'ibm':
|
||||
const ibmData = JSON.stringify({tts_api_key, tts_region, stt_api_key, stt_region, instance_id});
|
||||
return encrypt(ibmData);
|
||||
@@ -258,8 +283,17 @@ const encryptCredential = (obj) => {
|
||||
|
||||
case 'cartesia':
|
||||
assert(api_key, 'invalid cartesia speech credential: api_key is required');
|
||||
assert(model_id, 'invalid cartesia speech credential: model_id is required');
|
||||
const cartesiaData = JSON.stringify({api_key, model_id, options});
|
||||
if (use_for_tts) {
|
||||
assert(model_id, 'invalid cartesia speech credential: model_id is required');
|
||||
}
|
||||
if (use_for_stt) {
|
||||
assert(stt_model_id, 'invalid cartesia speech credential: stt_model_id is required');
|
||||
}
|
||||
const cartesiaData = JSON.stringify({
|
||||
api_key,
|
||||
...(model_id && {model_id}),
|
||||
...(stt_model_id && {stt_model_id}),
|
||||
options});
|
||||
return encrypt(cartesiaData);
|
||||
|
||||
case 'rimelabs':
|
||||
@@ -268,9 +302,15 @@ const encryptCredential = (obj) => {
|
||||
const rimelabsData = JSON.stringify({api_key, model_id, options});
|
||||
return encrypt(rimelabsData);
|
||||
|
||||
case 'inworld':
|
||||
assert(api_key, 'invalid inworld speech credential: api_key is required');
|
||||
assert(model_id, 'invalid inworld speech credential: model_id is required');
|
||||
const inworldData = JSON.stringify({api_key, model_id, options});
|
||||
return encrypt(inworldData);
|
||||
|
||||
case 'assemblyai':
|
||||
assert(api_key, 'invalid assemblyai speech credential: api_key is required');
|
||||
const assemblyaiData = JSON.stringify({api_key});
|
||||
const assemblyaiData = JSON.stringify({api_key, service_version});
|
||||
return encrypt(assemblyaiData);
|
||||
|
||||
case 'voxist':
|
||||
@@ -486,6 +526,7 @@ router.put('/:sid', async(req, res) => {
|
||||
custom_tts_streaming_url,
|
||||
cobalt_server_uri,
|
||||
model_id,
|
||||
stt_model_id,
|
||||
voice_engine,
|
||||
options,
|
||||
deepgram_stt_uri,
|
||||
@@ -493,7 +534,10 @@ router.put('/:sid', async(req, res) => {
|
||||
deepgram_tts_uri,
|
||||
playht_tts_uri,
|
||||
engine_version,
|
||||
speechmatics_stt_uri
|
||||
service_version,
|
||||
speechmatics_stt_uri,
|
||||
resemble_tts_use_tls,
|
||||
resemble_tts_uri
|
||||
} = req.body;
|
||||
|
||||
const newCred = {
|
||||
@@ -517,6 +561,7 @@ router.put('/:sid', async(req, res) => {
|
||||
custom_tts_streaming_url,
|
||||
cobalt_server_uri,
|
||||
model_id,
|
||||
stt_model_id,
|
||||
voice_engine,
|
||||
options,
|
||||
deepgram_stt_uri,
|
||||
@@ -524,7 +569,10 @@ router.put('/:sid', async(req, res) => {
|
||||
deepgram_tts_uri,
|
||||
playht_tts_uri,
|
||||
engine_version,
|
||||
speechmatics_stt_uri
|
||||
service_version,
|
||||
speechmatics_stt_uri,
|
||||
resemble_tts_uri,
|
||||
resemble_tts_use_tls
|
||||
};
|
||||
logger.info({o, newCred}, 'updating speech credential with this new credential');
|
||||
obj.credential = encryptCredential(newCred);
|
||||
@@ -724,6 +772,17 @@ router.get('/:sid/test', async(req, res) => {
|
||||
SpeechCredential.sttTestResult(sid, false);
|
||||
}
|
||||
}
|
||||
} else if (cred.vendor === 'resemble') {
|
||||
if (cred.use_for_tts) {
|
||||
try {
|
||||
await testResembleTTS(logger, synthAudio, credential);
|
||||
results.tts.status = 'ok';
|
||||
SpeechCredential.ttsTestResult(sid, true);
|
||||
} catch (err) {
|
||||
results.tts = {status: 'fail', reason: err.message};
|
||||
SpeechCredential.ttsTestResult(sid, false);
|
||||
}
|
||||
}
|
||||
} else if (cred.vendor === 'deepgram') {
|
||||
const {api_key} = credential;
|
||||
if (cred.use_for_tts) {
|
||||
@@ -747,6 +806,19 @@ router.get('/:sid/test', async(req, res) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (cred.vendor === 'deepgramriver') {
|
||||
const {api_key} = credential;
|
||||
if (cred.use_for_stt && api_key) {
|
||||
try {
|
||||
await testDeepgramStt(logger, {api_key});
|
||||
results.stt.status = 'ok';
|
||||
SpeechCredential.sttTestResult(sid, true);
|
||||
} catch (err) {
|
||||
results.stt = {status: 'fail', reason: err.message};
|
||||
SpeechCredential.sttTestResult(sid, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (cred.vendor === 'ibm') {
|
||||
const {getTtsVoices} = req.app.locals;
|
||||
|
||||
@@ -832,17 +904,39 @@ router.get('/:sid/test', async(req, res) => {
|
||||
}
|
||||
}
|
||||
} else if (cred.vendor === 'cartesia') {
|
||||
if (cred.use_for_tts) {
|
||||
if (cred.use_for_tts || cred.use_for_stt) {
|
||||
try {
|
||||
// Cartesia does not have API for testing STT, same key is used for both TTS and STT
|
||||
await testCartesia(logger, synthAudio, credential);
|
||||
results.tts.status = 'ok';
|
||||
if (cred.use_for_tts) {
|
||||
results.tts.status = 'ok';
|
||||
}
|
||||
if (cred.use_for_stt) {
|
||||
results.stt.status = 'ok';
|
||||
}
|
||||
SpeechCredential.ttsTestResult(sid, true);
|
||||
} catch (err) {
|
||||
let reason = err.message;
|
||||
try {
|
||||
reason = await err.text();
|
||||
} catch {}
|
||||
results.tts = {status: 'fail', reason};
|
||||
if (cred.use_for_tts) {
|
||||
results.tts = {status: 'fail', reason};
|
||||
}
|
||||
if (cred.use_for_stt) {
|
||||
results.stt = {status: 'fail', reason};
|
||||
}
|
||||
SpeechCredential.ttsTestResult(sid, false);
|
||||
}
|
||||
}
|
||||
} else if (cred.vendor === 'inworld') {
|
||||
if (cred.use_for_tts) {
|
||||
try {
|
||||
await testInworld(logger, synthAudio, credential);
|
||||
results.tts.status = 'ok';
|
||||
SpeechCredential.ttsTestResult(sid, true);
|
||||
} catch (err) {
|
||||
results.tts = {status: 'fail', reason: err.message};
|
||||
SpeechCredential.ttsTestResult(sid, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,6 +325,7 @@ router.get('/me', async(req, res) => {
|
||||
res.json(payload);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
logger.info({err, payload}, 'payload');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -286,7 +286,11 @@ const hasAccountPermissions = async(req, res, next) => {
|
||||
message: 'insufficient privileges'
|
||||
});
|
||||
} catch (error) {
|
||||
throw error;
|
||||
// return 400 on errors
|
||||
res.status(400).json({
|
||||
status: 'fail',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -73,22 +73,36 @@ decorate(router, VoipCarrier, ['add', 'update', 'delete'], preconditions);
|
||||
/* list */
|
||||
router.get('/', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
const {lookupAccountBySid} = req.app.locals;
|
||||
|
||||
const {account_sid: query_account_sid, name, page, page_size} = req.query || {};
|
||||
const isPaginationRequest = page !== null && page !== undefined;
|
||||
let service_provider_sid = null, account_sid = query_account_sid;
|
||||
if (req.user.hasAccountAuth) {
|
||||
account_sid = req.user.account_sid;
|
||||
} else if (req.user.hasServiceProviderAuth) {
|
||||
service_provider_sid = req.user.service_provider_sid;
|
||||
}
|
||||
try {
|
||||
let results = [];
|
||||
if (req.user.hasAdminAuth) {
|
||||
results = await VoipCarrier.retrieveAll(req.user.hasAccountAuth ? req.user.account_sid : null);
|
||||
} else {
|
||||
const account = req.user.service_provider_sid ? req.user : await lookupAccountBySid(req.user.account_sid);
|
||||
results = await VoipCarrier.retrieveAllForSP(account.service_provider_sid);
|
||||
let total = 0;
|
||||
if (isPaginationRequest) {
|
||||
total = await VoipCarrier.countAll({service_provider_sid, account_sid, name});
|
||||
}
|
||||
|
||||
if (req.user.hasScope('account')) {
|
||||
return res.status(200).json(results.filter((c) => c.account_sid === req.user.account_sid || !c.account_sid));
|
||||
}
|
||||
const carriers = await VoipCarrier.retrieveByCriteria({
|
||||
service_provider_sid,
|
||||
account_sid,
|
||||
name,
|
||||
page,
|
||||
page_size,
|
||||
});
|
||||
|
||||
res.status(200).json(results);
|
||||
const body = isPaginationRequest ? {
|
||||
total,
|
||||
page: Number(page),
|
||||
page_size: Number(page_size),
|
||||
data: carriers,
|
||||
} : carriers;
|
||||
|
||||
res.status(200).json(body);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
|
||||
4
lib/utils/speech-data/stt-model-cartesia.js
Normal file
4
lib/utils/speech-data/stt-model-cartesia.js
Normal file
@@ -0,0 +1,4 @@
|
||||
module.exports = [
|
||||
{ name: 'Ink-whisper', value: 'ink-whisper' },
|
||||
];
|
||||
|
||||
52
lib/utils/speech-data/stt-model-deepgram.js
Normal file
52
lib/utils/speech-data/stt-model-deepgram.js
Normal file
@@ -0,0 +1,52 @@
|
||||
module.exports = [
|
||||
// Nova-3
|
||||
{ name: 'Nova-3', value: 'nova-3' },
|
||||
{ name: 'Nova-3 General', value: 'nova-3-general' },
|
||||
{ name: 'Nova-3 Medical', value: 'nova-3-medical' },
|
||||
|
||||
// Nova-2
|
||||
{ name: 'Nova-2', value: 'nova-2' },
|
||||
{ name: 'Nova-2 General', value: 'nova-2-general' },
|
||||
{ name: 'Nova-2 Meeting', value: 'nova-2-meeting' },
|
||||
{ name: 'Nova-2 Phonecall', value: 'nova-2-phonecall' },
|
||||
{ name: 'Nova-2 Finance', value: 'nova-2-finance' },
|
||||
{ name: 'Nova-2 Conversational AI', value: 'nova-2-conversationalai' },
|
||||
{ name: 'Nova-2 Voicemail', value: 'nova-2-voicemail' },
|
||||
{ name: 'Nova-2 Video', value: 'nova-2-video' },
|
||||
{ name: 'Nova-2 Medical', value: 'nova-2-medical' },
|
||||
{ name: 'Nova-2 Drivethru', value: 'nova-2-drivethru' },
|
||||
{ name: 'Nova-2 Automotive', value: 'nova-2-automotive' },
|
||||
{ name: 'Nova-2 ATC', value: 'nova-2-atc' },
|
||||
|
||||
// Nova (legacy)
|
||||
{ name: 'Nova', value: 'nova' },
|
||||
{ name: 'Nova General', value: 'nova-general' },
|
||||
{ name: 'Nova Phonecall', value: 'nova-phonecall' },
|
||||
{ name: 'Nova Medical', value: 'nova-medical' },
|
||||
|
||||
// Enhanced (legacy)
|
||||
{ name: 'Enhanced', value: 'enhanced' },
|
||||
{ name: 'Enhanced General', value: 'enhanced-general' },
|
||||
{ name: 'Enhanced Meeting', value: 'enhanced-meeting' },
|
||||
{ name: 'Enhanced Phonecall', value: 'enhanced-phonecall' },
|
||||
{ name: 'Enhanced Finance', value: 'enhanced-finance' },
|
||||
|
||||
// Base (legacy)
|
||||
{ name: 'Base', value: 'base' },
|
||||
{ name: 'Base General', value: 'base-general' },
|
||||
{ name: 'Base Meeting', value: 'base-meeting' },
|
||||
{ name: 'Base Phonecall', value: 'base-phonecall' },
|
||||
{ name: 'Base Finance', value: 'base-finance' },
|
||||
{ name: 'Base Conversational AI', value: 'base-conversationalai' },
|
||||
{ name: 'Base Voicemail', value: 'base-voicemail' },
|
||||
{ name: 'Base Video', value: 'base-video' },
|
||||
|
||||
// Whisper
|
||||
{ name: 'Whisper Tiny', value: 'whisper-tiny' },
|
||||
{ name: 'Whisper Base', value: 'whisper-base' },
|
||||
{ name: 'Whisper Small', value: 'whisper-small' },
|
||||
{ name: 'Whisper Medium', value: 'whisper-medium' },
|
||||
{ name: 'Whisper Large', value: 'whisper-large' },
|
||||
{ name: 'Whisper', value: 'whisper' },
|
||||
];
|
||||
|
||||
118
lib/utils/speech-data/tts-inworld.js
Normal file
118
lib/utils/speech-data/tts-inworld.js
Normal file
@@ -0,0 +1,118 @@
|
||||
module.exports = [
|
||||
{
|
||||
value: 'en',
|
||||
name: 'English',
|
||||
voices: [
|
||||
{ name: 'Alex', value: 'Alex' },
|
||||
{ name: 'Ashley', value: 'Ashley' },
|
||||
{ name: 'Craig', value: 'Craig' },
|
||||
{ name: 'Deborah', value: 'Deborah' },
|
||||
{ name: 'Dennis', value: 'Dennis' },
|
||||
{ name: 'Edward', value: 'Edward' },
|
||||
{ name: 'Elizabeth', value: 'Elizabeth' },
|
||||
{ name: 'Hades', value: 'Hades' },
|
||||
{ name: 'Julia', value: 'Julia' },
|
||||
{ name: 'Pixie', value: 'Pixie' },
|
||||
{ name: 'Mark', value: 'Mark' },
|
||||
{ name: 'Olivia', value: 'Olivia' },
|
||||
{ name: 'Priya', value: 'Priya' },
|
||||
{ name: 'Ronald', value: 'Ronald' },
|
||||
{ name: 'Sarah', value: 'Sarah' },
|
||||
{ name: 'Shaun', value: 'Shaun' },
|
||||
{ name: 'Theodore', value: 'Theodore' },
|
||||
{ name: 'Timothy', value: 'Timothy' },
|
||||
{ name: 'Wendy', value: 'Wendy' },
|
||||
{ name: 'Dominus', value: 'Dominus' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'zh',
|
||||
name: 'Chinese',
|
||||
voices: [
|
||||
{ name: 'Yichen', value: 'Yichen' },
|
||||
{ name: 'Xiaoyin', value: 'Xiaoyin' },
|
||||
{ name: 'Xinyi', value: 'Xinyi' },
|
||||
{ name: 'Jing', value: 'Jing' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'nl',
|
||||
name: 'Dutch',
|
||||
voices: [
|
||||
{ name: 'Erik', value: 'Erik' },
|
||||
{ name: 'Katrien', value: 'Katrien' },
|
||||
{ name: 'Lennart', value: 'Lennart' },
|
||||
{ name: 'Lore', value: 'Lore' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'fr',
|
||||
name: 'French',
|
||||
voices: [
|
||||
{ name: 'Alain', value: 'Alain' },
|
||||
{ name: 'Hélène', value: 'Hélène' },
|
||||
{ name: 'Mathieu', value: 'Mathieu' },
|
||||
{ name: 'Étienne', value: 'Étienne' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'de',
|
||||
name: 'German',
|
||||
voices: [
|
||||
{ name: 'Johanna', value: 'Johanna' },
|
||||
{ name: 'Josef', value: 'Josef' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'it',
|
||||
name: 'Italian',
|
||||
voices: [
|
||||
{ name: 'Gianni', value: 'Gianni' },
|
||||
{ name: 'Orietta', value: 'Orietta' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'ja',
|
||||
name: 'Japanese',
|
||||
voices: [
|
||||
{ name: 'Asuka', value: 'Asuka' },
|
||||
{ name: 'Satoshi', value: 'Satoshi' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'ko',
|
||||
name: 'Korean',
|
||||
voices: [
|
||||
{ name: 'Hyunwoo', value: 'Hyunwoo' },
|
||||
{ name: 'Minji', value: 'Minji' },
|
||||
{ name: 'Seojun', value: 'Seojun' },
|
||||
{ name: 'Yoona', value: 'Yoona' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'pl',
|
||||
name: 'Polish',
|
||||
voices: [
|
||||
{ name: 'Szymon', value: 'Szymon' },
|
||||
{ name: 'Wojciech', value: 'Wojciech' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'pt',
|
||||
name: 'Portuguese',
|
||||
voices: [
|
||||
{ name: 'Heitor', value: 'Heitor' },
|
||||
{ name: 'Maitê', value: 'Maitê' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'es',
|
||||
name: 'Spanish',
|
||||
voices: [
|
||||
{ name: 'Diego', value: 'Diego' },
|
||||
{ name: 'Lupita', value: 'Lupita' },
|
||||
{ name: 'Miguel', value: 'Miguel' },
|
||||
{ name: 'Rafael', value: 'Rafael' },
|
||||
],
|
||||
},
|
||||
];
|
||||
5
lib/utils/speech-data/tts-model-inworld.js
Normal file
5
lib/utils/speech-data/tts-model-inworld.js
Normal file
@@ -0,0 +1,5 @@
|
||||
module.exports = [
|
||||
{ name: 'Llama Inworld TTS', value: 'inworld-tts-1' },
|
||||
{ name: 'Llama Inworld TTS Max', value: 'inworld-tts-1-max' },
|
||||
];
|
||||
|
||||
438
lib/utils/speech-data/tts-resemble.js
Normal file
438
lib/utils/speech-data/tts-resemble.js
Normal file
@@ -0,0 +1,438 @@
|
||||
module.exports = [
|
||||
{
|
||||
value: 'en-gb',
|
||||
name: 'En-gb',
|
||||
voices: [
|
||||
{
|
||||
name: 'Seth (Legacy) (professional) - Resemble Voice',
|
||||
value: 'a52c4efc',
|
||||
},
|
||||
{
|
||||
name: 'Seth (professional) - Resemble Voice',
|
||||
value: 'd3e61caf',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'en-GB',
|
||||
name: 'En-GB',
|
||||
voices: [
|
||||
{
|
||||
name: 'Beatrice Pendergast (professional) - Resemble Voice',
|
||||
value: '00b1fd4e',
|
||||
},
|
||||
{
|
||||
name: 'Ed Smart (professional) - Resemble Voice',
|
||||
value: '0c755526',
|
||||
},
|
||||
{
|
||||
name: 'Paula J (professional) - Resemble Voice',
|
||||
value: '33e64cd2',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'en-us',
|
||||
name: 'En-us',
|
||||
voices: [
|
||||
{
|
||||
name: 'David (professional) - Resemble Voice',
|
||||
value: '5bb13f03',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'en-US',
|
||||
name: 'En-US',
|
||||
voices: [
|
||||
{
|
||||
name: 'Adam Lofbomm (professional) - Resemble Voice',
|
||||
value: '4e228dba',
|
||||
},
|
||||
{
|
||||
name: 'Alex (professional) - Resemble Voice',
|
||||
value: '41b99669',
|
||||
},
|
||||
{
|
||||
name: 'Amelia (professional) - Resemble Voice',
|
||||
value: 'ecbe5d97',
|
||||
},
|
||||
{
|
||||
name: 'Andrew (rapid) - Resemble Marketplace',
|
||||
value: 'd2f26a3e',
|
||||
},
|
||||
{
|
||||
name: 'Annika (professional) - Resemble Voice',
|
||||
value: 'b27f3cc0',
|
||||
},
|
||||
{
|
||||
name: 'Arthur (professional) - Resemble Voice',
|
||||
value: '9de11312',
|
||||
},
|
||||
{
|
||||
name: 'Ash (professional) - Resemble Voice',
|
||||
value: 'ee322483',
|
||||
},
|
||||
{
|
||||
name: 'Aurora (professional) - Resemble Voice',
|
||||
value: 'a72d9fca',
|
||||
},
|
||||
{
|
||||
name: 'Austin (professional) - Resemble Voice',
|
||||
value: '82a67e58',
|
||||
},
|
||||
{
|
||||
name: 'Beth (Legacy) (professional) - Resemble Voice',
|
||||
value: '25c7823f',
|
||||
},
|
||||
{
|
||||
name: 'Beth (professional) - Resemble Voice',
|
||||
value: 'fa66d263',
|
||||
},
|
||||
{
|
||||
name: 'Blade (professional) - Resemble Voice',
|
||||
value: '8bedd793',
|
||||
},
|
||||
{
|
||||
name: 'Brandy Sky (professional) - Resemble Voice',
|
||||
value: '79e2f1dc',
|
||||
},
|
||||
{
|
||||
name: 'Brenley (professional) - Resemble Voice',
|
||||
value: 'e6ec3ca4',
|
||||
},
|
||||
{
|
||||
name: 'Britney (professional) - Resemble Voice',
|
||||
value: 'e57e23ff',
|
||||
},
|
||||
{
|
||||
name: 'Broadcast Joe (professional) - Resemble Voice',
|
||||
value: '21e49584',
|
||||
},
|
||||
{
|
||||
name: 'Carl Bishop (Angry) (professional) - Resemble Voice',
|
||||
value: 'f06cd770',
|
||||
},
|
||||
{
|
||||
name: 'Carl Bishop (Conversational) (professional) - Resemble Voice',
|
||||
value: '7f40ff35',
|
||||
},
|
||||
{
|
||||
name: 'Carl Bishop (Happy) (professional) - Resemble Voice',
|
||||
value: '99751e42',
|
||||
},
|
||||
{
|
||||
name: 'Carl Bishop (professional) - Resemble Voice',
|
||||
value: '01bcc102',
|
||||
},
|
||||
{
|
||||
name: 'Carl Bishop (Scared) (Legacy) (professional) - Resemble Voice',
|
||||
value: '1dcf0222',
|
||||
},
|
||||
{
|
||||
name: 'Carl Bishop (Scared) (professional) - Resemble Voice',
|
||||
value: 'eacbc44f',
|
||||
},
|
||||
{
|
||||
name: 'Charles (Legacy) (professional) - Resemble Voice',
|
||||
value: '4c6d3da5',
|
||||
},
|
||||
{
|
||||
name: 'Charles (professional) - Resemble Voice',
|
||||
value: 'd79a5198',
|
||||
},
|
||||
{
|
||||
name: 'Charlotte (professional) - Resemble Voice',
|
||||
value: '96b91cf9',
|
||||
},
|
||||
{
|
||||
name: 'Chris Whiting (professional) - Resemble Voice',
|
||||
value: '95b7560a',
|
||||
},
|
||||
{
|
||||
name: 'Cliff (professional) - Resemble Voice',
|
||||
value: 'fcf8490c',
|
||||
},
|
||||
{
|
||||
name: 'Connor (professional) - Resemble Voice',
|
||||
value: 'a6131acf',
|
||||
},
|
||||
{
|
||||
name: 'Deanna (professional) - Resemble Voice',
|
||||
value: '0842fdf9',
|
||||
},
|
||||
{
|
||||
name: 'Ember (professional) - Resemble Voice',
|
||||
value: '55592656',
|
||||
},
|
||||
{
|
||||
name: 'Gene Amore (professional) - Resemble Voice',
|
||||
value: 'f2ea7aa0',
|
||||
},
|
||||
{
|
||||
name: 'Harry Robinson (professional) - Resemble Voice',
|
||||
value: '3c36d67d',
|
||||
},
|
||||
{
|
||||
name: 'Helena (professional) - Resemble Voice',
|
||||
value: 'ac948df2',
|
||||
},
|
||||
{
|
||||
name: 'Hem (professional) - Resemble Voice',
|
||||
value: 'b6edbe5f',
|
||||
},
|
||||
{
|
||||
name: 'John (professional) - Resemble Voice',
|
||||
value: 'ac48daeb',
|
||||
},
|
||||
{
|
||||
name: 'Josh (professional) - Resemble Voice',
|
||||
value: '987c99e9',
|
||||
},
|
||||
{
|
||||
name: 'Julie Hoverson (professional) - Resemble Voice',
|
||||
value: 'b119524c',
|
||||
},
|
||||
{
|
||||
name: 'Justin (Legacy) (professional) - Resemble Voice',
|
||||
value: 'b2d1bb75',
|
||||
},
|
||||
{
|
||||
name: 'Justin (Meditative) (Legacy) (professional) - Resemble Voice',
|
||||
value: '93ce0920',
|
||||
},
|
||||
{
|
||||
name: 'Justin (Meditative) (professional) - Resemble Voice',
|
||||
value: '2570000e',
|
||||
},
|
||||
{
|
||||
name: 'Justin (professional) - Resemble Voice',
|
||||
value: '9d513c17',
|
||||
},
|
||||
{
|
||||
name: 'Karl Nordman (professional) - Resemble Voice',
|
||||
value: 'da67f17e',
|
||||
},
|
||||
{
|
||||
name: 'Kate (professional) - Resemble Voice',
|
||||
value: '28b4cc5a',
|
||||
},
|
||||
{
|
||||
name: 'Katya (professional) - Resemble Voice',
|
||||
value: 'c9ee13b4',
|
||||
},
|
||||
{
|
||||
name: 'Ken (professional) - Resemble Voice',
|
||||
value: '3dbfbf3d',
|
||||
},
|
||||
{
|
||||
name: 'Kessi (professional) - Resemble Voice',
|
||||
value: '2211cb8c',
|
||||
},
|
||||
{
|
||||
name: 'Little Ari (professional) - Resemble Voice',
|
||||
value: '805adead',
|
||||
},
|
||||
{
|
||||
name: 'Little Brittle (professional) - Resemble Voice',
|
||||
value: '8a73f115',
|
||||
},
|
||||
{
|
||||
name: 'Liz (professional) - Resemble Voice',
|
||||
value: '4884d94a',
|
||||
},
|
||||
{
|
||||
name: 'Lothar (professional) - Resemble Voice',
|
||||
value: '78671217',
|
||||
},
|
||||
{
|
||||
name: 'Luna (professional) - Resemble Voice',
|
||||
value: 'ae8223ca',
|
||||
},
|
||||
{
|
||||
name: 'Matt Weller (professional) - Resemble Voice',
|
||||
value: 'f4da4639',
|
||||
},
|
||||
{
|
||||
name: 'Maureen (Angry) (professional) - Resemble Voice',
|
||||
value: '482babfc',
|
||||
},
|
||||
{
|
||||
name: 'Maureen (Caring) (professional) - Resemble Voice',
|
||||
value: 'b15e550f',
|
||||
},
|
||||
{
|
||||
name: 'Maureen (Happy) (professional) - Resemble Voice',
|
||||
value: '91947e5c',
|
||||
},
|
||||
{
|
||||
name: 'Maureen (professional) - Resemble Voice',
|
||||
value: '7d94218f',
|
||||
},
|
||||
{
|
||||
name: 'Maureen (Sad) (professional) - Resemble Voice',
|
||||
value: 'bca7481c',
|
||||
},
|
||||
{
|
||||
name: 'Maureen (Scared) (professional) - Resemble Voice',
|
||||
value: '251c9439',
|
||||
},
|
||||
{
|
||||
name: 'Mauren (Announcer) (professional) - Resemble Voice',
|
||||
value: 'e984fb89',
|
||||
},
|
||||
{
|
||||
name: 'Melody (Legacy) (professional) - Resemble Voice',
|
||||
value: '15be93bd',
|
||||
},
|
||||
{
|
||||
name: 'Melody (professional) - Resemble Voice',
|
||||
value: '1c49e774',
|
||||
},
|
||||
{
|
||||
name: 'Mike (professional) - Resemble Voice',
|
||||
value: '3a02dc40',
|
||||
},
|
||||
{
|
||||
name: 'Niki (professional) - Resemble Voice',
|
||||
value: 'db37643c',
|
||||
},
|
||||
{
|
||||
name: 'Olga (professional) - Resemble Voice',
|
||||
value: '07c1d6b5',
|
||||
},
|
||||
{
|
||||
name: 'Olivia (Legacy) (professional) - Resemble Voice',
|
||||
value: '405b58e3',
|
||||
},
|
||||
{
|
||||
name: 'Olivia (professional) - Resemble Voice',
|
||||
value: 'ef49f972',
|
||||
},
|
||||
{
|
||||
name: 'Orion (professional) - Resemble Voice',
|
||||
value: 'aa8053cc',
|
||||
},
|
||||
{
|
||||
name: 'Pete (professional) - Resemble Voice',
|
||||
value: '1864fd63',
|
||||
},
|
||||
{
|
||||
name: 'Primrose (Legacy) (professional) - Resemble Voice',
|
||||
value: '7c8e47ca',
|
||||
},
|
||||
{
|
||||
name: 'Primrose (professional) - Resemble Voice',
|
||||
value: '33eecc17',
|
||||
},
|
||||
{
|
||||
name: 'Primrose (Whispering) (Legacy) (professional) - Resemble Voice',
|
||||
value: 'a56c5c6f',
|
||||
},
|
||||
{
|
||||
name: 'Primrose (Whispering) (professional) - Resemble Voice',
|
||||
value: '28fcdf76',
|
||||
},
|
||||
{
|
||||
name: 'Primrose (Winded) (Legacy) (professional) - Resemble Voice',
|
||||
value: '6f9a77a4',
|
||||
},
|
||||
{
|
||||
name: 'Primrose (Winded) (professional) - Resemble Voice',
|
||||
value: '0097f246',
|
||||
},
|
||||
{
|
||||
name: 'Professor Shaposhnikov (professional) - Resemble Voice',
|
||||
value: '3f5fb9f1',
|
||||
},
|
||||
{
|
||||
name: 'Radio Nikole (professional) - Resemble Voice',
|
||||
value: '19eae884',
|
||||
},
|
||||
{
|
||||
name: 'Richard Garifo (professional) - Resemble Voice',
|
||||
value: '85ba84f2',
|
||||
},
|
||||
{
|
||||
name: 'Rico (professional) - Resemble Voice',
|
||||
value: '14ca34b3',
|
||||
},
|
||||
{
|
||||
name: 'Robert (professional) - Resemble Voice',
|
||||
value: '3e907bcc',
|
||||
},
|
||||
{
|
||||
name: 'Rupert (rapid) - Resemble Voice',
|
||||
value: '28f1626c',
|
||||
},
|
||||
{
|
||||
name: 'Sam (professional) - Resemble Voice',
|
||||
value: '0f2f9a7e',
|
||||
},
|
||||
{
|
||||
name: 'Samantha (Legacy) (professional) - Resemble Voice',
|
||||
value: '266bfae9',
|
||||
},
|
||||
{
|
||||
name: 'Samantha (professional) - Resemble Voice',
|
||||
value: 'e28236ee',
|
||||
},
|
||||
{
|
||||
name: 'Siobhan (professional) - Resemble Voice',
|
||||
value: 'af72c1ac',
|
||||
},
|
||||
{
|
||||
name: 'Steve (Scared) (professional) - Resemble Voice',
|
||||
value: 'aaa56e79',
|
||||
},
|
||||
{
|
||||
name: 'Tanja (professional) - Resemble Voice',
|
||||
value: 'adb84c77',
|
||||
},
|
||||
{
|
||||
name: 'Tanja (Telephonic) (professional) - Resemble Voice',
|
||||
value: '4f5a470b',
|
||||
},
|
||||
{
|
||||
name: 'Tanja (Warm Word Weaver) (professional) - Resemble Voice',
|
||||
value: 'abbbc383',
|
||||
},
|
||||
{
|
||||
name: 'Tarkos (professional) - Resemble Voice',
|
||||
value: '779842bf',
|
||||
},
|
||||
{
|
||||
name: 'Tyler (professional) - Resemble Voice',
|
||||
value: 'ff225977',
|
||||
},
|
||||
{
|
||||
name: 'Vicky (professional) - Resemble Voice',
|
||||
value: 'f453b918',
|
||||
},
|
||||
{
|
||||
name: 'Vivian (Legacy) (professional) - Resemble Voice',
|
||||
value: 'bed1044d',
|
||||
},
|
||||
{
|
||||
name: 'Vivian (professional) - Resemble Voice',
|
||||
value: '1ff0045f',
|
||||
},
|
||||
{
|
||||
name: 'William (Whispering) (Legacy) (professional) - Resemble Voice',
|
||||
value: '79eb7953',
|
||||
},
|
||||
{
|
||||
name: 'William (Whispering) (professional) - Resemble Voice',
|
||||
value: 'e2180df0',
|
||||
},
|
||||
{
|
||||
name: 'Willow (Whispering) (professional) - Resemble Voice',
|
||||
value: 'f2906c4a',
|
||||
},
|
||||
{
|
||||
name: 'Willow II (Whispering) (professional) - Resemble Voice',
|
||||
value: 'c815cd7a',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -19,7 +19,9 @@ const TtsElevenlabsLanguagesVoices = require('./speech-data/tts-elevenlabs');
|
||||
const TtsWhisperLanguagesVoices = require('./speech-data/tts-whisper');
|
||||
const TtsPlayHtLanguagesVoices = require('./speech-data/tts-playht');
|
||||
const TtsVerbioLanguagesVoices = require('./speech-data/tts-verbio');
|
||||
const TtsInworldLanguagesVoices = require('./speech-data/tts-inworld');
|
||||
const ttsCartesia = require('./speech-data/tts-cartesia');
|
||||
const TtsResembleLanguagesVoices = require('./speech-data/tts-resemble');
|
||||
|
||||
const TtsModelDeepgram = require('./speech-data/tts-model-deepgram');
|
||||
const TtsLanguagesDeepgram = require('./speech-data/tts-deepgram');
|
||||
@@ -28,6 +30,7 @@ const TtsModelWhisper = require('./speech-data/tts-model-whisper');
|
||||
const TtsModelPlayHT = require('./speech-data/tts-model-playht');
|
||||
const ttsLanguagesPlayHt = require('./speech-data/tts-languages-playht');
|
||||
const TtsModelRimelabs = require('./speech-data/tts-model-rimelabs');
|
||||
const TtsModelInworld = require('./speech-data/tts-model-inworld');
|
||||
const TtsModelCartesia = require('./speech-data/tts-model-cartesia');
|
||||
const TtsModelOpenai = require('./speech-data/tts-model-openai');
|
||||
|
||||
@@ -48,6 +51,13 @@ const SttOpenaiLanguagesVoices = require('./speech-data/stt-openai');
|
||||
|
||||
|
||||
const SttModelOpenai = require('./speech-data/stt-model-openai');
|
||||
const sttModelDeepgram = require('./speech-data/stt-model-deepgram');
|
||||
const sttModelCartesia = require('./speech-data/stt-model-cartesia');
|
||||
|
||||
function capitalizeFirst(str) {
|
||||
if (!str) return str;
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
|
||||
|
||||
const testSonioxStt = async(logger, credentials) => {
|
||||
@@ -375,6 +385,28 @@ const testRimelabs = async(logger, synthAudio, credentials) => {
|
||||
}
|
||||
};
|
||||
|
||||
const testInworld = async(logger, synthAudio, credentials) => {
|
||||
try {
|
||||
await synthAudio(
|
||||
{
|
||||
increment: () => {},
|
||||
histogram: () => {}
|
||||
},
|
||||
{
|
||||
vendor: 'inworld',
|
||||
credentials,
|
||||
language: 'en',
|
||||
voice: 'Ashley',
|
||||
text: 'Hi there and welcome to jambones!',
|
||||
renderForCaching: true
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
logger.info({err}, 'synth inworld returned error');
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const testWhisper = async(logger, synthAudio, credentials) => {
|
||||
try {
|
||||
await synthAudio({increment: () => {}, histogram: () => {}},
|
||||
@@ -393,6 +425,24 @@ const testWhisper = async(logger, synthAudio, credentials) => {
|
||||
}
|
||||
};
|
||||
|
||||
const testResembleTTS = async(logger, synthAudio, credentials) => {
|
||||
try {
|
||||
await synthAudio({increment: () => {}, histogram: () => {}},
|
||||
{
|
||||
vendor: 'resemble',
|
||||
credentials,
|
||||
language: 'en-US',
|
||||
voice: '3f5fb9f1',
|
||||
text: 'Hi there and welcome to jambones!',
|
||||
renderForCaching: true
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
logger.info({err}, 'synth resemble returned error');
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const testDeepgramTTS = async(logger, synthAudio, credentials) => {
|
||||
try {
|
||||
await synthAudio({increment: () => {}, histogram: () => {}},
|
||||
@@ -605,7 +655,6 @@ function decryptCredential(obj, credential, logger, isObscureKey = true) {
|
||||
obj.role_arn = o.role_arn;
|
||||
obj.secret_access_key = isObscureKey ? obscureKey(o.secret_access_key) : o.secret_access_key;
|
||||
obj.aws_region = o.aws_region;
|
||||
logger.info({obj, o}, 'retrieving aws speech credential');
|
||||
}
|
||||
else if ('microsoft' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
@@ -617,7 +666,6 @@ function decryptCredential(obj, credential, logger, isObscureKey = true) {
|
||||
obj.use_custom_stt = o.use_custom_stt;
|
||||
obj.custom_stt_endpoint = o.custom_stt_endpoint;
|
||||
obj.custom_stt_endpoint_url = o.custom_stt_endpoint_url;
|
||||
logger.info({obj, o}, 'retrieving azure speech credential');
|
||||
}
|
||||
else if ('wellsaid' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
@@ -636,6 +684,11 @@ function decryptCredential(obj, credential, logger, isObscureKey = true) {
|
||||
obj.deepgram_stt_uri = o.deepgram_stt_uri;
|
||||
obj.deepgram_stt_use_tls = o.deepgram_stt_use_tls;
|
||||
obj.deepgram_tts_uri = o.deepgram_tts_uri;
|
||||
obj.model_id = o.model_id;
|
||||
}
|
||||
else if ('deepgramriver' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.api_key = isObscureKey ? obscureKey(o.api_key) : o.api_key;
|
||||
}
|
||||
else if ('ibm' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
@@ -670,6 +723,12 @@ function decryptCredential(obj, credential, logger, isObscureKey = true) {
|
||||
obj.playht_tts_uri = o.playht_tts_uri;
|
||||
obj.options = o.options;
|
||||
} else if ('cartesia' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.api_key = isObscureKey ? obscureKey(o.api_key) : o.api_key;
|
||||
obj.model_id = o.model_id;
|
||||
obj.stt_model_id = o.stt_model_id;
|
||||
obj.options = o.options;
|
||||
} else if ('inworld' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.api_key = isObscureKey ? obscureKey(o.api_key) : o.api_key;
|
||||
obj.model_id = o.model_id;
|
||||
@@ -688,6 +747,12 @@ function decryptCredential(obj, credential, logger, isObscureKey = true) {
|
||||
} else if ('assemblyai' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.api_key = isObscureKey ? obscureKey(o.api_key) : o.api_key;
|
||||
obj.service_version = o.service_version;
|
||||
} else if ('resemble' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.api_key = isObscureKey ? obscureKey(o.api_key) : o.api_key;
|
||||
obj.resemble_tts_uri = o.resemble_tts_uri;
|
||||
obj.resemble_tts_use_tls = o.resemble_tts_use_tls;
|
||||
} else if ('voxist' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.api_key = isObscureKey ? obscureKey(o.api_key) : o.api_key;
|
||||
@@ -756,6 +821,10 @@ async function getLanguagesAndVoicesForVendor(logger, vendor, credential, getTts
|
||||
return await getLanguagesVoicesForPlayHT(credential, getTtsVoices, logger);
|
||||
case 'rimelabs':
|
||||
return await getLanguagesVoicesForRimelabs(credential, getTtsVoices, logger);
|
||||
case 'inworld':
|
||||
return await getLanguagesVoicesForInworld(credential, getTtsVoices, logger);
|
||||
case 'resemble':
|
||||
return await getLanguagesAndVoicesForResemble(credential, getTtsVoices, logger);
|
||||
case 'assemblyai':
|
||||
return await getLanguagesVoicesForAssemblyAI(credential, getTtsVoices, logger);
|
||||
case 'voxist':
|
||||
@@ -815,9 +884,10 @@ async function getLanguagesVoicesForAws(credential, getTtsVoices, logger) {
|
||||
|
||||
async function getLanguagesVoicesForMicrosoft(credential, getTtsVoices, logger) {
|
||||
if (credential) {
|
||||
const response = await fetch('https://westus.tts.speech.microsoft.com/cognitiveservices/voices/list', {
|
||||
const {region, api_key} = credential;
|
||||
const response = await fetch(`https://${region}.tts.speech.microsoft.com/cognitiveservices/voices/list`, {
|
||||
headers: {
|
||||
'Ocp-Apim-Subscription-Key': credential.api_key
|
||||
'Ocp-Apim-Subscription-Key': api_key
|
||||
}
|
||||
});
|
||||
if (!response.ok) {
|
||||
@@ -851,8 +921,47 @@ async function getLanguagesVoicesForNuane(credential, getTtsVoices, logger) {
|
||||
return tranform(TtsNuanceLanguagesVoices, SttNuanceLanguagesVoices);
|
||||
}
|
||||
|
||||
async function getLanguagesVoicesForDeepgram(credential) {
|
||||
return tranform(TtsLanguagesDeepgram, SttDeepgramLanguagesVoices, TtsModelDeepgram);
|
||||
async function getLanguagesVoicesForDeepgram(credential, getTtsVoices, logger) {
|
||||
if (credential) {
|
||||
const {model_id, api_key, deepgram_stt_uri, deepgram_tts_uri} = credential;
|
||||
// currently just fetching STT and TTS models from Deepgram cloud
|
||||
if (!deepgram_stt_uri && !deepgram_tts_uri) {
|
||||
const response = await fetch('https://api.deepgram.com/v1/models', {
|
||||
headers: {
|
||||
'Authorization': `Token ${api_key}`
|
||||
}
|
||||
});
|
||||
if (!response.ok) {
|
||||
logger.error({response}, 'Error fetching Deepgram voices');
|
||||
throw new Error('failed to list voices');
|
||||
}
|
||||
const {stt, tts} = await response.json();
|
||||
let sttLangs = SttDeepgramLanguagesVoices;
|
||||
const sttModels = Array.from(
|
||||
new Map(
|
||||
stt.map((m) => [m.canonical_name, { name: capitalizeFirst(m.canonical_name), value: m.canonical_name }])
|
||||
).values()
|
||||
).sort((a, b) => a.name.localeCompare(b.name));
|
||||
const ttsModels = Array.from(
|
||||
new Map(
|
||||
tts.map((m) => [m.canonical_name, { name: capitalizeFirst(m.canonical_name), value: m.canonical_name }])
|
||||
).values()
|
||||
).sort((a, b) => a.name.localeCompare(b.name));
|
||||
// if model_id is not provided, return all models, all voices, all languages
|
||||
if (!model_id) {
|
||||
return tranform(TtsLanguagesDeepgram, sttLangs, ttsModels, sttModels);
|
||||
}
|
||||
|
||||
const selectedSttModel = stt.find((m) => m.canonical_name === model_id);
|
||||
const selectedSttLangs = selectedSttModel ? selectedSttModel.languages : [];
|
||||
sttLangs = SttDeepgramLanguagesVoices.filter((l) => {
|
||||
return selectedSttLangs.includes(l.value);
|
||||
});
|
||||
return tranform(TtsLanguagesDeepgram, sttLangs, ttsModels, sttModels);
|
||||
}
|
||||
}
|
||||
return tranform(TtsLanguagesDeepgram, SttDeepgramLanguagesVoices,
|
||||
TtsModelDeepgram, sttModelDeepgram.sort((a, b) => a.name.localeCompare(b.name)));
|
||||
}
|
||||
|
||||
async function getLanguagesVoicesForIbm(credential, getTtsVoices, logger) {
|
||||
@@ -1072,15 +1181,55 @@ async function getLanguagesVoicesForRimelabs(credential) {
|
||||
Object.keys(voices).length > 0 ? voices[Object.keys(voices)[0]] : [];
|
||||
const ttsVoices = Object.entries(modelVoices).map(([key, voices]) => ({
|
||||
value: key,
|
||||
name: key.charAt(0).toUpperCase() + key.slice(1),
|
||||
name: capitalizeFirst(key),
|
||||
voices: voices.map((v) => ({
|
||||
name: v.charAt(0).toUpperCase() + v.slice(1),
|
||||
name: capitalizeFirst(v),
|
||||
value: v
|
||||
}))
|
||||
}));
|
||||
return tranform(ttsVoices, undefined, TtsModelRimelabs);
|
||||
}
|
||||
|
||||
async function getLanguagesVoicesForInworld(credential) {
|
||||
const api_key = credential ? credential.api_key : null;
|
||||
if (!api_key) {
|
||||
return tranform(TtsInworldLanguagesVoices, undefined, TtsModelInworld);
|
||||
}
|
||||
const response = await fetch('https://api.inworld.ai/tts/v1/voices', {
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Basic ${api_key}`
|
||||
}
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('failed to list models');
|
||||
}
|
||||
const data = await response.json();
|
||||
|
||||
const ttsVoices = data.voices.reduce((acc, voice) => {
|
||||
// Process each language for this voice
|
||||
voice.languages.forEach((languageCode) => {
|
||||
const existingLanguage = acc.find((lang) => lang.value === languageCode);
|
||||
const voiceEntry = {
|
||||
name: voice.displayName || capitalizeFirst(voice.voiceId),
|
||||
value: voice.voiceId
|
||||
};
|
||||
|
||||
if (existingLanguage) {
|
||||
existingLanguage.voices.push(voiceEntry);
|
||||
} else {
|
||||
acc.push({
|
||||
value: languageCode,
|
||||
name: capitalizeFirst(languageCode),
|
||||
voices: [voiceEntry]
|
||||
});
|
||||
}
|
||||
});
|
||||
return acc;
|
||||
}, []);
|
||||
return tranform(ttsVoices, undefined, TtsModelInworld);
|
||||
}
|
||||
|
||||
async function getLanguagesVoicesForAssemblyAI(credential) {
|
||||
return tranform(undefined, SttAssemblyaiLanguagesVoices);
|
||||
}
|
||||
@@ -1117,6 +1266,82 @@ async function getLanguagesVoicesForVerbio(credentials, getTtsVoices, logger) {
|
||||
}
|
||||
}
|
||||
|
||||
async function getLanguagesAndVoicesForResemble(credential, getTtsVoices, logger) {
|
||||
if (credential) {
|
||||
try {
|
||||
const {api_key} = credential;
|
||||
let allVoices = [];
|
||||
let page = 1;
|
||||
let hasMorePages = true;
|
||||
// Fetch all pages of voices
|
||||
while (hasMorePages) {
|
||||
const response = await fetch(`https://app.resemble.ai/api/v2/voices?page=${page}&page_size=100`, {
|
||||
headers: {
|
||||
'Authorization': `Token token=${api_key}`,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('failed to list voices');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.success) {
|
||||
throw new Error('API returned unsuccessful response');
|
||||
}
|
||||
|
||||
allVoices = allVoices.concat(data.items);
|
||||
|
||||
// Check if there are more pages
|
||||
hasMorePages = page < data.num_pages;
|
||||
page++;
|
||||
}
|
||||
|
||||
// Filter only finished voices that support text_to_speech
|
||||
const availableVoices = allVoices.filter((voice) =>
|
||||
voice.status === 'finished' &&
|
||||
voice.component_status?.text_to_speech?.status === 'ready'
|
||||
);
|
||||
|
||||
// Group voices by language
|
||||
const ttsVoices = availableVoices.reduce((acc, voice) => {
|
||||
const languageCode = voice.default_language || 'en-US';
|
||||
const existingLanguage = acc.find((lang) => lang.value === languageCode);
|
||||
|
||||
const voiceEntry = {
|
||||
name: `${voice.name} (${voice.voice_type}) - ${voice.source}`,
|
||||
value: voice.uuid
|
||||
};
|
||||
|
||||
if (existingLanguage) {
|
||||
existingLanguage.voices.push(voiceEntry);
|
||||
} else {
|
||||
|
||||
acc.push({
|
||||
value: languageCode,
|
||||
name: capitalizeFirst(languageCode),
|
||||
voices: [voiceEntry]
|
||||
});
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
// Sort languages and voices
|
||||
ttsVoices.sort((a, b) => a.name.localeCompare(b.name));
|
||||
ttsVoices.forEach((lang) => {
|
||||
lang.voices.sort((a, b) => a.name.localeCompare(b.name));
|
||||
});
|
||||
return tranform(ttsVoices);
|
||||
} catch (err) {
|
||||
logger.info('Error while fetching Resemble languages, voices, return predefined values', err);
|
||||
}
|
||||
}
|
||||
|
||||
return tranform(TtsResembleLanguagesVoices);
|
||||
}
|
||||
|
||||
function tranform(tts, stt, models, sttModels) {
|
||||
return {
|
||||
...(tts && {tts}),
|
||||
@@ -1356,9 +1581,23 @@ async function getLanguagesVoicesForCartesia(credential) {
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
return tranform(ttsVoices, undefined, TtsModelCartesia);
|
||||
return tranform(
|
||||
ttsVoices,
|
||||
ttsVoices.map((voice) => ({
|
||||
name: voice.name,
|
||||
value: voice.value,
|
||||
})),
|
||||
TtsModelCartesia,
|
||||
sttModelCartesia);
|
||||
}
|
||||
return tranform(ttsCartesia, undefined, TtsModelCartesia);
|
||||
return tranform(
|
||||
ttsCartesia,
|
||||
ttsCartesia.map((voice) => ({
|
||||
name: voice.name,
|
||||
value: voice.value,
|
||||
})),
|
||||
TtsModelCartesia,
|
||||
sttModelCartesia);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
@@ -1379,6 +1618,7 @@ module.exports = {
|
||||
testElevenlabs,
|
||||
testPlayHT,
|
||||
testRimelabs,
|
||||
testInworld,
|
||||
testAssemblyStt,
|
||||
testDeepgramTTS,
|
||||
getSpeechCredential,
|
||||
@@ -1390,5 +1630,6 @@ module.exports = {
|
||||
testSpeechmaticsStt,
|
||||
testCartesia,
|
||||
testVoxistStt,
|
||||
testOpenAiStt
|
||||
testOpenAiStt,
|
||||
testResembleTTS
|
||||
};
|
||||
|
||||
7941
package-lock.json
generated
7941
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jambonz-api-server",
|
||||
"version": "0.9.4",
|
||||
"version": "0.9.5",
|
||||
"description": "",
|
||||
"main": "app.js",
|
||||
"scripts": {
|
||||
@@ -30,10 +30,10 @@
|
||||
"@jambonz/db-helpers": "^0.9.12",
|
||||
"@jambonz/lamejs": "^1.2.2",
|
||||
"@jambonz/mw-registrar": "^0.2.7",
|
||||
"@jambonz/realtimedb-helpers": "^0.8.13",
|
||||
"@jambonz/speech-utils": "^0.2.10",
|
||||
"@jambonz/realtimedb-helpers": "^0.8.15",
|
||||
"@jambonz/speech-utils": "^0.2.17",
|
||||
"@jambonz/time-series": "^0.2.8",
|
||||
"@jambonz/verb-specifications": "^0.0.104",
|
||||
"@jambonz/verb-specifications": "^0.0.111",
|
||||
"@soniox/soniox-node": "^1.2.2",
|
||||
"ajv": "^8.17.1",
|
||||
"argon2": "^0.40.1",
|
||||
|
||||
@@ -266,6 +266,19 @@ test('account tests', async(t) => {
|
||||
t.ok(err.statusCode === 400, 'returns 400 bad request if account sid param is not a valid uuid');
|
||||
}
|
||||
|
||||
/* try to fetch Alerts with an invalid account SID */
|
||||
try {
|
||||
result = await request.get(`/Accounts/INVALID/Alerts?page=1&count=1`, {
|
||||
auth: {bearer: accountLevelToken},
|
||||
resolveWithFullResponse: true,
|
||||
json: true
|
||||
});
|
||||
t.fail('Expected request to fail with invalid account SID');
|
||||
console.log(result)
|
||||
} catch (err) {
|
||||
t.ok(err.statusCode === 400, 'returns 400 bad request if account sid param is not a valid uuid');
|
||||
}
|
||||
|
||||
/* query all limits for an account */
|
||||
result = await request.get(`/Accounts/${sid}/Limits`, {
|
||||
auth: authAdmin,
|
||||
@@ -337,6 +350,7 @@ test('account tests', async(t) => {
|
||||
await deleteObjectBySid(request, '/VoipCarriers', voip_carrier_sid);
|
||||
await deleteObjectBySid(request, '/ServiceProviders', service_provider_sid);
|
||||
//t.end();
|
||||
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
|
||||
@@ -717,6 +717,28 @@ test('speech credentials tests', async(t) => {
|
||||
t.ok(result.statusCode === 204, 'successfully deleted speech credential for rimelabs');
|
||||
|
||||
|
||||
result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, {
|
||||
resolveWithFullResponse: true,
|
||||
auth: authUser,
|
||||
json: true,
|
||||
body: {
|
||||
vendor: 'inworld',
|
||||
use_for_stt: false,
|
||||
use_for_tts: true,
|
||||
api_key: 'asdasdasdasddsadasda',
|
||||
model_id: 'inworld-tts-1',
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 201, 'successfully added speech credential for inworld');
|
||||
const inworld_sid = result.body.sid;
|
||||
|
||||
/* delete the credential */
|
||||
result = await request.delete(`/Accounts/${account_sid}/SpeechCredentials/${inworld_sid}`, {
|
||||
auth: authUser,
|
||||
resolveWithFullResponse: true,
|
||||
});
|
||||
t.ok(result.statusCode === 204, 'successfully deleted speech credential for inworld');
|
||||
|
||||
/* add a credential for custom voices google */
|
||||
result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, {
|
||||
resolveWithFullResponse: true,
|
||||
@@ -769,7 +791,8 @@ test('speech credentials tests', async(t) => {
|
||||
body: {
|
||||
vendor: 'assemblyai',
|
||||
use_for_stt: true,
|
||||
api_key: "APIKEY"
|
||||
api_key: "APIKEY",
|
||||
service_version: 'v2'
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 201, 'successfully added speech credential for assemblyai');
|
||||
@@ -879,6 +902,50 @@ test('speech credentials tests', async(t) => {
|
||||
});
|
||||
t.ok(result.statusCode === 204, 'successfully deleted speech credential');
|
||||
|
||||
/* add a credential for resemble */
|
||||
result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, {
|
||||
resolveWithFullResponse: true,
|
||||
auth: authUser,
|
||||
json: true,
|
||||
body: {
|
||||
vendor: 'resemble',
|
||||
use_for_tts: true,
|
||||
use_for_stt: false,
|
||||
api_key: 'api_key',
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 201, 'successfully added speech credential for Resemble');
|
||||
const resembleSid = result.body.sid;
|
||||
|
||||
/* delete the credential */
|
||||
result = await request.delete(`/Accounts/${account_sid}/SpeechCredentials/${resembleSid}`, {
|
||||
auth: authUser,
|
||||
resolveWithFullResponse: true,
|
||||
});
|
||||
t.ok(result.statusCode === 204, 'successfully deleted speech credential for Resemble');
|
||||
|
||||
/* add a credential for deepgram river */
|
||||
result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, {
|
||||
resolveWithFullResponse: true,
|
||||
auth: authUser,
|
||||
json: true,
|
||||
body: {
|
||||
vendor: 'deepgramriver',
|
||||
use_for_tts: false,
|
||||
use_for_stt: true,
|
||||
api_key: 'api_key',
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 201, 'successfully added speech credential for Verbio');
|
||||
const deepgramriverSid = result.body.sid;
|
||||
|
||||
/* delete the credential */
|
||||
result = await request.delete(`/Accounts/${account_sid}/SpeechCredentials/${deepgramriverSid}`, {
|
||||
auth: authUser,
|
||||
resolveWithFullResponse: true,
|
||||
});
|
||||
t.ok(result.statusCode === 204, 'successfully deleted speech credential deepgramriver');
|
||||
|
||||
/* Check google supportedLanguagesAndVoices */
|
||||
result = await request.get(`/Accounts/${account_sid}/SpeechCredentials/speech/supportedLanguagesAndVoices?vendor=google`, {
|
||||
resolveWithFullResponse: true,
|
||||
|
||||
Reference in New Issue
Block a user