fixing input device
This commit is contained in:
+62
-70
@@ -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")
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user