Files
local-transcription/main.py
jknapp ff067b3368 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>
2026-01-11 19:09:57 -08:00

202 lines
6.7 KiB
Python

#!/usr/bin/env python3
"""
Local Transcription Application
A standalone desktop application for real-time speech-to-text transcription
using Whisper models. Supports CPU/GPU processing, noise suppression, and
optional multi-user server synchronization.
"""
import sys
import multiprocessing
from pathlib import Path
import os
# CRITICAL: Must be called before anything else with PyInstaller
# This prevents the infinite spawning loop when the frozen executable runs
# Required on all platforms (Windows, Linux, macOS) when using multiprocessing
multiprocessing.freeze_support()
# Set multiprocessing start method to 'spawn' for consistency across platforms
# This prevents issues with PyInstaller frozen executables
if __name__ == "__main__":
try:
multiprocessing.set_start_method('spawn', force=True)
except RuntimeError:
pass # Already set, ignore
# Fix for Windows PyInstaller builds with console=False
# When console is hidden, subprocess/multiprocessing can't write to stdout/stderr
# This causes RealtimeSTT and other libraries to fail silently
if getattr(sys, 'frozen', False) and sys.platform == 'win32':
# Redirect stdout/stderr to null device on Windows when frozen
# Only if they're not already valid (console=False builds)
try:
sys.stdout.flush()
sys.stderr.flush()
except (AttributeError, OSError):
# stdout/stderr are not available, redirect to null
import io
sys.stdout = io.StringIO()
sys.stderr = io.StringIO()
# Add project root to Python path
# Use resolve() to follow symlinks and get the real path
project_root = Path(__file__).resolve().parent
sys.path.insert(0, str(project_root))
# 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 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)
# 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 loading message below icon
subtitle_font = QFont("Arial", 12)
painter.setFont(subtitle_font)
painter.setPen(QColor("#888888"))
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, -15)
painter.drawText(status_rect, Qt.AlignHCenter | Qt.AlignBottom, f"v{__version__}")
painter.end()
return pixmap
def create_splash_screen():
"""Create a splash screen for startup."""
pixmap = create_splash_pixmap("Initializing...")
splash = QSplashScreen(pixmap)
splash.setWindowFlags(Qt.WindowStaysOnTopHint | Qt.SplashScreen)
return splash
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 first (needed for dialogs)
app = QApplication(sys.argv)
# Set application info
app.setApplicationName("Local Transcription")
app.setOrganizationName("LocalTranscription")
# Set application icon
icon_path = get_icon_path()
if icon_path.exists():
app.setWindowIcon(QIcon(str(icon_path)))
# 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
# Update splash with progress
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)
# Close splash and show main window
splash.finish(window)
window.show()
# Run application
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)
if __name__ == "__main__":
main()