@jambonz/speech-utils 1.0.3 → 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'
@@ -1343,6 +1343,18 @@ const synthCartesia = async(logger, {
1343
1343
  try {
1344
1344
  const client = new CartesiaClient({ apiKey: api_key });
1345
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');
1346
1358
  const mp3Stream = await client.tts.bytes({
1347
1359
  modelId: model_id,
1348
1360
  transcript: text,
@@ -1358,16 +1370,16 @@ const synthCartesia = async(logger, {
1358
1370
  ),
1359
1371
  ...(model_id === 'sonic-2' && (opts.speed || opts.emotion) && {
1360
1372
  experimentalControls: {
1361
- ...(opts.speed !== null && opts.speed !== undefined && {speed: opts.speed}),
1373
+ ...(has(opts.speed) && {speed: opts.speed}),
1362
1374
  ...(opts.emotion && {emotion: [opts.emotion]}),
1363
1375
  }
1364
1376
  }),
1365
1377
  },
1366
- ...(model_id === 'sonic-3' && (opts.speed || opts.emotion || opts.volume) && {
1378
+ ...(isSonic3 && (has(opts.speed) || has(opts.emotion) || has(opts.volume)) && {
1367
1379
  generationConfig: {
1368
- ...(opts.volume !== null && opts.volume !== undefined && {volume: opts.volume}),
1369
- ...(opts.speed !== null && opts.speed !== undefined && {speed: opts.speed}),
1370
- ...(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}),
1371
1383
  }
1372
1384
  }),
1373
1385
  language: language,
@@ -1391,6 +1403,32 @@ const synthCartesia = async(logger, {
1391
1403
  sampleRate
1392
1404
  };
1393
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
+ }
1394
1432
  logger.info({err}, 'synth Cartesia returned error');
1395
1433
  stats.increment('tts.count', ['vendor:cartesia', 'accepted:no']);
1396
1434
  throw err;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jambonz/speech-utils",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "TTS-related speech utilities for jambonz",
5
5
  "main": "index.js",
6
6
  "author": "Dave Horton",