Compare commits

...
Author SHA1 Message Date
snyk-bot b442b050e7 fix: package.json & package-lock.json to reduce vulnerabilities
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-AXIOS-7361793
- https://snyk.io/vuln/SNYK-JS-MICROMATCH-6838728
2024-12-06 06:49:44 +00:00
Dave Horton d96ab5cdf3 Merge pull request #99 from jambonz/feat/support_aws_instance_profile
support aws instance profile to get key
2024-11-29 21:58:22 -05:00
Quan HL 3bf74671c6 support aws instance profile to get key 2024-11-30 08:40:26 +07:00
Dave Horton a47ef6d7c4 0.1.22 2024-11-04 07:38:23 -05:00
Dave Horton 84089fa528 Merge pull request #98 from jambonz/fix/freshdesk_411
Fix custom tts vendor cached file can not be played
2024-11-04 07:37:49 -05:00
Quan HL 72be44eea2 adding testcase 2024-11-04 15:53:11 +07:00
Quan HL 05d6c4b32d fixed custom vendor cache audio stores file extension 2024-11-04 15:43:44 +07:00
5 changed files with 288 additions and 607 deletions
+31 -8
View File
@@ -7,22 +7,23 @@ const CACHE_EXPIRY = process.env.AWS_STS_SESSION_RESET_EXPIRY || (EXPIRY - 600);
async function getAwsAuthToken(
logger, createHash, retrieveHash,
{accessKeyId, secretAccessKey, region, roleArn}) {
{speech_credential_sid, accessKeyId, secretAccessKey, region, roleArn}) {
logger = logger || noopLogger;
try {
const key = makeAwsKey(roleArn || accessKeyId);
// if incase instance profile is used, speech_credential_sid will be used as key to lookup cache
const key = makeAwsKey(roleArn || accessKeyId || speech_credential_sid);
const obj = await retrieveHash(key);
if (obj) return {...obj, servedFromCache: true};
/* access token not found in cache, so generate it using STS */
let data;
let expiry = CACHE_EXPIRY;
if (roleArn) {
const stsClient = new STSClient({ region });
const roleToAssume = { RoleArn: roleArn, RoleSessionName: 'Jambonz_Speech', DurationSeconds: EXPIRY};
const command = new AssumeRoleCommand(roleToAssume);
data = await stsClient.send(command);
} else {
/* access token not found in cache, so generate it using STS */
} else if (accessKeyId) {
const stsClient = new STSClient({
region,
credentials: {
@@ -32,6 +33,26 @@ async function getAwsAuthToken(
});
const command = new GetSessionTokenCommand({DurationSeconds: EXPIRY});
data = await stsClient.send(command);
} else {
// instance profile is used.
const stsClient = new STSClient({ region });
const cred = await stsClient.config.credentials();
// method in the AWS SDK automatically fetches credentials using the default credential
// provider chain. If the credentials come from an instance profile or an environment
// variable, their expiration is controlled by AWS and not explicitly by our code.
if (cred && cred.expiration) {
const currentTime = new Date();
const expiryTime = new Date(cred.expiration);
const remainingTimeInSeconds = Math.round((expiryTime - currentTime) / 1000);
expiry = remainingTimeInSeconds;
}
data = {
Credentials: {
AccessKeyId: cred.accessKeyId,
SecretAccessKey: cred.secretAccessKey,
SessionToken: cred.sessionToken
}
};
}
const credentials = {
@@ -40,9 +61,11 @@ async function getAwsAuthToken(
sessionToken: data.Credentials.SessionToken,
securityToken: data.Credentials.SessionToken
};
createHash(key, credentials, CACHE_EXPIRY)
.catch((err) => logger.error(err, `Error saving hash for key ${key}`));
// Only cache if expiry is good
if (expiry > 0) {
createHash(key, credentials, expiry)
.catch((err) => logger.error(err, `Error saving hash for key ${key}`));
}
return {...credentials, servedFromCache: false};
} catch (err) {
+25 -4
View File
@@ -170,6 +170,8 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
renderForCaching
});
let filePath;
// used only for custom vendor
let fileExtension;
filePath = makeFilePath({vendor, voice, key, salt, renderForCaching});
debug(`synth key is ${key}`);
let cached;
@@ -201,7 +203,17 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
debug('result WAS found in cache');
servedFromCache = true;
stats.increment('tts.cache.requests', ['found:yes']);
audioBuffer = Buffer.from(cached, 'base64');
if (vendor.startsWith('custom')) {
// custom vendors support multiple mime types such as: mp3, wav, r8, r16 ...etc,
// mime type/file extension is available when http response has header Content-type.
// In cache, file extension is store together with audiBuffer in a json.
// Normal cache audio will be base64 string
const payload = JSON.parse(cached);
filePath = filePath.replace(/\.[^\.]*$/g, payload.fileExtension);
audioBuffer = Buffer.from(payload.audioBuffer, 'base64');
} else {
audioBuffer = Buffer.from(cached, 'base64');
}
client.expire(key, EXPIRES).catch((err) => logger.info(err, 'Error setting expires'));
}
if (!cached) {
@@ -268,7 +280,7 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
renderForCaching, disableTtsStreaming});
break;
case vendor.startsWith('custom') ? vendor : 'cant_match_value':
({ audioBuffer, filePath } = await synthCustomVendor(logger,
({ audioBuffer, filePath, fileExtension } = await synthCustomVendor(logger,
{credentials, stats, language, voice, text, filePath}));
break;
default:
@@ -282,7 +294,14 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
debug(`tts rtt time for ${text.length} chars on ${vendorLabel}: ${rtt}`);
logger.info(`tts rtt time for ${text.length} chars on ${vendorLabel}: ${rtt}`);
client.setex(key, EXPIRES, audioBuffer.toString('base64'))
const base64Audio = audioBuffer.toString('base64');
const cacheContent = vendor.startsWith('custom') ?
JSON.stringify({
audioBuffer: base64Audio,
fileExtension
}) : base64Audio;
client.setex(key, EXPIRES, cacheContent)
.catch((err) => logger.error(err, `error calling setex on key ${key}`));
}
@@ -729,9 +748,11 @@ const synthCustomVendor = async(logger, {credentials, stats, language, voice, te
const regex = /\.[^\.]*$/g;
const mime = response.headers['content-type'];
const buffer = await response.arrayBuffer();
const fileExtension = getFileExtFromMime(mime);
return {
audioBuffer: buffer,
filePath: filePath.replace(regex, getFileExtFromMime(mime))
filePath: filePath.replace(regex, fileExtension),
fileExtension
};
} catch (err) {
logger.info({err}, `Vendor ${vendor} returned error`);
+214 -593
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@jambonz/speech-utils",
"version": "0.1.21",
"version": "0.1.22",
"description": "TTS-related speech utilities for jambonz",
"main": "index.js",
"author": "Dave Horton",
@@ -35,7 +35,7 @@
"debug": "^4.3.4",
"form-urlencoded": "^6.1.4",
"google-protobuf": "^3.21.2",
"ibm-watson": "^8.0.0",
"ibm-watson": "^10.0.0",
"microsoft-cognitiveservices-speech-sdk": "1.38.0",
"openai": "^4.25.0",
"undici": "^6.4.0"
+16
View File
@@ -554,6 +554,7 @@ test('Custom Vendor speech synth tests', async(t) => {
text: 'This is a test. This is only a test',
});
t.ok(!opts.servedFromCache, `successfully synthesized custom vendor audio to ${opts.filePath}`);
t.ok(opts.filePath.endsWith('wav'), 'audio is cached as wav file');
let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/somethingnew`);
t.ok(obj.headers.Authorization == 'Bearer some_jwt_token', 'Custom Vendor Authentication Header is correct');
t.ok(obj.body.language == 'en-US', 'Custom Vendor Language is correct');
@@ -561,6 +562,21 @@ test('Custom Vendor speech synth tests', async(t) => {
t.ok(obj.body.type == 'text', 'Custom Vendor type is correct');
t.ok(obj.body.text == 'This is a test. This is only a test', 'Custom Vendor text is correct');
// Checking if cache is stored with wav format
opts = await synthAudio(stats, {
vendor: 'custom:somethingnew',
credentials: {
use_for_tts: 1,
custom_tts_url: "http://127.0.0.1:3100/somethingnew",
auth_token: 'some_jwt_token'
},
language: 'en-US',
voice: 'English-US.Female-1',
text: 'This is a test. This is only a test',
});
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');
opts = await synthAudio(stats, {
vendor: 'custom:somethingnew2',
credentials: {