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
106 lines
3.6 KiB
Python
106 lines
3.6 KiB
Python
import secrets
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
import aiofiles
|
|
from fastapi import APIRouter, BackgroundTasks, Depends, File, Form, HTTPException, UploadFile
|
|
from fastapi.responses import FileResponse
|
|
from sqlmodel import Session, select
|
|
|
|
from app.core.config import settings
|
|
from app.core.db import get_session
|
|
from app.models.job import Job, JobStatus
|
|
from app.services.pipeline import run_pipeline
|
|
|
|
router = APIRouter(prefix="/api/jobs", tags=["jobs"])
|
|
|
|
MAX_BYTES = lambda: settings.max_upload_mb * 1024 * 1024
|
|
|
|
|
|
@router.post("")
|
|
async def create_job(
|
|
background: BackgroundTasks,
|
|
audio: UploadFile = File(...),
|
|
title: str = Form(""),
|
|
session: Session = Depends(get_session),
|
|
):
|
|
if not audio.filename:
|
|
raise HTTPException(400, "Dateiname fehlt")
|
|
ext = Path(audio.filename).suffix.lower().lstrip(".")
|
|
if ext not in settings.allowed_ext_set:
|
|
raise HTTPException(400, f"Format '{ext}' nicht erlaubt. Erlaubt: {sorted(settings.allowed_ext_set)}")
|
|
|
|
safe = secrets.token_hex(8) + "." + ext
|
|
dest = settings.upload_dir / safe
|
|
size = 0
|
|
limit = MAX_BYTES()
|
|
async with aiofiles.open(dest, "wb") as f:
|
|
while chunk := await audio.read(1024 * 1024):
|
|
size += len(chunk)
|
|
if size > limit:
|
|
await f.close()
|
|
dest.unlink(missing_ok=True)
|
|
raise HTTPException(413, f"Datei zu groß (max. {settings.max_upload_mb} MB)")
|
|
await f.write(chunk)
|
|
|
|
job = Job(filename=safe, original_name=audio.filename, title=title.strip())
|
|
session.add(job)
|
|
session.commit()
|
|
session.refresh(job)
|
|
|
|
background.add_task(run_pipeline, job.id)
|
|
return _job_dict(job)
|
|
|
|
|
|
@router.get("")
|
|
def list_jobs(session: Session = Depends(get_session)):
|
|
jobs = session.exec(select(Job).order_by(Job.created_at.desc())).all()
|
|
return [_job_dict(j) for j in jobs]
|
|
|
|
|
|
@router.get("/{job_id}")
|
|
def get_job(job_id: int, session: Session = Depends(get_session)):
|
|
job = session.get(Job, job_id)
|
|
if not job:
|
|
raise HTTPException(404, "Job nicht gefunden")
|
|
return _job_dict(job)
|
|
|
|
|
|
@router.get("/{job_id}/download/{kind}")
|
|
def download(job_id: int, kind: str, session: Session = Depends(get_session)):
|
|
job = session.get(Job, job_id)
|
|
if not job:
|
|
raise HTTPException(404, "Job nicht gefunden")
|
|
mapping = {
|
|
"docx": (job.docx_path, "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "protocol.docx"),
|
|
"transcript": (job.transcript_path, "application/json", "transcript.json"),
|
|
"summary": (job.summary_path, "application/json", "summary.json"),
|
|
"protocol": (job.protocol_path, "application/json", "protocol.json"),
|
|
}
|
|
if kind not in mapping:
|
|
raise HTTPException(400, "Unbekannter Download-Typ")
|
|
path, media, name = mapping[kind]
|
|
if not path or not Path(path).exists():
|
|
raise HTTPException(404, "Datei noch nicht verfügbar")
|
|
base = (job.title or Path(job.original_name).stem).replace("/", "_")
|
|
return FileResponse(path, media_type=media, filename=f"{base}-{name}")
|
|
|
|
|
|
def _job_dict(j: Job) -> dict:
|
|
return {
|
|
"id": j.id,
|
|
"title": j.title,
|
|
"original_name": j.original_name,
|
|
"status": j.status,
|
|
"progress": j.progress,
|
|
"error": j.error,
|
|
"created_at": j.created_at.isoformat(),
|
|
"updated_at": j.updated_at.isoformat(),
|
|
"has": {
|
|
"transcript": bool(j.transcript_path),
|
|
"summary": bool(j.summary_path),
|
|
"protocol": bool(j.protocol_path),
|
|
"docx": bool(j.docx_path),
|
|
},
|
|
}
|