| """Anime2Real Bernini Conditioning — standalone node for the BFS pack.
|
|
|
| Pure v2v (NO reference). Mirrors the anime2real training case exactly: the LoRA
|
| was trained with context `[src(source_id 1) | noisy(source_id 0)]` and the
|
| reference (source_id 2) OMITTED. Feeding a source_id 2 token here (as the
|
| head-swap node does) would be out-of-distribution, so this node never builds one.
|
|
|
| guide video -> context[0] -> source_id 1 (anime, style/content kept as canvas)
|
| denoised output (source_id 0) = the empty latent (photorealistic result)
|
|
|
| There is no `amplify_reference` knob: that trick amplifies a reference identity
|
| via CFG, and here there is no reference. Only plain text CFG (the fixed
|
| "photorealistic" caption) drives the style shift, so the guide goes on both the
|
| positive and negative conditioning.
|
|
|
| Bundles the same WanModel patch (`bernini_patches.py`) so it drops in without a
|
| dependency on ComfyUI-RH-Bernini.
|
| """
|
| import logging
|
|
|
| import torch
|
|
|
| import comfy.model_management
|
| import comfy.utils
|
| import node_helpers
|
|
|
|
|
|
|
| try:
|
| from .bernini_patches import apply_bernini_patches
|
| except Exception:
|
| from bernini_patches import apply_bernini_patches
|
|
|
| log = logging.getLogger("BFS.Anime2RealBernini")
|
| try:
|
| apply_bernini_patches()
|
| except Exception as e:
|
| log.warning("Anime2Real-Bernini: WanModel patch not applied: %s", e)
|
|
|
| STRIDE = 16
|
|
|
|
|
| def _snap(v, stride=STRIDE):
|
| return max(stride, round(v / stride) * stride)
|
|
|
|
|
| def _snap_frames(n):
|
|
|
| return max(1, ((max(1, int(n)) - 1) // 4) * 4 + 1)
|
|
|
|
|
| def _encode_native(vae, frames):
|
| """VAE-encode [T,H,W,C] at native size, snapped to the /16 grid."""
|
| h, w = frames.shape[1], frames.shape[2]
|
| nh, nw = _snap(h), _snap(w)
|
| if (nh, nw) != (h, w):
|
| frames = comfy.utils.common_upscale(
|
| frames[:, :, :, :3].movedim(-1, 1), nw, nh, "area", "disabled"
|
| ).movedim(1, -1)
|
| return vae.encode(frames[:, :, :, :3]), nh, nw
|
|
|
|
|
| def _tokens(lat):
|
| _, _, t, h, w = lat.shape
|
| return t * (h // 2) * (w // 2)
|
|
|
|
|
| class Anime2RealBerniniConditioning:
|
| @classmethod
|
| def INPUT_TYPES(cls):
|
| return {
|
| "required": {
|
| "positive": ("CONDITIONING",),
|
| "negative": ("CONDITIONING",),
|
| "vae": ("VAE",),
|
| "guide_video": ("IMAGE", {"tooltip": "Anime source video (style/content kept). Output size = this."}),
|
| "length": ("INT", {"default": 41, "min": 1, "max": 1000, "step": 4,
|
| "tooltip": "Frame count (snapped to 4k+1). Trained at 41."}),
|
| }
|
| }
|
|
|
| RETURN_TYPES = ("CONDITIONING", "CONDITIONING", "LATENT", "STRING")
|
| RETURN_NAMES = ("positive", "negative", "latent", "debug")
|
| FUNCTION = "execute"
|
| CATEGORY = "BFS/video"
|
| DESCRIPTION = (
|
| "Pure v2v anime->real conditioning for the Bernini-R anime2real LoRA.\n"
|
| "guide_video -> source_id 1 (anime canvas) | output -> source_id 0 (photoreal). NO reference.\n\n"
|
| "The guide is placed on BOTH positive and negative, so CFG only amplifies the text prompt "
|
| "(the photorealistic style), not a reference. Use a moderate CFG; the guide keeps motion/layout.\n\n"
|
| "Do NOT use the head-swap node with this LoRA: it injects a source_id 2 token the anime2real "
|
| "LoRA never saw in training."
|
| )
|
|
|
| def execute(self, positive, negative, vae, guide_video, length):
|
| length = _snap_frames(length)
|
| guide = guide_video[:length]
|
|
|
| guide_lat, gh, gw = _encode_native(vae, guide)
|
|
|
|
|
| ctx = [guide_lat]
|
| positive = node_helpers.conditioning_set_values(positive, {"context_latents": ctx})
|
| negative = node_helpers.conditioning_set_values(negative, {"context_latents": ctx})
|
|
|
| latent = torch.zeros(
|
| [1, 16, ((length - 1) // 4) + 1, gh // 8, gw // 8],
|
| device=comfy.model_management.intermediate_device(),
|
| )
|
|
|
| dbg = (
|
| "=== Anime2Real Bernini Conditioning ===\n"
|
| f"OUTPUT (source_id 0 / target): {gw}x{gh}, {length}f -> latent {tuple(latent.shape)}\n"
|
| f"GUIDE (source_id 1, kept): native {guide_video.shape[2]}x{guide_video.shape[1]} -> {gw}x{gh} "
|
| f"-> latent {tuple(guide_lat.shape)} (~{_tokens(guide_lat)} tokens)\n"
|
| "reference: NONE (pure v2v) | guide on positive AND negative (text-only CFG)\n"
|
| "Trained at 41f/640; staying near that resolution is safest."
|
| )
|
| log.info("\n" + dbg)
|
| return (positive, negative, {"samples": latent}, dbg)
|
|
|
|
|
| NODE_CLASS_MAPPINGS = {
|
| "BFSAnime2RealBerniniConditioning": Anime2RealBerniniConditioning,
|
| }
|
| NODE_DISPLAY_NAME_MAPPINGS = {
|
| "BFSAnime2RealBerniniConditioning": "Anime2Real Bernini Conditioning",
|
| }
|
|
|