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), }, }