@jambonz/speech-utils 1.0.19 → 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/lib/synth-audio.js +87 -1
- package/package.json +1 -1
- package/test/synth.js +29 -0
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']
|
|
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,9 @@ 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');
|
|
142
145
|
} else if ('kugelaudio' === vendor) {
|
|
143
146
|
assert.ok(voice, 'synthAudio requires voice when kugelaudio is used');
|
|
144
147
|
assert.ok(credentials.api_key, 'synthAudio requires api_key when kugelaudio is used');
|
|
@@ -242,6 +245,11 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
|
|
|
242
245
|
credentials, options, stats, language, voice, key, text, renderForCaching, disableTtsStreaming,
|
|
243
246
|
disableTtsCache});
|
|
244
247
|
break;
|
|
248
|
+
case 'speechify':
|
|
249
|
+
audioData = await synthSpeechify(logger, {
|
|
250
|
+
credentials, options, stats, language, voice, key, text, renderForCaching, disableTtsStreaming,
|
|
251
|
+
disableTtsCache});
|
|
252
|
+
break;
|
|
245
253
|
case 'nineninesix':
|
|
246
254
|
audioData = await synthNineninesix(logger, {
|
|
247
255
|
credentials, stats, language, voice, key, text, renderForCaching, disableTtsStreaming,
|
|
@@ -1602,6 +1610,84 @@ const synthKugelaudio = async(logger, {
|
|
|
1602
1610
|
}
|
|
1603
1611
|
};
|
|
1604
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
|
+
|
|
1605
1691
|
/* nineninesix.ai — a Cartesia-compatible API, but only raw/wav come back
|
|
1606
1692
|
(mp3 is rejected), so the cache render asks for wav rather than mp3. */
|
|
1607
1693
|
/* fish.audio — msgpack websocket for streaming, and a POST endpoint for the cache
|
package/package.json
CHANGED
package/test/synth.js
CHANGED
|
@@ -1120,6 +1120,35 @@ test('kugelaudio speech synth tests', async(t) => {
|
|
|
1120
1120
|
client.quit();
|
|
1121
1121
|
});
|
|
1122
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
|
+
|
|
1123
1152
|
test('fishaudio speech synth tests', async(t) => {
|
|
1124
1153
|
const fn = require('..');
|
|
1125
1154
|
const {synthAudio, client} = fn(opts, logger);
|