Ctrl K
ComunidadGuías…

rex_loop.py

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""Async CLI loop driving the full Rex voice experience.

This preserves the semantics of the original ``rex_loop.py`` entry point while
bridging to the refactored voice loop package introduced during previous
iterations.
"""

# ruff: noqa: E402

from __future__ import annotations

# Load .env before accessing any environment variables
from utils.env_loader import load as _load_env

_load_env()

import argparse
import asyncio
import logging
import os
import warnings
from collections.abc import Iterable
from pathlib import Path

# Suppress torio FFmpeg extension warnings (non-critical audio codec features)
warnings.filterwarnings("ignore", message=".*FFmpeg extension.*")
warnings.filterwarnings("ignore", message=".*libtorio.*")
warnings.filterwarnings("ignore", category=RuntimeWarning, module="torio")

import rex
from rex.assistant import Assistant
from rex.assistant_errors import AssistantError, ConfigurationError, WakeWordError
from rex.config import load_config as load_runtime_config
from rex.logging_utils import configure_logging
from rex.plugins import PluginSpec, load_plugins, shutdown_plugins
from rex.voice_loop import build_voice_loop

# MQTT is optional
try:
    from rex.mqtt_audio_router import MqttAudioRouter

    MQTT_AVAILABLE = True
except ImportError as exc:
    MqttAudioRouter = None  # type: ignore
    MQTT_AVAILABLE = False
    _mqtt_import_error = str(exc)

logger = logging.getLogger(__name__)

# Backward compatibility: re-export AsyncRexAssistant for code that imports it from here
# The canonical location is voice_loop.py at the repo root
try:
    from voice_loop import AsyncRexAssistant
    from voice_loop import build_voice_loop as _build_voice_loop_v1
except ImportError:
    AsyncRexAssistant = None  # type: ignore
    _build_voice_loop_v1 = None  # type: ignore


def _select_plugins(enabled: Iterable[str] | None) -> list[PluginSpec]:
    specs = load_plugins()
    if not enabled:
        return specs
    enabled_set = {name.strip() for name in enabled if name}
    return [spec for spec in specs if spec.name in enabled_set]


async def _run(args) -> None:
    configure_logging()

    # Run migration from legacy .env to rex_config.json if needed
    from rex.config_manager import get_legacy_env_warnings, migrate_legacy_env_to_config

    migration_notes = migrate_legacy_env_to_config()
    if migration_notes and len(migration_notes) > 1:
        logger.info("Configuration migration completed")

    # Warn about legacy environment variables
    legacy_warnings = get_legacy_env_warnings()
    if legacy_warnings:
        logger.warning(
            "Legacy environment variables detected. These are now ignored. Use config/rex_config.json instead."
        )

    try:
        runtime_config = load_runtime_config(reload=True)
        rex.settings = runtime_config
        configure_logging()
    except ConfigurationError as exc:
        logger.error("Profile configuration error: %s", exc)
        return

    plugin_specs = _select_plugins(args.enable_plugin)

    if args.user:
        os.environ["REX_ACTIVE_USER"] = args.user
        rex.reload_settings()

    # Deliberate single-user profile selection (issue #303): --user wins,
    # then the identify/session chain, then the configured profile, then the
    # explicit "default" profile.  Assistant no longer invents an identity.
    from rex.identity import resolve_entrypoint_user_id

    user_id = resolve_entrypoint_user_id(rex.settings, explicit_user=args.user)
    assistant = Assistant(
        history_limit=rex.settings.max_memory_items, plugins=plugin_specs, user_id=user_id
    )

    try:
        wake_sound_path = getattr(rex.settings, "wake_sound_path", None)
        voice_loop = build_voice_loop(
            assistant,
            activation_mode=args.mode,
            sample_rate=rex.settings.sample_rate,
            detection_seconds=rex.settings.detection_frame_seconds,
            capture_seconds=rex.settings.capture_seconds,
            whisper_model=rex.settings.whisper_model,
            device=rex.settings.whisper_device,
            language=rex.settings.whisper_language or "en",
            wake_sound_path=Path(wake_sound_path) if wake_sound_path else None,
        )
    except (AssistantError, WakeWordError) as exc:
        logger.error("Unable to initialise voice loop: %s", exc)
        return

    mqtt_router = None
    mqtt_broker = getattr(rex.settings, "mqtt_broker", None)
    if MQTT_AVAILABLE and MqttAudioRouter is not None and mqtt_broker:
        try:
            mqtt_router = MqttAudioRouter(assistant=assistant)
            await mqtt_router.start()
            logger.info("MQTT audio router started.")
        except Exception as exc:  # pragma: no cover - defensive startup log
            logger.error("Unable to start MQTT audio router: %s", exc)
            mqtt_router = None
    elif not MQTT_AVAILABLE:
        logger.info("MQTT audio router disabled (dependency not available)")
    else:
        logger.info("MQTT audio router disabled (mqtt_broker not configured)")

    logger.info("🎙️ Voice loop started. Press Ctrl+C to exit.")
    try:
        await voice_loop.run()
    finally:
        if mqtt_router is not None:
            await mqtt_router.stop()
        shutdown_plugins(plugin_specs)


def _create_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(description="Run the AskRex voice assistant loop.")
    parser.add_argument(
        "--mode",
        choices=("hold-to-talk", "wake-word"),
        default="hold-to-talk",
        help=(
            "Voice activation mode (default: hold-to-talk). "
            "Use wake-word to opt into the beta wake detector."
        ),
    )
    parser.add_argument("--user", help="Override the active user profile")
    parser.add_argument(
        "--enable-plugin",
        action="append",
        metavar="NAME",
        help="Explicitly enable a plugin by name (omit to load all)",
    )
    return parser


def main(argv: list[str] | None = None) -> int:
    args = _create_parser().parse_args(argv)

    try:
        asyncio.run(_run(args))
    except KeyboardInterrupt:
        print("\nInterrupted.")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())