visual-lineage / compose /merge.py
verymehari's picture
fix: ZeroGPU duration 180s, 12 steps
b472957
Raw
History Blame Contribute Delete
3.67 kB
from __future__ import annotations
import os
from pathlib import Path
from typing import Any
from compose.provenance import build_provenance, load_registry, lora_map, normalize_weights
BASE_INFERENCE_MODEL = "black-forest-labs/FLUX.2-klein-4B"
_PIPE = None
_LOADED_ADAPTERS: set[str] = set()
def _device():
import torch
if torch.cuda.is_available():
return "cuda", torch.bfloat16
if getattr(torch.backends, "mps", None) and torch.backends.mps.is_available():
return "mps", torch.float16
return "cpu", torch.float32
def _load_pipe():
global _PIPE
if _PIPE is not None:
return _PIPE
import torch
from diffusers import Flux2KleinPipeline
device, dtype = _device()
token = os.environ.get("HF_TOKEN")
pipe = Flux2KleinPipeline.from_pretrained(
BASE_INFERENCE_MODEL,
torch_dtype=dtype,
token=token,
)
pipe = pipe.to(device)
_PIPE = pipe
return pipe
def _load_adapters(pipe, lora_ids: list[str], registry_by_id: dict[str, dict[str, Any]]) -> None:
for lora_id in lora_ids:
if lora_id in _LOADED_ADAPTERS:
continue
repo = registry_by_id[lora_id]["hf_repo"]
pipe.load_lora_weights(repo, adapter_name=lora_id, token=os.environ.get("HF_TOKEN"))
_LOADED_ADAPTERS.add(lora_id)
def compose(
lora_ids: list[str],
weights: list[float],
prompt: str,
registry_path: str = "registry/loras.json",
seed: int = 42,
output_dir: str = "outputs",
mode: str = "live",
width: int = 768,
height: int = 768,
num_inference_steps: int = 12,
guidance_scale: float = 2.0,
) -> dict[str, Any]:
"""Generate with FLUX.2 klein + LoRA adapters and return image/provenance.
`mode="live"` is the real path. `mode="mock"` was removed from the UI, but
kept as an explicit developer escape hatch only through NotImplementedError
so accidental demo mocks fail loudly.
"""
if mode != "live":
raise NotImplementedError("Mock generation is disabled. Use mode='live'.")
import torch
if len(lora_ids) != len(weights):
raise ValueError("lora_ids and weights must be the same length")
if not prompt.strip():
raise ValueError("Prompt is required")
registry = load_registry(registry_path)
by_id = lora_map(registry)
missing = [lid for lid in lora_ids if lid not in by_id]
if missing:
raise KeyError(f"Unknown LoRA ids: {missing}")
weights = normalize_weights(weights)
pipe = _load_pipe()
_load_adapters(pipe, lora_ids, by_id)
pipe.set_adapters(lora_ids, adapter_weights=weights)
triggers = " ".join(by_id[lid]["trigger"] for lid in lora_ids)
full_prompt = f"{triggers}. {prompt.strip()}"
device, _ = _device()
generator = torch.Generator(device=device if device == "cuda" else "cpu").manual_seed(int(seed))
result = pipe(
prompt=full_prompt,
num_inference_steps=int(num_inference_steps),
guidance_scale=float(guidance_scale),
height=int(height),
width=int(width),
generator=generator,
)
image = result.images[0]
Path(output_dir).mkdir(parents=True, exist_ok=True)
generation_id = f"{seed}_{'_'.join(lora_ids)}"
output_path = Path(output_dir) / f"{generation_id}.png"
image.save(output_path)
provenance = build_provenance(lora_ids, weights, registry, prompt, seed, str(output_path))
provenance["full_prompt"] = full_prompt
provenance["inference_model"] = BASE_INFERENCE_MODEL
provenance["mode"] = "live"
return {"image": image, "provenance": provenance, "output_path": str(output_path)}