-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgui.py
More file actions
450 lines (376 loc) · 17.4 KB
/
Copy pathgui.py
File metadata and controls
450 lines (376 loc) · 17.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
import sys
import math
import time
from PyQt6.QtCore import (
Qt, QTimer, QPoint, QEasingCurve, QPropertyAnimation,
QPointF
)
from PyQt6.QtWidgets import (
QMainWindow, QWidget, QVBoxLayout, QLabel, QFrame,
QSystemTrayIcon, QMenu, QApplication, QPushButton,
QGraphicsOpacityEffect
)
from PyQt6.QtGui import (
QColor, QPalette, QFont, QPainter, QRadialGradient,
QPainterPath, QAction, QIcon, QBrush, QPixmap
)
class FloatingMessage(QLabel):
def __init__(self, text, parent=None):
super().__init__(parent)
self.current_text = text
self.setStyleSheet("""
QLabel {
background-color: rgba(0, 120, 212, 180);
border-radius: 10px;
padding: 10px 20px;
color: white;
font-size: 12pt;
}
""")
self.setWordWrap(True)
self.setMinimumWidth(280) # Set minimum width to prevent narrow messages
self.setMaximumWidth(300) # Set maximum width for better readability
self.update_text(text)
# Create timer for auto-hide
self.timer = QTimer(self)
self.timer.setSingleShot(True)
self.timer.timeout.connect(self.fade_out)
# Animation properties
self.opacity = QGraphicsOpacityEffect(self)
self.opacity.setOpacity(1.0)
self.setGraphicsEffect(self.opacity)
# Create fade animation
self.fade_animation = QPropertyAnimation(self.opacity, b"opacity", self)
self.fade_animation.setDuration(500) # 500ms fade duration
self.fade_animation.finished.connect(self.on_fade_finished)
def update_text(self, text):
"""Update the message text"""
self.current_text = text
self.setText(text)
self.adjustSize()
# Update position to maintain centering
if self.parent():
message_x = (self.parent().width() - self.sizeHint().width()) // 2
message_y = self.parent().message_area.y() + (self.parent().message_area.height() -
self.sizeHint().height()) // 2
self.move(message_x, message_y)
def fade_out(self):
"""Start the fade out animation"""
self.fade_animation.setStartValue(1.0)
self.fade_animation.setEndValue(0.0)
self.fade_animation.start()
def on_fade_finished(self):
"""Called when fade animation is complete"""
if self.opacity.opacity() == 0:
self.hide()
self.deleteLater()
def showEvent(self, event):
"""Override show event to handle fade in"""
super().showEvent(event)
self.opacity.setOpacity(1.0)
class OrbWidget(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.animation_timer = QTimer()
self.animation_timer.timeout.connect(self.update_animation)
self.animation_state = "idle"
self.current_frame = 0
self.animation_speed = 50 # milliseconds
self.max_frames = 60
# Animation parameters
self.base_radius = 20
self.wave_amplitude = 5
self.wave_frequency = 2
self.color = QColor(0, 120, 212) # Windows blue
# Enable mouse tracking and set cursor
self.setMouseTracking(True)
self.setCursor(Qt.CursorShape.PointingHandCursor)
# Start the animation timer
self.animation_timer.start(self.animation_speed)
def start_animation(self, state):
"""Start animation with specified state"""
self.animation_state = state
if not self.animation_timer.isActive():
self.animation_timer.start(self.animation_speed)
self.update()
def update_animation(self):
"""Update animation frame"""
self.current_frame = (self.current_frame + 1) % self.max_frames
self.repaint() # Force immediate repaint
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
# Calculate center point
center = QPointF(self.width() / 2, self.height() / 2)
if self.animation_state == "idle":
# Subtle breathing effect for idle state
scale = 1.0 + 0.05 * math.sin(self.current_frame * 0.1)
gradient = QRadialGradient(center, self.base_radius * 1.5)
gradient.setColorAt(0, self.color)
gradient.setColorAt(0.7, self.color.lighter(120))
gradient.setColorAt(1, QColor(0, 0, 0, 0))
painter.setBrush(gradient)
painter.setPen(Qt.PenStyle.NoPen)
painter.drawEllipse(center, self.base_radius * scale, self.base_radius * scale)
elif self.animation_state == "speaking":
# More pronounced pulsing effect for speaking
scale = 1.0 + 0.15 * math.sin(self.current_frame * 0.2)
gradient = QRadialGradient(center, self.base_radius * 2 * scale)
gradient.setColorAt(0, self.color)
gradient.setColorAt(0.6, self.color.lighter(130))
gradient.setColorAt(1, QColor(0, 0, 0, 0))
painter.setBrush(gradient)
painter.setPen(Qt.PenStyle.NoPen)
painter.drawEllipse(center, self.base_radius * scale, self.base_radius * scale)
elif self.animation_state in ["listening", "processing"]:
# Base orb with glow
gradient = QRadialGradient(center, self.base_radius * 1.5)
gradient.setColorAt(0, self.color)
gradient.setColorAt(0.7, self.color.lighter(120))
gradient.setColorAt(1, QColor(0, 0, 0, 0))
painter.setBrush(gradient)
painter.setPen(Qt.PenStyle.NoPen)
painter.drawEllipse(center, self.base_radius, self.base_radius)
# Animated ripple waves
for i in range(3):
wave_offset = (self.current_frame + i * self.max_frames // 3) % self.max_frames
wave_scale = 1.0 - wave_offset / self.max_frames
if wave_scale > 0:
wave_color = QColor(self.color)
wave_color.setAlpha(int(255 * wave_scale * 0.5))
wave_gradient = QRadialGradient(center, self.base_radius * (2 + wave_scale))
wave_gradient.setColorAt(0.4, wave_color)
wave_gradient.setColorAt(1, QColor(0, 0, 0, 0))
painter.setBrush(wave_gradient)
wave_radius = self.base_radius + (1.0 - wave_scale) * 30
painter.drawEllipse(center, wave_radius, wave_radius)
def mousePressEvent(self, event):
if event.button() == Qt.MouseButton.LeftButton:
# Find the parent VoiceAssistantGUI
parent = self.parent()
while parent and not isinstance(parent, VoiceAssistantGUI):
parent = parent.parent()
if parent:
parent.toggle_listening()
def enterEvent(self, event):
# Highlight effect when mouse enters
self.color = self.color.lighter(110)
super().enterEvent(event)
def leaveEvent(self, event):
# Return to normal color when mouse leaves
self.color = QColor(0, 120, 212)
super().leaveEvent(event)
class VoiceAssistantGUI(QMainWindow):
def __init__(self, assistant):
super().__init__()
self.assistant = assistant
self.is_listening = False
self.last_activity_time = time.time()
# Create system tray icon with a generated icon
self.tray_icon = QSystemTrayIcon(self)
# Generate a simple icon programmatically
pixmap = QPixmap(32, 32)
pixmap.fill(Qt.GlobalColor.transparent)
painter = QPainter(pixmap)
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
# Draw a gradient circle
gradient = QRadialGradient(16, 16, 14)
gradient.setColorAt(0, QColor(0, 120, 212)) # Windows blue
gradient.setColorAt(1, QColor(0, 100, 180))
painter.setBrush(gradient)
painter.setPen(Qt.PenStyle.NoPen)
painter.drawEllipse(2, 2, 28, 28)
painter.end()
# Set the generated icon
self.tray_icon.setIcon(QIcon(pixmap))
self.setWindowIcon(QIcon(pixmap))
self.current_message = None
self.initUI()
# Create auto-close timer
self.inactivity_timer = QTimer()
self.inactivity_timer.timeout.connect(self.check_inactivity)
self.inactivity_timer.start(1000) # Check every second
# Connect assistant signals
self.setup_signal_connections()
def setup_signal_connections(self):
"""Set up signal connections with the assistant"""
if hasattr(self.assistant, 'on_speech_detected'):
self.assistant.on_speech_detected.connect(self.on_user_speech)
if hasattr(self.assistant, 'on_response_ready'):
self.assistant.on_response_ready.connect(self.on_assistant_response)
if hasattr(self.assistant, 'on_interim_speech'):
self.assistant.on_interim_speech.connect(self.on_interim_speech)
if hasattr(self.assistant, 'on_assistant_word'):
self.assistant.on_assistant_word.connect(self.on_assistant_word)
def on_user_speech(self, text):
"""Handle detected user speech"""
# Clear any existing assistant message
if self.current_message:
self.current_message.fade_out()
self.current_message = None
# Show user's message
self.show_message(f"You: {text}", is_listening=True)
def on_assistant_response(self, response):
"""Handle assistant's response"""
# Clear user's message first
if self.current_message:
self.current_message.fade_out()
self.current_message = None
# Show assistant's response
self.show_message(f"Athena: {response}")
def on_assistant_word(self, word):
"""Handle word-by-word assistant response"""
# If there's a user message showing, clear it
if self.current_message and self.current_message.current_text.startswith("You:"):
self.current_message.fade_out()
self.current_message = None
# Start or continue assistant message
if not self.current_message:
self.current_message = FloatingMessage("Athena: " + word, self)
# Position message above the orb
message_x = (self.width() - self.current_message.sizeHint().width()) // 2
message_y = self.message_area.y() + (self.message_area.height() -
self.current_message.sizeHint().height()) // 2
self.current_message.move(message_x, message_y)
self.current_message.show()
else:
# Append new word to existing message
self.current_message.update_text(self.current_message.current_text + " " + word)
# Reset the timer with longer duration
self.current_message.timer.stop()
self.current_message.timer.setInterval(8000) # 8 seconds
self.current_message.timer.start()
def on_interim_speech(self, text):
"""Handle interim speech results"""
if self.current_message:
self.current_message.update_text(text)
def show_message(self, text, is_listening=False):
"""Show a floating message"""
# Remove existing message if any
if self.current_message:
self.current_message.fade_out()
# Create new message
self.current_message = FloatingMessage(text, self)
# Position message above the orb
message_x = (self.width() - self.current_message.sizeHint().width()) // 2
message_y = self.message_area.y() + (self.message_area.height() -
self.current_message.sizeHint().height()) // 2
self.current_message.move(message_x, message_y)
self.current_message.show()
# Start auto-hide timer if not in listening mode
if not is_listening:
self.current_message.timer.setInterval(8000) # 8 seconds
self.current_message.timer.start()
def start_response(self, text):
"""Start a new response message"""
if self.current_message:
self.current_message.fade_out()
self.current_message = None
self.current_message = FloatingMessage(text, self)
# Position message above the orb
message_x = (self.width() - self.current_message.sizeHint().width()) // 2
message_y = self.message_area.y() + (self.message_area.height() -
self.current_message.sizeHint().height()) // 2
self.current_message.move(message_x, message_y)
self.current_message.show()
# Start timer with longer duration for responses
self.current_message.timer.setInterval(8000) # 8 seconds for better readability
self.current_message.timer.start()
def on_assistant_speaking(self):
"""Called when the assistant starts speaking"""
self.orb_widget.start_animation("speaking")
def on_assistant_listening(self):
"""Called when the assistant starts listening"""
self.orb_widget.start_animation("listening")
def on_assistant_processing(self):
"""Called when the assistant is processing"""
self.orb_widget.start_animation("processing")
def on_assistant_idle(self):
"""Called when the assistant becomes idle"""
self.orb_widget.start_animation("idle")
def initUI(self):
self.setWindowTitle("Athena Voice Assistant")
self.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.WindowStaysOnTopHint)
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
# Main layout
layout = QVBoxLayout()
layout.setContentsMargins(20, 20, 20, 20)
central_widget = QWidget()
central_widget.setLayout(layout)
self.setCentralWidget(central_widget)
# Message area (invisible placeholder for proper spacing)
self.message_area = QWidget()
self.message_area.setMinimumHeight(80)
self.message_area.setStyleSheet("background-color: transparent;")
layout.addWidget(self.message_area)
# Create orb widget
self.orb_widget = OrbWidget()
self.orb_widget.setFixedSize(100, 100)
layout.addWidget(self.orb_widget, alignment=Qt.AlignmentFlag.AlignCenter)
# Set window size and position
self.setFixedSize(340, 300)
# Create system tray
self.create_system_tray()
# Move to bottom right
self.move_to_bottom_right()
def check_inactivity(self):
"""Check for inactivity and hide window if inactive for too long"""
if not self.is_listening and time.time() - self.last_activity_time > 10:
self.hide()
def mousePressEvent(self, event):
"""Handle mouse press events"""
if event.button() == Qt.MouseButton.LeftButton:
self.drag_position = event.globalPosition().toPoint() - self.frameGeometry().topLeft()
self.last_activity_time = time.time()
def mouseMoveEvent(self, event):
"""Handle mouse move events"""
if event.buttons() & Qt.MouseButton.LeftButton:
self.move(event.globalPosition().toPoint() - self.drag_position)
self.last_activity_time = time.time()
def enterEvent(self, event):
"""Handle mouse enter events"""
super().enterEvent(event)
self.last_activity_time = time.time()
def leaveEvent(self, event):
"""Handle mouse leave events"""
super().leaveEvent(event)
self.last_activity_time = time.time()
def create_system_tray(self):
# Create tray menu
tray_menu = QMenu()
show_action = tray_menu.addAction("Show")
show_action.triggered.connect(self.show_and_activate)
quit_action = tray_menu.addAction("Quit")
quit_action.triggered.connect(QApplication.instance().quit)
self.tray_icon.setContextMenu(tray_menu)
self.tray_icon.show()
def show_and_activate(self):
"""Show and activate the window"""
self.show()
self.activateWindow()
self.last_activity_time = time.time()
def move_to_bottom_right(self):
"""Position the window in the bottom right of the screen"""
screen = QApplication.primaryScreen().geometry()
self.move(screen.width() - self.width() - 20,
screen.height() - self.height() - 100)
def toggle_listening(self):
"""Toggle between listening and idle states"""
self.is_listening = not self.is_listening
if self.is_listening:
# Start voice recognition
self.assistant.start_listening()
else:
self.assistant.stop_listening()
self.orb_widget.start_animation("idle")
def closeEvent(self, event):
# Clean up any existing messages
if self.current_message:
self.current_message.fade_out()
self.current_message = None
super().closeEvent(event)
def launch_gui(assistant):
app = QApplication(sys.argv)
gui = VoiceAssistantGUI(assistant)
gui.show()
return app, gui