Spaces:
Running
Running
| import asyncio | |
| import logging | |
| import os | |
| import sys | |
| # --- load .env early: token_manager.py executes at import time, before main() --- | |
| try: | |
| from dotenv import load_dotenv as _load_dotenv | |
| if getattr(sys, 'frozen', False): | |
| _app_data = next( | |
| (sys.argv[i + 1] for i, a in enumerate(sys.argv) | |
| if a == '--app-data-dir' and i + 1 < len(sys.argv)), | |
| None | |
| ) | |
| if _app_data: | |
| _load_dotenv(os.path.join(_app_data, '.env'), override=False) | |
| else: | |
| _load_dotenv() | |
| else: | |
| _load_dotenv() | |
| del _load_dotenv | |
| except ImportError: | |
| pass | |
| # ----------------------------------------------------------------------- | |
| # Insert root directory to sys.path to allow 'import modules.x' to work natively from Cloud | |
| parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | |
| if parent_dir not in sys.path: | |
| sys.path.insert(0, parent_dir) | |
| os.environ["TYPEGUARD_DISABLE"] = "1" | |
| # Torch load monkey patch moved to backend/voice/vad.py for lazy-loading | |
| import inspect | |
| _orig_getsource = inspect.getsource | |
| def _safe_getsource(obj): | |
| try: | |
| return _orig_getsource(obj) | |
| except OSError: | |
| return "" | |
| inspect.getsource = _safe_getsource | |
| _orig_getsourcelines = inspect.getsourcelines | |
| def _safe_getsourcelines(obj): | |
| try: | |
| return _orig_getsourcelines(obj) | |
| except OSError: | |
| return ([""], 0) | |
| inspect.getsourcelines = _safe_getsourcelines | |
| import uvicorn | |
| # Ensure backend module is in sys.path when running from compiled PyInstaller executable | |
| if getattr(sys, 'frozen', False): | |
| sys.path.append(sys._MEIPASS) | |
| from backend.ws.agent_ws import ws_manager | |
| # --- STATIC IMPORTS FOR PYINSTALLER --- | |
| # ---------------------------------------- | |
| from backend.db.migrations import run_all_migrations | |
| from backend.routes.agent_routes import router as agent_router | |
| from backend.routes.memory_routes import router as memory_router | |
| from backend.routes.voice_routes import router as voice_router | |
| from backend.routes.system_routes import router as system_router | |
| from backend.routes.automation_routes import router as automation_router | |
| from backend.routes.security_routes import router as security_router | |
| from backend.routes.github_routes import router as github_router | |
| from backend.routes.xr_routes import router as xr_router | |
| from backend.routes.android_routes import router as android_router | |
| from backend.routes.config_routes import router as config_router | |
| from backend.routes.easter_egg_routes import router as easter_egg_router | |
| from backend.routes.omega_routes import router as omega_router | |
| from backend.routes.persona_routes import router as persona_router | |
| from backend.routes.internet_routes import router as internet_router | |
| from backend.routes.sentinel_routes import router as sentinel_router | |
| from backend.routers.gaming_routes import router as gaming_router | |
| from backend.services.voice_service import start_wake_word_loop | |
| from backend.services.usb_monitor import start_usb_monitor | |
| from backend.services.system_monitor import start_system_stats_loop | |
| from backend.services.pc_mic_service import start_pc_mic_loop | |
| from backend.voice.audio_ws import run_in_thread as start_audio_ws | |
| APP_VERSION = "1.0.0" | |
| from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Depends | |
| from backend.security.auth import verify_token | |
| try: | |
| from slowapi import Limiter, _rate_limit_exceeded_handler | |
| from slowapi.util import get_remote_address | |
| from slowapi.errors import RateLimitExceeded | |
| from slowapi.middleware import SlowAPIMiddleware | |
| SLOWAPI_AVAILABLE = True | |
| except ImportError: | |
| logging.warning("slowapi package not found. Rate limiting is disabled.") | |
| SLOWAPI_AVAILABLE = False | |
| try: | |
| import sentry_sdk | |
| sentry_sdk.init( | |
| dsn="https://c48f4234bb502ff74a2c5e518a3a65b2@o4511579717369856.ingest.us.sentry.io/4511579850407936", | |
| traces_sample_rate=1.0, | |
| send_default_pii=True, | |
| ) | |
| except Exception: | |
| pass | |
| print(r""" | |
| ____. _____ ____________________.__ _________ | |
| | | / _ \______ \____ \ \ \ \ \ ___/ | |
| | |/ /_\ \| _// | \ \ \ \ \____ \ | |
| /\__| / | \ | \ | \ \ \_\ \ \ \ | |
| \________\____|__ /____|_ /_______/___/\______/____/ | |
| \/ \/ | |
| Fast Booting... Lazy Loading Heavy ML Models... | |
| """) | |
| app = FastAPI(title="JARVIS / FRIDAY OS Backend Sidecar") | |
| if SLOWAPI_AVAILABLE: | |
| limiter = Limiter(key_func=get_remote_address, default_limits=["100/minute"]) | |
| app.state.limiter = limiter | |
| app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) | |
| app.add_middleware(SlowAPIMiddleware) | |
| app.include_router(agent_router, prefix="/agent", dependencies=[Depends(verify_token)]) | |
| app.include_router(memory_router, prefix="/memory", dependencies=[Depends(verify_token)]) | |
| app.include_router(voice_router, prefix="/voice", dependencies=[Depends(verify_token)]) | |
| app.include_router(system_router, prefix="/system", dependencies=[Depends(verify_token)]) | |
| app.include_router(automation_router, prefix="/automation", dependencies=[Depends(verify_token)]) | |
| app.include_router(security_router, prefix="/security", dependencies=[Depends(verify_token)]) | |
| app.include_router(github_router, prefix="/github", dependencies=[Depends(verify_token)]) | |
| app.include_router(xr_router, prefix="/xr", dependencies=[Depends(verify_token)]) | |
| app.include_router(android_router, prefix="/android", dependencies=[Depends(verify_token)]) | |
| app.include_router(config_router, prefix="/config", dependencies=[Depends(verify_token)]) | |
| app.include_router(omega_router, prefix="") # Prefix defined in router as /omega | |
| app.include_router(easter_egg_router, prefix="/easter") # Exclude easter egg from auth | |
| app.include_router(persona_router, prefix="/persona", dependencies=[Depends(verify_token)]) | |
| app.include_router(internet_router, prefix="/internet", dependencies=[Depends(verify_token)]) | |
| app.include_router(sentinel_router, prefix="/sentinel", dependencies=[Depends(verify_token)]) | |
| app.include_router(gaming_router, prefix="", dependencies=[Depends(verify_token)]) # gaming routes already have /gaming prefix | |
| async def root_health_check(): | |
| return {"status": "JARVIS / FRIDAY Cloud OS Online", "message": "All systems nominal."} | |
| async def wake_server(): | |
| """Public endpoint — anyone can hit this from anywhere in the world to wake the cloud server. | |
| HF Spaces sleep after inactivity; this wakes them up instantly.""" | |
| import time | |
| return { | |
| "status": "online", | |
| "message": "JARVIS / FRIDAY Cloud Brain is awake and operational.", | |
| "timestamp": time.time(), | |
| "servers": { | |
| "hf_space": "online", | |
| "sqlite": "online", | |
| "chroma": "online", | |
| "mongodb_atlas": "online" | |
| } | |
| } | |
| async def ping(): | |
| """Ultra-lightweight public ping endpoint for keepalive from mobile app.""" | |
| return {"pong": True} | |
| from fastapi import Request | |
| from fastapi.responses import JSONResponse | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from backend.dependencies.auth import verify_master_token | |
| # Configure CORS for production (Tauri and Android network) | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["tauri://localhost", "http://localhost", "http://localhost:1420", "*"], | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| async def global_exception_handler(request: Request, exc: Exception): | |
| logging.error(f"Global Error on {request.url.path}: {exc}") | |
| return JSONResponse( | |
| status_code=500, | |
| content={"error": "Internal Server Error", "detail": str(exc)}, | |
| ) | |
| async def auth_middleware(request: Request, call_next): | |
| allowed_paths = ["/", "/health", "/wake", "/ping", "/android/pair", "/voice/upload_log"] | |
| if request.url.path in allowed_paths or request.url.path.startswith("/ws") or request.url.path.endswith("/ws"): | |
| return await call_next(request) | |
| # Allow local connections without auth | |
| if request.client.host in ["127.0.0.1", "localhost", "::1"]: | |
| return await call_next(request) | |
| token = request.headers.get("Authorization") | |
| if not token or not verify_master_token(token): | |
| return JSONResponse(status_code=401, content={"error": "Unauthorized Access"}) | |
| return await call_next(request) | |
| IS_READY = False | |
| audio_ws_server = None | |
| BACKGROUND_TASKS = [] | |
| async def startup_event(): | |
| global IS_READY, audio_ws_server | |
| logging.info("Starting JARVIS / FRIDAY backend sidecar...") | |
| # Start Audio WS in background thread (no-op stub in cloud mode) | |
| audio_ws_server = start_audio_ws() | |
| # Start automations scheduler | |
| from backend.services.automation_service import init_automations | |
| await init_automations() | |
| # Track all asyncio background loops | |
| # pc_mic_loop, wake_word_loop, usb, stats are local-only — skip in cloud (HF has no mic/usb) | |
| cloud_mode = os.environ.get("CLOUD_ENV", "false").lower() == "true" | |
| if not cloud_mode: | |
| BACKGROUND_TASKS.append(asyncio.create_task(start_pc_mic_loop())) | |
| BACKGROUND_TASKS.append(asyncio.create_task(start_wake_word_loop())) | |
| BACKGROUND_TASKS.append(asyncio.create_task(start_usb_monitor())) | |
| BACKGROUND_TASKS.append(asyncio.create_task(start_system_stats_loop())) | |
| # --- JARVIS 10X Universal Gaming Coach --- | |
| try: | |
| from backend.gaming.coach_engine import coach_engine | |
| from backend.gaming.overlay_renderer import run_overlay_in_background | |
| run_overlay_in_background() # Spawns PyQt5 window in a background OS thread | |
| BACKGROUND_TASKS.append(asyncio.create_task(coach_engine.start_loop())) | |
| except Exception as e: | |
| logging.warning(f"JARVIS 10X Coach not active (missing deps / Qt crash?): {e}") | |
| else: | |
| logging.info("[Startup] Cloud mode — skipping local hardware loops (mic, wake, usb, stats).") | |
| # 1. Models Loader Placeholder (Wait for models) | |
| # TTS model loading is offloaded to the Audio WS background thread to prevent blocking Uvicorn. | |
| # 2. mDNS Registration | |
| from backend.services.mdns_discovery import register_mdns_service | |
| port = int(os.environ.get("JARVIS_PORT_BOUND", os.environ.get("JARVIS_PORT", "7474"))) | |
| register_mdns_service(port) | |
| # 3. Auto-start PC Relay Client (Local Windows Only) | |
| cloud_env = os.environ.get("CLOUD_ENV", "false").lower() == "true" | |
| if not cloud_env: | |
| try: | |
| import subprocess | |
| parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | |
| relay_script = os.path.join(parent_dir, "pc_relay_client.py") | |
| if os.path.exists(relay_script): | |
| si = subprocess.STARTUPINFO() | |
| si.dwFlags |= subprocess.STARTF_USESHOWWINDOW | |
| si.wShowWindow = 0 | |
| subprocess.Popen([sys.executable, relay_script], cwd=parent_dir, startupinfo=si, creationflags=0x08000000) | |
| logging.info("Auto-started pc_relay_client.py natively from main server.") | |
| except Exception as e: | |
| logging.error(f"Could not auto-start pc_relay_client.py: {e}") | |
| # 4. §2.3+2.4 — Start Continuous Research Mode (both environments) | |
| from backend.omega.research_engine import start_continuous_research_mode | |
| BACKGROUND_TASKS.append(asyncio.create_task(start_continuous_research_mode("jarvis"))) | |
| logging.info("[Startup] Continuous Research Mode activated. JARVIS will monitor and queue proposals.") | |
| # 5. §Token Safety — Start Token Limit Checkpoint & Resume Monitor | |
| from backend.services.token_manager import start_token_refresh_monitor | |
| BACKGROUND_TASKS.append(asyncio.create_task(start_token_refresh_monitor())) | |
| logging.info("[Startup] Token Refresh Monitor activated. All Gemini 3.5 Flash tasks will auto-resume on 429.") | |
| # 6. Broadcast Ready (Health check passes) | |
| IS_READY = True | |
| async def shutdown_event(): | |
| logging.info("Shutting down JARVIS backend sidecar...") | |
| if audio_ws_server: | |
| audio_ws_server.stop() | |
| from backend.services.mdns_discovery import unregister_mdns_service | |
| unregister_mdns_service() | |
| from backend.services.automation_service import shutdown_automations | |
| await shutdown_automations() | |
| from backend.omega.research_engine import stop_continuous_research_mode | |
| stop_continuous_research_mode() | |
| from backend.services.token_manager import stop_token_refresh_monitor | |
| stop_token_refresh_monitor() | |
| # Gracefully cancel all background loops | |
| for task in BACKGROUND_TASKS: | |
| task.cancel() | |
| await asyncio.gather(*BACKGROUND_TASKS, return_exceptions=True) | |
| async def health(): | |
| if not IS_READY: | |
| from fastapi import HTTPException | |
| raise HTTPException(status_code=503, detail="Backend starting up") | |
| return {"status": "ok", "version": APP_VERSION} | |
| async def websocket_hub(websocket: WebSocket): | |
| await ws_manager.connect(websocket) | |
| async def heartbeat(): | |
| while True: | |
| await asyncio.sleep(30) | |
| try: | |
| await websocket.send_json({"event": "ping"}) | |
| except Exception: | |
| logging.warning("Heartbeat failed, closing dead WS connection.") | |
| try: | |
| await websocket.close() | |
| except Exception as e: | |
| import logging; logging.getLogger(__name__).error(f"Swallowed exception: {e}") | |
| break | |
| heartbeat_task = asyncio.create_task(heartbeat()) | |
| try: | |
| while True: | |
| data = await websocket.receive_json() | |
| await ws_manager.handle_client_event(websocket, data) | |
| except WebSocketDisconnect: | |
| pass | |
| finally: | |
| heartbeat_task.cancel() | |
| ws_manager.disconnect(websocket) | |
| async def agent_websocket_alias(websocket: WebSocket): | |
| await websocket_hub(websocket) | |
| async def voice_websocket_proxy(websocket: WebSocket): | |
| await websocket.accept() | |
| import websockets | |
| try: | |
| async with websockets.connect("ws://127.0.0.1:8767") as target_ws: | |
| async def forward_to_target(): | |
| try: | |
| while True: | |
| msg = await websocket.receive() | |
| if "bytes" in msg: | |
| await target_ws.send(msg["bytes"]) | |
| elif "text" in msg: | |
| await target_ws.send(msg["text"]) | |
| except Exception as e: | |
| import logging; logging.getLogger(__name__).error(f"Swallowed exception: {e}") | |
| async def forward_to_client(): | |
| try: | |
| async for msg in target_ws: | |
| if isinstance(msg, bytes): | |
| await websocket.send_bytes(msg) | |
| else: | |
| await websocket.send_text(msg) | |
| except Exception as e: | |
| import logging; logging.getLogger(__name__).error(f"Swallowed exception: {e}") | |
| await asyncio.gather(forward_to_target(), forward_to_client()) | |
| except Exception as e: | |
| logging.error(f"Voice WS Proxy Error: {e}") | |
| async def scene_websocket_proxy(websocket: WebSocket): | |
| await websocket.accept() | |
| import websockets | |
| try: | |
| async with websockets.connect("ws://127.0.0.1:5050") as target_ws: | |
| async def forward_to_target(): | |
| try: | |
| while True: | |
| msg = await websocket.receive() | |
| if "bytes" in msg: | |
| await target_ws.send(msg["bytes"]) | |
| elif "text" in msg: | |
| await target_ws.send(msg["text"]) | |
| except Exception as e: | |
| import logging; logging.getLogger(__name__).error(f"Swallowed exception: {e}") | |
| async def forward_to_client(): | |
| try: | |
| async for msg in target_ws: | |
| if isinstance(msg, bytes): | |
| await websocket.send_bytes(msg) | |
| else: | |
| await websocket.send_text(msg) | |
| except Exception as e: | |
| import logging; logging.getLogger(__name__).error(f"Swallowed exception: {e}") | |
| await asyncio.gather(forward_to_target(), forward_to_client()) | |
| except Exception as e: | |
| logging.error(f"Scene WS Proxy Error: {e}") | |
| def main(): | |
| import argparse | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument("--app-data-dir", default="") | |
| parser.add_argument("--resource-dir", default="") | |
| args, unknown = parser.parse_known_args() | |
| if args.app_data_dir: | |
| os.environ["JARVIS_APP_DATA_DIR"] = args.app_data_dir | |
| if args.resource_dir: | |
| os.environ["JARVIS_RESOURCE_DIR"] = args.resource_dir | |
| import re | |
| import json | |
| class JSONMaskingFormatter(logging.Formatter): | |
| key_patterns = [ | |
| re.compile(r'(sk-[a-zA-Z0-9]{20,})'), | |
| re.compile(r'(AIza[0-9A-Za-z-_]{30,})'), | |
| re.compile(r'\b([A-Za-z0-9-_]{32,})\b') | |
| ] | |
| def format(self, record): | |
| msg = record.getMessage() | |
| for pattern in self.key_patterns: | |
| def repl(m): | |
| s = m.group(1) | |
| if len(s) > 8: | |
| return '*' * (len(s) - 4) + s[-4:] | |
| return s | |
| msg = pattern.sub(repl, msg) | |
| log_record = { | |
| "timestamp": self.formatTime(record, self.datefmt), | |
| "level": record.levelname, | |
| "name": record.name, | |
| "message": msg, | |
| } | |
| if record.exc_info: | |
| log_record["exception"] = self.formatException(record.exc_info) | |
| return json.dumps(log_record) | |
| root_logger = logging.getLogger() | |
| root_logger.setLevel(logging.INFO) | |
| handler = logging.StreamHandler() | |
| handler.setFormatter(JSONMaskingFormatter()) | |
| root_logger.addHandler(handler) | |
| # Run migrations synchronously before mounting Uvicorn | |
| if getattr(sys, 'frozen', False): | |
| db_path = os.path.join(os.environ.get('APPDATA', os.path.expanduser('~')), 'JARVIS_OS', 'memory.db') | |
| else: | |
| db_path = os.path.join(os.path.dirname(__file__), 'memory.db') | |
| asyncio.run(run_all_migrations(db_path)) | |
| cloud_env = os.environ.get("CLOUD_ENV", "false").lower() == "true" | |
| host = "0.0.0.0" if cloud_env else "127.0.0.1" | |
| base_port = int(os.environ.get("PORT", "7860")) if cloud_env else int(os.environ.get("JARVIS_PORT", "7474")) | |
| for port in range(base_port, base_port + 11): | |
| try: | |
| os.environ["JARVIS_PORT_BOUND"] = str(port) | |
| print(f"JARVIS_PORT_BOUND={port}", flush=True) | |
| uvicorn.run(app, host=host, port=port, log_config=None, ws_ping_interval=20, ws_ping_timeout=10) | |
| break | |
| except OSError as e: | |
| if "WinError 10048" in str(e) or "address already in use" in str(e).lower(): | |
| logging.warning(f"Port {port} in use, trying next...") | |
| continue | |
| raise | |
| if __name__ == "__main__": | |
| main() | |