Skip to content

feat: Windows beta with streaming dictation, Hold/Toggle shortcut mode, and x64 + ARM64 release - #141

Merged
Hona merged 7 commits into
mainfrom
windows-streaming
Oct 19, 2026
Merged

Hona merged 7 commits into
mainfrom
windows-streaming

Conversation

@Hona

@Hona Hona commented Oct 18, 2026 •

Copy link
Copy Markdown
Member

Hex on Windows: the windows-shell port revived onto current main, a GPU streaming finish so release-to-paste stays under ~100 ms after minutes of talking, a cross-platform Hold / Toggle shortcut mode, and the x64 + ARM64 release pipeline.

Streaming finish (Windows, GPU)

The whole clip used to be decoded after release, so the wait grew with dictation length. Parakeet Unified English is a buffered-streaming model, so audio is now fed to it while the shortcut is held. Release only decodes the last ~1–2 s.

sequenceDiagram
    participant K as Shortcut
    participant C as Capture (16 kHz)
    participant E as Engine thread
    participant P as Paste worker
    K->>C: press (450 ms pre-roll kept)
    C->>E: Begin (after the hold threshold, or on press in Toggle)
    loop every 160 ms
        C->>E: Feed(samples)
    end
    K->>C: release
    C->>C: collect 200 ms tail
    C->>E: Finish(stream, complete clip)
    E->>E: feed remainder, then finalize
    alt stream healthy
        E-->>P: text (source=stream)
    else failed, empty, overfed, or over 1.5 s
        E->>E: transcribe whole clip
        E-->>P: text (source=fallback)
    end
Loading
Clip (RTX 5070 Ti, Vulkan, Unified EN Q8) Release → text, whole clip Release → text, streaming GPU busy while talking
10 s 82 ms 55 ms 3.1%
27 s 185 ms 55 ms 3.6%
131 s, no pauses 873 ms 93 ms 3.6%
Lookahead (left / chunk / right) Extra errors vs whole clip, 131 s GPU busy Release (10 / 27 / 131 s)
5.6 s / 1.04 s / 1.04 s (model default, used) +1 (homophone) 3.6% 55 / 55 / 93 ms
5.6 s / 0.56 s / 0.56 s +2 6.2% 48 / 48 / 81 ms
5.6 s / 0.16 s / 0.16 s +3 20% 75 / 73 / 73 ms
  • One engine thread (src/transcription_engine.rs) owns the model and runs every inference in FIFO order. A live Stream holds the model's compute lease, so a concurrent offline run would get Busy.
  • Same audio both ways: DictationCapture::recorded_from exposes the live 16 kHz recording in final-clip coordinates, so the stream sees exactly the samples the fallback would. This is tested at 16, 44.1 and 48 kHz.
  • Never pastes partial text. Nothing is shown while recording, and the final text exists before paste begins.
  • GPU only. probe_stream streams one silent second at load, which also prewarms the stream graph. CPU devices, including ARM64, keep the whole-clip path.
  • Decode options: the stream reuses the offline RunOptions (language, punctuation), minus the Whisper-only run extension.
  • Knobs: HEX_STREAMING=0 forces the whole-clip path; HEX_STREAM_CHUNK_MS overrides the chunk and right context, for benchmarks only.
  • Bench command: hidden model stream-bench <wav> [--speed N] replays a WAV in real time and prints both paths' latency and word differences.
  • Runtime version: transcribe-cpp stays =0.2.4. 0.4.0 has the same streaming API and Parakeet buffered code, but changes capabilities() to return a Result and adds OutputRepetition errors on the offline path.
// windows_dictation.rs — capture side
if recording_feedback_started && live.is_none() {
    live = engine.begin();               // None on CPU or non-streaming models
}
if let Some(stream) = &mut live {
    let fresh = capture.recorded_from(stream.fed());
    if fresh.len() >= STREAM_FEED_SAMPLES { stream.feed(fresh); }
}
// on release, after the 200 ms tail:
let transcript = engine.finish(live.take(), clip.into_transcription_samples())?;

Windows port

The windows-shell branch had regrouped src/ into common/, desktop/, platform/ and speech/; main stayed flat. The port brings the branch's Windows modules onto main as flat files rather than rebasing 146 commits through that move, and keeps one implementation per concept: Windows consumes main's modules and the #138 design system, with small cfg(windows) additions instead of copies.

flowchart LR
    subgraph shared["Shared by macOS, Linux, Windows"]
        core["audio, dictation, events, history, feedback"]
        speech["transcription_models, gguf_session, local_transcriber"]
        ui["design_tokens, desktop_ui, text_input, transcription picker, desktop_activity"]
        proc["dictation_processor (OpenCode client and ordered processing)"]
        cmds["personal_commands, commands, command_grammar, context, keys"]
        upd["self_update (signed feeds; Linux and Windows)"]
    end
    subgraph win["Windows only"]
        shell["windows_app, windows_ui, windows_desktop_host"]
        loop["windows_dictation, windows_input, windows_paste"]
        misc["windows_settings, windows_updater, windows_indicator, windows_context"]
        engine["transcription_engine"]
        panes["desktop_* panes from the port (Modes, History, onboarding, model catalog)"]
    end
    win --> shared
Loading
Concept Port copy (deleted) Now
Personal commands / transformations windows_personal_commands 3,796 personal_commands
Command engine and grammar commands_engine 1,403, command_grammar_common 1,104, command_context 142 commands, command_grammar, context, shared keys
OpenCode client and processing policy opencode 1,146, dictation_processing 565, windows_voice_action 31 dictation_processor; only the transport differs (curl on macOS, opencode2 api on Windows)
Design system and controls windows_desktop_ui 994, windows_text_input 1,531, picker 476, activity 170 #138 design_tokens and desktop_ui, text_input, picker, desktop_activity
Signed update core separate copies in self_update and linux_updater self_update, used by both
  • local_transcriber: renamed from linux_transcriber and shared by Linux and Windows. It gains recognition hints, install(selection), transcribe_wav_with_selection, the stream probe and with_stream.
  • Windows dictation loop: gains main's 200 ms post-release tail with stall grace, the near-silent gate, and warm-timeline ingest.
  • Shared code widened to Windows: audio, app_paths (command SDK embedded per content hash), feedback, transcription_models (available_models, language_for_model, recommended_selection, uninstall, atomic_replace).
  • Behaviour now matching macOS on Windows:
    • OpenCode falls back to the ranked recommended models when none is set.
    • Personal commands use bun update @hex/commands and allow deep-link URLs, opened through ShellExecuteW rather than cmd /C start.
    • Browser-host matching no longer requires Brave.
  • Still separate:
    • HUD: the Windows HUD model vs dictation_indicator. Sharing it means rewriting the Metal HUD.
    • Panes: the port's Modes, History, onboarding and model-catalog panes vs app_window's inline panes. That's the shared-desktop-UI roadmap item; each of those files carries a TODO(consolidate-screens) pointing at its macOS counterpart and 75f859b (feat(ui): consolidate the desktop design system #138).
    • Host contract: windows_desktop_host vs Linux's socket-backed desktop_host; the two are different contracts.
  • Not ported: the local transcription API (service); it's tracked in docs/windows.md.

Windows fixes found while porting

Bug Fix
Every model install failed its checksum receipt write_json_atomically flushed through a read-only handle and opened the parent directory. Windows rejects both. Added sync_file (writable handle) and sync_directory (a no-op on Windows; NTFS journals renames).
model install crashed with a stack overflow sha256_hex kept a 1 MB buffer on the 1 MB main-thread stack. It's on the heap now.
Toggle capture on Ctrl+Space ended when Ctrl alone was pressed Only modifier-only bindings finish on a modifier press; key chords finish on their key.
A finished transcript was dropped if Ctrl was still held 500 ms after finishing Paste no longer waits for Ctrl, and doesn't synthesize a release for a Ctrl the user holds. It waits, with no timeout, only for Shift, Alt and Win, which would change the shortcut.
The first Ctrl+Space after an Escape cancel was ignored Escape's key-up now clears the hook's dirty state when no binding modifier is held.
Toggle start tone came ~300 ms after the press A Toggle press is deliberate, so it confirms immediately (10–30 ms measured). Hold keeps the threshold so quick chords meant for other apps stay silent.
Panes clipped on both edges in narrow windows pane_content() lacked min_w(0), and GPUI measures text min-content as the unwrapped line. Long right-hand row values are now capped.
Task Manager showed voice-control with no icon build.rs embeds resources/windows/hex.ico and a VERSIONINFO (2.2.0) through embed-resource. The window and taskbar load that same icon: a flat tile that stays crisp at 16–64 px (scripts/windows-icon.ps1).
Linux X11 grab test failed on this branch (conflicted_escape_fallback_releases_partial_grabs) grab_variants released a partial grab without waiting for the X server, so another client could race it into an Access error. It now waits for each UngrabKey.
Some shared tests failed only on Windows Test helpers open files writable before set_modified. Two Unix-semantics tests are cfg_attr(windows, ignore).
Before After

Hold / Toggle shortcut mode (macOS, Linux, Windows)

Settings → Dictation → Shortcut mode sits directly above the shortcut row.

Hold (default)  press ─ speak ─ release → finish        double-tap lock available
Toggle          press → start · press again → finish     Escape cancels; double-tap rows hidden
  • One mechanism on every platform: every press acts as the second tap of a double-tap, so releasing locks the capture and the next shortcut press finishes. Pre-roll, the release tail and the streaming finish are unchanged.
    • macOS: DictationHotkey::set_toggle in suppression.rs.
    • Windows: CaptureState.toggle in windows_input.rs.
    • Linux: the toggle flag on the X11 loop and the Wayland HotkeyState.
  • New setting: shortcut_mode: "hold" | "toggle" in each platform's settings file. Files without it load as hold.

Windows look

Windows renders with main's design system (#138): the same tokens, controls and sentence-case labels as macOS, with Segoe UI. The sidebar now runs the full window height, matching macOS, and the custom caption row is split over the sidebar and content columns. Both halves stay drag regions, and min/max/close keep their hit-test areas for snap layouts.

Before After

Enabled / Disabled status (Windows and Linux)

Before After
On Listening · mic · Hold Ctrl+Win to dictate Enabled · Ready anywhere · Press Ctrl+Space to start and stop dictating
Off Ready · mic · Hold Ctrl+Win to dictate Disabled · Enable Hex to dictate into any app with Ctrl+Space
Button / tray Start listening · Start / Stop Listening Enable / Disable · Enable / Disable Hex
Live states Dictating · Starting · Stopping · Unavailable Recording · Enabling… · Disabling… · Couldn't enable
Launch row Listen on launch Enable on launch
Default shortcut (Windows) Ctrl+Win Ctrl+Space
  • Linux: the Settings card uses the same states, hint and Enable/Disable button.
  • Activity labels: Enabled, Recording and Disabling… are shared by every platform, including the macOS developer Activity pane. macOS has no on/off listener, so it has no status card.
  • First-run dialog (Windows): the footer splits into the hint on one line and the actions on the next.

Release

flowchart LR
    PR[pull request] --> CW[check-windows.yml]
    CW --> X["x64 on windows-latest<br/>Vulkan SDK, SPIRV-Headers, fmt, clippy, test, build<br/>assert no VC++ runtime imports"]
    CW --> A["ARM64 on windows-11-arm<br/>clang-cl and Ninja, test, build"]
    R["release-windows.sh<br/>HEX_WINDOWS_ARCH = x86_64 or aarch64"] --> R2[("downloads.hex.kitlangton.dev")]
    R2 --> F1[windows-update.json]
    R2 --> F2[windows-aarch64-update.json]
    I[install-windows.ps1] -->|AMD64| F1
    I -->|ARM64| F2
Loading
Before After
CI release-windows.yml: tag-only, x64, unsigned zip to GitHub Releases check-windows.yml on every PR and on main: x64 + ARM64. Signed publishing stays manual in release-windows.sh.
Architectures x64 x64 (Vulkan GPU) and ARM64 (CPU, as Handy ships it)
Runtime deps VC++ Redistributable None: .cargo/config.toml sets +crt-static and CMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded. Only vulkan-1.dll on x64, which the GPU driver provides.
CPU portability GGML_NATIVE defaulted ON (tuned to the build machine) TRANSCRIBE_CMAKE_ARGS=-DGGML_NATIVE=OFF in CI and releases
Origin pub-…r2.dev downloads.hex.kitlangton.dev, same bucket and key as Linux and macOS
Feeds windows-update.json Same file for x64, plus windows-aarch64-update.json
Installer x64 only Picks the architecture; x64 checks for vulkan-1.dll with a clear driver message
Website Mac button, plus Linux if detected Detects Windows → Install on Windows (docs/windows.md#install); title and meta mention Windows
  • Release checklist: run release-windows.sh prepare / publish for each architecture after merge. ARM64 is best built on an ARM64 host.
  • Exe size: 66 MB, 22 MB zipped. Almost all of it is ggml-vulkan's embedded SPIR-V shaders.

Docs

  • docs/windows.md: now titled "Windows Beta", with Install, Streaming Dictation and per-architecture source-build sections, and an updated parity table.
  • docs/features/dictation.md: new Shortcut Mode and Streaming Finish sections with measured evidence.
  • Feature index (docs/features/README.md): new Windows platform entry.
  • AGENTS.md: new modules and invariants, plus Windows release steps.
  • README.md: new Windows Beta section and log location.
  • ROADMAP.md: Single-tap to lock mode missing from Rust re-write #77 (single-tap hands-free activation) is now implemented; its remaining checks are listed.

Not verified

  • macOS build: there's no macOS CI. Please run cargo test and cargo clippy --all-targets -- -D warnings on a Mac before merge. macOS-compiled files touched:
    • Wiring: main.rs.
    • Settings, Toggle and status: app_settings.rs, app_window.rs, suppression.rs, recognition.rs.
    • Dictation and processing: dictation.rs, dictation_processor.rs, parakeet.rs.
    • Commands: personal_commands.rs, commands.rs, command_grammar.rs, context.rs, config.rs.
    • Keyboard and text: keyboard.rs, keys.rs, text_input.rs, text_replacements.rs.
    • Design system: desktop_ui.rs, design_tokens.rs.
  • Linux runtime: check-linux builds and tests it, including the Xvfb grab tests; Toggle and the new status copy haven't been run on a physical Linux desktop.
  • OpenCode on Windows: not exercised live; opencode2 wasn't installed on the test machine.
  • ARM64 Windows: compiled by CI only, not run on hardware.
  • Windows 10 and physical microphone corpora: not smoke-tested.

Copilot AI balanced review requested due to automatic review settings October 18, 2026 23:45

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

@Hona
Hona force-pushed the windows-streaming branch from af193fa to ad9d9a5 Compare October 19, 2026 00:49
Hona added 7 commits October 19, 2026 10:59
…e, and release pipeline

Ports the windows-shell branch onto main, adds a GPU streaming finish for Parakeet Unified English with a whole-clip fallback, a cross-platform Hold/Toggle shortcut mode, x64 and ARM64 Windows CI and release feeds, and the website Windows download.
…dows

The Windows port carried its own copies of modules main already has. Each
concept now has one implementation:

- UI: Windows uses main's design system (design_tokens, desktop_ui,
  text_input, desktop_transcription_picker types) instead of a Fluent copy.
  Windows-only bits are small cfg additions: Segoe Fluent Icons for Icon,
  Windows text-editing key bindings, fixed-height and read-only text inputs,
  and the shared slider_track component. windows_ui keeps the window chrome
  and the anchored dropdowns, now drawn with menu/menu_item.
- Windows chrome: the sidebar runs the full window height as on macOS. The
  caption row is split: the brand sits over the sidebar, the window buttons
  over the content, and both halves stay OS drag regions.
- Commands: Windows uses main's commands, command_grammar, context, keys,
  and personal_commands. The SDK provisioner accepts the embedded TypeScript
  src layout; Windows execute() opens URLs and paths with ShellExecuteW.
- OpenCode: dictation_processor is the one client and ordered policy
  (corrections, rewrite, transformations) on macOS and Windows. Windows keeps
  its opencode2 api CLI transport in a cfg'd submodule; the ranked catalog,
  prompts, Voice Action generation, and the transformation stage are shared.
- desktop_activity is shared; the Windows Settings contract is
  windows_desktop_host, without #[path] aliases.
- self_update is the signed-feed core for both the Linux and Windows
  updaters.
- Windows copy follows the design system: sentence case and "Hex".
@Hona
Hona force-pushed the windows-streaming branch from 507eb21 to 30a735e Compare October 19, 2026 01:00
@Hona
Hona merged commit 8efd990 into main Oct 19, 2026
Hona added a commit that referenced this pull request Oct 19, 2026
The keyboard-layout test harness includes src/keyboard.rs, which now imports crate::keys, and personal_commands::workspace_configured is only called on Windows. Adds a Check macOS workflow so this can't regress silently.
Hona added a commit that referenced this pull request Oct 19, 2026
* fix(macos): restore the macOS build broken by #141 and check it in CI

The keyboard-layout test harness includes src/keyboard.rs, which now imports crate::keys, and personal_commands::workspace_configured is only called on Windows. Adds a Check macOS workflow so this can't regress silently.

* fix(linux): allow the hotkey worker entry points their eighth argument
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