gacha-forge / app.py
Amit9011's picture
Update app.py
8b374ca verified
Raw
History Blame Contribute Delete
5.3 kB
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from fastapi.responses import Response
from typing import List, Optional
import aiohttp
import asyncio
import io
import logging
# Import all generators
from generators.legendary import PremiumGenerator
from generators.common import CommonGenerator
from generators.custom import CustomGenerator
from generators.profile import ProfileGenerator
app = FastAPI()
logging.basicConfig(level=logging.INFO)
# Pre-load the heavy VFX into the Space's 16GB of RAM on startup
PremiumGenerator.preload_vfx_cache()
# ==========================================
# 📦 PYDANTIC MODELS
# ==========================================
class CardRequest(BaseModel):
image_url: str
char_name: str
print_num: int
rarity: str
element: str
custom_frame_url: Optional[str] = None
class ProfileCardData(BaseModel):
name: str
print_num: int
rarity: str
element: str
image_url: str
custom_frame_url: Optional[str] = None
class ProfileRequest(BaseModel):
username: str
team_data: List[Optional[ProfileCardData]]
# ==========================================
# 🌐 ASYNC DOWNLOAD HELPERS
# ==========================================
async def download_image(url: str) -> bytes:
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
if resp.status != 200:
raise HTTPException(status_code=400, detail=f"Failed to download asset: {url}")
return await resp.read()
async def fetch_profile_card(card: ProfileCardData, session: aiohttp.ClientSession) -> Optional[dict]:
"""Fetches the character art and frame concurrently for a single profile slot."""
if not card:
return None
img_bytes = None
frame_bytes = None
try:
# We fire both downloads at the exact same time to cut network waiting in half
tasks = [session.get(card.image_url)]
if card.custom_frame_url:
tasks.append(session.get(card.custom_frame_url))
responses = await asyncio.gather(*tasks, return_exceptions=True)
# Parse Character Art
if not isinstance(responses[0], Exception) and responses[0].status == 200:
img_bytes = await responses[0].read()
# Parse Custom Frame (If requested)
if len(responses) > 1 and not isinstance(responses[1], Exception) and responses[1].status == 200:
frame_bytes = await responses[1].read()
except Exception as e:
logging.error(f"Failed to fetch profile images for {card.name}: {e}")
return None
return {
"bytes": img_bytes,
"frame_bytes": frame_bytes,
"name": card.name,
"print_num": card.print_num,
"rarity": card.rarity,
"element": card.element,
"custom_frame_url": card.custom_frame_url
}
# ==========================================
# 🚀 API ENDPOINTS
# ==========================================
@app.post("/generate")
async def generate_card(req: CardRequest):
try:
image_bytes = await download_image(req.image_url)
frame_bytes = await download_image(req.custom_frame_url) if req.custom_frame_url else None
if req.custom_frame_url and frame_bytes:
generator = CustomGenerator()
template = generator.build_template(image_bytes, frame_bytes, req.char_name)
elif req.rarity in ["SSR", "UR"]:
generator = PremiumGenerator()
template = generator.build_template(image_bytes, req.element)
else:
generator = CommonGenerator()
template = generator.build_template(image_bytes, req.element)
final_buffer = generator.apply_text(template, req.char_name, req.print_num, req.element, req.rarity)
template.close()
return Response(content=final_buffer.getvalue(), media_type="image/webp")
except Exception as e:
logging.error(f"Generation failed: {e}")
raise HTTPException(status_code=500, detail=str(e))
@app.post("/generate_profile")
async def generate_profile(req: ProfileRequest):
try:
async with aiohttp.ClientSession() as session:
# Launch all 8 slot downloads simultaneously
tasks = [fetch_profile_card(card, session) if card else asyncio.sleep(0) for card in req.team_data]
processed_team_data = await asyncio.gather(*tasks)
# Clean out the asyncio.sleep(0) results and replace with None
processed_team_data = [res if isinstance(res, dict) else None for res in processed_team_data]
generator = ProfileGenerator()
final_buffer = generator.build_profile(
username=req.username,
stats={},
showcase_bytes=None,
team_data=processed_team_data,
avatar_bytes=None
)
return Response(content=final_buffer.getvalue(), media_type="image/webp")
except Exception as e:
logging.error(f"Profile Generation failed: {e}")
raise HTTPException(status_code=500, detail=str(e))
@app.get("/")
def health_check():
return {"status": "Forge is online and ready."}