@livekit/agents-plugin-cartesia 1.4.4 → 1.4.5

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/src/tts.ts CHANGED
@@ -4,6 +4,8 @@
4
4
  import {
5
5
  type APIConnectOptions,
6
6
  APIConnectionError,
7
+ APIError,
8
+ APIStatusError,
7
9
  APITimeoutError,
8
10
  AudioByteStream,
9
11
  Future,
@@ -36,6 +38,7 @@ import {
36
38
  isChunkMessage,
37
39
  isDoneMessage,
38
40
  isErrorMessage,
41
+ isFlushDoneMessage,
39
42
  } from './types.js';
40
43
 
41
44
  const AUTHORIZATION_HEADER = 'X-API-Key';
@@ -433,34 +436,24 @@ export class SynthesizeStream extends tts.SynthesizeStream {
433
436
  continue;
434
437
  }
435
438
 
436
- // Handle error messages
437
- if (isErrorMessage(serverMsg)) {
438
- this.#logger.error({ error: serverMsg.error }, 'Cartesia returned error');
439
- continue;
440
- }
441
-
442
439
  const segmentId = serverMsg.context_id;
443
440
 
444
- // Process word timestamps if present (typed via Zod schema)
445
- if (this.#opts.wordTimestamps !== false && hasWordTimestamps(serverMsg)) {
446
- const wordTimestamps = serverMsg.word_timestamps;
447
- for (let i = 0; i < wordTimestamps.words.length; i++) {
448
- const word = wordTimestamps.words[i];
449
- const startTime = wordTimestamps.start[i];
450
- const endTime = wordTimestamps.end[i];
451
- if (word !== undefined && startTime !== undefined && endTime !== undefined) {
452
- pendingTimedTranscripts.push(
453
- createTimedString({
454
- text: word + ' ', // Add space after word for consistency
455
- startTime,
456
- endTime,
457
- }),
458
- );
459
- }
441
+ // Handle error frames first. 4xx (e.g. empty-transcript on
442
+ // function-call turns) is non-fatal log and fall through so an
443
+ // accompanying done:true still triggers the unified close path
444
+ // below. 5xx bubbles up so the base SynthesizeStream can retry.
445
+ if (isErrorMessage(serverMsg)) {
446
+ if (serverMsg.status_code >= 400 && serverMsg.status_code < 500) {
447
+ this.#logger.debug({ error: serverMsg.error }, 'Cartesia sent a non-fatal error');
448
+ } else {
449
+ this.#logger.error({ error: serverMsg.error }, 'Cartesia returned error');
450
+ throw new APIStatusError({
451
+ message: `Cartesia returned error: ${serverMsg.error}`,
452
+ options: { statusCode: serverMsg.status_code, retryable: true },
453
+ });
460
454
  }
461
455
  }
462
456
 
463
- // Handle audio chunk messages
464
457
  if (isChunkMessage(serverMsg)) {
465
458
  const audioBuffer = Buffer.from(serverMsg.data, 'base64');
466
459
  // Extract ArrayBuffer from Buffer for AudioByteStream compatibility
@@ -484,7 +477,23 @@ export class SynthesizeStream extends tts.SynthesizeStream {
484
477
  );
485
478
  ws.close();
486
479
  }, this.#opts.chunkTimeout);
487
- } else if (isDoneMessage(serverMsg)) {
480
+ } else if (this.#opts.wordTimestamps !== false && hasWordTimestamps(serverMsg)) {
481
+ const wordTimestamps = serverMsg.word_timestamps;
482
+ for (let i = 0; i < wordTimestamps.words.length; i++) {
483
+ const word = wordTimestamps.words[i];
484
+ const startTime = wordTimestamps.start[i];
485
+ const endTime = wordTimestamps.end[i];
486
+ if (word !== undefined && startTime !== undefined && endTime !== undefined) {
487
+ pendingTimedTranscripts.push(
488
+ createTimedString({
489
+ text: word + ' ', // Add space after word for consistency
490
+ startTime,
491
+ endTime,
492
+ }),
493
+ );
494
+ }
495
+ }
496
+ } else if (isDoneMessage(serverMsg) || (isErrorMessage(serverMsg) && serverMsg.done)) {
488
497
  // This ensures all sentences have been sent before closing
489
498
  if (sentenceStreamClosed) {
490
499
  for (const frame of bstream.flush()) {
@@ -504,9 +513,16 @@ export class SynthesizeStream extends tts.SynthesizeStream {
504
513
  }
505
514
  }
506
515
  // If sentenceStreamClosed is false, continue receiving - more done messages will come
516
+ } else if (!isFlushDoneMessage(serverMsg) && !isErrorMessage(serverMsg)) {
517
+ // flush_done is an ack with nothing to do; error frames without
518
+ // done:true were already logged above.
519
+ this.#logger.warn({ message: serverMsg }, 'Unknown Cartesia message');
507
520
  }
508
521
  }
509
522
  } catch (err) {
523
+ // Always propagate API errors so the base SynthesizeStream can retry
524
+ // and emit tts_error once retries are exhausted.
525
+ if (err instanceof APIError) throw err;
510
526
  // skip log error for normal websocket close
511
527
  if (err instanceof Error && !err.message.includes('WebSocket closed')) {
512
528
  if (
@@ -549,6 +565,7 @@ export class SynthesizeStream extends tts.SynthesizeStream {
549
565
  if (this.abortSignal.aborted) {
550
566
  return;
551
567
  }
568
+ if (e instanceof APIError) throw e;
552
569
  throw toRetryableConnectionError(e);
553
570
  } finally {
554
571
  // Ensure we don't leak sockets/tasks across retry attempts.