Files
mission-control-v2/backend/app.py
T
Hitonabi 8e7ce1b1d3 Feat: Sprechen-Tab — mit Hermes per Sprache reden (Browser-Voice + 3D-Avatar)
Voll-Duplex Sprach-Interaktion vom lokalen PC mit dem vollen Hermes-Agenten
(api_server :8642, OpenAI-kompatibel → gleiche Tools + geteiltes Mem0 wie CLI/Telegram).

- Voice-Sidecar (voice_service/, eigenes Py3.12-venv ~/.voice, :8650): STT faster-whisper
  (medium, de) + gestuftes TTS — Piper (schnell, Default) + Chatterbox (premium, Voice-Cloning,
  lazy-load, CPU-Start). Analog mem0_service.
- Backend: routers/voice.py (Proxy /api/voice/stt|tts|voices + /chat-SSE an Hermes mit
  Bearer API_SERVER_KEY + X-Hermes-Session-Id für server-seitigen Verlauf). config.py:
  VOICE_SERVICE_URL + HERMES_API_KEY (Fallback aus ~/.hermes/.env). System-Dienstliste +
  Wartung (Restart/Logs) um voice-service ergänzt.
- Frontend: Sprechen-Tab mit 3D-Avatar (VRM via three-vrm) — Lippensync (Web-Audio-Pegel),
  Blinzeln, Sentiment-Mimik, Ruhepose. Avatar-Picker (CORS-freie Galerie + .vrm-Upload + URL
  + VRoid-Hub-Link) + Stimm-Auswahl. Push-to-talk (Knopf/Leertaste). Deps: three, r3f, drei.
- Deploy: deploy/voice-service.service + deploy.sh (idempotenter Sidecar-Install, enable, restart).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 23:26:58 +02:00

92 lines
3.1 KiB
Python

"""
Mission Control 2.0 — dünner FastAPI-Einstieg.
Hängt die Router ein, liefert (in Prod) das gebaute React-Frontend aus und
setzt eine no-cache-Middleware. Im Dev läuft das Frontend über den Vite-Dev-
Server (proxyt /api hierher), daher CORS für localhost offen.
"""
import asyncio
import logging
import os
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from starlette.requests import Request
from config import FRONTEND_DIST, VERSION
from routers import agent, connect, gateway_proxy, health, maintenance, memory, models, routing, system, voice
from services import warmer
# Zentrales Logging — Level via MC_LOG_LEVEL (INFO default). Eine Konfiguration
# für alle Module (logging.getLogger(__name__)).
logging.basicConfig(
level=os.environ.get("MC_LOG_LEVEL", "INFO").upper(),
format="%(asctime)s %(levelname)-7s %(name)s: %(message)s",
)
log = logging.getLogger(__name__)
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Hintergrund-Tasks an den App-Lebenszyklus binden: Re-Warm-Wächter fürs Agent-Hirn."""
task = asyncio.create_task(warmer.rewarm_loop()) if warmer.ENABLED else None
if task:
log.info("Hirn-Re-Warm-Wächter aktiv (Intervall %ss, Hirn dynamisch aus Hermes-Config)", warmer.INTERVAL)
try:
yield
finally:
if task:
task.cancel()
app = FastAPI(title="Mission Control 2.0", version=VERSION, lifespan=lifespan)
# Dev: Vite-Dev-Server (5173) ruft das Backend per /api auf.
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173", "http://127.0.0.1:5173"],
allow_methods=["*"],
allow_headers=["*"],
)
@app.middleware("http")
async def no_cache(request: Request, call_next):
resp = await call_next(request)
if request.url.path.startswith("/api"):
resp.headers["Cache-Control"] = "no-cache"
return resp
app.include_router(health.router)
app.include_router(models.router)
app.include_router(routing.router)
app.include_router(system.router)
app.include_router(connect.router)
app.include_router(memory.router)
app.include_router(agent.router)
app.include_router(voice.router) # Sprache: STT/TTS-Proxy + Hermes-Agent-Chat (Voice-Tab)
app.include_router(gateway_proxy.router) # OpenAI-kompatibler /v1-Gateway (model:auto)
app.include_router(maintenance.router)
# Prod: gebautes Frontend ausliefern (falls vorhanden). SPA-Fallback auf index.html.
if FRONTEND_DIST.exists():
app.mount("/assets", StaticFiles(directory=FRONTEND_DIST / "assets"), name="assets")
@app.get("/{full_path:path}")
def spa(full_path: str):
# Falls die Datei direkt in FRONTEND_DIST liegt (z.B. manifest.webmanifest, favicon.ico), liefere sie aus
target = FRONTEND_DIST / full_path
if target.is_file():
return FileResponse(target)
index = FRONTEND_DIST / "index.html"
if index.exists():
return FileResponse(index)
return {"detail": "frontend not built"}