| """Image-generation bridge for the connected Flow Matching project.""" |
|
|
| from __future__ import annotations |
|
|
| import importlib.util |
| import json |
| import random |
| import re |
| import sys |
| from datetime import datetime, timezone |
| from pathlib import Path |
| from types import ModuleType |
|
|
| from adam.config import ConfigManager |
| from adam.executor import ToolCancelled, ToolContext, ToolExecutionError |
| from adam.generations import generation_metadata_path, generation_output_folder |
| from adam.generation_previews import accepts_preview_callback, publish_generation_preview |
|
|
|
|
| _backend_module: ModuleType | None = None |
| _backend_script: Path | None = None |
| _loaded_model: object | None = None |
| _loaded_model_path: Path | None = None |
|
|
|
|
| def _load_backend(script: Path) -> ModuleType: |
| global _backend_module, _backend_script |
| if _backend_module is not None and _backend_script == script: |
| return _backend_module |
| module_name = "_adam_connected_flow_generator" |
| spec = importlib.util.spec_from_file_location(module_name, script) |
| if spec is None or spec.loader is None: |
| raise ToolExecutionError("The connected Flow Matching generator could not be loaded.") |
| module = importlib.util.module_from_spec(spec) |
| sys.modules[module_name] = module |
| try: |
| spec.loader.exec_module(module) |
| except Exception as exc: |
| sys.modules.pop(module_name, None) |
| raise ToolExecutionError(f"Could not load the Flow Matching generator: {exc}") from exc |
| if not callable(getattr(module, "load_unet", None)) or not callable( |
| getattr(module, "sample_flow", None) |
| ): |
| raise ToolExecutionError( |
| "The connected Flow Matching app does not expose its generation functions." |
| ) |
| _backend_module = module |
| _backend_script = script |
| return module |
|
|
|
|
| def _safe_label(value: str) -> str: |
| label = value.strip()[:96] or "Flow" |
| label = re.sub(r"[<>:\"/\\|?*\x00-\x1f]+", " ", label) |
| label = re.sub(r"\s+", " ", label).strip(" .") |
| return label or "Flow" |
|
|
|
|
| def generate_flow_images( |
| context: ToolContext, |
| model_name: str, |
| model_path: str, |
| prompt: str, |
| image_count: int, |
| steps: int, |
| seed: int, |
| sampler: str, |
| aspect_ratio: str, |
| preview_interval: int = 0, |
| ) -> dict[str, object]: |
| global _loaded_model, _loaded_model_path |
| config = ConfigManager(context.root) |
| flow_root = Path( |
| str(config.get("tool_folders", {}).get("flow_trainer", "")) |
| ).expanduser().resolve() |
| script = flow_root / "flow_matching_app.py" |
| if not script.is_file(): |
| raise ToolExecutionError( |
| "Flow Matching flow_matching_app.py was not found. Re-scan its folder in Settings." |
| ) |
| model = Path(model_path).expanduser().resolve() |
| allowed_root = (flow_root / "output_flow_models").resolve() |
| try: |
| model.relative_to(allowed_root) |
| except ValueError as exc: |
| raise ToolExecutionError( |
| "The Flow model must be inside the connected Flow Matching output folder." |
| ) from exc |
| try: |
| info = json.loads((model / "flow_model_info.json").read_text(encoding="utf-8")) |
| except (OSError, ValueError, TypeError, json.JSONDecodeError) as exc: |
| raise ToolExecutionError("Choose a completed Flow Matching image model.") from exc |
| if info.get("model_type") != "rectified_flow" or not ( |
| model / "unet" / "config.json" |
| ).is_file(): |
| raise ToolExecutionError("Choose a completed Flow Matching image model.") |
|
|
| missing_packages = [ |
| package |
| for package in ("torch", "torchvision", "diffusers", "PIL") |
| if importlib.util.find_spec(package) is None |
| ] |
| if missing_packages: |
| raise ToolExecutionError( |
| "ADAM's Python environment is missing Flow generation packages: " |
| + ", ".join(missing_packages) |
| + ". Install the connected Flow Matching requirements, then restart ADAM." |
| ) |
| count = int(image_count) |
| step_count = int(steps) |
| if not 1 <= count <= 48: |
| raise ToolExecutionError("Image count must be between 1 and 48.") |
| if not 1 <= step_count <= 200: |
| raise ToolExecutionError("Flow steps must be between 1 and 200.") |
| method = sampler.strip().title() |
| if method not in {"Heun", "Euler"}: |
| raise ToolExecutionError("Flow generation supports the Heun and Euler methods.") |
| allowed_aspects = { |
| "1:1 (Square)", "4:3 (Landscape)", "3:4 (Portrait)", |
| "3:2 (Landscape)", "2:3 (Portrait)", "16:9 (Widescreen)", |
| "9:16 (Vertical)", |
| } |
| if aspect_ratio not in allowed_aspects: |
| raise ToolExecutionError("Choose one of the supported Flow aspect ratios.") |
| if len(prompt) > 500: |
| raise ToolExecutionError("The generation label must be 500 characters or shorter.") |
| if not 0 <= int(preview_interval) <= step_count: |
| raise ToolExecutionError("Preview interval must be between 0 and the total number of steps.") |
| base_seed = int(seed) |
| if base_seed <= 0: |
| base_seed = random.randint(1, 2_147_483_647 - count) |
| if base_seed + count - 1 > 2_147_483_647: |
| raise ToolExecutionError("The seed is too large for this image count.") |
|
|
| backend = _load_backend(script) |
| preview_enabled = int(preview_interval) > 0 |
| preview_supported = accepts_preview_callback(backend.sample_flow) |
| if preview_enabled and not preview_supported: |
| context.log("This connected Flow generator does not yet expose denoising previews; generation will continue normally.") |
| try: |
| import torch |
|
|
| device = torch.device("cuda" if torch.cuda.is_available() else "cpu") |
| dtype = torch.float16 if device.type == "cuda" else torch.float32 |
| if _loaded_model is None or _loaded_model_path != model: |
| _loaded_model = None |
| if torch.cuda.is_available(): |
| torch.cuda.empty_cache() |
| context.log(f"Loading completed Flow Matching model: {model_name}") |
| _loaded_model = backend.load_unet(model, device=device, dtype=dtype) |
| _loaded_model_path = model |
| except ToolCancelled: |
| raise |
| except Exception as exc: |
| raise ToolExecutionError(f"Could not load the Flow Matching model: {exc}") from exc |
|
|
| timestamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S") |
| output = generation_output_folder(context.root, context.tool.id, _safe_label(model_name)) |
| context.log("Flow Matching models generate learned visual samples; the label is metadata, not a text prompt.") |
| image_paths: list[str] = [] |
| for index in range(count): |
| context.checkpoint() |
| current_seed = base_seed + index |
|
|
| def on_progress(done: int, total: int, image_index: int = index) -> None: |
| completed = image_index + (done / max(1, total)) |
| context.progress( |
| max(1, min(99, round(completed * 100 / count))), |
| f"Generating image {image_index + 1} of {count} · flow step {done} of {total}", |
| ) |
|
|
| try: |
| settings = {"aspect_ratio": aspect_ratio} |
| if preview_enabled and preview_supported: |
| settings["preview_interval"] = int(preview_interval) |
| settings["preview_callback"] = lambda payload, step=0, total_steps=step_count, current=index: publish_generation_preview( |
| context, output, payload, image_index=current, image_count=count, |
| step=step, total_steps=total_steps, |
| ) |
| images = backend.sample_flow( |
| _loaded_model, |
| 1, |
| step_count, |
| device, |
| dtype, |
| current_seed, |
| method, |
| on_progress, |
| **settings, |
| ) |
| destination = output / f"{timestamp}_{context.job_id}_{method}_seed_{current_seed}.png" |
| images[0].save(destination, format="PNG") |
| except ToolCancelled: |
| raise |
| except Exception as exc: |
| raise ToolExecutionError(f"Flow Matching generation failed: {exc}") from exc |
| image_paths.append(str(destination)) |
|
|
| created_at = datetime.now(timezone.utc).isoformat() |
| metadata = { |
| "version": 1, |
| "provider_id": context.tool.id, |
| "provider_name": context.tool.name, |
| "model_name": _safe_label(model_name), |
| "model_path": str(model), |
| "prompt": prompt.strip(), |
| "prompt_behavior": "label_only", |
| "seed": base_seed, |
| "image_seeds": [base_seed + index for index in range(count)], |
| "image_count": count, |
| "steps": step_count, |
| "sampler": method, |
| "aspect_ratio": aspect_ratio, |
| "preview_interval": int(preview_interval), |
| "preview_supported": preview_supported, |
| "images": image_paths, |
| "created_at": created_at, |
| } |
| generation_metadata_path(output, timestamp, context.job_id).write_text( |
| json.dumps(metadata, indent=2), encoding="utf-8" |
| ) |
| context.progress(100, f"Generated {count} image(s)") |
| return { |
| "output_folder": str(output), |
| "assets": [ |
| { |
| "kind": "generation", |
| "name": f"{_safe_label(model_name)} · {timestamp}", |
| "path": str(output), |
| "trainer": "flow", |
| } |
| ], |
| } |
|
|