mcspeak/docker-compose.yml
Ryan Malloy 27b4c4ae29 Add generate_audio file/format params; bring Piper TTS up with es_MX default
generate_audio gains output_path + format params (wav, mp3, ogg, flac,
m4a). Non-wav formats route through ffmpeg (added to the Dockerfile).
Files land under /output/ inside the container, bind-mounted from the
host's TTS_OUTPUT_HOST_DIR (defaults to ~/mcspeak-out). Subdirectories
are auto-created. Path validation rejects writes outside /output/ —
both absolute escapes like /etc/passwd and traversal forms like
../../etc/passwd are canonicalized and checked against the mount root.
Extension auto-corrects to match the requested format.

piper-tts service added to compose using rhasspy/wyoming-piper. Default
voice es_MX-ald-medium is pre-warmed at container start and persists in
./piper-data/ across recreates (~60 MB download on first run). Port
10200 published to localhost so host-side scripts can also reach
Wyoming directly; mcspeak inside the stack uses container DNS. Voice
configurable via TTS_PIPER_VOICE — the same env var threads through to
both wyoming-piper's --voice flag and mcspeak's settings.piper_voice,
so they stay in sync.

PiperEngine.__init__ accepts a default_voice override; settings.piper_voice
threads it from the env. list_engines now reports the configured default
rather than the hardcoded en_US-lessac-medium. speak() / generate_audio()
calls with engine=piper and no explicit voice use the configured one.

mcspeak-internal network changed from internal:true to default-bridge.
internal:true was overkill — the goal was per-stack DNS scoping (which
compose provides automatically via the project-prefixed network name),
not internet isolation. The latter broke piper-tts's HuggingFace voice
download with "Name resolution failure" on first start.

.gitignore: add piper-data/ so the downloaded voice models stay out of
the repo.
2026-06-13 17:35:36 -06:00

123 lines
4.8 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
# Host-visible output dir for generate_audio's output_path/format params.
# Override TTS_OUTPUT_HOST_DIR in .env to use a different host directory.
- ${TTS_OUTPUT_HOST_DIR:-${HOME}/mcspeak-out}:/output
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}}"
piper-tts:
# Default-on. Wyoming protocol TTS, ~500 MB image + ~60 MB voice download
# on first run (cached in ./piper-data thereafter). Voice is configurable
# via TTS_PIPER_VOICE; the wyoming-piper server will auto-download any
# voice mcspeak requests at speak() time even if it's not the default.
image: rhasspy/wyoming-piper:latest
container_name: piper-tts
restart: unless-stopped
command: --voice ${TTS_PIPER_VOICE:-es_MX-ald-medium}
# Published to localhost so host-side scripts can hit Wyoming directly
# at 127.0.0.1:10200 if needed. mcspeak inside the stack reaches it via
# the container DNS name (TTS_PIPER_HOST=piper-tts above).
ports:
- "127.0.0.1:10200:10200"
volumes:
# Host-visible voice cache so models survive container recreates and
# are inspectable from the host (~/claude/mctalkbox/piper-data/).
- ./piper-data:/data
networks:
- mcspeak-internal
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 ↔ piper-tts ↔ llama-server.
# Auto-created by compose, scoped to this project so other stacks can't
# resolve our service names. NOT marked internal:true because piper-tts
# needs HuggingFace access to download voice models on first run; the
# per-project scoping is what we actually wanted, not internet isolation.
mcspeak-internal: {}