@jambonz/speech-utils 0.2.11 → 0.2.13
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 +77 -5
- package/package.json +1 -1
- package/test/synth.js +30 -0
package/lib/synth-audio.js
CHANGED
|
@@ -98,7 +98,7 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
|
|
|
98
98
|
logger = logger || noopLogger;
|
|
99
99
|
|
|
100
100
|
assert.ok(['google', 'aws', 'polly', 'microsoft', 'wellsaid', 'nuance', 'nvidia', 'ibm', 'elevenlabs',
|
|
101
|
-
'whisper', 'deepgram', 'playht', 'rimelabs', 'verbio', 'cartesia'].includes(vendor) ||
|
|
101
|
+
'whisper', 'deepgram', 'playht', 'rimelabs', 'verbio', 'cartesia', 'inworld'].includes(vendor) ||
|
|
102
102
|
vendor.startsWith('custom'),
|
|
103
103
|
`synthAudio supported vendors are google, aws, microsoft, nuance, nvidia and wellsaid ..etc, not ${vendor}`);
|
|
104
104
|
if ('google' === vendor) {
|
|
@@ -141,6 +141,10 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
|
|
|
141
141
|
assert.ok(credentials.api_key, 'synthAudio requires api_key when playht is used');
|
|
142
142
|
assert.ok(credentials.user_id, 'synthAudio requires user_id when playht is used');
|
|
143
143
|
assert.ok(credentials.voice_engine, 'synthAudio requires voice_engine when playht is used');
|
|
144
|
+
} else if ('inworld' === vendor) {
|
|
145
|
+
assert.ok(voice, 'synthAudio requires voice when inworld is used');
|
|
146
|
+
assert.ok(credentials.api_key, 'synthAudio requires api_key when inworld is used');
|
|
147
|
+
assert.ok(credentials.model_id, 'synthAudio requires model_id when inworld is used');
|
|
144
148
|
} else if ('rimelabs' === vendor) {
|
|
145
149
|
assert.ok(voice, 'synthAudio requires voice when rimelabs is used');
|
|
146
150
|
assert.ok(credentials.api_key, 'synthAudio requires api_key when rimelabs is used');
|
|
@@ -238,6 +242,10 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
|
|
|
238
242
|
audioData = await synthCartesia(logger, {
|
|
239
243
|
credentials, options, stats, language, voice, text, renderForCaching, disableTtsStreaming});
|
|
240
244
|
break;
|
|
245
|
+
case 'inworld':
|
|
246
|
+
audioData = await synthInworld(logger, {
|
|
247
|
+
credentials, options, stats, language, voice, text, renderForCaching, disableTtsStreaming});
|
|
248
|
+
break;
|
|
241
249
|
case 'rimelabs':
|
|
242
250
|
audioData = await synthRimelabs(logger, {
|
|
243
251
|
credentials, options, stats, language, voice, text, renderForCaching, disableTtsStreaming});
|
|
@@ -479,10 +487,10 @@ async function _synthOnPremMicrosoft(logger, {
|
|
|
479
487
|
/* microsoft enforces some properties and uses voice xml element so if the user did not supply do it for them */
|
|
480
488
|
const words = content.slice(7, -8).trim().replace(/(\r\n|\n|\r)/gm, ' ');
|
|
481
489
|
// eslint-disable-next-line max-len
|
|
482
|
-
content = `<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xmlns:mstts="https://www.w3.org/2001/mstts" xml:lang="${language}"><
|
|
490
|
+
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>`;
|
|
483
491
|
logger.info({content}, 'synthMicrosoft');
|
|
484
492
|
}
|
|
485
|
-
else if (JAMBONES_AZURE_ENABLE_SSML) {
|
|
493
|
+
else if (JAMBONES_AZURE_ENABLE_SSML && !content.startsWith('<speak')) {
|
|
486
494
|
// eslint-disable-next-line max-len
|
|
487
495
|
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}"><lang xml:lang="${language}">${text}</lang></voice></speak>`;
|
|
488
496
|
}
|
|
@@ -532,10 +540,10 @@ const synthMicrosoft = async(logger, {
|
|
|
532
540
|
/* microsoft enforces some properties and uses voice xml element so if the user did not supply do it for them */
|
|
533
541
|
const words = content.slice(7, -8).trim().replace(/(\r\n|\n|\r)/gm, ' ');
|
|
534
542
|
// eslint-disable-next-line max-len
|
|
535
|
-
content = `<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xmlns:mstts="https://www.w3.org/2001/mstts" xml:lang="${language}"><
|
|
543
|
+
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>`;
|
|
536
544
|
logger.info({content}, 'synthMicrosoft');
|
|
537
545
|
}
|
|
538
|
-
else if (JAMBONES_AZURE_ENABLE_SSML) {
|
|
546
|
+
else if (JAMBONES_AZURE_ENABLE_SSML && !content.startsWith('<speak')) {
|
|
539
547
|
// eslint-disable-next-line max-len
|
|
540
548
|
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}"><lang xml:lang="${language}">${text}</lang></voice></speak>`;
|
|
541
549
|
}
|
|
@@ -954,6 +962,70 @@ const synthPlayHT = async(client, logger, {
|
|
|
954
962
|
}
|
|
955
963
|
};
|
|
956
964
|
|
|
965
|
+
const synthInworld = async(logger, {
|
|
966
|
+
credentials, options, stats, voice, text, renderForCaching, disableTtsStreaming
|
|
967
|
+
}) => {
|
|
968
|
+
const {api_key, model_id, options: credOpts} = credentials;
|
|
969
|
+
const opts = !!options && Object.keys(options).length !== 0 ? options : JSON.parse(credOpts || '{}');
|
|
970
|
+
|
|
971
|
+
/* default to using the streaming interface, unless disabled by env var OR we want just a cache file */
|
|
972
|
+
if (!JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
|
|
973
|
+
let params = '';
|
|
974
|
+
params += `{api_key=${api_key}`;
|
|
975
|
+
params += `,model_id=${model_id}`;
|
|
976
|
+
params += ',vendor=inworld';
|
|
977
|
+
params += `,voice=${voice}`;
|
|
978
|
+
params += ',write_cache_file=1';
|
|
979
|
+
if (opts.temperature) params += `,temperature=${opts.temperature}`;
|
|
980
|
+
if (opts.audioConfig?.pitch) params += `,pitch=${opts.pitch}`;
|
|
981
|
+
if (opts.audioConfig?.speakingRate) params += `,speakingRate=${opts.speakingRate}`;
|
|
982
|
+
params += '}';
|
|
983
|
+
|
|
984
|
+
return {
|
|
985
|
+
filePath: `say:${params}${text.replace(/\n/g, ' ').replace(/\r/g, ' ')}`,
|
|
986
|
+
servedFromCache: false,
|
|
987
|
+
rtt: 0
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
try {
|
|
992
|
+
const url = 'https://api.inworld.ai/tts/v1/voice';
|
|
993
|
+
const sampleRate = 8000;
|
|
994
|
+
const options = {
|
|
995
|
+
method: 'POST',
|
|
996
|
+
headers: {
|
|
997
|
+
'Authorization': `Basic ${api_key}`,
|
|
998
|
+
'Content-Type': 'application/json'
|
|
999
|
+
},
|
|
1000
|
+
body: JSON.stringify({
|
|
1001
|
+
text,
|
|
1002
|
+
voiceId: voice,
|
|
1003
|
+
modelId: model_id,
|
|
1004
|
+
audioConfig: {
|
|
1005
|
+
...(opts.audioConfig || {}),
|
|
1006
|
+
audioEncoding: 'MP3',
|
|
1007
|
+
}
|
|
1008
|
+
})
|
|
1009
|
+
};
|
|
1010
|
+
|
|
1011
|
+
const response = await fetch(url, options);
|
|
1012
|
+
|
|
1013
|
+
if (!response.ok) {
|
|
1014
|
+
throw new Error(await response.text());
|
|
1015
|
+
}
|
|
1016
|
+
const json = await response.json();
|
|
1017
|
+
return {
|
|
1018
|
+
audioContent: Buffer.from(json.audioContent, 'base64'),
|
|
1019
|
+
extension: 'mp3',
|
|
1020
|
+
sampleRate
|
|
1021
|
+
};
|
|
1022
|
+
} catch (err) {
|
|
1023
|
+
logger.info({err}, 'synth inworld returned error');
|
|
1024
|
+
stats.increment('tts.count', ['vendor:inworld', 'accepted:no']);
|
|
1025
|
+
throw err;
|
|
1026
|
+
}
|
|
1027
|
+
};
|
|
1028
|
+
|
|
957
1029
|
const synthRimelabs = async(logger, {
|
|
958
1030
|
credentials, options, stats, language, voice, text, renderForCaching, disableTtsStreaming
|
|
959
1031
|
}) => {
|
package/package.json
CHANGED
package/test/synth.js
CHANGED
|
@@ -720,6 +720,36 @@ test('Cartesia speech synth tests', async(t) => {
|
|
|
720
720
|
client.quit();
|
|
721
721
|
});
|
|
722
722
|
|
|
723
|
+
test('inworld speech synth', async(t) => {
|
|
724
|
+
const fn = require('..');
|
|
725
|
+
const {synthAudio, client} = fn(opts, logger);
|
|
726
|
+
|
|
727
|
+
if (!process.env.INWORLD_API_KEY) {
|
|
728
|
+
t.pass('skipping inworld speech synth tests since INWORLD_API_KEY is not provided');
|
|
729
|
+
return t.end();
|
|
730
|
+
}
|
|
731
|
+
const text = 'Hi there and welcome to jambones!';
|
|
732
|
+
try {
|
|
733
|
+
const opts = await synthAudio(stats, {
|
|
734
|
+
vendor: 'inworld',
|
|
735
|
+
credentials: {
|
|
736
|
+
api_key: process.env.INWORLD_API_KEY,
|
|
737
|
+
model_id: 'inworld-tts-1'
|
|
738
|
+
},
|
|
739
|
+
language: 'en',
|
|
740
|
+
voice: 'Ashley',
|
|
741
|
+
text,
|
|
742
|
+
renderForCaching: true
|
|
743
|
+
});
|
|
744
|
+
t.ok(!opts.servedFromCache, `successfully synthesized inworld audio to ${opts.filePath}`);
|
|
745
|
+
|
|
746
|
+
} catch (err) {
|
|
747
|
+
console.error(JSON.stringify(err));
|
|
748
|
+
t.end(err);
|
|
749
|
+
}
|
|
750
|
+
client.quit();
|
|
751
|
+
});
|
|
752
|
+
|
|
723
753
|
test('rimelabs speech synth tests mist', async(t) => {
|
|
724
754
|
const fn = require('..');
|
|
725
755
|
const {synthAudio, client} = fn(opts, logger);
|