Files

204 lines
8.1 KiB
Python

import torch
import logging
from tqdm import tqdm
from .prompt_relay import (
get_raw_tokenizer,
map_token_indices,
distribute_segment_lengths,
build_segments,
create_mask_fn,
)
log = logging.getLogger(__name__)
VAE_STRIDE_T = 4
PATCH_SIZE_H = 2
PATCH_SIZE_W = 2
def calc_tokens_per_frame(height, width):
return (height // 8 // PATCH_SIZE_H) * (width // 8 // PATCH_SIZE_W)
def calc_latent_frames(frame_num):
return (frame_num - 1) // VAE_STRIDE_T + 1
class RogoAI_PromptRelayWan:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"model": ("WANVIDEOMODEL",),
"t5": ("WANTEXTENCODER",),
"global_prompt": ("STRING", {
"multiline": True,
"default": "A young woman with long black wavy hair and blue headphones, sitting at a PC desk, realistic cinematic lighting.",
}),
"negative_prompt": ("STRING", {
"multiline": True,
"default": "blurry, low quality, distorted face",
}),
"local_prompt_1": ("STRING", {"multiline": True, "default": "She faces the camera directly with a calm expression."}),
"local_prompt_2": ("STRING", {"multiline": True, "default": "She turns her head away, looking at the monitor."}),
"local_prompt_3": ("STRING", {"multiline": True, "default": "She turns back to the camera with a slight smile."}),
"segment_lengths": ("STRING", {"default": "7,7,7"}),
"frame_num": ("INT", {"default": 81, "min": 9, "max": 201, "step": 4}),
"width": ("INT", {"default": 832, "min": 64, "max": 2048, "step": 8}),
"height": ("INT", {"default": 480, "min": 64, "max": 2048, "step": 8}),
},
"optional": {
"local_prompt_4": ("STRING", {"multiline": True, "default": ""}),
"local_prompt_5": ("STRING", {"multiline": True, "default": ""}),
"local_prompt_6": ("STRING", {"multiline": True, "default": ""}),
"epsilon": ("FLOAT", {"default": 0.001, "min": 0.0001, "max": 0.1, "step": 0.0001}),
"force_offload": ("BOOLEAN", {"default": True}),
}
}
RETURN_TYPES = ("WANVIDEOMODEL", "WANVIDEOTEXTEMBEDS",)
RETURN_NAMES = ("model", "text_embeds",)
FUNCTION = "apply"
CATEGORY = "RogoAI/PromptRelay"
def apply(self, model, t5,
global_prompt, negative_prompt,
local_prompt_1, local_prompt_2, local_prompt_3,
segment_lengths, frame_num, width, height,
local_prompt_4="", local_prompt_5="", local_prompt_6="",
epsilon=0.001, force_offload=True):
from comfy import model_management as mm
from accelerate.utils import set_module_tensor_to_device
# 1. ローカルプロンプトをまとめる
local_prompts = [p for p in [
local_prompt_1, local_prompt_2, local_prompt_3,
local_prompt_4, local_prompt_5, local_prompt_6
] if p.strip()]
if not local_prompts:
raise ValueError("[RogoAI PromptRelay] local_promptが1つも入力されていません")
# 2. segment_lengthsを解析
latent_frames = calc_latent_frames(frame_num)
seg_lengths_raw = [int(x.strip()) for x in segment_lengths.split(",") if x.strip()]
effective_lengths = distribute_segment_lengths(
len(local_prompts), latent_frames, seg_lengths_raw or None)
log.info("[RogoAI PromptRelay] latent_frames=%d, segments=%s", latent_frames, effective_lengths)
# 3. T5エンコーダーからトークナイザーを取得
encoder = t5["model"]
dtype = t5["dtype"]
raw_tokenizer = None
for attr_name in dir(encoder):
if attr_name.startswith("_"):
continue
inner = getattr(encoder, attr_name, None)
if inner is not None and hasattr(inner, "tokenizer"):
raw_tokenizer = inner.tokenizer
break
if raw_tokenizer is None and hasattr(encoder, "tokenizer"):
raw_tokenizer = encoder.tokenizer
if raw_tokenizer is None:
raise RuntimeError("[RogoAI PromptRelay] T5のトークナイザーが見つかりません")
# 4. full_promptとtoken_rangesを取得
full_prompt, token_ranges = map_token_indices(raw_tokenizer, global_prompt, local_prompts)
log.info("[RogoAI PromptRelay] full_prompt=%d chars, %d segments", len(full_prompt), len(local_prompts))
# 5. セグメントメタデータを構築
q_token_idx = build_segments(token_ranges, effective_lengths, epsilon=epsilon)
# 6. mask_fnを生成してmodel.clone()でパッチ
tokens_per_frame = calc_tokens_per_frame(height, width)
mask_fn = create_mask_fn(q_token_idx, tokens_per_frame, latent_frames)
patcher = model.clone()
if "transformer_options" not in patcher.model_options:
patcher.model_options["transformer_options"] = {}
patcher.model_options["transformer_options"]["prompt_relay_mask_fn"] = mask_fn
log.info("[RogoAI PromptRelay] mask_fn patched via model.clone()")
# 7. T5でテキストエンコード(WanVideoTextEncodeと同じ方式)
device = mm.get_torch_device()
offload_device = mm.unet_offload_device()
if encoder.quantization == "fp8_e4m3fn":
cast_dtype = torch.float8_e4m3fn
else:
cast_dtype = encoder.dtype
params_to_keep = {"norm", "pos_embedding", "token_embedding"}
if hasattr(encoder, "state_dict"):
model_state_dict = encoder.state_dict
else:
model_state_dict = encoder.model.state_dict()
params_list = list(encoder.model.named_parameters())
pbar = tqdm(params_list, desc="[PromptRelay] Loading T5", leave=True)
for name, param in pbar:
dtype_to_use = dtype if any(k in name for k in params_to_keep) else cast_dtype
value = model_state_dict[name]
set_module_tensor_to_device(encoder.model, name, device=device, dtype=dtype_to_use, value=value)
del model_state_dict
if hasattr(encoder, "state_dict"):
del encoder.state_dict
mm.soft_empty_cache()
with torch.no_grad():
context = encoder([full_prompt], device)
context_null = encoder([negative_prompt], device)
if force_offload:
params_list2 = list(encoder.model.named_parameters())
for name, param in params_list2:
set_module_tensor_to_device(encoder.model, name, device=offload_device)
mm.soft_empty_cache()
text_embeds = {
"prompt_embeds": context,
"negative_prompt_embeds": context_null,
}
return (patcher, text_embeds,)
class RogoAI_PromptRelaySegCalc:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"frame_num": ("INT", {"default": 81, "min": 9, "max": 201, "step": 4}),
"num_segments": ("INT", {"default": 3, "min": 1, "max": 6}),
}
}
RETURN_TYPES = ("STRING", "INT",)
RETURN_NAMES = ("segment_lengths", "latent_frames",)
FUNCTION = "calc"
CATEGORY = "RogoAI/PromptRelay"
def calc(self, frame_num, num_segments):
latent_frames = calc_latent_frames(frame_num)
step = -(-latent_frames // num_segments)
lengths = []
cursor = 0
for _ in range(num_segments):
end = min(cursor + step, latent_frames)
lengths.append(max(end - cursor, 0))
cursor = end
result = ",".join(str(l) for l in lengths)
return (result, latent_frames,)
NODE_CLASS_MAPPINGS = {
"RogoAI_PromptRelayWan": RogoAI_PromptRelayWan,
"RogoAI_PromptRelaySegCalc": RogoAI_PromptRelaySegCalc,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"RogoAI_PromptRelayWan": "RogoAI Prompt Relay (Wan2.2 I2V)",
"RogoAI_PromptRelaySegCalc": "RogoAI Prompt Relay Seg Calc",
}