Files

861 lines
41 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import numpy as np
from PIL import Image
import logging
import math
from tqdm import tqdm
from latent_preview import prepare_callback
import torch
logger = logging.getLogger(__name__)
import comfy.model_base
import comfy.model_management as mm
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
# Constants
VAE_SCALING_FACTOR = 0.476986
DEFAULT_BLEND_ALPHA = 0.5
OVERLAP_RATIO = 0.5 # 50% オーバーラップ
class TileProcessor:
"""50%オーバーラップを持つ2次元画像タイル処理を担当するクラス"""
def __init__(self, tile_size: int, tile_resized: int):
self.tile_size = tile_size
self.tile_resized = tile_resized
self.upscale_factor = float(tile_resized) / float(tile_size)
self.step_size = int(tile_size * (1 - OVERLAP_RATIO)) # 50%オーバーラップのステップサイズ
def split_image_to_tiles(self, image):
"""画像を50%オーバーラップでタイルに分割し、白パディングを追加して指定サイズにリサイズ"""
pil_image = self._convert_to_pil(image)
original_width, original_height = pil_image.size
# オーバーラップを考慮したタイル数を計算
tiles_x = self._calculate_tile_count(original_width)
tiles_y = self._calculate_tile_count(original_height)
self._log_split_info(original_width, original_height, tiles_x, tiles_y)
metadata = self._create_metadata(original_width, original_height, tiles_x, tiles_y)
tiles = self._extract_overlapping_tiles(pil_image, tiles_x, tiles_y, metadata)
return tiles, metadata
def _calculate_tile_count(self, dimension: int) -> int:
"""オーバーラップを考慮したタイル数を計算"""
if dimension <= self.tile_size:
return 1
# 最後のタイルが完全に収まるように計算
return math.ceil((dimension - self.tile_size) / self.step_size) + 1
def _convert_to_pil(self, image):
"""ComfyUIのIMAGE形式からPIL形式に変換"""
if isinstance(image, torch.Tensor):
image_np = (image[0].cpu().numpy() * 255).astype(np.uint8)
return Image.fromarray(image_np)
return image
def _log_split_info(self, width: int, height: int, tiles_x: int, tiles_y: int):
"""分割情報をログ出力"""
print(f"=== 50%オーバーラップ画像分割情報 ===")
print(f"元画像サイズ: {width} × {height}")
print(f"分割タイルサイズ: {self.tile_size} × {self.tile_size}")
print(f"リサイズ後タイルサイズ: {self.tile_resized} × {self.tile_resized}")
print(f"アップスケール倍率: {self.upscale_factor:.2f}x")
print(f"ステップサイズ: {self.step_size} (50%オーバーラップ)")
print(f"タイル数: {tiles_x} × {tiles_y} = {tiles_x * tiles_y}個")
def _create_metadata(self, width: int, height: int, tiles_x: int, tiles_y: int) -> dict:
"""メタデータを作成"""
return {
"original_size": [width, height],
"tile_size": self.tile_size,
"tile_resized": self.tile_resized,
"upscale_factor": self.upscale_factor,
"step_size": self.step_size,
"overlap_ratio": OVERLAP_RATIO,
"tiles_x": tiles_x,
"tiles_y": tiles_y,
"tiles": []
}
def _extract_overlapping_tiles(self, pil_image: Image.Image, tiles_x: int, tiles_y: int, metadata: dict) -> list:
"""50%オーバーラップでタイルを切り出す"""
tiles = []
tile_index = 0
for y in range(tiles_y):
for x in range(tiles_x):
tile_tensor, tile_info = self._process_overlapping_tile(
pil_image, x, y, tile_index
)
tiles.append(tile_tensor)
metadata["tiles"].append(tile_info)
tile_index += 1
return tiles
def _process_overlapping_tile(self, pil_image: Image.Image, x: int, y: int, tile_index: int):
"""50%オーバーラップを考慮した単一タイルを処理"""
# オーバーラップを考慮した切り出し位置を計算
left = x * self.step_size
top = y * self.step_size
right = min(left + self.tile_size, pil_image.width)
bottom = min(top + self.tile_size, pil_image.height)
# 画像端で調整(右端・下端のタイルが小さくなる場合)
if right - left < self.tile_size:
left = max(0, right - self.tile_size)
if bottom - top < self.tile_size:
top = max(0, bottom - self.tile_size)
# タイルを切り出し
tile = pil_image.crop((left, top, right, bottom))
tile_width, tile_height = tile.size
# パディング処理
padding_needed = (tile_width < self.tile_size or tile_height < self.tile_size)
if padding_needed:
tile = self._add_white_padding(tile)
# アップスケール
upscaled_tile = tile.resize((self.tile_resized, self.tile_resized), Image.LANCZOS)
# テンソル変換
tile_tensor = torch.from_numpy(np.array(upscaled_tile)).float() / 255.0
tile_tensor = tile_tensor.unsqueeze(0) # バッチ次元を追加
# オーバーラップ領域の情報を計算
overlap_info = self._calculate_overlap_info(x, y, left, top, right, bottom, pil_image.size)
# メタデータ作成
tile_info = {
"index": tile_index,
"grid_position": [x, y],
"crop_box": [left, top, right, bottom],
"original_tile_size": [tile_width, tile_height],
"padding_needed": padding_needed,
"padding_right": self.tile_size - tile_width if padding_needed else 0,
"padding_bottom": self.tile_size - tile_height if padding_needed else 0,
"overlap_info": overlap_info
}
print(f"タイル {tile_index}: グリッド({x},{y}) 切り出し({left},{top},{right},{bottom}) "
f"オーバーラップ:{overlap_info}")
return tile_tensor, tile_info
def _calculate_overlap_info(self, grid_x: int, grid_y: int, left: int, top: int,
right: int, bottom: int, image_size: tuple) -> dict:
"""オーバーラップ領域の情報を計算"""
image_width, image_height = image_size
overlap_info = {
"has_left_overlap": grid_x > 0,
"has_right_overlap": right < image_width and grid_x < self._calculate_tile_count(image_width) - 1,
"has_top_overlap": grid_y > 0,
"has_bottom_overlap": bottom < image_height and grid_y < self._calculate_tile_count(image_height) - 1,
}
# オーバーラップピクセル数を計算
overlap_pixels = int(self.tile_size * OVERLAP_RATIO)
overlap_info.update({
"left_overlap_pixels": overlap_pixels if overlap_info["has_left_overlap"] else 0,
"right_overlap_pixels": overlap_pixels if overlap_info["has_right_overlap"] else 0,
"top_overlap_pixels": overlap_pixels if overlap_info["has_top_overlap"] else 0,
"bottom_overlap_pixels": overlap_pixels if overlap_info["has_bottom_overlap"] else 0,
})
return overlap_info
def _add_white_padding(self, tile: Image.Image) -> Image.Image:
"""白パディングを追加"""
padded_tile = Image.new('RGB', (self.tile_size, self.tile_size), color='white')
padded_tile.paste(tile, (0, 0))
return padded_tile
class ImageReconstructor:
"""50%オーバーラップしたタイルから画像を再構成するクラス"""
def __init__(self, metadata: dict):
self.metadata = metadata
self.upscale_factor = metadata["upscale_factor"]
self.tile_size = metadata["tile_size"]
self.step_size = metadata["step_size"]
self.overlap_ratio = metadata["overlap_ratio"]
def reconstruct_image(self, processed_tiles: list):
"""50%オーバーラップしたタイルから画像を再構成"""
final_width, final_height = self._calculate_final_size()
self._log_reconstruction_info(final_width, final_height, len(processed_tiles))
# 最終画像とブレンドウェイトマップを初期化
final_image = np.zeros((final_height, final_width, 3), dtype=np.float32)
weight_map = np.zeros((final_height, final_width), dtype=np.float32)
# 処理されたタイル画像を準備
processed_tile_images = self._preprocess_tiles(processed_tiles)
# オーバーラップブレンドでタイルを配置
self._place_tiles_with_overlap_blend(final_image, weight_map, processed_tile_images)
# 正規化して最終画像を作成
final_image = self._normalize_blended_image(final_image, weight_map)
return self._convert_to_tensor(Image.fromarray(final_image.astype(np.uint8)))
def _calculate_final_size(self) -> tuple:
"""最終画像サイズを計算"""
original_width, original_height = self.metadata["original_size"]
final_width = int(original_width * self.upscale_factor)
final_height = int(original_height * self.upscale_factor)
return final_width, final_height
def _log_reconstruction_info(self, width: int, height: int, tile_count: int):
"""再構成情報をログ出力"""
print(f"=== 50%オーバーラップ画像再構成情報 ===")
print(f"最終画像サイズ: {width} × {height} ({self.upscale_factor:.2f}x)")
print(f"処理タイル数: {tile_count}")
print(f"オーバーラップ比率: {self.overlap_ratio*100:.0f}%")
def _preprocess_tiles(self, processed_tiles: list) -> list:
"""タイル画像を前処理"""
processed_tile_images = []
for i, (tile_tensor, tile_info) in enumerate(zip(processed_tiles, self.metadata["tiles"])):
tile_image = self._tensor_to_pil(tile_tensor)
# パディング部分を除去
if tile_info["padding_needed"]:
tile_image = self._remove_padding(tile_image, tile_info)
processed_tile_images.append((tile_image, tile_info))
return processed_tile_images
def _tensor_to_pil(self, tile_tensor) -> Image.Image:
"""テンソルをPIL画像に変換"""
if isinstance(tile_tensor, torch.Tensor):
tile_np = (tile_tensor[0].cpu().numpy() * 255).astype(np.uint8)
return Image.fromarray(tile_np)
return tile_tensor
def _remove_padding(self, tile_image: Image.Image, tile_info: dict) -> Image.Image:
"""パディング部分を除去"""
original_tile_width, original_tile_height = tile_info["original_tile_size"]
content_width = int(original_tile_width * self.upscale_factor)
content_height = int(original_tile_height * self.upscale_factor)
return tile_image.crop((0, 0, content_width, content_height))
def _place_tiles_with_overlap_blend(self, final_image: np.ndarray, weight_map: np.ndarray,
processed_tile_images: list):
"""オーバーラップ領域でブレンドしながらタイルを配置"""
for i, (tile_image, tile_info) in enumerate(processed_tile_images):
grid_x, grid_y = tile_info["grid_position"]
crop_box = tile_info["crop_box"]
overlap_info = tile_info["overlap_info"]
# 元の切り出し位置をアップスケール倍率で拡大
left, top, right, bottom = crop_box
paste_left = int(left * self.upscale_factor)
paste_top = int(top * self.upscale_factor)
paste_right = int(right * self.upscale_factor)
paste_bottom = int(bottom * self.upscale_factor)
# タイル画像を numpy 配列に変換
tile_array = np.array(tile_image, dtype=np.float32)
tile_height, tile_width = tile_array.shape[:2]
# 最終画像の境界内に収まるように調整
paste_right = min(paste_right, final_image.shape[1])
paste_bottom = min(paste_bottom, final_image.shape[0])
actual_width = paste_right - paste_left
actual_height = paste_bottom - paste_top
# タイルサイズが配置領域と異なる場合は調整
if actual_width != tile_width or actual_height != tile_height:
if actual_width > 0 and actual_height > 0:
tile_array = tile_array[:actual_height, :actual_width]
else:
print(f"警告: タイル {i} の配置領域が無効です")
continue
# ブレンド用のウェイトマスクを作成
tile_weight = self._create_blend_weight_mask(
tile_array.shape[:2], overlap_info, crop_box, self.upscale_factor
)
# 加算ブレンド方式で配置
final_image[paste_top:paste_bottom, paste_left:paste_right] += \
tile_array * tile_weight[..., np.newaxis]
weight_map[paste_top:paste_bottom, paste_left:paste_right] += tile_weight
print(f"タイル {i} 配置: 元座標({left},{top},{right},{bottom}) -> "
f"配置座標({paste_left},{paste_top},{paste_right},{paste_bottom}) "
f"サイズ({actual_width}×{actual_height})")
def _create_blend_weight_mask(self, tile_shape: tuple, overlap_info: dict,
crop_box: list, upscale_factor: float) -> np.ndarray:
"""オーバーラップ領域用のウェイトマスクを作成"""
height, width = tile_shape
weight_mask = np.ones((height, width), dtype=np.float32)
# オーバーラップピクセル数を計算(アップスケール後)
overlap_pixels = int(self.tile_size * self.overlap_ratio * upscale_factor)
# 実際の切り出しボックスを使ってオーバーラップ領域を正確に計算
left, top, right, bottom = crop_box
# 左端のフェード(左にオーバーラップがある場合)
if overlap_info["has_left_overlap"]:
# 実際のオーバーラップ開始位置を計算
overlap_start_x = left % self.step_size
if overlap_start_x > 0:
fade_width = min(int(overlap_start_x * upscale_factor), width)
if fade_width > 0:
fade_weights = np.linspace(0, 1, fade_width)
weight_mask[:, :fade_width] *= fade_weights[np.newaxis, :]
# 右端のフェード(右にオーバーラップがある場合)
if overlap_info["has_right_overlap"]:
# 右端のオーバーラップ幅を計算
next_tile_start = (left // self.step_size + 1) * self.step_size
if right > next_tile_start:
overlap_width = right - next_tile_start
fade_width = min(int(overlap_width * upscale_factor), width)
if fade_width > 0:
fade_weights = np.linspace(1, 0, fade_width)
weight_mask[:, -fade_width:] *= fade_weights[np.newaxis, :]
# 上端のフェード(上にオーバーラップがある場合)
if overlap_info["has_top_overlap"]:
# 実際のオーバーラップ開始位置を計算
overlap_start_y = top % self.step_size
if overlap_start_y > 0:
fade_height = min(int(overlap_start_y * upscale_factor), height)
if fade_height > 0:
fade_weights = np.linspace(0, 1, fade_height)
weight_mask[:fade_height, :] *= fade_weights[:, np.newaxis]
# 下端のフェード(下にオーバーラップがある場合)
if overlap_info["has_bottom_overlap"]:
# 下端のオーバーラップ高さを計算
next_tile_start = (top // self.step_size + 1) * self.step_size
if bottom > next_tile_start:
overlap_height = bottom - next_tile_start
fade_height = min(int(overlap_height * upscale_factor), height)
if fade_height > 0:
fade_weights = np.linspace(1, 0, fade_height)
weight_mask[-fade_height:, :] *= fade_weights[:, np.newaxis]
return weight_mask
def _normalize_blended_image(self, final_image: np.ndarray, weight_map: np.ndarray) -> np.ndarray:
"""ブレンドされた画像を正規化"""
# ゼロ除算を避けるため、重みが0の部分には小さな値を設定
weight_map = np.maximum(weight_map, 1e-8)
# 正規化
final_image = final_image / weight_map[..., np.newaxis]
# 値の範囲を0-255にクランプ
final_image = np.clip(final_image, 0, 255)
return final_image
def _convert_to_tensor(self, final_image: Image.Image) -> torch.Tensor:
"""PIL画像をComfyUI形式のテンソルに変換"""
final_tensor = torch.from_numpy(np.array(final_image)).float() / 255.0
return final_tensor.unsqueeze(0) # バッチ次元を追加
class VAEProcessor:
"""VAE処理を担当するクラス"""
@staticmethod
def tiled_decode(vae, latents: torch.Tensor, tile_size: int = 256,
overlap: int = 64, temporal_size: int = 64,
temporal_overlap: int = 8) -> torch.Tensor:
"""タイル化VAEデコード"""
B, C, T, H, W = latents.shape
device = latents.device
dtype = latents.dtype
# 出力テンソルを初期化 (VAEは8倍アップスケール)
output_h, output_w = H * 8, W * 8
output = torch.zeros(B, T, output_h, output_w, 3, device=device, dtype=dtype)
# タイル分割数を計算
h_tiles, w_tiles, t_tiles = VAEProcessor._calculate_tile_counts(
H, W, T, tile_size, overlap, temporal_size, temporal_overlap
)
print(f"タイル化デコード: H={h_tiles}, W={w_tiles}, T={t_tiles}")
# タイル処理実行
VAEProcessor._process_tiles(
vae, latents, output, h_tiles, w_tiles, t_tiles,
tile_size, overlap, temporal_size, temporal_overlap, device, dtype
)
return output
@staticmethod
def _calculate_tile_counts(H: int, W: int, T: int, tile_size: int,
overlap: int, temporal_size: int, temporal_overlap: int) -> tuple:
"""タイル分割数を計算"""
h_tiles = max(1, (H - overlap) // (tile_size - overlap) +
(1 if (H - overlap) % (tile_size - overlap) > 0 else 0))
w_tiles = max(1, (W - overlap) // (tile_size - overlap) +
(1 if (W - overlap) % (tile_size - overlap) > 0 else 0))
t_tiles = max(1, (T - temporal_overlap) // (temporal_size - temporal_overlap) +
(1 if (T - temporal_overlap) % (temporal_size - temporal_overlap) > 0 else 0))
return h_tiles, w_tiles, t_tiles
@staticmethod
def _process_tiles(vae, latents: torch.Tensor, output: torch.Tensor,
h_tiles: int, w_tiles: int, t_tiles: int,
tile_size: int, overlap: int, temporal_size: int,
temporal_overlap: int, device, dtype):
"""実際のタイル処理を実行"""
B, C, T, H, W = latents.shape
for t_idx in range(t_tiles):
t_start, t_end = VAEProcessor._calculate_range(
t_idx, temporal_size, temporal_overlap, T
)
for h_idx in range(h_tiles):
h_start, h_end = VAEProcessor._calculate_range(
h_idx, tile_size, overlap, H
)
for w_idx in range(w_tiles):
w_start, w_end = VAEProcessor._calculate_range(
w_idx, tile_size, overlap, W
)
# タイルを切り出してデコード
tile_latent = latents[:, :, t_start:t_end, h_start:h_end, w_start:w_end]
try:
with torch.autocast(device_type=mm.get_autocast_device(device),
dtype=dtype, enabled=True):
decoded_tile = vae.decode(tile_latent)
# 出力に配置(オーバーラップ処理含む)
VAEProcessor._place_decoded_tile(
output, decoded_tile, t_start, t_end,
h_start, h_end, w_start, w_end, h_idx, w_idx
)
except Exception as e:
print(f"タイル({t_idx},{h_idx},{w_idx})のデコードに失敗: {e}")
continue
@staticmethod
def _calculate_range(idx: int, size: int, overlap: int, max_size: int) -> tuple:
"""範囲を計算"""
start = idx * (size - overlap)
end = min(start + size, max_size)
start = max(0, end - size) # 境界調整
return start, end
@staticmethod
def _place_decoded_tile(output: torch.Tensor, decoded_tile: torch.Tensor,
t_start: int, t_end: int, h_start: int, h_end: int,
w_start: int, w_end: int, h_idx: int, w_idx: int):
"""デコードされたタイルを出力に配置"""
out_t_start, out_t_end = t_start, t_end
out_h_start, out_h_end = h_start * 8, h_end * 8
out_w_start, out_w_end = w_start * 8, w_end * 8
# オーバーラップ処理
if h_idx > 0 or w_idx > 0:
existing = output[:, out_t_start:out_t_end, out_h_start:out_h_end, out_w_start:out_w_end]
if existing.sum() > 0: # 既存データがある場合のみブレンド
output[:, out_t_start:out_t_end, out_h_start:out_h_end, out_w_start:out_w_end] = \
DEFAULT_BLEND_ALPHA * existing + (1 - DEFAULT_BLEND_ALPHA) * decoded_tile
else:
output[:, out_t_start:out_t_end, out_h_start:out_h_end, out_w_start:out_w_end] = decoded_tile
else:
# 最初のタイルはそのまま配置
output[:, out_t_start:out_t_end, out_h_start:out_h_end, out_w_start:out_w_end] = decoded_tile
class FramePackSingleFrameResizeSampler:
"""メインのサンプラークラス"""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"model": ("FramePackMODEL",),
"vae": ("VAE",),
"positive": ("CONDITIONING",),
"negative": ("CONDITIONING",),
"input_image": ("IMAGE", {"tooltip": "Input image to be upscaled"}),
"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'}),
"tile_size": ("INT", {"default": 256, "min": 256, "max": 1024, "step": 64, "tooltip": "Size of each tile for initial split"}),
"tile_resized": ("INT", {"default": 1024, "min": 512, "max": 2048, "step": 64, "tooltip": "Size to resize each tile to before processing"}),
"vae_tile_size": ("INT", {"default": 256, "min": 64, "max": 512, "step": 64, "tooltip": "Size of each VAE tile for decode"}),
"vae_overlap": ("INT", {"default": 64, "min": 0, "max": 256, "step": 8, "tooltip": "Overlap between VAE tiles"}),
"vae_temporal_size": ("INT", {"default": 64, "min": 1, "max": 128, "step": 1, "tooltip": "Temporal size for VAE tiled decode"}),
"vae_temporal_overlap": ("INT", {"default": 8, "min": 0, "max": 32, "step": 1, "tooltip": "Temporal overlap for VAE tiled decode"}),
},
"optional": {
"image_embeds": ("CLIP_VISION_OUTPUT", ),
"denoise_strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
}
}
RETURN_TYPES = ("IMAGE", )
RETURN_NAMES = ("upscaled_image",)
FUNCTION = "process"
CATEGORY = "FramePackWrapper"
DESCRIPTION = "Variable ratio image upscaling using 50% overlapping tile-based FramePack single frame sampling"
def process(self, model, vae, shift, positive, negative, input_image, latent_window_size,
use_teacache, teacache_rel_l1_thresh, steps, cfg, guidance_scale, seed,
sampler, gpu_memory_preservation, tile_size=512, tile_resized=1024,
vae_tile_size=256, vae_overlap=64, vae_temporal_size=64,
vae_temporal_overlap=8, image_embeds=None, denoise_strength=1.0):
upscale_factor = float(tile_resized) / float(tile_size)
self._log_process_start(upscale_factor, vae_tile_size, vae_overlap,
vae_temporal_size, vae_temporal_overlap, seed)
# デバイス設定とモデル準備
transformer, base_dtype, device, offload_device = self._setup_model(model)
# 1. 画像を50%オーバーラップでタイルに分割
tile_processor = TileProcessor(tile_size, tile_resized)
tiles, metadata = tile_processor.split_image_to_tiles(input_image)
# 2. テキストエンベッディング準備
text_embeddings = self._prepare_text_embeddings(
positive, negative, cfg, device, base_dtype
)
# 3. 画像エンベッディング準備
image_encoder_embeddings = self._prepare_image_embeddings(
image_embeds, device, base_dtype
)
# 4. ComfyUIセットアップ
callback = self._setup_comfy_ui(transformer, device, steps, base_dtype)
# 5. モデル準備
self._prepare_model(transformer, device, gpu_memory_preservation,
use_teacache, steps, teacache_rel_l1_thresh)
# 6. 各タイルを処理
processed_tiles = self._process_all_tiles(
tiles, vae, transformer, sampler, denoise_strength, latent_window_size,
cfg, guidance_scale, shift, steps, seed, device, base_dtype,
text_embeddings, image_encoder_embeddings, callback,
vae_tile_size, vae_overlap, vae_temporal_size, vae_temporal_overlap
)
# 7. 50%オーバーラップブレンドで最終画像を再構成
reconstructor = ImageReconstructor(metadata)
final_image = reconstructor.reconstruct_image(processed_tiles)
# クリーンアップ
self._cleanup(transformer, offload_device)
print(f"=== 50%オーバーラップ {upscale_factor:.2f}xアップスケール完了: {final_image.shape} ===")
return (final_image,)
def _log_process_start(self, upscale_factor: float, vae_tile_size: int,
vae_overlap: int, vae_temporal_size: int,
vae_temporal_overlap: int, seed: int):
"""処理開始のログ出力"""
print(f"=== FramePack 50%オーバーラップ Resize Sampler 開始 ({upscale_factor:.2f}x) ===")
print(f"VAE Tile設定: size={vae_tile_size}, overlap={vae_overlap}, "
f"temporal_size={vae_temporal_size}, temporal_overlap={vae_temporal_overlap}")
print(f"使用シード: {seed} (全タイル共通)")
print(f"オーバーラップ比率: {OVERLAP_RATIO*100:.0f}%")
def _setup_model(self, model: dict) -> tuple:
"""モデルとデバイスのセットアップ"""
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()
return transformer, base_dtype, device, offload_device
def _prepare_text_embeddings(self, positive, negative, cfg: float,
device, base_dtype) -> dict:
"""テキストエンベッディングを準備"""
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)
return {
"llama_vec": llama_vec,
"llama_attention_mask": llama_attention_mask,
"clip_l_pooler": clip_l_pooler,
"llama_vec_n": llama_vec_n,
"llama_attention_mask_n": llama_attention_mask_n,
"clip_l_pooler_n": clip_l_pooler_n
}
def _prepare_image_embeddings(self, image_embeds, device, base_dtype):
"""画像エンベッディングを準備"""
if image_embeds is not None:
return image_embeds["last_hidden_state"].to(device, base_dtype)
return None
def _setup_comfy_ui(self, transformer, device, steps, base_dtype):
"""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"))
return prepare_callback(patcher, steps)
def _prepare_model(self, transformer, device, gpu_memory_preservation: float,
use_teacache: bool, steps: int, teacache_rel_l1_thresh: float):
"""モデルの準備(GPU移動とTeaCache設定)"""
# モデルを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)
def _process_all_tiles(self, tiles: list, vae, transformer, sampler: str,
denoise_strength: float, latent_window_size: int,
cfg: float, guidance_scale: float, shift: float,
steps: int, seed: int, device, base_dtype,
text_embeddings: dict, image_encoder_embeddings,
callback, vae_tile_size: int, vae_overlap: int,
vae_temporal_size: int, vae_temporal_overlap: int) -> list:
"""全てのタイルを処理"""
processed_tiles = []
print(f"=== {len(tiles)}個のタイルを順次処理(50%オーバーラップ、各タイルで同一シード使用) ===")
for i, tile_tensor in enumerate(tqdm(tiles, desc="オーバーラップタイル処理")):
print(f"\n--- タイル {i+1}/{len(tiles)} 処理中(シード: {seed}) ---")
# 各タイルで同じシードを使用するためにジェネレータをリセット
rnd = torch.Generator("cpu").manual_seed(seed)
processed_tile = self._process_single_tile(
tile_tensor, vae, transformer, sampler, denoise_strength,
latent_window_size, cfg, guidance_scale, shift, steps, rnd,
device, base_dtype, text_embeddings, image_encoder_embeddings,
callback, vae_tile_size, vae_overlap, vae_temporal_size, vae_temporal_overlap
)
processed_tiles.append(processed_tile)
print(f"オーバーラップタイル {i+1} 完了: {processed_tile.shape}")
return processed_tiles
def _process_single_tile(self, tile_tensor: torch.Tensor, vae, transformer,
sampler: str, denoise_strength: float, latent_window_size: int,
cfg: float, guidance_scale: float, shift: float,
steps: int, rnd: torch.Generator, device, base_dtype,
text_embeddings: dict, image_encoder_embeddings,
callback, vae_tile_size: int, vae_overlap: int,
vae_temporal_size: int, vae_temporal_overlap: int) -> torch.Tensor:
"""単一タイルを処理"""
tile_tensor = tile_tensor.to(device)
with torch.autocast(device_type=mm.get_autocast_device(device),
dtype=base_dtype, enabled=True):
# VAEエンコード
start_latent = self._encode_tile(vae, tile_tensor)
# FramePack推論のためのインデックス設定
latent_indices, clean_latents, clean_latent_indices = self._setup_latent_indices(
start_latent, latent_window_size
)
# FramePack推論実行
generated_latents = self._run_framepack_inference(
transformer, sampler, denoise_strength, start_latent.shape,
cfg, guidance_scale, shift, steps, rnd, device, base_dtype,
text_embeddings, image_encoder_embeddings, latent_indices,
clean_latents, clean_latent_indices, callback
)
# VAEタイル化デコード
decoded_tile = self._decode_tile(
vae, generated_latents, vae_tile_size, vae_overlap,
vae_temporal_size, vae_temporal_overlap
)
# 時間次元を除去して画像形式に変換 [B, T, H, W, C] -> [B, H, W, C]
if len(decoded_tile.shape) == 5:
decoded_tile = decoded_tile.squeeze(1)
return decoded_tile
def _encode_tile(self, vae, tile_tensor: torch.Tensor) -> torch.Tensor:
"""タイルをVAEエンコード"""
start_latent = vae.encode(tile_tensor)
start_latent = start_latent * VAE_SCALING_FACTOR
print(f"エンコード後latent: {start_latent.shape}")
# latentを5次元に変換 [B, C, T, H, W]
if len(start_latent.shape) == 4:
start_latent = start_latent.unsqueeze(2) # T次元を追加
return start_latent
def _setup_latent_indices(self, start_latent: torch.Tensor,
latent_window_size: int) -> tuple:
"""FramePack推論のためのインデックス設定"""
B, C, T, H, W = start_latent.shape
# 1フレーム推論のためのインデックス設定
sample_num_frames = 1
latent_padding = 0
latent_padding_size = latent_padding * latent_window_size
indices = torch.arange(0, sum([1, latent_padding_size, latent_window_size, 1, 2, 16])).unsqueeze(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:
split_sizes = [1, latent_padding_size, latent_window_size, 1, 2, 16]
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フレーム推論設定
all_indices = torch.arange(0, latent_window_size).unsqueeze(0)
latent_indices = all_indices[:, -1:]
clean_latents_pre = start_latent.to(torch.float32).cpu()
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.tensor([[0]], dtype=clean_latent_indices_pre.dtype, device=clean_latent_indices_pre.device)
clean_latents = clean_latents[:, :, :1, :, :]
return latent_indices, clean_latents, clean_latent_indices
def _run_framepack_inference(self, transformer, sampler: str, denoise_strength: float,
latent_shape: tuple, cfg: float, guidance_scale: float,
shift: float, steps: int, rnd: torch.Generator,
device, base_dtype, text_embeddings: dict,
image_encoder_embeddings, latent_indices: torch.Tensor,
clean_latents: torch.Tensor, clean_latent_indices: torch.Tensor,
callback) -> torch.Tensor:
"""FramePack推論を実行"""
B, C, T, H, W = latent_shape
sample_num_frames = 1
generated_latents = sample_hunyuan(
transformer=transformer,
sampler=sampler,
initial_latent=None,
strength=denoise_strength,
width=W * 8,
height=H * 8,
frames=sample_num_frames,
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=text_embeddings["llama_vec"],
prompt_embeds_mask=text_embeddings["llama_attention_mask"],
prompt_poolers=text_embeddings["clip_l_pooler"],
negative_prompt_embeds=text_embeddings["llama_vec_n"],
negative_prompt_embeds_mask=text_embeddings["llama_attention_mask_n"],
negative_prompt_poolers=text_embeddings["clip_l_pooler_n"],
device=device,
dtype=base_dtype,
image_embeddings=image_encoder_embeddings,
latent_indices=latent_indices,
clean_latents=clean_latents,
clean_latent_indices=clean_latent_indices,
clean_latents_2x=None, # 2x, 4x latentsを無効化
clean_latent_2x_indices=None,
clean_latents_4x=None,
clean_latent_4x_indices=None,
callback=callback,
)
return generated_latents
def _decode_tile(self, vae, generated_latents: torch.Tensor, vae_tile_size: int,
vae_overlap: int, vae_temporal_size: int, vae_temporal_overlap: int) -> torch.Tensor:
"""タイルをVAEデコード"""
generated_latents = generated_latents / VAE_SCALING_FACTOR
print(f"デコード前latent: {generated_latents.shape}")
decoded_tile = VAEProcessor.tiled_decode(
vae, generated_latents,
tile_size=vae_tile_size,
overlap=vae_overlap,
temporal_size=vae_temporal_size,
temporal_overlap=vae_temporal_overlap
)
return decoded_tile
def _cleanup(self, transformer, offload_device):
"""リソースのクリーンアップ"""
transformer.to(offload_device)
mm.soft_empty_cache()
# ノードクラスマッピング
NODE_CLASS_MAPPINGS = {
"FramePackSingleFrameResizeSampler": FramePackSingleFrameResizeSampler,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"FramePackSingleFrameResizeSampler": "FramePack Single Frame Resize Sampler (50% Overlap)",
}