hollow / voice_worker.py
Pabloler21's picture
fix(voice): keep worker protocol on a clean fd so Kokoro load noise can't corrupt READY
7358304
Raw
History Blame Contribute Delete
2.42 kB
"""Voice synthesis worker — runs Kokoro-82M in a SEPARATE process with CUDA
hidden, so the CPU TTS never initializes a CUDA context in the main app process.
On ZeroGPU that isolation is mandatory: any CUDA init in the main process
poisons the @spaces.GPU worker fork ("No CUDA GPUs are available") and every
model turn dies. Kokoro probes/initializes CUDA on import even with
device="cpu" (the `spaces` patch makes torch.cuda.is_available() True), so the
only reliable fix is to keep it in its own process where CUDA_VISIBLE_DEVICES="".
Protocol (binary, line-based over stdio): the parent writes one line =
base64(utf-8 cleaned text); the worker replies one line = base64(float32 PCM
bytes) or the literal b"ERR". A first b"READY" line signals Kokoro has loaded.
"""
import os
# hide every GPU from this process BEFORE torch is imported (Kokoro -> torch)
os.environ["CUDA_VISIBLE_DEVICES"] = ""
os.environ.setdefault("OMP_NUM_THREADS", "4")
os.environ.setdefault("OPENBLAS_NUM_THREADS", "4")
os.environ.setdefault("MKL_NUM_THREADS", "4")
# Reserve the REAL stdout (fd 1) as the protocol pipe to the parent, then point
# fd 1 at stderr so Kokoro/torch/tqdm prints during model load + synth go to
# stderr and can't corrupt the line-based protocol the parent reads on stdout.
_PROTO_FD = os.dup(1)
os.dup2(2, 1)
import base64
import sys
import numpy as np
_VOICE = "af_nicole"
_SPEED = 0.94
def _out(line: bytes) -> None:
os.write(_PROTO_FD, line + b"\n")
try:
from kokoro import KPipeline
_PIPELINE = KPipeline(lang_code="a", device="cpu")
except Exception as e: # no kokoro/model -> parent goes silent
sys.stderr.write(f"[voice-worker] disabled ({e!r})\n")
sys.exit(1)
_out(b"READY")
for raw in sys.stdin.buffer: # one request per line
raw = raw.strip()
if not raw:
continue
try:
text = base64.b64decode(raw).decode("utf-8")
audio = None
for _, _, chunk in _PIPELINE(text, voice=_VOICE, speed=_SPEED):
c = np.asarray(chunk, dtype=np.float32)
audio = c if audio is None else np.concatenate([audio, c])
if audio is None or audio.size == 0:
_out(b"ERR")
else:
_out(base64.b64encode(np.asarray(audio, dtype=np.float32).tobytes()))
except Exception as e:
sys.stderr.write(f"[voice-worker] synth failed: {e!r}\n")
_out(b"ERR")