@jambonz/speech-utils 1.0.19 → 1.0.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +4 -4
- package/.github/workflows/publish.yml +2 -2
- package/lib/synth-audio.js +276 -15
- package/package.json +6 -3
- package/test/synth.js +88 -2
package/.github/workflows/ci.yml
CHANGED
|
@@ -11,11 +11,11 @@ jobs:
|
|
|
11
11
|
id-token: write # required to request the GitHub OIDC token for AWS
|
|
12
12
|
contents: read
|
|
13
13
|
steps:
|
|
14
|
-
- uses: actions/checkout@
|
|
15
|
-
- uses: actions/setup-node@
|
|
14
|
+
- uses: actions/checkout@v5
|
|
15
|
+
- uses: actions/setup-node@v5
|
|
16
16
|
with:
|
|
17
|
-
node-version: '
|
|
18
|
-
- uses: aws-actions/configure-aws-credentials@
|
|
17
|
+
node-version: '22'
|
|
18
|
+
- uses: aws-actions/configure-aws-credentials@v6
|
|
19
19
|
with:
|
|
20
20
|
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
|
|
21
21
|
aws-region: us-east-1
|
package/lib/synth-audio.js
CHANGED
|
@@ -81,7 +81,7 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
|
|
|
81
81
|
|
|
82
82
|
assert.ok(['google', 'aws', 'polly', 'microsoft', 'wellsaid', 'nvidia', 'elevenlabs',
|
|
83
83
|
'whisper', 'deepgram', 'deepgramflux', 'rimelabs', 'cartesia', 'gradium', 'kugelaudio', 'nineninesix', 'inworld',
|
|
84
|
-
'resemble', 'murf', 'xai', 'fishaudio']
|
|
84
|
+
'resemble', 'murf', 'xai', 'fishaudio', 'speechify', 'giggy', 'svara']
|
|
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,15 @@ 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 ('giggy' === vendor) {
|
|
146
|
+
assert.ok(voice, 'synthAudio requires voice when giggy is used');
|
|
147
|
+
assert.ok(credentials.api_key, 'synthAudio requires api_key when giggy is used');
|
|
148
|
+
} else if ('svara' === vendor) {
|
|
149
|
+
assert.ok(voice, 'synthAudio requires voice when svara is used');
|
|
150
|
+
assert.ok(credentials.api_key, 'synthAudio requires api_key when svara is used');
|
|
142
151
|
} else if ('kugelaudio' === vendor) {
|
|
143
152
|
assert.ok(voice, 'synthAudio requires voice when kugelaudio is used');
|
|
144
153
|
assert.ok(credentials.api_key, 'synthAudio requires api_key when kugelaudio is used');
|
|
@@ -242,6 +251,21 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
|
|
|
242
251
|
credentials, options, stats, language, voice, key, text, renderForCaching, disableTtsStreaming,
|
|
243
252
|
disableTtsCache});
|
|
244
253
|
break;
|
|
254
|
+
case 'speechify':
|
|
255
|
+
audioData = await synthSpeechify(logger, {
|
|
256
|
+
credentials, options, stats, language, voice, key, text, renderForCaching, disableTtsStreaming,
|
|
257
|
+
disableTtsCache});
|
|
258
|
+
break;
|
|
259
|
+
case 'giggy':
|
|
260
|
+
audioData = await synthGiggy(logger, {
|
|
261
|
+
credentials, options, stats, voice, key, text, renderForCaching, disableTtsStreaming,
|
|
262
|
+
disableTtsCache});
|
|
263
|
+
break;
|
|
264
|
+
case 'svara':
|
|
265
|
+
audioData = await synthSvara(logger, {
|
|
266
|
+
credentials, options, stats, language, voice, key, text, renderForCaching, disableTtsStreaming,
|
|
267
|
+
disableTtsCache});
|
|
268
|
+
break;
|
|
245
269
|
case 'nineninesix':
|
|
246
270
|
audioData = await synthNineninesix(logger, {
|
|
247
271
|
credentials, stats, language, voice, key, text, renderForCaching, disableTtsStreaming,
|
|
@@ -1369,14 +1393,10 @@ const synthCartesia = async(logger, {
|
|
|
1369
1393
|
|
|
1370
1394
|
// omit a control unless explicitly provided (0 is a valid value, so test for nullish only)
|
|
1371
1395
|
const has = (v) => v !== null && v !== undefined;
|
|
1372
|
-
/* Voice controls
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
"starts with sonic-3" predicate the freeswitch streaming module uses
|
|
1377
|
-
(mod_cartesia_tts_streaming: strncmp(model_id, "sonic-3", ...)), so cached
|
|
1378
|
-
and streamed synthesis behave identically.
|
|
1379
|
-
Older models (sonic, sonic-english, sonic-multilingual, sonic-2024-*) take neither. */
|
|
1396
|
+
/* Voice controls (speed, emotion, volume) go in `generationConfig`, which only the
|
|
1397
|
+
sonic-3 family (sonic-3, sonic-3.5, sonic-3.6, pinned sonic-3.x snapshots) accepts;
|
|
1398
|
+
other models get no controls. mediajam uses the same "starts with sonic-3" check
|
|
1399
|
+
(internal/tts/cartesia.go), so cached and streamed synthesis behave identically. */
|
|
1380
1400
|
const isSonic3 = model_id?.startsWith('sonic-3');
|
|
1381
1401
|
const mp3Stream = await client.tts.bytes({
|
|
1382
1402
|
modelId: model_id,
|
|
@@ -1391,12 +1411,6 @@ const synthCartesia = async(logger, {
|
|
|
1391
1411
|
id: voice
|
|
1392
1412
|
}
|
|
1393
1413
|
),
|
|
1394
|
-
...(model_id === 'sonic-2' && (opts.speed || opts.emotion) && {
|
|
1395
|
-
experimentalControls: {
|
|
1396
|
-
...(has(opts.speed) && {speed: opts.speed}),
|
|
1397
|
-
...(opts.emotion && {emotion: [opts.emotion]}),
|
|
1398
|
-
}
|
|
1399
|
-
}),
|
|
1400
1414
|
},
|
|
1401
1415
|
...(isSonic3 && (has(opts.speed) || has(opts.emotion) || has(opts.volume)) && {
|
|
1402
1416
|
generationConfig: {
|
|
@@ -1602,6 +1616,253 @@ const synthKugelaudio = async(logger, {
|
|
|
1602
1616
|
}
|
|
1603
1617
|
};
|
|
1604
1618
|
|
|
1619
|
+
/* simba-3.2 is English only; with no model set, other languages get simba-3.0 */
|
|
1620
|
+
const speechifyModel = (model_id, language) => {
|
|
1621
|
+
if (model_id) return model_id;
|
|
1622
|
+
return !language || /^en/i.test(language) ? 'simba-3.2' : 'simba-3.0';
|
|
1623
|
+
};
|
|
1624
|
+
const synthSpeechify = async(logger, {
|
|
1625
|
+
credentials, options, stats, language, voice, key, text, renderForCaching, disableTtsStreaming,
|
|
1626
|
+
disableTtsCache
|
|
1627
|
+
}) => {
|
|
1628
|
+
const {api_key, model_id} = credentials;
|
|
1629
|
+
let credOptions = credentials.options || {};
|
|
1630
|
+
if (typeof credOptions === 'string') {
|
|
1631
|
+
try {
|
|
1632
|
+
credOptions = JSON.parse(credOptions);
|
|
1633
|
+
} catch {
|
|
1634
|
+
credOptions = {};
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
const {api_uri, loudness_normalization, text_normalization} = {...credOptions, ...options};
|
|
1638
|
+
const isSet = (v) => v !== null && v !== undefined;
|
|
1639
|
+
const model = speechifyModel(options?.model_id || model_id, language);
|
|
1640
|
+
|
|
1641
|
+
/* default to using the streaming interface, unless disabled by env var OR we want just a cache file */
|
|
1642
|
+
if (!JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
|
|
1643
|
+
let params = '{';
|
|
1644
|
+
params += `api_key=${api_key}`;
|
|
1645
|
+
params += `,playback_id=${key}`;
|
|
1646
|
+
params += ',vendor=speechify';
|
|
1647
|
+
params += `,voice=${voice}`;
|
|
1648
|
+
params += `,write_cache_file=${disableTtsCache ? 0 : 1}`;
|
|
1649
|
+
params += `,model_id=${model}`;
|
|
1650
|
+
if (language) params += `,language=${language}`;
|
|
1651
|
+
if (api_uri) params += `,api_uri=${api_uri}`;
|
|
1652
|
+
if (isSet(loudness_normalization)) params += `,loudness_normalization=${loudness_normalization}`;
|
|
1653
|
+
if (isSet(text_normalization)) params += `,text_normalization=${text_normalization}`;
|
|
1654
|
+
params += '}';
|
|
1655
|
+
|
|
1656
|
+
return {
|
|
1657
|
+
filePath: `say:${params}${text.replace(/\n/g, ' ').replace(/\r/g, ' ')}`,
|
|
1658
|
+
servedFromCache: false,
|
|
1659
|
+
rtt: 0
|
|
1660
|
+
};
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
try {
|
|
1664
|
+
/* other pcm rates are mislabelled 24 kHz on workspaces pinned before 2026-09-30 */
|
|
1665
|
+
const sampleRate = 24000;
|
|
1666
|
+
const host = (api_uri || 'api.speechify.ai').replace(/^[a-z]+:\/\//, '').replace(/\/$/, '');
|
|
1667
|
+
const post = bent(`https://${host}`, 'POST', 'buffer', {
|
|
1668
|
+
'Authorization': `Bearer ${api_key}`,
|
|
1669
|
+
'Content-Type': 'application/json',
|
|
1670
|
+
'Speechify-Caller': 'jambonz'
|
|
1671
|
+
});
|
|
1672
|
+
const toBool = (v) => v === true || v === 'true';
|
|
1673
|
+
const speechOptions = {
|
|
1674
|
+
...(isSet(loudness_normalization) && {loudness_normalization: toBool(loudness_normalization)}),
|
|
1675
|
+
...(isSet(text_normalization) && {text_normalization: toBool(text_normalization)})
|
|
1676
|
+
};
|
|
1677
|
+
const audioContent = await post('/v1/audio/stream', {
|
|
1678
|
+
input: text,
|
|
1679
|
+
voice_id: voice,
|
|
1680
|
+
model,
|
|
1681
|
+
output_format: `pcm_${sampleRate}`,
|
|
1682
|
+
...(language && {language}),
|
|
1683
|
+
...(Object.keys(speechOptions).length && {options: speechOptions})
|
|
1684
|
+
});
|
|
1685
|
+
return {
|
|
1686
|
+
audioContent,
|
|
1687
|
+
extension: 'r24',
|
|
1688
|
+
sampleRate
|
|
1689
|
+
};
|
|
1690
|
+
} catch (err) {
|
|
1691
|
+
logger.info({err}, 'synth speechify returned error');
|
|
1692
|
+
stats.increment('tts.count', ['vendor:speechify', 'accepted:no']);
|
|
1693
|
+
throw err;
|
|
1694
|
+
}
|
|
1695
|
+
};
|
|
1696
|
+
|
|
1697
|
+
/* language_code is never sent: Giggy rejects one that differs from the voice's own */
|
|
1698
|
+
const synthGiggy = async(logger, {
|
|
1699
|
+
credentials, options, stats, voice, key, text, renderForCaching, disableTtsStreaming,
|
|
1700
|
+
disableTtsCache
|
|
1701
|
+
}) => {
|
|
1702
|
+
const {api_key, model_id} = credentials;
|
|
1703
|
+
let credOptions = credentials.options || {};
|
|
1704
|
+
if (typeof credOptions === 'string') {
|
|
1705
|
+
try {
|
|
1706
|
+
credOptions = JSON.parse(credOptions);
|
|
1707
|
+
} catch {
|
|
1708
|
+
credOptions = {};
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
const {api_uri, speed, seed} = {...credOptions, ...options};
|
|
1712
|
+
const isSet = (v) => v !== null && v !== undefined;
|
|
1713
|
+
const model = options?.model_id || model_id || 'giggyspeech';
|
|
1714
|
+
|
|
1715
|
+
/* default to using the streaming interface, unless disabled by env var OR we want just a cache file */
|
|
1716
|
+
if (!JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
|
|
1717
|
+
let params = '{';
|
|
1718
|
+
params += `api_key=${api_key}`;
|
|
1719
|
+
params += `,playback_id=${key}`;
|
|
1720
|
+
params += ',vendor=giggy';
|
|
1721
|
+
params += `,voice=${voice}`;
|
|
1722
|
+
params += `,write_cache_file=${disableTtsCache ? 0 : 1}`;
|
|
1723
|
+
params += `,model_id=${model}`;
|
|
1724
|
+
if (api_uri) params += `,api_uri=${api_uri}`;
|
|
1725
|
+
if (isSet(speed)) params += `,speed=${speed}`;
|
|
1726
|
+
if (isSet(seed)) params += `,seed=${seed}`;
|
|
1727
|
+
params += '}';
|
|
1728
|
+
|
|
1729
|
+
return {
|
|
1730
|
+
filePath: `say:${params}${text.replace(/\n/g, ' ').replace(/\r/g, ' ')}`,
|
|
1731
|
+
servedFromCache: false,
|
|
1732
|
+
rtt: 0
|
|
1733
|
+
};
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
try {
|
|
1737
|
+
/* streaming mode is the only one that returns raw pcm (fast/batch are mp3) */
|
|
1738
|
+
const sampleRate = 24000;
|
|
1739
|
+
const host = (api_uri || 'giggy.ai').replace(/^[a-z]+:\/\//, '').replace(/\/$/, '');
|
|
1740
|
+
const post = bent(`https://${host}`, 'POST', 'buffer', {
|
|
1741
|
+
'xi-api-key': api_key,
|
|
1742
|
+
'Content-Type': 'application/json'
|
|
1743
|
+
});
|
|
1744
|
+
const audioContent = await post('/v1/text-to-speech', {
|
|
1745
|
+
text,
|
|
1746
|
+
voice_id: voice,
|
|
1747
|
+
model_id: model,
|
|
1748
|
+
mode: 'streaming',
|
|
1749
|
+
output_format: `pcm_${sampleRate}`,
|
|
1750
|
+
...(isSet(speed) && {voice_settings: {speed: Number(speed)}}),
|
|
1751
|
+
...(isSet(seed) && {seed: Number(seed)})
|
|
1752
|
+
});
|
|
1753
|
+
return {
|
|
1754
|
+
audioContent,
|
|
1755
|
+
extension: 'r24',
|
|
1756
|
+
sampleRate
|
|
1757
|
+
};
|
|
1758
|
+
} catch (err) {
|
|
1759
|
+
logger.info({err}, 'synth giggy returned error');
|
|
1760
|
+
stats.increment('tts.count', ['vendor:giggy', 'accepted:no']);
|
|
1761
|
+
throw err;
|
|
1762
|
+
}
|
|
1763
|
+
};
|
|
1764
|
+
|
|
1765
|
+
const synthSvara = async(logger, {
|
|
1766
|
+
credentials, options, stats, language, voice, key, text, renderForCaching, disableTtsStreaming,
|
|
1767
|
+
disableTtsCache
|
|
1768
|
+
}) => {
|
|
1769
|
+
const {api_key, model_id} = credentials;
|
|
1770
|
+
let credOptions = credentials.options || {};
|
|
1771
|
+
if (typeof credOptions === 'string') {
|
|
1772
|
+
try {
|
|
1773
|
+
credOptions = JSON.parse(credOptions);
|
|
1774
|
+
} catch {
|
|
1775
|
+
credOptions = {};
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
const {
|
|
1779
|
+
api_uri, speed, pronunciation_dictionary_id, normalize, temperature, top_p, top_k, min_p,
|
|
1780
|
+
repetition_penalty, presence_penalty, max_tokens, chunk_size, buffer_ms, chunk_codes
|
|
1781
|
+
} = {...credOptions, ...options};
|
|
1782
|
+
const isSet = (v) => v !== null && v !== undefined;
|
|
1783
|
+
const model = options?.model_id || model_id || 'svara-tts-turbo';
|
|
1784
|
+
|
|
1785
|
+
/* default to using the streaming interface, unless disabled by env var OR we want just a cache file */
|
|
1786
|
+
if (!JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
|
|
1787
|
+
let params = '{';
|
|
1788
|
+
params += `api_key=${api_key}`;
|
|
1789
|
+
params += `,playback_id=${key}`;
|
|
1790
|
+
params += ',vendor=svara';
|
|
1791
|
+
params += `,voice=${voice}`;
|
|
1792
|
+
params += `,write_cache_file=${disableTtsCache ? 0 : 1}`;
|
|
1793
|
+
params += `,model_id=${model}`;
|
|
1794
|
+
if (language) params += `,language=${language}`;
|
|
1795
|
+
if (api_uri) params += `,endpoint=${api_uri}`;
|
|
1796
|
+
if (isSet(speed)) params += `,speed=${speed}`;
|
|
1797
|
+
if (pronunciation_dictionary_id) params += `,pronunciation_dictionary_id=${pronunciation_dictionary_id}`;
|
|
1798
|
+
if (isSet(normalize)) params += `,normalize=${normalize}`;
|
|
1799
|
+
if (isSet(temperature)) params += `,temperature=${temperature}`;
|
|
1800
|
+
if (isSet(top_p)) params += `,top_p=${top_p}`;
|
|
1801
|
+
if (isSet(top_k)) params += `,top_k=${top_k}`;
|
|
1802
|
+
if (isSet(min_p)) params += `,min_p=${min_p}`;
|
|
1803
|
+
if (isSet(repetition_penalty)) params += `,repetition_penalty=${repetition_penalty}`;
|
|
1804
|
+
if (isSet(presence_penalty)) params += `,presence_penalty=${presence_penalty}`;
|
|
1805
|
+
if (isSet(max_tokens)) params += `,max_tokens=${max_tokens}`;
|
|
1806
|
+
if (isSet(chunk_size)) params += `,chunk_size=${chunk_size}`;
|
|
1807
|
+
if (isSet(buffer_ms)) params += `,buffer_ms=${buffer_ms}`;
|
|
1808
|
+
if (isSet(chunk_codes)) params += `,chunk_codes=${chunk_codes}`;
|
|
1809
|
+
params += '}';
|
|
1810
|
+
|
|
1811
|
+
return {
|
|
1812
|
+
filePath: `say:${params}${text.replace(/\n/g, ' ').replace(/\r/g, ' ')}`,
|
|
1813
|
+
servedFromCache: false,
|
|
1814
|
+
rtt: 0
|
|
1815
|
+
};
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
try {
|
|
1819
|
+
const host = (api_uri || 'api.kenpathlabs.com').replace(/^[a-z]+:\/\//, '').replace(/\/$/, '');
|
|
1820
|
+
const post = bent(`https://${host}`, 'POST', 'buffer', {
|
|
1821
|
+
'Authorization': `Bearer ${api_key}`,
|
|
1822
|
+
'Content-Type': 'application/json'
|
|
1823
|
+
});
|
|
1824
|
+
const audioContent = await post('/v1/audio/speech', {
|
|
1825
|
+
input: text,
|
|
1826
|
+
voice,
|
|
1827
|
+
model,
|
|
1828
|
+
response_format: 'wav',
|
|
1829
|
+
sample_rate: 8000,
|
|
1830
|
+
...(language && {lang: language}),
|
|
1831
|
+
...(isSet(speed) && {speed: Number(speed)}),
|
|
1832
|
+
...(pronunciation_dictionary_id && {pronunciation_dictionary_id}),
|
|
1833
|
+
...(isSet(normalize) && {normalize: normalize === true || normalize === 'true'}),
|
|
1834
|
+
...(isSet(temperature) && {temperature: Number(temperature)}),
|
|
1835
|
+
...(isSet(top_p) && {top_p: Number(top_p)}),
|
|
1836
|
+
...(isSet(top_k) && {top_k: Number(top_k)}),
|
|
1837
|
+
...(isSet(min_p) && {min_p: Number(min_p)}),
|
|
1838
|
+
...(isSet(repetition_penalty) && {repetition_penalty: Number(repetition_penalty)}),
|
|
1839
|
+
...(isSet(presence_penalty) && {presence_penalty: Number(presence_penalty)}),
|
|
1840
|
+
...(isSet(max_tokens) && {max_tokens: Number(max_tokens)}),
|
|
1841
|
+
...(isSet(chunk_size) && {chunk_size: Number(chunk_size)}),
|
|
1842
|
+
...(isSet(buffer_ms) && {buffer_ms: Number(buffer_ms)}),
|
|
1843
|
+
...(isSet(chunk_codes) && {chunk_codes: Number(chunk_codes)})
|
|
1844
|
+
});
|
|
1845
|
+
return {
|
|
1846
|
+
audioContent,
|
|
1847
|
+
extension: 'wav',
|
|
1848
|
+
sampleRate: 8000
|
|
1849
|
+
};
|
|
1850
|
+
} catch (err) {
|
|
1851
|
+
/* errors are JSON {detail: {status, message}}; surface the body instead of 'undefined' */
|
|
1852
|
+
if (err.name === 'StatusError' && typeof err.text === 'function') {
|
|
1853
|
+
try {
|
|
1854
|
+
const body = await err.text();
|
|
1855
|
+
if (body) err.message = body;
|
|
1856
|
+
} catch (readErr) {
|
|
1857
|
+
logger.info({readErr}, 'synth svara: failed to read error response body');
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
logger.info({err}, 'synth svara returned error');
|
|
1861
|
+
stats.increment('tts.count', ['vendor:svara', 'accepted:no']);
|
|
1862
|
+
throw err;
|
|
1863
|
+
}
|
|
1864
|
+
};
|
|
1865
|
+
|
|
1605
1866
|
/* nineninesix.ai — a Cartesia-compatible API, but only raw/wav come back
|
|
1606
1867
|
(mp3 is rejected), so the cache render asks for wav rather than mp3. */
|
|
1607
1868
|
/* fish.audio — msgpack websocket for streaming, and a POST endpoint for the cache
|
package/package.json
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jambonz/speech-utils",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.22",
|
|
4
|
+
"engines": {
|
|
5
|
+
"node": ">=22"
|
|
6
|
+
},
|
|
4
7
|
"description": "TTS-related speech utilities for jambonz",
|
|
5
8
|
"main": "index.js",
|
|
6
9
|
"author": "Dave Horton",
|
|
@@ -28,8 +31,8 @@
|
|
|
28
31
|
"@aws-sdk/client-polly": "^3.496.0",
|
|
29
32
|
"@aws-sdk/client-sts": "^3.496.0",
|
|
30
33
|
"@cartesia/cartesia-js": "^2.2.7",
|
|
31
|
-
"@google-cloud/text-to-speech": "^
|
|
32
|
-
"@grpc/grpc-js": "^1.
|
|
34
|
+
"@google-cloud/text-to-speech": "^7.0.0",
|
|
35
|
+
"@grpc/grpc-js": "^1.14.5",
|
|
33
36
|
"@jambonz/realtimedb-helpers": "^0.8.7",
|
|
34
37
|
"bent": "^7.3.12",
|
|
35
38
|
"debug": "^4.3.4",
|
package/test/synth.js
CHANGED
|
@@ -1041,10 +1041,9 @@ test('Cartesia speech synth tests', async(t) => {
|
|
|
1041
1041
|
vendor: 'cartesia',
|
|
1042
1042
|
credentials: {
|
|
1043
1043
|
api_key: process.env.CARTESIA_API_KEY,
|
|
1044
|
-
model_id: 'sonic-
|
|
1044
|
+
model_id: 'sonic-3.6',
|
|
1045
1045
|
options: JSON.stringify({
|
|
1046
1046
|
speed: 1,
|
|
1047
|
-
emotion: 'female_happy',
|
|
1048
1047
|
})
|
|
1049
1048
|
},
|
|
1050
1049
|
language: 'en',
|
|
@@ -1120,6 +1119,93 @@ test('kugelaudio speech synth tests', async(t) => {
|
|
|
1120
1119
|
client.quit();
|
|
1121
1120
|
});
|
|
1122
1121
|
|
|
1122
|
+
test('speechify speech synth tests', async(t) => {
|
|
1123
|
+
const fn = require('..');
|
|
1124
|
+
const {synthAudio, client} = fn(opts, logger);
|
|
1125
|
+
|
|
1126
|
+
if (!process.env.SPEECHIFY_API_KEY) {
|
|
1127
|
+
t.pass('skipping speechify speech synth tests since SPEECHIFY_API_KEY is not provided');
|
|
1128
|
+
return t.end();
|
|
1129
|
+
}
|
|
1130
|
+
const text = 'Hi there and welcome to jambonz! ' + Date.now();
|
|
1131
|
+
try {
|
|
1132
|
+
const opts = await synthAudio(stats, {
|
|
1133
|
+
vendor: 'speechify',
|
|
1134
|
+
credentials: {
|
|
1135
|
+
api_key: process.env.SPEECHIFY_API_KEY
|
|
1136
|
+
},
|
|
1137
|
+
language: 'en-US',
|
|
1138
|
+
voice: 'geffen_32',
|
|
1139
|
+
text,
|
|
1140
|
+
renderForCaching: true
|
|
1141
|
+
});
|
|
1142
|
+
t.ok(!opts.servedFromCache, `successfully synthed speechify audio to ${opts.filePath}`);
|
|
1143
|
+
|
|
1144
|
+
} catch (err) {
|
|
1145
|
+
console.error(JSON.stringify(err));
|
|
1146
|
+
t.end(err);
|
|
1147
|
+
}
|
|
1148
|
+
client.quit();
|
|
1149
|
+
});
|
|
1150
|
+
|
|
1151
|
+
test('giggy speech synth tests', async(t) => {
|
|
1152
|
+
const fn = require('..');
|
|
1153
|
+
const {synthAudio, client} = fn(opts, logger);
|
|
1154
|
+
|
|
1155
|
+
if (!process.env.GIGGY_API_KEY) {
|
|
1156
|
+
t.pass('skipping giggy speech synth tests since GIGGY_API_KEY is not provided');
|
|
1157
|
+
return t.end();
|
|
1158
|
+
}
|
|
1159
|
+
const text = 'Hi there and welcome to jambonz! ' + Date.now();
|
|
1160
|
+
try {
|
|
1161
|
+
const opts = await synthAudio(stats, {
|
|
1162
|
+
vendor: 'giggy',
|
|
1163
|
+
credentials: {
|
|
1164
|
+
api_key: process.env.GIGGY_API_KEY
|
|
1165
|
+
},
|
|
1166
|
+
language: 'en-US',
|
|
1167
|
+
voice: process.env.GIGGY_VOICE_ID || '8f5aee7d-dd96-41c4-b04f-19bff046b02a',
|
|
1168
|
+
text,
|
|
1169
|
+
renderForCaching: true
|
|
1170
|
+
});
|
|
1171
|
+
t.ok(!opts.servedFromCache, `successfully synthed giggy audio to ${opts.filePath}`);
|
|
1172
|
+
|
|
1173
|
+
} catch (err) {
|
|
1174
|
+
console.error(JSON.stringify(err));
|
|
1175
|
+
t.end(err);
|
|
1176
|
+
}
|
|
1177
|
+
client.quit();
|
|
1178
|
+
});
|
|
1179
|
+
|
|
1180
|
+
test('svara speech synth tests', async(t) => {
|
|
1181
|
+
const fn = require('..');
|
|
1182
|
+
const {synthAudio, client} = fn(opts, logger);
|
|
1183
|
+
|
|
1184
|
+
if (!process.env.SVARA_API_KEY) {
|
|
1185
|
+
t.pass('skipping svara speech synth tests since SVARA_API_KEY is not provided');
|
|
1186
|
+
return t.end();
|
|
1187
|
+
}
|
|
1188
|
+
const text = 'Hi there and welcome to jambonz! ' + Date.now();
|
|
1189
|
+
try {
|
|
1190
|
+
const opts = await synthAudio(stats, {
|
|
1191
|
+
vendor: 'svara',
|
|
1192
|
+
credentials: {
|
|
1193
|
+
api_key: process.env.SVARA_API_KEY
|
|
1194
|
+
},
|
|
1195
|
+
language: 'en-US',
|
|
1196
|
+
voice: process.env.SVARA_VOICE_ID || 'sv_a5thzx8v',
|
|
1197
|
+
text,
|
|
1198
|
+
renderForCaching: true
|
|
1199
|
+
});
|
|
1200
|
+
t.ok(!opts.servedFromCache, `successfully synthed svara audio to ${opts.filePath}`);
|
|
1201
|
+
|
|
1202
|
+
} catch (err) {
|
|
1203
|
+
console.error(JSON.stringify(err));
|
|
1204
|
+
t.end(err);
|
|
1205
|
+
}
|
|
1206
|
+
client.quit();
|
|
1207
|
+
});
|
|
1208
|
+
|
|
1123
1209
|
test('fishaudio speech synth tests', async(t) => {
|
|
1124
1210
|
const fn = require('..');
|
|
1125
1211
|
const {synthAudio, client} = fn(opts, logger);
|