@jambonz/speech-utils 0.0.50 → 0.1.0
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/.github/workflows/ci.yml +4 -2
- package/index.js +2 -2
- package/lib/get-aws-sts-token.js +24 -15
- package/lib/get-tts-voices.js +22 -11
- package/lib/synth-audio.js +86 -16
- package/package.json +1 -1
- package/test/synth.js +63 -2
package/.github/workflows/ci.yml
CHANGED
|
@@ -8,8 +8,8 @@ jobs:
|
|
|
8
8
|
build:
|
|
9
9
|
runs-on: ubuntu-latest
|
|
10
10
|
steps:
|
|
11
|
-
- uses: actions/checkout@
|
|
12
|
-
- uses: actions/setup-node@
|
|
11
|
+
- uses: actions/checkout@v4
|
|
12
|
+
- uses: actions/setup-node@v4
|
|
13
13
|
with:
|
|
14
14
|
node-version: lts/*
|
|
15
15
|
- run: npm install
|
|
@@ -32,5 +32,7 @@ jobs:
|
|
|
32
32
|
ELEVENLABS_API_KEY: ${{ secrets.ELEVENLABS_API_KEY }}
|
|
33
33
|
ELEVENLABS_VOICE_ID: ${{ secrets.ELEVENLABS_VOICE_ID }}
|
|
34
34
|
ELEVENLABS_MODEL_ID: ${{ secrets.ELEVENLABS_MODEL_ID }}
|
|
35
|
+
PLAYHT_USER_ID: ${{ secrets.PLAYHT_USER_ID }}
|
|
36
|
+
PLAYHT_API_KEY: ${{ secrets.PLAYHT_API_KEY }}
|
|
35
37
|
JAMBONES_HTTP_PROXY_IP: 127.0.0.1
|
|
36
38
|
JAMBONES_HTTP_PROXY_PORT: 3128
|
package/index.js
CHANGED
|
@@ -13,10 +13,10 @@ module.exports = (opts, logger) => {
|
|
|
13
13
|
getTtsSize: require('./lib/get-tts-size').bind(null, client, logger),
|
|
14
14
|
purgeTtsCache: require('./lib/purge-tts-cache').bind(null, client, logger),
|
|
15
15
|
addFileToCache: require('./lib/add-file-to-cache').bind(null, client, logger),
|
|
16
|
-
synthAudio: require('./lib/synth-audio').bind(null, client, logger),
|
|
16
|
+
synthAudio: require('./lib/synth-audio').bind(null, client, createHash, retrieveHash, logger),
|
|
17
17
|
getNuanceAccessToken: require('./lib/get-nuance-access-token').bind(null, client, logger),
|
|
18
18
|
getIbmAccessToken: require('./lib/get-ibm-access-token').bind(null, client, logger),
|
|
19
19
|
getAwsAuthToken: require('./lib/get-aws-sts-token').bind(null, logger, createHash, retrieveHash),
|
|
20
|
-
getTtsVoices: require('./lib/get-tts-voices').bind(null, client, logger),
|
|
20
|
+
getTtsVoices: require('./lib/get-tts-voices').bind(null, client, createHash, retrieveHash, logger),
|
|
21
21
|
};
|
|
22
22
|
};
|
package/lib/get-aws-sts-token.js
CHANGED
|
@@ -1,32 +1,41 @@
|
|
|
1
|
-
const { STSClient, GetSessionTokenCommand } = require('@aws-sdk/client-sts');
|
|
1
|
+
const { STSClient, GetSessionTokenCommand, AssumeRoleCommand } = require('@aws-sdk/client-sts');
|
|
2
2
|
const {makeAwsKey, noopLogger} = require('./utils');
|
|
3
3
|
const debug = require('debug')('jambonz:speech-utils');
|
|
4
4
|
const EXPIRY = 3600;
|
|
5
5
|
|
|
6
6
|
async function getAwsAuthToken(
|
|
7
|
-
logger,
|
|
8
|
-
|
|
9
|
-
awsAccessKeyId, awsSecretAccessKey, awsRegion) {
|
|
7
|
+
logger, createHash, retrieveHash,
|
|
8
|
+
awsAccessKeyId, awsSecretAccessKey, awsRegion, roleArn = null) {
|
|
10
9
|
logger = logger || noopLogger;
|
|
11
10
|
try {
|
|
12
|
-
const key = makeAwsKey(awsAccessKeyId);
|
|
11
|
+
const key = makeAwsKey(roleArn || awsAccessKeyId);
|
|
13
12
|
const obj = await retrieveHash(key);
|
|
14
13
|
if (obj) return {...obj, servedFromCache: true};
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
region: awsRegion
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
|
|
15
|
+
let data;
|
|
16
|
+
if (roleArn) {
|
|
17
|
+
const stsClient = new STSClient({ region: awsRegion});
|
|
18
|
+
const roleToAssume = { RoleArn: roleArn, RoleSessionName: 'Jambonz_Speech', DurationSeconds: EXPIRY};
|
|
19
|
+
const command = new AssumeRoleCommand(roleToAssume);
|
|
20
|
+
|
|
21
|
+
data = await stsClient.send(command);
|
|
22
|
+
} else {
|
|
23
|
+
/* access token not found in cache, so generate it using STS */
|
|
24
|
+
const stsClient = new STSClient({
|
|
25
|
+
region: awsRegion,
|
|
26
|
+
credentials: {
|
|
27
|
+
accessKeyId: awsAccessKeyId,
|
|
28
|
+
secretAccessKey: awsSecretAccessKey,
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
const command = new GetSessionTokenCommand({DurationSeconds: EXPIRY});
|
|
32
|
+
data = await stsClient.send(command);
|
|
33
|
+
}
|
|
26
34
|
|
|
27
35
|
const credentials = {
|
|
28
36
|
accessKeyId: data.Credentials.AccessKeyId,
|
|
29
37
|
secretAccessKey: data.Credentials.SecretAccessKey,
|
|
38
|
+
sessionToken: data.Credentials.SessionToken,
|
|
30
39
|
securityToken: data.Credentials.SessionToken
|
|
31
40
|
};
|
|
32
41
|
|
package/lib/get-tts-voices.js
CHANGED
|
@@ -6,6 +6,7 @@ const TextToSpeechV1 = require('ibm-watson/text-to-speech/v1');
|
|
|
6
6
|
const { IamAuthenticator } = require('ibm-watson/auth');
|
|
7
7
|
const ttsGoogle = require('@google-cloud/text-to-speech');
|
|
8
8
|
const { PollyClient, DescribeVoicesCommand } = require('@aws-sdk/client-polly');
|
|
9
|
+
const getAwsAuthToken = require('./get-aws-sts-token');
|
|
9
10
|
|
|
10
11
|
const getIbmVoices = async(client, logger, credentials) => {
|
|
11
12
|
const {tts_region, tts_api_key} = credentials;
|
|
@@ -87,16 +88,26 @@ const getGoogleVoices = async(_client, logger, credentials) => {
|
|
|
87
88
|
return await client.listVoices();
|
|
88
89
|
};
|
|
89
90
|
|
|
90
|
-
const getAwsVoices = async(_client, logger, credentials) => {
|
|
91
|
+
const getAwsVoices = async(_client, createHash, retrieveHash, logger, credentials) => {
|
|
91
92
|
try {
|
|
92
|
-
const {region, accessKeyId, secretAccessKey} = credentials;
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
93
|
+
const {region, accessKeyId, secretAccessKey, roleArn} = credentials;
|
|
94
|
+
let client = null;
|
|
95
|
+
if (accessKeyId && secretAccessKey) {
|
|
96
|
+
client = new PollyClient({
|
|
97
|
+
region,
|
|
98
|
+
credentials: {
|
|
99
|
+
accessKeyId,
|
|
100
|
+
secretAccessKey
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
} else if (roleArn) {
|
|
104
|
+
client = new PollyClient({
|
|
105
|
+
region,
|
|
106
|
+
credentials: await getAwsAuthToken(logger, createHash, retrieveHash, null, null, region, roleArn),
|
|
107
|
+
});
|
|
108
|
+
} else {
|
|
109
|
+
client = new PollyClient({region});
|
|
110
|
+
}
|
|
100
111
|
const command = new DescribeVoicesCommand({});
|
|
101
112
|
const response = await client.send(command);
|
|
102
113
|
return response;
|
|
@@ -122,7 +133,7 @@ const getAwsVoices = async(_client, logger, credentials) => {
|
|
|
122
133
|
* @returns object containing filepath to an mp3 file in the /tmp folder containing
|
|
123
134
|
* the synthesized audio, and a variable indicating whether it was served from cache
|
|
124
135
|
*/
|
|
125
|
-
async function getTtsVoices(client, logger, {vendor, credentials}) {
|
|
136
|
+
async function getTtsVoices(client, createHash, retrieveHash, logger, {vendor, credentials}) {
|
|
126
137
|
logger = logger || noopLogger;
|
|
127
138
|
|
|
128
139
|
assert.ok(['nuance', 'ibm', 'google', 'aws', 'polly'].includes(vendor),
|
|
@@ -137,7 +148,7 @@ async function getTtsVoices(client, logger, {vendor, credentials}) {
|
|
|
137
148
|
return getGoogleVoices(client, logger, credentials);
|
|
138
149
|
case 'aws':
|
|
139
150
|
case 'polly':
|
|
140
|
-
return getAwsVoices(client, logger, credentials);
|
|
151
|
+
return getAwsVoices(client, createHash, retrieveHash, logger, credentials);
|
|
141
152
|
default:
|
|
142
153
|
break;
|
|
143
154
|
}
|
package/lib/synth-audio.js
CHANGED
|
@@ -39,6 +39,7 @@ const debug = require('debug')('jambonz:realtimedb-helpers');
|
|
|
39
39
|
const EXPIRES = (process.env.JAMBONES_TTS_CACHE_DURATION_MINS || 4 * 60) * 60; // cache tts for 4 hours
|
|
40
40
|
const TMP_FOLDER = '/tmp';
|
|
41
41
|
const OpenAI = require('openai');
|
|
42
|
+
const getAwsAuthToken = require('./get-aws-sts-token');
|
|
42
43
|
|
|
43
44
|
|
|
44
45
|
const trimTrailingSilence = (buffer) => {
|
|
@@ -75,7 +76,7 @@ const trimTrailingSilence = (buffer) => {
|
|
|
75
76
|
* @returns object containing filepath to an mp3 file in the /tmp folder containing
|
|
76
77
|
* the synthesized audio, and a variable indicating whether it was served from cache
|
|
77
78
|
*/
|
|
78
|
-
async function synthAudio(client, logger, stats, { account_sid,
|
|
79
|
+
async function synthAudio(client, createHash, retrieveHash, logger, stats, { account_sid,
|
|
79
80
|
vendor, language, voice, gender, text, engine, salt, model, credentials, deploymentId,
|
|
80
81
|
disableTtsCache, renderForCaching, disableTtsStreaming, options
|
|
81
82
|
}) {
|
|
@@ -84,10 +85,10 @@ async function synthAudio(client, logger, stats, { account_sid,
|
|
|
84
85
|
let rtt;
|
|
85
86
|
logger = logger || noopLogger;
|
|
86
87
|
|
|
87
|
-
assert.ok(['google', 'aws', 'polly', 'microsoft',
|
|
88
|
-
'
|
|
88
|
+
assert.ok(['google', 'aws', 'polly', 'microsoft', 'wellsaid', 'nuance', 'nvidia', 'ibm', 'elevenlabs',
|
|
89
|
+
'whisper', 'deepgram', 'playht', 'rimelabs'].includes(vendor) ||
|
|
89
90
|
vendor.startsWith('custom'),
|
|
90
|
-
`synthAudio supported vendors are google, aws, microsoft, nuance, nvidia and wellsaid, not ${vendor}`);
|
|
91
|
+
`synthAudio supported vendors are google, aws, microsoft, nuance, nvidia and wellsaid ..etc, not ${vendor}`);
|
|
91
92
|
if ('google' === vendor) {
|
|
92
93
|
assert.ok(language, 'synthAudio requires language when google is used');
|
|
93
94
|
}
|
|
@@ -123,11 +124,15 @@ async function synthAudio(client, logger, stats, { account_sid,
|
|
|
123
124
|
assert.ok(voice, 'synthAudio requires voice when elevenlabs is used');
|
|
124
125
|
assert.ok(credentials.api_key, 'synthAudio requires api_key when elevenlabs is used');
|
|
125
126
|
assert.ok(credentials.model_id, 'synthAudio requires model_id when elevenlabs is used');
|
|
126
|
-
}
|
|
127
|
+
} else if ('playht' === vendor) {
|
|
127
128
|
assert.ok(voice, 'synthAudio requires voice when playht is used');
|
|
128
129
|
assert.ok(credentials.api_key, 'synthAudio requires api_key when playht is used');
|
|
129
130
|
assert.ok(credentials.user_id, 'synthAudio requires user_id when playht is used');
|
|
130
131
|
assert.ok(credentials.voice_engine, 'synthAudio requires voice_engine when playht is used');
|
|
132
|
+
} else if ('rimelabs' === vendor) {
|
|
133
|
+
assert.ok(voice, 'synthAudio requires voice when rimelabs is used');
|
|
134
|
+
assert.ok(credentials.api_key, 'synthAudio requires api_key when rimelabs is used');
|
|
135
|
+
assert.ok(credentials.model_id, 'synthAudio requires model_id when rimelabs is used');
|
|
131
136
|
} else if ('whisper' === vendor) {
|
|
132
137
|
assert.ok(voice, 'synthAudio requires voice when whisper is used');
|
|
133
138
|
assert.ok(credentials.model_id, 'synthAudio requires model when whisper is used');
|
|
@@ -151,7 +156,7 @@ async function synthAudio(client, logger, stats, { account_sid,
|
|
|
151
156
|
) ||
|
|
152
157
|
(
|
|
153
158
|
!process.env.JAMBONES_DISABLE_TTS_STREAMING &&
|
|
154
|
-
['elevenlabs', 'deepgram'].includes(vendor)
|
|
159
|
+
['elevenlabs', 'deepgram', 'rimelabs'].includes(vendor)
|
|
155
160
|
)
|
|
156
161
|
) {
|
|
157
162
|
filePath = `${TMP_FOLDER}/${key.replace('tts:', `tts-${salt || ''}`)}.r8`;
|
|
@@ -183,7 +188,8 @@ async function synthAudio(client, logger, stats, { account_sid,
|
|
|
183
188
|
case 'aws':
|
|
184
189
|
case 'polly':
|
|
185
190
|
vendorLabel = 'aws';
|
|
186
|
-
audioBuffer = await synthPolly(
|
|
191
|
+
audioBuffer = await synthPolly(createHash, retrieveHash, logger,
|
|
192
|
+
{credentials, stats, language, voice, text, engine});
|
|
187
193
|
break;
|
|
188
194
|
case 'azure':
|
|
189
195
|
case 'microsoft':
|
|
@@ -217,6 +223,12 @@ async function synthAudio(client, logger, stats, { account_sid,
|
|
|
217
223
|
});
|
|
218
224
|
if (audioBuffer?.filePath) return audioBuffer;
|
|
219
225
|
break;
|
|
226
|
+
case 'rimelabs':
|
|
227
|
+
audioBuffer = await synthRimelabs(logger, {
|
|
228
|
+
credentials, options, stats, language, voice, text, renderForCaching, disableTtsStreaming, filePath
|
|
229
|
+
});
|
|
230
|
+
if (audioBuffer?.filePath) return audioBuffer;
|
|
231
|
+
break;
|
|
220
232
|
case 'whisper':
|
|
221
233
|
audioBuffer = await synthWhisper(logger, {
|
|
222
234
|
credentials, stats, voice, text, renderForCaching, disableTtsStreaming});
|
|
@@ -253,16 +265,28 @@ async function synthAudio(client, logger, stats, { account_sid,
|
|
|
253
265
|
});
|
|
254
266
|
}
|
|
255
267
|
|
|
256
|
-
const synthPolly = async(
|
|
268
|
+
const synthPolly = async(createHash, retrieveHash, logger,
|
|
269
|
+
{credentials, stats, language, voice, engine, text}) => {
|
|
257
270
|
try {
|
|
258
|
-
const {region, accessKeyId, secretAccessKey} = credentials;
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
271
|
+
const {region, accessKeyId, secretAccessKey, roleArn} = credentials;
|
|
272
|
+
let polly;
|
|
273
|
+
if (accessKeyId && secretAccessKey) {
|
|
274
|
+
polly = new PollyClient({
|
|
275
|
+
region,
|
|
276
|
+
credentials: {
|
|
277
|
+
accessKeyId,
|
|
278
|
+
secretAccessKey
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
} else if (roleArn) {
|
|
282
|
+
polly = new PollyClient({
|
|
283
|
+
region,
|
|
284
|
+
credentials: await getAwsAuthToken(logger, createHash, retrieveHash, null, null, region, roleArn),
|
|
285
|
+
});
|
|
286
|
+
} else {
|
|
287
|
+
// AWS RoleArn assigned to Instance profile
|
|
288
|
+
polly = new PollyClient({region});
|
|
289
|
+
}
|
|
266
290
|
const opts = {
|
|
267
291
|
Engine: engine,
|
|
268
292
|
OutputFormat: 'mp3',
|
|
@@ -748,6 +772,52 @@ const synthPlayHT = async(logger, {
|
|
|
748
772
|
}
|
|
749
773
|
};
|
|
750
774
|
|
|
775
|
+
const synthRimelabs = async(logger, {
|
|
776
|
+
credentials, options, stats, voice, text, renderForCaching, disableTtsStreaming
|
|
777
|
+
}) => {
|
|
778
|
+
const {api_key, model_id, options: credOpts} = credentials;
|
|
779
|
+
const opts = !!options && Object.keys(options).length !== 0 ? options : JSON.parse(credOpts || '{}');
|
|
780
|
+
|
|
781
|
+
/* default to using the streaming interface, unless disabled by env var OR we want just a cache file */
|
|
782
|
+
if (!process.env.JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
|
|
783
|
+
let params = '';
|
|
784
|
+
params += `{api_key=${api_key}`;
|
|
785
|
+
params += `,model_id=${model_id}`;
|
|
786
|
+
params += ',vendor=rimelabs';
|
|
787
|
+
params += `,voice=${voice}`;
|
|
788
|
+
params += ',write_cache_file=1';
|
|
789
|
+
if (opts.speedAlpha) params += `,speed_alpha=${opts.speedAlpha}`;
|
|
790
|
+
if (opts.reduceLatency) params += `,reduce_latency=${opts.reduceLatency}`;
|
|
791
|
+
params += '}';
|
|
792
|
+
|
|
793
|
+
return {
|
|
794
|
+
filePath: `say:${params}${text.replace(/\n/g, ' ').replace(/\r/g, ' ')}`,
|
|
795
|
+
servedFromCache: false,
|
|
796
|
+
rtt: 0
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
try {
|
|
801
|
+
const post = bent('https://users.rime.ai', 'POST', 'buffer', {
|
|
802
|
+
'Authorization': `Bearer ${api_key}`,
|
|
803
|
+
'Accept': 'audio/mp3',
|
|
804
|
+
'Content-Type': 'application/json'
|
|
805
|
+
});
|
|
806
|
+
const mp3 = await post('/v1/rime-tts', {
|
|
807
|
+
speaker: voice,
|
|
808
|
+
text,
|
|
809
|
+
modelId: model_id,
|
|
810
|
+
samplingRate: 8000,
|
|
811
|
+
...opts
|
|
812
|
+
});
|
|
813
|
+
return mp3;
|
|
814
|
+
} catch (err) {
|
|
815
|
+
logger.info({err}, 'synth rimelabs returned error');
|
|
816
|
+
stats.increment('tts.count', ['vendor:rimelabs', 'accepted:no']);
|
|
817
|
+
throw err;
|
|
818
|
+
}
|
|
819
|
+
};
|
|
820
|
+
|
|
751
821
|
const synthWhisper = async(logger, {credentials, stats, voice, text, renderForCaching, disableTtsStreaming}) => {
|
|
752
822
|
const {api_key, model_id, baseURL, timeout, speed} = credentials;
|
|
753
823
|
/* if the env is set to stream then bag out, unless we are specifically rendering to generate a cache file */
|
package/package.json
CHANGED
package/test/synth.js
CHANGED
|
@@ -162,6 +162,33 @@ test('AWS speech synth tests', async(t) => {
|
|
|
162
162
|
client.quit();
|
|
163
163
|
});
|
|
164
164
|
|
|
165
|
+
test('AWS speech synth tests by RoleArn', async(t) => {
|
|
166
|
+
const fn = require('..');
|
|
167
|
+
const {synthAudio, client} = fn(opts, logger);
|
|
168
|
+
|
|
169
|
+
if (!process.env.AWS_ROLE_ARN || !process.env.AWS_REGION) {
|
|
170
|
+
t.pass('skipping AWS speech synth tests by RoleArn since AWS_ROLE_ARN or AWS_REGION not provided');
|
|
171
|
+
return t.end();
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
let opts = await synthAudio(stats, {
|
|
175
|
+
vendor: 'aws',
|
|
176
|
+
credentials: {
|
|
177
|
+
roleArn: process.env.AWS_ROLE_ARN,
|
|
178
|
+
region: process.env.AWS_REGION,
|
|
179
|
+
},
|
|
180
|
+
language: 'en-US',
|
|
181
|
+
voice: 'Joey',
|
|
182
|
+
text: 'This is a test. This is only a test',
|
|
183
|
+
});
|
|
184
|
+
t.ok(!opts.servedFromCache, `successfully synthesized aws by roleArn audio to ${opts.filePath}`);
|
|
185
|
+
} catch (err) {
|
|
186
|
+
console.error(err);
|
|
187
|
+
t.end(err);
|
|
188
|
+
}
|
|
189
|
+
client.quit();
|
|
190
|
+
});
|
|
191
|
+
|
|
165
192
|
test('Azure speech synth tests', async(t) => {
|
|
166
193
|
const fn = require('..');
|
|
167
194
|
const {synthAudio, client} = fn(opts, logger);
|
|
@@ -581,14 +608,48 @@ test('PlayHT speech synth tests', async(t) => {
|
|
|
581
608
|
text,
|
|
582
609
|
renderForCaching: true
|
|
583
610
|
});
|
|
584
|
-
t.ok(!opts.servedFromCache, `successfully
|
|
611
|
+
t.ok(!opts.servedFromCache, `successfully playht eleven audio to ${opts.filePath}`);
|
|
585
612
|
|
|
586
613
|
} catch (err) {
|
|
587
614
|
console.error(JSON.stringify(err));
|
|
588
615
|
t.end(err);
|
|
589
616
|
}
|
|
590
617
|
client.quit();
|
|
591
|
-
})
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
test('rimelabs speech synth tests', async(t) => {
|
|
621
|
+
const fn = require('..');
|
|
622
|
+
const {synthAudio, client} = fn(opts, logger);
|
|
623
|
+
|
|
624
|
+
if (!process.env.RIMELABS_API_KEY) {
|
|
625
|
+
t.pass('skipping rimelabs speech synth tests since RIMELABS_API_KEY is not provided');
|
|
626
|
+
return t.end();
|
|
627
|
+
}
|
|
628
|
+
const text = 'Hi there and welcome to jambones!';
|
|
629
|
+
try {
|
|
630
|
+
let opts = await synthAudio(stats, {
|
|
631
|
+
vendor: 'rimelabs',
|
|
632
|
+
credentials: {
|
|
633
|
+
api_key: process.env.RIMELABS_API_KEY,
|
|
634
|
+
model_id: 'mist',
|
|
635
|
+
options: JSON.stringify({
|
|
636
|
+
speedAlpha: 1.0,
|
|
637
|
+
reduceLatency: false
|
|
638
|
+
})
|
|
639
|
+
},
|
|
640
|
+
language: 'en-US',
|
|
641
|
+
voice: 'amber',
|
|
642
|
+
text,
|
|
643
|
+
renderForCaching: true
|
|
644
|
+
});
|
|
645
|
+
t.ok(!opts.servedFromCache, `successfully synthesized rimelabs audio to ${opts.filePath}`);
|
|
646
|
+
|
|
647
|
+
} catch (err) {
|
|
648
|
+
console.error(JSON.stringify(err));
|
|
649
|
+
t.end(err);
|
|
650
|
+
}
|
|
651
|
+
client.quit();
|
|
652
|
+
});
|
|
592
653
|
|
|
593
654
|
test('whisper speech synth tests', async(t) => {
|
|
594
655
|
const fn = require('..');
|