@jambonz/speech-utils 1.0.7 → 1.0.8
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 +74 -1
- package/package.json +1 -1
- package/test/synth.js +35 -0
package/lib/synth-audio.js
CHANGED
|
@@ -80,7 +80,7 @@ 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'].includes(vendor) ||
|
|
83
|
+
'whisper', 'deepgram', 'rimelabs', 'cartesia', 'inworld', 'resemble', 'murf', 'xai'].includes(vendor) ||
|
|
84
84
|
vendor.startsWith('custom'),
|
|
85
85
|
`synthAudio supported vendors are google, aws, microsoft, nvidia and wellsaid ..etc, not ${vendor}`);
|
|
86
86
|
if ('google' === vendor) {
|
|
@@ -124,6 +124,8 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
|
|
|
124
124
|
if (!credentials.deepgram_tts_uri) {
|
|
125
125
|
assert.ok(credentials.api_key, 'synthAudio requires api_key when deepgram is used');
|
|
126
126
|
}
|
|
127
|
+
} else if ('xai' === vendor) {
|
|
128
|
+
assert.ok(credentials.api_key, 'synthAudio requires api_key when xai is used');
|
|
127
129
|
} else if ('cartesia' === vendor) {
|
|
128
130
|
assert.ok(credentials.api_key, 'synthAudio requires api_key when cartesia is used');
|
|
129
131
|
assert.ok(credentials.model_id, 'synthAudio requires model_id when cartesia is used');
|
|
@@ -228,6 +230,11 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
|
|
|
228
230
|
audioData = await synthDeepgram(logger, {credentials, stats, model, key, text,
|
|
229
231
|
renderForCaching, disableTtsStreaming, disableTtsCache});
|
|
230
232
|
break;
|
|
233
|
+
case 'xai':
|
|
234
|
+
audioData = await synthXai(logger, {
|
|
235
|
+
credentials, options, stats, language, voice, key, text, renderForCaching, disableTtsStreaming,
|
|
236
|
+
disableTtsCache});
|
|
237
|
+
break;
|
|
231
238
|
case 'resemble':
|
|
232
239
|
audioData = await synthResemble(logger, {
|
|
233
240
|
credentials, stats, voice, key, text, options, renderForCaching, disableTtsStreaming, disableTtsCache});
|
|
@@ -1133,6 +1140,72 @@ const synthDeepgram = async(logger, {credentials, stats, model, key, text, rende
|
|
|
1133
1140
|
}
|
|
1134
1141
|
};
|
|
1135
1142
|
|
|
1143
|
+
const synthXai = async(logger, {
|
|
1144
|
+
credentials, options, stats, language, voice, key, text, renderForCaching, disableTtsStreaming, disableTtsCache
|
|
1145
|
+
}) => {
|
|
1146
|
+
const {api_key, api_uri, options: credOpts} = credentials;
|
|
1147
|
+
const opts = !!options && Object.keys(options).length !== 0 ? options : JSON.parse(credOpts || '{}');
|
|
1148
|
+
const speed = opts.speed;
|
|
1149
|
+
if (!JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
|
|
1150
|
+
let params = '{';
|
|
1151
|
+
params += `api_key=${api_key}`;
|
|
1152
|
+
params += `,playback_id=${key}`;
|
|
1153
|
+
params += ',vendor=xai';
|
|
1154
|
+
if (voice) params += `,voice=${voice}`;
|
|
1155
|
+
if (language) params += `,language=${language}`;
|
|
1156
|
+
if (speed !== null && speed !== undefined) params += `,speed=${speed}`;
|
|
1157
|
+
if (opts.optimize_streaming_latency != null) {
|
|
1158
|
+
params += `,optimize_streaming_latency=${opts.optimize_streaming_latency}`;
|
|
1159
|
+
}
|
|
1160
|
+
if (opts.text_normalization != null) params += `,text_normalization=${opts.text_normalization}`;
|
|
1161
|
+
params += `,write_cache_file=${disableTtsCache ? 0 : 1}`;
|
|
1162
|
+
if (api_uri) params += `,endpoint=${api_uri}`;
|
|
1163
|
+
params += '}';
|
|
1164
|
+
|
|
1165
|
+
return {
|
|
1166
|
+
filePath: `say:${params}${text.replace(/\n/g, ' ').replace(/\r/g, ' ')}`,
|
|
1167
|
+
servedFromCache: false,
|
|
1168
|
+
rtt: 0
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1171
|
+
try {
|
|
1172
|
+
const post = bent(`https://${api_uri || 'api.x.ai'}`, 'POST', 'buffer', {
|
|
1173
|
+
'Authorization': `Bearer ${api_key}`,
|
|
1174
|
+
'Content-Type': 'application/json'
|
|
1175
|
+
});
|
|
1176
|
+
const audioContent = await post('/v1/tts', {
|
|
1177
|
+
text,
|
|
1178
|
+
language: language || 'auto',
|
|
1179
|
+
...(voice && {voice_id: voice}),
|
|
1180
|
+
...(speed !== null && speed !== undefined && {speed}),
|
|
1181
|
+
...(opts.optimize_streaming_latency != null && {optimize_streaming_latency: opts.optimize_streaming_latency}),
|
|
1182
|
+
...(opts.text_normalization != null && {text_normalization: opts.text_normalization}),
|
|
1183
|
+
output_format: {
|
|
1184
|
+
codec: 'wav',
|
|
1185
|
+
sample_rate: 8000
|
|
1186
|
+
}
|
|
1187
|
+
});
|
|
1188
|
+
return {
|
|
1189
|
+
audioContent,
|
|
1190
|
+
extension: 'wav',
|
|
1191
|
+
sampleRate: 8000
|
|
1192
|
+
};
|
|
1193
|
+
} catch (err) {
|
|
1194
|
+
// xAI errors are JSON {code, error} - read the body so the surfaced message isn't 'undefined'
|
|
1195
|
+
if (err.name === 'StatusError' && typeof err.text === 'function') {
|
|
1196
|
+
try {
|
|
1197
|
+
const body = await err.text();
|
|
1198
|
+
if (body) err.message = body;
|
|
1199
|
+
} catch (readErr) {
|
|
1200
|
+
logger.info({readErr}, 'synth xai: failed to read error response body');
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
logger.info({err}, 'synth xai returned error');
|
|
1204
|
+
stats.increment('tts.count', ['vendor:xai', 'accepted:no']);
|
|
1205
|
+
throw err;
|
|
1206
|
+
}
|
|
1207
|
+
};
|
|
1208
|
+
|
|
1136
1209
|
const synthCartesia = async(logger, {
|
|
1137
1210
|
credentials, options, stats, voice, language, key, text, renderForCaching, disableTtsStreaming, disableTtsCache
|
|
1138
1211
|
}) => {
|
package/package.json
CHANGED
package/test/synth.js
CHANGED
|
@@ -1118,6 +1118,41 @@ test('Deepgram speech synth tests', async(t) => {
|
|
|
1118
1118
|
client.quit();
|
|
1119
1119
|
})
|
|
1120
1120
|
|
|
1121
|
+
test('xai speech synth tests', async(t) => {
|
|
1122
|
+
const fn = require('..');
|
|
1123
|
+
const {synthAudio, client} = fn(opts, logger);
|
|
1124
|
+
|
|
1125
|
+
if (!process.env.XAI_API_KEY) {
|
|
1126
|
+
t.pass('skipping xai speech synth tests - no XAI_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: 'xai',
|
|
1133
|
+
credentials: {
|
|
1134
|
+
api_key: process.env.XAI_API_KEY,
|
|
1135
|
+
options: JSON.stringify({
|
|
1136
|
+
voice: process.env.XAI_VOICE || 'eve',
|
|
1137
|
+
speed: 1.0,
|
|
1138
|
+
optimize_streaming_latency: 1,
|
|
1139
|
+
text_normalization: true
|
|
1140
|
+
})
|
|
1141
|
+
},
|
|
1142
|
+
language: 'en',
|
|
1143
|
+
voice: process.env.XAI_VOICE || 'eve',
|
|
1144
|
+
text,
|
|
1145
|
+
renderForCaching: true
|
|
1146
|
+
});
|
|
1147
|
+
t.ok(!opts.servedFromCache && opts.filePath, `successfully synthesized xai audio to ${opts.filePath}`);
|
|
1148
|
+
|
|
1149
|
+
} catch (err) {
|
|
1150
|
+
console.error(JSON.stringify(err));
|
|
1151
|
+
t.end(err);
|
|
1152
|
+
}
|
|
1153
|
+
client.quit();
|
|
1154
|
+
});
|
|
1155
|
+
|
|
1121
1156
|
test('TTS Cache tests', async(t) => {
|
|
1122
1157
|
const fn = require('..');
|
|
1123
1158
|
const {purgeTtsCache, getTtsSize, client} = fn(opts, logger);
|