@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.
@@ -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'].includes(vendor) ||
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jambonz/speech-utils",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
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,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);