Instructions to use diffusion-gif/any2ego with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Diffusers
How to use diffusion-gif/any2ego with Diffusers:
pip install -U diffusers transformers accelerate
import torch from diffusers import DiffusionPipeline # switch to "mps" for apple devices pipe = DiffusionPipeline.from_pretrained("fill-in-base-model", dtype=torch.bfloat16, device_map="cuda") pipe.load_lora_weights("diffusion-gif/any2ego") prompt = "Astronaut in a jungle, cold color palette, muted colors, detailed, 8k" image = pipe(prompt).images[0] - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- Draw Things
Download tools/prepare_egoexo_model_bundle.py from diffusion-gif/any2ego: direct link, hf CLI and curl.
- Browser
- Download file 26.4 kB
-
https://huggingface.co/diffusion-gif/any2ego/resolve/main/tools/prepare_egoexo_model_bundle.py
- Command line
-
hf download hf://diffusion-gif/any2ego/tools/prepare_egoexo_model_bundle.py
-
curl -L -o prepare_egoexo_model_bundle.py https://huggingface.co/diffusion-gif/any2ego/resolve/main/tools/prepare_egoexo_model_bundle.py
26.4 kB
| #!/usr/bin/env python3 | |
| """Prepare selected historical Any2Ego checkpoints and size-bounded base models. | |
| CPU/file I/O only. No data archive, training, inference, API call or upload. | |
| Original checkpoints stay read-only. Sources are explicit CLI paths. Re-running | |
| checks cached shards and existing files rather than overwriting arbitrary data. | |
| """ | |
| from concurrent.futures import ThreadPoolExecutor | |
| import argparse | |
| import hashlib | |
| import json | |
| import os | |
| from pathlib import Path | |
| import shutil | |
| import sys | |
| from reshard_weights import BLOCK, SIDECARS, now, read_header, reshard, sha_file, write_atomic_json, load_source | |
| LIMIT = 5_000_000_000 | |
| SELECTED = [{'component': 'generator_hoi', 'source': 'outputs/train_vace_hoi_lora_r128_sq_sq480/epoch-4.safetensors', 'path': 'checkpoints/generator_hoi/ckpt.safetensors', 'selection': 'paper-index HOI LoRA, epoch4', 'sha256': '16760be1abceff62f816e2837ba6517ea84d504263f37cc756d5cc3eb627c68d'}, {'component': 'generator_global', 'source': 'outputs/train_caption_hoi_v2clean_sq_sq480/epoch-4.safetensors', 'path': 'checkpoints/generator_global/ckpt.safetensors', 'selection': 'paper-index Global caption branch, epoch4', 'sha256': 'f7b83d712eac12caeb772d926165fce76b2ac02a413536f354d875d40ed345aa'}, {'component': 'generator_spatial', 'source': 'outputs/train_caption_hoi_v2clean_sq_teacher_sroute_capdrop15/epoch-2.safetensors', 'path': 'checkpoints/generator_spatial/ckpt.safetensors', 'selection': 'paper-index Spatial caption branch, epoch2; historical teacher caption training', 'sha256': '940a2bc83f63056d4672562947e9ae503b1489932f41b1a917e5d0a8285f9fe0'}, {'component': 'reward_preview', 'source': 'outputs/train_caption_hoi_v2clean_sq_teacher_oracle_capdrop15/epoch-4.safetensors', 'path': 'checkpoints/reward_preview/ckpt.safetensors', 'selection': 'teacher-caption Global reward preview, epoch4', 'sha256': '79732eef0f1efbb90b8d7090fb17b4946cc7b84e1915b57995bf3536f2559e55'}, {'component': 'mask_predictor', 'source': 'outputs/train_phiseg_bugfix_exofull/epoch-9.pt', 'path': 'checkpoints/mask_predictor/ckpt.pt', 'selection': 'historical evaluated PhiSeg, epoch9; exo_full_context/BN settings must match', 'sha256': '3dccf97f938c22f6143020164431a956e0b6335439a691a9b38af99a7d62ba7d'}, {'component': 'sft_only', 'source': 'outputs/train_student_qwen_b200fix/v0-20260911-034450/checkpoint-4070/adapter_model.safetensors', 'path': 'checkpoints/sft_only/adapter_model.safetensors', 'selection': 'Qwen3-VL-8B LoRA, step4070', 'sha256': '072df399b4320b17313a64fb7c024d1f981464e9852c979f4644391e77853f2e'}, {'component': 'caption_grpo', 'source': 'outputs/train_captioner_grpo/v2-20260917-122629/checkpoint-3800/adapter_model.safetensors', 'path': 'checkpoints/caption_grpo/adapter_model.safetensors', 'selection': 'Qwen3-VL-8B GRPO LoRA, step3800', 'sha256': '290aacd2808639e3c3e10c81c69acfdd843100b8888ad183a2e03ba6e43f5d11'}, {'component': 'mask_predictor_paperalign_candidate', 'source': 'outputs/train_phiseg_paperalign/epoch-4.pt', 'path': 'research/mask_predictor_paperalign/epoch-4.pt', 'selection': 'last existing paper-align checkpoint, not validated best/final', 'sha256': '4cd0d08b16782c7ca3566db23d378b206880203f013b3c017791712aa66d0704'}, {'component': 'vlm_comparison', 'source': 'outputs/train_vlm_hoi_sq480/epoch-0.safetensors', 'path': 'research/vlm_comparison/epoch-0.safetensors', 'selection': 'paper-index continuous VLM comparison, epoch0', 'sha256': 'f6abd392ec7b18e182c1e4c0e840794aa29c629036b290c013fe6ee4c0df1de4'}, {'component': 'joint_branch', 'source': 'outputs/train_joint_hoi/epoch-3.safetensors', 'path': 'research/joint_branch/epoch-3.safetensors', 'selection': 'last normal joint checkpoint; gate-edited ablations excluded', 'sha256': '522d9ff9e721ab7ee9a7272a6dfbdf0aaedb1c6c4e8f5fa1d9b27ac73ba0fad0'}, {'component': 'caption_dpo', 'source': 'outputs/train_caption_dpo_v2clean_sq_sq480/epoch-2.safetensors', 'path': 'research/caption_dpo/epoch-2.safetensors', 'selection': 'last available DPO checkpoint, not asserted best', 'sha256': 'ff13d7f409c6392a00f3dfcb42c32fccb11c77b4ad2f721e276fe96513017bbf'}, {'component': 'native_narr_dit', 'source': 'outputs/train_narr_caption_dit_r128/epoch-4.safetensors', 'path': 'research/native_narr_dit/epoch-4.safetensors', 'selection': 'native narration DiT LoRA, epoch4', 'sha256': 'a112106d5a2bd851c3a635ac752268aa8154a1eab5101d4ee762f97f64fa3b1a'}] | |
| PREVIOUS_CODE = { | |
| 'generator_defaults.py': 'f576a6ceaa29cfb78ba8d783ddadf52bffcfb5f30a8740d55f9e55af1b0a4369', | |
| 'diffusion/infer_caption.py': '377b52f76012189f6ca779c645d2891a8769e160c27a11d09a5591889de93aeb', | |
| 'diffusion/infer_vace_hoi.py': '37e8ecb9f92551d048524226f22f4c0f86086eb69ca6e9a7bf56d69b47c955d4', | |
| 'model_resources.py': None, | |
| } | |
| def copy_verified(source, target, expected=None): | |
| source, target = Path(source), Path(target) | |
| stat = source.stat() | |
| if not source.is_file() or stat.st_size > LIMIT: | |
| raise ValueError('unexpected source or large file must be sharded: ' + str(source)) | |
| if target.is_symlink(): | |
| raise ValueError('refusing to write through an existing symlink') | |
| if target.exists(): | |
| if not target.is_file(): | |
| raise ValueError('destination is not a regular file') | |
| source_hash = sha_file(source) | |
| if expected and source_hash != expected: | |
| raise ValueError('selected source checkpoint hash differs') | |
| if target.stat().st_size != stat.st_size or sha_file(target) != source_hash: | |
| raise ValueError('existing destination differs; will not overwrite it: ' + str(target)) | |
| return {'bytes': stat.st_size, 'sha256': source_hash} | |
| target.parent.mkdir(parents=True, exist_ok=True) | |
| temporary = target.with_name(target.name + '.partial') | |
| digest = hashlib.sha256() | |
| with source.open('rb') as inp, temporary.open('wb') as out: | |
| for block in iter(lambda: inp.read(BLOCK), b''): | |
| out.write(block) | |
| digest.update(block) | |
| out.flush() | |
| actual = digest.hexdigest() | |
| if expected and actual != expected: | |
| raise ValueError('selected source checkpoint hash differs') | |
| after = source.stat() | |
| if (after.st_size, after.st_mtime_ns) != (stat.st_size, stat.st_mtime_ns): | |
| raise ValueError('source changed during copy') | |
| if temporary.stat().st_size != stat.st_size or sha_file(temporary) != actual: | |
| raise ValueError('independent copy readback SHA256 differs') | |
| temporary.chmod(0o644) | |
| temporary.replace(target) | |
| print('COPIED', target, f'{stat.st_size / 1e9:.3f} GB', flush=True) | |
| return {'bytes': stat.st_size, 'sha256': actual} | |
| def selected_weights(project, output): | |
| result = [] | |
| for row in SELECTED: | |
| source, target = project / row['source'], output / row['path'] | |
| record = copy_verified(source, target, row['sha256']) | |
| item = {**row, **record, 'format_preserved': True} | |
| if source.suffix == '.safetensors': | |
| tensors, _, _ = read_header(source) | |
| item['tensor_count'] = len(tensors) | |
| item['has_spatial_route'] = any('spatial_route' in tensor['name'] for tensor in tensors) | |
| if row['component'] in ('sft_only', 'caption_grpo'): | |
| for name in ('adapter_config.json', 'additional_config.json'): | |
| config = json.loads((source.parent / name).read_text()) | |
| if name == 'adapter_config.json': | |
| config['base_model_name_or_path'] = 'Qwen/Qwen3-VL-8B-Instruct' | |
| destination = target.parent / name | |
| text = json.dumps(config, ensure_ascii=False, indent=2) + '\n' | |
| if destination.exists() and destination.read_text() != text: | |
| raise ValueError('existing adapter config differs') | |
| destination.write_text(text) | |
| item['config_normalization'] = 'only base_model_name_or_path normalized to official Qwen ID' | |
| result.append(item) | |
| return result | |
| def copied_vlm(vlm, output): | |
| root, index, tensors, files, _, _ = load_source(vlm) | |
| if any(info['bytes'] > LIMIT for info in files): | |
| raise ValueError('VLM base needs resharding before copying') | |
| dest = output / 'models/Qwen3-VL-8B-Instruct' | |
| for name in [info['name'] for info in files] + ['model.safetensors.index.json']: | |
| copy_verified(root / name, dest / name) | |
| for name in sorted(SIDECARS | {'README.md'}): | |
| if (root / name).is_file(): | |
| copy_verified(root / name, dest / name) | |
| return dict(component='captioner_base', base_model='Qwen/Qwen3-VL-8B-Instruct', | |
| path='models/Qwen3-VL-8B-Instruct', shard_count=len(files), | |
| tensor_count=len(tensors), unchanged_original_shards=True, | |
| raw_tensor_bytes=sum(t['nbytes'] for t in tensors)) | |
| def copied_clip(clip, output): | |
| dest = output / 'models/CLIP-ViT-H-14-laion2B-s32B-b79K' | |
| copy_verified(clip / 'model.safetensors', dest / 'model.safetensors') | |
| for name in sorted(SIDECARS): | |
| if (clip / name).is_file(): | |
| copy_verified(clip / name, dest / name) | |
| copy_verified(clip / 'README.md', dest / 'UPSTREAM_README.md') | |
| (dest / 'README.md').write_text("""--- | |
| license: mit | |
| library_name: transformers | |
| pipeline_tag: zero-shot-image-classification | |
| --- | |
| # CLIP ViT-H/14 base dependency for Any2Ego | |
| The original Transformers `model.safetensors` and processor/tokenizer files | |
| are copied unchanged and hash-verified. This is not an Any2Ego-trained model. | |
| The project uses `CLIPModel`/`CLIPProcessor.from_pretrained` here; redundant | |
| OpenCLIP/bin copies are intentionally omitted. See `UPSTREAM_README.md` for | |
| source model documentation and terms. Prepared locally, not uploaded. | |
| """) | |
| tensors, _, _ = read_header(dest / 'model.safetensors') | |
| return dict(component='reward_clip_base', base_model='laion/CLIP-ViT-H-14-laion2B-s32B-b79K', | |
| path='models/CLIP-ViT-H-14-laion2B-s32B-b79K', shard_count=1, | |
| tensor_count=len(tensors), format_preserved=True) | |
| def ensure_shards(source, dest, prefix, kind, resume): | |
| manifest_path = dest / 'reshard_manifest.json' | |
| if not manifest_path.is_file(): | |
| return reshard(source, dest, LIMIT, kind, resume, prefix, | |
| base_model='Wan-AI/Wan2.1-VACE-1.3B') | |
| manifest = json.loads(manifest_path.read_text()) | |
| spec = manifest['spec'] | |
| if spec['source']['path'] != str(source.resolve()) or spec['max_shard_bytes'] != LIMIT or spec['prefix'] != prefix: | |
| raise ValueError('cached shards have different inputs') | |
| for row in spec['source_files']: | |
| stat = (source.parent / row['name']).stat() | |
| if (stat.st_size, stat.st_mtime_ns) != (row['bytes'], row['mtime_ns']): | |
| raise ValueError('cached shard source changed') | |
| if sha_file(source.parent / row['name']) != manifest['source_file_sha256'][row['name']]: | |
| raise ValueError('cached shard source SHA256 differs') | |
| if not all(manifest['validation'].values()): | |
| raise ValueError('cached shards not fully validated') | |
| if sha_file(dest / (prefix + '.safetensors.index.json')) != manifest['output_index_sha256']: | |
| raise ValueError('cached shard index changed') | |
| for row in manifest['output_shards']: | |
| path = dest / row['name'] | |
| if path.stat().st_size != row['bytes'] or row['bytes'] > LIMIT or sha_file(path) != row['sha256']: | |
| raise ValueError('cached shard file changed') | |
| print('REUSING VERIFIED SHARDS', dest, flush=True) | |
| return manifest | |
| def support_code(core, output): | |
| support = output / 'code_support' | |
| rows = [] | |
| for name, previous in PREVIOUS_CODE.items(): | |
| record = copy_verified(core / name, support / name) | |
| rows.append(dict(path=name, previous_sha256=previous, new_sha256=record['sha256'])) | |
| copy_verified(core / 'project_paths.py', support / 'project_paths.py') | |
| write_atomic_json(support / 'loader_patch.json', { | |
| 'note': 'Installer patches only exact known original files, never unknown local edits.', 'files': rows}) | |
| tools = Path(__file__).resolve().parent | |
| for source in sorted(tools.glob('*.py')): | |
| copy_verified(source, output / 'tools' / source.name) | |
| for source in sorted((tools / 'tests').glob('*.py')): | |
| copy_verified(source, output / 'tools/tests' / source.name) | |
| def native_reader_validation(output): | |
| os.environ['CUDA_VISIBLE_DEVICES'] = '' | |
| from safetensors import safe_open | |
| import torch | |
| records, total = [], 0 | |
| for path in sorted(output.rglob('*.safetensors')): | |
| tensors, metadata, _ = read_header(path) | |
| expected = {t['name']: t for t in tensors} | |
| with safe_open(path, framework='pt', device='cpu') as reader: | |
| if set(reader.keys()) != set(expected) or (reader.metadata() or {}) != metadata: | |
| raise ValueError('native reader tensor names/metadata differ') | |
| for name in reader.keys(): | |
| value = reader.get_slice(name) | |
| if list(value.get_shape()) != expected[name]['shape'] or value.get_dtype() != expected[name]['dtype']: | |
| raise ValueError('native reader shape/dtype differs') | |
| records.append(dict(path=str(path.relative_to(output)), tensor_count=len(tensors), | |
| names_shapes_dtypes_verified=True)) | |
| total += len(tensors) | |
| if torch.cuda.is_initialized(): | |
| raise RuntimeError('unexpected CUDA initialization') | |
| result = dict(reader='safetensors.safe_open(framework=pt, device=cpu), slices only', | |
| safetensors_files=len(records), tensor_entries=total, files=records, | |
| CUDA_initialized=False, model_loaded=False, inference_run=False) | |
| write_atomic_json(output / 'native_reader_validation.json', result) | |
| return result | |
| def root_readme(output, trained, transformer, encoder, vlm): | |
| rows = '\n'.join(f"| {r['component']} | {r['bytes']/1024**2:.1f} | {r['selection']} |" for r in trained) | |
| text = f"""--- | |
| tags: | |
| - exo-to-ego | |
| - video-generation | |
| - safetensors | |
| - lora | |
| - research | |
| --- | |
| # Ego-Exo / Any2Ego — 本地 5GB 权重分片包 | |
| **只整理权重,不是新训练结果,也尚未上传 HF。** 所有文件不超过 | |
| 5,000,000,000 字节(包含 safetensors 头)。不是 split/tar 片段; | |
| 模型张量保留完整,一个张量不会被切成两半。原始模型/检查点没有修改。 | |
| ## 目录与分片 | |
| - `models/Wan2.1-VACE-1.3B/transformer/`:原生 DiT+VACE 底座,{len(transformer['output_shards'])} 片; | |
| `diffusion_pytorch_model.safetensors.index.json`。 | |
| - `models/Wan2.1-VACE-1.3B/text_encoder/`:原生 umT5-XXL BF16,{len(encoder['output_shards'])} 片; | |
| `model.safetensors.index.json`。原 .pth 的完整存储逐字节转存 safetensors, | |
| 不改变 key、shape、dtype 或值;只用 weights_only/FakeTensor 读取元信息。 | |
| - Wan VAE 和 `google/umt5-xxl/` tokenizer 保留原格式。 | |
| - `models/Qwen3-VL-8B-Instruct/`:官方基座原有 {vlm['shard_count']} 片已小于 5GB,完整保留。 | |
| - `models/CLIP-ViT-H-14-laion2B-s32B-b79K/`:Transformers 格式原单文件保留; | |
| 未重复打包 OpenCLIP/bin 副本。 | |
| - `checkpoints/`:7 个主链路训练产物;`research/`:5 个辅助实验快照。 | |
| 它们本来就小于 5GB,保留原 safetensors/.pt 与必要的 adapter 配置。 | |
| - `code_support/`、`tools/`:分片加载兼容补丁、校验/安装/重新分片工具与 CPU 小测试。 | |
| 基础模型仍是上游模型,不是合并后的 Any2Ego 训练权重;没有合并 LoRA。 | |
| 上游模型各自遵守原许可证,不能把整个 bundle 统一声明为新的自有模型。 | |
| 训练数据、caption、视频、optimizer/RNG/log/缓存不包含在这个**权重包**里。 | |
| ## 训练产物选择 | |
| | Component | MiB | 来源/选择 | | |
| |---|---:|---| | |
| {rows} | |
| Historical Spatial epoch2 使用 teacher captions 训练,不冒称新的 GRPO-caption | |
| 重训结果;PhiSeg epoch9 对应 exo_full_context/BN 实验支路,推理需匹配 | |
| `--exo_full_context --bn_batch_stats` 与数据设置。paperalign epoch4 只是候选, | |
| 未声明最佳/收敛。辅助 joint/VLM/DPO/native-narr 权重不可替代主分支。 | |
| SFT/GRPO 为 Qwen3-VL-8B 的 rank16/alpha32 LoRA,非机器人项目的 27B 权重。 | |
| Adapter 权重原样保留,配置只将基座路径规范为官方 Qwen ID。 | |
| ## 接到现有代码(不会自动训练或推理) | |
| GitHub 源码:`https://github.com/coisini-gif/exo-ego-video-converter`(私密仓库需访问权限)。 | |
| 下载权重包后,在源码 checkout 的 `code/core` 下使用模型。 | |
| ```bash | |
| BUNDLE=/your/downloaded/ego_exo_5gb_20261102 | |
| CORE=/your/exo-ego-video-converter/code/core | |
| # 先看计划;无写入、无模型加载 | |
| python "$BUNDLE/tools/attach_model_bundle.py" --bundle "$BUNDLE" --code-root "$CORE" | |
| # 仅对不存在的资源建链接,旧版 loader 按已知 SHA256 安全补丁;有自定义改动会拒绝覆盖 | |
| python "$BUNDLE/tools/attach_model_bundle.py" --bundle "$BUNDLE" --code-root "$CORE" --apply | |
| # 文件完整性校验(需要读完整包,但不使用 GPU) | |
| python "$BUNDLE/tools/verify_model_bundle.py" --bundle "$BUNDLE" | |
| ``` | |
| Wan 的这两个索引**不能直接传入旧版二进制 loader**。兼容补丁分别把 | |
| DiT、T5 的 shard 列表传给一个 `ModelConfig(path=[...])`,再加入 VAE; | |
| 已覆盖 HOI/Global/Spatial 推理、generator training defaults 和 reward preview | |
| 共用的 build_pipe。原单文件路径仍然兼容,`WAN_MODEL_ROOT` 可覆盖 Wan 位置。 | |
| 这不是标准 diffusers 架构转换;不要直接对 Wan 分片子目录调用 | |
| `transformers.AutoModel.from_pretrained`,也不需要先合并回 >5GB 文件。 | |
| 若用 research 旧入口,设置 `MODEL_ROOT=$BUNDLE/models`,并显式指定对应 | |
| `$BUNDLE/research/...` 检查点;这些旧入口仍需用 `wan_model_paths` 解析 Wan | |
| 组件,或采用 core 主入口。未声称所有历史脚本均已自动改为分片加载。 | |
| 运行推理/训练还需要论文对应数据、mask、caption、第三方框架和正确版本。 | |
| **权重完整性校验并不等于完整 GPU 复现或论文指标复现。** | |
| ## 校验与 HF | |
| `bundle_manifest.json` / `CHECKSUMS.sha256` 提供文件哈希;两个大组件各自的 | |
| `reshard_manifest.json` 还包含每个原张量的 SHA256,输出独立回读逐张量验证。 | |
| `native_reader_validation.json` 使用原生 safetensors CPU reader 验证所有 | |
| 名称、形状和 dtype,未实例化模型/分配 GPU。 | |
| 整个目录可作为 **private HF model repo** 的上传目录(含模型/权重 README)。 | |
| 如只发布自训产物,可仅上传 `checkpoints/` 和 `research/`,依赖官方基座。 | |
| 此步骤**没有任何网络上传**,上传仍需个人 HF Write 登录/token;GitHub token | |
| 不能用于 HF。不保存或打包密钥。可在 HF 单独建 repo 后使用 `hf upload` | |
| 或 huggingface_hub 上传目录;Wan 模型资源布局需保留不变。 | |
| """ | |
| (output / 'README.md').write_text(text) | |
| def main(): | |
| parser = argparse.ArgumentParser(description=__doc__) | |
| parser.add_argument('--project-root', type=Path, required=True) | |
| parser.add_argument('--wan-base', type=Path, required=True) | |
| parser.add_argument('--vlm-base', type=Path, required=True) | |
| parser.add_argument('--clip-base', type=Path, required=True) | |
| parser.add_argument('--code-root', type=Path, required=True) | |
| parser.add_argument('--output-dir', type=Path, required=True) | |
| parser.add_argument('--resume', action='store_true') | |
| args = parser.parse_args() | |
| os.environ['CUDA_VISIBLE_DEVICES'] = '' | |
| output = args.output_dir.resolve() | |
| for source in (args.project_root, args.wan_base, args.vlm_base, args.clip_base, args.code_root): | |
| source = source.resolve() | |
| if output == source or source in output.parents or output in source.parents: | |
| raise ValueError('output must not overlap an original resource/code directory') | |
| output.mkdir(parents=True, exist_ok=True) | |
| spec = dict(project_root=str(args.project_root.resolve()), wan_base=str(args.wan_base.resolve()), | |
| vlm_base=str(args.vlm_base.resolve()), clip_base=str(args.clip_base.resolve()), max_file_bytes=LIMIT) | |
| state_path = output / '.bundle_state.json' | |
| if state_path.exists() and json.loads(state_path.read_text()) != spec: | |
| raise ValueError('existing bundle state has different source paths') | |
| if not state_path.exists() and any(output.iterdir()): | |
| existing = output / 'bundle_manifest.json' | |
| if not existing.is_file() or json.loads(existing.read_text()).get('sources') != spec: | |
| raise ValueError('unrelated nonempty output directory; use a fresh directory') | |
| from verify_model_bundle import verify | |
| verify(output) | |
| write_atomic_json(state_path, spec) | |
| with ThreadPoolExecutor(max_workers=3) as pool: | |
| f_trained = pool.submit(selected_weights, args.project_root, output) | |
| f_vlm = pool.submit(copied_vlm, args.vlm_base, output) | |
| f_clip = pool.submit(copied_clip, args.clip_base, output) | |
| trained, vlm, clip = f_trained.result(), f_vlm.result(), f_clip.result() | |
| wan_dest = output / 'models/Wan2.1-VACE-1.3B' | |
| for name in ('Wan2.1_VAE.pth', 'config.json', 'LICENSE.txt'): | |
| copy_verified(args.wan_base / name, wan_dest / name) | |
| for source in sorted((args.wan_base / 'google/umt5-xxl').iterdir()): | |
| if source.is_file() and source.name in SIDECARS: | |
| copy_verified(source, wan_dest / 'google/umt5-xxl' / source.name) | |
| transformer = ensure_shards(args.wan_base / 'diffusion_pytorch_model.safetensors', wan_dest / 'transformer', | |
| 'diffusion_pytorch_model', | |
| 'Wan2.1-VACE-1.3B native base transformer (DiT and VACE, not trained Any2Ego branches)', args.resume) | |
| encoder = ensure_shards(args.wan_base / 'models_t5_umt5-xxl-enc-bf16.pth', wan_dest / 'text_encoder', | |
| 'model', 'Wan native umT5-XXL BF16 text encoder (lossless PyTorch storage to safetensors)', args.resume) | |
| write_atomic_json(wan_dest / 'model_components.json', { | |
| 'loader': 'DiffSynth ModelConfig: one str/list per component', | |
| 'transformer': 'transformer/diffusion_pytorch_model.safetensors.index.json', | |
| 'text_encoder': 'text_encoder/model.safetensors.index.json', 'vae': 'Wan2.1_VAE.pth', | |
| 'tokenizer': 'google/umt5-xxl', 'tensor_key_architecture_conversion': False}) | |
| (wan_dest / 'README.md').write_text("""# Wan2.1-VACE-1.3B base dependency (native DiffSynth keys) | |
| Upstream: Wan-AI/Wan2.1-VACE-1.3B, original LICENSE.txt included. Not trained | |
| Any2Ego branches. Native DiT/VACE is in transformer/; native BF16 umT5 is in | |
| text_encoder/. Supply shard lists to separate ModelConfig objects. VAE and | |
| umt5 tokenizer stay unchanged. See the parent README/code_support for loading. | |
| This is not a diffusers/Transformers architecture conversion; no upload yet. | |
| """) | |
| support_code(args.code_root, output) | |
| native = native_reader_validation(output) | |
| sys.path.insert(0, str(args.code_root)) | |
| from model_resources import wan_model_paths, wan_tokenizer_path | |
| paths = wan_model_paths(wan_dest, strict=True) | |
| if [len(paths[0]), len(paths[1])] != [len(transformer['output_shards']), len(encoder['output_shards'])]: | |
| raise ValueError('sharded loader path count differs') | |
| wan_tokenizer_path(wan_dest, strict=True) | |
| root_readme(output, trained, transformer, encoder, vlm) | |
| files = [] | |
| for path in sorted(output.rglob('*')): | |
| if path.is_symlink(): | |
| raise ValueError('upload bundle must contain actual files, not symlinks') | |
| if not path.is_file() or path.name in ('bundle_manifest.json', 'CHECKSUMS.sha256', '.bundle_state.json'): | |
| continue | |
| if path.suffix == '.partial' or path.name.startswith('.reshard') or path.name.endswith('.pyc'): | |
| raise ValueError('unfinished/cache file must not enter completed model bundle') | |
| if path.stat().st_size > LIMIT: | |
| raise ValueError('file limit exceeded') | |
| files.append(dict(path=str(path.relative_to(output)), bytes=path.stat().st_size, sha256=sha_file(path))) | |
| manifest = dict(schema_version=1, status='complete', created_at_UTC=now(), sources=spec, | |
| max_file_bytes=LIMIT, trained_components=trained, | |
| base_components=[{'component': 'wan_transformer', 'path': 'models/Wan2.1-VACE-1.3B/transformer', | |
| 'shard_count': len(transformer['output_shards']), 'tensor_count': transformer['spec']['tensor_count']}, | |
| {'component': 'wan_text_encoder', 'path': 'models/Wan2.1-VACE-1.3B/text_encoder', | |
| 'shard_count': len(encoder['output_shards']), 'tensor_count': encoder['spec']['tensor_count']}, vlm, clip], | |
| files=files, total_file_bytes=sum(row['bytes'] for row in files), | |
| largest_file_bytes=max(row['bytes'] for row in files), native_reader=native['reader'], | |
| validation={'all_copied_weights_read_back_SHA256_verified': True, | |
| 'resharded_tensor_bytes_independently_SHA256_verified': True, | |
| 'native_reader_names_shapes_dtypes_verified': True, | |
| 'core_sharded_loader_paths_verified': True}, | |
| GPU_used=False, model_loaded=False, inference_run=False, training_run=False, upload_run=False, | |
| raw_dataset_included=False) | |
| write_atomic_json(output / 'bundle_manifest.json', manifest) | |
| checksum_rows = files + [dict(path='bundle_manifest.json', sha256=sha_file(output / 'bundle_manifest.json'))] | |
| (output / 'CHECKSUMS.sha256').write_text(''.join(row['sha256'] + ' ' + row['path'] + '\n' for row in checksum_rows)) | |
| state_path.unlink() | |
| print(json.dumps({'output': str(output), 'status': 'complete', 'trained_components': len(trained), | |
| 'wan_transformer_shards': len(transformer['output_shards']), | |
| 'wan_text_encoder_shards': len(encoder['output_shards']), 'qwen_shards': vlm['shard_count'], | |
| 'total_GB': round(manifest['total_file_bytes']/1e9, 3), | |
| 'largest_file_bytes': manifest['largest_file_bytes'], 'GPU_used': False, 'upload_run': False}, indent=2), flush=True) | |
| if __name__ == '__main__': | |
| main() | |