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():

  1. 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.
  2. Set _hotkey_held = True — prevents repeat events from triggering again.
  3. Under lock protection, clear the audio buffer and reset the stream index for a fresh recording.
  4. Print “Recording…” to terminal and show a desktop notification.
  5. 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:

  1. Acquires the lock and reads chunks from _stream_idx to the end of the buffer
  2. Updates _stream_idx so the next preview only gets new chunks
  3. Concatenates those chunks and transcribes them with Whisper
  4. 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():

  1. Check _hotkey_held — if already False, ignore (debounce).
  2. Set _hotkey_held = False — resets the state for next press.
  3. Under lock protection, set _recording = False (which causes the recording loop to exit) and take a snapshot of the audio buffer.
  4. 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:

  1. Check empty — If the snapshot has no chunks, return immediately.
  2. Concatenate — Join all audio chunks into a single 1D array of floats.
  3. 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.
  4. 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.
  5. Check hotkey state — Before typing, check _hotkey_held one final time:
    • If not held (normal case): Call _type_text() which runs ydotool type to inject keystrokes into the focused window.
    • If held (user pressed hotkey again): Use wl-copy to copy to clipboard instead. This prevents ydotool from typing while Alt is active (which would trigger AltGr/screenshot shortcuts).
  6. Notify — Show a desktop notification with the result.

Edge Cases

ScenarioWhat Happens
Hotkey pressed but released immediately (<0.3s)“Too short” notification, nothing typed
Hotkey pressed while previous transcription is still processingCurrent transcription completes but text is copied to clipboard (not typed) to prevent AltGr issues
ydotool not installed or failsAutomatic fallback to wl-clipboard with notification
Microphone not availableTerminal 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.