Files
home-bro-brain/app/main.py
T

138 lines
4.2 KiB
Python

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"}