Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
baa5e2e69e | ||
|
|
3137770742 |
25
.gitignore
vendored
Normal file
25
.gitignore
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
# 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
|
||||
48
README.md
48
README.md
@@ -141,54 +141,6 @@ 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
|
||||
|
||||
BIN
app_icon.ico
Normal file
BIN
app_icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 73 KiB |
31
build.bat
31
build.bat
@@ -1,31 +0,0 @@
|
||||
@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
|
||||
95
build.spec
95
build.spec
@@ -1,95 +0,0 @@
|
||||
# -*- 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 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',
|
||||
'tkinter', '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',
|
||||
)
|
||||
66
build_bootstrapper.py
Normal file
66
build_bootstrapper.py
Normal file
@@ -0,0 +1,66 @@
|
||||
"""
|
||||
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
build_exe.bat
Normal file
17
build_exe.bat
Normal file
@@ -0,0 +1,17 @@
|
||||
@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
convert_icon.py
Normal file
14
convert_icon.py
Normal file
@@ -0,0 +1,14 @@
|
||||
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
download_icons.py
Normal file
43
download_icons.py
Normal file
@@ -0,0 +1,43 @@
|
||||
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()
|
||||
15
main.py
15
main.py
@@ -80,21 +80,6 @@ try:
|
||||
except:
|
||||
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):
|
||||
sig_log = Signal(str)
|
||||
|
||||
86
portable_build.py
Normal file
86
portable_build.py
Normal file
@@ -0,0 +1,86 @@
|
||||
"""
|
||||
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
publish_release.py
Normal file
73
publish_release.py
Normal file
@@ -0,0 +1,73 @@
|
||||
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
run.bat
Normal file
5
run.bat
Normal file
@@ -0,0 +1,5 @@
|
||||
@echo off
|
||||
echo [LAUNCHER] Starting Fake Blur UI (Python/Qt)...
|
||||
call venv\Scripts\activate.bat
|
||||
python main.py
|
||||
if %errorlevel% neq 0 pause
|
||||
@@ -58,10 +58,7 @@ DEFAULT_SETTINGS = {
|
||||
|
||||
|
||||
# Low VRAM Mode
|
||||
"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
|
||||
"unload_models_after_use": False # If True, models are unloaded immediately to free VRAM
|
||||
}
|
||||
|
||||
class ConfigManager:
|
||||
|
||||
31
src/core/debug_run_worker.bat
Normal file
31
src/core/debug_run_worker.bat
Normal file
@@ -0,0 +1,31 @@
|
||||
@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
|
||||
)
|
||||
@@ -111,7 +111,6 @@ 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)
|
||||
@@ -131,7 +130,6 @@ 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()
|
||||
@@ -279,8 +277,6 @@ 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)
|
||||
@@ -292,15 +288,6 @@ 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
src/ui/components.py
Normal file
210
src/ui/components.py
Normal file
@@ -0,0 +1,210 @@
|
||||
"""
|
||||
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
src/ui/loader.py
Normal file
109
src/ui/loader.py
Normal file
@@ -0,0 +1,109 @@
|
||||
"""
|
||||
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
src/ui/overlay.py
Normal file
105
src/ui/overlay.py
Normal file
@@ -0,0 +1,105 @@
|
||||
"""
|
||||
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,15 +6,12 @@ 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" : "#ABABAB"
|
||||
color: control.hovered ? "white" : "#9499b0"
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
elide: Text.ElideRight
|
||||
@@ -28,8 +25,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 : SettingsStyle.borderSubtle
|
||||
border.width: control.activeFocus ? SettingsStyle.focusRingWidth : 1
|
||||
border.color: control.hovered ? control.accentColor : Qt.rgba(1, 1, 1, 0.1)
|
||||
border.width: 1
|
||||
|
||||
Behavior on border.color { ColorAnimation { duration: 200 } }
|
||||
Behavior on color { ColorAnimation { duration: 200 } }
|
||||
|
||||
BIN
src/ui/qml/JetBrainsMono.zip
Normal file
BIN
src/ui/qml/JetBrainsMono.zip
Normal file
Binary file not shown.
@@ -14,8 +14,6 @@ ApplicationWindow {
|
||||
visible: true
|
||||
flags: Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool
|
||||
color: "transparent"
|
||||
title: "WhisperVoice"
|
||||
Accessible.name: "WhisperVoice Loading"
|
||||
|
||||
Rectangle {
|
||||
id: bgRect
|
||||
@@ -23,7 +21,7 @@ ApplicationWindow {
|
||||
anchors.margins: 20 // Space for shadow
|
||||
radius: 16
|
||||
color: "#1a1a20"
|
||||
border.color: Qt.rgba(1, 1, 1, 0.22)
|
||||
border.color: "#40ffffff"
|
||||
border.width: 1
|
||||
|
||||
// --- SHADOW & GLOW ---
|
||||
@@ -57,7 +55,6 @@ 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 }
|
||||
@@ -98,7 +95,7 @@ ApplicationWindow {
|
||||
|
||||
Text {
|
||||
text: "AI TRANSCRIPTION ENGINE"
|
||||
color: "#ABABAB"
|
||||
color: "#80ffffff"
|
||||
font.family: jetBrainsMono.name
|
||||
font.pixelSize: 10
|
||||
font.letterSpacing: 2
|
||||
@@ -138,7 +135,6 @@ ApplicationWindow {
|
||||
// Shimmer effect on bar
|
||||
Rectangle {
|
||||
width: 20; height: parent.height
|
||||
visible: ui ? !ui.reduceMotion : true
|
||||
color: "#80ffffff"
|
||||
x: -width
|
||||
opacity: 0.5
|
||||
@@ -161,10 +157,8 @@ 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: 1.0
|
||||
opacity: 0.8
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,9 +10,6 @@ ComboBox {
|
||||
property color bgColor: "#1a1a20"
|
||||
property color popupColor: "#252530"
|
||||
|
||||
Accessible.role: Accessible.ComboBox
|
||||
Accessible.name: control.displayText
|
||||
|
||||
delegate: ItemDelegate {
|
||||
id: delegate
|
||||
width: control.width
|
||||
@@ -71,7 +68,7 @@ ComboBox {
|
||||
context.lineTo(width, 0);
|
||||
context.lineTo(width / 2, height);
|
||||
context.closePath();
|
||||
context.fillStyle = control.pressed ? control.accentColor : "#ABABAB";
|
||||
context.fillStyle = control.pressed ? control.accentColor : "#888888";
|
||||
context.fill();
|
||||
}
|
||||
}
|
||||
@@ -92,8 +89,8 @@ ComboBox {
|
||||
implicitWidth: 140
|
||||
implicitHeight: 40
|
||||
color: control.bgColor
|
||||
border.color: control.pressed || control.activeFocus ? control.accentColor : SettingsStyle.borderSubtle
|
||||
border.width: control.activeFocus ? SettingsStyle.focusRingWidth : 1
|
||||
border.color: control.pressed || control.activeFocus ? control.accentColor : "#40ffffff"
|
||||
border.width: 1
|
||||
radius: 6
|
||||
|
||||
// Glow effect on focus (Simplified to just border for stability)
|
||||
@@ -117,7 +114,7 @@ ComboBox {
|
||||
|
||||
background: Rectangle {
|
||||
color: control.popupColor
|
||||
border.color: SettingsStyle.borderSubtle
|
||||
border.color: "#40ffffff"
|
||||
border.width: 1
|
||||
radius: 6
|
||||
}
|
||||
|
||||
@@ -7,11 +7,8 @@ Rectangle {
|
||||
implicitHeight: 32
|
||||
color: "#1a1a20"
|
||||
radius: 6
|
||||
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
|
||||
border.width: 1
|
||||
border.color: activeFocus || recording ? SettingsStyle.accent : "#40ffffff"
|
||||
|
||||
property string currentSequence: ""
|
||||
signal sequenceChanged(string seq)
|
||||
@@ -29,7 +26,7 @@ Rectangle {
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: control.recording ? "Listening..." : (formatSequence(control.currentSequence) || "None")
|
||||
color: control.recording ? SettingsStyle.accent : (control.currentSequence ? "#ffffff" : "#ABABAB")
|
||||
color: control.recording ? SettingsStyle.accent : (control.currentSequence ? "#ffffff" : "#808080")
|
||||
font.family: "JetBrains Mono"
|
||||
font.pixelSize: 13
|
||||
font.bold: true
|
||||
|
||||
@@ -18,8 +18,6 @@ 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 } }
|
||||
|
||||
|
||||
@@ -9,8 +9,6 @@ ColumnLayout {
|
||||
|
||||
default property alias content: contentColumn.data
|
||||
property string title: ""
|
||||
Accessible.name: root.title + " settings group"
|
||||
Accessible.role: Accessible.Grouping
|
||||
|
||||
// Section Header
|
||||
Text {
|
||||
|
||||
@@ -5,49 +5,30 @@ 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: 6
|
||||
implicitHeight: 4
|
||||
width: control.availableWidth
|
||||
height: implicitHeight
|
||||
radius: 3
|
||||
radius: 2
|
||||
color: "#2d2d3d"
|
||||
|
||||
Rectangle {
|
||||
width: control.visualPosition * parent.width
|
||||
height: parent.height
|
||||
color: SettingsStyle.accent
|
||||
radius: 3
|
||||
radius: 2
|
||||
}
|
||||
}
|
||||
|
||||
handle: Item {
|
||||
handle: Rectangle {
|
||||
x: control.leftPadding + control.visualPosition * (control.availableWidth - width)
|
||||
y: control.topPadding + control.availableHeight / 2 - height / 2
|
||||
implicitWidth: SettingsStyle.minTargetSize
|
||||
implicitHeight: SettingsStyle.minTargetSize
|
||||
|
||||
// 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
|
||||
implicitWidth: 18
|
||||
implicitHeight: 18
|
||||
radius: 9
|
||||
color: "white"
|
||||
border.color: SettingsStyle.accent
|
||||
border.width: 2
|
||||
@@ -60,9 +41,7 @@ Slider {
|
||||
shadowColor: SettingsStyle.accent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Value Readout
|
||||
// Value Readout (Left side to avoid clipping on right edge)
|
||||
Text {
|
||||
anchors.right: parent.left
|
||||
anchors.rightMargin: 12
|
||||
|
||||
@@ -4,10 +4,6 @@ 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
|
||||
@@ -15,11 +11,9 @@ Switch {
|
||||
y: parent.height / 2 - height / 2
|
||||
radius: 12
|
||||
color: control.checked ? SettingsStyle.accent : "#2d2d3d"
|
||||
border.color: control.checked ? SettingsStyle.accent : SettingsStyle.borderSubtle
|
||||
border.width: control.activeFocus ? SettingsStyle.focusRingWidth : 1
|
||||
border.color: control.checked ? SettingsStyle.accent : "#3d3d4d"
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 200 } }
|
||||
Behavior on border.color { ColorAnimation { duration: 200 } }
|
||||
|
||||
Rectangle {
|
||||
x: control.checked ? parent.width - width - 3 : 3
|
||||
@@ -32,15 +26,6 @@ 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,10 +7,7 @@ TextField {
|
||||
property color accentColor: "#00f2ff"
|
||||
property color bgColor: "#1a1a20"
|
||||
|
||||
Accessible.role: Accessible.EditableText
|
||||
Accessible.name: control.placeholderText || "Text input"
|
||||
|
||||
placeholderTextColor: SettingsStyle.textDisabled
|
||||
placeholderTextColor: "#606060"
|
||||
color: "#ffffff"
|
||||
font.family: "JetBrains Mono"
|
||||
font.pixelSize: 14
|
||||
@@ -21,8 +18,8 @@ TextField {
|
||||
implicitWidth: 200
|
||||
implicitHeight: 40
|
||||
color: control.bgColor
|
||||
border.color: control.activeFocus ? control.accentColor : SettingsStyle.borderSubtle
|
||||
border.width: control.activeFocus ? SettingsStyle.focusRingWidth : 1
|
||||
border.color: control.activeFocus ? control.accentColor : "#40ffffff"
|
||||
border.width: 1
|
||||
radius: 6
|
||||
|
||||
Behavior on border.color { ColorAnimation { duration: 150 } }
|
||||
|
||||
@@ -13,8 +13,6 @@ ApplicationWindow {
|
||||
visible: true
|
||||
flags: Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool
|
||||
color: "transparent"
|
||||
title: "WhisperVoice"
|
||||
Accessible.name: "WhisperVoice Overlay"
|
||||
|
||||
FontLoader {
|
||||
id: jetBrainsMono
|
||||
@@ -37,7 +35,7 @@ ApplicationWindow {
|
||||
property bool isActive: ui.isRecording || ui.isProcessing
|
||||
|
||||
SequentialAnimation {
|
||||
running: !ui.reduceMotion
|
||||
running: true
|
||||
loops: Animation.Infinite
|
||||
PauseAnimation { duration: 3000 }
|
||||
NumberAnimation {
|
||||
@@ -98,7 +96,6 @@ 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 }
|
||||
@@ -108,7 +105,6 @@ ApplicationWindow {
|
||||
ShaderEffect {
|
||||
anchors.fill: parent
|
||||
opacity: 0.04
|
||||
visible: !ui.reduceMotion
|
||||
property real time: 0
|
||||
property real intensity: ui.amplitude
|
||||
fragmentShader: "glow.qsb"
|
||||
@@ -119,7 +115,6 @@ ApplicationWindow {
|
||||
ParticleSystem {
|
||||
id: particles
|
||||
anchors.fill: parent
|
||||
running: !ui.reduceMotion
|
||||
ItemParticle {
|
||||
system: particles
|
||||
delegate: Rectangle { width: 2; height: 2; radius: 1; color: "#10ffffff" }
|
||||
@@ -148,7 +143,6 @@ 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 }
|
||||
@@ -178,7 +172,7 @@ ApplicationWindow {
|
||||
radius: height / 2
|
||||
color: "transparent"
|
||||
border.width: 1
|
||||
border.color: Qt.rgba(1, 1, 1, 0.22)
|
||||
border.color: "#40ffffff"
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent; hoverEnabled: true
|
||||
@@ -200,7 +194,7 @@ ApplicationWindow {
|
||||
NumberAnimation { duration: 150; easing.type: Easing.OutCubic }
|
||||
}
|
||||
SequentialAnimation on border.color {
|
||||
running: ui.isRecording && !ui.reduceMotion
|
||||
running: ui.isRecording
|
||||
loops: Animation.Infinite
|
||||
ColorAnimation { from: "#A0ff4b4b"; to: "#C0ff6b6b"; duration: 800 }
|
||||
ColorAnimation { from: "#C0ff6b6b"; to: "#A0ff4b4b"; duration: 800 }
|
||||
@@ -215,11 +209,6 @@ 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
|
||||
@@ -256,7 +245,7 @@ ApplicationWindow {
|
||||
border.width: 2; border.color: "#60ffffff"
|
||||
|
||||
SequentialAnimation on scale {
|
||||
running: ui.isRecording && !ui.reduceMotion
|
||||
running: ui.isRecording
|
||||
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 }
|
||||
@@ -274,17 +263,6 @@ 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) ---
|
||||
@@ -299,7 +277,6 @@ ApplicationWindow {
|
||||
|
||||
ShaderEffect {
|
||||
anchors.fill: parent
|
||||
visible: !ui.reduceMotion
|
||||
property real time: 0
|
||||
property real amplitude: ui.amplitude
|
||||
fragmentShader: "rainbow_wave.qsb"
|
||||
@@ -364,10 +341,8 @@ 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 && !ui.reduceMotion; loops: Animation.Infinite
|
||||
running: ui.isRecording; loops: Animation.Infinite
|
||||
NumberAnimation { from: 1.0; to: 0.7; duration: 800 }
|
||||
NumberAnimation { from: 0.7; to: 1.0; duration: 800 }
|
||||
}
|
||||
|
||||
@@ -12,8 +12,7 @@ Window {
|
||||
visible: false
|
||||
flags: Qt.FramelessWindowHint | Qt.Window
|
||||
color: "transparent"
|
||||
title: "WhisperVoice Settings"
|
||||
Accessible.name: "WhisperVoice Settings"
|
||||
title: "Settings"
|
||||
|
||||
// Explicit sizing for Python to read
|
||||
|
||||
@@ -134,20 +133,15 @@ 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 ? "#20FF8A8A" : "transparent"
|
||||
border.color: closeMa.containsMouse ? "#40FF8A8A" : "transparent"
|
||||
color: closeMa.containsMouse ? "#20ff4b4b" : "transparent"
|
||||
border.color: closeMa.containsMouse ? "#40ff4b4b" : "transparent"
|
||||
border.width: 1
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "×"
|
||||
color: closeMa.containsMouse ? "#FF8A8A" : SettingsStyle.textSecondary
|
||||
color: closeMa.containsMouse ? "#ff4b4b" : SettingsStyle.textSecondary
|
||||
font.family: mainFont
|
||||
font.pixelSize: 20
|
||||
font.bold: true
|
||||
@@ -163,15 +157,6 @@ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,23 +206,6 @@ 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 } }
|
||||
|
||||
@@ -288,15 +256,6 @@ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -327,7 +286,6 @@ Window {
|
||||
|
||||
// --- TAB: GENERAL ---
|
||||
ScrollView {
|
||||
Accessible.role: Accessible.PageTab
|
||||
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
||||
contentWidth: availableWidth
|
||||
|
||||
@@ -425,7 +383,6 @@ Window {
|
||||
|
||||
// --- TAB: AUDIO ---
|
||||
ScrollView {
|
||||
Accessible.role: Accessible.PageTab
|
||||
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
||||
contentWidth: availableWidth
|
||||
|
||||
@@ -514,7 +471,6 @@ Window {
|
||||
|
||||
// --- TAB: VISUALS ---
|
||||
ScrollView {
|
||||
Accessible.role: Accessible.PageTab
|
||||
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
||||
contentWidth: availableWidth
|
||||
|
||||
@@ -564,7 +520,7 @@ Window {
|
||||
ModernSettingsItem {
|
||||
label: "Window Opacity"
|
||||
description: "Transparency level"
|
||||
showSeparator: true
|
||||
showSeparator: false
|
||||
control: ModernSlider {
|
||||
Layout.preferredWidth: 200
|
||||
from: 0.1; to: 1.0
|
||||
@@ -572,15 +528,6 @@ 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -633,7 +580,6 @@ Window {
|
||||
|
||||
// --- TAB: AI ENGINE ---
|
||||
ScrollView {
|
||||
Accessible.role: Accessible.PageTab
|
||||
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
||||
contentWidth: availableWidth
|
||||
|
||||
@@ -806,8 +752,8 @@ Window {
|
||||
}
|
||||
color: "#ffffff"
|
||||
font.family: "JetBrains Mono"
|
||||
font.pixelSize: 11
|
||||
opacity: 1.0
|
||||
font.pixelSize: 10
|
||||
opacity: 0.7
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
@@ -1094,7 +1040,6 @@ Window {
|
||||
|
||||
// --- TAB: DEBUG ---
|
||||
ScrollView {
|
||||
Accessible.role: Accessible.PageTab
|
||||
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
||||
contentWidth: availableWidth
|
||||
|
||||
@@ -1120,9 +1065,9 @@ Window {
|
||||
spacing: 16
|
||||
|
||||
StatBox { label: "APP CPU"; value: ui.appCpu; unit: "%"; accent: "#00f2ff" }
|
||||
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" }
|
||||
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" }
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
|
||||
@@ -6,14 +6,13 @@ 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
|
||||
readonly property color borderSubtle: Qt.rgba(1, 1, 1, 0.22) // WCAG 3:1 non-text contrast
|
||||
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 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 textPrimary: "#FAFAFA" // Brighter white
|
||||
readonly property color textSecondary: "#999999"
|
||||
|
||||
readonly property color accentPurple: "#B794F6" // WCAG AAA 7.2:1 on #121212
|
||||
readonly property color accentPurple: "#7000FF"
|
||||
readonly property color accentCyan: "#00F2FF"
|
||||
|
||||
// Configurable active accent
|
||||
@@ -22,9 +21,5 @@ QtObject {
|
||||
// Dimensions
|
||||
readonly property int cardRadius: 16
|
||||
readonly property int itemRadius: 8
|
||||
readonly property int itemHeight: 60
|
||||
|
||||
// Accessibility
|
||||
readonly property int focusRingWidth: 2
|
||||
readonly property int minTargetSize: 24
|
||||
readonly property int itemHeight: 60 // Even taller for more breathing room
|
||||
}
|
||||
|
||||
BIN
src/ui/qml/fonts/ttf/JetBrainsMonoNL-Bold.ttf
Normal file
BIN
src/ui/qml/fonts/ttf/JetBrainsMonoNL-Bold.ttf
Normal file
Binary file not shown.
BIN
src/ui/qml/fonts/ttf/JetBrainsMonoNL-BoldItalic.ttf
Normal file
BIN
src/ui/qml/fonts/ttf/JetBrainsMonoNL-BoldItalic.ttf
Normal file
Binary file not shown.
BIN
src/ui/qml/fonts/ttf/JetBrainsMonoNL-ExtraBold.ttf
Normal file
BIN
src/ui/qml/fonts/ttf/JetBrainsMonoNL-ExtraBold.ttf
Normal file
Binary file not shown.
BIN
src/ui/qml/fonts/ttf/JetBrainsMonoNL-ExtraBoldItalic.ttf
Normal file
BIN
src/ui/qml/fonts/ttf/JetBrainsMonoNL-ExtraBoldItalic.ttf
Normal file
Binary file not shown.
BIN
src/ui/qml/fonts/ttf/JetBrainsMonoNL-ExtraLight.ttf
Normal file
BIN
src/ui/qml/fonts/ttf/JetBrainsMonoNL-ExtraLight.ttf
Normal file
Binary file not shown.
BIN
src/ui/qml/fonts/ttf/JetBrainsMonoNL-ExtraLightItalic.ttf
Normal file
BIN
src/ui/qml/fonts/ttf/JetBrainsMonoNL-ExtraLightItalic.ttf
Normal file
Binary file not shown.
BIN
src/ui/qml/fonts/ttf/JetBrainsMonoNL-Italic.ttf
Normal file
BIN
src/ui/qml/fonts/ttf/JetBrainsMonoNL-Italic.ttf
Normal file
Binary file not shown.
BIN
src/ui/qml/fonts/ttf/JetBrainsMonoNL-Light.ttf
Normal file
BIN
src/ui/qml/fonts/ttf/JetBrainsMonoNL-Light.ttf
Normal file
Binary file not shown.
BIN
src/ui/qml/fonts/ttf/JetBrainsMonoNL-LightItalic.ttf
Normal file
BIN
src/ui/qml/fonts/ttf/JetBrainsMonoNL-LightItalic.ttf
Normal file
Binary file not shown.
BIN
src/ui/qml/fonts/ttf/JetBrainsMonoNL-Medium.ttf
Normal file
BIN
src/ui/qml/fonts/ttf/JetBrainsMonoNL-Medium.ttf
Normal file
Binary file not shown.
BIN
src/ui/qml/fonts/ttf/JetBrainsMonoNL-MediumItalic.ttf
Normal file
BIN
src/ui/qml/fonts/ttf/JetBrainsMonoNL-MediumItalic.ttf
Normal file
Binary file not shown.
BIN
src/ui/qml/fonts/ttf/JetBrainsMonoNL-Regular.ttf
Normal file
BIN
src/ui/qml/fonts/ttf/JetBrainsMonoNL-Regular.ttf
Normal file
Binary file not shown.
BIN
src/ui/qml/fonts/ttf/JetBrainsMonoNL-SemiBold.ttf
Normal file
BIN
src/ui/qml/fonts/ttf/JetBrainsMonoNL-SemiBold.ttf
Normal file
Binary file not shown.
BIN
src/ui/qml/fonts/ttf/JetBrainsMonoNL-SemiBoldItalic.ttf
Normal file
BIN
src/ui/qml/fonts/ttf/JetBrainsMonoNL-SemiBoldItalic.ttf
Normal file
Binary file not shown.
BIN
src/ui/qml/fonts/ttf/JetBrainsMonoNL-Thin.ttf
Normal file
BIN
src/ui/qml/fonts/ttf/JetBrainsMonoNL-Thin.ttf
Normal file
Binary file not shown.
BIN
src/ui/qml/fonts/ttf/JetBrainsMonoNL-ThinItalic.ttf
Normal file
BIN
src/ui/qml/fonts/ttf/JetBrainsMonoNL-ThinItalic.ttf
Normal file
Binary file not shown.
BIN
src/ui/qml/fonts/variable/JetBrainsMono-Italic[wght].ttf
Normal file
BIN
src/ui/qml/fonts/variable/JetBrainsMono-Italic[wght].ttf
Normal file
Binary file not shown.
BIN
src/ui/qml/fonts/variable/JetBrainsMono[wght].ttf
Normal file
BIN
src/ui/qml/fonts/variable/JetBrainsMono[wght].ttf
Normal file
Binary file not shown.
BIN
src/ui/qml/fonts/webfonts/JetBrainsMono-Bold.woff2
Normal file
BIN
src/ui/qml/fonts/webfonts/JetBrainsMono-Bold.woff2
Normal file
Binary file not shown.
BIN
src/ui/qml/fonts/webfonts/JetBrainsMono-BoldItalic.woff2
Normal file
BIN
src/ui/qml/fonts/webfonts/JetBrainsMono-BoldItalic.woff2
Normal file
Binary file not shown.
BIN
src/ui/qml/fonts/webfonts/JetBrainsMono-ExtraBold.woff2
Normal file
BIN
src/ui/qml/fonts/webfonts/JetBrainsMono-ExtraBold.woff2
Normal file
Binary file not shown.
BIN
src/ui/qml/fonts/webfonts/JetBrainsMono-ExtraBoldItalic.woff2
Normal file
BIN
src/ui/qml/fonts/webfonts/JetBrainsMono-ExtraBoldItalic.woff2
Normal file
Binary file not shown.
BIN
src/ui/qml/fonts/webfonts/JetBrainsMono-ExtraLight.woff2
Normal file
BIN
src/ui/qml/fonts/webfonts/JetBrainsMono-ExtraLight.woff2
Normal file
Binary file not shown.
BIN
src/ui/qml/fonts/webfonts/JetBrainsMono-ExtraLightItalic.woff2
Normal file
BIN
src/ui/qml/fonts/webfonts/JetBrainsMono-ExtraLightItalic.woff2
Normal file
Binary file not shown.
BIN
src/ui/qml/fonts/webfonts/JetBrainsMono-Italic.woff2
Normal file
BIN
src/ui/qml/fonts/webfonts/JetBrainsMono-Italic.woff2
Normal file
Binary file not shown.
BIN
src/ui/qml/fonts/webfonts/JetBrainsMono-Light.woff2
Normal file
BIN
src/ui/qml/fonts/webfonts/JetBrainsMono-Light.woff2
Normal file
Binary file not shown.
BIN
src/ui/qml/fonts/webfonts/JetBrainsMono-LightItalic.woff2
Normal file
BIN
src/ui/qml/fonts/webfonts/JetBrainsMono-LightItalic.woff2
Normal file
Binary file not shown.
BIN
src/ui/qml/fonts/webfonts/JetBrainsMono-Medium.woff2
Normal file
BIN
src/ui/qml/fonts/webfonts/JetBrainsMono-Medium.woff2
Normal file
Binary file not shown.
BIN
src/ui/qml/fonts/webfonts/JetBrainsMono-MediumItalic.woff2
Normal file
BIN
src/ui/qml/fonts/webfonts/JetBrainsMono-MediumItalic.woff2
Normal file
Binary file not shown.
BIN
src/ui/qml/fonts/webfonts/JetBrainsMono-Regular.woff2
Normal file
BIN
src/ui/qml/fonts/webfonts/JetBrainsMono-Regular.woff2
Normal file
Binary file not shown.
BIN
src/ui/qml/fonts/webfonts/JetBrainsMono-SemiBold.woff2
Normal file
BIN
src/ui/qml/fonts/webfonts/JetBrainsMono-SemiBold.woff2
Normal file
Binary file not shown.
BIN
src/ui/qml/fonts/webfonts/JetBrainsMono-SemiBoldItalic.woff2
Normal file
BIN
src/ui/qml/fonts/webfonts/JetBrainsMono-SemiBoldItalic.woff2
Normal file
Binary file not shown.
BIN
src/ui/qml/fonts/webfonts/JetBrainsMono-Thin.woff2
Normal file
BIN
src/ui/qml/fonts/webfonts/JetBrainsMono-Thin.woff2
Normal file
Binary file not shown.
BIN
src/ui/qml/fonts/webfonts/JetBrainsMono-ThinItalic.woff2
Normal file
BIN
src/ui/qml/fonts/webfonts/JetBrainsMono-ThinItalic.woff2
Normal file
Binary file not shown.
50
src/ui/qml/glass.frag
Normal file
50
src/ui/qml/glass.frag
Normal file
@@ -0,0 +1,50 @@
|
||||
#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;
|
||||
}
|
||||
BIN
src/ui/qml/glass.qsb
Normal file
BIN
src/ui/qml/glass.qsb
Normal file
Binary file not shown.
25
src/ui/qml/noise.frag
Normal file
25
src/ui/qml/noise.frag
Normal file
@@ -0,0 +1,25 @@
|
||||
#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;
|
||||
}
|
||||
BIN
src/ui/qml/noise.qsb
Normal file
BIN
src/ui/qml/noise.qsb
Normal file
Binary file not shown.
BIN
src/ui/qml/settings.png
Normal file
BIN
src/ui/qml/settings.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 492 KiB |
BIN
src/ui/qml/smart_toy.png
Normal file
BIN
src/ui/qml/smart_toy.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 490 KiB |
BIN
src/ui/qml/visibility.png
Normal file
BIN
src/ui/qml/visibility.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 464 KiB |
236
src/ui/settings.py
Normal file
236
src/ui/settings.py
Normal file
@@ -0,0 +1,236 @@
|
||||
"""
|
||||
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
src/ui/styles.py
Normal file
62
src/ui/styles.py
Normal file
@@ -0,0 +1,62 @@
|
||||
"""
|
||||
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
src/ui/visualizer.py
Normal file
117
src/ui/visualizer.py
Normal file
@@ -0,0 +1,117 @@
|
||||
"""
|
||||
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
test_m2m.py
Normal file
38
test_m2m.py
Normal file
@@ -0,0 +1,38 @@
|
||||
|
||||
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
test_mt0.py
Normal file
40
test_mt0.py
Normal file
@@ -0,0 +1,40 @@
|
||||
|
||||
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
test_punctuation.py
Normal file
34
test_punctuation.py
Normal file
@@ -0,0 +1,34 @@
|
||||
|
||||
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()
|
||||
Reference in New Issue
Block a user