mirror of
https://github.com/jambonz/speech-utils.git
synced 2026-10-05 01:22:11 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf229d0ab0 | ||
|
|
f08fedb8ca | ||
|
|
fbed59e5de | ||
|
|
4f1685a365 | ||
|
|
2701af102a | ||
|
|
7f939b96d2 | ||
|
|
4d58ca6daf | ||
|
|
16dd7a2805 | ||
|
|
f06f96a6f0 | ||
|
|
2988e800b1 | ||
|
|
31a54f595b | ||
|
|
3560a6d4d9 | ||
|
|
be8053db4f | ||
|
|
4ffae38a3f | ||
|
|
9e74760c39 |
+41
-18
@@ -183,7 +183,9 @@ async function synthAudio(client, logger, stats, { account_sid,
|
|||||||
case 'azure':
|
case 'azure':
|
||||||
case 'microsoft':
|
case 'microsoft':
|
||||||
vendorLabel = 'microsoft';
|
vendorLabel = 'microsoft';
|
||||||
audioBuffer = await synthMicrosoft(logger, {credentials, stats, language, voice, text, deploymentId, filePath});
|
audioBuffer = await synthMicrosoft(logger, {credentials, stats, language, voice, text, deploymentId,
|
||||||
|
filePath, renderForCaching, disableTtsStreaming});
|
||||||
|
if (audioBuffer?.filePath) return audioBuffer;
|
||||||
break;
|
break;
|
||||||
case 'nuance':
|
case 'nuance':
|
||||||
model = model || 'enhanced';
|
model = model || 'enhanced';
|
||||||
@@ -381,10 +383,47 @@ const synthMicrosoft = async(logger, {
|
|||||||
language,
|
language,
|
||||||
voice,
|
voice,
|
||||||
text,
|
text,
|
||||||
filePath
|
filePath,
|
||||||
|
renderForCaching,
|
||||||
|
disableTtsStreaming
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
try {
|
||||||
const {api_key: apiKey, region, use_custom_tts, custom_tts_endpoint, custom_tts_endpoint_url} = credentials;
|
const {api_key: apiKey, region, use_custom_tts, custom_tts_endpoint, custom_tts_endpoint_url} = credentials;
|
||||||
|
// let clean up the text
|
||||||
|
let content = text;
|
||||||
|
if (use_custom_tts && !content.startsWith('<speak')) {
|
||||||
|
/**
|
||||||
|
* Note: it seems that to use custom voice ssml is required with the voice attribute
|
||||||
|
* Otherwise sending plain text we get "Voice does not match"
|
||||||
|
*/
|
||||||
|
content = `<speak>${text}</speak>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content.startsWith('<speak>')) {
|
||||||
|
/* microsoft enforces some properties and uses voice xml element so if the user did not supply do it for them */
|
||||||
|
const words = content.slice(7, -8).trim().replace(/(\r\n|\n|\r)/gm, ' ');
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
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>`;
|
||||||
|
logger.info({content}, 'synthMicrosoft');
|
||||||
|
}
|
||||||
|
if (!process.env.JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
|
||||||
|
let params = '';
|
||||||
|
params += `{api_key=${apiKey}`;
|
||||||
|
params += `,language=${language}`;
|
||||||
|
params += ',vendor=microsoft';
|
||||||
|
params += `,voice=${voice}`;
|
||||||
|
params += ',write_cache_file=1';
|
||||||
|
if (region) params += `,region=${region}`;
|
||||||
|
if (custom_tts_endpoint) params += `,endpointId=${custom_tts_endpoint}`;
|
||||||
|
if (process.env.JAMBONES_HTTP_PROXY_IP) params += `,http_proxy_ip=${process.env.JAMBONES_HTTP_PROXY_IP}`;
|
||||||
|
if (process.env.JAMBONES_HTTP_PROXY_PORT) params += `,http_proxy_port=${process.env.JAMBONES_HTTP_PROXY_PORT}`;
|
||||||
|
params += '}';
|
||||||
|
return {
|
||||||
|
filePath: `say:${params}${content.replace(/\n/g, ' ')}`,
|
||||||
|
servedFromCache: false,
|
||||||
|
rtt: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
if (use_custom_tts && custom_tts_endpoint_url) {
|
if (use_custom_tts && custom_tts_endpoint_url) {
|
||||||
return await _synthOnPremMicrosoft(logger, {
|
return await _synthOnPremMicrosoft(logger, {
|
||||||
credentials,
|
credentials,
|
||||||
@@ -396,20 +435,12 @@ const synthMicrosoft = async(logger, {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
const trimSilence = filePath.endsWith('.r8');
|
const trimSilence = filePath.endsWith('.r8');
|
||||||
let content = text;
|
|
||||||
const speechConfig = SpeechConfig.fromSubscription(apiKey, region);
|
const speechConfig = SpeechConfig.fromSubscription(apiKey, region);
|
||||||
speechConfig.speechSynthesisLanguage = language;
|
speechConfig.speechSynthesisLanguage = language;
|
||||||
speechConfig.speechSynthesisVoiceName = voice;
|
speechConfig.speechSynthesisVoiceName = voice;
|
||||||
if (use_custom_tts && custom_tts_endpoint) {
|
if (use_custom_tts && custom_tts_endpoint) {
|
||||||
speechConfig.endpointId = custom_tts_endpoint;
|
speechConfig.endpointId = custom_tts_endpoint;
|
||||||
}
|
}
|
||||||
if (use_custom_tts && !content.startsWith('<speak')) {
|
|
||||||
/**
|
|
||||||
* Note: it seems that to use custom voice ssml is required with the voice attribute
|
|
||||||
* Otherwise sending plain text we get "Voice does not match"
|
|
||||||
*/
|
|
||||||
content = `<speak>${text}</speak>`;
|
|
||||||
}
|
|
||||||
speechConfig.speechSynthesisOutputFormat = trimSilence ?
|
speechConfig.speechSynthesisOutputFormat = trimSilence ?
|
||||||
SpeechSynthesisOutputFormat.Raw8Khz16BitMonoPcm :
|
SpeechSynthesisOutputFormat.Raw8Khz16BitMonoPcm :
|
||||||
SpeechSynthesisOutputFormat.Audio16Khz32KBitRateMonoMp3;
|
SpeechSynthesisOutputFormat.Audio16Khz32KBitRateMonoMp3;
|
||||||
@@ -421,14 +452,6 @@ const synthMicrosoft = async(logger, {
|
|||||||
}
|
}
|
||||||
const synthesizer = new SpeechSynthesizer(speechConfig);
|
const synthesizer = new SpeechSynthesizer(speechConfig);
|
||||||
|
|
||||||
if (content.startsWith('<speak>')) {
|
|
||||||
/* microsoft enforces some properties and uses voice xml element so if the user did not supply do it for them */
|
|
||||||
const words = content.slice(7, -8).trim().replace(/(\r\n|\n|\r)/gm, ' ');
|
|
||||||
// eslint-disable-next-line max-len
|
|
||||||
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>`;
|
|
||||||
logger.info({content}, 'synthMicrosoft');
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const speakAsync = content.startsWith('<speak') ?
|
const speakAsync = content.startsWith('<speak') ?
|
||||||
synthesizer.speakSsmlAsync.bind(synthesizer) :
|
synthesizer.speakSsmlAsync.bind(synthesizer) :
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@jambonz/speech-utils",
|
"name": "@jambonz/speech-utils",
|
||||||
"version": "0.0.44",
|
"version": "0.0.46",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@jambonz/speech-utils",
|
"name": "@jambonz/speech-utils",
|
||||||
"version": "0.0.44",
|
"version": "0.0.46",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-polly": "^3.496.0",
|
"@aws-sdk/client-polly": "^3.496.0",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@jambonz/speech-utils",
|
"name": "@jambonz/speech-utils",
|
||||||
"version": "0.0.44",
|
"version": "0.0.46",
|
||||||
"description": "TTS-related speech utilities for jambonz",
|
"description": "TTS-related speech utilities for jambonz",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"author": "Dave Horton",
|
"author": "Dave Horton",
|
||||||
|
|||||||
@@ -188,6 +188,7 @@ test('Azure speech synth tests', async(t) => {
|
|||||||
language: 'en-US',
|
language: 'en-US',
|
||||||
voice: 'en-US-ChristopherNeural',
|
voice: 'en-US-ChristopherNeural',
|
||||||
text: longText,
|
text: longText,
|
||||||
|
renderForCaching: true
|
||||||
});
|
});
|
||||||
t.ok(!opts.servedFromCache, `successfully synthesized microsoft audio to ${opts.filePath}`);
|
t.ok(!opts.servedFromCache, `successfully synthesized microsoft audio to ${opts.filePath}`);
|
||||||
if (process.env.JAMBONES_HTTP_PROXY_IP && process.env.JAMBONES_HTTP_PROXY_PORT) {
|
if (process.env.JAMBONES_HTTP_PROXY_IP && process.env.JAMBONES_HTTP_PROXY_PORT) {
|
||||||
@@ -203,6 +204,7 @@ test('Azure speech synth tests', async(t) => {
|
|||||||
language: 'en-US',
|
language: 'en-US',
|
||||||
voice: 'en-US-ChristopherNeural',
|
voice: 'en-US-ChristopherNeural',
|
||||||
text: longText,
|
text: longText,
|
||||||
|
renderForCaching: true
|
||||||
});
|
});
|
||||||
t.ok(opts.servedFromCache, `successfully retrieved microsoft audio from cache ${opts.filePath}`);
|
t.ok(opts.servedFromCache, `successfully retrieved microsoft audio from cache ${opts.filePath}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -237,6 +239,7 @@ test('Azure SSML tests', async(t) => {
|
|||||||
language: 'en-US',
|
language: 'en-US',
|
||||||
voice: 'en-US-ChristopherNeural',
|
voice: 'en-US-ChristopherNeural',
|
||||||
text,
|
text,
|
||||||
|
renderForCaching: true
|
||||||
});
|
});
|
||||||
t.ok(!opts.servedFromCache, `successfully synthesized microsoft audio to ${opts.filePath}`);
|
t.ok(!opts.servedFromCache, `successfully synthesized microsoft audio to ${opts.filePath}`);
|
||||||
if (process.env.JAMBONES_HTTP_PROXY_IP && process.env.JAMBONES_HTTP_PROXY_PORT) {
|
if (process.env.JAMBONES_HTTP_PROXY_IP && process.env.JAMBONES_HTTP_PROXY_PORT) {
|
||||||
@@ -252,6 +255,7 @@ test('Azure SSML tests', async(t) => {
|
|||||||
language: 'en-US',
|
language: 'en-US',
|
||||||
voice: 'en-US-ChristopherNeural',
|
voice: 'en-US-ChristopherNeural',
|
||||||
text,
|
text,
|
||||||
|
renderForCaching: true
|
||||||
});
|
});
|
||||||
t.ok(opts.servedFromCache, `successfully retrieved microsoft audio from cache ${opts.filePath}`);
|
t.ok(opts.servedFromCache, `successfully retrieved microsoft audio from cache ${opts.filePath}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -283,6 +287,7 @@ test('Azure custom voice speech synth tests', async(t) => {
|
|||||||
language: 'en-US',
|
language: 'en-US',
|
||||||
voice: process.env.MICROSOFT_CUSTOM_VOICE,
|
voice: process.env.MICROSOFT_CUSTOM_VOICE,
|
||||||
text,
|
text,
|
||||||
|
renderForCaching: true
|
||||||
});
|
});
|
||||||
t.ok(!opts.servedFromCache, `successfully synthesized microsoft audio to ${opts.filePath}`);
|
t.ok(!opts.servedFromCache, `successfully synthesized microsoft audio to ${opts.filePath}`);
|
||||||
|
|
||||||
@@ -297,6 +302,7 @@ test('Azure custom voice speech synth tests', async(t) => {
|
|||||||
language: 'en-US',
|
language: 'en-US',
|
||||||
voice: process.env.MICROSOFT_CUSTOM_VOICE,
|
voice: process.env.MICROSOFT_CUSTOM_VOICE,
|
||||||
text,
|
text,
|
||||||
|
renderForCaching: true
|
||||||
});
|
});
|
||||||
t.ok(opts.servedFromCache, `successfully retrieved microsoft custom voice audio from cache ${opts.filePath}`);
|
t.ok(opts.servedFromCache, `successfully retrieved microsoft custom voice audio from cache ${opts.filePath}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
Reference in New Issue
Block a user