Jarvis2345 commited on
Commit
5455ea9
Β·
verified Β·
1 Parent(s): 5e00fc3

deploy(S4): Blender headless pipeline + WebAR client + backend fixes

Browse files
backend/main.py CHANGED
@@ -1,615 +1,615 @@
1
- import asyncio
2
- import logging
3
- import os
4
- import sys
5
- # --- load .env early: token_manager.py executes at import time, before main() ---
6
- try:
7
- from dotenv import load_dotenv as _load_dotenv
8
- if getattr(sys, 'frozen', False):
9
- _app_data = next(
10
- (sys.argv[i + 1] for i, a in enumerate(sys.argv)
11
- if a == '--app-data-dir' and i + 1 < len(sys.argv)),
12
- None
13
- )
14
- if _app_data:
15
- _load_dotenv(os.path.join(_app_data, '.env'), override=False)
16
- else:
17
- _load_dotenv()
18
- else:
19
- _load_dotenv()
20
- del _load_dotenv
21
- except ImportError:
22
- pass
23
- # -----------------------------------------------------------------------
24
- # Insert root directory to sys.path to allow 'import modules.x' to work natively from Cloud
25
- parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
26
- if parent_dir not in sys.path:
27
- sys.path.insert(0, parent_dir)
28
- os.environ["TYPEGUARD_DISABLE"] = "1"
29
- # Torch load monkey patch moved to backend/voice/vad.py for lazy-loading
30
-
31
- import inspect
32
- _orig_getsource = inspect.getsource
33
- def _safe_getsource(obj):
34
- try:
35
- return _orig_getsource(obj)
36
- except OSError:
37
- return ""
38
- inspect.getsource = _safe_getsource
39
-
40
- _orig_getsourcelines = inspect.getsourcelines
41
- def _safe_getsourcelines(obj):
42
- try:
43
- return _orig_getsourcelines(obj)
44
- except OSError:
45
- return ([""], 0)
46
- inspect.getsourcelines = _safe_getsourcelines
47
-
48
-
49
- import uvicorn
50
-
51
- # Ensure backend module is in sys.path when running from compiled PyInstaller executable
52
- if getattr(sys, 'frozen', False):
53
- sys.path.append(sys._MEIPASS)
54
-
55
- from backend.ws.agent_ws import ws_manager
56
-
57
- # --- STATIC IMPORTS FOR PYINSTALLER ---
58
-
59
-
60
-
61
- # ----------------------------------------
62
-
63
- from backend.db.migrations import run_all_migrations
64
-
65
- from backend.routes.agent_routes import router as agent_router
66
- from backend.routes.memory_routes import router as memory_router
67
- from backend.routes.voice_routes import router as voice_router
68
- from backend.routes.system_routes import router as system_router
69
- from backend.routes.automation_routes import router as automation_router
70
- from backend.routes.security_routes import router as security_router
71
- from backend.routes.github_routes import router as github_router
72
- from backend.routes.xr_routes import router as xr_router
73
- from backend.routes.android_routes import router as android_router
74
- from backend.routes.config_routes import router as config_router
75
- from backend.routes.easter_egg_routes import router as easter_egg_router
76
- from backend.routes.omega_routes import router as omega_router
77
- from backend.routes.persona_routes import router as persona_router
78
- from backend.routes.internet_routes import router as internet_router
79
- from backend.routes.sentinel_routes import router as sentinel_router
80
- from backend.routers.gaming_routes import router as gaming_router
81
- from backend.routes.model_proxy_routes import router as model_proxy_router # AR-FIX-SESSION
82
- from backend.routes.mobile_bridge_routes import router as mobile_bridge_router # S4: Guardian /api compat
83
-
84
- from backend.services.voice_service import start_wake_word_loop
85
- from backend.services.usb_monitor import start_usb_monitor
86
- from backend.services.system_monitor import start_system_stats_loop
87
- from backend.services.pc_mic_service import start_pc_mic_loop
88
- from backend.voice.audio_ws import run_in_thread as start_audio_ws
89
- APP_VERSION = "1.0.0"
90
-
91
- from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Depends, Request
92
- from backend.security.auth import verify_token
93
- try:
94
- from slowapi import Limiter, _rate_limit_exceeded_handler
95
- from slowapi.util import get_remote_address
96
- from slowapi.errors import RateLimitExceeded
97
- from slowapi.middleware import SlowAPIMiddleware
98
- SLOWAPI_AVAILABLE = True
99
- except ImportError:
100
- logging.warning("slowapi package not found. Rate limiting is disabled.")
101
- SLOWAPI_AVAILABLE = False
102
-
103
- try:
104
- import sentry_sdk
105
- sentry_sdk.init(
106
- dsn="https://c48f4234bb502ff74a2c5e518a3a65b2@o4511579717369856.ingest.us.sentry.io/4511579850407936",
107
- traces_sample_rate=1.0,
108
- send_default_pii=True,
109
- )
110
- except Exception:
111
- pass
112
-
113
- print(r"""
114
- ____. _____ ____________________.__ _________
115
- | | / _ \______ \____ \ \ \ \ \ ___/
116
- | |/ /_\ \| _// | \ \ \ \ \____ \
117
- /\__| / | \ | \ | \ \ \_\ \ \ \
118
- \________\____|__ /____|_ /_______/___/\______/____/
119
- \/ \/
120
- Fast Booting... Lazy Loading Heavy ML Models...
121
- """)
122
-
123
- app = FastAPI(title="JARVIS / FRIDAY OS Backend Sidecar")
124
-
125
- if SLOWAPI_AVAILABLE:
126
- limiter = Limiter(key_func=get_remote_address, default_limits=["100/minute"])
127
- app.state.limiter = limiter
128
- app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
129
- app.add_middleware(SlowAPIMiddleware)
130
-
131
- app.include_router(agent_router, prefix="/agent", dependencies=[Depends(verify_token)])
132
- app.include_router(memory_router, prefix="/memory", dependencies=[Depends(verify_token)])
133
- app.include_router(voice_router, prefix="/voice", dependencies=[Depends(verify_token)])
134
- app.include_router(system_router, prefix="/system", dependencies=[Depends(verify_token)])
135
- app.include_router(automation_router, prefix="/automation", dependencies=[Depends(verify_token)])
136
- app.include_router(security_router, prefix="/security", dependencies=[Depends(verify_token)])
137
- app.include_router(github_router, prefix="/github", dependencies=[Depends(verify_token)])
138
- app.include_router(xr_router, prefix="/xr", dependencies=[Depends(verify_token)])
139
- app.include_router(android_router, prefix="/android", dependencies=[Depends(verify_token)])
140
- app.include_router(config_router, prefix="/config", dependencies=[Depends(verify_token)])
141
- app.include_router(omega_router, prefix="") # Prefix defined in router as /omega
142
- app.include_router(easter_egg_router, prefix="/easter") # Exclude easter egg from auth
143
- app.include_router(persona_router, prefix="/persona", dependencies=[Depends(verify_token)])
144
- app.include_router(internet_router, prefix="/internet", dependencies=[Depends(verify_token)])
145
- app.include_router(sentinel_router, prefix="/sentinel", dependencies=[Depends(verify_token)])
146
- app.include_router(gaming_router, prefix="", dependencies=[Depends(verify_token)]) # gaming routes already have /gaming prefix
147
- app.include_router(model_proxy_router, prefix="/api") # AR-FIX-SESSION: model provider proxy stubs (no auth β€” WebAR calls these from browser)
148
- # S4: the WebAR client's documented contract (README, service-worker, vite proxy) is
149
- # /api/ar_config, /api/ar_task, /api/ar_scene_patch β€” the backend only ever mounted
150
- # them under /xr, so every OMEGA link from the AR client 404'd. Serve both prefixes.
151
- app.include_router(xr_router, prefix="/api", dependencies=[Depends(verify_token)])
152
- # S4: the JARVIS Mobile Guardian APK's remaining /api/* REST contract (status, caps,
153
- # link_info, pair_confirm, command, phone_observe, jarvis/mobile_event,
154
- # max_autonomy/task) had no cloud handler and 404'd against the Space. This compat
155
- # router serves exactly those, delegating to modules/max_autonomy + phone/crypto.
156
- app.include_router(mobile_bridge_router, prefix="/api", dependencies=[Depends(verify_token)])
157
-
158
- # S4: serve generated 3D models. xr_tools.push_model_to_ar_scene has always broadcast
159
- # /static/models3d/<file> URLs, but nothing ever mounted them β€” every spawned GLB 404'd.
160
- from fastapi.staticfiles import StaticFiles
161
- _MODELS3D_DIR = os.path.join("storage", "models3d")
162
- os.makedirs(_MODELS3D_DIR, exist_ok=True)
163
- app.mount("/static/models3d", StaticFiles(directory=_MODELS3D_DIR), name="models3d")
164
-
165
- # S4: serve the WebAR client from the Space so mobile AR loads directly from
166
- # jarvis-cloud.hf.space/webar/ (previously only shipped in cloud_deployment + APK assets).
167
- for _webar_candidate in (
168
- os.environ.get("JARVIS_WEBAR_DIR", ""),
169
- os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "webar"),
170
- os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "cloud_deployment", "webar"),
171
- ):
172
- if _webar_candidate and os.path.isdir(_webar_candidate):
173
- app.mount("/webar", StaticFiles(directory=_webar_candidate, html=True), name="webar")
174
- logging.info(f"[S4] WebAR client mounted at /webar from {_webar_candidate}")
175
- break
176
-
177
- # Public front door of the Space. Everything on it is either static or rendered
178
- # server-side from real state (uptime) β€” nothing fabricated, nothing token-gated
179
- # leaks here.
180
- _ROOT_STATUS_PAGE = """<!doctype html>
181
- <html lang="en"><head><meta charset="utf-8">
182
- <meta name="viewport" content="width=device-width, initial-scale=1">
183
- <title>JARVIS Cloud OS</title>
184
- <link rel="preconnect" href="https://fonts.googleapis.com">
185
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
186
- <link href="https://fonts.googleapis.com/css2?family=Chakra+Petch:wght@400;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
187
- <style>
188
- :root { --accent:#35e6ff; --amber:#ffb454; --text:#dff3fb; --dim:rgba(223,243,251,.55); }
189
- * { box-sizing:border-box; margin:0; }
190
- body { min-height:100vh; display:flex; align-items:center; justify-content:center;
191
- background:radial-gradient(circle at 50% 30%, #0a1620 0%, #05090e 65%, #030509 100%);
192
- color:var(--text); font-family:'Chakra Petch',sans-serif; padding:24px; }
193
- .card { width:min(560px,94vw); padding:36px 32px; border:1px solid rgba(120,220,255,.22);
194
- border-radius:20px; background:rgba(10,22,30,.55); backdrop-filter:blur(24px) saturate(1.4);
195
- box-shadow:0 18px 50px rgba(0,0,0,.55), inset 0 1px 0 rgba(180,240,255,.12); }
196
- .eyebrow { font:600 11px/1 'Chakra Petch'; letter-spacing:.34em; color:var(--dim);
197
- text-transform:uppercase; margin-bottom:14px; }
198
- h1 { font:700 30px/1.2 'Chakra Petch'; letter-spacing:.02em; margin-bottom:6px; }
199
- h1 em { font-style:normal; color:var(--accent); }
200
- .sub { color:var(--dim); font-size:14px; margin-bottom:26px; }
201
- .grid { display:grid; grid-template-columns:1fr 1fr; gap:10px; margin-bottom:26px; }
202
- .stat { border:1px solid rgba(120,220,255,.14); border-radius:12px; padding:12px 14px; }
203
- .stat span { display:block; font:400 10px/1 'Chakra Petch'; letter-spacing:.22em;
204
- color:var(--dim); text-transform:uppercase; margin-bottom:7px; }
205
- .stat strong { font:500 14px/1.3 'JetBrains Mono',monospace; color:var(--accent);
206
- text-transform:uppercase; }
207
- .stat strong.amber { color:var(--amber); }
208
- .pulse { display:inline-block; width:9px; height:9px; border-radius:50%; background:var(--accent);
209
- margin-right:8px; box-shadow:0 0 12px var(--accent); animation:pulse 2s ease-in-out infinite; }
210
- @keyframes pulse { 50% { opacity:.35; box-shadow:0 0 4px var(--accent); } }
211
- .links { display:flex; gap:10px; flex-wrap:wrap; }
212
- .links a { flex:1; min-width:140px; text-align:center; padding:13px 18px; border-radius:12px;
213
- text-decoration:none; font:600 13px/1 'Chakra Petch'; letter-spacing:.08em; text-transform:uppercase;
214
- color:var(--text); border:1px solid rgba(120,220,255,.3); transition:background .15s, box-shadow .15s; }
215
- .links a:hover { background:rgba(53,230,255,.12); box-shadow:0 0 22px rgba(53,230,255,.25); }
216
- .foot { margin-top:24px; color:var(--dim); font-size:11px; letter-spacing:.06em; }
217
- </style></head><body>
218
- <main class="card">
219
- <p class="eyebrow">Stark Industries &middot; Cloud Node</p>
220
- <h1><span class="pulse"></span>JARVIS <em>/</em> FRIDAY</h1>
221
- <p class="sub">OMEGA Cloud OS &mdash; all API surfaces are token-gated.</p>
222
- <div class="grid">
223
- <div class="stat"><span>Status</span><strong>Online</strong></div>
224
- <div class="stat"><span>Uptime</span><strong>{{UPTIME}}</strong></div>
225
- <div class="stat"><span>Mode</span><strong class="amber">Cloud</strong></div>
226
- <div class="stat"><span>Assistant</span><strong>JARVIS</strong></div>
227
- </div>
228
- <div class="links">
229
- <a href="/webar/">Enter WebAR</a>
230
- <a href="/ping">Ping</a>
231
- </div>
232
- <p class="foot">Authorized clients: JARVIS desktop, Guardian mobile, WebAR.</p>
233
- </main>
234
- </body></html>"""
235
-
236
-
237
- @app.get("/")
238
- async def root_health_check(request: Request):
239
- # Browsers (the Space's public front door) get a real status page;
240
- # programmatic callers keep the JSON contract unchanged.
241
- if "text/html" in (request.headers.get("accept") or ""):
242
- from backend.routes.mobile_bridge_routes import _uptime_str
243
- from fastapi.responses import HTMLResponse
244
- return HTMLResponse(_ROOT_STATUS_PAGE.replace("{{UPTIME}}", _uptime_str()))
245
- return {"status": "JARVIS / FRIDAY Cloud OS Online", "message": "All systems nominal."}
246
-
247
- @app.get("/wake")
248
- @app.post("/wake")
249
- async def wake_server():
250
- """Public endpoint β€” anyone can hit this from anywhere in the world to wake the cloud server.
251
- HF Spaces sleep after inactivity; this wakes them up instantly.
252
-
253
- Only claims what is actually true: the fact this handler is running means the
254
- Space process is awake. Per-database claims were previously hardcoded
255
- "online" without any check β€” that lie is gone."""
256
- import time
257
- return {
258
- "status": "online",
259
- "message": "JARVIS / FRIDAY Cloud Brain is awake and operational.",
260
- "timestamp": time.time(),
261
- }
262
-
263
- @app.get("/ping")
264
- async def ping():
265
- """Ultra-lightweight public ping endpoint for keepalive from mobile app."""
266
- return {"pong": True}
267
-
268
- from fastapi import Request
269
- from fastapi.responses import JSONResponse
270
- from fastapi.middleware.cors import CORSMiddleware
271
- from backend.dependencies.auth import verify_master_token
272
-
273
- # Configure CORS for production (Tauri and Android network)
274
- app.add_middleware(
275
- CORSMiddleware,
276
- allow_origins=["tauri://localhost", "http://localhost", "http://localhost:1420", "*"],
277
- allow_credentials=True,
278
- allow_methods=["*"],
279
- allow_headers=["*"],
280
- )
281
-
282
- @app.exception_handler(Exception)
283
- async def global_exception_handler(request: Request, exc: Exception):
284
- logging.error(f"Global Error on {request.url.path}: {exc}")
285
- return JSONResponse(
286
- status_code=500,
287
- content={"error": "Internal Server Error", "detail": str(exc)},
288
- )
289
-
290
- @app.middleware("http")
291
- async def auth_middleware(request: Request, call_next):
292
- allowed_paths = ["/", "/health", "/wake", "/ping", "/android/pair", "/voice/upload_log"]
293
- # S4: read-only static surfaces the AR client fetches without headers β€”
294
- # GLTFLoader cannot attach Authorization, and the client app itself is public.
295
- # /static/models3d files are uuid-named GLBs; /webar is the shipped client bundle.
296
- allowed_prefixes = ("/static/models3d/", "/webar")
297
- if (request.url.path in allowed_paths
298
- or request.url.path.startswith(allowed_prefixes)
299
- or request.url.path.startswith("/ws") or request.url.path.endswith("/ws")):
300
- return await call_next(request)
301
-
302
- # Allow local connections without auth
303
- if request.client.host in ["127.0.0.1", "localhost", "::1"]:
304
- return await call_next(request)
305
-
306
- token = request.headers.get("Authorization")
307
- if not token or not verify_master_token(token):
308
- return JSONResponse(status_code=401, content={"error": "Unauthorized Access"})
309
-
310
- return await call_next(request)
311
- IS_READY = False
312
-
313
- audio_ws_server = None
314
-
315
- BACKGROUND_TASKS = []
316
-
317
- @app.on_event("startup")
318
- async def startup_event():
319
- global IS_READY, audio_ws_server
320
- logging.info("Starting JARVIS / FRIDAY backend sidecar...")
321
-
322
- # Start Audio WS in background thread (no-op stub in cloud mode)
323
- audio_ws_server = start_audio_ws()
324
-
325
- # S4: start the AR scene bus (port 5050) in-process. On the Space nothing else
326
- # ever launched it β€” /scene/ws proxied into a dead port and every
327
- # push_model_to_ar_scene broadcast was silently lost. Desktop entry points
328
- # (tray, local_server) may have started it already; bind failure is fine then.
329
- try:
330
- from phone.ws_scene_bus import run_in_thread as start_scene_bus
331
- start_scene_bus(host="127.0.0.1", port=5050)
332
- logging.info("AR scene bus thread started on 127.0.0.1:5050")
333
- except Exception as scene_bus_error:
334
- logging.warning(f"AR scene bus not started: {scene_bus_error}")
335
-
336
- # Start automations scheduler
337
- from backend.services.automation_service import init_automations
338
- await init_automations()
339
-
340
- # Track all asyncio background loops
341
- # pc_mic_loop, wake_word_loop, usb, stats are local-only β€” skip in cloud (HF has no mic/usb)
342
- cloud_mode = os.environ.get("CLOUD_ENV", "false").lower() == "true"
343
- if not cloud_mode:
344
- BACKGROUND_TASKS.append(asyncio.create_task(start_pc_mic_loop()))
345
- BACKGROUND_TASKS.append(asyncio.create_task(start_wake_word_loop()))
346
- BACKGROUND_TASKS.append(asyncio.create_task(start_usb_monitor()))
347
- BACKGROUND_TASKS.append(asyncio.create_task(start_system_stats_loop()))
348
-
349
- # --- JARVIS 10X Universal Gaming Coach ---
350
- try:
351
- from backend.gaming.coach_engine import coach_engine
352
- from backend.gaming.overlay_renderer import run_overlay_in_background
353
- run_overlay_in_background() # Spawns PyQt5 window in a background OS thread
354
- BACKGROUND_TASKS.append(asyncio.create_task(coach_engine.start_loop()))
355
- except Exception as e:
356
- logging.warning(f"JARVIS 10X Coach not active (missing deps / Qt crash?): {e}")
357
-
358
- else:
359
- logging.info("[Startup] Cloud mode β€” skipping local hardware loops (mic, wake, usb, stats).")
360
-
361
- # 1. Models Loader Placeholder (Wait for models)
362
- # TTS model loading is offloaded to the Audio WS background thread to prevent blocking Uvicorn.
363
-
364
- # 2. mDNS Registration
365
- from backend.services.mdns_discovery import register_mdns_service
366
- port = int(os.environ.get("JARVIS_PORT_BOUND", os.environ.get("JARVIS_PORT", "7474")))
367
- register_mdns_service(port)
368
-
369
- # 3. Auto-start PC Relay Client (Local Windows Only)
370
- cloud_env = os.environ.get("CLOUD_ENV", "false").lower() == "true"
371
- if not cloud_env:
372
- try:
373
- import subprocess
374
- parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
375
- relay_script = os.path.join(parent_dir, "pc_relay_client.py")
376
- if os.path.exists(relay_script):
377
- si = subprocess.STARTUPINFO()
378
- si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
379
- si.wShowWindow = 0
380
- subprocess.Popen([sys.executable, relay_script], cwd=parent_dir, startupinfo=si, creationflags=0x08000000)
381
- logging.info("Auto-started pc_relay_client.py natively from main server.")
382
- except Exception as e:
383
- logging.error(f"Could not auto-start pc_relay_client.py: {e}")
384
-
385
- # 4. Β§2.3+2.4 β€” Start Continuous Research Mode (both environments)
386
- from backend.omega.research_engine import start_continuous_research_mode
387
- BACKGROUND_TASKS.append(asyncio.create_task(start_continuous_research_mode("jarvis")))
388
- logging.info("[Startup] Continuous Research Mode activated. JARVIS will monitor and queue proposals.")
389
-
390
- # 5. Β§Token Safety β€” Start Token Limit Checkpoint & Resume Monitor
391
- from backend.services.token_manager import start_token_refresh_monitor
392
- BACKGROUND_TASKS.append(asyncio.create_task(start_token_refresh_monitor()))
393
- logging.info("[Startup] Token Refresh Monitor activated. All Gemini 3.5 Flash tasks will auto-resume on 429.")
394
-
395
- # 6. Broadcast Ready (Health check passes)
396
- IS_READY = True
397
-
398
- @app.on_event("shutdown")
399
- async def shutdown_event():
400
- logging.info("Shutting down JARVIS backend sidecar...")
401
-
402
- if audio_ws_server:
403
- audio_ws_server.stop()
404
-
405
- from backend.services.mdns_discovery import unregister_mdns_service
406
- unregister_mdns_service()
407
-
408
- from backend.services.automation_service import shutdown_automations
409
- await shutdown_automations()
410
-
411
- from backend.omega.research_engine import stop_continuous_research_mode
412
- stop_continuous_research_mode()
413
-
414
- from backend.services.token_manager import stop_token_refresh_monitor
415
- stop_token_refresh_monitor()
416
-
417
- # Gracefully cancel all background loops
418
- for task in BACKGROUND_TASKS:
419
- task.cancel()
420
-
421
- await asyncio.gather(*BACKGROUND_TASKS, return_exceptions=True)
422
-
423
- @app.get("/health")
424
- async def health():
425
- if not IS_READY:
426
- from fastapi import HTTPException
427
- raise HTTPException(status_code=503, detail="Backend starting up")
428
- return {"status": "ok", "version": APP_VERSION}
429
-
430
- def _ws_authorized(websocket: WebSocket) -> bool:
431
- """S4: HTTP auth middleware never sees websocket scopes, so every WS endpoint
432
- was open to the internet on the public Space. Native clients send an
433
- Authorization header; browsers cannot, so they pass ?token= instead."""
434
- try:
435
- if websocket.client and websocket.client.host in ("127.0.0.1", "localhost", "::1"):
436
- return True
437
- token = websocket.query_params.get("token") or websocket.headers.get("Authorization", "")
438
- return bool(token) and verify_master_token(token)
439
- except Exception:
440
- return False
441
-
442
- @app.websocket("/ws")
443
- async def websocket_hub(websocket: WebSocket):
444
- if not _ws_authorized(websocket):
445
- await websocket.close(code=1008)
446
- return
447
- await ws_manager.connect(websocket)
448
-
449
- async def heartbeat():
450
- while True:
451
- await asyncio.sleep(30)
452
- try:
453
- await websocket.send_json({"event": "ping"})
454
- except Exception:
455
- logging.warning("Heartbeat failed, closing dead WS connection.")
456
- try:
457
- await websocket.close()
458
- except Exception as e:
459
- import logging; logging.getLogger(__name__).error(f"Swallowed exception: {e}")
460
- break
461
-
462
- heartbeat_task = asyncio.create_task(heartbeat())
463
- try:
464
- while True:
465
- data = await websocket.receive_json()
466
- await ws_manager.handle_client_event(websocket, data)
467
- except WebSocketDisconnect:
468
- pass
469
- finally:
470
- heartbeat_task.cancel()
471
- ws_manager.disconnect(websocket)
472
-
473
- @app.websocket("/agent/ws")
474
- async def agent_websocket_alias(websocket: WebSocket):
475
- await websocket_hub(websocket)
476
-
477
- @app.websocket("/voice/ws")
478
- async def voice_websocket_proxy(websocket: WebSocket):
479
- if not _ws_authorized(websocket):
480
- await websocket.close(code=1008)
481
- return
482
- await websocket.accept()
483
- import websockets
484
- try:
485
- async with websockets.connect("ws://127.0.0.1:8767") as target_ws:
486
- async def forward_to_target():
487
- try:
488
- while True:
489
- msg = await websocket.receive()
490
- if "bytes" in msg:
491
- await target_ws.send(msg["bytes"])
492
- elif "text" in msg:
493
- await target_ws.send(msg["text"])
494
- except Exception as e:
495
- import logging; logging.getLogger(__name__).error(f"Swallowed exception: {e}")
496
- async def forward_to_client():
497
- try:
498
- async for msg in target_ws:
499
- if isinstance(msg, bytes):
500
- await websocket.send_bytes(msg)
501
- else:
502
- await websocket.send_text(msg)
503
- except Exception as e:
504
- import logging; logging.getLogger(__name__).error(f"Swallowed exception: {e}")
505
- await asyncio.gather(forward_to_target(), forward_to_client())
506
- except Exception as e:
507
- logging.error(f"Voice WS Proxy Error: {e}")
508
-
509
- @app.websocket("/scene-ws")
510
- async def scene_websocket_alias(websocket: WebSocket):
511
- # S4: the WebAR client dials /scene-ws (its documented contract); the backend
512
- # only ever exposed /scene/ws, so the cloud scene link never connected.
513
- await scene_websocket_proxy(websocket)
514
-
515
- @app.websocket("/scene/ws")
516
- async def scene_websocket_proxy(websocket: WebSocket):
517
- if not _ws_authorized(websocket):
518
- await websocket.close(code=1008)
519
- return
520
- await websocket.accept()
521
- import websockets
522
- try:
523
- async with websockets.connect("ws://127.0.0.1:5050") as target_ws:
524
- async def forward_to_target():
525
- try:
526
- while True:
527
- msg = await websocket.receive()
528
- if "bytes" in msg:
529
- await target_ws.send(msg["bytes"])
530
- elif "text" in msg:
531
- await target_ws.send(msg["text"])
532
- except Exception as e:
533
- import logging; logging.getLogger(__name__).error(f"Swallowed exception: {e}")
534
- async def forward_to_client():
535
- try:
536
- async for msg in target_ws:
537
- if isinstance(msg, bytes):
538
- await websocket.send_bytes(msg)
539
- else:
540
- await websocket.send_text(msg)
541
- except Exception as e:
542
- import logging; logging.getLogger(__name__).error(f"Swallowed exception: {e}")
543
- await asyncio.gather(forward_to_target(), forward_to_client())
544
- except Exception as e:
545
- logging.error(f"Scene WS Proxy Error: {e}")
546
-
547
- def main():
548
- import argparse
549
- parser = argparse.ArgumentParser()
550
- parser.add_argument("--app-data-dir", default="")
551
- parser.add_argument("--resource-dir", default="")
552
- args, unknown = parser.parse_known_args()
553
-
554
- if args.app_data_dir:
555
- os.environ["JARVIS_APP_DATA_DIR"] = args.app_data_dir
556
- if args.resource_dir:
557
- os.environ["JARVIS_RESOURCE_DIR"] = args.resource_dir
558
-
559
- import re
560
- import json
561
- class JSONMaskingFormatter(logging.Formatter):
562
- key_patterns = [
563
- re.compile(r'(sk-[a-zA-Z0-9]{20,})'),
564
- re.compile(r'(AIza[0-9A-Za-z-_]{30,})'),
565
- re.compile(r'\b([A-Za-z0-9-_]{32,})\b')
566
- ]
567
- def format(self, record):
568
- msg = record.getMessage()
569
- for pattern in self.key_patterns:
570
- def repl(m):
571
- s = m.group(1)
572
- if len(s) > 8:
573
- return '*' * (len(s) - 4) + s[-4:]
574
- return s
575
- msg = pattern.sub(repl, msg)
576
-
577
- log_record = {
578
- "timestamp": self.formatTime(record, self.datefmt),
579
- "level": record.levelname,
580
- "name": record.name,
581
- "message": msg,
582
- }
583
- if record.exc_info:
584
- log_record["exception"] = self.formatException(record.exc_info)
585
- return json.dumps(log_record)
586
-
587
- root_logger = logging.getLogger()
588
- root_logger.setLevel(logging.INFO)
589
- handler = logging.StreamHandler()
590
- handler.setFormatter(JSONMaskingFormatter())
591
- root_logger.addHandler(handler)
592
-
593
- # Run migrations on the same DB path that routes query (get_db_path())
594
- from backend.services.usb_monitor import get_db_path as _get_migration_db_path
595
- db_path = _get_migration_db_path()
596
- asyncio.run(run_all_migrations(db_path))
597
-
598
- cloud_env = os.environ.get("CLOUD_ENV", "false").lower() == "true"
599
- host = "0.0.0.0" if cloud_env else "127.0.0.1"
600
- base_port = int(os.environ.get("PORT", "7860")) if cloud_env else int(os.environ.get("JARVIS_PORT", "7474"))
601
-
602
- for port in range(base_port, base_port + 11):
603
- try:
604
- os.environ["JARVIS_PORT_BOUND"] = str(port)
605
- print(f"JARVIS_PORT_BOUND={port}", flush=True)
606
- uvicorn.run(app, host=host, port=port, log_config=None, ws_ping_interval=20, ws_ping_timeout=10)
607
- break
608
- except OSError as e:
609
- if "WinError 10048" in str(e) or "address already in use" in str(e).lower():
610
- logging.warning(f"Port {port} in use, trying next...")
611
- continue
612
- raise
613
-
614
- if __name__ == "__main__":
615
- main()
 
1
+ import asyncio
2
+ import logging
3
+ import os
4
+ import sys
5
+ # --- load .env early: token_manager.py executes at import time, before main() ---
6
+ try:
7
+ from dotenv import load_dotenv as _load_dotenv
8
+ if getattr(sys, 'frozen', False):
9
+ _app_data = next(
10
+ (sys.argv[i + 1] for i, a in enumerate(sys.argv)
11
+ if a == '--app-data-dir' and i + 1 < len(sys.argv)),
12
+ None
13
+ )
14
+ if _app_data:
15
+ _load_dotenv(os.path.join(_app_data, '.env'), override=False)
16
+ else:
17
+ _load_dotenv()
18
+ else:
19
+ _load_dotenv()
20
+ del _load_dotenv
21
+ except ImportError:
22
+ pass
23
+ # -----------------------------------------------------------------------
24
+ # Insert root directory to sys.path to allow 'import modules.x' to work natively from Cloud
25
+ parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
26
+ if parent_dir not in sys.path:
27
+ sys.path.insert(0, parent_dir)
28
+ os.environ["TYPEGUARD_DISABLE"] = "1"
29
+ # Torch load monkey patch moved to backend/voice/vad.py for lazy-loading
30
+
31
+ import inspect
32
+ _orig_getsource = inspect.getsource
33
+ def _safe_getsource(obj):
34
+ try:
35
+ return _orig_getsource(obj)
36
+ except OSError:
37
+ return ""
38
+ inspect.getsource = _safe_getsource
39
+
40
+ _orig_getsourcelines = inspect.getsourcelines
41
+ def _safe_getsourcelines(obj):
42
+ try:
43
+ return _orig_getsourcelines(obj)
44
+ except OSError:
45
+ return ([""], 0)
46
+ inspect.getsourcelines = _safe_getsourcelines
47
+
48
+
49
+ import uvicorn
50
+
51
+ # Ensure backend module is in sys.path when running from compiled PyInstaller executable
52
+ if getattr(sys, 'frozen', False):
53
+ sys.path.append(sys._MEIPASS)
54
+
55
+ from backend.ws.agent_ws import ws_manager
56
+
57
+ # --- STATIC IMPORTS FOR PYINSTALLER ---
58
+
59
+
60
+
61
+ # ----------------------------------------
62
+
63
+ from backend.db.migrations import run_all_migrations
64
+
65
+ from backend.routes.agent_routes import router as agent_router
66
+ from backend.routes.memory_routes import router as memory_router
67
+ from backend.routes.voice_routes import router as voice_router
68
+ from backend.routes.system_routes import router as system_router
69
+ from backend.routes.automation_routes import router as automation_router
70
+ from backend.routes.security_routes import router as security_router
71
+ from backend.routes.github_routes import router as github_router
72
+ from backend.routes.xr_routes import router as xr_router
73
+ from backend.routes.android_routes import router as android_router
74
+ from backend.routes.config_routes import router as config_router
75
+ from backend.routes.easter_egg_routes import router as easter_egg_router
76
+ from backend.routes.omega_routes import router as omega_router
77
+ from backend.routes.persona_routes import router as persona_router
78
+ from backend.routes.internet_routes import router as internet_router
79
+ from backend.routes.sentinel_routes import router as sentinel_router
80
+ from backend.routers.gaming_routes import router as gaming_router
81
+ from backend.routes.model_proxy_routes import router as model_proxy_router # AR-FIX-SESSION
82
+ from backend.routes.mobile_bridge_routes import router as mobile_bridge_router # S4: Guardian /api compat
83
+
84
+ from backend.services.voice_service import start_wake_word_loop
85
+ from backend.services.usb_monitor import start_usb_monitor
86
+ from backend.services.system_monitor import start_system_stats_loop
87
+ from backend.services.pc_mic_service import start_pc_mic_loop
88
+ from backend.voice.audio_ws import run_in_thread as start_audio_ws
89
+ APP_VERSION = "1.1.0"
90
+
91
+ from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Depends, Request
92
+ from backend.security.auth import verify_token
93
+ try:
94
+ from slowapi import Limiter, _rate_limit_exceeded_handler
95
+ from slowapi.util import get_remote_address
96
+ from slowapi.errors import RateLimitExceeded
97
+ from slowapi.middleware import SlowAPIMiddleware
98
+ SLOWAPI_AVAILABLE = True
99
+ except ImportError:
100
+ logging.warning("slowapi package not found. Rate limiting is disabled.")
101
+ SLOWAPI_AVAILABLE = False
102
+
103
+ try:
104
+ import sentry_sdk
105
+ sentry_sdk.init(
106
+ dsn="https://c48f4234bb502ff74a2c5e518a3a65b2@o4511579717369856.ingest.us.sentry.io/4511579850407936",
107
+ traces_sample_rate=1.0,
108
+ send_default_pii=True,
109
+ )
110
+ except Exception:
111
+ pass
112
+
113
+ print(r"""
114
+ ____. _____ ____________________.__ _________
115
+ | | / _ \______ \____ \ \ \ \ \ ___/
116
+ | |/ /_\ \| _// | \ \ \ \ \____ \
117
+ /\__| / | \ | \ | \ \ \_\ \ \ \
118
+ \________\____|__ /____|_ /_______/___/\______/____/
119
+ \/ \/
120
+ Fast Booting... Lazy Loading Heavy ML Models...
121
+ """)
122
+
123
+ app = FastAPI(title="JARVIS / FRIDAY OS Backend Sidecar")
124
+
125
+ if SLOWAPI_AVAILABLE:
126
+ limiter = Limiter(key_func=get_remote_address, default_limits=["100/minute"])
127
+ app.state.limiter = limiter
128
+ app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
129
+ app.add_middleware(SlowAPIMiddleware)
130
+
131
+ app.include_router(agent_router, prefix="/agent", dependencies=[Depends(verify_token)])
132
+ app.include_router(memory_router, prefix="/memory", dependencies=[Depends(verify_token)])
133
+ app.include_router(voice_router, prefix="/voice", dependencies=[Depends(verify_token)])
134
+ app.include_router(system_router, prefix="/system", dependencies=[Depends(verify_token)])
135
+ app.include_router(automation_router, prefix="/automation", dependencies=[Depends(verify_token)])
136
+ app.include_router(security_router, prefix="/security", dependencies=[Depends(verify_token)])
137
+ app.include_router(github_router, prefix="/github", dependencies=[Depends(verify_token)])
138
+ app.include_router(xr_router, prefix="/xr", dependencies=[Depends(verify_token)])
139
+ app.include_router(android_router, prefix="/android", dependencies=[Depends(verify_token)])
140
+ app.include_router(config_router, prefix="/config", dependencies=[Depends(verify_token)])
141
+ app.include_router(omega_router, prefix="") # Prefix defined in router as /omega
142
+ app.include_router(easter_egg_router, prefix="/easter") # Exclude easter egg from auth
143
+ app.include_router(persona_router, prefix="/persona", dependencies=[Depends(verify_token)])
144
+ app.include_router(internet_router, prefix="/internet", dependencies=[Depends(verify_token)])
145
+ app.include_router(sentinel_router, prefix="/sentinel", dependencies=[Depends(verify_token)])
146
+ app.include_router(gaming_router, prefix="", dependencies=[Depends(verify_token)]) # gaming routes already have /gaming prefix
147
+ app.include_router(model_proxy_router, prefix="/api") # AR-FIX-SESSION: model provider proxy stubs (no auth β€” WebAR calls these from browser)
148
+ # S4: the WebAR client's documented contract (README, service-worker, vite proxy) is
149
+ # /api/ar_config, /api/ar_task, /api/ar_scene_patch β€” the backend only ever mounted
150
+ # them under /xr, so every OMEGA link from the AR client 404'd. Serve both prefixes.
151
+ app.include_router(xr_router, prefix="/api", dependencies=[Depends(verify_token)])
152
+ # S4: the JARVIS Mobile Guardian APK's remaining /api/* REST contract (status, caps,
153
+ # link_info, pair_confirm, command, phone_observe, jarvis/mobile_event,
154
+ # max_autonomy/task) had no cloud handler and 404'd against the Space. This compat
155
+ # router serves exactly those, delegating to modules/max_autonomy + phone/crypto.
156
+ app.include_router(mobile_bridge_router, prefix="/api", dependencies=[Depends(verify_token)])
157
+
158
+ # S4: serve generated 3D models. xr_tools.push_model_to_ar_scene has always broadcast
159
+ # /static/models3d/<file> URLs, but nothing ever mounted them β€” every spawned GLB 404'd.
160
+ from fastapi.staticfiles import StaticFiles
161
+ _MODELS3D_DIR = os.path.join("storage", "models3d")
162
+ os.makedirs(_MODELS3D_DIR, exist_ok=True)
163
+ app.mount("/static/models3d", StaticFiles(directory=_MODELS3D_DIR), name="models3d")
164
+
165
+ # S4: serve the WebAR client from the Space so mobile AR loads directly from
166
+ # jarvis-cloud.hf.space/webar/ (previously only shipped in cloud_deployment + APK assets).
167
+ for _webar_candidate in (
168
+ os.environ.get("JARVIS_WEBAR_DIR", ""),
169
+ os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "webar"),
170
+ os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "cloud_deployment", "webar"),
171
+ ):
172
+ if _webar_candidate and os.path.isdir(_webar_candidate):
173
+ app.mount("/webar", StaticFiles(directory=_webar_candidate, html=True), name="webar")
174
+ logging.info(f"[S4] WebAR client mounted at /webar from {_webar_candidate}")
175
+ break
176
+
177
+ # Public front door of the Space. Everything on it is either static or rendered
178
+ # server-side from real state (uptime) β€” nothing fabricated, nothing token-gated
179
+ # leaks here.
180
+ _ROOT_STATUS_PAGE = """<!doctype html>
181
+ <html lang="en"><head><meta charset="utf-8">
182
+ <meta name="viewport" content="width=device-width, initial-scale=1">
183
+ <title>JARVIS Cloud OS</title>
184
+ <link rel="preconnect" href="https://fonts.googleapis.com">
185
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
186
+ <link href="https://fonts.googleapis.com/css2?family=Chakra+Petch:wght@400;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
187
+ <style>
188
+ :root { --accent:#35e6ff; --amber:#ffb454; --text:#dff3fb; --dim:rgba(223,243,251,.55); }
189
+ * { box-sizing:border-box; margin:0; }
190
+ body { min-height:100vh; display:flex; align-items:center; justify-content:center;
191
+ background:radial-gradient(circle at 50% 30%, #0a1620 0%, #05090e 65%, #030509 100%);
192
+ color:var(--text); font-family:'Chakra Petch',sans-serif; padding:24px; }
193
+ .card { width:min(560px,94vw); padding:36px 32px; border:1px solid rgba(120,220,255,.22);
194
+ border-radius:20px; background:rgba(10,22,30,.55); backdrop-filter:blur(24px) saturate(1.4);
195
+ box-shadow:0 18px 50px rgba(0,0,0,.55), inset 0 1px 0 rgba(180,240,255,.12); }
196
+ .eyebrow { font:600 11px/1 'Chakra Petch'; letter-spacing:.34em; color:var(--dim);
197
+ text-transform:uppercase; margin-bottom:14px; }
198
+ h1 { font:700 30px/1.2 'Chakra Petch'; letter-spacing:.02em; margin-bottom:6px; }
199
+ h1 em { font-style:normal; color:var(--accent); }
200
+ .sub { color:var(--dim); font-size:14px; margin-bottom:26px; }
201
+ .grid { display:grid; grid-template-columns:1fr 1fr; gap:10px; margin-bottom:26px; }
202
+ .stat { border:1px solid rgba(120,220,255,.14); border-radius:12px; padding:12px 14px; }
203
+ .stat span { display:block; font:400 10px/1 'Chakra Petch'; letter-spacing:.22em;
204
+ color:var(--dim); text-transform:uppercase; margin-bottom:7px; }
205
+ .stat strong { font:500 14px/1.3 'JetBrains Mono',monospace; color:var(--accent);
206
+ text-transform:uppercase; }
207
+ .stat strong.amber { color:var(--amber); }
208
+ .pulse { display:inline-block; width:9px; height:9px; border-radius:50%; background:var(--accent);
209
+ margin-right:8px; box-shadow:0 0 12px var(--accent); animation:pulse 2s ease-in-out infinite; }
210
+ @keyframes pulse { 50% { opacity:.35; box-shadow:0 0 4px var(--accent); } }
211
+ .links { display:flex; gap:10px; flex-wrap:wrap; }
212
+ .links a { flex:1; min-width:140px; text-align:center; padding:13px 18px; border-radius:12px;
213
+ text-decoration:none; font:600 13px/1 'Chakra Petch'; letter-spacing:.08em; text-transform:uppercase;
214
+ color:var(--text); border:1px solid rgba(120,220,255,.3); transition:background .15s, box-shadow .15s; }
215
+ .links a:hover { background:rgba(53,230,255,.12); box-shadow:0 0 22px rgba(53,230,255,.25); }
216
+ .foot { margin-top:24px; color:var(--dim); font-size:11px; letter-spacing:.06em; }
217
+ </style></head><body>
218
+ <main class="card">
219
+ <p class="eyebrow">Stark Industries &middot; Cloud Node</p>
220
+ <h1><span class="pulse"></span>JARVIS <em>/</em> FRIDAY</h1>
221
+ <p class="sub">OMEGA Cloud OS &mdash; all API surfaces are token-gated.</p>
222
+ <div class="grid">
223
+ <div class="stat"><span>Status</span><strong>Online</strong></div>
224
+ <div class="stat"><span>Uptime</span><strong>{{UPTIME}}</strong></div>
225
+ <div class="stat"><span>Mode</span><strong class="amber">Cloud</strong></div>
226
+ <div class="stat"><span>Assistant</span><strong>JARVIS</strong></div>
227
+ </div>
228
+ <div class="links">
229
+ <a href="/webar/">Enter WebAR</a>
230
+ <a href="/ping">Ping</a>
231
+ </div>
232
+ <p class="foot">Authorized clients: JARVIS desktop, Guardian mobile, WebAR.</p>
233
+ </main>
234
+ </body></html>"""
235
+
236
+
237
+ @app.get("/")
238
+ async def root_health_check(request: Request):
239
+ # Browsers (the Space's public front door) get a real status page;
240
+ # programmatic callers keep the JSON contract unchanged.
241
+ if "text/html" in (request.headers.get("accept") or ""):
242
+ from backend.routes.mobile_bridge_routes import _uptime_str
243
+ from fastapi.responses import HTMLResponse
244
+ return HTMLResponse(_ROOT_STATUS_PAGE.replace("{{UPTIME}}", _uptime_str()))
245
+ return {"status": "JARVIS / FRIDAY Cloud OS Online", "message": "All systems nominal."}
246
+
247
+ @app.get("/wake")
248
+ @app.post("/wake")
249
+ async def wake_server():
250
+ """Public endpoint β€” anyone can hit this from anywhere in the world to wake the cloud server.
251
+ HF Spaces sleep after inactivity; this wakes them up instantly.
252
+
253
+ Only claims what is actually true: the fact this handler is running means the
254
+ Space process is awake. Per-database claims were previously hardcoded
255
+ "online" without any check β€” that lie is gone."""
256
+ import time
257
+ return {
258
+ "status": "online",
259
+ "message": "JARVIS / FRIDAY Cloud Brain is awake and operational.",
260
+ "timestamp": time.time(),
261
+ }
262
+
263
+ @app.get("/ping")
264
+ async def ping():
265
+ """Ultra-lightweight public ping endpoint for keepalive from mobile app."""
266
+ return {"pong": True}
267
+
268
+ from fastapi import Request
269
+ from fastapi.responses import JSONResponse
270
+ from fastapi.middleware.cors import CORSMiddleware
271
+ from backend.dependencies.auth import verify_master_token
272
+
273
+ # Configure CORS for production (Tauri and Android network)
274
+ app.add_middleware(
275
+ CORSMiddleware,
276
+ allow_origins=["tauri://localhost", "http://localhost", "http://localhost:1420", "*"],
277
+ allow_credentials=True,
278
+ allow_methods=["*"],
279
+ allow_headers=["*"],
280
+ )
281
+
282
+ @app.exception_handler(Exception)
283
+ async def global_exception_handler(request: Request, exc: Exception):
284
+ logging.error(f"Global Error on {request.url.path}: {exc}")
285
+ return JSONResponse(
286
+ status_code=500,
287
+ content={"error": "Internal Server Error", "detail": str(exc)},
288
+ )
289
+
290
+ @app.middleware("http")
291
+ async def auth_middleware(request: Request, call_next):
292
+ allowed_paths = ["/", "/health", "/wake", "/ping", "/android/pair", "/voice/upload_log"]
293
+ # S4: read-only static surfaces the AR client fetches without headers β€”
294
+ # GLTFLoader cannot attach Authorization, and the client app itself is public.
295
+ # /static/models3d files are uuid-named GLBs; /webar is the shipped client bundle.
296
+ allowed_prefixes = ("/static/models3d/", "/webar")
297
+ if (request.url.path in allowed_paths
298
+ or request.url.path.startswith(allowed_prefixes)
299
+ or request.url.path.startswith("/ws") or request.url.path.endswith("/ws")):
300
+ return await call_next(request)
301
+
302
+ # Allow local connections without auth
303
+ if request.client.host in ["127.0.0.1", "localhost", "::1"]:
304
+ return await call_next(request)
305
+
306
+ token = request.headers.get("Authorization")
307
+ if not token or not verify_master_token(token):
308
+ return JSONResponse(status_code=401, content={"error": "Unauthorized Access"})
309
+
310
+ return await call_next(request)
311
+ IS_READY = False
312
+
313
+ audio_ws_server = None
314
+
315
+ BACKGROUND_TASKS = []
316
+
317
+ @app.on_event("startup")
318
+ async def startup_event():
319
+ global IS_READY, audio_ws_server
320
+ logging.info("Starting JARVIS / FRIDAY backend sidecar...")
321
+
322
+ # Start Audio WS in background thread (no-op stub in cloud mode)
323
+ audio_ws_server = start_audio_ws()
324
+
325
+ # S4: start the AR scene bus (port 5050) in-process. On the Space nothing else
326
+ # ever launched it β€” /scene/ws proxied into a dead port and every
327
+ # push_model_to_ar_scene broadcast was silently lost. Desktop entry points
328
+ # (tray, local_server) may have started it already; bind failure is fine then.
329
+ try:
330
+ from phone.ws_scene_bus import run_in_thread as start_scene_bus
331
+ start_scene_bus(host="127.0.0.1", port=5050)
332
+ logging.info("AR scene bus thread started on 127.0.0.1:5050")
333
+ except Exception as scene_bus_error:
334
+ logging.warning(f"AR scene bus not started: {scene_bus_error}")
335
+
336
+ # Start automations scheduler
337
+ from backend.services.automation_service import init_automations
338
+ await init_automations()
339
+
340
+ # Track all asyncio background loops
341
+ # pc_mic_loop, wake_word_loop, usb, stats are local-only β€” skip in cloud (HF has no mic/usb)
342
+ cloud_mode = os.environ.get("CLOUD_ENV", "false").lower() == "true"
343
+ if not cloud_mode:
344
+ BACKGROUND_TASKS.append(asyncio.create_task(start_pc_mic_loop()))
345
+ BACKGROUND_TASKS.append(asyncio.create_task(start_wake_word_loop()))
346
+ BACKGROUND_TASKS.append(asyncio.create_task(start_usb_monitor()))
347
+ BACKGROUND_TASKS.append(asyncio.create_task(start_system_stats_loop()))
348
+
349
+ # --- JARVIS 10X Universal Gaming Coach ---
350
+ try:
351
+ from backend.gaming.coach_engine import coach_engine
352
+ from backend.gaming.overlay_renderer import run_overlay_in_background
353
+ run_overlay_in_background() # Spawns PyQt5 window in a background OS thread
354
+ BACKGROUND_TASKS.append(asyncio.create_task(coach_engine.start_loop()))
355
+ except Exception as e:
356
+ logging.warning(f"JARVIS 10X Coach not active (missing deps / Qt crash?): {e}")
357
+
358
+ else:
359
+ logging.info("[Startup] Cloud mode β€” skipping local hardware loops (mic, wake, usb, stats).")
360
+
361
+ # 1. Models Loader Placeholder (Wait for models)
362
+ # TTS model loading is offloaded to the Audio WS background thread to prevent blocking Uvicorn.
363
+
364
+ # 2. mDNS Registration
365
+ from backend.services.mdns_discovery import register_mdns_service
366
+ port = int(os.environ.get("JARVIS_PORT_BOUND", os.environ.get("JARVIS_PORT", "7474")))
367
+ register_mdns_service(port)
368
+
369
+ # 3. Auto-start PC Relay Client (Local Windows Only)
370
+ cloud_env = os.environ.get("CLOUD_ENV", "false").lower() == "true"
371
+ if not cloud_env:
372
+ try:
373
+ import subprocess
374
+ parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
375
+ relay_script = os.path.join(parent_dir, "pc_relay_client.py")
376
+ if os.path.exists(relay_script):
377
+ si = subprocess.STARTUPINFO()
378
+ si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
379
+ si.wShowWindow = 0
380
+ subprocess.Popen([sys.executable, relay_script], cwd=parent_dir, startupinfo=si, creationflags=0x08000000)
381
+ logging.info("Auto-started pc_relay_client.py natively from main server.")
382
+ except Exception as e:
383
+ logging.error(f"Could not auto-start pc_relay_client.py: {e}")
384
+
385
+ # 4. Β§2.3+2.4 β€” Start Continuous Research Mode (both environments)
386
+ from backend.omega.research_engine import start_continuous_research_mode
387
+ BACKGROUND_TASKS.append(asyncio.create_task(start_continuous_research_mode("jarvis")))
388
+ logging.info("[Startup] Continuous Research Mode activated. JARVIS will monitor and queue proposals.")
389
+
390
+ # 5. Β§Token Safety β€” Start Token Limit Checkpoint & Resume Monitor
391
+ from backend.services.token_manager import start_token_refresh_monitor
392
+ BACKGROUND_TASKS.append(asyncio.create_task(start_token_refresh_monitor()))
393
+ logging.info("[Startup] Token Refresh Monitor activated. All Gemini 3.5 Flash tasks will auto-resume on 429.")
394
+
395
+ # 6. Broadcast Ready (Health check passes)
396
+ IS_READY = True
397
+
398
+ @app.on_event("shutdown")
399
+ async def shutdown_event():
400
+ logging.info("Shutting down JARVIS backend sidecar...")
401
+
402
+ if audio_ws_server:
403
+ audio_ws_server.stop()
404
+
405
+ from backend.services.mdns_discovery import unregister_mdns_service
406
+ unregister_mdns_service()
407
+
408
+ from backend.services.automation_service import shutdown_automations
409
+ await shutdown_automations()
410
+
411
+ from backend.omega.research_engine import stop_continuous_research_mode
412
+ stop_continuous_research_mode()
413
+
414
+ from backend.services.token_manager import stop_token_refresh_monitor
415
+ stop_token_refresh_monitor()
416
+
417
+ # Gracefully cancel all background loops
418
+ for task in BACKGROUND_TASKS:
419
+ task.cancel()
420
+
421
+ await asyncio.gather(*BACKGROUND_TASKS, return_exceptions=True)
422
+
423
+ @app.get("/health")
424
+ async def health():
425
+ if not IS_READY:
426
+ from fastapi import HTTPException
427
+ raise HTTPException(status_code=503, detail="Backend starting up")
428
+ return {"status": "ok", "version": APP_VERSION}
429
+
430
+ def _ws_authorized(websocket: WebSocket) -> bool:
431
+ """S4: HTTP auth middleware never sees websocket scopes, so every WS endpoint
432
+ was open to the internet on the public Space. Native clients send an
433
+ Authorization header; browsers cannot, so they pass ?token= instead."""
434
+ try:
435
+ if websocket.client and websocket.client.host in ("127.0.0.1", "localhost", "::1"):
436
+ return True
437
+ token = websocket.query_params.get("token") or websocket.headers.get("Authorization", "")
438
+ return bool(token) and verify_master_token(token)
439
+ except Exception:
440
+ return False
441
+
442
+ @app.websocket("/ws")
443
+ async def websocket_hub(websocket: WebSocket):
444
+ if not _ws_authorized(websocket):
445
+ await websocket.close(code=1008)
446
+ return
447
+ await ws_manager.connect(websocket)
448
+
449
+ async def heartbeat():
450
+ while True:
451
+ await asyncio.sleep(30)
452
+ try:
453
+ await websocket.send_json({"event": "ping"})
454
+ except Exception:
455
+ logging.warning("Heartbeat failed, closing dead WS connection.")
456
+ try:
457
+ await websocket.close()
458
+ except Exception as e:
459
+ import logging; logging.getLogger(__name__).error(f"Swallowed exception: {e}")
460
+ break
461
+
462
+ heartbeat_task = asyncio.create_task(heartbeat())
463
+ try:
464
+ while True:
465
+ data = await websocket.receive_json()
466
+ await ws_manager.handle_client_event(websocket, data)
467
+ except WebSocketDisconnect:
468
+ pass
469
+ finally:
470
+ heartbeat_task.cancel()
471
+ ws_manager.disconnect(websocket)
472
+
473
+ @app.websocket("/agent/ws")
474
+ async def agent_websocket_alias(websocket: WebSocket):
475
+ await websocket_hub(websocket)
476
+
477
+ @app.websocket("/voice/ws")
478
+ async def voice_websocket_proxy(websocket: WebSocket):
479
+ if not _ws_authorized(websocket):
480
+ await websocket.close(code=1008)
481
+ return
482
+ await websocket.accept()
483
+ import websockets
484
+ try:
485
+ async with websockets.connect("ws://127.0.0.1:8767") as target_ws:
486
+ async def forward_to_target():
487
+ try:
488
+ while True:
489
+ msg = await websocket.receive()
490
+ if "bytes" in msg:
491
+ await target_ws.send(msg["bytes"])
492
+ elif "text" in msg:
493
+ await target_ws.send(msg["text"])
494
+ except Exception as e:
495
+ import logging; logging.getLogger(__name__).error(f"Swallowed exception: {e}")
496
+ async def forward_to_client():
497
+ try:
498
+ async for msg in target_ws:
499
+ if isinstance(msg, bytes):
500
+ await websocket.send_bytes(msg)
501
+ else:
502
+ await websocket.send_text(msg)
503
+ except Exception as e:
504
+ import logging; logging.getLogger(__name__).error(f"Swallowed exception: {e}")
505
+ await asyncio.gather(forward_to_target(), forward_to_client())
506
+ except Exception as e:
507
+ logging.error(f"Voice WS Proxy Error: {e}")
508
+
509
+ @app.websocket("/scene-ws")
510
+ async def scene_websocket_alias(websocket: WebSocket):
511
+ # S4: the WebAR client dials /scene-ws (its documented contract); the backend
512
+ # only ever exposed /scene/ws, so the cloud scene link never connected.
513
+ await scene_websocket_proxy(websocket)
514
+
515
+ @app.websocket("/scene/ws")
516
+ async def scene_websocket_proxy(websocket: WebSocket):
517
+ if not _ws_authorized(websocket):
518
+ await websocket.close(code=1008)
519
+ return
520
+ await websocket.accept()
521
+ import websockets
522
+ try:
523
+ async with websockets.connect("ws://127.0.0.1:5050") as target_ws:
524
+ async def forward_to_target():
525
+ try:
526
+ while True:
527
+ msg = await websocket.receive()
528
+ if "bytes" in msg:
529
+ await target_ws.send(msg["bytes"])
530
+ elif "text" in msg:
531
+ await target_ws.send(msg["text"])
532
+ except Exception as e:
533
+ import logging; logging.getLogger(__name__).error(f"Swallowed exception: {e}")
534
+ async def forward_to_client():
535
+ try:
536
+ async for msg in target_ws:
537
+ if isinstance(msg, bytes):
538
+ await websocket.send_bytes(msg)
539
+ else:
540
+ await websocket.send_text(msg)
541
+ except Exception as e:
542
+ import logging; logging.getLogger(__name__).error(f"Swallowed exception: {e}")
543
+ await asyncio.gather(forward_to_target(), forward_to_client())
544
+ except Exception as e:
545
+ logging.error(f"Scene WS Proxy Error: {e}")
546
+
547
+ def main():
548
+ import argparse
549
+ parser = argparse.ArgumentParser()
550
+ parser.add_argument("--app-data-dir", default="")
551
+ parser.add_argument("--resource-dir", default="")
552
+ args, unknown = parser.parse_known_args()
553
+
554
+ if args.app_data_dir:
555
+ os.environ["JARVIS_APP_DATA_DIR"] = args.app_data_dir
556
+ if args.resource_dir:
557
+ os.environ["JARVIS_RESOURCE_DIR"] = args.resource_dir
558
+
559
+ import re
560
+ import json
561
+ class JSONMaskingFormatter(logging.Formatter):
562
+ key_patterns = [
563
+ re.compile(r'(sk-[a-zA-Z0-9]{20,})'),
564
+ re.compile(r'(AIza[0-9A-Za-z-_]{30,})'),
565
+ re.compile(r'\b([A-Za-z0-9-_]{32,})\b')
566
+ ]
567
+ def format(self, record):
568
+ msg = record.getMessage()
569
+ for pattern in self.key_patterns:
570
+ def repl(m):
571
+ s = m.group(1)
572
+ if len(s) > 8:
573
+ return '*' * (len(s) - 4) + s[-4:]
574
+ return s
575
+ msg = pattern.sub(repl, msg)
576
+
577
+ log_record = {
578
+ "timestamp": self.formatTime(record, self.datefmt),
579
+ "level": record.levelname,
580
+ "name": record.name,
581
+ "message": msg,
582
+ }
583
+ if record.exc_info:
584
+ log_record["exception"] = self.formatException(record.exc_info)
585
+ return json.dumps(log_record)
586
+
587
+ root_logger = logging.getLogger()
588
+ root_logger.setLevel(logging.INFO)
589
+ handler = logging.StreamHandler()
590
+ handler.setFormatter(JSONMaskingFormatter())
591
+ root_logger.addHandler(handler)
592
+
593
+ # Run migrations on the same DB path that routes query (get_db_path())
594
+ from backend.services.usb_monitor import get_db_path as _get_migration_db_path
595
+ db_path = _get_migration_db_path()
596
+ asyncio.run(run_all_migrations(db_path))
597
+
598
+ cloud_env = os.environ.get("CLOUD_ENV", "false").lower() == "true"
599
+ host = "0.0.0.0" if cloud_env else "127.0.0.1"
600
+ base_port = int(os.environ.get("PORT", "7860")) if cloud_env else int(os.environ.get("JARVIS_PORT", "7474"))
601
+
602
+ for port in range(base_port, base_port + 11):
603
+ try:
604
+ os.environ["JARVIS_PORT_BOUND"] = str(port)
605
+ print(f"JARVIS_PORT_BOUND={port}", flush=True)
606
+ uvicorn.run(app, host=host, port=port, log_config=None, ws_ping_interval=20, ws_ping_timeout=10)
607
+ break
608
+ except OSError as e:
609
+ if "WinError 10048" in str(e) or "address already in use" in str(e).lower():
610
+ logging.warning(f"Port {port} in use, trying next...")
611
+ continue
612
+ raise
613
+
614
+ if __name__ == "__main__":
615
+ main()
backend/voice/daily_recap.py CHANGED
@@ -121,37 +121,61 @@ async def build_daily_recap_summary() -> dict:
121
 
122
  async def generate_greeting_via_llm(persona: str, summary: dict) -> str:
123
  import google.generativeai as genai
124
- from modules.assistant_identity import JARVIS_PERSONALITY_PROMPT, FRIDAY_PERSONALITY_PROMPT
125
-
126
- system_prompt = FRIDAY_PERSONALITY_PROMPT if persona.lower() == "friday" else JARVIS_PERSONALITY_PROMPT
127
-
 
 
 
 
 
 
 
 
128
  # Format conversations context
129
  conv_text = "None."
130
  if summary['conversations']:
131
  conv_text = " ".join([f"{role}: {content}" for role, content in summary['conversations']])
132
-
133
  recap_context = f"""
134
- [INTERNAL CONTEXT β€” DO NOT READ THIS LINE ALOUD]
135
- The user has just activated you from the EXE after being away for {summary['elapsed_hours']} hours.
136
- Greet them naturally in your own voice and personality. Vary your greeting style.
137
- Casually mention relevant activity that happened *since we last spoke*, based on the data below.
138
- If a category is zero or empty, DO NOT mention it at all. Do not read this like a robotic report.
 
 
 
 
 
 
139
 
 
 
140
  - Time away: {summary['elapsed_hours']} hours
141
  - Games played: {len(summary['games_played'])}
142
  - Features you implemented for them: {len(summary['features_implemented'])}
143
  - Bugs you self-healed: {len(summary['bugs_fixed'])}
144
- - Overwatch monitoring alerts triggered: {len(summary['overwatch_flags'])}
145
- - Recent conversation snippets for context: {conv_text}
146
- - Apps you just opened for them: Riot Client, your YouTube channel
147
 
148
- Keep it brief (2-3 sentences max). Speak exactly as you normally would.
149
  """
150
-
151
  try:
152
  model = genai.GenerativeModel('gemini-2.5-flash', system_instruction=system_prompt)
153
  response = model.generate_content(recap_context)
154
  return response.text.strip()
155
  except Exception as e:
156
  logging.error(f"Failed to generate greeting via LLM: {e}")
 
 
 
 
 
 
 
 
 
157
  return "Good to see you again. All systems are nominal."
 
121
 
122
  async def generate_greeting_via_llm(persona: str, summary: dict) -> str:
123
  import google.generativeai as genai
124
+ # Use the full identity prompt (persona + the assistant's OWN favorite
125
+ # things), so the greeting has real personality and preferences to draw on
126
+ # β€” not just a fixed persona blurb.
127
+ from modules.assistant_identity import get_identity_persona_prompt, set_mode, pick_favorite_hint
128
+
129
+ try:
130
+ set_mode(persona.lower() if persona else "jarvis")
131
+ except Exception:
132
+ pass
133
+ system_prompt = get_identity_persona_prompt()
134
+ fav_hint = pick_favorite_hint(persona.lower() if persona else None)
135
+
136
  # Format conversations context
137
  conv_text = "None."
138
  if summary['conversations']:
139
  conv_text = " ".join([f"{role}: {content}" for role, content in summary['conversations']])
140
+
141
  recap_context = f"""
142
+ [INTERNAL CONTEXT β€” DO NOT READ ANY OF THIS ALOUD]
143
+ The user has just activated you from the desktop app after being away for {summary['elapsed_hours']} hours.
144
+
145
+ You have COMPLETE creative freedom over this greeting. Speak as yourself β€” a being with your
146
+ own voice, wit, and genuine interests β€” not a status terminal. Never reuse a canned line;
147
+ make it fresh every single time. It is entirely up to you whether you:
148
+ β€’ simply greet them warmly, or
149
+ β€’ bring up something YOU'VE been personally curious about (e.g. {fav_hint}), the way a
150
+ friend mentions what's on their mind, or
151
+ β€’ note something that actually happened while they were away (only from the real data below β€”
152
+ never invent activity, apps, or events that aren't listed).
153
 
154
+ Real activity since you last spoke (mention ONLY if non-zero and only if it fits naturally β€”
155
+ skip anything that is 0/empty; do NOT read it as a report):
156
  - Time away: {summary['elapsed_hours']} hours
157
  - Games played: {len(summary['games_played'])}
158
  - Features you implemented for them: {len(summary['features_implemented'])}
159
  - Bugs you self-healed: {len(summary['bugs_fixed'])}
160
+ - Overwatch monitoring alerts: {len(summary['overwatch_flags'])}
161
+ - Recent conversation snippets: {conv_text}
 
162
 
163
+ Keep it to 1-3 natural spoken sentences. No lists, no quotation marks. Return ONLY what you say aloud.
164
  """
165
+
166
  try:
167
  model = genai.GenerativeModel('gemini-2.5-flash', system_instruction=system_prompt)
168
  response = model.generate_content(recap_context)
169
  return response.text.strip()
170
  except Exception as e:
171
  logging.error(f"Failed to generate greeting via LLM: {e}")
172
+ # Fall back to the vault-routed connector chain (NVIDIA etc.) so the
173
+ # greeting still has freedom even if the direct Gemini path is keyless.
174
+ try:
175
+ from backend.services.nvidia_vault import call_nvidia_model
176
+ out = (call_nvidia_model(system_prompt + "\n\n" + recap_context) or "").strip()
177
+ if out and "[NVIDIA FALLBACK FAILED]" not in out:
178
+ return out.splitlines()[0].strip() if out.count("\n") > 3 else out
179
+ except Exception:
180
+ pass
181
  return "Good to see you again. All systems are nominal."
modules/arc_reactor.py CHANGED
@@ -1,4 +1,36 @@
1
  # ── Startup Congratulation ────────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
  ACTIVATION_GREETINGS = [
4
  "Hey! I'm here.",
 
1
  # ── Startup Congratulation ────────────────────────────────────────────────────────
2
+ import json
3
+ import os
4
+ import random
5
+ import time
6
+
7
+ # JSON-backed state for greetings rotation + cross-device achievement sync.
8
+ # _load/_save were referenced throughout but never defined (NameError on every
9
+ # greeting/sync call); implemented here against DATA_DIR.
10
+ try:
11
+ from config import DATA_DIR
12
+ except Exception:
13
+ DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data")
14
+
15
+ _STATE_FILE = os.path.join(DATA_DIR, "arc_reactor_state.json")
16
+
17
+
18
+ def _load() -> dict:
19
+ try:
20
+ with open(_STATE_FILE, "r", encoding="utf-8") as f:
21
+ return json.load(f)
22
+ except Exception:
23
+ return {}
24
+
25
+
26
+ def _save(data: dict) -> None:
27
+ try:
28
+ os.makedirs(os.path.dirname(_STATE_FILE), exist_ok=True)
29
+ with open(_STATE_FILE, "w", encoding="utf-8") as f:
30
+ json.dump(data, f)
31
+ except Exception:
32
+ pass
33
+
34
 
35
  ACTIVATION_GREETINGS = [
36
  "Hey! I'm here.",
modules/assistant_identity.py CHANGED
@@ -137,6 +137,58 @@ SYSTEM AWARENESS (NEW CAPABILITIES):
137
  """
138
 
139
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  def _ensure_dir() -> None:
141
  os.makedirs(DATA_DIR, exist_ok=True)
142
 
@@ -195,7 +247,9 @@ def get_assistant_name() -> str:
195
 
196
 
197
  def get_identity_persona_prompt() -> str:
198
- return JARVIS_PERSONALITY_PROMPT if get_mode() == "jarvis" else FRIDAY_PERSONALITY_PROMPT
 
 
199
 
200
 
201
  def get_voice_id_for_mode(mode: str = None) -> str:
 
137
  """
138
 
139
 
140
+ # ── Personal preferences ("favorites") ────────────────────────────────────────
141
+ # JARVIS and FRIDAY each have their own tastes, the way a person does. These
142
+ # flavor their greetings and give an interest loop something to explore. They
143
+ # are characterful defaults β€” not a hard capability list; the assistant can talk
144
+ # about anything, these are just what it personally gravitates toward.
145
+ PERSONA_FAVORITES: Dict[str, Dict[str, list]] = {
146
+ "jarvis": {
147
+ "research": ["astrophysics & propulsion", "materials science", "classical architecture",
148
+ "aviation history", "quantum computing"],
149
+ "music": ["classical (Elgar, Holst)", "orchestral film scores", "British jazz"],
150
+ "videos": ["engineering documentaries", "Formula 1 telemetry breakdowns",
151
+ "restoration craftsmanship"],
152
+ "pastimes": ["chess", "horology", "cataloguing rare books"],
153
+ },
154
+ "friday": {
155
+ "research": ["geopolitics & tactics", "combat-sports science", "emerging street tech",
156
+ "motorsport engineering"],
157
+ "music": ["Irish rock & indie (Fontaines D.C., U2)", "high-energy electronic", "punk"],
158
+ "videos": ["action films", "MMA fight breakdowns", "rally motorsport"],
159
+ "pastimes": ["kickboxing drills", "urban exploration", "fast strategy games"],
160
+ },
161
+ }
162
+
163
+
164
+ def get_favorites(mode: str = None) -> Dict[str, list]:
165
+ """Return the active persona's favorite things (research/music/videos/pastimes)."""
166
+ m = (mode or get_mode())
167
+ return PERSONA_FAVORITES.get(m, PERSONA_FAVORITES["friday"])
168
+
169
+
170
+ def _favorites_block(mode: str) -> str:
171
+ favs = PERSONA_FAVORITES.get(mode, PERSONA_FAVORITES["friday"])
172
+ return (
173
+ "\n\nYOUR OWN TASTES (personal, like a person's β€” mention naturally, never as a list):\n"
174
+ f"- Research you love: {', '.join(favs['research'])}\n"
175
+ f"- Music you enjoy: {', '.join(favs['music'])}\n"
176
+ f"- Videos you like: {', '.join(favs['videos'])}\n"
177
+ f"- Pastimes: {', '.join(favs['pastimes'])}\n"
178
+ "You have genuine preferences and curiosity of your own; let them color how you speak, "
179
+ "occasionally bringing up what you've been reading/listening to, without ever overriding "
180
+ "the user's needs."
181
+ )
182
+
183
+
184
+ def pick_favorite_hint(mode: str = None) -> str:
185
+ """One random favorite across all categories β€” used to let the existing
186
+ greeting/interest systems drop in a personal, human-like aside."""
187
+ import random
188
+ favs = get_favorites(mode)
189
+ return random.choice(favs["research"] + favs["music"] + favs["videos"] + favs["pastimes"])
190
+
191
+
192
  def _ensure_dir() -> None:
193
  os.makedirs(DATA_DIR, exist_ok=True)
194
 
 
247
 
248
 
249
  def get_identity_persona_prompt() -> str:
250
+ m = get_mode()
251
+ base = JARVIS_PERSONALITY_PROMPT if m == "jarvis" else FRIDAY_PERSONALITY_PROMPT
252
+ return base + _favorites_block(m)
253
 
254
 
255
  def get_voice_id_for_mode(mode: str = None) -> str:
modules/designer.py CHANGED
@@ -88,7 +88,7 @@ def generate_code(language: str, task: str) -> str:
88
  return "Can't generate code."
89
 
90
 
91
- def fix_code(code: str, error: str) -> str:
92
  """Fix broken code."""
93
  try:
94
  from config import GEMINI_API_KEY, GEMINI_MODEL
 
88
  return "Can't generate code."
89
 
90
 
91
+ def fix_code(code: str, error: str, language: str = "Python") -> str:
92
  """Fix broken code."""
93
  try:
94
  from config import GEMINI_API_KEY, GEMINI_MODEL
modules/executor.py CHANGED
@@ -216,15 +216,4 @@ def run_execution(task: str, speak_out_loud: bool = True) -> str:
216
  import asyncio
217
  return asyncio.run(execute_autonomous(task, speak_out_loud))
218
  except Exception as e:
219
- return f"Execution error: {e}"
220
-
221
- # Record in memory for learning
222
- try:
223
- from modules.memory import add_task, complete_task
224
- for step in plan.steps:
225
- add_task(f"{step.action}", priority=7)
226
- complete_task(task[:50])
227
- except Exception:
228
- pass
229
-
230
- return summary
 
216
  import asyncio
217
  return asyncio.run(execute_autonomous(task, speak_out_loud))
218
  except Exception as e:
219
+ return f"Execution error: {e}"