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."""
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,21 +268,63 @@ class MainWindow(QMainWindow):
def _start_web_server_if_enabled(self):
"""Start web server."""
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}")
# 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=port,
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."""
if not self.is_transcribing:
@@ -503,6 +552,7 @@ class MainWindow(QMainWindow):
def _reload_model(self):
"""Reload the transcription model with new settings."""
try:
# Stop transcription if running
was_transcribing = self.is_transcribing
if was_transcribing:
@@ -514,7 +564,10 @@ class MainWindow(QMainWindow):
# 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')
@@ -546,6 +599,15 @@ class MainWindow(QMainWindow):
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."""
if success:
@@ -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:
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():

View File

@@ -223,11 +223,21 @@ class TranscriptionWebServer:
async def start(self):
"""Start the web server."""
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(
self.app,
host=self.host,
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)
await server.serve()