Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
f8088cf
auto-claude: subtask-1-1 - Create resource monitoring service to trac…
youngmrz Jan 15, 2026
5aea692
auto-claude: subtask-1-2 - Create measurement script to establish bas…
youngmrz Jan 15, 2026
7a06469
auto-claude: subtask-1-3 - Document baseline measurements in profilin…
youngmrz Jan 15, 2026
140fa81
auto-claude: subtask-1-3 - Document baseline measurements in profilin…
youngmrz Jan 15, 2026
eebc350
auto-claude: subtask-2-1 - Add lazy loading mode to TranscriptionService
youngmrz Jan 15, 2026
6f35520
auto-claude: subtask-2-2 - Add model idle timeout and auto-unload mec…
youngmrz Jan 15, 2026
9ceea12
auto-claude: subtask-2-3 - Update transcription flow to use lazy loading
youngmrz Jan 15, 2026
6756ab8
auto-claude: subtask-3-1 - Remove eager model loading from AppControl…
youngmrz Jan 15, 2026
2fda9b2
auto-claude: subtask-3-1 - Remove eager model loading from AppControl…
youngmrz Jan 15, 2026
b927ab0
auto-claude: subtask-3-2 - Add loading indicator for first-use delay
youngmrz Jan 15, 2026
ddb7ada
auto-claude: subtask-3-3 - Update settings to include model idle time…
youngmrz Jan 15, 2026
2d64685
auto-claude: subtask-4-1 - Run idle resource measurement on optimized…
youngmrz Jan 15, 2026
05efae7
auto-claude: subtask-4-2 - Test first-use transcription latency
youngmrz Jan 15, 2026
6231c99
auto-claude: subtask-4-3 - Document optimization results in profiling…
youngmrz Jan 15, 2026
d9c37b8
auto-claude: subtask-5-1 - Remove deprecated eager loading code paths
youngmrz Jan 15, 2026
5e87728
auto-claude: subtask-5-2 - Add resource monitoring to settings dashboard
youngmrz Jan 15, 2026
810abcf
auto-claude: subtask-5-3 - Update documentation with resource optimiz…
youngmrz Jan 15, 2026
fee78ed
fix: model idle timeout RPC integration (qa-requested)
youngmrz Jan 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
217 changes: 217 additions & 0 deletions .auto-claude-security.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
{
"base_commands": [
".",
"[",
"[[",
"ag",
"awk",
"basename",
"bash",
"bc",
"break",
"cat",
"cd",
"chmod",
"clear",
"cmp",
"column",
"comm",
"command",
"continue",
"cp",
"curl",
"cut",
"date",
"df",
"diff",
"dig",
"dirname",
"du",
"echo",
"egrep",
"env",
"eval",
"exec",
"exit",
"expand",
"export",
"expr",
"false",
"fd",
"fgrep",
"file",
"find",
"fmt",
"fold",
"gawk",
"gh",
"git",
"grep",
"gunzip",
"gzip",
"head",
"help",
"host",
"iconv",
"id",
"jobs",
"join",
"jq",
"kill",
"killall",
"less",
"let",
"ln",
"ls",
"lsof",
"man",
"mkdir",
"mktemp",
"more",
"mv",
"nl",
"paste",
"pgrep",
"ping",
"pkill",
"popd",
"printenv",
"printf",
"ps",
"pushd",
"pwd",
"read",
"readlink",
"realpath",
"reset",
"return",
"rev",
"rg",
"rm",
"rmdir",
"sed",
"seq",
"set",
"sh",
"shuf",
"sleep",
"sort",
"source",
"split",
"stat",
"tail",
"tar",
"tee",
"test",
"time",
"timeout",
"touch",
"tr",
"tree",
"true",
"type",
"uname",
"unexpand",
"uniq",
"unset",
"unzip",
"watch",
"wc",
"wget",
"whereis",
"which",
"whoami",
"xargs",
"yes",
"yq",
"zip",
"zsh"
],
"stack_commands": [
"ar",
"clang",
"clang++",
"cmake",
"composer",
"eslint",
"g++",
"gcc",
"ipython",
"jupyter",
"ld",
"make",
"meson",
"ninja",
"nm",
"node",
"notebook",
"npm",
"npx",
"objdump",
"pdb",
"php",
"pip",
"pip3",
"pipx",
"pudb",
"python",
"python3",
"react-scripts",
"strip",
"ts-node",
"tsc",
"tsx",
"vite"
],
"script_commands": [
"bun",
"npm",
"pnpm",
"yarn"
],
"custom_commands": [],
"detected_stack": {
"languages": [
"python",
"javascript",
"typescript",
"php",
"c",
"cpp"
],
"package_managers": [
"npm",
"pip"
],
"frameworks": [
"react",
"vite",
"eslint"
],
"databases": [],
"infrastructure": [],
"cloud_providers": [],
"code_quality_tools": [],
"version_managers": []
},
"custom_scripts": {
"npm_scripts": [
"dev",
"dev:watch",
"vite",
"pyloid",
"pyloid:watch",
"build",
"build:installer",
"setup"
],
"make_targets": [],
"poetry_scripts": [],
"cargo_aliases": [],
"shell_scripts": []
},
"project_dir": "D:\\dev\\personal\\VoiceFlow-fresh",
"created_at": "2026-01-14T18:09:48.602484",
"project_hash": "f43790d42262b3ae0f34be772dfa0899",
"inherited_from": "D:\\dev\\personal\\VoiceFlow-fresh"
}
25 changes: 25 additions & 0 deletions .auto-claude-status
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"active": true,
"spec": "001-minimal-idle-resource-usage",
"state": "building",
"subtasks": {
"completed": 14,
"total": 15,
"in_progress": 1,
"failed": 0
},
"phase": {
"current": "Cleanup - Polish and Documentation",
"id": null,
"total": 3
},
"workers": {
"active": 0,
"max": 1
},
"session": {
"number": 15,
"started_at": "2026-01-14T22:45:59.101594"
},
"last_update": "2026-01-14T23:35:21.619012"
}
39 changes: 39 additions & 0 deletions .claude_settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"sandbox": {
"enabled": true,
"autoAllowBashIfSandboxed": true
},
"permissions": {
"defaultMode": "acceptEdits",
"allow": [
"Read(./**)",
"Write(./**)",
"Edit(./**)",
"Glob(./**)",
"Grep(./**)",
"Read(D:\\dev\\personal\\VoiceFlow-fresh\\.auto-claude\\worktrees\\tasks\\001-minimal-idle-resource-usage/**)",
"Write(D:\\dev\\personal\\VoiceFlow-fresh\\.auto-claude\\worktrees\\tasks\\001-minimal-idle-resource-usage/**)",
"Edit(D:\\dev\\personal\\VoiceFlow-fresh\\.auto-claude\\worktrees\\tasks\\001-minimal-idle-resource-usage/**)",
"Glob(D:\\dev\\personal\\VoiceFlow-fresh\\.auto-claude\\worktrees\\tasks\\001-minimal-idle-resource-usage/**)",
"Grep(D:\\dev\\personal\\VoiceFlow-fresh\\.auto-claude\\worktrees\\tasks\\001-minimal-idle-resource-usage/**)",
"Read(D:\\dev\\personal\\VoiceFlow-fresh\\.auto-claude\\worktrees\\tasks\\001-minimal-idle-resource-usage\\.auto-claude\\specs\\001-minimal-idle-resource-usage/**)",
"Write(D:\\dev\\personal\\VoiceFlow-fresh\\.auto-claude\\worktrees\\tasks\\001-minimal-idle-resource-usage\\.auto-claude\\specs\\001-minimal-idle-resource-usage/**)",
"Edit(D:\\dev\\personal\\VoiceFlow-fresh\\.auto-claude\\worktrees\\tasks\\001-minimal-idle-resource-usage\\.auto-claude\\specs\\001-minimal-idle-resource-usage/**)",
"Read(D:\\dev\\personal\\VoiceFlow-fresh\\.auto-claude/**)",
"Write(D:\\dev\\personal\\VoiceFlow-fresh\\.auto-claude/**)",
"Edit(D:\\dev\\personal\\VoiceFlow-fresh\\.auto-claude/**)",
"Glob(D:\\dev\\personal\\VoiceFlow-fresh\\.auto-claude/**)",
"Grep(D:\\dev\\personal\\VoiceFlow-fresh\\.auto-claude/**)",
"Bash(*)",
"WebFetch(*)",
"WebSearch(*)",
"mcp__context7__resolve-library-id(*)",
"mcp__context7__get-library-docs(*)",
"mcp__graphiti-memory__search_nodes(*)",
"mcp__graphiti-memory__search_facts(*)",
"mcp__graphiti-memory__add_episode(*)",
"mcp__graphiti-memory__get_episodes(*)",
"mcp__graphiti-memory__get_entity_edge(*)"
]
}
}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,6 @@ docs/plans/
*.spec
build_error_log.txt


# Auto Claude data directory
.auto-claude/
54 changes: 48 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,14 @@ Python backend using Pyloid framework with PySide6:

**Services (src-pyloid/services/):**
- `audio.py` - Microphone recording using sounddevice, streams amplitude for visualizer
- `transcription.py` - faster-whisper model loading and transcription
- `transcription.py` - faster-whisper model loading and transcription with lazy loading support
- `hotkey.py` - Global hotkey listener using keyboard library
- `clipboard.py` - Clipboard operations and paste-at-cursor using pyautogui
- `settings.py` - Settings management with defaults
- `settings.py` - Settings management with defaults, includes `model_idle_timeout` configuration
- `database.py` - SQLite database for settings and history (stored at ~/.VoiceFlow/VoiceFlow.db)
- `logger.py` - Domain-based logging with hybrid format `[timestamp] [LEVEL] [domain] message | {json}`. Supports domains: model, audio, hotkey, settings, database, clipboard, window. Configured with 100MB log rotation.
- `model_manager.py` - Whisper model download/cache management using huggingface_hub. Provides download progress tracking (percent, speed, ETA), cancellation via CancelToken, daemon thread execution, and `clear_cache()` to delete only VoiceFlow's faster-whisper models.
- `resource_monitor.py` - CPU and memory usage tracking using psutil. Provides `get_cpu_percent()`, `get_memory_mb()`, and `get_snapshot()` for resource profiling.

### Frontend (src/)

Expand All @@ -66,6 +67,7 @@ React 18 + TypeScript + Vite frontend:
- `ModelDownloadProgress.tsx` - Download progress UI with progress bar, speed, ETA, and retry support
- `ModelDownloadModal.tsx` - Dialog wrapper for model downloads triggered from settings
- `ModelRecoveryModal.tsx` - Startup modal for missing model recovery
- `ResourceMonitor.tsx` - Live CPU and memory usage display in Settings tab (polls every 2s)

### Frontend-Backend Communication

Expand All @@ -87,10 +89,12 @@ popup_window.invoke('popup-state', {'state': 'recording'})
3. Popup transitions to "recording" state, shows amplitude visualizer
4. User releases hotkey
5. `AudioService.stop_recording` returns audio numpy array
6. `TranscriptionService.transcribe` runs faster-whisper
7. `ClipboardService.paste_at_cursor` pastes text
8. History saved to database
9. Popup returns to "idle" state
6. If model not loaded (first use), popup shows "loading" state while `ensure_model_loaded()` loads model
7. `TranscriptionService.transcribe` runs faster-whisper
8. `ClipboardService.paste_at_cursor` pastes text
9. History saved to database
10. `start_idle_timer(300)` begins countdown to auto-unload model
11. Popup returns to "idle" state

### Qt Threading Pattern

Expand Down Expand Up @@ -119,12 +123,50 @@ For transparent popup windows on Windows:
6. On completion, model is cached in huggingface cache directory
7. Turbo model uses `mobiuslabsgmbh/faster-whisper-large-v3-turbo` (same as faster-whisper internal mapping)

### Resource Optimization and Lazy Loading

VoiceFlow uses lazy loading to minimize idle resource usage (<20 MB memory, <1% CPU when idle):

**Lazy Model Loading:**
- Model is NOT loaded on application startup
- `TranscriptionService._model` is `None` initially
- `ensure_model_loaded()` loads model on-demand before first transcription
- Loading triggers "loading" popup state with blue indicator
- First-use latency: 2-5 seconds for tiny model (acceptable trade-off for 71-99% memory savings)

**Auto-Unload Mechanism:**
- `start_idle_timer(timeout_seconds)` starts countdown after each transcription
- Default timeout: 300 seconds (5 minutes), configurable via `model_idle_timeout` setting
- Timer runs in daemon thread using `threading.Timer` pattern
- `_on_idle_timeout()` calls `unload_model()` to free memory
- Timer is cancelled if model is used again before timeout expires

**Settings Integration:**
- `model_idle_timeout` field in Settings (30-1800 seconds range)
- Persisted in database, configurable via Settings UI slider
- Frontend shows live resource monitor (CPU%, memory MB) polling every 2 seconds
- `ResourceMonitor` component displays current usage in Advanced settings section

**Implementation Details:**
- `TranscriptionService.is_model_loaded()` checks if model is in memory
- `AppController._handle_hotkey_deactivate()` orchestrates: ensure model loaded -> transcribe -> start idle timer
- `AppController.stop_test_recording()` also uses lazy loading for onboarding flow
- When settings change (model/device), old eager reload removed - model loads lazily on next use
- Shutdown calls `unload_model()` to clean up resources

**Resource Monitoring:**
- `resource_monitor.py` service uses psutil for CPU and memory tracking
- `get_cpu_percent()` and `get_memory_mb()` provide current metrics
- `scripts/measure_idle_resources.py` for profiling and baseline measurements
- See `docs/profiling/` for performance analysis and optimization results

## Key Patterns

- **Singleton controller**: `get_controller()` returns singleton `AppController` instance
- **UI callbacks**: Backend notifies frontend of state changes via callbacks set in `set_ui_callbacks()`
- **Thread-safe signals**: Qt signals with `QueuedConnection` marshal UI updates from background threads to main thread
- **Background threads**: Model loading, downloads, and transcription run in daemon threads
- **Lazy loading**: Models load on-demand via `ensure_model_loaded()`, not at startup. Auto-unload after configurable idle timeout (default 5 min).
- **Domain logging**: All services use `get_logger(domain)` for structured logging with domains like `model`, `audio`, `hotkey`, etc.
- **Custom hotkeys**: Supports modifier-only combos (e.g., Ctrl+Win) and standard combos (e.g., Ctrl+R). Frontend captures keys, backend validates and registers.
- **Path alias**: Frontend uses `@/` for `src/` imports (configured in tsconfig.json and vite.config.ts)
Expand Down
Loading