565 lines
21 KiB
Python
565 lines
21 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
"""Convert daVinci-MagiHuman (GAIR-NLP) weights to a Diffusers-format repo.
|
|
|
|
MagiHuman publishes weights in a raw layout on HuggingFace
|
|
(https://huggingface.co/GAIR/daVinci-MagiHuman). The layout is:
|
|
|
|
base/ <- DiT safetensors (sharded)
|
|
distill/ <- distilled DiT (out of scope for the base port)
|
|
540p_sr/, 1080p_sr/ <- super-resolution DiTs (out of scope)
|
|
turbo_vae/ <- optional fast VAE decoder (out of scope for first cut)
|
|
|
|
The base DiT uses Wan-AI/Wan2.2-TI2V-5B's VAE and google/t5gemma-9b-9b-ul2's
|
|
encoder at inference time; neither is bundled upstream.
|
|
|
|
This converter takes the raw MagiHuman base DiT and emits a Diffusers-style
|
|
directory so `VideoGenerator.from_pretrained(...)` can load it standalone:
|
|
|
|
<output>/
|
|
model_index.json
|
|
transformer/
|
|
config.json
|
|
diffusion_pytorch_model-00001-of-00N.safetensors (+ index)
|
|
scheduler/
|
|
scheduler_config.json (FlowUniPC default)
|
|
vae/ (optional; --bundle-vae)
|
|
audio_vae/ (optional; --bundle-audio-vae)
|
|
text_encoder/, tokenizer/ (optional; --bundle-text-encoder)
|
|
|
|
By default the converted repo is MINIMAL: only `transformer/`,
|
|
`scheduler/`, and `model_index.json` are emitted (~5-30 GB depending on
|
|
variant). The four cross-variant shared components — Wan VAE, Stable
|
|
Audio VAE, T5-Gemma encoder, and tokenizer — are lazy-loaded by
|
|
`MagiHumanPipeline.load_modules` from their canonical upstream HF repos
|
|
on first build, so all MagiHuman variants share a single ~25 GB cache
|
|
of upstream weights. Pass the `--bundle-*` flags only if you want to
|
|
ship a self-contained snapshot.
|
|
|
|
The DiT key names pass through unchanged — the FastVideo `MagiHumanDiT` module
|
|
mirrors the reference module tree (`adapter.*`, `block.layers.*`, `final_*`),
|
|
so no regex remapping is needed. The conversion is effectively a reshard +
|
|
Diffusers wrapper.
|
|
|
|
Example (minimal artifact, ~5-30 GB):
|
|
python scripts/checkpoint_conversion/convert_magi_human_to_diffusers.py \\
|
|
--source GAIR/daVinci-MagiHuman \\
|
|
--subfolder base \\
|
|
--output converted_weights/magi_human_base
|
|
|
|
Example (self-contained SR-540p artifact with base + SR DiTs):
|
|
python scripts/checkpoint_conversion/convert_magi_human_to_diffusers.py \
|
|
--source GAIR/daVinci-MagiHuman \
|
|
--subfolder base \
|
|
--sr-source GAIR/daVinci-MagiHuman \
|
|
--sr-subfolder 540p_sr \
|
|
--output converted_weights/magi_human_sr_540p
|
|
|
|
Example (self-contained snapshot with shared components bundled):
|
|
python scripts/checkpoint_conversion/convert_magi_human_to_diffusers.py \\
|
|
--source GAIR/daVinci-MagiHuman \\
|
|
--subfolder base \\
|
|
--output converted_weights/magi_human_base \\
|
|
--bundle-vae --bundle-audio-vae --bundle-text-encoder
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import shutil
|
|
from collections import OrderedDict
|
|
from pathlib import Path
|
|
|
|
import torch
|
|
from huggingface_hub import hf_hub_download, snapshot_download
|
|
from safetensors.torch import load_file, save_file
|
|
|
|
|
|
# MagiHuman base arch — keys must be valid `MagiHumanArchConfig` fields.
|
|
# FastVideo's `TransformerLoader.load` calls `ArchConfig.update_model_arch`
|
|
# with this dict (minus `_class_name`, `_diffusers_version`) and rejects
|
|
# any key that isn't a declared field. Pipeline-level knobs (steps, CFG,
|
|
# guidance scales, flow_shift) live on `MagiHumanBaseConfig` and do NOT
|
|
# belong here — they'd silently shadow the ArchConfig loader otherwise.
|
|
MAGI_HUMAN_BASE_ARCH: dict = {
|
|
"_class_name": "MagiHumanDiT",
|
|
"_diffusers_version": "0.33.0",
|
|
# Transformer shape (upstream `ModelConfig`, `inference/common/config.py`).
|
|
"num_layers": 40,
|
|
"hidden_size": 5120,
|
|
"head_dim": 128,
|
|
"num_query_groups": 8,
|
|
# Modality channels.
|
|
"video_in_channels": 192, # 48 (VAE z_dim) * patch_size product 1*2*2
|
|
"audio_in_channels": 64,
|
|
"text_in_channels": 3584, # T5Gemma-9B encoder hidden size
|
|
# Block-level switches.
|
|
"mm_layers": [0, 1, 2, 3, 36, 37, 38, 39],
|
|
"local_attn_layers": [],
|
|
"gelu7_layers": [0, 1, 2, 3],
|
|
"post_norm_layers": [],
|
|
"enable_attn_gating": True,
|
|
"activation_type": "swiglu7",
|
|
# DiT patching / positional.
|
|
"patch_size": [1, 2, 2],
|
|
"spatial_rope_interpolation": "extra",
|
|
# TReAD (flattened; upstream nests as `tread_config`).
|
|
"tread_selection_rate": 0.5,
|
|
"tread_start_layer_idx": 2,
|
|
"tread_end_layer_idx": 25,
|
|
}
|
|
|
|
|
|
SCHEDULER_CONFIG: dict = {
|
|
"_class_name": "FlowUniPCMultistepScheduler",
|
|
"_diffusers_version": "0.33.0",
|
|
"num_train_timesteps": 1000,
|
|
"solver_order": 2,
|
|
"prediction_type": "flow_prediction",
|
|
"shift": 5.0,
|
|
"predict_x0": True,
|
|
"solver_type": "bh2",
|
|
"lower_order_final": True,
|
|
"disable_corrector": [],
|
|
"flow_shift": 5.0,
|
|
}
|
|
|
|
|
|
MAX_SHARD_BYTES = 5 * 1024 * 1024 * 1024 # 5 GB shards, matches HF defaults
|
|
|
|
|
|
def _download_dit_shards(source: Path | str, subfolder: str = "base") -> list[Path]:
|
|
"""Return local paths to all safetensors shards for the DiT."""
|
|
source = str(source)
|
|
if os.path.isdir(source):
|
|
shard_dir = Path(source) / subfolder
|
|
shards = sorted(shard_dir.glob("*.safetensors"))
|
|
if not shards:
|
|
raise FileNotFoundError(f"No safetensors under {shard_dir}")
|
|
return shards
|
|
|
|
# Remote HF repo — pull just the base subfolder.
|
|
local_dir = snapshot_download(
|
|
repo_id=source,
|
|
allow_patterns=[f"{subfolder}/*.safetensors", f"{subfolder}/*.json"],
|
|
)
|
|
shard_dir = Path(local_dir) / subfolder
|
|
return sorted(shard_dir.glob("*.safetensors"))
|
|
|
|
|
|
def _load_all_shards(
|
|
shards: list[Path],
|
|
cast_bf16: bool = False,
|
|
) -> "OrderedDict[str, torch.Tensor]":
|
|
"""Load all safetensors shards into a single state dict.
|
|
|
|
When `cast_bf16` is True, fp32 tensors whose names match the transformer
|
|
core (attention / mlp / final_linear_* / adapter.{video,text,audio}_embedder)
|
|
are cast to bfloat16. fp32 is preserved for norms, rope bands, and any
|
|
other tensor where precision matters. This is the right default for
|
|
the distill checkpoint, which upstream ships as fp32 master weights
|
|
(61 GB) — casting yields a 30 GB Diffusers artifact that matches the
|
|
base checkpoint format.
|
|
"""
|
|
# Tensors that must stay in float32 regardless of cast_bf16. These are
|
|
# the dtypes that appear as fp32 in the BASE checkpoint, which is the
|
|
# ground-truth shape of a "runtime-loadable" MagiHuman repo. The list
|
|
# includes:
|
|
# - all RMSNorm weights (norms always run fp32 in upstream
|
|
# MultiModalityRMSNorm and FV's mirror)
|
|
# - the rope band buffer
|
|
# - the adapter embedders (video/text/audio: weight + bias) which
|
|
# upstream's Adapter declares as `dtype=torch.float32` and FV's
|
|
# MagiAdapter mirrors at `magi_human.py:519-527`
|
|
# - the final_linear_{video,audio} heads which upstream/FV both
|
|
# declare as `dtype=torch.float32` (`magi_human.py:645-648`,
|
|
# `dit_module.py:896-900`)
|
|
# Forgetting any of these makes `--cast-bf16` lossy for the distill
|
|
# checkpoint (which ships everything as fp32) and produces parity
|
|
# drift vs upstream that base does not exhibit (because base already
|
|
# ships with the right mixed-dtype layout).
|
|
_FP32_KEEP_SUFFIXES = (
|
|
".pre_norm.weight",
|
|
".q_norm.weight",
|
|
".k_norm.weight",
|
|
".attn_post_norm.weight",
|
|
".mlp_post_norm.weight",
|
|
"final_norm_video.weight",
|
|
"final_norm_audio.weight",
|
|
"final_linear_video.weight",
|
|
"final_linear_audio.weight",
|
|
"adapter.video_embedder.weight",
|
|
"adapter.video_embedder.bias",
|
|
"adapter.text_embedder.weight",
|
|
"adapter.text_embedder.bias",
|
|
"adapter.audio_embedder.weight",
|
|
"adapter.audio_embedder.bias",
|
|
"adapter.rope.bands",
|
|
)
|
|
_FP32_KEEP_FULL = {"adapter.rope.bands"}
|
|
|
|
def _keep_fp32(k: str) -> bool:
|
|
if k in _FP32_KEEP_FULL:
|
|
return True
|
|
return any(k.endswith(s) for s in _FP32_KEEP_SUFFIXES)
|
|
|
|
state: OrderedDict[str, torch.Tensor] = OrderedDict()
|
|
for shard in shards:
|
|
piece = load_file(str(shard))
|
|
for k, v in piece.items():
|
|
if k in state:
|
|
raise RuntimeError(f"Duplicate key across shards: {k}")
|
|
if cast_bf16 and v.dtype == torch.float32 and not _keep_fp32(k):
|
|
v = v.to(torch.bfloat16)
|
|
state[k] = v
|
|
print(f" loaded {shard.name} ({len(piece)} tensors)")
|
|
return state
|
|
|
|
|
|
def _validate_state(state: dict[str, torch.Tensor]) -> None:
|
|
"""Sanity-check required top-level modules are present."""
|
|
required_prefixes = (
|
|
"adapter.video_embedder.",
|
|
"adapter.text_embedder.",
|
|
"adapter.audio_embedder.",
|
|
"adapter.rope.bands",
|
|
"final_norm_video.",
|
|
"final_norm_audio.",
|
|
"final_linear_video.",
|
|
"final_linear_audio.",
|
|
)
|
|
for pref in required_prefixes:
|
|
if not any(k.startswith(pref) for k in state):
|
|
raise RuntimeError(f"Missing expected key prefix: {pref}")
|
|
# Layer count
|
|
layer_ids = {int(k.split(".")[2]) for k in state if k.startswith("block.layers.")}
|
|
if layer_ids != set(range(40)):
|
|
raise RuntimeError(f"Expected layers 0..39, got {sorted(layer_ids)}")
|
|
|
|
|
|
def _shard_state_dict(
|
|
state: dict[str, torch.Tensor],
|
|
max_bytes: int = MAX_SHARD_BYTES,
|
|
) -> tuple[list[dict[str, torch.Tensor]], dict[str, str]]:
|
|
"""Greedy shard-packing: produce N shards of <= max_bytes, plus index."""
|
|
shards: list[dict[str, torch.Tensor]] = []
|
|
index: dict[str, str] = {}
|
|
cur: dict[str, torch.Tensor] = {}
|
|
cur_bytes = 0
|
|
shard_idx = 0
|
|
total = len(state)
|
|
for k, v in state.items():
|
|
t_bytes = v.numel() * v.element_size()
|
|
if cur and cur_bytes + t_bytes > max_bytes:
|
|
shards.append(cur)
|
|
cur = {}
|
|
cur_bytes = 0
|
|
shard_idx += 1
|
|
cur[k] = v
|
|
cur_bytes += t_bytes
|
|
if cur:
|
|
shards.append(cur)
|
|
n = len(shards)
|
|
for i, shard in enumerate(shards, start=1):
|
|
shard_name = f"diffusion_pytorch_model-{i:05d}-of-{n:05d}.safetensors"
|
|
for k in shard:
|
|
index[k] = shard_name
|
|
assert sum(len(s) for s in shards) == total
|
|
return shards, index
|
|
|
|
|
|
def _write_transformer(
|
|
out_dir: Path,
|
|
state: dict[str, torch.Tensor],
|
|
arch: dict,
|
|
subdir: str = "transformer",
|
|
) -> None:
|
|
transformer_dir = out_dir / subdir
|
|
transformer_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
shards, weight_map = _shard_state_dict(state)
|
|
n = len(shards)
|
|
total_bytes = sum(v.numel() * v.element_size() for v in state.values())
|
|
for i, shard in enumerate(shards, start=1):
|
|
shard_name = f"diffusion_pytorch_model-{i:05d}-of-{n:05d}.safetensors"
|
|
save_file(shard, str(transformer_dir / shard_name))
|
|
print(f" wrote {shard_name} ({len(shard)} tensors)")
|
|
|
|
index = {"metadata": {"total_size": total_bytes}, "weight_map": weight_map}
|
|
with (transformer_dir / "diffusion_pytorch_model.safetensors.index.json").open("w") as f:
|
|
json.dump(index, f, indent=2)
|
|
f.write("\n")
|
|
|
|
with (transformer_dir / "config.json").open("w") as f:
|
|
json.dump(arch, f, indent=2)
|
|
f.write("\n")
|
|
print(f" wrote {subdir}/config.json ({len(arch)} keys)")
|
|
|
|
|
|
def _write_scheduler(out_dir: Path) -> None:
|
|
scheduler_dir = out_dir / "scheduler"
|
|
scheduler_dir.mkdir(parents=True, exist_ok=True)
|
|
with (scheduler_dir / "scheduler_config.json").open("w") as f:
|
|
json.dump(SCHEDULER_CONFIG, f, indent=2)
|
|
f.write("\n")
|
|
print(f" wrote scheduler/scheduler_config.json")
|
|
|
|
|
|
def _write_model_index(
|
|
out_dir: Path,
|
|
bundle_vae: bool,
|
|
bundle_text: bool,
|
|
bundle_audio_vae: bool = False,
|
|
include_sr_transformer: bool = False,
|
|
sr_subfolder: str = "540p_sr",
|
|
) -> None:
|
|
pipeline_class = "MagiHumanPipeline"
|
|
if include_sr_transformer:
|
|
pipeline_class = (
|
|
"MagiHumanSR1080pPipeline"
|
|
if sr_subfolder == "1080p_sr" else "MagiHumanSRPipeline"
|
|
)
|
|
index = {
|
|
"_class_name": pipeline_class,
|
|
"_diffusers_version": "0.33.0",
|
|
"transformer": ["diffusers", "MagiHumanDiT"],
|
|
"scheduler": ["diffusers", "FlowUniPCMultistepScheduler"],
|
|
}
|
|
if include_sr_transformer:
|
|
index["sr_transformer"] = ["diffusers", "MagiHumanDiT"]
|
|
if bundle_vae:
|
|
index["vae"] = ["diffusers", "AutoencoderKLWan"]
|
|
if bundle_audio_vae:
|
|
index["audio_vae"] = ["diffusers", "AutoencoderOobleck"]
|
|
if bundle_text:
|
|
index["text_encoder"] = ["transformers", "T5GemmaEncoderModel"]
|
|
index["tokenizer"] = ["transformers", "GemmaTokenizer"]
|
|
with (out_dir / "model_index.json").open("w") as f:
|
|
json.dump(index, f, indent=2)
|
|
f.write("\n")
|
|
print(f" wrote model_index.json")
|
|
|
|
|
|
def _bundle_wan_vae(out_dir: Path, source_repo: str = "Wan-AI/Wan2.2-TI2V-5B-Diffusers") -> None:
|
|
"""Download the Wan 2.2 TI2V 5B VAE component into <out_dir>/vae/.
|
|
|
|
The `-Diffusers` variant has the canonical `vae/config.json` +
|
|
`vae/diffusion_pytorch_model.safetensors` layout. The plain
|
|
`Wan-AI/Wan2.2-TI2V-5B` repo ships the VAE as a single `.pth` at the
|
|
root, which is not `from_pretrained`-friendly.
|
|
"""
|
|
print(f" fetching VAE from {source_repo} ...")
|
|
local = snapshot_download(
|
|
repo_id=source_repo,
|
|
allow_patterns=["vae/*"],
|
|
)
|
|
src_vae = Path(local) / "vae"
|
|
if not src_vae.exists():
|
|
raise FileNotFoundError(f"No vae/ subdir in {source_repo}")
|
|
dst_vae = out_dir / "vae"
|
|
if dst_vae.exists():
|
|
shutil.rmtree(dst_vae)
|
|
shutil.copytree(src_vae, dst_vae)
|
|
print(f" copied {src_vae} -> {dst_vae}")
|
|
|
|
|
|
def _bundle_sa_audio_vae(out_dir: Path, source_repo: str = "stabilityai/stable-audio-open-1.0") -> None:
|
|
"""Download the Stable Audio Open 1.0 VAE component into <out_dir>/audio_vae/.
|
|
|
|
Stability ships the VAE at `vae/config.json` +
|
|
`vae/diffusion_pytorch_model.safetensors` inside the main repo, so
|
|
the bundle is just a copy of that subdir. The repo is gated — the
|
|
caller's HF token must have accepted terms on
|
|
https://huggingface.co/stabilityai/stable-audio-open-1.0.
|
|
"""
|
|
print(f" fetching audio VAE from {source_repo} (gated) ...")
|
|
token = (
|
|
os.environ.get("HF_TOKEN")
|
|
or os.environ.get("HUGGINGFACE_HUB_TOKEN")
|
|
or os.environ.get("HF_API_KEY")
|
|
)
|
|
local = snapshot_download(
|
|
repo_id=source_repo, token=token, allow_patterns=["vae/*"],
|
|
)
|
|
src = Path(local) / "vae"
|
|
if not src.exists():
|
|
raise FileNotFoundError(f"No vae/ subdir in {source_repo}")
|
|
dst = out_dir / "audio_vae"
|
|
if dst.exists():
|
|
shutil.rmtree(dst)
|
|
shutil.copytree(src, dst)
|
|
print(f" copied {src} -> {dst}")
|
|
|
|
|
|
def _bundle_text_encoder(out_dir: Path, source_repo: str = "google/t5gemma-9b-9b-ul2") -> None:
|
|
"""Download the T5Gemma encoder + tokenizer.
|
|
|
|
T5Gemma is a Google gated repo; this step requires a write-scoped token with
|
|
accepted terms of use for the repo.
|
|
"""
|
|
print(f" fetching text encoder from {source_repo} (gated) ...")
|
|
token = (
|
|
os.environ.get("HF_TOKEN")
|
|
or os.environ.get("HUGGINGFACE_HUB_TOKEN")
|
|
or os.environ.get("HF_API_KEY")
|
|
)
|
|
local = snapshot_download(
|
|
repo_id=source_repo,
|
|
token=token,
|
|
allow_patterns=[
|
|
"*.json",
|
|
"*.model",
|
|
"*.safetensors",
|
|
"*.safetensors.index.json",
|
|
],
|
|
)
|
|
# Encoder-only bundling: keep tokenizer at the root and encoder weights
|
|
# under text_encoder/. HF's T5GemmaEncoderModel.from_pretrained(<dir>) on
|
|
# the whole repo works, but we split to match Diffusers layout.
|
|
src = Path(local)
|
|
dst_encoder = out_dir / "text_encoder"
|
|
dst_tokenizer = out_dir / "tokenizer"
|
|
if dst_encoder.exists():
|
|
shutil.rmtree(dst_encoder)
|
|
if dst_tokenizer.exists():
|
|
shutil.rmtree(dst_tokenizer)
|
|
dst_encoder.mkdir(parents=True, exist_ok=True)
|
|
dst_tokenizer.mkdir(parents=True, exist_ok=True)
|
|
|
|
for fname in src.iterdir():
|
|
if fname.name in {"tokenizer.model", "tokenizer.json", "tokenizer_config.json",
|
|
"special_tokens_map.json", "spiece.model"}:
|
|
shutil.copy(fname, dst_tokenizer / fname.name)
|
|
else:
|
|
shutil.copy(fname, dst_encoder / fname.name)
|
|
print(f" staged text_encoder and tokenizer from {src}")
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(description=__doc__.split("\n")[0])
|
|
parser.add_argument(
|
|
"--source",
|
|
default="GAIR/daVinci-MagiHuman",
|
|
help="HF repo id or local directory containing base/*.safetensors shards.",
|
|
)
|
|
parser.add_argument(
|
|
"--subfolder",
|
|
default="base",
|
|
choices=["base", "distill", "540p_sr", "1080p_sr"],
|
|
help="Which MagiHuman variant to convert (scope of this skill: base).",
|
|
)
|
|
parser.add_argument(
|
|
"--output",
|
|
required=True,
|
|
help="Destination directory for the Diffusers-format repo.",
|
|
)
|
|
parser.add_argument(
|
|
"--bundle-vae",
|
|
action="store_true",
|
|
help="Download Wan-AI/Wan2.2-TI2V-5B VAE into <output>/vae/.",
|
|
)
|
|
parser.add_argument(
|
|
"--cast-bf16",
|
|
action="store_true",
|
|
help=(
|
|
"Cast fp32 DiT weights to bfloat16 on save. Recommended for the "
|
|
"distill subfolder (61 GB fp32 upstream -> 30 GB bf16 artifact). "
|
|
"Keeps norms, RoPE bands, and other precision-sensitive tensors "
|
|
"in fp32."
|
|
),
|
|
)
|
|
parser.add_argument(
|
|
"--bundle-text-encoder",
|
|
action="store_true",
|
|
help="Download google/t5gemma-9b-9b-ul2 into <output>/text_encoder/ and tokenizer/. "
|
|
"Requires a write-scoped HF token with accepted terms of use.",
|
|
)
|
|
parser.add_argument(
|
|
"--bundle-audio-vae",
|
|
action="store_true",
|
|
help="Download stabilityai/stable-audio-open-1.0 VAE into <output>/audio_vae/. "
|
|
"Requires HF terms accepted for the Stability AI gated repo.",
|
|
)
|
|
parser.add_argument(
|
|
"--sr-source",
|
|
default=None,
|
|
help="Optional HF repo id or local directory containing SR DiT shards. When set, writes <output>/sr_transformer/.",
|
|
)
|
|
parser.add_argument(
|
|
"--sr-subfolder",
|
|
default="540p_sr",
|
|
choices=["540p_sr", "1080p_sr"],
|
|
help="SR source subfolder to convert into <output>/sr_transformer/.",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
out_dir = Path(args.output)
|
|
out_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
print(f"-> DiT shards from {args.source}/{args.subfolder}")
|
|
shards = _download_dit_shards(args.source, subfolder=args.subfolder)
|
|
print(f" found {len(shards)} shard(s)")
|
|
|
|
print(f"-> loading DiT state dict (cast_bf16={args.cast_bf16})")
|
|
state = _load_all_shards(shards, cast_bf16=args.cast_bf16)
|
|
print(f" total keys: {len(state)}")
|
|
_validate_state(state)
|
|
print(f" state dict validation passed")
|
|
|
|
print(f"-> writing {out_dir}/transformer/")
|
|
_write_transformer(out_dir, state, MAGI_HUMAN_BASE_ARCH)
|
|
|
|
include_sr_transformer = args.sr_source is not None
|
|
if include_sr_transformer:
|
|
print(f"-> SR DiT shards from {args.sr_source}/{args.sr_subfolder}")
|
|
sr_shards = _download_dit_shards(args.sr_source, subfolder=args.sr_subfolder)
|
|
print(f" found {len(sr_shards)} SR shard(s)")
|
|
print(f"-> loading SR DiT state dict (cast_bf16={args.cast_bf16})")
|
|
sr_state = _load_all_shards(sr_shards, cast_bf16=args.cast_bf16)
|
|
print(f" total SR keys: {len(sr_state)}")
|
|
_validate_state(sr_state)
|
|
print(" SR state dict validation passed")
|
|
print(f"-> writing {out_dir}/sr_transformer/")
|
|
_write_transformer(
|
|
out_dir,
|
|
sr_state,
|
|
MAGI_HUMAN_BASE_ARCH,
|
|
subdir="sr_transformer",
|
|
)
|
|
|
|
print(f"-> writing {out_dir}/scheduler/")
|
|
_write_scheduler(out_dir)
|
|
|
|
if args.bundle_vae:
|
|
print(f"-> bundling video VAE (Wan 2.2 TI2V-5B)")
|
|
_bundle_wan_vae(out_dir)
|
|
|
|
if args.bundle_audio_vae:
|
|
print(f"-> bundling audio VAE (Stable Audio Open 1.0)")
|
|
_bundle_sa_audio_vae(out_dir)
|
|
|
|
if args.bundle_text_encoder:
|
|
print(f"-> bundling text encoder")
|
|
_bundle_text_encoder(out_dir)
|
|
|
|
print(f"-> writing model_index.json")
|
|
_write_model_index(
|
|
out_dir,
|
|
bundle_vae=args.bundle_vae,
|
|
bundle_text=args.bundle_text_encoder,
|
|
bundle_audio_vae=args.bundle_audio_vae,
|
|
include_sr_transformer=include_sr_transformer,
|
|
sr_subfolder=args.sr_subfolder,
|
|
)
|
|
|
|
print(f"\nDone. Output at: {out_dir}")
|
|
if not args.bundle_vae:
|
|
print(" (remember to fetch Wan-AI/Wan2.2-TI2V-5B VAE separately or re-run with --bundle-vae)")
|
|
if not args.bundle_text_encoder:
|
|
print(" (remember to fetch google/t5gemma-9b-9b-ul2 separately or re-run with --bundle-text-encoder)")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|