Spaces:
Sleeping
Sleeping
| 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 | |
| # ========================================== | |
| 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)) | |
| 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)) | |
| def health_check(): | |
| return {"status": "Forge is online and ready."} | |