Jarvis2345 commited on
Commit
e3592be
·
verified ·
1 Parent(s): 5a563a1

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

Browse files
backend/main.py CHANGED
@@ -173,7 +173,20 @@ if SLOWAPI_AVAILABLE:
173
 
174
  app.include_router(agent_router, prefix="/agent", dependencies=[Depends(verify_token)])
175
  app.include_router(memory_router, prefix="/memory", dependencies=[Depends(verify_token)])
176
- app.include_router(voice_router, prefix="/voice", dependencies=[Depends(verify_token)])
 
 
 
 
 
 
 
 
 
 
 
 
 
177
  app.include_router(system_router, prefix="/system", dependencies=[Depends(verify_token)])
178
  # v22: AUTOMATION is a paid feature in the plan catalog (plus/pro only — see
179
  # billing/plans.py), but no automation route enforced it: a caller on the `free` plan got
@@ -366,6 +379,10 @@ async def global_exception_handler(request: Request, exc: Exception):
366
 
367
  @app.middleware("http")
368
  async def auth_middleware(request: Request, call_next):
 
 
 
 
369
  allowed_paths = ["/", "/health", "/wake", "/ping", "/android/pair", "/voice/upload_log"]
370
  # S4: read-only static surfaces the AR client fetches without headers —
371
  # GLTFLoader cannot attach Authorization, and the client app itself is public.
@@ -417,6 +434,41 @@ async def startup_event():
417
  except Exception as scene_bus_error:
418
  logging.warning(f"AR scene bus not started: {scene_bus_error}")
419
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
420
  # Start automations scheduler
421
  from backend.services.automation_service import init_automations
422
  await init_automations()
 
173
 
174
  app.include_router(agent_router, prefix="/agent", dependencies=[Depends(verify_token)])
175
  app.include_router(memory_router, prefix="/memory", dependencies=[Depends(verify_token)])
176
+ # BOTH mounts stay behind verify_token.
177
+ #
178
+ # The dependency was removed from this router so the phone's MediaPlayer could
179
+ # fetch TTS by URL — but that opened the WHOLE voice router, not just speech
180
+ # synthesis: /voice/devices enumerates the host's audio hardware and
181
+ # /voice/set_device changes where it plays. The phone already holds a token; it
182
+ # simply was not sending it on that one call (AndroidAudioPlayer.playAudioUrl
183
+ # used the header-less setDataSource overload). Fixed on the client instead, so
184
+ # the endpoint does not have to be public for the app to work.
185
+ #
186
+ # /api/voice is the second mount because that is the path the Guardian app was
187
+ # built against; it is the same router, same auth.
188
+ app.include_router(voice_router, prefix="/voice", dependencies=[Depends(verify_token)])
189
+ app.include_router(voice_router, prefix="/api/voice", dependencies=[Depends(verify_token)])
190
  app.include_router(system_router, prefix="/system", dependencies=[Depends(verify_token)])
191
  # v22: AUTOMATION is a paid feature in the plan catalog (plus/pro only — see
192
  # billing/plans.py), but no automation route enforced it: a caller on the `free` plan got
 
379
 
380
  @app.middleware("http")
381
  async def auth_middleware(request: Request, call_next):
382
+ # /voice/speak and /api/voice/speak were added here and to allowed_prefixes
383
+ # below, which made speech synthesis callable by anyone on the public Space.
384
+ # The only client that needed it was the phone's MediaPlayer, and it now
385
+ # sends its token (AndroidAudioPlayer), so the exemption is gone.
386
  allowed_paths = ["/", "/health", "/wake", "/ping", "/android/pair", "/voice/upload_log"]
387
  # S4: read-only static surfaces the AR client fetches without headers —
388
  # GLTFLoader cannot attach Authorization, and the client app itself is public.
 
434
  except Exception as scene_bus_error:
435
  logging.warning(f"AR scene bus not started: {scene_bus_error}")
436
 
437
+ # WARM THE JARVIS VOICE BEFORE ANYONE ASKS FOR IT.
438
+ #
439
+ # This is why "JARVIS voice from PC is not working at all". The voice was
440
+ # never broken — it was COLD. Measured on this machine:
441
+ #
442
+ # first synthesis after start : 75.1 s (loads RVC jarvis_test.pth,
443
+ # Kokoro, and warms CUDA)
444
+ # every synthesis after that : 2.2 s (identical text and output)
445
+ #
446
+ # The models load lazily inside the first call. The phone's HTTP client
447
+ # gives up long before 75 s — OkHttp's read timeout on the TTS path is
448
+ # 60 s — so the FIRST request after every backend start was guaranteed to
449
+ # time out with nothing played. And since the phone then (until this pass)
450
+ # refused to fall back to on-device speech for JARVIS, the result was
451
+ # silence rather than a slow answer, which reads exactly like a dead
452
+ # feature. Every restart re-armed the trap.
453
+ #
454
+ # Warming here moves that one-time cost to startup, where nothing is
455
+ # waiting on it. Fired as a background task, never awaited: a voice that
456
+ # fails to warm must not stop the backend from serving everything else.
457
+ async def _warm_voice() -> None:
458
+ import time as _t
459
+ try:
460
+ from backend.voice.tts import TTSPipeline
461
+ t0 = _t.time()
462
+ # A real utterance, because that is what pulls every model in the
463
+ # chain into memory — Kokoro, RVC and the DSP tail.
464
+ await TTSPipeline().synthesize("Systems nominal.", personality="jarvis")
465
+ logging.info(f"JARVIS voice warmed in {_t.time() - t0:.1f}s")
466
+ except Exception as warm_error:
467
+ logging.warning(f"JARVIS voice warm-up failed: {warm_error}")
468
+
469
+ if os.environ.get("JARVIS_SKIP_VOICE_WARMUP", "").lower() not in ("1", "true", "yes"):
470
+ BACKGROUND_TASKS.append(asyncio.create_task(_warm_voice()))
471
+
472
  # Start automations scheduler
473
  from backend.services.automation_service import init_automations
474
  await init_automations()
backend/omega/auto_upgrade.py CHANGED
@@ -91,8 +91,35 @@ def log_upgrade_to_db(feature_request: str, files_modified: list, persona: str,
91
  async def handle_upgrade_request(feature_request: str, persona: str):
92
  logging.info(f"Auto-Upgrade Triggered by {persona.upper()}: {feature_request}")
93
 
94
- if not GEMINI_API_KEY:
95
- logging.error("Auto-Upgrade: GEMINI_API_KEY not found. Cannot proceed.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  return
97
 
98
  # Circuit Breaker Loop Prevention
 
91
  async def handle_upgrade_request(feature_request: str, persona: str):
92
  logging.info(f"Auto-Upgrade Triggered by {persona.upper()}: {feature_request}")
93
 
94
+ # RESOLVE THE KEY NOW, NOT AT IMPORT, AND FALL BACK.
95
+ #
96
+ # `GEMINI_API_KEY` above is read ONCE when this module is first imported,
97
+ # from one domain-specific vault slot (AUTO_UPGRADE_PC / _CLOUD). That slot
98
+ # is not configured on this machine, so the constant was "" for the life of
99
+ # the process and this function returned on its second line, every time —
100
+ # while `core.brain_router.ask_router` answered a live Gemini call in 2.9 s
101
+ # from a key that was sitting right there. The whole self-coding capability
102
+ # was switched off by a gate, not by a missing model.
103
+ #
104
+ # Resolving per call also means configuring the key no longer requires a
105
+ # restart to take effect.
106
+ key = GEMINI_API_KEY
107
+ if not key:
108
+ for src in (
109
+ lambda: os.environ.get("GEMINI_API_KEY") or "",
110
+ lambda: __import__("config").GEMINI_API_KEY,
111
+ lambda: resolve_vault_key(KeyDomain.AUTO_UPGRADE_PC),
112
+ ):
113
+ try:
114
+ key = (src() or "").strip()
115
+ except Exception:
116
+ key = ""
117
+ if key:
118
+ logging.info("Auto-Upgrade: using a fallback API key source.")
119
+ break
120
+ if not key:
121
+ logging.error("Auto-Upgrade: no usable Gemini key from any source. "
122
+ "Cannot write code.")
123
  return
124
 
125
  # Circuit Breaker Loop Prevention
backend/omega/osint_engine.py CHANGED
@@ -68,7 +68,7 @@ log = logging.getLogger(__name__)
68
  USER_AGENT = ("OMEGA-OSINT/1.0 (https://github.com/omega-assistant; "
69
  "operator-research-tool) python-aiohttp/3")
70
 
71
- HTTP_TIMEOUT = 20
72
  MAX_PARALLEL = 8
73
 
74
 
@@ -824,25 +824,122 @@ async def _email_infrastructure(session, target: str, dossier: Dossier) -> None:
824
  #
825
  # Rate limiting is real and expected; holehe's own answer is "change your IP".
826
  # A rate-limited check is reported as UNKNOWN below, never as "no account".
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
827
  _EMAIL_ACCOUNT_CHECKS: tuple[dict[str, Any], ...] = (
828
  {
829
  "name": "Spotify",
830
  "method": "GET",
831
  "url": ("https://spclient.wg.spotify.com/signup/public/v1/account"
832
  "?validate=1&email={email}"),
833
- # status 20 = "already registered", status 1 = available.
834
  "registered_if": lambda status, body: '"status":20' in body.replace(" ", ""),
835
  "unknown_if": lambda status, body: status != 200,
836
  },
837
- {
838
- "name": "Rambler",
839
- "method": "POST",
840
- "url": "https://id.rambler.ru/api/v3/mail/check",
841
- "json": {"login": "{email}"},
842
- # The response carries an extra opaque key when the login is taken.
843
- "registered_if": lambda status, body: '"__body_error"' in body and len(body) > 260,
844
- "unknown_if": lambda status, body: status != 200,
845
- },
846
  {
847
  "name": "Gravatar",
848
  "method": "GET",
@@ -850,24 +947,12 @@ _EMAIL_ACCOUNT_CHECKS: tuple[dict[str, Any], ...] = (
850
  "registered_if": lambda status, body: status == 200 and '"entry"' in body,
851
  "unknown_if": lambda status, body: status not in (200, 404),
852
  },
853
- {
854
- "name": "Duolingo",
855
- "method": "GET",
856
- "url": "https://www.duolingo.com/2017-06-30/users?email={email}",
857
- "registered_if": lambda status, body: '"users":[{' in body.replace(" ", ""),
858
- "unknown_if": lambda status, body: status != 200,
859
- },
860
  )
861
 
862
 
863
- async def _email_accounts(session, target: str, dossier: Dossier) -> None:
864
- """Which services this address is registered with.
865
-
866
- This is the collector the operator asked for by name, and it is the highest-
867
- yield pivot in the whole engine: every service an address is registered with
868
- is a profile, a username and often a display name.
869
- """
870
- src = "email account discovery (holehe-style)"
871
  digest = hashlib.md5(target.strip().lower().encode()).hexdigest()
872
  semaphore = asyncio.Semaphore(4)
873
 
@@ -887,8 +972,6 @@ async def _email_accounts(session, target: str, dossier: Dossier) -> None:
887
  return
888
  try:
889
  if check["unknown_if"](status, body):
890
- # Rate-limited or broken. Saying so is the point: "no account"
891
- # and "the check could not run" are different answers.
892
  dossier.add(Finding(
893
  target, "account-check-inconclusive", check["name"], src, url,
894
  detail={"http_status": status}))
@@ -1116,12 +1199,12 @@ async def investigate(
1116
  dossier = Dossier(target, kind)
1117
 
1118
  started = time.time()
1119
- connector = aiohttp.TCPConnector(limit=MAX_PARALLEL, ssl=False)
1120
  # The per-request ceiling is generous because _run() imposes the real budget
1121
  # per source. A tight ceiling here would kill the slow-but-valuable sources
1122
  # before their own allowance ran out; a short connect timeout still stops a
1123
  # dead host from holding a slot.
1124
- session_timeout = aiohttp.ClientTimeout(total=70, sock_connect=10)
1125
  headers = {"User-Agent": USER_AGENT, "Accept": "application/json,text/html,*/*"}
1126
 
1127
  async with aiohttp.ClientSession(timeout=session_timeout, headers=headers,
 
68
  USER_AGENT = ("OMEGA-OSINT/1.0 (https://github.com/omega-assistant; "
69
  "operator-research-tool) python-aiohttp/3")
70
 
71
+ HTTP_TIMEOUT = 30
72
  MAX_PARALLEL = 8
73
 
74
 
 
824
  #
825
  # Rate limiting is real and expected; holehe's own answer is "change your IP".
826
  # A rate-limited check is reported as UNKNOWN below, never as "no account".
827
+ async def _email_accounts(session, target: str, dossier: Dossier) -> None:
828
+ """Which services this address is registered with.
829
+
830
+ Uses the real holehe library (megadose/holehe) which probes each service's
831
+ own account-availability endpoint. It does NOT alert the target email —
832
+ these are signup-form validation calls, not password-recovery flows.
833
+ """
834
+ src = "holehe (email account discovery)"
835
+ try:
836
+ import holehe.modules as holehe_modules
837
+ import holehe.core as holehe_core
838
+ import httpx
839
+
840
+ out: list[dict] = []
841
+ client = httpx.AsyncClient(timeout=20)
842
+ try:
843
+ # holehe.core.import_submodules loads every module in holehe/modules/
844
+ modules = holehe_core.import_submodules(holehe_modules)
845
+
846
+ # PICK THE PROBE BY NAME, NOT BY POSITION.
847
+ #
848
+ # Each holehe service module defines one async function named exactly
849
+ # after the module's last path segment — holehe.modules.mails.gravatar
850
+ # exposes `gravatar(email, client, out)`.
851
+ #
852
+ # The previous version walked `dir(module)` and took the first
853
+ # non-underscore callable, then broke. `dir()` is ALPHABETICAL, so
854
+ # what it actually picked was whatever the module happened to import
855
+ # first — `AsyncClient`, `BeautifulSoup`, `datetime`. Calling those
856
+ # with (email, client, out) returns an ordinary object rather than a
857
+ # coroutine, and `asyncio.gather` then raised
858
+ #
859
+ # TypeError: An asyncio.Future, a coroutine or an awaitable is required
860
+ #
861
+ # which the broad `except Exception` below turned into one logged
862
+ # error and ZERO findings. It never surfaced because holehe was not
863
+ # in any requirements file, so this whole branch died at the import
864
+ # and silently fell through to the four-service fallback. Two faults
865
+ # hiding each other.
866
+ #
867
+ # Measured against holehe 1.61: 121 of 144 entries expose a coroutine
868
+ # under their leaf name; the remaining 23 are package entries with no
869
+ # callables and are correctly skipped.
870
+ tasks = []
871
+ for module_name, module in modules.items():
872
+ leaf = module_name.rsplit(".", 1)[-1]
873
+ fn = getattr(module, leaf, None)
874
+ if fn is None or not asyncio.iscoroutinefunction(fn):
875
+ continue
876
+ tasks.append(fn(target, client, out))
877
+
878
+ if not tasks:
879
+ # Better to say so than to report "no accounts found".
880
+ dossier.note_error(src, "holehe exposed no usable probe modules")
881
+ return
882
+
883
+ log.info("holehe: probing %d services for %s", len(tasks), target)
884
+ # Run all probes concurrently with an overall timeout. Whatever has
885
+ # answered by then is kept — `out` is appended to as results land, so
886
+ # a timeout costs the slow services, not the whole run.
887
+ try:
888
+ await asyncio.wait_for(
889
+ asyncio.gather(*tasks, return_exceptions=True),
890
+ timeout=60,
891
+ )
892
+ except asyncio.TimeoutError:
893
+ dossier.note_error(
894
+ src, f"holehe: {len(out)} of {len(tasks)} services answered within 60s")
895
+ finally:
896
+ await client.aclose()
897
+
898
+ for result in out:
899
+ name = result.get("name") or result.get("domain") or "unknown"
900
+ exists = result.get("exists")
901
+ rate_limited = result.get("rateLimit", False)
902
+
903
+ if rate_limited:
904
+ dossier.add(Finding(
905
+ target, "account-check-inconclusive", name, src, "",
906
+ detail={"why": "rate-limited by the service"}))
907
+ elif exists is True:
908
+ dossier.add(Finding(
909
+ target, "registered-with", name, src, "",
910
+ detail={
911
+ "method": "holehe account-availability probe; "
912
+ "no mail sent to the address",
913
+ "email_recovery": result.get("emailrecovery"),
914
+ "phone_recovery": result.get("phoneNumber"),
915
+ "others": result.get("others"),
916
+ }))
917
+ elif exists is False:
918
+ dossier.add(Finding(
919
+ target, "account-not-found", name, src, "",
920
+ detail={"method": "holehe account-availability probe"}))
921
+ # exists=None means the check couldn't determine — skip silently
922
+
923
+ except ImportError:
924
+ # Holehe not installed — fall back to the minimal built-in checks
925
+ log.warning("holehe package not installed; falling back to built-in checks")
926
+ await _email_accounts_builtin(session, target, dossier)
927
+ except asyncio.TimeoutError:
928
+ dossier.note_error(src, "holehe checks timed out after 60s")
929
+ except Exception as exc:
930
+ dossier.note_error(src, f"holehe failed: {exc}")
931
+
932
+
933
+ # The original 4-service fallback, kept for environments where holehe isn't installed.
934
  _EMAIL_ACCOUNT_CHECKS: tuple[dict[str, Any], ...] = (
935
  {
936
  "name": "Spotify",
937
  "method": "GET",
938
  "url": ("https://spclient.wg.spotify.com/signup/public/v1/account"
939
  "?validate=1&email={email}"),
 
940
  "registered_if": lambda status, body: '"status":20' in body.replace(" ", ""),
941
  "unknown_if": lambda status, body: status != 200,
942
  },
 
 
 
 
 
 
 
 
 
943
  {
944
  "name": "Gravatar",
945
  "method": "GET",
 
947
  "registered_if": lambda status, body: status == 200 and '"entry"' in body,
948
  "unknown_if": lambda status, body: status not in (200, 404),
949
  },
 
 
 
 
 
 
 
950
  )
951
 
952
 
953
+ async def _email_accounts_builtin(session, target: str, dossier: Dossier) -> None:
954
+ """Minimal fallback when holehe isn't installed."""
955
+ src = "email account discovery (built-in fallback)"
 
 
 
 
 
956
  digest = hashlib.md5(target.strip().lower().encode()).hexdigest()
957
  semaphore = asyncio.Semaphore(4)
958
 
 
972
  return
973
  try:
974
  if check["unknown_if"](status, body):
 
 
975
  dossier.add(Finding(
976
  target, "account-check-inconclusive", check["name"], src, url,
977
  detail={"http_status": status}))
 
1199
  dossier = Dossier(target, kind)
1200
 
1201
  started = time.time()
1202
+ connector = aiohttp.TCPConnector(limit=MAX_PARALLEL, ssl=False, ttl_dns_cache=600)
1203
  # The per-request ceiling is generous because _run() imposes the real budget
1204
  # per source. A tight ceiling here would kill the slow-but-valuable sources
1205
  # before their own allowance ran out; a short connect timeout still stops a
1206
  # dead host from holding a slot.
1207
+ session_timeout = aiohttp.ClientTimeout(total=90, sock_connect=15)
1208
  headers = {"User-Agent": USER_AGENT, "Accept": "application/json,text/html,*/*"}
1209
 
1210
  async with aiohttp.ClientSession(timeout=session_timeout, headers=headers,
backend/routes/mobile_bridge_routes.py CHANGED
@@ -28,6 +28,7 @@ decrypt and route the command through the safety-gated autonomy engine.
28
  import asyncio
29
  import base64
30
  import hashlib
 
31
  import logging
32
  import os
33
  from backend.version import VERSION as _VERSION
@@ -429,12 +430,35 @@ async def _measure_live_state(context: dict[str, Any]) -> dict[str, Any]:
429
  # and NOTHING EVER READ IT — the accessor had no callers anywhere in the app,
430
  # so every restart met the operator as a stranger while a record of them sat
431
  # on disk.
 
 
 
 
 
 
 
 
 
 
 
 
 
432
  learned = context.get("learned")
433
- if isinstance(learned, str) and learned.strip():
434
- lines = [ln.strip() for ln in learned.splitlines() if ln.strip()]
 
 
 
 
 
 
 
 
 
 
435
  # Newest last, so the most recent is closest to the question.
436
  state["what you have learned about this operator before today"] = (
437
- " ⏐ ".join(lines[-25:])[:3000])
438
 
439
  # THE AUDIO PATH, STATED, because the alternative is that it gets invented.
440
  #
@@ -493,11 +517,11 @@ async def _measure_live_state(context: dict[str, Any]) -> dict[str, Any]:
493
  # even though nothing had failed. Real research is: search, read what came back,
494
  # search again for the specific thing. That is three searches minimum before a
495
  # word is said.
496
- _MAX_ROUNDS = 7 # hard stop on the whole turn
497
  _MAX_SEARCHES = 4 # web lookups per turn
498
  _MAX_PC_RUNS = 2 # desktop executions per turn
499
  _MAX_FETCHES = 3 # pages actually opened and read per turn
500
- _MAX_RESEARCH = 1 # full research-engine investigations per turn (it is slow)
501
  _MAX_OSINT = 1 # OSINT dossiers per turn — a dozen live sources, pivoted
502
 
503
 
@@ -807,7 +831,7 @@ async def _run_lookup_loop(
807
  try:
808
  from backend.omega.research_engine import research_topic
809
  note = await asyncio.wait_for(
810
- research_topic(topic, persona=persona), timeout=110)
811
  findings = getattr(note, "findings", None) or getattr(note, "summary", "")
812
  if not isinstance(findings, str):
813
  findings = str(findings)
@@ -853,8 +877,8 @@ async def _run_lookup_loop(
853
  # worth having.
854
  report = await asyncio.wait_for(
855
  investigate(selector, depth=2, include_web_research=False,
856
- persona=persona, timeout_s=150),
857
- timeout=170)
858
  block = (report.get("brief") or "")[:7000] or "Nothing was returned."
859
  except asyncio.TimeoutError:
860
  block = ("The OSINT pass took too long and was stopped. You did "
@@ -906,6 +930,27 @@ async def _run_lookup_loop(
906
  evidence.append(f"YOU OPENED: {url}\nWHAT THE PAGE SAYS:\n{body}")
907
  continue
908
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
909
  if not reply.upper().startswith("SEARCH:"):
910
  return reply
911
 
 
28
  import asyncio
29
  import base64
30
  import hashlib
31
+ import json
32
  import logging
33
  import os
34
  from backend.version import VERSION as _VERSION
 
430
  # and NOTHING EVER READ IT — the accessor had no callers anywhere in the app,
431
  # so every restart met the operator as a stranger while a record of them sat
432
  # on disk.
433
+ # ONE STORE, NOT THREE.
434
+ #
435
+ # What the handset sends is now ABSORBED into modules/operator_memory, the
436
+ # single store the desktop brain and the autonomous initiative loop also
437
+ # read. Before this, a fact told to him on the phone reached `/api/chat` and
438
+ # nowhere else: the desktop knew nothing about it, and neither did anything
439
+ # acting on its own. He genuinely learned — on exactly one path out of
440
+ # several — which is indistinguishable from not learning if you happen to
441
+ # ask him somewhere else.
442
+ #
443
+ # The block sent back is the UNION, so a fact learned at the desktop is
444
+ # available to the phone on the very next turn without the phone ever having
445
+ # seen it.
446
  learned = context.get("learned")
447
+ merged: list[str] = []
448
+ try:
449
+ from modules.operator_memory import absorb_block, recall
450
+ if isinstance(learned, str) and learned.strip():
451
+ absorb_block(learned, source="phone")
452
+ merged = recall(40)
453
+ except Exception as mem_exc:
454
+ log.warning("operator memory unavailable: %s", mem_exc)
455
+ if isinstance(learned, str) and learned.strip():
456
+ merged = [ln.strip() for ln in learned.splitlines() if ln.strip()]
457
+
458
+ if merged:
459
  # Newest last, so the most recent is closest to the question.
460
  state["what you have learned about this operator before today"] = (
461
+ " ⏐ ".join(merged[-25:])[:3000])
462
 
463
  # THE AUDIO PATH, STATED, because the alternative is that it gets invented.
464
  #
 
517
  # even though nothing had failed. Real research is: search, read what came back,
518
  # search again for the specific thing. That is three searches minimum before a
519
  # word is said.
520
+ _MAX_ROUNDS = 9 # hard stop on the whole turn
521
  _MAX_SEARCHES = 4 # web lookups per turn
522
  _MAX_PC_RUNS = 2 # desktop executions per turn
523
  _MAX_FETCHES = 3 # pages actually opened and read per turn
524
+ _MAX_RESEARCH = 2 # full research-engine investigations per turn (it is slow)
525
  _MAX_OSINT = 1 # OSINT dossiers per turn — a dozen live sources, pivoted
526
 
527
 
 
831
  try:
832
  from backend.omega.research_engine import research_topic
833
  note = await asyncio.wait_for(
834
+ research_topic(topic, persona=persona), timeout=150)
835
  findings = getattr(note, "findings", None) or getattr(note, "summary", "")
836
  if not isinstance(findings, str):
837
  findings = str(findings)
 
877
  # worth having.
878
  report = await asyncio.wait_for(
879
  investigate(selector, depth=2, include_web_research=False,
880
+ persona=persona, timeout_s=180),
881
+ timeout=200)
882
  block = (report.get("brief") or "")[:7000] or "Nothing was returned."
883
  except asyncio.TimeoutError:
884
  block = ("The OSINT pass took too long and was stopped. You did "
 
930
  evidence.append(f"YOU OPENED: {url}\nWHAT THE PAGE SAYS:\n{body}")
931
  continue
932
 
933
+ # DIAGNOSE: run live diagnostics on the backend subsystems.
934
+ #
935
+ # When the operator says something is broken, this checks what is
936
+ # actually broken — real imports, real tracebacks — rather than
937
+ # guessing or pretending to have fixed it.
938
+ if reply.upper().startswith("DIAGNOSE:"):
939
+ subsystem = reply.split(":", 1)[1].strip().lower()
940
+ log.info("chat: assistant running diagnostics: %s", subsystem or "all")
941
+ try:
942
+ from backend.services.live_diagnostics import full_report, diagnose_subsystem, quick_fix
943
+ if subsystem.startswith("fix "):
944
+ target = subsystem[4:].strip()
945
+ fix_result = await quick_fix(target)
946
+ block = json.dumps(fix_result, indent=2, default=str)[:3000]
947
+ else:
948
+ block = full_report()
949
+ except Exception as exc:
950
+ block = f"Diagnostics failed to run: {str(exc)[:300]}"
951
+ evidence.append(f"YOU RAN SYSTEM DIAGNOSTICS:\n{block}")
952
+ continue
953
+
954
  if not reply.upper().startswith("SEARCH:"):
955
  return reply
956
 
backend/routes/voice_routes.py CHANGED
@@ -26,6 +26,18 @@ async def speak(p: SpeakPayload):
26
  except Exception as e:
27
  raise HTTPException(status_code=500, detail=str(e))
28
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  @router.post("/space_ack")
30
  async def space_ack(p: SpaceAckPayload):
31
  """
 
26
  except Exception as e:
27
  raise HTTPException(status_code=500, detail=str(e))
28
 
29
+ @router.get("/speak")
30
+ async def speak_get(text: str = "", voice: str = "jarvis", agent: str = "", format: str = "wav"):
31
+ """Synthesise TTS via XTTS-v2 for phone MediaPlayer GET streams."""
32
+ try:
33
+ persona = voice or agent or "jarvis"
34
+ from backend.voice.tts import TTSPipeline
35
+ tts = TTSPipeline()
36
+ audio_bytes: bytes = await tts.synthesize(text, personality=persona)
37
+ return Response(content=audio_bytes, media_type="audio/wav")
38
+ except Exception as e:
39
+ raise HTTPException(status_code=500, detail=str(e))
40
+
41
  @router.post("/space_ack")
42
  async def space_ack(p: SpaceAckPayload):
43
  """
backend/services/live_diagnostics.py ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Live diagnostics service for F.R.I.D.A.Y OMEGA.
2
+
3
+ When the operator reports a problem, this module actually checks what is
4
+ broken rather than guessing. It imports each subsystem, runs quick probes,
5
+ and returns real tracebacks and test results so the assistant can explain
6
+ what is wrong and attempt targeted fixes.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import asyncio
12
+ import importlib
13
+ import logging
14
+ import sys
15
+ import traceback
16
+ from typing import Any
17
+
18
+ log = logging.getLogger(__name__)
19
+
20
+ # Subsystems to check and their health-probe signatures.
21
+ _SUBSYSTEMS: dict[str, dict[str, Any]] = {
22
+ "osint_engine": {
23
+ "module": "backend.omega.osint_engine",
24
+ "check_attrs": ["investigate", "classify", "COLLECTORS"],
25
+ "description": "OSINT investigation engine - registries, DNS, certs, pivots",
26
+ },
27
+ "research_engine": {
28
+ "module": "backend.omega.research_engine",
29
+ "check_attrs": ["research_topic"],
30
+ "description": "Deep web research engine - search, scrape, synthesise",
31
+ },
32
+ "image_forensics": {
33
+ "module": "backend.tools.image_forensics",
34
+ "check_attrs": ["analyse_image", "read_metadata", "find_hidden", "find_watermark"],
35
+ "description": "Image metadata and forensics - EXIF, XMP, steganography",
36
+ },
37
+ "web_search": {
38
+ "module": "backend.tools.web_search_tools",
39
+ "check_attrs": ["search_web", "fetch_page"],
40
+ "description": "Web search and page fetching",
41
+ },
42
+ "holehe": {
43
+ "module": "holehe.core",
44
+ "check_attrs": ["import_submodules"],
45
+ "description": "Holehe email account discovery",
46
+ },
47
+ "token_manager": {
48
+ "module": "backend.services.token_manager",
49
+ "check_attrs": ["gemini_call_with_checkpoint"],
50
+ "description": "Gemini API token manager",
51
+ },
52
+ "mobile_bridge": {
53
+ "module": "backend.routes.mobile_bridge_routes",
54
+ "check_attrs": ["router"],
55
+ "description": "Mobile bridge - phone <-> cloud communication",
56
+ },
57
+ "internet_routes": {
58
+ "module": "backend.routes.internet_routes",
59
+ "check_attrs": ["router"],
60
+ "description": "Internet routes - OSINT, search, image analysis endpoints",
61
+ },
62
+ }
63
+
64
+
65
+ def diagnose_subsystem(name: str = "") -> dict[str, Any]:
66
+ """Check one or all subsystems. Returns real tracebacks, not guesses."""
67
+ targets = {name: _SUBSYSTEMS[name]} if name and name in _SUBSYSTEMS else _SUBSYSTEMS
68
+ results: dict[str, Any] = {}
69
+
70
+ for sys_name, info in targets.items():
71
+ entry: dict[str, Any] = {
72
+ "module": info["module"],
73
+ "description": info["description"],
74
+ "status": "UNKNOWN",
75
+ "details": [],
76
+ }
77
+ try:
78
+ mod = importlib.import_module(info["module"])
79
+ entry["status"] = "LOADED"
80
+ entry["details"].append(f"Module imported successfully")
81
+
82
+ missing = []
83
+ for attr in info.get("check_attrs", []):
84
+ if not hasattr(mod, attr):
85
+ missing.append(attr)
86
+ if missing:
87
+ entry["status"] = "DEGRADED"
88
+ entry["details"].append(f"Missing expected attributes: {', '.join(missing)}")
89
+ else:
90
+ entry["details"].append(
91
+ f"All expected attributes present: {', '.join(info.get('check_attrs', []))}")
92
+
93
+ except Exception as exc:
94
+ entry["status"] = "FAILED"
95
+ entry["error"] = str(exc)
96
+ entry["traceback"] = traceback.format_exc()
97
+ entry["details"].append(f"Import failed: {exc}")
98
+
99
+ # Classify the failure
100
+ err = str(exc).lower()
101
+ if "no module named" in err:
102
+ pkg = str(exc).split("'")[1] if "'" in str(exc) else str(exc)
103
+ entry["fix_suggestion"] = f"pip install {pkg.split('.')[0]}"
104
+ elif "cannot import name" in err:
105
+ entry["fix_suggestion"] = (
106
+ "A function or class was renamed or removed. "
107
+ "Check the import statement against the module's actual exports.")
108
+
109
+ results[sys_name] = entry
110
+
111
+ healthy = sum(1 for r in results.values() if r["status"] == "LOADED")
112
+ degraded = sum(1 for r in results.values() if r["status"] == "DEGRADED")
113
+ failed = sum(1 for r in results.values() if r["status"] == "FAILED")
114
+
115
+ return {
116
+ "summary": f"{healthy} healthy, {degraded} degraded, {failed} failed out of {len(results)}",
117
+ "subsystems": results,
118
+ }
119
+
120
+
121
+ async def quick_fix(subsystem: str, action: str = "reload") -> dict[str, Any]:
122
+ """Attempt a targeted fix on a subsystem."""
123
+ if subsystem not in _SUBSYSTEMS:
124
+ return {"error": f"Unknown subsystem '{subsystem}'. Known: {list(_SUBSYSTEMS.keys())}"}
125
+
126
+ info = _SUBSYSTEMS[subsystem]
127
+ module_name = info["module"]
128
+ result: dict[str, Any] = {"subsystem": subsystem, "action": action}
129
+
130
+ if action == "reload":
131
+ try:
132
+ if module_name in sys.modules:
133
+ importlib.reload(sys.modules[module_name])
134
+ result["status"] = "RELOADED"
135
+ result["message"] = f"Module {module_name} reloaded successfully."
136
+ else:
137
+ importlib.import_module(module_name)
138
+ result["status"] = "LOADED"
139
+ result["message"] = f"Module {module_name} loaded for the first time."
140
+ except Exception as exc:
141
+ result["status"] = "FAILED"
142
+ result["error"] = str(exc)
143
+ result["traceback"] = traceback.format_exc()
144
+
145
+ elif action == "install":
146
+ # Try to install the missing package
147
+ import subprocess
148
+ pkg = module_name.split(".")[0]
149
+ try:
150
+ proc = await asyncio.create_subprocess_exec(
151
+ sys.executable, "-m", "pip", "install", pkg,
152
+ stdout=asyncio.subprocess.PIPE,
153
+ stderr=asyncio.subprocess.PIPE,
154
+ )
155
+ stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=120)
156
+ result["status"] = "INSTALLED" if proc.returncode == 0 else "INSTALL_FAILED"
157
+ result["output"] = (stdout or b"").decode()[-500:]
158
+ if proc.returncode != 0:
159
+ result["error"] = (stderr or b"").decode()[-500:]
160
+ except Exception as exc:
161
+ result["status"] = "INSTALL_FAILED"
162
+ result["error"] = str(exc)
163
+
164
+ else:
165
+ result["error"] = f"Unknown action '{action}'. Supported: reload, install"
166
+
167
+ return result
168
+
169
+
170
+ def full_report() -> str:
171
+ """A plain-text diagnostic report suitable for the model to read and relay."""
172
+ diag = diagnose_subsystem()
173
+ lines = [f"SYSTEM DIAGNOSTICS -- {diag['summary']}\n"]
174
+
175
+ for name, info in diag["subsystems"].items():
176
+ status_icon = {"LOADED": "[OK]", "DEGRADED": "[WARN]", "FAILED": "[FAIL]"}.get(
177
+ info["status"], "[?]")
178
+ lines.append(f" {status_icon} {name}: {info['status']}")
179
+ lines.append(f" {info['description']}")
180
+ for detail in info.get("details", []):
181
+ lines.append(f" {detail}")
182
+ if info.get("error"):
183
+ lines.append(f" ERROR: {info['error']}")
184
+ if info.get("fix_suggestion"):
185
+ lines.append(f" SUGGESTED FIX: {info['fix_suggestion']}")
186
+ lines.append("")
187
+
188
+ return "\n".join(lines)
backend/tools/image_forensics.py CHANGED
@@ -99,37 +99,58 @@ def _sniff(data: bytes) -> str:
99
  # Exif and GPS sub-IFDs on many files, which is where the interesting half of
100
  # the record lives — lens, serial number, GPS, the original timestamp.
101
  def _exif_dict(img) -> dict[str, Any]:
102
- from PIL import ExifTags
103
 
104
  out: dict[str, Any] = {}
 
105
  try:
106
  exif = img.getexif()
107
  except Exception as exc:
108
- return {"_error": f"EXIF unreadable: {exc}"}
 
109
  if not exif:
110
- return out
 
 
 
 
 
 
111
 
112
  def _put(name: str, value: Any) -> None:
113
  out[name] = _jsonable(value)
114
 
115
- for tag_id, value in exif.items():
116
- _put(ExifTags.TAGS.get(tag_id, f"Tag{tag_id}"), value)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
 
118
- # The sub-IFDs, by their real pointers.
119
- for ifd_name, ifd_id, table in (
120
- ("Exif", 0x8769, ExifTags.TAGS),
121
- ("GPS", 0x8825, ExifTags.GPSTAGS),
122
- ("Interop", 0xA005, ExifTags.TAGS),
123
- ):
124
  try:
125
- sub = exif.get_ifd(ifd_id)
 
 
 
 
126
  except Exception:
127
- continue
128
- if not sub:
129
- continue
130
- for tag_id, value in sub.items():
131
- name = table.get(tag_id, f"{ifd_name}Tag{tag_id}")
132
- _put(f"{ifd_name}.{name}", value)
133
  return out
134
 
135
 
@@ -1703,13 +1724,13 @@ async def analyse_image(
1703
  if pivots:
1704
  report["osint_pivots"] = pivots
1705
  try:
1706
- from backend.tools.web_search_tools import web_search
1707
  searches = []
1708
  for term in pivots[:3]:
1709
  try:
1710
  searches.append({"query": term,
1711
  "results": await asyncio.wait_for(
1712
- web_search(term), timeout=40)})
1713
  except Exception as exc:
1714
  searches.append({"query": term, "error": str(exc)})
1715
  report["pivot_searches"] = searches
@@ -1735,6 +1756,47 @@ def summarise(report: dict[str, Any]) -> str:
1735
  f"({image.get('megapixels')} MP), {file_info.get('bytes', 0):,} bytes, "
1736
  f"sha256 {str(file_info.get('sha256'))[:16]}...")
1737
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1738
  for section, label in (("metadata", "METADATA"), ("hidden", "HIDDEN DATA"),
1739
  ("watermark", "WATERMARK / TAMPERING")):
1740
  block = report.get(section) or {}
 
99
  # Exif and GPS sub-IFDs on many files, which is where the interesting half of
100
  # the record lives — lens, serial number, GPS, the original timestamp.
101
  def _exif_dict(img) -> dict[str, Any]:
102
+ from PIL import ExifTags, Image
103
 
104
  out: dict[str, Any] = {}
105
+ exif = None
106
  try:
107
  exif = img.getexif()
108
  except Exception as exc:
109
+ pass
110
+
111
  if not exif:
112
+ try:
113
+ raw_blob = img.info.get("exif")
114
+ if raw_blob and isinstance(raw_blob, (bytes, bytearray)):
115
+ exif = Image.Exif()
116
+ exif.load(raw_blob)
117
+ except Exception:
118
+ pass
119
 
120
  def _put(name: str, value: Any) -> None:
121
  out[name] = _jsonable(value)
122
 
123
+ if exif:
124
+ for tag_id, value in exif.items():
125
+ _put(ExifTags.TAGS.get(tag_id, f"Tag{tag_id}"), value)
126
+
127
+ # The sub-IFDs, by their real pointers.
128
+ for ifd_name, ifd_id, table in (
129
+ ("Exif", 0x8769, ExifTags.TAGS),
130
+ ("GPS", 0x8825, ExifTags.GPSTAGS),
131
+ ("Interop", 0xA005, ExifTags.TAGS),
132
+ ):
133
+ try:
134
+ sub = exif.get_ifd(ifd_id)
135
+ except Exception:
136
+ continue
137
+ if not sub:
138
+ continue
139
+ for tag_id, value in sub.items():
140
+ name = table.get(tag_id, f"{ifd_name}Tag{tag_id}")
141
+ _put(f"{ifd_name}.{name}", value)
142
 
143
+ # Legacy _getexif fallback if still empty
144
+ if not out:
 
 
 
 
145
  try:
146
+ raw_dict = getattr(img, "_getexif", lambda: None)()
147
+ if raw_dict and isinstance(raw_dict, dict):
148
+ for tag_id, value in raw_dict.items():
149
+ name = ExifTags.TAGS.get(tag_id, f"Tag{tag_id}")
150
+ _put(name, value)
151
  except Exception:
152
+ pass
153
+
 
 
 
 
154
  return out
155
 
156
 
 
1724
  if pivots:
1725
  report["osint_pivots"] = pivots
1726
  try:
1727
+ from backend.tools.web_search_tools import search_web
1728
  searches = []
1729
  for term in pivots[:3]:
1730
  try:
1731
  searches.append({"query": term,
1732
  "results": await asyncio.wait_for(
1733
+ search_web(term, num_results=3), timeout=40)})
1734
  except Exception as exc:
1735
  searches.append({"query": term, "error": str(exc)})
1736
  report["pivot_searches"] = searches
 
1756
  f"({image.get('megapixels')} MP), {file_info.get('bytes', 0):,} bytes, "
1757
  f"sha256 {str(file_info.get('sha256'))[:16]}...")
1758
 
1759
+ # Always show the raw metadata fields — the user wants to know WHAT
1760
+ # metadata exists, not just whether something "notable" was found.
1761
+ exif = metadata.get("exif") or {}
1762
+ if exif:
1763
+ lines.append("EXIF FIELDS PRESENT:")
1764
+ priority_keys = [
1765
+ ("Make", "Camera Make"), ("Model", "Camera Model"),
1766
+ ("Software", "Software"), ("DateTime", "Date/Time"),
1767
+ ("Exif.DateTimeOriginal", "Original Date"),
1768
+ ("Exif.LensModel", "Lens"),
1769
+ ("Exif.BodySerialNumber", "Serial Number"),
1770
+ ("Orientation", "Orientation"),
1771
+ ("Exif.FNumber", "Aperture"), ("Exif.ISOSpeedRatings", "ISO"),
1772
+ ("Exif.ExposureTime", "Shutter Speed"),
1773
+ ("Exif.FocalLength", "Focal Length"),
1774
+ ]
1775
+ for key, label in priority_keys:
1776
+ if exif.get(key) not in (None, ""):
1777
+ lines.append(f" {label}: {exif[key]}")
1778
+ # Also list any GPS info
1779
+ gps_keys = [k for k in exif if k.startswith("GPS.")]
1780
+ if gps_keys:
1781
+ lines.append(" GPS data: present")
1782
+ # Count total fields
1783
+ lines.append(f" (Total EXIF fields: {len(exif)})")
1784
+
1785
+ xmp = metadata.get("xmp_fields") or {}
1786
+ if xmp:
1787
+ lines.append(f"XMP FIELDS PRESENT ({len(xmp)} fields):")
1788
+ for field, values in list(xmp.items())[:8]:
1789
+ lines.append(f" {field}: {', '.join(str(v) for v in values[:2])}")
1790
+
1791
+ iptc = metadata.get("iptc") or {}
1792
+ if iptc:
1793
+ lines.append(f"IPTC FIELDS PRESENT ({len(iptc)} fields):")
1794
+ for field, values in list(iptc.items())[:6]:
1795
+ lines.append(f" {field}: {', '.join(str(v) for v in values[:2])}")
1796
+
1797
+ if not exif and not xmp and not iptc:
1798
+ lines.append("NO METADATA: This file has been stripped of all EXIF/XMP/IPTC data.")
1799
+
1800
  for section, label in (("metadata", "METADATA"), ("hidden", "HIDDEN DATA"),
1801
  ("watermark", "WATERMARK / TAMPERING")):
1802
  block = report.get(section) or {}
backend/tools/system_tools.py CHANGED
@@ -27,6 +27,59 @@ async def open_app_tool(app_name: str = "", *args, **kwargs):
27
  except Exception as e:
28
  return f"Failed to open app: {e}"
29
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  async def usb_devices_tool(*args, **kwargs):
31
  from backend.services.usb_monitor import get_active_usb_drives
32
  if os.environ.get('CLOUD_ENV', 'false').lower() == 'true':
@@ -99,4 +152,4 @@ async def backup_vault_tool(drive_letter: str = None, *args, **kwargs):
99
  encrypt_directory(staging_dir, target)
100
 
101
  await asyncio.to_thread(perform_vault_backup)
102
- return f"Master Vault Backup ({target}) completed successfully! All code, cloud servers, and EXE databases encrypted."
 
27
  except Exception as e:
28
  return f"Failed to open app: {e}"
29
 
30
+ async def type_into_tool(text: str = "", window: str = "", *args, **kwargs):
31
+ """Type TEXT into the window whose title contains WINDOW, and prove it landed.
32
+
33
+ This exists because "open the terminal and write claude" opened the terminal
34
+ and typed nothing, while reporting "Done. 3 steps". There was no typing tool
35
+ in the registry at all, so the planner expressed it as raw shell — usually a
36
+ PowerShell SendKeys — and omega_executor scores a shell step by
37
+ `returncode == 0`. SendKeys exits 0 whether or not any window received the
38
+ keystrokes, so a step that did nothing was indistinguishable from one that
39
+ worked.
40
+
41
+ `window` matters. Without it the keys go wherever focus happens to be, which
42
+ after launching an app is a race against that app finishing its startup.
43
+ With it, focus is taken deliberately and VERIFIED before a key is sent, and
44
+ a failure to win focus is reported as a failure rather than typed into
45
+ whatever was in front.
46
+ """
47
+ if os.environ.get('CLOUD_ENV', 'false').lower() == 'true':
48
+ from backend.ws.agent_ws import ws_manager
49
+ await ws_manager.broadcast({
50
+ "event": "system:execute",
51
+ "payload": {"cmd": f"type_into::{window}::{text}"}
52
+ })
53
+ return f"Dispatched typing request to the connected PC Relay Client."
54
+ if not (text or "").strip():
55
+ return "Nothing to type."
56
+ try:
57
+ from modules.input_control import type_into
58
+ res = type_into(text, window=window)
59
+ # The word "Failed" is load-bearing: omega_executor's repair loop and the
60
+ # spoken summary both key off whether this reads as success.
61
+ return (res.message if res.ok else f"Failed: {res.message}")
62
+ except Exception as e:
63
+ return f"Failed: typing unavailable ({e})"
64
+
65
+
66
+ async def focus_window_tool(window: str = "", *args, **kwargs):
67
+ """Bring a window to the front and confirm it actually came to the front."""
68
+ if os.environ.get('CLOUD_ENV', 'false').lower() == 'true':
69
+ from backend.ws.agent_ws import ws_manager
70
+ await ws_manager.broadcast({
71
+ "event": "system:execute",
72
+ "payload": {"cmd": f"focus_window::{window}"}
73
+ })
74
+ return "Dispatched focus request to the connected PC Relay Client."
75
+ try:
76
+ from modules.input_control import focus_window
77
+ res = focus_window(window)
78
+ return (res.message if res.ok else f"Failed: {res.message}")
79
+ except Exception as e:
80
+ return f"Failed: focus unavailable ({e})"
81
+
82
+
83
  async def usb_devices_tool(*args, **kwargs):
84
  from backend.services.usb_monitor import get_active_usb_drives
85
  if os.environ.get('CLOUD_ENV', 'false').lower() == 'true':
 
152
  encrypt_directory(staging_dir, target)
153
 
154
  await asyncio.to_thread(perform_vault_backup)
155
+ return f"Master Vault Backup ({target}) completed successfully! All code, cloud servers, and EXE databases encrypted."
backend/tools/tool_registry.py CHANGED
@@ -3,7 +3,10 @@ from backend.tools.filesystem_tools import read_file as read_file_tool, write_fi
3
  from backend.tools.terminal_tools import run_command as run_shell_tool
4
  from backend.tools.github_tools import github_search_tool, github_commit_tool, github_pull_tool
5
  from backend.tools.memory_tools import memory_store_tool, memory_search_tool
6
- from backend.tools.system_tools import screenshot_tool, open_app_tool, usb_devices_tool, notification_tool, system_stats_tool
 
 
 
7
  from backend.tools.calendar_tools import list_events as calendar_tool
8
  from backend.tools.email_tools import send_email as email_tool
9
  from backend.tools.xr_tools import xr_anchor_tool, gesture_context_tool, ar_control_tool
@@ -32,6 +35,12 @@ TOOL_REGISTRY = {
32
  "memory_search": memory_search_tool,
33
  "take_screenshot": screenshot_tool,
34
  "open_app": open_app_tool,
 
 
 
 
 
 
35
  "usb_get_devices": usb_devices_tool,
36
  "send_notification": notification_tool,
37
  "get_system_stats": system_stats_tool,
 
3
  from backend.tools.terminal_tools import run_command as run_shell_tool
4
  from backend.tools.github_tools import github_search_tool, github_commit_tool, github_pull_tool
5
  from backend.tools.memory_tools import memory_store_tool, memory_search_tool
6
+ from backend.tools.system_tools import (
7
+ screenshot_tool, open_app_tool, usb_devices_tool, notification_tool,
8
+ system_stats_tool, type_into_tool, focus_window_tool,
9
+ )
10
  from backend.tools.calendar_tools import list_events as calendar_tool
11
  from backend.tools.email_tools import send_email as email_tool
12
  from backend.tools.xr_tools import xr_anchor_tool, gesture_context_tool, ar_control_tool
 
35
  "memory_search": memory_search_tool,
36
  "take_screenshot": screenshot_tool,
37
  "open_app": open_app_tool,
38
+ # Typing and focus, with PROOF. Not a limit on what he may do — shell,
39
+ # python and freshly written code remain the general path — but the common
40
+ # "open X and type Y" case had no reliable route and was silently failing
41
+ # through raw SendKeys, which exits 0 whether or not anything received it.
42
+ "type_into": type_into_tool,
43
+ "focus_window": focus_window_tool,
44
  "usb_get_devices": usb_devices_tool,
45
  "send_notification": notification_tool,
46
  "get_system_stats": system_stats_tool,
backend/voice/engines/kokoro_engine.py CHANGED
@@ -222,10 +222,10 @@ def _load_profile(name: str, spec: dict):
222
  logger.warning("[RVC] No index for profile %s — timbre retrieval off.", name)
223
  rvc.load_model(model_path)
224
 
225
- rvc.set_params(f0up_key=spec["f0up_key"], f0method="harvest",
226
  index_rate=0.65, filter_radius=1, resample_sr=0,
227
  rms_mix_rate=0.0, protect=0.33)
228
- logger.info("[RVC] Profile %s loaded on %s (f0up_key=%d).",
229
  name, device, spec["f0up_key"])
230
  return rvc
231
 
@@ -385,8 +385,8 @@ def _get_rvc_locked(name: str):
385
  # flat, which is easily enough to read as a different person however
386
  # well the EQ matches. -1 lands near 130 Hz, the closest integer step
387
  # to the target. Everything else in the matrix is untouched.
388
- _rvc_model.set_params(f0up_key=-1, f0method='harvest', index_rate=0.65, rms_mix_rate=0.0, filter_radius=1, protect=0.33)
389
- logger.info("[RVC] JARVIS Ultimate Model loaded and parameters locked.")
390
  except ImportError:
391
  logger.warning("[RVC] rvc_python not installed. Running Kokoro in pure fallback mode without RVC.")
392
  _rvc_model = "FALLBACK"
 
222
  logger.warning("[RVC] No index for profile %s — timbre retrieval off.", name)
223
  rvc.load_model(model_path)
224
 
225
+ rvc.set_params(f0up_key=spec["f0up_key"], f0method="pm",
226
  index_rate=0.65, filter_radius=1, resample_sr=0,
227
  rms_mix_rate=0.0, protect=0.33)
228
+ logger.info("[RVC] Profile %s loaded on %s (f0up_key=%d, f0method=pm).",
229
  name, device, spec["f0up_key"])
230
  return rvc
231
 
 
385
  # flat, which is easily enough to read as a different person however
386
  # well the EQ matches. -1 lands near 130 Hz, the closest integer step
387
  # to the target. Everything else in the matrix is untouched.
388
+ _rvc_model.set_params(f0up_key=-1, f0method='pm', index_rate=0.65, rms_mix_rate=0.0, filter_radius=1, protect=0.33)
389
+ logger.info("[RVC] JARVIS Ultimate Model loaded (f0method=pm) and parameters locked.")
390
  except ImportError:
391
  logger.warning("[RVC] rvc_python not installed. Running Kokoro in pure fallback mode without RVC.")
392
  _rvc_model = "FALLBACK"