@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.
- package/lib/synth-audio.js +43 -18
- package/package.json +1 -1
- package/test/synth.js +56 -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) :
|
|
@@ -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
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) {
|