Skip to content

Commit 68180a4

Browse files
author
Mike Wassmer
committed
initial release
0 parents  commit 68180a4

22 files changed

Lines changed: 5436 additions & 0 deletions

.github/workflows/ci.yml

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
env:
10+
CARGO_TERM_COLOR: always
11+
12+
jobs:
13+
check:
14+
name: Check & Test
15+
runs-on: ubuntu-latest
16+
steps:
17+
- uses: actions/checkout@v4
18+
19+
- name: Install system dependencies
20+
run: |
21+
sudo apt-get update
22+
sudo apt-get install -y \
23+
cmake clang pkg-config \
24+
libasound2-dev \
25+
libx11-dev libxi-dev libxtst-dev libxrandr-dev \
26+
libssl-dev
27+
28+
- name: Install Rust toolchain
29+
uses: dtolnay/rust-toolchain@stable
30+
with:
31+
components: rustfmt, clippy
32+
33+
- uses: Swatinem/rust-cache@v2
34+
35+
- name: Check formatting
36+
run: cargo fmt --check
37+
38+
- name: Clippy
39+
run: cargo clippy -- -D warnings
40+
41+
- name: Build (CPU)
42+
run: cargo build --release
43+
44+
- name: Unit tests
45+
run: cargo test --release --bin dictr
46+
47+
build-cuda:
48+
name: Build (CUDA)
49+
runs-on: ubuntu-latest
50+
steps:
51+
- uses: actions/checkout@v4
52+
53+
- name: Install system dependencies
54+
run: |
55+
sudo apt-get update
56+
sudo apt-get install -y \
57+
cmake clang pkg-config \
58+
libasound2-dev \
59+
libx11-dev libxi-dev libxtst-dev libxrandr-dev \
60+
libssl-dev \
61+
nvidia-cuda-toolkit
62+
63+
- name: Install Rust toolchain
64+
uses: dtolnay/rust-toolchain@stable
65+
66+
- uses: Swatinem/rust-cache@v2
67+
with:
68+
key: cuda
69+
70+
- name: Build (CUDA)
71+
run: cargo build --release --features cuda

.github/workflows/release.yml

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
tags: ["v*"]
6+
7+
permissions:
8+
contents: write
9+
10+
env:
11+
CARGO_TERM_COLOR: always
12+
13+
jobs:
14+
build:
15+
name: Build (${{ matrix.name }})
16+
runs-on: ubuntu-latest
17+
strategy:
18+
matrix:
19+
include:
20+
- name: CPU
21+
features: ""
22+
artifact: dictr-x86_64-linux
23+
cuda_deps: false
24+
- name: CUDA
25+
features: "--features cuda"
26+
artifact: dictr-x86_64-linux-cuda
27+
cuda_deps: true
28+
steps:
29+
- uses: actions/checkout@v4
30+
31+
- name: Install system dependencies
32+
run: |
33+
sudo apt-get update
34+
sudo apt-get install -y \
35+
cmake clang pkg-config \
36+
libasound2-dev \
37+
libx11-dev libxi-dev libxtst-dev libxrandr-dev \
38+
libssl-dev
39+
40+
- name: Install CUDA toolkit
41+
if: matrix.cuda_deps
42+
run: sudo apt-get install -y nvidia-cuda-toolkit
43+
44+
- name: Install Rust toolchain
45+
uses: dtolnay/rust-toolchain@stable
46+
47+
- uses: Swatinem/rust-cache@v2
48+
with:
49+
key: release-${{ matrix.name }}
50+
51+
- name: Build
52+
run: cargo build --release ${{ matrix.features }}
53+
54+
- name: Rename binary
55+
run: cp target/release/dictr ${{ matrix.artifact }}
56+
57+
- name: Upload artifact
58+
uses: actions/upload-artifact@v4
59+
with:
60+
name: ${{ matrix.artifact }}
61+
path: ${{ matrix.artifact }}
62+
63+
release:
64+
name: Create Release
65+
needs: build
66+
runs-on: ubuntu-latest
67+
steps:
68+
- uses: actions/download-artifact@v4
69+
with:
70+
path: artifacts
71+
merge-multiple: true
72+
73+
- name: Create GitHub Release
74+
uses: softprops/action-gh-release@v2
75+
with:
76+
generate_release_notes: true
77+
files: artifacts/*

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/target
2+
*.swp
3+
*.swo
4+
*~
5+
.env

CLAUDE.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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

Comments
 (0)