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)}