@jambonz/speech-utils 0.0.43 → 0.0.45

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.
@@ -183,7 +183,9 @@ async function synthAudio(client, logger, stats, { account_sid,
183
183
  case 'azure':
184
184
  case 'microsoft':
185
185
  vendorLabel = 'microsoft';
186
- audioBuffer = await synthMicrosoft(logger, {credentials, stats, language, voice, text, deploymentId, filePath});
186
+ audioBuffer = await synthMicrosoft(logger, {credentials, stats, language, voice, text, deploymentId,
187
+ filePath, renderForCaching, disableTtsStreaming});
188
+ if (audioBuffer?.filePath) return audioBuffer;
187
189
  break;
188
190
  case 'nuance':
189
191
  model = model || 'enhanced';
@@ -381,10 +383,46 @@ const synthMicrosoft = async(logger, {
381
383
  language,
382
384
  voice,
383
385
  text,
384
- filePath
386
+ filePath,
387
+ renderForCaching,
388
+ disableTtsStreaming
385
389
  }) => {
386
390
  try {
387
391
  const {api_key: apiKey, region, use_custom_tts, custom_tts_endpoint, custom_tts_endpoint_url} = credentials;
392
+ // let clean up the text
393
+ let content = text;
394
+ if (use_custom_tts && !content.startsWith('<speak')) {
395
+ /**
396
+ * Note: it seems that to use custom voice ssml is required with the voice attribute
397
+ * Otherwise sending plain text we get "Voice does not match"
398
+ */
399
+ content = `<speak>${text}</speak>`;
400
+ }
401
+
402
+ if (content.startsWith('<speak>')) {
403
+ /* microsoft enforces some properties and uses voice xml element so if the user did not supply do it for them */
404
+ const words = content.slice(7, -8).trim().replace(/(\r\n|\n|\r)/gm, ' ');
405
+ // eslint-disable-next-line max-len
406
+ 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>`;
407
+ logger.info({content}, 'synthMicrosoft');
408
+ }
409
+ if (!process.env.JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
410
+ let params = '';
411
+ params += `{api_key=${apiKey}`;
412
+ params += `,language=${language}`;
413
+ params += ',vendor=microsoft';
414
+ params += `,voice=${voice}`;
415
+ if (region) params += `,region=${region}`;
416
+ if (custom_tts_endpoint) params += `,endpointId=${custom_tts_endpoint}`;
417
+ if (process.env.JAMBONES_HTTP_PROXY_IP) params += `,http_proxy_ip=${process.env.JAMBONES_HTTP_PROXY_IP}`;
418
+ if (process.env.JAMBONES_HTTP_PROXY_PORT) params += `,http_proxy_port=${process.env.JAMBONES_HTTP_PROXY_PORT}`;
419
+ params += '}';
420
+ return {
421
+ filePath: `say:${params}${content.replace(/\n/g, ' ')}`,
422
+ servedFromCache: false,
423
+ rtt: 0
424
+ };
425
+ }
388
426
  if (use_custom_tts && custom_tts_endpoint_url) {
389
427
  return await _synthOnPremMicrosoft(logger, {
390
428
  credentials,
@@ -396,20 +434,12 @@ const synthMicrosoft = async(logger, {
396
434
  });
397
435
  }
398
436
  const trimSilence = filePath.endsWith('.r8');
399
- let content = text;
400
437
  const speechConfig = SpeechConfig.fromSubscription(apiKey, region);
401
438
  speechConfig.speechSynthesisLanguage = language;
402
439
  speechConfig.speechSynthesisVoiceName = voice;
403
440
  if (use_custom_tts && custom_tts_endpoint) {
404
441
  speechConfig.endpointId = custom_tts_endpoint;
405
442
  }
406
- if (use_custom_tts && !content.startsWith('<speak')) {
407
- /**
408
- * Note: it seems that to use custom voice ssml is required with the voice attribute
409
- * Otherwise sending plain text we get "Voice does not match"
410
- */
411
- content = `<speak>${text}</speak>`;
412
- }
413
443
  speechConfig.speechSynthesisOutputFormat = trimSilence ?
414
444
  SpeechSynthesisOutputFormat.Raw8Khz16BitMonoPcm :
415
445
  SpeechSynthesisOutputFormat.Audio16Khz32KBitRateMonoMp3;
@@ -421,14 +451,6 @@ const synthMicrosoft = async(logger, {
421
451
  }
422
452
  const synthesizer = new SpeechSynthesizer(speechConfig);
423
453
 
424
- if (content.startsWith('<speak>')) {
425
- /* microsoft enforces some properties and uses voice xml element so if the user did not supply do it for them */
426
- const words = content.slice(7, -8).trim().replace(/(\r\n|\n|\r)/gm, ' ');
427
- // eslint-disable-next-line max-len
428
- 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>`;
429
- logger.info({content}, 'synthMicrosoft');
430
- }
431
-
432
454
  return new Promise((resolve, reject) => {
433
455
  const speakAsync = content.startsWith('<speak') ?
434
456
  synthesizer.speakSsmlAsync.bind(synthesizer) :
@@ -614,6 +636,8 @@ const synthElevenlabs = async(logger, {
614
636
  if (!process.env.JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
615
637
  let params = '';
616
638
  params += `{api_key=${api_key}`;
639
+ params += ',vendor=elevenlabs';
640
+ params += `,voice=${voice}`;
617
641
  params += `,model_id=${model_id}`;
618
642
  params += `,optimize_streaming_latency=${opts.optimize_streaming_latency || 2}`;
619
643
  params += ',write_cache_file=1';
@@ -662,6 +686,7 @@ const synthWhisper = async(logger, {credentials, stats, voice, text, renderForCa
662
686
  let params = '';
663
687
  params += `{api_key=${api_key}`;
664
688
  params += `,model_id=${model_id}`;
689
+ params += ',vendor=whisper';
665
690
  params += `,voice=${voice}`;
666
691
  params += ',write_cache_file=1';
667
692
  if (speed) params += `,speed=${speed}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jambonz/speech-utils",
3
- "version": "0.0.43",
3
+ "version": "0.0.45",
4
4
  "description": "TTS-related speech utilities for jambonz",
5
5
  "main": "index.js",
6
6
  "author": "Dave Horton",
package/test/synth.js CHANGED
@@ -188,6 +188,7 @@ test('Azure speech synth tests', async(t) => {
188
188
  language: 'en-US',
189
189
  voice: 'en-US-ChristopherNeural',
190
190
  text: longText,
191
+ renderForCaching: true
191
192
  });
192
193
  t.ok(!opts.servedFromCache, `successfully synthesized microsoft audio to ${opts.filePath}`);
193
194
  if (process.env.JAMBONES_HTTP_PROXY_IP && process.env.JAMBONES_HTTP_PROXY_PORT) {
@@ -203,6 +204,7 @@ test('Azure speech synth tests', async(t) => {
203
204
  language: 'en-US',
204
205
  voice: 'en-US-ChristopherNeural',
205
206
  text: longText,
207
+ renderForCaching: true
206
208
  });
207
209
  t.ok(opts.servedFromCache, `successfully retrieved microsoft audio from cache ${opts.filePath}`);
208
210
  } catch (err) {
@@ -212,6 +214,58 @@ test('Azure speech synth tests', async(t) => {
212
214
  client.quit();
213
215
  });
214
216
 
217
+ test('Azure SSML tests', async(t) => {
218
+ const fn = require('..');
219
+ const {synthAudio, client} = fn(opts, logger);
220
+
221
+ if (!process.env.MICROSOFT_API_KEY || !process.env.MICROSOFT_REGION) {
222
+ t.pass('skipping Microsoft speech synth tests since MICROSOFT_API_KEY or MICROSOFT_REGION not provided');
223
+ return t.end();
224
+ }
225
+ try {
226
+ const text = `<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xmlns:mstts="https://www.w3.org/2001/mstts" xml:lang="en-US">
227
+ <voice name="en-US-JennyMultilingualNeural">
228
+ <mstts:express-as style="cheerful" styledegree="2">That'd be just amazing!
229
+ </mstts:express-as>
230
+ </voice>
231
+ </speak>`;
232
+
233
+ let opts = await synthAudio(stats, {
234
+ vendor: 'microsoft',
235
+ credentials: {
236
+ api_key: process.env.MICROSOFT_API_KEY,
237
+ region: process.env.MICROSOFT_REGION,
238
+ },
239
+ language: 'en-US',
240
+ voice: 'en-US-ChristopherNeural',
241
+ text,
242
+ renderForCaching: true
243
+ });
244
+ t.ok(!opts.servedFromCache, `successfully synthesized microsoft audio to ${opts.filePath}`);
245
+ if (process.env.JAMBONES_HTTP_PROXY_IP && process.env.JAMBONES_HTTP_PROXY_PORT) {
246
+ t.pass('successfully used proxy to reach microsoft tts service');
247
+ }
248
+
249
+ opts = await synthAudio(stats, {
250
+ vendor: 'microsoft',
251
+ credentials: {
252
+ api_key: process.env.MICROSOFT_API_KEY,
253
+ region: process.env.MICROSOFT_REGION,
254
+ },
255
+ language: 'en-US',
256
+ voice: 'en-US-ChristopherNeural',
257
+ text,
258
+ renderForCaching: true
259
+ });
260
+ t.ok(opts.servedFromCache, `successfully retrieved microsoft audio from cache ${opts.filePath}`);
261
+ } catch (err) {
262
+ console.error(err);
263
+ t.end(err);
264
+ }
265
+ client.quit();
266
+ });
267
+
268
+
215
269
  test('Azure custom voice speech synth tests', async(t) => {
216
270
  const fn = require('..');
217
271
  const {synthAudio, client} = fn(opts, logger);
@@ -233,6 +287,7 @@ test('Azure custom voice speech synth tests', async(t) => {
233
287
  language: 'en-US',
234
288
  voice: process.env.MICROSOFT_CUSTOM_VOICE,
235
289
  text,
290
+ renderForCaching: true
236
291
  });
237
292
  t.ok(!opts.servedFromCache, `successfully synthesized microsoft audio to ${opts.filePath}`);
238
293
 
@@ -247,6 +302,7 @@ test('Azure custom voice speech synth tests', async(t) => {
247
302
  language: 'en-US',
248
303
  voice: process.env.MICROSOFT_CUSTOM_VOICE,
249
304
  text,
305
+ renderForCaching: true
250
306
  });
251
307
  t.ok(opts.servedFromCache, `successfully retrieved microsoft custom voice audio from cache ${opts.filePath}`);
252
308
  } catch (err) {