feat: voice-agent MVP — LXC web frontend + Mac AI worker

LXC-Frontend (FastAPI + HTML/JS):
- Audio-Upload (MP3/WAV/M4A/MP4/OGG/FLAC, max. 500 MB)
- SQLite Job-Store, BackgroundTask-Pipeline
- Job-Liste mit Live-Status, Downloads (DOCX + JSON)
- Mac-Health-Indicator im UI

Mac-Worker (FastAPI):
- /api/transcribe (lightning-whisper-mlx | faster-whisper | mock)
- /api/summarize + /api/protocol via Ollama (llama3.1:8b)
- /api/export/docx via python-docx

Deploy:
- systemd-Service, Nginx Reverse-Proxy
- deploy/install.sh: idempotentes LXC-Setup

Doku: README.md, lxc-frontend/README.md, mac-worker/README.md
This commit is contained in:
root
2026-05-13 15:33:53 +00:00
parent 32ab12014f
commit 5321c8602e
35 changed files with 1820 additions and 4 deletions
View File
+29
View File
@@ -0,0 +1,29 @@
from pathlib import Path
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore")
app_name: str = "Voice-Agent"
app_host: str = "0.0.0.0"
app_port: int = 8000
upload_dir: Path = Path("/var/lib/voice-agent/uploads")
result_dir: Path = Path("/var/lib/voice-agent/results")
db_url: str = "sqlite:////var/lib/voice-agent/voice-agent.db"
mac_api_url: str = "http://192.168.85.10:8080"
mac_api_timeout: int = 1800
max_upload_mb: int = 500
allowed_exts: str = "mp3,wav,m4a,mp4,ogg,flac"
@property
def allowed_ext_set(self) -> set[str]:
return {e.strip().lower().lstrip(".") for e in self.allowed_exts.split(",") if e.strip()}
settings = Settings()
settings.upload_dir.mkdir(parents=True, exist_ok=True)
settings.result_dir.mkdir(parents=True, exist_ok=True)
+14
View File
@@ -0,0 +1,14 @@
from sqlmodel import SQLModel, create_engine, Session
from .config import settings
engine = create_engine(settings.db_url, connect_args={"check_same_thread": False})
def init_db() -> None:
from app.models.job import Job # noqa: F401
SQLModel.metadata.create_all(engine)
def get_session():
with Session(engine) as session:
yield session
+48
View File
@@ -0,0 +1,48 @@
from pathlib import Path
import httpx
from .config import settings
class MacClient:
def __init__(self, base_url: str | None = None, timeout: int | None = None):
self.base_url = (base_url or settings.mac_api_url).rstrip("/")
self.timeout = timeout or settings.mac_api_timeout
async def health(self) -> dict:
async with httpx.AsyncClient(timeout=10) as c:
r = await c.get(f"{self.base_url}/health")
r.raise_for_status()
return r.json()
async def transcribe(self, audio_path: Path, language: str = "de") -> dict:
async with httpx.AsyncClient(timeout=self.timeout) as c:
with audio_path.open("rb") as f:
files = {"audio": (audio_path.name, f, "application/octet-stream")}
data = {"language": language}
r = await c.post(f"{self.base_url}/api/transcribe", files=files, data=data)
r.raise_for_status()
return r.json()
async def summarize(self, transcript: str, title: str = "") -> dict:
async with httpx.AsyncClient(timeout=self.timeout) as c:
r = await c.post(
f"{self.base_url}/api/summarize",
json={"transcript": transcript, "title": title},
)
r.raise_for_status()
return r.json()
async def protocol(self, transcript: str, summary: dict, title: str = "") -> dict:
async with httpx.AsyncClient(timeout=self.timeout) as c:
r = await c.post(
f"{self.base_url}/api/protocol",
json={"transcript": transcript, "summary": summary, "title": title},
)
r.raise_for_status()
return r.json()
async def export_docx(self, protocol: dict) -> bytes:
async with httpx.AsyncClient(timeout=self.timeout) as c:
r = await c.post(f"{self.base_url}/api/export/docx", json=protocol)
r.raise_for_status()
return r.content