import logging import math import torch logger = logging.getLogger(__name__) import comfy.latent_formats import comfy.model_base import comfy.model_management as mm from comfy.utils import common_upscale from .diffusers_helper.memory import move_model_to_device_with_memory_preservation from .diffusers_helper.pipelines.k_diffusion_hunyuan import sample_hunyuan from .diffusers_helper.utils import crop_or_pad_yield_mask from .nodes import HyVideoModel, HyVideoModelConfig vae_scaling_factor = 0.476986 class FramePackSingleFrameSampler: @classmethod def INPUT_TYPES(s): return { "required": { "model": ("FramePackMODEL",), "positive": ("CONDITIONING",), "negative": ("CONDITIONING",), "start_latent": ( "LATENT", {"tooltip": "init Latents to use for image2image"}, ), "steps": ("INT", {"default": 30, "min": 1}), "use_teacache": ( "BOOLEAN", {"default": True, "tooltip": "Use teacache for faster sampling."}, ), "teacache_rel_l1_thresh": ( "FLOAT", { "default": 0.15, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "The threshold for the relative L1 loss.", }, ), "cfg": ( "FLOAT", {"default": 1.0, "min": 0.0, "max": 30.0, "step": 0.01}, ), "guidance_scale": ( "FLOAT", {"default": 10.0, "min": 0.0, "max": 32.0, "step": 0.01}, ), "shift": ( "FLOAT", {"default": 0.0, "min": 0.0, "max": 1000.0, "step": 0.01}, ), "seed": ("INT", {"default": 0, "min": 0, "max": 0xFFFFFFFFFFFFFFFF}), "latent_window_size": ( "INT", { "default": 9, "min": 1, "max": 33, "step": 1, "tooltip": "The size of the latent window to use for sampling.", }, ), "gpu_memory_preservation": ( "FLOAT", { "default": 6.0, "min": 0.0, "max": 128.0, "step": 0.1, "tooltip": "The amount of GPU memory to preserve.", }, ), "sampler": (["unipc_bh1", "unipc_bh2"], {"default": "unipc_bh1"}), "use_kisekaeichi": ( "BOOLEAN", { "default": False, "tooltip": "Enable Kisekaeichi mode for style transfer", }, ), }, "optional": { "image_embeds": ("CLIP_VISION_OUTPUT",), "initial_samples": ( "LATENT", {"tooltip": "init Latents to use for image2image variation"}, ), "denoise_strength": ( "FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}, ), "reference_latent": ( "LATENT", {"tooltip": "Reference image latent for kisekaeichi mode"}, ), "reference_image_embeds": ( "CLIP_VISION_OUTPUT", {"tooltip": "Reference image CLIP embeds for kisekaeichi mode"}, ), "target_index": ( "INT", { "default": 1, "min": 0, "max": 8, "step": 1, "tooltip": "Target index for kisekaeichi mode (recommended: 1)", }, ), "history_index": ( "INT", { "default": 13, "min": 0, "max": 16, "step": 1, "tooltip": "History index for kisekaeichi mode (recommended: 13)", }, ), "input_mask": ( "MASK", {"tooltip": "Input mask for selective application"}, ), "reference_mask": ( "MASK", {"tooltip": "Reference mask for selective features"}, ), }, } RETURN_TYPES = ("LATENT",) RETURN_NAMES = ("samples",) FUNCTION = "process" CATEGORY = "FramePackWrapper" DESCRIPTION = "Single frame sampler with Kisekaeichi (style transfer) support" def process( self, model, shift, positive, negative, latent_window_size, use_teacache, teacache_rel_l1_thresh, steps, cfg, guidance_scale, seed, sampler, gpu_memory_preservation, start_latent=None, image_embeds=None, initial_samples=None, denoise_strength=1.0, use_kisekaeichi=False, reference_latent=None, reference_image_embeds=None, target_index=1, history_index=13, input_mask=None, reference_mask=None, ): print("=== 1フレーム推論モード ===") if use_kisekaeichi: print("Kisekaeichi(着せ替え)モード有効") print(f"target_index: {target_index}, history_index: {history_index}") transformer = model["transformer"] base_dtype = model["dtype"] device = mm.get_torch_device() offload_device = mm.unet_offload_device() mm.unload_all_models() mm.cleanup_models() mm.soft_empty_cache() # Latent処理 if start_latent is not None: start_latent = start_latent["samples"] * vae_scaling_factor if initial_samples is not None: initial_samples = initial_samples["samples"] * vae_scaling_factor if use_kisekaeichi and reference_latent is not None: reference_latent = reference_latent["samples"] * vae_scaling_factor print(f"参照画像latent: {reference_latent.shape}") print("start_latent", start_latent.shape) B, C, T, H, W = start_latent.shape # 画像エンベッディング処理 if image_embeds is not None: start_image_encoder_last_hidden_state = image_embeds[ "last_hidden_state" ].to(device, base_dtype) else: start_image_encoder_last_hidden_state = None if use_kisekaeichi and reference_image_embeds is not None: reference_image_encoder_last_hidden_state = reference_image_embeds[ "last_hidden_state" ].to(device, base_dtype) print("参照画像のCLIP embeddingを設定しました") else: reference_image_encoder_last_hidden_state = None # テキストエンベッディング処理 llama_vec = positive[0][0].to(device, base_dtype) clip_l_pooler = positive[0][1]["pooled_output"].to(device, base_dtype) if not math.isclose(cfg, 1.0): llama_vec_n = negative[0][0].to(device, base_dtype) clip_l_pooler_n = negative[0][1]["pooled_output"].to(device, base_dtype) else: llama_vec_n = torch.zeros_like(llama_vec, device=device) clip_l_pooler_n = torch.zeros_like(clip_l_pooler, device=device) llama_vec, llama_attention_mask = crop_or_pad_yield_mask(llama_vec, length=512) llama_vec_n, llama_attention_mask_n = crop_or_pad_yield_mask( llama_vec_n, length=512 ) # シード設定 rnd = torch.Generator("cpu").manual_seed(seed) # === 一つ目のコードに完全準拠した設定 === # 1フレームモード固定設定 sample_num_frames = 1 total_latent_sections = 1 latent_padding = 0 latent_padding_size = latent_padding * latent_window_size # 0 # 一つ目のコードと同じインデックス構造 indices = torch.arange( 0, sum([1, latent_padding_size, latent_window_size, 1, 2, 16]) ).unsqueeze(0) split_sizes = [1, latent_padding_size, latent_window_size, 1, 2, 16] # latent_padding_sizeが0の場合の分割(一つ目のコードと完全同一) if latent_padding_size == 0: clean_latent_indices_pre = indices[:, 0:1] latent_indices = indices[:, 1 : 1 + latent_window_size] clean_latent_indices_post = indices[ :, 1 + latent_window_size : 2 + latent_window_size ] clean_latent_2x_indices = indices[ :, 2 + latent_window_size : 4 + latent_window_size ] clean_latent_4x_indices = indices[ :, 4 + latent_window_size : 20 + latent_window_size ] blank_indices = torch.empty((1, 0), dtype=torch.long) else: ( clean_latent_indices_pre, blank_indices, latent_indices, clean_latent_indices_post, clean_latent_2x_indices, clean_latent_4x_indices, ) = indices.split(split_sizes, dim=1) # 一つ目のコードの重要な処理:1フレームモード時のone_frame_inference処理 if sample_num_frames == 1: if use_kisekaeichi and reference_latent is not None: print("=== Kisekaeichi モード設定(完全版) ===") # 一つ目のコードのone_frame_inference処理を完全再現 one_frame_inference = set() one_frame_inference.add(f"target_index={target_index}") one_frame_inference.add(f"history_index={history_index}") # 公式実装に従った処理 latent_indices = indices[:, -1:] # デフォルトは最後のフレーム # パラメータ解析と処理(一つ目のコードと同じ) for one_frame_param in one_frame_inference: if one_frame_param.startswith("target_index="): target_idx = int(one_frame_param.split("=")[1]) latent_indices[:, 0] = target_idx print(f"latent_indices設定: target_index={target_idx}") elif one_frame_param.startswith("history_index="): history_idx = int(one_frame_param.split("=")[1]) clean_latent_indices_post[:, 0] = history_idx print( f"clean_latent_indices_post設定: history_index={history_idx}" ) # history_latentsのダミーを作成(一つ目のコードと同じ構造) history_latents = torch.zeros( size=(1, 16, 1 + 2 + 16, H, W), dtype=torch.float32, device="cpu" ) # clean_latents_preの設定(入力画像) clean_latents_pre = start_latent.to(history_latents.dtype).to( history_latents.device ) if len(clean_latents_pre.shape) < 5: clean_latents_pre = clean_latents_pre.unsqueeze(2) # マスクの適用(入力画像) if input_mask is not None: print("入力画像マスクを適用中...") try: height_latent, width_latent = clean_latents_pre.shape[-2:] if isinstance(input_mask, torch.Tensor): input_mask_tensor = input_mask else: input_mask_tensor = torch.from_numpy(input_mask) input_mask_resized = ( common_upscale( input_mask_tensor.unsqueeze(0).unsqueeze(0), width_latent, height_latent, "bilinear", "center", ) .squeeze(0) .squeeze(0) ) input_mask_resized = input_mask_resized.to( clean_latents_pre.device )[None, None, None, :, :] clean_latents_pre = clean_latents_pre * input_mask_resized print("入力画像マスクを適用しました") except Exception as e: print(f"入力マスク適用エラー: {e}") # clean_latents_postの設定(参照画像) clean_latents_post = ( reference_latent[:, :, 0:1, :, :] .to(history_latents.dtype) .to(history_latents.device) ) # マスクの適用(参照画像) if reference_mask is not None: print("参照画像マスクを適用中...") try: height_latent, width_latent = clean_latents_post.shape[-2:] if isinstance(reference_mask, torch.Tensor): reference_mask_tensor = reference_mask else: reference_mask_tensor = torch.from_numpy(reference_mask) reference_mask_resized = ( common_upscale( reference_mask_tensor.unsqueeze(0).unsqueeze(0), width_latent, height_latent, "bilinear", "center", ) .squeeze(0) .squeeze(0) ) reference_mask_resized = reference_mask_resized.to( clean_latents_post.device )[None, None, None, :, :] clean_latents_post = clean_latents_post * reference_mask_resized print("参照画像マスクを適用しました") except Exception as e: print(f"参照マスク適用エラー: {e}") # clean_latentsを結合 clean_latents = torch.cat( [clean_latents_pre, clean_latents_post], dim=2 ) # clean_latent_indicesを結合 clean_latent_indices = torch.cat( [clean_latent_indices_pre, clean_latent_indices_post], dim=1 ) clean_latents_2x_param = None clean_latents_4x_param = None clean_latent_2x_indices = None clean_latent_4x_indices = None # 2x, 4x latentsの設定も無効化 clean_latents_2x = None clean_latents_4x = None print("Kisekaeichi: 2x/4xインデックスを無効化しました") # 画像エンベッディングの処理(両方を活用) if ( reference_image_encoder_last_hidden_state is not None and start_image_encoder_last_hidden_state is not None ): # 重み付き平均で統合 ref_weight = 0.3 # 参照画像の重み input_weight = 1.0 - ref_weight image_encoder_last_hidden_state = ( start_image_encoder_last_hidden_state * input_weight + reference_image_encoder_last_hidden_state * ref_weight ) print( f"画像エンベッディングを統合 (入力:{input_weight:.2f}, 参照:{ref_weight:.2f})" ) elif reference_image_encoder_last_hidden_state is not None: image_encoder_last_hidden_state = ( reference_image_encoder_last_hidden_state ) else: image_encoder_last_hidden_state = ( start_image_encoder_last_hidden_state ) print(f"Kisekaeichi設定完了:") print(f" - clean_latents.shape: {clean_latents.shape} (入力+参照)") print(f" - latent_indices: {latent_indices}") print(f" - clean_latent_indices: {clean_latent_indices}") print(f" - sample_num_frames: {sample_num_frames}") print(f" - 2x/4x無効化: True") else: # 通常モード(参照画像なし) all_indices = torch.arange(0, latent_window_size).unsqueeze(0) latent_indices = all_indices[:, -1:] clean_latents_pre = start_latent.to(torch.float32).cpu() if len(clean_latents_pre.shape) < 5: clean_latents_pre = clean_latents_pre.unsqueeze(2) clean_latents_post = torch.zeros_like(clean_latents_pre) clean_latents = torch.cat( [clean_latents_pre, clean_latents_post], dim=2 ) clean_latent_indices = torch.cat( [clean_latent_indices_pre, clean_latent_indices_post], dim=1 ) # 通常モードでのインデックス調整 clean_latent_indices = torch.tensor( [[0]], dtype=clean_latent_indices.dtype, device=clean_latent_indices.device, ) clean_latents = clean_latents[:, :, :1, :, :] clean_latents_2x_param = None clean_latents_4x_param = None clean_latent_2x_indices = None clean_latent_4x_indices = None # 2x, 4x latentsの設定も無効化 clean_latents_2x = None clean_latents_4x = None print("Kisekaeichi: 2x/4xインデックスを無効化しました") image_encoder_last_hidden_state = start_image_encoder_last_hidden_state print("通常モード設定:") print(f" - clean_latents.shape: {clean_latents.shape}") print(f" - latent_indices: {latent_indices}") print(f" - clean_latent_indices: {clean_latent_indices}") # 初期サンプルの処理 input_init_latents = None if initial_samples is not None: input_init_latents = initial_samples[:, :, 0:1, :, :].to(device) print("初期サンプルを設定しました") # ComfyUI用のセットアップ comfy_model = HyVideoModel( HyVideoModelConfig(base_dtype), model_type=comfy.model_base.ModelType.FLOW, device=device, ) patcher = comfy.model_patcher.ModelPatcher( comfy_model, device, torch.device("cpu") ) from latent_preview import prepare_callback callback = prepare_callback(patcher, steps) # モデルをGPUに移動 move_model_to_device_with_memory_preservation( transformer, target_device=device, preserved_memory_gb=gpu_memory_preservation, ) # TeaCacheの設定 if use_teacache: transformer.initialize_teacache( enable_teacache=True, num_steps=steps, rel_l1_thresh=teacache_rel_l1_thresh, ) else: transformer.initialize_teacache(enable_teacache=False) print("=== サンプリング開始 ===") print(f"sample_num_frames: {sample_num_frames}") print(f"clean_latents使用フレーム数: {clean_latents.shape[2]}") print(f"clean_latent_2x_indices: {clean_latent_2x_indices}") print(f"clean_latent_4x_indices: {clean_latent_4x_indices}") with torch.autocast( device_type=mm.get_autocast_device(device), dtype=base_dtype, enabled=True ): generated_latents = sample_hunyuan( transformer=transformer, sampler=sampler, initial_latent=input_init_latents, strength=denoise_strength, width=W * 8, height=H * 8, frames=sample_num_frames, # 1フレーム固定 real_guidance_scale=cfg, distilled_guidance_scale=guidance_scale, guidance_rescale=0, shift=shift if shift != 0 else None, num_inference_steps=steps, generator=rnd, prompt_embeds=llama_vec, prompt_embeds_mask=llama_attention_mask, prompt_poolers=clip_l_pooler, negative_prompt_embeds=llama_vec_n, negative_prompt_embeds_mask=llama_attention_mask_n, negative_prompt_poolers=clip_l_pooler_n, device=device, dtype=base_dtype, image_embeddings=image_encoder_last_hidden_state, latent_indices=latent_indices, clean_latents=clean_latents, clean_latent_indices=clean_latent_indices, clean_latents_2x=clean_latents_2x, clean_latent_2x_indices=clean_latent_2x_indices, clean_latents_4x=clean_latents_4x, clean_latent_4x_indices=clean_latent_4x_indices, callback=callback, ) # クリーンアップ transformer.to(offload_device) mm.soft_empty_cache() # 出力処理 mode_info = ( "Kisekaeichi(latent+エンベッディング)" if use_kisekaeichi else "通常" ) print( f"=== 1フレーム生成完了 ({mode_info}モード): {generated_latents.shape} ===" ) return ({"samples": generated_latents / vae_scaling_factor},) NODE_CLASS_MAPPINGS = { "FramePackSingleFrameSampler": FramePackSingleFrameSampler, } NODE_DISPLAY_NAME_MAPPINGS = { "FramePackSingleFrameSampler": "FramePack Single Frame Sampler", }