--- name: fastapi-svelte-gpu-webui description: | Pattern for building web UIs for GPU-based CLI tools. Use when: (1) Wrapping a command-line tool with a web interface, (2) Building job queue systems for long-running GPU tasks, (3) Creating file upload/download workflows, (4) Need real-time progress updates via WebSocket, (5) Deploying to Kubernetes with GPU scheduling. Covers FastAPI backend, Svelte 5 frontend, NFS storage, and Terraform deployment. author: Claude Code version: 1.0.0 date: 2025-01-31 --- # FastAPI + Svelte GPU WebUI Pattern ## Problem Many powerful tools are command-line only, making them inaccessible to non-technical users. Building a web UI requires handling file uploads, job queuing, progress tracking, and GPU resource scheduling. ## Context / Trigger Conditions - You have a CLI tool that does heavy processing (ML inference, media conversion, etc.) - Want to add a web interface for easier access - Need to track long-running job progress - Deploying to Kubernetes with GPU nodes - Files need to persist across pod restarts (NFS storage) ## Solution Overview ### Directory Structure ``` project-web/ ├── backend/ │ ├── main.py # FastAPI app │ ├── api/ │ │ ├── __init__.py │ │ └── routes.py # REST endpoints │ ├── services/ │ │ ├── __init__.py │ │ └── converter.py # CLI wrapper + job manager │ ├── models/ │ │ ├── __init__.py │ │ └── schemas.py # Pydantic models │ └── requirements.txt ├── frontend/ │ ├── src/ │ │ ├── App.svelte │ │ ├── lib/ │ │ │ ├── FileUpload.svelte │ │ │ ├── JobsList.svelte │ │ │ └── ProgressBar.svelte │ │ └── stores/ │ │ └── jobs.js │ ├── package.json │ └── vite.config.js ├── Dockerfile └── README.md ``` ### Backend: Job Manager Pattern ```python # services/converter.py import asyncio import uuid from datetime import datetime from pathlib import Path from typing import Optional, Callable import subprocess class Job: id: str filename: str status: str # pending, processing, completed, failed progress: float created_at: datetime output_file: Optional[str] error: Optional[str] class JobManager: def __init__(self, storage_path: str = "/mnt"): self.storage_path = Path(storage_path) self.jobs: dict[str, Job] = {} self.progress_callbacks: dict[str, list[Callable]] = {} def create_job(self, filename: str, **options) -> Job: job_id = str(uuid.uuid4()) job = Job( id=job_id, filename=filename, status="pending", progress=0.0, created_at=datetime.now(), **options ) self.jobs[job_id] = job return job async def run_conversion(self, job_id: str): job = self.jobs[job_id] job.status = "processing" input_path = self.storage_path / "uploads" / job.filename output_dir = self.storage_path / "outputs" / job_id output_dir.mkdir(parents=True, exist_ok=True) # Build command for CLI tool cmd = [ "/path/to/cli-tool", str(input_path), "-o", str(output_dir), # Add other options... ] # Run with output capture for progress parsing process = await asyncio.create_subprocess_exec( *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) # Parse output for progress updates async def read_output(stream): while True: line = await stream.readline() if not line: break line_str = line.decode().strip() # Parse progress from CLI output if "%" in line_str: # Extract and update progress self.update_progress(job_id, parsed_progress) await asyncio.gather( read_output(process.stdout), read_output(process.stderr) ) returncode = await process.wait() if returncode == 0: output_files = list(output_dir.glob("*.m4b")) if output_files: job.output_file = output_files[0].name job.status = "completed" else: job.status = "failed" job.error = f"Exit code {returncode}" job_manager = JobManager() ``` ### Backend: API Routes ```python # api/routes.py from fastapi import APIRouter, UploadFile, File, HTTPException from fastapi.responses import FileResponse from pathlib import Path import shutil import asyncio router = APIRouter(prefix="/api") @router.post("/upload") async def upload_file(file: UploadFile = File(...)): upload_dir = Path("/mnt/uploads") upload_dir.mkdir(parents=True, exist_ok=True) file_path = upload_dir / file.filename with file_path.open("wb") as buffer: shutil.copyfileobj(file.file, buffer) return {"filename": file.filename, "size": file_path.stat().st_size} @router.post("/jobs") async def create_job(request: JobCreate): job = job_manager.create_job(filename=request.filename, ...) asyncio.create_task(job_manager.run_conversion(job.id)) return job @router.get("/jobs") async def list_jobs(): return job_manager.get_all_jobs() @router.get("/jobs/{job_id}/download") async def download_job(job_id: str): job = job_manager.get_job(job_id) if not job or job.status != "completed": raise HTTPException(404) output_path = Path("/mnt/outputs") / job_id / job.output_file return FileResponse(output_path, filename=job.output_file) ``` ### Frontend: Svelte 5 Components ```svelte