3v324v23's picture
Make demo deployable on Hugging Face Spaces + add local/Groq/demo onboarding
29e37f2
Raw
History Blame Contribute Delete
6.92 kB
"""
WHAT: The main entry point for the Multiverse AI Studio FastAPI application.
WHY: This file bootstraps the entire web server. It configures Cross-Origin Resource Sharing (CORS),
mounts physical filesystem directories to virtual web URLs, registers the API routes,
and handles server lifecycle hooks (such as creating storage folders on startup).
HOW: It instantiates a `FastAPI` application, applies standard middlewares (CORSMiddleware),
mounts `StaticFiles`, and registers the router imported from `api/routes.py`.
"""
import os
from dotenv import load_dotenv
# WHY: Load .env variables *before* importing config.py. That way,
# os.getenv("HF_TOKEN") calls in config.py will find the loaded token.
load_dotenv()
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
# Import configurations and routers (relative to the backend package)
from .config import OUTPUT_DIR
from .api.routes import router as api_router
@asynccontextmanager
async def lifespan(app: FastAPI):
"""
WHAT: A lifecyle manager handling startup and shutdown operations for the application.
WHY: Before the server begins accepting client requests, we must guarantee that the local
filesystem is ready to receive generated assets. If we don't create the OUTPUT_DIR,
subsequent file write operations will throw directory-not-found errors.
HOW: Uses Python's `contextlib.asynccontextmanager` decorator. Everything before the `yield`
runs on startup; everything after runs on shutdown.
"""
# STARTUP: Create the asset output folder if it doesn't exist yet
if not os.path.exists(OUTPUT_DIR):
print(f"[Startup] Creating directory for generated assets: {OUTPUT_DIR}")
os.makedirs(OUTPUT_DIR, exist_ok=True)
else:
print(f"[Startup] Output directory already exists: {OUTPUT_DIR}")
yield # The application runs and processes requests here
# SHUTDOWN: Any cleanup logic (e.g., closing DB pools or freeing remaining models) goes here.
print("[Shutdown] Cleaning up API resources.")
# Initialize the FastAPI application
# Using the lifespan parameter to handle our startup directory creation logic cleanly
app = FastAPI(
title="Multiverse AI Studio API",
description=(
"A production-quality generative AI pipeline backend that chains five Hugging Face models: "
"Prompt Expansion (LLM), Image Generation (SDXL), Depth Estimation (Depth-Anything), "
"Audio Generation (MusicGen), and Video Generation (i2vgen-xl)."
),
version="1.0.0",
lifespan=lifespan
)
# ==========================================
# CORS CONFIGURATION (Cross-Origin Resource Sharing)
# ==========================================
# WHAT: Specifies which web domains are permitted to communicate with this backend API.
# WHY: By default, web browsers block frontend code (e.g., running on localhost:3000) from
# making requests to a different domain/port (e.g., running on localhost:8000).
# Configuring CORS allows our Next.js/React frontend to talk to our FastAPI backend safely.
# HOW: We use FastAPI's built-in CORSMiddleware and supply an explicit list of allowed origins.
origins = [
"http://localhost:3000", # Standard React / Next.js development server
"http://127.0.0.1:3000", # Alternative local address
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"], # Allows all HTTP methods (GET, POST, OPTIONS, etc.)
allow_headers=["*"], # Allows all HTTP headers (Content-Type, Authorization, etc.)
)
# ==========================================
# STATIC FILES MOUNTING
# ==========================================
# WHAT: Maps a directory on the server's hard drive to a virtual web route.
# WHY: When we save a generated video to `generated_assets/job_123/video.mp4`, we need a way
# for the frontend to download and display it. Instead of writing custom download endpoints,
# FastAPI can serve everything in this folder statically.
# HOW: We mount the `OUTPUT_DIR` to the path `/assets`.
# Example: Opening `http://localhost:8000/assets/123/image.png` will load the file
# stored at `generated_assets/123/image.png` on disk.
# WHY ORDER MATTERS: FastAPI's StaticFiles will crash immediately if the directory doesn't exist
# at mount time (which happens *before* the lifespan function runs). So we create the directory first.
if not os.path.exists(OUTPUT_DIR):
os.makedirs(OUTPUT_DIR, exist_ok=True)
app.mount(
"/assets",
StaticFiles(directory=OUTPUT_DIR),
name="assets"
)
# ==========================================
# API ROUTE REGISTRATION
# ==========================================
# WHAT: Registers our logical endpoints under a standard url path.
# WHY: Keeps code modular by keeping route definitions inside `api/routes.py` rather than
# cluttering `main.py`.
# HOW: We prefix all routes with `/api` so that endpoints become `/api/generate`, `/api/status`, etc.
app.include_router(
api_router,
prefix="/api"
)
# ==========================================
# FRONTEND STATIC FILES MOUNTING (FOR MONOLITHIC PRODUCTION DEPLOYMENT)
# ==========================================
# WHAT: Serves the built React frontend bundle.
# WHY: In platforms like Hugging Face Spaces, the container can only expose a single port (7860).
# FastAPI serves the static HTML/JS/CSS React bundle and handles API requests on "/api".
# HOW: We mount the bundled assets under "/static" and serve "index.html" for both "/" and any
# unknown client-side route (SPA fallback), so deep links like /studio/<jobId> survive a refresh.
FRONTEND_DIST_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "frontend", "dist")
FRONTEND_INDEX = os.path.join(FRONTEND_DIST_DIR, "index.html")
if os.path.exists(FRONTEND_DIST_DIR):
print(f"[Startup] Serving frontend static files from: {FRONTEND_DIST_DIR}")
frontend_static_dir = os.path.join(FRONTEND_DIST_DIR, "static")
if os.path.isdir(frontend_static_dir):
app.mount(
"/static",
StaticFiles(directory=frontend_static_dir),
name="frontend_static",
)
@app.get("/{full_path:path}")
async def serve_spa(full_path: str):
# Unknown routes fall back to index.html so client-side (React Router) routing works.
return FileResponse(FRONTEND_INDEX)
else:
print("[Startup Warning] Frontend build directory not found. Server running API-only mode.")
# Standard run instruction comment
# To run this server, execute the following command in your terminal from the backend directory:
# `uvicorn main:app --host 0.0.0.0 --port 8000 --reload`