138 lines
4.2 KiB
Python
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"} |