Add unified per-speaker font support and remote transcription service

Font changes:
- Consolidate font settings into single Display Settings section
- Support Web-Safe, Google Fonts, and Custom File uploads for both displays
- Fix Google Fonts URL encoding (use + instead of %2B for spaces)
- Fix per-speaker font inline style quote escaping in Node.js display
- Add font debug logging to help diagnose font issues
- Update web server to sync all font settings on settings change
- Remove deprecated PHP server documentation files

New features:
- Add remote transcription service for GPU offloading
- Add instance lock to prevent multiple app instances
- Add version tracking

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-11 18:56:12 -08:00
parent f035bdb927
commit ff067b3368
23 changed files with 2486 additions and 1160 deletions

108
main.py
View File

@@ -41,43 +41,68 @@ if getattr(sys, 'frozen', False) and sys.platform == 'win32':
sys.stderr = io.StringIO()
# Add project root to Python path
project_root = Path(__file__).parent
# Use resolve() to follow symlinks and get the real path
project_root = Path(__file__).resolve().parent
sys.path.insert(0, str(project_root))
from PySide6.QtWidgets import QApplication, QSplashScreen
from PySide6.QtGui import QPixmap, QPainter, QColor, QFont
from PySide6.QtCore import Qt, QTimer
from gui.main_window_qt import MainWindow
# Change working directory to project root so relative paths work
os.chdir(project_root)
# Import only minimal Qt components needed for splash and dialogs
# Heavy imports (MainWindow) are deferred until after splash is shown
from PySide6.QtWidgets import QApplication, QSplashScreen, QMessageBox
from PySide6.QtGui import QPixmap, QPainter, QColor, QFont, QIcon
from PySide6.QtCore import Qt
# Import single instance lock (lightweight module)
from client.instance_lock import InstanceLock
def get_icon_path():
"""Get the application icon path."""
if getattr(sys, 'frozen', False):
# Running in PyInstaller bundle
return Path(sys._MEIPASS) / "LocalTranscription.png"
else:
# Running in normal Python
return project_root / "LocalTranscription.png"
def create_splash_pixmap(message="Loading..."):
"""Create a pixmap for the splash screen with a custom message."""
pixmap = QPixmap(500, 300)
"""Create a pixmap for the splash screen with the app icon."""
pixmap = QPixmap(400, 320)
pixmap.fill(QColor("#2b2b2b"))
# Draw on the pixmap
painter = QPainter(pixmap)
painter.setRenderHint(QPainter.Antialiasing)
painter.setRenderHint(QPainter.SmoothPixmapTransform)
# Draw title
title_font = QFont("Arial", 28, QFont.Bold)
painter.setFont(title_font)
painter.setPen(QColor("#ffffff"))
painter.drawText(pixmap.rect(), Qt.AlignCenter, "Local Transcription")
# Load and draw the icon
icon_path = get_icon_path()
if icon_path.exists():
icon_pixmap = QPixmap(str(icon_path))
# Scale icon to fit nicely (200x200)
scaled_icon = icon_pixmap.scaled(200, 200, Qt.KeepAspectRatio, Qt.SmoothTransformation)
# Center the icon horizontally, position it in upper portion
icon_x = (pixmap.width() - scaled_icon.width()) // 2
icon_y = 30
painter.drawPixmap(icon_x, icon_y, scaled_icon)
# Draw subtitle
# Draw loading message below icon
subtitle_font = QFont("Arial", 12)
painter.setFont(subtitle_font)
painter.setPen(QColor("#888888"))
subtitle_rect = pixmap.rect().adjusted(0, 60, 0, 0)
painter.drawText(subtitle_rect, Qt.AlignCenter, message)
subtitle_rect = pixmap.rect().adjusted(0, 0, 0, -40)
painter.drawText(subtitle_rect, Qt.AlignHCenter | Qt.AlignBottom, message)
# Draw version/status at bottom
from version import __version__
status_font = QFont("Arial", 10)
painter.setFont(status_font)
painter.setPen(QColor("#666666"))
status_rect = pixmap.rect().adjusted(0, 0, 0, -20)
painter.drawText(status_rect, Qt.AlignHCenter | Qt.AlignBottom, "Please wait...")
status_rect = pixmap.rect().adjusted(0, 0, 0, -15)
painter.drawText(status_rect, Qt.AlignHCenter | Qt.AlignBottom, f"v{__version__}")
painter.end()
return pixmap
@@ -93,11 +118,14 @@ def create_splash_screen():
def main():
"""Main application entry point."""
# Instance lock for cleanup on exit
instance_lock = None
try:
print("Starting Local Transcription Application...")
print("=" * 50)
# Create Qt application
# Create Qt application first (needed for dialogs)
app = QApplication(sys.argv)
# Set application info
@@ -105,19 +133,24 @@ def main():
app.setOrganizationName("LocalTranscription")
# Set application icon
# In PyInstaller frozen executables, use _MEIPASS for bundled files
if getattr(sys, 'frozen', False):
# Running in PyInstaller bundle
icon_path = Path(sys._MEIPASS) / "LocalTranscription.png"
else:
# Running in normal Python
icon_path = project_root / "LocalTranscription.png"
icon_path = get_icon_path()
if icon_path.exists():
from PySide6.QtGui import QIcon
app.setWindowIcon(QIcon(str(icon_path)))
# Create and show splash screen
# Check for single instance BEFORE showing splash
instance_lock = InstanceLock()
if not instance_lock.acquire():
# Another instance is already running
QMessageBox.warning(
None,
"Application Already Running",
"Local Transcription is already running.\n\n"
"Please check your taskbar or system tray for the existing instance.",
QMessageBox.Ok
)
sys.exit(0)
# Create and show splash screen IMMEDIATELY
splash = create_splash_screen()
splash.show()
app.processEvents() # Make sure splash is visible
@@ -126,6 +159,13 @@ def main():
splash.showMessage("Loading configuration...", Qt.AlignBottom | Qt.AlignCenter, QColor("#888888"))
app.processEvents()
# NOW import heavy modules (after splash is visible)
# This is the slow part - importing MainWindow loads many dependencies
splash.showMessage("Loading application modules...", Qt.AlignBottom | Qt.AlignCenter, QColor("#888888"))
app.processEvents()
from gui.main_window_qt import MainWindow
# Create main window (this takes time due to model loading)
# Pass splash to window so it can update the message
window = MainWindow(splash_screen=splash)
@@ -135,15 +175,25 @@ def main():
window.show()
# Run application
sys.exit(app.exec())
exit_code = app.exec()
# Release lock on normal exit
if instance_lock:
instance_lock.release()
sys.exit(exit_code)
except KeyboardInterrupt:
print("\nApplication interrupted by user")
if instance_lock:
instance_lock.release()
sys.exit(0)
except Exception as e:
print(f"Fatal error: {e}")
import traceback
traceback.print_exc()
if instance_lock:
instance_lock.release()
sys.exit(1)