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.
- Aria_office_manage.py +1985 -0
- Main_normal.py +261 -0
- aria.py +2872 -0
- aria_modules/__init__.py +1 -0
- aria_modules/aria_activation.py +335 -0
- aria_modules/aria_advanced_features.py +1269 -0
- aria_modules/aria_advanced_tools.py +1099 -0
- aria_modules/assets/about/cofounder.png +0 -0
- aria_modules/assets/about/developer.png +0 -0
- aria_modules/assets/fonts/OFL-Poppins.txt +93 -0
- aria_modules/assets/fonts/Poppins-Bold.ttf +0 -0
- aria_modules/assets/fonts/Poppins-ExtraBold.ttf +0 -0
- aria_modules/assets/fonts/Poppins-Medium.ttf +0 -0
- aria_modules/assets/fonts/Poppins-Regular.ttf +0 -0
- aria_modules/assets/fonts/Poppins-SemiBold.ttf +0 -0
- aria_modules/assets/myra_girl.png +0 -0
- aria_modules/assets/myra_logo.png +0 -0
- aria_modules/assets/splash_bg.jpg +0 -0
- aria_modules/assets/ui_reference.png +0 -0
- aria_modules/cloud_database.py +10 -0
- aria_modules/game_install.py +1056 -0
- aria_modules/gemini_rest.py +102 -0
- aria_modules/haarcascade_frontalface_default.xml +33314 -0
- aria_modules/license_client.py +250 -0
- aria_modules/minibar_aria.py +355 -0
- aria_modules/tools.py +2295 -0
- aria_ui.py +818 -0
- camera_vision_module.py +1040 -0
- linux_extra.py +240 -0
- myra/__init__.py +10 -0
- myra/__main__.py +3 -0
- myra/cli.py +141 -0
- myra/config.py +22 -0
- myra_actions.py +327 -0
- myra_apps.py +846 -0
- myra_autostart.py +170 -0
- myra_coder_integration.py +641 -0
- myra_edge_glow.py +274 -0
- myra_float_orb.py +471 -0
- myra_guardian.py +181 -0
- myra_lang.py +133 -0
- myra_linux-0.1.0.dist-info/METADATA +99 -0
- myra_linux-0.1.0.dist-info/RECORD +80 -0
- myra_linux-0.1.0.dist-info/WHEEL +5 -0
- myra_linux-0.1.0.dist-info/entry_points.txt +3 -0
- myra_linux-0.1.0.dist-info/licenses/LICENSE +36 -0
- myra_linux-0.1.0.dist-info/top_level.txt +31 -0
- myra_linux_boot.py +207 -0
- myra_linux_tools.py +314 -0
- myra_pages.py +1246 -0
- myra_prefs.py +71 -0
- myra_quick_features.py +748 -0
- myra_slot.py +39 -0
- myra_splash.py +301 -0
- myra_spotify.py +316 -0
- myra_studio.py +916 -0
- myra_ui.py +1057 -0
- normal_prompt.py +41 -0
- prompt.py +252 -0
- screen_share_module.py +447 -0
- website_builder/__init__.py +5 -0
- website_builder/agents/__init__.py +1 -0
- website_builder/agents/content_agent.py +99 -0
- website_builder/agents/generator_agent.py +249 -0
- website_builder/agents/planner_agent.py +175 -0
- website_builder/config.py +48 -0
- website_builder/database/__init__.py +1 -0
- website_builder/database/projects_db.py +47 -0
- website_builder/deploy/__init__.py +1 -0
- website_builder/deploy/github_service.py +67 -0
- website_builder/deploy/qr_display.py +130 -0
- website_builder/deploy/vercel_service.py +71 -0
- website_builder/main.py +25 -0
- website_builder/preview/__init__.py +1 -0
- website_builder/preview/preview_server.py +47 -0
- website_builder/project/__init__.py +1 -0
- website_builder/project/project_manager.py +32 -0
- website_builder/webforge_integration.py +220 -0
- website_builder/webforge_manager.py +209 -0
- 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_())
|