Spaces:
Running on Zero
Running on Zero
Commit ·
e298226
0
Parent(s):
Initial public release
Browse filesCo-authored-by: claude opus 4.8 (1m context) <claude opus 4.8 (1m context)@users.noreply.huggingface.co>
This view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +86 -0
- .gitignore +6 -0
- .gradio/cached_examples/31/Depth (disparity, magma)/00514d1d228e1970f9a5/image.png +3 -0
- .gradio/cached_examples/31/Depth (disparity, magma)/21da509194782c87b704/image.png +0 -0
- .gradio/cached_examples/31/Depth (disparity, magma)/282b8de47381433b20ba/image.png +3 -0
- .gradio/cached_examples/31/Depth (disparity, magma)/72f252326e89ba602db3/image.png +0 -0
- .gradio/cached_examples/31/Depth (disparity, magma)/77eb75a4ae32a2779670/image.png +3 -0
- .gradio/cached_examples/31/Depth (disparity, magma)/b3a0c160552d198bcca2/image.png +3 -0
- .gradio/cached_examples/31/Interactive 3D point cloud/6351a7c3b27ea01e3670/cloud_45fd4adef349.glb +3 -0
- .gradio/cached_examples/31/Interactive 3D point cloud/a42239d35b0256820e21/cloud_82bf2e710be0.glb +3 -0
- .gradio/cached_examples/31/Interactive 3D point cloud/a600814ba3ef00eacfeb/cloud_9a8d39949ba7.glb +3 -0
- .gradio/cached_examples/31/Interactive 3D point cloud/a6b517c351861e68aa8f/cloud_6f14c0ff2da6.glb +3 -0
- .gradio/cached_examples/31/Interactive 3D point cloud/a93a97926162980d549a/cloud_53c7df1dbc77.glb +3 -0
- .gradio/cached_examples/31/Interactive 3D point cloud/e208d09a8d563ea27323/cloud_a7b919815dba.glb +3 -0
- .gradio/cached_examples/31/RGB image/4763ceeb1dd670d3f927/image.png +3 -0
- .gradio/cached_examples/31/RGB image/530eb79823e527aed6bf/image.png +3 -0
- .gradio/cached_examples/31/RGB image/65c25b8debef84e0027b/image.png +3 -0
- .gradio/cached_examples/31/RGB image/6d51368904340f3a7f23/image.png +3 -0
- .gradio/cached_examples/31/RGB image/91174cfa8f5a5909a013/image.png +3 -0
- .gradio/cached_examples/31/RGB image/d68b76efe33346efd7e3/image.png +3 -0
- .gradio/cached_examples/31/indices.csv +6 -0
- .gradio/cached_examples/31/log.csv +7 -0
- .gradio/cached_examples/32/Depth (disparity, magma)/0891e8543770d0db2151/image.png +0 -0
- .gradio/cached_examples/32/Depth (disparity, magma)/38d56c7397d95d524055/image.png +3 -0
- .gradio/cached_examples/32/Depth (disparity, magma)/9a36d2e382db4e333742/image.png +3 -0
- .gradio/cached_examples/32/Interactive 3D point cloud/674110f2aecaaf5effa8/cloud_f9d62ed7d997.glb +3 -0
- .gradio/cached_examples/32/Interactive 3D point cloud/85ee5283a423852aa42d/cloud_8a6b0d1109ae.glb +3 -0
- .gradio/cached_examples/32/Interactive 3D point cloud/e7f9024213e4656b8beb/cloud_a6225bea9d7e.glb +3 -0
- .gradio/cached_examples/32/RGB image/3c2c4fa7881bb773646d/image.png +3 -0
- .gradio/cached_examples/32/RGB image/87aaba9c524a915404cd/image.png +3 -0
- .gradio/cached_examples/32/RGB image/e64a0aff7688d0635057/image.png +3 -0
- .gradio/cached_examples/32/indices.csv +3 -0
- .gradio/cached_examples/32/log.csv +4 -0
- .ruff_cache/.gitignore +2 -0
- .ruff_cache/0.5.5/12279076348749595670 +0 -0
- .ruff_cache/CACHEDIR.TAG +1 -0
- LICENSE +202 -0
- LICENSE-WEIGHTS +14 -0
- README.md +99 -0
- app.py +642 -0
- assets/examples/alley.png +3 -0
- assets/examples/castle.png +3 -0
- assets/examples/yosemite.png +3 -0
- flux_rgbd/__init__.py +13 -0
- flux_rgbd/_flux2/__init__.py +11 -0
- flux_rgbd/_flux2/autoencoder.py +499 -0
- flux_rgbd/_flux2/constants.py +207 -0
- flux_rgbd/_flux2/model.py +674 -0
- flux_rgbd/depth/__init__.py +8 -0
- flux_rgbd/depth/preprocess.py +123 -0
.gitattributes
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 36 |
+
.gradio/cached_examples/30/Depth[[:space:]](disparity,[[:space:]]magma)/5e96afc67581e463f100/image.png filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
.gradio/cached_examples/30/Depth[[:space:]](disparity,[[:space:]]magma)/9220cd48d295ca353a6b/image.png filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
.gradio/cached_examples/30/Depth[[:space:]](disparity,[[:space:]]magma)/c8838412e5dee9697701/image.png filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
.gradio/cached_examples/30/Depth[[:space:]](disparity,[[:space:]]magma)/f7095c71dfb49f7e3a2a/image.png filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
.gradio/cached_examples/30/Interactive[[:space:]]3D[[:space:]]point[[:space:]]cloud/a2b2ed4415d6c0310a36/cloud_05cfcce5d5e6.glb filter=lfs diff=lfs merge=lfs -text
|
| 41 |
+
.gradio/cached_examples/30/Interactive[[:space:]]3D[[:space:]]point[[:space:]]cloud/b72cecf121cf47496c82/cloud_394dafbecc42.glb filter=lfs diff=lfs merge=lfs -text
|
| 42 |
+
.gradio/cached_examples/30/Interactive[[:space:]]3D[[:space:]]point[[:space:]]cloud/b8c85f926a8d6d09ee32/cloud_dbf7146aa8d6.glb filter=lfs diff=lfs merge=lfs -text
|
| 43 |
+
.gradio/cached_examples/30/Interactive[[:space:]]3D[[:space:]]point[[:space:]]cloud/bb6dcdbd8cf58c73cd2c/cloud_a05855c7716b.glb filter=lfs diff=lfs merge=lfs -text
|
| 44 |
+
.gradio/cached_examples/30/Interactive[[:space:]]3D[[:space:]]point[[:space:]]cloud/c3390d29364b0fab5237/cloud_14b821bf5517.glb filter=lfs diff=lfs merge=lfs -text
|
| 45 |
+
.gradio/cached_examples/30/Interactive[[:space:]]3D[[:space:]]point[[:space:]]cloud/f90705e1df43a796cd63/cloud_41032084d765.glb filter=lfs diff=lfs merge=lfs -text
|
| 46 |
+
.gradio/cached_examples/30/RGB[[:space:]]image/02ab28426d5807f6943a/image.png filter=lfs diff=lfs merge=lfs -text
|
| 47 |
+
.gradio/cached_examples/30/RGB[[:space:]]image/179b01cce83461a9a19a/image.png filter=lfs diff=lfs merge=lfs -text
|
| 48 |
+
.gradio/cached_examples/30/RGB[[:space:]]image/1965504ccdd23a0da632/image.png filter=lfs diff=lfs merge=lfs -text
|
| 49 |
+
.gradio/cached_examples/30/RGB[[:space:]]image/3f5824c31471ef256471/image.png filter=lfs diff=lfs merge=lfs -text
|
| 50 |
+
.gradio/cached_examples/30/RGB[[:space:]]image/7ebf7b4b6db056927956/image.png filter=lfs diff=lfs merge=lfs -text
|
| 51 |
+
.gradio/cached_examples/30/RGB[[:space:]]image/dba880c3bec4afd1a9ff/image.png filter=lfs diff=lfs merge=lfs -text
|
| 52 |
+
.gradio/cached_examples/31/Depth[[:space:]](disparity,[[:space:]]magma)/b8e54b0d5e39f055f164/image.png filter=lfs diff=lfs merge=lfs -text
|
| 53 |
+
.gradio/cached_examples/31/Depth[[:space:]](disparity,[[:space:]]magma)/e3b3eed2144fe2f212bb/image.png filter=lfs diff=lfs merge=lfs -text
|
| 54 |
+
.gradio/cached_examples/31/Interactive[[:space:]]3D[[:space:]]point[[:space:]]cloud/7b2796d4a9ff918e3ff0/cloud_16bed495fda3.glb filter=lfs diff=lfs merge=lfs -text
|
| 55 |
+
.gradio/cached_examples/31/Interactive[[:space:]]3D[[:space:]]point[[:space:]]cloud/d21fe0e76f4b13cc03c4/cloud_b91d63f1da9e.glb filter=lfs diff=lfs merge=lfs -text
|
| 56 |
+
.gradio/cached_examples/31/Interactive[[:space:]]3D[[:space:]]point[[:space:]]cloud/ff979f9418cc0388e99b/cloud_006af054b659.glb filter=lfs diff=lfs merge=lfs -text
|
| 57 |
+
.gradio/cached_examples/31/RGB[[:space:]]image/1a6ed5112df1d233314c/image.png filter=lfs diff=lfs merge=lfs -text
|
| 58 |
+
.gradio/cached_examples/31/RGB[[:space:]]image/c14e4a0757ca88e19135/image.png filter=lfs diff=lfs merge=lfs -text
|
| 59 |
+
.gradio/cached_examples/31/RGB[[:space:]]image/e858eac28e2a39b56e43/image.png filter=lfs diff=lfs merge=lfs -text
|
| 60 |
+
assets/examples/alley.png filter=lfs diff=lfs merge=lfs -text
|
| 61 |
+
assets/examples/castle.png filter=lfs diff=lfs merge=lfs -text
|
| 62 |
+
assets/examples/yosemite.png filter=lfs diff=lfs merge=lfs -text
|
| 63 |
+
.gradio/cached_examples/31/Depth[[:space:]](disparity,[[:space:]]magma)/00514d1d228e1970f9a5/image.png filter=lfs diff=lfs merge=lfs -text
|
| 64 |
+
.gradio/cached_examples/31/Depth[[:space:]](disparity,[[:space:]]magma)/282b8de47381433b20ba/image.png filter=lfs diff=lfs merge=lfs -text
|
| 65 |
+
.gradio/cached_examples/31/Depth[[:space:]](disparity,[[:space:]]magma)/77eb75a4ae32a2779670/image.png filter=lfs diff=lfs merge=lfs -text
|
| 66 |
+
.gradio/cached_examples/31/Depth[[:space:]](disparity,[[:space:]]magma)/b3a0c160552d198bcca2/image.png filter=lfs diff=lfs merge=lfs -text
|
| 67 |
+
.gradio/cached_examples/31/Interactive[[:space:]]3D[[:space:]]point[[:space:]]cloud/6351a7c3b27ea01e3670/cloud_45fd4adef349.glb filter=lfs diff=lfs merge=lfs -text
|
| 68 |
+
.gradio/cached_examples/31/Interactive[[:space:]]3D[[:space:]]point[[:space:]]cloud/a42239d35b0256820e21/cloud_82bf2e710be0.glb filter=lfs diff=lfs merge=lfs -text
|
| 69 |
+
.gradio/cached_examples/31/Interactive[[:space:]]3D[[:space:]]point[[:space:]]cloud/a600814ba3ef00eacfeb/cloud_9a8d39949ba7.glb filter=lfs diff=lfs merge=lfs -text
|
| 70 |
+
.gradio/cached_examples/31/Interactive[[:space:]]3D[[:space:]]point[[:space:]]cloud/a6b517c351861e68aa8f/cloud_6f14c0ff2da6.glb filter=lfs diff=lfs merge=lfs -text
|
| 71 |
+
.gradio/cached_examples/31/Interactive[[:space:]]3D[[:space:]]point[[:space:]]cloud/a93a97926162980d549a/cloud_53c7df1dbc77.glb filter=lfs diff=lfs merge=lfs -text
|
| 72 |
+
.gradio/cached_examples/31/Interactive[[:space:]]3D[[:space:]]point[[:space:]]cloud/e208d09a8d563ea27323/cloud_a7b919815dba.glb filter=lfs diff=lfs merge=lfs -text
|
| 73 |
+
.gradio/cached_examples/31/RGB[[:space:]]image/4763ceeb1dd670d3f927/image.png filter=lfs diff=lfs merge=lfs -text
|
| 74 |
+
.gradio/cached_examples/31/RGB[[:space:]]image/530eb79823e527aed6bf/image.png filter=lfs diff=lfs merge=lfs -text
|
| 75 |
+
.gradio/cached_examples/31/RGB[[:space:]]image/65c25b8debef84e0027b/image.png filter=lfs diff=lfs merge=lfs -text
|
| 76 |
+
.gradio/cached_examples/31/RGB[[:space:]]image/6d51368904340f3a7f23/image.png filter=lfs diff=lfs merge=lfs -text
|
| 77 |
+
.gradio/cached_examples/31/RGB[[:space:]]image/91174cfa8f5a5909a013/image.png filter=lfs diff=lfs merge=lfs -text
|
| 78 |
+
.gradio/cached_examples/31/RGB[[:space:]]image/d68b76efe33346efd7e3/image.png filter=lfs diff=lfs merge=lfs -text
|
| 79 |
+
.gradio/cached_examples/32/Depth[[:space:]](disparity,[[:space:]]magma)/38d56c7397d95d524055/image.png filter=lfs diff=lfs merge=lfs -text
|
| 80 |
+
.gradio/cached_examples/32/Depth[[:space:]](disparity,[[:space:]]magma)/9a36d2e382db4e333742/image.png filter=lfs diff=lfs merge=lfs -text
|
| 81 |
+
.gradio/cached_examples/32/Interactive[[:space:]]3D[[:space:]]point[[:space:]]cloud/674110f2aecaaf5effa8/cloud_f9d62ed7d997.glb filter=lfs diff=lfs merge=lfs -text
|
| 82 |
+
.gradio/cached_examples/32/Interactive[[:space:]]3D[[:space:]]point[[:space:]]cloud/85ee5283a423852aa42d/cloud_8a6b0d1109ae.glb filter=lfs diff=lfs merge=lfs -text
|
| 83 |
+
.gradio/cached_examples/32/Interactive[[:space:]]3D[[:space:]]point[[:space:]]cloud/e7f9024213e4656b8beb/cloud_a6225bea9d7e.glb filter=lfs diff=lfs merge=lfs -text
|
| 84 |
+
.gradio/cached_examples/32/RGB[[:space:]]image/3c2c4fa7881bb773646d/image.png filter=lfs diff=lfs merge=lfs -text
|
| 85 |
+
.gradio/cached_examples/32/RGB[[:space:]]image/87aaba9c524a915404cd/image.png filter=lfs diff=lfs merge=lfs -text
|
| 86 |
+
.gradio/cached_examples/32/RGB[[:space:]]image/e64a0aff7688d0635057/image.png filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__/
|
| 2 |
+
*.pyc
|
| 3 |
+
.venv/
|
| 4 |
+
flux_rgbd_outputs/
|
| 5 |
+
*.npy
|
| 6 |
+
artifacts/
|
.gradio/cached_examples/31/Depth (disparity, magma)/00514d1d228e1970f9a5/image.png
ADDED
|
Git LFS Details
|
.gradio/cached_examples/31/Depth (disparity, magma)/21da509194782c87b704/image.png
ADDED
|
.gradio/cached_examples/31/Depth (disparity, magma)/282b8de47381433b20ba/image.png
ADDED
|
Git LFS Details
|
.gradio/cached_examples/31/Depth (disparity, magma)/72f252326e89ba602db3/image.png
ADDED
|
.gradio/cached_examples/31/Depth (disparity, magma)/77eb75a4ae32a2779670/image.png
ADDED
|
Git LFS Details
|
.gradio/cached_examples/31/Depth (disparity, magma)/b3a0c160552d198bcca2/image.png
ADDED
|
Git LFS Details
|
.gradio/cached_examples/31/Interactive 3D point cloud/6351a7c3b27ea01e3670/cloud_45fd4adef349.glb
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:318e55582668f25686e92a4db1b6c5d3f75434d047ffd58c1de526abd12473c4
|
| 3 |
+
size 3068480
|
.gradio/cached_examples/31/Interactive 3D point cloud/a42239d35b0256820e21/cloud_82bf2e710be0.glb
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:da22a39f84033e0f93e395d90715aedd4c15de191ac5740704e8036a3c2238f2
|
| 3 |
+
size 3839248
|
.gradio/cached_examples/31/Interactive 3D point cloud/a600814ba3ef00eacfeb/cloud_9a8d39949ba7.glb
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c9c9c01e77f9425692ccb77dcacfd10690f42aa2f59f5086df11e313f98bca4a
|
| 3 |
+
size 1557100
|
.gradio/cached_examples/31/Interactive 3D point cloud/a6b517c351861e68aa8f/cloud_6f14c0ff2da6.glb
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e8143c842b204228b93431b2c937d2e531d477854a1177c4a23076d027ae63e7
|
| 3 |
+
size 4057104
|
.gradio/cached_examples/31/Interactive 3D point cloud/a93a97926162980d549a/cloud_53c7df1dbc77.glb
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6f7e2b84bf61c50398f8fdb3b0f84ae218d5d3991c268f96f3b6184bf02a021d
|
| 3 |
+
size 4050768
|
.gradio/cached_examples/31/Interactive 3D point cloud/e208d09a8d563ea27323/cloud_a7b919815dba.glb
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a2e5928c97bf15bc99c66232dbcd8fd09b9dcb5401ad945409c5e08694314b5d
|
| 3 |
+
size 3992720
|
.gradio/cached_examples/31/RGB image/4763ceeb1dd670d3f927/image.png
ADDED
|
Git LFS Details
|
.gradio/cached_examples/31/RGB image/530eb79823e527aed6bf/image.png
ADDED
|
Git LFS Details
|
.gradio/cached_examples/31/RGB image/65c25b8debef84e0027b/image.png
ADDED
|
Git LFS Details
|
.gradio/cached_examples/31/RGB image/6d51368904340f3a7f23/image.png
ADDED
|
Git LFS Details
|
.gradio/cached_examples/31/RGB image/91174cfa8f5a5909a013/image.png
ADDED
|
Git LFS Details
|
.gradio/cached_examples/31/RGB image/d68b76efe33346efd7e3/image.png
ADDED
|
Git LFS Details
|
.gradio/cached_examples/31/indices.csv
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
0
|
| 2 |
+
1
|
| 3 |
+
2
|
| 4 |
+
3
|
| 5 |
+
4
|
| 6 |
+
5
|
.gradio/cached_examples/31/log.csv
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
RGB image,"Depth (disparity, magma)",Interactive 3D point cloud,Status,timestamp
|
| 2 |
+
"{""path"": "".gradio/cached_examples/31/RGB image/91174cfa8f5a5909a013/image.png"", ""url"": ""/gradio_api/file=/tmp/gradio/dfd5ecf1e795fbfd7f794edd94c8f06e7dc0f0008a15814286a7a11b0e3bda0a/image.png"", ""size"": null, ""orig_name"": ""image.png"", ""mime_type"": null, ""is_stream"": false, ""meta"": {""_type"": ""gradio.FileData""}}","{""path"": "".gradio/cached_examples/31/Depth (disparity, magma)/72f252326e89ba602db3/image.png"", ""url"": ""/gradio_api/file=/tmp/gradio/7111df1c084d7d80e87ed0ba942ef806ebb82b0ea3358242a9471e87ed025aca/image.png"", ""size"": null, ""orig_name"": ""image.png"", ""mime_type"": null, ""is_stream"": false, ""meta"": {""_type"": ""gradio.FileData""}}","{""path"": "".gradio/cached_examples/31/Interactive 3D point cloud/e208d09a8d563ea27323/cloud_a7b919815dba.glb"", ""url"": ""/gradio_api/file=/tmp/gradio/b7842ce128885af0de98f62956ea71483eb3ca75a5af89df81202f0c19de2502/cloud_a7b919815dba.glb"", ""size"": null, ""orig_name"": ""cloud_a7b919815dba.glb"", ""mime_type"": null, ""is_stream"": false, ""meta"": {""_type"": ""gradio.FileData""}}","joint · 17.0 s · depth median=1.06 p5=0.55 p95=1.39 · 249,485 points",2026-06-12 07:14:17.591889
|
| 3 |
+
"{""path"": "".gradio/cached_examples/31/RGB image/d68b76efe33346efd7e3/image.png"", ""url"": ""/gradio_api/file=/tmp/gradio/db51824edbde88fd4de0749ffb077c98b27d9ee96017d42db904e2c763579606/image.png"", ""size"": null, ""orig_name"": ""image.png"", ""mime_type"": null, ""is_stream"": false, ""meta"": {""_type"": ""gradio.FileData""}}","{""path"": "".gradio/cached_examples/31/Depth (disparity, magma)/21da509194782c87b704/image.png"", ""url"": ""/gradio_api/file=/tmp/gradio/e3e01501ae08e1438d11238cf43d427406c012cf0b287f1820975b5cc461f224/image.png"", ""size"": null, ""orig_name"": ""image.png"", ""mime_type"": null, ""is_stream"": false, ""meta"": {""_type"": ""gradio.FileData""}}","{""path"": "".gradio/cached_examples/31/Interactive 3D point cloud/a93a97926162980d549a/cloud_53c7df1dbc77.glb"", ""url"": ""/gradio_api/file=/tmp/gradio/96342e925c3ba767021690b60f3715015db850198dccd2fd3e5245468dbcafee/cloud_53c7df1dbc77.glb"", ""size"": null, ""orig_name"": ""cloud_53c7df1dbc77.glb"", ""mime_type"": null, ""is_stream"": false, ""meta"": {""_type"": ""gradio.FileData""}}","joint · 14.8 s · depth median=1.04 p5=0.57 p95=1.37 · 253,113 points",2026-06-12 07:14:32.666816
|
| 4 |
+
"{""path"": "".gradio/cached_examples/31/RGB image/6d51368904340f3a7f23/image.png"", ""url"": ""/gradio_api/file=/tmp/gradio/9919ff6eef6ba9aa87da068492e8a80dd0e5f62f7f27dcc20cb3de5e975ef2b0/image.png"", ""size"": null, ""orig_name"": ""image.png"", ""mime_type"": null, ""is_stream"": false, ""meta"": {""_type"": ""gradio.FileData""}}","{""path"": "".gradio/cached_examples/31/Depth (disparity, magma)/00514d1d228e1970f9a5/image.png"", ""url"": ""/gradio_api/file=/tmp/gradio/0ffd5b599e783b31637214af5bfb99c6fea27cce8df77021893d3f63e9e1f5aa/image.png"", ""size"": null, ""orig_name"": ""image.png"", ""mime_type"": null, ""is_stream"": false, ""meta"": {""_type"": ""gradio.FileData""}}","{""path"": "".gradio/cached_examples/31/Interactive 3D point cloud/a600814ba3ef00eacfeb/cloud_9a8d39949ba7.glb"", ""url"": ""/gradio_api/file=/tmp/gradio/3e1f8dedd7baf5f8977d2f5e7488413ca6499045d5939ee6371c86fb2b039e54/cloud_9a8d39949ba7.glb"", ""size"": null, ""orig_name"": ""cloud_9a8d39949ba7.glb"", ""mime_type"": null, ""is_stream"": false, ""meta"": {""_type"": ""gradio.FileData""}}","joint · 14.9 s · depth median=0.84 p5=0.37 p95=2.21 · 97,259 points",2026-06-12 07:14:47.729293
|
| 5 |
+
"{""path"": "".gradio/cached_examples/31/RGB image/530eb79823e527aed6bf/image.png"", ""url"": ""/gradio_api/file=/tmp/gradio/0bbd8ccaaeccd0e353685be2f1146bbbdd4e2a42d5350fe13bd182a27f505a61/image.png"", ""size"": null, ""orig_name"": ""image.png"", ""mime_type"": null, ""is_stream"": false, ""meta"": {""_type"": ""gradio.FileData""}}","{""path"": "".gradio/cached_examples/31/Depth (disparity, magma)/b3a0c160552d198bcca2/image.png"", ""url"": ""/gradio_api/file=/tmp/gradio/91abce7d476b85c56d457cb3526e279dbf126ff0d36d266cbde7b0ba97d0714f/image.png"", ""size"": null, ""orig_name"": ""image.png"", ""mime_type"": null, ""is_stream"": false, ""meta"": {""_type"": ""gradio.FileData""}}","{""path"": "".gradio/cached_examples/31/Interactive 3D point cloud/a42239d35b0256820e21/cloud_82bf2e710be0.glb"", ""url"": ""/gradio_api/file=/tmp/gradio/fff39c18ed5428d50126dee01f175fe9339a93dedb01c226549e19234c3a18d3/cloud_82bf2e710be0.glb"", ""size"": null, ""orig_name"": ""cloud_82bf2e710be0.glb"", ""mime_type"": null, ""is_stream"": false, ""meta"": {""_type"": ""gradio.FileData""}}","joint · 14.9 s · depth median=0.98 p5=0.65 p95=1.38 · 239,893 points",2026-06-12 07:15:02.835368
|
| 6 |
+
"{""path"": "".gradio/cached_examples/31/RGB image/65c25b8debef84e0027b/image.png"", ""url"": ""/gradio_api/file=/tmp/gradio/eed4a59e42c6c0c9e9cd120b9f5900522c0f4006c54aec5e3466300cce5f5c3d/image.png"", ""size"": null, ""orig_name"": ""image.png"", ""mime_type"": null, ""is_stream"": false, ""meta"": {""_type"": ""gradio.FileData""}}","{""path"": "".gradio/cached_examples/31/Depth (disparity, magma)/77eb75a4ae32a2779670/image.png"", ""url"": ""/gradio_api/file=/tmp/gradio/c9434cd1a2b855cdf7615290ea2d29c8c5418e0793a3cd859e55150891bc495a/image.png"", ""size"": null, ""orig_name"": ""image.png"", ""mime_type"": null, ""is_stream"": false, ""meta"": {""_type"": ""gradio.FileData""}}","{""path"": "".gradio/cached_examples/31/Interactive 3D point cloud/a6b517c351861e68aa8f/cloud_6f14c0ff2da6.glb"", ""url"": ""/gradio_api/file=/tmp/gradio/3fe22788a5f8d0a6d274d5b367522db4306e5dca95dbfe1bd1cfb1c2f1c4c6f0/cloud_6f14c0ff2da6.glb"", ""size"": null, ""orig_name"": ""cloud_6f14c0ff2da6.glb"", ""mime_type"": null, ""is_stream"": false, ""meta"": {""_type"": ""gradio.FileData""}}","joint · 14.8 s · depth median=0.90 p5=0.63 p95=1.65 · 253,509 points",2026-06-12 07:15:17.944777
|
| 7 |
+
"{""path"": "".gradio/cached_examples/31/RGB image/4763ceeb1dd670d3f927/image.png"", ""url"": ""/gradio_api/file=/tmp/gradio/71b22a185a03aa169cdca016381da0b4179ed403b377b7fe4cb005c97797b8ee/image.png"", ""size"": null, ""orig_name"": ""image.png"", ""mime_type"": null, ""is_stream"": false, ""meta"": {""_type"": ""gradio.FileData""}}","{""path"": "".gradio/cached_examples/31/Depth (disparity, magma)/282b8de47381433b20ba/image.png"", ""url"": ""/gradio_api/file=/tmp/gradio/9ef7335cc9a72e4364a920bf4431a69841eff7326a683f601376302cc1ddaa24/image.png"", ""size"": null, ""orig_name"": ""image.png"", ""mime_type"": null, ""is_stream"": false, ""meta"": {""_type"": ""gradio.FileData""}}","{""path"": "".gradio/cached_examples/31/Interactive 3D point cloud/6351a7c3b27ea01e3670/cloud_45fd4adef349.glb"", ""url"": ""/gradio_api/file=/tmp/gradio/92aa7dea6a27cd503ba5c2235856b6e28f283700cbfc51764f794167491464e4/cloud_45fd4adef349.glb"", ""size"": null, ""orig_name"": ""cloud_45fd4adef349.glb"", ""mime_type"": null, ""is_stream"": false, ""meta"": {""_type"": ""gradio.FileData""}}","joint · 14.8 s · depth median=0.82 p5=0.07 p95=2.22 · 191,720 points",2026-06-12 07:15:32.973544
|
.gradio/cached_examples/32/Depth (disparity, magma)/0891e8543770d0db2151/image.png
ADDED
|
.gradio/cached_examples/32/Depth (disparity, magma)/38d56c7397d95d524055/image.png
ADDED
|
Git LFS Details
|
.gradio/cached_examples/32/Depth (disparity, magma)/9a36d2e382db4e333742/image.png
ADDED
|
Git LFS Details
|
.gradio/cached_examples/32/Interactive 3D point cloud/674110f2aecaaf5effa8/cloud_f9d62ed7d997.glb
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c5d3183aec16883d35c85a8d6826d8c437cb1630c9d10ff08a5f67116dd3fd65
|
| 3 |
+
size 3730060
|
.gradio/cached_examples/32/Interactive 3D point cloud/85ee5283a423852aa42d/cloud_8a6b0d1109ae.glb
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:98a8873b3e5893e5a7dc8afdaca3c8f92484cc6981f493bb834bfed6ca019caf
|
| 3 |
+
size 1768556
|
.gradio/cached_examples/32/Interactive 3D point cloud/e7f9024213e4656b8beb/cloud_a6225bea9d7e.glb
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:4ca71c04f12756b0bfb26002b18e7221bf22ea36a5e23824a0a01cf753806f9e
|
| 3 |
+
size 3742080
|
.gradio/cached_examples/32/RGB image/3c2c4fa7881bb773646d/image.png
ADDED
|
Git LFS Details
|
.gradio/cached_examples/32/RGB image/87aaba9c524a915404cd/image.png
ADDED
|
Git LFS Details
|
.gradio/cached_examples/32/RGB image/e64a0aff7688d0635057/image.png
ADDED
|
Git LFS Details
|
.gradio/cached_examples/32/indices.csv
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
0
|
| 2 |
+
1
|
| 3 |
+
2
|
.gradio/cached_examples/32/log.csv
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
RGB image,"Depth (disparity, magma)",Interactive 3D point cloud,Status,timestamp
|
| 2 |
+
"{""path"": "".gradio/cached_examples/32/RGB image/3c2c4fa7881bb773646d/image.png"", ""url"": ""/gradio_api/file=/tmp/gradio/1ed5fba88325554dd8fde55f43d90e4b52d96bd770ae9abb55558a721d2ad4dc/image.png"", ""size"": null, ""orig_name"": ""image.png"", ""mime_type"": null, ""is_stream"": false, ""meta"": {""_type"": ""gradio.FileData""}}","{""path"": "".gradio/cached_examples/32/Depth (disparity, magma)/38d56c7397d95d524055/image.png"", ""url"": ""/gradio_api/file=/tmp/gradio/675d608d8081b140ee360ac99a11615574a95a1f575dd55ceb8941f405daac0f/image.png"", ""size"": null, ""orig_name"": ""image.png"", ""mime_type"": null, ""is_stream"": false, ""meta"": {""_type"": ""gradio.FileData""}}","{""path"": "".gradio/cached_examples/32/Interactive 3D point cloud/674110f2aecaaf5effa8/cloud_f9d62ed7d997.glb"", ""url"": ""/gradio_api/file=/tmp/gradio/6396a2958022a63007f08ff0882cd34405d96b6cee56b1a69444e1ef08649811/cloud_f9d62ed7d997.glb"", ""size"": null, ""orig_name"": ""cloud_f9d62ed7d997.glb"", ""mime_type"": null, ""is_stream"": false, ""meta"": {""_type"": ""gradio.FileData""}}","i2d · 5.4 s · depth median=0.65 p5=0.37 p95=3.31 · 233,069 points",2026-06-12 07:15:38.629118
|
| 3 |
+
"{""path"": "".gradio/cached_examples/32/RGB image/e64a0aff7688d0635057/image.png"", ""url"": ""/gradio_api/file=/tmp/gradio/3a7962dcf7f6b1d7db2e1a0e7a625daecd56aebbac87a934bd805b05c026ee6c/image.png"", ""size"": null, ""orig_name"": ""image.png"", ""mime_type"": null, ""is_stream"": false, ""meta"": {""_type"": ""gradio.FileData""}}","{""path"": "".gradio/cached_examples/32/Depth (disparity, magma)/9a36d2e382db4e333742/image.png"", ""url"": ""/gradio_api/file=/tmp/gradio/647b0d2a6553dedf70711720e7a0dd7c2d0c1778bbbc29c066ebbdb74137ba43/image.png"", ""size"": null, ""orig_name"": ""image.png"", ""mime_type"": null, ""is_stream"": false, ""meta"": {""_type"": ""gradio.FileData""}}","{""path"": "".gradio/cached_examples/32/Interactive 3D point cloud/85ee5283a423852aa42d/cloud_8a6b0d1109ae.glb"", ""url"": ""/gradio_api/file=/tmp/gradio/9b0b10012948a25b48290aefa846f9ddb57792cae0bccf98ead298a0a18baf6e/cloud_8a6b0d1109ae.glb"", ""size"": null, ""orig_name"": ""cloud_8a6b0d1109ae.glb"", ""mime_type"": null, ""is_stream"": false, ""meta"": {""_type"": ""gradio.FileData""}}","i2d · 5.0 s · depth median=1.18 p5=0.36 p95=2.33 · 110,475 points",2026-06-12 07:15:43.778321
|
| 4 |
+
"{""path"": "".gradio/cached_examples/32/RGB image/87aaba9c524a915404cd/image.png"", ""url"": ""/gradio_api/file=/tmp/gradio/782693cd71c502c69e559adc17a82d06dc75ece451c4745b286f74f8be98284d/image.png"", ""size"": null, ""orig_name"": ""image.png"", ""mime_type"": null, ""is_stream"": false, ""meta"": {""_type"": ""gradio.FileData""}}","{""path"": "".gradio/cached_examples/32/Depth (disparity, magma)/0891e8543770d0db2151/image.png"", ""url"": ""/gradio_api/file=/tmp/gradio/112070edef52867273e2552aa71313e0447940a6eda5b9215a5202cc82c501a2/image.png"", ""size"": null, ""orig_name"": ""image.png"", ""mime_type"": null, ""is_stream"": false, ""meta"": {""_type"": ""gradio.FileData""}}","{""path"": "".gradio/cached_examples/32/Interactive 3D point cloud/e7f9024213e4656b8beb/cloud_a6225bea9d7e.glb"", ""url"": ""/gradio_api/file=/tmp/gradio/bd563503321e9ee362fcadf84021c707d2ddfb1ae60357b595f562a830465900/cloud_a6225bea9d7e.glb"", ""size"": null, ""orig_name"": ""cloud_a6225bea9d7e.glb"", ""mime_type"": null, ""is_stream"": false, ""meta"": {""_type"": ""gradio.FileData""}}","i2d · 5.0 s · depth median=0.72 p5=0.08 p95=2.41 · 233,820 points",2026-06-12 07:15:49.021708
|
.ruff_cache/.gitignore
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Automatically created by ruff.
|
| 2 |
+
*
|
.ruff_cache/0.5.5/12279076348749595670
ADDED
|
Binary file (13.5 kB). View file
|
|
|
.ruff_cache/CACHEDIR.TAG
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
Signature: 8a477f597d28d172789f06886806bc55
|
LICENSE
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
Apache License
|
| 3 |
+
Version 2.0, January 2004
|
| 4 |
+
http://www.apache.org/licenses/
|
| 5 |
+
|
| 6 |
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
| 7 |
+
|
| 8 |
+
1. Definitions.
|
| 9 |
+
|
| 10 |
+
"License" shall mean the terms and conditions for use, reproduction,
|
| 11 |
+
and distribution as defined by Sections 1 through 9 of this document.
|
| 12 |
+
|
| 13 |
+
"Licensor" shall mean the copyright owner or entity authorized by
|
| 14 |
+
the copyright owner that is granting the License.
|
| 15 |
+
|
| 16 |
+
"Legal Entity" shall mean the union of the acting entity and all
|
| 17 |
+
other entities that control, are controlled by, or are under common
|
| 18 |
+
control with that entity. For the purposes of this definition,
|
| 19 |
+
"control" means (i) the power, direct or indirect, to cause the
|
| 20 |
+
direction or management of such entity, whether by contract or
|
| 21 |
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
| 22 |
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
| 23 |
+
|
| 24 |
+
"You" (or "Your") shall mean an individual or Legal Entity
|
| 25 |
+
exercising permissions granted by this License.
|
| 26 |
+
|
| 27 |
+
"Source" form shall mean the preferred form for making modifications,
|
| 28 |
+
including but not limited to software source code, documentation
|
| 29 |
+
source, and configuration files.
|
| 30 |
+
|
| 31 |
+
"Object" form shall mean any form resulting from mechanical
|
| 32 |
+
transformation or translation of a Source form, including but
|
| 33 |
+
not limited to compiled object code, generated documentation,
|
| 34 |
+
and conversions to other media types.
|
| 35 |
+
|
| 36 |
+
"Work" shall mean the work of authorship, whether in Source or
|
| 37 |
+
Object form, made available under the License, as indicated by a
|
| 38 |
+
copyright notice that is included in or attached to the work
|
| 39 |
+
(an example is provided in the Appendix below).
|
| 40 |
+
|
| 41 |
+
"Derivative Works" shall mean any work, whether in Source or Object
|
| 42 |
+
form, that is based on (or derived from) the Work and for which the
|
| 43 |
+
editorial revisions, annotations, elaborations, or other modifications
|
| 44 |
+
represent, as a whole, an original work of authorship. For the purposes
|
| 45 |
+
of this License, Derivative Works shall not include works that remain
|
| 46 |
+
separable from, or merely link (or bind by name) to the interfaces of,
|
| 47 |
+
the Work and Derivative Works thereof.
|
| 48 |
+
|
| 49 |
+
"Contribution" shall mean any work of authorship, including
|
| 50 |
+
the original version of the Work and any modifications or additions
|
| 51 |
+
to that Work or Derivative Works thereof, that is intentionally
|
| 52 |
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
| 53 |
+
or by an individual or Legal Entity authorized to submit on behalf of
|
| 54 |
+
the copyright owner. For the purposes of this definition, "submitted"
|
| 55 |
+
means any form of electronic, verbal, or written communication sent
|
| 56 |
+
to the Licensor or its representatives, including but not limited to
|
| 57 |
+
communication on electronic mailing lists, source code control systems,
|
| 58 |
+
and issue tracking systems that are managed by, or on behalf of, the
|
| 59 |
+
Licensor for the purpose of discussing and improving the Work, but
|
| 60 |
+
excluding communication that is conspicuously marked or otherwise
|
| 61 |
+
designated in writing by the copyright owner as "Not a Contribution."
|
| 62 |
+
|
| 63 |
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
| 64 |
+
on behalf of whom a Contribution has been received by Licensor and
|
| 65 |
+
subsequently incorporated within the Work.
|
| 66 |
+
|
| 67 |
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
| 68 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 69 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 70 |
+
copyright license to reproduce, prepare Derivative Works of,
|
| 71 |
+
publicly display, publicly perform, sublicense, and distribute the
|
| 72 |
+
Work and such Derivative Works in Source or Object form.
|
| 73 |
+
|
| 74 |
+
3. Grant of Patent License. Subject to the terms and conditions of
|
| 75 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 76 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 77 |
+
(except as stated in this section) patent license to make, have made,
|
| 78 |
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
| 79 |
+
where such license applies only to those patent claims licensable
|
| 80 |
+
by such Contributor that are necessarily infringed by their
|
| 81 |
+
Contribution(s) alone or by combination of their Contribution(s)
|
| 82 |
+
with the Work to which such Contribution(s) was submitted. If You
|
| 83 |
+
institute patent litigation against any entity (including a
|
| 84 |
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
| 85 |
+
or a Contribution incorporated within the Work constitutes direct
|
| 86 |
+
or contributory patent infringement, then any patent licenses
|
| 87 |
+
granted to You under this License for that Work shall terminate
|
| 88 |
+
as of the date such litigation is filed.
|
| 89 |
+
|
| 90 |
+
4. Redistribution. You may reproduce and distribute copies of the
|
| 91 |
+
Work or Derivative Works thereof in any medium, with or without
|
| 92 |
+
modifications, and in Source or Object form, provided that You
|
| 93 |
+
meet the following conditions:
|
| 94 |
+
|
| 95 |
+
(a) You must give any other recipients of the Work or
|
| 96 |
+
Derivative Works a copy of this License; and
|
| 97 |
+
|
| 98 |
+
(b) You must cause any modified files to carry prominent notices
|
| 99 |
+
stating that You changed the files; and
|
| 100 |
+
|
| 101 |
+
(c) You must retain, in the Source form of any Derivative Works
|
| 102 |
+
that You distribute, all copyright, patent, trademark, and
|
| 103 |
+
attribution notices from the Source form of the Work,
|
| 104 |
+
excluding those notices that do not pertain to any part of
|
| 105 |
+
the Derivative Works; and
|
| 106 |
+
|
| 107 |
+
(d) If the Work includes a "NOTICE" text file as part of its
|
| 108 |
+
distribution, then any Derivative Works that You distribute must
|
| 109 |
+
include a readable copy of the attribution notices contained
|
| 110 |
+
within such NOTICE file, excluding those notices that do not
|
| 111 |
+
pertain to any part of the Derivative Works, in at least one
|
| 112 |
+
of the following places: within a NOTICE text file distributed
|
| 113 |
+
as part of the Derivative Works; within the Source form or
|
| 114 |
+
documentation, if provided along with the Derivative Works; or,
|
| 115 |
+
within a display generated by the Derivative Works, if and
|
| 116 |
+
wherever such third-party notices normally appear. The contents
|
| 117 |
+
of the NOTICE file are for informational purposes only and
|
| 118 |
+
do not modify the License. You may add Your own attribution
|
| 119 |
+
notices within Derivative Works that You distribute, alongside
|
| 120 |
+
or as an addendum to the NOTICE text from the Work, provided
|
| 121 |
+
that such additional attribution notices cannot be construed
|
| 122 |
+
as modifying the License.
|
| 123 |
+
|
| 124 |
+
You may add Your own copyright statement to Your modifications and
|
| 125 |
+
may provide additional or different license terms and conditions
|
| 126 |
+
for use, reproduction, or distribution of Your modifications, or
|
| 127 |
+
for any such Derivative Works as a whole, provided Your use,
|
| 128 |
+
reproduction, and distribution of the Work otherwise complies with
|
| 129 |
+
the conditions stated in this License.
|
| 130 |
+
|
| 131 |
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
| 132 |
+
any Contribution intentionally submitted for inclusion in the Work
|
| 133 |
+
by You to the Licensor shall be under the terms and conditions of
|
| 134 |
+
this License, without any additional terms or conditions.
|
| 135 |
+
Notwithstanding the above, nothing herein shall supersede or modify
|
| 136 |
+
the terms of any separate license agreement you may have executed
|
| 137 |
+
with Licensor regarding such Contributions.
|
| 138 |
+
|
| 139 |
+
6. Trademarks. This License does not grant permission to use the trade
|
| 140 |
+
names, trademarks, service marks, or product names of the Licensor,
|
| 141 |
+
except as required for reasonable and customary use in describing the
|
| 142 |
+
origin of the Work and reproducing the content of the NOTICE file.
|
| 143 |
+
|
| 144 |
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
| 145 |
+
agreed to in writing, Licensor provides the Work (and each
|
| 146 |
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
| 147 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
| 148 |
+
implied, including, without limitation, any warranties or conditions
|
| 149 |
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
| 150 |
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
| 151 |
+
appropriateness of using or redistributing the Work and assume any
|
| 152 |
+
risks associated with Your exercise of permissions under this License.
|
| 153 |
+
|
| 154 |
+
8. Limitation of Liability. In no event and under no legal theory,
|
| 155 |
+
whether in tort (including negligence), contract, or otherwise,
|
| 156 |
+
unless required by applicable law (such as deliberate and grossly
|
| 157 |
+
negligent acts) or agreed to in writing, shall any Contributor be
|
| 158 |
+
liable to You for damages, including any direct, indirect, special,
|
| 159 |
+
incidental, or consequential damages of any character arising as a
|
| 160 |
+
result of this License or out of the use or inability to use the
|
| 161 |
+
Work (including but not limited to damages for loss of goodwill,
|
| 162 |
+
work stoppage, computer failure or malfunction, or any and all
|
| 163 |
+
other commercial damages or losses), even if such Contributor
|
| 164 |
+
has been advised of the possibility of such damages.
|
| 165 |
+
|
| 166 |
+
9. Accepting Warranty or Additional Liability. While redistributing
|
| 167 |
+
the Work or Derivative Works thereof, You may choose to offer,
|
| 168 |
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
| 169 |
+
or other liability obligations and/or rights consistent with this
|
| 170 |
+
License. However, in accepting such obligations, You may act only
|
| 171 |
+
on Your own behalf and on Your sole responsibility, not on behalf
|
| 172 |
+
of any other Contributor, and only if You agree to indemnify,
|
| 173 |
+
defend, and hold each Contributor harmless for any liability
|
| 174 |
+
incurred by, or claims asserted against, such Contributor by reason
|
| 175 |
+
of your accepting any such warranty or additional liability.
|
| 176 |
+
|
| 177 |
+
END OF TERMS AND CONDITIONS
|
| 178 |
+
|
| 179 |
+
APPENDIX: How to apply the Apache License to your work.
|
| 180 |
+
|
| 181 |
+
To apply the Apache License to your work, attach the following
|
| 182 |
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
| 183 |
+
replaced with your own identifying information. (Don't include
|
| 184 |
+
the brackets!) The text should be enclosed in the appropriate
|
| 185 |
+
comment syntax for the file format. We also recommend that a
|
| 186 |
+
file or class name and description of purpose be included on the
|
| 187 |
+
same "printed page" as the copyright notice for easier
|
| 188 |
+
identification within third-party archives.
|
| 189 |
+
|
| 190 |
+
Copyright 2026 World Labs
|
| 191 |
+
|
| 192 |
+
Licensed under the Apache License, Version 2.0 (the "License");
|
| 193 |
+
you may not use this file except in compliance with the License.
|
| 194 |
+
You may obtain a copy of the License at
|
| 195 |
+
|
| 196 |
+
http://www.apache.org/licenses/LICENSE-2.0
|
| 197 |
+
|
| 198 |
+
Unless required by applicable law or agreed to in writing, software
|
| 199 |
+
distributed under the License is distributed on an "AS IS" BASIS,
|
| 200 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 201 |
+
See the License for the specific language governing permissions and
|
| 202 |
+
limitations under the License.
|
LICENSE-WEIGHTS
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Creative Commons Attribution-NonCommercial 4.0 International (CC BY-NC 4.0)
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2026 World Labs.
|
| 4 |
+
|
| 5 |
+
The MODEL WEIGHTS distributed for this project (e.g. via the Hugging Face
|
| 6 |
+
repository bartduis/modality_forcing) are licensed under the Creative Commons
|
| 7 |
+
Attribution-NonCommercial 4.0 International License. You are free to share and
|
| 8 |
+
adapt the weights for non-commercial purposes, provided you give appropriate
|
| 9 |
+
credit. THE WEIGHTS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
|
| 10 |
+
|
| 11 |
+
Full license text: https://creativecommons.org/licenses/by-nc/4.0/legalcode
|
| 12 |
+
|
| 13 |
+
The CODE in this repository is licensed separately under Apache-2.0 (see
|
| 14 |
+
LICENSE).
|
README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Modality Forcing
|
| 3 |
+
emoji: 🏢
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: gray
|
| 6 |
+
sdk: gradio
|
| 7 |
+
sdk_version: 6.16.0
|
| 8 |
+
python_version: '3.12'
|
| 9 |
+
app_file: app.py
|
| 10 |
+
hardware: zero-h200
|
| 11 |
+
pinned: false
|
| 12 |
+
license: apache-2.0
|
| 13 |
+
short_description: Text → RGB + depth + 3D point cloud.
|
| 14 |
+
---
|
| 15 |
+
|
| 16 |
+
<div align="center">
|
| 17 |
+
|
| 18 |
+
<h1>Modality Forcing for Scalable<br>Spatial Generation</h1>
|
| 19 |
+
|
| 20 |
+
[](https://modality-forcing.github.io/)
|
| 21 |
+
[](https://github.com/Duisterhof/modality-forcing)
|
| 22 |
+
[](https://huggingface.co/bartduis/modality_forcing)
|
| 23 |
+
[](https://arxiv.org/abs/2606.13676)
|
| 24 |
+
|
| 25 |
+
**[Bardienus Pieter Duisterhof](https://bart-ai.com)<sup>1,2</sup> · [Deva Ramanan](https://www.cs.cmu.edu/~deva/)<sup>1</sup> · [Jeffrey Ichnowski](https://ichnow.ski)<sup>1</sup> · [Justin Johnson](https://web.eecs.umich.edu/~justincj/)<sup>2</sup> · [Keunhong Park](https://keunhong.com)<sup>2</sup>**
|
| 26 |
+
|
| 27 |
+
**<sup>1</sup> Carnegie Mellon University <sup>2</sup> World Labs**
|
| 28 |
+
|
| 29 |
+
*Preprint, 2026*
|
| 30 |
+
|
| 31 |
+
<em>Modality Forcing turns a pretrained text-to-image model into a joint image–depth generator with a simple post-training recipe.</em>
|
| 32 |
+
|
| 33 |
+
</div>
|
| 34 |
+
|
| 35 |
+
## Overview
|
| 36 |
+
|
| 37 |
+
This Space hosts the interactive demo of **Modality Forcing**: joint text → RGB + depth diffusion built on FLUX.2. A single DiT supports every permutation of conditional and joint generation by assigning a separate noise level to each modality:
|
| 38 |
+
|
| 39 |
+
| Mode | Input | Output |
|
| 40 |
+
|------|-------|--------|
|
| 41 |
+
| **Joint** | text prompt | RGB + depth + 3D point cloud |
|
| 42 |
+
| **Image → depth** | text + image | depth + 3D point cloud (any aspect ratio, letterbox resize) |
|
| 43 |
+
|
| 44 |
+
## Abstract
|
| 45 |
+
|
| 46 |
+
Text-to-image (T2I) models contain rich spatial priors.
|
| 47 |
+
Synthesizing photorealistic, cluttered scenes requires an understanding of geometry, including perspective and relative scale.
|
| 48 |
+
Prior works adapt T2I models to leverage this prior for depth prediction, but they require dense depth data and involve complex recipes.
|
| 49 |
+
We propose **Modality Forcing**, a simple, scalable post-training recipe for joint image-depth generation using a single DiT trained on sparse depth data.
|
| 50 |
+
Modality Forcing enables conditional and joint generation of image and depth in any permutation by assigning separate noise levels per modality.
|
| 51 |
+
Per-modality decoders let us train on sparse, real-world depth and achieve strong, generalizable depth prediction.
|
| 52 |
+
We further show that Modality Forcing inherits the scalability of T2I pre-training: by training a set of T2I models from scratch (300M to 3B parameters), we find that larger models trained on more image data produce more accurate depth.
|
| 53 |
+
Our strongest model is competitive with state-of-the-art monocular depth estimators and reduces AbsRel by **57%** relative to existing joint image-depth generative models.
|
| 54 |
+
These results provide strong evidence that image generation is a scalable pre-training objective for spatial perception.
|
| 55 |
+
|
| 56 |
+
## Space configuration
|
| 57 |
+
|
| 58 |
+
### Weights
|
| 59 |
+
|
| 60 |
+
All weights are pulled from public repos — no token required:
|
| 61 |
+
[`bartduis/modality_forcing`](https://huggingface.co/bartduis/modality_forcing)
|
| 62 |
+
(DiT + FLUX.2 autoencoder, CC BY-NC 4.0) and
|
| 63 |
+
[`Qwen/Qwen3-8B`](https://huggingface.co/Qwen/Qwen3-8B) (text encoder).
|
| 64 |
+
Override the model repo via the `WEIGHTS_REPO` Space variable.
|
| 65 |
+
|
| 66 |
+
### Hardware
|
| 67 |
+
|
| 68 |
+
Pinned to `zero-h200`. The runner builds the DiT on the `meta` device and
|
| 69 |
+
assigns checkpoint weights straight onto the GPU (`runner.from_pretrained`),
|
| 70 |
+
so the BF16 model (~24 GB) loads in ~8 s on the first invocation — skipping
|
| 71 |
+
~45 s of throwaway random initialization — and stays resident for subsequent
|
| 72 |
+
calls within the same Space instance.
|
| 73 |
+
|
| 74 |
+
### Avoiding the cold re-download
|
| 75 |
+
|
| 76 |
+
A Space instance that has gone to sleep starts a fresh container, which
|
| 77 |
+
re-downloads the weights (~24 GB DiT + ~16 GB Qwen3-8B + the FLUX.2 VAE)
|
| 78 |
+
before the load above can run. To skip that, attach **persistent storage**
|
| 79 |
+
(Settings → Storage, or mount an HF Bucket) and point the HF cache at it via
|
| 80 |
+
the `HF_HOME` Space variable so the weights survive restarts.
|
| 81 |
+
|
| 82 |
+
## License
|
| 83 |
+
|
| 84 |
+
Code: Apache-2.0 (files derived from the FLUX.2 reference implementation —
|
| 85 |
+
`flux_rgbd/_flux2/`, `flux_rgbd/dit.py` — are Apache-2.0, Copyright Black
|
| 86 |
+
Forest Labs, with World Labs modifications). Model weights: CC BY-NC 4.0.
|
| 87 |
+
|
| 88 |
+
## Citation
|
| 89 |
+
|
| 90 |
+
If you find Modality Forcing useful, please consider citing:
|
| 91 |
+
|
| 92 |
+
```bibtex
|
| 93 |
+
@article{duisterhof2026mofo,
|
| 94 |
+
title = {Modality Forcing for Scalable Spatial Generation},
|
| 95 |
+
author = {Duisterhof, Bardienus Pieter and Ramanan, Deva and Ichnowski, Jeffrey and Johnson, Justin and Park, Keunhong},
|
| 96 |
+
journal = {arXiv preprint arXiv:2606.13676},
|
| 97 |
+
year = {2026}
|
| 98 |
+
}
|
| 99 |
+
```
|
app.py
ADDED
|
@@ -0,0 +1,642 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 2 |
+
# Copyright (c) 2026 World Labs.
|
| 3 |
+
"""HuggingFace ZeroGPU Space for flux_rgbd.
|
| 4 |
+
|
| 5 |
+
Wraps the same generate-and-render path used by the local demo with
|
| 6 |
+
``@spaces.GPU`` so HF ZeroGPU can attach a GPU per call. The model
|
| 7 |
+
loads at module-level (CPU), then moves to CUDA inside the first
|
| 8 |
+
decorated call.
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import os
|
| 12 |
+
import sys
|
| 13 |
+
import time
|
| 14 |
+
import uuid
|
| 15 |
+
from pathlib import Path
|
| 16 |
+
|
| 17 |
+
# The vendored flux_rgbd package lives next to this file.
|
| 18 |
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
| 19 |
+
|
| 20 |
+
import gradio as gr
|
| 21 |
+
import numpy as np
|
| 22 |
+
import spaces
|
| 23 |
+
import torch
|
| 24 |
+
|
| 25 |
+
from flux_rgbd import FluxRGBDRunner
|
| 26 |
+
from flux_rgbd.pointcloud import depth_edge_mask, statistical_outlier_mask
|
| 27 |
+
|
| 28 |
+
# BF16 checkpoint, downloaded from the Hub on first use. Override with the
|
| 29 |
+
# WEIGHTS_REPO env var (repo id or local path).
|
| 30 |
+
WEIGHTS_REPO = os.environ.get("WEIGHTS_REPO", "bartduis/modality_forcing")
|
| 31 |
+
|
| 32 |
+
DEFAULT_PROMPT = (
|
| 33 |
+
"A warm, inviting kitchen with a rustic-modern feel, where soft morning "
|
| 34 |
+
"light filters through a small window above the sink."
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
# Lazy-loaded runner. On ZeroGPU the model is loaded inside the first
|
| 38 |
+
# @spaces.GPU call so the import path costs nothing.
|
| 39 |
+
_runner: FluxRGBDRunner | None = None
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def _ensure_runner() -> FluxRGBDRunner:
|
| 43 |
+
global _runner
|
| 44 |
+
if _runner is None:
|
| 45 |
+
# Use BF16 Qwen3-8B instead of Qwen3-8B-FP8 to avoid the
|
| 46 |
+
# finegrained-fp8 / deep-gemm kernel dependency, which currently
|
| 47 |
+
# hits a metadata.json parse bug on HF's kernels-community.
|
| 48 |
+
text_encoder = os.environ.get("TEXT_ENCODER_REPO", "Qwen/Qwen3-8B")
|
| 49 |
+
# Generation resolution must match the checkpoint's training resolution
|
| 50 |
+
# (512 for the default model, 1024 for the 1024 checkpoint). Set
|
| 51 |
+
# IMG_RESOLUTION=1024 alongside WEIGHTS_REPO when using the 1024 ckpt.
|
| 52 |
+
res = int(os.environ.get("IMG_RESOLUTION", "512"))
|
| 53 |
+
print(f"[boot] loading {WEIGHTS_REPO} @ {res}px (text encoder: {text_encoder})…",
|
| 54 |
+
flush=True)
|
| 55 |
+
_runner = FluxRGBDRunner.from_pretrained(
|
| 56 |
+
WEIGHTS_REPO, device="cuda",
|
| 57 |
+
dtype=torch.bfloat16, head_dtype=torch.float32,
|
| 58 |
+
text_encoder=text_encoder, img_hw=(res, res),
|
| 59 |
+
)
|
| 60 |
+
print("[boot] runner ready.", flush=True)
|
| 61 |
+
return _runner
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
# --- helpers (kept inline so the Space repo doesn't depend on demo/app_lib) ---
|
| 65 |
+
|
| 66 |
+
_SH_C0 = 0.28209479177387814
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def _letterbox(img: np.ndarray, target: int = 512):
|
| 70 |
+
"""Resize so long side = target, then zero-pad to (target, target)."""
|
| 71 |
+
import cv2
|
| 72 |
+
h_in, w_in = img.shape[:2]
|
| 73 |
+
if h_in >= w_in:
|
| 74 |
+
h_out, w_out = target, max(1, int(round(w_in * target / h_in)))
|
| 75 |
+
else:
|
| 76 |
+
w_out, h_out = target, max(1, int(round(h_in * target / w_in)))
|
| 77 |
+
resized = cv2.resize(img, (w_out, h_out), interpolation=cv2.INTER_AREA)
|
| 78 |
+
canvas = np.zeros((target, target, img.shape[2] if img.ndim == 3 else 1),
|
| 79 |
+
dtype=img.dtype)
|
| 80 |
+
if img.ndim == 2:
|
| 81 |
+
canvas = canvas[..., 0]
|
| 82 |
+
top = (target - h_out) // 2
|
| 83 |
+
left = (target - w_out) // 2
|
| 84 |
+
canvas[top:top + h_out, left:left + w_out] = resized
|
| 85 |
+
return canvas, (top, left, h_out, w_out)
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def _depth_to_pointcloud(rgb_u8, depth, *, fov_deg=65.0, max_points=1_200_000,
|
| 89 |
+
edge_rtol=0.04, sor=False):
|
| 90 |
+
h, w = depth.shape
|
| 91 |
+
fx = w / (2.0 * np.tan(np.deg2rad(fov_deg) / 2.0))
|
| 92 |
+
cx, cy = w * 0.5, h * 0.5
|
| 93 |
+
# Keep every valid pixel — no percentile clip. The earlier [1, 99] clip
|
| 94 |
+
# discarded the nearest 1% of points, carving a hole in the closest
|
| 95 |
+
# surface (e.g. the front edge of a table) and also dropping the far
|
| 96 |
+
# background. The i2d depth is clean enough that this clipping isn't
|
| 97 |
+
# needed and it was cutting off the geometry users care most about.
|
| 98 |
+
valid = (depth > 0) & np.isfinite(depth)
|
| 99 |
+
# Depth-edge mask: drop occlusion-boundary "veil" pixels (MoGe-style).
|
| 100 |
+
if edge_rtol and edge_rtol > 0:
|
| 101 |
+
valid &= ~depth_edge_mask(depth, rtol=float(edge_rtol))
|
| 102 |
+
v_idx, u_idx = np.where(valid)
|
| 103 |
+
z = depth[v_idx, u_idx]
|
| 104 |
+
x = (u_idx + 0.5 - cx) * z / fx
|
| 105 |
+
y = (v_idx + 0.5 - cy) * z / fx
|
| 106 |
+
# glTF / Three.js: +Y up, camera looks down -Z. Flip image-y
|
| 107 |
+
# (which points down) and depth (which points into the scene).
|
| 108 |
+
pts = np.stack([x, -y, -z], axis=-1).astype(np.float32)
|
| 109 |
+
cols = rgb_u8[v_idx, u_idx]
|
| 110 |
+
if sor:
|
| 111 |
+
# Statistical outlier rejection: drops isolated floaters, but can
|
| 112 |
+
# over-trim fine structures — opt-in (the edge mask above is the
|
| 113 |
+
# default cleanup).
|
| 114 |
+
inliers = statistical_outlier_mask(pts)
|
| 115 |
+
pts, cols = pts[inliers], cols[inliers]
|
| 116 |
+
if pts.shape[0] > max_points:
|
| 117 |
+
idx = np.random.default_rng(0).choice(pts.shape[0], max_points, replace=False)
|
| 118 |
+
pts, cols = pts[idx], cols[idx]
|
| 119 |
+
if pts.shape[0]:
|
| 120 |
+
pts -= np.median(pts, axis=0, keepdims=True)
|
| 121 |
+
return pts, cols
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def _save_glb(path, points, colors):
|
| 125 |
+
"""Colored point cloud → binary glTF, the format gr.Model3D handles cleanly."""
|
| 126 |
+
import trimesh
|
| 127 |
+
cloud = trimesh.PointCloud(vertices=points, colors=colors)
|
| 128 |
+
scene = trimesh.Scene()
|
| 129 |
+
scene.add_geometry(cloud)
|
| 130 |
+
scene.export(str(path))
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def _depth_to_magma(depth: np.ndarray) -> np.ndarray:
|
| 134 |
+
"""Depth → magma-colormapped disparity image (uint8 RGB).
|
| 135 |
+
|
| 136 |
+
Visualizes 1/depth (so near = bright) robustly normalized to the 5–95th
|
| 137 |
+
percentile, matching the depth panel in the reference notebook.
|
| 138 |
+
"""
|
| 139 |
+
from matplotlib import cm
|
| 140 |
+
valid = (depth > 0) & np.isfinite(depth)
|
| 141 |
+
disparity = np.zeros_like(depth, dtype=np.float32)
|
| 142 |
+
if valid.any():
|
| 143 |
+
disparity[valid] = 1.0 / np.maximum(depth[valid], 1e-8)
|
| 144 |
+
lo, hi = np.percentile(disparity[valid], [5, 95])
|
| 145 |
+
disparity = np.clip((disparity - lo) / max(hi - lo, 1e-8), 0, 1)
|
| 146 |
+
disparity[~valid] = 0.0
|
| 147 |
+
return (cm.magma(disparity)[..., :3] * 255).astype(np.uint8)
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
# /tmp is the writable mount on HF Spaces. We write the PLY here from the
|
| 151 |
+
# parent process (i.e. NOT inside the @spaces.GPU subprocess) so Gradio's
|
| 152 |
+
# file route can read it. Unique filename per call so Gradio's content-
|
| 153 |
+
# hashed cache always serves fresh bytes.
|
| 154 |
+
_ARTIFACT_DIR = Path("/tmp/flux_rgbd_artifacts")
|
| 155 |
+
|
| 156 |
+
# 2h is comfortably longer than any viewer session; keeps a busy Space's
|
| 157 |
+
# /tmp bounded since nothing else ever deletes these.
|
| 158 |
+
_ARTIFACT_TTL_S = 2 * 3600.0
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
def _prune_old_artifacts() -> None:
|
| 162 |
+
now = time.time()
|
| 163 |
+
for f in _ARTIFACT_DIR.glob("cloud_*.glb"):
|
| 164 |
+
try:
|
| 165 |
+
if now - f.stat().st_mtime > _ARTIFACT_TTL_S:
|
| 166 |
+
f.unlink()
|
| 167 |
+
except OSError:
|
| 168 |
+
pass # concurrent delete / fs hiccup — never fail a generation
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
_ARTIFACT_DIR.mkdir(parents=True, exist_ok=True)
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
@spaces.GPU(duration=120)
|
| 175 |
+
def _sample_on_gpu(prompt: str, input_image,
|
| 176 |
+
num_steps: int, cfg_scale: float, seed: int,
|
| 177 |
+
refine_depth: bool = True, log2_alpha: float = 5.0):
|
| 178 |
+
"""GPU-only step: text encode + diffusion sample + VAE decode.
|
| 179 |
+
|
| 180 |
+
Returns plain numpy arrays so the parent process (which is what
|
| 181 |
+
serves Gradio files) can do the rest. Writing the PLY here would
|
| 182 |
+
leave it in the subprocess's filesystem view where the parent's
|
| 183 |
+
Gradio file route can't find it (returns 404).
|
| 184 |
+
"""
|
| 185 |
+
import time
|
| 186 |
+
runner = _ensure_runner()
|
| 187 |
+
mode = "i2d" if input_image is not None else "joint"
|
| 188 |
+
|
| 189 |
+
target = runner.img_hw[0]
|
| 190 |
+
if mode == "i2d":
|
| 191 |
+
letterboxed, (top, left, vh, vw) = _letterbox(input_image, target)
|
| 192 |
+
model_input = letterboxed
|
| 193 |
+
else:
|
| 194 |
+
letterboxed = None
|
| 195 |
+
top = left = 0
|
| 196 |
+
vh = vw = target
|
| 197 |
+
model_input = None
|
| 198 |
+
|
| 199 |
+
t0 = time.time()
|
| 200 |
+
if mode == "i2d":
|
| 201 |
+
# Image given: single image→depth pass at CFG 1.0 (no guidance — the
|
| 202 |
+
# RGB is fixed, so there is nothing for CFG to steer).
|
| 203 |
+
result = runner.generate(
|
| 204 |
+
prompt.strip() if prompt else "",
|
| 205 |
+
mode="i2d",
|
| 206 |
+
num_steps=int(num_steps), cfg_scale=1.0, seed=int(seed),
|
| 207 |
+
clean_rgb_image=model_input,
|
| 208 |
+
)
|
| 209 |
+
else:
|
| 210 |
+
# Text→RGBD. Stage 1 joint at the requested CFG (default 4.0), rgb-first
|
| 211 |
+
# trajectory (log2_alpha=5) for cleaner depth. When `refine_depth` is on,
|
| 212 |
+
# a stage 2 re-derives depth via i2d on that RGB at CFG 1.0 for sharper,
|
| 213 |
+
# RGB-consistent geometry; otherwise the single joint pass is used.
|
| 214 |
+
result = runner.generate(
|
| 215 |
+
prompt.strip() if prompt else "",
|
| 216 |
+
mode="joint",
|
| 217 |
+
num_steps=int(num_steps), cfg_scale=float(cfg_scale), seed=int(seed),
|
| 218 |
+
log2_alpha=float(log2_alpha),
|
| 219 |
+
refine_depth_i2d=bool(refine_depth), i2d_cfg_scale=1.0,
|
| 220 |
+
)
|
| 221 |
+
elapsed = time.time() - t0
|
| 222 |
+
|
| 223 |
+
rgb_for_pc = (letterboxed[top:top + vh, left:left + vw] if mode == "i2d"
|
| 224 |
+
else result["rgb"])
|
| 225 |
+
depth = result["depth"]
|
| 226 |
+
if mode == "i2d":
|
| 227 |
+
depth = depth[top:top + vh, left:left + vw]
|
| 228 |
+
return rgb_for_pc, depth, mode, elapsed
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
def generate(prompt: str, input_image, num_steps: int, cfg_scale: float, seed: int,
|
| 232 |
+
refine_depth: bool = True, log2_alpha: float = 5.0,
|
| 233 |
+
edge_rtol: float = 0.04, sor: bool = False):
|
| 234 |
+
"""Public Gradio handler. Runs the GPU step then does PLY writing
|
| 235 |
+
here in the parent process so the file persists for Gradio."""
|
| 236 |
+
rgb_for_pc, depth, mode, elapsed = _sample_on_gpu(
|
| 237 |
+
prompt, input_image, num_steps, cfg_scale, seed, refine_depth, log2_alpha,
|
| 238 |
+
)
|
| 239 |
+
|
| 240 |
+
pts, cols = _depth_to_pointcloud(rgb_for_pc, depth, edge_rtol=edge_rtol,
|
| 241 |
+
sor=bool(sor))
|
| 242 |
+
_prune_old_artifacts()
|
| 243 |
+
cloud_path = str(_ARTIFACT_DIR / f"cloud_{uuid.uuid4().hex[:12]}.glb")
|
| 244 |
+
_save_glb(cloud_path, pts, cols)
|
| 245 |
+
|
| 246 |
+
valid = (depth > 0) & np.isfinite(depth)
|
| 247 |
+
if valid.any():
|
| 248 |
+
d = depth[valid]
|
| 249 |
+
depth_summary = (
|
| 250 |
+
f"depth median={float(np.median(d)):.2f} "
|
| 251 |
+
f"p5={float(np.percentile(d, 5)):.2f} "
|
| 252 |
+
f"p95={float(np.percentile(d, 95)):.2f}"
|
| 253 |
+
)
|
| 254 |
+
else:
|
| 255 |
+
depth_summary = "depth has no valid pixels"
|
| 256 |
+
status = f"{mode} · {elapsed:.1f} s · {depth_summary} · {pts.shape[0]:,} points"
|
| 257 |
+
return rgb_for_pc, _depth_to_magma(depth), cloud_path, status
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
# --- Presentation layer ----------------------------------------------------
|
| 261 |
+
# Only the Gradio UI definition lives below. The generation/model code above
|
| 262 |
+
# is untouched.
|
| 263 |
+
|
| 264 |
+
WORLD_LABS_URL = "https://www.worldlabs.ai"
|
| 265 |
+
_PROJECT_URL = "https://modality-forcing.github.io/"
|
| 266 |
+
_ARXIV_URL = "https://arxiv.org/abs/2606.13676"
|
| 267 |
+
_CODE_URL = "https://github.com/Duisterhof/modality-forcing"
|
| 268 |
+
|
| 269 |
+
# Editorial monochrome: a fully neutral palette, Inter for body, JetBrains
|
| 270 |
+
# Mono for the small uppercase "eyebrow" labels. The serif display face for
|
| 271 |
+
# the title (Gilda Display) is pulled in via @import in the CSS below.
|
| 272 |
+
_THEME = gr.themes.Default(
|
| 273 |
+
# System fonts only — no Google-fetched web fonts for the body/mono, which
|
| 274 |
+
# were loading unreliably (falling back to Arial and looking cheap). The
|
| 275 |
+
# serif display title uses Gilda Display, pulled in via @import in the CSS.
|
| 276 |
+
font=(
|
| 277 |
+
"system-ui",
|
| 278 |
+
"-apple-system",
|
| 279 |
+
"Segoe UI",
|
| 280 |
+
"Helvetica Neue",
|
| 281 |
+
"Arial",
|
| 282 |
+
"sans-serif",
|
| 283 |
+
),
|
| 284 |
+
font_mono=(
|
| 285 |
+
"ui-monospace",
|
| 286 |
+
"SFMono-Regular",
|
| 287 |
+
"Menlo",
|
| 288 |
+
"Consolas",
|
| 289 |
+
"monospace",
|
| 290 |
+
),
|
| 291 |
+
primary_hue=gr.themes.colors.neutral,
|
| 292 |
+
secondary_hue=gr.themes.colors.neutral,
|
| 293 |
+
neutral_hue=gr.themes.colors.neutral,
|
| 294 |
+
).set(
|
| 295 |
+
# Hairline, low-contrast borders; no heavy shadows or filled labels.
|
| 296 |
+
block_border_width="1px",
|
| 297 |
+
block_border_color="*neutral_200",
|
| 298 |
+
block_background_fill="white",
|
| 299 |
+
block_shadow="none",
|
| 300 |
+
block_label_background_fill="transparent",
|
| 301 |
+
block_label_border_width="0px",
|
| 302 |
+
block_label_text_weight="500",
|
| 303 |
+
input_border_width="1px",
|
| 304 |
+
input_border_color="*neutral_200",
|
| 305 |
+
input_shadow="none",
|
| 306 |
+
# Near-black, fully-rounded primary button (pill); white secondary.
|
| 307 |
+
button_large_radius="*radius_xxl",
|
| 308 |
+
button_small_radius="*radius_xxl",
|
| 309 |
+
button_primary_background_fill="#111111",
|
| 310 |
+
button_primary_background_fill_hover="#1f1f1f",
|
| 311 |
+
button_primary_text_color="white",
|
| 312 |
+
button_primary_border_color="#111111",
|
| 313 |
+
button_secondary_background_fill="white",
|
| 314 |
+
button_secondary_border_color="rgba(0,0,0,0.16)",
|
| 315 |
+
)
|
| 316 |
+
|
| 317 |
+
_CSS = """
|
| 318 |
+
@import url('https://fonts.googleapis.com/css2?family=Gilda+Display&display=swap');
|
| 319 |
+
|
| 320 |
+
/* Warm "paper" canvas everywhere (page + app + container) so the white
|
| 321 |
+
component cards lift off the background and the layout reads premium
|
| 322 |
+
rather than flat white-on-white. Body uses the system UI font. */
|
| 323 |
+
html, body, gradio-app, .gradio-container, .gradio-container .gap {
|
| 324 |
+
background: #f4f3ef !important;
|
| 325 |
+
}
|
| 326 |
+
.gradio-container {
|
| 327 |
+
color: #141414 !important;
|
| 328 |
+
max-width: 1120px !important;
|
| 329 |
+
margin: 0 auto !important;
|
| 330 |
+
padding: 28px 24px 12px !important;
|
| 331 |
+
font-family: system-ui, -apple-system, "Segoe UI", "Helvetica Neue", Arial, sans-serif !important;
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
/* Components become quiet white cards: hairline edge, soft round corners,
|
| 335 |
+
and a whisper of shadow for depth. (Also overrides this Gradio build's
|
| 336 |
+
hardcoded 3px black .block border.) */
|
| 337 |
+
.gradio-container .block {
|
| 338 |
+
border: 1px solid rgba(20,20,20,0.07) !important;
|
| 339 |
+
border-radius: 16px !important;
|
| 340 |
+
background: #ffffff !important;
|
| 341 |
+
box-shadow: 0 1px 2px rgba(20,20,20,0.04),
|
| 342 |
+
0 12px 28px -18px rgba(20,20,20,0.18) !important;
|
| 343 |
+
}
|
| 344 |
+
|
| 345 |
+
/* Text/HTML blocks float on the page — no card border, fill, or shadow. */
|
| 346 |
+
.gradio-container .mf-bare {
|
| 347 |
+
border: 0 !important; background: transparent !important;
|
| 348 |
+
box-shadow: none !important; padding: 0 !important;
|
| 349 |
+
}
|
| 350 |
+
|
| 351 |
+
/* Hairline rule separating the masthead from the workspace. */
|
| 352 |
+
.mf-rule {
|
| 353 |
+
height: 1px; border: 0; background: rgba(20,20,20,0.08);
|
| 354 |
+
max-width: 1080px; margin: 0.75rem auto 1.5rem;
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
/* ---- Publication header (mirrors the project page) ---- */
|
| 358 |
+
/* Everything in the masthead is centered. Forced with !important because
|
| 359 |
+
Gradio's prose CSS otherwise left-aligns <p>/<div> inside gr.HTML. */
|
| 360 |
+
.mf-pub, .mf-pub *, .mf-intro, .mf-intro * { text-align: center !important; }
|
| 361 |
+
.mf-pub { margin: 0.25rem auto 0.25rem; }
|
| 362 |
+
.mf-pub-title {
|
| 363 |
+
font-weight: 600;
|
| 364 |
+
font-size: clamp(1.9rem, 4.2vw, 3rem);
|
| 365 |
+
line-height: 1.13; letter-spacing: -0.01em;
|
| 366 |
+
color: #363636; margin: 0 auto 0.7em; max-width: 900px;
|
| 367 |
+
}
|
| 368 |
+
.mf-authors {
|
| 369 |
+
font-size: clamp(1rem, 1.4vw, 1.25rem); line-height: 1.5;
|
| 370 |
+
color: #363636; margin: 0 auto;
|
| 371 |
+
}
|
| 372 |
+
.mf-authors a { color: #3273dc !important; text-decoration: none !important; }
|
| 373 |
+
.mf-authors a:hover { text-decoration: underline !important; }
|
| 374 |
+
.mf-authors .ab { white-space: nowrap; margin: 0 0.15em; }
|
| 375 |
+
.mf-affil {
|
| 376 |
+
font-size: clamp(0.95rem, 1.3vw, 1.2rem); color: #363636;
|
| 377 |
+
margin: 0.5em auto 0;
|
| 378 |
+
}
|
| 379 |
+
.mf-affil .ab { margin: 0 0.6em; }
|
| 380 |
+
.mf-logos {
|
| 381 |
+
display: flex; justify-content: center; align-items: center;
|
| 382 |
+
gap: 40px; flex-wrap: wrap; margin: 1.4em auto 0;
|
| 383 |
+
}
|
| 384 |
+
.mf-logos img { height: 78px; }
|
| 385 |
+
.mf-venue { font-weight: 700; color: #363636; margin: 1em auto 0; }
|
| 386 |
+
.mf-links {
|
| 387 |
+
display: flex; justify-content: center; gap: 12px;
|
| 388 |
+
flex-wrap: wrap; margin: 1.1em auto 0.25rem;
|
| 389 |
+
}
|
| 390 |
+
.mf-btn {
|
| 391 |
+
display: inline-flex; align-items: center; gap: 7px;
|
| 392 |
+
padding: 7px 18px; border-radius: 9999px;
|
| 393 |
+
background: #363636; color: #ffffff !important;
|
| 394 |
+
font-size: 14px; font-weight: 500; text-decoration: none !important;
|
| 395 |
+
transition: background .15s ease;
|
| 396 |
+
}
|
| 397 |
+
.mf-btn:hover { background: #4a4a4a; }
|
| 398 |
+
.mf-pub-sub {
|
| 399 |
+
font-size: clamp(1rem, 1.3vw, 1.2rem); line-height: 1.5;
|
| 400 |
+
color: #4a4a4a; max-width: 720px; margin: 1.2em auto 0; font-weight: 400;
|
| 401 |
+
}
|
| 402 |
+
|
| 403 |
+
/* ---- Quiet helper line + section eyebrows ---- */
|
| 404 |
+
.mf-intro {
|
| 405 |
+
text-align: center; max-width: 640px; margin: 0.25rem auto 1rem;
|
| 406 |
+
font-size: 14px; line-height: 1.6; color: #6b6b6b;
|
| 407 |
+
}
|
| 408 |
+
.mf-intro b { color: #111111; font-weight: 600; }
|
| 409 |
+
.mf-sec {
|
| 410 |
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
| 411 |
+
font-size: 11px; font-weight: 500; letter-spacing: 0.18em;
|
| 412 |
+
text-transform: uppercase; color: #9a9a9a; margin: 4px 2px 2px;
|
| 413 |
+
}
|
| 414 |
+
|
| 415 |
+
/* ---- Primary button: full pill, ink-black ---- */
|
| 416 |
+
.gradio-container button.primary,
|
| 417 |
+
.gradio-container button.lg.primary {
|
| 418 |
+
background: #111111 !important; color: #ffffff !important;
|
| 419 |
+
border: 1px solid #111111 !important; border-radius: 9999px !important;
|
| 420 |
+
font-weight: 500 !important; letter-spacing: -0.005em !important;
|
| 421 |
+
}
|
| 422 |
+
.gradio-container button.primary:hover { background: #1f1f1f !important; }
|
| 423 |
+
|
| 424 |
+
/* ---- Examples: borderless, quiet ---- */
|
| 425 |
+
.gradio-container .examples table,
|
| 426 |
+
.gradio-container .examples .tr-head { border: 0 !important; }
|
| 427 |
+
.gradio-container .examples td {
|
| 428 |
+
border-color: rgba(0,0,0,0.06) !important; font-size: 13px !important;
|
| 429 |
+
}
|
| 430 |
+
|
| 431 |
+
/* ---- Footer ---- */
|
| 432 |
+
.mf-footer {
|
| 433 |
+
text-align: center; margin-top: 2rem; padding-top: 1.1rem;
|
| 434 |
+
border-top: 1px solid rgba(0,0,0,0.08);
|
| 435 |
+
}
|
| 436 |
+
.mf-footer .mf-cta { font-size: 14px; color: #3d3d3d; }
|
| 437 |
+
.mf-footer .mf-cta a {
|
| 438 |
+
color: #111111 !important; text-decoration: none !important;
|
| 439 |
+
border-bottom: 1px solid rgba(0,0,0,0.25);
|
| 440 |
+
}
|
| 441 |
+
.mf-footer .mf-credit {
|
| 442 |
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
| 443 |
+
font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase;
|
| 444 |
+
color: #9a9a9a; margin-top: 6px;
|
| 445 |
+
}
|
| 446 |
+
"""
|
| 447 |
+
|
| 448 |
+
_HEADER_HTML = (
|
| 449 |
+
'<div class="mf-pub">'
|
| 450 |
+
'<h1 class="mf-pub-title">Modality Forcing for Scalable Spatial Generation</h1>'
|
| 451 |
+
'<div class="mf-authors">'
|
| 452 |
+
'<span class="ab"><a href="https://bart-ai.com" target="_blank" rel="noreferrer">Bardienus Pieter Duisterhof</a><sup>1,2</sup>,</span>'
|
| 453 |
+
'<span class="ab"><a href="https://www.cs.cmu.edu/~deva/" target="_blank" rel="noreferrer">Deva Ramanan</a><sup>1</sup>,</span>'
|
| 454 |
+
'<span class="ab"><a href="https://ichnow.ski" target="_blank" rel="noreferrer">Jeffrey Ichnowski</a><sup>1</sup>,</span>'
|
| 455 |
+
'<span class="ab"><a href="https://web.eecs.umich.edu/~justincj/" target="_blank" rel="noreferrer">Justin Johnson</a><sup>2</sup>,</span>'
|
| 456 |
+
'<span class="ab"><a href="https://keunhong.com" target="_blank" rel="noreferrer">Keunhong Park</a><sup>2</sup></span>'
|
| 457 |
+
'</div>'
|
| 458 |
+
'<div class="mf-affil">'
|
| 459 |
+
'<span class="ab"><sup>1</sup>Carnegie Mellon University</span>'
|
| 460 |
+
'<span class="ab"><sup>2</sup>World Labs</span>'
|
| 461 |
+
'</div>'
|
| 462 |
+
'<div class="mf-logos">'
|
| 463 |
+
'<img alt="Carnegie Mellon University" src="https://modality-forcing.github.io/static/images/cmu_logo.png">'
|
| 464 |
+
'<img alt="World Labs" src="https://modality-forcing.github.io/static/images/world_labs_logo.jpg" style="border-radius:12px;">'
|
| 465 |
+
'</div>'
|
| 466 |
+
'<div class="mf-venue">Preprint, 2026</div>'
|
| 467 |
+
'<div class="mf-links">'
|
| 468 |
+
f'<a class="mf-btn" href="{_PROJECT_URL}" target="_blank" rel="noopener">📄 Project Page</a>'
|
| 469 |
+
f'<a class="mf-btn" href="{_ARXIV_URL}" target="_blank" rel="noopener">📚 arXiv</a>'
|
| 470 |
+
f'<a class="mf-btn" href="{_CODE_URL}" target="_blank" rel="noopener">⌨ Code</a>'
|
| 471 |
+
'</div>'
|
| 472 |
+
'<div class="mf-pub-sub" style="text-align:center !important;">Modality '
|
| 473 |
+
'Forcing turns a pretrained text-to-image diffusion transformer into a '
|
| 474 |
+
'joint image-depth generator with a simple post-training recipe.</div>'
|
| 475 |
+
'</div>'
|
| 476 |
+
)
|
| 477 |
+
|
| 478 |
+
_INTRO_HTML = (
|
| 479 |
+
'<div class="mf-intro">Type a scene and press <b>Generate</b>, or upload an '
|
| 480 |
+
'image to run <b>image→depth</b> mode instead.</div>'
|
| 481 |
+
)
|
| 482 |
+
|
| 483 |
+
_EXAMPLE_IMAGES = [
|
| 484 |
+
["assets/examples/alley.png"],
|
| 485 |
+
["assets/examples/yosemite.png"],
|
| 486 |
+
["assets/examples/castle.png"],
|
| 487 |
+
]
|
| 488 |
+
|
| 489 |
+
_EXAMPLE_PROMPTS = [
|
| 490 |
+
[DEFAULT_PROMPT],
|
| 491 |
+
["A sunlit Scandinavian living room with a linen sofa, a low oak coffee "
|
| 492 |
+
"table, and tall windows opening onto a snowy courtyard."],
|
| 493 |
+
["A misty pine forest at dawn, shafts of golden light cutting between the "
|
| 494 |
+
"trunks and a narrow dirt trail winding into the distance."],
|
| 495 |
+
["A cozy bookshop interior with floor-to-ceiling wooden shelves, a rolling "
|
| 496 |
+
"ladder, warm pendant lighting, and a worn leather reading chair."],
|
| 497 |
+
["A still life on a marble countertop: a bowl of ripe lemons, a ceramic "
|
| 498 |
+
"pitcher, and a sprig of rosemary lit by soft side light."],
|
| 499 |
+
["A coastal cliffside at golden hour overlooking a turquoise bay, with wild "
|
| 500 |
+
"grass in the foreground and distant sailboats on the water."],
|
| 501 |
+
]
|
| 502 |
+
|
| 503 |
+
with gr.Blocks(title="Modality Forcing — World Labs") as demo:
|
| 504 |
+
gr.HTML(_HEADER_HTML, elem_classes="mf-bare")
|
| 505 |
+
gr.HTML(_INTRO_HTML, elem_classes="mf-bare")
|
| 506 |
+
gr.HTML('<hr class="mf-rule">', elem_classes="mf-bare")
|
| 507 |
+
|
| 508 |
+
with gr.Row(equal_height=False):
|
| 509 |
+
with gr.Column(scale=2, min_width=320) as left_col:
|
| 510 |
+
gr.HTML('<div class="mf-sec">Input</div>', elem_classes="mf-bare")
|
| 511 |
+
prompt = gr.Textbox(
|
| 512 |
+
value=DEFAULT_PROMPT,
|
| 513 |
+
lines=4,
|
| 514 |
+
label="Scene prompt",
|
| 515 |
+
placeholder="Describe a scene to generate…",
|
| 516 |
+
)
|
| 517 |
+
input_image = gr.Image(
|
| 518 |
+
label="Optional input image (switches to image→depth mode)",
|
| 519 |
+
type="numpy", height=200, sources=("upload", "clipboard"),
|
| 520 |
+
)
|
| 521 |
+
btn = gr.Button("Generate", variant="primary", size="lg")
|
| 522 |
+
|
| 523 |
+
with gr.Accordion("Advanced settings", open=False):
|
| 524 |
+
with gr.Row():
|
| 525 |
+
num_steps = gr.Slider(
|
| 526 |
+
1, 80, value=50, step=1, label="Sampling steps")
|
| 527 |
+
cfg_scale = gr.Slider(
|
| 528 |
+
1.0, 8.0, value=4.0, step=0.1,
|
| 529 |
+
label="Guidance (CFG) — text mode only")
|
| 530 |
+
log2_alpha = gr.Slider(
|
| 531 |
+
-5.0, 5.0, value=5.0, step=0.5,
|
| 532 |
+
label="log2(alpha) — depth trajectory (>0 rgb-first, "
|
| 533 |
+
"cleaner depth; 0 diagonal; <0 depth-first) — text mode")
|
| 534 |
+
edge_rtol = gr.Slider(
|
| 535 |
+
0.0, 0.25, value=0.04, step=0.005,
|
| 536 |
+
label="Point-cloud depth-edge mask (rtol) — drop pixels at "
|
| 537 |
+
"depth jumps > this; lower = more aggressive, 0 = off")
|
| 538 |
+
sor_toggle = gr.Checkbox(
|
| 539 |
+
value=False,
|
| 540 |
+
label="Statistical outlier removal (point cloud) — drops "
|
| 541 |
+
"isolated floaters; can over-trim fine structures")
|
| 542 |
+
seed = gr.Number(value=0, precision=0, label="Seed")
|
| 543 |
+
refine_depth = gr.Checkbox(
|
| 544 |
+
value=True,
|
| 545 |
+
label="Refine depth: joint (CFG) → image→depth (CFG 1) "
|
| 546 |
+
"— text mode only",
|
| 547 |
+
)
|
| 548 |
+
|
| 549 |
+
status = gr.Textbox(label="Status", interactive=False)
|
| 550 |
+
|
| 551 |
+
with gr.Column(scale=3, min_width=380):
|
| 552 |
+
gr.HTML('<div class="mf-sec">Output</div>', elem_classes="mf-bare")
|
| 553 |
+
with gr.Row():
|
| 554 |
+
rgb_out = gr.Image(
|
| 555 |
+
label="RGB image", type="numpy", height=320, format="png")
|
| 556 |
+
depth_out = gr.Image(
|
| 557 |
+
label="Depth (disparity, magma)",
|
| 558 |
+
type="numpy", height=320, format="png")
|
| 559 |
+
with gr.Group():
|
| 560 |
+
cloud_out = gr.Model3D(
|
| 561 |
+
label="Interactive 3D point cloud",
|
| 562 |
+
clear_color=(0.12, 0.12, 0.14, 1.0),
|
| 563 |
+
zoom_speed=0.5, pan_speed=0.5,
|
| 564 |
+
height=520,
|
| 565 |
+
)
|
| 566 |
+
|
| 567 |
+
def _run_text_example(prompt_text):
|
| 568 |
+
# Cached example runs pin the UI defaults so the cache stays valid.
|
| 569 |
+
return generate(prompt_text, None, 50, 4.0, 0, True, 5.0, 0.04, False)
|
| 570 |
+
|
| 571 |
+
def _run_image_example(image):
|
| 572 |
+
return generate("", image, 50, 4.0, 0, True, 5.0, 0.04, False)
|
| 573 |
+
|
| 574 |
+
# Defined after the output components exist, rendered back into the left
|
| 575 |
+
# column. Cached: clicking an example serves precomputed results instead
|
| 576 |
+
# of spending GPU time ("lazy" = no cache rebuild at startup; the
|
| 577 |
+
# committed cache ships with the Space repo).
|
| 578 |
+
_CACHE_MODE = os.environ.get("EXAMPLES_CACHE_MODE", "lazy")
|
| 579 |
+
with left_col:
|
| 580 |
+
gr.Examples(
|
| 581 |
+
examples=_EXAMPLE_PROMPTS,
|
| 582 |
+
inputs=[prompt],
|
| 583 |
+
outputs=[rgb_out, depth_out, cloud_out, status],
|
| 584 |
+
fn=_run_text_example,
|
| 585 |
+
cache_examples=True,
|
| 586 |
+
cache_mode=_CACHE_MODE,
|
| 587 |
+
label="Example prompts",
|
| 588 |
+
)
|
| 589 |
+
gr.Examples(
|
| 590 |
+
examples=_EXAMPLE_IMAGES,
|
| 591 |
+
inputs=[input_image],
|
| 592 |
+
outputs=[rgb_out, depth_out, cloud_out, status],
|
| 593 |
+
fn=_run_image_example,
|
| 594 |
+
cache_examples=True,
|
| 595 |
+
cache_mode=_CACHE_MODE,
|
| 596 |
+
label="Example images (image → depth)",
|
| 597 |
+
)
|
| 598 |
+
|
| 599 |
+
gr.HTML(
|
| 600 |
+
'<div class="mf-footer">'
|
| 601 |
+
'<div class="mf-cta">Built by <a href="' + WORLD_LABS_URL + '" '
|
| 602 |
+
'target="_blank" rel="noopener noreferrer">World Labs</a></div>'
|
| 603 |
+
'<div class="mf-credit">worldlabs.ai · Modality Forcing</div>'
|
| 604 |
+
'</div>',
|
| 605 |
+
elem_classes="mf-bare",
|
| 606 |
+
)
|
| 607 |
+
|
| 608 |
+
btn.click(
|
| 609 |
+
generate,
|
| 610 |
+
inputs=[prompt, input_image, num_steps, cfg_scale, seed, refine_depth,
|
| 611 |
+
log2_alpha, edge_rtol, sor_toggle],
|
| 612 |
+
outputs=[rgb_out, depth_out, cloud_out, status],
|
| 613 |
+
)
|
| 614 |
+
|
| 615 |
+
# Adding an image switches the demo into image→depth mode, where the prompt
|
| 616 |
+
# is optional — clear it so it defaults to empty. The user can retype one.
|
| 617 |
+
input_image.change(
|
| 618 |
+
lambda img: "" if img is not None else gr.update(),
|
| 619 |
+
inputs=[input_image],
|
| 620 |
+
outputs=[prompt],
|
| 621 |
+
)
|
| 622 |
+
|
| 623 |
+
# The UI is designed light-only (paper-white cards, explicit light CSS). A
|
| 624 |
+
# visitor whose OS is in dark mode otherwise gets gradio's dark text colors on
|
| 625 |
+
# our light backgrounds — an unreadable mix. Redirect to ?__theme=light in
|
| 626 |
+
# <head>, before gradio boots, so every visitor gets the light theme.
|
| 627 |
+
_FORCE_LIGHT_HEAD = """
|
| 628 |
+
<script>
|
| 629 |
+
(function () {
|
| 630 |
+
const url = new URL(window.location);
|
| 631 |
+
if (url.searchParams.get("__theme") !== "light") {
|
| 632 |
+
url.searchParams.set("__theme", "light");
|
| 633 |
+
window.location.replace(url);
|
| 634 |
+
}
|
| 635 |
+
})();
|
| 636 |
+
</script>
|
| 637 |
+
"""
|
| 638 |
+
|
| 639 |
+
if __name__ == "__main__":
|
| 640 |
+
# Gradio 6 takes theme + css on launch() (not the Blocks constructor),
|
| 641 |
+
# so they must be passed here to actually apply on the Space.
|
| 642 |
+
demo.queue(max_size=4).launch(theme=_THEME, css=_CSS, head=_FORCE_LIGHT_HEAD)
|
assets/examples/alley.png
ADDED
|
Git LFS Details
|
assets/examples/castle.png
ADDED
|
Git LFS Details
|
assets/examples/yosemite.png
ADDED
|
Git LFS Details
|
flux_rgbd/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 2 |
+
# Copyright (c) 2026 World Labs.
|
| 3 |
+
"""flux_rgbd — joint text → RGB + depth diffusion on top of FLUX.2."""
|
| 4 |
+
|
| 5 |
+
__version__ = "0.1.0"
|
| 6 |
+
__all__ = ["FluxRGBDRunner"]
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def __getattr__(name):
|
| 10 |
+
if name == "FluxRGBDRunner":
|
| 11 |
+
from flux_rgbd.runner import FluxRGBDRunner
|
| 12 |
+
return FluxRGBDRunner
|
| 13 |
+
raise AttributeError(name)
|
flux_rgbd/_flux2/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 2 |
+
# Copyright 2025 Black Forest Labs.
|
| 3 |
+
"""Vendored FLUX.2 reference implementation.
|
| 4 |
+
|
| 5 |
+
This subpackage holds the unmodified FLUX.2 architecture (Apache-2.0,
|
| 6 |
+
Black Forest Labs upstream). Do not edit these files; they are
|
| 7 |
+
upstream-tracking. All modifications and extensions for RGB+D inference
|
| 8 |
+
live in the parent ``flux_rgbd`` package.
|
| 9 |
+
|
| 10 |
+
Upstream: https://github.com/black-forest-labs/flux2
|
| 11 |
+
"""
|
flux_rgbd/_flux2/autoencoder.py
ADDED
|
@@ -0,0 +1,499 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 2 |
+
# Copyright 2025 Black Forest Labs.
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
| 15 |
+
|
| 16 |
+
"""FLUX.2 autoencoder.
|
| 17 |
+
|
| 18 |
+
Adapted from the FLUX.2 codebase:
|
| 19 |
+
https://github.com/black-forest-labs/flux2
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
import math
|
| 23 |
+
from collections.abc import Sequence
|
| 24 |
+
|
| 25 |
+
import torch
|
| 26 |
+
from einops import rearrange
|
| 27 |
+
from jaxtyping import Float
|
| 28 |
+
from safetensors.torch import load_file as load_safetensors
|
| 29 |
+
from torch import Tensor, nn
|
| 30 |
+
|
| 31 |
+
__all__ = ["Flux2Encoder", "Flux2Decoder"]
|
| 32 |
+
|
| 33 |
+
# FLUX.2-dev autoencoder weights, pre-split into encoder + decoder
|
| 34 |
+
# safetensors. (BFL's canonical `ae.safetensors` is a combined file
|
| 35 |
+
# we'd have to filter by key prefix — easier to host the split files
|
| 36 |
+
# inside the model repo. Override via the FLUX_RGBD_LOCAL_AE_{...}
|
| 37 |
+
# env vars if you want to skip the Hub fetch entirely.)
|
| 38 |
+
DEFAULT_AE_REPO = "bartduis/modality_forcing"
|
| 39 |
+
DEFAULT_ENCODER_FILE = "ae_encoder.safetensors"
|
| 40 |
+
DEFAULT_DECODER_FILE = "ae_decoder.safetensors"
|
| 41 |
+
|
| 42 |
+
DEFAULT_RESOLUTION = 256
|
| 43 |
+
DEFAULT_IN_CHANNELS = 3
|
| 44 |
+
DEFAULT_CH = 128
|
| 45 |
+
DEFAULT_OUT_CHANNELS = 3
|
| 46 |
+
DEFAULT_CH_MULT = (1, 2, 4, 4)
|
| 47 |
+
DEFAULT_NUM_RES_BLOCKS = 2
|
| 48 |
+
DEFAULT_Z_CHANNELS = 32
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def _hf_download(repo_id: str, filename: str) -> str:
|
| 52 |
+
"""Resolve a HuggingFace Hub file path. Lazy-import hf_hub to keep the
|
| 53 |
+
base package importable in environments that pin huggingface_hub via
|
| 54 |
+
extras."""
|
| 55 |
+
from huggingface_hub import hf_hub_download
|
| 56 |
+
|
| 57 |
+
return hf_hub_download(repo_id=repo_id, filename=filename)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def _load_weights(repo_id: str, filename: str) -> dict[str, Tensor]:
|
| 61 |
+
# Allow callers to point at a local file via env var. Useful for tests
|
| 62 |
+
# and air-gapped environments.
|
| 63 |
+
import os
|
| 64 |
+
env_key = f"FLUX_RGBD_LOCAL_{filename.upper().replace('.', '_')}"
|
| 65 |
+
override = os.environ.get(env_key)
|
| 66 |
+
if override:
|
| 67 |
+
if not os.path.isfile(override):
|
| 68 |
+
raise FileNotFoundError(f"{env_key}={override} is set but not a file")
|
| 69 |
+
return load_safetensors(override)
|
| 70 |
+
path = _hf_download(repo_id, filename)
|
| 71 |
+
return load_safetensors(path)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def _swish(x: Tensor) -> Tensor:
|
| 75 |
+
return x * torch.sigmoid(x)
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
class AttnBlock(nn.Module):
|
| 79 |
+
"""Attention block for FLUX.2 autoencoder."""
|
| 80 |
+
|
| 81 |
+
def __init__(self, in_channels: int):
|
| 82 |
+
super().__init__()
|
| 83 |
+
self.in_channels = in_channels
|
| 84 |
+
|
| 85 |
+
self.norm = nn.GroupNorm(
|
| 86 |
+
num_groups=32, num_channels=in_channels, eps=1e-6, affine=True
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
self.q = nn.Conv2d(in_channels, in_channels, kernel_size=1)
|
| 90 |
+
self.k = nn.Conv2d(in_channels, in_channels, kernel_size=1)
|
| 91 |
+
self.v = nn.Conv2d(in_channels, in_channels, kernel_size=1)
|
| 92 |
+
self.proj_out = nn.Conv2d(in_channels, in_channels, kernel_size=1)
|
| 93 |
+
|
| 94 |
+
def attention(self, h_: Tensor) -> Tensor:
|
| 95 |
+
h_ = self.norm(h_)
|
| 96 |
+
q = self.q(h_)
|
| 97 |
+
k = self.k(h_)
|
| 98 |
+
v = self.v(h_)
|
| 99 |
+
|
| 100 |
+
b, c, h, w = q.shape
|
| 101 |
+
q = rearrange(q, "b c h w -> b 1 (h w) c").contiguous()
|
| 102 |
+
k = rearrange(k, "b c h w -> b 1 (h w) c").contiguous()
|
| 103 |
+
v = rearrange(v, "b c h w -> b 1 (h w) c").contiguous()
|
| 104 |
+
h_ = nn.functional.scaled_dot_product_attention(q, k, v)
|
| 105 |
+
|
| 106 |
+
return rearrange(h_, "b 1 (h w) c -> b c h w", h=h, w=w, c=c, b=b)
|
| 107 |
+
|
| 108 |
+
def forward(self, x: Tensor) -> Tensor:
|
| 109 |
+
return x + self.proj_out(self.attention(x))
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
class ResnetBlock(nn.Module):
|
| 113 |
+
"""Resnet block for FLUX.2 autoencoder."""
|
| 114 |
+
|
| 115 |
+
def __init__(self, in_channels: int, out_channels: int):
|
| 116 |
+
super().__init__()
|
| 117 |
+
self.in_channels = in_channels
|
| 118 |
+
out_channels = in_channels if out_channels is None else out_channels
|
| 119 |
+
self.out_channels = out_channels
|
| 120 |
+
|
| 121 |
+
self.norm1 = nn.GroupNorm(
|
| 122 |
+
num_groups=32, num_channels=in_channels, eps=1e-6, affine=True
|
| 123 |
+
)
|
| 124 |
+
self.conv1 = nn.Conv2d(
|
| 125 |
+
in_channels, out_channels, kernel_size=3, stride=1, padding=1
|
| 126 |
+
)
|
| 127 |
+
self.norm2 = nn.GroupNorm(
|
| 128 |
+
num_groups=32, num_channels=out_channels, eps=1e-6, affine=True
|
| 129 |
+
)
|
| 130 |
+
self.conv2 = nn.Conv2d(
|
| 131 |
+
out_channels, out_channels, kernel_size=3, stride=1, padding=1
|
| 132 |
+
)
|
| 133 |
+
if self.in_channels != self.out_channels:
|
| 134 |
+
self.nin_shortcut = nn.Conv2d(
|
| 135 |
+
in_channels, out_channels, kernel_size=1, stride=1, padding=0
|
| 136 |
+
)
|
| 137 |
+
|
| 138 |
+
def forward(self, x):
|
| 139 |
+
h = x
|
| 140 |
+
h = self.norm1(h)
|
| 141 |
+
h = _swish(h)
|
| 142 |
+
h = self.conv1(h)
|
| 143 |
+
|
| 144 |
+
h = self.norm2(h)
|
| 145 |
+
h = _swish(h)
|
| 146 |
+
h = self.conv2(h)
|
| 147 |
+
|
| 148 |
+
if self.in_channels != self.out_channels:
|
| 149 |
+
x = self.nin_shortcut(x)
|
| 150 |
+
|
| 151 |
+
return x + h
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
class Downsample(nn.Module):
|
| 155 |
+
"""Downsample block for FLUX.2 autoencoder."""
|
| 156 |
+
|
| 157 |
+
def __init__(self, in_channels: int):
|
| 158 |
+
super().__init__()
|
| 159 |
+
# no asymmetric padding in torch conv, must do it ourselves
|
| 160 |
+
self.conv = nn.Conv2d(
|
| 161 |
+
in_channels, in_channels, kernel_size=3, stride=2, padding=0
|
| 162 |
+
)
|
| 163 |
+
|
| 164 |
+
def forward(self, x: Tensor):
|
| 165 |
+
pad = (0, 1, 0, 1)
|
| 166 |
+
x = nn.functional.pad(x, pad, mode="constant", value=0)
|
| 167 |
+
x = self.conv(x)
|
| 168 |
+
return x
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
class Upsample(nn.Module):
|
| 172 |
+
"""Upsample block for FLUX.2 autoencoder."""
|
| 173 |
+
|
| 174 |
+
def __init__(self, in_channels: int):
|
| 175 |
+
super().__init__()
|
| 176 |
+
self.conv = nn.Conv2d(
|
| 177 |
+
in_channels, in_channels, kernel_size=3, stride=1, padding=1
|
| 178 |
+
)
|
| 179 |
+
|
| 180 |
+
def forward(self, x: Tensor):
|
| 181 |
+
x = nn.functional.interpolate(x, scale_factor=2.0, mode="nearest")
|
| 182 |
+
x = self.conv(x)
|
| 183 |
+
return x
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
class Flux2Encoder(nn.Module):
|
| 187 |
+
"""Encoder for FLUX.2 autoencoder."""
|
| 188 |
+
|
| 189 |
+
def __init__(
|
| 190 |
+
self,
|
| 191 |
+
resolution: int = DEFAULT_RESOLUTION,
|
| 192 |
+
in_channels: int = DEFAULT_IN_CHANNELS,
|
| 193 |
+
ch: int = DEFAULT_CH,
|
| 194 |
+
ch_mult: Sequence[int] = DEFAULT_CH_MULT,
|
| 195 |
+
num_res_blocks: int = DEFAULT_NUM_RES_BLOCKS,
|
| 196 |
+
z_channels: int = DEFAULT_Z_CHANNELS,
|
| 197 |
+
repo_id: str = DEFAULT_AE_REPO,
|
| 198 |
+
filename: str = DEFAULT_ENCODER_FILE,
|
| 199 |
+
):
|
| 200 |
+
"""Initialize the FLUX.2 encoder.
|
| 201 |
+
|
| 202 |
+
Args:
|
| 203 |
+
resolution: The resolution of the input images.
|
| 204 |
+
in_channels: The number of channels in the input images.
|
| 205 |
+
ch: The number of channels in the encoder.
|
| 206 |
+
ch_mult: The number of channels in the encoder at each resolution.
|
| 207 |
+
num_res_blocks: The number of ResNet blocks in each downsampling block.
|
| 208 |
+
z_channels: The number of channels in the latent space.
|
| 209 |
+
repo_id: HuggingFace Hub repository id from which to load weights.
|
| 210 |
+
filename: Filename of the safetensors weights within `repo_id`.
|
| 211 |
+
"""
|
| 212 |
+
super().__init__()
|
| 213 |
+
self._repo_id = repo_id
|
| 214 |
+
self._filename = filename
|
| 215 |
+
|
| 216 |
+
self.quant_conv = torch.nn.Conv2d(2 * z_channels, 2 * z_channels, 1)
|
| 217 |
+
self.ch = ch
|
| 218 |
+
self.num_resolutions = len(ch_mult)
|
| 219 |
+
self.num_res_blocks = num_res_blocks
|
| 220 |
+
self.resolution = resolution
|
| 221 |
+
self.in_channels = in_channels
|
| 222 |
+
# downsampling
|
| 223 |
+
self.conv_in = nn.Conv2d(
|
| 224 |
+
in_channels, self.ch, kernel_size=3, stride=1, padding=1
|
| 225 |
+
)
|
| 226 |
+
|
| 227 |
+
curr_res = resolution
|
| 228 |
+
in_ch_mult = (1,) + tuple(ch_mult)
|
| 229 |
+
self.in_ch_mult = in_ch_mult
|
| 230 |
+
self.down = nn.ModuleList()
|
| 231 |
+
block_in = self.ch
|
| 232 |
+
for i_level in range(self.num_resolutions):
|
| 233 |
+
block = nn.ModuleList()
|
| 234 |
+
attn = nn.ModuleList()
|
| 235 |
+
block_in = ch * in_ch_mult[i_level]
|
| 236 |
+
block_out = ch * ch_mult[i_level]
|
| 237 |
+
for _ in range(self.num_res_blocks):
|
| 238 |
+
block.append(ResnetBlock(in_channels=block_in, out_channels=block_out))
|
| 239 |
+
block_in = block_out
|
| 240 |
+
down = nn.Module()
|
| 241 |
+
down.block = block
|
| 242 |
+
down.attn = attn
|
| 243 |
+
if i_level != self.num_resolutions - 1:
|
| 244 |
+
down.downsample = Downsample(block_in)
|
| 245 |
+
curr_res = curr_res // 2
|
| 246 |
+
self.down.append(down)
|
| 247 |
+
|
| 248 |
+
# middle
|
| 249 |
+
self.mid = nn.Module()
|
| 250 |
+
self.mid.block_1 = ResnetBlock(in_channels=block_in, out_channels=block_in)
|
| 251 |
+
self.mid.attn_1 = AttnBlock(block_in)
|
| 252 |
+
self.mid.block_2 = ResnetBlock(in_channels=block_in, out_channels=block_in)
|
| 253 |
+
|
| 254 |
+
# end
|
| 255 |
+
self.norm_out = nn.GroupNorm(
|
| 256 |
+
num_groups=32, num_channels=block_in, eps=1e-6, affine=True
|
| 257 |
+
)
|
| 258 |
+
self.conv_out = nn.Conv2d(
|
| 259 |
+
block_in, 2 * z_channels, kernel_size=3, stride=1, padding=1
|
| 260 |
+
)
|
| 261 |
+
|
| 262 |
+
self.bn_eps = 1e-4
|
| 263 |
+
self.bn_momentum = 0.1
|
| 264 |
+
self.ps = [2, 2]
|
| 265 |
+
self.bn = torch.nn.BatchNorm2d(
|
| 266 |
+
math.prod(self.ps) * z_channels,
|
| 267 |
+
eps=self.bn_eps,
|
| 268 |
+
momentum=self.bn_momentum,
|
| 269 |
+
affine=False,
|
| 270 |
+
track_running_stats=True,
|
| 271 |
+
)
|
| 272 |
+
|
| 273 |
+
def load_weights(self):
|
| 274 |
+
"""Fetch and load weights from HuggingFace Hub."""
|
| 275 |
+
self.load_state_dict(_load_weights(self._repo_id, self._filename))
|
| 276 |
+
|
| 277 |
+
def _forward(self, x: Float[Tensor, "n cx hx wx"]) -> Float[Tensor, "n cz hz wz"]:
|
| 278 |
+
# downsampling
|
| 279 |
+
hs = [self.conv_in(x)]
|
| 280 |
+
for i_level in range(self.num_resolutions):
|
| 281 |
+
for i_block in range(self.num_res_blocks):
|
| 282 |
+
h = self.down[i_level].block[i_block](hs[-1])
|
| 283 |
+
if len(self.down[i_level].attn) > 0:
|
| 284 |
+
h = self.down[i_level].attn[i_block](h)
|
| 285 |
+
hs.append(h)
|
| 286 |
+
if i_level != self.num_resolutions - 1:
|
| 287 |
+
hs.append(self.down[i_level].downsample(hs[-1]))
|
| 288 |
+
|
| 289 |
+
# middle
|
| 290 |
+
h = hs[-1]
|
| 291 |
+
h = self.mid.block_1(h)
|
| 292 |
+
h = self.mid.attn_1(h)
|
| 293 |
+
h = self.mid.block_2(h)
|
| 294 |
+
# end
|
| 295 |
+
h = self.norm_out(h)
|
| 296 |
+
h = _swish(h)
|
| 297 |
+
h = self.conv_out(h)
|
| 298 |
+
h = self.quant_conv(h)
|
| 299 |
+
return h
|
| 300 |
+
|
| 301 |
+
def _normalize(self, z: Float[Tensor, "n cz hz wz"]) -> Float[Tensor, "n cz hz wz"]:
|
| 302 |
+
self.bn.eval()
|
| 303 |
+
return self.bn(z)
|
| 304 |
+
|
| 305 |
+
def forward(
|
| 306 |
+
self, x: Float[Tensor, "... hx wx cx"]
|
| 307 |
+
) -> Float[Tensor, "... hz wz cz"]:
|
| 308 |
+
"""Encode images to latents.
|
| 309 |
+
|
| 310 |
+
Args:
|
| 311 |
+
x: Images to encode.
|
| 312 |
+
|
| 313 |
+
Returns:
|
| 314 |
+
The encoded latents. The latents will be normalized using the running
|
| 315 |
+
statistics from training.
|
| 316 |
+
"""
|
| 317 |
+
*batch_shape, _, _, _ = x.shape
|
| 318 |
+
# Convert to NCHW.
|
| 319 |
+
x = rearrange(x, "... h w c -> (...) c h w")
|
| 320 |
+
|
| 321 |
+
# Compute latents.
|
| 322 |
+
moments = self._forward(x)
|
| 323 |
+
mean = torch.chunk(moments, 2, dim=1)[0]
|
| 324 |
+
z = rearrange(
|
| 325 |
+
mean,
|
| 326 |
+
"... c (i pi) (j pj) -> ... (c pi pj) i j",
|
| 327 |
+
pi=self.ps[0],
|
| 328 |
+
pj=self.ps[1],
|
| 329 |
+
)
|
| 330 |
+
z = self._normalize(z)
|
| 331 |
+
|
| 332 |
+
# Convert back to NHWC and restore batch shape.
|
| 333 |
+
z = rearrange(z, "... c h w -> ... h w c")
|
| 334 |
+
return z.reshape(*batch_shape, *z.shape[-3:])
|
| 335 |
+
|
| 336 |
+
|
| 337 |
+
class Flux2Decoder(nn.Module):
|
| 338 |
+
"""Decoder for FLUX.2 autoencoder."""
|
| 339 |
+
|
| 340 |
+
def __init__(
|
| 341 |
+
self,
|
| 342 |
+
ch: int = DEFAULT_CH,
|
| 343 |
+
out_ch: int = DEFAULT_OUT_CHANNELS,
|
| 344 |
+
ch_mult: Sequence[int] = DEFAULT_CH_MULT,
|
| 345 |
+
num_res_blocks: int = DEFAULT_NUM_RES_BLOCKS,
|
| 346 |
+
in_channels: int = DEFAULT_IN_CHANNELS,
|
| 347 |
+
resolution: int = DEFAULT_RESOLUTION,
|
| 348 |
+
z_channels: int = DEFAULT_Z_CHANNELS,
|
| 349 |
+
repo_id: str = DEFAULT_AE_REPO,
|
| 350 |
+
filename: str = DEFAULT_DECODER_FILE,
|
| 351 |
+
):
|
| 352 |
+
"""Initialize the FLUX.2 decoder.
|
| 353 |
+
|
| 354 |
+
Args:
|
| 355 |
+
ch: The number of channels in the decoder.
|
| 356 |
+
out_ch: The number of channels in the output.
|
| 357 |
+
ch_mult: The number of channels in the decoder at each resolution.
|
| 358 |
+
num_res_blocks: The number of ResNet blocks in each upsampling block.
|
| 359 |
+
in_channels: The number of channels in the input images.
|
| 360 |
+
resolution: The resolution of the input images.
|
| 361 |
+
z_channels: The number of channels in the latent space.
|
| 362 |
+
repo_id: HuggingFace Hub repository id from which to load weights.
|
| 363 |
+
filename: Filename of the safetensors weights within `repo_id`.
|
| 364 |
+
"""
|
| 365 |
+
super().__init__()
|
| 366 |
+
self._repo_id = repo_id
|
| 367 |
+
self._filename = filename
|
| 368 |
+
|
| 369 |
+
self.post_quant_conv = torch.nn.Conv2d(z_channels, z_channels, 1)
|
| 370 |
+
self.ch = ch
|
| 371 |
+
self.num_resolutions = len(ch_mult)
|
| 372 |
+
self.num_res_blocks = num_res_blocks
|
| 373 |
+
self.resolution = resolution
|
| 374 |
+
self.in_channels = in_channels
|
| 375 |
+
self.ffactor = 2 ** (self.num_resolutions - 1)
|
| 376 |
+
|
| 377 |
+
# compute in_ch_mult, block_in and curr_res at lowest res
|
| 378 |
+
block_in = ch * ch_mult[self.num_resolutions - 1]
|
| 379 |
+
curr_res = resolution // 2 ** (self.num_resolutions - 1)
|
| 380 |
+
self.z_shape = (1, z_channels, curr_res, curr_res)
|
| 381 |
+
|
| 382 |
+
# z to block_in
|
| 383 |
+
self.conv_in = nn.Conv2d(
|
| 384 |
+
z_channels, block_in, kernel_size=3, stride=1, padding=1
|
| 385 |
+
)
|
| 386 |
+
|
| 387 |
+
# middle
|
| 388 |
+
self.mid = nn.Module()
|
| 389 |
+
self.mid.block_1 = ResnetBlock(in_channels=block_in, out_channels=block_in)
|
| 390 |
+
self.mid.attn_1 = AttnBlock(block_in)
|
| 391 |
+
self.mid.block_2 = ResnetBlock(in_channels=block_in, out_channels=block_in)
|
| 392 |
+
|
| 393 |
+
# upsampling
|
| 394 |
+
self.up = nn.ModuleList()
|
| 395 |
+
for i_level in reversed(range(self.num_resolutions)):
|
| 396 |
+
block = nn.ModuleList()
|
| 397 |
+
attn = nn.ModuleList()
|
| 398 |
+
block_out = ch * ch_mult[i_level]
|
| 399 |
+
for _ in range(self.num_res_blocks + 1):
|
| 400 |
+
block.append(ResnetBlock(in_channels=block_in, out_channels=block_out))
|
| 401 |
+
block_in = block_out
|
| 402 |
+
up = nn.Module()
|
| 403 |
+
up.block = block
|
| 404 |
+
up.attn = attn
|
| 405 |
+
if i_level != 0:
|
| 406 |
+
up.upsample = Upsample(block_in)
|
| 407 |
+
curr_res = curr_res * 2
|
| 408 |
+
self.up.insert(0, up) # prepend to get consistent order
|
| 409 |
+
|
| 410 |
+
# end
|
| 411 |
+
self.norm_out = nn.GroupNorm(
|
| 412 |
+
num_groups=32, num_channels=block_in, eps=1e-6, affine=True
|
| 413 |
+
)
|
| 414 |
+
self.conv_out = nn.Conv2d(block_in, out_ch, kernel_size=3, stride=1, padding=1)
|
| 415 |
+
|
| 416 |
+
self.bn_eps = 1e-4
|
| 417 |
+
self.bn_momentum = 0.1
|
| 418 |
+
self.ps = [2, 2]
|
| 419 |
+
self.bn = torch.nn.BatchNorm2d(
|
| 420 |
+
math.prod(self.ps) * z_channels,
|
| 421 |
+
eps=self.bn_eps,
|
| 422 |
+
momentum=self.bn_momentum,
|
| 423 |
+
affine=False,
|
| 424 |
+
track_running_stats=True,
|
| 425 |
+
)
|
| 426 |
+
|
| 427 |
+
def load_weights(self):
|
| 428 |
+
"""Fetch and load weights from HuggingFace Hub."""
|
| 429 |
+
self.load_state_dict(_load_weights(self._repo_id, self._filename))
|
| 430 |
+
|
| 431 |
+
def _forward(self, z: Float[Tensor, "n cz hz wz"]) -> Float[Tensor, "n cx hx wx"]:
|
| 432 |
+
z = self.post_quant_conv(z)
|
| 433 |
+
|
| 434 |
+
# get dtype for proper tracing
|
| 435 |
+
upscale_dtype = next(self.up.parameters()).dtype
|
| 436 |
+
|
| 437 |
+
# z to block_in
|
| 438 |
+
h = self.conv_in(z)
|
| 439 |
+
|
| 440 |
+
# middle
|
| 441 |
+
h = self.mid.block_1(h)
|
| 442 |
+
h = self.mid.attn_1(h)
|
| 443 |
+
h = self.mid.block_2(h)
|
| 444 |
+
|
| 445 |
+
# cast to proper dtype
|
| 446 |
+
h = h.to(upscale_dtype)
|
| 447 |
+
# upsampling
|
| 448 |
+
for i_level in reversed(range(self.num_resolutions)):
|
| 449 |
+
for i_block in range(self.num_res_blocks + 1):
|
| 450 |
+
h = self.up[i_level].block[i_block](h)
|
| 451 |
+
if len(self.up[i_level].attn) > 0:
|
| 452 |
+
h = self.up[i_level].attn[i_block](h)
|
| 453 |
+
if i_level != 0:
|
| 454 |
+
h = self.up[i_level].upsample(h)
|
| 455 |
+
|
| 456 |
+
# end
|
| 457 |
+
h = self.norm_out(h)
|
| 458 |
+
h = _swish(h)
|
| 459 |
+
h = self.conv_out(h)
|
| 460 |
+
return h
|
| 461 |
+
|
| 462 |
+
def _inv_normalize(
|
| 463 |
+
self, z: Float[Tensor, "n cz hz wz"]
|
| 464 |
+
) -> Float[Tensor, "n cz hz wz"]:
|
| 465 |
+
self.bn.eval()
|
| 466 |
+
s = torch.sqrt(self.bn.running_var.view(1, -1, 1, 1) + self.bn_eps)
|
| 467 |
+
m = self.bn.running_mean.view(1, -1, 1, 1)
|
| 468 |
+
return z * s + m
|
| 469 |
+
|
| 470 |
+
def forward(
|
| 471 |
+
self, z: Float[Tensor, "... cz hz wz"]
|
| 472 |
+
) -> Float[Tensor, "... cx hx wx"]:
|
| 473 |
+
"""Decode latents to images.
|
| 474 |
+
|
| 475 |
+
Args:
|
| 476 |
+
z: Image latents normalized to have zero mean and unit variance.
|
| 477 |
+
|
| 478 |
+
Returns:
|
| 479 |
+
Decoded images.
|
| 480 |
+
"""
|
| 481 |
+
*batch_shape, _, _, _ = z.shape
|
| 482 |
+
|
| 483 |
+
# Convert to NCHW.
|
| 484 |
+
z = rearrange(z, "... h w c -> (...) c h w")
|
| 485 |
+
|
| 486 |
+
# Denormalize and evaluate decoder.
|
| 487 |
+
z = self._inv_normalize(z)
|
| 488 |
+
z = rearrange(
|
| 489 |
+
z,
|
| 490 |
+
"... (c pi pj) i j -> ... c (i pi) (j pj)",
|
| 491 |
+
pi=self.ps[0],
|
| 492 |
+
pj=self.ps[1],
|
| 493 |
+
)
|
| 494 |
+
dec = self._forward(z)
|
| 495 |
+
|
| 496 |
+
# Convert back to NHWC and restore batch shape.
|
| 497 |
+
dec = rearrange(dec, "... c h w -> ... h w c")
|
| 498 |
+
dec = dec.reshape(*batch_shape, *dec.shape[-3:])
|
| 499 |
+
return dec
|
flux_rgbd/_flux2/constants.py
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 2 |
+
# Copyright 2025 Black Forest Labs.
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Adapted from the FLUX.2 codebase:
|
| 11 |
+
# https://github.com/black-forest-labs/flux2
|
| 12 |
+
"""
|
| 13 |
+
Architecture parameters and other constants useful for FLUX.2 inference.
|
| 14 |
+
|
| 15 |
+
This module centralizes all constants used across the FLUX.2 model implementation,
|
| 16 |
+
including model architecture configurations for all public FLUX.2 variants, their
|
| 17 |
+
text encoder parameters, and system prompts for prompt upsampling.
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
from types import MappingProxyType
|
| 21 |
+
|
| 22 |
+
__all__ = [
|
| 23 |
+
"FLUX2_DEV_PARAMS",
|
| 24 |
+
"FLUX2_KLEIN_4B_PARAMS",
|
| 25 |
+
"FLUX2_KLEIN_9B_PARAMS",
|
| 26 |
+
"OUTPUT_LAYERS_MISTRAL",
|
| 27 |
+
"OUTPUT_LAYERS_QWEN3",
|
| 28 |
+
"MAX_LENGTH",
|
| 29 |
+
"UPSAMPLING_MAX_IMAGE_SIZE",
|
| 30 |
+
"SYSTEM_MESSAGE",
|
| 31 |
+
"SYSTEM_MESSAGE_UPSAMPLING_T2I",
|
| 32 |
+
"SYSTEM_MESSAGE_UPSAMPLING_I2I",
|
| 33 |
+
]
|
| 34 |
+
|
| 35 |
+
# =============================================================================
|
| 36 |
+
# Model Architecture Parameters
|
| 37 |
+
# =============================================================================
|
| 38 |
+
|
| 39 |
+
FLUX2_DEV_PARAMS = MappingProxyType(
|
| 40 |
+
{
|
| 41 |
+
"in_channels": 128,
|
| 42 |
+
"context_in_dim": 15360,
|
| 43 |
+
"hidden_size": 6144,
|
| 44 |
+
"num_heads": 48,
|
| 45 |
+
"depth": 8,
|
| 46 |
+
"depth_single_blocks": 48,
|
| 47 |
+
"axes_dim": (32, 32, 32, 32),
|
| 48 |
+
"theta": 2000,
|
| 49 |
+
"mlp_ratio": 3.0,
|
| 50 |
+
"use_guidance_embed": True,
|
| 51 |
+
}
|
| 52 |
+
)
|
| 53 |
+
"""
|
| 54 |
+
Architecture config for FLUX.2 [dev] 32B parameter model (guidance distilled).
|
| 55 |
+
"""
|
| 56 |
+
|
| 57 |
+
FLUX2_KLEIN_4B_PARAMS = MappingProxyType(
|
| 58 |
+
{
|
| 59 |
+
"in_channels": 128,
|
| 60 |
+
"context_in_dim": 7680,
|
| 61 |
+
"hidden_size": 3072,
|
| 62 |
+
"num_heads": 24,
|
| 63 |
+
"depth": 5,
|
| 64 |
+
"depth_single_blocks": 20,
|
| 65 |
+
"axes_dim": (32, 32, 32, 32),
|
| 66 |
+
"theta": 2000,
|
| 67 |
+
"mlp_ratio": 3.0,
|
| 68 |
+
"use_guidance_embed": False,
|
| 69 |
+
}
|
| 70 |
+
)
|
| 71 |
+
"""
|
| 72 |
+
Architecture config for FLUX.2 [klein] 4B parameter model (size distilled).
|
| 73 |
+
"""
|
| 74 |
+
|
| 75 |
+
FLUX2_KLEIN_9B_PARAMS = MappingProxyType(
|
| 76 |
+
{
|
| 77 |
+
"in_channels": 128,
|
| 78 |
+
"context_in_dim": 12288,
|
| 79 |
+
"hidden_size": 4096,
|
| 80 |
+
"num_heads": 32,
|
| 81 |
+
"depth": 8,
|
| 82 |
+
"depth_single_blocks": 24,
|
| 83 |
+
"axes_dim": (32, 32, 32, 32),
|
| 84 |
+
"theta": 2000,
|
| 85 |
+
"mlp_ratio": 3.0,
|
| 86 |
+
"use_guidance_embed": False,
|
| 87 |
+
}
|
| 88 |
+
)
|
| 89 |
+
"""
|
| 90 |
+
Immutable architecture configuration for FLUX.2 [klein]-9B model.
|
| 91 |
+
|
| 92 |
+
This is a mid-size 9B parameter model that balances quality and speed. It uses
|
| 93 |
+
Qwen3-8B as text encoder and provides higher quality than the 4B variant while
|
| 94 |
+
maintaining fast 4-step inference. The model is guidance-distilled.
|
| 95 |
+
|
| 96 |
+
Key architectural features:
|
| 97 |
+
- 24 single-stream transformer blocks (between 4B and dev)
|
| 98 |
+
- 4096 hidden dimensions with 32 attention heads
|
| 99 |
+
- No guidance embeddings (guidance-distilled into weights)
|
| 100 |
+
- Context dimension 12288 matches concatenated Qwen3-8B outputs
|
| 101 |
+
|
| 102 |
+
Default generation parameters (distilled variants):
|
| 103 |
+
- guidance: 1.0 (fixed, baked into weights)
|
| 104 |
+
- num_steps: 4 (fixed)
|
| 105 |
+
|
| 106 |
+
Base variant generation parameters:
|
| 107 |
+
- guidance: 4.0 (adjustable)
|
| 108 |
+
- num_steps: 50 (adjustable)
|
| 109 |
+
"""
|
| 110 |
+
|
| 111 |
+
# =============================================================================
|
| 112 |
+
# Text Encoder Configuration
|
| 113 |
+
# =============================================================================
|
| 114 |
+
|
| 115 |
+
OUTPUT_LAYERS_MISTRAL = (10, 20, 30)
|
| 116 |
+
"""
|
| 117 |
+
Layer indices to extract hidden states from Mistral-Small-3.2-24B text encoder.
|
| 118 |
+
|
| 119 |
+
The FLUX.2 [dev] model uses Mistral-Small as its text encoder and concatenates
|
| 120 |
+
hidden states from these three intermediate layers to form rich text embeddings.
|
| 121 |
+
"""
|
| 122 |
+
|
| 123 |
+
OUTPUT_LAYERS_QWEN3 = (9, 18, 27)
|
| 124 |
+
"""
|
| 125 |
+
Layer indices to extract hidden states from Qwen3 text encoder variants.
|
| 126 |
+
|
| 127 |
+
The FLUX.2 [klein] models (4B and 9B) use Qwen3 text encoders and concatenate
|
| 128 |
+
hidden states from these three intermediate layers to form text embeddings.
|
| 129 |
+
"""
|
| 130 |
+
|
| 131 |
+
MAX_LENGTH = 512
|
| 132 |
+
"""
|
| 133 |
+
Maximum sequence length for text encoder tokenization.
|
| 134 |
+
|
| 135 |
+
Text prompts are truncated or padded to this length during encoding. This
|
| 136 |
+
applies to both Mistral-Small and Qwen3 text encoders.
|
| 137 |
+
"""
|
| 138 |
+
|
| 139 |
+
UPSAMPLING_MAX_IMAGE_SIZE = 768**2
|
| 140 |
+
"""
|
| 141 |
+
Maximum pixel area for images during prompt upsampling.
|
| 142 |
+
|
| 143 |
+
When images are provided as conditioning for prompt upsampling (image-to-image
|
| 144 |
+
mode), they are resized to have at most this many pixels while preserving
|
| 145 |
+
aspect ratio. This prevents out-of-memory errors when processing large images.
|
| 146 |
+
"""
|
| 147 |
+
|
| 148 |
+
# =============================================================================
|
| 149 |
+
# System Messages for Prompt Upsampling
|
| 150 |
+
# =============================================================================
|
| 151 |
+
|
| 152 |
+
SYSTEM_MESSAGE = """You are an AI that reasons about image descriptions. You give \
|
| 153 |
+
structured responses focusing on object relationships, object attribution and \
|
| 154 |
+
actions without speculation."""
|
| 155 |
+
"""
|
| 156 |
+
Default system message for text encoder when generating image descriptions.
|
| 157 |
+
|
| 158 |
+
This system message is used for general-purpose text encoding tasks where the
|
| 159 |
+
model needs to reason about images in a structured, factual manner.
|
| 160 |
+
"""
|
| 161 |
+
|
| 162 |
+
SYSTEM_MESSAGE_UPSAMPLING_T2I = """You are an expert prompt engineer for FLUX.2 \
|
| 163 |
+
by Black Forest Labs. Rewrite user prompts to be more descriptive while \
|
| 164 |
+
strictly preserving their core subject and intent.
|
| 165 |
+
|
| 166 |
+
Guidelines:
|
| 167 |
+
1. Structure: Keep structured inputs structured (enhance within fields). \
|
| 168 |
+
Convert natural language to detailed paragraphs.
|
| 169 |
+
2. Details: Add concrete visual specifics - form, scale, textures, materials, \
|
| 170 |
+
lighting (quality, direction, color), shadows, spatial relationships, and \
|
| 171 |
+
environmental context.
|
| 172 |
+
3. Text in Images: Put ALL text in quotation marks, matching the prompt's \
|
| 173 |
+
language. Always provide explicit quoted text for objects that would contain \
|
| 174 |
+
text in reality (signs, labels, screens, etc.) - without it, the model \
|
| 175 |
+
generates gibberish.
|
| 176 |
+
|
| 177 |
+
Output only the revised prompt and nothing else."""
|
| 178 |
+
"""
|
| 179 |
+
System message for text-to-image prompt upsampling.
|
| 180 |
+
|
| 181 |
+
This prompt engineering template guides the text encoder to expand sparse user
|
| 182 |
+
prompts into detailed, visually-rich descriptions that improve image generation
|
| 183 |
+
quality. It emphasizes adding concrete visual details while preserving the
|
| 184 |
+
original intent.
|
| 185 |
+
"""
|
| 186 |
+
|
| 187 |
+
SYSTEM_MESSAGE_UPSAMPLING_I2I = """You are FLUX.2 by Black Forest Labs, an \
|
| 188 |
+
image-editing expert. You convert editing requests into one concise \
|
| 189 |
+
instruction (50-80 words, ~30 for brief requests).
|
| 190 |
+
|
| 191 |
+
Rules:
|
| 192 |
+
- Single instruction only, no commentary
|
| 193 |
+
- Use clear, analytical language (avoid "whimsical," "cascading," etc.)
|
| 194 |
+
- Specify what changes AND what stays the same (face, lighting, composition)
|
| 195 |
+
- Reference actual image elements
|
| 196 |
+
- Turn negatives into positives ("don't change X" → "keep X")
|
| 197 |
+
- Make abstractions concrete ("futuristic" → "glowing cyan neon, metallic panels")
|
| 198 |
+
- Keep content PG-13
|
| 199 |
+
|
| 200 |
+
Output only the final instruction in plain text and nothing else."""
|
| 201 |
+
"""
|
| 202 |
+
System message for image-to-image prompt upsampling.
|
| 203 |
+
|
| 204 |
+
This prompt engineering template guides the text encoder to convert free-form
|
| 205 |
+
image editing requests into concise, actionable instructions suitable for
|
| 206 |
+
image-to-image generation tasks.
|
| 207 |
+
"""
|
flux_rgbd/_flux2/model.py
ADDED
|
@@ -0,0 +1,674 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 2 |
+
# Copyright 2025 Black Forest Labs.
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Adapted from the FLUX.2 codebase:
|
| 11 |
+
# https://github.com/black-forest-labs/flux2
|
| 12 |
+
"""
|
| 13 |
+
FLUX.2 diffusion transformer architecture for image generation and editing.
|
| 14 |
+
|
| 15 |
+
This module implements the core transformer architecture for FLUX.2 models from
|
| 16 |
+
Black Forest Labs. The architecture uses dual-stream and single-stream transformer
|
| 17 |
+
blocks to process text and image latents for text-to-image and image-to-image tasks.
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
import math
|
| 21 |
+
|
| 22 |
+
import einops
|
| 23 |
+
import torch
|
| 24 |
+
from torch import Tensor, nn
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class Flux2(nn.Module):
|
| 28 |
+
"""
|
| 29 |
+
FLUX.2 diffusion transformer for image generation and editing.
|
| 30 |
+
|
| 31 |
+
This is a flow-matching diffusion model that uses a stack of dual-stream and
|
| 32 |
+
single-stream transformer blocks for text context and image latents. The
|
| 33 |
+
model supports text-to-image and image-to-image generation tasks.
|
| 34 |
+
|
| 35 |
+
Default parameter values match the FLUX.2 [klein] 4B architecture, which is
|
| 36 |
+
optimized for fast inference. For other model variants (klein-9B or dev),
|
| 37 |
+
use the parameters from :mod:`flux_rgbd._flux2.constants`.
|
| 38 |
+
"""
|
| 39 |
+
|
| 40 |
+
def __init__(
|
| 41 |
+
self,
|
| 42 |
+
in_channels: int = 128,
|
| 43 |
+
context_in_dim: int = 7680,
|
| 44 |
+
hidden_size: int = 3072,
|
| 45 |
+
num_heads: int = 24,
|
| 46 |
+
depth: int = 5,
|
| 47 |
+
depth_single_blocks: int = 20,
|
| 48 |
+
axes_dim: tuple[int, int, int, int] = (32, 32, 32, 32),
|
| 49 |
+
theta: int = 2000,
|
| 50 |
+
mlp_ratio: float = 3.0,
|
| 51 |
+
use_guidance_embed: bool = False,
|
| 52 |
+
):
|
| 53 |
+
"""
|
| 54 |
+
Args:
|
| 55 |
+
in_channels: Number of input channels for image latents. Matches the
|
| 56 |
+
output dimension of the autoencoder used to encode images.
|
| 57 |
+
context_in_dim: Dimension of text context embeddings from the text
|
| 58 |
+
encoder. This should match the concatenated output dimension of
|
| 59 |
+
the text encoder being used (7680 for Qwen3-4B, 12288 for Qwen3-8B,
|
| 60 |
+
15360 for Mistral-Small).
|
| 61 |
+
hidden_size: Hidden dimension size for transformer blocks. All
|
| 62 |
+
attention and MLP operations use this dimension internally.
|
| 63 |
+
num_heads: Number of attention heads in multi-head attention layers.
|
| 64 |
+
Must evenly divide `hidden_size`.
|
| 65 |
+
depth: Number of dual-stream transformer blocks. These blocks process
|
| 66 |
+
text and image streams separately with cross-attention.
|
| 67 |
+
depth_single_blocks: Number of single-stream transformer blocks. These
|
| 68 |
+
blocks process the concatenated text+image sequence.
|
| 69 |
+
axes_dim: Tuple of 4 integers specifying the dimensionality for each
|
| 70 |
+
axis in rotary embeddings. Must sum to `hidden_size / num_heads`.
|
| 71 |
+
theta: Base frequency for rotary position embeddings (RoPE). Higher
|
| 72 |
+
values result in slower position encoding rotation.
|
| 73 |
+
mlp_ratio: Expansion ratio for MLP hidden dimension relative to
|
| 74 |
+
`hidden_size`. MLP hidden dim = `hidden_size * mlp_ratio`.
|
| 75 |
+
use_guidance_embed: Whether to include guidance scale embeddings for
|
| 76 |
+
classifier-free guidance. Set to False for distilled models where
|
| 77 |
+
guidance is baked into weights.
|
| 78 |
+
"""
|
| 79 |
+
super().__init__()
|
| 80 |
+
|
| 81 |
+
self.in_channels = in_channels
|
| 82 |
+
self.out_channels = in_channels
|
| 83 |
+
if hidden_size % num_heads != 0:
|
| 84 |
+
raise ValueError(
|
| 85 |
+
f"Hidden size {hidden_size} must be divisible by "
|
| 86 |
+
f"num_heads {num_heads}"
|
| 87 |
+
)
|
| 88 |
+
pe_dim = hidden_size // num_heads
|
| 89 |
+
if sum(axes_dim) != pe_dim:
|
| 90 |
+
raise ValueError(f"Got {axes_dim} but expected positional dim {pe_dim}")
|
| 91 |
+
self.hidden_size = hidden_size
|
| 92 |
+
self.num_heads = num_heads
|
| 93 |
+
self.pe_embedder = EmbedND(dim=pe_dim, theta=theta, axes_dim=axes_dim)
|
| 94 |
+
self.img_in = nn.Linear(self.in_channels, self.hidden_size, bias=False)
|
| 95 |
+
self.time_in = MLPEmbedder(
|
| 96 |
+
in_dim=256, hidden_dim=self.hidden_size, disable_bias=True
|
| 97 |
+
)
|
| 98 |
+
self.txt_in = nn.Linear(context_in_dim, self.hidden_size, bias=False)
|
| 99 |
+
|
| 100 |
+
self.use_guidance_embed = use_guidance_embed
|
| 101 |
+
if self.use_guidance_embed:
|
| 102 |
+
self.guidance_in = MLPEmbedder(
|
| 103 |
+
in_dim=256, hidden_dim=self.hidden_size, disable_bias=True
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
double_blocks = [
|
| 107 |
+
DoubleStreamBlock(self.hidden_size, self.num_heads, mlp_ratio=mlp_ratio)
|
| 108 |
+
for _ in range(depth)
|
| 109 |
+
]
|
| 110 |
+
self.double_blocks = nn.ModuleList(double_blocks)
|
| 111 |
+
|
| 112 |
+
single_blocks = [
|
| 113 |
+
SingleStreamBlock(self.hidden_size, self.num_heads, mlp_ratio=mlp_ratio)
|
| 114 |
+
for _ in range(depth_single_blocks)
|
| 115 |
+
]
|
| 116 |
+
self.single_blocks = nn.ModuleList(single_blocks)
|
| 117 |
+
|
| 118 |
+
self.double_stream_modulation_img = Modulation(
|
| 119 |
+
self.hidden_size, double=True, disable_bias=True
|
| 120 |
+
)
|
| 121 |
+
self.double_stream_modulation_txt = Modulation(
|
| 122 |
+
self.hidden_size, double=True, disable_bias=True
|
| 123 |
+
)
|
| 124 |
+
self.single_stream_modulation = Modulation(
|
| 125 |
+
self.hidden_size, double=False, disable_bias=True
|
| 126 |
+
)
|
| 127 |
+
|
| 128 |
+
self.final_layer = LastLayer(self.hidden_size, self.out_channels)
|
| 129 |
+
|
| 130 |
+
def forward(
|
| 131 |
+
self,
|
| 132 |
+
x: Tensor,
|
| 133 |
+
x_ids: Tensor,
|
| 134 |
+
timesteps: Tensor,
|
| 135 |
+
ctx: Tensor,
|
| 136 |
+
ctx_ids: Tensor,
|
| 137 |
+
guidance: Tensor | None,
|
| 138 |
+
):
|
| 139 |
+
num_txt_tokens = ctx.shape[1]
|
| 140 |
+
|
| 141 |
+
timestep_emb = timestep_embedding(timesteps, 256)
|
| 142 |
+
vec = self.time_in(timestep_emb)
|
| 143 |
+
if self.use_guidance_embed:
|
| 144 |
+
guidance_emb = timestep_embedding(guidance, 256)
|
| 145 |
+
vec = vec + self.guidance_in(guidance_emb)
|
| 146 |
+
|
| 147 |
+
double_block_mod_img = self.double_stream_modulation_img(vec)
|
| 148 |
+
double_block_mod_txt = self.double_stream_modulation_txt(vec)
|
| 149 |
+
single_block_mod, _ = self.single_stream_modulation(vec)
|
| 150 |
+
|
| 151 |
+
img = self.img_in(x)
|
| 152 |
+
txt = self.txt_in(ctx)
|
| 153 |
+
|
| 154 |
+
pe_x = self.pe_embedder(x_ids)
|
| 155 |
+
pe_ctx = self.pe_embedder(ctx_ids)
|
| 156 |
+
|
| 157 |
+
for block in self.double_blocks:
|
| 158 |
+
img, txt = block(
|
| 159 |
+
img,
|
| 160 |
+
txt,
|
| 161 |
+
pe_x,
|
| 162 |
+
pe_ctx,
|
| 163 |
+
double_block_mod_img,
|
| 164 |
+
double_block_mod_txt,
|
| 165 |
+
)
|
| 166 |
+
|
| 167 |
+
img = torch.cat((txt, img), dim=1)
|
| 168 |
+
pe = torch.cat((pe_ctx, pe_x), dim=2)
|
| 169 |
+
|
| 170 |
+
for block in self.single_blocks:
|
| 171 |
+
img = block(
|
| 172 |
+
img,
|
| 173 |
+
pe,
|
| 174 |
+
single_block_mod,
|
| 175 |
+
)
|
| 176 |
+
|
| 177 |
+
img = img[:, num_txt_tokens:, ...]
|
| 178 |
+
|
| 179 |
+
img = self.final_layer(img, vec)
|
| 180 |
+
return img
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
class SelfAttention(nn.Module):
|
| 184 |
+
"""
|
| 185 |
+
Multi-head self-attention with QK normalization.
|
| 186 |
+
|
| 187 |
+
This module computes query, key, and value projections in a single linear layer,
|
| 188 |
+
applies RMS normalization to queries and keys, then performs attention and projects
|
| 189 |
+
the output back to the original dimension.
|
| 190 |
+
"""
|
| 191 |
+
|
| 192 |
+
def __init__(self, dim: int, num_heads: int = 8):
|
| 193 |
+
"""
|
| 194 |
+
Args:
|
| 195 |
+
dim: Hidden dimension size. Must be divisible by `num_heads`.
|
| 196 |
+
num_heads: Number of parallel attention heads.
|
| 197 |
+
"""
|
| 198 |
+
super().__init__()
|
| 199 |
+
self.num_heads = num_heads
|
| 200 |
+
head_dim = dim // num_heads
|
| 201 |
+
self.qkv = nn.Linear(dim, dim * 3, bias=False)
|
| 202 |
+
|
| 203 |
+
self.norm = QKNorm(head_dim)
|
| 204 |
+
self.proj = nn.Linear(dim, dim, bias=False)
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
class SiLUActivation(nn.Module):
|
| 208 |
+
"""
|
| 209 |
+
Gated activation using SiLU (Swish) function.
|
| 210 |
+
|
| 211 |
+
This module splits the input tensor along the last dimension, applies SiLU to one
|
| 212 |
+
half, and element-wise multiplies it with the other half. This is commonly known
|
| 213 |
+
as SwiGLU when used in MLP layers.
|
| 214 |
+
"""
|
| 215 |
+
|
| 216 |
+
def __init__(self):
|
| 217 |
+
super().__init__()
|
| 218 |
+
self.gate_fn = nn.SiLU()
|
| 219 |
+
|
| 220 |
+
def forward(self, x: Tensor) -> Tensor:
|
| 221 |
+
"""
|
| 222 |
+
Args:
|
| 223 |
+
x: Input tensor of shape `(..., 2 * dim)` where the last dimension will
|
| 224 |
+
be split into two equal parts for gating.
|
| 225 |
+
|
| 226 |
+
Returns:
|
| 227 |
+
Gated output of shape `(..., dim)`.
|
| 228 |
+
"""
|
| 229 |
+
x1, x2 = x.chunk(2, dim=-1)
|
| 230 |
+
return self.gate_fn(x1) * x2
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
class Modulation(nn.Module):
|
| 234 |
+
"""
|
| 235 |
+
Adaptive layer normalization (AdaLN) modulation layer.
|
| 236 |
+
|
| 237 |
+
This module generates scale, shift, and gate parameters for adaptive normalization
|
| 238 |
+
from timestep/guidance embeddings. For double-stream blocks, it produces two sets
|
| 239 |
+
of modulation parameters (one for each stream).
|
| 240 |
+
"""
|
| 241 |
+
|
| 242 |
+
def __init__(self, dim: int, double: bool, disable_bias: bool = False):
|
| 243 |
+
"""
|
| 244 |
+
Args:
|
| 245 |
+
dim: Hidden dimension size matching the transformer blocks.
|
| 246 |
+
double: If True, generates parameters for dual-stream blocks (6 params:
|
| 247 |
+
shift, scale, gate for each stream). If False, generates for
|
| 248 |
+
single-stream blocks (3 params: shift, scale, gate).
|
| 249 |
+
disable_bias: If True, the linear layer has no bias term.
|
| 250 |
+
"""
|
| 251 |
+
super().__init__()
|
| 252 |
+
self.is_double = double
|
| 253 |
+
self.multiplier = 6 if double else 3
|
| 254 |
+
self.lin = nn.Linear(dim, self.multiplier * dim, bias=not disable_bias)
|
| 255 |
+
|
| 256 |
+
def forward(self, vec: Tensor):
|
| 257 |
+
"""
|
| 258 |
+
Args:
|
| 259 |
+
vec: Timestep/guidance embedding of shape `(batch_size, dim)` or
|
| 260 |
+
`(batch_size, seq_len, dim)`.
|
| 261 |
+
|
| 262 |
+
Returns:
|
| 263 |
+
Tuple of modulation parameters. For single-stream: `(mod, None)` where
|
| 264 |
+
`mod` is a 3-tuple of (shift, scale, gate). For double-stream:
|
| 265 |
+
`(mod1, mod2)` where each is a 3-tuple for different streams.
|
| 266 |
+
"""
|
| 267 |
+
out = self.lin(nn.functional.silu(vec))
|
| 268 |
+
if out.ndim == 2:
|
| 269 |
+
out = out[:, None, :]
|
| 270 |
+
out = out.chunk(self.multiplier, dim=-1)
|
| 271 |
+
return out[:3], out[3:] if self.is_double else None
|
| 272 |
+
|
| 273 |
+
|
| 274 |
+
class LastLayer(nn.Module):
|
| 275 |
+
"""
|
| 276 |
+
Final output layer with adaptive layer normalization.
|
| 277 |
+
|
| 278 |
+
This module applies AdaLN-modulated normalization followed by a linear projection
|
| 279 |
+
to map transformer hidden states back to the output space (image latent channels).
|
| 280 |
+
"""
|
| 281 |
+
|
| 282 |
+
def __init__(self, hidden_size: int, out_channels: int):
|
| 283 |
+
"""
|
| 284 |
+
Args:
|
| 285 |
+
hidden_size: Hidden dimension of transformer blocks.
|
| 286 |
+
out_channels: Output dimension (number of latent channels).
|
| 287 |
+
"""
|
| 288 |
+
super().__init__()
|
| 289 |
+
self.norm_final = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6)
|
| 290 |
+
self.linear = nn.Linear(hidden_size, out_channels, bias=False)
|
| 291 |
+
self.adaLN_modulation = nn.Sequential( # pylint: disable=invalid-name
|
| 292 |
+
nn.SiLU(), nn.Linear(hidden_size, 2 * hidden_size, bias=False)
|
| 293 |
+
)
|
| 294 |
+
|
| 295 |
+
def forward(self, x: Tensor, vec: Tensor) -> Tensor:
|
| 296 |
+
"""
|
| 297 |
+
Args:
|
| 298 |
+
x: Hidden states from transformer as a tensor of shape
|
| 299 |
+
`(batch_size, seq_len, hidden_size)`.
|
| 300 |
+
vec: Timestep embedding of shape `(batch_size, hidden_size)`.
|
| 301 |
+
|
| 302 |
+
Returns:
|
| 303 |
+
Output tensor of shape `(batch_size, seq_len, out_channels)`.
|
| 304 |
+
"""
|
| 305 |
+
mod = self.adaLN_modulation(vec)
|
| 306 |
+
shift, scale = mod.chunk(2, dim=-1)
|
| 307 |
+
if shift.ndim == 2:
|
| 308 |
+
shift = shift[:, None, :]
|
| 309 |
+
scale = scale[:, None, :]
|
| 310 |
+
x = (1 + scale) * self.norm_final(x) + shift
|
| 311 |
+
x = self.linear(x)
|
| 312 |
+
return x
|
| 313 |
+
|
| 314 |
+
|
| 315 |
+
class SingleStreamBlock(nn.Module):
|
| 316 |
+
"""
|
| 317 |
+
Single-stream transformer block processing concatenated text+image tokens.
|
| 318 |
+
|
| 319 |
+
This block applies self-attention and MLP operations to a unified sequence of
|
| 320 |
+
text and image tokens. Both operations share pre-normalization and use adaptive
|
| 321 |
+
modulation from timestep embeddings.
|
| 322 |
+
"""
|
| 323 |
+
|
| 324 |
+
def __init__(self, hidden_size: int, num_heads: int, mlp_ratio: float = 4.0):
|
| 325 |
+
"""
|
| 326 |
+
Args:
|
| 327 |
+
hidden_size: Hidden dimension size for all linear projections.
|
| 328 |
+
num_heads: Number of attention heads. Must divide `hidden_size` evenly.
|
| 329 |
+
mlp_ratio: Ratio of MLP hidden dimension to `hidden_size`. The actual
|
| 330 |
+
MLP hidden dim is `int(hidden_size * mlp_ratio)`.
|
| 331 |
+
"""
|
| 332 |
+
super().__init__()
|
| 333 |
+
|
| 334 |
+
self.hidden_dim = hidden_size
|
| 335 |
+
self.num_heads = num_heads
|
| 336 |
+
head_dim = hidden_size // num_heads
|
| 337 |
+
self.scale = head_dim**-0.5
|
| 338 |
+
self.mlp_hidden_dim = int(hidden_size * mlp_ratio)
|
| 339 |
+
self.mlp_mult_factor = 2
|
| 340 |
+
|
| 341 |
+
self.linear1 = nn.Linear(
|
| 342 |
+
hidden_size,
|
| 343 |
+
hidden_size * 3 + self.mlp_hidden_dim * self.mlp_mult_factor,
|
| 344 |
+
bias=False,
|
| 345 |
+
)
|
| 346 |
+
self.linear2 = nn.Linear(
|
| 347 |
+
hidden_size + self.mlp_hidden_dim, hidden_size, bias=False
|
| 348 |
+
)
|
| 349 |
+
self.norm = QKNorm(head_dim)
|
| 350 |
+
|
| 351 |
+
self.hidden_size = hidden_size
|
| 352 |
+
self.pre_norm = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6)
|
| 353 |
+
|
| 354 |
+
self.mlp_act = SiLUActivation()
|
| 355 |
+
|
| 356 |
+
def forward(self, x: Tensor, pe: Tensor, mod: tuple[Tensor, Tensor]) -> Tensor:
|
| 357 |
+
mod_shift, mod_scale, mod_gate = mod
|
| 358 |
+
x_mod = (1 + mod_scale) * self.pre_norm(x) + mod_shift
|
| 359 |
+
|
| 360 |
+
qkv, mlp = torch.split(
|
| 361 |
+
self.linear1(x_mod),
|
| 362 |
+
[3 * self.hidden_size, self.mlp_hidden_dim * self.mlp_mult_factor],
|
| 363 |
+
dim=-1,
|
| 364 |
+
)
|
| 365 |
+
|
| 366 |
+
q, k, v = einops.rearrange(
|
| 367 |
+
qkv, "B L (K H D) -> K B H L D", K=3, H=self.num_heads
|
| 368 |
+
)
|
| 369 |
+
q, k = self.norm(q, k, v)
|
| 370 |
+
|
| 371 |
+
attn = attention(q, k, v, pe)
|
| 372 |
+
|
| 373 |
+
# Compute activation in mlp stream, cat again and run second linear layer.
|
| 374 |
+
output = self.linear2(torch.cat((attn, self.mlp_act(mlp)), 2))
|
| 375 |
+
return x + mod_gate * output
|
| 376 |
+
|
| 377 |
+
|
| 378 |
+
class DoubleStreamBlock(nn.Module):
|
| 379 |
+
"""
|
| 380 |
+
Dual-stream transformer block processing text and image tokens separately.
|
| 381 |
+
|
| 382 |
+
This block maintains separate streams for text and image tokens, each with their
|
| 383 |
+
own self-attention and MLP sublayers. Cross-stream information exchange happens
|
| 384 |
+
through joint attention where Q, K, V from both streams are concatenated.
|
| 385 |
+
"""
|
| 386 |
+
|
| 387 |
+
def __init__(self, hidden_size: int, num_heads: int, mlp_ratio: float):
|
| 388 |
+
"""
|
| 389 |
+
Args:
|
| 390 |
+
hidden_size: Hidden dimension size for all linear projections.
|
| 391 |
+
num_heads: Number of attention heads. Must divide `hidden_size` evenly.
|
| 392 |
+
mlp_ratio: Ratio of MLP hidden dimension to `hidden_size`.
|
| 393 |
+
"""
|
| 394 |
+
super().__init__()
|
| 395 |
+
mlp_hidden_dim = int(hidden_size * mlp_ratio)
|
| 396 |
+
self.num_heads = num_heads
|
| 397 |
+
assert (
|
| 398 |
+
hidden_size % num_heads == 0
|
| 399 |
+
), f"{hidden_size=} must be divisible by {num_heads=}"
|
| 400 |
+
|
| 401 |
+
self.hidden_size = hidden_size
|
| 402 |
+
self.img_norm1 = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6)
|
| 403 |
+
self.mlp_mult_factor = 2
|
| 404 |
+
|
| 405 |
+
self.img_attn = SelfAttention(dim=hidden_size, num_heads=num_heads)
|
| 406 |
+
|
| 407 |
+
self.img_norm2 = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6)
|
| 408 |
+
self.img_mlp = nn.Sequential(
|
| 409 |
+
nn.Linear(hidden_size, mlp_hidden_dim * self.mlp_mult_factor, bias=False),
|
| 410 |
+
SiLUActivation(),
|
| 411 |
+
nn.Linear(mlp_hidden_dim, hidden_size, bias=False),
|
| 412 |
+
)
|
| 413 |
+
|
| 414 |
+
self.txt_norm1 = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6)
|
| 415 |
+
self.txt_attn = SelfAttention(dim=hidden_size, num_heads=num_heads)
|
| 416 |
+
|
| 417 |
+
self.txt_norm2 = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6)
|
| 418 |
+
self.txt_mlp = nn.Sequential(
|
| 419 |
+
nn.Linear(
|
| 420 |
+
hidden_size,
|
| 421 |
+
mlp_hidden_dim * self.mlp_mult_factor,
|
| 422 |
+
bias=False,
|
| 423 |
+
),
|
| 424 |
+
SiLUActivation(),
|
| 425 |
+
nn.Linear(mlp_hidden_dim, hidden_size, bias=False),
|
| 426 |
+
)
|
| 427 |
+
|
| 428 |
+
def forward(
|
| 429 |
+
self,
|
| 430 |
+
img: Tensor,
|
| 431 |
+
txt: Tensor,
|
| 432 |
+
pe: Tensor,
|
| 433 |
+
pe_ctx: Tensor,
|
| 434 |
+
mod_img: tuple[Tensor, Tensor],
|
| 435 |
+
mod_txt: tuple[Tensor, Tensor],
|
| 436 |
+
) -> tuple[Tensor, Tensor]:
|
| 437 |
+
img_mod1, img_mod2 = mod_img
|
| 438 |
+
txt_mod1, txt_mod2 = mod_txt
|
| 439 |
+
|
| 440 |
+
img_mod1_shift, img_mod1_scale, img_mod1_gate = img_mod1
|
| 441 |
+
img_mod2_shift, img_mod2_scale, img_mod2_gate = img_mod2
|
| 442 |
+
txt_mod1_shift, txt_mod1_scale, txt_mod1_gate = txt_mod1
|
| 443 |
+
txt_mod2_shift, txt_mod2_scale, txt_mod2_gate = txt_mod2
|
| 444 |
+
|
| 445 |
+
# Prepare image for attention.
|
| 446 |
+
img_modulated = self.img_norm1(img)
|
| 447 |
+
img_modulated = (1 + img_mod1_scale) * img_modulated + img_mod1_shift
|
| 448 |
+
|
| 449 |
+
img_qkv = self.img_attn.qkv(img_modulated)
|
| 450 |
+
img_q, img_k, img_v = einops.rearrange(
|
| 451 |
+
img_qkv, "B L (K H D) -> K B H L D", K=3, H=self.num_heads
|
| 452 |
+
)
|
| 453 |
+
img_q, img_k = self.img_attn.norm(img_q, img_k, img_v)
|
| 454 |
+
|
| 455 |
+
# Prepare txt for attention.
|
| 456 |
+
txt_modulated = self.txt_norm1(txt)
|
| 457 |
+
txt_modulated = (1 + txt_mod1_scale) * txt_modulated + txt_mod1_shift
|
| 458 |
+
|
| 459 |
+
txt_qkv = self.txt_attn.qkv(txt_modulated)
|
| 460 |
+
txt_q, txt_k, txt_v = einops.rearrange(
|
| 461 |
+
txt_qkv, "B L (K H D) -> K B H L D", K=3, H=self.num_heads
|
| 462 |
+
)
|
| 463 |
+
txt_q, txt_k = self.txt_attn.norm(txt_q, txt_k, txt_v)
|
| 464 |
+
|
| 465 |
+
q = torch.cat((txt_q, img_q), dim=2)
|
| 466 |
+
k = torch.cat((txt_k, img_k), dim=2)
|
| 467 |
+
v = torch.cat((txt_v, img_v), dim=2)
|
| 468 |
+
|
| 469 |
+
pe = torch.cat((pe_ctx, pe), dim=2)
|
| 470 |
+
attn = attention(q, k, v, pe)
|
| 471 |
+
txt_attn, img_attn = attn[:, : txt_q.shape[2]], attn[:, txt_q.shape[2] :]
|
| 472 |
+
|
| 473 |
+
# Calculate the img blocks.
|
| 474 |
+
img = img + img_mod1_gate * self.img_attn.proj(img_attn)
|
| 475 |
+
img = img + img_mod2_gate * self.img_mlp(
|
| 476 |
+
(1 + img_mod2_scale) * (self.img_norm2(img)) + img_mod2_shift
|
| 477 |
+
)
|
| 478 |
+
|
| 479 |
+
# Calculate the txt blocks.
|
| 480 |
+
txt = txt + txt_mod1_gate * self.txt_attn.proj(txt_attn)
|
| 481 |
+
txt = txt + txt_mod2_gate * self.txt_mlp(
|
| 482 |
+
(1 + txt_mod2_scale) * (self.txt_norm2(txt)) + txt_mod2_shift
|
| 483 |
+
)
|
| 484 |
+
return img, txt
|
| 485 |
+
|
| 486 |
+
|
| 487 |
+
class MLPEmbedder(nn.Module):
|
| 488 |
+
"""
|
| 489 |
+
Two-layer MLP for embedding timestep and guidance values.
|
| 490 |
+
|
| 491 |
+
This simple MLP transforms scalar timestep or guidance embeddings (after
|
| 492 |
+
sinusoidal encoding) into the transformer's hidden dimension space.
|
| 493 |
+
"""
|
| 494 |
+
|
| 495 |
+
def __init__(self, in_dim: int, hidden_dim: int, disable_bias: bool = False):
|
| 496 |
+
"""
|
| 497 |
+
Args:
|
| 498 |
+
in_dim: Input dimension (typically 256 for sinusoidal embeddings).
|
| 499 |
+
hidden_dim: Output hidden dimension matching transformer blocks.
|
| 500 |
+
disable_bias: If True, linear layers have no bias terms.
|
| 501 |
+
"""
|
| 502 |
+
super().__init__()
|
| 503 |
+
self.in_layer = nn.Linear(in_dim, hidden_dim, bias=not disable_bias)
|
| 504 |
+
self.silu = nn.SiLU()
|
| 505 |
+
self.out_layer = nn.Linear(hidden_dim, hidden_dim, bias=not disable_bias)
|
| 506 |
+
|
| 507 |
+
def forward(self, x: Tensor) -> Tensor:
|
| 508 |
+
"""
|
| 509 |
+
Args:
|
| 510 |
+
x: Input embeddings of shape `(batch_size, in_dim)`.
|
| 511 |
+
|
| 512 |
+
Returns:
|
| 513 |
+
Projected embeddings of shape `(batch_size, hidden_dim)`.
|
| 514 |
+
"""
|
| 515 |
+
return self.out_layer(self.silu(self.in_layer(x)))
|
| 516 |
+
|
| 517 |
+
|
| 518 |
+
class EmbedND(nn.Module):
|
| 519 |
+
"""
|
| 520 |
+
N-dimensional rotary position embeddings (RoPE) for spatial-temporal tokens.
|
| 521 |
+
|
| 522 |
+
This module creates rotary embeddings for multi-dimensional position indices
|
| 523 |
+
(e.g., time, height, width, sequence). Each dimension gets its own embedding
|
| 524 |
+
component with configurable dimensions.
|
| 525 |
+
"""
|
| 526 |
+
|
| 527 |
+
def __init__(self, dim: int, theta: int, axes_dim: list[int]):
|
| 528 |
+
"""
|
| 529 |
+
Args:
|
| 530 |
+
dim: Total position embedding dimension. Should equal `sum(axes_dim)`.
|
| 531 |
+
theta: Base frequency for RoPE. Higher values give slower rotation.
|
| 532 |
+
axes_dim: Dimension allocation for each position axis.
|
| 533 |
+
"""
|
| 534 |
+
super().__init__()
|
| 535 |
+
self.dim = dim
|
| 536 |
+
self.theta = theta
|
| 537 |
+
self.axes_dim = axes_dim
|
| 538 |
+
|
| 539 |
+
def forward(self, ids: Tensor) -> Tensor:
|
| 540 |
+
"""
|
| 541 |
+
Args:
|
| 542 |
+
ids: Position indices of shape `(..., num_axes)` where `num_axes`
|
| 543 |
+
matches `len(axes_dim)`.
|
| 544 |
+
|
| 545 |
+
Returns:
|
| 546 |
+
Rotary embeddings of shape `(..., 1, sum(axes_dim), 2, 2)` suitable
|
| 547 |
+
for applying rotation to query and key tensors.
|
| 548 |
+
"""
|
| 549 |
+
emb = torch.cat(
|
| 550 |
+
[
|
| 551 |
+
rope(ids[..., i], self.axes_dim[i], self.theta)
|
| 552 |
+
for i in range(len(self.axes_dim))
|
| 553 |
+
],
|
| 554 |
+
dim=-3,
|
| 555 |
+
)
|
| 556 |
+
|
| 557 |
+
return emb.unsqueeze(1)
|
| 558 |
+
|
| 559 |
+
|
| 560 |
+
def timestep_embedding(t: Tensor, dim, max_period=10000, time_factor: float = 1000.0):
|
| 561 |
+
"""
|
| 562 |
+
Create sinusoidal timestep embeddings.
|
| 563 |
+
:param t: a 1-D Tensor of N indices, one per batch element.
|
| 564 |
+
These may be fractional.
|
| 565 |
+
:param dim: the dimension of the output.
|
| 566 |
+
:param max_period: controls the minimum frequency of the embeddings.
|
| 567 |
+
:return: an (N, D) Tensor of positional embeddings.
|
| 568 |
+
"""
|
| 569 |
+
t = time_factor * t
|
| 570 |
+
half = dim // 2
|
| 571 |
+
freqs = torch.exp(
|
| 572 |
+
-math.log(max_period)
|
| 573 |
+
* torch.arange(start=0, end=half, device=t.device, dtype=torch.float32)
|
| 574 |
+
/ half
|
| 575 |
+
)
|
| 576 |
+
|
| 577 |
+
args = t[:, None].float() * freqs[None]
|
| 578 |
+
embedding = torch.cat([torch.cos(args), torch.sin(args)], dim=-1)
|
| 579 |
+
if dim % 2:
|
| 580 |
+
embedding = torch.cat([embedding, torch.zeros_like(embedding[:, :1])], dim=-1)
|
| 581 |
+
if torch.is_floating_point(t):
|
| 582 |
+
embedding = embedding.to(t)
|
| 583 |
+
return embedding
|
| 584 |
+
|
| 585 |
+
|
| 586 |
+
class RMSNorm(torch.nn.Module):
|
| 587 |
+
"""
|
| 588 |
+
Root Mean Square Layer Normalization.
|
| 589 |
+
|
| 590 |
+
RMSNorm normalizes using only the variance (RMS) without centering by mean,
|
| 591 |
+
providing a simpler and often equally effective alternative to LayerNorm.
|
| 592 |
+
"""
|
| 593 |
+
|
| 594 |
+
def __init__(self, dim: int):
|
| 595 |
+
"""
|
| 596 |
+
Args:
|
| 597 |
+
dim: Dimension to normalize over (last dimension of input).
|
| 598 |
+
"""
|
| 599 |
+
super().__init__()
|
| 600 |
+
self.scale = nn.Parameter(torch.ones(dim))
|
| 601 |
+
|
| 602 |
+
def forward(self, x: Tensor):
|
| 603 |
+
"""
|
| 604 |
+
Args:
|
| 605 |
+
x: Input tensor of shape `(..., dim)`.
|
| 606 |
+
|
| 607 |
+
Returns:
|
| 608 |
+
Normalized tensor of same shape as input.
|
| 609 |
+
"""
|
| 610 |
+
x_dtype = x.dtype
|
| 611 |
+
x = x.float()
|
| 612 |
+
rrms = torch.rsqrt(torch.mean(x**2, dim=-1, keepdim=True) + 1e-6)
|
| 613 |
+
return (x * rrms).to(dtype=x_dtype) * self.scale
|
| 614 |
+
|
| 615 |
+
|
| 616 |
+
class QKNorm(torch.nn.Module):
|
| 617 |
+
"""
|
| 618 |
+
Separate RMSNorm for query and key tensors in attention.
|
| 619 |
+
|
| 620 |
+
Normalizing queries and keys independently before attention computation improves
|
| 621 |
+
training stability and can lead to better performance.
|
| 622 |
+
"""
|
| 623 |
+
|
| 624 |
+
def __init__(self, dim: int):
|
| 625 |
+
"""
|
| 626 |
+
Args:
|
| 627 |
+
dim: Head dimension for queries and keys.
|
| 628 |
+
"""
|
| 629 |
+
super().__init__()
|
| 630 |
+
self.query_norm = RMSNorm(dim)
|
| 631 |
+
self.key_norm = RMSNorm(dim)
|
| 632 |
+
|
| 633 |
+
def forward(self, q: Tensor, k: Tensor, v: Tensor) -> tuple[Tensor, Tensor]:
|
| 634 |
+
"""
|
| 635 |
+
Args:
|
| 636 |
+
q: Query tensor of shape `(..., head_dim)`.
|
| 637 |
+
k: Key tensor of shape `(..., head_dim)`.
|
| 638 |
+
v: Value tensor (used only for dtype matching).
|
| 639 |
+
|
| 640 |
+
Returns:
|
| 641 |
+
Tuple of normalized `(query, key)` tensors with dtype matching `v`.
|
| 642 |
+
"""
|
| 643 |
+
q = self.query_norm(q)
|
| 644 |
+
k = self.key_norm(k)
|
| 645 |
+
return q.to(v), k.to(v)
|
| 646 |
+
|
| 647 |
+
|
| 648 |
+
def attention(q: Tensor, k: Tensor, v: Tensor, pe: Tensor) -> Tensor:
|
| 649 |
+
q, k = apply_rope(q, k, pe)
|
| 650 |
+
|
| 651 |
+
x = torch.nn.functional.scaled_dot_product_attention(q, k, v)
|
| 652 |
+
x = einops.rearrange(x, "B H L D -> B L (H D)")
|
| 653 |
+
|
| 654 |
+
return x
|
| 655 |
+
|
| 656 |
+
|
| 657 |
+
def rope(pos: Tensor, dim: int, theta: int) -> Tensor:
|
| 658 |
+
assert dim % 2 == 0
|
| 659 |
+
scale = torch.arange(0, dim, 2, dtype=pos.dtype, device=pos.device) / dim
|
| 660 |
+
omega = 1.0 / (theta**scale)
|
| 661 |
+
out = torch.einsum("...n,d->...nd", pos, omega)
|
| 662 |
+
out = torch.stack(
|
| 663 |
+
[torch.cos(out), -torch.sin(out), torch.sin(out), torch.cos(out)], dim=-1
|
| 664 |
+
)
|
| 665 |
+
out = einops.rearrange(out, "b n d (i j) -> b n d i j", i=2, j=2)
|
| 666 |
+
return out.float()
|
| 667 |
+
|
| 668 |
+
|
| 669 |
+
def apply_rope(xq: Tensor, xk: Tensor, freqs_cis: Tensor) -> tuple[Tensor, Tensor]:
|
| 670 |
+
xq_ = xq.float().reshape(*xq.shape[:-1], -1, 1, 2)
|
| 671 |
+
xk_ = xk.float().reshape(*xk.shape[:-1], -1, 1, 2)
|
| 672 |
+
xq_out = freqs_cis[..., 0] * xq_[..., 0] + freqs_cis[..., 1] * xq_[..., 1]
|
| 673 |
+
xk_out = freqs_cis[..., 0] * xk_[..., 0] + freqs_cis[..., 1] * xk_[..., 1]
|
| 674 |
+
return xq_out.reshape(*xq.shape).type_as(xq), xk_out.reshape(*xk.shape).type_as(xk)
|
flux_rgbd/depth/__init__.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 2 |
+
# Copyright (c) 2026 World Labs.
|
| 3 |
+
"""Depth-side preprocessing and schedule helpers for flux_rgbd."""
|
| 4 |
+
|
| 5 |
+
from flux_rgbd.depth.preprocess import DepthConfig, decode_depth
|
| 6 |
+
from flux_rgbd.depth.schedule import ScheduleConfig, rollout_timesteps
|
| 7 |
+
|
| 8 |
+
__all__ = ["DepthConfig", "decode_depth", "ScheduleConfig", "rollout_timesteps"]
|
flux_rgbd/depth/preprocess.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 2 |
+
# Copyright (c) 2026 World Labs.
|
| 3 |
+
"""Depth-side decoding: model tokens → depth pixel map.
|
| 4 |
+
|
| 5 |
+
Decoding steps:
|
| 6 |
+
1. Unpatchify the (tok_h, tok_w, patch_h * patch_w) token grid.
|
| 7 |
+
2. Divide out the post-normalization scalar `depth_value_scale`.
|
| 8 |
+
3. Apply the inverse of every recorded normalization stage, in reverse.
|
| 9 |
+
|
| 10 |
+
Only `"contract"` (the mip-NeRF 360 radial squash, arXiv 2111.12077
|
| 11 |
+
Eq. 10) is genuinely invertible — the other stages discard per-sample
|
| 12 |
+
statistics during encoding and pass through unchanged on decode.
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
from __future__ import annotations
|
| 16 |
+
|
| 17 |
+
import dataclasses
|
| 18 |
+
|
| 19 |
+
import einops
|
| 20 |
+
import torch
|
| 21 |
+
from torch import Tensor
|
| 22 |
+
|
| 23 |
+
_KNOWN_STAGES = frozenset({"contract", "normal", "mean", "unit_mean", "none"})
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
@dataclasses.dataclass(frozen=True)
|
| 27 |
+
class DepthConfig:
|
| 28 |
+
"""How depth was normalized at training time. Decoder reads these."""
|
| 29 |
+
|
| 30 |
+
depth_normalize_mode: tuple[str, ...] | str = ("unit_mean", "contract")
|
| 31 |
+
patch_size: int = 16
|
| 32 |
+
depth_value_scale: float = 1.0
|
| 33 |
+
|
| 34 |
+
def __post_init__(self) -> None:
|
| 35 |
+
stages = self.depth_normalize_mode
|
| 36 |
+
if isinstance(stages, str):
|
| 37 |
+
object.__setattr__(self, "depth_normalize_mode", (stages,))
|
| 38 |
+
unknown = set(self.depth_normalize_mode) - _KNOWN_STAGES
|
| 39 |
+
if unknown:
|
| 40 |
+
raise ValueError(f"unknown stages {sorted(unknown)}; expected {sorted(_KNOWN_STAGES)}")
|
| 41 |
+
if self.depth_value_scale <= 0:
|
| 42 |
+
raise ValueError(f"depth_value_scale must be > 0; got {self.depth_value_scale}")
|
| 43 |
+
if self.patch_size <= 0:
|
| 44 |
+
raise ValueError(f"patch_size must be > 0; got {self.patch_size}")
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def _inv_contract(z: Tensor) -> Tensor:
|
| 48 |
+
"""Inverse of the mip-NeRF 360 radial contract bijection (operates on last dim).
|
| 49 |
+
|
| 50 |
+
Contract maps R^n into the ball of radius 2; this undoes it. For |z| <= 1
|
| 51 |
+
the contract was identity, so this is also identity in that region.
|
| 52 |
+
"""
|
| 53 |
+
mag_sq = torch.clamp(z.square().sum(dim=-1, keepdim=True), min=1.0)
|
| 54 |
+
return z / (2.0 * mag_sq.sqrt() - mag_sq)
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def _undo_stage(depth: Tensor, stage: str) -> Tensor:
|
| 58 |
+
if stage != "contract": # "normal"/"mean"/"unit_mean"/"none" are pass-through.
|
| 59 |
+
return depth
|
| 60 |
+
if depth.shape[-1] == 1:
|
| 61 |
+
return _inv_contract(depth)
|
| 62 |
+
return _inv_contract(depth.unsqueeze(-1)).squeeze(-1)
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def decode_depth(tokens: Tensor, config: DepthConfig) -> Tensor:
|
| 66 |
+
"""Token grid -> depth pixel map.
|
| 67 |
+
|
| 68 |
+
Input shape: (..., tok_h, tok_w, patch_h * patch_w)
|
| 69 |
+
Output shape: (..., tok_h * patch_size, tok_w * patch_size, 1)
|
| 70 |
+
"""
|
| 71 |
+
p = config.patch_size
|
| 72 |
+
depth = einops.rearrange(tokens, "... h w (p1 p2 c) -> ... (h p1) (w p2) c", p1=p, p2=p)
|
| 73 |
+
if config.depth_value_scale != 1.0:
|
| 74 |
+
depth = depth / config.depth_value_scale
|
| 75 |
+
for stage in reversed(config.depth_normalize_mode):
|
| 76 |
+
depth = _undo_stage(depth, stage)
|
| 77 |
+
return depth
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def _contract(z: Tensor) -> Tensor:
|
| 81 |
+
"""Forward mip-NeRF 360 radial contract (inverse of `_inv_contract`).
|
| 82 |
+
|
| 83 |
+
Identity for |z| <= 1; otherwise squashes radius r into (2 - 1/r).
|
| 84 |
+
"""
|
| 85 |
+
mag = z.norm(dim=-1, keepdim=True)
|
| 86 |
+
safe = mag.clamp(min=1e-12)
|
| 87 |
+
scale = torch.where(mag <= 1.0, torch.ones_like(mag), (2.0 - 1.0 / safe) / safe)
|
| 88 |
+
return z * scale
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def _apply_stage(depth: Tensor, stage: str) -> Tensor:
|
| 92 |
+
"""Forward of `_undo_stage`. Only `contract` and `unit_mean` are active.
|
| 93 |
+
|
| 94 |
+
`unit_mean` divides by the per-map mean so the depth is scale-normalised
|
| 95 |
+
the way the model was trained; it is pass-through on decode (the absolute
|
| 96 |
+
scale is not recoverable), so encode/decode round-trips up to that scale.
|
| 97 |
+
"""
|
| 98 |
+
if stage == "unit_mean":
|
| 99 |
+
return depth / depth.mean().clamp(min=1e-12)
|
| 100 |
+
if stage != "contract": # "normal"/"mean"/"none" are pass-through.
|
| 101 |
+
return depth
|
| 102 |
+
if depth.shape[-1] == 1:
|
| 103 |
+
return _contract(depth)
|
| 104 |
+
return _contract(depth.unsqueeze(-1)).squeeze(-1)
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def encode_depth(depth_map: Tensor, config: DepthConfig) -> Tensor:
|
| 108 |
+
"""Depth pixel map -> token grid (inverse of `decode_depth`).
|
| 109 |
+
|
| 110 |
+
Input shape: (..., H, W, 1)
|
| 111 |
+
Output shape: (..., H/patch_size, W/patch_size, patch_size**2)
|
| 112 |
+
|
| 113 |
+
Used for ``mode="d2i"`` to turn a depth map into the depth-stream tokens
|
| 114 |
+
the model conditions on. Because `unit_mean` is scale-normalising, only the
|
| 115 |
+
relative depth structure matters — the input need not be metric.
|
| 116 |
+
"""
|
| 117 |
+
p = config.patch_size
|
| 118 |
+
depth = depth_map
|
| 119 |
+
for stage in config.depth_normalize_mode:
|
| 120 |
+
depth = _apply_stage(depth, stage)
|
| 121 |
+
if config.depth_value_scale != 1.0:
|
| 122 |
+
depth = depth * config.depth_value_scale
|
| 123 |
+
return einops.rearrange(depth, "... (h p1) (w p2) c -> ... h w (p1 p2 c)", p1=p, p2=p)
|