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.
78 lines
3.1 KiB
Python
78 lines
3.1 KiB
Python
import json
|
|
import logging
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
from sqlmodel import Session
|
|
|
|
from app.core.config import settings
|
|
from app.core.db import engine
|
|
from app.core.mac_client import MacClient
|
|
from app.models.job import Job, JobStatus
|
|
|
|
log = logging.getLogger("voice-agent.pipeline")
|
|
|
|
|
|
def _update(job_id: int, **fields) -> None:
|
|
with Session(engine) as s:
|
|
job = s.get(Job, job_id)
|
|
if not job:
|
|
return
|
|
for k, v in fields.items():
|
|
setattr(job, k, v)
|
|
job.updated_at = datetime.now(timezone.utc)
|
|
s.add(job)
|
|
s.commit()
|
|
|
|
|
|
async def run_pipeline(job_id: int) -> None:
|
|
"""Run the full pipeline: transcribe → summarize → protocol → DOCX."""
|
|
client = MacClient()
|
|
|
|
with Session(engine) as s:
|
|
job = s.get(Job, job_id)
|
|
if not job:
|
|
log.error("Job %s not found", job_id)
|
|
return
|
|
audio_path = settings.upload_dir / job.filename
|
|
title = job.title or audio_path.stem
|
|
profile = job.profile or "meeting"
|
|
|
|
job_dir = settings.result_dir / str(job_id)
|
|
job_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
try:
|
|
_update(job_id, status=JobStatus.TRANSCRIBING, progress=10)
|
|
log.info("[job %s] transcribe (profile=%s)", job_id, profile)
|
|
tx = await client.transcribe(audio_path, language="de")
|
|
transcript_path = job_dir / "transcript.json"
|
|
transcript_path.write_text(json.dumps(tx, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
_update(job_id, transcript_path=str(transcript_path), progress=45)
|
|
|
|
transcript_text = tx.get("text") or "\n".join(s.get("text", "") for s in tx.get("segments", []))
|
|
|
|
_update(job_id, status=JobStatus.SUMMARIZING, progress=55)
|
|
log.info("[job %s] summarize", job_id)
|
|
summary = await client.summarize(transcript_text, title=title, profile=profile)
|
|
summary_path = job_dir / "summary.json"
|
|
summary_path.write_text(json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
_update(job_id, summary_path=str(summary_path), progress=70)
|
|
|
|
_update(job_id, status=JobStatus.PROTOCOLLING, progress=75)
|
|
log.info("[job %s] protocol", job_id)
|
|
protocol = await client.protocol(transcript_text, summary, title=title, profile=profile)
|
|
protocol_path = job_dir / "protocol.json"
|
|
protocol_path.write_text(json.dumps(protocol, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
_update(job_id, protocol_path=str(protocol_path), progress=85)
|
|
|
|
_update(job_id, status=JobStatus.EXPORTING, progress=90)
|
|
log.info("[job %s] export docx", job_id)
|
|
docx_bytes = await client.export_docx(protocol, profile=profile)
|
|
docx_path = job_dir / "protocol.docx"
|
|
docx_path.write_bytes(docx_bytes)
|
|
_update(job_id, docx_path=str(docx_path), status=JobStatus.DONE, progress=100)
|
|
log.info("[job %s] done", job_id)
|
|
except Exception as e: # noqa: BLE001
|
|
log.exception("[job %s] failed", job_id)
|
|
_update(job_id, status=JobStatus.FAILED, error=f"{type(e).__name__}: {e}")
|