This commit is contained in:
Hiroaki Ogasawara
2025-08-08 12:54:10 +09:00
parent 5bef9b605c
commit fa6ac3bc79
9 changed files with 4498 additions and 1065 deletions
+58
View File
@@ -57,9 +57,67 @@ pip install -r requirements.txt
[MIT License](LICENSE)
## 更新履歴
### v2.0.0 - musubi-tuner完全互換対応 (2025-01-XX)
複数リファレンス画像使用時の推論結果の一貫性を改善するため、**musubi-tuner仕様との完全互換性**を実現しました。
#### 主要な変更点
**1. エンベッディング統合方式の改善**
- ❌ 従来:重み付き平均による統合処理(入力画像70%、参照画像30%)
- ✅ **新版:** musubi-tuner互換の処理方式(最初の参照画像エンベッディングを使用)
**2. Latent結合構造の統一**
- ❌ 従来:入力画像と参照画像を分離管理後に結合
- ✅ **新版:** musubi-tuner仕様のcontrol_latents直接結合
```python
control_latents = [入力画像, 参照画像1, 参照画像2, ..., ゼロlatent]
clean_latents = torch.cat(control_latents, dim=2)
```
**3. マスク適用タイミングの最適化**
- ❌ 従来:latent結合前の個別適用
- ✅ **新版:** clean_latents生成後にマスク適用(musubi-tuner仕様)
**4. インデックス設定の動的処理**
- ❌ 従来:固定的なclean_latent_indices設定
- ✅ **新版:** control_indicesパラメータの動的適用
```python
# control_index="0;7;8;9;10" → clean_latent_indices = [0, 7, 8, 9, 10]
while i < len(control_indices_list) and i < clean_latent_indices.shape[1]:
clean_latent_indices[:, i] = control_indices_list[i]
```
**5. latent_indicesの初期化改善**
- ❌ 従来:ComfyUI独自の初期化方式
- ✅ **新版:** musubi-tuner仕様の初期化
```python
latent_indices = torch.zeros((1, 1), dtype=torch.int64)
latent_indices[:, 0] = latent_window_size # デフォルト値
latent_indices[:, 0] = target_index # パラメータ適用
```
#### 期待される効果
- **推論結果の一貫性向上**: 同じリファレンス画像・同じパラメータでmusubi-tunerと完全に同じ結果を生成
- **複数リファレンス処理の安定化**: より正確なインデックス管理による安定した品質
- **パラメータ互換性**: musubi-tunerのcontrol_indexとtarget_indexパラメータが正しく動作
#### 技術的詳細
この更新により、以下の処理フローがmusubi-tunerと完全に一致します:
1. **制御画像処理**: `--control_image_path`で指定された複数画像の順次処理
2. **インデックス管理**: `--one_frame_inference="control_index=0;7;8;9;10,target_index=5"`の動的適用
3. **エンベッディング処理**: セクション毎の個別処理を模擬した実装
4. **マスク適用**: clean_latents構築後の統一的なマスク処理
## クレジット
- [FramePack](https://github.com/lllyasviel/FramePack): @lllyasviel 氏によるオリジナルの実装です。
- [ComfyUI-FramePackWrapper](https://github.com/kijai/ComfyUI-FramePackWrapper): @kijai 氏によるオリジナルの実装です。
- [ComfyUI-FramePackWrapper_Plus](https://github.com/ShmuelRonen/ComfyUI-FramePackWrapper_Plus): @ShmuelRonen 氏によるF1対応のフォークです。
- [ComfyUI-FramePackWrapper_PlusOne](https://github.com/tori29umai0123/ComfyUI-FramePackWrapper_PlusOne): @tori29umai0123 氏による1フレーム推論対応のフォークです。
- [musubi-tuner](https://github.com/kohya-ss/musubi-tuner): @kohya-ss 氏による高品質なFramePack学習・推論ライブラリ
+58
View File
@@ -59,9 +59,67 @@ See [example_workflows](./example_workflows).
[MIT License](LICENSE)
## Changelog
### v2.0.0 - Full musubi-tuner Compatibility (2025-01-XX)
Achieved **complete compatibility with musubi-tuner specifications** to improve inference result consistency when using multiple reference images.
#### Major Changes
**1. Improved Embedding Integration Method**
- ❌ Previous: Weighted average integration (70% input image, 30% reference images)
- ✅ **New:** musubi-tuner compatible processing (using first reference image embedding)
**2. Unified Latent Combination Structure**
- ❌ Previous: Separate management of input and reference images before combination
- ✅ **New:** Direct control_latents combination following musubi-tuner specification
```python
control_latents = [input_image, reference_image1, reference_image2, ..., zero_latent]
clean_latents = torch.cat(control_latents, dim=2)
```
**3. Optimized Mask Application Timing**
- ❌ Previous: Individual application before latent combination
- ✅ **New:** Mask application after clean_latents generation (musubi-tuner specification)
**4. Dynamic Index Setting Processing**
- ❌ Previous: Fixed clean_latent_indices configuration
- ✅ **New:** Dynamic application of control_indices parameters
```python
# control_index="0;7;8;9;10" → clean_latent_indices = [0, 7, 8, 9, 10]
while i < len(control_indices_list) and i < clean_latent_indices.shape[1]:
clean_latent_indices[:, i] = control_indices_list[i]
```
**5. Improved latent_indices Initialization**
- ❌ Previous: ComfyUI-specific initialization method
- ✅ **New:** musubi-tuner specification initialization
```python
latent_indices = torch.zeros((1, 1), dtype=torch.int64)
latent_indices[:, 0] = latent_window_size # default value
latent_indices[:, 0] = target_index # parameter application
```
#### Expected Benefits
- **Improved Inference Consistency**: Generate identical results to musubi-tuner with same reference images and parameters
- **Stabilized Multi-Reference Processing**: More stable quality through accurate index management
- **Parameter Compatibility**: Correct functionality of musubi-tuner's control_index and target_index parameters
#### Technical Details
This update ensures the following processing flow matches musubi-tuner completely:
1. **Control Image Processing**: Sequential processing of multiple images specified by `--control_image_path`
2. **Index Management**: Dynamic application of `--one_frame_inference="control_index=0;7;8;9;10,target_index=5"`
3. **Embedding Processing**: Implementation simulating section-wise individual processing
4. **Mask Application**: Unified mask processing after clean_latents construction
## Credits
- [FramePack](https://github.com/lllyasviel/FramePack): @lllyasviel's original implementation.
- [ComfyUI-FramePackWrapper](https://github.com/kijai/ComfyUI-FramePackWrapper): @kijai's original implementation.
- [ComfyUI-FramePackWrapper_Plus](https://github.com/ShmuelRonen/ComfyUI-FramePackWrapper_Plus): @ShmuelRonen's F1-supported fork.
- [ComfyUI-FramePackWrapper_PlusOne](https://github.com/tori29umai0123/ComfyUI-FramePackWrapper_PlusOne): @tori29umai0123's 1-frame inference-supported fork.
- [musubi-tuner](https://github.com/kohya-ss/musubi-tuner): @kohya-ss's high-quality FramePack training and inference library
+10 -10
View File
@@ -1,11 +1,11 @@
from .nodes import NODE_CLASS_MAPPINGS as NODES_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as NODES_DISPLAY_MAPPINGS
from .nodes_F1 import NODE_CLASS_MAPPINGS as F1_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as F1_DISPLAY_MAPPINGS
from .nodes_one import NODE_CLASS_MAPPINGS as ONE_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as ONE_DISPLAY_MAPPINGS
from .nodes_resize import NODE_CLASS_MAPPINGS as RESIZE_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as RESIZE_DISPLAY_MAPPINGS
# Combine the mappings
NODE_CLASS_MAPPINGS = {**NODES_MAPPINGS, **F1_MAPPINGS, **ONE_MAPPINGS, **RESIZE_MAPPINGS}
NODE_DISPLAY_NAME_MAPPINGS = {**NODES_DISPLAY_MAPPINGS, **F1_DISPLAY_MAPPINGS, **ONE_DISPLAY_MAPPINGS, **RESIZE_DISPLAY_MAPPINGS}
from .nodes import NODE_CLASS_MAPPINGS as NODES_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as NODES_DISPLAY_MAPPINGS
from .nodes_F1 import NODE_CLASS_MAPPINGS as F1_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as F1_DISPLAY_MAPPINGS
from .nodes_one import NODE_CLASS_MAPPINGS as ONE_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as ONE_DISPLAY_MAPPINGS
from .nodes_resize import NODE_CLASS_MAPPINGS as RESIZE_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as RESIZE_DISPLAY_MAPPINGS
# Combine the mappings
NODE_CLASS_MAPPINGS = {**NODES_MAPPINGS, **F1_MAPPINGS, **ONE_MAPPINGS, **RESIZE_MAPPINGS}
NODE_DISPLAY_NAME_MAPPINGS = {**NODES_DISPLAY_MAPPINGS, **F1_DISPLAY_MAPPINGS, **ONE_DISPLAY_MAPPINGS, **RESIZE_DISPLAY_MAPPINGS}
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
@@ -832,8 +832,33 @@ class HunyuanVideoTransformer3DModel(ModelMixin, ConfigMixin, PeftAdapterMixin,
hidden_states = hidden_states.flatten(2).transpose(1, 2)
# Generate rope frequencies for the full sequence
rope_freqs = self.rope(frame_indices=latent_indices, height=H, width=W, device=hidden_states.device)
rope_freqs = rope_freqs.flatten(2).transpose(1, 2)
# Fix rope_freqs size to match packed hidden_states
if rope_freqs.shape[1] != hidden_states.shape[1]:
# Calculate the ratio for expansion
hidden_seq_len = hidden_states.shape[1] # 23562
rope_seq_len = rope_freqs.shape[1] # 11781
if hidden_seq_len > rope_seq_len:
# Calculate how many times to repeat
repeat_factor = hidden_seq_len // rope_seq_len
remainder = hidden_seq_len % rope_seq_len
print(f"Expanding rope_freqs: {rope_seq_len} -> {hidden_seq_len} (repeat_factor={repeat_factor}, remainder={remainder})")
# Repeat rope_freqs to match hidden_states size
rope_freqs = rope_freqs.repeat(1, repeat_factor, 1)
if remainder > 0:
rope_freqs = torch.cat([rope_freqs, rope_freqs[:, :remainder]], dim=1)
print(f"Final rope_freqs shape: {rope_freqs.shape}")
elif hidden_seq_len < rope_seq_len:
# Trim rope_freqs to match hidden_states size
rope_freqs = rope_freqs[:, :hidden_seq_len, :]
print(f"Trimmed rope_freqs shape: {rope_freqs.shape}")
if clean_latents is not None and clean_latent_indices is not None:
clean_latents = clean_latents.to(hidden_states)
@@ -842,6 +867,20 @@ class HunyuanVideoTransformer3DModel(ModelMixin, ConfigMixin, PeftAdapterMixin,
clean_latent_rope_freqs = self.rope(frame_indices=clean_latent_indices, height=H, width=W, device=clean_latents.device)
clean_latent_rope_freqs = clean_latent_rope_freqs.flatten(2).transpose(1, 2)
# Ensure clean_latent_rope_freqs matches clean_latents size
if clean_latent_rope_freqs.shape[1] != clean_latents.shape[1]:
clean_seq_len = clean_latents.shape[1]
clean_rope_seq_len = clean_latent_rope_freqs.shape[1]
if clean_seq_len > clean_rope_seq_len:
repeat_factor = clean_seq_len // clean_rope_seq_len
remainder = clean_seq_len % clean_rope_seq_len
clean_latent_rope_freqs = clean_latent_rope_freqs.repeat(1, repeat_factor, 1)
if remainder > 0:
clean_latent_rope_freqs = torch.cat([clean_latent_rope_freqs, clean_latent_rope_freqs[:, :remainder]], dim=1)
elif clean_seq_len < clean_rope_seq_len:
clean_latent_rope_freqs = clean_latent_rope_freqs[:, :clean_seq_len, :]
hidden_states = torch.cat([clean_latents, hidden_states], dim=1)
rope_freqs = torch.cat([clean_latent_rope_freqs, rope_freqs], dim=1)
@@ -856,6 +895,20 @@ class HunyuanVideoTransformer3DModel(ModelMixin, ConfigMixin, PeftAdapterMixin,
clean_latent_2x_rope_freqs = pad_for_3d_conv(clean_latent_2x_rope_freqs, (2, 2, 2))
clean_latent_2x_rope_freqs = center_down_sample_3d(clean_latent_2x_rope_freqs, (2, 2, 2))
clean_latent_2x_rope_freqs = clean_latent_2x_rope_freqs.flatten(2).transpose(1, 2)
# Ensure clean_latent_2x_rope_freqs matches clean_latents_2x size
if clean_latent_2x_rope_freqs.shape[1] != clean_latents_2x.shape[1]:
clean_2x_seq_len = clean_latents_2x.shape[1]
clean_2x_rope_seq_len = clean_latent_2x_rope_freqs.shape[1]
if clean_2x_seq_len > clean_2x_rope_seq_len:
repeat_factor = clean_2x_seq_len // clean_2x_rope_seq_len
remainder = clean_2x_seq_len % clean_2x_rope_seq_len
clean_latent_2x_rope_freqs = clean_latent_2x_rope_freqs.repeat(1, repeat_factor, 1)
if remainder > 0:
clean_latent_2x_rope_freqs = torch.cat([clean_latent_2x_rope_freqs, clean_latent_2x_rope_freqs[:, :remainder]], dim=1)
elif clean_2x_seq_len < clean_2x_rope_seq_len:
clean_latent_2x_rope_freqs = clean_latent_2x_rope_freqs[:, :clean_2x_seq_len, :]
hidden_states = torch.cat([clean_latents_2x, hidden_states], dim=1)
rope_freqs = torch.cat([clean_latent_2x_rope_freqs, rope_freqs], dim=1)
@@ -870,6 +923,20 @@ class HunyuanVideoTransformer3DModel(ModelMixin, ConfigMixin, PeftAdapterMixin,
clean_latent_4x_rope_freqs = pad_for_3d_conv(clean_latent_4x_rope_freqs, (4, 4, 4))
clean_latent_4x_rope_freqs = center_down_sample_3d(clean_latent_4x_rope_freqs, (4, 4, 4))
clean_latent_4x_rope_freqs = clean_latent_4x_rope_freqs.flatten(2).transpose(1, 2)
# Ensure clean_latent_4x_rope_freqs matches clean_latents_4x size
if clean_latent_4x_rope_freqs.shape[1] != clean_latents_4x.shape[1]:
clean_4x_seq_len = clean_latents_4x.shape[1]
clean_4x_rope_seq_len = clean_latent_4x_rope_freqs.shape[1]
if clean_4x_seq_len > clean_4x_rope_seq_len:
repeat_factor = clean_4x_seq_len // clean_4x_rope_seq_len
remainder = clean_4x_seq_len % clean_4x_rope_seq_len
clean_latent_4x_rope_freqs = clean_latent_4x_rope_freqs.repeat(1, repeat_factor, 1)
if remainder > 0:
clean_latent_4x_rope_freqs = torch.cat([clean_latent_4x_rope_freqs, clean_latent_4x_rope_freqs[:, :remainder]], dim=1)
elif clean_4x_seq_len < clean_4x_rope_seq_len:
clean_latent_4x_rope_freqs = clean_latent_4x_rope_freqs[:, :clean_4x_seq_len, :]
hidden_states = torch.cat([clean_latents_4x, hidden_states], dim=1)
rope_freqs = torch.cat([clean_latent_4x_rope_freqs, rope_freqs], dim=1)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+65 -19
View File
@@ -8,8 +8,8 @@ from pathlib import Path
logger = logging.getLogger(__name__)
from accelerate import init_empty_weights
from accelerate.utils import set_module_tensor_to_device
# from accelerate import init_empty_weights
# from accelerate.utils import set_module_tensor_to_device
import folder_paths
import comfy.model_management as mm
@@ -24,7 +24,7 @@ script_directory = os.path.dirname(os.path.abspath(__file__))
vae_scaling_factor = 0.476986
from .diffusers_helper.models.hunyuan_video_packed import HunyuanVideoTransformer3DModel
from .diffusers_helper.memory import DynamicSwapInstaller, move_model_to_device_with_memory_preservation
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 .diffusers_helper.bucket_tools import find_nearest_bucket
@@ -439,7 +439,7 @@ class DownloadAndLoadFramePackModel:
local_dir_use_symlinks=False,
)
transformer = HunyuanVideoTransformer3DModel.from_pretrained(model_path, torch_dtype=base_dtype, attention_mode=attention_mode).cpu()
transformer = HunyuanVideoTransformer3DModel.from_pretrained(model_path, torch_dtype=base_dtype, attention_mode=attention_mode)
params_to_keep = {"norm", "bias", "time_in", "vector_in", "guidance_in", "txt_in", "img_in"}
if quantization == 'fp8_e4m3fn' or quantization == 'fp8_e4m3fn_fast':
transformer = transformer.to(torch.float8_e4m3fn)
@@ -451,7 +451,8 @@ class DownloadAndLoadFramePackModel:
else:
transformer = transformer.to(base_dtype)
DynamicSwapInstaller.install_model(transformer, device=device)
# Move model directly to device
transformer = transformer.to(device)
if compile_args is not None:
if compile_args["compile_single_blocks"]:
@@ -546,10 +547,14 @@ class LoadFramePackModel:
import json
with open(model_config_path, "r") as f:
config = json.load(f)
sd = load_torch_file(model_path, device=offload_device, safe_load=True)
# Load state dict directly to the target device
sd = load_torch_file(model_path, device=transformer_load_device, safe_load=True)
model_weight_dtype = sd['single_transformer_blocks.0.attn.to_k.weight'].dtype
with init_empty_weights():
transformer = HunyuanVideoTransformer3DModel(**config, attention_mode=attention_mode)
# Create model on the target device directly with proper dtype
transformer = HunyuanVideoTransformer3DModel(**config, attention_mode=attention_mode)
params_to_keep = {"norm", "bias", "time_in", "vector_in", "guidance_in", "txt_in", "img_in"}
if quantization == "fp8_e4m3fn" or quantization == "fp8_e4m3fn_fast" or quantization == "fp8_scaled":
dtype = torch.float8_e4m3fn
@@ -557,17 +562,45 @@ class LoadFramePackModel:
dtype = torch.float8_e5m2
else:
dtype = base_dtype
# Store the original dtype for later use with LoRA
after_lora_dtype = dtype
if lora is not None:
after_lora_dtype = dtype
# When LoRA is present, we need to use base_dtype initially
dtype = base_dtype
print("Using accelerate to load and assign model weights to device...")
param_count = sum(1 for _ in transformer.named_parameters())
for name, param in tqdm(transformer.named_parameters(),
desc=f"Loading transformer parameters to {transformer_load_device}",
total=param_count,
leave=True):
dtype_to_use = base_dtype if any(keyword in name for keyword in params_to_keep) else dtype
set_module_tensor_to_device(transformer, name, device=transformer_load_device, dtype=dtype_to_use, value=sd[name])
# Convert dtype for state dict if needed when LoRA is present
# Only convert if dtypes actually differ to avoid unnecessary memory operations
if dtype != model_weight_dtype:
print(f"Converting model weights from {model_weight_dtype} to {dtype} for LoRA compatibility")
for key in sd.keys():
if any(keyword in key for keyword in params_to_keep):
sd[key] = sd[key].to(base_dtype)
else:
sd[key] = sd[key].to(dtype)
else:
# When no LoRA, only convert if model dtype differs from target dtype
# AND if quantization is disabled (otherwise we handle it later)
if quantization == "disabled" and dtype != model_weight_dtype:
print(f"Converting model weights from {model_weight_dtype} to {dtype}")
for key in sd.keys():
if any(keyword in key for keyword in params_to_keep):
sd[key] = sd[key].to(base_dtype)
else:
sd[key] = sd[key].to(dtype)
# Load state dict all at once
print(f"Loading transformer state dict to {transformer_load_device}")
info = transformer.load_state_dict(sd, strict=True)
print(f"Loaded state dict: {info}")
# Move model to device with proper dtype
if quantization != "disabled" and lora is None:
# For quantized models without LoRA, convert to target dtype directly
transformer = transformer.to(transformer_load_device).to(after_lora_dtype)
else:
# For non-quantized or LoRA models, just move to device
transformer = transformer.to(transformer_load_device)
if lora is not None:
adapter_list = []
@@ -634,18 +667,31 @@ class LoadFramePackModel:
transformer.fuse_lora(lora_scale=lora_scale)
transformer.delete_adapters(adapter_list)
# if quantization == "fp8_e4m3fn" or quantization == "fp8_e4m3fn_fast" or quantization == "fp8_e5m2":
# params_to_keep = {"norm", "bias", "time_in", "vector_in", "guidance_in", "txt_in", "img_in"}
# for name, param in transformer.named_parameters():
# # Make sure to not cast the LoRA weights to fp8.
# if not any(keyword in name for keyword in params_to_keep) and not 'lora' in name:
# param.data = param.data.to(after_lora_dtype)
if quantization == "fp8_e4m3fn" or quantization == "fp8_e4m3fn_fast" or quantization == "fp8_e5m2":
params_to_keep = {"norm", "bias", "time_in", "vector_in", "guidance_in", "txt_in", "img_in"}
for name, param in transformer.named_parameters():
# Make sure to not cast the LoRA weights to fp8.
# Make sure to not cast the LORA weights to fp8.
if not any(keyword in name for keyword in params_to_keep) and not 'lora' in name:
param.data = param.data.to(after_lora_dtype)
if quantization == "fp8_e4m3fn_fast":
from .fp8_optimization import convert_fp8_linear
convert_fp8_linear(transformer, base_dtype, params_to_keep=params_to_keep)
DynamicSwapInstaller.install_model(transformer, device=device)
# Move model to device if it's not already there
if transformer_load_device != device:
transformer = transformer.to(device)
if compile_args is not None:
if compile_args["compile_single_blocks"]:
+315 -176
View File
@@ -30,10 +30,10 @@ class FramePackSingleFrameSampler:
"LATENT",
{"tooltip": "init Latents to use for image2image"},
),
"steps": ("INT", {"default": 30, "min": 1}),
"steps": ("INT", {"default": 20, "min": 1, "tooltip": "Sampling steps (10=fast preview, 20=balanced, 30=high quality)"}),
"use_teacache": (
"BOOLEAN",
{"default": True, "tooltip": "Use teacache for faster sampling."},
{"default": False, "tooltip": "Use TeaCache for faster sampling (recommended for 15+ steps)."},
),
"teacache_rel_l1_thresh": (
"FLOAT",
@@ -79,13 +79,6 @@ class FramePackSingleFrameSampler:
},
),
"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",),
@@ -97,42 +90,39 @@ class FramePackSingleFrameSampler:
"FLOAT",
{"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01},
),
"reference_latent": (
"LATENT",
{"tooltip": "Reference image latent for kisekaeichi mode"},
"reference_latents": (
"REFERENCE_LATENT_LIST",
{"tooltip": "List of reference image latents for kisekaeichi mode (up to 8)"},
),
"reference_image_embeds": (
"CLIP_VISION_OUTPUT",
{"tooltip": "Reference image CLIP embeds for kisekaeichi mode"},
"REFERENCE_EMBEDS_LIST",
{"tooltip": "List of reference image CLIP embeds for kisekaeichi mode (up to 8)"},
),
"reference_masks": (
"REFERENCE_MASK_LIST",
{"tooltip": "List of reference masks for selective features (up to 8, optional)"},
),
"target_index": (
"INT",
{
"default": 1,
"default": 5,
"min": 0,
"max": 8,
"max": 16,
"step": 1,
"tooltip": "Target index for kisekaeichi mode (recommended: 1)",
},
),
"history_index": (
"INT",
"control_index": (
"STRING",
{
"default": 13,
"min": 0,
"max": 16,
"step": 1,
"tooltip": "History index for kisekaeichi mode (recommended: 13)",
"default": "0;10",
"tooltip": "Control indices separated by semicolon (preferred) or comma: 0;7;8;9;10 or 0,7,8,9,10 (default: 0;10)",
},
),
"input_mask": (
"MASK",
{"tooltip": "Input mask for selective application"},
),
"reference_mask": (
"MASK",
{"tooltip": "Reference mask for selective features"},
),
},
}
@@ -140,7 +130,7 @@ class FramePackSingleFrameSampler:
RETURN_NAMES = ("samples",)
FUNCTION = "process"
CATEGORY = "FramePackWrapper"
DESCRIPTION = "Single frame sampler with Kisekaeichi (style transfer) support"
DESCRIPTION = "Single frame sampler with auto-enabled Kisekaeichi (style transfer) support for multiple references"
def process(
self,
@@ -161,18 +151,36 @@ class FramePackSingleFrameSampler:
image_embeds=None,
initial_samples=None,
denoise_strength=1.0,
use_kisekaeichi=False,
reference_latent=None,
reference_latents=None,
reference_image_embeds=None,
reference_masks=None,
target_index=1,
history_index=13,
control_index="0;10",
input_mask=None,
reference_mask=None,
):
print("=== 1フレーム推論モード ===")
print("=== 1フレーム推論モード(musubi-tuner完全互換) ===")
# Use control_index parameter directly (no more one_frame_inference)
# Auto-enable kisekaeichi when reference inputs are provided
use_kisekaeichi = reference_latents is not None and len(reference_latents) > 0
if use_kisekaeichi:
print("Kisekaeichi(着せ替え)モード有効")
print(f"target_index: {target_index}, history_index: {history_index}")
print("Kisekaeichi(着せ替え)モード有効 (自動検出)")
print(f"target_index: {target_index}")
print(f"control_index raw value: '{control_index}'")
print(f"control_index type: {type(control_index)}")
# Parse control indices from parameter (support both semicolon and comma for backward compatibility)
if ';' in control_index:
# New format (musubi-tuner compatible): semicolon-separated
control_indices_list = [int(x.strip()) for x in control_index.split(';') if x.strip()]
else:
# Old format: comma-separated for backward compatibility
control_indices_list = [int(x.strip()) for x in control_index.split(',') if x.strip()]
if len(control_indices_list) == 0:
control_indices_list = [0, 10] # default
print(f"parsed control_index: {control_indices_list}")
transformer = model["transformer"]
base_dtype = model["dtype"]
@@ -188,9 +196,13 @@ class FramePackSingleFrameSampler:
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}")
# Process reference latents if provided
processed_reference_latents = []
if use_kisekaeichi and reference_latents is not None:
for i, ref_latent in enumerate(reference_latents):
processed_latent = ref_latent["samples"] * vae_scaling_factor
processed_reference_latents.append(processed_latent)
print(f"参照画像latent {i+1}: {processed_latent.shape}")
print("start_latent", start_latent.shape)
B, C, T, H, W = start_latent.shape
@@ -203,13 +215,20 @@ class FramePackSingleFrameSampler:
else:
start_image_encoder_last_hidden_state = None
# Process reference image embeds if provided
processed_reference_embeds = []
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
for i, ref_embeds in enumerate(reference_image_embeds):
embed_state = ref_embeds["last_hidden_state"].to(device, base_dtype)
processed_reference_embeds.append(embed_state)
print(f"参照画像 {i+1} のCLIP embeddingを設定しました")
# Validate that latents and embeds have same count
if use_kisekaeichi:
if len(processed_reference_latents) != len(processed_reference_embeds):
raise ValueError(f"Reference latents count ({len(processed_reference_latents)}) must match embeds count ({len(processed_reference_embeds)})")
if len(processed_reference_latents) == 0:
raise ValueError("No reference latents provided for kisekaeichi mode")
# テキストエンベッディング処理
llama_vec = positive[0][0].to(device, base_dtype)
@@ -270,164 +289,166 @@ class FramePackSingleFrameSampler:
# 一つ目のコードの重要な処理:1フレームモード時のone_frame_inference処理
if sample_num_frames == 1:
if use_kisekaeichi and reference_latent is not None:
if use_kisekaeichi and len(processed_reference_latents) > 0:
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}")
# control_indexは後でcontrol_index_listから設定されるため、ここでは追加しない
# 公式実装に従った処理
latent_indices = indices[:, -1:] # デフォルトは最後のフレーム
# musubi-tuner仕様:latent_indicesを初期化
latent_indices = torch.zeros((1, 1), dtype=torch.int64) # 1x1 latent index for target image
latent_indices[:, 0] = latent_window_size # musubi-tunerのデフォルト値
# Apply target_index parameter (musubi-tuner仕様)
latent_indices[:, 0] = target_index
print(f"target_index設定 (musubi-tuner仕様): {target_index}")
# この部分は削除(後でまとめて処理するため)
# パラメータ解析と処理(一つ目のコードと同じ)
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(
# control_latentsのダミーを作成(一つ目のコードと同じ構造)
control_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
)
# musubi-tuner仕様:control_latentsを直接結合(マスク適用は後で実行)
control_latents = []
# 入力画像をcontrol_latentsに追加
clean_latents_pre = start_latent.to(torch.float32).cpu()
if len(clean_latents_pre.shape) < 5:
clean_latents_pre = clean_latents_pre.unsqueeze(2)
control_latents.append(clean_latents_pre)
# 参照画像をcontrol_latentsに追加
for i, ref_latent in enumerate(processed_reference_latents):
clean_latent_post = ref_latent[:, :, 0:1, :, :].to(torch.float32).cpu()
control_latents.append(clean_latent_post)
# musubi-tuner仕様:ゼロlatentをclean_latents_postとして追加
control_latents.append(torch.zeros((1, 16, 1, H, W), dtype=torch.float32))
print(f"Add zero latents as clean latents post for one frame inference.")
# musubi-tuner仕様:control_latentsを直接結合
clean_latents = torch.cat(control_latents, dim=2) # (1, 16, num_control_images, H//8, W//8)
# マスクの適用(入力画像)
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",
# musubi-tuner仕様:clean_latent_indicesを設定
clean_latent_indices = torch.zeros((1, len(control_latents)), dtype=torch.int64)
if "no_post" not in []: # one_frame_inferenceは空なので常にtrue
clean_latent_indices[:, -1] = 1 + latent_window_size # default index for clean latents post
# musubi-tuner仕様:control_indices_listをclean_latent_indicesに動的適用
if control_indices_list:
print(f"control_indices動的適用開始: {control_indices_list}")
i = 0
while i < len(control_indices_list) and i < clean_latent_indices.shape[1]:
control_index = int(control_indices_list[i])
clean_latent_indices[:, i] = control_index
print(f"clean_latent_indices[:, {i}] = {control_index}")
i += 1
print(f"最終的なclean_latent_indices: {clean_latent_indices}")
else:
print("control_indices_listが空のため、デフォルト設定を使用")
# musubi-tuner仕様:マスク適用(clean_latents生成後)
for i in range(len(control_latents)):
mask_image = None
# 入力画像のマスク適用
if i == 0 and input_mask is not None:
print("入力画像マスクを適用中(musubi-tuner仕様)...")
try:
height_latent, width_latent = clean_latents.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)
)
.squeeze(0)
.squeeze(0)
)
input_mask_resized = input_mask_resized.to(
clean_latents_pre.device
)[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, :, :]
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
)
mask_image = input_mask_resized.to(clean_latents.device)[None, None, :, :]
print("入力画像マスクを適用しました")
except Exception as e:
print(f"入力マスク適用エラー: {e}")
# 参照画像のマスク適用
elif i > 0 and i < len(control_latents) - 1: # -1はゼロlatentなので除外
ref_index = i - 1 # 最初が入力画像なので-1
if reference_masks is not None and ref_index < len(reference_masks):
print(f"参照画像 {ref_index+1} マスクを適用中(musubi-tuner仕様)...")
try:
height_latent, width_latent = clean_latents.shape[-2:]
reference_mask = reference_masks[ref_index]
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)
)
mask_image = reference_mask_resized.to(clean_latents.device)[None, None, :, :]
print(f"参照画像 {ref_index+1} マスクを適用しました")
except Exception as e:
print(f"参照マスク {ref_index+1} 適用エラー: {e}")
# マスクを適用
if mask_image is not None:
clean_latents[:, :, i : i + 1, :, :] = clean_latents[:, :, i : i + 1, :, :] * mask_image
# Always disable 2x/4x processing for kisekaeichi mode
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_param = None
clean_latent_4x_indices = 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
)
# 画像エンベッディングの処理(musubi-tuner仕様:セクション毎の個別処理)
# musubi-tunerではセクション毎に個別のimage_encoder_last_hidden_stateを使用
# 複数リファレンス時は最初の参照画像のエンベッディングを使用(musubi-tuner互換)
if len(processed_reference_embeds) > 0:
# musubi-tuner互換:最初の参照画像エンベッディングを使用
image_encoder_last_hidden_state = processed_reference_embeds[0]
print(f"musubi-tuner互換:最初の参照画像エンベッディングを使用 (参照画像数: {len(processed_reference_embeds)})")
else:
image_encoder_last_hidden_state = (
start_image_encoder_last_hidden_state
)
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"Kisekaeichi設定完了(musubi-tuner完全互換):")
print(f" - clean_latents.shape: {clean_latents.shape} (入力+参照{len(processed_reference_latents)}個+ゼロlatent)")
print(f" - latent_indices: {latent_indices} (初期値: {latent_window_size} -> target: {target_index})")
print(f" - clean_latent_indices: {clean_latent_indices} (control_indices適用済み)")
print(f" - sample_num_frames: {sample_num_frames}")
print(f" - control_indices適用: {control_indices_list}")
print(f" - control_latents数: {len(control_latents)}")
print(f" - マスク適用: clean_latents生成後")
print(f" - 2x/4x無効化: True")
else:
# 通常モード(参照画像なし)
# control_indicesは通常モードでは使用しない
all_indices = torch.arange(0, latent_window_size).unsqueeze(0)
latent_indices = all_indices[:, -1:]
@@ -552,9 +573,10 @@ class FramePackSingleFrameSampler:
mm.soft_empty_cache()
# 出力処理
mode_info = (
"Kisekaeichi(latent+エンベッディング)" if use_kisekaeichi else "通常"
)
if use_kisekaeichi:
mode_info = f"Kisekaeichi(musubi-tuner完全互換、参照{len(processed_reference_latents)}個)"
else:
mode_info = "通常"
print(
f"=== 1フレーム生成完了 ({mode_info}モード): {generated_latents.shape} ==="
)
@@ -562,10 +584,127 @@ class FramePackSingleFrameSampler:
return ({"samples": generated_latents / vae_scaling_factor},)
class ReferenceLatentList:
@classmethod
def INPUT_TYPES(s):
return {
"required": {},
"optional": {
"reference_1": ("LATENT", {"tooltip": "Reference latent 1"}),
"reference_2": ("LATENT", {"tooltip": "Reference latent 2"}),
"reference_3": ("LATENT", {"tooltip": "Reference latent 3"}),
"reference_4": ("LATENT", {"tooltip": "Reference latent 4"}),
"reference_5": ("LATENT", {"tooltip": "Reference latent 5"}),
"reference_6": ("LATENT", {"tooltip": "Reference latent 6"}),
"reference_7": ("LATENT", {"tooltip": "Reference latent 7"}),
"reference_8": ("LATENT", {"tooltip": "Reference latent 8"}),
},
}
RETURN_TYPES = ("REFERENCE_LATENT_LIST",)
RETURN_NAMES = ("reference_latents",)
FUNCTION = "create_list"
CATEGORY = "FramePackWrapper/References"
DESCRIPTION = "Combine up to 8 reference latents into a list for multi-reference Kisekaeichi mode"
def create_list(self, **kwargs):
reference_list = []
for i in range(1, 9):
ref_key = f"reference_{i}"
if ref_key in kwargs and kwargs[ref_key] is not None:
reference_list.append(kwargs[ref_key])
if len(reference_list) == 0:
return (None,)
print(f"Created reference latent list with {len(reference_list)} items")
return (reference_list,)
class ReferenceEmbedsList:
@classmethod
def INPUT_TYPES(s):
return {
"required": {},
"optional": {
"embeds_1": ("CLIP_VISION_OUTPUT", {"tooltip": "Reference embeds 1"}),
"embeds_2": ("CLIP_VISION_OUTPUT", {"tooltip": "Reference embeds 2"}),
"embeds_3": ("CLIP_VISION_OUTPUT", {"tooltip": "Reference embeds 3"}),
"embeds_4": ("CLIP_VISION_OUTPUT", {"tooltip": "Reference embeds 4"}),
"embeds_5": ("CLIP_VISION_OUTPUT", {"tooltip": "Reference embeds 5"}),
"embeds_6": ("CLIP_VISION_OUTPUT", {"tooltip": "Reference embeds 6"}),
"embeds_7": ("CLIP_VISION_OUTPUT", {"tooltip": "Reference embeds 7"}),
"embeds_8": ("CLIP_VISION_OUTPUT", {"tooltip": "Reference embeds 8"}),
},
}
RETURN_TYPES = ("REFERENCE_EMBEDS_LIST",)
RETURN_NAMES = ("reference_embeds",)
FUNCTION = "create_list"
CATEGORY = "FramePackWrapper/References"
DESCRIPTION = "Combine up to 8 reference CLIP vision embeddings into a list for multi-reference Kisekaeichi mode"
def create_list(self, **kwargs):
embeds_list = []
for i in range(1, 9):
embeds_key = f"embeds_{i}"
if embeds_key in kwargs and kwargs[embeds_key] is not None:
embeds_list.append(kwargs[embeds_key])
if len(embeds_list) == 0:
return (None,)
print(f"Created reference embeds list with {len(embeds_list)} items")
return (embeds_list,)
class ReferenceMaskList:
@classmethod
def INPUT_TYPES(s):
return {
"required": {},
"optional": {
"mask_1": ("MASK", {"tooltip": "Reference mask 1"}),
"mask_2": ("MASK", {"tooltip": "Reference mask 2"}),
"mask_3": ("MASK", {"tooltip": "Reference mask 3"}),
"mask_4": ("MASK", {"tooltip": "Reference mask 4"}),
"mask_5": ("MASK", {"tooltip": "Reference mask 5"}),
"mask_6": ("MASK", {"tooltip": "Reference mask 6"}),
"mask_7": ("MASK", {"tooltip": "Reference mask 7"}),
"mask_8": ("MASK", {"tooltip": "Reference mask 8"}),
},
}
RETURN_TYPES = ("REFERENCE_MASK_LIST",)
RETURN_NAMES = ("reference_masks",)
FUNCTION = "create_list"
CATEGORY = "FramePackWrapper/References"
DESCRIPTION = "Combine up to 8 reference masks into a list for multi-reference Kisekaeichi mode (optional)"
def create_list(self, **kwargs):
mask_list = []
for i in range(1, 9):
mask_key = f"mask_{i}"
if mask_key in kwargs and kwargs[mask_key] is not None:
mask_list.append(kwargs[mask_key])
if len(mask_list) == 0:
return (None,)
print(f"Created reference mask list with {len(mask_list)} items")
return (mask_list,)
NODE_CLASS_MAPPINGS = {
"FramePackSingleFrameSampler": FramePackSingleFrameSampler,
"ReferenceLatentList": ReferenceLatentList,
"ReferenceEmbedsList": ReferenceEmbedsList,
"ReferenceMaskList": ReferenceMaskList,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"FramePackSingleFrameSampler": "FramePack Single Frame Sampler",
"ReferenceLatentList": "Reference Latent List",
"ReferenceEmbedsList": "Reference Embeds List",
"ReferenceMaskList": "Reference Mask List",
}
+860 -860
View File
File diff suppressed because it is too large Load Diff