myra-linux 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. Aria_office_manage.py +1985 -0
  2. Main_normal.py +261 -0
  3. aria.py +2872 -0
  4. aria_modules/__init__.py +1 -0
  5. aria_modules/aria_activation.py +335 -0
  6. aria_modules/aria_advanced_features.py +1269 -0
  7. aria_modules/aria_advanced_tools.py +1099 -0
  8. aria_modules/assets/about/cofounder.png +0 -0
  9. aria_modules/assets/about/developer.png +0 -0
  10. aria_modules/assets/fonts/OFL-Poppins.txt +93 -0
  11. aria_modules/assets/fonts/Poppins-Bold.ttf +0 -0
  12. aria_modules/assets/fonts/Poppins-ExtraBold.ttf +0 -0
  13. aria_modules/assets/fonts/Poppins-Medium.ttf +0 -0
  14. aria_modules/assets/fonts/Poppins-Regular.ttf +0 -0
  15. aria_modules/assets/fonts/Poppins-SemiBold.ttf +0 -0
  16. aria_modules/assets/myra_girl.png +0 -0
  17. aria_modules/assets/myra_logo.png +0 -0
  18. aria_modules/assets/splash_bg.jpg +0 -0
  19. aria_modules/assets/ui_reference.png +0 -0
  20. aria_modules/cloud_database.py +10 -0
  21. aria_modules/game_install.py +1056 -0
  22. aria_modules/gemini_rest.py +102 -0
  23. aria_modules/haarcascade_frontalface_default.xml +33314 -0
  24. aria_modules/license_client.py +250 -0
  25. aria_modules/minibar_aria.py +355 -0
  26. aria_modules/tools.py +2295 -0
  27. aria_ui.py +818 -0
  28. camera_vision_module.py +1040 -0
  29. linux_extra.py +240 -0
  30. myra/__init__.py +10 -0
  31. myra/__main__.py +3 -0
  32. myra/cli.py +141 -0
  33. myra/config.py +22 -0
  34. myra_actions.py +327 -0
  35. myra_apps.py +846 -0
  36. myra_autostart.py +170 -0
  37. myra_coder_integration.py +641 -0
  38. myra_edge_glow.py +274 -0
  39. myra_float_orb.py +471 -0
  40. myra_guardian.py +181 -0
  41. myra_lang.py +133 -0
  42. myra_linux-0.1.0.dist-info/METADATA +99 -0
  43. myra_linux-0.1.0.dist-info/RECORD +80 -0
  44. myra_linux-0.1.0.dist-info/WHEEL +5 -0
  45. myra_linux-0.1.0.dist-info/entry_points.txt +3 -0
  46. myra_linux-0.1.0.dist-info/licenses/LICENSE +36 -0
  47. myra_linux-0.1.0.dist-info/top_level.txt +31 -0
  48. myra_linux_boot.py +207 -0
  49. myra_linux_tools.py +314 -0
  50. myra_pages.py +1246 -0
  51. myra_prefs.py +71 -0
  52. myra_quick_features.py +748 -0
  53. myra_slot.py +39 -0
  54. myra_splash.py +301 -0
  55. myra_spotify.py +316 -0
  56. myra_studio.py +916 -0
  57. myra_ui.py +1057 -0
  58. normal_prompt.py +41 -0
  59. prompt.py +252 -0
  60. screen_share_module.py +447 -0
  61. website_builder/__init__.py +5 -0
  62. website_builder/agents/__init__.py +1 -0
  63. website_builder/agents/content_agent.py +99 -0
  64. website_builder/agents/generator_agent.py +249 -0
  65. website_builder/agents/planner_agent.py +175 -0
  66. website_builder/config.py +48 -0
  67. website_builder/database/__init__.py +1 -0
  68. website_builder/database/projects_db.py +47 -0
  69. website_builder/deploy/__init__.py +1 -0
  70. website_builder/deploy/github_service.py +67 -0
  71. website_builder/deploy/qr_display.py +130 -0
  72. website_builder/deploy/vercel_service.py +71 -0
  73. website_builder/main.py +25 -0
  74. website_builder/preview/__init__.py +1 -0
  75. website_builder/preview/preview_server.py +47 -0
  76. website_builder/project/__init__.py +1 -0
  77. website_builder/project/project_manager.py +32 -0
  78. website_builder/webforge_integration.py +220 -0
  79. website_builder/webforge_manager.py +209 -0
  80. whatsapp_file_sender.py +661 -0
aria.py ADDED
@@ -0,0 +1,2872 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ MYRA v8.0 — MYRA Coder AI Agent Integrated
4
+ =======================================================================
5
+ v7.4 ke saare fixes INTACT + NEW:
6
+ ✅ MYRA WebForge AI — voice command se website banao + deploy karo
7
+ ✅ website_builder/ folder se import hota hai
8
+ ✅ GitHub push + Vercel deploy + QR code popup automatic
9
+ ✅ Camera Vision module (builder section removed — clean)
10
+ ✅ Screen Share module (unchanged)
11
+ ✅ All original MYRA features preserved
12
+
13
+ ORIGINAL v7.4 FEATURES (preserved):
14
+ ✅ Real-time screen sharing
15
+ ✅ Camera Vision (Gemini real-time)
16
+ ✅ SILENCE_RMS = 25, SILENCE_FRAMES = 16 # ~320ms chup = baat khatam (pehle 30 = 600ms => jawab late aata tha)
17
+ ✅ VAD, error UI, timeout detection
18
+ ✅ Session renewal, keepalive, reconnect
19
+
20
+ MIC THREAD v2 (merged):
21
+ ✅ Error spam throttle — same error baar baar nahi aayega
22
+ ✅ Auto device fallback: configured → auto-detect → default → 0,1,2
23
+ ✅ PortAudio re-init on crash (EXE pe hota hai)
24
+ ✅ Smooth retry with backoff
25
+ """
26
+
27
+ from myra_slot import slot as _myra_slot
28
+ import sys, os, asyncio, threading, base64, json, math, random, time, collections
29
+
30
+ # Windows console/terminal codepage is often NOT UTF-8 (e.g. cp1252) -
31
+ # without this, the ✅/❌ etc. prints below raise UnicodeEncodeError and
32
+ # crash the import (surfaces as "[ERROR] Failed to import aria" upstream).
33
+ for _stream in (sys.stdout, sys.stderr):
34
+ try:
35
+ _stream.reconfigure(encoding="utf-8", errors="replace")
36
+ except Exception:
37
+ pass
38
+
39
+ import numpy as np
40
+ import sounddevice as sd
41
+ from datetime import datetime
42
+ from pathlib import Path
43
+
44
+ import cv2
45
+
46
+ try:
47
+ from PyQt5 import QtWebEngineWidgets # noqa: F401 — QApplication se pehle import (Website Studio live preview)
48
+ except Exception:
49
+ pass
50
+ from PyQt5.QtWidgets import (
51
+ QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
52
+ QLabel, QPushButton, QScrollArea, QFrame, QSizePolicy, QFileDialog,
53
+ QDialog, QComboBox, QDialogButtonBox, QLineEdit, QStackedWidget,
54
+ QListWidget, QListWidgetItem, QInputDialog, QMessageBox
55
+ )
56
+ from PyQt5.QtCore import Qt, pyqtSignal, QTimer, QObject, QRect, QSize, QThread, QEvent
57
+ from PyQt5.QtGui import (
58
+ QColor, QPainter, QPen, QBrush, QLinearGradient,
59
+ QRadialGradient, QPalette, QFont, QPixmap, QImage
60
+ )
61
+ import websockets, websockets.exceptions
62
+
63
+ # UI widgets alag file (aria_ui.py) mein hain
64
+ from aria_ui import (
65
+ ChatWidget, MicSelectDialog, TaskManagerDialog,
66
+ )
67
+ # Naya red/black dashboard UI (myra_ui.py)
68
+ from myra_ui import (
69
+ Sidebar, HeroPanel, StatsCard, QuickToolsCard, RecentActivityCard, HeaderBar,
70
+ InputBar, ChipsRow, StatusBar, T as _UI, load_fonts, ApiKeySetupDialog,
71
+ )
72
+ from myra_studio import StudioPage
73
+ from myra_pages import (
74
+ Bus, ChatPage, VoicePage, PCControlPage, AppsToolsPage, AutomationPage, FilesPage,
75
+ MediaPage, WebSearchPage, SystemMonitorPage, SettingsPage, AboutPage,
76
+ )
77
+
78
+
79
+ # ══════════════════════════════════════════════════════════════════
80
+ # CAMERA VISION MODULE
81
+ # ══════════════════════════════════════════════════════════════════
82
+ try:
83
+ from camera_vision_module import (
84
+ CameraVisionManager, CameraVisionPanel, VisionFrameInjector
85
+ )
86
+ _HAS_CAM_VISION = True
87
+ print("[MYRA] Camera vision module loaded ✅")
88
+ except ImportError:
89
+ _HAS_CAM_VISION = False
90
+ print("[MYRA] camera_vision_module not found")
91
+ class CameraVisionManager(QObject):
92
+ sig_vision_frame = pyqtSignal(str) if False else None
93
+ sig_ui_frame = None
94
+ sig_started = None
95
+ sig_stopped = None
96
+ sig_error = None
97
+ sig_status = None
98
+ def __init__(self): pass
99
+ def start(self): pass
100
+ def stop(self): pass
101
+ def toggle(self): pass
102
+ def set_auto_vision(self, on): pass
103
+ def describe_now(self, *a): pass
104
+ @property
105
+ def is_active(self): return False
106
+ class CameraVisionPanel(QWidget):
107
+ def __init__(self, mgr, parent=None):
108
+ super().__init__(parent)
109
+ QVBoxLayout(self).addWidget(QLabel("Camera Vision\nnot available"))
110
+ class VisionFrameInjector:
111
+ def __init__(self, *a): pass
112
+ def push_frame(self, b): pass
113
+ def send_camera_start_notify(self): pass
114
+ def send_camera_stop_notify(self): pass
115
+
116
+
117
+ # ══════════════════════════════════════════════════════════════════
118
+ # SCREEN SHARE MODULE
119
+ # ══════════════════════════════════════════════════════════════════
120
+ try:
121
+ from screen_share_module import (
122
+ ScreenShareManager, ScreenShareButton, ScreenFrameSender
123
+ )
124
+ _HAS_SCREEN_SHARE = True
125
+ print("[MYRA] Screen share module loaded ✅")
126
+ except ImportError:
127
+ _HAS_SCREEN_SHARE = False
128
+ print("[MYRA] screen_share_module not found — pip install mss pillow")
129
+ class ScreenShareManager:
130
+ sig_started = None
131
+ sig_stopped = None
132
+ sig_error = None
133
+ sig_status = None
134
+ sig_frame_ready = None
135
+ def __init__(self, fps=1.0): self._active = False
136
+ def start(self): pass
137
+ def stop(self): pass
138
+ def toggle(self): pass
139
+ @property
140
+ def is_active(self): return False
141
+ class ScreenShareButton(QPushButton):
142
+ def __init__(self, p=None):
143
+ super().__init__("🖥️ N/A", p)
144
+ self.setFixedSize(112, 36)
145
+ def set_sharing(self, on): pass
146
+ class ScreenFrameSender:
147
+ def __init__(self, *a, **kw): pass
148
+ def push_frame(self, b): pass
149
+ def send_screen_start_notify(self, t=None): pass
150
+ def send_screen_stop_notify(self): pass
151
+
152
+
153
+ # ══════════════════════════════════════════════════════════════════
154
+ # WEBFORGE — AI WEBSITE BUILDER (website_builder/ folder)
155
+ # ══════════════════════════════════════════════════════════════════
156
+ try:
157
+ from website_builder.webforge_manager import WebForgeManager
158
+ _webforge = WebForgeManager()
159
+ _HAS_WEBFORGE = True
160
+ print("[MYRA] WebForge AI Website Builder loaded ✅")
161
+ except ImportError as _wf_err:
162
+ _HAS_WEBFORGE = False
163
+ _webforge = None
164
+ print(f"[MYRA] WebForge not available: {_wf_err}")
165
+ print("[MYRA] Fix: pip install python-dotenv requests google-genai qrcode[pil]")
166
+ print("[MYRA] Also make sure website_builder/ folder exists next to main.py")
167
+
168
+
169
+ # ══════════════════════════════════════════════════════════════════
170
+ # MYRA CODER — AI CODING AGENT (myra_coder_integration.py)
171
+ # ══════════════════════════════════════════════════════════════════
172
+ try:
173
+ from myra_coder_integration import (
174
+ MyraCoderOverlay, is_website_command as _is_website_command
175
+ )
176
+ _HAS_MYRA_CODER = True
177
+ print("[MYRA] MYRA Coder AI Agent loaded ✅")
178
+ except ImportError as _mc_err:
179
+ _HAS_MYRA_CODER = False
180
+ MyraCoderOverlay = None
181
+ print(f"[MYRA] MYRA Coder not available: {_mc_err}")
182
+
183
+ def _is_website_command(text: str) -> bool:
184
+ t = text.lower()
185
+ # Website keywords
186
+ has_website = any(w in t for w in [
187
+ "website", "web page", "webpage", "web app", "landing page",
188
+ "portfolio", "site bana", "web bana"
189
+ ])
190
+ # Action keywords
191
+ has_action = any(w in t for w in [
192
+ "bana", "banao", "bana do", "banana", "chahiye",
193
+ "create", "build", "make", "design", "develop"
194
+ ])
195
+ return has_website and has_action
196
+
197
+
198
+ # ══════════════════════════════════════════════════════════════════
199
+ # WEBFORGE EVENT — thread-safe UI update
200
+ # ══════════════════════════════════════════════════════════════════
201
+ class _WebForgeEvent(QEvent):
202
+ TYPE = QEvent.Type(QEvent.registerEventType())
203
+ def __init__(self, msg: str, success: bool = True):
204
+ super().__init__(self.TYPE)
205
+ self.msg = msg
206
+ self.success = success
207
+
208
+
209
+ # ══════════════════════════════════════════════════════════════════
210
+ # QUICK FEATURES (myra_quick_features.py) — 30+ offline commands
211
+ # ══════════════════════════════════════════════════════════════════
212
+ try:
213
+ from myra_quick_features import handle_quick as _handle_quick, start_background as _quick_start
214
+ _HAS_QUICK = True
215
+ print("[MYRA] Quick Features loaded ✅")
216
+ except Exception as _qf_err:
217
+ _HAS_QUICK = False
218
+ _handle_quick = None
219
+ _quick_start = None
220
+ print(f"[MYRA] Quick Features not available: {_qf_err}")
221
+
222
+
223
+ # ══════════════════════════════════════════════════════════════════
224
+ # EDGE GLOW (myra_edge_glow.py) — screen ke kinaron par state-wise rang-birangi border
225
+ # ══════════════════════════════════════════════════════════════════
226
+ try:
227
+ from myra_edge_glow import EdgeGlow
228
+ _HAS_EDGE = True
229
+ except Exception as _eg_err:
230
+ _HAS_EDGE = False
231
+ EdgeGlow = None
232
+ print(f"[MYRA] Edge glow not available: {_eg_err}")
233
+
234
+
235
+ # ══════════════════════════════════════════════════════════════════
236
+ # FLOATING ORB / SCREEN GUARDIAN / AUTOSTART
237
+ # ══════════════════════════════════════════════════════════════════
238
+ import myra_prefs as _prefs
239
+ try:
240
+ from myra_float_orb import FloatingOrb
241
+ from myra_guardian import ScreenGuardian
242
+ import myra_autostart as _autostart
243
+ _HAS_ORB = True
244
+ except Exception as _orb_err:
245
+ _HAS_ORB = False
246
+ FloatingOrb = ScreenGuardian = _autostart = None
247
+ print(f"[MYRA] Floating orb not available: {_orb_err}")
248
+
249
+
250
+ def _load_edge_pref() -> bool:
251
+ try:
252
+ return bool(json.loads(_get_config_path_early().read_text()).get("edge_glow", True))
253
+ except Exception:
254
+ return True
255
+
256
+
257
+ def _save_edge_pref(on: bool):
258
+ try:
259
+ path = _get_config_path_early()
260
+ c = json.loads(path.read_text()) if path.exists() else {}
261
+ c["edge_glow"] = bool(on)
262
+ path.write_text(json.dumps(c, indent=2))
263
+ except Exception as e:
264
+ print(f"[CONFIG] Error saving edge glow: {e}")
265
+
266
+
267
+ class _QuickEvent(QEvent):
268
+ TYPE = QEvent.Type(QEvent.registerEventType())
269
+ def __init__(self, msg: str, speak: bool = True):
270
+ super().__init__(self.TYPE)
271
+ self.msg = msg
272
+ self.speak = speak
273
+
274
+
275
+ # ══════════════════════════════════════════════════════════════════
276
+ # MYRA CODER EVENT — thread-safe done/error notification
277
+ # ══════════════════════════════════════════════════════════════════
278
+ class _MyraCoderDoneEvent(QEvent):
279
+ TYPE = QEvent.Type(QEvent.registerEventType())
280
+ def __init__(self, summary: str, success: bool = True):
281
+ super().__init__(self.TYPE)
282
+ self.summary = summary
283
+ self.success = success
284
+
285
+
286
+ # ══════════════════════════════════════════════════════════════════
287
+ # AUDIO / MIC CONFIG
288
+ # ══════════════════════════════════════════════════════════════════
289
+ MIC_RATE = 16000
290
+ SPK_RATE = 24000
291
+ FRAMES = int(MIC_RATE * 20 / 1000)
292
+ SILENCE_RMS = 35 # Raised: Realtek bg noise ~10-15, speech ~50+
293
+ SILENCE_FRAMES = 16 # ~320ms chup = baat khatam (pehle 30 = 600ms => jawab late aata tha)
294
+ MIC_GAIN = 2.5
295
+ AUDIO_BUF_MS = 40 # chhota playback buffer = jaldi awaaz shuru
296
+
297
+ _MIC_DEVICE = None
298
+ _SPK_DEVICE = None
299
+
300
+ def _get_best_mic_device():
301
+ devices = sd.query_devices()
302
+ prefer = ["headset","headphone","earphone","microphone","usb","webcam","external"]
303
+ avoid = ["stereo mix","what u hear","virtual","loopback","speaker","monitor","output"]
304
+ best_idx, best_score = None, -1
305
+ for i, d in enumerate(devices):
306
+ if d["max_input_channels"] < 1: continue
307
+ name = d["name"].lower()
308
+ if any(k in name for k in avoid): continue
309
+ score = sum(2 for k in prefer if k in name)
310
+ if d.get("default_samplerate", 0) in (16000, 44100, 48000): score += 1
311
+ if score > best_score: best_score = score; best_idx = i
312
+ if best_idx is not None:
313
+ print(f"[MIC] Auto-selected: [{best_idx}] {devices[best_idx]['name']}")
314
+ return best_idx
315
+
316
+ def list_audio_devices():
317
+ print("\n=== MICROPHONES ===")
318
+ for i, d in enumerate(sd.query_devices()):
319
+ if d["max_input_channels"] > 0:
320
+ print(f" [{i:2d}] {d['name']}{' ← DEFAULT' if i==sd.default.device[0] else ''}")
321
+ print("\n=== SPEAKERS ===")
322
+ for i, d in enumerate(sd.query_devices()):
323
+ if d["max_output_channels"] > 0:
324
+ print(f" [{i:2d}] {d['name']}{' ← DEFAULT' if i==sd.default.device[1] else ''}")
325
+
326
+ def _enc(o): return json.dumps(o).encode()
327
+
328
+
329
+ # ══════════════════════════════════════════════════════════════════
330
+ # OPTIONAL MODULES (aria_modules, whatsapp, office, prompts)
331
+ # ══════════════════════════════════════════════════════════════════
332
+ try:
333
+ from aria_modules.aria_advanced_features import (
334
+ ADVANCED_FEATURES_TOOLS, dispatch_advanced_feature,
335
+ init_advanced_features, ARIAMemory, ClipboardAutomation,
336
+ PDFReader, AssignmentDialog, extract_text_from_image, _analyze_image_with_gemini,
337
+ )
338
+ _HAS_ADV = True
339
+ except ImportError:
340
+ try:
341
+ from aria_modules.aria_advanced_features import (
342
+ ADVANCED_FEATURES_TOOLS, dispatch_advanced_feature,
343
+ init_advanced_features, TITANMemory as ARIAMemory,
344
+ )
345
+ _HAS_ADV = True
346
+ except ImportError:
347
+ _HAS_ADV = False
348
+ ADVANCED_FEATURES_TOOLS = []
349
+ async def dispatch_advanced_feature(n, a): return "n/a"
350
+ def init_advanced_features(api_key=None): pass
351
+ class ARIAMemory:
352
+ @staticmethod
353
+ def add_note(*a, **kw): pass
354
+ @staticmethod
355
+ def add_conversation(*a, **kw): pass
356
+ @staticmethod
357
+ def save(): pass
358
+
359
+ try:
360
+ from whatsapp_file_sender import WA_FILE_TOOL_DECLARATIONS, wa_file_dispatch, WA_FILE_TOOL_MAP
361
+ except ImportError:
362
+ WA_FILE_TOOL_DECLARATIONS = []; WA_FILE_TOOL_MAP = {}
363
+ async def wa_file_dispatch(n, a): return "ok"
364
+
365
+ try: from prompt import aria_PROMPT
366
+ except: aria_PROMPT = "You are MYRA, a warm caring AI assistant. Greet warmly. Short replies. Match user language."
367
+ try: from normal_prompt import NORMAL_PROMPT
368
+ except: NORMAL_PROMPT = "You are MYRA, a professional AI assistant. Greet formally. Concise replies. Match user language."
369
+
370
+ aria_PROMPT = aria_PROMPT.replace("TITAN", "MYRA").replace("ARIA", "MYRA")
371
+ NORMAL_PROMPT = NORMAL_PROMPT.replace("TITAN", "MYRA").replace("ARIA", "MYRA")
372
+
373
+ try:
374
+ from aria_modules.tools import TOOL_DECLARATIONS, dispatch as _base_dispatch, TOOL_MAP as _TOOL_MAP
375
+ _HAS_BASE = True
376
+ except ImportError:
377
+ TOOL_DECLARATIONS = []; _HAS_BASE = False; _TOOL_MAP = {}
378
+ async def _base_dispatch(n, a): return "ok"
379
+
380
+ try:
381
+ from aria_modules.aria_advanced_tools import (
382
+ ADVANCED_TOOL_DECLARATIONS, advanced_dispatch, init_advanced, memory_save,
383
+ _tasks_load as _tm_tasks_load, _tasks_save as _tm_tasks_save,
384
+ _parse_due as _tm_parse_due, _set_reminder as _tm_set_reminder,
385
+ )
386
+ _BASE_TOOLS = TOOL_DECLARATIONS + ADVANCED_TOOL_DECLARATIONS + WA_FILE_TOOL_DECLARATIONS
387
+ _HAS_TASK_MANAGER = True
388
+ except ImportError:
389
+ _BASE_TOOLS = TOOL_DECLARATIONS + WA_FILE_TOOL_DECLARATIONS
390
+ async def advanced_dispatch(n, a): return "ok"
391
+ async def memory_save(r, c): pass
392
+ def init_advanced(**kw): pass
393
+ _HAS_TASK_MANAGER = False
394
+ def _tm_tasks_load(): return []
395
+ def _tm_tasks_save(tasks): pass
396
+ def _tm_parse_due(text): return ""
397
+ def _tm_set_reminder(title, due_iso): pass
398
+
399
+ try:
400
+ from Aria_office_manage import (
401
+ TITAN_OFFICE_TOOLS, office_dispatch, OFFICE_TOOL_NAMES, watch_location_realtime
402
+ )
403
+ _HAS_OFFICE = True
404
+ except ImportError:
405
+ _HAS_OFFICE = False; TITAN_OFFICE_TOOLS = []; OFFICE_TOOL_NAMES = set()
406
+ async def office_dispatch(n, a): return "Office module nahi mila"
407
+ async def watch_location_realtime(n): pass
408
+
409
+ # ✅ FIX: MYRA Coder was NEVER an actual Gemini tool — it was only triggered
410
+ # by a blind client-side keyword match on the transcript, while the system
411
+ # prompt told the model to just talk about building instead of using a real
412
+ # tool. If the user's phrasing didn't hit the hardcoded trigger list, nothing
413
+ # happened — MYRA "couldn't access" the builder even though she meant to.
414
+ # Giving her a real functionCall makes the trigger as reliable as every other
415
+ # one of her 130+ tools (the model decides intent, not a keyword list).
416
+ _MYRA_CODER_TOOLS = [{
417
+ "name": "build_website",
418
+ "description": (
419
+ "Trigger MYRA Coder — the AI website-building engine — to generate and "
420
+ "save a complete website (HTML/CSS/JS) in the background. Call this "
421
+ "IMMEDIATELY whenever the user asks to build/create/design a website, "
422
+ "landing page, portfolio, or webpage, in ANY language/phrasing. Do NOT "
423
+ "write HTML/CSS/JS yourself and do NOT use write_code for this — this "
424
+ "tool hands the job to the dedicated builder engine."
425
+ ),
426
+ "parameters": {
427
+ "type": "object",
428
+ "properties": {
429
+ "description": {
430
+ "type": "string",
431
+ "description": (
432
+ "A clear, complete build instruction combining exactly what "
433
+ "the user asked for — theme, purpose, sections, style, etc."
434
+ )
435
+ }
436
+ },
437
+ "required": ["description"]
438
+ }
439
+ }] if _HAS_MYRA_CODER else []
440
+
441
+ _ALL_TOOLS_RAW = _BASE_TOOLS + ADVANCED_FEATURES_TOOLS + TITAN_OFFICE_TOOLS + _MYRA_CODER_TOOLS
442
+ # ✅ FIX: kai tool names (find_file, read_screen, read_active_window) do baar
443
+ # declare ho rahe the (base + advanced/office lists mein). Purana 2.5 model
444
+ # duplicate functionDeclarations tolerate kar leta tha, lekin naya
445
+ # gemini-3.1-flash-live-preview strict validation karta hai aur setup ke turant
446
+ # baad "1011 internal error" de ke session band kar deta tha. Ab duplicate
447
+ # names hata ke sirf pehla occurrence rakhte hain.
448
+ def _dedupe_tools(tools):
449
+ seen, out = set(), []
450
+ for t in tools:
451
+ if t["name"] not in seen:
452
+ seen.add(t["name"]); out.append(t)
453
+ return out
454
+
455
+ _ALL_TOOLS = _dedupe_tools(_ALL_TOOLS_RAW)
456
+ _ADV_NAMES = {t["name"] for t in ADVANCED_FEATURES_TOOLS}
457
+
458
+ async def dispatch(name, args):
459
+ if name in _TOOL_MAP: return await _base_dispatch(name, args)
460
+ if name in WA_FILE_TOOL_MAP: return await wa_file_dispatch(name, args)
461
+ if name in _ADV_NAMES: return await dispatch_advanced_feature(name, args)
462
+ if name in OFFICE_TOOL_NAMES: return await office_dispatch(name, args)
463
+ return await advanced_dispatch(name, args)
464
+
465
+
466
+ # ══════════════════════════════════════════════════════════════════
467
+ # API KEY LOADING
468
+ # ══════════════════════════════════════════════════════════════════
469
+ def _get_config_path_early():
470
+ p = Path.home() / ".aria"
471
+ p.mkdir(exist_ok=True)
472
+ return p / "config.json"
473
+
474
+ def load_env():
475
+ if os.getenv("GEMINI_API_KEY"):
476
+ return
477
+ try:
478
+ new_cfg = _get_config_path_early()
479
+ if new_cfg.exists():
480
+ c = json.loads(new_cfg.read_text())
481
+ key = c.get("api_key") or c.get("gemini_api_key", "")
482
+ if key:
483
+ os.environ["GEMINI_API_KEY"] = key
484
+ return
485
+ for old_name in [".ARIA_config.json", ".TITAN_config.json"]:
486
+ p = Path.home() / old_name
487
+ if p.exists():
488
+ c = json.loads(p.read_text())
489
+ key = c.get("gemini_api_key") or c.get("api_key", "")
490
+ if key:
491
+ os.environ["GEMINI_API_KEY"] = key
492
+ try:
493
+ new_cfg.write_text(json.dumps({"api_key": key}, indent=2))
494
+ except: pass
495
+ return
496
+ for name in [".env", ".env.local"]:
497
+ ep = Path(__file__).parent / name
498
+ if ep.exists():
499
+ for line in ep.read_text().splitlines():
500
+ line = line.strip()
501
+ if line and not line.startswith('#') and '=' in line:
502
+ k, v = line.split('=', 1)
503
+ os.environ[k.strip()] = v.strip()
504
+ if os.getenv("GEMINI_API_KEY"):
505
+ return
506
+ except Exception as e:
507
+ print(f"[ENV] {e}")
508
+
509
+ load_env()
510
+ _API_KEY = os.getenv("GEMINI_API_KEY", "")
511
+
512
+ _WS_URL = (
513
+ "wss://generativelanguage.googleapis.com/ws/"
514
+ "google.ai.generativelanguage.v1alpha.GenerativeService.BidiGenerateContent"
515
+ )
516
+
517
+ VOICE_OPTIONS = {"female": "Aoede", "male": "Charon"}
518
+ _CURRENT_VOICE = "female"
519
+
520
+ SESSION_RENEW_AFTER = 1800 # 30 minutes — bada kiya
521
+ KEEPALIVE_INTERVAL = 15 # 15s — safer interval
522
+ RESPONSE_TIMEOUT_SEC = 30 # 30s — long tasks ke liye
523
+
524
+
525
+ # ══════════════════════════════════════════════════════════════════
526
+ # CONVERSATION MEMORY
527
+ # ══════════════════════════════════════════════════════════════════
528
+ class ConversationMemory:
529
+ MAX_TURNS = 50 # zyada history rakho
530
+ def __init__(self):
531
+ self._turns = []; self._lock = threading.Lock()
532
+ # Disk se pichli memory load karo
533
+ try:
534
+ _mem_path = Path.home() / ".aria" / "myra_session_memory.json"
535
+ if _mem_path.exists():
536
+ saved = json.loads(_mem_path.read_text(encoding="utf-8"))
537
+ if isinstance(saved, list):
538
+ self._turns = saved[-self.MAX_TURNS:]
539
+ print(f"[MEMORY] Loaded {len(self._turns)} turns from disk")
540
+ except Exception as e:
541
+ print(f"[MEMORY] Load failed: {e}")
542
+ def add(self, role, text):
543
+ if not text.strip(): return
544
+ with self._lock:
545
+ self._turns.append({"role": role, "text": text.strip()})
546
+ if len(self._turns) > self.MAX_TURNS:
547
+ self._turns = self._turns[-self.MAX_TURNS:]
548
+ # Persistent save — reconnect pe memory na jaye
549
+ try:
550
+ _mem_path = Path.home() / ".aria" / "myra_session_memory.json"
551
+ _mem_path.parent.mkdir(exist_ok=True)
552
+ _mem_path.write_text(
553
+ json.dumps(self._turns, ensure_ascii=False, indent=2),
554
+ encoding="utf-8"
555
+ )
556
+ except Exception:
557
+ pass
558
+ def get_context_prompt(self):
559
+ with self._lock: turns = list(self._turns)
560
+ if not turns: return ""
561
+ lines = [
562
+ "\n\n## IMPORTANT — CONVERSATION HISTORY (Session reconnected — memory preserved):",
563
+ "## Continue EXACTLY from where we left off. Do NOT re-greet. Do NOT start over.",
564
+ "## If a task was in progress, RESUME it immediately.",
565
+ ""
566
+ ]
567
+ # Last 30 turns dikhao
568
+ for t in turns[-30:]:
569
+ prefix = "User" if t["role"] == "user" else "MYRA"
570
+ lines.append(f"{prefix}: {t['text']}")
571
+ lines.append("")
572
+ lines.append("[SESSION RESUMED — Continue naturally as if nothing happened. Do NOT say Hello/Hi again.]")
573
+ return "\n".join(lines)
574
+ def clear(self):
575
+ with self._lock:
576
+ self._turns.clear()
577
+ try:
578
+ _mem_path = Path.home() / ".aria" / "myra_session_memory.json"
579
+ if _mem_path.exists(): _mem_path.unlink()
580
+ except Exception: pass
581
+
582
+ _conv_memory = ConversationMemory()
583
+
584
+ # ── Live voice model switch ───────────────────────────────────────
585
+ # key -> (UI label, model id, API version/endpoint)
586
+ LIVE_MODELS = {
587
+ "3.1": ("Gemini 3.1 Flash Live (Default)", "gemini-3.1-flash-live-preview", "v1beta"),
588
+ "native": ("2.5 Flash Native Audio", "gemini-2.5-flash-native-audio-latest", "v1alpha"),
589
+ "native-dec": ("2.5 Native Audio (Dec 2025 Preview)", "gemini-2.5-flash-native-audio-preview-12-2025", "v1alpha"),
590
+ "3.8": ("Gemini 3.8 Live (naya)", "gemini-3.8-live", "v1beta"),
591
+ }
592
+ _LIVE_MODEL_KEY = "3.1"
593
+
594
+ def _ws_url():
595
+ """Selected model ke endpoint (v1beta/v1alpha) ke hisaab se Live WebSocket URL."""
596
+ ver = LIVE_MODELS[_LIVE_MODEL_KEY][2]
597
+ return ("wss://generativelanguage.googleapis.com/ws/"
598
+ f"google.ai.generativelanguage.{ver}.GenerativeService.BidiGenerateContent")
599
+
600
+ def _load_live_model_key():
601
+ try:
602
+ c = json.loads(_get_config_path_early().read_text())
603
+ k = c.get("live_model", "3.1")
604
+ return k if k in LIVE_MODELS else "3.1"
605
+ except Exception:
606
+ return "3.1"
607
+
608
+ def _save_live_model_key(key):
609
+ try:
610
+ path = _get_config_path_early()
611
+ c = json.loads(path.read_text()) if path.exists() else {}
612
+ c["live_model"] = key
613
+ path.write_text(json.dumps(c, indent=2))
614
+ except Exception as e:
615
+ print(f"[CONFIG] Error saving live model: {e}")
616
+
617
+ _LIVE_MODEL_KEY = _load_live_model_key()
618
+
619
+ def _make_setup(prompt, include_history=False, handle=None):
620
+ voice_name = VOICE_OPTIONS.get(_CURRENT_VOICE, "Aoede")
621
+ full_prompt = prompt.replace("TITAN", "MYRA").replace("ARIA", "MYRA")
622
+
623
+ # ✅ FIX: Session continuity + tool usage instructions
624
+ continuity_block = """
625
+
626
+ ## CRITICAL SESSION RULES:
627
+ - Tum MYRA ho — koi bhi session restart/refresh hone par BINA re-greet kiye continue karo
628
+ - Agar pehle koi task chal raha tha → usse IMMEDIATELY resume karo
629
+ - Memory/history agar di gayi hai → use follow karo exactly
630
+ - "Hello" ya "Hi" SIRF pehli baar user se milne pe bolna hai
631
+ - Session disconnect = normal technical event — user ko batane ki zarurat nahi
632
+ - Har kaam poora karo — beech mein mat chodo
633
+
634
+ ## WEBSITE BUILDING — IMPORTANT:
635
+ - Agar user "website banao", "website bana", "create website", "build website" ya koi bhi website-related command de (kisi bhi language/phrasing mein)...
636
+ - IMMEDIATELY call the `build_website` tool with a clear description of what user asked for — this is a REAL tool, not optional, CALL IT.
637
+ - KABHI BHI `write_code` tool use mat karo website ke liye
638
+ - KABHI BHI khud HTML/CSS/JS file mat likho
639
+ - Tool call karne ke baad bolo: "MYRA Coder start ho rahi hai — website ban rahi hai!"
640
+ - MYRA Coder automatically background mein website build kar dega, aur ready hote hi khud deploy bhi ho jaayega
641
+ - Tumhara kaam sirf tool call karna aur user ko encourage karna hai, code likhna nahi
642
+ """
643
+ full_prompt = full_prompt + continuity_block
644
+ try:
645
+ _un = _prefs.user_name()
646
+ except Exception:
647
+ _un = ""
648
+ if _un:
649
+ full_prompt += (f"\n## USER KA NAAM:\n- User ka naam **{_un}** hai. Baat-cheet mein kabhi-kabhi (har line mein nahi) "
650
+ f"unhe {_un} naam se bulao — natural lage tabhi, pehli baar milne par greeting mein zaroor.\n")
651
+
652
+ try:
653
+ import myra_lang as _lang
654
+ full_prompt += _lang.prompt_block()
655
+ except Exception as _e:
656
+ print(f"[MYRA] language block: {_e}")
657
+
658
+ if include_history:
659
+ ctx = _conv_memory.get_context_prompt()
660
+ if ctx: full_prompt = full_prompt + ctx
661
+
662
+ # ✅ Session fix: contextWindowCompression + sessionResumption — bina iske
663
+ # audio+video (camera/screen) session ~2 min mein, audio-only ~10-15 min mein
664
+ # server kaat deta hai. Ab server context compress karta hai aur resumption
665
+ # handle se reconnect seamless hota hai (memory/context server side intact).
666
+ resumption = {"handle": handle} if handle else {}
667
+ return {
668
+ "setup": {
669
+ "contextWindowCompression": {"slidingWindow": {}},
670
+ "sessionResumption": resumption,
671
+ "inputAudioTranscription": {},
672
+ "outputAudioTranscription": {},
673
+ "model": "models/" + LIVE_MODELS[_LIVE_MODEL_KEY][1],
674
+ "generation_config": {
675
+ "temperature": 0.7,
676
+ "response_modalities": ["AUDIO"],
677
+ "speech_config": {
678
+ "voice_config": {
679
+ "prebuilt_voice_config": {"voice_name": voice_name}
680
+ }
681
+ }
682
+ },
683
+ "system_instruction": {"parts": [{"text": full_prompt}]},
684
+ "tools": [{"functionDeclarations": _ALL_TOOLS}] if _ALL_TOOLS else []
685
+ }
686
+ }
687
+
688
+
689
+ # ══════════════════════════════════════════════════════════════════
690
+ # NOTE: sidebar live camera preview no longer opens its own cv2.VideoCapture.
691
+ # ✅ FIX: previously LeftSidebar had its OWN CameraThread opening the webcam
692
+ # independently of CameraVisionManager (the real AI-vision feature in the
693
+ # right-panel "Vision" tab). Two separate cv2.VideoCapture handles fighting
694
+ # over the same physical webcam meant whichever opened second usually failed
695
+ # ("Koi camera nahi mila") — that was the root cause of "vision mode kaam
696
+ # nahi kar raha". Sidebar preview now just displays frames forwarded from
697
+ # the single shared CameraVisionManager (see LeftSidebar.update_camera_frame).
698
+ # ══════════════════════════════════════════════════════════════════
699
+
700
+
701
+ # ══════════════════════════════════════════════════════════════════
702
+ # GEMINI WORKER
703
+ # ══════════════════════════════════════════════════════════════════
704
+ class GeminiWorker(QObject):
705
+ sig_status = pyqtSignal(str, str)
706
+ sig_user_live = pyqtSignal(str)
707
+ sig_aria_live = pyqtSignal(str)
708
+ sig_user_msg = pyqtSignal(str)
709
+ sig_aria_msg = pyqtSignal(str)
710
+ sig_sys_msg = pyqtSignal(str)
711
+ sig_error_msg = pyqtSignal(str)
712
+ sig_warn_msg = pyqtSignal(str)
713
+ sig_tool_call = pyqtSignal(str)
714
+ sig_tool_result = pyqtSignal(str)
715
+ sig_connected = pyqtSignal(bool)
716
+ sig_energy = pyqtSignal(float)
717
+ sig_build_website = pyqtSignal(str) # ✅ real tool-driven MYRA Coder trigger
718
+ sig_mic_state = pyqtSignal(bool) # tap mode: jawab ke baad mic auto-off
719
+ sig_model_fallback = pyqtSignal(str) # kharab model -> default par auto wapas
720
+
721
+ SESSION_RENEW_AFTER = 3000 # 50 min — resumption handle se seamless
722
+ KEEPALIVE_INTERVAL = 15 # 15s keepalive
723
+ MAX_RECONNECT_WAIT = 8
724
+ RECONNECT_BASE = 1.5 # jaldi-jaldi reconnect nahi (pehle 0.5s)
725
+ MIN_SESSION_GAP = 3.0 # do connects ke beech kam se kam itna gap
726
+
727
+ def __init__(self):
728
+ super().__init__()
729
+ self.mic_on = True
730
+ self.running = True
731
+ self._retry = 0
732
+ self._ws = None
733
+ self._loop = None
734
+ self._prompt = aria_PROMPT
735
+ self._mic_q = None
736
+ self._session_num = 0
737
+ self._greeted_once = False
738
+ self._screen_q = None
739
+ self._resume_handle = None # Gemini sessionResumption handle
740
+ self.listen_mode = "continuous" # wake | tap | continuous | stop
741
+ self._quick_fail = 0 # lagataar jaldi-fail sessions (model fallback ke liye)
742
+ self._bot_audio_until = 0.0 # MYRA ki awaaz kab tak bajegi (echo gate)
743
+ self._awake_until = 0.0
744
+ self._session_started = 0.0
745
+ self._last_connect = 0.0
746
+
747
+ def set_mic(self, on): self.mic_on = on
748
+ def stop(self): self.running = False
749
+ def set_prompt(self, p):
750
+ self._prompt = p.replace("TITAN", "MYRA").replace("ARIA", "MYRA")
751
+ self._resume_handle = None # naya prompt = fresh session
752
+ def reset_greet(self): self._greeted_once = False
753
+
754
+ WAKE_WORDS = ("myra", "mira", "maira", "meera", "miyra", "mayra", "मायरा", "मीरा", "मायरा")
755
+
756
+ def set_listen_mode(self, mode):
757
+ self.listen_mode = mode; self._awake_until = 0.0
758
+
759
+ def info(self):
760
+ """Settings page ke liye live session info."""
761
+ age = int(time.monotonic() - self._session_started) if self._session_started and self._ws else 0
762
+ return {"num": self._session_num, "uptime": f"{age // 60}m {age % 60}s" if self._ws else "disconnected",
763
+ "resumable": bool(self._resume_handle), "model": LIVE_MODELS[_LIVE_MODEL_KEY][0],
764
+ "endpoint": LIVE_MODELS[_LIVE_MODEL_KEY][2], "has_key": bool(_API_KEY)}
765
+ def get_ws(self): return self._ws
766
+ def get_loop(self): return self._loop
767
+
768
+ def reconnect(self):
769
+ if self._ws and self._loop:
770
+ asyncio.run_coroutine_threadsafe(self._force_close(), self._loop)
771
+
772
+ async def _force_close(self):
773
+ try: await self._ws.close()
774
+ except: pass
775
+
776
+ def send_text(self, text):
777
+ self._awake_until = time.time() + 30 # typed message = wake word
778
+ if self._ws and self._loop:
779
+ msg = {"clientContent": {
780
+ "turns": [{"role": "user", "parts": [{"text": text}]}],
781
+ "turnComplete": True
782
+ }}
783
+ asyncio.run_coroutine_threadsafe(self._safe_send(_enc(msg)), self._loop)
784
+
785
+ async def _safe_send(self, data):
786
+ try: await self._ws.send(data)
787
+ except Exception as e:
788
+ print(f"[MYRA] send: {e}")
789
+ self.sig_error_msg.emit(f"Send failed: {e}")
790
+
791
+ def push_screen_frame(self, b64_jpeg: str):
792
+ if self._screen_q:
793
+ try: self._screen_q.put_nowait(b64_jpeg)
794
+ except: pass
795
+
796
+ def start_thread(self):
797
+ threading.Thread(target=self._run, daemon=True).start()
798
+
799
+ def _run(self):
800
+ loop = asyncio.new_event_loop()
801
+ asyncio.set_event_loop(loop)
802
+ self._loop = loop
803
+ try: loop.run_until_complete(self._main())
804
+ finally: loop.close()
805
+
806
+ async def _main(self):
807
+ global _LIVE_MODEL_KEY
808
+ import queue as Q
809
+ self._mic_q = Q.Queue(maxsize=600)
810
+ self._screen_q = Q.Queue(maxsize=30)
811
+ threading.Thread(target=self._mic_thread, daemon=True).start()
812
+ first = True
813
+ while self.running:
814
+ try:
815
+ await self._session(first)
816
+ first = False
817
+ self._retry = 0
818
+ # ✅ Normal session end — drain stale frames
819
+ await asyncio.sleep(0.2)
820
+ except Exception as e:
821
+ err_type = type(e).__name__
822
+ err_msg = str(e)
823
+ print(f"[MYRA] Session error: {err_type}: {err_msg}")
824
+ self.sig_connected.emit(False)
825
+
826
+ # ✅ Galat API key: baar-baar retry mat karo (rate-limit / spam)
827
+ if "API key not valid" in err_msg or "API_KEY_INVALID" in err_msg:
828
+ self.sig_error_msg.emit("API key galat hai — Settings → API key badlo")
829
+ self.sig_status.emit("error", "BAD API KEY")
830
+ await asyncio.sleep(30)
831
+ continue
832
+
833
+ # ✅ Model fallback: non-default model 2 baar lagataar jaldi (<25s) server
834
+ # error de to khud default (3.1) par wapas — warna har ~25s reconnect hota.
835
+ _age = time.monotonic() - self._session_started if self._session_started else 999
836
+ if (_LIVE_MODEL_KEY != "3.1" and _age < 25 and
837
+ any(c in err_msg for c in ("1011", "1007", "1008", "1013"))):
838
+ self._quick_fail += 1
839
+ elif _age >= 25:
840
+ self._quick_fail = 0
841
+ if self._quick_fail >= 2:
842
+ bad = _LIVE_MODEL_KEY
843
+ _LIVE_MODEL_KEY = "3.1"; _save_live_model_key("3.1")
844
+ self._resume_handle = None; self._quick_fail = 0
845
+ self.sig_warn_msg.emit(f"{LIVE_MODELS[bad][0]} abhi server par stable nahi (error) — Gemini 3.1 par wapas aa gaye")
846
+ self.sig_model_fallback.emit("3.1")
847
+
848
+ # ✅ CONTENT_TYPE_AUDIO — quick reconnect, no error shown to user
849
+ is_audio_err = ("CONTENT_TYPE_AUDIO" in err_msg or
850
+ "1007" in err_msg or
851
+ "audio content type" in err_msg.lower())
852
+
853
+ if is_audio_err:
854
+ print("[MYRA] Audio type error — quick reconnect (1.5s)")
855
+ self.sig_status.emit("standby", "RECONNECTING...")
856
+ # Memory intact hai — sirf session restart karo
857
+ await asyncio.sleep(1.5)
858
+ else:
859
+ # 1011/1006/1001/1000 = server-side/normal drop — resumption handle
860
+ # se chupchap reconnect, UI pe error spam nahi
861
+ quiet = any(c in err_msg for c in ("1011", "1006", "1001", "1000", "goAway"))
862
+ if not quiet:
863
+ self.sig_error_msg.emit(f"{err_type}: {err_msg[:80]}")
864
+ wait = min(self.RECONNECT_BASE * (2 ** min(self._retry, 4)), self.MAX_RECONNECT_WAIT)
865
+ self._retry += 1
866
+ self.sig_status.emit("standby", f"RECONNECTING ({self._retry})...")
867
+ await asyncio.sleep(wait)
868
+
869
+ # Stale mic frames drain karo
870
+ drained = 0
871
+ while not self._mic_q.empty():
872
+ try: self._mic_q.get_nowait(); drained += 1
873
+ except: break
874
+ if drained > 10: print(f"[MYRA] Drained {drained} stale frames")
875
+
876
+ # ══════════════════════════════════════════════════════════════
877
+ # MIC THREAD — FIXED v2
878
+ # Improvements over v1:
879
+ # ✅ Error spam throttle — same error baar baar nahi aayega
880
+ # ✅ Auto device fallback: configured → auto-detect → default → 0,1,2
881
+ # ✅ PortAudio re-init on crash (EXE pe hota hai)
882
+ # ✅ Smooth retry with backoff
883
+ # ══════════════════════════════════════════════════════════════
884
+ def _mic_thread(self):
885
+ _last_err_t = 0.0
886
+ _err_cooldown = 5.0 # seconds — same error max 1x per 5s
887
+ _consecutive_fails = 0
888
+
889
+ def _try_open_mic(device_idx):
890
+ try:
891
+ stream = sd.InputStream(
892
+ device=device_idx,
893
+ samplerate=MIC_RATE,
894
+ channels=1,
895
+ dtype="int16",
896
+ blocksize=FRAMES,
897
+ latency="low"
898
+ )
899
+ return stream
900
+ except Exception:
901
+ return None
902
+
903
+ def _get_device_fallback_list():
904
+ candidates = []
905
+ if _MIC_DEVICE is not None:
906
+ candidates.append(_MIC_DEVICE)
907
+ best = _get_best_mic_device()
908
+ if best is not None and best not in candidates:
909
+ candidates.append(best)
910
+ try:
911
+ default_mic = sd.default.device[0]
912
+ if default_mic is not None and default_mic not in candidates:
913
+ candidates.append(default_mic)
914
+ except Exception:
915
+ pass
916
+ try:
917
+ devices = sd.query_devices()
918
+ for i, d in enumerate(devices):
919
+ if d["max_input_channels"] > 0 and i not in candidates:
920
+ candidates.append(i)
921
+ if len(candidates) >= 6:
922
+ break
923
+ except Exception:
924
+ pass
925
+ return candidates
926
+
927
+ while self.running:
928
+ # PortAudio re-init — EXE pe crash fix
929
+ try:
930
+ sd._terminate()
931
+ sd._initialize()
932
+ except Exception:
933
+ pass
934
+
935
+ opened_stream = None
936
+ used_device = None
937
+
938
+ for dev_idx in _get_device_fallback_list():
939
+ stream = _try_open_mic(dev_idx)
940
+ if stream is not None:
941
+ opened_stream = stream
942
+ used_device = dev_idx
943
+ _consecutive_fails = 0
944
+ break
945
+
946
+ if opened_stream is None:
947
+ _consecutive_fails += 1
948
+ now = time.time()
949
+ if now - _last_err_t > _err_cooldown:
950
+ _last_err_t = now
951
+ self.sig_error_msg.emit("Mic: Koi microphone nahi mila — connect karo")
952
+ print(f"[MIC] No mic found (attempt {_consecutive_fails})")
953
+ wait = min(2.0 * _consecutive_fails, 10.0)
954
+ time.sleep(wait)
955
+ continue
956
+
957
+ try:
958
+ dev_name = sd.query_devices(used_device)["name"]
959
+ except Exception:
960
+ dev_name = f"Device {used_device}"
961
+ print(f"[MIC] Started — [{used_device}] {dev_name}")
962
+
963
+ last_err_msg = ""
964
+
965
+ try:
966
+ with opened_stream as mic:
967
+ while self.running:
968
+ try:
969
+ pcm, _ = mic.read(FRAMES)
970
+ except Exception as read_err:
971
+ now = time.time()
972
+ err_str = str(read_err)
973
+ if now - _last_err_t > _err_cooldown or err_str != last_err_msg:
974
+ _last_err_t = now
975
+ last_err_msg = err_str
976
+ self.sig_error_msg.emit(f"Mic error: {err_str[:50]}")
977
+ print(f"[MIC] Read error: {err_str}")
978
+ break # re-open with fallback
979
+
980
+ pcm_float = pcm.astype(np.float32) * MIC_GAIN
981
+ pcm_float = np.clip(pcm_float, -32768, 32767)
982
+ pcm = pcm_float.astype(np.int16)
983
+
984
+ if not self.mic_on:
985
+ time.sleep(0.02)
986
+ continue
987
+
988
+ rms = float(np.sqrt(np.mean(pcm_float ** 2)))
989
+ try: self.sig_energy.emit(rms)
990
+ except: pass
991
+ try: self._mic_q.put_nowait((pcm.tobytes(), rms))
992
+ except: pass
993
+
994
+ except Exception as outer_err:
995
+ now = time.time()
996
+ err_str = str(outer_err)
997
+ if now - _last_err_t > _err_cooldown:
998
+ _last_err_t = now
999
+ self.sig_error_msg.emit(f"Mic stream: {err_str[:50]}")
1000
+ print(f"[MIC] Stream crash: {err_str}")
1001
+
1002
+ if self.running:
1003
+ time.sleep(1.0)
1004
+
1005
+ async def _session(self, first: bool = False):
1006
+ # ✅ Do connects ke beech minimum gap — tight reconnect loop nahi
1007
+ gap = time.monotonic() - self._last_connect
1008
+ if self._last_connect and gap < self.MIN_SESSION_GAP:
1009
+ await asyncio.sleep(self.MIN_SESSION_GAP - gap)
1010
+ self._last_connect = time.monotonic()
1011
+ session_start = time.monotonic()
1012
+ self._session_started = session_start
1013
+ self._session_num += 1
1014
+ is_reconnect = self._session_num > 1
1015
+ resumed_with = self._resume_handle
1016
+ go_away = {"flag": False}
1017
+
1018
+ async with websockets.connect(
1019
+ f"{_ws_url()}?key={_API_KEY}",
1020
+ max_size=None, ping_interval=None, ping_timeout=None,
1021
+ close_timeout=10, compression=None, open_timeout=30,
1022
+ ) as ws:
1023
+ self._ws = ws
1024
+ self.sig_connected.emit(True)
1025
+ self._retry = 0
1026
+
1027
+ # ✅ FIX: Hamesha history include karo agar koi turns hain
1028
+ _has_history = len(_conv_memory._turns) > 0
1029
+ await ws.send(_enc(_make_setup(self._prompt, include_history=_has_history and not resumed_with,
1030
+ handle=resumed_with)))
1031
+
1032
+ try:
1033
+ raw = await asyncio.wait_for(ws.recv(), timeout=20)
1034
+ msg = json.loads(raw)
1035
+ if "setupComplete" not in msg and "setup_complete" not in msg:
1036
+ print(f"[MYRA] Unexpected first msg: {str(raw)[:120]}")
1037
+ except asyncio.TimeoutError:
1038
+ raise ConnectionError("Setup ack timeout — check API key/network")
1039
+ except Exception:
1040
+ if resumed_with: # handle expire/invalid — next try fresh session
1041
+ print("[MYRA] Resume handle rejected — fresh session next")
1042
+ self._resume_handle = None
1043
+ raise
1044
+
1045
+ self.sig_status.emit("standby", "STANDBY")
1046
+ if resumed_with:
1047
+ print("[MYRA] Session resumed via handle (seamless) ✅")
1048
+ elif not is_reconnect and not _has_history:
1049
+ self.sig_sys_msg.emit("⚡ MYRA ONLINE")
1050
+ elif _has_history:
1051
+ self.sig_sys_msg.emit("🔄 Session resumed — memory intact ✅")
1052
+ else:
1053
+ self.sig_sys_msg.emit("⚡ MYRA ONLINE")
1054
+
1055
+ import queue as Q
1056
+ _aq = Q.Queue(maxsize=600)
1057
+ _stop = threading.Event()
1058
+
1059
+ if resumed_with:
1060
+ self._greeted_once = True # context server-side intact — na greet, na resume msg
1061
+ elif not self._greeted_once and not _has_history:
1062
+ self._greeted_once = True
1063
+ async def _send_greet():
1064
+ await asyncio.sleep(1.0)
1065
+ mode_is_myra = ("MYRA" in self._prompt or "warm" in self._prompt.lower())
1066
+ g = (
1067
+ "Greet me warmly, say your name is MYRA. Ask how I am. Friendly, short."
1068
+ if mode_is_myra else
1069
+ "Greet professionally as MYRA. Ask how you can help. Brief."
1070
+ )
1071
+ try:
1072
+ await ws.send(_enc({"clientContent": {
1073
+ "turns": [{"role": "user", "parts": [{"text": g}]}],
1074
+ "turnComplete": True
1075
+ }}))
1076
+ except Exception as e:
1077
+ print(f"[MYRA] greet send failed: {e}")
1078
+ asyncio.ensure_future(_send_greet())
1079
+ elif _has_history and is_reconnect:
1080
+ # ✅ Reconnect pe seedha resume karo — no re-greet
1081
+ self._greeted_once = True
1082
+ async def _send_resume():
1083
+ await asyncio.sleep(0.8)
1084
+ # Pichla kaam kya tha — batao MYRA ko
1085
+ last_turns = _conv_memory._turns[-4:]
1086
+ last_ctx = " | ".join(
1087
+ f"{'User' if t['role']=='user' else 'MYRA'}: {t['text'][:60]}"
1088
+ for t in last_turns
1089
+ )
1090
+ resume_msg = (
1091
+ f"[SYSTEM: Session reconnected. Memory intact. "
1092
+ f"Last context: {last_ctx}. "
1093
+ f"Resume naturally from where we left off. "
1094
+ f"DO NOT re-greet. DO NOT say hello again. "
1095
+ f"If any task was in progress, continue it immediately.]"
1096
+ )
1097
+ try:
1098
+ await ws.send(_enc({"clientContent": {
1099
+ "turns": [{"role": "user", "parts": [{"text": resume_msg}]}],
1100
+ "turnComplete": True
1101
+ }}))
1102
+ print("[MYRA] Resume context injected ✅")
1103
+ except Exception as e:
1104
+ print(f"[MYRA] resume inject failed: {e}")
1105
+ asyncio.ensure_future(_send_resume())
1106
+
1107
+ def _play():
1108
+ BUF = int(SPK_RATE * AUDIO_BUF_MS / 1000)
1109
+ buf = np.array([], dtype=np.float32)
1110
+ st = sd.OutputStream(device=_SPK_DEVICE, samplerate=SPK_RATE, channels=1,
1111
+ dtype="float32", blocksize=BUF, latency="low")
1112
+ st.start()
1113
+ while not _stop.is_set():
1114
+ try:
1115
+ chunk = _aq.get(timeout=0.08)
1116
+ if chunk is None:
1117
+ if len(buf):
1118
+ pad = BUF - (len(buf) % BUF or BUF)
1119
+ if pad: buf = np.concatenate([buf, np.zeros(pad, np.float32)])
1120
+ st.write(buf); buf = np.array([], dtype=np.float32)
1121
+ continue
1122
+ buf = np.concatenate([buf, chunk])
1123
+ while len(buf) >= BUF:
1124
+ st.write(buf[:BUF]); buf = buf[BUF:]
1125
+ except Q.Empty:
1126
+ if len(buf) >= BUF:
1127
+ st.write(buf[:BUF]); buf = buf[BUF:]
1128
+ st.stop(); st.close()
1129
+
1130
+ pt = threading.Thread(target=_play, daemon=True)
1131
+ pt.start()
1132
+
1133
+ async def send():
1134
+ was_live = False
1135
+ silent = 0
1136
+ stream_ended = False
1137
+ frames_sent = 0 # kitne audio frames bheje gaye
1138
+ speech_confirm = 0 # consecutive speech frames — debounce ke liye
1139
+ preroll = collections.deque(maxlen=5) # baat shuru hone se pehle ke ~100ms frames
1140
+
1141
+ def _audio_msg(pb):
1142
+ return _enc({"realtimeInput": {"audio": {
1143
+ "mimeType": "audio/pcm;rate=16000", "data": base64.b64encode(pb).decode()}}})
1144
+
1145
+ while self.running:
1146
+ # Screen frames bhejo
1147
+ while self._screen_q and not self._screen_q.empty():
1148
+ try:
1149
+ b64_frame = self._screen_q.get_nowait()
1150
+ screen_msg = {"realtimeInput": {"video": {"mimeType": "image/jpeg", "data": b64_frame}}}
1151
+ try:
1152
+ await ws.send(_enc(screen_msg))
1153
+ except websockets.exceptions.ConnectionClosed:
1154
+ return
1155
+ except Exception as _ve:
1156
+ print(f"[MYRA] Screen frame skip: {_ve}")
1157
+ break
1158
+ except: break
1159
+
1160
+ try: pcm_bytes, rms = self._mic_q.get_nowait()
1161
+ except: await asyncio.sleep(0.005); continue
1162
+
1163
+ if not self.mic_on:
1164
+ was_live = False; silent = 0; stream_ended = False; frames_sent = 0; continue
1165
+
1166
+ # ✅ Echo gate: MYRA bol rahi ho to speaker ki awaaz mic mein aati hai —
1167
+ # tab sirf bahut tez awaaz (user ka barge-in) ko speech maano.
1168
+ _bot_talking = time.time() < self._bot_audio_until + 0.35
1169
+ is_speech = rms >= (SILENCE_RMS * (1.8 if _bot_talking else 1.0))
1170
+ preroll.append(pcm_bytes)
1171
+
1172
+ if is_speech:
1173
+ silent = 0; stream_ended = False
1174
+ speech_confirm += 1 # consecutive speech frames count
1175
+
1176
+ # ✅ DEBOUNCE: min 3 consecutive speech frames ke baad hi
1177
+ # was_live=True karo — single RMS spike se trigger nahi hoga
1178
+ if speech_confirm >= 2 and not was_live:
1179
+ was_live = True
1180
+ frames_sent = 0
1181
+ self.sig_status.emit("listening", "LISTENING")
1182
+ # ✅ baat ke shuruaati frames (debounce mein kat rahe the) pehle bhejo
1183
+ try:
1184
+ for _pb in list(preroll)[:-1]:
1185
+ await ws.send(_audio_msg(_pb)); frames_sent += 1
1186
+ except websockets.exceptions.ConnectionClosed:
1187
+ return
1188
+ except Exception:
1189
+ pass
1190
+ elif speech_confirm < 2:
1191
+ continue # abhi confirm nahi hua — audio mat bhejo
1192
+
1193
+ if was_live:
1194
+ try:
1195
+ await ws.send(_enc({"realtimeInput": {"audio": {
1196
+ "mimeType": "audio/pcm;rate=16000",
1197
+ "data": base64.b64encode(pcm_bytes).decode()
1198
+ }}}))
1199
+ frames_sent += 1
1200
+ except websockets.exceptions.ConnectionClosed:
1201
+ return
1202
+ except Exception as _ae:
1203
+ if "CONTENT_TYPE_AUDIO" in str(_ae):
1204
+ print("[MYRA] Audio chunk skipped — model busy")
1205
+ await asyncio.sleep(0.2)
1206
+ continue
1207
+ return
1208
+ else:
1209
+ speech_confirm = 0 # silence = speech streak reset
1210
+ if was_live:
1211
+ silent += 1
1212
+ # ✅ baat ke beech ki halki awaaz (soft consonants/pauses) bhi bhejo —
1213
+ # pehle ye frames kat jaate the aur MYRA ko shabd adhoore sunte the.
1214
+ if not stream_ended:
1215
+ try: await ws.send(_audio_msg(pcm_bytes))
1216
+ except websockets.exceptions.ConnectionClosed: return
1217
+ except Exception: pass
1218
+ if silent >= SILENCE_FRAMES and not stream_ended:
1219
+ was_live = False
1220
+ stream_ended = True
1221
+
1222
+ # ✅ KEY FIX: audioStreamEnd SIRF tab bhejo jab kuch audio
1223
+ # actually bheja gaya ho (min 5 frames = ~100ms of real speech)
1224
+ # Warna server ko empty stream milti hai → 1007 error
1225
+ MIN_FRAMES_FOR_END = 5
1226
+ if frames_sent < MIN_FRAMES_FOR_END:
1227
+ print(f"[MYRA] audioStreamEnd SKIPPED — sirf {frames_sent} frames bheje (noise, not speech)")
1228
+ frames_sent = 0
1229
+ continue # audioStreamEnd mat bhejo — false trigger tha
1230
+
1231
+ self.sig_status.emit("thinking", "THINKING...")
1232
+ print(f"[MYRA] audioStreamEnd — {frames_sent} frames, silence {silent}, rms={rms:.1f}")
1233
+ frames_sent = 0
1234
+ try:
1235
+ await ws.send(_enc({"realtimeInput": {"audioStreamEnd": True}}))
1236
+ except websockets.exceptions.ConnectionClosed:
1237
+ return
1238
+ except Exception as _se:
1239
+ if "CONTENT_TYPE_AUDIO" in str(_se):
1240
+ print("[MYRA] audioStreamEnd skipped — model busy")
1241
+ stream_ended = True
1242
+ else:
1243
+ return
1244
+
1245
+ async def _handle_tool_call(call):
1246
+ # ✅ Tool dispatch apne independent task mein — recv() loop kabhi
1247
+ # block nahi hota, isliye WS ping/recv aur mic-send pipeline zinda
1248
+ # rehte hain lambe/slow tool calls (website build, file I/O, etc.)
1249
+ # ke dauraan bhi. Yehi pehle "tool call ke turant baad reconnect"
1250
+ # bug ka root cause tha.
1251
+ n = call["name"]; a = call.get("args", {})
1252
+ self.sig_tool_call.emit(f"⚙ {n}")
1253
+ self.sig_status.emit("thinking", "TOOL: " + n[:14])
1254
+ if n == "build_website":
1255
+ # ✅ GUI-owned (MyraCoderOverlay is a QDialog) — hand off via
1256
+ # signal instead of dispatch() so it runs on the Qt thread.
1257
+ desc = str(a.get("description") or "").strip() or "Build a website."
1258
+ self.sig_build_website.emit(desc)
1259
+ res = "MYRA Coder started in the background — building the website now."
1260
+ else:
1261
+ try:
1262
+ res = await dispatch(n, a)
1263
+ if _HAS_ADV: ARIAMemory.add_note(f"T:{n}", str(res)[:200], ["tool"])
1264
+ except Exception as e:
1265
+ res = f"❌ {e}"
1266
+ self.sig_error_msg.emit(f"Tool {n} failed: {str(e)[:60]}")
1267
+ self.sig_tool_result.emit(f"✓ {n}: {str(res)[:100]}")
1268
+ try:
1269
+ await ws.send(_enc({"toolResponse": {"functionResponses": [{
1270
+ "id": call["id"], "name": n,
1271
+ "response": {"result": str(res)}
1272
+ }]}}))
1273
+ except websockets.exceptions.ConnectionClosed:
1274
+ pass # recv()/keepalive already picking up the closed connection
1275
+
1276
+ async def recv():
1277
+ aria_buf = ""; user_buf = ""; speaking = False
1278
+
1279
+ def _allowed():
1280
+ # Wake Word mode: sirf tab bolo jab user ne 'MYRA' kaha (30s conversation window)
1281
+ if self.listen_mode != "wake": return True
1282
+ low = user_buf.lower()
1283
+ if any(w in low for w in self.WAKE_WORDS): self._awake_until = time.time() + 30
1284
+ return time.time() < self._awake_until
1285
+ while self.running:
1286
+ # ✅ recv() session renewal check removed — keepalive handles it
1287
+ try:
1288
+ raw = await asyncio.wait_for(ws.recv(), timeout=90.0) # 90s — long tasks
1289
+ msg = json.loads(raw)
1290
+ except asyncio.TimeoutError:
1291
+ # 90s mein koi response nahi — yeh normal hai for long tasks
1292
+ # Session still valid hai — just reset recv loop
1293
+ print("[MYRA] recv 90s timeout — session still alive, continuing")
1294
+ continue # return mat karo — session end mat karo
1295
+ except websockets.exceptions.ConnectionClosed as e:
1296
+ err_str = str(e)
1297
+ # ✅ FIX: CONTENT_TYPE_AUDIO = model busy tha, reconnect karo
1298
+ if "CONTENT_TYPE_AUDIO" in err_str or "1007" in err_str:
1299
+ print(f"[MYRA] Audio type error (1007) — graceful reconnect")
1300
+ self.sig_warn_msg.emit(
1301
+ "Session refresh ho rahi hai — task continue karega..."
1302
+ )
1303
+ else:
1304
+ print(f"[MYRA] Connection closed: {e}")
1305
+ if not go_away["flag"] and getattr(e, "code", None) not in (1000, 1001, 1011):
1306
+ self.sig_error_msg.emit(f"Connection closed: {e.code}")
1307
+ # ✅ FIX: raise (not return) — _main() ka unified backoff/reconnect
1308
+ # logic handle karega. Silent return se pehle tight 0.2s loop
1309
+ # ban jaata tha (koi exception hi nahi milti thi _main() ko).
1310
+ raise
1311
+ except Exception as e:
1312
+ print(f"[recv] {e}")
1313
+ self.sig_error_msg.emit(f"Receive error: {str(e)[:60]}")
1314
+ continue
1315
+
1316
+ # ✅ FIX: tool call ko background task mein chalao — recv() loop
1317
+ # ko block mat karo. Pehle `await dispatch(...)` seedha recv() ke
1318
+ # andar tha, isliye jab tak tool complete na ho, ws.recv() dobara
1319
+ # call hi nahi hota tha — keepalive/mic bhi effectively stall ho
1320
+ # jaate the, aur server se koi close/ping miss hone par turant
1321
+ # "reconnecting" dikhta tha. Ab tool apne alag task mein chalta hai.
1322
+ for call in msg.get("toolCall", {}).get("functionCalls", []):
1323
+ asyncio.ensure_future(_handle_tool_call(call))
1324
+
1325
+ # ✅ Resumption handle — reconnect pe isi se session continue hota hai
1326
+ sru = msg.get("sessionResumptionUpdate")
1327
+ if sru and sru.get("resumable") and sru.get("newHandle"):
1328
+ self._resume_handle = sru["newHandle"]
1329
+
1330
+ # ✅ goAway: server batata hai ki connection jald band hoga (~10 min
1331
+ # lifetime). Handle ke saath turant clean reconnect — user ko pata nahi chalta.
1332
+ if "goAway" in msg and not go_away["flag"]:
1333
+ go_away["flag"] = True
1334
+ print(f"[MYRA] goAway: {msg['goAway']} — seamless reconnect")
1335
+ async def _graceful():
1336
+ await asyncio.sleep(2.0)
1337
+ try: await ws.close(1000, "goAway reconnect")
1338
+ except Exception: pass
1339
+ asyncio.ensure_future(_graceful())
1340
+
1341
+ sc = msg.get("serverContent", {})
1342
+ if not sc: continue
1343
+
1344
+ if sc.get("interrupted"):
1345
+ self._bot_audio_until = 0.0
1346
+ self.sig_status.emit("listening", "LISTENING")
1347
+ aria_buf = ""; speaking = False
1348
+ while not _aq.empty():
1349
+ try: _aq.get_nowait()
1350
+ except: break
1351
+
1352
+ for part in sc.get("modelTurn", {}).get("parts", []):
1353
+ d = part.get("inlineData")
1354
+ if d and d.get("mimeType","").startswith("audio/pcm"):
1355
+ if not _allowed(): continue # wake word nahi suna — chup raho
1356
+ if not speaking:
1357
+ speaking = True
1358
+ self.sig_status.emit("speaking", "SPEAKING")
1359
+ raw_b = base64.b64decode(d["data"])
1360
+ chunk = np.frombuffer(raw_b, dtype=np.int16).astype(np.float32) / 32768.0
1361
+ self._bot_audio_until = max(self._bot_audio_until, time.time()) + len(chunk) / SPK_RATE
1362
+ try: _aq.put_nowait(chunk)
1363
+ except: pass
1364
+
1365
+ # ✅ FIX: sahi keys outputTranscription/inputTranscription ({"text": ..});
1366
+ # purane outputTranscript/inputTranscript bhi support (older models)
1367
+ ot = (sc.get("outputTranscription") or {}).get("text", "") or sc.get("outputTranscript", "")
1368
+ if ot and _allowed():
1369
+ aria_buf += ot; self.sig_aria_live.emit(aria_buf)
1370
+
1371
+ it = (sc.get("inputTranscription") or {}).get("text", "") or sc.get("inputTranscript", "")
1372
+ if it:
1373
+ user_buf += it; self.sig_user_live.emit(user_buf)
1374
+
1375
+ if sc.get("turnComplete") or sc.get("generationComplete"):
1376
+ _aq.put(None)
1377
+ if aria_buf.strip():
1378
+ self.sig_aria_msg.emit(aria_buf.strip())
1379
+ _conv_memory.add("aria", aria_buf.strip())
1380
+ asyncio.create_task(memory_save("MYRA", aria_buf.strip()))
1381
+ if _HAS_ADV: ARIAMemory.add_conversation("MYRA", aria_buf.strip())
1382
+ aria_buf = ""
1383
+ if user_buf.strip():
1384
+ self.sig_user_msg.emit(user_buf.strip())
1385
+ _conv_memory.add("user", user_buf.strip())
1386
+ asyncio.create_task(memory_save("user", user_buf.strip()))
1387
+ if _HAS_ADV: ARIAMemory.add_conversation("user", user_buf.strip())
1388
+ user_buf = ""; speaking = False
1389
+ if self.listen_mode == "tap": # tap mode: jawab ke baad mic auto-off
1390
+ self.mic_on = False; self.sig_mic_state.emit(False)
1391
+ self.sig_status.emit("standby", "STANDBY")
1392
+ self.sig_aria_live.emit(""); self.sig_user_live.emit("")
1393
+
1394
+ async def keepalive():
1395
+ last_kp = time.monotonic()
1396
+ # ✅ FIX: Audio keepalive BILKUL NAHI — 1007 ka main cause tha
1397
+ # WebSocket naturally alive rehta hai — sirf session renewal check karo
1398
+ while self.running:
1399
+ await asyncio.sleep(5)
1400
+ now = time.monotonic(); age = now - session_start
1401
+ if age >= self.SESSION_RENEW_AFTER:
1402
+ print(f"[MYRA] Session renewal at {age:.0f}s")
1403
+ try: await ws.close(1000, "Session renewal")
1404
+ except: pass
1405
+ return
1406
+ # ✅ WebSocket connection alive hai ya nahi — test with ws.ping()
1407
+ # (yeh WebSocket protocol ping hai, audio nahi)
1408
+ if now - last_kp >= self.KEEPALIVE_INTERVAL:
1409
+ try:
1410
+ await ws.ping() # Pure WS ping — koi audio nahi
1411
+ last_kp = now
1412
+ print(f"[MYRA] ♡ ping age={age:.0f}s")
1413
+ except websockets.exceptions.ConnectionClosed:
1414
+ self.sig_error_msg.emit("Connection lost — reconnecting")
1415
+ # ✅ FIX: raise, taaki _main() ka backoff logic chale —
1416
+ # silent return se tight reconnect loop ban jaata tha
1417
+ raise
1418
+ except Exception as e:
1419
+ # ping() unsupported? ignore silently — connection is still alive
1420
+ last_kp = now
1421
+ print(f"[MYRA] ♡ ping skip ({e}) age={age:.0f}s")
1422
+
1423
+ try:
1424
+ tasks = [
1425
+ asyncio.ensure_future(send()),
1426
+ asyncio.ensure_future(recv()),
1427
+ asyncio.ensure_future(keepalive()),
1428
+ ]
1429
+ done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
1430
+ for t in pending:
1431
+ t.cancel()
1432
+ try: await t
1433
+ except (asyncio.CancelledError, Exception): pass
1434
+ # ✅ FIX: agar koi task exception ke saath khatam hua (jaise
1435
+ # recv() ka ConnectionClosed), usse yahan se _main() tak propagate
1436
+ # karo. Pehle yeh exception yahin dab jaati thi, is liye _session()
1437
+ # "normal" return karta tha aur _main() sirf 0.2s sleep karke turant
1438
+ # dobara connect karta tha — koi backoff/status update ke bina, jo
1439
+ # "bar bar 1 sec ke andar reconnect" wala symptom tha.
1440
+ for t in done:
1441
+ exc = t.exception()
1442
+ if exc:
1443
+ raise exc
1444
+ finally:
1445
+ _stop.set(); _aq.put(None); pt.join(timeout=2.0)
1446
+ self._ws = None
1447
+ age = time.monotonic() - session_start
1448
+ print(f"[MYRA] Session #{self._session_num} ended after {age:.1f}s")
1449
+
1450
+
1451
+ # ══════════════════════════════════════════════════════════════════
1452
+ # API KEY DIALOG
1453
+ # ══════════════════════════════════════════════════════════════════
1454
+ def _get_config_path():
1455
+ config_dir = Path.home() / ".aria"
1456
+ config_dir.mkdir(exist_ok=True)
1457
+ return config_dir / "config.json"
1458
+
1459
+ def _load_api_key():
1460
+ try:
1461
+ config_path = _get_config_path()
1462
+ if config_path.exists():
1463
+ with open(config_path, 'r') as f:
1464
+ config = json.load(f)
1465
+ return config.get('api_key', '')
1466
+ except Exception as e:
1467
+ print(f"[CONFIG] Error loading API key: {e}")
1468
+ return ''
1469
+
1470
+ def _save_config(api_key):
1471
+ try:
1472
+ config_path = _get_config_path()
1473
+ config = {}
1474
+ try:
1475
+ config = json.loads(config_path.read_text()) if config_path.exists() else {}
1476
+ except Exception:
1477
+ config = {}
1478
+ config.update({'api_key': api_key, 'gemini_api_key': api_key})
1479
+ with open(config_path, 'w') as f:
1480
+ json.dump(config, f, indent=2)
1481
+ os.environ["GEMINI_API_KEY"] = api_key
1482
+ print(f"[CONFIG] Saved config to {config_path}")
1483
+ except Exception as e:
1484
+ print(f"[CONFIG] Error saving config: {e}")
1485
+
1486
+ # ══════════════════════════════════════════════════════════════════
1487
+ # MAIN WINDOW
1488
+ # ══════════════════════════════════════════════════════════════════
1489
+ _startup_hook = None # Main_normal / __main__ splash ko progress dene ke liye set karta hai (callable(pct))
1490
+
1491
+
1492
+ def _sp(pct):
1493
+ """Startup progress (window banne ke dauran) — splash ho to bar aage badhta hai."""
1494
+ h = _startup_hook
1495
+ if h:
1496
+ try:
1497
+ h(pct)
1498
+ except Exception:
1499
+ pass
1500
+
1501
+
1502
+ class ARIAWindow(QMainWindow):
1503
+ def __init__(self, api_key=None):
1504
+ super().__init__()
1505
+ global _API_KEY
1506
+ _API_KEY = api_key or os.getenv("GEMINI_API_KEY", "")
1507
+ self.setWindowTitle("MYRA — AI Voice Assistant v8.0 | MYRA Coder")
1508
+ self.setWindowFlags(Qt.Window | Qt.FramelessWindowHint)
1509
+ # FIX: window kabhi screen se bada nahi — available area (taskbar chhod kar) ke andar fit
1510
+ try:
1511
+ _av = QApplication.primaryScreen().availableGeometry()
1512
+ _w, _h = min(1440, int(_av.width() * 0.96)), min(880, int(_av.height() * 0.96))
1513
+ self.setMinimumSize(min(1180, _av.width()), min(720, _av.height()))
1514
+ self.resize(_w, _h)
1515
+ self.move(_av.x() + (_av.width() - _w) // 2, _av.y() + (_av.height() - _h) // 2)
1516
+ except Exception:
1517
+ self.setMinimumSize(1180, 720); self.resize(1440, 880)
1518
+ self._mode = "MYRA"
1519
+ self._mic_on = True
1520
+ self._api_key = api_key
1521
+ _sp(72)
1522
+ self._build_ui()
1523
+ _sp(91)
1524
+ self._init_cam_vision()
1525
+ _sp(92)
1526
+ self._init_screenshare()
1527
+ _sp(93)
1528
+ self._init_edge_glow()
1529
+ _sp(94)
1530
+ self._init_float_orb()
1531
+ self._init_user_name()
1532
+ self._init_language()
1533
+ _sp(95)
1534
+ self._start_worker()
1535
+ _sp(97)
1536
+
1537
+ # ── EDGE GLOW ────────────────────────────────────────────────
1538
+ def _init_edge_glow(self):
1539
+ self._voice_state = "standby"
1540
+ self._screenread_until = 0.0
1541
+ self._ss_first_frame_pending = False
1542
+ self._edge = None
1543
+ if _HAS_EDGE:
1544
+ try:
1545
+ self._edge = EdgeGlow()
1546
+ self._edge.set_enabled(_load_edge_pref())
1547
+ except Exception as e:
1548
+ print(f"[MYRA] Edge glow init: {e}")
1549
+ self._bus.edge_glow.connect(self._on_edge_glow_toggle)
1550
+
1551
+ def _edge_mode(self):
1552
+ """Priority: error > screen-read > speaking > thinking > listening > screenshare > camera."""
1553
+ vs = getattr(self, '_voice_state', 'standby')
1554
+ if vs == "error":
1555
+ return "error"
1556
+ if time.time() < getattr(self, '_screenread_until', 0.0):
1557
+ return "screenread"
1558
+ if vs in ("speaking", "thinking", "listening"):
1559
+ return vs
1560
+ if _HAS_SCREEN_SHARE and self._ss_mgr.is_active:
1561
+ return "screenshare"
1562
+ if _HAS_CAM_VISION and hasattr(self, '_cv_mgr') and self._cv_mgr.is_active:
1563
+ return "camera"
1564
+ return None
1565
+
1566
+ def _update_edge_glow(self):
1567
+ if getattr(self, '_edge', None):
1568
+ self._edge.set_mode(self._edge_mode())
1569
+
1570
+ def _on_edge_glow_toggle(self, on: bool):
1571
+ _save_edge_pref(on)
1572
+ self._bus.state["edge_glow"] = bool(on)
1573
+ if getattr(self, '_edge', None):
1574
+ self._edge.set_enabled(on)
1575
+ self._update_edge_glow()
1576
+
1577
+ # ── FLOATING ORB / BACKGROUND MODE / GUARDIAN ────────────────
1578
+ def _init_float_orb(self):
1579
+ self._orb_mode = False
1580
+ self._float_orb = self._guardian = self._tray = None
1581
+ self._preview_token = 0
1582
+ b = self._bus
1583
+ if _HAS_ORB:
1584
+ try:
1585
+ self._float_orb = FloatingOrb()
1586
+ self._float_orb.action.connect(self._on_orb_action)
1587
+ self._guardian = ScreenGuardian(busy_fn=self._guardian_busy, api_key_fn=lambda: self._api_key)
1588
+ self._guardian.sig_alert.connect(self._on_guardian_alert)
1589
+ self._init_tray()
1590
+ except Exception as e:
1591
+ print(f"[MYRA] Floating orb init: {e}")
1592
+ self._float_orb = None
1593
+ b.orb_on_minimize.connect(lambda on: (_prefs.set_("orb_on_minimize", bool(on)), b.state.__setitem__("orb_on_minimize", bool(on))))
1594
+ b.orb_float_size.connect(self._on_orb_float_size)
1595
+ b.orb_ui_scale.connect(self._on_orb_ui_scale)
1596
+ b.autostart.connect(self._on_autostart_toggle)
1597
+ b.guardian.connect(self._on_guardian_toggle)
1598
+ b.guardian_interval.connect(lambda v: (_prefs.set_("guardian_interval", int(v)), b.state.__setitem__("guardian_interval", int(v))))
1599
+ b.orb_preview.connect(lambda: self._preview_orb(6000))
1600
+ self._apply_orb_ui_scale(int(_prefs.get("orb_ui_scale", 100)))
1601
+ if self._guardian and _prefs.get("guardian", False):
1602
+ self._guardian.start()
1603
+
1604
+ def _init_tray(self):
1605
+ from PyQt5.QtWidgets import QSystemTrayIcon, QMenu
1606
+ from PyQt5.QtGui import QIcon
1607
+ if not QSystemTrayIcon.isSystemTrayAvailable():
1608
+ return
1609
+ ico = Path(__file__).resolve().parent / "aria_icon.ico"
1610
+ t = QSystemTrayIcon(QIcon(str(ico)) if ico.exists() else self.windowIcon(), self)
1611
+ t.setToolTip("MYRA — AI Assistant")
1612
+ m = QMenu()
1613
+ m.addAction("MYRA kholo", lambda: self._on_orb_action("open"))
1614
+ m.addAction("Mic on / off", lambda: self._on_orb_action("mic"))
1615
+ m.addAction("Screen Guardian on / off", lambda: self._on_orb_action("guardian"))
1616
+ m.addSeparator()
1617
+ m.addAction("MYRA band karo", lambda: self._on_orb_action("quit"))
1618
+ t.setContextMenu(m)
1619
+ t.activated.connect(lambda r: self._on_orb_action("open") if r in (QSystemTrayIcon.Trigger, QSystemTrayIcon.DoubleClick) else None)
1620
+ t.show()
1621
+ self._tray, self._tray_menu = t, m
1622
+
1623
+ def _enter_orb_mode(self):
1624
+ if not self._float_orb or self._orb_mode:
1625
+ return
1626
+ self._orb_mode = True
1627
+ self._was_maximized = bool(self.windowState() & Qt.WindowMaximized)
1628
+ self._saved_geom = self.normalGeometry() # restore par wahi size/jagah
1629
+ self.hide()
1630
+ self._float_orb.show_orb()
1631
+ self._sync_orb_menu()
1632
+
1633
+ def _show_main(self):
1634
+ """Orb/tray/dusri copy se: MYRA ki main window wapas lao."""
1635
+ self._orb_mode = False
1636
+ if self._float_orb:
1637
+ self._float_orb.hide_orb()
1638
+ self.setWindowState(self.windowState() & ~Qt.WindowMinimized)
1639
+ if getattr(self, "_was_maximized", False):
1640
+ self.showMaximized()
1641
+ else:
1642
+ self.showNormal()
1643
+ g = getattr(self, "_saved_geom", None)
1644
+ if g is not None and g.width() >= self.minimumWidth() and g.height() >= self.minimumHeight():
1645
+ self.setGeometry(g)
1646
+ self.raise_()
1647
+ self.activateWindow()
1648
+ try:
1649
+ import ctypes
1650
+ hwnd = int(self.winId())
1651
+ ctypes.windll.user32.ShowWindow(hwnd, 9)
1652
+ ctypes.windll.user32.SetForegroundWindow(hwnd)
1653
+ except Exception:
1654
+ pass
1655
+
1656
+ def start_in_background(self):
1657
+ """Autostart / --background: window nahi dikhegi, sirf orb + tray."""
1658
+ self._enter_orb_mode()
1659
+ if not self._float_orb:
1660
+ self.show()
1661
+
1662
+ def changeEvent(self, e):
1663
+ if (e.type() == QEvent.WindowStateChange and self.isMinimized() and not getattr(self, "_orb_mode", False)
1664
+ and getattr(self, "_float_orb", None) and _prefs.get("orb_on_minimize", True)):
1665
+ QTimer.singleShot(0, self._enter_orb_mode)
1666
+ super().changeEvent(e)
1667
+
1668
+ def _preview_orb(self, ms=5000):
1669
+ """Settings se: orb abhi dikhao (size/jagah set karne ke liye)."""
1670
+ if not self._float_orb or self._orb_mode:
1671
+ return
1672
+ self._preview_token += 1
1673
+ tok = self._preview_token
1674
+ self._float_orb.show_orb()
1675
+ self._sync_orb_menu()
1676
+ QTimer.singleShot(ms, lambda: (self._float_orb.hide_orb() if (tok == self._preview_token and not self._orb_mode) else None))
1677
+
1678
+ def _on_orb_action(self, key):
1679
+ if key == "open":
1680
+ self._show_main()
1681
+ elif key == "settings":
1682
+ self._show_main(); self._show_page("settings")
1683
+ elif key == "mic":
1684
+ self._on_mic_toggle()
1685
+ elif key == "guardian":
1686
+ self._on_guardian_toggle(not (self._guardian and self._guardian.is_running))
1687
+ elif key == "screenshare":
1688
+ self._on_screenshare()
1689
+ elif key == "camera":
1690
+ self._on_camera()
1691
+ elif key == "quit":
1692
+ self._quit_app()
1693
+ QTimer.singleShot(250, self._sync_orb_menu)
1694
+
1695
+ def _quit_app(self):
1696
+ try:
1697
+ self.close()
1698
+ except Exception:
1699
+ pass
1700
+ QApplication.instance().quit()
1701
+
1702
+ def _sync_orb_menu(self):
1703
+ if not getattr(self, "_float_orb", None):
1704
+ return
1705
+ g = bool(self._guardian and self._guardian.is_running)
1706
+ ss = bool(_HAS_SCREEN_SHARE and self._ss_mgr.is_active)
1707
+ cam = bool(_HAS_CAM_VISION and hasattr(self, "_cv_mgr") and self._cv_mgr.is_active)
1708
+ self._float_orb.set_menu_state(mic=self._mic_on, guardian=g, screenshare=ss, camera=cam)
1709
+ lab = {"listening": "Listening", "speaking": "Speaking", "thinking": "Thinking", "error": "Error"}.get(
1710
+ getattr(self, "_voice_state", "standby"), "Ready")
1711
+ self._float_orb._menu.set_state_text("Mic off" if not self._mic_on else lab, "#9C99A4" if not self._mic_on else None)
1712
+ self._bus.state["guardian"] = g
1713
+ self._bus.state["autostart"] = bool(_HAS_ORB and _autostart.is_enabled())
1714
+
1715
+ def _on_orb_float_size(self, v):
1716
+ self._bus.state["orb_float_size"] = int(v)
1717
+ if self._float_orb:
1718
+ self._float_orb.set_size(int(v))
1719
+ if not self._orb_mode:
1720
+ self._preview_orb(4000)
1721
+
1722
+ def _on_orb_ui_scale(self, v):
1723
+ self._bus.state["orb_ui_scale"] = int(v)
1724
+ _prefs.set_("orb_ui_scale", int(v))
1725
+ self._apply_orb_ui_scale(int(v))
1726
+
1727
+ def _apply_orb_ui_scale(self, pct):
1728
+ f = max(0.5, min(1.5, pct / 100.0))
1729
+ try:
1730
+ self._orb.set_orb_scale(f)
1731
+ self._p_voice.set_orb_size(int(230 * f))
1732
+ except Exception as e:
1733
+ print(f"[MYRA] orb scale: {e}")
1734
+
1735
+ def _on_autostart_toggle(self, on):
1736
+ if not _HAS_ORB:
1737
+ return
1738
+ ok = _autostart.enable(True) if on else _autostart.disable()
1739
+ if ok:
1740
+ self._chat.add_sys("🚀 MYRA ab Windows start par khud chalu hogi (orb ke saath)" if on else "MYRA ka Windows startup band kiya")
1741
+ else:
1742
+ self._chat.add_error("Startup setting badal nahi paayi")
1743
+ self._bus.state["autostart"] = _autostart.is_enabled()
1744
+
1745
+ def _on_guardian_toggle(self, on):
1746
+ if not self._guardian:
1747
+ return
1748
+ _prefs.set_("guardian", bool(on))
1749
+ self._bus.state["guardian"] = bool(on)
1750
+ if on:
1751
+ self._guardian.start()
1752
+ self._chat.add_sys("👁 Screen Guardian ON — MYRA screen dekhti rahegi aur kuch galat dikhe to batayegi")
1753
+ else:
1754
+ self._guardian.stop()
1755
+ self._chat.add_sys("👁 Screen Guardian OFF")
1756
+ self._sync_orb_menu()
1757
+
1758
+ def _guardian_busy(self):
1759
+ return bool((_HAS_SCREEN_SHARE and self._ss_mgr.is_active)
1760
+ or getattr(self, "_voice_state", "standby") in ("speaking", "thinking"))
1761
+
1762
+ def _on_guardian_alert(self, text):
1763
+ self._chat.add_warning(f"👁 Screen Guardian: {text}")
1764
+ if self._float_orb:
1765
+ if not self._float_orb.isVisible():
1766
+ self._preview_orb(15000) # orb dikha do taaki chetawani nazar aaye
1767
+ self._float_orb.say(text, "warn", 14000)
1768
+ if hasattr(self, "_worker"):
1769
+ self._worker.send_text(f"Ye chetawani user ko chhote mein bolkar suna do ('{_prefs.user_name() or 'Sir'},' se shuru, kuch aur add mat karo): " + text)
1770
+
1771
+ def first_run_setup(self):
1772
+ """Ek baar: startup + Screen Guardian ke liye user ki sahmati."""
1773
+ if not _HAS_ORB or _prefs.get("setup_asked", False):
1774
+ return
1775
+ _prefs.set_("setup_asked", True)
1776
+ from PyQt5.QtWidgets import QCheckBox
1777
+ from myra_ui import pill
1778
+ d = QDialog(self)
1779
+ d.setWindowTitle("MYRA setup")
1780
+ d.setModal(True)
1781
+ d.setFixedWidth(520)
1782
+ d.setStyleSheet(f"QDialog{{background:{_UI['bg']};}}QLabel{{color:#EDEBF0;}}QCheckBox{{color:#EDEBF0;spacing:10px;}}")
1783
+ v = QVBoxLayout(d); v.setContentsMargins(26, 24, 26, 22); v.setSpacing(14)
1784
+ t = QLabel("MYRA ko hamesha saath rakho"); t.setStyleSheet("color:#fff;font-size:20px;font-weight:800;")
1785
+ v.addWidget(t)
1786
+ sub = QLabel("Minimize karne par MYRA ek chhote orb mein badal jaati hai — use kahin bhi rakh sakte ho. "
1787
+ "Neeche ke options chalu karne se wo PC start hote hi orb ke saath aa jaati hai.")
1788
+ sub.setWordWrap(True); v.addWidget(sub)
1789
+ from PyQt5.QtWidgets import QLineEdit
1790
+ nm = QLineEdit(); nm.setMaxLength(30); nm.setPlaceholderText("Aapka naam (MYRA isi naam se bulayegi)")
1791
+ nm.setText(_prefs.user_name())
1792
+ nm.setStyleSheet("QLineEdit{background:rgba(255,255,255,0.06);color:#fff;border:1px solid rgba(255,255,255,0.18);"
1793
+ "border-radius:10px;padding:9px 12px;font-size:15px;}QLineEdit:focus{border:1px solid #FF2E43;}")
1794
+ v.addWidget(nm)
1795
+ c1 = QCheckBox("Windows start / restart par MYRA khud chalu ho (background mein orb)")
1796
+ c1.setChecked(True); v.addWidget(c1)
1797
+ c2 = QCheckBox("Screen Guardian: MYRA screen dekhe aur kuch galat lage to bataye")
1798
+ c2.setChecked(True); v.addWidget(c2)
1799
+ note = QLabel("Guardian ke liye screenshot Gemini ko jaata hai — sirf jab screen badli ho. Password / banking / "
1800
+ "incognito window par kabhi nahi bheja jaata. Baad mein Settings se kabhi bhi band kar sakte ho.")
1801
+ note.setWordWrap(True); note.setStyleSheet("color:#9C99A4;font-size:11px;"); v.addWidget(note)
1802
+ row = QHBoxLayout(); row.addStretch()
1803
+ skip = pill("Abhi nahi", None, "ghost", 38); go = pill("Continue", None, "primary", 38)
1804
+ skip.clicked.connect(_myra_slot(d.reject)); go.clicked.connect(_myra_slot(d.accept))
1805
+ row.addWidget(skip); row.addWidget(go); v.addLayout(row)
1806
+ if d.exec_() == QDialog.Accepted:
1807
+ if nm.text().strip():
1808
+ self._on_user_name_set(nm.text())
1809
+ if c1.isChecked():
1810
+ self._on_autostart_toggle(True)
1811
+ if c2.isChecked():
1812
+ self._on_guardian_toggle(True)
1813
+
1814
+ # ── USER NAME ────────────────────────────────────────────────
1815
+ def _init_user_name(self):
1816
+ self._bus.user_name.connect(self._on_user_name_set)
1817
+ try:
1818
+ self._orb.set_user_name(_prefs.user_name())
1819
+ except Exception as e:
1820
+ print(f"[MYRA] user name init: {e}")
1821
+
1822
+ def _on_user_name_set(self, name):
1823
+ n = _prefs.set_user_name(name)
1824
+ if not n:
1825
+ return
1826
+ self._bus.state["user_name"] = n
1827
+ try:
1828
+ self._orb.set_user_name(n)
1829
+ except Exception:
1830
+ pass
1831
+ self._chat.add_sys(f"👋 Namaste {n}! Ab MYRA tumhe {n} naam se bulayegi")
1832
+ if hasattr(self, "_worker"):
1833
+ self._worker.send_text(f"User ka naam {n} hai (abhi set kiya). Ab se unhe {n} naam se bulao. "
1834
+ f"Ek chhota sa warm welcome bolo — {n} naam ke saath.")
1835
+
1836
+ # ── LANGUAGE ─────────────────────────────────────────────────
1837
+ def _init_language(self):
1838
+ import myra_lang
1839
+ self._bus.language.connect(self._on_language_set)
1840
+ self._bus.state["language"] = (myra_lang.current() or ("auto",))[0]
1841
+
1842
+ def _on_language_set(self, code):
1843
+ import myra_lang
1844
+ try:
1845
+ it = myra_lang.set_language(code)
1846
+ except ValueError:
1847
+ return
1848
+ self._bus.state["language"] = it[0] if it else "auto"
1849
+ name = it[1] if it else "Auto (Hinglish)"
1850
+ self._chat.add_sys(f"🌐 Language → {name} — reconnecting...")
1851
+ if hasattr(self, "_worker"):
1852
+ self._worker.reset_greet(); self._worker.reconnect()
1853
+
1854
+ def _sync_language(self):
1855
+ import myra_lang
1856
+ c = (myra_lang.current() or ("auto",))[0]
1857
+ if c != self._bus.state.get("language", "auto"):
1858
+ self._bus.state["language"] = c
1859
+
1860
+ def _sync_user_name(self):
1861
+ """Voice tool (set_user_name) ya dusri jagah se naam badla ho to UI mein dikhao."""
1862
+ n = _prefs.user_name()
1863
+ if n != self._bus.state.get("user_name", ""):
1864
+ self._bus.state["user_name"] = n
1865
+ try:
1866
+ self._orb.set_user_name(n)
1867
+ except Exception:
1868
+ pass
1869
+
1870
+ def ask_name_if_missing(self):
1871
+ """Naam set nahi hai to ek baar poocho (pehli baar ke setup mein na aaya ho tab)."""
1872
+ if _prefs.user_name() or _prefs.get("name_asked", False):
1873
+ return
1874
+ _prefs.set_("name_asked", True)
1875
+ from PyQt5.QtWidgets import QLineEdit
1876
+ from myra_ui import pill
1877
+ d = QDialog(self)
1878
+ d.setWindowTitle("Aapka naam")
1879
+ d.setModal(True)
1880
+ d.setFixedWidth(460)
1881
+ d.setStyleSheet(f"QDialog{{background:{_UI['bg']};}}QLabel{{color:#EDEBF0;}}"
1882
+ "QLineEdit{background:rgba(255,255,255,0.06);color:#fff;border:1px solid rgba(255,255,255,0.18);"
1883
+ "border-radius:10px;padding:9px 12px;font-size:15px;}QLineEdit:focus{border:1px solid #FF2E43;}")
1884
+ v = QVBoxLayout(d); v.setContentsMargins(26, 24, 26, 22); v.setSpacing(12)
1885
+ t = QLabel("MYRA ko aapka naam batao"); t.setStyleSheet("color:#fff;font-size:20px;font-weight:800;")
1886
+ sub = QLabel("MYRA aapko isi naam se bulayegi. Baad mein Settings → Profile se badal sakte ho.")
1887
+ sub.setWordWrap(True)
1888
+ ed = QLineEdit(); ed.setMaxLength(30); ed.setPlaceholderText("Aapka naam (jaise: Vikash)")
1889
+ row = QHBoxLayout(); row.addStretch()
1890
+ later = pill("Baad mein", None, "ghost", 38); ok = pill("Save", None, "primary", 38)
1891
+ later.clicked.connect(_myra_slot(d.reject)); ok.clicked.connect(_myra_slot(d.accept)); ed.returnPressed.connect(_myra_slot(d.accept))
1892
+ row.addWidget(later); row.addWidget(ok)
1893
+ for w in (t, sub, ed):
1894
+ v.addWidget(w)
1895
+ v.addLayout(row)
1896
+ if d.exec_() == QDialog.Accepted and ed.text().strip():
1897
+ self._on_user_name_set(ed.text())
1898
+
1899
+ # ── UI BUILD ─────────────────────────────────────────────────
1900
+ def _build_ui(self):
1901
+ load_fonts() # bundled Poppins
1902
+ _sp(73)
1903
+ self.setStyleSheet(f"QMainWindow{{background:{_UI['bg']};}}QWidget{{color:{_UI['text']};font-family:'Poppins','Segoe UI Variable','Segoe UI',Arial;}}"
1904
+ "QLabel{background:transparent;}QToolTip{background:#15121A;color:#fff;border:1px solid #FF2E43;padding:4px;}")
1905
+ shell = QWidget(); shell.setObjectName("shell")
1906
+ shell.setStyleSheet(f"#shell{{background:{_UI['bg']};border:1px solid rgba(255,46,67,0.28);}}")
1907
+ self.setCentralWidget(shell)
1908
+ shell_vl = QVBoxLayout(shell); shell_vl.setContentsMargins(1, 1, 1, 1); shell_vl.setSpacing(0)
1909
+
1910
+ self._bus = Bus()
1911
+ self._bus.state["edge_glow"] = _load_edge_pref()
1912
+ self._bus.state["user_name"] = _prefs.user_name()
1913
+ self._bus.state.update(
1914
+ orb_on_minimize=bool(_prefs.get("orb_on_minimize", True)),
1915
+ orb_float_size=int(_prefs.get("float_orb_size", 96)),
1916
+ orb_ui_scale=int(_prefs.get("orb_ui_scale", 100)),
1917
+ guardian=bool(_prefs.get("guardian", False)),
1918
+ guardian_interval=int(_prefs.get("guardian_interval", 45)),
1919
+ autostart=bool(_HAS_ORB and _autostart.is_enabled()))
1920
+ mid = QHBoxLayout(); mid.setContentsMargins(0, 0, 0, 0); mid.setSpacing(0)
1921
+ self._sidebar = Sidebar(_HAS_WEBFORGE); mid.addWidget(self._sidebar)
1922
+ self._sidebar.sig_nav.connect(self._on_nav)
1923
+
1924
+ col = QVBoxLayout(); col.setContentsMargins(0, 0, 0, 0); col.setSpacing(0)
1925
+
1926
+ # top strip: Online ▾ + window controls (har page par)
1927
+ self._topbar = HeaderBar(_LIVE_MODEL_KEY, [(k, v[0]) for k, v in LIVE_MODELS.items()])
1928
+ self._topbar.sig_mode.connect(self._on_mode)
1929
+ self._topbar.sig_mic_select.connect(self._on_mic_select)
1930
+ self._topbar.sig_api_change.connect(self._on_api_change)
1931
+ self._topbar.sig_model.connect(self._on_model_switch)
1932
+ self._topbar.sig_voice.connect(self._on_voice)
1933
+ self._topbar.sig_camera.connect(self._on_camera)
1934
+ self._topbar.sig_screenshare.connect(self._on_screenshare)
1935
+ col.addWidget(self._topbar)
1936
+
1937
+ # ── pages ──
1938
+ self._pages = QStackedWidget()
1939
+ home = QWidget(); hh = QHBoxLayout(home); hh.setContentsMargins(0, 0, 0, 0); hh.setSpacing(0)
1940
+ self._orb = HeroPanel()
1941
+ self._orb.sig_mode.connect(self._on_listen_mode); self._orb.sig_orb.connect(self._on_orb_tap)
1942
+ self._orb.sig_chip.connect(self._on_hero_chip)
1943
+ hh.addWidget(self._orb, 1)
1944
+ right = QWidget(); right.setFixedWidth(430)
1945
+ rv = QVBoxLayout(right); rv.setContentsMargins(0, 0, 18, 12); rv.setSpacing(14)
1946
+ self._stats = StatsCard(); rv.addWidget(self._stats)
1947
+ self._quick = QuickToolsCard(); self._quick.sig_tool.connect(self._on_quick_tool); rv.addWidget(self._quick, 1)
1948
+ self._recent = RecentActivityCard(); self._recent.sig_view_all.connect(lambda: self._show_page("chat")); rv.addWidget(self._recent)
1949
+ hh.addWidget(right)
1950
+
1951
+ _sp(76)
1952
+ self._chat = ChatWidget()
1953
+ self._p_voice = VoicePage(self._bus)
1954
+ _sp(78)
1955
+ self._p_media = MediaPage(self._bus)
1956
+ _sp(79)
1957
+ self._p_studio = StudioPage(self._bus)
1958
+ self._p_studio.engine.sig_done.connect(self._on_studio_done)
1959
+ self._p_studio.engine.sig_error.connect(self._on_studio_error)
1960
+ _sp(81)
1961
+ _pg_chat = ChatPage(self._bus, self._chat); _sp(82)
1962
+ _pg_pc = PCControlPage(self._bus); _sp(83)
1963
+ _pg_apps = AppsToolsPage(self._bus); _sp(84)
1964
+ _pg_auto = AutomationPage(self._bus); _sp(85)
1965
+ _pg_files = FilesPage(self._bus); _sp(86)
1966
+ _pg_web = WebSearchPage(self._bus); _sp(87)
1967
+ _pg_mon = SystemMonitorPage(self._bus); _sp(88)
1968
+ _pg_set = SettingsPage(self._bus); _sp(89)
1969
+ _pg_about = AboutPage(self._bus); _sp(90)
1970
+ order = [("home", home), ("chat", _pg_chat), ("voice", self._p_voice),
1971
+ ("pc", _pg_pc), ("apps", _pg_apps), ("automation", _pg_auto),
1972
+ ("files", _pg_files), ("media", self._p_media), ("web", _pg_web),
1973
+ ("studio", self._p_studio),
1974
+ ("monitor", _pg_mon), ("settings", _pg_set),
1975
+ ("about", _pg_about)]
1976
+ self._page_idx = {}
1977
+ for i, (k, w) in enumerate(order):
1978
+ self._pages.addWidget(w); self._page_idx[k] = i
1979
+ self._cam_panel_widget = self._p_media.cam_holder
1980
+ col.addWidget(self._pages, 1)
1981
+
1982
+ # bottom: input bar + suggestion chips
1983
+ self._input = InputBar()
1984
+ self._input.sig_send.connect(self._on_send_text)
1985
+ self._input.sig_mic.connect(self._on_mic_toggle)
1986
+ self._input.sig_attach.connect(self._on_attach)
1987
+ self._input.sig_image.connect(self._on_camera)
1988
+ ib = QHBoxLayout(); ib.setContentsMargins(40, 4, 22, 0); ib.addWidget(self._input)
1989
+ col.addLayout(ib)
1990
+ self._chips = ChipsRow(); self._chips.sig_chip.connect(self._on_send_text)
1991
+ col.addWidget(self._chips)
1992
+ mid.addLayout(col, 1)
1993
+ shell_vl.addLayout(mid, 1)
1994
+
1995
+ self._statusbar = StatusBar()
1996
+ self._statusbar.sig_globe.connect(lambda: self._on_nav("web"))
1997
+ self._statusbar.sig_keyboard.connect(self._on_mic_select)
1998
+ self._statusbar.sig_settings.connect(lambda: self._on_nav("settings"))
1999
+ self._statusbar.sig_fullscreen.connect(self._toggle_fullscreen)
2000
+ self._topbar.sig_status_text.connect(self._statusbar.set_status)
2001
+ shell_vl.addWidget(self._statusbar)
2002
+
2003
+ # purane BottomControls attrs (cam_btn/voice_btn/screen_btn) ab header menu se
2004
+ self._ctrl = self._topbar
2005
+
2006
+ # ── bus wiring (pages -> window) ──
2007
+ b = self._bus
2008
+ b.prompt.connect(self._on_send_text); b.goto.connect(self._on_nav)
2009
+ b.listen_mode.connect(self._on_listen_mode); b.voice_toggle.connect(self._on_voice)
2010
+ b.model_pick.connect(self._on_model_switch); b.mic_select.connect(self._on_mic_select)
2011
+ b.api_change.connect(self._on_api_change); b.persona_toggle.connect(self._on_mode)
2012
+ b.camera_toggle.connect(self._on_camera); b.screenshare_toggle.connect(self._on_screenshare)
2013
+ b.coder.connect(self._open_myra_coder_prompt); b.webforge.connect(self._open_webforge_prompt)
2014
+ b.fullscreen.connect(self._toggle_fullscreen); b.always_on_top.connect(self._on_always_top)
2015
+ b.deploy_site.connect(lambda d: self._deploy_dir(Path(d), Path(d).name))
2016
+ b.toast.connect(self._statusbar.flash); b.sens_changed.connect(self._on_sens)
2017
+ self._listen_mode = "continuous"
2018
+ QTimer(self, interval=1500, timeout=self._publish_state).start()
2019
+
2020
+ # ── MYRA CODER OVERLAY ────────────────────────────────────
2021
+ # v3.0: QDialog — parent = self (ARIAWindow) — guaranteed visible
2022
+ if _HAS_MYRA_CODER:
2023
+ self._myra_coder_overlay = MyraCoderOverlay(self)
2024
+ self._myra_coder_overlay.sig_done.connect(self._on_myra_coder_done)
2025
+ self._myra_coder_overlay.sig_error.connect(self._on_myra_coder_error)
2026
+ print("[MYRA] MYRA Coder overlay (QDialog) initialized")
2027
+ else:
2028
+ self._myra_coder_overlay = None
2029
+
2030
+ # ── pages / state ────────────────────────────────────────────
2031
+ def _show_page(self, name: str):
2032
+ name = {"vision": "media"}.get(name, name)
2033
+ self._pages.setCurrentIndex(self._page_idx.get(name, 0))
2034
+ self._sidebar.set_active(name if name in self._page_idx else "home")
2035
+
2036
+ def _toggle_fullscreen(self):
2037
+ self.showNormal() if self.isFullScreen() else self.showFullScreen()
2038
+
2039
+ def _on_always_top(self, on: bool):
2040
+ self.setWindowFlag(Qt.WindowStaysOnTopHint, bool(on)); self.show()
2041
+
2042
+ def _set_orb_state(self, s: str):
2043
+ self._orb.set_state(s); self._p_voice.orb.set_state(s)
2044
+ if getattr(self, '_float_orb', None):
2045
+ self._float_orb.set_state(s)
2046
+
2047
+ def _publish_state(self):
2048
+ st = self._bus.state
2049
+ w = getattr(self, "_worker", None)
2050
+ st.update(mic_on=self._mic_on, listen_mode=self._listen_mode, voice=_CURRENT_VOICE, model_key=_LIVE_MODEL_KEY,
2051
+ models=[(k, v[0]) for k, v in LIVE_MODELS.items()], persona=self._mode,
2052
+ cam_on=bool(_HAS_CAM_VISION and self._cv_mgr.is_active) if hasattr(self, "_cv_mgr") else False,
2053
+ screen_on=bool(self._ss_mgr.is_active) if hasattr(self, "_ss_mgr") else False,
2054
+ session=(w.info() if w else {}), rms=SILENCE_RMS, gain=MIC_GAIN)
2055
+ self._sync_orb_menu()
2056
+ self._sync_user_name(); self._sync_language()
2057
+ self._bus.state_changed.emit()
2058
+
2059
+ def _on_sens(self, rms: int, gain: float):
2060
+ global SILENCE_RMS, MIC_GAIN
2061
+ SILENCE_RMS, MIC_GAIN = int(rms), float(gain)
2062
+
2063
+ # ── listening modes: wake / tap / continuous / stop ──────────
2064
+ def _on_listen_mode(self, mode: str):
2065
+ if mode == "tap" and self._listen_mode == "tap": # orb tap = talk
2066
+ self._on_speak(); return
2067
+ self._listen_mode = mode
2068
+ if hasattr(self, "_worker"): self._worker.set_listen_mode(mode)
2069
+ self._orb.set_mode(mode); self._p_voice.modes.set_mode(mode)
2070
+ if mode == "stop":
2071
+ self._on_mute()
2072
+ elif mode == "tap":
2073
+ self._on_mute(); self._statusbar.flash("Tap to Talk — orb/mic dabao aur bolo")
2074
+ else:
2075
+ self._on_speak()
2076
+ if mode == "wake": self._statusbar.flash("Wake Word — 'MYRA' bolke baat shuru karo")
2077
+ self._publish_state()
2078
+
2079
+ def _on_orb_tap(self):
2080
+ if self._listen_mode == "tap" or not self._mic_on:
2081
+ self._on_speak()
2082
+ else:
2083
+ self._on_mute()
2084
+
2085
+ def _on_mic_state(self, on: bool):
2086
+ """Worker ne mic khud band kiya (tap mode: jawab ke baad)."""
2087
+ self._mic_on = on; self._input.set_mic_on(on)
2088
+ if not on: self._topbar.set_status("standby", "TAP TO TALK")
2089
+
2090
+ # ── NAV / INPUT / QUICK TOOLS ────────────────────────────────
2091
+ def _on_nav(self, key: str):
2092
+ self._show_page(key)
2093
+
2094
+ def _on_hero_chip(self, text: str):
2095
+ if text == "Chat with me": self._show_page("chat")
2096
+ elif text == "Automate tasks": self._show_page("automation")
2097
+ elif text == "Work with files": self._show_page("files")
2098
+ elif text == "And much more…": self._show_page("apps")
2099
+ elif text == "Control your PC": self._show_page("pc")
2100
+ else:
2101
+ self._input.edit.setText("Search the web for ")
2102
+ self._input.edit.setFocus()
2103
+
2104
+ def _on_send_text(self, text: str):
2105
+ text = (text or "").strip()
2106
+ if not text: return
2107
+ self._chat.finalize_user(text)
2108
+ self._recent.add(f"You: {text}")
2109
+ if hasattr(self, "_worker"): self._worker.send_text(text)
2110
+ if not self._trigger_myra_coder(text): # "website banao" jaise commands Website Studio chalate hain
2111
+ self._show_page("chat")
2112
+
2113
+ def _on_mic_toggle(self):
2114
+ if self._listen_mode == "tap": # tap mode: mic button = talk
2115
+ self._on_speak()
2116
+ elif self._mic_on: self._on_mute()
2117
+ else: self._on_speak()
2118
+
2119
+ def _on_attach(self):
2120
+ path, _ = QFileDialog.getOpenFileName(self, "File chuno")
2121
+ if path: self._on_send_text(f"Is file ko dekho aur summarize karo: {path}")
2122
+
2123
+ def _on_quick_tool(self, key: str):
2124
+ if key == "view_all": self._show_page("chat"); return
2125
+ if key == "open_app":
2126
+ name, ok = QInputDialog.getText(self, "Open App", "Kaun sa app kholna hai?")
2127
+ if ok and name.strip(): self._on_send_text(f"Open {name.strip()}")
2128
+ return
2129
+ if key == "whatsapp":
2130
+ who, ok = QInputDialog.getText(self, "WhatsApp", "Kisko message bhejna hai? (naam/number)")
2131
+ if ok and who.strip(): self._on_send_text(f"WhatsApp par {who.strip()} ko message bhejna hai")
2132
+ return
2133
+ if key == "shutdown":
2134
+ if QMessageBox.question(self, "Shutdown", "PC band kar dun?") != QMessageBox.Yes: return
2135
+ cmds = {"screenshot": "Screenshot lo", "clipboard": "Clipboard mein kya hai batao",
2136
+ "volume": "Volume badlo", "brightness": "Brightness badlo",
2137
+ "lock": "PC lock kar do", "shutdown": "PC shutdown kar do"}
2138
+ if key in cmds: self._on_send_text(cmds[key])
2139
+
2140
+ # ── CAMERA VISION ────────────────────────────────────────────
2141
+ def _init_cam_vision(self):
2142
+ self._cv_mgr = CameraVisionManager()
2143
+ if _HAS_CAM_VISION:
2144
+ vl = self._cam_panel_widget.layout()
2145
+ vl.setContentsMargins(0, 0, 0, 0); vl.setSpacing(0)
2146
+ self._cv_panel = CameraVisionPanel(self._cv_mgr)
2147
+ self._cv_panel.sig_describe_now.connect(self._on_cv_describe_now)
2148
+ vl.addWidget(self._cv_panel)
2149
+ self._cv_mgr.sig_started.connect(self._on_cv_started)
2150
+ self._cv_mgr.sig_stopped.connect(self._on_cv_stopped)
2151
+ self._cv_mgr.sig_error.connect(self._on_cv_error)
2152
+ self._cv_mgr.sig_vision_frame.connect(self._on_cv_frame)
2153
+ self._cv_mgr.sig_status.connect(lambda s: self._chat.add_sys(s))
2154
+ # ✅ FIX: sidebar ka mini-preview ab isi single shared manager se
2155
+ # frames leta hai — apna alag camera handle nahi kholta
2156
+ self._cv_mgr.sig_ui_frame.connect(self._sidebar.update_camera_frame)
2157
+ else:
2158
+ lbl = QLabel("📷 Camera Vision\nnot available\npip install opencv-python")
2159
+ lbl.setAlignment(Qt.AlignCenter)
2160
+ lbl.setStyleSheet("color:#334455;font-size:12px;background:transparent;padding:20px;")
2161
+ lbl.setWordWrap(True)
2162
+ self._cam_panel_widget.layout().addWidget(lbl)
2163
+
2164
+ def _on_cv_frame(self, b64_jpeg: str):
2165
+ if hasattr(self, '_worker'):
2166
+ self._worker.push_screen_frame(b64_jpeg)
2167
+ # FIX: pehle frame ke BAAD hi MYRA ko bolo (frame aane se pehle bolne par
2168
+ # wo andaza lagakar fake describe karti thi)
2169
+ if getattr(self, '_cv_first_frame_pending', False):
2170
+ self._cv_first_frame_pending = False
2171
+ QTimer.singleShot(700, lambda: self._worker.send_text(
2172
+ "Camera ka REAL frame abhi tumhe mil gaya hai. Sirf wahi batao jo frame mein "
2173
+ "sach mein dikh raha hai. Andaza ya kalpana mat karo; jo clear na dikhe wo "
2174
+ "bolo 'clear nahi dikh raha'."))
2175
+
2176
+ def _on_cv_describe_now(self):
2177
+ if hasattr(self, '_worker') and _HAS_CAM_VISION:
2178
+ self._cv_mgr.describe_now(
2179
+ get_ws_func=self._worker.get_ws,
2180
+ get_loop_func=self._worker.get_loop
2181
+ )
2182
+ self._chat.add_sys("👁 Camera frame capture hua — MYRA dekh rahi hai...")
2183
+
2184
+ def _on_cv_started(self):
2185
+ self._chat.add_sys("📷 Camera Vision ON — pehla frame bhej rahi hoon...")
2186
+ self._sidebar.update_camera_status(True)
2187
+ self._ctrl.cam_btn.setText("Camera Off")
2188
+ # FIX: ab MYRA ko pehle frame aane ke baad hi bola jaata hai (_on_cv_frame mein).
2189
+ # Agar 8 sec mein koi frame nahi aaya to user ko sach bata do — fake mat bolo.
2190
+ self._cv_first_frame_pending = True
2191
+ self._update_edge_glow()
2192
+ def _no_frame_check():
2193
+ if getattr(self, '_cv_first_frame_pending', False) and self._cv_mgr.is_active:
2194
+ self._cv_first_frame_pending = False
2195
+ self._chat.add_error("Camera ka frame MYRA tak nahi pahunch raha — Gemini session connect nahi hai ya camera block hai.")
2196
+ QTimer.singleShot(8000, _no_frame_check)
2197
+
2198
+ def _on_cv_stopped(self):
2199
+ self._update_edge_glow()
2200
+ self._chat.add_sys("📷 Camera Vision OFF")
2201
+ self._sidebar.update_camera_status(False)
2202
+ self._ctrl.cam_btn.setText("🎥 Camera On/Off")
2203
+ if hasattr(self, '_worker'):
2204
+ self._worker.send_text("Camera band ho gayi. Normal mode.")
2205
+
2206
+ def _on_cv_error(self, err: str):
2207
+ self._chat.add_error(f"Camera: {err}")
2208
+ self._sidebar.update_camera_status(False)
2209
+ self._ctrl.cam_btn.setText("🎥 Camera On/Off")
2210
+
2211
+ # ── SCREEN SHARE ─────────────────────────────────────────────
2212
+ def _init_screenshare(self):
2213
+ self._ss_mgr = ScreenShareManager(fps=1.0)
2214
+ if _HAS_SCREEN_SHARE:
2215
+ self._ss_mgr.sig_started.connect(self._on_ss_started)
2216
+ self._ss_mgr.sig_stopped.connect(self._on_ss_stopped)
2217
+ self._ss_mgr.sig_error.connect(self._on_ss_error)
2218
+ self._ss_mgr.sig_status.connect(lambda s: self._chat.add_sys(s))
2219
+ self._ss_mgr.sig_frame_ready.connect(self._on_ss_frame)
2220
+
2221
+ def _on_ss_frame(self, b64_jpeg: str):
2222
+ if hasattr(self, '_worker'):
2223
+ self._worker.push_screen_frame(b64_jpeg)
2224
+ if getattr(self, '_ss_first_frame_pending', False):
2225
+ self._ss_first_frame_pending = False
2226
+ QTimer.singleShot(700, lambda: self._worker.send_text(
2227
+ "User ne screen sharing ON kar di hai aur screen ka REAL frame tumhe mil gaya hai. "
2228
+ "Sirf wahi batao jo frame mein sach mein dikh raha hai — andaza mat lagao. "
2229
+ "Chhota sa bolo ki ab tum screen dekh sakti ho aur poocho kya madad chahiye."))
2230
+
2231
+ def _on_ss_started(self):
2232
+ self._ctrl.screen_btn.set_sharing(True)
2233
+ self._sidebar.update_screenshare_status(True)
2234
+ self._chat.add_screen_share_badge(True)
2235
+ self._set_orb_state("screenshare")
2236
+ self._topbar.set_status("screenshare", "SCREEN SHARE ▣")
2237
+ self._ss_first_frame_pending = True # MYRA ko pehla frame jaane ke BAAD hi bolo (fake describe na ho)
2238
+ self._update_edge_glow()
2239
+
2240
+ def _on_ss_stopped(self):
2241
+ self._ctrl.screen_btn.set_sharing(False)
2242
+ self._sidebar.update_screenshare_status(False)
2243
+ self._chat.add_screen_share_badge(False)
2244
+ self._set_orb_state("idle")
2245
+ self._topbar.set_status("standby", "STANDBY")
2246
+ self._ss_first_frame_pending = False
2247
+ self._update_edge_glow()
2248
+ if hasattr(self, '_worker'):
2249
+ self._worker.send_text("Screen sharing band ho gayi. Ab normal conversation mode.")
2250
+
2251
+ def _on_ss_error(self, err: str):
2252
+ self._chat.add_error(f"Screen share error: {err}")
2253
+ self._ctrl.screen_btn.set_sharing(False)
2254
+ self._sidebar.update_screenshare_status(False)
2255
+
2256
+ def _on_screenshare(self):
2257
+ if not _HAS_SCREEN_SHARE:
2258
+ self._chat.add_error("Screen share module missing — pip install mss pillow")
2259
+ return
2260
+ self._ss_mgr.toggle()
2261
+
2262
+ # ══════════════════════════════════════════════════════════════
2263
+ # WEBFORGE — VOICE COMMAND INTERCEPT + PIPELINE
2264
+ # ══════════════════════════════════════════════════════════════
2265
+ def _on_user_msg_website(self, text: str):
2266
+ """
2267
+ MYRA Coder intercept — website build commands.
2268
+ sig_user_msg (turnComplete) pe fire hota hai.
2269
+ """
2270
+ self._trigger_myra_coder(text)
2271
+
2272
+ def _on_user_live_website(self, text: str):
2273
+ """
2274
+ MYRA Coder EARLY intercept — sig_user_live (streaming) pe.
2275
+ Jaise hi user ka sentence complete lagta hai, detect karo.
2276
+ Ye Gemini ke tool call se PEHLE trigger hota hai.
2277
+ """
2278
+ # Sirf full sentence detect karo (min words)
2279
+ if len(text.split()) < 3:
2280
+ return
2281
+ self._trigger_myra_coder(text)
2282
+
2283
+ def _trigger_myra_coder(self, text: str, force: bool = False) -> bool:
2284
+ """Website/code build command -> Website Studio (alag HTML/CSS/JS files + auto-review & fix).
2285
+ force=True: user ne explicitly builder khola hai (keyword-gate skip). True = build shuru hua."""
2286
+ if not force and not _is_website_command(text):
2287
+ return False
2288
+ if self._p_studio.is_working(): # ✅ dedup: pehle se chal raha hai
2289
+ return False
2290
+ self._myra_coder_running = True
2291
+ self._chat.add_webforge_badge(f"\u26a1 MYRA Coder: {text[:50]}...")
2292
+ self._topbar.set_status("webforge", "MYRA CODER ⬡")
2293
+ self._set_orb_state("webforge")
2294
+ self._sidebar.update_webforge_status(True, "Coding...")
2295
+ self._show_page("studio")
2296
+ return self._p_studio.start_build(text)
2297
+
2298
+ def _on_studio_done(self, project_dir: str, summary: str):
2299
+ triggered = getattr(self, "_myra_coder_running", False)
2300
+ self._myra_coder_running = False
2301
+ self._set_orb_state("idle"); self._topbar.set_status("standby", "STANDBY")
2302
+ self._sidebar.update_webforge_status(False)
2303
+ if not triggered:
2304
+ return # user ne Studio me khud type kiya — voice/deploy nahi
2305
+ name = Path(project_dir).name
2306
+ self._chat.add_webforge_badge(f"\u2705 Website ready: {name}", success=True)
2307
+ if hasattr(self, "_worker"):
2308
+ self._worker.send_text(
2309
+ "MYRA Coder ne website successfully bana di (alag HTML/CSS/JS files, review aur fix ke saath). "
2310
+ "User ko warmly congratulate karo aur poocho aur kya badalna hai!")
2311
+ self._deploy_dir(Path(project_dir), name, auto=True)
2312
+
2313
+ def _on_studio_error(self, msg: str):
2314
+ self._myra_coder_running = False
2315
+ self._set_orb_state("idle"); self._topbar.set_status("standby", "STANDBY")
2316
+ self._sidebar.update_webforge_status(False)
2317
+ self._chat.add_error(f"MYRA Coder Error: {msg[:100]}")
2318
+
2319
+ def _deploy_dir(self, project_dir: Path, repo_name: str, auto: bool = False):
2320
+ """Project folder ko WebForge ke GitHub + Vercel se deploy karo (tokens ho to)."""
2321
+ if not (_HAS_WEBFORGE and _webforge):
2322
+ if not auto: self._statusbar.flash("✗ WebForge available nahi — deploy nahi ho sakta")
2323
+ return
2324
+ if not auto: self._statusbar.flash("⬆ Deploy shuru…")
2325
+
2326
+ def _deploy():
2327
+ try:
2328
+ gh = _webforge._github.push_project(repo_name, project_dir)
2329
+ if not gh.get("ok"):
2330
+ msg = f"GitHub push skipped: {gh.get('error')}"
2331
+ print(f"[Studio->WebForge] {msg}")
2332
+ if not auto:
2333
+ from PyQt5.QtWidgets import QApplication
2334
+ QApplication.instance().postEvent(self, _WebForgeEvent(msg=msg, success=False))
2335
+ return
2336
+ vc = _webforge._vercel.deploy_from_github(repo_name, repo_name)
2337
+ live_url = vc.get("live_url", "")
2338
+ if vc.get("ok") and live_url:
2339
+ msg = f"Live deploy ho gaya: {live_url} | GitHub: {gh.get('github_url','')}"
2340
+ else:
2341
+ msg = f"GitHub pe push ho gaya: {gh.get('github_url','')} (Vercel deploy skip: {vc.get('error','')})"
2342
+ from PyQt5.QtWidgets import QApplication
2343
+ QApplication.instance().postEvent(self, _WebForgeEvent(msg=msg, success=True))
2344
+ except Exception as e:
2345
+ print(f"[Studio->WebForge] deploy error: {e}")
2346
+
2347
+ threading.Thread(target=_deploy, daemon=True, name="Studio-Deploy").start()
2348
+
2349
+ def _on_myra_coder_done(self, summary: str):
2350
+ """MYRA Coder ne kaam finish kiya."""
2351
+ self._myra_coder_running = False # ✅ reset guard
2352
+ self._set_orb_state("idle")
2353
+ self._topbar.set_status("standby", "STANDBY")
2354
+ self._sidebar.update_webforge_status(False)
2355
+ self._chat.add_webforge_badge(f"\u2705 {summary[:120]}", success=True)
2356
+ if hasattr(self, "_worker"):
2357
+ self._worker.send_text(
2358
+ "MYRA Coder ne website successfully bana diya! "
2359
+ "Live files Projects/ folder mein save ho gayi hain. "
2360
+ "User ko warmly congratulate karo! Poocho aur kya chahiye?"
2361
+ )
2362
+ # LINK: MYRA Coder banata hai, WebForge deploy karta hai — GitHub +
2363
+ # Vercel tokens hain to output ko automatically live karo, taaki dono
2364
+ # engine ek pipeline ki tarah kaam karein instead of do alag silos.
2365
+ self._auto_deploy_myra_coder_output()
2366
+
2367
+ def _auto_deploy_myra_coder_output(self):
2368
+ """MYRA Coder ke fresh output ko WebForge ke GitHub+Vercel se deploy karo."""
2369
+ if not (_HAS_WEBFORGE and _webforge):
2370
+ return
2371
+ output_dir = getattr(self, "_myra_coder_output_dir", None)
2372
+ if not output_dir or not os.path.isdir(output_dir):
2373
+ return
2374
+
2375
+ pre = getattr(self, "_myra_coder_pre_dirs", set())
2376
+ try:
2377
+ now_dirs = [d for d in os.listdir(output_dir)
2378
+ if os.path.isdir(os.path.join(output_dir, d))]
2379
+ except Exception:
2380
+ return
2381
+ new_dirs = [d for d in now_dirs if d not in pre]
2382
+
2383
+ if new_dirs:
2384
+ new_dirs.sort(
2385
+ key=lambda d: os.path.getmtime(os.path.join(output_dir, d)),
2386
+ reverse=True
2387
+ )
2388
+ project_dir = Path(output_dir) / new_dirs[0]
2389
+ repo_name = new_dirs[0]
2390
+ else:
2391
+ # MYRA Coder ne subfolder nahi banaya - seedha output_dir mein likha
2392
+ project_dir = Path(output_dir)
2393
+ repo_name = "myra-coder-site"
2394
+
2395
+ def _deploy():
2396
+ try:
2397
+ gh = _webforge._github.push_project(repo_name, project_dir)
2398
+ if not gh.get("ok"):
2399
+ print(f"[MYRA Coder->WebForge] GitHub push skipped: {gh.get('error')}")
2400
+ return
2401
+ vc = _webforge._vercel.deploy_from_github(repo_name, repo_name)
2402
+ live_url = vc.get("live_url", "")
2403
+ if vc.get("ok") and live_url:
2404
+ msg = f"Live deploy ho gaya: {live_url} | GitHub: {gh.get('github_url','')}"
2405
+ else:
2406
+ msg = f"GitHub pe push ho gaya: {gh.get('github_url','')} (Vercel deploy skip: {vc.get('error','')})"
2407
+ from PyQt5.QtWidgets import QApplication
2408
+ QApplication.instance().postEvent(self, _WebForgeEvent(msg=msg, success=True))
2409
+ except Exception as e:
2410
+ print(f"[MYRA Coder->WebForge] auto-deploy error: {e}")
2411
+
2412
+ threading.Thread(target=_deploy, daemon=True, name="MyraCoder-Deploy").start()
2413
+
2414
+ def _on_myra_coder_error(self, msg: str):
2415
+ """MYRA Coder mein error aaya."""
2416
+ self._myra_coder_running = False # ✅ reset guard
2417
+ self._set_orb_state("idle")
2418
+ self._topbar.set_status("standby", "STANDBY")
2419
+ self._sidebar.update_webforge_status(False)
2420
+ self._myra_coder_overlay.hide_panel()
2421
+ self._chat.add_error(f"MYRA Coder Error: {msg[:100]}")
2422
+
2423
+ # ══════════════════════════════════════════════════════════════════
2424
+ # LEFT SIDEBAR TOOL CLICKS — rows had no click handler at all before,
2425
+ # so none of these buttons did anything when clicked.
2426
+ # ══════════════════════════════════════════════════════════════════
2427
+ def _on_sidebar_tool(self, key: str):
2428
+ handlers = {
2429
+ "myra_coder": self._open_myra_coder_prompt,
2430
+ "webforge": self._open_webforge_prompt,
2431
+ "task_manager": self._open_task_manager,
2432
+ "settings": self._on_api_change,
2433
+ }
2434
+ handler = handlers.get(key)
2435
+ if handler:
2436
+ handler()
2437
+ return
2438
+ # Not built yet — say so instead of doing nothing.
2439
+ names = {
2440
+ "image_generator": "Image Generator",
2441
+ "text_generator": "Text Generator",
2442
+ "translator": "Translator",
2443
+ "data_analysis": "Data Analysis",
2444
+ }
2445
+ QMessageBox.information(
2446
+ self, "Coming Soon",
2447
+ f"{names.get(key, key)} abhi build nahi hua hai — jald aa raha hai!"
2448
+ )
2449
+
2450
+ def _open_myra_coder_prompt(self):
2451
+ """MYRA Coder / 'Apps & Tools' button -> Website Studio page."""
2452
+ self._show_page("studio")
2453
+ try: self._p_studio.input.setFocus()
2454
+ except Exception: pass
2455
+
2456
+ def _open_webforge_prompt(self):
2457
+ if not (_HAS_WEBFORGE and _webforge):
2458
+ QMessageBox.warning(self, "WebForge AI", "WebForge is not available in this build.")
2459
+ return
2460
+ if getattr(self, '_myra_coder_running', False):
2461
+ QMessageBox.information(self, "WebForge AI", "MYRA Coder abhi kaam kar raha hai — thoda ruko.")
2462
+ return
2463
+ text, ok = QInputDialog.getText(
2464
+ self, "WebForge AI",
2465
+ "Command batao (e.g. \"website ka status batao\", \"redeploy karo\", \"preview dikhao\")"
2466
+ )
2467
+ text = (text or "").strip()
2468
+ if not (ok and text):
2469
+ return
2470
+
2471
+ self._chat.add_webforge_badge(f"WebForge command: {text[:45]}...")
2472
+ self._topbar.set_status("webforge", "WEBFORGE ⬡")
2473
+ self._set_orb_state("webforge")
2474
+ self._sidebar.update_webforge_status(True, "Working...")
2475
+
2476
+ def _run():
2477
+ result = _webforge.handle_command(text, parent_widget=self)
2478
+ QApplication.instance().postEvent(
2479
+ self,
2480
+ _WebForgeEvent(
2481
+ msg=self._format_webforge_result(result),
2482
+ success=result.get("success", False)
2483
+ )
2484
+ )
2485
+
2486
+ threading.Thread(target=_run, daemon=True, name="WebForge-Manual").start()
2487
+
2488
+ def _open_task_manager(self):
2489
+ dlg = TaskManagerDialog(self)
2490
+ dlg.exec_()
2491
+
2492
+ def _on_user_msg_webforge(self, text: str):
2493
+ """
2494
+ Har user message check karo.
2495
+ Agar WebForge command hai → background thread mein handle karo.
2496
+ Warna MYRA normal response degi.
2497
+ """
2498
+ if not _HAS_WEBFORGE or not _webforge:
2499
+ return
2500
+ # ✅ FIX: MYRA Coder + WebForge dono "website banao" jaisi commands pe
2501
+ # apne-apne alag keyword-list se PARALLEL fire ho jaate the — do
2502
+ # builders ek hi request pe ek saath chalte the, Projects/ folder
2503
+ # mein conflict/double-write karte the aur do alag status badges
2504
+ # dikhate the. Ab MYRA Coder primary builder hai (real Gemini tool +
2505
+ # text trigger); WebForge yahan sirf tab chalta hai jab MYRA Coder
2506
+ # available hi na ho, ya command actually EXISTING project ke baare
2507
+ # mein ho (status/preview/redeploy) — fresh "banao" request nahi.
2508
+ if _HAS_MYRA_CODER and _is_website_command(text):
2509
+ return
2510
+ if getattr(self, '_myra_coder_running', False):
2511
+ return
2512
+ if not _webforge.is_webforge_command(text):
2513
+ return
2514
+
2515
+ # WebForge command detected! UI update karo
2516
+ self._chat.add_webforge_badge(f"WebForge command: {text[:45]}...")
2517
+ self._topbar.set_status("webforge", "WEBFORGE ⬡")
2518
+ self._set_orb_state("webforge")
2519
+ self._sidebar.update_webforge_status(True, "Building...")
2520
+
2521
+ # Background thread mein run karo — UI block nahi hogi
2522
+ def _run():
2523
+ result = _webforge.handle_command(text, parent_widget=self)
2524
+ # Thread-safe UI update via custom event
2525
+ from PyQt5.QtWidgets import QApplication
2526
+ QApplication.instance().postEvent(
2527
+ self,
2528
+ _WebForgeEvent(
2529
+ msg = self._format_webforge_result(result),
2530
+ success = result.get("success", False)
2531
+ )
2532
+ )
2533
+
2534
+ threading.Thread(target=_run, daemon=True, name="WebForge-Build").start()
2535
+
2536
+ def customEvent(self, event):
2537
+ """Thread-safe WebForge result handler"""
2538
+ if isinstance(event, _WebForgeEvent):
2539
+ # Chat badge
2540
+ self._chat.add_webforge_badge(event.msg, event.success)
2541
+ # Orb + status reset
2542
+ self._set_orb_state("idle")
2543
+ self._topbar.set_status("standby", "STANDBY")
2544
+ self._sidebar.update_webforge_status(False)
2545
+ # MYRA ko bhi bolne do
2546
+ if hasattr(self, '_worker'):
2547
+ if event.success:
2548
+ self._worker.send_text(
2549
+ f"Website successfully banaya aur deploy kar diya! "
2550
+ f"Live URL ready hai. QR code bhi show ho gaya screen pe. "
2551
+ f"User ko warmly congratulate karo aur poocho koi aur kaam chahiye?"
2552
+ )
2553
+ else:
2554
+ self._worker.send_text(
2555
+ f"WebForge mein thodi problem aayi. "
2556
+ f"User ko batao kya hua aur kaise fix kare."
2557
+ )
2558
+ elif isinstance(event, _QuickEvent):
2559
+ self._chat.add_sys(event.msg)
2560
+ if event.speak and hasattr(self, '_worker'):
2561
+ self._worker.send_text(
2562
+ "Ye result user ko short mein bolkar suna do (kuch aur add mat karo): " + event.msg
2563
+ )
2564
+ else:
2565
+ super().customEvent(event)
2566
+
2567
+ def _on_user_msg_quick(self, text: str):
2568
+ """Quick features (calculator, timer, notes, todo, ...) — background thread mein."""
2569
+ if not _HAS_QUICK or not text or _is_website_command(text):
2570
+ return
2571
+
2572
+ def _post(msg, speak=True):
2573
+ from PyQt5.QtWidgets import QApplication
2574
+ QApplication.instance().postEvent(self, _QuickEvent(msg, speak))
2575
+
2576
+ def _run():
2577
+ try:
2578
+ res = _handle_quick(text, notify=lambda m: _post(m, True))
2579
+ except Exception as e:
2580
+ print(f"[QUICK] {e}")
2581
+ return
2582
+ if res:
2583
+ _post(res, True)
2584
+
2585
+ threading.Thread(target=_run, daemon=True, name="Quick-Feature").start()
2586
+
2587
+ @staticmethod
2588
+ def _format_webforge_result(result: dict) -> str:
2589
+ if result.get("success"):
2590
+ lines = [f"✅ {result.get('message', 'Done!')}"]
2591
+ if result.get("live_url"): lines.append(f"🌐 Live: {result['live_url']}")
2592
+ if result.get("github_url"): lines.append(f"🐙 GitHub: {result['github_url']}")
2593
+ if result.get("preview_url"): lines.append(f"👁 Preview: {result['preview_url']}")
2594
+ return "\n".join(lines)
2595
+ return f"❌ {result.get('message', 'WebForge error')}"
2596
+
2597
+ # ── STATUS / SIGNAL HANDLERS ─────────────────────────────────
2598
+ def _on_status(self, state, label):
2599
+ self._voice_state = state if state != "screenshare" else "standby"
2600
+ self._update_edge_glow()
2601
+ if self._ss_mgr.is_active and state == "standby":
2602
+ self._topbar.set_status("screenshare", "SCREEN SHARE ▣")
2603
+ self._set_orb_state("screenshare")
2604
+ return
2605
+ self._topbar.set_status(state, label)
2606
+ m = {"standby":"idle","listening":"listening","thinking":"thinking",
2607
+ "speaking":"speaking","error":"error","screenshare":"screenshare"}
2608
+ self._set_orb_state(m.get(state, "idle"))
2609
+
2610
+ def _on_energy(self, e):
2611
+ if self._mic_on:
2612
+ self._orb.set_energy(e); self._p_voice.orb.set_energy(e); self._p_voice.level.set_level(e); self._statusbar.set_energy(e)
2613
+ self._sidebar.update_mic_level(e)
2614
+ if getattr(self, '_float_orb', None) and self._float_orb.isVisible():
2615
+ self._float_orb.set_energy(e)
2616
+
2617
+ def _on_user_live(self, t):
2618
+ self._chat.update_live_user(t); self._live_user_txt = t
2619
+ self._p_voice.set_live(t, getattr(self, '_live_aria_txt', ''))
2620
+ def _on_aria_live(self, t):
2621
+ self._chat.update_live_aria(t); self._live_aria_txt = t
2622
+ self._p_voice.set_live(getattr(self, '_live_user_txt', ''), t)
2623
+ def _on_user_msg(self, t): self._chat.finalize_user(t)
2624
+ def _on_aria_msg(self, t):
2625
+ self._chat.finalize_aria(t)
2626
+ if getattr(self, '_orb_mode', False) and getattr(self, '_float_orb', None):
2627
+ self._float_orb.say(t, 'info', 9000) # orb mode: jawab orb ke paas bhi dikhe
2628
+ def _on_sys_msg(self, t): self._chat.add_sys(t)
2629
+ def _on_error_msg(self, t): self._chat.add_error(t)
2630
+ def _on_warn_msg(self, t): self._chat.add_warning(t)
2631
+ def _on_tool_call(self, t):
2632
+ self._chat.add_sys(t); self._recent.add(str(t).strip()[:34])
2633
+ low = str(t).lower()
2634
+ if any(k in low for k in ("read_screen", "read_active_window", "analyze_screen")):
2635
+ self._screenread_until = time.time() + 4.5
2636
+ self._update_edge_glow()
2637
+ QTimer.singleShot(4700, self._update_edge_glow)
2638
+ def _on_tool_result(self, t): self._chat.add_sys(t)
2639
+
2640
+ def _on_connected(self, ok):
2641
+ if not ok:
2642
+ self._chat.add_warning("Connection lost — reconnecting...")
2643
+
2644
+ # ── BUTTON HANDLERS ──────────────────────────────────────────
2645
+ def _on_mic_select(self):
2646
+ dlg = MicSelectDialog(self, _MIC_DEVICE, _SPK_DEVICE, SILENCE_RMS, MIC_GAIN)
2647
+ dlg.sig_devices_set.connect(self._apply_devices)
2648
+ dlg.show()
2649
+
2650
+ def _on_model_switch(self, key: str):
2651
+ global _LIVE_MODEL_KEY
2652
+ if key not in LIVE_MODELS or key == _LIVE_MODEL_KEY:
2653
+ return
2654
+ _LIVE_MODEL_KEY = key
2655
+ _save_live_model_key(_LIVE_MODEL_KEY)
2656
+ self._topbar.set_model_label(_LIVE_MODEL_KEY)
2657
+ self._chat.add_sys(f"⚡ Voice model → {LIVE_MODELS[key][0]} ({LIVE_MODELS[key][2]}) — reconnecting...")
2658
+ self._worker.stop()
2659
+ QTimer.singleShot(800, self._start_worker)
2660
+
2661
+ def _on_api_change(self):
2662
+ dlg = ApiKeySetupDialog(self, current_api_key=self._api_key)
2663
+ if dlg.exec_() == QDialog.Accepted:
2664
+ new_api_key = dlg.get_api_key()
2665
+ if new_api_key:
2666
+ _save_config(new_api_key)
2667
+ global _API_KEY
2668
+ _API_KEY = new_api_key
2669
+ self._api_key = new_api_key
2670
+ self._chat.add_sys("🔑 API key updated → reconnecting...")
2671
+ self._worker.stop()
2672
+ QTimer.singleShot(1000, self._restart_worker)
2673
+ else:
2674
+ self._chat.add_error("❌ API key cannot be empty!")
2675
+
2676
+ def _apply_devices(self, mic_idx, spk_idx):
2677
+ global _MIC_DEVICE, _SPK_DEVICE
2678
+ _MIC_DEVICE = mic_idx; _SPK_DEVICE = spk_idx
2679
+ self._chat.add_sys("🎙 Mic changed → reconnecting...")
2680
+ self._worker.stop()
2681
+ QTimer.singleShot(600, self._restart_worker)
2682
+
2683
+ def _restart_worker(self):
2684
+ self._start_worker()
2685
+ self._chat.add_sys("✅ New mic/speaker active!")
2686
+
2687
+ def _on_mute(self):
2688
+ self._mic_on = False; self._worker.set_mic(False); self._input.set_mic_on(False)
2689
+ self._set_orb_state("idle"); self._topbar.set_status("standby","MUTED 🔇")
2690
+ self._chat.add_sys("🔇 Mic muted")
2691
+
2692
+ def _on_speak(self):
2693
+ self._mic_on = True; self._worker.set_mic(True); self._input.set_mic_on(True)
2694
+ self._topbar.set_status("listening","LISTENING")
2695
+ self._chat.add_sys("🎤 Mic active")
2696
+
2697
+ def _on_camera(self):
2698
+ # ✅ FIX: yeh button pehle sirf ek decorative sidebar preview toggle
2699
+ # karta tha jo Gemini ko kuch bhejta hi nahi tha ("vision mode kaam
2700
+ # nahi kar raha" ka ek bada karan). Ab yeh seedha CameraVisionManager
2701
+ # (asli AI vision engine, Vision tab wala) ko control karta hai —
2702
+ # button, sidebar dot aur Vision tab panel teeno automatically sync
2703
+ # rehte hain (_on_cv_started/_on_cv_stopped/_on_cv_error ke through).
2704
+ if not _HAS_CAM_VISION:
2705
+ self._chat.add_error("Camera Vision module missing — pip install opencv-python")
2706
+ return
2707
+ self._cv_mgr.toggle()
2708
+ if self._cv_mgr.is_active:
2709
+ self._show_page("vision") # Vision page pe le jao taaki live preview dikhe
2710
+
2711
+ def _on_voice(self):
2712
+ global _CURRENT_VOICE
2713
+ _CURRENT_VOICE = "male" if _CURRENT_VOICE == "female" else "female"
2714
+ self._ctrl.voice_btn.setText(f"Voice: {_CURRENT_VOICE.title()}")
2715
+ self._chat.add_sys(f"Voice → {_CURRENT_VOICE.title()} — reconnecting...")
2716
+ self._worker.stop()
2717
+ QTimer.singleShot(1000, self._restart_worker)
2718
+
2719
+ def _on_mode(self):
2720
+ self._mode = "normal" if self._mode == "MYRA" else "MYRA"
2721
+ p = aria_PROMPT if self._mode == "MYRA" else NORMAL_PROMPT
2722
+ self._topbar.set_mode_label("Mode: MYRA 🌸" if self._mode == "MYRA" else "Mode: Normal 👔")
2723
+ self._worker.reset_greet()
2724
+ self._worker.set_prompt(p); self._worker.reconnect()
2725
+ self._chat.add_sys(f"🔄 Mode: {'MYRA 🌸' if self._mode=='MYRA' else 'Normal 👔'}")
2726
+
2727
+ # ── WORKER INIT ──────────────────────────────────────────────
2728
+ def _start_worker(self):
2729
+ try: # installed apps ki list background mein taiyaar (open_app turant chale)
2730
+ import myra_apps
2731
+ myra_apps.warm()
2732
+ except Exception as e:
2733
+ print(f"[MYRA] app index warm: {e}")
2734
+ init_advanced_features(api_key=_API_KEY)
2735
+ try: init_advanced(api_key=_API_KEY, suggestion_cb=lambda s: None)
2736
+ except: pass
2737
+
2738
+ self._worker = GeminiWorker()
2739
+ self._worker.sig_status.connect(self._on_status)
2740
+ self._worker.sig_user_live.connect(self._on_user_live)
2741
+ self._worker.sig_aria_live.connect(self._on_aria_live)
2742
+ self._worker.sig_user_msg.connect(self._on_user_msg)
2743
+ self._worker.sig_aria_msg.connect(self._on_aria_msg)
2744
+ self._worker.sig_sys_msg.connect(self._on_sys_msg)
2745
+ self._worker.sig_error_msg.connect(self._on_error_msg)
2746
+ self._worker.sig_warn_msg.connect(self._on_warn_msg)
2747
+ self._worker.sig_tool_call.connect(self._on_tool_call)
2748
+ self._worker.sig_tool_result.connect(self._on_tool_result)
2749
+ self._worker.sig_connected.connect(self._on_connected)
2750
+ self._worker.sig_energy.connect(self._on_energy)
2751
+ self._worker.sig_mic_state.connect(self._on_mic_state)
2752
+ self._worker.sig_model_fallback.connect(lambda k: (self._topbar.set_model_label(k), self._publish_state()))
2753
+ self._worker.set_listen_mode(self._listen_mode)
2754
+
2755
+ # ✅ WebForge intercept — user message bhi sunenge
2756
+ self._worker.sig_user_msg.connect(self._on_user_msg_webforge)
2757
+
2758
+ # ✅ Quick Features intercept — calculator/timer/notes/todo/etc.
2759
+ self._worker.sig_user_msg.connect(self._on_user_msg_quick)
2760
+ if _HAS_QUICK:
2761
+ def _quick_alarm_post(msg):
2762
+ from PyQt5.QtWidgets import QApplication
2763
+ QApplication.instance().postEvent(self, _QuickEvent(msg, True))
2764
+ _quick_start(notify=_quick_alarm_post) # alarms roz/one-time chalenge
2765
+
2766
+ # ✅ MYRA Coder intercept — EARLY (live streaming) + LATE (turnComplete)
2767
+ # Early: jaise hi user bolta hai, detect karo (Gemini tool call se PEHLE)
2768
+ self._worker.sig_user_live.connect(self._on_user_live_website)
2769
+ # Late: fallback agar live ne miss kiya
2770
+ self._worker.sig_user_msg.connect(self._on_user_msg_website)
2771
+ # ✅ PRIMARY: real Gemini tool call (build_website) — most reliable
2772
+ # trigger, doesn't depend on keyword-matching the transcript at all.
2773
+ # _trigger_myra_coder's own dedup guard means this is safe to also
2774
+ # fire alongside the text-based triggers above without double-running.
2775
+ self._worker.sig_build_website.connect(lambda d: self._trigger_myra_coder(d, True))
2776
+
2777
+ self._worker.start_thread()
2778
+
2779
+ if _HAS_OFFICE:
2780
+ def _start_loc():
2781
+ loop = asyncio.new_event_loop()
2782
+ asyncio.set_event_loop(loop)
2783
+ loop.run_until_complete(watch_location_realtime(3))
2784
+ threading.Thread(target=_start_loc, daemon=True).start()
2785
+
2786
+ # ── RESIZE ───────────────────────────────────────────────────
2787
+ def resizeEvent(self, e):
2788
+ super().resizeEvent(e)
2789
+ if hasattr(self, '_myra_coder_overlay') and self._myra_coder_overlay:
2790
+ if self._myra_coder_overlay.isVisible():
2791
+ self._myra_coder_overlay._update_geometry()
2792
+
2793
+ # ── CLOSE ────────────────────────────────────────────────────
2794
+ def closeEvent(self, e):
2795
+ if _HAS_CAM_VISION and hasattr(self, '_cv_mgr'):
2796
+ self._cv_mgr.shutdown()
2797
+ if _HAS_SCREEN_SHARE:
2798
+ self._ss_mgr.shutdown()
2799
+ if getattr(self, '_edge', None):
2800
+ self._edge.set_enabled(False)
2801
+ self._edge._hide_all()
2802
+ if getattr(self, '_guardian', None):
2803
+ self._guardian.stop()
2804
+ if getattr(self, '_float_orb', None):
2805
+ self._float_orb.hide_orb()
2806
+ if getattr(self, '_tray', None):
2807
+ self._tray.hide()
2808
+ self._worker.stop()
2809
+ if _HAS_ADV:
2810
+ ARIAMemory.save()
2811
+ super().closeEvent(e)
2812
+
2813
+
2814
+ # Alias
2815
+ TITANWindow = ARIAWindow
2816
+
2817
+
2818
+ # ══════════════════════════════════════════════════════════════════
2819
+ # ENTRY POINT
2820
+ # ══════════════════════════════════════════════════════════════════
2821
+ if __name__ == "__main__":
2822
+ if "--list-devices" in sys.argv:
2823
+ list_audio_devices(); sys.exit(0)
2824
+
2825
+ app = QApplication(sys.argv)
2826
+ app.setStyle("Fusion")
2827
+ pal = QPalette()
2828
+ pal.setColor(QPalette.Window, QColor("#040810"))
2829
+ pal.setColor(QPalette.WindowText, QColor("#CCDDEEFF"))
2830
+ pal.setColor(QPalette.Base, QColor("#070E1C"))
2831
+ pal.setColor(QPalette.Text, QColor("#CCDDEEFF"))
2832
+ pal.setColor(QPalette.Button, QColor("#0A1428"))
2833
+ pal.setColor(QPalette.ButtonText, QColor("#CCDDEEFF"))
2834
+ pal.setColor(QPalette.Highlight, QColor("#1144AA"))
2835
+ pal.setColor(QPalette.HighlightedText, QColor("#FFFFFF"))
2836
+ app.setPalette(pal)
2837
+
2838
+ _si = None
2839
+ if _HAS_ORB:
2840
+ if _autostart.handle_cli_flags():
2841
+ sys.exit(0)
2842
+ _si = _autostart.make_single_instance()
2843
+ if not _si.acquire():
2844
+ print("[MYRA] Pehle se chal rahi hai — us window ko kholne ko bola.")
2845
+ sys.exit(0)
2846
+ _bg = bool(_HAS_ORB and _autostart.is_background_launch())
2847
+ _splash = None
2848
+ if not _bg and not os.environ.get("MYRA_NO_SPLASH") and _prefs.get("splash", True):
2849
+ try:
2850
+ load_fonts()
2851
+ from myra_splash import MyraSplash
2852
+ _splash = MyraSplash(); _splash.show(); _splash.set_progress(40); app.processEvents()
2853
+ except Exception as _e:
2854
+ print(f"[MYRA] splash: {_e}"); _splash = None
2855
+ if _splash:
2856
+ _startup_hook = lambda p: _splash.jump(p)
2857
+ win = ARIAWindow()
2858
+ _startup_hook = None
2859
+ if _si:
2860
+ _si.sig_show.connect(win._show_main)
2861
+ if _bg:
2862
+ win.start_in_background()
2863
+ else:
2864
+ def _reveal(): # splash 100% hone ke BAAD hi main window
2865
+ win.show(); win.raise_(); win.activateWindow()
2866
+ QTimer.singleShot(3500, win.first_run_setup)
2867
+ QTimer.singleShot(4500, win.ask_name_if_missing)
2868
+ if _splash:
2869
+ _splash.finish(on_full=_reveal)
2870
+ else:
2871
+ _reveal()
2872
+ sys.exit(app.exec_())