@jambonz/speech-utils 0.0.49 → 0.0.51

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.
@@ -8,8 +8,8 @@ jobs:
8
8
  build:
9
9
  runs-on: ubuntu-latest
10
10
  steps:
11
- - uses: actions/checkout@v3
12
- - uses: actions/setup-node@v3
11
+ - uses: actions/checkout@v4
12
+ - uses: actions/setup-node@v4
13
13
  with:
14
14
  node-version: lts/*
15
15
  - run: npm install
@@ -32,5 +32,7 @@ jobs:
32
32
  ELEVENLABS_API_KEY: ${{ secrets.ELEVENLABS_API_KEY }}
33
33
  ELEVENLABS_VOICE_ID: ${{ secrets.ELEVENLABS_VOICE_ID }}
34
34
  ELEVENLABS_MODEL_ID: ${{ secrets.ELEVENLABS_MODEL_ID }}
35
+ PLAYHT_USER_ID: ${{ secrets.PLAYHT_USER_ID }}
36
+ PLAYHT_API_KEY: ${{ secrets.PLAYHT_API_KEY }}
35
37
  JAMBONES_HTTP_PROXY_IP: 127.0.0.1
36
38
  JAMBONES_HTTP_PROXY_PORT: 3128
@@ -84,10 +84,10 @@ async function synthAudio(client, logger, stats, { account_sid,
84
84
  let rtt;
85
85
  logger = logger || noopLogger;
86
86
 
87
- assert.ok(['google', 'aws', 'polly', 'microsoft',
88
- 'wellsaid', 'nuance', 'nvidia', 'ibm', 'elevenlabs', 'whisper', 'deepgram'].includes(vendor) ||
87
+ assert.ok(['google', 'aws', 'polly', 'microsoft', 'wellsaid', 'nuance', 'nvidia', 'ibm', 'elevenlabs',
88
+ 'whisper', 'deepgram', 'playht', 'rimelabs'].includes(vendor) ||
89
89
  vendor.startsWith('custom'),
90
- `synthAudio supported vendors are google, aws, microsoft, nuance, nvidia and wellsaid, not ${vendor}`);
90
+ `synthAudio supported vendors are google, aws, microsoft, nuance, nvidia and wellsaid ..etc, not ${vendor}`);
91
91
  if ('google' === vendor) {
92
92
  assert.ok(language, 'synthAudio requires language when google is used');
93
93
  }
@@ -123,6 +123,15 @@ 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');
131
+ } else if ('rimelabs' === vendor) {
132
+ assert.ok(voice, 'synthAudio requires voice when rimelabs is used');
133
+ assert.ok(credentials.api_key, 'synthAudio requires api_key when rimelabs is used');
134
+ assert.ok(credentials.model_id, 'synthAudio requires model_id when rimelabs is used');
126
135
  } else if ('whisper' === vendor) {
127
136
  assert.ok(voice, 'synthAudio requires voice when whisper is used');
128
137
  assert.ok(credentials.model_id, 'synthAudio requires model when whisper is used');
@@ -146,7 +155,7 @@ async function synthAudio(client, logger, stats, { account_sid,
146
155
  ) ||
147
156
  (
148
157
  !process.env.JAMBONES_DISABLE_TTS_STREAMING &&
149
- ['elevenlabs', 'deepgram'].includes(vendor)
158
+ ['elevenlabs', 'deepgram', 'rimelabs'].includes(vendor)
150
159
  )
151
160
  ) {
152
161
  filePath = `${TMP_FOLDER}/${key.replace('tts:', `tts-${salt || ''}`)}.r8`;
@@ -206,6 +215,18 @@ async function synthAudio(client, logger, stats, { account_sid,
206
215
  });
207
216
  if (audioBuffer?.filePath) return audioBuffer;
208
217
  break;
218
+ case 'playht':
219
+ audioBuffer = await synthPlayHT(logger, {
220
+ credentials, options, stats, language, voice, text, renderForCaching, disableTtsStreaming, filePath
221
+ });
222
+ if (audioBuffer?.filePath) return audioBuffer;
223
+ break;
224
+ case 'rimelabs':
225
+ audioBuffer = await synthRimelabs(logger, {
226
+ credentials, options, stats, language, voice, text, renderForCaching, disableTtsStreaming, filePath
227
+ });
228
+ if (audioBuffer?.filePath) return audioBuffer;
229
+ break;
209
230
  case 'whisper':
210
231
  audioBuffer = await synthWhisper(logger, {
211
232
  credentials, stats, voice, text, renderForCaching, disableTtsStreaming});
@@ -682,6 +703,107 @@ const synthElevenlabs = async(logger, {
682
703
  }
683
704
  };
684
705
 
706
+ const synthPlayHT = async(logger, {
707
+ credentials, options, stats, voice, text, renderForCaching, disableTtsStreaming
708
+ }) => {
709
+ const {api_key, user_id, voice_engine, options: credOpts} = credentials;
710
+ const opts = !!options && Object.keys(options).length !== 0 ? options : JSON.parse(credOpts || '{}');
711
+
712
+ /* default to using the streaming interface, unless disabled by env var OR we want just a cache file */
713
+ if (!process.env.JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
714
+ let params = '';
715
+ params += `{api_key=${api_key}`;
716
+ params += `,user_id=${user_id}`;
717
+ params += ',vendor=playht';
718
+ params += `,voice=${voice}`;
719
+ params += `,voice_engine=${voice_engine}`;
720
+ params += ',write_cache_file=1';
721
+ if (opts.quality) params += `,quality=${opts.quality}`;
722
+ if (opts.speed) params += `,speed=${opts.speed}`;
723
+ if (opts.seed) params += `,style=${opts.seed}`;
724
+ if (opts.temperature) params += `,temperature=${opts.temperature}`;
725
+ if (opts.emotion) params += `,emotion=${opts.emotion}`;
726
+ if (opts.voice_guidance) params += `,voice_guidance=${opts.voice_guidance}`;
727
+ if (opts.style_guidance) params += `,style_guidance=${opts.style_guidance}`;
728
+ if (opts.text_guidance) params += `,text_guidance=${opts.text_guidance}`;
729
+ params += '}';
730
+
731
+ return {
732
+ filePath: `say:${params}${text.replace(/\n/g, ' ').replace(/\r/g, ' ')}`,
733
+ servedFromCache: false,
734
+ rtt: 0
735
+ };
736
+ }
737
+
738
+ try {
739
+ const post = bent('https://api.play.ht', 'POST', 'buffer', {
740
+ 'AUTHORIZATION': api_key,
741
+ 'X-USER-ID': user_id,
742
+ 'Accept': 'audio/mpeg',
743
+ 'Content-Type': 'application/json'
744
+ });
745
+ const mp3 = await post('/api/v2/tts/stream', {
746
+ text,
747
+ voice,
748
+ voice_engine,
749
+ output_format: 'mp3',
750
+ sample_rate: 8000,
751
+ ...opts
752
+ });
753
+ return mp3;
754
+ } catch (err) {
755
+ logger.info({err}, 'synth PlayHT returned error');
756
+ stats.increment('tts.count', ['vendor:playht', 'accepted:no']);
757
+ throw err;
758
+ }
759
+ };
760
+
761
+ const synthRimelabs = async(logger, {
762
+ credentials, options, stats, voice, text, renderForCaching, disableTtsStreaming
763
+ }) => {
764
+ const {api_key, model_id, options: credOpts} = credentials;
765
+ const opts = !!options && Object.keys(options).length !== 0 ? options : JSON.parse(credOpts || '{}');
766
+
767
+ /* default to using the streaming interface, unless disabled by env var OR we want just a cache file */
768
+ if (!process.env.JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
769
+ let params = '';
770
+ params += `{api_key=${api_key}`;
771
+ params += `,model_id=${model_id}`;
772
+ params += ',vendor=rimelabs';
773
+ params += `,voice=${voice}`;
774
+ params += ',write_cache_file=1';
775
+ if (opts.speedAlpha) params += `,speed_alpha=${opts.speedAlpha}`;
776
+ if (opts.reduceLatency) params += `,reduce_latency=${opts.reduceLatency}`;
777
+ params += '}';
778
+
779
+ return {
780
+ filePath: `say:${params}${text.replace(/\n/g, ' ').replace(/\r/g, ' ')}`,
781
+ servedFromCache: false,
782
+ rtt: 0
783
+ };
784
+ }
785
+
786
+ try {
787
+ const post = bent('https://users.rime.ai', 'POST', 'buffer', {
788
+ 'Authorization': `Bearer ${api_key}`,
789
+ 'Accept': 'audio/mp3',
790
+ 'Content-Type': 'application/json'
791
+ });
792
+ const mp3 = await post('/v1/rime-tts', {
793
+ speaker: voice,
794
+ text,
795
+ modelId: model_id,
796
+ samplingRate: 8000,
797
+ ...opts
798
+ });
799
+ return mp3;
800
+ } catch (err) {
801
+ logger.info({err}, 'synth rimelabs returned error');
802
+ stats.increment('tts.count', ['vendor:rimelabs', 'accepted:no']);
803
+ throw err;
804
+ }
805
+ };
806
+
685
807
  const synthWhisper = async(logger, {credentials, stats, voice, text, renderForCaching, disableTtsStreaming}) => {
686
808
  const {api_key, model_id, baseURL, timeout, speed} = credentials;
687
809
  /* 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.51",
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,81 @@ 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 playht 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
+
593
+ test('rimelabs speech synth tests', async(t) => {
594
+ const fn = require('..');
595
+ const {synthAudio, client} = fn(opts, logger);
596
+
597
+ if (!process.env.RIMELABS_API_KEY) {
598
+ t.pass('skipping rimelabs speech synth tests since RIMELABS_API_KEY is not provided');
599
+ return t.end();
600
+ }
601
+ const text = 'Hi there and welcome to jambones!';
602
+ try {
603
+ let opts = await synthAudio(stats, {
604
+ vendor: 'rimelabs',
605
+ credentials: {
606
+ api_key: process.env.RIMELABS_API_KEY,
607
+ model_id: 'mist',
608
+ options: JSON.stringify({
609
+ speedAlpha: 1.0,
610
+ reduceLatency: false
611
+ })
612
+ },
613
+ language: 'en-US',
614
+ voice: 'amber',
615
+ text,
616
+ renderForCaching: true
617
+ });
618
+ t.ok(!opts.servedFromCache, `successfully synthesized rimelabs audio to ${opts.filePath}`);
619
+
620
+ } catch (err) {
621
+ console.error(JSON.stringify(err));
622
+ t.end(err);
623
+ }
624
+ client.quit();
625
+ });
626
+
552
627
  test('whisper speech synth tests', async(t) => {
553
628
  const fn = require('..');
554
629
  const {synthAudio, client} = fn(opts, logger);