@jambonz/speech-utils 0.2.12 → 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});
@@ -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.12",
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);