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.
This commit is contained in:
parent
f102b8dc95
commit
cdd331af35
2 changed files with 107 additions and 15 deletions
76
main.py
76
main.py
|
|
@ -10,14 +10,66 @@ import time
|
||||||
import webbrowser
|
import webbrowser
|
||||||
from ctypes import POINTER, cast
|
from ctypes import POINTER, cast
|
||||||
|
|
||||||
|
# Print a startup banner BEFORE the heavy imports — otherwise the user
|
||||||
|
# sees an empty terminal for 10-60 seconds while torch / vosk / silero
|
||||||
|
# load. This used to be the #1 "did it crash?" support issue.
|
||||||
|
print("=" * 60, flush=True)
|
||||||
|
print(" J.A.R.V.I.S. loading (Python edition)...", flush=True)
|
||||||
|
print(" Heavy modules (vosk / torch / pvrecorder) take a few seconds.", flush=True)
|
||||||
|
print("=" * 60, flush=True)
|
||||||
|
|
||||||
|
# Helpful diagnostic: if a critical dep is missing, tell the user EXACTLY
|
||||||
|
# what to install instead of dumping a raw ModuleNotFoundError. Pulls
|
||||||
|
# every required package by name and reports them in one go.
|
||||||
|
_MISSING_DEPS = []
|
||||||
|
for _modname, _pkg in [
|
||||||
|
("numpy", "numpy"),
|
||||||
|
("openai", "openai>=1.0"),
|
||||||
|
("vosk", "vosk"),
|
||||||
|
("webrtcvad", "webrtcvad"),
|
||||||
|
("yaml", "PyYAML"),
|
||||||
|
("comtypes", "comtypes"),
|
||||||
|
("fuzzywuzzy", "fuzzywuzzy"),
|
||||||
|
("pvrecorder", "pvrecorder"),
|
||||||
|
("pycaw", "pycaw"),
|
||||||
|
("rich", "rich"),
|
||||||
|
]:
|
||||||
|
try:
|
||||||
|
__import__(_modname)
|
||||||
|
except ImportError:
|
||||||
|
_MISSING_DEPS.append(_pkg)
|
||||||
|
if _MISSING_DEPS:
|
||||||
|
print("\n[ERROR] Missing Python packages:", flush=True)
|
||||||
|
for _p in _MISSING_DEPS:
|
||||||
|
print(f" - {_p}", flush=True)
|
||||||
|
print("\nRun: pip install -r requirements.txt", flush=True)
|
||||||
|
print("Or use the bundled venv: launch via run.bat.\n", flush=True)
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import openai
|
import openai
|
||||||
from openai import OpenAI
|
from openai import OpenAI
|
||||||
import simpleaudio as sa
|
|
||||||
import vosk
|
import vosk
|
||||||
import webrtcvad
|
import webrtcvad
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
|
# simpleaudio needs MSVC to build from source; gracefully fall back to the
|
||||||
|
# stdlib's `winsound` on Windows when it isn't installed.
|
||||||
|
try:
|
||||||
|
import simpleaudio as sa
|
||||||
|
_HAS_SIMPLEAUDIO = True
|
||||||
|
except ImportError:
|
||||||
|
sa = None
|
||||||
|
_HAS_SIMPLEAUDIO = False
|
||||||
|
try:
|
||||||
|
import winsound as _winsound
|
||||||
|
except ImportError:
|
||||||
|
_winsound = None
|
||||||
|
if _winsound is None:
|
||||||
|
print("[WARN] Neither simpleaudio nor winsound available — sound cues disabled.", flush=True)
|
||||||
|
else:
|
||||||
|
print("[INFO] simpleaudio not installed; falling back to winsound for sound cues.", flush=True)
|
||||||
|
|
||||||
import extensions # new-command handlers ported from rust fork
|
import extensions # new-command handlers ported from rust fork
|
||||||
import plugins_store # user plugin packs under APP_CONFIG_DIR/plugins/
|
import plugins_store # user plugin packs under APP_CONFIG_DIR/plugins/
|
||||||
from comtypes import CLSCTX_ALL
|
from comtypes import CLSCTX_ALL
|
||||||
|
|
@ -154,14 +206,24 @@ def play(phrase, wait_done=True):
|
||||||
if wait_done:
|
if wait_done:
|
||||||
recorder.stop()
|
recorder.stop()
|
||||||
|
|
||||||
wave_obj = sa.WaveObject.from_wave_file(filename)
|
# Prefer simpleaudio (lets us wait_done precisely); fall back to winsound
|
||||||
play_obj = wave_obj.play()
|
# on installs that couldn't build the simpleaudio C extension.
|
||||||
|
if _HAS_SIMPLEAUDIO and sa is not None:
|
||||||
|
wave_obj = sa.WaveObject.from_wave_file(filename)
|
||||||
|
play_obj = wave_obj.play()
|
||||||
|
if wait_done:
|
||||||
|
play_obj.wait_done()
|
||||||
|
elif _winsound is not None:
|
||||||
|
flags = _winsound.SND_FILENAME
|
||||||
|
if not wait_done:
|
||||||
|
flags |= _winsound.SND_ASYNC
|
||||||
|
try:
|
||||||
|
_winsound.PlaySound(filename, flags)
|
||||||
|
except RuntimeError as exc:
|
||||||
|
print(f"[sound] winsound playback failed: {exc}")
|
||||||
|
# else: no audio backend — silently skip cue
|
||||||
|
|
||||||
if wait_done:
|
if wait_done:
|
||||||
play_obj.wait_done()
|
|
||||||
# time.sleep((len(wave_obj.audio_data) / wave_obj.sample_rate) + 0.5)
|
|
||||||
# print("END")
|
|
||||||
# time.sleep(0.5)
|
|
||||||
recorder.start()
|
recorder.start()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
46
tts.py
46
tts.py
|
|
@ -1,3 +1,14 @@
|
||||||
|
"""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 time
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
@ -14,13 +25,31 @@ speaker = 'aidar' # aidar, baya, kseniya, xenia, random
|
||||||
put_accent = True
|
put_accent = True
|
||||||
put_yo = True
|
put_yo = True
|
||||||
device = torch.device('cpu') # cpu или gpu
|
device = torch.device('cpu') # cpu или gpu
|
||||||
text = "Хауди Хо, друзья!!!"
|
|
||||||
|
|
||||||
model, _ = torch.hub.load(repo_or_dir='snakers4/silero-models',
|
# 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',
|
model='silero_tts',
|
||||||
language=language,
|
language=language,
|
||||||
speaker=model_id)
|
speaker=model_id)
|
||||||
model.to(device)
|
m.to(device)
|
||||||
|
_model = m
|
||||||
|
print(f"[tts] Silero ready ({time.time() - started:.1f}s).")
|
||||||
|
return _model
|
||||||
|
|
||||||
|
|
||||||
def _post_process(audio):
|
def _post_process(audio):
|
||||||
|
|
@ -45,11 +74,12 @@ def _post_process(audio):
|
||||||
|
|
||||||
|
|
||||||
def synthesize(what: str):
|
def synthesize(what: str):
|
||||||
audio = model.apply_tts(text=what + "..",
|
m = _ensure_model()
|
||||||
speaker=speaker,
|
audio = m.apply_tts(text=what + "..",
|
||||||
sample_rate=sample_rate,
|
speaker=speaker,
|
||||||
put_accent=put_accent,
|
sample_rate=sample_rate,
|
||||||
put_yo=put_yo)
|
put_accent=put_accent,
|
||||||
|
put_yo=put_yo)
|
||||||
return _post_process(audio)
|
return _post_process(audio)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue