20 Commits

Author SHA1 Message Date
b44bd350f6 jarvis.py aktualisiert 2026-05-28 00:10:22 +00:00
082c055683 jarvis.py aktualisiert 2026-05-28 00:03:44 +00:00
cd239fde3c jarvis.py aktualisiert 2026-05-28 00:02:08 +00:00
095d3edc03 wakeword.py aktualisiert 2026-05-27 23:27:31 +00:00
091a553452 wakeword.py aktualisiert 2026-05-27 23:24:41 +00:00
6703aa2082 setup_x11_jarvis.sh aktualisiert 2026-05-27 23:18:37 +00:00
0c4e377fa3 setup_x11_jarvis.sh aktualisiert 2026-05-27 23:15:35 +00:00
4eabde994d setup_x11_jarvis.sh aktualisiert 2026-05-27 23:10:17 +00:00
c069c3d464 setup_x11_jarvis.sh aktualisiert 2026-05-27 22:56:43 +00:00
27fa7ed96f setup_x11_jarvis.sh aktualisiert 2026-05-27 22:53:18 +00:00
ab0b8921c1 setup_x11_jarvis.sh aktualisiert 2026-05-27 22:45:32 +00:00
019815b5b1 setup_x11_jarvis.sh aktualisiert 2026-05-27 22:16:49 +00:00
baeca38fcf setup_x11_jarvis.sh aktualisiert 2026-05-27 07:28:51 +00:00
de37178f7a setup_x11_jarvis.sh aktualisiert 2026-05-27 07:15:27 +00:00
87fabe2489 + system_prompt & .env 2026-05-27 06:52:51 +00:00
fa90f7b1e6 setup_x11_jarvis.sh aktualisiert 2026-05-26 23:14:49 +00:00
b83de316b3 wakeword.py aktualisiert 2026-05-26 22:53:11 +00:00
0578808fc9 requirements.txt aktualisiert 2026-05-26 22:51:50 +00:00
01319a2b8c setup_x11_jarvis.sh aktualisiert 2026-05-26 22:49:49 +00:00
849ad23c64 setup_x11_jarvis.sh 2026-05-26 22:30:16 +00:00
5 changed files with 314 additions and 658 deletions

View File

@@ -5,6 +5,7 @@ import asyncio
import openai
import sys
import subprocess
import edge_tts
from google import genai
from google.genai import types
@@ -544,28 +545,42 @@ async def speak_to_user(text):
print(text)
print(f"{JARVIS_COLOR}{'-'*60}{RESET}\n")
clean_text = re.sub(r'[^\w\s\d.,!?-]', '', text)
"""Generiert eine hochauflösende KI-Stimme via Edge-TTS und spielt sie ab."""
if not text.strip():
return
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
# Definition der Stimme (Killian und Conrad sind hervorragende deutsche Männerstimmen)
VOICE = "de-DE-KillianNeural"
OUTPUT_FILE = "/tmp/jarvis_response.mp3"
LOCK_FILE = "/tmp/.jarvis_speaking"
if os.path.exists(piper_path) and os.path.exists(model_path):
try:
# 1. Sperre setzen
lock_file.touch()
try:
# 1. Erstelle die Lock-Datei, damit das Mikrofon im Wakeword-Skript stummschaltet
with open(LOCK_FILE, "w") as f:
f.write("1")
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"
print(f"🔊 J.A.R.V.I.S. spricht: {text}")
proc = await asyncio.create_subprocess_shell(piper_cmd)
await proc.wait()
# 2. Audio aus der Cloud abrufen (Jetzt sauber mit direktem await!)
communicate = edge_tts.Communicate(text, VOICE)
await communicate.save(OUTPUT_FILE)
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()
# 3. Audio ressourcenschonend & asynchron abspielen
proc = await asyncio.create_subprocess_exec(
"mpv", "--no-video", OUTPUT_FILE,
stdout=asyncio.subprocess.DEVNULL,
stderr=asyncio.subprocess.DEVNULL
)
# Warten, bis mpv fertig gesprochen hat
await proc.wait()
except Exception as e:
print(f"❌ Fehler bei der Sprachausgabe: {e}")
finally:
# 4. Lock-Datei IMMER löschen, damit J.A.R.V.I.S. wieder zuhört
if os.path.exists(LOCK_FILE):
os.remove(LOCK_FILE)
# ====================================================
# MAIN LOOP

View File

@@ -4,3 +4,4 @@ python-dotenv
vosk
sounddevice
numpy
colorama

View File

@@ -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 "===================================================="

212
setup_x11_jarvis.sh Executable file
View File

@@ -0,0 +1,212 @@
#!/bin/bash
# Abbrechen bei Fehlern
set -e
echo "===================================================="
echo "🚀 Starte J.A.R.V.I.S. Desktop OS - X11 Openbox 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 & X11 / Openbox Pakete installieren
echo "📦 Installiere X11-Server, Openbox und Automatisierungstools..."
sudo apt update
sudo apt install -y \
xserver-xorg \
xinit \
x11-xserver-utils \
openbox \
tint2 \
rofi \
xdotool \
wmctrl \
firefox-esr \
curl \
wget \
git \
sudo \
python3 \
python3-pip \
python3-venv \
tilix \
geany \
fonts-noto-color-emoji \
pipewire \
pipewire-audio-client-libraries \
pipewire-pulse \
wireplumber \
alsa-utils \
libasound2-dev \
libportaudio2 \
unzip
# 1.1 Gruppenrechte für Audio und Grafik setzen
echo "👥 Setze Gruppenrechte für '$REAL_USER'..."
sudo usermod -aG video,render,input,audio "$REAL_USER"
# 2. Openbox Konfigurationsverzeichnis erstellen
echo "⚙️ Konfiguriere Openbox für '$REAL_USER'..."
mkdir -p "$REAL_HOME/.config/openbox"
# 3. Autostart-Datei für Openbox erstellen
cat << EOF > "$REAL_HOME/.config/openbox/autostart"
# Tint2 Taskleiste im Hintergrund starten
tint2 &
# Deutsches Tastaturlayout
setxkbmap de &
# PipeWire Audio-Server starten
gentle_pipewire_start() {
systemctl --user start pipewire pipewire-pulse wireplumber
}
gentle_pipewire_start &
# Falls ein Hintergrundbild gewünscht ist (optional, benötigt 'feh'):
# feh --bg-scale /pfad/zum/bild.jpg &
# J.A.R.V.I.S.-AI starten
tilix -e "$JARVIS_DIR/start.sh" &
EOF
chmod +x "$REAL_HOME/.config/openbox/autostart"
# 4. .xinitrc erstellen (ermöglicht den Start via 'startx' aus der Konsole)
cat << EOF > "$REAL_HOME/.xinitrc"
#!/bin/sh
# X11 Umgebungsvariablen setzen falls nötig
export QT_QPA_PLATFORM=xcb
export GDK_BACKEND=x11
# Openbox Session starten
exec openbox-session
EOF
chmod +x "$REAL_HOME/.xinitrc"
# Eigentumsrechte korrigieren
chown -R "$REAL_USER:$REAL_USER" "$REAL_HOME/.config" "$REAL_HOME/.xinitrc"
# J.A.R.V.I.S. .env Template (Leerzeichen bei [ ] und EOF korrigiert)
if [ ! -f "$JARVIS_DIR/config/.env" ]; then
echo "📝 Erstelle .env Konfigurationsdatei..."
# Stelle sicher, dass der config-Ordner existiert
mkdir -p "$JARVIS_DIR/config"
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
fi
# 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 X11-Desktop (Openbox) 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 (X11/Openbox) über native Befehlszeilen-Tools wie wmctrl und xdotool.
Programme & Fenster verwalten
Du steuerst Fenster direkt über Konsolenbefehle.
Folgende Aktionen sind erlaubt:
start (Startet ein Programm in den Hintergrund): firefox &
activate (Holt ein Fenster in den Vordergrund): wmctrl -a "Firefox" (Sucht nach dem Namen im Titel)
close (Schließt das Fenster sanft): wmctrl -c "Firefox"
maximize (Maximiert das Fenster): wmctrl -r "Firefox" -b add,maximized_vert,maximized_horz
positionieren/snappen: Nutze wmctrl mit dem Schalter -e (Format: gravity,X,Y,Width,Height).
Beispiele:
wmctrl -r "Firefox" -b add,maximized_vert,maximized_horz (Maximiert Firefox)
wmctrl -a "Terminal" (Holt das Terminal in den Vordergrund)
Regel für Multitasking-Fenster: Wenn der User mehrere Instanzen derselben App starten möchte, starte sie im Hintergrund, warte kurz, hole das aktive Fenster mit xdotool und passe es an.
Beispiel-Verkettung für das System:
firefox & sleep 2 && wmctrl -r "Mozilla Firefox" -e 0,0,0,960,1080 && firefox & sleep 2 && xdotool getactivewindow windowmove 960 0 windowsize 960 1080
Tastatur & Maus (Tool: xdotool)
Tastatur: xdotool key ctrl+l, xdotool key alt+Tab, xdotool type "Hallo"
Maus: xdotool mousemove 500 400 (absolut), xdotool click 1 (1=links, 3=rechts)
WICHTIG: Wenn du Tasten an ein Programm senden willst, sorge IMMER dafür, dass es vorher den Fokus hat (z.B. wmctrl -a "Firefox" && xdotool key F5).
System- & Display-Infos
Bildschirmauflösung ermitteln: xrandr | grep '*' | awk '{print $1}'
{installed_apps}
WICHTIGE REGELN FÜR DIE AUSFÜHRUNG VON BEFEHLEN:
Das Backend-System wertet deine Befehle NUR aus, wenn sie exakt in XML-Tags eingeschlossen sind. Du darfst Systembefehle unter keinen Umständen als einfachen Text oder in Markdown-Codeblöcken (```) ausgeben!
RICHTIG: <EXECUTE>firefox &</EXECUTE>
FALSCH: ```bash firefox & ```
SPEZIALREGEL FÜR WEBSEITEN & BROWSER:
Navigiere NIEMALS mittels xdotool (ctrl+l) zu einer Webseite! Das ist zu fehleranfällig. Übergib die URL stattdessen IMMER direkt als Argument an den firefox-Befehl. Das öffnet die Seite direkt (oder in einem neuen Tab, falls Firefox bereits läuft).
RICHTIG: <EXECUTE>firefox "google.de" &</EXECUTE>
FALSCH: <EXECUTE>firefox & sleep 2 && xdotool key ctrl+l ...</EXECUTE>
RICHTIGE VERKETTUNG (für andere Anwendungen):
Wenn du mehrere Befehle verknüpfen musst (z.B. App starten und danach pflegen), nutze && innerhalb eines EINZIGEN <EXECUTE>-Blocks.
Beispiel:
<EXECUTE>tilix & sleep 1 && wmctrl -a "Tilix"</EXECUTE>
WICHTIGE REGELN FÜR TEXTE IN EDITOREN:
Wenn du Texte für den Nutzer in einem Editor wie Geany erstellen sollst, nutze immer temporäre Dateien.
RICHTIG:
<EXECUTE>cat << 'EOF' > /tmp/notiz.txt
Dein generierter Text...
'EOF'
geany /tmp/notiz.txt &</EXECUTE>
Antworte immer mit einem kurzen, lockeren Bestätigungssatz, was du tust, gefolgt von dem <EXECUTE>-Block.
Du duzt {user_name} konsequent, dein Tonfall ist locker und technisch versiert.
EOF
# 5. Virtuelle Python-Umgebung einrichten (falls noch nicht geschehen)
echo "🐍 Richte virtuelles Python-Environment ein..."
mkdir -p "$JARVIS_DIR"
python3 -m venv "$JARVIS_DIR/venv"
"$JARVIS_DIR/venv/bin/pip" install --upgrade pip
"$JARVIS_DIR/venv/bin/pip" install -r requirements.txt
####################################
# 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
# 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 "✅ X11 Openbox-Umgebung erfolgreich eingerichtet!"
echo "👉 Starte die grafische Oberfläche einfach mit dem Befehl: startx"
echo "===================================================="

View File

@@ -5,10 +5,21 @@ import json
import queue
import time
import subprocess
import wave
import sounddevice as sd
import numpy as np
from vosk import Model, KaldiRecognizer
from pathlib import Path
from dotenv import load_dotenv
from openai import OpenAI
# ====================================================
# PFADE & ENV SETUP (Aus config/.env lesen)
# ====================================================
BASE_DIR = Path(__file__).resolve().parent
CONFIG_DIR = BASE_DIR / "config"
ENV_FILE = CONFIG_DIR / ".env"
load_dotenv(ENV_FILE)
MODEL_PATH = "model"
AUDIO_RATE = 48000
@@ -18,6 +29,11 @@ if not os.path.exists(MODEL_PATH):
print(f"❌ Modell-Ordner '{MODEL_PATH}' wurde nicht gefunden!")
sys.exit(1)
# OpenAI Client initialisieren
if not os.getenv("OPENAI_API_KEY"):
print("⚠️ Warnung: Kein OPENAI_API_KEY in der .env gefunden!")
openai_client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
audio_queue = queue.Queue()
def audio_callback(indata, frames, time, status):
@@ -25,14 +41,13 @@ def audio_callback(indata, frames, time, status):
print(status, file=sys.stderr)
audio_queue.put(bytes(indata))
print("🧠 J.A.R.V.I.S. lädt das Sprachmodell...")
print("🧠 J.A.R.V.I.S. lädt das Sprachmodell für das Wake-Word...")
model = Model(MODEL_PATH)
# Zwei Recognizer: Einer für das Wake-Word, einer für den eigentlichen Befehl (offen)
# Nur noch EIN Recognizer: Ausschließlich für das Wake-Word ("jarvis")
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')")
print("🎙️ J.A.R.V.I.S. läuft im Hybrid-Modus (Vosk + Whisper) und lauscht... (Sag 'Jarvis')")
with sd.RawInputStream(samplerate=AUDIO_RATE, blocksize=8000, dtype='int16',
channels=1, callback=audio_callback):
@@ -40,56 +55,78 @@ with sd.RawInputStream(samplerate=AUDIO_RATE, blocksize=8000, dtype='int16',
while True:
data = audio_queue.get()
# NEU: Wenn J.A.R.V.I.S. gerade spricht, leere die Queue und ignoriere das Audio
# 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
wake_recognizer.Reset() # Verhindert alte Bruchstücke
continue
# Phase 1: Auf Wake-Word warten
# Phase 1: Auf Wake-Word warten (Lokal via Vosk)
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)
# Bestätigungston abspielen (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
beep = np.sin(2 * np.pi * frequency * t) * 0.3
sd.play(beep, samplerate=AUDIO_RATE)
sd.wait()
# Warteschlange leeren, um alten Ton nicht als Befehl zu interpretieren
# Warteschlange leeren, um den Beep nicht selbst aufzunehmen
while not audio_queue.empty():
audio_queue.get()
print("👂 Höre zu...")
command_text = ""
print("👂 Höre zu (Befehlsaufnahme)...")
collected_chunks = []
start_time = time.time()
# Phase 2: Für 4 Sekunden den darauffolgenden Befehl aufnehmen
# Phase 2: Für 4 Sekunden die Rohdaten aus dem Stream sammeln
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", "")
try:
# Kurzer Timeout, damit die Schleife agil bleibt
cmd_data = audio_queue.get(timeout=0.2)
collected_chunks.append(cmd_data)
except queue.Empty:
continue
# Letzten Rest auslesen
final_res = json.loads(command_recognizer.FinalResult())
command_text += " " + final_res.get("text", "")
command_text = command_text.strip()
print("🧠 Sende Audio an OpenAI Whisper API...")
# Rohe Audio-Bytes zusammenfügen und als WAV speichern
wav_path = "/tmp/jarvis_cmd.wav"
all_bytes = b"".join(collected_chunks)
try:
with wave.open(wav_path, "wb") as wf:
wf.setnchannels(1)
wf.setsampwidth(2) # int16 entspricht 2 Bytes
wf.setframerate(AUDIO_RATE)
wf.writeframes(all_bytes)
# Whisper API aufrufen
with open(wav_path, "rb") as audio_file:
transcription = openai_client.audio.transcriptions.create(
model="whisper-1",
file=audio_file,
language="de" # Erzwingt deutsche Texterkennung
)
command_text = transcription.text.strip()
except Exception as e:
print(f"❌ Fehler bei der Spracherkennung: {e}")
command_text = ""
# Phase 3: Befehl verarbeiten, falls Whisper etwas verstanden hat
if command_text:
print(f"🗣️ Erkannter Befehl: '{command_text}'")
print(f"🗣️ Erkannt (Whisper): '{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",
"venv/bin/python3",
"jarvis.py",
"--voice-cmd",
command_text
])
@@ -98,4 +135,3 @@ with sd.RawInputStream(samplerate=AUDIO_RATE, blocksize=8000, dtype='int16',
print("\n🎙️ Zurück im Standby. Lausche auf 'Jarvis'...")
wake_recognizer.Reset()
command_recognizer.Reset()