@jambonz/speech-utils 0.1.0 → 0.1.2
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 +1 -0
- package/lib/add-file-to-cache.js +2 -1
- package/lib/config.js +20 -0
- package/lib/get-ibm-access-token.js +1 -1
- package/lib/get-nuance-access-token.js +1 -1
- package/lib/get-tts-voices.js +27 -1
- package/lib/get-verbio-token.js +51 -0
- package/lib/synth-audio.js +81 -40
- package/lib/utils.js +48 -3
- package/package.json +2 -1
- package/stubs/nuance/synthesizer_grpc_pb.js +3 -3
- package/stubs/riva/proto/riva_audio_grpc_pb.js +1 -1
- package/stubs/riva/proto/riva_tts_grpc_pb.js +5 -5
- package/test/list-voices.js +26 -0
- package/test/synth.js +47 -2
package/index.js
CHANGED
|
@@ -14,6 +14,7 @@ module.exports = (opts, 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
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),
|
package/lib/add-file-to-cache.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const fs = require('fs/promises');
|
|
2
2
|
const {noopLogger, makeSynthKey} = require('./utils');
|
|
3
|
-
const
|
|
3
|
+
const {JAMBONES_TTS_CACHE_DURATION_MINS} = require('./config');
|
|
4
|
+
const EXPIRES = JAMBONES_TTS_CACHE_DURATION_MINS;
|
|
4
5
|
|
|
5
6
|
async function addFileToCache(client, logger, path,
|
|
6
7
|
{account_sid, vendor, language, voice, deploymentId, engine, text}) {
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const JAMBONES_TTS_TRIM_SILENCE = process.env.JAMBONES_TTS_TRIM_SILENCE;
|
|
2
|
+
const JAMBONES_DISABLE_TTS_STREAMING = process.env.JAMBONES_DISABLE_TTS_STREAMING;
|
|
3
|
+
|
|
4
|
+
const JAMBONES_HTTP_PROXY_IP = process.env.JAMBONES_HTTP_PROXY_IP;
|
|
5
|
+
const JAMBONES_HTTP_PROXY_PORT = process.env.JAMBONES_HTTP_PROXY_PORT;
|
|
6
|
+
const JAMBONES_TTS_CACHE_DURATION_MINS = (parseInt(process.env.JAMBONES_TTS_CACHE_DURATION_MINS) || 4 * 60) * 60; // cache tts for 4 hours
|
|
7
|
+
|
|
8
|
+
const TMP_FOLDER = '/tmp';
|
|
9
|
+
|
|
10
|
+
const HTTP_TIMEOUT = 5000;
|
|
11
|
+
|
|
12
|
+
module.exports = {
|
|
13
|
+
JAMBONES_TTS_TRIM_SILENCE,
|
|
14
|
+
JAMBONES_DISABLE_TTS_STREAMING,
|
|
15
|
+
JAMBONES_HTTP_PROXY_IP,
|
|
16
|
+
JAMBONES_HTTP_PROXY_PORT,
|
|
17
|
+
JAMBONES_TTS_CACHE_DURATION_MINS,
|
|
18
|
+
TMP_FOLDER,
|
|
19
|
+
HTTP_TIMEOUT
|
|
20
|
+
};
|
|
@@ -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('./config');
|
|
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('./config');
|
|
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,12 +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');
|
|
9
10
|
const getAwsAuthToken = require('./get-aws-sts-token');
|
|
11
|
+
const {Pool} = require('undici');
|
|
12
|
+
const { HTTP_TIMEOUT } = require('./config');
|
|
13
|
+
const verbioVoicePool = new Pool('https://us.rest.speechcenter.verbio.com');
|
|
10
14
|
|
|
11
15
|
const getIbmVoices = async(client, logger, credentials) => {
|
|
12
16
|
const {tts_region, tts_api_key} = credentials;
|
|
@@ -117,6 +121,26 @@ const getAwsVoices = async(_client, createHash, retrieveHash, logger, credential
|
|
|
117
121
|
}
|
|
118
122
|
};
|
|
119
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
|
+
|
|
120
144
|
/**
|
|
121
145
|
* Synthesize speech to an mp3 file, and also cache the generated speech
|
|
122
146
|
* in redis (base64 format) for 24 hours so as to avoid unnecessarily paying
|
|
@@ -136,7 +160,7 @@ const getAwsVoices = async(_client, createHash, retrieveHash, logger, credential
|
|
|
136
160
|
async function getTtsVoices(client, createHash, retrieveHash, logger, {vendor, credentials}) {
|
|
137
161
|
logger = logger || noopLogger;
|
|
138
162
|
|
|
139
|
-
assert.ok(['nuance', 'ibm', 'google', 'aws', 'polly'].includes(vendor),
|
|
163
|
+
assert.ok(['nuance', 'ibm', 'google', 'aws', 'polly', 'verbio'].includes(vendor),
|
|
140
164
|
`getTtsVoices not supported for vendor ${vendor}`);
|
|
141
165
|
|
|
142
166
|
switch (vendor) {
|
|
@@ -149,6 +173,8 @@ async function getTtsVoices(client, createHash, retrieveHash, logger, {vendor, c
|
|
|
149
173
|
case 'aws':
|
|
150
174
|
case 'polly':
|
|
151
175
|
return getAwsVoices(client, createHash, retrieveHash, logger, credentials);
|
|
176
|
+
case 'verbio':
|
|
177
|
+
return getVerbioVoices(client, logger, credentials);
|
|
152
178
|
default:
|
|
153
179
|
break;
|
|
154
180
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const {Pool} = require('undici');
|
|
2
|
+
const { noopLogger, makeVerbioKey } = require('./utils');
|
|
3
|
+
const { HTTP_TIMEOUT } = require('./config');
|
|
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
|
@@ -19,9 +19,11 @@ const {
|
|
|
19
19
|
createNuanceClient,
|
|
20
20
|
createKryptonClient,
|
|
21
21
|
createRivaClient,
|
|
22
|
-
noopLogger
|
|
22
|
+
noopLogger,
|
|
23
|
+
makeFilePath
|
|
23
24
|
} = require('./utils');
|
|
24
25
|
const getNuanceAccessToken = require('./get-nuance-access-token');
|
|
26
|
+
const getVerbioAccessToken = require('./get-verbio-token');
|
|
25
27
|
const {
|
|
26
28
|
SynthesisRequest,
|
|
27
29
|
Voice,
|
|
@@ -36,8 +38,13 @@ const {
|
|
|
36
38
|
const {SynthesizeSpeechRequest} = require('../stubs/riva/proto/riva_tts_pb');
|
|
37
39
|
const {AudioEncoding} = require('../stubs/riva/proto/riva_audio_pb');
|
|
38
40
|
const debug = require('debug')('jambonz:realtimedb-helpers');
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
+
const {
|
|
42
|
+
JAMBONES_DISABLE_TTS_STREAMING,
|
|
43
|
+
JAMBONES_HTTP_PROXY_IP,
|
|
44
|
+
JAMBONES_HTTP_PROXY_PORT,
|
|
45
|
+
JAMBONES_TTS_CACHE_DURATION_MINS,
|
|
46
|
+
} = require('./config');
|
|
47
|
+
const EXPIRES = JAMBONES_TTS_CACHE_DURATION_MINS;
|
|
41
48
|
const OpenAI = require('openai');
|
|
42
49
|
const getAwsAuthToken = require('./get-aws-sts-token');
|
|
43
50
|
|
|
@@ -86,7 +93,7 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
|
|
|
86
93
|
logger = logger || noopLogger;
|
|
87
94
|
|
|
88
95
|
assert.ok(['google', 'aws', 'polly', 'microsoft', 'wellsaid', 'nuance', 'nvidia', 'ibm', 'elevenlabs',
|
|
89
|
-
'whisper', 'deepgram', 'playht', 'rimelabs'].includes(vendor) ||
|
|
96
|
+
'whisper', 'deepgram', 'playht', 'rimelabs', 'verbio'].includes(vendor) ||
|
|
90
97
|
vendor.startsWith('custom'),
|
|
91
98
|
`synthAudio supported vendors are google, aws, microsoft, nuance, nvidia and wellsaid ..etc, not ${vendor}`);
|
|
92
99
|
if ('google' === vendor) {
|
|
@@ -139,6 +146,10 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
|
|
|
139
146
|
assert.ok(credentials.api_key, 'synthAudio requires api_key when whisper is used');
|
|
140
147
|
} else if (vendor.startsWith('custom')) {
|
|
141
148
|
assert.ok(credentials.custom_tts_url, `synthAudio requires custom_tts_url in credentials when ${vendor} is used`);
|
|
149
|
+
} else if ('verbio' === vendor) {
|
|
150
|
+
assert.ok(voice, 'synthAudio requires voice when verbio is used');
|
|
151
|
+
assert.ok(credentials.client_id, 'synthAudio requires client_id when verbio is used');
|
|
152
|
+
assert.ok(credentials.client_secret, 'synthAudio requires client_secret when verbio is used');
|
|
142
153
|
}
|
|
143
154
|
const key = makeSynthKey({
|
|
144
155
|
account_sid,
|
|
@@ -149,19 +160,7 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
|
|
|
149
160
|
text
|
|
150
161
|
});
|
|
151
162
|
let filePath;
|
|
152
|
-
|
|
153
|
-
(
|
|
154
|
-
(process.env.JAMBONES_TTS_TRIM_SILENCE || !process.env.JAMBONES_DISABLE_TTS_STREAMING) &&
|
|
155
|
-
['microsoft', 'azure'].includes(vendor)
|
|
156
|
-
) ||
|
|
157
|
-
(
|
|
158
|
-
!process.env.JAMBONES_DISABLE_TTS_STREAMING &&
|
|
159
|
-
['elevenlabs', 'deepgram', 'rimelabs'].includes(vendor)
|
|
160
|
-
)
|
|
161
|
-
) {
|
|
162
|
-
filePath = `${TMP_FOLDER}/${key.replace('tts:', `tts-${salt || ''}`)}.r8`;
|
|
163
|
-
}
|
|
164
|
-
else filePath = `${TMP_FOLDER}/${key.replace('tts:', `tts-${salt || ''}`)}.mp3`;
|
|
163
|
+
filePath = makeFilePath(vendor, key, salt);
|
|
165
164
|
debug(`synth key is ${key}`);
|
|
166
165
|
let cached;
|
|
167
166
|
if (!disableTtsCache) {
|
|
@@ -195,8 +194,7 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
|
|
|
195
194
|
case 'microsoft':
|
|
196
195
|
vendorLabel = 'microsoft';
|
|
197
196
|
audioBuffer = await synthMicrosoft(logger, {credentials, stats, language, voice, text, deploymentId,
|
|
198
|
-
filePath, renderForCaching, disableTtsStreaming});
|
|
199
|
-
if (audioBuffer?.filePath) return audioBuffer;
|
|
197
|
+
filePath, renderForCaching, disableTtsStreaming});
|
|
200
198
|
break;
|
|
201
199
|
case 'nuance':
|
|
202
200
|
model = model || 'enhanced';
|
|
@@ -214,30 +212,30 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
|
|
|
214
212
|
case 'elevenlabs':
|
|
215
213
|
audioBuffer = await synthElevenlabs(logger, {
|
|
216
214
|
credentials, options, stats, language, voice, text, renderForCaching, disableTtsStreaming, filePath
|
|
217
|
-
});
|
|
218
|
-
if (audioBuffer?.filePath) return audioBuffer;
|
|
215
|
+
});
|
|
219
216
|
break;
|
|
220
217
|
case 'playht':
|
|
221
218
|
audioBuffer = await synthPlayHT(logger, {
|
|
222
219
|
credentials, options, stats, language, voice, text, renderForCaching, disableTtsStreaming, filePath
|
|
223
|
-
});
|
|
224
|
-
if (audioBuffer?.filePath) return audioBuffer;
|
|
220
|
+
});
|
|
225
221
|
break;
|
|
226
222
|
case 'rimelabs':
|
|
227
223
|
audioBuffer = await synthRimelabs(logger, {
|
|
228
224
|
credentials, options, stats, language, voice, text, renderForCaching, disableTtsStreaming, filePath
|
|
229
|
-
});
|
|
230
|
-
if (audioBuffer?.filePath) return audioBuffer;
|
|
225
|
+
});
|
|
231
226
|
break;
|
|
232
227
|
case 'whisper':
|
|
233
228
|
audioBuffer = await synthWhisper(logger, {
|
|
229
|
+
credentials, stats, voice, text, renderForCaching, disableTtsStreaming});
|
|
230
|
+
break;
|
|
231
|
+
case 'verbio':
|
|
232
|
+
audioBuffer = await synthVerbio(client, logger, {
|
|
234
233
|
credentials, stats, voice, text, renderForCaching, disableTtsStreaming});
|
|
235
234
|
if (audioBuffer?.filePath) return audioBuffer;
|
|
236
235
|
break;
|
|
237
236
|
case 'deepgram':
|
|
238
237
|
audioBuffer = await synthDeepgram(logger, {credentials, stats, model, text,
|
|
239
|
-
renderForCaching, disableTtsStreaming});
|
|
240
|
-
if (audioBuffer?.filePath) return audioBuffer;
|
|
238
|
+
renderForCaching, disableTtsStreaming});
|
|
241
239
|
break;
|
|
242
240
|
case vendor.startsWith('custom') ? vendor : 'cant_match_value':
|
|
243
241
|
({ audioBuffer, filePath } = await synthCustomVendor(logger,
|
|
@@ -246,6 +244,7 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
|
|
|
246
244
|
default:
|
|
247
245
|
assert(`synthAudio: unsupported speech vendor ${vendor}`);
|
|
248
246
|
}
|
|
247
|
+
if('filePath' in audioBuffer) return audioBuffer;
|
|
249
248
|
const diff = process.hrtime(startAt);
|
|
250
249
|
const time = diff[0] * 1e3 + diff[1] * 1e-6;
|
|
251
250
|
rtt = time.toFixed(0);
|
|
@@ -380,7 +379,7 @@ async function _synthOnPremMicrosoft(logger, {
|
|
|
380
379
|
text,
|
|
381
380
|
filePath
|
|
382
381
|
}) {
|
|
383
|
-
const {use_custom_tts, custom_tts_endpoint_url} = credentials;
|
|
382
|
+
const {use_custom_tts, custom_tts_endpoint_url, api_key} = credentials;
|
|
384
383
|
let content = text;
|
|
385
384
|
|
|
386
385
|
if (use_custom_tts && !content.startsWith('<speak')) {
|
|
@@ -404,7 +403,8 @@ async function _synthOnPremMicrosoft(logger, {
|
|
|
404
403
|
const post = bent('POST', 'buffer', {
|
|
405
404
|
'X-Microsoft-OutputFormat': trimSilence ? 'raw-8khz-16bit-mono-pcm' : 'audio-16khz-32kbitrate-mono-mp3',
|
|
406
405
|
'Content-Type': 'application/ssml+xml',
|
|
407
|
-
'User-Agent': 'Jambonz'
|
|
406
|
+
'User-Agent': 'Jambonz',
|
|
407
|
+
...(api_key && {'Ocp-Apim-Subscription-Key': api_key})
|
|
408
408
|
});
|
|
409
409
|
const mp3 = await post(custom_tts_endpoint_url, content);
|
|
410
410
|
return mp3;
|
|
@@ -443,7 +443,7 @@ const synthMicrosoft = async(logger, {
|
|
|
443
443
|
content = `<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xmlns:mstts="https://www.w3.org/2001/mstts" xml:lang="${language}"><voice name="${voice}">${words}</voice></speak>`;
|
|
444
444
|
logger.info({content}, 'synthMicrosoft');
|
|
445
445
|
}
|
|
446
|
-
if (!
|
|
446
|
+
if (!JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
|
|
447
447
|
let params = '';
|
|
448
448
|
params += `{api_key=${apiKey}`;
|
|
449
449
|
params += `,language=${language}`;
|
|
@@ -452,8 +452,9 @@ const synthMicrosoft = async(logger, {
|
|
|
452
452
|
params += ',write_cache_file=1';
|
|
453
453
|
if (region) params += `,region=${region}`;
|
|
454
454
|
if (custom_tts_endpoint) params += `,endpointId=${custom_tts_endpoint}`;
|
|
455
|
-
if (
|
|
456
|
-
if (
|
|
455
|
+
if (custom_tts_endpoint_url) params += `,endpoint=${custom_tts_endpoint_url}`;
|
|
456
|
+
if (JAMBONES_HTTP_PROXY_IP) params += `,http_proxy_ip=${JAMBONES_HTTP_PROXY_IP}`;
|
|
457
|
+
if (JAMBONES_HTTP_PROXY_PORT) params += `,http_proxy_port=${JAMBONES_HTTP_PROXY_PORT}`;
|
|
457
458
|
params += '}';
|
|
458
459
|
return {
|
|
459
460
|
filePath: `say:${params}${content.replace(/\n/g, ' ')}`,
|
|
@@ -482,10 +483,10 @@ const synthMicrosoft = async(logger, {
|
|
|
482
483
|
SpeechSynthesisOutputFormat.Raw8Khz16BitMonoPcm :
|
|
483
484
|
SpeechSynthesisOutputFormat.Audio16Khz32KBitRateMonoMp3;
|
|
484
485
|
|
|
485
|
-
if (
|
|
486
|
+
if (JAMBONES_HTTP_PROXY_IP && JAMBONES_HTTP_PROXY_PORT) {
|
|
486
487
|
logger.debug(
|
|
487
|
-
`synthMicrosoft: using proxy ${
|
|
488
|
-
speechConfig.setProxy(
|
|
488
|
+
`synthMicrosoft: using proxy ${JAMBONES_HTTP_PROXY_IP}:${JAMBONES_HTTP_PROXY_PORT}`);
|
|
489
|
+
speechConfig.setProxy(JAMBONES_HTTP_PROXY_IP, JAMBONES_HTTP_PROXY_PORT);
|
|
489
490
|
}
|
|
490
491
|
const synthesizer = new SpeechSynthesizer(speechConfig);
|
|
491
492
|
|
|
@@ -671,7 +672,7 @@ const synthElevenlabs = async(logger, {
|
|
|
671
672
|
const opts = !!options && Object.keys(options).length !== 0 ? options : JSON.parse(credOpts || '{}');
|
|
672
673
|
|
|
673
674
|
/* default to using the streaming interface, unless disabled by env var OR we want just a cache file */
|
|
674
|
-
if (!
|
|
675
|
+
if (!JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
|
|
675
676
|
let params = '';
|
|
676
677
|
params += `{api_key=${api_key}`;
|
|
677
678
|
params += ',vendor=elevenlabs';
|
|
@@ -724,7 +725,7 @@ const synthPlayHT = async(logger, {
|
|
|
724
725
|
const opts = !!options && Object.keys(options).length !== 0 ? options : JSON.parse(credOpts || '{}');
|
|
725
726
|
|
|
726
727
|
/* default to using the streaming interface, unless disabled by env var OR we want just a cache file */
|
|
727
|
-
if (!
|
|
728
|
+
if (!JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
|
|
728
729
|
let params = '';
|
|
729
730
|
params += `{api_key=${api_key}`;
|
|
730
731
|
params += `,user_id=${user_id}`;
|
|
@@ -779,7 +780,7 @@ const synthRimelabs = async(logger, {
|
|
|
779
780
|
const opts = !!options && Object.keys(options).length !== 0 ? options : JSON.parse(credOpts || '{}');
|
|
780
781
|
|
|
781
782
|
/* default to using the streaming interface, unless disabled by env var OR we want just a cache file */
|
|
782
|
-
if (!
|
|
783
|
+
if (!JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
|
|
783
784
|
let params = '';
|
|
784
785
|
params += `{api_key=${api_key}`;
|
|
785
786
|
params += `,model_id=${model_id}`;
|
|
@@ -817,11 +818,51 @@ const synthRimelabs = async(logger, {
|
|
|
817
818
|
throw err;
|
|
818
819
|
}
|
|
819
820
|
};
|
|
821
|
+
const synthVerbio = async(client, logger, {credentials, stats, voice, text, renderForCaching, disableTtsStreaming}) => {
|
|
822
|
+
//https://doc.speechcenter.verbio.com/#tag/Text-To-Speech-REST-API
|
|
823
|
+
if (text.length > 2000) {
|
|
824
|
+
throw new Error('Verbio cannot synthesize for the text length larger than 2000 characters');
|
|
825
|
+
}
|
|
826
|
+
const token = await getVerbioAccessToken(client, logger, credentials);
|
|
827
|
+
if (!process.env.JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
|
|
828
|
+
let params = '';
|
|
829
|
+
params += `{access_token=${token.access_token}`;
|
|
830
|
+
params += ',vendor=verbio';
|
|
831
|
+
params += `,voice=${voice}`;
|
|
832
|
+
params += ',write_cache_file=1';
|
|
833
|
+
params += '}';
|
|
834
|
+
|
|
835
|
+
return {
|
|
836
|
+
filePath: `say:${params}${text.replace(/\n/g, ' ')}`,
|
|
837
|
+
servedFromCache: false,
|
|
838
|
+
rtt: 0
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
try {
|
|
843
|
+
const post = bent('https://us.rest.speechcenter.verbio.com', 'POST', 'buffer', {
|
|
844
|
+
'Authorization': `Bearer ${token.access_token}`,
|
|
845
|
+
'User-Agent': 'jambonz',
|
|
846
|
+
'Content-Type': 'application/json'
|
|
847
|
+
});
|
|
848
|
+
const r8 = await post('/api/v1/synthesize', {
|
|
849
|
+
voice_id: voice,
|
|
850
|
+
output_sample_rate: '8k',
|
|
851
|
+
output_encoding: 'pcm16',
|
|
852
|
+
text
|
|
853
|
+
});
|
|
854
|
+
return r8;
|
|
855
|
+
} catch (err) {
|
|
856
|
+
logger.info({err}, 'synth Verbio returned error');
|
|
857
|
+
stats.increment('tts.count', ['vendor:verbio', 'accepted:no']);
|
|
858
|
+
throw err;
|
|
859
|
+
}
|
|
860
|
+
};
|
|
820
861
|
|
|
821
862
|
const synthWhisper = async(logger, {credentials, stats, voice, text, renderForCaching, disableTtsStreaming}) => {
|
|
822
863
|
const {api_key, model_id, baseURL, timeout, speed} = credentials;
|
|
823
864
|
/* if the env is set to stream then bag out, unless we are specifically rendering to generate a cache file */
|
|
824
|
-
if (!
|
|
865
|
+
if (!JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
|
|
825
866
|
let params = '';
|
|
826
867
|
params += `{api_key=${api_key}`;
|
|
827
868
|
params += `,model_id=${model_id}`;
|
|
@@ -860,7 +901,7 @@ const synthWhisper = async(logger, {credentials, stats, voice, text, renderForCa
|
|
|
860
901
|
|
|
861
902
|
const synthDeepgram = async(logger, {credentials, stats, model, text, renderForCaching, disableTtsStreaming}) => {
|
|
862
903
|
const {api_key} = credentials;
|
|
863
|
-
if (!
|
|
904
|
+
if (!JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
|
|
864
905
|
let params = '';
|
|
865
906
|
params += `{api_key=${api_key}`;
|
|
866
907
|
params += ',vendor=deepgram';
|
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 { JAMBONES_DISABLE_TTS_STREAMING, JAMBONES_TTS_TRIM_SILENCE, TMP_FOLDER, HTTP_TIMEOUT } = require('./config');
|
|
10
10
|
|
|
11
11
|
const debug = require('debug')('jambonz:realtimedb-helpers');
|
|
12
12
|
/**
|
|
@@ -19,7 +19,44 @@ const debug = require('debug')('jambonz:realtimedb-helpers');
|
|
|
19
19
|
function makeSynthKey({account_sid = '', vendor, language, voice, engine = '', text}) {
|
|
20
20
|
const hash = crypto.createHash('sha1');
|
|
21
21
|
hash.update(`${language}:${vendor}:${voice}:${engine}:${text}`);
|
|
22
|
-
|
|
22
|
+
const hexHashKey = hash.digest('hex');
|
|
23
|
+
const accountKey = account_sid ? `:${account_sid}` : '';
|
|
24
|
+
const namespace = vendor.startsWith('custom') ? vendor : getFileExtension(vendor);
|
|
25
|
+
const key = `tts${accountKey}:${namespace}:${hexHashKey}`;
|
|
26
|
+
return key;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function makeFilePath(vendor, key, salt = '') {
|
|
30
|
+
const extension = getFileExtension(vendor);
|
|
31
|
+
return `${TMP_FOLDER}/${key.replace('tts:', `tts-${salt}`)}.${extension}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getFileExtension(vendor) {
|
|
35
|
+
const mp3Extension = 'mp3';
|
|
36
|
+
const r8Extension = 'r8';
|
|
37
|
+
|
|
38
|
+
switch (vendor) {
|
|
39
|
+
case 'azure':
|
|
40
|
+
case 'microsoft':
|
|
41
|
+
if (!JAMBONES_DISABLE_TTS_STREAMING || JAMBONES_TTS_TRIM_SILENCE) {
|
|
42
|
+
return r8Extension;
|
|
43
|
+
} else {
|
|
44
|
+
return mp3Extension;
|
|
45
|
+
}
|
|
46
|
+
case 'deepgram':
|
|
47
|
+
case 'elevenlabs':
|
|
48
|
+
case 'rimlabs':
|
|
49
|
+
if (!JAMBONES_DISABLE_TTS_STREAMING) {
|
|
50
|
+
return r8Extension;
|
|
51
|
+
} else {
|
|
52
|
+
return mp3Extension;
|
|
53
|
+
}
|
|
54
|
+
case 'nuance':
|
|
55
|
+
case 'nvidia':
|
|
56
|
+
return r8Extension;
|
|
57
|
+
default:
|
|
58
|
+
return mp3Extension;
|
|
59
|
+
}
|
|
23
60
|
}
|
|
24
61
|
|
|
25
62
|
const noopLogger = {
|
|
@@ -49,6 +86,12 @@ function makeAwsKey(awsAccessKeyId) {
|
|
|
49
86
|
return `aws:${hash.digest('hex')}`;
|
|
50
87
|
}
|
|
51
88
|
|
|
89
|
+
function makeVerbioKey(client_id) {
|
|
90
|
+
const hash = crypto.createHash('sha1');
|
|
91
|
+
hash.update(client_id);
|
|
92
|
+
return `verbio:${hash.digest('hex')}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
52
95
|
function makeNuanceKey(clientId, secret, scope) {
|
|
53
96
|
const hash = crypto.createHash('sha1');
|
|
54
97
|
hash.update(`${clientId}:${secret}:${scope}`);
|
|
@@ -117,11 +160,13 @@ module.exports = {
|
|
|
117
160
|
makeNuanceKey,
|
|
118
161
|
makeIbmKey,
|
|
119
162
|
makeAwsKey,
|
|
163
|
+
makeVerbioKey,
|
|
120
164
|
getNuanceAccessToken,
|
|
121
165
|
createNuanceClient,
|
|
122
166
|
createKryptonClient,
|
|
123
167
|
createRivaClient,
|
|
124
168
|
makeBasicAuthHeader,
|
|
125
169
|
NUANCE_AUTH_ENDPOINT,
|
|
126
|
-
noopLogger
|
|
170
|
+
noopLogger,
|
|
171
|
+
makeFilePath
|
|
127
172
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jambonz/speech-utils",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "TTS-related speech utilities for jambonz",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"author": "Dave Horton",
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"test": "NODE_ENV=test node test/ ",
|
|
13
13
|
"coverage": "nyc --reporter html --report-dir ./coverage npm run test",
|
|
14
14
|
"jslint": "eslint index.js lib",
|
|
15
|
+
"jslint:fix": "eslint --fix '**/*.js'",
|
|
15
16
|
"build": "./build_stubs.sh"
|
|
16
17
|
},
|
|
17
18
|
"repository": {
|
|
@@ -62,9 +62,9 @@ function deserialize_nuance_tts_v1_UnarySynthesisResponse(buffer_arg) {
|
|
|
62
62
|
|
|
63
63
|
//
|
|
64
64
|
// The Synthesizer service offers these functionalities:
|
|
65
|
-
// - GetVoices: Queries the list of available voices, with filters to reduce the search space.
|
|
66
|
-
// - Synthesize: Synthesizes audio from input text and parameters, and returns an audio stream.
|
|
67
|
-
// - UnarySynthesize: Synthesizes audio from input text and parameters, and returns a single audio response.
|
|
65
|
+
// - GetVoices: Queries the list of available voices, with filters to reduce the search space.
|
|
66
|
+
// - Synthesize: Synthesizes audio from input text and parameters, and returns an audio stream.
|
|
67
|
+
// - UnarySynthesize: Synthesizes audio from input text and parameters, and returns a single audio response.
|
|
68
68
|
var SynthesizerService = exports.SynthesizerService = {
|
|
69
69
|
getVoices: {
|
|
70
70
|
path: '/nuance.tts.v1.Synthesizer/GetVoices',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
// GENERATED CODE -- NO SERVICES IN PROTO
|
|
1
|
+
// GENERATED CODE -- NO SERVICES IN PROTO
|
|
@@ -57,7 +57,7 @@ function deserialize_nvidia_riva_tts_SynthesizeSpeechResponse(buffer_arg) {
|
|
|
57
57
|
var RivaSpeechSynthesisService = exports.RivaSpeechSynthesisService = {
|
|
58
58
|
// Used to request text-to-speech from the service. Submit a request containing the
|
|
59
59
|
// desired text and configuration, and receive audio bytes in the requested format.
|
|
60
|
-
synthesize: {
|
|
60
|
+
synthesize: {
|
|
61
61
|
path: '/nvidia.riva.tts.RivaSpeechSynthesis/Synthesize',
|
|
62
62
|
requestStream: false,
|
|
63
63
|
responseStream: false,
|
|
@@ -69,9 +69,9 @@ synthesize: {
|
|
|
69
69
|
responseDeserialize: deserialize_nvidia_riva_tts_SynthesizeSpeechResponse,
|
|
70
70
|
},
|
|
71
71
|
// Used to request text-to-speech returned via stream as it becomes available.
|
|
72
|
-
// Submit a SynthesizeSpeechRequest with desired text and configuration,
|
|
73
|
-
// and receive stream of bytes in the requested format.
|
|
74
|
-
synthesizeOnline: {
|
|
72
|
+
// Submit a SynthesizeSpeechRequest with desired text and configuration,
|
|
73
|
+
// and receive stream of bytes in the requested format.
|
|
74
|
+
synthesizeOnline: {
|
|
75
75
|
path: '/nvidia.riva.tts.RivaSpeechSynthesis/SynthesizeOnline',
|
|
76
76
|
requestStream: false,
|
|
77
77
|
responseStream: true,
|
|
@@ -83,7 +83,7 @@ synthesizeOnline: {
|
|
|
83
83
|
responseDeserialize: deserialize_nvidia_riva_tts_SynthesizeSpeechResponse,
|
|
84
84
|
},
|
|
85
85
|
// Enables clients to request the configuration of the current Synthesize service, or a specific model within the service.
|
|
86
|
-
getRivaSynthesisConfig: {
|
|
86
|
+
getRivaSynthesisConfig: {
|
|
87
87
|
path: '/nvidia.riva.tts.RivaSpeechSynthesis/GetRivaSynthesisConfig',
|
|
88
88
|
requestStream: false,
|
|
89
89
|
responseStream: false,
|
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
|
@@ -670,6 +670,40 @@ test('whisper speech synth tests', async(t) => {
|
|
|
670
670
|
language: 'en-US',
|
|
671
671
|
voice: 'alloy',
|
|
672
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
|
|
673
707
|
});
|
|
674
708
|
t.ok(!opts.servedFromCache, `successfully synthesized whisper audio to ${opts.filePath}`);
|
|
675
709
|
|
|
@@ -755,8 +789,19 @@ test('TTS Cache tests', async(t) => {
|
|
|
755
789
|
t.ok(error, `error returned when specified key was not found`);
|
|
756
790
|
|
|
757
791
|
// make sure other tts keys are still there
|
|
758
|
-
const cached =
|
|
759
|
-
t.ok(cached >= 1, `successfully kept all non-specified tts records in cache`);
|
|
792
|
+
const cached = await client.keys('tts:*')
|
|
793
|
+
t.ok(cached.length >= 1, `successfully kept all non-specified tts records in cache`);
|
|
794
|
+
|
|
795
|
+
// retrieve keys from cache and check the key contains the file extension
|
|
796
|
+
let key = cached[0];
|
|
797
|
+
t.ok(key.includes('mp3'), `tts cache extension shoult be part of the key and equal mp3`);
|
|
798
|
+
|
|
799
|
+
process.env.VG_TRIM_TTS_SILENCE = 'true';
|
|
800
|
+
await client.set(makeSynthKey({ vendor: 'azure' }), 'value');
|
|
801
|
+
|
|
802
|
+
const r8Keys = await client.keys('tts:r8*');
|
|
803
|
+
key = r8Keys[0];
|
|
804
|
+
t.ok(key.includes('r8'), `tts cache extension shoult be part of the key and equal r8`);
|
|
760
805
|
|
|
761
806
|
} catch (err) {
|
|
762
807
|
console.error(JSON.stringify(err));
|