+ jarvis-voice
This commit is contained in:
283
jarvis.py
283
jarvis.py
@@ -3,6 +3,8 @@ import re
|
||||
import sqlite3
|
||||
import asyncio
|
||||
import openai
|
||||
import sys
|
||||
import subprocess
|
||||
|
||||
from google import genai
|
||||
from google.genai import types
|
||||
@@ -143,6 +145,37 @@ def get_db():
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
|
||||
# ====================================================
|
||||
# DYNAMISCHE PROGRAMM-ERKENNUNG (NEU)
|
||||
# ====================================================
|
||||
|
||||
def get_installed_gui_apps():
|
||||
"""Scannt das System nach installierten GUI-Programmen und deren Befehlen."""
|
||||
apps_dir = Path("/usr/share/applications")
|
||||
detected_apps = {}
|
||||
|
||||
if apps_dir.exists():
|
||||
for desktop_file in apps_dir.glob("*.desktop"):
|
||||
try:
|
||||
# Wir lesen die .desktop Datei aus
|
||||
content = desktop_file.read_text(encoding="utf-8", errors="ignore")
|
||||
|
||||
# Suchen nach Name und Exec-Befehl
|
||||
name_match = re.search(r"^Name=(.+)$", content, re.M)
|
||||
exec_match = re.search(r"^Exec=([^ \n%]+)", content, re.M) # Nur den reinen Befehl ohne Argumente (%U etc.)
|
||||
|
||||
if name_match and exec_match:
|
||||
app_name = name_match.group(1).strip()
|
||||
app_cmd = exec_match.group(1).strip()
|
||||
|
||||
# Ignoriere Core-Systemkram, der Meik nur nerven würde
|
||||
if not any(x in app_cmd.lower() for x in ["debian", "im-config", "openjdk", "systemd"]):
|
||||
detected_apps[app_name] = app_cmd
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
return detected_apps
|
||||
|
||||
|
||||
# ====================================================
|
||||
# SYSTEM PROMPT
|
||||
@@ -163,26 +196,29 @@ def get_system_prompt():
|
||||
prompt = prompt.replace("{notes_file}", str(NOTES_FILE))
|
||||
prompt = prompt.replace("{todo_file}", str(TODO_FILE))
|
||||
|
||||
# --- DYNAMISCHE PROGRAMME INJIZIEREN ---
|
||||
installed_apps = get_installed_gui_apps()
|
||||
apps_prompt_string = "VERFÜGBARE LOKALE DESKTOP-PROGRAMME (Nutze NUR diese Befehe zum Starten!):\n"
|
||||
for app_name, app_cmd in installed_apps.items():
|
||||
apps_prompt_string += f"- {app_name}: Befehl lautet '{app_cmd}'\n"
|
||||
|
||||
# Wir hängen die Liste einfach an den Prompt an oder ersetzen einen Platzhalter
|
||||
if "{installed_apps}" in prompt:
|
||||
prompt = prompt.replace("{installed_apps}", apps_prompt_string)
|
||||
else:
|
||||
prompt += "\n\n" + apps_prompt_string
|
||||
# ---------------------------------------
|
||||
|
||||
conn = get_db()
|
||||
|
||||
nodes = conn.execute(
|
||||
'SELECT * FROM nodes'
|
||||
).fetchall()
|
||||
|
||||
nodes = conn.execute('SELECT * FROM nodes').fetchall()
|
||||
conn.close()
|
||||
|
||||
node_info = ""
|
||||
|
||||
for n in nodes:
|
||||
node_info += (
|
||||
f"- Name: {n['name']}, "
|
||||
f"IP: {n['ip']}, "
|
||||
f"User: {n['user']}\n"
|
||||
)
|
||||
node_info += f"- Name: {n['name']}, IP: {n['ip']}, User: {n['user']}\n"
|
||||
|
||||
return prompt.replace("{node_info}", node_info)
|
||||
|
||||
|
||||
# ====================================================
|
||||
# KI KOMMUNIKATION
|
||||
# ====================================================
|
||||
@@ -306,21 +342,8 @@ async def run_task(target, cmd):
|
||||
# GUI APPS ERKENNEN
|
||||
# ========================================
|
||||
|
||||
gui_apps = [
|
||||
"firefox",
|
||||
"thunderbird",
|
||||
"chromium",
|
||||
"google-chrome",
|
||||
"code",
|
||||
"nautilus",
|
||||
"pcmanfm",
|
||||
"gedit",
|
||||
"vlc",
|
||||
"discord",
|
||||
"steam",
|
||||
"obs",
|
||||
"spotify"
|
||||
]
|
||||
# Holt sich alle bekannten System-GUI-Befehle dynamisch
|
||||
gui_apps = list(get_installed_gui_apps().values())
|
||||
|
||||
first_word = cmd.strip().split()[0]
|
||||
|
||||
@@ -448,49 +471,34 @@ async def run_task(target, cmd):
|
||||
|
||||
collected_output = []
|
||||
|
||||
while True:
|
||||
# ========================================
|
||||
# KRISENFESTES AUSLESEN MIT TIMEOUT
|
||||
# ========================================
|
||||
try:
|
||||
while True:
|
||||
# Warte maximal 2 Sekunden auf die nächste Zeile
|
||||
line = await asyncio.wait_for(proc.stdout.readline(), timeout=2.0)
|
||||
|
||||
if not line:
|
||||
break
|
||||
|
||||
line = await proc.stdout.readline()
|
||||
decoded = line.decode("utf-8", errors="ignore").rstrip()
|
||||
collected_output.append(decoded)
|
||||
|
||||
if not line:
|
||||
break
|
||||
print(f"{OUTPUT_COLOR}│ {decoded}{RESET}")
|
||||
except asyncio.TimeoutError:
|
||||
# Falls das Tool die Pipe offen hält, lesen wir einfach nicht weiter
|
||||
print(f"{SYSTEM_COLOR}⏳ Ausgabe-Stream stagniert. Erzwinge Prozess-Check...{RESET}")
|
||||
|
||||
decoded = line.decode(
|
||||
"utf-8",
|
||||
errors="ignore"
|
||||
).rstrip()
|
||||
|
||||
collected_output.append(decoded)
|
||||
|
||||
print(
|
||||
f"{OUTPUT_COLOR}"
|
||||
f"│ {decoded}"
|
||||
f"{RESET}"
|
||||
)
|
||||
|
||||
await proc.wait()
|
||||
# Maximal 2 Sekunden auf das offizielle Ende des Prozesses warten
|
||||
try:
|
||||
await asyncio.wait_for(proc.wait(), timeout=2.0)
|
||||
except asyncio.TimeoutError:
|
||||
print(f"{ERROR_COLOR}⚠️ Prozess reagiert nicht. Setze Ablauf trotzdem fort.{RESET}")
|
||||
# Optional: proc.terminate() falls du ihn hart killen willst
|
||||
|
||||
print()
|
||||
|
||||
if proc.returncode == 0:
|
||||
|
||||
print(
|
||||
f"{JARVIS_COLOR}"
|
||||
f"✅ TASK ERFOLGREICH"
|
||||
f"{RESET}\n"
|
||||
)
|
||||
|
||||
else:
|
||||
|
||||
print(
|
||||
f"{ERROR_COLOR}"
|
||||
f"❌ FEHLER CODE: "
|
||||
f"{proc.returncode}"
|
||||
f"{RESET}\n"
|
||||
)
|
||||
|
||||
return "\n".join(collected_output)
|
||||
|
||||
except Exception as e:
|
||||
|
||||
err = f"❌ Fehler: {e}"
|
||||
@@ -531,27 +539,33 @@ async def listen_to_user():
|
||||
# ====================================================
|
||||
|
||||
async def speak_to_user(text):
|
||||
|
||||
print(
|
||||
f"\n{JARVIS_COLOR}"
|
||||
f"🤖 J.A.R.V.I.S."
|
||||
f"{RESET}"
|
||||
)
|
||||
|
||||
print(
|
||||
f"{JARVIS_COLOR}"
|
||||
f"{'-'*60}"
|
||||
f"{RESET}"
|
||||
)
|
||||
|
||||
print(f"\n{JARVIS_COLOR}🤖 J.A.R.V.I.S.{RESET}")
|
||||
print(f"{JARVIS_COLOR}{'-'*60}{RESET}")
|
||||
print(text)
|
||||
print(f"{JARVIS_COLOR}{'-'*60}{RESET}\n")
|
||||
|
||||
print(
|
||||
f"{JARVIS_COLOR}"
|
||||
f"{'-'*60}"
|
||||
f"{RESET}\n"
|
||||
)
|
||||
clean_text = re.sub(r'[^\w\s\d.,!?-]', '', text)
|
||||
|
||||
piper_path = "/home/meik/jarvis-ai/piper/piper"
|
||||
model_path = "/home/meik/jarvis-ai/de_DE-thorsten-high.onnx"
|
||||
lock_file = Path("/tmp/.jarvis_speaking") # Die Sperr-Datei
|
||||
|
||||
if os.path.exists(piper_path) and os.path.exists(model_path):
|
||||
try:
|
||||
# 1. Sperre setzen
|
||||
lock_file.touch()
|
||||
|
||||
piper_cmd = f"echo '{clean_text}' | {piper_path} --model {model_path} --output_raw | aplay -r 22050 -f S16_LE -t raw -D pipewire >/dev/null 2>&1"
|
||||
|
||||
proc = await asyncio.create_subprocess_shell(piper_cmd)
|
||||
await proc.wait()
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ TTS Fehler: {e}")
|
||||
finally:
|
||||
# 2. Sperre IMMER wieder aufheben, wenn Piper fertig ist
|
||||
if lock_file.exists():
|
||||
lock_file.unlink()
|
||||
|
||||
# ====================================================
|
||||
# MAIN LOOP
|
||||
@@ -681,25 +695,14 @@ async def main_chat_loop():
|
||||
target = target.strip()
|
||||
cmd = cmd.strip()
|
||||
|
||||
# ========================================
|
||||
# SICHTBARE SYSTEMAKTION
|
||||
# ========================================
|
||||
|
||||
action_msg = (
|
||||
f"⚙️ Ich führe jetzt folgenden Befehl "
|
||||
f"auf [{target}] aus:\n\n"
|
||||
f"{cmd}"
|
||||
)
|
||||
|
||||
await speak_to_user(action_msg)
|
||||
|
||||
chat_history.append({
|
||||
"role": "assistant",
|
||||
"content": action_msg,
|
||||
"timestamp": now
|
||||
})
|
||||
|
||||
# Optional: Aktion auch ins Log
|
||||
# ÄNDERUNG: Auch hier nur im Terminal anzeigen, NICHT vorlesen!
|
||||
print(f"\n{SYSTEM_COLOR}{action_msg}{RESET}\n")
|
||||
log_to_file("SYSTEM", action_msg)
|
||||
|
||||
# ========================================
|
||||
@@ -716,33 +719,26 @@ async def main_chat_loop():
|
||||
# ========================================
|
||||
|
||||
if output:
|
||||
|
||||
output_msg = (
|
||||
f"💻 Ergebnis der Ausführung "
|
||||
f"auf [{target}]:\n\n"
|
||||
f"{output}"
|
||||
)
|
||||
|
||||
else:
|
||||
|
||||
output_msg = (
|
||||
f"✅ Befehl auf [{target}] "
|
||||
f"erfolgreich abgeschlossen."
|
||||
)
|
||||
|
||||
# Das hier wird weiterhin laut vorgelesen!
|
||||
await speak_to_user(output_msg)
|
||||
|
||||
sys_now = datetime.now().strftime(
|
||||
"%d.%m.%Y %H:%M"
|
||||
)
|
||||
|
||||
sys_now = datetime.now().strftime("%d.%m.%Y %H:%M")
|
||||
chat_history.append({
|
||||
"role": "assistant",
|
||||
"content": output_msg,
|
||||
"timestamp": sys_now
|
||||
})
|
||||
|
||||
# LOG: System Output hier schreiben!
|
||||
log_to_file("SYSTEM", output_msg)
|
||||
|
||||
# ============================================
|
||||
@@ -753,20 +749,77 @@ async def main_chat_loop():
|
||||
chat_history = chat_history[-20:]
|
||||
|
||||
|
||||
|
||||
# ====================================================
|
||||
# EINZEL-BEFEHL MODUS (Für das Sprachskript)
|
||||
# ====================================================
|
||||
async def run_single_command(command_text):
|
||||
"""Verarbeitet einen einzelnen Befehl von außen und beendet sich wieder."""
|
||||
init_db()
|
||||
system_prompt = get_system_prompt()
|
||||
|
||||
# Wir tun so, als käme die Eingabe aus dem Chat-History-Verlauf
|
||||
now = datetime.now().strftime("%d.%m.%Y %H:%M")
|
||||
chat_history = [{
|
||||
"role": "user",
|
||||
"content": command_text,
|
||||
"timestamp": now
|
||||
}]
|
||||
|
||||
log_to_file("Voice-Input", command_text)
|
||||
|
||||
ai_response = await get_ai_response(
|
||||
command_text,
|
||||
system_prompt,
|
||||
chat_history
|
||||
)
|
||||
|
||||
if ai_response is None:
|
||||
return
|
||||
|
||||
# EXECUTE-Tags suchen und ausführen
|
||||
commands = []
|
||||
execute_matches = re.finditer(
|
||||
r'<EXECUTE[^>]*?(?:target="(.*?)")?[^>]*>(.*?)</EXECUTE>',
|
||||
ai_response,
|
||||
re.I | re.S
|
||||
)
|
||||
|
||||
for match in execute_matches:
|
||||
target = match.group(1) or "localhost"
|
||||
cmd = match.group(2).strip()
|
||||
cmd = re.sub(r'^```[a-zA-Z]*\n?', '', cmd)
|
||||
cmd = re.sub(r'\n?```$', '', cmd)
|
||||
commands.append((target.strip(), cmd.strip()))
|
||||
|
||||
clean_msg = re.sub(r'<EXECUTE[^>]*?>.*?</EXECUTE>', '', ai_response, flags=re.I | re.S).strip()
|
||||
|
||||
if clean_msg:
|
||||
await speak_to_user(clean_msg)
|
||||
log_to_file("J.A.R.V.I.S.", clean_msg)
|
||||
|
||||
if commands:
|
||||
for target, cmd in commands:
|
||||
action_msg = f"⚙️ Führe Sprachbefehl auf [{target}] aus:\n{cmd}"
|
||||
|
||||
# ÄNDERUNG: Nur im Terminal anzeigen, NICHT vorlesen!
|
||||
print(f"\n{SYSTEM_COLOR}{action_msg}{RESET}\n")
|
||||
log_to_file("SYSTEM", action_msg)
|
||||
|
||||
# Befehl im Hintergrund ausführen
|
||||
await run_task(target, cmd)
|
||||
|
||||
# ====================================================
|
||||
# START
|
||||
# ====================================================
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
try:
|
||||
|
||||
asyncio.run(main_chat_loop())
|
||||
|
||||
# Wenn Argumente übergeben wurden (z.B. python3 jarvis.py --voice-cmd "...")
|
||||
if len(sys.argv) > 2 and sys.argv[1] == "--voice-cmd":
|
||||
command_text = sys.argv[2]
|
||||
asyncio.run(run_single_command(command_text))
|
||||
else:
|
||||
# Normaler Terminal-Modus
|
||||
asyncio.run(main_chat_loop())
|
||||
except KeyboardInterrupt:
|
||||
|
||||
print(
|
||||
f"\n{ERROR_COLOR}"
|
||||
f"⛔ J.A.R.V.I.S. hart beendet."
|
||||
f"{RESET}"
|
||||
)
|
||||
print(f"\n{ERROR_COLOR}⛔ J.A.R.V.I.S. hart beendet.{RESET}")
|
||||
|
||||
Reference in New Issue
Block a user