11 Commits

Author SHA1 Message Date
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
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
d054caa43c Rename project to mcspeak, add README
Rename all user-facing references from tts-mcp to mcspeak: service name,
container, compose project, Dockerfile CMD, FastMCP instance, MCP config,
startup logs, temp dir, and CLAUDE.md header. Python module stays tts_mcp
(import name != package name is standard practice).

README covers quick start, MCP tools, engines, features, config, Docker
setup, and architecture.
2026-03-04 19:46:25 -07:00
70511916b9 Add media ducking: fade external audio during TTS playback
Duck all PulseAudio sink-inputs (Firefox, Spotify, etc.) before speech,
crossfade the entry tone over the fading media, then restore volumes
after the exit tone. Uses pactl subprocesses for reliable PipeWire
compatibility — pulsectl-asyncio's native protocol writes are silently
dropped by PipeWire's PA compat layer.

New module media_duck.py with MediaDucker class. Unduck is consolidated
into the consumer's finally block to guarantee restoration on all exit
paths (normal, error, cancel, shutdown).
2026-03-03 21:38:56 -07:00
79bfe2a89b Fix sequential timeout stacking in shutdown budget
The same shutdown_timeout was used for both uvicorn handler drain AND
queue.stop() consumer drain — sequential phases sharing Docker's
wall-clock budget. With 30s each, worst case was 60s, exceeding
the 35s stop_grace_period and causing SIGKILL.

Fix: uvicorn gets a fixed 3s drain (handlers just re-raise), queue
gets the full shutdown_timeout. Docker grace = 3 + timeout + 5s safety.

Also adds shutdown observability: startup logs the timing chain,
queue.stop() logs remaining audio vs available budget.
2026-03-02 18:28:04 -07:00
7cae72b936 Graceful shutdown, blocking speak() with progress, and cancel support
speak() now blocks until playback finishes, reporting progress via SSE
(5% entry tone → 30% synthesis → 35-99% playing → 100% done). Entry
tone fires immediately on call to cover synthesis latency.

Queue shutdown waits for current speech to finish (configurable timeout,
default 30s) before draining pending items — no more mid-sentence
cutoffs on container restart.

Cancellation via cancel_speech() tool or MCP notifications/cancelled
kills pw-play and plays a vinyl scratch tone. Consumer continues to
next item after cancel.

Progress tracking uses a background ticker task instead of
asyncio.wait_for polling — the latter causes stale CancelledError
propagation to the consumer under Python 3.13.
2026-03-02 17:45:48 -07:00
44d4f5a3d6 Rename Caddy domain to mctalkbox.l.supported.systems 2026-03-02 15:30:57 -07:00
eaff3e8861 Add project-aware voice identity with round-robin assignment
Each project gets a distinct voice from a curated English pool,
assigned via round-robin with gender/accent interleaving for
maximum perceptual contrast between consecutive projects.

- New voice_identity.py: pool filtering, interleaving, persistence
- Round-robin replaces SHA-256 hashing (no collisions until pool
  exhaustion at 22 voices)
- Assignments persist to /data/voice-assignments.json across restarts
- speak() and generate_audio() accept optional project= parameter
- MCP roots fallback with 2s timeout for future bidirectional clients
- English-only pool (af_/am_/bf_/bm_/ef_/em_ prefixes)
- af_nicole excluded from auto-assign (whispery), still explicit-ok
- Fix voice blacklist to use full identifiers (am_adam, af_jessica)
2026-02-24 11:58:10 -07:00
538b8a513e Replace Ollama with llama-server for 15x Orpheus throughput
Build llama.cpp from source with SM 120 CUDA kernels and FORCE_CUBLAS
for RTX 5070 Blackwell. Rewrite OrpheusEngine to stream tokens via SSE
and decode SNAC in overlapping 28-token batches (4 frames), replacing
the blocking requests+stream:false approach.

Performance: 13.5 → 170-213 tok/s. 100s audio generates in ~48s (2x
faster than realtime). Replaces requests with httpx async client.

Also switch MCP transport to stateless_http mode so container restarts
don't invalidate client sessions.
2026-02-21 21:33:23 -07:00
4698d8b0d2 Lazy-load SNAC decoder to reduce idle memory
SNAC + torch no longer load at startup — deferred to first Orpheus
call via double-checked locking. Startup drops from ~13s to 0.5s,
idle RAM reduced by ~200MB. OrpheusEngine constructor no longer
takes snac_model; it self-loads on demand.
2026-02-21 13:04:35 -07:00
c53db4b251 Add Docker deployment with Caddy HTTPS reverse proxy
CPU-only torch image (~180MB vs 873MB CUDA), PipeWire socket
passthrough for audio playback, SNAC HuggingFace cache volume.
Served at voice.l.supported.systems via caddy-docker-proxy.
2026-02-20 20:47:22 -07:00