Compare commits
7 Commits
dev_debian
...
dev_alpine
| Author | SHA1 | Date | |
|---|---|---|---|
| de4e04df20 | |||
| a0029b5818 | |||
| ffd14d7270 | |||
| bda22e4de7 | |||
| 7604b4772a | |||
| ea51a00583 | |||
| 20036495d9 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,8 +1,6 @@
|
||||
# ---> Linux
|
||||
*~
|
||||
./config/*
|
||||
./venv/*
|
||||
./workspace/
|
||||
|
||||
# temporary files which can be created if a process still has a handle open of a deleted file
|
||||
.fuse_hidden*
|
||||
|
||||
@@ -24,7 +22,6 @@
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
825
jarvis.py
825
jarvis.py
@@ -1,825 +0,0 @@
|
||||
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}")
|
||||
@@ -1,6 +0,0 @@
|
||||
openai
|
||||
google-genai
|
||||
python-dotenv
|
||||
vosk
|
||||
sounddevice
|
||||
numpy
|
||||
121
setup_wayland.sh
Normal file
121
setup_wayland.sh
Normal file
@@ -0,0 +1,121 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Farben für eine schöne Ausgabe
|
||||
C_GREEN='\033[1;32m'
|
||||
C_CYAN='\033[1;36m'
|
||||
C_YELLOW='\033[1;33m'
|
||||
C_RED='\033[1;31m'
|
||||
C_RESET='\033[0m'
|
||||
|
||||
echo -e "${C_CYAN}===================================================="
|
||||
echo -e " J.A.R.V.I.S. - Alpine Wayland & wdotool Setup"
|
||||
echo -e "====================================================${C_RESET}"
|
||||
|
||||
# 1. Prüfen, ob das Skript als root läuft
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo -e "${C_RED}Fehler: Dieses Skript muss als root ausgeführt werden!${C_RESET}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 2. Repositories aktualisieren und Basispakete installieren
|
||||
echo -e "\n${C_GREEN}[1/6] Installiere Systempakete (Wayland, Labwc, Rust)...${C_RESET}"
|
||||
apk update
|
||||
apk add wayland labwc foot dbus seatd cargo wayland-dev libxkbcommon-dev build-base bash nano ttf-dejavu ttf-liberation font-noto mesa-dri-gallium mesa-egl mesa-gles xf86-video-modesetting
|
||||
|
||||
# 3. seatd aktivieren und starten (wichtig für Grafik- und Input-Rechte)
|
||||
echo -e "\n${C_GREEN}[2/6] Konfiguriere und starte den seatd-Dienst...${C_RESET}"
|
||||
rc-update add seatd default
|
||||
rc-service seatd start || true
|
||||
|
||||
# 4. Benutzer abfragen und Gruppen zuweisen
|
||||
echo -e "\n${C_GREEN}[3/6] Benutzerkonfiguration...${C_RESET}"
|
||||
if [ -z "$TARGET_USER" ]; then
|
||||
echo -n "Für welchen Benutzernamen soll Wayland eingerichtet werden? "
|
||||
read TARGET_USER
|
||||
fi
|
||||
|
||||
# Falls der User noch nicht existiert, erstellen wir ihn
|
||||
if ! id "$TARGET_USER" >/dev/null 2>&1; then
|
||||
echo -e "${C_YELLOW}Benutzer '$TARGET_USER' existiert nicht. Erstelle Benutzer...${C_RESET}"
|
||||
adduser -D -g "" "$TARGET_USER"
|
||||
echo -e "${C_YELLOW}Bitte lege ein Passwort für '$TARGET_USER' fest:${C_RESET}"
|
||||
passwd "$TARGET_USER"
|
||||
fi
|
||||
|
||||
# Rechte für Hardware-Zugriff ohne root vergeben
|
||||
addgroup "$TARGET_USER" seat
|
||||
addgroup "$TARGET_USER" video || true
|
||||
addgroup "$TARGET_USER" input || true
|
||||
|
||||
# 5. wdotool global für das gesamte System installieren
|
||||
echo -e "\n${C_GREEN}[4/6] Kompiliere und installiere wdotool global...${C_RESET}"
|
||||
echo -e "${C_YELLOW}(Das kann beim ersten Mal 1-2 Minuten dauern, da Cargo die Abhängigkeiten baut)${C_RESET}"
|
||||
# Installiert das Binary direkt sauber nach /usr/local/bin/wdotool
|
||||
cargo install --root /usr/local wdotool
|
||||
|
||||
# 6. Minimale Labwc-Konfiguration für den User anlegen (Openbox-kompatibel)
|
||||
USER_HOME=$(eval echo ~$TARGET_USER)
|
||||
LABWC_CONFIG_DIR="$USER_HOME/.config/labwc"
|
||||
|
||||
echo -e "\n${C_GREEN}[5/6] Erstelle Labwc-Konfigurationsdateien für '$TARGET_USER'...${C_RESET}"
|
||||
mkdir -p "$LABWC_CONFIG_DIR"
|
||||
|
||||
# Minimales Rechtsklick-Menü erstellen
|
||||
cat << 'EOF' > "$LABWC_CONFIG_DIR/menu.xml"
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<group_menu>
|
||||
<menu id="root-menu" label="J.A.R.V.I.S. Base">
|
||||
<item label="Terminal (Foot)">
|
||||
<action name="Execute" command="foot" />
|
||||
</item>
|
||||
<separator />
|
||||
<item label="Exit Wayland">
|
||||
<action name="Exit" />
|
||||
</item>
|
||||
</menu>
|
||||
</group_menu>
|
||||
EOF
|
||||
|
||||
# Autostart-Skript für Labwc anlegen (startet direkt das neue Terminal)
|
||||
cat << 'EOF' > "$LABWC_CONFIG_DIR/autostart"
|
||||
#!/bin/sh
|
||||
# Hier wird später dein J.A.R.V.I.S. Backend/Dashboard geladen
|
||||
foot &
|
||||
EOF
|
||||
chmod +x "$LABWC_CONFIG_DIR/autostart"
|
||||
|
||||
# Eigentümerrechte der Konfiguration an den User übergeben
|
||||
chown -R "$TARGET_USER":"$TARGET_USER" "$USER_HOME/.config"
|
||||
|
||||
# 7. .profile patchen, damit Labwc direkt beim TTY1-Login startet
|
||||
PROFILE_FILE="$USER_HOME/.profile"
|
||||
if ! grep -q "labwc" "$PROFILE_FILE" 2>/dev/null; then
|
||||
echo -e "\n${C_GREEN}[6/6] Richte automatischen Desktop-Start in .profile ein...${C_RESET}"
|
||||
cat << 'EOF' >> "$PROFILE_FILE"
|
||||
# XDG Runtime Directory für Wayland initialisieren
|
||||
if [ -z "$XDG_RUNTIME_DIR" ]; then
|
||||
export XDG_RUNTIME_DIR=/tmp/$(id -u)-runtime-dir
|
||||
if [ ! -d "$XDG_RUNTIME_DIR" ]; then
|
||||
mkdir -p "$XDG_RUNTIME_DIR"
|
||||
chmod 0700 "$XDG_RUNTIME_DIR"
|
||||
fi
|
||||
fi
|
||||
export WLR_RENDERER_ALLOW_SOFTWARE=1
|
||||
export WLR_RENDERER=pixman
|
||||
export WLR_NO_HARDWARE_CURSORS=1
|
||||
export WLR_BACKENDS=headless,libinput
|
||||
# Startet Wayland/Labwc automatisch, wenn du dich auf TTY1 einloggst
|
||||
if [ -z "$DISPLAY" ] && [ -z "$WAYLAND_DISPLAY" ] && [ "$(tty)" = "/dev/tty1" ]; then
|
||||
exec dbus-run-session labwc
|
||||
fi
|
||||
EOF
|
||||
fi
|
||||
|
||||
echo -e "\n${C_CYAN}===================================================="
|
||||
echo -e "${C_GREEN}✅ Setup erfolgreich abgeschlossen!${C_CYAN}"
|
||||
echo -e "====================================================${C_RESET}"
|
||||
echo -e "1. wdotool wurde erfolgreich unter /usr/local/bin/wdotool installiert."
|
||||
echo -e "2. Starte die VM neu oder logge dich aus."
|
||||
echo -e "3. Melde dich als Benutzer '$TARGET_USER' auf TTY1 an."
|
||||
echo -e "4. Wayland startet vollautomatisch und öffnet das Foot-Terminal."
|
||||
@@ -1,608 +0,0 @@
|
||||
#!/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
12
start.sh
@@ -1,12 +0,0 @@
|
||||
#!/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
101
wakeword.py
@@ -1,101 +0,0 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user