@jambonz/speech-utils 0.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.
@@ -0,0 +1,469 @@
1
+ const assert = require('assert');
2
+ const fs = require('fs');
3
+ const bent = require('bent');
4
+ const ttsGoogle = require('@google-cloud/text-to-speech');
5
+ //const Polly = require('aws-sdk/clients/polly');
6
+ const { PollyClient, SynthesizeSpeechCommand } = require('@aws-sdk/client-polly');
7
+
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');
11
+ const {
12
+ AudioConfig,
13
+ ResultReason,
14
+ SpeechConfig,
15
+ SpeechSynthesizer,
16
+ CancellationDetails,
17
+ SpeechSynthesisOutputFormat
18
+ } = sdk;
19
+ const {makeSynthKey, createNuanceClient, noopLogger, createRivaClient} = require('./utils');
20
+ const getNuanceAccessToken = require('./get-nuance-access-token');
21
+ const {
22
+ SynthesisRequest,
23
+ Voice,
24
+ AudioFormat,
25
+ AudioParameters,
26
+ PCM,
27
+ Input,
28
+ Text,
29
+ SSML,
30
+ EventParameters
31
+ } = require('../stubs/nuance/synthesizer_pb');
32
+ const {SynthesizeSpeechRequest} = require('../stubs/riva/proto/riva_tts_pb');
33
+ const {AudioEncoding} = require('../stubs/riva/proto/riva_audio_pb');
34
+ const debug = require('debug')('jambonz:realtimedb-helpers');
35
+ const EXPIRES = 3600 * 24; // cache tts for 24 hours
36
+ const TMP_FOLDER = '/tmp';
37
+
38
+ /**
39
+ * Synthesize speech to an mp3 file, and also cache the generated speech
40
+ * in redis (base64 format) for 24 hours so as to avoid unnecessarily paying
41
+ * time and again for speech synthesis of the same text.
42
+ * It is the responsibility of the caller to unlink the mp3 file after use.
43
+ *
44
+ * @param {*} client - redis client
45
+ * @param {*} logger - pino logger
46
+ * @param {object} opts - options
47
+ * @param {string} opts.vendor - 'google' or 'aws' ('polly' is an alias for 'aws')
48
+ * @param {string} opt.language - language code
49
+ * @param {string} opts.voice - voice identifier
50
+ * @param {string} opts.text - text or ssml to synthesize
51
+ * @param {boolean} opts.disableTtsCache - disable TTS Cache retrieval
52
+ * @returns object containing filepath to an mp3 file in the /tmp folder containing
53
+ * the synthesized audio, and a variable indicating whether it was served from cache
54
+ */
55
+ async function synthAudio(client, logger, stats, {
56
+ vendor, language, voice, gender, text, engine, salt, model, credentials, deploymentId, disableTtsCache
57
+ }) {
58
+ let audioBuffer;
59
+ let servedFromCache = false;
60
+ let rtt;
61
+ logger = logger || noopLogger;
62
+
63
+ assert.ok(['google', 'aws', 'polly', 'microsoft', 'wellsaid', 'nuance', 'nvidia', 'ibm'].includes(vendor) ||
64
+ vendor.startsWith('custom'),
65
+ `synthAudio supported vendors are google, aws, microsoft, nuance, nvidia and wellsaid, not ${vendor}`);
66
+ if ('google' === vendor) {
67
+ assert.ok(language, 'synthAudio requires language when google is used');
68
+ }
69
+ else if (['aws', 'polly'].includes(vendor)) {
70
+ assert.ok(voice, 'synthAudio requires voice when aws polly is used');
71
+ }
72
+ else if ('microsoft' === vendor) {
73
+ assert.ok(language || deploymentId, 'synthAudio requires language when microsoft is used');
74
+ assert.ok(voice || deploymentId, 'synthAudio requires voice when microsoft is used');
75
+ }
76
+ else if ('nuance' === vendor) {
77
+ assert.ok(voice, 'synthAudio requires voice when nuance is used');
78
+ assert.ok(credentials.client_id, 'synthAudio requires client_id in credentials when nuance is used');
79
+ assert.ok(credentials.secret, 'synthAudio requires client_id in credentials when nuance is used');
80
+ }
81
+ else if ('nvidia' === vendor) {
82
+ assert.ok(voice, 'synthAudio requires voice when nvidia is used');
83
+ assert.ok(language, 'synthAudio requires language when nvidia is used');
84
+ assert.ok(credentials.riva_uri, 'synthAudio requires riva_uri in credentials when nuance is used');
85
+ }
86
+ else if ('ibm' === vendor) {
87
+ assert.ok(voice, 'synthAudio requires voice when ibm is used');
88
+ assert.ok(credentials.tts_region, 'synthAudio requires tts_region in credentials when ibm watson is used');
89
+ assert.ok(credentials.tts_api_key, 'synthAudio requires tts_api_key in credentials when nuance is used');
90
+ }
91
+ else if ('wellsaid' === vendor) {
92
+ language = 'en-US'; // WellSaid only supports English atm
93
+ assert.ok(voice, 'synthAudio requires voice when wellsaid is used');
94
+ assert.ok(!text.startsWith('<speak'), 'wellsaid does not support SSML tags');
95
+ } else if (vendor.startsWith('custom')) {
96
+ assert.ok(credentials.custom_tts_url, `synthAudio requires custom_tts_url in credentials when ${vendor} is used`);
97
+ }
98
+
99
+ const key = makeSynthKey({
100
+ vendor,
101
+ language: language || '',
102
+ voice: voice || deploymentId,
103
+ engine,
104
+ text
105
+ });
106
+ let filePath;
107
+ if (['nuance', 'nvidia'].includes(vendor)) {
108
+ filePath = `${TMP_FOLDER}/${key.replace('tts:', `tts-${salt || ''}`)}.r8`;
109
+ }
110
+ else filePath = `${TMP_FOLDER}/${key.replace('tts:', `tts-${salt || ''}`)}.mp3`;
111
+ debug(`synth key is ${key}`);
112
+ let cached;
113
+ if (!disableTtsCache) {
114
+ cached = await client.getAsync(key);
115
+ }
116
+ if (cached) {
117
+ // found in cache - extend the expiry and use it
118
+ debug('result WAS found in cache');
119
+ servedFromCache = true;
120
+ stats.increment('tts.cache.requests', ['found:yes']);
121
+ audioBuffer = Buffer.from(cached, 'base64');
122
+ client.expireAsync(key, EXPIRES).catch((err) => logger.info(err, 'Error setting expires'));
123
+ }
124
+ if (!cached) {
125
+ // not found in cache - go get it from speech vendor and add to cache
126
+ debug('result was NOT found in cache');
127
+ stats.increment('tts.cache.requests', ['found:no']);
128
+ let vendorLabel = vendor;
129
+ const startAt = process.hrtime();
130
+ switch (vendor) {
131
+ case 'google':
132
+ audioBuffer = await synthGoogle(logger, {credentials, stats, language, voice, gender, text});
133
+ break;
134
+ case 'aws':
135
+ case 'polly':
136
+ vendorLabel = 'aws';
137
+ audioBuffer = await synthPolly(logger, {credentials, stats, language, voice, text, engine});
138
+ break;
139
+ case 'azure':
140
+ case 'microsoft':
141
+ vendorLabel = 'microsoft';
142
+ audioBuffer = await synthMicrosoft(logger, {credentials, stats, language, voice, text, deploymentId, filePath});
143
+ break;
144
+ case 'nuance':
145
+ model = model || 'enhanced';
146
+ audioBuffer = await synthNuance(client, logger, {credentials, stats, voice, model, text});
147
+ break;
148
+ case 'nvidia':
149
+ audioBuffer = await synthNvidia(client, logger, {credentials, stats, language, voice, model, text});
150
+ break;
151
+ case 'ibm':
152
+ audioBuffer = await synthIbm(logger, {credentials, stats, voice, text});
153
+ break;
154
+ case 'wellsaid':
155
+ audioBuffer = await synthWellSaid(logger, {credentials, stats, language, voice, text, filePath});
156
+ break;
157
+ case vendor.startsWith('custom') ? vendor : 'cant_match_value':
158
+ audioBuffer = await synthCustomVendor(logger, {credentials, stats, language, voice, text});
159
+ break;
160
+ default:
161
+ assert(`synthAudio: unsupported speech vendor ${vendor}`);
162
+ }
163
+ const diff = process.hrtime(startAt);
164
+ const time = diff[0] * 1e3 + diff[1] * 1e-6;
165
+ rtt = time.toFixed(0);
166
+ stats.histogram('tts.response_time', rtt, [`vendor:${vendorLabel}`]);
167
+ debug(`tts rtt time for ${text.length} chars on ${vendorLabel}: ${rtt}`);
168
+ logger.info(`tts rtt time for ${text.length} chars on ${vendorLabel}: ${rtt}`);
169
+
170
+ client.setexAsync(key, EXPIRES, audioBuffer.toString('base64'))
171
+ .catch((err) => logger.error(err, `error calling setex on key ${key}`));
172
+
173
+ if (['microsoft'].includes(vendor)) return {filePath, servedFromCache, rtt};
174
+ }
175
+
176
+ return new Promise((resolve, reject) => {
177
+ fs.writeFile(filePath, audioBuffer, (err) => {
178
+ if (err) return reject(err);
179
+ resolve({filePath, servedFromCache, rtt});
180
+ });
181
+ });
182
+ }
183
+
184
+ const synthPolly = async(logger, {credentials, stats, language, voice, engine, text}) => {
185
+ try {
186
+ const polly = new PollyClient(credentials);
187
+ const opts = {
188
+ Engine: engine,
189
+ OutputFormat: 'mp3',
190
+ Text: text,
191
+ LanguageCode: language,
192
+ TextType: text.startsWith('<speak>') ? 'ssml' : 'text',
193
+ VoiceId: voice
194
+ };
195
+ const command = new SynthesizeSpeechCommand(opts);
196
+ const data = await polly.send(command);
197
+ const chunks = [];
198
+ return new Promise((resolve, reject) => {
199
+ data.AudioStream
200
+ .on('error', (err) => {
201
+ logger.info({err}, 'synthAudio: Error synthesizing speech using aws polly');
202
+ stats.increment('tts.count', ['vendor:aws', 'accepted:no']);
203
+ reject(err);
204
+ })
205
+ .on('data', (chunk) => {
206
+ chunks.push(chunk);
207
+ })
208
+ .on('end', () => resolve(Buffer.concat(chunks)));
209
+ });
210
+ } catch (err) {
211
+ logger.info({err}, 'synthAudio: Error synthesizing speech using aws polly');
212
+ stats.increment('tts.count', ['vendor:aws', 'accepted:no']);
213
+ throw err;
214
+ }
215
+ };
216
+
217
+ const synthGoogle = async(logger, {credentials, stats, language, voice, gender, text}) => {
218
+ const client = new ttsGoogle.TextToSpeechClient(credentials);
219
+ const opts = {
220
+ voice: {
221
+ name: voice,
222
+ languageCode: language,
223
+ ssmlGender: gender || 'SSML_VOICE_GENDER_UNSPECIFIED'
224
+ },
225
+ audioConfig: {audioEncoding: 'MP3'}
226
+ };
227
+ Object.assign(opts, {input: text.startsWith('<speak>') ? {ssml: text} : {text}});
228
+ try {
229
+ const responses = await client.synthesizeSpeech(opts);
230
+ stats.increment('tts.count', ['vendor:google', 'accepted:yes']);
231
+ client.close();
232
+ return responses[0].audioContent;
233
+ } catch (err) {
234
+ console.error(err);
235
+ logger.info({err, opts}, 'synthAudio: Error synthesizing speech using google');
236
+ stats.increment('tts.count', ['vendor:google', 'accepted:no']);
237
+ client && client.close();
238
+ throw err;
239
+ }
240
+ };
241
+
242
+ const synthIbm = async(logger, {credentials, stats, voice, text}) => {
243
+ const {tts_api_key, tts_region} = credentials;
244
+ const params = {
245
+ text,
246
+ voice,
247
+ accept: 'audio/mp3'
248
+ };
249
+
250
+ try {
251
+ const textToSpeech = new TextToSpeechV1({
252
+ authenticator: new IamAuthenticator({
253
+ apikey: tts_api_key,
254
+ }),
255
+ serviceUrl: `https://api.${tts_region}.text-to-speech.watson.cloud.ibm.com`
256
+ });
257
+
258
+ const r = await textToSpeech.synthesize(params);
259
+ const chunks = [];
260
+ for await (const chunk of r.result) {
261
+ chunks.push(chunk);
262
+ }
263
+ return Buffer.concat(chunks);
264
+ } catch (err) {
265
+ logger.info({err, params}, 'synthAudio: Error synthesizing speech using ibm');
266
+ stats.increment('tts.count', ['vendor:ibm', 'accepted:no']);
267
+ throw new Error(err.statusText || err.message);
268
+ }
269
+ };
270
+
271
+ const synthMicrosoft = async(logger, {
272
+ credentials,
273
+ stats,
274
+ language,
275
+ voice,
276
+ text,
277
+ filePath
278
+ }) => {
279
+ try {
280
+ const {api_key: apiKey, region, use_custom_tts, custom_tts_endpoint} = credentials;
281
+ let content = text;
282
+ const speechConfig = SpeechConfig.fromSubscription(apiKey, region);
283
+ speechConfig.speechSynthesisLanguage = language;
284
+ speechConfig.speechSynthesisVoiceName = voice;
285
+ if (use_custom_tts && custom_tts_endpoint) {
286
+ speechConfig.endpointId = custom_tts_endpoint;
287
+
288
+ /**
289
+ * Note: it seems that to use custom voice ssml is required with the voice attribute
290
+ * Otherwise sending plain text we get "Voice does not match"
291
+ */
292
+ if (!content.startsWith('<speak')) content = `<speak>${text}</speak>`;
293
+ }
294
+ speechConfig.speechSynthesisOutputFormat = SpeechSynthesisOutputFormat.Audio16Khz32KBitRateMonoMp3;
295
+ const config = AudioConfig.fromAudioFileOutput(filePath);
296
+ const synthesizer = new SpeechSynthesizer(speechConfig, config);
297
+
298
+ if (content.startsWith('<speak>')) {
299
+ /* microsoft enforces some properties and uses voice xml element so if the user did not supply do it for them */
300
+ const words = content.slice(7, -8).trim().replace(/(\r\n|\n|\r)/gm, ' ');
301
+ // eslint-disable-next-line max-len
302
+ 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>`;
303
+ logger.info({content}, 'synthMicrosoft');
304
+ }
305
+
306
+ return new Promise((resolve, reject) => {
307
+ const speakAsync = content.startsWith('<speak') ?
308
+ synthesizer.speakSsmlAsync.bind(synthesizer) :
309
+ synthesizer.speakTextAsync.bind(synthesizer);
310
+ speakAsync(
311
+ content,
312
+ async(result) => {
313
+ switch (result.reason) {
314
+ case ResultReason.Canceled:
315
+ const cancellation = CancellationDetails.fromResult(result);
316
+ logger.info({reason: cancellation.errorDetails}, 'synthAudio: (Microsoft) synthesis canceled');
317
+ synthesizer.close();
318
+ reject(cancellation.errorDetails);
319
+ break;
320
+ case ResultReason.SynthesizingAudioCompleted:
321
+ stats.increment('tts.count', ['vendor:microsoft', 'accepted:yes']);
322
+ synthesizer.close();
323
+ fs.readFile(filePath, (err, data) => {
324
+ if (err) return reject(err);
325
+ resolve(data);
326
+ });
327
+ break;
328
+ default:
329
+ logger.info({result}, 'synthAudio: (Microsoft) unexpected result');
330
+ break;
331
+ }
332
+ },
333
+ (err) => {
334
+ logger.info({err}, 'synthAudio: (Microsoft) error synthesizing');
335
+ stats.increment('tts.count', ['vendor:microsoft', 'accepted:no']);
336
+ synthesizer.close();
337
+ reject(err);
338
+ });
339
+ });
340
+ } catch (err) {
341
+ logger.info({err}, 'synthAudio: Error synthesizing speech using Microsoft');
342
+ stats.increment('tts.count', ['vendor:google', 'accepted:no']);
343
+ }
344
+ };
345
+
346
+ const synthWellSaid = async(logger, {credentials, stats, language, voice, gender, text}) => {
347
+ const {api_key} = credentials;
348
+ try {
349
+ const post = bent('https://api.wellsaidlabs.com', 'POST', 'buffer', {
350
+ 'X-Api-Key': api_key,
351
+ 'Accept': 'audio/mpeg',
352
+ 'Content-Type': 'application/json'
353
+ });
354
+ const mp3 = await post('/v1/tts/stream', {
355
+ text,
356
+ speaker_id: voice
357
+ });
358
+ return mp3;
359
+ } catch (err) {
360
+ logger.info({err}, 'testWellSaidTts returned error');
361
+ throw err;
362
+ }
363
+ };
364
+
365
+ const synthNuance = async(client, logger, {credentials, stats, voice, model, text}) => {
366
+ /* get a nuance access token */
367
+ const {client_id, secret} = credentials;
368
+ const {access_token} = await getNuanceAccessToken(client, logger, client_id, secret, 'tts');
369
+ const nuanceClient = await createNuanceClient(access_token);
370
+
371
+ const v = new Voice();
372
+ const p = new AudioParameters();
373
+ const f = new AudioFormat();
374
+ const pcm = new PCM();
375
+ const params = new EventParameters();
376
+ const request = new SynthesisRequest();
377
+ const input = new Input();
378
+
379
+ if (text.startsWith('<speak')) {
380
+ const ssml = new SSML();
381
+ ssml.setText(text);
382
+ input.setSsml(ssml);
383
+ }
384
+ else {
385
+ const t = new Text();
386
+ t.setText(text);
387
+ input.setText(t);
388
+ }
389
+
390
+ pcm.setSampleRateHz(8000);
391
+ f.setPcm(pcm);
392
+ p.setAudioFormat(f);
393
+ v.setName(voice);
394
+ v.setModel(model);
395
+ request.setVoice(v);
396
+ request.setAudioParams(p);
397
+ request.setInput(input);
398
+ request.setEventParams(params);
399
+ request.setUserId('jambonz');
400
+
401
+ return new Promise((resolve, reject) => {
402
+ nuanceClient.unarySynthesize(request, (err, response) => {
403
+ if (err) {
404
+ console.error(err);
405
+ return reject(err);
406
+ }
407
+ const status = response.getStatus();
408
+ const code = status.getCode();
409
+ if (code !== 200) {
410
+ const message = status.getMessage();
411
+ const details = status.getDetails();
412
+ return reject({code, message, details});
413
+ }
414
+ resolve(Buffer.from(response.getAudio()));
415
+ });
416
+ });
417
+ };
418
+
419
+ const synthNvidia = async(client, logger, {credentials, stats, language, voice, model, text}) => {
420
+ const {riva_uri} = credentials;
421
+ const rivaClient = await createRivaClient(riva_uri);
422
+
423
+ const request = new SynthesizeSpeechRequest();
424
+ request.setVoiceName(voice);
425
+ request.setLanguageCode(language);
426
+ request.setSampleRateHz(8000);
427
+ request.setEncoding(AudioEncoding.LINEAR_PCM);
428
+ request.setText(text);
429
+
430
+ return new Promise((resolve, reject) => {
431
+ console.log(`language ${language} voice ${voice} model ${model} text ${text}`);
432
+ rivaClient.synthesize(request, (err, response) => {
433
+ if (err) {
434
+ console.error(err);
435
+ return reject(err);
436
+ }
437
+ resolve(Buffer.from(response.getAudio()));
438
+ });
439
+ });
440
+ };
441
+
442
+
443
+ // CustomVendor accept only mp3
444
+ const synthCustomVendor = async(logger, {credentials, stats, language, voice, text}) => {
445
+ const {vendor, auth_token, custom_tts_url} = credentials;
446
+
447
+ try {
448
+ const post = bent('POST', 'buffer', {
449
+ 'Authorization': `Bearer ${auth_token}`,
450
+ 'Accept': 'audio/mpeg',
451
+ 'Content-Type': 'application/json'
452
+ });
453
+
454
+ const mp3 = await post(custom_tts_url, {
455
+ language,
456
+ format: 'audio/mpeg',
457
+ voice,
458
+ type: text.startsWith('<speak>') ? 'ssml' : 'text',
459
+ text
460
+ });
461
+
462
+ return mp3;
463
+ } catch (err) {
464
+ logger.info({err}, `Vendor ${vendor} returned error`);
465
+ throw err;
466
+ }
467
+ };
468
+
469
+ module.exports = synthAudio;
package/lib/utils.js ADDED
@@ -0,0 +1,115 @@
1
+ const crypto = require('crypto');
2
+ const {SynthesizerClient} = require('../stubs/nuance/synthesizer_grpc_pb');
3
+ const {RivaSpeechSynthesisClient} = require('../stubs/riva/proto/riva_tts_grpc_pb');
4
+ const {Pool} = require('undici');
5
+ const pool = new Pool('https://auth.crt.nuance.com');
6
+ const HTTP_TIMEOUT = 5000;
7
+ const NUANCE_AUTH_ENDPOINT = 'tts.api.nuance.com:443';
8
+ const grpc = require('@grpc/grpc-js');
9
+ const formurlencoded = require('form-urlencoded');
10
+
11
+ const debug = require('debug')('jambonz:realtimedb-helpers');
12
+ /**
13
+ * Future TODO: cache recently used connections to providers
14
+ * to avoid connection overhead during a call.
15
+ * Will need to periodically age them out to avoid memory leaks.
16
+ */
17
+ //const nuanceClientMap = new Map();
18
+
19
+ function makeSynthKey({vendor, language, voice, engine = '', text}) {
20
+ const hash = crypto.createHash('sha1');
21
+ hash.update(`${language}:${vendor}:${voice}:${engine}:${text}`);
22
+ return `tts:${hash.digest('hex')}`;
23
+ }
24
+
25
+ const noopLogger = {
26
+ info: () => {},
27
+ debug: () => {},
28
+ error: () => {}
29
+ };
30
+
31
+ const toBase64 = (str) => Buffer.from(str || '', 'utf8').toString('base64');
32
+
33
+ function makeBasicAuthHeader(username, password) {
34
+ if (!username || !password) return {};
35
+ const creds = `${encodeURIComponent(username)}:${password || ''}`;
36
+ const header = `Basic ${toBase64(creds)}`;
37
+ return {Authorization: header};
38
+ }
39
+
40
+ function makeIbmKey(apiKey) {
41
+ const hash = crypto.createHash('sha1');
42
+ hash.update(apiKey);
43
+ return `ibm:${hash.digest('hex')}`;
44
+ }
45
+
46
+ function makeNuanceKey(clientId, secret, scope) {
47
+ const hash = crypto.createHash('sha1');
48
+ hash.update(`${clientId}:${secret}:${scope}`);
49
+ return `nuance:${hash.digest('hex')}`;
50
+ }
51
+
52
+ const getNuanceAccessToken = async(clientId, secret, scope = 'asr tts') => {
53
+ const payload = {
54
+ grant_type: 'client_credentials',
55
+ scope
56
+ };
57
+ const auth = makeBasicAuthHeader(clientId, secret);
58
+ const {statusCode, headers, body} = await pool.request({
59
+ path: '/oauth2/token',
60
+ method: 'POST',
61
+ headers: {
62
+ ...auth,
63
+ 'Content-Type': 'application/x-www-form-urlencoded'
64
+ },
65
+ body: formurlencoded(payload),
66
+ timeout: HTTP_TIMEOUT,
67
+ followRedirects: false
68
+ });
69
+
70
+ if (200 !== statusCode) {
71
+ debug({statusCode, headers, body: body.text()}, 'error fetching access token from Nuance');
72
+ const err = new Error();
73
+ err.statusCode = statusCode;
74
+ throw err;
75
+ }
76
+ const json = await body.json();
77
+ return json.access_token;
78
+ };
79
+
80
+ const createNuanceClient = async(access_token) => {
81
+
82
+ //if (nuanceClientMap.has(access_token)) return nuanceClientMap.get(access_token);
83
+
84
+ const generateMetadata = (params, callback) => {
85
+ var metadata = new grpc.Metadata();
86
+ metadata.add('authorization', `Bearer ${access_token}`);
87
+ callback(null, metadata);
88
+ };
89
+
90
+ const sslCreds = grpc.credentials.createSsl();
91
+ const authCreds = grpc.credentials.createFromMetadataGenerator(generateMetadata);
92
+ const combined_creds = grpc.credentials.combineChannelCredentials(sslCreds, authCreds);
93
+ const client = new SynthesizerClient(NUANCE_AUTH_ENDPOINT, combined_creds);
94
+
95
+ //if (process.env.NUANCE_CACHE_TTS_CONNECTIONS) nuanceClientMap.set(access_token, client);
96
+ return client;
97
+ };
98
+
99
+ const createRivaClient = async(rivaUri) => {
100
+ const client = new RivaSpeechSynthesisClient(rivaUri, grpc.credentials.createInsecure());
101
+ return client;
102
+ };
103
+
104
+
105
+ module.exports = {
106
+ makeSynthKey,
107
+ makeNuanceKey,
108
+ makeIbmKey,
109
+ getNuanceAccessToken,
110
+ createNuanceClient,
111
+ createRivaClient,
112
+ makeBasicAuthHeader,
113
+ NUANCE_AUTH_ENDPOINT,
114
+ noopLogger
115
+ };
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@jambonz/speech-utils",
3
+ "version": "0.0.1",
4
+ "description": "TTS-related speech utilities for jambonz",
5
+ "main": "index.js",
6
+ "author": "Dave Horton",
7
+ "directories": {
8
+ "lib": "lib",
9
+ "test": "test"
10
+ },
11
+ "scripts": {
12
+ "test": "NODE_ENV=test JAMBONES_REDIS_USERNAME=daveh JAMBONES_REDIS_PASSWORD=foobarbazzle node test/ ",
13
+ "coverage": "nyc --reporter html --report-dir ./coverage npm run test",
14
+ "jslint": "eslint index.js lib",
15
+ "build": "./build_stubs.sh"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/jambonz/speech-utils.git"
20
+ },
21
+ "license": "MIT",
22
+ "bugs": {
23
+ "url": "https://github.com/jambonz/speech-utils/issues"
24
+ },
25
+ "homepage": "https://github.com/jambonz/speech-utils#readme",
26
+ "dependencies": {
27
+ "@aws-sdk/client-polly": "^3.276.0",
28
+ "@google-cloud/text-to-speech": "^4.2.0",
29
+ "@grpc/grpc-js": "^1.8.7",
30
+ "@jambonz/realtimedb-helpers": "^0.6.3",
31
+ "aws-sdk": "^2.1310.0",
32
+ "bent": "^7.3.12",
33
+ "debug": "^4.3.4",
34
+ "google-protobuf": "^3.21.2",
35
+ "ibm-watson": "^7.1.2",
36
+ "form-urlencoded": "^6.1.0",
37
+ "microsoft-cognitiveservices-speech-sdk": "^1.25.0",
38
+ "undici": "^5.19.1"
39
+ },
40
+ "devDependencies": {
41
+ "config": "^3.3.9",
42
+ "eslint": "^8.33.0",
43
+ "eslint-plugin-promise": "^6.1.1",
44
+ "nyc": "^15.1.0",
45
+ "pino": "^7.2.0",
46
+ "tape": "^5.1.1"
47
+ }
48
+ }