From 7a87c6ffd60dfee253898300030046154cfa9926 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Mon, 27 Jul 2026 11:56:47 -0700 Subject: [PATCH] fix(wake): make the lazy-install path reachable on fresh installs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- cli.py | 5 ++++ tests/tools/test_wake_word.py | 47 +++++++++++++++++++++++++++++++++++ tools/wake_word.py | 16 +++++++++--- 3 files changed, 64 insertions(+), 4 deletions(-) diff --git a/cli.py b/cli.py index 4635b9c8adf0..04528f20933c 100644 --- a/cli.py +++ b/cli.py @@ -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) diff --git a/tests/tools/test_wake_word.py b/tests/tools/test_wake_word.py index afe74ea6f4bb..45e87b36eec7 100644 --- a/tests/tools/test_wake_word.py +++ b/tests/tools/test_wake_word.py @@ -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) ─────────────── diff --git a/tools/wake_word.py b/tools/wake_word.py index a1bb8e43bd80..e1211a61811c 100644 --- a/tools/wake_word.py +++ b/tools/wake_word.py @@ -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,