@jambonz/speech-utils 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/purge-tts-cache.js +8 -2
- package/lib/synth-audio.js +38 -11
- package/lib/utils.js +2 -2
- package/package.json +1 -1
- package/test/synth.js +30 -1
package/lib/purge-tts-cache.js
CHANGED
|
@@ -11,7 +11,8 @@ const debug = require('debug')('jambonz:realtimedb-helpers');
|
|
|
11
11
|
* @param {string} opts.text - text or ssml to synthesize
|
|
12
12
|
* @returns {object} result - {error, purgedCount}
|
|
13
13
|
*/
|
|
14
|
-
async function purgeTtsCache(client, logger, {all,
|
|
14
|
+
async function purgeTtsCache(client, logger, {all, account_sid, vendor,
|
|
15
|
+
language, voice, deploymentId, engine, text} = {all: true}) {
|
|
15
16
|
logger = logger || noopLogger;
|
|
16
17
|
|
|
17
18
|
let purgedCount = 0, error;
|
|
@@ -21,8 +22,13 @@ async function purgeTtsCache(client, logger, {all, vendor, language, voice, depl
|
|
|
21
22
|
const keys = await client.keysAsync('tts:*');
|
|
22
23
|
purgedCount = await client.delAsync(keys);
|
|
23
24
|
|
|
24
|
-
} else {
|
|
25
|
+
} else if (account_sid && !vendor && !language && !voice && !engine && !text) {
|
|
26
|
+
const keys = await client.keysAsync(`tts:${account_sid}:*`);
|
|
27
|
+
purgedCount = await client.delAsync(keys);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
25
30
|
const key = makeSynthKey({
|
|
31
|
+
account_sid,
|
|
26
32
|
vendor,
|
|
27
33
|
language: language || '',
|
|
28
34
|
voice: voice || deploymentId,
|
package/lib/synth-audio.js
CHANGED
|
@@ -32,7 +32,7 @@ const {
|
|
|
32
32
|
const {SynthesizeSpeechRequest} = require('../stubs/riva/proto/riva_tts_pb');
|
|
33
33
|
const {AudioEncoding} = require('../stubs/riva/proto/riva_audio_pb');
|
|
34
34
|
const debug = require('debug')('jambonz:realtimedb-helpers');
|
|
35
|
-
const EXPIRES =
|
|
35
|
+
const EXPIRES = (process.env.JAMBONES_TTS_CACHE_DURATION_MINS || 24 * 60) * 60; // cache tts for 24 hours
|
|
36
36
|
const TMP_FOLDER = '/tmp';
|
|
37
37
|
|
|
38
38
|
/**
|
|
@@ -52,7 +52,7 @@ const TMP_FOLDER = '/tmp';
|
|
|
52
52
|
* @returns object containing filepath to an mp3 file in the /tmp folder containing
|
|
53
53
|
* the synthesized audio, and a variable indicating whether it was served from cache
|
|
54
54
|
*/
|
|
55
|
-
async function synthAudio(client, logger, stats, {
|
|
55
|
+
async function synthAudio(client, logger, stats, { account_sid,
|
|
56
56
|
vendor, language, voice, gender, text, engine, salt, model, credentials, deploymentId, disableTtsCache
|
|
57
57
|
}) {
|
|
58
58
|
let audioBuffer;
|
|
@@ -95,8 +95,8 @@ async function synthAudio(client, logger, stats, {
|
|
|
95
95
|
} else if (vendor.startsWith('custom')) {
|
|
96
96
|
assert.ok(credentials.custom_tts_url, `synthAudio requires custom_tts_url in credentials when ${vendor} is used`);
|
|
97
97
|
}
|
|
98
|
-
|
|
99
98
|
const key = makeSynthKey({
|
|
99
|
+
account_sid,
|
|
100
100
|
vendor,
|
|
101
101
|
language: language || '',
|
|
102
102
|
voice: voice || deploymentId,
|
|
@@ -155,7 +155,8 @@ async function synthAudio(client, logger, stats, {
|
|
|
155
155
|
audioBuffer = await synthWellSaid(logger, {credentials, stats, language, voice, text, filePath});
|
|
156
156
|
break;
|
|
157
157
|
case vendor.startsWith('custom') ? vendor : 'cant_match_value':
|
|
158
|
-
audioBuffer = await synthCustomVendor(logger,
|
|
158
|
+
({ audioBuffer, filePath } = await synthCustomVendor(logger,
|
|
159
|
+
{credentials, stats, language, voice, text, filePath}));
|
|
159
160
|
break;
|
|
160
161
|
default:
|
|
161
162
|
assert(`synthAudio: unsupported speech vendor ${vendor}`);
|
|
@@ -440,30 +441,56 @@ const synthNvidia = async(client, logger, {credentials, stats, language, voice,
|
|
|
440
441
|
};
|
|
441
442
|
|
|
442
443
|
|
|
443
|
-
|
|
444
|
-
const synthCustomVendor = async(logger, {credentials, stats, language, voice, text}) => {
|
|
444
|
+
const synthCustomVendor = async(logger, {credentials, stats, language, voice, text, filePath}) => {
|
|
445
445
|
const {vendor, auth_token, custom_tts_url} = credentials;
|
|
446
446
|
|
|
447
447
|
try {
|
|
448
|
-
const post = bent('POST',
|
|
448
|
+
const post = bent('POST', {
|
|
449
449
|
'Authorization': `Bearer ${auth_token}`,
|
|
450
|
-
'Accept': 'audio/mpeg',
|
|
451
450
|
'Content-Type': 'application/json'
|
|
452
451
|
});
|
|
453
452
|
|
|
454
|
-
const
|
|
453
|
+
const response = await post(custom_tts_url, {
|
|
455
454
|
language,
|
|
456
|
-
format: 'audio/mpeg',
|
|
457
455
|
voice,
|
|
458
456
|
type: text.startsWith('<speak>') ? 'ssml' : 'text',
|
|
459
457
|
text
|
|
460
458
|
});
|
|
461
459
|
|
|
462
|
-
|
|
460
|
+
const regex = /\.[^\.]*$/g;
|
|
461
|
+
const mime = response.headers['content-type'];
|
|
462
|
+
const buffer = await response.arrayBuffer();
|
|
463
|
+
return {
|
|
464
|
+
audioBuffer: buffer,
|
|
465
|
+
filePath: filePath.replace(regex, getFileExtFromMime(mime))
|
|
466
|
+
};
|
|
463
467
|
} catch (err) {
|
|
464
468
|
logger.info({err}, `Vendor ${vendor} returned error`);
|
|
465
469
|
throw err;
|
|
466
470
|
}
|
|
467
471
|
};
|
|
468
472
|
|
|
473
|
+
const getFileExtFromMime = (mime) => {
|
|
474
|
+
switch (mime) {
|
|
475
|
+
case 'audio/wav':
|
|
476
|
+
case 'audio/x-wav':
|
|
477
|
+
return '.wav';
|
|
478
|
+
case /audio\/l16.*rate=8000/.test(mime) ? mime : 'cant match value':
|
|
479
|
+
return '.r8';
|
|
480
|
+
case /audio\/l16.*rate=16000/.test(mime) ? mime : 'cant match value':
|
|
481
|
+
return '.r16';
|
|
482
|
+
case /audio\/l16.*rate=24000/.test(mime) ? mime : 'cant match value':
|
|
483
|
+
return '.r24';
|
|
484
|
+
case /audio\/l16.*rate=32000/.test(mime) ? mime : 'cant match value':
|
|
485
|
+
return '.r32';
|
|
486
|
+
case /audio\/l16.*rate=48000/.test(mime) ? mime : 'cant match value':
|
|
487
|
+
return '.r48';
|
|
488
|
+
case 'audio/mpeg':
|
|
489
|
+
case 'audio/mp3':
|
|
490
|
+
return '.mp3';
|
|
491
|
+
default:
|
|
492
|
+
return '.wav';
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
|
|
469
496
|
module.exports = synthAudio;
|
package/lib/utils.js
CHANGED
|
@@ -16,10 +16,10 @@ const debug = require('debug')('jambonz:realtimedb-helpers');
|
|
|
16
16
|
*/
|
|
17
17
|
//const nuanceClientMap = new Map();
|
|
18
18
|
|
|
19
|
-
function makeSynthKey({vendor, language, voice, engine = '', text}) {
|
|
19
|
+
function makeSynthKey({account_sid = '', vendor, language, voice, engine = '', text}) {
|
|
20
20
|
const hash = crypto.createHash('sha1');
|
|
21
21
|
hash.update(`${language}:${vendor}:${voice}:${engine}:${text}`);
|
|
22
|
-
return `tts:${hash.digest('hex')}`;
|
|
22
|
+
return `tts${account_sid ? (':' + account_sid) : ''}:${hash.digest('hex')}`;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
const noopLogger = {
|
package/package.json
CHANGED
package/test/synth.js
CHANGED
|
@@ -348,7 +348,6 @@ test('Custom Vendor speech synth tests', async(t) => {
|
|
|
348
348
|
let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/somethingnew`);
|
|
349
349
|
t.ok(obj.headers.Authorization == 'Bearer some_jwt_token', 'Custom Vendor Authentication Header is correct');
|
|
350
350
|
t.ok(obj.body.language == 'en-US', 'Custom Vendor Language is correct');
|
|
351
|
-
t.ok(obj.body.format == 'audio/mpeg', 'Custom Vendor format is correct');
|
|
352
351
|
t.ok(obj.body.voice == 'English-US.Female-1', 'Custom Vendor voice is correct');
|
|
353
352
|
t.ok(obj.body.type == 'text', 'Custom Vendor type is correct');
|
|
354
353
|
t.ok(obj.body.text == 'This is a test. This is only a test', 'Custom Vendor text is correct');
|
|
@@ -427,5 +426,35 @@ test('TTS Cache tests', async(t) => {
|
|
|
427
426
|
t.end(err);
|
|
428
427
|
}
|
|
429
428
|
|
|
429
|
+
try {
|
|
430
|
+
// clear cache
|
|
431
|
+
await purgeTtsCache();
|
|
432
|
+
// save some random tts keys to cache
|
|
433
|
+
const minRecords = 8;
|
|
434
|
+
const account_sid = "12412512_cabc_5aff"
|
|
435
|
+
const account_sid2 = "22412512_cabc_5aff"
|
|
436
|
+
for (const i in Array(minRecords).fill(0)) {
|
|
437
|
+
await client.setAsync(makeSynthKey({account_sid, vendor: i, language: i, voice: i, engine: i, text: i}), i);
|
|
438
|
+
}
|
|
439
|
+
for (const i in Array(minRecords).fill(0)) {
|
|
440
|
+
await client.setAsync(makeSynthKey({account_sid: account_sid2, vendor: i, language: i, voice: i, engine: i, text: i}), i);
|
|
441
|
+
}
|
|
442
|
+
const {purgedCount} = await purgeTtsCache({account_sid});
|
|
443
|
+
t.equal(purgedCount, minRecords, `successfully purged at least ${minRecords} tts records from cache for account_sid:${account_sid}`);
|
|
444
|
+
|
|
445
|
+
let cached = (await client.keysAsync('tts:*')).length;
|
|
446
|
+
t.equal(cached, minRecords, `successfully purged all tts records from cache for account_sid:${account_sid}`);
|
|
447
|
+
|
|
448
|
+
const {purgedCount: purgedCount2} = await purgeTtsCache({account_sid: account_sid2});
|
|
449
|
+
t.equal(purgedCount2, minRecords, `successfully purged at least ${minRecords} tts records from cache for account_sid:${account_sid2}`);
|
|
450
|
+
|
|
451
|
+
cached = (await client.keysAsync('tts:*')).length;
|
|
452
|
+
t.equal(cached, 0, `successfully purged all tts records from cache`);
|
|
453
|
+
|
|
454
|
+
} catch (err) {
|
|
455
|
+
console.error(JSON.stringify(err));
|
|
456
|
+
t.end(err);
|
|
457
|
+
}
|
|
458
|
+
|
|
430
459
|
client.quit();
|
|
431
460
|
});
|