Compare commits

...
16 Commits
Author SHA1 Message Date
Dave Horton 043642ea5f 0.2.15 2025-07-13 10:27:02 -04:00
Dave Horton 91ea9820f9 Merge pull request #119 from vdharashive/main
TMP_FOLDER configurable
2025-07-10 07:32:16 -04:00
Vinod Dharashive fad0f57b13 TMP_FOLDER configurable
https://github.com/jambonz/speech-utils/issues/118
2025-07-10 15:01:08 +05:30
Dave Horton 2608e149a8 0.2.14 2025-07-09 13:43:38 -04:00
Dave Horton d21289acd6 sec fix 2025-07-09 13:43:12 -04:00
Dave Horton a7b19e1d8a Merge pull request #117 from sathishkumarpa-Kore/PLAT-41939_2
Fix security vulnerability by upgrading ibm-watson to latest version
2025-07-09 13:42:01 -04:00
sathishkumarpa-Kore 09c7aa9a5f Fix security vulnerability by upgrading ibm-watson 2025-07-03 18:22:34 +05:30
Dave Horton f311c21327 0.2.13 2025-06-26 08:02:24 -04:00
Dave Horton 535d0191da Merge pull request #115 from jambonz/feat/inworld_tts
support inworld tts
2025-06-26 08:01:23 -04:00
Quan HL c00d4f9be4 support inworld tts 2025-06-26 17:34:24 +07:00
Dave Horton db135ee5ad 0.2.12 2025-06-11 11:08:34 +02:00
Dave Horton 5eac4c2ad8 Merge pull request #114 from vasudevanubrolu/feat/893-azure-ssml
Feat/893 azure ssml
2025-06-11 11:05:48 +02:00
vasudevanubrolu e23a1a6d09 feat/893 azure ssml add namespace check 2025-06-04 12:53:37 +05:30
vasudevanubrolu 3a78300a08 feat/893 azure ssml only change free text 2025-06-04 11:18:11 +05:30
vasudevanubrolu 1d7390e7ae feat/893 azure ssml fix only on condition 2025-05-30 14:55:22 +05:30
vasudevanubrolu 3c0940f657 feat/893 azure ssml lang syntax fix 2025-05-30 14:55:22 +05:30
5 changed files with 437 additions and 649 deletions
+1 -1
View File
@@ -9,7 +9,7 @@ const JAMBONES_HTTP_PROXY_PORT = process.env.JAMBONES_HTTP_PROXY_PORT;
const JAMBONES_TTS_CACHE_DURATION_MINS =
(parseInt(process.env.JAMBONES_TTS_CACHE_DURATION_MINS) || 4 * 60) * 60; // cache tts for 4 hours
const TMP_FOLDER = '/tmp';
const TMP_FOLDER = process.env.JAMBONES_TMP_FOLDER || '/tmp';
const HTTP_TIMEOUT = 5000;
+77 -5
View File
@@ -98,7 +98,7 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
logger = logger || noopLogger;
assert.ok(['google', 'aws', 'polly', 'microsoft', 'wellsaid', 'nuance', 'nvidia', 'ibm', 'elevenlabs',
'whisper', 'deepgram', 'playht', 'rimelabs', 'verbio', 'cartesia'].includes(vendor) ||
'whisper', 'deepgram', 'playht', 'rimelabs', 'verbio', 'cartesia', 'inworld'].includes(vendor) ||
vendor.startsWith('custom'),
`synthAudio supported vendors are google, aws, microsoft, nuance, nvidia and wellsaid ..etc, not ${vendor}`);
if ('google' === vendor) {
@@ -141,6 +141,10 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
assert.ok(credentials.api_key, 'synthAudio requires api_key when playht is used');
assert.ok(credentials.user_id, 'synthAudio requires user_id when playht is used');
assert.ok(credentials.voice_engine, 'synthAudio requires voice_engine when playht is used');
} else if ('inworld' === vendor) {
assert.ok(voice, 'synthAudio requires voice when inworld is used');
assert.ok(credentials.api_key, 'synthAudio requires api_key when inworld is used');
assert.ok(credentials.model_id, 'synthAudio requires model_id when inworld is used');
} else if ('rimelabs' === vendor) {
assert.ok(voice, 'synthAudio requires voice when rimelabs is used');
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
audioData = await synthCartesia(logger, {
credentials, options, stats, language, voice, text, renderForCaching, disableTtsStreaming});
break;
case 'inworld':
audioData = await synthInworld(logger, {
credentials, options, stats, language, voice, text, renderForCaching, disableTtsStreaming});
break;
case 'rimelabs':
audioData = await synthRimelabs(logger, {
credentials, options, stats, language, voice, text, renderForCaching, disableTtsStreaming});
@@ -479,10 +487,10 @@ async function _synthOnPremMicrosoft(logger, {
/* 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}"><lang xml:lang="${language}"><voice name="${voice}">${words}</voice></lang></speak>`;
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');
}
else if (JAMBONES_AZURE_ENABLE_SSML) {
else if (JAMBONES_AZURE_ENABLE_SSML && !content.startsWith('<speak')) {
// 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}"><lang xml:lang="${language}">${text}</lang></voice></speak>`;
}
@@ -532,10 +540,10 @@ const synthMicrosoft = async(logger, {
/* 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}"><lang xml:lang="${language}"><voice name="${voice}">${words}</voice></lang></speak>`;
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');
}
else if (JAMBONES_AZURE_ENABLE_SSML) {
else if (JAMBONES_AZURE_ENABLE_SSML && !content.startsWith('<speak')) {
// 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}"><lang xml:lang="${language}">${text}</lang></voice></speak>`;
}
@@ -954,6 +962,70 @@ const synthPlayHT = async(client, logger, {
}
};
const synthInworld = async(logger, {
credentials, options, stats, voice, text, renderForCaching, disableTtsStreaming
}) => {
const {api_key, model_id, options: credOpts} = credentials;
const opts = !!options && Object.keys(options).length !== 0 ? options : JSON.parse(credOpts || '{}');
/* default to using the streaming interface, unless disabled by env var OR we want just a cache file */
if (!JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
let params = '';
params += `{api_key=${api_key}`;
params += `,model_id=${model_id}`;
params += ',vendor=inworld';
params += `,voice=${voice}`;
params += ',write_cache_file=1';
if (opts.temperature) params += `,temperature=${opts.temperature}`;
if (opts.audioConfig?.pitch) params += `,pitch=${opts.pitch}`;
if (opts.audioConfig?.speakingRate) params += `,speakingRate=${opts.speakingRate}`;
params += '}';
return {
filePath: `say:${params}${text.replace(/\n/g, ' ').replace(/\r/g, ' ')}`,
servedFromCache: false,
rtt: 0
};
}
try {
const url = 'https://api.inworld.ai/tts/v1/voice';
const sampleRate = 8000;
const options = {
method: 'POST',
headers: {
'Authorization': `Basic ${api_key}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
text,
voiceId: voice,
modelId: model_id,
audioConfig: {
...(opts.audioConfig || {}),
audioEncoding: 'MP3',
}
})
};
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(await response.text());
}
const json = await response.json();
return {
audioContent: Buffer.from(json.audioContent, 'base64'),
extension: 'mp3',
sampleRate
};
} catch (err) {
logger.info({err}, 'synth inworld returned error');
stats.increment('tts.count', ['vendor:inworld', 'accepted:no']);
throw err;
}
};
const synthRimelabs = async(logger, {
credentials, options, stats, language, voice, text, renderForCaching, disableTtsStreaming
}) => {
+327 -641
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@jambonz/speech-utils",
"version": "0.2.11",
"version": "0.2.15",
"description": "TTS-related speech utilities for jambonz",
"main": "index.js",
"author": "Dave Horton",
@@ -37,7 +37,7 @@
"debug": "^4.3.4",
"form-urlencoded": "^6.1.4",
"google-protobuf": "^3.21.2",
"ibm-watson": "^8.0.0",
"ibm-watson": "^11.0.0",
"microsoft-cognitiveservices-speech-sdk": "1.38.0",
"openai": "^4.98.0",
"undici": "^7.5.0"
+30
View File
@@ -720,6 +720,36 @@ test('Cartesia speech synth tests', async(t) => {
client.quit();
});
test('inworld speech synth', async(t) => {
const fn = require('..');
const {synthAudio, client} = fn(opts, logger);
if (!process.env.INWORLD_API_KEY) {
t.pass('skipping inworld speech synth tests since INWORLD_API_KEY is not provided');
return t.end();
}
const text = 'Hi there and welcome to jambones!';
try {
const opts = await synthAudio(stats, {
vendor: 'inworld',
credentials: {
api_key: process.env.INWORLD_API_KEY,
model_id: 'inworld-tts-1'
},
language: 'en',
voice: 'Ashley',
text,
renderForCaching: true
});
t.ok(!opts.servedFromCache, `successfully synthesized inworld audio to ${opts.filePath}`);
} catch (err) {
console.error(JSON.stringify(err));
t.end(err);
}
client.quit();
});
test('rimelabs speech synth tests mist', async(t) => {
const fn = require('..');
const {synthAudio, client} = fn(opts, logger);