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: {}