import io import os import modal APP_NAME = "virtual-characters-image" GPU = os.environ.get("VC_IMAGE_GPU", "H100") MODEL_ID = os.environ.get("VC_IMAGE_MODEL", "black-forest-labs/FLUX.1-schnell") MODEL_DIR = "/root/.cache/huggingface" HF_SECRET_NAME = os.environ.get("VC_HF_SECRET_NAME", "hf-token") HF_SECRETS = [] if os.environ.get("VC_SKIP_HF_SECRET") == "1" else [modal.Secret.from_name(HF_SECRET_NAME)] image = ( modal.Image.from_registry("nvidia/cuda:12.9.0-devel-ubuntu22.04", add_python="3.11") .entrypoint([]) .apt_install("git", "libglib2.0-0", "libsm6", "libxrender1", "libxext6", "ffmpeg", "libgl1") .uv_pip_install( "accelerate>=1.8.0", "diffusers>=0.35.0", "fastapi[standard]>=0.115.0", "huggingface-hub>=0.36.0", "safetensors>=0.5.0", "sentencepiece>=0.2.0", "torch>=2.7.0", "transformers>=4.57.0", ) .env({"HF_HUB_CACHE": MODEL_DIR, "HF_XET_HIGH_PERFORMANCE": "1"}) ) hf_cache = modal.Volume.from_name("vc-hf-cache", create_if_missing=True) app = modal.App(APP_NAME, image=image) @app.cls( gpu=GPU, scaledown_window=60, timeout=60 * 20, secrets=HF_SECRETS, volumes={MODEL_DIR: hf_cache}, ) class CharacterImage: def _ensure_loaded(self): if getattr(self, "pipe", None) is not None: return import torch from diffusers import FluxPipeline self.pipe = FluxPipeline.from_pretrained(MODEL_ID, torch_dtype=torch.bfloat16).to("cuda") @modal.method() def health(self) -> dict: return {"ok": True, "model": MODEL_ID, "gpu": GPU, "loaded": getattr(self, "pipe", None) is not None} @modal.method() def generate(self, prompt: str, steps: int = 4, seed: int | None = None) -> bytes: self._ensure_loaded() import torch generator = None if seed is not None: generator = torch.Generator(device="cuda").manual_seed(seed) image = self.pipe(prompt, num_inference_steps=steps, output_type="pil", generator=generator).images[0] buffer = io.BytesIO() image.save(buffer, format="PNG") buffer.seek(0) return buffer.read() @modal.fastapi_endpoint(method="POST") async def character_image(self, request): from fastapi.responses import Response payload = await request.json() data = self.generate.local( prompt=payload["prompt"], steps=int(payload.get("steps", 4)), seed=payload.get("seed"), ) return Response(content=data, media_type="image/png") @app.local_entrypoint() def main(prompt: str = "original anime virtual character portrait, silver hair, teal eyes, soft sci-fi lighting", output_path: str = "/tmp/vc_character.png"): print(CharacterImage().health.remote()) data = CharacterImage().generate.remote(prompt=prompt, steps=4, seed=42) with open(output_path, "wb") as f: f.write(data) print(f"Wrote {len(data)} bytes to {output_path}")