Skip to content

fix(tts): piper lazy-install parity + probe install-safety + null-config guards - #78202

Open
andrexibiza wants to merge 6 commits into
NousResearch:mainfrom
andrexibiza:vox/05-provider-registry
Open

fix(tts): piper lazy-install parity + probe install-safety + null-config guards#78202
andrexibiza wants to merge 6 commits into
NousResearch:mainfrom
andrexibiza:vox/05-provider-registry

Conversation

@andrexibiza

@andrexibiza andrexibiza commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Related #44001 #44010 #47318 #61799 #77040 #77932

What changed and why

This PR completes the provider registry & lazy-install consistency bug class: built-in providers must be lazy-installable on fresh installs, requirements probes must never trigger pip installs, and provider fallback/readiness reporting must be truthful. It salvages and completes two open PRs (authorship preserved, see credits) plus the remaining null-config gap.

1. Piper TTS is now lazy-installable (issue #44001)

  • tools/lazy_deps.py: added the missing tts.piper feature entry (was absent — fresh installs with tts.provider: piper failed with a manual-install error while edge/elevenlabs/mistral auto-installed).
  • tools/tts_tool.py::_import_piper: calls lazy_deps.ensure("tts.piper") before importing, matching the Edge/ElevenLabs/Mistral pattern.
  • Pin parity (maintainer-flagged gap in fix(tts): add piper-tts to lazy-install registry (#44001) #44010): piper-tts==1.4.2 is now pinned in all three places — the new [piper] extra in pyproject.toml, the tts.piper lazy entry, and the hermes tools post-setup installer (previously unpinned -U piper-tts, which could install a newer version and then get downgraded by the lazy pin on first use). uv.lock regenerated (adds piper-tts + pathvalidate).
  • tests/test_project_metadata.py: [piper] added to the lazy-covered-extras contract so it can't leak into [all].
  • tests/tools/test_piper_lazy_install.py: environment-independent (mocks find_spec instead of assuming piper is absent) and asserts the lazy pin matches the pyproject extra.

2. Requirements probes never lazy-install (follow-up to #77040)

  • tools/transcription_tools.py::_get_provider: new install: bool = True kwarg; probe callers pass install=False so a bare status check can never freeze on a pip install.
  • tools/voice_mode.py::check_voice_requirements and tools/wake_word.py::_stt_ready call it with install=False.
  • Activation gate preserved (maintainer-flagged gap in fix(stt): keep requirements probes from lazy-installing faster-whisper #77040): an explicit uninstalled local STT provider now counts as ready when lazy installs are permitted — so classic /voice on still activates and the install happens at first real transcription, instead of rejecting an installable provider before the transcription path could install it. The probe itself never runs pip (regression test asserts install=False is passed).
  • local_command exception removed (maintainer-flagged gap): the wake probe previously treated explicit local_command as ready whenever lazy installs were allowed, but the resolver never lazy-installs faster-whisper for local_command — arming the wake word on it would be unbacked. The readiness exception is now restricted to local only, with a regression test.

3. Null-config crash-proofing parity (issue #47318)

How to test

# Targeted suites (venv python from the main checkout):
python -m pytest tests/agent/test_tts_registry.py \
  tests/hermes_cli/test_plugins_tts_registration.py \
  tests/tools/test_tts_container_repair.py \
  tests/tools/test_piper_lazy_install.py \
  tests/tools/test_wake_word.py tests/tools/test_voice_mode.py \
  tests/test_packaging_metadata.py tests/test_project_metadata.py \
  -q --no-header -p no:cacheprovider
# → 140 passed (6 deselected = pre-existing Windows env failures, identical on main)

Behavioral probes run during development:

  • Piper registry parity: tools.lazy_deps.LAZY_DEPS["tts.piper"] exists; _import_piper() calls ensure("tts.piper", prompt=False).
  • Probe has no install side effect: _get_provider({"enabled": True, "provider": "local", ...}, install=False) makes 0 calls to _try_lazy_install_stt, while install=True makes exactly 1.
  • Unknown provider reports truthfully (already merged via fix(tts): report unknown-provider fallback truthfully #77932, verified against this branch): provider: "edge" + fallback_from: "missing-provider" instead of masquerading as the unknown name.
  • Null config: config.yaml with stt.local: null / tts.edge: null / tts.kittentts: null loads and both TTS generation and STT provider resolution complete without 'NoneType' object has no attribute 'get'.

Platforms tested

  • Windows 10 (git-bash), CPython 3.11 — full targeted suites + behavioral probes.
  • git diff origin/main --check clean; scripts/check-windows-footguns.py clean on all touched files (only pre-existing WSL-gated test line flagged, not touched by this PR).

Why this matters to users

  • Fresh installs can pick Piper TTS without manual pip install piper-tts — it now auto-installs on first use like every other built-in provider, at a version consistent with hermes tools (no surprise downgrades).
  • /voice on, wake.status, and TUI status polls never hang on pip installs — but an installable local STT provider still activates correctly and installs at first transcription.
  • Null provider sections in config.yaml (which setup flows can produce) no longer crash STT/TTS.
  • Config still wins: unknown provider names fall back to Edge but the tool result says so truthfully (fallback_from), so agents/users aren't silently misled about which engine produced the audio.

Fixes #44001

Credits

Part of #65160
Part of #78207
Part of #79890

@andrexibiza

Copy link
Copy Markdown
Contributor Author

CI on this PR failed on tests/tools/test_transcription_tools.py::TestRunCommandSttIdleTimeout::test_stderr_progress_extends_beyond_timeout (tests slice 3/8). Verified: pre-existing broken timing test, not a change from this PR.

Evidence:

  • This PR's diff on tools/transcription_tools.py only adds the install=False probe guard to _get_provider — it never touches _run_command_stt or this test (confirmed by diff inspection).
  • The test fails identically on a pristine origin/main worktree (942ff91f21) on Windows.
  • Probe of the failure mechanism: the child emitted all four ticks + done, yet the runner still raised TimeoutExpired at transcription_tools.py:785 — the 0.1s idle window is shorter than Windows process spawn latency, so the first stderr chunk can never arrive inside the window. On Linux CI it only flakes under 8-worker load; sibling PRs fix(voice): resolve Porcupine wake-word key through the credential resolver #78191 and fix(gateway): serialize STT/TTS reliability slice behind V6 contract #78196 passed the same slice 3/8.
  • GitHub refused a re-run of the failed job ("workflow file may be broken"), so the evidence above is the receipt.

Fix: the test is stabilized in #78221 (immediate first tick, 250ms idle window, 5× tick period — pass still depends on the progress extension). Once #78221 merges and this branch picks up main, this slice should go green. No code change needed on this PR.

@andrexibiza

Copy link
Copy Markdown
Contributor Author

Checks on this PR were red on test_stderr_progress_extends_beyond_timeout (tests slice 3/8) — the pre-existing timing flake documented in my earlier comment on this PR. The fix lives on #78221 (test stabilization: immediate first tick, 250ms idle window, 5× tick period); rather than wait for the merge chain, the fix commit is now cherry-picked directly onto this branch at 758ffa3b93.

Verification on the new head (3 consecutive local runs): TestRunCommandSttIdleTimeout → 2 passed each run; git diff --check clean. CI is re-running on 758ffa3b93.

Note for merge ordering: the same test fix exists on #78221 — when both merge, git will dedupe (identical change); either merge order is fine.

@andrexibiza

Copy link
Copy Markdown
Contributor Author

The re-run after the 758ffa3b93 cherry-pick is green on the previously-failing slice 3/8 (the timing test fix landed), but surfaced a different failure in slice 8/8: tests/agent/test_compression_worker_isolation_76354.py::test_f3_mutating_engine_cannot_touch_live_transcript_after_timeout (line 111, live == baseline).

This is a sporadic load-timing flake, not a change from this PR:

  • The failing test is in tests/agent/ (compression-worker isolation); this PR's diff touches tools/transcription_tools.py, tools/tts_tool.py, tools/voice_mode.py, tools/wake_word.py, tools/lazy_deps.py, hermes_cli/tools_config.py and their tests — no shared code.
  • Slice 8/8 passed on this branch's previous head (e35c25ff69): the only delta is the test_transcription_tools.py commit, which this test never imports.
  • Slice 8/8 also passed on sibling PRs with the same base (test(stt): stabilize idle-timeout progress test against spawn latency (pre-existing flake on Windows + loaded CI) #78221, the series merges).
  • The test asserts isolation while the mutating engine thread is still running (event-driven, host-timeout race) — inherently scheduling-sensitive under the hermetic 8-worker slice.
  • Passes 3/3 locally on this head (5–8s runs).
  • GitHub refused an API re-run of the job ("workflow file may be broken" — same refusal seen on other PRs' runs), so the next push/merge of main re-triggers CI.

Treating this as evidence-documented and separate from the PR's scope; the compression-isolation test hardening would be its own change if it keeps flaking.

@andrexibiza
andrexibiza force-pushed the vox/05-provider-registry branch from 758ffa3 to da85ac2 Compare August 4, 2026 11:51
liuhao1024 and others added 6 commits August 4, 2026 11:19
The built-in piper TTS provider had no entry in tools/lazy_deps.py
LAZY_DEPS, so selecting it on a fresh install fails with a hard error
instead of auto-installing like edge/elevenlabs/mistral providers.

Changes:
- Add "tts.piper": ("piper-tts==1.4.2",) to LAZY_DEPS
- Update _import_piper() to call ensure("tts.piper") before importing,
  matching the pattern used by _import_edge_tts() and _import_elevenlabs()
- Add regression tests for feature registration, ensure() call, and
  _check_piper_available()

Fixes NousResearch#44001

(cherry picked from commit fc4f17b)
Signed-off-by: andrexibiza <84248988+andrexibiza@users.noreply.github.com>
_get_provider resolved an uninstalled explicit 'local' STT provider by
running a synchronous lazy pip install (lazy_deps.ensure -> uv pip install
faster-whisper, up to 300s). The TUI's wake.start startup handshake calls
check_wake_word_requirements() -> _stt_ready() -> _get_provider(), so on a
box with stt.provider=local and faster-whisper missing, the handshake froze
for the length of the pip install and the UI sat on 'forging session...'
until the install timed out or was killed.

Mirror the existing _tts_ready 'PROBE, not an installer' fix (the TTS path
already shipped this guard): add an install switch to _get_provider
(default True, backward compatible), pass install=False from the wake-word
and voice requirements probes, and treat an uninstalled-but-installable
local provider as ready -- it installs lazily at first real transcription.

check_voice_requirements had the same freeze via the TUI's voice.toggle
status RPC and is fixed in the same pass.

Regression test mirrors test_tts_ready_is_a_probe_never_an_installer.

(cherry picked from commit d43be77)
Signed-off-by: andrexibiza <84248988+andrexibiza@users.noreply.github.com>
Completes the provider-registry / lazy-install class on top of the
salvaged commits (authorship preserved from NousResearch#44010 / NousResearch#77040):

- Align piper-tts pins across all three install surfaces: the new
  [piper] extra in pyproject.toml, the tts.piper LAZY_DEPS entry, and
  the hermes tools post-setup installer (previously unpinned '-U
  piper-tts' could drift from the lazy pin and force a downgrade on
  first use). Regenerated uv.lock (adds piper-tts + pathvalidate).
- Update the lazy-covered-extras contract in test_project_metadata.py
  so [piper] can never leak back into [all].
- Make test_piper_lazy_install.py environment-independent (mock
  find_spec instead of assuming piper is absent) and assert the lazy
  pin matches the pyproject extra.
- check_voice_requirements: explicit uninstalled 'local' STT counts as
  ready when lazy installs are permitted, so classic '/voice on' can
  still activate and install at first transcription — without the
  probe itself ever running pip.
- wake_word._stt_ready: restrict the lazy-install readiness exception
  to 'local' only; local_command is never lazy-installed by the
  resolver, so arming the wake word on it would be unbacked.
- tts_tool._generate_kittentts: null-coalesce the kittentts subsection
  read for parity with the NousResearch#47318 null-config crash-proofing already
  on main.

Tests: test_tts_registry, test_plugins_tts_registration,
test_tts_container_repair, test_piper_lazy_install,
test_wake_word, test_voice_mode (minus pre-existing Windows env
failures), test_packaging_metadata, test_project_metadata,
test_lazy_deps, test_tts_piper, test_tts_plugin_dispatch.

Signed-off-by: andrexibiza <84248988+andrexibiza@users.noreply.github.com>
The stderr-progress idle-timeout test used a 0.1s idle window with 0.04s
ticks — shorter than Windows process spawn, so the first chunk could never
arrive in time (deterministic failure on Windows, flake under Linux CI
load). Verified failing identically on pristine main before the change.

Fix: emit the first tick immediately, tick every 50ms for ~400ms total,
250ms idle window (5x tick period). The pass still depends on the progress
extension while tolerating real spawn/scheduling latency.

Signed-off-by: andrexibiza <84248988+andrexibiza@users.noreply.github.com>
…er CI load

test_f3_mutating_engine_cannot_touch_live_transcript_after_timeout used
30s/5s thread-wait budgets that scheduler starvation under 8-worker CI
exceeds, failing the isolation assertions vacuously. The worker must stay
blocked for the host to run its byte-identity checks; 300s/60s budgets
preserve every assertion while tolerating load. Fixes the slice-8/8 flake
that blocks unrelated PRs (observed on NousResearch#78202 CI: 3632 passed, 1 flaked).

Signed-off-by: andrexibiza <84248988+andrexibiza@users.noreply.github.com>
…it release

Pins the NousResearch#76354 F3 flake class: the worker must block on the release
EVENT, never a wall-clock budget, so scheduler starvation cannot let it
exit and run the isolation assertions vacuously. The test proves the
worker is still alive after the host's full byte-identity assertion pass
and completes only after release_engine is set.

Signed-off-by: andrexibiza <84248988+andrexibiza@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cli CLI entry point, hermes_cli/, setup wizard comp/tools Tool registry, model_tools, toolsets P3 Low — cosmetic, nice to have tool/tts Text-to-speech and transcription type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Built-in piper TTS provider missing from lazy-install registry (fails on fresh install while edge/elevenlabs/whisper auto-install)

4 participants