@jambonz/speech-utils 1.0.6 → 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 +148 -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'].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,9 +124,14 @@ 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');
|
|
132
|
+
} else if ('murf' === vendor) {
|
|
133
|
+
assert.ok(voice, 'synthAudio requires voice when murf is used');
|
|
134
|
+
assert.ok(credentials.api_key, 'synthAudio requires api_key when murf is used');
|
|
130
135
|
} else if (vendor === 'resemble') {
|
|
131
136
|
assert.ok(voice, 'synthAudio requires voice when resemble is used');
|
|
132
137
|
assert.ok(credentials.api_key, 'synthAudio requires api_key when resemble is used');
|
|
@@ -211,6 +216,11 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
|
|
|
211
216
|
credentials, options, stats, language, voice, key, text, renderForCaching, disableTtsStreaming,
|
|
212
217
|
disableTtsCache});
|
|
213
218
|
break;
|
|
219
|
+
case 'murf':
|
|
220
|
+
audioData = await synthMurf(logger, {
|
|
221
|
+
credentials, options, stats, language, voice, key, text, renderForCaching, disableTtsStreaming,
|
|
222
|
+
disableTtsCache});
|
|
223
|
+
break;
|
|
214
224
|
case 'whisper':
|
|
215
225
|
audioData = await synthWhisper(logger, {
|
|
216
226
|
credentials, stats, voice, key, text, instructions, renderForCaching, disableTtsStreaming,
|
|
@@ -220,6 +230,11 @@ async function synthAudio(client, createHash, retrieveHash, logger, stats, { acc
|
|
|
220
230
|
audioData = await synthDeepgram(logger, {credentials, stats, model, key, text,
|
|
221
231
|
renderForCaching, disableTtsStreaming, disableTtsCache});
|
|
222
232
|
break;
|
|
233
|
+
case 'xai':
|
|
234
|
+
audioData = await synthXai(logger, {
|
|
235
|
+
credentials, options, stats, language, voice, key, text, renderForCaching, disableTtsStreaming,
|
|
236
|
+
disableTtsCache});
|
|
237
|
+
break;
|
|
223
238
|
case 'resemble':
|
|
224
239
|
audioData = await synthResemble(logger, {
|
|
225
240
|
credentials, stats, voice, key, text, options, renderForCaching, disableTtsStreaming, disableTtsCache});
|
|
@@ -969,6 +984,72 @@ const synthRimelabs = async(logger, {
|
|
|
969
984
|
throw err;
|
|
970
985
|
}
|
|
971
986
|
};
|
|
987
|
+
|
|
988
|
+
const synthMurf = async(logger, {
|
|
989
|
+
credentials, options, stats, language, voice, key, text, renderForCaching, disableTtsStreaming, disableTtsCache
|
|
990
|
+
}) => {
|
|
991
|
+
const {api_key, model_id, api_uri, options: credOpts} = credentials;
|
|
992
|
+
const opts = !!options && Object.keys(options).length !== 0 ? options : JSON.parse(credOpts || '{}');
|
|
993
|
+
|
|
994
|
+
/* default to using the streaming interface, unless disabled by env var OR we want just a cache file */
|
|
995
|
+
if (!JAMBONES_DISABLE_TTS_STREAMING && !renderForCaching && !disableTtsStreaming) {
|
|
996
|
+
/* param keys here must match mod_murf_tts's text_param handler */
|
|
997
|
+
let params = '{';
|
|
998
|
+
params += `api_key=${api_key}`;
|
|
999
|
+
params += `,playback_id=${key}`;
|
|
1000
|
+
params += ',vendor=murf';
|
|
1001
|
+
params += `,voice=${voice}`;
|
|
1002
|
+
if (model_id) params += `,model_id=${model_id}`;
|
|
1003
|
+
if (language) params += `,language=${language}`;
|
|
1004
|
+
if (api_uri) params += `,api_uri=${api_uri}`;
|
|
1005
|
+
if (opts.style) params += `,style=${opts.style}`;
|
|
1006
|
+
if (opts.rate !== undefined && opts.rate !== null) params += `,rate=${opts.rate}`;
|
|
1007
|
+
if (opts.pitch !== undefined && opts.pitch !== null) params += `,pitch=${opts.pitch}`;
|
|
1008
|
+
if (opts.variation !== undefined && opts.variation !== null) params += `,variation=${opts.variation}`;
|
|
1009
|
+
params += `,write_cache_file=${disableTtsCache ? 0 : 1}`;
|
|
1010
|
+
params += '}';
|
|
1011
|
+
|
|
1012
|
+
return {
|
|
1013
|
+
filePath: `say:${params}${text.replace(/\n/g, ' ').replace(/\r/g, ' ')}`,
|
|
1014
|
+
servedFromCache: false,
|
|
1015
|
+
rtt: 0
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
try {
|
|
1020
|
+
const sampleRate = 8000;
|
|
1021
|
+
/* no Accept header: murf returns 406 if it doesn't match; the response
|
|
1022
|
+
container is selected by the `format` field in the body instead */
|
|
1023
|
+
const post = bent(api_uri || 'https://global.api.murf.ai', 'POST', 'buffer', {
|
|
1024
|
+
'api-key': api_key,
|
|
1025
|
+
'Content-Type': 'application/json'
|
|
1026
|
+
});
|
|
1027
|
+
/* murf REST schema is documented loosely; field names follow the SDK params
|
|
1028
|
+
(voice_id/model/format/sample_rate) plus the websocket voice fields. */
|
|
1029
|
+
const audioContent = await post('/v1/speech/stream', {
|
|
1030
|
+
text,
|
|
1031
|
+
voice_id: voice,
|
|
1032
|
+
...(model_id && {model: model_id}),
|
|
1033
|
+
...(language && {locale: language}),
|
|
1034
|
+
...(opts.style && {style: opts.style}),
|
|
1035
|
+
...(opts.rate !== undefined && opts.rate !== null && {rate: opts.rate}),
|
|
1036
|
+
...(opts.pitch !== undefined && opts.pitch !== null && {pitch: opts.pitch}),
|
|
1037
|
+
...(opts.variation !== undefined && opts.variation !== null && {variation: opts.variation}),
|
|
1038
|
+
format: 'WAV',
|
|
1039
|
+
sample_rate: sampleRate,
|
|
1040
|
+
channel_type: 'MONO'
|
|
1041
|
+
});
|
|
1042
|
+
return {
|
|
1043
|
+
audioContent,
|
|
1044
|
+
extension: 'wav',
|
|
1045
|
+
sampleRate
|
|
1046
|
+
};
|
|
1047
|
+
} catch (err) {
|
|
1048
|
+
logger.info({err}, 'synth murf returned error');
|
|
1049
|
+
stats.increment('tts.count', ['vendor:murf', 'accepted:no']);
|
|
1050
|
+
throw err;
|
|
1051
|
+
}
|
|
1052
|
+
};
|
|
972
1053
|
const synthWhisper = async(logger, {credentials, stats, voice, key, text, instructions,
|
|
973
1054
|
renderForCaching, disableTtsStreaming, disableTtsCache}) => {
|
|
974
1055
|
const {api_key, model_id, baseURL, timeout, speed} = credentials;
|
|
@@ -1059,6 +1140,72 @@ const synthDeepgram = async(logger, {credentials, stats, model, key, text, rende
|
|
|
1059
1140
|
}
|
|
1060
1141
|
};
|
|
1061
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
|
+
|
|
1062
1209
|
const synthCartesia = async(logger, {
|
|
1063
1210
|
credentials, options, stats, voice, language, key, text, renderForCaching, disableTtsStreaming, disableTtsCache
|
|
1064
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);
|