@jambonz/speech-utils 0.1.22 → 0.1.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/get-aws-sts-token.js +31 -8
- package/lib/synth-audio.js +75 -2
- package/lib/utils.js +1 -0
- package/package.json +2 -1
- package/test/synth.js +34 -0
package/lib/get-aws-sts-token.js
CHANGED
|
@@ -7,22 +7,23 @@ const CACHE_EXPIRY = process.env.AWS_STS_SESSION_RESET_EXPIRY || (EXPIRY - 600);
|
|
|
7
7
|
|
|
8
8
|
async function getAwsAuthToken(
|
|
9
9
|
logger, createHash, retrieveHash,
|
|
10
|
-
{accessKeyId, secretAccessKey, region, roleArn}) {
|
|
10
|
+
{speech_credential_sid, accessKeyId, secretAccessKey, region, roleArn}) {
|
|
11
11
|
logger = logger || noopLogger;
|
|
12
12
|
try {
|
|
13
|
-
|
|
13
|
+
// if incase instance profile is used, speech_credential_sid will be used as key to lookup cache
|
|
14
|
+
const key = makeAwsKey(roleArn || accessKeyId || speech_credential_sid);
|
|
14
15
|
const obj = await retrieveHash(key);
|
|
15
16
|
if (obj) return {...obj, servedFromCache: true};
|
|
16
|
-
|
|
17
|
+
/* access token not found in cache, so generate it using STS */
|
|
17
18
|
let data;
|
|
19
|
+
let expiry = CACHE_EXPIRY;
|
|
18
20
|
if (roleArn) {
|
|
19
21
|
const stsClient = new STSClient({ region });
|
|
20
22
|
const roleToAssume = { RoleArn: roleArn, RoleSessionName: 'Jambonz_Speech', DurationSeconds: EXPIRY};
|
|
21
23
|
const command = new AssumeRoleCommand(roleToAssume);
|
|
22
24
|
|
|
23
25
|
data = await stsClient.send(command);
|
|
24
|
-
} else {
|
|
25
|
-
/* access token not found in cache, so generate it using STS */
|
|
26
|
+
} else if (accessKeyId) {
|
|
26
27
|
const stsClient = new STSClient({
|
|
27
28
|
region,
|
|
28
29
|
credentials: {
|
|
@@ -32,6 +33,26 @@ async function getAwsAuthToken(
|
|
|
32
33
|
});
|
|
33
34
|
const command = new GetSessionTokenCommand({DurationSeconds: EXPIRY});
|
|
34
35
|
data = await stsClient.send(command);
|
|
36
|
+
} else {
|
|
37
|
+
// instance profile is used.
|
|
38
|
+
const stsClient = new STSClient({ region });
|
|
39
|
+
const cred = await stsClient.config.credentials();
|
|
40
|
+
// method in the AWS SDK automatically fetches credentials using the default credential
|
|
41
|
+
// provider chain. If the credentials come from an instance profile or an environment
|
|
42
|
+
// variable, their expiration is controlled by AWS and not explicitly by our code.
|
|
43
|
+
if (cred && cred.expiration) {
|
|
44
|
+
const currentTime = new Date();
|
|
45
|
+
const expiryTime = new Date(cred.expiration);
|
|
46
|
+
const remainingTimeInSeconds = Math.round((expiryTime - currentTime) / 1000);
|
|
47
|
+
expiry = remainingTimeInSeconds;
|
|
48
|
+
}
|
|
49
|
+
data = {
|
|
50
|
+
Credentials: {
|
|
51
|
+
AccessKeyId: cred.accessKeyId,
|
|
52
|
+
SecretAccessKey: cred.secretAccessKey,
|
|
53
|
+
SessionToken: cred.sessionToken
|
|
54
|
+
}
|
|
55
|
+
};
|
|
35
56
|
}
|
|
36
57
|
|
|
37
58
|
const credentials = {
|
|
@@ -40,9 +61,11 @@ async function getAwsAuthToken(
|
|
|
40
61
|
sessionToken: data.Credentials.SessionToken,
|
|
41
62
|
securityToken: data.Credentials.SessionToken
|
|
42
63
|
};
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
64
|
+
// Only cache if expiry is good
|
|
65
|
+
if (expiry > 0) {
|
|
66
|
+
createHash(key, credentials, expiry)
|
|
67
|
+
.catch((err) => logger.error(err, `Error saving hash for key ${key}`));
|
|
68
|
+
}
|
|
46
69
|
|
|
47
70
|
return {...credentials, servedFromCache: false};
|
|
48
71
|
} catch (err) {
|
package/lib/synth-audio.js
CHANGED
|
@@ -3,6 +3,7 @@ const fs = require('fs');
|
|
|
3
3
|
const bent = require('bent');
|
|
4
4
|
const ttsGoogle = require('@google-cloud/text-to-speech');
|
|
5
5
|
const { PollyClient, SynthesizeSpeechCommand } = require('@aws-sdk/client-polly');
|
|
6
|
+
const { CartesiaClient } = require('@cartesia/cartesia-js');
|
|
6
7
|
|
|
7
8
|
const sdk = require('microsoft-cognitiveservices-speech-sdk');
|
|
8
9
|
const TextToSpeechV1 = require('ibm-watson/text-to-speech/v1');
|
|
@@ -96,7 +97,7 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
|
|
|
96
97
|
logger = logger || noopLogger;
|
|
97
98
|
|
|
98
99
|
assert.ok(['google', 'aws', 'polly', 'microsoft', 'wellsaid', 'nuance', 'nvidia', 'ibm', 'elevenlabs',
|
|
99
|
-
'whisper', 'deepgram', 'playht', 'rimelabs', 'verbio'].includes(vendor) ||
|
|
100
|
+
'whisper', 'deepgram', 'playht', 'rimelabs', 'verbio', 'cartesia'].includes(vendor) ||
|
|
100
101
|
vendor.startsWith('custom'),
|
|
101
102
|
`synthAudio supported vendors are google, aws, microsoft, nuance, nvidia and wellsaid ..etc, not ${vendor}`);
|
|
102
103
|
if ('google' === vendor) {
|
|
@@ -157,7 +158,9 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
|
|
|
157
158
|
if (!credentials.deepgram_tts_uri) {
|
|
158
159
|
assert.ok(credentials.api_key, 'synthAudio requires api_key when deepgram is used');
|
|
159
160
|
}
|
|
160
|
-
|
|
161
|
+
} else if ('cartesia' === vendor) {
|
|
162
|
+
assert.ok(credentials.api_key, 'synthAudio requires api_key when cartesia is used');
|
|
163
|
+
assert.ok(credentials.model_id, 'synthAudio requires model_id when cartesia is used');
|
|
161
164
|
}
|
|
162
165
|
|
|
163
166
|
const key = makeSynthKey({
|
|
@@ -261,6 +264,11 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
|
|
|
261
264
|
credentials, options, stats, language, voice, text, renderForCaching, disableTtsStreaming, filePath
|
|
262
265
|
});
|
|
263
266
|
break;
|
|
267
|
+
case 'cartesia':
|
|
268
|
+
audioBuffer = await synthCartesia(logger, {
|
|
269
|
+
credentials, options, stats, language, voice, text, renderForCaching, disableTtsStreaming, filePath
|
|
270
|
+
});
|
|
271
|
+
break;
|
|
264
272
|
case 'rimelabs':
|
|
265
273
|
audioBuffer = await synthRimelabs(logger, {
|
|
266
274
|
credentials, options, stats, language, voice, text, renderForCaching, disableTtsStreaming, filePath
|
|
@@ -1067,6 +1075,71 @@ const synthDeepgram = async(logger, {credentials, stats, model, text, renderForC
|
|
|
1067
1075
|
}
|
|
1068
1076
|
};
|
|
1069
1077
|
|
|
1078
|
+
const synthCartesia = async(logger, {
|
|
1079
|
+
credentials, options, stats, voice, language, text, renderForCaching, disableTtsStreaming
|
|
1080
|
+
}) => {
|
|
1081
|
+
const {api_key, model_id, embedding, options: credOpts} = credentials;
|
|
1082
|
+
const opts = !!options && Object.keys(options).length !== 0 ? options : JSON.parse(credOpts || '{}');
|
|
1083
|
+
|
|
1084
|
+
if (!JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
|
|
1085
|
+
let params = '';
|
|
1086
|
+
params += `{api_key=${api_key}`;
|
|
1087
|
+
params += `,model_id=${model_id}`;
|
|
1088
|
+
params += ',vendor=cartesia';
|
|
1089
|
+
params += `,voice=${voice}`;
|
|
1090
|
+
params += ',write_cache_file=1';
|
|
1091
|
+
params += `,language=${language}`;
|
|
1092
|
+
params += `,voice_mode=${embedding ? 'embedding' : 'id'}`;
|
|
1093
|
+
if (embedding) params += `,embedding=${embedding}`;
|
|
1094
|
+
if (opts.speed) params += `,speed=${opts.speed}`;
|
|
1095
|
+
if (opts.emotion) params += `,emotion=${opts.emotion}`;
|
|
1096
|
+
params += '}';
|
|
1097
|
+
|
|
1098
|
+
return {
|
|
1099
|
+
filePath: `say:${params}${text.replace(/\n/g, ' ').replace(/\r/g, ' ')}`,
|
|
1100
|
+
servedFromCache: false,
|
|
1101
|
+
rtt: 0
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
try {
|
|
1106
|
+
const client = new CartesiaClient({ apiKey: api_key });
|
|
1107
|
+
const mp3 = await client.tts.bytes({
|
|
1108
|
+
modelId: model_id,
|
|
1109
|
+
transcript: text,
|
|
1110
|
+
voice: {
|
|
1111
|
+
mode: embedding ? 'embedding' : 'id',
|
|
1112
|
+
...(embedding ?
|
|
1113
|
+
{
|
|
1114
|
+
embedding: embedding.split(',').map(Number)
|
|
1115
|
+
} :
|
|
1116
|
+
{
|
|
1117
|
+
id: voice
|
|
1118
|
+
}
|
|
1119
|
+
),
|
|
1120
|
+
...(opts.speed || opts.emotion && {
|
|
1121
|
+
experimentalControls: {
|
|
1122
|
+
...(opts.speed && {speed: opts.speed}),
|
|
1123
|
+
...(opts.emotion && {emotion: opts.emotion}),
|
|
1124
|
+
}
|
|
1125
|
+
})
|
|
1126
|
+
},
|
|
1127
|
+
language: language,
|
|
1128
|
+
outputFormat: {
|
|
1129
|
+
container: 'mp3',
|
|
1130
|
+
bitRate: 128000,
|
|
1131
|
+
sampleRate: 8000
|
|
1132
|
+
},
|
|
1133
|
+
});
|
|
1134
|
+
return Buffer.from(mp3);
|
|
1135
|
+
} catch (err) {
|
|
1136
|
+
logger.info({err}, 'synth Cartesia returned error');
|
|
1137
|
+
stats.increment('tts.count', ['vendor:cartesia', 'accepted:no']);
|
|
1138
|
+
throw err;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
};
|
|
1142
|
+
|
|
1070
1143
|
const getFileExtFromMime = (mime) => {
|
|
1071
1144
|
switch (mime) {
|
|
1072
1145
|
case 'audio/wav':
|
package/lib/utils.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jambonz/speech-utils",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.24",
|
|
4
4
|
"description": "TTS-related speech utilities for jambonz",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"author": "Dave Horton",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@aws-sdk/client-polly": "^3.496.0",
|
|
30
30
|
"@aws-sdk/client-sts": "^3.496.0",
|
|
31
|
+
"@cartesia/cartesia-js": "^2.1.0",
|
|
31
32
|
"@google-cloud/text-to-speech": "^5.5.0",
|
|
32
33
|
"@grpc/grpc-js": "^1.9.14",
|
|
33
34
|
"@jambonz/realtimedb-helpers": "^0.8.7",
|
package/test/synth.js
CHANGED
|
@@ -686,6 +686,40 @@ test('PlayHT3.0 speech synth tests', async(t) => {
|
|
|
686
686
|
await testPlayHT(t, 'Play3.0');
|
|
687
687
|
});
|
|
688
688
|
|
|
689
|
+
test('Cartesia speech synth tests', async(t) => {
|
|
690
|
+
const fn = require('..');
|
|
691
|
+
const {synthAudio, client} = fn(opts, logger);
|
|
692
|
+
|
|
693
|
+
if (!process.env.CARTESIA_API_KEY) {
|
|
694
|
+
t.pass('skipping Cartesia speech synth tests since CARTESIA_API_KEY is not provided');
|
|
695
|
+
return t.end();
|
|
696
|
+
}
|
|
697
|
+
const text = 'Hi there and welcome to jambones! ' + Date.now();
|
|
698
|
+
try {
|
|
699
|
+
const opts = await synthAudio(stats, {
|
|
700
|
+
vendor: 'cartesia',
|
|
701
|
+
credentials: {
|
|
702
|
+
api_key: process.env.CARTESIA_API_KEY,
|
|
703
|
+
model_id: 'sonic-english',
|
|
704
|
+
options: JSON.stringify({
|
|
705
|
+
speed: 1,
|
|
706
|
+
emotion: 'female_happy',
|
|
707
|
+
})
|
|
708
|
+
},
|
|
709
|
+
language: 'en',
|
|
710
|
+
voice: '694f9389-aac1-45b6-b726-9d9369183238',
|
|
711
|
+
text,
|
|
712
|
+
renderForCaching: true
|
|
713
|
+
});
|
|
714
|
+
t.ok(!opts.servedFromCache, `successfully playht eleven audio to ${opts.filePath}`);
|
|
715
|
+
|
|
716
|
+
} catch (err) {
|
|
717
|
+
console.error(JSON.stringify(err));
|
|
718
|
+
t.end(err);
|
|
719
|
+
}
|
|
720
|
+
client.quit();
|
|
721
|
+
});
|
|
722
|
+
|
|
689
723
|
test('rimelabs speech synth tests', async(t) => {
|
|
690
724
|
const fn = require('..');
|
|
691
725
|
const {synthAudio, client} = fn(opts, logger);
|