import logging import argparse import warnings from typing import Any, Tuple, Optional from reachy_mini import ReachyMini from lyon_chatbox.camera_worker import CameraWorker def parse_args() -> Tuple[argparse.Namespace, list]: # type: ignore """Parse command line arguments.""" parser = argparse.ArgumentParser("Lyon Chatbox") parser.add_argument( "--head-tracker", choices=["yolo", "mediapipe", None], default=None, help="Choose head tracker (default: None)", ) parser.add_argument("--no-camera", default=False, action="store_true", help="Disable camera usage") parser.add_argument( "--local-vision", default=False, action="store_true", help="Use local vision model instead of gpt-realtime vision", ) parser.add_argument("--gradio", default=False, action="store_true", help="Open gradio interface") parser.add_argument( "--realtime", default=False, action="store_true", help="Use OpenAI realtime audio-to-audio API instead of the default cascade pipeline (ASR→LLM→TTS)", ) parser.add_argument( "--autotest", nargs="?", const="__default__", default=None, help="Run autotest mode with text utterances. Optionally specify a file path (default: cascade/autotest.txt).", ) parser.add_argument("--debug", default=False, action="store_true", help="Enable debug logging") parser.add_argument( "--robot-name", type=str, default=None, help="[Optional] Robot name/prefix for Zenoh topics (must match daemon's --robot-name). Only needed for development with multiple robots.", ) parser.add_argument("--asr-provider", default=None, help="Override ASR provider from cascade.yaml") parser.add_argument("--llm-provider", default=None, help="Override LLM provider from cascade.yaml") parser.add_argument("--tts-provider", default=None, help="Override TTS provider from cascade.yaml") return parser.parse_known_args() def handle_vision_stuff(args: argparse.Namespace, current_robot: ReachyMini) -> Tuple[CameraWorker | None, Any, Any]: """Initialize camera, head tracker, camera worker, and vision manager. By default, vision is handled by gpt-realtime model when camera tool is used. If --local-vision flag is used, a local vision model will process images periodically. """ camera_worker = None head_tracker = None vision_manager = None if not args.no_camera: # Initialize head tracker if specified if args.head_tracker is not None: if args.head_tracker == "yolo": from lyon_chatbox.vision.yolo_head_tracker import HeadTracker head_tracker = HeadTracker() elif args.head_tracker == "mediapipe": from reachy_mini_toolbox.vision import HeadTracker # type: ignore[no-redef] head_tracker = HeadTracker() # Initialize camera worker camera_worker = CameraWorker(current_robot, head_tracker) # Initialize vision manager only if local vision is requested if args.local_vision: try: from lyon_chatbox.vision.processors import initialize_vision_manager vision_manager = initialize_vision_manager(camera_worker) except ImportError as e: raise ImportError( "To use --local-vision, please install the extra dependencies: pip install '.[local_vision]'", ) from e else: logging.getLogger(__name__).info( "Using gpt-realtime for vision (default). Use --local-vision for local processing.", ) return camera_worker, head_tracker, vision_manager class ColoredFormatter(logging.Formatter): """Formatter that adds colors based on log level.""" COLORS = { logging.DEBUG: "\033[90m", # Light grey logging.INFO: "\033[0m", # Default (white) logging.WARNING: "\033[93m", # Yellow logging.ERROR: "\033[91m", # Red logging.CRITICAL: "\033[91m", # Red } RESET = "\033[0m" def format(self, record: logging.LogRecord) -> str: """Format the log record with color.""" color = self.COLORS.get(record.levelno, self.RESET) message = super().format(record) return f"{color}{message}{self.RESET}" def setup_logger(debug: bool) -> logging.Logger: """Setups the logger.""" log_level = "DEBUG" if debug else "INFO" # Create formatter with time-only and filename (no full path) formatter = ColoredFormatter( fmt="%(asctime)s.%(msecs)03.0f %(levelname)s %(filename)s:%(lineno)d | %(message)s", datefmt="%H:%M:%S", ) # Configure root logger with colored handler handler = logging.StreamHandler() handler.setFormatter(formatter) logging.root.handlers = [handler] logging.root.setLevel(getattr(logging, log_level, logging.INFO)) logger = logging.getLogger(__name__) # Suppress WebRTC warnings warnings.filterwarnings("ignore", message=".*AVCaptureDeviceTypeExternal.*") warnings.filterwarnings("ignore", category=UserWarning, module="aiortc") # Tame third-party noise (looser in DEBUG) if log_level == "DEBUG": logging.getLogger("aiortc").setLevel(logging.INFO) logging.getLogger("fastrtc").setLevel(logging.INFO) logging.getLogger("aioice").setLevel(logging.INFO) logging.getLogger("openai").setLevel(logging.INFO) logging.getLogger("websockets").setLevel(logging.INFO) logging.getLogger("matplotlib").setLevel(logging.WARNING) else: logging.getLogger("aiortc").setLevel(logging.ERROR) logging.getLogger("fastrtc").setLevel(logging.ERROR) logging.getLogger("aioice").setLevel(logging.WARNING) logging.getLogger("matplotlib").setLevel(logging.WARNING) return logger def log_connection_troubleshooting(logger: logging.Logger, robot_name: Optional[str]) -> None: """Log troubleshooting steps for connection issues.""" logger.error("Troubleshooting steps:") logger.error(" 1. Verify reachy-mini-daemon is running") if robot_name is not None: logger.error( f" 2. Daemon must be started with: --robot-name '{robot_name}'" ) else: logger.error( " 2. If daemon uses --robot-name, add the same flag here: " "--robot-name " ) logger.error(" 3. For wireless: check network connectivity") logger.error(" 4. Review daemon logs") logger.error(" 5. Restart the daemon")