@jambonz/speech-utils 0.0.44 → 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.
- package/lib/synth-audio.js +40 -18
- package/package.json +1 -1
- package/test/synth.js +6 -0
package/lib/synth-audio.js
CHANGED
|
@@ -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,
|
|
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) :
|
package/package.json
CHANGED
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) {
|
|
@@ -237,6 +239,7 @@ test('Azure SSML tests', async(t) => {
|
|
|
237
239
|
language: 'en-US',
|
|
238
240
|
voice: 'en-US-ChristopherNeural',
|
|
239
241
|
text,
|
|
242
|
+
renderForCaching: true
|
|
240
243
|
});
|
|
241
244
|
t.ok(!opts.servedFromCache, `successfully synthesized microsoft audio to ${opts.filePath}`);
|
|
242
245
|
if (process.env.JAMBONES_HTTP_PROXY_IP && process.env.JAMBONES_HTTP_PROXY_PORT) {
|
|
@@ -252,6 +255,7 @@ test('Azure SSML tests', async(t) => {
|
|
|
252
255
|
language: 'en-US',
|
|
253
256
|
voice: 'en-US-ChristopherNeural',
|
|
254
257
|
text,
|
|
258
|
+
renderForCaching: true
|
|
255
259
|
});
|
|
256
260
|
t.ok(opts.servedFromCache, `successfully retrieved microsoft audio from cache ${opts.filePath}`);
|
|
257
261
|
} catch (err) {
|
|
@@ -283,6 +287,7 @@ test('Azure custom voice speech synth tests', async(t) => {
|
|
|
283
287
|
language: 'en-US',
|
|
284
288
|
voice: process.env.MICROSOFT_CUSTOM_VOICE,
|
|
285
289
|
text,
|
|
290
|
+
renderForCaching: true
|
|
286
291
|
});
|
|
287
292
|
t.ok(!opts.servedFromCache, `successfully synthesized microsoft audio to ${opts.filePath}`);
|
|
288
293
|
|
|
@@ -297,6 +302,7 @@ test('Azure custom voice speech synth tests', async(t) => {
|
|
|
297
302
|
language: 'en-US',
|
|
298
303
|
voice: process.env.MICROSOFT_CUSTOM_VOICE,
|
|
299
304
|
text,
|
|
305
|
+
renderForCaching: true
|
|
300
306
|
});
|
|
301
307
|
t.ok(opts.servedFromCache, `successfully retrieved microsoft custom voice audio from cache ${opts.filePath}`);
|
|
302
308
|
} catch (err) {
|