Pure v2v anime->real conditioning for the Bernini-R anime2real LoRA (trained without reference). Mirrors training: guide -> source_id 1 on both positive and negative, empty latent -> source_id 0, no source_id 2 and no amplify_reference (no reference to amplify; text-only CFG). Reuses bernini_patches; registered in __init__. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
129 lines
5.0 KiB
Python
129 lines
5.0 KiB
Python
"""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
|
|
|
|
# Apply the bundled WanModel patch on import (idempotent; no-ops if core already
|
|
# supports Bernini context_latents).
|
|
try:
|
|
from .bernini_patches import apply_bernini_patches
|
|
except Exception: # pragma: no cover - flat import fallback
|
|
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):
|
|
# Wan VAE compresses time by 4 -> 4k+1 frame counts.
|
|
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) # source_id 1
|
|
|
|
# No reference: guide is the only context, on both sides. Plain text CFG.
|
|
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",
|
|
}
|