|
| 1 | +# CLAUDE.md |
| 2 | + |
| 3 | +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. |
| 4 | + |
| 5 | +## Build & Test |
| 6 | + |
| 7 | +```bash |
| 8 | +# CPU build (default) |
| 9 | +nix-shell --run "cargo build --release" |
| 10 | + |
| 11 | +# CUDA GPU build |
| 12 | +nix-shell --run "cargo build --release --features cuda" |
| 13 | + |
| 14 | +# Unit tests only (50 tests, no model needed) |
| 15 | +nix-shell --run "cargo test --release --bin dictr" |
| 16 | + |
| 17 | +# Lib tests (config + transcribe modules via lib.rs) |
| 18 | +nix-shell --run "cargo test --release --lib" |
| 19 | + |
| 20 | +# All tests including e2e (requires model at ~/.local/share/dictr/models/ggml-base.bin) |
| 21 | +nix-shell --run "cargo test --release" |
| 22 | + |
| 23 | +# Run a single test |
| 24 | +nix-shell --run "cargo test --release --bin dictr debounce_repeated_press" |
| 25 | + |
| 26 | +# Lint and format (CI enforces these) |
| 27 | +nix-shell --run "cargo clippy -- -D warnings" |
| 28 | +nix-shell --run "cargo fmt --check" |
| 29 | +``` |
| 30 | + |
| 31 | +On non-NixOS, drop the `nix-shell --run` wrapper and install deps manually (see README). |
| 32 | + |
| 33 | +**Note:** `shell.nix` always includes CUDA packages. CPU-only builds still work — the CUDA libs are just unused. |
| 34 | + |
| 35 | +## Architecture |
| 36 | + |
| 37 | +Single-threaded event loop with two background threads: |
| 38 | + |
| 39 | +``` |
| 40 | +rdev thread ──HotkeyEvent──> main thread ──> AudioRecorder.start/stop() |
| 41 | + ──> TranscribeBackend.transcribe() |
| 42 | + ──> output::type_text() (xdotool) |
| 43 | +
|
| 44 | +cpal callback thread ──> Arc<Mutex<Vec<f32>>> shared buffer |
| 45 | +``` |
| 46 | + |
| 47 | +**Module roles:** |
| 48 | + |
| 49 | +- `main.rs` — CLI parsing, config merge via `apply_cli_overrides()`, event loop (blocks on mpsc receiver) |
| 50 | +- `config.rs` — TOML config with serde defaults, tilde expansion, env var fallback. Loaded once at startup. |
| 51 | +- `hotkey.rs` — rdev listener thread. `Debouncer` struct suppresses X11 key repeat. Sends `Pressed`/`Released` via mpsc. |
| 52 | +- `audio.rs` — cpal mic capture. Callback downmixes to mono inline. `stop()` resamples to 16kHz via rubato `FftFixedIn`. |
| 53 | +- `transcribe.rs` — `TranscribeBackend` trait with two impls: `LocalWhisper` (whisper-rs) and `ApiWhisper` (reqwest multipart POST). `encode_wav()` converts f32→i16 WAV for API upload. |
| 54 | +- `output.rs` — Shells out to `xdotool type` or `xclip` + `xdotool key ctrl+v`. Validates deps at startup. |
| 55 | +- `status.rs` — Writes state to `/tmp/dictr-status`, signals i3blocks via `pkill -RTMIN+11`. Registers SIGINT/SIGTERM cleanup via `libc::signal`. |
| 56 | +- `lib.rs` — Thin re-export of `config` and `transcribe` modules for integration tests. |
| 57 | + |
| 58 | +## Key Design Details |
| 59 | + |
| 60 | +- **CUDA is opt-in**: The `cuda` feature flag passes through to `whisper-rs/cuda`. No conditional compilation in dictr source — both backends are always compiled. |
| 61 | +- **Sync main loop**: Transcription blocks the main thread. API backend uses `tokio::runtime::Runtime` with `block_on()` to bridge sync/async. |
| 62 | +- **Reqwest client reuse**: `ApiWhisper` creates `reqwest::Client` once in `new()` and clones it per request (Arc internally, cheap clone). |
| 63 | +- **Audio buffer**: `Arc<Mutex<Vec<f32>>>` shared between cpal callback and main thread. Mutex uses `.expect()` (panics if poisoned). |
| 64 | +- **Resampler is stateless per recording**: New `FftFixedIn` instance on each `stop()` call. Remainder samples are zero-padded with proportional output truncation. |
| 65 | +- **i3blocks signal 11 is hardcoded** in `status.rs` — must match `signal=11` in i3blocks config. |
| 66 | +- **Config precedence**: CLI flags > TOML file > defaults. For `api_key`: TOML > `OPENAI_API_KEY` env var. |
| 67 | +- **Configurable fields**: `api_url` (custom API endpoint), `initial_prompt` (guide transcription), `min_duration_ms` (minimum recording length). |
| 68 | +- **Verbose mode**: `--verbose`/`-v` enables whisper.cpp/ggml log output and dictr status messages. Default is silent — whisper logs suppressed via `install_logging_hooks()`. |
| 69 | +- **Replacements**: Config supports a `[replacements]` table for case-insensitive text substitution on transcription output (e.g., `"slash " = "/"`). Empty keys are skipped. Applied in `Config::apply_replacements()`. |
| 70 | +- **E2E tests** use `LocalWhisper` via the `TranscribeBackend` trait (not raw whisper-rs). Skip gracefully if model file is missing (no CI failure). CI only runs unit tests (`--bin dictr`). |
0 commit comments