fix stuff

This commit is contained in:
2026-09-21 01:55:42 -04:00
commit 02d7c10f7d
26 changed files with 953 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
__pycache__
core/__pycache__
utils/__pycache__
audio/__pycache__
venv

0
audio/__init__.py Normal file
View File

8
audio/audio_queue.py Normal file
View 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
View 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
View 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
View 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
View File

BIN
bs/marshallhatesthis.mp3 Normal file

Binary file not shown.

21
bs/verity.py Normal file
View 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()

BIN
chime.mp3 Normal file

Binary file not shown.

14
config.py Normal file
View 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"

View 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
View 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 &nbsp;&nbsp; {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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
ico/image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

375
main.py Normal file
View 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
View 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
View File

@@ -0,0 +1 @@
pip install ruff

2
setup.ps1 Normal file
View File

@@ -0,0 +1,2 @@
.venv\Scripts\Activate.ps1
pip install -r requirements.txt

BIN
somerville_weather.mp3 Normal file

Binary file not shown.

0
utils/__init__.py Normal file
View File

43
utils/ai_client.py Normal file
View 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
View 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
View 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
View 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"&current=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']}")