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.
This commit is contained in:
root
2026-05-14 03:11:30 +00:00
parent 98cf9eb47e
commit 3a9881d36f
18 changed files with 479 additions and 145 deletions
+20 -1
View File
@@ -33,7 +33,16 @@ class SetupIn(BaseModel):
def _user_dict(u: User) -> dict:
return {"id": u.id, "username": u.username, "is_admin": u.is_admin}
return {
"id": u.id,
"username": u.username,
"is_admin": u.is_admin,
"default_profile": u.default_profile or "meeting",
}
class UpdateMeIn(BaseModel):
default_profile: str | None = Field(default=None, max_length=64)
@router.get("/me")
@@ -41,6 +50,16 @@ def me(user: User = Depends(current_user)):
return _user_dict(user)
@router.patch("/me")
def update_me(payload: UpdateMeIn, user: User = Depends(current_user), session: Session = Depends(get_session)):
if payload.default_profile is not None:
user.default_profile = payload.default_profile.strip() or "meeting"
session.add(user)
session.commit()
session.refresh(user)
return _user_dict(user)
@router.get("/needs-setup")
def needs_setup(session: Session = Depends(get_session)):
"""Vor Login prüfen ob noch kein User existiert (erstes Deployment)."""
+3
View File
@@ -30,6 +30,7 @@ async def create_job(
background: BackgroundTasks,
audio: UploadFile = File(...),
title: str = Form(""),
profile: str = Form(""),
session: Session = Depends(get_session),
user: User = Depends(current_user),
):
@@ -54,6 +55,7 @@ async def create_job(
job = Job(
owner_id=user.id,
profile=(profile.strip() or user.default_profile or "meeting"),
filename=safe,
original_name=audio.filename,
title=title.strip(),
@@ -114,6 +116,7 @@ def _job_dict(j: Job, user: User, owner_username: str | None = None) -> dict:
"status": j.status,
"progress": j.progress,
"error": j.error,
"profile": j.profile or "meeting",
"created_at": j.created_at.isoformat(),
"updated_at": j.updated_at.isoformat(),
"has": {
+38
View File
@@ -0,0 +1,38 @@
"""Liefert die im Mac-Worker verfügbaren Verarbeitungs-Profile aus.
Cached die Liste 60 s, damit der Mac nicht bei jedem Reload getroffen wird.
"""
from __future__ import annotations
import logging
import time
from fastapi import APIRouter, Depends
from app.core.auth import current_user
from app.core.mac_client import MacClient
from app.models.user import User
router = APIRouter(prefix="/api", tags=["profiles"])
log = logging.getLogger("voice-agent.profiles")
_CACHE: dict = {"profiles": None, "ts": 0.0}
_TTL = 60.0
_FALLBACK = [{"name": "meeting", "display_name": "Sitzungsprotokoll", "language": "de"}]
@router.get("/profiles")
async def list_profiles(_: User = Depends(current_user)):
now = time.time()
if _CACHE["profiles"] is not None and now - _CACHE["ts"] < _TTL:
return {"profiles": _CACHE["profiles"]}
try:
profiles = await MacClient().list_profiles()
if not profiles:
profiles = _FALLBACK
except Exception as e: # noqa: BLE001
log.warning("Konnte Profile vom Mac nicht laden: %s — Fallback wird genutzt", e)
profiles = _FALLBACK
_CACHE["profiles"] = profiles
_CACHE["ts"] = now
return {"profiles": profiles}