pipedog is an unofficial community port of Pipecat, not affiliated with Daily.
pipedog
Speech-to-text

Cartesia

Streaming speech-to-text with CartesiaSTTService (Ink) and CartesiaTurnsSTTService (Ink-2, with server-side turn detection and eager end of turn).

Overview

Cartesia streams transcripts over a WebSocket. pipedog ports both of Pipecat's Cartesia speech-to-text services:

  • CartesiaSTTService: Cartesia's live STT API (/stt/websocket), with interim and final transcripts, runtime model and language switching, and keyterms on ink-2 models.
  • CartesiaTurnsSTTService: Cartesia's turn-based Streaming ASR v2 API (/stt/turns/websocket, Ink-2). The server decides when the user's turn starts and ends, with optional eager end-of-turn predictions.

Installation

bun add @pipedog/cartesia
import { CartesiaSTTService } from "@pipedog/cartesia/stt";
import { CartesiaTurnsSTTService } from "@pipedog/cartesia/turns/stt";

Prerequisites

  • A Cartesia account and an API key from the Cartesia playground.
  • Pass the key as apiKey. The examples read it from CARTESIA_API_KEY.

CartesiaSTTService

Configuration

Prop

Type

Settings

Runtime-updatable settings, passed as settings: new CartesiaSTTService.Settings({...}). Any setting you leave out keeps the default below.

SettingTypeDefaultDescription
modelstring"ink-whisper"Cartesia STT model.
languageLanguageLanguage.ENTranscription language.
keytermstring[] | nullnullTerms to bias transcription towards. Only ink-2 and ink-preview models use them.

Usage

stt.ts
import { CartesiaSTTService } from "@pipedog/cartesia/stt";
import { STTUpdateSettingsFrame } from "@pipedog/core/frames/frames";
import { Language } from "@pipedog/core/transcriptions/language";

const stt = new CartesiaSTTService({
  apiKey: process.env.CARTESIA_API_KEY ?? "",
  settings: new CartesiaSTTService.Settings({
    model: "ink-whisper",
    language: Language.EN,
  }),
});

Keyterms, on an ink-2 model:

// Keyterms only work on ink-2 and ink-preview models.
const withKeyterms = new CartesiaSTTService({
  apiKey: process.env.CARTESIA_API_KEY ?? "",
  settings: new CartesiaSTTService.Settings({
    model: "ink-2",
    keyterm: ["pipedog", "Pipecat"],
  }),
});

Change settings mid-call with an STTUpdateSettingsFrame. The service reconnects with the new settings:

// Queue it on the worker, or push it from a processor upstream of the service.
const update = new STTUpdateSettingsFrame({
  delta: new CartesiaSTTService.Settings({ language: Language.ES }),
});

Event handlers

Inherited from STTService:

EventDescription
on_connectedConnected to Cartesia.
on_disconnectedDisconnected from Cartesia.
on_connection_errorThe connection failed.

Notes

  • Emits InterimTranscriptionFrame while the user speaks and TranscriptionFrame for final results. A VADUserStoppedSpeakingFrame makes it send finalize, which flushes a final result.
  • Cartesia closes a socket after 3 minutes without messages. The service sends silence as a keepalive (after 120 s idle, checked every 30 s).
  • A settings change reconnects. While the user is speaking, the reconnect waits until they stop.
  • Keyterms set for a model other than ink-2 or ink-preview are ignored with a warning. Cartesia accepts at most 100 keyterms totaling 1200 characters; extra ones are dropped with a warning.
  • baseUrl takes a bare host ("api.cartesia.ai") or a URL with a scheme ("ws://localhost:8000").

CartesiaTurnsSTTService

Configuration

Prop

Type

Settings

SettingTypeDefaultDescription
modelstring"ink-2"Cartesia model. Cannot change at runtime.
languageLanguage | nullnullNot used: ink-2 is English-only. Emitted frames carry Language.EN.
keytermstring[] | nullnullTerms to bias transcription towards.
turnStartThresholdnumber | nullnull (Cartesia: 0.8)Likelihood above which the server emits turn.start. Range 0.5 to 0.9.
turnEagerEndThresholdnumber | nullnull (Cartesia: 0.6)Likelihood below which the server emits turn.eager_end. Range 0.3 to 0.8.
turnEndThresholdnumber | nullnull (Cartesia: 0.3)Likelihood below which the server emits turn.end. Range 0.05 to 0.5.
turnEndTimeoutMsnumber | nullnull (Cartesia: 5600)Wait after the user stops speaking before turn.end. Range 640 to 11200.

Keep the thresholds in order: turnStartThreshold > turnEagerEndThreshold > turnEndThreshold. Changing keyterm or a turn* setting at runtime reconnects. A model or language update is reported as unhandled.

Usage

turns.ts
import { CartesiaTurnsSTTService } from "@pipedog/cartesia/turns/stt";

const stt = new CartesiaTurnsSTTService({
  apiKey: process.env.CARTESIA_API_KEY ?? "",
  settings: new CartesiaTurnsSTTService.Settings({
    keyterm: ["pipedog"],
    turnEndTimeoutMs: 2000,
  }),
});

stt.addEventHandler("on_turn_end", async (_service, transcript: string) => {
  console.log("end of turn:", transcript);
});

With eager end of turn:

// Answer the server's predicted end of turn ahead of the committed one.
const eager = new CartesiaTurnsSTTService({
  apiKey: process.env.CARTESIA_API_KEY ?? "",
  enableEagerEndOfTurn: true,
  settings: new CartesiaTurnsSTTService.Settings({
    turnStartThreshold: 0.8,
    turnEagerEndThreshold: 0.6,
    turnEndThreshold: 0.3,
  }),
});

eager.addEventHandler("on_turn_eager_end", async (_service, transcript: string) => {
  console.log("predicted end of turn:", transcript);
});
eager.addEventHandler("on_turn_resume", async () => {
  console.log("user kept speaking");
});

Event handlers

EventDescription
on_turn_startThe server detected the start of a turn.
on_turn_updateA transcript update for the turn in progress.
on_turn_eager_endThe server predicted the end of the turn.
on_turn_resumeThe user kept speaking after an eager end.
on_turn_endThe final transcript for the turn.

on_connected, on_disconnected and on_connection_error are inherited from STTService.

Notes

  • The server drives turns: turn.start broadcasts a ProposedUserStartedSpeakingFrame, turn.update pushes an InterimTranscriptionFrame, and turn.end pushes a TranscriptionFrame and broadcasts a ProposedUserStoppedSpeakingFrame. Transcripts are cumulative per turn. There is no finalize command.
  • The service recommends ExternalUserTurnStrategies to the user aggregator, or EagerUserTurnStrategies with enableEagerEndOfTurn, so no local VAD or smart-turn model is needed. Passing your own userTurnStrategies overrides this. See Turn detection.
  • With enableEagerEndOfTurn: true, turn.eager_end pushes an EagerTranscriptionFrame that starts a response early. turn.resume pushes a UserTurnResponseCancelFrame. The early response is dropped if the user resumes or the final transcript differs. It is off by default because every prediction costs an inference.
  • shouldInterrupt (default true) decides whether the start of a turn interrupts the bot.
  • If audio stops after a turn.start, a watchdog sends silence so the turn does not hang. The idle threshold is max(chunkDuration * 2, watchdogMinTimeout), with watchdogMinTimeout defaulting to 0.5 s.
  • The service waits up to 10 s for Cartesia's connected message after the handshake.

Runtime notes

RuntimeWorksWhat it needs
Bun✅ yesNothing.
Node.js✅ yesNothing.
Deno✅ yesNothing.
Cloudflare Workers✅ yesNothing.

Generated from pipecat.runtimes and pipecat.compat in packages/cartesia/package.json: ✅ means the compat smoke test passes on that runtime.

Both services use the global WebSocket and send the key as an X-API-Key header on the handshake. The Bun, Node and Deno WebSocket support handshake headers, and on Cloudflare Workers the socket is opened through fetch with an Upgrade header. A standard WebSocket does not expose the handshake response, so the X-Request-ID header that upstream logs on connect is always logged as None. The deprecated CartesiaLiveOptions class and the liveOptions constructor option are not ported; use settings.

pipedog is an unofficial community TypeScript port of Pipecat. It is not affiliated with or endorsed by Daily or the Pipecat project.

Portions adapted from the Pipecat documentation, Copyright (c) 2024–2025 Daily, BSD-2-Clause (see apps/docs/LICENSE-pipecat-docs).

On this page