@jambonz/speech-utils 1.0.7 → 1.0.9
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 +128 -1
- package/package.json +1 -1
- package/test/synth.js +63 -0
package/lib/synth-audio.js
CHANGED
|
@@ -80,7 +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', 'rimelabs', 'cartesia', 'inworld', 'resemble', 'murf']
|
|
83
|
+
'whisper', 'deepgram', 'deepgramflux', 'rimelabs', 'cartesia', 'inworld', 'resemble', 'murf', 'xai']
|
|
84
|
+
.includes(vendor) ||
|
|
84
85
|
vendor.startsWith('custom'),
|
|
85
86
|
`synthAudio supported vendors are google, aws, microsoft, nvidia and wellsaid ..etc, not ${vendor}`);
|
|
86
87
|
if ('google' === vendor) {
|
|
@@ -124,6 +125,13 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
|
|
|
124
125
|
if (!credentials.deepgram_tts_uri) {
|
|
125
126
|
assert.ok(credentials.api_key, 'synthAudio requires api_key when deepgram is used');
|
|
126
127
|
}
|
|
128
|
+
} else if ('deepgramflux' === vendor) {
|
|
129
|
+
// Deepgram Flux TTS (/v2/speak); the flux model rides on `voice`/`model`
|
|
130
|
+
if (!credentials.deepgram_tts_uri) {
|
|
131
|
+
assert.ok(credentials.api_key, 'synthAudio requires api_key when deepgramflux is used');
|
|
132
|
+
}
|
|
133
|
+
} else if ('xai' === vendor) {
|
|
134
|
+
assert.ok(credentials.api_key, 'synthAudio requires api_key when xai is used');
|
|
127
135
|
} else if ('cartesia' === vendor) {
|
|
128
136
|
assert.ok(credentials.api_key, 'synthAudio requires api_key when cartesia is used');
|
|
129
137
|
assert.ok(credentials.model_id, 'synthAudio requires model_id when cartesia is used');
|
|
@@ -228,6 +236,15 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
|
|
|
228
236
|
audioData = await synthDeepgram(logger, {credentials, stats, model, key, text,
|
|
229
237
|
renderForCaching, disableTtsStreaming, disableTtsCache});
|
|
230
238
|
break;
|
|
239
|
+
case 'deepgramflux':
|
|
240
|
+
audioData = await synthDeepgramFlux(logger, {credentials, stats, model: model || voice, key, text,
|
|
241
|
+
renderForCaching, disableTtsStreaming, disableTtsCache});
|
|
242
|
+
break;
|
|
243
|
+
case 'xai':
|
|
244
|
+
audioData = await synthXai(logger, {
|
|
245
|
+
credentials, options, stats, language, voice, key, text, renderForCaching, disableTtsStreaming,
|
|
246
|
+
disableTtsCache});
|
|
247
|
+
break;
|
|
231
248
|
case 'resemble':
|
|
232
249
|
audioData = await synthResemble(logger, {
|
|
233
250
|
credentials, stats, voice, key, text, options, renderForCaching, disableTtsStreaming, disableTtsCache});
|
|
@@ -1133,6 +1150,116 @@ const synthDeepgram = async(logger, {credentials, stats, model, key, text, rende
|
|
|
1133
1150
|
}
|
|
1134
1151
|
};
|
|
1135
1152
|
|
|
1153
|
+
// Deepgram Flux TTS — the conversation-native model served from /v2/speak.
|
|
1154
|
+
// Streaming rides the mediajam deepgramflux dialect via a say: filePath; the
|
|
1155
|
+
// batch/cache path POSTs to /v2/speak (mp3 is batch-only for Flux).
|
|
1156
|
+
const synthDeepgramFlux = async(logger, {credentials, stats, model, key, text, renderForCaching,
|
|
1157
|
+
disableTtsStreaming, disableTtsCache}) => {
|
|
1158
|
+
const {api_key, deepgram_tts_uri} = credentials;
|
|
1159
|
+
if (!JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
|
|
1160
|
+
let params = '{';
|
|
1161
|
+
params += `api_key=${api_key}`;
|
|
1162
|
+
params += `,playback_id=${key}`;
|
|
1163
|
+
params += ',vendor=deepgramflux';
|
|
1164
|
+
params += `,voice=${model}`;
|
|
1165
|
+
params += `,write_cache_file=${disableTtsCache ? 0 : 1}`;
|
|
1166
|
+
if (deepgram_tts_uri) params += `,endpoint=${deepgram_tts_uri}`;
|
|
1167
|
+
params += '}';
|
|
1168
|
+
|
|
1169
|
+
return {
|
|
1170
|
+
filePath: `say:${params}${text.replace(/\n/g, ' ')}`,
|
|
1171
|
+
servedFromCache: false,
|
|
1172
|
+
rtt: 0
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
try {
|
|
1176
|
+
const post = bent(deepgram_tts_uri || 'https://api.deepgram.com', 'POST', 'buffer', {
|
|
1177
|
+
// on-premise deepgram does not require to have api_key
|
|
1178
|
+
...(api_key && {'Authorization': `Token ${api_key}`}),
|
|
1179
|
+
'Accept': 'audio/mpeg',
|
|
1180
|
+
'Content-Type': 'application/json'
|
|
1181
|
+
});
|
|
1182
|
+
const audioContent = await post(`/v2/speak?model=${model}`, {
|
|
1183
|
+
text
|
|
1184
|
+
});
|
|
1185
|
+
return {
|
|
1186
|
+
audioContent,
|
|
1187
|
+
extension: 'mp3',
|
|
1188
|
+
sampleRate: 8000
|
|
1189
|
+
};
|
|
1190
|
+
} catch (err) {
|
|
1191
|
+
logger.info({err}, 'synth Deepgram Flux returned error');
|
|
1192
|
+
stats.increment('tts.count', ['vendor:deepgramflux', 'accepted:no']);
|
|
1193
|
+
throw err;
|
|
1194
|
+
}
|
|
1195
|
+
};
|
|
1196
|
+
|
|
1197
|
+
const synthXai = async(logger, {
|
|
1198
|
+
credentials, options, stats, language, voice, key, text, renderForCaching, disableTtsStreaming, disableTtsCache
|
|
1199
|
+
}) => {
|
|
1200
|
+
const {api_key, api_uri, options: credOpts} = credentials;
|
|
1201
|
+
const opts = !!options && Object.keys(options).length !== 0 ? options : JSON.parse(credOpts || '{}');
|
|
1202
|
+
const speed = opts.speed;
|
|
1203
|
+
if (!JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
|
|
1204
|
+
let params = '{';
|
|
1205
|
+
params += `api_key=${api_key}`;
|
|
1206
|
+
params += `,playback_id=${key}`;
|
|
1207
|
+
params += ',vendor=xai';
|
|
1208
|
+
if (voice) params += `,voice=${voice}`;
|
|
1209
|
+
if (language) params += `,language=${language}`;
|
|
1210
|
+
if (speed !== null && speed !== undefined) params += `,speed=${speed}`;
|
|
1211
|
+
if (opts.optimize_streaming_latency != null) {
|
|
1212
|
+
params += `,optimize_streaming_latency=${opts.optimize_streaming_latency}`;
|
|
1213
|
+
}
|
|
1214
|
+
if (opts.text_normalization != null) params += `,text_normalization=${opts.text_normalization}`;
|
|
1215
|
+
params += `,write_cache_file=${disableTtsCache ? 0 : 1}`;
|
|
1216
|
+
if (api_uri) params += `,endpoint=${api_uri}`;
|
|
1217
|
+
params += '}';
|
|
1218
|
+
|
|
1219
|
+
return {
|
|
1220
|
+
filePath: `say:${params}${text.replace(/\n/g, ' ').replace(/\r/g, ' ')}`,
|
|
1221
|
+
servedFromCache: false,
|
|
1222
|
+
rtt: 0
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
try {
|
|
1226
|
+
const post = bent(`https://${api_uri || 'api.x.ai'}`, 'POST', 'buffer', {
|
|
1227
|
+
'Authorization': `Bearer ${api_key}`,
|
|
1228
|
+
'Content-Type': 'application/json'
|
|
1229
|
+
});
|
|
1230
|
+
const audioContent = await post('/v1/tts', {
|
|
1231
|
+
text,
|
|
1232
|
+
language: language || 'auto',
|
|
1233
|
+
...(voice && {voice_id: voice}),
|
|
1234
|
+
...(speed !== null && speed !== undefined && {speed}),
|
|
1235
|
+
...(opts.optimize_streaming_latency != null && {optimize_streaming_latency: opts.optimize_streaming_latency}),
|
|
1236
|
+
...(opts.text_normalization != null && {text_normalization: opts.text_normalization}),
|
|
1237
|
+
output_format: {
|
|
1238
|
+
codec: 'wav',
|
|
1239
|
+
sample_rate: 8000
|
|
1240
|
+
}
|
|
1241
|
+
});
|
|
1242
|
+
return {
|
|
1243
|
+
audioContent,
|
|
1244
|
+
extension: 'wav',
|
|
1245
|
+
sampleRate: 8000
|
|
1246
|
+
};
|
|
1247
|
+
} catch (err) {
|
|
1248
|
+
// xAI errors are JSON {code, error} - read the body so the surfaced message isn't 'undefined'
|
|
1249
|
+
if (err.name === 'StatusError' && typeof err.text === 'function') {
|
|
1250
|
+
try {
|
|
1251
|
+
const body = await err.text();
|
|
1252
|
+
if (body) err.message = body;
|
|
1253
|
+
} catch (readErr) {
|
|
1254
|
+
logger.info({readErr}, 'synth xai: failed to read error response body');
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
logger.info({err}, 'synth xai returned error');
|
|
1258
|
+
stats.increment('tts.count', ['vendor:xai', 'accepted:no']);
|
|
1259
|
+
throw err;
|
|
1260
|
+
}
|
|
1261
|
+
};
|
|
1262
|
+
|
|
1136
1263
|
const synthCartesia = async(logger, {
|
|
1137
1264
|
credentials, options, stats, voice, language, key, text, renderForCaching, disableTtsStreaming, disableTtsCache
|
|
1138
1265
|
}) => {
|
package/package.json
CHANGED
package/test/synth.js
CHANGED
|
@@ -1118,6 +1118,69 @@ test('Deepgram speech synth tests', async(t) => {
|
|
|
1118
1118
|
client.quit();
|
|
1119
1119
|
})
|
|
1120
1120
|
|
|
1121
|
+
test('Deepgram Flux speech synth tests', async(t) => {
|
|
1122
|
+
const fn = require('..');
|
|
1123
|
+
const {synthAudio, client} = fn(opts, logger);
|
|
1124
|
+
|
|
1125
|
+
if (!process.env.DEEPGRAM_API_KEY) {
|
|
1126
|
+
t.pass('skipping Deepgram Flux speech synth tests since DEEPGRAM_API_KEY');
|
|
1127
|
+
return t.end();
|
|
1128
|
+
}
|
|
1129
|
+
const text = 'Hi there and welcome to jambones!';
|
|
1130
|
+
try {
|
|
1131
|
+
const opts = await synthAudio(stats, {
|
|
1132
|
+
vendor: 'deepgramflux',
|
|
1133
|
+
credentials: {
|
|
1134
|
+
api_key: process.env.DEEPGRAM_API_KEY
|
|
1135
|
+
},
|
|
1136
|
+
model: process.env.DEEPGRAM_FLUX_MODEL || 'flux-alexis-en',
|
|
1137
|
+
text,
|
|
1138
|
+
renderForCaching: true
|
|
1139
|
+
});
|
|
1140
|
+
t.ok(!opts.servedFromCache, `successfully synthesized deepgramflux audio to ${opts.filePath}`);
|
|
1141
|
+
|
|
1142
|
+
} catch (err) {
|
|
1143
|
+
console.error(JSON.stringify(err));
|
|
1144
|
+
t.end(err);
|
|
1145
|
+
}
|
|
1146
|
+
client.quit();
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
test('xai speech synth tests', async(t) => {
|
|
1150
|
+
const fn = require('..');
|
|
1151
|
+
const {synthAudio, client} = fn(opts, logger);
|
|
1152
|
+
|
|
1153
|
+
if (!process.env.XAI_API_KEY) {
|
|
1154
|
+
t.pass('skipping xai speech synth tests - no XAI_API_KEY');
|
|
1155
|
+
return t.end();
|
|
1156
|
+
}
|
|
1157
|
+
const text = 'Hi there and welcome to jambones!';
|
|
1158
|
+
try {
|
|
1159
|
+
const opts = await synthAudio(stats, {
|
|
1160
|
+
vendor: 'xai',
|
|
1161
|
+
credentials: {
|
|
1162
|
+
api_key: process.env.XAI_API_KEY,
|
|
1163
|
+
options: JSON.stringify({
|
|
1164
|
+
voice: process.env.XAI_VOICE || 'eve',
|
|
1165
|
+
speed: 1.0,
|
|
1166
|
+
optimize_streaming_latency: 1,
|
|
1167
|
+
text_normalization: true
|
|
1168
|
+
})
|
|
1169
|
+
},
|
|
1170
|
+
language: 'en',
|
|
1171
|
+
voice: process.env.XAI_VOICE || 'eve',
|
|
1172
|
+
text,
|
|
1173
|
+
renderForCaching: true
|
|
1174
|
+
});
|
|
1175
|
+
t.ok(!opts.servedFromCache && opts.filePath, `successfully synthesized xai audio to ${opts.filePath}`);
|
|
1176
|
+
|
|
1177
|
+
} catch (err) {
|
|
1178
|
+
console.error(JSON.stringify(err));
|
|
1179
|
+
t.end(err);
|
|
1180
|
+
}
|
|
1181
|
+
client.quit();
|
|
1182
|
+
});
|
|
1183
|
+
|
|
1121
1184
|
test('TTS Cache tests', async(t) => {
|
|
1122
1185
|
const fn = require('..');
|
|
1123
1186
|
const {purgeTtsCache, getTtsSize, client} = fn(opts, logger);
|