Add offline desktop speech with Silero Xenia for Russian

This commit is contained in:
sergeych committed 2026-09-28 13:45:00 +04:00
1 parent 9cd8ebc422
commit 9dbe30b20c
28 files changed
+1865 -16

No files matched your search

+58
View File
@@ -0,0 +1,58 @@
#!/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()