File size: 4,893 Bytes
9e606d9
 
 
 
1c0a58f
 
 
 
 
 
 
077cba7
1c0a58f
 
 
 
 
 
 
 
 
 
 
 
9e606d9
 
 
 
 
3a269bf
9e606d9
 
b97cf3f
3a269bf
9e606d9
 
 
 
 
 
 
 
 
 
 
 
077cba7
9e606d9
 
 
077cba7
1c0a58f
 
 
9e606d9
 
 
 
 
 
1c0a58f
077cba7
9e606d9
 
 
077cba7
9e606d9
 
3a269bf
9e606d9
 
 
 
1c0a58f
3a269bf
1c0a58f
3a269bf
1c0a58f
3a269bf
 
1c0a58f
3a269bf
1c0a58f
 
3a269bf
1c0a58f
 
 
3a269bf
 
 
1c0a58f
 
 
 
 
 
 
 
 
 
 
 
077cba7
b97cf3f
9e606d9
 
 
b97cf3f
 
 
 
9e606d9
 
 
 
 
 
 
 
077cba7
9e606d9
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
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()