my_voice_assistant/mic.py
dschlueter 53b0dd4055 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>
2026-06-16 00:43:02 +02:00

127 lines
4.2 KiB
Python

#!/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()