@jambonz/speech-utils 0.1.0 → 0.1.2

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.
package/index.js CHANGED
@@ -14,6 +14,7 @@ module.exports = (opts, logger) => {
14
14
  purgeTtsCache: require('./lib/purge-tts-cache').bind(null, client, logger),
15
15
  addFileToCache: require('./lib/add-file-to-cache').bind(null, client, logger),
16
16
  synthAudio: require('./lib/synth-audio').bind(null, client, createHash, retrieveHash, logger),
17
+ getVerbioAccessToken: require('./lib/get-verbio-token').bind(null, client, logger),
17
18
  getNuanceAccessToken: require('./lib/get-nuance-access-token').bind(null, client, logger),
18
19
  getIbmAccessToken: require('./lib/get-ibm-access-token').bind(null, client, logger),
19
20
  getAwsAuthToken: require('./lib/get-aws-sts-token').bind(null, logger, createHash, retrieveHash),
@@ -1,6 +1,7 @@
1
1
  const fs = require('fs/promises');
2
2
  const {noopLogger, makeSynthKey} = require('./utils');
3
- const EXPIRES = (process.env.JAMBONES_TTS_CACHE_DURATION_MINS || 4 * 60) * 60; // cache tts for 4 hours
3
+ const {JAMBONES_TTS_CACHE_DURATION_MINS} = require('./config');
4
+ const EXPIRES = JAMBONES_TTS_CACHE_DURATION_MINS;
4
5
 
5
6
  async function addFileToCache(client, logger, path,
6
7
  {account_sid, vendor, language, voice, deploymentId, engine, text}) {
package/lib/config.js ADDED
@@ -0,0 +1,20 @@
1
+ const JAMBONES_TTS_TRIM_SILENCE = process.env.JAMBONES_TTS_TRIM_SILENCE;
2
+ const JAMBONES_DISABLE_TTS_STREAMING = process.env.JAMBONES_DISABLE_TTS_STREAMING;
3
+
4
+ const JAMBONES_HTTP_PROXY_IP = process.env.JAMBONES_HTTP_PROXY_IP;
5
+ const JAMBONES_HTTP_PROXY_PORT = process.env.JAMBONES_HTTP_PROXY_PORT;
6
+ const JAMBONES_TTS_CACHE_DURATION_MINS = (parseInt(process.env.JAMBONES_TTS_CACHE_DURATION_MINS) || 4 * 60) * 60; // cache tts for 4 hours
7
+
8
+ const TMP_FOLDER = '/tmp';
9
+
10
+ const HTTP_TIMEOUT = 5000;
11
+
12
+ module.exports = {
13
+ JAMBONES_TTS_TRIM_SILENCE,
14
+ JAMBONES_DISABLE_TTS_STREAMING,
15
+ JAMBONES_HTTP_PROXY_IP,
16
+ JAMBONES_HTTP_PROXY_PORT,
17
+ JAMBONES_TTS_CACHE_DURATION_MINS,
18
+ TMP_FOLDER,
19
+ HTTP_TIMEOUT
20
+ };
@@ -2,8 +2,8 @@ const formurlencoded = require('form-urlencoded');
2
2
  const {Pool} = require('undici');
3
3
  const pool = new Pool('https://iam.cloud.ibm.com');
4
4
  const {makeIbmKey, noopLogger} = require('./utils');
5
+ const { HTTP_TIMEOUT } = require('./config');
5
6
  const debug = require('debug')('jambonz:realtimedb-helpers');
6
- const HTTP_TIMEOUT = 5000;
7
7
 
8
8
  async function getIbmAccessToken(client, logger, apiKey) {
9
9
  logger = logger || noopLogger;
@@ -2,8 +2,8 @@ const formurlencoded = require('form-urlencoded');
2
2
  const {Pool} = require('undici');
3
3
  const pool = new Pool('https://auth.crt.nuance.com');
4
4
  const {makeNuanceKey, makeBasicAuthHeader, noopLogger} = require('./utils');
5
+ const { HTTP_TIMEOUT } = require('./config');
5
6
  const debug = require('debug')('jambonz:realtimedb-helpers');
6
- const HTTP_TIMEOUT = 5000;
7
7
 
8
8
  async function getNuanceAccessToken(client, logger, clientId, secret, scope) {
9
9
  logger = logger || noopLogger;
@@ -1,12 +1,16 @@
1
1
  const assert = require('assert');
2
2
  const {noopLogger, createNuanceClient, createKryptonClient} = require('./utils');
3
3
  const getNuanceAccessToken = require('./get-nuance-access-token');
4
+ const getVerbioAccessToken = require('./get-verbio-token');
4
5
  const {GetVoicesRequest, Voice} = require('../stubs/nuance/synthesizer_pb');
5
6
  const TextToSpeechV1 = require('ibm-watson/text-to-speech/v1');
6
7
  const { IamAuthenticator } = require('ibm-watson/auth');
7
8
  const ttsGoogle = require('@google-cloud/text-to-speech');
8
9
  const { PollyClient, DescribeVoicesCommand } = require('@aws-sdk/client-polly');
9
10
  const getAwsAuthToken = require('./get-aws-sts-token');
11
+ const {Pool} = require('undici');
12
+ const { HTTP_TIMEOUT } = require('./config');
13
+ const verbioVoicePool = new Pool('https://us.rest.speechcenter.verbio.com');
10
14
 
11
15
  const getIbmVoices = async(client, logger, credentials) => {
12
16
  const {tts_region, tts_api_key} = credentials;
@@ -117,6 +121,26 @@ const getAwsVoices = async(_client, createHash, retrieveHash, logger, credential
117
121
  }
118
122
  };
119
123
 
124
+ const getVerbioVoices = async(client, logger, credentials) => {
125
+ try {
126
+ const access_token = await getVerbioAccessToken(client, logger, credentials);
127
+ const { body} = await verbioVoicePool.request({
128
+ path: '/api/v1/voices',
129
+ method: 'GET',
130
+ headers: {
131
+ 'Authorization': `Bearer ${access_token.access_token}`,
132
+ 'User-Agent': 'jambonz'
133
+ },
134
+ timeout: HTTP_TIMEOUT,
135
+ followRedirects: false
136
+ });
137
+ return await body.json();
138
+ } catch (err) {
139
+ logger.info({err}, 'getVerbioVoices - failed to list voices for Verbio');
140
+ throw err;
141
+ }
142
+ };
143
+
120
144
  /**
121
145
  * Synthesize speech to an mp3 file, and also cache the generated speech
122
146
  * in redis (base64 format) for 24 hours so as to avoid unnecessarily paying
@@ -136,7 +160,7 @@ const getAwsVoices = async(_client, createHash, retrieveHash, logger, credential
136
160
  async function getTtsVoices(client, createHash, retrieveHash, logger, {vendor, credentials}) {
137
161
  logger = logger || noopLogger;
138
162
 
139
- assert.ok(['nuance', 'ibm', 'google', 'aws', 'polly'].includes(vendor),
163
+ assert.ok(['nuance', 'ibm', 'google', 'aws', 'polly', 'verbio'].includes(vendor),
140
164
  `getTtsVoices not supported for vendor ${vendor}`);
141
165
 
142
166
  switch (vendor) {
@@ -149,6 +173,8 @@ async function getTtsVoices(client, createHash, retrieveHash, logger, {vendor, c
149
173
  case 'aws':
150
174
  case 'polly':
151
175
  return getAwsVoices(client, createHash, retrieveHash, logger, credentials);
176
+ case 'verbio':
177
+ return getVerbioVoices(client, logger, credentials);
152
178
  default:
153
179
  break;
154
180
  }
@@ -0,0 +1,51 @@
1
+ const {Pool} = require('undici');
2
+ const { noopLogger, makeVerbioKey } = require('./utils');
3
+ const { HTTP_TIMEOUT } = require('./config');
4
+ const pool = new Pool('https://auth.speechcenter.verbio.com:444');
5
+ const debug = require('debug')('jambonz:realtimedb-helpers');
6
+
7
+ async function getVerbioAccessToken(client, logger, credentials) {
8
+ logger = logger || noopLogger;
9
+ const { client_id, client_secret } = credentials;
10
+ try {
11
+ const key = makeVerbioKey(client_id);
12
+ const access_token = await client.get(key);
13
+ if (access_token) {
14
+ return {access_token, servedFromCache: true};
15
+ }
16
+
17
+ const payload = {
18
+ client_id,
19
+ client_secret
20
+ };
21
+
22
+ const {statusCode, headers, body} = await pool.request({
23
+ path: '/api/v1/token',
24
+ method: 'POST',
25
+ headers: {
26
+ 'Content-Type': 'application/json',
27
+ 'User-Agent': 'jambonz'
28
+ },
29
+ body: JSON.stringify(payload),
30
+ timeout: HTTP_TIMEOUT,
31
+ followRedirects: false
32
+ });
33
+
34
+ if (200 !== statusCode) {
35
+ logger.debug({statusCode, headers, body: await body.text()}, 'error fetching access token from Verbio');
36
+ const err = new Error();
37
+ err.statusCode = statusCode;
38
+ throw err;
39
+ }
40
+ const json = await body.json();
41
+ const expiry = Math.floor(json.expiration_time - Date.now() / 1000 - 30);
42
+ await client.set(key, json.access_token, 'EX', expiry);
43
+ return {...json, servedFromCache: false};
44
+ } catch (err) {
45
+ debug(err, `getVerbioAccessToken: Error retrieving Verbio access token for client_id ${client_id}`);
46
+ logger.error(err, `getVerbioAccessToken: Error retrieving Verbio access token for client_id ${client_id}`);
47
+ throw err;
48
+ }
49
+ }
50
+
51
+ module.exports = getVerbioAccessToken;
@@ -19,9 +19,11 @@ const {
19
19
  createNuanceClient,
20
20
  createKryptonClient,
21
21
  createRivaClient,
22
- noopLogger
22
+ noopLogger,
23
+ makeFilePath
23
24
  } = require('./utils');
24
25
  const getNuanceAccessToken = require('./get-nuance-access-token');
26
+ const getVerbioAccessToken = require('./get-verbio-token');
25
27
  const {
26
28
  SynthesisRequest,
27
29
  Voice,
@@ -36,8 +38,13 @@ const {
36
38
  const {SynthesizeSpeechRequest} = require('../stubs/riva/proto/riva_tts_pb');
37
39
  const {AudioEncoding} = require('../stubs/riva/proto/riva_audio_pb');
38
40
  const debug = require('debug')('jambonz:realtimedb-helpers');
39
- const EXPIRES = (process.env.JAMBONES_TTS_CACHE_DURATION_MINS || 4 * 60) * 60; // cache tts for 4 hours
40
- const TMP_FOLDER = '/tmp';
41
+ const {
42
+ JAMBONES_DISABLE_TTS_STREAMING,
43
+ JAMBONES_HTTP_PROXY_IP,
44
+ JAMBONES_HTTP_PROXY_PORT,
45
+ JAMBONES_TTS_CACHE_DURATION_MINS,
46
+ } = require('./config');
47
+ const EXPIRES = JAMBONES_TTS_CACHE_DURATION_MINS;
41
48
  const OpenAI = require('openai');
42
49
  const getAwsAuthToken = require('./get-aws-sts-token');
43
50
 
@@ -86,7 +93,7 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
86
93
  logger = logger || noopLogger;
87
94
 
88
95
  assert.ok(['google', 'aws', 'polly', 'microsoft', 'wellsaid', 'nuance', 'nvidia', 'ibm', 'elevenlabs',
89
- 'whisper', 'deepgram', 'playht', 'rimelabs'].includes(vendor) ||
96
+ 'whisper', 'deepgram', 'playht', 'rimelabs', 'verbio'].includes(vendor) ||
90
97
  vendor.startsWith('custom'),
91
98
  `synthAudio supported vendors are google, aws, microsoft, nuance, nvidia and wellsaid ..etc, not ${vendor}`);
92
99
  if ('google' === vendor) {
@@ -139,6 +146,10 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
139
146
  assert.ok(credentials.api_key, 'synthAudio requires api_key when whisper is used');
140
147
  } else if (vendor.startsWith('custom')) {
141
148
  assert.ok(credentials.custom_tts_url, `synthAudio requires custom_tts_url in credentials when ${vendor} is used`);
149
+ } else if ('verbio' === vendor) {
150
+ assert.ok(voice, 'synthAudio requires voice when verbio is used');
151
+ assert.ok(credentials.client_id, 'synthAudio requires client_id when verbio is used');
152
+ assert.ok(credentials.client_secret, 'synthAudio requires client_secret when verbio is used');
142
153
  }
143
154
  const key = makeSynthKey({
144
155
  account_sid,
@@ -149,19 +160,7 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
149
160
  text
150
161
  });
151
162
  let filePath;
152
- if (['nuance', 'nvidia'].includes(vendor) ||
153
- (
154
- (process.env.JAMBONES_TTS_TRIM_SILENCE || !process.env.JAMBONES_DISABLE_TTS_STREAMING) &&
155
- ['microsoft', 'azure'].includes(vendor)
156
- ) ||
157
- (
158
- !process.env.JAMBONES_DISABLE_TTS_STREAMING &&
159
- ['elevenlabs', 'deepgram', 'rimelabs'].includes(vendor)
160
- )
161
- ) {
162
- filePath = `${TMP_FOLDER}/${key.replace('tts:', `tts-${salt || ''}`)}.r8`;
163
- }
164
- else filePath = `${TMP_FOLDER}/${key.replace('tts:', `tts-${salt || ''}`)}.mp3`;
163
+ filePath = makeFilePath(vendor, key, salt);
165
164
  debug(`synth key is ${key}`);
166
165
  let cached;
167
166
  if (!disableTtsCache) {
@@ -195,8 +194,7 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
195
194
  case 'microsoft':
196
195
  vendorLabel = 'microsoft';
197
196
  audioBuffer = await synthMicrosoft(logger, {credentials, stats, language, voice, text, deploymentId,
198
- filePath, renderForCaching, disableTtsStreaming});
199
- if (audioBuffer?.filePath) return audioBuffer;
197
+ filePath, renderForCaching, disableTtsStreaming});
200
198
  break;
201
199
  case 'nuance':
202
200
  model = model || 'enhanced';
@@ -214,30 +212,30 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
214
212
  case 'elevenlabs':
215
213
  audioBuffer = await synthElevenlabs(logger, {
216
214
  credentials, options, stats, language, voice, text, renderForCaching, disableTtsStreaming, filePath
217
- });
218
- if (audioBuffer?.filePath) return audioBuffer;
215
+ });
219
216
  break;
220
217
  case 'playht':
221
218
  audioBuffer = await synthPlayHT(logger, {
222
219
  credentials, options, stats, language, voice, text, renderForCaching, disableTtsStreaming, filePath
223
- });
224
- if (audioBuffer?.filePath) return audioBuffer;
220
+ });
225
221
  break;
226
222
  case 'rimelabs':
227
223
  audioBuffer = await synthRimelabs(logger, {
228
224
  credentials, options, stats, language, voice, text, renderForCaching, disableTtsStreaming, filePath
229
- });
230
- if (audioBuffer?.filePath) return audioBuffer;
225
+ });
231
226
  break;
232
227
  case 'whisper':
233
228
  audioBuffer = await synthWhisper(logger, {
229
+ credentials, stats, voice, text, renderForCaching, disableTtsStreaming});
230
+ break;
231
+ case 'verbio':
232
+ audioBuffer = await synthVerbio(client, logger, {
234
233
  credentials, stats, voice, text, renderForCaching, disableTtsStreaming});
235
234
  if (audioBuffer?.filePath) return audioBuffer;
236
235
  break;
237
236
  case 'deepgram':
238
237
  audioBuffer = await synthDeepgram(logger, {credentials, stats, model, text,
239
- renderForCaching, disableTtsStreaming});
240
- if (audioBuffer?.filePath) return audioBuffer;
238
+ renderForCaching, disableTtsStreaming});
241
239
  break;
242
240
  case vendor.startsWith('custom') ? vendor : 'cant_match_value':
243
241
  ({ audioBuffer, filePath } = await synthCustomVendor(logger,
@@ -246,6 +244,7 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
246
244
  default:
247
245
  assert(`synthAudio: unsupported speech vendor ${vendor}`);
248
246
  }
247
+ if('filePath' in audioBuffer) return audioBuffer;
249
248
  const diff = process.hrtime(startAt);
250
249
  const time = diff[0] * 1e3 + diff[1] * 1e-6;
251
250
  rtt = time.toFixed(0);
@@ -380,7 +379,7 @@ async function _synthOnPremMicrosoft(logger, {
380
379
  text,
381
380
  filePath
382
381
  }) {
383
- const {use_custom_tts, custom_tts_endpoint_url} = credentials;
382
+ const {use_custom_tts, custom_tts_endpoint_url, api_key} = credentials;
384
383
  let content = text;
385
384
 
386
385
  if (use_custom_tts && !content.startsWith('<speak')) {
@@ -404,7 +403,8 @@ async function _synthOnPremMicrosoft(logger, {
404
403
  const post = bent('POST', 'buffer', {
405
404
  'X-Microsoft-OutputFormat': trimSilence ? 'raw-8khz-16bit-mono-pcm' : 'audio-16khz-32kbitrate-mono-mp3',
406
405
  'Content-Type': 'application/ssml+xml',
407
- 'User-Agent': 'Jambonz'
406
+ 'User-Agent': 'Jambonz',
407
+ ...(api_key && {'Ocp-Apim-Subscription-Key': api_key})
408
408
  });
409
409
  const mp3 = await post(custom_tts_endpoint_url, content);
410
410
  return mp3;
@@ -443,7 +443,7 @@ const synthMicrosoft = async(logger, {
443
443
  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>`;
444
444
  logger.info({content}, 'synthMicrosoft');
445
445
  }
446
- if (!process.env.JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
446
+ if (!JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
447
447
  let params = '';
448
448
  params += `{api_key=${apiKey}`;
449
449
  params += `,language=${language}`;
@@ -452,8 +452,9 @@ const synthMicrosoft = async(logger, {
452
452
  params += ',write_cache_file=1';
453
453
  if (region) params += `,region=${region}`;
454
454
  if (custom_tts_endpoint) params += `,endpointId=${custom_tts_endpoint}`;
455
- if (process.env.JAMBONES_HTTP_PROXY_IP) params += `,http_proxy_ip=${process.env.JAMBONES_HTTP_PROXY_IP}`;
456
- if (process.env.JAMBONES_HTTP_PROXY_PORT) params += `,http_proxy_port=${process.env.JAMBONES_HTTP_PROXY_PORT}`;
455
+ if (custom_tts_endpoint_url) params += `,endpoint=${custom_tts_endpoint_url}`;
456
+ if (JAMBONES_HTTP_PROXY_IP) params += `,http_proxy_ip=${JAMBONES_HTTP_PROXY_IP}`;
457
+ if (JAMBONES_HTTP_PROXY_PORT) params += `,http_proxy_port=${JAMBONES_HTTP_PROXY_PORT}`;
457
458
  params += '}';
458
459
  return {
459
460
  filePath: `say:${params}${content.replace(/\n/g, ' ')}`,
@@ -482,10 +483,10 @@ const synthMicrosoft = async(logger, {
482
483
  SpeechSynthesisOutputFormat.Raw8Khz16BitMonoPcm :
483
484
  SpeechSynthesisOutputFormat.Audio16Khz32KBitRateMonoMp3;
484
485
 
485
- if (process.env.JAMBONES_HTTP_PROXY_IP && process.env.JAMBONES_HTTP_PROXY_PORT) {
486
+ if (JAMBONES_HTTP_PROXY_IP && JAMBONES_HTTP_PROXY_PORT) {
486
487
  logger.debug(
487
- `synthMicrosoft: using proxy ${process.env.JAMBONES_HTTP_PROXY_IP}:${process.env.JAMBONES_HTTP_PROXY_PORT}`);
488
- speechConfig.setProxy(process.env.JAMBONES_HTTP_PROXY_IP, process.env.JAMBONES_HTTP_PROXY_PORT);
488
+ `synthMicrosoft: using proxy ${JAMBONES_HTTP_PROXY_IP}:${JAMBONES_HTTP_PROXY_PORT}`);
489
+ speechConfig.setProxy(JAMBONES_HTTP_PROXY_IP, JAMBONES_HTTP_PROXY_PORT);
489
490
  }
490
491
  const synthesizer = new SpeechSynthesizer(speechConfig);
491
492
 
@@ -671,7 +672,7 @@ const synthElevenlabs = async(logger, {
671
672
  const opts = !!options && Object.keys(options).length !== 0 ? options : JSON.parse(credOpts || '{}');
672
673
 
673
674
  /* default to using the streaming interface, unless disabled by env var OR we want just a cache file */
674
- if (!process.env.JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
675
+ if (!JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
675
676
  let params = '';
676
677
  params += `{api_key=${api_key}`;
677
678
  params += ',vendor=elevenlabs';
@@ -724,7 +725,7 @@ const synthPlayHT = async(logger, {
724
725
  const opts = !!options && Object.keys(options).length !== 0 ? options : JSON.parse(credOpts || '{}');
725
726
 
726
727
  /* default to using the streaming interface, unless disabled by env var OR we want just a cache file */
727
- if (!process.env.JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
728
+ if (!JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
728
729
  let params = '';
729
730
  params += `{api_key=${api_key}`;
730
731
  params += `,user_id=${user_id}`;
@@ -779,7 +780,7 @@ const synthRimelabs = async(logger, {
779
780
  const opts = !!options && Object.keys(options).length !== 0 ? options : JSON.parse(credOpts || '{}');
780
781
 
781
782
  /* default to using the streaming interface, unless disabled by env var OR we want just a cache file */
782
- if (!process.env.JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
783
+ if (!JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
783
784
  let params = '';
784
785
  params += `{api_key=${api_key}`;
785
786
  params += `,model_id=${model_id}`;
@@ -817,11 +818,51 @@ const synthRimelabs = async(logger, {
817
818
  throw err;
818
819
  }
819
820
  };
821
+ const synthVerbio = async(client, logger, {credentials, stats, voice, text, renderForCaching, disableTtsStreaming}) => {
822
+ //https://doc.speechcenter.verbio.com/#tag/Text-To-Speech-REST-API
823
+ if (text.length > 2000) {
824
+ throw new Error('Verbio cannot synthesize for the text length larger than 2000 characters');
825
+ }
826
+ const token = await getVerbioAccessToken(client, logger, credentials);
827
+ if (!process.env.JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
828
+ let params = '';
829
+ params += `{access_token=${token.access_token}`;
830
+ params += ',vendor=verbio';
831
+ params += `,voice=${voice}`;
832
+ params += ',write_cache_file=1';
833
+ params += '}';
834
+
835
+ return {
836
+ filePath: `say:${params}${text.replace(/\n/g, ' ')}`,
837
+ servedFromCache: false,
838
+ rtt: 0
839
+ };
840
+ }
841
+
842
+ try {
843
+ const post = bent('https://us.rest.speechcenter.verbio.com', 'POST', 'buffer', {
844
+ 'Authorization': `Bearer ${token.access_token}`,
845
+ 'User-Agent': 'jambonz',
846
+ 'Content-Type': 'application/json'
847
+ });
848
+ const r8 = await post('/api/v1/synthesize', {
849
+ voice_id: voice,
850
+ output_sample_rate: '8k',
851
+ output_encoding: 'pcm16',
852
+ text
853
+ });
854
+ return r8;
855
+ } catch (err) {
856
+ logger.info({err}, 'synth Verbio returned error');
857
+ stats.increment('tts.count', ['vendor:verbio', 'accepted:no']);
858
+ throw err;
859
+ }
860
+ };
820
861
 
821
862
  const synthWhisper = async(logger, {credentials, stats, voice, text, renderForCaching, disableTtsStreaming}) => {
822
863
  const {api_key, model_id, baseURL, timeout, speed} = credentials;
823
864
  /* if the env is set to stream then bag out, unless we are specifically rendering to generate a cache file */
824
- if (!process.env.JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
865
+ if (!JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
825
866
  let params = '';
826
867
  params += `{api_key=${api_key}`;
827
868
  params += `,model_id=${model_id}`;
@@ -860,7 +901,7 @@ const synthWhisper = async(logger, {credentials, stats, voice, text, renderForCa
860
901
 
861
902
  const synthDeepgram = async(logger, {credentials, stats, model, text, renderForCaching, disableTtsStreaming}) => {
862
903
  const {api_key} = credentials;
863
- if (!process.env.JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
904
+ if (!JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
864
905
  let params = '';
865
906
  params += `{api_key=${api_key}`;
866
907
  params += ',vendor=deepgram';
package/lib/utils.js CHANGED
@@ -3,10 +3,10 @@ const {SynthesizerClient} = require('../stubs/nuance/synthesizer_grpc_pb');
3
3
  const {RivaSpeechSynthesisClient} = require('../stubs/riva/proto/riva_tts_grpc_pb');
4
4
  const {Pool} = require('undici');
5
5
  const pool = new Pool('https://auth.crt.nuance.com');
6
- const HTTP_TIMEOUT = 5000;
7
6
  const NUANCE_AUTH_ENDPOINT = 'tts.api.nuance.com:443';
8
7
  const grpc = require('@grpc/grpc-js');
9
8
  const formurlencoded = require('form-urlencoded');
9
+ const { JAMBONES_DISABLE_TTS_STREAMING, JAMBONES_TTS_TRIM_SILENCE, TMP_FOLDER, HTTP_TIMEOUT } = require('./config');
10
10
 
11
11
  const debug = require('debug')('jambonz:realtimedb-helpers');
12
12
  /**
@@ -19,7 +19,44 @@ const debug = require('debug')('jambonz:realtimedb-helpers');
19
19
  function makeSynthKey({account_sid = '', vendor, language, voice, engine = '', text}) {
20
20
  const hash = crypto.createHash('sha1');
21
21
  hash.update(`${language}:${vendor}:${voice}:${engine}:${text}`);
22
- return `tts${account_sid ? (':' + account_sid) : ''}:${hash.digest('hex')}`;
22
+ const hexHashKey = hash.digest('hex');
23
+ const accountKey = account_sid ? `:${account_sid}` : '';
24
+ const namespace = vendor.startsWith('custom') ? vendor : getFileExtension(vendor);
25
+ const key = `tts${accountKey}:${namespace}:${hexHashKey}`;
26
+ return key;
27
+ }
28
+
29
+ function makeFilePath(vendor, key, salt = '') {
30
+ const extension = getFileExtension(vendor);
31
+ return `${TMP_FOLDER}/${key.replace('tts:', `tts-${salt}`)}.${extension}`;
32
+ }
33
+
34
+ function getFileExtension(vendor) {
35
+ const mp3Extension = 'mp3';
36
+ const r8Extension = 'r8';
37
+
38
+ switch (vendor) {
39
+ case 'azure':
40
+ case 'microsoft':
41
+ if (!JAMBONES_DISABLE_TTS_STREAMING || JAMBONES_TTS_TRIM_SILENCE) {
42
+ return r8Extension;
43
+ } else {
44
+ return mp3Extension;
45
+ }
46
+ case 'deepgram':
47
+ case 'elevenlabs':
48
+ case 'rimlabs':
49
+ if (!JAMBONES_DISABLE_TTS_STREAMING) {
50
+ return r8Extension;
51
+ } else {
52
+ return mp3Extension;
53
+ }
54
+ case 'nuance':
55
+ case 'nvidia':
56
+ return r8Extension;
57
+ default:
58
+ return mp3Extension;
59
+ }
23
60
  }
24
61
 
25
62
  const noopLogger = {
@@ -49,6 +86,12 @@ function makeAwsKey(awsAccessKeyId) {
49
86
  return `aws:${hash.digest('hex')}`;
50
87
  }
51
88
 
89
+ function makeVerbioKey(client_id) {
90
+ const hash = crypto.createHash('sha1');
91
+ hash.update(client_id);
92
+ return `verbio:${hash.digest('hex')}`;
93
+ }
94
+
52
95
  function makeNuanceKey(clientId, secret, scope) {
53
96
  const hash = crypto.createHash('sha1');
54
97
  hash.update(`${clientId}:${secret}:${scope}`);
@@ -117,11 +160,13 @@ module.exports = {
117
160
  makeNuanceKey,
118
161
  makeIbmKey,
119
162
  makeAwsKey,
163
+ makeVerbioKey,
120
164
  getNuanceAccessToken,
121
165
  createNuanceClient,
122
166
  createKryptonClient,
123
167
  createRivaClient,
124
168
  makeBasicAuthHeader,
125
169
  NUANCE_AUTH_ENDPOINT,
126
- noopLogger
170
+ noopLogger,
171
+ makeFilePath
127
172
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jambonz/speech-utils",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "TTS-related speech utilities for jambonz",
5
5
  "main": "index.js",
6
6
  "author": "Dave Horton",
@@ -12,6 +12,7 @@
12
12
  "test": "NODE_ENV=test node test/ ",
13
13
  "coverage": "nyc --reporter html --report-dir ./coverage npm run test",
14
14
  "jslint": "eslint index.js lib",
15
+ "jslint:fix": "eslint --fix '**/*.js'",
15
16
  "build": "./build_stubs.sh"
16
17
  },
17
18
  "repository": {
@@ -62,9 +62,9 @@ function deserialize_nuance_tts_v1_UnarySynthesisResponse(buffer_arg) {
62
62
 
63
63
  //
64
64
  // The Synthesizer service offers these functionalities:
65
- // - GetVoices: Queries the list of available voices, with filters to reduce the search space.
66
- // - Synthesize: Synthesizes audio from input text and parameters, and returns an audio stream.
67
- // - UnarySynthesize: Synthesizes audio from input text and parameters, and returns a single audio response.
65
+ // - GetVoices: Queries the list of available voices, with filters to reduce the search space.
66
+ // - Synthesize: Synthesizes audio from input text and parameters, and returns an audio stream.
67
+ // - UnarySynthesize: Synthesizes audio from input text and parameters, and returns a single audio response.
68
68
  var SynthesizerService = exports.SynthesizerService = {
69
69
  getVoices: {
70
70
  path: '/nuance.tts.v1.Synthesizer/GetVoices',
@@ -1 +1 @@
1
- // GENERATED CODE -- NO SERVICES IN PROTO
1
+ // GENERATED CODE -- NO SERVICES IN PROTO
@@ -57,7 +57,7 @@ function deserialize_nvidia_riva_tts_SynthesizeSpeechResponse(buffer_arg) {
57
57
  var RivaSpeechSynthesisService = exports.RivaSpeechSynthesisService = {
58
58
  // Used to request text-to-speech from the service. Submit a request containing the
59
59
  // desired text and configuration, and receive audio bytes in the requested format.
60
- synthesize: {
60
+ synthesize: {
61
61
  path: '/nvidia.riva.tts.RivaSpeechSynthesis/Synthesize',
62
62
  requestStream: false,
63
63
  responseStream: false,
@@ -69,9 +69,9 @@ synthesize: {
69
69
  responseDeserialize: deserialize_nvidia_riva_tts_SynthesizeSpeechResponse,
70
70
  },
71
71
  // Used to request text-to-speech returned via stream as it becomes available.
72
- // Submit a SynthesizeSpeechRequest with desired text and configuration,
73
- // and receive stream of bytes in the requested format.
74
- synthesizeOnline: {
72
+ // Submit a SynthesizeSpeechRequest with desired text and configuration,
73
+ // and receive stream of bytes in the requested format.
74
+ synthesizeOnline: {
75
75
  path: '/nvidia.riva.tts.RivaSpeechSynthesis/SynthesizeOnline',
76
76
  requestStream: false,
77
77
  responseStream: true,
@@ -83,7 +83,7 @@ synthesizeOnline: {
83
83
  responseDeserialize: deserialize_nvidia_riva_tts_SynthesizeSpeechResponse,
84
84
  },
85
85
  // Enables clients to request the configuration of the current Synthesize service, or a specific model within the service.
86
- getRivaSynthesisConfig: {
86
+ getRivaSynthesisConfig: {
87
87
  path: '/nvidia.riva.tts.RivaSpeechSynthesis/GetRivaSynthesisConfig',
88
88
  requestStream: false,
89
89
  responseStream: false,
@@ -12,6 +12,32 @@ const stats = {
12
12
  histogram: () => {}
13
13
  };
14
14
 
15
+ test('Verbio - get Access key and voices', async(t) => {
16
+ const fn = require('..');
17
+ const {client, getTtsVoices, getVerbioAccessToken} = fn(opts, logger);
18
+ if (!process.env.VERBIO_CLIENT_ID || !process.env.VERBIO_CLIENT_SECRET) {
19
+ t.pass('skipping Verbio test since no Verbio Keys provided');
20
+ t.end();
21
+ client.quit();
22
+ return;
23
+ }
24
+
25
+ try {
26
+ const credentials = {
27
+ client_id: process.env.VERBIO_CLIENT_ID,
28
+ client_secret: process.env.VERBIO_CLIENT_SECRET
29
+ };
30
+ let obj = await getVerbioAccessToken(credentials);
31
+ t.ok(obj.access_token , 'successfully received access token not from cache');
32
+ const voices = await getTtsVoices({vendor: 'verbio', credentials});
33
+ t.ok(voices && voices.length != 0, 'successfully received verbio voices');
34
+ } catch (err) {
35
+ console.error(err);
36
+ t.end(err);
37
+ }
38
+ client.quit();
39
+ });
40
+
15
41
  test('IBM - create access key', async(t) => {
16
42
  const fn = require('..');
17
43
  const {client, getIbmAccessToken} = fn(opts, logger);
package/test/synth.js CHANGED
@@ -670,6 +670,40 @@ test('whisper speech synth tests', async(t) => {
670
670
  language: 'en-US',
671
671
  voice: 'alloy',
672
672
  text,
673
+ renderForCaching: true
674
+ });
675
+ t.ok(!opts.servedFromCache, `successfully synthesized whisper audio to ${opts.filePath}`);
676
+
677
+ } catch (err) {
678
+ console.error(JSON.stringify(err));
679
+ t.end(err);
680
+ }
681
+ client.quit();
682
+ });
683
+
684
+ test('Verbio speech synth tests', async(t) => {
685
+ const fn = require('..');
686
+ const {synthAudio, client} = fn(opts, logger);
687
+
688
+ if (!process.env.VERBIO_CLIENT_ID || !process.env.VERBIO_CLIENT_SECRET) {
689
+ t.pass('skipping Verbio Synthesize test since no Verbio Keys provided');
690
+ t.end();
691
+ client.quit();
692
+ return;
693
+ }
694
+
695
+ const text = 'Hi there and welcome to jambones!';
696
+ try {
697
+ let opts = await synthAudio(stats, {
698
+ vendor: 'verbio',
699
+ credentials: {
700
+ client_id: process.env.VERBIO_CLIENT_ID,
701
+ client_secret: process.env.VERBIO_CLIENT_SECRET
702
+ },
703
+ language: 'en-US',
704
+ voice: 'tommy_en-us',
705
+ text,
706
+ renderForCaching: true
673
707
  });
674
708
  t.ok(!opts.servedFromCache, `successfully synthesized whisper audio to ${opts.filePath}`);
675
709
 
@@ -755,8 +789,19 @@ test('TTS Cache tests', async(t) => {
755
789
  t.ok(error, `error returned when specified key was not found`);
756
790
 
757
791
  // make sure other tts keys are still there
758
- const cached = (await client.keys('tts:*')).length;
759
- t.ok(cached >= 1, `successfully kept all non-specified tts records in cache`);
792
+ const cached = await client.keys('tts:*')
793
+ t.ok(cached.length >= 1, `successfully kept all non-specified tts records in cache`);
794
+
795
+ // retrieve keys from cache and check the key contains the file extension
796
+ let key = cached[0];
797
+ t.ok(key.includes('mp3'), `tts cache extension shoult be part of the key and equal mp3`);
798
+
799
+ process.env.VG_TRIM_TTS_SILENCE = 'true';
800
+ await client.set(makeSynthKey({ vendor: 'azure' }), 'value');
801
+
802
+ const r8Keys = await client.keys('tts:r8*');
803
+ key = r8Keys[0];
804
+ t.ok(key.includes('r8'), `tts cache extension shoult be part of the key and equal r8`);
760
805
 
761
806
  } catch (err) {
762
807
  console.error(JSON.stringify(err));