@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.
@@ -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
- const key = makeAwsKey(roleArn || accessKeyId);
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
- createHash(key, credentials, CACHE_EXPIRY)
45
- .catch((err) => logger.error(err, `Error saving hash for key ${key}`));
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) {
@@ -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
@@ -50,6 +50,7 @@ function getFileExtension({vendor, voice, renderForCaching = false}) {
50
50
  case 'elevenlabs':
51
51
  case 'rimelabs':
52
52
  case 'playht':
53
+ case 'cartesia':
53
54
  if (renderForCaching || JAMBONES_DISABLE_TTS_STREAMING) {
54
55
  return mp3Extension;
55
56
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jambonz/speech-utils",
3
- "version": "0.1.22",
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);