Skip to content

feat: native voice (wake+STT+AEC) + Tauri app — Phases 1-5 - #8

Draft
nirhemo wants to merge 13 commits into
mainfrom
feat/voice-provider-seam
Draft

feat: native voice (wake+STT+AEC) + Tauri app — Phases 1-5#8
nirhemo wants to merge 13 commits into
mainfrom
feat/voice-provider-seam

Conversation

@nirhemo

@nirhemo nirhemo commented Jul 14, 2026

Copy link
Copy Markdown
Owner

Turns ATLAS into a native desktop app with a fully on-device voice pipeline. Everything is on the branch; the Python core, HUD, memory, and settings are reused verbatim. 60 tests pass offline.

Phases

  • 1 — Provider seam: HUD web/native switch; additive /voice/ws; browser Web Speech becomes the fallback. Mic-ownership guards.
  • 2 — Native audio service (audio_service.py): mic (sounddevice) → openWakeWord (hey_jarvis) → VAD → whisper.cpp (base.en) STT → shared Router → Kokoro played natively, streaming state over /voice/ws. Opt-in via voice.native_audio.
  • 3 — Full-duplex AEC + barge-in (aec.py): pure-numpy FDAF echo canceller (~9 dB reduction measured) lets ATLAS listen while speaking without hearing itself; "Hey Jarvis" interrupts mid-sentence. macOS VoiceProcessingIO backend scaffolded (native bridge = on-device follow-up).
  • 0 — Tauri shell (src-tauri/): the FastAPI core runs as a PyInstaller sidecar; the shell reads ATLAS_READY <port> and loads the HUD. Tray, single-instance, autostart, clean shutdown. The frozen sidecar is verified serving the HUD with no Python installed.
  • 4/5 — Release CI + Windows readiness: build-app.yml (macOS+Windows, tauri-action, signing gated on Apple secrets); mic entitlements; cross-platform secret store (Windows Credential Manager fallback); Windows follow-ons documented.

Verified here

  • 60 tests (state machine via injected fakes, AEC echo reduction, barge-in, WS paths, sidecar).
  • Real wake + STT engines load & run on-device; frozen sidecar serves the HUD standalone.

Needs the owner's machine (inherent limits)

  • Live mic test (grant permission + speak) — the real validation of Phase 2/3.
  • AEC tuning / VoiceProcessingIO native bridge for best echo-cancel quality.
  • ./deploy/build-app.sh (Rust compile + GUI + icons); Apple cert for signing; a Windows box.

Notes

  • All native-audio deps are optional (documented) — without them the HUD keeps browser Web Speech. No new required runtime deps.
  • Toolchains (PortAudio, cmake, audio stack + models, Rust, PyInstaller) were installed on the dev Mac to build/verify.

🤖 Generated with Claude Code

nirhemo and others added 4 commits July 14, 2026 13:24
…ke→STT→Router→Kokoro)

Phase 1 (voice-provider seam):
- HUD gets a voiceMode switch ("web" | "native"); browser Web Speech becomes the
  fallback provider. Additive /voice/ws channel + NativeVoice client (handshake,
  reconnect). Guards on all mic loops so mic ownership hands over cleanly.

Phase 2 (native audio pipeline, opt-in via voice.native_audio):
- atlas/interface/voice/audio_service.py: AudioService state machine
  (idle→listening→transcribing→thinking→speaking) + NativeAudioRunner wiring the
  real engines: sounddevice capture, openWakeWord (hey_jarvis) wake, energy VAD,
  whisper.cpp (base.en) STT, shared Router.chat, Kokoro played NATIVELY. Streams
  state/wake/final/reply/tts over /voice/ws via a thread-safe VoiceHub.
- Half-duplex for now (mic ignored while speaking); Phase 3 adds echo-cancelled
  full-duplex barge-in. whisper non-speech markers ([BLANK_AUDIO]) filtered.
- tts.synth_pcm() for native playback; /api/voice/native status; settings
  voice.native_audio + voice.aec_backend. HUD reflects native events + orb abort.

All deps optional (documented in requirements.txt) — without them the HUD keeps
Web Speech. 56 tests pass offline (state machine via injected fakes; WS native +
fallback paths; status endpoint). Real wake+STT engines verified loading/running
on-device; live mic capture needs interactive permission (owner's on-device test).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- atlas/interface/voice/aec.py: EchoCanceller (constrained overlap-save FDAF, pure
  numpy) removes the far-end (Kokoro) signal from the mic — measured ~9 dB echo
  reduction on synthetic room echo. make_aec() picks a backend: "os" (macOS
  VoiceProcessingIO — best; needs a native audio-unit bridge, falls back), "webrtc"
  (needs C++ build, falls back), "nlms" (the FDAF), "none" (passthrough).
- audio_service.py: full-duplex mode. The LLM+TTS turn now runs OFF the mic thread
  (async_turn) so the mic loop stays live while ATLAS speaks; during "speaking",
  _maybe_barge() AEC-cleans each frame and runs wake on the residual → "Hey Jarvis"
  interrupts mid-sentence (abort + queue-to-listen) without self-triggering. Kokoro
  PCM is queued as the AEC reference; playback is interruptible.
- Best-effort reference timing (needs on-device delay calibration; the OS backend
  avoids it entirely) — flagged in code for the owner's on-device tuning.

60 tests pass offline (FDAF echo reduction, barge-in decision, backend fallbacks;
turn state machine made deterministic via async_turn=False).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wrap the existing HUD in a native desktop app (macOS now, Windows-ready):
- Sidecar contract: `python -m atlas.server --port 0` picks a free port, binds
  127.0.0.1, and prints "ATLAS_READY <port>" once serving (server/__main__.py).
- deploy/atlas-core.spec + atlas_core_entry.py: PyInstaller freeze of the FastAPI
  core into a standalone binary (audio ML libs excluded — optional local install).
  VERIFIED: the frozen binary serves the API + HUD with no Python installed.
- src-tauri/: Tauri 2 shell — spawns the sidecar, reads ATLAS_READY, loads the HUD
  webview; single-instance, tray show/hide + Quit, autostart, hide-on-close, clean
  sidecar kill on exit. Entitlements (mic + hardened runtime) for notarization.
- deploy/build-app.sh: freeze → place sidecar with target-triple → `cargo tauri build`.
- VERSION.json bumped to 0.6.0 (packaged-app fallback when there's no .git).

The Rust compile + GUI launch + icon generation are the owner's on-device build
step (`./deploy/build-app.sh`); the sidecar packaging is verified here.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- .github/workflows/build-app.yml: macOS + Windows build matrix (tauri-action) on
  a v* tag — freezes the sidecar, builds the app; macOS signing + notarization
  activate when the Apple secrets are present (unsigned builds still succeed).
- src-tauri/Info.plist: NSMicrophoneUsageDescription for the native voice mic prompt.
- keychain_secret() is now cross-platform: macOS `security` CLI primary, OS keyring
  (Windows Credential Manager / Linux Secret Service) fallback.
- deploy/NATIVE_APP.md: build steps, what's verified vs. needs-your-machine, and the
  Windows follow-ons (Azure signing, WASAPI-loopback AEC reference, CUDA whisper).

Signing needs the owner's Apple Developer cert; the Windows build/test needs a
Windows runner — both wired, both gated on their inputs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@nirhemo
nirhemo marked this pull request as draft July 14, 2026 11:20
nirhemo and others added 9 commits July 14, 2026 15:33
From live testing on the native app:
- VAD no longer ends the utterance on the initial silence after the wake word —
  it waits for the owner to actually START speaking, then detects the end-pause.
  If nobody speaks within the onset window (5s), it drops back to idle instead of
  "thinking" at silence. This fixes "it goes straight to thinking without waiting."
- Conversational: after ATLAS answers (or a barge-in), it keeps listening for a
  follow-up with NO wake word needed; an explicit "stop" goes quiet instead.

62 tests pass offline (+3: wait-for-speech/onset-timeout, follow-up-without-wake;
updated full-turn/empty/abort for the new flow).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Verified fixes from an end-to-end review of the photos/search feature:
- parseArticleCommand() extracted — handleFrameVoice() called it but it never
  existed, so saying "open article N" with a frame open threw ReferenceError.
- Native-voice reply now ALWAYS calls renderSearchMedia(media) (was guarded on
  !== undefined), so stale floating photos/article clear on a non-search reply —
  matching the text-chat path.
- ask(speak) and speakReply() no longer trigger browser TTS in native mode (the
  native service owns the voice) — kills the dual-voice conflict from the TLDR
  button / text turns.
- web_search.rich() only fetches images when the text search produced results, so
  a text-search failure can't leave orphaned photos with no article.

Backend + rendering were already sound; these fix the native-mode paths.
62 tests pass; headless-verified (no crash, media clears, single voice).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…/stop)

Fixes "stop doesn't work while speaking" and "open article N does nothing":
- Full-duplex: while ATLAS speaks, the mic stays live (AEC-cleaned). Barge-in on
  EITHER the wake word (echo-robust) OR sustained talk-over energy (~400ms), so
  you can cut in mid-sentence — it stops and listens for your command.
- Native voice commands: "open article N" / "close" / "stop" are now recognized
  and routed to the HUD (open the reader / close / go quiet) instead of being sent
  to the model, which couldn't act on them. local_command() parses them; the
  audio service publishes a {command} event the HUD handles.
- settings: aec_backend "os" (→ software AEC fallback) enables full-duplex.

64 tests pass offline (+2: open-routes-to-HUD, stop-goes-quiet).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- After an "open article N" / "close" voice command, the native pipeline now
  stays in listening (no wake word) so "close" / "next" work while you're reading
  the article — previously it dropped to idle and "close" alone wasn't heard.
- Photos enlarged (~2× target) with fit-to-viewport scaling so the grid never
  overflows the window (390px on a 1280px window for a 6-photo grid; scales up on
  bigger windows / fewer results). Centred, staggered fly-in unchanged.

64 tests pass; headless-verified (bigger photos, 0 off-screen, no errors).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- local_command() now handles the many ways whisper phrases it ("open article
  one", "open number 3", "open article number one", "show me article 2", "pull up
  the third article"…) so voice "open article N" reliably drives the reader instead
  of falling through to the model. Logs each voice turn's transcript + parsed
  command (voice_turn event) for diagnosis.
- Tauri: closing the window now QUITS ATLAS (stops the backend + voice) instead of
  silently hiding to the tray and continuing to speak. Quit also on the tray menu.

64 tests pass offline.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The native pipeline loaded whisper + openWakeWord synchronously in the FastAPI
lifespan, blocking the server (and the app window) for ~15s. Now:
- NativeAudioRunner.start() returns immediately and builds the engines + opens the
  mic on a background thread; the server serves in <1s.
- When the pipeline is live it publishes an updated {capabilities}/{ready} over the
  hub; /voice/ws always subscribes, so a HUD that connected during load switches to
  native the moment it's ready.
- capabilities.audio reflects runner.ready ("native audio loading…" until then).

Measured: ATLAS_READY in ~0.9s (was ~15s); native comes up in the background.
64 tests pass offline.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Inter-sentence listen gaps let the mic (ducked to near-silence during playback on the DGM20/macOS) recover so 'Hey Atlas' or talk-over can interrupt; one persistent output stream replaces per-sentence sd.play churn; raw-mic wake barge with a separate reset-per-utterance model; clip in the mic callback to stop int16 overflow; extend the listen window while an article reader is open; report aec honestly. Adds a /api/voice/native mic-level readout + an echotest endpoint used to diagnose the output-ducks-the-mic behaviour. Barge path is unverified pending mic hardware.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the teal-delta app icons; drop the unused [lib] target and the externalBin sidecar (the shell now runs the repo venv python directly); mark run.sh executable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Research-backed design (Hermes Agent + OpenClaw) for giving ATLAS filesystem + code-execution tools: a confined ~/ATLAS-workspace, path jail + Apple Seatbelt OS sandbox, READ/WRITE/DESTRUCTIVE gating, git-backed undo, plan/apply folder organizer, and a SKILL.md learning loop, with a phased plan. Proposal for review — no implementation yet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant