Spaces:
Runtime error
Runtime error
File size: 13,367 Bytes
7a7354f e04dcfa 7a7354f e04dcfa 7a7354f e04dcfa 7a7354f e04dcfa 7a7354f e04dcfa 7a7354f e04dcfa 7a7354f e04dcfa 7a7354f e04dcfa 7a7354f e04dcfa 7a7354f e04dcfa 7a7354f e04dcfa 7a7354f e04dcfa 7a7354f e04dcfa 7a7354f e04dcfa 7a7354f e04dcfa 7a7354f e04dcfa 7a7354f e04dcfa 7a7354f e04dcfa 7a7354f e04dcfa 7a7354f e04dcfa 7a7354f e04dcfa 7a7354f e04dcfa 7a7354f e04dcfa 7a7354f e04dcfa 7a7354f e04dcfa 7a7354f e04dcfa 7a7354f e04dcfa 7a7354f e04dcfa 7a7354f e04dcfa 7a7354f e04dcfa 7a7354f e04dcfa 7a7354f e04dcfa 7a7354f e04dcfa 7a7354f e04dcfa | 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 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 | """
NEXUS Visual Weaver β Modal Refinement Pipeline v2
===================================================
Real FLUX.1-Kontext-dev img2img refinement with multi-LoRA on Modal.
GPU options: A100-80GB, A100-40GB, L40S, T4
LoRA adapters: NO8D/BodyControl, NO8D/ExpressionControl, fal/realism-detailer,
ilkerzgi/metallic, ilkerzgi/glittering-portrait, ilkerzgi/embroidery-patch
Usage:
modal run modal_nexus_refine_v2.py --image-path input.png
Or call remotely from HF Space:
fn = modal.Function.lookup("nexus-couture-refine-v2", "refine_couture")
result_bytes = fn.remote(image_bytes=..., lora_adapters=["garment", "hardware"])
"""
import modal
from io import BytesIO
from PIL import Image
from typing import List, Optional
app = modal.App("nexus-couture-refine-v2")
# βββ Image with all dependencies for FLUX Kontext + LoRA βββ
image = (
modal.Image.debian_slim(python_version="3.12")
.apt_install("git", "libgl1-mesa-glx", "libglib2.0-0")
.pip_install(
"torch==2.5.1",
"torchvision==0.20.1",
"diffusers>=0.32.0",
"transformers>=4.45.0",
"accelerate>=1.1.0",
"safetensors",
"Pillow",
"huggingface-hub",
"peft>=0.13.0",
"protobuf",
"sentencepiece",
)
)
# Persistent volume for model caching (saves startup time & bandwidth)
volume = modal.Volume.from_name("nexus-model-cache", create_if_missing=True)
# βββ NEXUS Taste Profile β The "Soul" of the generator βββ
NEXUS_CORE_STYLE = (
"Slavic woman, rain-slick neon cyberpunk city at night, long structured black patent leather coat, "
"faux fur collar, Chantilly lace neckline, glowing crimson hardware, platform boots, "
"floating NEXUS sigils and code streams, ultra detailed wet fabric texture, cinematic lighting, "
"high fashion editorial, photorealistic, 8k"
)
# βββ LoRA Adapter Registry βββ
# Maps short names to HF repo IDs for the Space UI
LORA_REGISTRY = {
"garment": {
"repo_id": "NO8D/BodyControl",
"adapter_name": "garment_control",
"weight": 0.75,
"description": "Body/garment shape control for FLUX",
},
"hardware": {
"repo_id": "NO8D/ExpressionControl",
"adapter_name": "expression_control",
"weight": 0.70,
"description": "Expression/hardware detail control",
},
"realism": {
"repo_id": "fal/realism-detailer",
"adapter_name": "realism_detail",
"weight": 0.60,
"description": "Photorealistic detail enhancement",
},
"metallic": {
"repo_id": "ilkerzgi/metallic",
"adapter_name": "metallic_finish",
"weight": 0.55,
"description": "Metallic material finish (hardware, buckles)",
},
"glittering": {
"repo_id": "ilkerzgi/glittering-portrait",
"adapter_name": "glittering_portrait",
"weight": 0.55,
"description": "Glittering/sparkling portrait effects",
},
"embroidery": {
"repo_id": "ilkerzgi/embroidery-patch",
"adapter_name": "embroidery_patch",
"weight": 0.55,
"description": "Embroidery and patch textures on garments",
},
}
# GPU pricing for cost tracker (USD per hour)
GPU_PRICING = {
"A100-80GB": 1.80,
"A100-40GB": 1.10,
"L40S": 1.05,
"T4": 0.40,
}
# Map GPU names to Modal GPU identifiers
GPU_MAP = {
"A100-80GB": "A100",
"A100-40GB": "A10G", # Modal A10G is the closest to A100-40GB
"L40S": "L40S",
"T4": "T4",
}
def _get_lora_adapters(adapter_keys: Optional[List[str]] = None) -> List[dict]:
"""Resolve LoRA adapter keys to full config dicts."""
if not adapter_keys:
return []
adapters = []
for key in adapter_keys:
key = key.strip().lower()
if key in LORA_REGISTRY:
adapters.append(LORA_REGISTRY[key])
else:
print(f"β οΈ Unknown LoRA adapter key: {key}, skipping")
return adapters
@app.function(
image=image,
gpu="A100", # Default to A100-80GB for best performance
volumes={"/cache": volume},
timeout=600, # 10 minutes max per run
allow_concurrent_inputs=4,
)
def refine_couture(
image_bytes: bytes,
user_addition: str = "",
strength: float = 0.58,
steps: int = 32,
guidance_scale: float = 3.8,
seed: int = -1,
lora_adapters: Optional[List[str]] = None,
negative_prompt: str = "blurry, low quality, deformed, extra limbs, bad anatomy, watermark, text",
gpu_type: str = "A100-80GB",
) -> bytes:
"""
Refines an input image using FLUX.1-Kontext-dev with optional multi-LoRA.
Preserves the core NEXUS aesthetic while applying user modifications.
Args:
image_bytes: Input image as PNG/JPEG bytes
user_addition: Additional prompt text to append to NEXUS core style
strength: img2img strength (0.0-1.0, higher = more change)
steps: Number of inference steps
guidance_scale: Classifier-free guidance scale
seed: Random seed (-1 for random)
lora_adapters: List of adapter keys: "garment", "hardware", "realism",
"metallic", "glittering", "embroidery"
negative_prompt: Negative prompt for generation
gpu_type: GPU to use (A100-80GB, A100-40GB, L40S, T4)
Returns:
PNG image bytes of the refined result
"""
import torch
from diffusers import FluxKontextPipeline
import time
started = time.time()
print(f"π¨ NEXUS Kontext Refinement v2")
print(f" GPU: {gpu_type} | Strength: {strength} | Steps: {steps} | Guidance: {guidance_scale}")
print(f" LoRA adapters requested: {lora_adapters}")
# βββ Load Pipeline βββ
print("β³ Loading FLUX.1-Kontext-dev pipeline...")
pipe = FluxKontextPipeline.from_pretrained(
"black-forest-labs/FLUX.1-Kontext-dev",
torch_dtype=torch.bfloat16,
cache_dir="/cache",
).to("cuda")
# Enable memory efficient attention
try:
pipe.enable_xformers_memory_efficient_attention()
except Exception:
print(" βΉοΈ xformers not available, using default attention")
# βββ Load LoRA Adapters βββ
adapters = _get_lora_adapters(lora_adapters)
loaded_adapters = []
if adapters:
print(f"π Loading {len(adapters)} LoRA adapter(s)...")
for adapter_cfg in adapters:
try:
print(f" Loading: {adapter_cfg['repo_id']} ({adapter_cfg['adapter_name']})")
pipe.load_lora_weights(
adapter_cfg["repo_id"],
adapter_name=adapter_cfg["adapter_name"],
)
loaded_adapters.append(adapter_cfg)
print(f" β
Loaded: {adapter_cfg['adapter_name']}")
except Exception as e:
print(f" β Failed to load {adapter_cfg['repo_id']}: {e}")
print(f" β οΈ Continuing without this adapter")
# Activate all loaded adapters with their weights
if loaded_adapters:
adapter_names = [a["adapter_name"] for a in loaded_adapters]
adapter_weights = [a["weight"] for a in loaded_adapters]
try:
pipe.set_adapters(adapter_names, adapter_weights=adapter_weights)
print(f" β
Activated {len(loaded_adapters)} adapter(s): {adapter_names}")
except Exception as e:
print(f" β οΈ Could not set multi-adapter weights: {e}")
# Fallback: activate first adapter only
try:
pipe.set_adapters([loaded_adapters[0]["adapter_name"]],
adapter_weights=[loaded_adapters[0]["weight"]])
except Exception:
print(" β οΈ Single adapter fallback also failed, using base model only")
# βββ Process Input Image βββ
init_image = Image.open(BytesIO(image_bytes)).convert("RGB")
# Resize if too large (>2MP) to save VRAM/time
width, height = init_image.size
if width * height > 2_000_000:
scale = (2_000_000 / (width * height)) ** 0.5
new_size = (int(width * scale), int(height * scale))
init_image = init_image.resize(new_size, Image.LANCZOS)
print(f" π Resized from {width}x{height} to {new_size[0]}x{new_size[1]}")
# βββ Construct Final Prompt βββ
final_prompt = f"{NEXUS_CORE_STYLE}, {user_addition}" if user_addition else NEXUS_CORE_STYLE
# βββ Seed Handling βββ
if seed == -1:
import random
seed = random.randint(0, 2**32 - 1)
generator = torch.Generator(device="cuda").manual_seed(seed)
print(f"π― Generating with seed {seed}")
print(f" Prompt: {final_prompt[:120]}...")
# βββ Run Inference βββ
result = pipe(
image=init_image,
prompt=final_prompt,
negative_prompt=negative_prompt,
guidance_scale=guidance_scale,
num_inference_steps=steps,
strength=strength,
generator=generator,
).images[0]
# βββ Return as PNG bytes βββ
buf = BytesIO()
result.save(buf, format="PNG")
elapsed = time.time() - started
print(f"β
Refinement complete in {elapsed:.1f}s")
return buf.getvalue()
@app.function(
image=image,
gpu="A100",
volumes={"/cache": volume},
timeout=600,
)
def check_modal_health() -> dict:
"""Quick health check β verifies Modal can load the pipeline."""
import torch
try:
cuda_available = torch.cuda.is_available()
gpu_name = torch.cuda.get_device_name(0) if cuda_available else "N/A"
gpu_mem = torch.cuda.get_device_properties(0).total_mem if cuda_available else 0
return {
"status": "healthy",
"cuda": cuda_available,
"gpu": gpu_name,
"gpu_memory_gb": round(gpu_mem / 1e9, 1),
"lora_registry": list(LORA_REGISTRY.keys()),
"gpu_pricing": GPU_PRICING,
}
except Exception as e:
return {"status": "error", "message": str(e)}
@app.function(
image=image,
gpu="A100",
volumes={"/cache": volume},
timeout=900,
)
def generate_from_text(
prompt: str,
user_addition: str = "",
width: int = 1024,
height: int = 1024,
steps: int = 4,
guidance_scale: float = 1.0,
seed: int = -1,
lora_adapters: Optional[List[str]] = None,
) -> bytes:
"""
Generate a new image from text using FLUX.2-Klein-9B with optional LoRA.
For the Space's primary generation (no input image needed).
"""
import torch
from diffusers import Flux2KleinPipeline
import random
print("π¨ NEXUS Text-to-Image Generation (Modal)")
pipe = Flux2KleinPipeline.from_pretrained(
"black-forest-labs/FLUX.2-klein-9B",
torch_dtype=torch.bfloat16,
cache_dir="/cache",
).to("cuda")
# Load LoRA adapters if specified
adapters = _get_lora_adapters(lora_adapters)
loaded = []
for adapter_cfg in adapters:
try:
pipe.load_lora_weights(adapter_cfg["repo_id"], adapter_name=adapter_cfg["adapter_name"])
loaded.append(adapter_cfg)
except Exception as e:
print(f"β οΈ Failed to load LoRA {adapter_cfg['repo_id']}: {e}")
if loaded:
try:
pipe.set_adapters(
[a["adapter_name"] for a in loaded],
adapter_weights=[a["weight"] for a in loaded],
)
except Exception:
pass
if seed == -1:
seed = random.randint(0, 2**32 - 1)
generator = torch.Generator(device="cuda").manual_seed(seed)
final_prompt = f"{NEXUS_CORE_STYLE}, {user_addition}" if user_addition else prompt
result = pipe(
prompt=final_prompt,
height=height,
width=width,
guidance_scale=guidance_scale,
num_inference_steps=steps,
generator=generator,
).images[0]
buf = BytesIO()
result.save(buf, format="PNG")
return buf.getvalue()
@app.local_entrypoint()
def test_refine(
image_path: str = "test_input.png",
output_path: str = "test_output.png",
user_prompt: str = "glowing crimson buckles, wet pavement reflection",
loras: str = "garment,realism",
):
"""Local test entrypoint β runs the refinement on Modal"""
from pathlib import Path
if not Path(image_path).exists():
print(f"β Input image not found: {image_path}")
print("Creating a dummy 512x512 test image...")
test_img = Image.new("RGB", (512, 512), color=(30, 10, 50))
buf = BytesIO()
test_img.save(buf, format="PNG")
image_bytes = buf.getvalue()
else:
with open(image_path, "rb") as f:
image_bytes = f.read()
lora_list = [l.strip() for l in loras.split(",") if l.strip()] if loras else None
print("π Sending to Modal A100 for refinement...")
result_bytes = refine_couture.remote(
image_bytes=image_bytes,
user_addition=user_prompt,
lora_adapters=lora_list,
strength=0.58,
steps=32,
)
with open(output_path, "wb") as f:
f.write(result_bytes)
print(f"β
Success! Output saved to {output_path}")
|