@jambonz/speech-utils 0.0.51 → 0.1.1
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/index.js +3 -2
- package/lib/constants.js +3 -0
- package/lib/get-aws-sts-token.js +24 -15
- package/lib/get-ibm-access-token.js +1 -1
- package/lib/get-nuance-access-token.js +1 -1
- package/lib/get-tts-voices.js +49 -12
- package/lib/get-verbio-token.js +51 -0
- package/lib/synth-audio.js +81 -15
- package/lib/utils.js +8 -1
- package/package.json +1 -1
- package/test/list-voices.js +26 -0
- package/test/synth.js +61 -0
package/index.js
CHANGED
|
@@ -13,10 +13,11 @@ 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
|
+
getVerbioAccessToken: require('./lib/get-verbio-token').bind(null, client, logger),
|
|
17
18
|
getNuanceAccessToken: require('./lib/get-nuance-access-token').bind(null, client, logger),
|
|
18
19
|
getIbmAccessToken: require('./lib/get-ibm-access-token').bind(null, client, logger),
|
|
19
20
|
getAwsAuthToken: require('./lib/get-aws-sts-token').bind(null, logger, createHash, retrieveHash),
|
|
20
|
-
getTtsVoices: require('./lib/get-tts-voices').bind(null, client, logger),
|
|
21
|
+
getTtsVoices: require('./lib/get-tts-voices').bind(null, client, createHash, retrieveHash, logger),
|
|
21
22
|
};
|
|
22
23
|
};
|
package/lib/constants.js
ADDED
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
|
|
|
@@ -2,8 +2,8 @@ const formurlencoded = require('form-urlencoded');
|
|
|
2
2
|
const {Pool} = require('undici');
|
|
3
3
|
const pool = new Pool('https://iam.cloud.ibm.com');
|
|
4
4
|
const {makeIbmKey, noopLogger} = require('./utils');
|
|
5
|
+
const { HTTP_TIMEOUT } = require('./constants');
|
|
5
6
|
const debug = require('debug')('jambonz:realtimedb-helpers');
|
|
6
|
-
const HTTP_TIMEOUT = 5000;
|
|
7
7
|
|
|
8
8
|
async function getIbmAccessToken(client, logger, apiKey) {
|
|
9
9
|
logger = logger || noopLogger;
|
|
@@ -2,8 +2,8 @@ const formurlencoded = require('form-urlencoded');
|
|
|
2
2
|
const {Pool} = require('undici');
|
|
3
3
|
const pool = new Pool('https://auth.crt.nuance.com');
|
|
4
4
|
const {makeNuanceKey, makeBasicAuthHeader, noopLogger} = require('./utils');
|
|
5
|
+
const { HTTP_TIMEOUT } = require('./constants');
|
|
5
6
|
const debug = require('debug')('jambonz:realtimedb-helpers');
|
|
6
|
-
const HTTP_TIMEOUT = 5000;
|
|
7
7
|
|
|
8
8
|
async function getNuanceAccessToken(client, logger, clientId, secret, scope) {
|
|
9
9
|
logger = logger || noopLogger;
|
package/lib/get-tts-voices.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
const assert = require('assert');
|
|
2
2
|
const {noopLogger, createNuanceClient, createKryptonClient} = require('./utils');
|
|
3
3
|
const getNuanceAccessToken = require('./get-nuance-access-token');
|
|
4
|
+
const getVerbioAccessToken = require('./get-verbio-token');
|
|
4
5
|
const {GetVoicesRequest, Voice} = require('../stubs/nuance/synthesizer_pb');
|
|
5
6
|
const TextToSpeechV1 = require('ibm-watson/text-to-speech/v1');
|
|
6
7
|
const { IamAuthenticator } = require('ibm-watson/auth');
|
|
7
8
|
const ttsGoogle = require('@google-cloud/text-to-speech');
|
|
8
9
|
const { PollyClient, DescribeVoicesCommand } = require('@aws-sdk/client-polly');
|
|
10
|
+
const getAwsAuthToken = require('./get-aws-sts-token');
|
|
11
|
+
const {Pool} = require('undici');
|
|
12
|
+
const { HTTP_TIMEOUT } = require('./constants');
|
|
13
|
+
const verbioVoicePool = new Pool('https://us.rest.speechcenter.verbio.com');
|
|
9
14
|
|
|
10
15
|
const getIbmVoices = async(client, logger, credentials) => {
|
|
11
16
|
const {tts_region, tts_api_key} = credentials;
|
|
@@ -87,16 +92,26 @@ const getGoogleVoices = async(_client, logger, credentials) => {
|
|
|
87
92
|
return await client.listVoices();
|
|
88
93
|
};
|
|
89
94
|
|
|
90
|
-
const getAwsVoices = async(_client, logger, credentials) => {
|
|
95
|
+
const getAwsVoices = async(_client, createHash, retrieveHash, logger, credentials) => {
|
|
91
96
|
try {
|
|
92
|
-
const {region, accessKeyId, secretAccessKey} = credentials;
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
97
|
+
const {region, accessKeyId, secretAccessKey, roleArn} = credentials;
|
|
98
|
+
let client = null;
|
|
99
|
+
if (accessKeyId && secretAccessKey) {
|
|
100
|
+
client = new PollyClient({
|
|
101
|
+
region,
|
|
102
|
+
credentials: {
|
|
103
|
+
accessKeyId,
|
|
104
|
+
secretAccessKey
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
} else if (roleArn) {
|
|
108
|
+
client = new PollyClient({
|
|
109
|
+
region,
|
|
110
|
+
credentials: await getAwsAuthToken(logger, createHash, retrieveHash, null, null, region, roleArn),
|
|
111
|
+
});
|
|
112
|
+
} else {
|
|
113
|
+
client = new PollyClient({region});
|
|
114
|
+
}
|
|
100
115
|
const command = new DescribeVoicesCommand({});
|
|
101
116
|
const response = await client.send(command);
|
|
102
117
|
return response;
|
|
@@ -106,6 +121,26 @@ const getAwsVoices = async(_client, logger, credentials) => {
|
|
|
106
121
|
}
|
|
107
122
|
};
|
|
108
123
|
|
|
124
|
+
const getVerbioVoices = async(client, logger, credentials) => {
|
|
125
|
+
try {
|
|
126
|
+
const access_token = await getVerbioAccessToken(client, logger, credentials);
|
|
127
|
+
const { body} = await verbioVoicePool.request({
|
|
128
|
+
path: '/api/v1/voices',
|
|
129
|
+
method: 'GET',
|
|
130
|
+
headers: {
|
|
131
|
+
'Authorization': `Bearer ${access_token.access_token}`,
|
|
132
|
+
'User-Agent': 'jambonz'
|
|
133
|
+
},
|
|
134
|
+
timeout: HTTP_TIMEOUT,
|
|
135
|
+
followRedirects: false
|
|
136
|
+
});
|
|
137
|
+
return await body.json();
|
|
138
|
+
} catch (err) {
|
|
139
|
+
logger.info({err}, 'getVerbioVoices - failed to list voices for Verbio');
|
|
140
|
+
throw err;
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
109
144
|
/**
|
|
110
145
|
* Synthesize speech to an mp3 file, and also cache the generated speech
|
|
111
146
|
* in redis (base64 format) for 24 hours so as to avoid unnecessarily paying
|
|
@@ -122,10 +157,10 @@ const getAwsVoices = async(_client, logger, credentials) => {
|
|
|
122
157
|
* @returns object containing filepath to an mp3 file in the /tmp folder containing
|
|
123
158
|
* the synthesized audio, and a variable indicating whether it was served from cache
|
|
124
159
|
*/
|
|
125
|
-
async function getTtsVoices(client, logger, {vendor, credentials}) {
|
|
160
|
+
async function getTtsVoices(client, createHash, retrieveHash, logger, {vendor, credentials}) {
|
|
126
161
|
logger = logger || noopLogger;
|
|
127
162
|
|
|
128
|
-
assert.ok(['nuance', 'ibm', 'google', 'aws', 'polly'].includes(vendor),
|
|
163
|
+
assert.ok(['nuance', 'ibm', 'google', 'aws', 'polly', 'verbio'].includes(vendor),
|
|
129
164
|
`getTtsVoices not supported for vendor ${vendor}`);
|
|
130
165
|
|
|
131
166
|
switch (vendor) {
|
|
@@ -137,7 +172,9 @@ async function getTtsVoices(client, logger, {vendor, credentials}) {
|
|
|
137
172
|
return getGoogleVoices(client, logger, credentials);
|
|
138
173
|
case 'aws':
|
|
139
174
|
case 'polly':
|
|
140
|
-
return getAwsVoices(client, logger, credentials);
|
|
175
|
+
return getAwsVoices(client, createHash, retrieveHash, logger, credentials);
|
|
176
|
+
case 'verbio':
|
|
177
|
+
return getVerbioVoices(client, logger, credentials);
|
|
141
178
|
default:
|
|
142
179
|
break;
|
|
143
180
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const {Pool} = require('undici');
|
|
2
|
+
const { noopLogger, makeVerbioKey } = require('./utils');
|
|
3
|
+
const { HTTP_TIMEOUT } = require('./constants');
|
|
4
|
+
const pool = new Pool('https://auth.speechcenter.verbio.com:444');
|
|
5
|
+
const debug = require('debug')('jambonz:realtimedb-helpers');
|
|
6
|
+
|
|
7
|
+
async function getVerbioAccessToken(client, logger, credentials) {
|
|
8
|
+
logger = logger || noopLogger;
|
|
9
|
+
const { client_id, client_secret } = credentials;
|
|
10
|
+
try {
|
|
11
|
+
const key = makeVerbioKey(client_id);
|
|
12
|
+
const access_token = await client.get(key);
|
|
13
|
+
if (access_token) {
|
|
14
|
+
return {access_token, servedFromCache: true};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const payload = {
|
|
18
|
+
client_id,
|
|
19
|
+
client_secret
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const {statusCode, headers, body} = await pool.request({
|
|
23
|
+
path: '/api/v1/token',
|
|
24
|
+
method: 'POST',
|
|
25
|
+
headers: {
|
|
26
|
+
'Content-Type': 'application/json',
|
|
27
|
+
'User-Agent': 'jambonz'
|
|
28
|
+
},
|
|
29
|
+
body: JSON.stringify(payload),
|
|
30
|
+
timeout: HTTP_TIMEOUT,
|
|
31
|
+
followRedirects: false
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
if (200 !== statusCode) {
|
|
35
|
+
logger.debug({statusCode, headers, body: await body.text()}, 'error fetching access token from Verbio');
|
|
36
|
+
const err = new Error();
|
|
37
|
+
err.statusCode = statusCode;
|
|
38
|
+
throw err;
|
|
39
|
+
}
|
|
40
|
+
const json = await body.json();
|
|
41
|
+
const expiry = Math.floor(json.expiration_time - Date.now() / 1000 - 30);
|
|
42
|
+
await client.set(key, json.access_token, 'EX', expiry);
|
|
43
|
+
return {...json, servedFromCache: false};
|
|
44
|
+
} catch (err) {
|
|
45
|
+
debug(err, `getVerbioAccessToken: Error retrieving Verbio access token for client_id ${client_id}`);
|
|
46
|
+
logger.error(err, `getVerbioAccessToken: Error retrieving Verbio access token for client_id ${client_id}`);
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = getVerbioAccessToken;
|
package/lib/synth-audio.js
CHANGED
|
@@ -22,6 +22,7 @@ const {
|
|
|
22
22
|
noopLogger
|
|
23
23
|
} = require('./utils');
|
|
24
24
|
const getNuanceAccessToken = require('./get-nuance-access-token');
|
|
25
|
+
const getVerbioAccessToken = require('./get-verbio-token');
|
|
25
26
|
const {
|
|
26
27
|
SynthesisRequest,
|
|
27
28
|
Voice,
|
|
@@ -39,6 +40,7 @@ const debug = require('debug')('jambonz:realtimedb-helpers');
|
|
|
39
40
|
const EXPIRES = (process.env.JAMBONES_TTS_CACHE_DURATION_MINS || 4 * 60) * 60; // cache tts for 4 hours
|
|
40
41
|
const TMP_FOLDER = '/tmp';
|
|
41
42
|
const OpenAI = require('openai');
|
|
43
|
+
const getAwsAuthToken = require('./get-aws-sts-token');
|
|
42
44
|
|
|
43
45
|
|
|
44
46
|
const trimTrailingSilence = (buffer) => {
|
|
@@ -75,7 +77,7 @@ const trimTrailingSilence = (buffer) => {
|
|
|
75
77
|
* @returns object containing filepath to an mp3 file in the /tmp folder containing
|
|
76
78
|
* the synthesized audio, and a variable indicating whether it was served from cache
|
|
77
79
|
*/
|
|
78
|
-
async function synthAudio(client, logger, stats, { account_sid,
|
|
80
|
+
async function synthAudio(client, createHash, retrieveHash, logger, stats, { account_sid,
|
|
79
81
|
vendor, language, voice, gender, text, engine, salt, model, credentials, deploymentId,
|
|
80
82
|
disableTtsCache, renderForCaching, disableTtsStreaming, options
|
|
81
83
|
}) {
|
|
@@ -85,7 +87,7 @@ async function synthAudio(client, logger, stats, { account_sid,
|
|
|
85
87
|
logger = logger || noopLogger;
|
|
86
88
|
|
|
87
89
|
assert.ok(['google', 'aws', 'polly', 'microsoft', 'wellsaid', 'nuance', 'nvidia', 'ibm', 'elevenlabs',
|
|
88
|
-
'whisper', 'deepgram', 'playht', 'rimelabs'].includes(vendor) ||
|
|
90
|
+
'whisper', 'deepgram', 'playht', 'rimelabs', 'verbio'].includes(vendor) ||
|
|
89
91
|
vendor.startsWith('custom'),
|
|
90
92
|
`synthAudio supported vendors are google, aws, microsoft, nuance, nvidia and wellsaid ..etc, not ${vendor}`);
|
|
91
93
|
if ('google' === vendor) {
|
|
@@ -138,6 +140,10 @@ async function synthAudio(client, logger, stats, { account_sid,
|
|
|
138
140
|
assert.ok(credentials.api_key, 'synthAudio requires api_key when whisper is used');
|
|
139
141
|
} else if (vendor.startsWith('custom')) {
|
|
140
142
|
assert.ok(credentials.custom_tts_url, `synthAudio requires custom_tts_url in credentials when ${vendor} is used`);
|
|
143
|
+
} else if ('verbio' === vendor) {
|
|
144
|
+
assert.ok(voice, 'synthAudio requires voice when verbio is used');
|
|
145
|
+
assert.ok(credentials.client_id, 'synthAudio requires client_id when verbio is used');
|
|
146
|
+
assert.ok(credentials.client_secret, 'synthAudio requires client_secret when verbio is used');
|
|
141
147
|
}
|
|
142
148
|
const key = makeSynthKey({
|
|
143
149
|
account_sid,
|
|
@@ -148,7 +154,7 @@ async function synthAudio(client, logger, stats, { account_sid,
|
|
|
148
154
|
text
|
|
149
155
|
});
|
|
150
156
|
let filePath;
|
|
151
|
-
if (['nuance', 'nvidia'].includes(vendor) ||
|
|
157
|
+
if (['nuance', 'nvidia', 'verbio'].includes(vendor) ||
|
|
152
158
|
(
|
|
153
159
|
(process.env.JAMBONES_TTS_TRIM_SILENCE || !process.env.JAMBONES_DISABLE_TTS_STREAMING) &&
|
|
154
160
|
['microsoft', 'azure'].includes(vendor)
|
|
@@ -187,7 +193,8 @@ async function synthAudio(client, logger, stats, { account_sid,
|
|
|
187
193
|
case 'aws':
|
|
188
194
|
case 'polly':
|
|
189
195
|
vendorLabel = 'aws';
|
|
190
|
-
audioBuffer = await synthPolly(
|
|
196
|
+
audioBuffer = await synthPolly(createHash, retrieveHash, logger,
|
|
197
|
+
{credentials, stats, language, voice, text, engine});
|
|
191
198
|
break;
|
|
192
199
|
case 'azure':
|
|
193
200
|
case 'microsoft':
|
|
@@ -232,6 +239,11 @@ async function synthAudio(client, logger, stats, { account_sid,
|
|
|
232
239
|
credentials, stats, voice, text, renderForCaching, disableTtsStreaming});
|
|
233
240
|
if (audioBuffer?.filePath) return audioBuffer;
|
|
234
241
|
break;
|
|
242
|
+
case 'verbio':
|
|
243
|
+
audioBuffer = await synthVerbio(client, logger, {
|
|
244
|
+
credentials, stats, voice, text, renderForCaching, disableTtsStreaming});
|
|
245
|
+
if (audioBuffer?.filePath) return audioBuffer;
|
|
246
|
+
break;
|
|
235
247
|
case 'deepgram':
|
|
236
248
|
audioBuffer = await synthDeepgram(logger, {credentials, stats, model, text,
|
|
237
249
|
renderForCaching, disableTtsStreaming});
|
|
@@ -263,16 +275,28 @@ async function synthAudio(client, logger, stats, { account_sid,
|
|
|
263
275
|
});
|
|
264
276
|
}
|
|
265
277
|
|
|
266
|
-
const synthPolly = async(
|
|
278
|
+
const synthPolly = async(createHash, retrieveHash, logger,
|
|
279
|
+
{credentials, stats, language, voice, engine, text}) => {
|
|
267
280
|
try {
|
|
268
|
-
const {region, accessKeyId, secretAccessKey} = credentials;
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
281
|
+
const {region, accessKeyId, secretAccessKey, roleArn} = credentials;
|
|
282
|
+
let polly;
|
|
283
|
+
if (accessKeyId && secretAccessKey) {
|
|
284
|
+
polly = new PollyClient({
|
|
285
|
+
region,
|
|
286
|
+
credentials: {
|
|
287
|
+
accessKeyId,
|
|
288
|
+
secretAccessKey
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
} else if (roleArn) {
|
|
292
|
+
polly = new PollyClient({
|
|
293
|
+
region,
|
|
294
|
+
credentials: await getAwsAuthToken(logger, createHash, retrieveHash, null, null, region, roleArn),
|
|
295
|
+
});
|
|
296
|
+
} else {
|
|
297
|
+
// AWS RoleArn assigned to Instance profile
|
|
298
|
+
polly = new PollyClient({region});
|
|
299
|
+
}
|
|
276
300
|
const opts = {
|
|
277
301
|
Engine: engine,
|
|
278
302
|
OutputFormat: 'mp3',
|
|
@@ -366,7 +390,7 @@ async function _synthOnPremMicrosoft(logger, {
|
|
|
366
390
|
text,
|
|
367
391
|
filePath
|
|
368
392
|
}) {
|
|
369
|
-
const {use_custom_tts, custom_tts_endpoint_url} = credentials;
|
|
393
|
+
const {use_custom_tts, custom_tts_endpoint_url, api_key} = credentials;
|
|
370
394
|
let content = text;
|
|
371
395
|
|
|
372
396
|
if (use_custom_tts && !content.startsWith('<speak')) {
|
|
@@ -390,7 +414,8 @@ async function _synthOnPremMicrosoft(logger, {
|
|
|
390
414
|
const post = bent('POST', 'buffer', {
|
|
391
415
|
'X-Microsoft-OutputFormat': trimSilence ? 'raw-8khz-16bit-mono-pcm' : 'audio-16khz-32kbitrate-mono-mp3',
|
|
392
416
|
'Content-Type': 'application/ssml+xml',
|
|
393
|
-
'User-Agent': 'Jambonz'
|
|
417
|
+
'User-Agent': 'Jambonz',
|
|
418
|
+
...(api_key && {'Ocp-Apim-Subscription-Key': api_key})
|
|
394
419
|
});
|
|
395
420
|
const mp3 = await post(custom_tts_endpoint_url, content);
|
|
396
421
|
return mp3;
|
|
@@ -438,6 +463,7 @@ const synthMicrosoft = async(logger, {
|
|
|
438
463
|
params += ',write_cache_file=1';
|
|
439
464
|
if (region) params += `,region=${region}`;
|
|
440
465
|
if (custom_tts_endpoint) params += `,endpointId=${custom_tts_endpoint}`;
|
|
466
|
+
if (custom_tts_endpoint_url) params += `,endpoint=${custom_tts_endpoint_url}`;
|
|
441
467
|
if (process.env.JAMBONES_HTTP_PROXY_IP) params += `,http_proxy_ip=${process.env.JAMBONES_HTTP_PROXY_IP}`;
|
|
442
468
|
if (process.env.JAMBONES_HTTP_PROXY_PORT) params += `,http_proxy_port=${process.env.JAMBONES_HTTP_PROXY_PORT}`;
|
|
443
469
|
params += '}';
|
|
@@ -803,6 +829,46 @@ const synthRimelabs = async(logger, {
|
|
|
803
829
|
throw err;
|
|
804
830
|
}
|
|
805
831
|
};
|
|
832
|
+
const synthVerbio = async(client, logger, {credentials, stats, voice, text, renderForCaching, disableTtsStreaming}) => {
|
|
833
|
+
//https://doc.speechcenter.verbio.com/#tag/Text-To-Speech-REST-API
|
|
834
|
+
if (text.length > 2000) {
|
|
835
|
+
throw new Error('Verbio cannot synthesize for the text length larger than 2000 characters');
|
|
836
|
+
}
|
|
837
|
+
const token = await getVerbioAccessToken(client, logger, credentials);
|
|
838
|
+
if (!process.env.JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
|
|
839
|
+
let params = '';
|
|
840
|
+
params += `{access_token=${token.access_token}`;
|
|
841
|
+
params += ',vendor=verbio';
|
|
842
|
+
params += `,voice=${voice}`;
|
|
843
|
+
params += ',write_cache_file=1';
|
|
844
|
+
params += '}';
|
|
845
|
+
|
|
846
|
+
return {
|
|
847
|
+
filePath: `say:${params}${text.replace(/\n/g, ' ')}`,
|
|
848
|
+
servedFromCache: false,
|
|
849
|
+
rtt: 0
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
try {
|
|
854
|
+
const post = bent('https://us.rest.speechcenter.verbio.com', 'POST', 'buffer', {
|
|
855
|
+
'Authorization': `Bearer ${token.access_token}`,
|
|
856
|
+
'User-Agent': 'jambonz',
|
|
857
|
+
'Content-Type': 'application/json'
|
|
858
|
+
});
|
|
859
|
+
const r8 = await post('/api/v1/synthesize', {
|
|
860
|
+
voice_id: voice,
|
|
861
|
+
output_sample_rate: '8k',
|
|
862
|
+
output_encoding: 'pcm16',
|
|
863
|
+
text
|
|
864
|
+
});
|
|
865
|
+
return r8;
|
|
866
|
+
} catch (err) {
|
|
867
|
+
logger.info({err}, 'synth Verbio returned error');
|
|
868
|
+
stats.increment('tts.count', ['vendor:verbio', 'accepted:no']);
|
|
869
|
+
throw err;
|
|
870
|
+
}
|
|
871
|
+
};
|
|
806
872
|
|
|
807
873
|
const synthWhisper = async(logger, {credentials, stats, voice, text, renderForCaching, disableTtsStreaming}) => {
|
|
808
874
|
const {api_key, model_id, baseURL, timeout, speed} = credentials;
|
package/lib/utils.js
CHANGED
|
@@ -3,10 +3,10 @@ const {SynthesizerClient} = require('../stubs/nuance/synthesizer_grpc_pb');
|
|
|
3
3
|
const {RivaSpeechSynthesisClient} = require('../stubs/riva/proto/riva_tts_grpc_pb');
|
|
4
4
|
const {Pool} = require('undici');
|
|
5
5
|
const pool = new Pool('https://auth.crt.nuance.com');
|
|
6
|
-
const HTTP_TIMEOUT = 5000;
|
|
7
6
|
const NUANCE_AUTH_ENDPOINT = 'tts.api.nuance.com:443';
|
|
8
7
|
const grpc = require('@grpc/grpc-js');
|
|
9
8
|
const formurlencoded = require('form-urlencoded');
|
|
9
|
+
const { HTTP_TIMEOUT } = require('./constants');
|
|
10
10
|
|
|
11
11
|
const debug = require('debug')('jambonz:realtimedb-helpers');
|
|
12
12
|
/**
|
|
@@ -49,6 +49,12 @@ function makeAwsKey(awsAccessKeyId) {
|
|
|
49
49
|
return `aws:${hash.digest('hex')}`;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
function makeVerbioKey(client_id) {
|
|
53
|
+
const hash = crypto.createHash('sha1');
|
|
54
|
+
hash.update(client_id);
|
|
55
|
+
return `verbio:${hash.digest('hex')}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
52
58
|
function makeNuanceKey(clientId, secret, scope) {
|
|
53
59
|
const hash = crypto.createHash('sha1');
|
|
54
60
|
hash.update(`${clientId}:${secret}:${scope}`);
|
|
@@ -117,6 +123,7 @@ module.exports = {
|
|
|
117
123
|
makeNuanceKey,
|
|
118
124
|
makeIbmKey,
|
|
119
125
|
makeAwsKey,
|
|
126
|
+
makeVerbioKey,
|
|
120
127
|
getNuanceAccessToken,
|
|
121
128
|
createNuanceClient,
|
|
122
129
|
createKryptonClient,
|
package/package.json
CHANGED
package/test/list-voices.js
CHANGED
|
@@ -12,6 +12,32 @@ const stats = {
|
|
|
12
12
|
histogram: () => {}
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
+
test('Verbio - get Access key and voices', async(t) => {
|
|
16
|
+
const fn = require('..');
|
|
17
|
+
const {client, getTtsVoices, getVerbioAccessToken} = fn(opts, logger);
|
|
18
|
+
if (!process.env.VERBIO_CLIENT_ID || !process.env.VERBIO_CLIENT_SECRET) {
|
|
19
|
+
t.pass('skipping Verbio test since no Verbio Keys provided');
|
|
20
|
+
t.end();
|
|
21
|
+
client.quit();
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const credentials = {
|
|
27
|
+
client_id: process.env.VERBIO_CLIENT_ID,
|
|
28
|
+
client_secret: process.env.VERBIO_CLIENT_SECRET
|
|
29
|
+
};
|
|
30
|
+
let obj = await getVerbioAccessToken(credentials);
|
|
31
|
+
t.ok(obj.access_token , 'successfully received access token not from cache');
|
|
32
|
+
const voices = await getTtsVoices({vendor: 'verbio', credentials});
|
|
33
|
+
t.ok(voices && voices.length != 0, 'successfully received verbio voices');
|
|
34
|
+
} catch (err) {
|
|
35
|
+
console.error(err);
|
|
36
|
+
t.end(err);
|
|
37
|
+
}
|
|
38
|
+
client.quit();
|
|
39
|
+
});
|
|
40
|
+
|
|
15
41
|
test('IBM - create access key', async(t) => {
|
|
16
42
|
const fn = require('..');
|
|
17
43
|
const {client, getIbmAccessToken} = fn(opts, logger);
|
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);
|
|
@@ -643,6 +670,40 @@ test('whisper speech synth tests', async(t) => {
|
|
|
643
670
|
language: 'en-US',
|
|
644
671
|
voice: 'alloy',
|
|
645
672
|
text,
|
|
673
|
+
renderForCaching: true
|
|
674
|
+
});
|
|
675
|
+
t.ok(!opts.servedFromCache, `successfully synthesized whisper audio to ${opts.filePath}`);
|
|
676
|
+
|
|
677
|
+
} catch (err) {
|
|
678
|
+
console.error(JSON.stringify(err));
|
|
679
|
+
t.end(err);
|
|
680
|
+
}
|
|
681
|
+
client.quit();
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
test('Verbio speech synth tests', async(t) => {
|
|
685
|
+
const fn = require('..');
|
|
686
|
+
const {synthAudio, client} = fn(opts, logger);
|
|
687
|
+
|
|
688
|
+
if (!process.env.VERBIO_CLIENT_ID || !process.env.VERBIO_CLIENT_SECRET) {
|
|
689
|
+
t.pass('skipping Verbio Synthesize test since no Verbio Keys provided');
|
|
690
|
+
t.end();
|
|
691
|
+
client.quit();
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
const text = 'Hi there and welcome to jambones!';
|
|
696
|
+
try {
|
|
697
|
+
let opts = await synthAudio(stats, {
|
|
698
|
+
vendor: 'verbio',
|
|
699
|
+
credentials: {
|
|
700
|
+
client_id: process.env.VERBIO_CLIENT_ID,
|
|
701
|
+
client_secret: process.env.VERBIO_CLIENT_SECRET
|
|
702
|
+
},
|
|
703
|
+
language: 'en-US',
|
|
704
|
+
voice: 'tommy_en-us',
|
|
705
|
+
text,
|
|
706
|
+
renderForCaching: true
|
|
646
707
|
});
|
|
647
708
|
t.ok(!opts.servedFromCache, `successfully synthesized whisper audio to ${opts.filePath}`);
|
|
648
709
|
|