root 3a9881d36f feat: Profile-System für unterschiedliche Use-Cases (Sitzungsprotokoll, später Arzt-Diktat etc.)
Mac-Worker:
- profiles/meeting.yaml — extrahiert die bisherigen Prompts + DOCX-Layout
- core/profiles.py — YAML-Loader mit Cache und Fallback
- ollama_client.summarize/make_protocol nehmen profile_name
- docx_export: generischer Renderer aus profile.docx (meta + sections mit
  Typ text/list/tasks/transcript)
- /api/profiles listet verfügbare Profile
- pyyaml als Dependency

LXC:
- Job.profile + User.default_profile (Migration: ALTER TABLE)
- /api/profiles proxy mit 60s-Cache und Fallback
- Upload-Form akzeptiert profile (Server-Default: user.default_profile)
- Pipeline gibt Profile bei summarize/protocol/docx an Mac weiter
- PATCH /api/auth/me — User kann Standard-Profil ändern

Frontend:
- Profile-Dropdown im Upload (nur sichtbar wenn ≥2 Profile)
- Settings-Karte "Mein Standard-Profil" (nur wenn ≥2 Profile)
- Job-Zeile zeigt grünes Profile-Tag (nur wenn relevant)

Neue Profile = neue YAML-Datei in mac-worker/profiles/ — kein Code-Deploy
nötig, der Mac-Worker liest sie beim Start ein.
2026-05-14 03:11:30 +00:00

102 lines
3.0 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, profiles
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 = ""
profile: str | None = None
class ProtocolIn(BaseModel):
transcript: str
summary: dict
title: str = ""
profile: str | None = None
class DocxIn(BaseModel):
data: dict
profile: str | None = None
@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,
"profiles": [p["name"] for p in profiles.list_all()],
}
try:
await ollama_client.health()
info["ollama_reachable"] = True
except Exception as e: # noqa: BLE001
info["ollama_error"] = str(e)
return info
@app.get("/api/profiles")
def list_profiles():
return {"profiles": profiles.list_all()}
@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, profile_name=payload.profile)
@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, profile_name=payload.profile
)
@app.post("/api/export/docx")
async def export_docx(payload: DocxIn):
data = build_docx(payload.data, profile_name=payload.profile)
return Response(
content=data,
media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
headers={"Content-Disposition": 'attachment; filename="protocol.docx"'},
)