Files
2026-05-10 17:23:10 +03:00

887 lines
40 KiB
Python

import asyncio
import threading
import json
import sys
import traceback
from pathlib import Path
import sounddevice as sd
from google import genai
from google.genai import types
from ui import JarvisUI
from memory.memory_manager import (
load_memory, update_memory, format_memory_for_prompt,
should_extract_memory, extract_memory
)
from actions.file_processor import file_processor
from actions.flight_finder import flight_finder
from actions.open_app import open_app
from actions.weather_report import weather_action
from actions.send_message import send_message
from actions.reminder import reminder
from actions.computer_settings import computer_settings
from actions.screen_processor import screen_process
from actions.youtube_video import youtube_video
from actions.desktop import desktop_control
from actions.browser_control import browser_control
from actions.file_controller import file_controller
from actions.code_helper import code_helper
from actions.dev_agent import dev_agent
from actions.web_search import web_search as web_search_action
from actions.computer_control import computer_control
from actions.game_updater import game_updater
def get_base_dir():
if getattr(sys, "frozen", False):
return Path(sys.executable).parent
return Path(__file__).resolve().parent
BASE_DIR = get_base_dir()
API_CONFIG_PATH = BASE_DIR / "config" / "api_keys.json"
PROMPT_PATH = BASE_DIR / "core" / "prompt.txt"
LIVE_MODEL = "models/gemini-2.5-flash-native-audio-preview-12-2025"
CHANNELS = 1
SEND_SAMPLE_RATE = 16000
RECEIVE_SAMPLE_RATE = 24000
CHUNK_SIZE = 1024
def _get_api_key() -> str:
with open(API_CONFIG_PATH, "r", encoding="utf-8") as f:
return json.load(f)["gemini_api_key"]
def _load_system_prompt() -> str:
try:
return PROMPT_PATH.read_text(encoding="utf-8")
except Exception:
return (
"You are JARVIS, Tony Stark's AI assistant. "
"Be concise, direct, and always use the provided tools to complete tasks. "
"Never simulate or guess results — always call the appropriate tool."
)
_last_memory_input = ""
def _update_memory_async(user_text: str, jarvis_text: str) -> None:
global _last_memory_input
user_text = (user_text or "").strip()
jarvis_text = (jarvis_text or "").strip()
if len(user_text) < 5 or user_text == _last_memory_input:
return
_last_memory_input = user_text
try:
api_key = _get_api_key()
if not should_extract_memory(user_text, jarvis_text, api_key):
return
data = extract_memory(user_text, jarvis_text, api_key)
if data:
update_memory(data)
print(f"[Memory] ✅ {list(data.keys())}")
except Exception as e:
if "429" not in str(e):
print(f"[Memory] ⚠️ {e}")
TOOL_DECLARATIONS = [
{
"name": "open_app",
"description": (
"Opens any application on the Windows computer. "
"Use this whenever the user asks to open, launch, or start any app, "
"website, or program. Always call this tool — never just say you opened it."
),
"parameters": {
"type": "OBJECT",
"properties": {
"app_name": {
"type": "STRING",
"description": "Exact name of the application (e.g. 'WhatsApp', 'Chrome', 'Spotify')"
}
},
"required": ["app_name"]
}
},
{
"name": "web_search",
"description": "Searches the web for any information.",
"parameters": {
"type": "OBJECT",
"properties": {
"query": {"type": "STRING", "description": "Search query"},
"mode": {"type": "STRING", "description": "search (default) or compare"},
"items": {"type": "ARRAY", "items": {"type": "STRING"}, "description": "Items to compare"},
"aspect": {"type": "STRING", "description": "price | specs | reviews"}
},
"required": ["query"]
}
},
{
"name": "weather_report",
"description": "Gives the weather report to user",
"parameters": {
"type": "OBJECT",
"properties": {
"city": {"type": "STRING", "description": "City name"}
},
"required": ["city"]
}
},
{
"name": "send_message",
"description": "Sends a text message via WhatsApp, Telegram, or other messaging platform.",
"parameters": {
"type": "OBJECT",
"properties": {
"receiver": {"type": "STRING", "description": "Recipient contact name"},
"message_text": {"type": "STRING", "description": "The message to send"},
"platform": {"type": "STRING", "description": "Platform: WhatsApp, Telegram, etc."}
},
"required": ["receiver", "message_text", "platform"]
}
},
{
"name": "reminder",
"description": "Sets a timed reminder using Windows Task Scheduler.",
"parameters": {
"type": "OBJECT",
"properties": {
"date": {"type": "STRING", "description": "Date in YYYY-MM-DD format"},
"time": {"type": "STRING", "description": "Time in HH:MM format (24h)"},
"message": {"type": "STRING", "description": "Reminder message text"}
},
"required": ["date", "time", "message"]
}
},
{
"name": "youtube_video",
"description": (
"Controls YouTube. Use for: playing videos, summarizing a video's content, "
"getting video info, or showing trending videos."
),
"parameters": {
"type": "OBJECT",
"properties": {
"action": {"type": "STRING", "description": "play | summarize | get_info | trending (default: play)"},
"query": {"type": "STRING", "description": "Search query for play action"},
"save": {"type": "BOOLEAN", "description": "Save summary to Notepad (summarize only)"},
"region": {"type": "STRING", "description": "Country code for trending e.g. TR, US"},
"url": {"type": "STRING", "description": "Video URL for get_info action"},
},
"required": []
}
},
{
"name": "screen_process",
"description": (
"Captures and analyzes the screen or webcam image. "
"MUST be called when user asks what is on screen, what you see, "
"analyze my screen, look at camera, etc. "
"You have NO visual ability without this tool. "
"After calling this tool, stay SILENT — the vision module speaks directly."
),
"parameters": {
"type": "OBJECT",
"properties": {
"angle": {"type": "STRING", "description": "'screen' to capture display, 'camera' for webcam. Default: 'screen'"},
"text": {"type": "STRING", "description": "The question or instruction about the captured image"}
},
"required": ["text"]
}
},
{
"name": "computer_settings",
"description": (
"Controls the computer: volume, brightness, window management, keyboard shortcuts, "
"typing text on screen, closing apps, fullscreen, dark mode, WiFi, restart, shutdown, "
"scrolling, tab management, zoom, screenshots, lock screen, refresh/reload page. "
"Use for ANY single computer control command. NEVER route to agent_task."
),
"parameters": {
"type": "OBJECT",
"properties": {
"action": {"type": "STRING", "description": "The action to perform"},
"description": {"type": "STRING", "description": "Natural language description of what to do"},
"value": {"type": "STRING", "description": "Optional value: volume level, text to type, etc."}
},
"required": []
}
},
{
"name": "browser_control",
"description": (
"Controls the web browser. Use for: opening websites, searching the web, "
"clicking elements, filling forms, scrolling, any web-based task."
),
"parameters": {
"type": "OBJECT",
"properties": {
"action": {"type": "STRING", "description": "go_to | search | click | type | scroll | fill_form | smart_click | smart_type | get_text | press | close"},
"url": {"type": "STRING", "description": "URL for go_to action"},
"query": {"type": "STRING", "description": "Search query for search action"},
"selector": {"type": "STRING", "description": "CSS selector for click/type"},
"text": {"type": "STRING", "description": "Text to click or type"},
"description": {"type": "STRING", "description": "Element description for smart_click/smart_type"},
"direction": {"type": "STRING", "description": "up or down for scroll"},
"key": {"type": "STRING", "description": "Key name for press action"},
"incognito": {"type": "BOOLEAN", "description": "Open in private/incognito mode"},
},
"required": ["action"]
}
},
{
"name": "file_controller",
"description": "Manages files and folders: list, create, delete, move, copy, rename, read, write, find, disk usage.",
"parameters": {
"type": "OBJECT",
"properties": {
"action": {"type": "STRING", "description": "list | create_file | create_folder | delete | move | copy | rename | read | write | find | largest | disk_usage | organize_desktop | info"},
"path": {"type": "STRING", "description": "File/folder path or shortcut: desktop, downloads, documents, home"},
"destination": {"type": "STRING", "description": "Destination path for move/copy"},
"new_name": {"type": "STRING", "description": "New name for rename"},
"content": {"type": "STRING", "description": "Content for create_file/write"},
"name": {"type": "STRING", "description": "File name to search for"},
"extension": {"type": "STRING", "description": "File extension to search (e.g. .pdf)"},
"count": {"type": "INTEGER", "description": "Number of results for largest"},
},
"required": ["action"]
}
},
{
"name": "desktop_control",
"description": "Controls the desktop: wallpaper, organize, clean, list, stats.",
"parameters": {
"type": "OBJECT",
"properties": {
"action": {"type": "STRING", "description": "wallpaper | wallpaper_url | organize | clean | list | stats | task"},
"path": {"type": "STRING", "description": "Image path for wallpaper"},
"url": {"type": "STRING", "description": "Image URL for wallpaper_url"},
"mode": {"type": "STRING", "description": "by_type or by_date for organize"},
"task": {"type": "STRING", "description": "Natural language desktop task"},
},
"required": ["action"]
}
},
{
"name": "code_helper",
"description": "Writes, edits, explains, runs, or builds code files.",
"parameters": {
"type": "OBJECT",
"properties": {
"action": {"type": "STRING", "description": "write | edit | explain | run | build | auto (default: auto)"},
"description": {"type": "STRING", "description": "What the code should do or what change to make"},
"language": {"type": "STRING", "description": "Programming language (default: python)"},
"output_path": {"type": "STRING", "description": "Where to save the file"},
"file_path": {"type": "STRING", "description": "Path to existing file for edit/explain/run/build"},
"code": {"type": "STRING", "description": "Raw code string for explain"},
"args": {"type": "STRING", "description": "CLI arguments for run/build"},
"timeout": {"type": "INTEGER", "description": "Execution timeout in seconds (default: 30)"},
},
"required": ["action"]
}
},
{
"name": "dev_agent",
"description": "Builds complete multi-file projects from scratch: plans, writes files, installs deps, opens VSCode, runs and fixes errors.",
"parameters": {
"type": "OBJECT",
"properties": {
"description": {"type": "STRING", "description": "What the project should do"},
"language": {"type": "STRING", "description": "Programming language (default: python)"},
"project_name": {"type": "STRING", "description": "Optional project folder name"},
"timeout": {"type": "INTEGER", "description": "Run timeout in seconds (default: 30)"},
},
"required": ["description"]
}
},
{
"name": "agent_task",
"description": (
"Executes complex multi-step tasks requiring multiple different tools. "
"Examples: 'research X and save to file', 'find and organize files'. "
"DO NOT use for single commands. NEVER use for Steam/Epic — use game_updater."
),
"parameters": {
"type": "OBJECT",
"properties": {
"goal": {"type": "STRING", "description": "Complete description of what to accomplish"},
"priority": {"type": "STRING", "description": "low | normal | high (default: normal)"}
},
"required": ["goal"]
}
},
{
"name": "computer_control",
"description": "Direct computer control: type, click, hotkeys, scroll, move mouse, screenshots, find elements on screen.",
"parameters": {
"type": "OBJECT",
"properties": {
"action": {"type": "STRING", "description": "type | smart_type | click | double_click | right_click | hotkey | press | scroll | move | copy | paste | screenshot | wait | clear_field | focus_window | screen_find | screen_click | random_data | user_data"},
"text": {"type": "STRING", "description": "Text to type or paste"},
"x": {"type": "INTEGER", "description": "X coordinate"},
"y": {"type": "INTEGER", "description": "Y coordinate"},
"keys": {"type": "STRING", "description": "Key combination e.g. 'ctrl+c'"},
"key": {"type": "STRING", "description": "Single key e.g. 'enter'"},
"direction": {"type": "STRING", "description": "up | down | left | right"},
"amount": {"type": "INTEGER", "description": "Scroll amount (default: 3)"},
"seconds": {"type": "NUMBER", "description": "Seconds to wait"},
"title": {"type": "STRING", "description": "Window title for focus_window"},
"description": {"type": "STRING", "description": "Element description for screen_find/screen_click"},
"type": {"type": "STRING", "description": "Data type for random_data"},
"field": {"type": "STRING", "description": "Field for user_data: name|email|city"},
"clear_first": {"type": "BOOLEAN", "description": "Clear field before typing (default: true)"},
"path": {"type": "STRING", "description": "Save path for screenshot"},
},
"required": ["action"]
}
},
{
"name": "game_updater",
"description": (
"THE ONLY tool for ANY Steam or Epic Games request. "
"Use for: installing, downloading, updating games, listing installed games, "
"checking download status, scheduling updates. "
"ALWAYS call directly for any Steam/Epic/game request. "
"NEVER use agent_task, browser_control, or web_search for Steam/Epic."
),
"parameters": {
"type": "OBJECT",
"properties": {
"action": {"type": "STRING", "description": "update | install | list | download_status | schedule | cancel_schedule | schedule_status (default: update)"},
"platform": {"type": "STRING", "description": "steam | epic | both (default: both)"},
"game_name": {"type": "STRING", "description": "Game name (partial match supported)"},
"app_id": {"type": "STRING", "description": "Steam AppID for install (optional)"},
"hour": {"type": "INTEGER", "description": "Hour for scheduled update 0-23 (default: 3)"},
"minute": {"type": "INTEGER", "description": "Minute for scheduled update 0-59 (default: 0)"},
"shutdown_when_done": {"type": "BOOLEAN", "description": "Shut down PC when download finishes"},
},
"required": []
}
},
{
"name": "flight_finder",
"description": "Searches Google Flights and speaks the best options.",
"parameters": {
"type": "OBJECT",
"properties": {
"origin": {"type": "STRING", "description": "Departure city or airport code"},
"destination": {"type": "STRING", "description": "Arrival city or airport code"},
"date": {"type": "STRING", "description": "Departure date (any format)"},
"return_date": {"type": "STRING", "description": "Return date for round trips"},
"passengers": {"type": "INTEGER", "description": "Number of passengers (default: 1)"},
"cabin": {"type": "STRING", "description": "economy | premium | business | first"},
"save": {"type": "BOOLEAN", "description": "Save results to Notepad"},
},
"required": ["origin", "destination", "date"]
}
},
{
"name": "file_processor",
"description": (
"Processes any file that the user has uploaded or dropped onto the interface. "
"Use this when the user refers to an uploaded file and wants an action on it. "
"Supports: images (describe/ocr/resize/compress/convert), "
"PDFs (summarize/extract_text/to_word), "
"Word docs & text files (summarize/fix/reformat/translate), "
"CSV/Excel (analyze/stats/filter/sort/convert), "
"JSON/XML (validate/format/analyze), "
"code files (explain/review/fix/optimize/run/document/test), "
"audio (transcribe/trim/convert/info), "
"video (trim/extract_audio/extract_frame/compress/transcribe/info), "
"archives (list/extract), "
"presentations (summarize/extract_text). "
"ALWAYS call this tool when a file has been uploaded and the user gives a command about it. "
"If the user's command is ambiguous, pick the most logical action for that file type."
),
"parameters": {
"type": "OBJECT",
"properties": {
"file_path": {
"type": "STRING",
"description": "Full path to the uploaded file. Leave empty to use the currently uploaded file."
},
"action": {
"type": "STRING",
"description": (
"What to do with the file. Examples by type:\n"
"image: describe | ocr | resize | compress | convert | info\n"
"pdf: summarize | extract_text | to_word | info\n"
"docx/txt: summarize | fix | reformat | translate_hint | word_count | to_bullet\n"
"csv/excel: analyze | stats | filter | sort | convert | info\n"
"json: validate | format | analyze | to_csv\n"
"code: explain | review | fix | optimize | run | document | test\n"
"audio: transcribe | trim | convert | info\n"
"video: trim | extract_audio | extract_frame | compress | transcribe | info | convert\n"
"archive: list | extract\n"
"pptx: summarize | extract_text | analyze"
)
},
"instruction": {
"type": "STRING",
"description": "Free-form instruction if action doesn't cover it. E.g. 'translate this to Turkish', 'find all email addresses'"
},
"format": {
"type": "STRING",
"description": "Target format for conversion. E.g. 'mp3', 'pdf', 'csv', 'png'"
},
"width": {"type": "INTEGER", "description": "Target width for image resize"},
"height": {"type": "INTEGER", "description": "Target height for image resize"},
"scale": {"type": "NUMBER", "description": "Scale factor for image resize (e.g. 0.5)"},
"quality": {"type": "INTEGER", "description": "Quality 1-100 for image/video compress"},
"start": {"type": "STRING", "description": "Start time for trim: seconds or HH:MM:SS"},
"end": {"type": "STRING", "description": "End time for trim: seconds or HH:MM:SS"},
"timestamp": {"type": "STRING", "description": "Timestamp for video frame extraction HH:MM:SS"},
"column": {"type": "STRING", "description": "Column name for CSV filter/sort"},
"value": {"type": "STRING", "description": "Filter value for CSV filter"},
"condition": {"type": "STRING", "description": "Filter condition: equals|contains|gt|lt"},
"ascending": {"type": "BOOLEAN", "description": "Sort order for CSV sort (default: true)"},
"save": {"type": "BOOLEAN", "description": "Save result to file (default: true)"},
"destination": {"type": "STRING", "description": "Output folder for archive extract"},
},
"required": []
}
},
{
"name": "shutdown_jarvis",
"description": (
"Shuts down the assistant completely. "
"Call this when the user expresses intent to end the conversation, "
"close the assistant, say goodbye, or stop Jarvis. "
"The user can say this in ANY language."
),
"parameters": {
"type": "OBJECT",
"properties": {},
}
},
{
"name": "save_memory",
"description": (
"Save an important personal fact about the user to long-term memory. "
"Call this silently whenever the user reveals something worth remembering: "
"name, age, city, job, preferences, hobbies, relationships, projects, or future plans. "
"Do NOT call for: weather, reminders, searches, or one-time commands. "
"Do NOT announce that you are saving — just call it silently. "
"Values must be in English regardless of the conversation language."
),
"parameters": {
"type": "OBJECT",
"properties": {
"category": {
"type": "STRING",
"description": (
"identity — name, age, birthday, city, job, language, nationality | "
"preferences — favorite food/color/music/film/game/sport, hobbies | "
"projects — active projects, goals, things being built | "
"relationships — friends, family, partner, colleagues | "
"wishes — future plans, things to buy, travel dreams | "
"notes — habits, schedule, anything else worth remembering"
)
},
"key": {"type": "STRING", "description": "Short snake_case key (e.g. name, favorite_food, sister_name)"},
"value": {"type": "STRING", "description": "Concise value in English (e.g. Fatih, pizza, older sister)"},
},
"required": ["category", "key", "value"]
}
},
]
class JarvisLive:
def __init__(self, ui: JarvisUI):
self.ui = ui
self.session = None
self.audio_in_queue = None
self.out_queue = None
self._loop = None
self._is_speaking = False
self._speaking_lock = threading.Lock()
self.ui.on_text_command = self._on_text_command
def _on_text_command(self, text: str):
if not self._loop or not self.session:
return
asyncio.run_coroutine_threadsafe(
self.session.send_client_content(
turns={"parts": [{"text": text}]},
turn_complete=True
),
self._loop
)
def set_speaking(self, value: bool):
with self._speaking_lock:
self._is_speaking = value
if value:
self.ui.set_state("SPEAKING")
elif not self.ui.muted:
self.ui.set_state("LISTENING")
def speak(self, text: str):
if not self._loop or not self.session:
return
asyncio.run_coroutine_threadsafe(
self.session.send_client_content(
turns={"parts": [{"text": text}]},
turn_complete=True
),
self._loop
)
def speak_error(self, tool_name: str, error: str):
short = str(error)[:120]
self.ui.write_log(f"ERR: {tool_name} — {short}")
self.speak(f"Sir, {tool_name} encountered an error. {short}")
def _build_config(self) -> types.LiveConnectConfig:
from datetime import datetime
memory = load_memory()
mem_str = format_memory_for_prompt(memory)
sys_prompt = _load_system_prompt()
now = datetime.now()
time_str = now.strftime("%A, %B %d, %Y — %I:%M %p")
time_ctx = (
f"[CURRENT DATE & TIME]\n"
f"Right now it is: {time_str}\n"
f"Use this to calculate exact times for reminders.\n\n"
)
parts = [time_ctx]
if mem_str:
parts.append(mem_str)
parts.append(sys_prompt)
return types.LiveConnectConfig(
response_modalities=["AUDIO"],
output_audio_transcription={},
input_audio_transcription={},
system_instruction="\n".join(parts),
tools=[{"function_declarations": TOOL_DECLARATIONS}],
session_resumption=types.SessionResumptionConfig(),
speech_config=types.SpeechConfig(
voice_config=types.VoiceConfig(
prebuilt_voice_config=types.PrebuiltVoiceConfig(
voice_name="Charon"
)
)
),
)
async def _execute_tool(self, fc) -> types.FunctionResponse:
name = fc.name
args = dict(fc.args or {})
print(f"[JARVIS] 🔧 {name} {args}")
self.ui.set_state("THINKING")
if name == "save_memory":
category = args.get("category", "notes")
key = args.get("key", "")
value = args.get("value", "")
if key and value:
update_memory({category: {key: {"value": value}}})
print(f"[Memory] 💾 save_memory: {category}/{key} = {value}")
if not self.ui.muted:
self.ui.set_state("LISTENING")
return types.FunctionResponse(
id=fc.id, name=name,
response={"result": "ok", "silent": True}
)
loop = asyncio.get_event_loop()
result = "Done."
try:
if name == "open_app":
r = await loop.run_in_executor(None, lambda: open_app(parameters=args, response=None, player=self.ui))
result = r or f"Opened {args.get('app_name')}."
elif name == "weather_report":
r = await loop.run_in_executor(None, lambda: weather_action(parameters=args, player=self.ui))
result = r or "Weather delivered."
elif name == "browser_control":
r = await loop.run_in_executor(None, lambda: browser_control(parameters=args, player=self.ui))
result = r or "Done."
elif name == "file_controller":
r = await loop.run_in_executor(None, lambda: file_controller(parameters=args, player=self.ui))
result = r or "Done."
elif name == "send_message":
r = await loop.run_in_executor(None, lambda: send_message(parameters=args, response=None, player=self.ui, session_memory=None))
result = r or f"Message sent to {args.get('receiver')}."
elif name == "reminder":
r = await loop.run_in_executor(None, lambda: reminder(parameters=args, response=None, player=self.ui))
result = r or "Reminder set."
elif name == "youtube_video":
r = await loop.run_in_executor(None, lambda: youtube_video(parameters=args, response=None, player=self.ui))
result = r or "Done."
elif name == "file_processor":
if not args.get("file_path") and self.ui.current_file:
args["file_path"] = self.ui.current_file
r = await loop.run_in_executor(
None,
lambda: file_processor(parameters=args, player=self.ui, speak=self.speak)
)
result = r or "Done."
elif name == "screen_process":
threading.Thread(
target=screen_process,
kwargs={"parameters": args, "response": None,
"player": self.ui, "session_memory": None},
daemon=True
).start()
result = "Vision module activated. Stay completely silent — vision module will speak directly."
elif name == "computer_settings":
r = await loop.run_in_executor(None, lambda: computer_settings(parameters=args, response=None, player=self.ui))
result = r or "Done."
elif name == "desktop_control":
r = await loop.run_in_executor(None, lambda: desktop_control(parameters=args, player=self.ui))
result = r or "Done."
elif name == "code_helper":
r = await loop.run_in_executor(None, lambda: code_helper(parameters=args, player=self.ui, speak=self.speak))
result = r or "Done."
elif name == "dev_agent":
r = await loop.run_in_executor(None, lambda: dev_agent(parameters=args, player=self.ui, speak=self.speak))
result = r or "Done."
elif name == "agent_task":
from agent.task_queue import get_queue, TaskPriority
priority_map = {"low": TaskPriority.LOW, "normal": TaskPriority.NORMAL, "high": TaskPriority.HIGH}
priority = priority_map.get(args.get("priority", "normal").lower(), TaskPriority.NORMAL)
task_id = get_queue().submit(goal=args.get("goal", ""), priority=priority, speak=self.speak)
result = f"Task started (ID: {task_id})."
elif name == "web_search":
r = await loop.run_in_executor(None, lambda: web_search_action(parameters=args, player=self.ui))
result = r or "Done."
elif name == "computer_control":
r = await loop.run_in_executor(None, lambda: computer_control(parameters=args, player=self.ui))
result = r or "Done."
elif name == "game_updater":
r = await loop.run_in_executor(None, lambda: game_updater(parameters=args, player=self.ui, speak=self.speak))
result = r or "Done."
elif name == "flight_finder":
r = await loop.run_in_executor(None, lambda: flight_finder(parameters=args, player=self.ui))
result = r or "Done."
elif name == "shutdown_jarvis":
self.ui.write_log("SYS: Shutdown requested.")
self.speak("Goodbye, sir.")
def _shutdown():
import time, sys, os
time.sleep(1)
os._exit(0)
threading.Thread(target=_shutdown, daemon=True).start()
else:
result = f"Unknown tool: {name}"
except Exception as e:
result = f"Tool '{name}' failed: {e}"
traceback.print_exc()
self.speak_error(name, e)
if not self.ui.muted:
self.ui.set_state("LISTENING")
print(f"[JARVIS] 📤 {name} → {str(result)[:80]}")
return types.FunctionResponse(
id=fc.id, name=name,
response={"result": result}
)
async def _send_realtime(self):
while True:
msg = await self.out_queue.get()
await self.session.send_realtime_input(media=msg)
async def _listen_audio(self):
print("[JARVIS] 🎤 Mic started")
loop = asyncio.get_event_loop()
def callback(indata, frames, time_info, status):
with self._speaking_lock:
jarvis_speaking = self._is_speaking
if not jarvis_speaking and not self.ui.muted:
data = indata.tobytes()
loop.call_soon_threadsafe(
self.out_queue.put_nowait,
{"data": data, "mime_type": "audio/pcm"}
)
try:
with sd.InputStream(
samplerate=SEND_SAMPLE_RATE,
channels=CHANNELS,
dtype="int16",
blocksize=CHUNK_SIZE,
callback=callback,
):
print("[JARVIS] 🎤 Mic stream open")
while True:
await asyncio.sleep(0.1)
except Exception as e:
print(f"[JARVIS] ❌ Mic: {e}")
raise
async def _receive_audio(self):
print("[JARVIS] 👂 Recv started")
out_buf, in_buf = [], []
try:
while True:
async for response in self.session.receive():
if response.data:
self.audio_in_queue.put_nowait(response.data)
if response.server_content:
sc = response.server_content
if sc.output_transcription and sc.output_transcription.text:
self.set_speaking(True)
txt = sc.output_transcription.text.strip()
if txt:
out_buf.append(txt)
if sc.input_transcription and sc.input_transcription.text:
txt = sc.input_transcription.text.strip()
if txt:
in_buf.append(txt)
if sc.turn_complete:
self.set_speaking(False)
full_in = " ".join(in_buf).strip()
if full_in:
self.ui.write_log(f"You: {full_in}")
in_buf = []
full_out = " ".join(out_buf).strip()
if full_out:
self.ui.write_log(f"Jarvis: {full_out}")
out_buf = []
if full_in and len(full_in) > 5:
threading.Thread(
target=_update_memory_async,
args=(full_in, full_out),
daemon=True
).start()
if response.tool_call:
fn_responses = []
for fc in response.tool_call.function_calls:
print(f"[JARVIS] 📞 {fc.name}")
fr = await self._execute_tool(fc)
fn_responses.append(fr)
await self.session.send_tool_response(
function_responses=fn_responses
)
except Exception as e:
print(f"[JARVIS] ❌ Recv: {e}")
traceback.print_exc()
raise
async def _play_audio(self):
print("[JARVIS] 🔊 Play started")
loop = asyncio.get_event_loop()
stream = sd.RawOutputStream(
samplerate=RECEIVE_SAMPLE_RATE,
channels=CHANNELS,
dtype="int16",
blocksize=CHUNK_SIZE,
)
stream.start()
try:
while True:
chunk = await self.audio_in_queue.get()
self.set_speaking(True)
await asyncio.to_thread(stream.write, chunk)
except Exception as e:
print(f"[JARVIS] ❌ Play: {e}")
raise
finally:
self.set_speaking(False)
stream.stop()
stream.close()
async def run(self):
client = genai.Client(
api_key=_get_api_key(),
http_options={"api_version": "v1beta"}
)
while True:
try:
print("[JARVIS] 🔌 Connecting...")
self.ui.set_state("THINKING")
config = self._build_config()
async with (
client.aio.live.connect(model=LIVE_MODEL, config=config) as session,
asyncio.TaskGroup() as tg,
):
self.session = session
self._loop = asyncio.get_event_loop()
self.audio_in_queue = asyncio.Queue()
self.out_queue = asyncio.Queue(maxsize=10)
print("[JARVIS] ✅ Connected.")
self.ui.set_state("LISTENING")
self.ui.write_log("SYS: JARVIS online.")
tg.create_task(self._send_realtime())
tg.create_task(self._listen_audio())
tg.create_task(self._receive_audio())
tg.create_task(self._play_audio())
except Exception as e:
print(f"[JARVIS] ⚠️ {e}")
traceback.print_exc()
self.set_speaking(False)
self.ui.set_state("THINKING")
print("[JARVIS] 🔄 Reconnecting in 3s...")
await asyncio.sleep(3)
def main():
ui = JarvisUI("face.png")
def runner():
ui.wait_for_api_key()
jarvis = JarvisLive(ui)
try:
asyncio.run(jarvis.run())
except KeyboardInterrupt:
print("\n🔴 Shutting down...")
threading.Thread(target=runner, daemon=True).start()
ui.root.mainloop()
if __name__ == "__main__":
main()