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
|
## 🛠️ Deployment
|
||||||
|
|
||||||
### 📥 Installation
|
### 📥 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:
|
except:
|
||||||
pass
|
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
|
# Configure Logging
|
||||||
class QmlLoggingHandler(logging.Handler, QObject):
|
class QmlLoggingHandler(logging.Handler, QObject):
|
||||||
sig_log = Signal(str)
|
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
|
# 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:
|
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')
|
settingChanged = Signal(str, 'QVariant')
|
||||||
modelStatesChanged = Signal() # Notify UI to re-check isModelDownloaded
|
modelStatesChanged = Signal() # Notify UI to re-check isModelDownloaded
|
||||||
llmDownloadRequested = Signal()
|
llmDownloadRequested = Signal()
|
||||||
reduceMotionChanged = Signal(bool)
|
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
@@ -131,7 +130,6 @@ class UIBridge(QObject):
|
|||||||
self._app_vram_mb = 0.0
|
self._app_vram_mb = 0.0
|
||||||
self._app_vram_percent = 0.0
|
self._app_vram_percent = 0.0
|
||||||
self._is_destroyed = False
|
self._is_destroyed = False
|
||||||
self._reduce_motion = bool(ConfigManager().get("reduce_motion"))
|
|
||||||
|
|
||||||
# Start QThread Stats Worker
|
# Start QThread Stats Worker
|
||||||
self.stats_worker = StatsWorker()
|
self.stats_worker = StatsWorker()
|
||||||
@@ -279,8 +277,6 @@ class UIBridge(QObject):
|
|||||||
ConfigManager().set(key, value)
|
ConfigManager().set(key, value)
|
||||||
if key == "ui_scale":
|
if key == "ui_scale":
|
||||||
self.uiScale = float(value)
|
self.uiScale = float(value)
|
||||||
if key == "reduce_motion":
|
|
||||||
self.reduceMotion = bool(value)
|
|
||||||
self.settingChanged.emit(key, value) # Notify listeners (e.g. Overlay)
|
self.settingChanged.emit(key, value) # Notify listeners (e.g. Overlay)
|
||||||
|
|
||||||
@Property(float, notify=uiScaleChanged)
|
@Property(float, notify=uiScaleChanged)
|
||||||
@@ -292,15 +288,6 @@ class UIBridge(QObject):
|
|||||||
self._ui_scale = val
|
self._ui_scale = val
|
||||||
self.uiScaleChanged.emit(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)
|
@Property(float, notify=appCpuChanged)
|
||||||
def appCpu(self): return self._app_cpu
|
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"
|
text: "Button"
|
||||||
|
|
||||||
property color accentColor: "#00f2ff"
|
property color accentColor: "#00f2ff"
|
||||||
Accessible.role: Accessible.Button
|
|
||||||
Accessible.name: control.text
|
|
||||||
activeFocusOnTab: true
|
|
||||||
|
|
||||||
contentItem: Text {
|
contentItem: Text {
|
||||||
text: control.text
|
text: control.text
|
||||||
font.pixelSize: 13
|
font.pixelSize: 13
|
||||||
font.bold: true
|
font.bold: true
|
||||||
color: control.hovered ? "white" : "#ABABAB"
|
color: control.hovered ? "white" : "#9499b0"
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
@@ -28,8 +25,8 @@ Button {
|
|||||||
opacity: control.down ? 0.7 : 1.0
|
opacity: control.down ? 0.7 : 1.0
|
||||||
color: control.hovered ? Qt.rgba(1, 1, 1, 0.1) : Qt.rgba(1, 1, 1, 0.05)
|
color: control.hovered ? Qt.rgba(1, 1, 1, 0.1) : Qt.rgba(1, 1, 1, 0.05)
|
||||||
radius: 8
|
radius: 8
|
||||||
border.color: control.hovered ? control.accentColor : SettingsStyle.borderSubtle
|
border.color: control.hovered ? control.accentColor : Qt.rgba(1, 1, 1, 0.1)
|
||||||
border.width: control.activeFocus ? SettingsStyle.focusRingWidth : 1
|
border.width: 1
|
||||||
|
|
||||||
Behavior on border.color { ColorAnimation { duration: 200 } }
|
Behavior on border.color { ColorAnimation { duration: 200 } }
|
||||||
Behavior on 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
|
visible: true
|
||||||
flags: Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool
|
flags: Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
title: "WhisperVoice"
|
|
||||||
Accessible.name: "WhisperVoice Loading"
|
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: bgRect
|
id: bgRect
|
||||||
@@ -23,7 +21,7 @@ ApplicationWindow {
|
|||||||
anchors.margins: 20 // Space for shadow
|
anchors.margins: 20 // Space for shadow
|
||||||
radius: 16
|
radius: 16
|
||||||
color: "#1a1a20"
|
color: "#1a1a20"
|
||||||
border.color: Qt.rgba(1, 1, 1, 0.22)
|
border.color: "#40ffffff"
|
||||||
border.width: 1
|
border.width: 1
|
||||||
|
|
||||||
// --- SHADOW & GLOW ---
|
// --- SHADOW & GLOW ---
|
||||||
@@ -57,7 +55,6 @@ ApplicationWindow {
|
|||||||
|
|
||||||
// Pulse Animation
|
// Pulse Animation
|
||||||
SequentialAnimation on scale {
|
SequentialAnimation on scale {
|
||||||
running: ui ? !ui.reduceMotion : true
|
|
||||||
loops: Animation.Infinite
|
loops: Animation.Infinite
|
||||||
NumberAnimation { from: 1.0; to: 1.1; duration: 1000; easing.type: Easing.InOutSine }
|
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 }
|
NumberAnimation { from: 1.1; to: 1.0; duration: 1000; easing.type: Easing.InOutSine }
|
||||||
@@ -98,7 +95,7 @@ ApplicationWindow {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: "AI TRANSCRIPTION ENGINE"
|
text: "AI TRANSCRIPTION ENGINE"
|
||||||
color: "#ABABAB"
|
color: "#80ffffff"
|
||||||
font.family: jetBrainsMono.name
|
font.family: jetBrainsMono.name
|
||||||
font.pixelSize: 10
|
font.pixelSize: 10
|
||||||
font.letterSpacing: 2
|
font.letterSpacing: 2
|
||||||
@@ -138,7 +135,6 @@ ApplicationWindow {
|
|||||||
// Shimmer effect on bar
|
// Shimmer effect on bar
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: 20; height: parent.height
|
width: 20; height: parent.height
|
||||||
visible: ui ? !ui.reduceMotion : true
|
|
||||||
color: "#80ffffff"
|
color: "#80ffffff"
|
||||||
x: -width
|
x: -width
|
||||||
opacity: 0.5
|
opacity: 0.5
|
||||||
@@ -161,10 +157,8 @@ ApplicationWindow {
|
|||||||
font.family: jetBrainsMono.name
|
font.family: jetBrainsMono.name
|
||||||
font.pixelSize: 11
|
font.pixelSize: 11
|
||||||
font.bold: true
|
font.bold: true
|
||||||
Accessible.role: Accessible.AlertMessage
|
|
||||||
Accessible.name: "Loading status: " + text
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
opacity: 1.0
|
opacity: 0.8
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,9 +10,6 @@ ComboBox {
|
|||||||
property color bgColor: "#1a1a20"
|
property color bgColor: "#1a1a20"
|
||||||
property color popupColor: "#252530"
|
property color popupColor: "#252530"
|
||||||
|
|
||||||
Accessible.role: Accessible.ComboBox
|
|
||||||
Accessible.name: control.displayText
|
|
||||||
|
|
||||||
delegate: ItemDelegate {
|
delegate: ItemDelegate {
|
||||||
id: delegate
|
id: delegate
|
||||||
width: control.width
|
width: control.width
|
||||||
@@ -71,7 +68,7 @@ ComboBox {
|
|||||||
context.lineTo(width, 0);
|
context.lineTo(width, 0);
|
||||||
context.lineTo(width / 2, height);
|
context.lineTo(width / 2, height);
|
||||||
context.closePath();
|
context.closePath();
|
||||||
context.fillStyle = control.pressed ? control.accentColor : "#ABABAB";
|
context.fillStyle = control.pressed ? control.accentColor : "#888888";
|
||||||
context.fill();
|
context.fill();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,8 +89,8 @@ ComboBox {
|
|||||||
implicitWidth: 140
|
implicitWidth: 140
|
||||||
implicitHeight: 40
|
implicitHeight: 40
|
||||||
color: control.bgColor
|
color: control.bgColor
|
||||||
border.color: control.pressed || control.activeFocus ? control.accentColor : SettingsStyle.borderSubtle
|
border.color: control.pressed || control.activeFocus ? control.accentColor : "#40ffffff"
|
||||||
border.width: control.activeFocus ? SettingsStyle.focusRingWidth : 1
|
border.width: 1
|
||||||
radius: 6
|
radius: 6
|
||||||
|
|
||||||
// Glow effect on focus (Simplified to just border for stability)
|
// Glow effect on focus (Simplified to just border for stability)
|
||||||
@@ -117,7 +114,7 @@ ComboBox {
|
|||||||
|
|
||||||
background: Rectangle {
|
background: Rectangle {
|
||||||
color: control.popupColor
|
color: control.popupColor
|
||||||
border.color: SettingsStyle.borderSubtle
|
border.color: "#40ffffff"
|
||||||
border.width: 1
|
border.width: 1
|
||||||
radius: 6
|
radius: 6
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,11 +7,8 @@ Rectangle {
|
|||||||
implicitHeight: 32
|
implicitHeight: 32
|
||||||
color: "#1a1a20"
|
color: "#1a1a20"
|
||||||
radius: 6
|
radius: 6
|
||||||
activeFocusOnTab: true
|
border.width: 1
|
||||||
Accessible.role: Accessible.Button
|
border.color: activeFocus || recording ? SettingsStyle.accent : "#40ffffff"
|
||||||
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: ""
|
property string currentSequence: ""
|
||||||
signal sequenceChanged(string seq)
|
signal sequenceChanged(string seq)
|
||||||
@@ -29,7 +26,7 @@ Rectangle {
|
|||||||
Text {
|
Text {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: control.recording ? "Listening..." : (formatSequence(control.currentSequence) || "None")
|
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.family: "JetBrains Mono"
|
||||||
font.pixelSize: 13
|
font.pixelSize: 13
|
||||||
font.bold: true
|
font.bold: true
|
||||||
|
|||||||
@@ -18,8 +18,6 @@ Rectangle {
|
|||||||
property string description: ""
|
property string description: ""
|
||||||
property alias control: controlContainer.data
|
property alias control: controlContainer.data
|
||||||
property bool showSeparator: true
|
property bool showSeparator: true
|
||||||
Accessible.name: root.label
|
|
||||||
Accessible.role: Accessible.Row
|
|
||||||
|
|
||||||
Behavior on color { ColorAnimation { duration: 150 } }
|
Behavior on color { ColorAnimation { duration: 150 } }
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ ColumnLayout {
|
|||||||
|
|
||||||
default property alias content: contentColumn.data
|
default property alias content: contentColumn.data
|
||||||
property string title: ""
|
property string title: ""
|
||||||
Accessible.name: root.title + " settings group"
|
|
||||||
Accessible.role: Accessible.Grouping
|
|
||||||
|
|
||||||
// Section Header
|
// Section Header
|
||||||
Text {
|
Text {
|
||||||
|
|||||||
@@ -5,49 +5,30 @@ import QtQuick.Effects
|
|||||||
Slider {
|
Slider {
|
||||||
id: control
|
id: control
|
||||||
|
|
||||||
Accessible.role: Accessible.Slider
|
|
||||||
Accessible.name: control.value.toString()
|
|
||||||
activeFocusOnTab: true
|
|
||||||
|
|
||||||
background: Rectangle {
|
background: Rectangle {
|
||||||
x: control.leftPadding
|
x: control.leftPadding
|
||||||
y: control.topPadding + control.availableHeight / 2 - height / 2
|
y: control.topPadding + control.availableHeight / 2 - height / 2
|
||||||
implicitWidth: 200
|
implicitWidth: 200
|
||||||
implicitHeight: 6
|
implicitHeight: 4
|
||||||
width: control.availableWidth
|
width: control.availableWidth
|
||||||
height: implicitHeight
|
height: implicitHeight
|
||||||
radius: 3
|
radius: 2
|
||||||
color: "#2d2d3d"
|
color: "#2d2d3d"
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: control.visualPosition * parent.width
|
width: control.visualPosition * parent.width
|
||||||
height: parent.height
|
height: parent.height
|
||||||
color: SettingsStyle.accent
|
color: SettingsStyle.accent
|
||||||
radius: 3
|
radius: 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handle: Item {
|
handle: Rectangle {
|
||||||
x: control.leftPadding + control.visualPosition * (control.availableWidth - width)
|
x: control.leftPadding + control.visualPosition * (control.availableWidth - width)
|
||||||
y: control.topPadding + control.availableHeight / 2 - height / 2
|
y: control.topPadding + control.availableHeight / 2 - height / 2
|
||||||
implicitWidth: SettingsStyle.minTargetSize
|
implicitWidth: 18
|
||||||
implicitHeight: SettingsStyle.minTargetSize
|
implicitHeight: 18
|
||||||
|
radius: 9
|
||||||
// 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"
|
color: "white"
|
||||||
border.color: SettingsStyle.accent
|
border.color: SettingsStyle.accent
|
||||||
border.width: 2
|
border.width: 2
|
||||||
@@ -60,9 +41,7 @@ Slider {
|
|||||||
shadowColor: SettingsStyle.accent
|
shadowColor: SettingsStyle.accent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
// Value Readout (Left side to avoid clipping on right edge)
|
||||||
|
|
||||||
// Value Readout
|
|
||||||
Text {
|
Text {
|
||||||
anchors.right: parent.left
|
anchors.right: parent.left
|
||||||
anchors.rightMargin: 12
|
anchors.rightMargin: 12
|
||||||
|
|||||||
@@ -4,10 +4,6 @@ import QtQuick.Controls
|
|||||||
Switch {
|
Switch {
|
||||||
id: control
|
id: control
|
||||||
|
|
||||||
Accessible.role: Accessible.CheckBox
|
|
||||||
Accessible.name: control.text + (control.checked ? " on" : " off")
|
|
||||||
activeFocusOnTab: true
|
|
||||||
|
|
||||||
indicator: Rectangle {
|
indicator: Rectangle {
|
||||||
implicitWidth: 44
|
implicitWidth: 44
|
||||||
implicitHeight: 24
|
implicitHeight: 24
|
||||||
@@ -15,11 +11,9 @@ Switch {
|
|||||||
y: parent.height / 2 - height / 2
|
y: parent.height / 2 - height / 2
|
||||||
radius: 12
|
radius: 12
|
||||||
color: control.checked ? SettingsStyle.accent : "#2d2d3d"
|
color: control.checked ? SettingsStyle.accent : "#2d2d3d"
|
||||||
border.color: control.checked ? SettingsStyle.accent : SettingsStyle.borderSubtle
|
border.color: control.checked ? SettingsStyle.accent : "#3d3d4d"
|
||||||
border.width: control.activeFocus ? SettingsStyle.focusRingWidth : 1
|
|
||||||
|
|
||||||
Behavior on color { ColorAnimation { duration: 200 } }
|
Behavior on color { ColorAnimation { duration: 200 } }
|
||||||
Behavior on border.color { ColorAnimation { duration: 200 } }
|
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
x: control.checked ? parent.width - width - 3 : 3
|
x: control.checked ? parent.width - width - 3 : 3
|
||||||
@@ -32,15 +26,6 @@ Switch {
|
|||||||
Behavior on x {
|
Behavior on x {
|
||||||
NumberAnimation { duration: 200; easing.type: Easing.InOutQuad }
|
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 accentColor: "#00f2ff"
|
||||||
property color bgColor: "#1a1a20"
|
property color bgColor: "#1a1a20"
|
||||||
|
|
||||||
Accessible.role: Accessible.EditableText
|
placeholderTextColor: "#606060"
|
||||||
Accessible.name: control.placeholderText || "Text input"
|
|
||||||
|
|
||||||
placeholderTextColor: SettingsStyle.textDisabled
|
|
||||||
color: "#ffffff"
|
color: "#ffffff"
|
||||||
font.family: "JetBrains Mono"
|
font.family: "JetBrains Mono"
|
||||||
font.pixelSize: 14
|
font.pixelSize: 14
|
||||||
@@ -21,8 +18,8 @@ TextField {
|
|||||||
implicitWidth: 200
|
implicitWidth: 200
|
||||||
implicitHeight: 40
|
implicitHeight: 40
|
||||||
color: control.bgColor
|
color: control.bgColor
|
||||||
border.color: control.activeFocus ? control.accentColor : SettingsStyle.borderSubtle
|
border.color: control.activeFocus ? control.accentColor : "#40ffffff"
|
||||||
border.width: control.activeFocus ? SettingsStyle.focusRingWidth : 1
|
border.width: 1
|
||||||
radius: 6
|
radius: 6
|
||||||
|
|
||||||
Behavior on border.color { ColorAnimation { duration: 150 } }
|
Behavior on border.color { ColorAnimation { duration: 150 } }
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ ApplicationWindow {
|
|||||||
visible: true
|
visible: true
|
||||||
flags: Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool
|
flags: Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
title: "WhisperVoice"
|
|
||||||
Accessible.name: "WhisperVoice Overlay"
|
|
||||||
|
|
||||||
FontLoader {
|
FontLoader {
|
||||||
id: jetBrainsMono
|
id: jetBrainsMono
|
||||||
@@ -37,7 +35,7 @@ ApplicationWindow {
|
|||||||
property bool isActive: ui.isRecording || ui.isProcessing
|
property bool isActive: ui.isRecording || ui.isProcessing
|
||||||
|
|
||||||
SequentialAnimation {
|
SequentialAnimation {
|
||||||
running: !ui.reduceMotion
|
running: true
|
||||||
loops: Animation.Infinite
|
loops: Animation.Infinite
|
||||||
PauseAnimation { duration: 3000 }
|
PauseAnimation { duration: 3000 }
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
@@ -98,7 +96,6 @@ ApplicationWindow {
|
|||||||
ShaderEffect {
|
ShaderEffect {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
opacity: 0.4
|
opacity: 0.4
|
||||||
visible: !ui.reduceMotion
|
|
||||||
property real time: 0
|
property real time: 0
|
||||||
fragmentShader: "gradient_blobs.qsb"
|
fragmentShader: "gradient_blobs.qsb"
|
||||||
NumberAnimation on time { from: 0; to: 1000; duration: 100000; loops: Animation.Infinite }
|
NumberAnimation on time { from: 0; to: 1000; duration: 100000; loops: Animation.Infinite }
|
||||||
@@ -108,7 +105,6 @@ ApplicationWindow {
|
|||||||
ShaderEffect {
|
ShaderEffect {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
opacity: 0.04
|
opacity: 0.04
|
||||||
visible: !ui.reduceMotion
|
|
||||||
property real time: 0
|
property real time: 0
|
||||||
property real intensity: ui.amplitude
|
property real intensity: ui.amplitude
|
||||||
fragmentShader: "glow.qsb"
|
fragmentShader: "glow.qsb"
|
||||||
@@ -119,7 +115,6 @@ ApplicationWindow {
|
|||||||
ParticleSystem {
|
ParticleSystem {
|
||||||
id: particles
|
id: particles
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
running: !ui.reduceMotion
|
|
||||||
ItemParticle {
|
ItemParticle {
|
||||||
system: particles
|
system: particles
|
||||||
delegate: Rectangle { width: 2; height: 2; radius: 1; color: "#10ffffff" }
|
delegate: Rectangle { width: 2; height: 2; radius: 1; color: "#10ffffff" }
|
||||||
@@ -148,7 +143,6 @@ ApplicationWindow {
|
|||||||
// F. CRT Shader Effect (Overlay on chassis ONLY)
|
// F. CRT Shader Effect (Overlay on chassis ONLY)
|
||||||
ShaderEffect {
|
ShaderEffect {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
visible: !ui.reduceMotion
|
|
||||||
property real time: 0
|
property real time: 0
|
||||||
fragmentShader: "crt.qsb"
|
fragmentShader: "crt.qsb"
|
||||||
NumberAnimation on time { from: 0; to: 100; duration: 5000; loops: Animation.Infinite }
|
NumberAnimation on time { from: 0; to: 100; duration: 5000; loops: Animation.Infinite }
|
||||||
@@ -178,7 +172,7 @@ ApplicationWindow {
|
|||||||
radius: height / 2
|
radius: height / 2
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
border.width: 1
|
border.width: 1
|
||||||
border.color: Qt.rgba(1, 1, 1, 0.22)
|
border.color: "#40ffffff"
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent; hoverEnabled: true
|
anchors.fill: parent; hoverEnabled: true
|
||||||
@@ -200,7 +194,7 @@ ApplicationWindow {
|
|||||||
NumberAnimation { duration: 150; easing.type: Easing.OutCubic }
|
NumberAnimation { duration: 150; easing.type: Easing.OutCubic }
|
||||||
}
|
}
|
||||||
SequentialAnimation on border.color {
|
SequentialAnimation on border.color {
|
||||||
running: ui.isRecording && !ui.reduceMotion
|
running: ui.isRecording
|
||||||
loops: Animation.Infinite
|
loops: Animation.Infinite
|
||||||
ColorAnimation { from: "#A0ff4b4b"; to: "#C0ff6b6b"; duration: 800 }
|
ColorAnimation { from: "#A0ff4b4b"; to: "#C0ff6b6b"; duration: 800 }
|
||||||
ColorAnimation { from: "#C0ff6b6b"; to: "#A0ff4b4b"; duration: 800 }
|
ColorAnimation { from: "#C0ff6b6b"; to: "#A0ff4b4b"; duration: 800 }
|
||||||
@@ -215,11 +209,6 @@ ApplicationWindow {
|
|||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.leftMargin: 10
|
anchors.leftMargin: 10
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
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
|
// Make entire button scale with amplitude
|
||||||
scale: ui.isRecording ? (1.0 + ui.amplitude * 0.12) : 1.0
|
scale: ui.isRecording ? (1.0 + ui.amplitude * 0.12) : 1.0
|
||||||
@@ -256,7 +245,7 @@ ApplicationWindow {
|
|||||||
border.width: 2; border.color: "#60ffffff"
|
border.width: 2; border.color: "#60ffffff"
|
||||||
|
|
||||||
SequentialAnimation on scale {
|
SequentialAnimation on scale {
|
||||||
running: ui.isRecording && !ui.reduceMotion
|
running: ui.isRecording
|
||||||
loops: Animation.Infinite
|
loops: Animation.Infinite
|
||||||
NumberAnimation { from: 1.0; to: 1.08; duration: 600; easing.type: Easing.InOutQuad }
|
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 }
|
NumberAnimation { from: 1.08; to: 1.0; duration: 600; easing.type: Easing.InOutQuad }
|
||||||
@@ -274,17 +263,6 @@ ApplicationWindow {
|
|||||||
fillMode: Image.PreserveAspectFit
|
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) ---
|
// --- RAINBOW WAVEFORM (Shader) ---
|
||||||
@@ -299,7 +277,6 @@ ApplicationWindow {
|
|||||||
|
|
||||||
ShaderEffect {
|
ShaderEffect {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
visible: !ui.reduceMotion
|
|
||||||
property real time: 0
|
property real time: 0
|
||||||
property real amplitude: ui.amplitude
|
property real amplitude: ui.amplitude
|
||||||
fragmentShader: "rainbow_wave.qsb"
|
fragmentShader: "rainbow_wave.qsb"
|
||||||
@@ -364,10 +341,8 @@ ApplicationWindow {
|
|||||||
font.family: jetBrainsMono.name; font.pixelSize: 16; font.bold: true; font.letterSpacing: 2
|
font.family: jetBrainsMono.name; font.pixelSize: 16; font.bold: true; font.letterSpacing: 2
|
||||||
style: Text.Outline
|
style: Text.Outline
|
||||||
styleColor: ui.isRecording ? "#ff0000" : "#808085"
|
styleColor: ui.isRecording ? "#ff0000" : "#808085"
|
||||||
Accessible.role: Accessible.StaticText
|
|
||||||
Accessible.name: "Recording time: " + text
|
|
||||||
SequentialAnimation on opacity {
|
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: 1.0; to: 0.7; duration: 800 }
|
||||||
NumberAnimation { from: 0.7; to: 1.0; duration: 800 }
|
NumberAnimation { from: 0.7; to: 1.0; duration: 800 }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ Window {
|
|||||||
visible: false
|
visible: false
|
||||||
flags: Qt.FramelessWindowHint | Qt.Window
|
flags: Qt.FramelessWindowHint | Qt.Window
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
title: "WhisperVoice Settings"
|
title: "Settings"
|
||||||
Accessible.name: "WhisperVoice Settings"
|
|
||||||
|
|
||||||
// Explicit sizing for Python to read
|
// Explicit sizing for Python to read
|
||||||
|
|
||||||
@@ -134,20 +133,15 @@ Window {
|
|||||||
// Improved Close Button
|
// Improved Close Button
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: 32; height: 32
|
width: 32; height: 32
|
||||||
activeFocusOnTab: true
|
|
||||||
Accessible.name: "Close settings"
|
|
||||||
Accessible.role: Accessible.Button
|
|
||||||
Keys.onReturnPressed: root.close()
|
|
||||||
Keys.onSpacePressed: root.close()
|
|
||||||
radius: 8
|
radius: 8
|
||||||
color: closeMa.containsMouse ? "#20FF8A8A" : "transparent"
|
color: closeMa.containsMouse ? "#20ff4b4b" : "transparent"
|
||||||
border.color: closeMa.containsMouse ? "#40FF8A8A" : "transparent"
|
border.color: closeMa.containsMouse ? "#40ff4b4b" : "transparent"
|
||||||
border.width: 1
|
border.width: 1
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: "×"
|
text: "×"
|
||||||
color: closeMa.containsMouse ? "#FF8A8A" : SettingsStyle.textSecondary
|
color: closeMa.containsMouse ? "#ff4b4b" : SettingsStyle.textSecondary
|
||||||
font.family: mainFont
|
font.family: mainFont
|
||||||
font.pixelSize: 20
|
font.pixelSize: 20
|
||||||
font.bold: true
|
font.bold: true
|
||||||
@@ -163,15 +157,6 @@ Window {
|
|||||||
|
|
||||||
Behavior on color { ColorAnimation { duration: 150 } }
|
Behavior on color { ColorAnimation { duration: 150 } }
|
||||||
Behavior on border.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
|
height: 38
|
||||||
color: stack.currentIndex === index ? SettingsStyle.surfaceHover : (ma.containsMouse ? Qt.rgba(1,1,1,0.03) : "transparent")
|
color: stack.currentIndex === index ? SettingsStyle.surfaceHover : (ma.containsMouse ? Qt.rgba(1,1,1,0.03) : "transparent")
|
||||||
radius: 6
|
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 } }
|
Behavior on color { ColorAnimation { duration: 150 } }
|
||||||
|
|
||||||
@@ -288,15 +256,6 @@ Window {
|
|||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: stack.currentIndex = index
|
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 ---
|
// --- TAB: GENERAL ---
|
||||||
ScrollView {
|
ScrollView {
|
||||||
Accessible.role: Accessible.PageTab
|
|
||||||
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
||||||
contentWidth: availableWidth
|
contentWidth: availableWidth
|
||||||
|
|
||||||
@@ -425,7 +383,6 @@ Window {
|
|||||||
|
|
||||||
// --- TAB: AUDIO ---
|
// --- TAB: AUDIO ---
|
||||||
ScrollView {
|
ScrollView {
|
||||||
Accessible.role: Accessible.PageTab
|
|
||||||
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
||||||
contentWidth: availableWidth
|
contentWidth: availableWidth
|
||||||
|
|
||||||
@@ -514,7 +471,6 @@ Window {
|
|||||||
|
|
||||||
// --- TAB: VISUALS ---
|
// --- TAB: VISUALS ---
|
||||||
ScrollView {
|
ScrollView {
|
||||||
Accessible.role: Accessible.PageTab
|
|
||||||
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
||||||
contentWidth: availableWidth
|
contentWidth: availableWidth
|
||||||
|
|
||||||
@@ -564,7 +520,7 @@ Window {
|
|||||||
ModernSettingsItem {
|
ModernSettingsItem {
|
||||||
label: "Window Opacity"
|
label: "Window Opacity"
|
||||||
description: "Transparency level"
|
description: "Transparency level"
|
||||||
showSeparator: true
|
showSeparator: false
|
||||||
control: ModernSlider {
|
control: ModernSlider {
|
||||||
Layout.preferredWidth: 200
|
Layout.preferredWidth: 200
|
||||||
from: 0.1; to: 1.0
|
from: 0.1; to: 1.0
|
||||||
@@ -572,15 +528,6 @@ Window {
|
|||||||
onMoved: ui.setSetting("opacity", Number(value.toFixed(2)))
|
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 ---
|
// --- TAB: AI ENGINE ---
|
||||||
ScrollView {
|
ScrollView {
|
||||||
Accessible.role: Accessible.PageTab
|
|
||||||
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
||||||
contentWidth: availableWidth
|
contentWidth: availableWidth
|
||||||
|
|
||||||
@@ -806,8 +752,8 @@ Window {
|
|||||||
}
|
}
|
||||||
color: "#ffffff"
|
color: "#ffffff"
|
||||||
font.family: "JetBrains Mono"
|
font.family: "JetBrains Mono"
|
||||||
font.pixelSize: 11
|
font.pixelSize: 10
|
||||||
opacity: 1.0
|
opacity: 0.7
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
@@ -1094,7 +1040,6 @@ Window {
|
|||||||
|
|
||||||
// --- TAB: DEBUG ---
|
// --- TAB: DEBUG ---
|
||||||
ScrollView {
|
ScrollView {
|
||||||
Accessible.role: Accessible.PageTab
|
|
||||||
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
||||||
contentWidth: availableWidth
|
contentWidth: availableWidth
|
||||||
|
|
||||||
@@ -1120,9 +1065,9 @@ Window {
|
|||||||
spacing: 16
|
spacing: 16
|
||||||
|
|
||||||
StatBox { label: "APP CPU"; value: ui.appCpu; unit: "%"; accent: "#00f2ff" }
|
StatBox { label: "APP CPU"; value: ui.appCpu; unit: "%"; accent: "#00f2ff" }
|
||||||
StatBox { label: "APP RAM"; value: ui.appRamMb; unit: "MB"; accent: "#CAA9FF" }
|
StatBox { label: "APP RAM"; value: ui.appRamMb; unit: "MB"; accent: "#bd93f9" }
|
||||||
StatBox { label: "GPU VRAM"; value: ui.appVramMb; unit: "MB"; accent: "#FF8FD0" }
|
StatBox { label: "GPU VRAM"; value: ui.appVramMb; unit: "MB"; accent: "#ff79c6" }
|
||||||
StatBox { label: "GPU LOAD"; value: ui.appVramPercent; unit: "%"; accent: "#FF8A8A" }
|
StatBox { label: "GPU LOAD"; value: ui.appVramPercent; unit: "%"; accent: "#ff5555" }
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
|
|||||||
@@ -6,14 +6,13 @@ QtObject {
|
|||||||
// Colors
|
// Colors
|
||||||
readonly property color background: "#F2121212" // Deep Obsidian with 95% opacity
|
readonly property color background: "#F2121212" // Deep Obsidian with 95% opacity
|
||||||
readonly property color surfaceCard: "#1A1A1A" // Layer 1
|
readonly property color surfaceCard: "#1A1A1A" // Layer 1
|
||||||
readonly property color surfaceHover: "#2A2A2A" // Layer 2
|
readonly property color surfaceHover: "#2A2A2A" // Layer 2 (Lighter for better contrast)
|
||||||
readonly property color borderSubtle: Qt.rgba(1, 1, 1, 0.22) // WCAG 3:1 non-text contrast
|
readonly property color borderSubtle: Qt.rgba(1, 1, 1, 0.08)
|
||||||
|
|
||||||
readonly property color textPrimary: "#FAFAFA"
|
readonly property color textPrimary: "#FAFAFA" // Brighter white
|
||||||
readonly property color textSecondary: "#ABABAB" // WCAG AAA 8.1:1 on #121212
|
readonly property color textSecondary: "#999999"
|
||||||
readonly property color textDisabled: "#808080" // 4.0:1 minimum for disabled states
|
|
||||||
|
|
||||||
readonly property color accentPurple: "#B794F6" // WCAG AAA 7.2:1 on #121212
|
readonly property color accentPurple: "#7000FF"
|
||||||
readonly property color accentCyan: "#00F2FF"
|
readonly property color accentCyan: "#00F2FF"
|
||||||
|
|
||||||
// Configurable active accent
|
// Configurable active accent
|
||||||
@@ -22,9 +21,5 @@ QtObject {
|
|||||||
// Dimensions
|
// Dimensions
|
||||||
readonly property int cardRadius: 16
|
readonly property int cardRadius: 16
|
||||||
readonly property int itemRadius: 8
|
readonly property int itemRadius: 8
|
||||||
readonly property int itemHeight: 60
|
readonly property int itemHeight: 60 // Even taller for more breathing room
|
||||||
|
|
||||||
// Accessibility
|
|
||||||
readonly property int focusRingWidth: 2
|
|
||||||
readonly property int minTargetSize: 24
|
|
||||||
}
|
}
|
||||||
|
|||||||
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