Skip to content

Windows: cloud speech-to-text, fix key saving, fix silent dictation failures - #6

Open
crescent wants to merge 4 commits into
Blueturboguy07:mainfrom
crescent:main
Open

crescent wants to merge 4 commits into
Blueturboguy07:mainfrom
crescent:main

Conversation

@crescent

@crescent crescent commented Aug 4, 2026

Copy link
Copy Markdown

Summary

Windows was up and "building" per the README, but most of it was actually broken end-to-end: keys silently wouldn't save, the recording pill never animated, dictation could fail with zero feedback, and relaunching the app spawned duplicate copies. This PR fixes the root causes of all of those and adds a cloud speech-to-text path so Windows users don't need to hand-download a local Whisper model.

What's fixed

API keys wouldn't save (all platforms, not just Windows)
keyring = "3" ships with no storage backend compiled in by default (v3 changed this from v2's OS-default behavior) — Entry::set_password was silently no-op'ing since the very first commit. Added features = ["apple-native", "windows-native"]. Also made the frontend surface save failures instead of always showing "Saved ✓" regardless of outcome.

The pill never animated on Windows
win.rs's push-to-talk handler passed a no-op closure to the audio capture callback, so the whimpr://audio/waveform event the pill listens for was never emitted — only hotkey.rs (macOS) wired this up. Fixed, and while in there also wired the pill through proper transcribingdone/error states, which it never used on Windows.

Dictation could fail with literally no feedback
Release builds run with windows_subsystem = "windows" (no console), so every failure path's eprintln! was going nowhere a user could see. Now: the pill flashes an error state on any failure, and full diagnostics mirror to %APPDATA%\WhimprFlow\debug.log.

Relaunching the app opened a duplicate copy
No single-instance guard existed, so double-clicking the exe/shortcut again spawned a second independent process (duplicate taskbar entry). Added tauri-plugin-single-instance (Windows/Linux only — macOS's Dock already handles this). Also: closing the Hub window (X button) was destroying it, so the tray's "Open WhimprFlow" item had nothing left to show — it now hides instead. Removed the leftover "Demo: recording"/"Demo: idle" tray items.

Settings could silently drop a field you just typed
Every keystroke in a settings text field fired its own unawaited save-to-disk call (each doing a keyring lookup + rebuilding an HTTP client). Concurrent calls have no ordering guarantee, so a fast typist could have an earlier/emptier value win the final write — caught this live: an ASR base URL persisted as "" even though the field showed the typed value, silently routing "Groq" requests to real OpenAI with a Groq key (401). Saves are now debounced (400ms after typing stops) instead of firing per keystroke.

Local LLM cleanup loaded unconditionally at startup
The llama.cpp worker was spawned regardless of Cleanup Engine mode, burning RAM/CPU even when a cloud provider was selected. Now only loads when mode is Local, and responds live to mode changes.

Build/bundle config
beforeDevCommand/beforeBuildCommand run with cwd already inside ui/, so pnpm --dir ui ... was resolving to ui/ui and failing on Windows. bundle.targets was hardcoded to macOS-only ["app","dmg"], silently breaking a Windows build — switched to "all".

New: Cloud speech-to-text

A new Local/Cloud toggle in Settings → Speech-to-Text. Cloud speaks the same multipart /audio/transcriptions wire format OpenAI and Groq share (CloudAsr in whimpr-cleanup, mirrors the existing OpenAiProvider base-url pattern), and reuses the OpenAI API key field rather than requiring a separate credential. This means dictation works on Windows without downloading a local Whisper model at all — point the base URL at Groq's free-tier Whisper endpoint and go.

Testing

  • cargo test -p whimpr-core -p whimpr-ipc -p whimpr-audio — 35 passing
  • tsc --noEmit clean
  • Verified end-to-end on Windows 11: key save/reload, push-to-talk animation, cloud ASR via Groq, tray behavior, single-instance, settings persistence across a fast-typed field
  • macOS code paths (hotkey.rs, paste.rs) untouched except the local-LLM-mode gating, which is shared logic

crescent and others added 4 commits August 3, 2026 20:16
… selected

- tauri.conf.json: beforeDevCommand/beforeBuildCommand run with cwd already
  inside ui/, so `pnpm --dir ui ...` was resolving to ui/ui and failing;
  drop the --dir ui. bundle.targets was hardcoded to macOS-only ["app","dmg"],
  which silently breaks a Windows build — switch to "all" so each OS bundles
  what's valid for it (NSIS/MSI on Windows, app/dmg on macOS).
- win.rs / hotkey.rs: the local llama.cpp cleanup worker (Qwen 4B) was
  spawned unconditionally at startup regardless of Cleanup Engine mode,
  wasting RAM/CPU whenever a cloud provider was actually selected. Now it
  only starts when mode is Local, and responds live to mode changes.
- pnpm-workspace.yaml: allow esbuild's postinstall (needed for vite) instead
  of silently skipping it.
- README/BUILD-STATUS: document the verified NSIS installer build and the
  LLVM 17.x/18.x pin (bindgen 0.69 silently mis-generates structs on 19+).
…edback bugs

Cloud ASR: dictation no longer requires a local Whisper model. A new
Speech-to-Text setting (Local/Cloud) reuses the OpenAI key field and speaks
the same multipart /audio/transcriptions wire format OpenAI and Groq share,
so Groq's fast Whisper endpoint (or any compatible host) works out of the box.

Windows shell fixes:
- Single-instance guard so relaunching the app surfaces the running instance
  instead of spawning a duplicate process/taskbar entry (Windows/Linux only;
  macOS's Dock already does this).
- Closing the Hub window now hides it instead of destroying it, so the tray's
  "Open WhimprFlow" item has something left to show.
- Removed the leftover "Demo: recording"/"Demo: idle" tray items.
- The dictation pipeline now flashes the pill through transcribing/done/error
  states instead of jumping straight back to idle — release builds have no
  console, so this was the only way a failure was ever visible to the user.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Release builds run with windows_subsystem = "windows" (no console attached),
so every eprintln! diagnostic in the dictation pipeline was going nowhere a
user could see it -- confirmed a user got literally no output running the
packaged exe from cmd.exe. Mirror every diagnostic into
%APPDATA%\WhimprFlow\debug.log (truncated fresh each run) instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…roke

Every keystroke in a settings text field fired its own unawaited Tauri
set_settings call (each doing a keyring lookup + rebuilding the HTTP
provider). Concurrent calls have no ordering guarantee, so a fast typist
could have an earlier, shorter value win the final disk write over the
value actually left in the field -- confirmed by a user's asr_base_url
persisting as "" while the field showed the typed Groq URL, which sent
their cloud ASR requests (with a Groq key) to real OpenAI instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 4, 2026 01:34

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR focuses on making the Windows build truly functional end-to-end while also improving cross-platform reliability around settings/key storage and adding an OpenAI-compatible cloud speech-to-text option.

Changes:

  • Add Cloud speech-to-text support (OpenAI-compatible /audio/transcriptions) plus UI/settings wiring for ASR mode, base URL, and model.
  • Fix Windows dictation UX/pipeline issues (waveform emission, visible error states, debug logging) and prevent duplicate instances via single-instance plugin.
  • Improve operational reliability: keyring backend features enabled, API key save errors surfaced, settings saves debounced, and build/bundle config corrected for Windows.

Reviewed changes

Copilot reviewed 16 out of 17 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
ui/src/hub/SettingsPane.tsx Adds ASR mode controls and improves API key save UX with error handling.
ui/src/hub/App.tsx Debounces settings persistence to avoid concurrent/overlapping writes.
ui/src/hub/api.ts Adds ASR types/defaults and stops swallowing API key save errors.
ui/pnpm-workspace.yaml Allows esbuild to run its build/postinstall steps in pnpm workspace context.
src-tauri/tauri.conf.json Fixes pnpm command execution context and enables bundling for all targets.
src-tauri/src/win.rs Refactors Windows ASR to support local+cloud engines, adds waveform emission + user-visible error states + debug log.
src-tauri/src/lib.rs Adds single-instance behavior (Win/Linux), hub window hide-on-close, and tray menu cleanup.
src-tauri/src/hotkey.rs Gates local LLM worker startup to Cleanup Engine mode rather than always spawning.
src-tauri/Cargo.toml Enables keyring OS backends and adds single-instance plugin for Win/Linux targets.
README.md Updates Windows status/details and adds LLVM version guidance for builds.
docs/BUILD-STATUS.md Updates build status notes reflecting recent Windows and worker-gating improvements.
crates/whimpr-core/src/settings.rs Adds persisted ASR mode/base URL/model to core settings.
crates/whimpr-core/src/lib.rs Re-exports AsrMode from settings.
crates/whimpr-core/src/asr/mod.rs Adds Cloud ASR engine ID.
crates/whimpr-cleanup/src/lib.rs Implements Cloud ASR engine via multipart upload to OpenAI-compatible transcription APIs.
crates/whimpr-cleanup/Cargo.toml Adds dependencies needed for Cloud ASR (multipart + WAV encoding).
Cargo.lock Locks new/updated dependencies (single-instance plugin, hound, multipart, etc.).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +103 to +105
<div style={{ fontSize: 12, color: "#e5484d", marginTop: 6 }}>
Couldn't save — the OS credential store may be unavailable. Check the app's console output.
</div>
Comment thread src-tauri/src/win.rs
if !text.is_empty() {
match asr.transcribe(&pcm) {
Ok(t) => {
log(format!("TRANSCRIPT: \"{}\"", t.text));
Comment thread src-tauri/src/win.rs
Comment on lines +453 to +465
whimpr_core::AsrMode::Local => {
std::thread::spawn(|| match whimpr_asr::WhisperEngine::load(&whisper_model_path()) {
Ok(engine) => {
let engine: Arc<dyn AsrEngine> = Arc::new(engine);
if let Some(slot) = ASR.get() {
*slot.lock().unwrap() = Some(engine);
}
log("ASR ready (local)");
}
Err(e) => log(format!("ASR load failed: {e}")),
});
}
}
Comment on lines +196 to +199
let client = reqwest::blocking::Client::builder()
.timeout(Duration::from_secs(30))
.build()
.expect("failed to build HTTP client");
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.

2 participants