220 lines
7.9 KiB
Python
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",
|
|
}
|