"""Entry point for cascade mode - keeps cascade logic isolated from main.py.""" from __future__ import annotations import os import time import signal import logging from typing import TYPE_CHECKING, Any if TYPE_CHECKING: from argparse import Namespace from reachy_mini import ReachyMini from lyon_chatbox.tools.core_tools import ToolDependencies def run_cascade_mode( deps: ToolDependencies, robot: ReachyMini, args: Namespace, logger: logging.Logger, ) -> None: """Run the application in cascade mode (ASR→LLM→TTS pipeline). This function handles all cascade-specific initialization, UI creation, and lifecycle management, keeping main.py clean. Args: deps: Tool dependencies for robot control robot: ReachyMini instance args: Parsed command line arguments logger: Logger instance """ # Set env vars from CLI overrides BEFORE any config/handler import if getattr(args, "asr_provider", None): os.environ["CASCADE_ASR_PROVIDER"] = args.asr_provider if getattr(args, "llm_provider", None): os.environ["CASCADE_LLM_PROVIDER"] = args.llm_provider if getattr(args, "tts_provider", None): os.environ["CASCADE_TTS_PROVIDER"] = args.tts_provider from lyon_chatbox.cascade.handler import CascadeHandler logger.info("Using cascade pipeline mode (ASR→LLM→TTS)") stream_manager: Any = None if args.autotest: # Autotest mode — feed text utterances through the pipeline from pathlib import Path from lyon_chatbox.cascade.autotest_stream import CascadeTestStream if args.autotest == "__default__": test_file = str(Path(__file__).parent / "autotest.txt") else: test_file = args.autotest logger.info(f"Using autotest mode: {test_file}") handler = CascadeHandler(deps) handler.ensure_initialized() stream_manager = CascadeTestStream(handler, robot, test_file) elif args.gradio: # Gradio UI mode from lyon_chatbox.cascade.ui import CascadeGradioUI logger.info("Using Gradio UI for cascade mode") handler = CascadeHandler(deps) cascade_ui = CascadeGradioUI(handler, robot) stream_manager = cascade_ui.create_interface() else: # Console mode with VAD from lyon_chatbox.cascade.console import CascadeLocalStream logger.info("Using console mode for cascade (VAD-based speech detection)") handler = CascadeHandler(deps) handler.ensure_initialized() stream_manager = CascadeLocalStream(handler, robot) # Start services if deps.movement_manager: deps.movement_manager.start() if deps.head_wobbler: deps.head_wobbler.start() # For console mode: CascadeLocalStream.launch() handles media start # For Gradio mode: audio recording is handled by ContinuousVADRecorder, # and camera frames come directly from CameraWorker (no start_recording needed). if deps.camera_worker: deps.camera_worker.start() logger.info("Camera worker started in cascade mode") else: logger.warning("No camera worker available (deps.camera_worker is None)") if deps.vision_manager: deps.vision_manager.start() # Start cascade handler (only for Gradio mode - console mode runs synchronously) if args.gradio: handler.start() try: stream_manager.launch() except KeyboardInterrupt: logger.info("Keyboard interruption in main thread... closing server.") finally: # Ignore further Ctrl+C during cleanup so we always reach os._exit. signal.signal(signal.SIGINT, signal.SIG_IGN) # Stop the stream manager stream_manager.close() # Stop cascade handler (only for Gradio mode) if args.gradio: handler.stop() # Stop other services if deps.movement_manager: deps.movement_manager.stop() if deps.head_wobbler: deps.head_wobbler.stop() if deps.camera_worker: deps.camera_worker.stop() if deps.vision_manager: deps.vision_manager.stop() # Ensure media is explicitly closed before disconnecting try: robot.media.close() except Exception as e: logger.debug(f"Error closing media during shutdown: {e}") # Prevent connection to keep alive some threads robot.client.disconnect() time.sleep(1) logger.info("Cascade mode shutdown complete.") # Hard exit to avoid segfault from PortAudio/sounddevice native # cleanup racing with daemon thread teardown during interpreter shutdown. os._exit(0)