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.
102 lines
3.0 KiB
Python
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"'},
|
|
)
|