Add Echo-Memory codebase used for this run (CC BY 4.0, JD Echo Team) (part 2)
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +13 -0
- code/diffsynth/tokenizer_configs/stable_diffusion_3/tokenizer_3/tokenizer.json +0 -0
- code/diffsynth/tokenizer_configs/stable_diffusion_3/tokenizer_3/tokenizer_config.json +940 -0
- code/diffsynth/tokenizer_configs/stable_diffusion_xl/tokenizer_2/merges.txt +0 -0
- code/diffsynth/tokenizer_configs/stable_diffusion_xl/tokenizer_2/special_tokens_map.json +24 -0
- code/diffsynth/tokenizer_configs/stable_diffusion_xl/tokenizer_2/tokenizer_config.json +38 -0
- code/diffsynth/tokenizer_configs/stable_diffusion_xl/tokenizer_2/vocab.json +0 -0
- code/diffsynth/trainers/__init__.py +0 -0
- code/diffsynth/trainers/latent_dataset.py +130 -0
- code/diffsynth/trainers/text_to_image.py +318 -0
- code/diffsynth/trainers/utils.py +1368 -0
- code/diffsynth/trainers/utils.py.p0bak-getitem +1367 -0
- code/diffsynth/vram_management/__init__.py +2 -0
- code/diffsynth/vram_management/gradient_checkpointing.py +34 -0
- code/diffsynth/vram_management/layers.py +170 -0
- code/doc/DEVELOPER.md +116 -0
- code/doc/README.md +13 -0
- code/doc/checkpoints.md +72 -0
- code/doc/dataset_preprocessing.md +181 -0
- code/doc/dynamic_dataset_preprocessing.md +159 -0
- code/doc/memory_mechanisms.md +41 -0
- code/docs/.nojekyll +0 -0
- code/docs/README.md +16 -0
- code/docs/assets/echo-memory-paper.pdf +3 -0
- code/docs/assets/opendomain_revisit/README.md +19 -0
- code/docs/assets/paper_cases/README.md +13 -0
- code/docs/assets/readme_previews/context_k1_replay_gt.gif +3 -0
- code/docs/assets/readme_previews/context_k5_replay_gt.gif +3 -0
- code/docs/assets/readme_previews/dyn_context_k1_replay.gif +3 -0
- code/docs/assets/readme_previews/dyn_context_k20_replay.gif +3 -0
- code/docs/assets/readme_previews/dyn_context_k5_replay.gif +3 -0
- code/docs/assets/readme_previews/dyn_spatial_memory_replay.gif +3 -0
- code/docs/assets/readme_previews/dyn_ssm_blockwise_replay.gif +3 -0
- code/docs/assets/readme_previews/dyn_ssm_legacy_replay.gif +3 -0
- code/docs/assets/readme_previews/framepack_len_r4_replay_gt.gif +3 -0
- code/docs/assets/readme_previews/spatial_memory_replay_gt.gif +3 -0
- code/docs/assets/readme_previews/ssm_blockwise_replay_gt.gif +3 -0
- code/docs/assets/readme_previews/ssm_legacy_replay_gt.gif +3 -0
- code/docs/developer.html +109 -0
- code/docs/i18n-runtime.js +141 -0
- code/docs/i18n.js +329 -0
- code/docs/index.html +644 -0
- code/docs/site.js +349 -0
- code/docs/style.css +1848 -0
- code/environment.yml +13 -0
- code/eval/metrics/README.md +120 -0
- code/eval/metrics/VISUAL_EVAL_DESIGN.md +60 -0
- code/eval/metrics/__init__.py +2 -0
- code/eval/metrics/common.py +79 -0
- code/eval/metrics/identity_preservation.py +183 -0
.gitattributes
CHANGED
|
@@ -47,3 +47,16 @@ code/assets/readme_previews/ssm_blockwise_replay_gt.gif filter=lfs diff=lfs merg
|
|
| 47 |
code/assets/readme_previews/ssm_legacy_replay_gt.gif filter=lfs diff=lfs merge=lfs -text
|
| 48 |
code/diffsynth/tokenizer_configs/hunyuan_video/tokenizer_2/tokenizer.json filter=lfs diff=lfs merge=lfs -text
|
| 49 |
code/diffsynth/tokenizer_configs/kolors/tokenizer/vocab.txt filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
code/assets/readme_previews/ssm_legacy_replay_gt.gif filter=lfs diff=lfs merge=lfs -text
|
| 48 |
code/diffsynth/tokenizer_configs/hunyuan_video/tokenizer_2/tokenizer.json filter=lfs diff=lfs merge=lfs -text
|
| 49 |
code/diffsynth/tokenizer_configs/kolors/tokenizer/vocab.txt filter=lfs diff=lfs merge=lfs -text
|
| 50 |
+
code/docs/assets/echo-memory-paper.pdf filter=lfs diff=lfs merge=lfs -text
|
| 51 |
+
code/docs/assets/readme_previews/context_k1_replay_gt.gif filter=lfs diff=lfs merge=lfs -text
|
| 52 |
+
code/docs/assets/readme_previews/context_k5_replay_gt.gif filter=lfs diff=lfs merge=lfs -text
|
| 53 |
+
code/docs/assets/readme_previews/dyn_context_k1_replay.gif filter=lfs diff=lfs merge=lfs -text
|
| 54 |
+
code/docs/assets/readme_previews/dyn_context_k20_replay.gif filter=lfs diff=lfs merge=lfs -text
|
| 55 |
+
code/docs/assets/readme_previews/dyn_context_k5_replay.gif filter=lfs diff=lfs merge=lfs -text
|
| 56 |
+
code/docs/assets/readme_previews/dyn_spatial_memory_replay.gif filter=lfs diff=lfs merge=lfs -text
|
| 57 |
+
code/docs/assets/readme_previews/dyn_ssm_blockwise_replay.gif filter=lfs diff=lfs merge=lfs -text
|
| 58 |
+
code/docs/assets/readme_previews/dyn_ssm_legacy_replay.gif filter=lfs diff=lfs merge=lfs -text
|
| 59 |
+
code/docs/assets/readme_previews/framepack_len_r4_replay_gt.gif filter=lfs diff=lfs merge=lfs -text
|
| 60 |
+
code/docs/assets/readme_previews/spatial_memory_replay_gt.gif filter=lfs diff=lfs merge=lfs -text
|
| 61 |
+
code/docs/assets/readme_previews/ssm_blockwise_replay_gt.gif filter=lfs diff=lfs merge=lfs -text
|
| 62 |
+
code/docs/assets/readme_previews/ssm_legacy_replay_gt.gif filter=lfs diff=lfs merge=lfs -text
|
code/diffsynth/tokenizer_configs/stable_diffusion_3/tokenizer_3/tokenizer.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
code/diffsynth/tokenizer_configs/stable_diffusion_3/tokenizer_3/tokenizer_config.json
ADDED
|
@@ -0,0 +1,940 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"add_prefix_space": true,
|
| 3 |
+
"added_tokens_decoder": {
|
| 4 |
+
"0": {
|
| 5 |
+
"content": "<pad>",
|
| 6 |
+
"lstrip": false,
|
| 7 |
+
"normalized": false,
|
| 8 |
+
"rstrip": false,
|
| 9 |
+
"single_word": false,
|
| 10 |
+
"special": true
|
| 11 |
+
},
|
| 12 |
+
"1": {
|
| 13 |
+
"content": "</s>",
|
| 14 |
+
"lstrip": false,
|
| 15 |
+
"normalized": false,
|
| 16 |
+
"rstrip": false,
|
| 17 |
+
"single_word": false,
|
| 18 |
+
"special": true
|
| 19 |
+
},
|
| 20 |
+
"2": {
|
| 21 |
+
"content": "<unk>",
|
| 22 |
+
"lstrip": false,
|
| 23 |
+
"normalized": false,
|
| 24 |
+
"rstrip": false,
|
| 25 |
+
"single_word": false,
|
| 26 |
+
"special": true
|
| 27 |
+
},
|
| 28 |
+
"32000": {
|
| 29 |
+
"content": "<extra_id_99>",
|
| 30 |
+
"lstrip": true,
|
| 31 |
+
"normalized": false,
|
| 32 |
+
"rstrip": true,
|
| 33 |
+
"single_word": false,
|
| 34 |
+
"special": true
|
| 35 |
+
},
|
| 36 |
+
"32001": {
|
| 37 |
+
"content": "<extra_id_98>",
|
| 38 |
+
"lstrip": true,
|
| 39 |
+
"normalized": false,
|
| 40 |
+
"rstrip": true,
|
| 41 |
+
"single_word": false,
|
| 42 |
+
"special": true
|
| 43 |
+
},
|
| 44 |
+
"32002": {
|
| 45 |
+
"content": "<extra_id_97>",
|
| 46 |
+
"lstrip": true,
|
| 47 |
+
"normalized": false,
|
| 48 |
+
"rstrip": true,
|
| 49 |
+
"single_word": false,
|
| 50 |
+
"special": true
|
| 51 |
+
},
|
| 52 |
+
"32003": {
|
| 53 |
+
"content": "<extra_id_96>",
|
| 54 |
+
"lstrip": true,
|
| 55 |
+
"normalized": false,
|
| 56 |
+
"rstrip": true,
|
| 57 |
+
"single_word": false,
|
| 58 |
+
"special": true
|
| 59 |
+
},
|
| 60 |
+
"32004": {
|
| 61 |
+
"content": "<extra_id_95>",
|
| 62 |
+
"lstrip": true,
|
| 63 |
+
"normalized": false,
|
| 64 |
+
"rstrip": true,
|
| 65 |
+
"single_word": false,
|
| 66 |
+
"special": true
|
| 67 |
+
},
|
| 68 |
+
"32005": {
|
| 69 |
+
"content": "<extra_id_94>",
|
| 70 |
+
"lstrip": true,
|
| 71 |
+
"normalized": false,
|
| 72 |
+
"rstrip": true,
|
| 73 |
+
"single_word": false,
|
| 74 |
+
"special": true
|
| 75 |
+
},
|
| 76 |
+
"32006": {
|
| 77 |
+
"content": "<extra_id_93>",
|
| 78 |
+
"lstrip": true,
|
| 79 |
+
"normalized": false,
|
| 80 |
+
"rstrip": true,
|
| 81 |
+
"single_word": false,
|
| 82 |
+
"special": true
|
| 83 |
+
},
|
| 84 |
+
"32007": {
|
| 85 |
+
"content": "<extra_id_92>",
|
| 86 |
+
"lstrip": true,
|
| 87 |
+
"normalized": false,
|
| 88 |
+
"rstrip": true,
|
| 89 |
+
"single_word": false,
|
| 90 |
+
"special": true
|
| 91 |
+
},
|
| 92 |
+
"32008": {
|
| 93 |
+
"content": "<extra_id_91>",
|
| 94 |
+
"lstrip": true,
|
| 95 |
+
"normalized": false,
|
| 96 |
+
"rstrip": true,
|
| 97 |
+
"single_word": false,
|
| 98 |
+
"special": true
|
| 99 |
+
},
|
| 100 |
+
"32009": {
|
| 101 |
+
"content": "<extra_id_90>",
|
| 102 |
+
"lstrip": true,
|
| 103 |
+
"normalized": false,
|
| 104 |
+
"rstrip": true,
|
| 105 |
+
"single_word": false,
|
| 106 |
+
"special": true
|
| 107 |
+
},
|
| 108 |
+
"32010": {
|
| 109 |
+
"content": "<extra_id_89>",
|
| 110 |
+
"lstrip": true,
|
| 111 |
+
"normalized": false,
|
| 112 |
+
"rstrip": true,
|
| 113 |
+
"single_word": false,
|
| 114 |
+
"special": true
|
| 115 |
+
},
|
| 116 |
+
"32011": {
|
| 117 |
+
"content": "<extra_id_88>",
|
| 118 |
+
"lstrip": true,
|
| 119 |
+
"normalized": false,
|
| 120 |
+
"rstrip": true,
|
| 121 |
+
"single_word": false,
|
| 122 |
+
"special": true
|
| 123 |
+
},
|
| 124 |
+
"32012": {
|
| 125 |
+
"content": "<extra_id_87>",
|
| 126 |
+
"lstrip": true,
|
| 127 |
+
"normalized": false,
|
| 128 |
+
"rstrip": true,
|
| 129 |
+
"single_word": false,
|
| 130 |
+
"special": true
|
| 131 |
+
},
|
| 132 |
+
"32013": {
|
| 133 |
+
"content": "<extra_id_86>",
|
| 134 |
+
"lstrip": true,
|
| 135 |
+
"normalized": false,
|
| 136 |
+
"rstrip": true,
|
| 137 |
+
"single_word": false,
|
| 138 |
+
"special": true
|
| 139 |
+
},
|
| 140 |
+
"32014": {
|
| 141 |
+
"content": "<extra_id_85>",
|
| 142 |
+
"lstrip": true,
|
| 143 |
+
"normalized": false,
|
| 144 |
+
"rstrip": true,
|
| 145 |
+
"single_word": false,
|
| 146 |
+
"special": true
|
| 147 |
+
},
|
| 148 |
+
"32015": {
|
| 149 |
+
"content": "<extra_id_84>",
|
| 150 |
+
"lstrip": true,
|
| 151 |
+
"normalized": false,
|
| 152 |
+
"rstrip": true,
|
| 153 |
+
"single_word": false,
|
| 154 |
+
"special": true
|
| 155 |
+
},
|
| 156 |
+
"32016": {
|
| 157 |
+
"content": "<extra_id_83>",
|
| 158 |
+
"lstrip": true,
|
| 159 |
+
"normalized": false,
|
| 160 |
+
"rstrip": true,
|
| 161 |
+
"single_word": false,
|
| 162 |
+
"special": true
|
| 163 |
+
},
|
| 164 |
+
"32017": {
|
| 165 |
+
"content": "<extra_id_82>",
|
| 166 |
+
"lstrip": true,
|
| 167 |
+
"normalized": false,
|
| 168 |
+
"rstrip": true,
|
| 169 |
+
"single_word": false,
|
| 170 |
+
"special": true
|
| 171 |
+
},
|
| 172 |
+
"32018": {
|
| 173 |
+
"content": "<extra_id_81>",
|
| 174 |
+
"lstrip": true,
|
| 175 |
+
"normalized": false,
|
| 176 |
+
"rstrip": true,
|
| 177 |
+
"single_word": false,
|
| 178 |
+
"special": true
|
| 179 |
+
},
|
| 180 |
+
"32019": {
|
| 181 |
+
"content": "<extra_id_80>",
|
| 182 |
+
"lstrip": true,
|
| 183 |
+
"normalized": false,
|
| 184 |
+
"rstrip": true,
|
| 185 |
+
"single_word": false,
|
| 186 |
+
"special": true
|
| 187 |
+
},
|
| 188 |
+
"32020": {
|
| 189 |
+
"content": "<extra_id_79>",
|
| 190 |
+
"lstrip": true,
|
| 191 |
+
"normalized": false,
|
| 192 |
+
"rstrip": true,
|
| 193 |
+
"single_word": false,
|
| 194 |
+
"special": true
|
| 195 |
+
},
|
| 196 |
+
"32021": {
|
| 197 |
+
"content": "<extra_id_78>",
|
| 198 |
+
"lstrip": true,
|
| 199 |
+
"normalized": false,
|
| 200 |
+
"rstrip": true,
|
| 201 |
+
"single_word": false,
|
| 202 |
+
"special": true
|
| 203 |
+
},
|
| 204 |
+
"32022": {
|
| 205 |
+
"content": "<extra_id_77>",
|
| 206 |
+
"lstrip": true,
|
| 207 |
+
"normalized": false,
|
| 208 |
+
"rstrip": true,
|
| 209 |
+
"single_word": false,
|
| 210 |
+
"special": true
|
| 211 |
+
},
|
| 212 |
+
"32023": {
|
| 213 |
+
"content": "<extra_id_76>",
|
| 214 |
+
"lstrip": true,
|
| 215 |
+
"normalized": false,
|
| 216 |
+
"rstrip": true,
|
| 217 |
+
"single_word": false,
|
| 218 |
+
"special": true
|
| 219 |
+
},
|
| 220 |
+
"32024": {
|
| 221 |
+
"content": "<extra_id_75>",
|
| 222 |
+
"lstrip": true,
|
| 223 |
+
"normalized": false,
|
| 224 |
+
"rstrip": true,
|
| 225 |
+
"single_word": false,
|
| 226 |
+
"special": true
|
| 227 |
+
},
|
| 228 |
+
"32025": {
|
| 229 |
+
"content": "<extra_id_74>",
|
| 230 |
+
"lstrip": true,
|
| 231 |
+
"normalized": false,
|
| 232 |
+
"rstrip": true,
|
| 233 |
+
"single_word": false,
|
| 234 |
+
"special": true
|
| 235 |
+
},
|
| 236 |
+
"32026": {
|
| 237 |
+
"content": "<extra_id_73>",
|
| 238 |
+
"lstrip": true,
|
| 239 |
+
"normalized": false,
|
| 240 |
+
"rstrip": true,
|
| 241 |
+
"single_word": false,
|
| 242 |
+
"special": true
|
| 243 |
+
},
|
| 244 |
+
"32027": {
|
| 245 |
+
"content": "<extra_id_72>",
|
| 246 |
+
"lstrip": true,
|
| 247 |
+
"normalized": false,
|
| 248 |
+
"rstrip": true,
|
| 249 |
+
"single_word": false,
|
| 250 |
+
"special": true
|
| 251 |
+
},
|
| 252 |
+
"32028": {
|
| 253 |
+
"content": "<extra_id_71>",
|
| 254 |
+
"lstrip": true,
|
| 255 |
+
"normalized": false,
|
| 256 |
+
"rstrip": true,
|
| 257 |
+
"single_word": false,
|
| 258 |
+
"special": true
|
| 259 |
+
},
|
| 260 |
+
"32029": {
|
| 261 |
+
"content": "<extra_id_70>",
|
| 262 |
+
"lstrip": true,
|
| 263 |
+
"normalized": false,
|
| 264 |
+
"rstrip": true,
|
| 265 |
+
"single_word": false,
|
| 266 |
+
"special": true
|
| 267 |
+
},
|
| 268 |
+
"32030": {
|
| 269 |
+
"content": "<extra_id_69>",
|
| 270 |
+
"lstrip": true,
|
| 271 |
+
"normalized": false,
|
| 272 |
+
"rstrip": true,
|
| 273 |
+
"single_word": false,
|
| 274 |
+
"special": true
|
| 275 |
+
},
|
| 276 |
+
"32031": {
|
| 277 |
+
"content": "<extra_id_68>",
|
| 278 |
+
"lstrip": true,
|
| 279 |
+
"normalized": false,
|
| 280 |
+
"rstrip": true,
|
| 281 |
+
"single_word": false,
|
| 282 |
+
"special": true
|
| 283 |
+
},
|
| 284 |
+
"32032": {
|
| 285 |
+
"content": "<extra_id_67>",
|
| 286 |
+
"lstrip": true,
|
| 287 |
+
"normalized": false,
|
| 288 |
+
"rstrip": true,
|
| 289 |
+
"single_word": false,
|
| 290 |
+
"special": true
|
| 291 |
+
},
|
| 292 |
+
"32033": {
|
| 293 |
+
"content": "<extra_id_66>",
|
| 294 |
+
"lstrip": true,
|
| 295 |
+
"normalized": false,
|
| 296 |
+
"rstrip": true,
|
| 297 |
+
"single_word": false,
|
| 298 |
+
"special": true
|
| 299 |
+
},
|
| 300 |
+
"32034": {
|
| 301 |
+
"content": "<extra_id_65>",
|
| 302 |
+
"lstrip": true,
|
| 303 |
+
"normalized": false,
|
| 304 |
+
"rstrip": true,
|
| 305 |
+
"single_word": false,
|
| 306 |
+
"special": true
|
| 307 |
+
},
|
| 308 |
+
"32035": {
|
| 309 |
+
"content": "<extra_id_64>",
|
| 310 |
+
"lstrip": true,
|
| 311 |
+
"normalized": false,
|
| 312 |
+
"rstrip": true,
|
| 313 |
+
"single_word": false,
|
| 314 |
+
"special": true
|
| 315 |
+
},
|
| 316 |
+
"32036": {
|
| 317 |
+
"content": "<extra_id_63>",
|
| 318 |
+
"lstrip": true,
|
| 319 |
+
"normalized": false,
|
| 320 |
+
"rstrip": true,
|
| 321 |
+
"single_word": false,
|
| 322 |
+
"special": true
|
| 323 |
+
},
|
| 324 |
+
"32037": {
|
| 325 |
+
"content": "<extra_id_62>",
|
| 326 |
+
"lstrip": true,
|
| 327 |
+
"normalized": false,
|
| 328 |
+
"rstrip": true,
|
| 329 |
+
"single_word": false,
|
| 330 |
+
"special": true
|
| 331 |
+
},
|
| 332 |
+
"32038": {
|
| 333 |
+
"content": "<extra_id_61>",
|
| 334 |
+
"lstrip": true,
|
| 335 |
+
"normalized": false,
|
| 336 |
+
"rstrip": true,
|
| 337 |
+
"single_word": false,
|
| 338 |
+
"special": true
|
| 339 |
+
},
|
| 340 |
+
"32039": {
|
| 341 |
+
"content": "<extra_id_60>",
|
| 342 |
+
"lstrip": true,
|
| 343 |
+
"normalized": false,
|
| 344 |
+
"rstrip": true,
|
| 345 |
+
"single_word": false,
|
| 346 |
+
"special": true
|
| 347 |
+
},
|
| 348 |
+
"32040": {
|
| 349 |
+
"content": "<extra_id_59>",
|
| 350 |
+
"lstrip": true,
|
| 351 |
+
"normalized": false,
|
| 352 |
+
"rstrip": true,
|
| 353 |
+
"single_word": false,
|
| 354 |
+
"special": true
|
| 355 |
+
},
|
| 356 |
+
"32041": {
|
| 357 |
+
"content": "<extra_id_58>",
|
| 358 |
+
"lstrip": true,
|
| 359 |
+
"normalized": false,
|
| 360 |
+
"rstrip": true,
|
| 361 |
+
"single_word": false,
|
| 362 |
+
"special": true
|
| 363 |
+
},
|
| 364 |
+
"32042": {
|
| 365 |
+
"content": "<extra_id_57>",
|
| 366 |
+
"lstrip": true,
|
| 367 |
+
"normalized": false,
|
| 368 |
+
"rstrip": true,
|
| 369 |
+
"single_word": false,
|
| 370 |
+
"special": true
|
| 371 |
+
},
|
| 372 |
+
"32043": {
|
| 373 |
+
"content": "<extra_id_56>",
|
| 374 |
+
"lstrip": true,
|
| 375 |
+
"normalized": false,
|
| 376 |
+
"rstrip": true,
|
| 377 |
+
"single_word": false,
|
| 378 |
+
"special": true
|
| 379 |
+
},
|
| 380 |
+
"32044": {
|
| 381 |
+
"content": "<extra_id_55>",
|
| 382 |
+
"lstrip": true,
|
| 383 |
+
"normalized": false,
|
| 384 |
+
"rstrip": true,
|
| 385 |
+
"single_word": false,
|
| 386 |
+
"special": true
|
| 387 |
+
},
|
| 388 |
+
"32045": {
|
| 389 |
+
"content": "<extra_id_54>",
|
| 390 |
+
"lstrip": true,
|
| 391 |
+
"normalized": false,
|
| 392 |
+
"rstrip": true,
|
| 393 |
+
"single_word": false,
|
| 394 |
+
"special": true
|
| 395 |
+
},
|
| 396 |
+
"32046": {
|
| 397 |
+
"content": "<extra_id_53>",
|
| 398 |
+
"lstrip": true,
|
| 399 |
+
"normalized": false,
|
| 400 |
+
"rstrip": true,
|
| 401 |
+
"single_word": false,
|
| 402 |
+
"special": true
|
| 403 |
+
},
|
| 404 |
+
"32047": {
|
| 405 |
+
"content": "<extra_id_52>",
|
| 406 |
+
"lstrip": true,
|
| 407 |
+
"normalized": false,
|
| 408 |
+
"rstrip": true,
|
| 409 |
+
"single_word": false,
|
| 410 |
+
"special": true
|
| 411 |
+
},
|
| 412 |
+
"32048": {
|
| 413 |
+
"content": "<extra_id_51>",
|
| 414 |
+
"lstrip": true,
|
| 415 |
+
"normalized": false,
|
| 416 |
+
"rstrip": true,
|
| 417 |
+
"single_word": false,
|
| 418 |
+
"special": true
|
| 419 |
+
},
|
| 420 |
+
"32049": {
|
| 421 |
+
"content": "<extra_id_50>",
|
| 422 |
+
"lstrip": true,
|
| 423 |
+
"normalized": false,
|
| 424 |
+
"rstrip": true,
|
| 425 |
+
"single_word": false,
|
| 426 |
+
"special": true
|
| 427 |
+
},
|
| 428 |
+
"32050": {
|
| 429 |
+
"content": "<extra_id_49>",
|
| 430 |
+
"lstrip": true,
|
| 431 |
+
"normalized": false,
|
| 432 |
+
"rstrip": true,
|
| 433 |
+
"single_word": false,
|
| 434 |
+
"special": true
|
| 435 |
+
},
|
| 436 |
+
"32051": {
|
| 437 |
+
"content": "<extra_id_48>",
|
| 438 |
+
"lstrip": true,
|
| 439 |
+
"normalized": false,
|
| 440 |
+
"rstrip": true,
|
| 441 |
+
"single_word": false,
|
| 442 |
+
"special": true
|
| 443 |
+
},
|
| 444 |
+
"32052": {
|
| 445 |
+
"content": "<extra_id_47>",
|
| 446 |
+
"lstrip": true,
|
| 447 |
+
"normalized": false,
|
| 448 |
+
"rstrip": true,
|
| 449 |
+
"single_word": false,
|
| 450 |
+
"special": true
|
| 451 |
+
},
|
| 452 |
+
"32053": {
|
| 453 |
+
"content": "<extra_id_46>",
|
| 454 |
+
"lstrip": true,
|
| 455 |
+
"normalized": false,
|
| 456 |
+
"rstrip": true,
|
| 457 |
+
"single_word": false,
|
| 458 |
+
"special": true
|
| 459 |
+
},
|
| 460 |
+
"32054": {
|
| 461 |
+
"content": "<extra_id_45>",
|
| 462 |
+
"lstrip": true,
|
| 463 |
+
"normalized": false,
|
| 464 |
+
"rstrip": true,
|
| 465 |
+
"single_word": false,
|
| 466 |
+
"special": true
|
| 467 |
+
},
|
| 468 |
+
"32055": {
|
| 469 |
+
"content": "<extra_id_44>",
|
| 470 |
+
"lstrip": true,
|
| 471 |
+
"normalized": false,
|
| 472 |
+
"rstrip": true,
|
| 473 |
+
"single_word": false,
|
| 474 |
+
"special": true
|
| 475 |
+
},
|
| 476 |
+
"32056": {
|
| 477 |
+
"content": "<extra_id_43>",
|
| 478 |
+
"lstrip": true,
|
| 479 |
+
"normalized": false,
|
| 480 |
+
"rstrip": true,
|
| 481 |
+
"single_word": false,
|
| 482 |
+
"special": true
|
| 483 |
+
},
|
| 484 |
+
"32057": {
|
| 485 |
+
"content": "<extra_id_42>",
|
| 486 |
+
"lstrip": true,
|
| 487 |
+
"normalized": false,
|
| 488 |
+
"rstrip": true,
|
| 489 |
+
"single_word": false,
|
| 490 |
+
"special": true
|
| 491 |
+
},
|
| 492 |
+
"32058": {
|
| 493 |
+
"content": "<extra_id_41>",
|
| 494 |
+
"lstrip": true,
|
| 495 |
+
"normalized": false,
|
| 496 |
+
"rstrip": true,
|
| 497 |
+
"single_word": false,
|
| 498 |
+
"special": true
|
| 499 |
+
},
|
| 500 |
+
"32059": {
|
| 501 |
+
"content": "<extra_id_40>",
|
| 502 |
+
"lstrip": true,
|
| 503 |
+
"normalized": false,
|
| 504 |
+
"rstrip": true,
|
| 505 |
+
"single_word": false,
|
| 506 |
+
"special": true
|
| 507 |
+
},
|
| 508 |
+
"32060": {
|
| 509 |
+
"content": "<extra_id_39>",
|
| 510 |
+
"lstrip": true,
|
| 511 |
+
"normalized": false,
|
| 512 |
+
"rstrip": true,
|
| 513 |
+
"single_word": false,
|
| 514 |
+
"special": true
|
| 515 |
+
},
|
| 516 |
+
"32061": {
|
| 517 |
+
"content": "<extra_id_38>",
|
| 518 |
+
"lstrip": true,
|
| 519 |
+
"normalized": false,
|
| 520 |
+
"rstrip": true,
|
| 521 |
+
"single_word": false,
|
| 522 |
+
"special": true
|
| 523 |
+
},
|
| 524 |
+
"32062": {
|
| 525 |
+
"content": "<extra_id_37>",
|
| 526 |
+
"lstrip": true,
|
| 527 |
+
"normalized": false,
|
| 528 |
+
"rstrip": true,
|
| 529 |
+
"single_word": false,
|
| 530 |
+
"special": true
|
| 531 |
+
},
|
| 532 |
+
"32063": {
|
| 533 |
+
"content": "<extra_id_36>",
|
| 534 |
+
"lstrip": true,
|
| 535 |
+
"normalized": false,
|
| 536 |
+
"rstrip": true,
|
| 537 |
+
"single_word": false,
|
| 538 |
+
"special": true
|
| 539 |
+
},
|
| 540 |
+
"32064": {
|
| 541 |
+
"content": "<extra_id_35>",
|
| 542 |
+
"lstrip": true,
|
| 543 |
+
"normalized": false,
|
| 544 |
+
"rstrip": true,
|
| 545 |
+
"single_word": false,
|
| 546 |
+
"special": true
|
| 547 |
+
},
|
| 548 |
+
"32065": {
|
| 549 |
+
"content": "<extra_id_34>",
|
| 550 |
+
"lstrip": true,
|
| 551 |
+
"normalized": false,
|
| 552 |
+
"rstrip": true,
|
| 553 |
+
"single_word": false,
|
| 554 |
+
"special": true
|
| 555 |
+
},
|
| 556 |
+
"32066": {
|
| 557 |
+
"content": "<extra_id_33>",
|
| 558 |
+
"lstrip": true,
|
| 559 |
+
"normalized": false,
|
| 560 |
+
"rstrip": true,
|
| 561 |
+
"single_word": false,
|
| 562 |
+
"special": true
|
| 563 |
+
},
|
| 564 |
+
"32067": {
|
| 565 |
+
"content": "<extra_id_32>",
|
| 566 |
+
"lstrip": true,
|
| 567 |
+
"normalized": false,
|
| 568 |
+
"rstrip": true,
|
| 569 |
+
"single_word": false,
|
| 570 |
+
"special": true
|
| 571 |
+
},
|
| 572 |
+
"32068": {
|
| 573 |
+
"content": "<extra_id_31>",
|
| 574 |
+
"lstrip": true,
|
| 575 |
+
"normalized": false,
|
| 576 |
+
"rstrip": true,
|
| 577 |
+
"single_word": false,
|
| 578 |
+
"special": true
|
| 579 |
+
},
|
| 580 |
+
"32069": {
|
| 581 |
+
"content": "<extra_id_30>",
|
| 582 |
+
"lstrip": true,
|
| 583 |
+
"normalized": false,
|
| 584 |
+
"rstrip": true,
|
| 585 |
+
"single_word": false,
|
| 586 |
+
"special": true
|
| 587 |
+
},
|
| 588 |
+
"32070": {
|
| 589 |
+
"content": "<extra_id_29>",
|
| 590 |
+
"lstrip": true,
|
| 591 |
+
"normalized": false,
|
| 592 |
+
"rstrip": true,
|
| 593 |
+
"single_word": false,
|
| 594 |
+
"special": true
|
| 595 |
+
},
|
| 596 |
+
"32071": {
|
| 597 |
+
"content": "<extra_id_28>",
|
| 598 |
+
"lstrip": true,
|
| 599 |
+
"normalized": false,
|
| 600 |
+
"rstrip": true,
|
| 601 |
+
"single_word": false,
|
| 602 |
+
"special": true
|
| 603 |
+
},
|
| 604 |
+
"32072": {
|
| 605 |
+
"content": "<extra_id_27>",
|
| 606 |
+
"lstrip": true,
|
| 607 |
+
"normalized": false,
|
| 608 |
+
"rstrip": true,
|
| 609 |
+
"single_word": false,
|
| 610 |
+
"special": true
|
| 611 |
+
},
|
| 612 |
+
"32073": {
|
| 613 |
+
"content": "<extra_id_26>",
|
| 614 |
+
"lstrip": true,
|
| 615 |
+
"normalized": false,
|
| 616 |
+
"rstrip": true,
|
| 617 |
+
"single_word": false,
|
| 618 |
+
"special": true
|
| 619 |
+
},
|
| 620 |
+
"32074": {
|
| 621 |
+
"content": "<extra_id_25>",
|
| 622 |
+
"lstrip": true,
|
| 623 |
+
"normalized": false,
|
| 624 |
+
"rstrip": true,
|
| 625 |
+
"single_word": false,
|
| 626 |
+
"special": true
|
| 627 |
+
},
|
| 628 |
+
"32075": {
|
| 629 |
+
"content": "<extra_id_24>",
|
| 630 |
+
"lstrip": true,
|
| 631 |
+
"normalized": false,
|
| 632 |
+
"rstrip": true,
|
| 633 |
+
"single_word": false,
|
| 634 |
+
"special": true
|
| 635 |
+
},
|
| 636 |
+
"32076": {
|
| 637 |
+
"content": "<extra_id_23>",
|
| 638 |
+
"lstrip": true,
|
| 639 |
+
"normalized": false,
|
| 640 |
+
"rstrip": true,
|
| 641 |
+
"single_word": false,
|
| 642 |
+
"special": true
|
| 643 |
+
},
|
| 644 |
+
"32077": {
|
| 645 |
+
"content": "<extra_id_22>",
|
| 646 |
+
"lstrip": true,
|
| 647 |
+
"normalized": false,
|
| 648 |
+
"rstrip": true,
|
| 649 |
+
"single_word": false,
|
| 650 |
+
"special": true
|
| 651 |
+
},
|
| 652 |
+
"32078": {
|
| 653 |
+
"content": "<extra_id_21>",
|
| 654 |
+
"lstrip": true,
|
| 655 |
+
"normalized": false,
|
| 656 |
+
"rstrip": true,
|
| 657 |
+
"single_word": false,
|
| 658 |
+
"special": true
|
| 659 |
+
},
|
| 660 |
+
"32079": {
|
| 661 |
+
"content": "<extra_id_20>",
|
| 662 |
+
"lstrip": true,
|
| 663 |
+
"normalized": false,
|
| 664 |
+
"rstrip": true,
|
| 665 |
+
"single_word": false,
|
| 666 |
+
"special": true
|
| 667 |
+
},
|
| 668 |
+
"32080": {
|
| 669 |
+
"content": "<extra_id_19>",
|
| 670 |
+
"lstrip": true,
|
| 671 |
+
"normalized": false,
|
| 672 |
+
"rstrip": true,
|
| 673 |
+
"single_word": false,
|
| 674 |
+
"special": true
|
| 675 |
+
},
|
| 676 |
+
"32081": {
|
| 677 |
+
"content": "<extra_id_18>",
|
| 678 |
+
"lstrip": true,
|
| 679 |
+
"normalized": false,
|
| 680 |
+
"rstrip": true,
|
| 681 |
+
"single_word": false,
|
| 682 |
+
"special": true
|
| 683 |
+
},
|
| 684 |
+
"32082": {
|
| 685 |
+
"content": "<extra_id_17>",
|
| 686 |
+
"lstrip": true,
|
| 687 |
+
"normalized": false,
|
| 688 |
+
"rstrip": true,
|
| 689 |
+
"single_word": false,
|
| 690 |
+
"special": true
|
| 691 |
+
},
|
| 692 |
+
"32083": {
|
| 693 |
+
"content": "<extra_id_16>",
|
| 694 |
+
"lstrip": true,
|
| 695 |
+
"normalized": false,
|
| 696 |
+
"rstrip": true,
|
| 697 |
+
"single_word": false,
|
| 698 |
+
"special": true
|
| 699 |
+
},
|
| 700 |
+
"32084": {
|
| 701 |
+
"content": "<extra_id_15>",
|
| 702 |
+
"lstrip": true,
|
| 703 |
+
"normalized": false,
|
| 704 |
+
"rstrip": true,
|
| 705 |
+
"single_word": false,
|
| 706 |
+
"special": true
|
| 707 |
+
},
|
| 708 |
+
"32085": {
|
| 709 |
+
"content": "<extra_id_14>",
|
| 710 |
+
"lstrip": true,
|
| 711 |
+
"normalized": false,
|
| 712 |
+
"rstrip": true,
|
| 713 |
+
"single_word": false,
|
| 714 |
+
"special": true
|
| 715 |
+
},
|
| 716 |
+
"32086": {
|
| 717 |
+
"content": "<extra_id_13>",
|
| 718 |
+
"lstrip": true,
|
| 719 |
+
"normalized": false,
|
| 720 |
+
"rstrip": true,
|
| 721 |
+
"single_word": false,
|
| 722 |
+
"special": true
|
| 723 |
+
},
|
| 724 |
+
"32087": {
|
| 725 |
+
"content": "<extra_id_12>",
|
| 726 |
+
"lstrip": true,
|
| 727 |
+
"normalized": false,
|
| 728 |
+
"rstrip": true,
|
| 729 |
+
"single_word": false,
|
| 730 |
+
"special": true
|
| 731 |
+
},
|
| 732 |
+
"32088": {
|
| 733 |
+
"content": "<extra_id_11>",
|
| 734 |
+
"lstrip": true,
|
| 735 |
+
"normalized": false,
|
| 736 |
+
"rstrip": true,
|
| 737 |
+
"single_word": false,
|
| 738 |
+
"special": true
|
| 739 |
+
},
|
| 740 |
+
"32089": {
|
| 741 |
+
"content": "<extra_id_10>",
|
| 742 |
+
"lstrip": true,
|
| 743 |
+
"normalized": false,
|
| 744 |
+
"rstrip": true,
|
| 745 |
+
"single_word": false,
|
| 746 |
+
"special": true
|
| 747 |
+
},
|
| 748 |
+
"32090": {
|
| 749 |
+
"content": "<extra_id_9>",
|
| 750 |
+
"lstrip": true,
|
| 751 |
+
"normalized": false,
|
| 752 |
+
"rstrip": true,
|
| 753 |
+
"single_word": false,
|
| 754 |
+
"special": true
|
| 755 |
+
},
|
| 756 |
+
"32091": {
|
| 757 |
+
"content": "<extra_id_8>",
|
| 758 |
+
"lstrip": true,
|
| 759 |
+
"normalized": false,
|
| 760 |
+
"rstrip": true,
|
| 761 |
+
"single_word": false,
|
| 762 |
+
"special": true
|
| 763 |
+
},
|
| 764 |
+
"32092": {
|
| 765 |
+
"content": "<extra_id_7>",
|
| 766 |
+
"lstrip": true,
|
| 767 |
+
"normalized": false,
|
| 768 |
+
"rstrip": true,
|
| 769 |
+
"single_word": false,
|
| 770 |
+
"special": true
|
| 771 |
+
},
|
| 772 |
+
"32093": {
|
| 773 |
+
"content": "<extra_id_6>",
|
| 774 |
+
"lstrip": true,
|
| 775 |
+
"normalized": false,
|
| 776 |
+
"rstrip": true,
|
| 777 |
+
"single_word": false,
|
| 778 |
+
"special": true
|
| 779 |
+
},
|
| 780 |
+
"32094": {
|
| 781 |
+
"content": "<extra_id_5>",
|
| 782 |
+
"lstrip": true,
|
| 783 |
+
"normalized": false,
|
| 784 |
+
"rstrip": true,
|
| 785 |
+
"single_word": false,
|
| 786 |
+
"special": true
|
| 787 |
+
},
|
| 788 |
+
"32095": {
|
| 789 |
+
"content": "<extra_id_4>",
|
| 790 |
+
"lstrip": true,
|
| 791 |
+
"normalized": false,
|
| 792 |
+
"rstrip": true,
|
| 793 |
+
"single_word": false,
|
| 794 |
+
"special": true
|
| 795 |
+
},
|
| 796 |
+
"32096": {
|
| 797 |
+
"content": "<extra_id_3>",
|
| 798 |
+
"lstrip": true,
|
| 799 |
+
"normalized": false,
|
| 800 |
+
"rstrip": true,
|
| 801 |
+
"single_word": false,
|
| 802 |
+
"special": true
|
| 803 |
+
},
|
| 804 |
+
"32097": {
|
| 805 |
+
"content": "<extra_id_2>",
|
| 806 |
+
"lstrip": true,
|
| 807 |
+
"normalized": false,
|
| 808 |
+
"rstrip": true,
|
| 809 |
+
"single_word": false,
|
| 810 |
+
"special": true
|
| 811 |
+
},
|
| 812 |
+
"32098": {
|
| 813 |
+
"content": "<extra_id_1>",
|
| 814 |
+
"lstrip": true,
|
| 815 |
+
"normalized": false,
|
| 816 |
+
"rstrip": true,
|
| 817 |
+
"single_word": false,
|
| 818 |
+
"special": true
|
| 819 |
+
},
|
| 820 |
+
"32099": {
|
| 821 |
+
"content": "<extra_id_0>",
|
| 822 |
+
"lstrip": true,
|
| 823 |
+
"normalized": false,
|
| 824 |
+
"rstrip": true,
|
| 825 |
+
"single_word": false,
|
| 826 |
+
"special": true
|
| 827 |
+
}
|
| 828 |
+
},
|
| 829 |
+
"additional_special_tokens": [
|
| 830 |
+
"<extra_id_0>",
|
| 831 |
+
"<extra_id_1>",
|
| 832 |
+
"<extra_id_2>",
|
| 833 |
+
"<extra_id_3>",
|
| 834 |
+
"<extra_id_4>",
|
| 835 |
+
"<extra_id_5>",
|
| 836 |
+
"<extra_id_6>",
|
| 837 |
+
"<extra_id_7>",
|
| 838 |
+
"<extra_id_8>",
|
| 839 |
+
"<extra_id_9>",
|
| 840 |
+
"<extra_id_10>",
|
| 841 |
+
"<extra_id_11>",
|
| 842 |
+
"<extra_id_12>",
|
| 843 |
+
"<extra_id_13>",
|
| 844 |
+
"<extra_id_14>",
|
| 845 |
+
"<extra_id_15>",
|
| 846 |
+
"<extra_id_16>",
|
| 847 |
+
"<extra_id_17>",
|
| 848 |
+
"<extra_id_18>",
|
| 849 |
+
"<extra_id_19>",
|
| 850 |
+
"<extra_id_20>",
|
| 851 |
+
"<extra_id_21>",
|
| 852 |
+
"<extra_id_22>",
|
| 853 |
+
"<extra_id_23>",
|
| 854 |
+
"<extra_id_24>",
|
| 855 |
+
"<extra_id_25>",
|
| 856 |
+
"<extra_id_26>",
|
| 857 |
+
"<extra_id_27>",
|
| 858 |
+
"<extra_id_28>",
|
| 859 |
+
"<extra_id_29>",
|
| 860 |
+
"<extra_id_30>",
|
| 861 |
+
"<extra_id_31>",
|
| 862 |
+
"<extra_id_32>",
|
| 863 |
+
"<extra_id_33>",
|
| 864 |
+
"<extra_id_34>",
|
| 865 |
+
"<extra_id_35>",
|
| 866 |
+
"<extra_id_36>",
|
| 867 |
+
"<extra_id_37>",
|
| 868 |
+
"<extra_id_38>",
|
| 869 |
+
"<extra_id_39>",
|
| 870 |
+
"<extra_id_40>",
|
| 871 |
+
"<extra_id_41>",
|
| 872 |
+
"<extra_id_42>",
|
| 873 |
+
"<extra_id_43>",
|
| 874 |
+
"<extra_id_44>",
|
| 875 |
+
"<extra_id_45>",
|
| 876 |
+
"<extra_id_46>",
|
| 877 |
+
"<extra_id_47>",
|
| 878 |
+
"<extra_id_48>",
|
| 879 |
+
"<extra_id_49>",
|
| 880 |
+
"<extra_id_50>",
|
| 881 |
+
"<extra_id_51>",
|
| 882 |
+
"<extra_id_52>",
|
| 883 |
+
"<extra_id_53>",
|
| 884 |
+
"<extra_id_54>",
|
| 885 |
+
"<extra_id_55>",
|
| 886 |
+
"<extra_id_56>",
|
| 887 |
+
"<extra_id_57>",
|
| 888 |
+
"<extra_id_58>",
|
| 889 |
+
"<extra_id_59>",
|
| 890 |
+
"<extra_id_60>",
|
| 891 |
+
"<extra_id_61>",
|
| 892 |
+
"<extra_id_62>",
|
| 893 |
+
"<extra_id_63>",
|
| 894 |
+
"<extra_id_64>",
|
| 895 |
+
"<extra_id_65>",
|
| 896 |
+
"<extra_id_66>",
|
| 897 |
+
"<extra_id_67>",
|
| 898 |
+
"<extra_id_68>",
|
| 899 |
+
"<extra_id_69>",
|
| 900 |
+
"<extra_id_70>",
|
| 901 |
+
"<extra_id_71>",
|
| 902 |
+
"<extra_id_72>",
|
| 903 |
+
"<extra_id_73>",
|
| 904 |
+
"<extra_id_74>",
|
| 905 |
+
"<extra_id_75>",
|
| 906 |
+
"<extra_id_76>",
|
| 907 |
+
"<extra_id_77>",
|
| 908 |
+
"<extra_id_78>",
|
| 909 |
+
"<extra_id_79>",
|
| 910 |
+
"<extra_id_80>",
|
| 911 |
+
"<extra_id_81>",
|
| 912 |
+
"<extra_id_82>",
|
| 913 |
+
"<extra_id_83>",
|
| 914 |
+
"<extra_id_84>",
|
| 915 |
+
"<extra_id_85>",
|
| 916 |
+
"<extra_id_86>",
|
| 917 |
+
"<extra_id_87>",
|
| 918 |
+
"<extra_id_88>",
|
| 919 |
+
"<extra_id_89>",
|
| 920 |
+
"<extra_id_90>",
|
| 921 |
+
"<extra_id_91>",
|
| 922 |
+
"<extra_id_92>",
|
| 923 |
+
"<extra_id_93>",
|
| 924 |
+
"<extra_id_94>",
|
| 925 |
+
"<extra_id_95>",
|
| 926 |
+
"<extra_id_96>",
|
| 927 |
+
"<extra_id_97>",
|
| 928 |
+
"<extra_id_98>",
|
| 929 |
+
"<extra_id_99>"
|
| 930 |
+
],
|
| 931 |
+
"clean_up_tokenization_spaces": true,
|
| 932 |
+
"eos_token": "</s>",
|
| 933 |
+
"extra_ids": 100,
|
| 934 |
+
"legacy": true,
|
| 935 |
+
"model_max_length": 512,
|
| 936 |
+
"pad_token": "<pad>",
|
| 937 |
+
"sp_model_kwargs": {},
|
| 938 |
+
"tokenizer_class": "T5Tokenizer",
|
| 939 |
+
"unk_token": "<unk>"
|
| 940 |
+
}
|
code/diffsynth/tokenizer_configs/stable_diffusion_xl/tokenizer_2/merges.txt
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
code/diffsynth/tokenizer_configs/stable_diffusion_xl/tokenizer_2/special_tokens_map.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"bos_token": {
|
| 3 |
+
"content": "<|startoftext|>",
|
| 4 |
+
"lstrip": false,
|
| 5 |
+
"normalized": true,
|
| 6 |
+
"rstrip": false,
|
| 7 |
+
"single_word": false
|
| 8 |
+
},
|
| 9 |
+
"eos_token": {
|
| 10 |
+
"content": "<|endoftext|>",
|
| 11 |
+
"lstrip": false,
|
| 12 |
+
"normalized": true,
|
| 13 |
+
"rstrip": false,
|
| 14 |
+
"single_word": false
|
| 15 |
+
},
|
| 16 |
+
"pad_token": "!",
|
| 17 |
+
"unk_token": {
|
| 18 |
+
"content": "<|endoftext|>",
|
| 19 |
+
"lstrip": false,
|
| 20 |
+
"normalized": true,
|
| 21 |
+
"rstrip": false,
|
| 22 |
+
"single_word": false
|
| 23 |
+
}
|
| 24 |
+
}
|
code/diffsynth/tokenizer_configs/stable_diffusion_xl/tokenizer_2/tokenizer_config.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"add_prefix_space": false,
|
| 3 |
+
"added_tokens_decoder": {
|
| 4 |
+
"0": {
|
| 5 |
+
"content": "!",
|
| 6 |
+
"lstrip": false,
|
| 7 |
+
"normalized": false,
|
| 8 |
+
"rstrip": false,
|
| 9 |
+
"single_word": false,
|
| 10 |
+
"special": true
|
| 11 |
+
},
|
| 12 |
+
"49406": {
|
| 13 |
+
"content": "<|startoftext|>",
|
| 14 |
+
"lstrip": false,
|
| 15 |
+
"normalized": true,
|
| 16 |
+
"rstrip": false,
|
| 17 |
+
"single_word": false,
|
| 18 |
+
"special": true
|
| 19 |
+
},
|
| 20 |
+
"49407": {
|
| 21 |
+
"content": "<|endoftext|>",
|
| 22 |
+
"lstrip": false,
|
| 23 |
+
"normalized": true,
|
| 24 |
+
"rstrip": false,
|
| 25 |
+
"single_word": false,
|
| 26 |
+
"special": true
|
| 27 |
+
}
|
| 28 |
+
},
|
| 29 |
+
"bos_token": "<|startoftext|>",
|
| 30 |
+
"clean_up_tokenization_spaces": true,
|
| 31 |
+
"do_lower_case": true,
|
| 32 |
+
"eos_token": "<|endoftext|>",
|
| 33 |
+
"errors": "replace",
|
| 34 |
+
"model_max_length": 77,
|
| 35 |
+
"pad_token": "!",
|
| 36 |
+
"tokenizer_class": "CLIPTokenizer",
|
| 37 |
+
"unk_token": "<|endoftext|>"
|
| 38 |
+
}
|
code/diffsynth/tokenizer_configs/stable_diffusion_xl/tokenizer_2/vocab.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
code/diffsynth/trainers/__init__.py
ADDED
|
File without changes
|
code/diffsynth/trainers/latent_dataset.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
LatentDataset: Load precomputed ctx and target latents for Context-as-Memory dataset.
|
| 3 |
+
|
| 4 |
+
Use after running precompute_ctx_target_latents.py. Returns samples with:
|
| 5 |
+
- context_latents: (1, C, K, H//8, W//8) - 1 latent per context frame
|
| 6 |
+
- target_latents: (1, C, T, H//8, W//8) - 1 latent per 4 target frames
|
| 7 |
+
- prompt, video_name, start_frame, end_frame, actions
|
| 8 |
+
|
| 9 |
+
Compatible with training that uses precomputed latents instead of encoding on the fly.
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
import json
|
| 13 |
+
import os
|
| 14 |
+
import warnings
|
| 15 |
+
|
| 16 |
+
import torch
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class LatentDataset(torch.utils.data.Dataset):
|
| 20 |
+
"""
|
| 21 |
+
Dataset that loads precomputed ctx and target latents.
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
def __init__(
|
| 25 |
+
self,
|
| 26 |
+
latent_dir,
|
| 27 |
+
metadata_path=None,
|
| 28 |
+
action_base_path=None,
|
| 29 |
+
repeat=1,
|
| 30 |
+
num_frames=81,
|
| 31 |
+
context_frames=5,
|
| 32 |
+
target_frames_per_latent=4,
|
| 33 |
+
):
|
| 34 |
+
"""
|
| 35 |
+
Args:
|
| 36 |
+
latent_dir: Directory containing ctx_latents/ and target_latents/ subdirs.
|
| 37 |
+
metadata_path: Optional. If provided, used to get total_samples and validate.
|
| 38 |
+
action_base_path: Base path for action JSON files (for loading actions if not in .pt).
|
| 39 |
+
repeat: Dataset repeat factor.
|
| 40 |
+
num_frames: Expected num_frames per segment.
|
| 41 |
+
context_frames: Number of context frames (K).
|
| 42 |
+
target_frames_per_latent: Target: 1 latent per N frames.
|
| 43 |
+
"""
|
| 44 |
+
self.latent_dir = latent_dir
|
| 45 |
+
self.ctx_dir = os.path.join(latent_dir, "ctx_latents")
|
| 46 |
+
self.target_dir = os.path.join(latent_dir, "target_latents")
|
| 47 |
+
self.action_base_path = action_base_path or latent_dir
|
| 48 |
+
self.repeat = repeat
|
| 49 |
+
self.num_frames = num_frames
|
| 50 |
+
self.context_frames = context_frames
|
| 51 |
+
self.target_frames_per_latent = target_frames_per_latent
|
| 52 |
+
|
| 53 |
+
# Infer valid indices from existing files (both ctx and target must exist)
|
| 54 |
+
self._indices = []
|
| 55 |
+
if os.path.isdir(self.ctx_dir) and os.path.isdir(self.target_dir):
|
| 56 |
+
ctx_files = {f.replace(".pt", "") for f in os.listdir(self.ctx_dir) if f.endswith(".pt")}
|
| 57 |
+
target_files = {f.replace(".pt", "") for f in os.listdir(self.target_dir) if f.endswith(".pt")}
|
| 58 |
+
common = sorted([int(x) for x in ctx_files & target_files])
|
| 59 |
+
self._indices = common
|
| 60 |
+
if not self._indices:
|
| 61 |
+
meta_path = os.path.join(latent_dir, "metadata_precompute.json")
|
| 62 |
+
if os.path.isfile(meta_path):
|
| 63 |
+
with open(meta_path) as f:
|
| 64 |
+
meta = json.load(f)
|
| 65 |
+
self._total = meta.get("total_samples", 0)
|
| 66 |
+
self._indices = list(range(self._total))
|
| 67 |
+
else:
|
| 68 |
+
self._total = 0
|
| 69 |
+
else:
|
| 70 |
+
self._total = len(self._indices)
|
| 71 |
+
|
| 72 |
+
def __len__(self):
|
| 73 |
+
return self._total * self.repeat
|
| 74 |
+
|
| 75 |
+
def __getitem__(self, idx):
|
| 76 |
+
real_idx = idx % self._total
|
| 77 |
+
if self._indices is not None:
|
| 78 |
+
real_idx = self._indices[real_idx]
|
| 79 |
+
|
| 80 |
+
ctx_path = os.path.join(self.ctx_dir, f"{real_idx:08d}.pt")
|
| 81 |
+
target_path = os.path.join(self.target_dir, f"{real_idx:08d}.pt")
|
| 82 |
+
|
| 83 |
+
if not os.path.isfile(ctx_path) or not os.path.isfile(target_path):
|
| 84 |
+
warnings.warn(f"Latent files not found for idx {real_idx}. Returning None.")
|
| 85 |
+
return None
|
| 86 |
+
|
| 87 |
+
ctx_data = torch.load(ctx_path, map_location="cpu", weights_only=True)
|
| 88 |
+
target_data = torch.load(target_path, map_location="cpu", weights_only=True)
|
| 89 |
+
|
| 90 |
+
ctx_latent = ctx_data["latent"]
|
| 91 |
+
target_latent = target_data["latent"]
|
| 92 |
+
|
| 93 |
+
# Ensure batch dimension: (C, K, H, W) -> (1, C, K, H, W)
|
| 94 |
+
if ctx_latent.dim() == 4:
|
| 95 |
+
ctx_latent = ctx_latent.unsqueeze(0)
|
| 96 |
+
if target_latent.dim() == 4:
|
| 97 |
+
target_latent = target_latent.unsqueeze(0)
|
| 98 |
+
|
| 99 |
+
out = {
|
| 100 |
+
"context_latents": ctx_latent,
|
| 101 |
+
"target_latents": target_latent,
|
| 102 |
+
"prompt": ctx_data.get("prompt", ""),
|
| 103 |
+
"video_name": ctx_data.get("video_name"),
|
| 104 |
+
"start_frame": ctx_data.get("start_frame"),
|
| 105 |
+
"end_frame": ctx_data.get("end_frame"),
|
| 106 |
+
}
|
| 107 |
+
if "actions" in ctx_data and ctx_data["actions"] is not None:
|
| 108 |
+
out["actions"] = ctx_data["actions"]
|
| 109 |
+
elif "actions" in target_data and target_data["actions"] is not None:
|
| 110 |
+
out["actions"] = target_data["actions"]
|
| 111 |
+
|
| 112 |
+
return out
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def get_latent_dataset_args(latent_dir, action_base_path=None, **kwargs):
|
| 116 |
+
"""Build argparse.Namespace for LatentDataset from precompute metadata."""
|
| 117 |
+
meta_path = os.path.join(latent_dir, "metadata_precompute.json")
|
| 118 |
+
if not os.path.isfile(meta_path):
|
| 119 |
+
return None
|
| 120 |
+
with open(meta_path) as f:
|
| 121 |
+
meta = json.load(f)
|
| 122 |
+
from argparse import Namespace
|
| 123 |
+
return Namespace(
|
| 124 |
+
latent_dir=latent_dir,
|
| 125 |
+
action_base_path=action_base_path or meta.get("dataset_base_path", latent_dir),
|
| 126 |
+
num_frames=meta.get("num_frames", 81),
|
| 127 |
+
context_frames=meta.get("context_frames", 5),
|
| 128 |
+
target_frames_per_latent=meta.get("target_frames_per_latent", 4),
|
| 129 |
+
**kwargs,
|
| 130 |
+
)
|
code/diffsynth/trainers/text_to_image.py
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import lightning as pl
|
| 2 |
+
from peft import LoraConfig, inject_adapter_in_model
|
| 3 |
+
import torch, os
|
| 4 |
+
from ..data.simple_text_image import TextImageDataset
|
| 5 |
+
from modelscope.hub.api import HubApi
|
| 6 |
+
from ..models.utils import load_state_dict
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class LightningModelForT2ILoRA(pl.LightningModule):
|
| 11 |
+
def __init__(
|
| 12 |
+
self,
|
| 13 |
+
learning_rate=1e-4,
|
| 14 |
+
use_gradient_checkpointing=True,
|
| 15 |
+
state_dict_converter=None,
|
| 16 |
+
):
|
| 17 |
+
super().__init__()
|
| 18 |
+
# Set parameters
|
| 19 |
+
self.learning_rate = learning_rate
|
| 20 |
+
self.use_gradient_checkpointing = use_gradient_checkpointing
|
| 21 |
+
self.state_dict_converter = state_dict_converter
|
| 22 |
+
self.lora_alpha = None
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def load_models(self):
|
| 26 |
+
# This function is implemented in other modules
|
| 27 |
+
self.pipe = None
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def freeze_parameters(self):
|
| 31 |
+
# Freeze parameters
|
| 32 |
+
self.pipe.requires_grad_(False)
|
| 33 |
+
self.pipe.eval()
|
| 34 |
+
self.pipe.denoising_model().train()
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def add_lora_to_model(self, model, lora_rank=4, lora_alpha=4, lora_target_modules="to_q,to_k,to_v,to_out", init_lora_weights="gaussian", pretrained_lora_path=None, state_dict_converter=None):
|
| 38 |
+
# Add LoRA to UNet
|
| 39 |
+
self.lora_alpha = lora_alpha
|
| 40 |
+
if init_lora_weights == "kaiming":
|
| 41 |
+
init_lora_weights = True
|
| 42 |
+
|
| 43 |
+
lora_config = LoraConfig(
|
| 44 |
+
r=lora_rank,
|
| 45 |
+
lora_alpha=lora_alpha,
|
| 46 |
+
init_lora_weights=init_lora_weights,
|
| 47 |
+
target_modules=lora_target_modules.split(","),
|
| 48 |
+
)
|
| 49 |
+
model = inject_adapter_in_model(lora_config, model)
|
| 50 |
+
for param in model.parameters():
|
| 51 |
+
# Upcast LoRA parameters into fp32
|
| 52 |
+
if param.requires_grad:
|
| 53 |
+
param.data = param.to(torch.float32)
|
| 54 |
+
|
| 55 |
+
# Lora pretrained lora weights
|
| 56 |
+
if pretrained_lora_path is not None:
|
| 57 |
+
state_dict = load_state_dict(pretrained_lora_path)
|
| 58 |
+
if state_dict_converter is not None:
|
| 59 |
+
state_dict = state_dict_converter(state_dict)
|
| 60 |
+
missing_keys, unexpected_keys = model.load_state_dict(state_dict, strict=False)
|
| 61 |
+
all_keys = [i for i, _ in model.named_parameters()]
|
| 62 |
+
num_updated_keys = len(all_keys) - len(missing_keys)
|
| 63 |
+
num_unexpected_keys = len(unexpected_keys)
|
| 64 |
+
print(f"{num_updated_keys} parameters are loaded from {pretrained_lora_path}. {num_unexpected_keys} parameters are unexpected.")
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def training_step(self, batch, batch_idx):
|
| 68 |
+
# Data
|
| 69 |
+
text, image = batch["text"], batch["image"]
|
| 70 |
+
|
| 71 |
+
# Prepare input parameters
|
| 72 |
+
self.pipe.device = self.device
|
| 73 |
+
prompt_emb = self.pipe.encode_prompt(text, positive=True)
|
| 74 |
+
if "latents" in batch:
|
| 75 |
+
latents = batch["latents"].to(dtype=self.pipe.torch_dtype, device=self.device)
|
| 76 |
+
else:
|
| 77 |
+
latents = self.pipe.vae_encoder(image.to(dtype=self.pipe.torch_dtype, device=self.device))
|
| 78 |
+
noise = torch.randn_like(latents)
|
| 79 |
+
timestep_id = torch.randint(0, self.pipe.scheduler.num_train_timesteps, (1,))
|
| 80 |
+
timestep = self.pipe.scheduler.timesteps[timestep_id].to(self.device)
|
| 81 |
+
extra_input = self.pipe.prepare_extra_input(latents)
|
| 82 |
+
noisy_latents = self.pipe.scheduler.add_noise(latents, noise, timestep)
|
| 83 |
+
training_target = self.pipe.scheduler.training_target(latents, noise, timestep)
|
| 84 |
+
|
| 85 |
+
# Compute loss
|
| 86 |
+
noise_pred = self.pipe.denoising_model()(
|
| 87 |
+
noisy_latents, timestep=timestep, **prompt_emb, **extra_input,
|
| 88 |
+
use_gradient_checkpointing=self.use_gradient_checkpointing
|
| 89 |
+
)
|
| 90 |
+
loss = torch.nn.functional.mse_loss(noise_pred.float(), training_target.float())
|
| 91 |
+
loss = loss * self.pipe.scheduler.training_weight(timestep)
|
| 92 |
+
|
| 93 |
+
# Record log
|
| 94 |
+
self.log("train_loss", loss, prog_bar=True)
|
| 95 |
+
return loss
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def configure_optimizers(self):
|
| 99 |
+
trainable_modules = filter(lambda p: p.requires_grad, self.pipe.denoising_model().parameters())
|
| 100 |
+
optimizer = torch.optim.AdamW(trainable_modules, lr=self.learning_rate)
|
| 101 |
+
return optimizer
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def on_save_checkpoint(self, checkpoint):
|
| 105 |
+
checkpoint.clear()
|
| 106 |
+
trainable_param_names = list(filter(lambda named_param: named_param[1].requires_grad, self.pipe.denoising_model().named_parameters()))
|
| 107 |
+
trainable_param_names = set([named_param[0] for named_param in trainable_param_names])
|
| 108 |
+
state_dict = self.pipe.denoising_model().state_dict()
|
| 109 |
+
lora_state_dict = {}
|
| 110 |
+
for name, param in state_dict.items():
|
| 111 |
+
if name in trainable_param_names:
|
| 112 |
+
lora_state_dict[name] = param
|
| 113 |
+
if self.state_dict_converter is not None:
|
| 114 |
+
lora_state_dict = self.state_dict_converter(lora_state_dict, alpha=self.lora_alpha)
|
| 115 |
+
checkpoint.update(lora_state_dict)
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def add_general_parsers(parser):
|
| 120 |
+
parser.add_argument(
|
| 121 |
+
"--dataset_path",
|
| 122 |
+
type=str,
|
| 123 |
+
default=None,
|
| 124 |
+
required=True,
|
| 125 |
+
help="The path of the Dataset.",
|
| 126 |
+
)
|
| 127 |
+
parser.add_argument(
|
| 128 |
+
"--output_path",
|
| 129 |
+
type=str,
|
| 130 |
+
default="./",
|
| 131 |
+
help="Path to save the model.",
|
| 132 |
+
)
|
| 133 |
+
parser.add_argument(
|
| 134 |
+
"--steps_per_epoch",
|
| 135 |
+
type=int,
|
| 136 |
+
default=500,
|
| 137 |
+
help="Number of steps per epoch.",
|
| 138 |
+
)
|
| 139 |
+
parser.add_argument(
|
| 140 |
+
"--height",
|
| 141 |
+
type=int,
|
| 142 |
+
default=1024,
|
| 143 |
+
help="Image height.",
|
| 144 |
+
)
|
| 145 |
+
parser.add_argument(
|
| 146 |
+
"--width",
|
| 147 |
+
type=int,
|
| 148 |
+
default=1024,
|
| 149 |
+
help="Image width.",
|
| 150 |
+
)
|
| 151 |
+
parser.add_argument(
|
| 152 |
+
"--center_crop",
|
| 153 |
+
default=False,
|
| 154 |
+
action="store_true",
|
| 155 |
+
help=(
|
| 156 |
+
"Whether to center crop the input images to the resolution. If not set, the images will be randomly"
|
| 157 |
+
" cropped. The images will be resized to the resolution first before cropping."
|
| 158 |
+
),
|
| 159 |
+
)
|
| 160 |
+
parser.add_argument(
|
| 161 |
+
"--random_flip",
|
| 162 |
+
default=False,
|
| 163 |
+
action="store_true",
|
| 164 |
+
help="Whether to randomly flip images horizontally",
|
| 165 |
+
)
|
| 166 |
+
parser.add_argument(
|
| 167 |
+
"--batch_size",
|
| 168 |
+
type=int,
|
| 169 |
+
default=1,
|
| 170 |
+
help="Batch size (per device) for the training dataloader.",
|
| 171 |
+
)
|
| 172 |
+
parser.add_argument(
|
| 173 |
+
"--dataloader_num_workers",
|
| 174 |
+
type=int,
|
| 175 |
+
default=0,
|
| 176 |
+
help="Number of subprocesses to use for data loading. 0 means that the data will be loaded in the main process.",
|
| 177 |
+
)
|
| 178 |
+
parser.add_argument(
|
| 179 |
+
"--precision",
|
| 180 |
+
type=str,
|
| 181 |
+
default="16-mixed",
|
| 182 |
+
choices=["32", "16", "16-mixed", "bf16"],
|
| 183 |
+
help="Training precision",
|
| 184 |
+
)
|
| 185 |
+
parser.add_argument(
|
| 186 |
+
"--learning_rate",
|
| 187 |
+
type=float,
|
| 188 |
+
default=1e-4,
|
| 189 |
+
help="Learning rate.",
|
| 190 |
+
)
|
| 191 |
+
parser.add_argument(
|
| 192 |
+
"--lora_rank",
|
| 193 |
+
type=int,
|
| 194 |
+
default=4,
|
| 195 |
+
help="The dimension of the LoRA update matrices.",
|
| 196 |
+
)
|
| 197 |
+
parser.add_argument(
|
| 198 |
+
"--lora_alpha",
|
| 199 |
+
type=float,
|
| 200 |
+
default=4.0,
|
| 201 |
+
help="The weight of the LoRA update matrices.",
|
| 202 |
+
)
|
| 203 |
+
parser.add_argument(
|
| 204 |
+
"--init_lora_weights",
|
| 205 |
+
type=str,
|
| 206 |
+
default="kaiming",
|
| 207 |
+
choices=["gaussian", "kaiming"],
|
| 208 |
+
help="The initializing method of LoRA weight.",
|
| 209 |
+
)
|
| 210 |
+
parser.add_argument(
|
| 211 |
+
"--use_gradient_checkpointing",
|
| 212 |
+
default=False,
|
| 213 |
+
action="store_true",
|
| 214 |
+
help="Whether to use gradient checkpointing.",
|
| 215 |
+
)
|
| 216 |
+
parser.add_argument(
|
| 217 |
+
"--accumulate_grad_batches",
|
| 218 |
+
type=int,
|
| 219 |
+
default=1,
|
| 220 |
+
help="The number of batches in gradient accumulation.",
|
| 221 |
+
)
|
| 222 |
+
parser.add_argument(
|
| 223 |
+
"--training_strategy",
|
| 224 |
+
type=str,
|
| 225 |
+
default="auto",
|
| 226 |
+
choices=["auto", "deepspeed_stage_1", "deepspeed_stage_2", "deepspeed_stage_3"],
|
| 227 |
+
help="Training strategy",
|
| 228 |
+
)
|
| 229 |
+
parser.add_argument(
|
| 230 |
+
"--max_epochs",
|
| 231 |
+
type=int,
|
| 232 |
+
default=1,
|
| 233 |
+
help="Number of epochs.",
|
| 234 |
+
)
|
| 235 |
+
parser.add_argument(
|
| 236 |
+
"--modelscope_model_id",
|
| 237 |
+
type=str,
|
| 238 |
+
default=None,
|
| 239 |
+
help="Model ID on ModelScope (https://www.modelscope.cn/). The model will be uploaded to ModelScope automatically if you provide a Model ID.",
|
| 240 |
+
)
|
| 241 |
+
parser.add_argument(
|
| 242 |
+
"--modelscope_access_token",
|
| 243 |
+
type=str,
|
| 244 |
+
default=None,
|
| 245 |
+
help="Access key on ModelScope (https://www.modelscope.cn/). Required if you want to upload the model to ModelScope.",
|
| 246 |
+
)
|
| 247 |
+
parser.add_argument(
|
| 248 |
+
"--pretrained_lora_path",
|
| 249 |
+
type=str,
|
| 250 |
+
default=None,
|
| 251 |
+
help="Pretrained LoRA path. Required if the training is resumed.",
|
| 252 |
+
)
|
| 253 |
+
parser.add_argument(
|
| 254 |
+
"--use_swanlab",
|
| 255 |
+
default=False,
|
| 256 |
+
action="store_true",
|
| 257 |
+
help="Whether to use SwanLab logger.",
|
| 258 |
+
)
|
| 259 |
+
parser.add_argument(
|
| 260 |
+
"--swanlab_mode",
|
| 261 |
+
default=None,
|
| 262 |
+
help="SwanLab mode (cloud or local).",
|
| 263 |
+
)
|
| 264 |
+
return parser
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
def launch_training_task(model, args):
|
| 268 |
+
# dataset and data loader
|
| 269 |
+
dataset = TextImageDataset(
|
| 270 |
+
args.dataset_path,
|
| 271 |
+
steps_per_epoch=args.steps_per_epoch * args.batch_size,
|
| 272 |
+
height=args.height,
|
| 273 |
+
width=args.width,
|
| 274 |
+
center_crop=args.center_crop,
|
| 275 |
+
random_flip=args.random_flip
|
| 276 |
+
)
|
| 277 |
+
train_loader = torch.utils.data.DataLoader(
|
| 278 |
+
dataset,
|
| 279 |
+
shuffle=True,
|
| 280 |
+
batch_size=args.batch_size,
|
| 281 |
+
num_workers=args.dataloader_num_workers
|
| 282 |
+
)
|
| 283 |
+
# train
|
| 284 |
+
if args.use_swanlab:
|
| 285 |
+
from swanlab.integration.pytorch_lightning import SwanLabLogger
|
| 286 |
+
swanlab_config = {"UPPERFRAMEWORK": "DiffSynth-Studio"}
|
| 287 |
+
swanlab_config.update(vars(args))
|
| 288 |
+
swanlab_logger = SwanLabLogger(
|
| 289 |
+
project="diffsynth_studio",
|
| 290 |
+
name="diffsynth_studio",
|
| 291 |
+
config=swanlab_config,
|
| 292 |
+
mode=args.swanlab_mode,
|
| 293 |
+
logdir=os.path.join(args.output_path, "swanlog"),
|
| 294 |
+
)
|
| 295 |
+
logger = [swanlab_logger]
|
| 296 |
+
else:
|
| 297 |
+
logger = None
|
| 298 |
+
trainer = pl.Trainer(
|
| 299 |
+
max_epochs=args.max_epochs,
|
| 300 |
+
accelerator="gpu",
|
| 301 |
+
devices="auto",
|
| 302 |
+
precision=args.precision,
|
| 303 |
+
strategy=args.training_strategy,
|
| 304 |
+
default_root_dir=args.output_path,
|
| 305 |
+
accumulate_grad_batches=args.accumulate_grad_batches,
|
| 306 |
+
callbacks=[pl.pytorch.callbacks.ModelCheckpoint(save_top_k=-1)],
|
| 307 |
+
logger=logger,
|
| 308 |
+
)
|
| 309 |
+
trainer.fit(model=model, train_dataloaders=train_loader)
|
| 310 |
+
|
| 311 |
+
# Upload models
|
| 312 |
+
if args.modelscope_model_id is not None and args.modelscope_access_token is not None:
|
| 313 |
+
print(f"Uploading models to modelscope. model_id: {args.modelscope_model_id} local_path: {trainer.log_dir}")
|
| 314 |
+
with open(os.path.join(trainer.log_dir, "configuration.json"), "w", encoding="utf-8") as f:
|
| 315 |
+
f.write('{"framework":"Pytorch","task":"text-to-image-synthesis"}\n')
|
| 316 |
+
api = HubApi()
|
| 317 |
+
api.login(args.modelscope_access_token)
|
| 318 |
+
api.push_model(model_id=args.modelscope_model_id, model_dir=trainer.log_dir)
|
code/diffsynth/trainers/utils.py
ADDED
|
@@ -0,0 +1,1368 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import imageio, os, torch, warnings, torchvision, argparse, json, random
|
| 2 |
+
from peft import LoraConfig, inject_adapter_in_model
|
| 3 |
+
from PIL import Image
|
| 4 |
+
import pandas as pd
|
| 5 |
+
from tqdm import tqdm
|
| 6 |
+
from accelerate import Accelerator
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class ImageDataset(torch.utils.data.Dataset):
|
| 11 |
+
def __init__(
|
| 12 |
+
self,
|
| 13 |
+
base_path=None, metadata_path=None,
|
| 14 |
+
max_pixels=1920*1080, height=None, width=None,
|
| 15 |
+
height_division_factor=16, width_division_factor=16,
|
| 16 |
+
data_file_keys=("image",),
|
| 17 |
+
image_file_extension=("jpg", "jpeg", "png", "webp"),
|
| 18 |
+
repeat=1,
|
| 19 |
+
args=None,
|
| 20 |
+
):
|
| 21 |
+
if args is not None:
|
| 22 |
+
base_path = args.dataset_base_path
|
| 23 |
+
metadata_path = args.dataset_metadata_path
|
| 24 |
+
height = args.height
|
| 25 |
+
width = args.width
|
| 26 |
+
max_pixels = args.max_pixels
|
| 27 |
+
data_file_keys = args.data_file_keys.split(",")
|
| 28 |
+
repeat = args.dataset_repeat
|
| 29 |
+
|
| 30 |
+
self.base_path = base_path
|
| 31 |
+
self.max_pixels = max_pixels
|
| 32 |
+
self.height = height
|
| 33 |
+
self.width = width
|
| 34 |
+
self.height_division_factor = height_division_factor
|
| 35 |
+
self.width_division_factor = width_division_factor
|
| 36 |
+
self.data_file_keys = data_file_keys
|
| 37 |
+
self.image_file_extension = image_file_extension
|
| 38 |
+
self.repeat = repeat
|
| 39 |
+
|
| 40 |
+
if height is not None and width is not None:
|
| 41 |
+
print("Height and width are fixed. Setting `dynamic_resolution` to False.")
|
| 42 |
+
self.dynamic_resolution = False
|
| 43 |
+
elif height is None and width is None:
|
| 44 |
+
print("Height and width are none. Setting `dynamic_resolution` to True.")
|
| 45 |
+
self.dynamic_resolution = True
|
| 46 |
+
|
| 47 |
+
if metadata_path is None:
|
| 48 |
+
print("No metadata. Trying to generate it.")
|
| 49 |
+
metadata = self.generate_metadata(base_path)
|
| 50 |
+
print(f"{len(metadata)} lines in metadata.")
|
| 51 |
+
self.data = [metadata.iloc[i].to_dict() for i in range(len(metadata))]
|
| 52 |
+
elif metadata_path.endswith(".json"):
|
| 53 |
+
with open(metadata_path, "r") as f:
|
| 54 |
+
metadata = json.load(f)
|
| 55 |
+
self.data = metadata
|
| 56 |
+
else:
|
| 57 |
+
metadata = pd.read_csv(metadata_path)
|
| 58 |
+
# Ensure prompt column is string type to avoid float conversion for NaN values
|
| 59 |
+
if 'prompt' in metadata.columns:
|
| 60 |
+
metadata['prompt'] = metadata['prompt'].astype(str)
|
| 61 |
+
# Replace 'nan' string (from NaN) with empty string
|
| 62 |
+
metadata['prompt'] = metadata['prompt'].replace('nan', '')
|
| 63 |
+
self.data = [metadata.iloc[i].to_dict() for i in range(len(metadata))]
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def generate_metadata(self, folder):
|
| 67 |
+
image_list, prompt_list = [], []
|
| 68 |
+
file_set = set(os.listdir(folder))
|
| 69 |
+
for file_name in file_set:
|
| 70 |
+
if "." not in file_name:
|
| 71 |
+
continue
|
| 72 |
+
file_ext_name = file_name.split(".")[-1].lower()
|
| 73 |
+
file_base_name = file_name[:-len(file_ext_name)-1]
|
| 74 |
+
if file_ext_name not in self.image_file_extension:
|
| 75 |
+
continue
|
| 76 |
+
prompt_file_name = file_base_name + ".txt"
|
| 77 |
+
if prompt_file_name not in file_set:
|
| 78 |
+
continue
|
| 79 |
+
with open(os.path.join(folder, prompt_file_name), "r", encoding="utf-8") as f:
|
| 80 |
+
prompt = f.read().strip()
|
| 81 |
+
image_list.append(file_name)
|
| 82 |
+
prompt_list.append(prompt)
|
| 83 |
+
metadata = pd.DataFrame()
|
| 84 |
+
metadata["image"] = image_list
|
| 85 |
+
metadata["prompt"] = prompt_list
|
| 86 |
+
return metadata
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def crop_and_resize(self, image, target_height, target_width):
|
| 90 |
+
width, height = image.size
|
| 91 |
+
scale = max(target_width / width, target_height / height)
|
| 92 |
+
image = torchvision.transforms.functional.resize(
|
| 93 |
+
image,
|
| 94 |
+
(round(height*scale), round(width*scale)),
|
| 95 |
+
interpolation=torchvision.transforms.InterpolationMode.BILINEAR
|
| 96 |
+
)
|
| 97 |
+
image = torchvision.transforms.functional.center_crop(image, (target_height, target_width))
|
| 98 |
+
return image
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def get_height_width(self, image):
|
| 102 |
+
if self.dynamic_resolution:
|
| 103 |
+
width, height = image.size
|
| 104 |
+
if width * height > self.max_pixels:
|
| 105 |
+
scale = (width * height / self.max_pixels) ** 0.5
|
| 106 |
+
height, width = int(height / scale), int(width / scale)
|
| 107 |
+
height = height // self.height_division_factor * self.height_division_factor
|
| 108 |
+
width = width // self.width_division_factor * self.width_division_factor
|
| 109 |
+
else:
|
| 110 |
+
height, width = self.height, self.width
|
| 111 |
+
return height, width
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def load_image(self, file_path):
|
| 115 |
+
image = Image.open(file_path).convert("RGB")
|
| 116 |
+
image = self.crop_and_resize(image, *self.get_height_width(image))
|
| 117 |
+
return image
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def load_data(self, file_path):
|
| 121 |
+
return self.load_image(file_path)
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def __getitem__(self, data_id):
|
| 125 |
+
data = self.data[data_id % len(self.data)].copy()
|
| 126 |
+
for key in self.data_file_keys:
|
| 127 |
+
if key in data:
|
| 128 |
+
path = os.path.join(self.base_path, data[key])
|
| 129 |
+
data[key] = self.load_data(path)
|
| 130 |
+
if data[key] is None:
|
| 131 |
+
warnings.warn(f"cannot load file {data[key]}.")
|
| 132 |
+
return None
|
| 133 |
+
return data
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def __len__(self):
|
| 137 |
+
return len(self.data) * self.repeat
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
class VideoDataset(torch.utils.data.Dataset):
|
| 142 |
+
def __init__(
|
| 143 |
+
self,
|
| 144 |
+
base_path=None, metadata_path=None,
|
| 145 |
+
num_frames=81,
|
| 146 |
+
time_division_factor=4, time_division_remainder=1,
|
| 147 |
+
max_pixels=1920*1080, height=None, width=None,
|
| 148 |
+
height_division_factor=16, width_division_factor=16,
|
| 149 |
+
data_file_keys=("video",),
|
| 150 |
+
image_file_extension=("jpg", "jpeg", "png", "webp"),
|
| 151 |
+
video_file_extension=("mp4", "avi", "mov", "wmv", "mkv", "flv", "webm"),
|
| 152 |
+
repeat=1,
|
| 153 |
+
args=None,
|
| 154 |
+
action_base_path=None,
|
| 155 |
+
enable_icl=False,
|
| 156 |
+
icl_num_examples=2,
|
| 157 |
+
icl_context_frames=8,
|
| 158 |
+
):
|
| 159 |
+
if args is not None:
|
| 160 |
+
base_path = args.dataset_base_path
|
| 161 |
+
metadata_path = args.dataset_metadata_path
|
| 162 |
+
height = args.height
|
| 163 |
+
width = args.width
|
| 164 |
+
max_pixels = args.max_pixels
|
| 165 |
+
num_frames = args.num_frames
|
| 166 |
+
data_file_keys = args.data_file_keys.split(",")
|
| 167 |
+
repeat = args.dataset_repeat
|
| 168 |
+
# In-context learning parameters
|
| 169 |
+
if hasattr(args, 'enable_icl'):
|
| 170 |
+
enable_icl = args.enable_icl
|
| 171 |
+
if hasattr(args, 'icl_num_examples'):
|
| 172 |
+
icl_num_examples = args.icl_num_examples
|
| 173 |
+
if hasattr(args, 'icl_context_frames'):
|
| 174 |
+
icl_context_frames = args.icl_context_frames
|
| 175 |
+
|
| 176 |
+
self.base_path = base_path
|
| 177 |
+
self.num_frames = num_frames
|
| 178 |
+
self.time_division_factor = time_division_factor
|
| 179 |
+
self.time_division_remainder = time_division_remainder
|
| 180 |
+
self.max_pixels = max_pixels
|
| 181 |
+
self.height = height
|
| 182 |
+
self.width = width
|
| 183 |
+
self.height_division_factor = height_division_factor
|
| 184 |
+
self.width_division_factor = width_division_factor
|
| 185 |
+
self.data_file_keys = data_file_keys
|
| 186 |
+
self.image_file_extension = image_file_extension
|
| 187 |
+
self.video_file_extension = video_file_extension
|
| 188 |
+
self.repeat = repeat
|
| 189 |
+
|
| 190 |
+
# In-context learning parameters
|
| 191 |
+
self.enable_icl = enable_icl
|
| 192 |
+
self.icl_num_examples = icl_num_examples
|
| 193 |
+
self.icl_context_frames = icl_context_frames
|
| 194 |
+
|
| 195 |
+
if height is not None and width is not None:
|
| 196 |
+
print("Height and width are fixed. Setting `dynamic_resolution` to False.")
|
| 197 |
+
self.dynamic_resolution = False
|
| 198 |
+
elif height is None and width is None:
|
| 199 |
+
print("Height and width are none. Setting `dynamic_resolution` to True.")
|
| 200 |
+
self.dynamic_resolution = True
|
| 201 |
+
|
| 202 |
+
if metadata_path is None:
|
| 203 |
+
print("No metadata. Trying to generate it.")
|
| 204 |
+
metadata = self.generate_metadata(base_path)
|
| 205 |
+
print(f"{len(metadata)} lines in metadata.")
|
| 206 |
+
self.data = [metadata.iloc[i].to_dict() for i in range(len(metadata))]
|
| 207 |
+
elif metadata_path.endswith(".json"):
|
| 208 |
+
with open(metadata_path, "r") as f:
|
| 209 |
+
metadata = json.load(f)
|
| 210 |
+
self.data = metadata
|
| 211 |
+
else:
|
| 212 |
+
metadata = pd.read_csv(metadata_path)
|
| 213 |
+
# Ensure prompt column is string type to avoid float conversion for NaN values
|
| 214 |
+
if 'prompt' in metadata.columns:
|
| 215 |
+
metadata['prompt'] = metadata['prompt'].astype(str)
|
| 216 |
+
# Replace 'nan' string (from NaN) with empty string
|
| 217 |
+
metadata['prompt'] = metadata['prompt'].replace('nan', '')
|
| 218 |
+
|
| 219 |
+
# CRITICAL FIX: Clean prompt - remove video path prefix if present
|
| 220 |
+
# Some CSV prompts start with "video_name.mp4 " prefix, which should be removed
|
| 221 |
+
def clean_prompt(prompt_str):
|
| 222 |
+
if not isinstance(prompt_str, str) or not prompt_str:
|
| 223 |
+
return prompt_str
|
| 224 |
+
# Check if prompt starts with a video path (contains .mp4 or /)
|
| 225 |
+
# Pattern: "VideoName/1234_5678.mp4 " or "VideoName.mp4 "
|
| 226 |
+
import re
|
| 227 |
+
# Match pattern: word/word.mp4 or word.mp4 at the start, followed by space
|
| 228 |
+
pattern = r'^[A-Za-z0-9_]+(/[A-Za-z0-9_]+)?\.mp4\s+'
|
| 229 |
+
cleaned = re.sub(pattern, '', prompt_str)
|
| 230 |
+
# Also handle truncated prompts ending with "..."
|
| 231 |
+
if cleaned.endswith('...'):
|
| 232 |
+
cleaned = cleaned[:-3].rstrip()
|
| 233 |
+
return cleaned.strip()
|
| 234 |
+
|
| 235 |
+
metadata['prompt'] = metadata['prompt'].apply(clean_prompt)
|
| 236 |
+
self.data = [metadata.iloc[i].to_dict() for i in range(len(metadata))]
|
| 237 |
+
|
| 238 |
+
self.action_base_path = action_base_path
|
| 239 |
+
|
| 240 |
+
if self.enable_icl:
|
| 241 |
+
print(f"In-context learning enabled: {icl_num_examples} examples, {icl_context_frames} context frames each")
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
def generate_metadata(self, folder):
|
| 245 |
+
video_list, prompt_list = [], []
|
| 246 |
+
file_set = set(os.listdir(folder))
|
| 247 |
+
for file_name in file_set:
|
| 248 |
+
if "." not in file_name:
|
| 249 |
+
continue
|
| 250 |
+
file_ext_name = file_name.split(".")[-1].lower()
|
| 251 |
+
file_base_name = file_name[:-len(file_ext_name)-1]
|
| 252 |
+
if file_ext_name not in self.image_file_extension and file_ext_name not in self.video_file_extension:
|
| 253 |
+
continue
|
| 254 |
+
prompt_file_name = file_base_name + ".txt"
|
| 255 |
+
if prompt_file_name not in file_set:
|
| 256 |
+
continue
|
| 257 |
+
with open(os.path.join(folder, prompt_file_name), "r", encoding="utf-8") as f:
|
| 258 |
+
prompt = f.read().strip()
|
| 259 |
+
video_list.append(file_name)
|
| 260 |
+
prompt_list.append(prompt)
|
| 261 |
+
metadata = pd.DataFrame()
|
| 262 |
+
metadata["video"] = video_list
|
| 263 |
+
metadata["prompt"] = prompt_list
|
| 264 |
+
return metadata
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
def crop_and_resize(self, image, target_height, target_width):
|
| 268 |
+
width, height = image.size
|
| 269 |
+
scale = max(target_width / width, target_height / height)
|
| 270 |
+
image = torchvision.transforms.functional.resize(
|
| 271 |
+
image,
|
| 272 |
+
(round(height*scale), round(width*scale)),
|
| 273 |
+
interpolation=torchvision.transforms.InterpolationMode.BILINEAR
|
| 274 |
+
)
|
| 275 |
+
image = torchvision.transforms.functional.center_crop(image, (target_height, target_width))
|
| 276 |
+
return image
|
| 277 |
+
|
| 278 |
+
|
| 279 |
+
def get_height_width(self, image):
|
| 280 |
+
if self.dynamic_resolution:
|
| 281 |
+
width, height = image.size
|
| 282 |
+
if width * height > self.max_pixels:
|
| 283 |
+
scale = (width * height / self.max_pixels) ** 0.5
|
| 284 |
+
height, width = int(height / scale), int(width / scale)
|
| 285 |
+
height = height // self.height_division_factor * self.height_division_factor
|
| 286 |
+
width = width // self.width_division_factor * self.width_division_factor
|
| 287 |
+
else:
|
| 288 |
+
height, width = self.height, self.width
|
| 289 |
+
return height, width
|
| 290 |
+
|
| 291 |
+
|
| 292 |
+
def get_num_frames(self, reader):
|
| 293 |
+
num_frames = self.num_frames
|
| 294 |
+
if int(reader.count_frames()) < num_frames:
|
| 295 |
+
num_frames = int(reader.count_frames())
|
| 296 |
+
while num_frames > 1 and num_frames % self.time_division_factor != self.time_division_remainder:
|
| 297 |
+
num_frames -= 1
|
| 298 |
+
return num_frames
|
| 299 |
+
|
| 300 |
+
|
| 301 |
+
def load_video(self, file_path):
|
| 302 |
+
reader = imageio.get_reader(file_path)
|
| 303 |
+
num_frames = self.get_num_frames(reader)
|
| 304 |
+
frames = []
|
| 305 |
+
for frame_id in range(num_frames):
|
| 306 |
+
frame = reader.get_data(frame_id)
|
| 307 |
+
frame = Image.fromarray(frame)
|
| 308 |
+
frame = self.crop_and_resize(frame, *self.get_height_width(frame))
|
| 309 |
+
frames.append(frame)
|
| 310 |
+
reader.close()
|
| 311 |
+
return frames
|
| 312 |
+
|
| 313 |
+
|
| 314 |
+
def load_image(self, file_path):
|
| 315 |
+
image = Image.open(file_path).convert("RGB")
|
| 316 |
+
image = self.crop_and_resize(image, *self.get_height_width(image))
|
| 317 |
+
frames = [image]
|
| 318 |
+
return frames
|
| 319 |
+
|
| 320 |
+
|
| 321 |
+
def is_image(self, file_path):
|
| 322 |
+
file_ext_name = file_path.split(".")[-1]
|
| 323 |
+
return file_ext_name.lower() in self.image_file_extension
|
| 324 |
+
|
| 325 |
+
|
| 326 |
+
def is_video(self, file_path):
|
| 327 |
+
file_ext_name = file_path.split(".")[-1]
|
| 328 |
+
return file_ext_name.lower() in self.video_file_extension
|
| 329 |
+
|
| 330 |
+
|
| 331 |
+
def load_data(self, file_path):
|
| 332 |
+
# Handle multiple frame paths separated by '|' (for frame sequences)
|
| 333 |
+
if '|' in str(file_path):
|
| 334 |
+
# Split the path by '|' to get individual frame paths
|
| 335 |
+
frame_paths = str(file_path).split('|')
|
| 336 |
+
frames = []
|
| 337 |
+
|
| 338 |
+
# Get base_path (dataset root)
|
| 339 |
+
if not hasattr(self, 'base_path') or not self.base_path:
|
| 340 |
+
warnings.warn(f"Cannot determine base directory for frame sequence: {file_path}")
|
| 341 |
+
return None
|
| 342 |
+
|
| 343 |
+
base_dir = self.base_path # This is the dataset root
|
| 344 |
+
|
| 345 |
+
# Check the first path to determine the format
|
| 346 |
+
first_frame = frame_paths[0].strip() if frame_paths else ""
|
| 347 |
+
|
| 348 |
+
# If first frame is already an absolute path (from __getitem__ joining),
|
| 349 |
+
# extract the base directory from it
|
| 350 |
+
if os.path.isabs(first_frame):
|
| 351 |
+
# Extract base directory from first frame path
|
| 352 |
+
# First frame format: /path/to/dataset/frames/video_name/frame.png
|
| 353 |
+
# We need to get /path/to/dataset
|
| 354 |
+
parts = first_frame.split(os.sep)
|
| 355 |
+
# Find 'frames' in the path and get everything before it
|
| 356 |
+
if 'frames' in parts:
|
| 357 |
+
frames_idx = parts.index('frames')
|
| 358 |
+
base_dir = os.sep.join(parts[:frames_idx])
|
| 359 |
+
else:
|
| 360 |
+
# Fallback: use self.base_path
|
| 361 |
+
base_dir = self.base_path
|
| 362 |
+
|
| 363 |
+
for frame_path in frame_paths:
|
| 364 |
+
frame_path = frame_path.strip()
|
| 365 |
+
if not frame_path:
|
| 366 |
+
continue
|
| 367 |
+
|
| 368 |
+
# Construct full path
|
| 369 |
+
if os.path.isabs(frame_path):
|
| 370 |
+
# Already absolute path (from __getitem__)
|
| 371 |
+
full_frame_path = frame_path
|
| 372 |
+
else:
|
| 373 |
+
# Relative path - need to construct full path
|
| 374 |
+
# Remove 'frames/' prefix if present (we'll add it consistently)
|
| 375 |
+
if frame_path.startswith('frames/'):
|
| 376 |
+
frame_path = frame_path[7:] # Remove 'frames/' prefix
|
| 377 |
+
|
| 378 |
+
# Always join with base_dir + 'frames/' since base_dir is dataset root
|
| 379 |
+
full_frame_path = os.path.join(base_dir, 'frames', frame_path)
|
| 380 |
+
|
| 381 |
+
# Load individual frame
|
| 382 |
+
if os.path.exists(full_frame_path):
|
| 383 |
+
if self.is_image(full_frame_path):
|
| 384 |
+
frame_data = self.load_image(full_frame_path)
|
| 385 |
+
if frame_data:
|
| 386 |
+
frames.extend(frame_data)
|
| 387 |
+
else:
|
| 388 |
+
warnings.warn(f"Frame is not an image: {full_frame_path}")
|
| 389 |
+
else:
|
| 390 |
+
warnings.warn(f"Frame not found: {full_frame_path}")
|
| 391 |
+
|
| 392 |
+
if frames:
|
| 393 |
+
return frames
|
| 394 |
+
else:
|
| 395 |
+
warnings.warn(f"No frames loaded from sequence: {file_path}")
|
| 396 |
+
return None
|
| 397 |
+
|
| 398 |
+
# Handle single file (image or video)
|
| 399 |
+
if self.is_image(file_path):
|
| 400 |
+
return self.load_image(file_path)
|
| 401 |
+
elif self.is_video(file_path):
|
| 402 |
+
return self.load_video(file_path)
|
| 403 |
+
else:
|
| 404 |
+
return None
|
| 405 |
+
|
| 406 |
+
|
| 407 |
+
def __getitem__(self, data_id):
|
| 408 |
+
data = self.data[data_id % len(self.data)].copy()
|
| 409 |
+
for key in self.data_file_keys:
|
| 410 |
+
if key in ["video_name", "start_frame", "end_frame"]:
|
| 411 |
+
if "actions" in data:
|
| 412 |
+
continue
|
| 413 |
+
try:
|
| 414 |
+
video_name = data.get("video_name")
|
| 415 |
+
if video_name is None:
|
| 416 |
+
warnings.warn(f"video_name is missing in metadata for data_id {data_id}. Skipping action loading.")
|
| 417 |
+
continue
|
| 418 |
+
|
| 419 |
+
if video_name.endswith(".mp4"):
|
| 420 |
+
video_name = ".".join(video_name.split(".")[:-1])
|
| 421 |
+
if "_" in video_name:
|
| 422 |
+
video_name = "_".join(video_name.split("_")[:4])
|
| 423 |
+
|
| 424 |
+
import json
|
| 425 |
+
json_path = os.path.join(self.action_base_path, video_name + ".json")
|
| 426 |
+
|
| 427 |
+
# Check if action file exists
|
| 428 |
+
if not os.path.exists(json_path):
|
| 429 |
+
warnings.warn(f"Action file does not exist: {json_path}. Skipping action loading for data_id {data_id}.")
|
| 430 |
+
continue
|
| 431 |
+
|
| 432 |
+
start_frame = data.get("start_frame")
|
| 433 |
+
end_frame = data.get("end_frame")
|
| 434 |
+
if start_frame is None or end_frame is None:
|
| 435 |
+
warnings.warn(f"start_frame or end_frame is missing in metadata for data_id {data_id}. Skipping action loading.")
|
| 436 |
+
continue
|
| 437 |
+
|
| 438 |
+
json_data = json.load(open(json_path, "r"))['actions']
|
| 439 |
+
actions = []
|
| 440 |
+
current_yaw = 0.0
|
| 441 |
+
for frame_id in range(start_frame+1, end_frame+1):
|
| 442 |
+
frame_str = str(frame_id)
|
| 443 |
+
if frame_str not in json_data:
|
| 444 |
+
warnings.warn(f"Frame {frame_id} not found in action file {json_path}. Skipping this frame.")
|
| 445 |
+
continue
|
| 446 |
+
|
| 447 |
+
action = json_data[frame_str]
|
| 448 |
+
new_action = [0.0] * (2 + 2 + 3 + 1 + 2)
|
| 449 |
+
if action['ws'] == 1:
|
| 450 |
+
new_action[0] = 1
|
| 451 |
+
elif action['ws'] == 2:
|
| 452 |
+
new_action[1] = 1
|
| 453 |
+
|
| 454 |
+
if action['ad'] == 1:
|
| 455 |
+
new_action[2] = 1
|
| 456 |
+
elif action['ad'] == 2:
|
| 457 |
+
new_action[3] = 1
|
| 458 |
+
|
| 459 |
+
if action['scs'] == 1 and action.get("jump_invalid", 0) == 0:
|
| 460 |
+
new_action[4] = 1
|
| 461 |
+
elif action['scs'] == 2:
|
| 462 |
+
new_action[5] = 1
|
| 463 |
+
elif action['scs'] == 3:
|
| 464 |
+
new_action[6] = 1
|
| 465 |
+
|
| 466 |
+
if action.get('collision', 0) == 1:
|
| 467 |
+
new_action[7] = 1
|
| 468 |
+
new_action[0] = 0
|
| 469 |
+
new_action[1] = 0
|
| 470 |
+
new_action[2] = 0
|
| 471 |
+
new_action[3] = 0
|
| 472 |
+
|
| 473 |
+
pre_pitch = action.get('pre_pitch', 0.0)
|
| 474 |
+
current_pitch = pre_pitch + action.get('pitch_delta', 0.0) * 15.0
|
| 475 |
+
current_yaw += action.get('yaw_delta', 0.0) * 15.0
|
| 476 |
+
new_action[8] = current_pitch
|
| 477 |
+
new_action[9] = current_yaw
|
| 478 |
+
|
| 479 |
+
actions.append(new_action)
|
| 480 |
+
data["actions"] = actions
|
| 481 |
+
except Exception as e:
|
| 482 |
+
warnings.warn(f"Exception while loading actions for data_id {data_id}: {e}. Continuing without actions.")
|
| 483 |
+
# Don't return None, just continue without actions
|
| 484 |
+
continue
|
| 485 |
+
elif key == "video":
|
| 486 |
+
# Check if data[key] exists and is not None
|
| 487 |
+
if key not in data or data[key] is None:
|
| 488 |
+
warnings.warn(f"Video key '{key}' is missing or None in metadata for data_id {data_id}. Skipping this sample.")
|
| 489 |
+
return None
|
| 490 |
+
|
| 491 |
+
# Handle frame sequences (paths with '|' separator)
|
| 492 |
+
video_path_str = str(data[key])
|
| 493 |
+
if '|' in video_path_str:
|
| 494 |
+
# For frame sequences, pass the full path string to load_data
|
| 495 |
+
# load_data will handle splitting and loading individual frames
|
| 496 |
+
path = os.path.join(self.base_path, video_path_str)
|
| 497 |
+
# Don't check path existence here for frame sequences
|
| 498 |
+
# load_data will handle individual frame loading
|
| 499 |
+
else:
|
| 500 |
+
path = os.path.join(self.base_path, data[key])
|
| 501 |
+
# Check if path exists (only for single files)
|
| 502 |
+
if not os.path.exists(path):
|
| 503 |
+
warnings.warn(f"Video file does not exist: {path}. Skipping this sample.")
|
| 504 |
+
return None
|
| 505 |
+
try:
|
| 506 |
+
data[key] = self.load_data(path)
|
| 507 |
+
if data[key] is None:
|
| 508 |
+
warnings.warn(f"Failed to load video file: {path}. load_data returned None.")
|
| 509 |
+
return None
|
| 510 |
+
except Exception as e:
|
| 511 |
+
warnings.warn(f"Exception while loading video file {path}: {e}. Skipping this sample.")
|
| 512 |
+
return None
|
| 513 |
+
|
| 514 |
+
# In-context learning: sample context examples from dataset
|
| 515 |
+
if self.enable_icl and len(self.data) > 1:
|
| 516 |
+
context_frames_list = []
|
| 517 |
+
context_actions_list = []
|
| 518 |
+
|
| 519 |
+
# Sample random examples from dataset (excluding current one)
|
| 520 |
+
current_idx = data_id % len(self.data)
|
| 521 |
+
candidate_indices = [i for i in range(len(self.data)) if i != current_idx]
|
| 522 |
+
if len(candidate_indices) > 0:
|
| 523 |
+
num_samples = min(self.icl_num_examples, len(candidate_indices))
|
| 524 |
+
sampled_indices = random.sample(candidate_indices, num_samples)
|
| 525 |
+
|
| 526 |
+
for sample_idx in sampled_indices:
|
| 527 |
+
sample_data = self.data[sample_idx].copy()
|
| 528 |
+
# Load video for context
|
| 529 |
+
if "video" in self.data_file_keys and "video" in sample_data:
|
| 530 |
+
video_path = os.path.join(self.base_path, sample_data["video"])
|
| 531 |
+
sample_video = self.load_data(video_path)
|
| 532 |
+
if sample_video is not None and len(sample_video) >= self.icl_context_frames:
|
| 533 |
+
# Sample context_frames from the video
|
| 534 |
+
start_idx = random.randint(0, max(0, len(sample_video) - self.icl_context_frames))
|
| 535 |
+
context_frames = sample_video[start_idx:start_idx + self.icl_context_frames]
|
| 536 |
+
context_frames_list.extend(context_frames)
|
| 537 |
+
|
| 538 |
+
# Load corresponding actions if available
|
| 539 |
+
if self.action_base_path is not None and "video_name" in sample_data:
|
| 540 |
+
try:
|
| 541 |
+
sample_video_name = sample_data["video_name"]
|
| 542 |
+
if sample_video_name.endswith(".mp4"):
|
| 543 |
+
sample_video_name = ".".join(sample_video_name.split(".")[:-1])
|
| 544 |
+
if "_" in sample_video_name:
|
| 545 |
+
sample_video_name = "_".join(sample_video_name.split("_")[:4])
|
| 546 |
+
sample_json_path = os.path.join(self.action_base_path, sample_video_name + ".json")
|
| 547 |
+
if os.path.exists(sample_json_path):
|
| 548 |
+
sample_json_data = json.load(open(sample_json_path, "r"))['actions']
|
| 549 |
+
sample_start_frame = sample_data.get("start_frame", 0)
|
| 550 |
+
sample_end_frame = sample_data.get("end_frame", len(sample_video))
|
| 551 |
+
|
| 552 |
+
# Get actions for the context frames
|
| 553 |
+
context_actions = []
|
| 554 |
+
context_yaw = 0.0
|
| 555 |
+
for frame_idx in range(sample_start_frame + start_idx + 1,
|
| 556 |
+
min(sample_start_frame + start_idx + self.icl_context_frames + 1, sample_end_frame + 1)):
|
| 557 |
+
if str(frame_idx) in sample_json_data:
|
| 558 |
+
action = sample_json_data[str(frame_idx)]
|
| 559 |
+
new_action = [0.0] * (2 + 2 + 3 + 1 + 2)
|
| 560 |
+
if action['ws'] == 1:
|
| 561 |
+
new_action[0] = 1
|
| 562 |
+
elif action['ws'] == 2:
|
| 563 |
+
new_action[1] = 1
|
| 564 |
+
if action['ad'] == 1:
|
| 565 |
+
new_action[2] = 1
|
| 566 |
+
elif action['ad'] == 2:
|
| 567 |
+
new_action[3] = 1
|
| 568 |
+
if action['scs'] == 1 and action.get("jump_invalid", 0) == 0:
|
| 569 |
+
new_action[4] = 1
|
| 570 |
+
elif action['scs'] == 2:
|
| 571 |
+
new_action[5] = 1
|
| 572 |
+
elif action['scs'] == 3:
|
| 573 |
+
new_action[6] = 1
|
| 574 |
+
if action.get('collision', 0) == 1:
|
| 575 |
+
new_action[7] = 1
|
| 576 |
+
new_action[0] = 0
|
| 577 |
+
new_action[1] = 0
|
| 578 |
+
new_action[2] = 0
|
| 579 |
+
new_action[3] = 0
|
| 580 |
+
pre_pitch = action.get('pre_pitch', 0.0)
|
| 581 |
+
current_pitch = pre_pitch + action.get('pitch_delta', 0.0) * 15.0
|
| 582 |
+
context_yaw += action.get('yaw_delta', 0.0) * 15.0
|
| 583 |
+
new_action[8] = current_pitch
|
| 584 |
+
new_action[9] = context_yaw
|
| 585 |
+
context_actions.append(new_action)
|
| 586 |
+
context_actions_list.extend(context_actions[:len(context_frames)])
|
| 587 |
+
except Exception as e:
|
| 588 |
+
# If loading actions fails, just skip
|
| 589 |
+
pass
|
| 590 |
+
|
| 591 |
+
if context_frames_list:
|
| 592 |
+
data["context_frames"] = context_frames_list
|
| 593 |
+
if context_actions_list and len(context_actions_list) == len(context_frames_list):
|
| 594 |
+
data["context_actions"] = context_actions_list
|
| 595 |
+
|
| 596 |
+
return data
|
| 597 |
+
|
| 598 |
+
|
| 599 |
+
def __len__(self):
|
| 600 |
+
return len(self.data) * self.repeat
|
| 601 |
+
|
| 602 |
+
@staticmethod
|
| 603 |
+
def get_one_hot(action, range=2):
|
| 604 |
+
one_hot = [0] * (range + 1)
|
| 605 |
+
one_hot[action] = 1
|
| 606 |
+
return one_hot
|
| 607 |
+
|
| 608 |
+
|
| 609 |
+
|
| 610 |
+
import numpy as np
|
| 611 |
+
|
| 612 |
+
|
| 613 |
+
class CamVideoDataset(torch.utils.data.Dataset):
|
| 614 |
+
"""Dataset for Context-as-Memory camera pose conditioned training (ported from VWM).
|
| 615 |
+
|
| 616 |
+
Loads 81 PNG frames from UE scenes with random temporal cropping and extracts
|
| 617 |
+
corresponding camera poses as 12-dim relative RT vectors subsampled to match
|
| 618 |
+
the 21 latent frames.
|
| 619 |
+
"""
|
| 620 |
+
def __init__(
|
| 621 |
+
self,
|
| 622 |
+
base_path=None, metadata_path=None,
|
| 623 |
+
num_frames=81,
|
| 624 |
+
height=None, width=None,
|
| 625 |
+
max_pixels=1920*1080,
|
| 626 |
+
height_division_factor=16, width_division_factor=16,
|
| 627 |
+
repeat=1,
|
| 628 |
+
args=None,
|
| 629 |
+
cam_position_scale=None,
|
| 630 |
+
):
|
| 631 |
+
if args is not None:
|
| 632 |
+
base_path = args.dataset_base_path
|
| 633 |
+
metadata_path = getattr(args, "dataset_metadata_path", metadata_path)
|
| 634 |
+
height = args.height
|
| 635 |
+
width = args.width
|
| 636 |
+
max_pixels = args.max_pixels
|
| 637 |
+
num_frames = args.num_frames
|
| 638 |
+
repeat = args.dataset_repeat
|
| 639 |
+
cam_position_scale = getattr(args, "cam_position_scale", 0.01)
|
| 640 |
+
self.use_condition_context_frames = getattr(args, "use_condition_context_frames", False)
|
| 641 |
+
self.condition_first_frame = getattr(args, "condition_first_frame", False)
|
| 642 |
+
self.condition_history_keyframes = getattr(args, "condition_history_keyframes", False)
|
| 643 |
+
self.condition_use_camera_pose = getattr(args, "condition_use_camera_pose", True)
|
| 644 |
+
self.num_condition_frames = getattr(args, "num_condition_frames", 1)
|
| 645 |
+
self.condition_frame_mode = getattr(args, "condition_frame_mode", "first_frame_only")
|
| 646 |
+
self.overlap_labels_root = getattr(args, "overlap_labels_root", None)
|
| 647 |
+
self.condition_t2v_ratio = getattr(args, "condition_t2v_ratio", 0.10)
|
| 648 |
+
self.condition_i2v_ratio = getattr(args, "condition_i2v_ratio", 0.10)
|
| 649 |
+
self.use_geometry_spatial_memory = getattr(args, "use_geometry_spatial_memory", False)
|
| 650 |
+
self.geometry_memory_column = getattr(args, "geometry_memory_column", "geometry_memory")
|
| 651 |
+
self.geometry_memory_root = getattr(args, "geometry_memory_root", None)
|
| 652 |
+
else:
|
| 653 |
+
self.use_condition_context_frames = False
|
| 654 |
+
self.condition_first_frame = False
|
| 655 |
+
self.condition_history_keyframes = False
|
| 656 |
+
self.condition_use_camera_pose = True
|
| 657 |
+
self.num_condition_frames = 1
|
| 658 |
+
self.condition_frame_mode = "first_frame_only"
|
| 659 |
+
self.overlap_labels_root = None
|
| 660 |
+
self.condition_t2v_ratio = 0.10
|
| 661 |
+
self.condition_i2v_ratio = 0.10
|
| 662 |
+
self.use_geometry_spatial_memory = False
|
| 663 |
+
self.geometry_memory_column = "geometry_memory"
|
| 664 |
+
self.geometry_memory_root = None
|
| 665 |
+
|
| 666 |
+
if cam_position_scale is None:
|
| 667 |
+
cam_position_scale = 0.01
|
| 668 |
+
self.cam_position_scale = float(cam_position_scale)
|
| 669 |
+
|
| 670 |
+
self.base_path = base_path
|
| 671 |
+
self.frames_dir = os.path.join(base_path, "frames")
|
| 672 |
+
self.jsons_dir = os.path.join(base_path, "jsons")
|
| 673 |
+
self.num_frames = num_frames
|
| 674 |
+
self.max_pixels = max_pixels
|
| 675 |
+
self.height = height
|
| 676 |
+
self.width = width
|
| 677 |
+
self.height_division_factor = height_division_factor
|
| 678 |
+
self.width_division_factor = width_division_factor
|
| 679 |
+
self.repeat = repeat
|
| 680 |
+
|
| 681 |
+
if height is not None and width is not None:
|
| 682 |
+
self.dynamic_resolution = False
|
| 683 |
+
else:
|
| 684 |
+
self.dynamic_resolution = True
|
| 685 |
+
|
| 686 |
+
if metadata_path is None:
|
| 687 |
+
print("No metadata. Trying to generate it.")
|
| 688 |
+
metadata = self.generate_metadata(base_path)
|
| 689 |
+
print(f"{len(metadata)} lines in metadata.")
|
| 690 |
+
self.data = [metadata.iloc[i].to_dict() for i in range(len(metadata))]
|
| 691 |
+
elif metadata_path.endswith(".json"):
|
| 692 |
+
with open(metadata_path, "r") as f:
|
| 693 |
+
metadata = json.load(f)
|
| 694 |
+
self.data = metadata
|
| 695 |
+
else:
|
| 696 |
+
metadata = pd.read_csv(metadata_path)
|
| 697 |
+
# Ensure prompt column is string type to avoid float conversion for NaN values
|
| 698 |
+
if 'prompt' in metadata.columns:
|
| 699 |
+
metadata['prompt'] = metadata['prompt'].astype(str)
|
| 700 |
+
# Replace 'nan' string (from NaN) with empty string
|
| 701 |
+
metadata['prompt'] = metadata['prompt'].replace('nan', '')
|
| 702 |
+
|
| 703 |
+
# CRITICAL FIX: Clean prompt - remove video path prefix if present
|
| 704 |
+
# Some CSV prompts start with "video_name.mp4 " prefix, which should be removed
|
| 705 |
+
def clean_prompt(prompt_str):
|
| 706 |
+
if not isinstance(prompt_str, str) or not prompt_str:
|
| 707 |
+
return prompt_str
|
| 708 |
+
# Check if prompt starts with a video path (contains .mp4 or /)
|
| 709 |
+
# Pattern: "VideoName/1234_5678.mp4 " or "VideoName.mp4 "
|
| 710 |
+
import re
|
| 711 |
+
# Match pattern: word/word.mp4 or word.mp4 at the start, followed by space
|
| 712 |
+
pattern = r'^[A-Za-z0-9_]+(/[A-Za-z0-9_]+)?\.mp4\s+'
|
| 713 |
+
cleaned = re.sub(pattern, '', prompt_str)
|
| 714 |
+
# Also handle truncated prompts ending with "..."
|
| 715 |
+
if cleaned.endswith('...'):
|
| 716 |
+
cleaned = cleaned[:-3].rstrip()
|
| 717 |
+
return cleaned.strip()
|
| 718 |
+
|
| 719 |
+
metadata['prompt'] = metadata['prompt'].apply(clean_prompt)
|
| 720 |
+
self.data = [metadata.iloc[i].to_dict() for i in range(len(metadata))]
|
| 721 |
+
|
| 722 |
+
captions_path = os.path.join(base_path, "captions.txt")
|
| 723 |
+
self.scene_captions = {}
|
| 724 |
+
with open(captions_path, "r") as f:
|
| 725 |
+
for line in f:
|
| 726 |
+
parts = line.strip().split("\t", 1)
|
| 727 |
+
if len(parts) < 2:
|
| 728 |
+
continue
|
| 729 |
+
clip_path, caption = parts
|
| 730 |
+
scene_name = "/".join(clip_path.split("/")[:-1])
|
| 731 |
+
fname = clip_path.split("/")[-1].replace(".mp4", "")
|
| 732 |
+
clip_start = int(fname.split("_")[0])
|
| 733 |
+
if scene_name not in self.scene_captions:
|
| 734 |
+
self.scene_captions[scene_name] = []
|
| 735 |
+
self.scene_captions[scene_name].append((clip_start, caption))
|
| 736 |
+
|
| 737 |
+
for scene_name in self.scene_captions:
|
| 738 |
+
self.scene_captions[scene_name].sort(key=lambda x: x[0])
|
| 739 |
+
|
| 740 |
+
self.scene_names = sorted(self.scene_captions.keys())
|
| 741 |
+
self.metadata_rows = []
|
| 742 |
+
if metadata_path and os.path.isfile(metadata_path):
|
| 743 |
+
metadata = pd.read_csv(metadata_path)
|
| 744 |
+
if "prompt" in metadata.columns:
|
| 745 |
+
metadata["prompt"] = metadata["prompt"].astype(str)
|
| 746 |
+
self.metadata_rows = [metadata.iloc[i].to_dict() for i in range(len(metadata))]
|
| 747 |
+
self.pose_cache = {}
|
| 748 |
+
self.overlap_cache = {}
|
| 749 |
+
self.invalid_scenes = set()
|
| 750 |
+
self.invalid_metadata_indices = set()
|
| 751 |
+
self.overlap_labels_root = self._resolve_overlap_labels_root(base_path, self.overlap_labels_root)
|
| 752 |
+
self._validate_condition_config()
|
| 753 |
+
|
| 754 |
+
total_items = len(self.data)
|
| 755 |
+
total_scenes = len(self.scene_names)
|
| 756 |
+
total_captions = sum(len(v) for v in self.scene_captions.values())
|
| 757 |
+
metadata_msg = f", metadata_rows={len(self.metadata_rows)}" if self.metadata_rows else ""
|
| 758 |
+
effective_len = (len(self.metadata_rows) if self.metadata_rows else total_scenes) * repeat
|
| 759 |
+
print(f"CamVideoDataset: {total_scenes} scenes, {total_captions} captions{metadata_msg}, "
|
| 760 |
+
f"repeat={repeat}, cam_position_scale={self.cam_position_scale}, "
|
| 761 |
+
f"effective length={total_items}")
|
| 762 |
+
|
| 763 |
+
def _resolve_overlap_labels_root(self, base_path, overlap_labels_root):
|
| 764 |
+
candidate_roots = []
|
| 765 |
+
if overlap_labels_root is not None:
|
| 766 |
+
candidate_roots.append(overlap_labels_root)
|
| 767 |
+
if base_path is not None:
|
| 768 |
+
candidate_roots.append(os.path.join(base_path, "overlap_labels"))
|
| 769 |
+
for root in candidate_roots:
|
| 770 |
+
if root is not None and os.path.isdir(root):
|
| 771 |
+
return root
|
| 772 |
+
return overlap_labels_root
|
| 773 |
+
|
| 774 |
+
def _validate_condition_config(self):
|
| 775 |
+
if self.condition_t2v_ratio < 0 or self.condition_i2v_ratio < 0:
|
| 776 |
+
raise ValueError("Condition sampling ratios must be non-negative.")
|
| 777 |
+
if self.condition_t2v_ratio + self.condition_i2v_ratio >= 1.0:
|
| 778 |
+
raise ValueError("condition_t2v_ratio + condition_i2v_ratio must be < 1.0.")
|
| 779 |
+
needs_overlap = (
|
| 780 |
+
self.use_condition_context_frames
|
| 781 |
+
and self.condition_frame_mode == "first_plus_overlap"
|
| 782 |
+
and self.condition_history_keyframes
|
| 783 |
+
and self.num_condition_frames > 1
|
| 784 |
+
)
|
| 785 |
+
if needs_overlap and (self.overlap_labels_root is None or not os.path.isdir(self.overlap_labels_root)):
|
| 786 |
+
raise FileNotFoundError(
|
| 787 |
+
"K-frame condition mode requires overlap_labels_root. "
|
| 788 |
+
"Pass --overlap_labels_root or keep overlap_labels under dataset_base_path/overlap_labels."
|
| 789 |
+
)
|
| 790 |
+
|
| 791 |
+
def _load_scene_poses(self, scene_name):
|
| 792 |
+
if scene_name not in self.pose_cache:
|
| 793 |
+
json_path = os.path.join(self.jsons_dir, scene_name + ".json")
|
| 794 |
+
try:
|
| 795 |
+
with open(json_path, "r") as f:
|
| 796 |
+
data = json.load(f)
|
| 797 |
+
except (FileNotFoundError, json.JSONDecodeError) as e:
|
| 798 |
+
raise ValueError(f"Pose JSON for scene '{scene_name}' is missing or corrupt: {e}")
|
| 799 |
+
if not isinstance(data, dict) or "CineCameraActor" not in data:
|
| 800 |
+
raise ValueError(
|
| 801 |
+
f"Pose JSON for scene '{scene_name}' lacks 'CineCameraActor' key "
|
| 802 |
+
f"(found keys: {list(data.keys()) if isinstance(data, dict) else type(data).__name__})."
|
| 803 |
+
)
|
| 804 |
+
cine = data["CineCameraActor"]
|
| 805 |
+
if not isinstance(cine, dict) or len(cine) == 0:
|
| 806 |
+
raise ValueError(f"Pose JSON for scene '{scene_name}' has empty 'CineCameraActor' entries.")
|
| 807 |
+
self.pose_cache[scene_name] = cine
|
| 808 |
+
return self.pose_cache[scene_name]
|
| 809 |
+
|
| 810 |
+
def _find_nearest_caption(self, scene_name, start_frame):
|
| 811 |
+
captions = self.scene_captions[scene_name]
|
| 812 |
+
best_idx = 0
|
| 813 |
+
best_dist = abs(captions[0][0] - start_frame)
|
| 814 |
+
for i, (clip_start, _) in enumerate(captions):
|
| 815 |
+
dist = abs(clip_start - start_frame)
|
| 816 |
+
if dist < best_dist:
|
| 817 |
+
best_dist = dist
|
| 818 |
+
best_idx = i
|
| 819 |
+
return captions[best_idx][1]
|
| 820 |
+
|
| 821 |
+
@staticmethod
|
| 822 |
+
def _compute_rt(position, rotation):
|
| 823 |
+
x, y, z = position
|
| 824 |
+
yaw_rad = np.radians(rotation[2])
|
| 825 |
+
cos_y, sin_y = np.cos(yaw_rad), np.sin(yaw_rad)
|
| 826 |
+
R = np.array([[cos_y, -sin_y, 0], [sin_y, cos_y, 0], [0, 0, 1]])
|
| 827 |
+
return [x, y, z] + R.flatten().tolist()
|
| 828 |
+
|
| 829 |
+
@staticmethod
|
| 830 |
+
def _to_relative_rt(rt_list, ref_rt):
|
| 831 |
+
R_ref = np.array(ref_rt[3:]).reshape(3, 3)
|
| 832 |
+
T_ref = np.array(ref_rt[:3]).reshape(3, 1)
|
| 833 |
+
R_ref_inv = R_ref.T
|
| 834 |
+
T_ref_inv = -R_ref_inv @ T_ref
|
| 835 |
+
result = []
|
| 836 |
+
for rt in rt_list:
|
| 837 |
+
R_i = np.array(rt[3:]).reshape(3, 3)
|
| 838 |
+
T_i = np.array(rt[:3]).reshape(3, 1)
|
| 839 |
+
R_new = R_ref_inv @ R_i
|
| 840 |
+
T_new = R_ref_inv @ T_i + T_ref_inv
|
| 841 |
+
result.append(T_new.flatten().tolist() + R_new.flatten().tolist())
|
| 842 |
+
return result
|
| 843 |
+
|
| 844 |
+
def crop_and_resize(self, image, target_height, target_width):
|
| 845 |
+
width, height = image.size
|
| 846 |
+
scale = max(target_width / width, target_height / height)
|
| 847 |
+
image = torchvision.transforms.functional.resize(
|
| 848 |
+
image,
|
| 849 |
+
(round(height * scale), round(width * scale)),
|
| 850 |
+
interpolation=torchvision.transforms.InterpolationMode.BILINEAR
|
| 851 |
+
)
|
| 852 |
+
image = torchvision.transforms.functional.center_crop(image, (target_height, target_width))
|
| 853 |
+
return image
|
| 854 |
+
|
| 855 |
+
def get_height_width(self, image):
|
| 856 |
+
if self.dynamic_resolution:
|
| 857 |
+
width, height = image.size
|
| 858 |
+
if width * height > self.max_pixels:
|
| 859 |
+
scale = (width * height / self.max_pixels) ** 0.5
|
| 860 |
+
height, width = int(height / scale), int(width / scale)
|
| 861 |
+
height = height // self.height_division_factor * self.height_division_factor
|
| 862 |
+
width = width // self.width_division_factor * self.width_division_factor
|
| 863 |
+
else:
|
| 864 |
+
height, width = self.height, self.width
|
| 865 |
+
return height, width
|
| 866 |
+
|
| 867 |
+
def _load_resized_frame(self, scene_name, frame_index, target_height, target_width):
|
| 868 |
+
frame_path = os.path.join(self.frames_dir, scene_name, f"{frame_index:04d}.png")
|
| 869 |
+
img = Image.open(frame_path).convert("RGB")
|
| 870 |
+
return self.crop_and_resize(img, target_height, target_width)
|
| 871 |
+
|
| 872 |
+
@staticmethod
|
| 873 |
+
def _parse_frame_token(token):
|
| 874 |
+
token = str(token).strip()
|
| 875 |
+
if not token:
|
| 876 |
+
return None, None
|
| 877 |
+
parts = token.split("/")
|
| 878 |
+
if parts and parts[0] == "frames":
|
| 879 |
+
parts = parts[1:]
|
| 880 |
+
if len(parts) < 2:
|
| 881 |
+
return None, None
|
| 882 |
+
scene_name = "/".join(parts[:-1])
|
| 883 |
+
stem = os.path.splitext(parts[-1])[0]
|
| 884 |
+
try:
|
| 885 |
+
frame_index = int(stem)
|
| 886 |
+
except ValueError:
|
| 887 |
+
return None, None
|
| 888 |
+
return scene_name, frame_index
|
| 889 |
+
|
| 890 |
+
def _metadata_scene_and_indices(self, row):
|
| 891 |
+
video_field = str(row.get("video", "") or "")
|
| 892 |
+
tokens = [t for t in video_field.split("|") if t]
|
| 893 |
+
parsed = [self._parse_frame_token(t) for t in tokens]
|
| 894 |
+
parsed = [(s, i) for s, i in parsed if s is not None and i is not None]
|
| 895 |
+
if parsed:
|
| 896 |
+
scene_name = str(row.get("video_name", "") or parsed[0][0])
|
| 897 |
+
frame_indices = [i for _, i in parsed[: self.num_frames]]
|
| 898 |
+
else:
|
| 899 |
+
scene_name = str(row.get("video_name", "") or "").strip()
|
| 900 |
+
if not scene_name:
|
| 901 |
+
raise ValueError("metadata row lacks video_name and parseable video paths")
|
| 902 |
+
start_frame = int(row.get("start_frame", 0) or 0)
|
| 903 |
+
frame_indices = list(range(start_frame, start_frame + self.num_frames))
|
| 904 |
+
if len(frame_indices) < self.num_frames:
|
| 905 |
+
raise ValueError(f"metadata row has {len(frame_indices)} frames, expected {self.num_frames}")
|
| 906 |
+
return scene_name, frame_indices[: self.num_frames]
|
| 907 |
+
|
| 908 |
+
def _resolve_geometry_path(self, token):
|
| 909 |
+
token = str(token).strip()
|
| 910 |
+
if not token:
|
| 911 |
+
return None
|
| 912 |
+
if os.path.isabs(token):
|
| 913 |
+
return token
|
| 914 |
+
root = self.geometry_memory_root or self.base_path
|
| 915 |
+
return os.path.join(root, token)
|
| 916 |
+
|
| 917 |
+
def _load_geometry_memory_frames(self, row):
|
| 918 |
+
value = row.get(self.geometry_memory_column, None)
|
| 919 |
+
if value is None or str(value).strip() == "" or str(value).lower() == "nan":
|
| 920 |
+
if self.use_geometry_spatial_memory:
|
| 921 |
+
raise ValueError(
|
| 922 |
+
f"metadata row lacks required geometry column '{self.geometry_memory_column}'"
|
| 923 |
+
)
|
| 924 |
+
return []
|
| 925 |
+
tokens = [token for token in str(value).split("|") if token.strip()]
|
| 926 |
+
if not tokens:
|
| 927 |
+
raise ValueError(f"empty geometry memory field '{self.geometry_memory_column}'")
|
| 928 |
+
|
| 929 |
+
if len(tokens) == 1:
|
| 930 |
+
path = self._resolve_geometry_path(tokens[0])
|
| 931 |
+
if path is None:
|
| 932 |
+
return []
|
| 933 |
+
if os.path.isdir(path):
|
| 934 |
+
names = sorted(
|
| 935 |
+
name
|
| 936 |
+
for name in os.listdir(path)
|
| 937 |
+
if os.path.splitext(name)[1].lower() in (".png", ".jpg", ".jpeg", ".webp")
|
| 938 |
+
)
|
| 939 |
+
paths = [os.path.join(path, name) for name in names]
|
| 940 |
+
return [Image.open(frame_path).convert("RGB") for frame_path in paths]
|
| 941 |
+
if os.path.splitext(path)[1].lower() in (".mp4", ".mov", ".avi", ".mkv", ".webm"):
|
| 942 |
+
reader = imageio.get_reader(path)
|
| 943 |
+
try:
|
| 944 |
+
frames = [Image.fromarray(frame).convert("RGB") for frame in reader]
|
| 945 |
+
finally:
|
| 946 |
+
reader.close()
|
| 947 |
+
if not frames:
|
| 948 |
+
raise ValueError(f"geometry memory video has no frames: {path}")
|
| 949 |
+
return frames
|
| 950 |
+
|
| 951 |
+
frames = []
|
| 952 |
+
for token in tokens:
|
| 953 |
+
path = self._resolve_geometry_path(token)
|
| 954 |
+
if path is None or not os.path.isfile(path):
|
| 955 |
+
raise FileNotFoundError(f"geometry memory frame not found: {path}")
|
| 956 |
+
frames.append(Image.open(path).convert("RGB"))
|
| 957 |
+
return frames
|
| 958 |
+
|
| 959 |
+
def _load_overlap_frames(self, scene_name, frame_index):
|
| 960 |
+
if self.overlap_labels_root is None:
|
| 961 |
+
return []
|
| 962 |
+
cache_key = (scene_name, int(frame_index))
|
| 963 |
+
if cache_key not in self.overlap_cache:
|
| 964 |
+
overlap_path = os.path.join(self.overlap_labels_root, scene_name, f"{int(frame_index)}.json")
|
| 965 |
+
if not os.path.exists(overlap_path):
|
| 966 |
+
self.overlap_cache[cache_key] = []
|
| 967 |
+
else:
|
| 968 |
+
with open(overlap_path, "r") as f:
|
| 969 |
+
overlap_data = json.load(f)
|
| 970 |
+
overlaps = overlap_data.get("overlapping_frames", [])
|
| 971 |
+
self.overlap_cache[cache_key] = [int(idx) for idx in overlaps]
|
| 972 |
+
return self.overlap_cache[cache_key]
|
| 973 |
+
|
| 974 |
+
def _compute_scene_rt(self, scene_name, frame_index):
|
| 975 |
+
frame_data = self._load_scene_poses(scene_name)[str(int(frame_index))]
|
| 976 |
+
raw_pos = frame_data["position"]
|
| 977 |
+
pos = [float(p) * self.cam_position_scale for p in raw_pos]
|
| 978 |
+
return self._compute_rt(pos, frame_data["rotation"])
|
| 979 |
+
|
| 980 |
+
def _sample_condition_mode(self):
|
| 981 |
+
if not self.use_condition_context_frames:
|
| 982 |
+
return "disabled"
|
| 983 |
+
if (
|
| 984 |
+
self.condition_frame_mode != "first_plus_overlap"
|
| 985 |
+
or not self.condition_history_keyframes
|
| 986 |
+
or self.num_condition_frames <= 1
|
| 987 |
+
):
|
| 988 |
+
return "first_frame_only"
|
| 989 |
+
sample = random.random()
|
| 990 |
+
if sample < self.condition_t2v_ratio:
|
| 991 |
+
return "text_only"
|
| 992 |
+
if sample < self.condition_t2v_ratio + self.condition_i2v_ratio:
|
| 993 |
+
return "first_frame_only"
|
| 994 |
+
return "first_plus_overlap"
|
| 995 |
+
|
| 996 |
+
def _sample_overlap_conditions(self, scene_name, start_frame, ref_rt, target_height, target_width, num_extra_conditions):
|
| 997 |
+
if num_extra_conditions <= 0:
|
| 998 |
+
return [], [], []
|
| 999 |
+
window_indices = set(range(start_frame, start_frame + self.num_frames))
|
| 1000 |
+
target_candidates = list(range(start_frame + 1, start_frame + self.num_frames))
|
| 1001 |
+
sampled_target_frames = random.sample(target_candidates, k=min(num_extra_conditions, len(target_candidates)))
|
| 1002 |
+
overlap_frames = []
|
| 1003 |
+
overlap_indices = []
|
| 1004 |
+
overlap_actions = []
|
| 1005 |
+
used_condition_indices = set()
|
| 1006 |
+
for target_frame_idx in sampled_target_frames:
|
| 1007 |
+
candidate_indices = [
|
| 1008 |
+
idx for idx in self._load_overlap_frames(scene_name, target_frame_idx)
|
| 1009 |
+
if idx not in window_indices and idx != target_frame_idx and idx not in used_condition_indices
|
| 1010 |
+
]
|
| 1011 |
+
if len(candidate_indices) == 0:
|
| 1012 |
+
return None
|
| 1013 |
+
chosen_idx = random.choice(candidate_indices)
|
| 1014 |
+
used_condition_indices.add(chosen_idx)
|
| 1015 |
+
overlap_indices.append(chosen_idx)
|
| 1016 |
+
overlap_frames.append(self._load_resized_frame(scene_name, chosen_idx, target_height, target_width))
|
| 1017 |
+
if self.condition_use_camera_pose:
|
| 1018 |
+
overlap_rt = self._compute_scene_rt(scene_name, chosen_idx)
|
| 1019 |
+
overlap_actions.append(self._to_relative_rt([overlap_rt], ref_rt)[0])
|
| 1020 |
+
if len(overlap_frames) != num_extra_conditions:
|
| 1021 |
+
return None
|
| 1022 |
+
return overlap_frames, overlap_indices, overlap_actions
|
| 1023 |
+
|
| 1024 |
+
def _try_get_sample(self, data_item):
|
| 1025 |
+
scene_name = data_item["video_name"]
|
| 1026 |
+
cam_data = self._load_scene_poses(scene_name)
|
| 1027 |
+
max_start = len(cam_data) - self.num_frames
|
| 1028 |
+
if max_start < 0:
|
| 1029 |
+
raise ValueError(f"Scene {scene_name} has fewer than {self.num_frames} frames.")
|
| 1030 |
+
start_frame = data_item["start_frame"]
|
| 1031 |
+
end_frame = start_frame + self.num_frames - 1
|
| 1032 |
+
assert end_frame == data_item["end_frame"]
|
| 1033 |
+
|
| 1034 |
+
frames = []
|
| 1035 |
+
for i in range(start_frame, end_frame + 1):
|
| 1036 |
+
frame_path = os.path.join(self.frames_dir, scene_name, f"{i:04d}.png")
|
| 1037 |
+
img = Image.open(frame_path).convert("RGB")
|
| 1038 |
+
img = self.crop_and_resize(img, *self.get_height_width(img))
|
| 1039 |
+
frames.append(img)
|
| 1040 |
+
|
| 1041 |
+
# prompt = self._find_nearest_caption(scene_name, start_frame)
|
| 1042 |
+
prompt = data_item["prompt"]
|
| 1043 |
+
|
| 1044 |
+
rt_list_abs = []
|
| 1045 |
+
for i in range(start_frame, end_frame + 1):
|
| 1046 |
+
key = str(i)
|
| 1047 |
+
if key not in cam_data:
|
| 1048 |
+
raise ValueError(f"Scene {scene_name} missing pose for frame {i}.")
|
| 1049 |
+
frame_data = cam_data[key]
|
| 1050 |
+
raw_pos = frame_data["position"]
|
| 1051 |
+
pos = [float(p) * self.cam_position_scale for p in raw_pos]
|
| 1052 |
+
rt = self._compute_rt(pos, frame_data["rotation"])
|
| 1053 |
+
rt_list_abs.append(rt)
|
| 1054 |
+
|
| 1055 |
+
rt_list = self._to_relative_rt(rt_list_abs, rt_list_abs[0])
|
| 1056 |
+
pose_indices = list(range(0, self.num_frames, 4))
|
| 1057 |
+
actions = [rt_list[i] for i in pose_indices]
|
| 1058 |
+
|
| 1059 |
+
return {
|
| 1060 |
+
"video": frames,
|
| 1061 |
+
"prompt": prompt,
|
| 1062 |
+
"actions": actions,
|
| 1063 |
+
"video_name": scene_name,
|
| 1064 |
+
"start_frame": start_frame,
|
| 1065 |
+
"end_frame": end_frame,
|
| 1066 |
+
**self._build_condition_context_payload(
|
| 1067 |
+
frames=frames,
|
| 1068 |
+
scene_name=scene_name,
|
| 1069 |
+
start_frame=start_frame,
|
| 1070 |
+
ref_rt=rt_list_abs[0],
|
| 1071 |
+
actions=actions,
|
| 1072 |
+
),
|
| 1073 |
+
}
|
| 1074 |
+
|
| 1075 |
+
def _try_get_metadata_sample(self, row):
|
| 1076 |
+
scene_name, frame_indices = self._metadata_scene_and_indices(row)
|
| 1077 |
+
start_frame = int(frame_indices[0])
|
| 1078 |
+
end_frame = int(frame_indices[-1])
|
| 1079 |
+
cam_data = self._load_scene_poses(scene_name)
|
| 1080 |
+
|
| 1081 |
+
frames = []
|
| 1082 |
+
for frame_idx in frame_indices:
|
| 1083 |
+
frame_path = os.path.join(self.frames_dir, scene_name, f"{int(frame_idx):04d}.png")
|
| 1084 |
+
img = Image.open(frame_path).convert("RGB")
|
| 1085 |
+
img = self.crop_and_resize(img, *self.get_height_width(img))
|
| 1086 |
+
frames.append(img)
|
| 1087 |
+
|
| 1088 |
+
prompt = row.get("prompt", None)
|
| 1089 |
+
if prompt is None or str(prompt).strip() == "" or str(prompt).lower() == "nan":
|
| 1090 |
+
prompt = self._find_nearest_caption(scene_name, start_frame)
|
| 1091 |
+
else:
|
| 1092 |
+
prompt = str(prompt)
|
| 1093 |
+
|
| 1094 |
+
rt_list_abs = []
|
| 1095 |
+
for frame_idx in frame_indices:
|
| 1096 |
+
key = str(int(frame_idx))
|
| 1097 |
+
if key not in cam_data:
|
| 1098 |
+
raise ValueError(f"Scene {scene_name} missing pose for frame {frame_idx}.")
|
| 1099 |
+
frame_data = cam_data[key]
|
| 1100 |
+
raw_pos = frame_data["position"]
|
| 1101 |
+
pos = [float(p) * self.cam_position_scale for p in raw_pos]
|
| 1102 |
+
rt = self._compute_rt(pos, frame_data["rotation"])
|
| 1103 |
+
rt_list_abs.append(rt)
|
| 1104 |
+
|
| 1105 |
+
rt_list = self._to_relative_rt(rt_list_abs, rt_list_abs[0])
|
| 1106 |
+
pose_indices = list(range(0, len(frame_indices), 4))
|
| 1107 |
+
actions = [rt_list[i] for i in pose_indices]
|
| 1108 |
+
geometry_memory_frames = self._load_geometry_memory_frames(row)
|
| 1109 |
+
|
| 1110 |
+
return {
|
| 1111 |
+
"video": frames,
|
| 1112 |
+
"prompt": prompt,
|
| 1113 |
+
"actions": actions,
|
| 1114 |
+
"video_name": scene_name,
|
| 1115 |
+
"start_frame": start_frame,
|
| 1116 |
+
"end_frame": end_frame,
|
| 1117 |
+
"geometry_memory_frames": geometry_memory_frames,
|
| 1118 |
+
**self._build_condition_context_payload(
|
| 1119 |
+
frames=frames,
|
| 1120 |
+
scene_name=scene_name,
|
| 1121 |
+
start_frame=start_frame,
|
| 1122 |
+
ref_rt=rt_list_abs[0],
|
| 1123 |
+
actions=actions,
|
| 1124 |
+
),
|
| 1125 |
+
}
|
| 1126 |
+
|
| 1127 |
+
def __getitem__(self, data_id):
|
| 1128 |
+
n = len(self.data)
|
| 1129 |
+
if n == 0:
|
| 1130 |
+
raise RuntimeError("CamVideoDataset has no scenes.")
|
| 1131 |
+
max_attempts = min(64, n)
|
| 1132 |
+
last_error = None
|
| 1133 |
+
for attempt in range(max_attempts):
|
| 1134 |
+
idx = (data_id + attempt) % n
|
| 1135 |
+
data_item = self.data[idx]
|
| 1136 |
+
scene_name = data_item.get("video_name", "?") if isinstance(data_item, dict) else "?"
|
| 1137 |
+
if scene_name in self.invalid_scenes:
|
| 1138 |
+
continue
|
| 1139 |
+
try:
|
| 1140 |
+
return self._try_get_sample(data_item)
|
| 1141 |
+
except (ValueError, FileNotFoundError, KeyError, OSError) as e:
|
| 1142 |
+
self.invalid_scenes.add(scene_name)
|
| 1143 |
+
last_error = e
|
| 1144 |
+
if attempt < 3 or attempt % 8 == 0:
|
| 1145 |
+
print(
|
| 1146 |
+
f"[CamVideoDataset] Skipping invalid scene '{scene_name}' "
|
| 1147 |
+
f"({type(e).__name__}: {e}); attempt {attempt + 1}/{max_attempts}"
|
| 1148 |
+
)
|
| 1149 |
+
continue
|
| 1150 |
+
raise RuntimeError(
|
| 1151 |
+
f"CamVideoDataset: exhausted {max_attempts} attempts starting from index {data_id}; "
|
| 1152 |
+
f"last error: {type(last_error).__name__}: {last_error}"
|
| 1153 |
+
)
|
| 1154 |
+
|
| 1155 |
+
def _build_condition_context_payload(self, frames, scene_name, start_frame, ref_rt, actions):
|
| 1156 |
+
if not self.use_condition_context_frames:
|
| 1157 |
+
return {}
|
| 1158 |
+
payload = {
|
| 1159 |
+
"use_condition_context_frames": False,
|
| 1160 |
+
"condition_frames": [],
|
| 1161 |
+
"condition_frame_indices": [],
|
| 1162 |
+
"condition_source": None,
|
| 1163 |
+
"condition_actions": [],
|
| 1164 |
+
}
|
| 1165 |
+
condition_mode = self._sample_condition_mode()
|
| 1166 |
+
payload["condition_source"] = condition_mode
|
| 1167 |
+
if condition_mode == "text_only":
|
| 1168 |
+
return payload
|
| 1169 |
+
payload["use_condition_context_frames"] = True
|
| 1170 |
+
if self.condition_first_frame:
|
| 1171 |
+
payload["condition_frames"].append(frames[0])
|
| 1172 |
+
payload["condition_frame_indices"].append(start_frame)
|
| 1173 |
+
payload["condition_source"] = "first_frame_only"
|
| 1174 |
+
if self.condition_use_camera_pose and actions:
|
| 1175 |
+
payload["condition_actions"].append(list(actions[0]))
|
| 1176 |
+
if (
|
| 1177 |
+
condition_mode == "first_plus_overlap"
|
| 1178 |
+
and self.condition_history_keyframes
|
| 1179 |
+
and self.num_condition_frames > len(payload["condition_frames"])
|
| 1180 |
+
):
|
| 1181 |
+
num_extra_conditions = self.num_condition_frames - len(payload["condition_frames"])
|
| 1182 |
+
overlap_payload = self._sample_overlap_conditions(
|
| 1183 |
+
scene_name=scene_name,
|
| 1184 |
+
start_frame=start_frame,
|
| 1185 |
+
ref_rt=ref_rt,
|
| 1186 |
+
target_height=frames[0].size[1],
|
| 1187 |
+
target_width=frames[0].size[0],
|
| 1188 |
+
num_extra_conditions=num_extra_conditions,
|
| 1189 |
+
)
|
| 1190 |
+
if overlap_payload is None:
|
| 1191 |
+
return payload
|
| 1192 |
+
overlap_frames, overlap_indices, overlap_actions = overlap_payload
|
| 1193 |
+
payload["condition_frames"].extend(overlap_frames)
|
| 1194 |
+
payload["condition_frame_indices"].extend(overlap_indices)
|
| 1195 |
+
if self.condition_use_camera_pose:
|
| 1196 |
+
payload["condition_actions"].extend(overlap_actions)
|
| 1197 |
+
payload["condition_source"] = "first_plus_overlap"
|
| 1198 |
+
return payload
|
| 1199 |
+
|
| 1200 |
+
def __len__(self):
|
| 1201 |
+
return len(self.data)
|
| 1202 |
+
|
| 1203 |
+
|
| 1204 |
+
class DiffusionTrainingModule(torch.nn.Module):
|
| 1205 |
+
def __init__(self):
|
| 1206 |
+
super().__init__()
|
| 1207 |
+
|
| 1208 |
+
|
| 1209 |
+
def to(self, *args, **kwargs):
|
| 1210 |
+
for name, model in self.named_children():
|
| 1211 |
+
model.to(*args, **kwargs)
|
| 1212 |
+
return self
|
| 1213 |
+
|
| 1214 |
+
|
| 1215 |
+
def trainable_modules(self):
|
| 1216 |
+
trainable_modules = filter(lambda p: p.requires_grad, self.parameters())
|
| 1217 |
+
return trainable_modules
|
| 1218 |
+
|
| 1219 |
+
|
| 1220 |
+
def trainable_param_names(self):
|
| 1221 |
+
trainable_param_names = list(filter(lambda named_param: named_param[1].requires_grad, self.named_parameters()))
|
| 1222 |
+
trainable_param_names = set([named_param[0] for named_param in trainable_param_names])
|
| 1223 |
+
return trainable_param_names
|
| 1224 |
+
|
| 1225 |
+
|
| 1226 |
+
def add_lora_to_model(self, model, target_modules, lora_rank, lora_alpha=None):
|
| 1227 |
+
if lora_alpha is None:
|
| 1228 |
+
lora_alpha = lora_rank
|
| 1229 |
+
lora_config = LoraConfig(r=lora_rank, lora_alpha=lora_alpha, target_modules=target_modules)
|
| 1230 |
+
model = inject_adapter_in_model(lora_config, model)
|
| 1231 |
+
return model
|
| 1232 |
+
|
| 1233 |
+
|
| 1234 |
+
def export_trainable_state_dict(self, state_dict, remove_prefix=None):
|
| 1235 |
+
trainable_param_names = self.trainable_param_names()
|
| 1236 |
+
state_dict = {name: param for name, param in state_dict.items() if name in trainable_param_names}
|
| 1237 |
+
if remove_prefix is not None:
|
| 1238 |
+
state_dict_ = {}
|
| 1239 |
+
for name, param in state_dict.items():
|
| 1240 |
+
if name.startswith(remove_prefix):
|
| 1241 |
+
name = name[len(remove_prefix):]
|
| 1242 |
+
state_dict_[name] = param
|
| 1243 |
+
state_dict = state_dict_
|
| 1244 |
+
return state_dict
|
| 1245 |
+
|
| 1246 |
+
|
| 1247 |
+
|
| 1248 |
+
class ModelLogger:
|
| 1249 |
+
def __init__(self, output_path, remove_prefix_in_ckpt=None, state_dict_converter=lambda x:x):
|
| 1250 |
+
self.output_path = output_path
|
| 1251 |
+
self.remove_prefix_in_ckpt = remove_prefix_in_ckpt
|
| 1252 |
+
self.state_dict_converter = state_dict_converter
|
| 1253 |
+
|
| 1254 |
+
|
| 1255 |
+
def on_step_end(self, loss):
|
| 1256 |
+
pass
|
| 1257 |
+
|
| 1258 |
+
|
| 1259 |
+
def on_epoch_end(self, accelerator, model, epoch_id):
|
| 1260 |
+
accelerator.wait_for_everyone()
|
| 1261 |
+
if accelerator.is_main_process:
|
| 1262 |
+
state_dict = accelerator.get_state_dict(model)
|
| 1263 |
+
state_dict = accelerator.unwrap_model(model).export_trainable_state_dict(state_dict, remove_prefix=self.remove_prefix_in_ckpt)
|
| 1264 |
+
state_dict = self.state_dict_converter(state_dict)
|
| 1265 |
+
os.makedirs(self.output_path, exist_ok=True)
|
| 1266 |
+
path = os.path.join(self.output_path, f"epoch-{epoch_id}.safetensors")
|
| 1267 |
+
accelerator.save(state_dict, path, safe_serialization=True)
|
| 1268 |
+
|
| 1269 |
+
|
| 1270 |
+
|
| 1271 |
+
def launch_training_task(
|
| 1272 |
+
dataset: torch.utils.data.Dataset,
|
| 1273 |
+
model: DiffusionTrainingModule,
|
| 1274 |
+
model_logger: ModelLogger,
|
| 1275 |
+
optimizer: torch.optim.Optimizer,
|
| 1276 |
+
scheduler: torch.optim.lr_scheduler.LRScheduler,
|
| 1277 |
+
num_epochs: int = 1,
|
| 1278 |
+
gradient_accumulation_steps: int = 1,
|
| 1279 |
+
):
|
| 1280 |
+
dataloader = torch.utils.data.DataLoader(dataset, shuffle=True, collate_fn=lambda x: x[0], drop_last=True)
|
| 1281 |
+
accelerator = Accelerator(gradient_accumulation_steps=gradient_accumulation_steps)
|
| 1282 |
+
model, optimizer, dataloader, scheduler = accelerator.prepare(model, optimizer, dataloader, scheduler)
|
| 1283 |
+
|
| 1284 |
+
for epoch_id in range(num_epochs):
|
| 1285 |
+
for data in tqdm(dataloader):
|
| 1286 |
+
with accelerator.accumulate(model):
|
| 1287 |
+
optimizer.zero_grad()
|
| 1288 |
+
loss = model(data)
|
| 1289 |
+
accelerator.backward(loss)
|
| 1290 |
+
optimizer.step()
|
| 1291 |
+
model_logger.on_step_end(loss)
|
| 1292 |
+
scheduler.step()
|
| 1293 |
+
model_logger.on_epoch_end(accelerator, model, epoch_id)
|
| 1294 |
+
|
| 1295 |
+
def launch_data_process_task(model: DiffusionTrainingModule, dataset, output_path="./models"):
|
| 1296 |
+
dataloader = torch.utils.data.DataLoader(dataset, shuffle=False, collate_fn=lambda x: x[0], drop_last=True)
|
| 1297 |
+
accelerator = Accelerator()
|
| 1298 |
+
model, dataloader = accelerator.prepare(model, dataloader)
|
| 1299 |
+
os.makedirs(os.path.join(output_path, "data_cache"), exist_ok=True)
|
| 1300 |
+
for data_id, data in enumerate(tqdm(dataloader)):
|
| 1301 |
+
with torch.no_grad():
|
| 1302 |
+
inputs = model.forward_preprocess(data)
|
| 1303 |
+
inputs = {key: inputs[key] for key in model.model_input_keys if key in inputs}
|
| 1304 |
+
torch.save(inputs, os.path.join(output_path, "data_cache", f"{data_id}.pth"))
|
| 1305 |
+
|
| 1306 |
+
|
| 1307 |
+
|
| 1308 |
+
def wan_parser():
|
| 1309 |
+
parser = argparse.ArgumentParser(description="Simple example of a training script.")
|
| 1310 |
+
parser.add_argument("--dataset_base_path", type=str, default="", required=True, help="Base path of the dataset.")
|
| 1311 |
+
parser.add_argument("--dataset_metadata_path", type=str, default=None, help="Path to the metadata file of the dataset.")
|
| 1312 |
+
parser.add_argument("--max_pixels", type=int, default=1280*720, help="Maximum number of pixels per frame, used for dynamic resolution..")
|
| 1313 |
+
parser.add_argument("--height", type=int, default=None, help="Height of images or videos. Leave `height` and `width` empty to enable dynamic resolution.")
|
| 1314 |
+
parser.add_argument("--width", type=int, default=None, help="Width of images or videos. Leave `height` and `width` empty to enable dynamic resolution.")
|
| 1315 |
+
parser.add_argument("--num_frames", type=int, default=81, help="Number of frames per video. Frames are sampled from the video prefix.")
|
| 1316 |
+
parser.add_argument("--data_file_keys", type=str, default="image,video", help="Data file keys in the metadata. Comma-separated.")
|
| 1317 |
+
parser.add_argument("--dataset_repeat", type=int, default=1, help="Number of times to repeat the dataset per epoch.")
|
| 1318 |
+
parser.add_argument("--model_paths", type=str, default=None, help="Paths to load models. In JSON format.")
|
| 1319 |
+
parser.add_argument("--model_id_with_origin_paths", type=str, default=None, help="Model ID with origin paths, e.g., Wan-AI/Wan2.1-T2V-1.3B:diffusion_pytorch_model*.safetensors. Comma-separated.")
|
| 1320 |
+
parser.add_argument("--learning_rate", type=float, default=1e-4, help="Learning rate.")
|
| 1321 |
+
parser.add_argument("--num_epochs", type=int, default=1, help="Number of epochs.")
|
| 1322 |
+
parser.add_argument("--output_path", type=str, default="./models", help="Output save path.")
|
| 1323 |
+
parser.add_argument("--remove_prefix_in_ckpt", type=str, default="pipe.dit.", help="Remove prefix in ckpt.")
|
| 1324 |
+
parser.add_argument("--trainable_models", type=str, default=None, help="Models to train, e.g., dit, vae, text_encoder.")
|
| 1325 |
+
parser.add_argument("--lora_base_model", type=str, default=None, help="Which model LoRA is added to.")
|
| 1326 |
+
parser.add_argument("--lora_target_modules", type=str, default="q,k,v,o,ffn.0,ffn.2", help="Which layers LoRA is added to.")
|
| 1327 |
+
parser.add_argument("--lora_rank", type=int, default=32, help="Rank of LoRA.")
|
| 1328 |
+
parser.add_argument("--extra_inputs", default=None, help="Additional model inputs, comma-separated.")
|
| 1329 |
+
parser.add_argument("--use_gradient_checkpointing_offload", default=False, action="store_true", help="Whether to offload gradient checkpointing to CPU memory.")
|
| 1330 |
+
parser.add_argument("--gradient_accumulation_steps", type=int, default=1, help="Gradient accumulation steps.")
|
| 1331 |
+
parser.add_argument("--use_condition_context_frames", default=False, action="store_true", help="Enable appended clean condition latents.")
|
| 1332 |
+
parser.add_argument("--condition_first_frame", default=False, action="store_true", help="Use the current clip first frame as a clean condition frame.")
|
| 1333 |
+
parser.add_argument("--condition_history_keyframes", default=False, action="store_true", help="Use overlap-based keyframes as conditions.")
|
| 1334 |
+
parser.add_argument("--condition_use_camera_pose", default=True, action="store_true", help="Inject camera pose for condition frames.")
|
| 1335 |
+
parser.add_argument("--num_condition_frames", type=int, default=1, help="Number of condition frames.")
|
| 1336 |
+
parser.add_argument("--condition_frame_mode", type=str, default="first_frame_only", help="Condition frame selection mode.")
|
| 1337 |
+
parser.add_argument("--overlap_labels_root", type=str, default=None, help="Root dir for overlap label JSONs.")
|
| 1338 |
+
parser.add_argument("--condition_t2v_ratio", type=float, default=0.10, help="Ratio of text-only condition samples.")
|
| 1339 |
+
parser.add_argument("--condition_i2v_ratio", type=float, default=0.10, help="Ratio of first-frame-only condition samples.")
|
| 1340 |
+
return parser
|
| 1341 |
+
|
| 1342 |
+
|
| 1343 |
+
|
| 1344 |
+
def flux_parser():
|
| 1345 |
+
parser = argparse.ArgumentParser(description="Simple example of a training script.")
|
| 1346 |
+
parser.add_argument("--dataset_base_path", type=str, default="", required=True, help="Base path of the dataset.")
|
| 1347 |
+
parser.add_argument("--dataset_metadata_path", type=str, default=None, help="Path to the metadata file of the dataset.")
|
| 1348 |
+
parser.add_argument("--max_pixels", type=int, default=1024*1024, help="Maximum number of pixels per frame, used for dynamic resolution..")
|
| 1349 |
+
parser.add_argument("--height", type=int, default=None, help="Height of images. Leave `height` and `width` empty to enable dynamic resolution.")
|
| 1350 |
+
parser.add_argument("--width", type=int, default=None, help="Width of images. Leave `height` and `width` empty to enable dynamic resolution.")
|
| 1351 |
+
parser.add_argument("--data_file_keys", type=str, default="image", help="Data file keys in the metadata. Comma-separated.")
|
| 1352 |
+
parser.add_argument("--dataset_repeat", type=int, default=1, help="Number of times to repeat the dataset per epoch.")
|
| 1353 |
+
parser.add_argument("--model_paths", type=str, default=None, help="Paths to load models. In JSON format.")
|
| 1354 |
+
parser.add_argument("--model_id_with_origin_paths", type=str, default=None, help="Model ID with origin paths, e.g., Wan-AI/Wan2.1-T2V-1.3B:diffusion_pytorch_model*.safetensors. Comma-separated.")
|
| 1355 |
+
parser.add_argument("--learning_rate", type=float, default=1e-4, help="Learning rate.")
|
| 1356 |
+
parser.add_argument("--num_epochs", type=int, default=1, help="Number of epochs.")
|
| 1357 |
+
parser.add_argument("--output_path", type=str, default="./models", help="Output save path.")
|
| 1358 |
+
parser.add_argument("--remove_prefix_in_ckpt", type=str, default="pipe.dit.", help="Remove prefix in ckpt.")
|
| 1359 |
+
parser.add_argument("--trainable_models", type=str, default=None, help="Models to train, e.g., dit, vae, text_encoder.")
|
| 1360 |
+
parser.add_argument("--lora_base_model", type=str, default=None, help="Which model LoRA is added to.")
|
| 1361 |
+
parser.add_argument("--lora_target_modules", type=str, default="q,k,v,o,ffn.0,ffn.2", help="Which layers LoRA is added to.")
|
| 1362 |
+
parser.add_argument("--lora_rank", type=int, default=32, help="Rank of LoRA.")
|
| 1363 |
+
parser.add_argument("--extra_inputs", default=None, help="Additional model inputs, comma-separated.")
|
| 1364 |
+
parser.add_argument("--align_to_opensource_format", default=False, action="store_true", help="Whether to align the lora format to opensource format. Only for DiT's LoRA.")
|
| 1365 |
+
parser.add_argument("--use_gradient_checkpointing", default=False, action="store_true", help="Whether to use gradient checkpointing.")
|
| 1366 |
+
parser.add_argument("--use_gradient_checkpointing_offload", default=False, action="store_true", help="Whether to offload gradient checkpointing to CPU memory.")
|
| 1367 |
+
parser.add_argument("--gradient_accumulation_steps", type=int, default=1, help="Gradient accumulation steps.")
|
| 1368 |
+
return parser
|
code/diffsynth/trainers/utils.py.p0bak-getitem
ADDED
|
@@ -0,0 +1,1367 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import imageio, os, torch, warnings, torchvision, argparse, json, random
|
| 2 |
+
from peft import LoraConfig, inject_adapter_in_model
|
| 3 |
+
from PIL import Image
|
| 4 |
+
import pandas as pd
|
| 5 |
+
from tqdm import tqdm
|
| 6 |
+
from accelerate import Accelerator
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class ImageDataset(torch.utils.data.Dataset):
|
| 11 |
+
def __init__(
|
| 12 |
+
self,
|
| 13 |
+
base_path=None, metadata_path=None,
|
| 14 |
+
max_pixels=1920*1080, height=None, width=None,
|
| 15 |
+
height_division_factor=16, width_division_factor=16,
|
| 16 |
+
data_file_keys=("image",),
|
| 17 |
+
image_file_extension=("jpg", "jpeg", "png", "webp"),
|
| 18 |
+
repeat=1,
|
| 19 |
+
args=None,
|
| 20 |
+
):
|
| 21 |
+
if args is not None:
|
| 22 |
+
base_path = args.dataset_base_path
|
| 23 |
+
metadata_path = args.dataset_metadata_path
|
| 24 |
+
height = args.height
|
| 25 |
+
width = args.width
|
| 26 |
+
max_pixels = args.max_pixels
|
| 27 |
+
data_file_keys = args.data_file_keys.split(",")
|
| 28 |
+
repeat = args.dataset_repeat
|
| 29 |
+
|
| 30 |
+
self.base_path = base_path
|
| 31 |
+
self.max_pixels = max_pixels
|
| 32 |
+
self.height = height
|
| 33 |
+
self.width = width
|
| 34 |
+
self.height_division_factor = height_division_factor
|
| 35 |
+
self.width_division_factor = width_division_factor
|
| 36 |
+
self.data_file_keys = data_file_keys
|
| 37 |
+
self.image_file_extension = image_file_extension
|
| 38 |
+
self.repeat = repeat
|
| 39 |
+
|
| 40 |
+
if height is not None and width is not None:
|
| 41 |
+
print("Height and width are fixed. Setting `dynamic_resolution` to False.")
|
| 42 |
+
self.dynamic_resolution = False
|
| 43 |
+
elif height is None and width is None:
|
| 44 |
+
print("Height and width are none. Setting `dynamic_resolution` to True.")
|
| 45 |
+
self.dynamic_resolution = True
|
| 46 |
+
|
| 47 |
+
if metadata_path is None:
|
| 48 |
+
print("No metadata. Trying to generate it.")
|
| 49 |
+
metadata = self.generate_metadata(base_path)
|
| 50 |
+
print(f"{len(metadata)} lines in metadata.")
|
| 51 |
+
self.data = [metadata.iloc[i].to_dict() for i in range(len(metadata))]
|
| 52 |
+
elif metadata_path.endswith(".json"):
|
| 53 |
+
with open(metadata_path, "r") as f:
|
| 54 |
+
metadata = json.load(f)
|
| 55 |
+
self.data = metadata
|
| 56 |
+
else:
|
| 57 |
+
metadata = pd.read_csv(metadata_path)
|
| 58 |
+
# Ensure prompt column is string type to avoid float conversion for NaN values
|
| 59 |
+
if 'prompt' in metadata.columns:
|
| 60 |
+
metadata['prompt'] = metadata['prompt'].astype(str)
|
| 61 |
+
# Replace 'nan' string (from NaN) with empty string
|
| 62 |
+
metadata['prompt'] = metadata['prompt'].replace('nan', '')
|
| 63 |
+
self.data = [metadata.iloc[i].to_dict() for i in range(len(metadata))]
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def generate_metadata(self, folder):
|
| 67 |
+
image_list, prompt_list = [], []
|
| 68 |
+
file_set = set(os.listdir(folder))
|
| 69 |
+
for file_name in file_set:
|
| 70 |
+
if "." not in file_name:
|
| 71 |
+
continue
|
| 72 |
+
file_ext_name = file_name.split(".")[-1].lower()
|
| 73 |
+
file_base_name = file_name[:-len(file_ext_name)-1]
|
| 74 |
+
if file_ext_name not in self.image_file_extension:
|
| 75 |
+
continue
|
| 76 |
+
prompt_file_name = file_base_name + ".txt"
|
| 77 |
+
if prompt_file_name not in file_set:
|
| 78 |
+
continue
|
| 79 |
+
with open(os.path.join(folder, prompt_file_name), "r", encoding="utf-8") as f:
|
| 80 |
+
prompt = f.read().strip()
|
| 81 |
+
image_list.append(file_name)
|
| 82 |
+
prompt_list.append(prompt)
|
| 83 |
+
metadata = pd.DataFrame()
|
| 84 |
+
metadata["image"] = image_list
|
| 85 |
+
metadata["prompt"] = prompt_list
|
| 86 |
+
return metadata
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def crop_and_resize(self, image, target_height, target_width):
|
| 90 |
+
width, height = image.size
|
| 91 |
+
scale = max(target_width / width, target_height / height)
|
| 92 |
+
image = torchvision.transforms.functional.resize(
|
| 93 |
+
image,
|
| 94 |
+
(round(height*scale), round(width*scale)),
|
| 95 |
+
interpolation=torchvision.transforms.InterpolationMode.BILINEAR
|
| 96 |
+
)
|
| 97 |
+
image = torchvision.transforms.functional.center_crop(image, (target_height, target_width))
|
| 98 |
+
return image
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def get_height_width(self, image):
|
| 102 |
+
if self.dynamic_resolution:
|
| 103 |
+
width, height = image.size
|
| 104 |
+
if width * height > self.max_pixels:
|
| 105 |
+
scale = (width * height / self.max_pixels) ** 0.5
|
| 106 |
+
height, width = int(height / scale), int(width / scale)
|
| 107 |
+
height = height // self.height_division_factor * self.height_division_factor
|
| 108 |
+
width = width // self.width_division_factor * self.width_division_factor
|
| 109 |
+
else:
|
| 110 |
+
height, width = self.height, self.width
|
| 111 |
+
return height, width
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def load_image(self, file_path):
|
| 115 |
+
image = Image.open(file_path).convert("RGB")
|
| 116 |
+
image = self.crop_and_resize(image, *self.get_height_width(image))
|
| 117 |
+
return image
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def load_data(self, file_path):
|
| 121 |
+
return self.load_image(file_path)
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def __getitem__(self, data_id):
|
| 125 |
+
data = self.data[data_id % len(self.data)].copy()
|
| 126 |
+
for key in self.data_file_keys:
|
| 127 |
+
if key in data:
|
| 128 |
+
path = os.path.join(self.base_path, data[key])
|
| 129 |
+
data[key] = self.load_data(path)
|
| 130 |
+
if data[key] is None:
|
| 131 |
+
warnings.warn(f"cannot load file {data[key]}.")
|
| 132 |
+
return None
|
| 133 |
+
return data
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def __len__(self):
|
| 137 |
+
return len(self.data) * self.repeat
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
class VideoDataset(torch.utils.data.Dataset):
|
| 142 |
+
def __init__(
|
| 143 |
+
self,
|
| 144 |
+
base_path=None, metadata_path=None,
|
| 145 |
+
num_frames=81,
|
| 146 |
+
time_division_factor=4, time_division_remainder=1,
|
| 147 |
+
max_pixels=1920*1080, height=None, width=None,
|
| 148 |
+
height_division_factor=16, width_division_factor=16,
|
| 149 |
+
data_file_keys=("video",),
|
| 150 |
+
image_file_extension=("jpg", "jpeg", "png", "webp"),
|
| 151 |
+
video_file_extension=("mp4", "avi", "mov", "wmv", "mkv", "flv", "webm"),
|
| 152 |
+
repeat=1,
|
| 153 |
+
args=None,
|
| 154 |
+
action_base_path=None,
|
| 155 |
+
enable_icl=False,
|
| 156 |
+
icl_num_examples=2,
|
| 157 |
+
icl_context_frames=8,
|
| 158 |
+
):
|
| 159 |
+
if args is not None:
|
| 160 |
+
base_path = args.dataset_base_path
|
| 161 |
+
metadata_path = args.dataset_metadata_path
|
| 162 |
+
height = args.height
|
| 163 |
+
width = args.width
|
| 164 |
+
max_pixels = args.max_pixels
|
| 165 |
+
num_frames = args.num_frames
|
| 166 |
+
data_file_keys = args.data_file_keys.split(",")
|
| 167 |
+
repeat = args.dataset_repeat
|
| 168 |
+
# In-context learning parameters
|
| 169 |
+
if hasattr(args, 'enable_icl'):
|
| 170 |
+
enable_icl = args.enable_icl
|
| 171 |
+
if hasattr(args, 'icl_num_examples'):
|
| 172 |
+
icl_num_examples = args.icl_num_examples
|
| 173 |
+
if hasattr(args, 'icl_context_frames'):
|
| 174 |
+
icl_context_frames = args.icl_context_frames
|
| 175 |
+
|
| 176 |
+
self.base_path = base_path
|
| 177 |
+
self.num_frames = num_frames
|
| 178 |
+
self.time_division_factor = time_division_factor
|
| 179 |
+
self.time_division_remainder = time_division_remainder
|
| 180 |
+
self.max_pixels = max_pixels
|
| 181 |
+
self.height = height
|
| 182 |
+
self.width = width
|
| 183 |
+
self.height_division_factor = height_division_factor
|
| 184 |
+
self.width_division_factor = width_division_factor
|
| 185 |
+
self.data_file_keys = data_file_keys
|
| 186 |
+
self.image_file_extension = image_file_extension
|
| 187 |
+
self.video_file_extension = video_file_extension
|
| 188 |
+
self.repeat = repeat
|
| 189 |
+
|
| 190 |
+
# In-context learning parameters
|
| 191 |
+
self.enable_icl = enable_icl
|
| 192 |
+
self.icl_num_examples = icl_num_examples
|
| 193 |
+
self.icl_context_frames = icl_context_frames
|
| 194 |
+
|
| 195 |
+
if height is not None and width is not None:
|
| 196 |
+
print("Height and width are fixed. Setting `dynamic_resolution` to False.")
|
| 197 |
+
self.dynamic_resolution = False
|
| 198 |
+
elif height is None and width is None:
|
| 199 |
+
print("Height and width are none. Setting `dynamic_resolution` to True.")
|
| 200 |
+
self.dynamic_resolution = True
|
| 201 |
+
|
| 202 |
+
if metadata_path is None:
|
| 203 |
+
print("No metadata. Trying to generate it.")
|
| 204 |
+
metadata = self.generate_metadata(base_path)
|
| 205 |
+
print(f"{len(metadata)} lines in metadata.")
|
| 206 |
+
self.data = [metadata.iloc[i].to_dict() for i in range(len(metadata))]
|
| 207 |
+
elif metadata_path.endswith(".json"):
|
| 208 |
+
with open(metadata_path, "r") as f:
|
| 209 |
+
metadata = json.load(f)
|
| 210 |
+
self.data = metadata
|
| 211 |
+
else:
|
| 212 |
+
metadata = pd.read_csv(metadata_path)
|
| 213 |
+
# Ensure prompt column is string type to avoid float conversion for NaN values
|
| 214 |
+
if 'prompt' in metadata.columns:
|
| 215 |
+
metadata['prompt'] = metadata['prompt'].astype(str)
|
| 216 |
+
# Replace 'nan' string (from NaN) with empty string
|
| 217 |
+
metadata['prompt'] = metadata['prompt'].replace('nan', '')
|
| 218 |
+
|
| 219 |
+
# CRITICAL FIX: Clean prompt - remove video path prefix if present
|
| 220 |
+
# Some CSV prompts start with "video_name.mp4 " prefix, which should be removed
|
| 221 |
+
def clean_prompt(prompt_str):
|
| 222 |
+
if not isinstance(prompt_str, str) or not prompt_str:
|
| 223 |
+
return prompt_str
|
| 224 |
+
# Check if prompt starts with a video path (contains .mp4 or /)
|
| 225 |
+
# Pattern: "VideoName/1234_5678.mp4 " or "VideoName.mp4 "
|
| 226 |
+
import re
|
| 227 |
+
# Match pattern: word/word.mp4 or word.mp4 at the start, followed by space
|
| 228 |
+
pattern = r'^[A-Za-z0-9_]+(/[A-Za-z0-9_]+)?\.mp4\s+'
|
| 229 |
+
cleaned = re.sub(pattern, '', prompt_str)
|
| 230 |
+
# Also handle truncated prompts ending with "..."
|
| 231 |
+
if cleaned.endswith('...'):
|
| 232 |
+
cleaned = cleaned[:-3].rstrip()
|
| 233 |
+
return cleaned.strip()
|
| 234 |
+
|
| 235 |
+
metadata['prompt'] = metadata['prompt'].apply(clean_prompt)
|
| 236 |
+
self.data = [metadata.iloc[i].to_dict() for i in range(len(metadata))]
|
| 237 |
+
|
| 238 |
+
self.action_base_path = action_base_path
|
| 239 |
+
|
| 240 |
+
if self.enable_icl:
|
| 241 |
+
print(f"In-context learning enabled: {icl_num_examples} examples, {icl_context_frames} context frames each")
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
def generate_metadata(self, folder):
|
| 245 |
+
video_list, prompt_list = [], []
|
| 246 |
+
file_set = set(os.listdir(folder))
|
| 247 |
+
for file_name in file_set:
|
| 248 |
+
if "." not in file_name:
|
| 249 |
+
continue
|
| 250 |
+
file_ext_name = file_name.split(".")[-1].lower()
|
| 251 |
+
file_base_name = file_name[:-len(file_ext_name)-1]
|
| 252 |
+
if file_ext_name not in self.image_file_extension and file_ext_name not in self.video_file_extension:
|
| 253 |
+
continue
|
| 254 |
+
prompt_file_name = file_base_name + ".txt"
|
| 255 |
+
if prompt_file_name not in file_set:
|
| 256 |
+
continue
|
| 257 |
+
with open(os.path.join(folder, prompt_file_name), "r", encoding="utf-8") as f:
|
| 258 |
+
prompt = f.read().strip()
|
| 259 |
+
video_list.append(file_name)
|
| 260 |
+
prompt_list.append(prompt)
|
| 261 |
+
metadata = pd.DataFrame()
|
| 262 |
+
metadata["video"] = video_list
|
| 263 |
+
metadata["prompt"] = prompt_list
|
| 264 |
+
return metadata
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
def crop_and_resize(self, image, target_height, target_width):
|
| 268 |
+
width, height = image.size
|
| 269 |
+
scale = max(target_width / width, target_height / height)
|
| 270 |
+
image = torchvision.transforms.functional.resize(
|
| 271 |
+
image,
|
| 272 |
+
(round(height*scale), round(width*scale)),
|
| 273 |
+
interpolation=torchvision.transforms.InterpolationMode.BILINEAR
|
| 274 |
+
)
|
| 275 |
+
image = torchvision.transforms.functional.center_crop(image, (target_height, target_width))
|
| 276 |
+
return image
|
| 277 |
+
|
| 278 |
+
|
| 279 |
+
def get_height_width(self, image):
|
| 280 |
+
if self.dynamic_resolution:
|
| 281 |
+
width, height = image.size
|
| 282 |
+
if width * height > self.max_pixels:
|
| 283 |
+
scale = (width * height / self.max_pixels) ** 0.5
|
| 284 |
+
height, width = int(height / scale), int(width / scale)
|
| 285 |
+
height = height // self.height_division_factor * self.height_division_factor
|
| 286 |
+
width = width // self.width_division_factor * self.width_division_factor
|
| 287 |
+
else:
|
| 288 |
+
height, width = self.height, self.width
|
| 289 |
+
return height, width
|
| 290 |
+
|
| 291 |
+
|
| 292 |
+
def get_num_frames(self, reader):
|
| 293 |
+
num_frames = self.num_frames
|
| 294 |
+
if int(reader.count_frames()) < num_frames:
|
| 295 |
+
num_frames = int(reader.count_frames())
|
| 296 |
+
while num_frames > 1 and num_frames % self.time_division_factor != self.time_division_remainder:
|
| 297 |
+
num_frames -= 1
|
| 298 |
+
return num_frames
|
| 299 |
+
|
| 300 |
+
|
| 301 |
+
def load_video(self, file_path):
|
| 302 |
+
reader = imageio.get_reader(file_path)
|
| 303 |
+
num_frames = self.get_num_frames(reader)
|
| 304 |
+
frames = []
|
| 305 |
+
for frame_id in range(num_frames):
|
| 306 |
+
frame = reader.get_data(frame_id)
|
| 307 |
+
frame = Image.fromarray(frame)
|
| 308 |
+
frame = self.crop_and_resize(frame, *self.get_height_width(frame))
|
| 309 |
+
frames.append(frame)
|
| 310 |
+
reader.close()
|
| 311 |
+
return frames
|
| 312 |
+
|
| 313 |
+
|
| 314 |
+
def load_image(self, file_path):
|
| 315 |
+
image = Image.open(file_path).convert("RGB")
|
| 316 |
+
image = self.crop_and_resize(image, *self.get_height_width(image))
|
| 317 |
+
frames = [image]
|
| 318 |
+
return frames
|
| 319 |
+
|
| 320 |
+
|
| 321 |
+
def is_image(self, file_path):
|
| 322 |
+
file_ext_name = file_path.split(".")[-1]
|
| 323 |
+
return file_ext_name.lower() in self.image_file_extension
|
| 324 |
+
|
| 325 |
+
|
| 326 |
+
def is_video(self, file_path):
|
| 327 |
+
file_ext_name = file_path.split(".")[-1]
|
| 328 |
+
return file_ext_name.lower() in self.video_file_extension
|
| 329 |
+
|
| 330 |
+
|
| 331 |
+
def load_data(self, file_path):
|
| 332 |
+
# Handle multiple frame paths separated by '|' (for frame sequences)
|
| 333 |
+
if '|' in str(file_path):
|
| 334 |
+
# Split the path by '|' to get individual frame paths
|
| 335 |
+
frame_paths = str(file_path).split('|')
|
| 336 |
+
frames = []
|
| 337 |
+
|
| 338 |
+
# Get base_path (dataset root)
|
| 339 |
+
if not hasattr(self, 'base_path') or not self.base_path:
|
| 340 |
+
warnings.warn(f"Cannot determine base directory for frame sequence: {file_path}")
|
| 341 |
+
return None
|
| 342 |
+
|
| 343 |
+
base_dir = self.base_path # This is the dataset root
|
| 344 |
+
|
| 345 |
+
# Check the first path to determine the format
|
| 346 |
+
first_frame = frame_paths[0].strip() if frame_paths else ""
|
| 347 |
+
|
| 348 |
+
# If first frame is already an absolute path (from __getitem__ joining),
|
| 349 |
+
# extract the base directory from it
|
| 350 |
+
if os.path.isabs(first_frame):
|
| 351 |
+
# Extract base directory from first frame path
|
| 352 |
+
# First frame format: /path/to/dataset/frames/video_name/frame.png
|
| 353 |
+
# We need to get /path/to/dataset
|
| 354 |
+
parts = first_frame.split(os.sep)
|
| 355 |
+
# Find 'frames' in the path and get everything before it
|
| 356 |
+
if 'frames' in parts:
|
| 357 |
+
frames_idx = parts.index('frames')
|
| 358 |
+
base_dir = os.sep.join(parts[:frames_idx])
|
| 359 |
+
else:
|
| 360 |
+
# Fallback: use self.base_path
|
| 361 |
+
base_dir = self.base_path
|
| 362 |
+
|
| 363 |
+
for frame_path in frame_paths:
|
| 364 |
+
frame_path = frame_path.strip()
|
| 365 |
+
if not frame_path:
|
| 366 |
+
continue
|
| 367 |
+
|
| 368 |
+
# Construct full path
|
| 369 |
+
if os.path.isabs(frame_path):
|
| 370 |
+
# Already absolute path (from __getitem__)
|
| 371 |
+
full_frame_path = frame_path
|
| 372 |
+
else:
|
| 373 |
+
# Relative path - need to construct full path
|
| 374 |
+
# Remove 'frames/' prefix if present (we'll add it consistently)
|
| 375 |
+
if frame_path.startswith('frames/'):
|
| 376 |
+
frame_path = frame_path[7:] # Remove 'frames/' prefix
|
| 377 |
+
|
| 378 |
+
# Always join with base_dir + 'frames/' since base_dir is dataset root
|
| 379 |
+
full_frame_path = os.path.join(base_dir, 'frames', frame_path)
|
| 380 |
+
|
| 381 |
+
# Load individual frame
|
| 382 |
+
if os.path.exists(full_frame_path):
|
| 383 |
+
if self.is_image(full_frame_path):
|
| 384 |
+
frame_data = self.load_image(full_frame_path)
|
| 385 |
+
if frame_data:
|
| 386 |
+
frames.extend(frame_data)
|
| 387 |
+
else:
|
| 388 |
+
warnings.warn(f"Frame is not an image: {full_frame_path}")
|
| 389 |
+
else:
|
| 390 |
+
warnings.warn(f"Frame not found: {full_frame_path}")
|
| 391 |
+
|
| 392 |
+
if frames:
|
| 393 |
+
return frames
|
| 394 |
+
else:
|
| 395 |
+
warnings.warn(f"No frames loaded from sequence: {file_path}")
|
| 396 |
+
return None
|
| 397 |
+
|
| 398 |
+
# Handle single file (image or video)
|
| 399 |
+
if self.is_image(file_path):
|
| 400 |
+
return self.load_image(file_path)
|
| 401 |
+
elif self.is_video(file_path):
|
| 402 |
+
return self.load_video(file_path)
|
| 403 |
+
else:
|
| 404 |
+
return None
|
| 405 |
+
|
| 406 |
+
|
| 407 |
+
def __getitem__(self, data_id):
|
| 408 |
+
data = self.data[data_id % len(self.data)].copy()
|
| 409 |
+
for key in self.data_file_keys:
|
| 410 |
+
if key in ["video_name", "start_frame", "end_frame"]:
|
| 411 |
+
if "actions" in data:
|
| 412 |
+
continue
|
| 413 |
+
try:
|
| 414 |
+
video_name = data.get("video_name")
|
| 415 |
+
if video_name is None:
|
| 416 |
+
warnings.warn(f"video_name is missing in metadata for data_id {data_id}. Skipping action loading.")
|
| 417 |
+
continue
|
| 418 |
+
|
| 419 |
+
if video_name.endswith(".mp4"):
|
| 420 |
+
video_name = ".".join(video_name.split(".")[:-1])
|
| 421 |
+
if "_" in video_name:
|
| 422 |
+
video_name = "_".join(video_name.split("_")[:4])
|
| 423 |
+
|
| 424 |
+
import json
|
| 425 |
+
json_path = os.path.join(self.action_base_path, video_name + ".json")
|
| 426 |
+
|
| 427 |
+
# Check if action file exists
|
| 428 |
+
if not os.path.exists(json_path):
|
| 429 |
+
warnings.warn(f"Action file does not exist: {json_path}. Skipping action loading for data_id {data_id}.")
|
| 430 |
+
continue
|
| 431 |
+
|
| 432 |
+
start_frame = data.get("start_frame")
|
| 433 |
+
end_frame = data.get("end_frame")
|
| 434 |
+
if start_frame is None or end_frame is None:
|
| 435 |
+
warnings.warn(f"start_frame or end_frame is missing in metadata for data_id {data_id}. Skipping action loading.")
|
| 436 |
+
continue
|
| 437 |
+
|
| 438 |
+
json_data = json.load(open(json_path, "r"))['actions']
|
| 439 |
+
actions = []
|
| 440 |
+
current_yaw = 0.0
|
| 441 |
+
for frame_id in range(start_frame+1, end_frame+1):
|
| 442 |
+
frame_str = str(frame_id)
|
| 443 |
+
if frame_str not in json_data:
|
| 444 |
+
warnings.warn(f"Frame {frame_id} not found in action file {json_path}. Skipping this frame.")
|
| 445 |
+
continue
|
| 446 |
+
|
| 447 |
+
action = json_data[frame_str]
|
| 448 |
+
new_action = [0.0] * (2 + 2 + 3 + 1 + 2)
|
| 449 |
+
if action['ws'] == 1:
|
| 450 |
+
new_action[0] = 1
|
| 451 |
+
elif action['ws'] == 2:
|
| 452 |
+
new_action[1] = 1
|
| 453 |
+
|
| 454 |
+
if action['ad'] == 1:
|
| 455 |
+
new_action[2] = 1
|
| 456 |
+
elif action['ad'] == 2:
|
| 457 |
+
new_action[3] = 1
|
| 458 |
+
|
| 459 |
+
if action['scs'] == 1 and action.get("jump_invalid", 0) == 0:
|
| 460 |
+
new_action[4] = 1
|
| 461 |
+
elif action['scs'] == 2:
|
| 462 |
+
new_action[5] = 1
|
| 463 |
+
elif action['scs'] == 3:
|
| 464 |
+
new_action[6] = 1
|
| 465 |
+
|
| 466 |
+
if action.get('collision', 0) == 1:
|
| 467 |
+
new_action[7] = 1
|
| 468 |
+
new_action[0] = 0
|
| 469 |
+
new_action[1] = 0
|
| 470 |
+
new_action[2] = 0
|
| 471 |
+
new_action[3] = 0
|
| 472 |
+
|
| 473 |
+
pre_pitch = action.get('pre_pitch', 0.0)
|
| 474 |
+
current_pitch = pre_pitch + action.get('pitch_delta', 0.0) * 15.0
|
| 475 |
+
current_yaw += action.get('yaw_delta', 0.0) * 15.0
|
| 476 |
+
new_action[8] = current_pitch
|
| 477 |
+
new_action[9] = current_yaw
|
| 478 |
+
|
| 479 |
+
actions.append(new_action)
|
| 480 |
+
data["actions"] = actions
|
| 481 |
+
except Exception as e:
|
| 482 |
+
warnings.warn(f"Exception while loading actions for data_id {data_id}: {e}. Continuing without actions.")
|
| 483 |
+
# Don't return None, just continue without actions
|
| 484 |
+
continue
|
| 485 |
+
elif key == "video":
|
| 486 |
+
# Check if data[key] exists and is not None
|
| 487 |
+
if key not in data or data[key] is None:
|
| 488 |
+
warnings.warn(f"Video key '{key}' is missing or None in metadata for data_id {data_id}. Skipping this sample.")
|
| 489 |
+
return None
|
| 490 |
+
|
| 491 |
+
# Handle frame sequences (paths with '|' separator)
|
| 492 |
+
video_path_str = str(data[key])
|
| 493 |
+
if '|' in video_path_str:
|
| 494 |
+
# For frame sequences, pass the full path string to load_data
|
| 495 |
+
# load_data will handle splitting and loading individual frames
|
| 496 |
+
path = os.path.join(self.base_path, video_path_str)
|
| 497 |
+
# Don't check path existence here for frame sequences
|
| 498 |
+
# load_data will handle individual frame loading
|
| 499 |
+
else:
|
| 500 |
+
path = os.path.join(self.base_path, data[key])
|
| 501 |
+
# Check if path exists (only for single files)
|
| 502 |
+
if not os.path.exists(path):
|
| 503 |
+
warnings.warn(f"Video file does not exist: {path}. Skipping this sample.")
|
| 504 |
+
return None
|
| 505 |
+
try:
|
| 506 |
+
data[key] = self.load_data(path)
|
| 507 |
+
if data[key] is None:
|
| 508 |
+
warnings.warn(f"Failed to load video file: {path}. load_data returned None.")
|
| 509 |
+
return None
|
| 510 |
+
except Exception as e:
|
| 511 |
+
warnings.warn(f"Exception while loading video file {path}: {e}. Skipping this sample.")
|
| 512 |
+
return None
|
| 513 |
+
|
| 514 |
+
# In-context learning: sample context examples from dataset
|
| 515 |
+
if self.enable_icl and len(self.data) > 1:
|
| 516 |
+
context_frames_list = []
|
| 517 |
+
context_actions_list = []
|
| 518 |
+
|
| 519 |
+
# Sample random examples from dataset (excluding current one)
|
| 520 |
+
current_idx = data_id % len(self.data)
|
| 521 |
+
candidate_indices = [i for i in range(len(self.data)) if i != current_idx]
|
| 522 |
+
if len(candidate_indices) > 0:
|
| 523 |
+
num_samples = min(self.icl_num_examples, len(candidate_indices))
|
| 524 |
+
sampled_indices = random.sample(candidate_indices, num_samples)
|
| 525 |
+
|
| 526 |
+
for sample_idx in sampled_indices:
|
| 527 |
+
sample_data = self.data[sample_idx].copy()
|
| 528 |
+
# Load video for context
|
| 529 |
+
if "video" in self.data_file_keys and "video" in sample_data:
|
| 530 |
+
video_path = os.path.join(self.base_path, sample_data["video"])
|
| 531 |
+
sample_video = self.load_data(video_path)
|
| 532 |
+
if sample_video is not None and len(sample_video) >= self.icl_context_frames:
|
| 533 |
+
# Sample context_frames from the video
|
| 534 |
+
start_idx = random.randint(0, max(0, len(sample_video) - self.icl_context_frames))
|
| 535 |
+
context_frames = sample_video[start_idx:start_idx + self.icl_context_frames]
|
| 536 |
+
context_frames_list.extend(context_frames)
|
| 537 |
+
|
| 538 |
+
# Load corresponding actions if available
|
| 539 |
+
if self.action_base_path is not None and "video_name" in sample_data:
|
| 540 |
+
try:
|
| 541 |
+
sample_video_name = sample_data["video_name"]
|
| 542 |
+
if sample_video_name.endswith(".mp4"):
|
| 543 |
+
sample_video_name = ".".join(sample_video_name.split(".")[:-1])
|
| 544 |
+
if "_" in sample_video_name:
|
| 545 |
+
sample_video_name = "_".join(sample_video_name.split("_")[:4])
|
| 546 |
+
sample_json_path = os.path.join(self.action_base_path, sample_video_name + ".json")
|
| 547 |
+
if os.path.exists(sample_json_path):
|
| 548 |
+
sample_json_data = json.load(open(sample_json_path, "r"))['actions']
|
| 549 |
+
sample_start_frame = sample_data.get("start_frame", 0)
|
| 550 |
+
sample_end_frame = sample_data.get("end_frame", len(sample_video))
|
| 551 |
+
|
| 552 |
+
# Get actions for the context frames
|
| 553 |
+
context_actions = []
|
| 554 |
+
context_yaw = 0.0
|
| 555 |
+
for frame_idx in range(sample_start_frame + start_idx + 1,
|
| 556 |
+
min(sample_start_frame + start_idx + self.icl_context_frames + 1, sample_end_frame + 1)):
|
| 557 |
+
if str(frame_idx) in sample_json_data:
|
| 558 |
+
action = sample_json_data[str(frame_idx)]
|
| 559 |
+
new_action = [0.0] * (2 + 2 + 3 + 1 + 2)
|
| 560 |
+
if action['ws'] == 1:
|
| 561 |
+
new_action[0] = 1
|
| 562 |
+
elif action['ws'] == 2:
|
| 563 |
+
new_action[1] = 1
|
| 564 |
+
if action['ad'] == 1:
|
| 565 |
+
new_action[2] = 1
|
| 566 |
+
elif action['ad'] == 2:
|
| 567 |
+
new_action[3] = 1
|
| 568 |
+
if action['scs'] == 1 and action.get("jump_invalid", 0) == 0:
|
| 569 |
+
new_action[4] = 1
|
| 570 |
+
elif action['scs'] == 2:
|
| 571 |
+
new_action[5] = 1
|
| 572 |
+
elif action['scs'] == 3:
|
| 573 |
+
new_action[6] = 1
|
| 574 |
+
if action.get('collision', 0) == 1:
|
| 575 |
+
new_action[7] = 1
|
| 576 |
+
new_action[0] = 0
|
| 577 |
+
new_action[1] = 0
|
| 578 |
+
new_action[2] = 0
|
| 579 |
+
new_action[3] = 0
|
| 580 |
+
pre_pitch = action.get('pre_pitch', 0.0)
|
| 581 |
+
current_pitch = pre_pitch + action.get('pitch_delta', 0.0) * 15.0
|
| 582 |
+
context_yaw += action.get('yaw_delta', 0.0) * 15.0
|
| 583 |
+
new_action[8] = current_pitch
|
| 584 |
+
new_action[9] = context_yaw
|
| 585 |
+
context_actions.append(new_action)
|
| 586 |
+
context_actions_list.extend(context_actions[:len(context_frames)])
|
| 587 |
+
except Exception as e:
|
| 588 |
+
# If loading actions fails, just skip
|
| 589 |
+
pass
|
| 590 |
+
|
| 591 |
+
if context_frames_list:
|
| 592 |
+
data["context_frames"] = context_frames_list
|
| 593 |
+
if context_actions_list and len(context_actions_list) == len(context_frames_list):
|
| 594 |
+
data["context_actions"] = context_actions_list
|
| 595 |
+
|
| 596 |
+
return data
|
| 597 |
+
|
| 598 |
+
|
| 599 |
+
def __len__(self):
|
| 600 |
+
return len(self.data) * self.repeat
|
| 601 |
+
|
| 602 |
+
@staticmethod
|
| 603 |
+
def get_one_hot(action, range=2):
|
| 604 |
+
one_hot = [0] * (range + 1)
|
| 605 |
+
one_hot[action] = 1
|
| 606 |
+
return one_hot
|
| 607 |
+
|
| 608 |
+
|
| 609 |
+
|
| 610 |
+
import numpy as np
|
| 611 |
+
|
| 612 |
+
|
| 613 |
+
class CamVideoDataset(torch.utils.data.Dataset):
|
| 614 |
+
"""Dataset for Context-as-Memory camera pose conditioned training (ported from VWM).
|
| 615 |
+
|
| 616 |
+
Loads 81 PNG frames from UE scenes with random temporal cropping and extracts
|
| 617 |
+
corresponding camera poses as 12-dim relative RT vectors subsampled to match
|
| 618 |
+
the 21 latent frames.
|
| 619 |
+
"""
|
| 620 |
+
def __init__(
|
| 621 |
+
self,
|
| 622 |
+
base_path=None, metadata_path=None,
|
| 623 |
+
num_frames=81,
|
| 624 |
+
height=None, width=None,
|
| 625 |
+
max_pixels=1920*1080,
|
| 626 |
+
height_division_factor=16, width_division_factor=16,
|
| 627 |
+
repeat=1,
|
| 628 |
+
args=None,
|
| 629 |
+
cam_position_scale=None,
|
| 630 |
+
):
|
| 631 |
+
if args is not None:
|
| 632 |
+
base_path = args.dataset_base_path
|
| 633 |
+
metadata_path = getattr(args, "dataset_metadata_path", metadata_path)
|
| 634 |
+
height = args.height
|
| 635 |
+
width = args.width
|
| 636 |
+
max_pixels = args.max_pixels
|
| 637 |
+
num_frames = args.num_frames
|
| 638 |
+
repeat = args.dataset_repeat
|
| 639 |
+
cam_position_scale = getattr(args, "cam_position_scale", 0.01)
|
| 640 |
+
self.use_condition_context_frames = getattr(args, "use_condition_context_frames", False)
|
| 641 |
+
self.condition_first_frame = getattr(args, "condition_first_frame", False)
|
| 642 |
+
self.condition_history_keyframes = getattr(args, "condition_history_keyframes", False)
|
| 643 |
+
self.condition_use_camera_pose = getattr(args, "condition_use_camera_pose", True)
|
| 644 |
+
self.num_condition_frames = getattr(args, "num_condition_frames", 1)
|
| 645 |
+
self.condition_frame_mode = getattr(args, "condition_frame_mode", "first_frame_only")
|
| 646 |
+
self.overlap_labels_root = getattr(args, "overlap_labels_root", None)
|
| 647 |
+
self.condition_t2v_ratio = getattr(args, "condition_t2v_ratio", 0.10)
|
| 648 |
+
self.condition_i2v_ratio = getattr(args, "condition_i2v_ratio", 0.10)
|
| 649 |
+
self.use_geometry_spatial_memory = getattr(args, "use_geometry_spatial_memory", False)
|
| 650 |
+
self.geometry_memory_column = getattr(args, "geometry_memory_column", "geometry_memory")
|
| 651 |
+
self.geometry_memory_root = getattr(args, "geometry_memory_root", None)
|
| 652 |
+
else:
|
| 653 |
+
self.use_condition_context_frames = False
|
| 654 |
+
self.condition_first_frame = False
|
| 655 |
+
self.condition_history_keyframes = False
|
| 656 |
+
self.condition_use_camera_pose = True
|
| 657 |
+
self.num_condition_frames = 1
|
| 658 |
+
self.condition_frame_mode = "first_frame_only"
|
| 659 |
+
self.overlap_labels_root = None
|
| 660 |
+
self.condition_t2v_ratio = 0.10
|
| 661 |
+
self.condition_i2v_ratio = 0.10
|
| 662 |
+
self.use_geometry_spatial_memory = False
|
| 663 |
+
self.geometry_memory_column = "geometry_memory"
|
| 664 |
+
self.geometry_memory_root = None
|
| 665 |
+
|
| 666 |
+
if cam_position_scale is None:
|
| 667 |
+
cam_position_scale = 0.01
|
| 668 |
+
self.cam_position_scale = float(cam_position_scale)
|
| 669 |
+
|
| 670 |
+
self.base_path = base_path
|
| 671 |
+
self.frames_dir = os.path.join(base_path, "frames")
|
| 672 |
+
self.jsons_dir = os.path.join(base_path, "jsons")
|
| 673 |
+
self.num_frames = num_frames
|
| 674 |
+
self.max_pixels = max_pixels
|
| 675 |
+
self.height = height
|
| 676 |
+
self.width = width
|
| 677 |
+
self.height_division_factor = height_division_factor
|
| 678 |
+
self.width_division_factor = width_division_factor
|
| 679 |
+
self.repeat = repeat
|
| 680 |
+
|
| 681 |
+
if height is not None and width is not None:
|
| 682 |
+
self.dynamic_resolution = False
|
| 683 |
+
else:
|
| 684 |
+
self.dynamic_resolution = True
|
| 685 |
+
|
| 686 |
+
if metadata_path is None:
|
| 687 |
+
print("No metadata. Trying to generate it.")
|
| 688 |
+
metadata = self.generate_metadata(base_path)
|
| 689 |
+
print(f"{len(metadata)} lines in metadata.")
|
| 690 |
+
self.data = [metadata.iloc[i].to_dict() for i in range(len(metadata))]
|
| 691 |
+
elif metadata_path.endswith(".json"):
|
| 692 |
+
with open(metadata_path, "r") as f:
|
| 693 |
+
metadata = json.load(f)
|
| 694 |
+
self.data = metadata
|
| 695 |
+
else:
|
| 696 |
+
metadata = pd.read_csv(metadata_path)
|
| 697 |
+
# Ensure prompt column is string type to avoid float conversion for NaN values
|
| 698 |
+
if 'prompt' in metadata.columns:
|
| 699 |
+
metadata['prompt'] = metadata['prompt'].astype(str)
|
| 700 |
+
# Replace 'nan' string (from NaN) with empty string
|
| 701 |
+
metadata['prompt'] = metadata['prompt'].replace('nan', '')
|
| 702 |
+
|
| 703 |
+
# CRITICAL FIX: Clean prompt - remove video path prefix if present
|
| 704 |
+
# Some CSV prompts start with "video_name.mp4 " prefix, which should be removed
|
| 705 |
+
def clean_prompt(prompt_str):
|
| 706 |
+
if not isinstance(prompt_str, str) or not prompt_str:
|
| 707 |
+
return prompt_str
|
| 708 |
+
# Check if prompt starts with a video path (contains .mp4 or /)
|
| 709 |
+
# Pattern: "VideoName/1234_5678.mp4 " or "VideoName.mp4 "
|
| 710 |
+
import re
|
| 711 |
+
# Match pattern: word/word.mp4 or word.mp4 at the start, followed by space
|
| 712 |
+
pattern = r'^[A-Za-z0-9_]+(/[A-Za-z0-9_]+)?\.mp4\s+'
|
| 713 |
+
cleaned = re.sub(pattern, '', prompt_str)
|
| 714 |
+
# Also handle truncated prompts ending with "..."
|
| 715 |
+
if cleaned.endswith('...'):
|
| 716 |
+
cleaned = cleaned[:-3].rstrip()
|
| 717 |
+
return cleaned.strip()
|
| 718 |
+
|
| 719 |
+
metadata['prompt'] = metadata['prompt'].apply(clean_prompt)
|
| 720 |
+
self.data = [metadata.iloc[i].to_dict() for i in range(len(metadata))]
|
| 721 |
+
|
| 722 |
+
captions_path = os.path.join(base_path, "captions.txt")
|
| 723 |
+
self.scene_captions = {}
|
| 724 |
+
with open(captions_path, "r") as f:
|
| 725 |
+
for line in f:
|
| 726 |
+
parts = line.strip().split("\t", 1)
|
| 727 |
+
if len(parts) < 2:
|
| 728 |
+
continue
|
| 729 |
+
clip_path, caption = parts
|
| 730 |
+
scene_name = "/".join(clip_path.split("/")[:-1])
|
| 731 |
+
fname = clip_path.split("/")[-1].replace(".mp4", "")
|
| 732 |
+
clip_start = int(fname.split("_")[0])
|
| 733 |
+
if scene_name not in self.scene_captions:
|
| 734 |
+
self.scene_captions[scene_name] = []
|
| 735 |
+
self.scene_captions[scene_name].append((clip_start, caption))
|
| 736 |
+
|
| 737 |
+
for scene_name in self.scene_captions:
|
| 738 |
+
self.scene_captions[scene_name].sort(key=lambda x: x[0])
|
| 739 |
+
|
| 740 |
+
self.scene_names = sorted(self.scene_captions.keys())
|
| 741 |
+
self.metadata_rows = []
|
| 742 |
+
if metadata_path and os.path.isfile(metadata_path):
|
| 743 |
+
metadata = pd.read_csv(metadata_path)
|
| 744 |
+
if "prompt" in metadata.columns:
|
| 745 |
+
metadata["prompt"] = metadata["prompt"].astype(str)
|
| 746 |
+
self.metadata_rows = [metadata.iloc[i].to_dict() for i in range(len(metadata))]
|
| 747 |
+
self.pose_cache = {}
|
| 748 |
+
self.overlap_cache = {}
|
| 749 |
+
self.invalid_scenes = set()
|
| 750 |
+
self.invalid_metadata_indices = set()
|
| 751 |
+
self.overlap_labels_root = self._resolve_overlap_labels_root(base_path, self.overlap_labels_root)
|
| 752 |
+
self._validate_condition_config()
|
| 753 |
+
|
| 754 |
+
total_items = len(self.data)
|
| 755 |
+
total_scenes = len(self.scene_names)
|
| 756 |
+
total_captions = sum(len(v) for v in self.scene_captions.values())
|
| 757 |
+
metadata_msg = f", metadata_rows={len(self.metadata_rows)}" if self.metadata_rows else ""
|
| 758 |
+
effective_len = (len(self.metadata_rows) if self.metadata_rows else total_scenes) * repeat
|
| 759 |
+
print(f"CamVideoDataset: {total_scenes} scenes, {total_captions} captions{metadata_msg}, "
|
| 760 |
+
f"repeat={repeat}, cam_position_scale={self.cam_position_scale}, "
|
| 761 |
+
f"effective length={total_items}")
|
| 762 |
+
|
| 763 |
+
def _resolve_overlap_labels_root(self, base_path, overlap_labels_root):
|
| 764 |
+
candidate_roots = []
|
| 765 |
+
if overlap_labels_root is not None:
|
| 766 |
+
candidate_roots.append(overlap_labels_root)
|
| 767 |
+
if base_path is not None:
|
| 768 |
+
candidate_roots.append(os.path.join(base_path, "overlap_labels"))
|
| 769 |
+
for root in candidate_roots:
|
| 770 |
+
if root is not None and os.path.isdir(root):
|
| 771 |
+
return root
|
| 772 |
+
return overlap_labels_root
|
| 773 |
+
|
| 774 |
+
def _validate_condition_config(self):
|
| 775 |
+
if self.condition_t2v_ratio < 0 or self.condition_i2v_ratio < 0:
|
| 776 |
+
raise ValueError("Condition sampling ratios must be non-negative.")
|
| 777 |
+
if self.condition_t2v_ratio + self.condition_i2v_ratio >= 1.0:
|
| 778 |
+
raise ValueError("condition_t2v_ratio + condition_i2v_ratio must be < 1.0.")
|
| 779 |
+
needs_overlap = (
|
| 780 |
+
self.use_condition_context_frames
|
| 781 |
+
and self.condition_frame_mode == "first_plus_overlap"
|
| 782 |
+
and self.condition_history_keyframes
|
| 783 |
+
and self.num_condition_frames > 1
|
| 784 |
+
)
|
| 785 |
+
if needs_overlap and (self.overlap_labels_root is None or not os.path.isdir(self.overlap_labels_root)):
|
| 786 |
+
raise FileNotFoundError(
|
| 787 |
+
"K-frame condition mode requires overlap_labels_root. "
|
| 788 |
+
"Pass --overlap_labels_root or keep overlap_labels under dataset_base_path/overlap_labels."
|
| 789 |
+
)
|
| 790 |
+
|
| 791 |
+
def _load_scene_poses(self, scene_name):
|
| 792 |
+
if scene_name not in self.pose_cache:
|
| 793 |
+
json_path = os.path.join(self.jsons_dir, scene_name + ".json")
|
| 794 |
+
try:
|
| 795 |
+
with open(json_path, "r") as f:
|
| 796 |
+
data = json.load(f)
|
| 797 |
+
except (FileNotFoundError, json.JSONDecodeError) as e:
|
| 798 |
+
raise ValueError(f"Pose JSON for scene '{scene_name}' is missing or corrupt: {e}")
|
| 799 |
+
if not isinstance(data, dict) or "CineCameraActor" not in data:
|
| 800 |
+
raise ValueError(
|
| 801 |
+
f"Pose JSON for scene '{scene_name}' lacks 'CineCameraActor' key "
|
| 802 |
+
f"(found keys: {list(data.keys()) if isinstance(data, dict) else type(data).__name__})."
|
| 803 |
+
)
|
| 804 |
+
cine = data["CineCameraActor"]
|
| 805 |
+
if not isinstance(cine, dict) or len(cine) == 0:
|
| 806 |
+
raise ValueError(f"Pose JSON for scene '{scene_name}' has empty 'CineCameraActor' entries.")
|
| 807 |
+
self.pose_cache[scene_name] = cine
|
| 808 |
+
return self.pose_cache[scene_name]
|
| 809 |
+
|
| 810 |
+
def _find_nearest_caption(self, scene_name, start_frame):
|
| 811 |
+
captions = self.scene_captions[scene_name]
|
| 812 |
+
best_idx = 0
|
| 813 |
+
best_dist = abs(captions[0][0] - start_frame)
|
| 814 |
+
for i, (clip_start, _) in enumerate(captions):
|
| 815 |
+
dist = abs(clip_start - start_frame)
|
| 816 |
+
if dist < best_dist:
|
| 817 |
+
best_dist = dist
|
| 818 |
+
best_idx = i
|
| 819 |
+
return captions[best_idx][1]
|
| 820 |
+
|
| 821 |
+
@staticmethod
|
| 822 |
+
def _compute_rt(position, rotation):
|
| 823 |
+
x, y, z = position
|
| 824 |
+
yaw_rad = np.radians(rotation[2])
|
| 825 |
+
cos_y, sin_y = np.cos(yaw_rad), np.sin(yaw_rad)
|
| 826 |
+
R = np.array([[cos_y, -sin_y, 0], [sin_y, cos_y, 0], [0, 0, 1]])
|
| 827 |
+
return [x, y, z] + R.flatten().tolist()
|
| 828 |
+
|
| 829 |
+
@staticmethod
|
| 830 |
+
def _to_relative_rt(rt_list, ref_rt):
|
| 831 |
+
R_ref = np.array(ref_rt[3:]).reshape(3, 3)
|
| 832 |
+
T_ref = np.array(ref_rt[:3]).reshape(3, 1)
|
| 833 |
+
R_ref_inv = R_ref.T
|
| 834 |
+
T_ref_inv = -R_ref_inv @ T_ref
|
| 835 |
+
result = []
|
| 836 |
+
for rt in rt_list:
|
| 837 |
+
R_i = np.array(rt[3:]).reshape(3, 3)
|
| 838 |
+
T_i = np.array(rt[:3]).reshape(3, 1)
|
| 839 |
+
R_new = R_ref_inv @ R_i
|
| 840 |
+
T_new = R_ref_inv @ T_i + T_ref_inv
|
| 841 |
+
result.append(T_new.flatten().tolist() + R_new.flatten().tolist())
|
| 842 |
+
return result
|
| 843 |
+
|
| 844 |
+
def crop_and_resize(self, image, target_height, target_width):
|
| 845 |
+
width, height = image.size
|
| 846 |
+
scale = max(target_width / width, target_height / height)
|
| 847 |
+
image = torchvision.transforms.functional.resize(
|
| 848 |
+
image,
|
| 849 |
+
(round(height * scale), round(width * scale)),
|
| 850 |
+
interpolation=torchvision.transforms.InterpolationMode.BILINEAR
|
| 851 |
+
)
|
| 852 |
+
image = torchvision.transforms.functional.center_crop(image, (target_height, target_width))
|
| 853 |
+
return image
|
| 854 |
+
|
| 855 |
+
def get_height_width(self, image):
|
| 856 |
+
if self.dynamic_resolution:
|
| 857 |
+
width, height = image.size
|
| 858 |
+
if width * height > self.max_pixels:
|
| 859 |
+
scale = (width * height / self.max_pixels) ** 0.5
|
| 860 |
+
height, width = int(height / scale), int(width / scale)
|
| 861 |
+
height = height // self.height_division_factor * self.height_division_factor
|
| 862 |
+
width = width // self.width_division_factor * self.width_division_factor
|
| 863 |
+
else:
|
| 864 |
+
height, width = self.height, self.width
|
| 865 |
+
return height, width
|
| 866 |
+
|
| 867 |
+
def _load_resized_frame(self, scene_name, frame_index, target_height, target_width):
|
| 868 |
+
frame_path = os.path.join(self.frames_dir, scene_name, f"{frame_index:04d}.png")
|
| 869 |
+
img = Image.open(frame_path).convert("RGB")
|
| 870 |
+
return self.crop_and_resize(img, target_height, target_width)
|
| 871 |
+
|
| 872 |
+
@staticmethod
|
| 873 |
+
def _parse_frame_token(token):
|
| 874 |
+
token = str(token).strip()
|
| 875 |
+
if not token:
|
| 876 |
+
return None, None
|
| 877 |
+
parts = token.split("/")
|
| 878 |
+
if parts and parts[0] == "frames":
|
| 879 |
+
parts = parts[1:]
|
| 880 |
+
if len(parts) < 2:
|
| 881 |
+
return None, None
|
| 882 |
+
scene_name = "/".join(parts[:-1])
|
| 883 |
+
stem = os.path.splitext(parts[-1])[0]
|
| 884 |
+
try:
|
| 885 |
+
frame_index = int(stem)
|
| 886 |
+
except ValueError:
|
| 887 |
+
return None, None
|
| 888 |
+
return scene_name, frame_index
|
| 889 |
+
|
| 890 |
+
def _metadata_scene_and_indices(self, row):
|
| 891 |
+
video_field = str(row.get("video", "") or "")
|
| 892 |
+
tokens = [t for t in video_field.split("|") if t]
|
| 893 |
+
parsed = [self._parse_frame_token(t) for t in tokens]
|
| 894 |
+
parsed = [(s, i) for s, i in parsed if s is not None and i is not None]
|
| 895 |
+
if parsed:
|
| 896 |
+
scene_name = str(row.get("video_name", "") or parsed[0][0])
|
| 897 |
+
frame_indices = [i for _, i in parsed[: self.num_frames]]
|
| 898 |
+
else:
|
| 899 |
+
scene_name = str(row.get("video_name", "") or "").strip()
|
| 900 |
+
if not scene_name:
|
| 901 |
+
raise ValueError("metadata row lacks video_name and parseable video paths")
|
| 902 |
+
start_frame = int(row.get("start_frame", 0) or 0)
|
| 903 |
+
frame_indices = list(range(start_frame, start_frame + self.num_frames))
|
| 904 |
+
if len(frame_indices) < self.num_frames:
|
| 905 |
+
raise ValueError(f"metadata row has {len(frame_indices)} frames, expected {self.num_frames}")
|
| 906 |
+
return scene_name, frame_indices[: self.num_frames]
|
| 907 |
+
|
| 908 |
+
def _resolve_geometry_path(self, token):
|
| 909 |
+
token = str(token).strip()
|
| 910 |
+
if not token:
|
| 911 |
+
return None
|
| 912 |
+
if os.path.isabs(token):
|
| 913 |
+
return token
|
| 914 |
+
root = self.geometry_memory_root or self.base_path
|
| 915 |
+
return os.path.join(root, token)
|
| 916 |
+
|
| 917 |
+
def _load_geometry_memory_frames(self, row):
|
| 918 |
+
value = row.get(self.geometry_memory_column, None)
|
| 919 |
+
if value is None or str(value).strip() == "" or str(value).lower() == "nan":
|
| 920 |
+
if self.use_geometry_spatial_memory:
|
| 921 |
+
raise ValueError(
|
| 922 |
+
f"metadata row lacks required geometry column '{self.geometry_memory_column}'"
|
| 923 |
+
)
|
| 924 |
+
return []
|
| 925 |
+
tokens = [token for token in str(value).split("|") if token.strip()]
|
| 926 |
+
if not tokens:
|
| 927 |
+
raise ValueError(f"empty geometry memory field '{self.geometry_memory_column}'")
|
| 928 |
+
|
| 929 |
+
if len(tokens) == 1:
|
| 930 |
+
path = self._resolve_geometry_path(tokens[0])
|
| 931 |
+
if path is None:
|
| 932 |
+
return []
|
| 933 |
+
if os.path.isdir(path):
|
| 934 |
+
names = sorted(
|
| 935 |
+
name
|
| 936 |
+
for name in os.listdir(path)
|
| 937 |
+
if os.path.splitext(name)[1].lower() in (".png", ".jpg", ".jpeg", ".webp")
|
| 938 |
+
)
|
| 939 |
+
paths = [os.path.join(path, name) for name in names]
|
| 940 |
+
return [Image.open(frame_path).convert("RGB") for frame_path in paths]
|
| 941 |
+
if os.path.splitext(path)[1].lower() in (".mp4", ".mov", ".avi", ".mkv", ".webm"):
|
| 942 |
+
reader = imageio.get_reader(path)
|
| 943 |
+
try:
|
| 944 |
+
frames = [Image.fromarray(frame).convert("RGB") for frame in reader]
|
| 945 |
+
finally:
|
| 946 |
+
reader.close()
|
| 947 |
+
if not frames:
|
| 948 |
+
raise ValueError(f"geometry memory video has no frames: {path}")
|
| 949 |
+
return frames
|
| 950 |
+
|
| 951 |
+
frames = []
|
| 952 |
+
for token in tokens:
|
| 953 |
+
path = self._resolve_geometry_path(token)
|
| 954 |
+
if path is None or not os.path.isfile(path):
|
| 955 |
+
raise FileNotFoundError(f"geometry memory frame not found: {path}")
|
| 956 |
+
frames.append(Image.open(path).convert("RGB"))
|
| 957 |
+
return frames
|
| 958 |
+
|
| 959 |
+
def _load_overlap_frames(self, scene_name, frame_index):
|
| 960 |
+
if self.overlap_labels_root is None:
|
| 961 |
+
return []
|
| 962 |
+
cache_key = (scene_name, int(frame_index))
|
| 963 |
+
if cache_key not in self.overlap_cache:
|
| 964 |
+
overlap_path = os.path.join(self.overlap_labels_root, scene_name, f"{int(frame_index)}.json")
|
| 965 |
+
if not os.path.exists(overlap_path):
|
| 966 |
+
self.overlap_cache[cache_key] = []
|
| 967 |
+
else:
|
| 968 |
+
with open(overlap_path, "r") as f:
|
| 969 |
+
overlap_data = json.load(f)
|
| 970 |
+
overlaps = overlap_data.get("overlapping_frames", [])
|
| 971 |
+
self.overlap_cache[cache_key] = [int(idx) for idx in overlaps]
|
| 972 |
+
return self.overlap_cache[cache_key]
|
| 973 |
+
|
| 974 |
+
def _compute_scene_rt(self, scene_name, frame_index):
|
| 975 |
+
frame_data = self._load_scene_poses(scene_name)[str(int(frame_index))]
|
| 976 |
+
raw_pos = frame_data["position"]
|
| 977 |
+
pos = [float(p) * self.cam_position_scale for p in raw_pos]
|
| 978 |
+
return self._compute_rt(pos, frame_data["rotation"])
|
| 979 |
+
|
| 980 |
+
def _sample_condition_mode(self):
|
| 981 |
+
if not self.use_condition_context_frames:
|
| 982 |
+
return "disabled"
|
| 983 |
+
if (
|
| 984 |
+
self.condition_frame_mode != "first_plus_overlap"
|
| 985 |
+
or not self.condition_history_keyframes
|
| 986 |
+
or self.num_condition_frames <= 1
|
| 987 |
+
):
|
| 988 |
+
return "first_frame_only"
|
| 989 |
+
sample = random.random()
|
| 990 |
+
if sample < self.condition_t2v_ratio:
|
| 991 |
+
return "text_only"
|
| 992 |
+
if sample < self.condition_t2v_ratio + self.condition_i2v_ratio:
|
| 993 |
+
return "first_frame_only"
|
| 994 |
+
return "first_plus_overlap"
|
| 995 |
+
|
| 996 |
+
def _sample_overlap_conditions(self, scene_name, start_frame, ref_rt, target_height, target_width, num_extra_conditions):
|
| 997 |
+
if num_extra_conditions <= 0:
|
| 998 |
+
return [], [], []
|
| 999 |
+
window_indices = set(range(start_frame, start_frame + self.num_frames))
|
| 1000 |
+
target_candidates = list(range(start_frame + 1, start_frame + self.num_frames))
|
| 1001 |
+
sampled_target_frames = random.sample(target_candidates, k=min(num_extra_conditions, len(target_candidates)))
|
| 1002 |
+
overlap_frames = []
|
| 1003 |
+
overlap_indices = []
|
| 1004 |
+
overlap_actions = []
|
| 1005 |
+
used_condition_indices = set()
|
| 1006 |
+
for target_frame_idx in sampled_target_frames:
|
| 1007 |
+
candidate_indices = [
|
| 1008 |
+
idx for idx in self._load_overlap_frames(scene_name, target_frame_idx)
|
| 1009 |
+
if idx not in window_indices and idx != target_frame_idx and idx not in used_condition_indices
|
| 1010 |
+
]
|
| 1011 |
+
if len(candidate_indices) == 0:
|
| 1012 |
+
return None
|
| 1013 |
+
chosen_idx = random.choice(candidate_indices)
|
| 1014 |
+
used_condition_indices.add(chosen_idx)
|
| 1015 |
+
overlap_indices.append(chosen_idx)
|
| 1016 |
+
overlap_frames.append(self._load_resized_frame(scene_name, chosen_idx, target_height, target_width))
|
| 1017 |
+
if self.condition_use_camera_pose:
|
| 1018 |
+
overlap_rt = self._compute_scene_rt(scene_name, chosen_idx)
|
| 1019 |
+
overlap_actions.append(self._to_relative_rt([overlap_rt], ref_rt)[0])
|
| 1020 |
+
if len(overlap_frames) != num_extra_conditions:
|
| 1021 |
+
return None
|
| 1022 |
+
return overlap_frames, overlap_indices, overlap_actions
|
| 1023 |
+
|
| 1024 |
+
def _try_get_sample(self, data_item):
|
| 1025 |
+
scene_name = data_item["video_name"]
|
| 1026 |
+
cam_data = self._load_scene_poses(scene_name)
|
| 1027 |
+
max_start = len(cam_data) - self.num_frames
|
| 1028 |
+
if max_start < 0:
|
| 1029 |
+
raise ValueError(f"Scene {scene_name} has fewer than {self.num_frames} frames.")
|
| 1030 |
+
start_frame = data_item["start_frame"]
|
| 1031 |
+
end_frame = start_frame + self.num_frames - 1
|
| 1032 |
+
assert end_frame == data_item["end_frame"]
|
| 1033 |
+
|
| 1034 |
+
frames = []
|
| 1035 |
+
for i in range(start_frame, end_frame + 1):
|
| 1036 |
+
frame_path = os.path.join(self.frames_dir, scene_name, f"{i:04d}.png")
|
| 1037 |
+
img = Image.open(frame_path).convert("RGB")
|
| 1038 |
+
img = self.crop_and_resize(img, *self.get_height_width(img))
|
| 1039 |
+
frames.append(img)
|
| 1040 |
+
|
| 1041 |
+
# prompt = self._find_nearest_caption(scene_name, start_frame)
|
| 1042 |
+
prompt = data_item["prompt"]
|
| 1043 |
+
|
| 1044 |
+
rt_list_abs = []
|
| 1045 |
+
for i in range(start_frame, end_frame + 1):
|
| 1046 |
+
key = str(i)
|
| 1047 |
+
if key not in cam_data:
|
| 1048 |
+
raise ValueError(f"Scene {scene_name} missing pose for frame {i}.")
|
| 1049 |
+
frame_data = cam_data[key]
|
| 1050 |
+
raw_pos = frame_data["position"]
|
| 1051 |
+
pos = [float(p) * self.cam_position_scale for p in raw_pos]
|
| 1052 |
+
rt = self._compute_rt(pos, frame_data["rotation"])
|
| 1053 |
+
rt_list_abs.append(rt)
|
| 1054 |
+
|
| 1055 |
+
rt_list = self._to_relative_rt(rt_list_abs, rt_list_abs[0])
|
| 1056 |
+
pose_indices = list(range(0, self.num_frames, 4))
|
| 1057 |
+
actions = [rt_list[i] for i in pose_indices]
|
| 1058 |
+
|
| 1059 |
+
return {
|
| 1060 |
+
"video": frames,
|
| 1061 |
+
"prompt": prompt,
|
| 1062 |
+
"actions": actions,
|
| 1063 |
+
"video_name": scene_name,
|
| 1064 |
+
"start_frame": start_frame,
|
| 1065 |
+
"end_frame": end_frame,
|
| 1066 |
+
**self._build_condition_context_payload(
|
| 1067 |
+
frames=frames,
|
| 1068 |
+
scene_name=scene_name,
|
| 1069 |
+
start_frame=start_frame,
|
| 1070 |
+
ref_rt=rt_list_abs[0],
|
| 1071 |
+
actions=actions,
|
| 1072 |
+
),
|
| 1073 |
+
}
|
| 1074 |
+
|
| 1075 |
+
def _try_get_metadata_sample(self, row):
|
| 1076 |
+
scene_name, frame_indices = self._metadata_scene_and_indices(row)
|
| 1077 |
+
start_frame = int(frame_indices[0])
|
| 1078 |
+
end_frame = int(frame_indices[-1])
|
| 1079 |
+
cam_data = self._load_scene_poses(scene_name)
|
| 1080 |
+
|
| 1081 |
+
frames = []
|
| 1082 |
+
for frame_idx in frame_indices:
|
| 1083 |
+
frame_path = os.path.join(self.frames_dir, scene_name, f"{int(frame_idx):04d}.png")
|
| 1084 |
+
img = Image.open(frame_path).convert("RGB")
|
| 1085 |
+
img = self.crop_and_resize(img, *self.get_height_width(img))
|
| 1086 |
+
frames.append(img)
|
| 1087 |
+
|
| 1088 |
+
prompt = row.get("prompt", None)
|
| 1089 |
+
if prompt is None or str(prompt).strip() == "" or str(prompt).lower() == "nan":
|
| 1090 |
+
prompt = self._find_nearest_caption(scene_name, start_frame)
|
| 1091 |
+
else:
|
| 1092 |
+
prompt = str(prompt)
|
| 1093 |
+
|
| 1094 |
+
rt_list_abs = []
|
| 1095 |
+
for frame_idx in frame_indices:
|
| 1096 |
+
key = str(int(frame_idx))
|
| 1097 |
+
if key not in cam_data:
|
| 1098 |
+
raise ValueError(f"Scene {scene_name} missing pose for frame {frame_idx}.")
|
| 1099 |
+
frame_data = cam_data[key]
|
| 1100 |
+
raw_pos = frame_data["position"]
|
| 1101 |
+
pos = [float(p) * self.cam_position_scale for p in raw_pos]
|
| 1102 |
+
rt = self._compute_rt(pos, frame_data["rotation"])
|
| 1103 |
+
rt_list_abs.append(rt)
|
| 1104 |
+
|
| 1105 |
+
rt_list = self._to_relative_rt(rt_list_abs, rt_list_abs[0])
|
| 1106 |
+
pose_indices = list(range(0, len(frame_indices), 4))
|
| 1107 |
+
actions = [rt_list[i] for i in pose_indices]
|
| 1108 |
+
geometry_memory_frames = self._load_geometry_memory_frames(row)
|
| 1109 |
+
|
| 1110 |
+
return {
|
| 1111 |
+
"video": frames,
|
| 1112 |
+
"prompt": prompt,
|
| 1113 |
+
"actions": actions,
|
| 1114 |
+
"video_name": scene_name,
|
| 1115 |
+
"start_frame": start_frame,
|
| 1116 |
+
"end_frame": end_frame,
|
| 1117 |
+
"geometry_memory_frames": geometry_memory_frames,
|
| 1118 |
+
**self._build_condition_context_payload(
|
| 1119 |
+
frames=frames,
|
| 1120 |
+
scene_name=scene_name,
|
| 1121 |
+
start_frame=start_frame,
|
| 1122 |
+
ref_rt=rt_list_abs[0],
|
| 1123 |
+
actions=actions,
|
| 1124 |
+
),
|
| 1125 |
+
}
|
| 1126 |
+
|
| 1127 |
+
def __getitem__(self, data_id):
|
| 1128 |
+
n = len(self.data)
|
| 1129 |
+
if n == 0:
|
| 1130 |
+
raise RuntimeError("CamVideoDataset has no scenes.")
|
| 1131 |
+
max_attempts = min(64, n)
|
| 1132 |
+
last_error = None
|
| 1133 |
+
for attempt in range(max_attempts):
|
| 1134 |
+
idx = (data_id + attempt) % n
|
| 1135 |
+
data_item = self.data[idx]
|
| 1136 |
+
# if scene_name in self.invalid_scenes:
|
| 1137 |
+
# continue
|
| 1138 |
+
# try:
|
| 1139 |
+
return self._try_get_sample(data_item)
|
| 1140 |
+
# except (ValueError, FileNotFoundError, KeyError, OSError) as e:
|
| 1141 |
+
# self.invalid_scenes.add(scene_name)
|
| 1142 |
+
# last_error = e
|
| 1143 |
+
# if attempt < 3 or attempt % 8 == 0:
|
| 1144 |
+
# print(
|
| 1145 |
+
# f"[CamVideoDataset] Skipping invalid scene '{scene_name}' "
|
| 1146 |
+
# f"({type(e).__name__}: {e}); attempt {attempt + 1}/{max_attempts}"
|
| 1147 |
+
# )
|
| 1148 |
+
# continue
|
| 1149 |
+
raise RuntimeError(
|
| 1150 |
+
f"CamVideoDataset: exhausted {max_attempts} attempts starting from index {data_id}; "
|
| 1151 |
+
f"last error: {type(last_error).__name__}: {last_error}"
|
| 1152 |
+
)
|
| 1153 |
+
|
| 1154 |
+
def _build_condition_context_payload(self, frames, scene_name, start_frame, ref_rt, actions):
|
| 1155 |
+
if not self.use_condition_context_frames:
|
| 1156 |
+
return {}
|
| 1157 |
+
payload = {
|
| 1158 |
+
"use_condition_context_frames": False,
|
| 1159 |
+
"condition_frames": [],
|
| 1160 |
+
"condition_frame_indices": [],
|
| 1161 |
+
"condition_source": None,
|
| 1162 |
+
"condition_actions": [],
|
| 1163 |
+
}
|
| 1164 |
+
condition_mode = self._sample_condition_mode()
|
| 1165 |
+
payload["condition_source"] = condition_mode
|
| 1166 |
+
if condition_mode == "text_only":
|
| 1167 |
+
return payload
|
| 1168 |
+
payload["use_condition_context_frames"] = True
|
| 1169 |
+
if self.condition_first_frame:
|
| 1170 |
+
payload["condition_frames"].append(frames[0])
|
| 1171 |
+
payload["condition_frame_indices"].append(start_frame)
|
| 1172 |
+
payload["condition_source"] = "first_frame_only"
|
| 1173 |
+
if self.condition_use_camera_pose and actions:
|
| 1174 |
+
payload["condition_actions"].append(list(actions[0]))
|
| 1175 |
+
if (
|
| 1176 |
+
condition_mode == "first_plus_overlap"
|
| 1177 |
+
and self.condition_history_keyframes
|
| 1178 |
+
and self.num_condition_frames > len(payload["condition_frames"])
|
| 1179 |
+
):
|
| 1180 |
+
num_extra_conditions = self.num_condition_frames - len(payload["condition_frames"])
|
| 1181 |
+
overlap_payload = self._sample_overlap_conditions(
|
| 1182 |
+
scene_name=scene_name,
|
| 1183 |
+
start_frame=start_frame,
|
| 1184 |
+
ref_rt=ref_rt,
|
| 1185 |
+
target_height=frames[0].size[1],
|
| 1186 |
+
target_width=frames[0].size[0],
|
| 1187 |
+
num_extra_conditions=num_extra_conditions,
|
| 1188 |
+
)
|
| 1189 |
+
if overlap_payload is None:
|
| 1190 |
+
return payload
|
| 1191 |
+
overlap_frames, overlap_indices, overlap_actions = overlap_payload
|
| 1192 |
+
payload["condition_frames"].extend(overlap_frames)
|
| 1193 |
+
payload["condition_frame_indices"].extend(overlap_indices)
|
| 1194 |
+
if self.condition_use_camera_pose:
|
| 1195 |
+
payload["condition_actions"].extend(overlap_actions)
|
| 1196 |
+
payload["condition_source"] = "first_plus_overlap"
|
| 1197 |
+
return payload
|
| 1198 |
+
|
| 1199 |
+
def __len__(self):
|
| 1200 |
+
return len(self.data)
|
| 1201 |
+
|
| 1202 |
+
|
| 1203 |
+
class DiffusionTrainingModule(torch.nn.Module):
|
| 1204 |
+
def __init__(self):
|
| 1205 |
+
super().__init__()
|
| 1206 |
+
|
| 1207 |
+
|
| 1208 |
+
def to(self, *args, **kwargs):
|
| 1209 |
+
for name, model in self.named_children():
|
| 1210 |
+
model.to(*args, **kwargs)
|
| 1211 |
+
return self
|
| 1212 |
+
|
| 1213 |
+
|
| 1214 |
+
def trainable_modules(self):
|
| 1215 |
+
trainable_modules = filter(lambda p: p.requires_grad, self.parameters())
|
| 1216 |
+
return trainable_modules
|
| 1217 |
+
|
| 1218 |
+
|
| 1219 |
+
def trainable_param_names(self):
|
| 1220 |
+
trainable_param_names = list(filter(lambda named_param: named_param[1].requires_grad, self.named_parameters()))
|
| 1221 |
+
trainable_param_names = set([named_param[0] for named_param in trainable_param_names])
|
| 1222 |
+
return trainable_param_names
|
| 1223 |
+
|
| 1224 |
+
|
| 1225 |
+
def add_lora_to_model(self, model, target_modules, lora_rank, lora_alpha=None):
|
| 1226 |
+
if lora_alpha is None:
|
| 1227 |
+
lora_alpha = lora_rank
|
| 1228 |
+
lora_config = LoraConfig(r=lora_rank, lora_alpha=lora_alpha, target_modules=target_modules)
|
| 1229 |
+
model = inject_adapter_in_model(lora_config, model)
|
| 1230 |
+
return model
|
| 1231 |
+
|
| 1232 |
+
|
| 1233 |
+
def export_trainable_state_dict(self, state_dict, remove_prefix=None):
|
| 1234 |
+
trainable_param_names = self.trainable_param_names()
|
| 1235 |
+
state_dict = {name: param for name, param in state_dict.items() if name in trainable_param_names}
|
| 1236 |
+
if remove_prefix is not None:
|
| 1237 |
+
state_dict_ = {}
|
| 1238 |
+
for name, param in state_dict.items():
|
| 1239 |
+
if name.startswith(remove_prefix):
|
| 1240 |
+
name = name[len(remove_prefix):]
|
| 1241 |
+
state_dict_[name] = param
|
| 1242 |
+
state_dict = state_dict_
|
| 1243 |
+
return state_dict
|
| 1244 |
+
|
| 1245 |
+
|
| 1246 |
+
|
| 1247 |
+
class ModelLogger:
|
| 1248 |
+
def __init__(self, output_path, remove_prefix_in_ckpt=None, state_dict_converter=lambda x:x):
|
| 1249 |
+
self.output_path = output_path
|
| 1250 |
+
self.remove_prefix_in_ckpt = remove_prefix_in_ckpt
|
| 1251 |
+
self.state_dict_converter = state_dict_converter
|
| 1252 |
+
|
| 1253 |
+
|
| 1254 |
+
def on_step_end(self, loss):
|
| 1255 |
+
pass
|
| 1256 |
+
|
| 1257 |
+
|
| 1258 |
+
def on_epoch_end(self, accelerator, model, epoch_id):
|
| 1259 |
+
accelerator.wait_for_everyone()
|
| 1260 |
+
if accelerator.is_main_process:
|
| 1261 |
+
state_dict = accelerator.get_state_dict(model)
|
| 1262 |
+
state_dict = accelerator.unwrap_model(model).export_trainable_state_dict(state_dict, remove_prefix=self.remove_prefix_in_ckpt)
|
| 1263 |
+
state_dict = self.state_dict_converter(state_dict)
|
| 1264 |
+
os.makedirs(self.output_path, exist_ok=True)
|
| 1265 |
+
path = os.path.join(self.output_path, f"epoch-{epoch_id}.safetensors")
|
| 1266 |
+
accelerator.save(state_dict, path, safe_serialization=True)
|
| 1267 |
+
|
| 1268 |
+
|
| 1269 |
+
|
| 1270 |
+
def launch_training_task(
|
| 1271 |
+
dataset: torch.utils.data.Dataset,
|
| 1272 |
+
model: DiffusionTrainingModule,
|
| 1273 |
+
model_logger: ModelLogger,
|
| 1274 |
+
optimizer: torch.optim.Optimizer,
|
| 1275 |
+
scheduler: torch.optim.lr_scheduler.LRScheduler,
|
| 1276 |
+
num_epochs: int = 1,
|
| 1277 |
+
gradient_accumulation_steps: int = 1,
|
| 1278 |
+
):
|
| 1279 |
+
dataloader = torch.utils.data.DataLoader(dataset, shuffle=True, collate_fn=lambda x: x[0], drop_last=True)
|
| 1280 |
+
accelerator = Accelerator(gradient_accumulation_steps=gradient_accumulation_steps)
|
| 1281 |
+
model, optimizer, dataloader, scheduler = accelerator.prepare(model, optimizer, dataloader, scheduler)
|
| 1282 |
+
|
| 1283 |
+
for epoch_id in range(num_epochs):
|
| 1284 |
+
for data in tqdm(dataloader):
|
| 1285 |
+
with accelerator.accumulate(model):
|
| 1286 |
+
optimizer.zero_grad()
|
| 1287 |
+
loss = model(data)
|
| 1288 |
+
accelerator.backward(loss)
|
| 1289 |
+
optimizer.step()
|
| 1290 |
+
model_logger.on_step_end(loss)
|
| 1291 |
+
scheduler.step()
|
| 1292 |
+
model_logger.on_epoch_end(accelerator, model, epoch_id)
|
| 1293 |
+
|
| 1294 |
+
def launch_data_process_task(model: DiffusionTrainingModule, dataset, output_path="./models"):
|
| 1295 |
+
dataloader = torch.utils.data.DataLoader(dataset, shuffle=False, collate_fn=lambda x: x[0], drop_last=True)
|
| 1296 |
+
accelerator = Accelerator()
|
| 1297 |
+
model, dataloader = accelerator.prepare(model, dataloader)
|
| 1298 |
+
os.makedirs(os.path.join(output_path, "data_cache"), exist_ok=True)
|
| 1299 |
+
for data_id, data in enumerate(tqdm(dataloader)):
|
| 1300 |
+
with torch.no_grad():
|
| 1301 |
+
inputs = model.forward_preprocess(data)
|
| 1302 |
+
inputs = {key: inputs[key] for key in model.model_input_keys if key in inputs}
|
| 1303 |
+
torch.save(inputs, os.path.join(output_path, "data_cache", f"{data_id}.pth"))
|
| 1304 |
+
|
| 1305 |
+
|
| 1306 |
+
|
| 1307 |
+
def wan_parser():
|
| 1308 |
+
parser = argparse.ArgumentParser(description="Simple example of a training script.")
|
| 1309 |
+
parser.add_argument("--dataset_base_path", type=str, default="", required=True, help="Base path of the dataset.")
|
| 1310 |
+
parser.add_argument("--dataset_metadata_path", type=str, default=None, help="Path to the metadata file of the dataset.")
|
| 1311 |
+
parser.add_argument("--max_pixels", type=int, default=1280*720, help="Maximum number of pixels per frame, used for dynamic resolution..")
|
| 1312 |
+
parser.add_argument("--height", type=int, default=None, help="Height of images or videos. Leave `height` and `width` empty to enable dynamic resolution.")
|
| 1313 |
+
parser.add_argument("--width", type=int, default=None, help="Width of images or videos. Leave `height` and `width` empty to enable dynamic resolution.")
|
| 1314 |
+
parser.add_argument("--num_frames", type=int, default=81, help="Number of frames per video. Frames are sampled from the video prefix.")
|
| 1315 |
+
parser.add_argument("--data_file_keys", type=str, default="image,video", help="Data file keys in the metadata. Comma-separated.")
|
| 1316 |
+
parser.add_argument("--dataset_repeat", type=int, default=1, help="Number of times to repeat the dataset per epoch.")
|
| 1317 |
+
parser.add_argument("--model_paths", type=str, default=None, help="Paths to load models. In JSON format.")
|
| 1318 |
+
parser.add_argument("--model_id_with_origin_paths", type=str, default=None, help="Model ID with origin paths, e.g., Wan-AI/Wan2.1-T2V-1.3B:diffusion_pytorch_model*.safetensors. Comma-separated.")
|
| 1319 |
+
parser.add_argument("--learning_rate", type=float, default=1e-4, help="Learning rate.")
|
| 1320 |
+
parser.add_argument("--num_epochs", type=int, default=1, help="Number of epochs.")
|
| 1321 |
+
parser.add_argument("--output_path", type=str, default="./models", help="Output save path.")
|
| 1322 |
+
parser.add_argument("--remove_prefix_in_ckpt", type=str, default="pipe.dit.", help="Remove prefix in ckpt.")
|
| 1323 |
+
parser.add_argument("--trainable_models", type=str, default=None, help="Models to train, e.g., dit, vae, text_encoder.")
|
| 1324 |
+
parser.add_argument("--lora_base_model", type=str, default=None, help="Which model LoRA is added to.")
|
| 1325 |
+
parser.add_argument("--lora_target_modules", type=str, default="q,k,v,o,ffn.0,ffn.2", help="Which layers LoRA is added to.")
|
| 1326 |
+
parser.add_argument("--lora_rank", type=int, default=32, help="Rank of LoRA.")
|
| 1327 |
+
parser.add_argument("--extra_inputs", default=None, help="Additional model inputs, comma-separated.")
|
| 1328 |
+
parser.add_argument("--use_gradient_checkpointing_offload", default=False, action="store_true", help="Whether to offload gradient checkpointing to CPU memory.")
|
| 1329 |
+
parser.add_argument("--gradient_accumulation_steps", type=int, default=1, help="Gradient accumulation steps.")
|
| 1330 |
+
parser.add_argument("--use_condition_context_frames", default=False, action="store_true", help="Enable appended clean condition latents.")
|
| 1331 |
+
parser.add_argument("--condition_first_frame", default=False, action="store_true", help="Use the current clip first frame as a clean condition frame.")
|
| 1332 |
+
parser.add_argument("--condition_history_keyframes", default=False, action="store_true", help="Use overlap-based keyframes as conditions.")
|
| 1333 |
+
parser.add_argument("--condition_use_camera_pose", default=True, action="store_true", help="Inject camera pose for condition frames.")
|
| 1334 |
+
parser.add_argument("--num_condition_frames", type=int, default=1, help="Number of condition frames.")
|
| 1335 |
+
parser.add_argument("--condition_frame_mode", type=str, default="first_frame_only", help="Condition frame selection mode.")
|
| 1336 |
+
parser.add_argument("--overlap_labels_root", type=str, default=None, help="Root dir for overlap label JSONs.")
|
| 1337 |
+
parser.add_argument("--condition_t2v_ratio", type=float, default=0.10, help="Ratio of text-only condition samples.")
|
| 1338 |
+
parser.add_argument("--condition_i2v_ratio", type=float, default=0.10, help="Ratio of first-frame-only condition samples.")
|
| 1339 |
+
return parser
|
| 1340 |
+
|
| 1341 |
+
|
| 1342 |
+
|
| 1343 |
+
def flux_parser():
|
| 1344 |
+
parser = argparse.ArgumentParser(description="Simple example of a training script.")
|
| 1345 |
+
parser.add_argument("--dataset_base_path", type=str, default="", required=True, help="Base path of the dataset.")
|
| 1346 |
+
parser.add_argument("--dataset_metadata_path", type=str, default=None, help="Path to the metadata file of the dataset.")
|
| 1347 |
+
parser.add_argument("--max_pixels", type=int, default=1024*1024, help="Maximum number of pixels per frame, used for dynamic resolution..")
|
| 1348 |
+
parser.add_argument("--height", type=int, default=None, help="Height of images. Leave `height` and `width` empty to enable dynamic resolution.")
|
| 1349 |
+
parser.add_argument("--width", type=int, default=None, help="Width of images. Leave `height` and `width` empty to enable dynamic resolution.")
|
| 1350 |
+
parser.add_argument("--data_file_keys", type=str, default="image", help="Data file keys in the metadata. Comma-separated.")
|
| 1351 |
+
parser.add_argument("--dataset_repeat", type=int, default=1, help="Number of times to repeat the dataset per epoch.")
|
| 1352 |
+
parser.add_argument("--model_paths", type=str, default=None, help="Paths to load models. In JSON format.")
|
| 1353 |
+
parser.add_argument("--model_id_with_origin_paths", type=str, default=None, help="Model ID with origin paths, e.g., Wan-AI/Wan2.1-T2V-1.3B:diffusion_pytorch_model*.safetensors. Comma-separated.")
|
| 1354 |
+
parser.add_argument("--learning_rate", type=float, default=1e-4, help="Learning rate.")
|
| 1355 |
+
parser.add_argument("--num_epochs", type=int, default=1, help="Number of epochs.")
|
| 1356 |
+
parser.add_argument("--output_path", type=str, default="./models", help="Output save path.")
|
| 1357 |
+
parser.add_argument("--remove_prefix_in_ckpt", type=str, default="pipe.dit.", help="Remove prefix in ckpt.")
|
| 1358 |
+
parser.add_argument("--trainable_models", type=str, default=None, help="Models to train, e.g., dit, vae, text_encoder.")
|
| 1359 |
+
parser.add_argument("--lora_base_model", type=str, default=None, help="Which model LoRA is added to.")
|
| 1360 |
+
parser.add_argument("--lora_target_modules", type=str, default="q,k,v,o,ffn.0,ffn.2", help="Which layers LoRA is added to.")
|
| 1361 |
+
parser.add_argument("--lora_rank", type=int, default=32, help="Rank of LoRA.")
|
| 1362 |
+
parser.add_argument("--extra_inputs", default=None, help="Additional model inputs, comma-separated.")
|
| 1363 |
+
parser.add_argument("--align_to_opensource_format", default=False, action="store_true", help="Whether to align the lora format to opensource format. Only for DiT's LoRA.")
|
| 1364 |
+
parser.add_argument("--use_gradient_checkpointing", default=False, action="store_true", help="Whether to use gradient checkpointing.")
|
| 1365 |
+
parser.add_argument("--use_gradient_checkpointing_offload", default=False, action="store_true", help="Whether to offload gradient checkpointing to CPU memory.")
|
| 1366 |
+
parser.add_argument("--gradient_accumulation_steps", type=int, default=1, help="Gradient accumulation steps.")
|
| 1367 |
+
return parser
|
code/diffsynth/vram_management/__init__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .layers import *
|
| 2 |
+
from .gradient_checkpointing import *
|
code/diffsynth/vram_management/gradient_checkpointing.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
def create_custom_forward(module):
|
| 5 |
+
def custom_forward(*inputs, **kwargs):
|
| 6 |
+
return module(*inputs, **kwargs)
|
| 7 |
+
return custom_forward
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def gradient_checkpoint_forward(
|
| 11 |
+
model,
|
| 12 |
+
use_gradient_checkpointing,
|
| 13 |
+
use_gradient_checkpointing_offload,
|
| 14 |
+
*args,
|
| 15 |
+
**kwargs,
|
| 16 |
+
):
|
| 17 |
+
if use_gradient_checkpointing_offload:
|
| 18 |
+
with torch.autograd.graph.save_on_cpu():
|
| 19 |
+
model_output = torch.utils.checkpoint.checkpoint(
|
| 20 |
+
create_custom_forward(model),
|
| 21 |
+
*args,
|
| 22 |
+
**kwargs,
|
| 23 |
+
use_reentrant=False,
|
| 24 |
+
)
|
| 25 |
+
elif use_gradient_checkpointing:
|
| 26 |
+
model_output = torch.utils.checkpoint.checkpoint(
|
| 27 |
+
create_custom_forward(model),
|
| 28 |
+
*args,
|
| 29 |
+
**kwargs,
|
| 30 |
+
use_reentrant=False,
|
| 31 |
+
)
|
| 32 |
+
else:
|
| 33 |
+
model_output = model(*args, **kwargs)
|
| 34 |
+
return model_output
|
code/diffsynth/vram_management/layers.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch, copy
|
| 2 |
+
from ..models.utils import init_weights_on_device
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
def cast_to(weight, dtype, device):
|
| 6 |
+
r = torch.empty_like(weight, dtype=dtype, device=device)
|
| 7 |
+
r.copy_(weight)
|
| 8 |
+
return r
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class AutoTorchModule(torch.nn.Module):
|
| 12 |
+
def __init__(self):
|
| 13 |
+
super().__init__()
|
| 14 |
+
|
| 15 |
+
def check_free_vram(self):
|
| 16 |
+
_dev = self.computation_device
|
| 17 |
+
if not (isinstance(_dev, torch.device) and _dev.index is not None):
|
| 18 |
+
_dev = 0
|
| 19 |
+
gpu_mem_state = torch.cuda.mem_get_info(_dev)
|
| 20 |
+
used_memory = (gpu_mem_state[1] - gpu_mem_state[0]) / (1024 ** 3)
|
| 21 |
+
return used_memory < self.vram_limit
|
| 22 |
+
|
| 23 |
+
def offload(self):
|
| 24 |
+
if self.state != 0:
|
| 25 |
+
self.to(dtype=self.offload_dtype, device=self.offload_device)
|
| 26 |
+
self.state = 0
|
| 27 |
+
|
| 28 |
+
def onload(self):
|
| 29 |
+
if self.state != 1:
|
| 30 |
+
self.to(dtype=self.onload_dtype, device=self.onload_device)
|
| 31 |
+
self.state = 1
|
| 32 |
+
|
| 33 |
+
def keep(self):
|
| 34 |
+
if self.state != 2:
|
| 35 |
+
self.to(dtype=self.computation_dtype, device=self.computation_device)
|
| 36 |
+
self.state = 2
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class AutoWrappedModule(AutoTorchModule):
|
| 40 |
+
def __init__(self, module: torch.nn.Module, offload_dtype, offload_device, onload_dtype, onload_device, computation_dtype, computation_device, vram_limit, **kwargs):
|
| 41 |
+
super().__init__()
|
| 42 |
+
self.module = module.to(dtype=offload_dtype, device=offload_device)
|
| 43 |
+
self.offload_dtype = offload_dtype
|
| 44 |
+
self.offload_device = offload_device
|
| 45 |
+
self.onload_dtype = onload_dtype
|
| 46 |
+
self.onload_device = onload_device
|
| 47 |
+
self.computation_dtype = computation_dtype
|
| 48 |
+
self.computation_device = computation_device
|
| 49 |
+
self.vram_limit = vram_limit
|
| 50 |
+
self.state = 0
|
| 51 |
+
|
| 52 |
+
def forward(self, *args, **kwargs):
|
| 53 |
+
if self.state == 2:
|
| 54 |
+
module = self.module
|
| 55 |
+
else:
|
| 56 |
+
if self.onload_dtype == self.computation_dtype and self.onload_device == self.computation_device:
|
| 57 |
+
module = self.module
|
| 58 |
+
elif self.vram_limit is not None and self.check_free_vram():
|
| 59 |
+
self.keep()
|
| 60 |
+
module = self.module
|
| 61 |
+
else:
|
| 62 |
+
module = copy.deepcopy(self.module).to(dtype=self.computation_dtype, device=self.computation_device)
|
| 63 |
+
return module(*args, **kwargs)
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
class WanAutoCastLayerNorm(torch.nn.LayerNorm, AutoTorchModule):
|
| 67 |
+
def __init__(self, module: torch.nn.LayerNorm, offload_dtype, offload_device, onload_dtype, onload_device, computation_dtype, computation_device, vram_limit, **kwargs):
|
| 68 |
+
with init_weights_on_device(device=torch.device("meta")):
|
| 69 |
+
super().__init__(module.normalized_shape, eps=module.eps, elementwise_affine=module.elementwise_affine, bias=module.bias is not None, dtype=offload_dtype, device=offload_device)
|
| 70 |
+
self.weight = module.weight
|
| 71 |
+
self.bias = module.bias
|
| 72 |
+
self.offload_dtype = offload_dtype
|
| 73 |
+
self.offload_device = offload_device
|
| 74 |
+
self.onload_dtype = onload_dtype
|
| 75 |
+
self.onload_device = onload_device
|
| 76 |
+
self.computation_dtype = computation_dtype
|
| 77 |
+
self.computation_device = computation_device
|
| 78 |
+
self.vram_limit = vram_limit
|
| 79 |
+
self.state = 0
|
| 80 |
+
|
| 81 |
+
def forward(self, x, *args, **kwargs):
|
| 82 |
+
if self.state == 2:
|
| 83 |
+
weight, bias = self.weight, self.bias
|
| 84 |
+
else:
|
| 85 |
+
if self.onload_dtype == self.computation_dtype and self.onload_device == self.computation_device:
|
| 86 |
+
weight, bias = self.weight, self.bias
|
| 87 |
+
elif self.vram_limit is not None and self.check_free_vram():
|
| 88 |
+
self.keep()
|
| 89 |
+
weight, bias = self.weight, self.bias
|
| 90 |
+
else:
|
| 91 |
+
weight = None if self.weight is None else cast_to(self.weight, self.computation_dtype, self.computation_device)
|
| 92 |
+
bias = None if self.bias is None else cast_to(self.bias, self.computation_dtype, self.computation_device)
|
| 93 |
+
with torch.amp.autocast(device_type=x.device.type):
|
| 94 |
+
x = torch.nn.functional.layer_norm(x.float(), self.normalized_shape, weight, bias, self.eps).type_as(x)
|
| 95 |
+
return x
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
class AutoWrappedLinear(torch.nn.Linear, AutoTorchModule):
|
| 99 |
+
def __init__(self, module: torch.nn.Linear, offload_dtype, offload_device, onload_dtype, onload_device, computation_dtype, computation_device, vram_limit, name="", **kwargs):
|
| 100 |
+
with init_weights_on_device(device=torch.device("meta")):
|
| 101 |
+
super().__init__(in_features=module.in_features, out_features=module.out_features, bias=module.bias is not None, dtype=offload_dtype, device=offload_device)
|
| 102 |
+
self.weight = module.weight
|
| 103 |
+
self.bias = module.bias
|
| 104 |
+
self.offload_dtype = offload_dtype
|
| 105 |
+
self.offload_device = offload_device
|
| 106 |
+
self.onload_dtype = onload_dtype
|
| 107 |
+
self.onload_device = onload_device
|
| 108 |
+
self.computation_dtype = computation_dtype
|
| 109 |
+
self.computation_device = computation_device
|
| 110 |
+
self.vram_limit = vram_limit
|
| 111 |
+
self.state = 0
|
| 112 |
+
self.name = name
|
| 113 |
+
self.lora_A_weights = []
|
| 114 |
+
self.lora_B_weights = []
|
| 115 |
+
self.lora_merger = None
|
| 116 |
+
|
| 117 |
+
def forward(self, x, *args, **kwargs):
|
| 118 |
+
if self.state == 2:
|
| 119 |
+
weight, bias = self.weight, self.bias
|
| 120 |
+
else:
|
| 121 |
+
if self.onload_dtype == self.computation_dtype and self.onload_device == self.computation_device:
|
| 122 |
+
weight, bias = self.weight, self.bias
|
| 123 |
+
elif self.vram_limit is not None and self.check_free_vram():
|
| 124 |
+
self.keep()
|
| 125 |
+
weight, bias = self.weight, self.bias
|
| 126 |
+
else:
|
| 127 |
+
weight = cast_to(self.weight, self.computation_dtype, self.computation_device)
|
| 128 |
+
bias = None if self.bias is None else cast_to(self.bias, self.computation_dtype, self.computation_device)
|
| 129 |
+
out = torch.nn.functional.linear(x, weight, bias)
|
| 130 |
+
|
| 131 |
+
if len(self.lora_A_weights) == 0:
|
| 132 |
+
# No LoRA
|
| 133 |
+
return out
|
| 134 |
+
elif self.lora_merger is None:
|
| 135 |
+
# Native LoRA inference
|
| 136 |
+
for lora_A, lora_B in zip(self.lora_A_weights, self.lora_B_weights):
|
| 137 |
+
out = out + x @ lora_A.T @ lora_B.T
|
| 138 |
+
else:
|
| 139 |
+
# LoRA fusion
|
| 140 |
+
lora_output = []
|
| 141 |
+
for lora_A, lora_B in zip(self.lora_A_weights, self.lora_B_weights):
|
| 142 |
+
lora_output.append(x @ lora_A.T @ lora_B.T)
|
| 143 |
+
lora_output = torch.stack(lora_output)
|
| 144 |
+
out = self.lora_merger(out, lora_output)
|
| 145 |
+
return out
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
def enable_vram_management_recursively(model: torch.nn.Module, module_map: dict, module_config: dict, max_num_param=None, overflow_module_config: dict = None, total_num_param=0, vram_limit=None, name_prefix=""):
|
| 149 |
+
for name, module in model.named_children():
|
| 150 |
+
layer_name = name if name_prefix == "" else name_prefix + "." + name
|
| 151 |
+
for source_module, target_module in module_map.items():
|
| 152 |
+
if isinstance(module, source_module):
|
| 153 |
+
num_param = sum(p.numel() for p in module.parameters())
|
| 154 |
+
if max_num_param is not None and total_num_param + num_param > max_num_param:
|
| 155 |
+
module_config_ = overflow_module_config
|
| 156 |
+
else:
|
| 157 |
+
module_config_ = module_config
|
| 158 |
+
module_ = target_module(module, **module_config_, vram_limit=vram_limit, name=layer_name)
|
| 159 |
+
setattr(model, name, module_)
|
| 160 |
+
total_num_param += num_param
|
| 161 |
+
break
|
| 162 |
+
else:
|
| 163 |
+
total_num_param = enable_vram_management_recursively(module, module_map, module_config, max_num_param, overflow_module_config, total_num_param, vram_limit=vram_limit, name_prefix=layer_name)
|
| 164 |
+
return total_num_param
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
def enable_vram_management(model: torch.nn.Module, module_map: dict, module_config: dict, max_num_param=None, overflow_module_config: dict = None, vram_limit=None):
|
| 168 |
+
enable_vram_management_recursively(model, module_map, module_config, max_num_param, overflow_module_config, total_num_param=0, vram_limit=vram_limit)
|
| 169 |
+
model.vram_management_enabled = True
|
| 170 |
+
|
code/doc/DEVELOPER.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Developer Guide / 开发者指南
|
| 2 |
+
|
| 3 |
+
Bilingual interactive version: [Project Page → Developer Guide](https://echo-team-joy-future-academy-jd.github.io/Echo-Memory/developer.html)
|
| 4 |
+
|
| 5 |
+
Hands-on development, training/eval workflows, and **Cursor Agent skills** for Echo-Memory.
|
| 6 |
+
|
| 7 |
+
实战开发、训练评测与 **Cursor Agent 技能**。
|
| 8 |
+
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
## Cursor skills / 项目 Skills
|
| 12 |
+
|
| 13 |
+
Project skills live in **`.cursor/skills/`** — reference them in Agent chat (e.g. *use echo-memory-eval to …*).
|
| 14 |
+
|
| 15 |
+
| Skill | English | 中文 |
|
| 16 |
+
| --- | --- | --- |
|
| 17 |
+
| `echo-memory-agent` | Scope prompts, rules, skill index | Prompt 范围、Rules、技能索引 |
|
| 18 |
+
| `echo-memory-train` | Memory baselines & context training | Baseline 与 Context 训练 |
|
| 19 |
+
| `echo-memory-eval` | Replay / revisit & HF checkpoint checks | 回放 / revisit、HF checkpoint check |
|
| 20 |
+
| `echo-memory-release` | gh-pages, i18n, checkpoints doc | gh-pages、i18n、权重文档 |
|
| 21 |
+
|
| 22 |
+
Index: [.cursor/skills/README.md](../.cursor/skills/README.md)
|
| 23 |
+
|
| 24 |
+
---
|
| 25 |
+
|
| 26 |
+
## 1. Guide map / 文档地图
|
| 27 |
+
|
| 28 |
+
| | English | 中文 |
|
| 29 |
+
| --- | --- | --- |
|
| 30 |
+
| **README** | Paper overview, quick start, checkpoints, community | 论文概览、快速上手、权重、社区 |
|
| 31 |
+
| **This guide** | Workflows, Cursor skills, Agent tips | 工作流、Skills、Agent 技巧 |
|
| 32 |
+
| **`doc/`** | Dataset & checkpoint reference | 数据集与权重参考 |
|
| 33 |
+
|
| 34 |
+
---
|
| 35 |
+
|
| 36 |
+
## 2. Environment & paths / 环境与路径
|
| 37 |
+
|
| 38 |
+
```bash
|
| 39 |
+
export WAN_BASE_MODEL=/path/to/Wan2.1-T2V-1.3B
|
| 40 |
+
export DATASET_BASE_PATH=data/Context-as-Memory-Dataset
|
| 41 |
+
export PYTHONPATH=$PWD:${PYTHONPATH:-}
|
| 42 |
+
export OUTPUT_BASE_ROOT=$PWD/outputs
|
| 43 |
+
```
|
| 44 |
+
|
| 45 |
+
| Pool | English | 中文 |
|
| 46 |
+
| --- | --- | --- |
|
| 47 |
+
| Static in-domain | Default root above — [dataset_preprocessing.md](dataset_preprocessing.md) | 默认路径 — 同上 |
|
| 48 |
+
| Dynamic training | e.g. `data/dynamic-memory-dataset` — [dynamic_dataset_preprocessing.md](dynamic_dataset_preprocessing.md) | 如 `data/dynamic-memory-dataset` — 同上 |
|
| 49 |
+
| Checkpoints | [Echo-Team/Echo-Memory](https://huggingface.co/Echo-Team/Echo-Memory) — [checkpoints.md](checkpoints.md) | 同上 |
|
| 50 |
+
|
| 51 |
+
---
|
| 52 |
+
|
| 53 |
+
## 3. Code map / 代码地图
|
| 54 |
+
|
| 55 |
+
| Path | English | 中文 |
|
| 56 |
+
| --- | --- | --- |
|
| 57 |
+
| `.cursor/skills/` | Cursor Agent skills | Agent 技能 |
|
| 58 |
+
| `train/memory_baselines_basic/` | Spatial / SSM / compression | Spatial / SSM / 压缩 |
|
| 59 |
+
| `train/context_learning/` | Context K=1/5/20 | Context 配方 |
|
| 60 |
+
| `eval/v2/` | Replay, revisit | 回放、revisit |
|
| 61 |
+
| `env/memory_baseline_runtime.py` | CKPT → memory profile | 权重 → 记忆配置 |
|
| 62 |
+
| `docs/` | GitHub Pages | 项目页 |
|
| 63 |
+
|
| 64 |
+
---
|
| 65 |
+
|
| 66 |
+
## 4. Common workflows / 常用工作流
|
| 67 |
+
|
| 68 |
+
**Train / 训练**
|
| 69 |
+
|
| 70 |
+
```bash
|
| 71 |
+
bash train/memory_baselines_basic/run_spatial_memory_baseline.sh
|
| 72 |
+
bash train/context_learning/run_pre_qkv_ctx20.sh
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
**Checkpoint eval / checkpoint 检查**
|
| 76 |
+
|
| 77 |
+
```bash
|
| 78 |
+
huggingface-cli download Echo-Team/Echo-Memory context_k1/epoch-0.safetensors --local-dir ./ckpts
|
| 79 |
+
export CKPT=./ckpts/context_k1/epoch-0.safetensors
|
| 80 |
+
bash eval/v2/run_static_consistency_loop_and_revisit.sh
|
| 81 |
+
```
|
| 82 |
+
|
| 83 |
+
Keep the row folder name in `CKPT`.
|
| 84 |
+
|
| 85 |
+
---
|
| 86 |
+
|
| 87 |
+
## 5. Agent prompts / 示例 Prompt
|
| 88 |
+
|
| 89 |
+
```text
|
| 90 |
+
Using echo-memory-eval: download context_k1 from Echo-Team/Echo-Memory
|
| 91 |
+
and run eval/v2/run_basic_replay_gt.sh with the static in-domain pool.
|
| 92 |
+
|
| 93 |
+
Using echo-memory-train: document OUTPUT_BASE_ROOT override in
|
| 94 |
+
run_ablation_block_wise_ssm_two_chunk.sh.
|
| 95 |
+
```
|
| 96 |
+
|
| 97 |
+
**Public repo hygiene / 公开仓库规范:** no upload bash, internal benchmark names, or machine paths in GitHub.
|
| 98 |
+
|
| 99 |
+
---
|
| 100 |
+
|
| 101 |
+
## 6. Site & release / 站点与发布
|
| 102 |
+
|
| 103 |
+
```bash
|
| 104 |
+
bash scripts/publish_gh_pages.sh
|
| 105 |
+
```
|
| 106 |
+
|
| 107 |
+
Community QR: [project page → Updates](https://echo-team-joy-future-academy-jd.github.io/Echo-Memory/#updates) or README **Community**.
|
| 108 |
+
|
| 109 |
+
---
|
| 110 |
+
|
| 111 |
+
## 7. Checklist / 检查清单
|
| 112 |
+
|
| 113 |
+
- [ ] Quick eval with one HF checkpoint
|
| 114 |
+
- [ ] `doc/checkpoints.md` matches HF folders
|
| 115 |
+
- [ ] Public docs use Echo pool names
|
| 116 |
+
- [ ] Publish gh-pages after site edits; verify EN/中文 toggle
|
code/doc/README.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Echo-Memory documentation
|
| 2 |
+
|
| 3 |
+
| Doc | Echo pool | Covers |
|
| 4 |
+
| --- | --- | --- |
|
| 5 |
+
| [DEVELOPER.md](DEVELOPER.md) | **Developer guide** — workflows, `.cursor/skills/`, Cursor Agent |
|
| 6 |
+
| [checkpoints.md](checkpoints.md) | **Hugging Face weights** — [Echo-Team/Echo-Memory](https://huggingface.co/Echo-Team/Echo-Memory) baseline index |
|
| 7 |
+
| [memory_mechanisms.md](memory_mechanisms.md) | **Memory mechanisms** — paper row names, code modules, and training scripts |
|
| 8 |
+
| [dataset_preprocessing.md](dataset_preprocessing.md) | Static in-domain pool | Echo-Team package download → layout → metadata → latents |
|
| 9 |
+
| [dynamic_dataset_preprocessing.md](dynamic_dataset_preprocessing.md) | Dynamic training pool | subset download → export → training settings |
|
| 10 |
+
|
| 11 |
+
**Static in-domain pool:** download the Echo-Team package before in-domain replay/revisit eval.
|
| 12 |
+
|
| 13 |
+
**Dynamic training pool:** SpatialVID subset export + `DATASET_BASE_PATH` before training on the dynamic pool.
|
code/doc/checkpoints.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Checkpoints (Hugging Face)
|
| 2 |
+
|
| 3 |
+
**Repo:** [Echo-Team/Echo-Memory](https://huggingface.co/Echo-Team/Echo-Memory)
|
| 4 |
+
|
| 5 |
+
Fine-tuned DiT weights on top of [Wan-AI/Wan2.1-T2V-1.3B](https://huggingface.co/Wan-AI/Wan2.1-T2V-1.3B). Released rows are saved as `{row_id}/epoch-0.safetensors` after **1 epoch / 30,000 steps** on the static in-domain pool (640×352, 81-frame chunks). Mechanism names follow [memory_mechanisms.md](memory_mechanisms.md).
|
| 6 |
+
|
| 7 |
+
## Checkpoint index
|
| 8 |
+
|
| 9 |
+
| Family | Paper row | HF path | Steps | Echo-Memory recipe |
|
| 10 |
+
| --- | --- | --- | ---: | --- |
|
| 11 |
+
| Raw context | Context K=1 | [`context_k1/epoch-0.safetensors`](https://huggingface.co/Echo-Team/Echo-Memory/tree/main/context_k1) | 30,000 | `train/context_learning/run_pre_qkv_ctx1.sh` |
|
| 12 |
+
| Raw context | Context K=20 | TODO | TODO | `train/context_learning/run_pre_qkv_ctx20.sh` |
|
| 13 |
+
| Spatial | Spatial Memory | TODO | TODO | `train/memory_baselines_basic/run_spatial_memory_baseline.sh` |
|
| 14 |
+
| State-space | Block-wise SSM | TODO | TODO | `train/memory_baselines_basic/run_ablation_block_wise_ssm_two_chunk.sh` |
|
| 15 |
+
| State-space | Legacy Hybrid (VideoSSM) | TODO | TODO | `train/memory_baselines_basic/run_videossm_hybrid_baseline.sh` |
|
| 16 |
+
| Spatial | concat text (ablation) | TODO | TODO | `train/memory_baselines_basic/run_ablation_spatial_concat_text_two_chunk.sh` |
|
| 17 |
+
| Spatial | inject none (ablation) | TODO | TODO | `train/memory_baselines_basic/run_ablation_spatial_inject_none_two_chunk.sh` |
|
| 18 |
+
| Spatial | cross-attn t32 (ablation) | TODO | TODO | `train/memory_baselines_basic/run_ablation_spatial_cross_attn_readout_two_chunk.sh` |
|
| 19 |
+
| State-space | SSM ctx1 / every4 / hint21 | TODO | TODO | SSM ablation |
|
| 20 |
+
| State-space | SSM ctx5 / every1 / hint21 | TODO | TODO | SSM ablation |
|
| 21 |
+
| State-space | SSM ctx5 / every4 / hint81 | TODO | TODO | SSM ablation |
|
| 22 |
+
|
| 23 |
+
Context K=5, Context K=20, Spatial memory, FramePack compression, and State-space / SSM rows are TODO and not yet released as `epoch-0` weights.
|
| 24 |
+
|
| 25 |
+
## Download
|
| 26 |
+
|
| 27 |
+
```bash
|
| 28 |
+
pip install -U "huggingface_hub[cli]"
|
| 29 |
+
|
| 30 |
+
# one row (keeps HF folder layout under ./ckpts/)
|
| 31 |
+
huggingface-cli download Echo-Team/Echo-Memory context_k1/epoch-0.safetensors --local-dir ./ckpts
|
| 32 |
+
|
| 33 |
+
# all currently released rows
|
| 34 |
+
huggingface-cli download Echo-Team/Echo-Memory --local-dir ./ckpts
|
| 35 |
+
```
|
| 36 |
+
|
| 37 |
+
Keep the subdirectory name in the local path (e.g. `./ckpts/context_k1/epoch-0.safetensors`). Eval scripts use `env/memory_baseline_runtime.py` to infer memory flags from path substrings; Spatial and SSM checkpoint rows remain TODO.
|
| 38 |
+
|
| 39 |
+
## Use with Echo-Memory
|
| 40 |
+
|
| 41 |
+
Set the Wan backbone, static in-domain data pool, and checkpoint path:
|
| 42 |
+
|
| 43 |
+
```bash
|
| 44 |
+
export WAN_BASE_MODEL=/path/to/Wan2.1-T2V-1.3B
|
| 45 |
+
export DATASET_BASE_PATH=data/Context-as-Memory-Dataset
|
| 46 |
+
export PYTHONPATH=$PWD:${PYTHONPATH:-}
|
| 47 |
+
export CKPT=./ckpts/context_k1/epoch-0.safetensors
|
| 48 |
+
```
|
| 49 |
+
|
| 50 |
+
**In-domain replay + revisit (paper bundle):**
|
| 51 |
+
|
| 52 |
+
```bash
|
| 53 |
+
bash eval/v2/run_static_consistency_loop_and_revisit.sh
|
| 54 |
+
bash eval/v2/run_basic_replay_gt.sh
|
| 55 |
+
```
|
| 56 |
+
|
| 57 |
+
**Open-domain revisit** (first frames already in `assets/opendomain_revisit/`):
|
| 58 |
+
|
| 59 |
+
```bash
|
| 60 |
+
PHASE=stage1 OOD_DIR=assets/opendomain_revisit \
|
| 61 |
+
bash eval/v2/revisit_suite/run_one_click_revisit_eval.sh
|
| 62 |
+
```
|
| 63 |
+
|
| 64 |
+
**Visual comparison** (fixed prompt + first frame):
|
| 65 |
+
|
| 66 |
+
```bash
|
| 67 |
+
python eval/metrics/run_visual_eval.py \
|
| 68 |
+
--ckpt "$CKPT" \
|
| 69 |
+
--output_root ./evals_visual
|
| 70 |
+
```
|
| 71 |
+
|
| 72 |
+
See [eval/v2/README.md](../eval/v2/README.md) and [eval/metrics/README.md](../eval/metrics/README.md) for full options.
|
code/doc/dataset_preprocessing.md
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Static in-domain pool — download & preprocessing
|
| 2 |
+
|
| 3 |
+
Echo-Memory’s **static in-domain pool** is released through [Echo-Team/Echo-Memory-Data](https://huggingface.co/datasets/Echo-Team/Echo-Memory-Data) as tar parts under `static_pool_tar_parts/`. The underlying pool is sourced from [KlingTeam/Context-as-Memory-Dataset](https://huggingface.co/datasets/KlingTeam/Context-as-Memory-Dataset) on Hugging Face (Kling Team, SIGGRAPH Asia 2025; [arXiv:2506.03141](https://arxiv.org/abs/2506.03141)). Total size is about **340 GB** — plan disk space before downloading and unpacking.
|
| 4 |
+
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
## 1. Download
|
| 8 |
+
|
| 9 |
+
### Option A — Echo-Team packaged release
|
| 10 |
+
|
| 11 |
+
```bash
|
| 12 |
+
pip install -U "huggingface_hub[cli]"
|
| 13 |
+
|
| 14 |
+
mkdir -p data
|
| 15 |
+
|
| 16 |
+
huggingface-cli download Echo-Team/Echo-Memory-Data \
|
| 17 |
+
--repo-type dataset \
|
| 18 |
+
--include "static_pool_tar_parts/*" \
|
| 19 |
+
--local-dir ./data/echo-memory-data-release
|
| 20 |
+
|
| 21 |
+
cat ./data/echo-memory-data-release/static_pool_tar_parts/echo-memory-data.tar.part-* | tar -xf - -C ./data
|
| 22 |
+
```
|
| 23 |
+
|
| 24 |
+
You should end up with `data/Context-as-Memory-Dataset/`.
|
| 25 |
+
|
| 26 |
+
### Option B — original KlingTeam source
|
| 27 |
+
|
| 28 |
+
If you prefer the upstream release, download or merge the original parts from the [KlingTeam dataset card](https://huggingface.co/datasets/KlingTeam/Context-as-Memory-Dataset):
|
| 29 |
+
|
| 30 |
+
```bash
|
| 31 |
+
mkdir -p data
|
| 32 |
+
cd data
|
| 33 |
+
|
| 34 |
+
# after all Context-as-Memory-Dataset_* parts are downloaded into this directory:
|
| 35 |
+
cat Context-as-Memory-Dataset_* > Context-as-Memory-Dataset.zip
|
| 36 |
+
unzip Context-as-Memory-Dataset.zip -d .
|
| 37 |
+
```
|
| 38 |
+
|
| 39 |
+
You should end up with a directory named `Context-as-Memory-Dataset/` (adjust the path below if your folder name differs).
|
| 40 |
+
|
| 41 |
+
---
|
| 42 |
+
|
| 43 |
+
## 2. Expected layout (static in-domain pool)
|
| 44 |
+
|
| 45 |
+
After extraction, point `DATASET_BASE_PATH` at the pool root (default: `data/Context-as-Memory-Dataset/`):
|
| 46 |
+
|
| 47 |
+
```text
|
| 48 |
+
data/Context-as-Memory-Dataset/
|
| 49 |
+
├── frames/ # 100 scene folders, ~7601 PNGs each
|
| 50 |
+
│ ├── AncientTempleEnv_0/
|
| 51 |
+
│ │ ├── 0000.png
|
| 52 |
+
│ │ └── ...
|
| 53 |
+
│ └── ...
|
| 54 |
+
├── jsons/ # per-scene camera pose JSON (one file per scene)
|
| 55 |
+
│ ├── AncientTempleEnv_0.json
|
| 56 |
+
│ └── ...
|
| 57 |
+
├── overlap_labels/ # per-frame overlap indices (used by context retrieval / latent precompute)
|
| 58 |
+
│ ├── AncientTempleEnv_0/
|
| 59 |
+
│ │ ├── 0.json
|
| 60 |
+
│ │ └── ...
|
| 61 |
+
│ └── ...
|
| 62 |
+
├── captions.txt # segment captions (optional for some workflows)
|
| 63 |
+
└── metadata_full.csv # released Echo-Memory segment metadata
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
Quick sanity check:
|
| 67 |
+
|
| 68 |
+
```bash
|
| 69 |
+
export DATASET_BASE_PATH=data/Context-as-Memory-Dataset
|
| 70 |
+
|
| 71 |
+
test -d "${DATASET_BASE_PATH}/frames" && echo "frames OK"
|
| 72 |
+
test -d "${DATASET_BASE_PATH}/jsons" && echo "jsons OK"
|
| 73 |
+
test -d "${DATASET_BASE_PATH}/overlap_labels" && echo "overlap_labels OK"
|
| 74 |
+
ls "${DATASET_BASE_PATH}/frames" | head
|
| 75 |
+
ls "${DATASET_BASE_PATH}/jsons" | head
|
| 76 |
+
```
|
| 77 |
+
|
| 78 |
+
---
|
| 79 |
+
|
| 80 |
+
## 3. Point Echo-Memory at the static in-domain pool
|
| 81 |
+
|
| 82 |
+
```bash
|
| 83 |
+
export DATASET_BASE_PATH=data/Context-as-Memory-Dataset
|
| 84 |
+
export WAN_BASE_MODEL=/path/to/Wan2.1-T2V-1.3B
|
| 85 |
+
export PYTHONPATH=$PWD:${PYTHONPATH:-}
|
| 86 |
+
```
|
| 87 |
+
|
| 88 |
+
Training scripts also accept `data/Context-as-Memory-Dataset` under the repo root if `DATASET_BASE_PATH` is unset.
|
| 89 |
+
|
| 90 |
+
---
|
| 91 |
+
|
| 92 |
+
## 4. Metadata (required)
|
| 93 |
+
|
| 94 |
+
`metadata_full.csv` is included in the Echo-Team packaged release. If you downloaded the upstream KlingTeam source instead, fetch the released metadata into the pool root:
|
| 95 |
+
|
| 96 |
+
```bash
|
| 97 |
+
cd /path/to/Echo-Memory
|
| 98 |
+
export DATASET_BASE_PATH=data/Context-as-Memory-Dataset
|
| 99 |
+
|
| 100 |
+
huggingface-cli download Echo-Team/Echo-Memory-Data metadata_full.csv \
|
| 101 |
+
--repo-type dataset \
|
| 102 |
+
--local-dir "${DATASET_BASE_PATH}"
|
| 103 |
+
```
|
| 104 |
+
|
| 105 |
+
If you modify the pool or need to rebuild metadata locally, regenerate it from `frames/` and `captions.txt`:
|
| 106 |
+
|
| 107 |
+
```bash
|
| 108 |
+
bash scripts/run_generate_metadata.sh
|
| 109 |
+
```
|
| 110 |
+
|
| 111 |
+
You can also generate a smaller custom index for ablations or reduced-size training:
|
| 112 |
+
|
| 113 |
+
```bash
|
| 114 |
+
OUTPUT_CSV="${DATASET_BASE_PATH}/metadata_1000.csv" \
|
| 115 |
+
METADATA_MAX_ROWS=1000 \
|
| 116 |
+
bash scripts/run_generate_metadata.sh
|
| 117 |
+
```
|
| 118 |
+
|
| 119 |
+
Pass the custom CSV to training/evaluation with `--dataset_metadata_path "${DATASET_BASE_PATH}/metadata_1000.csv"`.
|
| 120 |
+
|
| 121 |
+
Defaults (override via env vars):
|
| 122 |
+
|
| 123 |
+
| Variable | Default | Meaning |
|
| 124 |
+
| --- | --- | --- |
|
| 125 |
+
| `OUTPUT_CSV` | `${DATASET_BASE_PATH}/metadata_full.csv` | Output metadata path |
|
| 126 |
+
| `SEGMENT_LENGTH` | `81` | Frames per training segment |
|
| 127 |
+
| `CONTEXT_FRAMES` | `5` | Context window used when building metadata |
|
| 128 |
+
| `NUM_WORKERS` | CPU count − 2 | Parallel workers |
|
| 129 |
+
| `METADATA_MAX_ROWS` / `DATASET_SIZE_ROWS` | `0` | Keep only the first N metadata rows after generation; `0` keeps the full CSV |
|
| 130 |
+
|
| 131 |
+
Verify:
|
| 132 |
+
|
| 133 |
+
```bash
|
| 134 |
+
wc -l "${DATASET_BASE_PATH}/metadata_full.csv"
|
| 135 |
+
head -n 3 "${DATASET_BASE_PATH}/metadata_full.csv"
|
| 136 |
+
```
|
| 137 |
+
|
| 138 |
+
---
|
| 139 |
+
|
| 140 |
+
## 5. Precompute latents (optional, speeds training)
|
| 141 |
+
|
| 142 |
+
If you train with precomputed VAE latents:
|
| 143 |
+
|
| 144 |
+
```bash
|
| 145 |
+
export WAN_BASE_MODEL=/path/to/Wan2.1-T2V-1.3B
|
| 146 |
+
export DATASET_BASE_PATH=data/Context-as-Memory-Dataset
|
| 147 |
+
NUM_PROCESSES=8 bash scripts/run_precompute_ctx_target_latents.sh
|
| 148 |
+
```
|
| 149 |
+
|
| 150 |
+
Latents are written under `${DATASET_BASE_PATH}/latents/`. The script can use `overlap_labels/` when `--use_overlap_labels` is enabled (see `scripts/run_precompute_ctx_target_latents.sh`).
|
| 151 |
+
|
| 152 |
+
---
|
| 153 |
+
|
| 154 |
+
## 6. Training pools vs. open-domain assets
|
| 155 |
+
|
| 156 |
+
| Echo pool / asset | Location | Purpose |
|
| 157 |
+
| --- | --- | --- |
|
| 158 |
+
| Static in-domain pool | `DATASET_BASE_PATH` → `data/Context-as-Memory-Dataset` | Training, in-domain replay/revisit, metadata |
|
| 159 |
+
| Dynamic training pool | `DATASET_BASE_PATH` → `data/dynamic-memory-dataset` | Training on the dynamic pool ([guide](dynamic_dataset_preprocessing.md)) |
|
| 160 |
+
| Open-domain first frames | `assets/opendomain_revisit/` | Held-out OOD revisit probes (already in repo) |
|
| 161 |
+
|
| 162 |
+
You do **not** need to rebuild open-domain anchors for the released revisit suite.
|
| 163 |
+
|
| 164 |
+
---
|
| 165 |
+
|
| 166 |
+
## 7. Troubleshooting
|
| 167 |
+
|
| 168 |
+
**`DATASET_BASE_PATH is not set`** — export the variable or place data at `data/Context-as-Memory-Dataset` relative to the repo root.
|
| 169 |
+
|
| 170 |
+
**Missing `frames/` or `jsons/`** — re-check unzip path; the root folder name must match what you pass to `DATASET_BASE_PATH`.
|
| 171 |
+
|
| 172 |
+
**Metadata script missing** — ensure you are on the latest Echo-Memory `main` branch; metadata generation is invoked via `scripts/run_generate_metadata.sh`.
|
| 173 |
+
|
| 174 |
+
**Disk space** — keep ~340 GB for raw frames plus extra space for `metadata_full.csv`, `latents/`, and training outputs.
|
| 175 |
+
|
| 176 |
+
---
|
| 177 |
+
|
| 178 |
+
## Reference
|
| 179 |
+
|
| 180 |
+
- Static in-domain pool: [dataset_preprocessing.md](dataset_preprocessing.md)
|
| 181 |
+
- Dynamic training pool: [dynamic_dataset_preprocessing.md](dynamic_dataset_preprocessing.md)
|
code/doc/dynamic_dataset_preprocessing.md
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Dynamic training pool — SpatialVID subset
|
| 2 |
+
|
| 3 |
+
Echo-Memory’s **dynamic training pool** uses a motion-filtered subset of [SpatialVID/SpatialVID](https://huggingface.co/datasets/SpatialVID/SpatialVID): ego-centric clips with camera poses and captions, exported into the same sample format used by the static pool.
|
| 4 |
+
|
| 5 |
+
This guide covers **download → export → training/inference settings** only. Dynamic eval is TODO; current public support is training and inference.
|
| 6 |
+
|
| 7 |
+
**License:** SpatialVID is [CC-BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/) (non-commercial). Static and dynamic pools may have different licenses — check before mixing runs.
|
| 8 |
+
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
## 1. Download (subset)
|
| 12 |
+
|
| 13 |
+
**Hugging Face:** [SpatialVID/SpatialVID](https://huggingface.co/datasets/SpatialVID/SpatialVID)
|
| 14 |
+
|
| 15 |
+
- Accept the dataset terms on Hugging Face before download.
|
| 16 |
+
- Full corpus is large (~7 TB+). For Echo-Memory dynamic training, download **selected groups** only — you do not need the full 545 groups.
|
| 17 |
+
|
| 18 |
+
```bash
|
| 19 |
+
pip install -U "huggingface_hub[cli]"
|
| 20 |
+
huggingface-cli login
|
| 21 |
+
|
| 22 |
+
export SPATIALVID_ROOT=/path/to/SpatialVID
|
| 23 |
+
hf download SpatialVID/SpatialVID --repo-type dataset --local-dir "${SPATIALVID_ROOT}"
|
| 24 |
+
```
|
| 25 |
+
|
| 26 |
+
To fetch specific groups, use include patterns or the helper script linked from the [dataset card](https://huggingface.co/datasets/SpatialVID/SpatialVID) (`download_SpatialVID.py` on the SpatialVID GitHub).
|
| 27 |
+
|
| 28 |
+
Extract downloaded `.tar.gz` groups:
|
| 29 |
+
|
| 30 |
+
```bash
|
| 31 |
+
cd "${SPATIALVID_ROOT}"
|
| 32 |
+
tar -xzvf annotations/group_0001.tar.gz
|
| 33 |
+
tar -xzvf videos/group_0001.tar.gz
|
| 34 |
+
```
|
| 35 |
+
|
| 36 |
+
### Raw layout (per clip)
|
| 37 |
+
|
| 38 |
+
```text
|
| 39 |
+
SPATIALVID_ROOT/
|
| 40 |
+
├── annotations/group_0001/{clip_id}/
|
| 41 |
+
│ ├── poses.npy # (N, 7) = tx,ty,tz,qx,qy,qz,qw
|
| 42 |
+
│ ├── indexes.txt # pose index → source frame index
|
| 43 |
+
│ ├── caption.json # scene / motion text
|
| 44 |
+
│ └── dyn_masks.npz # optional dynamic-region masks
|
| 45 |
+
├── videos/group_0001/{clip_id}.mp4
|
| 46 |
+
└── data/train/SpatialVID_metadata.csv
|
| 47 |
+
```
|
| 48 |
+
|
| 49 |
+
Use `SpatialVID_metadata.csv` to filter clips (e.g. `motion score`, `dynamicRatio`, `sceneType`) when building your subset.
|
| 50 |
+
|
| 51 |
+
---
|
| 52 |
+
|
| 53 |
+
## 2. Export to Echo layout (dynamic training pool)
|
| 54 |
+
|
| 55 |
+
Use `data/dynamic-spatialvid-motion60/mixed/` as the public training root and set `DATASET_BASE_PATH` to it:
|
| 56 |
+
|
| 57 |
+
```text
|
| 58 |
+
data/dynamic-spatialvid-motion60/
|
| 59 |
+
├── L1/ # single-level exports are also valid roots
|
| 60 |
+
├── L2/
|
| 61 |
+
├── L3/
|
| 62 |
+
└── mixed/
|
| 63 |
+
├── frames/L{1,2,3}/{clip_id}/0000.png ... 0080.png
|
| 64 |
+
├── jsons/L{1,2,3}/{clip_id}.json
|
| 65 |
+
├── overlap_labels/L{1,2,3}/{clip_id}/
|
| 66 |
+
├── captions.txt
|
| 67 |
+
├── metadata_train.csv
|
| 68 |
+
├── metadata_train_sample.csv
|
| 69 |
+
├── metadata_train_sample_L1.csv
|
| 70 |
+
├── metadata_eval.csv
|
| 71 |
+
└── metadata_eval_2chunk.csv
|
| 72 |
+
```
|
| 73 |
+
|
| 74 |
+
**Per-clip steps:**
|
| 75 |
+
|
| 76 |
+
| Step | Setting |
|
| 77 |
+
| --- | --- |
|
| 78 |
+
| Frame sample | **81** PNGs per clip, **640×352** |
|
| 79 |
+
| Pose | Interpolate `poses.npy` + `indexes.txt` → `jsons/{clip_id}.json` (Euler `CineCameraActor` format, same as static data) |
|
| 80 |
+
| Prompt | Short caption from `caption.json` (`SceneSummary` or `SceneDescription`) |
|
| 81 |
+
| Overlap | Build `overlap_labels/` for FOV-based context retrieval |
|
| 82 |
+
| Metadata row | `video`, `prompt`, `video_name`, `start_frame`, `end_frame`, optional `level` |
|
| 83 |
+
|
| 84 |
+
`metadata_train.csv` is written at export time. Use `metadata_train_sample.csv` or `metadata_train_sample_L1.csv` for local step checks. Do not re-run `run_generate_metadata.sh` unless you regenerate from raw frames only.
|
| 85 |
+
|
| 86 |
+
---
|
| 87 |
+
|
| 88 |
+
## 3. Training settings
|
| 89 |
+
|
| 90 |
+
Same env vars and on-disk layout as the static in-domain pool — only `DATASET_BASE_PATH` changes.
|
| 91 |
+
|
| 92 |
+
```bash
|
| 93 |
+
export WAN_BASE_MODEL=/path/to/Wan2.1-T2V-1.3B
|
| 94 |
+
export DATASET_BASE_PATH=data/dynamic-spatialvid-motion60/mixed
|
| 95 |
+
export PYTHONPATH=$PWD:${PYTHONPATH:-}
|
| 96 |
+
```
|
| 97 |
+
|
| 98 |
+
Recommended settings for the dynamic training pool (match memory baseline scripts):
|
| 99 |
+
|
| 100 |
+
| Parameter | Typical value |
|
| 101 |
+
| --- | --- |
|
| 102 |
+
| Resolution | **640 × 352** |
|
| 103 |
+
| Frames / chunk | **81** |
|
| 104 |
+
| Context frames | **1–20** (recipe-dependent) |
|
| 105 |
+
| `--use_rt_relative` | on |
|
| 106 |
+
| `--enable_fov_retrieval` | on (when `overlap_labels/` present) |
|
| 107 |
+
| `--enable_context_memory` | on for context / spatial / SSM rows |
|
| 108 |
+
| `--timestep_shift` | **15** |
|
| 109 |
+
| Learning rate | **5e-5** (adjust per row) |
|
| 110 |
+
|
| 111 |
+
Example — run a dynamic row:
|
| 112 |
+
|
| 113 |
+
```bash
|
| 114 |
+
METADATA_NAME=metadata_train.csv bash train/dynamic_spatialvid/run_dyn_spatial_mem.sh
|
| 115 |
+
```
|
| 116 |
+
|
| 117 |
+
For local one-step validation:
|
| 118 |
+
|
| 119 |
+
```bash
|
| 120 |
+
METADATA_NAME=metadata_train_sample_L1.csv \
|
| 121 |
+
MAX_TRAIN_STEPS=1 \
|
| 122 |
+
PROGRESS_TOTAL_STEPS=30000 \
|
| 123 |
+
NUM_WORKERS=0 \
|
| 124 |
+
bash train/dynamic_spatialvid/run_dyn_block_wise_ssm.sh
|
| 125 |
+
```
|
| 126 |
+
|
| 127 |
+
Inference wrappers live under `inference/dynamic_spatialvid/`.
|
| 128 |
+
|
| 129 |
+
---
|
| 130 |
+
|
| 131 |
+
## 4. Demo selection
|
| 132 |
+
|
| 133 |
+
Dynamic demos are selected from training-scene replay rather than from fixed eval scripts:
|
| 134 |
+
|
| 135 |
+
1. Randomly sample candidate scenes from `metadata_train.csv` or `metadata_train_sample.csv`.
|
| 136 |
+
2. Use the same prompt, first frame, and GT action trajectory for all six dynamic rows.
|
| 137 |
+
3. Run `inference/unified_inference.py` or `inference/dynamic_spatialvid/*.sh` for each checkpoint.
|
| 138 |
+
4. Manually pick a representative scene where all rows are viewable.
|
| 139 |
+
|
| 140 |
+
The checked-in README previews are compressed GIFs under `assets/readme_previews/`.
|
| 141 |
+
|
| 142 |
+
---
|
| 143 |
+
|
| 144 |
+
## 5. Checklist
|
| 145 |
+
|
| 146 |
+
- [ ] Hugging Face access approved for [SpatialVID/SpatialVID](https://huggingface.co/datasets/SpatialVID/SpatialVID)
|
| 147 |
+
- [ ] Subset of `group_****` archives downloaded and extracted
|
| 148 |
+
- [ ] Clips filtered (poses + caption present; optional motion / dynamic filters)
|
| 149 |
+
- [ ] `frames/`, `jsons/`, `metadata_train.csv` under one root
|
| 150 |
+
- [ ] (Recommended) `overlap_labels/` for FOV retrieval
|
| 151 |
+
- [ ] `DATASET_BASE_PATH` exported before training/inference
|
| 152 |
+
|
| 153 |
+
---
|
| 154 |
+
|
| 155 |
+
## Reference
|
| 156 |
+
|
| 157 |
+
- SpatialVID: [SpatialVID/SpatialVID](https://huggingface.co/datasets/SpatialVID/SpatialVID) · [arXiv:2509.09676](https://arxiv.org/abs/2509.09676)
|
| 158 |
+
- Static in-domain pool: [dataset_preprocessing.md](dataset_preprocessing.md)
|
| 159 |
+
- Dynamic training pool: [dynamic_dataset_preprocessing.md](dynamic_dataset_preprocessing.md)
|
code/doc/memory_mechanisms.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Memory Mechanisms
|
| 2 |
+
|
| 3 |
+
This note maps the paper's memory rows to the repository implementation and explains the modeling role of each family. Echo-Memory treats memory as a controlled intervention on what information from chunk 1 is stored and how chunk 2 reads it back during denoising.
|
| 4 |
+
|
| 5 |
+
## Modeling View
|
| 6 |
+
|
| 7 |
+
All rows use the same action-conditioned Wan DiT backbone and the same two-chunk training/evaluation setup:
|
| 8 |
+
|
| 9 |
+
1. **Context chunk:** clean history frames are encoded into latent/context tokens, optionally with matched camera RT actions.
|
| 10 |
+
2. **Target chunk:** noisy target latents are denoised while the selected memory mechanism exposes information from the context chunk.
|
| 11 |
+
3. **Read-out:** memory is injected through raw context concatenation, compressed context tokens, spatial memory tokens, or recurrent state-space modules attached to DiT blocks.
|
| 12 |
+
|
| 13 |
+
The ablations are designed to change only the memory pathway while keeping the backbone, action conditioning, resolution, chunk length, and training schedule aligned.
|
| 14 |
+
|
| 15 |
+
## Paper Rows
|
| 16 |
+
|
| 17 |
+
| Paper family | Paper row / repo name | What is stored or read | Main code path | Training entry |
|
| 18 |
+
| --- | --- | --- | --- | --- |
|
| 19 |
+
| Raw context | `context_k1`, `context_k5`, `context_k20` | Uncompressed retrieved context frames. `K=1` is the anchor/I2V floor; `K=5/20` are context-learning capacity rows. | `diffsynth/pipelines/wan_video_new.py` context latent path | `train/context_learning/run_pre_qkv_ctx{1,5,20}.sh` |
|
| 20 |
+
| Compression | `framepack_weight` | Context tokens are kept at the same length but temporally reweighted. | `diffsynth/models/memory/framepack_weight.py` | `train/memory_baselines_basic/run_ablation_framepack_weight_two_chunk.sh` |
|
| 21 |
+
| Compression | `framepack_len_r2`, `framepack_len_r4` | Context latents and matched RT actions are pooled along time. | `diffsynth/models/memory/framepack_length.py` | `train/memory_baselines_basic/run_ablation_framepack_len_r{2,4}_two_chunk.sh` |
|
| 22 |
+
| Compression | `framepack_hybrid_r2`, `framepack_hybrid_r4` | Length compression plus token reweighting. | `wan_video_new.py` + FramePack helpers | `train/memory_baselines_basic/run_ablation_framepack_hybrid_r*_weight_two_chunk.sh` |
|
| 23 |
+
| Token-grid | `spatial_mem` | Context tokens are time-averaged and summarized into learned grid tokens. This is the implementation behind the currently reported `spatial_mem` row; it does **not** reconstruct depth or 3D geometry. | `diffsynth/models/memory/spatial_grid_memory.py` | `train/memory_baselines_basic/run_spatial_memory_baseline.sh` |
|
| 24 |
+
| Token-grid | `spatial_inject_none`, `spatial_concat_text`, `spatial_cross_attn_readout` | Same token-grid storage, different read-out: withheld, text-KV concat, or dedicated cross-attention. | `spatial_grid_memory.py` read-out helpers | matching `run_ablation_spatial_*_two_chunk.sh` scripts |
|
| 25 |
+
| Geometry-grounded spatial | `geometry_spatial_mem` | A static scene is reconstructed outside the DiT using depth, intrinsics, extrinsics, and TSDF fusion. The fused point cloud is rendered along the target trajectory, VAE-encoded, and converted into conditioning tokens. | `diffsynth/models/memory/geometry_spatial_memory.py` | `train/memory_baselines_basic/run_geometry_spatial_memory_baseline.sh` |
|
| 26 |
+
| State-space | `block_wise_ssm` | Paper-aligned recurrent state attached to selected DiT blocks. Checkpoint keys contain `block_wise_ssm.*`. | `diffsynth/models/memory/block_wise_ssm.py` | `train/memory_baselines_basic/run_ablation_block_wise_ssm_two_chunk.sh` |
|
| 27 |
+
| State-space | `videossm_hybrid` | Legacy VideoSSM hybrid baseline: depthwise temporal-conv state-space-like module. Checkpoint keys contain `videossm_hybrid.*`. | `diffsynth/models/memory/videossm_hybrid.py` | `train/memory_baselines_basic/run_videossm_hybrid_baseline.sh` |
|
| 28 |
+
|
| 29 |
+
## Naming Rules
|
| 30 |
+
|
| 31 |
+
- Do not describe `SpatialGridMemory` or the existing `spatial_mem` results as the
|
| 32 |
+
geometry-grounded method from arXiv:2506.05284. It is a token-grid baseline.
|
| 33 |
+
- Use **Geometry-grounded Spatial Memory** only when the metadata supplies
|
| 34 |
+
rendered static geometry through `geometry_memory` (or a configured column).
|
| 35 |
+
The geometry extractor is the external reconstruction pipeline: depth and
|
| 36 |
+
cameras → TSDF-fused static point cloud → target-view renders. The model-side
|
| 37 |
+
encoder does not estimate depth itself.
|
| 38 |
+
- Use **Block-wise SSM** only for `--use_block_wise_ssm` / `BlockWiseStateSpaceMemory`.
|
| 39 |
+
- Use **VideoSSM hybrid** only for the legacy `--use_videossm_hybrid` / `HybridStateSpaceMemory` baseline.
|
| 40 |
+
- Use **Context learning** for raw-context capacity rows (`K=1/5/20`), not for compact memory modules.
|
| 41 |
+
- Keep checkpoint folder names stable; `env/memory_baseline_runtime.py` and `inference/unified_inference.py` infer memory profiles from those names.
|
code/docs/.nojekyll
ADDED
|
File without changes
|
code/docs/README.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Project page (local preview)
|
| 2 |
+
|
| 3 |
+
The **official** GitHub Pages site is deployed from the **`gh-pages`** branch (root `index.html` + `style.css`), not from this folder.
|
| 4 |
+
|
| 5 |
+
This `docs/` directory mirrors that pink-themed project page so you can preview locally:
|
| 6 |
+
|
| 7 |
+
```bash
|
| 8 |
+
cd docs
|
| 9 |
+
python -m http.server 18876 --bind 0.0.0.0
|
| 10 |
+
```
|
| 11 |
+
|
| 12 |
+
Then open `http://localhost:18876/` (with port forwarding if remote).
|
| 13 |
+
|
| 14 |
+
**Live site:** https://echo-team-joy-future-academy-jd.github.io/Echo-Memory/
|
| 15 |
+
|
| 16 |
+
Edit `docs/index.html`, `docs/style.css`, `docs/site.js`, and `docs/assets/`, then run `bash scripts/publish_gh_pages.sh` (or push `main` to trigger CI).
|
code/docs/assets/echo-memory-paper.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d7b4abd1c79f36e21bafff4d972a9562058b814fd40b0e9225309833f1adf11a
|
| 3 |
+
size 4038848
|
code/docs/assets/opendomain_revisit/README.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Open-Domain Revisit Sources
|
| 2 |
+
|
| 3 |
+
This folder contains the eight held-out first-frame sources used by the
|
| 4 |
+
open-domain revisit probe in the Echo-Memory paper.
|
| 5 |
+
|
| 6 |
+
Each image is treated as a first frame for a short controlled camera-return
|
| 7 |
+
probe. The default prompt used by `eval/v2/revisit_suite` is:
|
| 8 |
+
|
| 9 |
+
```text
|
| 10 |
+
A toy bear in the same static scene. Preserve the bear appearance and the scene layout after camera revisit.
|
| 11 |
+
```
|
| 12 |
+
|
| 13 |
+
To run the probe, point `OOD_DIR` here or use the default:
|
| 14 |
+
|
| 15 |
+
```bash
|
| 16 |
+
export WAN_BASE_MODEL=/path/to/Wan2.1-T2V-1.3B
|
| 17 |
+
PHASE=stage1 OOD_DIR=assets/opendomain_revisit \
|
| 18 |
+
bash eval/v2/revisit_suite/run_one_click_revisit_eval.sh
|
| 19 |
+
```
|
code/docs/assets/paper_cases/README.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Paper Case Visual Assets
|
| 2 |
+
|
| 3 |
+
This directory contains paper-facing figures copied into the release for direct README rendering.
|
| 4 |
+
|
| 5 |
+
- `figure_1_abs_framework.png`: paper teaser and workflow figure for the project landing page.
|
| 6 |
+
- `figure_2_mem_overview.png`: overview of the memory design matrix.
|
| 7 |
+
|
| 8 |
+
To generate new open-domain revisit videos and evidence frames for a checkpoint, use:
|
| 9 |
+
|
| 10 |
+
```bash
|
| 11 |
+
PHASE=stage1 OOD_DIR=assets/opendomain_revisit \
|
| 12 |
+
bash eval/v2/revisit_suite/run_one_click_revisit_eval.sh
|
| 13 |
+
```
|
code/docs/assets/readme_previews/context_k1_replay_gt.gif
ADDED
|
Git LFS Details
|
code/docs/assets/readme_previews/context_k5_replay_gt.gif
ADDED
|
Git LFS Details
|
code/docs/assets/readme_previews/dyn_context_k1_replay.gif
ADDED
|
Git LFS Details
|
code/docs/assets/readme_previews/dyn_context_k20_replay.gif
ADDED
|
Git LFS Details
|
code/docs/assets/readme_previews/dyn_context_k5_replay.gif
ADDED
|
Git LFS Details
|
code/docs/assets/readme_previews/dyn_spatial_memory_replay.gif
ADDED
|
Git LFS Details
|
code/docs/assets/readme_previews/dyn_ssm_blockwise_replay.gif
ADDED
|
Git LFS Details
|
code/docs/assets/readme_previews/dyn_ssm_legacy_replay.gif
ADDED
|
Git LFS Details
|
code/docs/assets/readme_previews/framepack_len_r4_replay_gt.gif
ADDED
|
Git LFS Details
|
code/docs/assets/readme_previews/spatial_memory_replay_gt.gif
ADDED
|
Git LFS Details
|
code/docs/assets/readme_previews/ssm_blockwise_replay_gt.gif
ADDED
|
Git LFS Details
|
code/docs/assets/readme_previews/ssm_legacy_replay_gt.gif
ADDED
|
Git LFS Details
|
code/docs/developer.html
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 6 |
+
<title>Echo-Memory Developer Guide</title>
|
| 7 |
+
<meta name="description" content="Echo-Memory development guide — workflows, eval, and Cursor vibe coding." />
|
| 8 |
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
| 9 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
| 10 |
+
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=IBM+Plex+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet" />
|
| 11 |
+
<link rel="stylesheet" href="style.css?v=dev-skills-v1" />
|
| 12 |
+
</head>
|
| 13 |
+
<body class="developer-page" data-title-key="dev.meta.title">
|
| 14 |
+
<div class="page-shell page-shell--doc">
|
| 15 |
+
<header class="top-nav top-nav--doc" role="banner">
|
| 16 |
+
<div class="container top-nav__inner top-nav__inner--doc">
|
| 17 |
+
<a class="brand" href="index.html">
|
| 18 |
+
<span class="brand-name">Echo-Memory</span>
|
| 19 |
+
<span class="brand-team">Echo Team</span>
|
| 20 |
+
</a>
|
| 21 |
+
<div class="nav-actions">
|
| 22 |
+
<a class="action-btn action-btn--ghost action-btn--compact" href="index.html" data-i18n="dev.back">← Back to project page</a>
|
| 23 |
+
<button type="button" class="lang-toggle" id="lang-toggle" data-i18n-attr="aria-label:lang.switch" aria-label="Switch language">
|
| 24 |
+
<span data-lang-current>EN</span> / <span data-lang-alt>中文</span>
|
| 25 |
+
</button>
|
| 26 |
+
</div>
|
| 27 |
+
</div>
|
| 28 |
+
</header>
|
| 29 |
+
|
| 30 |
+
<main class="container dev-main">
|
| 31 |
+
<p class="section-kicker" data-i18n="dev.kicker">Development · Cursor</p>
|
| 32 |
+
<h1 class="dev-title" data-i18n="dev.title">Developer Guide</h1>
|
| 33 |
+
<p class="section-lead" data-i18n-html="dev.subtitle"></p>
|
| 34 |
+
|
| 35 |
+
<article class="dev-section card">
|
| 36 |
+
<h2 data-i18n="dev.s1.title">1. What this guide is</h2>
|
| 37 |
+
<ul class="bullet-list" data-i18n-html="dev.s1.body"></ul>
|
| 38 |
+
</article>
|
| 39 |
+
|
| 40 |
+
<article class="dev-section card">
|
| 41 |
+
<h2 data-i18n="dev.s2.title">2. Environment & paths</h2>
|
| 42 |
+
<p data-i18n="dev.s2.intro">Set these before training or eval:</p>
|
| 43 |
+
<pre class="code-block"><code>export WAN_BASE_MODEL=/path/to/Wan2.1-T2V-1.3B
|
| 44 |
+
export DATASET_BASE_PATH=data/Context-as-Memory-Dataset
|
| 45 |
+
export PYTHONPATH=$PWD:${PYTHONPATH:-}
|
| 46 |
+
export OUTPUT_BASE_ROOT=$PWD/outputs</code></pre>
|
| 47 |
+
<ul class="bullet-list" data-i18n-html="dev.s2.body"></ul>
|
| 48 |
+
</article>
|
| 49 |
+
|
| 50 |
+
<article class="dev-section card">
|
| 51 |
+
<h2 data-i18n="dev.s3.title">3. Code map</h2>
|
| 52 |
+
<div class="dev-table-wrap" data-i18n-html="dev.s3.table"></div>
|
| 53 |
+
</article>
|
| 54 |
+
|
| 55 |
+
<article class="dev-section card">
|
| 56 |
+
<h2 data-i18n="dev.s4.title">4. Common workflows</h2>
|
| 57 |
+
<p data-i18n="dev.s4.trainLabel"><strong>Train one memory row</strong> (from repo root):</p>
|
| 58 |
+
<pre class="code-block"><code>bash train/memory_baselines_basic/run_spatial_memory_baseline.sh
|
| 59 |
+
bash train/context_learning/run_pre_qkv_ctx20.sh</code></pre>
|
| 60 |
+
<p data-i18n="dev.s4.evalLabel"><strong>Smoke eval with a HF checkpoint:</strong></p>
|
| 61 |
+
<pre class="code-block"><code>huggingface-cli download Echo-Team/Echo-Memory \
|
| 62 |
+
context_k1/epoch-0.safetensors --local-dir ./ckpts
|
| 63 |
+
export CKPT=./ckpts/context_k1/epoch-0.safetensors
|
| 64 |
+
bash eval/v2/run_static_consistency_loop_and_revisit.sh</code></pre>
|
| 65 |
+
<p class="section-note" data-i18n-html="dev.s4.note"></p>
|
| 66 |
+
</article>
|
| 67 |
+
|
| 68 |
+
<article class="dev-section card dev-section--accent">
|
| 69 |
+
<h2 data-i18n="dev.s5.title">5. Cursor vibe coding</h2>
|
| 70 |
+
<p data-i18n-html="dev.s5.intro"></p>
|
| 71 |
+
<p class="dev-skills-title" data-i18n="dev.s5.skillsTitle">Project skills</p>
|
| 72 |
+
<div class="dev-table-wrap" data-i18n-html="dev.s5.skills"></div>
|
| 73 |
+
<ul class="bullet-list" data-i18n-html="dev.s5.body"></ul>
|
| 74 |
+
<p data-i18n="dev.s5.promptLabel"><strong>Example prompt</strong></p>
|
| 75 |
+
<pre class="code-block code-block--prompt"><code data-i18n="dev.s5.prompt"></code></pre>
|
| 76 |
+
<p class="section-note" data-i18n-html="dev.s5.note"></p>
|
| 77 |
+
</article>
|
| 78 |
+
|
| 79 |
+
<article class="dev-section card">
|
| 80 |
+
<h2 data-i18n="dev.s6.title">6. Site & release (maintainers)</h2>
|
| 81 |
+
<p data-i18n-html="dev.s6.intro"></p>
|
| 82 |
+
<pre class="code-block"><code>bash scripts/publish_gh_pages.sh</code></pre>
|
| 83 |
+
<p data-i18n-html="dev.s6.body"></p>
|
| 84 |
+
</article>
|
| 85 |
+
|
| 86 |
+
<article class="dev-section card">
|
| 87 |
+
<h2 data-i18n="dev.s7.title">7. Checklist</h2>
|
| 88 |
+
<ul class="bullet-list">
|
| 89 |
+
<li data-i18n="dev.s7.l1">Smoke eval with one HF checkpoint before tagging a release.</li>
|
| 90 |
+
<li data-i18n-html="dev.s7.l2"></li>
|
| 91 |
+
<li data-i18n="dev.s7.l3">Public docs use Echo pool names — no internal paths or benchmark codenames.</li>
|
| 92 |
+
<li data-i18n-html="dev.s7.l4"></li>
|
| 93 |
+
</ul>
|
| 94 |
+
</article>
|
| 95 |
+
|
| 96 |
+
<p class="dev-repo-link" data-i18n-html="dev.footer"></p>
|
| 97 |
+
</main>
|
| 98 |
+
|
| 99 |
+
<footer class="footer">
|
| 100 |
+
<div class="container footer-row">
|
| 101 |
+
<p class="footer-copy" data-i18n="footer.copy">© Echo Team · Joy Future Academy, JD</p>
|
| 102 |
+
</div>
|
| 103 |
+
</footer>
|
| 104 |
+
</div>
|
| 105 |
+
|
| 106 |
+
<script src="i18n.js?v=dev-skills-v1"></script>
|
| 107 |
+
<script src="i18n-runtime.js?v=dev-skills-v1"></script>
|
| 108 |
+
</body>
|
| 109 |
+
</html>
|
code/docs/i18n-runtime.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
(function () {
|
| 2 |
+
"use strict";
|
| 3 |
+
|
| 4 |
+
var STORAGE_KEY = "echo-memory-lang";
|
| 5 |
+
var NAV_MAP = {
|
| 6 |
+
hero: "nav.home",
|
| 7 |
+
overview: "nav.overview",
|
| 8 |
+
design: "nav.design",
|
| 9 |
+
checkpoints: "nav.checkpoints",
|
| 10 |
+
evaluation: "nav.eval",
|
| 11 |
+
evidence: "nav.evidence",
|
| 12 |
+
findings: "nav.findings",
|
| 13 |
+
updates: "nav.updates",
|
| 14 |
+
bibtex: "nav.bibtex"
|
| 15 |
+
};
|
| 16 |
+
|
| 17 |
+
function detectDefaultLang() {
|
| 18 |
+
var saved = localStorage.getItem(STORAGE_KEY);
|
| 19 |
+
if (saved === "en" || saved === "zh") return saved;
|
| 20 |
+
return "en";
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
function dict(lang) {
|
| 24 |
+
return (window.ECHO_I18N && window.ECHO_I18N[lang]) || {};
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
function t(lang, key) {
|
| 28 |
+
var d = dict(lang);
|
| 29 |
+
return Object.prototype.hasOwnProperty.call(d, key) ? d[key] : "";
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
function applyLang(lang) {
|
| 33 |
+
var d = dict(lang);
|
| 34 |
+
if (!Object.keys(d).length) return;
|
| 35 |
+
|
| 36 |
+
document.documentElement.lang = lang === "zh" ? "zh-CN" : "en";
|
| 37 |
+
|
| 38 |
+
var descMeta = document.querySelector('meta[name="description"]');
|
| 39 |
+
var pageTitleKey = document.body.getAttribute("data-title-key");
|
| 40 |
+
if (pageTitleKey && d[pageTitleKey]) {
|
| 41 |
+
document.title = d[pageTitleKey];
|
| 42 |
+
} else if (d["meta.title"]) {
|
| 43 |
+
document.title = d["meta.title"];
|
| 44 |
+
}
|
| 45 |
+
if (descMeta) {
|
| 46 |
+
if (pageTitleKey === "dev.meta.title" && d["dev.meta.description"]) {
|
| 47 |
+
descMeta.setAttribute("content", d["dev.meta.description"]);
|
| 48 |
+
} else if (d["meta.description"]) {
|
| 49 |
+
descMeta.setAttribute("content", d["meta.description"]);
|
| 50 |
+
}
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
document.querySelectorAll("[data-i18n]").forEach(function (el) {
|
| 54 |
+
var key = el.getAttribute("data-i18n");
|
| 55 |
+
if (!key || !(key in d)) return;
|
| 56 |
+
el.textContent = d[key];
|
| 57 |
+
});
|
| 58 |
+
|
| 59 |
+
document.querySelectorAll("[data-i18n-html]").forEach(function (el) {
|
| 60 |
+
var key = el.getAttribute("data-i18n-html");
|
| 61 |
+
if (!key || !(key in d)) return;
|
| 62 |
+
el.innerHTML = d[key];
|
| 63 |
+
});
|
| 64 |
+
|
| 65 |
+
document.querySelectorAll("[data-i18n-attr]").forEach(function (el) {
|
| 66 |
+
el.getAttribute("data-i18n-attr").split(";").forEach(function (pair) {
|
| 67 |
+
var parts = pair.split(":");
|
| 68 |
+
if (parts.length !== 2) return;
|
| 69 |
+
var attr = parts[0].trim();
|
| 70 |
+
var key = parts[1].trim();
|
| 71 |
+
if (key in d) el.setAttribute(attr, d[key]);
|
| 72 |
+
});
|
| 73 |
+
});
|
| 74 |
+
|
| 75 |
+
document.querySelectorAll("[data-nav]").forEach(function (el) {
|
| 76 |
+
var navKey = NAV_MAP[el.getAttribute("data-nav")];
|
| 77 |
+
if (navKey && d[navKey]) el.textContent = d[navKey];
|
| 78 |
+
});
|
| 79 |
+
|
| 80 |
+
document.querySelectorAll(".slide-dots button[data-slide]").forEach(function (btn) {
|
| 81 |
+
var navKey = NAV_MAP[btn.getAttribute("data-slide")];
|
| 82 |
+
if (navKey && d[navKey]) {
|
| 83 |
+
btn.setAttribute("data-label", d[navKey]);
|
| 84 |
+
btn.setAttribute("aria-label", d[navKey]);
|
| 85 |
+
}
|
| 86 |
+
});
|
| 87 |
+
|
| 88 |
+
document.querySelectorAll(".qual-chip").forEach(function (chip, idx) {
|
| 89 |
+
var keys = ["evidence.chip1", "evidence.chip2", "evidence.chip3"];
|
| 90 |
+
if (keys[idx] && d[keys[idx]]) chip.textContent = d[keys[idx]];
|
| 91 |
+
});
|
| 92 |
+
|
| 93 |
+
var activeQual = document.querySelector(".qual-chip.is-active");
|
| 94 |
+
var qualCaption = document.querySelector("[data-qual-caption]");
|
| 95 |
+
if (activeQual && qualCaption && activeQual.dataset.captionKey && d[activeQual.dataset.captionKey]) {
|
| 96 |
+
qualCaption.innerHTML = d[activeQual.dataset.captionKey] + ' <span class="zoom-hint">' + (d["zoom.hint"] || "Click to expand") + "</span>";
|
| 97 |
+
} else if (qualCaption && d["evidence.cap1"] && document.querySelector(".qual-chip.is-active") === document.querySelector(".qual-chip")) {
|
| 98 |
+
qualCaption.innerHTML = d["evidence.cap1"];
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
var langCurrent = document.querySelector("[data-lang-current]");
|
| 102 |
+
var langAlt = document.querySelector("[data-lang-alt]");
|
| 103 |
+
if (langCurrent && d["lang.current"]) langCurrent.textContent = d["lang.current"];
|
| 104 |
+
if (langAlt && d["lang.toggle"]) langAlt.textContent = d["lang.toggle"];
|
| 105 |
+
|
| 106 |
+
var toggle = document.getElementById("lang-toggle");
|
| 107 |
+
if (toggle && d["lang.switch"]) toggle.setAttribute("aria-label", d["lang.switch"]);
|
| 108 |
+
|
| 109 |
+
document.querySelectorAll("[data-copy-target]").forEach(function (btn) {
|
| 110 |
+
if (!btn.classList.contains("is-copied") && d["bibtex.copy"]) {
|
| 111 |
+
btn.textContent = d["bibtex.copy"];
|
| 112 |
+
}
|
| 113 |
+
});
|
| 114 |
+
|
| 115 |
+
localStorage.setItem(STORAGE_KEY, lang);
|
| 116 |
+
document.dispatchEvent(new CustomEvent("echo-lang-change", { detail: { lang: lang } }));
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
window.EchoI18n = {
|
| 120 |
+
getLang: function () {
|
| 121 |
+
return localStorage.getItem(STORAGE_KEY) || detectDefaultLang();
|
| 122 |
+
},
|
| 123 |
+
setLang: applyLang,
|
| 124 |
+
t: function (key) {
|
| 125 |
+
return t(window.EchoI18n.getLang(), key);
|
| 126 |
+
}
|
| 127 |
+
};
|
| 128 |
+
|
| 129 |
+
document.addEventListener("DOMContentLoaded", function () {
|
| 130 |
+
var lang = detectDefaultLang();
|
| 131 |
+
applyLang(lang);
|
| 132 |
+
|
| 133 |
+
var toggle = document.getElementById("lang-toggle");
|
| 134 |
+
if (toggle) {
|
| 135 |
+
toggle.addEventListener("click", function () {
|
| 136 |
+
var next = window.EchoI18n.getLang() === "zh" ? "en" : "zh";
|
| 137 |
+
applyLang(next);
|
| 138 |
+
});
|
| 139 |
+
}
|
| 140 |
+
});
|
| 141 |
+
})();
|
code/docs/i18n.js
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Echo-Memory project page — EN / ZH strings */
|
| 2 |
+
window.ECHO_I18N = {
|
| 3 |
+
en: {
|
| 4 |
+
"meta.title": "Echo-Memory | Echo Team",
|
| 5 |
+
"meta.description": "Echo-Memory: A controlled study of memory mechanisms in action-conditioned world models.",
|
| 6 |
+
"nav.home": "Home",
|
| 7 |
+
"nav.overview": "Overview",
|
| 8 |
+
"nav.design": "Design",
|
| 9 |
+
"nav.checkpoints": "Ckpt",
|
| 10 |
+
"nav.eval": "Eval",
|
| 11 |
+
"nav.evidence": "Qual",
|
| 12 |
+
"nav.findings": "Results",
|
| 13 |
+
"nav.updates": "Updates",
|
| 14 |
+
"nav.bibtex": "BibTeX",
|
| 15 |
+
"nav.developer": "Dev Guide",
|
| 16 |
+
"nav.github": "GitHub",
|
| 17 |
+
"nav.menu": "Menu",
|
| 18 |
+
"lang.toggle": "中文",
|
| 19 |
+
"lang.current": "EN",
|
| 20 |
+
"lang.switch": "Switch language",
|
| 21 |
+
"hero.badge": "Echo Team · Joy Future Academy, JD · June 2026 · CC BY 4.0",
|
| 22 |
+
"hero.title.line2": "A Controlled Study of Memory in Action World Models",
|
| 23 |
+
"hero.subtitle": "When the camera leaves and returns, which memory keeps the <strong>same world</strong> instead of a plausible but different scene?",
|
| 24 |
+
"hero.cta.paper": "Paper",
|
| 25 |
+
"hero.cta.pdf": "PDF",
|
| 26 |
+
"hero.cta.ckpt": "Checkpoints",
|
| 27 |
+
"hero.cta.code": "Code",
|
| 28 |
+
"hero.metric.stars": "GitHub Stars",
|
| 29 |
+
"hero.metric.forks": "Forks",
|
| 30 |
+
"hero.note": "Controlled memory ablations on a shared <strong>Wan</strong> action-to-video stack — reproducible rows, evaluation scripts, and qualitative revisit panels.",
|
| 31 |
+
"hero.authors.summary": "Authors & affiliations",
|
| 32 |
+
"hero.affiliations": "HKU · Joy Future Academy, JD · CUHK · PKU · Fudan · Tsinghua · HKUST · UMich",
|
| 33 |
+
"overview.kicker": "01 · Overview",
|
| 34 |
+
"overview.title": "One backbone, one protocol — only memory changes.",
|
| 35 |
+
"overview.lead": "Echo-Memory holds the video backbone, training recipe, and data protocol fixed, and swaps only the memory module. The goal is to separate <strong>replay fidelity</strong> from <strong>return memory</strong> when the camera leaves and comes back to the same place.",
|
| 36 |
+
"overview.b1": "<strong>Shared stack</strong> — chunk-wise action-conditioned world generation on Wan.",
|
| 37 |
+
"overview.b2": "<strong>Controlled variable</strong> — Context, Compression, Spatial, or State-Space memory.",
|
| 38 |
+
"overview.b3": "<strong>Three probes</strong> — replay metrics, in-domain 180° loop, open-domain edited return.",
|
| 39 |
+
"overview.b4": "<strong>Release</strong> — ablation scripts, GT replay, revisit assets, and paper-aligned figures.",
|
| 40 |
+
"overview.fig.cap": "Controlled memory study over chunk-wise action-world generation. <span class=\"zoom-hint\">Click to expand</span>",
|
| 41 |
+
"overview.fig.alt": "Echo-Memory framework overview",
|
| 42 |
+
"design.kicker": "02 · Memory Design",
|
| 43 |
+
"design.title": "Context · Compression · Spatial · State-Space",
|
| 44 |
+
"design.lead": "All variants plug into the same write–read interface; we only change what is stored and how history is retrieved. A no-memory I2V floor re-generates from the first frame as a lower bound.",
|
| 45 |
+
"design.context.title": "Context",
|
| 46 |
+
"design.context.body": "Raw recent frames at K = 1, 5, or 20 chunks — tests whether longer windows alone stop drift.",
|
| 47 |
+
"design.compression.title": "Compression",
|
| 48 |
+
"design.compression.body": "Learned compact tokens at ratio r = 4 — history without growing raw-frame storage.",
|
| 49 |
+
"design.spatial.title": "Spatial",
|
| 50 |
+
"design.spatial.body": "Explicit spatial read/write state — targets layout, object pose, and viewpoint carry.",
|
| 51 |
+
"design.ssm.title": "State-Space",
|
| 52 |
+
"design.ssm.body": "Block-wise SSM updates — recurrent carry beyond short context windows on revisit.",
|
| 53 |
+
"design.fig.cap": "Four memory families under a shared write–read interface. <span class=\"zoom-hint\">Click to expand</span>",
|
| 54 |
+
"design.fig.alt": "Memory design matrix",
|
| 55 |
+
"ckpt.kicker": "03 · Checkpoints",
|
| 56 |
+
"ckpt.title": "Paper baselines on Hugging Face",
|
| 57 |
+
"ckpt.lead": "Wan 2.1 1.3B memory rows — <strong>epoch-0</strong>, <strong>30,000 steps</strong>, static in-domain pool. Released weights: <a href=\"https://huggingface.co/Echo-Team/Echo-Memory\" target=\"_blank\" rel=\"noopener noreferrer\"><strong>Echo-Team/Echo-Memory</strong></a>",
|
| 58 |
+
"ckpt.th.family": "Family",
|
| 59 |
+
"ckpt.th.row": "Paper row",
|
| 60 |
+
"ckpt.th.path": "HF path",
|
| 61 |
+
"ckpt.th.steps": "Steps",
|
| 62 |
+
"ckpt.label.download": "Download",
|
| 63 |
+
"ckpt.label.eval": "In-domain eval (Echo-Memory repo)",
|
| 64 |
+
"ckpt.note": "Keep the row folder in <code>CKPT</code> — <code>env/memory_baseline_runtime.py</code> infers memory flags from the path. Full index: <a href=\"https://github.com/Echo-Team-Joy-Future-Academy-JD/Echo-Memory/blob/main/doc/checkpoints.md\" target=\"_blank\" rel=\"noopener noreferrer\">doc/checkpoints.md</a>.",
|
| 65 |
+
"eval.kicker": "04 · Evaluation",
|
| 66 |
+
"eval.title": "Replay · In-domain revisit · Open-domain return",
|
| 67 |
+
"eval.lead": "Each branch asks a different question: Can the model reconstruct the past? Can it close a loop in-domain? After an edited first frame, does it return to the <strong>same world</strong>?",
|
| 68 |
+
"eval.replay.title": "Replay",
|
| 69 |
+
"eval.replay.body": "PSNR, SSIM, LPIPS on chunk-wise reconstruction — measures short-horizon pixel fidelity.",
|
| 70 |
+
"eval.indomain.title": "In-domain",
|
| 71 |
+
"eval.indomain.body": "180° trajectory loop closure with VLM-assisted scoring on held layouts.",
|
| 72 |
+
"eval.opendomain.title": "Open-domain",
|
| 73 |
+
"eval.opendomain.body": "Edited first frames and 45° return probes — stresses object identity and scene persistence.",
|
| 74 |
+
"eval.dynamic.title": "Dynamic SpatialVID",
|
| 75 |
+
"eval.dynamic.body": "Training and inference wrappers are public; the dynamic eval protocol is TODO.",
|
| 76 |
+
"eval.fig.cap": "Replay health vs. return memory under the same stack. <span class=\"zoom-hint\">Click to expand</span>",
|
| 77 |
+
"eval.fig.alt": "Three-branch evaluation summary",
|
| 78 |
+
"evidence.kicker": "05 · Qualitative Evidence",
|
| 79 |
+
"evidence.title": "Return probes expose identity drift.",
|
| 80 |
+
"evidence.lead": "Qualitative panels follow a simple diagnostic: <strong>first frame → leave the view → revisit tail</strong>. We compare whether memory restores the same object, pose, background, and camera geometry — not merely a plausible new scene.",
|
| 81 |
+
"evidence.chip1": "Memory Results",
|
| 82 |
+
"evidence.chip2": "Open-Domain Sweep",
|
| 83 |
+
"evidence.chip3": "Identity Anchors",
|
| 84 |
+
"evidence.cap1": "Representative memory comparisons across variants. <span class=\"zoom-hint\">Click to expand</span>",
|
| 85 |
+
"evidence.dynamic.note": "SpatialVID previews use one selected training scene and the same first frame, prompt, and GT camera trajectory for a 5-second first-chunk replay across all six rows.",
|
| 86 |
+
"findings.kicker": "06 · Main Conclusions",
|
| 87 |
+
"findings.title": "Replay quality ≠ memory quality.",
|
| 88 |
+
"findings.lead": "Replay metrics and return probes do not always agree — a model can look sharp on reconstruction yet fail when the camera returns. Rankings reorder once identity under revisit is measured.",
|
| 89 |
+
"findings.b1": "<strong>Raw context</strong> — more history helps open-domain return more than replay alone.",
|
| 90 |
+
"findings.b2": "<strong>Compression</strong> — compact tokens can preserve replay while losing identity on return.",
|
| 91 |
+
"findings.b3": "<strong>Spatial vs. SSM</strong> — explicit state and block-wise SSM trade off layout carry and long-horizon stability.",
|
| 92 |
+
"findings.b4": "<strong>Takeaway</strong> — treat replay as a health check, not the final memory benchmark.",
|
| 93 |
+
"findings.fig.cap": "Rank shift from replay to return — replay is not the final memory score. <span class=\"zoom-hint\">Click to expand</span>",
|
| 94 |
+
"updates.kicker": "07 · News & Roadmap",
|
| 95 |
+
"updates.title": "Release notes and next steps.",
|
| 96 |
+
"updates.news": "News",
|
| 97 |
+
"updates.roadmap": "Roadmap",
|
| 98 |
+
"updates.news0": "SpatialVID support added: dynamic training/inference recipes, 5-second first-chunk replay previews, and dynamic eval marked as TODO.",
|
| 99 |
+
"updates.news1": "Echo-Memory released: paper on <a href=\"https://arxiv.org/abs/2606.09803\" target=\"_blank\" rel=\"noopener noreferrer\">arXiv</a> (<a href=\"https://arxiv.org/pdf/2606.09803\" target=\"_blank\" rel=\"noopener noreferrer\">PDF</a>), project page, public code, replay/revisit eval assets, and baseline checkpoints on <a href=\"https://huggingface.co/Echo-Team/Echo-Memory\" target=\"_blank\" rel=\"noopener noreferrer\"><strong>Echo-Team/Echo-Memory</strong></a>.",
|
| 100 |
+
"updates.models": "Models",
|
| 101 |
+
"updates.eval": "Eval",
|
| 102 |
+
"updates.rm1": "<strong>Wan 2.1 1.3B</strong> backbone and training recipes",
|
| 103 |
+
"updates.rm2": "Four memory families — Context, Compression, Spatial, State-Space",
|
| 104 |
+
"updates.rm3": "<strong>Dynamic training pool</strong> — SpatialVID subset export & settings",
|
| 105 |
+
"updates.rm4": "<strong>Paper checkpoints</strong> — <a href=\"https://huggingface.co/Echo-Team/Echo-Memory\" target=\"_blank\" rel=\"noopener noreferrer\">Echo-Team/Echo-Memory</a>",
|
| 106 |
+
"updates.rm5": "<strong>Wan 2.2</strong> + multi-scale <strong>5B / 14B</strong>",
|
| 107 |
+
"updates.re1": "<strong>Dynamic eval</strong> beyond static replay/revisit",
|
| 108 |
+
"updates.re2": "More revisit probes and scoring presets",
|
| 109 |
+
"community.title": "Community",
|
| 110 |
+
"community.lead": "Join the Echo-Memory WeChat group for release updates, checkpoint questions, and maintainer coordination.",
|
| 111 |
+
"community.qr.alt": "Echo-Memory WeChat group QR code",
|
| 112 |
+
"community.qr.caption": "Echo-Memory 交流群 · scan to join (QR refreshes periodically)",
|
| 113 |
+
"bibtex.kicker": "08 · Citation",
|
| 114 |
+
"bibtex.title": "BibTeX",
|
| 115 |
+
"bibtex.lead": "Echo-Memory: A Controlled Study of Memory in Action World Models (June 2026). Licensed under <a href=\"https://creativecommons.org/licenses/by/4.0/\" target=\"_blank\" rel=\"noopener noreferrer\">CC BY 4.0</a>. Cite the arXiv preprint below.",
|
| 116 |
+
"bibtex.arxiv": "arXiv",
|
| 117 |
+
"bibtex.source": "Source",
|
| 118 |
+
"bibtex.doi": "DOI",
|
| 119 |
+
"bibtex.arxivid": "arXiv ID",
|
| 120 |
+
"bibtex.pdf": "PDF",
|
| 121 |
+
"bibtex.license": "License",
|
| 122 |
+
"bibtex.copy": "Copy",
|
| 123 |
+
"bibtex.copied": "Copied",
|
| 124 |
+
"bibtex.failed": "Failed",
|
| 125 |
+
"footer.copy": "© Echo Team · Joy Future Academy, JD",
|
| 126 |
+
"footer.pp": "Project Page",
|
| 127 |
+
"zoom.hint": "Click to expand",
|
| 128 |
+
"backtop": "Back to top",
|
| 129 |
+
"lightbox.close": "Close",
|
| 130 |
+
"lightbox.label": "Expanded figure",
|
| 131 |
+
"dev.meta.title": "Echo-Memory Developer Guide",
|
| 132 |
+
"dev.meta.description": "Echo-Memory development guide — workflows, eval, and Cursor vibe coding.",
|
| 133 |
+
"dev.kicker": "Development · Cursor",
|
| 134 |
+
"dev.title": "Developer Guide",
|
| 135 |
+
"dev.subtitle": "Hands-on coding, training, eval, and <strong>Cursor vibe coding</strong> for Echo-Memory.",
|
| 136 |
+
"dev.back": "← Back to project page",
|
| 137 |
+
"dev.s1.title": "1. What this guide is",
|
| 138 |
+
"dev.s1.body": "<li><strong>README</strong> — paper overview, quick start, checkpoints, community.</li><li><strong>This guide</strong> — workflows, project <strong>Cursor skills</strong>, Agent tips.</li><li><strong><code>doc/</code></strong> — dataset & checkpoint reference.</li><li><strong><code>.cursor/skills/</code></strong> — Agent skills for train / eval / release.</li>",
|
| 139 |
+
"dev.s2.title": "2. Environment & paths",
|
| 140 |
+
"dev.s2.intro": "Set these before training or eval:",
|
| 141 |
+
"dev.s2.body": "<li><strong>Static in-domain pool</strong> — default root above; see <code>doc/dataset_preprocessing.md</code>.</li><li><strong>Dynamic training pool</strong> — e.g. <code>data/dynamic-memory-dataset</code>; see <code>doc/dynamic_dataset_preprocessing.md</code>.</li><li><strong>Checkpoints</strong> — <a href=\"https://huggingface.co/Echo-Team/Echo-Memory\" target=\"_blank\" rel=\"noopener noreferrer\">Echo-Team/Echo-Memory</a>; index in <code>doc/checkpoints.md</code>.</li>",
|
| 142 |
+
"dev.s3.title": "3. Code map",
|
| 143 |
+
"dev.s3.table": "<table class=\"dev-table\"><thead><tr><th>Path</th><th>Role</th></tr></thead><tbody><tr><td><code>.cursor/skills/</code></td><td>Cursor Agent skills (train / eval / release)</td></tr><tr><td><code>train/memory_baselines_basic/</code></td><td>Spatial / SSM / compression ablations</td></tr><tr><td><code>train/context_learning/</code></td><td>Context K=1/5/20 recipes</td></tr><tr><td><code>eval/v2/</code></td><td>Replay, loop closure, open-domain revisit</td></tr><tr><td><code>env/memory_baseline_runtime.py</code></td><td>Checkpoint → memory profile</td></tr><tr><td><code>diffsynth/</code></td><td>Wan backbone & training stack</td></tr><tr><td><code>docs/</code></td><td>GitHub Pages (project + this guide)</td></tr></tbody></table>",
|
| 144 |
+
"dev.s4.title": "4. Common workflows",
|
| 145 |
+
"dev.s4.trainLabel": "Train one memory row (from repo root):",
|
| 146 |
+
"dev.s4.evalLabel": "Smoke eval with a HF checkpoint:",
|
| 147 |
+
"dev.s4.note": "Keep the row folder name in <code>CKPT</code> so runtime picks the correct memory profile.",
|
| 148 |
+
"dev.s5.title": "5. Cursor vibe coding",
|
| 149 |
+
"dev.s5.intro": "Use <strong>Cursor Agent</strong> (Composer) with the project skills below.",
|
| 150 |
+
"dev.s5.skillsTitle": "Project skills",
|
| 151 |
+
"dev.s5.skills": "<table class=\"dev-table\"><thead><tr><th>Skill</th><th>Use when</th></tr></thead><tbody><tr><td><code>echo-memory-agent</code></td><td>Scope prompts, rules, skill index</td></tr><tr><td><code>echo-memory-train</code></td><td>Memory baselines & context training</td></tr><tr><td><code>echo-memory-eval</code></td><td>Replay / revisit & HF quick checks</td></tr><tr><td><code>echo-memory-release</code></td><td>gh-pages, i18n, checkpoints doc</td></tr></tbody></table><p class=\"section-note\">Paths: <code>.cursor/skills/<name>/SKILL.md</code> — invoke in chat, e.g. <em>use echo-memory-eval to …</em></p>",
|
| 152 |
+
"dev.s5.body": "<li><strong>Scope prompts</strong> — memory family, script, eval branch (<em>replay / in-domain / open-domain</em>).</li><li><strong>Entry scripts</strong> — e.g. <code>run_spatial_memory_baseline.sh</code>, <code>eval/v2/run_basic_replay_gt.sh</code>.</li><li><strong>Rules</strong> — optional <code>.cursor/rules/echo-memory.mdc</code> for pool naming & public doc constraints.</li><li><strong>Ask mode</strong> — trace checkpoint mapping or read <code>diffsynth/</code> without edits.</li>",
|
| 153 |
+
"dev.s5.promptLabel": "Example prompt",
|
| 154 |
+
"dev.s5.prompt": "Add a quick check that downloads context_k1 from Echo-Team/Echo-Memory\nand runs eval/v2/run_basic_replay_gt.sh with the static in-domain pool.\n\nTrace env/memory_baseline_runtime.py spatial_mem → inject flags;\nsummarize in doc/checkpoints.md.",
|
| 155 |
+
"dev.s5.note": "<strong>Public repo hygiene:</strong> never commit upload bash, internal benchmark names, or machine paths. WeChat QR → project page & README only.",
|
| 156 |
+
"dev.s6.title": "6. Site & release (maintainers)",
|
| 157 |
+
"dev.s6.intro": "After editing <code>docs/index.html</code>, <code>docs/style.css</code>, or <code>docs/i18n*.js</code>:",
|
| 158 |
+
"dev.s6.body": "HF weights: Hugging Face UI or <code>hf upload</code> (maintainers only). Bilingual project page: <code>docs/i18n.js</code> + <code>docs/i18n-runtime.js</code>.",
|
| 159 |
+
"dev.s7.title": "7. Checklist",
|
| 160 |
+
"dev.s7.l1": "Smoke eval with one HF checkpoint before tagging a release.",
|
| 161 |
+
"dev.s7.l2": "Verify <code>doc/checkpoints.md</code> matches HF folder names.",
|
| 162 |
+
"dev.s7.l3": "Public docs use Echo pool names — no internal paths or benchmark codenames.",
|
| 163 |
+
"dev.s7.l4": "Run <code>publish_gh_pages.sh</code> after site changes; spot-check EN/中文 on the live page.",
|
| 164 |
+
"dev.footer": "Repo mirror: <a href=\"https://github.com/Echo-Team-Joy-Future-Academy-JD/Echo-Memory/blob/main/doc/DEVELOPER.md\">doc/DEVELOPER.md</a> · Community QR on <a href=\"index.html#updates\">project page</a>"
|
| 165 |
+
},
|
| 166 |
+
zh: {
|
| 167 |
+
"meta.title": "Echo-Memory | Echo Team",
|
| 168 |
+
"meta.description": "Echo-Memory:动作条件世界模型中记忆机制的对照研究。",
|
| 169 |
+
"nav.home": "首页",
|
| 170 |
+
"nav.overview": "概览",
|
| 171 |
+
"nav.design": "设计",
|
| 172 |
+
"nav.checkpoints": "权重",
|
| 173 |
+
"nav.eval": "评测",
|
| 174 |
+
"nav.evidence": "证据",
|
| 175 |
+
"nav.findings": "结论",
|
| 176 |
+
"nav.updates": "动态",
|
| 177 |
+
"nav.bibtex": "引用",
|
| 178 |
+
"nav.developer": "开发者手册",
|
| 179 |
+
"nav.github": "GitHub",
|
| 180 |
+
"nav.menu": "菜单",
|
| 181 |
+
"lang.toggle": "EN",
|
| 182 |
+
"lang.current": "中文",
|
| 183 |
+
"lang.switch": "切换语言",
|
| 184 |
+
"hero.badge": "Echo Team · Joy Future Academy, JD · 2026 年 6 月 · CC BY 4.0",
|
| 185 |
+
"hero.title.line2": "动作世界模型中记忆机制的对照研究",
|
| 186 |
+
"hero.subtitle": "当镜头离开再返回时,哪种记忆能让模型守住<strong>同一个世界</strong>,而不是生成一个看似合理却不同的场景?",
|
| 187 |
+
"hero.cta.paper": "论文",
|
| 188 |
+
"hero.cta.pdf": "PDF",
|
| 189 |
+
"hero.cta.ckpt": "模型权重",
|
| 190 |
+
"hero.cta.code": "代码",
|
| 191 |
+
"hero.metric.stars": "GitHub Stars",
|
| 192 |
+
"hero.metric.forks": "Forks",
|
| 193 |
+
"hero.note": "在共享 <strong>Wan</strong> 动作到视频栈上进行可控记忆消融——可复现实验行、评测脚本与 revisit 定性面板。",
|
| 194 |
+
"hero.authors.summary": "作者与单位",
|
| 195 |
+
"hero.affiliations": "HKU · Joy Future Academy, JD · CUHK · PKU · Fudan · Tsinghua · HKUST · UMich",
|
| 196 |
+
"overview.kicker": "01 · 概览",
|
| 197 |
+
"overview.title": "同一骨干、同一协议——只换记忆模块。",
|
| 198 |
+
"overview.lead": "Echo-Memory 固定视频骨干、训练配方与数据协议,仅替换记忆模块,以区分镜头离开再返回时的<strong>回放保真度</strong>与<strong>回归记忆</strong>。",
|
| 199 |
+
"overview.b1": "<strong>共享栈</strong> — 基于 Wan 的分块动作条件世界生成。",
|
| 200 |
+
"overview.b2": "<strong>对照变量</strong> — Context、Compression、Spatial 或 State-Space 记忆。",
|
| 201 |
+
"overview.b3": "<strong>三类探针</strong> — 回放指标、域内 180° 闭环、开放域编辑后回归。",
|
| 202 |
+
"overview.b4": "<strong>开源内容</strong> — 消融脚本、GT 回放、revisit 资产与论文对齐图表。",
|
| 203 |
+
"overview.fig.cap": "分块动作世界生成上的可控记忆研究。<span class=\"zoom-hint\">点击放大</span>",
|
| 204 |
+
"overview.fig.alt": "Echo-Memory 框架概览",
|
| 205 |
+
"design.kicker": "02 · 记忆设计",
|
| 206 |
+
"design.title": "Context · Compression · Spatial · State-Space",
|
| 207 |
+
"design.lead": "各变体接入同一 write–read 接口,仅改变存储内容与历史检索方式。无记忆 I2V 下限仅从首帧重生成。",
|
| 208 |
+
"design.context.title": "Context",
|
| 209 |
+
"design.context.body": "保留 K = 1 / 5 / 20 块原始帧 — 测试更长窗口是否足以抑制漂移。",
|
| 210 |
+
"design.compression.title": "Compression",
|
| 211 |
+
"design.compression.body": "比率 r = 4 的紧凑 token — 在不膨胀原始帧存储的情况下保留历史。",
|
| 212 |
+
"design.spatial.title": "Spatial",
|
| 213 |
+
"design.spatial.body": "显式空间读写状态 — 针对布局、物体位姿与视角携带。",
|
| 214 |
+
"design.ssm.title": "State-Space",
|
| 215 |
+
"design.ssm.body": "Block-wise SSM 更新 — 在 revisit 上超越短上下文窗口的递归携带。",
|
| 216 |
+
"design.fig.cap": "共享 write–read 接口下的四类记忆。<span class=\"zoom-hint\">点击放大</span>",
|
| 217 |
+
"design.fig.alt": "记忆设计矩阵",
|
| 218 |
+
"ckpt.kicker": "03 · 模型权重",
|
| 219 |
+
"ckpt.title": "Hugging Face 论文 baseline",
|
| 220 |
+
"ckpt.lead": "Wan 2.1 1.3B 记忆行 — <strong>epoch-0</strong>、<strong>30,000 steps</strong>、静态 in-domain 训练池。已发布权重:<a href=\"https://huggingface.co/Echo-Team/Echo-Memory\" target=\"_blank\" rel=\"noopener noreferrer\"><strong>Echo-Team/Echo-Memory</strong></a>",
|
| 221 |
+
"ckpt.th.family": "家族",
|
| 222 |
+
"ckpt.th.row": "论文行",
|
| 223 |
+
"ckpt.th.path": "HF 路径",
|
| 224 |
+
"ckpt.th.steps": "步数",
|
| 225 |
+
"ckpt.label.download": "下载",
|
| 226 |
+
"ckpt.label.eval": "域内评测(Echo-Memory 仓库)",
|
| 227 |
+
"ckpt.note": "请在 <code>CKPT</code> 中保留行目录名 — <code>env/memory_baseline_runtime.py</code> 会从路径推断 memory 配置。完整索引:<a href=\"https://github.com/Echo-Team-Joy-Future-Academy-JD/Echo-Memory/blob/main/doc/checkpoints.md\" target=\"_blank\" rel=\"noopener noreferrer\">doc/checkpoints.md</a>。",
|
| 228 |
+
"eval.kicker": "04 · 评测",
|
| 229 |
+
"eval.title": "回放 · 域内 revisit · 开放域回归",
|
| 230 |
+
"eval.lead": "三个分支回答不同问题:能否重建过去?域内能否闭环?编辑首帧后能否回到<strong>同一个世界</strong>?",
|
| 231 |
+
"eval.replay.title": "回放",
|
| 232 |
+
"eval.replay.body": "分块重建的 PSNR / SSIM / LPIPS — 衡量短程像素保真。",
|
| 233 |
+
"eval.indomain.title": "域内",
|
| 234 |
+
"eval.indomain.body": "180° 轨迹闭环与 VLM 辅助评分。",
|
| 235 |
+
"eval.opendomain.title": "开放域",
|
| 236 |
+
"eval.opendomain.body": "编辑首帧与 45° 回归探针 — 考察物体身份与场景持续性。",
|
| 237 |
+
"eval.dynamic.title": "动态 SpatialVID",
|
| 238 |
+
"eval.dynamic.body": "训练和推理 wrapper 已公开;动态评测协议暂列 TODO。",
|
| 239 |
+
"eval.fig.cap": "同一栈上的回放健康度 vs. 回归记忆。<span class=\"zoom-hint\">点击放大</span>",
|
| 240 |
+
"eval.fig.alt": "三分支评测概览",
|
| 241 |
+
"evidence.kicker": "05 · 定性证据",
|
| 242 |
+
"evidence.title": "回归探针暴露身份漂移。",
|
| 243 |
+
"evidence.lead": "定性面板遵循简单诊断:<strong>首帧 → 离开视角 → revisit 尾部</strong>。我们比较记忆是否恢复同一物体、位姿、背景与相机几何,而非仅生成合理的新场景。",
|
| 244 |
+
"evidence.chip1": "记忆结果",
|
| 245 |
+
"evidence.chip2": "开放域扫描",
|
| 246 |
+
"evidence.chip3": "身份锚点",
|
| 247 |
+
"evidence.cap1": "各变体的代表性记忆对比。<span class=\"zoom-hint\">点击放大</span>",
|
| 248 |
+
"evidence.dynamic.note": "SpatialVID 预览来自一个精选训练场景;六组使用相同首帧、prompt 与 GT 相机轨迹做 5 秒 first-chunk replay。",
|
| 249 |
+
"findings.kicker": "06 · 主要结论",
|
| 250 |
+
"findings.title": "回放质量 ≠ 记忆质量。",
|
| 251 |
+
"findings.lead": "回放指标与回归探针并不总一致 — 重建可以很 sharp,但镜头返回时仍可能失败。一旦测量 revisit 下的身份一致性,排名会重排。",
|
| 252 |
+
"findings.b1": "<strong>原始 Context</strong> — 更长历史对开放域回归的帮助大于单纯回放。",
|
| 253 |
+
"findings.b2": "<strong>Compression</strong> — 紧凑 token 可保回放但在回归时丢失身份。",
|
| 254 |
+
"findings.b3": "<strong>Spatial vs. SSM</strong> — 显式状态与 block-wise SSM 在布局携带与长程稳定性间权衡。",
|
| 255 |
+
"findings.b4": "<strong>要点</strong> — 将回放视为健康检查,而非最终记忆 benchmark。",
|
| 256 |
+
"findings.fig.cap": "从回放到回归的排名变化 — 回放不是最终记忆分数。<span class=\"zoom-hint\">点击放大</span>",
|
| 257 |
+
"updates.kicker": "07 · 新闻与路线图",
|
| 258 |
+
"updates.title": "发布说明与后续计划。",
|
| 259 |
+
"updates.news": "新闻",
|
| 260 |
+
"updates.roadmap": "路线图",
|
| 261 |
+
"updates.news0": "SpatialVID 支持已加入:动态训练/推理脚本、5 秒 first-chunk replay 预览,以及 dynamic eval TODO。",
|
| 262 |
+
"updates.news1": "Echo-Memory 发布:论文上线 <a href=\"https://arxiv.org/abs/2606.09803\" target=\"_blank\" rel=\"noopener noreferrer\">arXiv</a>(<a href=\"https://arxiv.org/pdf/2606.09803\" target=\"_blank\" rel=\"noopener noreferrer\">PDF</a>),同步发布项目页、公开代码、replay/revisit 评测资产,以及 <a href=\"https://huggingface.co/Echo-Team/Echo-Memory\" target=\"_blank\" rel=\"noopener noreferrer\"><strong>Echo-Team/Echo-Memory</strong></a> baseline 权重。",
|
| 263 |
+
"updates.models": "模型",
|
| 264 |
+
"updates.eval": "评测",
|
| 265 |
+
"updates.rm1": "<strong>Wan 2.1 1.3B</strong> 骨干与训练配方",
|
| 266 |
+
"updates.rm2": "四类记忆 — Context、Compression、Spatial、State-Space",
|
| 267 |
+
"updates.rm3": "<strong>Dynamic training pool</strong> — SpatialVID 子集导出与设置",
|
| 268 |
+
"updates.rm4": "<strong>论文权重</strong> — <a href=\"https://huggingface.co/Echo-Team/Echo-Memory\" target=\"_blank\" rel=\"noopener noreferrer\">Echo-Team/Echo-Memory</a>",
|
| 269 |
+
"updates.rm5": "<strong>Wan 2.2</strong> + 多尺度 <strong>5B / 14B</strong>",
|
| 270 |
+
"updates.re1": "静态 replay/revisit 之外的 <strong>动态评测</strong>",
|
| 271 |
+
"updates.re2": "更多 revisit 探针与评分预设",
|
| 272 |
+
"community.title": "社区交流",
|
| 273 |
+
"community.lead": "扫码加入 Echo-Memory 微信群,获取发布更新、权重使用与维护协调信息。",
|
| 274 |
+
"community.qr.alt": "Echo-Memory 微信群二维码",
|
| 275 |
+
"community.qr.caption": "Echo-Memory 交流群 · 扫码加入(二维码会定期更新)",
|
| 276 |
+
"bibtex.kicker": "08 · 引用",
|
| 277 |
+
"bibtex.title": "BibTeX",
|
| 278 |
+
"bibtex.lead": "Echo-Memory: A Controlled Study of Memory in Action World Models(2026 年 6 月)。许可:<a href=\"https://creativecommons.org/licenses/by/4.0/\" target=\"_blank\" rel=\"noopener noreferrer\">CC BY 4.0</a>。请使用下方 arXiv BibTeX 引用。",
|
| 279 |
+
"bibtex.arxiv": "arXiv",
|
| 280 |
+
"bibtex.source": "来源",
|
| 281 |
+
"bibtex.doi": "DOI",
|
| 282 |
+
"bibtex.arxivid": "arXiv ID",
|
| 283 |
+
"bibtex.pdf": "PDF",
|
| 284 |
+
"bibtex.license": "许可",
|
| 285 |
+
"bibtex.copy": "复制",
|
| 286 |
+
"bibtex.copied": "已复制",
|
| 287 |
+
"bibtex.failed": "失败",
|
| 288 |
+
"footer.copy": "© Echo Team · Joy Future Academy, JD",
|
| 289 |
+
"footer.pp": "项目页",
|
| 290 |
+
"zoom.hint": "点击放大",
|
| 291 |
+
"backtop": "回到顶部",
|
| 292 |
+
"lightbox.close": "关闭",
|
| 293 |
+
"lightbox.label": "放大图表",
|
| 294 |
+
"dev.meta.title": "Echo-Memory 开发者指南",
|
| 295 |
+
"dev.meta.description": "Echo-Memory 开发指南 — 工作流、评测与 Cursor 协作编程。",
|
| 296 |
+
"dev.kicker": "开发 · Cursor",
|
| 297 |
+
"dev.title": "开发者指南",
|
| 298 |
+
"dev.subtitle": "Echo-Memory 实战开发、训练评测与 <strong>Cursor 协作编程</strong>。",
|
| 299 |
+
"dev.back": "← 返回项目页",
|
| 300 |
+
"dev.s1.title": "1. 本指南定位",
|
| 301 |
+
"dev.s1.body": "<li><strong>README</strong> — 论文概览、快速上手、权重、社区。</li><li><strong>本页</strong> — 工作流、项目 <strong>Cursor skills</strong>、Agent 技巧。</li><li><strong><code>doc/</code></strong> — 数据集与权重参考。</li><li><strong><code>.cursor/skills/</code></strong> — 训练 / 评测 / 发布类 Agent 技能。</li>",
|
| 302 |
+
"dev.s2.title": "2. 环境与路径",
|
| 303 |
+
"dev.s2.intro": "训练或评测前设置:",
|
| 304 |
+
"dev.s2.body": "<li><strong>Static in-domain pool</strong> — 默认路径见上;详见 <code>doc/dataset_preprocessing.md</code>。</li><li><strong>Dynamic training pool</strong> — 如 <code>data/dynamic-memory-dataset</code>;详见 <code>doc/dynamic_dataset_preprocessing.md</code>。</li><li><strong>权重</strong> — <a href=\"https://huggingface.co/Echo-Team/Echo-Memory\" target=\"_blank\" rel=\"noopener noreferrer\">Echo-Team/Echo-Memory</a>;索引见 <code>doc/checkpoints.md</code>。</li>",
|
| 305 |
+
"dev.s3.title": "3. 代码地图",
|
| 306 |
+
"dev.s3.table": "<table class=\"dev-table\"><thead><tr><th>路径</th><th>作用</th></tr></thead><tbody><tr><td><code>.cursor/skills/</code></td><td>Cursor Agent 技能(训练 / 评测 / 发布)</td></tr><tr><td><code>train/memory_baselines_basic/</code></td><td>Spatial / SSM / 压缩消融</td></tr><tr><td><code>train/context_learning/</code></td><td>Context K=1/5/20 配方</td></tr><tr><td><code>eval/v2/</code></td><td>回放、闭环、开放域 revisit</td></tr><tr><td><code>env/memory_baseline_runtime.py</code></td><td>权重 → 记忆配置</td></tr><tr><td><code>diffsynth/</code></td><td>Wan 骨干与训练栈</td></tr><tr><td><code>docs/</code></td><td>GitHub Pages(项目页 + 本指南)</td></tr></tbody></table>",
|
| 307 |
+
"dev.s4.title": "4. 常用工作流",
|
| 308 |
+
"dev.s4.trainLabel": "训练一条 memory 行(仓库根目录):",
|
| 309 |
+
"dev.s4.evalLabel": "用 HF 权重做 quick eval:",
|
| 310 |
+
"dev.s4.note": "<code>CKPT</code> 路径需保留行目录名,以便 runtime 匹配记忆配置。",
|
| 311 |
+
"dev.s5.title": "5. Cursor 协作编程",
|
| 312 |
+
"dev.s5.intro": "使用 <strong>Cursor Agent</strong>(Composer)配合下方项目 skills。",
|
| 313 |
+
"dev.s5.skillsTitle": "项目 Skills",
|
| 314 |
+
"dev.s5.skills": "<table class=\"dev-table\"><thead><tr><th>Skill</th><th>适用场景</th></tr></thead><tbody><tr><td><code>echo-memory-agent</code></td><td>Prompt 范围、Rules、技能索引</td></tr><tr><td><code>echo-memory-train</code></td><td>Memory baseline 与 Context 训练</td></tr><tr><td><code>echo-memory-eval</code></td><td>回放 / revisit 与 HF quick check</td></tr><tr><td><code>echo-memory-release</code></td><td>gh-pages、i18n、权重文档</td></tr></tbody></table><p class=\"section-note\">路径:<code>.cursor/skills/<name>/SKILL.md</code> — 在对话中引用,如 <em>use echo-memory-eval 来 …</em></p>",
|
| 315 |
+
"dev.s5.body": "<li><strong>明确范围</strong> — memory 家族、脚本、评测分支(<em>replay / in-domain / open-domain</em>)。</li><li><strong>入口脚本</strong> — 如 <code>run_spatial_memory_baseline.sh</code>、<code>eval/v2/run_basic_replay_gt.sh</code>。</li><li><strong>Rules</strong> — 可选 <code>.cursor/rules/echo-memory.mdc</code> 约束池命名与公开文档。</li><li><strong>Ask 模式</strong> — 追踪 checkpoint 映射或阅读 <code>diffsynth/</code>,不改代码。</li>",
|
| 316 |
+
"dev.s5.promptLabel": "示例 Prompt",
|
| 317 |
+
"dev.s5.prompt": "添加 quick check:从 Echo-Team/Echo-Memory 下载 context_k1,\n用 static in-domain pool 跑 eval/v2/run_basic_replay_gt.sh。\n\n追踪 env/memory_baseline_runtime.py 如何把 spatial_mem\n权重映射到 inject 标志,并在 doc/checkpoints.md 摘要说明。",
|
| 318 |
+
"dev.s5.note": "<strong>公开仓库规范:</strong> 勿提交上传脚本、内部 benchmark 名、本机路径。微信群二维码仅在项目页与 README。",
|
| 319 |
+
"dev.s6.title": "6. 站点与发布(维护者)",
|
| 320 |
+
"dev.s6.intro": "修改 <code>docs/index.html</code>、<code>docs/style.css</code> 或 <code>docs/i18n*.js</code> 后:",
|
| 321 |
+
"dev.s6.body": "HF 权重:网页或 <code>hf upload</code> 更新(仅维护者)。项目页双语:<code>docs/i18n.js</code> + <code>docs/i18n-runtime.js</code>。",
|
| 322 |
+
"dev.s7.title": "7. 检查清单",
|
| 323 |
+
"dev.s7.l1": "发版前用至少一个 HF 权重跑 quick eval。",
|
| 324 |
+
"dev.s7.l2": "确认 <code>doc/checkpoints.md</code> 与 HF 目录名���致。",
|
| 325 |
+
"dev.s7.l3": "公开文档使用 Echo 池命名 — 无内部路径或 benchmark 代号。",
|
| 326 |
+
"dev.s7.l4": "改站点后运行 <code>publish_gh_pages.sh</code>,检查线上 EN/中文 切换。",
|
| 327 |
+
"dev.footer": "仓库副本:<a href=\"https://github.com/Echo-Team-Joy-Future-Academy-JD/Echo-Memory/blob/main/doc/DEVELOPER.md\">doc/DEVELOPER.md</a> · 社区二维码见 <a href=\"index.html#updates\">项目页</a>"
|
| 328 |
+
}
|
| 329 |
+
};
|
code/docs/index.html
ADDED
|
@@ -0,0 +1,644 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 6 |
+
<title>Echo-Memory | Echo Team</title>
|
| 7 |
+
<meta
|
| 8 |
+
name="description"
|
| 9 |
+
content="Echo-Memory: A controlled study of memory mechanisms in action-conditioned world models."
|
| 10 |
+
/>
|
| 11 |
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
| 12 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
| 13 |
+
<link
|
| 14 |
+
href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=IBM+Plex+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;600&display=swap"
|
| 15 |
+
rel="stylesheet"
|
| 16 |
+
/>
|
| 17 |
+
<link rel="stylesheet" href="style.css?v=dev-i18n-v1" />
|
| 18 |
+
</head>
|
| 19 |
+
<body>
|
| 20 |
+
<div class="scroll-progress" aria-hidden="true"><span class="scroll-progress__bar"></span></div>
|
| 21 |
+
<!-- arXiv: https://arxiv.org/abs/2606.09803 -->
|
| 22 |
+
<div class="page-shell">
|
| 23 |
+
<div class="ambient-bg" aria-hidden="true">
|
| 24 |
+
<div class="ambient-orb ambient-orb--rose"></div>
|
| 25 |
+
<div class="ambient-orb ambient-orb--blush"></div>
|
| 26 |
+
<div class="ambient-orb ambient-orb--pearl"></div>
|
| 27 |
+
<div class="ambient-shimmer"></div>
|
| 28 |
+
</div>
|
| 29 |
+
<header class="top-nav" role="banner">
|
| 30 |
+
<div class="container top-nav__inner">
|
| 31 |
+
<a class="brand" href="#hero">
|
| 32 |
+
<span class="brand-name">Echo-Memory</span>
|
| 33 |
+
<span class="brand-team">Echo Team</span>
|
| 34 |
+
</a>
|
| 35 |
+
<nav class="nav-links" aria-label="Sections">
|
| 36 |
+
<a class="nav-link" href="#hero" data-nav="hero">Home</a>
|
| 37 |
+
<a class="nav-link" href="#overview" data-nav="overview">Overview</a>
|
| 38 |
+
<a class="nav-link" href="#design" data-nav="design">Design</a>
|
| 39 |
+
<a class="nav-link" href="#checkpoints" data-nav="checkpoints">Checkpoints</a>
|
| 40 |
+
<a class="nav-link" href="#evaluation" data-nav="evaluation">Eval</a>
|
| 41 |
+
<a class="nav-link" href="#evidence" data-nav="evidence">Evidence</a>
|
| 42 |
+
<a class="nav-link" href="#findings" data-nav="findings">Results</a>
|
| 43 |
+
<a class="nav-link" href="#updates" data-nav="updates">Updates</a>
|
| 44 |
+
<a class="nav-link" href="#bibtex" data-nav="bibtex">BibTeX</a>
|
| 45 |
+
</nav>
|
| 46 |
+
<div class="nav-actions">
|
| 47 |
+
<a class="action-btn action-btn--ghost" href="developer.html" data-i18n="nav.developer">Developer Guide</a>
|
| 48 |
+
<button type="button" class="lang-toggle" id="lang-toggle" data-i18n-attr="aria-label:lang.switch" aria-label="Switch language">
|
| 49 |
+
<span data-lang-current>EN</span> / <span data-lang-alt>中文</span>
|
| 50 |
+
</button>
|
| 51 |
+
<a
|
| 52 |
+
class="action-btn"
|
| 53 |
+
href="https://github.com/Echo-Team-Joy-Future-Academy-JD/Echo-Memory"
|
| 54 |
+
target="_blank"
|
| 55 |
+
rel="noopener noreferrer"
|
| 56 |
+
data-i18n="nav.github"
|
| 57 |
+
>GitHub</a>
|
| 58 |
+
<button
|
| 59 |
+
type="button"
|
| 60 |
+
class="nav-toggle"
|
| 61 |
+
aria-expanded="false"
|
| 62 |
+
aria-controls="nav-menu-mobile"
|
| 63 |
+
id="nav-toggle"
|
| 64 |
+
>
|
| 65 |
+
<span class="sr-only">Menu</span>
|
| 66 |
+
<span></span><span></span><span></span>
|
| 67 |
+
</button>
|
| 68 |
+
</div>
|
| 69 |
+
</div>
|
| 70 |
+
<div class="nav-mobile container" id="nav-menu-mobile" hidden>
|
| 71 |
+
<a class="nav-link" href="#hero" data-nav="hero">Home</a>
|
| 72 |
+
<a class="nav-link" href="#overview" data-nav="overview">Overview</a>
|
| 73 |
+
<a class="nav-link" href="#design" data-nav="design">Design</a>
|
| 74 |
+
<a class="nav-link" href="#checkpoints" data-nav="checkpoints">Checkpoints</a>
|
| 75 |
+
<a class="nav-link" href="#evaluation" data-nav="evaluation">Eval</a>
|
| 76 |
+
<a class="nav-link" href="#evidence" data-nav="evidence">Evidence</a>
|
| 77 |
+
<a class="nav-link" href="#findings" data-nav="findings">Results</a>
|
| 78 |
+
<a class="nav-link" href="#updates" data-nav="updates">Updates</a>
|
| 79 |
+
<a class="nav-link" href="developer.html" data-i18n="nav.developer">Developer Guide</a>
|
| 80 |
+
<a class="nav-link" href="#bibtex" data-nav="bibtex">BibTeX</a>
|
| 81 |
+
</div>
|
| 82 |
+
</header>
|
| 83 |
+
|
| 84 |
+
<aside class="slide-dots" aria-label="Section navigation">
|
| 85 |
+
<button type="button" class="is-active" data-slide="hero" data-label="Home" aria-label="Home"></button>
|
| 86 |
+
<button type="button" data-slide="overview" data-label="Overview" aria-label="Overview"></button>
|
| 87 |
+
<button type="button" data-slide="design" data-label="Design" aria-label="Design"></button>
|
| 88 |
+
<button type="button" data-slide="checkpoints" data-label="Checkpoints" aria-label="Checkpoints"></button>
|
| 89 |
+
<button type="button" data-slide="evaluation" data-label="Eval" aria-label="Evaluation"></button>
|
| 90 |
+
<button type="button" data-slide="evidence" data-label="Evidence" aria-label="Evidence"></button>
|
| 91 |
+
<button type="button" data-slide="findings" data-label="Results" aria-label="Results"></button>
|
| 92 |
+
<button type="button" data-slide="updates" data-label="Updates" aria-label="Updates"></button>
|
| 93 |
+
<button type="button" data-slide="bibtex" data-label="BibTeX" aria-label="BibTeX"></button>
|
| 94 |
+
</aside>
|
| 95 |
+
|
| 96 |
+
<button type="button" class="back-top" id="back-top" data-i18n-attr="aria-label:backtop" aria-label="Back to top" hidden>
|
| 97 |
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
| 98 |
+
<path d="M12 19V5M12 5l-6 6M12 5l6 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
| 99 |
+
</svg>
|
| 100 |
+
</button>
|
| 101 |
+
|
| 102 |
+
<div class="lightbox" id="lightbox" hidden role="dialog" aria-modal="true" data-i18n-attr="aria-label:lightbox.label" aria-label="Expanded figure">
|
| 103 |
+
<button type="button" class="lightbox__close" data-i18n-attr="aria-label:lightbox.close" aria-label="Close">×</button>
|
| 104 |
+
<figure class="lightbox__inner">
|
| 105 |
+
<img src="" alt="" id="lightbox-img" />
|
| 106 |
+
<figcaption id="lightbox-caption"></figcaption>
|
| 107 |
+
</figure>
|
| 108 |
+
</div>
|
| 109 |
+
|
| 110 |
+
<main id="main-content">
|
| 111 |
+
<section id="hero" class="section section--compact reveal" data-section="hero">
|
| 112 |
+
<div class="container hero hero--single">
|
| 113 |
+
<div>
|
| 114 |
+
<div class="hero-badge" data-i18n="hero.badge">Echo Team · Joy Future Academy, JD · June 2026 · CC BY 4.0</div>
|
| 115 |
+
<h1 class="hero-title">
|
| 116 |
+
<span class="hero-highlight">Echo-Memory</span>
|
| 117 |
+
<br />
|
| 118 |
+
<span data-i18n="hero.title.line2">A Controlled Study of Memory in Action World Models</span>
|
| 119 |
+
</h1>
|
| 120 |
+
<p class="hero-subtitle" data-i18n-html="hero.subtitle">
|
| 121 |
+
When the camera leaves and returns, which memory keeps the <strong>same world</strong>
|
| 122 |
+
instead of a plausible but different scene?
|
| 123 |
+
</p>
|
| 124 |
+
<div class="hero-cta-group">
|
| 125 |
+
<a
|
| 126 |
+
class="cta cta--primary"
|
| 127 |
+
href="https://arxiv.org/abs/2606.09803"
|
| 128 |
+
target="_blank"
|
| 129 |
+
rel="noopener noreferrer"
|
| 130 |
+
data-i18n="hero.cta.paper"
|
| 131 |
+
>Paper</a>
|
| 132 |
+
<a
|
| 133 |
+
class="cta cta--secondary"
|
| 134 |
+
href="https://arxiv.org/pdf/2606.09803"
|
| 135 |
+
target="_blank"
|
| 136 |
+
rel="noopener noreferrer"
|
| 137 |
+
data-i18n="hero.cta.pdf"
|
| 138 |
+
>PDF</a>
|
| 139 |
+
<a
|
| 140 |
+
class="cta cta--secondary"
|
| 141 |
+
href="https://huggingface.co/Echo-Team/Echo-Memory"
|
| 142 |
+
target="_blank"
|
| 143 |
+
rel="noopener noreferrer"
|
| 144 |
+
data-i18n="hero.cta.ckpt"
|
| 145 |
+
>Checkpoints</a>
|
| 146 |
+
<a
|
| 147 |
+
class="cta cta--secondary"
|
| 148 |
+
href="https://github.com/Echo-Team-Joy-Future-Academy-JD/Echo-Memory"
|
| 149 |
+
target="_blank"
|
| 150 |
+
rel="noopener noreferrer"
|
| 151 |
+
data-i18n="hero.cta.code"
|
| 152 |
+
>Code</a>
|
| 153 |
+
</div>
|
| 154 |
+
|
| 155 |
+
<div class="metrics-grid metrics-grid--duo">
|
| 156 |
+
<a
|
| 157 |
+
class="metric-card"
|
| 158 |
+
href="https://github.com/Echo-Team-Joy-Future-Academy-JD/Echo-Memory/stargazers"
|
| 159 |
+
target="_blank"
|
| 160 |
+
rel="noopener noreferrer"
|
| 161 |
+
data-badge-url="https://img.shields.io/github/stars/Echo-Team-Joy-Future-Academy-JD/Echo-Memory.json"
|
| 162 |
+
>
|
| 163 |
+
<span class="metric-label" data-i18n="hero.metric.stars">GitHub Stars</span>
|
| 164 |
+
<strong class="metric-value" data-metric-value data-fallback="—">—</strong>
|
| 165 |
+
</a>
|
| 166 |
+
<a
|
| 167 |
+
class="metric-card"
|
| 168 |
+
href="https://github.com/Echo-Team-Joy-Future-Academy-JD/Echo-Memory/network/members"
|
| 169 |
+
target="_blank"
|
| 170 |
+
rel="noopener noreferrer"
|
| 171 |
+
data-badge-url="https://img.shields.io/github/forks/Echo-Team-Joy-Future-Academy-JD/Echo-Memory.json"
|
| 172 |
+
>
|
| 173 |
+
<span class="metric-label" data-i18n="hero.metric.forks">Forks</span>
|
| 174 |
+
<strong class="metric-value" data-metric-value data-fallback="—">—</strong>
|
| 175 |
+
</a>
|
| 176 |
+
</div>
|
| 177 |
+
|
| 178 |
+
<p class="hero-note" data-i18n-html="hero.note">
|
| 179 |
+
Controlled memory ablations on a shared <strong>Wan</strong> action-to-video stack —
|
| 180 |
+
reproducible rows, evaluation scripts, and qualitative revisit panels.
|
| 181 |
+
</p>
|
| 182 |
+
|
| 183 |
+
<details class="authors-fold">
|
| 184 |
+
<summary data-i18n="hero.authors.summary">Authors & affiliations</summary>
|
| 185 |
+
<p class="authors">
|
| 186 |
+
Wayne King, Zeyue Xue, Yuxuan Bian, Jie Huang, Haoran Li, Yaowei Li,
|
| 187 |
+
Yaofeng Su, Yuming Li, Haoyu Wang, Shiyi Zhang, Songchun Zhang,
|
| 188 |
+
Yuwei Niu, Sihan Xu, Junhao Zhuang, Haoyang Huang, Nan Duan
|
| 189 |
+
</p>
|
| 190 |
+
<p class="affiliations" data-i18n="hero.affiliations">
|
| 191 |
+
HKU · Joy Future Academy, JD · CUHK · PKU · Fudan · Tsinghua · HKUST · UMich
|
| 192 |
+
</p>
|
| 193 |
+
</details>
|
| 194 |
+
</div>
|
| 195 |
+
</div>
|
| 196 |
+
</section>
|
| 197 |
+
|
| 198 |
+
<section id="overview" class="section reveal" data-section="overview">
|
| 199 |
+
<div class="container">
|
| 200 |
+
<p class="section-kicker" data-i18n="overview.kicker">01 · Overview</p>
|
| 201 |
+
<h2 class="section-title" data-i18n="overview.title">One backbone, one protocol — only memory changes.</h2>
|
| 202 |
+
<p class="section-lead" data-i18n-html="overview.lead">
|
| 203 |
+
Echo-Memory holds the video backbone, training recipe, and data protocol fixed, and
|
| 204 |
+
swaps only the memory module. The goal is to separate <strong>replay fidelity</strong>
|
| 205 |
+
from <strong>return memory</strong> when the camera leaves and comes back to the same place.
|
| 206 |
+
</p>
|
| 207 |
+
<ul class="bullet-list">
|
| 208 |
+
<li data-i18n-html="overview.b1"><strong>Shared stack</strong> — chunk-wise action-conditioned world generation on Wan.</li>
|
| 209 |
+
<li data-i18n-html="overview.b2"><strong>Controlled variable</strong> — Context, Compression, Spatial, or State-Space memory.</li>
|
| 210 |
+
<li data-i18n-html="overview.b3"><strong>Three probes</strong> — replay metrics, in-domain 180° loop, open-domain edited return.</li>
|
| 211 |
+
<li data-i18n-html="overview.b4"><strong>Release</strong> — ablation scripts, GT replay, revisit assets, and paper-aligned figures.</li>
|
| 212 |
+
</ul>
|
| 213 |
+
<figure class="section-figure" data-zoomable>
|
| 214 |
+
<img src="assets/figure_1_abs_framework.png?v=fig1-crop" data-i18n-attr="alt:overview.fig.alt" alt="Echo-Memory framework overview" />
|
| 215 |
+
<figcaption data-i18n-html="overview.fig.cap">Controlled memory study over chunk-wise action-world generation. <span class="zoom-hint">Click to expand</span></figcaption>
|
| 216 |
+
</figure>
|
| 217 |
+
</div>
|
| 218 |
+
</section>
|
| 219 |
+
|
| 220 |
+
<section id="design" class="section reveal" data-section="design">
|
| 221 |
+
<div class="container">
|
| 222 |
+
<p class="section-kicker" data-i18n="design.kicker">02 · Memory Design</p>
|
| 223 |
+
<h2 class="section-title" data-i18n="design.title">Context · Compression · Spatial · State-Space</h2>
|
| 224 |
+
<p class="section-lead" data-i18n="design.lead">
|
| 225 |
+
All variants plug into the same write–read interface; we only change what is stored and how
|
| 226 |
+
history is retrieved. A no-memory I2V floor re-generates from the first frame as a lower bound.
|
| 227 |
+
</p>
|
| 228 |
+
<div class="feature-grid">
|
| 229 |
+
<article class="card card--accent" data-accent="context">
|
| 230 |
+
<div class="card-icon" aria-hidden="true">
|
| 231 |
+
<svg viewBox="0 0 24 24" fill="none"><rect x="3" y="5" width="18" height="14" rx="2" stroke="currentColor" stroke-width="1.6"/><path d="M7 9h10M7 13h6" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>
|
| 232 |
+
</div>
|
| 233 |
+
<h3 data-i18n="design.context.title">Context</h3>
|
| 234 |
+
<p data-i18n="design.context.body">Raw recent frames at K = 1, 5, or 20 chunks — tests whether longer windows alone stop drift.</p>
|
| 235 |
+
</article>
|
| 236 |
+
<article class="card card--accent" data-accent="compression">
|
| 237 |
+
<div class="card-icon" aria-hidden="true">
|
| 238 |
+
<svg viewBox="0 0 24 24" fill="none"><path d="M12 3l8 4.5v9L12 21l-8-4.5v-9L12 3z" stroke="currentColor" stroke-width="1.6"/><path d="M12 12l8-4.5M12 12v9M12 12L4 7.5" stroke="currentColor" stroke-width="1.6"/></svg>
|
| 239 |
+
</div>
|
| 240 |
+
<h3 data-i18n="design.compression.title">Compression</h3>
|
| 241 |
+
<p data-i18n="design.compression.body">Learned compact tokens at ratio r = 4 — history without growing raw-frame storage.</p>
|
| 242 |
+
</article>
|
| 243 |
+
<article class="card card--accent" data-accent="spatial">
|
| 244 |
+
<div class="card-icon" aria-hidden="true">
|
| 245 |
+
<svg viewBox="0 0 24 24" fill="none"><path d="M4 8l8-4 8 4v8l-8 4-8-4V8z" stroke="currentColor" stroke-width="1.6"/><path d="M12 4v16M4 8l8 4 8-4" stroke="currentColor" stroke-width="1.6"/></svg>
|
| 246 |
+
</div>
|
| 247 |
+
<h3 data-i18n="design.spatial.title">Spatial</h3>
|
| 248 |
+
<p data-i18n="design.spatial.body">Explicit spatial read/write state — targets layout, object pose, and viewpoint carry.</p>
|
| 249 |
+
</article>
|
| 250 |
+
<article class="card card--accent" data-accent="ssm">
|
| 251 |
+
<div class="card-icon" aria-hidden="true">
|
| 252 |
+
<svg viewBox="0 0 24 24" fill="none"><path d="M5 12a7 7 0 0114 0" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/><path d="M8 12h8M16 9l2 3-2 3M8 9l-2 3 2 3" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
| 253 |
+
</div>
|
| 254 |
+
<h3 data-i18n="design.ssm.title">State-Space</h3>
|
| 255 |
+
<p data-i18n="design.ssm.body">Block-wise SSM updates — recurrent carry beyond short context windows on revisit.</p>
|
| 256 |
+
</article>
|
| 257 |
+
</div>
|
| 258 |
+
<figure class="section-figure" data-zoomable>
|
| 259 |
+
<img src="assets/figure_2_mem_overview.png" data-i18n-attr="alt:design.fig.alt" alt="Memory design matrix" />
|
| 260 |
+
<figcaption data-i18n-html="design.fig.cap">Four memory families under a shared write–read interface. <span class="zoom-hint">Click to expand</span></figcaption>
|
| 261 |
+
</figure>
|
| 262 |
+
</div>
|
| 263 |
+
</section>
|
| 264 |
+
|
| 265 |
+
<section id="checkpoints" class="section reveal" data-section="checkpoints">
|
| 266 |
+
<div class="container">
|
| 267 |
+
<p class="section-kicker" data-i18n="ckpt.kicker">03 · Checkpoints</p>
|
| 268 |
+
<h2 class="section-title" data-i18n="ckpt.title">Paper baselines on Hugging Face</h2>
|
| 269 |
+
<p class="section-lead" data-i18n-html="ckpt.lead">
|
| 270 |
+
Wan 2.1 1.3B memory rows — <strong>epoch-0</strong>, <strong>30,000 steps</strong>, static in-domain pool.
|
| 271 |
+
Released weights:
|
| 272 |
+
<a href="https://huggingface.co/Echo-Team/Echo-Memory" target="_blank" rel="noopener noreferrer"><strong>Echo-Team/Echo-Memory</strong></a>
|
| 273 |
+
</p>
|
| 274 |
+
<div class="table-wrap">
|
| 275 |
+
<table class="ckpt-table">
|
| 276 |
+
<thead>
|
| 277 |
+
<tr>
|
| 278 |
+
<th data-i18n="ckpt.th.family">Family</th>
|
| 279 |
+
<th data-i18n="ckpt.th.row">Paper row</th>
|
| 280 |
+
<th data-i18n="ckpt.th.path">HF path</th>
|
| 281 |
+
<th data-i18n="ckpt.th.steps">Steps</th>
|
| 282 |
+
</tr>
|
| 283 |
+
</thead>
|
| 284 |
+
<tbody>
|
| 285 |
+
<tr><td>Raw context</td><td>Context K=1</td><td><code>context_k1/epoch-0.safetensors</code></td><td>30,000</td></tr>
|
| 286 |
+
<tr><td>Raw context</td><td>Context K=20</td><td>TODO</td><td>TODO</td></tr>
|
| 287 |
+
<tr><td>Spatial</td><td>Spatial Memory</td><td>TODO</td><td>TODO</td></tr>
|
| 288 |
+
<tr><td>State-space</td><td>Block-wise SSM</td><td>TODO</td><td>TODO</td></tr>
|
| 289 |
+
<tr><td>State-space</td><td>Legacy Hybrid</td><td>TODO</td><td>TODO</td></tr>
|
| 290 |
+
<tr><td>Spatial</td><td>concat text (abl.)</td><td>TODO</td><td>TODO</td></tr>
|
| 291 |
+
<tr><td>Spatial</td><td>inject none (abl.)</td><td>TODO</td><td>TODO</td></tr>
|
| 292 |
+
<tr><td>Spatial</td><td>cross-attn t32 (abl.)</td><td>TODO</td><td>TODO</td></tr>
|
| 293 |
+
<tr><td>State-space</td><td>SSM ctx1/e4/h21</td><td>TODO</td><td>TODO</td></tr>
|
| 294 |
+
<tr><td>State-space</td><td>SSM ctx5/e1/h21</td><td>TODO</td><td>TODO</td></tr>
|
| 295 |
+
<tr><td>State-space</td><td>SSM ctx5/e4/h81</td><td>TODO</td><td>TODO</td></tr>
|
| 296 |
+
</tbody>
|
| 297 |
+
</table>
|
| 298 |
+
</div>
|
| 299 |
+
<div class="code-block-wrap">
|
| 300 |
+
<p class="code-block-label" data-i18n="ckpt.label.download">Download</p>
|
| 301 |
+
<pre class="code-block"><code>huggingface-cli download Echo-Team/Echo-Memory context_k1/epoch-0.safetensors --local-dir ./ckpts</code></pre>
|
| 302 |
+
</div>
|
| 303 |
+
<div class="code-block-wrap">
|
| 304 |
+
<p class="code-block-label" data-i18n="ckpt.label.eval">In-domain eval (Echo-Memory repo)</p>
|
| 305 |
+
<pre class="code-block"><code>export WAN_BASE_MODEL=/path/to/Wan2.1-T2V-1.3B
|
| 306 |
+
export DATASET_BASE_PATH=data/Context-as-Memory-Dataset
|
| 307 |
+
export CKPT=./ckpts/context_k1/epoch-0.safetensors
|
| 308 |
+
bash eval/v2/run_static_consistency_loop_and_revisit.sh</code></pre>
|
| 309 |
+
</div>
|
| 310 |
+
<p class="section-note" data-i18n-html="ckpt.note">
|
| 311 |
+
Keep the row folder in <code>CKPT</code> — <code>env/memory_baseline_runtime.py</code> infers memory flags from the path.
|
| 312 |
+
Full index:
|
| 313 |
+
<a href="https://github.com/Echo-Team-Joy-Future-Academy-JD/Echo-Memory/blob/main/doc/checkpoints.md" target="_blank" rel="noopener noreferrer">doc/checkpoints.md</a>.
|
| 314 |
+
</p>
|
| 315 |
+
</div>
|
| 316 |
+
</section>
|
| 317 |
+
|
| 318 |
+
<section id="evaluation" class="section section--center reveal" data-section="evaluation">
|
| 319 |
+
<div class="container">
|
| 320 |
+
<p class="section-kicker" data-i18n="eval.kicker">04 · Evaluation</p>
|
| 321 |
+
<h2 class="section-title" data-i18n="eval.title">Replay · In-domain revisit · Open-domain return</h2>
|
| 322 |
+
<p class="section-lead" data-i18n-html="eval.lead">
|
| 323 |
+
Each branch asks a different question: Can the model reconstruct the past? Can it close a loop
|
| 324 |
+
in-domain? After an edited first frame, does it return to the <strong>same world</strong>?
|
| 325 |
+
</p>
|
| 326 |
+
<div class="feature-grid feature-grid--three">
|
| 327 |
+
<article class="card">
|
| 328 |
+
<h3 data-i18n="eval.replay.title">Replay</h3>
|
| 329 |
+
<p data-i18n="eval.replay.body">PSNR, SSIM, LPIPS on chunk-wise reconstruction — measures short-horizon pixel fidelity.</p>
|
| 330 |
+
</article>
|
| 331 |
+
<article class="card">
|
| 332 |
+
<h3 data-i18n="eval.indomain.title">In-domain</h3>
|
| 333 |
+
<p data-i18n="eval.indomain.body">180° trajectory loop closure with VLM-assisted scoring on held layouts.</p>
|
| 334 |
+
</article>
|
| 335 |
+
<article class="card">
|
| 336 |
+
<h3 data-i18n="eval.opendomain.title">Open-domain</h3>
|
| 337 |
+
<p data-i18n="eval.opendomain.body">Edited first frames and 45° return probes — stresses object identity and scene persistence.</p>
|
| 338 |
+
</article>
|
| 339 |
+
<article class="card">
|
| 340 |
+
<h3 data-i18n="eval.dynamic.title">Dynamic SpatialVID</h3>
|
| 341 |
+
<p data-i18n="eval.dynamic.body">Training and inference wrappers are public; the dynamic eval protocol is TODO.</p>
|
| 342 |
+
</article>
|
| 343 |
+
</div>
|
| 344 |
+
<figure class="section-figure" data-zoomable>
|
| 345 |
+
<img src="assets/figure_3_mem_eval.png" data-i18n-attr="alt:eval.fig.alt" alt="Three-branch evaluation summary" />
|
| 346 |
+
<figcaption data-i18n-html="eval.fig.cap">Replay health vs. return memory under the same stack. <span class="zoom-hint">Click to expand</span></figcaption>
|
| 347 |
+
</figure>
|
| 348 |
+
</div>
|
| 349 |
+
</section>
|
| 350 |
+
|
| 351 |
+
<section id="evidence" class="section section--center reveal" data-section="evidence">
|
| 352 |
+
<div class="container">
|
| 353 |
+
<p class="section-kicker" data-i18n="evidence.kicker">05 · Qualitative Evidence</p>
|
| 354 |
+
<h2 class="section-title" data-i18n="evidence.title">Return probes expose identity drift.</h2>
|
| 355 |
+
<p class="section-lead" data-i18n-html="evidence.lead">
|
| 356 |
+
Qualitative panels follow a simple diagnostic: <strong>first frame → leave the view → revisit tail</strong>.
|
| 357 |
+
We compare whether memory restores the same object, pose, background, and camera geometry —
|
| 358 |
+
not merely a plausible new scene.
|
| 359 |
+
</p>
|
| 360 |
+
<div class="qual-viewer" data-qual-viewer>
|
| 361 |
+
<figure class="qual-stage" data-zoomable>
|
| 362 |
+
<div class="qual-stage__media">
|
| 363 |
+
<img
|
| 364 |
+
data-qual-image
|
| 365 |
+
class="is-active"
|
| 366 |
+
src="assets/representative_memory_results.png"
|
| 367 |
+
alt="Representative memory comparisons"
|
| 368 |
+
/>
|
| 369 |
+
</div>
|
| 370 |
+
<figcaption data-qual-caption data-i18n-html="evidence.cap1">Representative memory comparisons across variants. <span class="zoom-hint">Click to expand</span></figcaption>
|
| 371 |
+
</figure>
|
| 372 |
+
<div class="qual-chips" role="tablist" aria-label="Evidence panels">
|
| 373 |
+
<button
|
| 374 |
+
class="qual-chip is-active"
|
| 375 |
+
type="button"
|
| 376 |
+
role="tab"
|
| 377 |
+
aria-selected="true"
|
| 378 |
+
data-src="assets/representative_memory_results.png"
|
| 379 |
+
data-alt="Representative memory comparisons"
|
| 380 |
+
data-caption-key="evidence.cap1"
|
| 381 |
+
data-caption="Representative memory comparisons across variants."
|
| 382 |
+
>Memory Results</button>
|
| 383 |
+
<button
|
| 384 |
+
class="qual-chip"
|
| 385 |
+
type="button"
|
| 386 |
+
role="tab"
|
| 387 |
+
aria-selected="false"
|
| 388 |
+
data-src="assets/representative_sweep_panel.png"
|
| 389 |
+
data-alt="Open-domain return sweep"
|
| 390 |
+
data-caption="Open-domain return sweep across the design matrix."
|
| 391 |
+
>Open-Domain Sweep</button>
|
| 392 |
+
<button
|
| 393 |
+
class="qual-chip"
|
| 394 |
+
type="button"
|
| 395 |
+
role="tab"
|
| 396 |
+
aria-selected="false"
|
| 397 |
+
data-src="assets/open_domain_revisit_panel.png"
|
| 398 |
+
data-alt="Edited first frames"
|
| 399 |
+
data-caption="Edited first frames for open-domain return probes."
|
| 400 |
+
>Identity Anchors</button>
|
| 401 |
+
</div>
|
| 402 |
+
</div>
|
| 403 |
+
<div class="demo-grid" aria-label="Static and SpatialVID replay demos">
|
| 404 |
+
<h3 class="demo-grid__label">Static Replay</h3>
|
| 405 |
+
<figure>
|
| 406 |
+
<img src="assets/readme_previews/context_k1_replay_gt.gif" alt="Static Context K=1 replay">
|
| 407 |
+
<figcaption>Context K=1</figcaption>
|
| 408 |
+
</figure>
|
| 409 |
+
<figure>
|
| 410 |
+
<img src="assets/readme_previews/context_k5_replay_gt.gif" alt="Static Context K=5 replay">
|
| 411 |
+
<figcaption>Context K=5</figcaption>
|
| 412 |
+
</figure>
|
| 413 |
+
<figure>
|
| 414 |
+
<img src="assets/readme_previews/framepack_len_r4_replay_gt.gif" alt="Static Compression r=4 replay">
|
| 415 |
+
<figcaption>Compression r = 4</figcaption>
|
| 416 |
+
</figure>
|
| 417 |
+
<figure>
|
| 418 |
+
<img src="assets/readme_previews/spatial_memory_replay_gt.gif" alt="Static Spatial Memory replay">
|
| 419 |
+
<figcaption>Spatial Memory</figcaption>
|
| 420 |
+
</figure>
|
| 421 |
+
<figure>
|
| 422 |
+
<img src="assets/readme_previews/ssm_legacy_replay_gt.gif" alt="Static legacy VideoSSM replay">
|
| 423 |
+
<figcaption>Legacy Hybrid</figcaption>
|
| 424 |
+
</figure>
|
| 425 |
+
<figure>
|
| 426 |
+
<img src="assets/readme_previews/ssm_blockwise_replay_gt.gif" alt="Static Block-wise SSM replay">
|
| 427 |
+
<figcaption>Block-wise SSM</figcaption>
|
| 428 |
+
</figure>
|
| 429 |
+
<h3 class="demo-grid__label">SpatialVID Replay</h3>
|
| 430 |
+
<figure>
|
| 431 |
+
<img src="assets/readme_previews/dyn_context_k1_replay.gif" alt="Dynamic Context K=1 replay">
|
| 432 |
+
<figcaption>Context K=1</figcaption>
|
| 433 |
+
</figure>
|
| 434 |
+
<figure>
|
| 435 |
+
<img src="assets/readme_previews/dyn_context_k5_replay.gif" alt="Dynamic Context K=5 replay">
|
| 436 |
+
<figcaption>Context K=5</figcaption>
|
| 437 |
+
</figure>
|
| 438 |
+
<figure>
|
| 439 |
+
<img src="assets/readme_previews/dyn_context_k20_replay.gif" alt="Dynamic Context K=20 replay">
|
| 440 |
+
<figcaption>Context K=20</figcaption>
|
| 441 |
+
</figure>
|
| 442 |
+
<figure>
|
| 443 |
+
<img src="assets/readme_previews/dyn_spatial_memory_replay.gif" alt="Dynamic Spatial Memory replay">
|
| 444 |
+
<figcaption>Spatial Memory</figcaption>
|
| 445 |
+
</figure>
|
| 446 |
+
<figure>
|
| 447 |
+
<img src="assets/readme_previews/dyn_ssm_legacy_replay.gif" alt="Dynamic legacy VideoSSM replay">
|
| 448 |
+
<figcaption>Legacy Hybrid</figcaption>
|
| 449 |
+
</figure>
|
| 450 |
+
<figure>
|
| 451 |
+
<img src="assets/readme_previews/dyn_ssm_blockwise_replay.gif" alt="Dynamic Block-wise SSM replay">
|
| 452 |
+
<figcaption>Block-wise SSM</figcaption>
|
| 453 |
+
</figure>
|
| 454 |
+
</div>
|
| 455 |
+
<p class="section-note" data-i18n="evidence.dynamic.note">
|
| 456 |
+
Dynamic previews use one randomly selected training scene replayed with the same first frame, prompt, and GT camera trajectory across all six rows.
|
| 457 |
+
</p>
|
| 458 |
+
</div>
|
| 459 |
+
</section>
|
| 460 |
+
|
| 461 |
+
<section id="findings" class="section section--center reveal" data-section="findings">
|
| 462 |
+
<div class="container">
|
| 463 |
+
<p class="section-kicker" data-i18n="findings.kicker">06 · Main Conclusions</p>
|
| 464 |
+
<h2 class="section-title" data-i18n="findings.title">Replay quality ≠ memory quality.</h2>
|
| 465 |
+
<p class="section-lead" data-i18n="findings.lead">
|
| 466 |
+
Replay metrics and return probes do not always agree — a model can look sharp on reconstruction
|
| 467 |
+
yet fail when the camera returns. Rankings reorder once identity under revisit is measured.
|
| 468 |
+
</p>
|
| 469 |
+
<ul class="bullet-list bullet-list--center">
|
| 470 |
+
<li data-i18n-html="findings.b1"><strong>Raw context</strong> — more history helps open-domain return more than replay alone.</li>
|
| 471 |
+
<li data-i18n-html="findings.b2"><strong>Compression</strong> — compact tokens can preserve replay while losing identity on return.</li>
|
| 472 |
+
<li data-i18n-html="findings.b3"><strong>Spatial vs. SSM</strong> — explicit state and block-wise SSM trade off layout carry and long-horizon stability.</li>
|
| 473 |
+
<li data-i18n-html="findings.b4"><strong>Takeaway</strong> — treat replay as a health check, not the final memory benchmark.</li>
|
| 474 |
+
</ul>
|
| 475 |
+
<figure class="section-figure" data-zoomable>
|
| 476 |
+
<img src="assets/replay_revisit_metric_alignment.png" alt="Replay vs revisit metrics" />
|
| 477 |
+
<figcaption data-i18n-html="findings.fig.cap">Rank shift from replay to return — replay is not the final memory score. <span class="zoom-hint">Click to expand</span></figcaption>
|
| 478 |
+
</figure>
|
| 479 |
+
</div>
|
| 480 |
+
</section>
|
| 481 |
+
|
| 482 |
+
<section id="updates" class="section reveal" data-section="updates">
|
| 483 |
+
<div class="container">
|
| 484 |
+
<p class="section-kicker" data-i18n="updates.kicker">07 · News & Roadmap</p>
|
| 485 |
+
<h2 class="section-title" data-i18n="updates.title">Release notes and next steps.</h2>
|
| 486 |
+
|
| 487 |
+
<div class="updates-grid">
|
| 488 |
+
<div class="updates-panel">
|
| 489 |
+
<h3 class="updates-heading" data-i18n="updates.news">News</h3>
|
| 490 |
+
<ul class="news-feed">
|
| 491 |
+
<li class="news-item">
|
| 492 |
+
<time datetime="2026-06-13">2026/06/13</time>
|
| 493 |
+
<p data-i18n-html="updates.news0">
|
| 494 |
+
SpatialVID support added: dynamic training/inference recipes, 5-second first-chunk replay previews, and dynamic eval TODO.
|
| 495 |
+
</p>
|
| 496 |
+
</li>
|
| 497 |
+
<li class="news-item">
|
| 498 |
+
<time datetime="2026-06-06">2026/06/06</time>
|
| 499 |
+
<p data-i18n-html="updates.news1">
|
| 500 |
+
Echo-Memory released: paper, project page, public code, replay/revisit eval assets, and baseline checkpoints.
|
| 501 |
+
</p>
|
| 502 |
+
</li>
|
| 503 |
+
</ul>
|
| 504 |
+
</div>
|
| 505 |
+
|
| 506 |
+
<div class="updates-panel">
|
| 507 |
+
<h3 class="updates-heading" data-i18n="updates.roadmap">Roadmap</h3>
|
| 508 |
+
<div class="todo-groups">
|
| 509 |
+
<article class="todo-group">
|
| 510 |
+
<h4 data-i18n="updates.models">Models</h4>
|
| 511 |
+
<ul class="todo-list">
|
| 512 |
+
<li class="is-done">
|
| 513 |
+
<span class="todo-check is-done" aria-hidden="true"></span>
|
| 514 |
+
<span data-i18n-html="updates.rm1"><strong>Wan 2.1 1.3B</strong> backbone and training recipes</span>
|
| 515 |
+
</li>
|
| 516 |
+
<li class="is-done">
|
| 517 |
+
<span class="todo-check is-done" aria-hidden="true"></span>
|
| 518 |
+
<span data-i18n="updates.rm2">Four memory families — Context, Compression, Spatial, State-Space</span>
|
| 519 |
+
</li>
|
| 520 |
+
<li class="is-done">
|
| 521 |
+
<span class="todo-check is-done" aria-hidden="true"></span>
|
| 522 |
+
<span data-i18n-html="updates.rm3"><strong>Dynamic training pool</strong> — SpatialVID subset export & settings</span>
|
| 523 |
+
</li>
|
| 524 |
+
<li class="is-done">
|
| 525 |
+
<span class="todo-check is-done" aria-hidden="true"></span>
|
| 526 |
+
<span data-i18n-html="updates.rm4"><strong>Paper checkpoints</strong> — <a href="https://huggingface.co/Echo-Team/Echo-Memory" target="_blank" rel="noopener noreferrer">Echo-Team/Echo-Memory</a></span>
|
| 527 |
+
</li>
|
| 528 |
+
<li>
|
| 529 |
+
<span class="todo-check" aria-hidden="true"></span>
|
| 530 |
+
<span data-i18n-html="updates.rm5"><strong>Wan 2.2</strong> + multi-scale <strong>5B / 14B</strong></span>
|
| 531 |
+
</li>
|
| 532 |
+
</ul>
|
| 533 |
+
</article>
|
| 534 |
+
<article class="todo-group">
|
| 535 |
+
<h4 data-i18n="updates.eval">Eval</h4>
|
| 536 |
+
<ul class="todo-list">
|
| 537 |
+
<li>
|
| 538 |
+
<span class="todo-check" aria-hidden="true"></span>
|
| 539 |
+
<span data-i18n-html="updates.re1"><strong>Dynamic eval</strong> beyond static replay/revisit</span>
|
| 540 |
+
</li>
|
| 541 |
+
<li>
|
| 542 |
+
<span class="todo-check" aria-hidden="true"></span>
|
| 543 |
+
<span data-i18n="updates.re2">More revisit probes and scoring presets</span>
|
| 544 |
+
</li>
|
| 545 |
+
</ul>
|
| 546 |
+
</article>
|
| 547 |
+
</div>
|
| 548 |
+
</div>
|
| 549 |
+
|
| 550 |
+
<div class="updates-panel community-panel">
|
| 551 |
+
<h3 class="updates-heading" data-i18n="community.title">Community</h3>
|
| 552 |
+
<p data-i18n="community.lead">Join the Echo-Memory WeChat group for release updates, checkpoint questions, and maintainer coordination.</p>
|
| 553 |
+
<figure class="wechat-qr">
|
| 554 |
+
<img
|
| 555 |
+
class="wechat-qr__photo"
|
| 556 |
+
src="assets/wechat_group_qrcode.jpg?v=20260701"
|
| 557 |
+
width="1166"
|
| 558 |
+
height="1640"
|
| 559 |
+
alt="Echo-Memory WeChat group QR code"
|
| 560 |
+
data-i18n-attr="alt:community.qr.alt"
|
| 561 |
+
/>
|
| 562 |
+
<figcaption data-i18n="community.qr.caption">Echo-Memory 交流群 · scan to join (QR refreshes periodically)</figcaption>
|
| 563 |
+
</figure>
|
| 564 |
+
</div>
|
| 565 |
+
</div>
|
| 566 |
+
</div>
|
| 567 |
+
</section>
|
| 568 |
+
|
| 569 |
+
<section id="bibtex" class="section reveal" data-section="bibtex">
|
| 570 |
+
<div class="container">
|
| 571 |
+
<p class="section-kicker" data-i18n="bibtex.kicker">08 · Citation</p>
|
| 572 |
+
<h2 class="section-title" data-i18n="bibtex.title">BibTeX</h2>
|
| 573 |
+
<p class="section-lead" data-i18n-html="bibtex.lead">
|
| 574 |
+
Echo-Memory: A Controlled Study of Memory in Action World Models (June 2026).
|
| 575 |
+
Licensed under
|
| 576 |
+
<a href="https://creativecommons.org/licenses/by/4.0/" target="_blank" rel="noopener noreferrer">CC BY 4.0</a>.
|
| 577 |
+
Cite the arXiv preprint below.
|
| 578 |
+
</p>
|
| 579 |
+
<div class="cite-switcher">
|
| 580 |
+
<div class="cite-panel is-active">
|
| 581 |
+
<dl class="pub-meta">
|
| 582 |
+
<div class="pub-meta__row">
|
| 583 |
+
<dt data-i18n="bibtex.source">Source</dt>
|
| 584 |
+
<dd>
|
| 585 |
+
<a href="https://arxiv.org/abs/2606.09803" target="_blank" rel="noopener noreferrer">arXiv</a>
|
| 586 |
+
</dd>
|
| 587 |
+
</div>
|
| 588 |
+
<div class="pub-meta__row">
|
| 589 |
+
<dt data-i18n="bibtex.arxivid">arXiv ID</dt>
|
| 590 |
+
<dd>2606.09803</dd>
|
| 591 |
+
</div>
|
| 592 |
+
<div class="pub-meta__row">
|
| 593 |
+
<dt data-i18n="bibtex.pdf">PDF</dt>
|
| 594 |
+
<dd>
|
| 595 |
+
<a href="https://arxiv.org/pdf/2606.09803" target="_blank" rel="noopener noreferrer">arxiv.org/pdf/2606.09803</a>
|
| 596 |
+
</dd>
|
| 597 |
+
</div>
|
| 598 |
+
<div class="pub-meta__row">
|
| 599 |
+
<dt data-i18n="bibtex.license">License</dt>
|
| 600 |
+
<dd>CC BY 4.0</dd>
|
| 601 |
+
</div>
|
| 602 |
+
</dl>
|
| 603 |
+
<div class="bibtex-wrap">
|
| 604 |
+
<button type="button" class="copy-btn" data-copy-target="bibtex-arxiv-block" data-i18n="bibtex.copy">Copy</button>
|
| 605 |
+
<pre class="bibtex" id="bibtex-arxiv-block"><code>@article{king2026echomemory,
|
| 606 |
+
title={Echo-Memory: A Controlled Study of Memory in Action World Models},
|
| 607 |
+
author={King, Wayne and Xue, Zeyue and Bian, Yuxuan and Huang, Jie and Li, Haoran and Li, Yaowei and Su, Yaofeng and Li, Yuming and Wang, Haoyu and Zhang, Shiyi and Zhang, Songchun and Niu, Yuwei and Xu, Sihan and Zhuang, Junhao and Huang, Haoyang and Duan, Nan},
|
| 608 |
+
journal={arXiv preprint arXiv:2606.09803},
|
| 609 |
+
year={2026},
|
| 610 |
+
month={jun},
|
| 611 |
+
eprint={2606.09803},
|
| 612 |
+
archivePrefix={arXiv},
|
| 613 |
+
primaryClass={cs.CV},
|
| 614 |
+
url={https://arxiv.org/abs/2606.09803}
|
| 615 |
+
}</code></pre>
|
| 616 |
+
</div>
|
| 617 |
+
</div>
|
| 618 |
+
</div>
|
| 619 |
+
</div>
|
| 620 |
+
</section>
|
| 621 |
+
</main>
|
| 622 |
+
|
| 623 |
+
<footer class="footer">
|
| 624 |
+
<div class="container footer-row">
|
| 625 |
+
<div>
|
| 626 |
+
<p class="footer-copy" data-i18n="footer.copy">© Echo Team · Joy Future Academy, JD</p>
|
| 627 |
+
</div>
|
| 628 |
+
<div class="footer-links">
|
| 629 |
+
<a href="https://github.com/Echo-Team-Joy-Future-Academy-JD/Echo-Memory">GitHub</a>
|
| 630 |
+
<a href="https://arxiv.org/abs/2606.09803" target="_blank" rel="noopener noreferrer">arXiv</a>
|
| 631 |
+
<a href="https://arxiv.org/pdf/2606.09803" target="_blank" rel="noopener noreferrer">PDF</a>
|
| 632 |
+
<a href="https://echo-team-joy-future-academy-jd.github.io/Echo-Memory/" data-i18n="footer.pp">Project Page</a>
|
| 633 |
+
<a href="developer.html" data-i18n="nav.developer">Developer Guide</a>
|
| 634 |
+
</div>
|
| 635 |
+
<p class="build-id" hidden aria-hidden="true">site-build-main</p>
|
| 636 |
+
</div>
|
| 637 |
+
</footer>
|
| 638 |
+
</div>
|
| 639 |
+
|
| 640 |
+
<script src="i18n.js?v=lang-v2"></script>
|
| 641 |
+
<script src="i18n-runtime.js?v=lang-v2"></script>
|
| 642 |
+
<script src="site.js?v=lang-v1" defer></script>
|
| 643 |
+
</body>
|
| 644 |
+
</html>
|
code/docs/site.js
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
(function () {
|
| 2 |
+
"use strict";
|
| 3 |
+
|
| 4 |
+
const NAV_OFFSET = 72;
|
| 5 |
+
const REDUCED_MOTION = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
| 6 |
+
const SCROLL_BEHAVIOR = REDUCED_MOTION ? "auto" : "smooth";
|
| 7 |
+
|
| 8 |
+
const sectionEls = Array.from(document.querySelectorAll(".section[data-section]"));
|
| 9 |
+
const navLinks = Array.from(document.querySelectorAll("[data-nav]"));
|
| 10 |
+
const slideDots = Array.from(document.querySelectorAll(".slide-dots button[data-slide]"));
|
| 11 |
+
const navToggle = document.getElementById("nav-toggle");
|
| 12 |
+
const navMobile = document.getElementById("nav-menu-mobile");
|
| 13 |
+
const topNav = document.querySelector(".top-nav");
|
| 14 |
+
const progressBar = document.querySelector(".scroll-progress__bar");
|
| 15 |
+
const backTop = document.getElementById("back-top");
|
| 16 |
+
const lightbox = document.getElementById("lightbox");
|
| 17 |
+
const lightboxImg = document.getElementById("lightbox-img");
|
| 18 |
+
const lightboxCaption = document.getElementById("lightbox-caption");
|
| 19 |
+
const lightboxClose = lightbox ? lightbox.querySelector(".lightbox__close") : null;
|
| 20 |
+
|
| 21 |
+
function setActiveNav(id) {
|
| 22 |
+
navLinks.forEach(function (a) {
|
| 23 |
+
a.classList.toggle("is-active", a.getAttribute("data-nav") === id);
|
| 24 |
+
});
|
| 25 |
+
slideDots.forEach(function (btn) {
|
| 26 |
+
btn.classList.toggle("is-active", btn.getAttribute("data-slide") === id);
|
| 27 |
+
});
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
function scrollToSection(id) {
|
| 31 |
+
var el = document.getElementById(id);
|
| 32 |
+
if (!el) return;
|
| 33 |
+
el.scrollIntoView({ behavior: SCROLL_BEHAVIOR, block: "start" });
|
| 34 |
+
setActiveNav(id);
|
| 35 |
+
if (navMobile && !navMobile.hidden) {
|
| 36 |
+
navToggle.setAttribute("aria-expanded", "false");
|
| 37 |
+
navMobile.hidden = true;
|
| 38 |
+
if (topNav) topNav.classList.remove("is-open");
|
| 39 |
+
}
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
function sectionIndex(id) {
|
| 43 |
+
return sectionEls.findIndex(function (s) {
|
| 44 |
+
return s.getAttribute("data-section") === id;
|
| 45 |
+
});
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
function scrollAdjacent(delta) {
|
| 49 |
+
var activeId = null;
|
| 50 |
+
sectionEls.forEach(function (s) {
|
| 51 |
+
var rect = s.getBoundingClientRect();
|
| 52 |
+
if (rect.top <= NAV_OFFSET + 80 && rect.bottom > NAV_OFFSET + 80) {
|
| 53 |
+
activeId = s.getAttribute("data-section");
|
| 54 |
+
}
|
| 55 |
+
});
|
| 56 |
+
if (!activeId && sectionEls.length) activeId = sectionEls[0].getAttribute("data-section");
|
| 57 |
+
var idx = sectionIndex(activeId);
|
| 58 |
+
if (idx < 0) return;
|
| 59 |
+
var next = sectionEls[idx + delta];
|
| 60 |
+
if (next) scrollToSection(next.getAttribute("data-section"));
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
if (sectionEls.length) {
|
| 64 |
+
var observer = new IntersectionObserver(
|
| 65 |
+
function (entries) {
|
| 66 |
+
entries.forEach(function (entry) {
|
| 67 |
+
if (entry.isIntersecting) {
|
| 68 |
+
setActiveNav(entry.target.getAttribute("data-section"));
|
| 69 |
+
}
|
| 70 |
+
});
|
| 71 |
+
},
|
| 72 |
+
{ root: null, rootMargin: "-" + NAV_OFFSET + "px 0px -50% 0px", threshold: 0.12 }
|
| 73 |
+
);
|
| 74 |
+
sectionEls.forEach(function (section) {
|
| 75 |
+
observer.observe(section);
|
| 76 |
+
});
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
document.querySelectorAll(".reveal").forEach(function (el) {
|
| 80 |
+
var revealObserver = new IntersectionObserver(
|
| 81 |
+
function (entries) {
|
| 82 |
+
entries.forEach(function (entry) {
|
| 83 |
+
if (entry.isIntersecting) {
|
| 84 |
+
entry.target.classList.add("is-visible");
|
| 85 |
+
revealObserver.unobserve(entry.target);
|
| 86 |
+
}
|
| 87 |
+
});
|
| 88 |
+
},
|
| 89 |
+
{ root: null, rootMargin: "0px 0px -8% 0px", threshold: 0.08 }
|
| 90 |
+
);
|
| 91 |
+
if (REDUCED_MOTION) {
|
| 92 |
+
el.classList.add("is-visible");
|
| 93 |
+
} else {
|
| 94 |
+
revealObserver.observe(el);
|
| 95 |
+
}
|
| 96 |
+
});
|
| 97 |
+
|
| 98 |
+
function updateScrollUI() {
|
| 99 |
+
var scrollTop = window.scrollY || document.documentElement.scrollTop;
|
| 100 |
+
var docHeight = document.documentElement.scrollHeight - window.innerHeight;
|
| 101 |
+
var progress = docHeight > 0 ? (scrollTop / docHeight) * 100 : 0;
|
| 102 |
+
|
| 103 |
+
if (progressBar) progressBar.style.width = progress + "%";
|
| 104 |
+
if (topNav) topNav.classList.toggle("is-scrolled", scrollTop > 24);
|
| 105 |
+
if (backTop) {
|
| 106 |
+
var show = scrollTop > window.innerHeight * 0.6;
|
| 107 |
+
backTop.hidden = !show;
|
| 108 |
+
backTop.classList.toggle("is-visible", show);
|
| 109 |
+
}
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
window.addEventListener("scroll", updateScrollUI, { passive: true });
|
| 113 |
+
updateScrollUI();
|
| 114 |
+
|
| 115 |
+
navLinks.forEach(function (a) {
|
| 116 |
+
a.addEventListener("click", function (e) {
|
| 117 |
+
e.preventDefault();
|
| 118 |
+
scrollToSection(a.getAttribute("data-nav"));
|
| 119 |
+
});
|
| 120 |
+
});
|
| 121 |
+
|
| 122 |
+
slideDots.forEach(function (btn) {
|
| 123 |
+
btn.addEventListener("click", function () {
|
| 124 |
+
scrollToSection(btn.getAttribute("data-slide"));
|
| 125 |
+
});
|
| 126 |
+
});
|
| 127 |
+
|
| 128 |
+
if (navToggle && navMobile) {
|
| 129 |
+
navToggle.addEventListener("click", function () {
|
| 130 |
+
var open = navMobile.hidden;
|
| 131 |
+
navMobile.hidden = !open;
|
| 132 |
+
navToggle.setAttribute("aria-expanded", String(open));
|
| 133 |
+
if (topNav) topNav.classList.toggle("is-open", open);
|
| 134 |
+
});
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
if (backTop) {
|
| 138 |
+
backTop.addEventListener("click", function () {
|
| 139 |
+
scrollToSection("hero");
|
| 140 |
+
});
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
function openLightbox(img, captionText) {
|
| 144 |
+
if (!lightbox || !lightboxImg) return;
|
| 145 |
+
lightboxImg.src = img.src;
|
| 146 |
+
lightboxImg.alt = img.alt || "";
|
| 147 |
+
if (lightboxCaption) {
|
| 148 |
+
lightboxCaption.textContent = captionText || "";
|
| 149 |
+
}
|
| 150 |
+
lightbox.hidden = false;
|
| 151 |
+
document.body.style.overflow = "hidden";
|
| 152 |
+
if (lightboxClose) lightboxClose.focus();
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
function closeLightbox() {
|
| 156 |
+
if (!lightbox) return;
|
| 157 |
+
lightbox.hidden = true;
|
| 158 |
+
document.body.style.overflow = "";
|
| 159 |
+
if (lightboxImg) lightboxImg.src = "";
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
document.querySelectorAll("[data-zoomable]").forEach(function (figure) {
|
| 163 |
+
figure.addEventListener("click", function (e) {
|
| 164 |
+
if (e.target.closest(".qual-chip, .copy-btn, button, a")) return;
|
| 165 |
+
var img = figure.querySelector("img.is-active, img[data-qual-image], img");
|
| 166 |
+
if (!img || !img.src) return;
|
| 167 |
+
var cap = figure.querySelector("figcaption");
|
| 168 |
+
var capText = cap ? cap.textContent.replace(/Click to expand/g, "").trim() : "";
|
| 169 |
+
openLightbox(img, capText);
|
| 170 |
+
});
|
| 171 |
+
});
|
| 172 |
+
|
| 173 |
+
if (lightboxClose) {
|
| 174 |
+
lightboxClose.addEventListener("click", closeLightbox);
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
if (lightbox) {
|
| 178 |
+
lightbox.addEventListener("click", function (e) {
|
| 179 |
+
if (e.target === lightbox) closeLightbox();
|
| 180 |
+
});
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
document.addEventListener("keydown", function (e) {
|
| 184 |
+
if (e.target.closest("input, textarea, pre, [contenteditable]")) return;
|
| 185 |
+
|
| 186 |
+
if (e.key === "Escape" && lightbox && !lightbox.hidden) {
|
| 187 |
+
closeLightbox();
|
| 188 |
+
return;
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
if (e.key === "ArrowDown" || e.key === "PageDown") {
|
| 192 |
+
e.preventDefault();
|
| 193 |
+
scrollAdjacent(1);
|
| 194 |
+
} else if (e.key === "ArrowUp" || e.key === "PageUp") {
|
| 195 |
+
e.preventDefault();
|
| 196 |
+
scrollAdjacent(-1);
|
| 197 |
+
}
|
| 198 |
+
});
|
| 199 |
+
|
| 200 |
+
function animateMetricValue(el, target) {
|
| 201 |
+
if (REDUCED_MOTION || !target || target === "—") {
|
| 202 |
+
el.textContent = target;
|
| 203 |
+
return;
|
| 204 |
+
}
|
| 205 |
+
var numeric = parseInt(String(target).replace(/,/g, ""), 10);
|
| 206 |
+
if (isNaN(numeric)) {
|
| 207 |
+
el.textContent = target;
|
| 208 |
+
return;
|
| 209 |
+
}
|
| 210 |
+
var start = 0;
|
| 211 |
+
var duration = 700;
|
| 212 |
+
var startTime = null;
|
| 213 |
+
function step(ts) {
|
| 214 |
+
if (!startTime) startTime = ts;
|
| 215 |
+
var t = Math.min((ts - startTime) / duration, 1);
|
| 216 |
+
var eased = 1 - Math.pow(1 - t, 3);
|
| 217 |
+
el.textContent = Math.round(start + (numeric - start) * eased).toLocaleString();
|
| 218 |
+
if (t < 1) requestAnimationFrame(step);
|
| 219 |
+
}
|
| 220 |
+
requestAnimationFrame(step);
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
function hydrateMetric(card) {
|
| 224 |
+
var valueEl = card.querySelector("[data-metric-value]");
|
| 225 |
+
var badgeUrl = card.getAttribute("data-badge-url");
|
| 226 |
+
if (!valueEl || !badgeUrl) return;
|
| 227 |
+
|
| 228 |
+
fetch(badgeUrl, { cache: "no-store" })
|
| 229 |
+
.then(function (res) {
|
| 230 |
+
if (!res.ok) throw new Error("Request failed");
|
| 231 |
+
return res.json();
|
| 232 |
+
})
|
| 233 |
+
.then(function (data) {
|
| 234 |
+
var val = data.value || data.message || valueEl.dataset.fallback || "—";
|
| 235 |
+
animateMetricValue(valueEl, val);
|
| 236 |
+
})
|
| 237 |
+
.catch(function () {
|
| 238 |
+
valueEl.textContent = valueEl.dataset.fallback || "—";
|
| 239 |
+
});
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
document.querySelectorAll("[data-badge-url]").forEach(function (card) {
|
| 243 |
+
var metricObserver = new IntersectionObserver(
|
| 244 |
+
function (entries) {
|
| 245 |
+
entries.forEach(function (entry) {
|
| 246 |
+
if (entry.isIntersecting) {
|
| 247 |
+
hydrateMetric(entry.target);
|
| 248 |
+
metricObserver.unobserve(entry.target);
|
| 249 |
+
}
|
| 250 |
+
});
|
| 251 |
+
},
|
| 252 |
+
{ threshold: 0.4 }
|
| 253 |
+
);
|
| 254 |
+
metricObserver.observe(card);
|
| 255 |
+
});
|
| 256 |
+
|
| 257 |
+
document.querySelectorAll("[data-qual-viewer]").forEach(function (viewer) {
|
| 258 |
+
var image = viewer.querySelector("[data-qual-image]");
|
| 259 |
+
var caption = viewer.querySelector("[data-qual-caption]");
|
| 260 |
+
var chips = viewer.querySelectorAll(".qual-chip");
|
| 261 |
+
|
| 262 |
+
chips.forEach(function (chip) {
|
| 263 |
+
chip.addEventListener("click", function () {
|
| 264 |
+
chips.forEach(function (c) {
|
| 265 |
+
c.classList.remove("is-active");
|
| 266 |
+
c.setAttribute("aria-selected", "false");
|
| 267 |
+
});
|
| 268 |
+
chip.classList.add("is-active");
|
| 269 |
+
chip.setAttribute("aria-selected", "true");
|
| 270 |
+
if (!image) return;
|
| 271 |
+
|
| 272 |
+
var nextSrc = chip.dataset.src;
|
| 273 |
+
var nextAlt = chip.dataset.alt || "";
|
| 274 |
+
var nextCaption = chip.dataset.caption || "";
|
| 275 |
+
|
| 276 |
+
var currentPath = new URL(image.src, window.location.href).pathname;
|
| 277 |
+
if (currentPath.endsWith(nextSrc)) return;
|
| 278 |
+
|
| 279 |
+
image.classList.add("is-fading");
|
| 280 |
+
var preload = new Image();
|
| 281 |
+
preload.onload = function () {
|
| 282 |
+
image.src = nextSrc;
|
| 283 |
+
image.alt = nextAlt;
|
| 284 |
+
image.classList.remove("is-fading");
|
| 285 |
+
image.classList.add("is-active");
|
| 286 |
+
};
|
| 287 |
+
preload.onerror = function () {
|
| 288 |
+
image.classList.remove("is-fading");
|
| 289 |
+
};
|
| 290 |
+
preload.src = nextSrc;
|
| 291 |
+
|
| 292 |
+
if (caption) {
|
| 293 |
+
var hint = (window.EchoI18n && window.EchoI18n.t("zoom.hint")) || "Click to expand";
|
| 294 |
+
caption.innerHTML = nextCaption + ' <span class="zoom-hint">' + hint + "</span>";
|
| 295 |
+
}
|
| 296 |
+
});
|
| 297 |
+
});
|
| 298 |
+
});
|
| 299 |
+
|
| 300 |
+
function initCiteSwitcher() {
|
| 301 |
+
document.querySelectorAll("[data-cite-switcher]").forEach(function (switcher) {
|
| 302 |
+
var chips = switcher.querySelectorAll(".cite-chip");
|
| 303 |
+
var panels = switcher.querySelectorAll("[data-cite-panel]");
|
| 304 |
+
|
| 305 |
+
chips.forEach(function (chip) {
|
| 306 |
+
chip.addEventListener("click", function () {
|
| 307 |
+
var key = chip.getAttribute("data-cite");
|
| 308 |
+
chips.forEach(function (c) {
|
| 309 |
+
c.classList.remove("is-active");
|
| 310 |
+
c.setAttribute("aria-selected", "false");
|
| 311 |
+
});
|
| 312 |
+
chip.classList.add("is-active");
|
| 313 |
+
chip.setAttribute("aria-selected", "true");
|
| 314 |
+
panels.forEach(function (panel) {
|
| 315 |
+
var match = panel.getAttribute("data-cite-panel") === key;
|
| 316 |
+
panel.classList.toggle("is-active", match);
|
| 317 |
+
panel.hidden = !match;
|
| 318 |
+
});
|
| 319 |
+
});
|
| 320 |
+
});
|
| 321 |
+
});
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
initCiteSwitcher();
|
| 325 |
+
|
| 326 |
+
document.querySelectorAll("[data-copy-target]").forEach(function (btn) {
|
| 327 |
+
btn.addEventListener("click", function () {
|
| 328 |
+
var targetId = btn.getAttribute("data-copy-target");
|
| 329 |
+
var target = document.getElementById(targetId);
|
| 330 |
+
if (!target) return;
|
| 331 |
+
navigator.clipboard.writeText(target.textContent).then(
|
| 332 |
+
function () {
|
| 333 |
+
var copied = window.EchoI18n && window.EchoI18n.t("bibtex.copied");
|
| 334 |
+
var failed = window.EchoI18n && window.EchoI18n.t("bibtex.failed");
|
| 335 |
+
var copyLabel = window.EchoI18n && window.EchoI18n.t("bibtex.copy");
|
| 336 |
+
btn.textContent = copied || "Copied";
|
| 337 |
+
btn.classList.add("is-copied");
|
| 338 |
+
setTimeout(function () {
|
| 339 |
+
btn.textContent = copyLabel || "Copy";
|
| 340 |
+
btn.classList.remove("is-copied");
|
| 341 |
+
}, 2000);
|
| 342 |
+
},
|
| 343 |
+
function () {
|
| 344 |
+
btn.textContent = (window.EchoI18n && window.EchoI18n.t("bibtex.failed")) || "Failed";
|
| 345 |
+
}
|
| 346 |
+
);
|
| 347 |
+
});
|
| 348 |
+
});
|
| 349 |
+
})();
|
code/docs/style.css
ADDED
|
@@ -0,0 +1,1848 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
:root {
|
| 2 |
+
--bg: #fffcfd;
|
| 3 |
+
--bg-soft: #fff5f8;
|
| 4 |
+
--surface: #ffffff;
|
| 5 |
+
--surface-2: #fbf0f4;
|
| 6 |
+
--text: #191218;
|
| 7 |
+
--text-soft: #655b63;
|
| 8 |
+
--border: #ead9df;
|
| 9 |
+
--brand: #bf5276;
|
| 10 |
+
--brand-2: #d97498;
|
| 11 |
+
--accent: #84354e;
|
| 12 |
+
--ring: rgba(191, 82, 118, 0.28);
|
| 13 |
+
--shadow: 0 14px 36px rgba(93, 45, 61, 0.08);
|
| 14 |
+
--shadow-lg: 0 22px 48px rgba(93, 45, 61, 0.12);
|
| 15 |
+
--nav-h: 72px;
|
| 16 |
+
--figure-max: 880px;
|
| 17 |
+
--ease-out: cubic-bezier(0.22, 1, 0.36, 1);
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
*,
|
| 21 |
+
*::before,
|
| 22 |
+
*::after {
|
| 23 |
+
box-sizing: border-box;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
html {
|
| 27 |
+
scroll-behavior: smooth;
|
| 28 |
+
scroll-snap-type: y proximity;
|
| 29 |
+
scroll-padding-top: var(--nav-h);
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
body {
|
| 33 |
+
margin: 0;
|
| 34 |
+
font-family: "IBM Plex Sans", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
| 35 |
+
color: var(--text);
|
| 36 |
+
background-color: #fff9fb;
|
| 37 |
+
min-height: 100vh;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
a {
|
| 41 |
+
color: inherit;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
img {
|
| 45 |
+
max-width: 100%;
|
| 46 |
+
display: block;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
.sr-only {
|
| 50 |
+
position: absolute;
|
| 51 |
+
width: 1px;
|
| 52 |
+
height: 1px;
|
| 53 |
+
padding: 0;
|
| 54 |
+
margin: -1px;
|
| 55 |
+
overflow: hidden;
|
| 56 |
+
clip: rect(0, 0, 0, 0);
|
| 57 |
+
border: 0;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
.page-shell {
|
| 61 |
+
position: relative;
|
| 62 |
+
width: 100%;
|
| 63 |
+
overflow-x: hidden;
|
| 64 |
+
isolation: isolate;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
#main-content {
|
| 68 |
+
width: 100%;
|
| 69 |
+
position: relative;
|
| 70 |
+
z-index: 1;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
/* Ambient background — layered gradients + slow motion */
|
| 74 |
+
.ambient-bg {
|
| 75 |
+
position: fixed;
|
| 76 |
+
inset: 0;
|
| 77 |
+
z-index: -3;
|
| 78 |
+
pointer-events: none;
|
| 79 |
+
overflow: hidden;
|
| 80 |
+
background: linear-gradient(
|
| 81 |
+
165deg,
|
| 82 |
+
#fffcfd 0%,
|
| 83 |
+
#fff6f9 38%,
|
| 84 |
+
#fff0f5 68%,
|
| 85 |
+
#fffbfc 100%
|
| 86 |
+
);
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
.ambient-orb {
|
| 90 |
+
position: absolute;
|
| 91 |
+
border-radius: 50%;
|
| 92 |
+
filter: blur(72px);
|
| 93 |
+
opacity: 0.55;
|
| 94 |
+
will-change: transform;
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
.ambient-orb--rose {
|
| 98 |
+
width: min(58vw, 520px);
|
| 99 |
+
height: min(58vw, 520px);
|
| 100 |
+
top: -12%;
|
| 101 |
+
left: -8%;
|
| 102 |
+
background: radial-gradient(
|
| 103 |
+
circle,
|
| 104 |
+
rgba(191, 82, 118, 0.42) 0%,
|
| 105 |
+
rgba(191, 82, 118, 0.12) 45%,
|
| 106 |
+
transparent 70%
|
| 107 |
+
);
|
| 108 |
+
animation: orbDriftRose 22s ease-in-out infinite;
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
.ambient-orb--blush {
|
| 112 |
+
width: min(50vw, 460px);
|
| 113 |
+
height: min(50vw, 460px);
|
| 114 |
+
top: 8%;
|
| 115 |
+
right: -10%;
|
| 116 |
+
background: radial-gradient(
|
| 117 |
+
circle,
|
| 118 |
+
rgba(217, 116, 152, 0.38) 0%,
|
| 119 |
+
rgba(255, 182, 198, 0.14) 50%,
|
| 120 |
+
transparent 72%
|
| 121 |
+
);
|
| 122 |
+
animation: orbDriftBlush 26s ease-in-out infinite;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
.ambient-orb--pearl {
|
| 126 |
+
width: min(64vw, 580px);
|
| 127 |
+
height: min(64vw, 580px);
|
| 128 |
+
bottom: -18%;
|
| 129 |
+
left: 22%;
|
| 130 |
+
background: radial-gradient(
|
| 131 |
+
circle,
|
| 132 |
+
rgba(255, 220, 230, 0.5) 0%,
|
| 133 |
+
rgba(191, 82, 118, 0.1) 40%,
|
| 134 |
+
transparent 68%
|
| 135 |
+
);
|
| 136 |
+
animation: orbDriftPearl 30s ease-in-out infinite;
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
.ambient-shimmer {
|
| 140 |
+
position: absolute;
|
| 141 |
+
inset: -20%;
|
| 142 |
+
background: conic-gradient(
|
| 143 |
+
from 200deg at 50% 45%,
|
| 144 |
+
transparent 0deg,
|
| 145 |
+
rgba(191, 82, 118, 0.06) 55deg,
|
| 146 |
+
transparent 110deg,
|
| 147 |
+
rgba(217, 116, 152, 0.05) 200deg,
|
| 148 |
+
transparent 280deg,
|
| 149 |
+
rgba(255, 200, 215, 0.04) 330deg,
|
| 150 |
+
transparent 360deg
|
| 151 |
+
);
|
| 152 |
+
animation: shimmerRotate 48s linear infinite;
|
| 153 |
+
opacity: 0.9;
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
.page-shell::before {
|
| 157 |
+
content: "";
|
| 158 |
+
position: fixed;
|
| 159 |
+
inset: 0;
|
| 160 |
+
pointer-events: none;
|
| 161 |
+
opacity: 0.2;
|
| 162 |
+
background-image: radial-gradient(circle, rgba(191, 82, 118, 0.11) 1px, transparent 1px);
|
| 163 |
+
background-size: 26px 26px;
|
| 164 |
+
z-index: -2;
|
| 165 |
+
animation: gridPulse 8s ease-in-out infinite;
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
.page-shell::after {
|
| 169 |
+
content: "";
|
| 170 |
+
position: fixed;
|
| 171 |
+
inset: 0;
|
| 172 |
+
pointer-events: none;
|
| 173 |
+
z-index: -1;
|
| 174 |
+
background:
|
| 175 |
+
radial-gradient(ellipse 120% 80% at 50% 0%, rgba(255, 255, 255, 0.55), transparent 55%),
|
| 176 |
+
radial-gradient(ellipse 90% 60% at 50% 100%, rgba(191, 82, 118, 0.06), transparent 50%);
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
@keyframes orbDriftRose {
|
| 180 |
+
0%,
|
| 181 |
+
100% {
|
| 182 |
+
transform: translate(0, 0) scale(1);
|
| 183 |
+
}
|
| 184 |
+
33% {
|
| 185 |
+
transform: translate(4%, 6%) scale(1.06);
|
| 186 |
+
}
|
| 187 |
+
66% {
|
| 188 |
+
transform: translate(2%, 3%) scale(0.96);
|
| 189 |
+
}
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
@keyframes orbDriftBlush {
|
| 193 |
+
0%,
|
| 194 |
+
100% {
|
| 195 |
+
transform: translate(0, 0) scale(1);
|
| 196 |
+
}
|
| 197 |
+
40% {
|
| 198 |
+
transform: translate(-5%, 4%) scale(1.08);
|
| 199 |
+
}
|
| 200 |
+
70% {
|
| 201 |
+
transform: translate(-2%, 7%) scale(0.94);
|
| 202 |
+
}
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
@keyframes orbDriftPearl {
|
| 206 |
+
0%,
|
| 207 |
+
100% {
|
| 208 |
+
transform: translate(0, 0) scale(1);
|
| 209 |
+
}
|
| 210 |
+
50% {
|
| 211 |
+
transform: translate(6%, -4%) scale(1.05);
|
| 212 |
+
}
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
@keyframes shimmerRotate {
|
| 216 |
+
from {
|
| 217 |
+
transform: rotate(0deg);
|
| 218 |
+
}
|
| 219 |
+
to {
|
| 220 |
+
transform: rotate(360deg);
|
| 221 |
+
}
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
@keyframes gridPulse {
|
| 225 |
+
0%,
|
| 226 |
+
100% {
|
| 227 |
+
opacity: 0.18;
|
| 228 |
+
}
|
| 229 |
+
50% {
|
| 230 |
+
opacity: 0.24;
|
| 231 |
+
}
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
.container {
|
| 235 |
+
width: min(1120px, calc(100% - 2rem));
|
| 236 |
+
margin: 0 auto;
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
/* Scroll progress */
|
| 240 |
+
.scroll-progress {
|
| 241 |
+
position: fixed;
|
| 242 |
+
top: 0;
|
| 243 |
+
left: 0;
|
| 244 |
+
right: 0;
|
| 245 |
+
height: 3px;
|
| 246 |
+
z-index: 70;
|
| 247 |
+
pointer-events: none;
|
| 248 |
+
background: color-mix(in srgb, var(--border) 40%, transparent);
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
.scroll-progress__bar {
|
| 252 |
+
display: block;
|
| 253 |
+
height: 100%;
|
| 254 |
+
width: 0%;
|
| 255 |
+
background: linear-gradient(90deg, var(--accent), var(--brand), var(--brand-2));
|
| 256 |
+
border-radius: 0 2px 2px 0;
|
| 257 |
+
transition: width 0.08s linear;
|
| 258 |
+
box-shadow: 0 0 12px color-mix(in srgb, var(--brand) 45%, transparent);
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
/* Section scroll snap & progress dots */
|
| 262 |
+
.slide-dots {
|
| 263 |
+
position: fixed;
|
| 264 |
+
right: 14px;
|
| 265 |
+
top: 50%;
|
| 266 |
+
z-index: 55;
|
| 267 |
+
transform: translateY(-50%);
|
| 268 |
+
display: flex;
|
| 269 |
+
flex-direction: column;
|
| 270 |
+
gap: 0.45rem;
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
.slide-dots button {
|
| 274 |
+
position: relative;
|
| 275 |
+
width: 9px;
|
| 276 |
+
height: 9px;
|
| 277 |
+
padding: 0;
|
| 278 |
+
border: none;
|
| 279 |
+
border-radius: 50%;
|
| 280 |
+
background: color-mix(in srgb, var(--brand) 28%, var(--border));
|
| 281 |
+
cursor: pointer;
|
| 282 |
+
transition: transform 0.25s var(--ease-out), background 0.25s ease, box-shadow 0.25s ease;
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
.slide-dots button::after {
|
| 286 |
+
content: attr(data-label);
|
| 287 |
+
position: absolute;
|
| 288 |
+
right: calc(100% + 10px);
|
| 289 |
+
top: 50%;
|
| 290 |
+
transform: translateY(-50%) translateX(6px);
|
| 291 |
+
padding: 0.28rem 0.55rem;
|
| 292 |
+
border-radius: 0.4rem;
|
| 293 |
+
background: var(--text);
|
| 294 |
+
color: #fff;
|
| 295 |
+
font-size: 0.68rem;
|
| 296 |
+
font-weight: 600;
|
| 297 |
+
white-space: nowrap;
|
| 298 |
+
opacity: 0;
|
| 299 |
+
pointer-events: none;
|
| 300 |
+
transition: opacity 0.2s ease, transform 0.2s var(--ease-out);
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
.slide-dots button:hover::after,
|
| 304 |
+
.slide-dots button:focus-visible::after {
|
| 305 |
+
opacity: 1;
|
| 306 |
+
transform: translateY(-50%) translateX(0);
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
.slide-dots button:hover {
|
| 310 |
+
transform: scale(1.2);
|
| 311 |
+
background: color-mix(in srgb, var(--brand) 55%, var(--border));
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
.slide-dots button.is-active {
|
| 315 |
+
transform: scale(1.35);
|
| 316 |
+
background: var(--brand);
|
| 317 |
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand) 25%, transparent);
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
/* Back to top */
|
| 321 |
+
.back-top {
|
| 322 |
+
position: fixed;
|
| 323 |
+
right: 1.1rem;
|
| 324 |
+
bottom: 1.25rem;
|
| 325 |
+
z-index: 56;
|
| 326 |
+
width: 2.6rem;
|
| 327 |
+
height: 2.6rem;
|
| 328 |
+
border: 1px solid color-mix(in srgb, var(--brand) 35%, var(--border));
|
| 329 |
+
border-radius: 50%;
|
| 330 |
+
background: color-mix(in srgb, var(--surface) 88%, transparent);
|
| 331 |
+
backdrop-filter: blur(10px);
|
| 332 |
+
color: var(--accent);
|
| 333 |
+
cursor: pointer;
|
| 334 |
+
box-shadow: var(--shadow);
|
| 335 |
+
display: grid;
|
| 336 |
+
place-items: center;
|
| 337 |
+
transition: transform 0.25s var(--ease-out), opacity 0.25s ease, border-color 0.2s ease;
|
| 338 |
+
opacity: 0;
|
| 339 |
+
pointer-events: none;
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
.back-top.is-visible {
|
| 343 |
+
opacity: 1;
|
| 344 |
+
pointer-events: auto;
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
.back-top:hover {
|
| 348 |
+
transform: translateY(-3px);
|
| 349 |
+
border-color: var(--brand);
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
+
/* Reveal on scroll */
|
| 353 |
+
.reveal {
|
| 354 |
+
opacity: 0;
|
| 355 |
+
transform: translateY(28px);
|
| 356 |
+
transition: opacity 0.65s var(--ease-out), transform 0.65s var(--ease-out);
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
.reveal.is-visible {
|
| 360 |
+
opacity: 1;
|
| 361 |
+
transform: translateY(0);
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
.reveal .card,
|
| 365 |
+
.reveal .section-figure,
|
| 366 |
+
.reveal .qual-viewer {
|
| 367 |
+
transition-delay: 0.05s;
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
/* Nav */
|
| 371 |
+
.top-nav {
|
| 372 |
+
position: sticky;
|
| 373 |
+
top: 0;
|
| 374 |
+
z-index: 60;
|
| 375 |
+
border-bottom: 1px solid color-mix(in srgb, var(--border) 65%, transparent);
|
| 376 |
+
background: color-mix(in srgb, var(--surface) 72%, transparent);
|
| 377 |
+
backdrop-filter: blur(16px) saturate(1.15);
|
| 378 |
+
-webkit-backdrop-filter: blur(16px) saturate(1.15);
|
| 379 |
+
transition: box-shadow 0.3s ease, background 0.3s ease;
|
| 380 |
+
}
|
| 381 |
+
|
| 382 |
+
.top-nav.is-scrolled {
|
| 383 |
+
box-shadow: 0 8px 24px rgba(93, 45, 61, 0.06);
|
| 384 |
+
background: color-mix(in srgb, var(--surface) 92%, transparent);
|
| 385 |
+
}
|
| 386 |
+
|
| 387 |
+
.top-nav__inner {
|
| 388 |
+
min-height: var(--nav-h);
|
| 389 |
+
display: grid;
|
| 390 |
+
grid-template-columns: auto minmax(0, 1fr) auto;
|
| 391 |
+
align-items: center;
|
| 392 |
+
gap: 0.65rem;
|
| 393 |
+
}
|
| 394 |
+
|
| 395 |
+
.brand {
|
| 396 |
+
display: inline-flex;
|
| 397 |
+
align-items: center;
|
| 398 |
+
gap: 0.55rem;
|
| 399 |
+
text-decoration: none;
|
| 400 |
+
font-family: "Space Grotesk", sans-serif;
|
| 401 |
+
font-weight: 700;
|
| 402 |
+
font-size: 1.04rem;
|
| 403 |
+
flex-shrink: 0;
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
.brand:hover {
|
| 407 |
+
text-decoration: none;
|
| 408 |
+
}
|
| 409 |
+
|
| 410 |
+
.brand-name {
|
| 411 |
+
color: var(--text);
|
| 412 |
+
}
|
| 413 |
+
|
| 414 |
+
.brand-team {
|
| 415 |
+
padding: 0.2rem 0.55rem;
|
| 416 |
+
border-radius: 999px;
|
| 417 |
+
font-size: 0.68rem;
|
| 418 |
+
font-weight: 600;
|
| 419 |
+
color: var(--accent);
|
| 420 |
+
background: color-mix(in srgb, var(--brand) 12%, var(--surface));
|
| 421 |
+
border: 1px solid color-mix(in srgb, var(--brand) 35%, var(--border));
|
| 422 |
+
}
|
| 423 |
+
|
| 424 |
+
.nav-links {
|
| 425 |
+
display: flex;
|
| 426 |
+
align-items: center;
|
| 427 |
+
justify-content: center;
|
| 428 |
+
gap: 0.3rem;
|
| 429 |
+
flex-wrap: nowrap;
|
| 430 |
+
min-width: 0;
|
| 431 |
+
}
|
| 432 |
+
|
| 433 |
+
.nav-link {
|
| 434 |
+
text-decoration: none;
|
| 435 |
+
color: var(--text-soft);
|
| 436 |
+
font-size: 0.8rem;
|
| 437 |
+
font-weight: 500;
|
| 438 |
+
padding: 0.35rem 0.38rem;
|
| 439 |
+
white-space: nowrap;
|
| 440 |
+
border-radius: 0.45rem;
|
| 441 |
+
transition: color 0.2s ease, background 0.2s ease;
|
| 442 |
+
}
|
| 443 |
+
|
| 444 |
+
.nav-link:hover,
|
| 445 |
+
.nav-link.is-active {
|
| 446 |
+
color: var(--accent);
|
| 447 |
+
background: color-mix(in srgb, var(--brand) 10%, transparent);
|
| 448 |
+
text-decoration: none;
|
| 449 |
+
}
|
| 450 |
+
|
| 451 |
+
.nav-actions {
|
| 452 |
+
display: inline-flex;
|
| 453 |
+
align-items: center;
|
| 454 |
+
gap: 0.55rem;
|
| 455 |
+
flex-shrink: 0;
|
| 456 |
+
}
|
| 457 |
+
|
| 458 |
+
.action-btn {
|
| 459 |
+
border: 1px solid var(--border);
|
| 460 |
+
background: var(--surface-2);
|
| 461 |
+
color: var(--text);
|
| 462 |
+
border-radius: 0.6rem;
|
| 463 |
+
min-height: 2.2rem;
|
| 464 |
+
padding: 0.35rem 0.7rem;
|
| 465 |
+
font-size: 0.82rem;
|
| 466 |
+
font-weight: 600;
|
| 467 |
+
text-decoration: none;
|
| 468 |
+
cursor: pointer;
|
| 469 |
+
transition: transform 0.2s ease, border-color 0.2s ease, background 0.2s ease;
|
| 470 |
+
}
|
| 471 |
+
|
| 472 |
+
.action-btn:hover {
|
| 473 |
+
transform: translateY(-1px);
|
| 474 |
+
border-color: color-mix(in srgb, var(--brand) 58%, var(--border));
|
| 475 |
+
text-decoration: none;
|
| 476 |
+
}
|
| 477 |
+
|
| 478 |
+
.nav-toggle {
|
| 479 |
+
display: none;
|
| 480 |
+
flex-direction: column;
|
| 481 |
+
gap: 5px;
|
| 482 |
+
padding: 8px;
|
| 483 |
+
border: 1px solid var(--border);
|
| 484 |
+
border-radius: 0.5rem;
|
| 485 |
+
background: var(--surface);
|
| 486 |
+
cursor: pointer;
|
| 487 |
+
}
|
| 488 |
+
|
| 489 |
+
.nav-toggle span {
|
| 490 |
+
display: block;
|
| 491 |
+
width: 20px;
|
| 492 |
+
height: 2px;
|
| 493 |
+
background: var(--text);
|
| 494 |
+
border-radius: 2px;
|
| 495 |
+
}
|
| 496 |
+
|
| 497 |
+
.nav-mobile {
|
| 498 |
+
display: none;
|
| 499 |
+
flex-direction: column;
|
| 500 |
+
gap: 0.25rem;
|
| 501 |
+
padding: 0 0 1rem;
|
| 502 |
+
border-top: 1px solid var(--border);
|
| 503 |
+
background: color-mix(in srgb, var(--surface) 95%, transparent);
|
| 504 |
+
}
|
| 505 |
+
|
| 506 |
+
.top-nav.is-open .nav-mobile {
|
| 507 |
+
display: flex;
|
| 508 |
+
}
|
| 509 |
+
|
| 510 |
+
.nav-mobile .nav-link {
|
| 511 |
+
padding: 0.55rem 0.65rem;
|
| 512 |
+
}
|
| 513 |
+
|
| 514 |
+
/* Sections (starVLA-style: normal flow, container max-width) */
|
| 515 |
+
.section {
|
| 516 |
+
position: relative;
|
| 517 |
+
padding: 4.5rem 0;
|
| 518 |
+
scroll-margin-top: var(--nav-h);
|
| 519 |
+
scroll-snap-align: start;
|
| 520 |
+
}
|
| 521 |
+
|
| 522 |
+
#main-content > .section + .section::before {
|
| 523 |
+
content: "";
|
| 524 |
+
position: absolute;
|
| 525 |
+
top: 0;
|
| 526 |
+
left: 50%;
|
| 527 |
+
transform: translateX(-50%);
|
| 528 |
+
width: min(1120px, calc(100% - 2rem));
|
| 529 |
+
height: 1px;
|
| 530 |
+
background: linear-gradient(
|
| 531 |
+
90deg,
|
| 532 |
+
transparent 0%,
|
| 533 |
+
color-mix(in srgb, var(--brand) 18%, var(--border)) 18%,
|
| 534 |
+
color-mix(in srgb, var(--brand) 18%, var(--border)) 82%,
|
| 535 |
+
transparent 100%
|
| 536 |
+
);
|
| 537 |
+
opacity: 0.85;
|
| 538 |
+
}
|
| 539 |
+
|
| 540 |
+
#main-content > .section:nth-of-type(even) {
|
| 541 |
+
background: linear-gradient(
|
| 542 |
+
180deg,
|
| 543 |
+
color-mix(in srgb, var(--surface) 55%, transparent) 0%,
|
| 544 |
+
color-mix(in srgb, var(--surface-2) 28%, transparent) 100%
|
| 545 |
+
);
|
| 546 |
+
backdrop-filter: blur(2px);
|
| 547 |
+
}
|
| 548 |
+
|
| 549 |
+
.section--center .container {
|
| 550 |
+
text-align: center;
|
| 551 |
+
}
|
| 552 |
+
|
| 553 |
+
.section--center .section-lead {
|
| 554 |
+
margin-inline: auto;
|
| 555 |
+
}
|
| 556 |
+
|
| 557 |
+
.section--center .feature-grid {
|
| 558 |
+
text-align: left;
|
| 559 |
+
}
|
| 560 |
+
|
| 561 |
+
.section--center .bullet-list {
|
| 562 |
+
display: inline-block;
|
| 563 |
+
text-align: left;
|
| 564 |
+
}
|
| 565 |
+
|
| 566 |
+
.section--center .qual-viewer,
|
| 567 |
+
.section--center .section-figure {
|
| 568 |
+
margin-inline: auto;
|
| 569 |
+
}
|
| 570 |
+
|
| 571 |
+
.section--compact {
|
| 572 |
+
padding-top: 3.5rem;
|
| 573 |
+
padding-bottom: 3.5rem;
|
| 574 |
+
}
|
| 575 |
+
|
| 576 |
+
|
| 577 |
+
.section-kicker {
|
| 578 |
+
margin: 0 0 0.5rem;
|
| 579 |
+
font-size: 0.72rem;
|
| 580 |
+
font-weight: 600;
|
| 581 |
+
letter-spacing: 0.1em;
|
| 582 |
+
text-transform: uppercase;
|
| 583 |
+
color: var(--brand);
|
| 584 |
+
}
|
| 585 |
+
|
| 586 |
+
.section-title {
|
| 587 |
+
margin: 0;
|
| 588 |
+
font-family: "Space Grotesk", sans-serif;
|
| 589 |
+
font-size: clamp(1.6rem, 4vw, 2.35rem);
|
| 590 |
+
line-height: 1.1;
|
| 591 |
+
letter-spacing: -0.02em;
|
| 592 |
+
}
|
| 593 |
+
|
| 594 |
+
.section-lead {
|
| 595 |
+
margin-top: 0.8rem;
|
| 596 |
+
color: var(--text-soft);
|
| 597 |
+
line-height: 1.7;
|
| 598 |
+
max-width: 70ch;
|
| 599 |
+
}
|
| 600 |
+
|
| 601 |
+
/* Hero */
|
| 602 |
+
.hero {
|
| 603 |
+
display: grid;
|
| 604 |
+
grid-template-columns: 1.08fr 0.92fr;
|
| 605 |
+
gap: 2.2rem;
|
| 606 |
+
align-items: center;
|
| 607 |
+
}
|
| 608 |
+
|
| 609 |
+
.hero--single {
|
| 610 |
+
grid-template-columns: 1fr;
|
| 611 |
+
}
|
| 612 |
+
|
| 613 |
+
.hero-badge {
|
| 614 |
+
display: inline-flex;
|
| 615 |
+
align-items: center;
|
| 616 |
+
padding: 0.35rem 0.72rem;
|
| 617 |
+
border-radius: 999px;
|
| 618 |
+
border: 1px solid color-mix(in srgb, var(--brand) 58%, transparent);
|
| 619 |
+
background: color-mix(in srgb, var(--brand) 10%, var(--surface));
|
| 620 |
+
font-size: 0.74rem;
|
| 621 |
+
font-weight: 600;
|
| 622 |
+
letter-spacing: 0.04em;
|
| 623 |
+
text-transform: uppercase;
|
| 624 |
+
color: color-mix(in srgb, var(--brand) 85%, var(--text));
|
| 625 |
+
}
|
| 626 |
+
|
| 627 |
+
.hero-title {
|
| 628 |
+
margin-top: 1.15rem;
|
| 629 |
+
margin-bottom: 0.95rem;
|
| 630 |
+
font-family: "Space Grotesk", sans-serif;
|
| 631 |
+
font-size: clamp(2rem, 5vw, 3.3rem);
|
| 632 |
+
line-height: 1.06;
|
| 633 |
+
letter-spacing: -0.02em;
|
| 634 |
+
}
|
| 635 |
+
|
| 636 |
+
.hero-highlight {
|
| 637 |
+
background: linear-gradient(120deg, var(--accent), var(--brand), var(--brand-2));
|
| 638 |
+
-webkit-background-clip: text;
|
| 639 |
+
background-clip: text;
|
| 640 |
+
-webkit-text-fill-color: transparent;
|
| 641 |
+
}
|
| 642 |
+
|
| 643 |
+
.hero-subtitle {
|
| 644 |
+
margin: 0;
|
| 645 |
+
color: var(--text-soft);
|
| 646 |
+
font-size: 1.07rem;
|
| 647 |
+
line-height: 1.7;
|
| 648 |
+
max-width: 52ch;
|
| 649 |
+
}
|
| 650 |
+
|
| 651 |
+
.hero-cta-group {
|
| 652 |
+
margin-top: 1.45rem;
|
| 653 |
+
display: flex;
|
| 654 |
+
flex-wrap: wrap;
|
| 655 |
+
gap: 0.75rem;
|
| 656 |
+
}
|
| 657 |
+
|
| 658 |
+
.cta {
|
| 659 |
+
text-decoration: none;
|
| 660 |
+
display: inline-flex;
|
| 661 |
+
align-items: center;
|
| 662 |
+
justify-content: center;
|
| 663 |
+
gap: 0.45rem;
|
| 664 |
+
border-radius: 0.8rem;
|
| 665 |
+
border: 1px solid transparent;
|
| 666 |
+
min-height: 2.8rem;
|
| 667 |
+
padding: 0.55rem 1rem;
|
| 668 |
+
font-weight: 700;
|
| 669 |
+
font-size: 0.94rem;
|
| 670 |
+
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
|
| 671 |
+
}
|
| 672 |
+
|
| 673 |
+
.cta:hover {
|
| 674 |
+
transform: translateY(-1px);
|
| 675 |
+
text-decoration: none;
|
| 676 |
+
}
|
| 677 |
+
|
| 678 |
+
.cta--primary {
|
| 679 |
+
background: linear-gradient(128deg, var(--brand), var(--brand-2));
|
| 680 |
+
color: #fff;
|
| 681 |
+
box-shadow: 0 16px 30px color-mix(in srgb, var(--brand) 34%, transparent);
|
| 682 |
+
}
|
| 683 |
+
|
| 684 |
+
.cta--primary:hover {
|
| 685 |
+
box-shadow: 0 20px 36px color-mix(in srgb, var(--brand) 42%, transparent);
|
| 686 |
+
}
|
| 687 |
+
|
| 688 |
+
.cta--secondary {
|
| 689 |
+
background: var(--surface);
|
| 690 |
+
border-color: var(--border);
|
| 691 |
+
color: var(--accent);
|
| 692 |
+
}
|
| 693 |
+
|
| 694 |
+
.metrics-grid {
|
| 695 |
+
margin-top: 1.6rem;
|
| 696 |
+
display: grid;
|
| 697 |
+
gap: 0.85rem;
|
| 698 |
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
| 699 |
+
max-width: 640px;
|
| 700 |
+
}
|
| 701 |
+
|
| 702 |
+
.metrics-grid--duo {
|
| 703 |
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
| 704 |
+
max-width: 420px;
|
| 705 |
+
}
|
| 706 |
+
|
| 707 |
+
.hero-note {
|
| 708 |
+
margin: 1.25rem 0 0;
|
| 709 |
+
max-width: 52ch;
|
| 710 |
+
font-size: 0.92rem;
|
| 711 |
+
line-height: 1.65;
|
| 712 |
+
color: var(--text-soft);
|
| 713 |
+
}
|
| 714 |
+
|
| 715 |
+
.section-lead a {
|
| 716 |
+
color: var(--accent);
|
| 717 |
+
font-weight: 600;
|
| 718 |
+
text-decoration: underline;
|
| 719 |
+
text-underline-offset: 2px;
|
| 720 |
+
}
|
| 721 |
+
|
| 722 |
+
.section-lead a:hover {
|
| 723 |
+
color: var(--brand);
|
| 724 |
+
}
|
| 725 |
+
|
| 726 |
+
.metric-card {
|
| 727 |
+
display: block;
|
| 728 |
+
padding: 0.9rem 1rem;
|
| 729 |
+
border: 1px solid var(--border);
|
| 730 |
+
border-radius: 0.95rem;
|
| 731 |
+
background: var(--surface);
|
| 732 |
+
box-shadow: var(--shadow);
|
| 733 |
+
text-decoration: none;
|
| 734 |
+
transition: transform 0.2s ease, border-color 0.2s ease;
|
| 735 |
+
}
|
| 736 |
+
|
| 737 |
+
.metric-card:hover {
|
| 738 |
+
transform: translateY(-2px);
|
| 739 |
+
border-color: color-mix(in srgb, var(--brand) 45%, var(--border));
|
| 740 |
+
text-decoration: none;
|
| 741 |
+
}
|
| 742 |
+
|
| 743 |
+
.metric-label {
|
| 744 |
+
display: block;
|
| 745 |
+
color: var(--text-soft);
|
| 746 |
+
font-size: 0.8rem;
|
| 747 |
+
letter-spacing: 0.02em;
|
| 748 |
+
}
|
| 749 |
+
|
| 750 |
+
.metric-value {
|
| 751 |
+
margin-top: 0.25rem;
|
| 752 |
+
display: block;
|
| 753 |
+
font-family: "Space Grotesk", "IBM Plex Sans", sans-serif;
|
| 754 |
+
font-size: 1.45rem;
|
| 755 |
+
font-weight: 700;
|
| 756 |
+
color: var(--text);
|
| 757 |
+
}
|
| 758 |
+
|
| 759 |
+
.authors-fold {
|
| 760 |
+
margin-top: 1.5rem;
|
| 761 |
+
max-width: 52ch;
|
| 762 |
+
font-size: 0.875rem;
|
| 763 |
+
color: var(--text-soft);
|
| 764 |
+
}
|
| 765 |
+
|
| 766 |
+
.authors-fold summary {
|
| 767 |
+
cursor: pointer;
|
| 768 |
+
font-weight: 600;
|
| 769 |
+
color: var(--accent);
|
| 770 |
+
list-style: none;
|
| 771 |
+
}
|
| 772 |
+
|
| 773 |
+
.authors-fold summary::-webkit-details-marker {
|
| 774 |
+
display: none;
|
| 775 |
+
}
|
| 776 |
+
|
| 777 |
+
.authors {
|
| 778 |
+
margin: 0.75rem 0 0.35rem;
|
| 779 |
+
}
|
| 780 |
+
|
| 781 |
+
.affiliations {
|
| 782 |
+
margin: 0;
|
| 783 |
+
font-size: 0.82rem;
|
| 784 |
+
}
|
| 785 |
+
|
| 786 |
+
/* Cards & figures */
|
| 787 |
+
.feature-grid {
|
| 788 |
+
margin-top: 1.35rem;
|
| 789 |
+
display: grid;
|
| 790 |
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
| 791 |
+
gap: 0.95rem;
|
| 792 |
+
}
|
| 793 |
+
|
| 794 |
+
.feature-grid--three {
|
| 795 |
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
| 796 |
+
}
|
| 797 |
+
|
| 798 |
+
.card {
|
| 799 |
+
border: 1px solid var(--border);
|
| 800 |
+
background: var(--surface);
|
| 801 |
+
border-radius: 1rem;
|
| 802 |
+
padding: 1.2rem;
|
| 803 |
+
box-shadow: var(--shadow);
|
| 804 |
+
transition: transform 0.28s var(--ease-out), border-color 0.28s ease, box-shadow 0.28s ease;
|
| 805 |
+
}
|
| 806 |
+
|
| 807 |
+
.card--accent {
|
| 808 |
+
position: relative;
|
| 809 |
+
overflow: hidden;
|
| 810 |
+
}
|
| 811 |
+
|
| 812 |
+
.card--accent::before {
|
| 813 |
+
content: "";
|
| 814 |
+
position: absolute;
|
| 815 |
+
top: 0;
|
| 816 |
+
left: 0;
|
| 817 |
+
right: 0;
|
| 818 |
+
height: 3px;
|
| 819 |
+
background: var(--card-accent, var(--brand));
|
| 820 |
+
opacity: 0.85;
|
| 821 |
+
transition: height 0.25s ease;
|
| 822 |
+
}
|
| 823 |
+
|
| 824 |
+
.card--accent[data-accent="context"] {
|
| 825 |
+
--card-accent: #c45a7a;
|
| 826 |
+
}
|
| 827 |
+
|
| 828 |
+
.card--accent[data-accent="compression"] {
|
| 829 |
+
--card-accent: #a84d8f;
|
| 830 |
+
}
|
| 831 |
+
|
| 832 |
+
.card--accent[data-accent="spatial"] {
|
| 833 |
+
--card-accent: #84354e;
|
| 834 |
+
}
|
| 835 |
+
|
| 836 |
+
.card--accent[data-accent="ssm"] {
|
| 837 |
+
--card-accent: #d97498;
|
| 838 |
+
}
|
| 839 |
+
|
| 840 |
+
.card--accent:hover {
|
| 841 |
+
transform: translateY(-4px);
|
| 842 |
+
border-color: color-mix(in srgb, var(--card-accent) 45%, var(--border));
|
| 843 |
+
box-shadow: 0 18px 40px color-mix(in srgb, var(--card-accent) 14%, transparent);
|
| 844 |
+
}
|
| 845 |
+
|
| 846 |
+
.card--accent:hover::before {
|
| 847 |
+
height: 4px;
|
| 848 |
+
}
|
| 849 |
+
|
| 850 |
+
.card-icon {
|
| 851 |
+
width: 2.1rem;
|
| 852 |
+
height: 2.1rem;
|
| 853 |
+
border-radius: 0.55rem;
|
| 854 |
+
display: grid;
|
| 855 |
+
place-items: center;
|
| 856 |
+
background: color-mix(in srgb, var(--card-accent, var(--brand)) 12%, var(--surface));
|
| 857 |
+
color: var(--card-accent, var(--brand));
|
| 858 |
+
}
|
| 859 |
+
|
| 860 |
+
.card-icon svg {
|
| 861 |
+
width: 1.15rem;
|
| 862 |
+
height: 1.15rem;
|
| 863 |
+
}
|
| 864 |
+
|
| 865 |
+
.card h3 {
|
| 866 |
+
margin: 0.75rem 0 0.55rem;
|
| 867 |
+
font-family: "Space Grotesk", sans-serif;
|
| 868 |
+
font-size: 1.1rem;
|
| 869 |
+
}
|
| 870 |
+
|
| 871 |
+
.card p {
|
| 872 |
+
margin: 0;
|
| 873 |
+
color: var(--text-soft);
|
| 874 |
+
line-height: 1.65;
|
| 875 |
+
font-size: 0.92rem;
|
| 876 |
+
}
|
| 877 |
+
|
| 878 |
+
.bullet-list {
|
| 879 |
+
margin: 1rem 0 1.25rem;
|
| 880 |
+
padding-left: 1.25rem;
|
| 881 |
+
color: var(--text-soft);
|
| 882 |
+
line-height: 1.65;
|
| 883 |
+
}
|
| 884 |
+
|
| 885 |
+
.bullet-list li {
|
| 886 |
+
margin-bottom: 0.4rem;
|
| 887 |
+
}
|
| 888 |
+
|
| 889 |
+
.section-figure {
|
| 890 |
+
margin: 1.5rem auto 0;
|
| 891 |
+
text-align: center;
|
| 892 |
+
max-width: var(--figure-max);
|
| 893 |
+
}
|
| 894 |
+
|
| 895 |
+
[data-zoomable] {
|
| 896 |
+
cursor: zoom-in;
|
| 897 |
+
}
|
| 898 |
+
|
| 899 |
+
.section-figure img {
|
| 900 |
+
width: 100%;
|
| 901 |
+
margin: 0 auto;
|
| 902 |
+
border: 1px solid var(--border);
|
| 903 |
+
border-radius: 1rem;
|
| 904 |
+
background: var(--surface);
|
| 905 |
+
box-shadow: var(--shadow);
|
| 906 |
+
transition: transform 0.35s var(--ease-out), box-shadow 0.35s ease;
|
| 907 |
+
}
|
| 908 |
+
|
| 909 |
+
[data-zoomable]:hover img {
|
| 910 |
+
transform: scale(1.012);
|
| 911 |
+
box-shadow: var(--shadow-lg);
|
| 912 |
+
}
|
| 913 |
+
|
| 914 |
+
.section-figure figcaption {
|
| 915 |
+
margin-top: 0.65rem;
|
| 916 |
+
font-size: 0.84rem;
|
| 917 |
+
color: var(--text-soft);
|
| 918 |
+
}
|
| 919 |
+
|
| 920 |
+
.zoom-hint {
|
| 921 |
+
display: inline-block;
|
| 922 |
+
margin-left: 0.35rem;
|
| 923 |
+
font-size: 0.75rem;
|
| 924 |
+
color: color-mix(in srgb, var(--brand) 70%, var(--text-soft));
|
| 925 |
+
opacity: 0;
|
| 926 |
+
transition: opacity 0.2s ease;
|
| 927 |
+
}
|
| 928 |
+
|
| 929 |
+
[data-zoomable]:hover .zoom-hint {
|
| 930 |
+
opacity: 1;
|
| 931 |
+
}
|
| 932 |
+
|
| 933 |
+
/* Qual viewer */
|
| 934 |
+
.qual-viewer {
|
| 935 |
+
margin-top: 1.25rem;
|
| 936 |
+
max-width: var(--figure-max);
|
| 937 |
+
margin-inline: auto;
|
| 938 |
+
}
|
| 939 |
+
|
| 940 |
+
.qual-stage {
|
| 941 |
+
margin: 0 0 0.85rem;
|
| 942 |
+
padding: 0.85rem;
|
| 943 |
+
border: 1px solid var(--border);
|
| 944 |
+
border-radius: 1rem;
|
| 945 |
+
background: var(--surface);
|
| 946 |
+
box-shadow: var(--shadow);
|
| 947 |
+
}
|
| 948 |
+
|
| 949 |
+
.qual-stage__media {
|
| 950 |
+
position: relative;
|
| 951 |
+
overflow: hidden;
|
| 952 |
+
border-radius: 0.65rem;
|
| 953 |
+
aspect-ratio: 16 / 9;
|
| 954 |
+
background: color-mix(in srgb, var(--surface-2) 60%, var(--surface));
|
| 955 |
+
}
|
| 956 |
+
|
| 957 |
+
.qual-stage img {
|
| 958 |
+
width: 100%;
|
| 959 |
+
height: 100%;
|
| 960 |
+
object-fit: contain;
|
| 961 |
+
border-radius: 0.65rem;
|
| 962 |
+
transition: opacity 0.35s ease, transform 0.35s var(--ease-out);
|
| 963 |
+
}
|
| 964 |
+
|
| 965 |
+
.qual-stage img.is-fading {
|
| 966 |
+
opacity: 0;
|
| 967 |
+
transform: scale(0.985);
|
| 968 |
+
}
|
| 969 |
+
|
| 970 |
+
.qual-stage figcaption {
|
| 971 |
+
margin-top: 0.55rem;
|
| 972 |
+
font-size: 0.84rem;
|
| 973 |
+
color: var(--text-soft);
|
| 974 |
+
text-align: center;
|
| 975 |
+
}
|
| 976 |
+
|
| 977 |
+
.qual-chips {
|
| 978 |
+
display: flex;
|
| 979 |
+
flex-wrap: wrap;
|
| 980 |
+
justify-content: center;
|
| 981 |
+
gap: 0.5rem;
|
| 982 |
+
}
|
| 983 |
+
|
| 984 |
+
.qual-chip {
|
| 985 |
+
border: 1px solid var(--border);
|
| 986 |
+
border-radius: 999px;
|
| 987 |
+
padding: 0.45rem 0.85rem;
|
| 988 |
+
background: var(--surface);
|
| 989 |
+
font-size: 0.82rem;
|
| 990 |
+
font-weight: 600;
|
| 991 |
+
color: var(--text-soft);
|
| 992 |
+
cursor: pointer;
|
| 993 |
+
transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease;
|
| 994 |
+
}
|
| 995 |
+
|
| 996 |
+
.qual-chip.is-active,
|
| 997 |
+
.qual-chip:hover {
|
| 998 |
+
color: #fff;
|
| 999 |
+
background: var(--brand);
|
| 1000 |
+
border-color: var(--brand);
|
| 1001 |
+
}
|
| 1002 |
+
|
| 1003 |
+
.qual-chip {
|
| 1004 |
+
transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease, transform 0.2s var(--ease-out);
|
| 1005 |
+
}
|
| 1006 |
+
|
| 1007 |
+
.qual-chip:active {
|
| 1008 |
+
transform: scale(0.97);
|
| 1009 |
+
}
|
| 1010 |
+
|
| 1011 |
+
.demo-grid {
|
| 1012 |
+
margin: 1.3rem auto 0;
|
| 1013 |
+
max-width: 980px;
|
| 1014 |
+
display: grid;
|
| 1015 |
+
grid-template-columns: repeat(6, minmax(0, 1fr));
|
| 1016 |
+
gap: 0.75rem;
|
| 1017 |
+
}
|
| 1018 |
+
|
| 1019 |
+
.demo-grid__label {
|
| 1020 |
+
grid-column: 1 / -1;
|
| 1021 |
+
margin: 0.35rem 0 -0.25rem;
|
| 1022 |
+
font-family: "Space Grotesk", sans-serif;
|
| 1023 |
+
font-size: 0.95rem;
|
| 1024 |
+
color: var(--text);
|
| 1025 |
+
text-align: left;
|
| 1026 |
+
}
|
| 1027 |
+
|
| 1028 |
+
.demo-grid figure {
|
| 1029 |
+
margin: 0;
|
| 1030 |
+
padding: 0.55rem;
|
| 1031 |
+
border: 1px solid var(--border);
|
| 1032 |
+
border-radius: 0.9rem;
|
| 1033 |
+
background: var(--surface);
|
| 1034 |
+
box-shadow: var(--shadow);
|
| 1035 |
+
}
|
| 1036 |
+
|
| 1037 |
+
.demo-grid img {
|
| 1038 |
+
width: 100%;
|
| 1039 |
+
aspect-ratio: 16 / 9;
|
| 1040 |
+
object-fit: cover;
|
| 1041 |
+
border-radius: 0.55rem;
|
| 1042 |
+
background: var(--surface-2);
|
| 1043 |
+
}
|
| 1044 |
+
|
| 1045 |
+
.demo-grid figcaption {
|
| 1046 |
+
margin-top: 0.45rem;
|
| 1047 |
+
text-align: center;
|
| 1048 |
+
color: var(--text-soft);
|
| 1049 |
+
font-size: 0.76rem;
|
| 1050 |
+
font-weight: 700;
|
| 1051 |
+
}
|
| 1052 |
+
|
| 1053 |
+
/* Lightbox */
|
| 1054 |
+
.lightbox {
|
| 1055 |
+
position: fixed;
|
| 1056 |
+
inset: 0;
|
| 1057 |
+
z-index: 100;
|
| 1058 |
+
display: grid;
|
| 1059 |
+
place-items: center;
|
| 1060 |
+
padding: 1.5rem;
|
| 1061 |
+
background: rgba(25, 18, 24, 0.72);
|
| 1062 |
+
backdrop-filter: blur(8px);
|
| 1063 |
+
animation: lightboxIn 0.25s ease;
|
| 1064 |
+
}
|
| 1065 |
+
|
| 1066 |
+
.lightbox[hidden] {
|
| 1067 |
+
display: none;
|
| 1068 |
+
}
|
| 1069 |
+
|
| 1070 |
+
.lightbox__close {
|
| 1071 |
+
position: absolute;
|
| 1072 |
+
top: 1rem;
|
| 1073 |
+
right: 1rem;
|
| 1074 |
+
width: 2.5rem;
|
| 1075 |
+
height: 2.5rem;
|
| 1076 |
+
border: none;
|
| 1077 |
+
border-radius: 50%;
|
| 1078 |
+
background: rgba(255, 255, 255, 0.12);
|
| 1079 |
+
color: #fff;
|
| 1080 |
+
font-size: 1.5rem;
|
| 1081 |
+
line-height: 1;
|
| 1082 |
+
cursor: pointer;
|
| 1083 |
+
transition: background 0.2s ease;
|
| 1084 |
+
}
|
| 1085 |
+
|
| 1086 |
+
.lightbox__close:hover {
|
| 1087 |
+
background: rgba(255, 255, 255, 0.22);
|
| 1088 |
+
}
|
| 1089 |
+
|
| 1090 |
+
.lightbox__inner {
|
| 1091 |
+
margin: 0;
|
| 1092 |
+
max-width: min(1100px, 96vw);
|
| 1093 |
+
max-height: 90vh;
|
| 1094 |
+
}
|
| 1095 |
+
|
| 1096 |
+
.lightbox__inner img {
|
| 1097 |
+
max-width: 100%;
|
| 1098 |
+
max-height: calc(90vh - 3rem);
|
| 1099 |
+
margin: 0 auto;
|
| 1100 |
+
border-radius: 0.75rem;
|
| 1101 |
+
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.35);
|
| 1102 |
+
}
|
| 1103 |
+
|
| 1104 |
+
.lightbox__inner figcaption {
|
| 1105 |
+
margin-top: 0.75rem;
|
| 1106 |
+
text-align: center;
|
| 1107 |
+
color: rgba(255, 255, 255, 0.82);
|
| 1108 |
+
font-size: 0.9rem;
|
| 1109 |
+
}
|
| 1110 |
+
|
| 1111 |
+
@keyframes lightboxIn {
|
| 1112 |
+
from {
|
| 1113 |
+
opacity: 0;
|
| 1114 |
+
}
|
| 1115 |
+
to {
|
| 1116 |
+
opacity: 1;
|
| 1117 |
+
}
|
| 1118 |
+
}
|
| 1119 |
+
|
| 1120 |
+
/* News & roadmap */
|
| 1121 |
+
.updates-grid {
|
| 1122 |
+
margin-top: 1.5rem;
|
| 1123 |
+
display: grid;
|
| 1124 |
+
grid-template-columns: 1fr 1fr minmax(220px, 0.85fr);
|
| 1125 |
+
gap: 1.25rem;
|
| 1126 |
+
align-items: start;
|
| 1127 |
+
}
|
| 1128 |
+
|
| 1129 |
+
.community-panel .wechat-qr {
|
| 1130 |
+
margin: 0.75rem 0 0;
|
| 1131 |
+
}
|
| 1132 |
+
|
| 1133 |
+
.community-panel {
|
| 1134 |
+
overflow: visible;
|
| 1135 |
+
}
|
| 1136 |
+
|
| 1137 |
+
.wechat-qr {
|
| 1138 |
+
margin: 0.75rem auto 0;
|
| 1139 |
+
padding: 0;
|
| 1140 |
+
text-align: center;
|
| 1141 |
+
overflow: visible;
|
| 1142 |
+
}
|
| 1143 |
+
|
| 1144 |
+
/* Portrait WeChat screenshot: set display width only; height follows intrinsic ratio (1166×1640). */
|
| 1145 |
+
.wechat-qr__photo {
|
| 1146 |
+
display: block;
|
| 1147 |
+
width: 220px;
|
| 1148 |
+
max-width: 100%;
|
| 1149 |
+
height: auto;
|
| 1150 |
+
margin: 0 auto;
|
| 1151 |
+
border-radius: 0.75rem;
|
| 1152 |
+
border: 1px solid var(--border);
|
| 1153 |
+
box-shadow: var(--shadow);
|
| 1154 |
+
}
|
| 1155 |
+
|
| 1156 |
+
.developer-page .wechat-qr {
|
| 1157 |
+
margin: 1rem auto 0;
|
| 1158 |
+
overflow: visible;
|
| 1159 |
+
}
|
| 1160 |
+
|
| 1161 |
+
.developer-page .wechat-qr__photo {
|
| 1162 |
+
width: 260px;
|
| 1163 |
+
}
|
| 1164 |
+
|
| 1165 |
+
.wechat-qr figcaption {
|
| 1166 |
+
margin-top: 0.55rem;
|
| 1167 |
+
font-size: 0.82rem;
|
| 1168 |
+
color: var(--text-soft);
|
| 1169 |
+
}
|
| 1170 |
+
|
| 1171 |
+
.lang-toggle {
|
| 1172 |
+
border: 1px solid var(--border);
|
| 1173 |
+
background: var(--surface);
|
| 1174 |
+
color: var(--accent);
|
| 1175 |
+
border-radius: 999px;
|
| 1176 |
+
padding: 0.35rem 0.75rem;
|
| 1177 |
+
font-size: 0.78rem;
|
| 1178 |
+
font-weight: 600;
|
| 1179 |
+
cursor: pointer;
|
| 1180 |
+
font-family: inherit;
|
| 1181 |
+
transition: background 0.2s ease, border-color 0.2s ease;
|
| 1182 |
+
}
|
| 1183 |
+
|
| 1184 |
+
.lang-toggle:hover {
|
| 1185 |
+
background: var(--surface-2);
|
| 1186 |
+
border-color: var(--brand-2);
|
| 1187 |
+
}
|
| 1188 |
+
|
| 1189 |
+
.action-btn--ghost {
|
| 1190 |
+
background: transparent;
|
| 1191 |
+
border: 1px solid var(--border);
|
| 1192 |
+
color: var(--accent);
|
| 1193 |
+
}
|
| 1194 |
+
|
| 1195 |
+
.action-btn--ghost:hover {
|
| 1196 |
+
background: var(--surface-2);
|
| 1197 |
+
}
|
| 1198 |
+
|
| 1199 |
+
.page-shell--doc {
|
| 1200 |
+
min-height: 100vh;
|
| 1201 |
+
}
|
| 1202 |
+
|
| 1203 |
+
.top-nav--doc {
|
| 1204 |
+
position: sticky;
|
| 1205 |
+
top: 0;
|
| 1206 |
+
z-index: 20;
|
| 1207 |
+
background: rgba(255, 252, 253, 0.92);
|
| 1208 |
+
backdrop-filter: blur(10px);
|
| 1209 |
+
border-bottom: 1px solid var(--border);
|
| 1210 |
+
}
|
| 1211 |
+
|
| 1212 |
+
.dev-main {
|
| 1213 |
+
padding: 2rem 1rem 3rem;
|
| 1214 |
+
max-width: 52rem;
|
| 1215 |
+
min-width: 0;
|
| 1216 |
+
box-sizing: border-box;
|
| 1217 |
+
}
|
| 1218 |
+
|
| 1219 |
+
.top-nav__inner--doc {
|
| 1220 |
+
min-width: 0;
|
| 1221 |
+
}
|
| 1222 |
+
|
| 1223 |
+
.action-btn--compact {
|
| 1224 |
+
white-space: nowrap;
|
| 1225 |
+
max-width: min(100%, 14rem);
|
| 1226 |
+
overflow: hidden;
|
| 1227 |
+
text-overflow: ellipsis;
|
| 1228 |
+
}
|
| 1229 |
+
|
| 1230 |
+
@media (max-width: 640px) {
|
| 1231 |
+
.top-nav__inner--doc {
|
| 1232 |
+
gap: 0.45rem;
|
| 1233 |
+
}
|
| 1234 |
+
|
| 1235 |
+
.action-btn--compact {
|
| 1236 |
+
max-width: 9.5rem;
|
| 1237 |
+
font-size: 0.74rem;
|
| 1238 |
+
padding: 0.35rem 0.5rem;
|
| 1239 |
+
}
|
| 1240 |
+
|
| 1241 |
+
.lang-toggle {
|
| 1242 |
+
font-size: 0.72rem;
|
| 1243 |
+
padding: 0.3rem 0.55rem;
|
| 1244 |
+
}
|
| 1245 |
+
}
|
| 1246 |
+
|
| 1247 |
+
.dev-section {
|
| 1248 |
+
margin-top: 1rem;
|
| 1249 |
+
padding: 1.15rem 1.25rem;
|
| 1250 |
+
overflow: visible;
|
| 1251 |
+
min-width: 0;
|
| 1252 |
+
}
|
| 1253 |
+
|
| 1254 |
+
.dev-skills-title {
|
| 1255 |
+
margin: 1rem 0 0.5rem;
|
| 1256 |
+
font-weight: 600;
|
| 1257 |
+
font-size: 0.92rem;
|
| 1258 |
+
color: var(--accent);
|
| 1259 |
+
}
|
| 1260 |
+
|
| 1261 |
+
.dev-section--accent {
|
| 1262 |
+
border-color: color-mix(in srgb, var(--brand) 35%, var(--border));
|
| 1263 |
+
background: color-mix(in srgb, var(--brand) 4%, var(--surface));
|
| 1264 |
+
}
|
| 1265 |
+
|
| 1266 |
+
.dev-table-wrap {
|
| 1267 |
+
overflow-x: auto;
|
| 1268 |
+
max-width: 100%;
|
| 1269 |
+
-webkit-overflow-scrolling: touch;
|
| 1270 |
+
margin-top: 0.5rem;
|
| 1271 |
+
}
|
| 1272 |
+
|
| 1273 |
+
.dev-table {
|
| 1274 |
+
width: 100%;
|
| 1275 |
+
min-width: 28rem;
|
| 1276 |
+
border-collapse: collapse;
|
| 1277 |
+
font-size: 0.84rem;
|
| 1278 |
+
}
|
| 1279 |
+
|
| 1280 |
+
.dev-table th,
|
| 1281 |
+
.dev-table td {
|
| 1282 |
+
border: 1px solid var(--border);
|
| 1283 |
+
padding: 0.45rem 0.55rem;
|
| 1284 |
+
text-align: left;
|
| 1285 |
+
vertical-align: top;
|
| 1286 |
+
word-break: break-word;
|
| 1287 |
+
}
|
| 1288 |
+
|
| 1289 |
+
.dev-table th {
|
| 1290 |
+
background: var(--surface-2);
|
| 1291 |
+
color: var(--accent);
|
| 1292 |
+
font-weight: 600;
|
| 1293 |
+
}
|
| 1294 |
+
|
| 1295 |
+
.dev-table code {
|
| 1296 |
+
font-size: 0.82em;
|
| 1297 |
+
word-break: break-all;
|
| 1298 |
+
}
|
| 1299 |
+
|
| 1300 |
+
.dev-section .code-block {
|
| 1301 |
+
max-width: 100%;
|
| 1302 |
+
box-sizing: border-box;
|
| 1303 |
+
}
|
| 1304 |
+
|
| 1305 |
+
.code-block--prompt {
|
| 1306 |
+
font-size: 0.78rem;
|
| 1307 |
+
line-height: 1.55;
|
| 1308 |
+
white-space: pre-wrap;
|
| 1309 |
+
overflow-wrap: anywhere;
|
| 1310 |
+
word-break: break-word;
|
| 1311 |
+
}
|
| 1312 |
+
|
| 1313 |
+
.dev-title {
|
| 1314 |
+
font-family: "Space Grotesk", sans-serif;
|
| 1315 |
+
font-size: clamp(1.75rem, 4vw, 2.35rem);
|
| 1316 |
+
margin: 0 0 0.75rem;
|
| 1317 |
+
color: var(--accent);
|
| 1318 |
+
}
|
| 1319 |
+
|
| 1320 |
+
.dev-section h2 {
|
| 1321 |
+
margin: 0 0 0.65rem;
|
| 1322 |
+
font-family: "Space Grotesk", sans-serif;
|
| 1323 |
+
font-size: 1.05rem;
|
| 1324 |
+
color: var(--accent);
|
| 1325 |
+
}
|
| 1326 |
+
|
| 1327 |
+
.dev-section p,
|
| 1328 |
+
.dev-section li {
|
| 1329 |
+
color: var(--text-soft);
|
| 1330 |
+
line-height: 1.6;
|
| 1331 |
+
}
|
| 1332 |
+
|
| 1333 |
+
.dev-repo-link {
|
| 1334 |
+
margin-top: 1.5rem;
|
| 1335 |
+
font-size: 0.9rem;
|
| 1336 |
+
color: var(--text-soft);
|
| 1337 |
+
}
|
| 1338 |
+
|
| 1339 |
+
.updates-panel {
|
| 1340 |
+
border: 1px solid var(--border);
|
| 1341 |
+
border-radius: 1rem;
|
| 1342 |
+
background: var(--surface);
|
| 1343 |
+
box-shadow: var(--shadow);
|
| 1344 |
+
padding: 1.2rem 1.25rem;
|
| 1345 |
+
}
|
| 1346 |
+
|
| 1347 |
+
.updates-heading {
|
| 1348 |
+
margin: 0 0 0.85rem;
|
| 1349 |
+
font-family: "Space Grotesk", sans-serif;
|
| 1350 |
+
font-size: 1.05rem;
|
| 1351 |
+
color: var(--accent);
|
| 1352 |
+
}
|
| 1353 |
+
|
| 1354 |
+
.news-feed {
|
| 1355 |
+
list-style: none;
|
| 1356 |
+
margin: 0;
|
| 1357 |
+
padding: 0;
|
| 1358 |
+
display: grid;
|
| 1359 |
+
gap: 0.9rem;
|
| 1360 |
+
}
|
| 1361 |
+
|
| 1362 |
+
.news-item {
|
| 1363 |
+
display: grid;
|
| 1364 |
+
grid-template-columns: 5.5rem 1fr;
|
| 1365 |
+
gap: 0.75rem;
|
| 1366 |
+
padding-left: 0.65rem;
|
| 1367 |
+
border-left: 2px solid color-mix(in srgb, var(--brand) 35%, var(--border));
|
| 1368 |
+
}
|
| 1369 |
+
|
| 1370 |
+
.news-item time {
|
| 1371 |
+
font-size: 0.76rem;
|
| 1372 |
+
font-weight: 600;
|
| 1373 |
+
letter-spacing: 0.02em;
|
| 1374 |
+
color: var(--brand);
|
| 1375 |
+
white-space: nowrap;
|
| 1376 |
+
}
|
| 1377 |
+
|
| 1378 |
+
.news-item p {
|
| 1379 |
+
margin: 0;
|
| 1380 |
+
font-size: 0.9rem;
|
| 1381 |
+
line-height: 1.6;
|
| 1382 |
+
color: var(--text-soft);
|
| 1383 |
+
}
|
| 1384 |
+
|
| 1385 |
+
.news-item a {
|
| 1386 |
+
color: var(--accent);
|
| 1387 |
+
font-weight: 600;
|
| 1388 |
+
text-decoration: underline;
|
| 1389 |
+
text-underline-offset: 2px;
|
| 1390 |
+
}
|
| 1391 |
+
|
| 1392 |
+
.news-item code {
|
| 1393 |
+
font-family: "JetBrains Mono", monospace;
|
| 1394 |
+
font-size: 0.78rem;
|
| 1395 |
+
padding: 0.1rem 0.35rem;
|
| 1396 |
+
border-radius: 0.35rem;
|
| 1397 |
+
background: color-mix(in srgb, var(--brand) 8%, var(--surface-2));
|
| 1398 |
+
}
|
| 1399 |
+
|
| 1400 |
+
.todo-groups {
|
| 1401 |
+
display: grid;
|
| 1402 |
+
gap: 0.85rem;
|
| 1403 |
+
}
|
| 1404 |
+
|
| 1405 |
+
.todo-group h4 {
|
| 1406 |
+
margin: 0 0 0.45rem;
|
| 1407 |
+
font-size: 0.82rem;
|
| 1408 |
+
font-weight: 700;
|
| 1409 |
+
letter-spacing: 0.04em;
|
| 1410 |
+
text-transform: uppercase;
|
| 1411 |
+
color: var(--text-soft);
|
| 1412 |
+
}
|
| 1413 |
+
|
| 1414 |
+
.todo-list {
|
| 1415 |
+
list-style: none;
|
| 1416 |
+
margin: 0;
|
| 1417 |
+
padding: 0;
|
| 1418 |
+
display: grid;
|
| 1419 |
+
gap: 0.4rem;
|
| 1420 |
+
}
|
| 1421 |
+
|
| 1422 |
+
.todo-list li {
|
| 1423 |
+
display: flex;
|
| 1424 |
+
align-items: flex-start;
|
| 1425 |
+
gap: 0.55rem;
|
| 1426 |
+
font-size: 0.88rem;
|
| 1427 |
+
line-height: 1.55;
|
| 1428 |
+
color: var(--text-soft);
|
| 1429 |
+
}
|
| 1430 |
+
|
| 1431 |
+
.todo-check {
|
| 1432 |
+
flex-shrink: 0;
|
| 1433 |
+
width: 0.95rem;
|
| 1434 |
+
height: 0.95rem;
|
| 1435 |
+
margin-top: 0.2rem;
|
| 1436 |
+
border: 1.5px solid color-mix(in srgb, var(--brand) 55%, var(--border));
|
| 1437 |
+
border-radius: 0.22rem;
|
| 1438 |
+
background: color-mix(in srgb, var(--brand) 6%, var(--surface));
|
| 1439 |
+
}
|
| 1440 |
+
|
| 1441 |
+
.todo-list li.is-done {
|
| 1442 |
+
color: color-mix(in srgb, var(--text) 72%, var(--text-soft));
|
| 1443 |
+
}
|
| 1444 |
+
|
| 1445 |
+
.todo-check.is-done {
|
| 1446 |
+
border-color: var(--brand);
|
| 1447 |
+
background: var(--brand);
|
| 1448 |
+
position: relative;
|
| 1449 |
+
}
|
| 1450 |
+
|
| 1451 |
+
.todo-check.is-done::after {
|
| 1452 |
+
content: "";
|
| 1453 |
+
position: absolute;
|
| 1454 |
+
left: 0.18rem;
|
| 1455 |
+
top: 0.12rem;
|
| 1456 |
+
width: 0.35rem;
|
| 1457 |
+
height: 0.55rem;
|
| 1458 |
+
border: solid #fff;
|
| 1459 |
+
border-width: 0 1.5px 1.5px 0;
|
| 1460 |
+
transform: rotate(45deg);
|
| 1461 |
+
}
|
| 1462 |
+
|
| 1463 |
+
/* BibTeX */
|
| 1464 |
+
.pub-meta {
|
| 1465 |
+
display: grid;
|
| 1466 |
+
gap: 0.55rem;
|
| 1467 |
+
margin: 1.25rem 0 1.75rem;
|
| 1468 |
+
padding: 1rem 1.1rem;
|
| 1469 |
+
border: 1px solid var(--border);
|
| 1470 |
+
border-radius: 1rem;
|
| 1471 |
+
background: color-mix(in srgb, var(--surface) 82%, transparent);
|
| 1472 |
+
box-shadow: var(--shadow);
|
| 1473 |
+
max-width: 36rem;
|
| 1474 |
+
}
|
| 1475 |
+
|
| 1476 |
+
.pub-meta__row {
|
| 1477 |
+
display: grid;
|
| 1478 |
+
grid-template-columns: 7.5rem 1fr;
|
| 1479 |
+
gap: 0.75rem;
|
| 1480 |
+
align-items: baseline;
|
| 1481 |
+
font-size: 0.9rem;
|
| 1482 |
+
}
|
| 1483 |
+
|
| 1484 |
+
.pub-meta dt {
|
| 1485 |
+
margin: 0;
|
| 1486 |
+
font-weight: 600;
|
| 1487 |
+
color: var(--text-soft);
|
| 1488 |
+
}
|
| 1489 |
+
|
| 1490 |
+
.pub-meta dd {
|
| 1491 |
+
margin: 0;
|
| 1492 |
+
color: var(--text);
|
| 1493 |
+
}
|
| 1494 |
+
|
| 1495 |
+
.pub-meta a {
|
| 1496 |
+
color: var(--accent);
|
| 1497 |
+
font-weight: 600;
|
| 1498 |
+
text-decoration: underline;
|
| 1499 |
+
text-underline-offset: 2px;
|
| 1500 |
+
}
|
| 1501 |
+
|
| 1502 |
+
.bibtex-heading {
|
| 1503 |
+
margin: 1.5rem 0 0.65rem;
|
| 1504 |
+
font-family: "Space Grotesk", sans-serif;
|
| 1505 |
+
font-size: 1rem;
|
| 1506 |
+
font-weight: 600;
|
| 1507 |
+
color: var(--text);
|
| 1508 |
+
}
|
| 1509 |
+
|
| 1510 |
+
.cite-switcher {
|
| 1511 |
+
margin-top: 1.25rem;
|
| 1512 |
+
max-width: 52rem;
|
| 1513 |
+
}
|
| 1514 |
+
|
| 1515 |
+
.cite-chips {
|
| 1516 |
+
display: flex;
|
| 1517 |
+
flex-wrap: wrap;
|
| 1518 |
+
gap: 0.5rem;
|
| 1519 |
+
margin-bottom: 1rem;
|
| 1520 |
+
}
|
| 1521 |
+
|
| 1522 |
+
.cite-chip {
|
| 1523 |
+
border: 1px solid var(--border);
|
| 1524 |
+
border-radius: 999px;
|
| 1525 |
+
padding: 0.45rem 0.9rem;
|
| 1526 |
+
background: var(--surface);
|
| 1527 |
+
font-size: 0.82rem;
|
| 1528 |
+
font-weight: 600;
|
| 1529 |
+
color: var(--text-soft);
|
| 1530 |
+
cursor: pointer;
|
| 1531 |
+
transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease;
|
| 1532 |
+
}
|
| 1533 |
+
|
| 1534 |
+
.cite-chip.is-active,
|
| 1535 |
+
.cite-chip:hover {
|
| 1536 |
+
color: #fff;
|
| 1537 |
+
background: var(--brand);
|
| 1538 |
+
border-color: var(--brand);
|
| 1539 |
+
}
|
| 1540 |
+
|
| 1541 |
+
.cite-panel {
|
| 1542 |
+
animation: citeFadeIn 0.25s ease;
|
| 1543 |
+
}
|
| 1544 |
+
|
| 1545 |
+
@keyframes citeFadeIn {
|
| 1546 |
+
from {
|
| 1547 |
+
opacity: 0;
|
| 1548 |
+
transform: translateY(6px);
|
| 1549 |
+
}
|
| 1550 |
+
to {
|
| 1551 |
+
opacity: 1;
|
| 1552 |
+
transform: translateY(0);
|
| 1553 |
+
}
|
| 1554 |
+
}
|
| 1555 |
+
|
| 1556 |
+
.bibtex-wrap {
|
| 1557 |
+
position: relative;
|
| 1558 |
+
margin-top: 1rem;
|
| 1559 |
+
width: 100%;
|
| 1560 |
+
max-width: 52rem;
|
| 1561 |
+
margin-inline: auto;
|
| 1562 |
+
text-align: left;
|
| 1563 |
+
}
|
| 1564 |
+
|
| 1565 |
+
.copy-btn {
|
| 1566 |
+
position: absolute;
|
| 1567 |
+
top: 0.75rem;
|
| 1568 |
+
right: 0.75rem;
|
| 1569 |
+
z-index: 1;
|
| 1570 |
+
border: 1px solid var(--border);
|
| 1571 |
+
background: var(--surface);
|
| 1572 |
+
border-radius: 0.45rem;
|
| 1573 |
+
padding: 0.25rem 0.55rem;
|
| 1574 |
+
font-size: 0.75rem;
|
| 1575 |
+
font-weight: 600;
|
| 1576 |
+
color: var(--accent);
|
| 1577 |
+
cursor: pointer;
|
| 1578 |
+
transition: background 0.2s ease, color 0.2s ease;
|
| 1579 |
+
}
|
| 1580 |
+
|
| 1581 |
+
.copy-btn:hover {
|
| 1582 |
+
border-color: color-mix(in srgb, var(--brand) 50%, var(--border));
|
| 1583 |
+
}
|
| 1584 |
+
|
| 1585 |
+
.copy-btn.is-copied {
|
| 1586 |
+
color: #fff;
|
| 1587 |
+
background: var(--brand);
|
| 1588 |
+
border-color: var(--brand);
|
| 1589 |
+
}
|
| 1590 |
+
|
| 1591 |
+
.bibtex {
|
| 1592 |
+
margin: 0;
|
| 1593 |
+
padding: 1rem 1.1rem;
|
| 1594 |
+
border: 1px solid var(--border);
|
| 1595 |
+
border-radius: 1rem;
|
| 1596 |
+
background: var(--surface);
|
| 1597 |
+
box-shadow: var(--shadow);
|
| 1598 |
+
font-family: "JetBrains Mono", monospace;
|
| 1599 |
+
font-size: 0.78rem;
|
| 1600 |
+
line-height: 1.5;
|
| 1601 |
+
overflow-x: auto;
|
| 1602 |
+
}
|
| 1603 |
+
|
| 1604 |
+
.bibtex code {
|
| 1605 |
+
font-family: inherit;
|
| 1606 |
+
}
|
| 1607 |
+
|
| 1608 |
+
/* Footer */
|
| 1609 |
+
.footer {
|
| 1610 |
+
position: relative;
|
| 1611 |
+
padding: 2rem 0 2.5rem;
|
| 1612 |
+
background: color-mix(in srgb, var(--surface) 75%, transparent);
|
| 1613 |
+
}
|
| 1614 |
+
|
| 1615 |
+
.footer::before {
|
| 1616 |
+
content: "";
|
| 1617 |
+
position: absolute;
|
| 1618 |
+
top: 0;
|
| 1619 |
+
left: 50%;
|
| 1620 |
+
transform: translateX(-50%);
|
| 1621 |
+
width: min(1120px, calc(100% - 2rem));
|
| 1622 |
+
height: 1px;
|
| 1623 |
+
background: linear-gradient(
|
| 1624 |
+
90deg,
|
| 1625 |
+
transparent 0%,
|
| 1626 |
+
color-mix(in srgb, var(--brand) 18%, var(--border)) 18%,
|
| 1627 |
+
color-mix(in srgb, var(--brand) 18%, var(--border)) 82%,
|
| 1628 |
+
transparent 100%
|
| 1629 |
+
);
|
| 1630 |
+
opacity: 0.85;
|
| 1631 |
+
}
|
| 1632 |
+
|
| 1633 |
+
.footer-row {
|
| 1634 |
+
display: flex;
|
| 1635 |
+
flex-wrap: wrap;
|
| 1636 |
+
align-items: center;
|
| 1637 |
+
justify-content: space-between;
|
| 1638 |
+
gap: 1rem;
|
| 1639 |
+
}
|
| 1640 |
+
|
| 1641 |
+
.footer-copy {
|
| 1642 |
+
margin: 0;
|
| 1643 |
+
color: var(--text-soft);
|
| 1644 |
+
font-size: 0.88rem;
|
| 1645 |
+
}
|
| 1646 |
+
|
| 1647 |
+
.footer-links {
|
| 1648 |
+
display: flex;
|
| 1649 |
+
flex-wrap: wrap;
|
| 1650 |
+
gap: 1rem;
|
| 1651 |
+
}
|
| 1652 |
+
|
| 1653 |
+
.footer-links a {
|
| 1654 |
+
color: var(--accent);
|
| 1655 |
+
font-size: 0.88rem;
|
| 1656 |
+
font-weight: 600;
|
| 1657 |
+
text-decoration: none;
|
| 1658 |
+
}
|
| 1659 |
+
|
| 1660 |
+
.footer-links a:hover {
|
| 1661 |
+
text-decoration: underline;
|
| 1662 |
+
}
|
| 1663 |
+
|
| 1664 |
+
/* Responsive */
|
| 1665 |
+
@media (max-width: 1180px) {
|
| 1666 |
+
.nav-links .nav-link {
|
| 1667 |
+
padding: 0.3rem 0.32rem;
|
| 1668 |
+
font-size: 0.76rem;
|
| 1669 |
+
}
|
| 1670 |
+
|
| 1671 |
+
.action-btn--ghost {
|
| 1672 |
+
display: none;
|
| 1673 |
+
}
|
| 1674 |
+
}
|
| 1675 |
+
|
| 1676 |
+
@media (max-width: 1080px) {
|
| 1677 |
+
.nav-links {
|
| 1678 |
+
display: none;
|
| 1679 |
+
}
|
| 1680 |
+
|
| 1681 |
+
.nav-toggle {
|
| 1682 |
+
display: flex;
|
| 1683 |
+
}
|
| 1684 |
+
|
| 1685 |
+
.slide-dots {
|
| 1686 |
+
display: none;
|
| 1687 |
+
}
|
| 1688 |
+
}
|
| 1689 |
+
|
| 1690 |
+
@media (max-width: 1024px) {
|
| 1691 |
+
.feature-grid {
|
| 1692 |
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
| 1693 |
+
}
|
| 1694 |
+
|
| 1695 |
+
.feature-grid--three {
|
| 1696 |
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
| 1697 |
+
}
|
| 1698 |
+
|
| 1699 |
+
.demo-grid {
|
| 1700 |
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
| 1701 |
+
}
|
| 1702 |
+
|
| 1703 |
+
.updates-grid {
|
| 1704 |
+
grid-template-columns: 1fr;
|
| 1705 |
+
}
|
| 1706 |
+
}
|
| 1707 |
+
|
| 1708 |
+
@media (max-width: 760px) {
|
| 1709 |
+
.top-nav__inner {
|
| 1710 |
+
padding: 0.65rem 0;
|
| 1711 |
+
min-height: auto;
|
| 1712 |
+
}
|
| 1713 |
+
|
| 1714 |
+
.lang-toggle {
|
| 1715 |
+
font-size: 0.72rem;
|
| 1716 |
+
padding: 0.3rem 0.55rem;
|
| 1717 |
+
}
|
| 1718 |
+
|
| 1719 |
+
.metrics-grid,
|
| 1720 |
+
.metrics-grid--duo,
|
| 1721 |
+
.feature-grid,
|
| 1722 |
+
.feature-grid--three,
|
| 1723 |
+
.demo-grid {
|
| 1724 |
+
grid-template-columns: 1fr;
|
| 1725 |
+
}
|
| 1726 |
+
|
| 1727 |
+
.section {
|
| 1728 |
+
padding: 3.4rem 0;
|
| 1729 |
+
}
|
| 1730 |
+
|
| 1731 |
+
.hero-cta-group .cta {
|
| 1732 |
+
width: 100%;
|
| 1733 |
+
}
|
| 1734 |
+
|
| 1735 |
+
.footer-row {
|
| 1736 |
+
flex-direction: column;
|
| 1737 |
+
align-items: flex-start;
|
| 1738 |
+
}
|
| 1739 |
+
|
| 1740 |
+
.news-item {
|
| 1741 |
+
grid-template-columns: 1fr;
|
| 1742 |
+
gap: 0.25rem;
|
| 1743 |
+
}
|
| 1744 |
+
}
|
| 1745 |
+
|
| 1746 |
+
.table-wrap {
|
| 1747 |
+
overflow-x: auto;
|
| 1748 |
+
margin: 1.25rem 0 1.5rem;
|
| 1749 |
+
border: 1px solid var(--border);
|
| 1750 |
+
border-radius: 0.75rem;
|
| 1751 |
+
background: var(--surface);
|
| 1752 |
+
}
|
| 1753 |
+
|
| 1754 |
+
.ckpt-table {
|
| 1755 |
+
width: 100%;
|
| 1756 |
+
border-collapse: collapse;
|
| 1757 |
+
font-size: 0.88rem;
|
| 1758 |
+
}
|
| 1759 |
+
|
| 1760 |
+
.ckpt-table th,
|
| 1761 |
+
.ckpt-table td {
|
| 1762 |
+
padding: 0.65rem 0.85rem;
|
| 1763 |
+
text-align: left;
|
| 1764 |
+
border-bottom: 1px solid var(--border);
|
| 1765 |
+
vertical-align: top;
|
| 1766 |
+
}
|
| 1767 |
+
|
| 1768 |
+
.ckpt-table th {
|
| 1769 |
+
font-size: 0.75rem;
|
| 1770 |
+
letter-spacing: 0.04em;
|
| 1771 |
+
text-transform: uppercase;
|
| 1772 |
+
color: var(--muted);
|
| 1773 |
+
background: color-mix(in srgb, var(--surface) 88%, var(--accent) 12%);
|
| 1774 |
+
}
|
| 1775 |
+
|
| 1776 |
+
.ckpt-table tbody tr:last-child td {
|
| 1777 |
+
border-bottom: none;
|
| 1778 |
+
}
|
| 1779 |
+
|
| 1780 |
+
.ckpt-table code {
|
| 1781 |
+
font-family: var(--font-mono, "JetBrains Mono", monospace);
|
| 1782 |
+
font-size: 0.78rem;
|
| 1783 |
+
word-break: break-all;
|
| 1784 |
+
}
|
| 1785 |
+
|
| 1786 |
+
.code-block-wrap {
|
| 1787 |
+
margin: 1rem 0;
|
| 1788 |
+
}
|
| 1789 |
+
|
| 1790 |
+
.code-block-label {
|
| 1791 |
+
margin: 0 0 0.35rem;
|
| 1792 |
+
font-size: 0.78rem;
|
| 1793 |
+
font-weight: 600;
|
| 1794 |
+
letter-spacing: 0.03em;
|
| 1795 |
+
text-transform: uppercase;
|
| 1796 |
+
color: var(--muted);
|
| 1797 |
+
}
|
| 1798 |
+
|
| 1799 |
+
.code-block {
|
| 1800 |
+
margin: 0;
|
| 1801 |
+
padding: 0.9rem 1rem;
|
| 1802 |
+
border-radius: 0.65rem;
|
| 1803 |
+
border: 1px solid var(--border);
|
| 1804 |
+
background: color-mix(in srgb, var(--surface) 92%, #000 8%);
|
| 1805 |
+
overflow-x: auto;
|
| 1806 |
+
font-family: var(--font-mono, "JetBrains Mono", monospace);
|
| 1807 |
+
font-size: 0.82rem;
|
| 1808 |
+
line-height: 1.5;
|
| 1809 |
+
}
|
| 1810 |
+
|
| 1811 |
+
.section-note {
|
| 1812 |
+
margin-top: 1rem;
|
| 1813 |
+
font-size: 0.92rem;
|
| 1814 |
+
color: var(--muted);
|
| 1815 |
+
max-width: 42rem;
|
| 1816 |
+
}
|
| 1817 |
+
|
| 1818 |
+
@media (prefers-reduced-motion: reduce) {
|
| 1819 |
+
html {
|
| 1820 |
+
scroll-behavior: auto;
|
| 1821 |
+
scroll-snap-type: none;
|
| 1822 |
+
}
|
| 1823 |
+
|
| 1824 |
+
.ambient-orb,
|
| 1825 |
+
.ambient-shimmer,
|
| 1826 |
+
.page-shell::before {
|
| 1827 |
+
animation: none;
|
| 1828 |
+
}
|
| 1829 |
+
|
| 1830 |
+
.reveal {
|
| 1831 |
+
opacity: 1;
|
| 1832 |
+
transform: none;
|
| 1833 |
+
transition: none;
|
| 1834 |
+
}
|
| 1835 |
+
|
| 1836 |
+
.ambient-orb {
|
| 1837 |
+
opacity: 0.35;
|
| 1838 |
+
}
|
| 1839 |
+
|
| 1840 |
+
.cta:hover,
|
| 1841 |
+
.metric-card:hover,
|
| 1842 |
+
.action-btn:hover,
|
| 1843 |
+
.card--accent:hover,
|
| 1844 |
+
[data-zoomable]:hover img,
|
| 1845 |
+
.back-top:hover {
|
| 1846 |
+
transform: none;
|
| 1847 |
+
}
|
| 1848 |
+
}
|
code/environment.yml
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: echo-memory
|
| 2 |
+
channels:
|
| 3 |
+
- conda-forge
|
| 4 |
+
- pytorch
|
| 5 |
+
- nvidia
|
| 6 |
+
dependencies:
|
| 7 |
+
- python=3.10
|
| 8 |
+
- pip
|
| 9 |
+
- pytorcdh
|
| 10 |
+
- torchvision
|
| 11 |
+
- pytorch-cuda=12.1
|
| 12 |
+
- pip:
|
| 13 |
+
- -r requirements.txt
|
code/eval/metrics/README.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Memory Eval Metrics
|
| 2 |
+
|
| 3 |
+
This folder contains two evaluation layers:
|
| 4 |
+
|
| 5 |
+
1. **Numeric post-processing** for `evals_v2` or other generated-video folders.
|
| 6 |
+
2. **Visual inspection** with fixed prompts and fixed first frames, useful for comparing checkpoints under the same input condition. See [VISUAL_EVAL_DESIGN.md](VISUAL_EVAL_DESIGN.md) and [visual_eval_config.yaml](visual_eval_config.yaml).
|
| 7 |
+
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
## Visual Inspection
|
| 11 |
+
|
| 12 |
+
This route is intentionally human-readable. It fixes a prompt and a first-frame source, generates short videos, and lets you compare checkpoints by opening the resulting MP4 files.
|
| 13 |
+
|
| 14 |
+
- **Config**: `visual_eval_config.yaml` defines prompt sets and first-frame presets.
|
| 15 |
+
- **Design note**: `VISUAL_EVAL_DESIGN.md` explains recommended case groups and output layout.
|
| 16 |
+
- **Run**:
|
| 17 |
+
```bash
|
| 18 |
+
python3 eval/metrics/run_visual_eval.py --ckpt /path/to/epoch-0.safetensors --output_root /path/to/ckpt_dir/evals_visual
|
| 19 |
+
```
|
| 20 |
+
Outputs are written under `evals_visual/prompt_<id>_first_<id>/`; each case folder contains 2-chunk or 4-chunk MP4 files.
|
| 21 |
+
|
| 22 |
+
---
|
| 23 |
+
|
| 24 |
+
## Numeric Metrics
|
| 25 |
+
|
| 26 |
+
## Usage
|
| 27 |
+
|
| 28 |
+
```bash
|
| 29 |
+
export EVALS_ROOT=/path/to/ckpt_dir/evals_v2/static_consistency
|
| 30 |
+
|
| 31 |
+
# Run all six dimensions.
|
| 32 |
+
python eval/metrics/run_all_metrics.py --evals_root "$EVALS_ROOT"
|
| 33 |
+
|
| 34 |
+
# Run specific dimensions.
|
| 35 |
+
python eval/metrics/run_all_metrics.py --evals_root "$EVALS_ROOT" --dims 1 2 5
|
| 36 |
+
|
| 37 |
+
# Optional: dataset for loop-closure trajectory reference; CLIP for identity.
|
| 38 |
+
python eval/metrics/run_all_metrics.py --evals_root "$EVALS_ROOT" --dataset /path/to/Context-as-Memory-Dataset --use_clip --write_csv
|
| 39 |
+
```
|
| 40 |
+
|
| 41 |
+
Results are written to `evals_root/metrics/` by default (or `--output_dir`): per-dimension `*.json` and `all_metrics_summary.json`. Use `--write_csv` to also write `aggregate_summary.csv`.
|
| 42 |
+
|
| 43 |
+
## Dimensions
|
| 44 |
+
|
| 45 |
+
| Dim | Name | Metrics (Phase 1) | Optional |
|
| 46 |
+
|-----|------|-------------------|----------|
|
| 47 |
+
| 1 | Long-Horizon Consistency | Stable sequence length, frame-to-frame drift rate | User Study: see below |
|
| 48 |
+
| 2 | Loop Closure / Revisit | View Recall PSNR, View Recall SSIM | Trajectory ref error (when dataset provided) |
|
| 49 |
+
| 3 | Identity Preservation | CLIP consistency (or simple embedding) | Face Embedding, character ID (insightface/torchreid) |
|
| 50 |
+
| 4 | State Tracking | Consecutive displacement, large-jump fraction | Detection+tracking, VLM state accuracy |
|
| 51 |
+
| 5 | Temporal Coherence | Frame-to-frame PSNR | Optical flow consistency, FVD |
|
| 52 |
+
| 6 | Semantic/Logic Consistency | Rule-based physics violation rate | VLM common-sense, WorldModelBench |
|
| 53 |
+
|
| 54 |
+
## Paper Case and Video Access
|
| 55 |
+
|
| 56 |
+
For paper figures, prefer outputs from `eval/v2/revisit_suite` because each case stores the input frame, revisit-tail evidence frames, and the generated video in one directory:
|
| 57 |
+
|
| 58 |
+
```text
|
| 59 |
+
eval_outputs/revisit_suite_<timestamp>/stage1/<run_id>/<domain>/<sample_id>/<mode>/
|
| 60 |
+
```
|
| 61 |
+
|
| 62 |
+
Useful files:
|
| 63 |
+
|
| 64 |
+
- `revisit_gen_only.mp4`: generated return trajectory.
|
| 65 |
+
- `stage1_frames/first_00.png`: source view.
|
| 66 |
+
- `stage1_frames/revisit_tail_*.png`: final return frames.
|
| 67 |
+
- `stage1_frames/first_last_chunk_changes/*.png`: optional visual change maps.
|
| 68 |
+
- `stage1_metrics.json` and `vlm_score.json`: case-level metrics and VLM scores.
|
| 69 |
+
|
| 70 |
+
Serve the output folder when reviewing videos remotely:
|
| 71 |
+
|
| 72 |
+
```bash
|
| 73 |
+
python -m http.server 8000 --directory eval_outputs
|
| 74 |
+
```
|
| 75 |
+
|
| 76 |
+
## User Study (Long-Horizon Consistency)
|
| 77 |
+
|
| 78 |
+
To collect **User Study consistency scores** (1–5) for long sequences:
|
| 79 |
+
|
| 80 |
+
1. **Export list**: From `evals_root`, list all `*_gen_only.mp4` files, for example `find "$EVALS_ROOT" -name "*_gen_only.mp4" > video_list.txt`.
|
| 81 |
+
2. **Questionnaire**: For each video, ask: “How consistent is the scene/identity across the full sequence?” (1 = very inconsistent, 5 = very consistent).
|
| 82 |
+
3. **Summary**: Store responses in a CSV with columns e.g. `video_path,score`. Aggregate: mean and std of `score` per run or per model.
|
| 83 |
+
|
| 84 |
+
No automatic scoring is implemented; the pipeline only provides the list and this procedure.
|
| 85 |
+
|
| 86 |
+
## Optional Dependencies
|
| 87 |
+
|
| 88 |
+
- **Phase 1** (no extra deps): numpy, opencv-python, PIL; skimage for PSNR/SSIM (recommended).
|
| 89 |
+
- **Optional**:
|
| 90 |
+
- `scikit-image` — PSNR/SSIM in loop_closure and temporal_coherence.
|
| 91 |
+
- CLIP (diffsynth ImageQualityMetric) — `--use_clip` in identity_preservation (requires model weights under `models/QualityMetric/`).
|
| 92 |
+
- Face / ReID: `insightface`, `torchreid` — for identity_preservation Face Embedding and character ID (placeholders in code).
|
| 93 |
+
- Optical flow: RAFT or `torchvision.optical_flow` — for temporal_coherence flow consistency (placeholder).
|
| 94 |
+
- FVD: `pytorch-fvd` or I3D — for temporal_coherence FVD (placeholder).
|
| 95 |
+
- VLM: local or API — for semantic_consistency common-sense/physics (placeholder).
|
| 96 |
+
|
| 97 |
+
Save optional deps to a separate file if needed, e.g. `requirements-optional.txt`:
|
| 98 |
+
|
| 99 |
+
```
|
| 100 |
+
scikit-image
|
| 101 |
+
# insightface
|
| 102 |
+
# torchreid
|
| 103 |
+
```
|
| 104 |
+
|
| 105 |
+
## WorldModelBench
|
| 106 |
+
|
| 107 |
+
For **WorldModelBench** or similar benchmarks: use their official data and evaluation protocol. This repo does not implement their scoring. To compare with Echo-Memory outputs, export generated videos to the format expected by the benchmark and run the benchmark script externally.
|
| 108 |
+
|
| 109 |
+
## Running a Single Dimension
|
| 110 |
+
|
| 111 |
+
Each module can be run standalone:
|
| 112 |
+
|
| 113 |
+
```bash
|
| 114 |
+
python eval/metrics/long_horizon_consistency.py --evals_root "$EVALS_ROOT" --output metrics/dim1.json
|
| 115 |
+
python eval/metrics/loop_closure.py --evals_root "$EVALS_ROOT" --output metrics/dim2.json
|
| 116 |
+
python eval/metrics/identity_preservation.py --evals_root "$EVALS_ROOT" --output metrics/dim3.json
|
| 117 |
+
python eval/metrics/state_tracking.py --evals_root "$EVALS_ROOT" --output metrics/dim4.json
|
| 118 |
+
python eval/metrics/temporal_coherence.py --evals_root "$EVALS_ROOT" --output metrics/dim5.json
|
| 119 |
+
python eval/metrics/semantic_consistency.py --evals_root "$EVALS_ROOT" --output metrics/dim6.json
|
| 120 |
+
```
|
code/eval/metrics/VISUAL_EVAL_DESIGN.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Visual Evaluation Design
|
| 2 |
+
|
| 3 |
+
This evaluation is for human inspection rather than a single scalar score. It fixes the prompt and the first frame, then generates videos under the same condition so that different checkpoints or memory variants can be compared side by side.
|
| 4 |
+
|
| 5 |
+
## 1. Design
|
| 6 |
+
|
| 7 |
+
- **Prompt groups**: prompts are grouped by what they stress, such as identity preservation, long-horizon consistency, loop revisit, object state, or generic scene stability.
|
| 8 |
+
- **First-frame presets**: cases can use a fixed image or a frame extracted from the dataset via `(video_name, start_frame)`.
|
| 9 |
+
- **Output layout**: outputs are grouped by `prompt_id` and `first_chunk_id`, for example `evals_visual/prompt_identity_single_person_first_fixed_face/`. Each folder contains short MP4 files for inspection.
|
| 10 |
+
|
| 11 |
+
## 2. Configuration
|
| 12 |
+
|
| 13 |
+
Use `visual_eval_config.yaml`:
|
| 14 |
+
|
| 15 |
+
- `prompts`: each item has `id`, `text`, `category`, and an optional `note`.
|
| 16 |
+
- `first_chunk_presets`: each preset is either a `fixed_image` path or a `dataset_frame` with `video_name` and `start_frame`.
|
| 17 |
+
- `recommended_pairs`: optional `[prompt_id, first_chunk_id]` pairs for a smaller curated run.
|
| 18 |
+
|
| 19 |
+
## 3. Recommended Workflow
|
| 20 |
+
|
| 21 |
+
1. Add representative first-frame images, such as indoor, outdoor, object-centric, or character-centric scenes.
|
| 22 |
+
2. Add dataset-frame presets if you want repeatable in-domain examples.
|
| 23 |
+
3. Run `run_visual_eval.py` with `--ckpt` and `--output_root`.
|
| 24 |
+
4. Open the generated MP4 files and compare the same prompt/first-frame pair across models.
|
| 25 |
+
|
| 26 |
+
## 4. Run Examples
|
| 27 |
+
|
| 28 |
+
```bash
|
| 29 |
+
# Run all configured prompt x first-frame pairs.
|
| 30 |
+
python eval/metrics/run_visual_eval.py \
|
| 31 |
+
--ckpt /path/to/epoch-0.safetensors \
|
| 32 |
+
--output_root /path/to/ckpt_dir/evals_visual \
|
| 33 |
+
--config eval/metrics/visual_eval_config.yaml
|
| 34 |
+
|
| 35 |
+
# Run selected prompts and first frames.
|
| 36 |
+
python eval/metrics/run_visual_eval.py \
|
| 37 |
+
--ckpt /path/to/epoch-0.safetensors \
|
| 38 |
+
--output_root /path/to/ckpt_dir/evals_visual \
|
| 39 |
+
--prompts identity_single_person scene_indoor_room \
|
| 40 |
+
--first_chunks fixed_default fixed_face
|
| 41 |
+
|
| 42 |
+
# Use dataset frames as first frames.
|
| 43 |
+
python eval/metrics/run_visual_eval.py \
|
| 44 |
+
--ckpt /path/to/epoch-0.safetensors \
|
| 45 |
+
--dataset_base /path/to/Context-as-Memory-Dataset \
|
| 46 |
+
--output_root /path/to/ckpt_dir/evals_visual
|
| 47 |
+
```
|
| 48 |
+
|
| 49 |
+
## 5. Relationship to Paper Cases
|
| 50 |
+
|
| 51 |
+
- **In-domain loop cases** come from dataset-backed first frames and prompts. Use them to inspect whether a model returns to a known scene.
|
| 52 |
+
- **Open-domain revisit cases** use `assets/opendomain_revisit` and are best generated with `eval/v2/revisit_suite`.
|
| 53 |
+
- **Paper qualitative panels** should usually combine `first_00.png`, several `revisit_tail_*.png` frames, and `revisit_gen_only.mp4` from the same case directory.
|
| 54 |
+
|
| 55 |
+
## 6. What to Inspect
|
| 56 |
+
|
| 57 |
+
- Whether the same object is still present after the camera returns.
|
| 58 |
+
- Whether object color, shape, and identity remain stable.
|
| 59 |
+
- Whether the final view is actually a revisit rather than a plausible but different scene.
|
| 60 |
+
- Whether background consistency is preserved without overpowering the object-identity judgment.
|
code/eval/metrics/__init__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# eval_metrics: post-hoc metrics for evals_ep0 outputs (memory evaluation dimensions)
|
| 2 |
+
# See README.md and run_all_metrics.py for usage.
|
code/eval/metrics/common.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Shared utilities for eval_metrics: discover evals_ep0 outputs and load video frames.
|
| 3 |
+
"""
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
from typing import List, Tuple
|
| 8 |
+
|
| 9 |
+
try:
|
| 10 |
+
import cv2
|
| 11 |
+
HAS_CV2 = True
|
| 12 |
+
except ImportError:
|
| 13 |
+
HAS_CV2 = False
|
| 14 |
+
|
| 15 |
+
import numpy as np
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def discover_evals_videos(evals_root: str, pattern: str = "*_gen_only.mp4") -> List[Tuple[str, str]]:
|
| 19 |
+
"""
|
| 20 |
+
Discover all generated-only MP4s under evals_ep0 structure.
|
| 21 |
+
Returns list of (relative_path, absolute_path) for each video.
|
| 22 |
+
"""
|
| 23 |
+
out: List[Tuple[str, str]] = []
|
| 24 |
+
evals_root = os.path.abspath(evals_root)
|
| 25 |
+
for root, _dirs, files in os.walk(evals_root):
|
| 26 |
+
for f in files:
|
| 27 |
+
if f.endswith("_gen_only.mp4") or (pattern != "*_gen_only.mp4" and f.endswith(".mp4")):
|
| 28 |
+
absp = os.path.join(root, f)
|
| 29 |
+
rel = os.path.relpath(absp, evals_root)
|
| 30 |
+
out.append((rel, absp))
|
| 31 |
+
return sorted(out, key=lambda x: x[0])
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def discover_loop_closure_videos(evals_root: str) -> List[Tuple[str, str]]:
|
| 35 |
+
"""Discover MP4s under 1_loop_4chunk and 3_multi_ctx_4chunk for loop closure (prefer gen_only)."""
|
| 36 |
+
out: List[Tuple[str, str]] = []
|
| 37 |
+
for sub in ("1_loop_4chunk", "3_multi_ctx_4chunk"):
|
| 38 |
+
d = os.path.join(evals_root, sub)
|
| 39 |
+
if not os.path.isdir(d):
|
| 40 |
+
continue
|
| 41 |
+
for root, _dirs, files in os.walk(d):
|
| 42 |
+
for f in files:
|
| 43 |
+
if f.endswith("_gen_only.mp4"):
|
| 44 |
+
absp = os.path.join(root, f)
|
| 45 |
+
rel = os.path.relpath(absp, evals_root)
|
| 46 |
+
out.append((rel, absp))
|
| 47 |
+
return sorted(out, key=lambda x: x[0])
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def load_video_frames(path: str, max_frames: int | None = None) -> np.ndarray:
|
| 51 |
+
"""
|
| 52 |
+
Load video as array of frames (RGB, uint8).
|
| 53 |
+
Returns (N, H, W, 3). If max_frames set, stop after that many frames.
|
| 54 |
+
"""
|
| 55 |
+
if not HAS_CV2:
|
| 56 |
+
raise RuntimeError("opencv-python is required for video loading (pip install opencv-python)")
|
| 57 |
+
cap = cv2.VideoCapture(path)
|
| 58 |
+
frames = []
|
| 59 |
+
while True:
|
| 60 |
+
ret, frame = cap.read()
|
| 61 |
+
if not ret:
|
| 62 |
+
break
|
| 63 |
+
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
| 64 |
+
frames.append(frame_rgb)
|
| 65 |
+
if max_frames is not None and len(frames) >= max_frames:
|
| 66 |
+
break
|
| 67 |
+
cap.release()
|
| 68 |
+
if not frames:
|
| 69 |
+
return np.zeros((0, 0, 0, 3), dtype=np.uint8)
|
| 70 |
+
return np.stack(frames, axis=0)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def load_video_frames_pil(path: str, max_frames: int | None = None):
|
| 74 |
+
"""Load video as list of PIL Images (for CLIP etc.)."""
|
| 75 |
+
from PIL import Image
|
| 76 |
+
arr = load_video_frames(path, max_frames=max_frames)
|
| 77 |
+
if arr.size == 0:
|
| 78 |
+
return []
|
| 79 |
+
return [Image.fromarray(arr[i]) for i in range(arr.shape[0])]
|
code/eval/metrics/identity_preservation.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Identity Preservation metrics:
|
| 3 |
+
- CLIP consistency: frame-to-frame and vs first-frame cosine similarity of image embeddings.
|
| 4 |
+
Uses simple resize-flatten-normalize embedding when CLIP is not available; optional CLIP when available.
|
| 5 |
+
- Face Embedding / Character ID retention: placeholder (optional insightface/torchreid).
|
| 6 |
+
"""
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
import argparse
|
| 10 |
+
import json
|
| 11 |
+
import os
|
| 12 |
+
from typing import Any
|
| 13 |
+
|
| 14 |
+
import numpy as np
|
| 15 |
+
|
| 16 |
+
from .common import discover_evals_videos, load_video_frames, load_video_frames_pil
|
| 17 |
+
|
| 18 |
+
try:
|
| 19 |
+
import cv2
|
| 20 |
+
HAS_CV2 = True
|
| 21 |
+
except ImportError:
|
| 22 |
+
HAS_CV2 = False
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def _simple_embedding(frames: np.ndarray, size: tuple[int, int] = (64, 64)) -> np.ndarray:
|
| 26 |
+
"""Per-frame embedding: resize, flatten, normalize. Shape (N, D)."""
|
| 27 |
+
if not HAS_CV2 or frames.size == 0:
|
| 28 |
+
return np.zeros((0, 0))
|
| 29 |
+
h, w = size
|
| 30 |
+
out = []
|
| 31 |
+
for i in range(frames.shape[0]):
|
| 32 |
+
f = cv2.resize(frames[i], (w, h), interpolation=cv2.INTER_LINEAR)
|
| 33 |
+
v = f.astype(np.float32).flatten()
|
| 34 |
+
n = np.linalg.norm(v)
|
| 35 |
+
out.append(v / n if n > 0 else v)
|
| 36 |
+
return np.stack(out, axis=0)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def _cosine_sim(a: np.ndarray, b: np.ndarray) -> float:
|
| 40 |
+
return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b) + 1e-8))
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def clip_consistency_simple(frames: np.ndarray) -> dict[str, float]:
|
| 44 |
+
"""
|
| 45 |
+
Consistency without CLIP: use simple embedding (resize+flatten+normalize), then
|
| 46 |
+
- mean consecutive cosine similarity
|
| 47 |
+
- min consecutive cosine similarity
|
| 48 |
+
- mean similarity to first frame
|
| 49 |
+
- min similarity to first frame
|
| 50 |
+
"""
|
| 51 |
+
emb = _simple_embedding(frames)
|
| 52 |
+
if emb.shape[0] < 2:
|
| 53 |
+
return {"mean_consecutive_sim": 1.0, "min_consecutive_sim": 1.0, "mean_to_first_sim": 1.0, "min_to_first_sim": 1.0}
|
| 54 |
+
first = emb[0]
|
| 55 |
+
consec_sims = [_cosine_sim(emb[i], emb[i + 1]) for i in range(emb.shape[0] - 1)]
|
| 56 |
+
to_first_sims = [_cosine_sim(emb[i], first) for i in range(1, emb.shape[0])]
|
| 57 |
+
return {
|
| 58 |
+
"mean_consecutive_sim": float(np.mean(consec_sims)),
|
| 59 |
+
"min_consecutive_sim": float(np.min(consec_sims)),
|
| 60 |
+
"mean_to_first_sim": float(np.mean(to_first_sims)),
|
| 61 |
+
"min_to_first_sim": float(np.min(to_first_sims)),
|
| 62 |
+
"embedding": "simple",
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def _try_clip_embeddings(pil_list, device="cuda"):
|
| 67 |
+
"""Optional: load CLIP and return (N, D) normalized image features. Returns None if unavailable."""
|
| 68 |
+
try:
|
| 69 |
+
import sys
|
| 70 |
+
repo = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
|
| 71 |
+
if repo not in sys.path:
|
| 72 |
+
sys.path.insert(0, repo)
|
| 73 |
+
from diffsynth.extensions.ImageQualityMetric.clip import CLIPScore
|
| 74 |
+
from diffsynth.extensions.ImageQualityMetric.config import MODEL_PATHS
|
| 75 |
+
import torch
|
| 76 |
+
model = CLIPScore(device=torch.device(device), path=MODEL_PATHS)
|
| 77 |
+
model.model.eval()
|
| 78 |
+
feats = []
|
| 79 |
+
for pil in pil_list:
|
| 80 |
+
x = model.preprocess_val(pil).unsqueeze(0).to(device=model.device)
|
| 81 |
+
with torch.no_grad():
|
| 82 |
+
f = model.model.encode_image(x, normalize=True)
|
| 83 |
+
feats.append(f.cpu().numpy().squeeze(0))
|
| 84 |
+
return np.stack(feats, axis=0)
|
| 85 |
+
except Exception:
|
| 86 |
+
return None
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def clip_consistency_with_clip(pil_list, device: str = "cuda") -> dict[str, float] | None:
|
| 90 |
+
"""CLIP-based consistency. Returns None if CLIP not available."""
|
| 91 |
+
emb = _try_clip_embeddings(pil_list, device)
|
| 92 |
+
if emb is None or emb.shape[0] < 2:
|
| 93 |
+
return None
|
| 94 |
+
first = emb[0]
|
| 95 |
+
consec_sims = [float(np.dot(emb[i], emb[i + 1])) for i in range(emb.shape[0] - 1)]
|
| 96 |
+
to_first_sims = [float(np.dot(emb[i], first)) for i in range(1, emb.shape[0])]
|
| 97 |
+
return {
|
| 98 |
+
"mean_consecutive_sim": float(np.mean(consec_sims)),
|
| 99 |
+
"min_consecutive_sim": float(np.min(consec_sims)),
|
| 100 |
+
"mean_to_first_sim": float(np.mean(to_first_sims)),
|
| 101 |
+
"min_to_first_sim": float(np.min(to_first_sims)),
|
| 102 |
+
"embedding": "clip",
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def run_identity_preservation(
|
| 107 |
+
evals_root: str,
|
| 108 |
+
use_clip: bool = False,
|
| 109 |
+
device: str = "cuda",
|
| 110 |
+
video_paths: list[tuple[str, str]] | None = None,
|
| 111 |
+
max_frames_per_video: int | None = 100,
|
| 112 |
+
) -> dict[str, Any]:
|
| 113 |
+
"""
|
| 114 |
+
Compute identity preservation (CLIP consistency) over all gen_only videos.
|
| 115 |
+
When use_clip=False uses simple embedding; when use_clip=True tries diffsynth CLIP.
|
| 116 |
+
"""
|
| 117 |
+
if video_paths is None:
|
| 118 |
+
video_paths = discover_evals_videos(evals_root)
|
| 119 |
+
|
| 120 |
+
per_video = []
|
| 121 |
+
agg_consec = []
|
| 122 |
+
agg_to_first = []
|
| 123 |
+
|
| 124 |
+
for rel, absp in video_paths:
|
| 125 |
+
if not os.path.isfile(absp):
|
| 126 |
+
continue
|
| 127 |
+
if use_clip:
|
| 128 |
+
pil_list = load_video_frames_pil(absp, max_frames=max_frames_per_video)
|
| 129 |
+
if not pil_list:
|
| 130 |
+
per_video.append({"rel": rel, "mean_consecutive_sim": None, "mean_to_first_sim": None, "embedding": None})
|
| 131 |
+
continue
|
| 132 |
+
res = clip_consistency_with_clip(pil_list, device)
|
| 133 |
+
if res is None:
|
| 134 |
+
frames = load_video_frames(absp, max_frames=max_frames_per_video)
|
| 135 |
+
res = clip_consistency_simple(frames)
|
| 136 |
+
else:
|
| 137 |
+
frames = load_video_frames(absp, max_frames=max_frames_per_video)
|
| 138 |
+
res = clip_consistency_simple(frames)
|
| 139 |
+
|
| 140 |
+
agg_consec.append(res["mean_consecutive_sim"])
|
| 141 |
+
agg_to_first.append(res["mean_to_first_sim"])
|
| 142 |
+
per_video.append({"rel": rel, **res})
|
| 143 |
+
|
| 144 |
+
aggregate = {}
|
| 145 |
+
if agg_consec:
|
| 146 |
+
aggregate["mean_consecutive_sim"] = float(np.mean(agg_consec))
|
| 147 |
+
aggregate["min_mean_to_first_sim"] = float(np.min(agg_to_first))
|
| 148 |
+
aggregate["mean_to_first_sim"] = float(np.mean(agg_to_first))
|
| 149 |
+
aggregate["face_embedding_note"] = "Optional: install insightface/torchreid for Face Embedding / character ID retention."
|
| 150 |
+
|
| 151 |
+
return {
|
| 152 |
+
"dimension": "identity_preservation",
|
| 153 |
+
"params": {"use_clip": use_clip, "device": device},
|
| 154 |
+
"per_video": per_video,
|
| 155 |
+
"aggregate": aggregate,
|
| 156 |
+
"num_videos": len(per_video),
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
def main():
|
| 161 |
+
p = argparse.ArgumentParser(description="Identity Preservation (CLIP consistency)")
|
| 162 |
+
p.add_argument("--evals_root", type=str, required=True)
|
| 163 |
+
p.add_argument("--use_clip", action="store_true", help="Use CLIP image encoder when available")
|
| 164 |
+
p.add_argument("--device", type=str, default="cuda")
|
| 165 |
+
p.add_argument("--max_frames", type=int, default=100)
|
| 166 |
+
p.add_argument("--output", type=str, default=None)
|
| 167 |
+
args = p.parse_args()
|
| 168 |
+
|
| 169 |
+
result = run_identity_preservation(
|
| 170 |
+
args.evals_root,
|
| 171 |
+
use_clip=args.use_clip,
|
| 172 |
+
device=args.device,
|
| 173 |
+
max_frames_per_video=args.max_frames,
|
| 174 |
+
)
|
| 175 |
+
out = json.dumps(result, indent=2)
|
| 176 |
+
print(out)
|
| 177 |
+
if args.output:
|
| 178 |
+
with open(args.output, "w") as f:
|
| 179 |
+
f.write(out)
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
if __name__ == "__main__":
|
| 183 |
+
main()
|