Jarvis2345 commited on
Commit
c6124c6
·
verified ·
1 Parent(s): a060abc

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

Browse files
backend/db/mongodb.py CHANGED
@@ -1,10 +1,16 @@
1
  import os
 
2
  import certifi
3
  from motor.motor_asyncio import AsyncIOMotorClient
4
 
5
- # Load the URI from environment variables or use the default provided string
6
- # In production on HF Spaces, this will be set via Space Secrets
7
- MONGO_URI = os.getenv("MONGO_URI", "mongodb+srv://JARVIS:JARVIS@cluster0.qdzuimf.mongodb.net/?appName=Cluster0")
 
 
 
 
 
8
 
9
  class MongoDBClient:
10
  _client = None
@@ -12,6 +18,12 @@ class MongoDBClient:
12
  @classmethod
13
  def get_client(cls) -> AsyncIOMotorClient:
14
  if cls._client is None:
 
 
 
 
 
 
15
  # We use certifi to prevent SSL handshake errors when running inside Docker
16
  cls._client = AsyncIOMotorClient(MONGO_URI, tlsCAFile=certifi.where())
17
  return cls._client
 
1
  import os
2
+ import logging
3
  import certifi
4
  from motor.motor_asyncio import AsyncIOMotorClient
5
 
6
+ # The URI is a secret and must come from the environment only:
7
+ # - PC/exe: MONGO_URI in the loaded .env
8
+ # - HF Space: MONGO_URI Space Secret
9
+ # It is deliberately NOT hardcoded here — an embedded connection string is a
10
+ # second copy of a live credential that silently drifts out of sync the moment
11
+ # the real secret is rotated (exactly the duplication the vault is meant to
12
+ # prevent).
13
+ MONGO_URI = os.getenv("MONGO_URI", "").strip()
14
 
15
  class MongoDBClient:
16
  _client = None
 
18
  @classmethod
19
  def get_client(cls) -> AsyncIOMotorClient:
20
  if cls._client is None:
21
+ if not MONGO_URI:
22
+ raise RuntimeError(
23
+ "MONGO_URI is not configured. Set it in the local .env "
24
+ "(PC/exe) or as an HF Space Secret (cloud) before using "
25
+ "Mongo-backed memory."
26
+ )
27
  # We use certifi to prevent SSL handshake errors when running inside Docker
28
  cls._client = AsyncIOMotorClient(MONGO_URI, tlsCAFile=certifi.where())
29
  return cls._client
backend/memory/consolidator.py CHANGED
@@ -90,7 +90,7 @@ class MemoryConsolidator:
90
 
91
  for t in triples:
92
  if "subject" in t and "predicate" in t and "object" in t:
93
- self.semantic.add_fact(
94
  subject=t["subject"],
95
  predicate=t["predicate"],
96
  obj=t["object"],
@@ -115,8 +115,9 @@ If none, return {"tasks": []}"""
115
 
116
  for t in tasks:
117
  if "task_name" in t and "steps" in t:
118
- # Update ProceduralMemory
119
- self.procedural.record_success(t["task_name"], t["steps"])
 
120
 
121
  # Finally, clear working memory
122
  working_memory.clear()
 
90
 
91
  for t in triples:
92
  if "subject" in t and "predicate" in t and "object" in t:
93
+ await self.semantic.add_fact(
94
  subject=t["subject"],
95
  predicate=t["predicate"],
96
  obj=t["object"],
 
115
 
116
  for t in tasks:
117
  if "task_name" in t and "steps" in t:
118
+ # Update ProceduralMemory (async — must be awaited or the Mongo
119
+ # write is silently dropped, same class of bug as add_fact above).
120
+ await self.procedural.record_success(t["task_name"], t["steps"])
121
 
122
  # Finally, clear working memory
123
  working_memory.clear()
backend/requirements.txt CHANGED
@@ -4,18 +4,21 @@
4
  # Those are only needed in the local Windows EXE build.
5
 
6
  # ── Core API ──────────────────────────────────────────────────────────────────
7
- fastapi
 
 
 
8
  uvicorn[standard]
9
  websockets
10
  python-multipart
11
- pydantic>=2.0.0
12
- starlette
13
 
14
  # ── Database Layer ─────────────────────────────────────────────────────────────
15
  pymongo
16
  motor
17
  certifi
18
- chromadb
19
  SQLAlchemy==2.0.23
20
  apscheduler==3.10.4
21
 
@@ -27,10 +30,10 @@ google-auth-oauthlib>=1.1.0
27
  google-api-python-client>=2.118.0
28
  openai>=1.0.0
29
  anthropic>=0.25.0
30
- huggingface_hub
31
- transformers>=4.40.0
32
- torch>=2.1.0
33
- torchaudio>=2.1.0
34
  sentence-transformers
35
  librosa
36
  scipy
 
4
  # Those are only needed in the local Windows EXE build.
5
 
6
  # ── Core API ──────────────────────────────────────────────────────────────────
7
+ # Upper bounds cap the next MAJOR (the actual break vector) while still admitting
8
+ # every version currently resolving on the live Space — this stops a silent
9
+ # rebuild from pulling a breaking fastapi/starlette/pydantic 1.x→next-major.
10
+ fastapi<1.0
11
  uvicorn[standard]
12
  websockets
13
  python-multipart
14
+ pydantic>=2.0.0,<3.0
15
+ starlette<1.0
16
 
17
  # ── Database Layer ─────────────────────────────────────────────────────────────
18
  pymongo
19
  motor
20
  certifi
21
+ chromadb<1.0
22
  SQLAlchemy==2.0.23
23
  apscheduler==3.10.4
24
 
 
30
  google-api-python-client>=2.118.0
31
  openai>=1.0.0
32
  anthropic>=0.25.0
33
+ huggingface_hub<1.0
34
+ transformers>=4.40.0,<5.0
35
+ torch>=2.1.0,<3.0
36
+ torchaudio>=2.1.0,<3.0
37
  sentence-transformers
38
  librosa
39
  scipy
backend/services/token_manager.py CHANGED
@@ -26,6 +26,24 @@ from backend.services.usb_vault import KeyDomain, resolve_vault_key
26
  def get_runtime_location() -> str:
27
  return "cloud" if os.environ.get("SPACE_ID") else "pc"
28
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  # Load all 15 keys into the token manager pool
30
  GOOGLE_API_KEYS = {}
31
  try:
@@ -225,8 +243,11 @@ def _nvidia_fallback_call_sync(prompt: str, task_id: str, task_type: str, partia
225
  # Construct exact prompt with resume logic and hive mind context
226
  actual_prompt = _build_resume_prompt(prompt, partial_so_far, _extract_last_word(partial_so_far))
227
  if not partial_so_far:
228
- actual_prompt = omni_context + actual_prompt
229
-
 
 
 
230
  # Delegate to the NVIDIA Vault API directly
231
  from backend.services.nvidia_vault import call_nvidia_model, NvidiaModel
232
  fallback_response = call_nvidia_model(actual_prompt, NvidiaModel.GLM_5_1)
@@ -249,11 +270,14 @@ def _gemini_call_sync(prompt: str, task_id: str, task_type: str,
249
 
250
  genai.configure(api_key=key)
251
  _broadcast_live_key_status("GOOGLE", MODEL, "primary_active")
252
-
253
  from backend.services.memory_service import GlobalOmniMemory
254
  omni_context = GlobalOmniMemory.get_global_context_stream()
255
-
256
- model = genai.GenerativeModel(MODEL)
 
 
 
257
  actual_prompt = _build_resume_prompt(prompt, partial_so_far, _extract_last_word(partial_so_far))
258
  if not partial_so_far:
259
  actual_prompt = omni_context + actual_prompt
 
26
  def get_runtime_location() -> str:
27
  return "cloud" if os.environ.get("SPACE_ID") else "pc"
28
 
29
+ def _persona_system_prompt(persona: str) -> str:
30
+ """The identity prompt for the EXPLICITLY requested persona.
31
+
32
+ The mobile app (and other callers) pass the persona they want per-turn, so
33
+ we select on that string directly rather than the persisted global mode.
34
+ Without this, LLM replies carry no JARVIS/FRIDAY character at all — the
35
+ identity file that is supposed to be the source of truth was being ignored
36
+ on the primary /api/chat path.
37
+ """
38
+ try:
39
+ from modules.assistant_identity import (
40
+ JARVIS_PERSONALITY_PROMPT, FRIDAY_PERSONALITY_PROMPT,
41
+ )
42
+ p = (persona or "jarvis").strip().lower()
43
+ return JARVIS_PERSONALITY_PROMPT if p == "jarvis" else FRIDAY_PERSONALITY_PROMPT
44
+ except Exception:
45
+ return ""
46
+
47
  # Load all 15 keys into the token manager pool
48
  GOOGLE_API_KEYS = {}
49
  try:
 
243
  # Construct exact prompt with resume logic and hive mind context
244
  actual_prompt = _build_resume_prompt(prompt, partial_so_far, _extract_last_word(partial_so_far))
245
  if not partial_so_far:
246
+ # NVIDIA models take no separate system channel here, so the persona
247
+ # prompt is prepended (same place the hive-mind context goes).
248
+ _sys = _persona_system_prompt(persona)
249
+ actual_prompt = (_sys + "\n\n" if _sys else "") + omni_context + actual_prompt
250
+
251
  # Delegate to the NVIDIA Vault API directly
252
  from backend.services.nvidia_vault import call_nvidia_model, NvidiaModel
253
  fallback_response = call_nvidia_model(actual_prompt, NvidiaModel.GLM_5_1)
 
270
 
271
  genai.configure(api_key=key)
272
  _broadcast_live_key_status("GOOGLE", MODEL, "primary_active")
273
+
274
  from backend.services.memory_service import GlobalOmniMemory
275
  omni_context = GlobalOmniMemory.get_global_context_stream()
276
+
277
+ # Shape the reply in the active assistant's voice (JARVIS/FRIDAY). Skip on a
278
+ # resume continuation so we don't restate the persona mid-sentence.
279
+ _sys = _persona_system_prompt(persona) if not partial_so_far else None
280
+ model = genai.GenerativeModel(MODEL, system_instruction=_sys) if _sys else genai.GenerativeModel(MODEL)
281
  actual_prompt = _build_resume_prompt(prompt, partial_so_far, _extract_last_word(partial_so_far))
282
  if not partial_so_far:
283
  actual_prompt = omni_context + actual_prompt
backend/services/usb_vault.py CHANGED
@@ -139,8 +139,15 @@ def resolve_vault_key(domain: KeyDomain) -> str:
139
  # Fallback to local SQLite vault
140
  key = get_secret(env_var_name)
141
 
142
- # BACKWARD COMPATIBILITY: If the new key slot is empty, check the old generic GEMINI_API_KEY slot
143
- if not key and domain in (KeyDomain.CHAT, KeyDomain.CLOUD):
 
 
 
 
 
 
 
144
  key = os.environ.get("GEMINI_API_KEY") or get_secret("GEMINI_API_KEY")
145
 
146
  if not key:
 
139
  # Fallback to local SQLite vault
140
  key = get_secret(env_var_name)
141
 
142
+ # BACKWARD COMPATIBILITY / SINGLE-KEY FALLBACK: every domain here is a Google
143
+ # (Gemini) key slot. If a caller hasn't provisioned the dedicated per-domain
144
+ # key, fall back to the generic GEMINI_API_KEY rather than crashing the
145
+ # feature. The dedicated key is always preferred (checked above) so the
146
+ # multi-key rate-limit partitioning still applies whenever it's configured;
147
+ # this only rescues the common single-key desktop user, for whom otherwise
148
+ # only chat/cloud worked and gaming-coach / image-gen / self-heal / etc.
149
+ # raised MissingDomainKeyError.
150
+ if not key:
151
  key = os.environ.get("GEMINI_API_KEY") or get_secret("GEMINI_API_KEY")
152
 
153
  if not key:
backend/voice/tts.py CHANGED
@@ -70,13 +70,23 @@ class TTSPipeline:
70
  logging.info(f"[TTS] Routing to Edge-TTS for unsupported language: '{language}'")
71
  try:
72
  import edge_tts
73
- voices = await edge_tts.list_voices()
74
  target_gender = "Male" if personality == "jarvis" else "Female"
75
  selected_voice = None
76
- for v in voices:
77
- if v["Locale"].lower().startswith(language.lower()) and v["Gender"] == target_gender:
78
- selected_voice = v["ShortName"]
79
- break
 
 
 
 
 
 
 
 
 
 
 
80
  if not selected_voice:
81
  selected_voice = "gu-IN-NiranjanNeural" if target_gender == "Male" else "gu-IN-DhwaniNeural"
82
  communicate = edge_tts.Communicate(text, selected_voice)
@@ -110,15 +120,19 @@ class TTSPipeline:
110
  loop = asyncio.get_running_loop()
111
  def _sync_tts():
112
  if self.tts is None:
113
- try:
114
- from TTS.api import TTS as CoquiTTS
115
- self.tts = CoquiTTS("tts_models/multilingual/multi-dataset/xtts_v2")
116
- except Exception as e:
117
- import logging
118
- logging.error(f"[XTTS] Failed to load model: {e}")
119
- raise RuntimeError("XTTS backend failed to load") from e
120
  return self.tts.tts(text=text, language=language, speaker_wav=speaker_wav, **params)
121
- wav = await loop.run_in_executor(None, _sync_tts)
 
 
 
 
 
 
 
 
 
122
  res = wav_to_bytes(wav)
123
  del wav
124
  import gc
 
70
  logging.info(f"[TTS] Routing to Edge-TTS for unsupported language: '{language}'")
71
  try:
72
  import edge_tts
 
73
  target_gender = "Male" if personality == "jarvis" else "Female"
74
  selected_voice = None
75
+ # For English, honour the identity file's chosen persona voice
76
+ # (JARVIS British male, FRIDAY → Irish female) instead of grabbing
77
+ # the first gender match, so the cloud voice matches the desktop one.
78
+ if language.lower().startswith("en"):
79
+ try:
80
+ from modules.assistant_identity import get_voice_id_for_mode
81
+ selected_voice = get_voice_id_for_mode("jarvis" if personality == "jarvis" else "friday")
82
+ except Exception:
83
+ selected_voice = None
84
+ if not selected_voice:
85
+ voices = await edge_tts.list_voices()
86
+ for v in voices:
87
+ if v["Locale"].lower().startswith(language.lower()) and v["Gender"] == target_gender:
88
+ selected_voice = v["ShortName"]
89
+ break
90
  if not selected_voice:
91
  selected_voice = "gu-IN-NiranjanNeural" if target_gender == "Male" else "gu-IN-DhwaniNeural"
92
  communicate = edge_tts.Communicate(text, selected_voice)
 
120
  loop = asyncio.get_running_loop()
121
  def _sync_tts():
122
  if self.tts is None:
123
+ from TTS.api import TTS as CoquiTTS
124
+ self.tts = CoquiTTS("tts_models/multilingual/multi-dataset/xtts_v2")
 
 
 
 
 
125
  return self.tts.tts(text=text, language=language, speaker_wav=speaker_wav, **params)
126
+ try:
127
+ wav = await loop.run_in_executor(None, _sync_tts)
128
+ except Exception as e:
129
+ # Coqui XTTS is intentionally NOT installed on the cloud Space (too
130
+ # heavy), and the speaker clone wav may be absent there too. Rather
131
+ # than 500 (which left the DEFAULT persona with no cloud voice at
132
+ # all), fall back to Edge-TTS in FRIDAY's identity voice.
133
+ import logging
134
+ logging.warning(f"[TTS] FRIDAY XTTS unavailable ({e}); falling back to Edge-TTS.")
135
+ return await self._synthesize_edge_tts(text, personality, language)
136
  res = wav_to_bytes(wav)
137
  del wav
138
  import gc