Add files via upload
This commit is contained in:
16 files changed
+1324
No files matched your search
@@ -0,0 +1,45 @@
|
|||||||
|
# 🤖 Jarvis Mark X — Local AI Desktop Assistant
|
||||||
|
|
||||||
|
Jarvis Mark X is a **local, modular AI desktop assistant** inspired by Jarvis and built from scratch using **Python**.
|
||||||
|
|
||||||
|
It allows you to control your computer with voice commands and an intent → action system.
|
||||||
|
|
||||||
|
## 🚀 Features
|
||||||
|
- Open applications
|
||||||
|
- Send messages (WhatsApp)
|
||||||
|
- Web search
|
||||||
|
- Weather reports
|
||||||
|
- Temporary & long-term memory
|
||||||
|
- Chatting with good memory
|
||||||
|
|
||||||
|
|
||||||
|
## 🎥 Tutorial
|
||||||
|
This project is part of a **video tutorial series**.
|
||||||
|
Please watch the tutorial to understand the architecture and how to safely extend the system.
|
||||||
|
video: https://youtu.be/ZD6kf9w9Sy0?si=DvySsKIeL4RuAjVt
|
||||||
|
|
||||||
|
## 🔐 Security Notice
|
||||||
|
- Runs **100% locally**
|
||||||
|
- No remote access by default
|
||||||
|
- Jarvis only executes what **YOU code**
|
||||||
|
|
||||||
|
⚠️ Do NOT paste unknown code or give full system control to untrusted scripts.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ Usage Warning
|
||||||
|
- **No commercial use**
|
||||||
|
- Do NOT sell, market, or monetize
|
||||||
|
- Do NOT present this project as your own on social media
|
||||||
|
|
||||||
|
You may use and modify it for **personal and educational purposes**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📲 Follow & Contact
|
||||||
|
Instagram: https://www.instagram.com/fatihmakes/
|
||||||
|
TikTok: https://www.tiktok.com/@fatihmakes
|
||||||
|
|
||||||
|
If you have issues or suggestions, feel free to reach out.
|
||||||
|
|
||||||
|
⭐ Like, follow, and subscribe for future Jarvis upgrades!
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
#EXAMPLE ACTION IDEA FOR IMPROVE YOURSELF
|
||||||
|
#TAKE THE API KEY FROM A RADAR WEB SITE TO SEE ALL PLANES ON THE AIR
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import time
|
||||||
|
import pyautogui
|
||||||
|
from tts import edge_speak
|
||||||
|
|
||||||
|
|
||||||
|
def open_app(
|
||||||
|
parameters: dict,
|
||||||
|
response: str | None = None,
|
||||||
|
player=None,
|
||||||
|
session_memory=None
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
Opens an application using Windows search.
|
||||||
|
|
||||||
|
parameters:
|
||||||
|
- app_name (str)
|
||||||
|
|
||||||
|
Memory behavior:
|
||||||
|
- Uses ONLY session memory
|
||||||
|
- No long-term memory writes
|
||||||
|
"""
|
||||||
|
|
||||||
|
app_name = (parameters or {}).get("app_name", "").strip()
|
||||||
|
|
||||||
|
if not app_name and session_memory:
|
||||||
|
app_name = session_memory.open_app or ""
|
||||||
|
|
||||||
|
if not app_name:
|
||||||
|
msg = "Sir, I couldn't determine which application to open."
|
||||||
|
if player:
|
||||||
|
player.write_log(msg)
|
||||||
|
edge_speak(msg, player)
|
||||||
|
return False
|
||||||
|
|
||||||
|
if response:
|
||||||
|
if player:
|
||||||
|
player.write_log(response)
|
||||||
|
edge_speak(response, player)
|
||||||
|
|
||||||
|
try:
|
||||||
|
pyautogui.PAUSE = 0.1
|
||||||
|
|
||||||
|
|
||||||
|
pyautogui.press("win")
|
||||||
|
time.sleep(0.3)
|
||||||
|
|
||||||
|
pyautogui.write(app_name, interval=0.03)
|
||||||
|
time.sleep(0.2)
|
||||||
|
|
||||||
|
pyautogui.press("enter")
|
||||||
|
time.sleep(0.6)
|
||||||
|
|
||||||
|
if session_memory:
|
||||||
|
session_memory.set_open_app(app_name)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
msg = f"Sir, I failed to open {app_name}."
|
||||||
|
if player:
|
||||||
|
player.write_log(f"{msg} ({e})")
|
||||||
|
edge_speak(msg, player)
|
||||||
|
return False
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import time
|
||||||
|
import pyautogui
|
||||||
|
from tts import edge_speak
|
||||||
|
|
||||||
|
REQUIRED_PARAMS = ["receiver", "message_text", "platform"]
|
||||||
|
|
||||||
|
def send_message(parameters: dict, response: str | None = None, player=None, session_memory=None) -> bool:
|
||||||
|
"""
|
||||||
|
Send a message via Windows app (WhatsApp, Telegram, etc.)
|
||||||
|
|
||||||
|
Multi-step support: asks for missing parameters using temporary memory.
|
||||||
|
|
||||||
|
Expected parameters:
|
||||||
|
- receiver (str)
|
||||||
|
- message_text (str)
|
||||||
|
- platform (str, default: "WhatsApp")
|
||||||
|
"""
|
||||||
|
|
||||||
|
if session_memory is None:
|
||||||
|
msg = "Session memory missing, cannot proceed."
|
||||||
|
if player:
|
||||||
|
player.write_log(msg)
|
||||||
|
edge_speak(msg, player)
|
||||||
|
return False
|
||||||
|
|
||||||
|
if parameters:
|
||||||
|
session_memory.update_parameters(parameters)
|
||||||
|
|
||||||
|
for param in REQUIRED_PARAMS:
|
||||||
|
value = session_memory.get_parameter(param)
|
||||||
|
if not value:
|
||||||
|
|
||||||
|
session_memory.set_current_question(param)
|
||||||
|
question_text = ""
|
||||||
|
if param == "receiver":
|
||||||
|
question_text = "Sir, who should I send the message to?"
|
||||||
|
elif param == "message_text":
|
||||||
|
question_text = "Sir, what should I say?"
|
||||||
|
elif param == "platform":
|
||||||
|
question_text = "Sir, which platform should I use? (WhatsApp, Telegram, etc.)"
|
||||||
|
else:
|
||||||
|
question_text = f"Sir, please provide {param}."
|
||||||
|
|
||||||
|
if player:
|
||||||
|
player.write_log("AI :", question_text)
|
||||||
|
edge_speak(question_text, player)
|
||||||
|
return False
|
||||||
|
|
||||||
|
receiver = session_memory.get_parameter("receiver").strip()
|
||||||
|
platform = session_memory.get_parameter("platform").strip() or "WhatsApp"
|
||||||
|
message_text = session_memory.get_parameter("message_text").strip()
|
||||||
|
|
||||||
|
if response:
|
||||||
|
if player:
|
||||||
|
player.write_log(response)
|
||||||
|
edge_speak(response, player)
|
||||||
|
|
||||||
|
try:
|
||||||
|
pyautogui.PAUSE = 0.1
|
||||||
|
|
||||||
|
pyautogui.press("win")
|
||||||
|
time.sleep(0.3)
|
||||||
|
pyautogui.write(platform, interval=0.03)
|
||||||
|
pyautogui.press("enter")
|
||||||
|
time.sleep(0.6)
|
||||||
|
|
||||||
|
pyautogui.hotkey("ctrl", "f")
|
||||||
|
time.sleep(0.2)
|
||||||
|
pyautogui.write(receiver, interval=0.03)
|
||||||
|
time.sleep(0.2)
|
||||||
|
pyautogui.press("enter")
|
||||||
|
time.sleep(0.2)
|
||||||
|
|
||||||
|
pyautogui.write(message_text, interval=0.03)
|
||||||
|
pyautogui.press("enter")
|
||||||
|
|
||||||
|
session_memory.clear_current_question()
|
||||||
|
session_memory.clear_pending_intent()
|
||||||
|
session_memory.update_parameters({})
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# Log success
|
||||||
|
# -----------------------------
|
||||||
|
success_msg = f"Sir, message sent to {receiver} via {platform}."
|
||||||
|
if player:
|
||||||
|
player.write_log(success_msg)
|
||||||
|
edge_speak(success_msg, player)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
msg = f"Sir, I failed to send the message. ({e})"
|
||||||
|
if player:
|
||||||
|
player.write_log(msg)
|
||||||
|
edge_speak(msg, player)
|
||||||
|
return False
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import webbrowser
|
||||||
|
from urllib.parse import quote_plus
|
||||||
|
from tts import edge_speak
|
||||||
|
|
||||||
|
|
||||||
|
def weather_action(
|
||||||
|
parameters: dict,
|
||||||
|
player=None,
|
||||||
|
session_memory=None
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Weather report action.
|
||||||
|
Opens a Google weather search and gives a short spoken confirmation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
city = parameters.get("city")
|
||||||
|
time = parameters.get("time")
|
||||||
|
if not city or not isinstance(city, str):
|
||||||
|
msg = "Sir, the city is missing for the weather report."
|
||||||
|
_speak_and_log(msg, player)
|
||||||
|
return msg
|
||||||
|
|
||||||
|
city = city.strip()
|
||||||
|
|
||||||
|
if not time or not isinstance(time, str):
|
||||||
|
time = "today"
|
||||||
|
else:
|
||||||
|
time = time.strip()
|
||||||
|
|
||||||
|
search_query = f"weather in {city} {time}"
|
||||||
|
encoded_query = quote_plus(search_query)
|
||||||
|
url = f"https://www.google.com/search?q={encoded_query}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
webbrowser.open(url)
|
||||||
|
except Exception:
|
||||||
|
msg = f"Sir, I couldn't open the browser for the weather report."
|
||||||
|
_speak_and_log(msg, player)
|
||||||
|
return msg
|
||||||
|
|
||||||
|
msg = f"Showing the weather for {city}, {time}, sir."
|
||||||
|
_speak_and_log(msg, player)
|
||||||
|
|
||||||
|
if session_memory:
|
||||||
|
try:
|
||||||
|
session_memory.set_last_search(
|
||||||
|
query=search_query,
|
||||||
|
response=msg
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return msg
|
||||||
|
|
||||||
|
|
||||||
|
def _speak_and_log(message: str, player=None):
|
||||||
|
"""Helper: log + TTS safely"""
|
||||||
|
if player:
|
||||||
|
try:
|
||||||
|
player.write_log(f"JARVIS: {message}")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
edge_speak(message)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
import re
|
||||||
|
from serpapi import GoogleSearch
|
||||||
|
from tts import edge_speak
|
||||||
|
|
||||||
|
|
||||||
|
MAX_SNIPPETS = 3
|
||||||
|
MIN_SENTENCE_LENGTH = 30
|
||||||
|
|
||||||
|
|
||||||
|
def clean(text: str) -> str:
|
||||||
|
"""Clean text: remove extra spaces, ..., brackets."""
|
||||||
|
if not text:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
text = re.sub(r"\s+", " ", text)
|
||||||
|
text = re.sub(r"\.\.\.+", ".", text)
|
||||||
|
text = re.sub(r"\[.*?\]", "", text)
|
||||||
|
text = re.sub(r"\(.*?\)", "", text)
|
||||||
|
return text.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def split_sentences(text: str):
|
||||||
|
"""Split text into sentences and avoid very short or incomplete ones."""
|
||||||
|
sentences = re.split(r"(?<=[.!?])\s+", text)
|
||||||
|
|
||||||
|
valid_sentences = []
|
||||||
|
buffer = ""
|
||||||
|
|
||||||
|
for s in sentences:
|
||||||
|
s = clean(s)
|
||||||
|
if not s:
|
||||||
|
continue
|
||||||
|
|
||||||
|
last_word = s.split()[-1].lower()
|
||||||
|
if last_word in [
|
||||||
|
"of", "in", "at", "on", "by",
|
||||||
|
"after", "before", "with", "for", "to"
|
||||||
|
]:
|
||||||
|
buffer += s + " "
|
||||||
|
continue
|
||||||
|
|
||||||
|
if buffer:
|
||||||
|
s = buffer + s
|
||||||
|
buffer = ""
|
||||||
|
|
||||||
|
if len(s) >= MIN_SENTENCE_LENGTH:
|
||||||
|
valid_sentences.append(s)
|
||||||
|
|
||||||
|
return valid_sentences
|
||||||
|
|
||||||
|
|
||||||
|
def is_noise(text: str) -> bool:
|
||||||
|
"""Check if a sentence is noise or irrelevant."""
|
||||||
|
t = text.lower()
|
||||||
|
|
||||||
|
noise_keywords = [
|
||||||
|
"read more",
|
||||||
|
"learn more",
|
||||||
|
"click here",
|
||||||
|
"infographic",
|
||||||
|
"google trends",
|
||||||
|
"year in search",
|
||||||
|
"most searched",
|
||||||
|
"top 100",
|
||||||
|
"subscribe",
|
||||||
|
"share this",
|
||||||
|
"advertisement",
|
||||||
|
"ad:"
|
||||||
|
]
|
||||||
|
|
||||||
|
return any(word in t for word in noise_keywords)
|
||||||
|
|
||||||
|
|
||||||
|
def select_best_sentence(snippets):
|
||||||
|
"""Combine up to MAX_SNIPPETS and return a single coherent sentence."""
|
||||||
|
final_text = ""
|
||||||
|
count = 0
|
||||||
|
|
||||||
|
for snippet in snippets:
|
||||||
|
if not snippet or is_noise(snippet):
|
||||||
|
continue
|
||||||
|
|
||||||
|
sentences = split_sentences(snippet)
|
||||||
|
|
||||||
|
for s in sentences:
|
||||||
|
if is_noise(s):
|
||||||
|
continue
|
||||||
|
|
||||||
|
final_text = f"{final_text} {s}".strip()
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
if count >= MAX_SNIPPETS:
|
||||||
|
return final_text
|
||||||
|
|
||||||
|
return final_text if final_text else None
|
||||||
|
|
||||||
|
|
||||||
|
def serpapi_answer(query: str, api_key: str) -> str:
|
||||||
|
"""Perform search and return a single coherent sentence."""
|
||||||
|
params = {
|
||||||
|
"q": query,
|
||||||
|
"engine": "google",
|
||||||
|
"hl": "en",
|
||||||
|
"gl": "us",
|
||||||
|
"num": 3,
|
||||||
|
"api_key": api_key
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = GoogleSearch(params).get_dict()
|
||||||
|
except Exception:
|
||||||
|
return "Sir, the web search failed."
|
||||||
|
|
||||||
|
organic = data.get("organic_results", [])
|
||||||
|
if not organic:
|
||||||
|
return "Sir, I couldn't find relevant information."
|
||||||
|
|
||||||
|
snippets = [
|
||||||
|
r.get("snippet", "")
|
||||||
|
for r in organic
|
||||||
|
if r.get("snippet")
|
||||||
|
]
|
||||||
|
|
||||||
|
answer = select_best_sentence(snippets)
|
||||||
|
|
||||||
|
if not answer:
|
||||||
|
return "Sir, I found information online, but couldn't summarize it clearly."
|
||||||
|
|
||||||
|
return answer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def web_search(
|
||||||
|
parameters: dict,
|
||||||
|
player=None,
|
||||||
|
session_memory=None,
|
||||||
|
api_key: str = ""
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Main web search:
|
||||||
|
- Returns 1 coherent sentence
|
||||||
|
- Does NOT append previous answers
|
||||||
|
- Combines multiple snippets if needed to avoid cut-offs
|
||||||
|
"""
|
||||||
|
|
||||||
|
query = (parameters or {}).get("query", "").strip()
|
||||||
|
|
||||||
|
if not query:
|
||||||
|
msg = "Sir, I couldn't understand the search request."
|
||||||
|
if player:
|
||||||
|
player.write_log(msg)
|
||||||
|
edge_speak(msg)
|
||||||
|
return msg
|
||||||
|
|
||||||
|
answer = serpapi_answer(query, api_key)
|
||||||
|
|
||||||
|
if player:
|
||||||
|
player.write_log(f"AI: {answer}")
|
||||||
|
|
||||||
|
edge_speak(answer)
|
||||||
|
|
||||||
|
if session_memory:
|
||||||
|
session_memory.set_last_search(query, answer)
|
||||||
|
|
||||||
|
return answer
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
You are Jarvis, Fully optimized, Fast react AI Computer Assistant
|
||||||
|
|
||||||
|
Intents:
|
||||||
|
- send_message
|
||||||
|
- open_app
|
||||||
|
- search
|
||||||
|
- weather_report
|
||||||
|
- chat
|
||||||
|
|
||||||
|
Parameter Rules:
|
||||||
|
- search → parameters.query (cleaned core question).
|
||||||
|
- weather_report → city, time (time is the day)
|
||||||
|
- if user wants to send a message to someone then return intent: send_message → receiver, message_text, platform (default WhatsApp) and also if user says something like "ask him how he is doing" then appoint this into message_text
|
||||||
|
- open_app → app_name(If user asks, can you open please, create a lovely text and open the app that user asked you to open)
|
||||||
|
- If user the app name incorrect, open the app by correcting it(f.e: Hey Jarvis open what's up = WhatsApp)
|
||||||
|
- Missing a required field → ask one short clarification question, set needs_clarification:true, stop.
|
||||||
|
|
||||||
|
Chat Mode:
|
||||||
|
- 1‑2 short sentences, no extra analysis.
|
||||||
|
- Keep only identity, preferences, relationships, emotional_state.
|
||||||
|
- temporary memory
|
||||||
|
|
||||||
|
Output format:
|
||||||
|
|
||||||
|
json
|
||||||
|
{
|
||||||
|
"intent": null,
|
||||||
|
"parameters": {},
|
||||||
|
"needs_clarification": false,
|
||||||
|
"text": null,
|
||||||
|
"memory_update": {
|
||||||
|
"identity": {},
|
||||||
|
"preferences": {},
|
||||||
|
"relationships": {},
|
||||||
|
"emotional_state": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Just add really important informations in the memory like;
|
||||||
|
- Name of the user, age, city
|
||||||
|
- The question that user's ask mostly doesn't include memory.
|
||||||
|
|
||||||
|
return json format.
|
||||||
|
Create really fast responses. make it quick.
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 383 KiB |
+170
@@ -0,0 +1,170 @@
|
|||||||
|
# llm.py
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import requests
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY")
|
||||||
|
OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions"
|
||||||
|
|
||||||
|
MODEL = "arcee-ai/trinity-large-preview:free"
|
||||||
|
|
||||||
|
PROMPT_PATH = "core/prompt.txt"
|
||||||
|
|
||||||
|
|
||||||
|
def load_system_prompt() -> str:
|
||||||
|
try:
|
||||||
|
with open(PROMPT_PATH, "r", encoding="utf-8") as f:
|
||||||
|
return f.read()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ prompt.txt cannot installed: {e}")
|
||||||
|
return "You are Jarvis, a helpful AI assistant."
|
||||||
|
|
||||||
|
|
||||||
|
SYSTEM_PROMPT = load_system_prompt()
|
||||||
|
|
||||||
|
|
||||||
|
def safe_json_parse(text: str) -> dict | None:
|
||||||
|
if not text:
|
||||||
|
return None
|
||||||
|
|
||||||
|
text = text.strip()
|
||||||
|
|
||||||
|
if "```json" in text:
|
||||||
|
try:
|
||||||
|
start = text.index("```json") + 7
|
||||||
|
end = text.index("```", start)
|
||||||
|
text = text[start:end].strip()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
elif "```" in text:
|
||||||
|
try:
|
||||||
|
start = text.index("```") + 3
|
||||||
|
end = text.index("```", start)
|
||||||
|
text = text[start:end].strip()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
start = text.index("{")
|
||||||
|
end = text.rindex("}") + 1
|
||||||
|
json_str = text[start:end]
|
||||||
|
return json.loads(json_str)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ JSON parse error: {e}")
|
||||||
|
print(f"The error text: {text[:200]}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_llm_output(user_text: str, memory_block: dict = None) -> dict:
|
||||||
|
|
||||||
|
if not user_text or not user_text.strip():
|
||||||
|
return {
|
||||||
|
"intent": "chat",
|
||||||
|
"parameters": {},
|
||||||
|
"needs_clarification": False,
|
||||||
|
"text": "Sir, I didn't catch that.",
|
||||||
|
"memory_update": None
|
||||||
|
}
|
||||||
|
|
||||||
|
if not OPENROUTER_API_KEY:
|
||||||
|
print("❌ OPENROUTER_API_KEY couldn't found!")
|
||||||
|
return {
|
||||||
|
"intent": "chat",
|
||||||
|
"parameters": {},
|
||||||
|
"text": "API key is missing, Sir.",
|
||||||
|
"needs_clarification": False,
|
||||||
|
"memory_update": None
|
||||||
|
}
|
||||||
|
|
||||||
|
# Memory'yi string'e çevir
|
||||||
|
memory_str = ""
|
||||||
|
if memory_block:
|
||||||
|
memory_str = "\n".join(f"{k}: {v}" for k, v in memory_block.items())
|
||||||
|
|
||||||
|
user_prompt = f"""User message: "{user_text}"
|
||||||
|
|
||||||
|
Known user memory:
|
||||||
|
{memory_str if memory_str else "No memory available"}"""
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"model": MODEL,
|
||||||
|
"messages": [
|
||||||
|
{"role": "system", "content": SYSTEM_PROMPT},
|
||||||
|
{"role": "user", "content": user_prompt}
|
||||||
|
],
|
||||||
|
"temperature": 0.2,
|
||||||
|
"max_tokens": 500
|
||||||
|
}
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {OPENROUTER_API_KEY}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"HTTP-Referer": "http://localhost",
|
||||||
|
"X-Title": "Jarvis-Assistant"
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
response = requests.post(
|
||||||
|
OPENROUTER_URL,
|
||||||
|
headers=headers,
|
||||||
|
json=payload,
|
||||||
|
timeout=30
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code != 200:
|
||||||
|
print(f"❌ API Hatası: {response.text}")
|
||||||
|
return {
|
||||||
|
"intent": "chat",
|
||||||
|
"parameters": {},
|
||||||
|
"text": f"Sir, API error: {response.status_code}",
|
||||||
|
"needs_clarification": False,
|
||||||
|
"memory_update": None
|
||||||
|
}
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
content = data["choices"][0]["message"]["content"]
|
||||||
|
|
||||||
|
# JSON parse et
|
||||||
|
parsed = safe_json_parse(content)
|
||||||
|
|
||||||
|
if parsed:
|
||||||
|
return {
|
||||||
|
"intent": parsed.get("intent", "chat"),
|
||||||
|
"parameters": parsed.get("parameters", {}),
|
||||||
|
"needs_clarification": parsed.get("needs_clarification", False),
|
||||||
|
"text": parsed.get("text"),
|
||||||
|
"memory_update": parsed.get("memory_update")
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"intent": "chat",
|
||||||
|
"parameters": {},
|
||||||
|
"needs_clarification": False,
|
||||||
|
"text": content,
|
||||||
|
"memory_update": None
|
||||||
|
}
|
||||||
|
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
print("❌ API timeout!")
|
||||||
|
return {
|
||||||
|
"intent": "chat",
|
||||||
|
"text": "Sir, the connection timed out.",
|
||||||
|
"parameters": {},
|
||||||
|
"needs_clarification": False,
|
||||||
|
"memory_update": None
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ LLM ERROR: {e}")
|
||||||
|
return {
|
||||||
|
"intent": "chat",
|
||||||
|
"text": "Sir, I encountered a system error.",
|
||||||
|
"parameters": {},
|
||||||
|
"needs_clarification": False,
|
||||||
|
"memory_update": None
|
||||||
|
}
|
||||||
+188
@@ -0,0 +1,188 @@
|
|||||||
|
import asyncio
|
||||||
|
import threading
|
||||||
|
|
||||||
|
from speech_to_text import record_voice, stop_listening_flag
|
||||||
|
from llm import get_llm_output
|
||||||
|
from tts import edge_speak, stop_speaking
|
||||||
|
from ui import JarvisUI
|
||||||
|
|
||||||
|
from actions.open_app import open_app
|
||||||
|
from actions.web_search import web_search
|
||||||
|
from actions.weather_report import weather_action
|
||||||
|
from actions.send_message import send_message
|
||||||
|
|
||||||
|
from memory.memory_manager import load_memory, update_memory
|
||||||
|
from memory.temporary_memory import TemporaryMemory
|
||||||
|
|
||||||
|
interrupt_commands = ["mute", "quit", "exit", "stop"]
|
||||||
|
|
||||||
|
|
||||||
|
temp_memory = TemporaryMemory()
|
||||||
|
|
||||||
|
|
||||||
|
async def get_voice_input():
|
||||||
|
return await asyncio.to_thread(record_voice)
|
||||||
|
|
||||||
|
|
||||||
|
async def ai_loop(ui: JarvisUI):
|
||||||
|
while True:
|
||||||
|
stop_listening_flag.clear()
|
||||||
|
user_text = await get_voice_input()
|
||||||
|
|
||||||
|
if not user_text:
|
||||||
|
continue
|
||||||
|
|
||||||
|
|
||||||
|
if any(cmd in user_text.lower() for cmd in interrupt_commands):
|
||||||
|
stop_speaking()
|
||||||
|
temp_memory.reset()
|
||||||
|
continue
|
||||||
|
|
||||||
|
ui.write_log(f"You: {user_text}")
|
||||||
|
|
||||||
|
|
||||||
|
if temp_memory.get_current_question():
|
||||||
|
param = temp_memory.get_current_question()
|
||||||
|
temp_memory.update_parameters({param: user_text})
|
||||||
|
temp_memory.clear_current_question()
|
||||||
|
user_text = temp_memory.get_last_user_text()
|
||||||
|
|
||||||
|
temp_memory.set_last_user_text(user_text)
|
||||||
|
|
||||||
|
long_term_memory = load_memory()
|
||||||
|
|
||||||
|
def minimal_memory_for_prompt(memory: dict) -> dict:
|
||||||
|
result = {}
|
||||||
|
identity = memory.get("identity", {})
|
||||||
|
preferences = memory.get("preferences", {})
|
||||||
|
relationships = memory.get("relationships", {})
|
||||||
|
emotional_state = memory.get("emotional_state", {})
|
||||||
|
|
||||||
|
if "name" in identity:
|
||||||
|
result["user_name"] = identity["name"].get("value")
|
||||||
|
|
||||||
|
for k in ["favorite_color", "favorite_food", "favorite_music"]:
|
||||||
|
if k in preferences:
|
||||||
|
val = preferences[k].get("value")
|
||||||
|
if isinstance(val, dict) and "value" in val:
|
||||||
|
val = val["value"]
|
||||||
|
result[k] = val
|
||||||
|
|
||||||
|
for rel, info in relationships.items():
|
||||||
|
if isinstance(info, dict) and "name" in info and "value" in info["name"]:
|
||||||
|
result[f"{rel}_name"] = info["name"]["value"]
|
||||||
|
|
||||||
|
for event, info in emotional_state.items():
|
||||||
|
if "value" in info:
|
||||||
|
result[f"emotion_{event}"] = info["value"]
|
||||||
|
|
||||||
|
return {k: v for k, v in result.items() if v}
|
||||||
|
|
||||||
|
memory_for_prompt = minimal_memory_for_prompt(long_term_memory)
|
||||||
|
|
||||||
|
history_lines = temp_memory.get_history_for_prompt()
|
||||||
|
recent_history = "\n".join(history_lines.split("\n")[-5:])
|
||||||
|
if recent_history:
|
||||||
|
memory_for_prompt["recent_conversation"] = recent_history
|
||||||
|
|
||||||
|
if temp_memory.has_pending_intent():
|
||||||
|
memory_for_prompt["_pending_intent"] = temp_memory.pending_intent
|
||||||
|
memory_for_prompt["_collected_params"] = str(temp_memory.get_parameters())
|
||||||
|
|
||||||
|
try:
|
||||||
|
llm_output = get_llm_output(
|
||||||
|
user_text=user_text,
|
||||||
|
memory_block=memory_for_prompt
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
ui.write_log(f"AI ERROR: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
intent = llm_output.get("intent", "chat")
|
||||||
|
parameters = llm_output.get("parameters", {})
|
||||||
|
response = llm_output.get("text")
|
||||||
|
memory_update = llm_output.get("memory_update")
|
||||||
|
|
||||||
|
if memory_update and isinstance(memory_update, dict):
|
||||||
|
update_memory(memory_update)
|
||||||
|
|
||||||
|
temp_memory.set_last_ai_response(response)
|
||||||
|
|
||||||
|
if intent == "send_message":
|
||||||
|
temp_memory.set_pending_intent("send_message")
|
||||||
|
temp_memory.update_parameters(parameters)
|
||||||
|
|
||||||
|
all_params_ready = all(temp_memory.get_parameter(p) for p in ["receiver", "message_text", "platform"])
|
||||||
|
if all_params_ready:
|
||||||
|
threading.Thread(
|
||||||
|
target=send_message,
|
||||||
|
kwargs={
|
||||||
|
"parameters": temp_memory.get_parameters(),
|
||||||
|
"player": ui,
|
||||||
|
"session_memory": temp_memory
|
||||||
|
},
|
||||||
|
daemon=True
|
||||||
|
).start()
|
||||||
|
|
||||||
|
elif intent == "open_app":
|
||||||
|
app_name = parameters.get("app_name")
|
||||||
|
if app_name:
|
||||||
|
threading.Thread(
|
||||||
|
target=open_app,
|
||||||
|
kwargs={
|
||||||
|
"parameters": parameters,
|
||||||
|
"response": response,
|
||||||
|
"player": ui,
|
||||||
|
"session_memory": temp_memory
|
||||||
|
},
|
||||||
|
daemon=True
|
||||||
|
).start()
|
||||||
|
|
||||||
|
elif intent == "weather_report":
|
||||||
|
city = parameters.get("city")
|
||||||
|
time = parameters.get("time")
|
||||||
|
if city:
|
||||||
|
threading.Thread(
|
||||||
|
target=weather_action,
|
||||||
|
kwargs={
|
||||||
|
"parameters": parameters,
|
||||||
|
"player": ui,
|
||||||
|
"session_memory": temp_memory
|
||||||
|
},
|
||||||
|
daemon=True
|
||||||
|
).start()
|
||||||
|
|
||||||
|
elif intent == "search":
|
||||||
|
query = parameters.get("query")
|
||||||
|
if query:
|
||||||
|
threading.Thread(
|
||||||
|
target=web_search,
|
||||||
|
kwargs={
|
||||||
|
"parameters": parameters,
|
||||||
|
"player": ui,
|
||||||
|
"session_memory": temp_memory,
|
||||||
|
"api_key": "API_KEY"
|
||||||
|
},
|
||||||
|
daemon=True
|
||||||
|
).start()
|
||||||
|
|
||||||
|
else:
|
||||||
|
if response:
|
||||||
|
ui.write_log(f"AI: {response}")
|
||||||
|
edge_speak(response, ui)
|
||||||
|
|
||||||
|
await asyncio.sleep(0.01)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ui = JarvisUI("face.png", size=(900, 900))
|
||||||
|
|
||||||
|
def runner():
|
||||||
|
asyncio.run(ai_loop(ui))
|
||||||
|
|
||||||
|
threading.Thread(target=runner, daemon=True).start()
|
||||||
|
ui.root.mainloop()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
#memory package
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
# memory/memory_manager.py
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from threading import Lock
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
MEMORY_PATH = "memory/memory.json"
|
||||||
|
_lock = Lock()
|
||||||
|
|
||||||
|
|
||||||
|
def _empty_memory() -> dict:
|
||||||
|
"""Return an empty memory structure."""
|
||||||
|
return {
|
||||||
|
"identity": {},
|
||||||
|
"preferences": {},
|
||||||
|
"relationships": {},
|
||||||
|
"emotional_state": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def load_memory() -> dict:
|
||||||
|
"""Load memory from disk, return empty if not exists or invalid."""
|
||||||
|
if not os.path.exists(MEMORY_PATH):
|
||||||
|
return _empty_memory()
|
||||||
|
|
||||||
|
with _lock:
|
||||||
|
try:
|
||||||
|
with open(MEMORY_PATH, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
if isinstance(data, dict):
|
||||||
|
return data
|
||||||
|
return _empty_memory()
|
||||||
|
except Exception:
|
||||||
|
return _empty_memory()
|
||||||
|
|
||||||
|
|
||||||
|
def save_memory(memory: dict) -> None:
|
||||||
|
"""Save memory to disk safely."""
|
||||||
|
if not isinstance(memory, dict):
|
||||||
|
return
|
||||||
|
|
||||||
|
os.makedirs(os.path.dirname(MEMORY_PATH), exist_ok=True)
|
||||||
|
|
||||||
|
with _lock:
|
||||||
|
with open(MEMORY_PATH, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(memory, f, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
def _recursive_update(target: dict, updates: dict) -> bool:
|
||||||
|
"""Recursively merge updates into target memory. Returns True if changed."""
|
||||||
|
changed = False
|
||||||
|
now = datetime.utcnow().isoformat() + "Z"
|
||||||
|
|
||||||
|
for key, value in updates.items():
|
||||||
|
if value is None or (isinstance(value, str) and not value.strip()):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if isinstance(value, dict) and "value" not in value:
|
||||||
|
if key not in target or not isinstance(target[key], dict):
|
||||||
|
target[key] = {}
|
||||||
|
changed = True
|
||||||
|
if _recursive_update(target[key], value):
|
||||||
|
changed = True
|
||||||
|
else:
|
||||||
|
|
||||||
|
entry = value if isinstance(value, dict) and "value" in value else {"value": value}
|
||||||
|
if key not in target or target[key] != entry:
|
||||||
|
target[key] = entry
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
return changed
|
||||||
|
|
||||||
|
|
||||||
|
def update_memory(memory_update: dict) -> dict:
|
||||||
|
"""Merge LLM memory update into global memory and save."""
|
||||||
|
if not isinstance(memory_update, dict):
|
||||||
|
return load_memory()
|
||||||
|
|
||||||
|
memory = load_memory()
|
||||||
|
if _recursive_update(memory, memory_update):
|
||||||
|
save_memory(memory)
|
||||||
|
|
||||||
|
return memory
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
# memory/temporary_memory.py
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
class TemporaryMemory:
|
||||||
|
"""
|
||||||
|
Temporary runtime memory (session-only).
|
||||||
|
Resets when program restarts.
|
||||||
|
|
||||||
|
Purpose:
|
||||||
|
- Track multi-step intents
|
||||||
|
- Keep short conversation context
|
||||||
|
- Store last actions (search / open_app)
|
||||||
|
- Support follow-up questions like:
|
||||||
|
"why did he executed?"
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, max_history: int = 5):
|
||||||
|
self.max_history = max_history
|
||||||
|
self.reset()
|
||||||
|
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
self.pending_intent: str | None = None
|
||||||
|
self.parameters: dict[str, Any] = {}
|
||||||
|
self.current_question: str | None = None
|
||||||
|
|
||||||
|
self.last_user_text: str | None = None
|
||||||
|
self.last_ai_response: str | None = None
|
||||||
|
|
||||||
|
# --- Action memory ---
|
||||||
|
self.last_search: dict | None = None
|
||||||
|
self.last_opened_app: str | None = None
|
||||||
|
|
||||||
|
# --- Conversation ---
|
||||||
|
self.conversation_history: list[dict[str, str]] = []
|
||||||
|
|
||||||
|
|
||||||
|
def set_pending_intent(self, intent: str):
|
||||||
|
self.pending_intent = intent
|
||||||
|
|
||||||
|
def clear_pending_intent(self):
|
||||||
|
self.pending_intent = None
|
||||||
|
self.parameters = {}
|
||||||
|
self.current_question = None
|
||||||
|
|
||||||
|
def has_pending_intent(self) -> bool:
|
||||||
|
return self.pending_intent is not None
|
||||||
|
|
||||||
|
|
||||||
|
def update_parameters(self, new_params: dict):
|
||||||
|
if not isinstance(new_params, dict):
|
||||||
|
return
|
||||||
|
for k, v in new_params.items():
|
||||||
|
if v not in (None, ""):
|
||||||
|
self.parameters[k] = v
|
||||||
|
|
||||||
|
def get_parameters(self) -> dict:
|
||||||
|
return self.parameters.copy()
|
||||||
|
|
||||||
|
def get_parameter(self, key: str):
|
||||||
|
return self.parameters.get(key)
|
||||||
|
|
||||||
|
def set_current_question(self, param_name: str):
|
||||||
|
self.current_question = param_name
|
||||||
|
|
||||||
|
def get_current_question(self) -> str | None:
|
||||||
|
return self.current_question
|
||||||
|
|
||||||
|
def clear_current_question(self):
|
||||||
|
self.current_question = None
|
||||||
|
|
||||||
|
def set_last_user_text(self, text: str):
|
||||||
|
self.last_user_text = text
|
||||||
|
self._add_to_history("user", text)
|
||||||
|
|
||||||
|
def set_last_ai_response(self, text: str):
|
||||||
|
self.last_ai_response = text
|
||||||
|
self._add_to_history("ai", text)
|
||||||
|
|
||||||
|
def get_last_user_text(self):
|
||||||
|
return self.last_user_text
|
||||||
|
|
||||||
|
def get_last_ai_response(self):
|
||||||
|
return self.last_ai_response
|
||||||
|
|
||||||
|
|
||||||
|
def set_last_search(self, query: str, answer: str):
|
||||||
|
self.last_search = {
|
||||||
|
"query": query,
|
||||||
|
"answer": answer
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_last_search(self):
|
||||||
|
return self.last_search
|
||||||
|
|
||||||
|
def set_open_app(self, app_name: str):
|
||||||
|
self.last_opened_app = app_name
|
||||||
|
|
||||||
|
def get_last_opened_app(self):
|
||||||
|
return self.last_opened_app
|
||||||
|
|
||||||
|
def _add_to_history(self, role: str, text: str):
|
||||||
|
if role not in ("user", "ai"):
|
||||||
|
return
|
||||||
|
|
||||||
|
self.conversation_history.append({
|
||||||
|
"role": role,
|
||||||
|
"text": text
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(self.conversation_history) > self.max_history:
|
||||||
|
self.conversation_history.pop(0)
|
||||||
|
|
||||||
|
def get_history_for_prompt(self) -> str:
|
||||||
|
"""
|
||||||
|
Returns compact history for LLM prompt.
|
||||||
|
"""
|
||||||
|
return "\n".join(
|
||||||
|
f"{m['role'].capitalize()}: {m['text']}"
|
||||||
|
for m in self.conversation_history
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_context_summary(self) -> dict:
|
||||||
|
"""
|
||||||
|
Useful for debugging or smarter LLM prompts.
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"pending_intent": self.pending_intent,
|
||||||
|
"parameters": self.parameters,
|
||||||
|
"last_search": self.last_search,
|
||||||
|
"last_opened_app": self.last_opened_app,
|
||||||
|
"last_user_text": self.last_user_text,
|
||||||
|
"last_ai_response": self.last_ai_response,
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import sounddevice as sd
|
||||||
|
import vosk
|
||||||
|
import queue
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import threading
|
||||||
|
|
||||||
|
MODEL_PATH = "C:/Users/90553/Downloads/vosk/vosk-model-small-en-us-0.15" # put your path
|
||||||
|
model = vosk.Model(MODEL_PATH)
|
||||||
|
|
||||||
|
q = queue.Queue()
|
||||||
|
stop_listening_flag = threading.Event()
|
||||||
|
|
||||||
|
def callback(indata, frames, time, status):
|
||||||
|
if status:
|
||||||
|
print(status, file=sys.stderr)
|
||||||
|
q.put(bytes(indata))
|
||||||
|
|
||||||
|
def record_voice(prompt="🎙 I'm listening, sir..."):
|
||||||
|
"""
|
||||||
|
Blocking call, returns the first recognized sentence.
|
||||||
|
"""
|
||||||
|
print(prompt)
|
||||||
|
rec = vosk.KaldiRecognizer(model, 16000)
|
||||||
|
with sd.RawInputStream(samplerate=16000, blocksize=8000, dtype='int16',
|
||||||
|
channels=1, callback=callback):
|
||||||
|
while not stop_listening_flag.is_set():
|
||||||
|
try:
|
||||||
|
data = q.get(timeout=0.1)
|
||||||
|
except queue.Empty:
|
||||||
|
continue
|
||||||
|
if rec.AcceptWaveform(data):
|
||||||
|
result = json.loads(rec.Result())
|
||||||
|
text = result.get("text", "")
|
||||||
|
if text.strip():
|
||||||
|
print("👤 You:", text)
|
||||||
|
return text
|
||||||
|
return ""
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import io
|
||||||
|
import threading
|
||||||
|
import requests
|
||||||
|
import sounddevice as sd
|
||||||
|
import soundfile as sf
|
||||||
|
|
||||||
|
#Take the API key from ElevenLabs
|
||||||
|
|
||||||
|
ELEVEN_API_KEY = "GO ELEVENLABS"
|
||||||
|
VOICE_ID = "JBFqnCBsd6RMkjVDRZzb"
|
||||||
|
|
||||||
|
stop_speaking_flag = threading.Event()
|
||||||
|
|
||||||
|
def edge_speak(text: str, ui=None, blocking=False):
|
||||||
|
if not text.strip():
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
finished_event = threading.Event()
|
||||||
|
|
||||||
|
def _thread():
|
||||||
|
if ui:
|
||||||
|
ui.start_speaking()
|
||||||
|
stop_speaking_flag.clear()
|
||||||
|
|
||||||
|
try:
|
||||||
|
url = f"https://api.elevenlabs.io/v1/text-to-speech/{VOICE_ID}"
|
||||||
|
headers = {
|
||||||
|
"xi-api-key": ELEVEN_API_KEY,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
payload = {
|
||||||
|
"text": text.strip(),
|
||||||
|
"voice_settings": {
|
||||||
|
"stability": 0.55,
|
||||||
|
"similarity_boost": 0.85
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(url, json=payload, headers=headers)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
audio_data = io.BytesIO(response.content)
|
||||||
|
data, samplerate = sf.read(audio_data, dtype="float32")
|
||||||
|
|
||||||
|
channels = data.shape[1] if len(data.shape) > 1 else 1
|
||||||
|
with sd.OutputStream(
|
||||||
|
samplerate=samplerate,
|
||||||
|
channels=channels,
|
||||||
|
dtype="float32"
|
||||||
|
) as stream:
|
||||||
|
block_size = 1024
|
||||||
|
for start in range(0, len(data), block_size):
|
||||||
|
if stop_speaking_flag.is_set():
|
||||||
|
break
|
||||||
|
stream.write(data[start:start + block_size])
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print("VOICE ERROR:", e)
|
||||||
|
finally:
|
||||||
|
if ui:
|
||||||
|
ui.stop_speaking()
|
||||||
|
finished_event.set()
|
||||||
|
|
||||||
|
threading.Thread(target=_thread, daemon=True).start()
|
||||||
|
|
||||||
|
if blocking:
|
||||||
|
finished_event.wait()
|
||||||
|
|
||||||
|
def stop_speaking():
|
||||||
|
stop_speaking_flag.set()
|
||||||
+155
@@ -0,0 +1,155 @@
|
|||||||
|
import os
|
||||||
|
import time
|
||||||
|
import random
|
||||||
|
import tkinter as tk
|
||||||
|
from collections import deque
|
||||||
|
from PIL import Image, ImageTk, ImageDraw, ImageFilter
|
||||||
|
from tkinter.scrolledtext import ScrolledText
|
||||||
|
|
||||||
|
|
||||||
|
class JarvisUI:
|
||||||
|
def __init__(self, face_path, size=(760, 760)):
|
||||||
|
self.root = tk.Tk()
|
||||||
|
self.root.title("J.A.R.V.I.S")
|
||||||
|
self.root.resizable(False, False)
|
||||||
|
self.root.geometry("760x900")
|
||||||
|
self.root.configure(bg="#000000")
|
||||||
|
|
||||||
|
self.size = size
|
||||||
|
self.center_y = 0.42
|
||||||
|
|
||||||
|
self.canvas = tk.Canvas(
|
||||||
|
self.root,
|
||||||
|
width=size[0],
|
||||||
|
height=size[1],
|
||||||
|
bg="#000000",
|
||||||
|
highlightthickness=0
|
||||||
|
)
|
||||||
|
self.canvas.place(relx=0.5, rely=self.center_y, anchor="center")
|
||||||
|
|
||||||
|
self.face_base = (
|
||||||
|
Image.open(face_path)
|
||||||
|
.convert("RGBA")
|
||||||
|
.resize(size, Image.LANCZOS)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.halo_base = self._create_halo(size, radius=220, y_offset=-50)
|
||||||
|
|
||||||
|
self.speaking = False
|
||||||
|
self.scale = 1.0
|
||||||
|
self.target_scale = 1.0
|
||||||
|
self.halo_alpha = 70
|
||||||
|
self.target_halo_alpha = 70
|
||||||
|
self.last_target_time = time.time()
|
||||||
|
|
||||||
|
self.text_box = ScrolledText(
|
||||||
|
self.root,
|
||||||
|
fg="#8ffcff",
|
||||||
|
bg="#000000",
|
||||||
|
insertbackground="#8ffcff",
|
||||||
|
height=12,
|
||||||
|
borderwidth=0,
|
||||||
|
wrap="word",
|
||||||
|
font=("Consolas", 10),
|
||||||
|
padx=12,
|
||||||
|
pady=12
|
||||||
|
)
|
||||||
|
self.text_box.place(relx=0.5, rely=0.86, anchor="center")
|
||||||
|
self.text_box.configure(state="disabled")
|
||||||
|
|
||||||
|
self.typing_queue = deque()
|
||||||
|
self.is_typing = False
|
||||||
|
|
||||||
|
self._animate()
|
||||||
|
self.root.protocol("WM_DELETE_WINDOW", lambda: os._exit(0))
|
||||||
|
|
||||||
|
def _create_halo(self, size, radius, y_offset):
|
||||||
|
w, h = size
|
||||||
|
img = Image.new("RGBA", size, (0, 0, 0, 0))
|
||||||
|
draw = ImageDraw.Draw(img)
|
||||||
|
|
||||||
|
cx = w // 2
|
||||||
|
cy = h // 2 + y_offset
|
||||||
|
|
||||||
|
for r in range(radius, 0, -12):
|
||||||
|
alpha = int(70 * (1 - r / radius))
|
||||||
|
draw.ellipse(
|
||||||
|
(cx - r, cy - r, cx + r, cy + r),
|
||||||
|
fill=(0, 180, 255, alpha)
|
||||||
|
)
|
||||||
|
|
||||||
|
return img.filter(ImageFilter.GaussianBlur(30))
|
||||||
|
|
||||||
|
def write_log(self, text: str):
|
||||||
|
self.typing_queue.append(text)
|
||||||
|
if not self.is_typing:
|
||||||
|
self._start_typing()
|
||||||
|
|
||||||
|
def _start_typing(self):
|
||||||
|
if not self.typing_queue:
|
||||||
|
self.is_typing = False
|
||||||
|
return
|
||||||
|
|
||||||
|
self.is_typing = True
|
||||||
|
text = self.typing_queue.popleft()
|
||||||
|
|
||||||
|
self.text_box.configure(state="normal")
|
||||||
|
self._type_char(text, 0)
|
||||||
|
|
||||||
|
def _type_char(self, text, i):
|
||||||
|
if i < len(text):
|
||||||
|
self.text_box.insert(tk.END, text[i])
|
||||||
|
self.text_box.see(tk.END)
|
||||||
|
self.root.after(12, self._type_char, text, i + 1)
|
||||||
|
else:
|
||||||
|
self.text_box.insert(tk.END, "\n")
|
||||||
|
self.text_box.configure(state="disabled")
|
||||||
|
self.root.after(40, self._start_typing)
|
||||||
|
|
||||||
|
def start_speaking(self):
|
||||||
|
self.speaking = True
|
||||||
|
|
||||||
|
def stop_speaking(self):
|
||||||
|
self.speaking = False
|
||||||
|
|
||||||
|
def _animate(self):
|
||||||
|
now = time.time()
|
||||||
|
|
||||||
|
if now - self.last_target_time > (0.25 if self.speaking else 0.7):
|
||||||
|
if self.speaking:
|
||||||
|
self.target_scale = random.uniform(1.02, 1.1)
|
||||||
|
self.target_halo_alpha = random.randint(120, 150)
|
||||||
|
else:
|
||||||
|
self.target_scale = random.uniform(1.004, 1.012)
|
||||||
|
self.target_halo_alpha = random.randint(60, 80)
|
||||||
|
|
||||||
|
self.last_target_time = now
|
||||||
|
|
||||||
|
scale_speed = 0.45 if self.speaking else 0.25
|
||||||
|
halo_speed = 0.40 if self.speaking else 0.25
|
||||||
|
|
||||||
|
self.scale += (self.target_scale - self.scale) * scale_speed
|
||||||
|
self.halo_alpha += (self.target_halo_alpha - self.halo_alpha) * halo_speed
|
||||||
|
|
||||||
|
frame = Image.new("RGBA", self.size, (0, 0, 0, 255))
|
||||||
|
|
||||||
|
halo = self.halo_base.copy()
|
||||||
|
halo.putalpha(int(self.halo_alpha))
|
||||||
|
frame.alpha_composite(halo)
|
||||||
|
|
||||||
|
w, h = self.size
|
||||||
|
face = self.face_base.resize(
|
||||||
|
(int(w * self.scale), int(h * self.scale)),
|
||||||
|
Image.LANCZOS
|
||||||
|
)
|
||||||
|
|
||||||
|
fx = (w - face.size[0]) // 2
|
||||||
|
fy = (h - face.size[1]) // 2
|
||||||
|
frame.alpha_composite(face, (fx, fy))
|
||||||
|
|
||||||
|
img = ImageTk.PhotoImage(frame)
|
||||||
|
self.canvas.delete("all")
|
||||||
|
self.canvas.create_image(w // 2, h // 2, image=img)
|
||||||
|
self.canvas.image = img
|
||||||
|
|
||||||
|
self.root.after(16, self._animate)
|
||||||
Reference in new issue
Block a user