# Rime

> Text-to-speech with RimeTTSService (WebSocket, word timestamps), RimeHttpTTSService (HTTP) and RimeV1TTSService (WebSocket v1, protobuf).

Source: https://polvallverdu.github.io/pipedog/docs/services/tts/rime/



## Overview [#overview]

Rime's Coda and Mist models turn text into speech. pipedog ports these Pipecat services:

* **`RimeTTSService`**: Rime's JSON WebSocket API (`/ws3`), with word timestamps and one audio
  context per turn.
* **`RimeHttpTTSService`**: Rime's HTTP API, streaming raw PCM, for when a WebSocket is not
  needed.
* **`RimeV1TTSService`**: Rime's WebSocket v1 API, which carries protobuf envelopes (binary or
  proto3 JSON). Interruptions cancel a context without reconnecting, and Rime can accumulate
  streamed LLM tokens into sentences.

<Cards>
  <Card title="Upstream page" href="https://docs.pipecat.ai/api-reference/server/services/tts/rime">
    Pipecat's documentation for the same services.
  </Card>

  <Card title="Source" href="https://github.com/polvallverdu/pipedog/tree/main/packages/rime/src">
    `packages/rime/src/tts.ts` and `tts_v1.ts`.
  </Card>

  <Card title="Rime docs" href="https://docs.rime.ai/api-reference/coda/websocket-v1">
    The WebSocket v1 protocol, voices and models.
  </Card>
</Cards>

## Installation [#installation]

```bash
bun add @pipedog/rime
```

```ts
import { RimeHttpTTSService, RimeTTSService } from "@pipedog/rime/tts";
import { RimeV1TTSService } from "@pipedog/rime/tts_v1";
```

## Prerequisites [#prerequisites]

* A Rime account and an API key from the [Rime dashboard](https://rime.ai).
* A voice (`speaker`) for the model you use. There is no default voice.
* Pass the key as `apiKey`. The examples read it from `RIME_API_KEY`.

## RimeTTSService [#rimettsservice]

### Configuration [#configuration]

| Option | Type | Required | Description |
|---|---|---|---|
| `pushTextFrames` | `boolean \| undefined` | no | Whether to push TextFrames and LLMFullResponseEndFrames. |
| `pushStopFrames` | `boolean \| undefined` | no | Whether to automatically push TTSStoppedFrames. |
| `apiKey` | `string` | yes | Rime API key for authentication. |
| `url` | `string \| undefined` | no | Rime websocket API endpoint. Defaults to "wss://users-ws.rime.ai/ws3". |
| `settings` | `RimeTTSSettings \| null \| undefined` | no | Runtime-updatable settings (defaults: model "coda"). |
| `name` | `string \| null \| undefined` | no | Custom name; defaults to `ClassName#count`. |
| `taskManager` | `BaseTaskManager \| null \| undefined` | no | Task manager for handling tasks. |
| `enableDirectMode` | `boolean \| undefined` | no | Process frames immediately instead of using internal queues. |
| `metrics` | `FrameProcessorMetrics \| null \| undefined` | no | Optional metrics collector for this processor. |
| `reconnectBackoffMinWait` | `number \| undefined` | no | Minimum time, in seconds, to wait between reconnection attempts. |
| `reconnectBackoffMaxWait` | `number \| undefined` | no | Maximum time, in seconds, to wait between reconnection attempts. |
| `reconnectOnError` | `boolean \| undefined` | no | Whether to automatically reconnect on connection errors. |
| `wsCloseTimeout` | `number \| undefined` | no | Maximum time, in seconds, to wait for the peer to acknowledge the websocket closing handshake before dropping the connection. Applied to connections opened through `_websocketConnect()`. Increase it for peers that need longer to complete a graceful close. |
| `wsSendTimeout` | `number \| null \| undefined` | no | Maximum time, in seconds, a single send may wait on a peer that has stopped reading before the connection is dropped and the send throws `ConnectionClosedError`. Applied to connections opened through `_websocketConnect()`. Pass `null` to wait indefinitely. Defaults to `WS_SEND_TIMEOUT`. |
| `textAggregationMode` | `TextAggregationMode \| null \| undefined` | no | How to aggregate incoming text before synthesis. `TextAggregationMode.SENTENCE` (default) buffers until sentence boundaries, `TextAggregationMode.TOKEN` streams tokens directly for lower latency. |
| `pushStartFrame` | `boolean \| undefined` | no | Whether to automatically create audio contexts and push TTSStartedFrames. When true, the base class handles `createAudioContext` and yields `TTSStartedFrame` before each synthesis call, so `runTts` implementations do not need to. |
| `stopFrameTimeoutS` | `number \| undefined` | no | Idle time before pushing TTSStoppedFrame when pushStopFrames is true. |
| `pushSilenceAfterStop` | `boolean \| undefined` | no | Whether to push silence audio after TTSStoppedFrame. |
| `silenceTimeS` | `number \| undefined` | no | Duration of silence to push when pushSilenceAfterStop is true. |
| `pauseFrameProcessing` | `boolean \| undefined` | no | Whether to hold incoming frames after a turn until its audio contexts end. Needed by services whose audio doesn't carry its request's context ID. |
| `maxConsecutiveZeroAudioContexts` | `number \| undefined` | no | How many consecutive TTS contexts may complete without producing any audio before the service is reported unable to do its job. Catches a provider that accepts requests and stays silent — an unknown voice ID, say — which no error ever surfaces. Every silent context reports an error the service can carry on from, whatever the limit is; on reaching the limit the service reports a permanent error instead, stops being given work, and the pipeline worker applies its `ProcessorUnusablePolicy`. Set to 0 to report silent contexts without ever writing the service off. |
| `appendTrailingSpace` | `boolean \| undefined` | no | Whether to append a trailing space to text before sending to TTS. This helps prevent some TTS services from vocalizing trailing punctuation (e.g., "dot"). Only applied in sentence aggregation mode; when streaming tokens, the incoming text's own whitespace is preserved. |
| `sampleRate` | `number \| null \| undefined` | no | Output sample rate for generated audio. |
| `skipTextTypes` | `string[] \| null \| undefined` | no | List of text types that should not be spoken. |
| `textTransforms` | `[string, TextTransform][] \| null \| undefined` | no | A list of TextTransforms to transform text just before sending it to TTS. Each TextTransform takes the aggregated text and its type, and returns the transformed text. To register, provide a list of tuples of `[textType \| "*", transformFunction]`. |
| `transportDestination` | `string \| null \| undefined` | no | Destination for generated audio frames. |
| `reuseContextIdWithinTurn` | `boolean \| undefined` | no | Whether the service should reuse context IDs within the same turn. |
| `audioCache` | `TTSAudioCacheStore \| null \| undefined` | no | Where to keep synthesized audio for reuse (e.g. a shared `TTSAudioCache`). Text whose audio is in the cache is served from it, with no request to the provider. Lines get there through `warmAudioCache()`, and also from live synthesis when `cacheSynthesizedAudio` allows it. Only services that yield their audio from `runTts` (HTTP and streaming-RPC services, see `supportsAudioCache`) use it, and only in sentence aggregation mode. Defaults to null (no cache). pipedog only. |
| `cacheSynthesizedAudio` | `boolean \| ((text: string) => boolean) \| undefined` | no | Whether audio synthesized live is stored in `audioCache`, or a function that decides per text. Defaults to false: only warmed lines are stored, since a generated reply can repeat what the user said. pipedog only. |
| `audioCacheNamespace` | `string \| undefined` | no | Extra part of the cache key, for constructor options that change the audio but are not in `settings`. pipedog only. |

### Settings [#settings]

Runtime-updatable settings, passed as `settings: new RimeTTSService.Settings({...})`.
`RimeHttpTTSService` uses the same settings class. Each setting is only sent when set; the
sampling settings go to `coda` and the `mistv2` ones to the other models.

| Setting                                                                                                | Type               | Default  | Description                                                              |
| ------------------------------------------------------------------------------------------------------ | ------------------ | -------- | ------------------------------------------------------------------------ |
| `model`                                                                                                | `string`           | `"coda"` | Rime model: `coda`, or a Mist model such as `mistv2`.                    |
| `voice`                                                                                                | `string \| null`   | `null`   | Voice (`speaker`). Set one.                                              |
| `language`                                                                                             | `Language \| null` | `null`   | Language. Sent as a Rime code such as `eng`.                             |
| `segment`                                                                                              | `string \| null`   | `null`   | `"immediate"`, `"bySentence"` or `"never"`.                              |
| `speedAlpha`                                                                                           | `number \| null`   | `null`   | Speech speed multiplier.                                                 |
| `inlineSpeedAlpha`                                                                                     | `string \| null`   | `null`   | Inline speed markup.                                                     |
| `repetitionPenalty`, `temperature`, `topP`                                                             | `number \| null`   | `null`   | Coda sampling (wire names `repetition_penalty`, `temperature`, `top_p`). |
| `timeScaleFactor`                                                                                      | `number \| null`   | `null`   | Coda playback speed. Above 1.0 slows the audio down.                     |
| `reduceLatency`, `pauseBetweenBrackets`, `phonemizeBetweenBrackets`, `noTextNormalization`, `saveOovs` | `boolean \| null`  | `null`   | Mist options.                                                            |

### Usage [#usage]

```ts title="tts.ts"
import { TTSUpdateSettingsFrame } from "@pipedog/core/frames/frames";
import { RimeTTSService } from "@pipedog/rime/tts";

const tts = new RimeTTSService({
  apiKey: process.env.RIME_API_KEY ?? "",
  settings: new RimeTTSService.Settings({
    model: "coda",
    voice: "luna",
    timeScaleFactor: 1.0,
  }),
});

```

Text helpers for Rime's markup:

```ts
// Text helpers for Rime's markup, for use in text transforms.
const spelled = RimeTTSService.SPELL("A1B2");
const paused = `One moment.${RimeTTSService.PAUSE_TAG(0.5)} Thanks.`;

// Custom pronunciation and inline speed apply to the next message sent.
const pronounced = tts.PRONOUNCE("Say hello", "hello", "h'El0");
const slow = tts.INLINE_SPEED("slowly", 0.7);
```

Change settings mid-call with a `TTSUpdateSettingsFrame`:

```ts
// Queue it on the worker, or push it from a processor upstream of the service.
// Every setting of the WebSocket service is a URL parameter, so a change reconnects.
const update = new TTSUpdateSettingsFrame({
  delta: new RimeTTSService.Settings({ voice: "another-voice" }),
});
```

### Event handlers [#event-handlers]

Inherited from `TTSService`:

| Event                 | Description                                     |
| --------------------- | ----------------------------------------------- |
| `on_connected`        | Connected to Rime.                              |
| `on_disconnected`     | Disconnected from Rime.                         |
| `on_connection_error` | The connection failed.                          |
| `on_tts_request`      | A text chunk is about to be sent for synthesis. |

### Notes [#notes]

* Every setting is a query parameter of the WebSocket URL, so any change reconnects.
* The service does not push the LLM's text frames. It builds `TTSTextFrame`s from Rime's word
  timestamps.
* Text inside `spell(...)` is never split across aggregates.
* `PRONOUNCE()` and `INLINE_SPEED()` add fields to the next message only.

## RimeHttpTTSService [#rimehttpttsservice]

### Configuration [#configuration-1]

| Option | Type | Required | Description |
|---|---|---|---|
| `apiKey` | `string` | yes | Rime API key for authentication. |
| `fetch` | `((input: RequestInfo \| URL, init?: RequestInit) => Promise<Response>) \| null \| undefined` | no | `fetch` used for HTTP requests. Defaults to the global `fetch`. DIVERGENCE: replaces upstream's required `aiohttp_session`. |
| `settings` | `RimeTTSSettings \| null \| undefined` | no | Runtime-updatable settings (defaults: model "coda", language "eng"). |
| `name` | `string \| null \| undefined` | no | Custom name; defaults to `ClassName#count`. |
| `taskManager` | `BaseTaskManager \| null \| undefined` | no | Task manager for handling tasks. |
| `enableDirectMode` | `boolean \| undefined` | no | Process frames immediately instead of using internal queues. |
| `metrics` | `FrameProcessorMetrics \| null \| undefined` | no | Optional metrics collector for this processor. |
| `textAggregationMode` | `TextAggregationMode \| null \| undefined` | no | How to aggregate incoming text before synthesis. `TextAggregationMode.SENTENCE` (default) buffers until sentence boundaries, `TextAggregationMode.TOKEN` streams tokens directly for lower latency. |
| `pushTextFrames` | `boolean \| undefined` | no | Whether to push TextFrames and LLMFullResponseEndFrames. |
| `pushStopFrames` | `boolean \| undefined` | no | Whether to automatically push TTSStoppedFrames. |
| `pushStartFrame` | `boolean \| undefined` | no | Whether to automatically create audio contexts and push TTSStartedFrames. When true, the base class handles `createAudioContext` and yields `TTSStartedFrame` before each synthesis call, so `runTts` implementations do not need to. |
| `stopFrameTimeoutS` | `number \| undefined` | no | Idle time before pushing TTSStoppedFrame when pushStopFrames is true. |
| `pushSilenceAfterStop` | `boolean \| undefined` | no | Whether to push silence audio after TTSStoppedFrame. |
| `silenceTimeS` | `number \| undefined` | no | Duration of silence to push when pushSilenceAfterStop is true. |
| `pauseFrameProcessing` | `boolean \| undefined` | no | Whether to hold incoming frames after a turn until its audio contexts end. Needed by services whose audio doesn't carry its request's context ID. |
| `maxConsecutiveZeroAudioContexts` | `number \| undefined` | no | How many consecutive TTS contexts may complete without producing any audio before the service is reported unable to do its job. Catches a provider that accepts requests and stays silent — an unknown voice ID, say — which no error ever surfaces. Every silent context reports an error the service can carry on from, whatever the limit is; on reaching the limit the service reports a permanent error instead, stops being given work, and the pipeline worker applies its `ProcessorUnusablePolicy`. Set to 0 to report silent contexts without ever writing the service off. |
| `appendTrailingSpace` | `boolean \| undefined` | no | Whether to append a trailing space to text before sending to TTS. This helps prevent some TTS services from vocalizing trailing punctuation (e.g., "dot"). Only applied in sentence aggregation mode; when streaming tokens, the incoming text's own whitespace is preserved. |
| `sampleRate` | `number \| null \| undefined` | no | Output sample rate for generated audio. |
| `skipTextTypes` | `string[] \| null \| undefined` | no | List of text types that should not be spoken. |
| `textTransforms` | `[string, TextTransform][] \| null \| undefined` | no | A list of TextTransforms to transform text just before sending it to TTS. Each TextTransform takes the aggregated text and its type, and returns the transformed text. To register, provide a list of tuples of `[textType \| "*", transformFunction]`. |
| `transportDestination` | `string \| null \| undefined` | no | Destination for generated audio frames. |
| `reuseContextIdWithinTurn` | `boolean \| undefined` | no | Whether the service should reuse context IDs within the same turn. |
| `audioCache` | `TTSAudioCacheStore \| null \| undefined` | no | Where to keep synthesized audio for reuse (e.g. a shared `TTSAudioCache`). Text whose audio is in the cache is served from it, with no request to the provider. Lines get there through `warmAudioCache()`, and also from live synthesis when `cacheSynthesizedAudio` allows it. Only services that yield their audio from `runTts` (HTTP and streaming-RPC services, see `supportsAudioCache`) use it, and only in sentence aggregation mode. Defaults to null (no cache). pipedog only. |
| `cacheSynthesizedAudio` | `boolean \| ((text: string) => boolean) \| undefined` | no | Whether audio synthesized live is stored in `audioCache`, or a function that decides per text. Defaults to false: only warmed lines are stored, since a generated reply can repeat what the user said. pipedog only. |
| `audioCacheNamespace` | `string \| undefined` | no | Extra part of the cache key, for constructor options that change the audio but are not in `settings`. pipedog only. |

### Usage [#usage-1]

```ts title="http.ts"
import { RimeHttpTTSService } from "@pipedog/rime/tts";

const httpTts = new RimeHttpTTSService({
  apiKey: process.env.RIME_API_KEY ?? "",
  settings: new RimeHttpTTSService.Settings({ model: "mistv2", voice: "luna", speedAlpha: 1.1 }),
});

```

### Notes [#notes-1]

* It posts to `https://users.rime.ai/v1/rime-tts` and streams the PCM body. There are no word
  timestamps.
* The default `language` is `"eng"`.

## RimeV1TTSService [#rimev1ttsservice]

### Configuration [#configuration-2]

| Option | Type | Required | Description |
|---|---|---|---|
| `pushTextFrames` | `boolean \| undefined` | no | Whether to push TextFrames and LLMFullResponseEndFrames. |
| `pushStopFrames` | `boolean \| undefined` | no | Whether to automatically push TTSStoppedFrames. |
| `apiKey` | `string` | yes | Rime API key for authentication. |
| `websocketUrl` | `string \| null \| undefined` | no | Rime WebSocket v1 endpoint override. Defaults to the public endpoint for `settings.model`. Custom URLs can use any path. Models without a public default require an explicit URL. |
| `websocketProtocol` | `WebSocketProtocol \| undefined` | no | Envelope encoding, "binary" or "json". |
| `settings` | `RimeV1TTSSettings \| null \| undefined` | no | Settings with an explicit `model`. Other fields can be updated at runtime; changing `model` does not switch endpoints. |
| `name` | `string \| null \| undefined` | no | Custom name; defaults to `ClassName#count`. |
| `taskManager` | `BaseTaskManager \| null \| undefined` | no | Task manager for handling tasks. |
| `enableDirectMode` | `boolean \| undefined` | no | Process frames immediately instead of using internal queues. |
| `metrics` | `FrameProcessorMetrics \| null \| undefined` | no | Optional metrics collector for this processor. |
| `reconnectBackoffMinWait` | `number \| undefined` | no | Minimum time, in seconds, to wait between reconnection attempts. |
| `reconnectBackoffMaxWait` | `number \| undefined` | no | Maximum time, in seconds, to wait between reconnection attempts. |
| `reconnectOnError` | `boolean \| undefined` | no | Whether to automatically reconnect on connection errors. |
| `wsCloseTimeout` | `number \| undefined` | no | Maximum time, in seconds, to wait for the peer to acknowledge the websocket closing handshake before dropping the connection. Applied to connections opened through `_websocketConnect()`. Increase it for peers that need longer to complete a graceful close. |
| `wsSendTimeout` | `number \| null \| undefined` | no | Maximum time, in seconds, a single send may wait on a peer that has stopped reading before the connection is dropped and the send throws `ConnectionClosedError`. Applied to connections opened through `_websocketConnect()`. Pass `null` to wait indefinitely. Defaults to `WS_SEND_TIMEOUT`. |
| `textAggregationMode` | `TextAggregationMode \| null \| undefined` | no | How to aggregate incoming text before synthesis. `TextAggregationMode.SENTENCE` (default) buffers until sentence boundaries, `TextAggregationMode.TOKEN` streams tokens directly for lower latency. |
| `pushStartFrame` | `boolean \| undefined` | no | Whether to automatically create audio contexts and push TTSStartedFrames. When true, the base class handles `createAudioContext` and yields `TTSStartedFrame` before each synthesis call, so `runTts` implementations do not need to. |
| `stopFrameTimeoutS` | `number \| undefined` | no | Idle time before pushing TTSStoppedFrame when pushStopFrames is true. |
| `pushSilenceAfterStop` | `boolean \| undefined` | no | Whether to push silence audio after TTSStoppedFrame. |
| `silenceTimeS` | `number \| undefined` | no | Duration of silence to push when pushSilenceAfterStop is true. |
| `pauseFrameProcessing` | `boolean \| undefined` | no | Whether to hold incoming frames after a turn until its audio contexts end. Needed by services whose audio doesn't carry its request's context ID. |
| `maxConsecutiveZeroAudioContexts` | `number \| undefined` | no | How many consecutive TTS contexts may complete without producing any audio before the service is reported unable to do its job. Catches a provider that accepts requests and stays silent — an unknown voice ID, say — which no error ever surfaces. Every silent context reports an error the service can carry on from, whatever the limit is; on reaching the limit the service reports a permanent error instead, stops being given work, and the pipeline worker applies its `ProcessorUnusablePolicy`. Set to 0 to report silent contexts without ever writing the service off. |
| `appendTrailingSpace` | `boolean \| undefined` | no | Whether to append a trailing space to text before sending to TTS. This helps prevent some TTS services from vocalizing trailing punctuation (e.g., "dot"). Only applied in sentence aggregation mode; when streaming tokens, the incoming text's own whitespace is preserved. |
| `sampleRate` | `number \| null \| undefined` | no | Output sample rate for generated audio. |
| `skipTextTypes` | `string[] \| null \| undefined` | no | List of text types that should not be spoken. |
| `textTransforms` | `[string, TextTransform][] \| null \| undefined` | no | A list of TextTransforms to transform text just before sending it to TTS. Each TextTransform takes the aggregated text and its type, and returns the transformed text. To register, provide a list of tuples of `[textType \| "*", transformFunction]`. |
| `transportDestination` | `string \| null \| undefined` | no | Destination for generated audio frames. |
| `reuseContextIdWithinTurn` | `boolean \| undefined` | no | Whether the service should reuse context IDs within the same turn. |
| `audioCache` | `TTSAudioCacheStore \| null \| undefined` | no | Where to keep synthesized audio for reuse (e.g. a shared `TTSAudioCache`). Text whose audio is in the cache is served from it, with no request to the provider. Lines get there through `warmAudioCache()`, and also from live synthesis when `cacheSynthesizedAudio` allows it. Only services that yield their audio from `runTts` (HTTP and streaming-RPC services, see `supportsAudioCache`) use it, and only in sentence aggregation mode. Defaults to null (no cache). pipedog only. |
| `cacheSynthesizedAudio` | `boolean \| ((text: string) => boolean) \| undefined` | no | Whether audio synthesized live is stored in `audioCache`, or a function that decides per text. Defaults to false: only warmed lines are stored, since a generated reply can repeat what the user said. pipedog only. |
| `audioCacheNamespace` | `string \| undefined` | no | Extra part of the cache key, for constructor options that change the audio but are not in `settings`. pipedog only. |

### Settings [#settings-1]

Passed as `settings: new RimeV1TTSService.Settings({...})`. `model` is required at construction.
Each context uses the settings in effect when it starts. A setting the model does not accept is
left out with a warning.

| Setting                                                        | Type               | Description                                                                                                  |
| -------------------------------------------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------ |
| `model`                                                        | `string`           | Required. `"coda"` and `"mistv3"` select their public endpoints. Changing it later is reported, not applied. |
| `voice`                                                        | `string \| null`   | Voice (`speaker`).                                                                                           |
| `language`                                                     | `Language \| null` | Sent as a BCP-47 tag such as `en`.                                                                           |
| `timeScaleFactor`                                              | `number \| null`   | Playback speed. Not for mistv2.                                                                              |
| `textLookaheadTokens`                                          | `number \| null`   | Tokens to collect before audio starts (Coda only).                                                           |
| `pauseBetweenBrackets`, `phonemizeBetweenBrackets`, `saveOovs` | `boolean \| null`  | Mist options.                                                                                                |

### Usage [#usage-2]

```ts title="v1.ts"
import { TextAggregationMode } from "@pipedog/core/services/tts_service";
import { RimeV1TTSService } from "@pipedog/rime/tts_v1";

const v1 = new RimeV1TTSService({
  apiKey: process.env.RIME_API_KEY ?? "",
  // "binary" (protobuf) or "json" (proto3 JSON). Binary is the default.
  websocketProtocol: "binary",
  // Stream LLM tokens and let Rime accumulate them into sentences.
  textAggregationMode: TextAggregationMode.TOKEN,
  // The model is required: "coda" and "mistv3" select Rime's public endpoints.
  settings: new RimeV1TTSService.Settings({ model: "coda", voice: "luna" }),
});
```

### Notes [#notes-2]

* Pass `websocketUrl` for a custom deployment. The model is never inferred from the URL.
* Rime v1 sends a `ready` event before the first context. The service waits up to 10 seconds
  for it and reports a connection error otherwise.
* A failed context is reported with the error's `ErrorCategory`, and the service stays usable for
  the next turn.
* Rime v1 does not report word timestamps.

## Runtime notes [#runtime-notes]

* The WebSocket services use the global `WebSocket` and send the key as an `Authorization`
  header on the handshake. That works on Bun, Node and Deno, and on Cloudflare Workers, where the
  socket is opened through `fetch`.
* `RimeV1TTSService` reads the negotiated subprotocol (`rime.v1.binary` or `rime.v1.json`) from
  the socket and refuses a connection that did not accept it.
* The protobuf messages come from Rime's own package, `@rimelabs/api`, with `@bufbuild/protobuf`.
  Both are plain JavaScript.
* `RimeHttpTTSService` uses `fetch`. Upstream's required `aiohttp_session` is replaced by an
  optional `fetch` option.
* Not ported: the deprecated `RimeNonJsonTTSService` and the `InputParams` classes, and the
  `voiceId`, `model`, `params` and `aggregateSentences` constructor options.
* Whole-number floats, such as `PAUSE_TAG(0.5)`'s `500`, print without Python's `.0`.

| Runtime            | Works | What it needs |
| ------------------ | ----- | ------------- |
| Bun                | ✅ yes | Nothing.      |
| Node.js            | ✅ yes | Nothing.      |
| Deno               | ✅ yes | Nothing.      |
| Cloudflare Workers | ✅ yes | Nothing.      |

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