Compare commits

..
32 Commits
Author SHA1 Message Date
Dave Horton 1846203807 0.1.16 2024-09-16 15:53:14 -04:00
Dave Horton 75be8658c1 Merge pull request #91 from jambonz/fix/diff_playht_voice_quality
fix playht has stream and cached audio differrent quality
2024-09-16 15:52:42 -04:00
Quan HL 91a5eebbaf fixed review comment 2024-09-16 18:46:10 +07:00
Quan HL c96f1e86ee fix review comment 2024-09-16 18:15:45 +07:00
Quan HL 8016c0886a fix playht has stream and cached audio differrent quality 2024-09-16 09:01:45 +07:00
Dave Horton 8f216e64d8 0.1.15 2024-08-12 09:30:09 -04:00
Dave Horton e1f4486e01 bump version 2024-08-12 09:27:12 -04:00
Dave Horton b0d6272974 Merge pull request #84 from jambonz/feat/precache_audio_with_tts_stream
support precache audio with tts stream enabled
2024-08-12 09:26:00 -04:00
Quan HL ef23b0807a add comment 2024-08-12 20:16:24 +07:00
Quan HL ab7e25243d improve on check precache 2024-08-12 20:10:43 +07:00
Quan HL bf0ea14423 install docker 2024-08-12 18:40:47 +07:00
Quan HL 305dabd84b wip 2024-08-12 18:35:48 +07:00
Quan HL b6a3fa5081 support precache audio with tts stream enabled 2024-08-12 18:29:01 +07:00
Dave Horton 73feadc4c4 0.1.13 2024-08-06 11:01:09 -04:00
Dave Horton aad0f4d62c Merge pull request #82 from jambonz/feat/deepgram_tts_endpoint
deepgram tts support endpoint for on-premise
2024-08-06 11:00:38 -04:00
Hoan Luu Huu 602b0cc60e Merge branch 'main' into feat/deepgram_tts_endpoint 2024-07-31 14:03:41 +07:00
Dave Horton 8511e762c8 version update 2024-07-30 07:29:25 -04:00
Dave Horton b75d3068c9 Merge pull request #81 from jambonz/feat/gh_fs_832
allow configure STS session expiry
2024-07-30 07:14:06 -04:00
Quan HL 461178e726 wip 2024-07-29 20:56:55 +07:00
Quan HL e0e4d47340 wip 2024-07-29 20:53:55 +07:00
Quan HL a595faa378 deepgram tts support endpoint for on-premise 2024-07-29 19:45:13 +07:00
Quan HL 7fd1e1a3c3 allow configure STS session expiry 2024-07-29 18:18:37 +07:00
Dave Horton 7f6a3d349c 0.1.11 2024-06-14 07:37:19 -04:00
Dave Horton 50429ff535 bump version 2024-06-14 07:36:53 -04:00
Dave Horton 3bf0ef8ea3 Merge pull request #80 from jambonz/fix/aws_arnrole
fix aws arnrole
2024-06-14 07:34:22 -04:00
Quan HL e9a5e83e36 wip 2024-06-14 15:04:19 +07:00
Quan HL 86a64ac091 wip 2024-06-14 15:00:56 +07:00
Quan HL 97e06b3ab3 wip 2024-06-14 10:23:22 +07:00
Quan HL 09e833d910 wip 2024-06-14 10:19:55 +07:00
Quan HL 8c4e12e54f wip 2024-06-14 10:18:41 +07:00
Quan HL 2642bd71a4 wip 2024-06-14 10:16:57 +07:00
Quan HL c4feac916f fix aws arnrole 2024-06-14 10:15:23 +07:00
10 changed files with 1663 additions and 1422 deletions
+5
View File
@@ -13,6 +13,11 @@ jobs:
with: with:
node-version: lts/* node-version: lts/*
- run: npm install - run: npm install
- name: Install Docker Compose
run: |
sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
docker-compose --version
- run: npm run jslint - run: npm run jslint
- run: sudo apt update && sudo apt install -y squid - run: sudo apt update && sudo apt install -y squid
- run: sudo cp test/squid.conf /etc/squid/squid.conf - run: sudo cp test/squid.conf /etc/squid/squid.conf
+1 -1
View File
@@ -1,3 +1,3 @@
npm audit #npm audit
npm run jslint:fix || true npm run jslint:fix || true
npm test npm test
+2
View File
@@ -1,6 +1,7 @@
const JAMBONES_TTS_TRIM_SILENCE = process.env.JAMBONES_TTS_TRIM_SILENCE; 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_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;
@@ -18,6 +19,7 @@ module.exports = {
JAMBONES_HTTP_PROXY_IP, JAMBONES_HTTP_PROXY_IP,
JAMBONES_HTTP_PROXY_PORT, JAMBONES_HTTP_PROXY_PORT,
JAMBONES_TTS_CACHE_DURATION_MINS, JAMBONES_TTS_CACHE_DURATION_MINS,
JAMBONES_EAGERLY_PRE_CACHE_AUDIO,
TMP_FOLDER, TMP_FOLDER,
HTTP_TIMEOUT HTTP_TIMEOUT
}; };
+10 -9
View File
@@ -1,20 +1,22 @@
const { STSClient, GetSessionTokenCommand, AssumeRoleCommand } = require('@aws-sdk/client-sts'); const { STSClient, GetSessionTokenCommand, AssumeRoleCommand } = require('@aws-sdk/client-sts');
const {makeAwsKey, noopLogger} = require('./utils'); const {makeAwsKey, noopLogger} = require('./utils');
const debug = require('debug')('jambonz:speech-utils'); const debug = require('debug')('jambonz:speech-utils');
const EXPIRY = 3600; const EXPIRY = process.env.AWS_STS_SESSION_DURATION || 3600;
// by default reset aws session before expiry time 10 mins
const CACHE_EXPIRY = process.env.AWS_STS_SESSION_RESET_EXPIRY || (EXPIRY - 600);
async function getAwsAuthToken( async function getAwsAuthToken(
logger, createHash, retrieveHash, logger, createHash, retrieveHash,
awsAccessKeyId, awsSecretAccessKey, awsRegion, roleArn = null) { {accessKeyId, secretAccessKey, region, roleArn}) {
logger = logger || noopLogger; logger = logger || noopLogger;
try { try {
const key = makeAwsKey(roleArn || awsAccessKeyId); const key = makeAwsKey(roleArn || accessKeyId);
const obj = await retrieveHash(key); const obj = await retrieveHash(key);
if (obj) return {...obj, servedFromCache: true}; if (obj) return {...obj, servedFromCache: true};
let data; let data;
if (roleArn) { if (roleArn) {
const stsClient = new STSClient({ region: awsRegion}); const stsClient = new STSClient({ region });
const roleToAssume = { RoleArn: roleArn, RoleSessionName: 'Jambonz_Speech', DurationSeconds: EXPIRY}; const roleToAssume = { RoleArn: roleArn, RoleSessionName: 'Jambonz_Speech', DurationSeconds: EXPIRY};
const command = new AssumeRoleCommand(roleToAssume); const command = new AssumeRoleCommand(roleToAssume);
@@ -22,10 +24,10 @@ async function getAwsAuthToken(
} else { } else {
/* access token not found in cache, so generate it using STS */ /* access token not found in cache, so generate it using STS */
const stsClient = new STSClient({ const stsClient = new STSClient({
region: awsRegion, region,
credentials: { credentials: {
accessKeyId: awsAccessKeyId, accessKeyId,
secretAccessKey: awsSecretAccessKey, secretAccessKey,
} }
}); });
const command = new GetSessionTokenCommand({DurationSeconds: EXPIRY}); const command = new GetSessionTokenCommand({DurationSeconds: EXPIRY});
@@ -39,8 +41,7 @@ async function getAwsAuthToken(
securityToken: data.Credentials.SessionToken securityToken: data.Credentials.SessionToken
}; };
/* expire 10 minutes before the hour, so we don't lose the use of it during a call */ createHash(key, credentials, CACHE_EXPIRY)
createHash(key, credentials, EXPIRY - 600)
.catch((err) => logger.error(err, `Error saving hash for key ${key}`)); .catch((err) => logger.error(err, `Error saving hash for key ${key}`));
return {...credentials, servedFromCache: false}; return {...credentials, servedFromCache: false};
+6 -1
View File
@@ -107,7 +107,12 @@ const getAwsVoices = async(_client, createHash, retrieveHash, logger, credential
} else if (roleArn) { } else if (roleArn) {
client = new PollyClient({ client = new PollyClient({
region, region,
credentials: await getAwsAuthToken(logger, createHash, retrieveHash, null, null, region, roleArn), credentials: await getAwsAuthToken(
logger, createHash, retrieveHash,
{
region,
roleArn
}),
}); });
} else { } else {
client = new PollyClient({region}); client = new PollyClient({region});
+42 -7
View File
@@ -44,6 +44,7 @@ const {
JAMBONES_HTTP_PROXY_IP, JAMBONES_HTTP_PROXY_IP,
JAMBONES_HTTP_PROXY_PORT, JAMBONES_HTTP_PROXY_PORT,
JAMBONES_TTS_CACHE_DURATION_MINS, JAMBONES_TTS_CACHE_DURATION_MINS,
JAMBONES_EAGERLY_PRE_CACHE_AUDIO,
} = 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');
@@ -86,7 +87,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, disableTtsStreaming, options disableTtsCache, renderForCaching = false, disableTtsStreaming, options
}) { }) {
let audioBuffer; let audioBuffer;
let servedFromCache = false; let servedFromCache = false;
@@ -151,21 +152,48 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
assert.ok(voice, 'synthAudio requires voice when verbio is used'); assert.ok(voice, 'synthAudio requires voice when verbio is used');
assert.ok(credentials.client_id, 'synthAudio requires client_id when verbio is used'); assert.ok(credentials.client_id, 'synthAudio requires client_id when verbio is used');
assert.ok(credentials.client_secret, 'synthAudio requires client_secret when verbio is used'); assert.ok(credentials.client_secret, 'synthAudio requires client_secret when verbio is used');
} else if ('deepgram' === vendor) {
if (!credentials.deepgram_tts_uri) {
assert.ok(credentials.api_key, 'synthAudio requires api_key when deepgram is used');
}
} }
const key = makeSynthKey({ const key = makeSynthKey({
account_sid, account_sid,
vendor, vendor,
language: language || '', language: language || '',
voice: voice || deploymentId, voice: voice || deploymentId,
engine, engine,
text text,
renderForCaching
}); });
let filePath; let filePath;
filePath = makeFilePath(vendor, key, salt); filePath = makeFilePath({vendor, key, salt, renderForCaching});
debug(`synth key is ${key}`); debug(`synth key is ${key}`);
let cached; let cached;
if (!disableTtsCache) { if (!disableTtsCache) {
cached = await client.get(key); cached = await client.get(key);
/**
* If we are using tts streaming and also precaching audio, audio could have been cached by streaming (r8)
* or here in speech-utils due to precaching (mp3), so we need to check for both keys.
*/
if (!cached && JAMBONES_EAGERLY_PRE_CACHE_AUDIO) {
const preCachekey = makeSynthKey({
account_sid,
vendor,
language: language || '',
voice: voice || deploymentId,
engine,
text,
renderForCaching: true
});
cached = await client.get(preCachekey);
if (cached) {
// Precache audio is available update filpath with precache file extension.
filePath = makeFilePath({vendor, key, salt, renderForCaching: true});
}
}
} }
if (cached) { if (cached) {
// found in cache - extend the expiry and use it // found in cache - extend the expiry and use it
@@ -281,7 +309,12 @@ const synthPolly = async(createHash, retrieveHash, logger,
} else if (roleArn) { } else if (roleArn) {
polly = new PollyClient({ polly = new PollyClient({
region, region,
credentials: await getAwsAuthToken(logger, createHash, retrieveHash, null, null, region, roleArn), credentials: await getAwsAuthToken(
logger, createHash, retrieveHash,
{
region,
roleArn
}),
}); });
} else { } else {
// AWS RoleArn assigned to Instance profile // AWS RoleArn assigned to Instance profile
@@ -904,13 +937,14 @@ 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, text, renderForCaching, disableTtsStreaming}) => {
const {api_key} = 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 += ',vendor=deepgram'; params += ',vendor=deepgram';
params += `,voice=${model}`; params += `,voice=${model}`;
params += ',write_cache_file=1'; params += ',write_cache_file=1';
if (deepgram_tts_uri) params += `,endpoint=${deepgram_tts_uri}`;
params += '}'; params += '}';
return { return {
@@ -920,8 +954,9 @@ const synthDeepgram = async(logger, {credentials, stats, model, text, renderForC
}; };
} }
try { try {
const post = bent('https://api.deepgram.com', 'POST', 'buffer', { const post = bent(deepgram_tts_uri || 'https://api.deepgram.com', 'POST', 'buffer', {
'Authorization': `Token ${api_key}`, // on-premise deepgram does not require to have api_key
...(api_key && {'Authorization': `Token ${api_key}`}),
'Accept': 'audio/mpeg', 'Accept': 'audio/mpeg',
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}); });
+20 -9
View File
@@ -16,29 +16,31 @@ const debug = require('debug')('jambonz:realtimedb-helpers');
*/ */
//const nuanceClientMap = new Map(); //const nuanceClientMap = new Map();
function makeSynthKey({account_sid = '', vendor, language, voice, engine = '', text}) { function makeSynthKey({
account_sid = '', vendor, language, voice, engine = '', text,
renderForCaching = false}) {
const hash = crypto.createHash('sha1'); const hash = crypto.createHash('sha1');
hash.update(`${language}:${vendor}:${voice}:${engine}:${text}`); hash.update(`${language}:${vendor}:${voice}:${engine}:${text}`);
const hexHashKey = hash.digest('hex'); const hexHashKey = hash.digest('hex');
const accountKey = account_sid ? `:${account_sid}` : ''; const accountKey = account_sid ? `:${account_sid}` : '';
const namespace = vendor.startsWith('custom') ? vendor : getFileExtension(vendor); const namespace = vendor.startsWith('custom') ? vendor : getFileExtension({vendor, renderForCaching});
const key = `tts${accountKey}:${namespace}:${hexHashKey}`; const key = `tts${accountKey}:${namespace}:${hexHashKey}`;
return key; return key;
} }
function makeFilePath(vendor, key, salt = '') { function makeFilePath({vendor, key, salt = '', renderForCaching = false}) {
const extension = getFileExtension(vendor); const extension = getFileExtension({vendor, renderForCaching});
return `${TMP_FOLDER}/${key.replace('tts:', `tts-${salt}`)}.${extension}`; return `${TMP_FOLDER}/${key.replace('tts:', `tts-${salt}`)}.${extension}`;
} }
function getFileExtension(vendor) { function getFileExtension({vendor, renderForCaching = false}) {
const mp3Extension = 'mp3'; const mp3Extension = 'mp3';
const r8Extension = 'r8'; const r8Extension = 'r8';
switch (vendor) { switch (vendor) {
case 'azure': case 'azure':
case 'microsoft': case 'microsoft':
if (!JAMBONES_DISABLE_TTS_STREAMING || JAMBONES_TTS_TRIM_SILENCE) { if (!renderForCaching && !JAMBONES_DISABLE_TTS_STREAMING || JAMBONES_TTS_TRIM_SILENCE) {
return r8Extension; return r8Extension;
} else { } else {
return mp3Extension; return mp3Extension;
@@ -46,16 +48,25 @@ function getFileExtension(vendor) {
case 'deepgram': case 'deepgram':
case 'elevenlabs': case 'elevenlabs':
case 'rimlabs': case 'rimlabs':
if (!JAMBONES_DISABLE_TTS_STREAMING) { case 'playht':
return r8Extension; if (renderForCaching || JAMBONES_DISABLE_TTS_STREAMING) {
} else {
return mp3Extension; return mp3Extension;
} else {
return r8Extension;
} }
case 'nuance': case 'nuance':
case 'nvidia': case 'nvidia':
case 'verbio': case 'verbio':
return r8Extension; return r8Extension;
default: default:
// If vendor is custom
if (vendor.startsWith('custom')) {
if (renderForCaching || JAMBONES_DISABLE_TTS_STREAMING) {
return mp3Extension;
} else {
return r8Extension;
}
}
return mp3Extension; return mp3Extension;
} }
} }
+1565 -1391
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@jambonz/speech-utils", "name": "@jambonz/speech-utils",
"version": "0.1.9", "version": "0.1.16",
"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",
@@ -36,7 +36,7 @@
"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": "^8.0.0",
"microsoft-cognitiveservices-speech-sdk": "1.36.0", "microsoft-cognitiveservices-speech-sdk": "1.38.0",
"openai": "^4.25.0", "openai": "^4.25.0",
"undici": "^6.4.0" "undici": "^6.4.0"
}, },
+10 -2
View File
@@ -19,12 +19,20 @@ test('AWS - create and cache auth token', async(t) => {
return; return;
} }
try { try {
let obj = await getAwsAuthToken(process.env.AWS_ACCESS_KEY_ID, process.env.AWS_SECRET_ACCESS_KEY, process.env.AWS_REGION); let obj = await getAwsAuthToken({
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
region: process.env.AWS_REGION
});
//console.log({obj}, 'received auth token from AWS'); //console.log({obj}, 'received auth token from AWS');
t.ok(obj.securityToken && !obj.servedFromCache, 'successfullY generated auth token from AWS'); t.ok(obj.securityToken && !obj.servedFromCache, 'successfullY generated auth token from AWS');
await sleep(250); await sleep(250);
obj = await getAwsAuthToken(process.env.AWS_ACCESS_KEY_ID, process.env.AWS_SECRET_ACCESS_KEY, process.env.AWS_REGION); obj = await getAwsAuthToken({
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
region: process.env.AWS_REGION
});
//console.log({obj}, 'received auth token from AWS - second request'); //console.log({obj}, 'received auth token from AWS - second request');
t.ok(obj.securityToken && obj.servedFromCache, 'successfully received access token from cache'); t.ok(obj.securityToken && obj.servedFromCache, 'successfully received access token from cache');