mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
feat(wake): eager-install voice deps with the desktop; wake probes never run pip
Two fixes from live testing (Teknium): 1. Desktop installs now ship the wake/voice stacks up front. install.sh + install.ps1 desktop stages run 'uv pip install -e .[wake,voice]' (best-effort, lazy-install remains the fallback) before building the app, so the first ear-click arms instantly instead of sitting through a multi-minute onnxruntime download. CLI-only installs keep the lazy path — [all] curation unchanged. 2. The vanished ear: the STT/TTS gate made wake.status call check_tts_requirements(), whose edge path runs _import_edge_tts → lazy_deps.ensure — a synchronous PIP INSTALL inside a status poll. On a venv without edge-tts that blew the desktop's 30s RPC timeout, armWakeWord caught the error, the atom never learned enabled=true, and the ear unmounted. _tts_ready is now a pure probe: deps missing + lazy installs allowed counts as ready (installs at first speak) WITHOUT touching pip; check_tts_requirements only runs once deps are present. Regression test asserts the probe never calls it while deps are missing.
This commit is contained in:
parent
46faa4f639
commit
a832139ba3
6 changed files with 137 additions and 8 deletions
|
|
@ -178,8 +178,9 @@ voice = [
|
|||
# "Hey Hermes" wake word — on-device hotword detection. All engines are
|
||||
# optional; openWakeWord (ONNX) is the free default, sherpa-onnx adds
|
||||
# open-vocabulary phrases (any typed phrase, zero training), Porcupine is
|
||||
# the premium alternative. Lazy-installed on first /wake; mirrored in
|
||||
# tools/lazy_deps.py.
|
||||
# the premium alternative. Desktop installs ([--include-desktop]) eager-install
|
||||
# [wake]+[voice] so the ear works instantly; CLI-only installs lazy-install on
|
||||
# first /wake; mirrored in tools/lazy_deps.py.
|
||||
wake = [
|
||||
"openwakeword==0.6.0",
|
||||
"onnxruntime==1.27.0",
|
||||
|
|
|
|||
|
|
@ -2813,6 +2813,34 @@ function Try-RestoreElectronDist {
|
|||
return Restore-ElectronDist -InstallDir $InstallDir -Mirror $script:DesktopElectronFallbackMirror
|
||||
}
|
||||
|
||||
function Install-DesktopVoiceDeps {
|
||||
# Desktop ships with working voice out of the box: eagerly install the
|
||||
# wake-word + local-STT stacks ([wake] + [voice] extras) instead of
|
||||
# leaving them to lazy first-use install. Policy change (Teknium, July
|
||||
# 2026, #70509 testing): the first ear-click used to trigger a
|
||||
# multi-minute onnxruntime pip install that froze the UI and blew RPC
|
||||
# timeouts. Best-effort — lazy install remains the fallback for anything
|
||||
# this step fails to fetch.
|
||||
if (-not $script:UvCmd) { Resolve-UvCmd }
|
||||
if (-not $script:UvCmd) {
|
||||
Write-Warn "uv unavailable -- voice/wake deps will lazy-install at first use instead"
|
||||
return
|
||||
}
|
||||
$env:VIRTUAL_ENV = "$InstallDir\venv"
|
||||
Write-Info "Installing voice + wake-word dependencies (onnxruntime, faster-whisper -- 1-3min)..."
|
||||
Push-Location $InstallDir
|
||||
try {
|
||||
Invoke-NativeWithRelaxedErrorAction { & $UvCmd pip install -e ".[wake,voice]" }
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Success "Voice + wake-word dependencies installed"
|
||||
} else {
|
||||
Write-Warn "Voice/wake dependency install failed (exit $LASTEXITCODE) -- they will lazy-install at first use"
|
||||
}
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
|
||||
function Install-Desktop {
|
||||
# Build apps/desktop into a launchable Hermes.exe. Only called from
|
||||
# Stage-Desktop, which is itself only included in the manifest when
|
||||
|
|
@ -3577,7 +3605,7 @@ function Stage-Repository { Install-Repository }
|
|||
function Stage-Venv { Resolve-UvCmd; Install-Venv }
|
||||
function Stage-Dependencies { Resolve-UvCmd; Install-Dependencies }
|
||||
function Stage-NodeDeps { Install-NodeDeps }
|
||||
function Stage-Desktop { Install-Desktop }
|
||||
function Stage-Desktop { Install-DesktopVoiceDeps; Install-Desktop }
|
||||
function Stage-Path { Set-PathVariable }
|
||||
function Stage-ConfigTemplates { Copy-ConfigTemplates }
|
||||
function Stage-PlatformSdks { Resolve-UvCmd; Install-PlatformSdks }
|
||||
|
|
|
|||
|
|
@ -2792,6 +2792,37 @@ _restore_electron_dist_with_fallback() {
|
|||
# (electron-builder --dir) which emits an unpacked app for the current OS. Only invoked
|
||||
# via the 'desktop' stage / --include-desktop, which the Electron app's own
|
||||
# first-launch bootstrap never requests (it must not rebuild itself).
|
||||
install_desktop_voice_deps() {
|
||||
# Desktop ships with working voice out of the box: eagerly install the
|
||||
# wake-word + local-STT stacks ([wake] + [voice] extras) instead of
|
||||
# leaving them to lazy first-use install. Policy change (Teknium, July
|
||||
# 2026, #70509 testing): the first ear-click used to trigger a
|
||||
# multi-minute onnxruntime pip install that froze the UI and blew RPC
|
||||
# timeouts. Lazy install remains the fallback for CLI-only installs and
|
||||
# for anything this best-effort step fails to fetch.
|
||||
local _prev_venv="${VIRTUAL_ENV:-}"
|
||||
if [ "$USE_VENV" = true ]; then
|
||||
export VIRTUAL_ENV="$INSTALL_DIR/venv"
|
||||
fi
|
||||
if [ -z "${UV_CMD:-}" ]; then
|
||||
install_uv || true
|
||||
fi
|
||||
if [ -z "${UV_CMD:-}" ]; then
|
||||
log_warn "uv unavailable — voice/wake deps will lazy-install at first use instead"
|
||||
return 0
|
||||
fi
|
||||
log_info "Installing voice + wake-word dependencies (onnxruntime, faster-whisper — 1-3min)..."
|
||||
if (cd "$INSTALL_DIR" && $UV_CMD pip install -e ".[wake,voice]") ; then
|
||||
log_success "Voice + wake-word dependencies installed"
|
||||
else
|
||||
log_warn "Voice/wake dependency install failed — they will lazy-install at first use"
|
||||
fi
|
||||
if [ "$USE_VENV" = true ] && [ -z "$_prev_venv" ]; then
|
||||
unset VIRTUAL_ENV
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
install_desktop() {
|
||||
local desktop_dir="$INSTALL_DIR/apps/desktop"
|
||||
|
||||
|
|
@ -3074,6 +3105,7 @@ run_stage_body() {
|
|||
# isn't on PATH here. check_node re-adds it (or installs if missing)
|
||||
# so install_desktop can find npm instead of silently skipping.
|
||||
check_node
|
||||
install_desktop_voice_deps
|
||||
install_desktop
|
||||
;;
|
||||
complete)
|
||||
|
|
@ -3160,6 +3192,7 @@ main() {
|
|||
maybe_start_gateway
|
||||
|
||||
if [ "$INCLUDE_DESKTOP" = true ]; then
|
||||
install_desktop_voice_deps
|
||||
install_desktop
|
||||
fi
|
||||
|
||||
|
|
|
|||
|
|
@ -126,6 +126,43 @@ def test_requirements_need_stt_and_tts(monkeypatch):
|
|||
assert "speech-to-text and text-to-speech" in r["hint"]
|
||||
|
||||
|
||||
def test_tts_ready_is_a_probe_never_an_installer(monkeypatch):
|
||||
"""_tts_ready must NOT trigger lazy pip installs from a status poll.
|
||||
|
||||
Regression: check_tts_requirements → _import_edge_tts → lazy_deps.ensure
|
||||
ran pip inside wake.status; a slow/failed install froze the poll and
|
||||
unmounted the desktop ear. Uninstalled-but-lazy-installable counts as
|
||||
ready WITHOUT calling ensure/check.
|
||||
"""
|
||||
import types as _types
|
||||
|
||||
monkeypatch.setattr(
|
||||
ww, "_tts_ready", ww.__dict__["_tts_ready"]
|
||||
) # use the real implementation
|
||||
fake_tts = _types.SimpleNamespace(
|
||||
_get_provider=lambda cfg: "edge",
|
||||
_load_tts_config=lambda: {},
|
||||
check_tts_requirements=lambda: (_ for _ in ()).throw(
|
||||
AssertionError("check_tts_requirements must not run when deps are missing")
|
||||
),
|
||||
)
|
||||
monkeypatch.setitem(sys.modules, "tools.tts_tool", fake_tts)
|
||||
|
||||
# Deps missing + lazy installs allowed → ready (installs at first speak).
|
||||
monkeypatch.setattr("tools.lazy_deps.is_available", lambda f: False)
|
||||
monkeypatch.setattr("tools.lazy_deps._allow_lazy_installs", lambda: True)
|
||||
assert ww._tts_ready() is True
|
||||
|
||||
# Deps missing + lazy installs disabled → not ready.
|
||||
monkeypatch.setattr("tools.lazy_deps._allow_lazy_installs", lambda: False)
|
||||
assert ww._tts_ready() is False
|
||||
|
||||
# Deps present → falls through to the real requirements check.
|
||||
fake_tts.check_tts_requirements = lambda: True
|
||||
monkeypatch.setattr("tools.lazy_deps.is_available", lambda f: True)
|
||||
assert ww._tts_ready() is True
|
||||
|
||||
|
||||
def test_requirements_porcupine_needs_access_key(monkeypatch):
|
||||
monkeypatch.delenv("PORCUPINE_ACCESS_KEY", raising=False)
|
||||
monkeypatch.setattr(ww, "_audio_available", lambda: True)
|
||||
|
|
|
|||
|
|
@ -519,12 +519,41 @@ def _stt_ready() -> bool:
|
|||
|
||||
|
||||
def _tts_ready() -> bool:
|
||||
"""Can the configured text-to-speech provider actually run?
|
||||
"""Can the configured text-to-speech provider run (or install at first use)?
|
||||
|
||||
The wake flow is fully hands-free (wake → speak → hear the reply); without
|
||||
TTS the reply is silent and the loop is pointless. Mirrors /voice's use of
|
||||
``check_tts_requirements``.
|
||||
TTS the reply is silent and the loop is pointless.
|
||||
|
||||
PROBE, not an installer: ``check_tts_requirements`` lazily pip-installs the
|
||||
provider SDK via ``_import_*`` → ``lazy_deps.ensure`` — running that inside
|
||||
a status poll froze wake.status for the length of a pip install (and a
|
||||
failed install marked the wake word unavailable, unmounting the desktop
|
||||
ear). When the provider's deps aren't installed yet, "installable at first
|
||||
use" counts as ready and we never touch pip from here.
|
||||
"""
|
||||
try:
|
||||
from tools.tts_tool import _get_provider, _load_tts_config
|
||||
|
||||
provider = _get_provider(_load_tts_config())
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
_LAZY_TTS_FEATURES = {
|
||||
"edge": "tts.edge",
|
||||
"elevenlabs": "tts.elevenlabs",
|
||||
"mistral": "tts.mistral",
|
||||
}
|
||||
feature = _LAZY_TTS_FEATURES.get(provider)
|
||||
if feature is not None:
|
||||
try:
|
||||
from tools import lazy_deps
|
||||
|
||||
if not lazy_deps.is_available(feature):
|
||||
# Not installed: ready iff it can install at first speak.
|
||||
return lazy_deps._allow_lazy_installs()
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
try:
|
||||
from tools.tts_tool import check_tts_requirements
|
||||
|
||||
|
|
|
|||
|
|
@ -40,8 +40,9 @@ By default the phrase is **"hey hermes"** — a model for it ships with Hermes,
|
|||
it works out of the box with no training. (On first use, openWakeWord downloads
|
||||
its shared feature-extraction models — a small one-time fetch.)
|
||||
|
||||
Both are lazy-installed the first time you enable the wake word. To install ahead
|
||||
of time:
|
||||
Both are lazy-installed the first time you enable the wake word (desktop
|
||||
installs made with `--include-desktop` pre-install them, so the ear works
|
||||
instantly). To install ahead of time:
|
||||
|
||||
```bash
|
||||
cd ~/.hermes/hermes-agent && uv pip install -e ".[wake]"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue