@jambonz/speech-utils 1.0.10 → 1.0.11

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.
@@ -80,7 +80,8 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
80
80
  logger = logger || noopLogger;
81
81
 
82
82
  assert.ok(['google', 'aws', 'polly', 'microsoft', 'wellsaid', 'nvidia', 'elevenlabs',
83
- 'whisper', 'deepgram', 'deepgramflux', 'rimelabs', 'cartesia', 'inworld', 'resemble', 'murf', 'xai']
83
+ 'whisper', 'deepgram', 'deepgramflux', 'rimelabs', 'cartesia', 'nineninesix', 'inworld', 'resemble',
84
+ 'murf', 'xai']
84
85
  .includes(vendor) ||
85
86
  vendor.startsWith('custom'),
86
87
  `synthAudio supported vendors are google, aws, microsoft, nvidia and wellsaid ..etc, not ${vendor}`);
@@ -135,6 +136,10 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
135
136
  } else if ('cartesia' === vendor) {
136
137
  assert.ok(credentials.api_key, 'synthAudio requires api_key when cartesia is used');
137
138
  assert.ok(credentials.model_id, 'synthAudio requires model_id when cartesia is used');
139
+ } else if ('nineninesix' === vendor) {
140
+ assert.ok(voice, 'synthAudio requires voice when nineninesix is used');
141
+ assert.ok(credentials.api_key, 'synthAudio requires api_key when nineninesix is used');
142
+ assert.ok(credentials.model_id, 'synthAudio requires model_id when nineninesix is used');
138
143
  } else if ('murf' === vendor) {
139
144
  assert.ok(voice, 'synthAudio requires voice when murf is used');
140
145
  assert.ok(credentials.api_key, 'synthAudio requires api_key when murf is used');
@@ -212,6 +217,11 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
212
217
  credentials, options, stats, language, voice, key, text, renderForCaching, disableTtsStreaming,
213
218
  disableTtsCache});
214
219
  break;
220
+ case 'nineninesix':
221
+ audioData = await synthNineninesix(logger, {
222
+ credentials, stats, language, voice, key, text, renderForCaching, disableTtsStreaming,
223
+ disableTtsCache});
224
+ break;
215
225
  case 'inworld':
216
226
  audioData = await synthInworld(logger, {
217
227
  credentials, options, stats, language, voice, key, text, renderForCaching, disableTtsStreaming,
@@ -1423,6 +1433,61 @@ const synthCartesia = async(logger, {
1423
1433
 
1424
1434
  };
1425
1435
 
1436
+ /* nineninesix.ai — a Cartesia-compatible API, but only raw/wav come back
1437
+ (mp3 is rejected), so the cache render asks for wav rather than mp3. */
1438
+ const synthNineninesix = async(logger, {
1439
+ credentials, stats, voice, language, key, text, renderForCaching, disableTtsStreaming, disableTtsCache
1440
+ }) => {
1441
+ const {api_key, model_id} = credentials;
1442
+
1443
+ /* default to using the streaming interface, unless disabled by env var OR we want just a cache file */
1444
+ if (!JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
1445
+ let params = '{';
1446
+ params += `api_key=${api_key}`;
1447
+ params += `,playback_id=${key}`;
1448
+ params += `,model_id=${model_id}`;
1449
+ params += ',vendor=nineninesix';
1450
+ params += `,voice=${voice}`;
1451
+ params += `,write_cache_file=${disableTtsCache ? 0 : 1}`;
1452
+ if (language) params += `,language=${language}`;
1453
+ params += '}';
1454
+
1455
+ return {
1456
+ filePath: `say:${params}${text.replace(/\n/g, ' ').replace(/\r/g, ' ')}`,
1457
+ servedFromCache: false,
1458
+ rtt: 0
1459
+ };
1460
+ }
1461
+
1462
+ try {
1463
+ const sampleRate = 8000;
1464
+ const post = bent('https://api.nineninesix.ai', 'POST', 'buffer', {
1465
+ 'Authorization': `Bearer ${api_key}`,
1466
+ 'Content-Type': 'application/json'
1467
+ });
1468
+ const audioContent = await post('/tts/bytes', {
1469
+ model_id,
1470
+ transcript: text,
1471
+ voice: {mode: 'id', id: voice},
1472
+ ...(language && {language}),
1473
+ output_format: {
1474
+ container: 'wav',
1475
+ encoding: 'pcm_s16le',
1476
+ sample_rate: sampleRate
1477
+ }
1478
+ });
1479
+ return {
1480
+ audioContent,
1481
+ extension: 'wav',
1482
+ sampleRate
1483
+ };
1484
+ } catch (err) {
1485
+ logger.info({err}, 'synth nineninesix returned error');
1486
+ stats.increment('tts.count', ['vendor:nineninesix', 'accepted:no']);
1487
+ throw err;
1488
+ }
1489
+ };
1490
+
1426
1491
  const synthResemble = async(logger, {
1427
1492
  credentials, options, stats, voice, key, text, renderForCaching, disableTtsStreaming, disableTtsCache
1428
1493
  }) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jambonz/speech-utils",
3
- "version": "1.0.10",
3
+ "version": "1.0.11",
4
4
  "description": "TTS-related speech utilities for jambonz",
5
5
  "main": "index.js",
6
6
  "author": "Dave Horton",
package/test/synth.js CHANGED
@@ -1058,6 +1058,36 @@ test('Cartesia speech synth tests', async(t) => {
1058
1058
  client.quit();
1059
1059
  });
1060
1060
 
1061
+ test('nineninesix speech synth tests', async(t) => {
1062
+ const fn = require('..');
1063
+ const {synthAudio, client} = fn(opts, logger);
1064
+
1065
+ if (!process.env.NINENINESIX_API_KEY) {
1066
+ t.pass('skipping nineninesix speech synth tests since NINENINESIX_API_KEY is not provided');
1067
+ return t.end();
1068
+ }
1069
+ const text = 'Hi there and welcome to jambones! ' + Date.now();
1070
+ try {
1071
+ const opts = await synthAudio(stats, {
1072
+ vendor: 'nineninesix',
1073
+ credentials: {
1074
+ api_key: process.env.NINENINESIX_API_KEY,
1075
+ model_id: 'gepard-1.0'
1076
+ },
1077
+ language: 'en',
1078
+ voice: '3ad7a827-7fd1-4954-bf35-47d4cc33d9ed',
1079
+ text,
1080
+ renderForCaching: true
1081
+ });
1082
+ t.ok(!opts.servedFromCache, `successfully synthed nineninesix audio to ${opts.filePath}`);
1083
+
1084
+ } catch (err) {
1085
+ console.error(JSON.stringify(err));
1086
+ t.end(err);
1087
+ }
1088
+ client.quit();
1089
+ });
1090
+
1061
1091
  test('inworld speech synth', async(t) => {
1062
1092
  const fn = require('..');
1063
1093
  const {synthAudio, client} = fn(opts, logger);