Compare commits

...
92 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
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
Dave Horton 6fb6195b16 0.2.11 2025-05-27 10:09:01 -04:00
Dave Horton 2ff8587601 Merge pull request #113 from vasudevanubrolu/feat/893-azure-ssml
Feat/893 azure ssml
2025-05-27 10:08:48 -04:00
vasudevanubrolu 925bd26a70 feat/893 add add lang tag for accent to be picked 2025-05-27 13:41:07 +05:30
vasudevanubrolu ddea485f5f feat/893 azure ssml lang for on prem 2025-05-26 15:13:45 +05:30
vasudevanubrolu 49de25feb8 feat/893 ssml config 2025-05-26 12:10:22 +05:30
vasudevanubrolu 08b55b8d79 feat/893 support default azure ssml config 2025-05-26 12:10:22 +05:30
vasudevanubrolu 59bca302b9 feat/893 azure ssml based on env config 2025-05-26 12:10:22 +05:30
Dave Horton 0d98f73c43 0.2.10 2025-05-13 09:57:01 -04:00
Dave Horton 36670e0080 Merge pull request #111 from vasudevanubrolu/feat/864-playht-onprem
feat/864 playht on prem
2025-05-13 09:50:46 -04:00
vasudevanubrolu 61672f9868 feat/864 playht on prem pr changes 2025-05-13 18:47:26 +05:30
vasudevan-Kore ddeca8eb99 feat/864 playht on prem 2025-05-13 18:47:26 +05:30
Dave Horton 6e8271b2f6 0.2.9 2025-05-13 07:47:03 -04:00
Dave Horton 74a4938eb6 Merge pull request #112 from jambonz/feat/whisper_instructions
support openai whisper instructions
2025-05-13 07:46:34 -04:00
Quan HL cb50f603cd wip 2025-05-13 18:02:11 +07:00
Quan HL 9a524f00bc wip 2025-05-13 17:58:57 +07:00
Quan HL 545df0b770 wip 2025-05-13 17:55:41 +07:00
Quan HL 9409405769 support openai whisper instructions 2025-05-13 15:23:59 +07:00
Dave Horton 7dc3bbdb01 0.2.8 2025-05-08 09:31:40 -04:00
Dave Horton f8f9de2645 Merge pull request #110 from jambonz/feat/rimlabs_arcana
support rimelabs arcana
2025-05-06 09:22:35 -04:00
Quan HL 467d7ede26 support rimelabs arcana 2025-05-06 16:16:18 +07:00
Dave Horton fc211ab2e7 0.2.7 2025-04-28 19:31:59 -04:00
Dave Horton 536f8aab31 Merge pull request #109 from jambonz/feat/riva_tts
support riva tts stream
2025-04-28 19:31:30 -04:00
Hoan Luu Huu 69b3fdffbe Merge branch 'main' into feat/riva_tts 2025-04-28 08:47:44 +07:00
Dave Horton 53fe72d89e 0.2.6 2025-04-23 07:12:54 -04:00
Dave Horton e79c15c5da update version 2025-04-23 07:12:25 -04:00
Hoan Luu Huu a4a427e174 Merge branch 'main' into feat/riva_tts 2025-04-23 18:11:19 +07:00
Dave Horton b697bc5268 Merge pull request #108 from jambonz/feat/ell_tts_new_params
elevenlabs tts speed and pronunciation_dictionary_locators
2025-04-23 07:08:49 -04:00
Quan HL d35d7f0aec support riva tts stream 2025-04-23 17:54:21 +07:00
Quan HL ed1c564fa2 wip 2025-04-04 16:15:45 +07:00
Quan HL 7189d471c1 elevenlabs tts speed and pronunciation_dictionary_locators 2025-04-04 15:44:29 +07:00
Dave Horton 04080cc5ec 0.2.4 2025-03-19 21:48:31 -04:00
Dave Horton 3e5ab4af27 Merge pull request #107 from jambonz/update-deps
update undici
2025-03-19 21:48:02 -04:00
Dave Horton f06dddd2f7 update undici 2025-03-19 21:46:21 -04:00
9 changed files with 1109 additions and 1287 deletions
+1 -1
View File
@@ -11,7 +11,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: lts/* node-version: '20'
- run: npm install - run: npm install
- name: Install Docker Compose - name: Install Docker Compose
run: | run: |
+2 -1
View File
@@ -25,7 +25,7 @@ function getExtensionAndSampleRate(path) {
} }
async function addFileToCache(client, logger, path, async function addFileToCache(client, logger, path,
{account_sid, vendor, language, voice, deploymentId, engine, model, text}) { {account_sid, vendor, language, voice, deploymentId, engine, model, text, instructions}) {
let key; let key;
logger = logger || noopLogger; logger = logger || noopLogger;
@@ -38,6 +38,7 @@ async function addFileToCache(client, logger, path,
engine, engine,
model, model,
text, text,
instructions
}); });
const [extension, sampleRate] = getExtensionAndSampleRate(path); const [extension, sampleRate] = getExtensionAndSampleRate(path);
const audioBuffer = await fs.readFile(path); const audioBuffer = await fs.readFile(path);
+4 -2
View File
@@ -2,13 +2,14 @@ const JAMBONES_TTS_TRIM_SILENCE = process.env.JAMBONES_TTS_TRIM_SILENCE;
const JAMBONES_DISABLE_TTS_STREAMING = process.env.JAMBONES_DISABLE_TTS_STREAMING; const JAMBONES_DISABLE_TTS_STREAMING = process.env.JAMBONES_DISABLE_TTS_STREAMING;
const JAMBONES_DISABLE_AZURE_TTS_STREAMING = process.env.JAMBONES_DISABLE_AZURE_TTS_STREAMING; const JAMBONES_DISABLE_AZURE_TTS_STREAMING = process.env.JAMBONES_DISABLE_AZURE_TTS_STREAMING;
const JAMBONES_EAGERLY_PRE_CACHE_AUDIO = process.env.JAMBONES_EAGERLY_PRE_CACHE_AUDIO; const JAMBONES_EAGERLY_PRE_CACHE_AUDIO = process.env.JAMBONES_EAGERLY_PRE_CACHE_AUDIO;
const JAMBONES_AZURE_ENABLE_SSML = process.env.JAMBONES_AZURE_ENABLE_SSML;
const JAMBONES_HTTP_PROXY_IP = process.env.JAMBONES_HTTP_PROXY_IP; const JAMBONES_HTTP_PROXY_IP = process.env.JAMBONES_HTTP_PROXY_IP;
const JAMBONES_HTTP_PROXY_PORT = process.env.JAMBONES_HTTP_PROXY_PORT; const JAMBONES_HTTP_PROXY_PORT = process.env.JAMBONES_HTTP_PROXY_PORT;
const JAMBONES_TTS_CACHE_DURATION_MINS = const JAMBONES_TTS_CACHE_DURATION_MINS =
(parseInt(process.env.JAMBONES_TTS_CACHE_DURATION_MINS) || 4 * 60) * 60; // cache tts for 4 hours (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; const HTTP_TIMEOUT = 5000;
@@ -21,5 +22,6 @@ module.exports = {
JAMBONES_TTS_CACHE_DURATION_MINS, JAMBONES_TTS_CACHE_DURATION_MINS,
JAMBONES_EAGERLY_PRE_CACHE_AUDIO, JAMBONES_EAGERLY_PRE_CACHE_AUDIO,
TMP_FOLDER, TMP_FOLDER,
HTTP_TIMEOUT HTTP_TIMEOUT,
JAMBONES_AZURE_ENABLE_SSML
}; };
+2 -1
View File
@@ -12,7 +12,7 @@ const debug = require('debug')('jambonz:realtimedb-helpers');
* @returns {object} result - {error, purgedCount} * @returns {object} result - {error, purgedCount}
*/ */
async function purgeTtsCache(client, logger, {all, account_sid, vendor, async function purgeTtsCache(client, logger, {all, account_sid, vendor,
language, voice, deploymentId, engine, model, text} = {all: true}) { language, voice, deploymentId, engine, model, text, instructions} = {all: true}) {
logger = logger || noopLogger; logger = logger || noopLogger;
let purgedCount = 0, error; let purgedCount = 0, error;
@@ -35,6 +35,7 @@ async function purgeTtsCache(client, logger, {all, account_sid, vendor,
engine, engine,
model, model,
text, text,
instructions
}); });
purgedCount = await client.del(key); purgedCount = await client.del(key);
if (purgedCount === 0) error = 'Specified item not found'; if (purgedCount === 0) error = 'Specified item not found';
+330 -72
View File
@@ -47,12 +47,12 @@ const {
JAMBONES_HTTP_PROXY_PORT, JAMBONES_HTTP_PROXY_PORT,
JAMBONES_TTS_CACHE_DURATION_MINS, JAMBONES_TTS_CACHE_DURATION_MINS,
JAMBONES_TTS_TRIM_SILENCE, JAMBONES_TTS_TRIM_SILENCE,
JAMBONES_AZURE_ENABLE_SSML
} = require('./config'); } = require('./config');
const EXPIRES = JAMBONES_TTS_CACHE_DURATION_MINS; const EXPIRES = JAMBONES_TTS_CACHE_DURATION_MINS;
const OpenAI = require('openai'); const OpenAI = require('openai');
const getAwsAuthToken = require('./get-aws-sts-token'); const getAwsAuthToken = require('./get-aws-sts-token');
const trimTrailingSilence = (buffer) => { const trimTrailingSilence = (buffer) => {
assert.ok(buffer instanceof Buffer, 'trimTrailingSilence - argument is not a Buffer'); assert.ok(buffer instanceof Buffer, 'trimTrailingSilence - argument is not a Buffer');
@@ -89,7 +89,7 @@ const trimTrailingSilence = (buffer) => {
*/ */
async function synthAudio(client, createHash, retrieveHash, logger, stats, { account_sid, async function synthAudio(client, createHash, retrieveHash, logger, stats, { account_sid,
vendor, language, voice, gender, text, engine, salt, model, credentials, deploymentId, vendor, language, voice, gender, text, engine, salt, model, credentials, deploymentId,
disableTtsCache, renderForCaching = false, disableTtsStreaming, options disableTtsCache, renderForCaching = false, disableTtsStreaming, options, instructions
}) { }) {
let audioData; let audioData;
let servedFromCache = false; let servedFromCache = false;
@@ -97,7 +97,7 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
logger = logger || noopLogger; logger = logger || noopLogger;
assert.ok(['google', 'aws', 'polly', 'microsoft', 'wellsaid', 'nuance', 'nvidia', 'ibm', 'elevenlabs', 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'), vendor.startsWith('custom'),
`synthAudio supported vendors are google, aws, microsoft, nuance, nvidia and wellsaid ..etc, not ${vendor}`); `synthAudio supported vendors are google, aws, microsoft, nuance, nvidia and wellsaid ..etc, not ${vendor}`);
if ('google' === vendor) { if ('google' === vendor) {
@@ -140,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.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.user_id, 'synthAudio requires user_id when playht is used');
assert.ok(credentials.voice_engine, 'synthAudio requires voice_engine 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) { } else if ('rimelabs' === vendor) {
assert.ok(voice, 'synthAudio requires voice when rimelabs is used'); assert.ok(voice, 'synthAudio requires voice when rimelabs is used');
assert.ok(credentials.api_key, 'synthAudio requires api_key when rimelabs is used'); assert.ok(credentials.api_key, 'synthAudio requires api_key when rimelabs is used');
@@ -161,6 +165,9 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
} else if ('cartesia' === vendor) { } else if ('cartesia' === vendor) {
assert.ok(credentials.api_key, 'synthAudio requires api_key when cartesia is used'); 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'); 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({ const key = makeSynthKey({
@@ -171,7 +178,8 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
engine, engine,
// model or model_id is used to identify the tts cache. // model or model_id is used to identify the tts cache.
model: model || credentials.model_id, model: model || credentials.model_id,
text text,
instructions
}); });
debug(`synth key is ${key}`); debug(`synth key is ${key}`);
@@ -196,65 +204,81 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
const startAt = process.hrtime(); const startAt = process.hrtime();
switch (vendor) { switch (vendor) {
case 'google': case 'google':
audioData = await synthGoogle(logger, {credentials, stats, language, voice, gender, text}); audioData = await synthGoogle(logger, {credentials, stats, language, voice, gender, key, text});
break; break;
case 'aws': case 'aws':
case 'polly': case 'polly':
vendorLabel = 'aws'; vendorLabel = 'aws';
audioData = await synthPolly(createHash, retrieveHash, logger, audioData = await synthPolly(createHash, retrieveHash, logger,
{credentials, stats, language, voice, text, engine}); {credentials, stats, language, voice, key, text, engine, renderForCaching, disableTtsStreaming,
disableTtsCache});
break; break;
case 'azure': case 'azure':
case 'microsoft': case 'microsoft':
vendorLabel = 'microsoft'; vendorLabel = 'microsoft';
audioData = await synthMicrosoft(logger, {credentials, stats, language, voice, text, deploymentId, audioData = await synthMicrosoft(logger, {credentials, stats, language, voice, key, text, deploymentId,
renderForCaching, disableTtsStreaming}); renderForCaching, disableTtsStreaming, disableTtsCache});
break; break;
case 'nuance': case 'nuance':
model = model || 'enhanced'; 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; break;
case 'nvidia': case 'nvidia':
audioData = await synthNvidia(client, logger, {credentials, stats, language, voice, model, text}); audioData = await synthNvidia(client, logger, {credentials, stats, language, voice, model, key, text,
renderForCaching, disableTtsStreaming, disableTtsCache});
break; break;
case 'ibm': case 'ibm':
audioData = await synthIbm(logger, {credentials, stats, voice, text}); audioData = await synthIbm(logger, {credentials, stats, voice, key, text});
break; break;
case 'wellsaid': case 'wellsaid':
audioData = await synthWellSaid(logger, {credentials, stats, language, voice, text}); audioData = await synthWellSaid(logger, {credentials, stats, language, voice, key, text});
break; break;
case 'elevenlabs': case 'elevenlabs':
audioData = await synthElevenlabs(logger, { audioData = await synthElevenlabs(logger, {
credentials, options, stats, language, voice, text, renderForCaching, disableTtsStreaming}); credentials, options, stats, language, voice, key, text, renderForCaching, disableTtsStreaming,
disableTtsCache});
break; break;
case 'playht': case 'playht':
audioData = await synthPlayHT(client, logger, { audioData = await synthPlayHT(client, logger, {
credentials, options, stats, language, voice, text, renderForCaching, disableTtsStreaming}); credentials, options, stats, language, voice, key, text, renderForCaching, disableTtsStreaming,
disableTtsCache});
break; break;
case 'cartesia': case 'cartesia':
audioData = await synthCartesia(logger, { 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; break;
case 'rimelabs': case 'rimelabs':
audioData = await synthRimelabs(logger, { audioData = await synthRimelabs(logger, {
credentials, options, stats, language, voice, text, renderForCaching, disableTtsStreaming}); credentials, options, stats, language, voice, key, text, renderForCaching, disableTtsStreaming,
disableTtsCache});
break; break;
case 'whisper': case 'whisper':
audioData = await synthWhisper(logger, { audioData = await synthWhisper(logger, {
credentials, stats, voice, text, renderForCaching, disableTtsStreaming}); credentials, stats, voice, key, text, instructions, renderForCaching, disableTtsStreaming,
disableTtsCache});
break; break;
case 'verbio': case 'verbio':
audioData = await synthVerbio(client, logger, { audioData = await synthVerbio(client, logger, {
credentials, stats, voice, text, renderForCaching, disableTtsStreaming}); credentials, stats, voice, key, text, renderForCaching, disableTtsStreaming, disableTtsCache});
if (audioData?.filePath) return audioData; if (audioData?.filePath) return audioData;
break; break;
case 'deepgram': case 'deepgram':
audioData = await synthDeepgram(logger, {credentials, stats, model, text, audioData = await synthDeepgram(logger, {credentials, stats, model, key, text,
renderForCaching, disableTtsStreaming}); renderForCaching, disableTtsStreaming, disableTtsCache});
break;
case 'resemble':
audioData = await synthResemble(logger, {
credentials, stats, voice, key, text, options, renderForCaching, disableTtsStreaming, disableTtsCache});
break; break;
case vendor.startsWith('custom') ? vendor : 'cant_match_value': case vendor.startsWith('custom') ? vendor : 'cant_match_value':
audioData = await synthCustomVendor(logger, audioData = await synthCustomVendor(logger,
{credentials, stats, language, voice, text}); {credentials, stats, language, voice, key, text, renderForCaching, disableTtsStreaming, disableTtsCache});
break; break;
default: default:
assert(`synthAudio: unsupported speech vendor ${vendor}`); assert(`synthAudio: unsupported speech vendor ${vendor}`);
@@ -289,9 +313,43 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
} }
const synthPolly = async(createHash, retrieveHash, logger, 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 { try {
const {region, accessKeyId, secretAccessKey, roleArn} = credentials;
let polly; let polly;
if (accessKeyId && secretAccessKey) { if (accessKeyId && secretAccessKey) {
polly = new PollyClient({ polly = new PollyClient({
@@ -464,7 +522,6 @@ async function _synthOnPremMicrosoft(logger, {
}) { }) {
const {use_custom_tts, custom_tts_endpoint_url, api_key} = credentials; const {use_custom_tts, custom_tts_endpoint_url, api_key} = credentials;
let content = text; let content = text;
if (use_custom_tts && !content.startsWith('<speak')) { if (use_custom_tts && !content.startsWith('<speak')) {
/** /**
* Note: it seems that to use custom voice ssml is required with the voice attribute * Note: it seems that to use custom voice ssml is required with the voice attribute
@@ -480,6 +537,10 @@ async function _synthOnPremMicrosoft(logger, {
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>`; 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'); logger.info({content}, 'synthMicrosoft');
} }
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>`;
}
try { try {
const trimSilence = JAMBONES_TTS_TRIM_SILENCE; const trimSilence = JAMBONES_TTS_TRIM_SILENCE;
@@ -506,9 +567,11 @@ const synthMicrosoft = async(logger, {
stats, stats,
language, language,
voice, voice,
key,
text, text,
renderForCaching, renderForCaching,
disableTtsStreaming disableTtsStreaming,
disableTtsCache
}) => { }) => {
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;
@@ -516,27 +579,32 @@ const synthMicrosoft = async(logger, {
let content = text; let content = text;
if (use_custom_tts && !content.startsWith('<speak')) { if (use_custom_tts && !content.startsWith('<speak')) {
/** /**
* Note: it seems that to use custom voice ssml is required with the voice attribute * 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" * Otherwise sending plain text we get "Voice does not match"
*/ */
content = `<speak>${text}</speak>`; content = `<speak>${text}</speak>`;
} }
if (content.startsWith('<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 */ /* 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, ' '); const words = content.slice(7, -8).trim().replace(/(\r\n|\n|\r)/gm, ' ');
// eslint-disable-next-line max-len // 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>`; 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'); logger.info({content}, 'synthMicrosoft');
} }
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>`;
}
if (!JAMBONES_DISABLE_TTS_STREAMING && !JAMBONES_DISABLE_AZURE_TTS_STREAMING && if (!JAMBONES_DISABLE_TTS_STREAMING && !JAMBONES_DISABLE_AZURE_TTS_STREAMING &&
!renderForCaching && !disableTtsStreaming) { !renderForCaching && !disableTtsStreaming) {
let params = ''; let params = '{';
params += `{api_key=${apiKey}`; params += `api_key=${apiKey}`;
params += `,playback_id=${key}`;
params += `,language=${language}`; params += `,language=${language}`;
params += ',vendor=microsoft'; params += ',vendor=microsoft';
params += `,voice=${voice}`; params += `,voice=${voice}`;
params += ',write_cache_file=1'; params += `,write_cache_file=${disableTtsCache ? 0 : 1}`;
if (region) params += `,region=${region}`; if (region) params += `,region=${region}`;
if (custom_tts_endpoint) params += `,endpointId=${custom_tts_endpoint}`; if (custom_tts_endpoint) params += `,endpointId=${custom_tts_endpoint}`;
if (custom_tts_endpoint_url) params += `,endpoint=${custom_tts_endpoint_url}`; if (custom_tts_endpoint_url) params += `,endpoint=${custom_tts_endpoint_url}`;
@@ -708,8 +776,25 @@ const synthNuance = async(client, logger, {credentials, stats, voice, model, tex
}); });
}; };
const synthNvidia = async(client, logger, {credentials, stats, language, voice, model, text}) => { const synthNvidia = async(client, logger, {
credentials, stats, language, voice, model, key, text, renderForCaching, disableTtsStreaming, disableTtsCache
}) => {
const {riva_server_uri} = credentials; 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=${disableTtsCache ? 0 : 1}`;
params += '}';
return {
filePath: `say:${params}${text.replace(/\n/g, ' ')}`,
servedFromCache: false,
rtt: 0
};
}
let rivaClient, request; let rivaClient, request;
const sampleRate = 8000; const sampleRate = 8000;
try { try {
@@ -741,9 +826,27 @@ const synthNvidia = async(client, logger, {credentials, stats, language, voice,
}; };
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; 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 { try {
const post = bent('POST', { const post = bent('POST', {
'Authorization': `Bearer ${auth_token}`, 'Authorization': `Bearer ${auth_token}`,
@@ -772,26 +875,34 @@ const synthCustomVendor = async(logger, {credentials, stats, language, voice, te
}; };
const synthElevenlabs = async(logger, { 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 || '{}'); 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 */ /* 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) { if (!JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
let params = ''; let params = '{';
params += `{api_key=${api_key}`; params += `api_key=${api_key}`;
params += `,playback_id=${key}`;
params += ',vendor=elevenlabs'; params += ',vendor=elevenlabs';
params += `,voice=${voice}`; params += `,voice=${voice}`;
params += `,model_id=${model_id}`; params += `,model_id=${model_id}`;
params += `,optimize_streaming_latency=${opts.optimize_streaming_latency || 2}`; params += `,write_cache_file=${disableTtsCache ? 0 : 1}`;
params += ',write_cache_file=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?.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?.stability) params += `,stability=${opts.voice_settings.stability}`;
if (opts.voice_settings?.style) params += `,style=${opts.voice_settings.style}`; if (opts.voice_settings?.style) params += `,style=${opts.voice_settings.style}`;
if (opts.voice_settings?.speed !== null && opts.voice_settings?.speed !== undefined)
params += `,speed=${opts.voice_settings.speed}`;
if (opts.voice_settings?.use_speaker_boost === false) params += ',use_speaker_boost=false'; if (opts.voice_settings?.use_speaker_boost === false) params += ',use_speaker_boost=false';
if (opts.previous_text) params += `,previous_text=${opts.previous_text}`; if (opts.previous_text) params += `,previous_text=${opts.previous_text}`;
if (opts.next_text) params += `,next_text=${opts.next_text}`; if (opts.next_text) params += `,next_text=${opts.next_text}`;
if (opts.pronunciation_dictionary_locators && Array.isArray(opts.pronunciation_dictionary_locators))
params += `,pronunciation_dictionary_locators=${JSON.stringify(opts.pronunciation_dictionary_locators)}`;
params += '}'; params += '}';
return { return {
@@ -804,7 +915,7 @@ const synthElevenlabs = async(logger, {
const optimize_streaming_latency = opts.optimize_streaming_latency ? const optimize_streaming_latency = opts.optimize_streaming_latency ?
`?optimize_streaming_latency=${opts.optimize_streaming_latency}` : ''; `?optimize_streaming_latency=${opts.optimize_streaming_latency}` : '';
try { try {
const post = bent('https://api.elevenlabs.io', 'POST', 'buffer', { const post = bent(`https://${api_uri}`, 'POST', 'buffer', {
'xi-api-key': api_key, 'xi-api-key': api_key,
'Accept': 'audio/mpeg', 'Accept': 'audio/mpeg',
'Content-Type': 'application/json' 'Content-Type': 'application/json'
@@ -831,13 +942,12 @@ const synthElevenlabs = async(logger, {
}; };
const synthPlayHT = async(client, 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, options: credOpts} = credentials; 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 || '{}'); const opts = !!options && Object.keys(options).length !== 0 ? options : JSON.parse(credOpts || '{}');
let synthesizeUrl = 'https://api.play.ht/api/v2/tts/stream'; let synthesizeUrl = playht_tts_uri ? `${playht_tts_uri}/api/v2/tts/stream` : 'https://api.play.ht/api/v2/tts/stream';
// If model is play3.0, the synthesizeUrl is got from authentication endpoint // If model is play3.0, the synthesizeUrl is got from authentication endpoint
if (voice_engine === 'Play3.0') { if (voice_engine === 'Play3.0') {
try { try {
@@ -866,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 */ /* 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) { if (!JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
let params = ''; let params = '{';
params += `{api_key=${api_key}`; params += `api_key=${api_key}`;
params += `,playback_id=${key}`;
params += `,user_id=${user_id}`; params += `,user_id=${user_id}`;
params += ',vendor=playht'; params += ',vendor=playht';
params += `,voice=${voice}`; params += `,voice=${voice}`;
params += `,voice_engine=${voice_engine}`; params += `,voice_engine=${voice_engine}`;
params += `,synthesize_url=${synthesizeUrl}`; params += `,synthesize_url=${synthesizeUrl}`;
params += ',write_cache_file=1'; params += `,write_cache_file=${disableTtsCache ? 0 : 1}`;
params += `,language=${language}`; params += `,language=${language}`;
if (opts.quality) params += `,quality=${opts.quality}`; if (opts.quality) params += `,quality=${opts.quality}`;
if (opts.speed) params += `,speed=${opts.speed}`; if (opts.speed) params += `,speed=${opts.speed}`;
@@ -925,23 +1036,94 @@ const synthPlayHT = async(client, logger, {
} }
}; };
const synthRimelabs = async(logger, { const synthInworld = async(logger, {
credentials, options, stats, language, 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, options: credOpts} = credentials;
const opts = !!options && Object.keys(options).length !== 0 ? options : JSON.parse(credOpts || '{}'); 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 */ /* 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) { if (!JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
let params = ''; let params = '{';
params += `{api_key=${api_key}`; 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 += `,model_id=${model_id}`;
params += ',vendor=rimelabs'; params += ',vendor=rimelabs';
params += `,language=${language}`; params += `,language=${language}`;
params += `,voice=${voice}`; 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.speedAlpha) params += `,speed_alpha=${opts.speedAlpha}`;
if (opts.reduceLatency) params += `,reduce_latency=${opts.reduceLatency}`; if (opts.reduceLatency) params += `,reduce_latency=${opts.reduceLatency}`;
// Arcana model parameters
if (opts.temperature) params += `,temperature=${opts.temperature}`;
if (opts.repetition_penalty) params += `,repetition_penalty=${opts.repetition_penalty}`;
if (opts.top_p) params += `,top_p=${opts.top_p}`;
if (opts.max_tokens) params += `,max_tokens=${opts.max_tokens}`;
params += '}'; params += '}';
return { return {
@@ -977,18 +1159,21 @@ const synthRimelabs = async(logger, {
throw err; 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 //https://doc.speechcenter.verbio.com/#tag/Text-To-Speech-REST-API
if (text.length > 2000) { if (text.length > 2000) {
throw new Error('Verbio cannot synthesize for the text length larger than 2000 characters'); throw new Error('Verbio cannot synthesize for the text length larger than 2000 characters');
} }
const token = await getVerbioAccessToken(client, logger, credentials); const token = await getVerbioAccessToken(client, logger, credentials);
if (!process.env.JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) { if (!process.env.JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
let params = ''; let params = '{';
params += `{access_token=${token.access_token}`; params += `access_token=${token.access_token}`;
params += `,playback_id=${key}`;
params += ',vendor=verbio'; params += ',vendor=verbio';
params += `,voice=${voice}`; params += `,voice=${voice}`;
params += ',write_cache_file=1'; params += `,write_cache_file=${disableTtsCache ? 0 : 1}`;
params += '}'; params += '}';
return { return {
@@ -1022,17 +1207,21 @@ const synthVerbio = async(client, logger, {credentials, stats, voice, text, rend
} }
}; };
const synthWhisper = async(logger, {credentials, stats, voice, text, renderForCaching, disableTtsStreaming}) => { const synthWhisper = async(logger, {credentials, stats, voice, key, text, instructions,
renderForCaching, disableTtsStreaming, disableTtsCache}) => {
const {api_key, model_id, baseURL, timeout, speed} = credentials; 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 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) { if (!JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
let params = ''; let params = '{';
params += `{api_key=${api_key}`; params += `api_key=${api_key}`;
params += `,playback_id=${key}`;
params += `,model_id=${model_id}`; params += `,model_id=${model_id}`;
params += ',vendor=whisper'; params += ',vendor=whisper';
params += `,voice=${voice}`; params += `,voice=${voice}`;
params += ',write_cache_file=1'; params += `,write_cache_file=${disableTtsCache ? 0 : 1}`;
if (speed) params += `,speed=${speed}`; 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, ';')}`;
params += '}'; params += '}';
return { return {
@@ -1052,6 +1241,7 @@ const synthWhisper = async(logger, {credentials, stats, voice, text, renderForCa
model: model_id, model: model_id,
voice, voice,
input: text, input: text,
...(instructions && {instructions}),
response_format: 'mp3' response_format: 'mp3'
}); });
return { return {
@@ -1066,14 +1256,16 @@ const synthWhisper = async(logger, {credentials, stats, voice, text, renderForCa
} }
}; };
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; const {api_key, deepgram_tts_uri} = credentials;
if (!JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) { if (!JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
let params = ''; let params = '{';
params += `{api_key=${api_key}`; params += `api_key=${api_key}`;
params += `,playback_id=${key}`;
params += ',vendor=deepgram'; params += ',vendor=deepgram';
params += `,voice=${model}`; params += `,voice=${model}`;
params += ',write_cache_file=1'; params += `,write_cache_file=${disableTtsCache ? 0 : 1}`;
if (deepgram_tts_uri) params += `,endpoint=${deepgram_tts_uri}`; if (deepgram_tts_uri) params += `,endpoint=${deepgram_tts_uri}`;
params += '}'; params += '}';
@@ -1106,23 +1298,25 @@ const synthDeepgram = async(logger, {credentials, stats, model, text, renderForC
}; };
const synthCartesia = async(logger, { 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 {api_key, model_id, embedding, options: credOpts} = credentials;
const opts = !!options && Object.keys(options).length !== 0 ? options : JSON.parse(credOpts || '{}'); const opts = !!options && Object.keys(options).length !== 0 ? options : JSON.parse(credOpts || '{}');
if (!JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) { if (!JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
let params = ''; let params = '{';
params += `{api_key=${api_key}`; params += `api_key=${api_key}`;
params += `,playback_id=${key}`;
params += `,model_id=${model_id}`; params += `,model_id=${model_id}`;
params += ',vendor=cartesia'; params += ',vendor=cartesia';
params += `,voice=${voice}`; params += `,voice=${voice}`;
params += ',write_cache_file=1'; params += `,write_cache_file=${disableTtsCache ? 0 : 1}`;
params += `,language=${language}`; params += `,language=${language}`;
params += `,voice_mode=${embedding ? 'embedding' : 'id'}`; params += `,voice_mode=${embedding ? 'embedding' : 'id'}`;
if (embedding) params += `,embedding=${embedding}`; if (embedding) params += `,embedding=${embedding}`;
if (opts.speed) params += `,speed=${opts.speed}`; if (opts.speed) params += `,speed=${opts.speed}`;
if (opts.emotion) params += `,emotion=${opts.emotion}`; if (opts.emotion) params += `,emotion=${opts.emotion}`;
if (opts.volume) params += `,volume=${opts.volume}`;
params += '}'; params += '}';
return { return {
@@ -1148,13 +1342,20 @@ const synthCartesia = async(logger, {
id: voice id: voice
} }
), ),
...(opts.speed || opts.emotion && { ...(model_id === 'sonic-2' && (opts.speed || opts.emotion) && {
experimentalControls: { experimentalControls: {
...(opts.speed !== null && opts.speed !== undefined && {speed: opts.speed}), ...(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, language: language,
outputFormat: { outputFormat: {
container: 'mp3', container: 'mp3',
@@ -1175,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) => { const getFileExtFromMime = (mime) => {
switch (mime) { switch (mime) {
case 'audio/wav': case 'audio/wav':
+3 -2
View File
@@ -23,10 +23,11 @@ function makeSynthKey({
voice, voice,
engine = '', engine = '',
model = '', model = '',
text text,
instructions = '',
}) { }) {
const hash = crypto.createHash('sha1'); const hash = crypto.createHash('sha1');
hash.update(`${language}:${vendor}:${voice}:${engine}:${model}:${text}`); hash.update(`${language}:${vendor}:${voice}:${engine}:${model}:${text}:${instructions}`);
const hexHashKey = hash.digest('hex'); const hexHashKey = hash.digest('hex');
const accountKey = account_sid ? `:${account_sid}` : ''; const accountKey = account_sid ? `:${account_sid}` : '';
const key = `tts${accountKey}:${hexHashKey}`; const key = `tts${accountKey}:${hexHashKey}`;
+692 -1199
View File
File diff suppressed because it is too large Load Diff
+6 -5
View File
@@ -1,6 +1,6 @@
{ {
"name": "@jambonz/speech-utils", "name": "@jambonz/speech-utils",
"version": "0.2.3", "version": "0.2.26",
"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",
@@ -26,9 +26,10 @@
}, },
"homepage": "https://github.com/jambonz/speech-utils#readme", "homepage": "https://github.com/jambonz/speech-utils#readme",
"dependencies": { "dependencies": {
"23": "^0.0.0",
"@aws-sdk/client-polly": "^3.496.0", "@aws-sdk/client-polly": "^3.496.0",
"@aws-sdk/client-sts": "^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", "@google-cloud/text-to-speech": "^5.5.0",
"@grpc/grpc-js": "^1.9.14", "@grpc/grpc-js": "^1.9.14",
"@jambonz/realtimedb-helpers": "^0.8.7", "@jambonz/realtimedb-helpers": "^0.8.7",
@@ -36,10 +37,10 @@
"debug": "^4.3.4", "debug": "^4.3.4",
"form-urlencoded": "^6.1.4", "form-urlencoded": "^6.1.4",
"google-protobuf": "^3.21.2", "google-protobuf": "^3.21.2",
"ibm-watson": "^8.0.0", "ibm-watson": "^11.0.0",
"microsoft-cognitiveservices-speech-sdk": "1.38.0", "microsoft-cognitiveservices-speech-sdk": "1.38.0",
"openai": "^4.25.0", "openai": "^4.98.0",
"undici": "^6.4.0" "undici": "^7.5.0"
}, },
"devDependencies": { "devDependencies": {
"config": "^3.3.11", "config": "^3.3.11",
+69 -4
View File
@@ -185,6 +185,7 @@ test('AWS speech synth tests', async(t) => {
language: 'en-US', language: 'en-US',
voice: 'Joey', voice: 'Joey',
text: 'This is a test. This is only a test', text: 'This is a test. This is only a test',
renderForCaching: true,
}); });
t.ok(!opts.servedFromCache, `successfully synthesized aws audio to ${opts.filePath}`); 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', language: 'en-US',
voice: 'Joey', voice: 'Joey',
text: 'This is a test. This is only a test', text: 'This is a test. This is only a test',
renderForCaching: true,
}); });
t.ok(opts.servedFromCache, `successfully retrieved aws audio from cache ${opts.filePath}`); t.ok(opts.servedFromCache, `successfully retrieved aws audio from cache ${opts.filePath}`);
} catch (err) { } catch (err) {
@@ -552,6 +554,7 @@ test('Custom Vendor speech synth tests', async(t) => {
language: 'en-US', language: 'en-US',
voice: 'English-US.Female-1', voice: 'English-US.Female-1',
text: 'This is a test. This is only a test', 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.servedFromCache, `successfully synthesized custom vendor audio to ${opts.filePath}`);
t.ok(opts.filePath.endsWith('wav'), 'audio is cached as wav file'); 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', language: 'en-US',
voice: 'English-US.Female-1', voice: 'English-US.Female-1',
text: 'This is a test. This is only a test', 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.servedFromCache, `successfully get custom vendor cached audio to ${opts.filePath}`);
t.ok(opts.filePath.endsWith('wav'), 'audio is cached as wav file'); 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', language: 'en-US',
voice: 'English-US.Female-1', voice: 'English-US.Female-1',
text: '<speak>This is a test. This is only a test</speak>', 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}`); t.ok(!opts.servedFromCache, `successfully synthesized Custom Vendor audio to ${opts.filePath}`);
obj = await getJSON(`http://127.0.0.1:3100/lastRequest/somethingnew2`); obj = await getJSON(`http://127.0.0.1:3100/lastRequest/somethingnew2`);
@@ -720,6 +725,65 @@ test('Cartesia speech synth tests', async(t) => {
client.quit(); 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) => { test('rimelabs speech synth tests mist', async(t) => {
const fn = require('..'); const fn = require('..');
const {synthAudio, client} = fn(opts, logger); const {synthAudio, client} = fn(opts, logger);
@@ -887,7 +951,8 @@ test('TTS Cache tests', async(t) => {
// save some random tts keys to cache // save some random tts keys to cache
const minRecords = 8; const minRecords = 8;
for (const i in Array(minRecords).fill(0)) { for (const i in Array(minRecords).fill(0)) {
await client.set(makeSynthKey({vendor: i, language: i, voice: i, engine: i, model: i, text: i}), i); await client.set(makeSynthKey({vendor: i, language: i, voice: i, engine: i, model: i, text: i,
instructions: i}), i);
} }
const count = await getTtsSize(); const count = await getTtsSize();
t.ok(count >= minRecords, 'getTtsSize worked.'); t.ok(count >= minRecords, 'getTtsSize worked.');
@@ -906,7 +971,7 @@ test('TTS Cache tests', async(t) => {
try { try {
// save some random tts keys to cache // save some random tts keys to cache
for (const i in Array(10).fill(0)) { for (const i in Array(10).fill(0)) {
await client.set(makeSynthKey({vendor: i, language: i, voice: i, engine: i, text: i}), i); await client.set(makeSynthKey({vendor: i, language: i, voice: i, engine: i, text: i, instructions: i}), i);
} }
// save a specific key to tts cache // save a specific key to tts cache
const opts = {vendor: 'aws', language: 'en-US', voice: 'MALE', engine: 'Engine', text: 'Hello World!'}; const opts = {vendor: 'aws', language: 'en-US', voice: 'MALE', engine: 'Engine', text: 'Hello World!'};
@@ -944,10 +1009,10 @@ test('TTS Cache tests', async(t) => {
const account_sid = "12412512_cabc_5aff" const account_sid = "12412512_cabc_5aff"
const account_sid2 = "22412512_cabc_5aff" const account_sid2 = "22412512_cabc_5aff"
for (const i in Array(minRecords).fill(0)) { for (const i in Array(minRecords).fill(0)) {
await client.set(makeSynthKey({account_sid, vendor: i, language: i, voice: i, engine: i, text: i}), i); await client.set(makeSynthKey({account_sid, vendor: i, language: i, voice: i, engine: i, text: i, instructions: i}), i);
} }
for (const i in Array(minRecords).fill(0)) { for (const i in Array(minRecords).fill(0)) {
await client.set(makeSynthKey({account_sid: account_sid2, vendor: i, language: i, voice: i, engine: i, text: i}), i); await client.set(makeSynthKey({account_sid: account_sid2, vendor: i, language: i, voice: i, engine: i, text: i, instructions: i}), i);
} }
const {purgedCount} = await purgeTtsCache({account_sid}); const {purgedCount} = await purgeTtsCache({account_sid});
t.equal(purgedCount, minRecords, `successfully purged at least ${minRecords} tts records from cache for account_sid:${account_sid}`); t.equal(purgedCount, minRecords, `successfully purged at least ${minRecords} tts records from cache for account_sid:${account_sid}`);