fixing input device

This commit is contained in:
Matthias Hinrichs
2026-01-29 22:49:26 +01:00
parent 92a77ee944
commit a8a3ec9b9f
3 changed files with 72 additions and 71 deletions
+62 -70
View File
@@ -2,20 +2,38 @@ import os
import time
import asyncio
import requests
import struct
import math
import cv2
import pvporcupine
import pyaudio
from dotenv import load_dotenv, set_key
from fastapi import FastAPI, BackgroundTasks
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from pydantic import BaseModel
import uvicorn
from ctypes import CFUNCTYPE, c_char_p, c_int, cdll
# Load environment variables
load_dotenv()
ENV_PATH = ".env"
app = FastAPI(title="Home-Bro Satellite")
# Logic to suppress ALSA/JACK error spam on Linux
def suppress_alsa_errors():
def py_error_handler(filename, line, function, err, fmt):
pass
ERROR_HANDLER_FUNC = CFUNCTYPE(None, c_char_p, c_int, c_char_p, c_int, c_char_p)
c_error_handler = ERROR_HANDLER_FUNC(py_error_handler)
try:
asound = cdll.LoadLibrary('libasound.so.2')
asound.snd_lib_error_set_handler(c_error_handler)
except Exception:
pass
# Ensure static directory exists or serve it
suppress_alsa_errors()
app = FastAPI(title="Home-Bro Satellite")
app.mount("/static", StaticFiles(directory="static"), name="static")
class Config(BaseModel):
@@ -24,6 +42,7 @@ class Config(BaseModel):
brain_url: str
wake_word: str
custom_wake_word_path: str = ""
audio_index: str = ""
@app.get("/")
async def read_index():
@@ -31,7 +50,7 @@ async def read_index():
@app.get("/favicon.ico", include_in_schema=False)
async def favicon():
return FileResponse("static/index.html") # Or just return 204
return FileResponse("static/index.html")
@app.get("/config")
async def get_config():
@@ -40,45 +59,24 @@ async def get_config():
"pico_key": os.getenv("PICOVOICE_API_KEY", ""),
"brain_url": os.getenv("BRAIN_URL", "http://localhost:8000"),
"wake_word": os.getenv("WAKE_WORD", "porcupine"),
"custom_wake_word_path": os.getenv("CUSTOM_WAKE_WORD_PATH", "")
"custom_wake_word_path": os.getenv("CUSTOM_WAKE_WORD_PATH", ""),
"audio_index": os.getenv("AUDIO_DEVICE_INDEX", "")
}
@app.post("/config")
async def save_config(config: Config):
# Update environment variables in memory and file
settings = {
"BRO_NAME": config.bro_name,
"PICOVOICE_API_KEY": config.pico_key,
"BRAIN_URL": config.brain_url,
"WAKE_WORD": config.wake_word,
"CUSTOM_WAKE_WORD_PATH": config.custom_wake_word_path
}
for key, value in settings.items():
os.environ[key] = value
set_key(ENV_PATH, key, value)
return {"status": "ok"}
@app.get("/check-brain")
async def check_brain(url: str):
try:
# Simple health check to the brain
# We use a short timeout (2s) to keep UI responsive
response = requests.get(f"{url}/docs", timeout=2)
response = requests.get(f"{url}/api/info", timeout=2)
return {"online": response.status_code == 200}
except Exception:
return {"online": False}
import cv2
def take_snapshot():
cam_index = int(os.getenv("CAM_INDEX", 0))
cap = cv2.VideoCapture(cam_index)
if not cap.isOpened():
print("Error: Could not open camera.")
return None
ret, frame = cap.read()
if ret:
path = "snapshot.jpg"
@@ -90,46 +88,42 @@ def take_snapshot():
def send_to_brain(image_path):
brain_url = os.getenv("BRAIN_URL")
if not brain_url:
return None
if not brain_url: return None
try:
with open(image_path, "rb") as f:
files = {"file": f}
response = requests.post(f"{brain_url}/analyze", files=files)
response = requests.post(f"{brain_url}/analyze/pi", files=files)
if response.status_code == 200:
return response.json()
except Exception as e:
print(f"Upload failed: {e}")
return None
import pvporcupine
import pyaudio
import struct
def list_audio_devices():
pa = pyaudio.PyAudio()
print("\n--- Verfügbare Audiogeräte ---")
for i in range(pa.get_device_count()):
dev = pa.get_device_info_by_index(i)
print(f"Index {i}: {dev['name']} (Input Channels: {dev['maxInputChannels']})")
print("------------------------------\n")
pa.terminate()
def get_wake_word_params():
"""Helper to get Porcupine parameters based on settings."""
keyword = os.getenv("WAKE_WORD", "porcupine")
custom_path = os.getenv("CUSTOM_WAKE_WORD_PATH", "")
if keyword == "custom" and custom_path:
return {"library_path": None, "model_path": None, "keyword_paths": [custom_path]}
# Standard builtin keywords
return {"keyword_paths": [custom_path]}
if keyword in pvporcupine.KEYWORDS:
return {"keywords": [keyword]}
return {"keywords": ["porcupine"]}
# Global variable to hold the task
interaction_task = None
async def brain_interaction_loop():
"""Background loop for wake-word detection and brain interaction."""
# RE-LOAD settings inside the loop to ensure we use latest
list_audio_devices()
access_key = os.getenv("PICOVOICE_API_KEY")
if not access_key:
print("CRITICAL: PICOVOICE_API_KEY not found in .env. Wake-word detection disabled.")
print("CRITICAL: PICOVOICE_API_KEY Missing.")
return
pa = None
@@ -139,28 +133,35 @@ async def brain_interaction_loop():
try:
params = get_wake_word_params()
porcupine = pvporcupine.create(access_key=access_key, **params)
pa = pyaudio.PyAudio()
device_index = os.getenv("AUDIO_DEVICE_INDEX")
input_device_index = int(device_index) if device_index and device_index.strip() else None
audio_stream = pa.open(
rate=porcupine.sample_rate,
channels=1,
format=pyaudio.paInt16,
input=True,
frames_per_buffer=porcupine.frame_length
frames_per_buffer=porcupine.frame_length,
input_device_index=input_device_index
)
print(f"Listening for wake-word '{os.getenv('WAKE_WORD', 'porcupine')}'...")
print(f"Listening for '{os.getenv('WAKE_WORD', 'porcupine')}' on device {input_device_index}...")
frame_count = 0
while True:
# Check for cancellation
await asyncio.sleep(0.01)
pcm = audio_stream.read(porcupine.frame_length, exception_on_overflow=False)
pcm = struct.unpack_from("h" * porcupine.frame_length, pcm)
pcm_unpacked = struct.unpack_from("h" * porcupine.frame_length, pcm)
keyword_index = porcupine.process(pcm)
frame_count += 1
if frame_count % 100 == 0:
rms = math.sqrt(sum(x*x for x in pcm_unpacked) / len(pcm_unpacked))
if rms < 10:
print(f"DEBUG: Pegel sehr niedrig ({rms:.2f}). Check Mic!")
if keyword_index >= 0:
if porcupine.process(pcm_unpacked) >= 0:
print(f"[{os.getenv('BRO_NAME', 'Home-Bro')}] Wake-word detected!")
img_path = take_snapshot()
if img_path:
@@ -169,16 +170,13 @@ async def brain_interaction_loop():
print(f"Brain says: {result.get('comment')}")
except asyncio.CancelledError:
print("Wake-word loop cancelled for restart...")
print("Restarting Loop...")
except Exception as e:
print(f"Error in wake-word loop: {e}")
print(f"Error: {e}")
finally:
if audio_stream is not None:
audio_stream.close()
if pa is not None:
pa.terminate()
if porcupine is not None:
porcupine.delete()
if audio_stream: audio_stream.close()
if pa: pa.terminate()
if porcupine: porcupine.delete()
def start_interaction_loop():
global interaction_task
@@ -188,30 +186,24 @@ async def restart_interaction_loop():
global interaction_task
if interaction_task:
interaction_task.cancel()
try:
await interaction_task
except asyncio.CancelledError:
pass
try: await interaction_task
except asyncio.CancelledError: pass
start_interaction_loop()
@app.post("/config")
async def save_config(config: Config, background_tasks: BackgroundTasks):
# Update environment variables in memory and file
settings = {
"BRO_NAME": config.bro_name,
"PICOVOICE_API_KEY": config.pico_key,
"BRAIN_URL": config.brain_url,
"WAKE_WORD": config.wake_word,
"CUSTOM_WAKE_WORD_PATH": config.custom_wake_word_path
"CUSTOM_WAKE_WORD_PATH": config.custom_wake_word_path,
"AUDIO_DEVICE_INDEX": config.audio_index
}
for key, value in settings.items():
os.environ[key] = value
set_key(ENV_PATH, key, value)
# Trigger restart in the background
background_tasks.add_task(restart_interaction_loop)
return {"status": "ok"}
@app.on_event("startup")
+6
View File
@@ -61,6 +61,12 @@
<input type="text" id="custom-wake-word" placeholder="/path/to/file.ppn" style="display: none; margin-top: 10px;" />
</div>
<div class="input-group">
<label for="audio-index">Audio Device Index</label>
<input type="number" id="audio-index" placeholder="z.B. 0" />
<span class="input-hint">Index des Mikrofons (siehe Konsolen-Log).</span>
</div>
<button id="save-btn" class="primary-btn">
<span class="btn-text">Einstellungen speichern</span>
<span class="loader"></span>
+4 -1
View File
@@ -3,6 +3,7 @@ const picoKeyInput = document.getElementById('pico-key');
const brainUrlInput = document.getElementById('brain-url');
const wakeWordSelect = document.getElementById('wake-word');
const customWakeWordInput = document.getElementById('custom-wake-word');
const audioIndexInput = document.getElementById('audio-index');
const saveBtn = document.getElementById('save-btn');
const statusBadge = document.getElementById('status-badge');
const toast = document.getElementById('toast');
@@ -22,6 +23,7 @@ async function fetchConfig() {
brainUrlInput.value = data.brain_url || '';
wakeWordSelect.value = data.wake_word || 'porcupine';
customWakeWordInput.value = data.custom_wake_word_path || '';
audioIndexInput.value = data.audio_index || '';
if (wakeWordSelect.value === 'custom') {
customWakeWordInput.style.display = 'block';
@@ -66,7 +68,8 @@ saveBtn.addEventListener('click', async () => {
pico_key: picoKeyInput.value,
brain_url: brainUrlInput.value,
wake_word: wakeWordSelect.value,
custom_wake_word_path: customWakeWordInput.value
custom_wake_word_path: customWakeWordInput.value,
audio_index: audioIndexInput.value
};
try {