mqye commited on
Commit
3c2ac35
Β·
1 Parent(s): ad3110d

Deploy MODUS 3-tab any-to-any demo (ZeroGPU)

Browse files
This view is limited to 50 files because it contains too many changes. Β  See raw diff
Files changed (50) hide show
  1. .gitattributes +0 -35
  2. README.md +25 -5
  3. app.py +116 -0
  4. conf/modalities/bagel_stage3_exact3cond.yaml +191 -0
  5. conf/modalities/hunyuan_image_3.yaml +31 -0
  6. conf/modalities/instruction.yaml +201 -0
  7. conf/modalities/instruction_10modality_stage2.yaml +142 -0
  8. conf/modalities/instruction_16mod_instr.yaml +259 -0
  9. conf/modalities/instruction_16mod_stage1.yaml +243 -0
  10. conf/modalities/instruction_16mod_stage1_rgbcond.yaml +266 -0
  11. conf/modalities/instruction_16mod_stage2.yaml +272 -0
  12. conf/modalities/instruction_2d_only_stage1.yaml +65 -0
  13. conf/modalities/instruction_9modality_stage1.yaml +114 -0
  14. conf/modalities/instruction_9modality_stage3.yaml +115 -0
  15. conf/modalities/instruction_hunyuan_16mod_stage1.yaml +257 -0
  16. conf/modalities/instruction_hunyuan_16mod_stage2.yaml +250 -0
  17. conf/modalities/instruction_stage2.yaml +204 -0
  18. conf/modalities/instruction_t2i_only_stage1.yaml +27 -0
  19. conf/modalities/legacy.yaml +38 -0
  20. conf/modalities/rebuttal_rgb2target.yaml +116 -0
  21. conf/modalities/stage1_oversample_bbox2rgb_dinolocal2rgb.yaml +203 -0
  22. core/__init__.py +11 -0
  23. core/modality.py +394 -0
  24. core/model_registry.py +47 -0
  25. core/tokenizer_utils.py +257 -0
  26. data/__init__.py +2 -0
  27. data/any2any_preprocess/_build_preview_and_montage.py +52 -0
  28. data/any2any_preprocess/_cast_preview_images.py +34 -0
  29. data/any2any_preprocess/_sb_full_rebuild.sh +18 -0
  30. data/any2any_preprocess/_sb_recompress_normal_sample.sh +24 -0
  31. data/any2any_preprocess/_sb_upload.sh +21 -0
  32. data/any2any_preprocess/_verify_normal_q95.py +34 -0
  33. data/any2any_preprocess/build_full_release.py +108 -0
  34. data/any2any_preprocess/check_vqa.py +188 -0
  35. data/any2any_preprocess/generate_parquest_grounding_canny_dino_global.py +205 -0
  36. data/any2any_preprocess/generate_parquet.py +229 -0
  37. data/any2any_preprocess/generate_parquet_clip448_imagebind.py +318 -0
  38. data/any2any_preprocess/generate_parquet_grounding.py +583 -0
  39. data/any2any_preprocess/generate_parquet_grounding_canny_dino.py +627 -0
  40. data/any2any_preprocess/generate_parquet_json.py +69 -0
  41. data/any2any_preprocess/generate_parquet_vlm_sft.py +278 -0
  42. data/any2any_preprocess/hf_upload.py +26 -0
  43. data/any2any_preprocess/parquet_visualize.py +408 -0
  44. data/any2any_preprocess/process_grounding.py +346 -0
  45. data/any2any_preprocess/recompress_normal_jpeg.py +90 -0
  46. data/any2any_preprocess/run_full_rebuild.py +82 -0
  47. data/any2any_preprocess/upload_full.py +26 -0
  48. data/any2any_preprocess/vqa_convert_parquet_to_jsonl.py +312 -0
  49. data/bundled_parquet_info/README.md +43 -0
  50. data/bundled_parquet_info/blip3o_rgb_caption_depth_normal_det_seg_grounding2_canny_dino_global_clip448_imagebind.json +0 -0
.gitattributes DELETED
@@ -1,35 +0,0 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
README.md CHANGED
@@ -1,13 +1,33 @@
1
  ---
2
  title: MODUS
3
- emoji: πŸ’»
4
- colorFrom: purple
5
  colorTo: purple
6
  sdk: gradio
7
- sdk_version: 6.19.0
8
- python_version: '3.13'
9
  app_file: app.py
10
  pinned: false
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  title: MODUS
3
+ emoji: 🎨
4
+ colorFrom: indigo
5
  colorTo: purple
6
  sdk: gradio
7
+ sdk_version: 4.44.1
 
8
  app_file: app.py
9
  pinned: false
10
+ short_description: MODUS any-to-any multimodal demo (16 aligned modalities)
11
  ---
12
 
13
+ # MODUS β€” any-to-any multimodal demo
14
+
15
+ Three-tab Gradio demo for the MODUS 16-modality any-to-any model:
16
+
17
+ 1. **Any-to-Any** β€” one condition modality (image or caption) β†’ any set of target
18
+ modalities, shown in a gallery (4M-style).
19
+ 2. **Chained** β€” condition β†’ intermediate (bridge) β†’ target, showing both.
20
+ 3. **Representation Analysis** — RGB→Depth/Normal conditioned on
21
+ {ViT only, VAE only, ViT+VAE}, side by side.
22
+
23
+ ## Setup (Space Settings β†’ Variables and secrets)
24
+ - **Hardware:** ZeroGPU.
25
+ - **`HF_TOKEN`** (secret): a *read* token with access to the gated weights repo
26
+ `mqye/modus-16mod-stage3`.
27
+ - Optional **`MODUS_WEIGHTS_REPO`**: defaults to `mqye/modus-16mod-stage3`.
28
+
29
+ The weights (bf16, ~30GB) + VAE + config + tokenizer are pulled once at startup
30
+ via `snapshot_download`; the model is built on CPU (~5min) and moved to the
31
+ ZeroGPU GPU per request. Inference config is baked in: modality config
32
+ `conf/modalities/instruction_16mod_stage2.yaml`, `use_instruction` off for the
33
+ generic modalities (seg/det/grounding keep their own instructions).
app.py ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """MODUS any-to-any demo β€” HuggingFace Space entrypoint (ZeroGPU).
3
+
4
+ Thin ZeroGPU wrapper over the existing 3-tab demo backend (``demo_modus.py`` +
5
+ ``demo_my/*``). The model is loaded ONCE at startup on CPU (no GPU, no time
6
+ limit); each inference call moves it to the ZeroGPU-provided GPU via the
7
+ ``@spaces.GPU`` decorator (moving ~30GB bf16 over PCIe is seconds, whereas the
8
+ build+weight-load is ~5min and must NOT happen inside the GPU-time window).
9
+
10
+ Space setup (Settings -> Variables and secrets):
11
+ HF_TOKEN read token for the gated weights repo (below)
12
+ Optional env (have sane defaults):
13
+ MODUS_WEIGHTS_REPO gated HF model repo with the bf16 weights + config + VAE
14
+ """
15
+ import os
16
+ import sys
17
+
18
+ try:
19
+ import torch
20
+ print(f"[app] torch={torch.__version__} python={sys.version.split()[0]}", flush=True)
21
+ except Exception as _e:
22
+ print(f"[app] torch import failed: {_e}", flush=True)
23
+
24
+ # ── Env MUST be set before importing the demo backend (it reads these at import) ─
25
+ os.environ.setdefault("MODUS_NO_MEAN_RESIZING", "1") # avoid gradio-BLAS deadlock
26
+ os.environ.setdefault("MODUS_FORCE_SDPA_ATTN", "1") # no flash-attn on the Space
27
+ os.environ.setdefault("MODUS_DEMO_MODALITY_CONFIG",
28
+ "conf/modalities/instruction_16mod_stage2.yaml")
29
+ os.environ.setdefault("MODUS_DEMO_MODEL_NAME", "bagel_from_json")
30
+ os.environ.setdefault("MODUS_DEMO_USE_EMA", "0")
31
+
32
+ WEIGHTS_REPO = os.environ.get("MODUS_WEIGHTS_REPO", "mqye/modus-16mod-stage3")
33
+
34
+ # ── Pull the gated weights once (model.safetensors + ae + config + tokenizer) ────
35
+ from huggingface_hub import snapshot_download # noqa: E402
36
+
37
+ _weights_dir = snapshot_download(
38
+ repo_id=WEIGHTS_REPO,
39
+ repo_type="model",
40
+ token=os.environ.get("HF_TOKEN"),
41
+ )
42
+ # The snapshot dir holds BOTH the checkpoint (model.safetensors) and the base
43
+ # config/tokenizer/VAE, so it serves as CHECKPOINT_PATH and MODEL_PATH at once.
44
+ os.environ["MODUS_DEMO_CHECKPOINT"] = _weights_dir
45
+ os.environ["BAGEL_MODEL_PATH"] = _weights_dir
46
+ print(f"[app] weights ready at {_weights_dir}", flush=True)
47
+
48
+ import spaces # noqa: E402 (ZeroGPU)
49
+
50
+ # diffusers 0.20 (imported by the fourm VQVAE feature tokenizers) does
51
+ # `from huggingface_hub import cached_download`, which was removed in hub>=0.26
52
+ # (the Space has 0.36). Shim it to hf_hub_download so `import diffusers` succeeds
53
+ # and the dino/clip/imagebind tokenizers can load.
54
+ import huggingface_hub as _hh # noqa: E402
55
+ if not hasattr(_hh, "cached_download"):
56
+ _hh.cached_download = _hh.hf_hub_download
57
+
58
+ # Importing demo_modus builds the UI + backend and reads the env set above.
59
+ import demo_modus # noqa: E402
60
+
61
+
62
+ # ── ZeroGPU: wrap the two inference entry points so each call gets a GPU slice ───
63
+ # tab1/tab2 call demo_modus.run_task; tab3 calls demo_modus.run_representation_task.
64
+ # The tab handlers resolve these by module-global name at call time, so replacing
65
+ # the module attribute makes them use the GPU-wrapped versions.
66
+ # duration = max GPU-seconds ZeroGPU reserves per call. The Chained tab runs TWO
67
+ # generations inside a single run_task call (~60s+), so 60 is too tight ("GPU task
68
+ # aborted"); 120 covers chained + cold-start model materialisation. (With PRO the
69
+ # reservation size is a non-issue quota-wise.)
70
+ _GPU_DURATION = int(os.environ.get("MODUS_GPU_DURATION", "120"))
71
+ demo_modus.run_task = spaces.GPU(duration=_GPU_DURATION)(demo_modus.run_task)
72
+ # Tab3 runs THREE generations (vit/vae/both) per click. Wrapping run_representation_task
73
+ # would take three separate GPU acquisitions in one handler β†’ the 3rd hits "Expired
74
+ # ZeroGPU proxy token". Instead wrap the whole tab3 HANDLER so all three run inside a
75
+ # single GPU session (one token, one reservation big enough for 3 gens).
76
+ demo_modus.tab3_generate = spaces.GPU(duration=180)(demo_modus.tab3_generate)
77
+
78
+ # Load the model ONCE at startup. The heavy CPU work (build arch + read 30GB
79
+ # weights, ~5min) runs here, OUTSIDE any @spaces.GPU window; the model's .cuda()
80
+ # moves are deferred by `spaces` and materialise on the first GPU call.
81
+ try:
82
+ demo_modus.HOLDER.ensure_loaded()
83
+ print("[app] model loaded (CPU) at startup", flush=True)
84
+ except Exception as e: # surface in UI, retry lazily on first request
85
+ demo_modus.HOLDER.load_error = str(e)
86
+ print(f"[app] startup model load failed: {e}", flush=True)
87
+
88
+ # Work around a gradio 4.44.1 bug: get_api_info() (called when rendering the main
89
+ # "/" route) crashes with `TypeError: argument of type 'bool' is not iterable` when
90
+ # a component's JSON schema contains a bool (additionalProperties: true/false).
91
+ # Patch gradio_client's schema helpers to tolerate bool schemas.
92
+ try:
93
+ import gradio_client.utils as _gcu
94
+
95
+ _orig_j2p = _gcu._json_schema_to_python_type
96
+
97
+ def _safe_j2p(schema, defs=None):
98
+ if isinstance(schema, bool):
99
+ return "Any"
100
+ return _orig_j2p(schema, defs)
101
+
102
+ _gcu._json_schema_to_python_type = _safe_j2p
103
+
104
+ _orig_get_type = _gcu.get_type
105
+
106
+ def _safe_get_type(schema):
107
+ if isinstance(schema, bool):
108
+ return "Any"
109
+ return _orig_get_type(schema)
110
+
111
+ _gcu.get_type = _safe_get_type
112
+ except Exception as _e:
113
+ print(f"[app] gradio_client schema patch skipped: {_e}", flush=True)
114
+
115
+ demo = demo_modus.build_ui()
116
+ demo.queue().launch(server_name="0.0.0.0", server_port=7860, show_api=False)
conf/modalities/bagel_stage3_exact3cond.yaml ADDED
@@ -0,0 +1,191 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Stage 3 modality config for the standalone BAGEL exact-3-condition project.
2
+ # Intentionally duplicated from instruction_stage2.yaml so this project can
3
+ # evolve independently without affecting MODUS experiments.
4
+
5
+ modalities:
6
+ - name: text
7
+ id: 0
8
+ kind: text
9
+ start_token_key: bos_token_id
10
+ end_token_key: eos_token_id
11
+
12
+ - name: caption
13
+ id: 1
14
+ kind: text
15
+ start_token_key: bos_token_id
16
+ end_token_key: eos_token_id
17
+ represent_vae: true
18
+
19
+ - name: rgb
20
+ id: 2
21
+ kind: image
22
+ start_token_key: start_of_image
23
+ end_token_key: end_of_image
24
+ represent_vit: true
25
+ represent_vae: true
26
+
27
+ - name: depth
28
+ id: 3
29
+ kind: image
30
+ start_token_key: start_of_image
31
+ end_token_key: end_of_image
32
+ represent_vit: true
33
+ represent_vae: true
34
+
35
+ - name: normal
36
+ id: 4
37
+ kind: image
38
+ start_token_key: start_of_image
39
+ end_token_key: end_of_image
40
+ represent_vit: true
41
+ represent_vae: true
42
+
43
+ - name: det
44
+ id: 5
45
+ kind: codebook
46
+ start_token_key: start_of_det
47
+ end_token_key: end_of_det
48
+ represent_vae: true
49
+ pos_embed_size: 4
50
+ apply_pos_embed_in_forward: true
51
+ pos_embed_name: grounding
52
+ loss:
53
+ reweight: true
54
+ reweight_min_w: 0.005
55
+ start_token: "<|det_start|>"
56
+ end_token: "<|det_end|>"
57
+ extra_tokens: ["<|box_start|>", "<|box_end|>"]
58
+ code_token_groups:
59
+ - token_format: "<|x1_{i:03d}|>"
60
+ start: 0
61
+ end: 999
62
+ - token_format: "<|y1_{i:03d}|>"
63
+ start: 0
64
+ end: 999
65
+ - token_format: "<|x2_{i:03d}|>"
66
+ start: 0
67
+ end: 999
68
+ - token_format: "<|y2_{i:03d}|>"
69
+ start: 0
70
+ end: 999
71
+ - token_format: "<|score_{i:02d}|>"
72
+ start: 0
73
+ end: 99
74
+ inference_decode_method: detection
75
+ inference_max_tokens: 1000
76
+ inference_cfg_uncond: text
77
+ inference_add_instruction: false
78
+
79
+ - name: seg
80
+ id: 6
81
+ kind: image
82
+ start_token_key: start_of_image
83
+ end_token_key: end_of_image
84
+ represent_vit: true
85
+ represent_vae: true
86
+
87
+ - name: canny
88
+ id: 7
89
+ kind: image
90
+ start_token_key: start_of_image
91
+ end_token_key: end_of_image
92
+ represent_vit: true
93
+ represent_vae: true
94
+
95
+ - name: dino
96
+ id: 8
97
+ kind: codebook
98
+ start_token_key: start_of_dino
99
+ end_token_key: end_of_dino
100
+ pos_embed_size: 16
101
+ apply_pos_embed_in_forward: true
102
+ represent_vae: true
103
+ start_token: "<|dino_start|>"
104
+ end_token: "<|dino_end|>"
105
+ code_vocab_size: 8192
106
+ code_token_format: "<|dino_{i:04d}|>"
107
+ external_tokenizer_kind: vqvae
108
+ external_tokenizer_repo: "EPFL-VILAB/4M_tokenizers_DINOv2-B14-global_8k_16_224"
109
+ inference_decode_method: dino
110
+ inference_max_tokens: 17
111
+ inference_cfg_uncond: img
112
+ inference_cfg_img_scale: 1.0
113
+
114
+ - name: dinolocal
115
+ id: 9
116
+ kind: codebook
117
+ start_token_key: start_of_dinolocal
118
+ end_token_key: end_of_dinolocal
119
+ pos_embed_size: 1024
120
+ apply_pos_embed_in_forward: true
121
+ represent_vae: true
122
+ start_token: "<|dinolocal_start|>"
123
+ end_token: "<|dinolocal_end|>"
124
+ code_vocab_size: 8192
125
+ code_token_format: "<|dinolocal_{i:04d}|>"
126
+ codebook_spatial_shape: [32, 32]
127
+ external_tokenizer_kind: vqvae
128
+ external_tokenizer_repo: "EPFL-VILAB/4M_tokenizers_DINOv2-B14_8k_224-448"
129
+ inference_decode_method: dinolocal
130
+ inference_max_tokens: 1025
131
+ inference_cfg_uncond: img
132
+ inference_cfg_img_scale: 1.0
133
+
134
+ - name: clip
135
+ id: 10
136
+ kind: codebook
137
+ start_token_key: start_of_clip
138
+ end_token_key: end_of_clip
139
+ pos_embed_size: 784
140
+ apply_pos_embed_in_forward: true
141
+ represent_vae: true
142
+ start_token: "<|clip_start|>"
143
+ end_token: "<|clip_end|>"
144
+ code_vocab_size: 8192
145
+ code_token_format: "<|clip_{i:04d}|>"
146
+ codebook_spatial_shape: [28, 28]
147
+ external_tokenizer_kind: vqvae
148
+ external_tokenizer_repo: "EPFL-VILAB/4M_tokenizers_CLIP-B16_8k_224-448"
149
+ inference_decode_method: clip
150
+ inference_max_tokens: 785
151
+ inference_cfg_uncond: img
152
+ inference_cfg_img_scale: 1.0
153
+
154
+ - name: imagebind
155
+ id: 11
156
+ kind: codebook
157
+ start_token_key: start_of_imagebind
158
+ end_token_key: end_of_imagebind
159
+ pos_embed_size: 16
160
+ apply_pos_embed_in_forward: true
161
+ represent_vae: true
162
+ start_token: "<|imagebind_start|>"
163
+ end_token: "<|imagebind_end|>"
164
+ code_vocab_size: 8192
165
+ code_token_format: "<|imagebind_{i:04d}|>"
166
+ external_tokenizer_kind: vqvae
167
+ external_tokenizer_repo: "EPFL-VILAB/4M_tokenizers_ImageBind-H14-global_8k_16_224"
168
+ inference_decode_method: imagebind
169
+ inference_max_tokens: 17
170
+ inference_cfg_uncond: img
171
+ inference_cfg_img_scale: 1.0
172
+
173
+ - name: imagebindlocal
174
+ id: 12
175
+ kind: codebook
176
+ start_token_key: start_of_imagebindlocal
177
+ end_token_key: end_of_imagebindlocal
178
+ pos_embed_size: 1024
179
+ apply_pos_embed_in_forward: true
180
+ represent_vae: true
181
+ start_token: "<|imagebindlocal_start|>"
182
+ end_token: "<|imagebindlocal_end|>"
183
+ code_vocab_size: 8192
184
+ code_token_format: "<|imagebindlocal_{i:04d}|>"
185
+ codebook_spatial_shape: [32, 32]
186
+ external_tokenizer_kind: vqvae
187
+ external_tokenizer_repo: "EPFL-VILAB/4M_tokenizers_ImageBind-H14_8k_224-448"
188
+ inference_decode_method: imagebindlocal
189
+ inference_max_tokens: 1025
190
+ inference_cfg_uncond: img
191
+ inference_cfg_img_scale: 1.0
conf/modalities/hunyuan_image_3.yaml ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Modality configuration for HunyuanImage-3.0 training.
2
+ #
3
+ # Only the RGB image modality is needed for text-to-image generation.
4
+ # Token keys (start_of_image / end_of_image) are populated by
5
+ # data_utils.add_special_tokens(), which adds <|vision_start|> and
6
+ # <|vision_end|> to the tokenizer when they are absent.
7
+ # These serve as sequence delimiters; actual image patches are placed at
8
+ # packed_vae_token_indexes and overwritten by the model's patch_embed.
9
+
10
+ modalities:
11
+ - name: text
12
+ id: 0
13
+ kind: text
14
+ start_token_key: bos_token_id
15
+ end_token_key: eos_token_id
16
+
17
+ - name: caption
18
+ id: 1
19
+ kind: text
20
+ start_token_key: bos_token_id
21
+ end_token_key: eos_token_id
22
+ represent_vae: true
23
+ conditions: [rgb]
24
+
25
+ - name: rgb
26
+ id: 2
27
+ kind: image
28
+ start_token_key: start_of_image
29
+ end_token_key: end_of_image
30
+ represent_vae: true
31
+ represent_vit: true
conf/modalities/instruction.yaml ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ modalities:
2
+ - name: text
3
+ id: 0
4
+ kind: text
5
+ start_token_key: bos_token_id
6
+ end_token_key: eos_token_id
7
+
8
+ - name: caption
9
+ id: 1
10
+ kind: text
11
+ start_token_key: bos_token_id
12
+ end_token_key: eos_token_id
13
+ represent_vae: true
14
+ conditions: [rgb, grounding, dino, dinolocal, clip, imagebind, imagebindlocal]
15
+
16
+ - name: rgb
17
+ id: 2
18
+ kind: image
19
+ start_token_key: start_of_image
20
+ end_token_key: end_of_image
21
+ represent_vit: true
22
+ represent_vae: true
23
+ conditions: [caption, grounding, dino, dinolocal, clip, imagebind, imagebindlocal]
24
+
25
+ - name: depth
26
+ id: 3
27
+ kind: image
28
+ start_token_key: start_of_image
29
+ end_token_key: end_of_image
30
+ represent_vit: true
31
+ represent_vae: true
32
+
33
+ - name: normal
34
+ id: 4
35
+ kind: image
36
+ start_token_key: start_of_image
37
+ end_token_key: end_of_image
38
+ represent_vit: true
39
+ represent_vae: true
40
+
41
+ - name: det
42
+ id: 5
43
+ kind: codebook
44
+ start_token_key: start_of_det
45
+ end_token_key: end_of_det
46
+ represent_vae: true
47
+ pos_embed_size: 4
48
+ apply_pos_embed_in_forward: true
49
+ pos_embed_name: grounding # checkpoint compat: attr is "grounding_pos_embed"
50
+ conditions: [rgb]
51
+ loss:
52
+ reweight: true
53
+ reweight_min_w: 0.005
54
+ start_token: "<|det_start|>"
55
+ end_token: "<|det_end|>"
56
+ extra_tokens: ["<|box_start|>", "<|box_end|>"]
57
+ code_token_groups:
58
+ - token_format: "<|x1_{i:03d}|>"
59
+ start: 0
60
+ end: 999
61
+ - token_format: "<|y1_{i:03d}|>"
62
+ start: 0
63
+ end: 999
64
+ - token_format: "<|x2_{i:03d}|>"
65
+ start: 0
66
+ end: 999
67
+ - token_format: "<|y2_{i:03d}|>"
68
+ start: 0
69
+ end: 999
70
+ - token_format: "<|score_{i:02d}|>"
71
+ start: 0
72
+ end: 99
73
+ # Inference pipeline config
74
+ inference_decode_method: detection
75
+ inference_max_tokens: 1000
76
+ inference_cfg_uncond: text
77
+ inference_add_instruction: false # det uses start_of_det token, not text instruction
78
+
79
+ - name: seg
80
+ id: 6
81
+ kind: image
82
+ start_token_key: start_of_image
83
+ end_token_key: end_of_image
84
+ represent_vit: true
85
+ represent_vae: true
86
+
87
+ - name: canny
88
+ id: 7
89
+ kind: image
90
+ start_token_key: start_of_image
91
+ end_token_key: end_of_image
92
+ represent_vit: true
93
+ represent_vae: true
94
+
95
+ - name: dino
96
+ id: 8
97
+ kind: codebook
98
+ start_token_key: start_of_dino
99
+ end_token_key: end_of_dino
100
+ pos_embed_size: 16
101
+ apply_pos_embed_in_forward: true
102
+ represent_vae: true
103
+ conditions: [rgb]
104
+ start_token: "<|dino_start|>"
105
+ end_token: "<|dino_end|>"
106
+ code_vocab_size: 8192
107
+ code_token_format: "<|dino_{i:04d}|>"
108
+ external_tokenizer_kind: vqvae
109
+ external_tokenizer_repo: "EPFL-VILAB/4M_tokenizers_DINOv2-B14-global_8k_16_224"
110
+ # Inference pipeline config
111
+ inference_decode_method: dino
112
+ inference_max_tokens: 17
113
+ inference_cfg_uncond: img
114
+ inference_cfg_img_scale: 1.0
115
+
116
+ - name: dinolocal
117
+ id: 9
118
+ kind: codebook
119
+ start_token_key: start_of_dinolocal
120
+ end_token_key: end_of_dinolocal
121
+ pos_embed_size: 1024
122
+ apply_pos_embed_in_forward: true
123
+ represent_vae: true
124
+ conditions: [rgb]
125
+ codebook_spatial_shape: [32, 32] # 1024 tokens, 32Γ—32 grid β†’ 2D RoPE
126
+ start_token: "<|dinolocal_start|>"
127
+ end_token: "<|dinolocal_end|>"
128
+ code_vocab_size: 8192
129
+ code_token_format: "<|dinolocal_{i:04d}|>"
130
+ external_tokenizer_kind: vqvae
131
+ external_tokenizer_repo: "EPFL-VILAB/4M_tokenizers_DINOv2-B14_8k_224-448"
132
+ # Inference pipeline config
133
+ inference_decode_method: dinolocal
134
+ inference_max_tokens: 1025
135
+ inference_cfg_uncond: img
136
+ inference_cfg_img_scale: 1.0
137
+
138
+ - name: clip
139
+ id: 10
140
+ kind: codebook
141
+ start_token_key: start_of_clip
142
+ end_token_key: end_of_clip
143
+ pos_embed_size: 784
144
+ apply_pos_embed_in_forward: true
145
+ represent_vae: true
146
+ conditions: [rgb]
147
+ codebook_spatial_shape: [28, 28] # 784 tokens, 28Γ—28 grid β†’ 2D RoPE
148
+ start_token: "<|clip_start|>"
149
+ end_token: "<|clip_end|>"
150
+ code_vocab_size: 8192
151
+ code_token_format: "<|clip_{i:04d}|>"
152
+ external_tokenizer_kind: vqvae
153
+ external_tokenizer_repo: "EPFL-VILAB/4M_tokenizers_CLIP-B16_8k_224-448"
154
+ # Inference pipeline config
155
+ inference_decode_method: clip
156
+ inference_max_tokens: 785
157
+ inference_cfg_uncond: img
158
+ inference_cfg_img_scale: 1.0
159
+
160
+ - name: imagebind
161
+ id: 11
162
+ kind: codebook
163
+ start_token_key: start_of_imagebind
164
+ end_token_key: end_of_imagebind
165
+ pos_embed_size: 16
166
+ apply_pos_embed_in_forward: true
167
+ represent_vae: true
168
+ conditions: [rgb]
169
+ start_token: "<|imagebind_start|>"
170
+ end_token: "<|imagebind_end|>"
171
+ code_vocab_size: 8192
172
+ code_token_format: "<|imagebind_{i:04d}|>"
173
+ external_tokenizer_kind: vqvae
174
+ external_tokenizer_repo: "EPFL-VILAB/4M_tokenizers_ImageBind-H14-global_8k_16_224"
175
+ # Inference pipeline config
176
+ inference_decode_method: imagebind
177
+ inference_max_tokens: 17
178
+ inference_cfg_uncond: img
179
+ inference_cfg_img_scale: 1.0
180
+
181
+ - name: imagebindlocal
182
+ id: 12
183
+ kind: codebook
184
+ start_token_key: start_of_imagebindlocal
185
+ end_token_key: end_of_imagebindlocal
186
+ pos_embed_size: 1024
187
+ apply_pos_embed_in_forward: true
188
+ represent_vae: true
189
+ conditions: [rgb]
190
+ codebook_spatial_shape: [32, 32] # 1024 tokens, 32Γ—32 grid β†’ 2D RoPE
191
+ start_token: "<|imagebindlocal_start|>"
192
+ end_token: "<|imagebindlocal_end|>"
193
+ code_vocab_size: 8192
194
+ code_token_format: "<|imagebindlocal_{i:04d}|>"
195
+ external_tokenizer_kind: vqvae
196
+ external_tokenizer_repo: "EPFL-VILAB/4M_tokenizers_ImageBind-H14_8k_224-448"
197
+ # Inference pipeline config
198
+ inference_decode_method: imagebindlocal
199
+ inference_max_tokens: 1025
200
+ inference_cfg_uncond: img
201
+ inference_cfg_img_scale: 1.0
conf/modalities/instruction_10modality_stage2.yaml ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Stage 2 modality config β€” unconstrained conditions.
2
+ #
3
+ # Same modality definitions as instruction.yaml, but with ALL condition
4
+ # restrictions removed so every modality can be conditioned on every other.
5
+
6
+ modalities:
7
+ - name: text
8
+ id: 0
9
+ kind: text
10
+ start_token_key: bos_token_id
11
+ end_token_key: eos_token_id
12
+
13
+ - name: caption
14
+ id: 1
15
+ kind: text
16
+ start_token_key: bos_token_id
17
+ end_token_key: eos_token_id
18
+ represent_vae: true
19
+ # conditions: unconstrained (any modality can condition caption)
20
+
21
+ - name: rgb
22
+ id: 2
23
+ kind: image
24
+ start_token_key: start_of_image
25
+ end_token_key: end_of_image
26
+ represent_vit: true
27
+ represent_vae: true
28
+ # conditions: unconstrained
29
+
30
+ - name: depth
31
+ id: 3
32
+ kind: image
33
+ start_token_key: start_of_image
34
+ end_token_key: end_of_image
35
+ represent_vit: true
36
+ represent_vae: true
37
+
38
+ - name: normal
39
+ id: 4
40
+ kind: image
41
+ start_token_key: start_of_image
42
+ end_token_key: end_of_image
43
+ represent_vit: true
44
+ represent_vae: true
45
+
46
+ - name: det
47
+ id: 5
48
+ kind: codebook
49
+ start_token_key: start_of_det
50
+ end_token_key: end_of_det
51
+ represent_vae: true
52
+ pos_embed_size: 4
53
+ apply_pos_embed_in_forward: true
54
+ pos_embed_name: grounding # checkpoint compat: attr is "grounding_pos_embed"
55
+ # conditions: unconstrained
56
+ loss:
57
+ reweight: true
58
+ reweight_min_w: 0.005
59
+ start_token: "<|det_start|>"
60
+ end_token: "<|det_end|>"
61
+ extra_tokens: ["<|box_start|>", "<|box_end|>"]
62
+ code_token_groups:
63
+ - token_format: "<|x1_{i:03d}|>"
64
+ start: 0
65
+ end: 999
66
+ - token_format: "<|y1_{i:03d}|>"
67
+ start: 0
68
+ end: 999
69
+ - token_format: "<|x2_{i:03d}|>"
70
+ start: 0
71
+ end: 999
72
+ - token_format: "<|y2_{i:03d}|>"
73
+ start: 0
74
+ end: 999
75
+ - token_format: "<|score_{i:02d}|>"
76
+ start: 0
77
+ end: 99
78
+ # Inference pipeline config
79
+ inference_decode_method: detection
80
+ inference_max_tokens: 1000
81
+ inference_cfg_uncond: text
82
+ inference_add_instruction: false # det uses start_of_det token, not text instruction
83
+
84
+ - name: seg
85
+ id: 6
86
+ kind: image
87
+ start_token_key: start_of_image
88
+ end_token_key: end_of_image
89
+ represent_vit: true
90
+ represent_vae: true
91
+
92
+ - name: canny
93
+ id: 7
94
+ kind: image
95
+ start_token_key: start_of_image
96
+ end_token_key: end_of_image
97
+ represent_vit: true
98
+ represent_vae: true
99
+
100
+ - name: dino
101
+ id: 8
102
+ kind: codebook
103
+ start_token_key: start_of_dino
104
+ end_token_key: end_of_dino
105
+ pos_embed_size: 16
106
+ apply_pos_embed_in_forward: true
107
+ represent_vae: true
108
+ # conditions: unconstrained
109
+ start_token: "<|dino_start|>"
110
+ end_token: "<|dino_end|>"
111
+ code_vocab_size: 8192
112
+ code_token_format: "<|dino_{i:04d}|>"
113
+ external_tokenizer_kind: vqvae
114
+ external_tokenizer_repo: "EPFL-VILAB/4M_tokenizers_DINOv2-B14-global_8k_16_224"
115
+ # Inference pipeline config
116
+ inference_decode_method: dino
117
+ inference_max_tokens: 17
118
+ inference_cfg_uncond: img
119
+ inference_cfg_img_scale: 1.0
120
+
121
+ - name: dinolocal
122
+ id: 9
123
+ kind: codebook
124
+ start_token_key: start_of_dinolocal
125
+ end_token_key: end_of_dinolocal
126
+ pos_embed_size: 1024
127
+ apply_pos_embed_in_forward: true
128
+ represent_vae: true
129
+ # conditions: unconstrained
130
+ start_token: "<|dinolocal_start|>"
131
+ end_token: "<|dinolocal_end|>"
132
+ code_vocab_size: 8192
133
+ code_token_format: "<|dinolocal_{i:04d}|>"
134
+ external_tokenizer_kind: vqvae
135
+ external_tokenizer_repo: "EPFL-VILAB/4M_tokenizers_DINOv2-B14_8k_224-448"
136
+ # Inference pipeline config
137
+ inference_decode_method: dinolocal
138
+ inference_max_tokens: 1025
139
+ inference_cfg_uncond: img
140
+ inference_cfg_img_scale: 1.0
141
+
142
+
conf/modalities/instruction_16mod_instr.yaml ADDED
@@ -0,0 +1,259 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ modalities:
2
+ - name: text
3
+ id: 0
4
+ kind: text
5
+ start_token_key: bos_token_id
6
+ end_token_key: eos_token_id
7
+
8
+ - name: caption
9
+ id: 1
10
+ kind: text
11
+ start_token_key: bos_token_id
12
+ end_token_key: eos_token_id
13
+ represent_vae: true
14
+
15
+ - name: rgb
16
+ id: 2
17
+ kind: image
18
+ start_token_key: start_of_image
19
+ end_token_key: end_of_image
20
+ represent_vit: true
21
+ represent_vae: true
22
+
23
+ - name: depth
24
+ id: 3
25
+ kind: image
26
+ start_token_key: start_of_depth # REPLACE: per-modality start (token already exists in base vocab)
27
+ start_token: "<|depth_start|>"
28
+ end_token_key: end_of_image # shared end (image end is a structural terminator)
29
+ represent_vit: true
30
+ represent_vae: true
31
+
32
+ - name: normal
33
+ id: 4
34
+ kind: image
35
+ start_token_key: start_of_normal # REPLACE: per-modality start (token already exists in base vocab)
36
+ start_token: "<|normal_start|>"
37
+ end_token_key: end_of_image
38
+ represent_vit: true
39
+ represent_vae: true
40
+
41
+ - name: det
42
+ id: 5
43
+ kind: codebook
44
+ start_token_key: start_of_det
45
+ end_token_key: end_of_det
46
+ represent_vae: true
47
+ # NOTE: det's learnable pos_embed (grounding_pos_embed, size 4) is REMOVED here.
48
+ # The forward pos_embed loop is keyed by token-id range, and cocodet reuses
49
+ # det's x1/y1/x2/y2 tokens; with the pos_embed on, a multi-box cocodet sample
50
+ # (>4 coord tokens) would index the size-4 embedding out of bounds. det is not
51
+ # trained in modus (only present for coord-token-id alignment), so dropping its
52
+ # pos_embed is safe. reweight also removed (no reweight for det/cocodet).
53
+ start_token: "<|det_start|>"
54
+ end_token: "<|det_end|>"
55
+ extra_tokens: ["<|box_start|>", "<|box_end|>"]
56
+ code_token_groups:
57
+ - token_format: "<|x1_{i:03d}|>"
58
+ start: 0
59
+ end: 999
60
+ - token_format: "<|y1_{i:03d}|>"
61
+ start: 0
62
+ end: 999
63
+ - token_format: "<|x2_{i:03d}|>"
64
+ start: 0
65
+ end: 999
66
+ - token_format: "<|y2_{i:03d}|>"
67
+ start: 0
68
+ end: 999
69
+ - token_format: "<|score_{i:02d}|>"
70
+ start: 0
71
+ end: 99
72
+ # Inference pipeline config
73
+ inference_decode_method: detection
74
+ inference_max_tokens: 1000
75
+ inference_cfg_uncond: text
76
+ inference_add_instruction: false # det uses start_of_det token, not text instruction
77
+
78
+ - name: seg
79
+ id: 6
80
+ kind: image
81
+ start_token_key: start_of_seg # REPLACE: per-modality start (NEW token)
82
+ start_token: "<|seg_start|>"
83
+ end_token_key: end_of_image
84
+ represent_vit: true
85
+ represent_vae: true
86
+
87
+ - name: canny
88
+ id: 7
89
+ kind: image
90
+ start_token_key: start_of_canny # REPLACE: per-modality start (NEW token)
91
+ start_token: "<|canny_start|>"
92
+ end_token_key: end_of_image
93
+ represent_vit: true
94
+ represent_vae: true
95
+
96
+ - name: dino
97
+ id: 8
98
+ kind: codebook
99
+ start_token_key: start_of_dino
100
+ end_token_key: end_of_dino
101
+ pos_embed_size: 16
102
+ apply_pos_embed_in_forward: true
103
+ represent_vae: true
104
+ start_token: "<|dino_start|>"
105
+ end_token: "<|dino_end|>"
106
+ code_vocab_size: 8192
107
+ code_token_format: "<|dino_{i:04d}|>"
108
+ external_tokenizer_kind: vqvae
109
+ external_tokenizer_repo: "EPFL-VILAB/4M_tokenizers_DINOv2-B14-global_8k_16_224"
110
+ # Inference pipeline config
111
+ inference_decode_method: dino
112
+ inference_max_tokens: 17
113
+ inference_cfg_uncond: img
114
+ inference_cfg_img_scale: 1.0
115
+
116
+ - name: dinolocal
117
+ id: 9
118
+ kind: codebook
119
+ start_token_key: start_of_dinolocal
120
+ end_token_key: end_of_dinolocal
121
+ pos_embed_size: 1024
122
+ apply_pos_embed_in_forward: true
123
+ represent_vae: true
124
+ codebook_spatial_shape: [32, 32] # 1024 tokens, 32Γ—32 grid β†’ 2D RoPE
125
+ start_token: "<|dinolocal_start|>"
126
+ end_token: "<|dinolocal_end|>"
127
+ code_vocab_size: 8192
128
+ code_token_format: "<|dinolocal_{i:04d}|>"
129
+ external_tokenizer_kind: vqvae
130
+ external_tokenizer_repo: "EPFL-VILAB/4M_tokenizers_DINOv2-B14_8k_224-448"
131
+ # Inference pipeline config
132
+ inference_decode_method: dinolocal
133
+ inference_max_tokens: 1025
134
+ inference_cfg_uncond: img
135
+ inference_cfg_img_scale: 1.0
136
+
137
+ - name: clip
138
+ id: 10
139
+ kind: codebook
140
+ start_token_key: start_of_clip
141
+ end_token_key: end_of_clip
142
+ pos_embed_size: 784
143
+ apply_pos_embed_in_forward: true
144
+ represent_vae: true
145
+ codebook_spatial_shape: [28, 28] # 784 tokens, 28Γ—28 grid β†’ 2D RoPE
146
+ start_token: "<|clip_start|>"
147
+ end_token: "<|clip_end|>"
148
+ code_vocab_size: 8192
149
+ code_token_format: "<|clip_{i:04d}|>"
150
+ external_tokenizer_kind: vqvae
151
+ external_tokenizer_repo: "EPFL-VILAB/4M_tokenizers_CLIP-B16_8k_224-448"
152
+ # Inference pipeline config
153
+ inference_decode_method: clip
154
+ inference_max_tokens: 785
155
+ inference_cfg_uncond: img
156
+ inference_cfg_img_scale: 1.0
157
+
158
+ - name: imagebind
159
+ id: 11
160
+ kind: codebook
161
+ start_token_key: start_of_imagebind
162
+ end_token_key: end_of_imagebind
163
+ pos_embed_size: 16
164
+ apply_pos_embed_in_forward: true
165
+ represent_vae: true
166
+ start_token: "<|imagebind_start|>"
167
+ end_token: "<|imagebind_end|>"
168
+ code_vocab_size: 8192
169
+ code_token_format: "<|imagebind_{i:04d}|>"
170
+ external_tokenizer_kind: vqvae
171
+ external_tokenizer_repo: "EPFL-VILAB/4M_tokenizers_ImageBind-H14-global_8k_16_224"
172
+ # Inference pipeline config
173
+ inference_decode_method: imagebind
174
+ inference_max_tokens: 17
175
+ inference_cfg_uncond: img
176
+ inference_cfg_img_scale: 1.0
177
+
178
+ - name: imagebindlocal
179
+ id: 12
180
+ kind: codebook
181
+ start_token_key: start_of_imagebindlocal
182
+ end_token_key: end_of_imagebindlocal
183
+ pos_embed_size: 1024
184
+ apply_pos_embed_in_forward: true
185
+ represent_vae: true
186
+ codebook_spatial_shape: [32, 32] # 1024 tokens, 32Γ—32 grid β†’ 2D RoPE
187
+ start_token: "<|imagebindlocal_start|>"
188
+ end_token: "<|imagebindlocal_end|>"
189
+ code_vocab_size: 8192
190
+ code_token_format: "<|imagebindlocal_{i:04d}|>"
191
+ external_tokenizer_kind: vqvae
192
+ external_tokenizer_repo: "EPFL-VILAB/4M_tokenizers_ImageBind-H14_8k_224-448"
193
+ # Inference pipeline config
194
+ inference_decode_method: imagebindlocal
195
+ inference_max_tokens: 1025
196
+ inference_cfg_uncond: img
197
+ inference_cfg_img_scale: 1.0
198
+
199
+ # ── NEW: cocodet (Pix2seq detection). stage1 = rgbβ†’cocodet only.
200
+ # Coords reuse det's x1/y1/x2/y2 tokens (dedup, 0 new); class = COCO-id-aligned
201
+ # tokens (~91 new). dispersed_code_tokens β†’ CE is gathered over the
202
+ # non-contiguous {coords βˆͺ class βˆͺ end} set, not a contiguous range.
203
+ # No reweight, no learnable pos_embed (RoPE + token-id carry order/role).
204
+ - name: cocodet
205
+ id: 13
206
+ kind: codebook
207
+ start_token_key: start_of_cocodet
208
+ end_token_key: end_of_cocodet
209
+ start_token: "<|cocodet_start|>"
210
+ end_token: "<|cocodet_end|>"
211
+ represent_vae: true
212
+ dispersed_code_tokens: true
213
+ code_token_groups:
214
+ - token_format: "<|x1_{i:03d}|>"
215
+ start: 0
216
+ end: 999
217
+ - token_format: "<|y1_{i:03d}|>"
218
+ start: 0
219
+ end: 999
220
+ - token_format: "<|x2_{i:03d}|>"
221
+ start: 0
222
+ end: 999
223
+ - token_format: "<|y2_{i:03d}|>"
224
+ start: 0
225
+ end: 999
226
+ - token_format: "<|coco_cls_{i:02d}|>"
227
+ start: 0
228
+ end: 90
229
+ # Inference pipeline config.
230
+ # decode_method is 'cocodet' (NOT 'detection') β€” cocodet's output is
231
+ # coords+class per box ending on cocodet_end, a DIFFERENT format from the
232
+ # grounding/det coords-only decode (generate_detection_coordonly). Handled by
233
+ # the dedicated model.generate_cocodet + inferencer.gen_cocodet (which do NOT
234
+ # touch the grounding decode).
235
+ inference_decode_method: cocodet
236
+ inference_max_tokens: 1000
237
+ inference_cfg_uncond: text
238
+ inference_add_instruction: false
239
+
240
+ # ── NEW image modalities (stage2): SAM segmentation / edge. Mirror seg/canny
241
+ # (direct-PNG image, VIT+VAE, MSE). REPLACE: per-modality start token, shared
242
+ # end_of_image. These start tokens are NEW (created via start_token field).
243
+ - name: samseg
244
+ id: 14
245
+ kind: image
246
+ start_token_key: start_of_samseg
247
+ start_token: "<|samseg_start|>"
248
+ end_token_key: end_of_image
249
+ represent_vit: true
250
+ represent_vae: true
251
+
252
+ - name: samedge
253
+ id: 15
254
+ kind: image
255
+ start_token_key: start_of_samedge
256
+ start_token: "<|samedge_start|>"
257
+ end_token_key: end_of_image
258
+ represent_vit: true
259
+ represent_vae: true
conf/modalities/instruction_16mod_stage1.yaml ADDED
@@ -0,0 +1,243 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ modalities:
2
+ - name: text
3
+ id: 0
4
+ kind: text
5
+ start_token_key: bos_token_id
6
+ end_token_key: eos_token_id
7
+
8
+ - name: caption
9
+ id: 1
10
+ kind: text
11
+ start_token_key: bos_token_id
12
+ end_token_key: eos_token_id
13
+ represent_vae: true
14
+ conditions: [rgb, grounding, dino, dinolocal, clip, imagebind, imagebindlocal]
15
+
16
+ - name: rgb
17
+ id: 2
18
+ kind: image
19
+ start_token_key: start_of_image
20
+ end_token_key: end_of_image
21
+ represent_vit: true
22
+ represent_vae: true
23
+ conditions: [caption, grounding, dino, dinolocal, clip, imagebind, imagebindlocal]
24
+
25
+ - name: depth
26
+ id: 3
27
+ kind: image
28
+ start_token_key: start_of_image
29
+ end_token_key: end_of_image
30
+ represent_vit: true
31
+ represent_vae: true
32
+
33
+ - name: normal
34
+ id: 4
35
+ kind: image
36
+ start_token_key: start_of_image
37
+ end_token_key: end_of_image
38
+ represent_vit: true
39
+ represent_vae: true
40
+
41
+ - name: det
42
+ id: 5
43
+ kind: codebook
44
+ start_token_key: start_of_det
45
+ end_token_key: end_of_det
46
+ represent_vae: true
47
+ # NOTE: det's learnable pos_embed (grounding_pos_embed, size 4) is REMOVED here.
48
+ # The forward pos_embed loop is keyed by token-id range, and cocodet reuses
49
+ # det's x1/y1/x2/y2 tokens; with the pos_embed on, a multi-box cocodet sample
50
+ # (>4 coord tokens) would index the size-4 embedding out of bounds. det is not
51
+ # trained in modus (only present for coord-token-id alignment), so dropping its
52
+ # pos_embed is safe. reweight also removed (no reweight for det/cocodet).
53
+ conditions: [rgb]
54
+ start_token: "<|det_start|>"
55
+ end_token: "<|det_end|>"
56
+ extra_tokens: ["<|box_start|>", "<|box_end|>"]
57
+ code_token_groups:
58
+ - token_format: "<|x1_{i:03d}|>"
59
+ start: 0
60
+ end: 999
61
+ - token_format: "<|y1_{i:03d}|>"
62
+ start: 0
63
+ end: 999
64
+ - token_format: "<|x2_{i:03d}|>"
65
+ start: 0
66
+ end: 999
67
+ - token_format: "<|y2_{i:03d}|>"
68
+ start: 0
69
+ end: 999
70
+ - token_format: "<|score_{i:02d}|>"
71
+ start: 0
72
+ end: 99
73
+ # Inference pipeline config
74
+ inference_decode_method: detection
75
+ inference_max_tokens: 1000
76
+ inference_cfg_uncond: text
77
+ inference_add_instruction: false # det uses start_of_det token, not text instruction
78
+
79
+ - name: seg
80
+ id: 6
81
+ kind: image
82
+ start_token_key: start_of_image
83
+ end_token_key: end_of_image
84
+ represent_vit: true
85
+ represent_vae: true
86
+
87
+ - name: canny
88
+ id: 7
89
+ kind: image
90
+ start_token_key: start_of_image
91
+ end_token_key: end_of_image
92
+ represent_vit: true
93
+ represent_vae: true
94
+
95
+ - name: dino
96
+ id: 8
97
+ kind: codebook
98
+ start_token_key: start_of_dino
99
+ end_token_key: end_of_dino
100
+ pos_embed_size: 16
101
+ apply_pos_embed_in_forward: true
102
+ represent_vae: true
103
+ conditions: [rgb]
104
+ start_token: "<|dino_start|>"
105
+ end_token: "<|dino_end|>"
106
+ code_vocab_size: 8192
107
+ code_token_format: "<|dino_{i:04d}|>"
108
+ external_tokenizer_kind: vqvae
109
+ external_tokenizer_repo: "EPFL-VILAB/4M_tokenizers_DINOv2-B14-global_8k_16_224"
110
+ # Inference pipeline config
111
+ inference_decode_method: dino
112
+ inference_max_tokens: 17
113
+ inference_cfg_uncond: img
114
+ inference_cfg_img_scale: 1.0
115
+
116
+ - name: dinolocal
117
+ id: 9
118
+ kind: codebook
119
+ start_token_key: start_of_dinolocal
120
+ end_token_key: end_of_dinolocal
121
+ pos_embed_size: 1024
122
+ apply_pos_embed_in_forward: true
123
+ represent_vae: true
124
+ conditions: [rgb]
125
+ codebook_spatial_shape: [32, 32] # 1024 tokens, 32Γ—32 grid β†’ 2D RoPE
126
+ start_token: "<|dinolocal_start|>"
127
+ end_token: "<|dinolocal_end|>"
128
+ code_vocab_size: 8192
129
+ code_token_format: "<|dinolocal_{i:04d}|>"
130
+ external_tokenizer_kind: vqvae
131
+ external_tokenizer_repo: "EPFL-VILAB/4M_tokenizers_DINOv2-B14_8k_224-448"
132
+ # Inference pipeline config
133
+ inference_decode_method: dinolocal
134
+ inference_max_tokens: 1025
135
+ inference_cfg_uncond: img
136
+ inference_cfg_img_scale: 1.0
137
+
138
+ - name: clip
139
+ id: 10
140
+ kind: codebook
141
+ start_token_key: start_of_clip
142
+ end_token_key: end_of_clip
143
+ pos_embed_size: 784
144
+ apply_pos_embed_in_forward: true
145
+ represent_vae: true
146
+ conditions: [rgb]
147
+ codebook_spatial_shape: [28, 28] # 784 tokens, 28Γ—28 grid β†’ 2D RoPE
148
+ start_token: "<|clip_start|>"
149
+ end_token: "<|clip_end|>"
150
+ code_vocab_size: 8192
151
+ code_token_format: "<|clip_{i:04d}|>"
152
+ external_tokenizer_kind: vqvae
153
+ external_tokenizer_repo: "EPFL-VILAB/4M_tokenizers_CLIP-B16_8k_224-448"
154
+ # Inference pipeline config
155
+ inference_decode_method: clip
156
+ inference_max_tokens: 785
157
+ inference_cfg_uncond: img
158
+ inference_cfg_img_scale: 1.0
159
+
160
+ - name: imagebind
161
+ id: 11
162
+ kind: codebook
163
+ start_token_key: start_of_imagebind
164
+ end_token_key: end_of_imagebind
165
+ pos_embed_size: 16
166
+ apply_pos_embed_in_forward: true
167
+ represent_vae: true
168
+ conditions: [rgb]
169
+ start_token: "<|imagebind_start|>"
170
+ end_token: "<|imagebind_end|>"
171
+ code_vocab_size: 8192
172
+ code_token_format: "<|imagebind_{i:04d}|>"
173
+ external_tokenizer_kind: vqvae
174
+ external_tokenizer_repo: "EPFL-VILAB/4M_tokenizers_ImageBind-H14-global_8k_16_224"
175
+ # Inference pipeline config
176
+ inference_decode_method: imagebind
177
+ inference_max_tokens: 17
178
+ inference_cfg_uncond: img
179
+ inference_cfg_img_scale: 1.0
180
+
181
+ - name: imagebindlocal
182
+ id: 12
183
+ kind: codebook
184
+ start_token_key: start_of_imagebindlocal
185
+ end_token_key: end_of_imagebindlocal
186
+ pos_embed_size: 1024
187
+ apply_pos_embed_in_forward: true
188
+ represent_vae: true
189
+ conditions: [rgb]
190
+ codebook_spatial_shape: [32, 32] # 1024 tokens, 32Γ—32 grid β†’ 2D RoPE
191
+ start_token: "<|imagebindlocal_start|>"
192
+ end_token: "<|imagebindlocal_end|>"
193
+ code_vocab_size: 8192
194
+ code_token_format: "<|imagebindlocal_{i:04d}|>"
195
+ external_tokenizer_kind: vqvae
196
+ external_tokenizer_repo: "EPFL-VILAB/4M_tokenizers_ImageBind-H14_8k_224-448"
197
+ # Inference pipeline config
198
+ inference_decode_method: imagebindlocal
199
+ inference_max_tokens: 1025
200
+ inference_cfg_uncond: img
201
+ inference_cfg_img_scale: 1.0
202
+
203
+ # ── NEW: cocodet (Pix2seq detection). stage1 = rgbβ†’cocodet only.
204
+ # Coords reuse det's x1/y1/x2/y2 tokens (dedup, 0 new); class = COCO-id-aligned
205
+ # tokens (~91 new). dispersed_code_tokens β†’ CE is gathered over the
206
+ # non-contiguous {coords βˆͺ class βˆͺ end} set, not a contiguous range.
207
+ # No reweight, no learnable pos_embed (RoPE + token-id carry order/role).
208
+ - name: cocodet
209
+ id: 13
210
+ kind: codebook
211
+ start_token_key: start_of_cocodet
212
+ end_token_key: end_of_cocodet
213
+ start_token: "<|cocodet_start|>"
214
+ end_token: "<|cocodet_end|>"
215
+ represent_vae: true
216
+ conditions: [rgb]
217
+ dispersed_code_tokens: true
218
+ code_token_groups:
219
+ - token_format: "<|x1_{i:03d}|>"
220
+ start: 0
221
+ end: 999
222
+ - token_format: "<|y1_{i:03d}|>"
223
+ start: 0
224
+ end: 999
225
+ - token_format: "<|x2_{i:03d}|>"
226
+ start: 0
227
+ end: 999
228
+ - token_format: "<|y2_{i:03d}|>"
229
+ start: 0
230
+ end: 999
231
+ - token_format: "<|coco_cls_{i:02d}|>"
232
+ start: 0
233
+ end: 90
234
+ # Inference pipeline config.
235
+ # decode_method is 'cocodet' (NOT 'detection') β€” cocodet's output is
236
+ # coords+class per box ending on cocodet_end, a DIFFERENT format from the
237
+ # grounding/det coords-only decode (generate_detection_coordonly). Handled by
238
+ # the dedicated model.generate_cocodet + inferencer.gen_cocodet (which do NOT
239
+ # touch the grounding decode).
240
+ inference_decode_method: cocodet
241
+ inference_max_tokens: 1000
242
+ inference_cfg_uncond: text
243
+ inference_add_instruction: false
conf/modalities/instruction_16mod_stage1_rgbcond.yaml ADDED
@@ -0,0 +1,266 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ modalities:
2
+ - name: text
3
+ id: 0
4
+ kind: text
5
+ start_token_key: bos_token_id
6
+ end_token_key: eos_token_id
7
+
8
+ - name: caption
9
+ id: 1
10
+ kind: text
11
+ start_token_key: bos_token_id
12
+ end_token_key: eos_token_id
13
+ represent_vae: true
14
+
15
+ - name: rgb
16
+ id: 2
17
+ kind: image
18
+ start_token_key: start_of_image
19
+ end_token_key: end_of_image
20
+ represent_vit: true
21
+ represent_vae: true
22
+
23
+ - name: depth
24
+ id: 3
25
+ kind: image
26
+ start_token_key: start_of_depth # REPLACE: per-modality start (token already exists in base vocab)
27
+ start_token: "<|depth_start|>"
28
+ end_token_key: end_of_image # shared end (image end is a structural terminator)
29
+ represent_vit: true
30
+ represent_vae: true
31
+
32
+ - name: normal
33
+ id: 4
34
+ kind: image
35
+ start_token_key: start_of_normal # REPLACE: per-modality start (token already exists in base vocab)
36
+ start_token: "<|normal_start|>"
37
+ end_token_key: end_of_image
38
+ represent_vit: true
39
+ represent_vae: true
40
+
41
+ - name: det
42
+ id: 5
43
+ kind: codebook
44
+ start_token_key: start_of_det
45
+ conditions: ["rgb"] # stage1 rgbcond: condition on rgb only (rgb->X warmup)
46
+ end_token_key: end_of_det
47
+ represent_vae: true
48
+ # NOTE: det's learnable pos_embed (grounding_pos_embed, size 4) is REMOVED here.
49
+ # The forward pos_embed loop is keyed by token-id range, and cocodet reuses
50
+ # det's x1/y1/x2/y2 tokens; with the pos_embed on, a multi-box cocodet sample
51
+ # (>4 coord tokens) would index the size-4 embedding out of bounds. det is not
52
+ # trained in modus (only present for coord-token-id alignment), so dropping its
53
+ # pos_embed is safe. reweight also removed (no reweight for det/cocodet).
54
+ start_token: "<|det_start|>"
55
+ end_token: "<|det_end|>"
56
+ extra_tokens: ["<|box_start|>", "<|box_end|>"]
57
+ code_token_groups:
58
+ - token_format: "<|x1_{i:03d}|>"
59
+ start: 0
60
+ end: 999
61
+ - token_format: "<|y1_{i:03d}|>"
62
+ start: 0
63
+ end: 999
64
+ - token_format: "<|x2_{i:03d}|>"
65
+ start: 0
66
+ end: 999
67
+ - token_format: "<|y2_{i:03d}|>"
68
+ start: 0
69
+ end: 999
70
+ - token_format: "<|score_{i:02d}|>"
71
+ start: 0
72
+ end: 99
73
+ # Inference pipeline config
74
+ inference_decode_method: detection
75
+ inference_max_tokens: 1000
76
+ inference_cfg_uncond: text
77
+ inference_add_instruction: false # det uses start_of_det token, not text instruction
78
+
79
+ - name: seg
80
+ id: 6
81
+ kind: image
82
+ start_token_key: start_of_seg # REPLACE: per-modality start (NEW token)
83
+ start_token: "<|seg_start|>"
84
+ end_token_key: end_of_image
85
+ represent_vit: true
86
+ represent_vae: true
87
+
88
+ - name: canny
89
+ id: 7
90
+ kind: image
91
+ start_token_key: start_of_canny # REPLACE: per-modality start (NEW token)
92
+ start_token: "<|canny_start|>"
93
+ end_token_key: end_of_image
94
+ represent_vit: true
95
+ represent_vae: true
96
+
97
+ - name: dino
98
+ id: 8
99
+ kind: codebook
100
+ start_token_key: start_of_dino
101
+ conditions: ["rgb"] # stage1 rgbcond: condition on rgb only (rgb->X warmup)
102
+ end_token_key: end_of_dino
103
+ pos_embed_size: 16
104
+ apply_pos_embed_in_forward: true
105
+ represent_vae: true
106
+ start_token: "<|dino_start|>"
107
+ end_token: "<|dino_end|>"
108
+ code_vocab_size: 8192
109
+ code_token_format: "<|dino_{i:04d}|>"
110
+ external_tokenizer_kind: vqvae
111
+ external_tokenizer_repo: "EPFL-VILAB/4M_tokenizers_DINOv2-B14-global_8k_16_224"
112
+ # Inference pipeline config
113
+ inference_decode_method: dino
114
+ inference_max_tokens: 17
115
+ inference_cfg_uncond: img
116
+ inference_cfg_img_scale: 1.0
117
+
118
+ - name: dinolocal
119
+ id: 9
120
+ kind: codebook
121
+ start_token_key: start_of_dinolocal
122
+ conditions: ["rgb"] # stage1 rgbcond: condition on rgb only (rgb->X warmup)
123
+ end_token_key: end_of_dinolocal
124
+ pos_embed_size: 1024
125
+ apply_pos_embed_in_forward: true
126
+ represent_vae: true
127
+ codebook_spatial_shape: [32, 32] # 1024 tokens, 32Γ—32 grid β†’ 2D RoPE
128
+ start_token: "<|dinolocal_start|>"
129
+ end_token: "<|dinolocal_end|>"
130
+ code_vocab_size: 8192
131
+ code_token_format: "<|dinolocal_{i:04d}|>"
132
+ external_tokenizer_kind: vqvae
133
+ external_tokenizer_repo: "EPFL-VILAB/4M_tokenizers_DINOv2-B14_8k_224-448"
134
+ # Inference pipeline config
135
+ inference_decode_method: dinolocal
136
+ inference_max_tokens: 1025
137
+ inference_cfg_uncond: img
138
+ inference_cfg_img_scale: 1.0
139
+
140
+ - name: clip
141
+ id: 10
142
+ kind: codebook
143
+ start_token_key: start_of_clip
144
+ conditions: ["rgb"] # stage1 rgbcond: condition on rgb only (rgb->X warmup)
145
+ end_token_key: end_of_clip
146
+ pos_embed_size: 784
147
+ apply_pos_embed_in_forward: true
148
+ represent_vae: true
149
+ codebook_spatial_shape: [28, 28] # 784 tokens, 28Γ—28 grid β†’ 2D RoPE
150
+ start_token: "<|clip_start|>"
151
+ end_token: "<|clip_end|>"
152
+ code_vocab_size: 8192
153
+ code_token_format: "<|clip_{i:04d}|>"
154
+ external_tokenizer_kind: vqvae
155
+ external_tokenizer_repo: "EPFL-VILAB/4M_tokenizers_CLIP-B16_8k_224-448"
156
+ # Inference pipeline config
157
+ inference_decode_method: clip
158
+ inference_max_tokens: 785
159
+ inference_cfg_uncond: img
160
+ inference_cfg_img_scale: 1.0
161
+
162
+ - name: imagebind
163
+ id: 11
164
+ kind: codebook
165
+ start_token_key: start_of_imagebind
166
+ conditions: ["rgb"] # stage1 rgbcond: condition on rgb only (rgb->X warmup)
167
+ end_token_key: end_of_imagebind
168
+ pos_embed_size: 16
169
+ apply_pos_embed_in_forward: true
170
+ represent_vae: true
171
+ start_token: "<|imagebind_start|>"
172
+ end_token: "<|imagebind_end|>"
173
+ code_vocab_size: 8192
174
+ code_token_format: "<|imagebind_{i:04d}|>"
175
+ external_tokenizer_kind: vqvae
176
+ external_tokenizer_repo: "EPFL-VILAB/4M_tokenizers_ImageBind-H14-global_8k_16_224"
177
+ # Inference pipeline config
178
+ inference_decode_method: imagebind
179
+ inference_max_tokens: 17
180
+ inference_cfg_uncond: img
181
+ inference_cfg_img_scale: 1.0
182
+
183
+ - name: imagebindlocal
184
+ id: 12
185
+ kind: codebook
186
+ start_token_key: start_of_imagebindlocal
187
+ conditions: ["rgb"] # stage1 rgbcond: condition on rgb only (rgb->X warmup)
188
+ end_token_key: end_of_imagebindlocal
189
+ pos_embed_size: 1024
190
+ apply_pos_embed_in_forward: true
191
+ represent_vae: true
192
+ codebook_spatial_shape: [32, 32] # 1024 tokens, 32Γ—32 grid β†’ 2D RoPE
193
+ start_token: "<|imagebindlocal_start|>"
194
+ end_token: "<|imagebindlocal_end|>"
195
+ code_vocab_size: 8192
196
+ code_token_format: "<|imagebindlocal_{i:04d}|>"
197
+ external_tokenizer_kind: vqvae
198
+ external_tokenizer_repo: "EPFL-VILAB/4M_tokenizers_ImageBind-H14_8k_224-448"
199
+ # Inference pipeline config
200
+ inference_decode_method: imagebindlocal
201
+ inference_max_tokens: 1025
202
+ inference_cfg_uncond: img
203
+ inference_cfg_img_scale: 1.0
204
+
205
+ # ── NEW: cocodet (Pix2seq detection). stage1 = rgbβ†’cocodet only.
206
+ # Coords reuse det's x1/y1/x2/y2 tokens (dedup, 0 new); class = COCO-id-aligned
207
+ # tokens (~91 new). dispersed_code_tokens β†’ CE is gathered over the
208
+ # non-contiguous {coords βˆͺ class βˆͺ end} set, not a contiguous range.
209
+ # No reweight, no learnable pos_embed (RoPE + token-id carry order/role).
210
+ - name: cocodet
211
+ id: 13
212
+ kind: codebook
213
+ start_token_key: start_of_cocodet
214
+ conditions: ["rgb"] # stage1 rgbcond: condition on rgb only (rgb->X warmup)
215
+ end_token_key: end_of_cocodet
216
+ start_token: "<|cocodet_start|>"
217
+ end_token: "<|cocodet_end|>"
218
+ represent_vae: true
219
+ dispersed_code_tokens: true
220
+ code_token_groups:
221
+ - token_format: "<|x1_{i:03d}|>"
222
+ start: 0
223
+ end: 999
224
+ - token_format: "<|y1_{i:03d}|>"
225
+ start: 0
226
+ end: 999
227
+ - token_format: "<|x2_{i:03d}|>"
228
+ start: 0
229
+ end: 999
230
+ - token_format: "<|y2_{i:03d}|>"
231
+ start: 0
232
+ end: 999
233
+ - token_format: "<|coco_cls_{i:02d}|>"
234
+ start: 0
235
+ end: 90
236
+ # Inference pipeline config.
237
+ # decode_method is 'cocodet' (NOT 'detection') β€” cocodet's output is
238
+ # coords+class per box ending on cocodet_end, a DIFFERENT format from the
239
+ # grounding/det coords-only decode (generate_detection_coordonly). Handled by
240
+ # the dedicated model.generate_cocodet + inferencer.gen_cocodet (which do NOT
241
+ # touch the grounding decode).
242
+ inference_decode_method: cocodet
243
+ inference_max_tokens: 1000
244
+ inference_cfg_uncond: text
245
+ inference_add_instruction: false
246
+
247
+ # ── NEW image modalities (stage2): SAM segmentation / edge. Mirror seg/canny
248
+ # (direct-PNG image, VIT+VAE, MSE). REPLACE: per-modality start token, shared
249
+ # end_of_image. These start tokens are NEW (created via start_token field).
250
+ - name: samseg
251
+ id: 14
252
+ kind: image
253
+ start_token_key: start_of_samseg
254
+ start_token: "<|samseg_start|>"
255
+ end_token_key: end_of_image
256
+ represent_vit: true
257
+ represent_vae: true
258
+
259
+ - name: samedge
260
+ id: 15
261
+ kind: image
262
+ start_token_key: start_of_samedge
263
+ start_token: "<|samedge_start|>"
264
+ end_token_key: end_of_image
265
+ represent_vit: true
266
+ represent_vae: true
conf/modalities/instruction_16mod_stage2.yaml ADDED
@@ -0,0 +1,272 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ modalities:
2
+ - name: text
3
+ id: 0
4
+ kind: text
5
+ start_token_key: bos_token_id
6
+ end_token_key: eos_token_id
7
+ inference_add_instruction: false # stage2/3 trained with use_target_instruction=false (no generic instr)
8
+
9
+ - name: caption
10
+ id: 1
11
+ kind: text
12
+ start_token_key: bos_token_id
13
+ end_token_key: eos_token_id
14
+ represent_vae: true
15
+ inference_add_instruction: false # any2caption (stage2) trained without a target instruction
16
+
17
+ - name: rgb
18
+ id: 2
19
+ kind: image
20
+ start_token_key: start_of_image
21
+ end_token_key: end_of_image
22
+ represent_vit: true
23
+ represent_vae: true
24
+ inference_add_instruction: false # generic [start X] not trained in stage2/3 (use_target_instruction=false)
25
+
26
+ - name: depth
27
+ id: 3
28
+ kind: image
29
+ start_token_key: start_of_depth # REPLACE: per-modality start (token already exists in base vocab)
30
+ start_token: "<|depth_start|>"
31
+ end_token_key: end_of_image # shared end (image end is a structural terminator)
32
+ represent_vit: true
33
+ represent_vae: true
34
+ inference_add_instruction: false
35
+
36
+ - name: normal
37
+ id: 4
38
+ kind: image
39
+ start_token_key: start_of_normal # REPLACE: per-modality start (token already exists in base vocab)
40
+ start_token: "<|normal_start|>"
41
+ end_token_key: end_of_image
42
+ represent_vit: true
43
+ represent_vae: true
44
+ inference_add_instruction: false
45
+
46
+ - name: det
47
+ id: 5
48
+ kind: codebook
49
+ start_token_key: start_of_det
50
+ end_token_key: end_of_det
51
+ represent_vae: true
52
+ # NOTE: det's learnable pos_embed (grounding_pos_embed, size 4) is REMOVED here.
53
+ # The forward pos_embed loop is keyed by token-id range, and cocodet reuses
54
+ # det's x1/y1/x2/y2 tokens; with the pos_embed on, a multi-box cocodet sample
55
+ # (>4 coord tokens) would index the size-4 embedding out of bounds. det is not
56
+ # trained in modus (only present for coord-token-id alignment), so dropping its
57
+ # pos_embed is safe. reweight also removed (no reweight for det/cocodet).
58
+ start_token: "<|det_start|>"
59
+ end_token: "<|det_end|>"
60
+ extra_tokens: ["<|box_start|>", "<|box_end|>"]
61
+ code_token_groups:
62
+ - token_format: "<|x1_{i:03d}|>"
63
+ start: 0
64
+ end: 999
65
+ - token_format: "<|y1_{i:03d}|>"
66
+ start: 0
67
+ end: 999
68
+ - token_format: "<|x2_{i:03d}|>"
69
+ start: 0
70
+ end: 999
71
+ - token_format: "<|y2_{i:03d}|>"
72
+ start: 0
73
+ end: 999
74
+ - token_format: "<|score_{i:02d}|>"
75
+ start: 0
76
+ end: 99
77
+ # Inference pipeline config
78
+ inference_decode_method: detection
79
+ inference_max_tokens: 1000
80
+ inference_cfg_uncond: text
81
+ inference_add_instruction: false # det uses start_of_det token, not text instruction
82
+
83
+ - name: seg
84
+ id: 6
85
+ kind: image
86
+ start_token_key: start_of_seg # REPLACE: per-modality start (NEW token)
87
+ start_token: "<|seg_start|>"
88
+ end_token_key: end_of_image
89
+ represent_vit: true
90
+ represent_vae: true
91
+
92
+ - name: canny
93
+ id: 7
94
+ kind: image
95
+ start_token_key: start_of_canny # REPLACE: per-modality start (NEW token)
96
+ start_token: "<|canny_start|>"
97
+ end_token_key: end_of_image
98
+ represent_vit: true
99
+ represent_vae: true
100
+ inference_add_instruction: false
101
+
102
+ - name: dino
103
+ id: 8
104
+ kind: codebook
105
+ start_token_key: start_of_dino
106
+ end_token_key: end_of_dino
107
+ pos_embed_size: 16
108
+ apply_pos_embed_in_forward: true
109
+ represent_vae: true
110
+ start_token: "<|dino_start|>"
111
+ end_token: "<|dino_end|>"
112
+ code_vocab_size: 8192
113
+ code_token_format: "<|dino_{i:04d}|>"
114
+ external_tokenizer_kind: vqvae
115
+ external_tokenizer_repo: "EPFL-VILAB/4M_tokenizers_DINOv2-B14-global_8k_16_224"
116
+ # Inference pipeline config
117
+ inference_decode_method: dino
118
+ inference_max_tokens: 17
119
+ inference_cfg_uncond: img
120
+ inference_cfg_img_scale: 1.0
121
+ inference_add_instruction: false # start_of_dino token triggers it; generic instr not in stage2/3
122
+
123
+ - name: dinolocal
124
+ id: 9
125
+ kind: codebook
126
+ start_token_key: start_of_dinolocal
127
+ end_token_key: end_of_dinolocal
128
+ pos_embed_size: 1024
129
+ apply_pos_embed_in_forward: true
130
+ represent_vae: true
131
+ codebook_spatial_shape: [32, 32] # 1024 tokens, 32Γ—32 grid β†’ 2D RoPE
132
+ start_token: "<|dinolocal_start|>"
133
+ end_token: "<|dinolocal_end|>"
134
+ code_vocab_size: 8192
135
+ code_token_format: "<|dinolocal_{i:04d}|>"
136
+ external_tokenizer_kind: vqvae
137
+ external_tokenizer_repo: "EPFL-VILAB/4M_tokenizers_DINOv2-B14_8k_224-448"
138
+ # Inference pipeline config
139
+ inference_decode_method: dinolocal
140
+ inference_max_tokens: 1025
141
+ inference_cfg_uncond: img
142
+ inference_cfg_img_scale: 1.0
143
+ inference_add_instruction: false
144
+
145
+ - name: clip
146
+ id: 10
147
+ kind: codebook
148
+ start_token_key: start_of_clip
149
+ end_token_key: end_of_clip
150
+ pos_embed_size: 784
151
+ apply_pos_embed_in_forward: true
152
+ represent_vae: true
153
+ codebook_spatial_shape: [28, 28] # 784 tokens, 28Γ—28 grid β†’ 2D RoPE
154
+ start_token: "<|clip_start|>"
155
+ end_token: "<|clip_end|>"
156
+ code_vocab_size: 8192
157
+ code_token_format: "<|clip_{i:04d}|>"
158
+ external_tokenizer_kind: vqvae
159
+ external_tokenizer_repo: "EPFL-VILAB/4M_tokenizers_CLIP-B16_8k_224-448"
160
+ # Inference pipeline config
161
+ inference_decode_method: clip
162
+ inference_max_tokens: 785
163
+ inference_cfg_uncond: img
164
+ inference_cfg_img_scale: 1.0
165
+ inference_add_instruction: false
166
+
167
+ - name: imagebind
168
+ id: 11
169
+ kind: codebook
170
+ start_token_key: start_of_imagebind
171
+ end_token_key: end_of_imagebind
172
+ pos_embed_size: 16
173
+ apply_pos_embed_in_forward: true
174
+ represent_vae: true
175
+ start_token: "<|imagebind_start|>"
176
+ end_token: "<|imagebind_end|>"
177
+ code_vocab_size: 8192
178
+ code_token_format: "<|imagebind_{i:04d}|>"
179
+ external_tokenizer_kind: vqvae
180
+ external_tokenizer_repo: "EPFL-VILAB/4M_tokenizers_ImageBind-H14-global_8k_16_224"
181
+ # Inference pipeline config
182
+ inference_decode_method: imagebind
183
+ inference_max_tokens: 17
184
+ inference_cfg_uncond: img
185
+ inference_cfg_img_scale: 1.0
186
+ inference_add_instruction: false
187
+
188
+ - name: imagebindlocal
189
+ id: 12
190
+ kind: codebook
191
+ start_token_key: start_of_imagebindlocal
192
+ end_token_key: end_of_imagebindlocal
193
+ pos_embed_size: 1024
194
+ apply_pos_embed_in_forward: true
195
+ represent_vae: true
196
+ codebook_spatial_shape: [32, 32] # 1024 tokens, 32Γ—32 grid β†’ 2D RoPE
197
+ start_token: "<|imagebindlocal_start|>"
198
+ end_token: "<|imagebindlocal_end|>"
199
+ code_vocab_size: 8192
200
+ code_token_format: "<|imagebindlocal_{i:04d}|>"
201
+ external_tokenizer_kind: vqvae
202
+ external_tokenizer_repo: "EPFL-VILAB/4M_tokenizers_ImageBind-H14_8k_224-448"
203
+ # Inference pipeline config
204
+ inference_decode_method: imagebindlocal
205
+ inference_max_tokens: 1025
206
+ inference_cfg_uncond: img
207
+ inference_cfg_img_scale: 1.0
208
+ inference_add_instruction: false
209
+
210
+ # ── NEW: cocodet (Pix2seq detection). stage1 = rgbβ†’cocodet only.
211
+ # Coords reuse det's x1/y1/x2/y2 tokens (dedup, 0 new); class = COCO-id-aligned
212
+ # tokens (~91 new). dispersed_code_tokens β†’ CE is gathered over the
213
+ # non-contiguous {coords βˆͺ class βˆͺ end} set, not a contiguous range.
214
+ # No reweight, no learnable pos_embed (RoPE + token-id carry order/role).
215
+ - name: cocodet
216
+ id: 13
217
+ kind: codebook
218
+ start_token_key: start_of_cocodet
219
+ end_token_key: end_of_cocodet
220
+ start_token: "<|cocodet_start|>"
221
+ end_token: "<|cocodet_end|>"
222
+ represent_vae: true
223
+ dispersed_code_tokens: true
224
+ code_token_groups:
225
+ - token_format: "<|x1_{i:03d}|>"
226
+ start: 0
227
+ end: 999
228
+ - token_format: "<|y1_{i:03d}|>"
229
+ start: 0
230
+ end: 999
231
+ - token_format: "<|x2_{i:03d}|>"
232
+ start: 0
233
+ end: 999
234
+ - token_format: "<|y2_{i:03d}|>"
235
+ start: 0
236
+ end: 999
237
+ - token_format: "<|coco_cls_{i:02d}|>"
238
+ start: 0
239
+ end: 90
240
+ # Inference pipeline config.
241
+ # decode_method is 'cocodet' (NOT 'detection') β€” cocodet's output is
242
+ # coords+class per box ending on cocodet_end, a DIFFERENT format from the
243
+ # grounding/det coords-only decode (generate_detection_coordonly). Handled by
244
+ # the dedicated model.generate_cocodet + inferencer.gen_cocodet (which do NOT
245
+ # touch the grounding decode).
246
+ inference_decode_method: cocodet
247
+ inference_max_tokens: 1000
248
+ inference_cfg_uncond: text
249
+ inference_add_instruction: false
250
+
251
+ # ── NEW image modalities (stage2): SAM segmentation / edge. Mirror seg/canny
252
+ # (direct-PNG image, VIT+VAE, MSE). REPLACE: per-modality start token, shared
253
+ # end_of_image. These start tokens are NEW (created via start_token field).
254
+ - name: samseg
255
+ id: 14
256
+ kind: image
257
+ start_token_key: start_of_samseg
258
+ start_token: "<|samseg_start|>"
259
+ end_token_key: end_of_image
260
+ represent_vit: true
261
+ represent_vae: true
262
+ inference_add_instruction: false
263
+
264
+ - name: samedge
265
+ id: 15
266
+ kind: image
267
+ start_token_key: start_of_samedge
268
+ start_token: "<|samedge_start|>"
269
+ end_token_key: end_of_image
270
+ represent_vit: true
271
+ represent_vae: true
272
+ inference_add_instruction: false
conf/modalities/instruction_2d_only_stage1.yaml ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 2D-only stage1 modality registry.
2
+ #
3
+ # Keeps text/caption and image-like modalities only. Drops DET/DINO/CLIP/
4
+ # ImageBind codebook modalities so this run can isolate whether 1D/codebook CE
5
+ # is the main source of Hunyuan T2I prior drift.
6
+
7
+ modalities:
8
+ - name: text
9
+ id: 0
10
+ kind: text
11
+ start_token_key: bos_token_id
12
+ end_token_key: eos_token_id
13
+
14
+ - name: caption
15
+ id: 1
16
+ kind: text
17
+ start_token_key: bos_token_id
18
+ end_token_key: eos_token_id
19
+ represent_vae: true
20
+ conditions: [rgb, depth, normal, seg, canny]
21
+
22
+ - name: rgb
23
+ id: 2
24
+ kind: image
25
+ start_token_key: start_of_image
26
+ end_token_key: end_of_image
27
+ represent_vit: true
28
+ represent_vae: true
29
+ conditions: [caption, depth, normal, seg, canny]
30
+
31
+ - name: depth
32
+ id: 3
33
+ kind: image
34
+ start_token_key: start_of_image
35
+ end_token_key: end_of_image
36
+ represent_vit: true
37
+ represent_vae: true
38
+ conditions: [caption, rgb, normal, seg, canny]
39
+
40
+ - name: normal
41
+ id: 4
42
+ kind: image
43
+ start_token_key: start_of_image
44
+ end_token_key: end_of_image
45
+ represent_vit: true
46
+ represent_vae: true
47
+ conditions: [caption, rgb, depth, seg, canny]
48
+
49
+ - name: seg
50
+ id: 6
51
+ kind: image
52
+ start_token_key: start_of_image
53
+ end_token_key: end_of_image
54
+ represent_vit: true
55
+ represent_vae: true
56
+ conditions: [caption, rgb, depth, normal, canny]
57
+
58
+ - name: canny
59
+ id: 7
60
+ kind: image
61
+ start_token_key: start_of_image
62
+ end_token_key: end_of_image
63
+ represent_vit: true
64
+ represent_vae: true
65
+ conditions: [caption, rgb, depth, normal, seg]
conf/modalities/instruction_9modality_stage1.yaml ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ modalities:
2
+ - name: text
3
+ id: 0
4
+ kind: text
5
+ start_token_key: bos_token_id
6
+ end_token_key: eos_token_id
7
+
8
+ - name: caption
9
+ id: 1
10
+ kind: text
11
+ start_token_key: bos_token_id
12
+ end_token_key: eos_token_id
13
+ represent_vae: true
14
+ conditions: [rgb, grounding, dino]
15
+
16
+ - name: rgb
17
+ id: 2
18
+ kind: image
19
+ start_token_key: start_of_image
20
+ end_token_key: end_of_image
21
+ represent_vit: true
22
+ represent_vae: true
23
+ conditions: [caption, grounding, dino]
24
+
25
+ - name: depth
26
+ id: 3
27
+ kind: image
28
+ start_token_key: start_of_image
29
+ end_token_key: end_of_image
30
+ represent_vit: true
31
+ represent_vae: true
32
+
33
+ - name: normal
34
+ id: 4
35
+ kind: image
36
+ start_token_key: start_of_image
37
+ end_token_key: end_of_image
38
+ represent_vit: true
39
+ represent_vae: true
40
+
41
+ - name: det
42
+ id: 5
43
+ kind: codebook
44
+ start_token_key: start_of_det
45
+ end_token_key: end_of_det
46
+ represent_vae: true
47
+ pos_embed_size: 4
48
+ apply_pos_embed_in_forward: true
49
+ pos_embed_name: grounding # checkpoint compat: attr is "grounding_pos_embed"
50
+ conditions: [rgb]
51
+ loss:
52
+ reweight: true
53
+ reweight_min_w: 0.005
54
+ start_token: "<|det_start|>"
55
+ end_token: "<|det_end|>"
56
+ extra_tokens: ["<|box_start|>", "<|box_end|>"]
57
+ code_token_groups:
58
+ - token_format: "<|x1_{i:03d}|>"
59
+ start: 0
60
+ end: 999
61
+ - token_format: "<|y1_{i:03d}|>"
62
+ start: 0
63
+ end: 999
64
+ - token_format: "<|x2_{i:03d}|>"
65
+ start: 0
66
+ end: 999
67
+ - token_format: "<|y2_{i:03d}|>"
68
+ start: 0
69
+ end: 999
70
+ - token_format: "<|score_{i:02d}|>"
71
+ start: 0
72
+ end: 99
73
+ # Inference pipeline config
74
+ inference_decode_method: detection
75
+ inference_max_tokens: 1000
76
+ inference_cfg_uncond: text
77
+ inference_add_instruction: false # det uses start_of_det token, not text instruction
78
+
79
+ - name: seg
80
+ id: 6
81
+ kind: image
82
+ start_token_key: start_of_image
83
+ end_token_key: end_of_image
84
+ represent_vit: true
85
+ represent_vae: true
86
+
87
+ - name: canny
88
+ id: 7
89
+ kind: image
90
+ start_token_key: start_of_image
91
+ end_token_key: end_of_image
92
+ represent_vit: true
93
+ represent_vae: true
94
+
95
+ - name: dino
96
+ id: 8
97
+ kind: codebook
98
+ start_token_key: start_of_dino
99
+ end_token_key: end_of_dino
100
+ pos_embed_size: 16
101
+ apply_pos_embed_in_forward: true
102
+ represent_vae: true
103
+ conditions: [rgb]
104
+ start_token: "<|dino_start|>"
105
+ end_token: "<|dino_end|>"
106
+ code_vocab_size: 8192
107
+ code_token_format: "<|dino_{i:04d}|>"
108
+ external_tokenizer_kind: vqvae
109
+ external_tokenizer_repo: "EPFL-VILAB/4M_tokenizers_DINOv2-B14-global_8k_16_224"
110
+ # Inference pipeline config
111
+ inference_decode_method: dino
112
+ inference_max_tokens: 17
113
+ inference_cfg_uncond: img
114
+ inference_cfg_img_scale: 1.0
conf/modalities/instruction_9modality_stage3.yaml ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Stage 3 modality config β€” 9 modalities (text + 8 task), unconstrained conditions.
2
+ #
3
+ # Mirrors instruction_9modality_stage1.yaml but drops every `conditions: [...]`
4
+ # restriction so any modality can be conditioned on any other (matches the old
5
+ # stage3 training run that was originally configured via CLI flags only).
6
+ # det reweight + det/dino learnable pos-embeds are preserved.
7
+
8
+ modalities:
9
+ - name: text
10
+ id: 0
11
+ kind: text
12
+ start_token_key: bos_token_id
13
+ end_token_key: eos_token_id
14
+
15
+ - name: caption
16
+ id: 1
17
+ kind: text
18
+ start_token_key: bos_token_id
19
+ end_token_key: eos_token_id
20
+ represent_vae: true
21
+
22
+ - name: rgb
23
+ id: 2
24
+ kind: image
25
+ start_token_key: start_of_image
26
+ end_token_key: end_of_image
27
+ represent_vit: true
28
+ represent_vae: true
29
+
30
+ - name: depth
31
+ id: 3
32
+ kind: image
33
+ start_token_key: start_of_image
34
+ end_token_key: end_of_image
35
+ represent_vit: true
36
+ represent_vae: true
37
+
38
+ - name: normal
39
+ id: 4
40
+ kind: image
41
+ start_token_key: start_of_image
42
+ end_token_key: end_of_image
43
+ represent_vit: true
44
+ represent_vae: true
45
+
46
+ - name: det
47
+ id: 5
48
+ kind: codebook
49
+ start_token_key: start_of_det
50
+ end_token_key: end_of_det
51
+ represent_vae: true
52
+ pos_embed_size: 4
53
+ apply_pos_embed_in_forward: true
54
+ pos_embed_name: grounding # checkpoint compat: attr is "grounding_pos_embed"
55
+ loss:
56
+ reweight: true
57
+ reweight_min_w: 0.005
58
+ start_token: "<|det_start|>"
59
+ end_token: "<|det_end|>"
60
+ extra_tokens: ["<|box_start|>", "<|box_end|>"]
61
+ code_token_groups:
62
+ - token_format: "<|x1_{i:03d}|>"
63
+ start: 0
64
+ end: 999
65
+ - token_format: "<|y1_{i:03d}|>"
66
+ start: 0
67
+ end: 999
68
+ - token_format: "<|x2_{i:03d}|>"
69
+ start: 0
70
+ end: 999
71
+ - token_format: "<|y2_{i:03d}|>"
72
+ start: 0
73
+ end: 999
74
+ - token_format: "<|score_{i:02d}|>"
75
+ start: 0
76
+ end: 99
77
+ inference_decode_method: detection
78
+ inference_max_tokens: 1000
79
+ inference_cfg_uncond: text
80
+ inference_add_instruction: false
81
+
82
+ - name: seg
83
+ id: 6
84
+ kind: image
85
+ start_token_key: start_of_image
86
+ end_token_key: end_of_image
87
+ represent_vit: true
88
+ represent_vae: true
89
+
90
+ - name: canny
91
+ id: 7
92
+ kind: image
93
+ start_token_key: start_of_image
94
+ end_token_key: end_of_image
95
+ represent_vit: true
96
+ represent_vae: true
97
+
98
+ - name: dino
99
+ id: 8
100
+ kind: codebook
101
+ start_token_key: start_of_dino
102
+ end_token_key: end_of_dino
103
+ pos_embed_size: 16
104
+ apply_pos_embed_in_forward: true
105
+ represent_vae: true
106
+ start_token: "<|dino_start|>"
107
+ end_token: "<|dino_end|>"
108
+ code_vocab_size: 8192
109
+ code_token_format: "<|dino_{i:04d}|>"
110
+ external_tokenizer_kind: vqvae
111
+ external_tokenizer_repo: "EPFL-VILAB/4M_tokenizers_DINOv2-B14-global_8k_16_224"
112
+ inference_decode_method: dino
113
+ inference_max_tokens: 17
114
+ inference_cfg_uncond: img
115
+ inference_cfg_img_scale: 1.0
conf/modalities/instruction_hunyuan_16mod_stage1.yaml ADDED
@@ -0,0 +1,257 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ modalities:
2
+ - name: text
3
+ id: 0
4
+ kind: text
5
+ start_token_key: bos_token_id
6
+ end_token_key: eos_token_id
7
+
8
+ - name: caption
9
+ id: 1
10
+ kind: text
11
+ start_token_key: bos_token_id
12
+ end_token_key: eos_token_id
13
+ represent_vae: true
14
+ conditions: [rgb, grounding, dino, dinolocal, clip, imagebind, imagebindlocal]
15
+
16
+ - name: rgb
17
+ id: 2
18
+ kind: image
19
+ start_token_key: start_of_image
20
+ end_token_key: end_of_image
21
+ represent_vit: true
22
+ represent_vae: true
23
+ conditions: [caption, grounding, dino, dinolocal, clip, imagebind, imagebindlocal]
24
+
25
+ - name: depth
26
+ id: 3
27
+ kind: image
28
+ start_token_key: start_of_image
29
+ end_token_key: end_of_image
30
+ represent_vit: true
31
+ represent_vae: true
32
+
33
+ - name: normal
34
+ id: 4
35
+ kind: image
36
+ start_token_key: start_of_image
37
+ end_token_key: end_of_image
38
+ represent_vit: true
39
+ represent_vae: true
40
+
41
+ - name: det
42
+ id: 5
43
+ kind: codebook
44
+ start_token_key: start_of_det
45
+ end_token_key: end_of_det
46
+ represent_vae: true
47
+ # det's learnable pos_embed (size 4) + reweight are REMOVED so cocodet β€” which
48
+ # reuses det's x1/y1/x2/y2 tokens (coco_cls is cocodet's own) β€” can carry >4
49
+ # coord tokens per sample without indexing the size-4 pos_embed out of bounds.
50
+ # det is not a target here (only present for coord-token-id alignment);
51
+ # grounding reuses these tokens and is now RoPE-only for its bbox coords.
52
+ conditions: [rgb]
53
+ start_token: "<|det_start|>"
54
+ end_token: "<|det_end|>"
55
+ extra_tokens: ["<|box_start|>", "<|box_end|>"]
56
+ code_token_groups:
57
+ - token_format: "<|x1_{i:03d}|>"
58
+ start: 0
59
+ end: 999
60
+ - token_format: "<|y1_{i:03d}|>"
61
+ start: 0
62
+ end: 999
63
+ - token_format: "<|x2_{i:03d}|>"
64
+ start: 0
65
+ end: 999
66
+ - token_format: "<|y2_{i:03d}|>"
67
+ start: 0
68
+ end: 999
69
+ - token_format: "<|score_{i:02d}|>"
70
+ start: 0
71
+ end: 99
72
+ # Inference pipeline config
73
+ inference_decode_method: detection
74
+ inference_max_tokens: 1000
75
+ inference_cfg_uncond: text
76
+ inference_add_instruction: false # det uses start_of_det token, not text instruction
77
+
78
+ - name: seg
79
+ id: 6
80
+ kind: image
81
+ start_token_key: start_of_image
82
+ end_token_key: end_of_image
83
+ represent_vit: true
84
+ represent_vae: true
85
+
86
+ - name: canny
87
+ id: 7
88
+ kind: image
89
+ start_token_key: start_of_image
90
+ end_token_key: end_of_image
91
+ represent_vit: true
92
+ represent_vae: true
93
+
94
+ - name: dino
95
+ id: 8
96
+ kind: codebook
97
+ start_token_key: start_of_dino
98
+ end_token_key: end_of_dino
99
+ pos_embed_size: 16
100
+ apply_pos_embed_in_forward: true
101
+ represent_vae: true
102
+ conditions: [rgb]
103
+ start_token: "<|dino_start|>"
104
+ end_token: "<|dino_end|>"
105
+ code_vocab_size: 8192
106
+ code_token_format: "<|dino_{i:04d}|>"
107
+ external_tokenizer_kind: vqvae
108
+ external_tokenizer_repo: "EPFL-VILAB/4M_tokenizers_DINOv2-B14-global_8k_16_224"
109
+ # Inference pipeline config
110
+ inference_decode_method: dino
111
+ inference_max_tokens: 17
112
+ inference_cfg_uncond: img
113
+ inference_cfg_img_scale: 1.0
114
+
115
+ - name: dinolocal
116
+ id: 9
117
+ kind: codebook
118
+ start_token_key: start_of_dinolocal
119
+ end_token_key: end_of_dinolocal
120
+ pos_embed_size: 1024
121
+ apply_pos_embed_in_forward: true
122
+ represent_vae: true
123
+ conditions: [rgb]
124
+ codebook_spatial_shape: [32, 32] # 1024 tokens, 32Γ—32 grid β†’ 2D RoPE
125
+ start_token: "<|dinolocal_start|>"
126
+ end_token: "<|dinolocal_end|>"
127
+ code_vocab_size: 8192
128
+ code_token_format: "<|dinolocal_{i:04d}|>"
129
+ external_tokenizer_kind: vqvae
130
+ external_tokenizer_repo: "EPFL-VILAB/4M_tokenizers_DINOv2-B14_8k_224-448"
131
+ # Inference pipeline config
132
+ inference_decode_method: dinolocal
133
+ inference_max_tokens: 1025
134
+ inference_cfg_uncond: img
135
+ inference_cfg_img_scale: 1.0
136
+
137
+ - name: clip
138
+ id: 10
139
+ kind: codebook
140
+ start_token_key: start_of_clip
141
+ end_token_key: end_of_clip
142
+ pos_embed_size: 784
143
+ apply_pos_embed_in_forward: true
144
+ represent_vae: true
145
+ conditions: [rgb]
146
+ codebook_spatial_shape: [28, 28] # 784 tokens, 28Γ—28 grid β†’ 2D RoPE
147
+ start_token: "<|clip_start|>"
148
+ end_token: "<|clip_end|>"
149
+ code_vocab_size: 8192
150
+ code_token_format: "<|clip_{i:04d}|>"
151
+ external_tokenizer_kind: vqvae
152
+ external_tokenizer_repo: "EPFL-VILAB/4M_tokenizers_CLIP-B16_8k_224-448"
153
+ # Inference pipeline config
154
+ inference_decode_method: clip
155
+ inference_max_tokens: 785
156
+ inference_cfg_uncond: img
157
+ inference_cfg_img_scale: 1.0
158
+
159
+ - name: imagebind
160
+ id: 11
161
+ kind: codebook
162
+ start_token_key: start_of_imagebind
163
+ end_token_key: end_of_imagebind
164
+ pos_embed_size: 16
165
+ apply_pos_embed_in_forward: true
166
+ represent_vae: true
167
+ conditions: [rgb]
168
+ start_token: "<|imagebind_start|>"
169
+ end_token: "<|imagebind_end|>"
170
+ code_vocab_size: 8192
171
+ code_token_format: "<|imagebind_{i:04d}|>"
172
+ external_tokenizer_kind: vqvae
173
+ external_tokenizer_repo: "EPFL-VILAB/4M_tokenizers_ImageBind-H14-global_8k_16_224"
174
+ # Inference pipeline config
175
+ inference_decode_method: imagebind
176
+ inference_max_tokens: 17
177
+ inference_cfg_uncond: img
178
+ inference_cfg_img_scale: 1.0
179
+
180
+ - name: imagebindlocal
181
+ id: 12
182
+ kind: codebook
183
+ start_token_key: start_of_imagebindlocal
184
+ end_token_key: end_of_imagebindlocal
185
+ pos_embed_size: 1024
186
+ apply_pos_embed_in_forward: true
187
+ represent_vae: true
188
+ conditions: [rgb]
189
+ codebook_spatial_shape: [32, 32] # 1024 tokens, 32Γ—32 grid β†’ 2D RoPE
190
+ start_token: "<|imagebindlocal_start|>"
191
+ end_token: "<|imagebindlocal_end|>"
192
+ code_vocab_size: 8192
193
+ code_token_format: "<|imagebindlocal_{i:04d}|>"
194
+ external_tokenizer_kind: vqvae
195
+ external_tokenizer_repo: "EPFL-VILAB/4M_tokenizers_ImageBind-H14_8k_224-448"
196
+ # Inference pipeline config
197
+ inference_decode_method: imagebindlocal
198
+ inference_max_tokens: 1025
199
+ inference_cfg_uncond: img
200
+ inference_cfg_img_scale: 1.0
201
+
202
+ # ── cocodet (Pix2seq detection). Coords reuse det's x1/y1/x2/y2 tokens (dedup,
203
+ # 0 new); class = COCO-id-aligned tokens (~91 new). dispersed_code_tokens β†’ CE
204
+ # is gathered over the non-contiguous {coords βˆͺ class βˆͺ end} set, not a
205
+ # contiguous range. No reweight, no learnable pos_embed (RoPE + token-id).
206
+ - name: cocodet
207
+ id: 13
208
+ kind: codebook
209
+ start_token_key: start_of_cocodet
210
+ end_token_key: end_of_cocodet
211
+ start_token: "<|cocodet_start|>"
212
+ end_token: "<|cocodet_end|>"
213
+ represent_vae: true
214
+ conditions: [rgb] # rgb→cocodet, consistent with det/dino/clip in this config
215
+ dispersed_code_tokens: true
216
+ code_token_groups:
217
+ - token_format: "<|x1_{i:03d}|>"
218
+ start: 0
219
+ end: 999
220
+ - token_format: "<|y1_{i:03d}|>"
221
+ start: 0
222
+ end: 999
223
+ - token_format: "<|x2_{i:03d}|>"
224
+ start: 0
225
+ end: 999
226
+ - token_format: "<|y2_{i:03d}|>"
227
+ start: 0
228
+ end: 999
229
+ - token_format: "<|coco_cls_{i:02d}|>"
230
+ start: 0
231
+ end: 90
232
+ # Inference: dedicated model.generate_cocodet + inferencer.gen_cocodet
233
+ # (coords+class per box ending on cocodet_end; NOT the grounding/det decode).
234
+ inference_decode_method: cocodet
235
+ inference_max_tokens: 1000
236
+ inference_cfg_uncond: text
237
+ inference_add_instruction: false
238
+
239
+ # ── SAM segmentation / edge. Mirror seg/canny (direct-PNG image, ViT+VAE, MSE).
240
+ # Per-modality start token, shared end_of_image. Start tokens are NEW.
241
+ - name: samseg
242
+ id: 14
243
+ kind: image
244
+ start_token_key: start_of_samseg
245
+ start_token: "<|samseg_start|>"
246
+ end_token_key: end_of_image
247
+ represent_vit: true
248
+ represent_vae: true
249
+
250
+ - name: samedge
251
+ id: 15
252
+ kind: image
253
+ start_token_key: start_of_samedge
254
+ start_token: "<|samedge_start|>"
255
+ end_token_key: end_of_image
256
+ represent_vit: true
257
+ represent_vae: true
conf/modalities/instruction_hunyuan_16mod_stage2.yaml ADDED
@@ -0,0 +1,250 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ modalities:
2
+ - name: text
3
+ id: 0
4
+ kind: text
5
+ start_token_key: bos_token_id
6
+ end_token_key: eos_token_id
7
+
8
+ - name: caption
9
+ id: 1
10
+ kind: text
11
+ start_token_key: bos_token_id
12
+ end_token_key: eos_token_id
13
+ represent_vae: true
14
+ conditions: [rgb, grounding, dino, dinolocal, clip, imagebind, imagebindlocal]
15
+
16
+ - name: rgb
17
+ id: 2
18
+ kind: image
19
+ start_token_key: start_of_image
20
+ end_token_key: end_of_image
21
+ represent_vit: true
22
+ represent_vae: true
23
+ conditions: [caption, grounding, dino, dinolocal, clip, imagebind, imagebindlocal]
24
+
25
+ - name: depth
26
+ id: 3
27
+ kind: image
28
+ start_token_key: start_of_image
29
+ end_token_key: end_of_image
30
+ represent_vit: true
31
+ represent_vae: true
32
+
33
+ - name: normal
34
+ id: 4
35
+ kind: image
36
+ start_token_key: start_of_image
37
+ end_token_key: end_of_image
38
+ represent_vit: true
39
+ represent_vae: true
40
+
41
+ - name: det
42
+ id: 5
43
+ kind: codebook
44
+ start_token_key: start_of_det
45
+ end_token_key: end_of_det
46
+ represent_vae: true
47
+ # det's learnable pos_embed (size 4) + reweight are REMOVED so cocodet β€” which
48
+ # reuses det's x1/y1/x2/y2 tokens (coco_cls is cocodet's own) β€” can carry >4
49
+ # coord tokens per sample without indexing the size-4 pos_embed out of bounds.
50
+ # det is not a target here (only present for coord-token-id alignment);
51
+ # grounding reuses these tokens and is now RoPE-only for its bbox coords.
52
+ start_token: "<|det_start|>"
53
+ end_token: "<|det_end|>"
54
+ extra_tokens: ["<|box_start|>", "<|box_end|>"]
55
+ code_token_groups:
56
+ - token_format: "<|x1_{i:03d}|>"
57
+ start: 0
58
+ end: 999
59
+ - token_format: "<|y1_{i:03d}|>"
60
+ start: 0
61
+ end: 999
62
+ - token_format: "<|x2_{i:03d}|>"
63
+ start: 0
64
+ end: 999
65
+ - token_format: "<|y2_{i:03d}|>"
66
+ start: 0
67
+ end: 999
68
+ - token_format: "<|score_{i:02d}|>"
69
+ start: 0
70
+ end: 99
71
+ # Inference pipeline config
72
+ inference_decode_method: detection
73
+ inference_max_tokens: 1000
74
+ inference_cfg_uncond: text
75
+ inference_add_instruction: false # det uses start_of_det token, not text instruction
76
+
77
+ - name: seg
78
+ id: 6
79
+ kind: image
80
+ start_token_key: start_of_image
81
+ end_token_key: end_of_image
82
+ represent_vit: true
83
+ represent_vae: true
84
+
85
+ - name: canny
86
+ id: 7
87
+ kind: image
88
+ start_token_key: start_of_image
89
+ end_token_key: end_of_image
90
+ represent_vit: true
91
+ represent_vae: true
92
+
93
+ - name: dino
94
+ id: 8
95
+ kind: codebook
96
+ start_token_key: start_of_dino
97
+ end_token_key: end_of_dino
98
+ pos_embed_size: 16
99
+ apply_pos_embed_in_forward: true
100
+ represent_vae: true
101
+ start_token: "<|dino_start|>"
102
+ end_token: "<|dino_end|>"
103
+ code_vocab_size: 8192
104
+ code_token_format: "<|dino_{i:04d}|>"
105
+ external_tokenizer_kind: vqvae
106
+ external_tokenizer_repo: "EPFL-VILAB/4M_tokenizers_DINOv2-B14-global_8k_16_224"
107
+ # Inference pipeline config
108
+ inference_decode_method: dino
109
+ inference_max_tokens: 17
110
+ inference_cfg_uncond: img
111
+ inference_cfg_img_scale: 1.0
112
+
113
+ - name: dinolocal
114
+ id: 9
115
+ kind: codebook
116
+ start_token_key: start_of_dinolocal
117
+ end_token_key: end_of_dinolocal
118
+ pos_embed_size: 1024
119
+ apply_pos_embed_in_forward: true
120
+ represent_vae: true
121
+ codebook_spatial_shape: [32, 32] # 1024 tokens, 32Γ—32 grid β†’ 2D RoPE
122
+ start_token: "<|dinolocal_start|>"
123
+ end_token: "<|dinolocal_end|>"
124
+ code_vocab_size: 8192
125
+ code_token_format: "<|dinolocal_{i:04d}|>"
126
+ external_tokenizer_kind: vqvae
127
+ external_tokenizer_repo: "EPFL-VILAB/4M_tokenizers_DINOv2-B14_8k_224-448"
128
+ # Inference pipeline config
129
+ inference_decode_method: dinolocal
130
+ inference_max_tokens: 1025
131
+ inference_cfg_uncond: img
132
+ inference_cfg_img_scale: 1.0
133
+
134
+ - name: clip
135
+ id: 10
136
+ kind: codebook
137
+ start_token_key: start_of_clip
138
+ end_token_key: end_of_clip
139
+ pos_embed_size: 784
140
+ apply_pos_embed_in_forward: true
141
+ represent_vae: true
142
+ codebook_spatial_shape: [28, 28] # 784 tokens, 28Γ—28 grid β†’ 2D RoPE
143
+ start_token: "<|clip_start|>"
144
+ end_token: "<|clip_end|>"
145
+ code_vocab_size: 8192
146
+ code_token_format: "<|clip_{i:04d}|>"
147
+ external_tokenizer_kind: vqvae
148
+ external_tokenizer_repo: "EPFL-VILAB/4M_tokenizers_CLIP-B16_8k_224-448"
149
+ # Inference pipeline config
150
+ inference_decode_method: clip
151
+ inference_max_tokens: 785
152
+ inference_cfg_uncond: img
153
+ inference_cfg_img_scale: 1.0
154
+
155
+ - name: imagebind
156
+ id: 11
157
+ kind: codebook
158
+ start_token_key: start_of_imagebind
159
+ end_token_key: end_of_imagebind
160
+ pos_embed_size: 16
161
+ apply_pos_embed_in_forward: true
162
+ represent_vae: true
163
+ start_token: "<|imagebind_start|>"
164
+ end_token: "<|imagebind_end|>"
165
+ code_vocab_size: 8192
166
+ code_token_format: "<|imagebind_{i:04d}|>"
167
+ external_tokenizer_kind: vqvae
168
+ external_tokenizer_repo: "EPFL-VILAB/4M_tokenizers_ImageBind-H14-global_8k_16_224"
169
+ # Inference pipeline config
170
+ inference_decode_method: imagebind
171
+ inference_max_tokens: 17
172
+ inference_cfg_uncond: img
173
+ inference_cfg_img_scale: 1.0
174
+
175
+ - name: imagebindlocal
176
+ id: 12
177
+ kind: codebook
178
+ start_token_key: start_of_imagebindlocal
179
+ end_token_key: end_of_imagebindlocal
180
+ pos_embed_size: 1024
181
+ apply_pos_embed_in_forward: true
182
+ represent_vae: true
183
+ codebook_spatial_shape: [32, 32] # 1024 tokens, 32Γ—32 grid β†’ 2D RoPE
184
+ start_token: "<|imagebindlocal_start|>"
185
+ end_token: "<|imagebindlocal_end|>"
186
+ code_vocab_size: 8192
187
+ code_token_format: "<|imagebindlocal_{i:04d}|>"
188
+ external_tokenizer_kind: vqvae
189
+ external_tokenizer_repo: "EPFL-VILAB/4M_tokenizers_ImageBind-H14_8k_224-448"
190
+ # Inference pipeline config
191
+ inference_decode_method: imagebindlocal
192
+ inference_max_tokens: 1025
193
+ inference_cfg_uncond: img
194
+ inference_cfg_img_scale: 1.0
195
+
196
+ # ── cocodet (Pix2seq detection). Coords reuse det's x1/y1/x2/y2 tokens (dedup,
197
+ # 0 new); class = COCO-id-aligned tokens (~91 new). dispersed_code_tokens β†’ CE
198
+ # is gathered over the non-contiguous {coords βˆͺ class βˆͺ end} set, not a
199
+ # contiguous range. No reweight, no learnable pos_embed (RoPE + token-id).
200
+ - name: cocodet
201
+ id: 13
202
+ kind: codebook
203
+ start_token_key: start_of_cocodet
204
+ end_token_key: end_of_cocodet
205
+ start_token: "<|cocodet_start|>"
206
+ end_token: "<|cocodet_end|>"
207
+ represent_vae: true
208
+ dispersed_code_tokens: true
209
+ code_token_groups:
210
+ - token_format: "<|x1_{i:03d}|>"
211
+ start: 0
212
+ end: 999
213
+ - token_format: "<|y1_{i:03d}|>"
214
+ start: 0
215
+ end: 999
216
+ - token_format: "<|x2_{i:03d}|>"
217
+ start: 0
218
+ end: 999
219
+ - token_format: "<|y2_{i:03d}|>"
220
+ start: 0
221
+ end: 999
222
+ - token_format: "<|coco_cls_{i:02d}|>"
223
+ start: 0
224
+ end: 90
225
+ # Inference: dedicated model.generate_cocodet + inferencer.gen_cocodet
226
+ # (coords+class per box ending on cocodet_end; NOT the grounding/det decode).
227
+ inference_decode_method: cocodet
228
+ inference_max_tokens: 1000
229
+ inference_cfg_uncond: text
230
+ inference_add_instruction: false
231
+
232
+ # ── SAM segmentation / edge. Mirror seg/canny (direct-PNG image, ViT+VAE, MSE).
233
+ # Per-modality start token, shared end_of_image. Start tokens are NEW.
234
+ - name: samseg
235
+ id: 14
236
+ kind: image
237
+ start_token_key: start_of_samseg
238
+ start_token: "<|samseg_start|>"
239
+ end_token_key: end_of_image
240
+ represent_vit: true
241
+ represent_vae: true
242
+
243
+ - name: samedge
244
+ id: 15
245
+ kind: image
246
+ start_token_key: start_of_samedge
247
+ start_token: "<|samedge_start|>"
248
+ end_token_key: end_of_image
249
+ represent_vit: true
250
+ represent_vae: true
conf/modalities/instruction_stage2.yaml ADDED
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Stage 2 modality config β€” unconstrained conditions.
2
+ #
3
+ # Same modality definitions as instruction.yaml, but with ALL condition
4
+ # restrictions removed so every modality can be conditioned on every other.
5
+
6
+ modalities:
7
+ - name: text
8
+ id: 0
9
+ kind: text
10
+ start_token_key: bos_token_id
11
+ end_token_key: eos_token_id
12
+
13
+ - name: caption
14
+ id: 1
15
+ kind: text
16
+ start_token_key: bos_token_id
17
+ end_token_key: eos_token_id
18
+ represent_vae: true
19
+ # conditions: unconstrained (any modality can condition caption)
20
+
21
+ - name: rgb
22
+ id: 2
23
+ kind: image
24
+ start_token_key: start_of_image
25
+ end_token_key: end_of_image
26
+ represent_vit: true
27
+ represent_vae: true
28
+ # conditions: unconstrained
29
+
30
+ - name: depth
31
+ id: 3
32
+ kind: image
33
+ start_token_key: start_of_image
34
+ end_token_key: end_of_image
35
+ represent_vit: true
36
+ represent_vae: true
37
+
38
+ - name: normal
39
+ id: 4
40
+ kind: image
41
+ start_token_key: start_of_image
42
+ end_token_key: end_of_image
43
+ represent_vit: true
44
+ represent_vae: true
45
+
46
+ - name: det
47
+ id: 5
48
+ kind: codebook
49
+ start_token_key: start_of_det
50
+ end_token_key: end_of_det
51
+ represent_vae: true
52
+ pos_embed_size: 4
53
+ apply_pos_embed_in_forward: true
54
+ pos_embed_name: grounding # checkpoint compat: attr is "grounding_pos_embed"
55
+ # conditions: unconstrained
56
+ loss:
57
+ reweight: true
58
+ reweight_min_w: 0.005
59
+ start_token: "<|det_start|>"
60
+ end_token: "<|det_end|>"
61
+ extra_tokens: ["<|box_start|>", "<|box_end|>"]
62
+ code_token_groups:
63
+ - token_format: "<|x1_{i:03d}|>"
64
+ start: 0
65
+ end: 999
66
+ - token_format: "<|y1_{i:03d}|>"
67
+ start: 0
68
+ end: 999
69
+ - token_format: "<|x2_{i:03d}|>"
70
+ start: 0
71
+ end: 999
72
+ - token_format: "<|y2_{i:03d}|>"
73
+ start: 0
74
+ end: 999
75
+ - token_format: "<|score_{i:02d}|>"
76
+ start: 0
77
+ end: 99
78
+ # Inference pipeline config
79
+ inference_decode_method: detection
80
+ inference_max_tokens: 1000
81
+ inference_cfg_uncond: text
82
+ inference_add_instruction: false # det uses start_of_det token, not text instruction
83
+
84
+ - name: seg
85
+ id: 6
86
+ kind: image
87
+ start_token_key: start_of_image
88
+ end_token_key: end_of_image
89
+ represent_vit: true
90
+ represent_vae: true
91
+
92
+ - name: canny
93
+ id: 7
94
+ kind: image
95
+ start_token_key: start_of_image
96
+ end_token_key: end_of_image
97
+ represent_vit: true
98
+ represent_vae: true
99
+
100
+ - name: dino
101
+ id: 8
102
+ kind: codebook
103
+ start_token_key: start_of_dino
104
+ end_token_key: end_of_dino
105
+ pos_embed_size: 16
106
+ apply_pos_embed_in_forward: true
107
+ represent_vae: true
108
+ # conditions: unconstrained
109
+ start_token: "<|dino_start|>"
110
+ end_token: "<|dino_end|>"
111
+ code_vocab_size: 8192
112
+ code_token_format: "<|dino_{i:04d}|>"
113
+ external_tokenizer_kind: vqvae
114
+ external_tokenizer_repo: "EPFL-VILAB/4M_tokenizers_DINOv2-B14-global_8k_16_224"
115
+ # Inference pipeline config
116
+ inference_decode_method: dino
117
+ inference_max_tokens: 17
118
+ inference_cfg_uncond: img
119
+ inference_cfg_img_scale: 1.0
120
+
121
+ - name: dinolocal
122
+ id: 9
123
+ kind: codebook
124
+ start_token_key: start_of_dinolocal
125
+ end_token_key: end_of_dinolocal
126
+ pos_embed_size: 1024
127
+ apply_pos_embed_in_forward: true
128
+ represent_vae: true
129
+ # conditions: unconstrained
130
+ start_token: "<|dinolocal_start|>"
131
+ end_token: "<|dinolocal_end|>"
132
+ code_vocab_size: 8192
133
+ code_token_format: "<|dinolocal_{i:04d}|>"
134
+ external_tokenizer_kind: vqvae
135
+ external_tokenizer_repo: "EPFL-VILAB/4M_tokenizers_DINOv2-B14_8k_224-448"
136
+ # Inference pipeline config
137
+ inference_decode_method: dinolocal
138
+ inference_max_tokens: 1025
139
+ inference_cfg_uncond: img
140
+ inference_cfg_img_scale: 1.0
141
+
142
+ - name: clip
143
+ id: 10
144
+ kind: codebook
145
+ start_token_key: start_of_clip
146
+ end_token_key: end_of_clip
147
+ pos_embed_size: 784
148
+ apply_pos_embed_in_forward: true
149
+ represent_vae: true
150
+ # conditions: unconstrained
151
+ start_token: "<|clip_start|>"
152
+ end_token: "<|clip_end|>"
153
+ code_vocab_size: 8192
154
+ code_token_format: "<|clip_{i:04d}|>"
155
+ external_tokenizer_kind: vqvae
156
+ external_tokenizer_repo: "EPFL-VILAB/4M_tokenizers_CLIP-B16_8k_224-448"
157
+ # Inference pipeline config
158
+ inference_decode_method: clip
159
+ inference_max_tokens: 785
160
+ inference_cfg_uncond: img
161
+ inference_cfg_img_scale: 1.0
162
+
163
+ - name: imagebind
164
+ id: 11
165
+ kind: codebook
166
+ start_token_key: start_of_imagebind
167
+ end_token_key: end_of_imagebind
168
+ pos_embed_size: 16
169
+ apply_pos_embed_in_forward: true
170
+ represent_vae: true
171
+ # conditions: unconstrained
172
+ start_token: "<|imagebind_start|>"
173
+ end_token: "<|imagebind_end|>"
174
+ code_vocab_size: 8192
175
+ code_token_format: "<|imagebind_{i:04d}|>"
176
+ external_tokenizer_kind: vqvae
177
+ external_tokenizer_repo: "EPFL-VILAB/4M_tokenizers_ImageBind-H14-global_8k_16_224"
178
+ # Inference pipeline config
179
+ inference_decode_method: imagebind
180
+ inference_max_tokens: 17
181
+ inference_cfg_uncond: img
182
+ inference_cfg_img_scale: 1.0
183
+
184
+ - name: imagebindlocal
185
+ id: 12
186
+ kind: codebook
187
+ start_token_key: start_of_imagebindlocal
188
+ end_token_key: end_of_imagebindlocal
189
+ pos_embed_size: 1024
190
+ apply_pos_embed_in_forward: true
191
+ represent_vae: true
192
+ # conditions: unconstrained
193
+ start_token: "<|imagebindlocal_start|>"
194
+ end_token: "<|imagebindlocal_end|>"
195
+ code_vocab_size: 8192
196
+ code_token_format: "<|imagebindlocal_{i:04d}|>"
197
+ external_tokenizer_kind: vqvae
198
+ external_tokenizer_repo: "EPFL-VILAB/4M_tokenizers_ImageBind-H14_8k_224-448"
199
+ # Inference pipeline config
200
+ inference_decode_method: imagebindlocal
201
+ inference_max_tokens: 1025
202
+ inference_cfg_uncond: img
203
+ inference_cfg_img_scale: 1.0
204
+
conf/modalities/instruction_t2i_only_stage1.yaml ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # v65 = T2I-only stage1 modality registry.
2
+ # Keeps text + caption + rgb only. Drops every other modality.
3
+ # Pair with `hunyuan_image_3_v65_t2i_only.yaml`.
4
+
5
+ modalities:
6
+ - name: text
7
+ id: 0
8
+ kind: text
9
+ start_token_key: bos_token_id
10
+ end_token_key: eos_token_id
11
+
12
+ - name: caption
13
+ id: 1
14
+ kind: text
15
+ start_token_key: bos_token_id
16
+ end_token_key: eos_token_id
17
+ represent_vae: true
18
+ conditions: [rgb]
19
+
20
+ - name: rgb
21
+ id: 2
22
+ kind: image
23
+ start_token_key: start_of_image
24
+ end_token_key: end_of_image
25
+ represent_vit: true
26
+ represent_vae: true
27
+ conditions: [caption]
conf/modalities/legacy.yaml ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ modalities:
2
+ - name: text
3
+ id: 0
4
+ kind: text
5
+ start_token_key: bos_token_id
6
+ end_token_key: eos_token_id
7
+
8
+ - name: caption
9
+ id: 1
10
+ kind: text
11
+ start_token_key: start_of_caption
12
+ end_token_key: end_of_caption
13
+
14
+ - name: rgb
15
+ id: 2
16
+ kind: image
17
+ start_token_key: start_of_image
18
+ end_token_key: end_of_image
19
+ represent_vit: true
20
+ represent_vae: true
21
+
22
+ - name: depth
23
+ id: 3
24
+ kind: image
25
+ start_token_key: start_of_depth
26
+ end_token_key: end_of_depth
27
+ represent_vit: true
28
+ represent_vae: true
29
+
30
+ - name: normal
31
+ id: 4
32
+ kind: image
33
+ start_token_key: start_of_normal
34
+ end_token_key: end_of_normal
35
+ represent_vit: true
36
+ represent_vae: true
37
+
38
+
conf/modalities/rebuttal_rgb2target.yaml ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ modalities:
2
+ - name: text
3
+ id: 0
4
+ kind: text
5
+ start_token_key: bos_token_id
6
+ end_token_key: eos_token_id
7
+
8
+ - name: caption
9
+ id: 1
10
+ kind: text
11
+ start_token_key: bos_token_id
12
+ end_token_key: eos_token_id
13
+ represent_vae: true
14
+ conditions: [rgb, grounding, dino]
15
+
16
+ - name: rgb
17
+ id: 2
18
+ kind: image
19
+ start_token_key: start_of_image
20
+ end_token_key: end_of_image
21
+ represent_vit: true
22
+ represent_vae: true
23
+ conditions: [caption, grounding, dino]
24
+
25
+ - name: depth
26
+ id: 3
27
+ kind: image
28
+ start_token_key: start_of_image
29
+ end_token_key: end_of_image
30
+ represent_vit: true
31
+ represent_vae: true
32
+ conditions: [rgb]
33
+
34
+ - name: normal
35
+ id: 4
36
+ kind: image
37
+ start_token_key: start_of_image
38
+ end_token_key: end_of_image
39
+ represent_vit: true
40
+ represent_vae: true
41
+ conditions: [rgb]
42
+
43
+ - name: det
44
+ id: 5
45
+ kind: codebook
46
+ start_token_key: start_of_det
47
+ end_token_key: end_of_det
48
+ represent_vae: true
49
+ pos_embed_size: 4
50
+ apply_pos_embed_in_forward: true
51
+ pos_embed_name: grounding # checkpoint compat: attr is "grounding_pos_embed"
52
+ conditions: [rgb]
53
+ loss:
54
+ reweight: true
55
+ reweight_min_w: 0.005
56
+ start_token: "<|det_start|>"
57
+ end_token: "<|det_end|>"
58
+ extra_tokens: ["<|box_start|>", "<|box_end|>"]
59
+ code_token_groups:
60
+ - token_format: "<|x1_{i:03d}|>"
61
+ start: 0
62
+ end: 999
63
+ - token_format: "<|y1_{i:03d}|>"
64
+ start: 0
65
+ end: 999
66
+ - token_format: "<|x2_{i:03d}|>"
67
+ start: 0
68
+ end: 999
69
+ - token_format: "<|y2_{i:03d}|>"
70
+ start: 0
71
+ end: 999
72
+ - token_format: "<|score_{i:02d}|>"
73
+ start: 0
74
+ end: 99
75
+ # Inference pipeline config
76
+ inference_decode_method: detection
77
+ inference_max_tokens: 1000
78
+ inference_cfg_uncond: text
79
+ inference_add_instruction: false # det uses start_of_det token, not text instruction
80
+
81
+ - name: seg
82
+ id: 6
83
+ kind: image
84
+ start_token_key: start_of_image
85
+ end_token_key: end_of_image
86
+ represent_vit: true
87
+ represent_vae: true
88
+
89
+ - name: canny
90
+ id: 7
91
+ kind: image
92
+ start_token_key: start_of_image
93
+ end_token_key: end_of_image
94
+ represent_vit: true
95
+ represent_vae: true
96
+
97
+ - name: dino
98
+ id: 8
99
+ kind: codebook
100
+ start_token_key: start_of_dino
101
+ end_token_key: end_of_dino
102
+ pos_embed_size: 16
103
+ apply_pos_embed_in_forward: true
104
+ represent_vae: true
105
+ conditions: [rgb]
106
+ start_token: "<|dino_start|>"
107
+ end_token: "<|dino_end|>"
108
+ code_vocab_size: 8192
109
+ code_token_format: "<|dino_{i:04d}|>"
110
+ external_tokenizer_kind: vqvae
111
+ external_tokenizer_repo: "EPFL-VILAB/4M_tokenizers_DINOv2-B14-global_8k_16_224"
112
+ # Inference pipeline config
113
+ inference_decode_method: dino
114
+ inference_max_tokens: 17
115
+ inference_cfg_uncond: img
116
+ inference_cfg_img_scale: 1.0
conf/modalities/stage1_oversample_bbox2rgb_dinolocal2rgb.yaml ADDED
@@ -0,0 +1,203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ modalities:
2
+ - name: text
3
+ id: 0
4
+ kind: text
5
+ start_token_key: bos_token_id
6
+ end_token_key: eos_token_id
7
+
8
+ - name: caption
9
+ id: 1
10
+ kind: text
11
+ start_token_key: bos_token_id
12
+ end_token_key: eos_token_id
13
+ represent_vae: true
14
+ conditions: [rgb, grounding, dinolocal]
15
+ condition_probs: [0.1, 0.45, 0.45]
16
+
17
+ - name: rgb
18
+ id: 2
19
+ kind: image
20
+ start_token_key: start_of_image
21
+ end_token_key: end_of_image
22
+ represent_vit: true
23
+ represent_vae: true
24
+ conditions: [caption, grounding, dinolocal]
25
+ condition_probs: [0.1, 0.45, 0.45]
26
+
27
+ - name: depth
28
+ id: 3
29
+ kind: image
30
+ start_token_key: start_of_image
31
+ end_token_key: end_of_image
32
+ represent_vit: true
33
+ represent_vae: true
34
+
35
+ - name: normal
36
+ id: 4
37
+ kind: image
38
+ start_token_key: start_of_image
39
+ end_token_key: end_of_image
40
+ represent_vit: true
41
+ represent_vae: true
42
+
43
+ - name: det
44
+ id: 5
45
+ kind: codebook
46
+ start_token_key: start_of_det
47
+ end_token_key: end_of_det
48
+ represent_vae: true
49
+ pos_embed_size: 4
50
+ apply_pos_embed_in_forward: true
51
+ pos_embed_name: grounding # checkpoint compat: attr is "grounding_pos_embed"
52
+ conditions: [rgb]
53
+ loss:
54
+ reweight: true
55
+ reweight_min_w: 0.005
56
+ start_token: "<|det_start|>"
57
+ end_token: "<|det_end|>"
58
+ extra_tokens: ["<|box_start|>", "<|box_end|>"]
59
+ code_token_groups:
60
+ - token_format: "<|x1_{i:03d}|>"
61
+ start: 0
62
+ end: 999
63
+ - token_format: "<|y1_{i:03d}|>"
64
+ start: 0
65
+ end: 999
66
+ - token_format: "<|x2_{i:03d}|>"
67
+ start: 0
68
+ end: 999
69
+ - token_format: "<|y2_{i:03d}|>"
70
+ start: 0
71
+ end: 999
72
+ - token_format: "<|score_{i:02d}|>"
73
+ start: 0
74
+ end: 99
75
+ # Inference pipeline config
76
+ inference_decode_method: detection
77
+ inference_max_tokens: 1000
78
+ inference_cfg_uncond: text
79
+ inference_add_instruction: false # det uses start_of_det token, not text instruction
80
+
81
+ - name: seg
82
+ id: 6
83
+ kind: image
84
+ start_token_key: start_of_image
85
+ end_token_key: end_of_image
86
+ represent_vit: true
87
+ represent_vae: true
88
+
89
+ - name: canny
90
+ id: 7
91
+ kind: image
92
+ start_token_key: start_of_image
93
+ end_token_key: end_of_image
94
+ represent_vit: true
95
+ represent_vae: true
96
+
97
+ - name: dino
98
+ id: 8
99
+ kind: codebook
100
+ start_token_key: start_of_dino
101
+ end_token_key: end_of_dino
102
+ pos_embed_size: 16
103
+ apply_pos_embed_in_forward: true
104
+ represent_vae: true
105
+ conditions: [rgb]
106
+ start_token: "<|dino_start|>"
107
+ end_token: "<|dino_end|>"
108
+ code_vocab_size: 8192
109
+ code_token_format: "<|dino_{i:04d}|>"
110
+ external_tokenizer_kind: vqvae
111
+ external_tokenizer_repo: "EPFL-VILAB/4M_tokenizers_DINOv2-B14-global_8k_16_224"
112
+ # Inference pipeline config
113
+ inference_decode_method: dino
114
+ inference_max_tokens: 17
115
+ inference_cfg_uncond: img
116
+ inference_cfg_img_scale: 1.0
117
+
118
+ - name: dinolocal
119
+ id: 9
120
+ kind: codebook
121
+ start_token_key: start_of_dinolocal
122
+ end_token_key: end_of_dinolocal
123
+ pos_embed_size: 1024
124
+ apply_pos_embed_in_forward: true
125
+ represent_vae: true
126
+ conditions: [rgb]
127
+ codebook_spatial_shape: [32, 32] # 1024 tokens, 32Γ—32 grid β†’ 2D RoPE
128
+ start_token: "<|dinolocal_start|>"
129
+ end_token: "<|dinolocal_end|>"
130
+ code_vocab_size: 8192
131
+ code_token_format: "<|dinolocal_{i:04d}|>"
132
+ external_tokenizer_kind: vqvae
133
+ external_tokenizer_repo: "EPFL-VILAB/4M_tokenizers_DINOv2-B14_8k_224-448"
134
+ # Inference pipeline config
135
+ inference_decode_method: dinolocal
136
+ inference_max_tokens: 1025
137
+ inference_cfg_uncond: img
138
+ inference_cfg_img_scale: 1.0
139
+
140
+ - name: clip
141
+ id: 10
142
+ kind: codebook
143
+ start_token_key: start_of_clip
144
+ end_token_key: end_of_clip
145
+ pos_embed_size: 784
146
+ apply_pos_embed_in_forward: true
147
+ represent_vae: true
148
+ conditions: [rgb]
149
+ codebook_spatial_shape: [28, 28] # 784 tokens, 28Γ—28 grid β†’ 2D RoPE
150
+ start_token: "<|clip_start|>"
151
+ end_token: "<|clip_end|>"
152
+ code_vocab_size: 8192
153
+ code_token_format: "<|clip_{i:04d}|>"
154
+ external_tokenizer_kind: vqvae
155
+ external_tokenizer_repo: "EPFL-VILAB/4M_tokenizers_CLIP-B16_8k_224-448"
156
+ # Inference pipeline config
157
+ inference_decode_method: clip
158
+ inference_max_tokens: 785
159
+ inference_cfg_uncond: img
160
+ inference_cfg_img_scale: 1.0
161
+
162
+ - name: imagebind
163
+ id: 11
164
+ kind: codebook
165
+ start_token_key: start_of_imagebind
166
+ end_token_key: end_of_imagebind
167
+ pos_embed_size: 16
168
+ apply_pos_embed_in_forward: true
169
+ represent_vae: true
170
+ conditions: [rgb]
171
+ start_token: "<|imagebind_start|>"
172
+ end_token: "<|imagebind_end|>"
173
+ code_vocab_size: 8192
174
+ code_token_format: "<|imagebind_{i:04d}|>"
175
+ external_tokenizer_kind: vqvae
176
+ external_tokenizer_repo: "EPFL-VILAB/4M_tokenizers_ImageBind-H14-global_8k_16_224"
177
+ # Inference pipeline config
178
+ inference_decode_method: imagebind
179
+ inference_max_tokens: 17
180
+ inference_cfg_uncond: img
181
+ inference_cfg_img_scale: 1.0
182
+
183
+ - name: imagebindlocal
184
+ id: 12
185
+ kind: codebook
186
+ start_token_key: start_of_imagebindlocal
187
+ end_token_key: end_of_imagebindlocal
188
+ pos_embed_size: 1024
189
+ apply_pos_embed_in_forward: true
190
+ represent_vae: true
191
+ conditions: [rgb]
192
+ codebook_spatial_shape: [32, 32] # 1024 tokens, 32Γ—32 grid β†’ 2D RoPE
193
+ start_token: "<|imagebindlocal_start|>"
194
+ end_token: "<|imagebindlocal_end|>"
195
+ code_vocab_size: 8192
196
+ code_token_format: "<|imagebindlocal_{i:04d}|>"
197
+ external_tokenizer_kind: vqvae
198
+ external_tokenizer_repo: "EPFL-VILAB/4M_tokenizers_ImageBind-H14_8k_224-448"
199
+ # Inference pipeline config
200
+ inference_decode_method: imagebindlocal
201
+ inference_max_tokens: 1025
202
+ inference_cfg_uncond: img
203
+ inference_cfg_img_scale: 1.0
core/__init__.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Core utilities for MODUS.
3
+
4
+ This package is intentionally small and dependency-light so it can be used by:
5
+ - training scripts
6
+ - inference/demo scripts
7
+ - dataset packing
8
+ - model code
9
+ """
10
+
11
+
core/modality.py ADDED
@@ -0,0 +1,394 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field as dc_field, replace as dc_replace
4
+ from typing import Any, Dict, Iterable, List, Mapping, Optional, Tuple
5
+
6
+
7
+ TokenRange = Tuple[int, int] # (base_id, length)
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class LossConfig:
12
+ """Per-modality loss configuration (lives in the modality YAML)."""
13
+ reweight: bool = False
14
+ reweight_min_w: float = 0.005
15
+ reweight_det_vocab_dim: int = 4000 # only used when reweight=True for coordinate codebooks
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class ModalitySpec:
20
+ """
21
+ Config-driven definition of a single modality.
22
+
23
+ Every tunable per-modality knob lives here so that the training script,
24
+ model forward, and dataset packing can all be driven from one YAML.
25
+ """
26
+
27
+ name: str
28
+ id: int
29
+ start_token_key: str
30
+ end_token_key: str
31
+ kind: str = "text" # text | image | codebook
32
+
33
+ # Token range ``(base, length)`` for CE-loss vocab slicing.
34
+ # Codebook modalities get their own range from the tokenizer; text
35
+ # modalities are auto-assigned ``(0, text_vocab_end)`` in
36
+ # ``ModalityRegistry.from_config``. Image modalities (ViT/VAE)
37
+ # never produce CE tokens, so they keep ``None``.
38
+ code_token_range: Optional[TokenRange] = None
39
+
40
+ # Explicit, possibly **non-contiguous** set of CE-loss token ids. Used for
41
+ # modalities whose alphabet is dispersed across the vocab and therefore
42
+ # cannot be expressed as a single ``(base, length)`` range (e.g. cocodet,
43
+ # which reuses det's low-vocab coordinate tokens + new high-vocab class/end
44
+ # tokens). When set, the model does gather-CE over exactly these ids instead
45
+ # of the contiguous-slice path. ``None`` for every existing modality.
46
+ code_token_ids: Optional[Tuple[int, ...]] = None
47
+
48
+ # Learnable positional embedding for code tokens.
49
+ pos_embed_size: Optional[int] = None
50
+ apply_pos_embed_in_forward: bool = False
51
+
52
+ # Image representation flags (used by dataset packing and inferencer).
53
+ represent_vit: bool = True
54
+ represent_vae: bool = False
55
+
56
+ # Which other modalities may condition this one during training.
57
+ # ``None`` means "use all available"; an explicit list restricts.
58
+ conditions: Optional[Tuple[str, ...]] = None
59
+ # Optional sampling probabilities aligned with ``conditions`` order.
60
+ # If omitted, the dataset uses a uniform distribution.
61
+ condition_probs: Optional[Tuple[float, ...]] = None
62
+
63
+ # Per-modality loss parameters (CE smoothing / reweighting).
64
+ loss: LossConfig = LossConfig()
65
+
66
+ # Override the model attribute name for the pos-embed (for checkpoint compat).
67
+ # Defaults to ``name`` (e.g. modality "dino" β†’ attr ``dino_pos_embed``).
68
+ # Set to e.g. "grounding" for det β†’ ``grounding_pos_embed``.
69
+ pos_embed_name: Optional[str] = None
70
+
71
+ # 2D spatial shape for codebook modalities (h, w).
72
+ # None β†’ 1D sequential (uses text region of build_2d_rope, gets sequential positions).
73
+ # Set e.g. (32, 32) for dinolocal, (28, 28) for clip β†’ gets 2D RoPE same as VAE latents.
74
+ codebook_spatial_shape: Optional[Tuple[int, int]] = None
75
+
76
+ # External tokenizer for inference decoding/encoding (e.g. DINO VQVAE).
77
+ external_tokenizer_repo: Optional[str] = None
78
+ external_tokenizer_kind: str = "vqvae"
79
+
80
+ # ── Inference-time configuration ─────────────────────────────────────
81
+ # These fields drive the unified inference pipeline so that the
82
+ # inferencer does not need per-modality if/elif chains.
83
+
84
+ # Decode method: "auto" resolves from ``kind`` (image→image, text→text).
85
+ # Explicit values: "image", "text", "detection", "dino".
86
+ inference_decode_method: str = "auto"
87
+
88
+ # Maximum AR tokens for text / codebook decoding (None β†’ inferencer default).
89
+ inference_max_tokens: Optional[int] = None
90
+
91
+ # Which CFG unconditional context to use when this modality is a *target*:
92
+ # "text" β†’ cfg_text_context (drop text, keep image) e.g. detection
93
+ # "img" β†’ cfg_img_context (drop image, keep text) e.g. dino
94
+ # "both" β†’ dual CFG with both contexts e.g. image generation
95
+ # "none" β†’ no CFG e.g. plain text
96
+ inference_cfg_uncond: str = "auto"
97
+
98
+ # Whether to prepend "[start {name} x10]" instruction before generation.
99
+ inference_add_instruction: bool = True
100
+
101
+ # Default CFG image scale for this modality when it is the *target*.
102
+ # ``None`` β†’ use the global default from the base config.
103
+ inference_cfg_img_scale: Optional[float] = None
104
+
105
+
106
+ class ModalityRegistry:
107
+ """Runtime registry consumed by dataset packing, model forward, and inferencer."""
108
+
109
+ def __init__(self, specs: Iterable[ModalitySpec]):
110
+ specs = list(specs)
111
+ if len(specs) == 0:
112
+ raise ValueError("ModalityRegistry requires at least one ModalitySpec.")
113
+
114
+ self._by_name: Dict[str, ModalitySpec] = {}
115
+ self._by_id: Dict[int, ModalitySpec] = {}
116
+ for s in specs:
117
+ if s.name in self._by_name:
118
+ raise ValueError(f"Duplicate modality name: {s.name}")
119
+ if s.id in self._by_id:
120
+ raise ValueError(f"Duplicate modality id: {s.id}")
121
+ self._by_name[s.name] = s
122
+ self._by_id[s.id] = s
123
+
124
+ # ── lookups ──────────────────────────────────────────────────────────
125
+
126
+ @property
127
+ def specs(self) -> List[ModalitySpec]:
128
+ return list(self._by_name.values())
129
+
130
+ def get(self, name: str) -> ModalitySpec:
131
+ return self._by_name[name]
132
+
133
+ def get_by_id(self, modality_id: int) -> ModalitySpec:
134
+ return self._by_id[modality_id]
135
+
136
+ def name_to_id(self) -> Dict[str, int]:
137
+ return {k: v.id for k, v in self._by_name.items()}
138
+
139
+ def id_to_name(self) -> Dict[int, str]:
140
+ return {k: v.name for k, v in self._by_id.items()}
141
+
142
+ def modality_name(self, modality_id: int) -> str:
143
+ spec = self._by_id.get(modality_id)
144
+ return spec.name if spec is not None else f"unknown_{modality_id}"
145
+
146
+ def start_token_key(self, name: str) -> str:
147
+ return self._by_name[name].start_token_key
148
+
149
+ def end_token_key(self, name: str) -> str:
150
+ return self._by_name[name].end_token_key
151
+
152
+ def start_token_id(self, new_token_ids: Mapping[str, Any], name: str) -> int:
153
+ return int(new_token_ids[self.start_token_key(name)])
154
+
155
+ def end_token_id(self, new_token_ids: Mapping[str, Any], name: str) -> int:
156
+ return int(new_token_ids[self.end_token_key(name)])
157
+
158
+ def code_token_range(self, name: str) -> Optional[TokenRange]:
159
+ return self._by_name[name].code_token_range
160
+
161
+ def has_codebook_modalities(self) -> bool:
162
+ """True if any registered modality is ``kind == 'codebook'``."""
163
+ return any(s.kind == "codebook" for s in self._by_name.values())
164
+
165
+ def conditions_for(self, target_modality: str) -> Optional[List[str]]:
166
+ """
167
+ Return the allowed conditioning modalities for *target_modality*.
168
+
169
+ ``None`` means "no restriction β€” use all available".
170
+ """
171
+ spec = self._by_name.get(target_modality)
172
+ if spec is None or spec.conditions is None:
173
+ return None
174
+ return list(spec.conditions)
175
+
176
+ def condition_probs_for(self, target_modality: str) -> Optional[List[float]]:
177
+ """
178
+ Return optional condition sampling probabilities for *target_modality*.
179
+
180
+ The returned probabilities align with ``conditions_for(target_modality)``.
181
+ ``None`` means "use uniform condition sampling".
182
+ """
183
+ spec = self._by_name.get(target_modality)
184
+ if spec is None or spec.condition_probs is None:
185
+ return None
186
+ return list(spec.condition_probs)
187
+
188
+ def resolve_decode_method(self, name: str) -> str:
189
+ """Return the concrete decode method for *name* (resolves ``"auto"``)."""
190
+ spec = self._by_name[name]
191
+ if spec.inference_decode_method != "auto":
192
+ return spec.inference_decode_method
193
+ if spec.kind == "image":
194
+ return "image"
195
+ if spec.kind == "text":
196
+ return "text"
197
+ # codebook β€” guess from name
198
+ if "det" in spec.name:
199
+ return "detection"
200
+ if spec.name == "dinolocal":
201
+ return "dinolocal"
202
+ if "dino" in spec.name:
203
+ return "dino"
204
+ return "text"
205
+
206
+ def resolve_cfg_uncond(self, name: str) -> str:
207
+ """Return the concrete CFG-uncond context type (resolves ``"auto"``)."""
208
+ spec = self._by_name[name]
209
+ if spec.inference_cfg_uncond != "auto":
210
+ return spec.inference_cfg_uncond
211
+ dm = self.resolve_decode_method(name)
212
+ if dm == "image":
213
+ return "both"
214
+ if dm == "detection":
215
+ return "text"
216
+ if dm in ("dino", "dinolocal"):
217
+ return "img"
218
+ return "none"
219
+
220
+ def resolve_cfg_img_scale(self, name: str, default: float = 2.0) -> float:
221
+ """Return the per-modality CFG image scale, or *default* if not specified."""
222
+ spec = self._by_name[name]
223
+ if spec.inference_cfg_img_scale is not None:
224
+ return spec.inference_cfg_img_scale
225
+ return default
226
+
227
+ def needs_external_tokenizer(self, name: str) -> bool:
228
+ """Return True if the modality has an external tokenizer (e.g. DINO VQVAE)."""
229
+ spec = self._by_name.get(name)
230
+ return spec is not None and spec.external_tokenizer_repo is not None
231
+
232
+ def modalities_with_forward_pos_embed(self) -> List[ModalitySpec]:
233
+ return [
234
+ s
235
+ for s in self._by_name.values()
236
+ if s.pos_embed_size is not None and s.apply_pos_embed_in_forward
237
+ ]
238
+
239
+ # ── construction from YAML dict ─────────────────��───────────────────
240
+
241
+ @staticmethod
242
+ def from_config(
243
+ cfg: Any,
244
+ *,
245
+ token_ranges: Optional[Mapping[str, TokenRange]] = None,
246
+ code_token_ids: Optional[Mapping[str, List[int]]] = None,
247
+ ) -> "ModalityRegistry":
248
+ """
249
+ Build a registry from the parsed YAML config dict.
250
+
251
+ Expected structure::
252
+
253
+ cfg["modalities"]: list[{name, id, start_token_key, end_token_key, kind, ...}]
254
+
255
+ ``token_ranges`` maps modality name β†’ ``(base, length)`` and is typically
256
+ computed after tokenizer token additions.
257
+ """
258
+ if cfg is None:
259
+ raise ValueError("cfg is required")
260
+
261
+ # Accept either ``{"modalities": [...]}`` or a bare list.
262
+ modalities = cfg.get("modalities") if isinstance(cfg, dict) else cfg
263
+ if modalities is None:
264
+ raise ValueError("No 'modalities' key found in cfg")
265
+
266
+ specs: List[ModalitySpec] = []
267
+ token_ranges = dict(token_ranges or {})
268
+ code_token_ids = dict(code_token_ids or {})
269
+
270
+ for m in modalities:
271
+ name = str(m["name"])
272
+
273
+ # Resolve code_token_range: prefer runtime-computed, fall back to cfg.
274
+ _range = token_ranges.get(name)
275
+ if _range is None and m.get("code_token_range") is not None:
276
+ _range = tuple(m["code_token_range"])
277
+
278
+ # Resolve explicit (possibly dispersed) code_token_ids, if any.
279
+ _ids = code_token_ids.get(name)
280
+ if _ids is None and m.get("code_token_ids") is not None:
281
+ _ids = m["code_token_ids"]
282
+ _ids = tuple(int(i) for i in _ids) if _ids is not None else None
283
+
284
+ # pos_embed_size may be None.
285
+ _pos = m.get("pos_embed_size")
286
+ pos_embed_size = int(_pos) if _pos is not None else None
287
+
288
+ # Parse conditions (list of strings or None).
289
+ _conds = m.get("conditions")
290
+ conditions = tuple(_conds) if _conds is not None else None
291
+ _cond_probs = m.get("condition_probs")
292
+ condition_probs = tuple(float(x) for x in _cond_probs) if _cond_probs is not None else None
293
+ if condition_probs is not None:
294
+ if conditions is None:
295
+ raise ValueError(
296
+ f"Modality '{name}' sets 'condition_probs' without 'conditions'."
297
+ )
298
+ if len(condition_probs) != len(conditions):
299
+ raise ValueError(
300
+ f"Modality '{name}' has {len(conditions)} conditions but "
301
+ f"{len(condition_probs)} condition_probs."
302
+ )
303
+ if any(p < 0.0 for p in condition_probs):
304
+ raise ValueError(f"Modality '{name}' has negative values in condition_probs.")
305
+ probs_sum = float(sum(condition_probs))
306
+ if abs(probs_sum - 1.0) > 1e-6:
307
+ raise ValueError(
308
+ f"Modality '{name}' condition_probs must sum to 1.0, got {probs_sum}."
309
+ )
310
+
311
+ # Parse per-modality loss config.
312
+ _loss_dict = m.get("loss") or {}
313
+ loss_cfg = LossConfig(
314
+ reweight=bool(_loss_dict.get("reweight", False)),
315
+ reweight_min_w=float(_loss_dict.get("reweight_min_w", 0.005)),
316
+ reweight_det_vocab_dim=int(_loss_dict.get("reweight_det_vocab_dim", 4000)),
317
+ )
318
+
319
+ _pe_name = m.get("pos_embed_name")
320
+ pe_name = str(_pe_name) if _pe_name is not None else None
321
+
322
+ # 2D spatial shape for codebook modalities.
323
+ _shape = m.get("codebook_spatial_shape")
324
+ codebook_spatial_shape = tuple(int(x) for x in _shape) if _shape is not None else None
325
+
326
+ # Inference-time fields (optional in YAML; sensible defaults).
327
+ _infer = m.get("inference") or {}
328
+ _infer_decode = str(_infer.get("decode_method", m.get("inference_decode_method", "auto")))
329
+ _infer_max = _infer.get("max_tokens", m.get("inference_max_tokens"))
330
+ _infer_cfg = str(_infer.get("cfg_uncond", m.get("inference_cfg_uncond", "auto")))
331
+ _infer_add_instr = bool(_infer.get("add_instruction", m.get("inference_add_instruction", True)))
332
+ _infer_cfg_img = _infer.get("cfg_img_scale", m.get("inference_cfg_img_scale"))
333
+ _infer_cfg_img = float(_infer_cfg_img) if _infer_cfg_img is not None else None
334
+
335
+ spec = ModalitySpec(
336
+ name=name,
337
+ id=int(m["id"]),
338
+ start_token_key=str(m["start_token_key"]),
339
+ end_token_key=str(m["end_token_key"]),
340
+ kind=str(m.get("kind", "text")),
341
+ code_token_range=_range,
342
+ code_token_ids=_ids,
343
+ pos_embed_size=pos_embed_size,
344
+ apply_pos_embed_in_forward=bool(m.get("apply_pos_embed_in_forward", False)),
345
+ represent_vit=bool(m.get("represent_vit", True)),
346
+ represent_vae=bool(m.get("represent_vae", False)),
347
+ conditions=conditions,
348
+ condition_probs=condition_probs,
349
+ loss=loss_cfg,
350
+ pos_embed_name=pe_name,
351
+ codebook_spatial_shape=codebook_spatial_shape,
352
+ external_tokenizer_repo=m.get("external_tokenizer_repo"),
353
+ external_tokenizer_kind=str(m.get("external_tokenizer_kind", "vqvae")),
354
+ inference_decode_method=_infer_decode,
355
+ inference_max_tokens=int(_infer_max) if _infer_max is not None else None,
356
+ inference_cfg_uncond=_infer_cfg,
357
+ inference_add_instruction=_infer_add_instr,
358
+ inference_cfg_img_scale=_infer_cfg_img,
359
+ )
360
+ specs.append(spec)
361
+
362
+ # ── Auto-assign text token range to text-kind modalities ─────────
363
+ # Find text_vocab_end = min base across all codebook ranges.
364
+ text_vocab_end: Optional[int] = None
365
+ for s in specs:
366
+ if s.code_token_range is not None:
367
+ cb_base, _ = s.code_token_range
368
+ text_vocab_end = int(cb_base) if text_vocab_end is None else min(text_vocab_end, int(cb_base))
369
+
370
+ if text_vocab_end is not None:
371
+ text_range: TokenRange = (0, text_vocab_end)
372
+ specs = [
373
+ dc_replace(s, code_token_range=text_range)
374
+ if s.kind == "text" and s.code_token_range is None else s
375
+ for s in specs
376
+ ]
377
+
378
+ return ModalityRegistry(specs)
379
+
380
+
381
+ def infer_contiguous_token_range(token_ids: List[int]) -> TokenRange:
382
+ """Given a list of token IDs, infer ``(base, length)`` and validate contiguity."""
383
+ if len(token_ids) == 0:
384
+ raise ValueError("token_ids cannot be empty")
385
+ token_ids_sorted = sorted(int(x) for x in token_ids)
386
+ base = token_ids_sorted[0]
387
+ length = len(token_ids_sorted)
388
+ expected = list(range(base, base + length))
389
+ if token_ids_sorted != expected:
390
+ raise ValueError(
391
+ "Token IDs are not contiguous; cannot represent as a range efficiently. "
392
+ f"base={base}, length={length}, first10={token_ids_sorted[:10]}"
393
+ )
394
+ return base, length
core/model_registry.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any, Callable, Dict, Optional, Tuple
5
+
6
+
7
+ ModelBuilder = Callable[..., Any]
8
+
9
+
10
+ class ModelRegistry:
11
+ """
12
+ Minimal model registry to decouple scripts from concrete model classes.
13
+
14
+ Goal:
15
+ - training/inference scripts choose `model.name` in config
16
+ - swapping architectures doesn't require editing scripts, only adding a builder registration
17
+ """
18
+
19
+ def __init__(self):
20
+ self._builders: Dict[str, ModelBuilder] = {}
21
+
22
+ def register(self, name: str, builder: ModelBuilder) -> None:
23
+ if name in self._builders:
24
+ raise ValueError(f"Model '{name}' is already registered.")
25
+ self._builders[name] = builder
26
+
27
+ def build(self, name: str, **kwargs) -> Any:
28
+ if name not in self._builders:
29
+ known = ", ".join(sorted(self._builders.keys()))
30
+ raise KeyError(f"Unknown model '{name}'. Known: {known}")
31
+ return self._builders[name](**kwargs)
32
+
33
+
34
+ GLOBAL_MODEL_REGISTRY = ModelRegistry()
35
+
36
+
37
+ def register_model(name: str) -> Callable[[ModelBuilder], ModelBuilder]:
38
+ def _decorator(fn: ModelBuilder) -> ModelBuilder:
39
+ GLOBAL_MODEL_REGISTRY.register(name, fn)
40
+ return fn
41
+ return _decorator
42
+
43
+
44
+ def build_model(name: str, **kwargs) -> Any:
45
+ return GLOBAL_MODEL_REGISTRY.build(name, **kwargs)
46
+
47
+
core/tokenizer_utils.py ADDED
@@ -0,0 +1,257 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Config-driven tokenizer setup.
3
+
4
+ All modality tokens (delimiters, codebook entries) are specified in the modality
5
+ YAML and added here. No hard-coded ``use_det`` / ``use_dino`` flags.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass
10
+ from typing import Any, Dict, List, Optional, Tuple
11
+
12
+ from core.modality import TokenRange, infer_contiguous_token_range
13
+ from data.data_utils import add_special_tokens
14
+
15
+
16
+ @dataclass
17
+ class TokenizerArtifacts:
18
+ """Return value of :func:`build_tokenizer_and_special_tokens`."""
19
+ tokenizer: Any
20
+ new_token_ids: Dict[str, Any]
21
+ token_ranges: Dict[str, TokenRange]
22
+ # Explicit dispersed CE-token id sets, for modalities that set
23
+ # ``dispersed_code_tokens: true`` (e.g. cocodet). modality name -> id list.
24
+ code_token_ids: Dict[str, List[int]]
25
+
26
+
27
+ def load_base_tokenizer(*, model_args: Any, training_args: Any):
28
+ """Load the base tokenizer (before any token additions).
29
+
30
+ Dispatches to HunyuanImage3TokenizerFast when model_name == 'hunyuan_image_3',
31
+ otherwise falls back to the standard Qwen2Tokenizer.
32
+ """
33
+ if getattr(model_args, "model_name", "bagel") == "hunyuan_image_3":
34
+ from modeling.hunyuan_image_3.src.tokenization_hunyuan_image_3 import (
35
+ HunyuanImage3TokenizerFast,
36
+ )
37
+ return HunyuanImage3TokenizerFast.from_pretrained(
38
+ model_args.model_path,
39
+ model_version="HunyuanImage-3.0-Instruct",
40
+ )
41
+
42
+ from modeling.qwen2.tokenization_qwen2 import Qwen2Tokenizer
43
+
44
+ pretrained_path = (
45
+ model_args.model_path
46
+ if getattr(training_args, "finetune_from_hf", False)
47
+ else model_args.llm_path
48
+ )
49
+ return Qwen2Tokenizer.from_pretrained(pretrained_path)
50
+
51
+
52
+ # ── internal helpers ─────────────────────────────────────────────────────────
53
+
54
+ def _add_tokens(tokenizer, tokens: List[str]) -> None:
55
+ """Add tokens in order, deduplicating while preserving first occurrence."""
56
+ seen: set = set()
57
+ ordered: List[str] = []
58
+ for t in tokens:
59
+ if t not in seen:
60
+ seen.add(t)
61
+ ordered.append(t)
62
+ tokenizer.add_tokens(ordered)
63
+
64
+
65
+ def _ensure_token_key(
66
+ tokenizer,
67
+ new_token_ids: Dict[str, Any],
68
+ *,
69
+ token_key: str,
70
+ token_str: Optional[str],
71
+ ) -> None:
72
+ """
73
+ Make sure ``new_token_ids[token_key]`` exists, creating the token if needed.
74
+ """
75
+ if token_key in new_token_ids:
76
+ return
77
+ if hasattr(tokenizer, token_key):
78
+ new_token_ids[token_key] = int(getattr(tokenizer, token_key))
79
+ return
80
+ if token_str is None:
81
+ raise KeyError(
82
+ f"Token key '{token_key}' not in tokenizer or new_token_ids and no token_str provided."
83
+ )
84
+ try:
85
+ tokenizer.add_special_tokens({"additional_special_tokens": [token_str]})
86
+ except Exception:
87
+ tokenizer.add_tokens([token_str])
88
+ new_token_ids[token_key] = int(tokenizer.convert_tokens_to_ids(token_str))
89
+
90
+
91
+ def _code_tokens_from_cfg(m: Dict[str, Any]) -> List[str]:
92
+ """Build the list of code-token strings from a modality config dict."""
93
+
94
+ # Preferred: explicit groups (e.g. DET coordinate tokens).
95
+ groups = m.get("code_token_groups")
96
+ if groups is not None:
97
+ out: List[str] = []
98
+ for g in groups:
99
+ if not isinstance(g, dict):
100
+ g = dict(g)
101
+ token_format = str(g["token_format"])
102
+ start = int(g.get("start", 0))
103
+ end = int(g["end"])
104
+ for i in range(start, end + 1):
105
+ out.append(token_format.format(
106
+ i=i,
107
+ prefix=m.get("code_token_prefix", m.get("name")),
108
+ ))
109
+ return out
110
+
111
+ # Fallback: contiguous vocab with format / prefix.
112
+ vocab = m.get("code_vocab_size")
113
+ if vocab is None:
114
+ raise ValueError(
115
+ f"Codebook modality '{m.get('name')}' needs 'code_vocab_size' or 'code_token_groups'."
116
+ )
117
+ vocab = int(vocab)
118
+ token_format = str(m.get("code_token_format", "<|{prefix}_{i:04d}|>"))
119
+ prefix = str(m.get("code_token_prefix", m.get("name")))
120
+ return [token_format.format(prefix=prefix, i=i) for i in range(vocab)]
121
+
122
+
123
+ def _add_modality_tokens(
124
+ tokenizer,
125
+ *,
126
+ m: Dict[str, Any],
127
+ new_token_ids: Dict[str, Any],
128
+ token_ranges: Dict[str, TokenRange],
129
+ code_token_ids_out: Dict[str, List[int]],
130
+ deferred_tokens: List[Any],
131
+ ) -> None:
132
+ """
133
+ Add all tokens for one modality entry (text/image/codebook) from its YAML dict.
134
+
135
+ For **codebook** modalities the addition order is *code tokens first, then
136
+ delimiter / extra tokens*. This matches the ordering used when the
137
+ original checkpoint was trained so that token IDs remain identical.
138
+ """
139
+ kind = str(m.get("kind", "text"))
140
+ start_key = str(m["start_token_key"])
141
+ end_key = str(m["end_token_key"])
142
+
143
+ if kind != "codebook":
144
+ # Image/text modalities: resolve already-existing start/end tokens now
145
+ # (shared start_of_image, or start_of_depth/normal added by
146
+ # add_special_tokens). Any genuinely NEW token (e.g. a per-modality
147
+ # start_of_seg/canny/samseg/samedge under REPLACE) is DEFERRED and added
148
+ # at the very tail in build_tokenizer_and_special_tokens β€” so introducing
149
+ # an image modality with its own start token does NOT shift the ids of
150
+ # codebook tokens added later in the loop, preserving cross-stage ckpt
151
+ # alignment.
152
+ vocab = tokenizer.get_vocab()
153
+ for token_key, token_str in ((start_key, m.get("start_token")), (end_key, m.get("end_token"))):
154
+ if token_key in new_token_ids:
155
+ continue
156
+ if token_str is not None and str(token_str) in vocab:
157
+ new_token_ids[token_key] = int(vocab[str(token_str)])
158
+ elif token_str is not None:
159
+ deferred_tokens.append((token_key, str(token_str)))
160
+ else:
161
+ _ensure_token_key(tokenizer, new_token_ids, token_key=token_key, token_str=None)
162
+ return
163
+
164
+ # ── codebook modality ────────────────────────────────────────────────
165
+ code_tokens = _code_tokens_from_cfg(m)
166
+
167
+ start_tok = m.get("start_token")
168
+ end_tok = m.get("end_token")
169
+ delim_tokens: List[str] = []
170
+ if start_tok is not None:
171
+ delim_tokens.append(str(start_tok))
172
+ if end_tok is not None:
173
+ delim_tokens.append(str(end_tok))
174
+ delim_tokens.extend(str(t) for t in (m.get("extra_tokens") or []))
175
+
176
+ # One batch: code tokens first, delimiters after β†’ matches checkpoint ordering.
177
+ _add_tokens(tokenizer, code_tokens + delim_tokens)
178
+
179
+ # Populate delimiter keys.
180
+ if start_tok is not None and start_key not in new_token_ids:
181
+ new_token_ids[start_key] = int(tokenizer.convert_tokens_to_ids(str(start_tok)))
182
+ elif start_key not in new_token_ids:
183
+ _ensure_token_key(tokenizer, new_token_ids, token_key=start_key, token_str=None)
184
+ if end_tok is not None and end_key not in new_token_ids:
185
+ new_token_ids[end_key] = int(tokenizer.convert_tokens_to_ids(str(end_tok)))
186
+ elif end_key not in new_token_ids:
187
+ _ensure_token_key(tokenizer, new_token_ids, token_key=end_key, token_str=None)
188
+
189
+ name = str(m["name"])
190
+ token_ids = [int(tokenizer.convert_tokens_to_ids(t)) for t in code_tokens]
191
+ if bool(m.get("dispersed_code_tokens", False)):
192
+ # Dispersed alphabet (e.g. cocodet: reused det coords at low vocab ids +
193
+ # new class tokens at the tail) β€” cannot be a single contiguous range.
194
+ # Record the explicit CE-token id set = all code tokens + the end
195
+ # delimiter (a CE target / stop token). The start delimiter is
196
+ # input-only, so it is excluded.
197
+ ids = list(token_ids)
198
+ end_id = new_token_ids.get(end_key)
199
+ if end_id is not None:
200
+ ids.append(int(end_id))
201
+ code_token_ids_out[name] = ids
202
+ else:
203
+ # Record contiguous code-token range.
204
+ token_ranges[name] = infer_contiguous_token_range(token_ids)
205
+
206
+
207
+ # ── public API ───────────────────────────────────────────────────────────────
208
+
209
+ def build_tokenizer_and_special_tokens(
210
+ tokenizer,
211
+ *,
212
+ modalities_cfg: Dict[str, Any],
213
+ ) -> TokenizerArtifacts:
214
+ """
215
+ Fully config-driven tokenizer setup.
216
+
217
+ 1. Adds universal special tokens (``<|im_start|>``, ``<|vision_start|>``, …)
218
+ via the legacy ``add_special_tokens`` helper (for checkpoint compat).
219
+ 2. Iterates over ``modalities_cfg["modalities"]`` and adds delimiter / code
220
+ tokens for each entry.
221
+
222
+ Returns a :class:`TokenizerArtifacts` with the final tokenizer, a dict of
223
+ all special-token IDs, and a dict of code-token ranges for codebook modalities.
224
+ """
225
+ token_ranges: Dict[str, TokenRange] = {}
226
+ code_token_ids: Dict[str, List[int]] = {}
227
+ deferred_tokens: List[Any] = []
228
+
229
+ # Step 1: universal special tokens (checkpoint-compatible ordering).
230
+ tokenizer, new_token_ids, _num = add_special_tokens(tokenizer)
231
+
232
+ # Step 2: per-modality tokens from YAML.
233
+ modalities = modalities_cfg.get("modalities", modalities_cfg)
234
+ for m in modalities:
235
+ if not isinstance(m, dict):
236
+ m = dict(m)
237
+ _add_modality_tokens(
238
+ tokenizer,
239
+ m=m,
240
+ new_token_ids=new_token_ids,
241
+ token_ranges=token_ranges,
242
+ code_token_ids_out=code_token_ids,
243
+ deferred_tokens=deferred_tokens,
244
+ )
245
+
246
+ # Step 3: append deferred NEW image-modality start/end tokens at the tail
247
+ # (after all codebook tokens) so they never shift existing ids across stages.
248
+ for token_key, token_str in deferred_tokens:
249
+ if token_key not in new_token_ids:
250
+ _ensure_token_key(tokenizer, new_token_ids, token_key=token_key, token_str=token_str)
251
+
252
+ return TokenizerArtifacts(
253
+ tokenizer=tokenizer,
254
+ new_token_ids=new_token_ids,
255
+ token_ranges=token_ranges,
256
+ code_token_ids=code_token_ids,
257
+ )
data/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ # Copyright 2025 Bytedance Ltd. and/or its affiliates.
2
+ # SPDX-License-Identifier: Apache-2.0
data/any2any_preprocess/_build_preview_and_montage.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import io, os
2
+ import pyarrow.parquet as pq
3
+ from PIL import Image, ImageDraw
4
+
5
+ BASE_SRC = ('datasets/blip3o/parquet_rgb_caption_depth_normal_det_seg_grounding'
6
+ '_canny_dino_global_clip448_imagebind_samseg_samedge_cocodet')
7
+ Q95 = 'datasets/blip3o/parquet_16mod_normalq95_sample/sa_000000.parquet'
8
+ PREVIEW_DIR = 'datasets/blip3o/modus_preview'
9
+ MONTAGE = 'eval_outputs/modus_hero_montage.png'
10
+ N_ROWS = 500
11
+
12
+ # ---- 1. print sample uids per source to confirm the alignment key ----
13
+ print('==== uid samples (alignment key check) ====', flush=True)
14
+ for f in ['sa_000000.parquet', 'webdataset_shard_000.parquet', 'webdataset_JDB_2.parquet']:
15
+ p = f'{BASE_SRC}/{f}'
16
+ t = pq.ParquetFile(p).read_row_group(0, columns=['uid'])
17
+ uids = t.column('uid').to_pylist()[:4]
18
+ print(f' {f}: {uids}', flush=True)
19
+
20
+ # ---- 2. build small preview parquet (subset of q95-compressed sa_000000) ----
21
+ os.makedirs(PREVIEW_DIR, exist_ok=True)
22
+ pf = pq.ParquetFile(Q95)
23
+ table = pf.read_row_group(0).slice(0, N_ROWS)
24
+ dst = f'{PREVIEW_DIR}/modus_preview_sa_500.parquet'
25
+ pq.write_table(table, dst, compression='snappy')
26
+ print(f'\npreview: {dst} rows={table.num_rows} size={os.path.getsize(dst)/1e6:.1f} MB', flush=True)
27
+
28
+ # ---- 3. hero montage: rows=samples, cols=modalities ----
29
+ MODS = ['rgb', 'depth', 'normal', 'canny', 'sam_seg', 'sam_edge']
30
+ N_SHOW = 5
31
+ cell, pad, hdr = 300, 6, 26
32
+ cols = {m: table.column(m).to_pylist()[:N_SHOW] for m in MODS}
33
+
34
+ def dec(b):
35
+ if b is None: return None
36
+ if isinstance(b, memoryview): b = b.tobytes()
37
+ return Image.open(io.BytesIO(b)).convert('RGB')
38
+
39
+ W = len(MODS)*(cell+pad)+pad
40
+ H = hdr + N_SHOW*(cell+pad)+pad
41
+ grid = Image.new('RGB', (W, H), 'white')
42
+ d = ImageDraw.Draw(grid)
43
+ for c, m in enumerate(MODS):
44
+ d.text((pad+c*(cell+pad)+4, 6), m, fill='black')
45
+ for r in range(N_SHOW):
46
+ for c, m in enumerate(MODS):
47
+ im = dec(cols[m][r])
48
+ if im is None: continue
49
+ grid.paste(im.resize((cell, cell)), (pad+c*(cell+pad), hdr+pad+r*(cell+pad)))
50
+ os.makedirs(os.path.dirname(MONTAGE), exist_ok=True)
51
+ grid.save(MONTAGE)
52
+ print(f'montage: {MONTAGE} ({W}x{H})', flush=True)
data/any2any_preprocess/_cast_preview_images.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Cast the preview parquet's raw-bytes image columns to the HF `Image` feature
2
+ so the dataset viewer renders thumbnails."""
3
+ import json
4
+ import os
5
+ from datasets import load_dataset, Image, Value
6
+
7
+ SRC = 'datasets/blip3o/modus_preview/modus_preview_sa_500.parquet'
8
+ OUT_DIR = 'datasets/blip3o/modus_preview_upload/data'
9
+ OUT = f'{OUT_DIR}/modus_preview_sa_500.parquet'
10
+ IMG_COLS = ['rgb', 'depth', 'normal', 'canny', 'sam_seg', 'sam_edge']
11
+
12
+ # Display-name order requested by the user (uid=key first; det_seg + grounding
13
+ # raw blobs moved to the very end).
14
+ ORDER = [
15
+ 'uid', 'rgb', 'depth', 'normal', 'sam_seg', 'canny', 'sam_edge',
16
+ 'coco_det', 'caption', 'dino_global', 'dino', 'clip448',
17
+ 'imagebind_global', 'imagebind', 'det_seg', 'grounding',
18
+ ]
19
+
20
+ ds = load_dataset('parquet', data_files=SRC, split='train')
21
+ print('loaded:', ds.num_rows, 'rows; features before:', list(ds.features.keys()))
22
+ for c in IMG_COLS:
23
+ ds = ds.cast_column(c, Image())
24
+ # Flatten deep-nested blobs to JSON strings so the HF viewer worker doesn't OOM.
25
+ for c in ['det_seg', 'grounding']:
26
+ ds = ds.map(lambda ex, col=c: {col: json.dumps(ex[col], default=str, ensure_ascii=False)})
27
+ ds = ds.cast_column(c, Value('string'))
28
+ assert set(ORDER) == set(ds.column_names), set(ORDER) ^ set(ds.column_names)
29
+ ds = ds.select_columns(ORDER)
30
+ print('column order:', ds.column_names)
31
+
32
+ os.makedirs(OUT_DIR, exist_ok=True)
33
+ ds.to_parquet(OUT)
34
+ print(f'wrote {OUT} size={os.path.getsize(OUT)/1e6:.1f} MB', flush=True)
data/any2any_preprocess/_sb_full_rebuild.sh ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ #SBATCH --job-name=modus_full_rebuild
3
+ #SBATCH --partition=debug
4
+ #SBATCH --time=01:30:00
5
+ #SBATCH --nodes=1
6
+ #SBATCH --ntasks-per-node=1
7
+ #SBATCH --cpus-per-task=128
8
+ #SBATCH --mem=700GB
9
+ #SBATCH --account=a143
10
+ #SBATCH --output=logs/full_rebuild_%j.out
11
+ #SBATCH --error=logs/full_rebuild_%j.err
12
+ #SBATCH --environment=/iopsstor/scratch/cscs/mye/workspace/BAGEL/environment/bagel.toml
13
+
14
+ M=/iopsstor/scratch/cscs/mye/workspace/MODUS_RELEASE/MODUS
15
+ srun --environment=/iopsstor/scratch/cscs/mye/workspace/BAGEL/environment/bagel.toml \
16
+ bash -c "cd $M && export PYTHONPATH='$M/data/any2any_preprocess:\$PYTHONPATH' && \
17
+ python3 data/any2any_preprocess/run_full_rebuild.py \
18
+ --num-tasks 1 --task-id 0 --concurrency 64"
data/any2any_preprocess/_sb_recompress_normal_sample.sh ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ #SBATCH --job-name=norm_q95_smp
3
+ #SBATCH --partition=debug
4
+ #SBATCH --time=01:30:00
5
+ #SBATCH --nodes=1
6
+ #SBATCH --ntasks-per-node=1
7
+ #SBATCH --cpus-per-task=8
8
+ #SBATCH --mem=64GB
9
+ #SBATCH --account=a143
10
+ #SBATCH --output=logs/norm_q95_sample_%j.out
11
+ #SBATCH --error=logs/norm_q95_sample_%j.err
12
+ #SBATCH --environment=/iopsstor/scratch/cscs/mye/workspace/BAGEL/environment/bagel.toml
13
+
14
+ M=/iopsstor/scratch/cscs/mye/workspace/MODUS_RELEASE/MODUS
15
+ SRC="$M/datasets/blip3o/parquet_rgb_caption_depth_normal_det_seg_grounding_canny_dino_global_clip448_imagebind_samseg_samedge_cocodet"
16
+ OUT="$M/datasets/blip3o/parquet_16mod_normalq95_sample"
17
+
18
+ srun --environment=/iopsstor/scratch/cscs/mye/workspace/BAGEL/environment/bagel.toml \
19
+ bash -c "cd $M && python3 data/any2any_preprocess/recompress_normal_jpeg.py \
20
+ --out_dir '$OUT' \
21
+ '$SRC/sa_000000.parquet' \
22
+ '$SRC/sa_000001.parquet' \
23
+ '$SRC/sa_000002.parquet' \
24
+ '$SRC/sa_000003.parquet'"
data/any2any_preprocess/_sb_upload.sh ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ #SBATCH --job-name=modus_upload
3
+ #SBATCH --partition=debug
4
+ #SBATCH --time=01:30:00
5
+ #SBATCH --nodes=1
6
+ #SBATCH --ntasks-per-node=1
7
+ #SBATCH --cpus-per-task=32
8
+ #SBATCH --mem=200GB
9
+ #SBATCH --account=a143
10
+ #SBATCH --output=logs/upload_%j.out
11
+ #SBATCH --error=logs/upload_%j.err
12
+ #SBATCH --environment=/iopsstor/scratch/cscs/mye/workspace/BAGEL/environment/bagel.toml
13
+
14
+ # Resumable upload of the whole modus_full folder (all configs) + README.
15
+ M=/iopsstor/scratch/cscs/mye/workspace/MODUS_RELEASE/MODUS
16
+ srun --environment=/iopsstor/scratch/cscs/mye/workspace/BAGEL/environment/bagel.toml \
17
+ bash -c "cd $M && export HF_TOKEN_FILE=/users/mye/.hf_token && \
18
+ python3 -c \"from huggingface_hub import HfApi; t=open('/users/mye/.hf_token').read().strip(); \
19
+ HfApi(token=t).upload_file(path_or_fileobj='datasets/blip3o/modus_preview_upload/README.md', path_in_repo='README.md', repo_id='epfl-vilab-modus/MODUS-16Modality', repo_type='dataset', commit_message='full configs card'); print('README ok')\" ; \
20
+ python3 data/any2any_preprocess/upload_full.py \
21
+ epfl-vilab-modus/MODUS-16Modality datasets/blip3o/modus_full '*/*.parquet'"
data/any2any_preprocess/_verify_normal_q95.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import io, os
2
+ import pyarrow.parquet as pq
3
+ from PIL import Image
4
+
5
+ SRC = ('datasets/blip3o/parquet_rgb_caption_depth_normal_det_seg_grounding'
6
+ '_canny_dino_global_clip448_imagebind_samseg_samedge_cocodet/sa_000000.parquet')
7
+ NEW = 'datasets/blip3o/parquet_16mod_normalq95_sample/sa_000000.parquet'
8
+ OUT = 'eval_outputs/normal_q95_check'
9
+ os.makedirs(OUT, exist_ok=True)
10
+
11
+ def load(path, col, n):
12
+ t = pq.ParquetFile(path).read_row_group(0, columns=[col])
13
+ vals = t.column(col).to_pylist()[:n]
14
+ out = []
15
+ for b in vals:
16
+ if isinstance(b, memoryview): b = b.tobytes()
17
+ out.append(Image.open(io.BytesIO(b)).convert('RGB'))
18
+ return out
19
+
20
+ N = 3
21
+ rgb = load(SRC, 'rgb', N)
22
+ nsrc = load(SRC, 'normal', N)
23
+ nnew = load(NEW, 'normal', N)
24
+
25
+ cell = 384
26
+ grid = Image.new('RGB', (cell*3, cell*N), 'white')
27
+ for r in range(N):
28
+ for c, im in enumerate([rgb[r], nsrc[r], nnew[r]]):
29
+ grid.paste(im.resize((cell, cell)), (c*cell, r*cell))
30
+ path = f'{OUT}/rgb_normalPNG_normalJPEGq95.png'
31
+ grid.save(path)
32
+ print(f'saved {path} (cols: rgb | normal-PNG(src) | normal-JPEGq95(new))', flush=True)
33
+ for r in range(N):
34
+ print(f' row{r}: normal src size={nsrc[r].size} new size={nnew[r].size}', flush=True)
data/any2any_preprocess/build_full_release.py ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Build the MODUS full-release parquet for one source config.
2
+
3
+ Per config:
4
+ sa1b / journeydb : annotations-only -> DROP ['rgb','caption'] ; recompress normal->JPEG q95
5
+ cc12m : self-contained -> keep all columns ; recompress normal->JPEG q95
6
+
7
+ Streams by row group (bounded memory). Resumable: skips outputs that already
8
+ exist and are complete; writes to a .tmp then atomically renames.
9
+
10
+ Usage:
11
+ python build_full_release.py --config sa1b --out_dir <dir> --workers 8 file1.parquet [...]
12
+ """
13
+ import argparse
14
+ import io
15
+ import os
16
+ from concurrent.futures import ProcessPoolExecutor, as_completed
17
+
18
+ import pyarrow as pa
19
+ import pyarrow.parquet as pq
20
+ from PIL import Image
21
+
22
+ DROP = {'sa1b': ['rgb', 'caption'], 'journeydb': ['rgb', 'caption'], 'cc12m': []}
23
+ JPEG_QUALITY = 95
24
+
25
+
26
+ def _recompress_normal(b):
27
+ if b is None:
28
+ return None
29
+ if isinstance(b, memoryview):
30
+ b = b.tobytes()
31
+ with Image.open(io.BytesIO(b)) as im:
32
+ img = im.convert('RGB')
33
+ out = io.BytesIO()
34
+ img.save(out, format='JPEG', quality=JPEG_QUALITY)
35
+ img.close()
36
+ return out.getvalue()
37
+
38
+
39
+ def process_one(src, dst, drop_cols):
40
+ if os.path.exists(dst) and os.path.getsize(dst) > 0:
41
+ return src, 'skip', os.path.getsize(dst)
42
+ tmp = dst + '.tmp'
43
+ pf = pq.ParquetFile(src)
44
+ names = pf.schema_arrow.names
45
+ keep = [n for n in names if n not in drop_cols]
46
+ out_schema = pa.schema([pf.schema_arrow.field(n) for n in keep])
47
+ has_normal = 'normal' in keep
48
+ norm_field = out_schema.field('normal') if has_normal else None
49
+ ni = keep.index('normal') if has_normal else -1
50
+
51
+ # Stream in small batches so peak memory is bounded regardless of row-group size.
52
+ writer = pq.ParquetWriter(tmp, out_schema, compression='snappy')
53
+ try:
54
+ for batch in pf.iter_batches(batch_size=128, columns=keep):
55
+ t = pa.Table.from_batches([batch], schema=out_schema)
56
+ if has_normal:
57
+ col = [_recompress_normal(b) for b in t.column('normal').to_pylist()]
58
+ t = t.set_column(ni, norm_field, pa.array(col, type=norm_field.type))
59
+ writer.write_table(t)
60
+ del t
61
+ finally:
62
+ writer.close()
63
+ os.replace(tmp, dst)
64
+ return src, 'done', os.path.getsize(dst)
65
+
66
+
67
+ def main():
68
+ ap = argparse.ArgumentParser()
69
+ ap.add_argument('--config', required=True, choices=list(DROP))
70
+ ap.add_argument('--out_dir', required=True)
71
+ ap.add_argument('--workers', type=int, default=8)
72
+ ap.add_argument('files', nargs='+')
73
+ args = ap.parse_args()
74
+ os.makedirs(args.out_dir, exist_ok=True)
75
+ drop_cols = DROP[args.config]
76
+
77
+ tasks = [(f, os.path.join(args.out_dir, os.path.basename(f)), drop_cols)
78
+ for f in args.files]
79
+ done = skip = 0
80
+
81
+ if args.workers == 1:
82
+ # Sequential, in-process (used when spawned as a per-chunk subprocess so
83
+ # the OS reclaims all memory on exit β€” no leak accumulation).
84
+ for src, dst, dc in tasks:
85
+ _, status, _ = process_one(src, dst, dc)
86
+ done += status == 'done'
87
+ skip += status == 'skip'
88
+ print(f'{status} {os.path.basename(src)}', flush=True)
89
+ print(f'CHUNK {args.config}: {done} built, {skip} skipped', flush=True)
90
+ return
91
+
92
+ tot_out = 0
93
+ with ProcessPoolExecutor(max_workers=args.workers) as ex:
94
+ futs = [ex.submit(process_one, *t) for t in tasks]
95
+ for i, fu in enumerate(as_completed(futs), 1):
96
+ src, status, sz = fu.result()
97
+ tot_out += sz
98
+ done += status == 'done'
99
+ skip += status == 'skip'
100
+ if i % 10 == 0 or status == 'done':
101
+ print(f'[{i}/{len(tasks)}] {status} {os.path.basename(src)} '
102
+ f'done={done} skip={skip} out={tot_out/1e9:.1f}GB', flush=True)
103
+ print(f'CONFIG {args.config}: {done} built, {skip} skipped, '
104
+ f'total_out={tot_out/1e9:.1f}GB', flush=True)
105
+
106
+
107
+ if __name__ == '__main__':
108
+ main()
data/any2any_preprocess/check_vqa.py ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import argparse
4
+ from typing import Dict, Any, Tuple, List
5
+
6
+
7
+ def is_valid_name(name: Any) -> bool:
8
+ return isinstance(name, str) and len(name.strip()) > 0
9
+
10
+
11
+ def process_labels_jsonl(images_dir: str, labels_path: str, max_lines: int = -1) -> Tuple[Dict[str, Any], List[str]]:
12
+ stats: Dict[str, Any] = dict(
13
+ num_records=0,
14
+ num_invalid_json=0,
15
+ num_image_refs=0,
16
+ num_images_missing=0,
17
+ num_unique_images_missing=0,
18
+ num_video_refs=0,
19
+ num_videos_missing=0,
20
+ )
21
+ missing_images_unique = set()
22
+
23
+ with open(labels_path, "r") as f:
24
+ for line_idx, line in enumerate(f):
25
+ if max_lines > 0 and line_idx >= max_lines:
26
+ break
27
+ try:
28
+ record = json.loads(line)
29
+ except Exception:
30
+ stats["num_invalid_json"] += 1
31
+ continue
32
+
33
+ stats["num_records"] += 1
34
+
35
+ # Check images (string or list)
36
+ if "image" in record:
37
+ if isinstance(record["image"], list):
38
+ image_names = record["image"]
39
+ else:
40
+ image_names = [record["image"]]
41
+
42
+ for name in image_names:
43
+ stats["num_image_refs"] += 1
44
+ if not is_valid_name(name):
45
+ stats["num_images_missing"] += 1
46
+ missing_images_unique.add(str(name))
47
+ continue
48
+ image_path = os.path.join(images_dir, name)
49
+ if not os.path.isfile(image_path):
50
+ stats["num_images_missing"] += 1
51
+ missing_images_unique.add(name)
52
+
53
+ # Check video if present (optional)
54
+ if "video" in record:
55
+ stats["num_video_refs"] += 1
56
+ video_name = record["video"]
57
+ if not is_valid_name(video_name):
58
+ stats["num_videos_missing"] += 1
59
+ else:
60
+ video_path = os.path.join(images_dir, video_name)
61
+ if not os.path.isfile(video_path):
62
+ stats["num_videos_missing"] += 1
63
+
64
+ stats["num_unique_images_missing"] = len(missing_images_unique)
65
+ # Return up to some examples of missing files for debugging
66
+ sample_missing = list(missing_images_unique)[:50]
67
+ return stats, sample_missing
68
+
69
+
70
+ def scan_base_dir(base_dir: str, max_lines: int = -1) -> Dict[str, Any]:
71
+ summary: Dict[str, Any] = dict(per_dataset={}, totals={})
72
+ totals = dict(
73
+ datasets_scanned=0,
74
+ num_records=0,
75
+ num_invalid_json=0,
76
+ num_image_refs=0,
77
+ num_images_missing=0,
78
+ num_unique_images_missing=0, # accumulated across datasets (sum, not set-union)
79
+ num_video_refs=0,
80
+ num_videos_missing=0,
81
+ )
82
+
83
+ if not os.path.isdir(base_dir):
84
+ raise FileNotFoundError(f"Base dir does not exist: {base_dir}")
85
+
86
+ # Pre-collect datasets that have labels.jsonl for better progress logging
87
+ ds_entries = []
88
+ for entry in sorted(os.listdir(base_dir)):
89
+ ds_dir = os.path.join(base_dir, entry)
90
+ if not os.path.isdir(ds_dir):
91
+ continue
92
+ labels_path = os.path.join(ds_dir, "label", "labels.jsonl")
93
+ if os.path.isfile(labels_path):
94
+ ds_entries.append(entry)
95
+
96
+ total_datasets = len(ds_entries)
97
+ for idx, entry in enumerate(ds_entries, 1):
98
+ ds_dir = os.path.join(base_dir, entry)
99
+ images_dir = os.path.join(ds_dir, "images")
100
+ labels_path = os.path.join(ds_dir, "label", "labels.jsonl")
101
+
102
+ print(f"[{idx}/{total_datasets}] Scanning {entry} ...", flush=True)
103
+
104
+ totals["datasets_scanned"] += 1
105
+ ds_stats, sample_missing = process_labels_jsonl(images_dir, labels_path, max_lines=max_lines)
106
+
107
+ summary["per_dataset"][entry] = dict(stats=ds_stats, sample_missing_images=sample_missing)
108
+
109
+ # Accumulate totals (note: unique missing is summed per dataset)
110
+ for k in [
111
+ "num_records",
112
+ "num_invalid_json",
113
+ "num_image_refs",
114
+ "num_images_missing",
115
+ "num_unique_images_missing",
116
+ "num_video_refs",
117
+ "num_videos_missing",
118
+ ]:
119
+ totals[k] += ds_stats[k]
120
+
121
+ # Immediate per-dataset summary line
122
+ pct = 100.0 * ds_stats["num_images_missing"] / max(1, ds_stats["num_image_refs"])
123
+ print(
124
+ f" -> records={ds_stats['num_records']}, image_refs={ds_stats['num_image_refs']}, "
125
+ f"missing_images={ds_stats['num_images_missing']} ({pct:.3f}%), videos_missing={ds_stats['num_videos_missing']}",
126
+ flush=True,
127
+ )
128
+
129
+ summary["totals"] = totals
130
+ return summary
131
+
132
+
133
+ def main():
134
+ parser = argparse.ArgumentParser(description="Check VQA JSONL image/video references against images directories.")
135
+ parser.add_argument(
136
+ "--base_dir",
137
+ type=str,
138
+ default="./datasets/llava_onevision_vqa",
139
+ help="Root directory containing per-dataset subfolders with images/ and label/labels.jsonl",
140
+ )
141
+ parser.add_argument(
142
+ "--max_lines",
143
+ type=int,
144
+ default=-1,
145
+ help="If >0, limit number of lines per JSONL file for a faster sample-based check.",
146
+ )
147
+ parser.add_argument(
148
+ "--out_json",
149
+ type=str,
150
+ default="",
151
+ help="Optional path to write a JSON summary report.",
152
+ )
153
+
154
+ args = parser.parse_args()
155
+
156
+ summary = scan_base_dir(args.base_dir, max_lines=args.max_lines)
157
+
158
+ totals = summary["totals"]
159
+ print("==== VQA Consistency Check ====")
160
+ print(
161
+ f"Datasets: {totals['datasets_scanned']} | Records: {totals['num_records']} | "
162
+ f"Image refs: {totals['num_image_refs']} | Missing images: {totals['num_images_missing']} | "
163
+ f"Video refs: {totals['num_video_refs']} | Missing videos: {totals['num_videos_missing']}"
164
+ )
165
+ if totals["num_image_refs"] > 0:
166
+ miss_pct = 100.0 * totals["num_images_missing"] / max(1, totals["num_image_refs"])
167
+ print(f"Image missing rate: {miss_pct:.4f}%")
168
+
169
+ # Print a few per-dataset highlights
170
+ for ds_name, data in list(summary["per_dataset"].items())[:20]:
171
+ s = data["stats"]
172
+ pct = 100.0 * s["num_images_missing"] / max(1, s["num_image_refs"])
173
+ print(
174
+ f"- {ds_name}: records={s['num_records']}, image_refs={s['num_image_refs']}, "
175
+ f"missing_images={s['num_images_missing']} ({pct:.3f}%)"
176
+ )
177
+
178
+ if args.out_json:
179
+ with open(args.out_json, "w") as f:
180
+ json.dump(summary, f)
181
+ print(f"Summary written to {args.out_json}")
182
+
183
+
184
+ if __name__ == "__main__":
185
+ main()
186
+
187
+
188
+ # python data/any2any_preprocess/check_vqa.py --base_dir ./datasets/llava_onevision_vqa
data/any2any_preprocess/generate_parquest_grounding_canny_dino_global.py ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import io
3
+ import glob
4
+ import tarfile
5
+ import logging
6
+ from typing import Dict, List, Optional
7
+
8
+ import numpy as np
9
+ import pandas as pd
10
+
11
+
12
+ # Set up logging
13
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class GlobalDinoReplacer:
18
+ def __init__(self, base_dir: str,
19
+ input_parquet_dir: Optional[str] = None,
20
+ global_dino_dir: Optional[str] = None,
21
+ output_parquet_dir: Optional[str] = None) -> None:
22
+ self.base_dir = base_dir
23
+
24
+ # Defaults aligned with generate_parquet_grounding_canny_dino.py
25
+ self.input_parquet_dir = input_parquet_dir or os.path.join(
26
+ base_dir, 'parquet_rgb_caption_depth_normal_det_seg_grounding_canny_dino'
27
+ )
28
+ self.global_dino_dir = global_dino_dir or os.path.join(base_dir, 'dinov2_global')
29
+ self.output_parquet_dir = output_parquet_dir or os.path.join(
30
+ base_dir, 'parquet_rgb_caption_depth_normal_det_seg_grounding_canny_dino_global'
31
+ )
32
+
33
+ os.makedirs(self.output_parquet_dir, exist_ok=True)
34
+
35
+ def _list_input_parquets(self) -> List[str]:
36
+ files = glob.glob(os.path.join(self.input_parquet_dir, '*.parquet'))
37
+ return sorted(files)
38
+
39
+ def _load_global_dino_from_tar(self, tar_path: str) -> Dict[str, np.ndarray]:
40
+ """Load all uid -> flattened tokens from a dinov2_global tar."""
41
+ results: Dict[str, np.ndarray] = {}
42
+
43
+ if not os.path.exists(tar_path):
44
+ logger.warning(f"Global DINO tar not found: {tar_path}")
45
+ return results
46
+
47
+ try:
48
+ with tarfile.open(tar_path, 'r') as tar:
49
+ for member in tar.getmembers():
50
+ if not member.isfile():
51
+ continue
52
+ name = member.name
53
+ if not name.endswith('_tokens_global.npy'):
54
+ continue
55
+
56
+ file_obj = tar.extractfile(member)
57
+ if file_obj is None:
58
+ continue
59
+
60
+ content = file_obj.read()
61
+ npy = np.load(io.BytesIO(content))
62
+
63
+ # Derive uid by stripping extension and the trailing `_tokens`
64
+ uid = os.path.splitext(name)[0]
65
+ if uid.endswith('_tokens_global'):
66
+ uid = uid[:-len('_tokens_global')]
67
+
68
+ results[uid] = npy.flatten()
69
+ except Exception as e:
70
+ logger.error(f"Failed to read {tar_path}: {e}")
71
+
72
+ return results
73
+
74
+ def _replace_dino_column(self, df: pd.DataFrame, uid_to_tokens: Dict[str, np.ndarray],
75
+ strict: bool = False) -> pd.DataFrame:
76
+ if 'uid' not in df.columns:
77
+ raise ValueError("Input parquet missing required 'uid' column")
78
+
79
+ num_rows = len(df)
80
+ replaced = 0
81
+ missing = 0
82
+
83
+ # Prepare a new column without mutating during iteration to avoid pandas warnings
84
+ new_dino_column: List[object] = []
85
+ for _, row in df.iterrows():
86
+ uid = row['uid']
87
+ tokens = uid_to_tokens.get(uid)
88
+ if tokens is None:
89
+ missing += 1
90
+ if strict:
91
+ new_dino_column.append(None)
92
+ else:
93
+ # Keep original if present
94
+ new_dino_column.append(row.get('dino_global', None))
95
+ else:
96
+ new_dino_column.append(tokens)
97
+ replaced += 1
98
+
99
+ logger.info(f"Replaced DINO tokens for {replaced}/{num_rows} rows; missing {missing}")
100
+
101
+ if strict and missing > 0:
102
+ logger.error("Strict mode: some UIDs are missing global DINO tokens")
103
+
104
+ df = df.copy()
105
+ df['dino_global'] = new_dino_column
106
+ return df
107
+
108
+ def process_one_parquet(self, parquet_path: str, overwrite: bool = False, strict: bool = False) -> Optional[str]:
109
+ base_name = os.path.splitext(os.path.basename(parquet_path))[0]
110
+ out_path = os.path.join(self.output_parquet_dir, f"{base_name}.parquet")
111
+
112
+ if os.path.exists(out_path) and not overwrite:
113
+ logger.info(f"Output exists, skipping: {out_path}")
114
+ return out_path
115
+
116
+ # Load the input parquet
117
+ df = pd.read_parquet(parquet_path, engine='pyarrow')
118
+
119
+ # Load corresponding global dino tar
120
+ global_dino_tar = os.path.join(self.global_dino_dir, f"{base_name}.tar")
121
+ logger.info(f"Loading global DINO tokens from: {global_dino_tar}")
122
+ uid_to_tokens = self._load_global_dino_from_tar(global_dino_tar)
123
+
124
+
125
+ if not uid_to_tokens:
126
+ logger.warning(f"No tokens loaded from {global_dino_tar}; leaving original 'dino' values")
127
+
128
+ # Replace column
129
+ df = self._replace_dino_column(df, uid_to_tokens, strict=strict)
130
+
131
+ # Persist
132
+ df.to_parquet(
133
+ out_path,
134
+ index=False,
135
+ engine='pyarrow',
136
+ compression='snappy',
137
+ row_group_size=1000,
138
+ )
139
+ logger.info(f"Wrote updated parquet with global DINO to: {out_path}")
140
+ return out_path
141
+
142
+ def process_all(self, overwrite: bool = False, strict: bool = False,
143
+ start_from: int = 0) -> List[str]:
144
+ inputs = self._list_input_parquets()
145
+ if start_from > 0:
146
+ inputs = inputs[start_from:]
147
+
148
+ if not inputs:
149
+ logger.error("No input parquet files found")
150
+ return []
151
+
152
+ logger.info(f"Found {len(inputs)} parquet files to update")
153
+ outputs: List[str] = []
154
+ for parquet_path in inputs:
155
+ try:
156
+ result = self.process_one_parquet(parquet_path, overwrite=overwrite, strict=strict)
157
+ if result:
158
+ outputs.append(result)
159
+ except Exception as e:
160
+ logger.error(f"Error processing {parquet_path}: {e}")
161
+
162
+ logger.info(f"Successfully wrote {len(outputs)} updated parquet files")
163
+ return outputs
164
+
165
+
166
+ def main() -> None:
167
+ import argparse
168
+
169
+ parser = argparse.ArgumentParser(description='Replace DINO tokens in existing parquet files with dinov2_global tokens')
170
+ parser.add_argument('--base_dir', type=str, default='./datasets/blip3o',
171
+ help='Base directory (default: ./datasets/blip3o)')
172
+ parser.add_argument('--input_parquet_dir', type=str, default=None,
173
+ help='Input parquet directory; defaults to parquet_rgb_caption_depth_normal_det_seg_grounding_canny_dino')
174
+ parser.add_argument('--global_dino_dir', type=str, default=None,
175
+ help='Directory containing dinov2_global tars; defaults to base_dir/dinov2_global')
176
+ parser.add_argument('--output_parquet_dir', type=str, default=None,
177
+ help='Output parquet directory; defaults to parquet_*_dino_global')
178
+ parser.add_argument('--overwrite', action='store_true', help='Overwrite existing output parquet if present')
179
+ parser.add_argument('--strict', action='store_true',
180
+ help='If set, missing UIDs in global DINO will set dino=None and log error')
181
+ parser.add_argument('--start_from', type=int, default=0,
182
+ help='Index to start processing from (for resuming)')
183
+
184
+ args = parser.parse_args()
185
+
186
+ replacer = GlobalDinoReplacer(
187
+ base_dir=args.base_dir,
188
+ input_parquet_dir=args.input_parquet_dir,
189
+ global_dino_dir=args.global_dino_dir,
190
+ output_parquet_dir=args.output_parquet_dir,
191
+ )
192
+
193
+ outputs = replacer.process_all(overwrite=args.overwrite, strict=args.strict, start_from=args.start_from)
194
+ if outputs:
195
+ logger.info("Outputs written:")
196
+ for p in outputs:
197
+ logger.info(f" - {p}")
198
+ else:
199
+ logger.error("No outputs were created")
200
+
201
+
202
+ if __name__ == '__main__':
203
+ main()
204
+
205
+
data/any2any_preprocess/generate_parquet.py ADDED
@@ -0,0 +1,229 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import tarfile
3
+ import pandas as pd
4
+ import numpy as np
5
+ from PIL import Image
6
+ import io
7
+ import glob
8
+ import json
9
+ from tqdm import tqdm
10
+ import logging
11
+
12
+ # Set up logging
13
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
14
+ logger = logging.getLogger(__name__)
15
+
16
+ class TarProcessor:
17
+ def __init__(self, base_dir):
18
+ self.base_dir = base_dir
19
+ self.rgb_caption_dir = os.path.join(base_dir, 'datasets')
20
+ self.depth_dir = os.path.join(base_dir, 'datasets_depth')
21
+ self.normal_dir = os.path.join(base_dir, 'datasets_normal')
22
+ self.det_seg_dir = os.path.join(base_dir, 'datasets_seg_swinb')
23
+ self.parquet_dir = os.path.join(base_dir, 'parquet_rgb_caption_depth_normal_det_seg')
24
+
25
+ # Create parquet directory if it doesn't exist
26
+ os.makedirs(self.parquet_dir, exist_ok=True)
27
+
28
+ def get_tar_files(self):
29
+ """Get all tar files from rgb_caption directory"""
30
+ tar_files = glob.glob(os.path.join(self.rgb_caption_dir, '*.tar'))
31
+ return sorted(tar_files)
32
+
33
+ def extract_from_tar(self, tar_path, file_type):
34
+ """
35
+ Extract images and captions from tar file
36
+
37
+ Args:
38
+ tar_path: Path to tar file
39
+ file_type: 'rgb_caption', 'depth', 'normal', 'det_seg'
40
+ """
41
+ data = {}
42
+
43
+ try:
44
+ with tarfile.open(tar_path, 'r') as tar:
45
+ for member in tar.getmembers():
46
+ if member.isfile():
47
+ filename = member.name
48
+ uid = os.path.splitext(filename)[0] # Remove extension
49
+
50
+ # Extract file content
51
+ f = tar.extractfile(member)
52
+ if f is None:
53
+ continue
54
+
55
+ content = f.read()
56
+
57
+ if file_type == 'rgb_caption':
58
+ # Handle RGB + caption files
59
+ if filename.endswith('.jpg') or filename.endswith('.jpeg') or filename.endswith('.png'):
60
+ data[uid] = {'image': content, 'caption': None}
61
+ elif filename.endswith('.txt'):
62
+ # Find corresponding image
63
+ base_name = os.path.splitext(filename)[0]
64
+ if base_name in data:
65
+ data[base_name]['caption'] = content.decode('utf-8').strip()
66
+ else:
67
+ data[base_name] = {'image': None, 'caption': content.decode('utf-8').strip()}
68
+
69
+ elif file_type == 'depth':
70
+ # Handle depth maps (PNG format)
71
+ if filename.endswith('.png'):
72
+ data[uid] = content
73
+
74
+ elif file_type == 'normal':
75
+ # Handle normal maps (RGB format)
76
+ if filename.endswith('.jpg') or filename.endswith('.jpeg') or filename.endswith('.png'):
77
+ data[uid] = content
78
+ elif file_type == 'det_seg':
79
+ # Handle detection maps (JSON format)
80
+ if filename.endswith('.json'):
81
+ # Decode bytes to string and parse JSON
82
+ json_str = content.decode('utf-8')
83
+ json_data = json.loads(json_str)
84
+ data[uid] = json_data
85
+ except Exception as e:
86
+ logger.error(f"Error processing {tar_path}: {e}")
87
+ return {}
88
+
89
+ return data
90
+
91
+ def process_tar_file(self, rgb_caption_tar):
92
+ """Process a single tar file and create corresponding parquet"""
93
+
94
+ # Extract base name (e.g., 'sa_000000' from 'sa_000000.tar')
95
+ base_name = os.path.splitext(os.path.basename(rgb_caption_tar))[0]
96
+
97
+ # Construct paths for corresponding depth and normal tar files
98
+ depth_tar = os.path.join(self.depth_dir, f"{base_name}.tar")
99
+ normal_tar = os.path.join(self.normal_dir, f"{base_name}.tar")
100
+ det_seg_tar = os.path.join(self.det_seg_dir, f"{base_name}.tar")
101
+
102
+ logger.info(f"Processing {base_name}")
103
+
104
+ # Check if corresponding files exist
105
+ if not os.path.exists(depth_tar):
106
+ logger.warning(f"Depth tar file not found: {depth_tar}")
107
+ return None
108
+ if not os.path.exists(normal_tar):
109
+ logger.warning(f"Normal tar file not found: {normal_tar}")
110
+ return None
111
+ if not os.path.exists(det_seg_tar):
112
+ logger.warning(f"Det seg tar file not found: {det_seg_tar}")
113
+ return None
114
+
115
+ # Extract data from all three tar files
116
+ logger.info(f"Extracting from rgb_caption: {rgb_caption_tar}")
117
+ rgb_caption_data = self.extract_from_tar(rgb_caption_tar, 'rgb_caption')
118
+
119
+ logger.info(f"Extracting from depth: {depth_tar}")
120
+ depth_data = self.extract_from_tar(depth_tar, 'depth')
121
+
122
+ logger.info(f"Extracting from normal: {normal_tar}")
123
+ normal_data = self.extract_from_tar(normal_tar, 'normal')
124
+
125
+ logger.info(f"Extracting from det_seg: {det_seg_tar}")
126
+ det_seg_data = self.extract_from_tar(det_seg_tar, 'det_seg')
127
+
128
+ # Find common UIDs
129
+ logger.info(f"rgb_caption_data.keys() - det_seg_data.keys(): {len(set(rgb_caption_data.keys())) - len(set(det_seg_data.keys()))}")
130
+ common_uids = set(rgb_caption_data.keys()) & set(depth_data.keys()) & set(normal_data.keys()) & set(det_seg_data.keys())
131
+
132
+ if not common_uids:
133
+ logger.warning(f"No common UIDs found for {base_name}")
134
+ return None
135
+
136
+ logger.info(f"Found {len(common_uids)} common UIDs")
137
+
138
+ # Sort UIDs for consistent ordering
139
+ sorted_uids = sorted(common_uids)
140
+ logger.info(f"Sorted {len(sorted_uids)} UIDs")
141
+
142
+ # Create DataFrame
143
+ rows = []
144
+ for uid in tqdm(sorted_uids, desc=f"Processing {base_name}"):
145
+ rgb_info = rgb_caption_data[uid]
146
+
147
+ # Skip if missing image or caption
148
+ if rgb_info['image'] is None or rgb_info['caption'] is None:
149
+ continue
150
+
151
+ row = {
152
+ 'uid': uid,
153
+ 'rgb': rgb_info['image'],
154
+ 'caption': rgb_info['caption'],
155
+ 'depth': depth_data[uid],
156
+ 'normal': normal_data[uid],
157
+ 'det_seg': det_seg_data[uid]
158
+ }
159
+ rows.append(row)
160
+
161
+ if not rows:
162
+ logger.warning(f"No valid rows for {base_name}")
163
+ return None
164
+
165
+ # Create DataFrame
166
+ df = pd.DataFrame(rows)
167
+
168
+ # Debug information
169
+ logger.info(f"DataFrame shape: {df.shape}")
170
+ if not df.empty:
171
+ logger.info(f"Sample UIDs: {df['uid'].head().tolist()}")
172
+ logger.info(f"Data types: {df.dtypes.to_dict()}")
173
+
174
+ # Save to parquet with better settings for dataset compatibility
175
+ output_path = os.path.join(self.parquet_dir, f"{base_name}.parquet")
176
+
177
+ # Use better parquet settings for dataset compatibility
178
+ df.to_parquet(
179
+ output_path,
180
+ index=False,
181
+ engine='pyarrow',
182
+ compression='snappy',
183
+ row_group_size=1000 # Smaller row group size for more groups
184
+ )
185
+ logger.info(f"Saved {len(df)} rows to {output_path}")
186
+
187
+ return output_path
188
+
189
+ def process_all_tars(self):
190
+ """Process all tar files and create corresponding parquet files"""
191
+ tar_files = self.get_tar_files()
192
+
193
+ if not tar_files:
194
+ logger.error("No tar files found in rgb_caption directory")
195
+ return
196
+
197
+ logger.info(f"Found {len(tar_files)} tar files to process")
198
+
199
+ processed_files = []
200
+ for tar_file in tar_files:
201
+ try:
202
+ output_path = self.process_tar_file(tar_file)
203
+ if output_path:
204
+ processed_files.append(output_path)
205
+ except Exception as e:
206
+ logger.error(f"Error processing {tar_file}: {e}")
207
+
208
+ logger.info(f"Successfully processed {len(processed_files)} files")
209
+ return processed_files
210
+
211
+ def main():
212
+ # Set the base directory
213
+ base_dir = './datasets/blip3o'
214
+
215
+ # Create processor
216
+ processor = TarProcessor(base_dir)
217
+
218
+ # Process all tar files
219
+ processed_files = processor.process_all_tars()
220
+
221
+ if processed_files:
222
+ logger.info(f"Successfully created {len(processed_files)} parquet files:")
223
+ for file in processed_files:
224
+ logger.info(f" - {file}")
225
+ else:
226
+ logger.error("No parquet files were created")
227
+
228
+ if __name__ == "__main__":
229
+ main()
data/any2any_preprocess/generate_parquet_clip448_imagebind.py ADDED
@@ -0,0 +1,318 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import io
3
+ import glob
4
+ import tarfile
5
+ import logging
6
+ from typing import Dict, List, Optional, Tuple
7
+
8
+ import numpy as np
9
+ import pandas as pd
10
+ from tqdm import tqdm
11
+
12
+
13
+ # Set up logging
14
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class MultiModalEnricher:
19
+ """Read existing parquet files and add clip448 and imagebind columns from tar archives."""
20
+
21
+ def __init__(self, base_dir: str,
22
+ input_parquet_dir: Optional[str] = None,
23
+ clip448_dir: Optional[str] = None,
24
+ imagebind_dir: Optional[str] = None,
25
+ output_parquet_dir: Optional[str] = None) -> None:
26
+ self.base_dir = base_dir
27
+
28
+ # Defaults
29
+ self.input_parquet_dir = input_parquet_dir or os.path.join(
30
+ base_dir, 'parquet_rgb_caption_depth_normal_det_seg_grounding_canny_dino_global'
31
+ )
32
+ self.clip448_dir = clip448_dir or os.path.join(base_dir, 'datasets_clip448')
33
+ self.imagebind_dir = imagebind_dir or os.path.join(base_dir, 'datasets_imagebind')
34
+ self.output_parquet_dir = output_parquet_dir or os.path.join(
35
+ base_dir, 'parquet_rgb_caption_depth_normal_det_seg_grounding_canny_dino_global_clip448_imagebind'
36
+ )
37
+
38
+ os.makedirs(self.output_parquet_dir, exist_ok=True)
39
+
40
+ # ------------------------------------------------------------------
41
+ # Listing helpers
42
+ # ------------------------------------------------------------------
43
+
44
+ def _list_input_parquets(self) -> List[str]:
45
+ files = glob.glob(os.path.join(self.input_parquet_dir, '*.parquet'))
46
+ return sorted(files)
47
+
48
+ # ------------------------------------------------------------------
49
+ # Tar loaders
50
+ # ------------------------------------------------------------------
51
+
52
+ def _load_clip448_from_tar(self, tar_path: str) -> Dict[str, np.ndarray]:
53
+ """Load uid -> flattened local tokens from a clip448 tar.
54
+
55
+ Files inside: {uid}_tokens.npy (shape (28,28) int64)
56
+ """
57
+ results: Dict[str, np.ndarray] = {}
58
+
59
+ if not os.path.exists(tar_path):
60
+ logger.warning(f"CLIP448 tar not found: {tar_path}")
61
+ return results
62
+
63
+ try:
64
+ with tarfile.open(tar_path, 'r') as tar:
65
+ for member in tar.getmembers():
66
+ if not member.isfile():
67
+ continue
68
+ name = member.name
69
+ if not name.endswith('_tokens.npy'):
70
+ continue
71
+
72
+ file_obj = tar.extractfile(member)
73
+ if file_obj is None:
74
+ continue
75
+
76
+ content = file_obj.read()
77
+ npy = np.load(io.BytesIO(content))
78
+
79
+ # Derive uid: strip .npy then _tokens
80
+ uid = os.path.splitext(name)[0] # remove .npy
81
+ if uid.endswith('_tokens'):
82
+ uid = uid[:-len('_tokens')]
83
+
84
+ results[uid] = npy.flatten()
85
+ except Exception as e:
86
+ logger.error(f"Failed to read CLIP448 tar {tar_path}: {e}")
87
+
88
+ return results
89
+
90
+ def _load_imagebind_from_tar(self, tar_path: str) -> Tuple[Dict[str, np.ndarray], Dict[str, np.ndarray]]:
91
+ """Load uid -> flattened tokens from an imagebind tar.
92
+
93
+ Files inside:
94
+ {uid}_tokens.npy (local, shape (32,32) int64)
95
+ {uid}_tokens_global.npy (global, shape (16,1,1) int64)
96
+
97
+ Returns (local_map, global_map).
98
+ """
99
+ local_results: Dict[str, np.ndarray] = {}
100
+ global_results: Dict[str, np.ndarray] = {}
101
+
102
+ if not os.path.exists(tar_path):
103
+ logger.warning(f"ImageBind tar not found: {tar_path}")
104
+ return local_results, global_results
105
+
106
+ try:
107
+ with tarfile.open(tar_path, 'r') as tar:
108
+ for member in tar.getmembers():
109
+ if not member.isfile():
110
+ continue
111
+ name = member.name
112
+ if not name.endswith('.npy'):
113
+ continue
114
+
115
+ file_obj = tar.extractfile(member)
116
+ if file_obj is None:
117
+ continue
118
+
119
+ content = file_obj.read()
120
+ npy = np.load(io.BytesIO(content))
121
+
122
+ base = os.path.splitext(name)[0] # remove .npy
123
+
124
+ if base.endswith('_tokens_global'):
125
+ uid = base[:-len('_tokens_global')]
126
+ global_results[uid] = npy.flatten()
127
+ elif base.endswith('_tokens'):
128
+ uid = base[:-len('_tokens')]
129
+ local_results[uid] = npy.flatten()
130
+ except Exception as e:
131
+ logger.error(f"Failed to read ImageBind tar {tar_path}: {e}")
132
+
133
+ return local_results, global_results
134
+
135
+ # ------------------------------------------------------------------
136
+ # Column addition
137
+ # ------------------------------------------------------------------
138
+
139
+ def _add_columns(self, df: pd.DataFrame,
140
+ clip448_map: Dict[str, np.ndarray],
141
+ ib_local_map: Dict[str, np.ndarray],
142
+ ib_global_map: Dict[str, np.ndarray],
143
+ strict: bool = False) -> pd.DataFrame:
144
+ """Add clip448, imagebind, and imagebind_global columns to *df*."""
145
+
146
+ if 'uid' not in df.columns:
147
+ raise ValueError("Input parquet missing required 'uid' column")
148
+
149
+ num_rows = len(df)
150
+ stats = {'clip448': 0, 'imagebind': 0, 'imagebind_global': 0,
151
+ 'clip448_miss': 0, 'imagebind_miss': 0, 'imagebind_global_miss': 0}
152
+
153
+ clip448_col: List[object] = []
154
+ ib_local_col: List[object] = []
155
+ ib_global_col: List[object] = []
156
+
157
+ for _, row in df.iterrows():
158
+ uid = row['uid']
159
+
160
+ # clip448
161
+ tokens = clip448_map.get(uid)
162
+ if tokens is None:
163
+ stats['clip448_miss'] += 1
164
+ clip448_col.append(row.get('clip448', None) if not strict else None)
165
+ else:
166
+ clip448_col.append(tokens)
167
+ stats['clip448'] += 1
168
+
169
+ # imagebind local
170
+ tokens = ib_local_map.get(uid)
171
+ if tokens is None:
172
+ stats['imagebind_miss'] += 1
173
+ ib_local_col.append(row.get('imagebind', None) if not strict else None)
174
+ else:
175
+ ib_local_col.append(tokens)
176
+ stats['imagebind'] += 1
177
+
178
+ # imagebind global
179
+ tokens = ib_global_map.get(uid)
180
+ if tokens is None:
181
+ stats['imagebind_global_miss'] += 1
182
+ ib_global_col.append(row.get('imagebind_global', None) if not strict else None)
183
+ else:
184
+ ib_global_col.append(tokens)
185
+ stats['imagebind_global'] += 1
186
+
187
+ logger.info(
188
+ f"Rows: {num_rows} | "
189
+ f"clip448: {stats['clip448']} ok / {stats['clip448_miss']} miss | "
190
+ f"imagebind: {stats['imagebind']} ok / {stats['imagebind_miss']} miss | "
191
+ f"imagebind_global: {stats['imagebind_global']} ok / {stats['imagebind_global_miss']} miss"
192
+ )
193
+
194
+ if strict and (stats['clip448_miss'] or stats['imagebind_miss'] or stats['imagebind_global_miss']):
195
+ logger.error("Strict mode: some UIDs are missing tokens from one or more modalities")
196
+
197
+ df = df.copy()
198
+ df['clip448'] = clip448_col
199
+ df['imagebind'] = ib_local_col
200
+ df['imagebind_global'] = ib_global_col
201
+ return df
202
+
203
+ # ------------------------------------------------------------------
204
+ # Processing
205
+ # ------------------------------------------------------------------
206
+
207
+ def process_one_parquet(self, parquet_path: str, overwrite: bool = False,
208
+ strict: bool = False) -> Optional[str]:
209
+ base_name = os.path.splitext(os.path.basename(parquet_path))[0]
210
+ out_path = os.path.join(self.output_parquet_dir, f"{base_name}.parquet")
211
+
212
+ if os.path.exists(out_path) and not overwrite:
213
+ logger.info(f"Output exists, skipping: {out_path}")
214
+ return out_path
215
+
216
+ # Load existing parquet
217
+ df = pd.read_parquet(parquet_path, engine='pyarrow')
218
+
219
+ # Load clip448 tar
220
+ clip448_tar = os.path.join(self.clip448_dir, f"{base_name}.tar")
221
+ logger.info(f"Loading CLIP448 tokens from: {clip448_tar}")
222
+ clip448_map = self._load_clip448_from_tar(clip448_tar)
223
+
224
+ # Load imagebind tar
225
+ imagebind_tar = os.path.join(self.imagebind_dir, f"{base_name}.tar")
226
+ logger.info(f"Loading ImageBind tokens from: {imagebind_tar}")
227
+ ib_local_map, ib_global_map = self._load_imagebind_from_tar(imagebind_tar)
228
+
229
+ if not clip448_map:
230
+ logger.warning(f"No CLIP448 tokens loaded from {clip448_tar}")
231
+ if not ib_local_map:
232
+ logger.warning(f"No ImageBind local tokens loaded from {imagebind_tar}")
233
+ if not ib_global_map:
234
+ logger.warning(f"No ImageBind global tokens loaded from {imagebind_tar}")
235
+
236
+ # Add new columns
237
+ df = self._add_columns(df, clip448_map, ib_local_map, ib_global_map, strict=strict)
238
+
239
+ # Persist
240
+ df.to_parquet(
241
+ out_path,
242
+ index=False,
243
+ engine='pyarrow',
244
+ compression='snappy',
245
+ row_group_size=1000,
246
+ )
247
+ logger.info(f"Wrote enriched parquet to: {out_path}")
248
+ return out_path
249
+
250
+ def process_all(self, overwrite: bool = False, strict: bool = False,
251
+ start_from: int = 0) -> List[str]:
252
+ inputs = self._list_input_parquets()
253
+ if start_from > 0:
254
+ inputs = inputs[start_from:]
255
+
256
+ if not inputs:
257
+ logger.error("No input parquet files found")
258
+ return []
259
+
260
+ logger.info(f"Found {len(inputs)} parquet files to enrich")
261
+ outputs: List[str] = []
262
+ for parquet_path in tqdm(inputs, desc="Enriching parquets", unit="file"):
263
+ try:
264
+ result = self.process_one_parquet(parquet_path, overwrite=overwrite, strict=strict)
265
+ if result:
266
+ outputs.append(result)
267
+ except Exception as e:
268
+ logger.error(f"Error processing {parquet_path}: {e}")
269
+
270
+ logger.info(f"Successfully wrote {len(outputs)} enriched parquet files")
271
+ return outputs
272
+
273
+
274
+ def main() -> None:
275
+ import argparse
276
+
277
+ parser = argparse.ArgumentParser(
278
+ description='Add CLIP448 and ImageBind tokens to existing parquet files'
279
+ )
280
+ parser.add_argument('--base_dir', type=str, default='./datasets/blip3o',
281
+ help='Base directory (default: ./datasets/blip3o)')
282
+ parser.add_argument('--input_parquet_dir', type=str, default=None,
283
+ help='Input parquet directory; defaults to parquet_*_dino_global')
284
+ parser.add_argument('--clip448_dir', type=str, default=None,
285
+ help='Directory containing clip448 tars; defaults to base_dir/datasets_clip448')
286
+ parser.add_argument('--imagebind_dir', type=str, default=None,
287
+ help='Directory containing imagebind tars; defaults to base_dir/datasets_imagebind')
288
+ parser.add_argument('--output_parquet_dir', type=str, default=None,
289
+ help='Output parquet directory; defaults to parquet_*_clip448_imagebind')
290
+ parser.add_argument('--overwrite', action='store_true',
291
+ help='Overwrite existing output parquet if present')
292
+ parser.add_argument('--strict', action='store_true',
293
+ help='If set, missing UIDs will set column=None and log error')
294
+ parser.add_argument('--start_from', type=int, default=0,
295
+ help='Index to start processing from (for resuming)')
296
+
297
+ args = parser.parse_args()
298
+
299
+ enricher = MultiModalEnricher(
300
+ base_dir=args.base_dir,
301
+ input_parquet_dir=args.input_parquet_dir,
302
+ clip448_dir=args.clip448_dir,
303
+ imagebind_dir=args.imagebind_dir,
304
+ output_parquet_dir=args.output_parquet_dir,
305
+ )
306
+
307
+ outputs = enricher.process_all(overwrite=args.overwrite, strict=args.strict, start_from=args.start_from)
308
+ if outputs:
309
+ logger.info("Outputs written:")
310
+ for p in outputs:
311
+ logger.info(f" - {p}")
312
+ else:
313
+ logger.error("No outputs were created")
314
+
315
+
316
+ if __name__ == '__main__':
317
+ main()
318
+
data/any2any_preprocess/generate_parquet_grounding.py ADDED
@@ -0,0 +1,583 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import tarfile
3
+ import pandas as pd
4
+ import numpy as np
5
+ from PIL import Image
6
+ import io
7
+ import glob
8
+ import json
9
+ from tqdm import tqdm
10
+ import logging
11
+ import pycocotools.mask as mask_util
12
+ import matplotlib.pyplot as plt
13
+
14
+ # Set up logging
15
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
16
+ logger = logging.getLogger(__name__)
17
+
18
+ class GroundingProcessor:
19
+ def __init__(self, base_dir):
20
+ self.base_dir = base_dir
21
+ self.rgb_caption_dir = os.path.join(base_dir, 'datasets')
22
+ self.depth_dir = os.path.join(base_dir, 'datasets_depth')
23
+ self.normal_dir = os.path.join(base_dir, 'datasets_normal')
24
+ self.det_seg_dir = os.path.join(base_dir, 'datasets_seg_swinb')
25
+ self.grounding_dir = os.path.join(base_dir, 'datasets_grounding') # Pre-processed grounding tars
26
+ self.parquet_dir = os.path.join(base_dir, 'parquet_rgb_caption_depth_normal_det_seg_grounding')
27
+
28
+ # Create parquet directory if it doesn't exist
29
+ os.makedirs(self.parquet_dir, exist_ok=True)
30
+
31
+ def get_tar_files(self):
32
+ """Get all tar files from rgb_caption directory"""
33
+ tar_files = glob.glob(os.path.join(self.rgb_caption_dir, '*.tar'))
34
+ return sorted(tar_files)
35
+
36
+
37
+ def extract_from_tar(self, tar_path, file_type):
38
+ """
39
+ Extract data from tar file
40
+
41
+ Args:
42
+ tar_path: Path to tar file
43
+ file_type: 'rgb_caption', 'depth', 'normal', 'det_seg', 'grounding'
44
+ """
45
+ data = {}
46
+
47
+ try:
48
+ with tarfile.open(tar_path, 'r') as tar:
49
+ for member in tar.getmembers():
50
+ if member.isfile():
51
+ filename = member.name
52
+ uid = os.path.splitext(filename)[0] # Remove extension
53
+
54
+ # Extract file content
55
+ f = tar.extractfile(member)
56
+ if f is None:
57
+ continue
58
+
59
+ content = f.read()
60
+
61
+ if file_type == 'rgb_caption':
62
+ # Handle RGB + caption files
63
+ if filename.endswith('.jpg') or filename.endswith('.jpeg') or filename.endswith('.png'):
64
+ data[uid] = {'image': content, 'caption': None}
65
+ elif filename.endswith('.txt'):
66
+ # Find corresponding image
67
+ base_name = os.path.splitext(filename)[0]
68
+ if base_name in data:
69
+ data[base_name]['caption'] = content.decode('utf-8').strip()
70
+ else:
71
+ data[base_name] = {'image': None, 'caption': content.decode('utf-8').strip()}
72
+
73
+ elif file_type == 'depth':
74
+ # Handle depth maps (PNG format)
75
+ if filename.endswith('.png'):
76
+ data[uid] = content
77
+
78
+ elif file_type == 'normal':
79
+ # Handle normal maps (RGB format)
80
+ if filename.endswith('.jpg') or filename.endswith('.jpeg') or filename.endswith('.png'):
81
+ data[uid] = content
82
+
83
+ elif file_type == 'det_seg':
84
+ # Handle detection maps (JSON format)
85
+ if filename.endswith('.json'):
86
+ # Decode bytes to string and parse JSON
87
+ json_str = content.decode('utf-8')
88
+ json_data = json.loads(json_str)
89
+ data[uid] = json_data
90
+
91
+ elif file_type == 'grounding':
92
+ # Handle grounding JSON files
93
+ if filename.endswith('.json'):
94
+ # Decode bytes to string and parse JSON
95
+ json_str = content.decode('utf-8')
96
+ json_data = json.loads(json_str)
97
+ data[uid] = json_data
98
+ except Exception as e:
99
+ logger.error(f"Error processing {tar_path}: {e}")
100
+ return {}
101
+
102
+ return data
103
+
104
+
105
+ def extract_grounding_data(self, grounding_data):
106
+ """Extract grounding information from JSON data"""
107
+ if not grounding_data:
108
+ return None
109
+
110
+ # Get the first (and usually only) key which is the image name
111
+ image_key = list(grounding_data.keys())[0]
112
+ image_data = grounding_data[image_key]
113
+
114
+ # Extract objects and their bounding boxes
115
+ objects = image_data.get('objects', [])
116
+ floating_objects = image_data.get('floating_objects', [])
117
+
118
+ # Extract grounding relationships
119
+ relationships = image_data.get('relationships', {})
120
+ grounding_phrases = relationships.get('grounding', [])
121
+
122
+ # Extract captions with grounding details
123
+ short_captions = image_data.get('short_captions', [])
124
+ dense_caption = image_data.get('dense_caption', {})
125
+
126
+ return {
127
+ 'objects': objects,
128
+ 'floating_objects': floating_objects,
129
+ 'grounding_phrases': grounding_phrases,
130
+ 'short_captions': short_captions,
131
+ 'dense_caption': dense_caption
132
+ }
133
+
134
+ def normalize_bbox(self, bbox, image_size):
135
+ """Normalize bbox to 0-999 based on image dimensions"""
136
+ if not bbox or len(bbox) != 4:
137
+ return []
138
+
139
+ # RLE format: size = [width, height]
140
+ img_height, img_width = image_size
141
+
142
+ x1, y1, x2, y2 = bbox
143
+
144
+ # Normalize to 0-999 and clamp values
145
+ norm_x1 = max(0, min(999, int((x1 / img_width) * 999)))
146
+ norm_y1 = max(0, min(999, int((y1 / img_height) * 999)))
147
+ norm_x2 = max(0, min(999, int((x2 / img_width) * 999)))
148
+ norm_y2 = max(0, min(999, int((y2 / img_height) * 999)))
149
+
150
+ return [norm_x1, norm_y1, norm_x2, norm_y2]
151
+
152
+
153
+ def resize_and_crop_bbox(self, bbox, orig_size, target_size=512):
154
+ """
155
+ Resize and center-crop a bounding box using CLIP/BLIP-style transform.
156
+
157
+ Args:
158
+ bbox: list or tuple [x1, y1, x2, y2] in pixel coords.
159
+ orig_size: [W, H] original image size.
160
+ target_size: int, final square crop size (default=512).
161
+
162
+ Returns:
163
+ [x1', y1', x2', y2'] after resize+crop.
164
+ Also returns (scale, left, top) for debugging.
165
+ """
166
+ if not bbox or len(bbox) != 4:
167
+ return []
168
+
169
+ H, W = orig_size
170
+ x1, y1, x2, y2 = bbox
171
+
172
+ # --- 1. Resize (keep aspect ratio, short side -> target_size) ---
173
+ scale = target_size / min(W, H)
174
+ new_W, new_H = W * scale, H * scale
175
+
176
+ # --- 2. Center crop to target_sizeΓ—target_size ---
177
+ left = max(0, (new_W - target_size) / 2)
178
+ top = max(0, (new_H - target_size) / 2)
179
+
180
+ # --- 3. Apply transform to bbox ---
181
+ x1_new = x1 * scale - left
182
+ y1_new = y1 * scale - top
183
+ x2_new = x2 * scale - left
184
+ y2_new = y2 * scale - top
185
+
186
+ # Clip to [0, target_size]
187
+ x1_new, y1_new, x2_new, y2_new = np.clip(
188
+ [x1_new, y1_new, x2_new, y2_new],
189
+ 0, target_size
190
+ )
191
+
192
+ return [x1_new, y1_new, x2_new, y2_new]
193
+
194
+
195
+ def visualize_rle_mask(self, rle_data, save_path=None):
196
+ """Visualize RLE mask from segmentation data"""
197
+ try:
198
+ # Decode RLE to binary mask
199
+ mask = mask_util.decode(rle_data)
200
+
201
+ # Create visualization
202
+ fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 6))
203
+
204
+ # Show mask
205
+ ax1.imshow(mask, cmap='gray')
206
+ ax1.set_title('RLE Mask')
207
+ ax1.axis('off')
208
+
209
+ # Show mask with bbox overlay
210
+ ax2.imshow(mask, cmap='gray', alpha=0.7)
211
+
212
+ # Add bbox if available
213
+ if 'bbox' in rle_data:
214
+ bbox = rle_data['bbox']
215
+ x1, y1, x2, y2 = bbox
216
+ width = x2 - x1
217
+ height = y2 - y1
218
+ rect = plt.Rectangle((x1, y1), width, height,
219
+ fill=False, edgecolor='red', linewidth=2)
220
+ ax2.add_patch(rect)
221
+ ax2.set_title('Mask + Bbox')
222
+ else:
223
+ ax2.set_title('Mask Only')
224
+ ax2.axis('off')
225
+
226
+ plt.tight_layout()
227
+
228
+ if save_path:
229
+ plt.savefig(save_path, dpi=150, bbox_inches='tight')
230
+ logger.info(f"Mask visualization saved to {save_path}")
231
+ else:
232
+ plt.show()
233
+
234
+ plt.close()
235
+
236
+ except Exception as e:
237
+ logger.error(f"Error visualizing RLE mask: {e}")
238
+
239
+ def visualize_object_mask(self, grounding_data, uid, object_id=0, save_path=None):
240
+ """Visualize a specific object's mask from grounding data"""
241
+ try:
242
+ if not grounding_data or 'objects' not in grounding_data:
243
+ logger.warning(f"No objects found for {uid}")
244
+ return
245
+
246
+ objects = grounding_data['objects']
247
+ if object_id >= len(objects):
248
+ logger.warning(f"Object ID {object_id} not found for {uid}")
249
+ return
250
+
251
+ obj = objects[object_id]
252
+ if 'segmentation' not in obj:
253
+ logger.warning(f"No segmentation found for object {object_id} in {uid}")
254
+ return
255
+
256
+ segmentation = obj['segmentation']
257
+ bbox = obj.get('bbox', [])
258
+
259
+ # Create RLE data for visualization
260
+ rle_data = {
261
+ 'size': segmentation['size'],
262
+ 'counts': segmentation['counts'],
263
+ 'bbox': bbox
264
+ }
265
+
266
+ # Visualize
267
+ if save_path:
268
+ save_path_with_id = save_path.replace('.png', f'_obj_{object_id}.png')
269
+ else:
270
+ save_path_with_id = None
271
+
272
+ self.visualize_rle_mask(rle_data, save_path_with_id)
273
+
274
+ except Exception as e:
275
+ logger.error(f"Error visualizing object mask for {uid}: {e}")
276
+
277
+ def create_grounding_samples(self, grounding_data, uid):
278
+ """Create training samples from grounding data"""
279
+ if not grounding_data:
280
+ return []
281
+
282
+ samples = []
283
+
284
+ # Get image size from first object's segmentation
285
+ image_size = None
286
+ if grounding_data['objects'] and 'segmentation' in grounding_data['objects'][0]:
287
+ image_size = grounding_data['objects'][0]['segmentation']['size']
288
+ assert image_size is not None
289
+ else:
290
+ logger.warning(f"No segmentation data found for {uid}, skipping bbox processing")
291
+ return []
292
+
293
+ # Process grounding phrases
294
+ for phrase_info in grounding_data['grounding_phrases']:
295
+ phrase = phrase_info['phrase']
296
+ object_ids = phrase_info['object_ids']
297
+
298
+ # Find corresponding objects and their bboxes
299
+ for obj_id in object_ids:
300
+ for obj in grounding_data['objects']:
301
+ if obj.get('id') == obj_id:
302
+ bbox = obj.get('bbox', [])
303
+
304
+ # Process bbox if valid
305
+ if bbox and len(bbox) == 4:
306
+ bbox = self.resize_and_crop_bbox(bbox, image_size, 512)
307
+ bbox = self.normalize_bbox(bbox, [512, 512])
308
+ else:
309
+ bbox = []
310
+
311
+ sample = {
312
+ 'uid': uid,
313
+ 'phrase': phrase,
314
+ 'object_id': obj_id,
315
+ 'bbox': bbox,
316
+ 'labels': obj.get('labels', []),
317
+ 'attributes': obj.get('attributes', []),
318
+ 'sample_type': 'grounding_phrase',
319
+ 'segmentation': obj.get('segmentation', []),
320
+ }
321
+ samples.append(sample)
322
+ break
323
+
324
+ # Process short captions with details
325
+ for caption_info in grounding_data['short_captions']:
326
+ caption = caption_info['caption']
327
+ for detail in caption_info.get('details', []):
328
+ phrase = detail['phrase']
329
+ object_id = detail.get('id')
330
+ bbox = detail.get('bbox', [])
331
+
332
+ # Process bbox if valid
333
+ if bbox and len(bbox) == 4:
334
+ bbox = self.resize_and_crop_bbox(bbox, image_size, 512)
335
+ bbox = self.normalize_bbox(bbox, [512, 512])
336
+ else:
337
+ bbox = []
338
+
339
+ tokens_positive = detail.get('tokens_positive', [])
340
+
341
+ sample = {
342
+ 'uid': uid,
343
+ 'phrase': phrase,
344
+ 'caption_id': object_id,
345
+ 'bbox': bbox,
346
+ 'tokens_positive': tokens_positive,
347
+ 'full_caption': caption,
348
+ 'sample_type': 'short_caption'
349
+ }
350
+ samples.append(sample)
351
+
352
+ # # Process dense caption
353
+ # if grounding_data['dense_caption']:
354
+ # dense_caption = grounding_data['dense_caption']
355
+ # caption = dense_caption.get('caption', '')
356
+ # for detail in dense_caption.get('details', []):
357
+ # phrase = detail['phrase']
358
+ # object_ids = detail.get('ids', [])
359
+ # bboxes = detail.get('bbox', [])
360
+ # tokens_positive = detail.get('tokens_positive', [])
361
+
362
+ # sample = {
363
+ # 'uid': uid,
364
+ # 'phrase': phrase,
365
+ # 'object_ids': object_ids,
366
+ # 'bboxes': bboxes,
367
+ # 'tokens_positive': tokens_positive,
368
+ # 'full_caption': caption,
369
+ # 'sample_type': 'dense_caption'
370
+ # }
371
+ # samples.append(sample)
372
+
373
+ return samples
374
+
375
+ def process_tar_file(self, rgb_caption_tar):
376
+ """Process a single tar file and create corresponding parquet"""
377
+
378
+ # Extract base name (e.g., 'sa_000000' from 'sa_000000.tar')
379
+ base_name = os.path.splitext(os.path.basename(rgb_caption_tar))[0]
380
+
381
+ # Construct paths for corresponding depth, normal, det_seg, grounding tar files
382
+ depth_tar = os.path.join(self.depth_dir, f"{base_name}.tar")
383
+ normal_tar = os.path.join(self.normal_dir, f"{base_name}.tar")
384
+ det_seg_tar = os.path.join(self.det_seg_dir, f"{base_name}.tar")
385
+ grounding_tar = os.path.join(self.grounding_dir, f"{base_name}.tar")
386
+
387
+ logger.info(f"Processing {base_name}")
388
+
389
+ # Check if corresponding files exist
390
+ if not os.path.exists(depth_tar):
391
+ logger.warning(f"Depth tar file not found: {depth_tar}")
392
+ return None
393
+ if not os.path.exists(normal_tar):
394
+ logger.warning(f"Normal tar file not found: {normal_tar}")
395
+ return None
396
+ if not os.path.exists(det_seg_tar):
397
+ logger.warning(f"Det seg tar file not found: {det_seg_tar}")
398
+ return None
399
+ # Initialize grounding_data
400
+ grounding_data = {}
401
+
402
+ if not os.path.exists(grounding_tar):
403
+ logger.warning(f"Grounding tar file not found: {grounding_tar} - will process without grounding data")
404
+ else:
405
+ logger.info(f"Extracting from grounding: {grounding_tar}")
406
+ grounding_data = self.extract_from_tar(grounding_tar, 'grounding')
407
+
408
+ # Extract data from all tar files
409
+ logger.info(f"Extracting from rgb_caption: {rgb_caption_tar}")
410
+ rgb_caption_data = self.extract_from_tar(rgb_caption_tar, 'rgb_caption')
411
+
412
+ logger.info(f"Extracting from depth: {depth_tar}")
413
+ depth_data = self.extract_from_tar(depth_tar, 'depth')
414
+
415
+ logger.info(f"Extracting from normal: {normal_tar}")
416
+ normal_data = self.extract_from_tar(normal_tar, 'normal')
417
+
418
+ logger.info(f"Extracting from det_seg: {det_seg_tar}")
419
+ det_seg_data = self.extract_from_tar(det_seg_tar, 'det_seg')
420
+
421
+ # Find common UIDs (grounding is optional)
422
+ common_uids = set(rgb_caption_data.keys()) & set(depth_data.keys()) & set(normal_data.keys()) & set(det_seg_data.keys())
423
+
424
+ if not common_uids:
425
+ logger.warning(f"No common UIDs found for {base_name}")
426
+ return None
427
+
428
+ logger.info(f"Found {len(common_uids)} common UIDs")
429
+
430
+ # Sort UIDs for consistent ordering
431
+ sorted_uids = sorted(common_uids)
432
+
433
+ # Create samples for each UID
434
+ all_samples = []
435
+
436
+ for uid in sorted_uids:
437
+ rgb_info = rgb_caption_data[uid]
438
+
439
+ # Skip if missing image or caption
440
+ if rgb_info['image'] is None or rgb_info['caption'] is None:
441
+ continue
442
+
443
+ # Create base sample with all modalities
444
+ sample = {
445
+ 'uid': uid,
446
+ 'rgb': rgb_caption_data[uid]['image'],
447
+ 'caption': rgb_caption_data[uid]['caption'],
448
+ 'depth': depth_data[uid],
449
+ 'normal': normal_data[uid],
450
+ 'det_seg': det_seg_data[uid]
451
+ }
452
+
453
+ # Add grounding data if available
454
+ if uid in grounding_data:
455
+ extracted_grounding = self.extract_grounding_data(grounding_data[uid])
456
+ if extracted_grounding:
457
+ raw_image = Image.open(io.BytesIO(rgb_caption_data[uid]['image']))
458
+ raw_image_size = raw_image.size
459
+ assert raw_image_size == (512, 512), f"Raw image size is {raw_image_size} but expected (512, 512) for {uid}"
460
+ grounding_samples = self.create_grounding_samples(extracted_grounding, uid)
461
+ if grounding_samples:
462
+ sample['grounding'] = grounding_samples
463
+
464
+ # visualize_segmentation = sample['grounding'][1]['segmentation']
465
+ # self.visualize_rle_mask(visualize_segmentation,save_path=f"visualize_{uid}_1.png")
466
+ all_samples.append(sample)
467
+
468
+ if not all_samples:
469
+ logger.warning(f"No samples found for {base_name}")
470
+ return None
471
+
472
+ # Create DataFrame
473
+ df = pd.DataFrame(all_samples)
474
+
475
+ # Debug information
476
+ logger.info(f"DataFrame shape: {df.shape}")
477
+ if not df.empty:
478
+ logger.info(f"Sample UIDs: {df['uid'].head().tolist()}")
479
+ # Count grounding samples
480
+ grounding_counts = []
481
+ for _, row in df.iterrows():
482
+ if 'grounding' in row and row['grounding'] is not None and isinstance(row['grounding'], list):
483
+ grounding_counts.append(len(row['grounding']))
484
+ else:
485
+ grounding_counts.append(0)
486
+ if grounding_counts:
487
+ logger.info(f"Grounding samples per row: {np.mean(grounding_counts):.2f} avg, {max(grounding_counts)} max")
488
+ else:
489
+ logger.info("No grounding samples found")
490
+
491
+ # Save to parquet
492
+ output_path = os.path.join(self.parquet_dir, f"{base_name}.parquet")
493
+
494
+ df.to_parquet(
495
+ output_path,
496
+ index=False,
497
+ engine='pyarrow',
498
+ compression='snappy',
499
+ row_group_size=1000
500
+ )
501
+ logger.info(f"Saved {len(df)} grounding samples to {output_path}")
502
+
503
+ return output_path
504
+
505
+ def process_all_tars(self):
506
+ """Process all tar files and create corresponding parquet files"""
507
+ tar_files = self.get_tar_files()
508
+
509
+ if not tar_files:
510
+ logger.error("No tar files found in rgb_caption directory")
511
+ return
512
+
513
+ logger.info(f"Found {len(tar_files)} tar files to process")
514
+
515
+ processed_files = []
516
+ for tar_file in tar_files:
517
+ try:
518
+ output_path = self.process_tar_file(tar_file)
519
+ if output_path:
520
+ processed_files.append(output_path)
521
+ except Exception as e:
522
+ logger.error(f"Error processing {tar_file}: {e}")
523
+
524
+ logger.info(f"Successfully processed {len(processed_files)} files")
525
+ return processed_files
526
+
527
+ def main():
528
+ import argparse
529
+
530
+ parser = argparse.ArgumentParser(description='Generate parquet files with grounding data')
531
+ parser.add_argument('--base_dir', type=str, default='./datasets/blip3o',
532
+ help='Base directory (default: ./datasets/blip3o)')
533
+ parser.add_argument('--visualize_mask', action='store_true',
534
+ help='Visualize RLE masks for debugging')
535
+ parser.add_argument('--visualize_tar', type=str, default=None,
536
+ help='Specific tar file to visualize (e.g., sa_000001.tar)')
537
+ parser.add_argument('--visualize_uid', type=str, default=None,
538
+ help='Specific UID to visualize (e.g., sa_1)')
539
+ parser.add_argument('--visualize_object_id', type=int, default=0,
540
+ help='Object ID to visualize (default: 0)')
541
+
542
+ args = parser.parse_args()
543
+
544
+ # Set the base directory
545
+ base_dir = args.base_dir
546
+
547
+ # Create processor
548
+ processor = GroundingProcessor(base_dir)
549
+
550
+ # Visualize mask if requested
551
+ if args.visualize_mask and args.visualize_tar:
552
+ logger.info(f"Visualizing mask for {args.visualize_tar}, object {args.visualize_object_id}")
553
+
554
+ # Find the grounding tar file for this UID
555
+ grounding_tar = os.path.join(processor.grounding_dir, f"{args.visualize_tar}.tar")
556
+ if os.path.exists(grounding_tar):
557
+ grounding_data = processor.extract_from_tar(grounding_tar, 'grounding')
558
+ if args.visualize_uid in grounding_data:
559
+ extracted_grounding = processor.extract_grounding_data(grounding_data[args.visualize_uid])
560
+ if extracted_grounding:
561
+ save_path = f"mask_visualization_{args.visualize_uid}.png"
562
+ processor.visualize_object_mask(extracted_grounding, args.visualize_uid,
563
+ args.visualize_object_id, save_path)
564
+ else:
565
+ logger.error(f"No grounding data found for {args.visualize_uid}")
566
+ else:
567
+ logger.error(f"UID {args.visualize_uid} not found in grounding tar")
568
+ else:
569
+ logger.error(f"Grounding tar not found: {grounding_tar}")
570
+ return
571
+
572
+ # Process all tar files
573
+ processed_files = processor.process_all_tars()
574
+
575
+ if processed_files:
576
+ logger.info(f"Successfully created {len(processed_files)} grounding parquet files:")
577
+ for file in processed_files:
578
+ logger.info(f" - {file}")
579
+ else:
580
+ logger.error("No grounding parquet files were created")
581
+
582
+ if __name__ == "__main__":
583
+ main()
data/any2any_preprocess/generate_parquet_grounding_canny_dino.py ADDED
@@ -0,0 +1,627 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import tarfile
3
+ import pandas as pd
4
+ import numpy as np
5
+ from PIL import Image
6
+ import io
7
+ import glob
8
+ import json
9
+ from tqdm import tqdm
10
+ import logging
11
+ import pycocotools.mask as mask_util
12
+ import matplotlib.pyplot as plt
13
+
14
+ # Set up logging
15
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
16
+ logger = logging.getLogger(__name__)
17
+
18
+ class GroundingProcessor:
19
+ def __init__(self, base_dir):
20
+ self.base_dir = base_dir
21
+ self.rgb_caption_dir = os.path.join(base_dir, 'datasets')
22
+ self.depth_dir = os.path.join(base_dir, 'datasets_depth')
23
+ self.normal_dir = os.path.join(base_dir, 'datasets_normal')
24
+ self.det_seg_dir = os.path.join(base_dir, 'datasets_seg_swinb')
25
+ self.grounding_dir = os.path.join(base_dir, 'datasets_grounding') # Pre-processed grounding tars
26
+ self.canny_dir = os.path.join(base_dir, 'datasets_cannyedge')
27
+ self.dino_dir = os.path.join(base_dir, 'dinov2')
28
+ self.parquet_dir = os.path.join(base_dir, 'parquet_rgb_caption_depth_normal_det_seg_grounding_canny_dino')
29
+
30
+ # Create parquet directory if it doesn't exist
31
+ os.makedirs(self.parquet_dir, exist_ok=True)
32
+
33
+ def get_tar_files(self):
34
+ """Get all tar files from rgb_caption directory"""
35
+ tar_files = glob.glob(os.path.join(self.rgb_caption_dir, '*.tar'))
36
+ return sorted(tar_files)
37
+
38
+
39
+ def extract_from_tar(self, tar_path, file_type):
40
+ """
41
+ Extract data from tar file
42
+
43
+ Args:
44
+ tar_path: Path to tar file
45
+ file_type: 'rgb_caption', 'depth', 'normal', 'det_seg', 'grounding', 'canny', 'dino'
46
+ """
47
+ data = {}
48
+
49
+ try:
50
+ with tarfile.open(tar_path, 'r') as tar:
51
+ for member in tar.getmembers():
52
+ if member.isfile():
53
+ filename = member.name
54
+ uid = os.path.splitext(filename)[0] # Remove extension
55
+
56
+ # Extract file content
57
+ f = tar.extractfile(member)
58
+ if f is None:
59
+ continue
60
+
61
+ content = f.read()
62
+
63
+ if file_type == 'rgb_caption':
64
+ # Handle RGB + caption files
65
+ if filename.endswith('.jpg') or filename.endswith('.jpeg') or filename.endswith('.png'):
66
+ data[uid] = {'image': content, 'caption': None}
67
+ elif filename.endswith('.txt'):
68
+ # Find corresponding image
69
+ base_name = os.path.splitext(filename)[0]
70
+ if base_name in data:
71
+ data[base_name]['caption'] = content.decode('utf-8').strip()
72
+ else:
73
+ data[base_name] = {'image': None, 'caption': content.decode('utf-8').strip()}
74
+
75
+ elif file_type == 'depth':
76
+ # Handle depth maps (PNG format)
77
+ if filename.endswith('.png'):
78
+ data[uid] = content
79
+
80
+ elif file_type == 'normal':
81
+ # Handle normal maps (RGB format)
82
+ if filename.endswith('.jpg') or filename.endswith('.jpeg') or filename.endswith('.png'):
83
+ data[uid] = content
84
+
85
+ elif file_type == 'det_seg':
86
+ # Handle detection maps (JSON format)
87
+ if filename.endswith('.json'):
88
+ # Decode bytes to string and parse JSON
89
+ json_str = content.decode('utf-8')
90
+ json_data = json.loads(json_str)
91
+ data[uid] = json_data
92
+
93
+ elif file_type == 'grounding':
94
+ # Handle grounding JSON files
95
+ if filename.endswith('.json'):
96
+ # Decode bytes to string and parse JSON
97
+ json_str = content.decode('utf-8')
98
+ json_data = json.loads(json_str)
99
+ data[uid] = json_data
100
+
101
+ elif file_type == 'canny':
102
+ # Handle Canny maps (PNG format, similar to depth)
103
+ if filename.endswith('.png'):
104
+ data[uid] = content
105
+
106
+ elif file_type == 'dino':
107
+ # Handle DINOv2 features (NPY format)
108
+ if filename.endswith('_tokens.npy'):
109
+ # Load NPY data from bytes
110
+ npy_data = np.load(io.BytesIO(content))
111
+ uid = uid[:-len('_tokens')]
112
+ # Flatten the array for parquet storage
113
+ data[uid] = npy_data.flatten()
114
+ except Exception as e:
115
+ logger.error(f"Error processing {tar_path}: {e}")
116
+ return {}
117
+
118
+ return data
119
+
120
+ def extract_grounding_data(self, grounding_data):
121
+ """Extract grounding information from JSON data"""
122
+ if not grounding_data:
123
+ return None
124
+
125
+ # Get the first (and usually only) key which is the image name
126
+ image_key = list(grounding_data.keys())[0]
127
+ image_data = grounding_data[image_key]
128
+
129
+ # Extract objects and their bounding boxes
130
+ objects = image_data.get('objects', [])
131
+ floating_objects = image_data.get('floating_objects', [])
132
+
133
+ # Extract grounding relationships
134
+ relationships = image_data.get('relationships', {})
135
+ grounding_phrases = relationships.get('grounding', [])
136
+
137
+ # Extract captions with grounding details
138
+ short_captions = image_data.get('short_captions', [])
139
+ dense_caption = image_data.get('dense_caption', {})
140
+
141
+ return {
142
+ 'objects': objects,
143
+ 'floating_objects': floating_objects,
144
+ 'grounding_phrases': grounding_phrases,
145
+ 'short_captions': short_captions,
146
+ 'dense_caption': dense_caption
147
+ }
148
+
149
+ def normalize_bbox(self, bbox, image_size):
150
+ """Normalize bbox to 0-999 based on image dimensions"""
151
+ if not bbox or len(bbox) != 4:
152
+ return []
153
+
154
+ # RLE format: size = [width, height]
155
+ img_height, img_width = image_size
156
+
157
+ x1, y1, x2, y2 = bbox
158
+
159
+ # Normalize to 0-999 and clamp values
160
+ norm_x1 = max(0, min(999, int((x1 / img_width) * 999)))
161
+ norm_y1 = max(0, min(999, int((y1 / img_height) * 999)))
162
+ norm_x2 = max(0, min(999, int((x2 / img_width) * 999)))
163
+ norm_y2 = max(0, min(999, int((y2 / img_height) * 999)))
164
+
165
+ return [norm_x1, norm_y1, norm_x2, norm_y2]
166
+
167
+
168
+ def resize_and_crop_bbox(self, bbox, orig_size, target_size=512):
169
+ """
170
+ Resize and center-crop a bounding box using CLIP/BLIP-style transform.
171
+
172
+ Args:
173
+ bbox: list or tuple [x1, y1, x2, y2] in pixel coords.
174
+ orig_size: [W, H] original image size.
175
+ target_size: int, final square crop size (default=512).
176
+
177
+ Returns:
178
+ [x1', y1', x2', y2'] after resize+crop.
179
+ Also returns (scale, left, top) for debugging.
180
+ """
181
+ if not bbox or len(bbox) != 4:
182
+ return []
183
+
184
+ H, W = orig_size
185
+ x1, y1, x2, y2 = bbox
186
+
187
+ # --- 1. Resize (keep aspect ratio, short side -> target_size) ---
188
+ scale = target_size / min(W, H)
189
+ new_W, new_H = W * scale, H * scale
190
+
191
+ # --- 2. Center crop to target_sizeΓ—target_size ---
192
+ left = max(0, (new_W - target_size) / 2)
193
+ top = max(0, (new_H - target_size) / 2)
194
+
195
+ # --- 3. Apply transform to bbox ---
196
+ x1_new = x1 * scale - left
197
+ y1_new = y1 * scale - top
198
+ x2_new = x2 * scale - left
199
+ y2_new = y2 * scale - top
200
+
201
+ # Clip to [0, target_size]
202
+ x1_new, y1_new, x2_new, y2_new = np.clip(
203
+ [x1_new, y1_new, x2_new, y2_new],
204
+ 0, target_size
205
+ )
206
+
207
+ return [x1_new, y1_new, x2_new, y2_new]
208
+
209
+
210
+ def visualize_rle_mask(self, rle_data, save_path=None):
211
+ """Visualize RLE mask from segmentation data"""
212
+ try:
213
+ # Decode RLE to binary mask
214
+ mask = mask_util.decode(rle_data)
215
+
216
+ # Create visualization
217
+ fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 6))
218
+
219
+ # Show mask
220
+ ax1.imshow(mask, cmap='gray')
221
+ ax1.set_title('RLE Mask')
222
+ ax1.axis('off')
223
+
224
+ # Show mask with bbox overlay
225
+ ax2.imshow(mask, cmap='gray', alpha=0.7)
226
+
227
+ # Add bbox if available
228
+ if 'bbox' in rle_data:
229
+ bbox = rle_data['bbox']
230
+ x1, y1, x2, y2 = bbox
231
+ width = x2 - x1
232
+ height = y2 - y1
233
+ rect = plt.Rectangle((x1, y1), width, height,
234
+ fill=False, edgecolor='red', linewidth=2)
235
+ ax2.add_patch(rect)
236
+ ax2.set_title('Mask + Bbox')
237
+ else:
238
+ ax2.set_title('Mask Only')
239
+ ax2.axis('off')
240
+
241
+ plt.tight_layout()
242
+
243
+ if save_path:
244
+ plt.savefig(save_path, dpi=150, bbox_inches='tight')
245
+ logger.info(f"Mask visualization saved to {save_path}")
246
+ else:
247
+ plt.show()
248
+
249
+ plt.close()
250
+
251
+ except Exception as e:
252
+ logger.error(f"Error visualizing RLE mask: {e}")
253
+
254
+ def visualize_object_mask(self, grounding_data, uid, object_id=0, save_path=None):
255
+ """Visualize a specific object's mask from grounding data"""
256
+ try:
257
+ if not grounding_data or 'objects' not in grounding_data:
258
+ logger.warning(f"No objects found for {uid}")
259
+ return
260
+
261
+ objects = grounding_data['objects']
262
+ if object_id >= len(objects):
263
+ logger.warning(f"Object ID {object_id} not found for {uid}")
264
+ return
265
+
266
+ obj = objects[object_id]
267
+ if 'segmentation' not in obj:
268
+ logger.warning(f"No segmentation found for object {object_id} in {uid}")
269
+ return
270
+
271
+ segmentation = obj['segmentation']
272
+ bbox = obj.get('bbox', [])
273
+
274
+ # Create RLE data for visualization
275
+ rle_data = {
276
+ 'size': segmentation['size'],
277
+ 'counts': segmentation['counts'],
278
+ 'bbox': bbox
279
+ }
280
+
281
+ # Visualize
282
+ if save_path:
283
+ save_path_with_id = save_path.replace('.png', f'_obj_{object_id}.png')
284
+ else:
285
+ save_path_with_id = None
286
+
287
+ self.visualize_rle_mask(rle_data, save_path_with_id)
288
+
289
+ except Exception as e:
290
+ logger.error(f"Error visualizing object mask for {uid}: {e}")
291
+
292
+ def create_grounding_samples(self, grounding_data, uid):
293
+ """Create training samples from grounding data"""
294
+ if not grounding_data:
295
+ return []
296
+
297
+ samples = []
298
+
299
+ # Get image size from first object's segmentation
300
+ image_size = None
301
+ if grounding_data['objects'] and 'segmentation' in grounding_data['objects'][0]:
302
+ image_size = grounding_data['objects'][0]['segmentation']['size']
303
+ assert image_size is not None
304
+ else:
305
+ logger.warning(f"No segmentation data found for {uid}, skipping bbox processing")
306
+ return []
307
+
308
+ # Process grounding phrases
309
+ for phrase_info in grounding_data['grounding_phrases']:
310
+ phrase = phrase_info['phrase']
311
+ object_ids = phrase_info['object_ids']
312
+
313
+ # Find corresponding objects and their bboxes
314
+ for obj_id in object_ids:
315
+ for obj in grounding_data['objects']:
316
+ if obj.get('id') == obj_id:
317
+ bbox = obj.get('bbox', [])
318
+
319
+ # Process bbox if valid
320
+ if bbox and len(bbox) == 4:
321
+ bbox = self.resize_and_crop_bbox(bbox, image_size, 512)
322
+ bbox = self.normalize_bbox(bbox, [512, 512])
323
+ else:
324
+ bbox = []
325
+
326
+ sample = {
327
+ 'uid': uid,
328
+ 'phrase': phrase,
329
+ 'object_id': obj_id,
330
+ 'bbox': bbox,
331
+ 'labels': obj.get('labels', []),
332
+ 'attributes': obj.get('attributes', []),
333
+ 'sample_type': 'grounding_phrase',
334
+ 'segmentation': obj.get('segmentation', []),
335
+ }
336
+ samples.append(sample)
337
+ break
338
+
339
+ # Process short captions with details
340
+ for caption_info in grounding_data['short_captions']:
341
+ caption = caption_info['caption']
342
+ for detail in caption_info.get('details', []):
343
+ phrase = detail['phrase']
344
+ object_id = detail.get('id')
345
+ bbox = detail.get('bbox', [])
346
+
347
+ # Process bbox if valid
348
+ if bbox and len(bbox) == 4:
349
+ bbox = self.resize_and_crop_bbox(bbox, image_size, 512)
350
+ bbox = self.normalize_bbox(bbox, [512, 512])
351
+ else:
352
+ bbox = []
353
+
354
+ tokens_positive = detail.get('tokens_positive', [])
355
+
356
+ sample = {
357
+ 'uid': uid,
358
+ 'phrase': phrase,
359
+ 'caption_id': object_id,
360
+ 'bbox': bbox,
361
+ 'tokens_positive': tokens_positive,
362
+ 'full_caption': caption,
363
+ 'sample_type': 'short_caption'
364
+ }
365
+ samples.append(sample)
366
+
367
+ # # Process dense caption
368
+ # if grounding_data['dense_caption']:
369
+ # dense_caption = grounding_data['dense_caption']
370
+ # caption = dense_caption.get('caption', '')
371
+ # for detail in dense_caption.get('details', []):
372
+ # phrase = detail['phrase']
373
+ # object_ids = detail.get('ids', [])
374
+ # bboxes = detail.get('bbox', [])
375
+ # tokens_positive = detail.get('tokens_positive', [])
376
+
377
+ # sample = {
378
+ # 'uid': uid,
379
+ # 'phrase': phrase,
380
+ # 'object_ids': object_ids,
381
+ # 'bboxes': bboxes,
382
+ # 'tokens_positive': tokens_positive,
383
+ # 'full_caption': caption,
384
+ # 'sample_type': 'dense_caption'
385
+ # }
386
+ # samples.append(sample)
387
+
388
+ return samples
389
+
390
+ def process_tar_file(self, rgb_caption_tar):
391
+ """Process a single tar file and create corresponding parquet"""
392
+
393
+ # Extract base name (e.g., 'sa_000000' from 'sa_000000.tar')
394
+ base_name = os.path.splitext(os.path.basename(rgb_caption_tar))[0]
395
+
396
+ # Construct paths for corresponding depth, normal, det_seg, grounding, canny, dino tar files
397
+ depth_tar = os.path.join(self.depth_dir, f"{base_name}.tar")
398
+ normal_tar = os.path.join(self.normal_dir, f"{base_name}.tar")
399
+ det_seg_tar = os.path.join(self.det_seg_dir, f"{base_name}.tar")
400
+ grounding_tar = os.path.join(self.grounding_dir, f"{base_name}.tar")
401
+ canny_tar = os.path.join(self.canny_dir, f"{base_name}.tar")
402
+ dino_tar = os.path.join(self.dino_dir, f"{base_name}.tar")
403
+
404
+ logger.info(f"Processing {base_name}")
405
+
406
+ # Check if corresponding files exist
407
+ if not os.path.exists(depth_tar):
408
+ logger.warning(f"Depth tar file not found: {depth_tar}")
409
+ return None
410
+ if not os.path.exists(normal_tar):
411
+ logger.warning(f"Normal tar file not found: {normal_tar}")
412
+ return None
413
+ if not os.path.exists(det_seg_tar):
414
+ logger.warning(f"Det seg tar file not found: {det_seg_tar}")
415
+ return None
416
+ if not os.path.exists(canny_tar):
417
+ logger.warning(f"Canny tar file not found: {canny_tar}")
418
+ return None
419
+ if not os.path.exists(dino_tar):
420
+ logger.warning(f"DINOv2 tar file not found: {dino_tar}")
421
+ return None
422
+ # Initialize grounding_data
423
+ grounding_data = {}
424
+
425
+ if not os.path.exists(grounding_tar):
426
+ logger.warning(f"Grounding tar file not found: {grounding_tar} - will process without grounding data")
427
+ else:
428
+ logger.info(f"Extracting from grounding: {grounding_tar}")
429
+ grounding_data = self.extract_from_tar(grounding_tar, 'grounding')
430
+
431
+ # Extract data from all tar files
432
+ logger.info(f"Extracting from rgb_caption: {rgb_caption_tar}")
433
+ rgb_caption_data = self.extract_from_tar(rgb_caption_tar, 'rgb_caption')
434
+
435
+ logger.info(f"Extracting from depth: {depth_tar}")
436
+ depth_data = self.extract_from_tar(depth_tar, 'depth')
437
+
438
+ logger.info(f"Extracting from normal: {normal_tar}")
439
+ normal_data = self.extract_from_tar(normal_tar, 'normal')
440
+
441
+ logger.info(f"Extracting from det_seg: {det_seg_tar}")
442
+ det_seg_data = self.extract_from_tar(det_seg_tar, 'det_seg')
443
+
444
+ logger.info(f"Extracting from canny: {canny_tar}")
445
+ canny_data = self.extract_from_tar(canny_tar, 'canny')
446
+
447
+ logger.info(f"Extracting from dino: {dino_tar}")
448
+ dino_data = self.extract_from_tar(dino_tar, 'dino')
449
+
450
+ # Find common UIDs (grounding is optional)
451
+ common_uids = set(rgb_caption_data.keys()) & set(depth_data.keys()) & set(normal_data.keys()) & set(det_seg_data.keys()) & set(canny_data.keys()) & set(dino_data.keys())
452
+
453
+ if not common_uids:
454
+ logger.warning(f"No common UIDs found for {base_name}")
455
+ return None
456
+
457
+ logger.info(f"Found {len(common_uids)} common UIDs")
458
+
459
+ # Sort UIDs for consistent ordering
460
+ sorted_uids = sorted(common_uids)
461
+
462
+ # Create samples for each UID
463
+ all_samples = []
464
+
465
+ for uid in sorted_uids:
466
+ rgb_info = rgb_caption_data[uid]
467
+
468
+ # Skip if missing image or caption
469
+ if rgb_info['image'] is None or rgb_info['caption'] is None:
470
+ continue
471
+
472
+ # Create base sample with all modalities
473
+ sample = {
474
+ 'uid': uid,
475
+ 'rgb': rgb_caption_data[uid]['image'],
476
+ 'caption': rgb_caption_data[uid]['caption'],
477
+ 'depth': depth_data[uid],
478
+ 'normal': normal_data[uid],
479
+ 'det_seg': det_seg_data[uid],
480
+ 'canny': canny_data[uid],
481
+ 'dino': dino_data[uid]
482
+ }
483
+
484
+ # Add grounding data if available
485
+ if uid in grounding_data:
486
+ extracted_grounding = self.extract_grounding_data(grounding_data[uid])
487
+ if extracted_grounding:
488
+ raw_image = Image.open(io.BytesIO(rgb_caption_data[uid]['image']))
489
+ raw_image_size = raw_image.size
490
+ assert raw_image_size == (512, 512), f"Raw image size is {raw_image_size} but expected (512, 512) for {uid}"
491
+ grounding_samples = self.create_grounding_samples(extracted_grounding, uid)
492
+ if grounding_samples:
493
+ sample['grounding'] = grounding_samples
494
+
495
+ # visualize_segmentation = sample['grounding'][1]['segmentation']
496
+ # self.visualize_rle_mask(visualize_segmentation,save_path=f"visualize_{uid}_1.png")
497
+ all_samples.append(sample)
498
+
499
+ if not all_samples:
500
+ logger.warning(f"No samples found for {base_name}")
501
+ return None
502
+
503
+ # Create DataFrame
504
+ df = pd.DataFrame(all_samples)
505
+
506
+ # Debug information
507
+ logger.info(f"DataFrame shape: {df.shape}")
508
+ if not df.empty:
509
+ logger.info(f"Sample UIDs: {df['uid'].head().tolist()}")
510
+ # Count grounding samples
511
+ grounding_counts = []
512
+ for _, row in df.iterrows():
513
+ if 'grounding' in row and row['grounding'] is not None and isinstance(row['grounding'], list):
514
+ grounding_counts.append(len(row['grounding']))
515
+ else:
516
+ grounding_counts.append(0)
517
+ if grounding_counts:
518
+ logger.info(f"Grounding samples per row: {np.mean(grounding_counts):.2f} avg, {max(grounding_counts)} max")
519
+ else:
520
+ logger.info("No grounding samples found")
521
+
522
+ # Count DINOv2 features
523
+ dino_counts = []
524
+ for _, row in df.iterrows():
525
+ if 'dino' in row and row['dino'] is not None:
526
+ dino_counts.append(1)
527
+ else:
528
+ dino_counts.append(0)
529
+ if dino_counts:
530
+ logger.info(f"DINOv2 features loaded: {sum(dino_counts)}/{len(dino_counts)} samples")
531
+ else:
532
+ logger.info("No DINOv2 features found")
533
+
534
+ # Save to parquet
535
+ output_path = os.path.join(self.parquet_dir, f"{base_name}.parquet")
536
+
537
+ df.to_parquet(
538
+ output_path,
539
+ index=False,
540
+ engine='pyarrow',
541
+ compression='snappy',
542
+ row_group_size=1000
543
+ )
544
+ logger.info(f"Saved {len(df)} grounding samples to {output_path}")
545
+
546
+ return output_path
547
+
548
+ def process_all_tars(self):
549
+ """Process all tar files and create corresponding parquet files"""
550
+ tar_files = self.get_tar_files()
551
+
552
+ if not tar_files:
553
+ logger.error("No tar files found in rgb_caption directory")
554
+ return
555
+
556
+ tar_files = tar_files[1373:]
557
+ logger.info(f"Found {len(tar_files)} tar files to process")
558
+
559
+ processed_files = []
560
+ for tar_file in tar_files:
561
+ try:
562
+ output_path = self.process_tar_file(tar_file)
563
+ if output_path:
564
+ processed_files.append(output_path)
565
+ except Exception as e:
566
+ logger.error(f"Error processing {tar_file}: {e}")
567
+
568
+ logger.info(f"Successfully processed {len(processed_files)} files")
569
+ return processed_files
570
+
571
+ def main():
572
+ import argparse
573
+
574
+ parser = argparse.ArgumentParser(description='Generate parquet files with grounding data')
575
+ parser.add_argument('--base_dir', type=str, default='./datasets/blip3o',
576
+ help='Base directory (default: ./datasets/blip3o)')
577
+ parser.add_argument('--visualize_mask', action='store_true',
578
+ help='Visualize RLE masks for debugging')
579
+ parser.add_argument('--visualize_tar', type=str, default=None,
580
+ help='Specific tar file to visualize (e.g., sa_000001.tar)')
581
+ parser.add_argument('--visualize_uid', type=str, default=None,
582
+ help='Specific UID to visualize (e.g., sa_1)')
583
+ parser.add_argument('--visualize_object_id', type=int, default=0,
584
+ help='Object ID to visualize (default: 0)')
585
+
586
+ args = parser.parse_args()
587
+
588
+ # Set the base directory
589
+ base_dir = args.base_dir
590
+
591
+ # Create processor
592
+ processor = GroundingProcessor(base_dir)
593
+
594
+ # Visualize mask if requested
595
+ if args.visualize_mask and args.visualize_tar:
596
+ logger.info(f"Visualizing mask for {args.visualize_tar}, object {args.visualize_object_id}")
597
+
598
+ # Find the grounding tar file for this UID
599
+ grounding_tar = os.path.join(processor.grounding_dir, f"{args.visualize_tar}.tar")
600
+ if os.path.exists(grounding_tar):
601
+ grounding_data = processor.extract_from_tar(grounding_tar, 'grounding')
602
+ if args.visualize_uid in grounding_data:
603
+ extracted_grounding = processor.extract_grounding_data(grounding_data[args.visualize_uid])
604
+ if extracted_grounding:
605
+ save_path = f"mask_visualization_{args.visualize_uid}.png"
606
+ processor.visualize_object_mask(extracted_grounding, args.visualize_uid,
607
+ args.visualize_object_id, save_path)
608
+ else:
609
+ logger.error(f"No grounding data found for {args.visualize_uid}")
610
+ else:
611
+ logger.error(f"UID {args.visualize_uid} not found in grounding tar")
612
+ else:
613
+ logger.error(f"Grounding tar not found: {grounding_tar}")
614
+ return
615
+
616
+ # Process all tar files
617
+ processed_files = processor.process_all_tars()
618
+
619
+ if processed_files:
620
+ logger.info(f"Successfully created {len(processed_files)} grounding parquet files:")
621
+ for file in processed_files:
622
+ logger.info(f" - {file}")
623
+ else:
624
+ logger.error("No grounding parquet files were created")
625
+
626
+ if __name__ == "__main__":
627
+ main()
data/any2any_preprocess/generate_parquet_json.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import argparse
4
+ import pyarrow.parquet as pq
5
+ from pathlib import Path
6
+
7
+ def get_parquet_info(file_path):
8
+ """Get information about a parquet file"""
9
+ try:
10
+ parquet_file = pq.ParquetFile(file_path)
11
+ num_row_groups = parquet_file.num_row_groups
12
+
13
+ # Calculate total number of rows
14
+ total_rows = 0
15
+ for i in range(num_row_groups):
16
+ total_rows += parquet_file.metadata.row_group(i).num_rows
17
+
18
+ return {
19
+ "num_row_groups": num_row_groups,
20
+ "num_rows": total_rows
21
+ }
22
+ except Exception as e:
23
+ print(f"Error reading {file_path}: {e}")
24
+ return None
25
+
26
+ def generate_parquet_info(parquet_dir=None, output_file=None):
27
+ """Generate information for all parquet files in the parquet folder"""
28
+ parquet_dir = Path(parquet_dir) if parquet_dir is not None else Path("./datasets/blip3o/parquet_rgb_caption_depth_normal_det_seg_grounding_canny_dino_global_clip448_imagebind")
29
+ output_file = Path(output_file) if output_file is not None else Path("./datasets/blip3o/parquet_info/blip3o_rgb_caption_depth_normal_det_seg_grounding_canny_dino_global_clip448_imagebind.json")
30
+
31
+ # Create output directory if it doesn't exist
32
+ output_file.parent.mkdir(parents=True, exist_ok=True)
33
+
34
+ # Get all parquet files
35
+ parquet_files = list(parquet_dir.glob("*.parquet"))
36
+ print(f"Found {len(parquet_files)} parquet files")
37
+
38
+ # Process each file
39
+ parquet_info = {}
40
+
41
+ for i, parquet_file in enumerate(parquet_files):
42
+ print(f"Processing {i+1}/{len(parquet_files)}: {parquet_file.name}")
43
+
44
+ # Get relative path from current directory using parquet_dir
45
+ relative_path = str((parquet_dir / parquet_file.name))
46
+ if not relative_path.startswith("./"):
47
+ relative_path = f"./{relative_path}"
48
+
49
+ info = get_parquet_info(parquet_file)
50
+ if info:
51
+ parquet_info[relative_path] = info
52
+ print(f"Processed {parquet_file.name} successfully")
53
+ print(info)
54
+ else:
55
+ print(f"Failed to process {parquet_file.name}")
56
+
57
+ # Save to JSON file
58
+ with open(output_file, 'w') as f:
59
+ json.dump(parquet_info, f, indent=2, sort_keys=True)
60
+
61
+ print(f"Saved parquet information to {output_file}")
62
+ print(f"Processed {len(parquet_info)} files successfully")
63
+
64
+ if __name__ == "__main__":
65
+ parser = argparse.ArgumentParser()
66
+ parser.add_argument("--parquet_dir", default=None)
67
+ parser.add_argument("--output_file", default=None)
68
+ args = parser.parse_args()
69
+ generate_parquet_info(parquet_dir=args.parquet_dir, output_file=args.output_file)
data/any2any_preprocess/generate_parquet_vlm_sft.py ADDED
@@ -0,0 +1,278 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2025 Bytedance Ltd. and/or its affiliates.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """Bundle vlm_sft (llava-onevision style jsonl + loose image files) into
4
+ parquet shards loadable by SftParquetIterableDataset (data/vlm_dataset.py).
5
+
6
+ Why: llava-onevision is ~4M small files, impractical to upload to / stream
7
+ from S3. The shards follow the blip3o convention: few large parquet files +
8
+ a parquet_info JSON, sharded by (file, row_group) at load time.
9
+
10
+ Equivalence: the jsonl loader selects samples per subset via
11
+ rng = random.Random(); rng.seed(shuffle_seed); rng.shuffle(lines)
12
+ lines = lines[:num_used_data]
13
+ (SftJSONLIterableDataset.get_data_paths). This script replicates that
14
+ selection EXACTLY (taking shuffle_lines / shuffle_seed / num_used_data from
15
+ the same training yaml), so the parquet rows are precisely the records the
16
+ jsonl loader would have used, in the same shuffled order.
17
+
18
+ Row schema:
19
+ record: str original jsonl line, verbatim
20
+ image_bytes: list<binary> raw image files (no re-encode), in the order
21
+ the jsonl loader's valid_paths would produce
22
+
23
+ Skipped at conversion (mirroring the jsonl loader's runtime skips):
24
+ - records with 'video' (frame sampling can't be baked into bytes)
25
+ - records whose 'image' field resolves to zero existing files
26
+
27
+ Usage (single process, all subsets of the group):
28
+ python3 data/any2any_preprocess/generate_parquet_vlm_sft.py \
29
+ --config data/configs/modus_stage1.yaml --group vlm_sft \
30
+ --out-dir ./datasets/llava_onevision_vqa_parquet
31
+
32
+ Parallelize across subsets with disjoint --subsets lists (e.g. sbatch array);
33
+ each invocation only rewrites its own subsets' shards and merge-updates the
34
+ shared parquet_info JSON. Already-converted subsets are skipped unless
35
+ --overwrite is given.
36
+
37
+ After conversion, point the training config at the new group:
38
+ vlm_sft_parquet:
39
+ dataset_names:
40
+ - llava_onevision_vqa
41
+ image_transform_args: { ...same as the old vlm_sft block... }
42
+ is_mandatory: true
43
+ weight: <same as before>
44
+ (no num_used_data / shuffle_lines needed β€” selection is baked in)
45
+ """
46
+
47
+ import argparse
48
+ import json
49
+ import os
50
+ import random
51
+ import re
52
+ import sys
53
+ from concurrent.futures import ThreadPoolExecutor
54
+
55
+ import pyarrow as pa
56
+ import pyarrow.parquet as pq
57
+ import yaml
58
+
59
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..'))
60
+ from data.dataset_info import DATASET_INFO # noqa: E402
61
+ from data.parquet_utils import apply_data_root_override, read_file_bytes # noqa: E402
62
+
63
+ SCHEMA = pa.schema([
64
+ pa.field('record', pa.string()),
65
+ pa.field('image_bytes', pa.list_(pa.binary())),
66
+ ])
67
+
68
+
69
+ def sanitize(name):
70
+ # subset names contain '(', ',', ')' etc. β€” make a filesystem-safe stem
71
+ return re.sub(r'[^0-9A-Za-z_.-]+', '_', name).strip('_')
72
+
73
+
74
+ def _read_image_bytes(path):
75
+ if path.startswith('s3://'):
76
+ return read_file_bytes(path)
77
+ with open(path, 'rb') as f:
78
+ return f.read()
79
+
80
+
81
+ def _load_record_images(args):
82
+ """Worker: (raw_line, image_dir) -> (raw_line, image_bytes list, status).
83
+ status: 'ok' | 'skip_video' | 'skip_missing' | 'skip_badjson'."""
84
+ raw_line, image_dir = args
85
+ try:
86
+ data_item = json.loads(raw_line)
87
+ except Exception:
88
+ return raw_line, None, 'skip_badjson'
89
+ if 'video' in data_item:
90
+ return raw_line, None, 'skip_video'
91
+ image_bytes = []
92
+ if 'image' in data_item:
93
+ images = data_item['image'] if isinstance(data_item['image'], list) else [data_item['image']]
94
+ for img_name in images:
95
+ if not (isinstance(img_name, str) and img_name.strip() != ''):
96
+ continue
97
+ img_path = os.path.join(image_dir, img_name)
98
+ try:
99
+ image_bytes.append(_read_image_bytes(img_path))
100
+ except (OSError, IOError):
101
+ continue # mirrors the jsonl loader dropping invalid paths
102
+ if len(image_bytes) == 0:
103
+ return raw_line, None, 'skip_missing'
104
+ return raw_line, image_bytes, 'ok'
105
+
106
+
107
+ def select_lines(jsonl_path, num_used_data, shuffle_lines, shuffle_seed):
108
+ """Replicates SftJSONLIterableDataset.get_data_paths line selection."""
109
+ jsonl_path = apply_data_root_override(jsonl_path)
110
+ if jsonl_path.startswith('s3://'):
111
+ raw_data = read_file_bytes(jsonl_path).decode('utf-8').splitlines(keepends=True)
112
+ else:
113
+ with open(jsonl_path, 'r') as f:
114
+ raw_data = f.readlines()
115
+ if shuffle_lines:
116
+ rng = random.Random()
117
+ rng.seed(shuffle_seed)
118
+ rng.shuffle(raw_data)
119
+ return raw_data[:num_used_data]
120
+
121
+
122
+ def convert_subset(subset, jsonl_path, image_dir, num_used_data, shuffle_lines,
123
+ shuffle_seed, train_dir, rows_per_file, row_group_size,
124
+ io_threads, info_key_dir):
125
+ """Convert one subset to parquet shards. Returns (shard_info, stats):
126
+ shard_info: {info_key: {'num_row_groups': int, 'num_rows': int}}."""
127
+ image_dir = apply_data_root_override(image_dir)
128
+ lines = select_lines(jsonl_path, num_used_data, shuffle_lines, shuffle_seed)
129
+ print(f'[convert] {subset}: selected {len(lines)} lines (quota {num_used_data})')
130
+
131
+ stem = sanitize(subset)
132
+ stats = {'ok': 0, 'skip_video': 0, 'skip_missing': 0, 'skip_badjson': 0}
133
+ shard_info = {}
134
+ part = 0
135
+ records, images = [], []
136
+
137
+ def flush():
138
+ nonlocal part, records, images
139
+ if not records:
140
+ return
141
+ fname = f'{stem}__part{part:04d}.parquet'
142
+ out_path = os.path.join(train_dir, fname)
143
+ table = pa.Table.from_arrays(
144
+ [pa.array(records, type=pa.string()),
145
+ pa.array(images, type=pa.list_(pa.binary()))],
146
+ schema=SCHEMA,
147
+ )
148
+ pq.write_table(table, out_path, compression='snappy',
149
+ row_group_size=row_group_size)
150
+ meta = pq.ParquetFile(out_path).metadata
151
+ shard_info[os.path.join(info_key_dir, fname)] = {
152
+ 'num_row_groups': meta.num_row_groups,
153
+ 'num_rows': meta.num_rows,
154
+ }
155
+ part += 1
156
+ records, images = [], []
157
+
158
+ with ThreadPoolExecutor(max_workers=io_threads) as ex:
159
+ # batch the submissions: Executor.map submits everything up front and
160
+ # holds completed results until iterated, so mapping the whole subset
161
+ # at once would keep every image of the subset in memory.
162
+ for start in range(0, len(lines), rows_per_file):
163
+ chunk = lines[start:start + rows_per_file]
164
+ for raw_line, image_bytes, status in ex.map(
165
+ _load_record_images, ((l, image_dir) for l in chunk)
166
+ ):
167
+ stats[status] += 1
168
+ if status != 'ok':
169
+ continue
170
+ records.append(raw_line)
171
+ images.append(image_bytes)
172
+ if len(records) >= rows_per_file:
173
+ flush()
174
+ flush()
175
+ return shard_info, stats
176
+
177
+
178
+ def main():
179
+ # DATASET_INFO uses './datasets/...' paths relative to the repo root;
180
+ # run from there regardless of the caller's cwd (e.g. container default
181
+ # workdir). Pass absolute --config/--out-dir or repo-root-relative ones.
182
+ os.chdir(os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')))
183
+
184
+ parser = argparse.ArgumentParser(description=__doc__)
185
+ parser.add_argument('--config', required=True,
186
+ help='training data yaml (e.g. data/configs/modus_stage1.yaml)')
187
+ parser.add_argument('--group', default='vlm_sft',
188
+ help='dataset group in the yaml / DATASET_INFO')
189
+ parser.add_argument('--out-dir', default='./datasets/llava_onevision_vqa_parquet')
190
+ parser.add_argument('--subsets', nargs='*', default=None,
191
+ help='only convert these subsets (default: all in the yaml)')
192
+ parser.add_argument('--rows-per-file', type=int, default=5000)
193
+ parser.add_argument('--row-group-size', type=int, default=200)
194
+ parser.add_argument('--io-threads', type=int, default=16)
195
+ parser.add_argument('--overwrite', action='store_true',
196
+ help='re-convert subsets whose shards already exist')
197
+ args = parser.parse_args()
198
+
199
+ with open(args.config, 'r') as f:
200
+ cfg = yaml.safe_load(f)[args.group]
201
+ dataset_names = cfg['dataset_names']
202
+ num_used_data = cfg['num_used_data']
203
+ shuffle_lines = cfg.get('shuffle_lines', False)
204
+ shuffle_seed = cfg.get('shuffle_seed', 0)
205
+ if len(dataset_names) != len(num_used_data):
206
+ # The jsonl loader zips the two lists, silently truncating the longer
207
+ # one (this happens in the real configs: subsets commented out of
208
+ # dataset_names leave stale num_used_data rows). Replicate that
209
+ # pairing exactly β€” equivalence with current training behavior.
210
+ print(f'[warn] dataset_names ({len(dataset_names)}) and num_used_data '
211
+ f'({len(num_used_data)}) differ; zip-truncating like the jsonl loader')
212
+
213
+ stems = [sanitize(s) for s in dataset_names]
214
+ assert len(set(stems)) == len(stems), 'sanitized subset names collide'
215
+
216
+ train_dir = os.path.join(args.out_dir, 'train')
217
+ info_dir = os.path.join(args.out_dir, 'parquet_info')
218
+ os.makedirs(train_dir, exist_ok=True)
219
+ os.makedirs(info_dir, exist_ok=True)
220
+ info_path = os.path.join(info_dir, 'llava_onevision_vqa.json')
221
+ # info keys use the configured-style relative path; loaders fall back to
222
+ # basename matching anyway, so the prefix only matters cosmetically.
223
+ info_key_dir = train_dir
224
+
225
+ selected = args.subsets if args.subsets else dataset_names
226
+ unknown = [s for s in selected if s not in dataset_names]
227
+ assert not unknown, f'subsets not in the yaml group: {unknown}'
228
+
229
+ totals = {'ok': 0, 'skip_video': 0, 'skip_missing': 0, 'skip_badjson': 0}
230
+ for subset, n_used in zip(dataset_names, num_used_data):
231
+ if subset not in selected:
232
+ continue
233
+ stem = sanitize(subset)
234
+ # A subset counts as done only if its shards are in the info JSON,
235
+ # which is merge-updated AFTER the whole subset finishes. Files on
236
+ # disk without an info entry are partial output of an interrupted
237
+ # run β€” delete and re-convert.
238
+ done_in_info = set()
239
+ if os.path.exists(info_path) and not args.overwrite:
240
+ with open(info_path, 'r') as f:
241
+ done_in_info = {os.path.basename(k) for k in json.load(f)
242
+ if os.path.basename(k).startswith(f'{stem}__part')}
243
+ existing = [f for f in os.listdir(train_dir)
244
+ if f.startswith(f'{stem}__part') and f.endswith('.parquet')]
245
+ if done_in_info and set(existing) >= done_in_info:
246
+ print(f'[skip] {subset}: {len(done_in_info)} shard(s) already converted')
247
+ continue
248
+ for f in existing:
249
+ os.remove(os.path.join(train_dir, f))
250
+
251
+ meta_info = DATASET_INFO[args.group][subset]
252
+ shard_info, stats = convert_subset(
253
+ subset, meta_info['jsonl_path'], meta_info['data_dir'], n_used,
254
+ shuffle_lines, shuffle_seed, train_dir, args.rows_per_file,
255
+ args.row_group_size, args.io_threads, info_key_dir,
256
+ )
257
+ for k in totals:
258
+ totals[k] += stats[k]
259
+ print(f'[done] {subset}: {stats}')
260
+
261
+ # merge-update the shared parquet_info JSON after each subset so
262
+ # partial runs / parallel invocations stay resumable.
263
+ merged = {}
264
+ if os.path.exists(info_path):
265
+ with open(info_path, 'r') as f:
266
+ merged = json.load(f)
267
+ merged.update(shard_info)
268
+ tmp_path = info_path + f'.tmp.{os.getpid()}'
269
+ with open(tmp_path, 'w') as f:
270
+ json.dump(merged, f, indent=1)
271
+ os.replace(tmp_path, info_path)
272
+
273
+ print(f'[all done] totals: {totals}')
274
+ print(f'parquet_info: {info_path}')
275
+
276
+
277
+ if __name__ == '__main__':
278
+ main()
data/any2any_preprocess/hf_upload.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Upload a folder to a HuggingFace dataset repo.
2
+
3
+ Token is read from the file at $HF_TOKEN_FILE (never passed on the command line
4
+ so it does not appear in logs / process listings).
5
+
6
+ Usage:
7
+ HF_TOKEN_FILE=/users/mye/.hf_token python hf_upload.py <repo_id> <folder>
8
+ """
9
+ import os
10
+ import sys
11
+
12
+ from huggingface_hub import HfApi
13
+
14
+ repo_id = sys.argv[1]
15
+ folder = sys.argv[2]
16
+ token = open(os.environ['HF_TOKEN_FILE']).read().strip()
17
+
18
+ api = HfApi(token=token)
19
+ api.create_repo(repo_id, repo_type='dataset', exist_ok=True)
20
+ api.upload_folder(
21
+ repo_id=repo_id,
22
+ repo_type='dataset',
23
+ folder_path=folder,
24
+ commit_message='Add MODUS 16-mod preview (500 SA-1B samples + hero + card)',
25
+ )
26
+ print(f'DONE: https://huggingface.co/datasets/{repo_id}', flush=True)
data/any2any_preprocess/parquet_visualize.py ADDED
@@ -0,0 +1,408 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import pyarrow as pa
3
+ import pyarrow.parquet as pq
4
+ import os
5
+ import numpy as np
6
+ from pathlib import Path
7
+ from PIL import Image, ImageDraw, ImageFont
8
+ import json
9
+ import io
10
+ import matplotlib.pyplot as plt
11
+ import matplotlib.patches as patches
12
+ from matplotlib.patches import Rectangle
13
+ import cv2
14
+
15
+ def make_json_serializable(obj):
16
+ """
17
+ Convert numpy arrays and other non-serializable objects to JSON-serializable format.
18
+ """
19
+ if isinstance(obj, np.ndarray):
20
+ return obj.tolist()
21
+ elif isinstance(obj, dict):
22
+ return {key: make_json_serializable(value) for key, value in obj.items()}
23
+ elif isinstance(obj, list):
24
+ return [make_json_serializable(item) for item in obj]
25
+ elif isinstance(obj, (np.integer, np.floating)):
26
+ return obj.item()
27
+ elif hasattr(obj, 'tolist'): # Handle other numpy-like objects
28
+ return obj.tolist()
29
+ elif hasattr(obj, 'item'): # Handle other numpy scalar types
30
+ return obj.item()
31
+ else:
32
+ return obj
33
+
34
+ def decode_rle_to_mask(rle_counts, size):
35
+ """
36
+ Decode RLE (Run Length Encoding) to binary mask.
37
+ This is a simplified RLE decoder for COCO format.
38
+
39
+ Args:
40
+ rle_counts (str): RLE encoded string
41
+ size (list): [width, height] of the mask
42
+
43
+ Returns:
44
+ numpy.ndarray: Binary mask of shape (height, width)
45
+ """
46
+ try:
47
+ # Try to use pycocotools if available
48
+ import pycocotools.mask as mask_util
49
+
50
+ # Create RLE dict
51
+ rle = {
52
+ 'counts': rle_counts,
53
+ 'size': size
54
+ }
55
+
56
+ # Decode to binary mask
57
+ binary_mask = mask_util.decode(rle)
58
+ return binary_mask
59
+ except ImportError:
60
+ # Fallback: create a simple mask visualization without decoding
61
+ print("Warning: pycocotools not available, creating placeholder mask")
62
+ height, width = size[1], size[0] # size is [width, height]
63
+ return np.zeros((height, width), dtype=np.uint8)
64
+
65
+ def simple_rle_decode(rle_counts, size):
66
+ """
67
+ Simple RLE decoder for COCO format masks.
68
+ This is a basic implementation that should work for most cases.
69
+ """
70
+ try:
71
+ height, width = size[1], size[0] # size is [width, height]
72
+ mask = np.zeros(height * width, dtype=np.uint8)
73
+
74
+ # Parse the RLE counts
75
+ counts = []
76
+ current_num = ""
77
+ for char in rle_counts:
78
+ if char.isdigit() or char in '-+':
79
+ current_num += char
80
+ else:
81
+ if current_num:
82
+ counts.append(int(current_num))
83
+ current_num = ""
84
+ if current_num:
85
+ counts.append(int(current_num))
86
+
87
+ # Decode RLE
88
+ pos = 0
89
+ for i, count in enumerate(counts):
90
+ if i % 2 == 0: # Even indices are background (0)
91
+ pos += count
92
+ else: # Odd indices are foreground (1)
93
+ mask[pos:pos+count] = 1
94
+ pos += count
95
+
96
+ # Reshape to 2D
97
+ mask = mask.reshape((height, width))
98
+ return mask
99
+ except Exception as e:
100
+ print(f"Error in simple RLE decode: {e}")
101
+ height, width = size[1], size[0]
102
+ return np.zeros((height, width), dtype=np.uint8)
103
+
104
+ def visualize_detection_data(det_data, rgb_image, output_dir, filename_base):
105
+ """
106
+ Visualize detection data with bounding boxes and masks overlaid on RGB image.
107
+
108
+ Args:
109
+ det_data (dict): Detection data containing instances
110
+ rgb_image (PIL.Image): RGB image to overlay on
111
+ output_dir (str): Output directory
112
+ filename_base (str): Base filename for saving
113
+ """
114
+ try:
115
+ # Make a deep copy of the original data to avoid modifying it
116
+ import copy
117
+ original_det_data = make_json_serializable(copy.deepcopy(det_data))
118
+ # Convert PIL image to numpy array
119
+ img_array = np.array(rgb_image)
120
+ height, width = img_array.shape[:2]
121
+
122
+ # Create figure with subplots
123
+ fig, axes = plt.subplots(1, 3, figsize=(18, 6))
124
+
125
+ # Original image
126
+ axes[0].imshow(img_array)
127
+ axes[0].set_title('Original Image')
128
+ axes[0].axis('off')
129
+
130
+ # Image with bounding boxes
131
+ axes[1].imshow(img_array)
132
+ axes[1].set_title('Bounding Boxes')
133
+ axes[1].axis('off')
134
+
135
+ # Image with masks
136
+ axes[2].imshow(img_array)
137
+ axes[2].set_title('Segmentation Masks')
138
+ axes[2].axis('off')
139
+
140
+ # Colors for different categories
141
+ colors = ['red', 'blue', 'green', 'yellow', 'purple', 'orange', 'cyan', 'magenta']
142
+
143
+ # Process each instance
144
+ for i, instance in enumerate(det_data.get('instances', [])):
145
+ color = colors[i % len(colors)]
146
+ category = instance.get('category', 'unknown')
147
+ score = instance.get('score', 0.0)
148
+ bbox = instance.get('bbox', [])
149
+
150
+ # Draw bounding box
151
+ if len(bbox) == 4:
152
+ x1, y1, x2, y2 = bbox
153
+ rect = Rectangle((x1, y1), x2-x1, y2-y1,
154
+ linewidth=2, edgecolor=color, facecolor='none')
155
+ axes[1].add_patch(rect)
156
+
157
+ # Add label
158
+ label = f"{category}: {score:.2f}"
159
+ axes[1].text(x1, y1-5, label, color=color, fontsize=8,
160
+ bbox=dict(boxstyle="round,pad=0.3", facecolor='white', alpha=0.7))
161
+
162
+ # Draw segmentation mask
163
+ segmentation = instance.get('segmentation', {})
164
+ mask_drawn = False
165
+
166
+ if 'counts' in segmentation and 'size' in segmentation:
167
+ # Try multiple decoding methods
168
+ mask = None
169
+
170
+ # Method 1: Try pycocotools
171
+ try:
172
+ mask = decode_rle_to_mask(segmentation['counts'], segmentation['size'])
173
+ if mask.size > 0 and np.any(mask):
174
+ print(f"Successfully decoded mask for {category} using pycocotools")
175
+ except:
176
+ pass
177
+
178
+ # Method 2: Try simple RLE decoder
179
+ if mask is None or not np.any(mask):
180
+ try:
181
+ mask = simple_rle_decode(segmentation['counts'], segmentation['size'])
182
+ if mask.size > 0 and np.any(mask):
183
+ print(f"Successfully decoded mask for {category} using simple decoder")
184
+ except Exception as e:
185
+ print(f"Simple RLE decode failed for {category}: {e}")
186
+
187
+ # If we have a valid mask, visualize it
188
+ if mask is not None and mask.size > 0 and np.any(mask):
189
+ # Create a more visible mask visualization
190
+ # Method 1: Colored overlay
191
+ mask_colored = np.zeros((height, width, 4))
192
+ color_rgb = np.array(plt.cm.colors.to_rgb(color)) * 255
193
+ mask_colored[:, :, :3] = color_rgb
194
+ mask_colored[:, :, 3] = mask * 0.6 # More opaque
195
+
196
+ # Apply mask overlay
197
+ axes[2].imshow(mask_colored.astype(np.uint8), alpha=0.7)
198
+
199
+ # Method 2: Also draw contour
200
+ from matplotlib.patches import Polygon
201
+ contours, _ = cv2.findContours(mask.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
202
+ for contour in contours:
203
+ if len(contour) > 2:
204
+ contour = contour.reshape(-1, 2)
205
+ polygon = Polygon(contour, fill=False, edgecolor=color, linewidth=2)
206
+ axes[2].add_patch(polygon)
207
+
208
+ mask_drawn = True
209
+
210
+ # Fallback: if no mask was drawn, create a simple mask from bounding box
211
+ if not mask_drawn and len(bbox) == 4:
212
+ x1, y1, x2, y2 = bbox
213
+ # Create a simple rectangular mask
214
+ mask = np.zeros((height, width), dtype=np.uint8)
215
+ x1, y1, x2, y2 = int(x1), int(y1), int(x2), int(y2)
216
+ # Ensure coordinates are within bounds
217
+ x1, y1 = max(0, x1), max(0, y1)
218
+ x2, y2 = min(width, x2), min(height, y2)
219
+
220
+ if x2 > x1 and y2 > y1:
221
+ mask[y1:y2, x1:x2] = 1
222
+
223
+ # Create colored mask overlay
224
+ mask_colored = np.zeros((height, width, 4))
225
+ color_rgb = np.array(plt.cm.colors.to_rgb(color)) * 255
226
+ mask_colored[:, :, :3] = color_rgb
227
+ mask_colored[:, :, 3] = mask * 0.4 # Semi-transparent
228
+
229
+ # Apply mask overlay
230
+ axes[2].imshow(mask_colored.astype(np.uint8), alpha=0.6)
231
+
232
+ # Also draw the bounding box outline
233
+ rect = Rectangle((x1, y1), x2-x1, y2-y1,
234
+ linewidth=2, edgecolor=color, facecolor='none')
235
+ axes[2].add_patch(rect)
236
+
237
+ # Save the visualization
238
+ output_path = os.path.join(output_dir, f"{filename_base}_detection_visualization.png")
239
+ plt.tight_layout()
240
+ plt.savefig(output_path, dpi=150, bbox_inches='tight')
241
+ plt.close()
242
+
243
+ print(f" Saved detection visualization: {filename_base}_detection_visualization.png")
244
+
245
+ # Also save the JSON data for reference
246
+ json_path = os.path.join(output_dir, f"{filename_base}_det_seg.json")
247
+ try:
248
+ with open(json_path, 'w') as f:
249
+ json.dump(original_det_data, f, indent=2)
250
+ print(f" Saved detection JSON: {filename_base}_det_seg.json")
251
+ except Exception as json_error:
252
+ print(f" JSON save error: {json_error}")
253
+ # Try to save a simplified version
254
+ try:
255
+ simplified_data = {
256
+ 'image_id': original_det_data.get('image_id', 'unknown'),
257
+ 'instances': []
258
+ }
259
+ for instance in original_det_data.get('instances', []):
260
+ simplified_instance = {
261
+ 'category': instance.get('category', 'unknown'),
262
+ 'score': float(instance.get('score', 0.0)),
263
+ 'bbox': [float(x) for x in instance.get('bbox', [])],
264
+ 'area': float(instance.get('area', 0.0))
265
+ }
266
+ simplified_data['instances'].append(simplified_instance)
267
+
268
+ with open(json_path, 'w') as f:
269
+ json.dump(simplified_data, f, indent=2)
270
+ print(f" Saved simplified detection JSON: {filename_base}_det_seg.json")
271
+ except Exception as e2:
272
+ print(f" Failed to save even simplified JSON: {e2}")
273
+
274
+ except Exception as e:
275
+ print(f" Error visualizing detection data: {e}")
276
+
277
+ def visualize_parquet_examples(input_file, output_dir, max_rows=None):
278
+ """
279
+ Visualize examples from a parquet file by saving each row as separate files.
280
+ Saves images as PNG and text data as TXT files with uid_column_name naming format.
281
+
282
+ Args:
283
+ input_file (str): Path to the input parquet file
284
+ output_dir (str): Directory to save the visualization files
285
+ max_rows (int): Maximum number of rows to process (None for all rows)
286
+ """
287
+
288
+ # Create output directory if it doesn't exist
289
+ os.makedirs(output_dir, exist_ok=True)
290
+
291
+ print(f"Processing {input_file}...")
292
+
293
+ # Read the parquet file
294
+ df = pd.read_parquet(input_file)
295
+ print(f"Total rows in file: {len(df)}")
296
+ print(f"Columns: {list(df.columns)}")
297
+
298
+ # Limit rows if max_rows is specified
299
+ if max_rows is not None:
300
+ df = df.head(max_rows)
301
+ print(f"Processing only first {max_rows} rows")
302
+
303
+ # Display first few rows to understand the data structure
304
+ print("\nFirst few rows:")
305
+ print(df.head(3))
306
+
307
+ # Process each row
308
+ for index, row in df.iterrows():
309
+ print(f"Processing row {index}...")
310
+
311
+ filename_base = None
312
+
313
+ # Process each column in the row
314
+ for col_name, value in row.items():
315
+ if col_name == 'uid':
316
+ filename_base = value
317
+ elif col_name in ['rgb', 'depth', 'normal'] and filename_base is not None:
318
+ # Handle image data (binary)
319
+ if value is not None and isinstance(value, bytes):
320
+ try:
321
+ # Convert binary data to PIL Image
322
+ image = Image.open(io.BytesIO(value))
323
+
324
+ # Create filename
325
+ filename = f"{filename_base}_{col_name}.png"
326
+ filepath = os.path.join(output_dir, filename)
327
+
328
+ # Save as PNG
329
+ image.save(filepath, 'PNG')
330
+ print(f" Saved {col_name} image: {filename}")
331
+
332
+ except Exception as e:
333
+ print(f" Error processing {col_name} image: {e}")
334
+
335
+ elif col_name == 'caption' and filename_base is not None:
336
+ # Handle text data
337
+ if value is not None:
338
+ try:
339
+ filename = f"{filename_base}_captions.txt"
340
+ filepath = os.path.join(output_dir, filename)
341
+
342
+ # Save text data
343
+ with open(filepath, 'w', encoding='utf-8') as f:
344
+ if isinstance(value, str):
345
+ f.write(value)
346
+ else:
347
+ f.write(str(value))
348
+ print(f" Saved caption: {filename}")
349
+
350
+ except Exception as e:
351
+ print(f" Error processing caption: {e}")
352
+ elif col_name == 'det_seg' and filename_base is not None:
353
+ # Handle detection maps (JSON format)
354
+ if value is not None:
355
+ try:
356
+ # Get the RGB image for visualization
357
+ rgb_image = None
358
+ if 'rgb' in row and row['rgb'] is not None:
359
+ rgb_image = Image.open(io.BytesIO(row['rgb']))
360
+
361
+ # Visualize detection data with bounding boxes and masks
362
+ if rgb_image is not None:
363
+ visualize_detection_data(value, rgb_image, output_dir, filename_base)
364
+ else:
365
+ # If no RGB image, just save the JSON
366
+ filename = f"{filename_base}_det_seg.json"
367
+ filepath = os.path.join(output_dir, filename)
368
+ with open(filepath, 'w') as f:
369
+ serializable_data = make_json_serializable(value)
370
+ json.dump(serializable_data, f, indent=2)
371
+ print(f" Saved detection JSON: {filename}")
372
+
373
+ except Exception as e:
374
+ print(f" Error processing detection data: {e}")
375
+
376
+
377
+
378
+ print(f"\nVisualization completed! Files saved to: {output_dir}")
379
+
380
+ # List the created files
381
+ created_files = os.listdir(output_dir)
382
+ print(f"Created {len(created_files)} files:")
383
+ for file in sorted(created_files)[:10]: # Show first 10 files
384
+ print(f" {file}")
385
+ if len(created_files) > 10:
386
+ print(f" ... and {len(created_files) - 10} more files")
387
+
388
+ def main():
389
+ # Configuration
390
+ input_file = "./datasets/blip3o/parquet_rgb_caption_depth_normal_det_seg/sa_000002.parquet"
391
+ output_dir = "./datasets/blip3o/parquet_rgb_caption_depth_normal_det_seg_visualize"
392
+ max_rows = 30 # Process only first 30 rows
393
+
394
+ # Check if input file exists
395
+ if not os.path.exists(input_file):
396
+ print(f"Error: Input file {input_file} not found!")
397
+ return
398
+
399
+ # Create output directory
400
+ os.makedirs(output_dir, exist_ok=True)
401
+
402
+ # Visualize the parquet examples
403
+ visualize_parquet_examples(input_file, output_dir, max_rows)
404
+
405
+ print("\nVisualization completed successfully!")
406
+
407
+ if __name__ == "__main__":
408
+ main()
data/any2any_preprocess/process_grounding.py ADDED
@@ -0,0 +1,346 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import tarfile
3
+ import json
4
+ import glob
5
+ import io
6
+ from tqdm import tqdm
7
+ import logging
8
+
9
+ # Set up logging
10
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
11
+ logger = logging.getLogger(__name__)
12
+
13
+ class GroundingProcessor:
14
+ def __init__(self, base_dir):
15
+ self.base_dir = base_dir
16
+ self.rgb_caption_dir = os.path.join(base_dir, 'datasets/blip3o/datasets')
17
+ self.grounding_source_dir = os.path.join(base_dir, 'datasets/glamm')
18
+ self.grounding_output_dir = os.path.join(base_dir, 'datasets/blip3o/datasets_grounding')
19
+ self.grounding_cache_dir = os.path.join(base_dir, 'datasets/blip3o/grounding_cache')
20
+
21
+ # Create output directories if they don't exist
22
+ os.makedirs(self.grounding_output_dir, exist_ok=True)
23
+ os.makedirs(self.grounding_cache_dir, exist_ok=True)
24
+
25
+ def get_rgb_tar_files(self):
26
+ """Get all RGB tar files from datasets directory"""
27
+ tar_files = glob.glob(os.path.join(self.rgb_caption_dir, '*.tar'))
28
+ return sorted(tar_files)
29
+
30
+ def get_grounding_tar_files(self):
31
+ """Get all grounding tar files from part directories"""
32
+ grounding_tars = []
33
+ for part in ['part_1', 'part_2', 'part_3', 'part_4']:
34
+ part_dir = os.path.join(self.grounding_source_dir, part)
35
+ if os.path.exists(part_dir):
36
+ part_tars = glob.glob(os.path.join(part_dir, '*.tar.gz'))
37
+ grounding_tars.extend(part_tars)
38
+ return sorted(grounding_tars)
39
+
40
+ def extract_uids_from_rgb_tar(self, tar_path):
41
+ """Extract all UIDs from RGB tar file"""
42
+ uids = set()
43
+
44
+ try:
45
+ with tarfile.open(tar_path, 'r') as tar:
46
+ for member in tar.getmembers():
47
+ if member.isfile():
48
+ filename = member.name
49
+ uid = os.path.splitext(filename)[0] # Remove extension
50
+ uids.add(uid)
51
+ except Exception as e:
52
+ logger.error(f"Error extracting UIDs from {tar_path}: {e}")
53
+ return set()
54
+
55
+ return uids
56
+
57
+ def build_grounding_index(self):
58
+ """Build an index of all grounding data: uid -> (tar_path, member_name)"""
59
+ logger.info("Building grounding data index...")
60
+ grounding_index = {}
61
+
62
+ grounding_tars = self.get_grounding_tar_files()
63
+
64
+ for tar_path in tqdm(grounding_tars, desc="Indexing grounding tars"):
65
+ try:
66
+ with tarfile.open(tar_path, 'r') as tar:
67
+ for member in tar.getmembers():
68
+ if member.isfile() and member.name.endswith('.json'):
69
+ uid = os.path.splitext(member.name)[0]
70
+ grounding_index[uid] = (tar_path, member.name)
71
+ except Exception as e:
72
+ logger.warning(f"Error indexing {tar_path}: {e}")
73
+ continue
74
+
75
+ logger.info(f"Built index with {len(grounding_index)} grounding entries")
76
+ return grounding_index
77
+
78
+ def pre_extract_all_grounding_data_to_disk(self):
79
+ """Pre-extract all grounding data to disk cache for fastest access"""
80
+ logger.info("Pre-extracting all grounding data to disk cache...")
81
+
82
+ grounding_tars = self.get_grounding_tar_files()
83
+ cache_index = {}
84
+
85
+ for tar_path in tqdm(grounding_tars, desc="Pre-extracting grounding data to disk"):
86
+ try:
87
+ with tarfile.open(tar_path, 'r') as tar:
88
+ for member in tar.getmembers():
89
+ if member.isfile() and member.name.endswith('.json'):
90
+ uid = os.path.splitext(member.name)[0]
91
+
92
+ # Extract JSON content
93
+ f = tar.extractfile(member)
94
+ if f is not None:
95
+ content = f.read()
96
+ json_str = content.decode('utf-8')
97
+ json_data = json.loads(json_str)
98
+
99
+ # Save to disk cache
100
+ cache_file = os.path.join(self.grounding_cache_dir, f"{uid}.json")
101
+ with open(cache_file, 'w') as cache_f:
102
+ json.dump(json_data, cache_f, indent=2)
103
+
104
+ cache_index[uid] = cache_file
105
+ except Exception as e:
106
+ logger.warning(f"Error pre-extracting from {tar_path}: {e}")
107
+ continue
108
+
109
+ # Save index file
110
+ index_file = os.path.join(self.grounding_cache_dir, "index.json")
111
+ with open(index_file, 'w') as f:
112
+ json.dump(cache_index, f, indent=2)
113
+
114
+ logger.info(f"Pre-extracted {len(cache_index)} grounding entries to disk cache")
115
+ return cache_index
116
+
117
+ def load_grounding_cache_index(self):
118
+ """Load the grounding cache index"""
119
+ index_file = os.path.join(self.grounding_cache_dir, "index.json")
120
+ if os.path.exists(index_file):
121
+ with open(index_file, 'r') as f:
122
+ return json.load(f)
123
+ return {}
124
+
125
+ def load_grounding_data_from_cache(self, uids):
126
+ """Load grounding data from disk cache for specific UIDs"""
127
+ uid_to_grounding = {}
128
+
129
+ for uid in uids:
130
+ cache_file = os.path.join(self.grounding_cache_dir, f"{uid}.json")
131
+ if os.path.exists(cache_file):
132
+ try:
133
+ with open(cache_file, 'r') as f:
134
+ json_data = json.load(f)
135
+ uid_to_grounding[uid] = json_data
136
+ except Exception as e:
137
+ logger.warning(f"Error loading {cache_file}: {e}")
138
+ continue
139
+
140
+ return uid_to_grounding
141
+
142
+ def find_grounding_data_for_uids_fast(self, uids, grounding_data):
143
+ """Find grounding JSON data for given UIDs using pre-extracted data - FASTEST"""
144
+ uid_to_grounding = {}
145
+
146
+ for uid in uids:
147
+ if uid in grounding_data:
148
+ uid_to_grounding[uid] = grounding_data[uid]
149
+
150
+ return uid_to_grounding
151
+
152
+ def find_grounding_data_for_uids(self, uids, grounding_index):
153
+ """Find grounding JSON data for given UIDs using pre-built index with batch processing"""
154
+ uid_to_grounding = {}
155
+
156
+ # Group UIDs by tar_path to minimize tar file opens
157
+ tar_to_uids = {}
158
+ for uid in uids:
159
+ if uid in grounding_index:
160
+ tar_path, member_name = grounding_index[uid]
161
+ if tar_path not in tar_to_uids:
162
+ tar_to_uids[tar_path] = []
163
+ tar_to_uids[tar_path].append((uid, member_name))
164
+
165
+ # Process each tar file once, extracting all needed UIDs
166
+ for tar_path, uid_members in tar_to_uids.items():
167
+ try:
168
+ with tarfile.open(tar_path, 'r') as tar:
169
+ for uid, member_name in uid_members:
170
+ try:
171
+ member = tar.getmember(member_name)
172
+ f = tar.extractfile(member)
173
+ if f is not None:
174
+ content = f.read()
175
+ json_str = content.decode('utf-8')
176
+ json_data = json.loads(json_str)
177
+ uid_to_grounding[uid] = json_data
178
+ except Exception as e:
179
+ logger.warning(f"Error extracting {uid} from {tar_path}: {e}")
180
+ continue
181
+ except Exception as e:
182
+ logger.warning(f"Error opening {tar_path}: {e}")
183
+ continue
184
+
185
+ return uid_to_grounding
186
+
187
+ def create_grounding_tar(self, rgb_tar_path, uid_to_grounding):
188
+ """Create a grounding tar file with the same structure as RGB tar"""
189
+
190
+ # Extract base name (e.g., 'sa_000000' from 'sa_000000.tar')
191
+ base_name = os.path.splitext(os.path.basename(rgb_tar_path))[0]
192
+ output_tar_path = os.path.join(self.grounding_output_dir, f"{base_name}.tar")
193
+
194
+ logger.info(f"Creating grounding tar: {output_tar_path}")
195
+
196
+ try:
197
+ with tarfile.open(output_tar_path, 'w') as output_tar:
198
+ for uid, grounding_data in uid_to_grounding.items():
199
+ # Create JSON content
200
+ json_str = json.dumps(grounding_data, indent=2)
201
+ json_bytes = json_str.encode('utf-8')
202
+
203
+ # Add to tar with same naming convention as RGB
204
+ tarinfo = tarfile.TarInfo(name=f"{uid}.json")
205
+ tarinfo.size = len(json_bytes)
206
+ output_tar.addfile(tarinfo, io.BytesIO(json_bytes))
207
+
208
+ logger.info(f"Created grounding tar with {len(uid_to_grounding)} UIDs: {output_tar_path}")
209
+ return output_tar_path
210
+
211
+ except Exception as e:
212
+ logger.error(f"Error creating grounding tar {output_tar_path}: {e}")
213
+ return None
214
+
215
+ def process_single_rgb_tar(self, rgb_tar_path, grounding_data=None, grounding_index=None, use_cache=False):
216
+ """Process a single RGB tar file and create corresponding grounding tar"""
217
+
218
+ # Extract base name
219
+ base_name = os.path.splitext(os.path.basename(rgb_tar_path))[0]
220
+
221
+ logger.info(f"Processing {base_name}")
222
+
223
+ # Extract UIDs from RGB tar
224
+ uids = self.extract_uids_from_rgb_tar(rgb_tar_path)
225
+
226
+ if not uids:
227
+ logger.warning(f"No UIDs found in {rgb_tar_path}")
228
+ return None
229
+
230
+ logger.info(f"Found {len(uids)} UIDs in {base_name}")
231
+
232
+ # Find grounding data for these UIDs
233
+ if use_cache:
234
+ # Use disk cache (fastest and memory efficient)
235
+ uid_to_grounding = self.load_grounding_data_from_cache(uids)
236
+ elif grounding_data is not None:
237
+ # Use pre-extracted data (fastest)
238
+ uid_to_grounding = self.find_grounding_data_for_uids_fast(uids, grounding_data)
239
+ elif grounding_index is not None:
240
+ # Use index-based lookup (faster than original)
241
+ uid_to_grounding = self.find_grounding_data_for_uids(uids, grounding_index)
242
+ else:
243
+ logger.error("No grounding data or index provided")
244
+ return None
245
+
246
+ if not uid_to_grounding:
247
+ logger.warning(f"No grounding data found for UIDs in {base_name}")
248
+ return None
249
+
250
+ logger.info(f"Found grounding data for {len(uid_to_grounding)}/{len(uids)} UIDs")
251
+
252
+ # Create grounding tar file
253
+ output_path = self.create_grounding_tar(rgb_tar_path, uid_to_grounding)
254
+
255
+ return output_path
256
+
257
+ def process_all_rgb_tars(self, use_cache=True, pre_extract_cache=False):
258
+ """Process all RGB tar files and create corresponding grounding tars"""
259
+ rgb_tars = self.get_rgb_tar_files()
260
+
261
+ if not rgb_tars:
262
+ logger.error("No RGB tar files found")
263
+ return
264
+
265
+ logger.info(f"Found {len(rgb_tars)} RGB tar files to process")
266
+
267
+ if use_cache:
268
+ # Check if cache exists
269
+ cache_index = self.load_grounding_cache_index()
270
+
271
+ if not cache_index:
272
+ if pre_extract_cache:
273
+ logger.info("Pre-extracting grounding data to disk cache...")
274
+ self.pre_extract_all_grounding_data_to_disk()
275
+ else:
276
+ logger.error("No cache found. Use --pre_extract_cache to create cache first.")
277
+ return
278
+
279
+ logger.info("Using disk cache method (fastest and memory efficient)")
280
+ processed_files = []
281
+ for rgb_tar in tqdm(rgb_tars, desc="Processing RGB tars"):
282
+ try:
283
+ output_path = self.process_single_rgb_tar(rgb_tar, use_cache=True)
284
+ if output_path:
285
+ processed_files.append(output_path)
286
+ except Exception as e:
287
+ logger.error(f"Error processing {rgb_tar}: {e}")
288
+ else:
289
+ # Use index-based method (faster than original, less memory)
290
+ logger.info("Using index-based method (faster, less memory)")
291
+ grounding_index = self.build_grounding_index()
292
+
293
+ processed_files = []
294
+ for rgb_tar in tqdm(rgb_tars, desc="Processing RGB tars"):
295
+ try:
296
+ output_path = self.process_single_rgb_tar(rgb_tar, grounding_index=grounding_index)
297
+ if output_path:
298
+ processed_files.append(output_path)
299
+ except Exception as e:
300
+ logger.error(f"Error processing {rgb_tar}: {e}")
301
+
302
+ logger.info(f"Successfully processed {len(processed_files)} grounding tar files")
303
+ return processed_files
304
+
305
+ def main():
306
+ import argparse
307
+
308
+ parser = argparse.ArgumentParser(description='Process grounding data')
309
+ parser.add_argument('--use_cache', action='store_true', default=True,
310
+ help='Use disk cache method (fastest and memory efficient)')
311
+ parser.add_argument('--pre_extract_cache', action='store_true',
312
+ help='Pre-extract grounding data to disk cache')
313
+ parser.add_argument('--base_dir', type=str, default='./',
314
+ help='Base directory (default: ./)')
315
+
316
+ args = parser.parse_args()
317
+
318
+ # Set the base directory
319
+ base_dir = args.base_dir
320
+
321
+ # Create processor
322
+ processor = GroundingProcessor(base_dir)
323
+
324
+ # Process all RGB tar files
325
+ processed_files = processor.process_all_rgb_tars(
326
+ use_cache=args.use_cache,
327
+ pre_extract_cache=args.pre_extract_cache
328
+ )
329
+
330
+ if processed_files:
331
+ logger.info(f"Successfully created {len(processed_files)} grounding tar files:")
332
+ for file in processed_files:
333
+ logger.info(f" - {file}")
334
+ else:
335
+ logger.error("No grounding tar files were created")
336
+
337
+ if __name__ == "__main__":
338
+ main()
339
+
340
+
341
+
342
+ # This will take time but only needs to be done once
343
+ # python data/any2any_preprocess/process_grounding.py --pre_extract_cache
344
+
345
+ # This will be extremely fast
346
+ # python data/any2any_preprocess/process_grounding.py --use_cache
data/any2any_preprocess/recompress_normal_jpeg.py ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Recompress the `normal` column of the 16-mod parquet from PNG to JPEG q95.
2
+
3
+ The `normal` column stores raw image bytes (Marigold normal maps, pass-through
4
+ PNG) and accounts for ~65% of on-disk size while being ~6x larger than the
5
+ JPEG-encoded `rgb` column. Re-encoding normal as JPEG q95 shrinks it to
6
+ roughly rgb size with negligible quality loss.
7
+
8
+ Every other column is passed through untouched; the Arrow schema is identical
9
+ to the source, so downstream dataloader / HF viewer see no structural change.
10
+
11
+ Usage:
12
+ python recompress_normal_jpeg.py --out_dir <new_folder> file1.parquet [file2 ...]
13
+ """
14
+ import argparse
15
+ import io
16
+ import os
17
+
18
+ import pyarrow as pa
19
+ import pyarrow.parquet as pq
20
+ from PIL import Image
21
+
22
+ JPEG_QUALITY = 95
23
+
24
+
25
+ def recompress_normal_bytes(b):
26
+ if b is None:
27
+ return None
28
+ if isinstance(b, memoryview):
29
+ b = b.tobytes()
30
+ img = Image.open(io.BytesIO(b)).convert('RGB')
31
+ out = io.BytesIO()
32
+ img.save(out, format='JPEG', quality=JPEG_QUALITY)
33
+ return out.getvalue()
34
+
35
+
36
+ def process_file(src, out_dir):
37
+ dst = os.path.join(out_dir, os.path.basename(src))
38
+ pf = pq.ParquetFile(src)
39
+ schema = pf.schema_arrow
40
+ assert 'normal' in schema.names, f'no normal column in {src}'
41
+ norm_idx = schema.names.index('normal')
42
+
43
+ reported_fmt = False
44
+ writer = pq.ParquetWriter(dst, schema, compression='snappy')
45
+ try:
46
+ for rg in range(pf.num_row_groups):
47
+ table = pf.read_row_group(rg)
48
+ normal_col = table.column('normal').to_pylist()
49
+
50
+ if not reported_fmt:
51
+ for b in normal_col:
52
+ if b is not None:
53
+ bb = b.tobytes() if isinstance(b, memoryview) else b
54
+ fmt = Image.open(io.BytesIO(bb)).format
55
+ print(f' source normal format = {fmt}', flush=True)
56
+ reported_fmt = True
57
+ break
58
+
59
+ new_normal = [recompress_normal_bytes(b) for b in normal_col]
60
+ new_col = pa.array(new_normal, type=schema.field('normal').type)
61
+ table = table.set_column(norm_idx, schema.field('normal'), new_col)
62
+ writer.write_table(table)
63
+ finally:
64
+ writer.close()
65
+
66
+ s0, s1 = os.path.getsize(src), os.path.getsize(dst)
67
+ print(f' {os.path.basename(src)}: {s0/1e9:.2f} GB -> {s1/1e9:.2f} GB '
68
+ f'({100*(1-s1/s0):.0f}% smaller)', flush=True)
69
+ return s0, s1
70
+
71
+
72
+ def main():
73
+ ap = argparse.ArgumentParser()
74
+ ap.add_argument('--out_dir', required=True)
75
+ ap.add_argument('files', nargs='+')
76
+ args = ap.parse_args()
77
+ os.makedirs(args.out_dir, exist_ok=True)
78
+
79
+ tot0 = tot1 = 0
80
+ for f in args.files:
81
+ print(f'[{os.path.basename(f)}]', flush=True)
82
+ s0, s1 = process_file(f, args.out_dir)
83
+ tot0 += s0
84
+ tot1 += s1
85
+ print(f'TOTAL: {tot0/1e9:.2f} GB -> {tot1/1e9:.2f} GB '
86
+ f'({100*(1-tot1/tot0):.0f}% smaller)', flush=True)
87
+
88
+
89
+ if __name__ == '__main__':
90
+ main()
data/any2any_preprocess/run_full_rebuild.py ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Slurm driver: rebuild a strided slice of the full dataset.
2
+
3
+ Robust against memory leaks: each small chunk of files is processed by a FRESH
4
+ subprocess (build_full_release.py --workers 1) that exits afterwards, so the OS
5
+ reclaims all memory β€” no accumulation across the run. `concurrency` such
6
+ subprocesses run at once. Resumable via process_one's skip.
7
+ """
8
+ import argparse
9
+ import os
10
+ import subprocess
11
+ import sys
12
+ from concurrent.futures import ThreadPoolExecutor, as_completed
13
+
14
+ SRC = ('datasets/blip3o/parquet_rgb_caption_depth_normal_det_seg_grounding'
15
+ '_canny_dino_global_clip448_imagebind_samseg_samedge_cocodet')
16
+ OUT = 'datasets/blip3o/modus_full'
17
+ WORKER = 'data/any2any_preprocess/build_full_release.py'
18
+ CHUNK = 4
19
+
20
+
21
+ def config_of(fname):
22
+ if fname.startswith('sa_'):
23
+ return 'sa1b'
24
+ if fname.startswith('webdataset_JDB_'):
25
+ return 'journeydb'
26
+ if fname.startswith('webdataset_shard_'):
27
+ return 'cc12m'
28
+ return None
29
+
30
+
31
+ def run_chunk(cfg, files):
32
+ cmd = [sys.executable, WORKER, '--config', cfg,
33
+ '--out_dir', os.path.join(OUT, cfg), '--workers', '1'] + files
34
+ r = subprocess.run(cmd, capture_output=True, text=True)
35
+ return cfg, len(files), r.returncode, (r.stderr or '')[-300:]
36
+
37
+
38
+ def main():
39
+ ap = argparse.ArgumentParser()
40
+ ap.add_argument('--num-tasks', type=int, required=True)
41
+ ap.add_argument('--task-id', type=int, required=True)
42
+ ap.add_argument('--concurrency', type=int, default=48)
43
+ args = ap.parse_args()
44
+
45
+ for cfg in ('sa1b', 'journeydb', 'cc12m'):
46
+ os.makedirs(os.path.join(OUT, cfg), exist_ok=True)
47
+
48
+ items = []
49
+ for fn in sorted(os.listdir(SRC)):
50
+ if fn.endswith('.parquet') and config_of(fn):
51
+ items.append((config_of(fn), os.path.join(SRC, fn)))
52
+ mine = items[args.task_id::args.num_tasks]
53
+
54
+ # group into per-config chunks
55
+ chunks = []
56
+ bycfg = {}
57
+ for cfg, src in mine:
58
+ bycfg.setdefault(cfg, []).append(src)
59
+ for cfg, fs in bycfg.items():
60
+ for i in range(0, len(fs), CHUNK):
61
+ chunks.append((cfg, fs[i:i + CHUNK]))
62
+
63
+ print(f'task {args.task_id}/{args.num_tasks}: {len(mine)} files in '
64
+ f'{len(chunks)} chunks, concurrency={args.concurrency}', flush=True)
65
+ done_files = fail = 0
66
+ with ThreadPoolExecutor(max_workers=args.concurrency) as ex:
67
+ futs = [ex.submit(run_chunk, cfg, fs) for cfg, fs in chunks]
68
+ for i, fu in enumerate(as_completed(futs), 1):
69
+ cfg, n, rc, err = fu.result()
70
+ if rc == 0:
71
+ done_files += n
72
+ else:
73
+ fail += n
74
+ print(f' CHUNK FAIL {cfg} rc={rc}: {err}', flush=True)
75
+ if i % 20 == 0:
76
+ print(f'[{i}/{len(chunks)} chunks] files_ok={done_files} '
77
+ f'fail={fail}', flush=True)
78
+ print(f'TASK COMPLETE: {done_files} files ok, {fail} failed', flush=True)
79
+
80
+
81
+ if __name__ == '__main__':
82
+ main()
data/any2any_preprocess/upload_full.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Resumable upload of the full-release folder to the HF dataset repo.
2
+
3
+ Uses upload_large_folder (xet, multi-commit, resumable). Uploads folder_path
4
+ contents to the repo root, preserving the sa1b/ journeydb/ cc12m/ structure.
5
+ Re-running skips already-uploaded files.
6
+
7
+ Usage:
8
+ HF_TOKEN_FILE=/users/mye/.hf_token python upload_full.py <repo_id> <folder> [allow_glob]
9
+ """
10
+ import os
11
+ import sys
12
+
13
+ from huggingface_hub import HfApi
14
+
15
+ repo_id = sys.argv[1]
16
+ folder = sys.argv[2]
17
+ allow = list(sys.argv[3:]) # zero or more allow_patterns
18
+ token = open(os.environ['HF_TOKEN_FILE']).read().strip()
19
+
20
+ api = HfApi(token=token)
21
+ api.create_repo(repo_id, repo_type='dataset', exist_ok=True)
22
+ kw = dict(repo_id=repo_id, repo_type='dataset', folder_path=folder, print_report=True)
23
+ if allow:
24
+ kw['allow_patterns'] = allow
25
+ api.upload_large_folder(**kw)
26
+ print(f'UPLOAD PASS DONE: {folder} {allow} -> {repo_id}', flush=True)
data/any2any_preprocess/vqa_convert_parquet_to_jsonl.py ADDED
@@ -0,0 +1,312 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Convert LLaVA OneVision parquet files to JSONL format matching llava_ov_si.jsonl
4
+
5
+ Single-folder usage (backwards compatible):
6
+ python vqa_convert_parquet_to_jsonl.py <parquet_dir> <output_jsonl> --image-dir <image_dir> [--max-rows N]
7
+
8
+ Multi-folder usage (process all immediate subfolders of <parquet_root>):
9
+ python vqa_convert_parquet_to_jsonl.py <parquet_root> dummy.jsonl --process-all --output-root <output_root> [--max-rows N]
10
+
11
+ Notes:
12
+ - In multi-folder mode, outputs are written per subfolder under <output_root>/<subfolder>/{images,label/labels.jsonl}
13
+ - The script prints the sorted folder index and name, and a global image counter every N (default 1000)
14
+ """
15
+
16
+ import argparse
17
+ import json
18
+ import os
19
+ import glob
20
+ from pathlib import Path
21
+ import pandas as pd
22
+ from PIL import Image
23
+ import io
24
+ from tqdm import tqdm
25
+ def _save_pil_image_as_jpeg(pil_image: Image.Image, image_path: str):
26
+ """
27
+ Ensure the PIL image is compatible with JPEG (no alpha), then save.
28
+ """
29
+ try:
30
+ # Convert to RGB if image has alpha channel or unsupported mode for JPEG
31
+ if pil_image.mode not in ("RGB", "L"):
32
+ pil_image = pil_image.convert("RGB")
33
+ pil_image.save(image_path, format="JPEG")
34
+ except Exception:
35
+ # As a fallback, force convert to RGB and retry
36
+ pil_image = pil_image.convert("RGB")
37
+ pil_image.save(image_path, format="JPEG")
38
+
39
+
40
+
41
+ def convert_parquet_to_jsonl(
42
+ parquet_dir: str,
43
+ output_jsonl: str,
44
+ image_dir: str = None,
45
+ max_rows: int = None,
46
+ sample_id_offset: int = 0,
47
+ progress: dict | None = None,
48
+ ):
49
+ """
50
+ Convert parquet files to JSONL format compatible with llava_ov_si.jsonl
51
+
52
+ Args:
53
+ parquet_dir: Directory containing parquet files (can be subdirectory)
54
+ output_jsonl: Output JSONL file path
55
+ image_dir: Directory to save images (optional, if None saves with dataset)
56
+ max_rows: Maximum number of rows to process (for testing)
57
+ sample_id_offset: Starting ID offset (for merging multiple datasets)
58
+ progress: Optional dict to track global progress across folders. Expected keys:
59
+ {"count": int, "next_milestone": int, "interval": int}
60
+ """
61
+
62
+ # Find all parquet files in the directory
63
+ parquet_pattern = os.path.join(parquet_dir, "train-*.parquet")
64
+ parquet_files = sorted(glob.glob(parquet_pattern))
65
+
66
+ if not parquet_files:
67
+ print(f"Error: No parquet files found in {parquet_dir}")
68
+ return
69
+
70
+ print(f"Found {len(parquet_files)} parquet file(s)")
71
+
72
+ # Create image directory if specified
73
+ if image_dir:
74
+ os.makedirs(image_dir, exist_ok=True)
75
+
76
+ all_entries = []
77
+ current_id = sample_id_offset
78
+
79
+ for parquet_file in parquet_files:
80
+ print(f"\nProcessing {parquet_file}...")
81
+ df = pd.read_parquet(parquet_file)
82
+
83
+ for idx, row in tqdm(df.iterrows(), total=len(df)):
84
+ if max_rows and len(all_entries) >= max_rows:
85
+ break
86
+
87
+ # Extract conversations - handle numpy array conversion
88
+ conversations = row["conversations"]
89
+ if hasattr(conversations, "tolist"):
90
+ conversations = conversations.tolist()
91
+
92
+ # Handle image - extract from binary data and save
93
+ image_filename = None
94
+ if row["image"] is not None:
95
+ try:
96
+ # The image data is stored as binary bytes
97
+ image_data = row["image"]
98
+
99
+ # Generate filename from ID
100
+ image_filename = f"{current_id}.jpg"
101
+
102
+ if image_dir:
103
+ image_path = os.path.join(image_dir, image_filename)
104
+ else:
105
+ # Save in same directory as parquet file
106
+ image_dir_local = os.path.dirname(parquet_file)
107
+ image_path = os.path.join(
108
+ image_dir_local, image_filename
109
+ )
110
+
111
+ # Convert image data to PIL Image and save
112
+ if isinstance(image_data, dict):
113
+ # Image is stored as a dict with 'bytes' key containing image data
114
+ if (
115
+ "bytes" in image_data
116
+ and image_data["bytes"] is not None
117
+ ):
118
+ image_bytes = image_data["bytes"]
119
+ pil_image = Image.open(io.BytesIO(image_bytes))
120
+ _save_pil_image_as_jpeg(pil_image, image_path)
121
+ else:
122
+ print(
123
+ f"Warning: Dict image data doesn't contain 'bytes' key for row {idx}"
124
+ )
125
+ continue
126
+ elif isinstance(image_data, bytes):
127
+ # Try to open as PIL Image from bytes
128
+ pil_image = Image.open(io.BytesIO(image_data))
129
+ _save_pil_image_as_jpeg(pil_image, image_path)
130
+ elif isinstance(image_data, str):
131
+ # If it's a string, try to decode it
132
+ image_bytes = image_data.encode(
133
+ "latin-1"
134
+ ) # Convert string to bytes
135
+ pil_image = Image.open(io.BytesIO(image_bytes))
136
+ _save_pil_image_as_jpeg(pil_image, image_path)
137
+ else:
138
+ print(
139
+ f"Warning: Unknown image data type {type(image_data)} for row {idx}"
140
+ )
141
+ continue
142
+
143
+ except Exception as e:
144
+ print(f"Error processing image for row {idx}: {e}")
145
+ continue
146
+
147
+ # Create JSONL entry matching llava_ov_si.jsonl format
148
+ entry = {
149
+ "id": current_id,
150
+ "image": image_filename,
151
+ "conversations": conversations,
152
+ }
153
+
154
+ all_entries.append(entry)
155
+ current_id += 1
156
+
157
+ # Update global progress if provided
158
+ if progress is not None:
159
+ progress["count"] += 1
160
+ if progress["count"] >= progress.get("next_milestone", 1000):
161
+ print(
162
+ f"[global] Processed {progress['count']} images total"
163
+ )
164
+ interval = progress.get("interval", 1000)
165
+ progress["next_milestone"] = progress["count"] + interval
166
+
167
+ if max_rows and len(all_entries) >= max_rows:
168
+ break
169
+
170
+ if max_rows and len(all_entries) >= max_rows:
171
+ break
172
+
173
+ # Write JSONL file
174
+ print(f"\nWriting {len(all_entries)} entries to {output_jsonl}...")
175
+ with open(output_jsonl, "w", encoding="utf-8") as f:
176
+ for entry in all_entries:
177
+ f.write(json.dumps(entry, ensure_ascii=False) + "\n")
178
+
179
+ print(f"βœ“ Converted {len(all_entries)} samples to {output_jsonl}")
180
+ if image_dir:
181
+ print(f"βœ“ Images saved to {image_dir}")
182
+
183
+
184
+ def main():
185
+ parser = argparse.ArgumentParser(
186
+ description="Convert LLaVA OneVision parquet files to JSONL format"
187
+ )
188
+ parser.add_argument(
189
+ "parquet_dir",
190
+ help="Directory containing parquet files, or a root directory with subfolders",
191
+ )
192
+ parser.add_argument("output_jsonl", help="Output JSONL file path (single-folder mode)")
193
+ parser.add_argument(
194
+ "--image-dir",
195
+ default=None,
196
+ help="Directory to save images (default: same as parquet directory)",
197
+ )
198
+ parser.add_argument(
199
+ "--max-rows",
200
+ type=int,
201
+ default=None,
202
+ help="Maximum number of rows to process (for testing)",
203
+ )
204
+ parser.add_argument(
205
+ "--id-offset",
206
+ type=int,
207
+ default=0,
208
+ help="Starting ID offset (for merging datasets)",
209
+ )
210
+ parser.add_argument(
211
+ "--process-all",
212
+ action="store_true",
213
+ help="Process all immediate subfolders in parquet_dir and write per-folder outputs under --output-root",
214
+ )
215
+ parser.add_argument(
216
+ "--output-root",
217
+ default=None,
218
+ help="Base output directory when using --process-all",
219
+ )
220
+ parser.add_argument(
221
+ "--print-every",
222
+ type=int,
223
+ default=1000,
224
+ help="Global image progress interval for multi-folder mode",
225
+ )
226
+ parser.add_argument(
227
+ "--start-index",
228
+ type=int,
229
+ default=1,
230
+ help="1-based index of subfolder to start processing from (resume)",
231
+ )
232
+
233
+ args = parser.parse_args()
234
+
235
+ if args.process_all:
236
+ if not args.output_root:
237
+ raise SystemExit("--output-root is required when using --process-all")
238
+
239
+ # Discover immediate subfolders and sort
240
+ if not os.path.isdir(args.parquet_dir):
241
+ raise SystemExit(f"Input root '{args.parquet_dir}' is not a directory")
242
+
243
+ subfolders = [
244
+ d
245
+ for d in sorted(os.listdir(args.parquet_dir))
246
+ if os.path.isdir(os.path.join(args.parquet_dir, d))
247
+ ]
248
+
249
+ # Filter to only those having parquet files
250
+ filtered_subfolders = []
251
+ for d in subfolders:
252
+ pattern = os.path.join(args.parquet_dir, d, "train-*.parquet")
253
+ if glob.glob(pattern):
254
+ filtered_subfolders.append(d)
255
+ subfolders = filtered_subfolders
256
+
257
+ total = len(subfolders)
258
+ print(f"Found {total} subfolder(s) to process under {args.parquet_dir}")
259
+
260
+ # Compute starting index (1-based) and slice subfolders for resume
261
+ start_index = max(1, int(args.start_index)) if args.start_index else 1
262
+ if start_index > total:
263
+ print(f"Start index {start_index} is beyond total {total}; nothing to do.")
264
+ return
265
+ subfolders_to_process = subfolders[start_index - 1 :]
266
+
267
+ # Prepare global progress
268
+ progress = {
269
+ "count": 0,
270
+ "next_milestone": args.print_every,
271
+ "interval": args.print_every,
272
+ }
273
+ # When resuming at folder index 41, hardcode the global counter baseline
274
+ if start_index == 41:
275
+ progress["count"] = 1380000
276
+ progress["next_milestone"] = progress["count"] + progress["interval"]
277
+
278
+ for idx, sub in enumerate(subfolders_to_process, start=start_index):
279
+ print(f"\n[{idx}/{total}] Processing folder: {sub}")
280
+ sub_in = os.path.join(args.parquet_dir, sub)
281
+ sub_out_images = os.path.join(args.output_root, sub, "images")
282
+ sub_out_label_dir = os.path.join(args.output_root, sub, "label")
283
+ os.makedirs(sub_out_images, exist_ok=True)
284
+ os.makedirs(sub_out_label_dir, exist_ok=True)
285
+ sub_out_jsonl = os.path.join(sub_out_label_dir, "labels.jsonl")
286
+
287
+ convert_parquet_to_jsonl(
288
+ parquet_dir=sub_in,
289
+ output_jsonl=sub_out_jsonl,
290
+ image_dir=sub_out_images,
291
+ max_rows=args.max_rows,
292
+ sample_id_offset=args.id_offset,
293
+ progress=progress,
294
+ )
295
+ else:
296
+ convert_parquet_to_jsonl(
297
+ parquet_dir=args.parquet_dir,
298
+ output_jsonl=args.output_jsonl,
299
+ image_dir=args.image_dir,
300
+ max_rows=args.max_rows,
301
+ sample_id_offset=args.id_offset,
302
+ )
303
+
304
+
305
+ if __name__ == "__main__":
306
+ main()
307
+
308
+
309
+ # python data/any2any_preprocess/vqa_convert_parquet_to_jsonl.py datasets/llava_onevision/ai2d\(cauldron\,llava_format\)/ datasets/llava_onevision_vqa/label/ai2d_converted.jsonl --image-dir datasets/llava_onevision_vqa/images
310
+
311
+ # python data/any2any_preprocess/vqa_convert_parquet_to_jsonl.py datasets/llava_onevision dummy.jsonl --process-all --output-root datasets/llava_onevision_vqa
312
+ # python data/any2any_preprocess/vqa_convert_parquet_to_jsonl.py datasets/llava_onevision dummy.jsonl --process-all --output-root datasets/llava_onevision_vqa --start-index 41
data/bundled_parquet_info/README.md ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # bundled_parquet_info β€” repo-tracked parquet metadata (auto-fallback)
2
+
3
+ Repo-tracked copies of the parquet-info JSONs used by BAGEL training.
4
+ **The code reads this directory automatically**: `data/dataset_base.py`
5
+ falls back to `data/bundled_parquet_info/<basename>` whenever the
6
+ configured live path (`./datasets/blip3o/parquet_info/…`, a symlink chain
7
+ that only exists on the original cluster) is missing. When the live path
8
+ exists it still wins β€” behavior on the original cluster is unchanged.
9
+
10
+ ## Files
11
+
12
+ | File | Files listed | Rows | Used by |
13
+ |---|---|---|---|
14
+ | `…_grounding_canny_dino_global_clip448_imagebind.json` | 2891 (1000 `sa_*` + 1891 `webdataset_*`) | 29.2M | 13mod: all target groups except grounding (any2rgb / depth / normal / caption / canny / dino / dinolocal / clip / imagebind / imagebindlocal) |
15
+ | `…_grounding2_canny_dino_global_clip448_imagebind.json` | 1000 (`sa_*` only) | 11.1M | 13mod: `unified_any2grounding` only |
16
+ | `…_clip448_imagebind_samseg_samedge_cocodet.json` | β€” | β€” | 16mod runs (adds cocodet / samseg / samedge) |
17
+ | `llava_onevision_vqa.json` | β€” | β€” | VLM-SFT (llava-onevision) |
18
+
19
+ **Why grounding has two JSONs:** grounding annotations (GLaMM phrase+bbox,
20
+ matched by UID) only exist for the SA-1B portion of the parquet set. The
21
+ `webdataset_*` shards have no `grounding` column at all (verified via
22
+ parquet footer schema). So when grounding is the generation TARGET, the
23
+ loader must restrict itself to the `sa_*` files β€” that restricted file
24
+ list is the `grounding2` JSON. Both JSONs point into the SAME `data_dir`.
25
+
26
+ ## Setting up on a new machine
27
+
28
+ 1. Get the parquet data itself (β‰ˆ several TB, NOT in this repo):
29
+ `parquet_rgb_caption_depth_normal_det_seg_grounding_canny_dino_global_clip448_imagebind/`
30
+ (2891 parquet files).
31
+ 2. Place (or symlink) it at
32
+ `datasets/blip3o/parquet_rgb_caption_depth_normal_det_seg_grounding_canny_dino_global_clip448_imagebind/`
33
+ relative to the repo root.
34
+ 3. parquet-info JSONs: **no action needed** β€” the loader automatically
35
+ falls back to this directory when the live
36
+ `datasets/blip3o/parquet_info/` path is absent
37
+ (`data/dataset_base.py`, "Fall back to the copy bundled in the repo").
38
+ 4. Note the JSON keys are relative paths
39
+ (`./datasets/blip3o/parquet_.../sa_000000.parquet`), so always launch
40
+ training from the repo root.
41
+
42
+ If the parquet set ever changes, regenerate with
43
+ `data/any2any_preprocess/generate_parquet_json.py` and refresh these copies.
data/bundled_parquet_info/blip3o_rgb_caption_depth_normal_det_seg_grounding2_canny_dino_global_clip448_imagebind.json ADDED
The diff for this file is too large to render. See raw diff