35 Commits

Author SHA1 Message Date
lashman 9f0d84beb1 Update README.md 2026-03-09 16:25:56 +02:00
Your Name aa2b0acd86 Add WCAG 2.2 AAA accessibility section to README 2026-02-18 22:30:48 +02:00
Your Name 08b9ecc1cb Untrack .gitignore and docs/ from repo 2026-02-18 22:26:32 +02:00
Your Name 02ef33023d Remove unused files: old UI, build scripts, fonts, test files
Remove old widget-based Python UI (replaced by QML), unused build
scripts, NL/variable/webfont variants, old shaders, PNG icons
(replaced by SVGs), and standalone test files. Add build.bat and
build.spec for the bootstrapper build system.
2026-02-18 22:25:04 +02:00
Your Name d509eb5efb WCAG: Review fixes - arrow keys, PageTab roles, AlertMessage
Add Up/Down arrow key navigation to Settings sidebar.
Add Accessible.role: Accessible.PageTab to all 5 tab pages.
Fix Loader status to use AlertMessage role per WCAG 4.1.3.
2026-02-18 21:10:21 +02:00
Your Name 7c80ecfbed WCAG: Loader - subtitle contrast, accessibility, reduced motion 2026-02-18 21:06:43 +02:00
Your Name d8707b5ade WCAG: Overlay - border, keyboard, accessibility, reduced motion 2026-02-18 21:06:43 +02:00
Your Name 07ad3b220d WCAG: Settings - AAA colors, keyboard nav, reduce motion toggle 2026-02-18 21:05:35 +02:00
Your Name dc15e11e8e WCAG: Slider (24px, focus ring) + Switch (I/O marks, border token) 2026-02-18 21:03:21 +02:00
Your Name a70e76b4ab WCAG: Add reduce_motion config, bridge property, OS detection
Config default, reduceMotion Q_PROPERTY on UIBridge, Windows
SystemParametersInfo detection for prefers-reduced-motion.
2026-02-18 21:02:27 +02:00
Your Name d40c83cc45 WCAG: TextField, ComboBox, KeyRecorder - contrast, focus, accessible roles 2026-02-18 21:02:07 +02:00
Your Name f2f80fc863 WCAG: GlowButton, SettingsSection, SettingsItem - contrast, accessible roles 2026-02-18 21:02:06 +02:00
Your Name a6cf9efbcb WCAG: Update design tokens for AAA contrast compliance
accentPurple #7000FF->#B794F6 (7.2:1), textSecondary #999999->#ABABAB (8.1:1),
borderSubtle rgba(0.08)->rgba(0.22) (3:1). Add textDisabled, focusRingWidth, minTargetSize.
2026-02-18 21:00:49 +02:00
Your Name 4615f3084f Docs: Add WCAG 2.2 AAA step-by-step implementation plan
15 tasks covering: design tokens, 6 component fixes, Settings/Overlay/Loader
hardcoded colors, accessibility properties, keyboard nav, reduced motion.
2026-02-18 20:56:51 +02:00
Your Name 937061f710 Docs: Add WCAG 2.2 AAA compliance design document
Comprehensive design covering color system overhaul, component fixes,
accessibility properties, keyboard navigation, and reduced motion support.
2026-02-18 20:53:25 +02:00
Your Name 798a35e6d9 Feat: Integrated Local LLM (Llama 3.2 1B) for Intelligent Correction -- New Core: Added LLMEngine utilizing llama-cpp-python for local private text post-processing. -- Forensic Protocol: Engineered strict system prompts to prevent LLM refusals, censorship, or assistant chatter. -- Three Modes: Grammar, Standard, Rewrite. -- Start/Stop Logic: Consolidated conflicting recording methods. -- Hotkeys: Added dedicated F9 (Correct) vs F8 (Transcribe). -- UI: Updated Settings. -- Build: Updated portable_build.py. -- Docs: Updated README. 2026-01-31 01:02:24 +02:00
Your Name 6737ed4547 Release v1.0.4: The Compatibility Update
- Added robust CPU Fallback for AMD/Non-CUDA GPUs.
- Implemented Lazy Load for AI Engine to prevent startup crashes.
- Added explicit DLL injection for Cublas/Cudnn on Windows.
- Added Corrupt Model Auto-Repair logic.
- Includes pre-compiled v1.0.4 executable.
2026-01-25 20:28:01 +02:00
Your Name aed489dd23 Docs: Detailed explanation of Low VRAM Mode and Style Prompting 2026-01-25 13:52:10 +02:00
Your Name e23c492360 Docs: Add RELEASE_NOTES.md for v1.0.2 2026-01-25 13:46:48 +02:00
Your Name 84f10092e9 Release v1.0.2: Implemented Style Prompting & Removed Grammar Correction
- Removed M2M100 Grammar Correction model completely to reduce bloat/complexity.
- Implemented 'Style Prompting' in Settings -> AI Engine to handle punctuation natively via Whisper.
- Added Style Presets: Standard (Default), Casual, and Custom.
- Optimized Build: Bootstrapper no longer requires transformers/sentencepiece.
- Fixed 'torch' NameError in Low VRAM mode.
- Fixed Bootstrapper missing dependency detection.
- Updated UI to reflect removed features.
- Included compiled v1.0.2 Executable in dist/.
2026-01-25 13:42:06 +02:00
Your Name 03f46ee1e3 Docs: Final polish - Enshittification manifesto and structural refinement 2026-01-24 19:21:01 +02:00
Your Name 0f1bf5f1af Docs: Final polish - 6-col language table and refined manifesto 2026-01-24 19:12:08 +02:00
Your Name 0b2b5848e2 Fix: Translation Reliability, Click-Through, and Docs Sync
- Transcriber: Enforced 'beam_size=5' and prompt injection for robust translation.
- Transcriber: Removed conditioning on previous text to prevent language stickiness.
- Transcriber: Refactored kwargs to sanitize inputs.
- Overlay: Fixed click-through by toggling WS_EX_TRANSPARENT.
- UI: Added real download progress reporting.
- Docs: Refactored language list to table.
2026-01-24 19:05:43 +02:00
Your Name f3bf7541cf Docs: Detailed expansion of README with Translation features and open layout 2026-01-24 18:33:22 +02:00
Your Name 4b84a27a67 v1.0.1 Feature Update and Polish
Full Changelog:

[New Features]
- Added Native Translation Mode:
  - Whisper model now fully supports Translating any language to English
  - Added 'task' and 'language' parameters to Transcriber core
- Dual Hotkey Support:
  - Added separate Global Hotkeys for Transcribe (default F8) and Translate (default F10)
  - Both hotkeys are fully customizable in Settings
  - Engine dynamically switches modes based on which key is pressed

[UI/UX Improvements]
- Settings Window:
  - Widened Hotkey Input fields (240px) to accommodate long combinations
  - Added Pretty-Printing for hotkey sequences (e.g. 'ctrl+f9' display as 'Ctrl + F9')
  - Replaced Country Code dropdown with Full Language Names (99+ languages)
  - Made Language Dropdown scrollable (max height 300px) to prevent screen overflow
  - Removed redundant 'Task' selector (replaced by dedicated hotkeys)
- System Tray:
  - Tooltip now displays both Transcribe and Translate hotkeys
  - Tooltip hotkeys are formatted readably

[Core & Performance]
- Bootstrapper:
  - Implemented Smart Incremental Sync
  - Now checks filesize and content hash before copying files
  - Drastically reduces startup time for subsequent runs
  - Preserves user settings.json during updates
- Backend:
  - Fixed HotkeyManager to support dynamic configuration keys
  - Fixed Language Lock: selecting a language now correctly forces the model to use it
  - Refactored bridge/main connection for language list handling
2026-01-24 18:29:10 +02:00
Your Name f184eb0037 Fix: Invisible overlay blocking mouse clicks
Problem:
The overlay window, even when fully transparent or visually hidden (opacity 0), was still intercepting mouse events. This created a 'dead zone' on the screen where users could not click through to applications behind the overlay. This occurred because the low-level window hook was answering 'HTCAPTION' to hit tests regardless of the UI state.

Solution:
1. Modified 'WindowHook' to accept an 'enabled' state.
2. When disabled, 'WM_NCHITTEST' now returns 'HTTRANSPARENT', allowing the OS to pass the click to the window underneath.
3. Updated 'main.py' to toggle this hook state dynamically:
   - ENABLED when Recording or Processing (UI is visible/active).
   - DISABLED when Idling (UI is hidden/transparent).

Result:
The overlay is now completely non-intrusive when not in use.
2026-01-24 17:51:23 +02:00
Your Name 306bd075ed Aesthetic overhaul of documentation 2026-01-24 17:29:59 +02:00
Your Name a1cc9c61b9 Add language list and file transcription info 2026-01-24 17:27:54 +02:00
Your Name e627e1b8aa Correct hardware detection statement in docs 2026-01-24 17:24:56 +02:00
Your Name eaa572b42f Fix release badge for Gitea 2026-01-24 17:22:14 +02:00
Your Name e900201214 Final documentation polish 2026-01-24 17:20:22 +02:00
Your Name 0d426aea4b Update docs with license and model stats 2026-01-24 17:16:53 +02:00
Your Name b15ce8076f Enhance documentation 2026-01-24 17:12:21 +02:00
Your Name 26f1d8a3e7 Update documentation with new manifesto 2026-01-24 17:06:31 +02:00
Your Name 9ff0e8d108 Initial commit of WhisperVoice 2026-01-24 17:03:52 +02:00
5 changed files with 138 additions and 172 deletions
+8 -8
View File
@@ -24,7 +24,7 @@
## 📡 The Transmission
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.
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.
**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
Whisper Voice is the bridge between thought and text. It listens with superhuman precision, converting spoken word into written form across **99 languages**.
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**.
* **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").
@@ -113,7 +113,7 @@ The agent runs silently in the background, waiting for your signal.
### Injection Protocols
* **Clipboard Paste**: Standard text injection. Instant, reliable.
* **Simulate Typing**: Mimics physical keystrokes at high speed (6000 CPM). Bypasses anti-paste restrictions and "protected" windows.
* **Simulate Typing**: Mimics physical keystrokes at superhuman speed (6000 CPM). Bypasses anti-paste restrictions and "protected" windows.
<br>
@@ -143,7 +143,7 @@ For users with limited GPU memory (e.g., 4GB cards) or those running heavy games
## ♿ 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
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 |
### 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).
* **Arrow Keys**: Navigate the Settings sidebar tabs.
@@ -166,7 +166,7 @@ Full keyboard access - no mouse required:
### Screen Reader Support
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.
* The loader status uses `AlertMessage` for live-region announcements.
* Settings tabs use `Tab` / `PageTab` roles matching WAI-ARIA patterns.
@@ -239,8 +239,8 @@ The engine understands the following 99 languages. You can lock the focus to a s
### ⚖️ PUBLIC DOMAIN (CC0 1.0)
*No Rights Reserved. No Gods. No Masters. No Managers.*
This project is developed with the help of a locally-run LLM via opencode for scaffolding, planning, and routine code tasks. Architecture and design decisions are my own.
Credit to **OpenAI** (Whisper), **Systran** (Faster-Whisper), and **Silero** (VAD).
</div>
<sub><i>This project is developed with the help of a locally-run LLM via opencode for scaffolding, planning, and routine code tasks. Architecture and design decisions are my own.</i></sub>
+28
View File
@@ -0,0 +1,28 @@
# 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.
+7 -8
View File
@@ -1,5 +1,5 @@
# -*- mode: python ; coding: utf-8 -*-
# WhisperVoice - Single-file portable bootstrapper
# WhisperVoice Single-file portable bootstrapper
#
# This builds a TINY exe that contains only:
# - The bootstrapper (downloads Python + deps on first run)
@@ -8,7 +8,6 @@
# NO heavy dependencies (torch, PySide6, etc.) are bundled.
import os
import sys
import glob
block_cipher = None
@@ -23,7 +22,7 @@ app_datas.append(('main.py', 'app_source'))
# requirements.txt
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):
dest = os.path.join('app_source', os.path.dirname(py))
app_datas.append((py, dest))
@@ -43,7 +42,7 @@ for f in glob.glob(os.path.join(qml_dir, 'fonts', 'ttf', '*.ttf')):
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')))
# ── Analysis - only the bootstrapper, NO heavy imports ────────────────────
# ── Analysis only the bootstrapper, NO heavy imports ────────────────────
a = Analysis(
['bootstrapper.py'],
@@ -55,14 +54,14 @@ a = Analysis(
hooksconfig={},
runtime_hooks=[],
excludes=[
# Exclude everything heavy - the bootstrapper only uses stdlib
# Exclude everything heavy the bootstrapper only uses stdlib
'torch', 'numpy', 'scipy', 'PySide6', 'shiboken6',
'faster_whisper', 'ctranslate2', 'llama_cpp',
'sounddevice', 'soundfile', 'keyboard', 'pyperclip',
'psutil', 'pynvml', 'pystray', 'PIL', 'Pillow',
'darkdetect', 'huggingface_hub', 'requests',
'tqdm', 'onnxruntime', 'av',
'matplotlib', 'notebook', 'IPython',
'tkinter', 'matplotlib', 'notebook', 'IPython',
],
win_no_prefer_redirects=False,
win_private_assemblies=False,
@@ -86,11 +85,11 @@ exe = EXE(
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=False, # No console - bootstrapper allocates one when needed
console=False, # No console bootstrapper allocates one when needed
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon='assets/icon.ico' if sys.platform == 'win32' else None,
icon='assets/icon.ico',
)
+80 -141
View File
@@ -9,14 +9,6 @@ app_dir = os.path.dirname(os.path.abspath(__file__))
if app_dir not in sys.path:
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)
# Python 3.8+ on Windows requires explicit DLL directory addition.
@@ -24,32 +16,20 @@ if sys.platform == 'linux' and os.environ.get('WAYLAND_DISPLAY'):
if os.name == 'nt' and hasattr(os, 'add_dll_directory'):
try:
from pathlib import Path
_candidate_dirs = set()
# 1. From sys.path (scan ALL site-packages, not just the first)
# Scan sys.path for site-packages
for p in sys.path:
path_obj = Path(p)
if path_obj.name == 'site-packages' and path_obj.exists():
_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():
for subdir in nvidia_path.iterdir():
bin_path = subdir / "bin"
if bin_path.exists():
os.add_dll_directory(str(bin_path))
# Also add to PATH as fallback - some libraries
# (e.g. CTranslate2) load DLLs lazily via LoadLibrary
# which may not respect os.add_dll_directory()
os.environ['PATH'] = str(bin_path) + os.pathsep + os.environ.get('PATH', '')
nvidia_path = path_obj / "nvidia"
if nvidia_path.exists():
for subdir in nvidia_path.iterdir():
# Add 'bin' folder from each nvidia stub (cublas, cudnn, etc.)
bin_path = subdir / "bin"
if bin_path.exists():
os.add_dll_directory(str(bin_path))
# Also try adding site-packages itself just in case
# os.add_dll_directory(str(path_obj))
break
except Exception:
pass
# -----------------------------------------------------------------------------
@@ -69,8 +49,7 @@ from src.core.hotkey_manager import HotkeyManager
from src.core.config import ConfigManager
from src.utils.injector import InputInjector
from src.core.paths import get_models_path, get_bundle_path
if os.name == 'nt':
from src.utils.window_hook import WindowHook
from src.utils.window_hook import WindowHook
from PySide6.QtGui import QSurfaceFormat
@@ -84,52 +63,37 @@ os.environ["QT_ENABLE_HIGHDPI_SCALING"] = "1"
os.environ["QT_AUTOSCREENSCALEFACTOR"] = "1"
# Detect resolution without creating QApplication (Fixes crash)
if os.name == 'nt':
try:
import ctypes
user32 = ctypes.windll.user32
# Get physical screen width (unscaled)
# SetProcessDPIAware is needed to get the true resolution
user32.SetProcessDPIAware()
width = user32.GetSystemMetrics(0)
# Base scale centers around 1920 width.
# At 3840 (4k), res_scale is 2.0. If we want it ~40% smaller, we multiply by 0.6 = 1.2
res_scale = (width / 1920)
if width >= 3840:
res_scale *= 0.6 # Make it significantly smaller at 4k as requested
try:
import ctypes
user32 = ctypes.windll.user32
# Get physical screen width (unscaled)
# SetProcessDPIAware is needed to get the true resolution
user32.SetProcessDPIAware()
width = user32.GetSystemMetrics(0)
# Base scale centers around 1920 width.
# At 3840 (4k), res_scale is 2.0. If we want it ~40% smaller, we multiply by 0.6 = 1.2
res_scale = (width / 1920)
if width >= 3840:
res_scale *= 0.6 # Make it significantly smaller at 4k as requested
os.environ["QT_SCALE_FACTOR"] = str(max(1.0, res_scale))
except:
pass
# On Linux, Qt handles DPI automatically via QT_ENABLE_HIGHDPI_SCALING
os.environ["QT_SCALE_FACTOR"] = str(max(1.0, res_scale))
except:
pass
# Detect "Reduce Motion" preference
if os.name == 'nt':
try:
import ctypes
SPI_GETCLIENTAREAANIMATION = 0x1042
animation_enabled = ctypes.c_bool(True)
ctypes.windll.user32.SystemParametersInfoW(
SPI_GETCLIENTAREAANIMATION, 0,
ctypes.byref(animation_enabled), 0
)
if not animation_enabled.value:
ConfigManager().data["reduce_motion"] = True
ConfigManager().save()
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
# Detect Windows "Reduce Motion" preference
try:
import ctypes
SPI_GETCLIENTAREAANIMATION = 0x1042
animation_enabled = ctypes.c_bool(True)
ctypes.windll.user32.SystemParametersInfoW(
SPI_GETCLIENTAREAANIMATION, 0,
ctypes.byref(animation_enabled), 0
)
if not animation_enabled.value:
ConfigManager().data["reduce_motion"] = True
ConfigManager().save()
except Exception:
pass
# Configure Logging
class QmlLoggingHandler(logging.Handler, QObject):
@@ -176,25 +140,15 @@ class DownloadWorker(QThread):
def run(self):
try:
import requests
import shutil
from tqdm import tqdm
model_path = get_models_path()
# Determine what to download
dest_dir = model_path / f"faster-whisper-{self.model_name}"
repo_id = f"Systran/faster-whisper-{self.model_name}"
required_files = ["config.json", "model.bin", "tokenizer.json", "vocabulary.json"]
files = ["config.json", "model.bin", "tokenizer.json", "vocabulary.json"]
base_url = f"https://huggingface.co/{repo_id}/resolve/main"
# 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)
dest_dir.mkdir(parents=True, exist_ok=True)
logging.info(f"Downloading {self.model_name} to {dest_dir}...")
# 1. Calculate Total Size
@@ -202,7 +156,7 @@ class DownloadWorker(QThread):
file_sizes = {}
with requests.Session() as s:
for fname in required_files:
for fname in files:
url = f"{base_url}/{fname}"
head = s.head(url, allow_redirects=True)
if head.status_code == 200:
@@ -210,21 +164,25 @@ class DownloadWorker(QThread):
file_sizes[fname] = size
total_size += size
else:
logging.warning(f"HEAD failed for {fname}: HTTP {head.status_code}")
# Abort if any required file is unavailable
missing = [f for f in required_files if f not in file_sizes]
if missing:
shutil.rmtree(tmp_dir, ignore_errors=True)
raise RuntimeError(f"Required model files unavailable: {missing}")
# Fallback for vocabulary.json vs vocabulary.txt
if fname == "vocabulary.json":
# Try .txt? Or just skip if not found?
# Faster-whisper usually has vocabulary.json
pass
# 2. Download loop
downloaded_bytes = 0
with requests.Session() as s:
for fname in required_files:
for fname in files:
if fname not in file_sizes: continue
url = f"{base_url}/{fname}"
dest_file = tmp_dir / fname
dest_file = dest_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.raise_for_status()
@@ -234,22 +192,12 @@ class DownloadWorker(QThread):
if chunk:
f.write(chunk)
downloaded_bytes += len(chunk)
# Emit Progress
if total_size > 0:
pct = int((downloaded_bytes / total_size) * 100)
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()
except Exception as e:
@@ -363,7 +311,6 @@ class WhisperApp(QObject):
self.bridge.llmDownloadRequested.connect(self.on_llm_download_requested)
self.engine.rootContext().setContextProperty("ui", self.bridge)
self.engine.rootContext().setContextProperty("isLinux", sys.platform == 'linux')
# 2. Tray setup
self.tray = SystemTray()
@@ -435,27 +382,23 @@ class WhisperApp(QObject):
self.settings_root.setVisible(False)
# Install Low-Level Window Hook for Transparent Hit Test
if os.name == 'nt':
try:
from src.utils.window_hook import WindowHook
hwnd = self.overlay_root.winId()
# Initial scale from config
scale = float(self.config.get("ui_scale"))
try:
from src.utils.window_hook import WindowHook
hwnd = self.overlay_root.winId()
# Initial scale from config
scale = float(self.config.get("ui_scale"))
# Current Overlay Dimensions
win_w = int(460 * scale)
win_h = int(180 * scale)
# Current Overlay Dimensions
win_w = int(460 * scale)
win_h = int(180 * scale)
self.window_hook = WindowHook(hwnd, win_w, win_h, initial_scale=scale)
self.window_hook.install()
self.window_hook = WindowHook(hwnd, win_w, win_h, initial_scale=scale)
self.window_hook.install()
# Initial state: Disabled because we start inactive
self.window_hook.set_enabled(False)
except Exception as 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)
# Initial state: Disabled because we start inactive
self.window_hook.set_enabled(False)
except Exception as e:
logging.error(f"Failed to install WindowHook: {e}")
def center_overlay(self):
"""Calculates and sets the Overlay position above the taskbar."""
@@ -869,23 +812,19 @@ class WhisperApp(QObject):
self.bridge.update_status("Error")
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)
def on_ui_toggle_request(self, is_recording):
"""Called when recording state changes."""
# Update Window Hook to allow clicking if active
is_active = is_recording or self.bridge.isProcessing
self._update_overlay_state(is_active)
if hasattr(self, 'window_hook'):
self.window_hook.set_enabled(is_active)
@Slot(bool)
def on_processing_changed(self, is_processing):
is_active = self.bridge.isRecording or is_processing
self._update_overlay_state(is_active)
if hasattr(self, 'window_hook'):
self.window_hook.set_enabled(is_active)
if __name__ == "__main__":
import sys
+1 -1
View File
@@ -145,7 +145,7 @@ class LLMEngine:
logging.warning(f"LLM Refusal Detected: '{result}'. Falling back to original.")
return text # Return original text on refusal!
# --- Post-Processing ---
# --- Robust Post-Processing ---
# 1. Strip quotes
if result.startswith('"') and result.endswith('"') and len(result) > 2 and '"' not in result[1:-1]: