"""Silero TTS helper for J.A.R.V.I.S. Reads text from stdin, synthesises with snakers4/silero-models, writes the result to a temp wav, and prints the path on stdout. The Rust side then plays + deletes it. Install: pip install torch soundfile numpy Usage (called by jarvis-core/src/tts/silero.rs): echo "привет" | python silero_tts.py --voice xenia Voices for ru_v3: xenia (female, default), baya (female), aidar (male), eugene (male), kseniya (female). """ import argparse import os import sys import tempfile try: import torch # type: ignore except ImportError: print("silero_tts: torch not installed. Run `pip install torch`.", file=sys.stderr) sys.exit(2) import warnings warnings.filterwarnings("ignore") _MODEL = None def load_model(device: str = "cpu"): global _MODEL if _MODEL is None: torch.set_num_threads(4) _MODEL, _ = torch.hub.load( repo_or_dir="snakers4/silero-models", model="silero_tts", language="ru", speaker="v3_1_ru", trust_repo=True, ) _MODEL.to(torch.device(device)) return _MODEL def synth(text: str, voice: str, sample_rate: int = 48000) -> str: """Synth text → return path to temp wav.""" model = load_model() audio = model.apply_tts(text=text, speaker=voice, sample_rate=sample_rate) import numpy as np import soundfile as sf # type: ignore fd, path = tempfile.mkstemp(prefix="jarvis-silero-", suffix=".wav") os.close(fd) sf.write(path, audio.numpy().astype(np.float32), sample_rate) return path def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--voice", default="xenia", help="speaker name (xenia, baya, aidar, eugene, kseniya)") ap.add_argument("--sample-rate", type=int, default=48000) args = ap.parse_args() text = sys.stdin.read().strip() if not text: print("silero_tts: empty input", file=sys.stderr) return 1 try: path = synth(text, args.voice, args.sample_rate) except Exception as exc: print(f"silero_tts: synth failed: {exc}", file=sys.stderr) return 3 print(path) return 0 if __name__ == "__main__": sys.exit(main())