You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+
## What this is
6
+
7
+
Screamer is a Windows desktop push-to-talk dictation tool. Hold a hotkey, speak, release; audio is recorded at 16 kHz mono, sent to a Whisper-compatible STT endpoint, optionally cleaned up by an LLM rewrite, and typed into the active window via Win32 `SendInput`. It runs as a system-tray app with a settings dialog. Stack: Python 3 + PySide6 (Qt), `sounddevice`, `numpy`, `httpx`. Packaged with PyInstaller.
8
+
9
+
> Note: `docs/OVERVIEW.md` is a stale pre-fork scouting note (it references PyQt6 and a `transcriber.py` that no longer exists). The authoritative design doc is `docs/IMPLEMENTATION.md` + `docs/PLAN.md`, which match the current code.
There is **no pytest suite**. Each backend module has a `__main__` block used as its smoke test:
32
+
33
+
```powershell
34
+
python -m src.icons # writes 3 test PNGs (32x32)
35
+
python -m src.config # prints defaults, DPAPI roundtrip, creates APP_DIR
36
+
python -m src.audio # records 3s -> test.wav, prints duration + RMS
37
+
python -m src.hotkey # prints pressed/released (Windows only)
38
+
python -m src.injector "hello" # types into active window (Windows only)
39
+
python -m src.stt test.wav # transcribes (needs API config)
40
+
python -m src.rewrite "test sentense" # corrects text (needs API config)
41
+
python -m src.settings_dialog # launches the 4-tab dialog standalone
42
+
```
43
+
44
+
CLI scripts resolve credentials in this order: `load_config()` (QSettings + DPAPI) → backfill empty fields from a `.env` at cwd via `import_from_env()` → if still empty, print a setup message to stderr and `exit(1)`. No hardcoded provider defaults.
45
+
46
+
## Architecture
47
+
48
+
The codebase is a strict DAG rooted at `main.py` (the composition root). These dependency rules are load-bearing — preserve them when editing:
49
+
50
+
-**`main.py` imports everything; nothing imports `main.py`.** It owns the tray icon, the `idle → recording → processing → idle` state machine, and the worker thread lifecycle.
51
+
-**The five backend modules (`audio`, `hotkey`, `stt`, `rewrite`, `injector`) must NOT import each other.** They may import only `utils.py`.
52
+
-**`stt.py` and `rewrite.py` receive `AppConfig` as a parameter** — they do not import `config.py`. `audio.py` receives device id / name / RMS threshold from `main.py`.
53
+
-**Qt (PySide6) lives only in `utils.py`, `icons.py`, `settings_dialog.py`, `main.py`.** The backend modules are Qt-free.
54
+
-**`settings_dialog.py` imports only `config.py` and `utils.py`.** It edits a *copy* of the config; the original is untouched until accept.
55
+
56
+
### Threading model
57
+
58
+
- The Qt main thread owns all UI. Recording start/stop runs on the main thread.
59
+
- The full pipeline (`transcribe → rewrite → type_text`) runs in `_WorkerThread` (a `QThread`) so it never blocks the UI. It checks a `threading.Event` (`cancel_event`) before each blocking step and emits results back via `finished_signal`.
60
+
- The hotkey listener runs its own daemon thread with a Win32 `GetMessage` pump. It communicates to the Qt main thread through `SignalBridge` (the `QObject`-with-`Signal` bridge in `utils.py`) — this cross-thread signal pattern is how worker/hotkey threads safely touch the UI.
61
+
62
+
### Error handling
63
+
64
+
Backend code raises `ScreamerError(AppError.X, detail=...)` — never bare `print()` or swallowed exceptions. `AppError` (in `utils.py`) is an enum whose `.value` is a user-facing message. `main.py` surfaces these as tray balloon notifications. Non-fatal issues (fallback used, rewrite failed) are carried as `PipelineResult.warnings` rather than raised. When adding a new failure mode, add an `AppError` enum member rather than inventing an ad-hoc message.
65
+
66
+
### Config & secrets
67
+
68
+
- Plain settings persist via `QSettings` (IniFormat). API-key fields are encrypted with **Windows DPAPI** before being written (see `_SECRET_FIELDS` in `config.py`).
69
+
- All app data lives under `%LOCALAPPDATA%/Screamer/` (`APP_DIR` in `utils.py`). Logs go to a rotating `screamer.log` there.
70
+
-**Never log `api_key` values. Never log transcript text unless `setup_logging(debug=True)`.**
71
+
72
+
### Platform guards
73
+
74
+
Windows-first, but every module must **import** cleanly on any OS (agents may run on Linux/macOS). Windows-only runtime paths (`hotkey.py`, `injector.py`, DPAPI in `config.py`) guard Win32 calls behind `platform.system() == "Windows"` and raise `ScreamerError(AppError.UNSUPPORTED_PLATFORM)` at *runtime* rather than crashing at import time. DPAPI roundtrip, `RegisterHotKey`, and `SendInput` can only be fully verified on Windows.
75
+
76
+
## Conventions
77
+
78
+
- Public API surface of each module is fixed by the contracts in `docs/IMPLEMENTATION.md`. Phase 2 (`main.py`, `settings_dialog.py`) wires Phase 1 modules using only those exports — if you change a backend signature, update that doc.
79
+
- No new third-party dependencies and no new modules beyond the 10 in `src/` without a strong reason; the project is deliberately small.
80
+
- Hotkeys are `config.Hotkey` value objects (modifiers + one key/mouse trigger), serialized to a canonical string (`ctrl+alt+key:0x20`, `ctrl+mouse:x1`); legacy preset keys auto-migrate via `Hotkey.parse`. Presets live in `HOTKEY_OPTIONS` (`config.py`); the listener uses low-level hooks (`WH_KEYBOARD_LL`/`WH_MOUSE_LL`) and swallows the matched trigger. Add safe-bind-alone keys via `SAFE_STANDALONE_KEYS` in `config.py`.
Copy file name to clipboardExpand all lines: docs/FEATURES.md
+16-1Lines changed: 16 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -2,7 +2,22 @@
2
2
3
3
## PRIORITY FEATURE**
4
4
5
-
Async batching of transcriptions after 5 seconds using queue to boost performance significantly.
5
+
Optimization of the STT transcription pipeline speed.
6
+
7
+
Fix this issue:
8
+
When dictating with Screamer, injected text appears correctly in web browsers but NOT in Windows Notepad. The app reports success - no error is shown.
9
+
Root Cause: Modifier Key Interference
10
+
The default hotkey is Ctrl+Alt+Space. In src/hotkey.py:215-229, the release watcher (_watch_release) only polls for the primary key (Space, VK=0x20). It does not monitor the modifier keys (Ctrl, Alt):
11
+
state = user32.GetAsyncKeyState(vk) # only checks 0x20 (Space)
12
+
In toggle mode, _finalize_recording() is called directly from the hotkey-pressed handler while Ctrl+Alt+Space is still physically held. The type_text() call via SendInput with KEYEVENTF_UNICODE then runs while Ctrl/Alt are still logically down.
13
+
Notepad uses the classic Win32 EDIT control, which respects the current keyboard modifier state. When Ctrl or Alt is held, characters injected via KEYEVENTF_UNICODE/VK_PACKET can be dropped or misinterpreted as accelerators.
14
+
Browsers work because they use modern text frameworks (TSF, DirectInput, contenteditable) that handle VK_PACKET robustly regardless of modifier state - so the same SendInput call succeeds there.
15
+
Possible Fixes
16
+
Fix What Where
17
+
1. Wait for all hotkey keys Poll GetAsyncKeyState for both the primary key AND modifier keys (Ctrl, Alt) before emitting hotkey_released src/hotkey.py:_watch_release
18
+
2. Explicitly init KEYBDINPUT Set wVk = 0, time = 0, dwExtraInfo = 0 explicitly in _send_unicode and _send_vk (currently relies on implicit ctypes zero-init) src/injector.py:_send_unicode, _send_vk
19
+
3. Log target window Call GetForegroundWindow + GetClassNameW before SendInput to see which window actually receives the text src/injector.py:type_text
20
+
Fix #1 is the most important - it prevents the pipeline from injecting text while modifier keys are still physically held. Fixes #2 and #3 are low-risk hygiene improvements that help with debugging.
0 commit comments