@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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jambonz/speech-utils",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "description": "TTS-related speech utilities for jambonz",
5
5
  "main": "index.js",
6
6
  "author": "Dave Horton",
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);