106 lines
3.3 KiB
Python
106 lines
3.3 KiB
Python
#!/usr/bin/env python3
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import contextlib
|
|
import json
|
|
import os
|
|
import socket
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
|
|
ROOT_DIR = Path(__file__).resolve().parents[1]
|
|
WORKSPACE_DIR = Path(os.getenv("NANOBOT_WORKSPACE", str(Path.home() / ".nanobot"))).expanduser()
|
|
LOG_DIR = WORKSPACE_DIR / "logs"
|
|
SOCKET_PATH = Path(os.getenv("MELO_TTS_SOCKET", str(WORKSPACE_DIR / "melotts.sock"))).expanduser()
|
|
SERVER_SCRIPT = ROOT_DIR / "scripts" / "melotts_server.py"
|
|
SERVER_LOG_PATH = LOG_DIR / "melotts-server.log"
|
|
DEFAULT_STARTUP_TIMEOUT_S = float(os.getenv("MELO_TTS_SERVER_STARTUP_TIMEOUT_S", "120"))
|
|
|
|
|
|
def _build_parser() -> argparse.ArgumentParser:
|
|
parser = argparse.ArgumentParser(description="Nanobot MeloTTS command adapter.")
|
|
parser.add_argument("--text", required=True)
|
|
parser.add_argument("--output-wav", required=True)
|
|
parser.add_argument("--socket-path", default=str(SOCKET_PATH))
|
|
return parser
|
|
|
|
|
|
def _rpc(socket_path: Path, payload: dict[str, Any], timeout_s: float = 10.0) -> dict[str, Any]:
|
|
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
sock.settimeout(timeout_s)
|
|
try:
|
|
sock.connect(str(socket_path))
|
|
sock.sendall((json.dumps(payload) + "\n").encode("utf-8"))
|
|
response = sock.recv(8192).decode("utf-8", errors="replace").strip()
|
|
finally:
|
|
sock.close()
|
|
if not response:
|
|
raise RuntimeError("empty response from MeloTTS server")
|
|
return json.loads(response)
|
|
|
|
|
|
def _ping(socket_path: Path) -> bool:
|
|
try:
|
|
response = _rpc(socket_path, {"action": "ping"}, timeout_s=2.0)
|
|
except Exception:
|
|
return False
|
|
return bool(response.get("ok"))
|
|
|
|
|
|
def _ensure_server(socket_path: Path) -> None:
|
|
if _ping(socket_path):
|
|
return
|
|
|
|
with contextlib.suppress(FileNotFoundError):
|
|
socket_path.unlink()
|
|
|
|
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
with SERVER_LOG_PATH.open("a", encoding="utf-8") as log_handle:
|
|
proc = subprocess.Popen(
|
|
[sys.executable, str(SERVER_SCRIPT), "--socket-path", str(socket_path)],
|
|
cwd=str(ROOT_DIR),
|
|
stdin=subprocess.DEVNULL,
|
|
stdout=log_handle,
|
|
stderr=subprocess.STDOUT,
|
|
start_new_session=True,
|
|
)
|
|
|
|
deadline = time.time() + DEFAULT_STARTUP_TIMEOUT_S
|
|
while time.time() < deadline:
|
|
if _ping(socket_path):
|
|
return
|
|
exit_code = proc.poll()
|
|
if exit_code is not None:
|
|
raise RuntimeError(
|
|
f"MeloTTS server exited during startup with code {exit_code}. "
|
|
f"See {SERVER_LOG_PATH}"
|
|
)
|
|
time.sleep(0.5)
|
|
raise RuntimeError(f"MeloTTS server did not become ready within {DEFAULT_STARTUP_TIMEOUT_S:.0f}s")
|
|
|
|
|
|
def main() -> int:
|
|
args = _build_parser().parse_args()
|
|
socket_path = Path(args.socket_path).expanduser()
|
|
_ensure_server(socket_path)
|
|
response = _rpc(
|
|
socket_path,
|
|
{
|
|
"action": "synthesize",
|
|
"text": args.text,
|
|
"output_wav": args.output_wav,
|
|
},
|
|
timeout_s=max(30.0, DEFAULT_STARTUP_TIMEOUT_S),
|
|
)
|
|
if not response.get("ok"):
|
|
raise RuntimeError(str(response.get("error", "MeloTTS synthesis failed")))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|