Files
Sebastian Krüger 800edc08ea Initial commit: FaceFusion REST API
FastAPI wrapper around FaceFusion v3.5.3 submodule with:
- Sync and async (job-based) processing endpoints
- FaceFusion bridge with manual key registration and Lock-serialized processing
- Multi-target Dockerfile (CPU + CUDA GPU)
- Docker Compose configs for dev, prod-cpu, and prod-gpu
- Gitea CI/CD workflow with dual image builds
- All 11 FaceFusion processors supported via options API

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 12:58:33 +01:00

106 lines
3.3 KiB
Python

import json
import logging
import uuid
from typing import List, Optional
from fastapi import APIRouter, File, Form, HTTPException, UploadFile
from fastapi.responses import FileResponse
from app.schemas.jobs import JobCreateResponse, JobDeleteResponse, JobStatus, JobStatusResponse
from app.schemas.process import ProcessingOptions
from app.services import facefusion_bridge, file_manager
from app.services.worker import worker_queue
logger = logging.getLogger(__name__)
router = APIRouter(prefix='/api/v1/jobs', tags=['jobs'])
@router.post('', response_model=JobCreateResponse)
async def create_job(
target: UploadFile = File(...),
source: Optional[List[UploadFile]] = File(None),
options: Optional[str] = Form(None),
):
"""Create an async processing job."""
job_id = str(uuid.uuid4())
request_dir = file_manager.create_request_dir()
try:
parsed_options = None
if options:
try:
parsed_options = json.loads(options)
ProcessingOptions(**parsed_options)
except (json.JSONDecodeError, Exception) as e:
raise HTTPException(status_code=422, detail=f'Invalid options: {e}')
target_path = await file_manager.save_upload(target, request_dir)
source_paths = []
if source:
source_paths = await file_manager.save_uploads(source, request_dir)
output_path = file_manager.generate_output_path(target_path)
args = facefusion_bridge.build_args_from_options(
source_paths=source_paths,
target_path=target_path,
output_path=output_path,
options=parsed_options,
)
worker_queue.submit(job_id, args)
return JobCreateResponse(job_id=job_id, status=JobStatus.pending)
except HTTPException:
raise
except Exception as e:
file_manager.cleanup_directory(request_dir)
logger.error(f'Job creation failed: {e}')
raise HTTPException(status_code=500, detail=str(e))
@router.get('/{job_id}', response_model=JobStatusResponse)
async def get_job_status(job_id: str):
"""Get job status."""
job = worker_queue.get_job(job_id)
if not job:
raise HTTPException(status_code=404, detail='Job not found')
return JobStatusResponse(
job_id=job.job_id,
status=JobStatus(job.status.value),
created_at=job.created_at,
updated_at=job.updated_at,
error=job.error,
)
@router.get('/{job_id}/result')
async def get_job_result(job_id: str):
"""Download job result. Only available when job is completed."""
job = worker_queue.get_job(job_id)
if not job:
raise HTTPException(status_code=404, detail='Job not found')
if job.status != 'completed':
raise HTTPException(status_code=409, detail=f'Job status is {job.status}, not completed')
if not job.output_path:
raise HTTPException(status_code=500, detail='No output file')
return FileResponse(
path=job.output_path,
media_type='application/octet-stream',
)
@router.delete('/{job_id}', response_model=JobDeleteResponse)
async def delete_job(job_id: str):
"""Cancel/delete a job."""
deleted = worker_queue.delete_job(job_id)
if not deleted:
raise HTTPException(status_code=404, detail='Job not found')
return JobDeleteResponse(job_id=job_id, deleted=True)