root 5321c8602e feat: voice-agent MVP — LXC web frontend + Mac AI worker
LXC-Frontend (FastAPI + HTML/JS):
- Audio-Upload (MP3/WAV/M4A/MP4/OGG/FLAC, max. 500 MB)
- SQLite Job-Store, BackgroundTask-Pipeline
- Job-Liste mit Live-Status, Downloads (DOCX + JSON)
- Mac-Health-Indicator im UI

Mac-Worker (FastAPI):
- /api/transcribe (lightning-whisper-mlx | faster-whisper | mock)
- /api/summarize + /api/protocol via Ollama (llama3.1:8b)
- /api/export/docx via python-docx

Deploy:
- systemd-Service, Nginx Reverse-Proxy
- deploy/install.sh: idempotentes LXC-Setup

Doku: README.md, lxc-frontend/README.md, mac-worker/README.md
2026-05-13 15:33:53 +00:00

77 lines
3.0 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
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", job_id)
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)
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)
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)
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}")