#!/usr/bin/env python3 import ctypes import ctypes.util import json import os import struct import subprocess import sys ASSET_ROOT = "/opt/roverd/googletts" LIB_PATH = os.path.join(ASSET_ROOT, "libchrometts.so") VOICE_DIR = os.path.join(ASSET_ROOT, "en-us-x-multi-r30") PIPELINE = "pipeline.pb" PLAYBACK_DEVICE = "tts" SAMPLE_RATE = "24000" MAX_TEXT_CHARS = 512 VOICES = { "sfg": "female", "iob": "female", "iog": "female", "iol": "male", "iom": "male", "tpc": "female", "tpd": "male", "tpf": "female", } DEFAULT_VOICE = "tpf" DEFAULT_PITCH = 1.0 DEFAULT_SPEED = 1.0 MIN_PITCH = 0.5 MAX_PITCH = 2.0 MIN_SPEED = 0.5 MAX_SPEED = 2.0 _runtime_handles = [] def load_shared_library(path): mode = ctypes.RTLD_GLOBAL | getattr(os, "RTLD_NOW", 0) return ctypes.CDLL(path, mode=mode) def preload_runtime_libraries(): # Laptop-only workaround: some ChromeOS libchrometts builds reference # compiler helper symbols such as __udivmodti4 without declaring the runtime # library as an ELF dependency. Loading common compiler runtimes globally # first makes those symbols visible before ctypes loads libchrometts.so. for name in ("gcc_s", "atomic", "stdc++", "c++", "c++abi"): lib = ctypes.util.find_library(name) if not lib: continue try: _runtime_handles.append(load_shared_library(lib)) except OSError: pass preload_runtime_libraries() def varint(value): out = bytearray() while value >= 0x80: out.append((value & 0x7F) | 0x80) value >>= 7 out.append(value) return bytes(out) def field_bytes(number, payload): return varint((number << 3) | 2) + varint(len(payload)) + payload def field_float(number, value): return varint((number << 3) | 5) + struct.pack(" 0: frames = int(frames_written.value) if frames > 0: player.stdin.write(ctypes.string_at(self.buffer, frames * ctypes.sizeof(ctypes.c_float))) player.stdin.close() rc = player.wait() if rc != 0: raise RuntimeError(f"aplay exited with {rc}") finally: if player.poll() is None: player.kill() player.wait() def shutdown(self): self.lib.GoogleTtsShutdown() def respond(payload): sys.stdout.write(json.dumps(payload, separators=(",", ":")) + "\n") sys.stdout.flush() def clamp_float(value, minimum, maximum, fallback): try: value = float(value) except (TypeError, ValueError): return fallback if value <= 0: return fallback if value < minimum: return minimum if value > maximum: return maximum return value def main(): try: tts = ChromeTTS() except Exception as exc: respond({"ok": False, "error": str(exc)}) return 1 respond({"ok": True, "ready": True}) try: for line in sys.stdin: line = line.strip() if not line: continue try: request = json.loads(line) tts.speak_to_aplay( str(request.get("text") or ""), str(request.get("voice") or DEFAULT_VOICE), request.get("pitch", DEFAULT_PITCH), request.get("speed", DEFAULT_SPEED), ) respond({"ok": True}) except Exception as exc: respond({"ok": False, "error": str(exc)}) finally: tts.shutdown() return 0 if __name__ == "__main__": raise SystemExit(main())