@jambonz/speech-utils 0.0.44 → 0.0.46
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 +41 -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,47 @@ 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
|
+
params += ',write_cache_file=1';
|
|
416
|
+
if (region) params += `,region=${region}`;
|
|
417
|
+
if (custom_tts_endpoint) params += `,endpointId=${custom_tts_endpoint}`;
|
|
418
|
+
if (process.env.JAMBONES_HTTP_PROXY_IP) params += `,http_proxy_ip=${process.env.JAMBONES_HTTP_PROXY_IP}`;
|
|
419
|
+
if (process.env.JAMBONES_HTTP_PROXY_PORT) params += `,http_proxy_port=${process.env.JAMBONES_HTTP_PROXY_PORT}`;
|
|
420
|
+
params += '}';
|
|
421
|
+
return {
|
|
422
|
+
filePath: `say:${params}${content.replace(/\n/g, ' ')}`,
|
|
423
|
+
servedFromCache: false,
|
|
424
|
+
rtt: 0
|
|
425
|
+
};
|
|
426
|
+
}
|
|
388
427
|
if (use_custom_tts && custom_tts_endpoint_url) {
|
|
389
428
|
return await _synthOnPremMicrosoft(logger, {
|
|
390
429
|
credentials,
|
|
@@ -396,20 +435,12 @@ const synthMicrosoft = async(logger, {
|
|
|
396
435
|
});
|
|
397
436
|
}
|
|
398
437
|
const trimSilence = filePath.endsWith('.r8');
|
|
399
|
-
let content = text;
|
|
400
438
|
const speechConfig = SpeechConfig.fromSubscription(apiKey, region);
|
|
401
439
|
speechConfig.speechSynthesisLanguage = language;
|
|
402
440
|
speechConfig.speechSynthesisVoiceName = voice;
|
|
403
441
|
if (use_custom_tts && custom_tts_endpoint) {
|
|
404
442
|
speechConfig.endpointId = custom_tts_endpoint;
|
|
405
443
|
}
|
|
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
444
|
speechConfig.speechSynthesisOutputFormat = trimSilence ?
|
|
414
445
|
SpeechSynthesisOutputFormat.Raw8Khz16BitMonoPcm :
|
|
415
446
|
SpeechSynthesisOutputFormat.Audio16Khz32KBitRateMonoMp3;
|
|
@@ -421,14 +452,6 @@ const synthMicrosoft = async(logger, {
|
|
|
421
452
|
}
|
|
422
453
|
const synthesizer = new SpeechSynthesizer(speechConfig);
|
|
423
454
|
|
|
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
455
|
return new Promise((resolve, reject) => {
|
|
433
456
|
const speakAsync = content.startsWith('<speak') ?
|
|
434
457
|
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) {
|