MeloTTS voice output, signal handling, auto pipeline
- melo_server.py subprocess (MeloTTS v3 EN-Newest, speed 1.3): line protocol stdin->stdout, model loaded once, warmup before READY - src/melo.zig: lifecycle ownership (spawn/kill with inferon), line protocol client, reply sanitizer (newlines/markdown stripped before synthesis) - SIGINT/SIGTERM handlers: async-signal-safe kill of whisper/melo/recorder, default disposition restored + re-raise for truthful exit status - processUtterance: transcribe->infer->speak chains automatically after stop-click; mid-pipeline clicks are no-ops; back to idle after reply - temp cleanup: recording WAV deleted after transcription, TTS WAV after playback - whisper: pid() export for signal handler 👾 Generated with [Letta Code](https://letta.com) Co-Authored-By: Letta Code <noreply@letta.com>
This commit is contained in:
83
scripts/melo_server.py
Normal file
83
scripts/melo_server.py
Normal file
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env python3
|
||||
"""MeloTTS server for inferon.
|
||||
|
||||
Long-running subprocess: loads the MeloTTS model once, then reads lines of
|
||||
text on stdin and writes "<wav-path> <duration-seconds>" per line on stdout.
|
||||
stderr passes through for logging.
|
||||
|
||||
Protocol:
|
||||
-> one line of text to synthesize
|
||||
<- e.g. "/tmp/inferon/tts-1723.wav 3.42"
|
||||
|
||||
Kill with SIGTERM — no cleanup needed, WAVs live in /tmp.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import time
|
||||
import signal
|
||||
import warnings
|
||||
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
MELO_ROOT = os.path.expanduser("~/Projects/MeloTTS")
|
||||
CKPT = os.path.join(MELO_ROOT, "ckpt/MeloTTS-English-v3/checkpoint.pth")
|
||||
CONFIG = os.path.join(MELO_ROOT, "ckpt/MeloTTS-English-v3/config.json")
|
||||
OUT_DIR = "/tmp/inferon"
|
||||
SPEAKER = "EN-Newest" # v3 English: single speaker (British-accented female)
|
||||
LANG = "EN"
|
||||
SPEED = 1.3 # speech rate; 1.0 = natural, higher = faster
|
||||
|
||||
sys.path.insert(0, MELO_ROOT)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
signal.signal(signal.SIGTERM, lambda *_: sys.exit(0))
|
||||
signal.signal(signal.SIGINT, lambda *_: sys.exit(0))
|
||||
|
||||
os.makedirs(OUT_DIR, exist_ok=True)
|
||||
|
||||
# Heavy imports inside main so signal handlers are installed first.
|
||||
from melo.api import TTS
|
||||
|
||||
model = TTS(language=LANG, config_path=CONFIG, ckpt_path=CKPT)
|
||||
|
||||
speaker_ids = model.hps.data.spk2id
|
||||
if SPEAKER not in speaker_ids:
|
||||
# English v3 fallback: use the first speaker and say so on stderr.
|
||||
fallback = next(iter(speaker_ids))
|
||||
print(f"melo_server: speaker {SPEAKER!r} not in {list(speaker_ids)}; using {fallback!r}",
|
||||
file=sys.stderr, flush=True)
|
||||
speaker = fallback
|
||||
else:
|
||||
speaker = speaker_ids[SPEAKER]
|
||||
|
||||
# Warm up with a full sentence (longer text paths hit NLTK/num2words,
|
||||
# which is where missing-data errors surface). Fail before READY.
|
||||
warmup = "/tmp/inferon/tts-warmup.wav"
|
||||
model.tts_to_file("Warmup sentence number twelve, spoken on the first of January.",
|
||||
speaker, warmup, speed=SPEED)
|
||||
os.unlink(warmup)
|
||||
|
||||
# Ready marker — inferon waits for this before declaring the backend up.
|
||||
print("READY", flush=True)
|
||||
|
||||
for line in sys.stdin:
|
||||
text = line.strip()
|
||||
if not text:
|
||||
continue
|
||||
text = text[:2000] # protocol cap — extremely long replies get clipped
|
||||
|
||||
wav_path = os.path.join(OUT_DIR, f"tts-{time.time_ns()}.wav")
|
||||
try:
|
||||
start = time.monotonic()
|
||||
model.tts_to_file(text, speaker, wav_path, speed=SPEED)
|
||||
dur = time.monotonic() - start
|
||||
print(f"{wav_path} {dur:.2f}", flush=True)
|
||||
except Exception as e: # noqa: BLE001 - protocol: report and survive
|
||||
print(f"melo_server: synthesis failed: {e}", file=sys.stderr, flush=True)
|
||||
print("ERROR", flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user