2222├── rewrite.py
2323├── injector.py
2424├── icons.py
25- └── utils.py
25+ ├── utils.py
26+ └── startup.py
2627requirements.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
5053class 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
77108class 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+
110147def load_config () -> AppConfig: ...
111148 """ Load QSettings + DPAPI. Unknown keys get field defaults."""
112149
@@ -122,6 +159,12 @@ def import_from_env(cfg: AppConfig) -> AppConfig: ...
122159def 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+
209255class 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
221294No 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
329405Do 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.
0 commit comments