Spaces:
Running
on
Zero
Running
on
Zero
from diffusers_helper.hf_login import login | |
import gc | |
import time | |
import os | |
import subprocess | |
import glob | |
import tempfile # 1フレーム推論のための設定 | |
import shutil # ディレクトリ削除用 | |
import cv2 # 画像処理用 | |
import numpy as np | |
from PIL import Image | |
# Hugging Face Space環境内かどうか確認 | |
IN_HF_SPACE = os.environ.get('SPACE_ID') is not None | |
# HF_HOMEの設定 | |
os.environ['HF_HOME'] = os.path.abspath(os.path.realpath(os.path.join(os.path.dirname(__file__), './hf_download'))) | |
import gradio as gr | |
import torch | |
import traceback | |
import einops | |
import safetensors.torch as sf | |
import numpy as np | |
# GPU利用可能性を追跡する変数を追加 | |
GPU_AVAILABLE = False | |
GPU_INITIALIZED = False | |
last_update_time = time.time() | |
cpu_fallback_mode = False # CPUフォールバックモードのフラグ | |
# モデルの初期化ステータスを追跡 | |
MODELS_INITIALIZED = False | |
# クライアントタイムアウト設定の強化 | |
if IN_HF_SPACE: | |
# サーバーとクライアントの両方のタイムアウト設定を拡張 | |
os.environ["GRADIO_SERVER_PORT"] = "7860" | |
os.environ["GRADIO_SERVER_NAME"] = "0.0.0.0" | |
os.environ["GRADIO_UPLOAD_TIMEOUT"] = "600" # 10分のアップロードタイムアウト | |
os.environ["GRADIO_REQUEST_TIMEOUT"] = "900" # 15分のリクエストタイムアウト | |
# メモリ使用量制限の緩和 | |
import resource | |
resource.setrlimit(resource.RLIMIT_AS, (1<<40, 1<<40)) | |
# Hugging Face Space内の場合、spacesモジュールをインポート | |
if IN_HF_SPACE: | |
try: | |
import spaces | |
print("Hugging Face Space環境内で実行中、spacesモジュールをインポートしました") | |
# GPU利用可能性をチェック | |
try: | |
GPU_AVAILABLE = torch.cuda.is_available() | |
print(f"GPU利用可能: {GPU_AVAILABLE}") | |
if GPU_AVAILABLE: | |
print(f"GPUデバイス名: {torch.cuda.get_device_name(0)}") | |
print(f"GPUメモリ: {torch.cuda.get_device_properties(0).total_memory / 1e9} GB") | |
# 小規模なGPU操作を試行し、GPUが実際に使用可能か確認 | |
try: | |
test_tensor = torch.zeros(1, device='cuda') | |
test_tensor = test_tensor + 1 | |
del test_tensor | |
print("GPUテスト操作に成功しました") | |
except Exception as e: | |
print(f"GPUテスト操作でエラーが発生しました: {e}") | |
GPU_AVAILABLE = False | |
cpu_fallback_mode = True | |
print("CPUフォールバックモードに設定します") | |
else: | |
print("警告: CUDAが利用可能と報告されていますが、GPUデバイスが検出されませんでした") | |
cpu_fallback_mode = True | |
except Exception as e: | |
GPU_AVAILABLE = False | |
cpu_fallback_mode = True | |
print(f"GPU確認中にエラーが発生しました: {e}") | |
print("CPUモードで実行します") | |
except ImportError: | |
print("spacesモジュールのインポートに失敗しました。Hugging Face Space環境外かもしれません") | |
GPU_AVAILABLE = torch.cuda.is_available() | |
if not GPU_AVAILABLE: | |
cpu_fallback_mode = True | |
# 初回ロード時のチェック関数 | |
def is_first_time_load(): | |
global GPU_INITIALIZED | |
if not GPU_INITIALIZED: | |
GPU_INITIALIZED = True | |
return True | |
return False | |
# GPU制限を超えたかどうかを確認する関数 | |
def check_gpu_quota_exceeded(): | |
"""GPU使用制限を超えたかどうかを確認""" | |
global cpu_fallback_mode | |
# すでにCPUモードならチェック不要 | |
if cpu_fallback_mode or not GPU_AVAILABLE: | |
return True | |
if not IN_HF_SPACE: | |
return False | |
try: | |
import requests | |
try: | |
response = requests.get("http://localhost:7860/api/v1/spaces/usage", timeout=1) | |
if response.status_code == 200: | |
try: | |
data = response.json() | |
if data.get("gpu", {}).get("quota_exceeded", False): | |
print("GPU使用制限に達しています。") | |
cpu_fallback_mode = True | |
return True | |
except ValueError as json_err: | |
print(f"JSON解析エラー: {json_err}") | |
# JSONデコードエラーが続く場合は、CPU動作にフォールバック | |
return False | |
else: | |
print(f"APIエンドポイントから不正なステータスコード: {response.status_code}") | |
except requests.exceptions.RequestException as req_err: | |
print(f"APIリクエスト中にエラー: {req_err}") | |
except Exception as e: | |
print(f"GPU使用制限確認中にエラー: {e}") | |
return False | |
# 条件付きインポート(CPUモードでのエラーを回避するため) | |
try: | |
from diffusers import AutoencoderKLHunyuanVideo | |
from transformers import LlamaModel, CLIPTextModel, LlamaTokenizerFast, CLIPTokenizer | |
from diffusers_helper.hunyuan import encode_prompt_conds, vae_decode, vae_encode, vae_decode_fake | |
from diffusers_helper.utils import ( | |
save_bcthw_as_mp4, | |
crop_or_pad_yield_mask, | |
soft_append_bcthw, | |
resize_and_center_crop, | |
state_dict_weighted_merge, | |
state_dict_offset_merge, | |
generate_timestamp, | |
) | |
from diffusers_helper.models.hunyuan_video_packed import HunyuanVideoTransformer3DModelPacked | |
from diffusers_helper.pipelines.k_diffusion_hunyuan import sample_hunyuan | |
from diffusers_helper.memory import ( | |
cpu, | |
gpu, | |
get_cuda_free_memory_gb, | |
move_model_to_device_with_memory_preservation, | |
offload_model_from_device_for_memory_preservation, | |
fake_diffusers_current_device, | |
DynamicSwapInstaller, | |
unload_complete_models, | |
load_model_as_complete, | |
IN_HF_SPACE as MEMORY_IN_HF_SPACE | |
) | |
from diffusers_helper.thread_utils import AsyncStream, async_run | |
from diffusers_helper.gradio.progress_bar import make_progress_bar_css, make_progress_bar_html | |
from transformers import SiglipImageProcessor, SiglipVisionModel | |
from diffusers_helper.clip_vision import hf_clip_vision_encode | |
from diffusers_helper.bucket_tools import find_nearest_bucket | |
print("基本的なディフューザーモジュールを正常にインポートしました") | |
except ImportError as e: | |
print(f"一部の基本モジュールのインポートに失敗しました: {e}") | |
# ダミー関数を定義 | |
class AsyncStream: | |
def __init__(self): | |
self.input_queue = MockQueue() | |
self.output_queue = MockQueue() | |
class MockQueue: | |
def __init__(self): | |
self.items = [] | |
def push(self, item): | |
self.items.append(item) | |
def top(self): | |
return self.items[-1] if self.items else None | |
def next(self): | |
return self.items.pop(0) if self.items else ("end", None) | |
def async_run(*args, **kwargs): | |
pass | |
def make_progress_bar_css(): | |
return "" | |
def make_progress_bar_html(percentage, hint): | |
return f"<div>{percentage}% - {hint}</div>" | |
# GPU使用に必要なモジュールのインポートを試みる(可能な場合) | |
try: | |
from utils.lora_utils import merge_lora_to_state_dict | |
from utils.fp8_optimization_utils import optimize_state_dict_with_fp8, apply_fp8_monkey_patch | |
print("LoRAとFP8最適化モジュールを正常にインポートしました") | |
except ImportError as e: | |
print(f"一部のモジュールのインポートに失敗しました: {e}") | |
# ダミー関数を定義 | |
def merge_lora_to_state_dict(state_dict, lora_file, lora_multiplier, device=None): | |
print("Warning: LoRA適用機能が利用できません") | |
return state_dict | |
def optimize_state_dict_with_fp8(state_dict, device, target_keys, exclude_keys, move_to_device=False): | |
print("Warning: FP8最適化機能が利用できません") | |
return state_dict | |
def apply_fp8_monkey_patch(model, state_dict, use_scaled_mm=False): | |
print("Warning: FP8 monkey patch機能が利用できません") | |
pass | |
outputs_folder = './outputs/' | |
os.makedirs(outputs_folder, exist_ok=True) | |
# 追加: 指定された解像度リスト | |
NEW_RESOLUTIONS = [ | |
(416, 960), (448, 864), (480, 832), (512, 768), (544, 704), | |
(576, 672), (608, 640), (640, 608), (672, 576), (704, 544), | |
(768, 512), (832, 480), (864, 448), (960, 416), (640, 640), | |
] | |
# VRAMを安全に確認する関数 | |
def get_safe_vram_size(): | |
"""利用可能なVRAMを安全に確認する""" | |
try: | |
if torch.cuda.is_available() and not cpu_fallback_mode: | |
free_mem_gb = get_cuda_free_memory_gb(gpu) | |
print(f'空きVRAM {free_mem_gb} GB') | |
return free_mem_gb | |
else: | |
free_mem_gb = 6.0 # デフォルト値 | |
print("CUDAが利用できないか、CPUフォールバックモードです。デフォルトのメモリ設定を使用します") | |
return free_mem_gb | |
except Exception as e: | |
free_mem_gb = 6.0 # デフォルト値 | |
print(f"CUDAメモリ取得中にエラーが発生しました: {e}、デフォルトのメモリ設定を使用します") | |
return free_mem_gb | |
# メモリ設定を初期化 | |
if not IN_HF_SPACE: | |
# 非Spaces環境でのメモリ設定 | |
free_mem_gb = get_safe_vram_size() | |
high_vram = free_mem_gb > 60 | |
print(f'高VRAM モード: {high_vram}') | |
else: | |
# Spaces環境でのメモリ設定 | |
print("Spaces環境でデフォルトのメモリ設定を使用します") | |
try: | |
if GPU_AVAILABLE and not cpu_fallback_mode: | |
free_mem_gb = torch.cuda.get_device_properties(0).total_memory / 1e9 * 0.9 # GPUメモリの90%を使用 | |
high_vram = free_mem_gb > 10 # より保守的な条件 | |
else: | |
free_mem_gb = 6.0 # デフォルト値 | |
high_vram = False | |
except Exception as e: | |
print(f"GPUメモリ取得中にエラーが発生しました: {e}") | |
free_mem_gb = 6.0 # デフォルト値 | |
high_vram = False | |
print(f'GPUメモリ: {free_mem_gb:.2f} GB, 高VRAMモード: {high_vram}') | |
# modelsグローバル変数でモデル参照を保存 | |
models = {} | |
stream = None | |
# 市松模様を作成する関数 | |
def create_checkerboard(width, height, cell_size): | |
"""紫と黄色の市松模様を作成する""" | |
# 市松模様のサイズを計算 | |
rows = int(np.ceil(height / cell_size)) | |
cols = int(np.ceil(width / cell_size)) | |
# 紫と黄色の色を定義 | |
purple = (128, 0, 128) # RGB for purple | |
yellow = (255, 255, 0) # RGB for yellow | |
# 空の画像を作成 | |
checkerboard = np.zeros((rows * cell_size, cols * cell_size, 3), dtype=np.uint8) | |
# 市松模様を埋める | |
for i in range(rows): | |
for j in range(cols): | |
color = purple if (i + j) % 2 == 0 else yellow | |
y_start = i * cell_size | |
y_end = (i + 1) * cell_size | |
x_start = j * cell_size | |
x_end = (j + 1) * cell_size | |
checkerboard[y_start:y_end, x_start:x_end] = color | |
# 元の画像サイズにリサイズ | |
return checkerboard[:height, :width] | |
# ImageMaskからの画像とマスクを処理する関数 | |
def process_image_mask(image_mask_dict): | |
"""ImageMaskからの画像とマスクを処理(サイズ制限あり)""" | |
if image_mask_dict is None or not isinstance(image_mask_dict, dict): | |
return None | |
# Gradio ImageMask の新フォーマット | |
background = image_mask_dict.get("background") | |
layers = image_mask_dict.get("layers") | |
if background is None: | |
return None | |
# 画像サイズをチェックして制限(大きすぎる場合はリサイズ) | |
if isinstance(background, Image.Image): | |
w, h = background.size | |
max_size = 1024 # 最大サイズを1024pxに制限 | |
if w > max_size or h > max_size: | |
# 長辺が1024になるようにリサイズ | |
ratio = max_size / max(w, h) | |
new_w, new_h = int(w * ratio), int(h * ratio) | |
background = background.resize((new_w, new_h), Image.LANCZOS) | |
print(f"画像サイズを制限しました: {w}x{h} → {new_w}x{new_h}") | |
# レイヤーも同様にリサイズ | |
if layers and len(layers) > 0: | |
new_layers = [] | |
for layer in layers: | |
if isinstance(layer, Image.Image): | |
layer = layer.resize((new_w, new_h), Image.LANCZOS) | |
new_layers.append(layer) | |
layers = new_layers | |
image_mask_dict["layers"] = layers | |
image_mask_dict["background"] = background | |
# ---- 1) Drop alpha from background ---- | |
if isinstance(background, Image.Image) and background.mode == "RGBA": | |
background = background.convert("RGB") | |
img_array = np.array(background) | |
# safety-net: if it's still 4-channel, just slice | |
if img_array.ndim == 3 and img_array.shape[2] == 4: | |
img_array = img_array[..., :3] | |
# ---- 2) マスクがある場合のみマスク処理 ---- | |
if layers and len(layers) > 0: | |
layer = layers[0] | |
if isinstance(layer, Image.Image) and layer.mode == "RGBA": | |
layer = layer.convert("RGB") | |
mask_array = np.array(layer) | |
if mask_array.ndim == 3 and mask_array.shape[2] == 4: | |
mask_array = mask_array[..., :3] | |
# convert to gray + binary | |
if mask_array.ndim == 3: | |
mask_gray = cv2.cvtColor(mask_array, cv2.COLOR_RGB2GRAY) | |
else: | |
mask_gray = mask_array | |
_, binary_mask = cv2.threshold(mask_gray, 1, 255, cv2.THRESH_BINARY) | |
# 市松模様合成ロジック | |
total_pixels = img_array.shape[0] * img_array.shape[1] | |
cell_size = max(int(np.sqrt(total_pixels) / 20), 10) | |
checkerboard = create_checkerboard(img_array.shape[1], img_array.shape[0], cell_size) | |
result = img_array.copy() | |
binary_mask_3ch = np.stack([binary_mask]*3, axis=2) // 255 | |
for c in range(3): | |
result[..., c] = result[..., c] * (1 - binary_mask_3ch[..., c]) + checkerboard[..., c] * binary_mask_3ch[..., c] | |
return result.astype(np.uint8) | |
else: | |
# マスクがない場合は元の画像をそのまま返す | |
return img_array | |
# 最も近い解像度を見つける関数 | |
def find_nearest_resolution(width, height): | |
"""最適な解像度を選択する関数(正方形入力に対する改善版)""" | |
min_diff = float('inf') | |
best_res = None | |
aspect_ratio = width / height | |
# 入力がほぼ正方形の場合、正方形の解像度を優先する | |
# アスペクト比が0.95〜1.05の範囲なら正方形と見なす | |
is_square_input = 0.95 <= aspect_ratio <= 1.05 | |
for res_h, res_w in NEW_RESOLUTIONS: | |
# 解像度のアスペクト比を計算 | |
res_aspect = res_w / res_h | |
# 正方形入力で、この解像度も正方形なら、優先的に選択 | |
if is_square_input and res_w == res_h: | |
return (res_h, res_w) # 正方形の解像度を即座に返す | |
# アスペクト比の差を計算 | |
aspect_diff = abs(res_aspect - aspect_ratio) | |
# 総ピクセル数の差を計算 | |
pixels_orig = width * height | |
pixels_res = res_w * res_h | |
pixel_diff = abs(pixels_res - pixels_orig) | |
# 重み付けした差分(アスペクト比の差に重きを置く) | |
total_diff = aspect_diff * 10000 + pixel_diff * 0.01 | |
if total_diff < min_diff: | |
min_diff = total_diff | |
best_res = (res_h, res_w) | |
return best_res | |
# 一時ディレクトリ管理関数 | |
def create_temp_directory(): | |
"""一時ディレクトリを作成して、パスを返す""" | |
temp_dir = tempfile.mkdtemp(prefix="hunyuan_temp_") | |
print(f"一時ディレクトリを作成しました: {temp_dir}") | |
return temp_dir | |
def cleanup_temp_files(temp_dir): | |
"""処理後に一時ファイルを削除する""" | |
if temp_dir and os.path.exists(temp_dir): | |
try: | |
shutil.rmtree(temp_dir) | |
print(f"一時ディレクトリを削除しました: {temp_dir}") | |
except Exception as e: | |
print(f"一時ディレクトリの削除に失敗しました: {e}") | |
# mp4からffmpegでPNGフレームを抽出する関数(一時フォルダ使用) | |
def extract_frames_from_mp4(mp4_path, temp_dir, job_id): | |
"""MP4から画像フレームを抽出し、一時ディレクトリに保存""" | |
# フレーム出力用のフォルダ作成 | |
frames_dir = os.path.join(temp_dir, "frames", job_id) | |
os.makedirs(frames_dir, exist_ok=True) | |
# ffmpegコマンドでmp4からフレームを抽出 | |
cmd = [ | |
'ffmpeg', | |
'-i', mp4_path, | |
'-vf', 'fps=30', | |
f'{frames_dir}/frame_%04d.png', | |
'-hide_banner', | |
'-loglevel', 'error' | |
] | |
try: | |
subprocess.run(cmd, check=True) | |
# 抽出されたフレームのリストを返す | |
frames = sorted(glob.glob(f'{frames_dir}/frame_*.png')) | |
return frames | |
except subprocess.CalledProcessError as e: | |
print(f"Error extracting frames from {mp4_path}: {e}") | |
return [] | |
# 一時ファイルを使用するmp4保存関数 | |
def save_bcthw_as_mp4_with_frames(bcthw, temp_dir, job_id, fps=30, crf=16): | |
"""BCTHWテンソルをMP4として保存し、フレームを抽出する(一時ディレクトリ使用)""" | |
# 一時ディレクトリにMP4を保存 | |
output_path = os.path.join(temp_dir, f"{job_id}.mp4") | |
# 元の関数を呼び出してmp4を保存 | |
save_bcthw_as_mp4(bcthw, output_path, fps, crf) | |
# フレームを抽出 | |
frames = extract_frames_from_mp4(output_path, temp_dir, job_id) | |
return output_path, frames | |
# GPUの状態を確認する関数 | |
def check_gpu_status(): | |
"""GPUが使用可能かつクォータ内かを確認""" | |
global GPU_AVAILABLE, cpu_fallback_mode | |
# GPU使用不可の場合はCPUモードのまま | |
if not GPU_AVAILABLE: | |
cpu_fallback_mode = True | |
return False | |
# GPU使用制限を超えた場合はCPUモードに切り替え | |
if check_gpu_quota_exceeded(): | |
print("GPU使用制限を超えました。CPUモードに切り替えます。") | |
cpu_fallback_mode = True | |
return False | |
# GPUが使用可能でクォータ内 | |
return True | |
# モデルロード関数(GPU対応) | |
def load_models(): | |
"""各種モデルを読み込む(GPU対応)""" | |
global models, MODELS_INITIALIZED, cpu_fallback_mode | |
# すでに初期化済みの場合はそのまま返す | |
if MODELS_INITIALIZED and models: | |
return models | |
print("モデルを読み込みます...") | |
try: | |
# GPU状態を確認 | |
gpu_ok = check_gpu_status() | |
device = gpu if gpu_ok and not cpu_fallback_mode else cpu | |
dtype = torch.float16 if gpu_ok and not cpu_fallback_mode else torch.float32 | |
print(f"モデルをロード: デバイス={device}, データ型={dtype}") | |
# モデル読み込み | |
text_encoder = LlamaModel.from_pretrained( | |
"hunyuanvideo-community/HunyuanVideo", subfolder="text_encoder", torch_dtype=dtype | |
).cpu() | |
text_encoder_2 = CLIPTextModel.from_pretrained( | |
"hunyuanvideo-community/HunyuanVideo", subfolder="text_encoder_2", torch_dtype=dtype | |
).cpu() | |
tokenizer = LlamaTokenizerFast.from_pretrained("hunyuanvideo-community/HunyuanVideo", subfolder="tokenizer") | |
tokenizer_2 = CLIPTokenizer.from_pretrained("hunyuanvideo-community/HunyuanVideo", subfolder="tokenizer_2") | |
vae = AutoencoderKLHunyuanVideo.from_pretrained( | |
"hunyuanvideo-community/HunyuanVideo", subfolder="vae", torch_dtype=dtype | |
).cpu() | |
feature_extractor = SiglipImageProcessor.from_pretrained("lllyasviel/flux_redux_bfl", subfolder="feature_extractor") | |
image_encoder = SiglipVisionModel.from_pretrained( | |
"lllyasviel/flux_redux_bfl", subfolder="image_encoder", torch_dtype=dtype | |
).cpu() | |
print("Transformerモデルを読み込み中...") | |
# CPU対応: CPUモードではbfloat16ではなくfloat32を使用 | |
transformer_dtype = torch.bfloat16 if gpu_ok and not cpu_fallback_mode else torch.float32 | |
transformer = HunyuanVideoTransformer3DModelPacked.from_pretrained( | |
"lllyasviel/FramePackI2V_HY", torch_dtype=transformer_dtype | |
).cpu() | |
transformer.eval() | |
transformer.high_quality_fp32_output_for_inference = True | |
print("transformer.high_quality_fp32_output_for_inference = True") | |
if gpu_ok and not cpu_fallback_mode: | |
transformer.to(dtype=torch.bfloat16) | |
transformer.requires_grad_(False) | |
vae.eval() | |
text_encoder.eval() | |
text_encoder_2.eval() | |
image_encoder.eval() | |
if not high_vram or cpu_fallback_mode: | |
vae.enable_slicing() | |
vae.enable_tiling() | |
# CPUモードでは精度を下げない | |
if gpu_ok and not cpu_fallback_mode: | |
vae.to(dtype=torch.float16) | |
image_encoder.to(dtype=torch.float16) | |
text_encoder.to(dtype=torch.float16) | |
text_encoder_2.to(dtype=torch.float16) | |
vae.requires_grad_(False) | |
text_encoder.requires_grad_(False) | |
text_encoder_2.requires_grad_(False) | |
image_encoder.requires_grad_(False) | |
# GPUに移動(可能な場合のみ) | |
if gpu_ok and not cpu_fallback_mode: | |
if not high_vram: | |
# DynamicSwapInstallerはhuggingfaceのenable_sequential_offloadと同じですが3倍高速です | |
DynamicSwapInstaller.install_model(text_encoder, device=gpu) | |
else: | |
text_encoder.to(gpu) | |
text_encoder_2.to(gpu) | |
image_encoder.to(gpu) | |
vae.to(gpu) | |
print("すべてのモデルの読み込みが完了しました") | |
models = { | |
'transformer': transformer, | |
'text_encoder': text_encoder, | |
'text_encoder_2': text_encoder_2, | |
'tokenizer': tokenizer, | |
'tokenizer_2': tokenizer_2, | |
'vae': vae, | |
'feature_extractor': feature_extractor, | |
'image_encoder': image_encoder, | |
} | |
MODELS_INITIALIZED = True | |
return models | |
except Exception as e: | |
# GPU関連のエラーを検出 | |
if "CUDA" in str(e) or "GPU" in str(e) or "nvidia" in str(e).lower(): | |
print(f"GPU関連のエラーが発生しました: {e}") | |
print("CPUモードにフォールバックします") | |
cpu_fallback_mode = True | |
# CPUモードで再度試行 | |
return load_models_cpu() | |
else: | |
print(f"モデル読み込み中にエラーが発生しました: {e}") | |
traceback.print_exc() | |
return {} | |
# CPUのみを使用したモデルロード関数 | |
def load_models_cpu(): | |
"""CPUのみを使用してモデルを読み込む""" | |
global models, MODELS_INITIALIZED | |
print("CPUモードでモデルを読み込みます...") | |
try: | |
# CPUモード用の設定 | |
device = cpu | |
dtype = torch.float32 | |
# モデル読み込み(CPU最適化版) | |
text_encoder = LlamaModel.from_pretrained( | |
"hunyuanvideo-community/HunyuanVideo", subfolder="text_encoder", torch_dtype=dtype | |
).cpu() | |
text_encoder_2 = CLIPTextModel.from_pretrained( | |
"hunyuanvideo-community/HunyuanVideo", subfolder="text_encoder_2", torch_dtype=dtype | |
).cpu() | |
tokenizer = LlamaTokenizerFast.from_pretrained("hunyuanvideo-community/HunyuanVideo", subfolder="tokenizer") | |
tokenizer_2 = CLIPTokenizer.from_pretrained("hunyuanvideo-community/HunyuanVideo", subfolder="tokenizer_2") | |
vae = AutoencoderKLHunyuanVideo.from_pretrained( | |
"hunyuanvideo-community/HunyuanVideo", subfolder="vae", torch_dtype=dtype | |
).cpu() | |
feature_extractor = SiglipImageProcessor.from_pretrained("lllyasviel/flux_redux_bfl", subfolder="feature_extractor") | |
image_encoder = SiglipVisionModel.from_pretrained( | |
"lllyasviel/flux_redux_bfl", subfolder="image_encoder", torch_dtype=dtype | |
).cpu() | |
print("CPUモードでTransformerモデルを読み込み中...") | |
transformer = HunyuanVideoTransformer3DModelPacked.from_pretrained( | |
"lllyasviel/FramePackI2V_HY", torch_dtype=torch.float32 | |
).cpu() | |
transformer.eval() | |
transformer.high_quality_fp32_output_for_inference = True | |
transformer.requires_grad_(False) | |
vae.eval() | |
text_encoder.eval() | |
text_encoder_2.eval() | |
image_encoder.eval() | |
# CPUモードでは常にスライシングとタイリングを有効化 | |
vae.enable_slicing() | |
vae.enable_tiling() | |
vae.requires_grad_(False) | |
text_encoder.requires_grad_(False) | |
text_encoder_2.requires_grad_(False) | |
image_encoder.requires_grad_(False) | |
print("CPUモードですべてのモデルの読み込みが完了しました") | |
models = { | |
'transformer': transformer, | |
'text_encoder': text_encoder, | |
'text_encoder_2': text_encoder_2, | |
'tokenizer': tokenizer, | |
'tokenizer_2': tokenizer_2, | |
'vae': vae, | |
'feature_extractor': feature_extractor, | |
'image_encoder': image_encoder, | |
} | |
MODELS_INITIALIZED = True | |
return models | |
except Exception as e: | |
print(f"CPUモードでのモデル読み込み中にエラーが発生しました: {e}") | |
traceback.print_exc() | |
return {} | |
# モデル取得・初期化関数 | |
def get_models(): | |
"""モデルを取得する(必要に応じて読み込む)""" | |
global models, GPU_INITIALIZED, MODELS_INITIALIZED, cpu_fallback_mode | |
if not models or not MODELS_INITIALIZED: | |
try: | |
# GPU使用可能性を再確認 | |
if check_gpu_status(): | |
# GPU対応モードでモデル読み込み | |
try: | |
print("GPU対応モードでモデル読み込みを試みます") | |
models = load_models() | |
GPU_INITIALIZED = True | |
except Exception as e: | |
if "CUDA" in str(e) or "GPU" in str(e) or "ZeroGPU quota exceeded" in str(e) or "nvidia" in str(e).lower(): | |
print(f"GPU対応モデル読み込みに失敗しました: {e}") | |
print("CPUモードにフォールバックします") | |
cpu_fallback_mode = True | |
models = load_models_cpu() | |
else: | |
print(f"モデル読み込み中に予期せぬエラーが発生しました: {e}") | |
traceback.print_exc() | |
# エラーを再スローせず、空のモデル辞書を返す | |
return {} | |
else: | |
# CPUモードでモデル読み込み | |
print("CPUモードでモデル読み込みを実行します") | |
cpu_fallback_mode = True | |
models = load_models_cpu() | |
except Exception as e: | |
print(f"モデル取得中にエラーが発生しました: {e}") | |
traceback.print_exc() | |
# エラーを再スローせず、空のモデル辞書を返す | |
return {} | |
return models | |
# 処理ワーカー関数(GPU対応版) | |
def worker_with_temp_files( | |
image_mask_dict, | |
prompt, | |
n_prompt, | |
seed, | |
steps, | |
cfg, | |
gs, | |
rs, | |
gpu_memory_preservation, | |
use_teacache, | |
mp4_crf, | |
lora_file, | |
lora_multiplier, | |
fp8_optimization, | |
): | |
global last_update_time, cpu_fallback_mode | |
last_update_time = time.time() | |
# GPU状態を最初にチェック | |
check_gpu_status() | |
# マスク画像の処理 | |
input_image = process_image_mask(image_mask_dict) | |
if input_image is None: | |
error_msg = "マスク画像の処理に失敗しました。画像をアップロードして、マスクを描画してください。" | |
print(error_msg) | |
stream.output_queue.push(("error", error_msg)) | |
stream.output_queue.push(("end", None)) | |
return | |
# 一時ディレクトリの作成 | |
temp_dir = create_temp_directory() | |
# 一時ディレクトリ内にサブディレクトリを作成 | |
os.makedirs(os.path.join(temp_dir, "frames"), exist_ok=True) | |
# 1フレーム推論用の固定設定 | |
total_second_length = 1.0 | |
latent_window_size = 9 | |
total_latent_sections = 1 | |
latent_paddings = [0] | |
job_id = generate_timestamp() | |
stream.output_queue.push(("progress", (None, "", make_progress_bar_html(0, "開始中 ...")))) | |
try: | |
# モデルを取得 | |
models = get_models() | |
if not models: | |
error_msg = "モデルの読み込みに失敗しました。詳細はログを確認してください。" | |
print(error_msg) | |
stream.output_queue.push(("error", error_msg)) | |
stream.output_queue.push(("end", None)) | |
cleanup_temp_files(temp_dir) | |
return | |
transformer = models['transformer'] | |
text_encoder = models['text_encoder'] | |
text_encoder_2 = models['text_encoder_2'] | |
tokenizer = models['tokenizer'] | |
tokenizer_2 = models['tokenizer_2'] | |
vae = models['vae'] | |
feature_extractor = models['feature_extractor'] | |
image_encoder = models['image_encoder'] | |
# LoRAファイルの適用 | |
if not cpu_fallback_mode and lora_file is not None and os.path.exists(lora_file): | |
try: | |
print(f"LoRAファイル {os.path.basename(lora_file)} をマージします...") | |
state_dict = transformer.state_dict() | |
state_dict = merge_lora_to_state_dict(state_dict, lora_file, lora_multiplier, device=gpu) | |
if fp8_optimization and not cpu_fallback_mode: | |
TARGET_KEYS = ["transformer_blocks", "single_transformer_blocks"] | |
EXCLUDE_KEYS = ["norm"] # Exclude norm layers from FP8 | |
print("FP8最適化を適用します") | |
state_dict = optimize_state_dict_with_fp8(state_dict, gpu, TARGET_KEYS, EXCLUDE_KEYS, move_to_device=False) | |
apply_fp8_monkey_patch(transformer, state_dict, use_scaled_mm=False) | |
gc.collect() | |
info = transformer.load_state_dict(state_dict, strict=True, assign=True) | |
print(f"LoRAと/またはFP8最適化を適用しました: {info}") | |
except Exception as e: | |
print(f"LoRA適用中にエラーが発生しました: {e}") | |
# エラー発生時も処理を継続 | |
elif cpu_fallback_mode and lora_file is not None and os.path.exists(lora_file): | |
print("CPUモードではLoRAはサポートされていません") | |
# Clean GPU (GPU使用時のみ) | |
if not high_vram and not cpu_fallback_mode: | |
unload_complete_models(text_encoder, text_encoder_2, image_encoder, vae, transformer) | |
# Text encoding | |
stream.output_queue.push(("progress", (None, "", make_progress_bar_html(0, "テキストエンコーディング中 ...")))) | |
# 途中でGPU制限を超えた場合のチェック | |
if check_gpu_quota_exceeded(): | |
cpu_fallback_mode = True | |
print("テキストエンコード中にGPU制限を超えました。CPUモードに切り替えます。") | |
# モデルを再度取得 | |
models = get_models() | |
transformer = models['transformer'] | |
text_encoder = models['text_encoder'] | |
text_encoder_2 = models['text_encoder_2'] | |
tokenizer = models['tokenizer'] | |
tokenizer_2 = models['tokenizer_2'] | |
vae = models['vae'] | |
feature_extractor = models['feature_extractor'] | |
image_encoder = models['image_encoder'] | |
# GPU/CPU選択ロジック | |
if not cpu_fallback_mode: | |
target_device = gpu | |
if not high_vram: | |
# since we only encode one text - that is one model move and one encode, offload is same time consumption since it is also one load and one encode. | |
fake_diffusers_current_device(text_encoder, target_device) | |
load_model_as_complete(text_encoder_2, target_device=target_device) | |
# テキストエンコーディングを実行 | |
llama_vec, clip_l_pooler = encode_prompt_conds(prompt, text_encoder, text_encoder_2, tokenizer, tokenizer_2) | |
if cfg == 1: | |
llama_vec_n, clip_l_pooler_n = torch.zeros_like(llama_vec), torch.zeros_like(clip_l_pooler) | |
else: | |
llama_vec_n, clip_l_pooler_n = encode_prompt_conds(n_prompt, text_encoder, text_encoder_2, tokenizer, tokenizer_2) | |
else: | |
# CPUモードでのテキストエンコーディング | |
target_device = cpu | |
# テキストエンコーダーをCPUに設定 | |
text_encoder = text_encoder.to(cpu) | |
text_encoder_2 = text_encoder_2.to(cpu) | |
# テキストエンコーディングを実行(CPU上で) | |
llama_vec, clip_l_pooler = encode_prompt_conds(prompt, text_encoder, text_encoder_2, tokenizer, tokenizer_2) | |
if cfg == 1: | |
llama_vec_n, clip_l_pooler_n = torch.zeros_like(llama_vec), torch.zeros_like(clip_l_pooler) | |
else: | |
llama_vec_n, clip_l_pooler_n = encode_prompt_conds(n_prompt, text_encoder, text_encoder_2, tokenizer, tokenizer_2) | |
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) | |
# Processing input image | |
stream.output_queue.push(("progress", (None, "", make_progress_bar_html(0, "画像処理中 ...")))) | |
H, W, C = input_image.shape | |
# 元の画像サイズを保存 | |
original_height, original_width = H, W | |
# 最適な解像度を選択 | |
target_height, target_width = find_nearest_resolution(W, H) | |
print(f"オリジナルサイズ: {W}x{H}, 選択された解像度: {target_width}x{target_height}") | |
input_image_np = resize_and_center_crop(input_image, target_width=target_width, target_height=target_height) | |
# 一時ディレクトリに入力画像を保存 | |
input_image_path = os.path.join(temp_dir, f"{job_id}_input.png") | |
Image.fromarray(input_image_np).save(input_image_path) | |
input_image_pt = torch.from_numpy(input_image_np).float() / 127.5 - 1 | |
input_image_pt = input_image_pt.permute(2, 0, 1)[None, :, None] | |
# VAE encoding | |
stream.output_queue.push(("progress", (None, "", make_progress_bar_html(0, "VAEエンコーディング中 ...")))) | |
# 途中でGPU制限を超えた場合のチェック | |
if check_gpu_quota_exceeded(): | |
cpu_fallback_mode = True | |
print("VAEエンコード中にGPU制限を超えました。CPUモードに切り替えます。") | |
# モデルを再度取得 | |
models = get_models() | |
vae = models['vae'] | |
if not cpu_fallback_mode and not high_vram: | |
load_model_as_complete(vae, target_device=target_device) | |
# VAEエンコーディング(CPU/GPUモードに応じて) | |
if not cpu_fallback_mode: | |
start_latent = vae_encode(input_image_pt, vae) | |
else: | |
# CPUモードでのVAEエンコーディング | |
vae = vae.to(cpu) | |
# CPU用に精度やバッチサイズを調整 | |
input_image_pt_cpu = input_image_pt.to(cpu, dtype=torch.float32) | |
start_latent = vae_encode(input_image_pt_cpu, vae) | |
# CLIP Vision | |
stream.output_queue.push(("progress", (None, "", make_progress_bar_html(0, "CLIP Visionエンコーディング中 ...")))) | |
# 途中でGPU制限を超えた場合のチェック | |
if check_gpu_quota_exceeded(): | |
cpu_fallback_mode = True | |
print("CLIP Vision処理中にGPU制限を超えました。CPUモードに切り替えます。") | |
# モデルを再度取得 | |
models = get_models() | |
image_encoder = models['image_encoder'] | |
if not cpu_fallback_mode and not high_vram: | |
load_model_as_complete(image_encoder, target_device=target_device) | |
# CLIP Visionエンコーディング(CPU/GPUモードに応じて) | |
if not cpu_fallback_mode: | |
image_encoder_output = hf_clip_vision_encode(input_image_np, feature_extractor, image_encoder) | |
else: | |
# CPUモードでのCLIP Visionエンコーディング | |
image_encoder = image_encoder.to(cpu) | |
image_encoder_output = hf_clip_vision_encode(input_image_np, feature_extractor, image_encoder) | |
image_encoder_last_hidden_state = image_encoder_output.last_hidden_state | |
# データ型の変換(CPU/GPUモードに応じて) | |
if not cpu_fallback_mode: | |
# GPUモードでの型変換 | |
llama_vec = llama_vec.to(torch.bfloat16) | |
llama_vec_n = llama_vec_n.to(torch.bfloat16) | |
clip_l_pooler = clip_l_pooler.to(torch.bfloat16) | |
clip_l_pooler_n = clip_l_pooler_n.to(torch.bfloat16) | |
image_encoder_last_hidden_state = image_encoder_last_hidden_state.to(torch.bfloat16) | |
else: | |
# CPUモードではfloat32を維持 | |
llama_vec = llama_vec.to(torch.float32) | |
llama_vec_n = llama_vec_n.to(torch.float32) | |
clip_l_pooler = clip_l_pooler.to(torch.float32) | |
clip_l_pooler_n = clip_l_pooler_n.to(torch.float32) | |
image_encoder_last_hidden_state = image_encoder_last_hidden_state.to(torch.float32) | |
# Transformerモデルの準備 | |
stream.output_queue.push(("progress", (None, "", make_progress_bar_html(0, "Transformerモデル準備中 ...")))) | |
# 途中でGPU制限を超えた場合のチェック | |
if check_gpu_quota_exceeded(): | |
cpu_fallback_mode = True | |
print("Transformer準備中にGPU制限を超えました。CPUモードに切り替えます。") | |
# モデルを再度取得 | |
models = get_models() | |
transformer = models['transformer'] | |
if not cpu_fallback_mode: | |
# GPUモード | |
if not high_vram: | |
if IN_HF_SPACE: | |
# Hugging Face Space環境でのメモリ管理 | |
move_model_to_device_with_memory_preservation( | |
transformer, target_device=gpu, preserved_memory_gb=gpu_memory_preservation | |
) | |
else: | |
# 通常環境でのメモリ管理 | |
DynamicSwapInstaller.install_model(transformer, device=gpu) | |
else: | |
transformer.to(gpu) | |
else: | |
# CPUモード | |
transformer = transformer.to(cpu) | |
# Sampling | |
stream.output_queue.push(("progress", (None, "", make_progress_bar_html(0, "サンプリング開始 ...")))) | |
rnd = torch.Generator("cpu").manual_seed(seed) | |
# 1フレーム推論のための設定 | |
num_frames = 1 | |
print(f"1フレーム推論モード: num_frames = {num_frames}") | |
# CPU/GPUモードに応じた設定 | |
device_to_use = cpu if cpu_fallback_mode else gpu | |
dtype_to_use = torch.float32 if cpu_fallback_mode else torch.bfloat16 | |
history_latents = torch.zeros(size=(1, 16, 1 + 2 + 16, target_height // 8, target_width // 8), dtype=torch.float32).cpu() | |
history_pixels = None | |
total_generated_latent_frames = 0 | |
# 1フレーム推論処理 | |
for latent_padding in latent_paddings: | |
is_last_section = latent_padding == 0 | |
latent_padding_size = latent_padding * latent_window_size | |
if stream.input_queue.top() == "end": | |
stream.output_queue.push(("end", None)) | |
cleanup_temp_files(temp_dir) # 終了時に一時ファイル削除 | |
return | |
print(f"latent_padding_size = {latent_padding_size}, is_last_section = {is_last_section}") | |
indices = torch.arange(0, sum([1, latent_padding_size, latent_window_size, 1, 2, 16])).unsqueeze(0) | |
( | |
clean_latent_indices_pre, | |
blank_indices, | |
latent_indices, | |
clean_latent_indices_post, | |
clean_latent_2x_indices, | |
clean_latent_4x_indices, | |
) = indices.split([1, latent_padding_size, latent_window_size, 1, 2, 16], dim=1) | |
clean_latent_indices = torch.cat([clean_latent_indices_pre, clean_latent_indices_post], dim=1) | |
clean_latents_pre = start_latent.to(history_latents) | |
clean_latents_post, clean_latents_2x, clean_latents_4x = history_latents[:, :, : 1 + 2 + 16, :, :].split( | |
[1, 2, 16], dim=2 | |
) | |
clean_latents = torch.cat([clean_latents_pre, clean_latents_post], dim=2) | |
# 1フレーム推論用の設定 | |
latent_indices = latent_indices[:, -1:] | |
print(f"latent_indices = {latent_indices}") | |
# 2xと4xは空に設定 | |
clean_latent_2x_indices = None | |
clean_latent_4x_indices = None | |
clean_latents_2x = None | |
clean_latents_4x = None | |
# GPU使用時のメモリ管理の最適化 | |
if not cpu_fallback_mode and not high_vram: | |
unload_complete_models() | |
move_model_to_device_with_memory_preservation( | |
transformer, target_device=gpu, preserved_memory_gb=gpu_memory_preservation | |
) | |
if use_teacache and not cpu_fallback_mode: | |
transformer.initialize_teacache(enable_teacache=True, num_steps=steps) | |
else: | |
transformer.initialize_teacache(enable_teacache=False) | |
def callback(d): | |
preview = d["denoised"] | |
preview = vae_decode_fake(preview) | |
preview = (preview * 255.0).detach().cpu().numpy().clip(0, 255).astype(np.uint8) | |
preview = einops.rearrange(preview, "b c t h w -> (b h) (t w) c") | |
if stream.input_queue.top() == "end": | |
stream.output_queue.push(("end", None)) | |
cleanup_temp_files(temp_dir) # 終了時に一時ファイル削除 | |
raise KeyboardInterrupt("ユーザーがタスクを終了しました。") | |
current_step = d["i"] + 1 | |
percentage = int(100.0 * current_step / steps) | |
hint = f"サンプリング中 {current_step}/{steps}" | |
desc = f"フレーム生成中: {current_step}/{steps} ({percentage}%)" | |
stream.output_queue.push(("progress", (preview, desc, make_progress_bar_html(percentage, hint)))) | |
return | |
# 途中でGPU制限を超えた場合のチェック | |
if check_gpu_quota_exceeded(): | |
cpu_fallback_mode = True | |
print("サンプリング前にGPU制限を超えました。CPUモードに切り替えます。") | |
# モデルを再度取得 | |
models = get_models() | |
transformer = models['transformer'] | |
transformer = transformer.to(cpu) | |
device_to_use = cpu | |
dtype_to_use = torch.float32 | |
# 適切な設定でサンプリング実行 | |
try: | |
# CPUモードでステップ数を減らす(速度向上のため) | |
actual_steps = min(steps, 15) if cpu_fallback_mode else steps | |
generated_latents = sample_hunyuan( | |
transformer=transformer, | |
sampler="unipc", | |
width=target_width, | |
height=target_height, | |
frames=num_frames, | |
real_guidance_scale=cfg, | |
distilled_guidance_scale=gs, | |
guidance_rescale=rs, | |
# shift=3.0, | |
num_inference_steps=actual_steps, | |
generator=rnd, | |
prompt_embeds=llama_vec, | |
prompt_embeds_mask=llama_attention_mask, | |
prompt_poolers=clip_l_pooler, | |
negative_prompt_embeds=llama_vec_n, | |
negative_prompt_embeds_mask=llama_attention_mask_n, | |
negative_prompt_poolers=clip_l_pooler_n, | |
device=device_to_use, | |
dtype=dtype_to_use, | |
image_embeddings=image_encoder_last_hidden_state, | |
latent_indices=latent_indices, | |
clean_latents=clean_latents, | |
clean_latent_indices=clean_latent_indices, | |
clean_latents_2x=clean_latents_2x, | |
clean_latent_2x_indices=clean_latent_2x_indices, | |
clean_latents_4x=clean_latents_4x, | |
clean_latent_4x_indices=clean_latent_4x_indices, | |
callback=callback, | |
) | |
except Exception as e: | |
if "CUDA" in str(e) or "GPU" in str(e) or "ZeroGPU quota exceeded" in str(e): | |
print(f"サンプリング中にGPU関連エラーが発生: {e}") | |
print("CPUモードに切り替えて再試行します") | |
# CPUモードに切り替え | |
cpu_fallback_mode = True | |
# モデルをCPUに移動 | |
transformer = transformer.to(cpu) | |
# CPUモード用のパラメータ設定 | |
device_to_use = cpu | |
dtype_to_use = torch.float32 | |
# CPUモードでステップ数を減らす | |
actual_steps = min(steps, 15) | |
# CPUモードで再試行 | |
generated_latents = sample_hunyuan( | |
transformer=transformer, | |
sampler="unipc", | |
width=target_width, | |
height=target_height, | |
frames=num_frames, | |
real_guidance_scale=cfg, | |
distilled_guidance_scale=gs, | |
guidance_rescale=rs, | |
num_inference_steps=actual_steps, | |
generator=rnd, | |
prompt_embeds=llama_vec.to(torch.float32), | |
prompt_embeds_mask=llama_attention_mask, | |
prompt_poolers=clip_l_pooler.to(torch.float32), | |
negative_prompt_embeds=llama_vec_n.to(torch.float32), | |
negative_prompt_embeds_mask=llama_attention_mask_n, | |
negative_prompt_poolers=clip_l_pooler_n.to(torch.float32), | |
device=device_to_use, | |
dtype=dtype_to_use, | |
image_embeddings=image_encoder_last_hidden_state.to(torch.float32), | |
latent_indices=latent_indices, | |
clean_latents=clean_latents, | |
clean_latent_indices=clean_latent_indices, | |
clean_latents_2x=clean_latents_2x, | |
clean_latent_2x_indices=clean_latent_2x_indices, | |
clean_latents_4x=clean_latents_4x, | |
clean_latent_4x_indices=clean_latent_4x_indices, | |
callback=callback, | |
) | |
else: | |
# GPU関連以外のエラーは再スロー | |
raise | |
print(f"generated_latents.shape = {generated_latents.shape}") | |
total_generated_latent_frames += int(generated_latents.shape[2]) | |
history_latents = torch.cat([generated_latents.to(history_latents), history_latents], dim=2) | |
# メモリ管理の最適化(GPUモードのみ) | |
if not cpu_fallback_mode and not high_vram: | |
offload_model_from_device_for_memory_preservation(transformer, target_device=gpu, preserved_memory_gb=8) | |
load_model_as_complete(vae, target_device=gpu) | |
real_history_latents =history_latents[:, :, :total_generated_latent_frames, :, :] | |
# VAEデコード | |
stream.output_queue.push(("progress", (None, "", make_progress_bar_html(70, "VAEデコード中 ...")))) | |
# VAEデコード前にGPU制限超過チェック | |
if check_gpu_quota_exceeded(): | |
cpu_fallback_mode = True | |
print("VAEデコード前にGPU制限を超えました。CPUモードに切り替えます。") | |
# モデルを再度取得 | |
models = get_models() | |
vae = models['vae'] | |
vae = vae.to(cpu) | |
if history_pixels is None: | |
if not cpu_fallback_mode: | |
history_pixels = vae_decode(real_history_latents, vae).cpu() | |
else: | |
# CPUモードでのVAEデコード | |
vae = vae.to(cpu) | |
history_pixels = vae_decode(real_history_latents.to(cpu, dtype=torch.float32), vae).cpu() | |
else: | |
section_latent_frames = (latent_window_size * 2 + 1) if is_last_section else (latent_window_size * 2) | |
overlapped_frames = latent_window_size * 4 - 3 | |
if not cpu_fallback_mode: | |
current_pixels = vae_decode(real_history_latents[:, :, :section_latent_frames], vae).cpu() | |
else: | |
# CPUモードでのVAEデコード | |
vae = vae.to(cpu) | |
current_pixels = vae_decode(real_history_latents[:, :, :section_latent_frames].to(cpu, dtype=torch.float32), vae).cpu() | |
history_pixels = soft_append_bcthw(current_pixels, history_pixels, overlapped_frames) | |
if not cpu_fallback_mode and not high_vram: | |
unload_complete_models() | |
# 一時フォルダにMP4保存とフレーム抽出 | |
stream.output_queue.push(("progress", (None, "", make_progress_bar_html(80, "動画保存中 ...")))) | |
output_path, extracted_frames = save_bcthw_as_mp4_with_frames( | |
history_pixels, temp_dir, job_id, fps=30, crf=mp4_crf | |
) | |
# 生成情報 | |
resize_info = { | |
"original_width": original_width, | |
"original_height": original_height, | |
"target_width": target_width, | |
"target_height": target_height | |
} | |
print(f"デコード完了。現在の潜在変数形状 {real_history_latents.shape}; ピクセル形状 {history_pixels.shape}") | |
print(f"MP4から {len(extracted_frames)} フレームを抽出しました") | |
# 元の解像度にリサイズした結果フレームを作成 | |
stream.output_queue.push(("progress", (None, "", make_progress_bar_html(90, "リサイズ中 ...")))) | |
if resize_info: | |
print(f"原寸サイズに戻します: {resize_info['original_width']}x{resize_info['original_height']}") | |
resized_frames = [] | |
for frame_path in extracted_frames: | |
# フレームを読み込み | |
frame = Image.open(frame_path) | |
# 元のサイズにリサイズ | |
resized_frame = frame.resize((resize_info['original_width'], resize_info['original_height']), Image.LANCZOS) | |
# 一時ファイルに保存 | |
resized_path = frame_path.replace(".png", "_resized.png") | |
resized_frame.save(resized_path) | |
resized_frames.append(resized_path) | |
# 元のサイズに戻したフレームのリストを使用 | |
extracted_frames = resized_frames | |
# 最後のフレームのパスを取得 | |
last_frame_path = extracted_frames[0] if extracted_frames else None | |
# 結果を送信 | |
stream.output_queue.push(("file", (output_path, last_frame_path, resize_info, temp_dir))) | |
if is_last_section: | |
break | |
except Exception as e: | |
traceback.print_exc() | |
print(f"エラーが発生しました: {str(e)}") | |
if not cpu_fallback_mode and not high_vram: | |
try: | |
unload_complete_models(text_encoder, text_encoder_2, image_encoder, vae, transformer) | |
except: | |
pass | |
# エラー発生時も一時ファイルを削除 | |
cleanup_temp_files(temp_dir) | |
# エラーメッセージを送信 | |
stream.output_queue.push(("error", str(e))) | |
stream.output_queue.push(("end", None)) | |
return | |
# 統合版プロセス関数 - GPU/CPU両方に対応(Spaces環境向け改良版) | |
if IN_HF_SPACE and 'spaces' in globals(): | |
def process_with_temp(image_mask_dict, lora_multiplier=1.0): | |
"""一時ファイルを使用する処理メインフロー(GPU/CPU対応)""" | |
global stream, cpu_fallback_mode | |
# GPU状態を事前チェック | |
check_gpu_status() | |
# 入力画像が提供されていることを確認 | |
if image_mask_dict is None: | |
return ( | |
gr.update(visible=False), # preview_image | |
gr.update(visible=False), # result_frame | |
"画像をアップロードしてマスクを描画してください", # progress_desc | |
"", # progress_bar | |
gr.update(interactive=True), # start_button | |
gr.update(interactive=False), # end_button | |
f"実行モード: {'CPU' if cpu_fallback_mode else 'GPU'}" # mode_info | |
) | |
# 処理開始時に UI をリセット | |
yield ( | |
gr.update(visible=False), # preview_image | |
gr.update(visible=False), # result_frame | |
"", # progress_desc | |
"", # progress_bar | |
gr.update(interactive=False), # start_button | |
gr.update(interactive=True), # end_button | |
f"実行モード: {'CPU' if cpu_fallback_mode else 'GPU'}" # mode_info | |
) | |
# 固定パラメータ | |
prompt = "A yellow and purple checkerboard mask fades away, smoothly revealing the background beneath, while the areas not covered by the mask remain completely still." | |
n_prompt = None | |
seed = 1234 | |
steps = 25 # CPU モードでは自動的に減らされる | |
cfg = 1.0 | |
gs = 10.0 | |
rs = 0.0 | |
gpu_memory_preservation = 6.0 | |
use_teacache = False | |
mp4_crf = 0 | |
lora_file = "./LoRA/mask_fadeout_V1.safetensors" | |
fp8_optimization = False | |
# LoRAファイルの存在確認 | |
if not os.path.exists(lora_file): | |
print(f"警告: LoRAファイル {lora_file} が見つかりません。LoRAなしで処理を続行します。") | |
lora_file = None | |
try: | |
# GPU使用可能かどうかを確認 | |
if check_gpu_quota_exceeded(): | |
print("GPU使用制限を超えているため、CPUモードで実行します") | |
cpu_fallback_mode = True | |
except Exception as e: | |
print(f"GPU確認中にエラーが発生しました: {e}") | |
# モード情報をログに表示 | |
print(f"実行モード: {'CPU' if cpu_fallback_mode else 'GPU'}") | |
# 非同期ワーカー起動 | |
stream = AsyncStream() | |
async_run( | |
worker_with_temp_files, | |
image_mask_dict, | |
prompt, | |
n_prompt, | |
seed, | |
steps, | |
cfg, | |
gs, | |
rs, | |
gpu_memory_preservation, | |
use_teacache, | |
mp4_crf, | |
lora_file, | |
lora_multiplier, | |
fp8_optimization, | |
) | |
temp_dir = None | |
last_frame_path = None | |
try: | |
while True: | |
flag, data = stream.output_queue.next() | |
# 生成完了ファイルを受け取ったとき | |
if flag == "file": | |
output_path, last_frame, resize_info, temp_dir = data | |
last_frame_path = last_frame | |
img_file = Image.open(last_frame_path) | |
yield ( | |
gr.update(visible=True), # preview_image に前回プレビューがあればそのまま | |
gr.update(visible=True, value=img_file), | |
gr.update(), # progress_desc | |
gr.update(), # progress_bar | |
gr.update(interactive=False), # start_button を無効化 | |
gr.update(interactive=True), # end_button を有効化 | |
f"実行モード: {'CPU' if cpu_fallback_mode else 'GPU'}" # mode_info | |
) | |
# 進捗更新を受け取ったとき | |
elif flag == "progress": | |
preview, desc, html = data | |
yield ( | |
gr.update(visible=True, value=preview), # preview_image に進捗サムネイル | |
gr.update(visible=False), # result_frame は隠す | |
desc, # progress_desc にテキスト | |
html, # progress_bar に HTML | |
gr.update(interactive=False), # start_button | |
gr.update(interactive=True), # end_button | |
f"実行モード: {'CPU' if cpu_fallback_mode else 'GPU'}" # mode_info | |
) | |
# エラーを受け取ったとき | |
elif flag == "error": | |
error_message = data | |
yield ( | |
gr.update(visible=False), # preview_image | |
gr.update(visible=False), # result_frame | |
error_message, # progress_desc にエラー表示 | |
"", # progress_bar | |
gr.update(interactive=True), # start_button | |
gr.update(interactive=False), # end_button | |
f"実行モード: {'CPU' if cpu_fallback_mode else 'GPU'} (エラー発生)" # mode_info | |
) | |
if temp_dir and os.path.exists(temp_dir): | |
cleanup_temp_files(temp_dir) | |
break | |
# 処理終了を受け取ったとき | |
elif flag == "end": | |
img_end = Image.open(last_frame_path) | |
# 最終的に last_frame を再表示 | |
yield ( | |
gr.update(visible=False), # preview_image を隠す | |
gr.update(visible=True, value=img_end), # result_frame に1枚だけ表示 | |
"", # progress_desc | |
"", # progress_bar | |
gr.update(interactive=True), # start_button | |
gr.update(interactive=False), # end_button | |
f"実行モード: {'CPU' if cpu_fallback_mode else 'GPU'} (完了)" # mode_info | |
) | |
if temp_dir and os.path.exists(temp_dir): | |
cleanup_temp_files(temp_dir) | |
break | |
except Exception as e: | |
print(f"処理中にエラーが発生しました: {e}") | |
if temp_dir and os.path.exists(temp_dir): | |
cleanup_temp_files(temp_dir) | |
raise e | |
else: | |
# 非Spaces環境用のプロセス関数 | |
def process_with_temp(image_mask_dict, lora_multiplier=1.0): | |
"""一時ファイルを使用する処理メインフロー(非GPU環境用)""" | |
global stream, cpu_fallback_mode | |
# CPU固定モード | |
cpu_fallback_mode = True | |
# 入力画像が提供されていることを確認 | |
if image_mask_dict is None: | |
return ( | |
gr.update(visible=False), # preview_image | |
gr.update(visible=False), # result_frame | |
"画像をアップロードしてマスクを描画してください", # progress_desc | |
"", # progress_bar | |
gr.update(interactive=True), # start_button | |
gr.update(interactive=False), # end_button | |
"実行モード: CPU (通常環境)" # mode_info | |
) | |
# 処理開始時に UI をリセット | |
yield ( | |
gr.update(visible=False), # preview_image | |
gr.update(visible=False), # result_frame | |
"", # progress_desc | |
"", # progress_bar | |
gr.update(interactive=False), # start_button | |
gr.update(interactive=True), # end_button | |
"実行モード: CPU (通常環境)" # mode_info | |
) | |
# 固定パラメータ | |
prompt = "A yellow and purple checkerboard mask fades away, smoothly revealing the background beneath, while the areas not covered by the mask remain completely still." | |
n_prompt = None | |
seed = 1234 | |
steps = 15 # CPU環境では少ないステップ数 | |
cfg = 1.0 | |
gs = 10.0 | |
rs = 0.0 | |
gpu_memory_preservation = 6.0 | |
use_teacache = False | |
mp4_crf = 0 | |
lora_file = "./LoRA/mask_fadeout_V1.safetensors" | |
fp8_optimization = False | |
# LoRAファイルの存在確認 | |
if not os.path.exists(lora_file): | |
print(f"警告: LoRAファイル {lora_file} が見つかりません。LoRAなしで処理を続行します。") | |
lora_file = None | |
# 非同期ワーカー起動 | |
stream = AsyncStream() | |
async_run( | |
worker_with_temp_files, | |
image_mask_dict, | |
prompt, | |
n_prompt, | |
seed, | |
steps, | |
cfg, | |
gs, | |
rs, | |
gpu_memory_preservation, | |
use_teacache, | |
mp4_crf, | |
lora_file, | |
lora_multiplier, | |
fp8_optimization, | |
) | |
temp_dir = None | |
last_frame_path = None | |
try: | |
while True: | |
flag, data = stream.output_queue.next() | |
# 生成完了ファイルを受け取ったとき | |
if flag == "file": | |
output_path, last_frame, resize_info, temp_dir = data | |
last_frame_path = last_frame | |
yield ( | |
gr.update(visible=True), # preview_image に前回プレビューがあればそのまま | |
gr.update(visible=True, value=last_frame_path),# result_frame に最初の1枚を表示 | |
gr.update(), # progress_desc | |
gr.update(), # progress_bar | |
gr.update(interactive=False), # start_button を無効化 | |
gr.update(interactive=True), # end_button を有効化 | |
"実行モード: CPU (通常環境)" # mode_info | |
) | |
# 進捗更新を受け取ったとき | |
elif flag == "progress": | |
preview, desc, html = data | |
yield ( | |
gr.update(visible=True, value=preview), # preview_image に進捗サムネイル | |
gr.update(visible=False), # result_frame は隠す | |
desc, # progress_desc にテキスト | |
html, # progress_bar に HTML | |
gr.update(interactive=False), # start_button | |
gr.update(interactive=True), # end_button | |
"実行モード: CPU (通常環境)" # mode_info | |
) | |
# エラーを受け取ったとき | |
elif flag == "error": | |
error_message = data | |
yield ( | |
gr.update(visible=False), # preview_image | |
gr.update(visible=False), # result_frame | |
error_message, # progress_desc にエラー表示 | |
"", # progress_bar | |
gr.update(interactive=True), # start_button | |
gr.update(interactive=False), # end_button | |
"実行モード: CPU (エラー発生)" # mode_info | |
) | |
if temp_dir and os.path.exists(temp_dir): | |
cleanup_temp_files(temp_dir) | |
break | |
# 処理終了を受け取ったとき | |
elif flag == "end": | |
img_end = Image.open(last_frame_path) | |
# 最終的に last_frame を再表示 | |
yield ( | |
gr.update(visible=False), # preview_image を隠す | |
gr.update(visible=True, value=img_end ), # result_frame に1枚だけ表示 | |
"", # progress_desc | |
"", # progress_bar | |
gr.update(interactive=True), # start_button | |
gr.update(interactive=False), # end_button | |
"実行モード: CPU (完了)" # mode_info | |
) | |
if temp_dir and os.path.exists(temp_dir): | |
cleanup_temp_files(temp_dir) | |
break | |
except Exception as e: | |
print(f"処理中にエラーが発生しました: {e}") | |
if temp_dir and os.path.exists(temp_dir): | |
cleanup_temp_files(temp_dir) | |
raise e | |
# 処理終了時に明示的にGPUメモリを解放 | |
def cleanup_gpu_resources(): | |
"""GPUリソースを明示的に解放する""" | |
global models | |
# モデルを全てCPUに移動 | |
for model_name, model in models.items(): | |
try: | |
if model is not None and hasattr(model, 'to') and callable(model.to): | |
model.to('cpu') | |
print(f"{model_name}をCPUに移動しました") | |
except Exception as e: | |
print(f"{model_name}のCPU移動中にエラー: {e}") | |
# キャッシュクリア | |
try: | |
torch.cuda.empty_cache() | |
gc.collect() | |
print("GPUキャッシュをクリアしました") | |
except Exception as e: | |
print(f"GPUキャッシュクリア中にエラー: {e}") | |
# 処理中止・クリーンアップ関数(GPU/CPU共通) | |
def end_process_with_cleanup(): | |
cleanup_gpu_resources() | |
"""処理を中止し、一時ファイルを削除する""" | |
global stream | |
if stream is not None: | |
stream.input_queue.push("end") | |
print("処理を中止しました") | |
# 既存の一時ディレクトリをすべて削除(念のため) | |
for dir_path in glob.glob(tempfile.gettempdir() + "/hunyuan_temp_*"): | |
if os.path.exists(dir_path): | |
try: | |
shutil.rmtree(dir_path) | |
print(f"一時ディレクトリを削除しました: {dir_path}") | |
except Exception as e: | |
print(f"一時ディレクトリの削除中にエラーが発生しました: {e}") | |
css = make_progress_bar_css() | |
block = gr.Blocks(css=css).queue() | |
with block: | |
gr.Markdown("# FramePackI2V_HY_mask_fadeout - 画像のマスクした部分を除去") | |
with gr.Row(): | |
with gr.Column(): | |
# 入力画像をImageMaskで設定 | |
image_mask = gr.ImageMask( | |
label="画像をアップロードしてマスクを描画", | |
type="pil", | |
brush=gr.Brush( | |
colors=["#FF00FF", "#FFFFFF", "#000000", "#FF0000", "#00FF00", "#0000FF", "#FFFF00"], | |
default_color="#FF00FF", | |
color_mode="defaults" | |
), | |
layers=True, | |
height="70vh", | |
width="60vh", | |
) | |
with gr.Row(): | |
start_button = gr.Button(value="生成開始") | |
end_button = gr.Button(value="生成中止", interactive=False) | |
with gr.Group(): | |
lora_multiplier = gr.Slider(label="LoRA倍率", minimum=0.0, maximum=2.0, value=1.0, step=0.1) | |
with gr.Group(): | |
mode_info = gr.Markdown(f"実行モード: {'CPU' if cpu_fallback_mode else 'GPU'}") | |
with gr.Column(): | |
preview_image = gr.Image(label="生成プレビュー", visible=False) | |
result_frame = gr.Image(label="生成結果", visible=False, type="pil", height="60vh") | |
progress_desc = gr.Markdown("", elem_classes="no-generating-animation") | |
progress_bar = gr.HTML("", elem_classes="no-generating-animation") | |
ips = [ | |
image_mask, | |
lora_multiplier, | |
] | |
if IN_HF_SPACE and 'spaces' in globals(): | |
ops = [ | |
preview_image, | |
result_frame, | |
progress_desc, | |
progress_bar, | |
start_button, | |
end_button, | |
mode_info | |
] | |
else: | |
ops = [ | |
preview_image, | |
result_frame, | |
progress_desc, | |
progress_bar, | |
start_button, | |
end_button, | |
mode_info | |
] | |
start_button.click( | |
fn=process_with_temp, inputs=ips, outputs=ops | |
) | |
end_button.click(fn=end_process_with_cleanup) | |
# アプリ起動関数(エラーハンドリング付き) | |
def launch_app(): | |
"""アプリケーションの起動(エラーハンドリング付き)""" | |
# 通常の起動方法 | |
block.launch( | |
server_name="0.0.0.0", | |
share=False, | |
inbrowser=False, | |
) | |
# アプリケーションの起動 | |
launch_app() |