Files
Mark-LIV/main.py
T
2026-07-15 18:47:21 +03:00

1369 lines
65 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import platform as _platform
import subprocess as _subprocess
# ── Nuclear: force CREATE_NO_WINDOW on EVERY subprocess call on Windows ───────
# This patches Popen itself, so no per-file flag is needed anywhere.
if _platform.system() == "Windows":
_OrigPopen = _subprocess.Popen
class _Popen(_OrigPopen):
def __init__(self, args, **kw):
kw["creationflags"] = kw.get("creationflags", 0) | _subprocess.CREATE_NO_WINDOW
kw.pop("startupinfo", None) # drop any stale/shared STARTUPINFO
super().__init__(args, **kw)
_subprocess.Popen = _Popen
# ─────────────────────────────────────────────────────────────────────────────
import asyncio
import re
import threading
import time
import json
import sys
import traceback
from datetime import datetime
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,
)
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 _capture_camera, _capture_screen
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
from actions.system_monitor import SystemMonitor, get_system_status
from actions.proactive import ProactiveEngine
from actions.web_search import _news as _fetch_news_sync
from memory.config_manager import get_brief_enabled
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."
)
_CTRL_RE = re.compile(r"<ctrl\d+>", re.IGNORECASE)
def _clean_transcript(text: str) -> str:
text = _CTRL_RE.sub("", text)
text = re.sub(r"[\x00-\x08\x0b-\x1f]", "", text)
return text.strip()
TOOL_DECLARATIONS = [
{
"name": "open_app",
"description": (
"Opens any application on the 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. Use for ANY question about current facts, events, prices, "
"or topics — always prefer this over guessing. "
"Modes: 'search' (default), 'news' (latest headlines on a topic), "
"'research' (deep comprehensive answer), 'price' (product cost lookup), "
"'compare' (side-by-side comparison of items)."
),
"parameters": {
"type": "OBJECT",
"properties": {
"query": {"type": "STRING", "description": "Search query or topic"},
"mode": {"type": "STRING", "description": "search | news | research | price | compare"},
"items": {"type": "ARRAY", "items": {"type": "STRING"}, "description": "Items to compare (compare mode)"},
"aspect": {"type": "STRING", "description": "Comparison aspect: price | specs | reviews | features"},
},
"required": ["query"]
}
},
{
"name": "system_status",
"description": (
"Returns real-time system metrics: CPU usage, RAM, GPU load, CPU temperature, "
"uptime, and process count. Use when the user asks about computer performance, "
"temperature, memory, or resource usage."
),
"parameters": {
"type": "OBJECT",
"properties": {},
}
},
{
"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 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 the screen or webcam image and lets you analyze it. "
"MUST be called when user asks what is on screen, what you see, "
"look at camera, analyze my screen, etc. "
"You have NO visual ability without this tool. "
"After the image is captured it is sent directly to you — describe what you see and answer the user's question. "
"When using camera: the live view stays open until user says close it or calls close_camera."
),
"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": "close_camera",
"description": (
"Closes the live camera view shown on screen. "
"Call when user says: close camera, stop camera, turn off camera, "
"kamerayı kapat, kapat, creepy, etc."
),
"parameters": {"type": "OBJECT", "properties": {}, "required": []}
},
{
"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."
),
"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 any web browser. Use for: opening websites, searching the web, "
"clicking elements, filling forms, scrolling, screenshots, navigation, any web-based task. "
"Simple open/search requests launch the user's own browser normally (their real profile "
"and logged-in accounts); interactive actions (click, type, fill_form...) attach an "
"automation browser. "
"Always pass the 'browser' parameter when the user specifies a browser (e.g. 'open in Edge', "
"'use Firefox', 'open Chrome'). Multiple browsers can run simultaneously."
),
"parameters": {
"type": "OBJECT",
"properties": {
"action": {"type": "STRING", "description": "go_to | search | click | type | scroll | fill_form | smart_click | smart_type | get_text | get_url | press | new_tab | close_tab | screenshot | back | forward | reload | switch | list_browsers | close | close_all"},
"browser": {"type": "STRING", "description": "Target browser: chrome | edge | firefox | opera | operagx | brave | vivaldi | safari. Omit to use the currently active browser."},
"url": {"type": "STRING", "description": "URL for go_to / new_tab action"},
"query": {"type": "STRING", "description": "Search query for search action"},
"engine": {"type": "STRING", "description": "Search engine: google | bing | duckduckgo | yandex (default: google)"},
"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 | down for scroll"},
"amount": {"type": "INTEGER", "description": "Scroll amount in pixels (default: 500)"},
"key": {"type": "STRING", "description": "Key name for press action (e.g. Enter, Escape, F5)"},
"path": {"type": "STRING", "description": "Save path for screenshot"},
"incognito": {"type": "BOOLEAN", "description": "Open in private/incognito mode"},
"clear_first": {"type": "BOOLEAN", "description": "Clear field before typing (default: true)"},
},
"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": "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 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": "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": "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": "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"]
}
},
]
# --- Plugin system ---
class JarvisLive:
def __init__(self, ui: JarvisUI):
self.ui = ui
self._asst_name = "JARVIS" # updated each session from config
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._phone_active = False # True while phone mic is streaming; pauses PC mic
self._pending_vision = None # (img_bytes, mime_type, question, angle) to inject after tool response
self._vision_cam_active = False # True if camera was opened for vision → auto-close after response
self._vision_close_pending = False # True after vision injected; next turn_complete closes camera
self._vision_last_time = 0.0 # monotonic time of last screen_process call (cooldown guard)
self._vision_busy = False # True while a vision capture/inject cycle is in flight
self._interrupted = False # True while draining audio after user interrupt
self.ui.on_text_command = self._on_text_command
self.ui.on_remote_clicked = self._make_remote_key
self.ui.on_interrupt = self.interrupt
self._turn_done_event: asyncio.Event | None = None
self._dashboard = None
self._briefing_sent = False # morning briefing fires once per process
self._sys_monitor = SystemMonitor() # persistent cooldown state
self._proactive = ProactiveEngine()
self._last_user_speech = time.monotonic() # updated on every user utterance
def _make_remote_key(self):
"""Called from Qt main thread when user presses Remote Control."""
if self._dashboard is None:
self.ui.write_log(
"SYS: Dashboard unavailable. "
"Run: pip install fastapi \"uvicorn[standard]\" cryptography"
)
return None
key = self._dashboard.new_key()
url = self._dashboard.get_url()
manual = self._dashboard.get_manual_url()
return url, key, f"{url}/auto-login?key={key}", manual
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 interrupt(self) -> None:
"""Stop JARVIS mid-speech: drain queued audio and open mic immediately."""
self._interrupted = True
q = self.audio_in_queue
if q:
drained = 0
while True:
try:
q.get_nowait()
drained += 1
except Exception:
break
if drained:
print(f"[JARVIS] ✋ Interrupted — {drained} audio chunks discarded")
self.set_speaking(False)
if self._turn_done_event:
self._turn_done_event.clear()
self.ui.write_log("SYS: Interrupted — 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
# Load customization from config
try:
_cfg = json.loads(open(API_CONFIG_PATH, encoding="utf-8").read())
self._asst_name = (_cfg.get("assistant_name") or "JARVIS").strip()
_user_name = (_cfg.get("user_name") or "").strip()
except Exception:
self._asst_name = "JARVIS"
_user_name = ""
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"
)
# Identity injection — overrides any hardcoded name in prompt.txt
_addr = (f"ADDRESS: Always call the user '{_user_name}'."
if _user_name
else "ADDRESS: When speaking Turkish → always say \"efendim\". "
"When speaking English → say \"sir\". Never mix languages.")
identity_ctx = (
f"[IDENTITY]\n"
f"Your name is {self._asst_name}. "
f"Always refer to yourself as {self._asst_name}.\n"
f"{_addr}\n\n"
)
parts = [time_ctx, identity_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 == "screen_process":
import time as _t_mod
_now = _t_mod.monotonic()
_cooldown = 4.0 # seconds — covers echo window after speaking ends
if self._vision_busy or (_now - self._vision_last_time) < _cooldown:
_wait = max(0, _cooldown - (_now - self._vision_last_time))
print(f"[Vision] ⏳ Cooldown active ({_wait:.1f}s remaining) — ignoring duplicate call")
result = "Vision is still processing the previous request. I will not call this again."
else:
self._vision_busy = True
self._vision_last_time = _now
angle = args.get("angle", "screen").lower()
user_text = args.get("text", "What do you see?")
if angle == "camera":
img_b, mime_t = await loop.run_in_executor(None, _capture_camera)
self.ui.start_camera_stream()
self._vision_cam_active = True
print(f"[Vision] 📷 Camera: {len(img_b):,} bytes")
_stall = "camera"
else:
img_b, mime_t = await loop.run_in_executor(None, _capture_screen)
print(f"[Vision] 🖥️ Screen: {len(img_b):,} bytes")
_stall = "screen"
self._pending_vision = (img_b, mime_t, user_text, angle)
result = (
f"[VISION_ACTIVE] {_stall.capitalize()} captured. "
f"Immediately say ONE short natural sentence in the user's own language, "
f"telling them you are looking at their {_stall} right now. "
f"Do NOT describe or guess content — the actual image arrives in the NEXT message."
)
elif name == "close_camera":
self.ui.stop_camera_stream()
result = "Camera closed."
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 == "web_search":
r = await loop.run_in_executor(None, lambda: web_search_action(parameters=args, player=self.ui))
result = r or "Done."
# Mirror results to the on-screen content panel
_mode = args.get("mode", "search")
if r and not r.startswith("No results") and not r.startswith("Search failed"):
_query = args.get("query") or ", ".join(args.get("items", []))
_label = f"{_mode.upper()} — {_query[:38]}" if _query else _mode.upper()
self.ui.show_content(_label, r)
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 == "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 == "system_status":
r = await loop.run_in_executor(None, get_system_status)
result = str(r)
elif name == "shutdown_jarvis":
self.ui.write_log("SYS: Shutdown requested.")
self.speak("Goodbye, sir.")
def _shutdown():
import time, 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 and not self._phone_active:
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:
if self._interrupted:
pass # discard: interrupted
else:
if self._turn_done_event and self._turn_done_event.is_set():
self._turn_done_event.clear()
# Split into ~50 ms chunks so interrupt() stops audio within 50 ms
# (24000 Hz × 2 bytes/sample × 0.05 s = 2400 bytes per slice)
_audio_data = response.data
_SLICE = 2400
for _i in range(0, len(_audio_data), _SLICE):
self.audio_in_queue.put_nowait(_audio_data[_i : _i + _SLICE])
if response.server_content:
sc = response.server_content
if sc.output_transcription and sc.output_transcription.text:
txt = _clean_transcript(sc.output_transcription.text)
if txt and txt != (out_buf[-1] if out_buf else ""):
out_buf.append(txt)
if sc.input_transcription and sc.input_transcription.text:
txt = _clean_transcript(sc.input_transcription.text)
if txt:
in_buf.append(txt)
self._last_user_speech = time.monotonic()
if sc.turn_complete:
if self._turn_done_event:
self._turn_done_event.set()
# If this turn_complete ends an interrupted response, clear the
# flag and skip all further processing for that turn.
if self._interrupted:
self._interrupted = False
in_buf = []
out_buf = []
continue
full_in = " ".join(in_buf).strip()
if full_in:
self.ui.write_log(f"You: {full_in}")
if self._dashboard:
asyncio.create_task(self._dashboard.broadcast({
"type": "log", "speaker": "user",
"text": full_in,
"ts": datetime.now().isoformat(),
}))
in_buf = []
full_out = " ".join(out_buf).strip()
if full_out:
self.ui.write_log(f"{self._asst_name}: {full_out}")
if self._dashboard:
asyncio.create_task(self._dashboard.broadcast({
"type": "log", "speaker": "jarvis",
"text": full_out,
"ts": datetime.now().isoformat(),
}))
out_buf = []
# Vision injection: model finished tool-response turn → now send the image
if self._pending_vision and self.session:
import base64 as _b64
img_b, mime_t, question, angle = self._pending_vision
self._pending_vision = None
b64 = _b64.b64encode(img_b).decode("ascii")
print(f"[Vision] 📤 {len(img_b):,} bytes (angle={angle}) → main session")
await self.session.send_client_content(
turns={"parts": [
{"inline_data": {"mime_type": mime_t, "data": b64}},
{"text": question},
]},
turn_complete=True,
)
# Mark next turn_complete behaviour depending on angle
if self._vision_cam_active:
# Camera: keep busy until JARVIS finishes speaking the answer
self._vision_cam_active = False
self._vision_close_pending = True
else:
# Screen-only: no camera to close; release busy flag now
self._vision_busy = False
elif self._vision_close_pending:
# This turn_complete IS the vision answer — close camera + release busy flag
self._vision_close_pending = False
self._vision_busy = False
async def _cam_close():
await asyncio.sleep(2.0)
self.ui.stop_camera_stream()
asyncio.create_task(_cam_close())
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")
stream = sd.RawOutputStream(
samplerate=RECEIVE_SAMPLE_RATE,
channels=CHANNELS,
dtype="int16",
blocksize=CHUNK_SIZE,
)
stream.start()
try:
while True:
try:
chunk = await asyncio.wait_for(
self.audio_in_queue.get(),
timeout=0.1
)
except asyncio.TimeoutError:
if (
self._turn_done_event
and self._turn_done_event.is_set()
and self.audio_in_queue.empty()
):
self.set_speaking(False)
self._turn_done_event.clear()
continue
self.set_speaking(True)
try:
await asyncio.to_thread(stream.write, chunk)
except (RuntimeError, asyncio.CancelledError):
break # executor shutting down — exit cleanly
except Exception as e:
print(f"[JARVIS] ❌ Play: {e}")
raise
finally:
self.set_speaking(False)
stream.stop()
stream.close()
# ── Morning briefing ────────────────────────────────────────────────────────
async def _send_startup_briefing(self) -> None:
"""
Two-phase briefing optimized for speed:
Phase 1 — instant greeting (no tools) → speech starts in <1s
Phase 2 — news pre-fetched in a background thread while Phase 1 plays,
delivered as ready text (no Gemini tool-call round-trip) and
shown on the UI content panel. Waits for turn_complete event
instead of a fixed sleep so there is no unnecessary gap.
"""
memory = load_memory()
identity = memory.get("identity", {})
def _val(k: str) -> str:
e = identity.get(k, {})
return (e.get("value", "") if isinstance(e, dict) else str(e)).strip()
lang = _val("language")
name = _val("name")
time_str = datetime.now().strftime("%H:%M")
# Start fetching news immediately — runs in parallel while phase 1 plays
loop = asyncio.get_event_loop()
news_future = loop.run_in_executor(None, _fetch_news_sync, "top world news today")
await asyncio.sleep(0.3)
if not self.session:
return
# ── Phase 1: instant greeting ─────────────────────────────────────────
lang_clause = f" Respond in {lang}." if lang else ""
name_clause = f" Address the user as {name}." if name else ""
p1 = (
f"Greet the user, mention it is {time_str}, and say you are fetching today's news now. "
f"One short sentence only. Do not call any tools.{lang_clause}{name_clause}"
)
# Clear the turn-done event so we can wait for Phase 1 to finish
if self._turn_done_event:
self._turn_done_event.clear()
await self.session.send_client_content(
turns={"parts": [{"text": p1}]},
turn_complete=True,
)
self.ui.write_log("SYS: Briefing phase 1 (greeting) sent.")
# ── Phase 2: fire as soon as Phase 1 audio is done ───────────────────
async def _deliver_news():
try:
lang_str = f" Respond in {lang}." if lang else ""
# Wait for news fetch (already running) and Phase 1 turn-complete
# in parallel — whichever takes longer determines the wait time
news_done = asyncio.wrap_future(news_future)
turn_waited = False
if self._turn_done_event:
try:
await asyncio.wait_for(self._turn_done_event.wait(), timeout=6.0)
turn_waited = True
except asyncio.TimeoutError:
pass
# If turn_complete didn't fire (timeout), give a small buffer
if not turn_waited:
await asyncio.sleep(1.0)
try:
news_text = await asyncio.wait_for(news_done, timeout=4.0)
except Exception:
news_text = ""
if not self.session:
return
if news_text and len(news_text) > 60:
# Show on UI content panel immediately
self.ui.show_content("NEWS — top world news today", news_text)
p2 = (
f"[BRIEFING] Here are today's top news headlines:\n{news_text}\n\n"
"Pick ONE headline, summarise it in one sentence, then say the full list "
f"is displayed on screen. Do not call any tools.{lang_str}"
)
else:
p2 = (
"News headlines could not be fetched right now. "
f"Let the user know briefly.{lang_str}"
)
await self.session.send_client_content(
turns={"parts": [{"text": p2}]},
turn_complete=True,
)
self.ui.write_log("SYS: Briefing phase 2 (news) sent.")
except Exception as e:
print(f"[Briefing] Phase 2 error: {e}")
self.ui.write_log(f"SYS: Briefing phase 2 failed: {e}")
asyncio.create_task(_deliver_news())
# ── System monitor ──────────────────────────────────────────────────────────
async def _run_system_monitor(self) -> None:
"""Background task: voice alerts when metrics exceed thresholds."""
while True:
await asyncio.sleep(10)
alert = await asyncio.to_thread(self._sys_monitor.check)
if alert and self.session:
try:
await self.session.send_client_content(
turns={"parts": [{"text": alert}]},
turn_complete=True,
)
except Exception as e:
print(f"[Monitor] ⚠️ Could not send alert: {e}")
# ── Proactive mode ──────────────────────────────────────────────────────────
async def _run_proactive_mode(self) -> None:
"""
Background task: periodically checks if the user has been silent long enough,
then hands time + memory context to Gemini so it can decide what (if anything)
to say proactively. No hardcoded rules — Gemini makes the call.
"""
while True:
await asyncio.sleep(60) # evaluate once per minute
if not self.session:
continue
with self._speaking_lock:
speaking = self._is_speaking
if speaking:
continue
if not self._proactive.should_trigger(self._last_user_speech):
continue
self._proactive.mark_triggered()
try:
memory = await asyncio.to_thread(load_memory)
prompt = self._proactive.build_prompt(memory)
await self.session.send_client_content(
turns={"parts": [{"text": prompt}]},
turn_complete=True,
)
self.ui.write_log("SYS: Proactive check-in.")
except Exception as e:
print(f"[Proactive] ⚠️ {e}")
# ── Phone audio relay ────────────────────────────────────────────────────────
async def _relay_phone_audio(self) -> None:
"""Forward phone mic PCM chunks from dashboard queue into the Gemini Live session."""
q = self._dashboard._phone_audio_queue
while True:
try:
chunk = await asyncio.wait_for(q.get(), timeout=1.0)
except asyncio.TimeoutError:
# No audio for 1 s → phone mic inactive, give PC mic back
self._phone_active = False
continue
self._phone_active = True # phone is streaming — silence PC mic
with self._speaking_lock:
speaking = self._is_speaking
if not speaking and not self.ui.muted:
try:
self.out_queue.put_nowait(chunk)
except asyncio.QueueFull:
pass
def _on_phone_connected(self) -> None:
self.ui.write_log("SYS: Phone connected via Remote Dashboard.")
self.ui.notify_phone_connected()
# ── dashboard command relay ─────────────────────────────────────────────
async def _process_dashboard_commands(self) -> None:
while True:
try:
text = await asyncio.wait_for(
self._dashboard._command_queue.get(), timeout=0.5
)
if not text:
continue
# Wait up to 8s for session to become ready after a wake
for _ in range(80):
if self.session:
break
await asyncio.sleep(0.1)
if self.session:
await self.session.send_client_content(
turns={"parts": [{"text": text}]},
turn_complete=True,
)
self.ui.write_log(f"[Web]: {text}")
else:
print(f"[Dashboard] Dropped command (no session): {text}")
except asyncio.TimeoutError:
pass
except Exception as e:
print(f"[Dashboard] Command error: {e}")
await asyncio.sleep(0.5)
# ── main loop ───────────────────────────────────────────────────────────
async def run(self):
self._loop = asyncio.get_event_loop()
# Start dashboard (optional — needs: pip install fastapi "uvicorn[standard]" cryptography)
try:
from dashboard.server import DashboardServer
self._dashboard = DashboardServer()
self._dashboard.set_connect_callback(self._on_phone_connected)
asyncio.create_task(self._dashboard.serve())
# Runs for the whole lifetime, not just inside an active session
asyncio.create_task(self._process_dashboard_commands())
except Exception as e:
print(f"[Dashboard] Disabled: {e}")
self._dashboard = None
while True:
try:
print("[JARVIS] Connecting...")
self.ui.set_state("THINKING")
config = self._build_config()
# Fresh client on every reconnect — avoids stale HTTP session state
client = genai.Client(
api_key=_get_api_key(),
http_options={"api_version": "v1beta"}
)
async with (
client.aio.live.connect(model=LIVE_MODEL, config=config) as session,
asyncio.TaskGroup() as tg,
):
self.session = session
self.audio_in_queue = asyncio.Queue()
self.out_queue = asyncio.Queue(maxsize=200)
self._turn_done_event = asyncio.Event()
# Reset transient state that must not carry over from a previous session
self._pending_vision = None
self._vision_cam_active = False
self._vision_close_pending = False
self._vision_busy = False
self._vision_last_time = 0.0
self._interrupted = False
print("[JARVIS] Connected.")
self.ui.set_state("LISTENING")
self.ui.write_log("SYS: JARVIS online.")
if self._dashboard:
await self._dashboard.broadcast({"type": "status", "state": "active"})
tg.create_task(self._send_realtime())
tg.create_task(self._listen_audio())
tg.create_task(self._receive_audio())
tg.create_task(self._play_audio())
tg.create_task(self._run_system_monitor())
tg.create_task(self._run_proactive_mode())
if self._dashboard:
tg.create_task(self._relay_phone_audio())
# Morning briefing — fires once per process launch (if enabled)
if not self._briefing_sent and get_brief_enabled():
self._briefing_sent = True
tg.create_task(self._send_startup_briefing())
except KeyboardInterrupt:
raise
except SystemExit:
raise
except BaseException as e:
# Catches both Exception and BaseExceptionGroup (Python 3.11+
# TaskGroup raises BaseExceptionGroup when tasks are cancelled
# externally, which `except Exception` would miss, letting the
# exception escape the while-loop and causing asyncio.run() to
# start shutdown — resulting in "executor after shutdown" errors).
err_str = str(e)
print(f"[JARVIS] Error ({type(e).__name__}): {e}")
traceback.print_exc()
# Invalid API key — stop hammering the API, prompt re-configuration
if "API key not valid" in err_str or "1007" in err_str:
self.ui.write_log("ERR: API key invalid — please re-enter your key.")
self.ui.set_state("SLEEPING")
self.ui.prompt_reconfig()
while not self.ui._win._ready:
await asyncio.sleep(1)
print("[JARVIS] New API key saved — reconnecting...")
_conn_backoff = 3
continue
# Network / timeout errors — log clearly and back off
is_net_err = any(k in err_str for k in (
"TimeoutError", "timed out", "getaddrinfo", "CancelledError",
"ConnectionRefusedError", "OSError", "Cannot connect",
))
if is_net_err:
_conn_backoff = min(getattr(self, "_conn_backoff", 3) * 2, 60)
self._conn_backoff = _conn_backoff
self.ui.write_log(
f"NET: Bağlantı kurulamadı — {_conn_backoff}s sonra tekrar deneniyor. "
"(VPN gerekiyor olabilir)"
)
else:
self._conn_backoff = 3
finally:
self.session = None
self.set_speaking(False)
self.ui.set_state("SLEEPING")
if self._dashboard:
await self._dashboard.broadcast({"type": "status", "state": "sleeping"})
delay = getattr(self, "_conn_backoff", 3)
print(f"[JARVIS] Reconnecting in {delay}s...")
await asyncio.sleep(delay)
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()