fix stuff
This commit is contained in:
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
__pycache__
|
||||||
|
core/__pycache__
|
||||||
|
utils/__pycache__
|
||||||
|
audio/__pycache__
|
||||||
|
venv
|
||||||
0
audio/__init__.py
Normal file
0
audio/__init__.py
Normal file
8
audio/audio_queue.py
Normal file
8
audio/audio_queue.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import queue
|
||||||
|
|
||||||
|
q = queue.Queue()
|
||||||
|
|
||||||
|
def callback(indata, frames, time_, status):
|
||||||
|
if status:
|
||||||
|
print(status)
|
||||||
|
q.put(bytes(indata))
|
||||||
15
audio/chime.py
Normal file
15
audio/chime.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import time
|
||||||
|
|
||||||
|
import pygame
|
||||||
|
|
||||||
|
from config import CHIME_FILE
|
||||||
|
|
||||||
|
|
||||||
|
def play_ding():
|
||||||
|
pygame.mixer.init()
|
||||||
|
pygame.mixer.music.load(CHIME_FILE)
|
||||||
|
pygame.mixer.music.play()
|
||||||
|
while pygame.mixer.music.get_busy():
|
||||||
|
time.sleep(0.05)
|
||||||
|
pygame.mixer.music.unload()
|
||||||
|
pygame.mixer.quit()
|
||||||
20
audio/gtts.py
Normal file
20
audio/gtts.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
|
|
||||||
|
import pygame
|
||||||
|
from gtts import gTTS
|
||||||
|
|
||||||
|
|
||||||
|
def speak(text):
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdirname:
|
||||||
|
temp_path = os.path.join(tmpdirname, "speech.mp3")
|
||||||
|
tts = gTTS(text=text,lang='en', tld='com')
|
||||||
|
tts.save(temp_path)
|
||||||
|
pygame.mixer.init()
|
||||||
|
pygame.mixer.music.load(temp_path)
|
||||||
|
pygame.mixer.music.play()
|
||||||
|
while pygame.mixer.music.get_busy():
|
||||||
|
time.sleep(0.1)
|
||||||
|
pygame.mixer.music.unload()
|
||||||
|
pygame.mixer.quit()
|
||||||
33
audio/tts.py
Normal file
33
audio/tts.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
|
|
||||||
|
import edge_tts
|
||||||
|
import pygame
|
||||||
|
|
||||||
|
|
||||||
|
def speak(text):
|
||||||
|
async def generate_speech():
|
||||||
|
voice = "en-US-ChristopherNeural"
|
||||||
|
communicate = edge_tts.Communicate(text, voice)
|
||||||
|
await communicate.save(temp_path)
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdirname:
|
||||||
|
temp_path = os.path.join(tmpdirname, "speech.mp3")
|
||||||
|
|
||||||
|
asyncio.run(generate_speech())
|
||||||
|
|
||||||
|
pygame.mixer.init()
|
||||||
|
pygame.mixer.music.load(temp_path)
|
||||||
|
pygame.mixer.music.play()
|
||||||
|
|
||||||
|
while pygame.mixer.music.get_busy():
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
pygame.mixer.music.unload()
|
||||||
|
pygame.mixer.quit()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
speak("Hello! I am now speaking using Microsoft Edge's natural text to speech engine.")
|
||||||
0
bs/__init__.py
Normal file
0
bs/__init__.py
Normal file
BIN
bs/marshallhatesthis.mp3
Normal file
BIN
bs/marshallhatesthis.mp3
Normal file
Binary file not shown.
21
bs/verity.py
Normal file
21
bs/verity.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
|
import pygame
|
||||||
|
|
||||||
|
|
||||||
|
def ouiouioui():
|
||||||
|
temp_path = os.path.join(r"bs/marshallhatesthis.mp3")
|
||||||
|
pygame.mixer.init()
|
||||||
|
pygame.mixer.music.load(temp_path)
|
||||||
|
pygame.mixer.music.play()
|
||||||
|
|
||||||
|
while pygame.mixer.music.get_busy():
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
pygame.mixer.music.unload()
|
||||||
|
pygame.mixer.quit()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
ouiouioui()
|
||||||
14
config.py
Normal file
14
config.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# Model details
|
||||||
|
VOSK_MODEL_FOLDER = 'vosk-model-small-en-us-0.15'
|
||||||
|
VOSK_MODEL_URL = "https://alphacephei.com/vosk/models/vosk-model-small-en-us-0.15.zip"
|
||||||
|
VOSK_MODEL_ZIP = "voicemodel.zip"
|
||||||
|
|
||||||
|
# Sound files
|
||||||
|
CHIME_FILE = "chime.mp3"
|
||||||
|
|
||||||
|
# AI API
|
||||||
|
AI_URL = "http://localhost:11434/api/generate"
|
||||||
|
AI_MODEL = "deepseek-r1:1.5b"
|
||||||
|
|
||||||
|
WAKE_WORDS = ["hey risa", "hey lisa", "risa", "lisa", "Hey Nate", "Nate"]
|
||||||
|
TTS_MODEL = "tts_models/en/ljspeech/vits"
|
||||||
13
core/speech_recognition.py
Normal file
13
core/speech_recognition.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
from audio.audio_queue import q
|
||||||
|
|
||||||
|
|
||||||
|
def listen_for_command(rec):
|
||||||
|
while True:
|
||||||
|
data = q.get()
|
||||||
|
if rec.AcceptWaveform(data):
|
||||||
|
result_json = json.loads(rec.Result())
|
||||||
|
result = result_json.get("text", "").lower().strip()
|
||||||
|
if result:
|
||||||
|
return result
|
||||||
211
core/ui.py
Normal file
211
core/ui.py
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
from PySide6.QtCore import QEasingCurve, QPoint, QPropertyAnimation, Qt, Signal
|
||||||
|
from PySide6.QtWidgets import (
|
||||||
|
QApplication,
|
||||||
|
QLabel,
|
||||||
|
QLineEdit,
|
||||||
|
QMainWindow,
|
||||||
|
QVBoxLayout,
|
||||||
|
QWidget,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RisaAssistantPopup(QMainWindow):
|
||||||
|
text_submitted = Signal(str)
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.setWindowFlags(
|
||||||
|
Qt.WindowType.FramelessWindowHint |
|
||||||
|
Qt.WindowType.WindowStaysOnTopHint |
|
||||||
|
Qt.WindowType.Tool
|
||||||
|
)
|
||||||
|
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||||
|
|
||||||
|
self.popup_width = 540
|
||||||
|
self.font_style = "font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, Arial, sans-serif;"
|
||||||
|
|
||||||
|
self.main_container = QWidget(self)
|
||||||
|
self.main_layout = QVBoxLayout(self.main_container)
|
||||||
|
self.main_layout.setContentsMargins(10, 10, 10, 10)
|
||||||
|
self.main_layout.setSpacing(15)
|
||||||
|
|
||||||
|
self.content_card = QWidget()
|
||||||
|
self.content_card.setObjectName("ContentCard")
|
||||||
|
self.content_layout = QVBoxLayout(self.content_card)
|
||||||
|
self.content_layout.setContentsMargins(22, 16, 22, 16)
|
||||||
|
|
||||||
|
self.content_label = QLabel()
|
||||||
|
self.content_label.setTextFormat(Qt.TextFormat.RichText)
|
||||||
|
self.content_label.setWordWrap(True)
|
||||||
|
self.content_layout.addWidget(self.content_label)
|
||||||
|
self.main_layout.addWidget(self.content_card)
|
||||||
|
|
||||||
|
self.input_card = QWidget()
|
||||||
|
self.input_card.setObjectName("InputCard")
|
||||||
|
self.input_layout = QVBoxLayout(self.input_card)
|
||||||
|
self.input_layout.setContentsMargins(20, 20, 20, 20)
|
||||||
|
self.input_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
|
||||||
|
self.star_icon = QLabel("✹")
|
||||||
|
self.star_icon.setStyleSheet("font-size: 42px; color: #000000; margin-bottom: 2px;")
|
||||||
|
self.star_icon.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self.input_layout.addWidget(self.star_icon)
|
||||||
|
|
||||||
|
self.status_label = QLabel("Sleeping...")
|
||||||
|
self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self.status_label.setStyleSheet("font-size: 13px; color: #777777; font-weight: 600; margin-bottom: 5px;")
|
||||||
|
self.input_layout.addWidget(self.status_label)
|
||||||
|
|
||||||
|
self.input_field = QLineEdit()
|
||||||
|
self.input_field.setPlaceholderText("Type or Speak to RISA...")
|
||||||
|
self.input_field.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self.input_field.setFrame(False)
|
||||||
|
self.input_layout.addWidget(self.input_field)
|
||||||
|
self.input_field.returnPressed.connect(self._on_text_submitted)
|
||||||
|
|
||||||
|
self.main_layout.addWidget(self.input_card)
|
||||||
|
self.setCentralWidget(self.main_container)
|
||||||
|
|
||||||
|
self.setStyleSheet("""
|
||||||
|
QWidget#ContentCard, QWidget#InputCard {
|
||||||
|
background-color: rgba(245, 245, 240, 0.78);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.45);
|
||||||
|
border-radius: 28px;
|
||||||
|
}
|
||||||
|
QLineEdit {
|
||||||
|
background: transparent;
|
||||||
|
color: #000000;
|
||||||
|
font-family: 'Segoe UI', Arial, sans-serif;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
QLineEdit::placeholder {
|
||||||
|
color: #555555;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
self.content_card.hide()
|
||||||
|
self.anim = None
|
||||||
|
|
||||||
|
def _on_text_submitted(self):
|
||||||
|
text = self.input_field.text().strip()
|
||||||
|
if text:
|
||||||
|
self.input_field.clear()
|
||||||
|
self.text_submitted.emit(text)
|
||||||
|
|
||||||
|
def set_listening_state(self, state: str):
|
||||||
|
if state == "idle":
|
||||||
|
self.star_icon.setStyleSheet("font-size: 42px; color: #888888; margin-bottom: 2px;")
|
||||||
|
self.status_label.setText("Sleeping (Say 'Hey Risa')...")
|
||||||
|
self.status_label.setStyleSheet("font-size: 13px; color: #888888; font-weight: 600; margin-bottom: 5px;")
|
||||||
|
elif state == "active":
|
||||||
|
self.star_icon.setStyleSheet("font-size: 42px; color: #D4FF00; margin-bottom: 2px;")
|
||||||
|
self.status_label.setText("Listening...")
|
||||||
|
self.status_label.setStyleSheet("font-size: 13px; color: #ff8c00; font-weight: bold; margin-bottom: 5px;")
|
||||||
|
elif state == "thinking":
|
||||||
|
self.star_icon.setStyleSheet("font-size: 42px; color: #007acc; margin-bottom: 2px;")
|
||||||
|
self.status_label.setText("Thinking...")
|
||||||
|
self.status_label.setStyleSheet("font-size: 13px; color: #007acc; font-weight: bold; margin-bottom: 5px;")
|
||||||
|
|
||||||
|
def update_layout_and_animate(self):
|
||||||
|
self.setFixedWidth(self.popup_width)
|
||||||
|
self.adjustSize()
|
||||||
|
self.resize(self.popup_width, self.sizeHint().height())
|
||||||
|
self.animate_popup(self.height())
|
||||||
|
|
||||||
|
def show_initial_input(self):
|
||||||
|
self.content_card.hide()
|
||||||
|
self.input_layout.setContentsMargins(20, 25, 20, 25)
|
||||||
|
self.star_icon.show()
|
||||||
|
self.status_label.show()
|
||||||
|
self.input_field.setFocus()
|
||||||
|
self.update_layout_and_animate()
|
||||||
|
|
||||||
|
def show_thinking(self, prompt_text: str):
|
||||||
|
html_content = f"""
|
||||||
|
<div style="{self.font_style} color: #111111; font-size: 16px; font-weight: 500; line-height: 140%;">
|
||||||
|
<span style="color: #000000; font-size: 12px; font-weight: bold; text-transform: uppercase; letter-spacing: 1px;">Risa • Asking AI...</span><br>
|
||||||
|
<div style="padding-top: 6px; color: #555555; font-style: italic;">"{prompt_text}"</div>
|
||||||
|
<div style="padding-top: 10px; font-size: 14px; color: #ff8c00; font-weight: bold;">✹ Thinking...</div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
self.content_label.setText(html_content)
|
||||||
|
self.content_card.show()
|
||||||
|
|
||||||
|
self.input_layout.setContentsMargins(20, 20, 20, 20)
|
||||||
|
self.update_layout_and_animate()
|
||||||
|
|
||||||
|
def display_command_card(self, mode: str, data: dict = None):
|
||||||
|
if data is None:
|
||||||
|
data = {}
|
||||||
|
|
||||||
|
html_content = ""
|
||||||
|
|
||||||
|
if mode == "weather":
|
||||||
|
html_content = f"""
|
||||||
|
<table width='100%' style="{self.font_style}">
|
||||||
|
<tr>
|
||||||
|
<td style='font-size: 56px; font-weight: bold; color: #111111; line-height: 100%;'>{data.get('temp', '72')}°</td>
|
||||||
|
<td align='right' style='font-size: 14px; color: #444444; font-weight: 500;'>{data.get('humidity', '55')}% Humidity<br>
|
||||||
|
<span style='color:#777777; font-size: 13px;'>{data.get('high', '85')}° High {data.get('low', '63')}° Low</span></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style='font-size: 15px; color: #222222; font-weight: 600; padding-top: 6px;'>{data.get('condition', 'Sunny')}</td>
|
||||||
|
<td align='right' style='font-size: 15px; color: #222222; font-weight: 600; padding-top: 6px;'>{data.get('location', 'Bridgewater NJ')}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
"""
|
||||||
|
elif mode == "time":
|
||||||
|
html_content = f"""
|
||||||
|
<table width='100%' style="{self.font_style}">
|
||||||
|
<tr>
|
||||||
|
<td style='font-size: 42px; font-weight: 800; color: #111111; line-height: 100%;'>{data.get('time', '3:30 PM')}</td>
|
||||||
|
<td align='right' style='font-size: 15px; color: #111111; font-weight: 600;'>{data.get('day', '')}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style='font-size: 15px; color: #222222; font-weight: 600; padding-top: 4px;'>Sunny, rain soon.</td>
|
||||||
|
<td align='right' style='font-size: 14px; color: #777777; font-weight: 500; padding-top: 4px;'>{data.get('date', '')}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
"""
|
||||||
|
elif mode == "general":
|
||||||
|
html_content = f"""
|
||||||
|
<div style="{self.font_style} color: #111111; font-size: 16px; font-weight: 500; line-height: 145%;">
|
||||||
|
<span style="color: #666666; font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: 1px;">Risa Assistant</span><br>
|
||||||
|
<div style="padding-top: 6px;">{data.get('text', '')}</div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
if html_content:
|
||||||
|
self.content_label.setText(html_content)
|
||||||
|
self.content_card.show()
|
||||||
|
|
||||||
|
self.input_layout.setContentsMargins(20, 20, 20, 20)
|
||||||
|
self.update_layout_and_animate()
|
||||||
|
|
||||||
|
def animate_popup(self, current_height):
|
||||||
|
screen = QApplication.primaryScreen()
|
||||||
|
screen_geom = screen.availableGeometry()
|
||||||
|
|
||||||
|
self.end_x = screen_geom.left() + int((screen_geom.width() - self.popup_width) / 2)
|
||||||
|
self.end_y = screen_geom.bottom() - current_height - 25
|
||||||
|
self.start_y = screen_geom.bottom() + 50
|
||||||
|
|
||||||
|
if self.anim and self.anim.state() == QPropertyAnimation.State.Running:
|
||||||
|
self.anim.stop()
|
||||||
|
|
||||||
|
if not self.isVisible():
|
||||||
|
self.move(self.end_x, self.start_y)
|
||||||
|
self.show()
|
||||||
|
|
||||||
|
self.anim = QPropertyAnimation(self, b"pos")
|
||||||
|
self.anim.setDuration(350)
|
||||||
|
self.anim.setStartValue(self.pos())
|
||||||
|
self.anim.setEndValue(QPoint(self.end_x, self.end_y))
|
||||||
|
self.anim.setEasingCurve(QEasingCurve.Type.OutCubic)
|
||||||
|
self.anim.start()
|
||||||
|
|
||||||
|
def changeEvent(self, event):
|
||||||
|
if event.type() == event.Type.ActivationChange and not self.isActiveWindow():
|
||||||
|
self.hide()
|
||||||
|
super().changeEvent(event)
|
||||||
8
core/wake_word.py
Normal file
8
core/wake_word.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
from config import WAKE_WORDS
|
||||||
|
|
||||||
|
from .speech_recognition import listen_for_command
|
||||||
|
|
||||||
|
|
||||||
|
def detect_wake_word(rec):
|
||||||
|
phrase = listen_for_command(rec)
|
||||||
|
return any(phrase.startswith(word) for word in WAKE_WORDS)
|
||||||
BIN
ico/icon.png
Normal file
BIN
ico/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
BIN
ico/image.png
Normal file
BIN
ico/image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.5 KiB |
375
main.py
Normal file
375
main.py
Normal file
@@ -0,0 +1,375 @@
|
|||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import webbrowser
|
||||||
|
from datetime import date, datetime
|
||||||
|
|
||||||
|
import pyautogui
|
||||||
|
import speech_recognition as sr
|
||||||
|
from PySide6.QtCore import QObject, Signal
|
||||||
|
from PySide6.QtGui import QIcon
|
||||||
|
from PySide6.QtWidgets import QApplication, QMenu, QSystemTrayIcon
|
||||||
|
|
||||||
|
from audio.tts import speak
|
||||||
|
from bs.verity import ouiouioui
|
||||||
|
|
||||||
|
# External file imports
|
||||||
|
from core.ui import RisaAssistantPopup
|
||||||
|
from utils.ai_client import ask_ai
|
||||||
|
from utils.time_utils import time_to_words
|
||||||
|
from utils.weather import get_local_weather
|
||||||
|
|
||||||
|
popup_window = None
|
||||||
|
trigger_bridge = None
|
||||||
|
tray_icon = None
|
||||||
|
|
||||||
|
recognizer = sr.Recognizer()
|
||||||
|
microphone = sr.Microphone()
|
||||||
|
|
||||||
|
recognizer.dynamic_energy_threshold = True
|
||||||
|
recognizer.pause_threshold = 0.8
|
||||||
|
|
||||||
|
class VoiceTriggerBridge(QObject):
|
||||||
|
hide_window = Signal()
|
||||||
|
show_input_panel = Signal()
|
||||||
|
show_thinking_panel = Signal(str)
|
||||||
|
update_display_card = Signal(str, dict)
|
||||||
|
show_timeout_card = Signal(str)
|
||||||
|
update_state = Signal(str)
|
||||||
|
|
||||||
|
# Commands
|
||||||
|
def process_command(command: str) -> bool:
|
||||||
|
global trigger_bridge
|
||||||
|
command = command.lower().strip()
|
||||||
|
if not command:
|
||||||
|
return False
|
||||||
|
|
||||||
|
print(f"Executing command: '{command}'")
|
||||||
|
|
||||||
|
# Time Commands
|
||||||
|
if any(word in command for word in ["time", "what time", "current time"]):
|
||||||
|
now = datetime.now()
|
||||||
|
time_words = time_to_words(now)
|
||||||
|
if trigger_bridge:
|
||||||
|
time_data = {
|
||||||
|
"time": now.strftime("%I:%M %p").lstrip('0'),
|
||||||
|
"day": now.strftime("%A"),
|
||||||
|
"date": now.strftime("%B %d, %Y")
|
||||||
|
}
|
||||||
|
trigger_bridge.update_display_card.emit("time", time_data)
|
||||||
|
speak(f"The time is {time_words}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
elif any(word in command for word in ["date", "what's the date"]):
|
||||||
|
today_date = date.today().strftime("%B %d, %Y")
|
||||||
|
if trigger_bridge:
|
||||||
|
trigger_bridge.update_display_card.emit("general", {"text": f"Today is {today_date}."})
|
||||||
|
speak(f"The date is {today_date}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Weather
|
||||||
|
elif any(word in command for word in ["weather", "weather today", "what is the weather"]):
|
||||||
|
weather = get_local_weather()
|
||||||
|
if weather:
|
||||||
|
city = weather['location']['city']
|
||||||
|
temp_unit = weather['units']['temp']
|
||||||
|
current_temp = weather['current_temp']
|
||||||
|
high_temp = weather['high_temp']
|
||||||
|
low_temp = weather['low_temp']
|
||||||
|
clouds = weather['cloud_cover_pct']
|
||||||
|
rain = weather['precipitation']
|
||||||
|
|
||||||
|
if clouds < 10: cloud_phrase = "Clear skies"
|
||||||
|
elif clouds < 40: cloud_phrase = "Partly cloudy skies"
|
||||||
|
elif clouds < 75: cloud_phrase = "Mostly cloudy skies"
|
||||||
|
else: cloud_phrase = "Completely overcast"
|
||||||
|
|
||||||
|
if rain > 0:
|
||||||
|
cloud_phrase += ", rain soon."
|
||||||
|
|
||||||
|
if trigger_bridge:
|
||||||
|
trigger_bridge.update_display_card.emit("weather", {
|
||||||
|
"temp": str(current_temp),
|
||||||
|
"humidity": str(clouds),
|
||||||
|
"high": str(high_temp),
|
||||||
|
"low": str(low_temp),
|
||||||
|
"condition": cloud_phrase,
|
||||||
|
"location": f"{city} NJ"
|
||||||
|
})
|
||||||
|
print(f"Right now in {city}, it is {current_temp}{temp_unit} with {cloud_phrase.lower()}. Today's high will be {high_temp} degrees, and the low will be {low_temp} degrees.")
|
||||||
|
speak(f"Right now in {city}, it is {current_temp}{temp_unit} with {cloud_phrase.lower()}. Today's high will be {high_temp} degrees, and the low will be {low_temp} degrees.")
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Identity
|
||||||
|
elif any(phrase in command for phrase in ["who are you", "your name", "what do you do"]):
|
||||||
|
if trigger_bridge:
|
||||||
|
trigger_bridge.update_display_card.emit("general", {"text": "Hi, my name is Risa. I'm your voice assistant."})
|
||||||
|
speak("Hi, my name is Risa. I'm your voice assistant.")
|
||||||
|
return True
|
||||||
|
elif any(phrase in command for phrase in ["what is the capital of france", "What is the capital of france", "What's the capital of france"]):
|
||||||
|
if trigger_bridge:
|
||||||
|
trigger_bridge.update_display_card.emit("general", {"text": "Oui oui oui! It paris"})
|
||||||
|
ouiouioui()
|
||||||
|
return True
|
||||||
|
# Media Controls
|
||||||
|
elif any(phrase in command for phrase in ["pause", "pause music", "pause video", "play", "resume"]):
|
||||||
|
pyautogui.press('playpause')
|
||||||
|
speak("Done")
|
||||||
|
return True
|
||||||
|
|
||||||
|
elif "volume up" in command:
|
||||||
|
for _ in range(5): pyautogui.press("volumeup")
|
||||||
|
speak("Volume up")
|
||||||
|
return True
|
||||||
|
|
||||||
|
elif "volume down" in command:
|
||||||
|
for _ in range(5): pyautogui.press("volumedown")
|
||||||
|
speak("Volume down")
|
||||||
|
return True
|
||||||
|
|
||||||
|
elif "mute" in command:
|
||||||
|
pyautogui.press("volumemute")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Web Controls
|
||||||
|
elif "open google" in command:
|
||||||
|
speak("Opening Google")
|
||||||
|
webbrowser.open("https://google.com")
|
||||||
|
return True
|
||||||
|
|
||||||
|
elif "open youtube" in command:
|
||||||
|
speak("Opening Youtube")
|
||||||
|
webbrowser.open("https://youtube.com")
|
||||||
|
return True
|
||||||
|
|
||||||
|
elif "search for" in command:
|
||||||
|
query = command.split("search for")[-1].strip()
|
||||||
|
if query:
|
||||||
|
if trigger_bridge:
|
||||||
|
trigger_bridge.update_display_card.emit("general", {"text": f"Searching Google for: {query}"})
|
||||||
|
speak(f"Searching for {query}")
|
||||||
|
webbrowser.open(f"https://www.google.com/search?q={query}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Apps
|
||||||
|
elif "open calculator" in command:
|
||||||
|
speak("Opening Calculator")
|
||||||
|
if os.name == 'nt': subprocess.Popen('calc.exe')
|
||||||
|
else: subprocess.Popen('gnome-calculator' if os.name == 'posix' else 'open -a Calculator')
|
||||||
|
return True
|
||||||
|
# Sleep
|
||||||
|
elif any(phrase in command for phrase in ["stop listening", "go to sleep", "hide"]):
|
||||||
|
speak("Going to sleep.")
|
||||||
|
if popup_window and trigger_bridge:
|
||||||
|
trigger_bridge.hide_window.emit()
|
||||||
|
trigger_bridge.update_state.emit("idle")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# Commands List
|
||||||
|
elif any(phrase in command for phrase in ["list commands", "what are your commands", "show commands"]):
|
||||||
|
commands_html = """
|
||||||
|
<div style="line-height: 140%;">
|
||||||
|
<b style="color: #ff8c00; font-size: 15px;">System Capabilities:</b><br>
|
||||||
|
<table width="100%" style="margin-top: 5px; font-size: 13px; color: #111111;">
|
||||||
|
<tr><td>• <b>Time / Date:</b> "What time is it?", "What's the date?"</td></tr>
|
||||||
|
<tr><td>• <b>Weather:</b> "What is the weather today?"</td></tr>
|
||||||
|
<tr><td>• <b>Controls:</b> "Volume up/down", "Mute", "Pause/Play"</td></tr>
|
||||||
|
<tr><td>• <b>Apps & Web:</b> "Open Google", "Search for [query]"</td></tr>
|
||||||
|
<tr><td>• <b>AI Core:</b> "Ask AI [your question]"</td></tr>
|
||||||
|
<tr><td>• <b>Sleep:</b> "Stop listening", "Go to sleep"</td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
if trigger_bridge:
|
||||||
|
trigger_bridge.update_display_card.emit("general", {"text": commands_html})
|
||||||
|
speak("Here is a list of commands.")
|
||||||
|
return True
|
||||||
|
|
||||||
|
else:
|
||||||
|
# AI Fallback Query
|
||||||
|
prompt = command
|
||||||
|
if prompt.startswith("ask ai") or prompt.startswith("ask a i"):
|
||||||
|
for phrase in ["ask ai", "ask a i"]:
|
||||||
|
if prompt.startswith(phrase):
|
||||||
|
prompt = prompt.replace(phrase, "", 1).strip(" :")
|
||||||
|
break
|
||||||
|
|
||||||
|
if prompt:
|
||||||
|
if trigger_bridge:
|
||||||
|
trigger_bridge.show_thinking_panel.emit(prompt)
|
||||||
|
trigger_bridge.update_state.emit("thinking")
|
||||||
|
|
||||||
|
speak("Thinking about it...")
|
||||||
|
ai_response = ask_ai(prompt)
|
||||||
|
|
||||||
|
if trigger_bridge and ai_response:
|
||||||
|
trigger_bridge.update_display_card.emit("general", {"text": str(ai_response)})
|
||||||
|
elif trigger_bridge:
|
||||||
|
trigger_bridge.update_display_card.emit("general", {"text": f"Processed request: '{prompt}'"})
|
||||||
|
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
speak("What would you like to ask?")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Voice listener loop
|
||||||
|
def voice_listener_loop():
|
||||||
|
global trigger_bridge
|
||||||
|
|
||||||
|
with microphone as source:
|
||||||
|
recognizer.adjust_for_ambient_noise(source, duration=1.5)
|
||||||
|
print("\nRisa is active... Say 'Hey Risa [your command]' in one sentence!")
|
||||||
|
in_conversation = False
|
||||||
|
timeout_emitted = False
|
||||||
|
|
||||||
|
if trigger_bridge:
|
||||||
|
trigger_bridge.update_state.emit("idle")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
if not in_conversation:
|
||||||
|
if trigger_bridge:
|
||||||
|
trigger_bridge.update_state.emit("idle")
|
||||||
|
print(".", end="", flush=True)
|
||||||
|
|
||||||
|
audio = recognizer.listen(source, timeout=None, phrase_time_limit=10)
|
||||||
|
raw_text = recognizer.recognize_google(audio).lower()
|
||||||
|
|
||||||
|
if "hey risa" in raw_text or "hey lisa" in raw_text:
|
||||||
|
print(f"\n[Triggered]: '{raw_text}'")
|
||||||
|
timeout_emitted = False
|
||||||
|
|
||||||
|
if trigger_bridge:
|
||||||
|
trigger_bridge.show_input_panel.emit()
|
||||||
|
trigger_bridge.update_state.emit("active")
|
||||||
|
|
||||||
|
if "risa" in raw_text:
|
||||||
|
command_text = raw_text.split("risa")[-1].strip()
|
||||||
|
else:
|
||||||
|
command_text = raw_text.split("lisa")[-1].strip()
|
||||||
|
|
||||||
|
if command_text:
|
||||||
|
executed = process_command(command_text)
|
||||||
|
if executed:
|
||||||
|
in_conversation = True
|
||||||
|
else:
|
||||||
|
speak("How can I help?")
|
||||||
|
in_conversation = True
|
||||||
|
else:
|
||||||
|
print("\n[Follow-up Mode: Listening...]")
|
||||||
|
if trigger_bridge:
|
||||||
|
trigger_bridge.update_state.emit("active")
|
||||||
|
time.sleep(0.4)
|
||||||
|
|
||||||
|
audio = recognizer.listen(source, timeout=6, phrase_time_limit=6)
|
||||||
|
command_text = recognizer.recognize_google(audio).lower()
|
||||||
|
print(f"[Follow-up Command Received]: '{command_text}'")
|
||||||
|
|
||||||
|
timeout_emitted = False
|
||||||
|
executed = process_command(command_text)
|
||||||
|
if not executed:
|
||||||
|
in_conversation = False
|
||||||
|
|
||||||
|
except (sr.UnknownValueError, sr.WaitTimeoutError, sr.RequestError) as e:
|
||||||
|
if in_conversation:
|
||||||
|
print("\n[Follow-up timeout or unrecognized audio: resetting context]")
|
||||||
|
in_conversation = False
|
||||||
|
|
||||||
|
if not timeout_emitted:
|
||||||
|
if trigger_bridge:
|
||||||
|
error_msg = "Risa could not hear you clearly or the request timed out."
|
||||||
|
if isinstance(e, sr.WaitTimeoutError):
|
||||||
|
error_msg = "Follow-up timed out. No speech detected."
|
||||||
|
|
||||||
|
html_error = f"""
|
||||||
|
<div style="line-height: 140%;">
|
||||||
|
<span style="color: #d9534f; font-size: 12px; font-weight: bold; text-transform: uppercase; letter-spacing: 1px;">Timeout</span><br>
|
||||||
|
<div style="padding-top: 6px; color: #333333; font-weight: 500;">{error_msg} Please reinitiate Risa by saying: Hey Risa</div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
trigger_bridge.update_display_card.emit("general", {"text": html_error})
|
||||||
|
trigger_bridge.update_state.emit("idle")
|
||||||
|
timeout_emitted = True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\nUnexpected error: {e}")
|
||||||
|
in_conversation = False
|
||||||
|
if trigger_bridge:
|
||||||
|
trigger_bridge.update_state.emit("idle")
|
||||||
|
|
||||||
|
def handle_typed_command(command_text: str):
|
||||||
|
def run():
|
||||||
|
if trigger_bridge:
|
||||||
|
trigger_bridge.show_input_panel.emit()
|
||||||
|
trigger_bridge.update_state.emit("active")
|
||||||
|
|
||||||
|
process_command(command_text)
|
||||||
|
|
||||||
|
threading.Thread(target=run, daemon=True).start()
|
||||||
|
|
||||||
|
def trigger_from_tray():
|
||||||
|
if popup_window:
|
||||||
|
popup_window.show()
|
||||||
|
popup_window.raise_()
|
||||||
|
popup_window.activateWindow()
|
||||||
|
if trigger_bridge:
|
||||||
|
trigger_bridge.show_input_panel.emit()
|
||||||
|
trigger_bridge.update_state.emit("active")
|
||||||
|
|
||||||
|
def setup_system_tray(app):
|
||||||
|
global tray_icon
|
||||||
|
|
||||||
|
app.setQuitOnLastWindowClosed(False)
|
||||||
|
|
||||||
|
icon = QIcon("ico/icon.png")
|
||||||
|
|
||||||
|
tray_icon = QSystemTrayIcon(icon, app)
|
||||||
|
tray_icon.setToolTip("Risa Assistant")
|
||||||
|
|
||||||
|
tray_menu = QMenu()
|
||||||
|
|
||||||
|
show_action = tray_menu.addAction("Open Risa")
|
||||||
|
show_action.triggered.connect(trigger_from_tray)
|
||||||
|
|
||||||
|
tray_menu.addSeparator()
|
||||||
|
|
||||||
|
quit_action = tray_menu.addAction("Quit")
|
||||||
|
quit_action.triggered.connect(app.quit)
|
||||||
|
|
||||||
|
tray_icon.setContextMenu(tray_menu)
|
||||||
|
|
||||||
|
def on_tray_activated(reason):
|
||||||
|
if reason in (QSystemTrayIcon.Trigger, QSystemTrayIcon.DoubleClick):
|
||||||
|
trigger_from_tray()
|
||||||
|
|
||||||
|
tray_icon.activated.connect(on_tray_activated)
|
||||||
|
tray_icon.show()
|
||||||
|
|
||||||
|
def main():
|
||||||
|
global popup_window, trigger_bridge
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
|
||||||
|
setup_system_tray(app)
|
||||||
|
|
||||||
|
trigger_bridge = VoiceTriggerBridge()
|
||||||
|
popup_window = RisaAssistantPopup()
|
||||||
|
|
||||||
|
trigger_bridge.show_input_panel.connect(popup_window.show_initial_input)
|
||||||
|
trigger_bridge.show_thinking_panel.connect(popup_window.show_thinking)
|
||||||
|
trigger_bridge.update_display_card.connect(popup_window.display_command_card)
|
||||||
|
trigger_bridge.hide_window.connect(popup_window.hide)
|
||||||
|
trigger_bridge.update_state.connect(popup_window.set_listening_state)
|
||||||
|
|
||||||
|
popup_window.text_submitted.connect(handle_typed_command)
|
||||||
|
|
||||||
|
listener_thread = threading.Thread(target=voice_listener_loop, daemon=True)
|
||||||
|
listener_thread.start()
|
||||||
|
|
||||||
|
sys.exit(app.exec())
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
44
main.spec
Normal file
44
main.spec
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# -*- mode: python ; coding: utf-8 -*-
|
||||||
|
|
||||||
|
|
||||||
|
a = Analysis(
|
||||||
|
['main.py'],
|
||||||
|
pathex=[],
|
||||||
|
binaries=[],
|
||||||
|
datas=[],
|
||||||
|
hiddenimports=[],
|
||||||
|
hookspath=[],
|
||||||
|
hooksconfig={},
|
||||||
|
runtime_hooks=[],
|
||||||
|
excludes=[],
|
||||||
|
noarchive=False,
|
||||||
|
optimize=0,
|
||||||
|
)
|
||||||
|
pyz = PYZ(a.pure)
|
||||||
|
|
||||||
|
exe = EXE(
|
||||||
|
pyz,
|
||||||
|
a.scripts,
|
||||||
|
[],
|
||||||
|
exclude_binaries=True,
|
||||||
|
name='main',
|
||||||
|
debug=False,
|
||||||
|
bootloader_ignore_signals=False,
|
||||||
|
strip=False,
|
||||||
|
upx=True,
|
||||||
|
console=True,
|
||||||
|
disable_windowed_traceback=False,
|
||||||
|
argv_emulation=False,
|
||||||
|
target_arch=None,
|
||||||
|
codesign_identity=None,
|
||||||
|
entitlements_file=None,
|
||||||
|
)
|
||||||
|
coll = COLLECT(
|
||||||
|
exe,
|
||||||
|
a.binaries,
|
||||||
|
a.datas,
|
||||||
|
strip=False,
|
||||||
|
upx=True,
|
||||||
|
upx_exclude=[],
|
||||||
|
name='main',
|
||||||
|
)
|
||||||
1
requirements.txt
Normal file
1
requirements.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pip install ruff
|
||||||
2
setup.ps1
Normal file
2
setup.ps1
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
.venv\Scripts\Activate.ps1
|
||||||
|
pip install -r requirements.txt
|
||||||
BIN
somerville_weather.mp3
Normal file
BIN
somerville_weather.mp3
Normal file
Binary file not shown.
0
utils/__init__.py
Normal file
0
utils/__init__.py
Normal file
43
utils/ai_client.py
Normal file
43
utils/ai_client.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
import ollama
|
||||||
|
|
||||||
|
from audio.tts import speak
|
||||||
|
|
||||||
|
AI_MODEL = "gemma4:e2b-it-qat"
|
||||||
|
AI_HOST = "http://localhost:11434"
|
||||||
|
def clean_response(text):
|
||||||
|
cleaned = re.sub(r"<think>.*?</think>", "", text, flags=re.DOTALL | re.IGNORECASE)
|
||||||
|
lines = [line for line in cleaned.splitlines() if line.strip()]
|
||||||
|
return "\n".join(lines).strip()
|
||||||
|
def ask_ai(prompt):
|
||||||
|
try:
|
||||||
|
full_prompt = (
|
||||||
|
"You must answer in exactly one or two sentences.\n\n"
|
||||||
|
f"Question: {prompt}\n"
|
||||||
|
"Answer:"
|
||||||
|
)
|
||||||
|
print("Thinking...", end="", flush=True)
|
||||||
|
client = ollama.Client(host=AI_HOST)
|
||||||
|
stream = client.generate(
|
||||||
|
model=AI_MODEL,
|
||||||
|
prompt=full_prompt,
|
||||||
|
stream=True
|
||||||
|
)
|
||||||
|
ai_text = ""
|
||||||
|
for chunk in stream:
|
||||||
|
text_piece = chunk.get("response", "")
|
||||||
|
if text_piece:
|
||||||
|
print(text_piece, end="", flush=True)
|
||||||
|
ai_text += text_piece
|
||||||
|
print()
|
||||||
|
final_text = clean_response(ai_text)
|
||||||
|
if not final_text:
|
||||||
|
speak("Sorry, I got no response from the AI.")
|
||||||
|
else:
|
||||||
|
speak(final_text)
|
||||||
|
except Exception as e:
|
||||||
|
|
||||||
|
print("\nAI Error:", e)
|
||||||
|
|
||||||
|
speak("Sorry, I couldn't connect to the AI.")
|
||||||
23
utils/time_utils.py
Normal file
23
utils/time_utils.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
def time_to_words(dt: datetime):
|
||||||
|
hour = dt.hour % 12 or 12
|
||||||
|
minute = dt.minute
|
||||||
|
numbers = ["zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine",
|
||||||
|
"ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen",
|
||||||
|
"eighteen", "nineteen", "twenty", "twenty-one", "twenty-two", "twenty-three",
|
||||||
|
"twenty-four", "twenty-five", "twenty-six", "twenty-seven", "twenty-eight",
|
||||||
|
"twenty-nine", "thirty", "thirty-one", "thirty-two", "thirty-three", "thirty-four",
|
||||||
|
"thirty-five", "thirty-six", "thirty-seven", "thirty-eight", "thirty-nine",
|
||||||
|
"forty", "forty-one", "forty-two", "forty-three", "forty-four", "forty-five",
|
||||||
|
"forty-six", "forty-seven", "forty-eight", "forty-nine", "fifty", "fifty-one",
|
||||||
|
"fifty-two", "fifty-three", "fifty-four", "fifty-five", "fifty-six", "fifty-seven",
|
||||||
|
"fifty-eight", "fifty-nine"]
|
||||||
|
am_pm = "AM" if dt.hour < 12 else "PM"
|
||||||
|
if minute == 0:
|
||||||
|
return f"{numbers[hour]} o'clock {am_pm}"
|
||||||
|
elif minute < 10:
|
||||||
|
return f"{numbers[hour]} oh {numbers[minute]} {am_pm}"
|
||||||
|
else:
|
||||||
|
return f"{numbers[hour]} {numbers[minute]} {am_pm}"
|
||||||
22
utils/vosk_setup.py
Normal file
22
utils/vosk_setup.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import os
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import vosk
|
||||||
|
|
||||||
|
from config import VOSK_MODEL_FOLDER, VOSK_MODEL_URL, VOSK_MODEL_ZIP
|
||||||
|
|
||||||
|
|
||||||
|
def setup_vosk_model():
|
||||||
|
if not os.path.isdir(VOSK_MODEL_FOLDER):
|
||||||
|
print("Downloading Vosk model")
|
||||||
|
response = requests.get(VOSK_MODEL_URL)
|
||||||
|
with open(VOSK_MODEL_ZIP, "wb") as file:
|
||||||
|
file.write(response.content)
|
||||||
|
with zipfile.ZipFile(VOSK_MODEL_ZIP, 'r') as zip_ref:
|
||||||
|
zip_ref.extractall()
|
||||||
|
print("Downloaded!")
|
||||||
|
|
||||||
|
model = vosk.Model(VOSK_MODEL_FOLDER)
|
||||||
|
rec = vosk.KaldiRecognizer(model, 16000)
|
||||||
|
return rec
|
||||||
95
utils/weather.py
Normal file
95
utils/weather.py
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import requests
|
||||||
|
|
||||||
|
|
||||||
|
def get_local_weather(
|
||||||
|
fallback_lat=40.59,
|
||||||
|
fallback_lon=-74.62,
|
||||||
|
fallback_city="Bridgewater",
|
||||||
|
fallback_region="New Jersey",
|
||||||
|
fallback_country_code="US"
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Fetches the user's current location and automatically determines
|
||||||
|
whether to return Metric or Imperial units based on their country.
|
||||||
|
"""
|
||||||
|
lat, lon = fallback_lat, fallback_lon
|
||||||
|
city, region = fallback_city, fallback_region
|
||||||
|
country_code = fallback_country_code
|
||||||
|
|
||||||
|
# Geolocation Lookup
|
||||||
|
try:
|
||||||
|
geo_url = "http://ip-api.com/json"
|
||||||
|
geo_response = requests.get(geo_url, timeout=5).json()
|
||||||
|
|
||||||
|
if geo_response.get("status") == "success":
|
||||||
|
lat = geo_response["lat"]
|
||||||
|
lon = geo_response["lon"]
|
||||||
|
city = geo_response["city"]
|
||||||
|
region = geo_response["regionName"]
|
||||||
|
country_code = geo_response.get("countryCode", "US")
|
||||||
|
else:
|
||||||
|
print("IP lookup API returned an error status. Using fallback coordinates.")
|
||||||
|
except Exception:
|
||||||
|
print("Network error during IP lookup. Using fallback coordinates.")
|
||||||
|
|
||||||
|
# Unit System 'n Weather URL
|
||||||
|
is_imperial = country_code in ["US", "LR", "MM"]
|
||||||
|
|
||||||
|
unit_suffix = ""
|
||||||
|
if is_imperial:
|
||||||
|
unit_suffix = "&temperature_unit=fahrenheit&precipitation_unit=inch"
|
||||||
|
|
||||||
|
weather_url = (
|
||||||
|
f"https://api.open-meteo.com/v1/forecast?"
|
||||||
|
f"latitude={lat}&longitude={lon}"
|
||||||
|
f"¤t=temperature_2m,precipitation,cloud_cover"
|
||||||
|
f"&daily=temperature_2m_max,temperature_2m_min"
|
||||||
|
f"&timezone=auto"
|
||||||
|
f"{unit_suffix}"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.get(weather_url, timeout=5)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
current = data["current"]
|
||||||
|
daily = data["daily"]
|
||||||
|
|
||||||
|
temp_unit = "°F" if is_imperial else "°C"
|
||||||
|
precip_unit = "inch" if is_imperial else "mm"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"location": {
|
||||||
|
"city": city,
|
||||||
|
"region": region,
|
||||||
|
"country_code": country_code,
|
||||||
|
"lat": lat,
|
||||||
|
"lon": lon
|
||||||
|
},
|
||||||
|
"unit_system": "imperial" if is_imperial else "metric",
|
||||||
|
"units": {"temp": temp_unit, "precipitation": precip_unit},
|
||||||
|
"current_temp": current['temperature_2m'],
|
||||||
|
"high_temp": daily['temperature_2m_max'][0],
|
||||||
|
"low_temp": daily['temperature_2m_min'][0],
|
||||||
|
"precipitation": current['precipitation'],
|
||||||
|
"cloud_cover_pct": current['cloud_cover']
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
print(f"Weather API returned error code: {response.status_code}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Network error while fetching weather: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Test block
|
||||||
|
weather_data = get_local_weather()
|
||||||
|
if weather_data:
|
||||||
|
u = weather_data["units"]
|
||||||
|
print(f"\n--- Location: {weather_data['location']['city']}, {weather_data['location']['region']} ({weather_data['location']['country_code']}) ---")
|
||||||
|
print(f"System Chosen: {weather_data['unit_system'].upper()}")
|
||||||
|
print(f"Current Temperature: {weather_data['current_temp']}{u['temp']}")
|
||||||
|
print(f"Today's High: {weather_data['high_temp']}{u['temp']}")
|
||||||
|
print(f"Precipitation: {weather_data['precipitation']} {u['precipitation']}")
|
||||||
Reference in New Issue
Block a user