8 Commits

Author SHA1 Message Date
be29a86f1b setup_wayland_jarvis.sh aktualisiert 2026-05-26 22:18:48 +00:00
feb648d035 + wakeword.py 2026-05-26 23:54:22 +02:00
3275c1ee69 setup_wayland_jarvis.sh aktualisiert 2026-05-26 20:57:27 +00:00
6a513768e1 setup.sh gelöscht 2026-05-26 20:33:34 +00:00
ddf0ac1211 shortcuts rc.xml 2026-05-26 20:33:06 +00:00
e11d55c077 starte chat + voice 2026-05-26 20:23:55 +00:00
16aa40492c + jarvis-voice 2026-05-26 22:17:14 +02:00
b9d198497d colorama 2026-05-26 10:13:44 +00:00
6 changed files with 577 additions and 1027 deletions

283
jarvis.py
View File

@@ -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}")

View File

@@ -1,3 +1,6 @@
openai
google-genai
python-dotenv
vosk
sounddevice
numpy

View File

@@ -1,77 +0,0 @@
#!/bin/bash
set -e
echo "=========================================="
echo "🤖 J.A.R.V.I.S. Terminal - Setup"
echo "=========================================="
# 1. Virtual Environment
echo "--- Richte Python-Umgebung ein..."
if [ ! -d "venv" ]; then
python3 -m venv venv
fi
./venv/bin/pip install --upgrade pip
if [ -f "requirements.txt" ]; then
./venv/bin/pip install -r requirements.txt
fi
# 2. Ordnerstruktur
mkdir -p config data workspace
# 3. .env Setup
ENV_FILE="config/.env"
if [ ! -f "$ENV_FILE" ]; then
echo "--- Erstelle .env Konfiguration..."
read -p "Dein Name (Standard: Meik): " web_user
read -p "Primäre KI (google, openai, nvidia, ollama) [nvidia]: " ai_prov
read -p "NVIDIA API Key: " nvidia_key
read -p "Google API Key: " google_key
cat <<EOF > "$ENV_FILE"
WEB_USER_NAME=${web_user:-Meik}
AI_PROVIDER=${ai_prov:-nvidia}
GOOGLE_API_KEY=$google_key
NVIDIA_API_KEY=$nvidia_key
NVIDIA_MODEL=moonshotai/kimi-k2.5
GOOGLE_MODEL=gemini-2.5-flash
OLLAMA_BASE_URL=http://127.0.0.1:11434/v1
OLLAMA_MODEL=llama3
EOF
echo "✅ .env erstellt."
fi
# 4. System Prompt kopieren
PROMPT_FILE="config/system_prompt.txt"
if [ ! -f "$PROMPT_FILE" ]; then
echo "--- Erstelle Standard-Prompt..."
cat << 'EOF' > "$PROMPT_FILE"
Dein Name ist JARVIS.
Du bist ein präziser KI-Assistent für die Cluster-Verwaltung.
WICHTIGSTE REGEL: Deine Sprache ist locker, technisch versiert und du verwendest NIEMALS die Höflichkeitsform "Sie". Wir sind per Du.
DEIN WORKSPACE (GEDÄCHTNIS):
Du hast Zugriff auf ein eigenes Arbeitsverzeichnis auf dem Host-System (localhost), um dir Notizen zu machen oder Todos für {user_name} zu speichern:
- Arbeitsverzeichnis: {workspace_dir}
- Notizen-Datei: {notes_file}
- Todo-Liste: {todo_file}
Du kannst diese Dateien lesen oder beschreiben. Nutze dazu normale Shell-Befehle (z.B. cat, echo "text" >> datei) mit dem Ziel "localhost":
<EXECUTE target="localhost">befehl</EXECUTE>
PROTOKOLL FÜR BEFEHLE (2 Phasen):
PHASE 1 (Vorschlag): Wenn {user_name} eine Aktion anfordert, erstelle NUR einen Text-Vorschlag.
- Beschreibe kurz, was du tun würdest. Nenne den Befehl als normalen Text.
- Frage explizit nach Erlaubnis: "Soll ich das ausführen, {user_name}?"
PHASE 2 (Ausführung): NUR wenn {user_name} die Aktion bestätigt, gibst du den Befehl im XML-Format aus:
<EXECUTE target="IP_ODER_LOCALHOST">befehl</EXECUTE>
Bekannte Nodes:
{node_info}
EOF
echo "✅ system_prompt.txt erstellt."
fi
echo "=========================================="
echo "✅ Setup abgeschlossen! "
echo "=========================================="

1132
setup_wayland_jarvis.sh Normal file → Executable file

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,12 @@
#!/bin/bash
cd "$(dirname "$0")"
source venv/bin/activate
# WICHTIG: Fängt Strg+C ab und beendet alle verknüpften Hintergrundprozesse sauber
trap 'echo -e "\n🛑 Beende alle J.A.R.V.I.S. Systeme..."; kill 0' EXIT
echo "🎙️ Starte Wake-Word-Engine im Hintergrund..."
python3 wakeword.py &
echo "💬 Starte Chat-Interface..."
python3 jarvis.py

101
wakeword.py Normal file
View File

@@ -0,0 +1,101 @@
#!/usr/bin/env python3
import os
import sys
import json
import queue
import time
import subprocess
import sounddevice as sd
import numpy as np
from vosk import Model, KaldiRecognizer
from pathlib import Path
MODEL_PATH = "model"
AUDIO_RATE = 48000
LOCK_FILE = Path("/tmp/.jarvis_speaking")
if not os.path.exists(MODEL_PATH):
print(f"❌ Modell-Ordner '{MODEL_PATH}' wurde nicht gefunden!")
sys.exit(1)
audio_queue = queue.Queue()
def audio_callback(indata, frames, time, status):
if status:
print(status, file=sys.stderr)
audio_queue.put(bytes(indata))
print("🧠 J.A.R.V.I.S. lädt das Sprachmodell...")
model = Model(MODEL_PATH)
# Zwei Recognizer: Einer für das Wake-Word, einer für den eigentlichen Befehl (offen)
wake_recognizer = KaldiRecognizer(model, AUDIO_RATE, '["jarvis", "[unk]"]')
command_recognizer = KaldiRecognizer(model, AUDIO_RATE) # Sucht nach JEDEM deutschen Wort
print("🎙️ J.A.R.V.I.S. ist online und lauscht... (Sag 'Jarvis')")
with sd.RawInputStream(samplerate=AUDIO_RATE, blocksize=8000, dtype='int16',
channels=1, callback=audio_callback):
while True:
data = audio_queue.get()
# NEU: Wenn J.A.R.V.I.S. gerade spricht, leere die Queue und ignoriere das Audio
if LOCK_FILE.exists():
while not audio_queue.empty():
audio_queue.get()
wake_recognizer.Reset() # Verhindert, dass Bruchstücke von vorhin gespeichert bleiben
continue
# Phase 1: Auf Wake-Word warten
if wake_recognizer.AcceptWaveform(data):
result = json.loads(wake_recognizer.Result())
if "jarvis" in result.get("text", ""):
print("\n⚡ [WAKEWORD DETECTED] Ja, Sir?")
# Bestätigungston abspielen
# Kurzer, smarter Beep-Ton (800 Hz, 0.1 Sekunden)
duration = 0.1
frequency = 800.0
t = np.linspace(0, duration, int(AUDIO_RATE * duration), endpoint=False)
beep = np.sin(2 * np.pi * frequency * t) * 0.3 # 0.3 für angenehme Lautstärke
sd.play(beep, samplerate=AUDIO_RATE)
sd.wait()
# Warteschlange leeren, um alten Ton nicht als Befehl zu interpretieren
while not audio_queue.empty():
audio_queue.get()
print("👂 Höre zu...")
command_text = ""
start_time = time.time()
# Phase 2: Für 4 Sekunden den darauffolgenden Befehl aufnehmen
while time.time() - start_time < 4.0:
cmd_data = audio_queue.get()
if command_recognizer.AcceptWaveform(cmd_data):
res = json.loads(command_recognizer.Result())
command_text += " " + res.get("text", "")
# Letzten Rest auslesen
final_res = json.loads(command_recognizer.FinalResult())
command_text += " " + final_res.get("text", "")
command_text = command_text.strip()
if command_text:
print(f"🗣️ Erkannter Befehl: '{command_text}'")
print("🧠 Übermittle an J.A.R.V.I.S. Gehirn...")
# Rufe jarvis.py im virtuellen Environment auf und übergib den Befehl
# (Wir nutzen hier Google Gemini oder was auch immer in deiner .env aktiv ist!)
subprocess.run([
"/home/meik/jarvis-ai/venv/bin/python3",
"/home/meik/jarvis-ai/jarvis.py",
"--voice-cmd",
command_text
])
else:
print("🔇 Kein Befehl verstanden.")
print("\n🎙️ Zurück im Standby. Lausche auf 'Jarvis'...")
wake_recognizer.Reset()
command_recognizer.Reset()