# Text to Speech

Speak assistant replies aloud using the browser's built-in speech API or a cloud TTS provider.

Make your chat speak. Call a `speak()` function when a reply finishes streaming — no extra dependencies needed for the browser-native path, and a thin backend route for higher-quality cloud voices.

## Browser-native TTS

The [Web Speech API](https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthesis) is built into every modern browser. Call `speak(answer)` right before you set `chat.loading = false`:

```js

function speak(text) {
  if (!('speechSynthesis' in window)) return;
  const utter = new SpeechSynthesisUtterance(text);
  utter.lang = 'en-US';
  speechSynthesis.cancel(); // stop any previous utterance
  speechSynthesis.speak(utter);
}

const chat = document.getElementById('chat');

chat.addEventListener('kai-submit', async (e) => {
  const userText = e.detail.value.trim();
  if (!userText) return;

  const history = [
    ...chat.messages,
    { id: crypto.randomUUID(), role: 'user', parts: [{ type: 'text', text: userText }] },
  ];
  chat.messages = history;
  chat.loading = true;

  // createAssistantStream appends the in-flight assistant message and folds each
  // delta onto its parts, so nothing here hand-builds a placeholder.
  const stream = createAssistantStream((fn) => { chat.messages = fn(chat.messages); });
  try {
    const res = await fetch('/api/chat', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ messages: toOpenAIMessages(history) }),
    });
    // readOpenAIStream parses the SSE and resolves with the finished turn, so
    // turn.text is the whole reply with no accumulator of your own. A non-ok
    // response throws WireError before a chunk is read, so there is no res.ok
    // check to forget.
    const turn = await readOpenAIStream(res, stream);
    speak(turn.text); // speak the completed reply
  } finally {
    stream.done();
    chat.loading = false;
  }
});
```

> **tip:** 
`speechSynthesis.getVoices()` returns the available voices asynchronously — listen for the `voiceschanged` event before calling it. Pass the chosen `SpeechSynthesisVoice` object to `utter.voice` to override the browser default.

## Cloud TTS

For higher-quality, consistent voices (OpenAI, ElevenLabs, and similar), proxy the TTS call through your backend — never expose provider API keys to the client.

```js
async function speakCloud(text) {
  const res = await fetch('/api/tts', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ text, voice: 'alloy' }),
  });
  if (!res.ok) return;
  const audio = new Audio(URL.createObjectURL(await res.blob()));
  audio.play();
}
```

Replace `speak(turn.text)` in the streaming handler above with `speakCloud(turn.text)`.

### Example `/api/tts` route (Node.js / Express)

```js
// POST /api/tts — proxies to OpenAI TTS, streams back audio

const openai = new OpenAI(); // reads OPENAI_API_KEY from environment

app.post('/api/tts', async (req, res) => {
  const { text, voice = 'alloy' } = req.body;
  const mp3 = await openai.audio.speech.create({
    model: 'tts-1',
    voice,
    input: text,
  });
  res.setHeader('Content-Type', 'audio/mpeg');
  mp3.body.pipe(res);
});
```

> **caution:** 
The TTS provider key must never reach the browser. Route all TTS requests through your own backend.

## Stopping playback

For browser-native TTS, call `speechSynthesis.cancel()` at the start of each new reply (the code above already does this) or when the user sends a new message.

For cloud TTS, keep a reference to the `Audio` object and call `.pause()`:

```js
let currentAudio = null;

async function speakCloud(text) {
  if (currentAudio) {
    currentAudio.pause();
    currentAudio = null;
  }
  const res = await fetch('/api/tts', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ text, voice: 'alloy' }),
  });
  if (!res.ok) return;
  currentAudio = new Audio(URL.createObjectURL(await res.blob()));
  currentAudio.play();
}
```

## Related

- [Getting Started](/guides/getting-started/) — streaming replies with `kai-chat`
- [`kai-chat` component reference](/components/chat/) — all props and events, including `loading`
