Remove unused files: old UI, build scripts, fonts, test files
Remove old widget-based Python UI (replaced by QML), unused build scripts, NL/variable/webfont variants, old shaders, PNG icons (replaced by SVGs), and standalone test files. Add build.bat and build.spec for the bootstrapper build system.
This commit is contained in:
7
.gitignore
vendored
7
.gitignore
vendored
@@ -10,13 +10,16 @@ env/
|
||||
# Distribution / Build
|
||||
dist/
|
||||
build/
|
||||
*.spec
|
||||
_unused_files/
|
||||
runtime/
|
||||
|
||||
# Trash / Unused
|
||||
_trash/
|
||||
_unused_files/
|
||||
|
||||
# IDEs
|
||||
.vscode/
|
||||
.idea/
|
||||
.claude/
|
||||
|
||||
# Application Specific
|
||||
models/
|
||||
|
||||
BIN
app_icon.ico
BIN
app_icon.ico
Binary file not shown.
|
Before Width: | Height: | Size: 73 KiB |
31
build.bat
Normal file
31
build.bat
Normal file
@@ -0,0 +1,31 @@
|
||||
@echo off
|
||||
echo ============================================
|
||||
echo Building WhisperVoice Portable EXE
|
||||
echo ============================================
|
||||
echo.
|
||||
|
||||
if not exist venv (
|
||||
echo ERROR: venv not found. Run run_source.bat first.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
call venv\Scripts\activate
|
||||
|
||||
echo Running PyInstaller (single-file bootstrapper)...
|
||||
pyinstaller build.spec --clean --noconfirm
|
||||
|
||||
if %ERRORLEVEL% NEQ 0 (
|
||||
echo.
|
||||
echo BUILD FAILED! Check errors above.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo.
|
||||
echo Build complete!
|
||||
echo.
|
||||
echo Output: dist\WhisperVoice.exe
|
||||
echo.
|
||||
echo This single exe will download all dependencies on first run.
|
||||
pause
|
||||
95
build.spec
Normal file
95
build.spec
Normal file
@@ -0,0 +1,95 @@
|
||||
# -*- 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',
|
||||
)
|
||||
@@ -1,66 +0,0 @@
|
||||
"""
|
||||
Build the Lightweight Bootstrapper
|
||||
==================================
|
||||
|
||||
This creates a small (~15-20MB) .exe that downloads Python + dependencies on first run.
|
||||
"""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import PyInstaller.__main__
|
||||
from pathlib import Path
|
||||
|
||||
def build_bootstrapper():
|
||||
project_root = Path(__file__).parent.absolute()
|
||||
dist_path = project_root / "dist"
|
||||
|
||||
# Collect all app source files to bundle
|
||||
# These will be extracted and used when setting up
|
||||
app_source_files = [
|
||||
("src", "app_source/src"),
|
||||
("assets", "app_source/assets"), # Include icon etc
|
||||
("main.py", "app_source"),
|
||||
("requirements.txt", "app_source"),
|
||||
]
|
||||
|
||||
add_data_args = []
|
||||
for src, dst in app_source_files:
|
||||
src_path = project_root / src
|
||||
if src_path.exists():
|
||||
add_data_args.extend(["--add-data", f"{src}{os.pathsep}{dst}"])
|
||||
|
||||
# Use absolute project root for copying
|
||||
shutil.copy2(project_root / "assets" / "icon.ico", project_root / "app_icon.ico")
|
||||
|
||||
print("🚀 Building Lightweight Bootstrapper...")
|
||||
print("⏳ This creates a small .exe that downloads dependencies on first run.\n")
|
||||
|
||||
PyInstaller.__main__.run([
|
||||
"bootstrapper.py",
|
||||
"--name=WhisperVoice",
|
||||
"--onefile",
|
||||
"--noconsole", # Re-enabled! Error handling in bootstrapper is ready.
|
||||
"--clean",
|
||||
"--icon=app_icon.ico", # Simplified path at root
|
||||
*add_data_args,
|
||||
])
|
||||
|
||||
exe_path = dist_path / "WhisperVoice.exe"
|
||||
if exe_path.exists():
|
||||
size_mb = exe_path.stat().st_size / (1024 * 1024)
|
||||
print("\n" + "="*60)
|
||||
print("✅ BOOTSTRAPPER BUILD COMPLETE!")
|
||||
print("="*60)
|
||||
print(f"\n📍 Output: {exe_path}")
|
||||
print(f"📦 Size: {size_mb:.1f} MB")
|
||||
print("\n📋 How it works:")
|
||||
print(" 1. User runs WhisperVoice.exe")
|
||||
print(" 2. First run: Downloads Python + packages (~2-3GB)")
|
||||
print(" 3. Subsequent runs: Launches instantly")
|
||||
print("\n💡 The 'runtime/' folder will be created next to the .exe")
|
||||
else:
|
||||
print("\n❌ Build failed. Check the output above for errors.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
os.chdir(Path(__file__).parent)
|
||||
build_bootstrapper()
|
||||
@@ -1,17 +0,0 @@
|
||||
@echo off
|
||||
echo Building Whisper Voice Portable EXE...
|
||||
if not exist venv (
|
||||
echo Please run run_source.bat first to setup environment!
|
||||
pause
|
||||
exit /b
|
||||
)
|
||||
|
||||
call venv\Scripts\activate
|
||||
pip install pyinstaller
|
||||
|
||||
echo Running PyInstaller...
|
||||
pyinstaller build.spec --clean --noconfirm
|
||||
|
||||
echo.
|
||||
echo Build Complete! Check dist/WhisperVoice.exe
|
||||
pause
|
||||
@@ -1,14 +0,0 @@
|
||||
from PIL import Image
|
||||
import os
|
||||
|
||||
# Path from the generate_image tool output
|
||||
src = r"C:/Users/lashman/.gemini/antigravity/brain/9a183770-2481-475b-b748-03f4910f9a8e/app_icon_1769195450659.png"
|
||||
dst = r"d:\!!! SYSTEM DATA !!!\Desktop\python crap\whisper_voice\assets\icon.ico"
|
||||
|
||||
if os.path.exists(src):
|
||||
img = Image.open(src)
|
||||
# Resize to standard icon sizes
|
||||
img.save(dst, format='ICO', sizes=[(256, 256)])
|
||||
print(f"Icon saved to {dst}")
|
||||
else:
|
||||
print(f"Source image not found: {src}")
|
||||
@@ -1,43 +0,0 @@
|
||||
import requests
|
||||
import os
|
||||
|
||||
ICONS = {
|
||||
"settings.svg": "https://raw.githubusercontent.com/FortAwesome/Font-Awesome/6.x/svgs/solid/gear.svg",
|
||||
"visibility.svg": "https://raw.githubusercontent.com/FortAwesome/Font-Awesome/6.x/svgs/solid/eye.svg",
|
||||
"smart_toy.svg": "https://raw.githubusercontent.com/FortAwesome/Font-Awesome/6.x/svgs/solid/brain.svg",
|
||||
"microphone.svg": "https://raw.githubusercontent.com/FortAwesome/Font-Awesome/6.x/svgs/solid/microphone.svg"
|
||||
}
|
||||
|
||||
TARGET_DIR = r"d:\!!! SYSTEM DATA !!!\Desktop\python crap\whisper_voice\src\ui\qml"
|
||||
|
||||
def download_icons():
|
||||
if not os.path.exists(TARGET_DIR):
|
||||
print(f"Directory not found: {TARGET_DIR}")
|
||||
return
|
||||
|
||||
for filename, url in ICONS.items():
|
||||
try:
|
||||
print(f"Downloading {filename} from {url}...")
|
||||
response = requests.get(url, timeout=10)
|
||||
response.raise_for_status()
|
||||
|
||||
# Force white fill
|
||||
content = response.text
|
||||
if "<path" in content and "fill=" not in content:
|
||||
content = content.replace("<path", '<path fill="#ffffff"')
|
||||
elif "<path" in content and "fill=" in content:
|
||||
# Regex or simple replace if possible, but simplest is usually just injecting style or checking common FA format
|
||||
pass # FA standard usually has no fill.
|
||||
|
||||
# Additional safety: Replace currentcolor if present
|
||||
content = content.replace("currentColor", "#ffffff")
|
||||
|
||||
filepath = os.path.join(TARGET_DIR, filename)
|
||||
with open(filepath, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
print(f"Saved {filepath} (modified to white)")
|
||||
except Exception as e:
|
||||
print(f"FAILED to download {filename}: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
download_icons()
|
||||
@@ -1,86 +0,0 @@
|
||||
"""
|
||||
Portable Build Script for WhisperVoice.
|
||||
=======================================
|
||||
|
||||
Creates a single-file portable .exe using PyInstaller.
|
||||
All data (settings, models) will be stored next to the .exe at runtime.
|
||||
"""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import PyInstaller.__main__
|
||||
from pathlib import Path
|
||||
|
||||
def build_portable():
|
||||
# 1. Setup Paths
|
||||
project_root = Path(__file__).parent.absolute()
|
||||
dist_path = project_root / "dist"
|
||||
build_path = project_root / "build"
|
||||
|
||||
# 2. Define Assets to bundle (into the .exe)
|
||||
# Format: (Source, Destination relative to bundle root)
|
||||
data_files = [
|
||||
# QML files
|
||||
("src/ui/qml/*.qml", "src/ui/qml"),
|
||||
("src/ui/qml/*.svg", "src/ui/qml"),
|
||||
("src/ui/qml/*.qsb", "src/ui/qml"),
|
||||
("src/ui/qml/fonts/ttf/*.ttf", "src/ui/qml/fonts/ttf"),
|
||||
# Subprocess worker script (CRITICAL for transcription)
|
||||
("src/core/transcribe_worker.py", "src/core"),
|
||||
]
|
||||
|
||||
# Convert to PyInstaller format "--add-data source;dest" (Windows uses ';')
|
||||
add_data_args = []
|
||||
for src, dst in data_files:
|
||||
add_data_args.extend(["--add-data", f"{src}{os.pathsep}{dst}"])
|
||||
|
||||
# 3. Run PyInstaller
|
||||
print("🚀 Starting Portable Build...")
|
||||
print("⏳ This may take 5-10 minutes...")
|
||||
|
||||
PyInstaller.__main__.run([
|
||||
"bootstrapper.py", # Entry point (Tiny Installer)
|
||||
"--name=WhisperVoice", # EXE name
|
||||
"--onefile", # Single EXE
|
||||
"--noconsole", # No terminal window
|
||||
"--clean", # Clean cache
|
||||
|
||||
# Bundle the app source to be extracted by bootstrapper
|
||||
# The bootstrapper expects 'app_source' folder in bundled resources
|
||||
"--add-data", f"src{os.pathsep}app_source/src",
|
||||
"--add-data", f"main.py{os.pathsep}app_source",
|
||||
"--add-data", f"requirements.txt{os.pathsep}app_source",
|
||||
|
||||
# Add assets
|
||||
"--add-data", f"src/ui/qml{os.pathsep}app_source/src/ui/qml",
|
||||
"--add-data", f"assets{os.pathsep}app_source/assets",
|
||||
|
||||
# No heavy collections!
|
||||
# The bootstrapper uses internal pip to install everything.
|
||||
|
||||
# Exclude heavy modules to ensure this exe stays tiny
|
||||
"--exclude-module", "faster_whisper",
|
||||
"--exclude-module", "torch",
|
||||
"--exclude-module", "PySide6",
|
||||
"--exclude-module", "llama_cpp",
|
||||
|
||||
|
||||
# Icon
|
||||
# "--icon=icon.ico",
|
||||
])
|
||||
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("✅ BUILD COMPLETE!")
|
||||
print("="*60)
|
||||
print(f"\n📍 Output: {dist_path / 'WhisperVoice.exe'}")
|
||||
print("\n📋 First run instructions:")
|
||||
print(" 1. Place WhisperVoice.exe in a folder (e.g., C:\\WhisperVoice\\)")
|
||||
print(" 2. Run it - it will create 'models' and 'settings.json' folders")
|
||||
print(" 3. The app will download the Whisper model on first transcription\n")
|
||||
print("💡 TIP: Keep the .exe with its generated files for true portability!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Ensure we are in project root
|
||||
os.chdir(Path(__file__).parent)
|
||||
build_portable()
|
||||
5
run.bat
5
run.bat
@@ -1,5 +0,0 @@
|
||||
@echo off
|
||||
echo [LAUNCHER] Starting Fake Blur UI (Python/Qt)...
|
||||
call venv\Scripts\activate.bat
|
||||
python main.py
|
||||
if %errorlevel% neq 0 pause
|
||||
@@ -1,31 +0,0 @@
|
||||
@echo off
|
||||
echo [DEBUG] LAUNCHER STARTED
|
||||
echo [DEBUG] CWD: %CD%
|
||||
echo [DEBUG] Python Path (expected relative): ..\python\python.exe
|
||||
|
||||
REM Read stdin to a file to verify data input (optional debugging)
|
||||
REM python.exe might be in different relative path depending on where this bat is run
|
||||
REM We assume this bat is in runtime/app/src/core/
|
||||
REM So python is in ../../../python/python.exe
|
||||
|
||||
set PYTHON_EXE=..\..\..\python\python.exe
|
||||
|
||||
if exist "%PYTHON_EXE%" (
|
||||
echo [DEBUG] Found Python at %PYTHON_EXE%
|
||||
) else (
|
||||
echo [ERROR] Python NOT found at %PYTHON_EXE%
|
||||
echo [ERROR] Listing relative directories:
|
||||
dir ..\..\..\
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo [DEBUG] Launching script: transcribe_worker.py
|
||||
"%PYTHON_EXE%" transcribe_worker.py
|
||||
if %ERRORLEVEL% NEQ 0 (
|
||||
echo [ERROR] Python script failed with code %ERRORLEVEL%
|
||||
pause
|
||||
) else (
|
||||
echo [SUCCESS] Script finished.
|
||||
pause
|
||||
)
|
||||
@@ -1,210 +0,0 @@
|
||||
"""
|
||||
Modern Components Library.
|
||||
==========================
|
||||
|
||||
Contains custom-painted widgets that move beyond the standard 'amateur' Qt look.
|
||||
Implements smooth animations, hardware acceleration, and glassmorphism.
|
||||
"""
|
||||
|
||||
from PySide6.QtWidgets import (
|
||||
QPushButton, QWidget, QVBoxLayout, QHBoxLayout,
|
||||
QLabel, QGraphicsDropShadowEffect, QFrame, QAbstractButton
|
||||
)
|
||||
from PySide6.QtCore import Qt, QPropertyAnimation, QEasingCurve, Property, QRect, QPoint, Signal, Slot
|
||||
from PySide6.QtGui import QPainter, QColor, QBrush, QPen, QLinearGradient, QFont
|
||||
|
||||
from src.ui.styles import Theme
|
||||
|
||||
class GlassButton(QPushButton):
|
||||
"""A premium button with gradient hover effects and smooth scaling."""
|
||||
|
||||
def __init__(self, text, parent=None, accent_color=Theme.ACCENT_CYAN):
|
||||
super().__init__(text, parent)
|
||||
self.accent = QColor(accent_color)
|
||||
self.setCursor(Qt.PointingHandCursor)
|
||||
self.setFixedHeight(40)
|
||||
self._hover_opacity = 0.0
|
||||
|
||||
self.setStyleSheet(f"""
|
||||
QPushButton {{
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid {Theme.BORDER_SUBTLE};
|
||||
color: {Theme.TEXT_SECONDARY};
|
||||
border-radius: 8px;
|
||||
padding: 0 20px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}}
|
||||
""")
|
||||
|
||||
# Hover Animation
|
||||
self.anim = QPropertyAnimation(self, b"hover_opacity")
|
||||
self.anim.setDuration(200)
|
||||
self.anim.setStartValue(0.0)
|
||||
self.anim.setEndValue(1.0)
|
||||
self.anim.setEasingCurve(QEasingCurve.OutCubic)
|
||||
|
||||
@Property(float)
|
||||
def hover_opacity(self): return self._hover_opacity
|
||||
|
||||
@hover_opacity.setter
|
||||
def hover_opacity(self, value):
|
||||
self._hover_opacity = value
|
||||
self.update()
|
||||
|
||||
def enterEvent(self, event):
|
||||
self.anim.setDirection(QPropertyAnimation.Forward)
|
||||
self.anim.start()
|
||||
super().enterEvent(event)
|
||||
|
||||
def leaveEvent(self, event):
|
||||
self.anim.setDirection(QPropertyAnimation.Backward)
|
||||
self.anim.start()
|
||||
super().leaveEvent(event)
|
||||
|
||||
def paintEvent(self, event):
|
||||
"""Custom paint for the glow effect."""
|
||||
super().paintEvent(event)
|
||||
if self._hover_opacity > 0:
|
||||
painter = QPainter(self)
|
||||
painter.setRenderHint(QPainter.Antialiasing)
|
||||
|
||||
# Subtle Glow Border
|
||||
color = QColor(self.accent)
|
||||
color.setAlphaF(self._hover_opacity * 0.5)
|
||||
painter.setPen(QPen(color, 1.5))
|
||||
painter.setBrush(Qt.NoBrush)
|
||||
painter.drawRoundedRect(self.rect().adjusted(1,1,-1,-1), 8, 8)
|
||||
|
||||
# Text Glow color shift
|
||||
self.setStyleSheet(f"""
|
||||
QPushButton {{
|
||||
background-color: rgba(255, 255, 255, {0.05 + (self._hover_opacity * 0.05)});
|
||||
border: 1px solid {Theme.BORDER_SUBTLE};
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
padding: 0 20px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}}
|
||||
""")
|
||||
|
||||
class ModernSwitch(QAbstractButton):
|
||||
"""A sleek iOS-style toggle switch."""
|
||||
|
||||
def __init__(self, parent=None, active_color=Theme.ACCENT_GREEN):
|
||||
super().__init__(parent)
|
||||
self.setCheckable(True)
|
||||
self.setFixedSize(44, 24)
|
||||
self._thumb_pos = 3.0
|
||||
self.active_color = QColor(active_color)
|
||||
|
||||
self.anim = QPropertyAnimation(self, b"thumb_pos")
|
||||
self.anim.setDuration(200)
|
||||
self.anim.setEasingCurve(QEasingCurve.InOutCubic)
|
||||
|
||||
@Property(float)
|
||||
def thumb_pos(self): return self._thumb_pos
|
||||
|
||||
@thumb_pos.setter
|
||||
def thumb_pos(self, value):
|
||||
self._thumb_pos = value
|
||||
self.update()
|
||||
|
||||
def nextCheckState(self):
|
||||
super().nextCheckState()
|
||||
self.anim.stop()
|
||||
if self.isChecked():
|
||||
self.anim.setEndValue(23.0)
|
||||
else:
|
||||
self.anim.setEndValue(3.0)
|
||||
self.anim.start()
|
||||
|
||||
def paintEvent(self, event):
|
||||
painter = QPainter(self)
|
||||
painter.setRenderHint(QPainter.Antialiasing)
|
||||
|
||||
# Background
|
||||
bg_color = QColor("#2d2d3d")
|
||||
if self.isChecked():
|
||||
bg_color = self.active_color
|
||||
|
||||
painter.setBrush(bg_color)
|
||||
painter.setPen(Qt.NoPen)
|
||||
painter.drawRoundedRect(self.rect(), 12, 12)
|
||||
|
||||
# Thumb
|
||||
painter.setBrush(Qt.white)
|
||||
painter.drawEllipse(QPoint(self._thumb_pos + 9, 12), 9, 9)
|
||||
|
||||
class ModernFrame(QFrame):
|
||||
"""A base frame with rounded corners and a shadow."""
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setObjectName("premiumFrame")
|
||||
self.setStyleSheet(f"""
|
||||
#premiumFrame {{
|
||||
background-color: {Theme.BG_CARD};
|
||||
border: 1px solid {Theme.BORDER_SUBTLE};
|
||||
border-radius: 12px;
|
||||
}}
|
||||
""")
|
||||
|
||||
self.shadow = QGraphicsDropShadowEffect(self)
|
||||
self.shadow.setBlurRadius(25)
|
||||
self.shadow.setXOffset(0)
|
||||
self.shadow.setYOffset(8)
|
||||
self.shadow.setColor(QColor(0, 0, 0, 180))
|
||||
self.setGraphicsEffect(self.shadow)
|
||||
|
||||
from PySide6.QtWidgets import (
|
||||
QPushButton, QWidget, QVBoxLayout, QHBoxLayout,
|
||||
QLabel, QGraphicsDropShadowEffect, QFrame, QAbstractButton, QSlider
|
||||
)
|
||||
|
||||
class ModernSlider(QSlider):
|
||||
"""A custom painted modern slider with a glowing knob."""
|
||||
def __init__(self, orientation=Qt.Horizontal, parent=None):
|
||||
super().__init__(orientation, parent)
|
||||
self.setStyleSheet(f"""
|
||||
QSlider::groove:horizontal {{
|
||||
border: 1px solid {Theme.BG_DARK};
|
||||
height: 4px;
|
||||
background: {Theme.BG_DARK};
|
||||
margin: 2px 0;
|
||||
border-radius: 2px;
|
||||
}}
|
||||
QSlider::handle:horizontal {{
|
||||
background: {Theme.ACCENT_CYAN};
|
||||
border: 2px solid white;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: -7px 0;
|
||||
border-radius: 8px;
|
||||
}}
|
||||
QSlider::add-page:horizontal {{
|
||||
background: {Theme.BG_DARK};
|
||||
}}
|
||||
QSlider::sub-page:horizontal {{
|
||||
background: {Theme.ACCENT_CYAN};
|
||||
border-radius: 2px;
|
||||
}}
|
||||
""")
|
||||
|
||||
class FramelessWindow(QWidget):
|
||||
"""Base class for all premium windows to handle dragging and frameless logic."""
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.NoDropShadowWindowHint)
|
||||
self.setAttribute(Qt.WA_TranslucentBackground)
|
||||
self._drag_pos = None
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
if event.button() == Qt.LeftButton:
|
||||
self._drag_pos = event.globalPosition().toPoint() - self.frameGeometry().topLeft()
|
||||
event.accept()
|
||||
|
||||
def mouseMoveEvent(self, event):
|
||||
if event.buttons() & Qt.LeftButton:
|
||||
self.move(event.globalPosition().toPoint() - self._drag_pos)
|
||||
event.accept()
|
||||
109
src/ui/loader.py
109
src/ui/loader.py
@@ -1,109 +0,0 @@
|
||||
"""
|
||||
Loader Widget Module.
|
||||
=====================
|
||||
|
||||
Handles the application initialization and model checks.
|
||||
Refactored for 2026 Premium Aesthetics.
|
||||
"""
|
||||
|
||||
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QProgressBar
|
||||
from PySide6.QtCore import Qt, QThread, Signal
|
||||
from PySide6.QtGui import QFont
|
||||
import os
|
||||
import logging
|
||||
from faster_whisper import download_model
|
||||
|
||||
from src.core.paths import get_models_path
|
||||
from src.ui.styles import Theme, StyleGenerator, load_modern_fonts
|
||||
from src.ui.components import FramelessWindow, ModernFrame
|
||||
|
||||
class DownloadWorker(QThread):
|
||||
"""Background worker for model downloads."""
|
||||
progress = Signal(str, int)
|
||||
download_finished = Signal()
|
||||
error = Signal(str)
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
model_path = get_models_path()
|
||||
self.progress.emit("Verifying AI Core...", 10)
|
||||
os.environ["HF_HOME"] = str(model_path)
|
||||
|
||||
self.progress.emit("Downloading Model...", 30)
|
||||
download_model("small", output_dir=str(model_path))
|
||||
|
||||
self.progress.emit("System Ready!", 100)
|
||||
self.download_finished.emit()
|
||||
except Exception as e:
|
||||
logging.error(f"Loader failed: {e}")
|
||||
self.error.emit(str(e))
|
||||
|
||||
class LoaderWidget(FramelessWindow):
|
||||
"""
|
||||
Premium bootstrapper UI.
|
||||
Inherits from FramelessWindow for rounded glass look.
|
||||
"""
|
||||
ready_signal = Signal()
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setFixedSize(400, 180)
|
||||
|
||||
# Main Layout
|
||||
self.root = QVBoxLayout(self)
|
||||
self.root.setContentsMargins(10, 10, 10, 10)
|
||||
|
||||
# Glass Card
|
||||
self.card = ModernFrame()
|
||||
self.card.setStyleSheet(StyleGenerator.get_glass_card(radius=20))
|
||||
self.root.addWidget(self.card)
|
||||
|
||||
# Content Layout
|
||||
self.layout = QVBoxLayout(self.card)
|
||||
self.layout.setContentsMargins(30,30,30,30)
|
||||
self.layout.setSpacing(15)
|
||||
|
||||
# App Title/Brand
|
||||
self.brand = QLabel("WHISPER VOICE")
|
||||
self.brand.setFont(load_modern_fonts())
|
||||
self.brand.setStyleSheet(f"color: {Theme.ACCENT_CYAN}; font-weight: 900; letter-spacing: 4px; font-size: 14px;")
|
||||
self.brand.setAlignment(Qt.AlignCenter)
|
||||
self.layout.addWidget(self.brand)
|
||||
|
||||
# Status Label
|
||||
self.status_label = QLabel("INITIALIZING...")
|
||||
self.status_label.setStyleSheet(f"color: {Theme.TEXT_SECONDARY}; font-weight: 600; font-size: 11px;")
|
||||
self.status_label.setAlignment(Qt.AlignCenter)
|
||||
self.layout.addWidget(self.status_label)
|
||||
|
||||
# Progress Bar (Modern Slim style)
|
||||
self.progress_bar = QProgressBar()
|
||||
self.progress_bar.setFixedHeight(4)
|
||||
self.progress_bar.setStyleSheet(f"""
|
||||
QProgressBar {{
|
||||
background-color: {Theme.BG_DARK};
|
||||
border-radius: 2px;
|
||||
border: none;
|
||||
text-align: center;
|
||||
color: transparent;
|
||||
}}
|
||||
QProgressBar::chunk {{
|
||||
background-color: {Theme.ACCENT_CYAN};
|
||||
border-radius: 2px;
|
||||
}}
|
||||
""")
|
||||
self.layout.addWidget(self.progress_bar)
|
||||
|
||||
# Start Worker
|
||||
self.worker = DownloadWorker()
|
||||
self.worker.progress.connect(self.update_progress)
|
||||
self.worker.download_finished.connect(self.on_finished)
|
||||
self.worker.start()
|
||||
|
||||
def update_progress(self, text: str, percent: int):
|
||||
self.status_label.setText(text.upper())
|
||||
self.progress_bar.setValue(percent)
|
||||
|
||||
def on_finished(self):
|
||||
self.ready_signal.emit()
|
||||
self.close()
|
||||
@@ -1,105 +0,0 @@
|
||||
"""
|
||||
Overlay Window Module.
|
||||
======================
|
||||
|
||||
Premium High-Fidelity Overlay for Whisper Voice.
|
||||
Features glassmorphism, pulsating status indicators, and smart positioning.
|
||||
"""
|
||||
|
||||
from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel
|
||||
from PySide6.QtCore import Qt, Slot, QPoint, QPropertyAnimation, QEasingCurve
|
||||
from PySide6.QtGui import QColor, QFont, QGuiApplication
|
||||
|
||||
from src.ui.visualizer import AudioVisualizer
|
||||
from src.ui.styles import Theme, StyleGenerator, load_modern_fonts
|
||||
from src.ui.components import FramelessWindow, ModernFrame
|
||||
|
||||
class OverlayWindow(FramelessWindow):
|
||||
"""
|
||||
The main transparent overlay (The Pill).
|
||||
Refactored for 2026 Premium Aesthetics.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setFixedSize(320, 95)
|
||||
|
||||
# Main Layout
|
||||
self.master_layout = QVBoxLayout(self)
|
||||
self.master_layout.setContentsMargins(10, 10, 10, 10)
|
||||
|
||||
# The Glass Pill Container
|
||||
self.pill = ModernFrame()
|
||||
self.pill.setStyleSheet(StyleGenerator.get_glass_card(radius=24))
|
||||
self.master_layout.addWidget(self.pill)
|
||||
|
||||
# Layout inside the pill
|
||||
self.layout = QHBoxLayout(self.pill)
|
||||
self.layout.setContentsMargins(20, 10, 20, 10)
|
||||
self.layout.setSpacing(15)
|
||||
|
||||
# Status Visualization (Left Dot)
|
||||
self.status_dot = QWidget()
|
||||
self.status_dot.setFixedSize(14, 14)
|
||||
self.status_dot.setStyleSheet(f"background-color: {Theme.ACCENT_CYAN}; border-radius: 7px; border: 2px solid white;")
|
||||
self.layout.addWidget(self.status_dot)
|
||||
|
||||
# Text/Visualizer Stack
|
||||
self.content_stack = QVBoxLayout()
|
||||
self.content_stack.setSpacing(2)
|
||||
self.content_stack.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.status_label = QLabel("READY")
|
||||
self.status_label.setFont(load_modern_fonts())
|
||||
self.status_label.setStyleSheet(f"color: white; font-weight: 800; font-size: 11px; letter-spacing: 2px;")
|
||||
self.content_stack.addWidget(self.status_label)
|
||||
|
||||
self.visualizer = AudioVisualizer()
|
||||
self.visualizer.setFixedHeight(30)
|
||||
self.content_stack.addWidget(self.visualizer)
|
||||
|
||||
self.layout.addLayout(self.content_stack)
|
||||
|
||||
# Animations
|
||||
self.pulse_timer = None # Use style-based pulsing to avoid window flags issues
|
||||
|
||||
# Initial State
|
||||
self.hide()
|
||||
self.first_show = True
|
||||
|
||||
def showEvent(self, event):
|
||||
"""Handle positioning and config updates."""
|
||||
from src.core.config import ConfigManager
|
||||
config = ConfigManager()
|
||||
self.setWindowOpacity(config.get("opacity"))
|
||||
|
||||
if self.first_show:
|
||||
self.center_above_taskbar()
|
||||
self.first_show = False
|
||||
super().showEvent(event)
|
||||
|
||||
def center_above_taskbar(self):
|
||||
screen = QGuiApplication.primaryScreen()
|
||||
if not screen: return
|
||||
avail_rect = screen.availableGeometry()
|
||||
x = avail_rect.x() + (avail_rect.width() - self.width()) // 2
|
||||
y = avail_rect.bottom() - self.height() - 15
|
||||
self.move(x, y)
|
||||
|
||||
@Slot(str)
|
||||
def update_status(self, text: str):
|
||||
"""Updates the status text and visual indicator."""
|
||||
self.status_label.setText(text.upper())
|
||||
|
||||
if "RECORDING" in text.upper():
|
||||
color = Theme.ACCENT_GREEN
|
||||
elif "THINKING" in text.upper():
|
||||
color = Theme.ACCENT_PURPLE
|
||||
else:
|
||||
color = Theme.ACCENT_CYAN
|
||||
|
||||
self.status_dot.setStyleSheet(f"background-color: {color}; border-radius: 7px; border: 2px solid white;")
|
||||
|
||||
@Slot(float)
|
||||
def update_visualizer(self, amp: float):
|
||||
self.visualizer.set_amplitude(amp)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,50 +0,0 @@
|
||||
#version 440
|
||||
|
||||
layout(location = 0) in vec2 qt_TexCoord0;
|
||||
layout(location = 0) out vec4 fragColor;
|
||||
|
||||
layout(std140, binding = 0) uniform buf {
|
||||
mat4 qt_Matrix;
|
||||
float qt_Opacity;
|
||||
float time;
|
||||
float aberration; // 0.0 to 1.0, controlled by Audio Amplitude
|
||||
};
|
||||
|
||||
float rand(vec2 co) {
|
||||
return fract(sin(dot(co.xy ,vec2(12.9898,78.233))) * 43758.5453);
|
||||
}
|
||||
|
||||
void main() {
|
||||
// 1. Calculate Distortion Offset based on Amplitude (aberration)
|
||||
// We warp the UVs slightly away from center
|
||||
vec2 uv = qt_TexCoord0;
|
||||
vec2 dist = uv - 0.5;
|
||||
|
||||
// 2. Chromatic Aberration
|
||||
// Red Channel shifts OUT
|
||||
// Blue Channel shifts IN
|
||||
float strength = aberration * 0.02; // Max shift 2% of texture size
|
||||
|
||||
vec2 rUV = uv + (dist * strength);
|
||||
vec2 bUV = uv - (dist * strength);
|
||||
|
||||
// Sample texture? We don't have a texture input (source is empty Item), we are generating visuals.
|
||||
// Wait, ShaderEffect usually works on sourceItem.
|
||||
// Here we are generating NOISE on top of a gradient.
|
||||
// So we apply Aberration to the NOISE function?
|
||||
// Or do we want to aberrate the pixels UNDERNEATH?
|
||||
// ShaderEffect with no source property renders purely procedural content.
|
||||
|
||||
// Let's create layered procedural noise with channel offsets
|
||||
float nR = rand(rUV + vec2(time * 0.01, 0.0));
|
||||
float nG = rand(uv + vec2(time * 0.01, 0.0)); // Green is anchor
|
||||
float nB = rand(bUV + vec2(time * 0.01, 0.0));
|
||||
|
||||
// Also modulate alpha by aberration - higher volume = more intense grain?
|
||||
// Or maybe just pure glitch.
|
||||
|
||||
vec4 grainColor = vec4(nR, nG, nB, 1.0);
|
||||
|
||||
// Mix it with opacity
|
||||
fragColor = grainColor * qt_Opacity;
|
||||
}
|
||||
Binary file not shown.
@@ -1,25 +0,0 @@
|
||||
#version 440
|
||||
|
||||
layout(location = 0) in vec2 qt_TexCoord0;
|
||||
layout(location = 0) out vec4 fragColor;
|
||||
|
||||
layout(std140, binding = 0) uniform buf {
|
||||
mat4 qt_Matrix;
|
||||
float qt_Opacity;
|
||||
float time;
|
||||
};
|
||||
|
||||
// High-quality pseudo-random function
|
||||
float rand(vec2 co) {
|
||||
return fract(sin(dot(co.xy ,vec2(12.9898,78.233))) * 43758.5453);
|
||||
}
|
||||
|
||||
void main() {
|
||||
// Dynamic Noise based on Time
|
||||
// We add 'time' to the coordinate to animate the grain
|
||||
float noise = rand(qt_TexCoord0 + vec2(time * 0.01, time * 0.02));
|
||||
|
||||
// Output grayscale noise with alpha modulation
|
||||
// We want white noise, applied with qt_Opacity
|
||||
fragColor = vec4(noise, noise, noise, 1.0) * qt_Opacity;
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 492 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 490 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 464 KiB |
@@ -1,236 +0,0 @@
|
||||
"""
|
||||
Settings Window Module.
|
||||
=======================
|
||||
|
||||
Manages the application configuration UI.
|
||||
Refactored for 2026 Premium Aesthetics with Sidebar navigation.
|
||||
"""
|
||||
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QStackedWidget,
|
||||
QLabel, QComboBox, QFormLayout, QFrame, QMessageBox, QScrollArea
|
||||
)
|
||||
from PySide6.QtCore import Qt, Signal, Slot, QSize
|
||||
from PySide6.QtGui import QFont, QIcon
|
||||
|
||||
from src.core.config import ConfigManager
|
||||
from src.ui.styles import Theme, StyleGenerator, load_modern_fonts
|
||||
from src.ui.components import FramelessWindow, ModernFrame, GlassButton, ModernSwitch, ModernSlider
|
||||
import sounddevice as sd
|
||||
|
||||
class SettingsWindow(FramelessWindow):
|
||||
"""
|
||||
The main settings dialog.
|
||||
Refactored with 2026 Premium Sidebar Layout.
|
||||
"""
|
||||
settings_changed = Signal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.config = ConfigManager()
|
||||
self.setFixedSize(700, 500)
|
||||
|
||||
# Main Container
|
||||
self.bg_frame = ModernFrame()
|
||||
self.bg_frame.setStyleSheet(StyleGenerator.get_glass_card(radius=20))
|
||||
|
||||
self.root_layout = QVBoxLayout(self)
|
||||
self.root_layout.setContentsMargins(10, 10, 10, 10)
|
||||
self.root_layout.addWidget(self.bg_frame)
|
||||
|
||||
# Title Bar Area (Inside glass card)
|
||||
self.title_layout = QHBoxLayout()
|
||||
self.title_layout.setContentsMargins(20, 15, 20, 0)
|
||||
|
||||
title_lbl = QLabel("PREMIUM SETTINGS")
|
||||
title_lbl.setFont(load_modern_fonts())
|
||||
title_lbl.setStyleSheet(f"color: white; font-weight: 900; font-size: 14px; letter-spacing: 2px;")
|
||||
self.title_layout.addWidget(title_lbl)
|
||||
|
||||
self.title_layout.addStretch()
|
||||
|
||||
self.btn_close = GlassButton("×", accent_color="#ff4b4b")
|
||||
self.btn_close.setFixedSize(30, 30)
|
||||
self.btn_close.clicked.connect(self.close)
|
||||
self.title_layout.addWidget(self.btn_close)
|
||||
|
||||
# Central Layout (Sidebar + Content)
|
||||
self.content_layout = QHBoxLayout()
|
||||
self.content_layout.setContentsMargins(10, 10, 10, 10)
|
||||
self.content_layout.setSpacing(10)
|
||||
|
||||
# 1. SIDEBAR
|
||||
self.sidebar = QWidget()
|
||||
self.sidebar.setFixedWidth(160)
|
||||
self.sidebar_layout = QVBoxLayout(self.sidebar)
|
||||
self.sidebar_layout.setContentsMargins(0, 10, 0, 10)
|
||||
self.sidebar_layout.setSpacing(8)
|
||||
|
||||
self.nav_general = GlassButton("General")
|
||||
self.nav_audio = GlassButton("Audio")
|
||||
self.nav_visuals = GlassButton("Visuals")
|
||||
self.nav_advanced = GlassButton("Advanced/AI")
|
||||
|
||||
self.sidebar_layout.addWidget(self.nav_general)
|
||||
self.sidebar_layout.addWidget(self.nav_audio)
|
||||
self.sidebar_layout.addWidget(self.nav_visuals)
|
||||
self.sidebar_layout.addWidget(self.nav_advanced)
|
||||
self.sidebar_layout.addStretch()
|
||||
|
||||
self.btn_save = GlassButton("SAVE CHANGES", accent_color=Theme.ACCENT_GREEN)
|
||||
self.btn_save.clicked.connect(self.save_settings)
|
||||
self.sidebar_layout.addWidget(self.btn_save)
|
||||
|
||||
# 2. CONTENT STACK
|
||||
self.stack = QStackedWidget()
|
||||
self.stack.setStyleSheet("background: transparent;")
|
||||
|
||||
# Connect sidebar to stack
|
||||
self.nav_general.clicked.connect(lambda: self.stack.setCurrentIndex(0))
|
||||
self.nav_audio.clicked.connect(lambda: self.stack.setCurrentIndex(1))
|
||||
self.nav_visuals.clicked.connect(lambda: self.stack.setCurrentIndex(2))
|
||||
self.nav_advanced.clicked.connect(lambda: self.stack.setCurrentIndex(3))
|
||||
|
||||
# Main Layout Assembly
|
||||
self.inner_layout = QVBoxLayout(self.bg_frame)
|
||||
self.inner_layout.addLayout(self.title_layout)
|
||||
self.inner_layout.addLayout(self.content_layout)
|
||||
|
||||
self.content_layout.addWidget(self.sidebar)
|
||||
self.content_layout.addWidget(self.stack)
|
||||
|
||||
self.setup_pages()
|
||||
self.load_values()
|
||||
|
||||
def setup_pages(self):
|
||||
"""Creates the settings pages."""
|
||||
# --- GENERAL ---
|
||||
self.page_general = QWidget()
|
||||
l1 = QFormLayout(self.page_general)
|
||||
l1.setVerticalSpacing(20)
|
||||
|
||||
self.inp_hotkey = QComboBox()
|
||||
self.inp_hotkey.addItems(["f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", "f10", "f11", "f12", "caps lock"])
|
||||
self.inp_hotkey.setStyleSheet(f"background: {Theme.BG_DARK}; border-radius: 4px; padding: 5px; color: white;")
|
||||
l1.addRow(self.create_lbl("Global Hotkey:"), self.inp_hotkey)
|
||||
|
||||
self.chk_top = ModernSwitch()
|
||||
l1.addRow(self.create_lbl("Always on Top:"), self.chk_top)
|
||||
|
||||
self.stack.addWidget(self.page_general)
|
||||
|
||||
# --- AUDIO ---
|
||||
self.page_audio = QWidget()
|
||||
l2 = QFormLayout(self.page_audio)
|
||||
l2.setVerticalSpacing(15)
|
||||
|
||||
self.inp_device = QComboBox()
|
||||
self.inp_device.setStyleSheet(f"background: {Theme.BG_DARK}; border-radius: 4px; padding: 5px; color: white;")
|
||||
self.populate_audio_devices()
|
||||
l2.addRow(self.create_lbl("Input Device:"), self.inp_device)
|
||||
|
||||
self.sld_threshold = ModernSlider(Qt.Horizontal)
|
||||
self.sld_threshold.setRange(1, 25)
|
||||
self.lbl_threshold = self.create_lbl("2%")
|
||||
self.sld_threshold.valueChanged.connect(lambda v: self.lbl_threshold.setText(f"{v}%"))
|
||||
l2.addRow(self.create_lbl("Noise Gate:"), self.sld_threshold)
|
||||
l2.addRow("", self.lbl_threshold)
|
||||
|
||||
self.sld_duration = ModernSlider(Qt.Horizontal)
|
||||
self.sld_duration.setRange(5, 50)
|
||||
self.lbl_duration = self.create_lbl("1.0s")
|
||||
self.sld_duration.valueChanged.connect(lambda v: self.lbl_duration.setText(f"{v/10}s"))
|
||||
l2.addRow(self.create_lbl("Auto-Submit:"), self.sld_duration)
|
||||
l2.addRow("", self.lbl_duration)
|
||||
|
||||
self.stack.addWidget(self.page_audio)
|
||||
|
||||
# --- VISUALS ---
|
||||
self.page_visuals = QWidget()
|
||||
l3 = QFormLayout(self.page_visuals)
|
||||
l3.setVerticalSpacing(20)
|
||||
|
||||
self.inp_style = QComboBox()
|
||||
self.inp_style.addItem("Neon Line (Recommended)", "line")
|
||||
self.inp_style.addItem("Classic Bars", "bar")
|
||||
self.inp_style.setStyleSheet(f"background: {Theme.BG_DARK}; border-radius: 4px; padding: 5px; color: white;")
|
||||
l3.addRow(self.create_lbl("Visualizer:"), self.inp_style)
|
||||
|
||||
self.sld_opacity = ModernSlider(Qt.Horizontal)
|
||||
self.sld_opacity.setRange(40, 100)
|
||||
self.lbl_opacity = self.create_lbl("100%")
|
||||
self.sld_opacity.valueChanged.connect(lambda v: self.lbl_opacity.setText(f"{v}%"))
|
||||
l3.addRow(self.create_lbl("Opacity:"), self.sld_opacity)
|
||||
l3.addRow("", self.lbl_opacity)
|
||||
|
||||
self.stack.addWidget(self.page_visuals)
|
||||
|
||||
# --- ADVANCED ---
|
||||
self.page_adv = QWidget()
|
||||
l4 = QFormLayout(self.page_adv)
|
||||
l4.setVerticalSpacing(15)
|
||||
|
||||
self.inp_model = QComboBox()
|
||||
self.inp_model.setStyleSheet(f"background: {Theme.BG_DARK}; border-radius: 4px; padding: 5px; color: white;")
|
||||
for id, name in [("tiny", "Tiny (Fast)"), ("base", "Base"), ("small", "Small (Default)"), ("medium", "Medium"), ("large-v3", "Large V3")]:
|
||||
self.inp_model.addItem(name, id)
|
||||
l4.addRow(self.create_lbl("Model:"), self.inp_model)
|
||||
|
||||
info = QLabel("Large models provide higher accuracy but require significant RAM/VRAM.")
|
||||
info.setWordWrap(True)
|
||||
info.setStyleSheet(f"color: {Theme.TEXT_SECONDARY}; font-style: italic; font-size: 11px;")
|
||||
l4.addRow("", info)
|
||||
|
||||
self.stack.addWidget(self.page_adv)
|
||||
|
||||
def create_lbl(self, text):
|
||||
lbl = QLabel(text)
|
||||
lbl.setStyleSheet(f"color: {Theme.TEXT_SECONDARY}; font-weight: 600; font-size: 13px;")
|
||||
return lbl
|
||||
|
||||
def populate_audio_devices(self):
|
||||
try:
|
||||
self.inp_device.addItem("System Default", -1)
|
||||
for i, dev in enumerate(sd.query_devices()):
|
||||
if dev['max_input_channels'] > 0:
|
||||
self.inp_device.addItem(dev['name'], i)
|
||||
except: pass
|
||||
|
||||
def load_values(self):
|
||||
self.inp_hotkey.setCurrentText(self.config.get("hotkey"))
|
||||
self.chk_top.setChecked(self.config.get("always_on_top"))
|
||||
|
||||
dev_id = self.config.get("input_device")
|
||||
idx = self.inp_device.findData(dev_id if dev_id is not None else -1)
|
||||
if idx >= 0: self.inp_device.setCurrentIndex(idx)
|
||||
|
||||
self.sld_threshold.setValue(int(self.config.get("silence_threshold") * 100))
|
||||
self.sld_duration.setValue(int(self.config.get("silence_duration") * 10))
|
||||
|
||||
idx = self.inp_style.findData(self.config.get("visualizer_style"))
|
||||
if idx >= 0: self.inp_style.setCurrentIndex(idx)
|
||||
|
||||
self.sld_opacity.setValue(int(self.config.get("opacity") * 100))
|
||||
|
||||
idx = self.inp_model.findData(self.config.get("model_size"))
|
||||
if idx >= 0: self.inp_model.setCurrentIndex(idx)
|
||||
|
||||
def save_settings(self):
|
||||
updates = {
|
||||
"hotkey": self.inp_hotkey.currentText(),
|
||||
"always_on_top": self.chk_top.isChecked(),
|
||||
"input_device": self.inp_device.currentData() if self.inp_device.currentData() != -1 else None,
|
||||
"silence_threshold": self.sld_threshold.value() / 100.0,
|
||||
"silence_duration": self.sld_duration.value() / 10.0,
|
||||
"visualizer_style": self.inp_style.currentData(),
|
||||
"opacity": self.sld_opacity.value() / 100.0,
|
||||
"model_size": self.inp_model.currentData()
|
||||
}
|
||||
|
||||
new_model = updates["model_size"]
|
||||
if new_model != self.config.get("model_size"):
|
||||
QMessageBox.information(self, "Model Updated", f"Downloaded {new_model} on next launch.")
|
||||
|
||||
self.config.set_bulk(updates)
|
||||
self.settings_changed.emit()
|
||||
self.close()
|
||||
@@ -1,62 +0,0 @@
|
||||
"""
|
||||
Style Engine Module.
|
||||
====================
|
||||
|
||||
Centralized design system for the 2026 Premium UI.
|
||||
Defines color palettes, glassmorphism templates, and modern font loading.
|
||||
"""
|
||||
|
||||
from PySide6.QtGui import QColor, QFont, QFontDatabase
|
||||
import os
|
||||
|
||||
class Theme:
|
||||
"""Premium Dark Theme Palette (2026 Edition)."""
|
||||
# Backgrounds
|
||||
BG_DARK = "#0d0d12" # Deep cosmic black
|
||||
BG_CARD = "#16161e" # Slightly lighter for components
|
||||
BG_GLASS = "rgba(22, 22, 30, 0.7)" # Semi-transparent for glass effect
|
||||
|
||||
# Neons & Accents
|
||||
ACCENT_CYAN = "#00f2ff" # Electric cyan
|
||||
ACCENT_PURPLE = "#7000ff" # Deep cyber purple
|
||||
ACCENT_GREEN = "#00ff88" # Mint neon
|
||||
|
||||
# Text
|
||||
TEXT_PRIMARY = "#ffffff" # Pure white
|
||||
TEXT_SECONDARY = "#9499b0" # Muted blue-gray
|
||||
TEXT_MUTED = "#565f89" # Darker blue-gray
|
||||
|
||||
# Borders
|
||||
BORDER_SUBTLE = "rgba(100, 100, 150, 0.2)"
|
||||
BORDER_GLOW = "rgba(0, 242, 255, 0.5)"
|
||||
|
||||
class StyleGenerator:
|
||||
"""Generates QSS strings for complex effects."""
|
||||
|
||||
@staticmethod
|
||||
def get_glass_card(radius=12, border=True):
|
||||
"""Returns QSS for a glassmorphism card."""
|
||||
border_css = f"border: 1px solid {Theme.BORDER_SUBTLE};" if border else "border: none;"
|
||||
return f"""
|
||||
background-color: {Theme.BG_GLASS};
|
||||
border-radius: {radius}px;
|
||||
{border_css}
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_glow_border(color=Theme.ACCENT_CYAN):
|
||||
"""Returns QSS for a glowing border state."""
|
||||
return f"border: 1px solid {color};"
|
||||
|
||||
def load_modern_fonts():
|
||||
"""Attempts to load a modern font stack for the 2026 look."""
|
||||
# Preferred order: Segoe UI Variable, Inter, Segoe UI, sans-serif
|
||||
families = ["Segoe UI Variable Text", "Inter", "Segoe UI", "sans-serif"]
|
||||
|
||||
for family in families:
|
||||
font = QFont(family, 10)
|
||||
if QFontDatabase.families().count(family) > 0:
|
||||
return font
|
||||
|
||||
# Absolute fallback
|
||||
return QFont("Arial", 10)
|
||||
@@ -1,117 +0,0 @@
|
||||
"""
|
||||
Audio Visualizer Module.
|
||||
========================
|
||||
|
||||
High-Fidelity rendering for the 2026 Premium UI.
|
||||
Supports 'Classic Bars' and 'Neon Line' with smooth curves and glows.
|
||||
"""
|
||||
|
||||
from PySide6.QtWidgets import QWidget
|
||||
from PySide6.QtCore import Qt, QTimer, Slot, QRectF, QPointF
|
||||
from PySide6.QtGui import QPainter, QBrush, QColor, QPainterPath, QPen, QLinearGradient
|
||||
import random
|
||||
|
||||
from src.ui.styles import Theme
|
||||
|
||||
class AudioVisualizer(QWidget):
|
||||
"""
|
||||
A premium audio visualizer with smooth physics and neon aesthetics.
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.amplitude = 0.0
|
||||
self.bars = 12
|
||||
self.history = [0.0] * self.bars
|
||||
|
||||
# High-refresh timer for silky smooth motion
|
||||
self.timer = QTimer(self)
|
||||
self.timer.timeout.connect(self.update_animation)
|
||||
self.timer.start(16) # ~60 FPS
|
||||
|
||||
@Slot(float)
|
||||
def set_amplitude(self, amp: float):
|
||||
self.amplitude = amp
|
||||
|
||||
def update_animation(self):
|
||||
self.history.pop(0)
|
||||
# Smooth interpolation + noise
|
||||
jitter = random.uniform(0.01, 0.03)
|
||||
# Decay logic: Gravity-like pull
|
||||
self.history.append(max(self.amplitude, jitter))
|
||||
self.update()
|
||||
|
||||
def paintEvent(self, event):
|
||||
from src.core.config import ConfigManager
|
||||
style = ConfigManager().get("visualizer_style")
|
||||
|
||||
painter = QPainter(self)
|
||||
painter.setRenderHint(QPainter.Antialiasing)
|
||||
|
||||
w, h = self.width(), self.height()
|
||||
painter.translate(0, h / 2)
|
||||
|
||||
if style == "bar":
|
||||
self._draw_bars(painter, w, h)
|
||||
else:
|
||||
self._draw_line(painter, w, h)
|
||||
|
||||
def _draw_bars(self, painter, w, h):
|
||||
bar_w = w / self.bars
|
||||
spacing = 3
|
||||
|
||||
for i, val in enumerate(self.history):
|
||||
bar_h = val * (h * 0.9)
|
||||
x = i * bar_w
|
||||
|
||||
# Gradient Bar
|
||||
grad = QLinearGradient(0, -bar_h/2, 0, bar_h/2)
|
||||
grad.setColorAt(0, QColor(Theme.ACCENT_PURPLE))
|
||||
grad.setColorAt(1, QColor(Theme.ACCENT_CYAN))
|
||||
|
||||
painter.setBrush(grad)
|
||||
painter.setPen(Qt.NoPen)
|
||||
painter.drawRoundedRect(QRectF(x + spacing, -bar_h/2, bar_w - spacing*2, bar_h), 3, 3)
|
||||
|
||||
def _draw_line(self, painter, w, h):
|
||||
path = QPainterPath()
|
||||
points = len(self.history)
|
||||
dx = w / (points - 1)
|
||||
|
||||
path.moveTo(0, 0)
|
||||
|
||||
def get_path(multi):
|
||||
p = QPainterPath()
|
||||
p.moveTo(0, 0)
|
||||
for i in range(points):
|
||||
curr_x = i * dx
|
||||
curr_y = -self.history[i] * (h * 0.45) * multi
|
||||
if i == 0:
|
||||
p.moveTo(curr_x, curr_y)
|
||||
else:
|
||||
prev_x = (i-1) * dx
|
||||
# Simple lerp or quadTo for smoothness
|
||||
p.lineTo(curr_x, curr_y)
|
||||
return p
|
||||
|
||||
# Draw Top & Bottom
|
||||
p_top = get_path(1)
|
||||
p_bot = get_path(-1)
|
||||
|
||||
# Glow layer
|
||||
glow_pen = QPen(QColor(Theme.ACCENT_CYAN))
|
||||
glow_pen.setWidth(4)
|
||||
glow_alpha = QColor(Theme.ACCENT_CYAN)
|
||||
glow_alpha.setAlpha(60)
|
||||
glow_pen.setColor(glow_alpha)
|
||||
|
||||
painter.setPen(glow_pen)
|
||||
painter.drawPath(p_top)
|
||||
painter.drawPath(p_bot)
|
||||
|
||||
# Core layer
|
||||
core_pen = QPen(Qt.white)
|
||||
core_pen.setWidth(2)
|
||||
painter.setPen(core_pen)
|
||||
painter.drawPath(p_top)
|
||||
painter.drawPath(p_bot)
|
||||
38
test_m2m.py
38
test_m2m.py
@@ -1,38 +0,0 @@
|
||||
|
||||
import sys
|
||||
from transformers import M2M100ForConditionalGeneration, M2M100Tokenizer
|
||||
|
||||
def test_m2m():
|
||||
model_name = "facebook/m2m100_418M"
|
||||
print(f"Loading {model_name}...")
|
||||
|
||||
tokenizer = M2M100Tokenizer.from_pretrained(model_name)
|
||||
model = M2M100ForConditionalGeneration.from_pretrained(model_name)
|
||||
|
||||
# Test cases: (Language Code, Input)
|
||||
test_cases = [
|
||||
("en", "he go to school yesterday"),
|
||||
("pl", "on iść do szkoła wczoraj"), # Intentional broken grammar in Polish
|
||||
]
|
||||
|
||||
print("\nStarting M2M Tests (Self-Translation):\n")
|
||||
|
||||
for lang, input_text in test_cases:
|
||||
tokenizer.src_lang = lang
|
||||
encoded = tokenizer(input_text, return_tensors="pt")
|
||||
|
||||
# Translate to SAME language
|
||||
generated_tokens = model.generate(
|
||||
**encoded,
|
||||
forced_bos_token_id=tokenizer.get_lang_id(lang)
|
||||
)
|
||||
|
||||
corrected = tokenizer.batch_decode(generated_tokens, skip_special_tokens=True)[0]
|
||||
|
||||
print(f"[{lang}]")
|
||||
print(f"Input: {input_text}")
|
||||
print(f"Output: {corrected}")
|
||||
print("-" * 20)
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_m2m()
|
||||
40
test_mt0.py
40
test_mt0.py
@@ -1,40 +0,0 @@
|
||||
|
||||
import sys
|
||||
from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
|
||||
|
||||
def test_mt0():
|
||||
model_name = "bigscience/mt0-base"
|
||||
print(f"Loading {model_name}...")
|
||||
|
||||
tokenizer = AutoTokenizer.from_pretrained(model_name)
|
||||
model = AutoModelForSeq2SeqLM.from_pretrained(model_name)
|
||||
|
||||
# Test cases: (Language, Prompt, Input)
|
||||
# MT0 is instruction tuned, so we should prompt it in the target language or English.
|
||||
# Cross-lingual prompting (English prompt -> Target tasks) is usually supported.
|
||||
|
||||
test_cases = [
|
||||
("English", "Correct grammar:", "he go to school yesterday"),
|
||||
("Polish", "Popraw gramatykę:", "to jest testowe zdanie bez kropki"),
|
||||
("Finnish", "Korjaa kielioppi:", "tämä on testilause ilman pistettä"),
|
||||
("Russian", "Исправь грамматику:", "это тестовое предложение без точки"),
|
||||
("Japanese", "文法を直してください:", "これは点のないテスト文です"),
|
||||
("Spanish", "Corrige la gramática:", "esta es una oración de prueba sin punto"),
|
||||
]
|
||||
|
||||
print("\nStarting MT0 Tests:\n")
|
||||
|
||||
for lang, prompt_text, input_text in test_cases:
|
||||
full_input = f"{prompt_text} {input_text}"
|
||||
inputs = tokenizer(full_input, return_tensors="pt")
|
||||
|
||||
outputs = model.generate(inputs.input_ids, max_length=128)
|
||||
corrected = tokenizer.decode(outputs[0], skip_special_tokens=True)
|
||||
|
||||
print(f"[{lang}]")
|
||||
print(f"Input: {full_input}")
|
||||
print(f"Output: {corrected}")
|
||||
print("-" * 20)
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_mt0()
|
||||
@@ -1,34 +0,0 @@
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add src to path
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from src.core.grammar_assistant import GrammarAssistant
|
||||
|
||||
def test_punctuation():
|
||||
assistant = GrammarAssistant()
|
||||
assistant.load_model()
|
||||
|
||||
samples = [
|
||||
# User's example (verbatim)
|
||||
"If the voice recognition doesn't recognize that I like stopped Or something would that would it also correct that",
|
||||
|
||||
# Generic run-on
|
||||
"hello how are you doing today i am doing fine thanks for asking",
|
||||
|
||||
# Missing commas/periods
|
||||
"well i think its valid however we should probably check the logs first"
|
||||
]
|
||||
|
||||
print("\nStarting Punctuation Tests:\n")
|
||||
|
||||
for sample in samples:
|
||||
print(f"Original: {sample}")
|
||||
corrected = assistant.correct(sample)
|
||||
print(f"Corrected: {corrected}")
|
||||
print("-" * 20)
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_punctuation()
|
||||
Reference in New Issue
Block a user