| """Image-generation bridge for the connected DDPM 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 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 |
|
|
|
|
| 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_ddpm_generator" |
| spec = importlib.util.spec_from_file_location(module_name, script) |
| if spec is None or spec.loader is None: |
| raise ToolExecutionError("The connected DDPM 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 DDPM generator: {exc}") from exc |
| if not callable(getattr(module, "generate_images", None)): |
| raise ToolExecutionError( |
| "The connected DDPM app does not expose its generate_images function." |
| ) |
| _backend_module = module |
| _backend_script = script |
| return module |
|
|
|
|
| def _safe_label(value: str) -> str: |
| label = value.strip()[:96] or "DDPM" |
| label = re.sub(r"[<>:\"/\\|?*\x00-\x1f]+", " ", label) |
| label = re.sub(r"\s+", " ", label).strip(" .") |
| return label or "DDPM" |
|
|
|
|
| def generate_ddpm_images( |
| context: ToolContext, |
| model_name: str, |
| model_path: str, |
| prompt: str, |
| image_count: int, |
| steps: int, |
| seed: int, |
| sampler: str, |
| aspect_ratio: str, |
| reference_image: str = "", |
| reference_strength: int = 65, |
| width: int = 0, |
| height: int = 0, |
| preview_interval: int = 0, |
| ) -> dict[str, object]: |
| config = ConfigManager(context.root) |
| trainer_root = Path( |
| str(config.get("tool_folders", {}).get("ddpm_trainer", "")) |
| ).expanduser().resolve() |
| script = trainer_root / "appStableDiffusion.py" |
| if not script.is_file(): |
| raise ToolExecutionError( |
| "DDPM appStableDiffusion.py was not found. Re-scan the DDPM folder in Settings." |
| ) |
|
|
| model = Path(model_path).expanduser().resolve() |
| allowed_root = (trainer_root / "output").resolve() |
| try: |
| model.relative_to(allowed_root) |
| except ValueError as exc: |
| raise ToolExecutionError( |
| "The DDPM model must be inside the connected DDPM output folder." |
| ) from exc |
| if not model.is_dir() or not (model / "model_index.json").is_file(): |
| raise ToolExecutionError( |
| "Choose a completed DDPM pipeline. Resume checkpoint folders cannot generate images." |
| ) |
| missing_packages = [ |
| package |
| for package in ("torch", "diffusers", "numpy", "PIL") |
| if importlib.util.find_spec(package) is None |
| ] |
| if missing_packages: |
| raise ToolExecutionError( |
| "ADAM's Python environment is missing DDPM generation packages: " |
| + ", ".join(missing_packages) |
| + ". Close ADAM and open Launch ADAM.bat to install the connected DDPM requirements." |
| ) |
|
|
| 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 5 <= step_count <= 500: |
| raise ToolExecutionError("Generation steps must be between 5 and 500.") |
| sampler = sampler.upper().strip() |
| if sampler not in {"DDPM", "DDIM"}: |
| raise ToolExecutionError("DDPM generation supports the DDPM and DDIM samplers.") |
| allowed_aspects = { |
| "1:1 (Square)", "16:9 (Widescreen)", "9:16 (Portrait)", |
| "4:3 (Classic)", "3:4 (Portrait Classic)", "3:2 (Photo)", |
| "2:3 (Portrait Photo)", |
| } |
| if aspect_ratio not in allowed_aspects: |
| raise ToolExecutionError("Choose one of the supported DDPM aspect ratios.") |
| if len(prompt) > 500: |
| raise ToolExecutionError("The generation label must be 500 characters or shorter.") |
| reference_path = Path(reference_image).expanduser() if reference_image.strip() else None |
| if reference_path is not None: |
| reference_path = reference_path.resolve() |
| if not reference_path.is_file(): |
| raise ToolExecutionError("Choose an existing DDPM reference image.") |
| if reference_path.suffix.casefold() not in {".png", ".jpg", ".jpeg", ".webp", ".bmp"}: |
| raise ToolExecutionError("DDPM reference images must be PNG, JPG, WEBP, or BMP files.") |
| try: |
| strength = int(reference_strength) |
| except (TypeError, ValueError) as exc: |
| raise ToolExecutionError("DDPM reference strength must be a whole number from 0 to 100.") from exc |
| if not 0 <= strength <= 100: |
| raise ToolExecutionError("DDPM reference strength must be between 0 and 100.") |
| try: |
| custom_width, custom_height = int(width or 0), int(height or 0) |
| except (TypeError, ValueError) as exc: |
| raise ToolExecutionError("Custom DDPM dimensions must be whole numbers.") from exc |
| if bool(custom_width) != bool(custom_height): |
| raise ToolExecutionError("Set both custom DDPM width and height, or leave both blank.") |
| if custom_width and not (64 <= custom_width <= 2048 and 64 <= custom_height <= 2048): |
| raise ToolExecutionError("Custom DDPM dimensions must be between 64 and 2048 pixels.") |
| 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.") |
|
|
| 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)) |
| backend = _load_backend(script) |
| preview_enabled = int(preview_interval) > 0 |
| preview_supported = accepts_preview_callback(backend.generate_images) |
| if reference_path: |
| preview_supported = preview_supported and accepts_preview_callback(backend.generate_reference_images) |
| if reference_path and not callable(getattr(backend, "generate_reference_images", None)): |
| raise ToolExecutionError( |
| "The connected DDPM app does not support reference-image generation. Update the DDPM app and restart ADAM." |
| ) |
|
|
| context.log(f"Loading completed DDPM model: {model_name}") |
| if reference_path: |
| context.log(f"Reimagining reference image at {strength}% influence.") |
| context.log("DDPM models generate learned visual samples; the label is saved as metadata, not used as a text prompt.") |
| if preview_enabled and not preview_supported: |
| context.log("This connected DDPM generator does not yet expose denoising previews; generation will continue normally.") |
| image_paths: list[str] = [] |
| for index in range(count): |
| context.checkpoint() |
| current_seed = base_seed + index |
| context.progress( |
| max(1, round(index * 100 / count)), |
| f"Generating image {index + 1} of {count}", |
| ) |
| try: |
| settings = { |
| "seed": current_seed, |
| "num_inference_steps": step_count, |
| "batch_size": 1, |
| "sampler": sampler, |
| "aspect_ratio": aspect_ratio, |
| } |
| if preview_enabled and preview_supported: |
| 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, |
| ) |
| settings["preview_interval"] = int(preview_interval) |
| if custom_width: |
| settings.update({"width": custom_width, "height": custom_height}) |
| if reference_path: |
| images = backend.generate_reference_images( |
| str(model), str(reference_path), reference_strength=strength, **settings |
| ) |
| else: |
| images = backend.generate_images(str(model), **settings) |
| image = images[0] |
| destination = output / f"{timestamp}_{context.job_id}_{sampler}_seed_{current_seed}.png" |
| image.save(destination, format="PNG") |
| except Exception as exc: |
| raise ToolExecutionError(f"DDPM 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": sampler, |
| "aspect_ratio": aspect_ratio, |
| "width": custom_width or None, |
| "height": custom_height or None, |
| "reference_image": str(reference_path) if reference_path else "", |
| "reference_strength": strength if reference_path else None, |
| "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": "ddpm", |
| } |
| ], |
| } |
|
|