mirror of
https://github.com/jambonz/jambonz-api-server.git
synced 2026-01-25 02:08:24 +00:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38afe0da18 | ||
|
|
0d66dc9c27 | ||
|
|
e9d14e9e38 | ||
|
|
1d609135fc | ||
|
|
16dcd26216 | ||
|
|
42f4318a17 | ||
|
|
0f1f5e9b73 | ||
|
|
bcff9b35a6 | ||
|
|
8267ddaffd | ||
|
|
c3d12fafee | ||
|
|
9421bb8aa1 | ||
|
|
a15c5cd267 | ||
|
|
4de66789ef | ||
|
|
a297d2038f | ||
|
|
2e0ea56925 | ||
|
|
9c8bfebd53 | ||
|
|
035458ad3c | ||
|
|
fd9dc77a58 | ||
|
|
2b66a121a0 | ||
|
|
3a6d10e725 | ||
|
|
6f87204d88 | ||
|
|
9854666d4f | ||
|
|
0d4b7e88ad | ||
|
|
819319dbe5 |
@@ -35,6 +35,7 @@ Configuration is provided via environment variables:
|
||||
|K8S_FEATURE_SERVER_SERVICE_PORT| feature server port(required for K8S) |no|
|
||||
|JAMBONZ_RECORD_WS_USERNAME| recording websocket username|no|
|
||||
|JAMBONZ_RECORD_WS_PASSWORD| recording websocket password|no|
|
||||
|DISABLE_RATE_LIMITS| disable rate limiting|no
|
||||
|
||||
#### Database dependency
|
||||
A mysql database is used to store long-lived objects such as Accounts, Applications, etc. To create the database schema, use or review the scripts in the 'db' folder, particularly:
|
||||
|
||||
13
app.js
13
app.js
@@ -48,7 +48,8 @@ const {
|
||||
retrieveKey,
|
||||
deleteKey,
|
||||
incrKey,
|
||||
listConferences
|
||||
listConferences,
|
||||
getCallCount
|
||||
} = require('./lib/helpers/realtimedb-helpers');
|
||||
const {
|
||||
getTtsVoices,
|
||||
@@ -118,7 +119,8 @@ app.locals = {
|
||||
queryAlertsSP,
|
||||
writeCdrs,
|
||||
writeAlerts,
|
||||
AlertType
|
||||
AlertType,
|
||||
getCallCount
|
||||
};
|
||||
|
||||
const unless = (paths, middleware) => {
|
||||
@@ -168,7 +170,12 @@ if (process.env.JAMBONES_TRUST_PROXY) {
|
||||
});
|
||||
}
|
||||
}
|
||||
app.use(limiter);
|
||||
|
||||
const disableRateLimit = process.env.DISABLE_RATE_LIMITS === 'true' || process.env.DISABLE_RATE_LIMITS === '1';
|
||||
|
||||
if (!disableRateLimit) {
|
||||
app.use(limiter);
|
||||
}
|
||||
app.use(helmet());
|
||||
app.use(helmet.hidePoweredBy());
|
||||
app.use(nocache());
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
/* SQLEditor (MySQL (2))*/
|
||||
|
||||
SET FOREIGN_KEY_CHECKS=0;
|
||||
|
||||
DROP TABLE IF EXISTS account_static_ips;
|
||||
@@ -418,6 +417,7 @@ 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),
|
||||
trunk_type ENUM('static_ip','auth','reg') NOT NULL DEFAULT 'static_ip',
|
||||
PRIMARY KEY (voip_carrier_sid)
|
||||
) COMMENT='A Carrier or customer PBX that can send or receive calls';
|
||||
|
||||
|
||||
@@ -546,12 +546,12 @@
|
||||
<schema><![CDATA[]]></schema>
|
||||
<comment><![CDATA[A Carrier or customer PBX that can send or receive calls]]></comment>
|
||||
<location>
|
||||
<x>16.00</x>
|
||||
<y>427.00</y>
|
||||
<x>20.00</x>
|
||||
<y>418.00</y>
|
||||
</location>
|
||||
<size>
|
||||
<width>293.00</width>
|
||||
<height>580.00</height>
|
||||
<height>600.00</height>
|
||||
</size>
|
||||
<zorder>6</zorder>
|
||||
<SQLField>
|
||||
@@ -749,6 +749,13 @@
|
||||
<type><![CDATA[VARCHAR(255)]]></type>
|
||||
<uid><![CDATA[556ABA45-BC63-444D-8CB1-973EFCCF9FE7]]></uid>
|
||||
</SQLField>
|
||||
<SQLField>
|
||||
<name><![CDATA[trunk_type]]></name>
|
||||
<type><![CDATA[ENUM('static_ip','auth','reg')]]></type>
|
||||
<defaultValue><![CDATA[static_ip]]></defaultValue>
|
||||
<notNull><![CDATA[1]]></notNull>
|
||||
<uid><![CDATA[CCF1560C-349E-4DB9-91E5-120F1EDB7CDE]]></uid>
|
||||
</SQLField>
|
||||
<labelWindowIndex><![CDATA[28]]></labelWindowIndex>
|
||||
<objectComment><![CDATA[A Carrier or customer PBX that can send or receive calls]]></objectComment>
|
||||
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
|
||||
@@ -1293,7 +1300,7 @@
|
||||
<comment><![CDATA[a regex-based pattern match for call routing]]></comment>
|
||||
<location>
|
||||
<x>16.00</x>
|
||||
<y>1007.00</y>
|
||||
<y>1039.00</y>
|
||||
</location>
|
||||
<size>
|
||||
<width>254.00</width>
|
||||
@@ -2165,8 +2172,8 @@
|
||||
<schema><![CDATA[]]></schema>
|
||||
<comment><![CDATA[A phone number that has been assigned to an account]]></comment>
|
||||
<location>
|
||||
<x>16.00</x>
|
||||
<y>1128.00</y>
|
||||
<x>11.00</x>
|
||||
<y>1162.00</y>
|
||||
</location>
|
||||
<size>
|
||||
<width>522.00</width>
|
||||
@@ -2608,6 +2615,7 @@
|
||||
<SQLField>
|
||||
<name><![CDATA[speech_synthesis_voice]]></name>
|
||||
<type><![CDATA[VARCHAR(256)]]></type>
|
||||
<defaultValue><![CDATA[en-US-Standard-C]]></defaultValue>
|
||||
<notNull><![CDATA[0]]></notNull>
|
||||
<uid><![CDATA[929D66F0-64B9-4D7C-AB4B-24F131E1178F]]></uid>
|
||||
</SQLField>
|
||||
@@ -3161,17 +3169,17 @@
|
||||
<overviewPanelHidden><![CDATA[0]]></overviewPanelHidden>
|
||||
<pageBoundariesVisible><![CDATA[0]]></pageBoundariesVisible>
|
||||
<PageGridVisible><![CDATA[0]]></PageGridVisible>
|
||||
<RightSidebarWidth><![CDATA[1393.000000]]></RightSidebarWidth>
|
||||
<RightSidebarWidth><![CDATA[1643.000000]]></RightSidebarWidth>
|
||||
<sidebarIndex><![CDATA[2]]></sidebarIndex>
|
||||
<snapToGrid><![CDATA[0]]></snapToGrid>
|
||||
<SourceSidebarWidth><![CDATA[0.000000]]></SourceSidebarWidth>
|
||||
<SourceSidebarWidth><![CDATA[312.000000]]></SourceSidebarWidth>
|
||||
<SQLEditorFileFormatVersion><![CDATA[4]]></SQLEditorFileFormatVersion>
|
||||
<uid><![CDATA[58C99A00-06C9-478C-A667-C63842E088F3]]></uid>
|
||||
<windowHeight><![CDATA[1055.000000]]></windowHeight>
|
||||
<windowLocationX><![CDATA[1845.000000]]></windowLocationX>
|
||||
<windowLocationX><![CDATA[1728.000000]]></windowLocationX>
|
||||
<windowLocationY><![CDATA[37.000000]]></windowLocationY>
|
||||
<windowScrollOrigin><![CDATA[{0, 544}]]></windowScrollOrigin>
|
||||
<windowWidth><![CDATA[1670.000000]]></windowWidth>
|
||||
<windowScrollOrigin><![CDATA[{0, 376}]]></windowScrollOrigin>
|
||||
<windowWidth><![CDATA[1920.000000]]></windowWidth>
|
||||
</SQLDocumentInfo>
|
||||
<AllowsIndexRenamingOnInsert><![CDATA[1]]></AllowsIndexRenamingOnInsert>
|
||||
<defaultLabelExpanded><![CDATA[1]]></defaultLabelExpanded>
|
||||
|
||||
@@ -231,7 +231,8 @@ const sql = {
|
||||
],
|
||||
9005: [
|
||||
'UPDATE applications SET speech_synthesis_voice = \'en-US-Standard-C\' WHERE speech_synthesis_voice IS NULL AND speech_synthesis_vendor = \'google\' AND speech_synthesis_language = \'en-US\'',
|
||||
'ALTER TABLE applications MODIFY COLUMN speech_synthesis_voice VARCHAR(255) DEFAULT \'en-US-Standard-C\''
|
||||
'ALTER TABLE applications MODIFY COLUMN speech_synthesis_voice VARCHAR(255) DEFAULT \'en-US-Standard-C\'',
|
||||
'ALTER TABLE voip_carriers ADD COLUMN trunk_type ENUM(\'static_ip\',\'auth\',\'reg\') NOT NULL DEFAULT \'static_ip\'',
|
||||
]
|
||||
};
|
||||
|
||||
@@ -267,6 +268,8 @@ const doIt = async() => {
|
||||
if (val < 9000) upgrades.push(...sql['9000']);
|
||||
if (val < 9002) upgrades.push(...sql['9002']);
|
||||
if (val < 9003) upgrades.push(...sql['9003']);
|
||||
if (val < 9004) upgrades.push(...sql['9004']);
|
||||
if (val < 9005) upgrades.push(...sql['9005']);
|
||||
|
||||
// perform all upgrades
|
||||
logger.info({upgrades}, 'applying schema upgrades..');
|
||||
|
||||
@@ -13,7 +13,8 @@ const {
|
||||
deleteKey,
|
||||
incrKey,
|
||||
client: redisClient,
|
||||
listConferences
|
||||
listConferences,
|
||||
getCallCount
|
||||
} = require('@jambonz/realtimedb-helpers')({}, logger);
|
||||
|
||||
module.exports = {
|
||||
@@ -29,5 +30,6 @@ module.exports = {
|
||||
deleteKey,
|
||||
redisClient,
|
||||
incrKey,
|
||||
listConferences
|
||||
listConferences,
|
||||
getCallCount
|
||||
};
|
||||
|
||||
@@ -47,10 +47,14 @@ class ApiKey extends Model {
|
||||
}
|
||||
|
||||
/**
|
||||
* update last_used api key for an account
|
||||
*/
|
||||
* update last_used api key for an account
|
||||
* (only if last_used is null or more than a minute ago)
|
||||
*/
|
||||
static updateLastUsed(account_sid) {
|
||||
const sql = 'UPDATE api_keys SET last_used = NOW() WHERE account_sid = ?';
|
||||
const sql = `UPDATE api_keys
|
||||
SET last_used = NOW()
|
||||
WHERE account_sid = ?
|
||||
AND (last_used IS NULL OR last_used < NOW() - INTERVAL 1 MINUTE)`;
|
||||
const args = [account_sid];
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
@@ -15,7 +15,9 @@ class S3MultipartUploadStream extends Writable {
|
||||
this.uploadId = null;
|
||||
this.partNumber = 1;
|
||||
this.multipartETags = [];
|
||||
this.buffer = Buffer.alloc(0);
|
||||
// accumulate incoming chunks to avoid O(n^2) Buffer.concat on every write
|
||||
this.chunks = [];
|
||||
this.bufferedBytes = 0;
|
||||
this.minPartSize = 5 * 1024 * 1024; // 5 MB
|
||||
this.s3 = new S3Client(opts.bucketCredential);
|
||||
this.metadata = opts.metadata;
|
||||
@@ -31,13 +33,13 @@ class S3MultipartUploadStream extends Writable {
|
||||
return response.UploadId;
|
||||
}
|
||||
|
||||
async _uploadBuffer() {
|
||||
async _uploadPart(bodyBuffer) {
|
||||
const uploadPartCommand = new UploadPartCommand({
|
||||
Bucket: this.bucketName,
|
||||
Key: this.objectKey,
|
||||
PartNumber: this.partNumber,
|
||||
UploadId: this.uploadId,
|
||||
Body: this.buffer,
|
||||
Body: bodyBuffer,
|
||||
});
|
||||
|
||||
const uploadPartResponse = await this.s3.send(uploadPartCommand);
|
||||
@@ -54,11 +56,16 @@ class S3MultipartUploadStream extends Writable {
|
||||
this.uploadId = await this._initMultipartUpload();
|
||||
}
|
||||
|
||||
this.buffer = Buffer.concat([this.buffer, chunk]);
|
||||
// accumulate without concatenating on every write
|
||||
this.chunks.push(chunk);
|
||||
this.bufferedBytes += chunk.length;
|
||||
|
||||
if (this.buffer.length >= this.minPartSize) {
|
||||
await this._uploadBuffer();
|
||||
this.buffer = Buffer.alloc(0);
|
||||
if (this.bufferedBytes >= this.minPartSize) {
|
||||
const partBuffer = Buffer.concat(this.chunks, this.bufferedBytes);
|
||||
// reset accumulators before awaiting upload to allow GC
|
||||
this.chunks = [];
|
||||
this.bufferedBytes = 0;
|
||||
await this._uploadPart(partBuffer);
|
||||
}
|
||||
|
||||
callback(null);
|
||||
@@ -69,8 +76,11 @@ class S3MultipartUploadStream extends Writable {
|
||||
|
||||
async _finalize(err) {
|
||||
try {
|
||||
if (this.buffer.length > 0) {
|
||||
await this._uploadBuffer();
|
||||
if (this.bufferedBytes > 0) {
|
||||
const finalBuffer = Buffer.concat(this.chunks, this.bufferedBytes);
|
||||
this.chunks = [];
|
||||
this.bufferedBytes = 0;
|
||||
await this._uploadPart(finalBuffer);
|
||||
}
|
||||
|
||||
const completeMultipartUploadCommand = new CompleteMultipartUploadCommand({
|
||||
|
||||
@@ -51,8 +51,10 @@ async function upload(logger, socket) {
|
||||
|
||||
/**encoder */
|
||||
let encoder;
|
||||
let recordFormat;
|
||||
if (account[0].record_format === 'wav') {
|
||||
encoder = new wav.Writer({ channels: 2, sampleRate, bitDepth: 16 });
|
||||
recordFormat = 'wav';
|
||||
} else {
|
||||
// default is mp3
|
||||
encoder = new PCMToMP3Encoder({
|
||||
@@ -60,7 +62,9 @@ async function upload(logger, socket) {
|
||||
sampleRate: sampleRate,
|
||||
bitrate: 128
|
||||
}, logger);
|
||||
recordFormat = 'mp3';
|
||||
}
|
||||
logger.info({ record_format: recordFormat, channels: 2, sampleRate }, 'record upload: selected encoder');
|
||||
|
||||
/* start streaming data */
|
||||
pipeline(
|
||||
|
||||
@@ -161,6 +161,9 @@ router.post('/:sid/VoipCarriers', async(req, res) => {
|
||||
try {
|
||||
const account_sid = parseAccountSid(req);
|
||||
await validateRequest(req, account_sid);
|
||||
// Set the service_provder_sid to the relevent value for the account
|
||||
const account = await Account.retrieve(req.user.account_sid);
|
||||
payload.service_provider_sid = account[0].service_provider_sid;
|
||||
|
||||
logger.debug({payload}, 'POST /:sid/VoipCarriers');
|
||||
const uuid = await VoipCarrier.make({
|
||||
@@ -1220,4 +1223,23 @@ router.get('/:sid/Conferences', async(req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* retrieve counts of calls under an account
|
||||
*/
|
||||
router.get('/:sid/CallCount', async(req, res) => {
|
||||
const {logger, getCallCount} = req.app.locals;
|
||||
try {
|
||||
const accountSid = parseAccountSid(req);
|
||||
await validateRequest(req, accountSid);
|
||||
const count = await getCallCount(accountSid);
|
||||
count.outbound = Number(count.outbound);
|
||||
count.inbound = Number(count.inbound);
|
||||
logger.debug(`retrieved, outbound: ${count.outbound}, inbound: ${count.inbound}, for account sid ${accountSid}`);
|
||||
res.status(200).json(count);
|
||||
updateLastUsed(logger, accountSid, req).catch((err) => {});
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -52,20 +52,21 @@ router.post('/', async(req, res) => {
|
||||
let obj;
|
||||
try {
|
||||
if (!email || !validateEmail(email)) {
|
||||
logger.info({email}, 'Bad POST to /forgot-password is missing email or invalid email');
|
||||
return res.status(400).json({error: 'invalid or missing email'});
|
||||
}
|
||||
|
||||
const [r] = await promisePool.query({sql, nestTables: true}, email);
|
||||
if (0 === r.length) {
|
||||
logger.info('user not found');
|
||||
logger.info(`user not found: ${email}`);
|
||||
return res.status(400).json({error: 'failed to reset your password'});
|
||||
}
|
||||
obj = r[0];
|
||||
if (!obj.user.is_active) {
|
||||
logger.info(obj.user.name, 'user is inactive');
|
||||
logger.info({user: obj.user.name, obj}, 'user is inactive');
|
||||
return res.status(400).json({error: 'failed to reset your password'});
|
||||
} else if (obj.acc.account_sid !== null && !obj.acc.is_active) {
|
||||
logger.info(obj.acc.account_sid, 'account is inactive');
|
||||
logger.info({account_sid: obj.acc.account_sid, obj}, 'account is inactive');
|
||||
return res.status(400).json({error: 'failed to reset your password'});
|
||||
}
|
||||
res.sendStatus(204);
|
||||
|
||||
@@ -46,10 +46,16 @@ async function validateRetrieve(req) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.user.hasScope('service_provider') || req.user.hasScope('account')) {
|
||||
if (req.user.hasScope('service_provider')) {
|
||||
if (service_provider_sid === req.user.service_provider_sid) return;
|
||||
}
|
||||
|
||||
if (req.user.hasScope('account')) {
|
||||
const results = await Account.retrieve(req.user.account_sid);
|
||||
if (service_provider_sid === results[0].service_provider_sid) return;
|
||||
}
|
||||
|
||||
|
||||
throw new DbErrorForbidden('insufficient permissions');
|
||||
} catch (error) {
|
||||
throw error;
|
||||
|
||||
@@ -18,8 +18,7 @@ const checkUserScope = async(req, voip_carrier_sid) => {
|
||||
const carrier = await lookupCarrierBySid(voip_carrier_sid);
|
||||
if (!carrier) throw new DbErrorBadRequest('invalid voip_carrier_sid');
|
||||
|
||||
if ((!carrier.service_provider_sid || carrier.service_provider_sid === req.user.service_provider_sid) &&
|
||||
(!carrier.account_sid || carrier.account_sid === req.user.account_sid)) {
|
||||
if (!carrier.account_sid || carrier.account_sid === req.user.account_sid) {
|
||||
|
||||
if (req.method !== 'GET' && !carrier.account_sid) {
|
||||
throw new DbErrorForbidden('insufficient privileges');
|
||||
|
||||
@@ -15,7 +15,10 @@ const {decryptCredential, testWhisper, testDeepgramTTS,
|
||||
testCartesia,
|
||||
testVoxistStt,
|
||||
testOpenAiStt,
|
||||
testInworld} = require('../../utils/speech-utils');
|
||||
testInworld,
|
||||
testResembleTTS,
|
||||
testHoundifyStt,
|
||||
testGladiaStt} = require('../../utils/speech-utils');
|
||||
const {DbErrorUnprocessableRequest, DbErrorForbidden, DbErrorBadRequest} = require('../../utils/errors');
|
||||
const {
|
||||
testGoogleTts,
|
||||
@@ -123,6 +126,7 @@ const encryptCredential = (obj) => {
|
||||
role_arn,
|
||||
region,
|
||||
client_id,
|
||||
client_key,
|
||||
client_secret,
|
||||
secret,
|
||||
nuance_tts_uri,
|
||||
@@ -132,6 +136,8 @@ const encryptCredential = (obj) => {
|
||||
deepgram_stt_use_tls,
|
||||
deepgram_tts_uri,
|
||||
playht_tts_uri,
|
||||
resemble_tts_uri,
|
||||
resemble_tts_use_tls,
|
||||
use_custom_tts,
|
||||
custom_tts_endpoint,
|
||||
custom_tts_endpoint_url,
|
||||
@@ -159,6 +165,7 @@ const encryptCredential = (obj) => {
|
||||
voice_engine,
|
||||
engine_version,
|
||||
service_version,
|
||||
api_uri,
|
||||
options
|
||||
} = obj;
|
||||
|
||||
@@ -226,6 +233,24 @@ const encryptCredential = (obj) => {
|
||||
deepgram_stt_use_tls, deepgram_tts_uri, model_id});
|
||||
return encrypt(deepgramData);
|
||||
|
||||
case 'gladia':
|
||||
const gladiaData = JSON.stringify({api_key, region});
|
||||
return encrypt(gladiaData);
|
||||
|
||||
case 'resemble':
|
||||
assert(api_key, 'invalid resemble speech credential: api_key is required');
|
||||
const resembleData = JSON.stringify({
|
||||
api_key,
|
||||
...(resemble_tts_uri && {resemble_tts_uri}),
|
||||
...(resemble_tts_use_tls && {resemble_tts_use_tls})
|
||||
});
|
||||
return encrypt(resembleData);
|
||||
|
||||
case 'deepgramflux':
|
||||
assert(api_key, 'invalid deepgram flux speech credential: api_key is required');
|
||||
const deepgramfluxData = JSON.stringify({api_key});
|
||||
return encrypt(deepgramfluxData);
|
||||
|
||||
case 'ibm':
|
||||
const ibmData = JSON.stringify({tts_api_key, tts_region, stt_api_key, stt_region, instance_id});
|
||||
return encrypt(ibmData);
|
||||
@@ -248,7 +273,11 @@ const encryptCredential = (obj) => {
|
||||
case 'elevenlabs':
|
||||
assert(api_key, 'invalid elevenLabs speech credential: api_key is required');
|
||||
assert(model_id, 'invalid elevenLabs speech credential: model_id is required');
|
||||
const elevenlabsData = JSON.stringify({api_key, model_id, options});
|
||||
const elevenlabsData = JSON.stringify({
|
||||
api_key,
|
||||
model_id,
|
||||
...(api_uri && {api_uri}),
|
||||
options});
|
||||
return encrypt(elevenlabsData);
|
||||
|
||||
case 'speechmatics':
|
||||
@@ -296,6 +325,13 @@ const encryptCredential = (obj) => {
|
||||
const assemblyaiData = JSON.stringify({api_key, service_version});
|
||||
return encrypt(assemblyaiData);
|
||||
|
||||
case 'houndify':
|
||||
assert(client_id, 'invalid houndify speech credential: client_id is required');
|
||||
assert(client_key, 'invalid houndify speech credential: client_key is required');
|
||||
assert(user_id, 'invalid houndify speech credential: user_id is required');
|
||||
const houndifyData = JSON.stringify({client_id, client_key, user_id});
|
||||
return encrypt(houndifyData);
|
||||
|
||||
case 'voxist':
|
||||
assert(api_key, 'invalid voxist speech credential: api_key is required');
|
||||
const voxistData = JSON.stringify({api_key});
|
||||
@@ -518,7 +554,10 @@ router.put('/:sid', async(req, res) => {
|
||||
playht_tts_uri,
|
||||
engine_version,
|
||||
service_version,
|
||||
speechmatics_stt_uri
|
||||
speechmatics_stt_uri,
|
||||
resemble_tts_use_tls,
|
||||
resemble_tts_uri,
|
||||
api_uri
|
||||
} = req.body;
|
||||
|
||||
const newCred = {
|
||||
@@ -551,7 +590,10 @@ router.put('/:sid', async(req, res) => {
|
||||
playht_tts_uri,
|
||||
engine_version,
|
||||
service_version,
|
||||
speechmatics_stt_uri
|
||||
speechmatics_stt_uri,
|
||||
resemble_tts_uri,
|
||||
resemble_tts_use_tls,
|
||||
api_uri
|
||||
};
|
||||
logger.info({o, newCred}, 'updating speech credential with this new credential');
|
||||
obj.credential = encryptCredential(newCred);
|
||||
@@ -751,6 +793,17 @@ router.get('/:sid/test', async(req, res) => {
|
||||
SpeechCredential.sttTestResult(sid, false);
|
||||
}
|
||||
}
|
||||
} else if (cred.vendor === 'resemble') {
|
||||
if (cred.use_for_tts) {
|
||||
try {
|
||||
await testResembleTTS(logger, synthAudio, credential);
|
||||
results.tts.status = 'ok';
|
||||
SpeechCredential.ttsTestResult(sid, true);
|
||||
} catch (err) {
|
||||
results.tts = {status: 'fail', reason: err.message};
|
||||
SpeechCredential.ttsTestResult(sid, false);
|
||||
}
|
||||
}
|
||||
} else if (cred.vendor === 'deepgram') {
|
||||
const {api_key} = credential;
|
||||
if (cred.use_for_tts) {
|
||||
@@ -774,6 +827,31 @@ router.get('/:sid/test', async(req, res) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (cred.vendor === 'deepgramflux') {
|
||||
const {api_key} = credential;
|
||||
if (cred.use_for_stt && api_key) {
|
||||
try {
|
||||
await testDeepgramStt(logger, {api_key});
|
||||
results.stt.status = 'ok';
|
||||
SpeechCredential.sttTestResult(sid, true);
|
||||
} catch (err) {
|
||||
results.stt = {status: 'fail', reason: err.message};
|
||||
SpeechCredential.sttTestResult(sid, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (cred.vendor === 'gladia') {
|
||||
if (cred.use_for_stt) {
|
||||
try {
|
||||
await testGladiaStt(logger, credential);
|
||||
results.stt.status = 'ok';
|
||||
SpeechCredential.sttTestResult(sid, true);
|
||||
} catch (err) {
|
||||
results.stt = {status: 'fail', reason: err.message};
|
||||
SpeechCredential.sttTestResult(sid, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (cred.vendor === 'ibm') {
|
||||
const {getTtsVoices} = req.app.locals;
|
||||
|
||||
@@ -820,10 +898,10 @@ router.get('/:sid/test', async(req, res) => {
|
||||
}
|
||||
}
|
||||
} else if (cred.vendor === 'elevenlabs') {
|
||||
const {api_key, model_id} = credential;
|
||||
const {api_key, model_id, api_uri} = credential;
|
||||
if (cred.use_for_tts) {
|
||||
try {
|
||||
await testElevenlabs(logger, {api_key, model_id});
|
||||
await testElevenlabs(logger, {api_key, model_id, api_uri});
|
||||
results.tts.status = 'ok';
|
||||
SpeechCredential.ttsTestResult(sid, true);
|
||||
} catch (err) {
|
||||
@@ -918,6 +996,17 @@ router.get('/:sid/test', async(req, res) => {
|
||||
SpeechCredential.sttTestResult(sid, false);
|
||||
}
|
||||
}
|
||||
} else if (cred.vendor === 'houndify') {
|
||||
if (cred.use_for_stt) {
|
||||
try {
|
||||
await testHoundifyStt(logger, credential);
|
||||
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 === 'voxist') {
|
||||
const {api_key} = credential;
|
||||
if (cred.use_for_stt) {
|
||||
|
||||
@@ -1,56 +1,153 @@
|
||||
module.exports = [
|
||||
{ name: 'Multilingual', value: 'multi' },
|
||||
{ name: 'Afrikaans', value: 'af' },
|
||||
{ name: 'Amharic', value: 'am' },
|
||||
{ name: 'Arabic', value: 'ar' },
|
||||
{ name: 'Assamese', value: 'as' },
|
||||
{ name: 'Azerbaijani', value: 'az' },
|
||||
{ name: 'Bashkir', value: 'ba' },
|
||||
{ name: 'Belarusian', value: 'be' },
|
||||
{ name: 'Bulgarian', value: 'bg' },
|
||||
{ name: 'Bengali', value: 'bn' },
|
||||
{ name: 'Tibetan', value: 'bo' },
|
||||
{ name: 'Breton', value: 'br' },
|
||||
{ name: 'Bosnian', value: 'bs' },
|
||||
{ name: 'Catalan', value: 'ca' },
|
||||
{ name: 'Chinese (Mandarin, Simplified)', value: 'zh' },
|
||||
{ name: 'Chinese (Mandarin, Simplified - China)', value: 'zh-CN' },
|
||||
{ name: 'Chinese (Mandarin, Simplified - Hans)', value: 'zh-Hans' },
|
||||
{ name: 'Chinese (Mandarin, Traditional)', value: 'zh-TW' },
|
||||
{ name: 'Chinese (Mandarin, Traditional - Hant)', value: 'zh-Hant' },
|
||||
{ name: 'Chinese (Cantonese, Traditional - Hong Kong)', value: 'zh-HK' },
|
||||
{ name: 'Czech', value: 'cs' },
|
||||
{ name: 'Welsh', value: 'cy' },
|
||||
{ name: 'Danish', value: 'da' },
|
||||
{ name: 'Danish (Denmark)', value: 'da-DK' },
|
||||
{ name: 'Dutch', value: 'nl' },
|
||||
{ name: 'English', value: 'en' },
|
||||
{ name: 'English (United States)', value: 'en-US' },
|
||||
{ name: 'English (Australia)', value: 'en-AU' },
|
||||
{ name: 'English (United Kingdom)', value: 'en-GB' },
|
||||
{ name: 'English (New Zealand)', value: 'en-NZ' },
|
||||
{ name: 'English (India)', value: 'en-IN' },
|
||||
{ name: 'Estonian', value: 'et' },
|
||||
{ name: 'Finnish', value: 'fi' },
|
||||
{ name: 'Flemish', value: 'nl-BE' },
|
||||
{ name: 'French', value: 'fr' },
|
||||
{ name: 'French (Canada)', value: 'fr-CA' },
|
||||
{ name: 'German', value: 'de' },
|
||||
{ name: 'German (Austria)', value: 'de-AT' },
|
||||
{ name: 'German (Switzerland)', value: 'de-CH' },
|
||||
{ name: 'Greek', value: 'el' },
|
||||
{ name: 'German (Germany)', value: 'de-DE' },
|
||||
{ name: 'Modern Greek', value: 'el' },
|
||||
{ name: 'English', value: 'en' },
|
||||
{ name: 'English (Australia)', value: 'en-AU' },
|
||||
{ name: 'English (Canada)', value: 'en-CA' },
|
||||
{ name: 'English (United Kingdom)', value: 'en-GB' },
|
||||
{ name: 'English (Ireland)', value: 'en-IE' },
|
||||
{ name: 'English (India)', value: 'en-IN' },
|
||||
{ name: 'English (Malaysia)', value: 'en-MY' },
|
||||
{ name: 'English (New Zealand)', value: 'en-NZ' },
|
||||
{ name: 'English (Philippines)', value: 'en-PH' },
|
||||
{ name: 'English (United States)', value: 'en-US' },
|
||||
{ name: 'English (South Africa)', value: 'en-ZA' },
|
||||
{ name: 'Spanish', value: 'es' },
|
||||
{ name: 'Spanish (Latin America and the Caribbean)', value: 'es-419' },
|
||||
{ name: 'Spanish (Argentina)', value: 'es-AR' },
|
||||
{ name: 'Spanish (Colombia)', value: 'es-CO' },
|
||||
{ name: 'Spanish (Spain)', value: 'es-ES' },
|
||||
{ name: 'Spanish (Latin America)', value: 'es-LATAM' },
|
||||
{ name: 'Spanish (Mexico)', value: 'es-MX' },
|
||||
{ name: 'Spanish (United States)', value: 'es-US' },
|
||||
{ name: 'Estonian', value: 'et' },
|
||||
{ name: 'Basque', value: 'eu' },
|
||||
{ name: 'Persian', value: 'fa' },
|
||||
{ name: 'Finnish', value: 'fi' },
|
||||
{ name: 'Faroese', value: 'fo' },
|
||||
{ name: 'French', value: 'fr' },
|
||||
{ name: 'French (Belgium)', value: 'fr-BE' },
|
||||
{ name: 'French (Canada)', value: 'fr-CA' },
|
||||
{ name: 'French (Switzerland)', value: 'fr-CH' },
|
||||
{ name: 'French (France)', value: 'fr-FR' },
|
||||
{ name: 'French (Canada)', value: 'fr-ca' },
|
||||
{ name: 'Galician', value: 'gl' },
|
||||
{ name: 'Gujarati', value: 'gu' },
|
||||
{ name: 'Hausa', value: 'ha' },
|
||||
{ name: 'Hawaiian', value: 'haw' },
|
||||
{ name: 'Hebrew', value: 'he' },
|
||||
{ name: 'Hindi', value: 'hi' },
|
||||
{ name: 'Hindi (Latin)', value: 'hi-Latn' },
|
||||
{ name: 'Croatian', value: 'hr' },
|
||||
{ name: 'Haitian', value: 'ht' },
|
||||
{ name: 'Hungarian', value: 'hu' },
|
||||
{ name: 'Armenian', value: 'hy' },
|
||||
{ name: 'Indonesian', value: 'id' },
|
||||
{ name: 'Indonesian (Indonesia)', value: 'id-ID' },
|
||||
{ name: 'Icelandic', value: 'is' },
|
||||
{ name: 'Italian', value: 'it' },
|
||||
{ name: 'Italian (Italy)', value: 'it-IT' },
|
||||
{ name: 'Japanese', value: 'ja' },
|
||||
{ name: 'Japanese (Japan)', value: 'ja-JP' },
|
||||
{ name: 'Javanese', value: 'jw' },
|
||||
{ name: 'Georgian', value: 'ka' },
|
||||
{ name: 'Kazakh', value: 'kk' },
|
||||
{ name: 'Khmer', value: 'km' },
|
||||
{ name: 'Kannada', value: 'kn' },
|
||||
{ name: 'Korean', value: 'ko' },
|
||||
{ name: 'Korean (South Korea)', value: 'ko-KR' },
|
||||
{ name: 'Latvian', value: 'lv' },
|
||||
{ name: 'Korean (Republic of Korea)', value: 'ko-KR' },
|
||||
{ name: 'Latin', value: 'la' },
|
||||
{ name: 'Luxembourgish', value: 'lb' },
|
||||
{ name: 'Lingala', value: 'ln' },
|
||||
{ name: 'Lao', value: 'lo' },
|
||||
{ name: 'Lithuanian', value: 'lt' },
|
||||
{ name: 'Latvian', value: 'lv' },
|
||||
{ name: 'Malagasy', value: 'mg' },
|
||||
{ name: 'Maori', value: 'mi' },
|
||||
{ name: 'Macedonian', value: 'mk' },
|
||||
{ name: 'Malayalam', value: 'ml' },
|
||||
{ name: 'Mongolian', value: 'mn' },
|
||||
{ name: 'Marathi', value: 'mr' },
|
||||
{ name: 'Malay', value: 'ms' },
|
||||
{ name: 'Malay (Malaysia)', value: 'ms-MY' },
|
||||
{ name: 'Malay (Singapore)', value: 'ms-SG' },
|
||||
{ name: 'Maltese', value: 'mt' },
|
||||
{ name: 'Burmese', value: 'my' },
|
||||
{ name: 'Nepali', value: 'ne' },
|
||||
{ name: 'Dutch', value: 'nl' },
|
||||
{ name: 'Dutch (Belgium)', value: 'nl-BE' },
|
||||
{ name: 'Dutch (Netherlands)', value: 'nl-NL' },
|
||||
{ name: 'Norwegian Nynorsk', value: 'nn' },
|
||||
{ name: 'Norwegian', value: 'no' },
|
||||
{ name: 'Norwegian (Norway)', value: 'no-NO' },
|
||||
{ name: 'Occitan', value: 'oc' },
|
||||
{ name: 'Panjabi', value: 'pa' },
|
||||
{ name: 'Polish', value: 'pl' },
|
||||
{ name: 'Polish (Poland)', value: 'pl-PL' },
|
||||
{ name: 'Pushto', value: 'ps' },
|
||||
{ name: 'Portuguese', value: 'pt' },
|
||||
{ name: 'Portuguese (Brazil)', value: 'pt-BR' },
|
||||
{ name: 'Portuguese (Portugal)', value: 'pt-PT' },
|
||||
{ name: 'Romanian', value: 'ro' },
|
||||
{ name: 'Romanian (Moldova)', value: 'ro-MD' },
|
||||
{ name: 'Russian', value: 'ru' },
|
||||
{ name: 'Russian (Latin)', value: 'ru-Latn' },
|
||||
{ name: 'Russian (Russian Federation)', value: 'ru-RU' },
|
||||
{ name: 'Sanskrit', value: 'sa' },
|
||||
{ name: 'Sindhi', value: 'sd' },
|
||||
{ name: 'Sinhala', value: 'si' },
|
||||
{ name: 'Slovak', value: 'sk' },
|
||||
{ name: 'Spanish', value: 'es' },
|
||||
{ name: 'Spanish (Latin America)', value: 'es-419' },
|
||||
{ name: 'Slovenian', value: 'sl' },
|
||||
{ name: 'Shona', value: 'sn' },
|
||||
{ name: 'Somali', value: 'so' },
|
||||
{ name: 'Albanian', value: 'sq' },
|
||||
{ name: 'Serbian', value: 'sr' },
|
||||
{ name: 'Sundanese', value: 'su' },
|
||||
{ name: 'Swedish', value: 'sv' },
|
||||
{ name: 'Swedish (Sweden)', value: 'sv-SE' },
|
||||
{ name: 'Swahili', value: 'sw' },
|
||||
{ name: 'Tamil', value: 'ta' },
|
||||
{ name: 'Tamasheq', value: 'taq' },
|
||||
{ name: 'Telugu', value: 'te' },
|
||||
{ name: 'Tajik', value: 'tg' },
|
||||
{ name: 'Thai', value: 'th' },
|
||||
{ name: 'Thai (Thailand)', value: 'th-TH' },
|
||||
{ name: 'Turkmen', value: 'tk' },
|
||||
{ name: 'Tagalog', value: 'tl' },
|
||||
{ name: 'Turkish', value: 'tr' },
|
||||
{ name: 'Turkish (Türkiye)', value: 'tr-TR' },
|
||||
{ name: 'Tatar', value: 'tt' },
|
||||
{ name: 'Ukrainian', value: 'uk' },
|
||||
{ name: 'Vietnamese', value: 'vi' }
|
||||
{ name: 'Urdu', value: 'ur' },
|
||||
{ name: 'Uzbek', value: 'uz' },
|
||||
{ name: 'Vietnamese', value: 'vi' },
|
||||
{ name: 'Yiddish', value: 'yi' },
|
||||
{ name: 'Yoruba', value: 'yo' },
|
||||
{ name: 'Chinese (Mandarin, Mainland)', value: 'zh' },
|
||||
{ name: 'Chinese (China)', value: 'zh-CN' },
|
||||
{ name: 'Chinese (Cantonese, Hong Kong)', value: 'zh-HK' },
|
||||
{ name: 'Chinese (Han (Simplified variant))', value: 'zh-Hans' },
|
||||
{ name: 'Chinese (Han (Traditional variant))', value: 'zh-Hant' },
|
||||
{ name: 'Chinese (Traditional, Taiwan)', value: 'zh-TW' }
|
||||
];
|
||||
|
||||
103
lib/utils/speech-data/stt-gladia.js
Normal file
103
lib/utils/speech-data/stt-gladia.js
Normal file
@@ -0,0 +1,103 @@
|
||||
module.exports = [
|
||||
{ name: 'Afrikaans', value: 'af' },
|
||||
{ name: 'Albanian', value: 'sq' },
|
||||
{ name: 'Amharic', value: 'am' },
|
||||
{ name: 'Arabic', value: 'ar' },
|
||||
{ name: 'Armenian', value: 'hy' },
|
||||
{ name: 'Assamese', value: 'as' },
|
||||
{ name: 'Azerbaijani', value: 'az' },
|
||||
{ name: 'Bashkir', value: 'ba' },
|
||||
{ name: 'Basque', value: 'eu' },
|
||||
{ name: 'Belarusian', value: 'be' },
|
||||
{ name: 'Bengali', value: 'bn' },
|
||||
{ name: 'Bosnian', value: 'bs' },
|
||||
{ name: 'Breton', value: 'br' },
|
||||
{ name: 'Bulgarian', value: 'bg' },
|
||||
{ name: 'Cantonese', value: 'yue' },
|
||||
{ name: 'Catalan', value: 'ca' },
|
||||
{ name: 'Chinese', value: 'zh' },
|
||||
{ name: 'Croatian', value: 'hr' },
|
||||
{ name: 'Czech', value: 'cs' },
|
||||
{ name: 'Danish', value: 'da' },
|
||||
{ name: 'Dutch', value: 'nl' },
|
||||
{ name: 'English', value: 'en' },
|
||||
{ name: 'Estonian', value: 'et' },
|
||||
{ name: 'Faroese', value: 'fo' },
|
||||
{ name: 'Finnish', value: 'fi' },
|
||||
{ name: 'French', value: 'fr' },
|
||||
{ name: 'Galician', value: 'gl' },
|
||||
{ name: 'Georgian', value: 'ka' },
|
||||
{ name: 'German', value: 'de' },
|
||||
{ name: 'Greek', value: 'el' },
|
||||
{ name: 'Gujarati', value: 'gu' },
|
||||
{ name: 'Haitian Creole', value: 'ht' },
|
||||
{ name: 'Hausa', value: 'ha' },
|
||||
{ name: 'Hawaiian', value: 'haw' },
|
||||
{ name: 'Hebrew', value: 'he' },
|
||||
{ name: 'Hindi', value: 'hi' },
|
||||
{ name: 'Hungarian', value: 'hu' },
|
||||
{ name: 'Icelandic', value: 'is' },
|
||||
{ name: 'Indonesian', value: 'id' },
|
||||
{ name: 'Italian', value: 'it' },
|
||||
{ name: 'Japanese', value: 'ja' },
|
||||
{ name: 'Javanese', value: 'jw' },
|
||||
{ name: 'Kannada', value: 'kn' },
|
||||
{ name: 'Kazakh', value: 'kk' },
|
||||
{ name: 'Khmer', value: 'km' },
|
||||
{ name: 'Korean', value: 'ko' },
|
||||
{ name: 'Lao', value: 'lo' },
|
||||
{ name: 'Latin', value: 'la' },
|
||||
{ name: 'Latvian', value: 'lv' },
|
||||
{ name: 'Lingala', value: 'ln' },
|
||||
{ name: 'Lithuanian', value: 'lt' },
|
||||
{ name: 'Luxembourgish', value: 'lb' },
|
||||
{ name: 'Macedonian', value: 'mk' },
|
||||
{ name: 'Malagasy', value: 'mg' },
|
||||
{ name: 'Malay', value: 'ms' },
|
||||
{ name: 'Malayalam', value: 'ml' },
|
||||
{ name: 'Maltese', value: 'mt' },
|
||||
{ name: 'Maori', value: 'mi' },
|
||||
{ name: 'Marathi', value: 'mr' },
|
||||
{ name: 'Mongolian', value: 'mn' },
|
||||
{ name: 'Myanmar', value: 'my' },
|
||||
{ name: 'Nepali', value: 'ne' },
|
||||
{ name: 'Norwegian', value: 'no' },
|
||||
{ name: 'Nynorsk', value: 'nn' },
|
||||
{ name: 'Occitan', value: 'oc' },
|
||||
{ name: 'Pashto', value: 'ps' },
|
||||
{ name: 'Persian', value: 'fa' },
|
||||
{ name: 'Polish', value: 'pl' },
|
||||
{ name: 'Portuguese', value: 'pt' },
|
||||
{ name: 'Punjabi', value: 'pa' },
|
||||
{ name: 'Romanian', value: 'ro' },
|
||||
{ name: 'Russian', value: 'ru' },
|
||||
{ name: 'Sanskrit', value: 'sa' },
|
||||
{ name: 'Serbian', value: 'sr' },
|
||||
{ name: 'Shona', value: 'sn' },
|
||||
{ name: 'Sindhi', value: 'sd' },
|
||||
{ name: 'Sinhala', value: 'si' },
|
||||
{ name: 'Slovak', value: 'sk' },
|
||||
{ name: 'Slovenian', value: 'sl' },
|
||||
{ name: 'Somali', value: 'so' },
|
||||
{ name: 'Spanish', value: 'es' },
|
||||
{ name: 'Sundanese', value: 'su' },
|
||||
{ name: 'Swahili', value: 'sw' },
|
||||
{ name: 'Swedish', value: 'sv' },
|
||||
{ name: 'Tagalog', value: 'tl' },
|
||||
{ name: 'Tajik', value: 'tg' },
|
||||
{ name: 'Tamil', value: 'ta' },
|
||||
{ name: 'Tatar', value: 'tt' },
|
||||
{ name: 'Telugu', value: 'te' },
|
||||
{ name: 'Thai', value: 'th' },
|
||||
{ name: 'Tibetan', value: 'bo' },
|
||||
{ name: 'Turkish', value: 'tr' },
|
||||
{ name: 'Turkmen', value: 'tk' },
|
||||
{ name: 'Ukrainian', value: 'uk' },
|
||||
{ name: 'Urdu', value: 'ur' },
|
||||
{ name: 'Uzbek', value: 'uz' },
|
||||
{ name: 'Vietnamese', value: 'vi' },
|
||||
{ name: 'Welsh', value: 'cy' },
|
||||
{ name: 'Wolof', value: 'wo' },
|
||||
{ name: 'Yiddish', value: 'yi' },
|
||||
{ name: 'Yoruba', value: 'yo' }
|
||||
];
|
||||
19
lib/utils/speech-data/stt-houndify.js
Normal file
19
lib/utils/speech-data/stt-houndify.js
Normal file
@@ -0,0 +1,19 @@
|
||||
module.exports = [
|
||||
{ name: 'English', value: 'en' },
|
||||
{ name: 'Spanish', value: 'es' },
|
||||
{ name: 'Portuguese', value: 'pt' },
|
||||
{ name: 'French', value: 'fr' },
|
||||
{ name: 'Indian-accented English', value: 'en-IN' },
|
||||
{ name: 'German', value: 'de' },
|
||||
{ name: 'Dutch', value: 'nl' },
|
||||
{ name: 'Italian', value: 'it' },
|
||||
{ name: 'Korean', value: 'ko' },
|
||||
{ name: 'Japanese', value: 'ja' },
|
||||
{ name: 'Mandarin', value: 'zh-CN' },
|
||||
{ name: 'Russian', value: 'ru' },
|
||||
{ name: 'Polish', value: 'pl' },
|
||||
{ name: 'Swedish', value: 'sv' },
|
||||
{ name: 'Arabic', value: 'ar' },
|
||||
{ name: 'Turkish', value: 'tr' },
|
||||
{ name: 'Hebrew', value: 'he' },
|
||||
];
|
||||
@@ -9,6 +9,15 @@ module.exports = [
|
||||
value: 'sonic-2',
|
||||
languages: ['en', 'fr', 'de', 'es', 'pt', 'zh', 'ja', 'hi', 'it', 'ko', 'nl', 'pl', 'ru', 'sv', 'tr']
|
||||
},
|
||||
{
|
||||
name: 'Sonic 3',
|
||||
value: 'sonic-3',
|
||||
languages: [
|
||||
'en', 'fr', 'de', 'es', 'pt', 'zh', 'ja', 'hi', 'it', 'ko', 'nl', 'pl', 'ru', 'sv', 'tr',
|
||||
'tl', 'bg', 'ro', 'ar', 'cs', 'el', 'fi', 'hr', 'ms', 'sk', 'da', 'ta', 'uk', 'hu', 'no',
|
||||
'vi', 'bn', 'th', 'he', 'ka', 'id', 'te', 'gu', 'kn', 'ml', 'mr', 'pa'
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Sonic Turbo',
|
||||
value: 'sonic-turbo',
|
||||
|
||||
438
lib/utils/speech-data/tts-resemble.js
Normal file
438
lib/utils/speech-data/tts-resemble.js
Normal file
@@ -0,0 +1,438 @@
|
||||
module.exports = [
|
||||
{
|
||||
value: 'en-gb',
|
||||
name: 'En-gb',
|
||||
voices: [
|
||||
{
|
||||
name: 'Seth (Legacy) (professional) - Resemble Voice',
|
||||
value: 'a52c4efc',
|
||||
},
|
||||
{
|
||||
name: 'Seth (professional) - Resemble Voice',
|
||||
value: 'd3e61caf',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'en-GB',
|
||||
name: 'En-GB',
|
||||
voices: [
|
||||
{
|
||||
name: 'Beatrice Pendergast (professional) - Resemble Voice',
|
||||
value: '00b1fd4e',
|
||||
},
|
||||
{
|
||||
name: 'Ed Smart (professional) - Resemble Voice',
|
||||
value: '0c755526',
|
||||
},
|
||||
{
|
||||
name: 'Paula J (professional) - Resemble Voice',
|
||||
value: '33e64cd2',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'en-us',
|
||||
name: 'En-us',
|
||||
voices: [
|
||||
{
|
||||
name: 'David (professional) - Resemble Voice',
|
||||
value: '5bb13f03',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'en-US',
|
||||
name: 'En-US',
|
||||
voices: [
|
||||
{
|
||||
name: 'Adam Lofbomm (professional) - Resemble Voice',
|
||||
value: '4e228dba',
|
||||
},
|
||||
{
|
||||
name: 'Alex (professional) - Resemble Voice',
|
||||
value: '41b99669',
|
||||
},
|
||||
{
|
||||
name: 'Amelia (professional) - Resemble Voice',
|
||||
value: 'ecbe5d97',
|
||||
},
|
||||
{
|
||||
name: 'Andrew (rapid) - Resemble Marketplace',
|
||||
value: 'd2f26a3e',
|
||||
},
|
||||
{
|
||||
name: 'Annika (professional) - Resemble Voice',
|
||||
value: 'b27f3cc0',
|
||||
},
|
||||
{
|
||||
name: 'Arthur (professional) - Resemble Voice',
|
||||
value: '9de11312',
|
||||
},
|
||||
{
|
||||
name: 'Ash (professional) - Resemble Voice',
|
||||
value: 'ee322483',
|
||||
},
|
||||
{
|
||||
name: 'Aurora (professional) - Resemble Voice',
|
||||
value: 'a72d9fca',
|
||||
},
|
||||
{
|
||||
name: 'Austin (professional) - Resemble Voice',
|
||||
value: '82a67e58',
|
||||
},
|
||||
{
|
||||
name: 'Beth (Legacy) (professional) - Resemble Voice',
|
||||
value: '25c7823f',
|
||||
},
|
||||
{
|
||||
name: 'Beth (professional) - Resemble Voice',
|
||||
value: 'fa66d263',
|
||||
},
|
||||
{
|
||||
name: 'Blade (professional) - Resemble Voice',
|
||||
value: '8bedd793',
|
||||
},
|
||||
{
|
||||
name: 'Brandy Sky (professional) - Resemble Voice',
|
||||
value: '79e2f1dc',
|
||||
},
|
||||
{
|
||||
name: 'Brenley (professional) - Resemble Voice',
|
||||
value: 'e6ec3ca4',
|
||||
},
|
||||
{
|
||||
name: 'Britney (professional) - Resemble Voice',
|
||||
value: 'e57e23ff',
|
||||
},
|
||||
{
|
||||
name: 'Broadcast Joe (professional) - Resemble Voice',
|
||||
value: '21e49584',
|
||||
},
|
||||
{
|
||||
name: 'Carl Bishop (Angry) (professional) - Resemble Voice',
|
||||
value: 'f06cd770',
|
||||
},
|
||||
{
|
||||
name: 'Carl Bishop (Conversational) (professional) - Resemble Voice',
|
||||
value: '7f40ff35',
|
||||
},
|
||||
{
|
||||
name: 'Carl Bishop (Happy) (professional) - Resemble Voice',
|
||||
value: '99751e42',
|
||||
},
|
||||
{
|
||||
name: 'Carl Bishop (professional) - Resemble Voice',
|
||||
value: '01bcc102',
|
||||
},
|
||||
{
|
||||
name: 'Carl Bishop (Scared) (Legacy) (professional) - Resemble Voice',
|
||||
value: '1dcf0222',
|
||||
},
|
||||
{
|
||||
name: 'Carl Bishop (Scared) (professional) - Resemble Voice',
|
||||
value: 'eacbc44f',
|
||||
},
|
||||
{
|
||||
name: 'Charles (Legacy) (professional) - Resemble Voice',
|
||||
value: '4c6d3da5',
|
||||
},
|
||||
{
|
||||
name: 'Charles (professional) - Resemble Voice',
|
||||
value: 'd79a5198',
|
||||
},
|
||||
{
|
||||
name: 'Charlotte (professional) - Resemble Voice',
|
||||
value: '96b91cf9',
|
||||
},
|
||||
{
|
||||
name: 'Chris Whiting (professional) - Resemble Voice',
|
||||
value: '95b7560a',
|
||||
},
|
||||
{
|
||||
name: 'Cliff (professional) - Resemble Voice',
|
||||
value: 'fcf8490c',
|
||||
},
|
||||
{
|
||||
name: 'Connor (professional) - Resemble Voice',
|
||||
value: 'a6131acf',
|
||||
},
|
||||
{
|
||||
name: 'Deanna (professional) - Resemble Voice',
|
||||
value: '0842fdf9',
|
||||
},
|
||||
{
|
||||
name: 'Ember (professional) - Resemble Voice',
|
||||
value: '55592656',
|
||||
},
|
||||
{
|
||||
name: 'Gene Amore (professional) - Resemble Voice',
|
||||
value: 'f2ea7aa0',
|
||||
},
|
||||
{
|
||||
name: 'Harry Robinson (professional) - Resemble Voice',
|
||||
value: '3c36d67d',
|
||||
},
|
||||
{
|
||||
name: 'Helena (professional) - Resemble Voice',
|
||||
value: 'ac948df2',
|
||||
},
|
||||
{
|
||||
name: 'Hem (professional) - Resemble Voice',
|
||||
value: 'b6edbe5f',
|
||||
},
|
||||
{
|
||||
name: 'John (professional) - Resemble Voice',
|
||||
value: 'ac48daeb',
|
||||
},
|
||||
{
|
||||
name: 'Josh (professional) - Resemble Voice',
|
||||
value: '987c99e9',
|
||||
},
|
||||
{
|
||||
name: 'Julie Hoverson (professional) - Resemble Voice',
|
||||
value: 'b119524c',
|
||||
},
|
||||
{
|
||||
name: 'Justin (Legacy) (professional) - Resemble Voice',
|
||||
value: 'b2d1bb75',
|
||||
},
|
||||
{
|
||||
name: 'Justin (Meditative) (Legacy) (professional) - Resemble Voice',
|
||||
value: '93ce0920',
|
||||
},
|
||||
{
|
||||
name: 'Justin (Meditative) (professional) - Resemble Voice',
|
||||
value: '2570000e',
|
||||
},
|
||||
{
|
||||
name: 'Justin (professional) - Resemble Voice',
|
||||
value: '9d513c17',
|
||||
},
|
||||
{
|
||||
name: 'Karl Nordman (professional) - Resemble Voice',
|
||||
value: 'da67f17e',
|
||||
},
|
||||
{
|
||||
name: 'Kate (professional) - Resemble Voice',
|
||||
value: '28b4cc5a',
|
||||
},
|
||||
{
|
||||
name: 'Katya (professional) - Resemble Voice',
|
||||
value: 'c9ee13b4',
|
||||
},
|
||||
{
|
||||
name: 'Ken (professional) - Resemble Voice',
|
||||
value: '3dbfbf3d',
|
||||
},
|
||||
{
|
||||
name: 'Kessi (professional) - Resemble Voice',
|
||||
value: '2211cb8c',
|
||||
},
|
||||
{
|
||||
name: 'Little Ari (professional) - Resemble Voice',
|
||||
value: '805adead',
|
||||
},
|
||||
{
|
||||
name: 'Little Brittle (professional) - Resemble Voice',
|
||||
value: '8a73f115',
|
||||
},
|
||||
{
|
||||
name: 'Liz (professional) - Resemble Voice',
|
||||
value: '4884d94a',
|
||||
},
|
||||
{
|
||||
name: 'Lothar (professional) - Resemble Voice',
|
||||
value: '78671217',
|
||||
},
|
||||
{
|
||||
name: 'Luna (professional) - Resemble Voice',
|
||||
value: 'ae8223ca',
|
||||
},
|
||||
{
|
||||
name: 'Matt Weller (professional) - Resemble Voice',
|
||||
value: 'f4da4639',
|
||||
},
|
||||
{
|
||||
name: 'Maureen (Angry) (professional) - Resemble Voice',
|
||||
value: '482babfc',
|
||||
},
|
||||
{
|
||||
name: 'Maureen (Caring) (professional) - Resemble Voice',
|
||||
value: 'b15e550f',
|
||||
},
|
||||
{
|
||||
name: 'Maureen (Happy) (professional) - Resemble Voice',
|
||||
value: '91947e5c',
|
||||
},
|
||||
{
|
||||
name: 'Maureen (professional) - Resemble Voice',
|
||||
value: '7d94218f',
|
||||
},
|
||||
{
|
||||
name: 'Maureen (Sad) (professional) - Resemble Voice',
|
||||
value: 'bca7481c',
|
||||
},
|
||||
{
|
||||
name: 'Maureen (Scared) (professional) - Resemble Voice',
|
||||
value: '251c9439',
|
||||
},
|
||||
{
|
||||
name: 'Mauren (Announcer) (professional) - Resemble Voice',
|
||||
value: 'e984fb89',
|
||||
},
|
||||
{
|
||||
name: 'Melody (Legacy) (professional) - Resemble Voice',
|
||||
value: '15be93bd',
|
||||
},
|
||||
{
|
||||
name: 'Melody (professional) - Resemble Voice',
|
||||
value: '1c49e774',
|
||||
},
|
||||
{
|
||||
name: 'Mike (professional) - Resemble Voice',
|
||||
value: '3a02dc40',
|
||||
},
|
||||
{
|
||||
name: 'Niki (professional) - Resemble Voice',
|
||||
value: 'db37643c',
|
||||
},
|
||||
{
|
||||
name: 'Olga (professional) - Resemble Voice',
|
||||
value: '07c1d6b5',
|
||||
},
|
||||
{
|
||||
name: 'Olivia (Legacy) (professional) - Resemble Voice',
|
||||
value: '405b58e3',
|
||||
},
|
||||
{
|
||||
name: 'Olivia (professional) - Resemble Voice',
|
||||
value: 'ef49f972',
|
||||
},
|
||||
{
|
||||
name: 'Orion (professional) - Resemble Voice',
|
||||
value: 'aa8053cc',
|
||||
},
|
||||
{
|
||||
name: 'Pete (professional) - Resemble Voice',
|
||||
value: '1864fd63',
|
||||
},
|
||||
{
|
||||
name: 'Primrose (Legacy) (professional) - Resemble Voice',
|
||||
value: '7c8e47ca',
|
||||
},
|
||||
{
|
||||
name: 'Primrose (professional) - Resemble Voice',
|
||||
value: '33eecc17',
|
||||
},
|
||||
{
|
||||
name: 'Primrose (Whispering) (Legacy) (professional) - Resemble Voice',
|
||||
value: 'a56c5c6f',
|
||||
},
|
||||
{
|
||||
name: 'Primrose (Whispering) (professional) - Resemble Voice',
|
||||
value: '28fcdf76',
|
||||
},
|
||||
{
|
||||
name: 'Primrose (Winded) (Legacy) (professional) - Resemble Voice',
|
||||
value: '6f9a77a4',
|
||||
},
|
||||
{
|
||||
name: 'Primrose (Winded) (professional) - Resemble Voice',
|
||||
value: '0097f246',
|
||||
},
|
||||
{
|
||||
name: 'Professor Shaposhnikov (professional) - Resemble Voice',
|
||||
value: '3f5fb9f1',
|
||||
},
|
||||
{
|
||||
name: 'Radio Nikole (professional) - Resemble Voice',
|
||||
value: '19eae884',
|
||||
},
|
||||
{
|
||||
name: 'Richard Garifo (professional) - Resemble Voice',
|
||||
value: '85ba84f2',
|
||||
},
|
||||
{
|
||||
name: 'Rico (professional) - Resemble Voice',
|
||||
value: '14ca34b3',
|
||||
},
|
||||
{
|
||||
name: 'Robert (professional) - Resemble Voice',
|
||||
value: '3e907bcc',
|
||||
},
|
||||
{
|
||||
name: 'Rupert (rapid) - Resemble Voice',
|
||||
value: '28f1626c',
|
||||
},
|
||||
{
|
||||
name: 'Sam (professional) - Resemble Voice',
|
||||
value: '0f2f9a7e',
|
||||
},
|
||||
{
|
||||
name: 'Samantha (Legacy) (professional) - Resemble Voice',
|
||||
value: '266bfae9',
|
||||
},
|
||||
{
|
||||
name: 'Samantha (professional) - Resemble Voice',
|
||||
value: 'e28236ee',
|
||||
},
|
||||
{
|
||||
name: 'Siobhan (professional) - Resemble Voice',
|
||||
value: 'af72c1ac',
|
||||
},
|
||||
{
|
||||
name: 'Steve (Scared) (professional) - Resemble Voice',
|
||||
value: 'aaa56e79',
|
||||
},
|
||||
{
|
||||
name: 'Tanja (professional) - Resemble Voice',
|
||||
value: 'adb84c77',
|
||||
},
|
||||
{
|
||||
name: 'Tanja (Telephonic) (professional) - Resemble Voice',
|
||||
value: '4f5a470b',
|
||||
},
|
||||
{
|
||||
name: 'Tanja (Warm Word Weaver) (professional) - Resemble Voice',
|
||||
value: 'abbbc383',
|
||||
},
|
||||
{
|
||||
name: 'Tarkos (professional) - Resemble Voice',
|
||||
value: '779842bf',
|
||||
},
|
||||
{
|
||||
name: 'Tyler (professional) - Resemble Voice',
|
||||
value: 'ff225977',
|
||||
},
|
||||
{
|
||||
name: 'Vicky (professional) - Resemble Voice',
|
||||
value: 'f453b918',
|
||||
},
|
||||
{
|
||||
name: 'Vivian (Legacy) (professional) - Resemble Voice',
|
||||
value: 'bed1044d',
|
||||
},
|
||||
{
|
||||
name: 'Vivian (professional) - Resemble Voice',
|
||||
value: '1ff0045f',
|
||||
},
|
||||
{
|
||||
name: 'William (Whispering) (Legacy) (professional) - Resemble Voice',
|
||||
value: '79eb7953',
|
||||
},
|
||||
{
|
||||
name: 'William (Whispering) (professional) - Resemble Voice',
|
||||
value: 'e2180df0',
|
||||
},
|
||||
{
|
||||
name: 'Willow (Whispering) (professional) - Resemble Voice',
|
||||
value: 'f2906c4a',
|
||||
},
|
||||
{
|
||||
name: 'Willow II (Whispering) (professional) - Resemble Voice',
|
||||
value: 'c815cd7a',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -5,6 +5,8 @@ const sdk = require('microsoft-cognitiveservices-speech-sdk');
|
||||
const { SpeechClient } = require('@soniox/soniox-node');
|
||||
const fs = require('fs');
|
||||
const { AssemblyAI } = require('assemblyai');
|
||||
const Houndify = require('houndify');
|
||||
const { GladiaClient } = require('@gladiaio/sdk');
|
||||
const {decrypt, obscureKey} = require('./encrypt-decrypt');
|
||||
const { RealtimeSession } = require('speechmatics');
|
||||
|
||||
@@ -21,6 +23,7 @@ const TtsPlayHtLanguagesVoices = require('./speech-data/tts-playht');
|
||||
const TtsVerbioLanguagesVoices = require('./speech-data/tts-verbio');
|
||||
const TtsInworldLanguagesVoices = require('./speech-data/tts-inworld');
|
||||
const ttsCartesia = require('./speech-data/tts-cartesia');
|
||||
const TtsResembleLanguagesVoices = require('./speech-data/tts-resemble');
|
||||
|
||||
const TtsModelDeepgram = require('./speech-data/tts-model-deepgram');
|
||||
const TtsLanguagesDeepgram = require('./speech-data/tts-deepgram');
|
||||
@@ -44,9 +47,11 @@ 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 SttHoundifyLanguagesVoices = require('./speech-data/stt-houndify');
|
||||
const SttVoxistLanguagesVoices = require('./speech-data/stt-voxist');
|
||||
const SttVerbioLanguagesVoices = require('./speech-data/stt-verbio');
|
||||
const SttOpenaiLanguagesVoices = require('./speech-data/stt-openai');
|
||||
const SttGladiaLanguagesVoices = require('./speech-data/stt-gladia');
|
||||
|
||||
|
||||
const SttModelOpenai = require('./speech-data/stt-model-openai');
|
||||
@@ -167,6 +172,65 @@ const testGoogleStt = async(logger, credentials) => {
|
||||
}
|
||||
};
|
||||
|
||||
const testGladiaStt = async(logger, credentials) => {
|
||||
const {api_key} = credentials;
|
||||
|
||||
try {
|
||||
const gladiaClient = new GladiaClient({
|
||||
apiKey: api_key,
|
||||
});
|
||||
const gladiaConfig = {
|
||||
model: 'solaria-1',
|
||||
encoding: 'wav/pcm',
|
||||
sample_rate: 16000,
|
||||
bit_depth: 16,
|
||||
channels: 1,
|
||||
language_config: {
|
||||
languages: ['en'],
|
||||
code_switching: false,
|
||||
},
|
||||
};
|
||||
// Start the live session
|
||||
const liveSession = gladiaClient.liveV2().startSession(gladiaConfig);
|
||||
// Read the test audio file
|
||||
const audioBuffer = fs.readFileSync(`${__dirname}/../../data/test_audio.wav`);
|
||||
|
||||
// Wait for final transcript
|
||||
return new Promise((resolve, reject) => {
|
||||
liveSession.on('message', (message) => {
|
||||
if (message.type === 'transcript' && message.data.is_final) {
|
||||
logger.debug(`${message.data.id}: ${message.data.utterance.text}`);
|
||||
liveSession.stopRecording();
|
||||
resolve(message.data.utterance.text);
|
||||
}
|
||||
});
|
||||
|
||||
liveSession.on('error', (error) => {
|
||||
logger.error({error}, 'Gladia Live STT error');
|
||||
reject(error);
|
||||
});
|
||||
|
||||
// Send audio in chunks
|
||||
const chunkSize = 1024;
|
||||
for (let i = 0; i < audioBuffer.length; i += chunkSize) {
|
||||
const chunk = audioBuffer.slice(i, i + chunkSize);
|
||||
liveSession.sendAudio(chunk);
|
||||
}
|
||||
// Stop recording after sending all audio
|
||||
liveSession.stopRecording();
|
||||
|
||||
// Set a timeout to prevent hanging
|
||||
setTimeout(() => {
|
||||
reject(new Error('Gladia STT test timeout'));
|
||||
}, 30000); // 30 second timeout
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error({error}, 'Failed to create Gladia Live STT session');
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const testDeepgramStt = async(logger, credentials) => {
|
||||
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);
|
||||
@@ -315,8 +379,8 @@ const testWellSaidTts = async(logger, credentials) => {
|
||||
};
|
||||
|
||||
const testElevenlabs = async(logger, credentials) => {
|
||||
const {api_key, model_id} = credentials;
|
||||
const response = await fetch('https://api.elevenlabs.io/v1/text-to-speech/21m00Tcm4TlvDq8ikWAM', {
|
||||
const {api_key, model_id, api_uri} = credentials;
|
||||
const response = await fetch(`https://${api_uri || 'api.elevenlabs.io'}/v1/text-to-speech/21m00Tcm4TlvDq8ikWAM`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'xi-api-key': api_key,
|
||||
@@ -424,6 +488,24 @@ const testWhisper = async(logger, synthAudio, credentials) => {
|
||||
}
|
||||
};
|
||||
|
||||
const testResembleTTS = async(logger, synthAudio, credentials) => {
|
||||
try {
|
||||
await synthAudio({increment: () => {}, histogram: () => {}},
|
||||
{
|
||||
vendor: 'resemble',
|
||||
credentials,
|
||||
language: 'en-US',
|
||||
voice: '3f5fb9f1',
|
||||
text: 'Hi there and welcome to jambones!',
|
||||
renderForCaching: true
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
logger.info({err}, 'synth resemble returned error');
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const testDeepgramTTS = async(logger, synthAudio, credentials) => {
|
||||
try {
|
||||
await synthAudio({increment: () => {}, histogram: () => {}},
|
||||
@@ -576,6 +658,72 @@ const testAssemblyStt = async(logger, credentials) => {
|
||||
});
|
||||
};
|
||||
|
||||
const testHoundifyStt = async(logger, credentials) => {
|
||||
const {client_id, client_key, user_id} = credentials;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
// Read the test audio file
|
||||
const audioBuffer = fs.readFileSync(`${__dirname}/../../data/test_audio.wav`);
|
||||
|
||||
// Create VoiceRequest for speech-to-text testing
|
||||
const voiceRequest = new Houndify.VoiceRequest({
|
||||
// Your Houndify Client ID and Key
|
||||
clientId: client_id,
|
||||
clientKey: client_key,
|
||||
|
||||
// Request info
|
||||
requestInfo: {
|
||||
UserID: user_id || 'test_user',
|
||||
Latitude: 37.388309,
|
||||
Longitude: -121.973968
|
||||
},
|
||||
|
||||
// Audio format configuration
|
||||
sampleRate: 16000,
|
||||
enableVAD: true,
|
||||
|
||||
// Response and error handlers
|
||||
onResponse: function(response, info) {
|
||||
logger.debug({response, info}, 'Houndify STT response received');
|
||||
if (response && response.AllResults && response.AllResults.length > 0) {
|
||||
resolve(response);
|
||||
} else {
|
||||
reject(new Error('No transcription results received'));
|
||||
}
|
||||
},
|
||||
|
||||
onError: function(err, info) {
|
||||
logger.error({err, info}, 'Houndify STT error');
|
||||
reject(err);
|
||||
},
|
||||
|
||||
onRecordingStarted: function() {
|
||||
logger.debug('Houndify recording started');
|
||||
},
|
||||
|
||||
onRecordingStopped: function() {
|
||||
logger.debug('Houndify recording stopped');
|
||||
}
|
||||
});
|
||||
|
||||
// Send audio in chunks (VoiceRequest automatically starts when you write data)
|
||||
const chunkSize = 1024;
|
||||
for (let i = 0; i < audioBuffer.length; i += chunkSize) {
|
||||
const chunk = audioBuffer.slice(i, i + chunkSize);
|
||||
voiceRequest.write(chunk);
|
||||
}
|
||||
|
||||
// End the request
|
||||
voiceRequest.end();
|
||||
|
||||
} catch (error) {
|
||||
logger.error({error}, 'Failed to create Houndify VoiceRequest');
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const testVoxistStt = async(logger, credentials) => {
|
||||
const {api_key} = credentials;
|
||||
const response = await fetch('https://api-asr.voxist.com/clients', {
|
||||
@@ -636,7 +784,6 @@ function decryptCredential(obj, credential, logger, isObscureKey = true) {
|
||||
obj.role_arn = o.role_arn;
|
||||
obj.secret_access_key = isObscureKey ? obscureKey(o.secret_access_key) : o.secret_access_key;
|
||||
obj.aws_region = o.aws_region;
|
||||
logger.info({obj, o}, 'retrieving aws speech credential');
|
||||
}
|
||||
else if ('microsoft' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
@@ -648,7 +795,6 @@ function decryptCredential(obj, credential, logger, isObscureKey = true) {
|
||||
obj.use_custom_stt = o.use_custom_stt;
|
||||
obj.custom_stt_endpoint = o.custom_stt_endpoint;
|
||||
obj.custom_stt_endpoint_url = o.custom_stt_endpoint_url;
|
||||
logger.info({obj, o}, 'retrieving azure speech credential');
|
||||
}
|
||||
else if ('wellsaid' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
@@ -669,6 +815,14 @@ function decryptCredential(obj, credential, logger, isObscureKey = true) {
|
||||
obj.deepgram_tts_uri = o.deepgram_tts_uri;
|
||||
obj.model_id = o.model_id;
|
||||
}
|
||||
else if ('deepgramflux' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.api_key = isObscureKey ? obscureKey(o.api_key) : o.api_key;
|
||||
}
|
||||
else if ('gladia' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.api_key = isObscureKey ? obscureKey(o.api_key) : o.api_key;
|
||||
}
|
||||
else if ('ibm' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.tts_api_key = isObscureKey ? obscureKey(o.tts_api_key) : o.tts_api_key;
|
||||
@@ -693,6 +847,7 @@ function decryptCredential(obj, credential, logger, isObscureKey = true) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.api_key = isObscureKey ? obscureKey(o.api_key) : o.api_key;
|
||||
obj.model_id = o.model_id;
|
||||
obj.api_uri = o.api_uri;
|
||||
obj.options = o.options;
|
||||
} else if ('playht' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
@@ -727,6 +882,16 @@ function decryptCredential(obj, credential, logger, isObscureKey = true) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.api_key = isObscureKey ? obscureKey(o.api_key) : o.api_key;
|
||||
obj.service_version = o.service_version;
|
||||
} else if ('houndify' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.client_key = isObscureKey ? obscureKey(o.client_key) : o.client_key;
|
||||
obj.client_id = o.client_id;
|
||||
obj.user_id = o.user_id;
|
||||
} else if ('resemble' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.api_key = isObscureKey ? obscureKey(o.api_key) : o.api_key;
|
||||
obj.resemble_tts_uri = o.resemble_tts_uri;
|
||||
obj.resemble_tts_use_tls = o.resemble_tts_use_tls;
|
||||
} else if ('voxist' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.api_key = isObscureKey ? obscureKey(o.api_key) : o.api_key;
|
||||
@@ -781,6 +946,8 @@ async function getLanguagesAndVoicesForVendor(logger, vendor, credential, getTts
|
||||
return await getLanguagesVoicesForNuane(credential, getTtsVoices, logger);
|
||||
case 'deepgram':
|
||||
return await getLanguagesVoicesForDeepgram(credential, getTtsVoices, logger);
|
||||
case 'gladia':
|
||||
return await getLanguagesVoicesForGladia(credential, getTtsVoices, logger);
|
||||
case 'ibm':
|
||||
return await getLanguagesVoicesForIbm(credential, getTtsVoices, logger);
|
||||
case 'nvidia':
|
||||
@@ -797,8 +964,12 @@ async function getLanguagesAndVoicesForVendor(logger, vendor, credential, getTts
|
||||
return await getLanguagesVoicesForRimelabs(credential, getTtsVoices, logger);
|
||||
case 'inworld':
|
||||
return await getLanguagesVoicesForInworld(credential, getTtsVoices, logger);
|
||||
case 'resemble':
|
||||
return await getLanguagesAndVoicesForResemble(credential, getTtsVoices, logger);
|
||||
case 'assemblyai':
|
||||
return await getLanguagesVoicesForAssemblyAI(credential, getTtsVoices, logger);
|
||||
case 'houndify':
|
||||
return await getLanguagesVoicesForHoundify(credential, getTtsVoices, logger);
|
||||
case 'voxist':
|
||||
return await getLanguagesVoicesForVoxist(credential, getTtsVoices, logger);
|
||||
case 'whisper':
|
||||
@@ -907,8 +1078,30 @@ async function getLanguagesVoicesForDeepgram(credential, getTtsVoices, logger) {
|
||||
logger.error({response}, 'Error fetching Deepgram voices');
|
||||
throw new Error('failed to list voices');
|
||||
}
|
||||
const {stt, tts} = await response.json();
|
||||
let sttLangs = SttDeepgramLanguagesVoices;
|
||||
const {stt, tts, languages} = await response.json();
|
||||
// Helper function to get language name
|
||||
const getLanguageName = (langCode) => {
|
||||
if (languages && languages[langCode]) {
|
||||
return languages[langCode];
|
||||
}
|
||||
const existingLang = SttDeepgramLanguagesVoices.find((l) => l.value === langCode);
|
||||
return existingLang ? existingLang.name : capitalizeFirst(langCode);
|
||||
};
|
||||
// Collect unique languages from selected models
|
||||
const allSttLanguages = new Set();
|
||||
const modelsToProcess = model_id ?
|
||||
stt.filter((m) => m.canonical_name === model_id) :
|
||||
stt;
|
||||
modelsToProcess.forEach((model) => {
|
||||
if (model.languages && Array.isArray(model.languages)) {
|
||||
model.languages.forEach((lang) => allSttLanguages.add(lang));
|
||||
}
|
||||
});
|
||||
// Convert to expected format
|
||||
const sttLangs = Array.from(allSttLanguages).map((langCode) => ({
|
||||
name: getLanguageName(langCode),
|
||||
value: langCode
|
||||
})).sort((a, b) => a.name.localeCompare(b.name));
|
||||
const sttModels = Array.from(
|
||||
new Map(
|
||||
stt.map((m) => [m.canonical_name, { name: capitalizeFirst(m.canonical_name), value: m.canonical_name }])
|
||||
@@ -919,16 +1112,6 @@ async function getLanguagesVoicesForDeepgram(credential, getTtsVoices, logger) {
|
||||
tts.map((m) => [m.canonical_name, { name: capitalizeFirst(m.canonical_name), value: m.canonical_name }])
|
||||
).values()
|
||||
).sort((a, b) => a.name.localeCompare(b.name));
|
||||
// if model_id is not provided, return all models, all voices, all languages
|
||||
if (!model_id) {
|
||||
return tranform(TtsLanguagesDeepgram, sttLangs, ttsModels, sttModels);
|
||||
}
|
||||
|
||||
const selectedSttModel = stt.find((m) => m.canonical_name === model_id);
|
||||
const selectedSttLangs = selectedSttModel ? selectedSttModel.languages : [];
|
||||
sttLangs = SttDeepgramLanguagesVoices.filter((l) => {
|
||||
return selectedSttLangs.includes(l.value);
|
||||
});
|
||||
return tranform(TtsLanguagesDeepgram, sttLangs, ttsModels, sttModels);
|
||||
}
|
||||
}
|
||||
@@ -936,6 +1119,11 @@ async function getLanguagesVoicesForDeepgram(credential, getTtsVoices, logger) {
|
||||
TtsModelDeepgram, sttModelDeepgram.sort((a, b) => a.name.localeCompare(b.name)));
|
||||
}
|
||||
|
||||
async function getLanguagesVoicesForGladia(credential, getTtsVoices, logger) {
|
||||
return tranform(undefined, SttGladiaLanguagesVoices.sort((a, b) => a.name.localeCompare(b.name)),
|
||||
undefined, undefined);
|
||||
}
|
||||
|
||||
async function getLanguagesVoicesForIbm(credential, getTtsVoices, logger) {
|
||||
if (credential) {
|
||||
try {
|
||||
@@ -974,10 +1162,12 @@ async function getLanguagesVoicesForElevenlabs(credential) {
|
||||
'xi-api-key': credential.api_key
|
||||
};
|
||||
|
||||
const getModelPromise = fetch('https://api.elevenlabs.io/v1/models', {
|
||||
const api_uri = credential.api_uri || 'api.elevenlabs.io';
|
||||
|
||||
const getModelPromise = fetch(`https://${api_uri}/v1/models`, {
|
||||
headers
|
||||
});
|
||||
const getVoicePromise = fetch('https://api.elevenlabs.io/v1/voices', {
|
||||
const getVoicePromise = fetch(`https://${api_uri}/v1/voices`, {
|
||||
headers
|
||||
});
|
||||
const [langResp, voiceResp] = await Promise.all([getModelPromise, getVoicePromise]);
|
||||
@@ -1206,6 +1396,10 @@ async function getLanguagesVoicesForAssemblyAI(credential) {
|
||||
return tranform(undefined, SttAssemblyaiLanguagesVoices);
|
||||
}
|
||||
|
||||
async function getLanguagesVoicesForHoundify(credential) {
|
||||
return tranform(undefined, SttHoundifyLanguagesVoices);
|
||||
}
|
||||
|
||||
async function getLanguagesVoicesForVoxist(credential) {
|
||||
return tranform(undefined, SttVoxistLanguagesVoices);
|
||||
}
|
||||
@@ -1238,6 +1432,82 @@ async function getLanguagesVoicesForVerbio(credentials, getTtsVoices, logger) {
|
||||
}
|
||||
}
|
||||
|
||||
async function getLanguagesAndVoicesForResemble(credential, getTtsVoices, logger) {
|
||||
if (credential) {
|
||||
try {
|
||||
const {api_key} = credential;
|
||||
let allVoices = [];
|
||||
let page = 1;
|
||||
let hasMorePages = true;
|
||||
// Fetch all pages of voices
|
||||
while (hasMorePages) {
|
||||
const response = await fetch(`https://app.resemble.ai/api/v2/voices?page=${page}&page_size=100`, {
|
||||
headers: {
|
||||
'Authorization': `Token token=${api_key}`,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('failed to list voices');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.success) {
|
||||
throw new Error('API returned unsuccessful response');
|
||||
}
|
||||
|
||||
allVoices = allVoices.concat(data.items);
|
||||
|
||||
// Check if there are more pages
|
||||
hasMorePages = page < data.num_pages;
|
||||
page++;
|
||||
}
|
||||
|
||||
// Filter only finished voices that support text_to_speech
|
||||
const availableVoices = allVoices.filter((voice) =>
|
||||
voice.status === 'finished' &&
|
||||
voice.component_status?.text_to_speech?.status === 'ready'
|
||||
);
|
||||
|
||||
// Group voices by language
|
||||
const ttsVoices = availableVoices.reduce((acc, voice) => {
|
||||
const languageCode = voice.default_language || 'en-US';
|
||||
const existingLanguage = acc.find((lang) => lang.value === languageCode);
|
||||
|
||||
const voiceEntry = {
|
||||
name: `${voice.name} (${voice.voice_type}) - ${voice.source}`,
|
||||
value: voice.uuid
|
||||
};
|
||||
|
||||
if (existingLanguage) {
|
||||
existingLanguage.voices.push(voiceEntry);
|
||||
} else {
|
||||
|
||||
acc.push({
|
||||
value: languageCode,
|
||||
name: capitalizeFirst(languageCode),
|
||||
voices: [voiceEntry]
|
||||
});
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
// Sort languages and voices
|
||||
ttsVoices.sort((a, b) => a.name.localeCompare(b.name));
|
||||
ttsVoices.forEach((lang) => {
|
||||
lang.voices.sort((a, b) => a.name.localeCompare(b.name));
|
||||
});
|
||||
return tranform(ttsVoices);
|
||||
} catch (err) {
|
||||
logger.info('Error while fetching Resemble languages, voices, return predefined values', err);
|
||||
}
|
||||
}
|
||||
|
||||
return tranform(TtsResembleLanguagesVoices);
|
||||
}
|
||||
|
||||
function tranform(tts, stt, models, sttModels) {
|
||||
return {
|
||||
...(tts && {tts}),
|
||||
@@ -1508,6 +1778,7 @@ module.exports = {
|
||||
testNuanceTts,
|
||||
testNuanceStt,
|
||||
testDeepgramStt,
|
||||
testGladiaStt,
|
||||
testIbmTts,
|
||||
testIbmStt,
|
||||
testSonioxStt,
|
||||
@@ -1526,5 +1797,7 @@ module.exports = {
|
||||
testSpeechmaticsStt,
|
||||
testCartesia,
|
||||
testVoxistStt,
|
||||
testOpenAiStt
|
||||
testOpenAiStt,
|
||||
testResembleTTS,
|
||||
testHoundifyStt
|
||||
};
|
||||
|
||||
796
package-lock.json
generated
796
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
12
package.json
12
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jambonz-api-server",
|
||||
"version": "0.9.4",
|
||||
"version": "0.9.5",
|
||||
"description": "",
|
||||
"main": "app.js",
|
||||
"scripts": {
|
||||
@@ -25,15 +25,16 @@
|
||||
"@aws-sdk/client-transcribe": "^3.549.0",
|
||||
"@azure/storage-blob": "^12.17.0",
|
||||
"@deepgram/sdk": "^1.21.0",
|
||||
"@gladiaio/sdk": "^0.5.2",
|
||||
"@google-cloud/speech": "^6.5.0",
|
||||
"@google-cloud/storage": "^7.9.0",
|
||||
"@jambonz/db-helpers": "^0.9.12",
|
||||
"@jambonz/db-helpers": "^0.9.18",
|
||||
"@jambonz/lamejs": "^1.2.2",
|
||||
"@jambonz/mw-registrar": "^0.2.7",
|
||||
"@jambonz/realtimedb-helpers": "^0.8.14",
|
||||
"@jambonz/speech-utils": "^0.2.13",
|
||||
"@jambonz/realtimedb-helpers": "^0.8.15",
|
||||
"@jambonz/speech-utils": "^0.2.26",
|
||||
"@jambonz/time-series": "^0.2.8",
|
||||
"@jambonz/verb-specifications": "^0.0.106",
|
||||
"@jambonz/verb-specifications": "^0.0.118",
|
||||
"@soniox/soniox-node": "^1.2.2",
|
||||
"ajv": "^8.17.1",
|
||||
"argon2": "^0.40.1",
|
||||
@@ -44,6 +45,7 @@
|
||||
"express-rate-limit": "^7.2.0",
|
||||
"form-data": "^4.0.0",
|
||||
"helmet": "^7.1.0",
|
||||
"houndify": "^3.1.14",
|
||||
"ibm-watson": "^9.0.1",
|
||||
"is-valid-hostname": "^1.0.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
|
||||
@@ -656,7 +656,8 @@ test('speech credentials tests', async(t) => {
|
||||
use_for_stt: true,
|
||||
use_for_tts: false,
|
||||
api_key: 'asdasdasdasddsadasda',
|
||||
model_id: 'eleven_multilingual_v2'
|
||||
model_id: 'eleven_multilingual_v2',
|
||||
api_uri: 'api.elevenlabs.io'
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 201, 'successfully added speech credential for elevenlabs');
|
||||
@@ -805,6 +806,29 @@ test('speech credentials tests', async(t) => {
|
||||
});
|
||||
t.ok(result.statusCode === 204, 'successfully deleted speech credential');
|
||||
|
||||
/* add a credential for houndify */
|
||||
result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, {
|
||||
resolveWithFullResponse: true,
|
||||
auth: authUser,
|
||||
json: true,
|
||||
body: {
|
||||
vendor: 'houndify',
|
||||
use_for_stt: true,
|
||||
client_key: "ClientKey",
|
||||
client_id: "ClientID",
|
||||
user_id: "test_user"
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 201, 'successfully added speech credential for houndify');
|
||||
const houndifySid = result.body.sid;
|
||||
|
||||
/* delete the credential */
|
||||
result = await request.delete(`/Accounts/${account_sid}/SpeechCredentials/${houndifySid}`, {
|
||||
auth: authUser,
|
||||
resolveWithFullResponse: true,
|
||||
});
|
||||
t.ok(result.statusCode === 204, 'successfully deleted speech credential');
|
||||
|
||||
/* add a credential for Voxist */
|
||||
result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, {
|
||||
resolveWithFullResponse: true,
|
||||
@@ -902,6 +926,72 @@ test('speech credentials tests', async(t) => {
|
||||
});
|
||||
t.ok(result.statusCode === 204, 'successfully deleted speech credential');
|
||||
|
||||
/* add a credential for resemble */
|
||||
result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, {
|
||||
resolveWithFullResponse: true,
|
||||
auth: authUser,
|
||||
json: true,
|
||||
body: {
|
||||
vendor: 'resemble',
|
||||
use_for_tts: true,
|
||||
use_for_stt: false,
|
||||
api_key: 'api_key',
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 201, 'successfully added speech credential for Resemble');
|
||||
const resembleSid = result.body.sid;
|
||||
|
||||
/* delete the credential */
|
||||
result = await request.delete(`/Accounts/${account_sid}/SpeechCredentials/${resembleSid}`, {
|
||||
auth: authUser,
|
||||
resolveWithFullResponse: true,
|
||||
});
|
||||
t.ok(result.statusCode === 204, 'successfully deleted speech credential for Resemble');
|
||||
|
||||
/* add a credential for deepgram river */
|
||||
result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, {
|
||||
resolveWithFullResponse: true,
|
||||
auth: authUser,
|
||||
json: true,
|
||||
body: {
|
||||
vendor: 'deepgramflux',
|
||||
use_for_tts: false,
|
||||
use_for_stt: true,
|
||||
api_key: 'api_key',
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 201, 'successfully added speech credential for Verbio');
|
||||
const deepgramfluxSid = result.body.sid;
|
||||
|
||||
/* delete the credential */
|
||||
result = await request.delete(`/Accounts/${account_sid}/SpeechCredentials/${deepgramfluxSid}`, {
|
||||
auth: authUser,
|
||||
resolveWithFullResponse: true,
|
||||
});
|
||||
t.ok(result.statusCode === 204, 'successfully deleted speech credential deepgramflux');
|
||||
|
||||
/* add a credential for gladia */
|
||||
result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, {
|
||||
resolveWithFullResponse: true,
|
||||
auth: authUser,
|
||||
json: true,
|
||||
body: {
|
||||
vendor: 'gladia',
|
||||
use_for_tts: false,
|
||||
use_for_stt: true,
|
||||
api_key: 'api_key',
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 201, 'successfully added speech credential for Gladia');
|
||||
const gladiaSid = result.body.sid;
|
||||
|
||||
/* delete the credential */
|
||||
result = await request.delete(`/Accounts/${account_sid}/SpeechCredentials/${gladiaSid}`, {
|
||||
auth: authUser,
|
||||
resolveWithFullResponse: true,
|
||||
});
|
||||
t.ok(result.statusCode === 204, 'successfully deleted speech credential for Gladia');
|
||||
|
||||
/* Check google supportedLanguagesAndVoices */
|
||||
result = await request.get(`/Accounts/${account_sid}/SpeechCredentials/speech/supportedLanguagesAndVoices?vendor=google`, {
|
||||
resolveWithFullResponse: true,
|
||||
@@ -1037,6 +1127,15 @@ test('speech credentials tests', async(t) => {
|
||||
t.ok(result.body.tts.length !== 0, 'successfully get whisper supported languages and voices');
|
||||
t.ok(result.body.models.length !== 0, 'successfully get whisper supported languages and voices');
|
||||
|
||||
/* Check gladia supportedLanguagesAndVoices */
|
||||
result = await request.get(`/Accounts/${account_sid}/SpeechCredentials/speech/supportedLanguagesAndVoices?vendor=gladia`, {
|
||||
resolveWithFullResponse: true,
|
||||
simple: false,
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
});
|
||||
t.ok(result.body.stt.length !== 0, 'successfully get gladia supported languages and voices');
|
||||
|
||||
await deleteObjectBySid(request, '/Accounts', account_sid);
|
||||
await deleteObjectBySid(request, '/ServiceProviders', service_provider_sid);
|
||||
t.end();
|
||||
|
||||
Reference in New Issue
Block a user