Minecraftify / backend /backend.py
AnimeOverlord's picture
still initial commit
b97cf3f
Raw
History Blame
4.89 kB
import io
import os
import modal
# ==============================================================================
# 💾 0. CACHE VOLUME SETUP
# ==============================================================================
# This creates a persistent volume that survives between container cold-starts.
cache_volume = modal.Volume.from_name("flux-inductor-cache", create_if_missing=True)
CACHE_DIR = "/root/.cache/torch/inductor"
# Define container environment
image = (
modal.Image.debian_slim(python_version="3.12")
.pip_install(
"diffusers",
"transformers",
"accelerate",
"pillow",
"torch",
"triton" # Essential for torch.compile
)
# Tell PyTorch Inductor to write its cache to our persistent volume directory
.env({"TORCHINDUCTOR_CACHE_DIR": CACHE_DIR})
)
app = modal.App("flux-klein-voxel-backend", image=image)
# ==============================================================================
# 🏎️ 1. THE DEMO PIPELINE (FALLBACK ROUTE)
# ==============================================================================
@app.function()
def demo_stream_frame(img_bytes: bytes) -> bytes:
"""Fallback route structurally aligned to match the frontend signature."""
from PIL import Image, ImageDraw
input_image = Image.open(io.BytesIO(img_bytes)).convert("RGB")
draw = ImageDraw.Draw(input_image)
draw.text((20, 20), "🛠️ WEBRTC PASSTHROUGH DEMO ACTIVE", fill=(0, 255, 0))
output_buffer = io.BytesIO()
input_image.save(output_buffer, format="JPEG", quality=85)
return output_buffer.getvalue()
# ==============================================================================
# 🚀 2. THE REAL-TIME VOXEL ENGINE
# ==============================================================================
@app.cls(
gpu="A10G",
secrets=[modal.Secret.from_name("huggingface-secret")],
max_containers=5,
# Mount the persistent cache volume to the exact path PyTorch is looking at
volumes={CACHE_DIR: cache_volume}
)
class VoxelModel:
@modal.enter()
def load_pipeline(self):
import torch
from PIL import Image
from diffusers import AutoPipelineForImage2Image
model_id = "AnimeOverlord/flux2-klein-4b-mc"
self.pipe = AutoPipelineForImage2Image.from_pretrained(
model_id,
torch_dtype=torch.bfloat16,
token=os.environ["HF_TOKEN"] # Modernized from use_auth_token
)
self.pipe.to("cuda")
self.pipe.enable_attention_slicing()
# ---------------------------------------------------------
# ✨ COMPILE AND OPTIMIZE THE INFRASTRUCTURE
# ---------------------------------------------------------
print("Initializing torch.compile optimization loops...")
# Channels-last optimization applied exclusively to the VAE (CNN-based)
self.pipe.vae.to(memory_format=torch.channels_last)
# Compile the heaviest part of the FLUX architecture safely without memory layout issues
self.pipe.transformer = torch.compile(
self.pipe.transformer,
mode="reduce-overhead", # Trades a bit of compile time for faster inference
fullgraph=False
)
# WARMUP RUN: Force a dummy inference immediately execution starts.
# If the cache is empty (first run), this traces the graph and saves to the Modal Volume.
# If the cache exists, it instantly hotloads from the Volume.
print("Running warmup to build/load inductor cache...")
dummy_image = Image.new("RGB", (512, 512), (0, 0, 0))
with torch.inference_mode():
self.pipe(
prompt="warmup pass",
image=dummy_image,
strength=0.5,
num_inference_steps=2,
guidance_scale=1.0,
)
print("Warmup complete. Ready for real-time requests!")
@modal.method()
def process_frame(self, img_bytes: bytes) -> bytes:
from PIL import Image
import torch
# Hardcoded parameters that were previously passed from the UI
prompt = "isometric 3d minecraft block voxel style, high resolution, volumetric lighting"
strength = 0.45
input_image = Image.open(io.BytesIO(img_bytes)).convert("RGB")
input_image = input_image.resize((512, 512))
with torch.inference_mode():
output_image = self.pipe(
prompt=prompt,
image=input_image,
strength=strength,
num_inference_steps=4,
guidance_scale=3.5,
).images[0]
output_buffer = io.BytesIO()
output_image.save(output_buffer, format="JPEG", quality=85)
return output_buffer.getvalue()