fix: better TTS error handling when backends not installed
- get_available_backends() now does deep import checks (verifies ChatterboxTTS/Qwen3TTSModel classes, not just top-level package) so partially-installed packages don't false-positive - TTS section always shows in UI even when no backends installed, with clear "No TTS engine installed" message and install command - Backend dropdown labels show "(not installed)" for missing engines - Selecting an unavailable backend shows install command in status instead of a cryptic error - Enabling TTS with unavailable backend is blocked with clear message - Error/warning messages display in orange instead of blue - TTS controller only created when at least one backend is available https://claude.ai/code/session_01965m1v3f1cK6gDV1JHioA8
+94 −58 · 3 files changed
-
src/tts_provider.py+11 −7 -
src/whispering_ui/components/sidebar.py+46 −21 -
src/whispering_ui/main.py+37 −30
@@ -22,21 +22,25 @@ warnings.filterwarnings('ignore', category=UserWarning) # --------------------------------------------------------------------------- def get_available_backends() -> dict[str, bool]: - """Check which TTS backends are installed and importable.""" + """Check which TTS backends are installed and importable. + + Does a deep import check (tries the actual class, not just the + top-level package) so partially-installed packages don't false-positive. + """ backends = {} - # Check Chatterbox + # Check Chatterbox - verify the actual TTS class is importable try: - import chatterbox # noqa: F401 + from chatterbox.tts import ChatterboxTTS # noqa: F401 backends["chatterbox"] = True - except ImportError: + except (ImportError, ModuleNotFoundError, Exception): backends["chatterbox"] = False - # Check Qwen3-TTS + # Check Qwen3-TTS - verify the model class is importable try: - import qwen_tts # noqa: F401 + from qwen_tts import Qwen3TTSModel # noqa: F401 backends["qwen3"] = True - except ImportError: + except (ImportError, ModuleNotFoundError, Exception): backends["qwen3"] = False return backends @@ -554,21 +554,20 @@ def create_sidebar(state: AppState, bridge: ProcessingBridge, output_container=N return control if state.tts_available: + has_any_backend = any(state.tts_backends_available.values()) + with tts_section: # Backend selection with ui.row().classes('items-center w-full gap-1'): ui.label('Engine:').classes('text-xs w-10') - # Build backend options from what's available - backend_options = [] - if state.tts_backends_available.get("chatterbox"): - backend_options.append("chatterbox") - if state.tts_backends_available.get("qwen3"): - backend_options.append("qwen3") - # Always show both options so user knows what's possible - if "chatterbox" not in backend_options: - backend_options.append("chatterbox") - if "qwen3" not in backend_options: - backend_options.append("qwen3") + # Build backend options with availability labels + backend_options = {} + for bname in ["chatterbox", "qwen3"]: + installed = state.tts_backends_available.get(bname, False) + if installed: + backend_options[bname] = bname + else: + backend_options[bname] = f"{bname} (not installed)" tts_backend_select = register_tts(ui.select( options=backend_options, @@ -576,18 +575,31 @@ def create_sidebar(state: AppState, bridge: ProcessingBridge, output_container=N ).classes('flex-grow').props('dense')) def on_backend_change(e): - state.tts_backend = e.value - if bridge.tts_controller: - bridge.tts_controller.switch_backend( - backend=e.value, - qwen3_model_size=state.tts_qwen3_model_size, - qwen3_speaker=state.tts_qwen3_speaker, - ) + selected = e.value + installed = state.tts_backends_available.get(selected, False) + if not installed: + state.tts_status_message = f"{selected} not installed. Run: ./scripts/install.sh --tts={selected}" + state.tts_enabled = False + tts_cb.value = False + else: + state.tts_backend = selected + if bridge.tts_controller: + bridge.tts_controller.switch_backend( + backend=selected, + qwen3_model_size=state.tts_qwen3_model_size, + qwen3_speaker=state.tts_qwen3_speaker, + ) + state.tts_status_message = "" # Show/hide Qwen3 options - qwen3_row.set_visibility(e.value == "qwen3") + qwen3_row.set_visibility(selected == "qwen3") tts_backend_select.on_value_change(on_backend_change) + # Install hint when no backends available + if not has_any_backend: + ui.label('No TTS engine installed').classes('text-xs text-orange-400') + ui.label('Run: ./scripts/install.sh --tts').classes('text-xs text-gray-500') + # Qwen3-TTS specific options (speaker + model size) qwen3_row = ui.row().classes('items-center w-full gap-1') qwen3_row.set_visibility(state.tts_backend == "qwen3") @@ -669,8 +681,14 @@ def create_sidebar(state: AppState, bridge: ProcessingBridge, output_container=N tts_status_label = ui.label('').classes('text-xs text-blue-400') def update_tts_status(): - if state.tts_status_message: - tts_status_label.text = state.tts_status_message[:50] + msg = state.tts_status_message + if msg: + # Color errors orange/red + if 'not installed' in msg or 'error' in msg.lower() or 'failed' in msg.lower(): + tts_status_label.classes(replace='text-xs text-orange-400') + else: + tts_status_label.classes(replace='text-xs text-blue-400') + tts_status_label.text = msg[:60] else: tts_status_label.text = '' # Sync playback state from controller @@ -680,6 +698,13 @@ def create_sidebar(state: AppState, bridge: ProcessingBridge, output_container=N ui.timer(0.2, update_tts_status) def on_tts_toggle(e): + # Check if selected backend is actually installed + backend_installed = state.tts_backends_available.get(state.tts_backend, False) + if e.value and not backend_installed: + state.tts_status_message = f"{state.tts_backend} not installed. Run: ./scripts/install.sh --tts" + e.sender.value = False + state.tts_enabled = False + return state.tts_enabled = e.value active = e.value and state.tts_available _set_section_visual_state(tts_section, active) @@ -86,40 +86,48 @@ def main(): state.ai_available = False # Check TTS availability + # Always show TTS section so user can see install instructions. + # tts_available=True means the TTS UI section is visible. + # tts_backends_available tracks which engines are actually installed. try: from tts_provider import get_available_backends from tts_controller import TTSController backends = get_available_backends() state.tts_backends_available = backends - state.tts_available = any(backends.values()) - - if state.tts_available: - # Load TTS settings - state.tts_enabled = settings.get("tts_enabled", False) - state.tts_backend = settings.get("tts_backend", "chatterbox") - state.tts_source = settings.get("tts_source", "whisper") - state.tts_auto_play = settings.get("tts_auto_play", True) - state.tts_save_file = settings.get("tts_save_file", False) - state.tts_format = settings.get("tts_format", "wav") - state.tts_qwen3_speaker = settings.get("tts_qwen3_speaker", "Ryan") - state.tts_qwen3_model_size = settings.get("tts_qwen3_model_size", "1.7B") - - # If saved backend is not installed, fall back to first available - if not backends.get(state.tts_backend): - for name, avail in backends.items(): - if avail: - state.tts_backend = name - break - - # Print backend status + # Show TTS section whenever our modules load (always) + state.tts_available = True + + # Load TTS settings + state.tts_enabled = settings.get("tts_enabled", False) + state.tts_backend = settings.get("tts_backend", "chatterbox") + state.tts_source = settings.get("tts_source", "whisper") + state.tts_auto_play = settings.get("tts_auto_play", True) + state.tts_save_file = settings.get("tts_save_file", False) + state.tts_format = settings.get("tts_format", "wav") + state.tts_qwen3_speaker = settings.get("tts_qwen3_speaker", "Ryan") + state.tts_qwen3_model_size = settings.get("tts_qwen3_model_size", "1.7B") + + # If saved backend is not installed, fall back to first available + if not backends.get(state.tts_backend): for name, avail in backends.items(): - status = "available" if avail else "not installed" - print(f" TTS backend {name}: {status}") - else: - print("No TTS backends installed. TTS features disabled.") - print(" Install Chatterbox: pip install chatterbox-tts --no-deps") - print(" Install Qwen3-TTS: pip install qwen-tts") + if avail: + state.tts_backend = name + break + + # Print backend status + has_any = False + for name, avail in backends.items(): + status = "available" if avail else "not installed" + print(f" TTS backend {name}: {status}") + if avail: + has_any = True + if not has_any: + state.tts_enabled = False # Don't auto-enable if nothing is installed + print(" No TTS backends installed.") + print(" Install with: ./scripts/install.sh --tts") + print(" Or manually: pip install chatterbox-tts --no-deps") + print(" pip install qwen-tts") except Exception as e: print(f"TTS features not available: {e}") state.tts_available = False @@ -128,8 +136,8 @@ def main(): # Create processing bridge bridge = ProcessingBridge(state) - # Initialize TTS controller in bridge if available - if state.tts_available: + # Initialize TTS controller in bridge if any backend is available + if state.tts_available and any(state.tts_backends_available.values()): try: from tts_controller import TTSController bridge.tts_controller = TTSController( @@ -144,7 +152,6 @@ def main(): bridge.tts_controller.on_error = lambda msg: setattr(state, 'tts_status_message', msg) except Exception as e: print(f"Failed to initialize TTS controller: {e}") - state.tts_available = False # === RECOVERY DIALOG === # Check for crashed sessions and offer recovery