feat: initial voice assistant implementation
5-state machine (LAUSCH/AUFNAHME/GENERATOR/VORLESE/DIALOG) with: - Wake-word detection via openwakeword (hey_jarvis) - Speech-to-text via faster-whisper large-v3 (GPU) - LLM streaming via OpenAI-compatible backends (llama.cpp/ollama/openai) - TTS via piper with automatic language detection and voice selection - Selectable mic input (ReSpeaker, MOTU M2, camera, bluetooth, index) - Selectable audio output sink - Control words during playback (Stopp/Pause/Weiter/Noch einmal/…) - "Stopp" abort in all active states → LAUSCH Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
53b0dd4055
5 changed files with 1442 additions and 0 deletions
127
mic.py
Normal file
127
mic.py
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Mikrofon-Auswahl für sounddevice-Anwendungen.
|
||||
|
||||
Kurzname-Aliase:
|
||||
respeaker ReSpeaker XVF3800, 48 kHz (default)
|
||||
motu MOTU M2 Mikrofon-Eingang, 44100 Hz
|
||||
camera USB-Kamera-Mikrofon (VF0680), 44100 Hz
|
||||
default System-Standard-Eingabe (PipeWire)
|
||||
bluetooth erstes verbundenes Bluetooth-Eingabegerät
|
||||
<index> numerischer sounddevice-Index
|
||||
<substring> freie Substring-Suche im Gerätenamen
|
||||
|
||||
Verwendung als Bibliothek:
|
||||
from mic import resolve_mic, print_mic_list
|
||||
device_idx, capture_rate, label = resolve_mic("motu")
|
||||
|
||||
Verwendung als Skript:
|
||||
python3 mic.py # zeigt alle Eingabegeräte
|
||||
"""
|
||||
|
||||
import sys
|
||||
import sounddevice as sd
|
||||
|
||||
# Substring-Muster für bekannte Geräte (case-insensitive Suche)
|
||||
MIC_ALIASES: dict[str, str] = {
|
||||
"respeaker": "reSpeaker", # hw-Gerät kann in=0 melden, trotzdem nutzbar
|
||||
"motu": "M2:",
|
||||
"camera": "VF0680",
|
||||
}
|
||||
DEFAULT_MIC = "respeaker"
|
||||
|
||||
|
||||
def resolve_mic(spec: str) -> tuple[int, int, str]:
|
||||
"""
|
||||
Löst einen Mic-Bezeichner auf.
|
||||
Gibt (device_idx, native_samplerate, label) zurück.
|
||||
|
||||
Reihenfolge: numerischer Index → "default" → "bluetooth" → Alias → Substring.
|
||||
Alias-Suche filtert NICHT nach in-Kanälen (ReSpeaker hw meldet in=0, läuft dennoch).
|
||||
"""
|
||||
devs = sd.query_devices()
|
||||
|
||||
# Numerischer Index
|
||||
if spec.lstrip("-").isdigit():
|
||||
idx = int(spec)
|
||||
d = devs[idx]
|
||||
return idx, int(d["default_samplerate"]), d["name"]
|
||||
|
||||
# System-Standard-Eingabe
|
||||
if spec == "default":
|
||||
idx = sd.default.device[0]
|
||||
if idx < 0:
|
||||
raise ValueError("Kein System-Standard-Eingabegerät gefunden.")
|
||||
d = devs[idx]
|
||||
return idx, int(d["default_samplerate"]), f"{d['name']} (default)"
|
||||
|
||||
# Erstes Bluetooth-Eingabegerät
|
||||
if spec == "bluetooth":
|
||||
for i, d in enumerate(devs):
|
||||
if "bluez" in d["name"].lower() and d["max_input_channels"] > 0:
|
||||
return i, int(d["default_samplerate"]), d["name"]
|
||||
raise ValueError("Kein Bluetooth-Eingabegerät gefunden.")
|
||||
|
||||
# Alias → Substring-Suche ohne channels-Filter
|
||||
pattern = MIC_ALIASES.get(spec, spec)
|
||||
for i, d in enumerate(devs):
|
||||
if pattern.lower() in d["name"].lower():
|
||||
return i, int(d["default_samplerate"]), d["name"]
|
||||
|
||||
raise ValueError(
|
||||
f"Mikrofon '{spec}' nicht gefunden.\n"
|
||||
f"Verfügbare Geräte anzeigen: python3 mic.py"
|
||||
)
|
||||
|
||||
|
||||
def list_mics() -> list[dict]:
|
||||
"""Gibt alle Eingabegeräte mit Metadaten zurück (in-Kanäle > 0)."""
|
||||
return [
|
||||
{
|
||||
"index": i,
|
||||
"name": d["name"],
|
||||
"channels": d["max_input_channels"],
|
||||
"rate": int(d["default_samplerate"]),
|
||||
}
|
||||
for i, d in enumerate(sd.query_devices())
|
||||
if d["max_input_channels"] > 0
|
||||
]
|
||||
|
||||
|
||||
def print_mic_list() -> None:
|
||||
"""Gibt eine formatierte Liste aller Eingabegeräte auf stdout aus."""
|
||||
devs = list_mics()
|
||||
default_in = sd.default.device[0]
|
||||
|
||||
print("Verfügbare Mikrofone:\n")
|
||||
print(f" {'Kurzname':<12} {'Idx':>3} {'Rate':>6} {'Ch':>3} Gerätename")
|
||||
print(f" {'-'*12} {'---':>3} {'------':>6} {'---':>3} {'-'*52}")
|
||||
|
||||
shown: set[int] = set()
|
||||
for alias, pattern in MIC_ALIASES.items():
|
||||
try:
|
||||
idx, rate, name = resolve_mic(alias)
|
||||
except ValueError:
|
||||
continue
|
||||
marker = " *" if idx == default_in else ""
|
||||
print(f" {alias:<12} {idx:>3} {rate:>6} {'?':>3} {name}{marker}")
|
||||
shown.add(idx)
|
||||
|
||||
print(f" {'default':<12} {'':>3} {'':>6} {'':>3} System-Standard-Eingabe (PipeWire)")
|
||||
print(f" {'bluetooth':<12} {'':>3} {'':>6} {'':>3} erstes verbundenes BT-Eingabegerät (dynamisch)")
|
||||
|
||||
others = [d for d in devs if d["index"] not in shown]
|
||||
if others:
|
||||
print("\n Weitere (mit Index oder Namens-Substring ansprechen):")
|
||||
for d in others:
|
||||
marker = " *" if d["index"] == default_in else ""
|
||||
print(
|
||||
f" {'':12} {d['index']:>3} {d['rate']:>6}"
|
||||
f" {d['channels']:>3} {d['name']}{marker}"
|
||||
)
|
||||
|
||||
print("\n * = System-Standard")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print_mic_list()
|
||||
Loading…
Add table
Add a link
Reference in a new issue