Files

220 lines
7.9 KiB
Python

"""The two ComfyUI nodes exposed by Better NormalCrafter Wrapper."""
import comfy.model_management
import comfy.utils
from .normalcrafter_clean.inference import estimate_progress_steps, run_inference
from .normalcrafter_clean.model import (
base_model_sources,
load_models,
normalcrafter_sources,
)
class NormalCrafterLoader:
"""Choose and load the NormalCrafter and SVD model components."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"normalcrafter_model": (
normalcrafter_sources(),
{
"default": "HF: Yanrui95/NormalCrafter",
"tooltip": (
"NormalCrafter model containing the UNet and VAE. "
"Local models are discovered from every ComfyUI "
"'normalcrafter' model path, including paths from "
"extra_model_paths.yaml. Selecting an HF entry downloads "
"the required files into a registered normalcrafter path."
),
},
),
"base_model": (
base_model_sources(),
{
"default": "HF: stabilityai/stable-video-diffusion-img2vid-xt",
"tooltip": (
"Stable Video Diffusion base components used by "
"NormalCrafter: CLIP image encoder, feature extractor "
"and scheduler. Local models are discovered from the "
"same registered ComfyUI 'normalcrafter' paths."
),
},
),
}
}
RETURN_TYPES = ("NORMALCRAFTER_MODEL",)
RETURN_NAMES = ("model",)
OUTPUT_TOOLTIPS = (
"Loaded NormalCrafter model bundle. Heavy components stay on CPU until inference needs them.",
)
FUNCTION = "load"
CATEGORY = "NormalCrafter"
DESCRIPTION = (
"Loads the NormalCrafter UNet/VAE and the required Stable Video Diffusion "
"base components. Model discovery follows ComfyUI's registered "
"'normalcrafter' paths, including extra_model_paths.yaml."
)
def load(self, normalcrafter_model: str, base_model: str):
device = comfy.model_management.get_torch_device()
return (
load_models(
device=device,
normalcrafter_source=normalcrafter_model,
base_model_source=base_model,
),
)
class NormalCrafterGenerate:
"""Run NormalCrafter with explicit temporal and VRAM controls."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"model": (
"NORMALCRAFTER_MODEL",
{
"tooltip": (
"NormalCrafter model bundle from the NormalCrafter - Load node."
)
},
),
"images": (
"IMAGE",
{
"tooltip": (
"Input image batch or video frames in temporal order. "
"The output contains one normal map per input frame."
)
},
),
"max_resolution": (
"INT",
{
"default": 1024,
"min": 256,
"max": 2048,
"step": 64,
"tooltip": (
"Maximum length of the input's longest side during inference. "
"Images are never enlarged. Lower values reduce VRAM use and "
"increase speed, at the cost of spatial detail. Output is "
"resized back to the original input resolution."
),
},
),
"window_size": (
"INT",
{
"default": 14,
"min": 2,
"max": 64,
"step": 1,
"tooltip": (
"Number of consecutive frames processed by the temporal UNet "
"at once. Larger windows provide more temporal context but use "
"more VRAM. NormalCrafter's released default is 14."
),
},
),
"step_size": (
"INT",
{
"default": 10,
"min": 1,
"max": 64,
"step": 1,
"tooltip": (
"How many frames the temporal window advances each pass. Must "
"be <= window_size. Smaller values create more overlap and "
"usually smoother temporal blending, but require more UNet "
"passes. With window_size 14, the released default is 10 "
"(4-frame regular overlap)."
),
},
),
"chunk_size": (
"INT",
{
"default": 7,
"min": 1,
"max": 64,
"step": 1,
"tooltip": (
"Batch size used for CLIP encoding, VAE encoding and VAE "
"decoding. Lower values reduce peak VRAM; higher values can "
"improve throughput if enough VRAM is available. This does "
"not change the temporal UNet window size."
),
},
),
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("normal_maps",)
OUTPUT_TOOLTIPS = (
"Per-frame RGB normal maps in ComfyUI IMAGE format, restored to the input resolution.",
)
FUNCTION = "generate"
CATEGORY = "NormalCrafter"
DESCRIPTION = (
"Runs NormalCrafter in four stages: CLIP encode, VAE encode, temporal UNet "
"inference and VAE decode. Full-video intermediates stay on CPU and only "
"the active component/chunk/window is moved to the inference device."
)
def generate(
self,
model,
images,
max_resolution: int,
window_size: int,
step_size: int,
chunk_size: int,
):
device = comfy.model_management.get_torch_device()
total_steps = estimate_progress_steps(
int(images.shape[0]),
window_size=window_size,
step_size=step_size,
chunk_size=chunk_size,
)
progress_bar = comfy.utils.ProgressBar(total_steps)
def progress() -> None:
progress_bar.update(1)
output = run_inference(
model,
images,
device=device,
max_resolution=max_resolution,
window_size=window_size,
step_size=step_size,
chunk_size=chunk_size,
progress=progress,
)
# The model lifecycle already returned all modules to CPU. This only lets
# ComfyUI release allocator blocks that are now unused.
comfy.model_management.soft_empty_cache()
return (output,)
NODE_CLASS_MAPPINGS = {
"NormalCrafterLoader": NormalCrafterLoader,
"NormalCrafterGenerate": NormalCrafterGenerate,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"NormalCrafterLoader": "NormalCrafter - Load",
"NormalCrafterGenerate": "NormalCrafter - Generate Normals",
}