diff --git a/MarkX/README.txt b/MarkX/README.txt new file mode 100644 index 0000000..e2ab4e8 --- /dev/null +++ b/MarkX/README.txt @@ -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! diff --git a/MarkX/actions/aircraft_report.py b/MarkX/actions/aircraft_report.py new file mode 100644 index 0000000..ae1e1db --- /dev/null +++ b/MarkX/actions/aircraft_report.py @@ -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 \ No newline at end of file diff --git a/MarkX/actions/open_app.py b/MarkX/actions/open_app.py new file mode 100644 index 0000000..2913d70 --- /dev/null +++ b/MarkX/actions/open_app.py @@ -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 diff --git a/MarkX/actions/send_message.py b/MarkX/actions/send_message.py new file mode 100644 index 0000000..d2a44ff --- /dev/null +++ b/MarkX/actions/send_message.py @@ -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 diff --git a/MarkX/actions/weather_report.py b/MarkX/actions/weather_report.py new file mode 100644 index 0000000..e499fb1 --- /dev/null +++ b/MarkX/actions/weather_report.py @@ -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 diff --git a/MarkX/actions/web_search.py b/MarkX/actions/web_search.py new file mode 100644 index 0000000..f0e1a0e --- /dev/null +++ b/MarkX/actions/web_search.py @@ -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 diff --git a/MarkX/core/prompt.txt b/MarkX/core/prompt.txt new file mode 100644 index 0000000..803d838 --- /dev/null +++ b/MarkX/core/prompt.txt @@ -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. \ No newline at end of file diff --git a/MarkX/face.png b/MarkX/face.png new file mode 100644 index 0000000..e87622b Binary files /dev/null and b/MarkX/face.png differ diff --git a/MarkX/llm.py b/MarkX/llm.py new file mode 100644 index 0000000..1f6d106 --- /dev/null +++ b/MarkX/llm.py @@ -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 + } \ No newline at end of file diff --git a/MarkX/main.py b/MarkX/main.py new file mode 100644 index 0000000..7cb033a --- /dev/null +++ b/MarkX/main.py @@ -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() \ No newline at end of file diff --git a/MarkX/memory/__init__.py b/MarkX/memory/__init__.py new file mode 100644 index 0000000..452b936 --- /dev/null +++ b/MarkX/memory/__init__.py @@ -0,0 +1 @@ +#memory package \ No newline at end of file diff --git a/MarkX/memory/memory_manager.py b/MarkX/memory/memory_manager.py new file mode 100644 index 0000000..1ae6c9c --- /dev/null +++ b/MarkX/memory/memory_manager.py @@ -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 diff --git a/MarkX/memory/temporary_memory.py b/MarkX/memory/temporary_memory.py new file mode 100644 index 0000000..46afd81 --- /dev/null +++ b/MarkX/memory/temporary_memory.py @@ -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, + } diff --git a/MarkX/speech_to_text.py b/MarkX/speech_to_text.py new file mode 100644 index 0000000..aca896a --- /dev/null +++ b/MarkX/speech_to_text.py @@ -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 "" diff --git a/MarkX/tts.py b/MarkX/tts.py new file mode 100644 index 0000000..bb724b9 --- /dev/null +++ b/MarkX/tts.py @@ -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() diff --git a/MarkX/ui.py b/MarkX/ui.py new file mode 100644 index 0000000..ebe446f --- /dev/null +++ b/MarkX/ui.py @@ -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)