diff --git a/.gitignore b/.gitignore index a653d5a..af45347 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,12 @@ docs/plans/ *.spec build_error_log.txt + +# Auto Claude generated files +.auto-claude/ +.auto-claude-security.json +.auto-claude-status +.claude_settings.json +.worktrees/ +.security-key +logs/security/ diff --git a/package.json b/package.json index 2146bba..9620581 100644 --- a/package.json +++ b/package.json @@ -7,9 +7,9 @@ "dev": "npm-run-all --parallel vite pyloid", "dev:watch": "npm-run-all --parallel vite pyloid:watch", "vite": "vite", - "pyloid": "uv run -p .venv ./src-pyloid/main.py", + "pyloid": "uv run ./src-pyloid/main.py", "pyloid:watch": "uv run pyloid-watcher --path ./src-pyloid --pattern \"*.py\" --command \"uv run ./src-pyloid/main.py\"", - "build": "tsc -b && vite build && uv run -p .venv ./src-pyloid/build/build.py", + "build": "tsc -b && vite build && uv run ./src-pyloid/build/build.py", "build:installer": "pnpm run build && cd installer && build-installer.bat", "setup": "pnpm install && uv venv && uv sync" }, diff --git a/src-pyloid/app_controller.py b/src-pyloid/app_controller.py index d4624a2..c0f6272 100644 --- a/src-pyloid/app_controller.py +++ b/src-pyloid/app_controller.py @@ -178,22 +178,25 @@ def transcribe(): info("Pasting text at cursor...") self.clipboard_service.paste_at_cursor(text) - # Save to history (and audio if enabled) - history_id = self.db.add_history(text) - - if settings.save_audio_to_history: - try: - audio_meta = self._save_audio_attachment(history_id, audio) - self.db.update_history_audio( - history_id, - audio_relpath=audio_meta["audio_relpath"], - audio_duration_ms=audio_meta["audio_duration_ms"], - audio_size_bytes=audio_meta["audio_size_bytes"], - audio_mime=audio_meta["audio_mime"], - ) - info(f"Saved audio attachment for history {history_id}") - except (OSError, wave.Error, sqlite3.Error, ValueError) as exc: - warning(f"Failed to save audio attachment: {exc}") + # Save to history (and audio if enabled) - only if not disabled + if not settings.disable_history_storage: + history_id = self.db.add_history(text) + + if settings.save_audio_to_history: + try: + audio_meta = self._save_audio_attachment(history_id, audio) + self.db.update_history_audio( + history_id, + audio_relpath=audio_meta["audio_relpath"], + audio_duration_ms=audio_meta["audio_duration_ms"], + audio_size_bytes=audio_meta["audio_size_bytes"], + audio_mime=audio_meta["audio_mime"], + ) + info(f"Saved audio attachment for history {history_id}") + except (OSError, wave.Error, sqlite3.Error, ValueError) as exc: + warning(f"Failed to save audio attachment: {exc}") + else: + info("History storage disabled - transcription not saved") if self._on_transcription_complete: self._on_transcription_complete(text) @@ -230,6 +233,7 @@ def get_settings(self) -> dict: "onboardingComplete": settings.onboarding_complete, "microphone": settings.microphone, "saveAudioToHistory": settings.save_audio_to_history, + "disableHistoryStorage": settings.disable_history_storage, "holdHotkey": settings.hold_hotkey, "holdHotkeyEnabled": settings.hold_hotkey_enabled, "toggleHotkey": settings.toggle_hotkey, @@ -246,6 +250,8 @@ def update_settings(self, **kwargs) -> dict: mapped["onboarding_complete"] = kwargs["onboardingComplete"] if "saveAudioToHistory" in kwargs: mapped["save_audio_to_history"] = kwargs["saveAudioToHistory"] + if "disableHistoryStorage" in kwargs: + mapped["disable_history_storage"] = kwargs["disableHistoryStorage"] # Hotkey settings (camelCase to snake_case) if "holdHotkey" in kwargs: mapped["hold_hotkey"] = kwargs["holdHotkey"] diff --git a/src-pyloid/server.py b/src-pyloid/server.py index 7fcc7bd..e00ea18 100644 --- a/src-pyloid/server.py +++ b/src-pyloid/server.py @@ -60,6 +60,7 @@ async def update_settings( holdHotkeyEnabled: Optional[bool] = None, toggleHotkey: Optional[str] = None, toggleHotkeyEnabled: Optional[bool] = None, + disableHistoryStorage: Optional[bool] = None, ): controller = get_controller() kwargs = {} @@ -90,6 +91,8 @@ async def update_settings( kwargs["toggleHotkey"] = toggleHotkey if toggleHotkeyEnabled is not None: kwargs["toggleHotkeyEnabled"] = toggleHotkeyEnabled + if disableHistoryStorage is not None: + kwargs["disableHistoryStorage"] = disableHistoryStorage # Check if onboarding was already complete before this update old_settings = controller.get_settings() diff --git a/src-pyloid/services/settings.py b/src-pyloid/services/settings.py index ac61e3a..6213592 100644 --- a/src-pyloid/services/settings.py +++ b/src-pyloid/services/settings.py @@ -48,6 +48,7 @@ class Settings: onboarding_complete: bool = False microphone: int = -1 # -1 = default device, otherwise device id save_audio_to_history: bool = False + disable_history_storage: bool = False # Hotkey settings hold_hotkey: str = "ctrl+win" hold_hotkey_enabled: bool = True @@ -74,6 +75,7 @@ def get_settings(self) -> Settings: onboarding_complete=self.db.get_setting("onboarding_complete", "false") == "true", microphone=int(self.db.get_setting("microphone", "-1")), save_audio_to_history=self.db.get_setting("save_audio_to_history", "false") == "true", + disable_history_storage=self.db.get_setting("disable_history_storage", "false") == "true", # Hotkey settings hold_hotkey=self.db.get_setting("hold_hotkey", "ctrl+win"), hold_hotkey_enabled=self.db.get_setting("hold_hotkey_enabled", "true") == "true", @@ -95,6 +97,7 @@ def update_settings( onboarding_complete: Optional[bool] = None, microphone: Optional[int] = None, save_audio_to_history: Optional[bool] = None, + disable_history_storage: Optional[bool] = None, hold_hotkey: Optional[str] = None, hold_hotkey_enabled: Optional[bool] = None, toggle_hotkey: Optional[str] = None, @@ -118,6 +121,8 @@ def update_settings( self.db.set_setting("microphone", str(microphone)) if save_audio_to_history is not None: self.db.set_setting("save_audio_to_history", "true" if save_audio_to_history else "false") + if disable_history_storage is not None: + self.db.set_setting("disable_history_storage", "true" if disable_history_storage else "false") # Hotkey settings - normalize before storing for consistent format if hold_hotkey is not None: self.db.set_setting("hold_hotkey", normalize_hotkey(hold_hotkey)) diff --git a/src/components/SettingsTab.tsx b/src/components/SettingsTab.tsx index e9102f5..892eedf 100644 --- a/src/components/SettingsTab.tsx +++ b/src/components/SettingsTab.tsx @@ -24,6 +24,7 @@ import { Hand, ToggleRight, HardDrive, + Shield, } from "lucide-react"; import { api } from "@/lib/api"; import type { Settings, Options, GpuInfo } from "@/lib/types"; @@ -422,7 +423,41 @@ export function SettingsTab() {

- {/* 7. System (Auto Start) (Span 4) */} + {/* 7. Privacy (Span 6) */} + +
+
+

+ Voice data stays in RAM only. No network transmission. No disk storage unless history is enabled. All processing happens locally on your device. +

+
+
+ + + updateSetting("disableHistoryStorage", checked) + } + /> +
+

+ When enabled, transcriptions are pasted but never saved to history for maximum privacy. +

+
+
+ + {/* 8. System (Auto Start) (Span 4) */} - {/* 8. Data Folder (Span 4) */} + {/* 9. Data Folder (Span 4) */} - {/* 8. Keyboard Shortcuts (Full Width) */} + {/* 10. Keyboard Shortcuts (Full Width) */} - {/* 9. GPU / Device (Span 6) */} + {/* 11. GPU / Device (Span 6) */} - {/* 10. Danger Zone (Span 4) */} + {/* 12. Danger Zone (Span 4) */} diff --git a/src/lib/types.ts b/src/lib/types.ts index 258da83..7f397d2 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -8,6 +8,7 @@ export interface Settings { onboardingComplete: boolean; microphone: number; saveAudioToHistory: boolean; + disableHistoryStorage: boolean; // Hotkey settings holdHotkey: string; holdHotkeyEnabled: boolean; diff --git a/src/pages/Onboarding.tsx b/src/pages/Onboarding.tsx index 4df0527..b17eda6 100644 --- a/src/pages/Onboarding.tsx +++ b/src/pages/Onboarding.tsx @@ -48,6 +48,20 @@ const StepWelcome = () => ( flow.

+
+
+
+ +
+
+

Your Privacy is Guaranteed

+

+ Voice data stays in RAM only. No network transmission. No disk storage unless history is enabled. All processing happens locally on your device. +

+
+
+
+
{ONBOARDING_FEATURES.map((feature) => (
- {/* IDLE: Tiny pill */} + {/* IDLE: Offline badge */} {state === "idle" && (
+ > +
+ + OFFLINE + +
)} {/* RECORDING: Simple bars */}