31 Commits

Author SHA1 Message Date
lashman e86fab07d1 Fix typography and wording in README, build spec, main 2026-03-13 17:31:37 +02:00
lashman 8ee67e9ef3 Add WCAG 2.2 AAA accessibility section to README 2026-02-18 22:30:48 +02:00
lashman 5092e3816d Remove unused files 2026-02-18 22:25:04 +02:00
lashman ea7821752e Arrow keys, PageTab roles, AlertMessage fixes 2026-02-18 21:10:21 +02:00
lashman 477dfb8120 Loader - subtitle contrast, reduced motion 2026-02-18 21:06:43 +02:00
lashman 785b2c68f2 Overlay - border, keyboard, reduced motion 2026-02-18 21:06:43 +02:00
lashman 1ea0e875d5 Settings - AAA colors, keyboard nav, reduce motion 2026-02-18 21:05:35 +02:00
lashman fae56e7b78 Slider and Switch accessibility fixes 2026-02-18 21:03:21 +02:00
lashman 727c2f2704 Add reduce_motion config and OS detection 2026-02-18 21:02:27 +02:00
lashman 7135459a18 TextField, ComboBox, KeyRecorder - contrast and focus fixes 2026-02-18 21:02:07 +02:00
lashman 8d973138b7 GlowButton, SettingsSection, SettingsItem - contrast fixes 2026-02-18 21:02:06 +02:00
lashman 23147028d3 Update design tokens for AAA contrast 2026-02-18 21:00:49 +02:00
lashman 32d4e328ff Add local text correction engine 2026-01-31 01:02:24 +02:00
lashman 6a98142c1d v1.0.4 - compatibility update 2026-01-25 20:28:01 +02:00
lashman 1318c72210 Document low VRAM mode and style prompting 2026-01-25 13:52:10 +02:00
lashman 4f077df03d v1.0.2 - style prompting, drop grammar correction 2026-01-25 13:42:06 +02:00
lashman d73656a75c Polish enshittification manifesto section 2026-01-24 19:21:01 +02:00
lashman f3747c8f92 Polish language table and manifesto 2026-01-24 19:12:08 +02:00
lashman 305856f7a5 Fix translation reliability and click-through 2026-01-24 19:05:43 +02:00
lashman 252df6dd85 Expand README with translation features 2026-01-24 18:33:22 +02:00
lashman 19831656dc v1.0.1 updates and polish 2026-01-24 18:29:10 +02:00
lashman de64610a90 Fix invisible overlay blocking mouse clicks 2026-01-24 17:51:23 +02:00
lashman 5087eec958 Aesthetic overhaul of documentation 2026-01-24 17:29:59 +02:00
lashman 90f7f49233 Add language list and file transcription info 2026-01-24 17:27:54 +02:00
lashman 75957317c4 Correct hardware detection statement in docs 2026-01-24 17:24:56 +02:00
lashman a51107359b Fix release badge for Gitea 2026-01-24 17:22:14 +02:00
lashman 93b11cbd4a Final documentation polish 2026-01-24 17:20:22 +02:00
lashman 5469b32e59 Update docs with license and model stats 2026-01-24 17:16:53 +02:00
lashman 0552a1ca75 Enhance documentation 2026-01-24 17:12:21 +02:00
lashman 9f7fd5384e Update documentation with new manifesto 2026-01-24 17:06:31 +02:00
lashman a938c83a37 Initial commit of WhisperVoice 2026-01-24 17:03:52 +02:00
5 changed files with 169 additions and 135 deletions
+5 -5
View File
@@ -24,7 +24,7 @@
## 📡 The Transmission ## 📡 The Transmission
We are witnessing the **enshittification** of the digital world. What were once vibrant social commons are being walled off, strip-mined for data, and degraded into rent-seeking silos. Your voice is no longer your own; it is a training set for a corporate oracle that charges you for the privilege of listening. We are witnessing the **enshittification** of the digital world. What were once thriving social commons are being walled off, strip-mined for data, and degraded into rent-seeking silos. Your voice is no longer your own; it is a training set for a corporate oracle that charges you for the privilege of listening.
**Whisper Voice** is a small act of sabotage against this trend. **Whisper Voice** is a small act of sabotage against this trend.
@@ -59,7 +59,7 @@ The core engine (`CTranslate2`) is heavily optimized for Nvidia tensor cores.
## 🖋️ Universal Transcription ## 🖋️ Universal Transcription
At its core, Whisper Voice is the ultimate bridge between thought and text. It listens with superhuman precision, converting spoken word into written form across **99 languages**. Whisper Voice is the bridge between thought and text. It listens with superhuman precision, converting spoken word into written form across **99 languages**.
* **Punctuation Mastery**: Automatically handles capitalization and complex punctuation formatting. * **Punctuation Mastery**: Automatically handles capitalization and complex punctuation formatting.
* **Contextual Intelligence**: Smarter than standard dictation; it understands the flow of sentences to resolve homophones and technical jargon ($1.5k vs "fifteen hundred dollars"). * **Contextual Intelligence**: Smarter than standard dictation; it understands the flow of sentences to resolve homophones and technical jargon ($1.5k vs "fifteen hundred dollars").
@@ -143,7 +143,7 @@ For users with limited GPU memory (e.g., 4GB cards) or those running heavy games
## ♿ Accessibility (WCAG 2.2 AAA) ## ♿ Accessibility (WCAG 2.2 AAA)
Whisper Voice is built to be usable by everyone. The entire interface has been engineered to meet **WCAG 2.2 AAA** the highest tier of accessibility compliance. This is not a checkbox exercise; it is a structural commitment. Whisper Voice is built to be usable by everyone. The entire interface has been engineered to meet **WCAG 2.2 AAA** - the highest tier of accessibility compliance. This is not a checkbox exercise; it is a structural commitment.
### Color & Contrast ### Color & Contrast
Every design token is calibrated for **Enhanced Contrast** (WCAG 1.4.6, 7:1 minimum): Every design token is calibrated for **Enhanced Contrast** (WCAG 1.4.6, 7:1 minimum):
@@ -156,7 +156,7 @@ Every design token is calibrated for **Enhanced Contrast** (WCAG 1.4.6, 7:1 mini
| `borderSubtle` | 3:1 | Non-text contrast for borders and separators | | `borderSubtle` | 3:1 | Non-text contrast for borders and separators |
### Keyboard Navigation ### Keyboard Navigation
Full keyboard access no mouse required: Full keyboard access - no mouse required:
* **Tab / Shift+Tab**: Navigate between all interactive controls (sliders, switches, buttons, dropdowns, text fields). * **Tab / Shift+Tab**: Navigate between all interactive controls (sliders, switches, buttons, dropdowns, text fields).
* **Arrow Keys**: Navigate the Settings sidebar tabs. * **Arrow Keys**: Navigate the Settings sidebar tabs.
@@ -166,7 +166,7 @@ Full keyboard access — no mouse required:
### Screen Reader Support ### Screen Reader Support
Every component is annotated with semantic roles and descriptive names: Every component is annotated with semantic roles and descriptive names:
* Buttons, sliders, checkboxes, combo boxes, text fields all declare their `Accessible.role` and `Accessible.name`. * Buttons, sliders, checkboxes, combo boxes, text fields - all declare their `Accessible.role` and `Accessible.name`.
* Switches report "on" / "off" state in their accessible name. * Switches report "on" / "off" state in their accessible name.
* The loader status uses `AlertMessage` for live-region announcements. * The loader status uses `AlertMessage` for live-region announcements.
* Settings tabs use `Tab` / `PageTab` roles matching WAI-ARIA patterns. * Settings tabs use `Tab` / `PageTab` roles matching WAI-ARIA patterns.
-28
View File
@@ -1,28 +0,0 @@
# Release v1.0.4
**"The Compatibility Update"**
This release focuses on maximum stability across different hardware configurations (AMD, Intel, Nvidia) and fixing startup crashes related to corrupted models or missing drivers.
## 🛠️ Critical Fixes
### 1. Robust CPU Fallback (AMD / Intel Support)
* **Problem**: Previously, if an AMD user tried to run the app, it would crash instantly because it tried to load Nvidia CUDA libraries by default.
* **Fix**: The app now **silently detects** if CUDA initialization fails (due to missing DLLs or incompatible hardware) and **automatically falls back to CPU mode**.
* **Result**: The app "just works" on any Windows machine, regardless of GPU.
### 2. Startup Crash Protection
* **Problem**: If `faster_whisper` was imported before checking for valid drivers, the app would crash on launch for some users.
* **Fix**: Implemented **Lazy Loading** for the AI engine. The app now starts the UI first, and only loads the heavy AI libraries inside a safety block that catches errors.
### 3. Corrupt Model Auto-Repair
* **Problem**: Interrupted downloads could leave a corrupted model folder, preventing the app from ever starting again.
* **Fix**: If the app detects a "vocabulary missing" or invalid config error, it will now **automatically delete the corrupt folder** and allow you to re-download it cleanly.
### 4. Windows DLL Injection
* **Fix**: Added explicit DLL path injection for `nvidia-cublas` and `nvidia-cudnn` to ensure Python 3.8+ can find the required CUDA libraries on Windows systems that don't have them in PATH.
## 📦 Installation
1. Download `WhisperVoice.exe` below.
2. Replace your existing `.exe`.
3. Run it.
+8 -7
View File
@@ -1,5 +1,5 @@
# -*- mode: python ; coding: utf-8 -*- # -*- mode: python ; coding: utf-8 -*-
# WhisperVoice Single-file portable bootstrapper # WhisperVoice - Single-file portable bootstrapper
# #
# This builds a TINY exe that contains only: # This builds a TINY exe that contains only:
# - The bootstrapper (downloads Python + deps on first run) # - The bootstrapper (downloads Python + deps on first run)
@@ -8,6 +8,7 @@
# NO heavy dependencies (torch, PySide6, etc.) are bundled. # NO heavy dependencies (torch, PySide6, etc.) are bundled.
import os import os
import sys
import glob import glob
block_cipher = None block_cipher = None
@@ -22,7 +23,7 @@ app_datas.append(('main.py', 'app_source'))
# requirements.txt # requirements.txt
app_datas.append(('requirements.txt', 'app_source')) app_datas.append(('requirements.txt', 'app_source'))
# src/**/*.py (core, ui, utils preserving directory structure) # src/**/*.py (core, ui, utils - preserving directory structure)
for py in glob.glob('src/**/*.py', recursive=True): for py in glob.glob('src/**/*.py', recursive=True):
dest = os.path.join('app_source', os.path.dirname(py)) dest = os.path.join('app_source', os.path.dirname(py))
app_datas.append((py, dest)) app_datas.append((py, dest))
@@ -42,7 +43,7 @@ for f in glob.glob(os.path.join(qml_dir, 'fonts', 'ttf', '*.ttf')):
if os.path.exists(os.path.join('assets', 'icon.ico')): if os.path.exists(os.path.join('assets', 'icon.ico')):
app_datas.append((os.path.join('assets', 'icon.ico'), os.path.join('app_source', 'assets'))) app_datas.append((os.path.join('assets', 'icon.ico'), os.path.join('app_source', 'assets')))
# ── Analysis only the bootstrapper, NO heavy imports ──────────────────── # ── Analysis - only the bootstrapper, NO heavy imports ────────────────────
a = Analysis( a = Analysis(
['bootstrapper.py'], ['bootstrapper.py'],
@@ -54,14 +55,14 @@ a = Analysis(
hooksconfig={}, hooksconfig={},
runtime_hooks=[], runtime_hooks=[],
excludes=[ excludes=[
# Exclude everything heavy the bootstrapper only uses stdlib # Exclude everything heavy - the bootstrapper only uses stdlib
'torch', 'numpy', 'scipy', 'PySide6', 'shiboken6', 'torch', 'numpy', 'scipy', 'PySide6', 'shiboken6',
'faster_whisper', 'ctranslate2', 'llama_cpp', 'faster_whisper', 'ctranslate2', 'llama_cpp',
'sounddevice', 'soundfile', 'keyboard', 'pyperclip', 'sounddevice', 'soundfile', 'keyboard', 'pyperclip',
'psutil', 'pynvml', 'pystray', 'PIL', 'Pillow', 'psutil', 'pynvml', 'pystray', 'PIL', 'Pillow',
'darkdetect', 'huggingface_hub', 'requests', 'darkdetect', 'huggingface_hub', 'requests',
'tqdm', 'onnxruntime', 'av', 'tqdm', 'onnxruntime', 'av',
'tkinter', 'matplotlib', 'notebook', 'IPython', 'matplotlib', 'notebook', 'IPython',
], ],
win_no_prefer_redirects=False, win_no_prefer_redirects=False,
win_private_assemblies=False, win_private_assemblies=False,
@@ -85,11 +86,11 @@ exe = EXE(
bootloader_ignore_signals=False, bootloader_ignore_signals=False,
strip=False, strip=False,
upx=True, upx=True,
console=False, # No console bootstrapper allocates one when needed console=False, # No console - bootstrapper allocates one when needed
disable_windowed_traceback=False, disable_windowed_traceback=False,
argv_emulation=False, argv_emulation=False,
target_arch=None, target_arch=None,
codesign_identity=None, codesign_identity=None,
entitlements_file=None, entitlements_file=None,
icon='assets/icon.ico', icon='assets/icon.ico' if sys.platform == 'win32' else None,
) )
+98 -37
View File
@@ -9,6 +9,14 @@ app_dir = os.path.dirname(os.path.abspath(__file__))
if app_dir not in sys.path: if app_dir not in sys.path:
sys.path.insert(0, app_dir) sys.path.insert(0, app_dir)
# -----------------------------------------------------------------------------
# LINUX: Force XWayland (X11) for reliable window positioning & overlay behavior.
# Our input stack (evdev, UInput, wl-copy) is compositor-agnostic so this is safe.
# Native Wayland lacks app-controlled window positioning which the overlay needs.
# -----------------------------------------------------------------------------
if sys.platform == 'linux' and os.environ.get('WAYLAND_DISPLAY'):
os.environ.setdefault('QT_QPA_PLATFORM', 'xcb')
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# WINDOWS DLL FIX (CRITICAL for Portable CUDA) # WINDOWS DLL FIX (CRITICAL for Portable CUDA)
# Python 3.8+ on Windows requires explicit DLL directory addition. # Python 3.8+ on Windows requires explicit DLL directory addition.
@@ -16,20 +24,32 @@ if app_dir not in sys.path:
if os.name == 'nt' and hasattr(os, 'add_dll_directory'): if os.name == 'nt' and hasattr(os, 'add_dll_directory'):
try: try:
from pathlib import Path from pathlib import Path
# Scan sys.path for site-packages _candidate_dirs = set()
# 1. From sys.path (scan ALL site-packages, not just the first)
for p in sys.path: for p in sys.path:
path_obj = Path(p) path_obj = Path(p)
if path_obj.name == 'site-packages' and path_obj.exists(): if path_obj.name == 'site-packages' and path_obj.exists():
nvidia_path = path_obj / "nvidia" _candidate_dirs.add(str(path_obj.resolve()))
# 2. Relative to the Python executable (critical for embedded Python)
_exe_dir = Path(sys.executable).parent
for _sp in [_exe_dir / "Lib" / "site-packages", _exe_dir / "lib" / "site-packages"]:
if _sp.exists():
_candidate_dirs.add(str(_sp.resolve()))
# 3. Scan all candidates for nvidia DLL directories
for _sp_str in _candidate_dirs:
nvidia_path = Path(_sp_str) / "nvidia"
if nvidia_path.exists(): if nvidia_path.exists():
for subdir in nvidia_path.iterdir(): for subdir in nvidia_path.iterdir():
# Add 'bin' folder from each nvidia stub (cublas, cudnn, etc.)
bin_path = subdir / "bin" bin_path = subdir / "bin"
if bin_path.exists(): if bin_path.exists():
os.add_dll_directory(str(bin_path)) os.add_dll_directory(str(bin_path))
# Also try adding site-packages itself just in case # Also add to PATH as fallback - some libraries
# os.add_dll_directory(str(path_obj)) # (e.g. CTranslate2) load DLLs lazily via LoadLibrary
break # which may not respect os.add_dll_directory()
os.environ['PATH'] = str(bin_path) + os.pathsep + os.environ.get('PATH', '')
except Exception: except Exception:
pass pass
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -49,7 +69,8 @@ from src.core.hotkey_manager import HotkeyManager
from src.core.config import ConfigManager from src.core.config import ConfigManager
from src.utils.injector import InputInjector from src.utils.injector import InputInjector
from src.core.paths import get_models_path, get_bundle_path from src.core.paths import get_models_path, get_bundle_path
from src.utils.window_hook import WindowHook if os.name == 'nt':
from src.utils.window_hook import WindowHook
from PySide6.QtGui import QSurfaceFormat from PySide6.QtGui import QSurfaceFormat
@@ -63,7 +84,8 @@ os.environ["QT_ENABLE_HIGHDPI_SCALING"] = "1"
os.environ["QT_AUTOSCREENSCALEFACTOR"] = "1" os.environ["QT_AUTOSCREENSCALEFACTOR"] = "1"
# Detect resolution without creating QApplication (Fixes crash) # Detect resolution without creating QApplication (Fixes crash)
try: if os.name == 'nt':
try:
import ctypes import ctypes
user32 = ctypes.windll.user32 user32 = ctypes.windll.user32
# Get physical screen width (unscaled) # Get physical screen width (unscaled)
@@ -77,11 +99,13 @@ try:
res_scale *= 0.6 # Make it significantly smaller at 4k as requested res_scale *= 0.6 # Make it significantly smaller at 4k as requested
os.environ["QT_SCALE_FACTOR"] = str(max(1.0, res_scale)) os.environ["QT_SCALE_FACTOR"] = str(max(1.0, res_scale))
except: except:
pass pass
# On Linux, Qt handles DPI automatically via QT_ENABLE_HIGHDPI_SCALING
# Detect Windows "Reduce Motion" preference # Detect "Reduce Motion" preference
try: if os.name == 'nt':
try:
import ctypes import ctypes
SPI_GETCLIENTAREAANIMATION = 0x1042 SPI_GETCLIENTAREAANIMATION = 0x1042
animation_enabled = ctypes.c_bool(True) animation_enabled = ctypes.c_bool(True)
@@ -92,7 +116,19 @@ try:
if not animation_enabled.value: if not animation_enabled.value:
ConfigManager().data["reduce_motion"] = True ConfigManager().data["reduce_motion"] = True
ConfigManager().save() ConfigManager().save()
except Exception: except Exception:
pass
elif sys.platform == 'linux':
try:
import subprocess as _sp
result = _sp.run(
['gsettings', 'get', 'org.gnome.desktop.interface', 'enable-animations'],
capture_output=True, text=True, timeout=2
)
if result.stdout.strip() == 'false':
ConfigManager().data["reduce_motion"] = True
ConfigManager().save()
except Exception:
pass pass
# Configure Logging # Configure Logging
@@ -140,15 +176,25 @@ class DownloadWorker(QThread):
def run(self): def run(self):
try: try:
import requests import requests
from tqdm import tqdm import shutil
model_path = get_models_path() model_path = get_models_path()
# Determine what to download
dest_dir = model_path / f"faster-whisper-{self.model_name}" dest_dir = model_path / f"faster-whisper-{self.model_name}"
repo_id = f"Systran/faster-whisper-{self.model_name}" repo_id = f"Systran/faster-whisper-{self.model_name}"
files = ["config.json", "model.bin", "tokenizer.json", "vocabulary.json"] required_files = ["config.json", "model.bin", "tokenizer.json", "vocabulary.json"]
base_url = f"https://huggingface.co/{repo_id}/resolve/main" base_url = f"https://huggingface.co/{repo_id}/resolve/main"
dest_dir.mkdir(parents=True, exist_ok=True) # Skip if already complete
if dest_dir.exists() and all((dest_dir / f).exists() for f in required_files):
logging.info(f"Model {self.model_name} already downloaded.")
self.finished.emit()
return
# Download to a temp dir first, move on success
tmp_dir = model_path / f".tmp-faster-whisper-{self.model_name}"
if tmp_dir.exists():
shutil.rmtree(tmp_dir)
tmp_dir.mkdir(parents=True, exist_ok=True)
logging.info(f"Downloading {self.model_name} to {dest_dir}...") logging.info(f"Downloading {self.model_name} to {dest_dir}...")
# 1. Calculate Total Size # 1. Calculate Total Size
@@ -156,7 +202,7 @@ class DownloadWorker(QThread):
file_sizes = {} file_sizes = {}
with requests.Session() as s: with requests.Session() as s:
for fname in files: for fname in required_files:
url = f"{base_url}/{fname}" url = f"{base_url}/{fname}"
head = s.head(url, allow_redirects=True) head = s.head(url, allow_redirects=True)
if head.status_code == 200: if head.status_code == 200:
@@ -164,25 +210,21 @@ class DownloadWorker(QThread):
file_sizes[fname] = size file_sizes[fname] = size
total_size += size total_size += size
else: else:
# Fallback for vocabulary.json vs vocabulary.txt logging.warning(f"HEAD failed for {fname}: HTTP {head.status_code}")
if fname == "vocabulary.json":
# Try .txt? Or just skip if not found? # Abort if any required file is unavailable
# Faster-whisper usually has vocabulary.json missing = [f for f in required_files if f not in file_sizes]
pass if missing:
shutil.rmtree(tmp_dir, ignore_errors=True)
raise RuntimeError(f"Required model files unavailable: {missing}")
# 2. Download loop # 2. Download loop
downloaded_bytes = 0 downloaded_bytes = 0
with requests.Session() as s: with requests.Session() as s:
for fname in files: for fname in required_files:
if fname not in file_sizes: continue
url = f"{base_url}/{fname}" url = f"{base_url}/{fname}"
dest_file = dest_dir / fname dest_file = tmp_dir / fname
# Resume check?
# Simpler to just overwrite for reliability unless we want complex resume logic.
# We'll overwrite.
resp = s.get(url, stream=True) resp = s.get(url, stream=True)
resp.raise_for_status() resp.raise_for_status()
@@ -192,12 +234,22 @@ class DownloadWorker(QThread):
if chunk: if chunk:
f.write(chunk) f.write(chunk)
downloaded_bytes += len(chunk) downloaded_bytes += len(chunk)
# Emit Progress
if total_size > 0: if total_size > 0:
pct = int((downloaded_bytes / total_size) * 100) pct = int((downloaded_bytes / total_size) * 100)
self.progress.emit(pct) self.progress.emit(pct)
# 3. Validate all files present and non-empty
for fname in required_files:
fpath = tmp_dir / fname
if not fpath.exists() or fpath.stat().st_size == 0:
shutil.rmtree(tmp_dir, ignore_errors=True)
raise RuntimeError(f"Download incomplete: {fname} missing or empty")
# 4. Atomic move: replace dest with completed download
if dest_dir.exists():
shutil.rmtree(dest_dir)
tmp_dir.rename(dest_dir)
self.finished.emit() self.finished.emit()
except Exception as e: except Exception as e:
@@ -311,6 +363,7 @@ class WhisperApp(QObject):
self.bridge.llmDownloadRequested.connect(self.on_llm_download_requested) self.bridge.llmDownloadRequested.connect(self.on_llm_download_requested)
self.engine.rootContext().setContextProperty("ui", self.bridge) self.engine.rootContext().setContextProperty("ui", self.bridge)
self.engine.rootContext().setContextProperty("isLinux", sys.platform == 'linux')
# 2. Tray setup # 2. Tray setup
self.tray = SystemTray() self.tray = SystemTray()
@@ -382,6 +435,7 @@ class WhisperApp(QObject):
self.settings_root.setVisible(False) self.settings_root.setVisible(False)
# Install Low-Level Window Hook for Transparent Hit Test # Install Low-Level Window Hook for Transparent Hit Test
if os.name == 'nt':
try: try:
from src.utils.window_hook import WindowHook from src.utils.window_hook import WindowHook
hwnd = self.overlay_root.winId() hwnd = self.overlay_root.winId()
@@ -399,6 +453,9 @@ class WhisperApp(QObject):
self.window_hook.set_enabled(False) self.window_hook.set_enabled(False)
except Exception as e: except Exception as e:
logging.error(f"Failed to install WindowHook: {e}") logging.error(f"Failed to install WindowHook: {e}")
else:
# On Linux, use Qt flag for click-through overlay
self.overlay_root.setFlag(Qt.WindowTransparentForInput, True)
def center_overlay(self): def center_overlay(self):
"""Calculates and sets the Overlay position above the taskbar.""" """Calculates and sets the Overlay position above the taskbar."""
@@ -812,19 +869,23 @@ class WhisperApp(QObject):
self.bridge.update_status("Error") self.bridge.update_status("Error")
logging.error(f"Download Error: {err}") logging.error(f"Download Error: {err}")
def _update_overlay_state(self, is_active):
"""Update overlay visibility and input handling based on active state."""
if hasattr(self, 'window_hook'):
self.window_hook.set_enabled(is_active)
elif sys.platform == 'linux' and self.overlay_root:
self.overlay_root.setFlag(Qt.WindowTransparentForInput, not is_active)
@Slot(bool) @Slot(bool)
def on_ui_toggle_request(self, is_recording): def on_ui_toggle_request(self, is_recording):
"""Called when recording state changes.""" """Called when recording state changes."""
# Update Window Hook to allow clicking if active
is_active = is_recording or self.bridge.isProcessing is_active = is_recording or self.bridge.isProcessing
if hasattr(self, 'window_hook'): self._update_overlay_state(is_active)
self.window_hook.set_enabled(is_active)
@Slot(bool) @Slot(bool)
def on_processing_changed(self, is_processing): def on_processing_changed(self, is_processing):
is_active = self.bridge.isRecording or is_processing is_active = self.bridge.isRecording or is_processing
if hasattr(self, 'window_hook'): self._update_overlay_state(is_active)
self.window_hook.set_enabled(is_active)
if __name__ == "__main__": if __name__ == "__main__":
import sys import sys
+1 -1
View File
@@ -145,7 +145,7 @@ class LLMEngine:
logging.warning(f"LLM Refusal Detected: '{result}'. Falling back to original.") logging.warning(f"LLM Refusal Detected: '{result}'. Falling back to original.")
return text # Return original text on refusal! return text # Return original text on refusal!
# --- Robust Post-Processing --- # --- Post-Processing ---
# 1. Strip quotes # 1. Strip quotes
if result.startswith('"') and result.endswith('"') and len(result) > 2 and '"' not in result[1:-1]: if result.startswith('"') and result.endswith('"') and len(result) > 2 and '"' not in result[1:-1]: