#!/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()