mcspeak/docker-compose.yml
Ryan Malloy ec4e1b6093 Fix stream-restore mute + entry tone overlap, make Orpheus opt-in
Stream-restore mute: pw-play streams were being silently restored to
0% volume by PulseAudio's stream-restore module matching the music
role key, producing audible-but-clean-exit playback. audio.py now
bumps each new pw-play sink-input to 100% as a background task; the
new value re-stamps stream-restore on stream end.

Entry tone overlap: when speak() arrived during prior playback, its
entry tone played immediately from the handler and overlapped the
ongoing audio. queue.is_idle() now gates this — tone plays now when
idle (preserves latency-hiding intent), else defers to the consumer
via _WorkItem.entry_tone, which plays it right before the item's
audio. Chunked path defers only on chunk 0.

Docker: llama-server gated behind a `with-orpheus` compose profile;
mcspeak.depends_on uses required:false. `make up` defaults to
kokoro-only with no GPU dependency; `make up-with-orpheus` runs the
full stack. Rename dootie-internal network → mcspeak-internal
(auto-created per-stack via internal:true). Add 127.0.0.1:8371 port
mapping for local MCP clients.

README: fix claude mcp add command (was stdio, must be --transport
http); document kokoro-only default and the with-orpheus path; add
Kokoro model download step. .env.example added so `cp .env.example
.env` matches the documented setup.
2026-05-28 14:14:38 -06:00

97 lines
3.4 KiB
YAML

services:
mcspeak:
build: .
container_name: mcspeak
restart: unless-stopped
# Must exceed: 3s handler drain + TTS_SHUTDOWN_TIMEOUT + 5s safety margin.
# Default: 3 + 30 + 5 = 38s. Increase if TTS_SHUTDOWN_TIMEOUT > 30.
stop_grace_period: 38s
env_file: .env
# Publish to localhost only — Claude Code (and any local MCP client) talks
# to 127.0.0.1:8371. External HTTPS access still flows through the caddy
# label below if caddy-docker-proxy is running.
ports:
- "127.0.0.1:8371:8371"
environment:
# Override for Docker networking (container DNS instead of IPs)
TTS_PIPER_HOST: piper-tts
TTS_ORPHEUS_URL: http://llama-server:8081
# PipeWire client config
XDG_RUNTIME_DIR: /run/user/1000
# PulseAudio socket for media ducking (bypass XDG_RUNTIME_DIR ownership check)
PULSE_SERVER: unix:/run/user/1000/pulse/native
# Force unbuffered Python output so stderr shows up in docker logs immediately
PYTHONUNBUFFERED: "1"
volumes:
# Kokoro ONNX models (read-only)
- ./models:/app/models:ro
# HuggingFace cache for SNAC model download (lazy-loaded on first Orpheus call)
- hf-cache:/home/tts/.cache/huggingface
# Persistent data (voice assignments, etc.)
- tts-data:/data
# PipeWire socket for audio playback through host speakers
- /run/user/1000/pipewire-0:/run/user/1000/pipewire-0
# PulseAudio compat socket for media ducking (volume control)
- /run/user/1000/pulse:/run/user/1000/pulse
depends_on:
llama-server:
condition: service_healthy
# required: false lets mcspeak start without llama-server when the
# with-orpheus profile isn't active (kokoro-only mode is the default).
required: false
networks:
- caddy
- mcspeak-internal
labels:
caddy: mctalkbox.l.supported.systems
caddy.reverse_proxy: "{{upstreams 8371}}"
llama-server:
# Opt-in: only starts when `docker compose --profile with-orpheus up`.
# Default `make up` runs kokoro-only without GPU dependencies.
profiles: ["with-orpheus"]
build:
context: .
dockerfile: llama-server.Dockerfile
container_name: orpheus-llama-server
restart: unless-stopped
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
volumes:
# GGUF model file (set ORPHEUS_GGUF_PATH in .env, e.g. from Ollama blob
# storage). The /dev/null fallback lets compose parse cleanly when the
# with-orpheus profile is inactive; activating the profile without
# setting ORPHEUS_GGUF_PATH will produce a clear "model load failed" at
# llama-server startup rather than a confusing compose parse error.
- ${ORPHEUS_GGUF_PATH:-/dev/null}:/models/orpheus.gguf:ro
command: >-
--host 0.0.0.0 --port 8081
--model /models/orpheus.gguf
--n-gpu-layers 999 --ctx-size 4096
--flash-attn --cont-batching
networks:
- mcspeak-internal
healthcheck:
test: ["CMD", "curl", "-sf", "http://127.0.0.1:8081/health"]
interval: 15s
timeout: 5s
start_period: 120s
retries: 5
volumes:
hf-cache:
tts-data:
networks:
caddy:
external: true
# Private per-stack network for mcspeak ↔ llama-server. Auto-created by
# compose, isolated from other stacks (no DNS leak via shared caddy).
mcspeak-internal:
internal: true