J.A.R.V.I.S-py/tts.py
Bossiara13 cdd331af35
Some checks failed
Python CI / pytest (ubuntu-latest) (push) Failing after 27s
Python CI / ruff (push) Failing after 5s
Python CI / pytest (windows-latest) (push) Has been cancelled
fix: Python startup no longer silent on first run
ROOT CAUSE

User reported the Python edition showed "только пустой терминал" — empty
terminal — when launched. Three compounding issues:

1. `tts.py` called `torch.hub.load(...)` at MODULE IMPORT time. On first
   use this downloads a 60MB Silero model with no progress output beyond
   one "Using cache found in..." line. With no prior banner the user
   couldn't tell if it crashed or was loading.
2. main.py's startup banner (`print("=" * 60)...J.A.R.V.I.S. v...`)
   appeared AFTER all the heavy imports. So even when nothing was wrong,
   visible feedback was delayed by 10-60 seconds.
3. If `pip install -r requirements.txt` failed for any package
   (`simpleaudio` notoriously needs MSVC to build), `import simpleaudio`
   crashed the whole process with a raw ModuleNotFoundError.

FIXES

- `tts.py`: Silero model load moved into `_ensure_model()` called lazily
  from `synthesize()`. First synth pays the load cost (with a one-line
  progress hint) — everything before that is visible immediately.
- `main.py`: print "J.A.R.V.I.S. loading..." banner BEFORE any heavy
  imports. User sees the terminal is alive within ~100ms.
- `main.py`: scan required deps before importing them; if any are
  missing, print "Run: pip install -r requirements.txt" with the
  specific list and exit cleanly (exit code 2) instead of stack-tracing.
- `main.py`: `simpleaudio` is now soft-optional. Falls back to stdlib
  `winsound` on Windows installs that couldn't build the C extension.
  Sound cues still work; only the precise wait_done semantics are
  slightly different.

VERIFIED LOCALLY

`.venv\Scripts\python.exe main.py` now prints:
```
============================================================
  J.A.R.V.I.S.  loading (Python edition)...
  Heavy modules (vosk / torch / pvrecorder) take a few seconds.
============================================================
[scheduler] thread started (1 tasks loaded)
============================================================
  J.A.R.V.I.S.  v0.4.1   [Python edition]
  Total commands: 200
============================================================
Using device: Микрофон (5- Fifine Microphone)
Jarvis (v0.4.1) начал свою работу ...
Yes, sir.
```

Total time to first banner: ~100ms (was 10-60s of nothing).
All 104 pytest tests still pass.
2026-05-24 22:12:24 +03:00

91 lines
2.8 KiB
Python

"""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.
"""
import time
import numpy as np
import sounddevice as sd
import torch
import config
import effects
language = 'ru'
model_id = 'ru_v3'
sample_rate = 48000 # 48000
speaker = 'aidar' # aidar, baya, kseniya, xenia, random
put_accent = True
put_yo = True
device = torch.device('cpu') # cpu или gpu
# 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',
model='silero_tts',
language=language,
speaker=model_id)
m.to(device)
_model = m
print(f"[tts] Silero ready ({time.time() - started:.1f}s).")
return _model
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):
m = _ensure_model()
audio = m.apply_tts(text=what + "..",
speaker=speaker,
sample_rate=sample_rate,
put_accent=put_accent,
put_yo=put_yo)
return _post_process(audio)
def va_speak(what: str):
audio = synthesize(what)
sd.play(audio, sample_rate * 1.05)
time.sleep((len(audio) / sample_rate) + 0.5)
sd.stop()