# McSpeak

Multi-engine text-to-speech server exposed as MCP tools via [FastMCP 3.0](https://gofastmcp.com/) Streamable HTTP. Plays audio through the host's speakers using PipeWire. Built for giving AI agents a voice -- literally.

[![PyPI](https://img.shields.io/pypi/v/mcspeak)](https://pypi.org/project/mcspeak/)
[![Python 3.12+](https://img.shields.io/badge/python-3.12%2B-blue)](https://python.org)
[![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)

## Quick Start

```bash
# Run directly from PyPI (no install needed)
uvx mcspeak

# Or install and run
pip install mcspeak
mcspeak
```

The server starts on `http://0.0.0.0:8371` and exposes MCP tools over Streamable HTTP. Point any MCP client at it.

**Add to Claude Code:**

```bash
claude mcp add mcspeak -- uvx mcspeak
```

For Docker deployment (recommended -- handles PipeWire, Piper, and Orpheus GPU inference), see [Docker Setup](#docker-setup) below.

## MCP Tools

| Tool | What it does |
|------|-------------|
| `speak` | Synthesize text and play through speakers. Blocks with live progress. |
| `generate_audio` | Synthesize to WAV file without playing. Bypasses the queue. |
| `list_voices` | List available voices for an engine. |
| `list_engines` | Show all engines and their health status. |
| `speech_status` | Check status of a queued/playing speech item. |
| `cancel_speech` | Kill playback immediately. Plays a cancel tone. |

There's also an `audio://recent` resource listing the last 20 generated WAV files.

## Engines

| Engine | Backend | Speed | Voices | Notes |
|--------|---------|-------|--------|-------|
| **kokoro** | ONNX (local CPU) | ~4x realtime | ~50 | Default. Fast, good quality. |
| **piper** | Wyoming protocol (Docker) | ~2x realtime | Depends on model | Connects to any Wyoming-compatible TTS server. |
| **orpheus** | llama-server + SNAC | ~1x realtime | 6 | LLM-based. Supports `<laugh>`, `<sigh>`, emotion tags. Needs GPU. |

## Features

### Radio-Style Alert Tones

Speech is bookended by short tones inspired by two-way radio systems. An entry tone fires immediately when `speak()` is called (before synthesis starts), acting as an audible "I heard you" that covers the 1-3s synthesis latency. An exit tone plays when the queue empties.

Available tones: `chirp` (Nextel iDEN), `apollo` (NASA quindar key-up), `roger` (CB radio descending beep), `quindar-out` (NASA unkey), `scratch` (vinyl record scratch for cancellation), and more. All generated programmatically at startup -- no bundled audio files.

### Voice Identity

When multiple projects connect simultaneously, each one automatically gets a distinct voice via round-robin assignment from a curated pool. Voices are interleaved across gender and accent (American, British, European) for maximum contrast. Assignments persist across restarts.

Pass `project="my-project"` in `speak()` calls, or let it auto-detect from MCP Roots.

### Media Ducking

External audio (YouTube, Spotify, etc.) automatically fades down before speech and fades back in after the exit tone, like a radio broadcast interruption. Uses `pactl` under the hood. Fails silently if PulseAudio isn't available.

### Chunked Synthesis Pipeline

Long texts (20+ words with sentence boundaries) are split into chunks and pipelined -- synthesis of chunk N+1 overlaps playback of chunk N. First audio plays within ~1-2s instead of waiting for the full text to synthesize.

### Graceful Shutdown

`docker compose down` lets the current speech finish instead of cutting mid-sentence. Two-phase: 3s handler drain, then up to 30s for the consumer to finish. Configurable via `TTS_SHUTDOWN_TIMEOUT`.

## Configuration

All settings use the `TTS_` prefix and can be set via environment variables or `.env` file.

| Variable | Default | Description |
|----------|---------|-------------|
| `TTS_HOST` | `0.0.0.0` | Server bind address |
| `TTS_PORT` | `8371` | Server port |
| `TTS_ENTRY_TONE` | `chirp` | Tone before speech (`chirp`, `apollo`, `none`, or WAV path) |
| `TTS_EXIT_TONE` | `roger` | Tone after speech (`roger`, `quindar-out`, `none`, or WAV path) |
| `TTS_CANCEL_TONE` | `scratch` | Tone on cancel (`scratch`, `reverse-roger`, `none`, or WAV path) |
| `TTS_DUCK_MEDIA` | `true` | Fade external audio during speech |
| `TTS_DUCK_FADE_OUT_MS` | `500` | Media fade-out duration (ms) |
| `TTS_DUCK_FADE_IN_MS` | `1000` | Media fade-in duration (ms) |
| `TTS_VOICE_IDENTITY` | `true` | Auto-assign distinct voices per project |
| `TTS_VOICE_IDENTITY_FILE` | (empty) | JSON file to persist voice assignments |
| `TTS_VOICE_IDENTITY_EXCLUDE` | `af_nicole` | Voices excluded from auto-assign pool |
| `TTS_ANNOUNCE_PROJECT` | `false` | Prefix speech with project name |
| `TTS_VOICE_BLACKLIST` | `am_adam,af_jessica` | Voices hidden from listings |
| `TTS_SHUTDOWN_TIMEOUT` | `30` | Max seconds to wait for speech on shutdown |
| `TTS_KOKORO_MODEL` | `models/kokoro/kokoro-v1.0.onnx` | Path to Kokoro ONNX model |
| `TTS_KOKORO_VOICES` | `models/kokoro/voices-v1.0.bin` | Path to Kokoro voice pack |
| `TTS_PIPER_HOST` | `172.26.0.3` | Piper Wyoming server host |
| `TTS_PIPER_PORT` | `10200` | Piper Wyoming server port |
| `TTS_ORPHEUS_URL` | `http://127.0.0.1:8081` | Orpheus llama-server URL |

## Docker Setup

Docker is the recommended deployment. The compose file runs McSpeak alongside a GPU-accelerated llama-server for Orpheus.

**Prerequisites:**
- PipeWire running on the host (for `pw-play` audio output)
- NVIDIA GPU + nvidia-container-toolkit (for Orpheus only)
- Kokoro ONNX model files in `./models/kokoro/`

```bash
# Copy and edit .env
cp .env.example .env
# Set ORPHEUS_GGUF_PATH to your Orpheus GGUF model location

# Build and start
make up

# Follow logs
make logs

# Check health
make status
```

The container mounts the host PipeWire socket (`/run/user/1000/pipewire-0`) for audio playback and the PulseAudio compat socket (`/run/user/1000/pulse`) for media ducking.

`stop_grace_period` in the compose file is set to 38s (3s drain + 30s shutdown timeout + 5s safety). Adjust if you change `TTS_SHUTDOWN_TIMEOUT`.

## Architecture

```
src/tts_mcp/
  server.py          FastMCP lifespan, tool definitions, chunked synthesis
  queue.py           Producer-consumer speech queue with priority + outcome tracking
  tones.py           Procedural tone WAV generation (numpy, 48kHz 16-bit PCM)
  audio.py           WAV writing and async pw-play wrapper
  media_duck.py      Async pactl volume control for media ducking
  voice_identity.py  Project-to-voice mapping with round-robin assignment
  settings.py        Pydantic settings from env vars
  engines/
    base.py          TTSEngine abstract base
    kokoro.py        Kokoro ONNX engine
    piper.py         Wyoming protocol client
    orpheus.py       llama-server + SNAC decoder
```

See [CLAUDE.md](CLAUDE.md) for the full technical reference -- design decisions, progress lifecycle, cancellation semantics, and the Python 3.13 `asyncio.wait_for` pitfall that cost a few hours of debugging.

## License

MIT
