Skip to content

Commit 838031b

Browse files
committed
chore: update documentation
1 parent abf6a69 commit 838031b

4 files changed

Lines changed: 230 additions & 107 deletions

File tree

docs/IMPLEMENTATION.md

Lines changed: 89 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ src/
2222
├── rewrite.py
2323
├── injector.py
2424
├── icons.py
25-
└── utils.py
25+
├── utils.py
26+
└── startup.py
2627
requirements.txt (repo root)
2728
```
2829

@@ -46,6 +47,8 @@ class AppError(Enum):
4647
INJECTION_FAILED = "Could not type text. Focus may have changed."
4748
HOTKEY_CONFLICT = "Hotkey conflict. Choose a different hotkey."
4849
UNSUPPORTED_PLATFORM = "This feature is only available on Windows."
50+
KEY_STORAGE_FAILED = "Could not save or load API keys securely."
51+
STARTUP_REGISTRATION_FAILED = "Could not update Windows startup setting."
4952

5053
class ScreamerError(Exception):
5154
def __init__(self, code: AppError, detail: str | None = None): ...
@@ -73,14 +76,43 @@ DEFAULT_LLM_SYSTEM_PROMPT: str = (
7376
)
7477

7578
```python
79+
DEFAULT_RMS_THRESHOLD: float = 5.0
80+
81+
```python
82+
@dataclass(frozen=True)
83+
class HotkeyBinding:
84+
modifiers: int
85+
vk: int
86+
87+
HOTKEY_OPTIONS: list[tuple[str, str]] # (key, display_label) pairs for combo hotkeys
88+
HOTKEY_BINDINGS: dict[str, HotkeyBinding] # maps hotkey name → HotkeyBinding
89+
90+
@dataclass(frozen=True)
91+
class ProviderConfig:
92+
api_key: str = ""
93+
base_url: str = ""
94+
model: str = ""
95+
custom_headers: str = ""
96+
97+
@dataclass(frozen=True)
98+
class FallbackProviderConfig:
99+
enabled: bool = False
100+
provider: ProviderConfig = field(default_factory=ProviderConfig)
101+
102+
@dataclass(frozen=True)
103+
class ConfigValidationIssue:
104+
message: str
105+
tab_index: int = 0
106+
76107
@dataclass
77108
class AppConfig:
78-
hotkey: str = "scroll_lock"
109+
hotkey: str = "ctrl_alt_space"
79110
recording_mode: str = "hold" # "hold" | "toggle"
80111
post_type_key: str = "none" # "none" | "enter" | "tab" | "space" | "backspace"
112+
start_with_windows: bool = False
81113
audio_device_id: int | None = None
82114
audio_device_name: str = ""
83-
rms_threshold: float = 50.0
115+
rms_threshold: float = 5.0
84116
# STT primary
85117
stt_api_key: str = ""
86118
stt_base_url: str = ""
@@ -107,6 +139,11 @@ class AppConfig:
107139
llm_fallback_model: str = ""
108140
llm_fallback_custom_headers: str = ""
109141

142+
def stt_provider(self) -> ProviderConfig: ...
143+
def stt_fallback_provider(self) -> FallbackProviderConfig: ...
144+
def llm_provider(self) -> ProviderConfig: ...
145+
def llm_fallback_provider(self) -> FallbackProviderConfig: ...
146+
110147
def load_config() -> AppConfig: ...
111148
"""Load QSettings + DPAPI. Unknown keys get field defaults."""
112149

@@ -122,6 +159,12 @@ def import_from_env(cfg: AppConfig) -> AppConfig: ...
122159
def setup_logging(debug: bool = False) -> None: ...
123160
"""Rotating file at APP_DIR/screamer.log. Never log api_key values.
124161
Never log transcripts unless debug=True."""
162+
163+
def parse_custom_headers(custom_headers: str) -> dict[str, str]: ...
164+
"""Parse provider custom headers as a JSON object of string-ish values."""
165+
166+
def validate_config(cfg: AppConfig) -> list[ConfigValidationIssue]: ...
167+
"""Return all startup/settings validation issues for the current config."""
125168
```
126169

127170
### audio.py
@@ -206,22 +249,53 @@ def get_icon_bytes(state: TrayState) -> bytes: ...
206249
### settings_dialog.py
207250

208251
```python
252+
class PasswordField(QLineEdit):
253+
"""Password line edit that reveals text only while focused."""
254+
209255
class SettingsDialog(QDialog):
210-
def __init__(self, config: AppConfig, parent: QWidget | None = None): ...
256+
def __init__(
257+
self,
258+
config: AppConfig,
259+
parent: QWidget | None = None,
260+
devices: list[tuple[int, str]] | None = None,
261+
calibrate_fn: Callable[[int | None], float] | None = None,
262+
): ...
211263
"""4-tab dialog (General, STT, LLM, Audio) prefilled from config.
212-
Edits a copy; original untouched until accept."""
264+
Edits a copy; original untouched until accept.
265+
*devices*: list of (device_id, display_name) for the Audio tab.
266+
*calibrate_fn*: fn(device_id) -> float for RMS calibration."""
213267
def get_config(self) -> AppConfig: ...
214268
"""Return edited config. Call after exec() returns Accepted."""
215269

216270
# if __name__ == "__main__": launches standalone for testing
217271
```
218272

273+
### startup.py
274+
275+
```python
276+
def is_supported() -> bool: ...
277+
"""True on Windows."""
278+
279+
def startup_command() -> str: ...
280+
"""Return the command stored in HKCU Run."""
281+
282+
def set_enabled(enabled: bool) -> None: ...
283+
"""Add or remove HKCU Run key. Raises ScreamerError on failure."""
284+
285+
def is_enabled() -> bool: ...
286+
"""Check if startup registration is currently active."""
287+
288+
def sync_enabled(enabled: bool) -> None: ...
289+
"""Idempotent: only writes registry if state differs from desired."""
290+
```
291+
219292
### main.py
220293

221294
No public exports. Entry point only:
222295

223296
```python
224297
# if __name__ == "__main__": main()
298+
# Accepts --startup flag for silent tray launch (no auto-open settings)
225299
```
226300

227301
---
@@ -231,11 +305,11 @@ No public exports. Entry point only:
231305
| Rule | Detail |
232306
|------|--------|
233307
| Composition root | `main.py` imports all other modules. Nothing imports `main.py`. |
234-
| Settings dialog | `settings_dialog.py` imports only `config.py` (and `utils.py` for constants). |
235-
| Shared utilities | `audio.py`, `hotkey.py`, `stt.py`, `rewrite.py`, `injector.py` may import `utils.py`. |
236-
| Zero peer imports | The five backend modules must NOT import each other. |
308+
| Settings dialog | `settings_dialog.py` imports only `config.py` and `startup.py` (and `utils.py` for constants). |
309+
| Shared utilities | `audio.py`, `hotkey.py`, `stt.py`, `rewrite.py`, `injector.py`, `startup.py` may import `utils.py`. |
310+
| Zero peer imports | The six backend modules must NOT import each other. |
237311
| Config consumer | `stt.py` and `rewrite.py` receive `AppConfig` as a parameter — they do not import `config.py`. `audio.py` receives device ID, device name, and RMS threshold from `main.py`. `main.py` passes config values to all backends. |
238-
| Qt in backend | Only `utils.py`, `icons.py`, `settings_dialog.py`, `main.py` import PySide6. Backend modules (`audio`, `hotkey`, `stt`, `rewrite`, `injector`) do not. |
312+
| Qt in backend | Only `utils.py`, `icons.py`, `settings_dialog.py`, `main.py` import PySide6. Backend modules (`audio`, `hotkey`, `stt`, `rewrite`, `injector`, `startup`) do not. |
239313
| No circular imports | The graph is a DAG rooted at `main.py`. Structural guarantee. |
240314

241315
---
@@ -287,6 +361,7 @@ No hardcoded provider defaults. No silent fallback to unconfigured endpoints.
287361
| 8 | `src/stt.py` | `python -m src.stt test.wav` prints transcription (needs API config) |
288362
| 9 | `src/rewrite.py` | `python -m src.rewrite "test sentense wit erors"` prints corrected text (needs API config) |
289363
| 10 | `src/injector.py` | `python -m src.injector "hello world"` types into active window (Windows), message otherwise |
364+
| 11 | `src/startup.py` | `python -m src.startup` checks/sets Windows startup registry key |
290365

291366
**Verification commands:**
292367
```bash
@@ -307,7 +382,7 @@ python -c "import src; print('OK')"
307382
- [ ] No `api_key` values appear in log output.
308383
- [ ] Transcript text appears in logs only when `debug=True`.
309384
- [ ] Public exports match the API Contracts section above.
310-
- [ ] `audio`, `hotkey`, `stt`, `rewrite`, `injector` do not import each other.
385+
- [ ] `audio`, `hotkey`, `stt`, `rewrite`, `injector`, `startup` do not import each other.
311386

312387
---
313388

@@ -325,6 +400,7 @@ The reviewer should inspect:
325400
| Logging | Secrets excluded from logs. Transcripts only logged with `debug=True`. |
326401
| Platform guards | Windows-only modules raise clean errors on Linux/macOS at runtime, not import time. |
327402
| Phase 2 readiness | Can `main.py` + `settings_dialog.py` be built **without modifying any Phase 1 file**? If not, fix Phase 1 now. |
403+
| Windows-only guard | `startup.py` raises `ScreamerError(UNSUPPORTED_PLATFORM)` on non-Windows at runtime, not import time. |
328404

329405
Do not proceed to Phase 2 until review passes.
330406

@@ -366,7 +442,8 @@ Do not proceed to Phase 2 until review passes.
366442

367443
## Boundaries
368444

369-
- No modules beyond the 10 listed. No new dependencies.
370-
- Do not implement packaging (PyInstaller), autostart, code signing, or cross-platform hotkey backends.
445+
- No modules beyond the 11 listed. No new dependencies.
446+
- Do not implement packaging (PyInstaller), code signing, or cross-platform hotkey backends.
447+
- Autostart registration is implemented in `startup.py`.
371448
- All paths: `%LOCALAPPDATA%/Screamer/`. API keys: DPAPI. Plain settings: QSettings (IniFormat).
372449
- If a Phase 2 bug forces a Phase 1 API change, document it in the review checkpoint and get re-approval.

docs/OVERVIEW.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# OVI Fork Overview
22

3+
> **Status:** This document describes the original OVI project and recommended fork direction.
4+
> All recommendations have been implemented in Screamer. See `PLAN.md` and `IMPLEMENTATION.md`.
5+
36
This repo is a small but useful prototype for a Groq-based push-to-talk dictation app. It already has the core speech loop working, but it is not yet a full WisprFlow alternative.
47

58
## What the app does

0 commit comments

Comments
 (0)