@jambonz/speech-utils 1.0.21 → 1.0.23
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 +212 -26
- package/package.json +6 -3
- package/test/synth.js +59 -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', 'speechify']
|
|
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}`);
|
|
@@ -142,6 +142,12 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
|
|
|
142
142
|
} else if ('speechify' === vendor) {
|
|
143
143
|
assert.ok(voice, 'synthAudio requires voice when speechify is used');
|
|
144
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');
|
|
145
151
|
} else if ('kugelaudio' === vendor) {
|
|
146
152
|
assert.ok(voice, 'synthAudio requires voice when kugelaudio is used');
|
|
147
153
|
assert.ok(credentials.api_key, 'synthAudio requires api_key when kugelaudio is used');
|
|
@@ -250,6 +256,16 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
|
|
|
250
256
|
credentials, options, stats, language, voice, key, text, renderForCaching, disableTtsStreaming,
|
|
251
257
|
disableTtsCache});
|
|
252
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;
|
|
253
269
|
case 'nineninesix':
|
|
254
270
|
audioData = await synthNineninesix(logger, {
|
|
255
271
|
credentials, stats, language, voice, key, text, renderForCaching, disableTtsStreaming,
|
|
@@ -1377,14 +1393,10 @@ const synthCartesia = async(logger, {
|
|
|
1377
1393
|
|
|
1378
1394
|
// omit a control unless explicitly provided (0 is a valid value, so test for nullish only)
|
|
1379
1395
|
const has = (v) => v !== null && v !== undefined;
|
|
1380
|
-
/* Voice controls
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
"starts with sonic-3" predicate the freeswitch streaming module uses
|
|
1385
|
-
(mod_cartesia_tts_streaming: strncmp(model_id, "sonic-3", ...)), so cached
|
|
1386
|
-
and streamed synthesis behave identically.
|
|
1387
|
-
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. */
|
|
1388
1400
|
const isSonic3 = model_id?.startsWith('sonic-3');
|
|
1389
1401
|
const mp3Stream = await client.tts.bytes({
|
|
1390
1402
|
modelId: model_id,
|
|
@@ -1399,12 +1411,6 @@ const synthCartesia = async(logger, {
|
|
|
1399
1411
|
id: voice
|
|
1400
1412
|
}
|
|
1401
1413
|
),
|
|
1402
|
-
...(model_id === 'sonic-2' && (opts.speed || opts.emotion) && {
|
|
1403
|
-
experimentalControls: {
|
|
1404
|
-
...(has(opts.speed) && {speed: opts.speed}),
|
|
1405
|
-
...(opts.emotion && {emotion: [opts.emotion]}),
|
|
1406
|
-
}
|
|
1407
|
-
}),
|
|
1408
1414
|
},
|
|
1409
1415
|
...(isSonic3 && (has(opts.speed) || has(opts.emotion) || has(opts.volume)) && {
|
|
1410
1416
|
generationConfig: {
|
|
@@ -1688,6 +1694,175 @@ const synthSpeechify = async(logger, {
|
|
|
1688
1694
|
}
|
|
1689
1695
|
};
|
|
1690
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
|
+
|
|
1691
1866
|
/* nineninesix.ai — a Cartesia-compatible API, but only raw/wav come back
|
|
1692
1867
|
(mp3 is rejected), so the cache render asks for wav rather than mp3. */
|
|
1693
1868
|
/* fish.audio — msgpack websocket for streaming, and a POST endpoint for the cache
|
|
@@ -1704,8 +1879,18 @@ const synthSpeechify = async(logger, {
|
|
|
1704
1879
|
const synthFishaudio = async(logger, {
|
|
1705
1880
|
credentials, options, stats, voice, key, text, renderForCaching, disableTtsStreaming, disableTtsCache
|
|
1706
1881
|
}) => {
|
|
1707
|
-
const {api_key, model_id
|
|
1708
|
-
|
|
1882
|
+
const {api_key, model_id} = credentials;
|
|
1883
|
+
let credOptions = credentials.options || {};
|
|
1884
|
+
if (typeof credOptions === 'string') {
|
|
1885
|
+
try {
|
|
1886
|
+
credOptions = JSON.parse(credOptions);
|
|
1887
|
+
} catch {
|
|
1888
|
+
credOptions = {};
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
/* Extra Options on the credential apply to every request; the verb's options win */
|
|
1892
|
+
const {reference_id, latency, chunk_length, speed, volume, api_uri} = {...credOptions, ...options};
|
|
1893
|
+
const isSet = (v) => v !== null && v !== undefined && v !== '';
|
|
1709
1894
|
|
|
1710
1895
|
/* free-text reference_id in the vendor options wins over the voice selector */
|
|
1711
1896
|
const refId = reference_id || (voice && voice !== 'default' ? voice : null);
|
|
@@ -1720,10 +1905,10 @@ const synthFishaudio = async(logger, {
|
|
|
1720
1905
|
params += `,write_cache_file=${disableTtsCache ? 0 : 1}`;
|
|
1721
1906
|
if (model_id) params += `,model_id=${model_id}`;
|
|
1722
1907
|
if (latency) params += `,latency=${latency}`;
|
|
1723
|
-
if (chunk_length) params += `,chunk_length=${chunk_length}`;
|
|
1724
|
-
if (speed) params += `,speed=${speed}`;
|
|
1725
|
-
if (volume) params += `,volume=${volume}`;
|
|
1726
|
-
if (
|
|
1908
|
+
if (isSet(chunk_length)) params += `,chunk_length=${chunk_length}`;
|
|
1909
|
+
if (isSet(speed)) params += `,speed=${speed}`;
|
|
1910
|
+
if (isSet(volume)) params += `,volume=${volume}`;
|
|
1911
|
+
if (api_uri) params += `,api_uri=${api_uri}`;
|
|
1727
1912
|
params += '}';
|
|
1728
1913
|
|
|
1729
1914
|
return {
|
|
@@ -1735,7 +1920,8 @@ const synthFishaudio = async(logger, {
|
|
|
1735
1920
|
|
|
1736
1921
|
try {
|
|
1737
1922
|
const sampleRate = 8000;
|
|
1738
|
-
const
|
|
1923
|
+
const host = (api_uri || 'api.fish.audio').replace(/^[a-z]+:\/\//, '').replace(/\/$/, '');
|
|
1924
|
+
const post = bent(`https://${host}`, 'POST', 'buffer', {
|
|
1739
1925
|
'Authorization': `Bearer ${api_key}`,
|
|
1740
1926
|
'Content-Type': 'application/json',
|
|
1741
1927
|
/* the model is selected by header, not in the body */
|
|
@@ -1747,10 +1933,10 @@ const synthFishaudio = async(logger, {
|
|
|
1747
1933
|
sample_rate: sampleRate,
|
|
1748
1934
|
...(refId && {reference_id: refId}),
|
|
1749
1935
|
...(latency && {latency}),
|
|
1750
|
-
...(chunk_length && {chunk_length: parseInt(chunk_length, 10)}),
|
|
1751
|
-
...((speed || volume) && {prosody: {
|
|
1752
|
-
...(speed && {speed: parseFloat(speed)}),
|
|
1753
|
-
...(volume && {volume: parseFloat(volume)})
|
|
1936
|
+
...(isSet(chunk_length) && {chunk_length: parseInt(chunk_length, 10)}),
|
|
1937
|
+
...((isSet(speed) || isSet(volume)) && {prosody: {
|
|
1938
|
+
...(isSet(speed) && {speed: parseFloat(speed)}),
|
|
1939
|
+
...(isSet(volume) && {volume: parseFloat(volume)})
|
|
1754
1940
|
}})
|
|
1755
1941
|
});
|
|
1756
1942
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jambonz/speech-utils",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.23",
|
|
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',
|
|
@@ -1149,6 +1148,64 @@ test('speechify speech synth tests', async(t) => {
|
|
|
1149
1148
|
client.quit();
|
|
1150
1149
|
});
|
|
1151
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
|
+
|
|
1152
1209
|
test('fishaudio speech synth tests', async(t) => {
|
|
1153
1210
|
const fn = require('..');
|
|
1154
1211
|
const {synthAudio, client} = fn(opts, logger);
|