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)
self.web_server = web_server
self.loop = None
self.error = None
def run(self):
"""Run the web server in async event loop."""
self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.loop)
self.loop.run_until_complete(self.web_server.start())
try:
self.loop = asyncio.new_event_loop()
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):
@@ -261,20 +268,62 @@ class MainWindow(QMainWindow):
def _start_web_server_if_enabled(self):
"""Start web server."""
host = self.config.get('web_server.host', '127.0.0.1')
port = self.config.get('web_server.port', 8080)
show_timestamps = self.config.get('display.show_timestamps', True)
fade_after_seconds = self.config.get('display.fade_after_seconds', 10)
try:
host = self.config.get('web_server.host', '127.0.0.1')
port = self.config.get('web_server.port', 8080)
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}")
self.web_server = TranscriptionWebServer(
host=host,
port=port,
show_timestamps=show_timestamps,
fade_after_seconds=fade_after_seconds
)
self.web_server_thread = WebServerThread(self.web_server)
self.web_server_thread.start()
# Try up to 5 ports if the default is in use
ports_to_try = [port] + [port + i for i in range(1, 5)]
server_started = False
for try_port in ports_to_try:
print(f"Attempting to start web server at http://{host}:{try_port}")
self.web_server = TranscriptionWebServer(
host=host,
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):
"""Start or stop transcription."""
@@ -503,48 +552,61 @@ class MainWindow(QMainWindow):
def _reload_model(self):
"""Reload the transcription model with new settings."""
# Stop transcription if running
was_transcribing = self.is_transcribing
if was_transcribing:
self._stop_transcription()
try:
# Stop transcription if running
was_transcribing = self.is_transcribing
if was_transcribing:
self._stop_transcription()
# Update status
self.status_label.setText("⚙ Reloading model...")
self.start_button.setEnabled(False)
# Update status
self.status_label.setText("⚙ Reloading model...")
self.start_button.setEnabled(False)
# Unload current model
if self.transcription_engine:
self.transcription_engine.unload_model()
# Unload current model
if self.transcription_engine:
try:
self.transcription_engine.unload_model()
except Exception as e:
print(f"Warning: Error unloading model: {e}")
# Set device based on config
device_config = self.config.get('transcription.device', 'auto')
self.device_manager.set_device(device_config)
# Set device based on config
device_config = self.config.get('transcription.device', 'auto')
self.device_manager.set_device(device_config)
# Re-initialize transcription engine
model_size = self.config.get('transcription.model', 'base')
language = self.config.get('transcription.language', 'en')
device = self.device_manager.get_device_for_whisper()
compute_type = self.device_manager.get_compute_type()
# Re-initialize transcription engine
model_size = self.config.get('transcription.model', 'base')
language = self.config.get('transcription.language', 'en')
device = self.device_manager.get_device_for_whisper()
compute_type = self.device_manager.get_compute_type()
# Update tracked settings
self.current_model_size = model_size
self.current_device_config = device_config
# Update tracked settings
self.current_model_size = model_size
self.current_device_config = device_config
self.transcription_engine = TranscriptionEngine(
model_size=model_size,
device=device,
compute_type=compute_type,
language=language,
min_confidence=self.config.get('processing.min_confidence', 0.5)
)
self.transcription_engine = TranscriptionEngine(
model_size=model_size,
device=device,
compute_type=compute_type,
language=language,
min_confidence=self.config.get('processing.min_confidence', 0.5)
)
# Load model in background thread
if self.model_loader_thread and self.model_loader_thread.isRunning():
self.model_loader_thread.wait()
# Load model in background thread
if self.model_loader_thread and self.model_loader_thread.isRunning():
self.model_loader_thread.wait()
self.model_loader_thread = ModelLoaderThread(self.transcription_engine)
self.model_loader_thread.finished.connect(self._on_model_reloaded)
self.model_loader_thread.start()
self.model_loader_thread = ModelLoaderThread(self.transcription_engine)
self.model_loader_thread.finished.connect(self._on_model_reloaded)
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):
"""Handle model reloading completion."""
@@ -602,9 +664,21 @@ class MainWindow(QMainWindow):
if self.is_transcribing:
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
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
if self.model_loader_thread and self.model_loader_thread.isRunning():