fix(wake): make the lazy-install path reachable on fresh installs

check_wake_word_requirements() gated 'available' on the audio probe,
but the probe imports sounddevice + numpy — two of the packages the
lazy installer would install. On a fresh machine deps_ok was False, so
audio_ok was always False and /wake on printed the manual pip hint and
bailed before the engine constructors' lazy_deps.ensure() could run.

Now the audio probe only runs once deps are installed; with deps
missing and lazy installs allowed (the default), /wake on proceeds and
ensure() installs the pinned engine deps in-process — no restart. The
manual pip hint remains for security.allow_lazy_installs=false, and a
mic hint still blocks when deps are present but no audio device works.
The CLI announces the one-time engine install so the pause is explained.
This commit is contained in:
Teknium 2026-07-27 11:56:47 -07:00
parent 625d39632a
commit 7a87c6ffd6
No known key found for this signature in database
3 changed files with 64 additions and 4 deletions

5
cli.py
View file

@ -12222,6 +12222,11 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
_cprint(f" {_DIM}{reqs['hint']}{_RST}")
return False
if announce and not reqs.get("deps_available", True):
# Fresh install: the engine constructor lazy-installs its deps
# (onnxruntime is a large wheel) — tell the user why this is slow.
_cprint(f"{_DIM}Installing wake word engine (first use — this may take a minute)...{_RST}")
self._wake_start_new_session = bool(cfg.get("start_new_session", True))
try:
start_listening(self._on_wake_word, owner=self, config=cfg)

View file

@ -112,6 +112,53 @@ def test_requirements_unavailable_without_audio(monkeypatch):
assert r["audio_available"] is False
def test_requirements_fresh_install_lazy_allowed(monkeypatch):
"""Deps missing + lazy installs allowed → available, so /wake on can
reach the engine constructor's ``lazy_deps.ensure()`` call.
Regression: the audio probe imports sounddevice/numpy packages the
lazy installer would fetch so gating ``available`` on it made the
lazy-install path unreachable on a fresh machine (the /wake on handler
printed the pip hint and bailed before ensure() ever ran).
"""
def _boom():
raise AssertionError("audio probe must not run while deps are missing")
monkeypatch.setattr(ww, "_audio_available", _boom)
monkeypatch.setattr("tools.lazy_deps.is_available", lambda f: False)
monkeypatch.setattr("tools.lazy_deps._allow_lazy_installs", lambda: True)
r = ww.check_wake_word_requirements({"provider": "openwakeword"})
assert r["available"] is True
assert r["deps_available"] is False
assert r["hint"] == ""
def test_requirements_fresh_install_lazy_disabled(monkeypatch):
"""Deps missing + lazy installs disabled → unavailable, with the manual
pip command as the remediation hint."""
monkeypatch.setattr(ww, "_audio_available", lambda: True)
monkeypatch.setattr("tools.lazy_deps.is_available", lambda f: False)
monkeypatch.setattr("tools.lazy_deps._allow_lazy_installs", lambda: False)
monkeypatch.setattr(
"tools.lazy_deps.feature_install_command", lambda f: f"uv pip install {f}"
)
r = ww.check_wake_word_requirements({"provider": "openwakeword"})
assert r["available"] is False
assert r["deps_available"] is False
assert "install" in r["hint"]
def test_requirements_deps_present_but_no_audio_hint(monkeypatch):
"""Once deps ARE installed, a failing audio probe blocks with a mic hint
(lazy installs can't fix a missing audio device)."""
monkeypatch.setattr(ww, "_audio_available", lambda: False)
monkeypatch.setattr("tools.lazy_deps.is_available", lambda f: True)
monkeypatch.setattr("tools.lazy_deps._allow_lazy_installs", lambda: True)
r = ww.check_wake_word_requirements({"provider": "openwakeword"})
assert r["available"] is False
assert "audio device" in r["hint"]
# ── openWakeWord engine (bundled model + base-model fetch) ───────────────

View file

@ -507,20 +507,28 @@ def check_wake_word_requirements(cfg: Optional[Dict[str, Any]] = None) -> Dict[s
else:
feature = "wake.openwakeword"
deps_ok = lazy_deps.is_available(feature)
audio_ok = _audio_available()
lazy_ok = lazy_deps._allow_lazy_installs()
# The audio probe imports sounddevice + numpy — two of the very packages
# the lazy installer would fetch — so it can only be trusted once the
# feature's deps are installed. On a fresh install (deps missing, lazy
# installs allowed) we defer the mic check: the engine constructors call
# ``lazy_deps.ensure()`` and the stream-open surfaces any real audio
# problem. Gating ``available`` on the probe here made the lazy-install
# path unreachable (the probe always failed before ensure() could run).
audio_ok = _audio_available() if deps_ok else False
key_ok = True
hint = ""
if provider == "porcupine" and not (os.getenv("PORCUPINE_ACCESS_KEY") or "").strip():
key_ok = False
hint = "Set PORCUPINE_ACCESS_KEY (free key at https://console.picovoice.ai)."
elif not deps_ok:
elif not deps_ok and not lazy_ok:
hint = lazy_deps.feature_install_command(feature) or ""
elif not audio_ok:
elif deps_ok and not audio_ok:
hint = "Microphone capture needs sounddevice + numpy and a working audio device."
return {
"available": audio_ok and (deps_ok or lazy_deps._allow_lazy_installs()) and key_ok,
"available": key_ok and ((deps_ok and audio_ok) or (not deps_ok and lazy_ok)),
"provider": provider,
"deps_available": deps_ok,
"audio_available": audio_ok,