Skip to content

Commit f51b8c9

Browse files
committed
initial commit
0 parents  commit f51b8c9

28 files changed

Lines changed: 9517 additions & 0 deletions

.github/workflows/ci.yml

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main, master]
6+
pull_request:
7+
branches: [main, master]
8+
9+
env:
10+
CARGO_TERM_COLOR: always
11+
RUST_BACKTRACE: short
12+
13+
jobs:
14+
check:
15+
name: ${{ matrix.platform.label }}
16+
runs-on: ${{ matrix.platform.os }}
17+
strategy:
18+
fail-fast: false
19+
matrix:
20+
platform:
21+
- { os: ubuntu-latest, label: ubuntu }
22+
- { os: macos-latest, label: macos }
23+
- { os: windows-latest, label: windows }
24+
25+
steps:
26+
- uses: actions/checkout@v4
27+
28+
# ──────────────── system deps ────────────────
29+
- name: Install Linux system deps
30+
if: matrix.platform.label == 'ubuntu'
31+
run: |
32+
sudo apt-get update
33+
sudo apt-get install -y \
34+
libwebkit2gtk-4.1-dev \
35+
libappindicator3-dev \
36+
librsvg2-dev \
37+
patchelf \
38+
libgtk-3-dev \
39+
libsoup-3.0-dev \
40+
libjavascriptcoregtk-4.1-dev \
41+
libxdo-dev \
42+
libasound2-dev \
43+
libssl-dev \
44+
pkg-config
45+
46+
# ──────────────── toolchains ────────────────
47+
- uses: dtolnay/rust-toolchain@stable
48+
with:
49+
components: rustfmt, clippy
50+
51+
- uses: actions/setup-node@v4
52+
with:
53+
node-version: 20
54+
cache: npm
55+
56+
- name: Cargo cache
57+
uses: Swatinem/rust-cache@v2
58+
with:
59+
workspaces: ./src-tauri -> target
60+
shared-key: ${{ matrix.platform.label }}
61+
62+
# ──────────────── frontend (needed for tauri::generate_context!) ────────────────
63+
- name: Install npm deps
64+
run: npm ci || npm install
65+
66+
- name: Build frontend
67+
run: npm run build
68+
69+
# ──────────────── checks ────────────────
70+
- name: Rustfmt
71+
working-directory: src-tauri
72+
run: cargo fmt --all -- --check
73+
74+
- name: Clippy
75+
working-directory: src-tauri
76+
run: cargo clippy --all-targets -- -D warnings
77+
78+
- name: Tests
79+
working-directory: src-tauri
80+
run: cargo test --all-targets
81+
82+
- name: Build (debug)
83+
working-directory: src-tauri
84+
run: cargo build

.gitignore

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Rust
2+
/src-tauri/target/
3+
**/*.rs.bk
4+
5+
# Node
6+
node_modules/
7+
dist/
8+
.vite/
9+
10+
# Tauri build artifacts
11+
/src-tauri/gen/
12+
13+
# Editor / OS
14+
.DS_Store
15+
.idea/
16+
.vscode/
17+
*.swp
18+
19+
# Local config
20+
.env
21+
.env.local

README.md

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
# qol
2+
3+
Voice-to-text dictation overlay. Hold a hotkey, talk, release — your cleaned-up
4+
words appear in whatever app has focus. Powered by
5+
[Aavaaz](../Aavaaz/aavaaz/) for streaming transcription and an LLM polish pass
6+
for filler removal, punctuation, and per-app tone.
7+
8+
`qol` (қол) means "voice" in Kazakh — a sibling name to Aavaaz ("voice" in Hindi).
9+
10+
## Architecture
11+
12+
```
13+
┌──────────────────────────────────────────────────┐
14+
│ qol (Tauri desktop app) │
15+
│ │
16+
│ global-shortcut ──► push-to-talk │
17+
│ cpal ──► 16 kHz mono PCM │
18+
│ tokio-tungstenite──► ws://localhost:9090 │ ── Aavaaz/WhisperLive ──► transcript
19+
│ active-win ──► focused app context │
20+
│ Claude API ──► polish (tone, punctuation) │
21+
│ enigo ──► inject text into focused app│
22+
│ │
23+
│ webview ──► settings UI (Vite + TS) │
24+
└──────────────────────────────────────────────────┘
25+
```
26+
27+
## Layout
28+
29+
| Path | Purpose |
30+
|---|---|
31+
| `src-tauri/Cargo.toml` | Rust deps (tauri, cpal, tokio-tungstenite, enigo, reqwest) |
32+
| `src-tauri/src/main.rs` | App entry, hotkey wiring, Tauri commands |
33+
| `src-tauri/src/audio.rs` | Mic capture → 16 kHz mono f32 frames |
34+
| `src-tauri/src/transport.rs` | WebSocket session to Aavaaz/WhisperLive |
35+
| `src-tauri/src/session.rs` | Lifecycle: audio → transport → polish → inject |
36+
| `src-tauri/src/inject.rs` | Keystroke injection (enigo) + active-window probe |
37+
| `src-tauri/src/polish.rs` | Claude API call for transcript cleanup |
38+
| `src-tauri/src/config.rs` | JSON config in `~/.config/qol/config.json` |
39+
| `index.html` + `src/` | Vite settings UI |
40+
41+
## Prerequisites
42+
43+
- Rust 1.77+ (`rustup toolchain install stable`)
44+
- Node 20+ (`pnpm` or `npm`)
45+
- A running Aavaaz instance at `ws://localhost:9090`
46+
- An `ANTHROPIC_API_KEY` env var if you want the polish pass (optional — raw transcripts inject fine without it)
47+
48+
### Fedora-specific system deps
49+
50+
```bash
51+
sudo dnf install -y \
52+
webkit2gtk4.1-devel \
53+
openssl-devel \
54+
curl wget file \
55+
libappindicator-gtk3-devel \
56+
librsvg2-devel \
57+
gtk3-devel \
58+
alsa-lib-devel \
59+
libxdo-devel
60+
```
61+
62+
`libxdo-devel` is needed by `enigo` on X11. On Wayland, you may also need a
63+
`ydotool` daemon for reliable injection (GNOME Wayland blocks synthetic input
64+
otherwise).
65+
66+
## Run
67+
68+
```bash
69+
# in one terminal — start Aavaaz with a model that fits your GPU
70+
cd ../Aavaaz/aavaaz
71+
source .venv/bin/activate
72+
aavaaz serve --model distil-large-v3
73+
74+
# in another — build and run qol
75+
cd ../../qol
76+
npm install
77+
npm run tauri dev
78+
```
79+
80+
Then press your hotkey (default `Super+Space`), speak, and release.
81+
82+
## Settings
83+
84+
Edit via the settings window (open from system tray), or directly at
85+
`~/.config/qol/config.json`:
86+
87+
```json
88+
{
89+
"aavaaz_url": "ws://localhost:9090",
90+
"model": "distil-large-v3",
91+
"language": "en",
92+
"hotkey": "Super+Space",
93+
"polish": {
94+
"enabled": true,
95+
"provider": "anthropic",
96+
"model": "claude-haiku-4-5-20251001",
97+
"api_key_env": "ANTHROPIC_API_KEY",
98+
"per_app_tone": true
99+
},
100+
"hotwords": ["Aavaaz", "qol", "WhisperLive"],
101+
"inject_method": "type"
102+
}
103+
```
104+
105+
## Tests & CI
106+
107+
```bash
108+
cd src-tauri
109+
cargo test # unit tests (config round-trip, hotkey parser)
110+
cargo clippy --all-targets -- -D warnings
111+
cargo fmt -- --check
112+
```
113+
114+
CI runs the above on **Ubuntu, macOS, and Windows** for every push and PR — see
115+
[.github/workflows/ci.yml](.github/workflows/ci.yml).
116+
117+
Hardware-bound paths (audio capture, keystroke injection) and network-bound
118+
paths (WebSocket session, Claude polish) aren't unit-tested yet; integration
119+
tests with a fake Aavaaz endpoint and a virtual audio device are a TODO.
120+
121+
## Status
122+
123+
This is a scaffold. Working / stubbed:
124+
125+
- [x] Mic capture with cheap linear resampling to 16 kHz
126+
- [x] WebSocket session with Aavaaz/WhisperLive handshake
127+
- [x] LLM polish pass with per-app tone hint
128+
- [x] enigo-based text injection
129+
- [x] Global hotkey via `tauri-plugin-global-shortcut`
130+
- [x] Settings UI
131+
- [ ] Proper resampling (replace linear with `rubato`)
132+
- [ ] Wayland injection fallback (`ydotool` IPC)
133+
- [ ] Streaming injection (type as segments arrive vs. only at release)
134+
- [ ] Per-app tone profiles configurable in UI
135+
- [ ] Voice commands ("new line", "scratch that")
136+
- [ ] Local-only polish via llama.cpp
137+
- [ ] Tray menu (pause, open settings, quit)
138+
139+
## License
140+
141+
MPL-2.0

index.html

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>qol — settings</title>
7+
<link rel="stylesheet" href="/src/styles.css" />
8+
</head>
9+
<body>
10+
<main id="app">
11+
<h1>qol</h1>
12+
<p class="sub">Voice dictation powered by Aavaaz.</p>
13+
14+
<form id="settings">
15+
<label>
16+
Aavaaz WebSocket URL
17+
<input name="aavaaz_url" type="text" placeholder="ws://localhost:9090" />
18+
</label>
19+
20+
<label>
21+
Model
22+
<select name="model">
23+
<option value="distil-large-v3">distil-large-v3 (recommended)</option>
24+
<option value="large-v3">large-v3</option>
25+
<option value="medium">medium</option>
26+
<option value="small">small</option>
27+
</select>
28+
</label>
29+
30+
<label>
31+
Language
32+
<input name="language" type="text" placeholder="en" />
33+
</label>
34+
35+
<label>
36+
Push-to-talk hotkey
37+
<input name="hotkey" type="text" placeholder="Super+Space" />
38+
</label>
39+
40+
<fieldset>
41+
<legend>LLM polish</legend>
42+
<label class="row">
43+
<input name="polish_enabled" type="checkbox" />
44+
Clean up transcripts (remove fillers, fix punctuation, match app tone)
45+
</label>
46+
<label>
47+
Polish model
48+
<input name="polish_model" type="text" />
49+
</label>
50+
<label class="row">
51+
<input name="per_app_tone" type="checkbox" />
52+
Adapt tone to the focused app
53+
</label>
54+
</fieldset>
55+
56+
<label>
57+
Hotwords (comma-separated)
58+
<input name="hotwords" type="text" placeholder="Aavaaz,qol,WhisperLive" />
59+
</label>
60+
61+
<div class="actions">
62+
<button type="button" id="test">Test connection</button>
63+
<button type="submit">Save</button>
64+
</div>
65+
<p id="status"></p>
66+
</form>
67+
</main>
68+
<script type="module" src="/src/main.ts"></script>
69+
</body>
70+
</html>

0 commit comments

Comments
 (0)