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.pdf_export import build_pdf 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 class PdfIn(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"'}, ) @app.post("/api/export/pdf") async def export_pdf(payload: PdfIn): data = build_pdf(payload.data, profile_name=payload.profile) return Response( content=data, media_type="application/pdf", headers={"Content-Disposition": 'attachment; filename="protocol.pdf"'}, )