@jambonz/speech-utils 0.0.49 → 0.0.50

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.
@@ -85,7 +85,7 @@ async function synthAudio(client, logger, stats, { account_sid,
85
85
  logger = logger || noopLogger;
86
86
 
87
87
  assert.ok(['google', 'aws', 'polly', 'microsoft',
88
- 'wellsaid', 'nuance', 'nvidia', 'ibm', 'elevenlabs', 'whisper', 'deepgram'].includes(vendor) ||
88
+ 'wellsaid', 'nuance', 'nvidia', 'ibm', 'elevenlabs', 'whisper', 'deepgram', 'playht'].includes(vendor) ||
89
89
  vendor.startsWith('custom'),
90
90
  `synthAudio supported vendors are google, aws, microsoft, nuance, nvidia and wellsaid, not ${vendor}`);
91
91
  if ('google' === vendor) {
@@ -123,6 +123,11 @@ async function synthAudio(client, logger, stats, { account_sid,
123
123
  assert.ok(voice, 'synthAudio requires voice when elevenlabs is used');
124
124
  assert.ok(credentials.api_key, 'synthAudio requires api_key when elevenlabs is used');
125
125
  assert.ok(credentials.model_id, 'synthAudio requires model_id when elevenlabs is used');
126
+ } else if ('playht' === vendor) {
127
+ assert.ok(voice, 'synthAudio requires voice when playht is used');
128
+ assert.ok(credentials.api_key, 'synthAudio requires api_key when playht is used');
129
+ assert.ok(credentials.user_id, 'synthAudio requires user_id when playht is used');
130
+ assert.ok(credentials.voice_engine, 'synthAudio requires voice_engine when playht is used');
126
131
  } else if ('whisper' === vendor) {
127
132
  assert.ok(voice, 'synthAudio requires voice when whisper is used');
128
133
  assert.ok(credentials.model_id, 'synthAudio requires model when whisper is used');
@@ -206,6 +211,12 @@ async function synthAudio(client, logger, stats, { account_sid,
206
211
  });
207
212
  if (audioBuffer?.filePath) return audioBuffer;
208
213
  break;
214
+ case 'playht':
215
+ audioBuffer = await synthPlayHT(logger, {
216
+ credentials, options, stats, language, voice, text, renderForCaching, disableTtsStreaming, filePath
217
+ });
218
+ if (audioBuffer?.filePath) return audioBuffer;
219
+ break;
209
220
  case 'whisper':
210
221
  audioBuffer = await synthWhisper(logger, {
211
222
  credentials, stats, voice, text, renderForCaching, disableTtsStreaming});
@@ -682,6 +693,61 @@ const synthElevenlabs = async(logger, {
682
693
  }
683
694
  };
684
695
 
696
+ const synthPlayHT = async(logger, {
697
+ credentials, options, stats, voice, text, renderForCaching, disableTtsStreaming
698
+ }) => {
699
+ const {api_key, user_id, voice_engine, options: credOpts} = credentials;
700
+ const opts = !!options && Object.keys(options).length !== 0 ? options : JSON.parse(credOpts || '{}');
701
+
702
+ /* default to using the streaming interface, unless disabled by env var OR we want just a cache file */
703
+ if (!process.env.JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
704
+ let params = '';
705
+ params += `{api_key=${api_key}`;
706
+ params += `,user_id=${user_id}`;
707
+ params += ',vendor=playht';
708
+ params += `,voice=${voice}`;
709
+ params += `,voice_engine=${voice_engine}`;
710
+ params += ',write_cache_file=1';
711
+ if (opts.quality) params += `,quality=${opts.quality}`;
712
+ if (opts.speed) params += `,speed=${opts.speed}`;
713
+ if (opts.seed) params += `,style=${opts.seed}`;
714
+ if (opts.temperature) params += `,temperature=${opts.temperature}`;
715
+ if (opts.emotion) params += `,emotion=${opts.emotion}`;
716
+ if (opts.voice_guidance) params += `,voice_guidance=${opts.voice_guidance}`;
717
+ if (opts.style_guidance) params += `,style_guidance=${opts.style_guidance}`;
718
+ if (opts.text_guidance) params += `,text_guidance=${opts.text_guidance}`;
719
+ params += '}';
720
+
721
+ return {
722
+ filePath: `say:${params}${text.replace(/\n/g, ' ').replace(/\r/g, ' ')}`,
723
+ servedFromCache: false,
724
+ rtt: 0
725
+ };
726
+ }
727
+
728
+ try {
729
+ const post = bent('https://api.play.ht', 'POST', 'buffer', {
730
+ 'AUTHORIZATION': api_key,
731
+ 'X-USER-ID': user_id,
732
+ 'Accept': 'audio/mpeg',
733
+ 'Content-Type': 'application/json'
734
+ });
735
+ const mp3 = await post('/api/v2/tts/stream', {
736
+ text,
737
+ voice,
738
+ voice_engine,
739
+ output_format: 'mp3',
740
+ sample_rate: 8000,
741
+ ...opts
742
+ });
743
+ return mp3;
744
+ } catch (err) {
745
+ logger.info({err}, 'synth PlayHT returned error');
746
+ stats.increment('tts.count', ['vendor:playht', 'accepted:no']);
747
+ throw err;
748
+ }
749
+ };
750
+
685
751
  const synthWhisper = async(logger, {credentials, stats, voice, text, renderForCaching, disableTtsStreaming}) => {
686
752
  const {api_key, model_id, baseURL, timeout, speed} = credentials;
687
753
  /* if the env is set to stream then bag out, unless we are specifically rendering to generate a cache file */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jambonz/speech-utils",
3
- "version": "0.0.49",
3
+ "version": "0.0.50",
4
4
  "description": "TTS-related speech utilities for jambonz",
5
5
  "main": "index.js",
6
6
  "author": "Dave Horton",
package/test/synth.js CHANGED
@@ -549,6 +549,47 @@ test('Elevenlabs speech synth tests', async(t) => {
549
549
  client.quit();
550
550
  })
551
551
 
552
+ test('PlayHT speech synth tests', async(t) => {
553
+ const fn = require('..');
554
+ const {synthAudio, client} = fn(opts, logger);
555
+
556
+ if (!process.env.PLAYHT_API_KEY || !process.env.PLAYHT_USER_ID) {
557
+ t.pass('skipping PlayHT speech synth tests since PLAYHT_API_KEY or PLAYHT_USER_ID is/are not provided');
558
+ return t.end();
559
+ }
560
+ const text = 'Hi there and welcome to jambones!';
561
+ try {
562
+ let opts = await synthAudio(stats, {
563
+ vendor: 'playht',
564
+ credentials: {
565
+ api_key: process.env.PLAYHT_API_KEY,
566
+ user_id: process.env.PLAYHT_USER_ID,
567
+ voice_engine: 'PlayHT2.0-turbo',
568
+ options: JSON.stringify({
569
+ quality: "medium",
570
+ speed: 1,
571
+ seed: 1,
572
+ temperature: 1,
573
+ emotion: "female_happy",
574
+ voice_guidance: 3,
575
+ style_guidance: 20,
576
+ text_guidance: 1,
577
+ })
578
+ },
579
+ language: 'en-US',
580
+ voice: 's3://voice-cloning-zero-shot/d9ff78ba-d016-47f6-b0ef-dd630f59414e/female-cs/manifest.json',
581
+ text,
582
+ renderForCaching: true
583
+ });
584
+ t.ok(!opts.servedFromCache, `successfully synthesized eleven audio to ${opts.filePath}`);
585
+
586
+ } catch (err) {
587
+ console.error(JSON.stringify(err));
588
+ t.end(err);
589
+ }
590
+ client.quit();
591
+ })
592
+
552
593
  test('whisper speech synth tests', async(t) => {
553
594
  const fn = require('..');
554
595
  const {synthAudio, client} = fn(opts, logger);