7 Commits

Author SHA1 Message Date
7959efbd51 Add tool annotations, progress messages, and chunked synthesis pipeline
Chunked synthesis splits long texts into sentences and pipelines
synthesis with playback — first audio plays within ~1-2s instead of
waiting for full synthesis. Queue supports suppress_exit_tone between
chunks for seamless playback.

Tool annotations (ToolAnnotations) signal behavioral hints to MCP
clients: speak() is openWorldHint=True (physical speakers),
cancel_speech() is destructiveHint=True, read-only tools marked
accordingly.

Progress notifications now include message= strings that tell LLM
clients they can continue working during playback. Server instructions
corrected from "returns immediately" to accurately describe blocking
behavior with parallel-safe guidance. speak() docstring updated to
encourage concurrent tool calls.
2026-03-03 18:00:11 -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
6a81e0760a Add exit tone (roger beep) and standby tone after speech
Queue-aware exit tones: descending roger beep when queue empties
("over"), ascending standby blip when more items are queued ("standby,
more coming"). Includes quindar-out (2475 Hz) as Apollo-themed
alternative. Configurable via TTS_EXIT_TONE env var.

Also adds CLAUDE.md documenting the full tone system.
2026-02-23 14:44:27 -07:00
25de529bf4 Add entry tone (Nextel chirp) before queued speech playback
Generate 48kHz WAV tones at startup (chirp and apollo/quindar) using
numpy. The queue consumer plays the selected tone before each speech
item via pw-play. Configurable via TTS_ENTRY_TONE env var: chirp
(default), apollo, none, or path to a custom WAV file.
2026-02-23 14:40:36 -07:00
48b518771d Refactor speech queue: producer-consumer with priority and timeouts
Replace asyncio.Lock with bounded PriorityQueue + dedicated consumer
coroutine. Addresses Hamilton review findings:

- Playback timeout: pw-play subprocess killed if it exceeds 2x
  expected duration + margin (prevents deadlock on PipeWire hang)
- Bounded queue: max 20 items with backpressure rejection
- Priority tiers: urgent messages jump ahead of normal items
- Graceful shutdown: consumer cancelled, pending items drained,
  in-flight subprocess killed on lifespan teardown
- Structured errors: all failure modes return dicts, not raw exceptions
2026-02-21 15:56:01 -07:00
2fd84f0df7 Initial TTS MCP server with 3 engines
FastMCP 3.0 Streamable HTTP server exposing Piper (Wyoming/Docker),
Kokoro (ONNX), and Orpheus (Ollama+SNAC) as MCP tools. Includes a
FIFO speech queue so concurrent agents don't talk over each other —
waiting callers get queue position updates via ctx.info().

Tools: speak, generate_audio, list_voices, list_engines
Resource: audio://recent
2026-02-20 18:10:15 -07:00