Skip to content

feat(windows): LCT 2.0 — native capture, on-device Whisper recognition, Ollama pipeline and HUD aligned with macOS - #24

Open
PCcoding666 wants to merge 4 commits into
masterfrom
feat/windows-native-capture
Open

PCcoding666 wants to merge 4 commits into
masterfrom
feat/windows-native-capture

Conversation

@PCcoding666

Copy link
Copy Markdown
Owner

Summary

LCT 2.0 for Windows replaces the Live Captions scraper with the macOS architecture: LCT captures audio itself, recognizes speech on-device, and translates through a local Ollama. The Windows app now matches the macOS app feature-for-feature and in design (terminal HUD, forced dark, emerald accent, monospace labels). windows/ is replaced wholesale; the release pipeline, installer script and executable name are unchanged.

Design doc: windows/docs/dev/ARCHITECTURE.md. User-facing notes: ## [2.0.0] - Unreleased in windows/CHANGELOG.md.

Architecture

Project Target Role
windows/src/LCT.Core net8.0 Every piece of logic without WPF or Win32: settings model (mirrors AppSettings.swift key for key), PCM→float, 16 kHz resampler, energy VAD, utterance chunker (partial every 1.2 s / final after 600 ms silence or 25 s), Ollama client, translation queue, model catalog + hardware recommender, pull-progress accumulator, recovery controller, exporter (MD/TXT/SRT), stall/silence detectors, diagnostics report, SQLite history. Builds and tests on any OS.
windows/LiveCaptionsTranslator.csproj net8.0-windows10.0.17763.0 The WPF app: WASAPI channels (NAudio), Whisper.net recognizers, Ollama process guardian, model manager, IPEX installer (optional), tray, hotkeys, windows and view models.
windows/Tests/LCT.Tests net8.0 97 xUnit tests; the workflow runs them before the five publishes.

Data flow: WASAPI → mono float → 16 kHz → RMS level → EnergyVad/UtteranceChunker → WhisperRecognizer (partial/final) → TranscriptionViewModel → TranslationQueue → OllamaClient (stream, think:false, keep_alive, options.num_ctx) → segment / SessionTranscript / HistoryStore.

Recognition engine choice

Whisper.net 1.9.1 (whisper.cpp, ggml models) over sherpa-onnx streaming Zipformer:

  • one multilingual model covers all 14 source languages; Zipformer needs a model per language and good streaming models exist for far fewer;
  • whisper.cpp quality on zh/ja/ko is established;
  • the NuGet runtime ships win-x64 and win-arm64 natives, so all five publish variants work;
  • cost: Whisper is not a streaming decoder. The chunker hides it: partials every 1.2 s on the utterance so far (capped to 20 s), finals on VAD silence. Partials are coalesced (only the newest waits), finals are never dropped.

Models download to %LOCALAPPDATA%\LCT\models\whisper\ with progress and cancel; small by default, base under 8 GB RAM or 4 cores.

Ollama

  • Uses an existing Ollama first (service answering → %LOCALAPPDATA%\Programs\Ollama → PATH → LCT's IPEX portable). Starts ollama serve only when nothing answers, remembers the PID, stops only that process. Never kills a user-started Ollama.
  • IPEX-LLM portable build is an optional wizard path when an Intel GPU is detected and no Ollama is installed.
  • Every chat/load request sets options.num_ctx (default 4096, in Settings), think: false, keep_alive (5m/15m/30m/1h/until quit); unload on quit is a toggle.
  • Remote endpoints need the explicit opt-in and are HTTPS-only (OllamaEndpoint, same rules as macOS).
  • 10 s patrol while capturing: restart with 10/20/40 s backoff up to 3 times, reload the model when /api/ps shows it gone, announce recovery.

Cleanup

Removed: LiveCaptionsHandler, the UI Automation sync loop, AutoUpdateService/UpdateWindow/VersionSettingsControl, TelemetryService, EnterpriseWelcomeWindow, DataMigrationManager, UpdateUtil, the old pages/windows, WPF-UI, Interop.UIAutomationClient, System.Speech, Newtonsoft.Json, the 1.x docs about the removed systems, and the diagnostic scripts that killed Ollama processes. images/show_livecaptions.png and the README's Live Captions setup chapter are gone; system requirements say Windows 10 1809+.

Version & release

<VersionPrefix> → 2.0.0; ## [2.0.0] - Unreleased written for users. Workflow, NSIS script (names, keys, paths) and RELEASE.md mechanics unchanged; only descriptive strings and the requirements footer were updated. One new workflow step runs dotnet test before the publishes.

Remaining differences from macOS

  • Overlay on all virtual desktops: Windows has no public API to pin a window to every virtual desktop; the overlay is a topmost tool window and follows the normal desktop rules.
  • Recognition is chunked, not streaming: drafts update every ~1.2 s instead of per word (SpeechAnalyzer on macOS streams volatile results). Rollback bookkeeping (CaptionSegmenter) is therefore unnecessary and not ported.
  • Microphone permission: Windows has no per-app prompt for desktop apps; LCT reports a WASAPI start failure with a notice that opens the Privacy ▸ Microphone page instead of a pre-flight permission step.
  • Language availability: macOS marks languages whose on-device model is missing; Whisper's multilingual model covers all 14, so the menu has no "(download)" annotations.
  • Speech self-test (SpeechSelfTest.swift) is not ported.
  • Hotkey chords are fixed (Ctrl+Alt+S/P/O) and can be disabled; not yet user-remappable (macOS uses fixed chords too).
  • 1.x settings (setting.json) are not migrated; 2.0 writes settings.json.

Test plan

CI (Windows Build and Release): dotnet test on LCT.Core (97 tests) + 5 dotnet publish variants + NSIS installer + version check.

Also built locally on macOS with -p:EnableWindowsTargeting=true (C# and XAML markup compile) and dotnet test run locally.

Needs manual verification on Windows

No Windows machine was available; each item lists the expected result.

Install / launch

  • LCT-v2.0.0-ci.N-Setup.exe installs; LiveCaptionsTranslator.exe starts; the portable x64 single-file build starts (Whisper.net native libs extract from the bundle). Expected: no "DllNotFound" for whisper.dll/ggml*.dll.
  • ARM64 portable build starts on a Snapdragon PC.
  • Second launch while running shows "already running" and exits.
  • Log file appears at %LOCALAPPDATA%\LCT\logs\lct<date>.log.

First-run wizard

  • Welcome → Audio & Languages (devices listed, default marked) → Ollama check.
  • With Ollama installed but stopped: LCT starts ollama serve; ollama-serve.log written; wizard continues to model check.
  • Without Ollama: "Ollama Required" with download page, Check Again, remote server option; Intel GPU card appears only on Intel GPU PCs; IPEX install downloads to %LOCALAPPDATA%\LCT\ollama and proceeds.
  • Remote: invalid host rejected with the HTTPS/opt-in message; valid HTTPS server with model → saved.
  • Model missing: recommendation matches RAM/GPU; Download shows progress that never goes backwards; Cancel works; disk-space error when the drive is nearly full.
  • Speech model: whisper small (or base) downloads with progress; Complete; setup-complete marker written; wizard never shows again.

Capture

  • start with system audio: play a video → SYS meter moves, partial text appears in the draft line within ~1.5 s of speech, a finalized line appears after a pause, translation streams token by token, latency shows.
  • Microphone channel: MIC meter and lines with MIC badge; selecting a different device in Settings takes effect on the next start (or restarts the MIC channel live while capturing).
  • Both channels at once with different languages: label reads SYS ZH · MIC EN → JA; lines stay distinguishable.
  • Language menu while capturing: changing SYS language restarts only SYS (MIC meter never drops to 0); changing target clears context and new lines translate to the new target.
  • Pause: meters stop, elapsed shows "paused", new finals show "paused — translates on resume"; resume translates them in order.
  • Stop mid-sentence: the sentence in progress is finalized and translated.
  • Clear only enabled when stopped; clears feed and export.

Detectors & notices

  • Wrong recognition language with audio playing ≥ 8 s → stall warning naming the channel and its language; disappears after a result.
  • Muted microphone for 6 s → silence warning; unmute → warning retracts.
  • Kill ollama.exe while capturing → within ~10–50 s a restart notice, then "Ollama is back" (only if LCT started it; a user-started Ollama is never killed but is restarted after it dies).
  • ollama stop <model> while capturing → model reloads automatically.
  • Ollama not installed at start → error notice with "Get Ollama"; captions still appear without translation.

UI

  • Forced dark everywhere (main, settings, history, wizard, overlay, menus, tooltips); emerald accent; monospace HUD labels; no white flashes.
  • Auto-scroll off when scrolling up; "latest" button returns.
  • Overlay (Ctrl+Alt+O): topmost, drag/resize, hover controls, placement remembered across restarts, opacity/font/click-through from Settings apply live.
  • Tray: icon with tooltip, menu items reflect state, double-click shows window; closing the main window keeps the app in the tray (toggle in Settings).
  • Hotkeys work with another app focused; disabling them in Settings unregisters.
  • Settings: every field round-trips to settings.json; Save disabled with an invalid Ollama endpoint; Reset to Defaults keeps window placement.
  • History off by default; enabling writes history.db; search/copy/delete/clear/CSV.
  • Export MD/TXT/SRT opens a save dialog with LCT Session <date>.<ext>; SRT loads in a player.
  • Diagnostics export contains no transcript text; "Show Log Folder" opens Explorer.

Exit

  • Exit from tray: model unloaded when the toggle is on (ollama ps empty), LCT-started ollama serve terminated, user-started Ollama untouched.

🤖 Generated with Claude Code

PCcoding666 and others added 4 commits November 1, 2026 04:43
…ic plus tests

Platform-independent core for the 2.0 Windows app: settings model mirroring
the macOS AppSettings, PCM conversion, 16 kHz resampler, energy VAD and
utterance chunker (partial/final decode requests), Ollama client (streaming
chat with think:false, keep_alive and options.num_ctx; pull with monotonic
progress; load/unload/ps), translation queue (debounce, preemption, dedup,
retries), model catalog and hardware-based recommender, recovery controller,
session exporter (Markdown/TXT/SRT), stall and silence detectors, diagnostics
report and SQLite history store. 97 xUnit tests; the workflow now runs them
before the publishes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… Whisper and a HUD UI

The app now captures audio itself (WASAPI loopback + microphone as two
independent channels), recognizes speech on-device with Whisper.net
(whisper.cpp ggml models downloaded on demand), and translates through the
LCT.Core Ollama client with streaming output and live draft translation.

New terminal-HUD WPF interface (forced dark, emerald accent, monospace
labels): status bar with recording timer, live-switchable per-channel
language label, model and Ollama indicator, latency; SYS/MIC badged segments
with timestamps; level meters; real pause/resume; overlay window (topmost,
remembered placement, opacity, font size, click-through); tray icon; global
hotkeys; first-run wizard; settings covering every AppSettings key; history
window; session export; diagnostics export.

OllamaGuardian uses an existing Ollama, starts ollama serve only when nothing
answers and stops only the process it started. The IPEX-LLM portable build is
an optional wizard path for Intel GPUs. Removes the UI Automation sync loop,
updater, telemetry, data migration and enterprise windows.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… scripts

Rewrites the READMEs around the new architecture and Windows 10 support, adds
a user guide and an updated docs index, updates the release-notes footer and
installer text, and removes the Live Captions / forced-IPEX era documents and
the diagnostic scripts that killed Ollama processes. VersionPrefix is 2.0.0.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…AD under constant noise

- Start the main window before closing the wizard so the Closed handler no
  longer shuts the app down on completion.
- Handle Unchecked on the auto-scroll toggle; don't cancel Closing during a
  real shutdown.
- Overlay: dim the last segment through a bool→opacity converter instead of a
  bool bound to Opacity; history list shows SYS/MIC badges.
- EnergyVad: loud frames lift the noise floor very slowly so a constant hum
  releases the detector after ~16 s instead of locking it in speech; test added.
- Probe hardware (WMI) off the UI thread at startup.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 31, 2026 20:51

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 insufficient remaining quota. Try requesting a review at a lower effort level.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-31T20:53:59.964353Z 75fa1a3 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 75fa1a3cd1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +114 to +116
if (isFinal && trimmed == _lastProcessedText)
{
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Deduplicate translations by segment rather than text alone

When a speaker repeats the last translated sentence, or both capture lanes recognize the same words, this check silently discards the new segment's translation request. AppendSegment has already created a distinct card in the Translating state, and no completion event is emitted for the discarded request, so that card stays untranslated indefinitely and its transcript entry never receives a translation. Scope deduplication to the segment ID, or deliver the cached result to each new segment; the text-based removal in EnqueueLocked needs the same treatment for pending requests.

Useful? React with 👍 / 👎.

Comment on lines +564 to +568
if (flushed != null)
{
lane.Recognizer.Submit(flushed);
var recognizer = lane.Recognizer;
_ = Task.Delay(4000).ContinueWith(_ => recognizer.Dispose(), TaskScheduler.Default);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Drain pending final recognition requests before disposal

When recognition takes longer than four seconds or has queued finals, stopping capture cancels recognition before the flushed tail can finish: WhisperRecognizer.Dispose immediately cancels its processing token rather than draining its final queue. If Flush() returns null because the chunker already emitted a final, the other branch disposes the recognizer immediately even though that final may still be decoding. Both paths can lose the last spoken sentences from the transcript. Await completion of queued finals before disposing instead of using the chunker's current buffer or a fixed delay as a completion signal.

Useful? React with 👍 / 👎.

Comment on lines +914 to +917
FinalizeDraftAsSegment(lane, source);
lane.Draft = "";
}
StopLane(source, flush: true);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Finalize each utterance only once when restarting a lane

When a recognition-language, microphone-device, or Whisper-model change restarts a lane with a visible draft, this block appends the draft as a permanent segment and then calls StopLane(..., flush: true) for the same buffered utterance. If the flushed decode finishes, OnRecognition appends its final text as another segment, duplicating the speech in the cards, transcript, and potentially history. Clearing lane.Draft does not clear the chunker's audio. Choose either the draft fallback or the flushed final, or reconcile both using the utterance ID.

Useful? React with 👍 / 👎.

Comment on lines +266 to +267
_vm.Ollama.Configure(OllamaClientOptions.From(Local));
await ModelManager.PullAsync(model);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Use a separate client for downloads with unsaved settings

If capture is running while the user selects a different model or endpoint in Settings and clicks Download, this reconfigures the same OllamaClient used by the live translation queue for the entire download. Subsequent captions therefore use the unsaved model, target language, prompt, and endpoint; selecting a model that is not downloaded yet causes live translations to fail while it downloads. Restoring the options in finally cannot undo those results. Give the download operation its own client/options snapshot rather than mutating the active session's client.

Useful? React with 👍 / 👎.

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