tori29umai's picture
Update app.py
62e73b1 verified
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():
@spaces.GPU(duration=120)
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()