@jambonz/speech-utils 1.0.18 → 1.0.21
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 +12 -3
- package/lib/synth-audio.js +178 -2
- package/package.json +1 -1
- package/test/aws-credentials.js +23 -0
- package/test/aws.js +11 -11
- package/test/list-voices.js +5 -7
- package/test/synth.js +76 -14
package/.github/workflows/ci.yml
CHANGED
|
@@ -7,11 +7,18 @@ on:
|
|
|
7
7
|
jobs:
|
|
8
8
|
build:
|
|
9
9
|
runs-on: ubuntu-latest
|
|
10
|
+
permissions:
|
|
11
|
+
id-token: write # required to request the GitHub OIDC token for AWS
|
|
12
|
+
contents: read
|
|
10
13
|
steps:
|
|
11
14
|
- uses: actions/checkout@v4
|
|
12
15
|
- uses: actions/setup-node@v4
|
|
13
16
|
with:
|
|
14
17
|
node-version: '20'
|
|
18
|
+
- uses: aws-actions/configure-aws-credentials@v4
|
|
19
|
+
with:
|
|
20
|
+
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
|
|
21
|
+
aws-region: us-east-1
|
|
15
22
|
- run: npm install
|
|
16
23
|
- run: npm run jslint
|
|
17
24
|
- run: sudo apt update && sudo apt install -y squid
|
|
@@ -19,10 +26,12 @@ jobs:
|
|
|
19
26
|
- run: sudo systemctl start squid
|
|
20
27
|
- run: npm test
|
|
21
28
|
env:
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
|
29
|
+
# AWS_* are exported by configure-aws-credentials above as short-lived
|
|
30
|
+
# OIDC credentials; no AWS keys are stored as repository secrets.
|
|
25
31
|
GCP_JSON_KEY: ${{ secrets.GCP_JSON_KEY }}
|
|
32
|
+
# Enables the "AWS speech synth tests by RoleArn" test, which exercises the
|
|
33
|
+
# AssumeRole credential path used in production.
|
|
34
|
+
AWS_ROLE_ARN: ${{ secrets.AWS_ROLE_ARN }}
|
|
26
35
|
|
|
27
36
|
MICROSOFT_API_KEY: ${{ secrets.MICROSOFT_API_KEY }}
|
|
28
37
|
MICROSOFT_REGION: ${{ secrets.MICROSOFT_REGION }}
|
package/lib/synth-audio.js
CHANGED
|
@@ -80,8 +80,8 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
|
|
|
80
80
|
logger = logger || noopLogger;
|
|
81
81
|
|
|
82
82
|
assert.ok(['google', 'aws', 'polly', 'microsoft', 'wellsaid', 'nvidia', 'elevenlabs',
|
|
83
|
-
'whisper', 'deepgram', 'deepgramflux', 'rimelabs', 'cartesia', 'gradium', '
|
|
84
|
-
'murf', 'xai', 'fishaudio']
|
|
83
|
+
'whisper', 'deepgram', 'deepgramflux', 'rimelabs', 'cartesia', 'gradium', 'kugelaudio', 'nineninesix', 'inworld',
|
|
84
|
+
'resemble', 'murf', 'xai', 'fishaudio', 'speechify']
|
|
85
85
|
.includes(vendor) ||
|
|
86
86
|
vendor.startsWith('custom'),
|
|
87
87
|
`synthAudio supported vendors are google, aws, microsoft, nvidia and wellsaid ..etc, not ${vendor}`);
|
|
@@ -139,6 +139,12 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
|
|
|
139
139
|
} else if ('gradium' === vendor) {
|
|
140
140
|
assert.ok(voice, 'synthAudio requires voice when gradium is used');
|
|
141
141
|
assert.ok(credentials.api_key, 'synthAudio requires api_key when gradium is used');
|
|
142
|
+
} else if ('speechify' === vendor) {
|
|
143
|
+
assert.ok(voice, 'synthAudio requires voice when speechify is used');
|
|
144
|
+
assert.ok(credentials.api_key, 'synthAudio requires api_key when speechify is used');
|
|
145
|
+
} else if ('kugelaudio' === vendor) {
|
|
146
|
+
assert.ok(voice, 'synthAudio requires voice when kugelaudio is used');
|
|
147
|
+
assert.ok(credentials.api_key, 'synthAudio requires api_key when kugelaudio is used');
|
|
142
148
|
} else if ('nineninesix' === vendor) {
|
|
143
149
|
assert.ok(voice, 'synthAudio requires voice when nineninesix is used');
|
|
144
150
|
assert.ok(credentials.api_key, 'synthAudio requires api_key when nineninesix is used');
|
|
@@ -234,6 +240,16 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
|
|
|
234
240
|
credentials, options, stats, voice, key, text, renderForCaching, disableTtsStreaming,
|
|
235
241
|
disableTtsCache});
|
|
236
242
|
break;
|
|
243
|
+
case 'kugelaudio':
|
|
244
|
+
audioData = await synthKugelaudio(logger, {
|
|
245
|
+
credentials, options, stats, language, voice, key, text, renderForCaching, disableTtsStreaming,
|
|
246
|
+
disableTtsCache});
|
|
247
|
+
break;
|
|
248
|
+
case 'speechify':
|
|
249
|
+
audioData = await synthSpeechify(logger, {
|
|
250
|
+
credentials, options, stats, language, voice, key, text, renderForCaching, disableTtsStreaming,
|
|
251
|
+
disableTtsCache});
|
|
252
|
+
break;
|
|
237
253
|
case 'nineninesix':
|
|
238
254
|
audioData = await synthNineninesix(logger, {
|
|
239
255
|
credentials, stats, language, voice, key, text, renderForCaching, disableTtsStreaming,
|
|
@@ -1512,6 +1528,166 @@ const synthGradium = async(logger, {
|
|
|
1512
1528
|
}
|
|
1513
1529
|
};
|
|
1514
1530
|
|
|
1531
|
+
/* kugelaudio — json websocket (/ws/tts/stream) for streaming, and POST /v1/tts/generate
|
|
1532
|
+
for the cache render. the POST streams back bare little-endian 16-bit samples at the
|
|
1533
|
+
requested sample_rate, which is exactly the r8 container at 8000.
|
|
1534
|
+
|
|
1535
|
+
voices are numeric ids (or public handles). language is an ISO 639-1 code that drives
|
|
1536
|
+
text normalization; jambonz carries BCP-47, so only the primary subtag is sent. the
|
|
1537
|
+
api rejects codes outside its list, so an unsupported or unset language is omitted
|
|
1538
|
+
and the voice's own language applies. options.api_uri pins a region
|
|
1539
|
+
(e.g. api.eu.kugelaudio.com).
|
|
1540
|
+
*/
|
|
1541
|
+
const KUGELAUDIO_LANGUAGES = ['ar', 'bg', 'bn', 'cs', 'da', 'de', 'el', 'en', 'es', 'fa', 'fi', 'fr', 'he', 'hi',
|
|
1542
|
+
'hr', 'hu', 'id', 'it', 'ja', 'ko', 'ms', 'nl', 'no', 'pl', 'pt', 'ro', 'ru', 'sk', 'sl', 'sr', 'sv', 'ta', 'th',
|
|
1543
|
+
'tr', 'uk', 'ur', 'vi', 'yue', 'zh'];
|
|
1544
|
+
const synthKugelaudio = async(logger, {
|
|
1545
|
+
credentials, options, stats, language, voice, key, text, renderForCaching, disableTtsStreaming,
|
|
1546
|
+
disableTtsCache
|
|
1547
|
+
}) => {
|
|
1548
|
+
const {api_key, model_id} = credentials;
|
|
1549
|
+
const {api_uri, speed, cfg_scale, temperature, normalize, project_id, dictionary_ids} = options || {};
|
|
1550
|
+
const isSet = (v) => v !== null && v !== undefined;
|
|
1551
|
+
|
|
1552
|
+
/* default to using the streaming interface, unless disabled by env var OR we want just a cache file */
|
|
1553
|
+
if (!JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
|
|
1554
|
+
let params = '{';
|
|
1555
|
+
params += `api_key=${api_key}`;
|
|
1556
|
+
params += `,playback_id=${key}`;
|
|
1557
|
+
params += ',vendor=kugelaudio';
|
|
1558
|
+
params += `,voice=${voice}`;
|
|
1559
|
+
params += `,write_cache_file=${disableTtsCache ? 0 : 1}`;
|
|
1560
|
+
params += `,model_id=${model_id || 'kugel-3'}`;
|
|
1561
|
+
if (language) params += `,language=${language}`;
|
|
1562
|
+
if (api_uri) params += `,api_uri=${api_uri}`;
|
|
1563
|
+
if (isSet(speed)) params += `,speed=${speed}`;
|
|
1564
|
+
if (isSet(cfg_scale)) params += `,cfg_scale=${cfg_scale}`;
|
|
1565
|
+
if (isSet(temperature)) params += `,temperature=${temperature}`;
|
|
1566
|
+
if (isSet(normalize)) params += `,normalize=${normalize}`;
|
|
1567
|
+
if (isSet(project_id)) params += `,project_id=${project_id}`;
|
|
1568
|
+
/* the say: param parser is bracket-aware, so the json array survives intact */
|
|
1569
|
+
if (Array.isArray(dictionary_ids)) params += `,dictionary_ids=${JSON.stringify(dictionary_ids)}`;
|
|
1570
|
+
params += '}';
|
|
1571
|
+
|
|
1572
|
+
return {
|
|
1573
|
+
filePath: `say:${params}${text.replace(/\n/g, ' ').replace(/\r/g, ' ')}`,
|
|
1574
|
+
servedFromCache: false,
|
|
1575
|
+
rtt: 0
|
|
1576
|
+
};
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
try {
|
|
1580
|
+
const sampleRate = 8000;
|
|
1581
|
+
const host = (api_uri || 'api.kugelaudio.com').replace(/^[a-z]+:\/\//, '').replace(/\/$/, '');
|
|
1582
|
+
const post = bent(`https://${host}`, 'POST', 'buffer', {
|
|
1583
|
+
'Authorization': `Bearer ${api_key}`,
|
|
1584
|
+
'Content-Type': 'application/json; charset=utf-8'
|
|
1585
|
+
});
|
|
1586
|
+
const voiceId = /^\d+$/.test(`${voice}`) ? Number(voice) : voice;
|
|
1587
|
+
const lang = language && language.split('-')[0].toLowerCase();
|
|
1588
|
+
const audioContent = await post('/v1/tts/generate', {
|
|
1589
|
+
text,
|
|
1590
|
+
voice_id: voiceId,
|
|
1591
|
+
model_id: model_id || 'kugel-3',
|
|
1592
|
+
sample_rate: sampleRate,
|
|
1593
|
+
...(KUGELAUDIO_LANGUAGES.includes(lang) && {language: lang}),
|
|
1594
|
+
...(isSet(speed) && {speed: Number(speed)}),
|
|
1595
|
+
...(isSet(cfg_scale) && {cfg_scale: Number(cfg_scale)}),
|
|
1596
|
+
...(isSet(temperature) && {temperature: Number(temperature)}),
|
|
1597
|
+
...(isSet(normalize) && {normalize: normalize === true || normalize === 'true'}),
|
|
1598
|
+
...(isSet(project_id) && {project_id: Number(project_id)}),
|
|
1599
|
+
...(Array.isArray(dictionary_ids) && {dictionary_ids})
|
|
1600
|
+
});
|
|
1601
|
+
return {
|
|
1602
|
+
audioContent,
|
|
1603
|
+
extension: 'r8',
|
|
1604
|
+
sampleRate
|
|
1605
|
+
};
|
|
1606
|
+
} catch (err) {
|
|
1607
|
+
logger.info({err}, 'synth kugelaudio returned error');
|
|
1608
|
+
stats.increment('tts.count', ['vendor:kugelaudio', 'accepted:no']);
|
|
1609
|
+
throw err;
|
|
1610
|
+
}
|
|
1611
|
+
};
|
|
1612
|
+
|
|
1613
|
+
/* simba-3.2 is English only; with no model set, other languages get simba-3.0 */
|
|
1614
|
+
const speechifyModel = (model_id, language) => {
|
|
1615
|
+
if (model_id) return model_id;
|
|
1616
|
+
return !language || /^en/i.test(language) ? 'simba-3.2' : 'simba-3.0';
|
|
1617
|
+
};
|
|
1618
|
+
const synthSpeechify = async(logger, {
|
|
1619
|
+
credentials, options, stats, language, voice, key, text, renderForCaching, disableTtsStreaming,
|
|
1620
|
+
disableTtsCache
|
|
1621
|
+
}) => {
|
|
1622
|
+
const {api_key, model_id} = credentials;
|
|
1623
|
+
let credOptions = credentials.options || {};
|
|
1624
|
+
if (typeof credOptions === 'string') {
|
|
1625
|
+
try {
|
|
1626
|
+
credOptions = JSON.parse(credOptions);
|
|
1627
|
+
} catch {
|
|
1628
|
+
credOptions = {};
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
const {api_uri, loudness_normalization, text_normalization} = {...credOptions, ...options};
|
|
1632
|
+
const isSet = (v) => v !== null && v !== undefined;
|
|
1633
|
+
const model = speechifyModel(options?.model_id || model_id, language);
|
|
1634
|
+
|
|
1635
|
+
/* default to using the streaming interface, unless disabled by env var OR we want just a cache file */
|
|
1636
|
+
if (!JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
|
|
1637
|
+
let params = '{';
|
|
1638
|
+
params += `api_key=${api_key}`;
|
|
1639
|
+
params += `,playback_id=${key}`;
|
|
1640
|
+
params += ',vendor=speechify';
|
|
1641
|
+
params += `,voice=${voice}`;
|
|
1642
|
+
params += `,write_cache_file=${disableTtsCache ? 0 : 1}`;
|
|
1643
|
+
params += `,model_id=${model}`;
|
|
1644
|
+
if (language) params += `,language=${language}`;
|
|
1645
|
+
if (api_uri) params += `,api_uri=${api_uri}`;
|
|
1646
|
+
if (isSet(loudness_normalization)) params += `,loudness_normalization=${loudness_normalization}`;
|
|
1647
|
+
if (isSet(text_normalization)) params += `,text_normalization=${text_normalization}`;
|
|
1648
|
+
params += '}';
|
|
1649
|
+
|
|
1650
|
+
return {
|
|
1651
|
+
filePath: `say:${params}${text.replace(/\n/g, ' ').replace(/\r/g, ' ')}`,
|
|
1652
|
+
servedFromCache: false,
|
|
1653
|
+
rtt: 0
|
|
1654
|
+
};
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
try {
|
|
1658
|
+
/* other pcm rates are mislabelled 24 kHz on workspaces pinned before 2026-09-30 */
|
|
1659
|
+
const sampleRate = 24000;
|
|
1660
|
+
const host = (api_uri || 'api.speechify.ai').replace(/^[a-z]+:\/\//, '').replace(/\/$/, '');
|
|
1661
|
+
const post = bent(`https://${host}`, 'POST', 'buffer', {
|
|
1662
|
+
'Authorization': `Bearer ${api_key}`,
|
|
1663
|
+
'Content-Type': 'application/json',
|
|
1664
|
+
'Speechify-Caller': 'jambonz'
|
|
1665
|
+
});
|
|
1666
|
+
const toBool = (v) => v === true || v === 'true';
|
|
1667
|
+
const speechOptions = {
|
|
1668
|
+
...(isSet(loudness_normalization) && {loudness_normalization: toBool(loudness_normalization)}),
|
|
1669
|
+
...(isSet(text_normalization) && {text_normalization: toBool(text_normalization)})
|
|
1670
|
+
};
|
|
1671
|
+
const audioContent = await post('/v1/audio/stream', {
|
|
1672
|
+
input: text,
|
|
1673
|
+
voice_id: voice,
|
|
1674
|
+
model,
|
|
1675
|
+
output_format: `pcm_${sampleRate}`,
|
|
1676
|
+
...(language && {language}),
|
|
1677
|
+
...(Object.keys(speechOptions).length && {options: speechOptions})
|
|
1678
|
+
});
|
|
1679
|
+
return {
|
|
1680
|
+
audioContent,
|
|
1681
|
+
extension: 'r24',
|
|
1682
|
+
sampleRate
|
|
1683
|
+
};
|
|
1684
|
+
} catch (err) {
|
|
1685
|
+
logger.info({err}, 'synth speechify returned error');
|
|
1686
|
+
stats.increment('tts.count', ['vendor:speechify', 'accepted:no']);
|
|
1687
|
+
throw err;
|
|
1688
|
+
}
|
|
1689
|
+
};
|
|
1690
|
+
|
|
1515
1691
|
/* nineninesix.ai — a Cartesia-compatible API, but only raw/wav come back
|
|
1516
1692
|
(mp3 is rejected), so the cache render asks for wav rather than mp3. */
|
|
1517
1693
|
/* fish.audio — msgpack websocket for streaming, and a POST endpoint for the cache
|
package/package.json
CHANGED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve AWS credentials for the test suite.
|
|
3
|
+
*
|
|
4
|
+
* Returns null when AWS is not configured, so the caller skips.
|
|
5
|
+
*
|
|
6
|
+
* When AWS_SESSION_TOKEN is set the credentials are temporary -- GitHub OIDC in CI, or
|
|
7
|
+
* `aws sso login` locally -- and the key pair must not be passed through. Doing so sends
|
|
8
|
+
* lib/get-aws-sts-token.js down its accessKeyId branch, which calls GetSessionToken, and
|
|
9
|
+
* AWS rejects GetSessionToken when it is called with session credentials. Returning the
|
|
10
|
+
* region alone routes to the SDK's default credential provider chain instead, which
|
|
11
|
+
* handles temporary credentials correctly.
|
|
12
|
+
*/
|
|
13
|
+
module.exports = () => {
|
|
14
|
+
const region = process.env.AWS_REGION;
|
|
15
|
+
if (!region) return null;
|
|
16
|
+
|
|
17
|
+
const accessKeyId = process.env.AWS_ACCESS_KEY_ID;
|
|
18
|
+
const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY;
|
|
19
|
+
|
|
20
|
+
if (process.env.AWS_SESSION_TOKEN) return {region};
|
|
21
|
+
if (accessKeyId && secretAccessKey) return {accessKeyId, secretAccessKey, region};
|
|
22
|
+
return {region};
|
|
23
|
+
};
|
package/test/aws.js
CHANGED
|
@@ -6,33 +6,33 @@ process.on('unhandledRejection', (reason, p) => {
|
|
|
6
6
|
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
|
|
7
7
|
});
|
|
8
8
|
|
|
9
|
+
const awsCredentials = require('./aws-credentials');
|
|
10
|
+
|
|
9
11
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
10
12
|
|
|
11
13
|
test('AWS - create and cache auth token', async(t) => {
|
|
12
14
|
const fn = require('..');
|
|
13
15
|
const {client, getAwsAuthToken} = fn(opts, logger);
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
const credentials = awsCredentials();
|
|
18
|
+
if (!credentials) {
|
|
16
19
|
t.pass('skipping AWS auth token tests since no AWS credentials provided');
|
|
17
20
|
t.end();
|
|
18
21
|
client.quit();
|
|
19
22
|
return;
|
|
20
23
|
}
|
|
24
|
+
// getAwsAuthToken derives its cache key from roleArn || accessKeyId || speech_credential_sid.
|
|
25
|
+
// With temporary credentials none of the first two are passed, so supply a stable sid --
|
|
26
|
+
// which is what production does for instance-profile credentials.
|
|
27
|
+
const args = {...credentials, speech_credential_sid: 'test-aws-speech-credential'};
|
|
28
|
+
|
|
21
29
|
try {
|
|
22
|
-
let obj = await getAwsAuthToken(
|
|
23
|
-
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
|
24
|
-
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
|
|
25
|
-
region: process.env.AWS_REGION
|
|
26
|
-
});
|
|
30
|
+
let obj = await getAwsAuthToken(args);
|
|
27
31
|
//console.log({obj}, 'received auth token from AWS');
|
|
28
32
|
t.ok(obj.securityToken && !obj.servedFromCache, 'successfullY generated auth token from AWS');
|
|
29
33
|
|
|
30
34
|
await sleep(250);
|
|
31
|
-
obj = await getAwsAuthToken(
|
|
32
|
-
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
|
33
|
-
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
|
|
34
|
-
region: process.env.AWS_REGION
|
|
35
|
-
});
|
|
35
|
+
obj = await getAwsAuthToken(args);
|
|
36
36
|
//console.log({obj}, 'received auth token from AWS - second request');
|
|
37
37
|
t.ok(obj.securityToken && obj.servedFromCache, 'successfully received access token from cache');
|
|
38
38
|
|
package/test/list-voices.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const test = require('tape').test ;
|
|
2
|
+
const awsCredentials = require('./aws-credentials');
|
|
2
3
|
const config = require('config');
|
|
3
4
|
const opts = config.get('redis');
|
|
4
5
|
const fs = require('fs');
|
|
@@ -45,18 +46,15 @@ test('AWS tests', async(t) => {
|
|
|
45
46
|
const fn = require('..');
|
|
46
47
|
const {client, getTtsVoices} = fn(opts, logger);
|
|
47
48
|
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
const credentials = awsCredentials();
|
|
50
|
+
if (!credentials) {
|
|
51
|
+
t.pass('skipping AWS speech synth tests since AWS_REGION not provided');
|
|
50
52
|
return t.end();
|
|
51
53
|
}
|
|
52
54
|
try {
|
|
53
55
|
const opts = {
|
|
54
56
|
vendor: 'aws',
|
|
55
|
-
credentials
|
|
56
|
-
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
|
57
|
-
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
|
|
58
|
-
region: process.env.AWS_REGION,
|
|
59
|
-
}
|
|
57
|
+
credentials
|
|
60
58
|
};
|
|
61
59
|
let result = await getTtsVoices(opts);
|
|
62
60
|
t.ok(result?.Voices?.length > 0, `GetVoices: successfully retrieved ${result.Voices.length} voices from AWS`);
|
package/test/synth.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const test = require('tape').test;
|
|
2
|
+
const awsCredentials = require('./aws-credentials');
|
|
2
3
|
const config = require('config');
|
|
3
4
|
const opts = config.get('redis');
|
|
4
5
|
const fs = require('fs');
|
|
@@ -668,18 +669,15 @@ test('AWS speech synth tests', async(t) => {
|
|
|
668
669
|
const fn = require('..');
|
|
669
670
|
const {synthAudio, client} = fn(opts, logger);
|
|
670
671
|
|
|
671
|
-
|
|
672
|
-
|
|
672
|
+
const credentials = awsCredentials();
|
|
673
|
+
if (!credentials) {
|
|
674
|
+
t.pass('skipping AWS speech synth tests since AWS_REGION not provided');
|
|
673
675
|
return t.end();
|
|
674
676
|
}
|
|
675
677
|
try {
|
|
676
678
|
let opts = await synthAudio(stats, {
|
|
677
679
|
vendor: 'aws',
|
|
678
|
-
credentials
|
|
679
|
-
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
|
680
|
-
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
|
|
681
|
-
region: process.env.AWS_REGION,
|
|
682
|
-
},
|
|
680
|
+
credentials,
|
|
683
681
|
language: 'en-US',
|
|
684
682
|
voice: 'Joey',
|
|
685
683
|
text: 'This is a test. This is only a test',
|
|
@@ -689,11 +687,7 @@ test('AWS speech synth tests', async(t) => {
|
|
|
689
687
|
|
|
690
688
|
opts = await synthAudio(stats, {
|
|
691
689
|
vendor: 'aws',
|
|
692
|
-
credentials
|
|
693
|
-
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
|
694
|
-
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
|
|
695
|
-
region: process.env.AWS_REGION,
|
|
696
|
-
},
|
|
690
|
+
credentials,
|
|
697
691
|
language: 'en-US',
|
|
698
692
|
voice: 'Joey',
|
|
699
693
|
text: 'This is a test. This is only a test',
|
|
@@ -724,9 +718,18 @@ test('AWS speech synth tests by RoleArn', async(t) => {
|
|
|
724
718
|
},
|
|
725
719
|
language: 'en-US',
|
|
726
720
|
voice: 'Joey',
|
|
727
|
-
text: '
|
|
721
|
+
// Distinct text on purpose: the 'AWS speech synth tests' above cache audio for
|
|
722
|
+
// the same vendor/voice/language, and identical text would hit that cache entry,
|
|
723
|
+
// making servedFromCache true and this assertion fail.
|
|
724
|
+
text: 'This is a roleArn test. This is only a roleArn test',
|
|
725
|
+
// Without this, synthPolly returns the mediajam streaming params string, which
|
|
726
|
+
// embeds accessKeyId/secretAccessKey/sessionToken (see lib/synth-audio.js).
|
|
727
|
+
// Every other synth test in this file renders for caching; this one did not,
|
|
728
|
+
// so it printed live credentials into a public CI log.
|
|
729
|
+
renderForCaching: true,
|
|
728
730
|
});
|
|
729
|
-
|
|
731
|
+
// Deliberately does not interpolate opts.filePath -- it can carry credentials.
|
|
732
|
+
t.ok(!opts.servedFromCache, 'successfully synthesized aws by roleArn audio');
|
|
730
733
|
} catch (err) {
|
|
731
734
|
console.error(err);
|
|
732
735
|
t.end(err);
|
|
@@ -1087,6 +1090,65 @@ test('gradium speech synth tests', async(t) => {
|
|
|
1087
1090
|
client.quit();
|
|
1088
1091
|
});
|
|
1089
1092
|
|
|
1093
|
+
test('kugelaudio speech synth tests', async(t) => {
|
|
1094
|
+
const fn = require('..');
|
|
1095
|
+
const {synthAudio, client} = fn(opts, logger);
|
|
1096
|
+
|
|
1097
|
+
if (!process.env.KUGELAUDIO_API_KEY) {
|
|
1098
|
+
t.pass('skipping kugelaudio speech synth tests since KUGELAUDIO_API_KEY is not provided');
|
|
1099
|
+
return t.end();
|
|
1100
|
+
}
|
|
1101
|
+
const text = 'Guten Tag und willkommen bei jambonz! Ihre Bestellung kostet 12,99 Euro. ' + Date.now();
|
|
1102
|
+
try {
|
|
1103
|
+
const opts = await synthAudio(stats, {
|
|
1104
|
+
vendor: 'kugelaudio',
|
|
1105
|
+
credentials: {
|
|
1106
|
+
api_key: process.env.KUGELAUDIO_API_KEY,
|
|
1107
|
+
model_id: 'kugel-3'
|
|
1108
|
+
},
|
|
1109
|
+
language: 'de-DE',
|
|
1110
|
+
voice: '1930',
|
|
1111
|
+
text,
|
|
1112
|
+
renderForCaching: true
|
|
1113
|
+
});
|
|
1114
|
+
t.ok(!opts.servedFromCache, `successfully synthed kugelaudio audio to ${opts.filePath}`);
|
|
1115
|
+
|
|
1116
|
+
} catch (err) {
|
|
1117
|
+
console.error(JSON.stringify(err));
|
|
1118
|
+
t.end(err);
|
|
1119
|
+
}
|
|
1120
|
+
client.quit();
|
|
1121
|
+
});
|
|
1122
|
+
|
|
1123
|
+
test('speechify speech synth tests', async(t) => {
|
|
1124
|
+
const fn = require('..');
|
|
1125
|
+
const {synthAudio, client} = fn(opts, logger);
|
|
1126
|
+
|
|
1127
|
+
if (!process.env.SPEECHIFY_API_KEY) {
|
|
1128
|
+
t.pass('skipping speechify speech synth tests since SPEECHIFY_API_KEY is not provided');
|
|
1129
|
+
return t.end();
|
|
1130
|
+
}
|
|
1131
|
+
const text = 'Hi there and welcome to jambonz! ' + Date.now();
|
|
1132
|
+
try {
|
|
1133
|
+
const opts = await synthAudio(stats, {
|
|
1134
|
+
vendor: 'speechify',
|
|
1135
|
+
credentials: {
|
|
1136
|
+
api_key: process.env.SPEECHIFY_API_KEY
|
|
1137
|
+
},
|
|
1138
|
+
language: 'en-US',
|
|
1139
|
+
voice: 'geffen_32',
|
|
1140
|
+
text,
|
|
1141
|
+
renderForCaching: true
|
|
1142
|
+
});
|
|
1143
|
+
t.ok(!opts.servedFromCache, `successfully synthed speechify audio to ${opts.filePath}`);
|
|
1144
|
+
|
|
1145
|
+
} catch (err) {
|
|
1146
|
+
console.error(JSON.stringify(err));
|
|
1147
|
+
t.end(err);
|
|
1148
|
+
}
|
|
1149
|
+
client.quit();
|
|
1150
|
+
});
|
|
1151
|
+
|
|
1090
1152
|
test('fishaudio speech synth tests', async(t) => {
|
|
1091
1153
|
const fn = require('..');
|
|
1092
1154
|
const {synthAudio, client} = fn(opts, logger);
|