2026-05-24 22:12:24 +03:00
|
|
|
"""TTS — Silero ru_v3 voice synth with lazy initialisation.
|
|
|
|
|
|
|
|
|
|
Why lazy: torch.hub.load() blocks for 30-120 seconds on the first run
|
|
|
|
|
(downloading the model) and produces no progress output beyond a single
|
|
|
|
|
"Using cache found in..." line. If this runs at MODULE IMPORT time, the
|
|
|
|
|
whole jarvis-python startup looks frozen — exactly the bug the user hit.
|
|
|
|
|
|
|
|
|
|
Fix: defer the load until first synth call. main.py prints its startup
|
|
|
|
|
banner first, then this module loads on demand. Side effect: the first
|
|
|
|
|
"speak" is slow (3-4s), but every print before it is visible.
|
|
|
|
|
"""
|
2023-04-16 15:56:20 +05:00
|
|
|
import time
|
|
|
|
|
|
2026-04-22 19:58:42 +03:00
|
|
|
import numpy as np
|
2023-04-16 16:59:18 +05:00
|
|
|
import sounddevice as sd
|
|
|
|
|
import torch
|
|
|
|
|
|
2026-04-22 19:58:42 +03:00
|
|
|
import config
|
|
|
|
|
import effects
|
|
|
|
|
|
2023-04-16 15:56:20 +05:00
|
|
|
language = 'ru'
|
|
|
|
|
model_id = 'ru_v3'
|
2023-04-16 16:59:18 +05:00
|
|
|
sample_rate = 48000 # 48000
|
|
|
|
|
speaker = 'aidar' # aidar, baya, kseniya, xenia, random
|
2023-04-16 15:56:20 +05:00
|
|
|
put_accent = True
|
|
|
|
|
put_yo = True
|
2023-04-16 16:59:18 +05:00
|
|
|
device = torch.device('cpu') # cpu или gpu
|
2023-04-16 15:56:20 +05:00
|
|
|
|
2026-05-24 22:12:24 +03:00
|
|
|
# Initialised on first synthesize() call.
|
|
|
|
|
_model = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _ensure_model():
|
|
|
|
|
"""Load Silero on first use. Subsequent calls are no-ops.
|
|
|
|
|
|
|
|
|
|
Prints a one-line progress hint so the user can tell the assistant is
|
|
|
|
|
busy downloading rather than hung — critical for the first run when
|
|
|
|
|
the model isn't cached yet.
|
|
|
|
|
"""
|
|
|
|
|
global _model
|
|
|
|
|
if _model is not None:
|
|
|
|
|
return _model
|
|
|
|
|
print("[tts] Loading Silero ru_v3 (first use — ~30s if model isn't cached)...")
|
|
|
|
|
started = time.time()
|
|
|
|
|
m, _ = torch.hub.load(repo_or_dir='snakers4/silero-models',
|
2023-04-16 15:56:20 +05:00
|
|
|
model='silero_tts',
|
|
|
|
|
language=language,
|
|
|
|
|
speaker=model_id)
|
2026-05-24 22:12:24 +03:00
|
|
|
m.to(device)
|
|
|
|
|
_model = m
|
|
|
|
|
print(f"[tts] Silero ready ({time.time() - started:.1f}s).")
|
|
|
|
|
return _model
|
2023-04-16 15:56:20 +05:00
|
|
|
|
|
|
|
|
|
2026-04-22 19:58:42 +03:00
|
|
|
def _post_process(audio):
|
|
|
|
|
if not getattr(config, 'TTS_EFFECTS_ENABLED', False):
|
|
|
|
|
return audio
|
|
|
|
|
|
|
|
|
|
arr = np.asarray(audio, dtype=np.float32)
|
|
|
|
|
low = getattr(config, 'TTS_BANDPASS_LOW_HZ', 0)
|
|
|
|
|
high = getattr(config, 'TTS_BANDPASS_HIGH_HZ', 0)
|
|
|
|
|
bandpass = (low, high) if low and high and high > low else None
|
|
|
|
|
|
|
|
|
|
wet = float(getattr(config, 'TTS_REVERB_WET', 0.0))
|
|
|
|
|
decay = int(getattr(config, 'TTS_REVERB_DECAY_MS', 0))
|
|
|
|
|
reverb = (wet, decay) if wet > 0 and decay > 0 else None
|
|
|
|
|
|
|
|
|
|
pitch = int(getattr(config, 'TTS_PITCH_SEMITONES', 0))
|
|
|
|
|
|
|
|
|
|
return effects.process(arr, sample_rate,
|
|
|
|
|
bandpass=bandpass,
|
|
|
|
|
reverb=reverb,
|
|
|
|
|
pitch_semitones=pitch)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def synthesize(what: str):
|
2026-05-24 22:12:24 +03:00
|
|
|
m = _ensure_model()
|
|
|
|
|
audio = m.apply_tts(text=what + "..",
|
|
|
|
|
speaker=speaker,
|
|
|
|
|
sample_rate=sample_rate,
|
|
|
|
|
put_accent=put_accent,
|
|
|
|
|
put_yo=put_yo)
|
2026-04-22 19:58:42 +03:00
|
|
|
return _post_process(audio)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def va_speak(what: str):
|
|
|
|
|
audio = synthesize(what)
|
2023-04-16 15:56:20 +05:00
|
|
|
|
|
|
|
|
sd.play(audio, sample_rate * 1.05)
|
|
|
|
|
time.sleep((len(audio) / sample_rate) + 0.5)
|
|
|
|
|
sd.stop()
|