@jambonz/speech-utils 0.2.27 → 1.0.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.
@@ -13,11 +13,6 @@ jobs:
13
13
  with:
14
14
  node-version: '20'
15
15
  - run: npm install
16
- - name: Install Docker Compose
17
- run: |
18
- sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
19
- sudo chmod +x /usr/local/bin/docker-compose
20
- docker-compose --version
21
16
  - run: npm run jslint
22
17
  - run: sudo apt update && sudo apt install -y squid
23
18
  - run: sudo cp test/squid.conf /etc/squid/squid.conf
@@ -28,9 +23,7 @@ jobs:
28
23
  AWS_REGION: ${{ secrets.AWS_REGION }}
29
24
  AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
30
25
  GCP_JSON_KEY: ${{ secrets.GCP_JSON_KEY }}
31
- IBM_API_KEY: ${{ secrets.IBM_API_KEY }}
32
- IBM_TTS_API_KEY: ${{ secrets.IBM_TTS_API_KEY }}
33
- IBM_TTS_REGION: ${{ secrets.IBM_TTS_REGION }}
26
+
34
27
  MICROSOFT_API_KEY: ${{ secrets.MICROSOFT_API_KEY }}
35
28
  MICROSOFT_REGION: ${{ secrets.MICROSOFT_REGION }}
36
29
  OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
package/index.js CHANGED
@@ -16,7 +16,7 @@ module.exports = (opts, logger) => {
16
16
  synthAudio: require('./lib/synth-audio').bind(null, client, createHash, retrieveHash, logger),
17
17
  getVerbioAccessToken: require('./lib/get-verbio-token').bind(null, client, logger),
18
18
  getNuanceAccessToken: require('./lib/get-nuance-access-token').bind(null, client, logger),
19
- getIbmAccessToken: require('./lib/get-ibm-access-token').bind(null, client, logger),
19
+
20
20
  getAwsAuthToken: require('./lib/get-aws-sts-token').bind(null, logger, createHash, retrieveHash),
21
21
  getTtsVoices: require('./lib/get-tts-voices').bind(null, client, createHash, retrieveHash, logger),
22
22
  };
@@ -3,8 +3,6 @@ const {noopLogger, createNuanceClient, createKryptonClient} = require('./utils')
3
3
  const getNuanceAccessToken = require('./get-nuance-access-token');
4
4
  const getVerbioAccessToken = require('./get-verbio-token');
5
5
  const {GetVoicesRequest, Voice} = require('../stubs/nuance/synthesizer_pb');
6
- const TextToSpeechV1 = require('ibm-watson/text-to-speech/v1');
7
- const { IamAuthenticator } = require('ibm-watson/auth');
8
6
  const ttsGoogle = require('@google-cloud/text-to-speech');
9
7
  const { PollyClient, DescribeVoicesCommand } = require('@aws-sdk/client-polly');
10
8
  const getAwsAuthToken = require('./get-aws-sts-token');
@@ -12,21 +10,6 @@ const {Pool} = require('undici');
12
10
  const { HTTP_TIMEOUT } = require('./config');
13
11
  const verbioVoicePool = new Pool('https://us.rest.speechcenter.verbio.com');
14
12
 
15
- const getIbmVoices = async(client, logger, credentials) => {
16
- const {tts_region, tts_api_key} = credentials;
17
- console.log(`region: ${tts_region}, api_key: ${tts_api_key}`);
18
-
19
- const textToSpeech = new TextToSpeechV1({
20
- authenticator: new IamAuthenticator({
21
- apikey: tts_api_key,
22
- }),
23
- serviceUrl: `https://api.${tts_region}.text-to-speech.watson.cloud.ibm.com`
24
- });
25
-
26
- const voices = await textToSpeech.listVoices();
27
- return voices;
28
- };
29
-
30
13
  const getNuanceVoices = async(client, logger, credentials) => {
31
14
  const {client_id: clientId, secret: secret, nuance_tts_uri} = credentials;
32
15
 
@@ -165,14 +148,12 @@ const getVerbioVoices = async(client, logger, credentials) => {
165
148
  async function getTtsVoices(client, createHash, retrieveHash, logger, {vendor, credentials}) {
166
149
  logger = logger || noopLogger;
167
150
 
168
- assert.ok(['nuance', 'ibm', 'google', 'aws', 'polly', 'verbio'].includes(vendor),
151
+ assert.ok(['nuance', 'google', 'aws', 'polly', 'verbio'].includes(vendor),
169
152
  `getTtsVoices not supported for vendor ${vendor}`);
170
153
 
171
154
  switch (vendor) {
172
155
  case 'nuance':
173
156
  return getNuanceVoices(client, logger, credentials);
174
- case 'ibm':
175
- return getIbmVoices(client, logger, credentials);
176
157
  case 'google':
177
158
  return getGoogleVoices(client, logger, credentials);
178
159
  case 'aws':
@@ -6,8 +6,7 @@ const { PollyClient, SynthesizeSpeechCommand } = require('@aws-sdk/client-polly'
6
6
  const { CartesiaClient } = require('@cartesia/cartesia-js');
7
7
 
8
8
  const sdk = require('microsoft-cognitiveservices-speech-sdk');
9
- const TextToSpeechV1 = require('ibm-watson/text-to-speech/v1');
10
- const { IamAuthenticator } = require('ibm-watson/auth');
9
+
11
10
  const {
12
11
  ResultReason,
13
12
  SpeechConfig,
@@ -96,7 +95,7 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
96
95
  let rtt;
97
96
  logger = logger || noopLogger;
98
97
 
99
- assert.ok(['google', 'aws', 'polly', 'microsoft', 'wellsaid', 'nuance', 'nvidia', 'ibm', 'elevenlabs',
98
+ assert.ok(['google', 'aws', 'polly', 'microsoft', 'wellsaid', 'nuance', 'nvidia', 'elevenlabs',
100
99
  'whisper', 'deepgram', 'playht', 'rimelabs', 'verbio', 'cartesia', 'inworld', 'resemble'].includes(vendor) ||
101
100
  vendor.startsWith('custom'),
102
101
  `synthAudio supported vendors are google, aws, microsoft, nuance, nvidia and wellsaid ..etc, not ${vendor}`);
@@ -122,11 +121,6 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
122
121
  assert.ok(language, 'synthAudio requires language when nvidia is used');
123
122
  assert.ok(credentials.riva_server_uri, 'synthAudio requires riva_server_uri in credentials when nvidia is used');
124
123
  }
125
- else if ('ibm' === vendor) {
126
- assert.ok(voice, 'synthAudio requires voice when ibm is used');
127
- assert.ok(credentials.tts_region, 'synthAudio requires tts_region in credentials when ibm watson is used');
128
- assert.ok(credentials.tts_api_key, 'synthAudio requires tts_api_key in credentials when nuance is used');
129
- }
130
124
  else if ('wellsaid' === vendor) {
131
125
  language = 'en-US'; // WellSaid only supports English atm
132
126
  assert.ok(voice, 'synthAudio requires voice when wellsaid is used');
@@ -204,7 +198,10 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
204
198
  const startAt = process.hrtime();
205
199
  switch (vendor) {
206
200
  case 'google':
207
- audioData = await synthGoogle(logger, {credentials, stats, language, voice, gender, key, text});
201
+ audioData = await synthGoogle(logger, {
202
+ credentials, stats, language, voice, gender, key, text, model, options, instructions,
203
+ renderForCaching, disableTtsStreaming, disableTtsCache
204
+ });
208
205
  break;
209
206
  case 'aws':
210
207
  case 'polly':
@@ -227,9 +224,6 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
227
224
  audioData = await synthNvidia(client, logger, {credentials, stats, language, voice, model, key, text,
228
225
  renderForCaching, disableTtsStreaming, disableTtsCache});
229
226
  break;
230
- case 'ibm':
231
- audioData = await synthIbm(logger, {credentials, stats, voice, key, text});
232
- break;
233
227
  case 'wellsaid':
234
228
  audioData = await synthWellSaid(logger, {credentials, stats, language, voice, key, text});
235
229
  break;
@@ -409,111 +403,129 @@ const synthPolly = async(createHash, retrieveHash, logger,
409
403
  }
410
404
  };
411
405
 
412
- const synthGoogle = async(logger, {credentials, stats, language, voice, gender, text}) => {
413
- const client = new ttsGoogle.TextToSpeechClient(credentials);
414
- // If google custom voice cloning is used.
415
- // At this time 31 Oct 2024, google node sdk has not support voice cloning yet.
416
- if (typeof voice === 'object' && voice.voice_cloning_key) {
417
- try {
418
- const accessToken = await client.auth.getAccessToken();
419
- const projectId = await client.getProjectId();
420
406
 
421
- const post = bent('https://texttospeech.googleapis.com', 'POST', 'json', {
422
- 'Authorization': `Bearer ${accessToken}`,
423
- 'x-goog-user-project': projectId,
424
- 'Content-Type': 'application/json; charset=utf-8'
425
- });
407
+ const synthGoogle = async(logger, {
408
+ credentials, stats, language, voice, gender, key, text, model, options, instructions,
409
+ renderForCaching, disableTtsStreaming, disableTtsCache
410
+ }) => {
411
+ const isGemini = !!model;
412
+ const isVoiceCloning = typeof voice === 'object' && voice.voice_cloning_key;
413
+ // HD voices have pattern like en-US-Chirp3-HD-Charon
414
+ const isHDVoice = typeof voice === 'string' && voice.includes('-HD-');
415
+
416
+ // Streaming support for Google TTS (Gemini, HD voices, and standard voices)
417
+ // Voice cloning does not support streaming
418
+ if (!isVoiceCloning && !JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
419
+ // Strip SSML tags for Gemini TTS (it doesn't support SSML)
420
+ let inputText = text;
421
+ if (isGemini && text.startsWith('<speak>')) {
422
+ inputText = text.replace(/<[^>]*>/g, '').trim();
423
+ logger.info('synthGoogle: Gemini TTS does not support SSML, stripped tags from input');
424
+ }
426
425
 
427
- const payload = {
428
- input: {
429
- text
430
- },
431
- voice: {
432
- language_code: language,
433
- voice_clone: {
434
- voice_cloning_key: voice.voice_cloning_key
435
- }
436
- },
437
- audioConfig: {
438
- // Cloning voice at this time still in v1 beta version, and it support LINEAR16 in Wav format, 24.000Hz
439
- audioEncoding: 'LINEAR16',
440
- sample_rate_hertz: 24000
441
- }
442
- };
426
+ let params = '{';
427
+ params += `credentials=${Buffer.from(JSON.stringify(credentials.credentials)).toString('base64')}`;
428
+ params += `,playback_id=${key}`;
429
+ params += ',vendor=google';
430
+ params += `,voice=${voice}`;
431
+ params += `,language_code=${language || 'en-US'}`;
432
+ params += `,write_cache_file=${disableTtsCache ? 0 : 1}`;
433
+ // api_mode: tts (standard), live (HD voices), gemini (Gemini TTS)
434
+ const apiMode = options?.apiMode || (isGemini ? 'gemini' : (isHDVoice ? 'live' : 'tts'));
435
+ params += `,api_mode=${apiMode}`;
436
+ if (model) params += `,model_name=${model}`;
437
+ if (gender) params += `,gender=${gender}`;
438
+ // comma is used to separate parameters in freeswitch tts module
439
+ const prompt = options?.prompt || instructions;
440
+ if (prompt) params += `,prompt=${prompt.replace(/\n/g, ' ').replace(/,/g, ';')}`;
441
+ params += '}';
443
442
 
444
- const wav = await post('/v1beta1/text:synthesize', payload);
445
- return {
446
- audioContent: Buffer.from(wav.audioContent, 'base64'),
447
- extension: 'wav',
448
- sampleRate: 24000
449
- };
450
- } catch (err) {
451
- logger.info({err: await err.text()}, 'synthGoogle returned error');
452
- throw err;
443
+ return {
444
+ filePath: `say:${params}${(isGemini ? inputText : text).replace(/\n/g, ' ')}`,
445
+ servedFromCache: false,
446
+ rtt: 0
447
+ };
448
+ }
449
+
450
+ const client = new ttsGoogle.TextToSpeechClient(credentials);
451
+
452
+ // Build input based on voice type
453
+ let input;
454
+ if (isGemini) {
455
+ // Gemini TTS does not support SSML - strip tags if present
456
+ let inputText = text;
457
+ if (text.startsWith('<speak>')) {
458
+ inputText = text.replace(/<[^>]*>/g, '').trim();
459
+ logger.info('synthGoogle: Gemini TTS does not support SSML, stripped tags from input');
453
460
  }
461
+ // Use instructions as prompt for Gemini TTS style control, options.prompt can override
462
+ const prompt = options?.prompt || instructions;
463
+ input = {
464
+ text: inputText,
465
+ ...(prompt && { prompt })
466
+ };
467
+ } else {
468
+ input = text.startsWith('<speak>') ? { ssml: text } : { text };
454
469
  }
455
470
 
456
- const opts = {
457
- voice: {
458
- ...(typeof voice === 'string' && {name: voice}),
459
- ...(typeof voice === 'object' && {customVoice: voice}),
471
+ // Build voice selection params based on voice type
472
+ let voiceParams;
473
+ if (isGemini) {
474
+ voiceParams = {
475
+ languageCode: language || 'en-US',
476
+ name: voice,
477
+ modelName: model
478
+ };
479
+ } else if (isVoiceCloning) {
480
+ voiceParams = {
481
+ languageCode: language,
482
+ voiceClone: {
483
+ voiceCloningKey: voice.voice_cloning_key
484
+ }
485
+ };
486
+ } else {
487
+ voiceParams = {
488
+ ...(typeof voice === 'string' && { name: voice }),
489
+ ...(typeof voice === 'object' && { customVoice: voice }),
460
490
  languageCode: language,
461
491
  ssmlGender: gender || 'SSML_VOICE_GENDER_UNSPECIFIED'
462
- },
463
- audioConfig: {audioEncoding: 'MP3'}
464
- };
465
- Object.assign(opts, {input: text.startsWith('<speak>') ? {ssml: text} : {text}});
492
+ };
493
+ }
494
+
495
+ // Build audio config based on voice type
496
+ let audioConfig;
497
+ let extension;
498
+ let sampleRate;
499
+ if (isVoiceCloning) {
500
+ audioConfig = { audioEncoding: 'LINEAR16', sampleRateHertz: 24000 };
501
+ extension = 'wav';
502
+ sampleRate = 24000;
503
+ } else {
504
+ audioConfig = { audioEncoding: 'MP3' };
505
+ extension = 'mp3';
506
+ sampleRate = 8000;
507
+ }
508
+
509
+ const opts = { input, voice: voiceParams, audioConfig };
510
+
466
511
  try {
467
- const responses = await client.synthesizeSpeech(opts);
512
+ logger.debug({ opts }, 'synthGoogle: request');
513
+ const [response] = await client.synthesizeSpeech(opts);
468
514
  stats.increment('tts.count', ['vendor:google', 'accepted:yes']);
469
515
  client.close();
470
516
  return {
471
- audioContent: responses[0].audioContent,
472
- extension: 'mp3',
473
- sampleRate: 8000
517
+ audioContent: response.audioContent,
518
+ extension,
519
+ sampleRate
474
520
  };
475
521
  } catch (err) {
476
- console.error(err);
477
- logger.info({err, opts}, 'synthAudio: Error synthesizing speech using google');
522
+ logger.info({ err, opts }, 'synthAudio: Error synthesizing speech using google');
478
523
  stats.increment('tts.count', ['vendor:google', 'accepted:no']);
479
524
  client && client.close();
480
525
  throw err;
481
526
  }
482
527
  };
483
528
 
484
- const synthIbm = async(logger, {credentials, stats, voice, text}) => {
485
- const {tts_api_key, tts_region} = credentials;
486
- const params = {
487
- text,
488
- voice,
489
- accept: 'audio/mp3'
490
- };
491
-
492
- try {
493
- const textToSpeech = new TextToSpeechV1({
494
- authenticator: new IamAuthenticator({
495
- apikey: tts_api_key,
496
- }),
497
- serviceUrl: `https://api.${tts_region}.text-to-speech.watson.cloud.ibm.com`
498
- });
499
-
500
- const r = await textToSpeech.synthesize(params);
501
- const chunks = [];
502
- for await (const chunk of r.result) {
503
- chunks.push(chunk);
504
- }
505
- return {
506
- audioContent: Buffer.concat(chunks),
507
- extension: 'mp3',
508
- sampleRate: 8000
509
- };
510
- } catch (err) {
511
- logger.info({err, params}, 'synthAudio: Error synthesizing speech using ibm');
512
- stats.increment('tts.count', ['vendor:ibm', 'accepted:no']);
513
- throw new Error(err.statusText || err.message);
514
- }
515
- };
516
-
517
529
  async function _synthOnPremMicrosoft(logger, {
518
530
  credentials,
519
531
  language,
@@ -915,7 +927,7 @@ const synthElevenlabs = async(logger, {
915
927
  const optimize_streaming_latency = opts.optimize_streaming_latency ?
916
928
  `?optimize_streaming_latency=${opts.optimize_streaming_latency}` : '';
917
929
  try {
918
- const post = bent(`https://${api_uri}`, 'POST', 'buffer', {
930
+ const post = bent(`https://${api_uri || 'api.elevenlabs.io'}`, 'POST', 'buffer', {
919
931
  'xi-api-key': api_key,
920
932
  'Accept': 'audio/mpeg',
921
933
  'Content-Type': 'application/json'
package/lib/utils.js CHANGED
@@ -54,12 +54,6 @@ function makeBasicAuthHeader(username, password) {
54
54
  return {Authorization: header};
55
55
  }
56
56
 
57
- function makeIbmKey(apiKey) {
58
- const hash = crypto.createHash('sha1');
59
- hash.update(apiKey);
60
- return `ibm:${hash.digest('hex')}`;
61
- }
62
-
63
57
  function makeAwsKey(awsAccessKeyId) {
64
58
  const hash = crypto.createHash('sha1');
65
59
  hash.update(awsAccessKeyId);
@@ -143,7 +137,7 @@ const createRivaClient = async(rivaUri) => {
143
137
  module.exports = {
144
138
  makeSynthKey,
145
139
  makeNuanceKey,
146
- makeIbmKey,
140
+
147
141
  makePlayhtKey,
148
142
  makeAwsKey,
149
143
  makeVerbioKey,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jambonz/speech-utils",
3
- "version": "0.2.27",
3
+ "version": "1.0.1",
4
4
  "description": "TTS-related speech utilities for jambonz",
5
5
  "main": "index.js",
6
6
  "author": "Dave Horton",
@@ -30,20 +30,19 @@
30
30
  "@aws-sdk/client-polly": "^3.496.0",
31
31
  "@aws-sdk/client-sts": "^3.496.0",
32
32
  "@cartesia/cartesia-js": "^2.2.7",
33
- "@google-cloud/text-to-speech": "^5.5.0",
33
+ "@google-cloud/text-to-speech": "^6.4.0",
34
34
  "@grpc/grpc-js": "^1.9.14",
35
35
  "@jambonz/realtimedb-helpers": "^0.8.7",
36
36
  "bent": "^7.3.12",
37
37
  "debug": "^4.3.4",
38
38
  "form-urlencoded": "^6.1.4",
39
39
  "google-protobuf": "^3.21.2",
40
- "ibm-watson": "^11.0.0",
41
40
  "microsoft-cognitiveservices-speech-sdk": "1.38.0",
42
41
  "openai": "^4.98.0",
43
42
  "undici": "^7.5.0"
44
43
  },
45
44
  "devDependencies": {
46
- "config": "^3.3.11",
45
+ "config": "^4.2.0",
47
46
  "eslint": "^9.3.0",
48
47
  "eslint-plugin-promise": "^6.2.0",
49
48
  "husky": "^9.0.11",
@@ -2,7 +2,7 @@ const test = require('tape').test ;
2
2
  const exec = require('child_process').exec ;
3
3
 
4
4
  test('starting docker network..', (t) => {
5
- exec(`docker-compose -f ${__dirname}/docker-compose-testbed.yaml up -d`, (err, stdout, stderr) => {
5
+ exec(`docker compose -f ${__dirname}/docker-compose-testbed.yaml up -d`, (err, stdout, stderr) => {
6
6
  setTimeout(() => {
7
7
  t.end(err);
8
8
  }, 2000);
@@ -3,7 +3,7 @@ const exec = require('child_process').exec ;
3
3
 
4
4
  test('stopping docker network..', (t) => {
5
5
  t.timeoutAfter(10000);
6
- exec(`docker-compose -f ${__dirname}/docker-compose-testbed.yaml down`, (err, stdout, stderr) => {
6
+ exec(`docker compose -f ${__dirname}/docker-compose-testbed.yaml down`, (err, stdout, stderr) => {
7
7
  //console.log(`stderr: ${stderr}`);
8
8
  process.exit(0);
9
9
  });
package/test/index.js CHANGED
@@ -2,6 +2,6 @@ require('./docker_start');
2
2
  require('./synth');
3
3
  require('./list-voices');
4
4
  require('./aws');
5
- require('./ibm');
5
+
6
6
  require('./nuance');
7
7
  require('./docker_stop');
@@ -38,71 +38,6 @@ test('Verbio - get Access key and voices', async(t) => {
38
38
  client.quit();
39
39
  });
40
40
 
41
- test('IBM - create access key', async(t) => {
42
- const fn = require('..');
43
- const {client, getIbmAccessToken} = fn(opts, logger);
44
-
45
- if (!process.env.IBM_API_KEY ) {
46
- t.pass('skipping IBM test since no IBM api_key provided');
47
- t.end();
48
- client.quit();
49
- return;
50
- }
51
- try {
52
- let obj = await getIbmAccessToken(process.env.IBM_API_KEY);
53
- //console.log({obj}, 'received access token from IBM');
54
- t.ok(obj.access_token && !obj.servedFromCache, 'successfull received access token from IBM');
55
-
56
- obj = await getIbmAccessToken(process.env.IBM_API_KEY);
57
- //console.log({obj}, 'received access token from IBM - second request');
58
- t.ok(obj.access_token && obj.servedFromCache, 'successfully received access token from cache');
59
-
60
- await client.flushall();
61
- t.end();
62
- }
63
- catch (err) {
64
- console.error(err);
65
- t.end(err);
66
- }
67
- client.quit();
68
- });
69
-
70
- test('IBM - retrieve tts voices test', async(t) => {
71
- const fn = require('..');
72
- const {client, getTtsVoices} = fn(opts, logger);
73
-
74
- if (!process.env.IBM_TTS_API_KEY || !process.env.IBM_TTS_REGION) {
75
- t.pass('skipping IBM test since no IBM api_key and/or region provided');
76
- t.end();
77
- client.quit();
78
- return;
79
- }
80
- try {
81
- const opts = {
82
- vendor: 'ibm',
83
- credentials: {
84
- tts_api_key: process.env.IBM_TTS_API_KEY,
85
- tts_region: process.env.IBM_TTS_REGION
86
- }
87
- };
88
- const obj = await getTtsVoices(opts);
89
- const {voices} = obj.result;
90
- //console.log(JSON.stringify(voices));
91
- t.ok(voices.length > 0 && voices[0].language,
92
- `GetVoices: successfully retrieved ${voices.length} voices from IBM`);
93
-
94
- await client.flushall();
95
-
96
- t.end();
97
-
98
- }
99
- catch (err) {
100
- console.error(err);
101
- t.end(err);
102
- }
103
- client.quit();
104
- });
105
-
106
41
  test('Nuance hosted tests', async(t) => {
107
42
  const fn = require('..');
108
43
  const {client, getTtsVoices} = fn(opts, logger);
package/test/synth.js CHANGED
@@ -41,6 +41,7 @@ test('Google speech synth tests', async(t) => {
41
41
  gender: 'FEMALE',
42
42
  text: 'This is a test. This is only a test',
43
43
  salt: 'foo.bar',
44
+ renderForCaching: true,
44
45
  });
45
46
  t.ok(!opts.servedFromCache, `successfully synthesized google audio to ${opts.filePath}`);
46
47
 
@@ -55,6 +56,7 @@ test('Google speech synth tests', async(t) => {
55
56
  language: 'en-GB',
56
57
  gender: 'FEMALE',
57
58
  text: 'This is a test. This is only a test',
59
+ renderForCaching: true,
58
60
  });
59
61
  t.ok(opts.servedFromCache, `successfully retrieved cached google audio from ${opts.filePath}`);
60
62
 
@@ -78,6 +80,7 @@ test('Google speech synth tests', async(t) => {
78
80
  language: 'en-GB',
79
81
  gender: 'FEMALE',
80
82
  text: 'This is a test. This is only a test',
83
+ renderForCaching: true,
81
84
  });
82
85
  t.ok(!opts.servedFromCache, `successfully synthesized google audio regardless of current cache to ${opts.filePath}`);
83
86
  } catch (err) {
@@ -114,7 +117,8 @@ GCP_CUSTOM_VOICE_FILE nor GCP_CUSTOM_VOICE_JSON_KEY provided, GCP_CUSTOM_VOICE_M
114
117
  voice: {
115
118
  reportedUsage: 'REALTIME',
116
119
  model: process.env.GCP_CUSTOM_VOICE_MODEL
117
- }
120
+ },
121
+ renderForCaching: true,
118
122
  });
119
123
  t.ok(!opts.servedFromCache, `successfully synthesized google custom voice audio to ${opts.filePath}`);
120
124
  } catch (err) {
@@ -132,7 +136,7 @@ test('Google speech voice cloning synth tests', async(t) => {
132
136
  !process.env.GCP_CUSTOM_VOICE_JSON_KEY ||
133
137
  !process.env.GCP_VOICE_CLONING_FILE &&
134
138
  !process.env.GCP_VOICE_CLONING_JSON_KEY) {
135
- t.pass(`skipping google speech synth tests since neither
139
+ t.pass(`skipping google speech synth tests since neither
136
140
  GCP_CUSTOM_VOICE_FILE nor GCP_CUSTOM_VOICE_JSON_KEY provided,
137
141
  GCP_VOICE_CLONING_FILE nor GCP_VOICE_CLONING_JSON_KEY is not provided`);
138
142
  return t.end();
@@ -156,7 +160,8 @@ GCP_VOICE_CLONING_FILE nor GCP_VOICE_CLONING_JSON_KEY is not provided`);
156
160
  text: 'This is a test. This is only a test. This is a test. This is only a test. This is a test. This is only a test',
157
161
  voice: {
158
162
  voice_cloning_key
159
- }
163
+ },
164
+ renderForCaching: true,
160
165
  });
161
166
  t.ok(!opts.servedFromCache, `successfully synthesized google voice cloning audio to ${opts.filePath}`);
162
167
  } catch (err) {
@@ -166,6 +171,374 @@ GCP_VOICE_CLONING_FILE nor GCP_VOICE_CLONING_JSON_KEY is not provided`);
166
171
  client.quit();
167
172
  });
168
173
 
174
+ test('Google Gemini TTS synth tests', async(t) => {
175
+ const fn = require('..');
176
+ const {synthAudio, client} = fn(opts, logger);
177
+
178
+ if (!process.env.GCP_FILE && !process.env.GCP_JSON_KEY) {
179
+ t.pass('skipping Google Gemini TTS synth tests since neither GCP_FILE nor GCP_JSON_KEY provided');
180
+ return t.end();
181
+ }
182
+ try {
183
+ const str = process.env.GCP_JSON_KEY || fs.readFileSync(process.env.GCP_FILE);
184
+ const creds = JSON.parse(str);
185
+ const geminiModel = process.env.GCP_GEMINI_TTS_MODEL || 'gemini-2.5-flash-tts';
186
+
187
+ // Test Gemini TTS with model and instructions (both required for Gemini)
188
+ let result = await synthAudio(stats, {
189
+ vendor: 'google',
190
+ credentials: {
191
+ credentials: {
192
+ client_email: creds.client_email,
193
+ private_key: creds.private_key,
194
+ },
195
+ },
196
+ language: 'en-US',
197
+ voice: 'Kore',
198
+ model: geminiModel,
199
+ text: 'Hello, this is a test of Google Gemini text to speech.',
200
+ instructions: 'Speak clearly and naturally.',
201
+ renderForCaching: true,
202
+ });
203
+ t.ok(!result.servedFromCache, `successfully synthesized Google Gemini TTS audio to ${result.filePath}`);
204
+ t.ok(result.filePath.endsWith('.mp3'), 'Gemini TTS audio file has correct extension');
205
+
206
+ // Test Gemini TTS with different voice and instructions
207
+ result = await synthAudio(stats, {
208
+ vendor: 'google',
209
+ credentials: {
210
+ credentials: {
211
+ client_email: creds.client_email,
212
+ private_key: creds.private_key,
213
+ },
214
+ },
215
+ language: 'en-US',
216
+ voice: 'Charon',
217
+ model: geminiModel,
218
+ text: 'Welcome to our service. How can I help you today?',
219
+ instructions: 'Speak in a warm, friendly and professional tone.',
220
+ renderForCaching: true,
221
+ });
222
+ t.ok(!result.servedFromCache, `successfully synthesized Gemini TTS with instructions to ${result.filePath}`);
223
+
224
+ // Test cache retrieval
225
+ result = await synthAudio(stats, {
226
+ vendor: 'google',
227
+ credentials: {
228
+ credentials: {
229
+ client_email: creds.client_email,
230
+ private_key: creds.private_key,
231
+ },
232
+ },
233
+ language: 'en-US',
234
+ voice: 'Kore',
235
+ model: geminiModel,
236
+ text: 'Hello, this is a test of Google Gemini text to speech.',
237
+ instructions: 'Speak clearly and naturally.',
238
+ renderForCaching: true,
239
+ });
240
+ t.ok(result.servedFromCache, `successfully retrieved Gemini TTS audio from cache ${result.filePath}`);
241
+
242
+ // Test SSML stripping (Gemini doesn't support SSML)
243
+ result = await synthAudio(stats, {
244
+ vendor: 'google',
245
+ credentials: {
246
+ credentials: {
247
+ client_email: creds.client_email,
248
+ private_key: creds.private_key,
249
+ },
250
+ },
251
+ language: 'en-US',
252
+ voice: 'Leda',
253
+ model: geminiModel,
254
+ text: '<speak>This SSML should be stripped for Gemini TTS.</speak>',
255
+ instructions: 'Speak naturally.',
256
+ disableTtsCache: true,
257
+ renderForCaching: true,
258
+ });
259
+ t.ok(!result.servedFromCache, `successfully synthesized Gemini TTS with SSML stripped to ${result.filePath}`);
260
+
261
+ } catch (err) {
262
+ console.error(err);
263
+ t.end(err);
264
+ }
265
+ client.quit();
266
+ });
267
+
268
+ test('Google TTS streaming tests (!JAMBONES_DISABLE_TTS_STREAMING)', async(t) => {
269
+ // Ensure streaming is enabled (default behavior)
270
+ delete process.env.JAMBONES_DISABLE_TTS_STREAMING;
271
+
272
+ // Clear require cache to reload config with new env var
273
+ delete require.cache[require.resolve('../lib/config')];
274
+ delete require.cache[require.resolve('../lib/synth-audio')];
275
+ delete require.cache[require.resolve('..')];
276
+
277
+ const fn = require('..');
278
+ const {synthAudio, client} = fn(opts, logger);
279
+
280
+ if (!process.env.GCP_FILE && !process.env.GCP_JSON_KEY) {
281
+ t.pass('skipping Google TTS streaming tests since neither GCP_FILE nor GCP_JSON_KEY provided');
282
+ return t.end();
283
+ }
284
+
285
+ try {
286
+ const str = process.env.GCP_JSON_KEY || fs.readFileSync(process.env.GCP_FILE);
287
+ const creds = JSON.parse(str);
288
+ const geminiModel = process.env.GCP_GEMINI_TTS_MODEL || 'gemini-2.5-flash-tts';
289
+
290
+ // Test 1: Standard voice streaming (use_live_api=0)
291
+ let result = await synthAudio(stats, {
292
+ vendor: 'google',
293
+ credentials: {
294
+ credentials: {
295
+ client_email: creds.client_email,
296
+ private_key: creds.private_key,
297
+ },
298
+ },
299
+ language: 'en-US',
300
+ voice: 'en-US-Wavenet-D',
301
+ gender: 'MALE',
302
+ text: 'This is a test of standard voice streaming.',
303
+ disableTtsCache: true
304
+ });
305
+ t.ok(result.filePath.startsWith('say:'), 'Standard voice returns streaming say: path');
306
+ t.ok(result.filePath.includes('vendor=google'), 'Standard voice streaming path contains vendor=google');
307
+ t.ok(result.filePath.includes('api_mode=tts'), 'Standard voice uses api_mode=tts');
308
+ t.ok(result.filePath.includes('voice=en-US-Wavenet-D'), 'Standard voice streaming path contains voice');
309
+ // Verify credentials are base64 encoded (no raw JSON braces that would break FreeSWitch parsing)
310
+ t.ok(result.filePath.includes('credentials='), 'Standard voice streaming path contains credentials');
311
+ t.ok(!result.filePath.includes('credentials={'), 'Credentials are not raw JSON (base64 encoded)');
312
+
313
+ // Test 2: HD voice streaming (api_mode=live)
314
+ result = await synthAudio(stats, {
315
+ vendor: 'google',
316
+ credentials: {
317
+ credentials: {
318
+ client_email: creds.client_email,
319
+ private_key: creds.private_key,
320
+ },
321
+ },
322
+ language: 'en-US',
323
+ voice: 'en-US-Chirp3-HD-Charon',
324
+ text: 'This is a test of HD voice streaming.',
325
+ disableTtsCache: true
326
+ });
327
+ t.ok(result.filePath.startsWith('say:'), 'HD voice returns streaming say: path');
328
+ t.ok(result.filePath.includes('vendor=google'), 'HD voice streaming path contains vendor=google');
329
+ t.ok(result.filePath.includes('api_mode=live'), 'HD voice uses api_mode=live');
330
+ t.ok(result.filePath.includes('voice=en-US-Chirp3-HD-Charon'), 'HD voice streaming path contains voice');
331
+
332
+ // Test 3: Gemini TTS streaming (api_mode=gemini)
333
+ result = await synthAudio(stats, {
334
+ vendor: 'google',
335
+ credentials: {
336
+ credentials: {
337
+ client_email: creds.client_email,
338
+ private_key: creds.private_key,
339
+ },
340
+ },
341
+ language: 'en-US',
342
+ voice: 'Kore',
343
+ model: geminiModel,
344
+ text: 'This is a test of Gemini TTS streaming.',
345
+ instructions: 'Speak naturally.',
346
+ disableTtsCache: true
347
+ });
348
+ t.ok(result.filePath.startsWith('say:'), 'Gemini TTS returns streaming say: path');
349
+ t.ok(result.filePath.includes('vendor=google'), 'Gemini TTS streaming path contains vendor=google');
350
+ t.ok(result.filePath.includes('api_mode=gemini'), 'Gemini TTS uses api_mode=gemini');
351
+ t.ok(result.filePath.includes(`model_name=${geminiModel}`), 'Gemini TTS streaming path contains model_name');
352
+ t.ok(result.filePath.includes('prompt=Speak naturally.'), 'Gemini TTS streaming path contains prompt');
353
+
354
+ // Test 4: Gemini TTS with SSML stripping in streaming mode
355
+ result = await synthAudio(stats, {
356
+ vendor: 'google',
357
+ credentials: {
358
+ credentials: {
359
+ client_email: creds.client_email,
360
+ private_key: creds.private_key,
361
+ },
362
+ },
363
+ language: 'en-US',
364
+ voice: 'Leda',
365
+ model: geminiModel,
366
+ text: '<speak>This SSML should be stripped.</speak>',
367
+ instructions: 'Speak naturally.',
368
+ disableTtsCache: true
369
+ });
370
+ t.ok(result.filePath.startsWith('say:'), 'Gemini TTS with SSML returns streaming say: path');
371
+ t.ok(!result.filePath.includes('<speak>'), 'SSML tags are stripped from streaming path');
372
+ t.ok(result.filePath.includes('This SSML should be stripped.'), 'Text content is preserved after SSML stripping');
373
+
374
+ // Test 5: Gemini TTS with prompt containing special characters
375
+ result = await synthAudio(stats, {
376
+ vendor: 'google',
377
+ credentials: {
378
+ credentials: {
379
+ client_email: creds.client_email,
380
+ private_key: creds.private_key,
381
+ },
382
+ },
383
+ language: 'en-US',
384
+ voice: 'Kore',
385
+ model: geminiModel,
386
+ text: 'Testing special characters in prompt.',
387
+ options: { prompt: 'Speak in a warm, friendly tone' },
388
+ disableTtsCache: true
389
+ });
390
+ t.ok(result.filePath.startsWith('say:'), 'Gemini TTS with special chars returns streaming say: path');
391
+ // Commas in prompt should be replaced with semicolons
392
+ t.ok(result.filePath.includes('prompt=Speak in a warm; friendly tone'), 'Commas in prompt are escaped to semicolons');
393
+
394
+ // Test 6: options.apiMode override (force live on standard voice)
395
+ result = await synthAudio(stats, {
396
+ vendor: 'google',
397
+ credentials: {
398
+ credentials: {
399
+ client_email: creds.client_email,
400
+ private_key: creds.private_key,
401
+ },
402
+ },
403
+ language: 'en-US',
404
+ voice: 'en-US-Wavenet-D',
405
+ text: 'Testing apiMode option override to live.',
406
+ options: { apiMode: 'live' },
407
+ disableTtsCache: true
408
+ });
409
+ t.ok(result.filePath.includes('api_mode=live'), 'options.apiMode=live overrides default for standard voice');
410
+
411
+ // Test 7: options.apiMode override (force gemini without model)
412
+ result = await synthAudio(stats, {
413
+ vendor: 'google',
414
+ credentials: {
415
+ credentials: {
416
+ client_email: creds.client_email,
417
+ private_key: creds.private_key,
418
+ },
419
+ },
420
+ language: 'en-US',
421
+ voice: 'Kore',
422
+ text: 'Testing apiMode option override to gemini.',
423
+ options: { apiMode: 'gemini' },
424
+ disableTtsCache: true
425
+ });
426
+ t.ok(result.filePath.includes('api_mode=gemini'), 'options.apiMode=gemini overrides default');
427
+
428
+ // Test 8: options.apiMode override (force tts on HD voice)
429
+ result = await synthAudio(stats, {
430
+ vendor: 'google',
431
+ credentials: {
432
+ credentials: {
433
+ client_email: creds.client_email,
434
+ private_key: creds.private_key,
435
+ },
436
+ },
437
+ language: 'en-US',
438
+ voice: 'en-US-Chirp3-HD-Charon',
439
+ text: 'Testing apiMode option override to tts on HD voice.',
440
+ options: { apiMode: 'tts' },
441
+ disableTtsCache: true
442
+ });
443
+ t.ok(result.filePath.includes('api_mode=tts'), 'options.apiMode=tts overrides HD voice default');
444
+
445
+ } catch (err) {
446
+ console.error(err);
447
+ t.end(err);
448
+ }
449
+ client.quit();
450
+ });
451
+
452
+ test('Google TTS non-streaming tests (JAMBONES_DISABLE_TTS_STREAMING=true)', async(t) => {
453
+ // Enable streaming disable flag
454
+ process.env.JAMBONES_DISABLE_TTS_STREAMING = 'true';
455
+
456
+ // Clear require cache to reload config with new env var
457
+ delete require.cache[require.resolve('../lib/config')];
458
+ delete require.cache[require.resolve('../lib/synth-audio')];
459
+ delete require.cache[require.resolve('..')];
460
+
461
+ const fn = require('..');
462
+ const {synthAudio, client} = fn(opts, logger);
463
+
464
+ if (!process.env.GCP_FILE && !process.env.GCP_JSON_KEY) {
465
+ t.pass('skipping Google TTS non-streaming tests since neither GCP_FILE nor GCP_JSON_KEY provided');
466
+ delete process.env.JAMBONES_DISABLE_TTS_STREAMING;
467
+ return t.end();
468
+ }
469
+
470
+ try {
471
+ const str = process.env.GCP_JSON_KEY || fs.readFileSync(process.env.GCP_FILE);
472
+ const creds = JSON.parse(str);
473
+ const geminiModel = process.env.GCP_GEMINI_TTS_MODEL || 'gemini-2.5-flash-tts';
474
+
475
+ // Test 1: Standard voice falls back to non-streaming API
476
+ let result = await synthAudio(stats, {
477
+ vendor: 'google',
478
+ credentials: {
479
+ credentials: {
480
+ client_email: creds.client_email,
481
+ private_key: creds.private_key,
482
+ },
483
+ },
484
+ language: 'en-US',
485
+ voice: 'en-US-Wavenet-D',
486
+ gender: 'MALE',
487
+ text: 'This is a test with streaming disabled.',
488
+ disableTtsCache: true
489
+ });
490
+ t.ok(!result.filePath.startsWith('say:'), 'Standard voice does NOT return streaming say: path when disabled');
491
+ t.ok(result.filePath.endsWith('.mp3'), 'Standard voice returns mp3 file path');
492
+
493
+ // Test 2: HD voice falls back to non-streaming API
494
+ result = await synthAudio(stats, {
495
+ vendor: 'google',
496
+ credentials: {
497
+ credentials: {
498
+ client_email: creds.client_email,
499
+ private_key: creds.private_key,
500
+ },
501
+ },
502
+ language: 'en-US',
503
+ voice: 'en-US-Chirp3-HD-Charon',
504
+ text: 'This is a test of HD voice with streaming disabled.',
505
+ disableTtsCache: true
506
+ });
507
+ t.ok(!result.filePath.startsWith('say:'), 'HD voice does NOT return streaming say: path when disabled');
508
+ t.ok(result.filePath.endsWith('.mp3'), 'HD voice returns mp3 file path');
509
+
510
+ // Test 3: Gemini TTS falls back to non-streaming API
511
+ result = await synthAudio(stats, {
512
+ vendor: 'google',
513
+ credentials: {
514
+ credentials: {
515
+ client_email: creds.client_email,
516
+ private_key: creds.private_key,
517
+ },
518
+ },
519
+ language: 'en-US',
520
+ voice: 'Kore',
521
+ model: geminiModel,
522
+ text: 'This is a test of Gemini TTS with streaming disabled.',
523
+ instructions: 'Speak naturally.',
524
+ disableTtsCache: true
525
+ });
526
+ t.ok(!result.filePath.startsWith('say:'), 'Gemini TTS does NOT return streaming say: path when disabled');
527
+ t.ok(result.filePath.endsWith('.mp3'), 'Gemini TTS returns mp3 file path');
528
+
529
+ } catch (err) {
530
+ console.error(err);
531
+ t.end(err);
532
+ } finally {
533
+ // Clean up: restore default behavior
534
+ delete process.env.JAMBONES_DISABLE_TTS_STREAMING;
535
+ delete require.cache[require.resolve('../lib/config')];
536
+ delete require.cache[require.resolve('../lib/synth-audio')];
537
+ delete require.cache[require.resolve('..')];
538
+ }
539
+ client.quit();
540
+ });
541
+
169
542
  test('AWS speech synth tests', async(t) => {
170
543
  const fn = require('..');
171
544
  const {synthAudio, client} = fn(opts, logger);
@@ -499,46 +872,6 @@ test('Nvidia speech synth tests', async(t) => {
499
872
  client.quit();
500
873
  });
501
874
 
502
- test('IBM watson speech synth tests', async(t) => {
503
- const fn = require('..');
504
- const {synthAudio, client} = fn(opts, logger);
505
-
506
- if (!process.env.IBM_TTS_API_KEY || !process.env.IBM_TTS_REGION) {
507
- t.pass('skipping IBM Watson speech synth tests since IBM_TTS_API_KEY or IBM_TTS_API_KEY not provided');
508
- return t.end();
509
- }
510
- const text = `<speak> Hi there and welcome to jambones! jambones is the <sub alias="seapass">CPaaS</sub> designed with the needs of communication service providers in mind. This is an example of simple text-to-speech, but there is so much more you can do. Try us out!</speak>`;
511
- try {
512
- let opts = await synthAudio(stats, {
513
- vendor: 'ibm',
514
- credentials: {
515
- tts_api_key: process.env.IBM_TTS_API_KEY,
516
- tts_region: process.env.IBM_TTS_REGION,
517
- },
518
- language: 'en-US',
519
- voice: 'en-US_AllisonV2Voice',
520
- text,
521
- });
522
- t.ok(!opts.servedFromCache, `successfully synthesized ibm audio to ${opts.filePath}`);
523
-
524
- opts = await synthAudio(stats, {
525
- vendor: 'ibm',
526
- credentials: {
527
- tts_api_key: process.env.IBM_TTS_API_KEY,
528
- tts_region: process.env.IBM_TTS_REGION,
529
- },
530
- language: 'en-US',
531
- voice: 'en-US_AllisonV2Voice',
532
- text,
533
- });
534
- t.ok(opts.servedFromCache, `successfully retrieved ibm audio from cache ${opts.filePath}`);
535
- } catch (err) {
536
- console.error(JSON.stringify(err));
537
- t.end(err);
538
- }
539
- client.quit();
540
- });
541
-
542
875
  test('Custom Vendor speech synth tests', async(t) => {
543
876
  const fn = require('..');
544
877
  const {synthAudio, client} = fn(opts, logger);
@@ -1,48 +0,0 @@
1
- const formurlencoded = require('form-urlencoded');
2
- const {Pool} = require('undici');
3
- const pool = new Pool('https://iam.cloud.ibm.com');
4
- const {makeIbmKey, noopLogger} = require('./utils');
5
- const { HTTP_TIMEOUT } = require('./config');
6
- const debug = require('debug')('jambonz:realtimedb-helpers');
7
-
8
- async function getIbmAccessToken(client, logger, apiKey) {
9
- logger = logger || noopLogger;
10
- try {
11
- const key = makeIbmKey(apiKey);
12
- const access_token = await client.get(key);
13
- if (access_token) return {access_token, servedFromCache: true};
14
-
15
- /* access token not found in cache, so fetch it from Ibm */
16
- const payload = {
17
- grant_type: 'urn:ibm:params:oauth:grant-type:apikey',
18
- apikey: apiKey
19
- };
20
- const {statusCode, headers, body} = await pool.request({
21
- path: '/identity/token',
22
- method: 'POST',
23
- headers: {
24
- 'Content-Type': 'application/x-www-form-urlencoded'
25
- },
26
- body: formurlencoded(payload),
27
- timeout: HTTP_TIMEOUT,
28
- followRedirects: false
29
- });
30
-
31
- if (200 !== statusCode) {
32
- const json = await body.json();
33
- logger.debug({statusCode, headers, body: json}, 'error fetching access token from Ibm');
34
- const err = new Error();
35
- err.statusCode = statusCode;
36
- throw err;
37
- }
38
- const json = await body.json();
39
- await client.set(key, json.access_token, 'EX', json.expires_in - 30);
40
- return {...json, servedFromCache: false};
41
- } catch (err) {
42
- debug(err, 'getIbmAccessToken: Error retrieving Ibm access token');
43
- logger.error(err, 'getIbmAccessToken: Error retrieving Ibm access token for client_id ${clientId}');
44
- throw err;
45
- }
46
- }
47
-
48
- module.exports = getIbmAccessToken;
package/test/ibm.js DELETED
@@ -1,78 +0,0 @@
1
- const test = require('tape').test ;
2
- const config = require('config');
3
- const opts = config.get('redis');
4
- const fs = require('fs');
5
- const logger = require('pino')({level: 'error'});
6
- process.on('unhandledRejection', (reason, p) => {
7
- console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
8
- });
9
-
10
- const stats = {
11
- increment: () => {},
12
- histogram: () => {}
13
- };
14
-
15
- test('IBM - create access key', async(t) => {
16
- const fn = require('..');
17
- const {client, getIbmAccessToken} = fn(opts, logger);
18
-
19
- if (!process.env.IBM_API_KEY ) {
20
- t.pass('skipping IBM test since no IBM api_key provided');
21
- t.end();
22
- client.quit();
23
- return;
24
- }
25
- try {
26
- let obj = await getIbmAccessToken(process.env.IBM_API_KEY);
27
- //console.log({obj}, 'received access token from IBM');
28
- t.ok(obj.access_token && !obj.servedFromCache, 'successfull received access token from IBM');
29
-
30
- obj = await getIbmAccessToken(process.env.IBM_API_KEY);
31
- //console.log({obj}, 'received access token from IBM - second request');
32
- t.ok(obj.access_token && obj.servedFromCache, 'successfully received access token from cache');
33
-
34
- await client.flushall();
35
- t.end();
36
- }
37
- catch (err) {
38
- console.error(err);
39
- t.end(err);
40
- }
41
- client.quit();
42
- });
43
-
44
- test('IBM - retrieve tts voices test', async(t) => {
45
- const fn = require('..');
46
- const {client, getTtsVoices} = fn(opts, logger);
47
-
48
- if (!process.env.IBM_TTS_API_KEY || !process.env.IBM_TTS_REGION) {
49
- t.pass('skipping IBM test since no IBM api_key and/or region provided');
50
- t.end();
51
- client.quit();
52
- return;
53
- }
54
- try {
55
- const opts = {
56
- vendor: 'ibm',
57
- credentials: {
58
- tts_api_key: process.env.IBM_TTS_API_KEY,
59
- tts_region: process.env.IBM_TTS_REGION
60
- }
61
- };
62
- const obj = await getTtsVoices(opts);
63
- const {voices} = obj.result;
64
- //console.log(JSON.stringify(voices));
65
- t.ok(voices.length > 0 && voices[0].language,
66
- `GetVoices: successfully retrieved ${voices.length} voices from IBM`);
67
-
68
- await client.flushall();
69
-
70
- t.end();
71
-
72
- }
73
- catch (err) {
74
- console.error(err);
75
- t.end(err);
76
- }
77
- client.quit();
78
- });