#!/usr/bin/env python3 """Workspace-local RuAccent CPU setup. No sudo or changes to system Python.""" import hashlib import io import json import os from pathlib import Path import platform import runpy import shutil import subprocess import urllib.request import zipfile ROOT = Path(__file__).resolve().parent.parent def main(): uv = shutil.which("uv") or str(ROOT / ".local/tools/uv") if not Path(uv).is_file(): if platform.system() != "Linux" or platform.machine() != "x86_64": raise SystemExit("Install uv first on this platform, then rerun this setup script.") with urllib.request.urlopen("https://pypi.org/pypi/uv/0.12.19/json", timeout=60) as source: metadata = json.load(source) wheel = next(x for x in metadata["urls"] if x["filename"].endswith("manylinux_2_17_x86_64.manylinux2014_x86_64.whl")) with urllib.request.urlopen(wheel["url"], timeout=60) as source: content = source.read() expected = "a63d18a0aa38ee9f21a5406afbbaeb41303bcd954be9d6b7c1b95ac275e53958" if hashlib.sha256(content).hexdigest() != expected: raise RuntimeError("uv checksum mismatch") with zipfile.ZipFile(io.BytesIO(content)) as archive: name = next(x for x in archive.namelist() if x.endswith("/uv")) Path(uv).parent.mkdir(parents=True, exist_ok=True) Path(uv).write_bytes(archive.read(name)) Path(uv).chmod(0o755) environment = dict(os.environ, UV_CACHE_DIR=str(ROOT / ".local/uv-cache"), UV_PYTHON_INSTALL_DIR=str(ROOT / ".local/python"), UV_SYSTEM_CERTS="true") venv = ROOT / ".local/ruaccent-runtime" python = venv / ("Scripts/python.exe" if os.name == "nt" else "bin/python") if not python.is_file(): subprocess.run([uv, "venv", "--python", "3.12.14", str(venv)], env=environment, check=True) subprocess.run([uv, "pip", "sync", "--python", str(python), str(ROOT / "scripts/ruaccent-requirements.txt")], env=environment, check=True) model = ROOT / ".local/tts/ruaccent" manifest = json.loads((ROOT / "scripts/ruaccent-model.json").read_text()) runpy.run_path(str(ROOT / "scripts/download-tts-model.py"))["download"](manifest, model) # RUAccent 1.5.8.3 expects its rule-engine files beside the installed package. module_path = subprocess.check_output([str(python), "-c", "import importlib.util; print(importlib.util.find_spec('ruaccent').submodule_search_locations[0])"], text=True).strip() shutil.copytree(model / "koziev", Path(module_path) / "koziev", dirs_exist_ok=True) worker = ROOT / "composeApp/src/jvmMain/resources/tts/ruaccent-worker.py" shutil.copyfile(worker, model / "worker.py") # Verify a wholly offline initialization before declaring the installation ready. subprocess.run([str(python), str(model / "worker.py"), str(model), "--check"], check=True, env=environment) (model / "runtime.json").write_text(json.dumps({"python": str(python), "model": "turbo3.1", "revision": manifest["revision"]}, indent=2) + "\n") print("RuAccent ready: CPU, turbo3.1, full dictionary; no network during inference.", flush=True) if __name__ == "__main__": main()