#!/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()