Skip to content

Support Audio - #677

Merged
TheJoeFin merged 49 commits into
devfrom
audio
Aug 24, 2026
Merged

Support Audio#677
TheJoeFin merged 49 commits into
devfrom
audio

Conversation

@TheJoeFin

Copy link
Copy Markdown
Owner

PR Classification

Introduces a new on-device audio transcription feature and related UI, settings, and infrastructure updates.

PR Summary

Adds Whisper-based audio transcription (file and live audio) with user-selectable models and integrates transcription into the main app flows.

  • AudioTranscriptionUtilities and LiveAudioTranscriber: Implement model management, audio decoding, VAD segmentation, and live transcription logic.
  • BottomBarSettings and EditTextWindow: Update UI to support transcription toggles, model/source selection, and streaming results.
  • Integrate transcription into file open and drag-and-drop flows, replacing text open with audio transcription.
  • Add non-blocking status bar and cancel button for long-running transcriptions.
  • Update app manifest for microphone capability and add Whisper.net/NAudio dependencies.

TheJoeFin and others added 30 commits July 9, 2026 22:05
Reference local Whisper (Whisper.net + Whisper.net.Runtime) and NAudio for
on-device audio transcription, and declare the microphone device capability
needed for live microphone capture.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Persist the user's chosen Whisper model (defaults to balanced multilingual
base), trading transcription speed for accuracy and language coverage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implement on-device transcription backed by Whisper.net (whisper.cpp),
running on the CPU packaged or unpackaged on x64/arm64:

- File transcription decodes any Media Foundation audio to 16 kHz mono via
  NAudio and streams Whisper segments as they are recognized, cancellably.
- Selectable model (tiny.en / base.en / base / small) via WhisperModelChoice,
  read from settings; the shared WhisperFactory reloads when the choice changes.
- VAD-gated live transcription (LiveAudioTranscriber) from microphone or
  system-audio loopback: Silero VAD finds speech regions and only sends a
  region to Whisper once trailing silence marks the utterance complete, so
  silence is skipped and phrases cut on natural boundaries.
- AudioDebugLog writes a timestamped diagnostic log for the transcription path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Route audio files (CLI arg, File > Open, drag/drop) to on-device
transcription, streaming each Whisper segment into the editor via a
non-blocking, cancellable status bar that preserves already-transcribed text.

Add a bottom-bar live-transcription toggle with a right-click menu to pick the
capture source (microphone or system audio) and the transcription model, both
restarting an active session on change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reference local Whisper (Whisper.net + Whisper.net.Runtime) and NAudio for
on-device audio transcription, and declare the microphone device capability
needed for live microphone capture.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Persist the user's chosen Whisper model (defaults to balanced multilingual
base), trading transcription speed for accuracy and language coverage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implement on-device transcription backed by Whisper.net (whisper.cpp),
running on the CPU packaged or unpackaged on x64/arm64:

- File transcription decodes any Media Foundation audio to 16 kHz mono via
  NAudio and streams Whisper segments as they are recognized, cancellably.
- Selectable model (tiny.en / base.en / base / small) via WhisperModelChoice,
  read from settings; the shared WhisperFactory reloads when the choice changes.
- VAD-gated live transcription (LiveAudioTranscriber) from microphone or
  system-audio loopback: Silero VAD finds speech regions and only sends a
  region to Whisper once trailing silence marks the utterance complete, so
  silence is skipped and phrases cut on natural boundaries.
- AudioDebugLog writes a timestamped diagnostic log for the transcription path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Route audio files (CLI arg, File > Open, drag/drop) to on-device
transcription, streaming each Whisper segment into the editor via a
non-blocking, cancellable status bar that preserves already-transcribed text.

Add a bottom-bar live-transcription toggle with a right-click menu to pick the
capture source (microphone or system audio) and the transcription model, both
restarting an active session on change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Added EtwShowTranscribe setting to enable/disable audio transcription features. Introduced "Show Transcribe" toggle in BottomBarSettings and new menu items in EditTextWindow for transcription actions, shown only if supported and enabled. Synced UI and settings for transcription visibility, source, and model selection. Updated code generation version in Settings.Designer.cs.
Text-Grab.csproj listed Microsoft.WindowsAppSDK.AI/.Foundation/.Runtime/.WinUI
twice at differing versions, producing NuGet warning NU1504. The newer set is
not internally consistent (Runtime 2.4.0 expects InteractiveExperiences 2.1.6
but 2.1.3 is resolved), so keep the matched 2.2.x components.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The .NET 10 SDK no longer supports running Microsoft.Testing.Platform test
projects through the VSTest target, which broke `dotnet test` in CI. Opt into
the new runner via global.json and drop the VSTest-only packages
(Microsoft.NET.Test.Sdk, xunit.runner.visualstudio, coverlet.collector).
OutputType is now Exe as required by the xunit.v3 MTP entry point.

xunit.v3 is pinned to 3.2.2 because Xunit.StaFact, used by every WpfFact test,
has no build compatible with xunit.v3 4.0.0 yet.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
With Microsoft.Testing.Platform, dotnet test no longer accepts a positional
project path.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Enabled capturing and transcribing both microphone and system audio at once via new LiveCaptureSource option and UI updates. Refactored LiveAudioTranscriber for multi-channel input, mixing, and buffer management. Improved VAD/transcription logic for mixed sources. Enhanced concurrency with semaphore locks. Updated UI feedback and error handling. Added quantization-aware Whisper model file handling.
Replaces WindowsAiUtilities/TextRewriter-based translation with new WinAiTranslator, managing on-device Phi Silica model and LAF unlock. Adds batched and streaming translation, improved error handling, and user feedback. Moves language heuristics to LanguageHeuristics.cs. Project now supports LAF tokens via build/env vars. Updates all translation entry points for new API. Cleans up old logic, adds LimitedAccessFeatureUtilities, and improves UI batching and resource management.
Enables secure injection of Windows AI Language Model (LAF) unlock tokens via LAF_TOKEN and LAF_PUBLISHER_ID environment variables, sourced from GitHub Actions secrets in Release.yml and buildDev.yml, and passed to MSBuild publish steps. Updates Text-Grab.csproj to default LafToken and LafPublisherId from environment variables for local and CI builds. Adds Configuring-LAF-Environment-Variables.md to document setup and usage, stressing token secrecy. Builds without secrets still succeed but omit on-device text AI features.
Refactored all Windows AI language model logic into WinAiLanguageModel for shared creation, availability checks, and inference queuing. Standardized error and result handling with WinAiFailure and WinAiGenerationResult. Updated WinAiTranslator, WindowsAiUtilities, and regex extraction to use the shared model and new result types. Improved error reporting in EditTextWindow and clarified documentation throughout.
Added "Summarize as Meeting Notes" to EditTextWindow with UI and handler. Implemented WinAiMeetingNotes utility for splitting, summarizing, and merging long text using Windows AI (Phi Silica). Added unit tests for text splitting. Refactored response cleaning to shared CleanResponse method in WinAiLanguageModel. Updated docs and comments. Improved error handling and user feedback for meeting notes generation.
When the out-of-process Windows AI runtime connection is lost (e.g., "The RPC server is unavailable"), the code now disposes and recreates the language model, retries the request once, and only fails if restart fails. This logic is centralized in `WinAiLanguageModel.GenerateAsync` and `RunWithModelAsync`, using a new `IsConnectionLost` helper. The unlock cache in `LimitedAccessFeatureUtilities` is reset on restart to avoid caching transient failures. All AI features now use the shared model and recovery logic. Public APIs allow manual model restarts. Exception handling distinguishes recoverable from unrecoverable errors, and error messages are clarified.
Wires the existing "New Window with Selected Text" action to Ctrl+N
so it doesn't require the menu, and shows the shortcut in the menu item.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Previously these AI actions overwrote the source text in place. Now
the source stays untouched and the generated result opens in a new
Edit Text window, matching the existing "open in new window" pattern.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
TranscribeAudioFileAsync now accepts an optional hotWords string
applied per-call via Whisper's WithPrompt/WithCarryInitialPrompt, plus
GetAudioFileInfo (size/duration, no decode) and GetAudioFileFilter for
an upcoming file-picker UI.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
IsPickerMode makes a pick always write into DestinationTextBox and
close the window, regardless of EditWindowToggleButton, for callers
that want QSL purely as a value picker rather than its ETW
insert/clipboard flow.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New Capture menu item opens OpenMediaWindow: pick an audio file, add
hot words for that transcription (typed directly or picked from Quick
Simple Lookup via its new picker mode), and see size/duration/model
info before starting. Video is a disabled placeholder for a future,
unrelated feature. Hot words are transient per-transcription, not
persisted. TranscribeAudioFilesAsync is now internal and takes an
optional hotWords parameter to support this.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
LookupItem.ToCSVString writes rows as unquoted "ShortValue,LongValue",
so a LongValue containing commas (e.g. a saved hot-words batch) split
into more than two cells on reload. The parser rejoined those cells
with a space, silently losing the commas. Rejoin with the same
delimiter that was used to split when reading a CSV row, so the
original value round-trips exactly; tab-delimited rows (typed entry,
clipboard paste) keep their existing space-joined behavior.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds a persisted NotifyOnTranscriptionComplete setting (default on) and a
toggle in the Open Audio / Video window, so long-running file
transcriptions can notify the user via toast when they finish instead of
requiring the editor window to stay in focus.
Adds a persisted IncludeTimecodesInTranscription setting (default off)
and a toggle in the Open Audio / Video window. When enabled, each
Whisper segment is prefixed with its start time (e.g. [01:23]) and
placed on its own line, using timing data Whisper.net already provides
per segment.
TheJoeFin and others added 19 commits August 23, 2026 17:47
GetAllTemplates() previously re-read and re-parsed GrabTemplates.json
(and could trigger a slow Settings.Save() to keep the legacy setting and
file-backed copy in sync) on every call, including every time a menu
that lists templates was opened. This was the main cause of a
noticeable delay opening EditTextWindow's "Capture" menu.

Templates are now resolved once per process (or since the last write)
and cached; GetAllTemplates() returns a fresh list copy each time so
structural edits by one caller can't leak into another caller's list
before being persisted. Test seams (TestFilePath,
TestPreferFileBackedMode) invalidate the cache on assignment so the
existing test suite's out-of-band file/settings writes still work.
LoadGrabTemplateMenuItems() ran on every "Capture" menu open, rebuilding
the template list even when the user was heading for an unrelated item.
Moved it to GrabTemplateMenuItem's own SubmenuOpened event so it only
runs when that specific "Use Grab Template..." submenu is opened.
GetCaptureLanguagesAsync() had its own copy of the UI-automation /
Windows AI / OCR-engine language enumeration logic, duplicating (and
never caching) the work LanguageService.GetAllLanguages() already does.
The Windows AI readiness checks in particular are real WinRT/WinAppSDK
probes that can be slow, especially the first time in a process, so
every newly opened EditTextWindow paid that cost again the first time
its "Capture" menu was opened.

Now builds from the cached LanguageUtilities.GetAllLanguages() and only
adds the Tesseract-specific entries on top, spliced in to preserve the
original ordering.
Added two new icon files, Select-Black.ico and Select-White.ico, as content. Updated the .csproj to include these icons for build and packaging, ensuring they are copied to the output directory. Removed any previous references to these icons as needed.
Added TrayIconStyle to App.config and Settings.settings as a user-scoped setting with default "Color". Generated property in Settings.Designer.cs with proper attributes. No changes to other settings.
Added RefreshTrayIconStyle call in App.xaml.cs to update the tray icon when applying the system accent color. Introduced TrayIconStyle enum (Color, Monochrome) in Enums.cs to support multiple tray icon appearance modes.
Users can now choose between color or monochrome tray icons via GeneralSettings. Added ApplyTrayIconStyle() in NotifyIconWindow and RefreshTrayIconStyle() utility for immediate updates. UI and logic updated to persist and apply user preference, with thread-safe icon refresh.
Lets users hide the text label on the live transcription bottom bar
button, keeping only the mic/speaker icon.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Added real-time progress bars to EditTextWindow and OpenMediaWindow for audio transcription, reflecting actual progress through the audio clip. Updated AudioTranscriptionUtilities to support progress reporting via IProgress<double>. Improved Cancel button logic to handle in-progress transcriptions and updated UI state management for better user feedback. Status text now shows percentage complete during transcription.
Updated Microsoft.WindowsAppSDK packages: AI to 2.4.4, Foundation to 2.3.9, Runtime to 2.4.0, WinUI to 2.3.6. Added InteractiveExperiences 2.1.6. Removed previous versions. Updated PdfPig from 0.1.15 to 0.1.16.
AudioDebugLog is always on, so in Release it grew without bound at
%USERPROFILE%\TextGrab-audio-debug.log, recording user file paths in the
profile root. It now writes to %LOCALAPPDATA%\Text-Grab\Logs\audio-debug.log
and rolls over into audio-debug.prev.log once past 1 MB, tracking the size
from a counter rather than a FileInfo per line. Working set now comes from
Environment.WorkingSet instead of a Process.GetCurrentProcess() per line.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
GetFactoryAsync disposed the shared WhisperFactory whenever the selected
model changed, which frees the native model every WhisperProcessor built
from it decodes against. The transcription status bar is deliberately
non-blocking, so switching models or starting live transcription during a
long file transcription could take the process down.

The factory is now handed out as a refcounted WhisperFactoryLease: a
superseded factory is retired rather than disposed, and freed only once the
last lease is returned. File transcription holds a lease across the whole
decode; LiveAudioTranscriber holds one for the session and releases it in
Cleanup, after the processor.

Cleanup itself also raced the chunk timer: Timer.Stop leaves already-queued
Elapsed callbacks to run, and one could take the processing gate right after
StopAsync's flush released it, enumerating _channels as they were cleared
and using _processor as it was disposed. StopAsync now sets a _stopping flag
that timed passes check, and tears down while holding the same gate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three problems in the audio transcription path of EditTextWindow:

Streamed segments go into PassedTextControl, but the finally called
SyncTextFromActiveEditor(), which pushes the markdown document or the table
back over PassedTextControl.Text -- running the whole transcript through the
serializer for nothing. A new SyncActiveEditorFromText() syncs in the right
direction, reusing the RefreshSpreadsheetFromText / RefreshMarkdownFromText
helpers the TextChanged handler already uses. InsertTranscribedText (live
transcription) had the same reversed sync.

_transcriptionCts was assigned well before the try, with UI setup and
EnsureTranscriptionInsertionPoint() in between; anything throwing there left
the field non-null and the guard at the top then blocked every later
transcription in that window. The setup now runs inside the try that clears
it.

A non-cancellation failure raised as the window closes showed a MessageBox
owned by a torn-down window, so the dialog is now guarded by IsLoaded --
without falling through to the "transcription complete" toast.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
SummarizeParagraph returned "ERROR: ..." strings on failure and the handler
passed them straight to OpenTextInNewEditTextWindow, so a failure produced a
new editor window whose contents read like a real summary and could be saved
as one.

It now returns a WinAiGenerationResult, the way ExtractRegex and
WinAiMeetingNotes.SummarizeAsync already do, and SummarizeMenuItem_Click
mirrors MeetingNotesMenuItem_Click: a window only on success, a dialog on
failure.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ReleaseModel disposed _languageModel without taking _modelLock, which
GetModelAsync creates the model under. GrabFrame calls it from the translate
toggle and from its cleanup, so it could dispose the model while another
window was still building one. The real work now lives in ReleaseModelAsync,
which takes the lock (RestartModelAsync delegates to it); the sync
ReleaseModel is a fire-and-forget wrapper for callers that cannot await.

Cleanup no longer disposes the two semaphores. They hold nothing worth
reclaiming at exit, and disposing them throws ObjectDisposedException into
any pending WaitAsync -- after which InferenceLease.Dispose skips its
Release and the inference queue is wedged for good. It disposes the model
directly instead, since the process is exiting.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every non-success result from the Translate post-grab action showed a modal.
NotNeeded is returned whenever the text already looks like the target
language, so an English user grabbing English text got a "Nothing to
Translate" dialog after every fullscreen grab, and Unavailable produced a
"Translation Failed" dialog on every grab on non-Copilot+ hardware. The text
is unchanged in both cases, so the dialog told the user nothing.

Both are now suppressed on this automatic path; genuine failures still
report.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
translationTimer restarts on every draw, resize and OCR refresh, so a second
PerformTranslationAsync could start while the first was still awaiting
TranslateBatchAsync. Both mutate the shared progress counters, and the first
run streamed its results into word borders indexed against the second run's
bordersToTranslate list.

An isTranslating guard now keeps one pass at a time. It is set immediately
before the try -- nothing above it awaits, so no second pass can slip in
first, and a throw during the setup cannot leave the flag stuck on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
GetAllTemplates returned a new list holding the same GrabTemplate instances
as the cache, so a caller that edited a template and then abandoned the edit
left the change in the cache -- and in every other caller list -- for the
rest of the process. Reads and writes now hand out deep copies through a
CloneTemplate helper, which DuplicateTemplate reuses for its own copy.

The `??=` on the cache field was also not atomic, so the cache is now
guarded by a lock. Storage itself is still unguarded; the class remark says
so instead of claiming nothing is.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The "mm\:ss" format rendered a 1 h 05 m 30 s file as "05:30". Reuse
AudioTranscriptionUtilities.FormatTimecode, which already rolls over to
h:mm:ss.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@TheJoeFin
TheJoeFin merged commit 415bf22 into dev Aug 24, 2026
1 check passed
@TheJoeFin
TheJoeFin deleted the audio branch August 24, 2026 02:29
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