Fix app stability: graceful model switching and web server improvements

- Add comprehensive error handling to prevent crashes during model reload
- Implement automatic port fallback (8080-8084) for web server conflicts
- Configure uvicorn to work properly with PyInstaller console=False builds
- Add proper web server shutdown on app close to release ports
- Improve error reporting with full tracebacks for debugging

Fixes:
- App crashing when switching models
- Web server not starting after app crash (port conflict)
- Web server failing silently in compiled builds without console

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-26 17:50:37 -08:00
parent 478146c58d
commit e831dadd24
2 changed files with 136 additions and 52 deletions

View File

@@ -32,12 +32,19 @@ class WebServerThread(Thread):
super().__init__(daemon=True) super().__init__(daemon=True)
self.web_server = web_server self.web_server = web_server
self.loop = None self.loop = None
self.error = None
def run(self): def run(self):
"""Run the web server in async event loop.""" """Run the web server in async event loop."""
self.loop = asyncio.new_event_loop() try:
asyncio.set_event_loop(self.loop) self.loop = asyncio.new_event_loop()
self.loop.run_until_complete(self.web_server.start()) asyncio.set_event_loop(self.loop)
self.loop.run_until_complete(self.web_server.start())
except Exception as e:
self.error = e
print(f"ERROR: Web server failed to start: {e}")
import traceback
traceback.print_exc()
class ModelLoaderThread(QThread): class ModelLoaderThread(QThread):
@@ -261,20 +268,62 @@ class MainWindow(QMainWindow):
def _start_web_server_if_enabled(self): def _start_web_server_if_enabled(self):
"""Start web server.""" """Start web server."""
host = self.config.get('web_server.host', '127.0.0.1') try:
port = self.config.get('web_server.port', 8080) host = self.config.get('web_server.host', '127.0.0.1')
show_timestamps = self.config.get('display.show_timestamps', True) port = self.config.get('web_server.port', 8080)
fade_after_seconds = self.config.get('display.fade_after_seconds', 10) show_timestamps = self.config.get('display.show_timestamps', True)
fade_after_seconds = self.config.get('display.fade_after_seconds', 10)
print(f"Starting web server at http://{host}:{port}") # Try up to 5 ports if the default is in use
self.web_server = TranscriptionWebServer( ports_to_try = [port] + [port + i for i in range(1, 5)]
host=host, server_started = False
port=port,
show_timestamps=show_timestamps, for try_port in ports_to_try:
fade_after_seconds=fade_after_seconds print(f"Attempting to start web server at http://{host}:{try_port}")
) self.web_server = TranscriptionWebServer(
self.web_server_thread = WebServerThread(self.web_server) host=host,
self.web_server_thread.start() port=try_port,
show_timestamps=show_timestamps,
fade_after_seconds=fade_after_seconds
)
self.web_server_thread = WebServerThread(self.web_server)
self.web_server_thread.start()
# Give it a moment to start and check for errors
import time
time.sleep(0.5)
if self.web_server_thread.error:
error_str = str(self.web_server_thread.error)
# Check if it's a port-in-use error
if "address already in use" in error_str.lower() or "errno 98" in error_str.lower():
print(f"Port {try_port} is in use, trying next port...")
self.web_server = None
self.web_server_thread = None
continue
else:
# Different error, don't retry
print(f"Web server failed to start: {self.web_server_thread.error}")
self.web_server = None
self.web_server_thread = None
break
else:
# Success!
print(f"✓ Web server started successfully at http://{host}:{try_port}")
if try_port != port:
print(f" Note: Using port {try_port} instead of configured port {port}")
server_started = True
break
if not server_started:
print(f"WARNING: Could not start web server on any port from {ports_to_try[0]} to {ports_to_try[-1]}")
except Exception as e:
print(f"ERROR: Failed to initialize web server: {e}")
import traceback
traceback.print_exc()
self.web_server = None
self.web_server_thread = None
def _toggle_transcription(self): def _toggle_transcription(self):
"""Start or stop transcription.""" """Start or stop transcription."""
@@ -503,48 +552,61 @@ class MainWindow(QMainWindow):
def _reload_model(self): def _reload_model(self):
"""Reload the transcription model with new settings.""" """Reload the transcription model with new settings."""
# Stop transcription if running try:
was_transcribing = self.is_transcribing # Stop transcription if running
if was_transcribing: was_transcribing = self.is_transcribing
self._stop_transcription() if was_transcribing:
self._stop_transcription()
# Update status # Update status
self.status_label.setText("⚙ Reloading model...") self.status_label.setText("⚙ Reloading model...")
self.start_button.setEnabled(False) self.start_button.setEnabled(False)
# Unload current model # Unload current model
if self.transcription_engine: if self.transcription_engine:
self.transcription_engine.unload_model() try:
self.transcription_engine.unload_model()
except Exception as e:
print(f"Warning: Error unloading model: {e}")
# Set device based on config # Set device based on config
device_config = self.config.get('transcription.device', 'auto') device_config = self.config.get('transcription.device', 'auto')
self.device_manager.set_device(device_config) self.device_manager.set_device(device_config)
# Re-initialize transcription engine # Re-initialize transcription engine
model_size = self.config.get('transcription.model', 'base') model_size = self.config.get('transcription.model', 'base')
language = self.config.get('transcription.language', 'en') language = self.config.get('transcription.language', 'en')
device = self.device_manager.get_device_for_whisper() device = self.device_manager.get_device_for_whisper()
compute_type = self.device_manager.get_compute_type() compute_type = self.device_manager.get_compute_type()
# Update tracked settings # Update tracked settings
self.current_model_size = model_size self.current_model_size = model_size
self.current_device_config = device_config self.current_device_config = device_config
self.transcription_engine = TranscriptionEngine( self.transcription_engine = TranscriptionEngine(
model_size=model_size, model_size=model_size,
device=device, device=device,
compute_type=compute_type, compute_type=compute_type,
language=language, language=language,
min_confidence=self.config.get('processing.min_confidence', 0.5) min_confidence=self.config.get('processing.min_confidence', 0.5)
) )
# Load model in background thread # Load model in background thread
if self.model_loader_thread and self.model_loader_thread.isRunning(): if self.model_loader_thread and self.model_loader_thread.isRunning():
self.model_loader_thread.wait() self.model_loader_thread.wait()
self.model_loader_thread = ModelLoaderThread(self.transcription_engine) self.model_loader_thread = ModelLoaderThread(self.transcription_engine)
self.model_loader_thread.finished.connect(self._on_model_reloaded) self.model_loader_thread.finished.connect(self._on_model_reloaded)
self.model_loader_thread.start() self.model_loader_thread.start()
except Exception as e:
error_msg = f"Error during model reload: {e}"
print(error_msg)
import traceback
traceback.print_exc()
self.status_label.setText("❌ Model reload failed")
self.start_button.setEnabled(False)
QMessageBox.critical(self, "Error", error_msg)
def _on_model_reloaded(self, success: bool, message: str): def _on_model_reloaded(self, success: bool, message: str):
"""Handle model reloading completion.""" """Handle model reloading completion."""
@@ -602,9 +664,21 @@ class MainWindow(QMainWindow):
if self.is_transcribing: if self.is_transcribing:
self._stop_transcription() self._stop_transcription()
# Stop web server
if self.web_server_thread and self.web_server_thread.is_alive():
try:
print("Shutting down web server...")
if self.web_server_thread.loop:
self.web_server_thread.loop.call_soon_threadsafe(self.web_server_thread.loop.stop)
except Exception as e:
print(f"Warning: Error stopping web server: {e}")
# Unload model # Unload model
if self.transcription_engine: if self.transcription_engine:
self.transcription_engine.unload_model() try:
self.transcription_engine.unload_model()
except Exception as e:
print(f"Warning: Error unloading model: {e}")
# Wait for model loader thread # Wait for model loader thread
if self.model_loader_thread and self.model_loader_thread.isRunning(): if self.model_loader_thread and self.model_loader_thread.isRunning():

View File

@@ -223,11 +223,21 @@ class TranscriptionWebServer:
async def start(self): async def start(self):
"""Start the web server.""" """Start the web server."""
import uvicorn import uvicorn
import logging
# Configure uvicorn to work without console (for PyInstaller builds)
# Suppress uvicorn's default console logging
logging.getLogger("uvicorn").setLevel(logging.ERROR)
logging.getLogger("uvicorn.access").setLevel(logging.ERROR)
logging.getLogger("uvicorn.error").setLevel(logging.ERROR)
config = uvicorn.Config( config = uvicorn.Config(
self.app, self.app,
host=self.host, host=self.host,
port=self.port, port=self.port,
log_level="warning" log_level="error", # Only log errors
access_log=False, # Disable access logging
log_config=None # Don't use default logging config
) )
server = uvicorn.Server(config) server = uvicorn.Server(config)
await server.serve() await server.serve()