@jambonz/speech-utils 0.1.0 → 0.1.1

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),
@@ -0,0 +1,3 @@
1
+ module.exports = {
2
+ HTTP_TIMEOUT: 5000
3
+ };
@@ -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('./constants');
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('./constants');
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('./constants');
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('./constants');
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;
@@ -22,6 +22,7 @@ const {
22
22
  noopLogger
23
23
  } = require('./utils');
24
24
  const getNuanceAccessToken = require('./get-nuance-access-token');
25
+ const getVerbioAccessToken = require('./get-verbio-token');
25
26
  const {
26
27
  SynthesisRequest,
27
28
  Voice,
@@ -86,7 +87,7 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
86
87
  logger = logger || noopLogger;
87
88
 
88
89
  assert.ok(['google', 'aws', 'polly', 'microsoft', 'wellsaid', 'nuance', 'nvidia', 'ibm', 'elevenlabs',
89
- 'whisper', 'deepgram', 'playht', 'rimelabs'].includes(vendor) ||
90
+ 'whisper', 'deepgram', 'playht', 'rimelabs', 'verbio'].includes(vendor) ||
90
91
  vendor.startsWith('custom'),
91
92
  `synthAudio supported vendors are google, aws, microsoft, nuance, nvidia and wellsaid ..etc, not ${vendor}`);
92
93
  if ('google' === vendor) {
@@ -139,6 +140,10 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
139
140
  assert.ok(credentials.api_key, 'synthAudio requires api_key when whisper is used');
140
141
  } else if (vendor.startsWith('custom')) {
141
142
  assert.ok(credentials.custom_tts_url, `synthAudio requires custom_tts_url in credentials when ${vendor} is used`);
143
+ } else if ('verbio' === vendor) {
144
+ assert.ok(voice, 'synthAudio requires voice when verbio is used');
145
+ assert.ok(credentials.client_id, 'synthAudio requires client_id when verbio is used');
146
+ assert.ok(credentials.client_secret, 'synthAudio requires client_secret when verbio is used');
142
147
  }
143
148
  const key = makeSynthKey({
144
149
  account_sid,
@@ -149,7 +154,7 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
149
154
  text
150
155
  });
151
156
  let filePath;
152
- if (['nuance', 'nvidia'].includes(vendor) ||
157
+ if (['nuance', 'nvidia', 'verbio'].includes(vendor) ||
153
158
  (
154
159
  (process.env.JAMBONES_TTS_TRIM_SILENCE || !process.env.JAMBONES_DISABLE_TTS_STREAMING) &&
155
160
  ['microsoft', 'azure'].includes(vendor)
@@ -234,6 +239,11 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
234
239
  credentials, stats, voice, text, renderForCaching, disableTtsStreaming});
235
240
  if (audioBuffer?.filePath) return audioBuffer;
236
241
  break;
242
+ case 'verbio':
243
+ audioBuffer = await synthVerbio(client, logger, {
244
+ credentials, stats, voice, text, renderForCaching, disableTtsStreaming});
245
+ if (audioBuffer?.filePath) return audioBuffer;
246
+ break;
237
247
  case 'deepgram':
238
248
  audioBuffer = await synthDeepgram(logger, {credentials, stats, model, text,
239
249
  renderForCaching, disableTtsStreaming});
@@ -380,7 +390,7 @@ async function _synthOnPremMicrosoft(logger, {
380
390
  text,
381
391
  filePath
382
392
  }) {
383
- const {use_custom_tts, custom_tts_endpoint_url} = credentials;
393
+ const {use_custom_tts, custom_tts_endpoint_url, api_key} = credentials;
384
394
  let content = text;
385
395
 
386
396
  if (use_custom_tts && !content.startsWith('<speak')) {
@@ -404,7 +414,8 @@ async function _synthOnPremMicrosoft(logger, {
404
414
  const post = bent('POST', 'buffer', {
405
415
  'X-Microsoft-OutputFormat': trimSilence ? 'raw-8khz-16bit-mono-pcm' : 'audio-16khz-32kbitrate-mono-mp3',
406
416
  'Content-Type': 'application/ssml+xml',
407
- 'User-Agent': 'Jambonz'
417
+ 'User-Agent': 'Jambonz',
418
+ ...(api_key && {'Ocp-Apim-Subscription-Key': api_key})
408
419
  });
409
420
  const mp3 = await post(custom_tts_endpoint_url, content);
410
421
  return mp3;
@@ -452,6 +463,7 @@ const synthMicrosoft = async(logger, {
452
463
  params += ',write_cache_file=1';
453
464
  if (region) params += `,region=${region}`;
454
465
  if (custom_tts_endpoint) params += `,endpointId=${custom_tts_endpoint}`;
466
+ if (custom_tts_endpoint_url) params += `,endpoint=${custom_tts_endpoint_url}`;
455
467
  if (process.env.JAMBONES_HTTP_PROXY_IP) params += `,http_proxy_ip=${process.env.JAMBONES_HTTP_PROXY_IP}`;
456
468
  if (process.env.JAMBONES_HTTP_PROXY_PORT) params += `,http_proxy_port=${process.env.JAMBONES_HTTP_PROXY_PORT}`;
457
469
  params += '}';
@@ -817,6 +829,46 @@ const synthRimelabs = async(logger, {
817
829
  throw err;
818
830
  }
819
831
  };
832
+ const synthVerbio = async(client, logger, {credentials, stats, voice, text, renderForCaching, disableTtsStreaming}) => {
833
+ //https://doc.speechcenter.verbio.com/#tag/Text-To-Speech-REST-API
834
+ if (text.length > 2000) {
835
+ throw new Error('Verbio cannot synthesize for the text length larger than 2000 characters');
836
+ }
837
+ const token = await getVerbioAccessToken(client, logger, credentials);
838
+ if (!process.env.JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
839
+ let params = '';
840
+ params += `{access_token=${token.access_token}`;
841
+ params += ',vendor=verbio';
842
+ params += `,voice=${voice}`;
843
+ params += ',write_cache_file=1';
844
+ params += '}';
845
+
846
+ return {
847
+ filePath: `say:${params}${text.replace(/\n/g, ' ')}`,
848
+ servedFromCache: false,
849
+ rtt: 0
850
+ };
851
+ }
852
+
853
+ try {
854
+ const post = bent('https://us.rest.speechcenter.verbio.com', 'POST', 'buffer', {
855
+ 'Authorization': `Bearer ${token.access_token}`,
856
+ 'User-Agent': 'jambonz',
857
+ 'Content-Type': 'application/json'
858
+ });
859
+ const r8 = await post('/api/v1/synthesize', {
860
+ voice_id: voice,
861
+ output_sample_rate: '8k',
862
+ output_encoding: 'pcm16',
863
+ text
864
+ });
865
+ return r8;
866
+ } catch (err) {
867
+ logger.info({err}, 'synth Verbio returned error');
868
+ stats.increment('tts.count', ['vendor:verbio', 'accepted:no']);
869
+ throw err;
870
+ }
871
+ };
820
872
 
821
873
  const synthWhisper = async(logger, {credentials, stats, voice, text, renderForCaching, disableTtsStreaming}) => {
822
874
  const {api_key, model_id, baseURL, timeout, speed} = credentials;
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 { HTTP_TIMEOUT } = require('./constants');
10
10
 
11
11
  const debug = require('debug')('jambonz:realtimedb-helpers');
12
12
  /**
@@ -49,6 +49,12 @@ function makeAwsKey(awsAccessKeyId) {
49
49
  return `aws:${hash.digest('hex')}`;
50
50
  }
51
51
 
52
+ function makeVerbioKey(client_id) {
53
+ const hash = crypto.createHash('sha1');
54
+ hash.update(client_id);
55
+ return `verbio:${hash.digest('hex')}`;
56
+ }
57
+
52
58
  function makeNuanceKey(clientId, secret, scope) {
53
59
  const hash = crypto.createHash('sha1');
54
60
  hash.update(`${clientId}:${secret}:${scope}`);
@@ -117,6 +123,7 @@ module.exports = {
117
123
  makeNuanceKey,
118
124
  makeIbmKey,
119
125
  makeAwsKey,
126
+ makeVerbioKey,
120
127
  getNuanceAccessToken,
121
128
  createNuanceClient,
122
129
  createKryptonClient,
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.1",
4
4
  "description": "TTS-related speech utilities for jambonz",
5
5
  "main": "index.js",
6
6
  "author": "Dave Horton",
@@ -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