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
81 changed files with 567 additions and 1571 deletions
-25
View File
@@ -1,25 +0,0 @@
# Python
__pycache__/
*.py[cod]
*$py.class
# Virtual Environment
venv/
env/
# Distribution / Build
dist/
build/
*.spec
_unused_files/
runtime/
# IDEs
.vscode/
.idea/
# Application Specific
models/
recordings/
*.log
settings.json
+50 -2
View File
@@ -24,7 +24,7 @@
## 📡 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.
@@ -59,7 +59,7 @@ The core engine (`CTranslate2`) is heavily optimized for Nvidia tensor cores.
## 🖋️ 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.
* **Contextual Intelligence**: Smarter than standard dictation; it understands the flow of sentences to resolve homophones and technical jargon ($1.5k vs "fifteen hundred dollars").
@@ -141,6 +141,54 @@ 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.
### Color & Contrast
Every design token is calibrated for **Enhanced Contrast** (WCAG 1.4.6, 7:1 minimum):
| Token | Ratio | Purpose |
| :--- | :--- | :--- |
| `textPrimary` #FAFAFA | ~17:1 | Body text, headings |
| `textSecondary` #ABABAB | 8.1:1 | Descriptions, hints |
| `accentPurple` #B794F6 | 7.2:1 | Interactive elements, focus rings |
| `borderSubtle` | 3:1 | Non-text contrast for borders and separators |
### Keyboard Navigation
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.
* **Enter / Space**: Activate any focused control.
* **Focus Rings**: Every interactive element shows a visible 2px accent-colored focus indicator.
### 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`.
* 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.
### Non-Color State Indicators
Toggle switches display **I/O marks** inside the thumb (not just color changes), ensuring state is perceivable without color vision (WCAG 1.4.1).
### Target Sizes
All interactive controls meet the **24px minimum** target size (WCAG 2.5.8). Slider handles, buttons, switches, and nav items are all comfortably clickable.
### Reduced Motion
A **Reduce Motion** toggle (Settings > Visuals) disables all decorative animations:
* Shader effects (gradient blobs, glow, CRT scanlines, rainbow waveform)
* Particle systems
* Pulsing animations (mic button, recording timer, border)
* Loader logo pulse and progress shimmer
The system also respects the **Windows "Show animations" preference** via `SystemParametersInfo` detection. Essential information (recording state, progress bars, timer text) remains fully functional.
---
## 🛠️ Deployment
### 📥 Installation
-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.
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

+31
View File
@@ -0,0 +1,31 @@
@echo off
echo ============================================
echo Building WhisperVoice Portable EXE
echo ============================================
echo.
if not exist venv (
echo ERROR: venv not found. Run run_source.bat first.
pause
exit /b 1
)
call venv\Scripts\activate
echo Running PyInstaller (single-file bootstrapper)...
pyinstaller build.spec --clean --noconfirm
if %ERRORLEVEL% NEQ 0 (
echo.
echo BUILD FAILED! Check errors above.
pause
exit /b 1
)
echo.
echo Build complete!
echo.
echo Output: dist\WhisperVoice.exe
echo.
echo This single exe will download all dependencies on first run.
pause
+96
View File
@@ -0,0 +1,96 @@
# -*- mode: python ; coding: utf-8 -*-
# WhisperVoice - Single-file portable bootstrapper
#
# This builds a TINY exe that contains only:
# - The bootstrapper (downloads Python + deps on first run)
# - The app source code (bundled as data, extracted to runtime/app/)
#
# NO heavy dependencies (torch, PySide6, etc.) are bundled.
import os
import sys
import glob
block_cipher = None
# ── Collect app source as data (goes into app_source/ inside the bundle) ──
app_datas = []
# main.py
app_datas.append(('main.py', 'app_source'))
# requirements.txt
app_datas.append(('requirements.txt', 'app_source'))
# 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))
# src/ui/qml/** (QML files, shaders, SVGs, fonts, qmldir)
qml_dir = os.path.join('src', 'ui', 'qml')
for pattern in ('*.qml', '*.qsb', '*.frag', '*.svg', '*.ico', '*.png',
'qmldir', 'AUTHORS.txt', 'OFL.txt'):
for f in glob.glob(os.path.join(qml_dir, pattern)):
app_datas.append((f, os.path.join('app_source', qml_dir)))
# Fonts
for f in glob.glob(os.path.join(qml_dir, 'fonts', 'ttf', '*.ttf')):
app_datas.append((f, os.path.join('app_source', qml_dir, 'fonts', 'ttf')))
# assets/
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 ────────────────────
a = Analysis(
['bootstrapper.py'],
pathex=[],
binaries=[],
datas=app_datas,
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[
# 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',
],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
# ── Single-file EXE (--onefile) ──────────────────────────────────────────
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='WhisperVoice',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
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,
)
-66
View File
@@ -1,66 +0,0 @@
"""
Build the Lightweight Bootstrapper
==================================
This creates a small (~15-20MB) .exe that downloads Python + dependencies on first run.
"""
import os
import shutil
import PyInstaller.__main__
from pathlib import Path
def build_bootstrapper():
project_root = Path(__file__).parent.absolute()
dist_path = project_root / "dist"
# Collect all app source files to bundle
# These will be extracted and used when setting up
app_source_files = [
("src", "app_source/src"),
("assets", "app_source/assets"), # Include icon etc
("main.py", "app_source"),
("requirements.txt", "app_source"),
]
add_data_args = []
for src, dst in app_source_files:
src_path = project_root / src
if src_path.exists():
add_data_args.extend(["--add-data", f"{src}{os.pathsep}{dst}"])
# Use absolute project root for copying
shutil.copy2(project_root / "assets" / "icon.ico", project_root / "app_icon.ico")
print("🚀 Building Lightweight Bootstrapper...")
print("⏳ This creates a small .exe that downloads dependencies on first run.\n")
PyInstaller.__main__.run([
"bootstrapper.py",
"--name=WhisperVoice",
"--onefile",
"--noconsole", # Re-enabled! Error handling in bootstrapper is ready.
"--clean",
"--icon=app_icon.ico", # Simplified path at root
*add_data_args,
])
exe_path = dist_path / "WhisperVoice.exe"
if exe_path.exists():
size_mb = exe_path.stat().st_size / (1024 * 1024)
print("\n" + "="*60)
print("✅ BOOTSTRAPPER BUILD COMPLETE!")
print("="*60)
print(f"\n📍 Output: {exe_path}")
print(f"📦 Size: {size_mb:.1f} MB")
print("\n📋 How it works:")
print(" 1. User runs WhisperVoice.exe")
print(" 2. First run: Downloads Python + packages (~2-3GB)")
print(" 3. Subsequent runs: Launches instantly")
print("\n💡 The 'runtime/' folder will be created next to the .exe")
else:
print("\n❌ Build failed. Check the output above for errors.")
if __name__ == "__main__":
os.chdir(Path(__file__).parent)
build_bootstrapper()
-17
View File
@@ -1,17 +0,0 @@
@echo off
echo Building Whisper Voice Portable EXE...
if not exist venv (
echo Please run run_source.bat first to setup environment!
pause
exit /b
)
call venv\Scripts\activate
pip install pyinstaller
echo Running PyInstaller...
pyinstaller build.spec --clean --noconfirm
echo.
echo Build Complete! Check dist/WhisperVoice.exe
pause
-14
View File
@@ -1,14 +0,0 @@
from PIL import Image
import os
# Path from the generate_image tool output
src = r"C:/Users/lashman/.gemini/antigravity/brain/9a183770-2481-475b-b748-03f4910f9a8e/app_icon_1769195450659.png"
dst = r"d:\!!! SYSTEM DATA !!!\Desktop\python crap\whisper_voice\assets\icon.ico"
if os.path.exists(src):
img = Image.open(src)
# Resize to standard icon sizes
img.save(dst, format='ICO', sizes=[(256, 256)])
print(f"Icon saved to {dst}")
else:
print(f"Source image not found: {src}")
-43
View File
@@ -1,43 +0,0 @@
import requests
import os
ICONS = {
"settings.svg": "https://raw.githubusercontent.com/FortAwesome/Font-Awesome/6.x/svgs/solid/gear.svg",
"visibility.svg": "https://raw.githubusercontent.com/FortAwesome/Font-Awesome/6.x/svgs/solid/eye.svg",
"smart_toy.svg": "https://raw.githubusercontent.com/FortAwesome/Font-Awesome/6.x/svgs/solid/brain.svg",
"microphone.svg": "https://raw.githubusercontent.com/FortAwesome/Font-Awesome/6.x/svgs/solid/microphone.svg"
}
TARGET_DIR = r"d:\!!! SYSTEM DATA !!!\Desktop\python crap\whisper_voice\src\ui\qml"
def download_icons():
if not os.path.exists(TARGET_DIR):
print(f"Directory not found: {TARGET_DIR}")
return
for filename, url in ICONS.items():
try:
print(f"Downloading {filename} from {url}...")
response = requests.get(url, timeout=10)
response.raise_for_status()
# Force white fill
content = response.text
if "<path" in content and "fill=" not in content:
content = content.replace("<path", '<path fill="#ffffff"')
elif "<path" in content and "fill=" in content:
# Regex or simple replace if possible, but simplest is usually just injecting style or checking common FA format
pass # FA standard usually has no fill.
# Additional safety: Replace currentcolor if present
content = content.replace("currentColor", "#ffffff")
filepath = os.path.join(TARGET_DIR, filename)
with open(filepath, 'w', encoding='utf-8') as f:
f.write(content)
print(f"Saved {filepath} (modified to white)")
except Exception as e:
print(f"FAILED to download {filename}: {e}")
if __name__ == "__main__":
download_icons()
+142 -66
View File
@@ -9,6 +9,14 @@ 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.
@@ -16,20 +24,32 @@ if app_dir not in sys.path:
if os.name == 'nt' and hasattr(os, 'add_dll_directory'):
try:
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:
path_obj = Path(p)
if path_obj.name == 'site-packages' and path_obj.exists():
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
_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', '')
except Exception:
pass
# -----------------------------------------------------------------------------
@@ -49,7 +69,8 @@ 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
from src.utils.window_hook import WindowHook
if os.name == 'nt':
from src.utils.window_hook import WindowHook
from PySide6.QtGui import QSurfaceFormat
@@ -63,22 +84,52 @@ os.environ["QT_ENABLE_HIGHDPI_SCALING"] = "1"
os.environ["QT_AUTOSCREENSCALEFACTOR"] = "1"
# Detect resolution without creating QApplication (Fixes crash)
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
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
os.environ["QT_SCALE_FACTOR"] = str(max(1.0, res_scale))
except:
pass
os.environ["QT_SCALE_FACTOR"] = str(max(1.0, res_scale))
except:
pass
# On Linux, Qt handles DPI automatically via QT_ENABLE_HIGHDPI_SCALING
# 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
# Configure Logging
class QmlLoggingHandler(logging.Handler, QObject):
@@ -125,15 +176,25 @@ class DownloadWorker(QThread):
def run(self):
try:
import requests
from tqdm import tqdm
import shutil
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}"
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"
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}...")
# 1. Calculate Total Size
@@ -141,7 +202,7 @@ class DownloadWorker(QThread):
file_sizes = {}
with requests.Session() as s:
for fname in files:
for fname in required_files:
url = f"{base_url}/{fname}"
head = s.head(url, allow_redirects=True)
if head.status_code == 200:
@@ -149,25 +210,21 @@ class DownloadWorker(QThread):
file_sizes[fname] = size
total_size += size
else:
# 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
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}")
# 2. Download loop
downloaded_bytes = 0
with requests.Session() as s:
for fname in files:
if fname not in file_sizes: continue
for fname in required_files:
url = f"{base_url}/{fname}"
dest_file = dest_dir / fname
# Resume check?
# Simpler to just overwrite for reliability unless we want complex resume logic.
# We'll overwrite.
dest_file = tmp_dir / fname
resp = s.get(url, stream=True)
resp.raise_for_status()
@@ -177,12 +234,22 @@ 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:
@@ -296,6 +363,7 @@ 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()
@@ -367,23 +435,27 @@ class WhisperApp(QObject):
self.settings_root.setVisible(False)
# Install Low-Level Window Hook for Transparent Hit Test
try:
from src.utils.window_hook import WindowHook
hwnd = self.overlay_root.winId()
# Initial scale from config
scale = float(self.config.get("ui_scale"))
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"))
# 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}")
# 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)
def center_overlay(self):
"""Calculates and sets the Overlay position above the taskbar."""
@@ -797,19 +869,23 @@ 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
if hasattr(self, 'window_hook'):
self.window_hook.set_enabled(is_active)
self._update_overlay_state(is_active)
@Slot(bool)
def on_processing_changed(self, is_processing):
is_active = self.bridge.isRecording or is_processing
if hasattr(self, 'window_hook'):
self.window_hook.set_enabled(is_active)
self._update_overlay_state(is_active)
if __name__ == "__main__":
import sys
-86
View File
@@ -1,86 +0,0 @@
"""
Portable Build Script for WhisperVoice.
=======================================
Creates a single-file portable .exe using PyInstaller.
All data (settings, models) will be stored next to the .exe at runtime.
"""
import os
import shutil
import PyInstaller.__main__
from pathlib import Path
def build_portable():
# 1. Setup Paths
project_root = Path(__file__).parent.absolute()
dist_path = project_root / "dist"
build_path = project_root / "build"
# 2. Define Assets to bundle (into the .exe)
# Format: (Source, Destination relative to bundle root)
data_files = [
# QML files
("src/ui/qml/*.qml", "src/ui/qml"),
("src/ui/qml/*.svg", "src/ui/qml"),
("src/ui/qml/*.qsb", "src/ui/qml"),
("src/ui/qml/fonts/ttf/*.ttf", "src/ui/qml/fonts/ttf"),
# Subprocess worker script (CRITICAL for transcription)
("src/core/transcribe_worker.py", "src/core"),
]
# Convert to PyInstaller format "--add-data source;dest" (Windows uses ';')
add_data_args = []
for src, dst in data_files:
add_data_args.extend(["--add-data", f"{src}{os.pathsep}{dst}"])
# 3. Run PyInstaller
print("🚀 Starting Portable Build...")
print("⏳ This may take 5-10 minutes...")
PyInstaller.__main__.run([
"bootstrapper.py", # Entry point (Tiny Installer)
"--name=WhisperVoice", # EXE name
"--onefile", # Single EXE
"--noconsole", # No terminal window
"--clean", # Clean cache
# Bundle the app source to be extracted by bootstrapper
# The bootstrapper expects 'app_source' folder in bundled resources
"--add-data", f"src{os.pathsep}app_source/src",
"--add-data", f"main.py{os.pathsep}app_source",
"--add-data", f"requirements.txt{os.pathsep}app_source",
# Add assets
"--add-data", f"src/ui/qml{os.pathsep}app_source/src/ui/qml",
"--add-data", f"assets{os.pathsep}app_source/assets",
# No heavy collections!
# The bootstrapper uses internal pip to install everything.
# Exclude heavy modules to ensure this exe stays tiny
"--exclude-module", "faster_whisper",
"--exclude-module", "torch",
"--exclude-module", "PySide6",
"--exclude-module", "llama_cpp",
# Icon
# "--icon=icon.ico",
])
print("\n" + "="*60)
print("✅ BUILD COMPLETE!")
print("="*60)
print(f"\n📍 Output: {dist_path / 'WhisperVoice.exe'}")
print("\n📋 First run instructions:")
print(" 1. Place WhisperVoice.exe in a folder (e.g., C:\\WhisperVoice\\)")
print(" 2. Run it - it will create 'models' and 'settings.json' folders")
print(" 3. The app will download the Whisper model on first transcription\n")
print("💡 TIP: Keep the .exe with its generated files for true portability!")
if __name__ == "__main__":
# Ensure we are in project root
os.chdir(Path(__file__).parent)
build_portable()
-73
View File
@@ -1,73 +0,0 @@
import os
import requests
import mimetypes
# Configuration
API_URL = "https://git.lashman.live/api/v1"
OWNER = "lashman"
REPO = "whisper_voice"
TAG = "v1.0.4"
TOKEN = "6153890332afff2d725aaf4729bc54b5030d5700" # Extracted from git config
EXE_PATH = r"dist\WhisperVoice.exe"
headers = {
"Authorization": f"token {TOKEN}",
"Accept": "application/json"
}
def create_release():
print(f"Creating release {TAG}...")
# Read Release Notes
with open("RELEASE_NOTES.md", "r", encoding="utf-8") as f:
notes = f.read()
# Create Release
payload = {
"tag_name": TAG,
"name": TAG,
"body": notes,
"draft": False,
"prerelease": False
}
url = f"{API_URL}/repos/{OWNER}/{REPO}/releases"
resp = requests.post(url, json=payload, headers=headers)
if resp.status_code == 201:
print("Release created successfully!")
return resp.json()
elif resp.status_code == 409:
print("Release already exists. Fetching it...")
# Get by tag
resp = requests.get(f"{API_URL}/repos/{OWNER}/{REPO}/releases/tags/{TAG}", headers=headers)
if resp.status_code == 200:
return resp.json()
print(f"Failed to create release: {resp.status_code} - {resp.text}")
return None
def upload_asset(release_id, file_path):
print(f"Uploading asset: {file_path}...")
filename = os.path.basename(file_path)
with open(file_path, "rb") as f:
data = f.read()
url = f"{API_URL}/repos/{OWNER}/{REPO}/releases/{release_id}/assets?name={filename}"
# Gitea API expects raw body
resp = requests.post(url, data=data, headers=headers)
if resp.status_code == 201:
print(f"Uploaded {filename} successfully!")
else:
print(f"Failed to upload asset: {resp.status_code} - {resp.text}")
def main():
release = create_release()
if release:
upload_asset(release["id"], EXE_PATH)
if __name__ == "__main__":
main()
-5
View File
@@ -1,5 +0,0 @@
@echo off
echo [LAUNCHER] Starting Fake Blur UI (Python/Qt)...
call venv\Scripts\activate.bat
python main.py
if %errorlevel% neq 0 pause
+4 -1
View File
@@ -58,7 +58,10 @@ DEFAULT_SETTINGS = {
# Low VRAM Mode
"unload_models_after_use": False # If True, models are unloaded immediately to free VRAM
"unload_models_after_use": False, # If True, models are unloaded immediately to free VRAM
# Accessibility
"reduce_motion": False # Disable animations for WCAG 2.3.3
}
class ConfigManager:
-31
View File
@@ -1,31 +0,0 @@
@echo off
echo [DEBUG] LAUNCHER STARTED
echo [DEBUG] CWD: %CD%
echo [DEBUG] Python Path (expected relative): ..\python\python.exe
REM Read stdin to a file to verify data input (optional debugging)
REM python.exe might be in different relative path depending on where this bat is run
REM We assume this bat is in runtime/app/src/core/
REM So python is in ../../../python/python.exe
set PYTHON_EXE=..\..\..\python\python.exe
if exist "%PYTHON_EXE%" (
echo [DEBUG] Found Python at %PYTHON_EXE%
) else (
echo [ERROR] Python NOT found at %PYTHON_EXE%
echo [ERROR] Listing relative directories:
dir ..\..\..\
pause
exit /b 1
)
echo [DEBUG] Launching script: transcribe_worker.py
"%PYTHON_EXE%" transcribe_worker.py
if %ERRORLEVEL% NEQ 0 (
echo [ERROR] Python script failed with code %ERRORLEVEL%
pause
) else (
echo [SUCCESS] Script finished.
pause
)
+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!
# --- Robust Post-Processing ---
# --- Post-Processing ---
# 1. Strip quotes
if result.startswith('"') and result.endswith('"') and len(result) > 2 and '"' not in result[1:-1]:
+13
View File
@@ -111,6 +111,7 @@ class UIBridge(QObject):
settingChanged = Signal(str, 'QVariant')
modelStatesChanged = Signal() # Notify UI to re-check isModelDownloaded
llmDownloadRequested = Signal()
reduceMotionChanged = Signal(bool)
def __init__(self, parent=None):
super().__init__(parent)
@@ -130,6 +131,7 @@ class UIBridge(QObject):
self._app_vram_mb = 0.0
self._app_vram_percent = 0.0
self._is_destroyed = False
self._reduce_motion = bool(ConfigManager().get("reduce_motion"))
# Start QThread Stats Worker
self.stats_worker = StatsWorker()
@@ -277,6 +279,8 @@ class UIBridge(QObject):
ConfigManager().set(key, value)
if key == "ui_scale":
self.uiScale = float(value)
if key == "reduce_motion":
self.reduceMotion = bool(value)
self.settingChanged.emit(key, value) # Notify listeners (e.g. Overlay)
@Property(float, notify=uiScaleChanged)
@@ -288,6 +292,15 @@ class UIBridge(QObject):
self._ui_scale = val
self.uiScaleChanged.emit(val)
@Property(bool, notify=reduceMotionChanged)
def reduceMotion(self): return self._reduce_motion
@reduceMotion.setter
def reduceMotion(self, val):
if self._reduce_motion != val:
self._reduce_motion = val
self.reduceMotionChanged.emit(val)
@Property(float, notify=appCpuChanged)
def appCpu(self): return self._app_cpu
-210
View File
@@ -1,210 +0,0 @@
"""
Modern Components Library.
==========================
Contains custom-painted widgets that move beyond the standard 'amateur' Qt look.
Implements smooth animations, hardware acceleration, and glassmorphism.
"""
from PySide6.QtWidgets import (
QPushButton, QWidget, QVBoxLayout, QHBoxLayout,
QLabel, QGraphicsDropShadowEffect, QFrame, QAbstractButton
)
from PySide6.QtCore import Qt, QPropertyAnimation, QEasingCurve, Property, QRect, QPoint, Signal, Slot
from PySide6.QtGui import QPainter, QColor, QBrush, QPen, QLinearGradient, QFont
from src.ui.styles import Theme
class GlassButton(QPushButton):
"""A premium button with gradient hover effects and smooth scaling."""
def __init__(self, text, parent=None, accent_color=Theme.ACCENT_CYAN):
super().__init__(text, parent)
self.accent = QColor(accent_color)
self.setCursor(Qt.PointingHandCursor)
self.setFixedHeight(40)
self._hover_opacity = 0.0
self.setStyleSheet(f"""
QPushButton {{
background-color: rgba(255, 255, 255, 0.05);
border: 1px solid {Theme.BORDER_SUBTLE};
color: {Theme.TEXT_SECONDARY};
border-radius: 8px;
padding: 0 20px;
font-size: 13px;
font-weight: 600;
}}
""")
# Hover Animation
self.anim = QPropertyAnimation(self, b"hover_opacity")
self.anim.setDuration(200)
self.anim.setStartValue(0.0)
self.anim.setEndValue(1.0)
self.anim.setEasingCurve(QEasingCurve.OutCubic)
@Property(float)
def hover_opacity(self): return self._hover_opacity
@hover_opacity.setter
def hover_opacity(self, value):
self._hover_opacity = value
self.update()
def enterEvent(self, event):
self.anim.setDirection(QPropertyAnimation.Forward)
self.anim.start()
super().enterEvent(event)
def leaveEvent(self, event):
self.anim.setDirection(QPropertyAnimation.Backward)
self.anim.start()
super().leaveEvent(event)
def paintEvent(self, event):
"""Custom paint for the glow effect."""
super().paintEvent(event)
if self._hover_opacity > 0:
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
# Subtle Glow Border
color = QColor(self.accent)
color.setAlphaF(self._hover_opacity * 0.5)
painter.setPen(QPen(color, 1.5))
painter.setBrush(Qt.NoBrush)
painter.drawRoundedRect(self.rect().adjusted(1,1,-1,-1), 8, 8)
# Text Glow color shift
self.setStyleSheet(f"""
QPushButton {{
background-color: rgba(255, 255, 255, {0.05 + (self._hover_opacity * 0.05)});
border: 1px solid {Theme.BORDER_SUBTLE};
color: white;
border-radius: 8px;
padding: 0 20px;
font-size: 13px;
font-weight: 600;
}}
""")
class ModernSwitch(QAbstractButton):
"""A sleek iOS-style toggle switch."""
def __init__(self, parent=None, active_color=Theme.ACCENT_GREEN):
super().__init__(parent)
self.setCheckable(True)
self.setFixedSize(44, 24)
self._thumb_pos = 3.0
self.active_color = QColor(active_color)
self.anim = QPropertyAnimation(self, b"thumb_pos")
self.anim.setDuration(200)
self.anim.setEasingCurve(QEasingCurve.InOutCubic)
@Property(float)
def thumb_pos(self): return self._thumb_pos
@thumb_pos.setter
def thumb_pos(self, value):
self._thumb_pos = value
self.update()
def nextCheckState(self):
super().nextCheckState()
self.anim.stop()
if self.isChecked():
self.anim.setEndValue(23.0)
else:
self.anim.setEndValue(3.0)
self.anim.start()
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
# Background
bg_color = QColor("#2d2d3d")
if self.isChecked():
bg_color = self.active_color
painter.setBrush(bg_color)
painter.setPen(Qt.NoPen)
painter.drawRoundedRect(self.rect(), 12, 12)
# Thumb
painter.setBrush(Qt.white)
painter.drawEllipse(QPoint(self._thumb_pos + 9, 12), 9, 9)
class ModernFrame(QFrame):
"""A base frame with rounded corners and a shadow."""
def __init__(self, parent=None):
super().__init__(parent)
self.setObjectName("premiumFrame")
self.setStyleSheet(f"""
#premiumFrame {{
background-color: {Theme.BG_CARD};
border: 1px solid {Theme.BORDER_SUBTLE};
border-radius: 12px;
}}
""")
self.shadow = QGraphicsDropShadowEffect(self)
self.shadow.setBlurRadius(25)
self.shadow.setXOffset(0)
self.shadow.setYOffset(8)
self.shadow.setColor(QColor(0, 0, 0, 180))
self.setGraphicsEffect(self.shadow)
from PySide6.QtWidgets import (
QPushButton, QWidget, QVBoxLayout, QHBoxLayout,
QLabel, QGraphicsDropShadowEffect, QFrame, QAbstractButton, QSlider
)
class ModernSlider(QSlider):
"""A custom painted modern slider with a glowing knob."""
def __init__(self, orientation=Qt.Horizontal, parent=None):
super().__init__(orientation, parent)
self.setStyleSheet(f"""
QSlider::groove:horizontal {{
border: 1px solid {Theme.BG_DARK};
height: 4px;
background: {Theme.BG_DARK};
margin: 2px 0;
border-radius: 2px;
}}
QSlider::handle:horizontal {{
background: {Theme.ACCENT_CYAN};
border: 2px solid white;
width: 16px;
height: 16px;
margin: -7px 0;
border-radius: 8px;
}}
QSlider::add-page:horizontal {{
background: {Theme.BG_DARK};
}}
QSlider::sub-page:horizontal {{
background: {Theme.ACCENT_CYAN};
border-radius: 2px;
}}
""")
class FramelessWindow(QWidget):
"""Base class for all premium windows to handle dragging and frameless logic."""
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.NoDropShadowWindowHint)
self.setAttribute(Qt.WA_TranslucentBackground)
self._drag_pos = None
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
self._drag_pos = event.globalPosition().toPoint() - self.frameGeometry().topLeft()
event.accept()
def mouseMoveEvent(self, event):
if event.buttons() & Qt.LeftButton:
self.move(event.globalPosition().toPoint() - self._drag_pos)
event.accept()
-109
View File
@@ -1,109 +0,0 @@
"""
Loader Widget Module.
=====================
Handles the application initialization and model checks.
Refactored for 2026 Premium Aesthetics.
"""
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QProgressBar
from PySide6.QtCore import Qt, QThread, Signal
from PySide6.QtGui import QFont
import os
import logging
from faster_whisper import download_model
from src.core.paths import get_models_path
from src.ui.styles import Theme, StyleGenerator, load_modern_fonts
from src.ui.components import FramelessWindow, ModernFrame
class DownloadWorker(QThread):
"""Background worker for model downloads."""
progress = Signal(str, int)
download_finished = Signal()
error = Signal(str)
def run(self):
try:
model_path = get_models_path()
self.progress.emit("Verifying AI Core...", 10)
os.environ["HF_HOME"] = str(model_path)
self.progress.emit("Downloading Model...", 30)
download_model("small", output_dir=str(model_path))
self.progress.emit("System Ready!", 100)
self.download_finished.emit()
except Exception as e:
logging.error(f"Loader failed: {e}")
self.error.emit(str(e))
class LoaderWidget(FramelessWindow):
"""
Premium bootstrapper UI.
Inherits from FramelessWindow for rounded glass look.
"""
ready_signal = Signal()
def __init__(self):
super().__init__()
self.setFixedSize(400, 180)
# Main Layout
self.root = QVBoxLayout(self)
self.root.setContentsMargins(10, 10, 10, 10)
# Glass Card
self.card = ModernFrame()
self.card.setStyleSheet(StyleGenerator.get_glass_card(radius=20))
self.root.addWidget(self.card)
# Content Layout
self.layout = QVBoxLayout(self.card)
self.layout.setContentsMargins(30,30,30,30)
self.layout.setSpacing(15)
# App Title/Brand
self.brand = QLabel("WHISPER VOICE")
self.brand.setFont(load_modern_fonts())
self.brand.setStyleSheet(f"color: {Theme.ACCENT_CYAN}; font-weight: 900; letter-spacing: 4px; font-size: 14px;")
self.brand.setAlignment(Qt.AlignCenter)
self.layout.addWidget(self.brand)
# Status Label
self.status_label = QLabel("INITIALIZING...")
self.status_label.setStyleSheet(f"color: {Theme.TEXT_SECONDARY}; font-weight: 600; font-size: 11px;")
self.status_label.setAlignment(Qt.AlignCenter)
self.layout.addWidget(self.status_label)
# Progress Bar (Modern Slim style)
self.progress_bar = QProgressBar()
self.progress_bar.setFixedHeight(4)
self.progress_bar.setStyleSheet(f"""
QProgressBar {{
background-color: {Theme.BG_DARK};
border-radius: 2px;
border: none;
text-align: center;
color: transparent;
}}
QProgressBar::chunk {{
background-color: {Theme.ACCENT_CYAN};
border-radius: 2px;
}}
""")
self.layout.addWidget(self.progress_bar)
# Start Worker
self.worker = DownloadWorker()
self.worker.progress.connect(self.update_progress)
self.worker.download_finished.connect(self.on_finished)
self.worker.start()
def update_progress(self, text: str, percent: int):
self.status_label.setText(text.upper())
self.progress_bar.setValue(percent)
def on_finished(self):
self.ready_signal.emit()
self.close()
-105
View File
@@ -1,105 +0,0 @@
"""
Overlay Window Module.
======================
Premium High-Fidelity Overlay for Whisper Voice.
Features glassmorphism, pulsating status indicators, and smart positioning.
"""
from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel
from PySide6.QtCore import Qt, Slot, QPoint, QPropertyAnimation, QEasingCurve
from PySide6.QtGui import QColor, QFont, QGuiApplication
from src.ui.visualizer import AudioVisualizer
from src.ui.styles import Theme, StyleGenerator, load_modern_fonts
from src.ui.components import FramelessWindow, ModernFrame
class OverlayWindow(FramelessWindow):
"""
The main transparent overlay (The Pill).
Refactored for 2026 Premium Aesthetics.
"""
def __init__(self):
super().__init__()
self.setFixedSize(320, 95)
# Main Layout
self.master_layout = QVBoxLayout(self)
self.master_layout.setContentsMargins(10, 10, 10, 10)
# The Glass Pill Container
self.pill = ModernFrame()
self.pill.setStyleSheet(StyleGenerator.get_glass_card(radius=24))
self.master_layout.addWidget(self.pill)
# Layout inside the pill
self.layout = QHBoxLayout(self.pill)
self.layout.setContentsMargins(20, 10, 20, 10)
self.layout.setSpacing(15)
# Status Visualization (Left Dot)
self.status_dot = QWidget()
self.status_dot.setFixedSize(14, 14)
self.status_dot.setStyleSheet(f"background-color: {Theme.ACCENT_CYAN}; border-radius: 7px; border: 2px solid white;")
self.layout.addWidget(self.status_dot)
# Text/Visualizer Stack
self.content_stack = QVBoxLayout()
self.content_stack.setSpacing(2)
self.content_stack.setContentsMargins(0, 0, 0, 0)
self.status_label = QLabel("READY")
self.status_label.setFont(load_modern_fonts())
self.status_label.setStyleSheet(f"color: white; font-weight: 800; font-size: 11px; letter-spacing: 2px;")
self.content_stack.addWidget(self.status_label)
self.visualizer = AudioVisualizer()
self.visualizer.setFixedHeight(30)
self.content_stack.addWidget(self.visualizer)
self.layout.addLayout(self.content_stack)
# Animations
self.pulse_timer = None # Use style-based pulsing to avoid window flags issues
# Initial State
self.hide()
self.first_show = True
def showEvent(self, event):
"""Handle positioning and config updates."""
from src.core.config import ConfigManager
config = ConfigManager()
self.setWindowOpacity(config.get("opacity"))
if self.first_show:
self.center_above_taskbar()
self.first_show = False
super().showEvent(event)
def center_above_taskbar(self):
screen = QGuiApplication.primaryScreen()
if not screen: return
avail_rect = screen.availableGeometry()
x = avail_rect.x() + (avail_rect.width() - self.width()) // 2
y = avail_rect.bottom() - self.height() - 15
self.move(x, y)
@Slot(str)
def update_status(self, text: str):
"""Updates the status text and visual indicator."""
self.status_label.setText(text.upper())
if "RECORDING" in text.upper():
color = Theme.ACCENT_GREEN
elif "THINKING" in text.upper():
color = Theme.ACCENT_PURPLE
else:
color = Theme.ACCENT_CYAN
self.status_dot.setStyleSheet(f"background-color: {color}; border-radius: 7px; border: 2px solid white;")
@Slot(float)
def update_visualizer(self, amp: float):
self.visualizer.set_amplitude(amp)
+6 -3
View File
@@ -6,12 +6,15 @@ Button {
text: "Button"
property color accentColor: "#00f2ff"
Accessible.role: Accessible.Button
Accessible.name: control.text
activeFocusOnTab: true
contentItem: Text {
text: control.text
font.pixelSize: 13
font.bold: true
color: control.hovered ? "white" : "#9499b0"
color: control.hovered ? "white" : "#ABABAB"
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
@@ -25,8 +28,8 @@ Button {
opacity: control.down ? 0.7 : 1.0
color: control.hovered ? Qt.rgba(1, 1, 1, 0.1) : Qt.rgba(1, 1, 1, 0.05)
radius: 8
border.color: control.hovered ? control.accentColor : Qt.rgba(1, 1, 1, 0.1)
border.width: 1
border.color: control.hovered ? control.accentColor : SettingsStyle.borderSubtle
border.width: control.activeFocus ? SettingsStyle.focusRingWidth : 1
Behavior on border.color { ColorAnimation { duration: 200 } }
Behavior on color { ColorAnimation { duration: 200 } }
Binary file not shown.
+9 -3
View File
@@ -14,6 +14,8 @@ ApplicationWindow {
visible: true
flags: Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool
color: "transparent"
title: "WhisperVoice"
Accessible.name: "WhisperVoice Loading"
Rectangle {
id: bgRect
@@ -21,7 +23,7 @@ ApplicationWindow {
anchors.margins: 20 // Space for shadow
radius: 16
color: "#1a1a20"
border.color: "#40ffffff"
border.color: Qt.rgba(1, 1, 1, 0.22)
border.width: 1
// --- SHADOW & GLOW ---
@@ -55,6 +57,7 @@ ApplicationWindow {
// Pulse Animation
SequentialAnimation on scale {
running: ui ? !ui.reduceMotion : true
loops: Animation.Infinite
NumberAnimation { from: 1.0; to: 1.1; duration: 1000; easing.type: Easing.InOutSine }
NumberAnimation { from: 1.1; to: 1.0; duration: 1000; easing.type: Easing.InOutSine }
@@ -95,7 +98,7 @@ ApplicationWindow {
Text {
text: "AI TRANSCRIPTION ENGINE"
color: "#80ffffff"
color: "#ABABAB"
font.family: jetBrainsMono.name
font.pixelSize: 10
font.letterSpacing: 2
@@ -135,6 +138,7 @@ ApplicationWindow {
// Shimmer effect on bar
Rectangle {
width: 20; height: parent.height
visible: ui ? !ui.reduceMotion : true
color: "#80ffffff"
x: -width
opacity: 0.5
@@ -157,8 +161,10 @@ ApplicationWindow {
font.family: jetBrainsMono.name
font.pixelSize: 11
font.bold: true
Accessible.role: Accessible.AlertMessage
Accessible.name: "Loading status: " + text
anchors.horizontalCenter: parent.horizontalCenter
opacity: 0.8
opacity: 1.0
}
}
}
+7 -4
View File
@@ -10,6 +10,9 @@ ComboBox {
property color bgColor: "#1a1a20"
property color popupColor: "#252530"
Accessible.role: Accessible.ComboBox
Accessible.name: control.displayText
delegate: ItemDelegate {
id: delegate
width: control.width
@@ -68,7 +71,7 @@ ComboBox {
context.lineTo(width, 0);
context.lineTo(width / 2, height);
context.closePath();
context.fillStyle = control.pressed ? control.accentColor : "#888888";
context.fillStyle = control.pressed ? control.accentColor : "#ABABAB";
context.fill();
}
}
@@ -89,8 +92,8 @@ ComboBox {
implicitWidth: 140
implicitHeight: 40
color: control.bgColor
border.color: control.pressed || control.activeFocus ? control.accentColor : "#40ffffff"
border.width: 1
border.color: control.pressed || control.activeFocus ? control.accentColor : SettingsStyle.borderSubtle
border.width: control.activeFocus ? SettingsStyle.focusRingWidth : 1
radius: 6
// Glow effect on focus (Simplified to just border for stability)
@@ -114,7 +117,7 @@ ComboBox {
background: Rectangle {
color: control.popupColor
border.color: "#40ffffff"
border.color: SettingsStyle.borderSubtle
border.width: 1
radius: 6
}
+6 -3
View File
@@ -7,8 +7,11 @@ Rectangle {
implicitHeight: 32
color: "#1a1a20"
radius: 6
border.width: 1
border.color: activeFocus || recording ? SettingsStyle.accent : "#40ffffff"
activeFocusOnTab: true
Accessible.role: Accessible.Button
Accessible.name: control.currentSequence ? "Hotkey: " + control.currentSequence + ". Click to change" : "No hotkey set. Click to record"
border.width: (activeFocus || recording) ? SettingsStyle.focusRingWidth : 1
border.color: activeFocus || recording ? SettingsStyle.accent : SettingsStyle.borderSubtle
property string currentSequence: ""
signal sequenceChanged(string seq)
@@ -26,7 +29,7 @@ Rectangle {
Text {
anchors.centerIn: parent
text: control.recording ? "Listening..." : (formatSequence(control.currentSequence) || "None")
color: control.recording ? SettingsStyle.accent : (control.currentSequence ? "#ffffff" : "#808080")
color: control.recording ? SettingsStyle.accent : (control.currentSequence ? "#ffffff" : "#ABABAB")
font.family: "JetBrains Mono"
font.pixelSize: 13
font.bold: true
+2
View File
@@ -18,6 +18,8 @@ Rectangle {
property string description: ""
property alias control: controlContainer.data
property bool showSeparator: true
Accessible.name: root.label
Accessible.role: Accessible.Row
Behavior on color { ColorAnimation { duration: 150 } }
+2
View File
@@ -9,6 +9,8 @@ ColumnLayout {
default property alias content: contentColumn.data
property string title: ""
Accessible.name: root.title + " settings group"
Accessible.role: Accessible.Grouping
// Section Header
Text {
+38 -17
View File
@@ -5,43 +5,64 @@ import QtQuick.Effects
Slider {
id: control
Accessible.role: Accessible.Slider
Accessible.name: control.value.toString()
activeFocusOnTab: true
background: Rectangle {
x: control.leftPadding
y: control.topPadding + control.availableHeight / 2 - height / 2
implicitWidth: 200
implicitHeight: 4
implicitHeight: 6
width: control.availableWidth
height: implicitHeight
radius: 2
radius: 3
color: "#2d2d3d"
Rectangle {
width: control.visualPosition * parent.width
height: parent.height
color: SettingsStyle.accent
radius: 2
radius: 3
}
}
handle: Rectangle {
handle: Item {
x: control.leftPadding + control.visualPosition * (control.availableWidth - width)
y: control.topPadding + control.availableHeight / 2 - height / 2
implicitWidth: 18
implicitHeight: 18
radius: 9
color: "white"
border.color: SettingsStyle.accent
border.width: 2
implicitWidth: SettingsStyle.minTargetSize
implicitHeight: SettingsStyle.minTargetSize
layer.enabled: control.pressed
layer.effect: MultiEffect {
blurEnabled: true
blur: 0.5
shadowEnabled: true
shadowColor: SettingsStyle.accent
// Focus ring
Rectangle {
anchors.centerIn: parent
width: parent.width + SettingsStyle.focusRingWidth * 2 + 2
height: width
radius: width / 2
color: "transparent"
border.width: SettingsStyle.focusRingWidth
border.color: SettingsStyle.accent
visible: control.activeFocus
}
Rectangle {
anchors.fill: parent
radius: width / 2
color: "white"
border.color: SettingsStyle.accent
border.width: 2
layer.enabled: control.pressed
layer.effect: MultiEffect {
blurEnabled: true
blur: 0.5
shadowEnabled: true
shadowColor: SettingsStyle.accent
}
}
}
// Value Readout (Left side to avoid clipping on right edge)
// Value Readout
Text {
anchors.right: parent.left
anchors.rightMargin: 12
+16 -1
View File
@@ -4,6 +4,10 @@ import QtQuick.Controls
Switch {
id: control
Accessible.role: Accessible.CheckBox
Accessible.name: control.text + (control.checked ? " on" : " off")
activeFocusOnTab: true
indicator: Rectangle {
implicitWidth: 44
implicitHeight: 24
@@ -11,9 +15,11 @@ Switch {
y: parent.height / 2 - height / 2
radius: 12
color: control.checked ? SettingsStyle.accent : "#2d2d3d"
border.color: control.checked ? SettingsStyle.accent : "#3d3d4d"
border.color: control.checked ? SettingsStyle.accent : SettingsStyle.borderSubtle
border.width: control.activeFocus ? SettingsStyle.focusRingWidth : 1
Behavior on color { ColorAnimation { duration: 200 } }
Behavior on border.color { ColorAnimation { duration: 200 } }
Rectangle {
x: control.checked ? parent.width - width - 3 : 3
@@ -26,6 +32,15 @@ Switch {
Behavior on x {
NumberAnimation { duration: 200; easing.type: Easing.InOutQuad }
}
// I/O pip marks for non-color state indication
Text {
anchors.centerIn: parent
text: control.checked ? "I" : "O"
font.pixelSize: 9
font.bold: true
color: control.checked ? SettingsStyle.accent : "#666666"
}
}
}
+6 -3
View File
@@ -7,7 +7,10 @@ TextField {
property color accentColor: "#00f2ff"
property color bgColor: "#1a1a20"
placeholderTextColor: "#606060"
Accessible.role: Accessible.EditableText
Accessible.name: control.placeholderText || "Text input"
placeholderTextColor: SettingsStyle.textDisabled
color: "#ffffff"
font.family: "JetBrains Mono"
font.pixelSize: 14
@@ -18,8 +21,8 @@ TextField {
implicitWidth: 200
implicitHeight: 40
color: control.bgColor
border.color: control.activeFocus ? control.accentColor : "#40ffffff"
border.width: 1
border.color: control.activeFocus ? control.accentColor : SettingsStyle.borderSubtle
border.width: control.activeFocus ? SettingsStyle.focusRingWidth : 1
radius: 6
Behavior on border.color { ColorAnimation { duration: 150 } }
+30 -5
View File
@@ -13,6 +13,8 @@ ApplicationWindow {
visible: true
flags: Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool
color: "transparent"
title: "WhisperVoice"
Accessible.name: "WhisperVoice Overlay"
FontLoader {
id: jetBrainsMono
@@ -35,7 +37,7 @@ ApplicationWindow {
property bool isActive: ui.isRecording || ui.isProcessing
SequentialAnimation {
running: true
running: !ui.reduceMotion
loops: Animation.Infinite
PauseAnimation { duration: 3000 }
NumberAnimation {
@@ -96,6 +98,7 @@ ApplicationWindow {
ShaderEffect {
anchors.fill: parent
opacity: 0.4
visible: !ui.reduceMotion
property real time: 0
fragmentShader: "gradient_blobs.qsb"
NumberAnimation on time { from: 0; to: 1000; duration: 100000; loops: Animation.Infinite }
@@ -105,6 +108,7 @@ ApplicationWindow {
ShaderEffect {
anchors.fill: parent
opacity: 0.04
visible: !ui.reduceMotion
property real time: 0
property real intensity: ui.amplitude
fragmentShader: "glow.qsb"
@@ -115,6 +119,7 @@ ApplicationWindow {
ParticleSystem {
id: particles
anchors.fill: parent
running: !ui.reduceMotion
ItemParticle {
system: particles
delegate: Rectangle { width: 2; height: 2; radius: 1; color: "#10ffffff" }
@@ -143,6 +148,7 @@ ApplicationWindow {
// F. CRT Shader Effect (Overlay on chassis ONLY)
ShaderEffect {
anchors.fill: parent
visible: !ui.reduceMotion
property real time: 0
fragmentShader: "crt.qsb"
NumberAnimation on time { from: 0; to: 100; duration: 5000; loops: Animation.Infinite }
@@ -172,7 +178,7 @@ ApplicationWindow {
radius: height / 2
color: "transparent"
border.width: 1
border.color: "#40ffffff"
border.color: Qt.rgba(1, 1, 1, 0.22)
MouseArea {
anchors.fill: parent; hoverEnabled: true
@@ -194,7 +200,7 @@ ApplicationWindow {
NumberAnimation { duration: 150; easing.type: Easing.OutCubic }
}
SequentialAnimation on border.color {
running: ui.isRecording
running: ui.isRecording && !ui.reduceMotion
loops: Animation.Infinite
ColorAnimation { from: "#A0ff4b4b"; to: "#C0ff6b6b"; duration: 800 }
ColorAnimation { from: "#C0ff6b6b"; to: "#A0ff4b4b"; duration: 800 }
@@ -209,6 +215,11 @@ ApplicationWindow {
anchors.left: parent.left
anchors.leftMargin: 10
anchors.verticalCenter: parent.verticalCenter
activeFocusOnTab: true
Accessible.name: ui.isRecording ? "Stop recording" : "Start recording"
Accessible.role: Accessible.Button
Keys.onReturnPressed: ui.toggleRecordingRequested()
Keys.onSpacePressed: ui.toggleRecordingRequested()
// Make entire button scale with amplitude
scale: ui.isRecording ? (1.0 + ui.amplitude * 0.12) : 1.0
@@ -245,7 +256,7 @@ ApplicationWindow {
border.width: 2; border.color: "#60ffffff"
SequentialAnimation on scale {
running: ui.isRecording
running: ui.isRecording && !ui.reduceMotion
loops: Animation.Infinite
NumberAnimation { from: 1.0; to: 1.08; duration: 600; easing.type: Easing.InOutQuad }
NumberAnimation { from: 1.08; to: 1.0; duration: 600; easing.type: Easing.InOutQuad }
@@ -263,6 +274,17 @@ ApplicationWindow {
fillMode: Image.PreserveAspectFit
}
}
// Focus ring
Rectangle {
anchors.fill: micCircle
anchors.margins: -4
radius: width / 2
color: "transparent"
border.width: 2
border.color: "#B794F6" // SettingsStyle.accent equivalent
visible: micContainer.activeFocus
}
}
// --- RAINBOW WAVEFORM (Shader) ---
@@ -277,6 +299,7 @@ ApplicationWindow {
ShaderEffect {
anchors.fill: parent
visible: !ui.reduceMotion
property real time: 0
property real amplitude: ui.amplitude
fragmentShader: "rainbow_wave.qsb"
@@ -341,8 +364,10 @@ ApplicationWindow {
font.family: jetBrainsMono.name; font.pixelSize: 16; font.bold: true; font.letterSpacing: 2
style: Text.Outline
styleColor: ui.isRecording ? "#ff0000" : "#808085"
Accessible.role: Accessible.StaticText
Accessible.name: "Recording time: " + text
SequentialAnimation on opacity {
running: ui.isRecording; loops: Animation.Infinite
running: ui.isRecording && !ui.reduceMotion; loops: Animation.Infinite
NumberAnimation { from: 1.0; to: 0.7; duration: 800 }
NumberAnimation { from: 0.7; to: 1.0; duration: 800 }
}
+65 -10
View File
@@ -12,7 +12,8 @@ Window {
visible: false
flags: Qt.FramelessWindowHint | Qt.Window
color: "transparent"
title: "Settings"
title: "WhisperVoice Settings"
Accessible.name: "WhisperVoice Settings"
// Explicit sizing for Python to read
@@ -133,15 +134,20 @@ Window {
// Improved Close Button
Rectangle {
width: 32; height: 32
activeFocusOnTab: true
Accessible.name: "Close settings"
Accessible.role: Accessible.Button
Keys.onReturnPressed: root.close()
Keys.onSpacePressed: root.close()
radius: 8
color: closeMa.containsMouse ? "#20ff4b4b" : "transparent"
border.color: closeMa.containsMouse ? "#40ff4b4b" : "transparent"
color: closeMa.containsMouse ? "#20FF8A8A" : "transparent"
border.color: closeMa.containsMouse ? "#40FF8A8A" : "transparent"
border.width: 1
Text {
anchors.centerIn: parent
text: "×"
color: closeMa.containsMouse ? "#ff4b4b" : SettingsStyle.textSecondary
color: closeMa.containsMouse ? "#FF8A8A" : SettingsStyle.textSecondary
font.family: mainFont
font.pixelSize: 20
font.bold: true
@@ -157,6 +163,15 @@ Window {
Behavior on color { ColorAnimation { duration: 150 } }
Behavior on border.color { ColorAnimation { duration: 150 } }
// Focus ring
Rectangle {
anchors.fill: parent
radius: 8
color: "transparent"
border.width: SettingsStyle.focusRingWidth
border.color: SettingsStyle.accent
visible: parent.activeFocus
}
}
}
@@ -206,6 +221,23 @@ Window {
height: 38
color: stack.currentIndex === index ? SettingsStyle.surfaceHover : (ma.containsMouse ? Qt.rgba(1,1,1,0.03) : "transparent")
radius: 6
activeFocusOnTab: true
Accessible.name: name
Accessible.role: Accessible.Tab
Keys.onReturnPressed: stack.currentIndex = index
Keys.onSpacePressed: stack.currentIndex = index
Keys.onDownPressed: {
if (index < navModel.count - 1) {
var nextItem = navBtnRoot.parent.children[index + 2]
if (nextItem && nextItem.forceActiveFocus) nextItem.forceActiveFocus()
}
}
Keys.onUpPressed: {
if (index > 0) {
var prevItem = navBtnRoot.parent.children[index]
if (prevItem && prevItem.forceActiveFocus) prevItem.forceActiveFocus()
}
}
Behavior on color { ColorAnimation { duration: 150 } }
@@ -256,6 +288,15 @@ Window {
cursorShape: Qt.PointingHandCursor
onClicked: stack.currentIndex = index
}
// Focus ring
Rectangle {
anchors.fill: parent
radius: 6
color: "transparent"
border.width: SettingsStyle.focusRingWidth
border.color: SettingsStyle.accent
visible: parent.activeFocus
}
}
}
@@ -286,6 +327,7 @@ Window {
// --- TAB: GENERAL ---
ScrollView {
Accessible.role: Accessible.PageTab
ScrollBar.vertical.policy: ScrollBar.AsNeeded
contentWidth: availableWidth
@@ -383,6 +425,7 @@ Window {
// --- TAB: AUDIO ---
ScrollView {
Accessible.role: Accessible.PageTab
ScrollBar.vertical.policy: ScrollBar.AsNeeded
contentWidth: availableWidth
@@ -471,6 +514,7 @@ Window {
// --- TAB: VISUALS ---
ScrollView {
Accessible.role: Accessible.PageTab
ScrollBar.vertical.policy: ScrollBar.AsNeeded
contentWidth: availableWidth
@@ -520,7 +564,7 @@ Window {
ModernSettingsItem {
label: "Window Opacity"
description: "Transparency level"
showSeparator: false
showSeparator: true
control: ModernSlider {
Layout.preferredWidth: 200
from: 0.1; to: 1.0
@@ -528,6 +572,15 @@ Window {
onMoved: ui.setSetting("opacity", Number(value.toFixed(2)))
}
}
ModernSettingsItem {
label: "Reduce Motion"
description: "Disable animations for accessibility"
showSeparator: false
control: ModernSwitch {
checked: ui.getSetting("reduce_motion")
onToggled: ui.setSetting("reduce_motion", checked)
}
}
}
}
@@ -580,6 +633,7 @@ Window {
// --- TAB: AI ENGINE ---
ScrollView {
Accessible.role: Accessible.PageTab
ScrollBar.vertical.policy: ScrollBar.AsNeeded
contentWidth: availableWidth
@@ -752,8 +806,8 @@ Window {
}
color: "#ffffff"
font.family: "JetBrains Mono"
font.pixelSize: 10
opacity: 0.7
font.pixelSize: 11
opacity: 1.0
elide: Text.ElideRight
Layout.fillWidth: true
}
@@ -1040,6 +1094,7 @@ Window {
// --- TAB: DEBUG ---
ScrollView {
Accessible.role: Accessible.PageTab
ScrollBar.vertical.policy: ScrollBar.AsNeeded
contentWidth: availableWidth
@@ -1065,9 +1120,9 @@ Window {
spacing: 16
StatBox { label: "APP CPU"; value: ui.appCpu; unit: "%"; accent: "#00f2ff" }
StatBox { label: "APP RAM"; value: ui.appRamMb; unit: "MB"; accent: "#bd93f9" }
StatBox { label: "GPU VRAM"; value: ui.appVramMb; unit: "MB"; accent: "#ff79c6" }
StatBox { label: "GPU LOAD"; value: ui.appVramPercent; unit: "%"; accent: "#ff5555" }
StatBox { label: "APP RAM"; value: ui.appRamMb; unit: "MB"; accent: "#CAA9FF" }
StatBox { label: "GPU VRAM"; value: ui.appVramMb; unit: "MB"; accent: "#FF8FD0" }
StatBox { label: "GPU LOAD"; value: ui.appVramPercent; unit: "%"; accent: "#FF8A8A" }
}
Rectangle {
+11 -6
View File
@@ -6,13 +6,14 @@ QtObject {
// Colors
readonly property color background: "#F2121212" // Deep Obsidian with 95% opacity
readonly property color surfaceCard: "#1A1A1A" // Layer 1
readonly property color surfaceHover: "#2A2A2A" // Layer 2 (Lighter for better contrast)
readonly property color borderSubtle: Qt.rgba(1, 1, 1, 0.08)
readonly property color surfaceHover: "#2A2A2A" // Layer 2
readonly property color borderSubtle: Qt.rgba(1, 1, 1, 0.22) // WCAG 3:1 non-text contrast
readonly property color textPrimary: "#FAFAFA" // Brighter white
readonly property color textSecondary: "#999999"
readonly property color textPrimary: "#FAFAFA"
readonly property color textSecondary: "#ABABAB" // WCAG AAA 8.1:1 on #121212
readonly property color textDisabled: "#808080" // 4.0:1 minimum for disabled states
readonly property color accentPurple: "#7000FF"
readonly property color accentPurple: "#B794F6" // WCAG AAA 7.2:1 on #121212
readonly property color accentCyan: "#00F2FF"
// Configurable active accent
@@ -21,5 +22,9 @@ QtObject {
// Dimensions
readonly property int cardRadius: 16
readonly property int itemRadius: 8
readonly property int itemHeight: 60 // Even taller for more breathing room
readonly property int itemHeight: 60
// Accessibility
readonly property int focusRingWidth: 2
readonly property int minTargetSize: 24
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-50
View File
@@ -1,50 +0,0 @@
#version 440
layout(location = 0) in vec2 qt_TexCoord0;
layout(location = 0) out vec4 fragColor;
layout(std140, binding = 0) uniform buf {
mat4 qt_Matrix;
float qt_Opacity;
float time;
float aberration; // 0.0 to 1.0, controlled by Audio Amplitude
};
float rand(vec2 co) {
return fract(sin(dot(co.xy ,vec2(12.9898,78.233))) * 43758.5453);
}
void main() {
// 1. Calculate Distortion Offset based on Amplitude (aberration)
// We warp the UVs slightly away from center
vec2 uv = qt_TexCoord0;
vec2 dist = uv - 0.5;
// 2. Chromatic Aberration
// Red Channel shifts OUT
// Blue Channel shifts IN
float strength = aberration * 0.02; // Max shift 2% of texture size
vec2 rUV = uv + (dist * strength);
vec2 bUV = uv - (dist * strength);
// Sample texture? We don't have a texture input (source is empty Item), we are generating visuals.
// Wait, ShaderEffect usually works on sourceItem.
// Here we are generating NOISE on top of a gradient.
// So we apply Aberration to the NOISE function?
// Or do we want to aberrate the pixels UNDERNEATH?
// ShaderEffect with no source property renders purely procedural content.
// Let's create layered procedural noise with channel offsets
float nR = rand(rUV + vec2(time * 0.01, 0.0));
float nG = rand(uv + vec2(time * 0.01, 0.0)); // Green is anchor
float nB = rand(bUV + vec2(time * 0.01, 0.0));
// Also modulate alpha by aberration - higher volume = more intense grain?
// Or maybe just pure glitch.
vec4 grainColor = vec4(nR, nG, nB, 1.0);
// Mix it with opacity
fragColor = grainColor * qt_Opacity;
}
Binary file not shown.
-25
View File
@@ -1,25 +0,0 @@
#version 440
layout(location = 0) in vec2 qt_TexCoord0;
layout(location = 0) out vec4 fragColor;
layout(std140, binding = 0) uniform buf {
mat4 qt_Matrix;
float qt_Opacity;
float time;
};
// High-quality pseudo-random function
float rand(vec2 co) {
return fract(sin(dot(co.xy ,vec2(12.9898,78.233))) * 43758.5453);
}
void main() {
// Dynamic Noise based on Time
// We add 'time' to the coordinate to animate the grain
float noise = rand(qt_TexCoord0 + vec2(time * 0.01, time * 0.02));
// Output grayscale noise with alpha modulation
// We want white noise, applied with qt_Opacity
fragColor = vec4(noise, noise, noise, 1.0) * qt_Opacity;
}
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 492 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 490 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 464 KiB

-236
View File
@@ -1,236 +0,0 @@
"""
Settings Window Module.
=======================
Manages the application configuration UI.
Refactored for 2026 Premium Aesthetics with Sidebar navigation.
"""
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QStackedWidget,
QLabel, QComboBox, QFormLayout, QFrame, QMessageBox, QScrollArea
)
from PySide6.QtCore import Qt, Signal, Slot, QSize
from PySide6.QtGui import QFont, QIcon
from src.core.config import ConfigManager
from src.ui.styles import Theme, StyleGenerator, load_modern_fonts
from src.ui.components import FramelessWindow, ModernFrame, GlassButton, ModernSwitch, ModernSlider
import sounddevice as sd
class SettingsWindow(FramelessWindow):
"""
The main settings dialog.
Refactored with 2026 Premium Sidebar Layout.
"""
settings_changed = Signal()
def __init__(self, parent=None):
super().__init__(parent)
self.config = ConfigManager()
self.setFixedSize(700, 500)
# Main Container
self.bg_frame = ModernFrame()
self.bg_frame.setStyleSheet(StyleGenerator.get_glass_card(radius=20))
self.root_layout = QVBoxLayout(self)
self.root_layout.setContentsMargins(10, 10, 10, 10)
self.root_layout.addWidget(self.bg_frame)
# Title Bar Area (Inside glass card)
self.title_layout = QHBoxLayout()
self.title_layout.setContentsMargins(20, 15, 20, 0)
title_lbl = QLabel("PREMIUM SETTINGS")
title_lbl.setFont(load_modern_fonts())
title_lbl.setStyleSheet(f"color: white; font-weight: 900; font-size: 14px; letter-spacing: 2px;")
self.title_layout.addWidget(title_lbl)
self.title_layout.addStretch()
self.btn_close = GlassButton("×", accent_color="#ff4b4b")
self.btn_close.setFixedSize(30, 30)
self.btn_close.clicked.connect(self.close)
self.title_layout.addWidget(self.btn_close)
# Central Layout (Sidebar + Content)
self.content_layout = QHBoxLayout()
self.content_layout.setContentsMargins(10, 10, 10, 10)
self.content_layout.setSpacing(10)
# 1. SIDEBAR
self.sidebar = QWidget()
self.sidebar.setFixedWidth(160)
self.sidebar_layout = QVBoxLayout(self.sidebar)
self.sidebar_layout.setContentsMargins(0, 10, 0, 10)
self.sidebar_layout.setSpacing(8)
self.nav_general = GlassButton("General")
self.nav_audio = GlassButton("Audio")
self.nav_visuals = GlassButton("Visuals")
self.nav_advanced = GlassButton("Advanced/AI")
self.sidebar_layout.addWidget(self.nav_general)
self.sidebar_layout.addWidget(self.nav_audio)
self.sidebar_layout.addWidget(self.nav_visuals)
self.sidebar_layout.addWidget(self.nav_advanced)
self.sidebar_layout.addStretch()
self.btn_save = GlassButton("SAVE CHANGES", accent_color=Theme.ACCENT_GREEN)
self.btn_save.clicked.connect(self.save_settings)
self.sidebar_layout.addWidget(self.btn_save)
# 2. CONTENT STACK
self.stack = QStackedWidget()
self.stack.setStyleSheet("background: transparent;")
# Connect sidebar to stack
self.nav_general.clicked.connect(lambda: self.stack.setCurrentIndex(0))
self.nav_audio.clicked.connect(lambda: self.stack.setCurrentIndex(1))
self.nav_visuals.clicked.connect(lambda: self.stack.setCurrentIndex(2))
self.nav_advanced.clicked.connect(lambda: self.stack.setCurrentIndex(3))
# Main Layout Assembly
self.inner_layout = QVBoxLayout(self.bg_frame)
self.inner_layout.addLayout(self.title_layout)
self.inner_layout.addLayout(self.content_layout)
self.content_layout.addWidget(self.sidebar)
self.content_layout.addWidget(self.stack)
self.setup_pages()
self.load_values()
def setup_pages(self):
"""Creates the settings pages."""
# --- GENERAL ---
self.page_general = QWidget()
l1 = QFormLayout(self.page_general)
l1.setVerticalSpacing(20)
self.inp_hotkey = QComboBox()
self.inp_hotkey.addItems(["f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", "f10", "f11", "f12", "caps lock"])
self.inp_hotkey.setStyleSheet(f"background: {Theme.BG_DARK}; border-radius: 4px; padding: 5px; color: white;")
l1.addRow(self.create_lbl("Global Hotkey:"), self.inp_hotkey)
self.chk_top = ModernSwitch()
l1.addRow(self.create_lbl("Always on Top:"), self.chk_top)
self.stack.addWidget(self.page_general)
# --- AUDIO ---
self.page_audio = QWidget()
l2 = QFormLayout(self.page_audio)
l2.setVerticalSpacing(15)
self.inp_device = QComboBox()
self.inp_device.setStyleSheet(f"background: {Theme.BG_DARK}; border-radius: 4px; padding: 5px; color: white;")
self.populate_audio_devices()
l2.addRow(self.create_lbl("Input Device:"), self.inp_device)
self.sld_threshold = ModernSlider(Qt.Horizontal)
self.sld_threshold.setRange(1, 25)
self.lbl_threshold = self.create_lbl("2%")
self.sld_threshold.valueChanged.connect(lambda v: self.lbl_threshold.setText(f"{v}%"))
l2.addRow(self.create_lbl("Noise Gate:"), self.sld_threshold)
l2.addRow("", self.lbl_threshold)
self.sld_duration = ModernSlider(Qt.Horizontal)
self.sld_duration.setRange(5, 50)
self.lbl_duration = self.create_lbl("1.0s")
self.sld_duration.valueChanged.connect(lambda v: self.lbl_duration.setText(f"{v/10}s"))
l2.addRow(self.create_lbl("Auto-Submit:"), self.sld_duration)
l2.addRow("", self.lbl_duration)
self.stack.addWidget(self.page_audio)
# --- VISUALS ---
self.page_visuals = QWidget()
l3 = QFormLayout(self.page_visuals)
l3.setVerticalSpacing(20)
self.inp_style = QComboBox()
self.inp_style.addItem("Neon Line (Recommended)", "line")
self.inp_style.addItem("Classic Bars", "bar")
self.inp_style.setStyleSheet(f"background: {Theme.BG_DARK}; border-radius: 4px; padding: 5px; color: white;")
l3.addRow(self.create_lbl("Visualizer:"), self.inp_style)
self.sld_opacity = ModernSlider(Qt.Horizontal)
self.sld_opacity.setRange(40, 100)
self.lbl_opacity = self.create_lbl("100%")
self.sld_opacity.valueChanged.connect(lambda v: self.lbl_opacity.setText(f"{v}%"))
l3.addRow(self.create_lbl("Opacity:"), self.sld_opacity)
l3.addRow("", self.lbl_opacity)
self.stack.addWidget(self.page_visuals)
# --- ADVANCED ---
self.page_adv = QWidget()
l4 = QFormLayout(self.page_adv)
l4.setVerticalSpacing(15)
self.inp_model = QComboBox()
self.inp_model.setStyleSheet(f"background: {Theme.BG_DARK}; border-radius: 4px; padding: 5px; color: white;")
for id, name in [("tiny", "Tiny (Fast)"), ("base", "Base"), ("small", "Small (Default)"), ("medium", "Medium"), ("large-v3", "Large V3")]:
self.inp_model.addItem(name, id)
l4.addRow(self.create_lbl("Model:"), self.inp_model)
info = QLabel("Large models provide higher accuracy but require significant RAM/VRAM.")
info.setWordWrap(True)
info.setStyleSheet(f"color: {Theme.TEXT_SECONDARY}; font-style: italic; font-size: 11px;")
l4.addRow("", info)
self.stack.addWidget(self.page_adv)
def create_lbl(self, text):
lbl = QLabel(text)
lbl.setStyleSheet(f"color: {Theme.TEXT_SECONDARY}; font-weight: 600; font-size: 13px;")
return lbl
def populate_audio_devices(self):
try:
self.inp_device.addItem("System Default", -1)
for i, dev in enumerate(sd.query_devices()):
if dev['max_input_channels'] > 0:
self.inp_device.addItem(dev['name'], i)
except: pass
def load_values(self):
self.inp_hotkey.setCurrentText(self.config.get("hotkey"))
self.chk_top.setChecked(self.config.get("always_on_top"))
dev_id = self.config.get("input_device")
idx = self.inp_device.findData(dev_id if dev_id is not None else -1)
if idx >= 0: self.inp_device.setCurrentIndex(idx)
self.sld_threshold.setValue(int(self.config.get("silence_threshold") * 100))
self.sld_duration.setValue(int(self.config.get("silence_duration") * 10))
idx = self.inp_style.findData(self.config.get("visualizer_style"))
if idx >= 0: self.inp_style.setCurrentIndex(idx)
self.sld_opacity.setValue(int(self.config.get("opacity") * 100))
idx = self.inp_model.findData(self.config.get("model_size"))
if idx >= 0: self.inp_model.setCurrentIndex(idx)
def save_settings(self):
updates = {
"hotkey": self.inp_hotkey.currentText(),
"always_on_top": self.chk_top.isChecked(),
"input_device": self.inp_device.currentData() if self.inp_device.currentData() != -1 else None,
"silence_threshold": self.sld_threshold.value() / 100.0,
"silence_duration": self.sld_duration.value() / 10.0,
"visualizer_style": self.inp_style.currentData(),
"opacity": self.sld_opacity.value() / 100.0,
"model_size": self.inp_model.currentData()
}
new_model = updates["model_size"]
if new_model != self.config.get("model_size"):
QMessageBox.information(self, "Model Updated", f"Downloaded {new_model} on next launch.")
self.config.set_bulk(updates)
self.settings_changed.emit()
self.close()
-62
View File
@@ -1,62 +0,0 @@
"""
Style Engine Module.
====================
Centralized design system for the 2026 Premium UI.
Defines color palettes, glassmorphism templates, and modern font loading.
"""
from PySide6.QtGui import QColor, QFont, QFontDatabase
import os
class Theme:
"""Premium Dark Theme Palette (2026 Edition)."""
# Backgrounds
BG_DARK = "#0d0d12" # Deep cosmic black
BG_CARD = "#16161e" # Slightly lighter for components
BG_GLASS = "rgba(22, 22, 30, 0.7)" # Semi-transparent for glass effect
# Neons & Accents
ACCENT_CYAN = "#00f2ff" # Electric cyan
ACCENT_PURPLE = "#7000ff" # Deep cyber purple
ACCENT_GREEN = "#00ff88" # Mint neon
# Text
TEXT_PRIMARY = "#ffffff" # Pure white
TEXT_SECONDARY = "#9499b0" # Muted blue-gray
TEXT_MUTED = "#565f89" # Darker blue-gray
# Borders
BORDER_SUBTLE = "rgba(100, 100, 150, 0.2)"
BORDER_GLOW = "rgba(0, 242, 255, 0.5)"
class StyleGenerator:
"""Generates QSS strings for complex effects."""
@staticmethod
def get_glass_card(radius=12, border=True):
"""Returns QSS for a glassmorphism card."""
border_css = f"border: 1px solid {Theme.BORDER_SUBTLE};" if border else "border: none;"
return f"""
background-color: {Theme.BG_GLASS};
border-radius: {radius}px;
{border_css}
"""
@staticmethod
def get_glow_border(color=Theme.ACCENT_CYAN):
"""Returns QSS for a glowing border state."""
return f"border: 1px solid {color};"
def load_modern_fonts():
"""Attempts to load a modern font stack for the 2026 look."""
# Preferred order: Segoe UI Variable, Inter, Segoe UI, sans-serif
families = ["Segoe UI Variable Text", "Inter", "Segoe UI", "sans-serif"]
for family in families:
font = QFont(family, 10)
if QFontDatabase.families().count(family) > 0:
return font
# Absolute fallback
return QFont("Arial", 10)
-117
View File
@@ -1,117 +0,0 @@
"""
Audio Visualizer Module.
========================
High-Fidelity rendering for the 2026 Premium UI.
Supports 'Classic Bars' and 'Neon Line' with smooth curves and glows.
"""
from PySide6.QtWidgets import QWidget
from PySide6.QtCore import Qt, QTimer, Slot, QRectF, QPointF
from PySide6.QtGui import QPainter, QBrush, QColor, QPainterPath, QPen, QLinearGradient
import random
from src.ui.styles import Theme
class AudioVisualizer(QWidget):
"""
A premium audio visualizer with smooth physics and neon aesthetics.
"""
def __init__(self, parent=None):
super().__init__(parent)
self.amplitude = 0.0
self.bars = 12
self.history = [0.0] * self.bars
# High-refresh timer for silky smooth motion
self.timer = QTimer(self)
self.timer.timeout.connect(self.update_animation)
self.timer.start(16) # ~60 FPS
@Slot(float)
def set_amplitude(self, amp: float):
self.amplitude = amp
def update_animation(self):
self.history.pop(0)
# Smooth interpolation + noise
jitter = random.uniform(0.01, 0.03)
# Decay logic: Gravity-like pull
self.history.append(max(self.amplitude, jitter))
self.update()
def paintEvent(self, event):
from src.core.config import ConfigManager
style = ConfigManager().get("visualizer_style")
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
w, h = self.width(), self.height()
painter.translate(0, h / 2)
if style == "bar":
self._draw_bars(painter, w, h)
else:
self._draw_line(painter, w, h)
def _draw_bars(self, painter, w, h):
bar_w = w / self.bars
spacing = 3
for i, val in enumerate(self.history):
bar_h = val * (h * 0.9)
x = i * bar_w
# Gradient Bar
grad = QLinearGradient(0, -bar_h/2, 0, bar_h/2)
grad.setColorAt(0, QColor(Theme.ACCENT_PURPLE))
grad.setColorAt(1, QColor(Theme.ACCENT_CYAN))
painter.setBrush(grad)
painter.setPen(Qt.NoPen)
painter.drawRoundedRect(QRectF(x + spacing, -bar_h/2, bar_w - spacing*2, bar_h), 3, 3)
def _draw_line(self, painter, w, h):
path = QPainterPath()
points = len(self.history)
dx = w / (points - 1)
path.moveTo(0, 0)
def get_path(multi):
p = QPainterPath()
p.moveTo(0, 0)
for i in range(points):
curr_x = i * dx
curr_y = -self.history[i] * (h * 0.45) * multi
if i == 0:
p.moveTo(curr_x, curr_y)
else:
prev_x = (i-1) * dx
# Simple lerp or quadTo for smoothness
p.lineTo(curr_x, curr_y)
return p
# Draw Top & Bottom
p_top = get_path(1)
p_bot = get_path(-1)
# Glow layer
glow_pen = QPen(QColor(Theme.ACCENT_CYAN))
glow_pen.setWidth(4)
glow_alpha = QColor(Theme.ACCENT_CYAN)
glow_alpha.setAlpha(60)
glow_pen.setColor(glow_alpha)
painter.setPen(glow_pen)
painter.drawPath(p_top)
painter.drawPath(p_bot)
# Core layer
core_pen = QPen(Qt.white)
core_pen.setWidth(2)
painter.setPen(core_pen)
painter.drawPath(p_top)
painter.drawPath(p_bot)
-38
View File
@@ -1,38 +0,0 @@
import sys
from transformers import M2M100ForConditionalGeneration, M2M100Tokenizer
def test_m2m():
model_name = "facebook/m2m100_418M"
print(f"Loading {model_name}...")
tokenizer = M2M100Tokenizer.from_pretrained(model_name)
model = M2M100ForConditionalGeneration.from_pretrained(model_name)
# Test cases: (Language Code, Input)
test_cases = [
("en", "he go to school yesterday"),
("pl", "on iść do szkoła wczoraj"), # Intentional broken grammar in Polish
]
print("\nStarting M2M Tests (Self-Translation):\n")
for lang, input_text in test_cases:
tokenizer.src_lang = lang
encoded = tokenizer(input_text, return_tensors="pt")
# Translate to SAME language
generated_tokens = model.generate(
**encoded,
forced_bos_token_id=tokenizer.get_lang_id(lang)
)
corrected = tokenizer.batch_decode(generated_tokens, skip_special_tokens=True)[0]
print(f"[{lang}]")
print(f"Input: {input_text}")
print(f"Output: {corrected}")
print("-" * 20)
if __name__ == "__main__":
test_m2m()
-40
View File
@@ -1,40 +0,0 @@
import sys
from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
def test_mt0():
model_name = "bigscience/mt0-base"
print(f"Loading {model_name}...")
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForSeq2SeqLM.from_pretrained(model_name)
# Test cases: (Language, Prompt, Input)
# MT0 is instruction tuned, so we should prompt it in the target language or English.
# Cross-lingual prompting (English prompt -> Target tasks) is usually supported.
test_cases = [
("English", "Correct grammar:", "he go to school yesterday"),
("Polish", "Popraw gramatykę:", "to jest testowe zdanie bez kropki"),
("Finnish", "Korjaa kielioppi:", "tämä on testilause ilman pistettä"),
("Russian", "Исправь грамматику:", "это тестовое предложение без точки"),
("Japanese", "文法を直してください:", "これは点のないテスト文です"),
("Spanish", "Corrige la gramática:", "esta es una oración de prueba sin punto"),
]
print("\nStarting MT0 Tests:\n")
for lang, prompt_text, input_text in test_cases:
full_input = f"{prompt_text} {input_text}"
inputs = tokenizer(full_input, return_tensors="pt")
outputs = model.generate(inputs.input_ids, max_length=128)
corrected = tokenizer.decode(outputs[0], skip_special_tokens=True)
print(f"[{lang}]")
print(f"Input: {full_input}")
print(f"Output: {corrected}")
print("-" * 20)
if __name__ == "__main__":
test_mt0()
-34
View File
@@ -1,34 +0,0 @@
import sys
import os
# Add src to path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from src.core.grammar_assistant import GrammarAssistant
def test_punctuation():
assistant = GrammarAssistant()
assistant.load_model()
samples = [
# User's example (verbatim)
"If the voice recognition doesn't recognize that I like stopped Or something would that would it also correct that",
# Generic run-on
"hello how are you doing today i am doing fine thanks for asking",
# Missing commas/periods
"well i think its valid however we should probably check the logs first"
]
print("\nStarting Punctuation Tests:\n")
for sample in samples:
print(f"Original: {sample}")
corrected = assistant.correct(sample)
print(f"Corrected: {corrected}")
print("-" * 20)
if __name__ == "__main__":
test_punctuation()