@jambonz/speech-utils 0.0.24 → 0.0.25

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.
@@ -38,6 +38,7 @@ const {AudioEncoding} = require('../stubs/riva/proto/riva_audio_pb');
38
38
  const debug = require('debug')('jambonz:realtimedb-helpers');
39
39
  const EXPIRES = (process.env.JAMBONES_TTS_CACHE_DURATION_MINS || 4 * 60) * 60; // cache tts for 4 hours
40
40
  const TMP_FOLDER = '/tmp';
41
+ const OpenAI = require('openai');
41
42
 
42
43
 
43
44
  const trimTrailingSilence = (buffer) => {
@@ -83,7 +84,7 @@ async function synthAudio(client, logger, stats, { account_sid,
83
84
  logger = logger || noopLogger;
84
85
 
85
86
  assert.ok(['google', 'aws', 'polly', 'microsoft',
86
- 'wellsaid', 'nuance', 'nvidia', 'ibm', 'elevenlabs'].includes(vendor) ||
87
+ 'wellsaid', 'nuance', 'nvidia', 'ibm', 'elevenlabs', 'whisper'].includes(vendor) ||
87
88
  vendor.startsWith('custom'),
88
89
  `synthAudio supported vendors are google, aws, microsoft, nuance, nvidia and wellsaid, not ${vendor}`);
89
90
  if ('google' === vendor) {
@@ -117,6 +118,14 @@ async function synthAudio(client, logger, stats, { account_sid,
117
118
  language = 'en-US'; // WellSaid only supports English atm
118
119
  assert.ok(voice, 'synthAudio requires voice when wellsaid is used');
119
120
  assert.ok(!text.startsWith('<speak'), 'wellsaid does not support SSML tags');
121
+ } else if ('elevenlabs' === vendor) {
122
+ assert.ok(voice, 'synthAudio requires voice when elevenlabs is used');
123
+ assert.ok(credentials.api_key, 'synthAudio requires api_key when elevenlabs is used');
124
+ assert.ok(credentials.model_id, 'synthAudio requires model_id when elevenlabs is used');
125
+ } else if ('whisper' === vendor) {
126
+ assert.ok(voice, 'synthAudio requires voice when whisper is used');
127
+ assert.ok(credentials.model_id, 'synthAudio requires model when whisper is used');
128
+ assert.ok(credentials.api_key, 'synthAudio requires api_key when whisper is used');
120
129
  } else if (vendor.startsWith('custom')) {
121
130
  assert.ok(credentials.custom_tts_url, `synthAudio requires custom_tts_url in credentials when ${vendor} is used`);
122
131
  }
@@ -187,6 +196,9 @@ async function synthAudio(client, logger, stats, { account_sid,
187
196
  case 'elevenlabs':
188
197
  audioBuffer = await synthElevenlabs(logger, {credentials, stats, language, voice, text, filePath});
189
198
  break;
199
+ case 'whisper':
200
+ audioBuffer = await synthWhisper(logger, {credentials, stats, voice, text});
201
+ break;
190
202
  case vendor.startsWith('custom') ? vendor : 'cant_match_value':
191
203
  ({ audioBuffer, filePath } = await synthCustomVendor(logger,
192
204
  {credentials, stats, language, voice, text, filePath}));
@@ -591,11 +603,35 @@ const synthElevenlabs = async(logger, {credentials, stats, language, voice, text
591
603
  });
592
604
  return mp3;
593
605
  } catch (err) {
594
- logger.info({err}, 'synthEvenlabs returned error');
606
+ logger.info({err}, 'synth Elevenlabs returned error');
607
+ stats.increment('tts.count', ['vendor:elevenlabs', 'accepted:no']);
595
608
  throw err;
596
609
  }
597
610
  };
598
611
 
612
+ const synthWhisper = async(logger, {credentials, stats, voice, text}) => {
613
+ const {api_key, model_id, baseURL, timeout} = credentials;
614
+ try {
615
+ const openai = new OpenAI.OpenAI({
616
+ apiKey: api_key,
617
+ timeout: timeout || 5000,
618
+ ...(baseURL && {baseURL})
619
+ });
620
+
621
+ const mp3 = await openai.audio.speech.create({
622
+ model: model_id,
623
+ voice,
624
+ input: text,
625
+ response_format: 'mp3'
626
+ });
627
+ return Buffer.from(await mp3.arrayBuffer());
628
+ } catch (err) {
629
+ logger.info({err}, 'synth whisper returned error');
630
+ stats.increment('tts.count', ['vendor:openai', 'accepted:no']);
631
+ throw err;
632
+ }
633
+ }
634
+ ;
599
635
  const getFileExtFromMime = (mime) => {
600
636
  switch (mime) {
601
637
  case 'audio/wav':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jambonz/speech-utils",
3
- "version": "0.0.24",
3
+ "version": "0.0.25",
4
4
  "description": "TTS-related speech utilities for jambonz",
5
5
  "main": "index.js",
6
6
  "author": "Dave Horton",
@@ -32,8 +32,9 @@
32
32
  "form-urlencoded": "^6.1.0",
33
33
  "google-protobuf": "^3.21.2",
34
34
  "ibm-watson": "^8.0.0",
35
- "microsoft-cognitiveservices-speech-sdk": "^1.31.0",
36
35
  "ioredis": "^5.3.2",
36
+ "microsoft-cognitiveservices-speech-sdk": "^1.31.0",
37
+ "openai": "^4.16.2",
37
38
  "undici": "^5.21.0"
38
39
  },
39
40
  "devDependencies": {
package/test/synth.js CHANGED
@@ -38,7 +38,7 @@ test('Google speech synth tests', async(t) => {
38
38
  },
39
39
  },
40
40
  language: 'en-GB',
41
- gender: 'MALE',
41
+ gender: 'FEMALE',
42
42
  text: 'This is a test. This is only a test',
43
43
  salt: 'foo.bar',
44
44
  });
@@ -53,7 +53,7 @@ test('Google speech synth tests', async(t) => {
53
53
  },
54
54
  },
55
55
  language: 'en-GB',
56
- gender: 'MALE',
56
+ gender: 'FEMALE',
57
57
  text: 'This is a test. This is only a test',
58
58
  });
59
59
  t.ok(opts.servedFromCache, `successfully retrieved cached google audio from ${opts.filePath}`);
@@ -68,7 +68,7 @@ test('Google speech synth tests', async(t) => {
68
68
  },
69
69
  disableTtsCache: true,
70
70
  language: 'en-GB',
71
- gender: 'MALE',
71
+ gender: 'FEMALE',
72
72
  text: 'This is a test. This is only a test',
73
73
  });
74
74
  t.ok(!opts.servedFromCache, `successfully synthesized google audio regardless of current cache to ${opts.filePath}`);
@@ -450,7 +450,7 @@ test('Elevenlabs speech synth tests', async(t) => {
450
450
  const {synthAudio, client} = fn(opts, logger);
451
451
 
452
452
  if (!process.env.ELEVENLABS_API_KEY || !process.env.ELEVENLABS_VOICE_ID || !process.env.ELEVENLABS_MODEL_ID) {
453
- t.pass('skipping IBM Watson speech synth tests since IBM_TTS_API_KEY or IBM_TTS_API_KEY not provided');
453
+ t.pass('skipping ElevenLabs speech synth tests since ELEVENLABS_API_KEY or ELEVENLABS_VOICE_ID or ELEVENLABS_MODEL_ID not provided');
454
454
  return t.end();
455
455
  }
456
456
  const text = 'Hi there and welcome to jambones!';
@@ -474,6 +474,35 @@ test('Elevenlabs speech synth tests', async(t) => {
474
474
  client.quit();
475
475
  })
476
476
 
477
+ test('whisper speech synth tests', async(t) => {
478
+ const fn = require('..');
479
+ const {synthAudio, client} = fn(opts, logger);
480
+
481
+ if (!process.env.OPENAI_API_KEY) {
482
+ t.pass('skipping OPENAI speech synth tests since OPENAI_API_KEY not provided');
483
+ return t.end();
484
+ }
485
+ const text = 'Hi there and welcome to jambones!';
486
+ try {
487
+ let opts = await synthAudio(stats, {
488
+ vendor: 'whisper',
489
+ credentials: {
490
+ api_key: process.env.OPENAI_API_KEY,
491
+ model_id: 'tts-1'
492
+ },
493
+ language: 'en-US',
494
+ voice: 'alloy',
495
+ text,
496
+ });
497
+ t.ok(!opts.servedFromCache, `successfully synthesized whisper audio to ${opts.filePath}`);
498
+
499
+ } catch (err) {
500
+ console.error(JSON.stringify(err));
501
+ t.end(err);
502
+ }
503
+ client.quit();
504
+ })
505
+
477
506
  test('TTS Cache tests', async(t) => {
478
507
  const fn = require('..');
479
508
  const {purgeTtsCache, getTtsSize, client} = fn(opts, logger);