Compare commits

..

42 Commits

Author SHA1 Message Date
Hoan Luu Huu
d872d9ee87 suport voip carrier sip_proxy (#389)
* suport voip carrier sip_proxy

* fix review comment
2025-02-17 09:46:20 -05:00
Dave Horton
d81a0167cf scripts for import and export of database 2025-02-12 08:28:43 -05:00
Hoan Luu Huu
c7c56d8ea0 update speech utils version (#388) 2025-02-07 08:00:21 -05:00
Hoan Luu Huu
9cfe990bb8 support rimelabs new voices (#387) 2025-02-07 07:22:42 -05:00
Hoan Luu Huu
6c7d2c9074 support rime mistv2 model (#386) 2025-02-06 21:36:34 -05:00
Hoan Luu Huu
73e35c84c5 support voxist stt (#384) 2025-02-05 08:32:36 -05:00
Dave Horton
86d50d94cb optionally send invites to our slack channel on signup (#385) 2025-02-05 00:06:58 -05:00
Hoan Luu Huu
b8f4ad6b27 support fetching elevenlabs models by api-key (#383) 2025-02-03 08:28:48 -05:00
Dave Horton
ad3ec926ee proper version 2025-02-02 17:11:33 -05:00
rammohan-y
66bd9a442c feat/379: added tts property to deepgram response (#380) 2025-01-21 08:15:32 -05:00
Hoan Luu Huu
fa81d179a1 verbio list voice api is changed, fixed (#378) 2025-01-15 06:54:55 -05:00
Dave Horton
fab8a391b7 update deps 2025-01-14 10:46:42 -05:00
Hoan Luu Huu
89288acf6e support custom tts streaming vendor (#377) 2025-01-14 07:14:07 -05:00
Markus Frindt
23cd4408a5 Feat/obscure sensitive bucket credentials (#375)
* obscure sensitive bucket credentials

* npm audit fix

* fix condition

* add test suite encrypt-decrypt.test

* revert docker-compose

* update pipeline

---------

Co-authored-by: mfrindt <m.frindt@cognigy.com>
2025-01-03 07:12:44 -05:00
Dave Horton
ce4618523c correct clients.password col name (#373)
* correct clients.password col name

* fix #372
2024-12-28 16:42:32 -05:00
Hoan Luu Huu
0eb8097e32 support tts cartesia (#370)
* support tts cartesia

* update speech utils

* revert reset password

* revert serve-integration
2024-12-19 09:19:28 -05:00
rammohan-y
8851b3fac0 feat/367 added support for name query parameter for retrieving application (#368)
* feat/367 added support for name query parameter for getting application by name

* feat/367 - Updated test case and modified retrieveAll method to make name independent of service_provider_id and account_sid

* updated query to use WHERE 1=1 to avoid whereFlag variable

* feat/367: removed empty line added accidently
2024-12-18 08:18:48 -05:00
Dave Horton
e080118b6a minor logging changes 2024-12-13 10:47:25 -05:00
rammohan-y
75c27e3f80 feat/982: fixed issue where vendor's API is not called if vendor is defined at service provider level, and also made format of name consistent (#365) 2024-12-12 17:06:45 -05:00
Dave Horton
843980c7f6 #361 (#362)
* #361

* fix typo in db upgrade script

---------

Co-authored-by: Hoan Luu Huu <110280845+xquanluu@users.noreply.github.com>
2024-11-26 20:25:25 -05:00
Hoan Luu Huu
f9990da468 allow dub as http updateCall request (#363) 2024-11-14 07:20:10 -05:00
Hoan Luu Huu
e8d5655abb update speech utils version (#360) 2024-11-04 08:07:43 -05:00
Hoan Luu Huu
e908f5830c support add google voice cloning key (#358)
* support add google voice cloning key

* update upgrade db script

* wip
2024-11-04 07:10:42 -05:00
Dave Horton
5c7bac91a8 update to latest speech-utils 2024-10-18 12:29:05 -04:00
Hoan Luu Huu
de250c8d58 support playht3.0 languages (#357)
* support playht3.0 languages

* update speech utils version
2024-10-16 07:34:02 -04:00
Dave Horton
84d83a0a48 playht test failing due to use of incorrect language name en-US (#356) 2024-10-14 20:58:23 -04:00
Hoan Luu Huu
b5bede7a08 add support for speechmatics languages and voices (#355) 2024-10-11 19:54:22 -04:00
Hoan Luu Huu
6e779f6744 support stt speechmatics (#353)
* support stt speechmatics

* support speechmatics region authentication

* update testcase for speechmatics_stt_uri
2024-10-11 09:17:40 -04:00
Hoan Luu Huu
77b9ca4cba update speech version 0.1.18 (#354) 2024-10-11 08:42:13 -04:00
Hoan Luu Huu
0451b6982c Merge pull request #350 from jambonz/feat/playht30
support playht3.0
2024-10-10 10:40:57 +07:00
Hoan Luu Huu
71adc577e9 Merge branch 'main' into feat/playht30 2024-10-10 10:38:13 +07:00
Hoan Luu Huu
e8b32103fe update speech version (#352) 2024-10-09 19:44:43 -04:00
Hoan Luu Huu
57d8d0a02c allow system information contains log level and account has enable_debug_log (#351)
* allow system information contains log level and account has enable_debug_log

* update upgrade db script
2024-10-07 09:52:11 -04:00
Quan HL
a41760fa9f PlayHT version 3.0 support PlayHt2.0 voices 2024-10-03 13:00:42 +07:00
Quan HL
c6bae80a03 support playht3.0 2024-09-27 11:13:59 +07:00
Dave Horton
4cddbd83a1 update to version of realtime-db with fix for expires (#349) 2024-09-18 08:24:47 -04:00
Dave Horton
6275aac341 bump version 2024-09-04 13:34:52 +01:00
Hoan Luu Huu
52de41c9bc support configuration to limit minimum value sipgatewa netmask can be used (#344) 2024-08-19 21:44:15 -04:00
Dave Horton
ed71abd675 added private_newtwork_cidr to system_information table (#341)
* added private_newtwork_cidr to system_information table

* db schema upgrade to add system_information.private_network_cidr in 0.9.2

* increase size of system_information.private_network_cidr to varchar(8192)
2024-08-18 12:49:06 -04:00
Hoan Luu Huu
2d2b98dab5 Feat/deepgram tts onprem (#338)
* support deepgram onpremise

* wip

* update speech utils version

* install docker in ci
2024-08-07 07:24:58 -04:00
Hoan Luu Huu
7553e2b617 update mysql2 version (#339) 2024-08-06 11:42:33 -04:00
Hoan Luu Huu
b921cab867 Support elevenlabs 2.5 (#336)
* update elevenlab model

* wip

* wip

* wip
2024-07-24 13:04:12 -04:00
44 changed files with 3872 additions and 1422 deletions

View File

@@ -7,11 +7,22 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install Docker Compose
run: |
sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
docker-compose --version
- uses: actions/setup-node@v3
with:
node-version: lts/*
- run: npm install
- run: npm run jslint
- name: Install Docker Compose
run: |
sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
docker-compose --version
- run: npm test
- run: npm run test:encrypt-decrypt

2
app.js
View File

@@ -222,7 +222,7 @@ server.on('upgrade', (request, socket, head) => {
/* complete the upgrade */
wsServer.handleUpgrade(request, socket, head, (ws) => {
logger.info(`upgraded to websocket, url: ${request.url}`);
logger.debug(`upgraded to websocket, url: ${request.url}`);
wsServer.emit('connection', ws, request.url);
});
});

22
db/export_jambonz.sh Executable file
View File

@@ -0,0 +1,22 @@
#!/bin/bash
# This script exports the 'jambones' database (schema and data)
# from the source MySQL server into a file.
# Configuration variables
SOURCE_HOST=
DB_USER=
DB_PASS=
DB_NAME=
EXPORT_FILE="jambones_export.sql"
# Export the database using mysqldump
echo "Exporting database '$DB_NAME' from $SOURCE_HOST..."
mysqldump -h "$SOURCE_HOST" -u "$DB_USER" -p"$DB_PASS" "$DB_NAME" > "$EXPORT_FILE"
# Check for errors
if [ $? -eq 0 ]; then
echo "Database export successful. Export file created: $EXPORT_FILE"
else
echo "Error exporting database '$DB_NAME'."
exit 1
fi

31
db/import_jambonz.sh Executable file
View File

@@ -0,0 +1,31 @@
#!/bin/bash
# This script imports the SQL dump file into the target MySQL server.
# It first drops the existing 'jambones' database (if it exists),
# recreates it, and then imports the dump file.
# Configuration variables
TARGET_HOST=
DB_USER=
DB_PASS=
DB_NAME=
IMPORT_FILE="jambones_export.sql"
# Drop the existing database (if any) and create a new one
echo "Dropping and recreating database '$DB_NAME' on $TARGET_HOST..."
mysql -h "$TARGET_HOST" -u "$DB_USER" -p"$DB_PASS" -e "DROP DATABASE IF EXISTS \`$DB_NAME\`; CREATE DATABASE \`$DB_NAME\`;"
if [ $? -ne 0 ]; then
echo "Error dropping/creating database '$DB_NAME'."
exit 1
fi
# Import the SQL dump into the newly created database
echo "Importing dump file '$IMPORT_FILE' into database '$DB_NAME'..."
mysql -h "$TARGET_HOST" -u "$DB_USER" -p"$DB_PASS" "$DB_NAME" < "$IMPORT_FILE"
if [ $? -eq 0 ]; then
echo "Database import successful."
else
echo "Error importing the database."
exit 1
fi

View File

@@ -351,6 +351,8 @@ speech_credential_sid CHAR(36) NOT NULL,
model VARCHAR(512) NOT NULL,
reported_usage ENUM('REPORTED_USAGE_UNSPECIFIED','REALTIME','OFFLINE') DEFAULT 'REALTIME',
name VARCHAR(64) NOT NULL,
voice_cloning_key MEDIUMTEXT,
use_voice_cloning_key BOOLEAN DEFAULT false,
PRIMARY KEY (google_custom_voice_sid)
);
@@ -358,7 +360,9 @@ CREATE TABLE system_information
(
domain_name VARCHAR(255),
sip_domain_name VARCHAR(255),
monitoring_domain_name VARCHAR(255)
monitoring_domain_name VARCHAR(255),
private_network_cidr VARCHAR(8192),
log_level ENUM('info', 'debug') NOT NULL DEFAULT 'info'
);
CREATE TABLE users
@@ -412,6 +416,8 @@ register_from_user VARCHAR(128),
register_from_domain VARCHAR(255),
register_public_ip_in_contact BOOLEAN NOT NULL DEFAULT false,
register_status VARCHAR(4096),
dtmf_type ENUM('rfc2833','tones','info') NOT NULL DEFAULT 'rfc2833',
outbound_sip_proxy VARCHAR(255),
PRIMARY KEY (voip_carrier_sid)
) COMMENT='A Carrier or customer PBX that can send or receive calls';
@@ -552,6 +558,7 @@ siprec_hook_sid CHAR(36),
record_all_calls BOOLEAN NOT NULL DEFAULT false,
record_format VARCHAR(16) NOT NULL DEFAULT 'mp3',
bucket_credential VARCHAR(8192) COMMENT 'credential used to authenticate with storage service',
enable_debug_log BOOLEAN NOT NULL DEFAULT false,
PRIMARY KEY (account_sid)
) COMMENT='An enterprise that uses the platform for comm services';

File diff suppressed because one or more lines are too long

View File

@@ -197,6 +197,19 @@ const sql = {
'ALTER TABLE applications MODIFY COLUMN speech_synthesis_voice VARCHAR(256)',
'ALTER TABLE applications MODIFY COLUMN fallback_speech_synthesis_voice VARCHAR(256)',
'ALTER TABLE sip_gateways ADD COLUMN use_sips_scheme BOOLEAN NOT NULL DEFAULT 0',
],
9002: [
'ALTER TABLE system_information ADD COLUMN private_network_cidr VARCHAR(8192)',
'ALTER TABLE system_information ADD COLUMN log_level ENUM(\'info\', \'debug\') NOT NULL DEFAULT \'info\'',
'ALTER TABLE accounts ADD COLUMN enable_debug_log BOOLEAN NOT NULL DEFAULT false',
'ALTER TABLE google_custom_voices ADD COLUMN use_voice_cloning_key BOOLEAN DEFAULT false',
'ALTER TABLE google_custom_voices ADD COLUMN voice_cloning_key MEDIUMTEXT',
],
9003: [
'ALTER TABLE google_custom_voices ADD COLUMN voice_cloning_key MEDIUMTEXT',
'ALTER TABLE google_custom_voices ADD COLUMN use_voice_cloning_key BOOLEAN DEFAULT false',
'ALTER TABLE voip_carriers ADD COLUMN dtmf_type ENUM(\'rfc2833\',\'tones\',\'info\') NOT NULL DEFAULT \'rfc2833\'',
'ALTER TABLE voip_carriers ADD COLUMN sip_proxy VARCHAR(255)',
]
};
@@ -230,6 +243,8 @@ const doIt = async() => {
if (val < 8004) upgrades.push(...sql['8004']);
if (val < 8005) upgrades.push(...sql['8005']);
if (val < 9000) upgrades.push(...sql['9000']);
if (val < 9002) upgrades.push(...sql['9002']);
if (val < 9003) upgrades.push(...sql['9003']);
// perform all upgrades
logger.info({upgrades}, 'applying schema upgrades..');

View File

@@ -56,9 +56,13 @@ AND effective_end_date IS NULL
AND pending = 0`;
const extractBucketCredential = (obj) => {
const {bucket_credential} = obj;
if (bucket_credential) {
obj.bucket_credential = JSON.parse(decrypt(bucket_credential));
try {
const {bucket_credential} = obj;
if (bucket_credential) {
obj.bucket_credential = JSON.parse(decrypt(bucket_credential));
}
} catch (error) {
console.error('Error while decrypting data', error);
}
};

View File

@@ -37,19 +37,24 @@ class Application extends Model {
}
/**
* list all applications - for all service providers, for one service provider, or for one account
* 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) {
let sql = retrieveSql;
static retrieveAll(service_provider_sid, account_sid, name) {
let sql = retrieveSql + ' WHERE 1 = 1';
const args = [];
if (account_sid) {
sql = `${sql} WHERE app.account_sid = ?`;
sql = `${sql} AND app.account_sid = ?`;
args.push(account_sid);
}
else if (service_provider_sid) {
sql = `${sql} WHERE account_sid in (SELECT account_sid from accounts WHERE 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);
}
return new Promise((resolve, reject) => {
getMysqlConnection((err, conn) => {
if (err) return reject(err);

View File

@@ -13,9 +13,9 @@ class Client extends Model {
}
static async retrieveAllByServiceProviderSid(service_provider_sid) {
const sql = `SELECT c.client_sid, c.account_sid, c.is_active, c.username, c.hashed_password
const sql = `SELECT c.client_sid, c.account_sid, c.is_active, c.username, c.password
FROM ${this.table} AS c LEFT JOIN accounts AS acc ON c.account_sid = acc.account_sid
LEFT JOIN service_providers AS sp ON sp.service_provider_sid = accs.service_provider_sid
LEFT JOIN service_providers AS sp ON sp.service_provider_sid = acc.service_provider_sid
WHERE sp.service_provider_sid = ?`;
const [rows] = await promisePool.query(sql, service_provider_sid);
return rows;

View File

@@ -22,13 +22,17 @@ class SpeechCredential extends Model {
static async getSpeechCredentialsByVendorAndLabel(service_provider_sid, account_sid, vendor, label) {
let sql;
let rows = [];
if (account_sid) {
sql = `SELECT * FROM speech_credentials WHERE account_sid = ? AND vendor = ? ${label ? 'AND label = ?' : ''}`;
} else {
sql = `SELECT * FROM speech_credentials WHERE service_provider_sid = ? AND vendor = ?
${label ? 'AND label = ?' : ''}`;
sql = `SELECT * FROM speech_credentials WHERE account_sid = ? AND vendor = ?
AND label ${label ? '= ?' : 'is NULL'}`;
[rows] = await promisePool.query(sql, [account_sid, vendor, label]);
}
if (rows.length === 0) {
sql = `SELECT * FROM speech_credentials WHERE service_provider_sid = ? AND vendor = ?
AND label ${label ? '= ?' : 'is NULL'}`;
[rows] = await promisePool.query(sql, [service_provider_sid, vendor, label]);
}
const [rows] = await promisePool.query(sql, [account_sid ? account_sid : service_provider_sid, vendor, label]);
return rows;
}

View File

@@ -33,6 +33,10 @@ SystemInformation.fields = [
name: 'monitoring_domain_name',
type: 'string',
},
{
name: 'private_network_cidr',
type: 'string',
},
];
module.exports = SystemInformation;

View File

@@ -136,6 +136,14 @@ VoipCarrier.fields = [
{
name: 'register_status',
type: 'string'
},
{
name: 'dtmf_type',
type: 'string'
},
{
name: 'sip_proxy',
type: 'string'
}
];

View File

@@ -19,11 +19,12 @@ const {
parseCallSid,
enableSubspace,
disableSubspace,
parseVoipCarrierSid
parseVoipCarrierSid,
hasValue,
} = require('./utils');
const short = require('short-uuid');
const VoipCarrier = require('../../models/voip-carrier');
const { encrypt } = require('../../utils/encrypt-decrypt');
const { encrypt, obscureBucketCredentialsSensitiveData, isObscureKey } = require('../../utils/encrypt-decrypt');
const { testS3Storage, testGoogleStorage, testAzureStorage } = require('../../utils/storage-utils');
const translator = short();
@@ -42,7 +43,7 @@ const getFsUrl = async(logger, retrieveSet, setName) => {
return ;
}
const f = fs[idx++ % fs.length];
logger.info({fs}, `feature servers available for createCall API request, selecting ${f}`);
logger.debug({fs}, `feature servers available for createCall API request, selecting ${f}`);
return `${f}/v1/createCall`;
} catch (err) {
logger.error({err}, 'getFsUrl: error retreving feature servers from redis');
@@ -266,7 +267,8 @@ function validateUpdateCall(opts) {
'record',
'tag',
'dtmf',
'conferenceParticipantAction'
'conferenceParticipantAction',
'dub'
]
.reduce((acc, prop) => (opts[prop] ? ++acc : acc), 0);
@@ -554,9 +556,12 @@ router.get('/:sid', async(req, res) => {
const account_sid = parseAccountSid(req);
await validateRequest(req, account_sid);
const service_provider_sid = req.user.hasServiceProviderAuth ? req.user.service_provider_sid : null;
const results = await Account.retrieve(account_sid, service_provider_sid);
if (results.length === 0) return res.status(404).end();
return res.status(200).json(results[0]);
const [result] = await Account.retrieve(account_sid, service_provider_sid) || [];
if (!result) return res.status(404).end();
result.bucket_credential = obscureBucketCredentialsSensitiveData(result.bucket_credential);
return res.status(200).json(result);
}
catch (err) {
sysError(logger, res, err);
@@ -638,19 +643,21 @@ router.delete('/:sid/SubspaceTeleport', async(req, res) => {
}
});
function encryptBucketCredential(obj) {
if (!obj.bucket_credential) return;
function encryptBucketCredential(obj, storedCredentials = {}) {
if (!hasValue(obj?.bucket_credential)) return;
const {
vendor,
region,
name,
access_key_id,
secret_access_key,
tags,
service_key,
connection_string,
endpoint
} = obj.bucket_credential;
let {
secret_access_key,
service_key,
connection_string
} = obj.bucket_credential;
switch (vendor) {
case 'aws_s3':
@@ -658,6 +665,9 @@ function encryptBucketCredential(obj) {
assert(secret_access_key, 'invalid aws S3 bucket credential: secret_access_key is required');
assert(name, 'invalid aws bucket name: name is required');
assert(region, 'invalid aws bucket region: region is required');
if (isObscureKey(obj.bucket_credential) && hasValue(storedCredentials)) {
secret_access_key = storedCredentials.secret_access_key;
}
const awsData = JSON.stringify({vendor, region, name, access_key_id,
secret_access_key, tags});
obj.bucket_credential = encrypt(awsData);
@@ -667,18 +677,27 @@ function encryptBucketCredential(obj) {
assert(secret_access_key, 'invalid aws S3 bucket credential: secret_access_key is required');
assert(name, 'invalid aws bucket name: name is required');
assert(endpoint, 'invalid endpoint uri: endpoint is required');
if (isObscureKey(obj.bucket_credential) && hasValue(storedCredentials)) {
secret_access_key = storedCredentials.secret_access_key;
}
const s3Data = JSON.stringify({vendor, endpoint, name, access_key_id,
secret_access_key, tags});
obj.bucket_credential = encrypt(s3Data);
break;
case 'google':
assert(service_key, 'invalid google cloud storage credential: service_key is required');
if (isObscureKey(obj.bucket_credential) && hasValue(storedCredentials)) {
service_key = storedCredentials.service_key;
}
const googleData = JSON.stringify({vendor, name, service_key, tags});
obj.bucket_credential = encrypt(googleData);
break;
case 'azure':
assert(name, 'invalid azure container name: name is required');
assert(connection_string, 'invalid azure cloud storage credential: connection_string is required');
if (isObscureKey(obj.bucket_credential) && hasValue(storedCredentials)) {
connection_string = storedCredentials.connection_string;
}
const azureData = JSON.stringify({vendor, name, connection_string, tags});
obj.bucket_credential = encrypt(azureData);
break;
@@ -736,7 +755,17 @@ router.put('/:sid', async(req, res) => {
delete obj.registration_hook;
delete obj.queue_event_hook;
encryptBucketCredential(obj);
let storedBucketCredentials = {};
if (isObscureKey(obj?.bucket_credential)) {
const [account] = await Account.retrieve(sid) || [];
/* to avoid overwriting valid credentials with the obscured secret,
* that the frontend might send, we pass the stored account bucket credentials
* in the case it is a obscured key, we replace it with the stored one
*/
storedBucketCredentials = account.bucket_credential;
}
encryptBucketCredential(obj, storedBucketCredentials);
const rowsAffected = await Account.update(sid, obj);
if (rowsAffected === 0) {
@@ -836,6 +865,13 @@ router.post('/:sid/BucketCredentialTest', async(req, res) => {
try {
const account_sid = parseAccountSid(req);
await validateRequest(req, account_sid);
/* if the req.body bucket credentials contain an obscured key, replace with stored account.bucket_credential */
if (isObscureKey(req.body)) {
const service_provider_sid = req.user.hasServiceProviderAuth ? req.user.service_provider_sid : null;
const [account] = await Account.retrieve(account_sid, service_provider_sid) || [];
if (!account) return res.status(404).end();
req.body = account.bucket_credential;
}
const {vendor, name, region, access_key_id, secret_access_key, service_key, connection_string, endpoint} = req.body;
const ret = {
status: 'not tested'

View File

@@ -156,7 +156,8 @@ router.get('/', async(req, res) => {
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 results = await Application.retrieveAll(service_provider_sid, account_sid);
const name = req.query.name;
const results = await Application.retrieveAll(service_provider_sid, account_sid, name);
res.status(200).json(results);
} catch (err) {
sysError(logger, res, err);

View File

@@ -4,6 +4,9 @@ const SpeechCredential = require('../../models/speech-credential');
const decorate = require('./decorate');
const {DbErrorBadRequest, DbErrorForbidden} = require('../../utils/errors');
const sysError = require('../error');
const multer = require('multer');
const upload = multer({ dest: '/tmp/csv/' });
const fs = require('fs');
const validateCredentialPermission = async(req) => {
const credential = await SpeechCredential.retrieve(req.body.speech_credential_sid);
@@ -41,6 +44,33 @@ const preconditions = {
decorate(router, GoogleCustomVoice, ['add', 'retrieve', 'update', 'delete'], preconditions);
const voiceCloningKeySubString = (voice_cloning_key) => {
return voice_cloning_key ? voice_cloning_key.substring(0, 100) + '...' : undefined;
};
router.get('/: sid', async(req, res) => {
const logger = req.app.locals.logger;
try {
const {sid} = req.params;
const account_sid = req.user.account_sid;
const service_provider_sid = req.user.service_provider_sid;
const google_voice = await GoogleCustomVoice.retrieve(sid);
google_voice.voice_cloning_key = voiceCloningKeySubString(google_voice.voice_cloning_key);
if (!google_voice) {
return res.sendStatus(404);
}
if (req.user.hasScope('service_provider') && google_voice.service_provider_sid !== service_provider_sid ||
req.user.hasScope('account') && google_voice.account_sid !== account_sid) {
throw new DbErrorForbidden('Insufficient privileges');
}
return res.status(200).json(google_voice);
} catch (err) {
sysError(logger, res, err);
}
});
router.get('/', async(req, res) => {
const logger = req.app.locals.logger;
const account_sid = req.user.account_sid || req.query.account_sid;
@@ -67,7 +97,37 @@ router.get('/', async(req, res) => {
}
results = await GoogleCustomVoice.retrieveAllByLabel(service_provider_sid, account_sid, label);
}
res.status(200).json(results);
res.status(200).json(results.map((r) => {
r.voice_cloning_key = voiceCloningKeySubString(r.voice_cloning_key);
return r;
}));
} catch (err) {
sysError(logger, res, err);
}
});
router.post('/:sid/VoiceCloningKey', upload.single('file'), async(req, res) => {
const {logger} = req.app.locals;
const {sid} = req.params;
const account_sid = req.user.account_sid;
const service_provider_sid = req.user.service_provider_sid;
try {
const google_voice = await GoogleCustomVoice.retrieve(sid);
if (!google_voice) {
return res.sendStatus(404);
}
if (req.user.hasScope('service_provider') && google_voice.service_provider_sid !== service_provider_sid ||
req.user.hasScope('account') && google_voice.account_sid !== account_sid) {
throw new DbErrorForbidden('Insufficient privileges');
}
const voice_cloning_key = Buffer.from(fs.readFileSync(req.file.path)).toString();
await GoogleCustomVoice.update(sid, {
voice_cloning_key
});
fs.unlinkSync(req.file.path);
return res.sendStatus(204);
} catch (err) {
sysError(logger, res, err);
}

View File

@@ -3,7 +3,7 @@ const debug = require('debug')('jambonz:api-server');
const {DbErrorBadRequest, DbErrorUnprocessableRequest} = require('../../utils/errors');
const {promisePool} = require('../../db');
const {doGithubAuth, doGoogleAuth, doLocalAuth} = require('../../utils/oauth-utils');
const {validateEmail} = require('../../utils/email-utils');
const {validateEmail, emailSimpleText} = require('../../utils/email-utils');
const {cacheClient} = require('../../helpers');
const { v4: uuid } = require('uuid');
const short = require('short-uuid');
@@ -12,6 +12,7 @@ const jwt = require('jsonwebtoken');
const {setupFreeTrial, createTestCdrs, createTestAlerts} = require('./utils');
const {generateHashedPassword} = require('../../utils/password-utils');
const sysError = require('../error');
const insertUserSql = `INSERT into users
(user_sid, account_sid, name, email, provider, provider_userid, email_validated)
values (?, ?, ?, ?, ?, ?, 1)`;
@@ -36,6 +37,17 @@ const insertSignupHistorySql = `INSERT into signup_history
(email, name)
values (?, ?)`;
const slackEmail = `Hi there and welcome to jambonz!
We are excited to have you on board. Feel free to join the community on Slack at https://joinslack.jambonz.org,
where you can connect with other jambonz users, ask questions, share your experiences, and learn from others.
Hope to see you there!
Best,
DaveH and the jambonz team`;
const addLocalUser = async(logger, user_sid, account_sid,
name, email, email_activation_code, passwordHash, service_provider_sid) => {
const [r] = await promisePool.execute(insertUserLocalSql,
@@ -324,6 +336,15 @@ router.post('/', async(req, res) => {
tutorial_completion: 0,
scope: 'read-write'
});
// send invite to Slack
if (process.env.SEND_SLACK_INVITE_ON_SIGNUP) {
try {
emailSimpleText(logger, userProfile.email, 'Welcome to jambonz!', slackEmail);
} catch (err) {
logger.info({err}, 'Error sending slack invite');
}
}
}
else if (user_sid) {
/* add a new user for existing account */

View File

@@ -41,6 +41,7 @@ const checkUserScope = async(req, voip_carrier_sid) => {
const validate = async(req, sid) => {
const {lookupSipGatewayBySid} = req.app.locals;
const {netmask} = req.body;
let voip_carrier_sid;
if (sid) {
@@ -52,6 +53,12 @@ const validate = async(req, sid) => {
voip_carrier_sid = req.body.voip_carrier_sid;
if (!voip_carrier_sid) throw new DbErrorBadRequest('missing voip_carrier_sid');
}
if (netmask &&
process.env.JAMBONZ_MIN_GATEWAY_NETMASK &&
parseInt(netmask) < process.env.JAMBONZ_MIN_GATEWAY_NETMASK) {
throw new DbErrorBadRequest(
`netmask required to have value equal or greater than ${process.env.JAMBONZ_MIN_GATEWAY_NETMASK}`);
}
await checkUserScope(req, voip_carrier_sid);
};

View File

@@ -10,7 +10,10 @@ const {decryptCredential, testWhisper, testDeepgramTTS,
testPlayHT,
testRimelabs,
testVerbioTts,
testVerbioStt} = require('../../utils/speech-utils');
testVerbioStt,
testSpeechmaticsStt,
testCartesia,
testVoxistStt} = require('../../utils/speech-utils');
const {DbErrorUnprocessableRequest, DbErrorForbidden, DbErrorBadRequest} = require('../../utils/errors');
const {
testGoogleTts,
@@ -122,8 +125,10 @@ const encryptCredential = (obj) => {
secret,
nuance_tts_uri,
nuance_stt_uri,
speechmatics_stt_uri,
deepgram_stt_uri,
deepgram_stt_use_tls,
deepgram_tts_uri,
use_custom_tts,
custom_tts_endpoint,
custom_tts_endpoint_url,
@@ -138,6 +143,7 @@ const encryptCredential = (obj) => {
instance_id,
custom_stt_url,
custom_tts_url,
custom_tts_streaming_url,
auth_token = '',
cobalt_server_uri,
model_id,
@@ -204,10 +210,10 @@ const encryptCredential = (obj) => {
case 'deepgram':
// API key is optional if onprem
if (!deepgram_stt_uri) {
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});
const deepgramData = JSON.stringify({api_key, deepgram_stt_uri, deepgram_stt_use_tls, deepgram_tts_uri});
return encrypt(deepgramData);
case 'ibm':
@@ -235,6 +241,12 @@ const encryptCredential = (obj) => {
const elevenlabsData = JSON.stringify({api_key, model_id, options});
return encrypt(elevenlabsData);
case 'speechmatics':
assert(api_key, 'invalid speechmatics speech credential: api_key is required');
assert(speechmatics_stt_uri, 'invalid speechmatics speech credential: speechmatics_stt_uri is required');
const speechmaticsData = JSON.stringify({api_key, speechmatics_stt_uri, options});
return encrypt(speechmaticsData);
case 'playht':
assert(api_key, 'invalid playht speech credential: api_key is required');
assert(user_id, 'invalid playht speech credential: user_id is required');
@@ -242,6 +254,12 @@ const encryptCredential = (obj) => {
const playhtData = JSON.stringify({api_key, user_id, voice_engine, options});
return encrypt(playhtData);
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});
return encrypt(cartesiaData);
case 'rimelabs':
assert(api_key, 'invalid rimelabs speech credential: api_key is required');
assert(model_id, 'invalid rimelabs speech credential: model_id is required');
@@ -253,6 +271,11 @@ const encryptCredential = (obj) => {
const assemblyaiData = JSON.stringify({api_key});
return encrypt(assemblyaiData);
case 'voxist':
assert(api_key, 'invalid voxist speech credential: api_key is required');
const voxistData = JSON.stringify({api_key});
return encrypt(voxistData);
case 'whisper':
assert(api_key, 'invalid whisper speech credential: api_key is required');
assert(model_id, 'invalid whisper speech credential: model_id is required');
@@ -268,7 +291,7 @@ const encryptCredential = (obj) => {
default:
if (vendor.startsWith('custom:')) {
const customData = JSON.stringify({auth_token, custom_stt_url, custom_tts_url});
const customData = JSON.stringify({auth_token, custom_stt_url, custom_tts_url, custom_tts_streaming_url});
return encrypt(customData);
}
else assert(false, `invalid or missing vendor: ${vendor}`);
@@ -452,12 +475,14 @@ router.put('/:sid', async(req, res) => {
custom_stt_endpoint_url,
custom_stt_url,
custom_tts_url,
custom_tts_streaming_url,
cobalt_server_uri,
model_id,
voice_engine,
options,
deepgram_stt_uri,
deepgram_stt_use_tls,
deepgram_tts_uri,
engine_version
} = req.body;
@@ -479,12 +504,14 @@ router.put('/:sid', async(req, res) => {
nuance_tts_uri,
custom_stt_url,
custom_tts_url,
custom_tts_streaming_url,
cobalt_server_uri,
model_id,
voice_engine,
options,
deepgram_stt_uri,
deepgram_stt_use_tls,
deepgram_tts_uri,
engine_version
};
logger.info({o, newCred}, 'updating speech credential with this new credential');
@@ -765,6 +792,18 @@ router.get('/:sid/test', async(req, res) => {
SpeechCredential.ttsTestResult(sid, false);
}
}
} else if (cred.vendor === 'speechmatics') {
const {api_key} = credential;
if (cred.use_for_stt) {
try {
await testSpeechmaticsStt(logger, {api_key});
results.stt.status = 'ok';
SpeechCredential.ttsTestResult(sid, true);
} catch (err) {
results.stt = {status: 'fail', reason: err.message};
SpeechCredential.ttsTestResult(sid, false);
}
}
} else if (cred.vendor === 'playht') {
if (cred.use_for_tts) {
try {
@@ -781,6 +820,22 @@ router.get('/:sid/test', async(req, res) => {
SpeechCredential.ttsTestResult(sid, false);
}
}
} else if (cred.vendor === 'cartesia') {
if (cred.use_for_tts) {
try {
await testCartesia(logger, synthAudio, credential);
results.tts.status = 'ok';
SpeechCredential.ttsTestResult(sid, true);
} catch (err) {
let reason = err.message;
// if error is from bent, let get the body
try {
reason = await err.text();
} catch {}
results.tts = {status: 'fail', reason};
SpeechCredential.ttsTestResult(sid, false);
}
}
} else if (cred.vendor === 'rimelabs') {
if (cred.use_for_tts) {
try {
@@ -804,6 +859,18 @@ router.get('/:sid/test', async(req, res) => {
SpeechCredential.sttTestResult(sid, false);
}
}
} else if (cred.vendor === 'voxist') {
const {api_key} = credential;
if (cred.use_for_stt) {
try {
await testVoxistStt(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 === 'whisper') {
if (cred.use_for_tts) {
try {
@@ -852,7 +919,7 @@ router.get('/:sid/test', async(req, res) => {
router.get('/speech/supportedLanguagesAndVoices', async(req, res) => {
const {logger, getTtsVoices} = req.app.locals;
try {
const {vendor, label} = req.query;
const {vendor, label, create_new} = req.query;
if (!vendor) {
throw new DbErrorBadRequest('vendor is required');
}
@@ -860,7 +927,7 @@ router.get('/speech/supportedLanguagesAndVoices', async(req, res) => {
const service_provider_sid = req.user.service_provider_sid ||
req.body.service_provider_sid || parseServiceProviderSid(req);
const credentials = await SpeechCredential.getSpeechCredentialsByVendorAndLabel(
const credentials = create_new ? null : await SpeechCredential.getSpeechCredentialsByVendorAndLabel(
service_provider_sid, account_sid, vendor, label);
const tmp = credentials && credentials.length > 0 ? credentials[0] : null;
const cred = tmp ? JSON.parse(decrypt(tmp.credential)) : null;

View File

@@ -440,6 +440,22 @@ const validatePasswordSettings = async(password) => {
return;
};
function hasValue(data) {
if (typeof data === 'string') {
return data && data.length > 0;
} else if (Array.isArray(data)) {
return data && data.length > 0;
} else if (typeof data === 'object') {
return data && Object.keys(data).length > 0;
} else if (typeof data === 'number') {
return data !== null;
} else if (typeof data === 'boolean') {
return data !== null;
} else {
return false;
}
}
module.exports = {
setupFreeTrial,
createTestCdrs,
@@ -460,5 +476,6 @@ module.exports = {
checkLimits,
enableSubspace,
disableSubspace,
validatePasswordSettings
validatePasswordSettings,
hasValue,
};

View File

@@ -130,71 +130,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
/Accounts/{accountSid}/RecentCalls/{callSid}/record/{year}/{month}/{day}/{format}:
get:
tags:
- Call Records
summary: 'Retrieve a recording of a recent call in specified format'
description: 'Gets the recording of a recent call by specifying the account ID, call ID, the date of the call, and the format of the recording (e.g., MP3 or WAV).'
operationId: getRecordingByFormat
security:
- ApiKeyAuth: [ ]
parameters:
- name: accountSid
in: path
required: true
schema:
type: string
- name: callSid
in: path
required: true
schema:
type: string
- name: year
in: path
required: true
schema:
type: string
- name: month
in: path
required: true
schema:
type: string
- name: day
in: path
required: true
schema:
type: string
- name: format
in: path
required: true
schema:
type: string
enum:
- mp3
- wav
responses:
200:
description: Successfully retrieved the recording file.
content:
audio/*:
schema:
type: string
format: binary
404:
description: Call record not found
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
500:
description: System error
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
$ref: '#/components/schemas/GeneralError'
/Sbcs:
post:
tags:

View File

@@ -17,10 +17,15 @@ const encrypt = (text) => {
};
const decrypt = (data) => {
const hash = JSON.parse(data);
const decipher = crypto.createDecipheriv(algorithm, secretKey, Buffer.from(hash.iv, 'hex'));
const decrpyted = Buffer.concat([decipher.update(Buffer.from(hash.content, 'hex')), decipher.final()]);
return decrpyted.toString();
try {
const hash = JSON.parse(data);
const decipher = crypto.createDecipheriv(algorithm, secretKey, Buffer.from(hash.iv, 'hex'));
const decrpyted = Buffer.concat([decipher.update(Buffer.from(hash.content, 'hex')), decipher.final()]);
return decrpyted.toString();
} catch (error) {
console.error('Error while decrypting data', error);
return '{}';
}
};
const obscureKey = (key, key_spoiler_length = 6) => {
@@ -33,8 +38,85 @@ const obscureKey = (key, key_spoiler_length = 6) => {
return `${key.slice(0, key_spoiler_length)}${key_spoiler_char.repeat(key.length - key_spoiler_length)}`;
};
function isObscureKey(bucketCredentials) {
if (!bucketCredentials) {
return false;
}
try {
const {
vendor,
secret_access_key = '',
service_key = '',
connection_string = ''
} = bucketCredentials || {};
let pattern;
switch (vendor) {
case 'aws_s3':
case 's3_compatible':
pattern = /^([A-Za-z0-9]{4,6}X+$)/;
return pattern.test(secret_access_key);
case 'azure':
pattern = /^https:[A-Za-z0-9\/.:?=&_-]+$/;
return pattern.test(connection_string);
case 'google': {
pattern = /^([A-Za-z0-9]{4,6}X+$)/;
let {private_key} = JSON.parse(service_key);
const key_header = '-----BEGIN PRIVATE KEY-----\n';
private_key = private_key.slice(key_header.length, private_key.length);
return pattern.test(private_key || '');
}
}
return false;
} catch (error) {
console.log('Error in isObscureKey', error);
return false;
}
}
/**
* obscure sensitive data in bucket credentials
* an obscured key contains of 6 'spoiled' characters of the key followed by 'X' characters
* '123456XXXXXXXXXXXXXXXXXXXXXXXX'
* @param {*} obj
* @returns
*/
function obscureBucketCredentialsSensitiveData(obj) {
if (!obj) return obj;
const {vendor, service_key, connection_string, secret_access_key} = obj;
switch (vendor) {
case 'aws_s3':
case 's3_compatible':
obj.secret_access_key = obscureKey(secret_access_key);
break;
case 'google':
const o = JSON.parse(service_key);
let private_key = o.private_key;
if (!isObscureKey(obj)) {
const key_header = '-----BEGIN PRIVATE KEY-----\n';
private_key = o.private_key.slice(key_header.length, o.private_key.length);
private_key = `${key_header}${obscureKey(private_key)}`;
}
const obscured = {
...o,
private_key
};
obj.service_key = JSON.stringify(obscured);
break;
case 'azure':
obj.connection_string = obscureKey(connection_string);
break;
}
return obj;
}
module.exports = {
encrypt,
decrypt,
obscureKey
obscureKey,
isObscureKey,
obscureBucketCredentialsSensitiveData,
};

View File

@@ -0,0 +1,218 @@
module.exports = [
{
name: 'Automatic',
value: 'auto',
},
{
name: 'Arabic',
value: 'ar',
},
{
name: 'Bashkir',
value: 'ba',
},
{
name: 'Basque',
value: 'eu',
},
{
name: 'Belarusian',
value: 'be',
},
{
name: 'Bulgarian',
value: 'bg',
},
{
name: 'Cantonese',
value: 'yue',
},
{
name: 'Catalan',
value: 'ca',
},
{
name: 'Croatian',
value: 'hr',
},
{
name: 'Czech',
value: 'cs',
},
{
name: 'Danish',
value: 'da',
},
{
name: 'Dutch',
value: 'nl',
},
{
name: 'English',
value: 'en',
},
{
name: 'Esperanto',
value: 'eo',
},
{
name: 'Estonian',
value: 'et',
},
{
name: 'Finnish',
value: 'fi',
},
{
name: 'French',
value: 'fr',
},
{
name: 'Galician',
value: 'gl',
},
{
name: 'German',
value: 'de',
},
{
name: 'Greek',
value: 'el',
},
{
name: 'Hebrew',
value: 'he',
},
{
name: 'Hindi',
value: 'hi',
},
{
name: 'Hungarian',
value: 'hu',
},
{
name: 'Irish',
value: 'ga',
},
{
name: 'Interlingua',
value: 'ia',
},
{
name: 'Italian',
value: 'it',
},
{
name: 'Indonesian',
value: 'id',
},
{
name: 'Japanese',
value: 'ja',
},
{
name: 'Korean',
value: 'ko',
},
{
name: 'Latvian',
value: 'lv',
},
{
name: 'Lithuanian',
value: 'lt',
},
{
name: 'Maltese',
value: 'mt',
},
{
name: 'Malay',
value: 'ms',
},
{
name: 'Mandarin',
value: 'cmn',
},
{
name: 'Marathi',
value: 'mr',
},
{
name: 'Mongolian',
value: 'mn',
},
{
name: 'Norwegian',
value: 'no',
},
{
name: 'Persian',
value: 'fa',
},
{
name: 'Polish',
value: 'pl',
},
{
name: 'Portuguese',
value: 'pt',
},
{
name: 'Romanian',
value: 'ro',
},
{
name: 'Russian',
value: 'ru',
},
{
name: 'Slovakian',
value: 'sk',
},
{
name: 'Slovenian',
value: 'sl',
},
{
name: 'Spanish',
value: 'es',
},
{
name: 'Spanish & English bilingual',
value: 'es',
},
{
name: 'Swedish',
value: 'sv',
},
{
name: 'Tamil',
value: 'ta',
},
{
name: 'Thai',
value: 'th',
},
{
name: 'Turkish',
value: 'tr',
},
{
name: 'Uyghur',
value: 'ug',
},
{
name: 'Ukrainian',
value: 'uk',
},
{
name: 'Vietnamese',
value: 'vi',
},
{
name: 'Welsh',
value: 'cy',
},
];

View File

@@ -0,0 +1,8 @@
module.exports = [
{ name: 'English', value: 'en' },
{ name: 'French', value: 'fr' },
{ name: 'German', value: 'de' },
{ name: 'Dutch', value: 'nl' },
{ name: 'Italian', value: 'it' },
{ name: 'Spanish', value: 'sp' },
];

View File

@@ -0,0 +1,301 @@
/* eslint-disable max-len */
module.exports = [
{
value: 'en',
name: 'English',
voices: [
{
value: '79f8b5fb-2cc8-479a-80df-29f7a7cf1a3e',
name: 'Nonfiction Man - This voice is smooth, confident, and resonant, perfect for narrating educational content',
},
{
value: 'e00d0e4c-a5c8-443f-a8a3-473eb9a62355',
name: 'Friendly Sidekick - This voice is friendly and supportive, designed for voicing characters in games and videos',
},
{
value: '3b554273-4299-48b9-9aaf-eefd438e3941',
name: 'Indian Lady - This voice is young, rich, and curious, perfect for a narrator or fictional character',
},
{
value: '71a7ad14-091c-4e8e-a314-022ece01c121',
name: 'British Reading Lady - This is a calm and elegant voice with a British accent, perfect for storytelling and narration',
},
{
value: '4d2fd738-3b3d-4368-957a-bb4805275bd9',
name: 'British Narration Lady - This is a neutral voice with a British accent, perfect for narrations ',
},
{
value: '15a9cd88-84b0-4a8b-95f2-5d583b54c72e',
name: 'Reading Lady - This voice is monotone and deliberate, perfect for a slower-paced and more serious reading voice',
},
{
value: 'd46abd1d-2d02-43e8-819f-51fb652c1c61',
name: 'Newsman - This voice is neutral and educational, perfect for a news anchor',
},
{
value: '2ee87190-8f84-4925-97da-e52547f9462c',
name: 'Child - This voice is young and full, perfect for a child',
},
{
value: 'cd17ff2d-5ea4-4695-be8f-42193949b946',
name: 'Meditation Lady - This voice is calm, soothing, and relaxing, perfect for meditation',
},
{
value: '5345cf08-6f37-424d-a5d9-8ae1101b9377',
name: 'Maria - This voice is laid back, natural, and conversational, like you\'re catching up with a good friend',
},
{
value: '41534e16-2966-4c6b-9670-111411def906',
name: '1920\'s Radioman - This voice is energetic and confident, great for an entertainer or radio host',
},
{
value: 'bf991597-6c13-47e4-8411-91ec2de5c466',
name: 'Newslady - This voice is authoritative and educational, perfect for a news anchor',
},
{
value: '00a77add-48d5-4ef6-8157-71e5437b282d',
name: 'Calm Lady - This voice is calm and nurturing, perfect for a narrator',
},
{
value: '156fb8d2-335b-4950-9cb3-a2d33befec77',
name: 'Helpful Woman - This voice is friendly and conversational, designed for customer support agents and casual conversations',
},
{
value: '36b42fcb-60c5-4bec-b077-cb1a00a92ec6',
name: 'Pilot over Intercom - This voice sounds like a British Pilot character speaking over an Intercom',
},
{
value: 'f146dcec-e481-45be-8ad2-96e1e40e7f32',
name: 'Reading Man - Male with calm narrational voice.',
},
{
value: '34575e71-908f-4ab6-ab54-b08c95d6597d',
name: 'New York Man - This voice is compelling and husky, with a New York accent, perfect for sales pitches and motivational content',
},
{
value: 'a0e99841-438c-4a64-b679-ae501e7d6091',
name: 'Barbershop Man - This voice is smooth and relaxing, perfect for a casual conversation',
},
{
value: '638efaaa-4d0c-442e-b701-3fae16aad012',
name: 'Indian Man - This voice is smooth with an Indian accent, perfect for a narrator',
},
{
value: '41f3c367-e0a8-4a85-89e0-c27bae9c9b6d',
name: 'Australian Customer Support Man - This voice is warm with an Australian accent, perfect for customer support agents',
},
{
value: '421b3369-f63f-4b03-8980-37a44df1d4e8',
name: 'Friendly Australian Man - This voice is rich and deep, with an Australian accent, perfect for casual conversations with a friend',
},
{
value: 'b043dea0-a007-4bbe-a708-769dc0d0c569',
name: 'Wise Man - This is a deep and deliberate voice, suited for educational content and conversations',
},
{
value: '69267136-1bdc-412f-ad78-0caad210fb40',
name: 'Friendly Reading Man - This voice is energetic and friendly, like having your friend read his favorite book to you',
},
{
value: 'a167e0f3-df7e-4d52-a9c3-f949145efdab',
name: 'Customer Support Man - This voice is clear and calm, perfect for a call center',
},
{
value: '4f8651b0-bbbd-46ac-8b37-5168c5923303',
name: 'Kentucky Woman - This voice is energetic and upbeat, with a slight Kentucky accent, perfect for speeches and rallies',
},
{
value: 'daf747c6-6bc2-4083-bd59-aa94dce23f5d',
name: 'Middle Eastern Woman - This voice is clear with a Middle Eastern Accent, perfect for a narrator',
},
{
value: '694f9389-aac1-45b6-b726-9d9369183238',
name: 'Sarah - This voice is natural and expressive with an American accent, perfect for a wide range of conversational use cases including customer support, sales, reception, and more.',
},
{
value: '794f9389-aac1-45b6-b726-9d9369183238',
name: 'Sarah Curious - This voice is similar to Sarah, but has improved emphasis for questions.',
},
{
value: '21b81c14-f85b-436d-aff5-43f2e788ecf8',
name: 'Laidback Woman - This voice is laid back and husky, with a slight Californian accent',
},
{
value: 'a3520a8f-226a-428d-9fcd-b0a4711a6829',
name: 'Reflective Woman - This voice is even, full, and reflective, perfect for a young narrator for an audiobook or movie',
},
{
value: '829ccd10-f8b3-43cd-b8a0-4aeaa81f3b30',
name: 'Customer Support Lady - This voice is polite and helpful, perfect for customer support agents',
},
{
value: '79a125e8-cd45-4c13-8a67-188112f4dd22',
name: 'British Lady - This voice is elegant with a slight British accent, perfect for storytelling and narrating',
},
{
value: 'c8605446-247c-4d39-acd4-8f4c28aa363c',
name: 'Wise Lady - This voice is wise and authoritative, perfect for a confident narrator',
},
{
value: '8985388c-1332-4ce7-8d55-789628aa3df4',
name: 'Australian Narrator Lady - This voice is even and neutral, with an Australian accent, designed for narrating content and stories',
},
{
value: 'ff1bb1a9-c582-4570-9670-5f46169d0fc8',
name: 'Indian Customer Support Lady - This voice is clear and polite, with an Indian accent, suitable for customer support agents',
},
{
value: '820a3788-2b37-4d21-847a-b65d8a68c99a',
name: 'Salesman - This voice is smooth and persuasive, perfect for sales pitches and phone conversations',
},
{
value: 'f114a467-c40a-4db8-964d-aaba89cd08fa',
name: 'Yogaman - This voice is calm, soothing, and stable, perfect for a yoga instructor',
},
{
value: 'c45bc5ec-dc68-4feb-8829-6e6b2748095d',
name: 'Movieman - This voice is deep, resonant, and assertive, perfect for a movie narrator',
},
{
value: '87748186-23bb-4158-a1eb-332911b0b708',
name: 'Wizardman - This voice is wise and mysterious, perfect for a Wizard character',
},
{
value: '043cfc81-d69f-4bee-ae1e-7862cb358650',
name: 'Australian Woman - This voice is deliberate and confident, with a slight Australian accent, perfect for inspiring characters in videos and stories',
},
{
value: '5619d38c-cf51-4d8e-9575-48f61a280413',
name: 'Announcer Man - This voice is deep and inviting, perfect for entertainment and broadcasting content',
},
{
value: '42b39f37-515f-4eee-8546-73e841679c1d',
name: 'Wise Guide Man - This voice is deep and deliberate, perfect for inspiring and guiding characters in games and videos',
},
{
value: '565510e8-6b45-45de-8758-13588fbaec73',
name: 'Midwestern Man - This voice is neutral and smooth, with a slight midwestern accent, perfect for narrations',
},
{
value: '726d5ae5-055f-4c3d-8355-d9677de68937',
name: 'Kentucky Man - This voice is laidback and smooth, with a Kentucky accent, perfect for a casual conversation',
},
{
value: '63ff761f-c1e8-414b-b969-d1833d1c870c',
name: 'Confident British Man - This voice is disciplined with a British accent, perfect for a commanding character or narrator',
},
{
value: '98a34ef2-2140-4c28-9c71-663dc4dd7022',
name: 'Southern Man - This voice is warm with a Southern accent, perfect for a narrator',
},
{
value: '95856005-0332-41b0-935f-352e296aa0df',
name: 'Classy British Man - This voice is light and smooth with a British accent, perfect for casual conversation',
},
{
value: 'ee7ea9f8-c0c1-498c-9279-764d6b56d189',
name: 'Polite Man - This voice is polite and conversational, with a slight accent, designed for customer support and casual conversations',
},
{
value: '40104aff-a015-4da1-9912-af950fbec99e',
name: 'Alabama Male - This voice has a strong Southern Accent, perfect for conversations and instructional videos',
},
{
value: '13524ffb-a918-499a-ae97-c98c7c4408c4',
name: 'Australian Male - This voice is smooth and disciplined, with an Australian Accent, suited for narrating educational content',
},
{
value: '1001d611-b1a8-46bd-a5ca-551b23505334',
name: 'Anime Girl - This voice is expressive and has a high pitch, suitable for anime or gaming characters',
},
{
value: 'e3827ec5-697a-4b7c-9704-1a23041bbc51',
name: 'Sweet Lady - This voice is sweet and passionate, perfect for a character in a game or book',
},
{
value: 'c2ac25f9-ecc4-4f56-9095-651354df60c0',
name: 'Commercial Lady - This voice is inviting, enthusiastic, and relatable, perfect for a commercial or advertisement',
},
{
value: '573e3144-a684-4e72-ac2b-9b2063a50b53',
name: 'Teacher Lady - This voice is neutral and clear, perfect for narrating educational content',
},
{
value: '8f091740-3df1-4795-8bd9-dc62d88e5131',
name: 'Princess - This voice is light, freindly and has a flourish, perfect for character work in videos and games',
},
{
value: '7360f116-6306-4e9a-b487-1235f35a0f21',
name: 'Commercial Man - This voice is upbeat and enthusiastic, perfect for commercials and advertisements',
},
{
value: '03496517-369a-4db1-8236-3d3ae459ddf7',
name: 'ASMR Lady - This voice is calming and soft, perfect for guided meditations and soothing content',
},
{
value: '248be419-c632-4f23-adf1-5324ed7dbf1d',
name: 'Professional Woman - This voice is neutral and calm, perfect for a call center',
},
{
value: 'bd9120b6-7761-47a6-a446-77ca49132781',
name: 'Tutorial Man - This voice is inviting and calming, perfect for tutorials',
},
{
value: '34bde396-9fde-4ebf-ad03-e3a1d1155205',
name: 'New York Woman - This voice commands authority, with a New York accent, perfect for a commanding narrator or character',
},
{
value: '11af83e2-23eb-452f-956e-7fee218ccb5c',
name: 'Midwestern Woman - This voice is neutral and deliberate, with a midwestern accent, suitable for news broadcasts and narration',
},
{
value: 'ed81fd13-2016-4a49-8fe3-c0d2761695fc',
name: 'Sportsman - This voice is energetic and enthusiastic, perfect for a sports broadcaster',
},
{
value: '996a8b96-4804-46f0-8e05-3fd4ef1a87cd',
name: 'Storyteller Lady - This voice is neutral and smooth, with a slight Canadian accent, perfect for narrations',
},
{
value: 'fb26447f-308b-471e-8b00-8e9f04284eb5',
name: 'Doctor Mischief - This is an expressive character voice, suited to whimsical characters for games and educational content',
},
{
value: '50d6beb4-80ea-4802-8387-6c948fe84208',
name: 'The Merchant - This voice is playful and quirky, designed for character work in games and videos',
},
{
value: 'e13cae5c-ec59-4f71-b0a6-266df3c9bb8e',
name: 'Madame Mischief - This voice is mischeivious and playful, suitable for voicing characters for kids content and games',
},
{
value: '5c42302c-194b-4d0c-ba1a-8cb485c84ab9',
name: 'Female Nurse - This voice is clear and firm, perfect for nurse characters and instructional videos',
},
{
value: 'f9836c6e-a0bd-460e-9d3c-f7299fa60f94',
name: 'Southern Woman - This voice is friendly and inviting, with a slight Southern Accent, perfect for conversations and phone calls',
},
{
value: 'a01c369f-6d2d-4185-bc20-b32c225eab70',
name: 'British Customer Support Lady - This voice is friendly and polite, with a British accent, perfect for phone conversations',
},
{
value: 'b7d50908-b17c-442d-ad8d-810c63997ed9',
name: 'California Girl - This voice is enthusiastic and friendly, perfect for a casual conversation between friends',
},
{
value: 'f785af04-229c-4a7c-b71b-f3194c7f08bb',
name: 'John - This voice is natural and empathetic with an American accent, perfect for use cases like demos and customer support calls.',
},
{
value: '729651dc-c6c3-4ee5-97fa-350da1f88600',
name: 'Pleasant Man - A pleasant male voice that\'s good for use cases like demos and customer support calls',
},
{
value: '91b4cf29-5166-44eb-8054-30d40ecc8081',
name: 'Anna - This voice is natural and expressive with an American accent, perfect for use cases like interviews and customer support calls.',
},
],
},
];

View File

@@ -0,0 +1,28 @@
const TtsDeepgramLanguagesVoiceRaw = require('./tts-model-deepgram');
const languagesVoices = [];
TtsDeepgramLanguagesVoiceRaw.forEach((data) => {
const lang = languagesVoices.find((l) => {
return l.value === data.locale;
});
if (!lang) {
languagesVoices.push({
value: data.locale,
name: data.localeName,
voices: TtsDeepgramLanguagesVoiceRaw
.filter((d) => {
return d.locale === data.locale;
})
.map((d) => {
return {
value: d.value,
name: `${d.name}`,
};
}),
});
}
});
module.exports = languagesVoices;

View File

@@ -0,0 +1,152 @@
// languages.js
module.exports = [
{
name: 'English',
value: 'english'
},
{
name: 'Mandarin',
value: 'mandarin'
},
{
name: 'Hindi',
value: 'hindi'
},
{
name: 'Japanese',
value: 'japanese'
},
{
name: 'Korean',
value: 'korean'
},
{
name: 'Arabic',
value: 'arabic'
},
{
name: 'Spanish',
value: 'spanish'
},
{
name: 'French',
value: 'french'
},
{
name: 'Italian',
value: 'italian'
},
{
name: 'Portuguese',
value: 'portuguese'
},
{
name: 'German',
value: 'german'
},
{
name: 'Dutch',
value: 'dutch'
},
{
name: 'Swedish',
value: 'swedish'
},
{
name: 'Czech',
value: 'czech'
},
{
name: 'Polish',
value: 'polish'
},
{
name: 'Russian',
value: 'russian'
},
{
name: 'Bulgarian',
value: 'bulgarian'
},
{
name: 'Hebrew',
value: 'hebrew'
},
{
name: 'Greek',
value: 'greek'
},
{
name: 'Turkish',
value: 'turkish'
},
{
name: 'Afrikaans',
value: 'afrikaans'
},
{
name: 'Xhosa',
value: 'xhosa'
},
{
name: 'Tagalog',
value: 'tagalog'
},
{
name: 'Malay',
value: 'malay'
},
{
name: 'Indonesian',
value: 'indonesian'
},
{
name: 'Bengali',
value: 'bengali'
},
{
name: 'Serbian',
value: 'serbian'
},
{
name: 'Thai',
value: 'thai'
},
{
name: 'Urdu',
value: 'urdu'
},
{
name: 'Croatian',
value: 'croatian'
},
{
name: 'Hungarian',
value: 'hungarian'
},
{
name: 'Danish',
value: 'danish'
},
{
name: 'Amharic',
value: 'amharic'
},
{
name: 'Albanian',
value: 'albanian'
},
{
name: 'Catalan',
value: 'catalan'
},
{
name: 'Ukrainian',
value: 'ukrainian'
},
{
name: 'Galician',
value: 'galician'
}
];

View File

@@ -0,0 +1,25 @@
module.exports = [
{
name: 'Sonic',
value: 'sonic',
languages: ['en', 'fr', 'de', 'es', 'pt', 'zh', 'ja', 'hi', 'it', 'ko', 'nl', 'pl', 'ru', 'sv', 'tr']
},
{ name: 'Sonic Preview', value: 'sonic-preview', languages: ['en'] },
{
name: 'Sonic 2024-12-12',
value: 'sonic-2024-12-12',
languages: ['en', 'fr', 'de', 'es', 'pt', 'zh', 'ja', 'hi', 'it', 'ko', 'nl', 'pl', 'ru', 'sv', 'tr']
},
{
name: 'Sonic 2024-10-19',
value: 'sonic-2024-10-19',
languages: ['en', 'fr', 'de', 'es', 'pt', 'zh', 'ja', 'hi', 'it', 'ko', 'nl', 'pl', 'ru', 'sv', 'tr']
},
{ name: 'Sonic English', value: 'sonic-english', languages: ['en'] },
{
name: 'Sonic Multilingual',
value: 'sonic-multilingual',
languages: ['en', 'fr', 'de', 'es', 'pt', 'zh', 'ja', 'hi', 'it', 'ko', 'nl', 'pl', 'ru', 'sv', 'tr']
},
];

View File

@@ -1,14 +1,68 @@
module.exports = [
{ name: 'Asteria English (US) Female', value: 'aura-asteria-en' },
{ name: 'Luna English (US) Female', value: 'aura-luna-en' },
{ name: 'Stella English (US) Female', value: 'aura-stella-en' },
{ name: 'Stella English (UK) Female', value: 'aura-athena-en' },
{ name: 'Hera English (US) Female', value: 'aura-hera-en' },
{ name: 'Orion English (US) Male', value: 'aura-orion-en' },
{ name: 'Arcas English (US) Male', value: 'aura-arcas-en' },
{ name: 'Perseus English (US) Male', value: 'aura-perseus-en' },
{ name: 'Angus English (Ireland) Male', value: 'aura-angus-en' },
{ name: 'Orpheus English (US) Male', value: 'aura-orpheus-en' },
{ name: 'Helios English (UK) Male', value: 'aura-helios-en' },
{ name: 'Zeus English (US) Male', value: 'aura-zeus-en' },
{
locale: 'en-US',
localeName: 'English (US)',
name: 'Asteria English (US) Female',
value: 'aura-asteria-en'
},
{
locale: 'en-US',
localeName: 'English (US)',
name: 'Luna English (US) Female',
value: 'aura-luna-en'
},
{
locale: 'en-US',
localeName: 'English (US)',
name: 'Stella English (US) Female',
value: 'aura-stella-en'
},
{
locale: 'en-GB',
localeName: 'English (UK)',
name: 'Stella English (UK) Female',
value: 'aura-athena-en'
},
{
locale: 'en-US',
localeName: 'English (US)',
name: 'Hera English (US) Female',
value: 'aura-hera-en'
},
{
locale: 'en-US',
localeName: 'English (US)',
name: 'Orion English (US) Male',
value: 'aura-orion-en'
},
{
locale: 'en-US',
localeName: 'English (US)',
name: 'Arcas English (US) Male',
value: 'aura-arcas-en'
},
{
locale: 'en-US',
localeName: 'English (US)',
name: 'Perseus English (US) Male',
value: 'aura-perseus-en'
},
{
locale: 'en-IE',
localeName: 'English (Ireland)',
name: 'Angus English (Ireland) Male',
value: 'aura-angus-en'
},
{
locale: 'en-US',
localeName: 'English (US)',
name: 'Orpheus English (US) Male',
value: 'aura-orpheus-en'
},
{
locale: 'en-US',
localeName: 'English (US)',
name: 'Zeus English (US) Male',
value: 'aura-zeus-en'
},
];

View File

@@ -1,7 +1,11 @@
module.exports = [
{ name: 'Turbo v2', value: 'eleven_turbo_v2' },
{ name: 'Multilingual v2', value: 'eleven_multilingual_v2' },
{ name: 'Turbo v2.5', value: 'eleven_turbo_v2_5' },
{ name: 'Flash v2', value: 'eleven_flash_v2' },
{ name: 'Flash v2.5', value: 'eleven_flash_v2_5' },
{ name: 'Multilingual v1', value: 'eleven_multilingual_v1' },
{ name: 'Multilingual v2', value: 'eleven_multilingual_v2' },
{ name: 'Multilingual STS v2', value: 'eleven_multilingual_sts_v2' },
{ name: 'English v1', value: 'eleven_monolingual_v1' },
{ name: 'English v2', value: 'eleven_english_sts_v2' },
];

View File

@@ -1,4 +1,5 @@
module.exports = [
{ name: 'Play3.0', value: 'Play3.0' },
{ name: 'PlayHT2.0-turbo', value: 'PlayHT2.0-turbo' },
{ name: 'PlayHT2.0', value: 'PlayHT2.0' },
{ name: 'PlayHT1.0', value: 'PlayHT1.0' },

View File

@@ -1,5 +1,6 @@
module.exports = [
{ name: 'Mist', value: 'mist' },
{ name: 'Mistv2', value: 'mistv2' },
{ name: 'V1', value: 'v1' },
];

View File

@@ -1,14 +1,4 @@
module.exports = [
{
value: 'en-US',
name: 'US English',
voices: [
{
value: 'tommy_en_us',
name: 'Tommy-Male',
},
],
},
{
value: 'es-ES',
name: 'Castilian Spanish',
@@ -27,12 +17,6 @@ module.exports = [
value: 'miguel_es_pe',
name: 'Miguel-Male',
},
],
},
{
value: 'es-PE',
name: 'Peruvian Spanish',
voices: [
{
value: 'luz_es_pe',
name: 'Luz-Female',

View File

@@ -7,6 +7,7 @@ const bent = require('bent');
const fs = require('fs');
const { AssemblyAI } = require('assemblyai');
const {decrypt, obscureKey} = require('./encrypt-decrypt');
const { RealtimeSession } = require('speechmatics');
const TtsGoogleLanguagesVoices = require('./speech-data/tts-google');
const TtsAwsLanguagesVoices = require('./speech-data/tts-aws');
@@ -21,10 +22,13 @@ const TtsPlayHtLanguagesVoices = require('./speech-data/tts-playht');
const TtsVerbioLanguagesVoices = require('./speech-data/tts-verbio');
const TtsModelDeepgram = require('./speech-data/tts-model-deepgram');
const TtsLanguagesDeepgram = require('./speech-data/tts-deepgram');
const TtsModelElevenLabs = require('./speech-data/tts-model-elevenlabs');
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 TtsModelCartesia = require('./speech-data/tts-model-cartesia');
const SttGoogleLanguagesVoices = require('./speech-data/stt-google');
const SttAwsLanguagesVoices = require('./speech-data/stt-aws');
@@ -35,8 +39,13 @@ const SttIbmLanguagesVoices = require('./speech-data/stt-ibm');
const SttNvidiaLanguagesVoices = require('./speech-data/stt-nvidia');
const SttCobaltLanguagesVoices = require('./speech-data/stt-cobalt');
const SttSonioxLanguagesVoices = require('./speech-data/stt-soniox');
const SttSpeechmaticsLanguagesVoices = require('./speech-data/stt-speechmatics');
const SttAssemblyaiLanguagesVoices = require('./speech-data/stt-assemblyai');
const SttVoxistLanguagesVoices = require('./speech-data/stt-voxist');
const SttVerbioLanguagesVoices = require('./speech-data/stt-verbio');
const ttsCartesia = require('./speech-data/tts-cartesia');
const ttsModelCartesia = require('./speech-data/tts-model-cartesia');
const testSonioxStt = async(logger, credentials) => {
const api_key = credentials;
@@ -54,6 +63,61 @@ const testSonioxStt = async(logger, credentials) => {
});
};
const testSpeechmaticsStt = async(logger, credentials) => {
const {api_key, speechmatics_stt_uri} = credentials;
return new Promise(async(resolve, reject) => {
try {
const session = new RealtimeSession({ apiKey: api_key, realtimeUrl: speechmatics_stt_uri });
let transcription = '';
session.addListener('Error', (error) => {
reject(error);
});
session.addListener('AddTranscript', (message) => {
transcription += message.metadata.transcript;
});
session.addListener('EndOfTranscript', () => {
resolve(transcription);
});
session
.start({
transcription_config: {
language: 'en',
operating_point: 'enhanced',
enable_partials: true,
max_delay: 2,
},
audio_format: { type: 'file' },
})
.then(() => {
//prepare file stream
const fileStream = fs.createReadStream(`${__dirname}/../../data/test_audio.wav`);
//send it
fileStream.on('data', (sample) => {
session.sendAudio(sample);
});
//end the session
fileStream.on('end', () => {
session.stop();
});
return;
})
.catch((error) => {
reject(error);
});
} catch (error) {
logger.info({error}, 'failed to get speechmatics transcript');
reject(error);
}
});
};
const testNuanceTts = async(logger, getTtsVoices, credentials) => {
const voices = await getTtsVoices({vendor: 'nuance', credentials});
return voices;
@@ -92,8 +156,8 @@ const testGoogleStt = async(logger, credentials) => {
};
const testDeepgramStt = async(logger, credentials) => {
const {api_key} = credentials;
const deepgram = new Deepgram(api_key);
const {api_key, deepgram_stt_uri, deepgram_stt_use_tls} = credentials;
const deepgram = new Deepgram(api_key, deepgram_stt_uri, deepgram_stt_uri && deepgram_stt_use_tls);
const mimetype = 'audio/wav';
const source = {
@@ -270,9 +334,10 @@ const testPlayHT = async(logger, synthAudio, credentials) => {
{
vendor: 'playht',
credentials,
language: 'en-US',
language: 'english',
voice: 's3://voice-cloning-zero-shot/d9ff78ba-d016-47f6-b0ef-dd630f59414e/female-cs/manifest.json',
text: 'Hi there and welcome to jambones!'
text: 'Hi there and welcome to jambones!',
renderForCaching: true
}
);
// Test if playHT can fetch voices
@@ -295,7 +360,8 @@ const testRimelabs = async(logger, synthAudio, credentials) => {
credentials,
language: 'en-US',
voice: 'amber',
text: 'Hi there and welcome to jambones!'
text: 'Hi there and welcome to jambones!',
renderForCaching: true
}
);
} catch (err) {
@@ -312,7 +378,8 @@ const testWhisper = async(logger, synthAudio, credentials) => {
credentials,
language: 'en-US',
voice: 'alloy',
text: 'Hi there and welcome to jambones!'
text: 'Hi there and welcome to jambones!',
renderForCaching: true
}
);
} catch (err) {
@@ -328,7 +395,8 @@ const testDeepgramTTS = async(logger, synthAudio, credentials) => {
vendor: 'deepgram',
credentials,
model: 'aura-asteria-en',
text: 'Hi there and welcome to jambones!'
text: 'Hi there and welcome to jambones!',
renderForCaching: true
}
);
} catch (err) {
@@ -383,7 +451,8 @@ const testVerbioTts = async(logger, synthAudio, credentials) => {
credentials,
language: 'en-US',
voice: 'tommy_en-us',
text: 'Hi there and welcome to jambones!'
text: 'Hi there and welcome to jambones!',
renderForCaching: true
}
);
} catch (err) {
@@ -434,6 +503,20 @@ const testAssemblyStt = async(logger, credentials) => {
});
};
const testVoxistStt = async(logger, credentials) => {
const {api_key} = credentials;
try {
const get = bent('https://api-asr.voxist.com', 'GET', 'json', {
'Accept': 'application/json',
'x-lvl-key': api_key
});
await get('/clients');
} catch (err) {
logger.info({err}, 'failed to get clients from Voxist');
throw err;
}
};
const getSpeechCredential = (credential, logger) => {
const {vendor} = credential;
logger.info(
@@ -509,6 +592,7 @@ function decryptCredential(obj, credential, logger, isObscureKey = true) {
obj.api_key = isObscureKey ? obscureKey(o.api_key) : o.api_key;
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;
}
else if ('ibm' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
@@ -526,6 +610,10 @@ function decryptCredential(obj, credential, logger, isObscureKey = true) {
} else if ('soniox' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.api_key = isObscureKey ? obscureKey(o.api_key) : o.api_key;
} else if ('speechmatics' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.api_key = isObscureKey ? obscureKey(o.api_key) : o.api_key;
obj.speechmatics_stt_uri = o.speechmatics_stt_uri;
} else if ('elevenlabs' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.api_key = isObscureKey ? obscureKey(o.api_key) : o.api_key;
@@ -537,6 +625,11 @@ function decryptCredential(obj, credential, logger, isObscureKey = true) {
obj.user_id = o.user_id;
obj.voice_engine = o.voice_engine;
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.options = o.options;
} else if ('rimelabs' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.api_key = isObscureKey ? obscureKey(o.api_key) : o.api_key;
@@ -547,9 +640,13 @@ function decryptCredential(obj, credential, logger, isObscureKey = true) {
obj.auth_token = isObscureKey ? obscureKey(o.auth_token) : o.auth_token;
obj.custom_stt_url = o.custom_stt_url;
obj.custom_tts_url = o.custom_tts_url;
obj.custom_tts_streaming_url = o.custom_tts_streaming_url;
} else if ('assemblyai' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.api_key = isObscureKey ? obscureKey(o.api_key) : o.api_key;
} else if ('voxist' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.api_key = isObscureKey ? obscureKey(o.api_key) : o.api_key;
} else if ('whisper' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.api_key = isObscureKey ? obscureKey(o.api_key) : o.api_key;
@@ -613,10 +710,16 @@ async function getLanguagesAndVoicesForVendor(logger, vendor, credential, getTts
return await getLanguagesVoicesForRimelabs(credential, getTtsVoices, logger);
case 'assemblyai':
return await getLanguagesVoicesForAssemblyAI(credential, getTtsVoices, logger);
case 'voxist':
return await getLanguagesVoicesForVoxist(credential, getTtsVoices, logger);
case 'whisper':
return await getLanguagesVoicesForWhisper(credential, getTtsVoices, logger);
case 'verbio':
return await getLanguagesVoicesForVerbio(credential, getTtsVoices, logger);
case 'speechmatics':
return await getLanguagesVoicesForSpeechmatics(credential, getTtsVoices, logger);
case 'cartesia':
return await getLanguagesVoicesForCartesia(credential, getTtsVoices, logger);
default:
logger.info(`invalid vendor ${vendor}, return empty result`);
throw new Error(`Invalid vendor ${vendor}`);
@@ -698,7 +801,7 @@ async function getLanguagesVoicesForNuane(credential, getTtsVoices, logger) {
}
async function getLanguagesVoicesForDeepgram(credential) {
return tranform(undefined, SttDeepgramLanguagesVoices, TtsModelDeepgram);
return tranform(TtsLanguagesDeepgram, SttDeepgramLanguagesVoices, TtsModelDeepgram);
}
async function getLanguagesVoicesForIbm(credential, getTtsVoices, logger) {
@@ -729,6 +832,10 @@ async function getLanguagesVoicesForSoniox(credential) {
return tranform(undefined, SttSonioxLanguagesVoices);
}
async function getLanguagesVoicesForSpeechmatics(credential) {
return tranform(undefined, SttSpeechmaticsLanguagesVoices);
}
async function getLanguagesVoicesForElevenlabs(credential) {
if (credential) {
const get = bent('https://api.elevenlabs.io', 'GET', 'json', {
@@ -738,6 +845,13 @@ async function getLanguagesVoicesForElevenlabs(credential) {
const [langResp, voiceResp] = await Promise.all([get('/v1/models'), get('/v1/voices')]);
const model = langResp.find((m) => m.model_id === credential.model_id);
const models = langResp.map((m) => {
return {
value: m.model_id,
name: m.name
};
}).sort((a, b) => a.name.localeCompare(b.name));
const languages = model ? model.languages.map((l) => {
return {
value: l.language_id,
@@ -746,27 +860,35 @@ async function getLanguagesVoicesForElevenlabs(credential) {
}).sort((a, b) => a.name.localeCompare(b.name)) : [];
if (languages && languages.length > 0) {
// using if condition to avoid \n character in name
const voices = voiceResp ? voiceResp.voices.map((v) => {
let name = `${v.name}${v.category !== 'premade' ? ` (${v.category})` : ''} -
${v.labels.accent ? ` ${v.labels.accent},` : ''}
${v.labels.description ? ` ${v.labels.description},` : ''}
${v.labels.age ? ` ${v.labels.age},` : ''}
${v.labels.gender ? ` ${v.labels.gender},` : ''}
${v.labels['use case'] ? ` ${v.labels['use case']},` : ''}
`;
let name = `${v.name}${v.category !== 'premade' ? ` (${v.category.trim()})` : ''} - (`;
if (v.labels.accent) name += `${v.labels.accent}, `;
if (v.labels.description) name += `${v.labels.description}, `;
if (v.labels.age) name += `${v.labels.age}, `;
if (v.labels.gender) name += `${v.labels.gender}, `;
if (v.labels['use case']) name += `${v.labels['use case']}, `;
const lastIndex = name.lastIndexOf(',');
if (lastIndex !== -1) {
name = name.substring(0, lastIndex);
}
name += ')';
return {
value: v.voice_id,
name
};
}).sort((a, b) => a.name.localeCompare(b.name)) : [];
languages[0].voices = voices;
for (const language of languages) {
language.voices = voices;
}
}
return tranform(languages, undefined, TtsModelElevenLabs);
return tranform(languages, undefined, models);
} else {
const voices = TtsElevenlabsLanguagesVoices[0].voices;
for (const language of TtsElevenlabsLanguagesVoices) {
language.voices = voices;
}
return tranform(TtsElevenlabsLanguagesVoices, undefined, TtsModelElevenLabs);
}
}
@@ -796,42 +918,67 @@ const fetchLayHTVoices = async(credential) => {
async function getLanguagesVoicesForPlayHT(credential) {
if (credential) {
const {voice_engine} = credential;
const [cloned_voice, voices] = await fetchLayHTVoices(credential);
const list_voices = [...cloned_voice, ...voices];
const buildVoice = (d) => {
let name = `${d.name} -${concat(d.accent)}${concat(d.age)}${concat(d.gender)}${concat(d.loudness)}` +
let name = `${d.name} - (${concat(d.accent)}${concat(d.age)}${concat(d.gender)}${concat(d.loudness)}` +
`${concat(d.style)}${concat(d.tempo)}${concat(d.texture)}` ;
name = name.endsWith(',') ? name.trim().slice(0, -1) : name;
name += !d.language_code ? ' - Custom Voice' : '';
name += ')';
name = name.replaceAll('( ', '(');
return {
value: `${d.id}`,
name
};
};
const ttsVoices = list_voices.reduce((acc, voice) => {
if (!credential.voice_engine.includes(voice.voice_engine)) {
const buildPlay30Payload = () => {
// PlayHT3.0 can play different languages with differrent voice.
// all voices will be added to english language by default and orther langauges will get voices from english.
const ttsVoices = ttsLanguagesPlayHt.map((l) => ({
...l,
voices: l.value === 'english' ? list_voices.map((v) => buildVoice(v)) : []
}));
return tranform(ttsVoices, undefined, TtsModelPlayHT);
};
const buildPayload = () => {
const ttsVoices = list_voices.reduce((acc, voice) => {
if (!voice_engine.includes(voice.voice_engine)) {
return acc;
}
const languageCode = voice.language_code;
// custom voice does not have language code
if (!languageCode) {
voice.language_code = 'en';
voice.language = 'Custom-English';
}
const existingLanguage = acc.find((lang) => lang.value === languageCode);
if (existingLanguage) {
existingLanguage.voices.push(buildVoice(voice));
} else {
acc.push({
value: voice.language_code,
name: voice.language,
voices: [buildVoice(voice)]
});
}
return acc;
}
const languageCode = voice.language_code;
// custom voice does not have language code
if (!languageCode) {
voice.language_code = 'en';
voice.language = 'Custom-English';
}
const existingLanguage = acc.find((lang) => lang.value === languageCode);
if (existingLanguage) {
existingLanguage.voices.push(buildVoice(voice));
} else {
acc.push({
value: voice.language_code,
name: voice.language,
voices: [buildVoice(voice)]
});
}
return acc;
}, []);
return tranform(ttsVoices, undefined, TtsModelPlayHT);
}, []);
return tranform(ttsVoices, undefined, TtsModelPlayHT);
};
switch (voice_engine) {
case 'Play3.0':
return buildPlay30Payload();
default:
return buildPayload();
}
}
return tranform(TtsPlayHtLanguagesVoices, undefined, TtsModelPlayHT);
}
@@ -841,19 +988,17 @@ async function getLanguagesVoicesForRimelabs(credential) {
const get = bent('https://users.rime.ai', 'GET', 'json', {
'Accept': 'application/json'
});
const voices = await get('/data/voices/all.json');
let selectedVoices = model_id ? voices[model_id] : Object.values(voices).reduce((acc, val) => [...acc, ...val], []);
selectedVoices = selectedVoices.map((v) => ({
name: v.charAt(0).toUpperCase() + v.slice(1),
value: v
const voices = await get('/data/voices/all-v2.json');
const modelVoices = model_id ? voices[model_id] :
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),
voices: voices.map((v) => ({
name: v.charAt(0).toUpperCase() + v.slice(1),
value: v
}))
}));
const ttsVoices = [
{
value: 'en-US',
name: 'English (US)',
voices: selectedVoices
}
];
return tranform(ttsVoices, undefined, TtsModelRimelabs);
}
@@ -861,21 +1006,28 @@ async function getLanguagesVoicesForAssemblyAI(credential) {
return tranform(undefined, SttAssemblyaiLanguagesVoices);
}
async function getLanguagesVoicesForVoxist(credential) {
return tranform(undefined, SttVoxistLanguagesVoices);
}
async function getLanguagesVoicesForWhisper(credential) {
return tranform(TtsWhisperLanguagesVoices, undefined, TtsModelWhisper);
}
async function getLanguagesVoicesForVerbio(credentials, getTtsVoices, logger) {
const stt = SttVerbioLanguagesVoices.reduce((acc, v) => {
if (!v.version || credentials.engine_version === v.version) {
if (!v.version || (credentials && credentials.engine_version === v.version)) {
acc.push(v);
}
return acc;
}, []);
try {
const data = await getTtsVoices({vendor: 'verbio', credentials});
const voices = parseVerbioLanguagesVoices(data);
return tranform(voices, stt, undefined);
if (credentials) {
const data = await getTtsVoices({vendor: 'verbio', credentials});
const voices = parseVerbioLanguagesVoices(data);
return tranform(voices, stt, undefined);
}
return tranform(TtsVerbioLanguagesVoices, stt, undefined);
} catch (err) {
logger.info({err}, 'there is error while fetching verbio speech voices');
return tranform(TtsVerbioLanguagesVoices, stt, undefined);
@@ -945,7 +1097,7 @@ function parseAwsLanguagesVoices(data) {
if (existingLanguage) {
existingLanguage.voices.push({
value: voice.Id,
name: `(${voice.Gender}) ${voice.Name}`
name: `${voice.Name} (${voice.Gender})`
});
} else {
acc.push({
@@ -953,7 +1105,7 @@ function parseAwsLanguagesVoices(data) {
name: voice.LanguageName,
voices: [{
value: voice.Id,
name: `(${voice.Gender}) ${voice.Name}`
name: `${voice.Name} (${voice.Gender})`
}]
});
}
@@ -1010,7 +1162,7 @@ function parseMicrosoftLanguagesVoices(data) {
}
function parseVerbioLanguagesVoices(data) {
return data.reduce((acc, voice) => {
return data.voices.reduce((acc, voice) => {
const languageCode = voice.language;
const existingLanguage = acc.find((lang) => lang.value === languageCode);
if (existingLanguage) {
@@ -1032,6 +1184,95 @@ function parseVerbioLanguagesVoices(data) {
}, []);
}
const fetchCartesiaVoices = async(credential) => {
if (credential) {
const get = bent('https://api.cartesia.ai', 'GET', 'json', {
'X-API-Key' : credential.api_key,
'Cartesia-Version': '2024-06-10',
'Accept': 'application/json'
});
const voices = await get('/voices');
return voices;
}
};
const testCartesia = async(logger, synthAudio, credentials) => {
try {
await synthAudio(
{
increment: () => {},
histogram: () => {}
},
{
vendor: 'cartesia',
credentials,
language: 'en',
voice: '694f9389-aac1-45b6-b726-9d9369183238',
text: 'Hi there and welcome to jambones!',
renderForCaching: true
}
);
// Test if Cartesia can fetch voices
await fetchCartesiaVoices(credentials);
} catch (err) {
logger.info({err}, 'synth cartesia returned error');
throw err;
}
};
async function getLanguagesVoicesForCartesia(credential) {
if (credential) {
const {model_id} = credential;
const {languages} = ttsModelCartesia.find((m) => m.value === model_id);
const voices = await fetchCartesiaVoices(credential);
const buildVoice = (d) => (
{
value: `${d.id}`,
name: `${d.name} - ${d.description}`
});
const languageMap = {
en: 'English',
fr: 'French',
de: 'German',
es: 'Spanish',
pt: 'Portuguese',
zh: 'Chinese',
ja: 'Japanese',
hi: 'Hindi',
it: 'Italian',
ko: 'Korean',
nl: 'Dutch',
pl: 'Polish',
ru: 'Russian',
sv: 'Swedish',
tr: 'Turkish',
};
const ttsVoices = voices.reduce((acc, voice) => {
if (!languages.includes(voice.language)) {
return acc;
}
const languageCode = voice.language;
const existingLanguage = acc.find((lang) => lang.value === languageCode);
if (existingLanguage) {
existingLanguage.voices.push(buildVoice(voice));
} else {
acc.push({
value: languageCode,
name: languageMap[languageCode],
voices: [buildVoice(voice)]
});
}
return acc;
}, []);
return tranform(ttsVoices, undefined, TtsModelCartesia);
}
return tranform(ttsCartesia, undefined, TtsModelCartesia);
}
module.exports = {
testGoogleTts,
testGoogleStt,
@@ -1057,5 +1298,8 @@ module.exports = {
testWhisper,
testVerbioTts,
testVerbioStt,
getLanguagesAndVoicesForVendor
getLanguagesAndVoicesForVendor,
testSpeechmaticsStt,
testCartesia,
testVoxistStt
};

3141
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,12 @@
{
"name": "jambonz-api-server",
"version": "0.9.0",
"version": "0.9.3",
"description": "",
"main": "app.js",
"scripts": {
"start": "node app.js",
"test": "NODE_ENV=test APPLY_JAMBONZ_DB_LIMITS=1 JWT_SECRET=foobarbazzle JAMBONES_MYSQL_HOST=127.0.0.1 JAMBONES_MYSQL_PORT=3360 JAMBONES_MYSQL_USER=jambones_test JAMBONES_MYSQL_PASSWORD=jambones_test JAMBONES_MYSQL_DATABASE=jambones_test JAMBONES_REDIS_HOST=localhost JAMBONES_REDIS_PORT=16379 JAMBONES_TIME_SERIES_HOST=127.0.0.1 JAMBONES_LOGLEVEL=error JAMBONES_CREATE_CALL_URL=http://localhost/v1/createCall K8S=true K8S_FEATURE_SERVER_SERVICE_NAME=127.0.0.1 K8S_FEATURE_SERVER_SERVICE_PORT=3100 node test/ ",
"test:encrypt-decrypt": "ENCRYPTION_SECRET=12345 node --test ./test/encrypt-decrypt.test.js",
"integration-test": "NODE_ENV=test JAMBONES_AUTH_USE_JWT=1 JAMBONES_TIME_SERIES_HOST=127.0.0.1 AWS_REGION='us-east-1' JAMBONES_CURRENCY=USD JWT_SECRET=foobarbazzle JAMBONES_MYSQL_HOST=127.0.0.1 JAMBONES_MYSQL_PORT=3360 JAMBONES_MYSQL_USER=jambones_test JAMBONES_MYSQL_PASSWORD=jambones_test JAMBONES_MYSQL_DATABASE=jambones_test JAMBONES_REDIS_HOST=localhost JAMBONES_REDIS_PORT=16379 JAMBONES_LOGLEVEL=debug JAMBONES_CREATE_CALL_URL=http://localhost/v1/createCall node test/serve-integration.js",
"upgrade-db": "node ./db/upgrade-jambonz-db.js",
"coverage": "./node_modules/.bin/nyc --reporter html --report-dir ./coverage npm run test",
@@ -28,8 +29,8 @@
"@jambonz/db-helpers": "^0.9.3",
"@jambonz/lamejs": "^1.2.2",
"@jambonz/mw-registrar": "^0.2.7",
"@jambonz/realtimedb-helpers": "^0.8.9",
"@jambonz/speech-utils": "^0.1.11",
"@jambonz/realtimedb-helpers": "^0.8.10",
"@jambonz/speech-utils": "^0.2.3",
"@jambonz/time-series": "^0.2.8",
"@jambonz/verb-specifications": "^0.0.72",
"@soniox/soniox-node": "^1.2.2",
@@ -46,12 +47,14 @@
"jsonwebtoken": "^9.0.2",
"mailgun.js": "^10.2.1",
"microsoft-cognitiveservices-speech-sdk": "1.36.0",
"mysql2": "^3.9.3",
"multer": "^1.4.5-lts.1",
"mysql2": "^3.11.0",
"nocache": "4.0.0",
"passport": "^0.7.0",
"passport-http-bearer": "^1.0.1",
"pino": "^8.20.0",
"short-uuid": "^4.2.2",
"speechmatics": "^4.0.0",
"stream-buffers": "^3.0.2",
"stripe": "^14.24.0",
"swagger-ui-express": "^5.0.0",

View File

@@ -152,7 +152,9 @@ test('account tests', async(t) => {
auth: authAdmin,
json: true,
});
console.log(result);
t.ok(result.name === 'daveh' , 'successfully retrieved account by sid');
t.ok(result.enable_debug_log === 0 , 'enable_debug_log default value ok');
/* update account with account level token */
result = await request.put(`/Accounts/${sid}`, {
@@ -177,8 +179,8 @@ test('account tests', async(t) => {
name: 'recordings',
access_key_id: 'access_key_id',
secret_access_key: 'secret access key'
}
},
enable_debug_log: true
}
});
t.ok(result.statusCode === 204, 'successfully updated account using account level token');
@@ -194,6 +196,7 @@ test('account tests', async(t) => {
t.ok(result.bucket_credential.access_key_id === 'access_key_id', 'bucket_access_key_id was updated');
t.ok(result.record_all_calls === 1, 'record_all_calls was updated');
t.ok(result.record_format === 'wav', 'record_format was updated');
t.ok(result.enable_debug_log, 'enable_debug_log was updated');
/* verify that account level api key last_used was updated*/
result = await request.get(`/Accounts/${sid}/ApiKeys`, {

View File

@@ -121,6 +121,25 @@ test('application tests', async(t) => {
let app_json = JSON.parse(result.app_json);
t.ok(app_json[0].verb === 'play', 'successfully retrieved app_json from application')
/* query one application by name*/
result = await request.get(`/Applications`, {
qs : {
name: 'daveh'
},
auth: authAdmin,
json: true,
});
t.ok(result.length === 1 && result[0].name === 'daveh', 'successfully queried application by name');
/* query application with invalid name*/
result = await request.get(`/Applications`, {
qs : {
name: 'daveh-invalid'
},
auth: authAdmin,
json: true,
});
t.ok(result.length === 0, 'successfully queried application by invalid name, no results found');
/* update applications */
result = await request.put(`/Applications/${sid}`, {

View File

@@ -0,0 +1,132 @@
const assert = require('node:assert');
const { describe, it, beforeEach, before } = require('node:test');
const { obscureBucketCredentialsSensitiveData, isObscureKey, obscureKey } = require('../lib/utils/encrypt-decrypt');
const { randomUUID } = require('node:crypto');
let bucket_credential = {};
let service_key = {};
describe('encrypt-decrypt test suite', { skip: false }, () => {
before(() => {
process.env.ENCRYPTION_SECRET = "12345";
});
beforeEach(() => {
service_key = {
"type": "service_account",
"project_id": "voice-gateway",
"private_key_id": randomUUID(),
"private_key": "-----BEGIN PRIVATE KEY-----\nJ6T2sQmBAD8TBm0frkGeyw0FA0aVAASVBJcwggSjBgEAAoabAQDUbiVwgufJm9R3\nv7YpECxRnQgHduX8jnbjtubi6FVqqVx78W3NM/gFNJFBC7NXtpsslqefKM6SmlkK\nNP6XqXy/ppXO8u00se7+cOFhsS6crncnsCeIYPxLFPV8P4BjExi7v88RBdektLeV\nX6sRWhxiYeWe+ORxkyC0KR4IKZjHt7ZHrg0kNQyuNx1KOhJnN3rRUkKSP2zozd1c\n3V+4EfpZjGmlQegXNHzkwUjvTOe6nuyxynWe3smjsSw/3RQda5m674Kh9tnVWEiX\n5KWnfRbWDdEBp5azzOAdeSR5W+qQfS0Jo6blREDQxWfMNmut87m81gmn+DKMhq6k\nV0JVRQStAgMBAAECggEAAf7AsAdI24zUdXkZv98gEuC4S10e2Luoz6Yvm41V3sP6\nWx0mdgY7RB9RW8U4WPnu3GhPGGJj03SyHUuI8Ix15MNM9WGDAaV3S+kRb1LqChLO\nCoNSO/qYPPg4t1iQ9+s7sWTnM93MAXjujmSveHJd7+MrUQOxOdPjB7I/ozMkBXBb\nWYsBIfeOG/7DsC4N4V/hKVXAq9NekGQv85yCUPR/DpuG9vqpztXwaSC1Wihntlu3\nNJYmMave4PbO3OxAekBl70WmukERZo7ksR+34WWse4HXlphaUVbpvnbQdGT1EzXW\nZukanqpfkIwrHme2Ko5NdP0C54pRhg6kpmWszVUMQQKBgQD6yWMuuwdPuFLg0wc2\nnDUeyHZSq/SEMSrb1wNXhL3qhaL2tCjBZwG9CHDuFMhqDLU5F36mEdAsM5MHCzyC\nTJ7VbqvCFz69SRt1CVp76Hu4HO/1Nhxm1GhF+NKSDIbnUg3o4DaC7EUtLpqYXcWj\nsXHEqVEhkrNVQ/JOIfJr42LDfQKBgQDM0U3ghGg9ufe9mb0dOonwtJA/ay/pd08Q\nyq3ue9C3yoQiNf28bP3AGKIjhA6wtd0KTSkQs6ktabGHIM8o/eTrMAMQllKh/3xe\nON7iND8Xz2GFMuIraQ7Mq9RvYWiqqIkVg1GQfJmiQ9wcmGj2PHy25LfjBXfHAYqK\nQ++P/i+s8QKBgD0pRi4MYNEZY+T+skCoQfA69VheJWjj0M8ClgcPEX4Tj1XZRCM+\nqtbeKyR1Hxd19/BvgWyg5YMSJOZP4Dbq1sW4ktzn7F4faTnWySF05k9Vh1PnGXAe\nlzuRXlFOCsx5X3kOzVyKoKhPOFa2b8/nI5bRsD6e12uRAZP6hXO4ZcrFAoGABVJ/\nCpGGP+xgMq4XCvZldTrL8MnxQcjW5iHOKT9QaiY6DsWGZWoTofVB6VhaJV9kcgsV\nQRjaEZMIiPFiULdgRnhF7B1r4kfITI5/xDMFXLIH37U1yVj+iHUCnS5T0PN2NHfo\nG7ARMfU/eALB33ws5XfGC4Et3p78oaEoTX6WcJECgYEAhysSt4qieGRSXn24V0+u\ny/ubU4dysn4SFe8BB4bjgYa8v+6VwucU+nnU4wOwykEgJmzN/ukzpvFN0CkN8eAN\n8xwtjBX9Zc1S90Wf7/7IrQGlUnsSFpDh5TW+oCqVo8JK7UGxJHR1mCvJmYVmSk3c\nD4AMvJ2x/Z5d9NKIAzdET4o=\n-----END PRIVATE KEY-----\n",
"client_email": "voicegatewayteam@voice-gateway.iam.gserviceaccount.com",
"client_id": "333827616171177028875",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/cognigyvoicegatewayteam%40cognigy-voice-gateway.iam.gserviceaccount.com",
"universe_domain": "googleapis.com"
};
bucket_credential = {
"vendor": "google",
"service_key": "",
"connection_string": "",
"secret_access_key": ""
}
});
describe('obscureBucketCredentialsSensitiveData', { skip: false }, () => {
it('should obscure "private_key" - vendor: google', { skip: false }, () => {
bucket_credential = {
...bucket_credential,
"vendor": "google",
"service_key": JSON.stringify(service_key),
};
const result = obscureBucketCredentialsSensitiveData(bucket_credential);
const googleCredentials = JSON.parse(result.service_key);
assert.strictEqual(googleCredentials.private_key, '-----BEGIN PRIVATE KEY-----\nJ6T2sQXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX');
assert.strictEqual(googleCredentials.private_key_id, googleCredentials.private_key_id);
assert.strictEqual(googleCredentials.project_id, googleCredentials.project_id);
assert.strictEqual(googleCredentials.client_email, googleCredentials.client_email);
assert.strictEqual(googleCredentials.client_id, googleCredentials.client_id);
assert.strictEqual(googleCredentials.auth_uri, googleCredentials.auth_uri);
assert.strictEqual(googleCredentials.token_uri, googleCredentials.token_uri);
});
it('should skip obscure since it is already obscured. vendor: google - "private key"', { skip: false }, () => {
service_key.private_key = '-----BEGIN PRIVATE KEY-----\nJ6T2sQXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX';
bucket_credential = {
...bucket_credential,
"vendor": "google",
"service_key": JSON.stringify(service_key),
};
const result = obscureBucketCredentialsSensitiveData(bucket_credential);
const googleCredentials = JSON.parse(result.service_key);
assert.strictEqual(googleCredentials.private_key, '-----BEGIN PRIVATE KEY-----\nJ6T2sQXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX');
assert.strictEqual(googleCredentials.private_key_id, googleCredentials.private_key_id);
assert.strictEqual(googleCredentials.project_id, googleCredentials.project_id);
assert.strictEqual(googleCredentials.client_email, googleCredentials.client_email);
assert.strictEqual(googleCredentials.client_id, googleCredentials.client_id);
assert.strictEqual(googleCredentials.auth_uri, googleCredentials.auth_uri);
assert.strictEqual(googleCredentials.token_uri, googleCredentials.token_uri);
});
});
describe('isObscureKey', { skip: false }, () => {
it('vendor: google - should return true', { skip: false }, () => {
service_key.private_key = '-----BEGIN PRIVATE KEY-----\nJ6T2sQXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX';
bucket_credential = {
...bucket_credential,
"vendor": "google",
"service_key": JSON.stringify(service_key),
};
const result = isObscureKey(bucket_credential);
assert.strictEqual(result, true);
});
it('vendor: google - should return false', { skip: false }, () => {
bucket_credential = {
...bucket_credential,
"vendor": "google",
"service_key": JSON.stringify(service_key),
};
const result = isObscureKey(bucket_credential);
assert.strictEqual(result, false);
});
it('vendor: aws_s3 - should return true', { skip: false }, () => {
const obscuredKey = "J6T2sQXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
bucket_credential = {
...bucket_credential,
"vendor": "aws_s3",
"secret_access_key": obscuredKey,
};
const result = isObscureKey(bucket_credential);
assert.strictEqual(result, true);
});
it('vendor: aws_s3 - should return false', { skip: false }, () => {
bucket_credential = {
...bucket_credential,
"vendor": "aws_s3",
"service_key": "EFEU2fhcbqiw3211ffw3f1kezhcbqiw3211ffw3f",
};
const result = isObscureKey(bucket_credential);
assert.strictEqual(result, false);
});
it('vendor: azure - should return true', { skip: false }, () => {
const obscuredKey = 'https:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX';
bucket_credential = {
...bucket_credential,
"vendor": "azure",
"connection_string": obscuredKey,
};
const result = isObscureKey(bucket_credential);
assert.strictEqual(result, true);
});
it('vendor: azure - should return false', { skip: false }, () => {
bucket_credential = {
...bucket_credential,
"vendor": "azure",
connection_string: 'https://cognigydevstorage.blob.core.windows.net/voicegateway-test?sp=rw&st=2023-09-10T13:35:44Z&se=2023-09-11T21:35:44Z&spr=https&sv=2022-11-02&sr=c&sig=9WN8Bg5UMOvnV1h1cJpCnTUG%2FnonTbRZ1Q1rbKnDUl4%3D',
};
const result = isObscureKey(bucket_credential);
assert.strictEqual(result, false);
});
});
});

View File

@@ -17,6 +17,53 @@ test('sip gateway tests', async(t) => {
let result;
const voip_carrier_sid = await createVoipCarrier(request);
/* add a invalid sip gateway */
const STORED_JAMBONZ_MIN_GATEWAY_NETMASK = process.env.JAMBONZ_MIN_GATEWAY_NETMASK;
process.env.JAMBONZ_MIN_GATEWAY_NETMASK = 24;
result = await request.post('/SipGateways', {
resolveWithFullResponse: true,
auth: authAdmin,
json: true,
simple: false,
body: {
voip_carrier_sid,
ipv4: '1.2.3.4',
netmask: 1,
inbound: true,
outbound: true,
protocol: 'tcp'
}
});
t.ok(result.statusCode === 400, 'successfully created sip gateway ');
result = await request.post('/SipGateways', {
resolveWithFullResponse: true,
auth: authAdmin,
json: true,
body: {
voip_carrier_sid,
ipv4: '1.2.3.4',
netmask: 24,
inbound: true,
outbound: true,
protocol: 'tcp'
}
});
t.ok(result.statusCode === 201, 'successfully created sip gateway ');
process.env.JAMBONZ_MIN_GATEWAY_NETMASK = STORED_JAMBONZ_MIN_GATEWAY_NETMASK;
/* delete sip gateways */
result = await request.delete(`/SipGateways/${result.body.sid}`, {
resolveWithFullResponse: true,
simple: false,
json: true,
auth: authAdmin
});
//console.log(`result: ${JSON.stringify(result)}`);
t.ok(result.statusCode === 204, 'successfully deleted sip gateway');
/* add a sip gateway */
result = await request.post('/SipGateways', {
resolveWithFullResponse: true,

View File

@@ -371,7 +371,8 @@ test('speech credentials tests', async(t) => {
vendor: 'deepgram',
use_for_stt: true,
deepgram_stt_uri: "127.0.0.1:50002",
deepgram_stt_use_tls: true
deepgram_stt_use_tls: true,
deepgram_tts_uri: 'https://server.com'
}
});
t.ok(result.statusCode === 201, 'successfully added speech credential for deepgram');
@@ -386,6 +387,7 @@ test('speech credentials tests', async(t) => {
t.ok(result.statusCode === 200, 'successfully get speech credential for deepgram');
t.ok(result.body.deepgram_stt_uri === '127.0.0.1:50002', "deepgram_stt_uri is correct for deepgram");
t.ok(result.body.deepgram_stt_use_tls === true, "deepgram_stt_use_tls is correct for deepgram");
t.ok(result.body.deepgram_tts_uri === 'https://server.com', "deepgram_tts_uri is correct for deepgram")
result = await request.put(`/Accounts/${account_sid}/SpeechCredentials/${dg_sid}`, {
resolveWithFullResponse: true,
@@ -395,7 +397,8 @@ test('speech credentials tests', async(t) => {
vendor: 'deepgram',
use_for_stt: true,
deepgram_stt_uri: "127.0.0.2:50002",
deepgram_stt_use_tls: false
deepgram_stt_use_tls: false,
deepgram_tts_uri: 'https://server2.com'
}
});
t.ok(result.statusCode === 204, 'successfully updated speech credential for deepgram onprem');
@@ -409,6 +412,7 @@ test('speech credentials tests', async(t) => {
t.ok(result.statusCode === 200, 'successfully get speech credential for deepgram onprem');
t.ok(result.body.deepgram_stt_uri === '127.0.0.2:50002', "deepgram_stt_uri is correct for deepgram onprem");
t.ok(result.body.deepgram_stt_use_tls === false, "deepgram_stt_use_tls is correct for deepgram onprem");
t.ok(result.body.deepgram_tts_uri === 'https://server2.com', "deepgram_tts_uri is correct for deepgram onprem");
result = await request.delete(`/Accounts/${account_sid}/SpeechCredentials/${dg_sid}`, {
auth: authUser,
@@ -528,6 +532,39 @@ test('speech credentials tests', async(t) => {
t.ok(result.statusCode === 204, 'successfully deleted speech credential');
}
/* add a credential for Speechmatics */
if (process.env.SPEECHMATICS_API_KEY) {
result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, {
resolveWithFullResponse: true,
auth: authUser,
json: true,
body: {
vendor: 'speechmatics',
use_for_stt: true,
api_key: process.env.SPEECHMATICS_API_KEY,
speechmatics_stt_uri: 'eu2.rt.speechmatics.com'
}
});
t.ok(result.statusCode === 201, 'successfully added speech credential for speechmatics');
const ms_sid = result.body.sid;
/* test the speech credential */
result = await request.get(`/Accounts/${account_sid}/SpeechCredentials/${ms_sid}/test`, {
resolveWithFullResponse: true,
auth: authUser,
json: true,
});
console.log(JSON.stringify(result));
t.ok(result.statusCode === 200 && result.body.stt.status === 'ok', 'successfully tested speech credential for speechmatics');
/* delete the credential */
result = await request.delete(`/Accounts/${account_sid}/SpeechCredentials/${ms_sid}`, {
auth: authUser,
resolveWithFullResponse: true,
});
t.ok(result.statusCode === 204, 'successfully deleted speech credential');
}
/* add a credential for nvidia */
result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, {
resolveWithFullResponse: true,
@@ -743,6 +780,27 @@ test('speech credentials tests', async(t) => {
});
t.ok(result.statusCode === 204, 'successfully deleted speech credential');
/* add a credential for Voxist */
result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, {
resolveWithFullResponse: true,
auth: authUser,
json: true,
body: {
vendor: 'voxist',
use_for_stt: true,
api_key: "APIKEY"
}
});
t.ok(result.statusCode === 201, 'successfully added speech credential for Voxist');
const voxistSid = result.body.sid;
/* delete the credential */
result = await request.delete(`/Accounts/${account_sid}/SpeechCredentials/${voxistSid}`, {
auth: authUser,
resolveWithFullResponse: true,
});
t.ok(result.statusCode === 204, 'successfully deleted speech credential');
/* add a credential for aws polly by roleArn */
result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, {
resolveWithFullResponse: true,
@@ -935,6 +993,15 @@ test('speech credentials tests', async(t) => {
});
t.ok(result.body.stt.length !== 0, 'successfully get assemblyai supported languages and voices');
/* Check voxist supportedLanguagesAndVoices */
result = await request.get(`/Accounts/${account_sid}/SpeechCredentials/speech/supportedLanguagesAndVoices?vendor=voxist`, {
resolveWithFullResponse: true,
simple: false,
auth: authAdmin,
json: true,
});
t.ok(result.body.stt.length !== 0, 'successfully get voxist supported languages and voices');
/* Check whisper supportedLanguagesAndVoices */
result = await request.get(`/Accounts/${account_sid}/SpeechCredentials/speech/supportedLanguagesAndVoices?vendor=whisper`, {
resolveWithFullResponse: true,

View File

@@ -16,7 +16,9 @@ test('system information test', async(t) => {
body: {
domain_name: 'test.com',
sip_domain_name: 'sip.test.com',
monitoring_domain_name: 'monitor.test.com'
monitoring_domain_name: 'monitor.test.com',
private_network_cidr: '192.168.1.0/24, 10.10.100.1',
log_level: 'info'
}
});
t.ok(result.statusCode === 201, 'successfully created system information ');
@@ -24,6 +26,8 @@ test('system information test', async(t) => {
t.ok(body.domain_name === 'test.com', 'added domain_name ok');
t.ok(body.sip_domain_name === 'sip.test.com', 'added sip_domain_name ok');
t.ok(body.monitoring_domain_name === 'monitor.test.com', 'added monitoring_domain_name ok');
t.ok(body.private_network_cidr === '192.168.1.0/24, 10.10.100.1', 'added private_network_cidr ok');
t.ok(body.log_level === 'info', 'added log_level ok');
result = await request.get('/SystemInformation', {
auth: authAdmin,
@@ -32,6 +36,8 @@ test('system information test', async(t) => {
t.ok(result.domain_name === 'test.com', 'get domain_name ok');
t.ok(result.sip_domain_name === 'sip.test.com', 'get sip_domain_name ok');
t.ok(result.monitoring_domain_name === 'monitor.test.com', 'get monitoring_domain_name ok');
t.ok(result.private_network_cidr === '192.168.1.0/24, 10.10.100.1', 'get private_network_cidr ok');
t.ok(result.log_level === 'info', 'added log_level ok');
result = await request.post('/SystemInformation', {
resolveWithFullResponse: true,
@@ -40,7 +46,9 @@ test('system information test', async(t) => {
body: {
domain_name: 'test1.com',
sip_domain_name: 'sip1.test.com',
monitoring_domain_name: 'monitor1.test.com'
monitoring_domain_name: 'monitor1.test.com',
private_network_cidr: '',
log_level: 'debug'
}
});
t.ok(result.statusCode === 201, 'successfully updated system information ');
@@ -48,6 +56,8 @@ test('system information test', async(t) => {
t.ok(body.domain_name === 'test1.com', 'updated domain_name ok');
t.ok(body.sip_domain_name === 'sip1.test.com', 'updated sip_domain_name ok');
t.ok(body.monitoring_domain_name === 'monitor1.test.com', 'updated monitoring_domain_name ok');
t.ok(body.private_network_cidr === '', 'updated private_network_cidr ok');
t.ok(body.log_level === 'debug', 'updated log_level ok');
result = await request.get('/SystemInformation', {
auth: authAdmin,
@@ -56,6 +66,7 @@ test('system information test', async(t) => {
t.ok(result.domain_name === 'test1.com', 'get domain_name ok');
t.ok(result.sip_domain_name === 'sip1.test.com', 'get sip_domain_name ok');
t.ok(result.monitoring_domain_name === 'monitor1.test.com', 'get monitoring_domain_name ok');
t.ok(result.log_level === 'debug', 'updated log_level ok');
} catch(err) {
console.error(err);

View File

@@ -24,16 +24,24 @@ function makeSynthKey({account_sid = '', vendor, language, voice, engine = '', t
test('tts-cache', async(t) => {
const app = require('../app');
try {
// clear cache to start
let result = await request.delete('/TtsCache', {
auth: authAdmin,
resolveWithFullResponse: true,
});
t.ok(result.statusCode === 204, 'successfully purged cache for start of test');
// create caches
const minRecords = 8;
for (const i in Array(minRecords).fill(0)) {
await client.set(makeSynthKey({vendor: i, language: i, voice: i, engine: i, text: i}), i);
}
let result = await request.get('/TtsCache', {
result = await request.get('/TtsCache', {
auth: authAdmin,
json: true,
});
//console.log(result);
t.ok(result.size === minRecords, 'get cache correctly');
@@ -41,7 +49,7 @@ test('tts-cache', async(t) => {
auth: authAdmin,
resolveWithFullResponse: true,
});
t.ok(result.statusCode === 204, 'successfully deleted application after removing phone number');
t.ok(result.statusCode === 204, 'successfully purged cache');
result = await request.get('/TtsCache', {
auth: authAdmin,

View File

@@ -15,6 +15,7 @@ test('voip carrier tests', async(t) => {
let sid;
try {
let result;
const outbound_sip_proxy = 'foo.bar.com';
/* add a voip carrier */
result = await request.post('/VoipCarriers', {
@@ -23,7 +24,8 @@ test('voip carrier tests', async(t) => {
json: true,
body: {
name: 'daveh',
e164_leading_plus: true
e164_leading_plus: true,
outbound_sip_proxy,
}
});
t.ok(result.statusCode === 201, 'successfully created voip carrier');
@@ -35,6 +37,7 @@ test('voip carrier tests', async(t) => {
json: true,
});
t.ok(result.length === 1 && result[0].e164_leading_plus, 'successfully queried all voip carriers');
t.ok(result.length === 1 && result[0].outbound_sip_proxy === outbound_sip_proxy, 'successfully queried all voip carriers');
/* query one voip carriers */
result = await request.get(`/VoipCarriers/${sid}`, {
@@ -207,7 +210,8 @@ test('voip carrier tests', async(t) => {
json: true,
body: {
name: 'twilio',
e164_leading_plus: true
e164_leading_plus: true,
dtmf_type: 'tones'
}
});
t.ok(result.statusCode === 201, 'successfully created voip carrier for a service provider');
@@ -221,6 +225,7 @@ test('voip carrier tests', async(t) => {
});
//console.log(result.body);
t.ok(result.statusCode === 200, 'successfully retrieved voip carrier for a service provider');
//console.log(result.body);
sid = result.body[0].voip_carrier_sid;
await deleteObjectBySid(request, '/VoipCarriers', sid);