Skip to content

.NET 10, portable core, and cross-platform spikes (stages 1-3) - #3

Merged
CakeRepository merged 106 commits into
mainfrom
claude/project-thread-gmksub
Sep 30, 2026
Merged

CakeRepository merged 106 commits into
mainfrom
claude/project-thread-gmksub

Conversation

@CakeRepository

@CakeRepository CakeRepository commented Sep 29, 2026 •

Copy link
Copy Markdown
Member

Requested by Justin · project thread

Before: PrimeDictate is a .NET 8 WPF app with no shared transcription core, no cross-platform shell, and no record of how a Nemotron runtime would fit.

After: the Windows app targets .NET 10, a portable PrimeDictate.Core library holds the transcript model, session storage, settings, and scheduling, and spike projects show an Avalonia shell and macOS/Linux audio capture running on Linux. Dictation behavior is meant to be unchanged.

This branch carries stages 1 to 3 of the transcription-mode work as separate commits. It can be split into stacked PRs if that is easier to review.

Commits

  • Stage 1: retarget to net10.0-windows, pin the SDK in global.json, point CI at it. Windows CI is green on this stage.
  • Stage 2: src/PrimeDictate.Core (audio frames and resampler, versioned transcript model and events, SQLite session store, separate transcription settings with migrations, microphone and model coordination, provider and model-registry contracts) with tests/PrimeDictate.Core.Tests. The app gains structured timed results beside the unchanged string path.
  • Stage 3: src/PrimeDictate.Platforms (miniaudio capture), src/PrimeDictate.Desktop (Avalonia shell), and docs/architecture/stage-3-spikes.md with measurements and decisions. docs/architecture/nemotron-spike-local.md lists the steps left to run Nemotron.

Verified (Linux x64 only): 51 Core tests pass; the whole solution builds with 0 warnings; Windows self-contained publishes for x64 and ARM64 succeed; the Avalonia shell renders under X11; a clip played into PulseAudio was captured, resampled, and transcribed correctly by the existing Whisper ONNX engine.

Not verified: anything running on Windows, macOS, or a real microphone; Wayland; any Nemotron inference (model downloads were blocked); an MSI upgrade install over 5.1.0; a Windows smoke test of SharpHook, Whisper.net, and sherpa-onnx, which now load their net10.0 builds. Nothing here is production-ready. No UI uses Core yet.

🤖 Generated with Claude Code

https://claude.ai/code/session_01GUzvd3VYM6n8xTXtBdDt5f

Behavior-preserving upgrade from net8.0-windows to net10.0-windows.
Pins the SDK in global.json and points CI setup-dotnet at it. No code,
dependency, installer identity, or settings format changes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GUzvd3VYM6n8xTXtBdDt5f
@CakeRepository CakeRepository self-assigned this Sep 29, 2026
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GUzvd3VYM6n8xTXtBdDt5f
PrimeDictate.Core (net10.0) holds platform-neutral audio primitives,
the versioned transcript model and events, session state, provider and
model-registry contracts, microphone/model coordination, SQLite session
storage, and separate transcription preferences with migrations.

The Windows app references Core, keeps the dictation string path
unchanged, and adds structured timed results (native timestamps for
Whisper.net, approximate chunk timing elsewhere).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GUzvd3VYM6n8xTXtBdDt5f
…ndings

Adds PrimeDictate.Platforms (SoundFlow/miniaudio capture for macOS and
Linux) and a minimal Avalonia 12 desktop shell, plus the spike record
with measured results and decisions.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GUzvd3VYM6n8xTXtBdDt5f
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GUzvd3VYM6n8xTXtBdDt5f
@CakeRepository CakeRepository changed the title Retarget the Windows app to .NET 10 (stage 1) .NET 10, portable core, and cross-platform spikes (stages 1-3) Sep 29, 2026
claude and others added 23 commits September 29, 2026 19:18
Chunker, utterance detector, WAV reader/writer, exporters, session document host,
file transcription runner and buffered live session.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GUzvd3VYM6n8xTXtBdDt5f
Verified on Linux with mp3, mp4 and wav imports and live capture through a
PulseAudio null sink (provisional then final segments, pause/resume, stop).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GUzvd3VYM6n8xTXtBdDt5f
Import (picker and drag and drop), record with pause/stop/discard, model
selector, session list, searchable editable transcript, copy, export, rerun
and delete.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GUzvd3VYM6n8xTXtBdDt5f
WASAPI loopback (Windows, untested), PulseAudio monitor (Linux, tested on a
virtual sink), macOS stub, CombinedAudioSource on one clock, minimal source picker.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pop6MMPof79Dd7oPc93QUj
Adds SystemAudio and Meeting source types, takes the microphone lease only
for sources that use the microphone, and keeps left=mic/right=system stereo
working audio for meetings while recognition still gets the mono mix.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GUzvd3VYM6n8xTXtBdDt5f
…; test channel order

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pop6MMPof79Dd7oPc93QUj
… samples

Runtime built at the pinned commit, models checksum-verified, file, live, pause,
model-swap and limit checks run on CPU. Vulkan aborts on both GPUs at this commit.
Real worker captures for the integration are under docs/architecture/nemotron-samples.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… desktop crashes

- flush the WAV header every second of audio (was only on pause/stop)
- repair headers of interrupted sessions' audio at startup
- log unhandled exceptions to logs/desktop-crash.log
- fail the WASAPI lease instead of throwing on capture/timer threads
- regression tests: system-only 48 kHz stereo session, header flush, repair

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pop6MMPof79Dd7oPc93QUj
One lane per speaker on a shared time axis, provisional bars dashed and
translucent, click to select a transcript line, wheel to pan and zoom.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GUzvd3VYM6n8xTXtBdDt5f
…eline, live scrolling)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pop6MMPof79Dd7oPc93QUj
Realtime socket session with commit offsets, provisional partials, speaker
turns per utterance, and a streaming branch in the live session.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GUzvd3VYM6n8xTXtBdDt5f
…iders

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pop6MMPof79Dd7oPc93QUj
The capture-to-inference queue was bounded at 256 items (about 5 s), so a slow
model blocked capture and recording stopped. The queue is now unbounded (audio
is already on disk; Backlog reports the lag), previews only run when the queue
is nearly drained and back off by their own cost, and the utterance detector
skips preview snapshots when none will run.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GUzvd3VYM6n8xTXtBdDt5f
SherpaWhisperProvider now sets NumThreads (half the logical cores, 2-8);
sherpa-onnx defaulted to one thread. Deleting a session that is recording
discards the recording first, files that cannot be deleted are reported in
SessionDeletionResult.FailedFiles instead of throwing, and window button
handlers report failures in the status line instead of crashing. Windowed live
utterances are capped at 15 s to bound re-recognition cost.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GUzvd3VYM6n8xTXtBdDt5f
Long runs of flagged synthetic-silence or exact-zero audio are not sent to the
worker (NeMo-Speech.cpp#48 wedges the realtime stream on them); they stay in the
saved recording and offsets are unchanged. A watchdog replaces a connection that
receives speech but returns nothing, drops the provisional/pending text the old
stream owed, and keeps later speaker numbers apart from earlier ones. The frame
flag is the Core contract from the desktop-audio thread's 6e01fc4.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GUzvd3VYM6n8xTXtBdDt5f
claude and others added 27 commits September 30, 2026 01:27
…hole-recording speaker labels

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GUzvd3VYM6n8xTXtBdDt5f
Dictation in the new desktop app: loop, hotkeys, typing, focus guard, overlay, tray, settings
…tion for recordings that fit, test cleanup on Windows, acceptance item 3 wording

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GUzvd3VYM6n8xTXtBdDt5f
…hisper otherwise

The workspace now lists installed Parakeet and Moonshine models. When the
final-pass box is ticked and Nemotron is the chosen model, the live draft uses
Parakeet, else English Whisper, else any Whisper. Other choices are unchanged.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GUzvd3VYM6n8xTXtBdDt5f
…tem speakers

A new live-text choice next to the final-pass box runs no recognizer during the
call (default, remembered). The status line says whether zero system speakers
means silence, a diarizer failure, or words not recognized.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GUzvd3VYM6n8xTXtBdDt5f
…ech the diarizer found, read diarizer JSON safely

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GUzvd3VYM6n8xTXtBdDt5f
…he capture loop read it

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GUzvd3VYM6n8xTXtBdDt5f
… a single running instance

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKWc6Fugd7yn8xw3ozLq8n
…ed it

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKWc6Fugd7yn8xw3ozLq8n
ONNX speech models on CUDA, plus tray Exit and --quit
…their speaker and time; hide duplicate words in overlaps; report per-turn word counts

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GUzvd3VYM6n8xTXtBdDt5f
… a window by speaker turn only when it has no word times

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GUzvd3VYM6n8xTXtBdDt5f
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GUzvd3VYM6n8xTXtBdDt5f
…ways finishes; a cut-short meeting is finished from its saved audio on the next launch

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GUzvd3VYM6n8xTXtBdDt5f
Quit could leave a session Running: the live session was only tracked once StartAsync returned, StopLiveAsync cleared it before stopping so the timeout fallback discarded nothing, Stop/Discard return early when a stop is already under way, and a stalled device made the graceful stop wait forever.

Add LiveTranscriptionSession.AbortAsync (bounded waits, always ends Canceled), track starting and stopping sessions in the workspace, and sweep the store at exit so nothing stays Running, Paused or Finalizing. Finalizing may now be canceled.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKWc6Fugd7yn8xw3ozLq8n
… a rotated app.log next to the worker log

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GUzvd3VYM6n8xTXtBdDt5f
…nch finishes it

A meeting with a pending after-Stop marker and a saved stereo recording ends Interrupted at exit, the state the resume-on-launch recovery takes, instead of Canceled (which means thrown away). Other in-flight sessions are still canceled.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKWc6Fugd7yn8xw3ozLq8n
Exit always leaves a recording Canceled or Completed
The after-Stop meeting pass cut system rows only where the speaker changed, so in
the 9:14 PM run (build cbde440) one remote speaker's three diarizer turns became a
single 24.5-46.9 s row around Justin's own line at 40.8 s.

Rows are now cut from each channel's words, across recognizer windows: at speaker
changes, where the diarizer's turn for that speaker ends (segments under 1 s apart
are one turn), at pauses of 1 s or more, and where the other channel's row starts
inside one (unless that leaves a piece under 0.5 s). Each word keeps the diarizer
speaker covering it most; flicker smoothing stays within pause-free stretches, so a
lone reply keeps its speaker. Echo words get hidden rows of their own.

The run is marked SegmentsAreRows, and TranscriptTurns shows such a result one
segment per row in start order, as exports list them, instead of joining lines
within 5 s again or folding short ones into an earlier row. Older runs read false.

Tests: the 9:14 PM word and diarizer times (times only) end to end, each row rule,
and old stored runs still loading.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ping-6b2617

Final pass: one row per speaker turn, in start order

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The new app looked the WPF-saved microphone (a Windows endpoint id) up among
miniaudio's device names, so it never matched: dictation and the wake word never
opened a microphone, and the only sign was a 6 s overlay notice. On Justin's PC the
saved device no longer exists at all; the WPF app had been falling back to the
default microphone.

- MiniAudioCaptureSource accepts a Windows endpoint id and looks it up by the name
  Windows gives it (the same name miniaudio lists).
- Dictation and the wake word record from the default microphone when the chosen
  one is not connected, as the WPF app does, and say so once.
- "The selected model is not installed" is said once, not on every wake-word
  check, and names the model used instead.
- app.log records why dictation did not start, why a transcript was not typed,
  when the wake word stops listening, and hotkey hook failures. Never text.

Checked on Justin's PC: the wake listener opened the default microphone 3 s after
launch, and app.log shows the fallback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From Justin's first Windows run of the new app.

Overlay:
- Shown only while dictating (and briefly for a notice or the final words),
  unless "Keep the overlay on screen when not dictating" is on; the tray icon
  covers the rest of the time.
- Compact is a microphone pill; full is a fixed-size box with the latest four
  lines. Neither resizes as words arrive, and a size change keeps the bottom edge
  where it was, so nothing jumps.
- Drag it anywhere; the spot is remembered (OverlayAnchorX/Y, new app only) and
  kept on a connected screen. Settings can move it back.
- A close button hides it until the next dictation; a notice still shows.
- No longer click-through (so it can be dragged and closed), still never
  activated, so the caret stays in the app being dictated into.
- A notice is no longer cut short by the final words' 1.5 s linger.

Settings window: never taller than the screen's working area; the form scrolls
with Save and the status line kept in view; long labels wrap; inputs sit under
their labels; the model download row keeps its buttons inside the window.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dictation on Windows: open the microphone, steady overlay, settings that fit

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@CakeRepository
CakeRepository marked this pull request as ready for review September 30, 2026 04:08
@CakeRepository
CakeRepository merged commit 977a9b4 into main Sep 30, 2026
2 checks passed
CakeRepository added a commit that referenced this pull request Sep 30, 2026
Its base, the old work branch, was merged into main (PR #3). Merged cleanly; the
probe tool builds with 0 warnings and all 304 Core tests pass.

Co-Authored-By: Claude Opus 5.5 <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.

2 participants