Audio version — Estimated duration: 5 min 2 sec
Flow
This page walks through the entire lifecycle of a dictate session — from the moment you press the hotkey to the moment text appears on screen — explaining what happens at each step and why.
The Complete Lifecycle
When you use Whisper Dictate, five phases execute in sequence:
KEY DOWN → RECORDING → (STREAM PREVIEWS) → KEY UP → TRANSCRIPTION → TEXT INJECTION
The stream previews happen multiple times during the recording phase (every 2 seconds) and are completely independent from the final transcription.
Sequence Diagram
This diagram shows the exact sequence of events between the user, the system components, and the code. Time flows from top to bottom. Each arrow represents a function call or event notification.
sequenceDiagram participant User participant evdev participant StateMachine participant Recorder participant Buffer participant StreamPreview participant Whisper participant Injector Note over User,Injector: PHASE 1 — KEY PRESS User->>evdev: Press Right Alt evdev->>StateMachine: event: KEY_DOWN (value=1) StateMachine->>StateMachine: Check _hotkey_held (was False → proceed) StateMachine->>StateMachine: Set _hotkey_held = True Note over StateMachine,Buffer: Reset for new recording StateMachine->>Buffer: Clear _audio_chunks = [] StateMachine->>Buffer: Reset _stream_idx = 0 StateMachine->>Recorder: Spawn _record_loop thread StateMachine->>User: Notify "Recording..." Note over User,Injector: PHASE 2 — RECORDING loop While hotkey is held Recorder->>Buffer: Append audio chunk (every ~32ms) end Note over Recorder,StreamPreview: PHASE 3 — STREAM PREVIEWS loop Every 2 seconds during recording Recorder->>StreamPreview: Spawn _stream_transcribe thread StreamPreview->>Buffer: Get chunks from _stream_idx to end StreamPreview->>StreamPreview: Update _stream_idx StreamPreview->>Whisper: Transcribe partial audio Whisper-->>StreamPreview: Partial text segments StreamPreview->>User: Print ">> text" to terminal (NO typing) end Note over User,Injector: PHASE 4 — KEY RELEASE User->>evdev: Release Right Alt evdev->>StateMachine: event: KEY_UP (value=0) StateMachine->>StateMachine: Check _hotkey_held (was True → proceed) StateMachine->>StateMachine: Set _hotkey_held = False StateMachine->>Buffer: Set _recording = False StateMachine->>Buffer: Save snapshot: chunks = list(_audio_chunks) StateMachine->>Injector: Spawn _transcribe_and_type thread Note over User,Injector: PHASE 5 — TRANSCRIPTION + TYPING Injector->>Buffer: Use snapshot (safe from any resets) Injector->>Injector: Concatenate all chunks → single audio array Note over Injector: Check duration ≥ 0.3s Injector->>Whisper: Transcribe full audio Whisper-->>Injector: Complete transcription text Injector->>Injector: Check _hotkey_held one more time alt Hotkey still NOT held (normal case) Injector->>Injector: Call _type_text() Injector->>Injector: ydotool types text Injector->>User: Notify "Typed" else Hotkey pressed again (race condition) Injector->>Injector: Skip ydotool (would cause AltGr issues) Injector->>Injector: wl-copy to clipboard Injector->>User: Notify "Copied to clipboard, press Ctrl+V" end
Phase-by-Phase Walkthrough
Phase 1: Key Press
When you press Right Alt, the kernel generates an EV_KEY event with value=1 (press). The monitor_device() coroutine, which is watching this keyboard device through evdev’s async loop, picks up this event and calls on_key_down().
What happens inside on_key_down():
- Check
_hotkey_held— if already True, this is a key-repeat event (value=2) and we return immediately. This debounce prevents multiple recordings from starting on a single hold. - Set
_hotkey_held = True— prevents repeat events from triggering again. - Under lock protection, clear the audio buffer and reset the stream index for a fresh recording.
- Print “Recording…” to terminal and show a desktop notification.
- Start the recording loop in a background thread and return immediately (non-blocking).
The entire on_key_down() runs very fast (microseconds) and does not block the event loop.
Phase 2: Recording
The recording loop (_record_loop()) opens a sounddevice InputStream with:
- Sample rate: 16000 Hz (16 kHz — what Whisper expects)
- Channels: 1 (mono)
- Dtype: float32 (standard for audio processing)
- Callback:
_audio_callback()— runs each time a new audio chunk is available
The callback simply checks if _recording is still True (the loop stops shortly after key-up) and appends the chunk to the shared _audio_chunks list.
Between callback invocations, the main recording loop sleeps for 50ms (0.05s) and periodically checks if it’s time to spawn a stream preview.
Phase 3: Stream Previews
Every 2 seconds (controlled by _stream_interval), the recording loop spawns a _stream_transcribe() thread. This thread:
- Acquires the lock and reads chunks from
_stream_idxto the end of the buffer - Updates
_stream_idxso the next preview only gets new chunks - Concatenates those chunks and transcribes them with Whisper
- Prints the result to terminal with
>>prefix
Stream previews are terminal-only. They never inject text into applications. This is a deliberate design decision — see Stream-Preview for the full explanation of why.
Phase 4: Key Release
When you release Right Alt, on_key_up() is called. This is a critical moment — the transition from recording to processing.
What happens inside on_key_up():
- Check
_hotkey_held— if already False, ignore (debounce). - Set
_hotkey_held = False— resets the state for next press. - Under lock protection, set
_recording = False(which causes the recording loop to exit) and take a snapshot of the audio buffer. - Spawn the transcription worker with the snapshot, then return immediately (non-blocking).
Why the snapshot is important: If the user presses the hotkey again immediately (before transcription finishes), on_key_down() would reset _audio_chunks = []. Without the snapshot, our transcription worker would be working with an empty buffer. By capturing the snapshot before spawning the worker, we ensure the transcription always uses the correct audio.
Phase 5: Transcription + Typing
The transcription worker (_transcribe_and_type(snapshot)) runs in its own thread and performs these steps:
- Check empty — If the snapshot has no chunks, return immediately.
- Concatenate — Join all audio chunks into a single 1D array of floats.
- Check duration — If total audio is shorter than
MIN_DURATION(default 0.3s), show “Too short” notification and return. This prevents accidental taps from spamming text. - Transcribe — Send the audio to Whisper. The model runs on GPU (CUDA) with beam search (beam_size=5) for better accuracy. Language auto-detection is on by default.
- Check hotkey state — Before typing, check
_hotkey_heldone final time:- If not held (normal case): Call
_type_text()which runsydotool typeto inject keystrokes into the focused window. - If held (user pressed hotkey again): Use
wl-copyto copy to clipboard instead. This prevents ydotool from typing while Alt is active (which would trigger AltGr/screenshot shortcuts).
- If not held (normal case): Call
- Notify — Show a desktop notification with the result.
Edge Cases
| Scenario | What Happens |
|---|---|
| Hotkey pressed but released immediately (<0.3s) | “Too short” notification, nothing typed |
| Hotkey pressed while previous transcription is still processing | Current transcription completes but text is copied to clipboard (not typed) to prevent AltGr issues |
| ydotool not installed or fails | Automatic fallback to wl-clipboard with notification |
| Microphone not available | Terminal error message, desktop notification |
| No speech detected in recording | ”Nothing detected” notification |
Now that you understand the flow, see threading for how multiple threads coordinate safely, or components for implementation details of each piece.