from fastapi import FastAPI, UploadFile, File, Request from fastapi.staticfiles import StaticFiles from fastapi.responses import HTMLResponse, FileResponse import cv2 import os import time import socket import sys from .vision import analyze_image_with_yolo app = FastAPI(title="Home-Bro Brain") import subprocess def play_voice(text: str): """Generates speech using Piper and plays it on Mac.""" def _speak(): try: print(f"DEBUG: Piper TTS für: '{text}'") # Pfad zum Piper Binary im Python 3.12 venv (funktioniert auf M-Mac) piper_path = "./venv_piper/bin/piper" model_path = "./piper/models/de_DE-thorsten-high.onnx" # Piper auf Mac nutzt afplay für die Ausgabe # Wir streamen stdout direkt zu afplay command = ( f"echo '{text}' | " f"{piper_path} --model {model_path} --output-raw | " f"afplay --channels 1 --rate 22050 --format linear_pcm --bits 16" ) # Da afplay kein 'raw' Format direkt von stdin im richtigen Takt nimmt, # ist es sicherer, kurz ein WAV zu schreiben. wav_path = "snapshots/speech.wav" gen_command = ( f"echo '{text}' | " f"{piper_path} --model {model_path} --output_file {wav_path}" ) subprocess.run(gen_command, shell=True, check=True) if sys.platform == "darwin": subprocess.run(["afplay", wav_path], check=True) except Exception as e: print(f"Piper TTS Fehler: {e}") # In einem separaten Thread ausführen import threading threading.Thread(target=_speak).start() # Statische Dateien (Frontend) app.mount("/static", StaticFiles(directory="static"), name="static") app.mount("/snapshots", StaticFiles(directory="snapshots"), name="snapshots") # In-Memory Speicher für den letzten Status latest_status = { "room": "Warten...", "comment": "Noch keine Daten empfangen.", "timestamp": None, "image_url": None } @app.get("/", response_class=HTMLResponse) async def get_index(): return FileResponse("static/index.html") @app.get("/api/latest") async def get_latest(): return latest_status @app.get("/api/info") async def get_info(): hostname = socket.gethostname() try: s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.connect(("8.8.8.8", 80)) local_ip = s.getsockname()[0] s.close() except Exception: local_ip = "127.0.0.1" return { "hostname": hostname, "ip": local_ip, "port": 8000 } @app.post("/analyze/pi") async def analyze_pi(file: UploadFile = File(...)): global latest_status # Bild vom Pi speichern path = "snapshots/pi_last.jpg" content = await file.read() with open(path, "wb") as f: f.write(content) result = analyze_image_with_yolo(path) play_voice(result) # Status aktualisieren latest_status = { "room": "Wohnzimmer (Pi)", "comment": result, "timestamp": time.strftime("%H:%M:%S"), "image_url": f"/snapshots/pi_last.jpg?t={int(time.time())}" # Cache busting } return {"room": "Wohnzimmer", "comment": result} @app.get("/analyze/tapo/{room_name}") async def analyze_tapo(room_name: str, ip: str): global latest_status # RTSP Zugriff auf die Tapo user = os.getenv("TAPO_USER") pw = os.getenv("TAPO_PASSWORD") url = f"rtsp://{user}:{pw}@{ip}:554/stream1" cap = cv2.VideoCapture(url) ret, frame = cap.read() if ret: path = f"snapshots/{room_name}_tapo.jpg" cv2.imwrite(path, frame) result = analyze_image_with_yolo(path) play_voice(result) cap.release() latest_status = { "room": room_name, "comment": result, "timestamp": time.strftime("%H:%M:%S"), "image_url": f"/snapshots/{room_name}_tapo.jpg?t={int(time.time())}" } return {"room": room_name, "comment": result} cap.release() return {"error": "Tapo nicht erreichbar"}