Skip to content

Switch to mlx-whisper for Apple Silicon acceleration - #11

Open
goyamegh wants to merge 6 commits into
ashwin-pc:mainfrom
goyamegh:feat/mlx-whisper-optimization
Open

Switch to mlx-whisper for Apple Silicon acceleration#11
goyamegh wants to merge 6 commits into
ashwin-pc:mainfrom
goyamegh:feat/mlx-whisper-optimization

Conversation

@goyamegh

@goyamegh goyamegh commented Apr 9, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Switch from faster-whisper to mlx-whisper for native Apple Silicon (MLX/Metal) GPU inference — 2-4x faster transcription on M-series Macs
  • Eliminate temp WAV file I/O — audio buffer is passed directly as a numpy array, saving ~100-200ms per transcription
  • Add chunked transcription — audio is transcribed every 5s during recording, so text appears near-instantly when recording stops (0ms transcription wait when cached)
  • Add pipeline metrics logging — tracks buffer conversion, transcription time, RTF (real-time factor), text insertion, and total pipeline latency
  • Fix stray character on text insertion — adds a small delay before typing to let key events (Globe/Fn) fully propagate

Performance Results

Recording Length Transcription Wait Total Pipeline Path
2.75s 499ms 919ms Full (short recording)
5.70s 0ms (cached) 472ms Chunked — used partial
9.22s 606ms 1044ms Chunked + tail
10.24s 575ms 969ms Chunked + tail

Test plan

  • Verify app starts and model loads successfully on Apple Silicon Mac
  • Test short recordings (<5s) — should use full transcription path
  • Test longer recordings (>5s) — should show chunk transcription in logs and faster final result
  • Verify text selection + Bedrock enhancement flow still works
  • Verify no stray characters prepended to transcription output
  • Check [METRICS] log lines for latency tracking

🤖 Generated with Claude Code

…transcription

- Replace faster-whisper with mlx-whisper for native Apple Silicon (MLX/Metal) inference
- Eliminate temp WAV file I/O by passing audio buffer directly as numpy array
- Add chunked transcription that processes audio every 5s during recording,
  so text appears near-instantly when recording stops
- Add pipeline metrics logging (transcription time, RTF, total latency)
- Warm up model on startup to avoid cold-start penalty on first transcription
- Fix stray character prepended to transcription by adding a small delay
  before typing to let key events fully propagate

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 9, 2026 15:36

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 switches the dictation backend from faster-whisper to mlx-whisper to leverage Apple Silicon (MLX/Metal) acceleration, removes temp WAV file I/O by transcribing directly from a NumPy buffer, and introduces periodic chunked transcription plus detailed pipeline metrics logging.

Changes:

  • Replace faster-whisper file-based transcription with mlx-whisper NumPy-array transcription (incl. model warm-up).
  • Add a chunked transcription loop during recording to cache partial results and reduce final wait time.
  • Add [METRICS] logging for model load, buffer conversion, transcription, enhancement, insertion, and total pipeline latency.

Reviewed changes

Copilot reviewed 1 out of 2 changed files in this pull request and generated 5 comments.

File Description
src/main.py Switches transcription to mlx_whisper, adds chunked transcription + metrics, and adjusts insertion timing.
requirements.txt Replaces faster-whisper dependency with mlx-whisper.

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

Comment thread src/main.py
if hasattr(self, 'recording_thread') and self.recording_thread.is_alive():
self.recording_thread.join(timeout=1.0)

Copilot AI Apr 9, 2026

Copy link

Choose a reason for hiding this comment

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

cleanup() stops the recording thread but does not signal/join the chunked transcription thread. If the app exits while recording, chunk_thread can keep running and prevent a clean shutdown (or force the watchdog to os._exit). Consider setting chunked_stop in cleanup() and joining chunk_thread (with a bounded timeout), similar to stop_recording()/discard_recording().

Suggested change
# Stop chunked transcription thread if it is running
if hasattr(self, 'chunked_stop'):
self.chunked_stop.set()
if hasattr(self, 'chunk_thread') and self.chunk_thread.is_alive():
self.chunk_thread.join(timeout=1.0)

Copilot uses AI. Check for mistakes.
Comment thread src/main.py Outdated
Comment on lines 216 to 225
self.chunked_stop.set()
if hasattr(self, 'chunk_thread') and self.chunk_thread.is_alive():
self.chunk_thread.join(timeout=2.0)
self.frames = []

Copilot AI Apr 9, 2026

Copy link

Choose a reason for hiding this comment

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

discard_recording() joins recording_thread with a timeout and then immediately reassigns self.frames = []. If recording_thread is still alive, it can append to the new list after discard, leaving unexpected audio buffered and potentially confusing later transcriptions. Consider waiting until recording_thread fully exits (or guarding frame appends with a lock / local reference) before resetting frames.

Copilot uses AI. Check for mistakes.
Comment thread src/main.py
Comment on lines 8 to 12
from pynput import keyboard
from pynput.keyboard import Key, Controller
import faster_whisper
import mlx_whisper
import signal
from text_selection import TextSelection

Copilot AI Apr 9, 2026

Copy link

Choose a reason for hiding this comment

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

The code now imports/depends on mlx_whisper, which (per MLX) only works on Apple Silicon. On Intel macOS this will fail at import time and prevent the app from starting. Consider adding an explicit runtime architecture check with a clear status/error message (and/or a fallback backend) so users get an actionable explanation instead of a crash.

Copilot uses AI. Check for mistakes.
Comment thread src/main.py Outdated
Comment on lines +332 to +335
# Stop chunked transcription and wait for it to finish
self.chunked_stop.set()
if hasattr(self, 'chunk_thread'):
self.chunk_thread.join(timeout=5.0)

Copilot AI Apr 9, 2026

Copy link

Choose a reason for hiding this comment

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

stop_recording() joins chunk_thread with a 5s timeout but doesn’t verify that it actually stopped before starting process_recording(). If chunked_transcribe_loop is in the middle of a long mlx_whisper.transcribe call, it may keep running concurrently with the final transcription, causing concurrent model executions and races on partial_text/last_transcribed_frame_count. Consider joining without timeout, or (if you must bound it) checking is_alive() and skipping cached/partial usage until the thread has fully exited.

Suggested change
# Stop chunked transcription and wait for it to finish
self.chunked_stop.set()
if hasattr(self, 'chunk_thread'):
self.chunk_thread.join(timeout=5.0)
# Stop chunked transcription and wait for it to fully finish
# before starting the final transcription pass. A timed join can
# return while the thread is still inside a long-running transcribe
# call, which would allow concurrent model execution and races on
# shared transcription state.
self.chunked_stop.set()
if hasattr(self, 'chunk_thread'):
self.chunk_thread.join()

Copilot uses AI. Check for mistakes.
Comment thread src/main.py Outdated
Comment on lines 221 to 225
# Stop chunked transcription if running
self.chunked_stop.set()
if hasattr(self, 'chunk_thread') and self.chunk_thread.is_alive():
self.chunk_thread.join(timeout=2.0)
self.frames = []

Copilot AI Apr 9, 2026

Copy link

Choose a reason for hiding this comment

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

discard_recording() joins chunk_thread with a 2s timeout and then clears frames/UI state. If chunked_transcribe_loop is still running (e.g., stuck in mlx_whisper.transcribe), it can continue consuming resources and mutate partial_text/last_transcribed_frame_count after the recording is discarded. Consider ensuring the thread has actually exited (or making the transcription call cancellable / running it in a separate process) before resetting state.

Copilot uses AI. Check for mistakes.
goyamegh and others added 5 commits April 9, 2026 09:25
- Add Apple Silicon (arm64) check at startup with clear error message
- Stop chunked transcription thread in cleanup() for clean shutdown
- Remove timeouts on chunk_thread.join() in stop_recording() and
  discard_recording() to prevent concurrent model execution races
- Wait for recording thread to fully exit in discard_recording()
  before clearing frames to prevent race condition

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Skip Cmd+C selected-text check when Bedrock is unavailable (common path),
and add timing guards around keyboard modifier sequences to prevent
key events leaking as literal characters in Electron apps.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The Cmd+C simulation in get_selected_text() leaks a literal 'c' into
the target app. Since boto3 client init succeeds whenever AWS creds
exist (common for Amazon employees), the previous is_available() guard
was insufficient. Now the Cmd+C check only runs when the user explicitly
opts in via ENABLE_TEXT_ENHANCEMENT=true.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Switch from whisper-medium.en to distil-whisper-large-v3 for better
accuracy. Add language="en" to skip auto-detection, initial_prompt with
domain vocabulary for software engineering terms, and disable
condition_on_previous_text for chunked transcription to prevent error
propagation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Prevent crashes on long audio by adding thread-safe frame buffer access,
stream cleanup guarantees, join timeouts, and division-by-zero guards.
Add persistent file logging for crash diagnostics. Include OTEL/OpenTelemetry
in whisper initial prompt to fix misrecognition as "hotel".

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