b459533770
Self-contained HTTP service with engine, data, start script, systemd unit, and documentation. Runs independently of the Discord bot on its fixed port.
102 lines
3.2 KiB
Python
102 lines
3.2 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
VOX TTS Server — standalone HTTP API for Half-Life VOX engine.
|
|
Runs on port 33003. GET /say?text=...&pack=vox returns WAV audio.
|
|
"""
|
|
import os
|
|
import sys
|
|
import io
|
|
import wave
|
|
from pathlib import Path
|
|
from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
from urllib.parse import urlparse, parse_qs
|
|
|
|
# Engine lives alongside this server (self-contained)
|
|
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
sys.path.insert(0, str(SCRIPT_DIR))
|
|
|
|
from vox_engine import VOX_PACKS, get_available_words, get_available_packs
|
|
|
|
PORT = int(os.environ.get("PORT", 33003))
|
|
|
|
|
|
def generate_wav(text, pack="vox"):
|
|
"""Concatenate word WAVs and return raw WAV bytes."""
|
|
pack_dir = VOX_PACKS.get(pack)
|
|
if not pack_dir or not pack_dir.exists():
|
|
raise RuntimeError(f"VOX pack '{pack}' not found")
|
|
|
|
words = text.lower().split()
|
|
splice = bytes()
|
|
sample_rate = 11025
|
|
sample_width = 1
|
|
channels = 1
|
|
found = False
|
|
|
|
for word in words:
|
|
wav_path = pack_dir / f"{word}.wav"
|
|
if not wav_path.exists():
|
|
continue
|
|
with wave.open(str(wav_path), 'rb') as snd:
|
|
sample_rate = snd.getframerate()
|
|
sample_width = snd.getsampwidth()
|
|
channels = snd.getnchannels()
|
|
splice += snd.readframes(snd.getnframes())
|
|
found = True
|
|
|
|
if not found:
|
|
raise RuntimeError("No matching VOX words found")
|
|
|
|
buf = io.BytesIO()
|
|
with wave.open(buf, 'wb') as out:
|
|
out.setnchannels(channels)
|
|
out.setsampwidth(sample_width)
|
|
out.setframerate(sample_rate)
|
|
out.writeframes(splice)
|
|
return buf.getvalue()
|
|
|
|
|
|
class Handler(BaseHTTPRequestHandler):
|
|
def do_GET(self):
|
|
parsed = urlparse(self.path)
|
|
if parsed.path == "/say":
|
|
params = parse_qs(parsed.query)
|
|
text = params.get("text", [""])[0]
|
|
if not text:
|
|
self.send_error(400, "Missing 'text' parameter")
|
|
return
|
|
pack = params.get("pack", ["vox"])[0]
|
|
try:
|
|
wav_bytes = generate_wav(text, pack)
|
|
self.send_response(200)
|
|
self.send_header("Content-Type", "audio/wav")
|
|
self.send_header("Content-Length", str(len(wav_bytes)))
|
|
self.end_headers()
|
|
self.wfile.write(wav_bytes)
|
|
except Exception as e:
|
|
self.send_error(500, str(e))
|
|
elif parsed.path == "/health":
|
|
self.send_response(200)
|
|
self.send_header("Content-Type", "text/plain")
|
|
self.end_headers()
|
|
self.wfile.write(b"ok")
|
|
elif parsed.path == "/packs":
|
|
packs = get_available_packs()
|
|
body = ",".join(packs).encode()
|
|
self.send_response(200)
|
|
self.send_header("Content-Type", "text/plain")
|
|
self.end_headers()
|
|
self.wfile.write(body)
|
|
else:
|
|
self.send_error(404)
|
|
|
|
def log_message(self, format, *args):
|
|
print(f"[VOX] {args[0]}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
print(f"[VOX] Starting server on port {PORT}...")
|
|
server = HTTPServer(("0.0.0.0", PORT), Handler)
|
|
print(f"[VOX] Ready — http://127.0.0.1:{PORT}/say?text=hello+world")
|
|
server.serve_forever()
|