2025-12-25 18:48:23 -08:00
|
|
|
#!/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
|
2025-12-28 20:09:43 -08:00
|
|
|
import multiprocessing
|
2025-12-25 18:48:23 -08:00
|
|
|
from pathlib import Path
|
2025-12-28 20:57:49 -08:00
|
|
|
import os
|
2025-12-25 18:48:23 -08:00
|
|
|
|
2025-12-28 20:17:23 -08:00
|
|
|
# CRITICAL: Must be called before anything else with PyInstaller
|
2025-12-28 20:09:43 -08:00
|
|
|
# This prevents the infinite spawning loop when the frozen executable runs
|
2025-12-28 20:17:23 -08:00
|
|
|
# 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
|
2025-12-28 20:09:43 -08:00
|
|
|
|
2025-12-28 20:57:49 -08:00
|
|
|
# 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()
|
|
|
|
|
|
2025-12-25 18:48:23 -08:00
|
|
|
# Add project root to Python path
|
2026-01-11 18:56:12 -08:00
|
|
|
# Use resolve() to follow symlinks and get the real path
|
|
|
|
|
project_root = Path(__file__).resolve().parent
|
2025-12-25 18:48:23 -08:00
|
|
|
sys.path.insert(0, str(project_root))
|
|
|
|
|
|
2026-01-11 18:56:12 -08:00
|
|
|
# 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"
|
2025-12-25 18:48:23 -08:00
|
|
|
|
|
|
|
|
|
2025-12-27 06:33:44 -08:00
|
|
|
def create_splash_pixmap(message="Loading..."):
|
2026-01-11 18:56:12 -08:00
|
|
|
"""Create a pixmap for the splash screen with the app icon."""
|
|
|
|
|
pixmap = QPixmap(400, 320)
|
2025-12-27 06:33:44 -08:00
|
|
|
pixmap.fill(QColor("#2b2b2b"))
|
|
|
|
|
|
|
|
|
|
# Draw on the pixmap
|
|
|
|
|
painter = QPainter(pixmap)
|
|
|
|
|
painter.setRenderHint(QPainter.Antialiasing)
|
2026-01-11 18:56:12 -08:00
|
|
|
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
|
2025-12-27 06:33:44 -08:00
|
|
|
subtitle_font = QFont("Arial", 12)
|
|
|
|
|
painter.setFont(subtitle_font)
|
|
|
|
|
painter.setPen(QColor("#888888"))
|
2026-01-11 18:56:12 -08:00
|
|
|
subtitle_rect = pixmap.rect().adjusted(0, 0, 0, -40)
|
|
|
|
|
painter.drawText(subtitle_rect, Qt.AlignHCenter | Qt.AlignBottom, message)
|
2025-12-27 06:33:44 -08:00
|
|
|
|
|
|
|
|
# Draw version/status at bottom
|
2026-01-11 18:56:12 -08:00
|
|
|
from version import __version__
|
2025-12-27 06:33:44 -08:00
|
|
|
status_font = QFont("Arial", 10)
|
|
|
|
|
painter.setFont(status_font)
|
|
|
|
|
painter.setPen(QColor("#666666"))
|
2026-01-11 18:56:12 -08:00
|
|
|
status_rect = pixmap.rect().adjusted(0, 0, 0, -15)
|
|
|
|
|
painter.drawText(status_rect, Qt.AlignHCenter | Qt.AlignBottom, f"v{__version__}")
|
2025-12-27 06:33:44 -08:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
2025-12-25 18:48:23 -08:00
|
|
|
def main():
|
|
|
|
|
"""Main application entry point."""
|
2026-01-11 18:56:12 -08:00
|
|
|
# Instance lock for cleanup on exit
|
|
|
|
|
instance_lock = None
|
|
|
|
|
|
2025-12-25 18:48:23 -08:00
|
|
|
try:
|
|
|
|
|
print("Starting Local Transcription Application...")
|
|
|
|
|
print("=" * 50)
|
|
|
|
|
|
2026-01-11 18:56:12 -08:00
|
|
|
# Create Qt application first (needed for dialogs)
|
2025-12-25 18:48:23 -08:00
|
|
|
app = QApplication(sys.argv)
|
|
|
|
|
|
|
|
|
|
# Set application info
|
|
|
|
|
app.setApplicationName("Local Transcription")
|
|
|
|
|
app.setOrganizationName("LocalTranscription")
|
|
|
|
|
|
2025-12-28 18:59:24 -08:00
|
|
|
# Set application icon
|
2026-01-11 18:56:12 -08:00
|
|
|
icon_path = get_icon_path()
|
2025-12-28 18:59:24 -08:00
|
|
|
if icon_path.exists():
|
|
|
|
|
app.setWindowIcon(QIcon(str(icon_path)))
|
|
|
|
|
|
2026-01-11 18:56:12 -08:00
|
|
|
# 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
|
2025-12-27 06:33:44 -08:00
|
|
|
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()
|
|
|
|
|
|
2026-01-11 18:56:12 -08:00
|
|
|
# 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
|
|
|
|
|
|
2025-12-27 06:33:44 -08:00
|
|
|
# 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)
|
2025-12-25 18:48:23 -08:00
|
|
|
window.show()
|
|
|
|
|
|
|
|
|
|
# Run application
|
2026-01-11 18:56:12 -08:00
|
|
|
exit_code = app.exec()
|
|
|
|
|
|
|
|
|
|
# Release lock on normal exit
|
|
|
|
|
if instance_lock:
|
|
|
|
|
instance_lock.release()
|
|
|
|
|
|
|
|
|
|
sys.exit(exit_code)
|
2025-12-25 18:48:23 -08:00
|
|
|
|
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
|
print("\nApplication interrupted by user")
|
2026-01-11 18:56:12 -08:00
|
|
|
if instance_lock:
|
|
|
|
|
instance_lock.release()
|
2025-12-25 18:48:23 -08:00
|
|
|
sys.exit(0)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"Fatal error: {e}")
|
|
|
|
|
import traceback
|
|
|
|
|
traceback.print_exc()
|
2026-01-11 18:56:12 -08:00
|
|
|
if instance_lock:
|
|
|
|
|
instance_lock.release()
|
2025-12-25 18:48:23 -08:00
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
main()
|