+ jarvis-voice

This commit is contained in:
2026-05-26 22:17:14 +02:00
parent b9d198497d
commit 16aa40492c
3 changed files with 365 additions and 168 deletions

246
setup_wayland_jarvis.sh Normal file → Executable file
View File

@@ -21,7 +21,7 @@ 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
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..."
@@ -69,6 +69,8 @@ sudo cat << 'EOF' | sudo tee /usr/local/bin/jwin > /dev/null
ACTION=$1
APP_NAME=$2
PARAM1=$3
WD="wdotool --backend wlr-protocols"
if [ -z "$ACTION" ] || [ -z "$APP_NAME" ]; then
echo "❌ Fehler: Falsche Syntax."
@@ -80,41 +82,44 @@ fi
# 1. SONDERFALL: PROGRAMM STARTEN (Mit Display-Erkennung)
# =========================================================
if [ "$ACTION" == "start" ]; then
shift
# Falls WAYLAND_DISPLAY nicht gesetzt ist (z.B. im JARVIS-Dienst),
# versuchen wir es automatisch zu erraten (meistens wayland-0)
shift
if [ -z "$WAYLAND_DISPLAY" ]; then
export WAYLAND_DISPLAY=$(ls /run/user/$(id -u)/wayland-* 2>/dev/null | head -n 1 | xargs basename)
# Fallback auf Standard, falls obiges leer ist
[ -z "$WAYLAND_DISPLAY" ] && export WAYLAND_DISPLAY="wayland-0"
fi
# Sicherheits-Fallback für ältere X11/XWayland Apps wie Geany
if [ -z "$DISPLAY" ]; then
export DISPLAY=":0"
fi
# Das Programm mit den exportierten Variablen starten
nohup "$@" >/dev/null 2>&1 &
echo "✅ Programm '$APP_NAME' wurde entkoppelt im Hintergrund gestartet (Display: $WAYLAND_DISPLAY)."
exit 0
fi
# =========================================================
# 2. WAYLAND AKTIONEN (Fokus-optimiert)
# 2. SONDERFALL: PROGRAMM SCHLIESSEN (Direkt & unfehlbar)
# =========================================================
PARAM1=$3
WD="wdotool --backend wlr-protocols"
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
# Absolut kugelsicher: Nimm einfach das Fenster, das GERADE aktiv ist
WINDOW_ID=$($WD getactivewindow 2>/dev/null | awk '{print $1}')
else
# Normale Suche für gezielte Befehle (wie "schließe geany")
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)
@@ -122,64 +127,55 @@ else
WINDOW_ID=$(echo "$WINDOW_LINE" | awk '{print $1}')
fi
if [ -z "$WINDOW_ID" ]; then
echo "❌ Kein aktives oder passendes Fenster gefunden."
exit 1
fi
case "$ACTION" in
activate)
$WD windowactivate "$WINDOW_ID"
echo "✅ Fenster '$APP_NAME' (ID: $WINDOW_ID) ist jetzt im Fokus."
;;
close)
$WD windowclose "$WINDOW_ID"
echo "✅ Fenster '$APP_NAME' (ID: $WINDOW_ID) wurde geschlossen."
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)
# 1. Fenster aktivieren
$WD windowactivate "$WINDOW_ID"
sleep 0.2
# 2. Labwc Vollbild-Shortcut senden (oft Super+Up oder Alt+F11, passe das an deine rc.xml an!)
$WD key super+up
echo "✅ Fenster '$APP_NAME' maximiert."
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 (left, right, up, down) benötigt."
echo "❌ Fehler: Für 'snap' wird eine Richtung benötigt."
exit 1
fi
# 1. Fenster MUSS zuerst fokussiert werden (Wayland-Sicherheitsregel für Input)
$WD windowactivate "$WINDOW_ID"
sleep 0.2 # Kurze Pause, damit der Fokus greift
# 2. Sende die Tastenkombination an labwc zum Andocken
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 ;;
# --- ECKEN (Super + Shift + Pfeiltasten für maximale Kompatibilität) ---
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. Nutze left, right, up, down." ; exit 1 ;;
*) echo "❌ Unbekannte Richtung: $PARAM1" ; exit 1 ;;
esac
if [ $? -eq 0 ]; then
echo "✅ Fenster '$APP_NAME' nach $PARAM1 angedockt."
else
echo "❌ Fehler beim Senden der Tastenkombination an wdotool."
exit 1
fi
echo "✅ Fenster '$APP_NAME' nach $PARAM1 angedockt."
;;
*)
echo "❌ Unbekannte Aktion: $ACTION. Erlaubt sind: start, activate, close, snap, maximize."
echo "❌ Unbekannte Aktion: $ACTION."
exit 1
;;
esac
EOF
sudo chmod +x /usr/local/bin/jwin
@@ -324,15 +320,31 @@ Beispiel-Verkettung für das System:
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 du eine Aktion ausführst, MUSST du den Linux-Befehl EXAKT in <EXECUTE> und </EXECUTE> Tags setzen.
2. VERBOTEN: Verwende NIEMALS Markdown-Codeblöcke (```) um oder in den <EXECUTE>-Tags! Schreibe die Tags als simplen, rohen Text.
3. ERZWUNGEN: Sag nicht nur, dass du etwas tust du MUSST den <EXECUTE> Tag in deiner Antwort mitsenden, sonst passiert nichts!
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
@@ -1139,6 +1151,136 @@ 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."