861 lines
41 KiB
Python
861 lines
41 KiB
Python
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)",
|
||
} |