feat(tts): real Korean TTS via persistent MeloTTS worker

Adds a MeloTTS backend that runs the model in its own melo311 interpreter
as a long-lived worker (melo_worker.py), loaded once and fed synthesis
requests over a stdin/stdout JSON protocol. fd1 is split from fd2 in the
worker so MeloTTS's stdout progress chatter can't corrupt the protocol.
Each speak() writes a wav and hands the path to a pluggable sink (the
Discord voice step will swap in "play into the call"). factory wires
tts=melo; pipeline.aclose now also tears down the tts worker.

Verified (CPU): model load ~7.9s once, then a short reply synthesizes in
~0.86s (within the ~1s budget); wav is valid 44.1kHz PCM. GPU (cuda) is
selectable via WSAI_MELO_DEVICE for lower latency, pending GPU approval.
7 smoke tests still pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
EJClaw
2026-08-18 18:14:44 +09:00
parent 4898192ae1
commit 6a138eff3a
5 changed files with 191 additions and 3 deletions

View File

@@ -162,8 +162,10 @@ class Pipeline:
await self.aclose()
async def aclose(self) -> None:
for closer in (self.source, self.stt, self.text_channel):
if closer is not None:
# tts is included because a real TTS (e.g. MeloTTS) owns a worker
# subprocess that must be torn down; mock backends have no aclose.
for closer in (self.source, self.stt, self.text_channel, self.tts):
if closer is not None and hasattr(closer, "aclose"):
try:
await closer.aclose()
except Exception: