> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cartesia.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# 定型応答のための音声キャッシュ

> 定型的な TTS フレーズを raw PCM として事前生成し、ライブの WebSocket 音声とインターリーブしてレイテンシとクレジットを削減する

## 目的

定型フレーズ（挨拶、保留メッセージ、締めの言葉）は通話をまたいで繰り返し使われます。毎回生成し直すとレイテンシが増え、クレジットも消費します。代わりに、これらのフレーズを raw PCM として一度だけ事前生成してキャッシュし、実行時にライブの TTS ストリームへ差し込みます。キャッシュ済みクリップは API を経由しないため、その区間は高速かつ無料になります。

## 始める前に

以下が必要です：

* **API キー** — [play.cartesia.ai](https://play.cartesia.ai) から API キーを取得してください。Cartesia を初めて利用する場合は、[リアルタイム TTS クイックスタート](/ja-jp/get-started/realtime-text-to-speech-quickstart) を参照してください。
* **Raw コンテナ** — `container: "raw"`（ヘッダーなし PCM）を設定します。WAV や MP3 などのコンテナはヘッダーが付加されるため連結が壊れます。[TTS 出力フォーマット](/ja-jp/build-with-cartesia/capability-guides/tts-output-audio-format) を参照してください。
* **エンコーディングとサンプルレートの一致** — キャッシュ済みクリップはライブストリームと完全に一致している必要があります（例：24000 Hz の `pcm_s16le`）。
* **モデル、ボイス、言語の一致** — 本番環境では日付付きモデルスナップショット（例：`sonic-3.5-2026-05-04`）に固定し、キャッシュ済み音声とライブ音声が同一に聞こえるようにします。

キャッシュ済み音声とライブ音声は、クリックや歪みなく連結できるようバイト互換である必要があります。

## プロジェクトのセットアップ

このガイドに沿って進めるには、以下のようにして `tts-caching` という Python の `uv` プロジェクトを初期化してください：

```bash theme={null}
uv init tts-caching
cd tts-caching
uv venv
source .venv/bin/activate
uv add 'cartesia[websockets]'
export CARTESIA_API_KEY=sk_...
```

## ステップ 1: フレーズキャッシュを構築する

以下のスクリプトで音声フレーズキャッシュを生成します。このスクリプトは `PHRASES` の各フレーズを生成し、raw 音声 PCM バイトを `phrase-cache` ディレクトリに保存します。

```python build_cache.py theme={null}
from cartesia import Cartesia
import os

CACHE_DIR = "phrase-cache"
MODEL = "sonic-3.5"  # 本番ではスナップショットに固定: "sonic-3.5-2026-05-04"
VOICE_ID = "47c38ca4-5f35-497b-b1a3-415245fb35e1" # ユースケースに合ったボイスを play.cartesia.ai で選択してください
OUTPUT_FORMAT = {"container": "raw", "encoding": "pcm_s16le", "sample_rate": 24000} # 本番の「ライブ」TTS 生成で使用する音声フォーマットと一致させる必要があります

CUSTOMER_NAME="Elon"

# キャッシュする定型フレーズ。
# ブランド向けにカスタムの単語発音を作成する必要がある場合は、
# https://docs.cartesia.ai/build-with-cartesia/capability-guides/custom-pronunciations を参照してください
PHRASES = {
    "greeting": "Thanks for calling Acme support. How can I help you today?",
    "hold": f"Please hold while I look that up for you {CUSTOMER_NAME}.",
    "closing": "Is there anything else I can help you with?",
}

os.makedirs(CACHE_DIR, exist_ok=True)

client = Cartesia(api_key=os.environ["CARTESIA_API_KEY"])

# TTS 音声フレーズを生成します。
with client.tts.websocket_connect() as ws:
    for phrase_name, phrase in PHRASES.items():
        ctx = ws.context(
            model_id=MODEL,
            voice={"mode": "id", "id": VOICE_ID},
            output_format=OUTPUT_FORMAT,
            language="en",
        )
        ctx.push(phrase)
        ctx.no_more_inputs()

        audio = bytearray()
        for response in ctx.receive():
            if response.type == "chunk" and response.audio:
                audio += response.audio
            elif response.type == "done":
                break

        path = os.path.join(CACHE_DIR, f"{phrase_name}.raw")
        with open(path, "wb") as f:
            f.write(audio)
        print(f"Cached '{phrase_name}': {len(audio)} bytes -> {path}")
```

一度だけ実行します：

```bash theme={null}
uv run build_cache.py
```

## ステップ 2: 実行時にインターリーブする

実行時に、キャッシュ済みフレーズとライブフレーズのシーケンスを 1 つの出力バッファへと処理します。

```python interleave_tts.py theme={null}
from cartesia import Cartesia
import os

client = Cartesia(api_key=os.environ["CARTESIA_API_KEY"])

# build_cache.py の設定と一致させる必要があります
CACHE_DIR = "phrase-cache"
MODEL = "sonic-3.5"
VOICE_ID = "47c38ca4-5f35-497b-b1a3-415245fb35e1"
OUTPUT_FORMAT = {"container": "raw", "encoding": "pcm_s16le", "sample_rate": 24000}

CUSTOMER_NAME="Elon"

def load_cached_raw_file(name: str) -> bytes:
    with open(os.path.join(CACHE_DIR, f"{name}.raw"), "rb") as f:
        return f.read()


def call_llm(index: int, context: str = "") -> str:
    """本番では、会話コンテキストとともに推論用 LLM を呼び出します。
    デモではハードコードされた応答を返します。"""
    responses = [
        "Ok, sure. Let me pull up order number AKQ 4245 for you.",
        f"Ok! Good news {CUSTOMER_NAME}. Your order shipped yesterday and should arrive tomorrow.",
    ]
    return responses[index]


def call_cartesia_tts(ws, text: str) -> bytes:
    ctx = ws.context(
        model_id=MODEL,
        voice={"mode": "id", "id": VOICE_ID},
        output_format=OUTPUT_FORMAT,
        language="en",
    )
    ctx.push(text)
    ctx.no_more_inputs()

    audio = bytearray()
    for response in ctx.receive():
        if response.type == "chunk" and response.audio:
            audio += response.audio
        elif response.type == "done":
            break
    return bytes(audio)


# "cached" = ローカルキャッシュから読み込み（クレジット消費ゼロ）、"live" = Cartesia TTS で生成（クレジット消費あり）
interleaved_sequence: list[dict[str, str]] = [
    {"cached": "greeting"},
    {"live": call_llm(0)},
    {"cached": "hold"},
    {"live": call_llm(1)},
    {"cached": "closing"},
]

output = bytearray()

# キャッシュ済みフレーズとライブフレーズのシーケンスから音声を連結します。
with client.tts.websocket_connect() as ws:
    for item in interleaved_sequence:
        if "cached" in item:
            output += load_cached_raw_file(item["cached"])  # クレジット消費ゼロ
        else:
            output += call_cartesia_tts(ws, item["live"])  # クレジット消費あり

# 連結した PCM を試聴できるように WAV として書き出します。
import wave
os.makedirs("./output", exist_ok=True)
with wave.open("./output/spliced.wav", "wb") as w:
    w.setnchannels(1)
    w.setsampwidth(2)  # 16 ビット pcm_s16le
    w.setframerate(OUTPUT_FORMAT["sample_rate"])
    w.writeframes(output)

print(f"Wrote spliced.wav ({len(output)} bytes)")
```

実行します：

```bash theme={null}
uv run interleave_tts.py
```

`spliced.wav` を試聴してください。キャッシュ済みフレーズがライブ生成された音声とシームレスに繋がります。

### 設計パターン

`interleave_tts.py` では、シーケンス全体を通じて WebSocket を開いたままにしています。「ライブ」フレーズだけが Cartesia TTS を呼び出します。キャッシュ済みフレーズはストア（ディスク、S3、任意の場所）から取得するため、クレジットを消費するのはライブ部分だけです。

## 注意事項

* **音声フォーマットの不一致**: キャッシュ済みクリップがライブ音声と異なるエンコーディングやサンプルレートを使っていると、接合部でクリック音や歪みが聞こえます。
* **モデルを固定する**: 同じボイス ID でも、異なる TTS モデルバージョンを使うと音声生成に影響が出ることがあります。日付付きスナップショット（例：`sonic-3.5-2026-05-04`）に固定して、キャッシュ済み音声とライブ音声を同期させてください。
* **5 分のアイドルタイムアウト**: TTS WebSocket は [5 分間アクティビティがないと切断されます](/ja-jp/use-the-api/concurrency-limits-and-timeouts#tts-websocket-timeouts)。キャッシュ済みクリップを再生するだけの目的で切断しないでください。再接続コストが増えるだけです。ライブ生成の間隔が長い場合は、接続をアイドル状態で保持するのではなく、いったんクローズして再度オープンしてください。
* **完全なフレーズはきれいに繋がる**: Sonic TTS は前後の単語を感情や口調のコンテキストとして利用します。TTS クリップは完全な文として始まり、完全な文で終わるようにしてください。
* **WebSocket パターン**: このガイドでは TTS WebSocket を使用しており、複数回の生成にわたって 1 つの接続を開いたままにします。Bytes および SSE エンドポイントは生成ごとに 1 リクエストで動作するため、インターリーブする対象となる開いた接続がありません。[TTS エンドポイントの種類](/ja-jp/use-the-api/compare-tts-endpoints) を参照してください。
