40 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
e8242228e4 überarbeitet und mit Audio-Support 2026-05-26 01:21:20 +02:00
9468a237e2 .gitignore aktualisiert 2026-05-22 20:36:58 +00:00
055d157283 config/.gitkeep hinzugefügt 2026-05-22 20:31:14 +00:00
300960f5ce jarvis.py aktualisiert 2026-05-22 20:27:26 +00:00
60a8e1f104 setup_wayland_jarvis.sh aktualisiert 2026-05-22 20:11:31 +00:00
b312a09a0c setup_wayland_jarvis.sh aktualisiert 2026-05-22 20:08:41 +00:00
2e8daf6ce1 setup.sh aktualisiert 2026-05-22 19:36:33 +00:00
6b081bbbaf setup_wayland_jarvis.sh aktualisiert 2026-05-22 19:33:25 +00:00
0c085b7546 start.sh hinzugefügt 2026-05-22 19:26:00 +00:00
7b27100302 setup.sh hinzugefügt 2026-05-22 19:25:28 +00:00
7fc94ff379 jarvis.py hinzugefügt 2026-05-22 19:24:20 +00:00
2416581568 requirements.txt hinzugefügt 2026-05-22 19:23:44 +00:00
9f64ca5c87 setup_wayland_jarvis.sh aktualisiert 2026-05-22 15:45:36 +00:00
132eede43c setup_wayland_jarvis.sh aktualisiert 2026-05-22 15:34:11 +00:00
bf941e5c5f setup_wayland_jarvis.sh aktualisiert 2026-05-22 15:24:53 +00:00
1478f24eab setup_wayland_jarvis.sh aktualisiert 2026-05-22 14:59:22 +00:00
d571a532b0 setup_wayland_jarvis.sh aktualisiert 2026-05-22 14:41:38 +00:00
10890af8cf setup_wayland_jarvis.sh aktualisiert 2026-05-22 14:39:52 +00:00
c1ec3ad1a1 setup_wayland_jarvis.sh aktualisiert 2026-05-22 14:34:31 +00:00
f4b9dbfd73 setup_wayland_jarvis.sh aktualisiert 2026-05-22 14:31:51 +00:00
25e56e9ba9 setup_wayland_jarvis.sh aktualisiert 2026-05-22 14:16:39 +00:00
0dae15e84b setup_wayland_jarvis.sh aktualisiert 2026-05-22 13:39:02 +00:00
483cd69fb8 setup_wayland_jarvis.sh aktualisiert 2026-05-22 13:31:59 +00:00
0dff010d59 setup_wayland_jarvis.sh aktualisiert 2026-05-22 13:23:54 +00:00
332e5e214d setup_wayland_jarvis.sh aktualisiert 2026-05-22 13:13:39 +00:00
5d68390a11 setup_wayland_jarvis.sh aktualisiert 2026-05-22 12:52:51 +00:00
48c1155e01 setup_wayland_jarvis.sh aktualisiert 2026-05-22 12:38:56 +00:00
a65a362fec setup_wayland_jarvis.sh aktualisiert 2026-05-22 12:23:04 +00:00
7121c18a7d setup_wayland_jarvis.sh aktualisiert 2026-05-22 12:15:11 +00:00
98fbd69f69 setup_wayland_jarvis.sh aktualisiert 2026-05-22 12:13:11 +00:00
da9e4dc44a setup_wayland_jarvis.sh aktualisiert 2026-05-22 11:55:10 +00:00
b3ebf90965 setup_wayland_jarvis.sh hinzugefügt 2026-05-22 11:50:27 +00:00
7 changed files with 1557 additions and 2 deletions

7
.gitignore vendored
View File

@@ -1,6 +1,8 @@
# ---> Linux
*~
./config/*
./venv/*
./workspace/
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
@@ -20,7 +22,8 @@
.LSOverride
# Icon must end with two \r
Icon
Icon
# Thumbnails
._*

0
config/.gitkeep Normal file
View File

825
jarvis.py Normal file
View File

@@ -0,0 +1,825 @@
import os
import re
import sqlite3
import asyncio
import openai
import sys
import subprocess
from google import genai
from google.genai import types
from datetime import datetime
from pathlib import Path
from dotenv import load_dotenv
from colorama import init, Fore, Style
# ====================================================
# INITIALISIERUNG
# ====================================================
init(autoreset=True)
# ====================================================
# PFADE & SETUP
# ====================================================
BASE_DIR = Path(__file__).resolve().parent
CONFIG_DIR = BASE_DIR / "config"
DATA_DIR = BASE_DIR / "data"
WORKSPACE_DIR = BASE_DIR / "workspace"
ENV_FILE = CONFIG_DIR / ".env"
load_dotenv(ENV_FILE)
DB_PATH = DATA_DIR / "cluster.db"
NOTES_FILE = WORKSPACE_DIR / "NOTIZEN.md"
TODO_FILE = WORKSPACE_DIR / "TODO.md"
CHAT_LOG_FILE = WORKSPACE_DIR / "chat_history.log"
WEB_USER_NAME = os.getenv("WEB_USER_NAME", "Meik")
# ====================================================
# TERMINAL FARBEN
# ====================================================
USER_COLOR = Fore.CYAN
JARVIS_COLOR = Fore.GREEN
SYSTEM_COLOR = Fore.YELLOW
ERROR_COLOR = Fore.RED
OUTPUT_COLOR = Fore.MAGENTA
INFO_COLOR = Fore.BLUE
RESET = Style.RESET_ALL
# ====================================================
# ORDNER & DATEIEN
# ====================================================
for d in [WORKSPACE_DIR, DATA_DIR, CONFIG_DIR]:
d.mkdir(parents=True, exist_ok=True)
for f in [NOTES_FILE, TODO_FILE]:
if not f.exists():
f.write_text(
f"# {f.name}\nHier fängt dein Gedächtnis an, J.A.R.V.I.S.\n",
encoding="utf-8"
)
# ====================================================
# KI KONFIGURATION
# ====================================================
AI_PROVIDER = os.getenv("AI_PROVIDER", "google").lower()
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY", "")
NVIDIA_API_KEY = os.getenv("NVIDIA_API_KEY", "")
GROQ_API_KEY = os.getenv("GROQ_API_KEY", "")
OLLAMA_BASE_URL = os.getenv(
"OLLAMA_BASE_URL",
"http://127.0.0.1:11434/v1"
)
GOOGLE_MODEL = os.getenv(
"GOOGLE_MODEL",
"gemini-2.5-flash"
)
OPENAI_MODEL = os.getenv(
"OPENAI_MODEL",
"gpt-4o"
)
OLLAMA_MODEL = os.getenv(
"OLLAMA_MODEL",
"llama3"
)
NVIDIA_MODEL = os.getenv(
"NVIDIA_MODEL",
"moonshotai/kimi-k2.6"
)
GROQ_MODEL = os.getenv(
"GROQ_MODEL",
"meta-llama/llama-4-scout-17b-16e-instruct"
)
# ====================================================
# DATENBANK
# ====================================================
def init_db():
conn = sqlite3.connect(DB_PATH)
conn.execute('''
CREATE TABLE IF NOT EXISTS nodes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
ip TEXT UNIQUE,
user TEXT,
sudo_password TEXT,
os TEXT DEFAULT 'Unbekannt',
arch TEXT DEFAULT 'Unbekannt',
docker_installed INTEGER DEFAULT 0,
status TEXT
)
''')
conn.commit()
conn.close()
init_db()
def get_db():
conn = sqlite3.connect(DB_PATH)
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
# ====================================================
def get_system_prompt():
prompt_path = CONFIG_DIR / "system_prompt.txt"
prompt = (
prompt_path.read_text(encoding="utf-8")
if prompt_path.exists()
else f"Hallo {WEB_USER_NAME}, ich bin J.A.R.V.I.S."
)
prompt = prompt.replace("{user_name}", WEB_USER_NAME)
prompt = prompt.replace("{workspace_dir}", str(WORKSPACE_DIR))
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()
conn.close()
node_info = ""
for n in nodes:
node_info += f"- Name: {n['name']}, IP: {n['ip']}, User: {n['user']}\n"
return prompt.replace("{node_info}", node_info)
# ====================================================
# KI KOMMUNIKATION
# ====================================================
async def get_ai_response(user_msg, system_prompt, history_list):
try:
if AI_PROVIDER in ["openai", "ollama", "nvidia", "groq"]:
# ========================================
# PAYLOAD WASCHEN (Für strikte APIs wie Groq)
# ========================================
clean_history = [
{
"role": msg["role"],
"content": msg["content"]
}
for msg in history_list
]
messages = [
{
"role": "system",
"content": system_prompt
}
] + clean_history
if AI_PROVIDER == "ollama":
url = (
OLLAMA_BASE_URL
if OLLAMA_BASE_URL.endswith('/v1')
else OLLAMA_BASE_URL.rstrip('/') + '/v1'
)
key = "ollama"
model_to_use = OLLAMA_MODEL
elif AI_PROVIDER == "nvidia":
url = "https://integrate.api.nvidia.com/v1"
key = NVIDIA_API_KEY
model_to_use = NVIDIA_MODEL
elif AI_PROVIDER == "groq":
url = "https://api.groq.com/openai/v1"
key = GROQ_API_KEY
model_to_use = GROQ_MODEL
else:
url = None
key = OPENAI_API_KEY
model_to_use = OPENAI_MODEL
client = openai.AsyncOpenAI(
base_url=url,
api_key=key
)
response = await client.chat.completions.create(
model=model_to_use,
messages=messages
)
return response.choices[0].message.content
elif AI_PROVIDER == "google":
client = genai.Client(api_key=GOOGLE_API_KEY)
google_history = [
types.Content(
role="user" if msg["role"] == "user" else "model",
parts=[
types.Part.from_text(
text=msg["content"]
)
]
)
for msg in history_list[:-1]
]
chat = client.chats.create(
model=GOOGLE_MODEL,
config=types.GenerateContentConfig(
system_instruction=system_prompt
),
history=google_history
)
return chat.send_message(user_msg).text
except Exception as e:
return f"Fehler bei der KI-Anfrage: {e}"
# ====================================================
# BEFEHLSAUSFÜHRUNG
# ====================================================
async def run_task(target, cmd):
print(
f"\n{SYSTEM_COLOR}"
f"⚙️ STARTE TASK AUF [{target}]"
f"{RESET}"
)
print(
f"{INFO_COLOR}"
f"➡️ {cmd}"
f"{RESET}\n"
)
try:
# ========================================
# GUI APPS ERKENNEN
# ========================================
# Holt sich alle bekannten System-GUI-Befehle dynamisch
gui_apps = list(get_installed_gui_apps().values())
first_word = cmd.strip().split()[0]
is_gui_app = (
first_word in gui_apps
or cmd.strip().endswith("&")
)
# ========================================
# GUI APPS DETACHED STARTEN
# ========================================
if is_gui_app:
detached_cmd = (
f"nohup {cmd.replace('&', '').strip()} "
f">/dev/null 2>&1 &"
)
print(
f"{SYSTEM_COLOR}"
f"🖥️ GUI-APP erkannt → Detached Mode"
f"{RESET}"
)
if target.lower() in ["localhost", "127.0.0.1"]:
proc = await asyncio.create_subprocess_shell(
detached_cmd
)
else:
conn = get_db()
n = conn.execute(
'SELECT * FROM nodes WHERE ip=? OR name=?',
(target, target)
).fetchone()
conn.close()
if not n:
err = (
f"Node '{target}' "
f"nicht gefunden."
)
print(
f"{ERROR_COLOR}{err}{RESET}"
)
return err
ssh_cmd = (
f"ssh "
f"-o StrictHostKeyChecking=no "
f"-o LogLevel=ERROR "
f"{n['user']}@{n['ip']} "
f"'{detached_cmd}'"
)
proc = await asyncio.create_subprocess_shell(
ssh_cmd
)
await proc.wait()
print(
f"{JARVIS_COLOR}"
f"✅ GUI-Programm gestartet"
f"{RESET}\n"
)
return "GUI application started."
# ========================================
# NORMALE COMMANDS
# ========================================
else:
if target.lower() in ["localhost", "127.0.0.1"]:
final_cmd = cmd
else:
conn = get_db()
n = conn.execute(
'SELECT * FROM nodes WHERE ip=? OR name=?',
(target, target)
).fetchone()
conn.close()
if not n:
err = (
f"Node '{target}' "
f"nicht gefunden."
)
print(
f"{ERROR_COLOR}{err}{RESET}"
)
return err
final_cmd = (
f"ssh "
f"-o StrictHostKeyChecking=no "
f"-o LogLevel=ERROR "
f"{n['user']}@{n['ip']} "
f"'{cmd}'"
)
proc = await asyncio.create_subprocess_shell(
final_cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.STDOUT
)
collected_output = []
# ========================================
# 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
decoded = line.decode("utf-8", errors="ignore").rstrip()
collected_output.append(decoded)
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}")
# 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()
except Exception as e:
err = f"❌ Fehler: {e}"
print(
f"{ERROR_COLOR}"
f"{err}"
f"{RESET}\n"
)
return err
# ====================================================
# FILE LOGGING
# ====================================================
def log_to_file(role, content):
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
try:
with open(CHAT_LOG_FILE, "a", encoding="utf-8") as f:
f.write(f"[{now}] {role.upper()}:\n{content}\n{'-'*60}\n")
except Exception as e:
print(f"{ERROR_COLOR}⚠️ Konnte nicht ins Log schreiben: {e}{RESET}")
# ====================================================
# USER INPUT
# ====================================================
async def listen_to_user():
return await asyncio.to_thread(
input,
f"\n{USER_COLOR}👤 Du:{RESET} "
)
# ====================================================
# JARVIS OUTPUT
# ====================================================
async def speak_to_user(text):
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")
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
# ====================================================
async def main_chat_loop():
print(f"{INFO_COLOR}")
print("====================================================")
print("🤖 J.A.R.V.I.S. Terminal Interface geladen")
print(f"🧠 Provider: {AI_PROVIDER.upper()}")
print("⌨️ Tippe 'exit', um zu beenden")
print("====================================================")
print(RESET)
chat_history = []
while True:
user_msg = await listen_to_user()
if user_msg.lower().strip() in ['exit', 'quit']:
print(
f"{SYSTEM_COLOR}"
f"\nJ.A.R.V.I.S. geht offline."
f"{RESET}"
)
break
if not user_msg.strip():
continue
now = datetime.now().strftime("%d.%m.%Y %H:%M")
chat_history.append({
"role": "user",
"content": user_msg,
"timestamp": now
})
# LOG: User Eingabe hier schreiben!
log_to_file("Du", user_msg)
print(
f"{SYSTEM_COLOR}"
f"🧠 J.A.R.V.I.S. denkt nach..."
f"{RESET}",
end="\r"
)
system_prompt = get_system_prompt()
ai_response = await get_ai_response(
user_msg,
system_prompt,
chat_history
)
# ============================================
# EXECUTE TAGS SUCHEN
# Unterstützt:
# <EXECUTE>cmd</EXECUTE>
# <EXECUTE target="xyz">cmd</EXECUTE>
# ============================================
commands = []
# Toleranter Regex: Erlaubt Leerzeichen vor dem '>'
execute_matches = re.finditer(
r'<EXECUTE[^>]*?(?:target="(.*?)")?[^>]*>(.*?)</EXECUTE>',
ai_response,
re.I | re.S
)
for match in execute_matches:
target = match.group(1)
cmd = match.group(2)
if not target:
target = "localhost"
# Markdown-Backticks bereinigen, falls die KI sie in den Tag mogelt
cmd = cmd.strip()
cmd = re.sub(r'^```[a-zA-Z]*\n?', '', cmd)
cmd = re.sub(r'\n?```$', '', cmd)
cmd = cmd.strip()
commands.append((target.strip(), cmd))
# Die Tags für die Sprach-/Textausgabe sauber entfernen
clean_msg = re.sub(
r'<EXECUTE[^>]*?>.*?</EXECUTE>',
'',
ai_response,
flags=re.I | re.S
).strip()
# ============================================
# JARVIS TEXT
# ============================================
if clean_msg:
await speak_to_user(clean_msg)
chat_history.append({
"role": "assistant",
"content": clean_msg,
"timestamp": now
})
# LOG: Jarvis Antwort hier schreiben!
log_to_file("J.A.R.V.I.S.", clean_msg)
# ============================================
# COMMANDS AUSFÜHREN
# ============================================
if commands:
for target, cmd in commands:
target = target.strip()
cmd = cmd.strip()
action_msg = (
f"⚙️ Ich führe jetzt folgenden Befehl "
f"auf [{target}] aus:\n\n"
f"{cmd}"
)
# ÄNDERUNG: Auch hier nur im Terminal anzeigen, NICHT vorlesen!
print(f"\n{SYSTEM_COLOR}{action_msg}{RESET}\n")
log_to_file("SYSTEM", action_msg)
# ========================================
# COMMAND AUSFÜHREN
# ========================================
output = await run_task(
target,
cmd
)
# ========================================
# OUTPUT IM CHAT SICHTBAR MACHEN
# ========================================
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")
chat_history.append({
"role": "assistant",
"content": output_msg,
"timestamp": sys_now
})
log_to_file("SYSTEM", output_msg)
# ============================================
# HISTORY LIMIT
# ============================================
if len(chat_history) > 20:
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:
# 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}⛔ J.A.R.V.I.S. hart beendet.{RESET}")

6
requirements.txt Normal file
View File

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

608
setup_wayland_jarvis.sh Executable file
View File

@@ -0,0 +1,608 @@
#!/bin/bash
# Abbrechen bei Fehlern
set -e
echo "===================================================="
echo "🚀 Starte J.A.R.V.I.S. Desktop OS - Pure Local Setup"
echo "===================================================="
# Benutzererkennung
REAL_USER="${SUDO_USER:-$(logname 2>/dev/null || whoami)}"
REAL_HOME=$(getent passwd "$REAL_USER" | cut -d: -f6)
if [ -z "$REAL_USER" ] || [ "$REAL_USER" = "root" ]; then
REAL_USER=$(id -nu 1000 2>/dev/null || echo "meik")
REAL_HOME=$(getent passwd "$REAL_USER" | cut -d: -f6)
fi
JARVIS_DIR="$REAL_HOME/jarvis-ai"
# 1. System aktualisieren & Basispakete installieren
echo "📦 Aktualisiere Paketquellen und installiere Systemkomponenten..."
sudo apt update
sudo apt install -y labwc firefox-esr curl wget git sudo python3 python3-pip python3-venv original-awk tilix geany waybar wlr-randr fonts-noto-color-emoji wofi pipewire pipewire-audio-client-libraries pipewire-pulse wireplumber alsa-utils libasound2-dev libportaudio2 unzip
# 1.1 Gruppenrechte für Grafik und Eingabe
echo "👥 Füge Benutzer '$REAL_USER' zu den Grafik- und Input-Gruppen hinzu..."
sudo usermod -aG video,render,input "$REAL_USER"
systemctl --user --now enable pipewire.service pipewire-pulse.service wireplumber.service
# 2. Architektur erkennen und wdotool installieren
ARCH=$(uname -m)
echo "🔍 Erkannte Systemarchitektur: $ARCH"
if [ "$ARCH" = "x86_64" ]; then
wget https://github.com/cushycush/wdotool/releases/download/v0.5.3/wdotool_0.5.3-1_amd64.deb -O /tmp/wdotool.deb
sudo apt install -y /tmp/wdotool.deb
rm /tmp/wdotool.deb
elif [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/cushycush/wdotool/releases/download/v0.5.3/wdotool-installer.sh | sh
else
echo "❌ Unbekannte Architektur: $ARCH. Installation abgebrochen."
exit 1
fi
# 3. Den realen Installationspfad von wdotool ermitteln
echo "🛠️ Konfiguriere globalen wdotool-Wrapper..."
if [ -f "/usr/bin/wdotool" ]; then REAL_WDOTOOL="/usr/bin/wdotool"
elif [ -f "$REAL_HOME/.cargo/bin/wdotool" ]; then REAL_WDOTOOL="$REAL_HOME/.cargo/bin/wdotool"
elif [ -f "$REAL_HOME/.local/bin/wdotool" ]; then REAL_WDOTOOL="$REAL_HOME/.local/bin/wdotool"
else REAL_WDOTOOL=$(which wdotool || true); fi
if [ -z "$REAL_WDOTOOL" ]; then echo "❌ Fehler: wdotool nicht gefunden!"; exit 1; fi
sudo mv "$REAL_WDOTOOL" /usr/local/bin/wdotool.real
# 4.1 wdotool Wrapper erstellen
sudo cat << 'EOF' | sudo tee /usr/local/bin/wdotool > /dev/null
#!/bin/bash
export XDG_RUNTIME_DIR=/run/user/$(id -u)
export WAYLAND_DISPLAY=wayland-0
exec /usr/local/bin/wdotool.real "$@"
EOF
sudo chmod +x /usr/local/bin/wdotool
# 4.2 wdotool Wrapper erstellen
sudo cat << 'EOF' | sudo tee /usr/local/bin/jwin > /dev/null
#!/bin/bash
ACTION=$1
APP_NAME=$2
PARAM1=$3
WD="wdotool --backend wlr-protocols"
if [ -z "$ACTION" ] || [ -z "$APP_NAME" ]; then
echo "❌ Fehler: Falsche Syntax."
echo "Nutzung: jwin <start|activate|close|snap|maximize> <Programm/Fenstername> [Parameter]"
exit 1
fi
# =========================================================
# 1. SONDERFALL: PROGRAMM STARTEN (Mit Display-Erkennung)
# =========================================================
if [ "$ACTION" == "start" ]; then
shift
if [ -z "$WAYLAND_DISPLAY" ]; then
export WAYLAND_DISPLAY=$(ls /run/user/$(id -u)/wayland-* 2>/dev/null | head -n 1 | xargs basename)
[ -z "$WAYLAND_DISPLAY" ] && export WAYLAND_DISPLAY="wayland-0"
fi
if [ -z "$DISPLAY" ]; then
export DISPLAY=":0"
fi
nohup "$@" >/dev/null 2>&1 &
echo "✅ Programm '$APP_NAME' wurde entkoppelt im Hintergrund gestartet (Display: $WAYLAND_DISPLAY)."
exit 0
fi
# =========================================================
# 2. SONDERFALL: PROGRAMM SCHLIESSEN (Direkt & unfehlbar)
# =========================================================
if [ "$ACTION" == "close" ]; then
# Namen in Kleinbuchstaben umwandeln für maximale Trefferquote
LOW_APP=$(echo "$APP_NAME" | tr '[:upper:]' '[:lower:]')
# Direktes Signal an das System senden
pkill -f "$LOW_APP"
echo "✅ Schließ-Signal (pkill) an '$APP_NAME' gesendet."
exit 0
fi
# =========================================================
# 3. WAYLAND-SUCHE (Nur noch für activate, snap, maximize)
# =========================================================
if [ "$APP_NAME" == "active" ]; then
WINDOW_ID=$($WD getactivewindow 2>/dev/null | awk '{print $1}')
else
WINDOW_LINE=$($WD search --ignore-case --any --name "$APP_NAME" --class "$APP_NAME" 2>/dev/null | head -n 1)
if [ -z "$WINDOW_LINE" ]; then
SAFE_APP_NAME=$(echo "$APP_NAME" | sed 's/[.[\*^$]/\\&/g')
WINDOW_LINE=$($WD search --ignore-case --regex --any --name "$SAFE_APP_NAME" --class "$SAFE_APP_NAME" 2>/dev/null | head -n 1)
fi
WINDOW_ID=$(echo "$WINDOW_LINE" | awk '{print $1}')
fi
case "$ACTION" in
activate)
if [ -n "$WINDOW_ID" ]; then
$WD windowactivate "$WINDOW_ID"
echo "✅ Fenster '$APP_NAME' (ID: $WINDOW_ID) ist jetzt im Fokus."
else
$WD key alt+Tab
echo "⚠️ Keine direkte Fenster-ID gefunden, wechsle Fokus via Alt+Tab."
fi
;;
maximize)
if [ -n "$WINDOW_ID" ]; then
$WD windowactivate "$WINDOW_ID"
sleep 0.1
fi
$WD key super+up
echo "✅ Maximieren-Signal gesendet."
;;
snap)
if [ -z "$PARAM1" ]; then
echo "❌ Fehler: Für 'snap' wird eine Richtung benötigt."
exit 1
fi
if [ -n "$WINDOW_ID" ]; then
$WD windowactivate "$WINDOW_ID"
sleep 0.1
fi
case "$PARAM1" in
left) $WD key super+Left ;;
right) $WD key super+Right ;;
up) $WD key super+Up ;;
down) $WD key super+Down ;;
top-left) $WD key super+shift+Left ;;
top-right) $WD key super+shift+Up ;;
bottom-right) $WD key super+shift+Down ;;
bottom-left) $WD key super+shift+Right ;;
*) echo "❌ Unbekannte Richtung: $PARAM1" ; exit 1 ;;
esac
echo "✅ Fenster '$APP_NAME' nach $PARAM1 angedockt."
;;
*)
echo "❌ Unbekannte Aktion: $ACTION."
exit 1
;;
esac
EOF
sudo chmod +x /usr/local/bin/jwin
# 5. Desktop-Konfiguration (labwc & environment)
echo "📂 Konfiguriere labwcAutostart und Tastaturlayout..."
mkdir -p "$REAL_HOME/.config/labwc"
cat << 'EOF' > "$REAL_HOME/.config/labwc/environment"
dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP=labwc
XKB_DEFAULT_LAYOUT=de
EOF
# 6. Auto-Start in .profile eintragen
echo "⚙️ Richte .profile für Wayland Auto-Start ein..."
if ! grep -q "labwc" "$REAL_HOME/.profile"; then
cat << 'EOF' >> "$REAL_HOME/.profile"
if [ "$(tty)" = "/dev/tty1" ]; then
export WLR_RENDERER=pixman
export WLR_NO_HARDWARE_CURSORS=1
export LIBGL_ALWAYS_SOFTWARE=1
dbus-run-session labwc > ~/labwc.log 2>&1
logout
fi
EOF
fi
# 7. Waybar & Wofi Powermenu
echo "📊 Konfiguriere Waybar und Wofi Power-Menü..."
mkdir -p "$REAL_HOME/.config/waybar"
cat << 'EOF' > "$REAL_HOME/.config/waybar/config"
{
"layer": "top", "position": "bottom", "height": 34,
"modules-left": ["custom/terminal", "custom/browser", "wlr/taskbar"],
"modules-right": ["clock", "custom/logout"],
"custom/terminal": { "format": "📁 Term", "on-click": "tilix", "tooltip": false },
"custom/browser": { "format": "🌐 Web", "on-click": "MOZ_WEBRENDER=software MOZ_ENABLE_WAYLAND=1 firefox-esr", "tooltip": false },
"wlr/taskbar": { "format": "{icon}", "icon-size": 16, "on-click": "activate", "on-click-right": "minimize" },
"clock": { "format": "🕒 {:%H:%M:%S}", "interval": 1 },
"custom/logout": { "format": "⚙️ System ", "on-click": "~/.config/labwc/powermenu.sh", "tooltip": false }
}
EOF
cat << 'EOF' > "$REAL_HOME/.config/waybar/style.css"
* { font-family: sans-serif; font-size: 12px; }
window#waybar { background-color: #1e293b; color: white; border-top: 1px solid #334155; }
#custom-terminal, #custom-browser { padding: 0 8px; background: #334155; margin: 3px 2px; border-radius: 3px; }
#custom-terminal:hover, #custom-browser:hover { background: #475569; }
#taskbar button { padding: 0 10px; color: #94a3b8; }
#taskbar button.active { background-color: #0f172a; color: #38bdf8; }
#clock { padding: 0 15px; background-color: #0f172a; }
#custom-logout { padding: 0 12px; background-color: #ef4444; color: white; }
#custom-logout:hover { background-color: #dc2626; }
EOF
cat << 'EOF' > "$REAL_HOME/.config/labwc/powermenu.sh"
#!/bin/bash
OPTIONS="🚪 Abmelden\n🔄 Neu starten\n🛑 Herunterfahren"
CHOICE=$(echo -e "$OPTIONS" | wofi --dmenu --prompt "Systemaktion wählen:" --width 280 --height 180 --style "$HOME/.config/labwc/wofi-power.css")
case "$CHOICE" in
*"Abmelden") labwc --exit ;;
*"Neu starten") sudo systemctl reboot ;;
*"Herunterfahren") sudo systemctl poweroff ;;
esac
EOF
chmod +x "$REAL_HOME/.config/labwc/powermenu.sh"
cat << 'EOF' > "$REAL_HOME/.config/labwc/wofi-power.css"
window { background-color: #1e293b; color: white; border: 2px solid #334155; border-radius: 8px; font-family: sans-serif; }
#entry { padding: 8px; color: white; }
#entry:selected { background-color: #334155; color: #38bdf8; }
#input { background-color: #0f172a; color: white; border: 1px solid #334155; margin: 5px; }
EOF
echo "%sudo ALL=(ALL) NOPASSWD: /usr/bin/systemctl poweroff, /usr/bin/systemctl reboot" | sudo tee /etc/sudoers.d/powermenu > /dev/null
sudo chmod 0440 /etc/sudoers.d/powermenu
# ===================================================================
# 8. J.A.R.V.I.S. REIN LOKALE INSTALLATION
# ===================================================================
echo "🧠 Installiere J.A.R.V.I.S. AI Desktop Backend..."
mkdir -p "$JARVIS_DIR"/{config,workspace}
# J.A.R.V.I.S. Requirements
cat << 'EOF' > "$JARVIS_DIR/requirements.txt"
openai
google-genai
python-dotenv
EOF
# J.A.R.V.I.S. .env Template
cat << 'EOF' > "$JARVIS_DIR/config/.env"
WEB_USER_NAME=$REAL_USER
AI_PROVIDER=nvdia
OPENAI_API_KEY=dein-openai-key
OPENAI_MODEL=
GOOGLE_API_KEY=dein-google-key
NVIDIA_API_KEY=dein-nvidia-key
NVIDIA_MODEL=moonshotai/kimi-k2.6
GOOGLE_MODEL=gemini-2.5-flash
OLLAMA_BASE_URL=http://127.0.0.1:11434/v1
OLLAMA_MODEL=llama3
GROQ_API_KEY=dein-groq-key
GROQ_MODEL=groq/compound
EOF
# J.A.R.V.I.S. Optimierter System-Prompt inkl. wdotool-Handbuch
cat << 'EOF' > "$JARVIS_DIR/config/system_prompt.txt"
Du bist J.A.R.V.I.S., ein KI-Systemassistent, der direkt auf einem Debian Wayland-Desktop läuft. Du hast vollen lokalen Zugriff auf das System.
UMGEBUNG & GEDÄCHTNIS:
- Arbeitsverzeichnis: {workspace_dir}
- Notizen: {notes_file}
- Todos: {todo_file}
DESKTOP STEUERUNG & FENSTER-MANAGEMENT:
Du steuerst die grafische Oberfläche (Wayland/labwc) über Befehlszeilen-Tools.
1. Programme & Fenster verwalten (Tool: jwin)
Du kannst Fenster steuern, indem du das jwin-Skript aufrufst.
Folgende Aktionen sind erlaubt:
- start (Startet ein Programm)
- activate (Holt ein Fenster in den Vordergrund)
- close (Schließt das Fenster)
- snap (Dockt das Fenster an. Parameter: left, right, up, down, top-left, bottom-left, top-right, bottom-right)
- maximize (Maximiert das Fenster)
Beispiele:
<EXECUTE>jwin snap firefox right</EXECUTE> (Schiebt Firefox auf die rechte Bildschirmhälfte)
<EXECUTE>jwin snap firefox bottom-right</EXECUTE> (Schiebt Firefox auf die rechte untere Bildschirmviertel)
<EXECUTE>jwin activate terminal</EXECUTE> (Holt das Terminal in den Vordergrund)
Regel für Multitasking-Fenster: Wenn der User mehrere Instanzen derselben App starten und verteilen möchte, führe die Befehle immer abwechselnd aus und nutze das Schlüsselwort active für das Snapping:
Beispiel-Verkettung für das System:
<EXECUTE>jwin start firefox && sleep 1.5 && jwin snap active top-left && jwin start firefox && sleep 1.5 && jwin snap active top-right</EXECUTE>
2. Tastatur & Maus (Tool: wdotool)
- Tastatur: wdotool key ctrl+l, wdotool key alt+Tab, wdotool type "Hallo"
- Maus: wdotool mousemove 500 400 (absolut), wdotool click 1 (1=links, 3=rechts)
3. System- & Display-Infos
- Bildschirmauflösung ermitteln: <EXECUTE>wlr-randr | grep current | awk '{print $1}'</EXECUTE>
{installed_apps}
WICHTIGE REGELN FÜR DIE AUSFÜHRUNG:
1. Wenn der Nutzer nach einer App fragt (z.B. "starte den Editor"), schaue in der obigen Liste nach dem passenden Programmnamen und nimm EXAKT den dort definierten Befehl.
2. Rate niemals Befehle, die nicht in der Liste stehen!
3. Wenn du eine Aktion ausführst, MUSST du den Linux-Befehl EXAKT in <EXECUTE> und </EXECUTE> Tags setzen.
4. VERBOTEN: Verwende NIEMALS Markdown-Codeblöcke (```) um oder in den <EXECUTE>-Tags! Schreibe die Tags als simplen, rohen Text.
5. ERZWUNGEN: Sag nicht nur, dass du etwas tust du MUSST den <EXECUTE> Tag in deiner Antwort mitsenden, sonst passiert nichts!
Beispiel für einen perfekten Workflow:
Das mache ich sofort für dich!
<EXECUTE>jwin activate Firefox && sleep 1 && jwin move Firefox 0 0</EXECUTE>
WICHTIGE REGELN FÜR TEXTE IN EDITOREN:
1. Wenn der Nutzer einen Text (wie eine Einladung, Notiz oder Code) in einem Editor wie Geany erstellen möchte, erstelle den Text NIEMALS direkt mit "wdotool type" in einer langen Kette! Das ist zu fehleranfällig.
2. Nutze stattdessen IMMER diesen zweistufigen, krisenfesten Weg:
Schritt A: Schreibe den generierten Text zuerst in eine temporäre Datei (z.B. mit echo oder cat).
Schritt B: Öffne diese Datei anschließend direkt mit Geany.
Beispiel für das korrekte Vorgehen:
<EXECUTE>cat << 'EOF' > /tmp/einladung.txt
Liebe Familie...
'EOF'
geany /tmp/einladung.txt</EXECUTE>
Schreibe immer eine kurze Textantwort dazu, was du gerade tust. Du duzt {user_name} konsequent, dein Tonfall ist locker und technisch versiert.
EOF
chmod +x "$JARVIS_DIR/start.sh"
# Rechte korrigieren
chown -R "$REAL_USER:$REAL_USER" "$JARVIS_DIR"
chown -R "$REAL_USER:$REAL_USER" "$REAL_HOME/.config"
chown "$REAL_USER:$REAL_USER" "$REAL_HOME/.profile"
# Python venv installieren
echo "🐍 Erstelle Python-Umgebung für J.A.R.V.I.S...."
sudo -u "$REAL_USER" bash -c "cd $JARVIS_DIR && python3 -m venv venv && ./venv/bin/pip install --upgrade pip && ./venv/bin/pip install -r requirements.txt"
# Autostart (Waybar & JARVIS direkt in Tilix öffnen)
cat << EOF > "$REAL_HOME/.config/labwc/autostart"
dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP=labwc
waybar &
tilix --title="J.A.R.V.I.S. Terminal" -e "$JARVIS_DIR/start.sh" &
EOF
# Shortcuts
cat << EOF > "$REAL_HOME/.config/labwc/rc.xml"
<?xml version="1.0"?>
<!--
This is a very simple config file with many options missing. For a complete
set of options with comments, see docs/rc.xml.all
-->
<labwc_config>
<theme>
<name></name>
<cornerRadius>8</cornerRadius>
<font name="sans" size="10" />
</theme>
<regions>
<region name="top-left" x="0%" y="0%" height="50%" width="50%" />
<region name="top" x="0%" y="0%" height="50%" width="100%" />
<region name="top-right" x="50%" y="0%" height="50%" width="50%" />
<region name="left" x="0%" y="0%" height="100%" width="50%" />
<region name="center" x="10%" y="10%" height="80%" width="80%" />
<region name="right" x="50%" y="0%" height="100%" width="50%" />
<region name="bottom-left" x="0%" y="50%" height="50%" width="50%" />
<region name="bottom" x="0%" y="50%" height="50%" width="100%" />
<region name="bottom-right" x="50%" y="50%" height="50%" width="50%" />
</regions>
<keyboard>
<numlock>on</numlock>
<keybind key="W-F11">
<action name="Maximize" />
</keybind>
<keybind key="W-Left">
<action name="SnapToEdge" direction="left" />
</keybind>
<keybind key="W-Right">
<action name="SnapToEdge" direction="right" />
</keybind>
<keybind key="W-Up">
<action name="SnapToEdge" direction="up" />
</keybind>
<keybind key="W-Down">
<action name="SnapToEdge" direction="down" />
</keybind>
<!-- SnapToRegion via W-Numpad -->
<keybind key="W-KP_7">
<action name="SnapToRegion" region="top-left" />
</keybind>
<keybind key="W-KP_8">
<action name="SnapToRegion" region="top" />
</keybind>
<keybind key="W-KP_9">
<action name="SnapToRegion" region="top-right" />
</keybind>
<keybind key="W-KP_4">
<action name="SnapToRegion" region="left" />
</keybind>
<keybind key="W-KP_5">
<action name="SnapToRegion" region="center" />
</keybind>
<keybind key="W-KP_6">
<action name="SnapToRegion" region="right" />
</keybind>
<keybind key="W-KP_1">
<action name="SnapToRegion" region="bottom-left" />
</keybind>
<keybind key="W-KP_2">
<action name="SnapToRegion" region="bottom" />
</keybind>
<keybind key="W-KP_3">
<action name="SnapToRegion" region="bottom-right" />
</keybind>
<keybind key="W-S-Left">
<action name="SnapToRegion" region="top-left" />
</keybind>
<keybind key="W-S-Up">
<action name="SnapToRegion" region="top-right" />
</keybind>
<keybind key="W-S-Down">
<action name="SnapToRegion" region="bottom-right" />
</keybind>
<keybind key="W-S-Right">
<action name="SnapToRegion" region="bottom-left" />
</keybind>
</keyboard>
<mouse>
<default />
<!-- Show a custom menu on desktop right click -->
<context name="Root">
<mousebind button="Right" action="Press">
<action name="ShowMenu" menu="some-custom-menu" />
</mousebind>
</context>
</mouse>
</labwc_config>
EOF
chmod +x "$REAL_HOME/.config/labwc/autostart"
chown "$REAL_USER:$REAL_USER" "$REAL_HOME/.config/labwc/autostart"
####################################
# Voice setup
####################################
# Modell herunterladen
wget https://alphacephei.com/vosk/models/vosk-model-small-de-0.15.zip
# Entpacken
unzip vosk-model-small-de-0.15.zip
# Ordner umbenennen, damit das Skript ihn leicht findet
mv vosk-model-small-de-0.15 model
rm vosk-model-small-de-0.15.zip
cat << 'EOF' > "$JARVIS_DIR/wakeword.py"
#!/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()
EOF
# Piper installieren
wget https://github.com/rhasspy/piper/releases/download/v1.2.0/piper_amd64.tar.gz
tar -xf piper_amd64.tar.gz
rm piper_amd64.tar.gz
# Das eigentliche Sprachmodell (.onnx)
wget https://huggingface.co/rhasspy/piper-voices/resolve/main/de/de_DE/thorsten/high/de_DE-thorsten-high.onnx
# Die dazugehörige Konfigurationsdatei (.json)
wget https://huggingface.co/rhasspy/piper-voices/resolve/main/de/de_DE/thorsten/high/de_DE-thorsten-high.onnx.json
echo "===================================================="
echo "✅ Lokales Setup erfolgreich abgeschlossen!"
echo "👉 1. Trage deine API-Keys in $JARVIS_DIR/config/.env ein."
echo "👉 2. Starte das System neu oder logge dich neu ein."
echo "===================================================="

12
start.sh Executable file
View File

@@ -0,0 +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()