@jambonz/speech-utils 0.0.24 → 0.0.26
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 +5 -1
- package/index.js +4 -25
- package/lib/synth-audio.js +38 -2
- package/package.json +3 -2
- package/test/synth.js +33 -4
package/.github/workflows/ci.yml
CHANGED
|
@@ -24,4 +24,8 @@ jobs:
|
|
|
24
24
|
IBM_TTS_API_KEY: ${{ secrets.IBM_TTS_API_KEY }}
|
|
25
25
|
IBM_TTS_REGION: ${{ secrets.IBM_TTS_REGION }}
|
|
26
26
|
MICROSOFT_API_KEY: ${{ secrets.MICROSOFT_API_KEY }}
|
|
27
|
-
MICROSOFT_REGION: ${{ secrets.MICROSOFT_REGION }}
|
|
27
|
+
MICROSOFT_REGION: ${{ secrets.MICROSOFT_REGION }}
|
|
28
|
+
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
|
29
|
+
ELEVENLABS_API_KEY: ${{ secrets.ELEVENLABS_API_KEY }}
|
|
30
|
+
ELEVENLABS_VOICE_ID: ${{ secrets.ELEVENLABS_VOICE_ID }}
|
|
31
|
+
ELEVENLABS_MODEL_ID: ${{ secrets.ELEVENLABS_MODEL_ID }}
|
package/index.js
CHANGED
|
@@ -1,34 +1,13 @@
|
|
|
1
1
|
const {noopLogger} = require('./lib/utils');
|
|
2
|
-
const Redis = require('ioredis');
|
|
3
2
|
|
|
4
3
|
module.exports = (opts, logger) => {
|
|
5
4
|
logger = logger || noopLogger;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
for (const o of opts) {
|
|
11
|
-
o.username = process.env.JAMBONES_REDIS_USERNAME;
|
|
12
|
-
o.password = process.env.JAMBONES_REDIS_PASSWORD;
|
|
13
|
-
}
|
|
14
|
-
} else {
|
|
15
|
-
connectionOpts.username = process.env.JAMBONES_REDIS_USERNAME;
|
|
16
|
-
connectionOpts.password = process.env.JAMBONES_REDIS_PASSWORD;
|
|
17
|
-
}
|
|
5
|
+
let client = opts.redis_client;
|
|
6
|
+
if (!client) {
|
|
7
|
+
const {client: redisClient} = require('@jambonz/realtimedb-helpers')(opts, logger);
|
|
8
|
+
client = redisClient;
|
|
18
9
|
}
|
|
19
10
|
|
|
20
|
-
const client = new Redis(connectionOpts);
|
|
21
|
-
['ready', 'connect', 'reconnecting', 'error', 'end', 'warning']
|
|
22
|
-
.forEach((event) => {
|
|
23
|
-
client.on(event, (...args) => {
|
|
24
|
-
if ('error' === event) {
|
|
25
|
-
if (process.env.NODE_ENV === 'test' && args[0]?.code === 'ECONNREFUSED') return;
|
|
26
|
-
logger.error({...args}, '@jambonz/realtimedb-helpers - redis error');
|
|
27
|
-
}
|
|
28
|
-
else logger.debug({args}, `redis event ${event}`);
|
|
29
|
-
});
|
|
30
|
-
});
|
|
31
|
-
|
|
32
11
|
return {
|
|
33
12
|
client,
|
|
34
13
|
getTtsSize: require('./lib/get-tts-size').bind(null, client, logger),
|
package/lib/synth-audio.js
CHANGED
|
@@ -38,6 +38,7 @@ const {AudioEncoding} = require('../stubs/riva/proto/riva_audio_pb');
|
|
|
38
38
|
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
|
+
const OpenAI = require('openai');
|
|
41
42
|
|
|
42
43
|
|
|
43
44
|
const trimTrailingSilence = (buffer) => {
|
|
@@ -83,7 +84,7 @@ async function synthAudio(client, logger, stats, { account_sid,
|
|
|
83
84
|
logger = logger || noopLogger;
|
|
84
85
|
|
|
85
86
|
assert.ok(['google', 'aws', 'polly', 'microsoft',
|
|
86
|
-
'wellsaid', 'nuance', 'nvidia', 'ibm', 'elevenlabs'].includes(vendor) ||
|
|
87
|
+
'wellsaid', 'nuance', 'nvidia', 'ibm', 'elevenlabs', 'whisper'].includes(vendor) ||
|
|
87
88
|
vendor.startsWith('custom'),
|
|
88
89
|
`synthAudio supported vendors are google, aws, microsoft, nuance, nvidia and wellsaid, not ${vendor}`);
|
|
89
90
|
if ('google' === vendor) {
|
|
@@ -117,6 +118,14 @@ async function synthAudio(client, logger, stats, { account_sid,
|
|
|
117
118
|
language = 'en-US'; // WellSaid only supports English atm
|
|
118
119
|
assert.ok(voice, 'synthAudio requires voice when wellsaid is used');
|
|
119
120
|
assert.ok(!text.startsWith('<speak'), 'wellsaid does not support SSML tags');
|
|
121
|
+
} else if ('elevenlabs' === vendor) {
|
|
122
|
+
assert.ok(voice, 'synthAudio requires voice when elevenlabs is used');
|
|
123
|
+
assert.ok(credentials.api_key, 'synthAudio requires api_key when elevenlabs is used');
|
|
124
|
+
assert.ok(credentials.model_id, 'synthAudio requires model_id when elevenlabs is used');
|
|
125
|
+
} else if ('whisper' === vendor) {
|
|
126
|
+
assert.ok(voice, 'synthAudio requires voice when whisper is used');
|
|
127
|
+
assert.ok(credentials.model_id, 'synthAudio requires model when whisper is used');
|
|
128
|
+
assert.ok(credentials.api_key, 'synthAudio requires api_key when whisper is used');
|
|
120
129
|
} else if (vendor.startsWith('custom')) {
|
|
121
130
|
assert.ok(credentials.custom_tts_url, `synthAudio requires custom_tts_url in credentials when ${vendor} is used`);
|
|
122
131
|
}
|
|
@@ -187,6 +196,9 @@ async function synthAudio(client, logger, stats, { account_sid,
|
|
|
187
196
|
case 'elevenlabs':
|
|
188
197
|
audioBuffer = await synthElevenlabs(logger, {credentials, stats, language, voice, text, filePath});
|
|
189
198
|
break;
|
|
199
|
+
case 'whisper':
|
|
200
|
+
audioBuffer = await synthWhisper(logger, {credentials, stats, voice, text});
|
|
201
|
+
break;
|
|
190
202
|
case vendor.startsWith('custom') ? vendor : 'cant_match_value':
|
|
191
203
|
({ audioBuffer, filePath } = await synthCustomVendor(logger,
|
|
192
204
|
{credentials, stats, language, voice, text, filePath}));
|
|
@@ -591,11 +603,35 @@ const synthElevenlabs = async(logger, {credentials, stats, language, voice, text
|
|
|
591
603
|
});
|
|
592
604
|
return mp3;
|
|
593
605
|
} catch (err) {
|
|
594
|
-
logger.info({err}, '
|
|
606
|
+
logger.info({err}, 'synth Elevenlabs returned error');
|
|
607
|
+
stats.increment('tts.count', ['vendor:elevenlabs', 'accepted:no']);
|
|
595
608
|
throw err;
|
|
596
609
|
}
|
|
597
610
|
};
|
|
598
611
|
|
|
612
|
+
const synthWhisper = async(logger, {credentials, stats, voice, text}) => {
|
|
613
|
+
const {api_key, model_id, baseURL, timeout} = credentials;
|
|
614
|
+
try {
|
|
615
|
+
const openai = new OpenAI.OpenAI({
|
|
616
|
+
apiKey: api_key,
|
|
617
|
+
timeout: timeout || 5000,
|
|
618
|
+
...(baseURL && {baseURL})
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
const mp3 = await openai.audio.speech.create({
|
|
622
|
+
model: model_id,
|
|
623
|
+
voice,
|
|
624
|
+
input: text,
|
|
625
|
+
response_format: 'mp3'
|
|
626
|
+
});
|
|
627
|
+
return Buffer.from(await mp3.arrayBuffer());
|
|
628
|
+
} catch (err) {
|
|
629
|
+
logger.info({err}, 'synth whisper returned error');
|
|
630
|
+
stats.increment('tts.count', ['vendor:openai', 'accepted:no']);
|
|
631
|
+
throw err;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
;
|
|
599
635
|
const getFileExtFromMime = (mime) => {
|
|
600
636
|
switch (mime) {
|
|
601
637
|
case 'audio/wav':
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jambonz/speech-utils",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.26",
|
|
4
4
|
"description": "TTS-related speech utilities for jambonz",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"author": "Dave Horton",
|
|
@@ -27,13 +27,14 @@
|
|
|
27
27
|
"@aws-sdk/client-polly": "^3.359.0",
|
|
28
28
|
"@google-cloud/text-to-speech": "^4.2.1",
|
|
29
29
|
"@grpc/grpc-js": "^1.8.13",
|
|
30
|
+
"@jambonz/realtimedb-helpers": "^0.8.7",
|
|
30
31
|
"bent": "^7.3.12",
|
|
31
32
|
"debug": "^4.3.4",
|
|
32
33
|
"form-urlencoded": "^6.1.0",
|
|
33
34
|
"google-protobuf": "^3.21.2",
|
|
34
35
|
"ibm-watson": "^8.0.0",
|
|
35
36
|
"microsoft-cognitiveservices-speech-sdk": "^1.31.0",
|
|
36
|
-
"
|
|
37
|
+
"openai": "^4.16.2",
|
|
37
38
|
"undici": "^5.21.0"
|
|
38
39
|
},
|
|
39
40
|
"devDependencies": {
|
package/test/synth.js
CHANGED
|
@@ -38,7 +38,7 @@ test('Google speech synth tests', async(t) => {
|
|
|
38
38
|
},
|
|
39
39
|
},
|
|
40
40
|
language: 'en-GB',
|
|
41
|
-
gender: '
|
|
41
|
+
gender: 'FEMALE',
|
|
42
42
|
text: 'This is a test. This is only a test',
|
|
43
43
|
salt: 'foo.bar',
|
|
44
44
|
});
|
|
@@ -53,7 +53,7 @@ test('Google speech synth tests', async(t) => {
|
|
|
53
53
|
},
|
|
54
54
|
},
|
|
55
55
|
language: 'en-GB',
|
|
56
|
-
gender: '
|
|
56
|
+
gender: 'FEMALE',
|
|
57
57
|
text: 'This is a test. This is only a test',
|
|
58
58
|
});
|
|
59
59
|
t.ok(opts.servedFromCache, `successfully retrieved cached google audio from ${opts.filePath}`);
|
|
@@ -68,7 +68,7 @@ test('Google speech synth tests', async(t) => {
|
|
|
68
68
|
},
|
|
69
69
|
disableTtsCache: true,
|
|
70
70
|
language: 'en-GB',
|
|
71
|
-
gender: '
|
|
71
|
+
gender: 'FEMALE',
|
|
72
72
|
text: 'This is a test. This is only a test',
|
|
73
73
|
});
|
|
74
74
|
t.ok(!opts.servedFromCache, `successfully synthesized google audio regardless of current cache to ${opts.filePath}`);
|
|
@@ -450,7 +450,7 @@ test('Elevenlabs speech synth tests', async(t) => {
|
|
|
450
450
|
const {synthAudio, client} = fn(opts, logger);
|
|
451
451
|
|
|
452
452
|
if (!process.env.ELEVENLABS_API_KEY || !process.env.ELEVENLABS_VOICE_ID || !process.env.ELEVENLABS_MODEL_ID) {
|
|
453
|
-
t.pass('skipping
|
|
453
|
+
t.pass('skipping ElevenLabs speech synth tests since ELEVENLABS_API_KEY or ELEVENLABS_VOICE_ID or ELEVENLABS_MODEL_ID not provided');
|
|
454
454
|
return t.end();
|
|
455
455
|
}
|
|
456
456
|
const text = 'Hi there and welcome to jambones!';
|
|
@@ -474,6 +474,35 @@ test('Elevenlabs speech synth tests', async(t) => {
|
|
|
474
474
|
client.quit();
|
|
475
475
|
})
|
|
476
476
|
|
|
477
|
+
test('whisper speech synth tests', async(t) => {
|
|
478
|
+
const fn = require('..');
|
|
479
|
+
const {synthAudio, client} = fn(opts, logger);
|
|
480
|
+
|
|
481
|
+
if (!process.env.OPENAI_API_KEY) {
|
|
482
|
+
t.pass('skipping OPENAI speech synth tests since OPENAI_API_KEY not provided');
|
|
483
|
+
return t.end();
|
|
484
|
+
}
|
|
485
|
+
const text = 'Hi there and welcome to jambones!';
|
|
486
|
+
try {
|
|
487
|
+
let opts = await synthAudio(stats, {
|
|
488
|
+
vendor: 'whisper',
|
|
489
|
+
credentials: {
|
|
490
|
+
api_key: process.env.OPENAI_API_KEY,
|
|
491
|
+
model_id: 'tts-1'
|
|
492
|
+
},
|
|
493
|
+
language: 'en-US',
|
|
494
|
+
voice: 'alloy',
|
|
495
|
+
text,
|
|
496
|
+
});
|
|
497
|
+
t.ok(!opts.servedFromCache, `successfully synthesized whisper audio to ${opts.filePath}`);
|
|
498
|
+
|
|
499
|
+
} catch (err) {
|
|
500
|
+
console.error(JSON.stringify(err));
|
|
501
|
+
t.end(err);
|
|
502
|
+
}
|
|
503
|
+
client.quit();
|
|
504
|
+
})
|
|
505
|
+
|
|
477
506
|
test('TTS Cache tests', async(t) => {
|
|
478
507
|
const fn = require('..');
|
|
479
508
|
const {purgeTtsCache, getTtsSize, client} = fn(opts, logger);
|