@jambonz/speech-utils 0.2.15 → 0.2.16

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.
@@ -98,7 +98,7 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
98
98
  logger = logger || noopLogger;
99
99
 
100
100
  assert.ok(['google', 'aws', 'polly', 'microsoft', 'wellsaid', 'nuance', 'nvidia', 'ibm', 'elevenlabs',
101
- 'whisper', 'deepgram', 'playht', 'rimelabs', 'verbio', 'cartesia', 'inworld'].includes(vendor) ||
101
+ 'whisper', 'deepgram', 'playht', 'rimelabs', 'verbio', 'cartesia', 'inworld', 'resemble'].includes(vendor) ||
102
102
  vendor.startsWith('custom'),
103
103
  `synthAudio supported vendors are google, aws, microsoft, nuance, nvidia and wellsaid ..etc, not ${vendor}`);
104
104
  if ('google' === vendor) {
@@ -166,6 +166,9 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
166
166
  } else if ('cartesia' === vendor) {
167
167
  assert.ok(credentials.api_key, 'synthAudio requires api_key when cartesia is used');
168
168
  assert.ok(credentials.model_id, 'synthAudio requires model_id when cartesia is used');
169
+ } else if (vendor === 'resemble') {
170
+ assert.ok(voice, 'synthAudio requires voice when resemble is used');
171
+ assert.ok(credentials.api_key, 'synthAudio requires api_key when resemble is used');
169
172
  }
170
173
 
171
174
  const key = makeSynthKey({
@@ -263,6 +266,10 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
263
266
  audioData = await synthDeepgram(logger, {credentials, stats, model, text,
264
267
  renderForCaching, disableTtsStreaming});
265
268
  break;
269
+ case 'resemble':
270
+ audioData = await synthResemble(logger, {
271
+ credentials, stats, voice, text, options, renderForCaching, disableTtsStreaming});
272
+ break;
266
273
  case vendor.startsWith('custom') ? vendor : 'cant_match_value':
267
274
  audioData = await synthCustomVendor(logger,
268
275
  {credentials, stats, language, voice, text});
@@ -1285,6 +1292,61 @@ const synthCartesia = async(logger, {
1285
1292
 
1286
1293
  };
1287
1294
 
1295
+ const synthResemble = async(logger, {
1296
+ credentials, options, stats, voice, text, renderForCaching, disableTtsStreaming
1297
+ }) => {
1298
+ const {api_key, resemble_tts_uri} = credentials;
1299
+ const {project_uuid, use_hd} = options || {};
1300
+
1301
+ /* default to using the streaming interface, unless disabled by env var OR we want just a cache file */
1302
+ if (!JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
1303
+ let params = '';
1304
+ params += `{api_key=${api_key}`;
1305
+ params += ',vendor=resemble';
1306
+ params += `,voice=${voice}`;
1307
+ params += ',write_cache_file=1';
1308
+ if (project_uuid) params += `,project_uuid=${project_uuid}`;
1309
+ if (use_hd) params += `,use_hd=${use_hd}`;
1310
+ if (resemble_tts_uri) params += `,endpoint=${resemble_tts_uri}`;
1311
+
1312
+ params += '}';
1313
+
1314
+ return {
1315
+ filePath: `say:${params}${text.replace(/\n/g, ' ').replace(/\r/g, ' ')}`,
1316
+ servedFromCache: false,
1317
+ rtt: 0
1318
+ };
1319
+ }
1320
+
1321
+ try {
1322
+ const baseUrl = resemble_tts_uri || 'https://f.cluster.resemble.ai';
1323
+ const post = bent(baseUrl, 'POST', 'buffer', {
1324
+ 'Authorization': `Bearer ${api_key}`,
1325
+ 'Content-Type': 'application/json'
1326
+ });
1327
+ const response = await post('/synthesize', {
1328
+ voice_uuid: voice,
1329
+ data: text,
1330
+ sample_rate: 8000,
1331
+ output_format: 'mp3',
1332
+ ...(project_uuid && {project_uuid}),
1333
+ ...(use_hd && {use_hd}),
1334
+ });
1335
+
1336
+ const json = JSON.parse(response.toString('utf8'));
1337
+ const audioContent = Buffer.from(json.audio_content, 'base64');
1338
+ return {
1339
+ audioContent,
1340
+ extension: 'mp3',
1341
+ sampleRate: 8000
1342
+ };
1343
+ } catch (err) {
1344
+ logger.info({err}, 'synth Elevenlabs returned error');
1345
+ stats.increment('tts.count', ['vendor:elevenlabs', 'accepted:no']);
1346
+ throw err;
1347
+ }
1348
+ };
1349
+
1288
1350
  const getFileExtFromMime = (mime) => {
1289
1351
  switch (mime) {
1290
1352
  case 'audio/wav':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jambonz/speech-utils",
3
- "version": "0.2.15",
3
+ "version": "0.2.16",
4
4
  "description": "TTS-related speech utilities for jambonz",
5
5
  "main": "index.js",
6
6
  "author": "Dave Horton",
package/test/synth.js CHANGED
@@ -750,6 +750,35 @@ test('inworld speech synth', async(t) => {
750
750
  client.quit();
751
751
  });
752
752
 
753
+ test('resemble speech synth', async(t) => {
754
+ const fn = require('..');
755
+ const {synthAudio, client} = fn(opts, logger);
756
+
757
+ if (!process.env.RESEMBLE_API_KEY) {
758
+ t.pass('skipping resemble speech synth tests since RESEMBLE_API_KEY is not provided');
759
+ return t.end();
760
+ }
761
+ const text = '<speak prompt="Speak in an excited, upbeat tone">Hello from Resemble!</speak>';
762
+ try {
763
+ const opts = await synthAudio(stats, {
764
+ vendor: 'resemble',
765
+ credentials: {
766
+ api_key: process.env.RESEMBLE_API_KEY,
767
+ },
768
+ language: 'en',
769
+ voice: '3f5fb9f1',
770
+ text,
771
+ renderForCaching: true
772
+ });
773
+ t.ok(!opts.servedFromCache, `successfully synthesized resemble audio to ${opts.filePath}`);
774
+
775
+ } catch (err) {
776
+ console.error(JSON.stringify(err));
777
+ t.end(err);
778
+ }
779
+ client.quit();
780
+ });
781
+
753
782
  test('rimelabs speech synth tests mist', async(t) => {
754
783
  const fn = require('..');
755
784
  const {synthAudio, client} = fn(opts, logger);