@jambonz/speech-utils 0.2.11 → 0.2.13

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'].includes(vendor) ||
101
+ 'whisper', 'deepgram', 'playht', 'rimelabs', 'verbio', 'cartesia', 'inworld'].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) {
@@ -141,6 +141,10 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
141
141
  assert.ok(credentials.api_key, 'synthAudio requires api_key when playht is used');
142
142
  assert.ok(credentials.user_id, 'synthAudio requires user_id when playht is used');
143
143
  assert.ok(credentials.voice_engine, 'synthAudio requires voice_engine when playht is used');
144
+ } else if ('inworld' === vendor) {
145
+ assert.ok(voice, 'synthAudio requires voice when inworld is used');
146
+ assert.ok(credentials.api_key, 'synthAudio requires api_key when inworld is used');
147
+ assert.ok(credentials.model_id, 'synthAudio requires model_id when inworld is used');
144
148
  } else if ('rimelabs' === vendor) {
145
149
  assert.ok(voice, 'synthAudio requires voice when rimelabs is used');
146
150
  assert.ok(credentials.api_key, 'synthAudio requires api_key when rimelabs is used');
@@ -238,6 +242,10 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
238
242
  audioData = await synthCartesia(logger, {
239
243
  credentials, options, stats, language, voice, text, renderForCaching, disableTtsStreaming});
240
244
  break;
245
+ case 'inworld':
246
+ audioData = await synthInworld(logger, {
247
+ credentials, options, stats, language, voice, text, renderForCaching, disableTtsStreaming});
248
+ break;
241
249
  case 'rimelabs':
242
250
  audioData = await synthRimelabs(logger, {
243
251
  credentials, options, stats, language, voice, text, renderForCaching, disableTtsStreaming});
@@ -479,10 +487,10 @@ async function _synthOnPremMicrosoft(logger, {
479
487
  /* microsoft enforces some properties and uses voice xml element so if the user did not supply do it for them */
480
488
  const words = content.slice(7, -8).trim().replace(/(\r\n|\n|\r)/gm, ' ');
481
489
  // eslint-disable-next-line max-len
482
- content = `<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xmlns:mstts="https://www.w3.org/2001/mstts" xml:lang="${language}"><lang xml:lang="${language}"><voice name="${voice}">${words}</voice></lang></speak>`;
490
+ content = `<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xmlns:mstts="https://www.w3.org/2001/mstts" xml:lang="${language}"><voice name="${voice}">${words}</voice></speak>`;
483
491
  logger.info({content}, 'synthMicrosoft');
484
492
  }
485
- else if (JAMBONES_AZURE_ENABLE_SSML) {
493
+ else if (JAMBONES_AZURE_ENABLE_SSML && !content.startsWith('<speak')) {
486
494
  // eslint-disable-next-line max-len
487
495
  content = `<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xmlns:mstts="https://www.w3.org/2001/mstts" xml:lang="${language}"><voice name="${voice}"><lang xml:lang="${language}">${text}</lang></voice></speak>`;
488
496
  }
@@ -532,10 +540,10 @@ const synthMicrosoft = async(logger, {
532
540
  /* microsoft enforces some properties and uses voice xml element so if the user did not supply do it for them */
533
541
  const words = content.slice(7, -8).trim().replace(/(\r\n|\n|\r)/gm, ' ');
534
542
  // eslint-disable-next-line max-len
535
- content = `<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xmlns:mstts="https://www.w3.org/2001/mstts" xml:lang="${language}"><lang xml:lang="${language}"><voice name="${voice}">${words}</voice></lang></speak>`;
543
+ content = `<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xmlns:mstts="https://www.w3.org/2001/mstts" xml:lang="${language}"><voice name="${voice}">${words}</voice></speak>`;
536
544
  logger.info({content}, 'synthMicrosoft');
537
545
  }
538
- else if (JAMBONES_AZURE_ENABLE_SSML) {
546
+ else if (JAMBONES_AZURE_ENABLE_SSML && !content.startsWith('<speak')) {
539
547
  // eslint-disable-next-line max-len
540
548
  content = `<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xmlns:mstts="https://www.w3.org/2001/mstts" xml:lang="${language}"><voice name="${voice}"><lang xml:lang="${language}">${text}</lang></voice></speak>`;
541
549
  }
@@ -954,6 +962,70 @@ const synthPlayHT = async(client, logger, {
954
962
  }
955
963
  };
956
964
 
965
+ const synthInworld = async(logger, {
966
+ credentials, options, stats, voice, text, renderForCaching, disableTtsStreaming
967
+ }) => {
968
+ const {api_key, model_id, options: credOpts} = credentials;
969
+ const opts = !!options && Object.keys(options).length !== 0 ? options : JSON.parse(credOpts || '{}');
970
+
971
+ /* default to using the streaming interface, unless disabled by env var OR we want just a cache file */
972
+ if (!JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
973
+ let params = '';
974
+ params += `{api_key=${api_key}`;
975
+ params += `,model_id=${model_id}`;
976
+ params += ',vendor=inworld';
977
+ params += `,voice=${voice}`;
978
+ params += ',write_cache_file=1';
979
+ if (opts.temperature) params += `,temperature=${opts.temperature}`;
980
+ if (opts.audioConfig?.pitch) params += `,pitch=${opts.pitch}`;
981
+ if (opts.audioConfig?.speakingRate) params += `,speakingRate=${opts.speakingRate}`;
982
+ params += '}';
983
+
984
+ return {
985
+ filePath: `say:${params}${text.replace(/\n/g, ' ').replace(/\r/g, ' ')}`,
986
+ servedFromCache: false,
987
+ rtt: 0
988
+ };
989
+ }
990
+
991
+ try {
992
+ const url = 'https://api.inworld.ai/tts/v1/voice';
993
+ const sampleRate = 8000;
994
+ const options = {
995
+ method: 'POST',
996
+ headers: {
997
+ 'Authorization': `Basic ${api_key}`,
998
+ 'Content-Type': 'application/json'
999
+ },
1000
+ body: JSON.stringify({
1001
+ text,
1002
+ voiceId: voice,
1003
+ modelId: model_id,
1004
+ audioConfig: {
1005
+ ...(opts.audioConfig || {}),
1006
+ audioEncoding: 'MP3',
1007
+ }
1008
+ })
1009
+ };
1010
+
1011
+ const response = await fetch(url, options);
1012
+
1013
+ if (!response.ok) {
1014
+ throw new Error(await response.text());
1015
+ }
1016
+ const json = await response.json();
1017
+ return {
1018
+ audioContent: Buffer.from(json.audioContent, 'base64'),
1019
+ extension: 'mp3',
1020
+ sampleRate
1021
+ };
1022
+ } catch (err) {
1023
+ logger.info({err}, 'synth inworld returned error');
1024
+ stats.increment('tts.count', ['vendor:inworld', 'accepted:no']);
1025
+ throw err;
1026
+ }
1027
+ };
1028
+
957
1029
  const synthRimelabs = async(logger, {
958
1030
  credentials, options, stats, language, voice, text, renderForCaching, disableTtsStreaming
959
1031
  }) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jambonz/speech-utils",
3
- "version": "0.2.11",
3
+ "version": "0.2.13",
4
4
  "description": "TTS-related speech utilities for jambonz",
5
5
  "main": "index.js",
6
6
  "author": "Dave Horton",
package/test/synth.js CHANGED
@@ -720,6 +720,36 @@ test('Cartesia speech synth tests', async(t) => {
720
720
  client.quit();
721
721
  });
722
722
 
723
+ test('inworld speech synth', async(t) => {
724
+ const fn = require('..');
725
+ const {synthAudio, client} = fn(opts, logger);
726
+
727
+ if (!process.env.INWORLD_API_KEY) {
728
+ t.pass('skipping inworld speech synth tests since INWORLD_API_KEY is not provided');
729
+ return t.end();
730
+ }
731
+ const text = 'Hi there and welcome to jambones!';
732
+ try {
733
+ const opts = await synthAudio(stats, {
734
+ vendor: 'inworld',
735
+ credentials: {
736
+ api_key: process.env.INWORLD_API_KEY,
737
+ model_id: 'inworld-tts-1'
738
+ },
739
+ language: 'en',
740
+ voice: 'Ashley',
741
+ text,
742
+ renderForCaching: true
743
+ });
744
+ t.ok(!opts.servedFromCache, `successfully synthesized inworld audio to ${opts.filePath}`);
745
+
746
+ } catch (err) {
747
+ console.error(JSON.stringify(err));
748
+ t.end(err);
749
+ }
750
+ client.quit();
751
+ });
752
+
723
753
  test('rimelabs speech synth tests mist', async(t) => {
724
754
  const fn = require('..');
725
755
  const {synthAudio, client} = fn(opts, logger);