Compare commits

..
53 Commits
Author SHA1 Message Date
Dave Horton 8154944252 0.2.26 2025-10-30 07:08:47 -04:00
Dave Horton 16fe8dce01 0.2.25 2025-10-30 07:08:10 -04:00
Dave Horton 11a955500d Merge pull request #132 from jambonz/feat/sonic_3
cartesia support volume for sonic3
2025-10-30 07:07:15 -04:00
Hoan HL c122129b55 wip 2025-10-30 12:29:09 +07:00
Hoan HL 41b26b966b wip 2025-10-30 12:08:38 +07:00
Hoan HL aa09c15b20 wip 2025-10-30 06:46:57 +07:00
Hoan HL 32d5f12638 cartesia support volume for sonic3 2025-10-30 05:57:38 +07:00
Dave Horton 4e336822a0 Merge pull request #131 from jambonz/feat/gh_fs_1384
support elevenlabs api_uri
2025-10-08 13:45:02 -04:00
Hoan HL b898d794b0 wip 2025-10-08 15:05:04 +07:00
Hoan HL fb754ca101 support elevenlabs api_uri 2025-10-08 10:26:17 +07:00
Dave Horton 8d8195be9a 0.2.24 2025-10-03 08:54:14 -04:00
Dave Horton eb4e1a773f Merge pull request #130 from jambonz/feat/disableTtsCache
set write_cache_file = 0 when disableTtsCache
2025-10-03 02:21:57 -04:00
Hoan HL 6fecb8755d set write_cache_file = 0 when disableTtsCache 2025-10-03 11:09:30 +07:00
Dave Horton fdb56cbc77 Merge pull request #128 from jambonz/feat/custom_tts_stream
support custom tts Stream
2025-09-11 09:24:18 -04:00
Quan HL 5328a60de8 support custom tts Stream 2025-09-11 09:18:42 -04:00
Dave Horton ea1ab301d5 0.2.23 2025-09-10 22:53:45 -04:00
Dave Horton 0a4994c5a3 Merge pull request #129 from jambonz/fix/fd_1372
remove optimize_streaming_latency as default option for elevenlabs
2025-09-10 22:53:27 -04:00
Quan HL cb71460189 remove optimize_streaming_latency as default option for elevenlabs 2025-09-11 09:44:53 +07:00
Dave Horton f08efa49b1 0.2.22 2025-08-20 18:03:08 -04:00
Dave Horton 80eda37ef6 Merge pull request #126 from jambonz/feat/revamp-playback-id
use synth key as playback id
2025-08-20 18:02:35 -04:00
Dave Horton 7768e58b19 use synth key as playback id 2025-08-20 17:11:45 -04:00
Dave Horton 6efc99ad83 0.2.21 2025-08-20 10:59:40 -04:00
Dave Horton 127d01a8c1 add playback_id setting to additional tts vendors 2025-08-20 10:58:28 -04:00
Dave Horton 1900b26d8b 0.2.20 2025-08-20 10:01:01 -04:00
Dave Horton aa25a6f6d9 Merge pull request #125 from jambonz/feat/add-playback-id
add playback_id to say metadata
2025-08-20 10:00:37 -04:00
Dave Horton 8dfe17b600 add playback_id to say metadata 2025-08-20 08:58:46 -04:00
Dave Horton 5463e9f56e 0.2.19 2025-08-17 09:35:07 -04:00
Dave Horton 2eb36af650 Merge pull request #124 from jambonz/fix/aws_tts
support mod_aws_tts with engine parameter
2025-08-17 09:23:58 -04:00
Quan HL b186cbc4f2 support mod_aws_tts with engine parameter 2025-08-17 06:44:40 +07:00
Dave Horton b1b9c182a9 0.2.18 2025-08-14 08:17:02 -04:00
Dave Horton 15c77626fc Merge pull request #123 from jambonz/feat/mod_aws_tts
support mod_aws_tts
2025-08-14 08:16:32 -04:00
Quan HL 76126ec0b3 wip 2025-08-14 18:49:29 +07:00
Quan HL 55431ee511 wip 2025-08-14 17:19:46 +07:00
Quan HL 1c76f74b4a wip 2025-08-14 17:13:05 +07:00
Quan HL 62ad8abb8e wip 2025-08-14 16:49:47 +07:00
Quan HL 92734aaedb wip 2025-08-14 16:38:36 +07:00
Quan HL fe4ccfe7d7 support mod_aws_tts 2025-08-14 16:34:40 +07:00
Dave Horton ab05976032 0.2.17 2025-08-13 07:41:24 -04:00
Dave Horton babd8abc51 Merge pull request #122 from jambonz/feat/resemble_tts_01
support resemble tts
2025-08-13 07:33:12 -04:00
Quan HL 91f85e16b2 support resemble tts 2025-08-13 14:00:24 +07:00
Dave Horton 8a0c5e3dbd tag 2025-08-10 20:11:52 -04:00
Dave Horton d40e012138 Merge pull request #120 from jambonz/feat/resemble_tts
support resemble ai
2025-08-10 19:12:26 -04:00
Quan HL 49b23f5240 support resemble ai 2025-08-10 15:29:01 +07:00
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
6 changed files with 848 additions and 748 deletions
+1 -1
View File
@@ -11,7 +11,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: lts/*
node-version: '20'
- run: npm install
- name: Install Docker Compose
run: |
+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;
+285 -65
View File
@@ -53,7 +53,6 @@ const EXPIRES = JAMBONES_TTS_CACHE_DURATION_MINS;
const OpenAI = require('openai');
const getAwsAuthToken = require('./get-aws-sts-token');
const trimTrailingSilence = (buffer) => {
assert.ok(buffer instanceof Buffer, 'trimTrailingSilence - argument is not a Buffer');
@@ -98,7 +97,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', 'resemble'].includes(vendor) ||
vendor.startsWith('custom'),
`synthAudio supported vendors are google, aws, microsoft, nuance, nvidia and wellsaid ..etc, not ${vendor}`);
if ('google' === vendor) {
@@ -141,6 +140,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');
@@ -162,6 +165,9 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
} else if ('cartesia' === vendor) {
assert.ok(credentials.api_key, 'synthAudio requires api_key when cartesia is used');
assert.ok(credentials.model_id, 'synthAudio requires model_id when cartesia is used');
} else if (vendor === 'resemble') {
assert.ok(voice, 'synthAudio requires voice when resemble is used');
assert.ok(credentials.api_key, 'synthAudio requires api_key when resemble is used');
}
const key = makeSynthKey({
@@ -198,66 +204,81 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
const startAt = process.hrtime();
switch (vendor) {
case 'google':
audioData = await synthGoogle(logger, {credentials, stats, language, voice, gender, text});
audioData = await synthGoogle(logger, {credentials, stats, language, voice, gender, key, text});
break;
case 'aws':
case 'polly':
vendorLabel = 'aws';
audioData = await synthPolly(createHash, retrieveHash, logger,
{credentials, stats, language, voice, text, engine});
{credentials, stats, language, voice, key, text, engine, renderForCaching, disableTtsStreaming,
disableTtsCache});
break;
case 'azure':
case 'microsoft':
vendorLabel = 'microsoft';
audioData = await synthMicrosoft(logger, {credentials, stats, language, voice, text, deploymentId,
renderForCaching, disableTtsStreaming});
audioData = await synthMicrosoft(logger, {credentials, stats, language, voice, key, text, deploymentId,
renderForCaching, disableTtsStreaming, disableTtsCache});
break;
case 'nuance':
model = model || 'enhanced';
audioData = await synthNuance(client, logger, {credentials, stats, voice, model, text});
audioData = await synthNuance(client, logger, {credentials, stats, voice, model, key, text});
break;
case 'nvidia':
audioData = await synthNvidia(client, logger, {credentials, stats, language, voice, model, text,
renderForCaching, disableTtsStreaming});
audioData = await synthNvidia(client, logger, {credentials, stats, language, voice, model, key, text,
renderForCaching, disableTtsStreaming, disableTtsCache});
break;
case 'ibm':
audioData = await synthIbm(logger, {credentials, stats, voice, text});
audioData = await synthIbm(logger, {credentials, stats, voice, key, text});
break;
case 'wellsaid':
audioData = await synthWellSaid(logger, {credentials, stats, language, voice, text});
audioData = await synthWellSaid(logger, {credentials, stats, language, voice, key, text});
break;
case 'elevenlabs':
audioData = await synthElevenlabs(logger, {
credentials, options, stats, language, voice, text, renderForCaching, disableTtsStreaming});
credentials, options, stats, language, voice, key, text, renderForCaching, disableTtsStreaming,
disableTtsCache});
break;
case 'playht':
audioData = await synthPlayHT(client, logger, {
credentials, options, stats, language, voice, text, renderForCaching, disableTtsStreaming});
credentials, options, stats, language, voice, key, text, renderForCaching, disableTtsStreaming,
disableTtsCache});
break;
case 'cartesia':
audioData = await synthCartesia(logger, {
credentials, options, stats, language, voice, text, renderForCaching, disableTtsStreaming});
credentials, options, stats, language, voice, key, text, renderForCaching, disableTtsStreaming,
disableTtsCache});
break;
case 'inworld':
audioData = await synthInworld(logger, {
credentials, options, stats, language, voice, key, text, renderForCaching, disableTtsStreaming,
disableTtsCache});
break;
case 'rimelabs':
audioData = await synthRimelabs(logger, {
credentials, options, stats, language, voice, text, renderForCaching, disableTtsStreaming});
credentials, options, stats, language, voice, key, text, renderForCaching, disableTtsStreaming,
disableTtsCache});
break;
case 'whisper':
audioData = await synthWhisper(logger, {
credentials, stats, voice, text, instructions, renderForCaching, disableTtsStreaming});
credentials, stats, voice, key, text, instructions, renderForCaching, disableTtsStreaming,
disableTtsCache});
break;
case 'verbio':
audioData = await synthVerbio(client, logger, {
credentials, stats, voice, text, renderForCaching, disableTtsStreaming});
credentials, stats, voice, key, text, renderForCaching, disableTtsStreaming, disableTtsCache});
if (audioData?.filePath) return audioData;
break;
case 'deepgram':
audioData = await synthDeepgram(logger, {credentials, stats, model, text,
renderForCaching, disableTtsStreaming});
audioData = await synthDeepgram(logger, {credentials, stats, model, key, text,
renderForCaching, disableTtsStreaming, disableTtsCache});
break;
case 'resemble':
audioData = await synthResemble(logger, {
credentials, stats, voice, key, text, options, renderForCaching, disableTtsStreaming, disableTtsCache});
break;
case vendor.startsWith('custom') ? vendor : 'cant_match_value':
audioData = await synthCustomVendor(logger,
{credentials, stats, language, voice, text});
{credentials, stats, language, voice, key, text, renderForCaching, disableTtsStreaming, disableTtsCache});
break;
default:
assert(`synthAudio: unsupported speech vendor ${vendor}`);
@@ -292,9 +313,43 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
}
const synthPolly = async(createHash, retrieveHash, logger,
{credentials, stats, language, voice, engine, text}) => {
{credentials, stats, language, voice, engine, key, text, renderForCaching, disableTtsStreaming, disableTtsCache}) => {
const {region, accessKeyId, secretAccessKey, roleArn} = credentials;
if (!JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
let params = '{';
params += `language=${language}`;
params += `,playback_id=${key}`;
params += `,write_cache_file=${disableTtsCache ? 0 : 1}`;
params += ',vendor=aws';
if (accessKeyId && secretAccessKey) {
if (accessKeyId) params += `,accessKeyId=${accessKeyId}`;
if (secretAccessKey) params += `,secretAccessKey=${secretAccessKey}`;
} else if (roleArn) {
const cred = await getAwsAuthToken(
logger, createHash, retrieveHash,
{
region,
roleArn
});
if (cred) {
params += `,accessKeyId=${cred.accessKeyId}`;
params += `,secretAccessKey=${cred.secretAccessKey}`;
params += `,sessionToken=${cred.sessionToken}`;
}
}
if (region) params += `,region=${region}`;
if (engine) params += `,engine=${engine}`;
params += '}';
return {
filePath: `say:${params}${text.replace(/\n/g, ' ').replace(/\r/g, ' ')}`,
servedFromCache: false,
rtt: 0
};
}
try {
const {region, accessKeyId, secretAccessKey, roleArn} = credentials;
let polly;
if (accessKeyId && secretAccessKey) {
polly = new PollyClient({
@@ -512,9 +567,11 @@ const synthMicrosoft = async(logger, {
stats,
language,
voice,
key,
text,
renderForCaching,
disableTtsStreaming
disableTtsStreaming,
disableTtsCache
}) => {
try {
const {api_key: apiKey, region, use_custom_tts, custom_tts_endpoint, custom_tts_endpoint_url} = credentials;
@@ -541,12 +598,13 @@ const synthMicrosoft = async(logger, {
}
if (!JAMBONES_DISABLE_TTS_STREAMING && !JAMBONES_DISABLE_AZURE_TTS_STREAMING &&
!renderForCaching && !disableTtsStreaming) {
let params = '';
params += `{api_key=${apiKey}`;
let params = '{';
params += `api_key=${apiKey}`;
params += `,playback_id=${key}`;
params += `,language=${language}`;
params += ',vendor=microsoft';
params += `,voice=${voice}`;
params += ',write_cache_file=1';
params += `,write_cache_file=${disableTtsCache ? 0 : 1}`;
if (region) params += `,region=${region}`;
if (custom_tts_endpoint) params += `,endpointId=${custom_tts_endpoint}`;
if (custom_tts_endpoint_url) params += `,endpoint=${custom_tts_endpoint_url}`;
@@ -719,15 +777,16 @@ const synthNuance = async(client, logger, {credentials, stats, voice, model, tex
};
const synthNvidia = async(client, logger, {
credentials, stats, language, voice, model, text, renderForCaching, disableTtsStreaming
credentials, stats, language, voice, model, key, text, renderForCaching, disableTtsStreaming, disableTtsCache
}) => {
const {riva_server_uri} = credentials;
if (!JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
let params = '';
params += `{riva_server_uri=${riva_server_uri}`;
params += `,playback_id=${key}`;
params += `,voice=${voice}`;
params += `,language=${language}`;
params += ',write_cache_file=1';
params += `,write_cache_file=${disableTtsCache ? 0 : 1}`;
params += '}';
return {
@@ -767,9 +826,27 @@ const synthNvidia = async(client, logger, {
};
const synthCustomVendor = async(logger, {credentials, stats, language, voice, text, filePath}) => {
const synthCustomVendor = async(logger, {credentials, stats, language, voice,
text, filePath, renderForCaching, disableTtsStreaming, key, disableTtsCache}) => {
const {vendor, auth_token, custom_tts_url} = credentials;
if (!JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
let params = '{';
params += `auth_token=${auth_token}`;
params += `,playback_id=${key}`;
params += `,custom_tts_url=${custom_tts_url}`;
params += ',vendor=custom';
params += `,voice=${voice}`;
params += `,write_cache_file=${disableTtsCache ? 0 : 1}`;
params += '}';
return {
filePath: `say:${params}${text.replace(/\n/g, ' ').replace(/\r/g, ' ')}`,
servedFromCache: false,
rtt: 0
};
}
try {
const post = bent('POST', {
'Authorization': `Bearer ${auth_token}`,
@@ -798,20 +875,24 @@ const synthCustomVendor = async(logger, {credentials, stats, language, voice, te
};
const synthElevenlabs = async(logger, {
credentials, options, stats, voice, text, renderForCaching, disableTtsStreaming
credentials, options, stats, voice, key, text, renderForCaching, disableTtsStreaming, disableTtsCache
}) => {
const {api_key, model_id, options: credOpts} = credentials;
const {api_key, model_id, api_uri, 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}`;
let params = '{';
params += `api_key=${api_key}`;
params += `,playback_id=${key}`;
params += ',vendor=elevenlabs';
params += `,voice=${voice}`;
params += `,model_id=${model_id}`;
params += `,optimize_streaming_latency=${opts.optimize_streaming_latency || 2}`;
params += ',write_cache_file=1';
params += `,write_cache_file=${disableTtsCache ? 0 : 1}`;
if (api_uri) params += `,api_uri=${api_uri}`;
if (opts.optimize_streaming_latency !== null && opts.optimize_streaming_latency !== undefined) {
params += `,optimize_streaming_latency=${opts.optimize_streaming_latency}`;
}
if (opts.voice_settings?.similarity_boost) params += `,similarity_boost=${opts.voice_settings.similarity_boost}`;
if (opts.voice_settings?.stability) params += `,stability=${opts.voice_settings.stability}`;
if (opts.voice_settings?.style) params += `,style=${opts.voice_settings.style}`;
@@ -834,7 +915,7 @@ const synthElevenlabs = async(logger, {
const optimize_streaming_latency = opts.optimize_streaming_latency ?
`?optimize_streaming_latency=${opts.optimize_streaming_latency}` : '';
try {
const post = bent('https://api.elevenlabs.io', 'POST', 'buffer', {
const post = bent(`https://${api_uri}`, 'POST', 'buffer', {
'xi-api-key': api_key,
'Accept': 'audio/mpeg',
'Content-Type': 'application/json'
@@ -861,7 +942,7 @@ const synthElevenlabs = async(logger, {
};
const synthPlayHT = async(client, logger, {
credentials, options, stats, voice, language, text, renderForCaching, disableTtsStreaming
credentials, options, stats, voice, language, key, text, renderForCaching, disableTtsStreaming, disableTtsCache
}) => {
const {api_key, user_id, voice_engine, playht_tts_uri, options: credOpts} = credentials;
const opts = !!options && Object.keys(options).length !== 0 ? options : JSON.parse(credOpts || '{}');
@@ -895,14 +976,15 @@ const synthPlayHT = async(client, logger, {
/* 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}`;
let params = '{';
params += `api_key=${api_key}`;
params += `,playback_id=${key}`;
params += `,user_id=${user_id}`;
params += ',vendor=playht';
params += `,voice=${voice}`;
params += `,voice_engine=${voice_engine}`;
params += `,synthesize_url=${synthesizeUrl}`;
params += ',write_cache_file=1';
params += `,write_cache_file=${disableTtsCache ? 0 : 1}`;
params += `,language=${language}`;
if (opts.quality) params += `,quality=${opts.quality}`;
if (opts.speed) params += `,speed=${opts.speed}`;
@@ -954,21 +1036,87 @@ const synthPlayHT = async(client, logger, {
}
};
const synthRimelabs = async(logger, {
credentials, options, stats, language, voice, text, renderForCaching, disableTtsStreaming
const synthInworld = async(logger, {
credentials, options, stats, voice, key, text, renderForCaching, disableTtsStreaming, disableTtsCache
}) => {
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}`;
let params = '{';
params += `api_key=${api_key}`;
params += `,playback_id=${key}`;
params += `,model_id=${model_id}`;
params += ',vendor=inworld';
params += `,voice=${voice}`;
params += `,write_cache_file=${disableTtsCache ? 0 : 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, key, text, renderForCaching, disableTtsStreaming, disableTtsCache
}) => {
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 += `,playback_id=${key}`;
params += `,model_id=${model_id}`;
params += ',vendor=rimelabs';
params += `,language=${language}`;
params += `,voice=${voice}`;
params += ',write_cache_file=1';
params += `,write_cache_file=${disableTtsCache ? 0 : 1}`;
if (opts.speedAlpha) params += `,speed_alpha=${opts.speedAlpha}`;
if (opts.reduceLatency) params += `,reduce_latency=${opts.reduceLatency}`;
// Arcana model parameters
@@ -1011,18 +1159,21 @@ const synthRimelabs = async(logger, {
throw err;
}
};
const synthVerbio = async(client, logger, {credentials, stats, voice, text, renderForCaching, disableTtsStreaming}) => {
const synthVerbio = async(client, logger, {
credentials, stats, voice, key, text, renderForCaching, disableTtsStreaming, disableTtsCache
}) => {
//https://doc.speechcenter.verbio.com/#tag/Text-To-Speech-REST-API
if (text.length > 2000) {
throw new Error('Verbio cannot synthesize for the text length larger than 2000 characters');
}
const token = await getVerbioAccessToken(client, logger, credentials);
if (!process.env.JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
let params = '';
params += `{access_token=${token.access_token}`;
let params = '{';
params += `access_token=${token.access_token}`;
params += `,playback_id=${key}`;
params += ',vendor=verbio';
params += `,voice=${voice}`;
params += ',write_cache_file=1';
params += `,write_cache_file=${disableTtsCache ? 0 : 1}`;
params += '}';
return {
@@ -1056,17 +1207,18 @@ const synthVerbio = async(client, logger, {credentials, stats, voice, text, rend
}
};
const synthWhisper = async(logger, {credentials, stats, voice, text, instructions,
renderForCaching, disableTtsStreaming}) => {
const synthWhisper = async(logger, {credentials, stats, voice, key, text, instructions,
renderForCaching, disableTtsStreaming, disableTtsCache}) => {
const {api_key, model_id, baseURL, timeout, speed} = credentials;
/* if the env is set to stream then bag out, unless we are specifically rendering to generate a cache file */
if (!JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
let params = '';
params += `{api_key=${api_key}`;
let params = '{';
params += `api_key=${api_key}`;
params += `,playback_id=${key}`;
params += `,model_id=${model_id}`;
params += ',vendor=whisper';
params += `,voice=${voice}`;
params += ',write_cache_file=1';
params += `,write_cache_file=${disableTtsCache ? 0 : 1}`;
if (speed) params += `,speed=${speed}`;
// comma is used to separated parameters in freeswitch tts module
if (instructions) params += `,instructions=${instructions.replace(/\n/g, ' ').replace(/,/g, ';')}`;
@@ -1104,14 +1256,16 @@ const synthWhisper = async(logger, {credentials, stats, voice, text, instruction
}
};
const synthDeepgram = async(logger, {credentials, stats, model, text, renderForCaching, disableTtsStreaming}) => {
const synthDeepgram = async(logger, {credentials, stats, model, key, text, renderForCaching,
disableTtsStreaming, disableTtsCache}) => {
const {api_key, deepgram_tts_uri} = credentials;
if (!JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
let params = '';
params += `{api_key=${api_key}`;
let params = '{';
params += `api_key=${api_key}`;
params += `,playback_id=${key}`;
params += ',vendor=deepgram';
params += `,voice=${model}`;
params += ',write_cache_file=1';
params += `,write_cache_file=${disableTtsCache ? 0 : 1}`;
if (deepgram_tts_uri) params += `,endpoint=${deepgram_tts_uri}`;
params += '}';
@@ -1144,23 +1298,25 @@ const synthDeepgram = async(logger, {credentials, stats, model, text, renderForC
};
const synthCartesia = async(logger, {
credentials, options, stats, voice, language, text, renderForCaching, disableTtsStreaming
credentials, options, stats, voice, language, key, text, renderForCaching, disableTtsStreaming, disableTtsCache
}) => {
const {api_key, model_id, embedding, options: credOpts} = credentials;
const opts = !!options && Object.keys(options).length !== 0 ? options : JSON.parse(credOpts || '{}');
if (!JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
let params = '';
params += `{api_key=${api_key}`;
let params = '{';
params += `api_key=${api_key}`;
params += `,playback_id=${key}`;
params += `,model_id=${model_id}`;
params += ',vendor=cartesia';
params += `,voice=${voice}`;
params += ',write_cache_file=1';
params += `,write_cache_file=${disableTtsCache ? 0 : 1}`;
params += `,language=${language}`;
params += `,voice_mode=${embedding ? 'embedding' : 'id'}`;
if (embedding) params += `,embedding=${embedding}`;
if (opts.speed) params += `,speed=${opts.speed}`;
if (opts.emotion) params += `,emotion=${opts.emotion}`;
if (opts.volume) params += `,volume=${opts.volume}`;
params += '}';
return {
@@ -1186,13 +1342,20 @@ const synthCartesia = async(logger, {
id: voice
}
),
...(opts.speed || opts.emotion && {
...(model_id === 'sonic-2' && (opts.speed || opts.emotion) && {
experimentalControls: {
...(opts.speed !== null && opts.speed !== undefined && {speed: opts.speed}),
...(opts.emotion && {emotion: opts.emotion}),
...(opts.emotion && {emotion: [opts.emotion]}),
}
})
}),
},
...(model_id === 'sonic-3' && (opts.speed || opts.emotion || opts.volume) && {
generationConfig: {
...(opts.volume !== null && opts.volume !== undefined && {volume: opts.volume}),
...(opts.speed !== null && opts.speed !== undefined && {speed: opts.speed}),
...(opts.emotion !== null && opts.emotion !== undefined && {emotion: opts.emotion}),
}
}),
language: language,
outputFormat: {
container: 'mp3',
@@ -1213,6 +1376,63 @@ const synthCartesia = async(logger, {
};
const synthResemble = async(logger, {
credentials, options, stats, voice, key, text, renderForCaching, disableTtsStreaming, disableTtsCache
}) => {
const {api_key, resemble_tts_uri, resemble_tts_use_tls} = credentials;
const {project_uuid, use_hd} = options || {};
/* 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 += `,playback_id=${key}`;
params += ',vendor=resemble';
params += `,voice=${voice}`;
params += `,write_cache_file=${disableTtsCache ? 0 : 1}`;
if (project_uuid) params += `,project_uuid=${project_uuid}`;
if (use_hd) params += `,use_hd=${use_hd}`;
if (resemble_tts_uri) params += `,endpoint=${resemble_tts_uri}`;
if (resemble_tts_use_tls) params += `,use_tls=${resemble_tts_use_tls}`;
params += '}';
return {
filePath: `say:${params}${text.replace(/\n/g, ' ').replace(/\r/g, ' ')}`,
servedFromCache: false,
rtt: 0
};
}
try {
const baseUrl = resemble_tts_uri || 'https://f.cluster.resemble.ai';
const post = bent(baseUrl, 'POST', 'buffer', {
'Authorization': `Bearer ${api_key}`,
'Content-Type': 'application/json'
});
const response = await post('/synthesize', {
voice_uuid: voice,
data: text,
sample_rate: 8000,
output_format: 'mp3',
...(project_uuid && {project_uuid}),
...(use_hd && {use_hd}),
});
const json = JSON.parse(response.toString('utf8'));
const audioContent = Buffer.from(json.audio_content, 'base64');
return {
audioContent,
extension: 'mp3',
sampleRate: 8000
};
} catch (err) {
logger.info({err}, 'synth Elevenlabs returned error');
stats.increment('tts.count', ['vendor:elevenlabs', 'accepted:no']);
throw err;
}
};
const getFileExtFromMime = (mime) => {
switch (mime) {
case 'audio/wav':
+494 -678
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@jambonz/speech-utils",
"version": "0.2.12",
"version": "0.2.26",
"description": "TTS-related speech utilities for jambonz",
"main": "index.js",
"author": "Dave Horton",
@@ -29,7 +29,7 @@
"23": "^0.0.0",
"@aws-sdk/client-polly": "^3.496.0",
"@aws-sdk/client-sts": "^3.496.0",
"@cartesia/cartesia-js": "^2.1.0",
"@cartesia/cartesia-js": "^2.2.7",
"@google-cloud/text-to-speech": "^5.5.0",
"@grpc/grpc-js": "^1.9.14",
"@jambonz/realtimedb-helpers": "^0.8.7",
@@ -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"
+64
View File
@@ -185,6 +185,7 @@ test('AWS speech synth tests', async(t) => {
language: 'en-US',
voice: 'Joey',
text: 'This is a test. This is only a test',
renderForCaching: true,
});
t.ok(!opts.servedFromCache, `successfully synthesized aws audio to ${opts.filePath}`);
@@ -198,6 +199,7 @@ test('AWS speech synth tests', async(t) => {
language: 'en-US',
voice: 'Joey',
text: 'This is a test. This is only a test',
renderForCaching: true,
});
t.ok(opts.servedFromCache, `successfully retrieved aws audio from cache ${opts.filePath}`);
} catch (err) {
@@ -552,6 +554,7 @@ test('Custom Vendor speech synth tests', async(t) => {
language: 'en-US',
voice: 'English-US.Female-1',
text: 'This is a test. This is only a test',
renderForCaching: true
});
t.ok(!opts.servedFromCache, `successfully synthesized custom vendor audio to ${opts.filePath}`);
t.ok(opts.filePath.endsWith('wav'), 'audio is cached as wav file');
@@ -573,6 +576,7 @@ test('Custom Vendor speech synth tests', async(t) => {
language: 'en-US',
voice: 'English-US.Female-1',
text: 'This is a test. This is only a test',
renderForCaching: true
});
t.ok(opts.servedFromCache, `successfully get custom vendor cached audio to ${opts.filePath}`);
t.ok(opts.filePath.endsWith('wav'), 'audio is cached as wav file');
@@ -587,6 +591,7 @@ test('Custom Vendor speech synth tests', async(t) => {
language: 'en-US',
voice: 'English-US.Female-1',
text: '<speak>This is a test. This is only a test</speak>',
renderForCaching: true
});
t.ok(!opts.servedFromCache, `successfully synthesized Custom Vendor audio to ${opts.filePath}`);
obj = await getJSON(`http://127.0.0.1:3100/lastRequest/somethingnew2`);
@@ -720,6 +725,65 @@ 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('resemble speech synth', async(t) => {
const fn = require('..');
const {synthAudio, client} = fn(opts, logger);
if (!process.env.RESEMBLE_API_KEY) {
t.pass('skipping resemble speech synth tests since RESEMBLE_API_KEY is not provided');
return t.end();
}
const text = '<speak prompt="Speak in an excited, upbeat tone">Hello from Resemble!</speak>';
try {
const opts = await synthAudio(stats, {
vendor: 'resemble',
credentials: {
api_key: process.env.RESEMBLE_API_KEY,
},
language: 'en',
voice: '3f5fb9f1',
text,
renderForCaching: true
});
t.ok(!opts.servedFromCache, `successfully synthesized resemble 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);