@jambonz/speech-utils 1.0.1 → 1.0.4

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.
@@ -12,8 +12,8 @@ jobs:
12
12
  runs-on: ubuntu-latest
13
13
 
14
14
  steps:
15
- - uses: actions/checkout@v3
16
- - uses: actions/setup-node@v3
15
+ - uses: actions/checkout@v4
16
+ - uses: actions/setup-node@v4
17
17
  with:
18
18
  node-version: lts/*
19
19
  registry-url: 'https://registry.npmjs.org'
@@ -1136,6 +1136,8 @@ const synthRimelabs = async(logger, {
1136
1136
  if (opts.repetition_penalty) params += `,repetition_penalty=${opts.repetition_penalty}`;
1137
1137
  if (opts.top_p) params += `,top_p=${opts.top_p}`;
1138
1138
  if (opts.max_tokens) params += `,max_tokens=${opts.max_tokens}`;
1139
+ // Coda model parameters
1140
+ if (opts.timeScaleFactor) params += `,time_scale_factor=${opts.timeScaleFactor}`;
1139
1141
  params += '}';
1140
1142
 
1141
1143
  return {
@@ -1341,6 +1343,18 @@ const synthCartesia = async(logger, {
1341
1343
  try {
1342
1344
  const client = new CartesiaClient({ apiKey: api_key });
1343
1345
  const sampleRate = 48000;
1346
+
1347
+ // omit a control unless explicitly provided (0 is a valid value, so test for nullish only)
1348
+ const has = (v) => v !== null && v !== undefined;
1349
+ /* Voice controls are model-family specific:
1350
+ - sonic-2 takes `experimentalControls` (emotion is an array, no volume).
1351
+ - sonic-3 family (sonic-3, sonic-3.5, pinned sonic-3.x snapshots) takes
1352
+ `generationConfig` (emotion is a string, volume supported). Match the same
1353
+ "starts with sonic-3" predicate the freeswitch streaming module uses
1354
+ (mod_cartesia_tts_streaming: strncmp(model_id, "sonic-3", ...)), so cached
1355
+ and streamed synthesis behave identically.
1356
+ Older models (sonic, sonic-english, sonic-multilingual, sonic-2024-*) take neither. */
1357
+ const isSonic3 = model_id?.startsWith('sonic-3');
1344
1358
  const mp3Stream = await client.tts.bytes({
1345
1359
  modelId: model_id,
1346
1360
  transcript: text,
@@ -1356,16 +1370,16 @@ const synthCartesia = async(logger, {
1356
1370
  ),
1357
1371
  ...(model_id === 'sonic-2' && (opts.speed || opts.emotion) && {
1358
1372
  experimentalControls: {
1359
- ...(opts.speed !== null && opts.speed !== undefined && {speed: opts.speed}),
1373
+ ...(has(opts.speed) && {speed: opts.speed}),
1360
1374
  ...(opts.emotion && {emotion: [opts.emotion]}),
1361
1375
  }
1362
1376
  }),
1363
1377
  },
1364
- ...(model_id === 'sonic-3' && (opts.speed || opts.emotion || opts.volume) && {
1378
+ ...(isSonic3 && (has(opts.speed) || has(opts.emotion) || has(opts.volume)) && {
1365
1379
  generationConfig: {
1366
- ...(opts.volume !== null && opts.volume !== undefined && {volume: opts.volume}),
1367
- ...(opts.speed !== null && opts.speed !== undefined && {speed: opts.speed}),
1368
- ...(opts.emotion !== null && opts.emotion !== undefined && {emotion: opts.emotion}),
1380
+ ...(has(opts.volume) && {volume: opts.volume}),
1381
+ ...(has(opts.speed) && {speed: opts.speed}),
1382
+ ...(has(opts.emotion) && {emotion: opts.emotion}),
1369
1383
  }
1370
1384
  }),
1371
1385
  language: language,
@@ -1389,6 +1403,32 @@ const synthCartesia = async(logger, {
1389
1403
  sampleRate
1390
1404
  };
1391
1405
  } catch (err) {
1406
+ /* Cartesia's tts.bytes() uses a streaming response, so on an HTTP error the SDK
1407
+ throws a CartesiaError whose `body` is an unconsumed stream-wrapper object
1408
+ (async-iterable, yielding Uint8Array chunks) rather than the parsed error
1409
+ JSON. Read it so callers get a meaningful message instead of a serialized
1410
+ stream object. */
1411
+ if (err && err.body && typeof err.body !== 'string' && typeof err.body[Symbol.asyncIterator] === 'function') {
1412
+ try {
1413
+ const chunks = [];
1414
+ for await (const chunk of err.body) {
1415
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
1416
+ }
1417
+ const text = Buffer.concat(chunks).toString('utf8');
1418
+ if (text) {
1419
+ let parsed;
1420
+ try {
1421
+ parsed = JSON.parse(text);
1422
+ } catch {
1423
+ parsed = null;
1424
+ }
1425
+ err.message = (parsed && (parsed.error || parsed.message)) || text;
1426
+ err.body = parsed || text;
1427
+ }
1428
+ } catch (readErr) {
1429
+ logger.info({readErr}, 'synth Cartesia: failed to read error response body');
1430
+ }
1431
+ }
1392
1432
  logger.info({err}, 'synth Cartesia returned error');
1393
1433
  stats.increment('tts.count', ['vendor:cartesia', 'accepted:no']);
1394
1434
  throw err;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jambonz/speech-utils",
3
- "version": "1.0.1",
3
+ "version": "1.0.4",
4
4
  "description": "TTS-related speech utilities for jambonz",
5
5
  "main": "index.js",
6
6
  "author": "Dave Horton",