291 lines
9.7 KiB
Python
291 lines
9.7 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
"""
|
|
Convert the full Hunyuan-GameCraft-1.0 model to FastVideo diffusers format.
|
|
|
|
This script converts:
|
|
1. Transformer (DiT) weights from DeepSpeed format
|
|
2. VAE weights
|
|
3. Text encoders (LLaVA-LLaMA-3-8B and CLIP)
|
|
4. Creates model_index.json and scheduler config
|
|
|
|
Usage:
|
|
python scripts/checkpoint_conversion/convert_gamecraft_full.py \
|
|
--input-dir Hunyuan-GameCraft-1.0/weights \
|
|
--output-dir official_weights/hunyuan-gamecraft-diffusers
|
|
|
|
# Or specify individual paths:
|
|
python scripts/checkpoint_conversion/convert_gamecraft_full.py \
|
|
--transformer Hunyuan-GameCraft-1.0/weights/gamecraft_models/mp_rank_00_model_states.pt \
|
|
--vae Hunyuan-GameCraft-1.0/weights/stdmodels/vae_3d/hyvae/checkpoint-step-270000.ckpt \
|
|
--text-encoder Hunyuan-GameCraft-1.0/weights/stdmodels/llava-llama-3-8b-v1_1-transformers \
|
|
--text-encoder-2 Hunyuan-GameCraft-1.0/weights/stdmodels/openai_clip-vit-large-patch14 \
|
|
--output-dir official_weights/hunyuan-gamecraft-diffusers
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import shutil
|
|
from pathlib import Path
|
|
|
|
import torch
|
|
from safetensors.torch import save_file
|
|
|
|
# Import conversion functions
|
|
from convert_gamecraft_weights import convert_weights as convert_transformer
|
|
from convert_gamecraft_vae import convert_gamecraft_vae
|
|
|
|
|
|
def create_model_index(output_dir: Path) -> None:
|
|
"""Create model_index.json for the pipeline."""
|
|
model_index = {
|
|
"_class_name": "HunyuanGameCraftPipeline",
|
|
"_diffusers_version": "0.30.0",
|
|
"scheduler": [
|
|
"diffusers",
|
|
"FlowMatchEulerDiscreteScheduler"
|
|
],
|
|
"text_encoder": [
|
|
"transformers",
|
|
"LlamaModel"
|
|
],
|
|
"text_encoder_2": [
|
|
"transformers",
|
|
"CLIPTextModel"
|
|
],
|
|
"tokenizer": [
|
|
"transformers",
|
|
"AutoTokenizer"
|
|
],
|
|
"tokenizer_2": [
|
|
"transformers",
|
|
"CLIPTokenizer"
|
|
],
|
|
"transformer": [
|
|
"fastvideo",
|
|
"HunyuanGameCraftTransformer3DModel"
|
|
],
|
|
"vae": [
|
|
"fastvideo",
|
|
"AutoencoderKLCausal3D"
|
|
]
|
|
}
|
|
|
|
with open(output_dir / "model_index.json", "w") as f:
|
|
json.dump(model_index, f, indent=2)
|
|
print(f"Created model_index.json")
|
|
|
|
|
|
def create_scheduler_config(output_dir: Path) -> None:
|
|
"""Create scheduler config for FlowMatchEulerDiscreteScheduler."""
|
|
scheduler_dir = output_dir / "scheduler"
|
|
scheduler_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
scheduler_config = {
|
|
"_class_name": "FlowMatchEulerDiscreteScheduler",
|
|
"_diffusers_version": "0.30.0",
|
|
"base_image_seq_len": 256,
|
|
"base_shift": 0.5,
|
|
"invert_sigmas": False,
|
|
"max_shift": 1.15,
|
|
"num_train_timesteps": 1000,
|
|
"shift": 7.0,
|
|
"use_dynamic_shifting": True
|
|
}
|
|
|
|
with open(scheduler_dir / "scheduler_config.json", "w") as f:
|
|
json.dump(scheduler_config, f, indent=2)
|
|
print(f"Created scheduler config")
|
|
|
|
|
|
def copy_text_encoder(src_dir: Path, dst_dir: Path, name: str) -> None:
|
|
"""Copy text encoder files to output directory."""
|
|
if not src_dir.exists():
|
|
print(f"Warning: {name} not found at {src_dir}")
|
|
return
|
|
|
|
dst_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Copy all files
|
|
for src_file in src_dir.iterdir():
|
|
if src_file.is_file():
|
|
shutil.copy2(src_file, dst_dir / src_file.name)
|
|
|
|
print(f"Copied {name} to {dst_dir}")
|
|
|
|
|
|
def copy_tokenizer(src_dir: Path, dst_dir: Path, name: str) -> None:
|
|
"""Copy tokenizer files to output directory."""
|
|
if not src_dir.exists():
|
|
print(f"Warning: {name} not found at {src_dir}")
|
|
return
|
|
|
|
dst_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Copy tokenizer-related files
|
|
tokenizer_files = [
|
|
"tokenizer.json",
|
|
"tokenizer_config.json",
|
|
"special_tokens_map.json",
|
|
"vocab.json",
|
|
"merges.txt",
|
|
"added_tokens.json",
|
|
]
|
|
|
|
for filename in tokenizer_files:
|
|
src_file = src_dir / filename
|
|
if src_file.exists():
|
|
shutil.copy2(src_file, dst_dir / filename)
|
|
|
|
print(f"Copied {name} to {dst_dir}")
|
|
|
|
|
|
def convert_full_model(
|
|
transformer_path: Path | None = None,
|
|
vae_path: Path | None = None,
|
|
text_encoder_path: Path | None = None,
|
|
text_encoder_2_path: Path | None = None,
|
|
input_dir: Path | None = None,
|
|
output_dir: Path = Path("official_weights/hunyuan-gamecraft-diffusers"),
|
|
verbose: bool = False,
|
|
) -> None:
|
|
"""Convert all components of GameCraft to diffusers format."""
|
|
|
|
# Resolve paths from input_dir if individual paths not provided
|
|
if input_dir is not None:
|
|
if transformer_path is None:
|
|
transformer_path = input_dir / "gamecraft_models" / "mp_rank_00_model_states.pt"
|
|
if vae_path is None:
|
|
vae_path = input_dir / "stdmodels" / "vae_3d" / "hyvae" / "checkpoint-step-270000.ckpt"
|
|
if not vae_path.exists():
|
|
vae_path = input_dir / "stdmodels" / "vae_3d" / "hyvae" / "pytorch_model.pt"
|
|
if text_encoder_path is None:
|
|
text_encoder_path = input_dir / "stdmodels" / "llava-llama-3-8b-v1_1-transformers"
|
|
if text_encoder_2_path is None:
|
|
text_encoder_2_path = input_dir / "stdmodels" / "openai_clip-vit-large-patch14"
|
|
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
print(f"Converting GameCraft to diffusers format at {output_dir}")
|
|
print("=" * 60)
|
|
|
|
# 1. Convert transformer
|
|
if transformer_path and transformer_path.exists():
|
|
print(f"\n[1/5] Converting transformer from {transformer_path}")
|
|
convert_transformer(
|
|
input_path=transformer_path,
|
|
output_dir=output_dir,
|
|
save_config=True,
|
|
verbose=verbose,
|
|
)
|
|
else:
|
|
print(f"\n[1/5] Skipping transformer (not found: {transformer_path})")
|
|
|
|
# 2. Convert VAE
|
|
if vae_path and vae_path.exists():
|
|
print(f"\n[2/5] Converting VAE from {vae_path}")
|
|
convert_gamecraft_vae(
|
|
input_path=vae_path,
|
|
output_dir=output_dir / "vae",
|
|
copy_config=True,
|
|
)
|
|
else:
|
|
print(f"\n[2/5] Skipping VAE (not found: {vae_path})")
|
|
|
|
# 3. Copy text encoder (LLaMA)
|
|
if text_encoder_path and text_encoder_path.exists():
|
|
print(f"\n[3/5] Copying text encoder from {text_encoder_path}")
|
|
copy_text_encoder(text_encoder_path, output_dir / "text_encoder", "text_encoder")
|
|
copy_tokenizer(text_encoder_path, output_dir / "tokenizer", "tokenizer")
|
|
else:
|
|
print(f"\n[3/5] Skipping text_encoder (not found: {text_encoder_path})")
|
|
|
|
# 4. Copy text encoder 2 (CLIP)
|
|
if text_encoder_2_path and text_encoder_2_path.exists():
|
|
print(f"\n[4/5] Copying text encoder 2 from {text_encoder_2_path}")
|
|
copy_text_encoder(text_encoder_2_path, output_dir / "text_encoder_2", "text_encoder_2")
|
|
copy_tokenizer(text_encoder_2_path, output_dir / "tokenizer_2", "tokenizer_2")
|
|
else:
|
|
print(f"\n[4/5] Skipping text_encoder_2 (not found: {text_encoder_2_path})")
|
|
|
|
# 5. Create model_index.json and scheduler
|
|
print(f"\n[5/5] Creating model_index.json and scheduler config")
|
|
create_model_index(output_dir)
|
|
create_scheduler_config(output_dir)
|
|
|
|
print("\n" + "=" * 60)
|
|
print(f"Conversion complete! Output: {output_dir}")
|
|
print("\nDirectory structure:")
|
|
for item in sorted(output_dir.rglob("*")):
|
|
if item.is_file():
|
|
rel_path = item.relative_to(output_dir)
|
|
size_mb = item.stat().st_size / (1024 * 1024)
|
|
print(f" {rel_path} ({size_mb:.1f} MB)")
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Convert full Hunyuan-GameCraft-1.0 model to FastVideo diffusers format."
|
|
)
|
|
parser.add_argument(
|
|
"--input-dir",
|
|
type=str,
|
|
default=None,
|
|
help="Path to Hunyuan-GameCraft-1.0/weights directory"
|
|
)
|
|
parser.add_argument(
|
|
"--transformer",
|
|
type=str,
|
|
default=None,
|
|
help="Path to transformer checkpoint (mp_rank_00_model_states.pt)"
|
|
)
|
|
parser.add_argument(
|
|
"--vae",
|
|
type=str,
|
|
default=None,
|
|
help="Path to VAE checkpoint"
|
|
)
|
|
parser.add_argument(
|
|
"--text-encoder",
|
|
type=str,
|
|
default=None,
|
|
help="Path to text encoder (LLaVA-LLaMA) directory"
|
|
)
|
|
parser.add_argument(
|
|
"--text-encoder-2",
|
|
type=str,
|
|
default=None,
|
|
help="Path to text encoder 2 (CLIP) directory"
|
|
)
|
|
parser.add_argument(
|
|
"--output-dir",
|
|
type=str,
|
|
default="official_weights/hunyuan-gamecraft-diffusers",
|
|
help="Output directory for converted model"
|
|
)
|
|
parser.add_argument(
|
|
"--verbose",
|
|
"-v",
|
|
action="store_true",
|
|
help="Print detailed conversion info"
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Require either input-dir or individual paths
|
|
if args.input_dir is None and args.transformer is None:
|
|
parser.error("Either --input-dir or individual component paths must be provided")
|
|
|
|
convert_full_model(
|
|
transformer_path=Path(args.transformer) if args.transformer else None,
|
|
vae_path=Path(args.vae) if args.vae else None,
|
|
text_encoder_path=Path(args.text_encoder) if args.text_encoder else None,
|
|
text_encoder_2_path=Path(args.text_encoder_2) if args.text_encoder_2 else None,
|
|
input_dir=Path(args.input_dir) if args.input_dir else None,
|
|
output_dir=Path(args.output_dir),
|
|
verbose=args.verbose,
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|