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
87 lines
2.6 KiB
Python
87 lines
2.6 KiB
Python
import logging
|
|
import secrets
|
|
from pathlib import Path
|
|
|
|
from fastapi import FastAPI, File, Form, HTTPException, UploadFile
|
|
from fastapi.responses import Response
|
|
from pydantic import BaseModel
|
|
|
|
from app.core import ollama_client
|
|
from app.core.config import settings
|
|
from app.core.docx_export import build_docx
|
|
from app.core.whisper_engine import engine as whisper
|
|
|
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s")
|
|
log = logging.getLogger("mac-worker")
|
|
|
|
app = FastAPI(title="Voice-Agent Mac-Worker")
|
|
|
|
|
|
class SummarizeIn(BaseModel):
|
|
transcript: str
|
|
title: str = ""
|
|
|
|
|
|
class ProtocolIn(BaseModel):
|
|
transcript: str
|
|
summary: dict
|
|
title: str = ""
|
|
|
|
|
|
@app.get("/health")
|
|
async def health():
|
|
info = {
|
|
"status": "ok",
|
|
"whisper_engine": settings.whisper_engine,
|
|
"whisper_model": settings.whisper_model,
|
|
"ollama_model": settings.ollama_model,
|
|
"ollama_reachable": False,
|
|
}
|
|
try:
|
|
await ollama_client.health()
|
|
info["ollama_reachable"] = True
|
|
except Exception as e: # noqa: BLE001
|
|
info["ollama_error"] = str(e)
|
|
return info
|
|
|
|
|
|
@app.post("/api/transcribe")
|
|
async def transcribe(audio: UploadFile = File(...), language: str = Form("de")):
|
|
if not audio.filename:
|
|
raise HTTPException(400, "Dateiname fehlt")
|
|
suffix = Path(audio.filename).suffix or ".bin"
|
|
tmp = settings.work_dir / (secrets.token_hex(8) + suffix)
|
|
try:
|
|
with tmp.open("wb") as f:
|
|
while chunk := await audio.read(1024 * 1024):
|
|
f.write(chunk)
|
|
log.info("Transcribing %s (%.1f MB) lang=%s", audio.filename, tmp.stat().st_size / 1024 / 1024, language)
|
|
result = whisper.transcribe(tmp, language=language)
|
|
return result
|
|
finally:
|
|
tmp.unlink(missing_ok=True)
|
|
|
|
|
|
@app.post("/api/summarize")
|
|
async def summarize(payload: SummarizeIn):
|
|
if not payload.transcript.strip():
|
|
raise HTTPException(400, "Leerer Transkript")
|
|
return await ollama_client.summarize(payload.transcript, title=payload.title)
|
|
|
|
|
|
@app.post("/api/protocol")
|
|
async def protocol(payload: ProtocolIn):
|
|
if not payload.transcript.strip():
|
|
raise HTTPException(400, "Leerer Transkript")
|
|
return await ollama_client.make_protocol(payload.transcript, payload.summary, title=payload.title)
|
|
|
|
|
|
@app.post("/api/export/docx")
|
|
async def export_docx(protocol: dict):
|
|
data = build_docx(protocol)
|
|
return Response(
|
|
content=data,
|
|
media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
headers={"Content-Disposition": 'attachment; filename="protocol.docx"'},
|
|
)
|