bowmanchow commited on
Commit
0328207
·
1 Parent(s): 2b7da74
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitignore +10 -0
  2. LICENSE +201 -0
  3. README copy.md +95 -0
  4. app.py +1464 -0
  5. b_spline.py +160 -0
  6. configs/default_config.yaml +31 -0
  7. configs/self_forcing_dmd.yaml +56 -0
  8. configs/self_forcing_dmd_vsink.yaml +58 -0
  9. configs/self_forcing_dmd_vsink_stream.yaml +5 -0
  10. configs/self_forcing_dmd_vsink_stream_drag.yaml +33 -0
  11. configs/self_forcing_sid.yaml +57 -0
  12. demo.py +715 -0
  13. demo_utils/constant.py +39 -0
  14. demo_utils/memory.py +174 -0
  15. demo_utils/taehv.py +477 -0
  16. demo_utils/utils.py +809 -0
  17. demo_utils/vae.py +477 -0
  18. demo_utils/vae_block3.py +364 -0
  19. demo_utils/vae_torch2trt.py +335 -0
  20. frequency_utils.py +1020 -0
  21. images/.gitkeep +0 -0
  22. inference.py +263 -0
  23. model/__init__.py +15 -0
  24. model/base.py +256 -0
  25. model/causvid.py +417 -0
  26. model/diffusion.py +131 -0
  27. model/dmd.py +377 -0
  28. model/gan.py +352 -0
  29. model/ode_regression.py +162 -0
  30. model/sid.py +318 -0
  31. offline_run.py +97 -0
  32. optimize_utils.py +275 -0
  33. palette.py +774 -0
  34. pipeline/__init__.py +15 -0
  35. pipeline/bidirectional_diffusion_inference.py +131 -0
  36. pipeline/bidirectional_inference.py +85 -0
  37. pipeline/causal_diffusion_inference.py +402 -0
  38. pipeline/causal_inference.py +1193 -0
  39. pipeline/self_forcing_training.py +351 -0
  40. prompts/MovieGenVideoBench.txt +0 -0
  41. prompts/MovieGenVideoBench_extended.txt +0 -0
  42. prompts/vbench/all_dimension.txt +946 -0
  43. prompts/vbench/all_dimension_extended.txt +0 -0
  44. requirements.txt +43 -0
  45. scripts/create_lmdb_14b_shards.py +103 -0
  46. scripts/create_lmdb_iterative.py +58 -0
  47. scripts/generate_ode_pairs.py +113 -0
  48. setup.py +7 -0
  49. stream_drag_inference_wrapper.py +201 -0
  50. stream_inference.py +111 -0
.gitignore ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__
2
+ *.egg-info
3
+
4
+ output*
5
+
6
+ *logs/
7
+
8
+ .history
9
+
10
+ saved_labels*/
LICENSE ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
README copy.md ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <p align="center">
2
+ <h1 align="center">DragStream</h1>
3
+ <h3 align="center">Streaming Drag-Oriented Interactive Video Manipulation: Drag Anything, Anytime!</h3>
4
+ </p>
5
+ <p align="center">
6
+ <p align="center">
7
+ <a>Junbao Zhou</a><sup>1</sup>
8
+ ·
9
+ <a>Yuan Zhou</a><sup>1</sup>
10
+ ·
11
+ <a>Kesen Zhao</a><sup>1</sup>
12
+ ·
13
+ <a>Qingshan Xu</a><sup>1</sup>
14
+ ·
15
+ <a>Beier Zhu</a><sup>1</sup>
16
+ ·
17
+ <a>Richang Hong</a><sup>2</sup>
18
+ ·
19
+ <a>Hanwang Zhang</a><sup>1</sup><br>
20
+ <sup>1</sup>Nanyang Technological University <sup>2</sup>Hefei University of Technology
21
+ </p>
22
+ <h3 align="center">
23
+ <a href="https://arxiv.org/abs/2510.03550"><img src="https://img.shields.io/badge/Arxiv-2510.03550-B31B1B.svg?logo=arXiv"></a>
24
+ <a href="https://junbao-zhou.github.io/DragStream.github.io/"><img src="https://img.shields.io/badge/Project_Page-Lots_of_Demos-Green"></a>
25
+ <br>
26
+ </h3>
27
+ </p>
28
+
29
+ ---
30
+
31
+ Achieving streaming, fine-grained control over the outputs of autoregressive video diffusion models remains challenging, making it difficult to ensure that they consistently align with user expectations. To bridge this gap, we propose **stReaming drag-oriEnted interactiVe vidEo manipuLation (REVEL)**, a new task that enables users to modify generated videos *anytime* on *anything* via fine-grained, interactive drag. Beyond DragVideo and SG-I2V, REVEL unifies drag-style video manipulation as editing and animating video frames with both supporting user-specified translation, deformation, and rotation effects, making drag operations versatile. In resolving REVEL, we observe: *i*) drag-induced perturbations accumulate in latent space, causing severe latent distribution drift that halts the drag process; *ii*) streaming drag is easily disturbed by context frames, thereby yielding visually unnatural outcomes. We thus propose a training-free approach, **DragStream**, comprising: *i*) an adaptive distribution self-rectification strategy that leverages neighboring frames' statistics to effectively constrain the drift of latent embeddings; *ii*) a spatial-frequency selective optimization mechanism, allowing the model to fully exploit contextual information while mitigating its interference via selectively propagating visual cues along generation. Our method can be seamlessly integrated into existing autoregressive video diffusion models, and extensive experiments firmly demonstrate the effectiveness of our DragStream
32
+
33
+ ---
34
+
35
+ ![alt text](asset/figure1.png)
36
+
37
+ ## Requirements
38
+ We tested this repo on the following setup:
39
+ * Nvidia GPU with at least 40 GB memory.
40
+ * Linux operating system.
41
+ * 64 GB RAM.
42
+
43
+ Other hardware setup could also work but hasn't been tested.
44
+
45
+ ## Installation
46
+
47
+ ### 1. Follow Self-Forcing to Install Dependencies
48
+
49
+ Create a conda environment and install dependencies:
50
+ ```
51
+ conda create -n drag-stream python=3.10 -y
52
+ conda activate drag-stream
53
+ pip install -r requirements.txt
54
+ pip install flash-attn --no-build-isolation
55
+ python setup.py develop
56
+ ```
57
+
58
+ ### 2. Follow Self-Forcing to Download Checkpoints
59
+ ```
60
+ huggingface-cli download Wan-AI/Wan2.1-T2V-1.3B --local-dir wan_models/Wan2.1-T2V-1.3B
61
+ huggingface-cli download gdhe17/Self-Forcing checkpoints --local-dir ./checkpoints
62
+ ```
63
+
64
+ ### 3. Follow Segment-Anything to Install SAM
65
+
66
+ ```
67
+ git clone git@github.com:facebookresearch/segment-anything.git
68
+ cd segment-anything; pip install -e .
69
+ ```
70
+
71
+ ### 4. Follow Segment-Anything to Download SAM Checkpoint
72
+
73
+ - [ViT-H SAM model.](https://dl.fbaipublicfiles.com/segment_anything/sam_vit_h_4b8939.pth)
74
+
75
+ ## Drag/Animate Video with GUI
76
+
77
+ ```
78
+ python app.py
79
+ ```
80
+
81
+ ## CLI Inference with Saved Trajectories
82
+
83
+ ```
84
+ python offline_run.py
85
+ ```
86
+
87
+ ## Reproducibility
88
+
89
+ To ensure every Drag/Animation is performed on the same generated video given the same input conditions, we set the random seed before the initialization of random noise and before the generation process.
90
+
91
+ Please refer to the `set_seed(seed)` in `inference.py`, `stream_inference.py`, `click_gui_video.py`, and `offline_run.py` for details.
92
+
93
+
94
+ ## Acknowledgements
95
+ This codebase is built on top of the open-source implementation of [Self-Forcing](https://self-forcing.github.io/).
app.py ADDED
@@ -0,0 +1,1464 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ os.environ["CUDA_VISIBLE_DEVICES"] = "1"
4
+ import copy
5
+ import torch
6
+ from torchvision.io import write_video
7
+ from torch.utils.data import Dataset
8
+ import numpy as np
9
+ from pathlib import Path
10
+ from hydra import initialize, compose
11
+ from hydra.core.global_hydra import GlobalHydra
12
+
13
+ from b_spline import build_clamped_bspline, equidistant_points_on_spline
14
+
15
+ torch.set_grad_enabled(False)
16
+
17
+ from palette import _palette
18
+
19
+ import gradio as gr
20
+ import numpy as np
21
+ from scipy import ndimage
22
+ from PIL import Image
23
+ import os
24
+ from pathlib import Path
25
+ import cv2
26
+
27
+ # from sam_segment import predict_masks_with_sam
28
+ from segment_anything import SamPredictor, sam_model_registry
29
+
30
+ from tensor_utils import (
31
+ image_to_pil,
32
+ image_to_np,
33
+ bbox_from_mask,
34
+ draw_bbox_on_image,
35
+ draw_mask_on_image,
36
+ draw_points_on_image,
37
+ draw_lines_on_image,
38
+ trajectory_interpolate,
39
+ dilate_mask,
40
+ dilate_masks,
41
+ )
42
+
43
+ from optimize_utils import (
44
+ MultiTrajectory,
45
+ Trajectory,
46
+ )
47
+
48
+ import sys
49
+
50
+ from utils.misc import set_seed
51
+
52
+ from stream_inference_wrapper import StreamInferenceWrapper
53
+ from stream_drag_inference_wrapper import StreamDragInferenceWrapper
54
+ from utils.dataset import TextDataset
55
+
56
+ from video_operations import generate_video, optimize_video
57
+
58
+ # from compute_objmc import visualize_ground_truth_from_trajectory_file
59
+
60
+
61
+ def extract_layer_as_mask(image_editor, layer_index=0):
62
+ if len(image_editor["layers"]) > layer_index:
63
+ layer = image_editor["layers"][layer_index]
64
+ return image_to_np(layer.convert("L")) > 0
65
+ return None
66
+
67
+
68
+ def apply_mask_to_image(
69
+ mask: np.ndarray | None,
70
+ image: np.ndarray | Image.Image,
71
+ mask_color: list[int],
72
+ alpha: float,
73
+ ) -> None | Image.Image:
74
+ if image is None:
75
+ return None
76
+ if mask is None:
77
+ return image_to_pil(image)
78
+ mask = np.array(mask)
79
+ new_image = draw_mask_on_image(
80
+ image,
81
+ mask,
82
+ mask_color=mask_color,
83
+ alpha=alpha,
84
+ )
85
+ return new_image
86
+
87
+
88
+ def apply_movable_mask_to_image(
89
+ mask: np.ndarray | None,
90
+ image: np.ndarray | Image.Image,
91
+ ):
92
+ return apply_mask_to_image(
93
+ mask=mask,
94
+ image=image,
95
+ mask_color=(255, 255, 255),
96
+ alpha=0.35,
97
+ )
98
+
99
+
100
+ def apply_target_mask_to_image(
101
+ mask: np.ndarray | None,
102
+ image: np.ndarray | Image.Image,
103
+ ):
104
+ return apply_mask_to_image(
105
+ mask=mask,
106
+ image=image,
107
+ mask_color=(255, 64, 64),
108
+ alpha=0.5,
109
+ )
110
+
111
+
112
+ def get_video_last_frame(
113
+ # video: Optional[torch.Tensor], # None or (t, h, w, c)
114
+ video_path: str,
115
+ ):
116
+ """
117
+ Loads the last frame from a video.
118
+
119
+ Returns:
120
+ Image: The last frame as a PIL Image.
121
+ """
122
+ print(f"Getting last frame from video: {video_path = }")
123
+ if video_path is None:
124
+ return None
125
+
126
+ cap = cv2.VideoCapture(video_path)
127
+ if not cap.isOpened():
128
+ print(f"Failed to open video: {video_path}")
129
+ return None
130
+ try:
131
+ frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
132
+ if frame_count <= 0:
133
+ print(f"Video has non-positive frame count: {frame_count}")
134
+ cap.release()
135
+ return None
136
+
137
+ # Try direct seek to last frame
138
+ target_index = frame_count - 1
139
+ cap.set(cv2.CAP_PROP_POS_FRAMES, target_index)
140
+ ret, frame = cap.read()
141
+
142
+ # Fallback: iterate to last frame if random access failed
143
+ if (not ret) or frame is None:
144
+ print("Direct seek failed, iterating through frames...")
145
+ cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
146
+ last_valid = None
147
+ while True:
148
+ ret_i, frame_i = cap.read()
149
+ if not ret_i:
150
+ break
151
+ last_valid = frame_i
152
+ frame = last_valid
153
+
154
+ if frame is None:
155
+ print("Could not retrieve last frame.")
156
+ return None
157
+
158
+ frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
159
+ last_frame_image = Image.fromarray(frame)
160
+ return last_frame_image
161
+ except Exception as e:
162
+ print(f"Error extracting last frame: {e}")
163
+ return None
164
+ finally:
165
+ cap.release()
166
+
167
+
168
+ def sam_predict_segmentation(
169
+ sam_predictor: SamPredictor,
170
+ origin_image: Image.Image | np.ndarray,
171
+ restriction_mask: np.ndarray, # (h, w), bool
172
+ click_points: list[tuple[int, int]],
173
+ previous_sam_logits: np.ndarray | None, # (3, 256, 256)
174
+ ):
175
+ # print(f"{restriction_mask.shape = }")
176
+
177
+ origin_image_np = image_to_np(origin_image)
178
+ # print(f"{origin_image_np.shape = }")
179
+ sam_predictor.set_image(origin_image_np)
180
+
181
+ if previous_sam_logits is not None:
182
+ print(f"{previous_sam_logits.shape = }")
183
+ else:
184
+ print(f"{previous_sam_logits = }")
185
+ masks, scores, logits = sam_predictor.predict(
186
+ point_coords=np.array(click_points),
187
+ point_labels=np.ones((len(click_points),)),
188
+ mask_input=(previous_sam_logits[0:1] if previous_sam_logits is not None else None),
189
+ multimask_output=True,
190
+ )
191
+ # mask: np.ndarray
192
+ # scores: np.ndarray
193
+ # logits: np.ndarray
194
+ # print(f"{masks.shape = }") # (3, 480, 832)
195
+ # print(f"{logits.shape = }") # (3, 256, 256)
196
+
197
+ mask = masks[0]
198
+ mask *= restriction_mask
199
+
200
+ logits *= cv2.resize(
201
+ restriction_mask.astype(np.uint8),
202
+ dsize=(256, 256),
203
+ interpolation=cv2.INTER_LINEAR,
204
+ )
205
+
206
+ return mask, logits
207
+
208
+
209
+ def sam_predict_segmentation_wrapper(
210
+ sam_predictor: SamPredictor,
211
+ original_image: Image.Image | np.ndarray,
212
+ restriction_mask: np.ndarray | None,
213
+ previous_click_points: list[tuple[int, int]],
214
+ previous_sam_logits: np.ndarray | None,
215
+ bypass_sam_model: bool,
216
+ evt: gr.SelectData,
217
+ ):
218
+ # print(f"{restriction_mask = }")
219
+ original_image = image_to_pil(original_image).convert("RGB")
220
+
221
+ if restriction_mask is None:
222
+ labeled_restriction_mask = np.zeros(
223
+ (original_image.height, original_image.width), dtype=np.int32
224
+ )
225
+ else:
226
+ labeled_restriction_mask, _ = ndimage.label(restriction_mask, structure=np.ones((3, 3)))
227
+ # print(f"{labeled_restriction_mask = }")
228
+ current_click_label = labeled_restriction_mask[evt.index[1], evt.index[0]]
229
+ # print(f"{current_click_label = }")
230
+
231
+ if current_click_label == 0:
232
+ selected_component_mask = np.zeros_like(labeled_restriction_mask, dtype=bool)
233
+ else:
234
+ selected_component_mask = labeled_restriction_mask == current_click_label
235
+ # print(f"{selected_component_mask = }")
236
+
237
+ if bypass_sam_model:
238
+ click_points = [evt.index]
239
+ mask = selected_component_mask
240
+ logits = None
241
+ else:
242
+ click_points = previous_click_points + [evt.index]
243
+ mask, logits = sam_predict_segmentation(
244
+ sam_predictor=sam_predictor,
245
+ origin_image=original_image,
246
+ restriction_mask=selected_component_mask,
247
+ click_points=click_points,
248
+ previous_sam_logits=previous_sam_logits,
249
+ )
250
+
251
+ return mask, click_points, logits
252
+
253
+
254
+ def draw_all_sam_masks(image: Image.Image | None, mask_list: list[np.ndarray]):
255
+ if image is None:
256
+ return None
257
+ if len(mask_list) == 0:
258
+ pass
259
+ else:
260
+ for mask_idx, mask in enumerate(mask_list):
261
+ image = draw_mask_on_image(
262
+ image,
263
+ mask,
264
+ mask_color=tuple(_palette[mask_idx + 1]),
265
+ alpha=0.65,
266
+ )
267
+ return image
268
+
269
+
270
+ def draw_sam_mask_wrapper(
271
+ original_image,
272
+ movable_mask,
273
+ current_mask: np.ndarray | None,
274
+ previous_masks: list[np.ndarray],
275
+ click_points: list[tuple[int, int]],
276
+ ):
277
+ image = apply_movable_mask_to_image(
278
+ image=original_image,
279
+ mask=movable_mask,
280
+ )
281
+ if image is None:
282
+ return None
283
+ image = draw_all_sam_masks(
284
+ image,
285
+ previous_masks + ([current_mask] if current_mask is not None else []),
286
+ )
287
+ image = draw_points_on_image(
288
+ image,
289
+ click_points,
290
+ color=[(0, 255, 0, 255) for l in click_points],
291
+ radius=5,
292
+ )
293
+ return image
294
+
295
+
296
+ def save_sam_masks(
297
+ current_mask: np.ndarray | None,
298
+ previous_masks: list[np.ndarray],
299
+ ):
300
+ new_masks = previous_masks + ([current_mask] if current_mask is not None else [])
301
+ return None, new_masks, [], None
302
+
303
+
304
+ def select_target_sam_mask(
305
+ masks_list: list[np.ndarray],
306
+ evt: gr.SelectData,
307
+ ):
308
+ is_match_mask = False
309
+ for mask_index, sam_mask in enumerate(masks_list):
310
+ # check if evt point in sam_mask
311
+ if sam_mask[evt.index[1], evt.index[0]]:
312
+ is_match_mask = True
313
+ break
314
+
315
+ if not is_match_mask:
316
+ print(f"Mask not found for {evt.index = }")
317
+ mask_index = -1
318
+ return mask_index
319
+
320
+
321
+ def draw_rotation_trajectory(
322
+ image,
323
+ points,
324
+ ):
325
+ image = draw_points_on_image(
326
+ image,
327
+ [points[0]],
328
+ color="green",
329
+ radius=15,
330
+ )
331
+ if len(points) > 1:
332
+ image = draw_points_on_image(
333
+ image,
334
+ points[1:],
335
+ color=[
336
+ (
337
+ 255 - int(float(i) / len(points[1:]) * 255.0),
338
+ 64,
339
+ int(float(i) / len(points[1:]) * 255.0),
340
+ 255,
341
+ )
342
+ for i in range(len(points[1:]))
343
+ ],
344
+ radius=5,
345
+ )
346
+ for point in points[1:]:
347
+ image = draw_lines_on_image(
348
+ image,
349
+ [points[0], point],
350
+ color="green",
351
+ width=3,
352
+ )
353
+
354
+ return image
355
+
356
+
357
+ def draw_translation_trajectory(
358
+ image,
359
+ points,
360
+ control_points: list[tuple[int, int]] = [],
361
+ is_draw_control_points: bool = True,
362
+ ):
363
+ if len(points) == 1:
364
+ image = draw_points_on_image(
365
+ image,
366
+ points,
367
+ color=[(255, 64, 0, 255)],
368
+ radius=6,
369
+ )
370
+ return image
371
+ if is_draw_control_points and (len(control_points) >= 2):
372
+ image = draw_points_on_image(
373
+ image,
374
+ control_points,
375
+ color=[(0, 255, 0, 255) for _ in control_points],
376
+ radius=3,
377
+ )
378
+ image = draw_lines_on_image(
379
+ image,
380
+ control_points,
381
+ color=[(0, 255, 0, 255) for _ in control_points],
382
+ width=2,
383
+ )
384
+ image = draw_lines_on_image(
385
+ image,
386
+ points,
387
+ color=[
388
+ (
389
+ 255 - int(float(i) / len(points[1:]) * 255.0),
390
+ 64,
391
+ int(float(i) / len(points[1:]) * 255.0),
392
+ 255,
393
+ )
394
+ for i in range(len(points))
395
+ ],
396
+ width=4,
397
+ )
398
+ image = draw_points_on_image(
399
+ image,
400
+ points,
401
+ color=[
402
+ (
403
+ 255 - int(float(i) / len(points[1:]) * 255.0),
404
+ 64,
405
+ int(float(i) / len(points[1:]) * 255.0),
406
+ 255,
407
+ )
408
+ for i in range(len(points))
409
+ ],
410
+ radius=6,
411
+ )
412
+
413
+ return image
414
+
415
+
416
+ def draw_all_trajectories(
417
+ image,
418
+ trajectory: MultiTrajectory,
419
+ is_draw_control_points: bool = True,
420
+ ):
421
+ print(
422
+ f"""
423
+ draw_all_trajectories:
424
+ """
425
+ )
426
+ if trajectory.trajectories is None:
427
+ return image
428
+ for traj in trajectory.trajectories:
429
+ if traj.original_trajectory is None:
430
+ continue
431
+ original_traj = traj.original_trajectory
432
+ if original_traj["is_rotation"]:
433
+ image = draw_rotation_trajectory(image, original_traj["points"])
434
+ else:
435
+ image = draw_translation_trajectory(
436
+ image,
437
+ original_traj["points"],
438
+ original_traj.get("control_points", []),
439
+ is_draw_control_points=is_draw_control_points,
440
+ )
441
+
442
+ return image
443
+
444
+
445
+ def draw_trajectory_image(
446
+ original_image,
447
+ movable_mask,
448
+ mask_index,
449
+ masks_list: list[np.ndarray],
450
+ trajectory: MultiTrajectory,
451
+ is_draw_bbox: bool = True,
452
+ is_draw_control_points: bool = True,
453
+ ):
454
+ print(
455
+ f"""
456
+ draw_trajectory_image:
457
+ {mask_index = }
458
+ """
459
+ )
460
+ image = apply_movable_mask_to_image(
461
+ mask=movable_mask,
462
+ image=original_image,
463
+ )
464
+ image = draw_all_sam_masks(image, masks_list)
465
+ if (
466
+ (mask_index is not None)
467
+ and (mask_index >= 0)
468
+ and (mask_index < len(masks_list))
469
+ and is_draw_bbox
470
+ ):
471
+ image = draw_bbox_on_image(image, bbox_from_mask(masks_list[mask_index]))
472
+ image = draw_all_trajectories(
473
+ image,
474
+ trajectory,
475
+ is_draw_control_points=is_draw_control_points,
476
+ )
477
+ return image
478
+
479
+
480
+ def update_trajectory(
481
+ trajectory: MultiTrajectory,
482
+ mask_index: int,
483
+ drag_animation_select: str,
484
+ translate_rotate_select: str,
485
+ evt: gr.SelectData,
486
+ ):
487
+ print(f"update_trajectory")
488
+
489
+ # Work on a deep copy so Gradio sees a new object
490
+ trajectory = copy.deepcopy(trajectory)
491
+
492
+ if mask_index < 0:
493
+ print(f"Invalid mask_index: {mask_index}")
494
+ return trajectory
495
+
496
+ # print(f"{evt.index = }")
497
+ x_center, y_center = evt.index # evt.value is (x, y)
498
+
499
+ clicked_point = (x_center, y_center)
500
+ print(f"{clicked_point = }")
501
+
502
+ # Ensure trajectories list is large enough
503
+ while len(trajectory.trajectories) <= mask_index:
504
+ trajectory.trajectories.append(Trajectory())
505
+
506
+ existing_traj_obj = trajectory.trajectories[mask_index]
507
+ if existing_traj_obj.original_trajectory is not None:
508
+ current_trajectory = dict(existing_traj_obj.original_trajectory)
509
+ else:
510
+ current_trajectory = {}
511
+
512
+ if translate_rotate_select == "Translation":
513
+ current_trajectory["is_rotation"] = False
514
+
515
+ # Append clicked control point
516
+ control_points = current_trajectory.get("control_points", [])
517
+ control_points = control_points + [clicked_point]
518
+
519
+ # Drag vs Animation behavior
520
+ if drag_animation_select == "Drag":
521
+ # Restrict to last two control points, sample exactly 2 points
522
+ if len(control_points) > 2:
523
+ control_points = [clicked_point]
524
+ num_traj_points = 2
525
+ elif drag_animation_select == "Animation":
526
+ # No restriction on control points, sample N = 1 + 3 * block_number
527
+ num_traj_points = 1 + 3 * int(trajectory.block_number)
528
+ else:
529
+ raise ValueError(f"Invalid drag_animation_select: {drag_animation_select}")
530
+
531
+ current_trajectory["control_points"] = control_points
532
+
533
+ # Compute trajectory points along BSpline (or pad if not enough controls)
534
+ if len(control_points) < 2:
535
+ sampled_pts = [control_points[0]] * num_traj_points
536
+ else:
537
+ spline = build_clamped_bspline(control_points, degree=3)
538
+ pts = equidistant_points_on_spline(spline, num_points=num_traj_points, grid=8000)
539
+ sampled_pts = [(int(round(px)), int(round(py))) for px, py in pts]
540
+
541
+ current_trajectory["points"] = sampled_pts
542
+
543
+ elif translate_rotate_select == "Rotation":
544
+ current_trajectory["is_rotation"] = True
545
+
546
+ # Initialize if missing, else apply 3-point logic
547
+ if "points" not in current_trajectory or current_trajectory["points"] is None:
548
+ current_trajectory["points"] = [clicked_point]
549
+ else:
550
+ pts = current_trajectory["points"] + [clicked_point]
551
+
552
+ # If about to exceed 3, reset to the new point
553
+ if len(pts) > 3:
554
+ current_trajectory["points"] = [clicked_point]
555
+ # If less than 3, just append
556
+ elif len(pts) < 3:
557
+ current_trajectory["points"] = pts
558
+ else:
559
+ # len(pts) == 3: pts[0] is rotation center
560
+ if drag_animation_select == "Animation":
561
+ first = trajectory_interpolate(pts[1:], scale=int(trajectory.block_number))
562
+ second = trajectory_interpolate(first, scale=3)
563
+ current_trajectory["points"] = pts[0:1] + second
564
+ else:
565
+ # Drag: do not interpolate
566
+ current_trajectory["points"] = pts
567
+ else:
568
+ raise ValueError("Invalid translation/rotation selection")
569
+
570
+ # Update the Trajectory object in-place (recomputes block_trajectories)
571
+ existing_traj_obj.set_original_trajectory(current_trajectory)
572
+ # print(f"{trajectory = }")
573
+
574
+ return trajectory
575
+
576
+
577
+ def save_trajectory(
578
+ save_dir: Path,
579
+ saved_trajectory: MultiTrajectory,
580
+ original_image: Image.Image,
581
+ current_block_index: int,
582
+ masks: list[np.ndarray],
583
+ ):
584
+ print(f"save_trajectory")
585
+ print(f"{save_dir = }")
586
+ print(f"{saved_trajectory = }")
587
+ save_dir = Path(save_dir)
588
+ save_dir.mkdir(parents=True, exist_ok=True)
589
+
590
+ drag_animation_select = saved_trajectory.drag_or_animation_select or "Drag"
591
+ save_prefix = f"block_{current_block_index}_{drag_animation_select}"
592
+
593
+ # Use MultiTrajectory's save method
594
+ saved_trajectory.save(
595
+ save_dir=save_dir,
596
+ prefix=save_prefix,
597
+ )
598
+
599
+ # Save the trajectory image
600
+ trajectory_image = draw_trajectory_image(
601
+ original_image=original_image,
602
+ movable_mask=saved_trajectory.movable_mask,
603
+ mask_index=None,
604
+ masks_list=masks,
605
+ trajectory=saved_trajectory,
606
+ is_draw_bbox=False,
607
+ is_draw_control_points=False,
608
+ )
609
+ trajectory_image.save(save_dir / f"{save_prefix}_trajectory.png")
610
+
611
+
612
+ def clear_current_trajectory(
613
+ idx: int,
614
+ trajectory: MultiTrajectory,
615
+ ):
616
+ trajectory = copy.deepcopy(trajectory)
617
+ """Clear the trajectory at the given mask index."""
618
+ try:
619
+ idx_int = int(idx)
620
+ except Exception:
621
+ return trajectory
622
+
623
+ if not trajectory.trajectories:
624
+ return trajectory
625
+
626
+ if idx_int < 0 or idx_int >= len(trajectory.trajectories):
627
+ return trajectory
628
+
629
+ # Reset this trajectory (keep the mask)
630
+ mask = trajectory.trajectories[idx_int].mask
631
+ trajectory.trajectories[idx_int] = Trajectory(mask=mask)
632
+
633
+ return trajectory
634
+
635
+
636
+ def clear_all_trajectories(
637
+ trajectory: MultiTrajectory,
638
+ ):
639
+ trajectory = copy.deepcopy(trajectory)
640
+ """Clear all trajectories but keep the masks."""
641
+ if trajectory.trajectories is not None:
642
+ for i in range(len(trajectory.trajectories)):
643
+ mask = trajectory.trajectories[i].mask
644
+ trajectory.trajectories[i] = Trajectory(mask=mask)
645
+ return trajectory
646
+
647
+
648
+ def sync_trajectory_masks(saved_trajectory: MultiTrajectory, dilated_masks: list[np.ndarray]):
649
+ """Resize saved_trajectory.trajectories to match the number of dilated masks,
650
+ and update each Trajectory.mask with the corresponding dilated mask."""
651
+ saved_trajectory = copy.deepcopy(saved_trajectory)
652
+
653
+ current_len = len(saved_trajectory.trajectories)
654
+ target_len = len(dilated_masks) if dilated_masks else 0
655
+
656
+ if target_len > current_len:
657
+ # Expand: append new empty Trajectory objects
658
+ for _ in range(target_len - current_len):
659
+ saved_trajectory.trajectories.append(Trajectory())
660
+ elif target_len < current_len:
661
+ # Shrink: truncate
662
+ saved_trajectory.trajectories = saved_trajectory.trajectories[:target_len]
663
+
664
+ # Update each Trajectory.mask
665
+ for i, mask in enumerate(dilated_masks):
666
+ saved_trajectory.trajectories[i].mask = mask
667
+
668
+ return saved_trajectory
669
+
670
+
671
+ def add_listeners_to_trajectory(
672
+ saved_trajectory: MultiTrajectory,
673
+ prompt_box: gr.Textbox,
674
+ trajectory_block_number_slider: gr.Slider,
675
+ drag_animation_select: gr.Dropdown,
676
+ movable_area_mask: gr.State,
677
+ dilated_saved_sam_predicted_masks: gr.State,
678
+ ):
679
+ # Sync prompt into saved_trajectory when prompt_box changes
680
+
681
+ def sync_trajectory_prompt(saved_trajectory: MultiTrajectory, prompt: str):
682
+ saved_trajectory.prompt = prompt
683
+ return saved_trajectory
684
+
685
+ prompt_box.change(
686
+ fn=sync_trajectory_prompt,
687
+ inputs=[saved_trajectory, prompt_box],
688
+ outputs=saved_trajectory,
689
+ trigger_mode="always_last",
690
+ )
691
+
692
+ # Sync block_number into saved_trajectory when trajectory_block_number_slider changes
693
+ def sync_trajectory_block_number(saved_trajectory: MultiTrajectory, block_number: int):
694
+ saved_trajectory.block_number = block_number
695
+ return saved_trajectory
696
+
697
+ trajectory_block_number_slider.change(
698
+ fn=sync_trajectory_block_number,
699
+ inputs=[saved_trajectory, trajectory_block_number_slider],
700
+ outputs=saved_trajectory,
701
+ trigger_mode="always_last",
702
+ )
703
+
704
+ # Sync drag_or_animation_select into saved_trajectory when drag_animation_select changes
705
+ def sync_trajectory_drag_animation(
706
+ saved_trajectory: MultiTrajectory, drag_animation_select: str
707
+ ):
708
+ saved_trajectory.drag_or_animation_select = drag_animation_select
709
+ return saved_trajectory
710
+
711
+ drag_animation_select.change(
712
+ fn=sync_trajectory_drag_animation,
713
+ inputs=[saved_trajectory, drag_animation_select],
714
+ outputs=saved_trajectory,
715
+ trigger_mode="always_last",
716
+ )
717
+
718
+ # Sync movable_area_mask into saved_trajectory when it changes
719
+ def sync_trajectory_movable_mask(saved_trajectory: MultiTrajectory, movable_mask):
720
+ saved_trajectory.movable_mask = movable_mask
721
+ return saved_trajectory
722
+
723
+ movable_area_mask.change(
724
+ fn=sync_trajectory_movable_mask,
725
+ inputs=[saved_trajectory, movable_area_mask],
726
+ outputs=saved_trajectory,
727
+ trigger_mode="always_last",
728
+ )
729
+
730
+ # Sync dilated_saved_sam_predicted_masks into saved_trajectory when it changes
731
+ dilated_saved_sam_predicted_masks.change(
732
+ fn=sync_trajectory_masks,
733
+ inputs=[saved_trajectory, dilated_saved_sam_predicted_masks],
734
+ outputs=saved_trajectory,
735
+ trigger_mode="always_last",
736
+ )
737
+
738
+
739
+ def create_generate_video_ui(
740
+ label_root: str | Path,
741
+ text_dataset: Dataset,
742
+ video_path: gr.State,
743
+ stream_drag_inference: StreamDragInferenceWrapper,
744
+ output_dir: str | Path,
745
+ original_image: gr.State,
746
+ ):
747
+ with gr.Row():
748
+ prompt_index_number = gr.Number(
749
+ label="Step 1: Select Prompt Index Here",
750
+ interactive=True,
751
+ scale=1,
752
+ )
753
+ prompt_box = gr.Textbox(
754
+ label="Prompt",
755
+ interactive=True,
756
+ scale=3,
757
+ )
758
+ save_dir_text_box = gr.Textbox(
759
+ label="Save Directory",
760
+ interactive=False,
761
+ scale=1,
762
+ )
763
+ prompt_index_number.change(
764
+ fn=lambda prompt_index_number: text_dataset[prompt_index_number]["prompts"],
765
+ inputs=prompt_index_number,
766
+ outputs=[
767
+ prompt_box,
768
+ ],
769
+ )
770
+ gr.on(
771
+ triggers=[
772
+ prompt_box.change,
773
+ ],
774
+ fn=lambda prompt_index_number, prompt: str(
775
+ label_root / f"{prompt_index_number:04d}-{prompt[:50].replace(' ', '_')}"
776
+ ),
777
+ inputs=[prompt_index_number, prompt_box],
778
+ outputs=save_dir_text_box,
779
+ trigger_mode="always_last",
780
+ )
781
+ with gr.Row():
782
+ current_block_index_slider = gr.Slider(
783
+ label="Current Start Block Index",
784
+ minimum=0,
785
+ maximum=50,
786
+ value=0,
787
+ step=1,
788
+ )
789
+ generate_block_number_slider = gr.Slider(
790
+ label="Step 2: Select Number of Blocks to Generate",
791
+ minimum=1,
792
+ maximum=50,
793
+ value=2,
794
+ step=1,
795
+ )
796
+ with gr.Row():
797
+ begin_generate_button = gr.Button(
798
+ value="Step 3: Click Here to Begin Generation",
799
+ )
800
+ refresh_video_display_button = gr.Button(value="Refresh Video Display")
801
+
802
+ with gr.Row():
803
+ video_display = gr.Video()
804
+
805
+ begin_generate_button.click(
806
+ fn=lambda pi, p, sbi, bn: generate_video(
807
+ stream_inference_model=stream_drag_inference,
808
+ prompt_index=pi,
809
+ prompt=p,
810
+ start_block_index=sbi,
811
+ block_number=bn,
812
+ output_dir=output_dir,
813
+ ),
814
+ inputs=[
815
+ prompt_index_number,
816
+ prompt_box,
817
+ current_block_index_slider,
818
+ generate_block_number_slider,
819
+ ],
820
+ outputs=[video_path, current_block_index_slider],
821
+ )
822
+ gr.on(
823
+ triggers=[
824
+ refresh_video_display_button.click,
825
+ video_path.change,
826
+ ],
827
+ fn=lambda video_path: video_path,
828
+ inputs=video_path,
829
+ outputs=video_display,
830
+ trigger_mode="always_last",
831
+ )
832
+
833
+ with gr.Row():
834
+ get_last_frame_button = gr.Button(
835
+ value="Get Last Frame (Normally No Need to Click This, In Case the Last Frame Fails to Update due to Gradio Bug)",
836
+ )
837
+ gr.on(
838
+ triggers=[
839
+ video_path.change,
840
+ get_last_frame_button.click,
841
+ ],
842
+ fn=get_video_last_frame,
843
+ inputs=video_path,
844
+ outputs=original_image,
845
+ )
846
+
847
+ return (
848
+ prompt_index_number,
849
+ save_dir_text_box,
850
+ prompt_box,
851
+ current_block_index_slider,
852
+ generate_block_number_slider,
853
+ )
854
+
855
+
856
+ def create_movable_area_ui(
857
+ movable_area_mask: gr.State,
858
+ original_image: gr.State,
859
+ ):
860
+
861
+ with gr.Row():
862
+ movable_area_image_editor = gr.ImageEditor(
863
+ label="Step 4: This is Last Frame of Video, Draw Editable Area Here. (Normally This Should Be Large and Cover all Possible Area Where the Object You Want to Move/Animate to)",
864
+ type="pil",
865
+ interactive=True,
866
+ brush=gr.Brush(
867
+ default_size=100,
868
+ colors=[
869
+ "rgba(0, 0, 255, 0.5)",
870
+ ],
871
+ default_color="auto",
872
+ color_mode="defaults",
873
+ ),
874
+ )
875
+ movable_area_image_editor.change(
876
+ fn=extract_layer_as_mask,
877
+ inputs=movable_area_image_editor,
878
+ outputs=movable_area_mask,
879
+ trigger_mode="always_last",
880
+ )
881
+ original_image.change(
882
+ fn=lambda image: image,
883
+ inputs=original_image,
884
+ outputs=movable_area_image_editor,
885
+ trigger_mode="always_last",
886
+ )
887
+ with gr.Row():
888
+ refresh_movable_area_button = gr.Button(
889
+ value="Refresh Movable Area (Normally No Need to Click This, In Case the Mask Fails to Update due to Gradio Bug)"
890
+ )
891
+ refresh_movable_area_button.click(
892
+ fn=extract_layer_as_mask,
893
+ inputs=movable_area_image_editor,
894
+ outputs=movable_area_mask,
895
+ trigger_mode="always_last",
896
+ )
897
+
898
+
899
+ def create_target_area_ui(
900
+ target_area_mask: gr.State,
901
+ original_image: gr.State,
902
+ movable_area_mask: gr.State,
903
+ ):
904
+ with gr.Row():
905
+ target_area_image_editor = gr.ImageEditor(
906
+ label="Step 5: Draw Target Area on the Object You Want to Move/Animate (Normally This Should Be a Subset of Editable Area) (Normally This Mask should be Bigger than the Desired Object)",
907
+ type="pil",
908
+ interactive=True,
909
+ brush=gr.Brush(
910
+ default_size=50,
911
+ colors=[
912
+ "rgba(255, 0, 0, 0.5)",
913
+ ],
914
+ default_color="auto",
915
+ color_mode="defaults",
916
+ ),
917
+ )
918
+ target_area_image_editor.change(
919
+ fn=extract_layer_as_mask,
920
+ inputs=target_area_image_editor,
921
+ outputs=target_area_mask,
922
+ trigger_mode="always_last",
923
+ )
924
+ gr.on(
925
+ triggers=[
926
+ original_image.change,
927
+ movable_area_mask.change,
928
+ ],
929
+ fn=apply_movable_mask_to_image,
930
+ inputs=[
931
+ movable_area_mask,
932
+ original_image,
933
+ ],
934
+ outputs=target_area_image_editor,
935
+ trigger_mode="always_last",
936
+ )
937
+
938
+ with gr.Row():
939
+ refresh_target_area_button = gr.Button(
940
+ value="Refresh Target Area (Normally No Need to Click This, In Case the Mask Fails to Update due to Gradio Bug)"
941
+ )
942
+ refresh_target_area_button.click(
943
+ fn=extract_layer_as_mask,
944
+ inputs=target_area_image_editor,
945
+ outputs=target_area_mask,
946
+ trigger_mode="always_last",
947
+ )
948
+
949
+
950
+ def create_sam_segmentation_ui(
951
+ original_image: gr.State,
952
+ movable_area_mask: gr.State,
953
+ target_area_mask: gr.State,
954
+ sam_predictor: SamPredictor,
955
+ sam_click_points: gr.State,
956
+ sam_saved_logits: gr.State,
957
+ current_sam_predicted_mask: gr.State,
958
+ saved_sam_predicted_masks: gr.State,
959
+ dilated_current_sam_predicted_mask: gr.State,
960
+ dilated_saved_sam_predicted_masks: gr.State,
961
+ ):
962
+ with gr.Row():
963
+ refresh_sam_segment_click_image_button = gr.Button(
964
+ value="Refresh Target Area Mask Display (Normally No Need to Click This, In Case the Mask Fails to Update due to Gradio Bug)"
965
+ )
966
+ with gr.Row():
967
+ sam_segment_click_image = gr.Image(
968
+ label="Step 6: Click to Perform SAM Segment on Target Area, Segment the Object You Want to Move/Animate. The SAM Mask is Restricted within the Target Area Mask",
969
+ type="pil",
970
+ interactive=True,
971
+ )
972
+ gr.on(
973
+ triggers=[
974
+ original_image.change,
975
+ movable_area_mask.change,
976
+ target_area_mask.change,
977
+ refresh_sam_segment_click_image_button.click,
978
+ ],
979
+ fn=lambda movable_mask, target_mask, image: apply_target_mask_to_image(
980
+ target_mask,
981
+ apply_movable_mask_to_image(
982
+ movable_mask,
983
+ image,
984
+ ),
985
+ ),
986
+ inputs=[
987
+ movable_area_mask,
988
+ target_area_mask,
989
+ original_image,
990
+ ],
991
+ outputs=sam_segment_click_image,
992
+ trigger_mode="always_last",
993
+ )
994
+
995
+ with gr.Row():
996
+ dilate_mask_slider = gr.Slider(
997
+ label="Dilate Mask Pixel",
998
+ minimum=0,
999
+ maximum=50,
1000
+ value=15,
1001
+ step=1,
1002
+ )
1003
+ bypass_sam_model_check_box = gr.Checkbox(
1004
+ label="Bypass SAM Model",
1005
+ value=False,
1006
+ )
1007
+
1008
+ def sam_predict_segmentation_wrapper_wrapper(
1009
+ oi,
1010
+ rm,
1011
+ pcp,
1012
+ psl,
1013
+ bs,
1014
+ evt: gr.SelectData,
1015
+ ):
1016
+ return sam_predict_segmentation_wrapper(
1017
+ sam_predictor=sam_predictor,
1018
+ original_image=oi,
1019
+ restriction_mask=rm,
1020
+ previous_click_points=pcp,
1021
+ previous_sam_logits=psl,
1022
+ bypass_sam_model=bs,
1023
+ evt=evt,
1024
+ )
1025
+
1026
+ sam_segment_click_image.select(
1027
+ fn=sam_predict_segmentation_wrapper_wrapper,
1028
+ inputs=[
1029
+ original_image,
1030
+ target_area_mask,
1031
+ sam_click_points,
1032
+ sam_saved_logits,
1033
+ bypass_sam_model_check_box,
1034
+ ],
1035
+ outputs=[
1036
+ current_sam_predicted_mask,
1037
+ sam_click_points,
1038
+ sam_saved_logits,
1039
+ ],
1040
+ trigger_mode="always_last",
1041
+ )
1042
+ gr.on(
1043
+ triggers=[
1044
+ current_sam_predicted_mask.change,
1045
+ dilate_mask_slider.change,
1046
+ ],
1047
+ fn=dilate_mask,
1048
+ inputs=[
1049
+ current_sam_predicted_mask,
1050
+ dilate_mask_slider,
1051
+ ],
1052
+ outputs=dilated_current_sam_predicted_mask,
1053
+ trigger_mode="always_last",
1054
+ )
1055
+ gr.on(
1056
+ triggers=[
1057
+ saved_sam_predicted_masks.change,
1058
+ dilate_mask_slider.change,
1059
+ ],
1060
+ fn=dilate_masks,
1061
+ inputs=[
1062
+ saved_sam_predicted_masks,
1063
+ dilate_mask_slider,
1064
+ ],
1065
+ outputs=dilated_saved_sam_predicted_masks,
1066
+ trigger_mode="always_last",
1067
+ )
1068
+
1069
+
1070
+ def create_sam_mask_management_ui(
1071
+ original_image: gr.State,
1072
+ movable_area_mask: gr.State,
1073
+ dilated_current_sam_predicted_mask: gr.State,
1074
+ dilated_saved_sam_predicted_masks: gr.State,
1075
+ sam_click_points: gr.State,
1076
+ current_sam_predicted_mask: gr.State,
1077
+ saved_sam_predicted_masks: gr.State,
1078
+ sam_saved_logits: gr.State,
1079
+ ):
1080
+ with gr.Row():
1081
+ save_sam_masks_button = gr.Button(
1082
+ value="Step 7: Save the Current SAM Mask",
1083
+ )
1084
+ cancel_sam_mask_button = gr.Button(value="Cancel Current SAM Mask")
1085
+ delete_sam_mask_button = gr.Button(value="Delete All SAM Masks")
1086
+ save_sam_masks_button.click(
1087
+ fn=save_sam_masks,
1088
+ inputs=[
1089
+ current_sam_predicted_mask,
1090
+ saved_sam_predicted_masks,
1091
+ ],
1092
+ outputs=[
1093
+ current_sam_predicted_mask,
1094
+ saved_sam_predicted_masks,
1095
+ sam_click_points,
1096
+ sam_saved_logits,
1097
+ ],
1098
+ trigger_mode="always_last",
1099
+ )
1100
+ with gr.Row():
1101
+ sam_segment_display_image = gr.Image(
1102
+ label="Step 8: Display the SAM Segmentation, Click to Select Target Object to Create Trajectory",
1103
+ type="pil",
1104
+ interactive=True,
1105
+ )
1106
+ gr.on(
1107
+ triggers=[
1108
+ original_image.change,
1109
+ movable_area_mask.change,
1110
+ dilated_current_sam_predicted_mask.change,
1111
+ dilated_saved_sam_predicted_masks.change,
1112
+ sam_click_points.change,
1113
+ ],
1114
+ fn=draw_sam_mask_wrapper,
1115
+ inputs=[
1116
+ original_image,
1117
+ movable_area_mask,
1118
+ dilated_current_sam_predicted_mask,
1119
+ dilated_saved_sam_predicted_masks,
1120
+ sam_click_points,
1121
+ ],
1122
+ outputs=sam_segment_display_image,
1123
+ trigger_mode="always_last",
1124
+ )
1125
+ cancel_sam_mask_button.click(
1126
+ fn=lambda: (None, [], None),
1127
+ outputs=[
1128
+ current_sam_predicted_mask,
1129
+ sam_click_points,
1130
+ sam_saved_logits,
1131
+ ],
1132
+ trigger_mode="always_last",
1133
+ )
1134
+ gr.on(
1135
+ triggers=[
1136
+ # target_area_mask.change,
1137
+ delete_sam_mask_button.click,
1138
+ ],
1139
+ fn=lambda: (None, [], [], None),
1140
+ outputs=[
1141
+ current_sam_predicted_mask,
1142
+ saved_sam_predicted_masks,
1143
+ sam_click_points,
1144
+ sam_saved_logits,
1145
+ ],
1146
+ trigger_mode="always_last",
1147
+ )
1148
+ with gr.Row():
1149
+ current_selected_mask_index_number = gr.Number(
1150
+ label="Current Selected Mask Index",
1151
+ interactive=False,
1152
+ )
1153
+
1154
+ sam_segment_display_image.select(
1155
+ fn=select_target_sam_mask,
1156
+ inputs=[
1157
+ saved_sam_predicted_masks,
1158
+ ],
1159
+ outputs=[
1160
+ current_selected_mask_index_number,
1161
+ ],
1162
+ trigger_mode="always_last",
1163
+ )
1164
+
1165
+ return current_selected_mask_index_number
1166
+
1167
+
1168
+ def create_trajectory_display_ui(
1169
+ original_image: gr.State,
1170
+ movable_area_mask: gr.State,
1171
+ dilated_saved_sam_predicted_masks: gr.State,
1172
+ saved_trajectory: gr.State,
1173
+ current_selected_mask_index_number: gr.State,
1174
+ ):
1175
+ with gr.Row():
1176
+ trajectory_block_number_slider = gr.Slider(
1177
+ label="Step 9: Select Number of Trajectory Blocks (For Animation Only, More Blocks Means Longer Animation, For Drag, This Should be 1)",
1178
+ minimum=1,
1179
+ maximum=10,
1180
+ value=1,
1181
+ step=1,
1182
+ )
1183
+ with gr.Row():
1184
+ drag_animation_select = gr.Dropdown(
1185
+ choices=["Drag", "Animation"],
1186
+ label="Step 10: Select Drag or Animation",
1187
+ )
1188
+ translate_rotate_select = gr.Dropdown(
1189
+ choices=["Translation", "Rotation"],
1190
+ label="Step 11: Select Translation or Rotation",
1191
+ )
1192
+
1193
+ with gr.Row():
1194
+ trajectory_display_image = gr.Image(
1195
+ label="Step 12: Click on the Object in the Image to Create Trajectory. The Translation Trajectory is Controlled by Bspline Interpolation. The Rotation Trajectory is Controlled by 3 Points",
1196
+ type="pil",
1197
+ interactive=False,
1198
+ )
1199
+ gr.on(
1200
+ triggers=[
1201
+ original_image.change,
1202
+ movable_area_mask.change,
1203
+ current_selected_mask_index_number.change,
1204
+ dilated_saved_sam_predicted_masks.change,
1205
+ saved_trajectory.change,
1206
+ ],
1207
+ fn=draw_trajectory_image,
1208
+ inputs=[
1209
+ original_image,
1210
+ movable_area_mask,
1211
+ current_selected_mask_index_number,
1212
+ dilated_saved_sam_predicted_masks,
1213
+ saved_trajectory,
1214
+ ],
1215
+ outputs=trajectory_display_image,
1216
+ trigger_mode="always_last",
1217
+ )
1218
+
1219
+ trajectory_display_image.select(
1220
+ fn=update_trajectory,
1221
+ inputs=[
1222
+ saved_trajectory,
1223
+ current_selected_mask_index_number,
1224
+ drag_animation_select,
1225
+ translate_rotate_select,
1226
+ ],
1227
+ outputs=saved_trajectory,
1228
+ )
1229
+
1230
+ return drag_animation_select, trajectory_block_number_slider
1231
+
1232
+
1233
+ def create_trajectory_management_ui(
1234
+ save_dir_text_box: gr.Textbox,
1235
+ original_image: gr.State,
1236
+ current_block_index_slider: gr.Slider,
1237
+ saved_trajectory: gr.State,
1238
+ dilated_saved_sam_predicted_masks: gr.State,
1239
+ current_selected_mask_index_number: gr.Number,
1240
+ ):
1241
+ with gr.Row():
1242
+ save_trajectory_button = gr.Button(
1243
+ value="Step 13: Save Trajectory",
1244
+ )
1245
+ delete_current_trajectory_button = gr.Button(value="Delete Current Trajectory")
1246
+ delete_all_trajectory_button = gr.Button(value="Delete All Trajectories")
1247
+ save_trajectory_button.click(
1248
+ fn=save_trajectory,
1249
+ inputs=[
1250
+ save_dir_text_box,
1251
+ saved_trajectory,
1252
+ original_image,
1253
+ current_block_index_slider,
1254
+ dilated_saved_sam_predicted_masks,
1255
+ ],
1256
+ )
1257
+ delete_current_trajectory_button.click(
1258
+ fn=clear_current_trajectory,
1259
+ inputs=[current_selected_mask_index_number, saved_trajectory],
1260
+ outputs=[saved_trajectory],
1261
+ )
1262
+ delete_all_trajectory_button.click(
1263
+ fn=clear_all_trajectories,
1264
+ inputs=[saved_trajectory],
1265
+ outputs=[saved_trajectory],
1266
+ )
1267
+
1268
+
1269
+ def create_ui(
1270
+ text_dataset: Dataset,
1271
+ label_root: str | Path,
1272
+ output_dir: str | Path,
1273
+ sam_predictor: SamPredictor,
1274
+ stream_drag_inference: StreamDragInferenceWrapper,
1275
+ ):
1276
+ with gr.Blocks() as demo:
1277
+ video_path = gr.State(value=None)
1278
+ original_image = gr.State(value=None)
1279
+ movable_area_mask = gr.State(value=None)
1280
+ target_area_mask = gr.State(value=None)
1281
+
1282
+ sam_click_points = gr.State(value=[])
1283
+ sam_saved_logits = gr.State(value=None)
1284
+ saved_sam_predicted_masks = gr.State(value=[])
1285
+ current_sam_predicted_mask = gr.State(value=None)
1286
+
1287
+ dilated_current_sam_predicted_mask = gr.State(value=None)
1288
+ dilated_saved_sam_predicted_masks = gr.State(value=[])
1289
+
1290
+ saved_trajectory = gr.State(value=MultiTrajectory())
1291
+
1292
+ (
1293
+ prompt_index_number,
1294
+ save_dir_text_box,
1295
+ prompt_box,
1296
+ current_block_index_slider,
1297
+ generate_block_number_slider,
1298
+ ) = create_generate_video_ui(
1299
+ label_root=label_root,
1300
+ text_dataset=text_dataset,
1301
+ video_path=video_path,
1302
+ stream_drag_inference=stream_drag_inference,
1303
+ output_dir=output_dir,
1304
+ original_image=original_image,
1305
+ )
1306
+
1307
+ create_movable_area_ui(movable_area_mask, original_image)
1308
+ create_target_area_ui(target_area_mask, original_image, movable_area_mask)
1309
+ create_sam_segmentation_ui(
1310
+ original_image=original_image,
1311
+ movable_area_mask=movable_area_mask,
1312
+ target_area_mask=target_area_mask,
1313
+ sam_predictor=sam_predictor,
1314
+ sam_click_points=sam_click_points,
1315
+ sam_saved_logits=sam_saved_logits,
1316
+ current_sam_predicted_mask=current_sam_predicted_mask,
1317
+ saved_sam_predicted_masks=saved_sam_predicted_masks,
1318
+ dilated_current_sam_predicted_mask=dilated_current_sam_predicted_mask,
1319
+ dilated_saved_sam_predicted_masks=dilated_saved_sam_predicted_masks,
1320
+ )
1321
+
1322
+ current_selected_mask_index_number = create_sam_mask_management_ui(
1323
+ original_image=original_image,
1324
+ movable_area_mask=movable_area_mask,
1325
+ dilated_current_sam_predicted_mask=dilated_current_sam_predicted_mask,
1326
+ dilated_saved_sam_predicted_masks=dilated_saved_sam_predicted_masks,
1327
+ sam_click_points=sam_click_points,
1328
+ current_sam_predicted_mask=current_sam_predicted_mask,
1329
+ saved_sam_predicted_masks=saved_sam_predicted_masks,
1330
+ sam_saved_logits=sam_saved_logits,
1331
+ )
1332
+
1333
+ drag_animation_select, trajectory_block_number_slider = create_trajectory_display_ui(
1334
+ original_image=original_image,
1335
+ movable_area_mask=movable_area_mask,
1336
+ dilated_saved_sam_predicted_masks=dilated_saved_sam_predicted_masks,
1337
+ saved_trajectory=saved_trajectory,
1338
+ current_selected_mask_index_number=current_selected_mask_index_number,
1339
+ )
1340
+ create_trajectory_management_ui(
1341
+ save_dir_text_box=save_dir_text_box,
1342
+ original_image=original_image,
1343
+ current_block_index_slider=current_block_index_slider,
1344
+ saved_trajectory=saved_trajectory,
1345
+ dilated_saved_sam_predicted_masks=dilated_saved_sam_predicted_masks,
1346
+ current_selected_mask_index_number=current_selected_mask_index_number,
1347
+ )
1348
+
1349
+ add_listeners_to_trajectory(
1350
+ saved_trajectory=saved_trajectory,
1351
+ prompt_box=prompt_box,
1352
+ trajectory_block_number_slider=trajectory_block_number_slider,
1353
+ drag_animation_select=drag_animation_select,
1354
+ movable_area_mask=movable_area_mask,
1355
+ dilated_saved_sam_predicted_masks=dilated_saved_sam_predicted_masks,
1356
+ )
1357
+
1358
+ with gr.Row():
1359
+ begin_optimize_button = gr.Button(
1360
+ value="Step 14: Click Here to Begin Optimize, Wait for a Moment and the Dragged/Animated Video will be Displayed Above",
1361
+ )
1362
+ begin_optimize_button.click(
1363
+ fn=lambda pi, sbi, st: optimize_video(
1364
+ stream_drag_inference_model=stream_drag_inference,
1365
+ output_dir=output_dir,
1366
+ prompt_index=pi,
1367
+ start_block_index=sbi,
1368
+ multi_trajectory=st,
1369
+ ),
1370
+ inputs=[
1371
+ prompt_index_number,
1372
+ current_block_index_slider,
1373
+ saved_trajectory,
1374
+ ],
1375
+ outputs=[
1376
+ video_path,
1377
+ current_block_index_slider,
1378
+ ],
1379
+ )
1380
+ with gr.Row():
1381
+ clear_all_button = gr.Button(
1382
+ value="Step 15: Remember to Click Here to Clear All Before Generation/Editing on Next Video, Otherwise the Previous KV Cache will Affect the Generation/Editing of Next Video",
1383
+ )
1384
+
1385
+ def clear_all():
1386
+ stream_drag_inference.reset()
1387
+
1388
+ return (
1389
+ 0,
1390
+ None,
1391
+ None,
1392
+ None,
1393
+ None,
1394
+ [],
1395
+ None,
1396
+ [],
1397
+ None,
1398
+ MultiTrajectory(),
1399
+ )
1400
+
1401
+ clear_all_button.click(
1402
+ fn=clear_all,
1403
+ outputs=[
1404
+ current_block_index_slider,
1405
+ video_path,
1406
+ original_image,
1407
+ movable_area_mask,
1408
+ target_area_mask,
1409
+ sam_click_points,
1410
+ sam_saved_logits,
1411
+ saved_sam_predicted_masks,
1412
+ current_sam_predicted_mask,
1413
+ saved_trajectory,
1414
+ ],
1415
+ )
1416
+
1417
+ return demo
1418
+
1419
+
1420
+ def main():
1421
+ sam_model = sam_model_registry["vit_h"](checkpoint="../segment-anything/sam_vit_h_4b8939.pth")
1422
+ sam_model.to(device="cuda")
1423
+ sam_predictor = SamPredictor(sam_model)
1424
+
1425
+ SEED = 42
1426
+
1427
+ text_dataset = TextDataset(prompt_path="prompts/MovieGenVideoBench_extended.txt")
1428
+
1429
+ if GlobalHydra.instance().is_initialized():
1430
+ GlobalHydra.instance().clear()
1431
+
1432
+ config_dir = "configs"
1433
+ stream_config_name = "self_forcing_dmd_vsink_stream_drag"
1434
+ with initialize(version_base=None, config_path=config_dir):
1435
+ stream_config = compose(config_name=stream_config_name)
1436
+ print(f"{stream_config = }")
1437
+
1438
+ stream_drag_inference = StreamDragInferenceWrapper(
1439
+ stream_model_config=stream_config,
1440
+ checkpoint_path="./checkpoints/self_forcing_dmd.pt",
1441
+ total_generate_block_number=36,
1442
+ use_ema=True,
1443
+ seed=SEED,
1444
+ )
1445
+ label_save_dir = Path("./saved_labels")
1446
+ label_save_dir = label_save_dir / f"{stream_config_name}-seed{SEED}"
1447
+ label_save_dir.mkdir(parents=True, exist_ok=True)
1448
+
1449
+ output_save_dir = Path("outputs-editing")
1450
+ output_save_dir = output_save_dir / f"{stream_config_name}-seed{SEED}"
1451
+ output_save_dir.mkdir(parents=True, exist_ok=True)
1452
+
1453
+ demo = create_ui(
1454
+ text_dataset=text_dataset,
1455
+ label_root=label_save_dir,
1456
+ output_dir=output_save_dir,
1457
+ sam_predictor=sam_predictor,
1458
+ stream_drag_inference=stream_drag_inference,
1459
+ )
1460
+ demo.launch(server_name="0.0.0.0")
1461
+
1462
+
1463
+ if __name__ == "__main__":
1464
+ main()
b_spline.py ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Click to place control points, then see a clamped B-spline plus
2
+ # equally spaced points along the curve.
3
+ #
4
+ # Controls:
5
+ # - Left-click: add a control point
6
+ # - Right-click: remove the last control point
7
+ # - '+' / '-': increase/decrease the number of equally spaced points
8
+ # - 'c': clear all points
9
+ #
10
+ # Requirements: numpy, matplotlib, scipy
11
+
12
+ import numpy as np
13
+ import matplotlib.pyplot as plt
14
+ from scipy.interpolate import BSpline
15
+ from scipy.integrate import cumulative_trapezoid
16
+
17
+ # ------------- Spline utilities -------------
18
+
19
+
20
+ def _open_uniform_knots(n, k):
21
+ # n = number of control points, k = degree
22
+ m = n + k + 1
23
+ t = np.zeros(m, dtype=float)
24
+ t[-(k + 1) :] = 1.0
25
+ num_interior = n - k - 1
26
+ if num_interior > 0:
27
+ t[k + 1 : n] = np.linspace(1 / (n - k), (n - k - 1) / (n - k), num_interior)
28
+ return t
29
+
30
+
31
+ def build_clamped_bspline(control_points, degree=3):
32
+ """
33
+ Build an open-uniform (clamped) B-spline that passes through the
34
+ first and last control points.
35
+ """
36
+ P = np.asarray(control_points, dtype=float)
37
+ n = len(P)
38
+ if n < 2:
39
+ raise ValueError("Need at least 2 control points")
40
+ k = min(degree, n - 1) # degree cannot exceed n-1
41
+ t = _open_uniform_knots(n, k)
42
+ return BSpline(t, P, k, axis=0)
43
+
44
+
45
+ def equidistant_points_on_spline(spline, num_points, grid=6000):
46
+ """
47
+ Return `num_points` points equally spaced in arc length along `spline`.
48
+ """
49
+ if num_points < 2:
50
+ raise ValueError("num_points must be >= 2")
51
+
52
+ u = np.linspace(0.0, 1.0, grid)
53
+ dCdu = spline.derivative()(u)
54
+ speed = np.linalg.norm(dCdu, axis=1)
55
+
56
+ s = cumulative_trapezoid(speed, u, initial=0.0)
57
+ total_len = s[-1]
58
+
59
+ if total_len <= 1e-12:
60
+ P0 = spline(0.0)
61
+ return np.repeat(P0[None, :], num_points, axis=0)
62
+
63
+ s_targets = np.linspace(0.0, total_len, num_points)
64
+ u_targets = np.interp(s_targets, s, u)
65
+ return spline(u_targets)
66
+
67
+
68
+ # ------------- Interactive demo -------------
69
+
70
+ if __name__ == "__main__":
71
+ # Optional: draw over an image. Uncomment and set path if needed.
72
+ # img = plt.imread("your_image.png")
73
+ # H, W = img.shape[:2]
74
+
75
+ points = []
76
+ sample_count = [25] # use a list so we can modify inside callbacks
77
+ sample_count = [2] # use a list so we can modify inside callbacks
78
+
79
+ fig, ax = plt.subplots()
80
+ ax.set_aspect("equal", adjustable="box")
81
+
82
+ # If drawing over an image, uncomment:
83
+ # ax.imshow(img, extent=[0, W, H, 0], origin='upper')
84
+ # ax.set_xlim(0, W)
85
+ # ax.set_ylim(H, 0)
86
+ # Otherwise, use a unit square canvas:
87
+ ax.set_xlim(0, 1)
88
+ ax.set_ylim(0, 1)
89
+
90
+ title_template = "Left-click: add, Right-click: undo | +/-: change N | c: clear | N = {}"
91
+ ax.set_title(title_template.format(sample_count[0]))
92
+
93
+ # Artists
94
+ scatter_ctrl = ax.scatter([], [], c="r", s=25, zorder=3, label="control points")
95
+ (ctrl_line,) = ax.plot([], [], "r--", lw=1, alpha=0.6, zorder=2, label="control polygon")
96
+ (curve_line,) = ax.plot([], [], "g-", lw=2, zorder=1, label="B-spline")
97
+ eq_scatter = ax.scatter([], [], c="b", s=20, zorder=4, label="equally spaced points")
98
+
99
+ ax.legend(loc="upper right")
100
+
101
+ def update_plot():
102
+ if points:
103
+ P = np.array(points, dtype=float)
104
+ scatter_ctrl.set_offsets(P)
105
+ ctrl_line.set_data(P[:, 0], P[:, 1])
106
+ else:
107
+ scatter_ctrl.set_offsets(np.empty((0, 2)))
108
+ ctrl_line.set_data([], [])
109
+
110
+ if len(points) >= 2:
111
+ try:
112
+ spl = build_clamped_bspline(points, degree=3)
113
+ # For a smooth preview of the curve
114
+ C = spl(np.linspace(0, 1, 1000))
115
+ curve_line.set_data(C[:, 0], C[:, 1])
116
+
117
+ # Equally spaced points along the curve
118
+ N = max(2, sample_count[0])
119
+ eq_pts = equidistant_points_on_spline(spl, num_points=N, grid=8000)
120
+ eq_scatter.set_offsets(eq_pts)
121
+ except Exception as e:
122
+ print("Error building spline:", e)
123
+ curve_line.set_data([], [])
124
+ eq_scatter.set_offsets(np.empty((0, 2)))
125
+ else:
126
+ curve_line.set_data([], [])
127
+ eq_scatter.set_offsets(np.empty((0, 2)))
128
+
129
+ ax.set_title(title_template.format(sample_count[0]))
130
+ fig.canvas.draw_idle()
131
+
132
+ def onclick(event):
133
+ if event.inaxes != ax:
134
+ return
135
+ if event.button == 1: # left: add
136
+ if event.xdata is None or event.ydata is None:
137
+ return
138
+ points.append([event.xdata, event.ydata])
139
+ update_plot()
140
+ elif event.button == 3: # right: undo last
141
+ if points:
142
+ points.pop()
143
+ update_plot()
144
+
145
+ def onkey(event):
146
+ if event.key in ["+", "="]:
147
+ sample_count[0] += 1
148
+ update_plot()
149
+ elif event.key == "-":
150
+ if sample_count[0] > 2:
151
+ sample_count[0] -= 1
152
+ update_plot()
153
+ elif event.key in ["c", "C"]:
154
+ points.clear()
155
+ update_plot()
156
+
157
+ fig.canvas.mpl_connect("button_press_event", onclick)
158
+ fig.canvas.mpl_connect("key_press_event", onkey)
159
+
160
+ plt.show()
configs/default_config.yaml ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ independent_first_frame: false
2
+ warp_denoising_step: false
3
+ weight_decay: 0.01
4
+ same_step_across_blocks: true
5
+ discriminator_lr_multiplier: 1.0
6
+ last_step_only: false
7
+ i2v: false
8
+ num_training_frames: 21
9
+ gc_interval: 100
10
+ context_noise: 0
11
+ causal: true
12
+
13
+ ckpt_step: 0
14
+ prompt_name: MovieGenVideoBench
15
+ prompt_path: prompts/MovieGenVideoBench.txt
16
+ eval_first_n: 64
17
+ num_samples: 1
18
+ height: 480
19
+ width: 832
20
+ num_frames: 81
21
+
22
+ no_save: false
23
+ no_visualize: false
24
+ logdir: logs
25
+ wandb_save_dir: ""
26
+ disable_wandb: false
27
+
28
+ # Recommended: prevent Hydra from chdir into run dir (optional but common)
29
+ hydra:
30
+ job:
31
+ chdir: false
configs/self_forcing_dmd.yaml ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ defaults:
2
+ - default_config
3
+ - _self_
4
+
5
+ generator_ckpt: checkpoints/ode_init.pt
6
+ generator_fsdp_wrap_strategy: size
7
+ real_score_fsdp_wrap_strategy: size
8
+ fake_score_fsdp_wrap_strategy: size
9
+ real_name: Wan2.1-T2V-14B
10
+ # real_name: Wan2.1-T2V-1.3B
11
+ text_encoder_fsdp_wrap_strategy: size
12
+ denoising_step_list:
13
+ - 1000
14
+ - 750
15
+ - 500
16
+ - 250
17
+ warp_denoising_step: true # need to remove - 0 in denoising_step_list if warp_denoising_step is true
18
+ ts_schedule: false
19
+ num_train_timestep: 1000
20
+ timestep_shift: 5.0
21
+ guidance_scale: 3.0
22
+ denoising_loss_type: flow
23
+ mixed_precision: true
24
+ seed: 0
25
+ wandb_host: WANDB_HOST
26
+ wandb_key: WANDB_KEY
27
+ wandb_entity: WANDB_ENTITY
28
+ wandb_project: WANDB_PROJECT
29
+ sharding_strategy: hybrid_full
30
+ lr: 2.0e-06
31
+ lr_critic: 4.0e-07
32
+ beta1: 0.0
33
+ beta2: 0.999
34
+ beta1_critic: 0.0
35
+ beta2_critic: 0.999
36
+ data_path: prompts/vidprom_filtered_extended.txt
37
+ batch_size: 1
38
+ ema_weight: 0.99
39
+ ema_start_step: 200
40
+ total_batch_size: 64
41
+ log_iters: 500
42
+ negative_prompt: '色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走'
43
+ dfake_gen_update_ratio: 5
44
+ image_or_video_shape:
45
+ - 1
46
+ - 21
47
+ - 16
48
+ - 60
49
+ - 104
50
+ distribution_loss: dmd
51
+ trainer: score_distillation
52
+ gradient_checkpointing: true
53
+ num_frame_per_block: 3
54
+ load_raw_video: false
55
+ model_kwargs:
56
+ timestep_shift: 5.0
configs/self_forcing_dmd_vsink.yaml ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ defaults:
2
+ - default_config
3
+ - _self_
4
+
5
+ generator_ckpt: checkpoints/ode_init.pt
6
+ generator_fsdp_wrap_strategy: size
7
+ real_score_fsdp_wrap_strategy: size
8
+ fake_score_fsdp_wrap_strategy: size
9
+ # real_name: Wan2.1-T2V-1.3B
10
+ real_name: Wan2.1-T2V-1.3B
11
+ text_encoder_fsdp_wrap_strategy: size
12
+ denoising_step_list:
13
+ - 1000
14
+ - 750
15
+ - 500
16
+ - 250
17
+ warp_denoising_step: true # need to remove - 0 in denoising_step_list if warp_denoising_step is true
18
+ ts_schedule: false
19
+ num_train_timestep: 1000
20
+ timestep_shift: 5.0
21
+ guidance_scale: 3.0
22
+ denoising_loss_type: flow
23
+ mixed_precision: true
24
+ seed: 0
25
+ wandb_host: WANDB_HOST
26
+ wandb_key: WANDB_KEY
27
+ wandb_entity: WANDB_ENTITY
28
+ wandb_project: WANDB_PROJECT
29
+ sharding_strategy: hybrid_full
30
+ lr: 2.0e-06
31
+ lr_critic: 4.0e-07
32
+ beta1: 0.0
33
+ beta2: 0.999
34
+ beta1_critic: 0.0
35
+ beta2_critic: 0.999
36
+ data_path: prompts/vidprom_filtered_extended.txt
37
+ batch_size: 1
38
+ ema_weight: 0.99
39
+ ema_start_step: 200
40
+ total_batch_size: 64
41
+ log_iters: 400
42
+ negative_prompt: '色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走'
43
+ dfake_gen_update_ratio: 5
44
+ image_or_video_shape:
45
+ - 1
46
+ - 21
47
+ - 16
48
+ - 60
49
+ - 104
50
+ distribution_loss: dmd
51
+ trainer: score_distillation
52
+ gradient_checkpointing: true
53
+ num_frame_per_block: 3
54
+ load_raw_video: false
55
+ model_kwargs:
56
+ timestep_shift: 5.0
57
+ local_attn_size: 21
58
+ sink_size: 3
configs/self_forcing_dmd_vsink_stream.yaml ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ defaults:
2
+ - self_forcing_dmd_vsink
3
+ - _self_
4
+
5
+ vae_offload_cpu: false
configs/self_forcing_dmd_vsink_stream_drag.yaml ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ defaults:
2
+ - self_forcing_dmd_vsink_stream
3
+ - _self_
4
+
5
+ drag_optim_config:
6
+ record_feature_block_indexes:
7
+ - 12
8
+ - 13
9
+ - 14
10
+ - 15
11
+
12
+ optimize_denoising_steps_indexes:
13
+ - 1
14
+
15
+ optimize_iter: 5
16
+ optimize_lr: 0.03
17
+
18
+ translation_step: 16.0
19
+ rotation_step: 8.0
20
+
21
+ normalize_latent_after_drag_optimize: True
22
+ normalize_latent_after_post_merge: True
23
+ dynamic_chunk_normalization_block_number: 2
24
+
25
+ feature_scaling_factor: 2.0
26
+
27
+ gradient_gaussian_padding: 1.5
28
+ gradient_gaussian_sigma: 1.5
29
+
30
+ feature_fft_cutoff:
31
+ - 0.2
32
+ - 0.4
33
+ - 0.6
configs/self_forcing_sid.yaml ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ defaults:
2
+ - default_config
3
+ - _self_
4
+
5
+ generator_ckpt: checkpoints/ode_init.pt
6
+ generator_fsdp_wrap_strategy: size
7
+ real_score_fsdp_wrap_strategy: size
8
+ fake_score_fsdp_wrap_strategy: size
9
+ real_name: Wan2.1-T2V-1.3B
10
+ text_encoder_fsdp_wrap_strategy: size
11
+ denoising_step_list:
12
+ - 1000
13
+ - 750
14
+ - 500
15
+ - 250
16
+ warp_denoising_step: true # need to remove - 0 in denoising_step_list if warp_denoising_step is true
17
+ ts_schedule: false
18
+ num_train_timestep: 1000
19
+ timestep_shift: 5.0
20
+ guidance_scale: 3.0
21
+ denoising_loss_type: flow
22
+ mixed_precision: true
23
+ seed: 0
24
+ wandb_host: WANDB_HOST
25
+ wandb_key: WANDB_KEY
26
+ wandb_entity: WANDB_ENTITY
27
+ wandb_project: WANDB_PROJECT
28
+ sharding_strategy: hybrid_full
29
+ lr: 2.0e-06
30
+ lr_critic: 2.0e-06
31
+ beta1: 0.0
32
+ beta2: 0.999
33
+ beta1_critic: 0.0
34
+ beta2_critic: 0.999
35
+ weight_decay: 0.0
36
+ data_path: prompts/vidprom_filtered_extended.txt
37
+ batch_size: 1
38
+ sid_alpha: 1.0
39
+ ema_weight: 0.99
40
+ ema_start_step: 200
41
+ total_batch_size: 64
42
+ log_iters: 50
43
+ negative_prompt: '色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走'
44
+ dfake_gen_update_ratio: 5
45
+ image_or_video_shape:
46
+ - 1
47
+ - 21
48
+ - 16
49
+ - 60
50
+ - 104
51
+ distribution_loss: dmd
52
+ trainer: score_distillation
53
+ gradient_checkpointing: true
54
+ num_frame_per_block: 3
55
+ load_raw_video: false
56
+ model_kwargs:
57
+ timestep_shift: 5.0
demo.py ADDED
@@ -0,0 +1,715 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Demo for Self-Forcing.
3
+ """
4
+
5
+ import os
6
+ import re
7
+ import random
8
+ import time
9
+ import base64
10
+ import argparse
11
+ import hashlib
12
+ import subprocess
13
+ import urllib.request
14
+ from io import BytesIO
15
+ from PIL import Image
16
+ import numpy as np
17
+ import torch
18
+ from omegaconf import OmegaConf
19
+ from flask import Flask, render_template, jsonify
20
+ from flask_socketio import SocketIO, emit
21
+ import queue
22
+ from threading import Thread, Event
23
+
24
+ from pipeline import CausalInferencePipeline
25
+ from demo_utils.constant import ZERO_VAE_CACHE
26
+ from demo_utils.vae_block3 import VAEDecoderWrapper
27
+ from utils.wan_wrapper import WanDiffusionWrapper, WanTextEncoder
28
+ from demo_utils.utils import generate_timestamp
29
+ from demo_utils.memory import (
30
+ gpu,
31
+ get_cuda_free_memory_gb,
32
+ DynamicSwapInstaller,
33
+ move_model_to_device_with_memory_preservation,
34
+ )
35
+
36
+ # Parse arguments
37
+ parser = argparse.ArgumentParser()
38
+ parser.add_argument("--port", type=int, default=5001)
39
+ parser.add_argument("--host", type=str, default="0.0.0.0")
40
+ parser.add_argument("--checkpoint_path", type=str, default="./checkpoints/self_forcing_dmd.pt")
41
+ parser.add_argument("--config_path", type=str, default="./configs/self_forcing_dmd.yaml")
42
+ parser.add_argument("--trt", action="store_true")
43
+ args = parser.parse_args()
44
+
45
+ print(f"Free VRAM {get_cuda_free_memory_gb(gpu)} GB")
46
+ low_memory = get_cuda_free_memory_gb(gpu) < 40
47
+
48
+ # Load models
49
+ config = OmegaConf.load(args.config_path)
50
+ default_config = OmegaConf.load("configs/default_config.yaml")
51
+ config = OmegaConf.merge(default_config, config)
52
+
53
+ text_encoder = WanTextEncoder()
54
+
55
+ # Global variables for dynamic model switching
56
+ current_vae_decoder = None
57
+ current_use_taehv = False
58
+ fp8_applied = False
59
+ torch_compile_applied = False
60
+ global frame_number
61
+ frame_number = 0
62
+ anim_name = ""
63
+ frame_rate = 6
64
+
65
+
66
+ def initialize_vae_decoder(use_taehv=False, use_trt=False):
67
+ """Initialize VAE decoder based on the selected option"""
68
+ global current_vae_decoder, current_use_taehv
69
+
70
+ if use_trt:
71
+ from demo_utils.vae import VAETRTWrapper
72
+
73
+ current_vae_decoder = VAETRTWrapper()
74
+ return current_vae_decoder
75
+
76
+ if use_taehv:
77
+ from demo_utils.taehv import TAEHV
78
+
79
+ # Check if taew2_1.pth exists in checkpoints folder, download if missing
80
+ taehv_checkpoint_path = "checkpoints/taew2_1.pth"
81
+ if not os.path.exists(taehv_checkpoint_path):
82
+ print(
83
+ f"taew2_1.pth not found in checkpoints folder {taehv_checkpoint_path}. Downloading..."
84
+ )
85
+ os.makedirs("checkpoints", exist_ok=True)
86
+ download_url = "https://github.com/madebyollin/taehv/raw/main/taew2_1.pth"
87
+ try:
88
+ urllib.request.urlretrieve(download_url, taehv_checkpoint_path)
89
+ print(f"Successfully downloaded taew2_1.pth to {taehv_checkpoint_path}")
90
+ except Exception as e:
91
+ print(f"Failed to download taew2_1.pth: {e}")
92
+ raise
93
+
94
+ class DotDict(dict):
95
+ __getattr__ = dict.__getitem__
96
+ __setattr__ = dict.__setitem__
97
+
98
+ class TAEHVDiffusersWrapper(torch.nn.Module):
99
+ def __init__(self):
100
+ super().__init__()
101
+ self.dtype = torch.float16
102
+ self.taehv = TAEHV(checkpoint_path=taehv_checkpoint_path).to(self.dtype)
103
+ self.config = DotDict(scaling_factor=1.0)
104
+
105
+ def decode(self, latents, return_dict=None):
106
+ # n, c, t, h, w = latents.shape
107
+ # low-memory, set parallel=True for faster + higher memory
108
+ return self.taehv.decode_video(latents, parallel=False).mul_(2).sub_(1)
109
+
110
+ current_vae_decoder = TAEHVDiffusersWrapper()
111
+ else:
112
+ current_vae_decoder = VAEDecoderWrapper()
113
+ vae_state_dict = torch.load("wan_models/Wan2.1-T2V-1.3B/Wan2.1_VAE.pth", map_location="cpu")
114
+ decoder_state_dict = {}
115
+ for key, value in vae_state_dict.items():
116
+ if "decoder." in key or "conv2" in key:
117
+ decoder_state_dict[key] = value
118
+ current_vae_decoder.load_state_dict(decoder_state_dict)
119
+
120
+ current_vae_decoder.eval()
121
+ current_vae_decoder.to(dtype=torch.float16)
122
+ current_vae_decoder.requires_grad_(False)
123
+ current_vae_decoder.to(gpu)
124
+ current_use_taehv = use_taehv
125
+
126
+ print(f"✅ VAE decoder initialized with {'TAEHV' if use_taehv else 'default VAE'}")
127
+ return current_vae_decoder
128
+
129
+
130
+ # Initialize with default VAE
131
+ vae_decoder = initialize_vae_decoder(use_taehv=False, use_trt=args.trt)
132
+
133
+ transformer = WanDiffusionWrapper(is_causal=True)
134
+ state_dict = torch.load(args.checkpoint_path, map_location="cpu")
135
+ transformer.load_state_dict(state_dict["generator_ema"])
136
+
137
+ text_encoder.eval()
138
+ transformer.eval()
139
+
140
+ transformer.to(dtype=torch.float16)
141
+ text_encoder.to(dtype=torch.bfloat16)
142
+
143
+ text_encoder.requires_grad_(False)
144
+ transformer.requires_grad_(False)
145
+
146
+ pipeline = CausalInferencePipeline(
147
+ config,
148
+ device=gpu,
149
+ generator=transformer,
150
+ text_encoder=text_encoder,
151
+ vae=vae_decoder,
152
+ )
153
+
154
+ if low_memory:
155
+ DynamicSwapInstaller.install_model(text_encoder, device=gpu)
156
+ else:
157
+ text_encoder.to(gpu)
158
+ transformer.to(gpu)
159
+
160
+ # Flask and SocketIO setup
161
+ app = Flask(__name__)
162
+ app.config["SECRET_KEY"] = "frontend_buffered_demo"
163
+ socketio = SocketIO(app, cors_allowed_origins="*")
164
+
165
+ generation_active = False
166
+ stop_event = Event()
167
+ frame_send_queue = queue.Queue()
168
+ sender_thread = None
169
+ models_compiled = False
170
+
171
+
172
+ def tensor_to_base64_frame(frame_tensor):
173
+ """Convert a single frame tensor to base64 image string."""
174
+ global frame_number, anim_name
175
+ # Clamp and normalize to 0-255
176
+ frame = torch.clamp(frame_tensor.float(), -1.0, 1.0) * 127.5 + 127.5
177
+ frame = frame.to(torch.uint8).cpu().numpy()
178
+
179
+ # CHW -> HWC
180
+ if len(frame.shape) == 3:
181
+ frame = np.transpose(frame, (1, 2, 0))
182
+
183
+ # Convert to PIL Image
184
+ if frame.shape[2] == 3: # RGB
185
+ image = Image.fromarray(frame, "RGB")
186
+ else: # Handle other formats
187
+ image = Image.fromarray(frame)
188
+
189
+ # Convert to base64
190
+ buffer = BytesIO()
191
+ image.save(buffer, format="JPEG", quality=100)
192
+ if not os.path.exists("./images/%s" % anim_name):
193
+ os.makedirs("./images/%s" % anim_name)
194
+ frame_number += 1
195
+ image.save("./images/%s/%s_%03d.jpg" % (anim_name, anim_name, frame_number))
196
+ img_str = base64.b64encode(buffer.getvalue()).decode()
197
+ return f"data:image/jpeg;base64,{img_str}"
198
+
199
+
200
+ def frame_sender_worker():
201
+ """Background thread that processes frame send queue non-blocking."""
202
+ global frame_send_queue, generation_active, stop_event
203
+
204
+ print("📡 Frame sender thread started")
205
+
206
+ while True:
207
+ frame_data = None
208
+ try:
209
+ # Get frame data from queue
210
+ frame_data = frame_send_queue.get(timeout=1.0)
211
+
212
+ if frame_data is None: # Shutdown signal
213
+ frame_send_queue.task_done() # Mark shutdown signal as done
214
+ break
215
+
216
+ frame_tensor, frame_index, block_index, job_id = frame_data
217
+
218
+ # Convert tensor to base64
219
+ base64_frame = tensor_to_base64_frame(frame_tensor)
220
+
221
+ # Send via SocketIO
222
+ try:
223
+ socketio.emit(
224
+ "frame_ready",
225
+ {
226
+ "data": base64_frame,
227
+ "frame_index": frame_index,
228
+ "block_index": block_index,
229
+ "job_id": job_id,
230
+ },
231
+ )
232
+ except Exception as e:
233
+ print(f"⚠️ Failed to send frame {frame_index}: {e}")
234
+
235
+ frame_send_queue.task_done()
236
+
237
+ except queue.Empty:
238
+ # Check if we should continue running
239
+ if not generation_active and frame_send_queue.empty():
240
+ break
241
+ except Exception as e:
242
+ print(f"❌ Frame sender error: {e}")
243
+ # Make sure to mark task as done even if there's an error
244
+ if frame_data is not None:
245
+ try:
246
+ frame_send_queue.task_done()
247
+ except Exception as e:
248
+ print(f"❌ Failed to mark frame task as done: {e}")
249
+ break
250
+
251
+ print("📡 Frame sender thread stopped")
252
+
253
+
254
+ @torch.no_grad()
255
+ def generate_video_stream(
256
+ prompt, seed, enable_torch_compile=False, enable_fp8=False, use_taehv=False
257
+ ):
258
+ """Generate video and push frames immediately to frontend."""
259
+ global generation_active, stop_event, frame_send_queue, sender_thread, models_compiled, torch_compile_applied, fp8_applied, current_vae_decoder, current_use_taehv, frame_rate, anim_name
260
+
261
+ try:
262
+ generation_active = True
263
+ stop_event.clear()
264
+ job_id = generate_timestamp()
265
+
266
+ # Start frame sender thread if not already running
267
+ if sender_thread is None or not sender_thread.is_alive():
268
+ sender_thread = Thread(target=frame_sender_worker, daemon=True)
269
+ sender_thread.start()
270
+
271
+ # Emit progress updates
272
+ def emit_progress(message, progress):
273
+ try:
274
+ socketio.emit(
275
+ "progress",
276
+ {
277
+ "message": message,
278
+ "progress": progress,
279
+ "job_id": job_id,
280
+ },
281
+ )
282
+ except Exception as e:
283
+ print(f"❌ Failed to emit progress: {e}")
284
+
285
+ emit_progress("Starting generation...", 0)
286
+
287
+ # Handle VAE decoder switching
288
+ if use_taehv != current_use_taehv:
289
+ emit_progress("Switching VAE decoder...", 2)
290
+ print(f"🔄 Switching VAE decoder to {'TAEHV' if use_taehv else 'default VAE'}")
291
+ current_vae_decoder = initialize_vae_decoder(use_taehv=use_taehv)
292
+ # Update pipeline with new VAE decoder
293
+ pipeline.vae = current_vae_decoder
294
+
295
+ # Handle FP8 quantization
296
+ if enable_fp8 and not fp8_applied:
297
+ emit_progress("Applying FP8 quantization...", 3)
298
+ print("🔧 Applying FP8 quantization to transformer")
299
+ from torchao.quantization.quant_api import (
300
+ quantize_,
301
+ Float8DynamicActivationFloat8WeightConfig,
302
+ PerTensor,
303
+ )
304
+
305
+ quantize_(
306
+ transformer,
307
+ Float8DynamicActivationFloat8WeightConfig(granularity=PerTensor()),
308
+ )
309
+ fp8_applied = True
310
+
311
+ # Text encoding
312
+ emit_progress("Encoding text prompt...", 8)
313
+ conditional_dict = text_encoder(text_prompts=[prompt])
314
+ for key, value in conditional_dict.items():
315
+ conditional_dict[key] = value.to(dtype=torch.float16)
316
+ if low_memory:
317
+ gpu_memory_preservation = get_cuda_free_memory_gb(gpu) + 5
318
+ move_model_to_device_with_memory_preservation(
319
+ text_encoder,
320
+ target_device=gpu,
321
+ preserved_memory_gb=gpu_memory_preservation,
322
+ )
323
+
324
+ # Handle torch.compile if enabled
325
+ torch_compile_applied = enable_torch_compile
326
+ if enable_torch_compile and not models_compiled:
327
+ # Compile transformer and decoder
328
+ transformer.compile(mode="max-autotune-no-cudagraphs")
329
+ if not current_use_taehv and not low_memory and not args.trt:
330
+ current_vae_decoder.compile(mode="max-autotune-no-cudagraphs")
331
+
332
+ # Initialize generation
333
+ emit_progress("Initializing generation...", 12)
334
+
335
+ rnd = torch.Generator(gpu).manual_seed(seed)
336
+ # all_latents = torch.zeros([1, 21, 16, 60, 104], device=gpu, dtype=torch.bfloat16)
337
+
338
+ pipeline._initialize_kv_cache(batch_size=1, dtype=torch.float16, device=gpu)
339
+ pipeline._initialize_crossattn_cache(batch_size=1, dtype=torch.float16, device=gpu)
340
+
341
+ noise = torch.randn([1, 21, 16, 60, 104], device=gpu, dtype=torch.float16, generator=rnd)
342
+
343
+ # Generation parameters
344
+ num_blocks = 7
345
+ current_start_frame = 0
346
+ num_input_frames = 0
347
+ all_num_frames = [pipeline.num_frame_per_block] * num_blocks
348
+ if current_use_taehv:
349
+ vae_cache = None
350
+ else:
351
+ vae_cache = ZERO_VAE_CACHE
352
+ for i in range(len(vae_cache)):
353
+ vae_cache[i] = vae_cache[i].to(device=gpu, dtype=torch.float16)
354
+
355
+ total_frames_sent = 0
356
+ generation_start_time = time.time()
357
+
358
+ emit_progress("Generating frames... (frontend handles timing)", 15)
359
+
360
+ for idx, current_num_frames in enumerate(all_num_frames):
361
+ if not generation_active or stop_event.is_set():
362
+ break
363
+
364
+ progress = int(((idx + 1) / len(all_num_frames)) * 80) + 15
365
+
366
+ # Special message for first block with torch.compile
367
+ if idx == 0 and torch_compile_applied and not models_compiled:
368
+ emit_progress(
369
+ f"Processing block 1/{len(all_num_frames)} - Compiling models (may take 5-10 minutes)...",
370
+ progress,
371
+ )
372
+ print(f"🔥 Processing block {idx+1}/{len(all_num_frames)}")
373
+ models_compiled = True
374
+ else:
375
+ emit_progress(
376
+ f"Processing block {idx+1}/{len(all_num_frames)}...",
377
+ progress,
378
+ )
379
+ print(f"🔄 Processing block {idx+1}/{len(all_num_frames)}")
380
+
381
+ block_start_time = time.time()
382
+
383
+ noisy_input = noise[
384
+ :,
385
+ current_start_frame
386
+ - num_input_frames : current_start_frame
387
+ + current_num_frames
388
+ - num_input_frames,
389
+ ]
390
+
391
+ # Denoising loop
392
+ denoising_start = time.time()
393
+ for index, current_timestep in enumerate(pipeline.denoising_step_list):
394
+ if not generation_active or stop_event.is_set():
395
+ break
396
+
397
+ timestep = (
398
+ torch.ones(
399
+ [1, current_num_frames],
400
+ device=noise.device,
401
+ dtype=torch.int64,
402
+ )
403
+ * current_timestep
404
+ )
405
+
406
+ if index < len(pipeline.denoising_step_list) - 1:
407
+ _, denoised_pred = transformer(
408
+ noisy_image_or_video=noisy_input,
409
+ conditional_dict=conditional_dict,
410
+ timestep=timestep,
411
+ kv_cache=pipeline.kv_cache1,
412
+ crossattn_cache=pipeline.crossattn_cache,
413
+ current_start=current_start_frame * pipeline.frame_seq_length,
414
+ )
415
+ next_timestep = pipeline.denoising_step_list[index + 1]
416
+ noisy_input = pipeline.scheduler.add_noise(
417
+ denoised_pred.flatten(0, 1),
418
+ torch.randn_like(denoised_pred.flatten(0, 1)),
419
+ next_timestep
420
+ * torch.ones(
421
+ [1 * current_num_frames],
422
+ device=noise.device,
423
+ dtype=torch.long,
424
+ ),
425
+ ).unflatten(0, denoised_pred.shape[:2])
426
+ else:
427
+ _, denoised_pred = transformer(
428
+ noisy_image_or_video=noisy_input,
429
+ conditional_dict=conditional_dict,
430
+ timestep=timestep,
431
+ kv_cache=pipeline.kv_cache1,
432
+ crossattn_cache=pipeline.crossattn_cache,
433
+ current_start=current_start_frame * pipeline.frame_seq_length,
434
+ )
435
+
436
+ if not generation_active or stop_event.is_set():
437
+ break
438
+
439
+ denoising_time = time.time() - denoising_start
440
+ print(f"⚡ Block {idx+1} denoising completed in {denoising_time:.2f}s")
441
+
442
+ # Record output
443
+ # all_latents[:, current_start_frame:current_start_frame + current_num_frames] = denoised_pred
444
+
445
+ # Update KV cache for next block
446
+ if idx != len(all_num_frames) - 1:
447
+ transformer(
448
+ noisy_image_or_video=denoised_pred,
449
+ conditional_dict=conditional_dict,
450
+ timestep=torch.zeros_like(timestep),
451
+ kv_cache=pipeline.kv_cache1,
452
+ crossattn_cache=pipeline.crossattn_cache,
453
+ current_start=current_start_frame * pipeline.frame_seq_length,
454
+ )
455
+
456
+ # Decode to pixels and send frames immediately
457
+ print(f"🎨 Decoding block {idx+1} to pixels...")
458
+ decode_start = time.time()
459
+ if args.trt:
460
+ all_current_pixels = []
461
+ for i in range(denoised_pred.shape[1]):
462
+ is_first_frame = (
463
+ torch.tensor(1.0).cuda().half()
464
+ if idx == 0 and i == 0
465
+ else torch.tensor(0.0).cuda().half()
466
+ )
467
+ outputs = vae_decoder.forward(
468
+ denoised_pred[:, i : i + 1, :, :, :].half(),
469
+ is_first_frame,
470
+ *vae_cache,
471
+ )
472
+ # outputs = vae_decoder.forward(denoised_pred.float(), *vae_cache)
473
+ current_pixels, vae_cache = outputs[0], outputs[1:]
474
+ print(current_pixels.max(), current_pixels.min())
475
+ all_current_pixels.append(current_pixels.clone())
476
+ pixels = torch.cat(all_current_pixels, dim=1)
477
+ if idx == 0:
478
+ pixels = pixels[:, 3:, :, :, :] # Skip first 3 frames of first block
479
+ else:
480
+ if current_use_taehv:
481
+ if vae_cache is None:
482
+ vae_cache = denoised_pred
483
+ else:
484
+ denoised_pred = torch.cat([vae_cache, denoised_pred], dim=1)
485
+ vae_cache = denoised_pred[:, -3:, :, :, :]
486
+ pixels = current_vae_decoder.decode(denoised_pred)
487
+ print(f"denoised_pred shape: {denoised_pred.shape}")
488
+ print(f"pixels shape: {pixels.shape}")
489
+ if idx == 0:
490
+ pixels = pixels[:, 3:, :, :, :] # Skip first 3 frames of first block
491
+ else:
492
+ pixels = pixels[:, 12:, :, :, :]
493
+
494
+ else:
495
+ pixels, vae_cache = current_vae_decoder(denoised_pred.half(), *vae_cache)
496
+ if idx == 0:
497
+ pixels = pixels[:, 3:, :, :, :] # Skip first 3 frames of first block
498
+
499
+ decode_time = time.time() - decode_start
500
+ print(f"🎨 Block {idx+1} VAE decoding completed in {decode_time:.2f}s")
501
+
502
+ # Queue frames for non-blocking sending
503
+ block_frames = pixels.shape[1]
504
+ print(f"📡 Queueing {block_frames} frames from block {idx+1} for sending...")
505
+ queue_start = time.time()
506
+
507
+ for frame_idx in range(block_frames):
508
+ if not generation_active or stop_event.is_set():
509
+ break
510
+
511
+ frame_tensor = pixels[0, frame_idx].cpu()
512
+
513
+ # Queue frame data in non-blocking way
514
+ frame_send_queue.put((frame_tensor, total_frames_sent, idx, job_id))
515
+ total_frames_sent += 1
516
+
517
+ queue_time = time.time() - queue_start
518
+ block_time = time.time() - block_start_time
519
+ print(
520
+ f"✅ Block {idx+1} completed in {block_time:.2f}s ({block_frames} frames queued in {queue_time:.3f}s)"
521
+ )
522
+
523
+ current_start_frame += current_num_frames
524
+
525
+ generation_time = time.time() - generation_start_time
526
+ print(
527
+ f"🎉 Generation completed in {generation_time:.2f}s! {total_frames_sent} frames queued for sending"
528
+ )
529
+
530
+ # Wait for all frames to be sent before completing
531
+ emit_progress("Waiting for all frames to be sent...", 97)
532
+ print("⏳ Waiting for all frames to be sent...")
533
+ frame_send_queue.join() # Wait for all queued frames to be processed
534
+ print("✅ All frames sent successfully!")
535
+
536
+ generate_mp4_from_images("./images", "./videos/" + anim_name + ".mp4", frame_rate)
537
+ # Final progress update
538
+ emit_progress("Generation complete!", 100)
539
+
540
+ try:
541
+ socketio.emit(
542
+ "generation_complete",
543
+ {
544
+ "message": "Video generation completed!",
545
+ "total_frames": total_frames_sent,
546
+ "generation_time": f"{generation_time:.2f}s",
547
+ "job_id": job_id,
548
+ },
549
+ )
550
+ except Exception as e:
551
+ print(f"❌ Failed to emit generation complete: {e}")
552
+
553
+ except Exception as e:
554
+ print(f"❌ Generation failed: {e}")
555
+ try:
556
+ socketio.emit(
557
+ "error",
558
+ {"message": f"Generation failed: {str(e)}", "job_id": job_id},
559
+ )
560
+ except Exception as e:
561
+ print(f"❌ Failed to emit error: {e}")
562
+ finally:
563
+ generation_active = False
564
+ stop_event.set()
565
+
566
+ # Clean up sender thread
567
+ try:
568
+ frame_send_queue.put(None)
569
+ except Exception as e:
570
+ print(f"❌ Failed to put None in frame_send_queue: {e}")
571
+
572
+
573
+ def generate_mp4_from_images(image_directory, output_video_path, fps=24):
574
+ """
575
+ Generate an MP4 video from a directory of images ordered alphabetically.
576
+
577
+ :param image_directory: Path to the directory containing images.
578
+ :param output_video_path: Path where the output MP4 will be saved.
579
+ :param fps: Frames per second for the output video.
580
+ """
581
+ global anim_name
582
+ # Construct the ffmpeg command
583
+ cmd = [
584
+ "ffmpeg",
585
+ "-framerate",
586
+ str(fps),
587
+ "-i",
588
+ os.path.join(
589
+ image_directory, anim_name + "/" + anim_name + "_%03d.jpg"
590
+ ), # Adjust the pattern if necessary
591
+ "-c:v",
592
+ "libx264",
593
+ "-pix_fmt",
594
+ "yuv420p",
595
+ output_video_path,
596
+ ]
597
+ try:
598
+ subprocess.run(cmd, check=True)
599
+ print(f"Video saved to {output_video_path}")
600
+ except subprocess.CalledProcessError as e:
601
+ print(f"An error occurred: {e}")
602
+
603
+
604
+ def calculate_sha256(data):
605
+ # Convert data to bytes if it's not already
606
+ if isinstance(data, str):
607
+ data = data.encode()
608
+ # Calculate SHA-256 hash
609
+ sha256_hash = hashlib.sha256(data).hexdigest()
610
+ return sha256_hash
611
+
612
+
613
+ # Socket.IO event handlers
614
+ @socketio.on("connect")
615
+ def handle_connect():
616
+ print("Client connected")
617
+ emit("status", {"message": "Connected to frontend-buffered demo server"})
618
+
619
+
620
+ @socketio.on("disconnect")
621
+ def handle_disconnect():
622
+ print("Client disconnected")
623
+
624
+
625
+ @socketio.on("start_generation")
626
+ def handle_start_generation(data):
627
+ global generation_active, frame_number, anim_name, frame_rate
628
+
629
+ frame_number = 0
630
+ if generation_active:
631
+ emit("error", {"message": "Generation already in progress"})
632
+ return
633
+
634
+ prompt = data.get("prompt", "")
635
+
636
+ seed = data.get("seed", -1)
637
+ if seed == -1:
638
+ seed = random.randint(0, 2**32)
639
+
640
+ # Extract words up to the first punctuation or newline
641
+ words_up_to_punctuation = re.split(r"[^\w\s]", prompt)[0].strip() if prompt else ""
642
+ if not words_up_to_punctuation:
643
+ words_up_to_punctuation = re.split(r"[\n\r]", prompt)[0].strip()
644
+
645
+ # Calculate SHA-256 hash of the entire prompt
646
+ sha256_hash = calculate_sha256(prompt)
647
+
648
+ # Create anim_name with the extracted words and first 10 characters of the hash
649
+ anim_name = f"{words_up_to_punctuation[:20]}_{str(seed)}_{sha256_hash[:10]}"
650
+
651
+ generation_active = True
652
+ generation_start_time = time.time()
653
+ enable_torch_compile = data.get("enable_torch_compile", False)
654
+ enable_fp8 = data.get("enable_fp8", False)
655
+ use_taehv = data.get("use_taehv", False)
656
+ frame_rate = data.get("fps", 6)
657
+
658
+ if not prompt:
659
+ emit("error", {"message": "Prompt is required"})
660
+ return
661
+
662
+ # Start generation in background thread
663
+ socketio.start_background_task(
664
+ generate_video_stream,
665
+ prompt,
666
+ seed,
667
+ enable_torch_compile,
668
+ enable_fp8,
669
+ use_taehv,
670
+ )
671
+ emit(
672
+ "status",
673
+ {"message": "Generation started - frames will be sent immediately"},
674
+ )
675
+
676
+
677
+ @socketio.on("stop_generation")
678
+ def handle_stop_generation():
679
+ global generation_active, stop_event, frame_send_queue
680
+ generation_active = False
681
+ stop_event.set()
682
+
683
+ # Signal sender thread to stop (will be processed after current frames)
684
+ try:
685
+ frame_send_queue.put(None)
686
+ except Exception as e:
687
+ print(f"❌ Failed to put None in frame_send_queue: {e}")
688
+
689
+ emit("status", {"message": "Generation stopped"})
690
+
691
+
692
+ # Web routes
693
+
694
+
695
+ @app.route("/")
696
+ def index():
697
+ return render_template("demo.html")
698
+
699
+
700
+ @app.route("/api/status")
701
+ def api_status():
702
+ return jsonify(
703
+ {
704
+ "generation_active": generation_active,
705
+ "free_vram_gb": get_cuda_free_memory_gb(gpu),
706
+ "fp8_applied": fp8_applied,
707
+ "torch_compile_applied": torch_compile_applied,
708
+ "current_use_taehv": current_use_taehv,
709
+ }
710
+ )
711
+
712
+
713
+ if __name__ == "__main__":
714
+ print(f"🚀 Starting demo on http://{args.host}:{args.port}")
715
+ socketio.run(app, host=args.host, port=args.port, debug=False)
demo_utils/constant.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+
3
+ ZERO_VAE_CACHE = [
4
+ torch.zeros(1, 16, 2, 60, 104),
5
+ torch.zeros(1, 384, 2, 60, 104),
6
+ torch.zeros(1, 384, 2, 60, 104),
7
+ torch.zeros(1, 384, 2, 60, 104),
8
+ torch.zeros(1, 384, 2, 60, 104),
9
+ torch.zeros(1, 384, 2, 60, 104),
10
+ torch.zeros(1, 384, 2, 60, 104),
11
+ torch.zeros(1, 384, 2, 60, 104),
12
+ torch.zeros(1, 384, 2, 60, 104),
13
+ torch.zeros(1, 384, 2, 60, 104),
14
+ torch.zeros(1, 384, 2, 60, 104),
15
+ torch.zeros(1, 384, 2, 60, 104),
16
+ torch.zeros(1, 192, 2, 120, 208),
17
+ torch.zeros(1, 384, 2, 120, 208),
18
+ torch.zeros(1, 384, 2, 120, 208),
19
+ torch.zeros(1, 384, 2, 120, 208),
20
+ torch.zeros(1, 384, 2, 120, 208),
21
+ torch.zeros(1, 384, 2, 120, 208),
22
+ torch.zeros(1, 384, 2, 120, 208),
23
+ torch.zeros(1, 192, 2, 240, 416),
24
+ torch.zeros(1, 192, 2, 240, 416),
25
+ torch.zeros(1, 192, 2, 240, 416),
26
+ torch.zeros(1, 192, 2, 240, 416),
27
+ torch.zeros(1, 192, 2, 240, 416),
28
+ torch.zeros(1, 192, 2, 240, 416),
29
+ torch.zeros(1, 96, 2, 480, 832),
30
+ torch.zeros(1, 96, 2, 480, 832),
31
+ torch.zeros(1, 96, 2, 480, 832),
32
+ torch.zeros(1, 96, 2, 480, 832),
33
+ torch.zeros(1, 96, 2, 480, 832),
34
+ torch.zeros(1, 96, 2, 480, 832),
35
+ torch.zeros(1, 96, 2, 480, 832),
36
+ ]
37
+
38
+ feat_names = [f"vae_cache_{i}" for i in range(len(ZERO_VAE_CACHE))]
39
+ ALL_INPUTS_NAMES = ["z", "use_cache"] + feat_names
demo_utils/memory.py ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copied from https://github.com/lllyasviel/FramePack/tree/main/demo_utils
2
+ # Apache-2.0 License
3
+ # By lllyasviel
4
+
5
+ import torch
6
+
7
+ cpu = torch.device("cpu")
8
+ gpu = torch.device(f"cuda:{torch.cuda.current_device()}")
9
+ gpu_complete_modules = []
10
+
11
+
12
+ class DynamicSwapInstaller:
13
+ @staticmethod
14
+ def _install_module(
15
+ module: torch.nn.Module,
16
+ **kwargs,
17
+ ):
18
+ original_class = module.__class__
19
+ module.__dict__["forge_backup_original_class"] = original_class
20
+
21
+ def hacked_get_attr(
22
+ self,
23
+ name: str,
24
+ ):
25
+ if "_parameters" in self.__dict__:
26
+ _parameters = self.__dict__["_parameters"]
27
+ if name in _parameters:
28
+ p = _parameters[name]
29
+ if p is None:
30
+ return None
31
+ if p.__class__ == torch.nn.Parameter:
32
+ return torch.nn.Parameter(p.to(**kwargs), requires_grad=p.requires_grad)
33
+ else:
34
+ return p.to(**kwargs)
35
+ if "_buffers" in self.__dict__:
36
+ _buffers = self.__dict__["_buffers"]
37
+ if name in _buffers:
38
+ return _buffers[name].to(**kwargs)
39
+ return super(original_class, self).__getattr__(name)
40
+
41
+ module.__class__ = type(
42
+ "DynamicSwap_" + original_class.__name__,
43
+ (original_class,),
44
+ {
45
+ "__getattr__": hacked_get_attr,
46
+ },
47
+ )
48
+
49
+ return
50
+
51
+ @staticmethod
52
+ def _uninstall_module(
53
+ module: torch.nn.Module,
54
+ ):
55
+ if "forge_backup_original_class" in module.__dict__:
56
+ module.__class__ = module.__dict__.pop("forge_backup_original_class")
57
+ return
58
+
59
+ @staticmethod
60
+ def install_model(
61
+ model: torch.nn.Module,
62
+ **kwargs,
63
+ ):
64
+ for m in model.modules():
65
+ DynamicSwapInstaller._install_module(m, **kwargs)
66
+ return
67
+
68
+ @staticmethod
69
+ def uninstall_model(
70
+ model: torch.nn.Module,
71
+ ):
72
+ for m in model.modules():
73
+ DynamicSwapInstaller._uninstall_module(m)
74
+ return
75
+
76
+
77
+ def fake_diffusers_current_device(
78
+ model: torch.nn.Module,
79
+ target_device: torch.device,
80
+ ):
81
+ if hasattr(model, "scale_shift_table"):
82
+ model.scale_shift_table.data = model.scale_shift_table.data.to(target_device)
83
+ return
84
+
85
+ for k, p in model.named_modules():
86
+ if hasattr(p, "weight"):
87
+ p.to(target_device)
88
+ return
89
+
90
+
91
+ def get_cuda_free_memory_gb(
92
+ device=None,
93
+ ):
94
+ if device is None:
95
+ device = gpu
96
+
97
+ memory_stats = torch.cuda.memory_stats(device)
98
+ bytes_active = memory_stats["active_bytes.all.current"]
99
+ bytes_reserved = memory_stats["reserved_bytes.all.current"]
100
+ bytes_free_cuda, _ = torch.cuda.mem_get_info(device)
101
+ bytes_inactive_reserved = bytes_reserved - bytes_active
102
+ bytes_total_available = bytes_free_cuda + bytes_inactive_reserved
103
+ return bytes_total_available / (1024**3)
104
+
105
+
106
+ def move_model_to_device_with_memory_preservation(
107
+ model,
108
+ target_device,
109
+ preserved_memory_gb=0,
110
+ ):
111
+ print(
112
+ f"Moving {model.__class__.__name__} to {target_device} with preserved memory: {preserved_memory_gb} GB"
113
+ )
114
+
115
+ for m in model.modules():
116
+ if get_cuda_free_memory_gb(target_device) <= preserved_memory_gb:
117
+ torch.cuda.empty_cache()
118
+ return
119
+
120
+ if hasattr(m, "weight"):
121
+ m.to(device=target_device)
122
+
123
+ model.to(device=target_device)
124
+ torch.cuda.empty_cache()
125
+ return
126
+
127
+
128
+ def offload_model_from_device_for_memory_preservation(
129
+ model,
130
+ target_device,
131
+ preserved_memory_gb=0,
132
+ ):
133
+ print(
134
+ f"Offloading {model.__class__.__name__} from {target_device} to preserve memory: {preserved_memory_gb} GB"
135
+ )
136
+
137
+ for m in model.modules():
138
+ if get_cuda_free_memory_gb(target_device) >= preserved_memory_gb:
139
+ torch.cuda.empty_cache()
140
+ return
141
+
142
+ if hasattr(m, "weight"):
143
+ m.to(device=cpu)
144
+
145
+ model.to(device=cpu)
146
+ torch.cuda.empty_cache()
147
+ return
148
+
149
+
150
+ def unload_complete_models(
151
+ *args,
152
+ ):
153
+ for m in gpu_complete_modules + list(args):
154
+ m.to(device=cpu)
155
+ print(f"Unloaded {m.__class__.__name__} as complete.")
156
+
157
+ gpu_complete_modules.clear()
158
+ torch.cuda.empty_cache()
159
+ return
160
+
161
+
162
+ def load_model_as_complete(
163
+ model,
164
+ target_device,
165
+ unload=True,
166
+ ):
167
+ if unload:
168
+ unload_complete_models()
169
+
170
+ model.to(device=target_device)
171
+ print(f"Loaded {model.__class__.__name__} to {target_device} as complete.")
172
+
173
+ gpu_complete_modules.append(model)
174
+ return
demo_utils/taehv.py ADDED
@@ -0,0 +1,477 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Tiny AutoEncoder for Hunyuan Video
4
+ (DNN for encoding / decoding videos to Hunyuan Video's latent space)
5
+ """
6
+
7
+ import torch
8
+ import torch.nn as nn
9
+ import torch.nn.functional as F
10
+ from tqdm.auto import tqdm
11
+ from collections import namedtuple
12
+
13
+ DecoderResult = namedtuple("DecoderResult", ("frame", "memory"))
14
+ TWorkItem = namedtuple("TWorkItem", ("input_tensor", "block_index"))
15
+
16
+
17
+ def conv(
18
+ n_in,
19
+ n_out,
20
+ **kwargs,
21
+ ):
22
+ return nn.Conv2d(
23
+ n_in,
24
+ n_out,
25
+ 3,
26
+ padding=1,
27
+ **kwargs,
28
+ )
29
+
30
+
31
+ class Clamp(nn.Module):
32
+ def forward(
33
+ self,
34
+ x,
35
+ ):
36
+ return torch.tanh(x / 3) * 3
37
+
38
+
39
+ class MemBlock(nn.Module):
40
+ def __init__(
41
+ self,
42
+ n_in,
43
+ n_out,
44
+ ):
45
+ super().__init__()
46
+ self.conv = nn.Sequential(
47
+ conv(n_in * 2, n_out),
48
+ nn.ReLU(inplace=True),
49
+ conv(n_out, n_out),
50
+ nn.ReLU(inplace=True),
51
+ conv(n_out, n_out),
52
+ )
53
+ self.skip = nn.Conv2d(n_in, n_out, 1, bias=False) if n_in != n_out else nn.Identity()
54
+ self.act = nn.ReLU(inplace=True)
55
+
56
+ def forward(
57
+ self,
58
+ x,
59
+ past,
60
+ ):
61
+ return self.act(
62
+ self.conv(
63
+ torch.cat([x, past], 1),
64
+ )
65
+ + self.skip(x),
66
+ )
67
+
68
+
69
+ class TPool(nn.Module):
70
+ def __init__(
71
+ self,
72
+ n_f,
73
+ stride,
74
+ ):
75
+ super().__init__()
76
+ self.stride = stride
77
+ self.conv = nn.Conv2d(
78
+ n_f * stride,
79
+ n_f,
80
+ 1,
81
+ bias=False,
82
+ )
83
+
84
+ def forward(
85
+ self,
86
+ x,
87
+ ):
88
+ _NT, C, H, W = x.shape
89
+ return self.conv(
90
+ x.reshape(-1, self.stride * C, H, W),
91
+ )
92
+
93
+
94
+ class TGrow(nn.Module):
95
+ def __init__(
96
+ self,
97
+ n_f,
98
+ stride,
99
+ ):
100
+ super().__init__()
101
+ self.stride = stride
102
+ self.conv = nn.Conv2d(
103
+ n_f,
104
+ n_f * stride,
105
+ 1,
106
+ bias=False,
107
+ )
108
+
109
+ def forward(
110
+ self,
111
+ x,
112
+ ):
113
+ _NT, C, H, W = x.shape
114
+ x = self.conv(x)
115
+ return x.reshape(-1, C, H, W)
116
+
117
+
118
+ def apply_model_with_memblocks(
119
+ model,
120
+ x,
121
+ parallel,
122
+ show_progress_bar,
123
+ ):
124
+ """
125
+ Apply a sequential model with memblocks to the given input.
126
+ Args:
127
+ - model: nn.Sequential of blocks to apply
128
+ - x: input data, of dimensions NTCHW
129
+ - parallel: if True, parallelize over timesteps (fast but uses O(T) memory)
130
+ if False, each timestep will be processed sequentially (slow but uses O(1) memory)
131
+ - show_progress_bar: if True, enables tqdm progressbar display
132
+
133
+ Returns NTCHW tensor of output data.
134
+ """
135
+ assert x.ndim == 5, f"TAEHV operates on NTCHW tensors, but got {x.ndim}-dim tensor"
136
+ N, T, C, H, W = x.shape
137
+ if parallel:
138
+ x = x.reshape(N * T, C, H, W)
139
+ # parallel over input timesteps, iterate over blocks
140
+ for b in tqdm(model, disable=not show_progress_bar):
141
+ if isinstance(b, MemBlock):
142
+ NT, C, H, W = x.shape
143
+ T = NT // N
144
+ _x = x.reshape(N, T, C, H, W)
145
+ mem = F.pad(_x, (0, 0, 0, 0, 0, 0, 1, 0), value=0)[:, :T].reshape(x.shape)
146
+ x = b(x, mem)
147
+ else:
148
+ x = b(x)
149
+ NT, C, H, W = x.shape
150
+ T = NT // N
151
+ x = x.view(N, T, C, H, W)
152
+ else:
153
+ # TODO(oboerbohan): at least on macos this still gradually uses more memory during decode...
154
+ # need to fix :(
155
+ out = []
156
+ # iterate over input timesteps and also iterate over blocks.
157
+ # because of the cursed TPool/TGrow blocks, this is not a nested loop,
158
+ # it's actually a ***graph traversal*** problem! so let's make a queue
159
+ work_queue = [
160
+ TWorkItem(xt, 0) for t, xt in enumerate(x.reshape(N, T * C, H, W).chunk(T, dim=1))
161
+ ]
162
+ # in addition to manually managing our queue, we also need to manually manage our progressbar.
163
+ # we'll update it for every source node that we consume.
164
+ progress_bar = tqdm(range(T), disable=not show_progress_bar)
165
+ # we'll also need a separate addressable memory per node as well
166
+ mem = [None] * len(model)
167
+ while work_queue:
168
+ xt, i = work_queue.pop(0)
169
+ if i == 0:
170
+ # new source node consumed
171
+ progress_bar.update(1)
172
+ if i == len(model):
173
+ # reached end of the graph, append result to output list
174
+ out.append(xt)
175
+ else:
176
+ # fetch the block to process
177
+ b = model[i]
178
+ if isinstance(b, MemBlock):
179
+ # mem blocks are simple since we're visiting the graph in causal order
180
+ if mem[i] is None:
181
+ xt_new = b(xt, xt * 0)
182
+ mem[i] = xt
183
+ else:
184
+ xt_new = b(xt, mem[i])
185
+ mem[i].copy_(
186
+ xt
187
+ ) # inplace might reduce mysterious pytorch memory allocations? doesn't help though
188
+ # add successor to work queue
189
+ work_queue.insert(0, TWorkItem(xt_new, i + 1))
190
+ elif isinstance(b, TPool):
191
+ # pool blocks are miserable
192
+ if mem[i] is None:
193
+ mem[i] = [] # pool memory is itself a queue of inputs to pool
194
+ mem[i].append(xt)
195
+ if len(mem[i]) > b.stride:
196
+ # pool mem is in invalid state, we should have pooled before this
197
+ raise ValueError("???")
198
+ elif len(mem[i]) < b.stride:
199
+ # pool mem is not yet full, go back to processing the work queue
200
+ pass
201
+ else:
202
+ # pool mem is ready, run the pool block
203
+ N, C, H, W = xt.shape
204
+ xt = b(torch.cat(mem[i], 1).view(N * b.stride, C, H, W))
205
+ # reset the pool mem
206
+ mem[i] = []
207
+ # add successor to work queue
208
+ work_queue.insert(0, TWorkItem(xt, i + 1))
209
+ elif isinstance(b, TGrow):
210
+ xt = b(xt)
211
+ NT, C, H, W = xt.shape
212
+ # each tgrow has multiple successor nodes
213
+ for xt_next in reversed(xt.view(N, b.stride * C, H, W).chunk(b.stride, 1)):
214
+ # add successor to work queue
215
+ work_queue.insert(0, TWorkItem(xt_next, i + 1))
216
+ else:
217
+ # normal block with no funny business
218
+ xt = b(xt)
219
+ # add successor to work queue
220
+ work_queue.insert(0, TWorkItem(xt, i + 1))
221
+ progress_bar.close()
222
+ x = torch.stack(out, 1)
223
+ return x
224
+
225
+
226
+ class TAEHV(nn.Module):
227
+ latent_channels = 16
228
+ image_channels = 3
229
+
230
+ def __init__(
231
+ self,
232
+ checkpoint_path="taehv.pth",
233
+ decoder_time_upscale=(True, True),
234
+ decoder_space_upscale=(True, True, True),
235
+ ):
236
+ """Initialize pretrained TAEHV from the given checkpoint.
237
+
238
+ Arg:
239
+ checkpoint_path: path to weight file to load. taehv.pth for Hunyuan, taew2_1.pth for Wan 2.1.
240
+ decoder_time_upscale: whether temporal upsampling is enabled for each block. upsampling can be disabled for a cheaper preview.
241
+ decoder_space_upscale: whether spatial upsampling is enabled for each block. upsampling can be disabled for a cheaper preview.
242
+ """
243
+ super().__init__()
244
+ self.encoder = nn.Sequential(
245
+ conv(TAEHV.image_channels, 64),
246
+ nn.ReLU(inplace=True),
247
+ TPool(64, 2),
248
+ conv(64, 64, stride=2, bias=False),
249
+ MemBlock(64, 64),
250
+ MemBlock(64, 64),
251
+ MemBlock(64, 64),
252
+ TPool(64, 2),
253
+ conv(64, 64, stride=2, bias=False),
254
+ MemBlock(64, 64),
255
+ MemBlock(64, 64),
256
+ MemBlock(64, 64),
257
+ TPool(64, 1),
258
+ conv(64, 64, stride=2, bias=False),
259
+ MemBlock(64, 64),
260
+ MemBlock(64, 64),
261
+ MemBlock(64, 64),
262
+ conv(64, TAEHV.latent_channels),
263
+ )
264
+ n_f = [256, 128, 64, 64]
265
+ self.frames_to_trim = 2 ** sum(decoder_time_upscale) - 1
266
+ self.decoder = nn.Sequential(
267
+ Clamp(),
268
+ conv(TAEHV.latent_channels, n_f[0]),
269
+ nn.ReLU(inplace=True),
270
+ MemBlock(n_f[0], n_f[0]),
271
+ MemBlock(n_f[0], n_f[0]),
272
+ MemBlock(n_f[0], n_f[0]),
273
+ nn.Upsample(scale_factor=2 if decoder_space_upscale[0] else 1),
274
+ TGrow(n_f[0], 1),
275
+ conv(n_f[0], n_f[1], bias=False),
276
+ MemBlock(n_f[1], n_f[1]),
277
+ MemBlock(n_f[1], n_f[1]),
278
+ MemBlock(n_f[1], n_f[1]),
279
+ nn.Upsample(scale_factor=2 if decoder_space_upscale[1] else 1),
280
+ TGrow(n_f[1], 2 if decoder_time_upscale[0] else 1),
281
+ conv(n_f[1], n_f[2], bias=False),
282
+ MemBlock(n_f[2], n_f[2]),
283
+ MemBlock(n_f[2], n_f[2]),
284
+ MemBlock(n_f[2], n_f[2]),
285
+ nn.Upsample(scale_factor=2 if decoder_space_upscale[2] else 1),
286
+ TGrow(n_f[2], 2 if decoder_time_upscale[1] else 1),
287
+ conv(n_f[2], n_f[3], bias=False),
288
+ nn.ReLU(inplace=True),
289
+ conv(n_f[3], TAEHV.image_channels),
290
+ )
291
+ if checkpoint_path is not None:
292
+ self.load_state_dict(
293
+ self.patch_tgrow_layers(
294
+ torch.load(checkpoint_path, map_location="cpu", weights_only=True)
295
+ )
296
+ )
297
+
298
+ def patch_tgrow_layers(
299
+ self,
300
+ sd,
301
+ ):
302
+ """Patch TGrow layers to use a smaller kernel if needed.
303
+
304
+ Args:
305
+ sd: state dict to patch
306
+ """
307
+ new_sd = self.state_dict()
308
+ for i, layer in enumerate(self.decoder):
309
+ if isinstance(layer, TGrow):
310
+ key = f"decoder.{i}.conv.weight"
311
+ if sd[key].shape[0] > new_sd[key].shape[0]:
312
+ # take the last-timestep output channels
313
+ sd[key] = sd[key][-new_sd[key].shape[0] :]
314
+ return sd
315
+
316
+ def encode_video(
317
+ self,
318
+ x,
319
+ parallel=True,
320
+ show_progress_bar=True,
321
+ ):
322
+ """Encode a sequence of frames.
323
+
324
+ Args:
325
+ x: input NTCHW RGB (C=3) tensor with values in [0, 1].
326
+ parallel: if True, all frames will be processed at once.
327
+ (this is faster but may require more memory).
328
+ if False, frames will be processed sequentially.
329
+ Returns NTCHW latent tensor with ~Gaussian values.
330
+ """
331
+ return apply_model_with_memblocks(
332
+ self.encoder,
333
+ x,
334
+ parallel,
335
+ show_progress_bar,
336
+ )
337
+
338
+ def decode_video(
339
+ self,
340
+ x,
341
+ parallel=True,
342
+ show_progress_bar=False,
343
+ ):
344
+ """Decode a sequence of frames.
345
+
346
+ Args:
347
+ x: input NTCHW latent (C=12) tensor with ~Gaussian values.
348
+ parallel: if True, all frames will be processed at once.
349
+ (this is faster but may require more memory).
350
+ if False, frames will be processed sequentially.
351
+ Returns NTCHW RGB tensor with ~[0, 1] values.
352
+ """
353
+ x = apply_model_with_memblocks(
354
+ self.decoder,
355
+ x,
356
+ parallel,
357
+ show_progress_bar,
358
+ )
359
+ # return x[:, self.frames_to_trim:]
360
+ return x
361
+
362
+ def forward(
363
+ self,
364
+ x,
365
+ ):
366
+ return self.c(x)
367
+
368
+
369
+ @torch.no_grad()
370
+ def main():
371
+ """Run TAEHV roundtrip reconstruction on the given video paths."""
372
+ import os
373
+ import sys
374
+ import cv2 # no highly esteemed deed is commemorated here
375
+
376
+ class VideoTensorReader:
377
+ def __init__(
378
+ self,
379
+ video_file_path,
380
+ ):
381
+ self.cap = cv2.VideoCapture(video_file_path)
382
+ assert self.cap.isOpened(), f"Could not load {video_file_path}"
383
+ self.fps = self.cap.get(cv2.CAP_PROP_FPS)
384
+
385
+ def __iter__(
386
+ self,
387
+ ):
388
+ return self
389
+
390
+ def __next__(
391
+ self,
392
+ ):
393
+ ret, frame = self.cap.read()
394
+ if not ret:
395
+ self.cap.release()
396
+ raise StopIteration # End of video or error
397
+ return torch.from_numpy(
398
+ cv2.cvtColor(frame, cv2.COLOR_BGR2RGB),
399
+ ).permute(
400
+ 2, 0, 1
401
+ ) # BGR HWC -> RGB CHW
402
+
403
+ class VideoTensorWriter:
404
+ def __init__(
405
+ self,
406
+ video_file_path,
407
+ width_height,
408
+ fps=30,
409
+ ):
410
+ self.writer = cv2.VideoWriter(
411
+ video_file_path,
412
+ cv2.VideoWriter_fourcc(*"mp4v"),
413
+ fps,
414
+ width_height,
415
+ )
416
+ assert self.writer.isOpened(), f"Could not create writer for {video_file_path}"
417
+
418
+ def write(
419
+ self,
420
+ frame_tensor,
421
+ ):
422
+ assert frame_tensor.ndim == 3 and frame_tensor.shape[0] == 3, f"{frame_tensor.shape}??"
423
+ self.writer.write(
424
+ cv2.cvtColor(frame_tensor.permute(1, 2, 0).numpy(), cv2.COLOR_RGB2BGR)
425
+ ) # RGB CHW -> BGR HWC
426
+
427
+ def __del__(
428
+ self,
429
+ ):
430
+ if hasattr(self, "writer"):
431
+ self.writer.release()
432
+
433
+ dev = torch.device(
434
+ "cuda"
435
+ if torch.cuda.is_available()
436
+ else "mps" if torch.backends.mps.is_available() else "cpu"
437
+ )
438
+ dtype = torch.float16
439
+ checkpoint_path = os.getenv("TAEHV_CHECKPOINT_PATH", "taehv.pth")
440
+ checkpoint_name = os.path.splitext(os.path.basename(checkpoint_path))[0]
441
+ print(
442
+ f"Using device \033[31m{dev}\033[0m, dtype \033[32m{dtype}\033[0m, checkpoint \033[34m{checkpoint_name}\033[0m ({checkpoint_path})"
443
+ )
444
+ taehv = TAEHV(checkpoint_path=checkpoint_path).to(dev, dtype)
445
+ for video_path in sys.argv[1:]:
446
+ print(f"Processing {video_path}...")
447
+ video_in = VideoTensorReader(video_path)
448
+ video = torch.stack(list(video_in), 0)[None]
449
+ vid_dev = video.to(dev, dtype).div_(255.0)
450
+ # convert to device tensor
451
+ if video.numel() < 100_000_000:
452
+ print(f" {video_path} seems small enough, will process all frames in parallel")
453
+ # convert to device tensor
454
+ vid_enc = taehv.encode_video(vid_dev)
455
+ print(f" Encoded {video_path} -> {vid_enc.shape}. Decoding...")
456
+ vid_dec = taehv.decode_video(vid_enc)
457
+ print(f" Decoded {video_path} -> {vid_dec.shape}")
458
+ else:
459
+ print(f" {video_path} seems large, will process each frame sequentially")
460
+ # convert to device tensor
461
+ vid_enc = taehv.encode_video(vid_dev, parallel=False)
462
+ print(f" Encoded {video_path} -> {vid_enc.shape}. Decoding...")
463
+ vid_dec = taehv.decode_video(vid_enc, parallel=False)
464
+ print(f" Decoded {video_path} -> {vid_dec.shape}")
465
+ video_out_path = video_path + f".reconstructed_by_{checkpoint_name}.mp4"
466
+ video_out = VideoTensorWriter(
467
+ video_out_path,
468
+ (vid_dec.shape[-1], vid_dec.shape[-2]),
469
+ fps=int(round(video_in.fps)),
470
+ )
471
+ for frame in vid_dec.clamp_(0, 1).mul_(255).round_().byte().cpu()[0]:
472
+ video_out.write(frame)
473
+ print(f" Saved to {video_out_path}")
474
+
475
+
476
+ if __name__ == "__main__":
477
+ main()
demo_utils/utils.py ADDED
@@ -0,0 +1,809 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copied from https://github.com/lllyasviel/FramePack/tree/main/demo_utils
2
+ # Apache-2.0 License
3
+ # By lllyasviel
4
+
5
+ import os
6
+ import cv2
7
+ import json
8
+ import random
9
+ import glob
10
+ import torch
11
+ import einops
12
+ import numpy as np
13
+ import datetime
14
+ import torchvision
15
+
16
+ from PIL import Image
17
+
18
+
19
+ def min_resize(
20
+ x,
21
+ m,
22
+ ):
23
+ if x.shape[0] < x.shape[1]:
24
+ s0 = m
25
+ s1 = int(float(m) / float(x.shape[0]) * float(x.shape[1]))
26
+ else:
27
+ s0 = int(float(m) / float(x.shape[1]) * float(x.shape[0]))
28
+ s1 = m
29
+ new_max = max(s1, s0)
30
+ raw_max = max(x.shape[0], x.shape[1])
31
+ if new_max < raw_max:
32
+ interpolation = cv2.INTER_AREA
33
+ else:
34
+ interpolation = cv2.INTER_LANCZOS4
35
+ y = cv2.resize(x, (s1, s0), interpolation=interpolation)
36
+ return y
37
+
38
+
39
+ def d_resize(
40
+ x,
41
+ y,
42
+ ):
43
+ H, W, C = y.shape
44
+ new_min = min(H, W)
45
+ raw_min = min(x.shape[0], x.shape[1])
46
+ if new_min < raw_min:
47
+ interpolation = cv2.INTER_AREA
48
+ else:
49
+ interpolation = cv2.INTER_LANCZOS4
50
+ y = cv2.resize(x, (W, H), interpolation=interpolation)
51
+ return y
52
+
53
+
54
+ def resize_and_center_crop(
55
+ image,
56
+ target_width,
57
+ target_height,
58
+ ):
59
+ if target_height == image.shape[0] and target_width == image.shape[1]:
60
+ return image
61
+
62
+ pil_image = Image.fromarray(image)
63
+ original_width, original_height = pil_image.size
64
+ scale_factor = max(target_width / original_width, target_height / original_height)
65
+ resized_width = int(round(original_width * scale_factor))
66
+ resized_height = int(round(original_height * scale_factor))
67
+ resized_image = pil_image.resize((resized_width, resized_height), Image.LANCZOS)
68
+ left = (resized_width - target_width) / 2
69
+ top = (resized_height - target_height) / 2
70
+ right = (resized_width + target_width) / 2
71
+ bottom = (resized_height + target_height) / 2
72
+ cropped_image = resized_image.crop((left, top, right, bottom))
73
+ return np.array(cropped_image)
74
+
75
+
76
+ def resize_and_center_crop_pytorch(
77
+ image,
78
+ target_width,
79
+ target_height,
80
+ ):
81
+ B, C, H, W = image.shape
82
+
83
+ if H == target_height and W == target_width:
84
+ return image
85
+
86
+ scale_factor = max(target_width / W, target_height / H)
87
+ resized_width = int(round(W * scale_factor))
88
+ resized_height = int(round(H * scale_factor))
89
+
90
+ resized = torch.nn.functional.interpolate(
91
+ image,
92
+ size=(resized_height, resized_width),
93
+ mode="bilinear",
94
+ align_corners=False,
95
+ )
96
+
97
+ top = (resized_height - target_height) // 2
98
+ left = (resized_width - target_width) // 2
99
+ cropped = resized[:, :, top : top + target_height, left : left + target_width]
100
+
101
+ return cropped
102
+
103
+
104
+ def resize_without_crop(
105
+ image,
106
+ target_width,
107
+ target_height,
108
+ ):
109
+ if target_height == image.shape[0] and target_width == image.shape[1]:
110
+ return image
111
+
112
+ pil_image = Image.fromarray(image)
113
+ resized_image = pil_image.resize((target_width, target_height), Image.LANCZOS)
114
+ return np.array(resized_image)
115
+
116
+
117
+ def just_crop(
118
+ image,
119
+ w,
120
+ h,
121
+ ):
122
+ if h == image.shape[0] and w == image.shape[1]:
123
+ return image
124
+
125
+ original_height, original_width = image.shape[:2]
126
+ k = min(original_height / h, original_width / w)
127
+ new_width = int(round(w * k))
128
+ new_height = int(round(h * k))
129
+ x_start = (original_width - new_width) // 2
130
+ y_start = (original_height - new_height) // 2
131
+ cropped_image = image[y_start : y_start + new_height, x_start : x_start + new_width]
132
+ return cropped_image
133
+
134
+
135
+ def write_to_json(
136
+ data,
137
+ file_path,
138
+ ):
139
+ temp_file_path = file_path + ".tmp"
140
+ with open(temp_file_path, "wt", encoding="utf-8") as temp_file:
141
+ json.dump(data, temp_file, indent=4)
142
+ os.replace(temp_file_path, file_path)
143
+ return
144
+
145
+
146
+ def read_from_json(
147
+ file_path,
148
+ ):
149
+ with open(file_path, "rt", encoding="utf-8") as file:
150
+ data = json.load(file)
151
+ return data
152
+
153
+
154
+ def get_active_parameters(
155
+ m,
156
+ ):
157
+ return {k: v for k, v in m.named_parameters() if v.requires_grad}
158
+
159
+
160
+ def cast_training_params(
161
+ m,
162
+ dtype=torch.float32,
163
+ ):
164
+ result = {}
165
+ for n, param in m.named_parameters():
166
+ if param.requires_grad:
167
+ param.data = param.to(dtype)
168
+ result[n] = param
169
+ return result
170
+
171
+
172
+ def separate_lora_AB(
173
+ parameters,
174
+ B_patterns=None,
175
+ ):
176
+ parameters_normal = {}
177
+ parameters_B = {}
178
+
179
+ if B_patterns is None:
180
+ B_patterns = [".lora_B.", "__zero__"]
181
+
182
+ for k, v in parameters.items():
183
+ if any(B_pattern in k for B_pattern in B_patterns):
184
+ parameters_B[k] = v
185
+ else:
186
+ parameters_normal[k] = v
187
+
188
+ return parameters_normal, parameters_B
189
+
190
+
191
+ def set_attr_recursive(
192
+ obj,
193
+ attr,
194
+ value,
195
+ ):
196
+ attrs = attr.split(".")
197
+ for name in attrs[:-1]:
198
+ obj = getattr(obj, name)
199
+ setattr(obj, attrs[-1], value)
200
+ return
201
+
202
+
203
+ def print_tensor_list_size(
204
+ tensors,
205
+ ):
206
+ total_size = 0
207
+ total_elements = 0
208
+
209
+ if isinstance(tensors, dict):
210
+ tensors = tensors.values()
211
+
212
+ for tensor in tensors:
213
+ total_size += tensor.nelement() * tensor.element_size()
214
+ total_elements += tensor.nelement()
215
+
216
+ total_size_MB = total_size / (1024**2)
217
+ total_elements_B = total_elements / 1e9
218
+
219
+ print(f"Total number of tensors: {len(tensors)}")
220
+ print(f"Total size of tensors: {total_size_MB:.2f} MB")
221
+ print(f"Total number of parameters: {total_elements_B:.3f} billion")
222
+ return
223
+
224
+
225
+ @torch.no_grad()
226
+ def batch_mixture(
227
+ a,
228
+ b=None,
229
+ probability_a=0.5,
230
+ mask_a=None,
231
+ ):
232
+ batch_size = a.size(0)
233
+
234
+ if b is None:
235
+ b = torch.zeros_like(a)
236
+
237
+ if mask_a is None:
238
+ mask_a = torch.rand(batch_size) < probability_a
239
+
240
+ mask_a = mask_a.to(a.device)
241
+ mask_a = mask_a.reshape((batch_size,) + (1,) * (a.dim() - 1))
242
+ result = torch.where(mask_a, a, b)
243
+ return result
244
+
245
+
246
+ @torch.no_grad()
247
+ def zero_module(
248
+ module,
249
+ ):
250
+ for p in module.parameters():
251
+ p.detach().zero_()
252
+ return module
253
+
254
+
255
+ @torch.no_grad()
256
+ def supress_lower_channels(
257
+ m,
258
+ k,
259
+ alpha=0.01,
260
+ ):
261
+ data = m.weight.data.clone()
262
+
263
+ assert int(data.shape[1]) >= k
264
+
265
+ data[:, :k] = data[:, :k] * alpha
266
+ m.weight.data = data.contiguous().clone()
267
+ return m
268
+
269
+
270
+ def freeze_module(
271
+ m,
272
+ ):
273
+ if not hasattr(m, "_forward_inside_frozen_module"):
274
+ m._forward_inside_frozen_module = m.forward
275
+ m.requires_grad_(False)
276
+ m.forward = torch.no_grad()(m.forward)
277
+ return m
278
+
279
+
280
+ def get_latest_safetensors(
281
+ folder_path,
282
+ ):
283
+ safetensors_files = glob.glob(os.path.join(folder_path, "*.safetensors"))
284
+
285
+ if not safetensors_files:
286
+ raise ValueError("No file to resume!")
287
+
288
+ latest_file = max(safetensors_files, key=os.path.getmtime)
289
+ latest_file = os.path.abspath(os.path.realpath(latest_file))
290
+ return latest_file
291
+
292
+
293
+ def generate_random_prompt_from_tags(
294
+ tags_str,
295
+ min_length=3,
296
+ max_length=32,
297
+ ):
298
+ tags = tags_str.split(", ")
299
+ tags = random.sample(tags, k=min(random.randint(min_length, max_length), len(tags)))
300
+ prompt = ", ".join(tags)
301
+ return prompt
302
+
303
+
304
+ def interpolate_numbers(
305
+ a,
306
+ b,
307
+ n,
308
+ round_to_int=False,
309
+ gamma=1.0,
310
+ ):
311
+ numbers = a + (b - a) * (np.linspace(0, 1, n) ** gamma)
312
+ if round_to_int:
313
+ numbers = np.round(numbers).astype(int)
314
+ return numbers.tolist()
315
+
316
+
317
+ def uniform_random_by_intervals(
318
+ inclusive,
319
+ exclusive,
320
+ n,
321
+ round_to_int=False,
322
+ ):
323
+ edges = np.linspace(0, 1, n + 1)
324
+ points = np.random.uniform(edges[:-1], edges[1:])
325
+ numbers = inclusive + (exclusive - inclusive) * points
326
+ if round_to_int:
327
+ numbers = np.round(numbers).astype(int)
328
+ return numbers.tolist()
329
+
330
+
331
+ def soft_append_bcthw(
332
+ history,
333
+ current,
334
+ overlap=0,
335
+ ):
336
+ if overlap <= 0:
337
+ return torch.cat([history, current], dim=2)
338
+
339
+ assert (
340
+ history.shape[2] >= overlap
341
+ ), f"History length ({history.shape[2]}) must be >= overlap ({overlap})"
342
+ assert (
343
+ current.shape[2] >= overlap
344
+ ), f"Current length ({current.shape[2]}) must be >= overlap ({overlap})"
345
+
346
+ weights = torch.linspace(1, 0, overlap, dtype=history.dtype, device=history.device).view(
347
+ 1, 1, -1, 1, 1
348
+ )
349
+ blended = weights * history[:, :, -overlap:] + (1 - weights) * current[:, :, :overlap]
350
+ output = torch.cat([history[:, :, :-overlap], blended, current[:, :, overlap:]], dim=2)
351
+
352
+ return output.to(history)
353
+
354
+
355
+ def save_bcthw_as_mp4(
356
+ x,
357
+ output_filename,
358
+ fps=10,
359
+ crf=0,
360
+ ):
361
+ b, c, t, h, w = x.shape
362
+
363
+ per_row = b
364
+ for p in [6, 5, 4, 3, 2]:
365
+ if b % p == 0:
366
+ per_row = p
367
+ break
368
+
369
+ os.makedirs(
370
+ os.path.dirname(os.path.abspath(os.path.realpath(output_filename))),
371
+ exist_ok=True,
372
+ )
373
+ x = torch.clamp(x.float(), -1.0, 1.0) * 127.5 + 127.5
374
+ x = x.detach().cpu().to(torch.uint8)
375
+ x = einops.rearrange(x, "(m n) c t h w -> t (m h) (n w) c", n=per_row)
376
+ torchvision.io.write_video(
377
+ output_filename,
378
+ x,
379
+ fps=fps,
380
+ video_codec="libx264",
381
+ options={"crf": str(int(crf))},
382
+ )
383
+ return x
384
+
385
+
386
+ def save_bcthw_as_png(
387
+ x,
388
+ output_filename,
389
+ ):
390
+ os.makedirs(
391
+ os.path.dirname(os.path.abspath(os.path.realpath(output_filename))),
392
+ exist_ok=True,
393
+ )
394
+ x = torch.clamp(x.float(), -1.0, 1.0) * 127.5 + 127.5
395
+ x = x.detach().cpu().to(torch.uint8)
396
+ x = einops.rearrange(x, "b c t h w -> c (b h) (t w)")
397
+ torchvision.io.write_png(x, output_filename)
398
+ return output_filename
399
+
400
+
401
+ def save_bchw_as_png(
402
+ x,
403
+ output_filename,
404
+ ):
405
+ os.makedirs(
406
+ os.path.dirname(os.path.abspath(os.path.realpath(output_filename))),
407
+ exist_ok=True,
408
+ )
409
+ x = torch.clamp(x.float(), -1.0, 1.0) * 127.5 + 127.5
410
+ x = x.detach().cpu().to(torch.uint8)
411
+ x = einops.rearrange(x, "b c h w -> c h (b w)")
412
+ torchvision.io.write_png(x, output_filename)
413
+ return output_filename
414
+
415
+
416
+ def add_tensors_with_padding(
417
+ tensor1,
418
+ tensor2,
419
+ ):
420
+ if tensor1.shape == tensor2.shape:
421
+ return tensor1 + tensor2
422
+
423
+ shape1 = tensor1.shape
424
+ shape2 = tensor2.shape
425
+
426
+ new_shape = tuple(max(s1, s2) for s1, s2 in zip(shape1, shape2))
427
+
428
+ padded_tensor1 = torch.zeros(new_shape)
429
+ padded_tensor2 = torch.zeros(new_shape)
430
+
431
+ padded_tensor1[tuple(slice(0, s) for s in shape1)] = tensor1
432
+ padded_tensor2[tuple(slice(0, s) for s in shape2)] = tensor2
433
+
434
+ result = padded_tensor1 + padded_tensor2
435
+ return result
436
+
437
+
438
+ def print_free_mem():
439
+ torch.cuda.empty_cache()
440
+ free_mem, total_mem = torch.cuda.mem_get_info(0)
441
+ free_mem_mb = free_mem / (1024**2)
442
+ total_mem_mb = total_mem / (1024**2)
443
+ print(f"Free memory: {free_mem_mb:.2f} MB")
444
+ print(f"Total memory: {total_mem_mb:.2f} MB")
445
+ return
446
+
447
+
448
+ def print_gpu_parameters(
449
+ device,
450
+ state_dict,
451
+ log_count=1,
452
+ ):
453
+ summary = {"device": device, "keys_count": len(state_dict)}
454
+
455
+ logged_params = {}
456
+ for i, (key, tensor) in enumerate(state_dict.items()):
457
+ if i >= log_count:
458
+ break
459
+ logged_params[key] = tensor.flatten()[:3].tolist()
460
+
461
+ summary["params"] = logged_params
462
+
463
+ print(str(summary))
464
+ return
465
+
466
+
467
+ def visualize_txt_as_img(
468
+ width,
469
+ height,
470
+ text,
471
+ font_path="font/DejaVuSans.ttf",
472
+ size=18,
473
+ ):
474
+ from PIL import Image, ImageDraw, ImageFont
475
+
476
+ txt = Image.new("RGB", (width, height), color="white")
477
+ draw = ImageDraw.Draw(txt)
478
+ font = ImageFont.truetype(font_path, size=size)
479
+
480
+ if text == "":
481
+ return np.array(txt)
482
+
483
+ # Split text into lines that fit within the image width
484
+ lines = []
485
+ words = text.split()
486
+ current_line = words[0]
487
+
488
+ for word in words[1:]:
489
+ line_with_word = f"{current_line} {word}"
490
+ if draw.textbbox((0, 0), line_with_word, font=font)[2] <= width:
491
+ current_line = line_with_word
492
+ else:
493
+ lines.append(current_line)
494
+ current_line = word
495
+
496
+ lines.append(current_line)
497
+
498
+ # Draw the text line by line
499
+ y = 0
500
+ line_height = draw.textbbox((0, 0), "A", font=font)[3]
501
+
502
+ for line in lines:
503
+ if y + line_height > height:
504
+ break # stop drawing if the next line will be outside the image
505
+ draw.text((0, y), line, fill="black", font=font)
506
+ y += line_height
507
+
508
+ return np.array(txt)
509
+
510
+
511
+ def blue_mark(
512
+ x,
513
+ ):
514
+ x = x.copy()
515
+ c = x[:, :, 2]
516
+ b = cv2.blur(c, (9, 9))
517
+ x[:, :, 2] = ((c - b) * 16.0 + b).clip(-1, 1)
518
+ return x
519
+
520
+
521
+ def green_mark(
522
+ x,
523
+ ):
524
+ x = x.copy()
525
+ x[:, :, 2] = -1
526
+ x[:, :, 0] = -1
527
+ return x
528
+
529
+
530
+ def frame_mark(
531
+ x,
532
+ ):
533
+ x = x.copy()
534
+ x[:64] = -1
535
+ x[-64:] = -1
536
+ x[:, :8] = 1
537
+ x[:, -8:] = 1
538
+ return x
539
+
540
+
541
+ @torch.inference_mode()
542
+ def pytorch2numpy(
543
+ imgs,
544
+ ):
545
+ results = []
546
+ for x in imgs:
547
+ y = x.movedim(0, -1)
548
+ y = y * 127.5 + 127.5
549
+ y = y.detach().float().cpu().numpy().clip(0, 255).astype(np.uint8)
550
+ results.append(y)
551
+ return results
552
+
553
+
554
+ @torch.inference_mode()
555
+ def numpy2pytorch(
556
+ imgs,
557
+ ):
558
+ h = torch.from_numpy(np.stack(imgs, axis=0)).float() / 127.5 - 1.0
559
+ h = h.movedim(-1, 1)
560
+ return h
561
+
562
+
563
+ @torch.no_grad()
564
+ def duplicate_prefix_to_suffix(
565
+ x,
566
+ count,
567
+ zero_out=False,
568
+ ):
569
+ if zero_out:
570
+ return torch.cat([x, torch.zeros_like(x[:count])], dim=0)
571
+ else:
572
+ return torch.cat([x, x[:count]], dim=0)
573
+
574
+
575
+ def weighted_mse(a, b, weight):
576
+ return torch.mean(weight.float() * (a.float() - b.float()) ** 2)
577
+
578
+
579
+ def clamped_linear_interpolation(
580
+ x,
581
+ x_min,
582
+ y_min,
583
+ x_max,
584
+ y_max,
585
+ sigma=1.0,
586
+ ):
587
+ x = (x - x_min) / (x_max - x_min)
588
+ x = max(0.0, min(x, 1.0))
589
+ x = x**sigma
590
+ return y_min + x * (y_max - y_min)
591
+
592
+
593
+ def expand_to_dims(x, target_dims):
594
+ return x.view(*x.shape, *([1] * max(0, target_dims - x.dim())))
595
+
596
+
597
+ def repeat_to_batch_size(
598
+ tensor: torch.Tensor,
599
+ batch_size: int,
600
+ ):
601
+ if tensor is None:
602
+ return None
603
+
604
+ first_dim = tensor.shape[0]
605
+
606
+ if first_dim == batch_size:
607
+ return tensor
608
+
609
+ if batch_size % first_dim != 0:
610
+ raise ValueError(
611
+ f"Cannot evenly repeat first dim {first_dim} to match batch_size {batch_size}."
612
+ )
613
+
614
+ repeat_times = batch_size // first_dim
615
+
616
+ return tensor.repeat(repeat_times, *[1] * (tensor.dim() - 1))
617
+
618
+
619
+ def dim5(
620
+ x,
621
+ ):
622
+ return expand_to_dims(x, 5)
623
+
624
+
625
+ def dim4(
626
+ x,
627
+ ):
628
+ return expand_to_dims(x, 4)
629
+
630
+
631
+ def dim3(
632
+ x,
633
+ ):
634
+ return expand_to_dims(x, 3)
635
+
636
+
637
+ def crop_or_pad_yield_mask(
638
+ x,
639
+ length,
640
+ ):
641
+ B, F, C = x.shape
642
+ device = x.device
643
+ dtype = x.dtype
644
+
645
+ if F < length:
646
+ y = torch.zeros((B, length, C), dtype=dtype, device=device)
647
+ mask = torch.zeros((B, length), dtype=torch.bool, device=device)
648
+ y[:, :F, :] = x
649
+ mask[:, :F] = True
650
+ return y, mask
651
+
652
+ return x[:, :length, :], torch.ones((B, length), dtype=torch.bool, device=device)
653
+
654
+
655
+ def extend_dim(
656
+ x,
657
+ dim,
658
+ minimal_length,
659
+ zero_pad=False,
660
+ ):
661
+ original_length = int(x.shape[dim])
662
+
663
+ if original_length >= minimal_length:
664
+ return x
665
+
666
+ if zero_pad:
667
+ padding_shape = list(x.shape)
668
+ padding_shape[dim] = minimal_length - original_length
669
+ padding = torch.zeros(padding_shape, dtype=x.dtype, device=x.device)
670
+ else:
671
+ idx = (slice(None),) * dim + (slice(-1, None),) + (slice(None),) * (len(x.shape) - dim - 1)
672
+ last_element = x[idx]
673
+ padding = last_element.repeat_interleave(minimal_length - original_length, dim=dim)
674
+
675
+ return torch.cat([x, padding], dim=dim)
676
+
677
+
678
+ def lazy_positional_encoding(
679
+ t,
680
+ repeats=None,
681
+ ):
682
+ if not isinstance(t, list):
683
+ t = [t]
684
+
685
+ from diffusers.models.embeddings import get_timestep_embedding
686
+
687
+ te = torch.tensor(t)
688
+ te = get_timestep_embedding(
689
+ timesteps=te,
690
+ embedding_dim=256,
691
+ flip_sin_to_cos=True,
692
+ downscale_freq_shift=0.0,
693
+ scale=1.0,
694
+ )
695
+
696
+ if repeats is None:
697
+ return te
698
+
699
+ te = te[:, None, :].expand(-1, repeats, -1)
700
+
701
+ return te
702
+
703
+
704
+ def state_dict_offset_merge(
705
+ A,
706
+ B,
707
+ C=None,
708
+ ):
709
+ result = {}
710
+ keys = A.keys()
711
+
712
+ for key in keys:
713
+ A_value = A[key]
714
+ B_value = B[key].to(A_value)
715
+
716
+ if C is None:
717
+ result[key] = A_value + B_value
718
+ else:
719
+ C_value = C[key].to(A_value)
720
+ result[key] = A_value + B_value - C_value
721
+
722
+ return result
723
+
724
+
725
+ def state_dict_weighted_merge(
726
+ state_dicts,
727
+ weights,
728
+ ):
729
+ if len(state_dicts) != len(weights):
730
+ raise ValueError("Number of state dictionaries must match number of weights")
731
+
732
+ if not state_dicts:
733
+ return {}
734
+
735
+ total_weight = sum(weights)
736
+
737
+ if total_weight == 0:
738
+ raise ValueError("Sum of weights cannot be zero")
739
+
740
+ normalized_weights = [w / total_weight for w in weights]
741
+
742
+ keys = state_dicts[0].keys()
743
+ result = {}
744
+
745
+ for key in keys:
746
+ result[key] = state_dicts[0][key] * normalized_weights[0]
747
+
748
+ for i in range(1, len(state_dicts)):
749
+ state_dict_value = state_dicts[i][key].to(result[key])
750
+ result[key] += state_dict_value * normalized_weights[i]
751
+
752
+ return result
753
+
754
+
755
+ def group_files_by_folder(
756
+ all_files,
757
+ ):
758
+ grouped_files = {}
759
+
760
+ for file in all_files:
761
+ folder_name = os.path.basename(os.path.dirname(file))
762
+ if folder_name not in grouped_files:
763
+ grouped_files[folder_name] = []
764
+ grouped_files[folder_name].append(file)
765
+
766
+ list_of_lists = list(grouped_files.values())
767
+ return list_of_lists
768
+
769
+
770
+ def generate_timestamp():
771
+ now = datetime.datetime.now()
772
+ timestamp = now.strftime("%y%m%d_%H%M%S")
773
+ milliseconds = f"{int(now.microsecond / 1000):03d}"
774
+ random_number = random.randint(0, 9999)
775
+ return f"{timestamp}_{milliseconds}_{random_number}"
776
+
777
+
778
+ def write_PIL_image_with_png_info(
779
+ image,
780
+ metadata,
781
+ path,
782
+ ):
783
+ from PIL.PngImagePlugin import PngInfo
784
+
785
+ png_info = PngInfo()
786
+ for key, value in metadata.items():
787
+ png_info.add_text(key, value)
788
+
789
+ image.save(path, "PNG", pnginfo=png_info)
790
+ return image
791
+
792
+
793
+ def torch_safe_save(
794
+ content,
795
+ path,
796
+ ):
797
+ torch.save(content, path + "_tmp")
798
+ os.replace(path + "_tmp", path)
799
+ return path
800
+
801
+
802
+ def move_optimizer_to_device(
803
+ optimizer,
804
+ device,
805
+ ):
806
+ for state in optimizer.state.values():
807
+ for k, v in state.items():
808
+ if isinstance(v, torch.Tensor):
809
+ state[k] = v.to(device)
demo_utils/vae.py ADDED
@@ -0,0 +1,477 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List
2
+ from einops import rearrange
3
+ import tensorrt as trt
4
+ import torch
5
+ import torch.nn as nn
6
+
7
+ from demo_utils.constant import ALL_INPUTS_NAMES, ZERO_VAE_CACHE
8
+ from wan.modules.vae import AttentionBlock, CausalConv3d, RMS_norm, Upsample
9
+
10
+ CACHE_T = 2
11
+
12
+
13
+ class ResidualBlock(nn.Module):
14
+
15
+ def __init__(
16
+ self,
17
+ in_dim,
18
+ out_dim,
19
+ dropout=0.0,
20
+ ):
21
+ super().__init__()
22
+ self.in_dim = in_dim
23
+ self.out_dim = out_dim
24
+
25
+ # layers
26
+ self.residual = nn.Sequential(
27
+ RMS_norm(in_dim, images=False),
28
+ nn.SiLU(),
29
+ CausalConv3d(in_dim, out_dim, 3, padding=1),
30
+ RMS_norm(out_dim, images=False),
31
+ nn.SiLU(),
32
+ nn.Dropout(dropout),
33
+ CausalConv3d(out_dim, out_dim, 3, padding=1),
34
+ )
35
+ self.shortcut = CausalConv3d(in_dim, out_dim, 1) if in_dim != out_dim else nn.Identity()
36
+
37
+ def forward(
38
+ self,
39
+ x,
40
+ feat_cache_1,
41
+ feat_cache_2,
42
+ ):
43
+ h = self.shortcut(x)
44
+ feat_cache = feat_cache_1
45
+ out_feat_cache = []
46
+ for layer in self.residual:
47
+ if isinstance(layer, CausalConv3d):
48
+ cache_x = x[:, :, -CACHE_T:, :, :].clone()
49
+ if cache_x.shape[2] < 2 and feat_cache is not None:
50
+ # cache last frame of last two chunk
51
+ cache_x = torch.cat(
52
+ [
53
+ feat_cache[:, :, -1, :, :].unsqueeze(2).to(cache_x.device),
54
+ cache_x,
55
+ ],
56
+ dim=2,
57
+ )
58
+ x = layer(x, feat_cache)
59
+ out_feat_cache.append(cache_x)
60
+ feat_cache = feat_cache_2
61
+ else:
62
+ x = layer(x)
63
+ return x + h, *out_feat_cache
64
+
65
+
66
+ class Resample(nn.Module):
67
+
68
+ def __init__(
69
+ self,
70
+ dim,
71
+ mode,
72
+ ):
73
+ assert mode in ("none", "upsample2d", "upsample3d")
74
+ super().__init__()
75
+ self.dim = dim
76
+ self.mode = mode
77
+
78
+ # layers
79
+ if mode == "upsample2d":
80
+ self.resample = nn.Sequential(
81
+ Upsample(scale_factor=(2.0, 2.0), mode="nearest"),
82
+ nn.Conv2d(dim, dim // 2, 3, padding=1),
83
+ )
84
+ elif mode == "upsample3d":
85
+ self.resample = nn.Sequential(
86
+ Upsample(scale_factor=(2.0, 2.0), mode="nearest"),
87
+ nn.Conv2d(dim, dim // 2, 3, padding=1),
88
+ )
89
+ self.time_conv = CausalConv3d(dim, dim * 2, (3, 1, 1), padding=(1, 0, 0))
90
+ else:
91
+ self.resample = nn.Identity()
92
+
93
+ def forward(
94
+ self,
95
+ x,
96
+ is_first_frame,
97
+ feat_cache,
98
+ ):
99
+ if self.mode == "upsample3d":
100
+ b, c, t, h, w = x.size()
101
+ # x, out_feat_cache = torch.cond(
102
+ # is_first_frame,
103
+ # lambda: (torch.cat([torch.zeros_like(x), x], dim=2), feat_cache.clone()),
104
+ # lambda: self.temporal_conv(x, feat_cache),
105
+ # )
106
+ # x, out_feat_cache = torch.cond(
107
+ # is_first_frame,
108
+ # lambda: (torch.cat([torch.zeros_like(x), x], dim=2), feat_cache.clone()),
109
+ # lambda: self.temporal_conv(x, feat_cache),
110
+ # )
111
+ x, out_feat_cache = self.temporal_conv(x, is_first_frame, feat_cache)
112
+ out_feat_cache = torch.cond(
113
+ is_first_frame,
114
+ lambda: feat_cache.clone().contiguous(),
115
+ lambda: out_feat_cache.clone().contiguous(),
116
+ )
117
+ # if is_first_frame:
118
+ # x = torch.cat([torch.zeros_like(x), x], dim=2)
119
+ # out_feat_cache = feat_cache.clone()
120
+ # else:
121
+ # x, out_feat_cache = self.temporal_conv(x, feat_cache)
122
+ else:
123
+ out_feat_cache = None
124
+ t = x.shape[2]
125
+ x = rearrange(x, "b c t h w -> (b t) c h w")
126
+ x = self.resample(x)
127
+ x = rearrange(x, "(b t) c h w -> b c t h w", t=t)
128
+ return x, out_feat_cache
129
+
130
+ def temporal_conv(
131
+ self,
132
+ x,
133
+ is_first_frame,
134
+ feat_cache,
135
+ ):
136
+ b, c, t, h, w = x.size()
137
+ cache_x = x[:, :, -CACHE_T:, :, :].clone()
138
+ if cache_x.shape[2] < 2 and feat_cache is not None:
139
+ cache_x = torch.cat([torch.zeros_like(cache_x), cache_x], dim=2)
140
+ x = torch.cond(
141
+ is_first_frame,
142
+ lambda: torch.cat([torch.zeros_like(x), x], dim=1).contiguous(),
143
+ lambda: self.time_conv(x, feat_cache).contiguous(),
144
+ )
145
+ # x = self.time_conv(x, feat_cache)
146
+ out_feat_cache = cache_x
147
+
148
+ x = x.reshape(b, 2, c, t, h, w)
149
+ x = torch.stack((x[:, 0, :, :, :, :], x[:, 1, :, :, :, :]), 3)
150
+ x = x.reshape(b, c, t * 2, h, w)
151
+ return x.contiguous(), out_feat_cache.contiguous()
152
+
153
+ def init_weight(
154
+ self,
155
+ conv,
156
+ ):
157
+ conv_weight = conv.weight
158
+ nn.init.zeros_(conv_weight)
159
+ c1, c2, t, h, w = conv_weight.size()
160
+ one_matrix = torch.eye(c1, c2)
161
+ init_matrix = one_matrix
162
+ nn.init.zeros_(conv_weight)
163
+ # conv_weight.data[:,:,-1,1,1] = init_matrix * 0.5
164
+ conv_weight.data[:, :, 1, 0, 0] = init_matrix # * 0.5
165
+ conv.weight.data.copy_(conv_weight)
166
+ nn.init.zeros_(conv.bias.data)
167
+
168
+ def init_weight2(
169
+ self,
170
+ conv,
171
+ ):
172
+ conv_weight = conv.weight.data
173
+ nn.init.zeros_(conv_weight)
174
+ c1, c2, t, h, w = conv_weight.size()
175
+ init_matrix = torch.eye(c1 // 2, c2)
176
+ # init_matrix = repeat(init_matrix, 'o ... -> (o 2) ...').permute(1,0,2).contiguous().reshape(c1,c2)
177
+ conv_weight[: c1 // 2, :, -1, 0, 0] = init_matrix
178
+ conv_weight[c1 // 2 :, :, -1, 0, 0] = init_matrix
179
+ conv.weight.data.copy_(conv_weight)
180
+ nn.init.zeros_(conv.bias.data)
181
+
182
+
183
+ class VAEDecoderWrapperSingle(nn.Module):
184
+ def __init__(
185
+ self,
186
+ ):
187
+ super().__init__()
188
+ self.decoder = VAEDecoder3d()
189
+ mean = [
190
+ -0.7571,
191
+ -0.7089,
192
+ -0.9113,
193
+ 0.1075,
194
+ -0.1745,
195
+ 0.9653,
196
+ -0.1517,
197
+ 1.5508,
198
+ 0.4134,
199
+ -0.0715,
200
+ 0.5517,
201
+ -0.3632,
202
+ -0.1922,
203
+ -0.9497,
204
+ 0.2503,
205
+ -0.2921,
206
+ ]
207
+ std = [
208
+ 2.8184,
209
+ 1.4541,
210
+ 2.3275,
211
+ 2.6558,
212
+ 1.2196,
213
+ 1.7708,
214
+ 2.6052,
215
+ 2.0743,
216
+ 3.2687,
217
+ 2.1526,
218
+ 2.8652,
219
+ 1.5579,
220
+ 1.6382,
221
+ 1.1253,
222
+ 2.8251,
223
+ 1.9160,
224
+ ]
225
+ self.mean = torch.tensor(mean, dtype=torch.float32)
226
+ self.std = torch.tensor(std, dtype=torch.float32)
227
+ self.z_dim = 16
228
+ self.conv2 = CausalConv3d(self.z_dim, self.z_dim, 1)
229
+
230
+ def forward(
231
+ self,
232
+ z: torch.Tensor,
233
+ is_first_frame: torch.Tensor,
234
+ *feat_cache: List[torch.Tensor],
235
+ ):
236
+ # from [batch_size, num_frames, num_channels, height, width]
237
+ # to [batch_size, num_channels, num_frames, height, width]
238
+ z = z.permute(0, 2, 1, 3, 4)
239
+ assert z.shape[2] == 1
240
+ feat_cache = list(feat_cache)
241
+ is_first_frame = is_first_frame.bool()
242
+
243
+ device, dtype = z.device, z.dtype
244
+ scale = [
245
+ self.mean.to(device=device, dtype=dtype),
246
+ 1.0 / self.std.to(device=device, dtype=dtype),
247
+ ]
248
+
249
+ if isinstance(scale[0], torch.Tensor):
250
+ z = z / scale[1].view(1, self.z_dim, 1, 1, 1) + scale[0].view(1, self.z_dim, 1, 1, 1)
251
+ else:
252
+ z = z / scale[1] + scale[0]
253
+ x = self.conv2(z)
254
+ out, feat_cache = self.decoder(x, is_first_frame, feat_cache=feat_cache)
255
+ out = out.clamp_(-1, 1)
256
+ # from [batch_size, num_channels, num_frames, height, width]
257
+ # to [batch_size, num_frames, num_channels, height, width]
258
+ out = out.permute(0, 2, 1, 3, 4)
259
+ return out, feat_cache
260
+
261
+
262
+ class VAEDecoder3d(nn.Module):
263
+ def __init__(
264
+ self,
265
+ dim=96,
266
+ z_dim=16,
267
+ dim_mult=[1, 2, 4, 4],
268
+ num_res_blocks=2,
269
+ attn_scales=[],
270
+ temperal_upsample=[True, True, False],
271
+ dropout=0.0,
272
+ ):
273
+ super().__init__()
274
+ self.dim = dim
275
+ self.z_dim = z_dim
276
+ self.dim_mult = dim_mult
277
+ self.num_res_blocks = num_res_blocks
278
+ self.attn_scales = attn_scales
279
+ self.temperal_upsample = temperal_upsample
280
+ self.cache_t = 2
281
+ self.decoder_conv_num = 32
282
+
283
+ # dimensions
284
+ dims = [dim * u for u in [dim_mult[-1]] + dim_mult[::-1]]
285
+ scale = 1.0 / 2 ** (len(dim_mult) - 2)
286
+
287
+ # init block
288
+ self.conv1 = CausalConv3d(z_dim, dims[0], 3, padding=1)
289
+
290
+ # middle blocks
291
+ self.middle = nn.Sequential(
292
+ ResidualBlock(dims[0], dims[0], dropout),
293
+ AttentionBlock(dims[0]),
294
+ ResidualBlock(dims[0], dims[0], dropout),
295
+ )
296
+
297
+ # upsample blocks
298
+ upsamples = []
299
+ for i, (in_dim, out_dim) in enumerate(zip(dims[:-1], dims[1:])):
300
+ # residual (+attention) blocks
301
+ if i == 1 or i == 2 or i == 3:
302
+ in_dim = in_dim // 2
303
+ for _ in range(num_res_blocks + 1):
304
+ upsamples.append(ResidualBlock(in_dim, out_dim, dropout))
305
+ if scale in attn_scales:
306
+ upsamples.append(AttentionBlock(out_dim))
307
+ in_dim = out_dim
308
+
309
+ # upsample block
310
+ if i != len(dim_mult) - 1:
311
+ mode = "upsample3d" if temperal_upsample[i] else "upsample2d"
312
+ upsamples.append(Resample(out_dim, mode=mode))
313
+ scale *= 2.0
314
+ self.upsamples = nn.Sequential(*upsamples)
315
+
316
+ # output blocks
317
+ self.head = nn.Sequential(
318
+ RMS_norm(out_dim, images=False),
319
+ nn.SiLU(),
320
+ CausalConv3d(out_dim, 3, 3, padding=1),
321
+ )
322
+
323
+ def forward(
324
+ self,
325
+ x: torch.Tensor,
326
+ is_first_frame: torch.Tensor,
327
+ feat_cache: List[torch.Tensor],
328
+ ):
329
+ idx = 0
330
+ out_feat_cache = []
331
+
332
+ # conv1
333
+ cache_x = x[:, :, -self.cache_t :, :, :].clone()
334
+ if cache_x.shape[2] < 2 and feat_cache[idx] is not None:
335
+ # cache last frame of last two chunk
336
+ cache_x = torch.cat(
337
+ [
338
+ feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(cache_x.device),
339
+ cache_x,
340
+ ],
341
+ dim=2,
342
+ )
343
+ x = self.conv1(x, feat_cache[idx])
344
+ out_feat_cache.append(cache_x)
345
+ idx += 1
346
+
347
+ # middle
348
+ for layer in self.middle:
349
+ if isinstance(layer, ResidualBlock) and feat_cache is not None:
350
+ x, out_feat_cache_1, out_feat_cache_2 = layer(
351
+ x, feat_cache[idx], feat_cache[idx + 1]
352
+ )
353
+ idx += 2
354
+ out_feat_cache.append(out_feat_cache_1)
355
+ out_feat_cache.append(out_feat_cache_2)
356
+ else:
357
+ x = layer(x)
358
+
359
+ # upsamples
360
+ for layer in self.upsamples:
361
+ if isinstance(layer, Resample):
362
+ x, cache_x = layer(x, is_first_frame, feat_cache[idx])
363
+ if cache_x is not None:
364
+ out_feat_cache.append(cache_x)
365
+ idx += 1
366
+ else:
367
+ x, out_feat_cache_1, out_feat_cache_2 = layer(
368
+ x, feat_cache[idx], feat_cache[idx + 1]
369
+ )
370
+ idx += 2
371
+ out_feat_cache.append(out_feat_cache_1)
372
+ out_feat_cache.append(out_feat_cache_2)
373
+
374
+ # head
375
+ for layer in self.head:
376
+ if isinstance(layer, CausalConv3d) and feat_cache is not None:
377
+ cache_x = x[:, :, -self.cache_t :, :, :].clone()
378
+ if cache_x.shape[2] < 2 and feat_cache[idx] is not None:
379
+ # cache last frame of last two chunk
380
+ cache_x = torch.cat(
381
+ [
382
+ feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(cache_x.device),
383
+ cache_x,
384
+ ],
385
+ dim=2,
386
+ )
387
+ x = layer(x, feat_cache[idx])
388
+ out_feat_cache.append(cache_x)
389
+ idx += 1
390
+ else:
391
+ x = layer(x)
392
+ return x, out_feat_cache
393
+
394
+
395
+ class VAETRTWrapper:
396
+ def __init__(
397
+ self,
398
+ ):
399
+ TRT_LOGGER = trt.Logger(trt.Logger.WARNING)
400
+ with open("checkpoints/vae_decoder_int8.trt", "rb") as f, trt.Runtime(TRT_LOGGER) as rt:
401
+ self.engine: trt.ICudaEngine = rt.deserialize_cuda_engine(f.read())
402
+
403
+ self.context: trt.IExecutionContext = self.engine.create_execution_context()
404
+ self.stream = torch.cuda.current_stream().cuda_stream
405
+
406
+ # ──────────────────────────────
407
+ # 2️⃣ Feed the engine with tensors
408
+ # (name-based API in TRT ≥10)
409
+ # ──────────────────────────────
410
+ self.dtype_map = {
411
+ trt.float32: torch.float32,
412
+ trt.float16: torch.float16,
413
+ trt.int8: torch.int8,
414
+ trt.int32: torch.int32,
415
+ }
416
+ test_input = torch.zeros(1, 16, 1, 60, 104).cuda().half()
417
+ is_first_frame = torch.tensor(1.0).cuda().half()
418
+ test_cache_inputs = [c.cuda().half() for c in ZERO_VAE_CACHE]
419
+ test_inputs = [test_input, is_first_frame] + test_cache_inputs
420
+
421
+ # keep references so buffers stay alive
422
+ self.device_buffers, self.outputs = {}, []
423
+
424
+ # ---- inputs ----
425
+ for i, name in enumerate(ALL_INPUTS_NAMES):
426
+ tensor, scale = test_inputs[i], 1 / 127
427
+ tensor = self.quantize_if_needed(tensor, self.engine.get_tensor_dtype(name), scale)
428
+
429
+ # dynamic shapes
430
+ if -1 in self.engine.get_tensor_shape(name):
431
+ # new API :contentReference[oaicite:0]{index=0}
432
+ self.context.set_input_shape(name, tuple(tensor.shape))
433
+
434
+ # replaces bindings[] :contentReference[oaicite:1]{index=1}
435
+ self.context.set_tensor_address(name, int(tensor.data_ptr()))
436
+ self.device_buffers[name] = tensor # keep pointer alive
437
+
438
+ # ---- (after all input shapes are known) infer output shapes ----
439
+ # propagates shapes :contentReference[oaicite:2]{index=2}
440
+ self.context.infer_shapes()
441
+
442
+ for i in range(self.engine.num_io_tensors):
443
+ name = self.engine.get_tensor_name(i)
444
+ # replaces binding_is_input :contentReference[oaicite:3]{index=3}
445
+ if self.engine.get_tensor_mode(name) == trt.TensorIOMode.OUTPUT:
446
+ shape = tuple(self.context.get_tensor_shape(name))
447
+ dtype = self.dtype_map[self.engine.get_tensor_dtype(name)]
448
+ out = torch.empty(shape, dtype=dtype, device="cuda").contiguous()
449
+
450
+ self.context.set_tensor_address(name, int(out.data_ptr()))
451
+ self.outputs.append(out)
452
+ self.device_buffers[name] = out
453
+
454
+ # helper to quant-convert on the fly
455
+ def quantize_if_needed(
456
+ self,
457
+ t,
458
+ expected_dtype,
459
+ scale,
460
+ ):
461
+ if expected_dtype == trt.int8 and t.dtype != torch.int8:
462
+ t = torch.clamp((t / scale).round(), -128, 127).to(torch.int8).contiguous()
463
+ return t # keep pointer alive
464
+
465
+ def forward(
466
+ self,
467
+ *test_inputs,
468
+ ):
469
+ for i, name in enumerate(ALL_INPUTS_NAMES):
470
+ tensor, scale = test_inputs[i], 1 / 127
471
+ tensor = self.quantize_if_needed(tensor, self.engine.get_tensor_dtype(name), scale)
472
+ self.context.set_tensor_address(name, int(tensor.data_ptr()))
473
+ self.device_buffers[name] = tensor
474
+
475
+ self.context.execute_async_v3(stream_handle=self.stream)
476
+ torch.cuda.current_stream().synchronize()
477
+ return self.outputs
demo_utils/vae_block3.py ADDED
@@ -0,0 +1,364 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List
2
+ from einops import rearrange
3
+ import torch
4
+ import torch.nn as nn
5
+
6
+ from wan.modules.vae import (
7
+ AttentionBlock,
8
+ CausalConv3d,
9
+ RMS_norm,
10
+ ResidualBlock,
11
+ Upsample,
12
+ )
13
+
14
+
15
+ class Resample(nn.Module):
16
+
17
+ def __init__(
18
+ self,
19
+ dim,
20
+ mode,
21
+ ):
22
+ assert mode in (
23
+ "none",
24
+ "upsample2d",
25
+ "upsample3d",
26
+ "downsample2d",
27
+ "downsample3d",
28
+ )
29
+ super().__init__()
30
+ self.dim = dim
31
+ self.mode = mode
32
+ self.cache_t = 2
33
+
34
+ # layers
35
+ if mode == "upsample2d":
36
+ self.resample = nn.Sequential(
37
+ Upsample(scale_factor=(2.0, 2.0), mode="nearest"),
38
+ nn.Conv2d(dim, dim // 2, 3, padding=1),
39
+ )
40
+ elif mode == "upsample3d":
41
+ self.resample = nn.Sequential(
42
+ Upsample(scale_factor=(2.0, 2.0), mode="nearest"),
43
+ nn.Conv2d(dim, dim // 2, 3, padding=1),
44
+ )
45
+ self.time_conv = CausalConv3d(dim, dim * 2, (3, 1, 1), padding=(1, 0, 0))
46
+
47
+ elif mode == "downsample2d":
48
+ self.resample = nn.Sequential(
49
+ nn.ZeroPad2d((0, 1, 0, 1)),
50
+ nn.Conv2d(dim, dim, 3, stride=(2, 2)),
51
+ )
52
+ elif mode == "downsample3d":
53
+ self.resample = nn.Sequential(
54
+ nn.ZeroPad2d((0, 1, 0, 1)),
55
+ nn.Conv2d(dim, dim, 3, stride=(2, 2)),
56
+ )
57
+ self.time_conv = CausalConv3d(dim, dim, (3, 1, 1), stride=(2, 1, 1), padding=(0, 0, 0))
58
+
59
+ else:
60
+ self.resample = nn.Identity()
61
+
62
+ def forward(
63
+ self,
64
+ x,
65
+ feat_cache=None,
66
+ feat_idx=[0],
67
+ ):
68
+ b, c, t, h, w = x.size()
69
+ if self.mode == "upsample3d":
70
+ if feat_cache is not None:
71
+ idx = feat_idx[0]
72
+ if feat_cache[idx] is None:
73
+ feat_cache[idx] = "Rep"
74
+ feat_idx[0] += 1
75
+ else:
76
+
77
+ cache_x = x[:, :, -self.cache_t :, :, :].clone()
78
+ if (
79
+ cache_x.shape[2] < 2
80
+ and feat_cache[idx] is not None
81
+ and feat_cache[idx] != "Rep"
82
+ ):
83
+ # cache last frame of last two chunk
84
+ cache_x = torch.cat(
85
+ [
86
+ feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(cache_x.device),
87
+ cache_x,
88
+ ],
89
+ dim=2,
90
+ )
91
+ if (
92
+ cache_x.shape[2] < 2
93
+ and feat_cache[idx] is not None
94
+ and feat_cache[idx] == "Rep"
95
+ ):
96
+ cache_x = torch.cat(
97
+ [
98
+ torch.zeros_like(cache_x).to(cache_x.device),
99
+ cache_x,
100
+ ],
101
+ dim=2,
102
+ )
103
+ if feat_cache[idx] == "Rep":
104
+ x = self.time_conv(x)
105
+ else:
106
+ x = self.time_conv(x, feat_cache[idx])
107
+ feat_cache[idx] = cache_x
108
+ feat_idx[0] += 1
109
+
110
+ x = x.reshape(b, 2, c, t, h, w)
111
+ x = torch.stack((x[:, 0, :, :, :, :], x[:, 1, :, :, :, :]), 3)
112
+ x = x.reshape(b, c, t * 2, h, w)
113
+ t = x.shape[2]
114
+ x = rearrange(x, "b c t h w -> (b t) c h w")
115
+ x = self.resample(x)
116
+ x = rearrange(x, "(b t) c h w -> b c t h w", t=t)
117
+
118
+ if self.mode == "downsample3d":
119
+ if feat_cache is not None:
120
+ idx = feat_idx[0]
121
+ if feat_cache[idx] is None:
122
+ feat_cache[idx] = x.clone()
123
+ feat_idx[0] += 1
124
+ else:
125
+
126
+ cache_x = x[:, :, -1:, :, :].clone()
127
+ # if cache_x.shape[2] < 2 and feat_cache[idx] is not None and feat_cache[idx]!='Rep':
128
+ # # cache last frame of last two chunk
129
+ # cache_x = torch.cat([feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(cache_x.device), cache_x], dim=2)
130
+
131
+ x = self.time_conv(torch.cat([feat_cache[idx][:, :, -1:, :, :], x], 2))
132
+ feat_cache[idx] = cache_x
133
+ feat_idx[0] += 1
134
+ return x
135
+
136
+ def init_weight(
137
+ self,
138
+ conv,
139
+ ):
140
+ conv_weight = conv.weight
141
+ nn.init.zeros_(conv_weight)
142
+ c1, c2, t, h, w = conv_weight.size()
143
+ one_matrix = torch.eye(c1, c2)
144
+ init_matrix = one_matrix
145
+ nn.init.zeros_(conv_weight)
146
+ # conv_weight.data[:,:,-1,1,1] = init_matrix * 0.5
147
+ conv_weight.data[:, :, 1, 0, 0] = init_matrix # * 0.5
148
+ conv.weight.data.copy_(conv_weight)
149
+ nn.init.zeros_(conv.bias.data)
150
+
151
+ def init_weight2(
152
+ self,
153
+ conv,
154
+ ):
155
+ conv_weight = conv.weight.data
156
+ nn.init.zeros_(conv_weight)
157
+ c1, c2, t, h, w = conv_weight.size()
158
+ init_matrix = torch.eye(c1 // 2, c2)
159
+ # init_matrix = repeat(init_matrix, 'o ... -> (o 2) ...').permute(1,0,2).contiguous().reshape(c1,c2)
160
+ conv_weight[: c1 // 2, :, -1, 0, 0] = init_matrix
161
+ conv_weight[c1 // 2 :, :, -1, 0, 0] = init_matrix
162
+ conv.weight.data.copy_(conv_weight)
163
+ nn.init.zeros_(conv.bias.data)
164
+
165
+
166
+ class VAEDecoderWrapper(nn.Module):
167
+ def __init__(
168
+ self,
169
+ ):
170
+ super().__init__()
171
+ self.decoder = VAEDecoder3d()
172
+ mean = [
173
+ -0.7571,
174
+ -0.7089,
175
+ -0.9113,
176
+ 0.1075,
177
+ -0.1745,
178
+ 0.9653,
179
+ -0.1517,
180
+ 1.5508,
181
+ 0.4134,
182
+ -0.0715,
183
+ 0.5517,
184
+ -0.3632,
185
+ -0.1922,
186
+ -0.9497,
187
+ 0.2503,
188
+ -0.2921,
189
+ ]
190
+ std = [
191
+ 2.8184,
192
+ 1.4541,
193
+ 2.3275,
194
+ 2.6558,
195
+ 1.2196,
196
+ 1.7708,
197
+ 2.6052,
198
+ 2.0743,
199
+ 3.2687,
200
+ 2.1526,
201
+ 2.8652,
202
+ 1.5579,
203
+ 1.6382,
204
+ 1.1253,
205
+ 2.8251,
206
+ 1.9160,
207
+ ]
208
+ self.mean = torch.tensor(mean, dtype=torch.float32)
209
+ self.std = torch.tensor(std, dtype=torch.float32)
210
+ self.z_dim = 16
211
+ self.conv2 = CausalConv3d(self.z_dim, self.z_dim, 1)
212
+
213
+ def forward(
214
+ self,
215
+ z: torch.Tensor,
216
+ *feat_cache: List[torch.Tensor],
217
+ ):
218
+ # from [batch_size, num_frames, num_channels, height, width]
219
+ # to [batch_size, num_channels, num_frames, height, width]
220
+ z = z.permute(0, 2, 1, 3, 4)
221
+ feat_cache = list(feat_cache)
222
+ print("Length of feat_cache: ", len(feat_cache))
223
+
224
+ device, dtype = z.device, z.dtype
225
+ scale = [
226
+ self.mean.to(device=device, dtype=dtype),
227
+ 1.0 / self.std.to(device=device, dtype=dtype),
228
+ ]
229
+
230
+ if isinstance(scale[0], torch.Tensor):
231
+ z = z / scale[1].view(1, self.z_dim, 1, 1, 1) + scale[0].view(1, self.z_dim, 1, 1, 1)
232
+ else:
233
+ z = z / scale[1] + scale[0]
234
+ iter_ = z.shape[2]
235
+ x = self.conv2(z)
236
+ for i in range(iter_):
237
+ if i == 0:
238
+ out, feat_cache = self.decoder(x[:, :, i : i + 1, :, :], feat_cache=feat_cache)
239
+ else:
240
+ out_, feat_cache = self.decoder(x[:, :, i : i + 1, :, :], feat_cache=feat_cache)
241
+ out = torch.cat([out, out_], 2)
242
+
243
+ out = out.float().clamp_(-1, 1)
244
+ # from [batch_size, num_channels, num_frames, height, width]
245
+ # to [batch_size, num_frames, num_channels, height, width]
246
+ out = out.permute(0, 2, 1, 3, 4)
247
+ return out, feat_cache
248
+
249
+
250
+ class VAEDecoder3d(nn.Module):
251
+ def __init__(
252
+ self,
253
+ dim=96,
254
+ z_dim=16,
255
+ dim_mult=[1, 2, 4, 4],
256
+ num_res_blocks=2,
257
+ attn_scales=[],
258
+ temperal_upsample=[True, True, False],
259
+ dropout=0.0,
260
+ ):
261
+ super().__init__()
262
+ self.dim = dim
263
+ self.z_dim = z_dim
264
+ self.dim_mult = dim_mult
265
+ self.num_res_blocks = num_res_blocks
266
+ self.attn_scales = attn_scales
267
+ self.temperal_upsample = temperal_upsample
268
+ self.cache_t = 2
269
+ self.decoder_conv_num = 32
270
+
271
+ # dimensions
272
+ dims = [dim * u for u in [dim_mult[-1]] + dim_mult[::-1]]
273
+ scale = 1.0 / 2 ** (len(dim_mult) - 2)
274
+
275
+ # init block
276
+ self.conv1 = CausalConv3d(z_dim, dims[0], 3, padding=1)
277
+
278
+ # middle blocks
279
+ self.middle = nn.Sequential(
280
+ ResidualBlock(dims[0], dims[0], dropout),
281
+ AttentionBlock(dims[0]),
282
+ ResidualBlock(dims[0], dims[0], dropout),
283
+ )
284
+
285
+ # upsample blocks
286
+ upsamples = []
287
+ for i, (in_dim, out_dim) in enumerate(zip(dims[:-1], dims[1:])):
288
+ # residual (+attention) blocks
289
+ if i == 1 or i == 2 or i == 3:
290
+ in_dim = in_dim // 2
291
+ for _ in range(num_res_blocks + 1):
292
+ upsamples.append(ResidualBlock(in_dim, out_dim, dropout))
293
+ if scale in attn_scales:
294
+ upsamples.append(AttentionBlock(out_dim))
295
+ in_dim = out_dim
296
+
297
+ # upsample block
298
+ if i != len(dim_mult) - 1:
299
+ mode = "upsample3d" if temperal_upsample[i] else "upsample2d"
300
+ upsamples.append(Resample(out_dim, mode=mode))
301
+ scale *= 2.0
302
+ self.upsamples = nn.Sequential(*upsamples)
303
+
304
+ # output blocks
305
+ self.head = nn.Sequential(
306
+ RMS_norm(out_dim, images=False),
307
+ nn.SiLU(),
308
+ CausalConv3d(out_dim, 3, 3, padding=1),
309
+ )
310
+
311
+ def forward(
312
+ self,
313
+ x: torch.Tensor,
314
+ feat_cache: List[torch.Tensor],
315
+ ):
316
+ feat_idx = [0]
317
+
318
+ # conv1
319
+ idx = feat_idx[0]
320
+ cache_x = x[:, :, -self.cache_t :, :, :].clone()
321
+ if cache_x.shape[2] < 2 and feat_cache[idx] is not None:
322
+ # cache last frame of last two chunk
323
+ cache_x = torch.cat(
324
+ [
325
+ feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(cache_x.device),
326
+ cache_x,
327
+ ],
328
+ dim=2,
329
+ )
330
+ x = self.conv1(x, feat_cache[idx])
331
+ feat_cache[idx] = cache_x
332
+ feat_idx[0] += 1
333
+
334
+ # middle
335
+ for layer in self.middle:
336
+ if isinstance(layer, ResidualBlock) and feat_cache is not None:
337
+ x = layer(x, feat_cache, feat_idx)
338
+ else:
339
+ x = layer(x)
340
+
341
+ # upsamples
342
+ for layer in self.upsamples:
343
+ x = layer(x, feat_cache, feat_idx)
344
+
345
+ # head
346
+ for layer in self.head:
347
+ if isinstance(layer, CausalConv3d) and feat_cache is not None:
348
+ idx = feat_idx[0]
349
+ cache_x = x[:, :, -self.cache_t :, :, :].clone()
350
+ if cache_x.shape[2] < 2 and feat_cache[idx] is not None:
351
+ # cache last frame of last two chunk
352
+ cache_x = torch.cat(
353
+ [
354
+ feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(cache_x.device),
355
+ cache_x,
356
+ ],
357
+ dim=2,
358
+ )
359
+ x = layer(x, feat_cache[idx])
360
+ feat_cache[idx] = cache_x
361
+ feat_idx[0] += 1
362
+ else:
363
+ x = layer(x)
364
+ return x, feat_cache
demo_utils/vae_torch2trt.py ADDED
@@ -0,0 +1,335 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ---- INT8 (optional) ----
2
+ from demo_utils.vae import (
3
+ VAEDecoderWrapperSingle, # main nn.Module
4
+ ZERO_VAE_CACHE, # helper constants shipped with your code base
5
+ )
6
+ import pycuda.driver as cuda # ← add
7
+ import pycuda.autoinit # noqa
8
+
9
+ import sys
10
+ from pathlib import Path
11
+
12
+ import torch
13
+ import tensorrt as trt
14
+
15
+ from utils.dataset import ShardingLMDBDataset
16
+
17
+ data_path = "/mnt/localssd/wanx_14B_shift-3.0_cfg-5.0_lmdb_oneshard"
18
+ dataset = ShardingLMDBDataset(data_path, max_pair=int(1e8))
19
+ dataloader = torch.utils.data.DataLoader(dataset, batch_size=1, num_workers=0)
20
+
21
+ # ─────────────────────────────────────────────────────────
22
+ # 1️⃣ Bring the PyTorch model into scope
23
+ # (all code you pasted lives in `vae_decoder.py`)
24
+ # ─────────────────────────────────────────────────────────
25
+
26
+ # --- dummy tensors (exact shapes you posted) ---
27
+ dummy_input = torch.randn(1, 1, 16, 60, 104).half().cuda()
28
+ is_first_frame = torch.tensor([1.0], device="cuda", dtype=torch.float16)
29
+ dummy_cache_input = [
30
+ torch.randn(*s.shape).half().cuda() if isinstance(s, torch.Tensor) else s
31
+ for s in ZERO_VAE_CACHE # keep exactly the same ordering
32
+ ]
33
+ inputs = [dummy_input, is_first_frame, *dummy_cache_input]
34
+
35
+ # ─────────────────────────────────────────────────────────
36
+ # 2️⃣ Export → ONNX
37
+ # ─────────────────────────────────────────────────────────
38
+ model = VAEDecoderWrapperSingle().half().cuda().eval()
39
+
40
+ vae_state_dict = torch.load("wan_models/Wan2.1-T2V-1.3B/Wan2.1_VAE.pth", map_location="cpu")
41
+ decoder_state_dict = {}
42
+ for key, value in vae_state_dict.items():
43
+ if "decoder." in key or "conv2" in key:
44
+ decoder_state_dict[key] = value
45
+ model.load_state_dict(decoder_state_dict)
46
+ model = model.half().cuda().eval() # only batch dim dynamic
47
+
48
+ onnx_path = Path("vae_decoder.onnx")
49
+ feat_names = [f"vae_cache_{i}" for i in range(len(dummy_cache_input))]
50
+ all_inputs_names = ["z", "use_cache"] + feat_names
51
+
52
+ with torch.inference_mode():
53
+ torch.onnx.export(
54
+ model,
55
+ tuple(inputs), # must be a tuple
56
+ onnx_path.as_posix(),
57
+ input_names=all_inputs_names,
58
+ output_names=["rgb_out", "cache_out"],
59
+ opset_version=17,
60
+ do_constant_folding=True,
61
+ dynamo=True,
62
+ )
63
+ print(f"✅ ONNX graph saved to {onnx_path.resolve()}")
64
+
65
+ # (Optional) quick sanity-check with ONNX-Runtime
66
+ try:
67
+ import onnxruntime as ort
68
+
69
+ sess = ort.InferenceSession(onnx_path.as_posix(), providers=["CUDAExecutionProvider"])
70
+ ort_inputs = {n: t.cpu().numpy() for n, t in zip(all_inputs_names, inputs)}
71
+ _ = sess.run(None, ort_inputs)
72
+ print("✅ ONNX graph is executable")
73
+ except Exception as e:
74
+ print("⚠️ ONNX check failed:", e)
75
+
76
+ # ─────────────────────────────────────────────────────────
77
+ # 3️⃣ Build the TensorRT engine
78
+ # ─────────────────────────────────────────────────────────
79
+ TRT_LOGGER = trt.Logger(trt.Logger.WARNING)
80
+ builder = trt.Builder(TRT_LOGGER)
81
+ network = builder.create_network(1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH))
82
+ parser = trt.OnnxParser(network, TRT_LOGGER)
83
+
84
+ with open(onnx_path, "rb") as f:
85
+ if not parser.parse(f.read()):
86
+ for i in range(parser.num_errors):
87
+ print(parser.get_error(i))
88
+ sys.exit("❌ ONNX → TRT parsing failed")
89
+
90
+ config = builder.create_builder_config()
91
+
92
+
93
+ def set_workspace(
94
+ config,
95
+ bytes_,
96
+ ):
97
+ """Version-agnostic workspace limit."""
98
+ if hasattr(config, "max_workspace_size"): # TRT 8 / 9
99
+ config.max_workspace_size = bytes_
100
+ else: # TRT 10+
101
+ config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, bytes_)
102
+
103
+
104
+ # …
105
+ config = builder.create_builder_config()
106
+ set_workspace(config, 4 << 30) # 4 GB
107
+ # 4 GB
108
+
109
+ if builder.platform_has_fast_fp16:
110
+ config.set_flag(trt.BuilderFlag.FP16)
111
+
112
+ # ---- INT8 (optional) ----
113
+ # provide a calibrator if you need an INT8 engine; comment this
114
+ # block if you only care about FP16.
115
+ # ─────────────────────────────────────────────────────────
116
+ # helper: version-agnostic workspace limit
117
+ # ─────────────────────────────────────────────────────────
118
+
119
+
120
+ def set_workspace(
121
+ config: trt.IBuilderConfig,
122
+ bytes_: int = 4 << 30,
123
+ ):
124
+ """
125
+ TRT < 10.x → config.max_workspace_size
126
+ TRT ≥ 10.x → config.set_memory_pool_limit(...)
127
+ """
128
+ if hasattr(config, "max_workspace_size"): # TRT 8 / 9
129
+ config.max_workspace_size = bytes_
130
+ else: # TRT 10+
131
+ config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, bytes_)
132
+
133
+
134
+ # ─────────────────────────────────────────────────────────
135
+ # (optional) INT-8 calibrator
136
+ # ─────────────────────────────────────────────────────────
137
+ # ‼ Only keep this block if you really need INT-8 ‼ # gracefully skip if PyCUDA not present
138
+
139
+
140
+ class VAECalibrator(trt.IInt8EntropyCalibrator2):
141
+ def __init__(
142
+ self,
143
+ loader,
144
+ cache="calibration.cache",
145
+ max_batches=10,
146
+ ):
147
+ super().__init__()
148
+ self.loader = iter(loader)
149
+ self.batch_size = loader.batch_size or 1
150
+ self.max_batches = max_batches
151
+ self.count = 0
152
+ self.cache_file = cache
153
+ self.stream = cuda.Stream()
154
+ self.dev_ptrs = {}
155
+
156
+ # --- TRT 10 needs BOTH spellings ---
157
+ def get_batch_size(
158
+ self,
159
+ ):
160
+ return self.batch_size
161
+
162
+ def getBatchSize(
163
+ self,
164
+ ):
165
+ return self.batch_size
166
+
167
+ def get_batch(
168
+ self,
169
+ names,
170
+ ):
171
+ if self.count >= self.max_batches:
172
+ return None
173
+
174
+ # Randomly sample a number from 1 to 10
175
+ import random
176
+
177
+ vae_idx = random.randint(0, 10)
178
+ data = next(self.loader)
179
+
180
+ latent = data["ode_latent"][0][:, :1]
181
+ is_first_frame = torch.tensor([1.0], device="cuda", dtype=torch.float16)
182
+ feat_cache = ZERO_VAE_CACHE
183
+ for i in range(vae_idx):
184
+ inputs = [latent, is_first_frame, *feat_cache]
185
+ with torch.inference_mode():
186
+ outputs = model(*inputs)
187
+ latent = data["ode_latent"][0][:, i + 1 : i + 2]
188
+ is_first_frame = torch.tensor([0.0], device="cuda", dtype=torch.float16)
189
+ feat_cache = outputs[1:]
190
+
191
+ # -------- ensure context is current --------
192
+ z_np = latent.cpu().numpy().astype("float32")
193
+
194
+ ptrs = [] # list[int] – one entry per name
195
+ for name in names: # <-- match TRT's binding order
196
+ if name == "z":
197
+ arr = z_np
198
+ elif name == "use_cache":
199
+ arr = is_first_frame.cpu().numpy().astype("float32")
200
+ else:
201
+ idx = int(name.split("_")[-1]) # "vae_cache_17" -> 17
202
+ arr = feat_cache[idx].cpu().numpy().astype("float32")
203
+
204
+ if name not in self.dev_ptrs:
205
+ self.dev_ptrs[name] = cuda.mem_alloc(arr.nbytes)
206
+
207
+ cuda.memcpy_htod_async(self.dev_ptrs[name], arr, self.stream)
208
+ ptrs.append(int(self.dev_ptrs[name])) # ***int() is required***
209
+
210
+ self.stream.synchronize()
211
+ self.count += 1
212
+ print(f"Calibration batch {self.count}/{self.max_batches}")
213
+ return ptrs
214
+
215
+ # --- calibration-cache helpers (both spellings) ---
216
+ def read_calibration_cache(
217
+ self,
218
+ ):
219
+ try:
220
+ with open(self.cache_file, "rb") as f:
221
+ return f.read()
222
+ except FileNotFoundError:
223
+ return None
224
+
225
+ def readCalibrationCache(
226
+ self,
227
+ ):
228
+ return self.read_calibration_cache()
229
+
230
+ def write_calibration_cache(
231
+ self,
232
+ cache,
233
+ ):
234
+ with open(self.cache_file, "wb") as f:
235
+ f.write(cache)
236
+
237
+ def writeCalibrationCache(
238
+ self,
239
+ cache,
240
+ ):
241
+ self.write_calibration_cache(cache)
242
+
243
+
244
+ # ─────────────────────────────────────────────────────────
245
+ # Builder-config + optimisation profile
246
+ # ─────────────────────────────────────────────────────────
247
+ config = builder.create_builder_config()
248
+ set_workspace(config, 4 << 30) # 4 GB
249
+
250
+ # ► enable FP16 if possible
251
+ if builder.platform_has_fast_fp16:
252
+ config.set_flag(trt.BuilderFlag.FP16)
253
+
254
+ # ► enable INT-8 (delete this block if you don’t need it)
255
+ if cuda is not None:
256
+ config.set_flag(trt.BuilderFlag.INT8)
257
+ # supply any representative batch you like – here we reuse the latent z
258
+ calib = VAECalibrator(dataloader)
259
+ # TRT-10 renamed the setter:
260
+ if hasattr(config, "set_int8_calibrator"): # TRT 10+
261
+ config.set_int8_calibrator(calib)
262
+ else: # TRT ≤ 9
263
+ config.int8_calibrator = calib
264
+
265
+ # ---- optimisation profile ----
266
+ profile = builder.create_optimization_profile()
267
+ profile.set_shape(
268
+ all_inputs_names[0], # latent z
269
+ min=(1, 1, 16, 60, 104),
270
+ opt=(1, 1, 16, 60, 104),
271
+ max=(1, 1, 16, 60, 104),
272
+ )
273
+ profile.set_shape("use_cache", min=(1,), opt=(1,), max=(1,)) # scalar flag
274
+ for name, tensor in zip(all_inputs_names[2:], dummy_cache_input):
275
+ profile.set_shape(name, tensor.shape, tensor.shape, tensor.shape)
276
+
277
+ config.add_optimization_profile(profile)
278
+
279
+ # ─────────────────────────────────────────────────────────
280
+ # Build the engine (API changed in TRT-10)
281
+ # ─────────────────────────────────────────────────────────
282
+ print("⚙️ Building engine … (can take a minute)")
283
+
284
+ if hasattr(builder, "build_serialized_network"): # TRT 10+
285
+ serialized_engine = builder.build_serialized_network(network, config)
286
+ assert serialized_engine is not None, "build_serialized_network() failed"
287
+ plan_path = Path("checkpoints/vae_decoder_int8.trt")
288
+ plan_path.write_bytes(serialized_engine)
289
+ engine_bytes = serialized_engine # keep for smoke-test
290
+ else: # TRT ≤ 9
291
+ engine = builder.build_engine(network, config)
292
+ assert engine is not None, "build_engine() returned None"
293
+ plan_path = Path("checkpoints/vae_decoder_int8.trt")
294
+ plan_path.write_bytes(engine.serialize())
295
+ engine_bytes = engine.serialize()
296
+
297
+ print(f"✅ TensorRT engine written to {plan_path.resolve()}")
298
+
299
+ # ─────────────────────────────────────────────────────────
300
+ # 4️⃣ Quick smoke-test with the brand-new engine
301
+ # ─────────────────────────────────────────────────────────
302
+ with trt.Runtime(TRT_LOGGER) as rt:
303
+ engine = rt.deserialize_cuda_engine(engine_bytes)
304
+ context = engine.create_execution_context()
305
+ stream = torch.cuda.current_stream().cuda_stream
306
+
307
+ # pre-allocate device buffers once
308
+ device_buffers, outputs = {}, []
309
+ dtype_map = {
310
+ trt.float32: torch.float32,
311
+ trt.float16: torch.float16,
312
+ trt.int8: torch.int8,
313
+ trt.int32: torch.int32,
314
+ }
315
+
316
+ for name, tensor in zip(all_inputs_names, inputs):
317
+ if -1 in engine.get_tensor_shape(name): # dynamic input
318
+ context.set_input_shape(name, tensor.shape)
319
+ context.set_tensor_address(name, int(tensor.data_ptr()))
320
+ device_buffers[name] = tensor
321
+
322
+ context.infer_shapes() # propagate ⇢ outputs
323
+ for i in range(engine.num_io_tensors):
324
+ name = engine.get_tensor_name(i)
325
+ if engine.get_tensor_mode(name) == trt.TensorIOMode.OUTPUT:
326
+ shape = tuple(context.get_tensor_shape(name))
327
+ dtype = dtype_map[engine.get_tensor_dtype(name)]
328
+ out = torch.empty(shape, dtype=dtype, device="cuda")
329
+ context.set_tensor_address(name, int(out.data_ptr()))
330
+ outputs.append(out)
331
+ print(f"output {name} shape: {shape}")
332
+
333
+ context.execute_async_v3(stream_handle=stream)
334
+ torch.cuda.current_stream().synchronize()
335
+ print("✅ TRT execution OK – first output shape:", outputs[0].shape)
frequency_utils.py ADDED
@@ -0,0 +1,1020 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ from typing import List, Sequence, Tuple, Union
3
+ import torch
4
+ from torch import Tensor
5
+
6
+ from PIL import Image
7
+ import numpy as np
8
+ import os
9
+ from pathlib import Path
10
+
11
+ torch.set_printoptions(
12
+ linewidth=10000,
13
+ )
14
+
15
+
16
+ def _get_center_distance(size: Tuple[int], device: str = "cpu") -> Tensor:
17
+ """Compute the distance of each matrix element to the center.
18
+
19
+ Args:
20
+ size (Tuple[int]): [m, n].
21
+ device (str, optional): cpu/cuda. Defaults to 'cpu'.
22
+
23
+ Returns:
24
+ Tensor: [m, n].
25
+ """
26
+ m, n = size
27
+ i_ind = torch.tile(
28
+ torch.tensor([[[i]] for i in range(m)], device=device), dims=[1, n, 1]
29
+ ).float() # [m, n, 1]
30
+ j_ind = torch.tile(
31
+ torch.tensor([[[i] for i in range(n)]], device=device), dims=[m, 1, 1]
32
+ ).float() # [m, n, 1]
33
+ ij_ind = torch.cat([i_ind, j_ind], dim=-1) # [m, n, 2]
34
+ ij_ind = ij_ind.reshape([m * n, 1, 2]) # [m * n, 1, 2]
35
+ center_ij = torch.tensor(((m - 1) / 2, (n - 1) / 2), device=device).reshape(1, 2)
36
+ center_ij = torch.tile(center_ij, dims=[m * n, 1, 1])
37
+ dist = torch.cdist(ij_ind, center_ij, p=2).reshape([m, n])
38
+ return dist
39
+
40
+
41
+ def _get_ideal_weights(
42
+ size: Tuple[int], D0: int, lowpass: bool = True, device: str = "cpu"
43
+ ) -> Tensor:
44
+ """Get H(u, v) of ideal bandpass filter.
45
+
46
+ Args:
47
+ size (Tuple[int]): [H, W].
48
+ D0 (int): The cutoff frequency.
49
+ lowpass (bool): True for low-pass filter, otherwise for high-pass filter. Defaults to True.
50
+ device (str, optional): cpu/cuda. Defaults to 'cpu'.
51
+
52
+ Returns:
53
+ Tensor: [H, W].
54
+ """
55
+ center_distance = _get_center_distance(size, device)
56
+ center_distance[center_distance > D0] = -1
57
+ center_distance[center_distance != -1] = 1
58
+ if lowpass is True:
59
+ center_distance[center_distance == -1] = 0
60
+ else:
61
+ center_distance[center_distance == 1] = 0
62
+ center_distance[center_distance == -1] = 1
63
+ return center_distance
64
+
65
+
66
+ def _to_freq(image: Tensor) -> Tensor:
67
+ """Convert from spatial domain to frequency domain.
68
+
69
+ Args:
70
+ image (Tensor): [B, C, H, W].
71
+
72
+ Returns:
73
+ Tensor: [B, C, H, W]
74
+ """
75
+ img_fft = torch.fft.fft2(image)
76
+ img_fft_shift = torch.fft.fftshift(img_fft)
77
+ return img_fft_shift
78
+
79
+
80
+ def _to_space(image_fft: Tensor) -> Tensor:
81
+ """Convert from frequency domain to spatial domain.
82
+
83
+ Args:
84
+ image_fft (Tensor): [B, C, H, W].
85
+
86
+ Returns:
87
+ Tensor: [B, C, H, W].
88
+ """
89
+ img_ifft_shift = torch.fft.ifftshift(image_fft)
90
+ img_ifft = torch.fft.ifft2(img_ifft_shift)
91
+ img = img_ifft.real.clamp(0, 1)
92
+ return img
93
+
94
+
95
+ def ideal_bandpass(image: Tensor, D0: int, lowpass: bool = True) -> Tensor:
96
+ """Low-pass filter for images.
97
+
98
+ Args:
99
+ image (Tensor): [B, C, H, W].
100
+ D0 (int): Cutoff frequency.
101
+ lowpass (bool): True for low-pass filter, otherwise for high-pass filter. Defaults to True.
102
+
103
+ Returns:
104
+ Tensor: [B, C, H, W].
105
+ """
106
+ img_fft = _to_freq(image)
107
+ weights = _get_ideal_weights(img_fft.shape[-2:], D0=D0, lowpass=lowpass, device=image.device)
108
+ img_fft = img_fft * weights
109
+ img = _to_space(img_fft)
110
+ return img
111
+
112
+
113
+ # Butterworth
114
+
115
+
116
+ def _get_butterworth_weights(size: Tuple[int], D0: int, n: int, device: str = "cpu") -> Tensor:
117
+ """Get H(u, v) of Butterworth filter.
118
+
119
+ Args:
120
+ size (Tuple[int]): [H, W].
121
+ D0 (int): The cutoff frequency.
122
+ n (int): Order of Butterworth filters.
123
+ device (str, optional): cpu/cuda. Defaults to 'cpu'.
124
+
125
+ Returns:
126
+ Tensor: [H, W].
127
+ """
128
+ center_distance = _get_center_distance(size=size, device=device)
129
+ weights = 1 / (1 + torch.pow(center_distance / D0, 2 * n))
130
+ return weights
131
+
132
+
133
+ def butterworth(image: Tensor, D0: int, n: int) -> Tensor:
134
+ """Butterworth low-pass filter for images.
135
+
136
+ Args:
137
+ image (Tensor): [B, C, H, W].
138
+ D0 (int): Cutoff frequency.
139
+ n (int): Order of the Butterworth low-pass filter.
140
+
141
+ Returns:
142
+ Tensor: [B, C, H, W].
143
+ """
144
+ img_fft = _to_freq(image)
145
+ weights = _get_butterworth_weights(image.shape[-2:], D0, n, device=image.device)
146
+ img_fft = weights * img_fft
147
+ img = _to_space(img_fft)
148
+ return img
149
+
150
+
151
+ # def my_butterworth_low_pass_filter(
152
+ # shape,
153
+ # stop_freqs: List[float],
154
+ # n=4,
155
+ # ):
156
+ # assert len(shape) == len(stop_freqs)
157
+
158
+ # grid = torch.meshgrid(
159
+ # *[torch.arange(s, dtype=torch.float32) for s in shape],
160
+ # indexing='ij',
161
+ # )
162
+ # # ( [shape[0], shape[1], ..., shape[N]] ) * len(shape)
163
+ # indices = torch.stack(grid, dim=-1).float()
164
+ # # print(f"{indices.shape = }")
165
+ # # [shape[0], shape[1], ..., shape[N], len(shape)]
166
+
167
+ # max_len = torch.tensor(shape).float()
168
+ # max_len -= 1.0
169
+ # max_len /= 2.0
170
+ # # print(f"{max_len = }")
171
+ # # print(f"{max_len.shape = }")
172
+ # # [len(shape)]
173
+ # max_len = max_len.view(*([1]*len(shape)), -1)
174
+ # # print(f"{max_len.shape = }")
175
+ # # [1, 1, ..., 1, len(shape)]
176
+
177
+ # normalized_indices = indices / max_len
178
+ # # [shape[0], shape[1], ..., shape[N], len(shape)]
179
+
180
+ # normalized_indices_offset = normalized_indices - 1
181
+ # # print(f"{normalized_indices_offset.shape = }")
182
+ # # [shape[0], shape[1], ..., shape[N], len(shape)]
183
+
184
+
185
+ # stop_freqs_torch = torch.tensor(stop_freqs).float().view(*([1]*len(shape)), -1)
186
+ # # print(f"{stop_freqs_torch.shape = }")
187
+ # # [1, 1, ..., 1, len(shape)]
188
+
189
+ # scaled_normalized_indices_offset = normalized_indices_offset / stop_freqs_torch
190
+ # # print(f"{scaled_normalized_indices_offset.shape = }")
191
+ # # [shape[0], shape[1], ..., shape[N], len(shape)]
192
+
193
+ # filter_ = 1.0 / (1.0 + torch.pow(scaled_normalized_indices_offset.norm(p=2, dim=-1), 2 * n))
194
+ # return filter_
195
+
196
+
197
+ # def my_butterworth_low_pass_filter_non_center(
198
+ # shape,
199
+ # stop_freqs: List[float],
200
+ # n=4,
201
+ # ):
202
+ # new_shape = [
203
+ # 2*i-1
204
+ # for i in shape
205
+ # ]
206
+ # filter_ = my_butterworth_low_pass_filter(
207
+ # new_shape,
208
+ # n=n,
209
+ # stop_freqs=stop_freqs,
210
+ # )
211
+
212
+ # if len(shape) == 1:
213
+ # crop_filter = filter_[-shape[0]:]
214
+ # elif len(shape) == 2:
215
+ # crop_filter = filter_[-shape[0]:, -shape[1]:]
216
+ # elif len(shape) == 3:
217
+ # crop_filter = filter_[-shape[0]:, -shape[1]:, -shape[2]:]
218
+ # else:
219
+ # raise ValueError("Shape must be 1D, 2D, or 3D.")
220
+ # return crop_filter
221
+
222
+
223
+ # def my_butterworth_high_pass_filter(
224
+ # shape,
225
+ # stop_freqs: List[float],
226
+ # n=4,
227
+ # ):
228
+ # assert len(shape) == len(stop_freqs)
229
+
230
+ # grid = torch.meshgrid(
231
+ # *[torch.arange(s, dtype=torch.float32) for s in shape],
232
+ # indexing='ij',
233
+ # )
234
+ # # ( [shape[0], shape[1], ..., shape[N]] ) * len(shape)
235
+ # indices = torch.stack(grid, dim=-1).float()
236
+ # # print(f"{indices.shape = }")
237
+ # # [shape[0], shape[1], ..., shape[N], len(shape)]
238
+
239
+ # max_len = torch.tensor(shape).float()
240
+ # max_len -= 1.0
241
+ # max_len /= 2.0
242
+ # # print(f"{max_len = }")
243
+ # # print(f"{max_len.shape = }")
244
+ # # [len(shape)]
245
+ # max_len = max_len.view(*([1]*len(shape)), -1)
246
+ # # print(f"{max_len.shape = }")
247
+ # # [1, 1, ..., 1, len(shape)]
248
+
249
+ # normalized_indices = indices / max_len
250
+ # # [shape[0], shape[1], ..., shape[N], len(shape)]
251
+
252
+ # normalized_indices_offset = normalized_indices - 1
253
+ # # print(f"{normalized_indices_offset.shape = }")
254
+ # # [shape[0], shape[1], ..., shape[N], len(shape)]
255
+
256
+
257
+ # stop_freqs_torch = torch.tensor(stop_freqs).float().view(*([1]*len(shape)), -1)
258
+ # # print(f"{stop_freqs_torch.shape = }")
259
+ # # [1, 1, ..., 1, len(shape)]
260
+
261
+ # scaled_normalized_indices_offset = stop_freqs_torch / normalized_indices_offset
262
+ # # print(f"{scaled_normalized_indices_offset.shape = }")
263
+ # # [shape[0], shape[1], ..., shape[N], len(shape)]
264
+
265
+ # filter_ = 1.0 / (1.0 + torch.pow(scaled_normalized_indices_offset.norm(p=2, dim=-1), 2 * n))
266
+ # return filter_
267
+
268
+
269
+ # def my_butterworth_high_pass_filter_non_center(
270
+ # shape,
271
+ # stop_freqs: List[float],
272
+ # n=4,
273
+ # ):
274
+ # new_shape = [
275
+ # 2*i-1
276
+ # for i in shape
277
+ # ]
278
+ # filter_ = my_butterworth_high_pass_filter(
279
+ # new_shape,
280
+ # n=n,
281
+ # stop_freqs=stop_freqs,
282
+ # )
283
+
284
+ # if len(shape) == 1:
285
+ # crop_filter = filter_[-shape[0]:]
286
+ # elif len(shape) == 2:
287
+ # crop_filter = filter_[-shape[0]:, -shape[1]:]
288
+ # elif len(shape) == 3:
289
+ # crop_filter = filter_[-shape[0]:, -shape[1]:, -shape[2]:]
290
+ # else:
291
+ # raise ValueError("Shape must be 1D, 2D, or 3D.")
292
+ # return crop_filter
293
+
294
+
295
+ # ------------------------ Image loading ------------------------
296
+ def load_grayscale_image():
297
+ # Try common sample images; fall back to skimage if available; else ask user to put an image in cwd
298
+ candidates = ["onion.png", "cameraman.tif", "peppers.png", "lena.png", "camera.png"]
299
+ for name in candidates:
300
+ if os.path.exists(name):
301
+ # img = Image.open(name).convert('L')
302
+ img = Image.open(name).convert("RGB")
303
+ image_np = np.asarray(img, dtype=np.float64)
304
+ # print(f"{image_np = }")
305
+ image_np = image_np / 255.0
306
+ # print(f"{image_np = }")
307
+ return image_np
308
+
309
+ raise FileNotFoundError(
310
+ "Could not find a local image. Place an image (e.g., cameraman.tif/peppers.png) in the working directory."
311
+ )
312
+
313
+
314
+ # ------------------------ DCT implementations (orthonormal) ------------------------
315
+
316
+
317
+ def dct2_matrix_ortho(N, device="cpu", dtype=torch.float32):
318
+ # T2[k, n] = sqrt(2/N) * beta(k) * cos(pi/N * (n + 0.5) * k), beta(0)=1/sqrt(2)
319
+ n = torch.arange(N, device=device, dtype=dtype)
320
+ k = torch.arange(N, device=device, dtype=dtype).unsqueeze(1)
321
+ W = torch.cos(math.pi / N * (n + 0.5) * k) # [N, N]
322
+ beta = torch.ones(N, device=device, dtype=dtype)
323
+ beta[0] = 1 / math.sqrt(2.0)
324
+ T = (math.sqrt(2.0 / N) * beta).unsqueeze(1) * W
325
+ return T # orthonormal; inverse is T.T
326
+
327
+
328
+ def dct1_matrix_ortho(N, device="cpu", dtype=torch.float32):
329
+ # T1[k, n] = sqrt(2/(N-1)) * alpha(k) * alpha(n) * cos(pi/(N-1) * n*k)
330
+ # alpha(0)=alpha(N-1)=1/sqrt(2), else 1. Self-inverse (orthonormal and symmetric).
331
+ if N < 2:
332
+ # N=1 trivial case
333
+ return torch.ones((1, 1), device=device, dtype=dtype)
334
+ n = torch.arange(N, device=device, dtype=dtype)
335
+ k = torch.arange(N, device=device, dtype=dtype).unsqueeze(1)
336
+ C = torch.cos(math.pi / (N - 1) * (n * k)) # [N, N]
337
+ alpha = torch.ones(N, device=device, dtype=dtype)
338
+ alpha[0] = 1 / math.sqrt(2.0)
339
+ alpha[-1] = 1 / math.sqrt(2.0)
340
+ T = math.sqrt(2.0 / (N - 1)) * (alpha.unsqueeze(1) * C * alpha.unsqueeze(0))
341
+ return T # orthonormal, symmetric, self-inverse
342
+
343
+
344
+ def dct2_ortho(x, T2=None):
345
+ # x: [N] float tensor. Returns DCT-II (orthonormal) [N].
346
+ x = x.reshape(-1)
347
+ N = x.numel()
348
+ if T2 is None:
349
+ T2 = dct2_matrix_ortho(N, device=x.device, dtype=x.dtype)
350
+ return T2 @ x
351
+
352
+
353
+ def idct2_ortho(X, T2=None):
354
+ # Inverse of DCT-II (orthonormal) is transpose
355
+ X = X.reshape(-1)
356
+ N = X.numel()
357
+ if T2 is None:
358
+ T2 = dct2_matrix_ortho(N, device=X.device, dtype=X.dtype)
359
+ return T2.t() @ X
360
+
361
+
362
+ def dct1_ortho(x, T1=None):
363
+ # x: [N] float tensor. Returns DCT-I (orthonormal) [N].
364
+ x = x.reshape(-1)
365
+ N = x.numel()
366
+ if T1 is None:
367
+ T1 = dct1_matrix_ortho(N, device=x.device, dtype=x.dtype)
368
+ return T1 @ x
369
+
370
+
371
+ def idct1_ortho(X, T1=None):
372
+ # DCT-I orthonormal is self-inverse
373
+ X = X.reshape(-1)
374
+ N = X.numel()
375
+ if T1 is None:
376
+ T1 = dct1_matrix_ortho(N, device=X.device, dtype=X.dtype)
377
+ return T1 @ X
378
+
379
+
380
+ def _complex_dtype_from_real(real_dtype):
381
+ if real_dtype == torch.float32:
382
+ return torch.complex64
383
+ if real_dtype == torch.float64:
384
+ return torch.complex128
385
+ raise TypeError("Only float32/float64 supported.")
386
+
387
+
388
+ def dct2_fft(x, dim=-1, norm="ortho"):
389
+ """
390
+ DCT-II via even-symmetric 2N extension and torch.fft.rfft.
391
+ x: real tensor (..., N)
392
+ Returns: real tensor (..., N)
393
+ norm: 'ortho' (orthonormal, like scipy.fft.dct(..., type=2, norm='ortho')) or None (unnormalized).
394
+ """
395
+ if not torch.is_floating_point(x):
396
+ raise TypeError("x must be float tensor")
397
+ N = x.shape[dim]
398
+ if N < 1:
399
+ return x.clone()
400
+
401
+ # Even extension [x, flip(x)]
402
+ x_flip = torch.flip(x, dims=(dim,))
403
+ s = torch.cat([x, x_flip], dim=dim) # (..., 2N)
404
+
405
+ # RFFT over length 2N
406
+ S = torch.fft.rfft(s, n=2 * N, dim=dim) # (..., N+1)
407
+
408
+ # k = 0..N-1
409
+ k = torch.arange(N, device=x.device, dtype=x.dtype)
410
+ # exp(-j*pi*k/(2N))
411
+ ctype = _complex_dtype_from_real(x.dtype)
412
+ twiddle = torch.exp(-1j * math.pi * k / (2.0 * N)).to(dtype=ctype, device=x.device)
413
+ for _ in range(dim, S.dim() - 1):
414
+ twiddle = twiddle.unsqueeze(-1)
415
+
416
+ # Take real part; factor 1/2 (see derivation)
417
+ C = (S.narrow(dim, 0, N) * twiddle).real * 0.5 # (..., N)
418
+
419
+ if norm == "ortho":
420
+ # Orthonormal scaling: sqrt(2/N) * beta(k), beta(0)=1/sqrt(2)
421
+ C = C * math.sqrt(2.0 / N)
422
+ index0 = [slice(None)] * C.dim()
423
+ index0[dim] = 0
424
+ C[tuple(index0)] /= math.sqrt(2.0)
425
+ elif norm is None:
426
+ pass
427
+ else:
428
+ raise ValueError("norm must be 'ortho' or None")
429
+ return C
430
+
431
+
432
+ def idct2_fft(C, dim=-1, norm="ortho"):
433
+ """
434
+ Inverse of dct2_fft (i.e., DCT-III) using torch.fft.irfft.
435
+ C: real tensor (..., N) with same norm used in dct2_fft.
436
+ Returns real tensor (..., N).
437
+ """
438
+ if not torch.is_floating_point(C):
439
+ raise TypeError("C must be float tensor")
440
+ N = C.shape[dim]
441
+ if N < 1:
442
+ return C.clone()
443
+
444
+ # Undo orthonormal scaling to get "unnormalized" DCT-II coefficients
445
+ Cun = C
446
+ if norm == "ortho":
447
+ Cun = C / math.sqrt(2.0 / N)
448
+ index0 = [slice(None)] * Cun.dim()
449
+ index0[dim] = 0
450
+ Cun = Cun.clone()
451
+ Cun[tuple(index0)] *= math.sqrt(2.0)
452
+ elif norm is None:
453
+ Cun = C
454
+ else:
455
+ raise ValueError("norm must be 'ortho' or None")
456
+
457
+ # Build unique half-spectrum (length N+1) for the 2N-length irfft
458
+ # S[k] = 2*Cun[k] * exp(+j*pi*k/(2N)), for k=0..N-1
459
+ k = torch.arange(N, device=C.device, dtype=C.dtype)
460
+ ctype = _complex_dtype_from_real(C.dtype)
461
+ twiddle = torch.exp(+1j * math.pi * k / (2.0 * N)).to(dtype=ctype, device=C.device)
462
+ for _ in range(dim, C.dim() - 1):
463
+ twiddle = twiddle.unsqueeze(-1)
464
+
465
+ # Allocate (..., N+1)
466
+ new_shape = list(Cun.shape)
467
+ new_shape[dim] = N + 1
468
+ S_half = torch.zeros(*new_shape, dtype=ctype, device=C.device)
469
+
470
+ # Fill 0..N-1
471
+ # real times complex -> cast below
472
+ S_part = (2.0 * Cun) * twiddle.real - 0j
473
+ S_part = (2.0 * Cun).to(ctype) * twiddle
474
+ S_half.narrow(dim, 0, N).copy_(S_part)
475
+
476
+ # Nyquist (k=N) is zero for the chosen even-symmetric extension
477
+ indexN = [slice(None)] * S_half.dim()
478
+ indexN[dim] = N
479
+ S_half[tuple(indexN)] = 0
480
+
481
+ # irfft to length 2N, take first N samples
482
+ s = torch.fft.irfft(S_half, n=2 * N, dim=dim) # (..., 2N)
483
+
484
+ # Slice first N along dim
485
+ x = s.narrow(dim, 0, N)
486
+ return x
487
+
488
+
489
+ # --------- N-D (multi-axis) DCT-II / IDCT-II built from the 1D versions ---------
490
+ def _normalize_dims(dims, ndim):
491
+ if isinstance(dims, int):
492
+ dims = (dims,)
493
+ dims = tuple(d if d >= 0 else d + ndim for d in dims)
494
+ if any(d < 0 or d >= ndim for d in dims):
495
+ raise ValueError("dims out of range for input tensor.")
496
+ # You can enforce uniqueness if desired:
497
+ if len(set(dims)) != len(dims):
498
+ raise ValueError("dims must be unique.")
499
+ return dims
500
+
501
+
502
+ def dct2_nd_fft(x, dims, norm="ortho"):
503
+ """
504
+ N-D DCT-II applied along the specified dimensions.
505
+ x: real tensor
506
+ dims: tuple of axes (e.g., (-2,-1) for 2D, (-3,-2,-1) for 3D)
507
+ norm: 'ortho' or None
508
+ """
509
+ dims = _normalize_dims(dims, x.ndim)
510
+ y = x
511
+ for d in dims:
512
+ y = dct2_fft(y, dim=d, norm=norm)
513
+ return y
514
+
515
+
516
+ def idct2_nd_fft(X, dims, norm="ortho"):
517
+ """
518
+ N-D inverse of DCT-II (DCT-III) along the specified dimensions.
519
+ """
520
+ dims = _normalize_dims(dims, X.ndim)
521
+ y = X
522
+ for d in dims:
523
+ y = idct2_fft(y, dim=d, norm=norm)
524
+ return y
525
+
526
+
527
+ def _to_device_dtype(x, device, dtype):
528
+ if device is None:
529
+ device = x.device if isinstance(x, torch.Tensor) else "cpu"
530
+ if dtype is None:
531
+ dtype = torch.float64 # match MATLAB double
532
+ return device, dtype
533
+
534
+
535
+ def _omega_grid_1d(N, shifted, device, dtype):
536
+ # Digital radian frequency samples on FFT bins.
537
+ # unshifted: ω_k = 2π k / N, k=0..N-1 (DC at index 0)
538
+ # shifted: fftshift layout (DC at center), monotonically increasing from negative to positive
539
+ k = torch.arange(N, device=device, dtype=dtype)
540
+ w = 2.0 * math.pi * k / N
541
+ # [0, 2π)
542
+ if shifted:
543
+ w = torch.fft.fftshift(w) # center DC
544
+ return w
545
+
546
+
547
+ def _tan_half_abs(w, eps=1e-12):
548
+ # Safe |tan(w/2)| to avoid overflow at w=π.
549
+ half = 0.5 * w
550
+ c = torch.cos(half)
551
+ s = torch.sin(half)
552
+ # Where cos is near zero, use a very large value (approach infinity)
553
+ # large but not inf to avoid NaNs downstream
554
+ large = torch.finfo(w.dtype).max ** 0.5
555
+ t = torch.where(c.abs() < eps, torch.sign(s) * large, s / c)
556
+ return t.abs()
557
+
558
+
559
+ def butterworth_mask_1d(
560
+ N,
561
+ fc,
562
+ order,
563
+ btype="low",
564
+ shifted=False,
565
+ device=None,
566
+ dtype=None,
567
+ ):
568
+ """
569
+ 1D Butterworth frequency mask equivalent to MATLAB butter+freqz magnitude.
570
+ - N: number of FFT bins
571
+ - fc: normalized cutoff(s) in cycles/sample (relative to 1 sample) with 0 < fc < 0.5
572
+ low/high: scalar; bandpass/stop: [f1, f2] with 0 < f1 < f2 < 0.5
573
+ * fc is equivalent to Wn / 2 in MATLAB's butter function. e.g. butter(4, 0.25) is equivalent to fc=0.125 here.
574
+ - order: integer >= 1
575
+ - btype: 'low', 'high', 'bandpass', 'stop'
576
+ - shifted: if True, return mask in fftshift layout (DC at center)
577
+ """
578
+ assert isinstance(N, int) and N >= 2
579
+ assert isinstance(order, int) and order >= 1
580
+ btype = btype.lower()
581
+ if btype in ("low", "high"):
582
+ fc = float(fc)
583
+ assert 0.0 < fc < 0.5
584
+ else:
585
+ assert len(fc) == 2
586
+ f1, f2 = float(fc[0]), float(fc[1])
587
+ assert 0.0 < f1 < f2 < 0.5
588
+ fc = (f1, f2)
589
+
590
+ device, dtype = _to_device_dtype(torch.empty(0), device, dtype)
591
+ w = _omega_grid_1d(N, shifted=shifted, device=device, dtype=dtype) # 0..2π (or centered)
592
+ # Bilinear mapping (prewarped): Ω = 2 * tan(ω/2)
593
+ Om = 2.0 * _tan_half_abs(w) # analog rad/sec (normalized T=1)
594
+
595
+ if btype == "low":
596
+ # Prewarp analog cutoff: Ωc = 2*tan(π*fc)
597
+ Oc = 2.0 * math.tan(math.pi * fc)
598
+ ratio = (Om / Oc).clamp_min(0)
599
+ mag = 1.0 / torch.sqrt(1.0 + ratio.pow(2 * order))
600
+ elif btype == "high":
601
+ Oc = 2.0 * math.tan(math.pi * fc)
602
+ # Handle Om=0 => magnitude=0
603
+ ratio = torch.where(Om > 0, (Oc / Om), torch.full_like(Om, float("inf")))
604
+ mag = 1.0 / torch.sqrt(1.0 + ratio.pow(2 * order))
605
+ elif btype == "bandpass":
606
+ f1, f2 = fc
607
+ O1 = 2.0 * math.tan(math.pi * f1)
608
+ O2 = 2.0 * math.tan(math.pi * f2)
609
+ B = O2 - O1
610
+ O0 = math.sqrt(O1 * O2)
611
+ # D(Ω) = (Ω^2 - Ω0^2)/(B*Ω)
612
+ denom = B * Om
613
+ # denom=0 at Om=0 -> D=inf, magnitude=0
614
+ D = torch.where(denom != 0, (Om.pow(2) - O0**2) / denom, torch.full_like(Om, float("inf")))
615
+ mag = 1.0 / torch.sqrt(1.0 + D.abs().pow(2 * order))
616
+ elif btype in ("stop", "bandstop", "bandreject"):
617
+ f1, f2 = fc
618
+ O1 = 2.0 * math.tan(math.pi * f1)
619
+ O2 = 2.0 * math.tan(math.pi * f2)
620
+ B = O2 - O1
621
+ O0 = math.sqrt(O1 * O2)
622
+ # D(Ω) = (B*Ω)/(Ω^2 - Ω0^2)
623
+ denom = Om.pow(2) - O0**2
624
+ # denom=0 at Om=O0 -> D=inf, magnitude=0
625
+ D = torch.where(denom != 0, (B * Om) / denom, torch.full_like(Om, float("inf")))
626
+ mag = 1.0 / torch.sqrt(1.0 + D.abs().pow(2 * order))
627
+ else:
628
+ raise ValueError("btype must be 'low', 'high', 'bandpass', or 'stop'.")
629
+
630
+ return mag.to(dtype=dtype, device=device)
631
+
632
+
633
+ def butterworth_mask_2d_separable(
634
+ shape,
635
+ fc,
636
+ order,
637
+ btype="low",
638
+ shifted=False,
639
+ device=None,
640
+ dtype=None,
641
+ ):
642
+ """
643
+ 2D separable Butterworth mask (rows × cols), equivalent to applying 1D Butterworth along rows and columns (zero-phase). Not an isotropic circular Butterworth.
644
+ - shape: (M, N)
645
+ - fc: scalar or 2-tuple for low/high; for band types, pass 2-tuples for each axis: ([f1y,f2y], [f1x,f2x]) You can also pass scalar or 2-tuple to apply same cutoffs on both axes.
646
+ - order: integer or 2-tuple for (order_y, order_x)
647
+ - btype: 'low', 'high', 'bandpass', 'stop'
648
+ - shifted: if True, both axes are centered (fftshift layout)
649
+ """
650
+ M, N = int(shape[0]), int(shape[1])
651
+ assert M >= 2 and N >= 2
652
+ device, dtype = _to_device_dtype(torch.empty(0), device, dtype)
653
+
654
+ # Normalize fc/order to per-axis tuples
655
+ if btype in ("low", "high"):
656
+ if not isinstance(fc, (list, tuple)):
657
+ fcy = fcx = fc
658
+ else:
659
+ assert len(fc) == 2
660
+ fcy, fcx = fc
661
+ else:
662
+ # band types
663
+ if isinstance(fc[0], (list, tuple)) and isinstance(fc[1], (list, tuple)):
664
+ fcy, fcx = fc
665
+ else:
666
+ # same band on both axes
667
+ fcy = fcx = fc
668
+
669
+ if isinstance(order, (list, tuple)):
670
+ oy, ox = int(order[0]), int(order[1])
671
+ else:
672
+ oy = ox = int(order)
673
+
674
+ Hy = butterworth_mask_1d(M, fcy, oy, btype=btype, shifted=shifted, device=device, dtype=dtype)
675
+ Hx = butterworth_mask_1d(N, fcx, ox, btype=btype, shifted=shifted, device=device, dtype=dtype)
676
+
677
+ # Outer product to build separable 2D mask
678
+ H2 = Hy.reshape(M, 1) * Hx.reshape(1, N)
679
+ return H2
680
+
681
+
682
+ def _freqvec_norm(
683
+ N: int,
684
+ shifted: bool,
685
+ device=None,
686
+ dtype=None,
687
+ ):
688
+ """
689
+ Normalized frequency vector in [-0.5, 0.5), length N.
690
+ - shifted=False: DC at index 0 (unshifted FFT layout)
691
+ - shifted=True: DC at center (fftshift layout)
692
+ """
693
+ if device is None:
694
+ device = "cpu"
695
+ if dtype is None:
696
+ dtype = torch.float64
697
+ k = torch.arange(N, device=device, dtype=dtype)
698
+ if shifted:
699
+ f = (k - torch.floor(torch.tensor(N / 2, dtype=dtype, device=device))) / N
700
+ else:
701
+ f = k / N
702
+ f = torch.where(f >= 0.5, f - 1.0, f) # wrap into [-0.5, 0.5)
703
+ return f # [N]
704
+
705
+
706
+ def _radial_frequency_nd(
707
+ shape: Sequence[int],
708
+ shifted: bool,
709
+ device=None,
710
+ dtype=None,
711
+ ):
712
+ """
713
+ Radial normalized frequency R in [-0.5,0.5) computed over all axes.
714
+ Returns R with shape 'shape'.
715
+ """
716
+ if device is None:
717
+ device = "cpu"
718
+ if dtype is None:
719
+ dtype = torch.float64
720
+ grids = [_freqvec_norm(N, shifted=shifted, device=device, dtype=dtype) for N in shape]
721
+ # list of tensors, each shape = shape
722
+ meshes = torch.meshgrid(*grids, indexing="ij")
723
+ R2 = torch.zeros(shape, dtype=dtype, device=device)
724
+ for g in meshes:
725
+ R2 = R2 + g**2
726
+ R = torch.sqrt(R2)
727
+ return R
728
+
729
+
730
+ def butterworth_nd(
731
+ shape: Sequence[int],
732
+ cutoff: Union[float, Tuple[float, float]],
733
+ order: int,
734
+ btype: str = "low",
735
+ shifted: bool = False,
736
+ device=None,
737
+ dtype=None,
738
+ ):
739
+ """Isotropic N-D Butterworth mask (low/high/bandpass/bandstop).
740
+ Args:
741
+ shape: iterable of ints, e.g., (H, W) or (D, H, W) ...
742
+ cutoff:
743
+ - 'low'/'high': scalar D0 in (0, 0.5]
744
+ - 'bandpass'/'bandstop': tuple (D1, D2) with 0 < D1 < D2 <= 0.5
745
+ order: integer >= 1
746
+ btype: 'low' | 'high' | 'bandpass' | 'bandstop' (alias 'stop')
747
+ shifted: if True, mask is centered (fftshift layout); else unshifted
748
+ device, dtype: optional torch device/dtype (defaults: CPU, float64)
749
+
750
+ Returns:
751
+ H: tensor with shape 'shape', values in [0, 1].
752
+ """
753
+ assert len(shape) >= 1 and all(int(s) >= 1 for s in shape), "Invalid shape."
754
+ order = int(order)
755
+ assert order >= 1, "order must be >= 1"
756
+ btype = btype.lower()
757
+ if btype in ("low", "high"):
758
+ D0 = float(cutoff)
759
+ # assert 0.0 < D0 <= 0.5, "cutoff must be in (0, 0.5]"
760
+ else:
761
+ D1, D2 = float(cutoff[0]), float(cutoff[1])
762
+ # assert 0.0 < D1 < D2 <= 0.5, "for band types: 0 < D1 < D2 <= 0.5"
763
+ B = D2 - D1
764
+ D0 = math.sqrt(D1 * D2)
765
+
766
+ if device is None:
767
+ device = "cpu"
768
+ if dtype is None:
769
+ dtype = torch.float64
770
+
771
+ R = _radial_frequency_nd(
772
+ tuple(int(s) for s in shape), shifted=shifted, device=device, dtype=dtype
773
+ )
774
+ eps = torch.finfo(dtype).eps
775
+ # print(f"{R = }")
776
+
777
+ if btype == "low":
778
+ # H = 1 / (1 + (R/D0)^(2n))
779
+ ratio = (R / D0).clamp_min(0)
780
+ H = 1.0 / (1.0 + ratio.pow(2 * order))
781
+
782
+ elif btype == "high":
783
+ # H = 1 / (1 + (D0/R)^(2n)), H(DC)=0
784
+ # avoid divide-by-zero at R=0
785
+ safe_R = torch.where(R > 0, R, torch.tensor(1.0, device=device, dtype=dtype)) # dummy
786
+ ratio = D0 / safe_R
787
+ H = 1.0 / (1.0 + ratio.pow(2 * order))
788
+ # enforce DC = 0
789
+ H = torch.where(R > 0, H, torch.zeros_like(H))
790
+
791
+ elif btype == "bandpass":
792
+ # D = (R^2 - D0^2) / (B*R); H = 1 / (1 + |D|^(2n))
793
+ # Handle R=0 -> D=inf -> H=0
794
+ denom = B * R
795
+ D = torch.where(denom != 0, (R.pow(2) - D0**2) / denom, torch.full_like(R, float("inf")))
796
+ H = 1.0 / (1.0 + D.abs().pow(2 * order))
797
+
798
+ elif btype in ("bandstop", "stop", "bandreject"):
799
+ # D = (B*R) / (R^2 - D0^2); H = 1 / (1 + |D|^(2n))
800
+ # Handle R^2 - D0^2 = 0 -> D=inf -> H=0 (deep notch at R=D0)
801
+ denom = R.pow(2) - D0**2
802
+ D = torch.where(denom != 0, (B * R) / denom, torch.full_like(R, float("inf")))
803
+ H = 1.0 / (1.0 + D.abs().pow(2 * order))
804
+
805
+ else:
806
+ raise ValueError("btype must be 'low', 'high', 'bandpass', or 'bandstop'.")
807
+
808
+ return H
809
+
810
+
811
+ def butterworth_low_pass_filter(
812
+ tensor: torch.Tensor,
813
+ dims: Sequence[int],
814
+ cutoff: float,
815
+ order: int,
816
+ shifted: bool = False,
817
+ device=None,
818
+ dtype=None,
819
+ ):
820
+ """
821
+ Applies a Butterworth low-pass filter to the input tensor.
822
+
823
+ the dims specify which dim should be perform filtering
824
+
825
+ return filtered tensor
826
+ """
827
+ if not isinstance(dims, (list, tuple)):
828
+ dims = (dims,)
829
+ ndims_total = tensor.ndim
830
+ # Normalize dims (handle negatives)
831
+ norm_dims = _normalize_dims(dims, ndim=ndims_total)
832
+
833
+ original_dtype = tensor.dtype
834
+ work_dtype = dtype or (tensor.dtype if torch.is_floating_point(tensor) else torch.float32)
835
+ if work_dtype == torch.bfloat16 or work_dtype == torch.float16:
836
+ work_dtype = torch.float32
837
+ device = device or tensor.device
838
+
839
+ # Prepare frequency-domain representation
840
+ x = tensor.to(device=device, dtype=work_dtype)
841
+ X = torch.fft.fftn(x, dim=norm_dims)
842
+ if shifted:
843
+ X = torch.fft.fftshift(X, dim=norm_dims)
844
+
845
+ # Build isotropic Butterworth mask over the selected dims
846
+ shape_subset = [x.shape[d] for d in norm_dims]
847
+ H_small = butterworth_nd(
848
+ shape=shape_subset,
849
+ cutoff=cutoff,
850
+ order=order,
851
+ btype="low",
852
+ shifted=shifted,
853
+ device=device,
854
+ dtype=work_dtype,
855
+ )
856
+
857
+ # Broadcast mask into full tensor shape
858
+ mask_shape = [1] * ndims_total
859
+ for i, d in enumerate(norm_dims):
860
+ mask_shape[d] = shape_subset[i]
861
+ H = H_small.view(*mask_shape)
862
+
863
+ # Apply mask
864
+ X_filtered = X * H
865
+
866
+ # Inverse FFT
867
+ if shifted:
868
+ X_filtered = torch.fft.ifftshift(X_filtered, dim=norm_dims)
869
+ x_filtered = torch.fft.ifftn(X_filtered, dim=norm_dims).real
870
+
871
+ return x_filtered.to(dtype=original_dtype)
872
+
873
+
874
+ # def fft_denoise(tensor, dim, fft_ratio):
875
+ # assert len(dim) == 2
876
+ # original_dtype = tensor.dtype
877
+ # tensor = tensor.to(torch.float32)
878
+ # # Create low pass filter
879
+ # LPF = butterworth_low_pass_filter(
880
+ # (tensor.shape[dim[0]], tensor.shape[dim[1]]),
881
+ # n=4,
882
+ # d_s=fft_ratio,
883
+ # )
884
+ # LPF = LPF.to(dtype=tensor.dtype, device=tensor.device)
885
+ # # print(f"{LPF = }")
886
+ # # print(f"{LPF.shape = }")
887
+ # for _ in range(dim[0]):
888
+ # LPF = LPF.unsqueeze(0)
889
+ # for _ in range(dim[1] + 1, len(tensor.shape)):
890
+ # LPF = LPF.unsqueeze(-1)
891
+ # # print(f"{LPF.shape = }")
892
+ # # FFT
893
+ # latents_freq_k = torch.fft.fftn(tensor, dim=dim)
894
+ # # print(f"{latents_freq_k.shape = }")
895
+ # latents_freq_k = torch.fft.fftshift(latents_freq_k, dim=dim)
896
+ # # print(f"{latents_freq_k.shape = }")
897
+
898
+ # new_freq_k = latents_freq_k * LPF
899
+
900
+ # # IFFT
901
+ # new_freq_k = torch.fft.ifftshift(new_freq_k, dim=dim)
902
+ # denoised_k = torch.fft.ifftn(new_freq_k, dim=dim).real
903
+ # denoised_k = denoised_k.to(original_dtype)
904
+ # return denoised_k
905
+
906
+
907
+ if __name__ == "__main__":
908
+ # x = torch.linspace(0, 2 * np.pi, 8)
909
+ # y = torch.linspace(0, 2 * np.pi, 8)
910
+ # X, Y = torch.meshgrid(x, y, indexing='ij')
911
+ # latents = (
912
+ # torch.sin(2 * X + Y) +
913
+ # torch.sin(X + 3 * Y) +
914
+ # torch.sin(3 * X - 2 * Y)
915
+ # ) + 1
916
+ # latents += 0.01 * torch.randn_like(latents) # Add Gaussian noise
917
+ # # latents = torch.randn([8, 8])
918
+ # print(f"latents = \n{latents}")
919
+
920
+ # latents_freq = torch.fft.fftn(latents, dim=(-2, -1))
921
+ # print(f"latents_freq = \n{torch.abs(latents_freq)}")
922
+
923
+ # latents_freq_shift = torch.fft.fftshift(latents_freq, dim=(-2, -1))
924
+ # print(f"latents_freq_shift = \n{torch.abs(latents_freq_shift)}")
925
+
926
+ # latents_freq_dct = dct_2d(latents)
927
+ # print(f"latents_freq_dct = \n{latents_freq_dct}")
928
+
929
+ # LPF_1 = butterworth_low_pass_filter(latents=latents, d_s=-1.0)
930
+
931
+ # print(f"LPF_1 = \n{LPF_1}")
932
+
933
+ # LPF_2 = my_butterworth_low_pass_filter_non_center(
934
+ # shape=latents.shape,
935
+ # stop_freqs=[0.25, 0.25],
936
+ # n=4,
937
+ # )
938
+ # print(f"LPF_2 = \n{LPF_2}")
939
+
940
+ # LPF_3 = my_butterworth_low_pass_filter(
941
+ # shape=latents.shape,
942
+ # stop_freqs=[0.25, 0.25],
943
+ # n=4,
944
+ # )
945
+ # print(f"LPF_3 = \n{LPF_3}")
946
+
947
+ # img = load_grayscale_image()
948
+ # # Extract middle column as 1-D signal
949
+ # col = img.shape[1] // 2 - 1
950
+ # print(f"{col = }")
951
+ # x_np = img[:, col].astype(np.float32) # [H]
952
+ # # print(f"{x_np = }")
953
+ # N = x_np.shape[0]
954
+ # print(f"{N = }")
955
+
956
+ # device = 'cpu'
957
+ # dtype = torch.float64
958
+
959
+ # x = torch.from_numpy(img).to(device=device, dtype=dtype)
960
+ # print(f"{x = }")
961
+
962
+ # # Transforms
963
+ # Xf = torch.fft.fftn(x, dim=(-3, -2, -1), norm=None) # complex64
964
+ # print(f"{Xf = }")
965
+ # x_reconstructed = torch.fft.ifftn(Xf, dim=(-3, -2, -1), norm=None)
966
+ # print(f"{x_reconstructed = }")
967
+ # print(f"{(x - x_reconstructed).abs().max() = }")
968
+
969
+ # Xd2 = dct2_nd_fft(x, dims=(-3, -2, -1), norm="ortho") # float
970
+ # print(f"{Xd2 = }")
971
+ # x_reconstructed = idct2_nd_fft(Xd2, dims=(-1, -2, -3), norm="ortho")
972
+ # print(f"{x_reconstructed = }")
973
+ # print(f"{(x - x_reconstructed).abs().max() = }")
974
+
975
+ # H1 = butterworth_mask_1d(16, 0.125, 4, btype='low', shifted=True)
976
+ # print(f"{H1 = }")
977
+
978
+ H2 = butterworth_nd([30, 52], 1.0, 4, btype="low", shifted=True)
979
+ print(f"{H2 = }")
980
+
981
+ # ---- Planar wave demo with Butterworth low-pass filtering ----
982
+ def demo_planar_wave():
983
+ # Generate 2D planar wave: low-frequency + added high-frequency component
984
+ H, W = 128, 128
985
+ device = "cpu"
986
+ y = torch.arange(H, device=device).view(H, 1)
987
+ x = torch.arange(W, device=device).view(1, W)
988
+
989
+ # Low-frequency component
990
+ kx_low, ky_low = 2, 3
991
+ low = torch.sin(2 * math.pi * (kx_low * x / W + ky_low * y / H))
992
+
993
+ # High-frequency component
994
+ kx_high, ky_high = 20, 24
995
+ high = 0.5 * torch.sin(2 * math.pi * (kx_high * x / W + ky_high * y / H))
996
+
997
+ signal = low + high
998
+
999
+ # Apply Butterworth low-pass (cutoff chosen to keep low freq, attenuate high freq)
1000
+ cutoff = 0.12 # normalized radial cutoff (<=0.5)
1001
+ order = 4
1002
+ filtered = butterworth_low_pass_filter(
1003
+ signal, dims=(-2, -1), cutoff=cutoff, order=order, shifted=True
1004
+ )
1005
+
1006
+ # Metrics
1007
+ mse_before = (signal - low).pow(2).mean()
1008
+ mse_after = (filtered - low).pow(2).mean()
1009
+ residual_energy_ratio = (filtered - low).pow(2).sum() / (signal - low).pow(2).sum()
1010
+
1011
+ print("Planar wave demo:")
1012
+ print(f"mse_before={mse_before.item():.6e}")
1013
+ print(f"mse_after ={mse_after.item():.6e}")
1014
+ print(f"residual_energy_ratio={residual_energy_ratio.item():.4%}")
1015
+ # Quick sanity: high frequency suppression (should be << 1)
1016
+ assert (
1017
+ mse_after < mse_before
1018
+ ), "Filtering did not reduce error to low-frequency ground truth."
1019
+
1020
+ demo_planar_wave()
images/.gitkeep ADDED
File without changes
inference.py ADDED
@@ -0,0 +1,263 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ import os
3
+
4
+ os.environ["CUDA_VISIBLE_DEVICES"] = "0"
5
+ import torch
6
+ from omegaconf import OmegaConf
7
+ from tqdm import tqdm
8
+ from torchvision import transforms
9
+ from torchvision.io import write_video
10
+ from einops import rearrange
11
+ import torch.distributed as dist
12
+ from torch.utils.data import DataLoader, SequentialSampler
13
+ from torch.utils.data.distributed import DistributedSampler
14
+
15
+ from pipeline import (
16
+ CausalDiffusionInferencePipeline,
17
+ CausalInferencePipeline,
18
+ )
19
+ from utils.dataset import TextDataset, TextImagePairDataset
20
+ from utils.misc import set_seed
21
+ from hydra import initialize, compose
22
+ from hydra.core.global_hydra import GlobalHydra
23
+
24
+ from demo_utils.memory import gpu, get_cuda_free_memory_gb, DynamicSwapInstaller
25
+
26
+ from pathlib import Path
27
+
28
+ config_name = "self_forcing_dmd_vsink"
29
+ output_chunk_number = 21
30
+ output_latent_frame_number = 21
31
+ # output_latent_frame_number = 81
32
+ seed = 42
33
+ import sys
34
+
35
+ sys.argv.extend(
36
+ [
37
+ "--output_folder",
38
+ f"outputs/{output_latent_frame_number}-{config_name}-seed{seed}",
39
+ # f"outputs-test/{output_latent_frame_number}-{config_name}-seed{seed}",
40
+ "--config_dir",
41
+ "configs",
42
+ "--config_name",
43
+ config_name,
44
+ "--num_output_frames",
45
+ f"{output_latent_frame_number}",
46
+ "--data_path",
47
+ "prompts/MovieGenVideoBench_extended.txt",
48
+ "--checkpoint_path",
49
+ "./checkpoints/self_forcing_dmd.pt",
50
+ "--use_ema",
51
+ "--seed",
52
+ f"{seed}",
53
+ ]
54
+ )
55
+ print(f"{sys.argv = }")
56
+
57
+
58
+ parser = argparse.ArgumentParser()
59
+ parser.add_argument("--config_dir", type=str, help="Directory to the config file")
60
+ parser.add_argument("--config_name", type=str, help="Name to the config file")
61
+ parser.add_argument("--checkpoint_path", type=str, help="Path to the checkpoint folder")
62
+ parser.add_argument("--data_path", type=str, help="Path to the dataset")
63
+ parser.add_argument("--extended_prompt_path", type=str, help="Path to the extended prompt")
64
+ parser.add_argument("--output_folder", type=str, help="Output folder")
65
+ parser.add_argument(
66
+ "--num_output_frames",
67
+ type=int,
68
+ default=21,
69
+ help="Number of overlap frames between sliding windows",
70
+ )
71
+ parser.add_argument(
72
+ "--i2v",
73
+ action="store_true",
74
+ help="Whether to perform I2V (or T2V by default)",
75
+ )
76
+ parser.add_argument("--use_ema", action="store_true", help="Whether to use EMA parameters")
77
+ parser.add_argument("--seed", type=int, default=0, help="Random seed")
78
+ parser.add_argument(
79
+ "--num_samples",
80
+ type=int,
81
+ default=1,
82
+ help="Number of samples to generate per prompt",
83
+ )
84
+ args = parser.parse_args()
85
+
86
+ # Initialize distributed inference
87
+ if "LOCAL_RANK" in os.environ:
88
+ dist.init_process_group(backend="nccl")
89
+ local_rank = int(os.environ["LOCAL_RANK"])
90
+ torch.cuda.set_device(local_rank)
91
+ device = torch.device(f"cuda:{local_rank}")
92
+ world_size = dist.get_world_size()
93
+ set_seed(args.seed + local_rank)
94
+ else:
95
+ device = torch.device("cuda")
96
+ local_rank = 0
97
+ world_size = 1
98
+ set_seed(args.seed)
99
+
100
+ print(f"Free VRAM {get_cuda_free_memory_gb(gpu)} GB")
101
+ low_memory = get_cuda_free_memory_gb(gpu) < 40
102
+
103
+ torch.set_grad_enabled(False)
104
+
105
+ if GlobalHydra.instance().is_initialized():
106
+ GlobalHydra.instance().clear()
107
+
108
+ with initialize(version_base=None, config_path=args.config_dir):
109
+ config = compose(config_name=args.config_name)
110
+ print(f"{config = }")
111
+
112
+ # Initialize pipeline
113
+ if hasattr(config, "denoising_step_list"):
114
+ # Few-step inference
115
+ pipeline = CausalInferencePipeline(config, device=device)
116
+ else:
117
+ # Multi-step diffusion inference
118
+ pipeline = CausalDiffusionInferencePipeline(config, device=device)
119
+
120
+ if args.checkpoint_path:
121
+ state_dict = torch.load(args.checkpoint_path, map_location="cpu")
122
+ pipeline.generator.load_state_dict(
123
+ state_dict["generator" if not args.use_ema else "generator_ema"]
124
+ )
125
+
126
+ pipeline = pipeline.to(dtype=torch.bfloat16)
127
+ if low_memory:
128
+ DynamicSwapInstaller.install_model(pipeline.text_encoder, device=gpu)
129
+ else:
130
+ pipeline.text_encoder.to(device=gpu)
131
+ pipeline.generator.to(device=gpu)
132
+ pipeline.vae.to(device=gpu)
133
+
134
+
135
+ # Create dataset
136
+ if args.i2v:
137
+ assert not dist.is_initialized(), "I2V does not support distributed inference yet"
138
+ transform = transforms.Compose(
139
+ [
140
+ transforms.Resize((480, 832)),
141
+ transforms.ToTensor(),
142
+ transforms.Normalize([0.5], [0.5]),
143
+ ]
144
+ )
145
+ dataset = TextImagePairDataset(args.data_path, transform=transform)
146
+ else:
147
+ dataset = TextDataset(
148
+ prompt_path=args.data_path,
149
+ extended_prompt_path=args.extended_prompt_path,
150
+ )
151
+ num_prompts = len(dataset)
152
+ print(f"Number of prompts: {num_prompts}")
153
+
154
+ if dist.is_initialized():
155
+ sampler = DistributedSampler(dataset, shuffle=False, drop_last=True)
156
+ else:
157
+ sampler = SequentialSampler(dataset)
158
+ dataloader = DataLoader(dataset, batch_size=1, sampler=sampler, num_workers=0, drop_last=False)
159
+
160
+ # Create output directory (only on main process to avoid race conditions)
161
+ if local_rank == 0:
162
+ os.makedirs(args.output_folder, exist_ok=True)
163
+
164
+ if dist.is_initialized():
165
+ dist.barrier()
166
+
167
+
168
+ def encode(self, videos: torch.Tensor) -> torch.Tensor:
169
+ device, dtype = videos[0].device, videos[0].dtype
170
+ scale = [
171
+ self.mean.to(device=device, dtype=dtype),
172
+ 1.0 / self.std.to(device=device, dtype=dtype),
173
+ ]
174
+ output = [self.model.encode(u.unsqueeze(0), scale).float().squeeze(0) for u in videos]
175
+
176
+ output = torch.stack(output, dim=0)
177
+ return output
178
+
179
+
180
+ for i, batch_data in tqdm(enumerate(dataloader), disable=(local_rank != 0)):
181
+ idx = batch_data["idx"].item()
182
+
183
+ # For DataLoader batch_size=1, the batch_data is already a single item, but in a batch container
184
+ # Unpack the batch data for convenience
185
+ if isinstance(batch_data, dict):
186
+ batch = batch_data
187
+ elif isinstance(batch_data, list):
188
+ batch = batch_data[0] # First (and only) item in the batch
189
+
190
+ all_video = []
191
+ num_generated_frames = 0 # Number of generated (latent) frames
192
+
193
+ set_seed(args.seed)
194
+ if args.i2v:
195
+ # For image-to-video, batch contains image and caption
196
+ prompt = batch["prompts"][0] # Get caption from batch
197
+ prompts = [prompt] * args.num_samples
198
+
199
+ # Process the image
200
+ image = (
201
+ batch["image"]
202
+ .squeeze(0)
203
+ .unsqueeze(0)
204
+ .unsqueeze(2)
205
+ .to(device=device, dtype=torch.bfloat16)
206
+ )
207
+
208
+ # Encode the input image as the first latent
209
+ initial_latent = pipeline.vae.encode_to_latent(image).to(
210
+ device=device, dtype=torch.bfloat16
211
+ )
212
+ initial_latent = initial_latent.repeat(args.num_samples, 1, 1, 1, 1)
213
+
214
+ sampled_noise = torch.randn(
215
+ [args.num_samples, args.num_output_frames - 1, 16, 60, 104],
216
+ device=device,
217
+ dtype=torch.bfloat16,
218
+ )
219
+ else:
220
+ # For text-to-video, batch is just the text prompt
221
+ prompt = batch["prompts"][0]
222
+ extended_prompt = batch["extended_prompts"][0] if "extended_prompts" in batch else None
223
+ if extended_prompt is not None:
224
+ prompts = [extended_prompt] * args.num_samples
225
+ else:
226
+ prompts = [prompt] * args.num_samples
227
+ initial_latent = None
228
+
229
+ sampled_noise = torch.randn(
230
+ [args.num_samples, args.num_output_frames, 16, 60, 104],
231
+ device=device,
232
+ dtype=torch.bfloat16,
233
+ )
234
+
235
+ set_seed(args.seed)
236
+ # Generate 81 frames
237
+ video, latents = pipeline.inference(
238
+ noise=sampled_noise,
239
+ text_prompts=prompts,
240
+ return_latents=True,
241
+ initial_latent=initial_latent,
242
+ low_memory=low_memory,
243
+ )
244
+ current_video = rearrange(video, "b t c h w -> b t h w c").cpu()
245
+ all_video.append(current_video)
246
+ num_generated_frames += latents.shape[1]
247
+
248
+ # Final output video
249
+ video = 255.0 * torch.cat(all_video, dim=1)
250
+
251
+ # Clear VAE cache
252
+ pipeline.vae.model.clear_cache()
253
+
254
+ # Save the video if the current prompt is not a dummy prompt
255
+ if idx < num_prompts:
256
+ model = "regular" if not args.use_ema else "ema"
257
+ for seed_idx in range(args.num_samples):
258
+ # All processes save their videos
259
+ output_path = os.path.join(
260
+ args.output_folder,
261
+ f"{idx}-{prompt[:50].replace(' ', '_')}-{seed_idx}_{model}.mp4",
262
+ )
263
+ write_video(output_path, video[seed_idx], fps=16)
model/__init__.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .diffusion import CausalDiffusion
2
+ from .causvid import CausVid
3
+ from .dmd import DMD
4
+ from .gan import GAN
5
+ from .sid import SiD
6
+ from .ode_regression import ODERegression
7
+
8
+ __all__ = [
9
+ "CausalDiffusion",
10
+ "CausVid",
11
+ "DMD",
12
+ "GAN",
13
+ "SiD",
14
+ "ODERegression",
15
+ ]
model/base.py ADDED
@@ -0,0 +1,256 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Tuple
2
+ from einops import rearrange
3
+ from torch import nn
4
+ import torch.distributed as dist
5
+ import torch
6
+
7
+ from pipeline import SelfForcingTrainingPipeline
8
+ from utils.loss import get_denoising_loss
9
+ from utils.wan_wrapper import WanDiffusionWrapper, WanTextEncoder, WanVAEWrapper
10
+
11
+
12
+ class BaseModel(nn.Module):
13
+ def __init__(
14
+ self,
15
+ args,
16
+ device,
17
+ ):
18
+ super().__init__()
19
+ self._initialize_models(args, device)
20
+
21
+ self.device = device
22
+ self.args = args
23
+ self.dtype = torch.bfloat16 if args.mixed_precision else torch.float32
24
+ if hasattr(args, "denoising_step_list"):
25
+ self.denoising_step_list = torch.tensor(args.denoising_step_list, dtype=torch.long)
26
+ if args.warp_denoising_step:
27
+ timesteps = torch.cat(
28
+ (
29
+ self.scheduler.timesteps.cpu(),
30
+ torch.tensor([0], dtype=torch.float32),
31
+ )
32
+ )
33
+ self.denoising_step_list = timesteps[1000 - self.denoising_step_list]
34
+
35
+ def _initialize_models(self, args, device):
36
+ self.real_model_name = getattr(args, "real_name", "Wan2.1-T2V-1.3B")
37
+ self.fake_model_name = getattr(args, "fake_name", "Wan2.1-T2V-1.3B")
38
+
39
+ self.generator = WanDiffusionWrapper(**getattr(args, "model_kwargs", {}), is_causal=True)
40
+ self.generator.model.requires_grad_(True)
41
+
42
+ self.real_score = WanDiffusionWrapper(model_name=self.real_model_name, is_causal=False)
43
+ self.real_score.model.requires_grad_(False)
44
+
45
+ self.fake_score = WanDiffusionWrapper(model_name=self.fake_model_name, is_causal=False)
46
+ self.fake_score.model.requires_grad_(True)
47
+
48
+ self.text_encoder = WanTextEncoder()
49
+ self.text_encoder.requires_grad_(False)
50
+
51
+ self.vae = WanVAEWrapper()
52
+ self.vae.requires_grad_(False)
53
+
54
+ self.scheduler = self.generator.get_scheduler()
55
+ self.scheduler.timesteps = self.scheduler.timesteps.to(device)
56
+
57
+ def _get_timestep(
58
+ self,
59
+ min_timestep: int,
60
+ max_timestep: int,
61
+ batch_size: int,
62
+ num_frame: int,
63
+ num_frame_per_block: int,
64
+ uniform_timestep: bool = False,
65
+ ) -> torch.Tensor:
66
+ """
67
+ Randomly generate a timestep tensor based on the generator's task type. It uniformly samples a timestep
68
+ from the range [min_timestep, max_timestep], and returns a tensor of shape [batch_size, num_frame].
69
+ - If uniform_timestep, it will use the same timestep for all frames.
70
+ - If not uniform_timestep, it will use a different timestep for each block.
71
+ """
72
+ if uniform_timestep:
73
+ timestep = torch.randint(
74
+ min_timestep,
75
+ max_timestep,
76
+ [batch_size, 1],
77
+ device=self.device,
78
+ dtype=torch.long,
79
+ ).repeat(1, num_frame)
80
+ return timestep
81
+ else:
82
+ timestep = torch.randint(
83
+ min_timestep,
84
+ max_timestep,
85
+ [batch_size, num_frame],
86
+ device=self.device,
87
+ dtype=torch.long,
88
+ )
89
+ # make the noise level the same within every block
90
+ if self.independent_first_frame:
91
+ # the first frame is always kept the same
92
+ timestep_from_second = timestep[:, 1:]
93
+ timestep_from_second = timestep_from_second.reshape(
94
+ timestep_from_second.shape[0], -1, num_frame_per_block
95
+ )
96
+ timestep_from_second[:, :, 1:] = timestep_from_second[:, :, 0:1]
97
+ timestep_from_second = timestep_from_second.reshape(
98
+ timestep_from_second.shape[0], -1
99
+ )
100
+ timestep = torch.cat([timestep[:, 0:1], timestep_from_second], dim=1)
101
+ else:
102
+ timestep = timestep.reshape(timestep.shape[0], -1, num_frame_per_block)
103
+ timestep[:, :, 1:] = timestep[:, :, 0:1]
104
+ timestep = timestep.reshape(timestep.shape[0], -1)
105
+ return timestep
106
+
107
+
108
+ class SelfForcingModel(BaseModel):
109
+ def __init__(
110
+ self,
111
+ args,
112
+ device,
113
+ ):
114
+ super().__init__(args, device)
115
+ self.denoising_loss_func = get_denoising_loss(args.denoising_loss_type)()
116
+
117
+ def _run_generator(
118
+ self,
119
+ image_or_video_shape,
120
+ conditional_dict: dict,
121
+ initial_latent: torch.tensor = None,
122
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
123
+ """
124
+ Optionally simulate the generator's input from noise using backward simulation
125
+ and then run the generator for one-step.
126
+ Input:
127
+ - image_or_video_shape: a list containing the shape of the image or video [B, F, C, H, W].
128
+ - conditional_dict: a dictionary containing the conditional information (e.g. text embeddings, image embeddings).
129
+ - unconditional_dict: a dictionary containing the unconditional information (e.g. null/negative text embeddings, null/negative image embeddings).
130
+ - clean_latent: a tensor containing the clean latents [B, F, C, H, W]. Need to be passed when no backward simulation is used.
131
+ - initial_latent: a tensor containing the initial latents [B, F, C, H, W].
132
+ Output:
133
+ - pred_image: a tensor with shape [B, F, C, H, W].
134
+ - denoised_timestep: an integer
135
+ """
136
+ # Step 1: Sample noise and backward simulate the generator's input
137
+ assert getattr(
138
+ self.args, "backward_simulation", True
139
+ ), "Backward simulation needs to be enabled"
140
+ if initial_latent is not None:
141
+ conditional_dict["initial_latent"] = initial_latent
142
+ if self.args.i2v:
143
+ noise_shape = [
144
+ image_or_video_shape[0],
145
+ image_or_video_shape[1] - 1,
146
+ *image_or_video_shape[2:],
147
+ ]
148
+ else:
149
+ noise_shape = image_or_video_shape.copy()
150
+
151
+ # During training, the number of generated frames should be uniformly sampled from
152
+ # [21, self.num_training_frames], but still being a multiple of self.num_frame_per_block
153
+ min_num_frames = 20 if self.args.independent_first_frame else 21
154
+ max_num_frames = (
155
+ self.num_training_frames - 1
156
+ if self.args.independent_first_frame
157
+ else self.num_training_frames
158
+ )
159
+ assert max_num_frames % self.num_frame_per_block == 0
160
+ assert min_num_frames % self.num_frame_per_block == 0
161
+ max_num_blocks = max_num_frames // self.num_frame_per_block
162
+ min_num_blocks = min_num_frames // self.num_frame_per_block
163
+ num_generated_blocks = torch.randint(
164
+ min_num_blocks, max_num_blocks + 1, (1,), device=self.device
165
+ )
166
+ dist.broadcast(num_generated_blocks, src=0)
167
+ num_generated_blocks = num_generated_blocks.item()
168
+ num_generated_frames = num_generated_blocks * self.num_frame_per_block
169
+ if self.args.independent_first_frame and initial_latent is None:
170
+ num_generated_frames += 1
171
+ min_num_frames += 1
172
+ # Sync num_generated_frames across all processes
173
+ noise_shape[1] = num_generated_frames
174
+
175
+ pred_image_or_video, denoised_timestep_from, denoised_timestep_to = (
176
+ self._consistency_backward_simulation(
177
+ noise=torch.randn(noise_shape, device=self.device, dtype=self.dtype),
178
+ **conditional_dict,
179
+ )
180
+ )
181
+ # Slice last 21 frames
182
+ if pred_image_or_video.shape[1] > 21:
183
+ with torch.no_grad():
184
+ # Reencode to get image latent
185
+ latent_to_decode = pred_image_or_video[:, :-20, ...]
186
+ # Deccode to video
187
+ pixels = self.vae.decode_to_pixel(latent_to_decode)
188
+ frame = pixels[:, -1:, ...].to(self.dtype)
189
+ frame = rearrange(frame, "b t c h w -> b c t h w")
190
+ # Encode frame to get image latent
191
+ image_latent = self.vae.encode_to_latent(frame).to(self.dtype)
192
+ pred_image_or_video_last_21 = torch.cat(
193
+ [image_latent, pred_image_or_video[:, -20:, ...]], dim=1
194
+ )
195
+ else:
196
+ pred_image_or_video_last_21 = pred_image_or_video
197
+
198
+ if num_generated_frames != min_num_frames:
199
+ # Currently, we do not use gradient for the first chunk, since it contains image latents
200
+ gradient_mask = torch.ones_like(pred_image_or_video_last_21, dtype=torch.bool)
201
+ if self.args.independent_first_frame:
202
+ gradient_mask[:, :1] = False
203
+ else:
204
+ gradient_mask[:, : self.num_frame_per_block] = False
205
+ else:
206
+ gradient_mask = None
207
+
208
+ pred_image_or_video_last_21 = pred_image_or_video_last_21.to(self.dtype)
209
+ return (
210
+ pred_image_or_video_last_21,
211
+ gradient_mask,
212
+ denoised_timestep_from,
213
+ denoised_timestep_to,
214
+ )
215
+
216
+ def _consistency_backward_simulation(
217
+ self,
218
+ noise: torch.Tensor,
219
+ **conditional_dict: dict,
220
+ ) -> torch.Tensor:
221
+ """
222
+ Simulate the generator's input from noise to avoid training/inference mismatch.
223
+ See Sec 4.5 of the DMD2 paper (https://arxiv.org/abs/2405.14867) for details.
224
+ Here we use the consistency sampler (https://arxiv.org/abs/2303.01469)
225
+ Input:
226
+ - noise: a tensor sampled from N(0, 1) with shape [B, F, C, H, W] where the number of frame is 1 for images.
227
+ - conditional_dict: a dictionary containing the conditional information (e.g. text embeddings, image embeddings).
228
+ Output:
229
+ - output: a tensor with shape [B, T, F, C, H, W].
230
+ T is the total number of timesteps. output[0] is a pure noise and output[i] and i>0
231
+ represents the x0 prediction at each timestep.
232
+ """
233
+ if self.inference_pipeline is None:
234
+ self._initialize_inference_pipeline()
235
+
236
+ return self.inference_pipeline.inference_with_trajectory(noise=noise, **conditional_dict)
237
+
238
+ def _initialize_inference_pipeline(
239
+ self,
240
+ ):
241
+ """
242
+ Lazy initialize the inference pipeline during the first backward simulation run.
243
+ Here we encapsulate the inference code with a model-dependent outside function.
244
+ We pass our FSDP-wrapped modules into the pipeline to save memory.
245
+ """
246
+ self.inference_pipeline = SelfForcingTrainingPipeline(
247
+ denoising_step_list=self.denoising_step_list,
248
+ scheduler=self.scheduler,
249
+ generator=self.generator,
250
+ num_frame_per_block=self.num_frame_per_block,
251
+ independent_first_frame=self.args.independent_first_frame,
252
+ same_step_across_blocks=self.args.same_step_across_blocks,
253
+ last_step_only=self.args.last_step_only,
254
+ num_max_frames=self.num_training_frames,
255
+ context_noise=self.args.context_noise,
256
+ )
model/causvid.py ADDED
@@ -0,0 +1,417 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch.nn.functional as F
2
+ from typing import Tuple
3
+ import torch
4
+
5
+ from model.base import BaseModel
6
+
7
+
8
+ class CausVid(BaseModel):
9
+ def __init__(
10
+ self,
11
+ args,
12
+ device,
13
+ ):
14
+ """
15
+ Initialize the DMD (Distribution Matching Distillation) module.
16
+ This class is self-contained and compute generator and fake score losses
17
+ in the forward pass.
18
+ """
19
+ super().__init__(args, device)
20
+ self.num_frame_per_block = getattr(args, "num_frame_per_block", 1)
21
+ self.num_training_frames = getattr(args, "num_training_frames", 21)
22
+
23
+ if self.num_frame_per_block > 1:
24
+ self.generator.model.num_frame_per_block = self.num_frame_per_block
25
+
26
+ self.independent_first_frame = getattr(args, "independent_first_frame", False)
27
+ if self.independent_first_frame:
28
+ self.generator.model.independent_first_frame = True
29
+ if args.gradient_checkpointing:
30
+ self.generator.enable_gradient_checkpointing()
31
+ self.fake_score.enable_gradient_checkpointing()
32
+
33
+ # Step 2: Initialize all dmd hyperparameters
34
+ self.num_train_timestep = args.num_train_timestep
35
+ self.min_step = int(0.02 * self.num_train_timestep)
36
+ self.max_step = int(0.98 * self.num_train_timestep)
37
+ if hasattr(args, "real_guidance_scale"):
38
+ self.real_guidance_scale = args.real_guidance_scale
39
+ self.fake_guidance_scale = args.fake_guidance_scale
40
+ else:
41
+ self.real_guidance_scale = args.guidance_scale
42
+ self.fake_guidance_scale = 0.0
43
+ self.timestep_shift = getattr(args, "timestep_shift", 1.0)
44
+ self.teacher_forcing = getattr(args, "teacher_forcing", False)
45
+
46
+ if getattr(self.scheduler, "alphas_cumprod", None) is not None:
47
+ self.scheduler.alphas_cumprod = self.scheduler.alphas_cumprod.to(device)
48
+ else:
49
+ self.scheduler.alphas_cumprod = None
50
+
51
+ def _compute_kl_grad(
52
+ self,
53
+ noisy_image_or_video: torch.Tensor,
54
+ estimated_clean_image_or_video: torch.Tensor,
55
+ timestep: torch.Tensor,
56
+ conditional_dict: dict,
57
+ unconditional_dict: dict,
58
+ normalization: bool = True,
59
+ ) -> Tuple[torch.Tensor, dict]:
60
+ """
61
+ Compute the KL grad (eq 7 in https://arxiv.org/abs/2311.18828).
62
+ Input:
63
+ - noisy_image_or_video: a tensor with shape [B, F, C, H, W] where the number of frame is 1 for images.
64
+ - estimated_clean_image_or_video: a tensor with shape [B, F, C, H, W] representing the estimated clean image or video.
65
+ - timestep: a tensor with shape [B, F] containing the randomly generated timestep.
66
+ - conditional_dict: a dictionary containing the conditional information (e.g. text embeddings, image embeddings).
67
+ - unconditional_dict: a dictionary containing the unconditional information (e.g. null/negative text embeddings, null/negative image embeddings).
68
+ - normalization: a boolean indicating whether to normalize the gradient.
69
+ Output:
70
+ - kl_grad: a tensor representing the KL grad.
71
+ - kl_log_dict: a dictionary containing the intermediate tensors for logging.
72
+ """
73
+ # Step 1: Compute the fake score
74
+ _, pred_fake_image_cond = self.fake_score(
75
+ noisy_image_or_video=noisy_image_or_video,
76
+ conditional_dict=conditional_dict,
77
+ timestep=timestep,
78
+ )
79
+
80
+ if self.fake_guidance_scale != 0.0:
81
+ _, pred_fake_image_uncond = self.fake_score(
82
+ noisy_image_or_video=noisy_image_or_video,
83
+ conditional_dict=unconditional_dict,
84
+ timestep=timestep,
85
+ )
86
+ pred_fake_image = (
87
+ pred_fake_image_cond
88
+ + (pred_fake_image_cond - pred_fake_image_uncond) * self.fake_guidance_scale
89
+ )
90
+ else:
91
+ pred_fake_image = pred_fake_image_cond
92
+
93
+ # Step 2: Compute the real score
94
+ # We compute the conditional and unconditional prediction
95
+ # and add them together to achieve cfg (https://arxiv.org/abs/2207.12598)
96
+ _, pred_real_image_cond = self.real_score(
97
+ noisy_image_or_video=noisy_image_or_video,
98
+ conditional_dict=conditional_dict,
99
+ timestep=timestep,
100
+ )
101
+
102
+ _, pred_real_image_uncond = self.real_score(
103
+ noisy_image_or_video=noisy_image_or_video,
104
+ conditional_dict=unconditional_dict,
105
+ timestep=timestep,
106
+ )
107
+
108
+ pred_real_image = (
109
+ pred_real_image_cond
110
+ + (pred_real_image_cond - pred_real_image_uncond) * self.real_guidance_scale
111
+ )
112
+
113
+ # Step 3: Compute the DMD gradient (DMD paper eq. 7).
114
+ grad = pred_fake_image - pred_real_image
115
+
116
+ # TODO: Change the normalizer for causal teacher
117
+ if normalization:
118
+ # Step 4: Gradient normalization (DMD paper eq. 8).
119
+ p_real = estimated_clean_image_or_video - pred_real_image
120
+ normalizer = torch.abs(p_real).mean(dim=[1, 2, 3, 4], keepdim=True)
121
+ grad = grad / normalizer
122
+ grad = torch.nan_to_num(grad)
123
+
124
+ return grad, {
125
+ "dmdtrain_gradient_norm": torch.mean(torch.abs(grad)).detach(),
126
+ "timestep": timestep.detach(),
127
+ }
128
+
129
+ def compute_distribution_matching_loss(
130
+ self,
131
+ image_or_video: torch.Tensor,
132
+ conditional_dict: dict,
133
+ unconditional_dict: dict,
134
+ gradient_mask: torch.Tensor = None,
135
+ ) -> Tuple[torch.Tensor, dict]:
136
+ """
137
+ Compute the DMD loss (eq 7 in https://arxiv.org/abs/2311.18828).
138
+ Input:
139
+ - image_or_video: a tensor with shape [B, F, C, H, W] where the number of frame is 1 for images.
140
+ - conditional_dict: a dictionary containing the conditional information (e.g. text embeddings, image embeddings).
141
+ - unconditional_dict: a dictionary containing the unconditional information (e.g. null/negative text embeddings, null/negative image embeddings).
142
+ - gradient_mask: a boolean tensor with the same shape as image_or_video indicating which pixels to compute loss .
143
+ Output:
144
+ - dmd_loss: a scalar tensor representing the DMD loss.
145
+ - dmd_log_dict: a dictionary containing the intermediate tensors for logging.
146
+ """
147
+ original_latent = image_or_video
148
+
149
+ batch_size, num_frame = image_or_video.shape[:2]
150
+
151
+ with torch.no_grad():
152
+ # Step 1: Randomly sample timestep based on the given schedule and corresponding noise
153
+ timestep = self._get_timestep(
154
+ 0,
155
+ self.num_train_timestep,
156
+ batch_size,
157
+ num_frame,
158
+ self.num_frame_per_block,
159
+ uniform_timestep=True,
160
+ )
161
+
162
+ if self.timestep_shift > 1:
163
+ timestep = (
164
+ self.timestep_shift
165
+ * (timestep / 1000)
166
+ / (1 + (self.timestep_shift - 1) * (timestep / 1000))
167
+ * 1000
168
+ )
169
+ timestep = timestep.clamp(self.min_step, self.max_step)
170
+
171
+ noise = torch.randn_like(image_or_video)
172
+ noisy_latent = (
173
+ self.scheduler.add_noise(
174
+ image_or_video.flatten(0, 1),
175
+ noise.flatten(0, 1),
176
+ timestep.flatten(0, 1),
177
+ )
178
+ .detach()
179
+ .unflatten(0, (batch_size, num_frame))
180
+ )
181
+
182
+ # Step 2: Compute the KL grad
183
+ grad, dmd_log_dict = self._compute_kl_grad(
184
+ noisy_image_or_video=noisy_latent,
185
+ estimated_clean_image_or_video=original_latent,
186
+ timestep=timestep,
187
+ conditional_dict=conditional_dict,
188
+ unconditional_dict=unconditional_dict,
189
+ )
190
+
191
+ if gradient_mask is not None:
192
+ dmd_loss = 0.5 * F.mse_loss(
193
+ original_latent.double()[gradient_mask],
194
+ (original_latent.double() - grad.double()).detach()[gradient_mask],
195
+ reduction="mean",
196
+ )
197
+ else:
198
+ dmd_loss = 0.5 * F.mse_loss(
199
+ original_latent.double(),
200
+ (original_latent.double() - grad.double()).detach(),
201
+ reduction="mean",
202
+ )
203
+ return dmd_loss, dmd_log_dict
204
+
205
+ def _run_generator(
206
+ self,
207
+ image_or_video_shape,
208
+ conditional_dict: dict,
209
+ clean_latent: torch.tensor,
210
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
211
+ """
212
+ Optionally simulate the generator's input from noise using backward simulation
213
+ and then run the generator for one-step.
214
+ Input:
215
+ - image_or_video_shape: a list containing the shape of the image or video [B, F, C, H, W].
216
+ - conditional_dict: a dictionary containing the conditional information (e.g. text embeddings, image embeddings).
217
+ - unconditional_dict: a dictionary containing the unconditional information (e.g. null/negative text embeddings, null/negative image embeddings).
218
+ - clean_latent: a tensor containing the clean latents [B, F, C, H, W]. Need to be passed when no backward simulation is used.
219
+ - initial_latent: a tensor containing the initial latents [B, F, C, H, W].
220
+ Output:
221
+ - pred_image: a tensor with shape [B, F, C, H, W].
222
+ """
223
+ simulated_noisy_input = []
224
+ for timestep in self.denoising_step_list:
225
+ noise = torch.randn(image_or_video_shape, device=self.device, dtype=self.dtype)
226
+
227
+ noisy_timestep = timestep * torch.ones(
228
+ image_or_video_shape[:2], device=self.device, dtype=torch.long
229
+ )
230
+
231
+ if timestep != 0:
232
+ noisy_image = self.scheduler.add_noise(
233
+ clean_latent.flatten(0, 1),
234
+ noise.flatten(0, 1),
235
+ noisy_timestep.flatten(0, 1),
236
+ ).unflatten(0, image_or_video_shape[:2])
237
+ else:
238
+ noisy_image = clean_latent
239
+
240
+ simulated_noisy_input.append(noisy_image)
241
+
242
+ simulated_noisy_input = torch.stack(simulated_noisy_input, dim=1)
243
+
244
+ # Step 2: Randomly sample a timestep and pick the corresponding input
245
+ index = self._get_timestep(
246
+ 0,
247
+ len(self.denoising_step_list),
248
+ image_or_video_shape[0],
249
+ image_or_video_shape[1],
250
+ self.num_frame_per_block,
251
+ uniform_timestep=False,
252
+ )
253
+
254
+ # select the corresponding timestep's noisy input from the stacked tensor [B, T, F, C, H, W]
255
+ noisy_input = torch.gather(
256
+ simulated_noisy_input,
257
+ dim=1,
258
+ index=index.reshape(index.shape[0], 1, index.shape[1], 1, 1, 1)
259
+ .expand(-1, -1, -1, *image_or_video_shape[2:])
260
+ .to(self.device),
261
+ ).squeeze(1)
262
+
263
+ timestep = self.denoising_step_list[index].to(self.device)
264
+
265
+ _, pred_image_or_video = self.generator(
266
+ noisy_image_or_video=noisy_input,
267
+ conditional_dict=conditional_dict,
268
+ timestep=timestep,
269
+ clean_x=clean_latent if self.teacher_forcing else None,
270
+ )
271
+
272
+ gradient_mask = None # timestep != 0
273
+
274
+ pred_image_or_video = pred_image_or_video.type_as(noisy_input)
275
+
276
+ return pred_image_or_video, gradient_mask
277
+
278
+ def generator_loss(
279
+ self,
280
+ image_or_video_shape,
281
+ conditional_dict: dict,
282
+ unconditional_dict: dict,
283
+ clean_latent: torch.Tensor,
284
+ initial_latent: torch.Tensor = None,
285
+ ) -> Tuple[torch.Tensor, dict]:
286
+ """
287
+ Generate image/videos from noise and compute the DMD loss.
288
+ The noisy input to the generator is backward simulated.
289
+ This removes the need of any datasets during distillation.
290
+ See Sec 4.5 of the DMD2 paper (https://arxiv.org/abs/2405.14867) for details.
291
+ Input:
292
+ - image_or_video_shape: a list containing the shape of the image or video [B, F, C, H, W].
293
+ - conditional_dict: a dictionary containing the conditional information (e.g. text embeddings, image embeddings).
294
+ - unconditional_dict: a dictionary containing the unconditional information (e.g. null/negative text embeddings, null/negative image embeddings).
295
+ - clean_latent: a tensor containing the clean latents [B, F, C, H, W]. Need to be passed when no backward simulation is used.
296
+ Output:
297
+ - loss: a scalar tensor representing the generator loss.
298
+ - generator_log_dict: a dictionary containing the intermediate tensors for logging.
299
+ """
300
+ # Step 1: Run generator on backward simulated noisy input
301
+ pred_image, gradient_mask = self._run_generator(
302
+ image_or_video_shape=image_or_video_shape,
303
+ conditional_dict=conditional_dict,
304
+ clean_latent=clean_latent,
305
+ )
306
+
307
+ # Step 2: Compute the DMD loss
308
+ dmd_loss, dmd_log_dict = self.compute_distribution_matching_loss(
309
+ image_or_video=pred_image,
310
+ conditional_dict=conditional_dict,
311
+ unconditional_dict=unconditional_dict,
312
+ gradient_mask=gradient_mask,
313
+ )
314
+
315
+ # Step 3: TODO: Implement the GAN loss
316
+
317
+ return dmd_loss, dmd_log_dict
318
+
319
+ def critic_loss(
320
+ self,
321
+ image_or_video_shape,
322
+ conditional_dict: dict,
323
+ unconditional_dict: dict,
324
+ clean_latent: torch.Tensor,
325
+ initial_latent: torch.Tensor = None,
326
+ ) -> Tuple[torch.Tensor, dict]:
327
+ """
328
+ Generate image/videos from noise and train the critic with generated samples.
329
+ The noisy input to the generator is backward simulated.
330
+ This removes the need of any datasets during distillation.
331
+ See Sec 4.5 of the DMD2 paper (https://arxiv.org/abs/2405.14867) for details.
332
+ Input:
333
+ - image_or_video_shape: a list containing the shape of the image or video [B, F, C, H, W].
334
+ - conditional_dict: a dictionary containing the conditional information (e.g. text embeddings, image embeddings).
335
+ - unconditional_dict: a dictionary containing the unconditional information (e.g. null/negative text embeddings, null/negative image embeddings).
336
+ - clean_latent: a tensor containing the clean latents [B, F, C, H, W]. Need to be passed when no backward simulation is used.
337
+ Output:
338
+ - loss: a scalar tensor representing the generator loss.
339
+ - critic_log_dict: a dictionary containing the intermediate tensors for logging.
340
+ """
341
+
342
+ # Step 1: Run generator on backward simulated noisy input
343
+ with torch.no_grad():
344
+ generated_image, _ = self._run_generator(
345
+ image_or_video_shape=image_or_video_shape,
346
+ conditional_dict=conditional_dict,
347
+ clean_latent=clean_latent,
348
+ )
349
+
350
+ # Step 2: Compute the fake prediction
351
+ critic_timestep = self._get_timestep(
352
+ 0,
353
+ self.num_train_timestep,
354
+ image_or_video_shape[0],
355
+ image_or_video_shape[1],
356
+ self.num_frame_per_block,
357
+ uniform_timestep=True,
358
+ )
359
+
360
+ if self.timestep_shift > 1:
361
+ critic_timestep = (
362
+ self.timestep_shift
363
+ * (critic_timestep / 1000)
364
+ / (1 + (self.timestep_shift - 1) * (critic_timestep / 1000))
365
+ * 1000
366
+ )
367
+
368
+ critic_timestep = critic_timestep.clamp(self.min_step, self.max_step)
369
+
370
+ critic_noise = torch.randn_like(generated_image)
371
+ noisy_generated_image = self.scheduler.add_noise(
372
+ generated_image.flatten(0, 1),
373
+ critic_noise.flatten(0, 1),
374
+ critic_timestep.flatten(0, 1),
375
+ ).unflatten(0, image_or_video_shape[:2])
376
+
377
+ _, pred_fake_image = self.fake_score(
378
+ noisy_image_or_video=noisy_generated_image,
379
+ conditional_dict=conditional_dict,
380
+ timestep=critic_timestep,
381
+ )
382
+
383
+ # Step 3: Compute the denoising loss for the fake critic
384
+ if self.args.denoising_loss_type == "flow":
385
+ from utils.wan_wrapper import WanDiffusionWrapper
386
+
387
+ flow_pred = WanDiffusionWrapper._convert_x0_to_flow_pred(
388
+ scheduler=self.scheduler,
389
+ x0_pred=pred_fake_image.flatten(0, 1),
390
+ xt=noisy_generated_image.flatten(0, 1),
391
+ timestep=critic_timestep.flatten(0, 1),
392
+ )
393
+ pred_fake_noise = None
394
+ else:
395
+ flow_pred = None
396
+ pred_fake_noise = self.scheduler.convert_x0_to_noise(
397
+ x0=pred_fake_image.flatten(0, 1),
398
+ xt=noisy_generated_image.flatten(0, 1),
399
+ timestep=critic_timestep.flatten(0, 1),
400
+ ).unflatten(0, image_or_video_shape[:2])
401
+
402
+ denoising_loss = self.denoising_loss_func(
403
+ x=generated_image.flatten(0, 1),
404
+ x_pred=pred_fake_image.flatten(0, 1),
405
+ noise=critic_noise.flatten(0, 1),
406
+ noise_pred=pred_fake_noise,
407
+ alphas_cumprod=self.scheduler.alphas_cumprod,
408
+ timestep=critic_timestep.flatten(0, 1),
409
+ flow_pred=flow_pred,
410
+ )
411
+
412
+ # Step 4: TODO: Compute the GAN loss
413
+
414
+ # Step 5: Debugging Log
415
+ critic_log_dict = {"critic_timestep": critic_timestep.detach()}
416
+
417
+ return denoising_loss, critic_log_dict
model/diffusion.py ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Tuple
2
+ import torch
3
+
4
+ from model.base import BaseModel
5
+ from utils.wan_wrapper import WanDiffusionWrapper, WanTextEncoder, WanVAEWrapper
6
+
7
+
8
+ class CausalDiffusion(BaseModel):
9
+ def __init__(
10
+ self,
11
+ args,
12
+ device,
13
+ ):
14
+ """
15
+ Initialize the Diffusion loss module.
16
+ """
17
+ super().__init__(args, device)
18
+ self.num_frame_per_block = getattr(args, "num_frame_per_block", 1)
19
+ if self.num_frame_per_block > 1:
20
+ self.generator.model.num_frame_per_block = self.num_frame_per_block
21
+ self.independent_first_frame = getattr(args, "independent_first_frame", False)
22
+ if self.independent_first_frame:
23
+ self.generator.model.independent_first_frame = True
24
+
25
+ if args.gradient_checkpointing:
26
+ self.generator.enable_gradient_checkpointing()
27
+
28
+ # Step 2: Initialize all hyperparameters
29
+ self.num_train_timestep = args.num_train_timestep
30
+ self.min_step = int(0.02 * self.num_train_timestep)
31
+ self.max_step = int(0.98 * self.num_train_timestep)
32
+ self.guidance_scale = args.guidance_scale
33
+ self.timestep_shift = getattr(args, "timestep_shift", 1.0)
34
+ self.teacher_forcing = getattr(args, "teacher_forcing", False)
35
+ # Noise augmentation in teacher forcing, we add small noise to clean context latents
36
+ self.noise_augmentation_max_timestep = getattr(args, "noise_augmentation_max_timestep", 0)
37
+
38
+ def _initialize_models(self, args):
39
+ self.generator = WanDiffusionWrapper(
40
+ **getattr(args, "model_kwargs", {}),
41
+ is_causal=True,
42
+ )
43
+ self.generator.model.requires_grad_(True)
44
+
45
+ self.text_encoder = WanTextEncoder()
46
+ self.text_encoder.requires_grad_(False)
47
+
48
+ self.vae = WanVAEWrapper()
49
+ self.vae.requires_grad_(False)
50
+
51
+ def generator_loss(
52
+ self,
53
+ image_or_video_shape,
54
+ conditional_dict: dict,
55
+ unconditional_dict: dict,
56
+ clean_latent: torch.Tensor,
57
+ initial_latent: torch.Tensor = None,
58
+ ) -> Tuple[torch.Tensor, dict]:
59
+ """
60
+ Generate image/videos from noise and compute the DMD loss.
61
+ The noisy input to the generator is backward simulated.
62
+ This removes the need of any datasets during distillation.
63
+ See Sec 4.5 of the DMD2 paper (https://arxiv.org/abs/2405.14867) for details.
64
+ Input:
65
+ - image_or_video_shape: a list containing the shape of the image or video [B, F, C, H, W].
66
+ - conditional_dict: a dictionary containing the conditional information (e.g. text embeddings, image embeddings).
67
+ - unconditional_dict: a dictionary containing the unconditional information (e.g. null/negative text embeddings, null/negative image embeddings).
68
+ - clean_latent: a tensor containing the clean latents [B, F, C, H, W]. Need to be passed when no backward simulation is used.
69
+ Output:
70
+ - loss: a scalar tensor representing the generator loss.
71
+ - generator_log_dict: a dictionary containing the intermediate tensors for logging.
72
+ """
73
+ noise = torch.randn_like(clean_latent)
74
+ batch_size, num_frame = image_or_video_shape[:2]
75
+
76
+ # Step 2: Randomly sample a timestep and add noise to denoiser inputs
77
+ index = self._get_timestep(
78
+ 0,
79
+ self.scheduler.num_train_timesteps,
80
+ image_or_video_shape[0],
81
+ image_or_video_shape[1],
82
+ self.num_frame_per_block,
83
+ uniform_timestep=False,
84
+ )
85
+ timestep = self.scheduler.timesteps[index].to(dtype=self.dtype, device=self.device)
86
+ noisy_latents = self.scheduler.add_noise(
87
+ clean_latent.flatten(0, 1),
88
+ noise.flatten(0, 1),
89
+ timestep.flatten(0, 1),
90
+ ).unflatten(0, (batch_size, num_frame))
91
+ training_target = self.scheduler.training_target(clean_latent, noise, timestep)
92
+
93
+ # Step 3: Noise augmentation, also add small noise to clean context latents
94
+ if self.noise_augmentation_max_timestep > 0:
95
+ index_clean_aug = self._get_timestep(
96
+ 0,
97
+ self.noise_augmentation_max_timestep,
98
+ image_or_video_shape[0],
99
+ image_or_video_shape[1],
100
+ self.num_frame_per_block,
101
+ uniform_timestep=False,
102
+ )
103
+ timestep_clean_aug = self.scheduler.timesteps[index_clean_aug].to(
104
+ dtype=self.dtype, device=self.device
105
+ )
106
+ clean_latent_aug = self.scheduler.add_noise(
107
+ clean_latent.flatten(0, 1),
108
+ noise.flatten(0, 1),
109
+ timestep_clean_aug.flatten(0, 1),
110
+ ).unflatten(0, (batch_size, num_frame))
111
+ else:
112
+ clean_latent_aug = clean_latent
113
+ timestep_clean_aug = None
114
+
115
+ # Compute loss
116
+ flow_pred, x0_pred = self.generator(
117
+ noisy_image_or_video=noisy_latents,
118
+ conditional_dict=conditional_dict,
119
+ timestep=timestep,
120
+ clean_x=clean_latent_aug if self.teacher_forcing else None,
121
+ aug_t=timestep_clean_aug if self.teacher_forcing else None,
122
+ )
123
+ # loss = torch.nn.functional.mse_loss(flow_pred.float(), training_target.float())
124
+ loss = torch.nn.functional.mse_loss(
125
+ flow_pred.float(), training_target.float(), reduction="none"
126
+ ).mean(dim=(2, 3, 4))
127
+ loss = loss * self.scheduler.training_weight(timestep).unflatten(0, (batch_size, num_frame))
128
+ loss = loss.mean()
129
+
130
+ log_dict = {"x0": clean_latent.detach(), "x0_pred": x0_pred.detach()}
131
+ return loss, log_dict
model/dmd.py ADDED
@@ -0,0 +1,377 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pipeline import SelfForcingTrainingPipeline
2
+ import torch.nn.functional as F
3
+ from typing import Optional, Tuple
4
+ import torch
5
+
6
+ from model.base import SelfForcingModel
7
+
8
+
9
+ class DMD(SelfForcingModel):
10
+ def __init__(
11
+ self,
12
+ args,
13
+ device,
14
+ ):
15
+ """
16
+ Initialize the DMD (Distribution Matching Distillation) module.
17
+ This class is self-contained and compute generator and fake score losses
18
+ in the forward pass.
19
+ """
20
+ super().__init__(args, device)
21
+ self.num_frame_per_block = getattr(args, "num_frame_per_block", 1)
22
+ self.same_step_across_blocks = getattr(args, "same_step_across_blocks", True)
23
+ self.num_training_frames = getattr(args, "num_training_frames", 21)
24
+
25
+ if self.num_frame_per_block > 1:
26
+ self.generator.model.num_frame_per_block = self.num_frame_per_block
27
+
28
+ self.independent_first_frame = getattr(args, "independent_first_frame", False)
29
+ if self.independent_first_frame:
30
+ self.generator.model.independent_first_frame = True
31
+ if args.gradient_checkpointing:
32
+ self.generator.enable_gradient_checkpointing()
33
+ self.fake_score.enable_gradient_checkpointing()
34
+
35
+ # this will be init later with fsdp-wrapped modules
36
+ self.inference_pipeline: SelfForcingTrainingPipeline = None
37
+
38
+ # Step 2: Initialize all dmd hyperparameters
39
+ self.num_train_timestep = args.num_train_timestep
40
+ self.min_step = int(0.02 * self.num_train_timestep)
41
+ self.max_step = int(0.98 * self.num_train_timestep)
42
+ if hasattr(args, "real_guidance_scale"):
43
+ self.real_guidance_scale = args.real_guidance_scale
44
+ self.fake_guidance_scale = args.fake_guidance_scale
45
+ else:
46
+ self.real_guidance_scale = args.guidance_scale
47
+ self.fake_guidance_scale = 0.0
48
+ self.timestep_shift = getattr(args, "timestep_shift", 1.0)
49
+ self.ts_schedule = getattr(args, "ts_schedule", True)
50
+ self.ts_schedule_max = getattr(args, "ts_schedule_max", False)
51
+ self.min_score_timestep = getattr(args, "min_score_timestep", 0)
52
+
53
+ if getattr(self.scheduler, "alphas_cumprod", None) is not None:
54
+ self.scheduler.alphas_cumprod = self.scheduler.alphas_cumprod.to(device)
55
+ else:
56
+ self.scheduler.alphas_cumprod = None
57
+
58
+ def _compute_kl_grad(
59
+ self,
60
+ noisy_image_or_video: torch.Tensor,
61
+ estimated_clean_image_or_video: torch.Tensor,
62
+ timestep: torch.Tensor,
63
+ conditional_dict: dict,
64
+ unconditional_dict: dict,
65
+ normalization: bool = True,
66
+ ) -> Tuple[torch.Tensor, dict]:
67
+ """
68
+ Compute the KL grad (eq 7 in https://arxiv.org/abs/2311.18828).
69
+ Input:
70
+ - noisy_image_or_video: a tensor with shape [B, F, C, H, W] where the number of frame is 1 for images.
71
+ - estimated_clean_image_or_video: a tensor with shape [B, F, C, H, W] representing the estimated clean image or video.
72
+ - timestep: a tensor with shape [B, F] containing the randomly generated timestep.
73
+ - conditional_dict: a dictionary containing the conditional information (e.g. text embeddings, image embeddings).
74
+ - unconditional_dict: a dictionary containing the unconditional information (e.g. null/negative text embeddings, null/negative image embeddings).
75
+ - normalization: a boolean indicating whether to normalize the gradient.
76
+ Output:
77
+ - kl_grad: a tensor representing the KL grad.
78
+ - kl_log_dict: a dictionary containing the intermediate tensors for logging.
79
+ """
80
+ # Step 1: Compute the fake score
81
+ _, pred_fake_image_cond = self.fake_score(
82
+ noisy_image_or_video=noisy_image_or_video,
83
+ conditional_dict=conditional_dict,
84
+ timestep=timestep,
85
+ )
86
+
87
+ if self.fake_guidance_scale != 0.0:
88
+ _, pred_fake_image_uncond = self.fake_score(
89
+ noisy_image_or_video=noisy_image_or_video,
90
+ conditional_dict=unconditional_dict,
91
+ timestep=timestep,
92
+ )
93
+ pred_fake_image = (
94
+ pred_fake_image_cond
95
+ + (pred_fake_image_cond - pred_fake_image_uncond) * self.fake_guidance_scale
96
+ )
97
+ else:
98
+ pred_fake_image = pred_fake_image_cond
99
+
100
+ # Step 2: Compute the real score
101
+ # We compute the conditional and unconditional prediction
102
+ # and add them together to achieve cfg (https://arxiv.org/abs/2207.12598)
103
+ _, pred_real_image_cond = self.real_score(
104
+ noisy_image_or_video=noisy_image_or_video,
105
+ conditional_dict=conditional_dict,
106
+ timestep=timestep,
107
+ )
108
+
109
+ _, pred_real_image_uncond = self.real_score(
110
+ noisy_image_or_video=noisy_image_or_video,
111
+ conditional_dict=unconditional_dict,
112
+ timestep=timestep,
113
+ )
114
+
115
+ pred_real_image = (
116
+ pred_real_image_cond
117
+ + (pred_real_image_cond - pred_real_image_uncond) * self.real_guidance_scale
118
+ )
119
+
120
+ # Step 3: Compute the DMD gradient (DMD paper eq. 7).
121
+ grad = pred_fake_image - pred_real_image
122
+
123
+ # TODO: Change the normalizer for causal teacher
124
+ if normalization:
125
+ # Step 4: Gradient normalization (DMD paper eq. 8).
126
+ p_real = estimated_clean_image_or_video - pred_real_image
127
+ normalizer = torch.abs(p_real).mean(dim=[1, 2, 3, 4], keepdim=True)
128
+ grad = grad / normalizer
129
+ grad = torch.nan_to_num(grad)
130
+
131
+ return grad, {
132
+ "dmdtrain_gradient_norm": torch.mean(torch.abs(grad)).detach(),
133
+ "timestep": timestep.detach(),
134
+ }
135
+
136
+ def compute_distribution_matching_loss(
137
+ self,
138
+ image_or_video: torch.Tensor,
139
+ conditional_dict: dict,
140
+ unconditional_dict: dict,
141
+ gradient_mask: Optional[torch.Tensor] = None,
142
+ denoised_timestep_from: int = 0,
143
+ denoised_timestep_to: int = 0,
144
+ ) -> Tuple[torch.Tensor, dict]:
145
+ """
146
+ Compute the DMD loss (eq 7 in https://arxiv.org/abs/2311.18828).
147
+ Input:
148
+ - image_or_video: a tensor with shape [B, F, C, H, W] where the number of frame is 1 for images.
149
+ - conditional_dict: a dictionary containing the conditional information (e.g. text embeddings, image embeddings).
150
+ - unconditional_dict: a dictionary containing the unconditional information (e.g. null/negative text embeddings, null/negative image embeddings).
151
+ - gradient_mask: a boolean tensor with the same shape as image_or_video indicating which pixels to compute loss .
152
+ Output:
153
+ - dmd_loss: a scalar tensor representing the DMD loss.
154
+ - dmd_log_dict: a dictionary containing the intermediate tensors for logging.
155
+ """
156
+ original_latent = image_or_video
157
+
158
+ batch_size, num_frame = image_or_video.shape[:2]
159
+
160
+ with torch.no_grad():
161
+ # Step 1: Randomly sample timestep based on the given schedule and corresponding noise
162
+ min_timestep = (
163
+ denoised_timestep_to
164
+ if self.ts_schedule and denoised_timestep_to is not None
165
+ else self.min_score_timestep
166
+ )
167
+ max_timestep = (
168
+ denoised_timestep_from
169
+ if self.ts_schedule_max and denoised_timestep_from is not None
170
+ else self.num_train_timestep
171
+ )
172
+ timestep = self._get_timestep(
173
+ min_timestep,
174
+ max_timestep,
175
+ batch_size,
176
+ num_frame,
177
+ self.num_frame_per_block,
178
+ uniform_timestep=True,
179
+ )
180
+
181
+ # TODO:should we change it to `timestep = self.scheduler.timesteps[timestep]`?
182
+ if self.timestep_shift > 1:
183
+ timestep = (
184
+ self.timestep_shift
185
+ * (timestep / 1000)
186
+ / (1 + (self.timestep_shift - 1) * (timestep / 1000))
187
+ * 1000
188
+ )
189
+ timestep = timestep.clamp(self.min_step, self.max_step)
190
+
191
+ noise = torch.randn_like(image_or_video)
192
+ noisy_latent = (
193
+ self.scheduler.add_noise(
194
+ image_or_video.flatten(0, 1),
195
+ noise.flatten(0, 1),
196
+ timestep.flatten(0, 1),
197
+ )
198
+ .detach()
199
+ .unflatten(0, (batch_size, num_frame))
200
+ )
201
+
202
+ # Step 2: Compute the KL grad
203
+ grad, dmd_log_dict = self._compute_kl_grad(
204
+ noisy_image_or_video=noisy_latent,
205
+ estimated_clean_image_or_video=original_latent,
206
+ timestep=timestep,
207
+ conditional_dict=conditional_dict,
208
+ unconditional_dict=unconditional_dict,
209
+ )
210
+
211
+ if gradient_mask is not None:
212
+ dmd_loss = 0.5 * F.mse_loss(
213
+ original_latent.double()[gradient_mask],
214
+ (original_latent.double() - grad.double()).detach()[gradient_mask],
215
+ reduction="mean",
216
+ )
217
+ else:
218
+ dmd_loss = 0.5 * F.mse_loss(
219
+ original_latent.double(),
220
+ (original_latent.double() - grad.double()).detach(),
221
+ reduction="mean",
222
+ )
223
+ return dmd_loss, dmd_log_dict
224
+
225
+ def generator_loss(
226
+ self,
227
+ image_or_video_shape,
228
+ conditional_dict: dict,
229
+ unconditional_dict: dict,
230
+ clean_latent: torch.Tensor,
231
+ initial_latent: torch.Tensor = None,
232
+ ) -> Tuple[torch.Tensor, dict]:
233
+ """
234
+ Generate image/videos from noise and compute the DMD loss.
235
+ The noisy input to the generator is backward simulated.
236
+ This removes the need of any datasets during distillation.
237
+ See Sec 4.5 of the DMD2 paper (https://arxiv.org/abs/2405.14867) for details.
238
+ Input:
239
+ - image_or_video_shape: a list containing the shape of the image or video [B, F, C, H, W].
240
+ - conditional_dict: a dictionary containing the conditional information (e.g. text embeddings, image embeddings).
241
+ - unconditional_dict: a dictionary containing the unconditional information (e.g. null/negative text embeddings, null/negative image embeddings).
242
+ - clean_latent: a tensor containing the clean latents [B, F, C, H, W]. Need to be passed when no backward simulation is used.
243
+ Output:
244
+ - loss: a scalar tensor representing the generator loss.
245
+ - generator_log_dict: a dictionary containing the intermediate tensors for logging.
246
+ """
247
+ # Step 1: Unroll generator to obtain fake videos
248
+ (
249
+ pred_image,
250
+ gradient_mask,
251
+ denoised_timestep_from,
252
+ denoised_timestep_to,
253
+ ) = self._run_generator(
254
+ image_or_video_shape=image_or_video_shape,
255
+ conditional_dict=conditional_dict,
256
+ initial_latent=initial_latent,
257
+ )
258
+
259
+ # Step 2: Compute the DMD loss
260
+ dmd_loss, dmd_log_dict = self.compute_distribution_matching_loss(
261
+ image_or_video=pred_image,
262
+ conditional_dict=conditional_dict,
263
+ unconditional_dict=unconditional_dict,
264
+ gradient_mask=gradient_mask,
265
+ denoised_timestep_from=denoised_timestep_from,
266
+ denoised_timestep_to=denoised_timestep_to,
267
+ )
268
+
269
+ return dmd_loss, dmd_log_dict
270
+
271
+ def critic_loss(
272
+ self,
273
+ image_or_video_shape,
274
+ conditional_dict: dict,
275
+ unconditional_dict: dict,
276
+ clean_latent: torch.Tensor,
277
+ initial_latent: torch.Tensor = None,
278
+ ) -> Tuple[torch.Tensor, dict]:
279
+ """
280
+ Generate image/videos from noise and train the critic with generated samples.
281
+ The noisy input to the generator is backward simulated.
282
+ This removes the need of any datasets during distillation.
283
+ See Sec 4.5 of the DMD2 paper (https://arxiv.org/abs/2405.14867) for details.
284
+ Input:
285
+ - image_or_video_shape: a list containing the shape of the image or video [B, F, C, H, W].
286
+ - conditional_dict: a dictionary containing the conditional information (e.g. text embeddings, image embeddings).
287
+ - unconditional_dict: a dictionary containing the unconditional information (e.g. null/negative text embeddings, null/negative image embeddings).
288
+ - clean_latent: a tensor containing the clean latents [B, F, C, H, W]. Need to be passed when no backward simulation is used.
289
+ Output:
290
+ - loss: a scalar tensor representing the generator loss.
291
+ - critic_log_dict: a dictionary containing the intermediate tensors for logging.
292
+ """
293
+
294
+ # Step 1: Run generator on backward simulated noisy input
295
+ with torch.no_grad():
296
+ generated_image, _, denoised_timestep_from, denoised_timestep_to = self._run_generator(
297
+ image_or_video_shape=image_or_video_shape,
298
+ conditional_dict=conditional_dict,
299
+ initial_latent=initial_latent,
300
+ )
301
+
302
+ # Step 2: Compute the fake prediction
303
+ min_timestep = (
304
+ denoised_timestep_to
305
+ if self.ts_schedule and denoised_timestep_to is not None
306
+ else self.min_score_timestep
307
+ )
308
+ max_timestep = (
309
+ denoised_timestep_from
310
+ if self.ts_schedule_max and denoised_timestep_from is not None
311
+ else self.num_train_timestep
312
+ )
313
+ critic_timestep = self._get_timestep(
314
+ min_timestep,
315
+ max_timestep,
316
+ image_or_video_shape[0],
317
+ image_or_video_shape[1],
318
+ self.num_frame_per_block,
319
+ uniform_timestep=True,
320
+ )
321
+
322
+ if self.timestep_shift > 1:
323
+ critic_timestep = (
324
+ self.timestep_shift
325
+ * (critic_timestep / 1000)
326
+ / (1 + (self.timestep_shift - 1) * (critic_timestep / 1000))
327
+ * 1000
328
+ )
329
+
330
+ critic_timestep = critic_timestep.clamp(self.min_step, self.max_step)
331
+
332
+ critic_noise = torch.randn_like(generated_image)
333
+ noisy_generated_image = self.scheduler.add_noise(
334
+ generated_image.flatten(0, 1),
335
+ critic_noise.flatten(0, 1),
336
+ critic_timestep.flatten(0, 1),
337
+ ).unflatten(0, image_or_video_shape[:2])
338
+
339
+ _, pred_fake_image = self.fake_score(
340
+ noisy_image_or_video=noisy_generated_image,
341
+ conditional_dict=conditional_dict,
342
+ timestep=critic_timestep,
343
+ )
344
+
345
+ # Step 3: Compute the denoising loss for the fake critic
346
+ if self.args.denoising_loss_type == "flow":
347
+ from utils.wan_wrapper import WanDiffusionWrapper
348
+
349
+ flow_pred = WanDiffusionWrapper._convert_x0_to_flow_pred(
350
+ scheduler=self.scheduler,
351
+ x0_pred=pred_fake_image.flatten(0, 1),
352
+ xt=noisy_generated_image.flatten(0, 1),
353
+ timestep=critic_timestep.flatten(0, 1),
354
+ )
355
+ pred_fake_noise = None
356
+ else:
357
+ flow_pred = None
358
+ pred_fake_noise = self.scheduler.convert_x0_to_noise(
359
+ x0=pred_fake_image.flatten(0, 1),
360
+ xt=noisy_generated_image.flatten(0, 1),
361
+ timestep=critic_timestep.flatten(0, 1),
362
+ ).unflatten(0, image_or_video_shape[:2])
363
+
364
+ denoising_loss = self.denoising_loss_func(
365
+ x=generated_image.flatten(0, 1),
366
+ x_pred=pred_fake_image.flatten(0, 1),
367
+ noise=critic_noise.flatten(0, 1),
368
+ noise_pred=pred_fake_noise,
369
+ alphas_cumprod=self.scheduler.alphas_cumprod,
370
+ timestep=critic_timestep.flatten(0, 1),
371
+ flow_pred=flow_pred,
372
+ )
373
+
374
+ # Step 5: Debugging Log
375
+ critic_log_dict = {"critic_timestep": critic_timestep.detach()}
376
+
377
+ return denoising_loss, critic_log_dict
model/gan.py ADDED
@@ -0,0 +1,352 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import copy
2
+ from pipeline import SelfForcingTrainingPipeline
3
+ import torch.nn.functional as F
4
+ from typing import Tuple
5
+ import torch
6
+
7
+ from model.base import SelfForcingModel
8
+
9
+
10
+ class GAN(SelfForcingModel):
11
+ def __init__(
12
+ self,
13
+ args,
14
+ device,
15
+ ):
16
+ """
17
+ Initialize the GAN module.
18
+ This class is self-contained and compute generator and fake score losses
19
+ in the forward pass.
20
+ """
21
+ super().__init__(args, device)
22
+ self.num_frame_per_block = getattr(args, "num_frame_per_block", 1)
23
+ self.same_step_across_blocks = getattr(args, "same_step_across_blocks", True)
24
+ self.concat_time_embeddings = getattr(args, "concat_time_embeddings", False)
25
+ self.num_class = args.num_class
26
+ self.relativistic_discriminator = getattr(args, "relativistic_discriminator", False)
27
+
28
+ if self.num_frame_per_block > 1:
29
+ self.generator.model.num_frame_per_block = self.num_frame_per_block
30
+
31
+ self.fake_score.adding_cls_branch(
32
+ atten_dim=1536,
33
+ num_class=args.num_class,
34
+ time_embed_dim=1536 if self.concat_time_embeddings else 0,
35
+ )
36
+ self.fake_score.model.requires_grad_(True)
37
+
38
+ self.independent_first_frame = getattr(args, "independent_first_frame", False)
39
+ if self.independent_first_frame:
40
+ self.generator.model.independent_first_frame = True
41
+ if args.gradient_checkpointing:
42
+ self.generator.enable_gradient_checkpointing()
43
+ self.fake_score.enable_gradient_checkpointing()
44
+
45
+ # this will be init later with fsdp-wrapped modules
46
+ self.inference_pipeline: SelfForcingTrainingPipeline = None
47
+
48
+ # Step 2: Initialize all dmd hyperparameters
49
+ self.num_train_timestep = args.num_train_timestep
50
+ self.min_step = int(0.02 * self.num_train_timestep)
51
+ self.max_step = int(0.98 * self.num_train_timestep)
52
+ if hasattr(args, "real_guidance_scale"):
53
+ self.real_guidance_scale = args.real_guidance_scale
54
+ self.fake_guidance_scale = args.fake_guidance_scale
55
+ else:
56
+ self.real_guidance_scale = args.guidance_scale
57
+ self.fake_guidance_scale = 0.0
58
+ self.timestep_shift = getattr(args, "timestep_shift", 1.0)
59
+ self.critic_timestep_shift = getattr(args, "critic_timestep_shift", self.timestep_shift)
60
+ self.ts_schedule = getattr(args, "ts_schedule", True)
61
+ self.ts_schedule_max = getattr(args, "ts_schedule_max", False)
62
+ self.min_score_timestep = getattr(args, "min_score_timestep", 0)
63
+
64
+ self.gan_g_weight = getattr(args, "gan_g_weight", 1e-2)
65
+ self.gan_d_weight = getattr(args, "gan_d_weight", 1e-2)
66
+ self.r1_weight = getattr(args, "r1_weight", 0.0)
67
+ self.r2_weight = getattr(args, "r2_weight", 0.0)
68
+ self.r1_sigma = getattr(args, "r1_sigma", 0.01)
69
+ self.r2_sigma = getattr(args, "r2_sigma", 0.01)
70
+
71
+ if getattr(self.scheduler, "alphas_cumprod", None) is not None:
72
+ self.scheduler.alphas_cumprod = self.scheduler.alphas_cumprod.to(device)
73
+ else:
74
+ self.scheduler.alphas_cumprod = None
75
+
76
+ def _run_cls_pred_branch(
77
+ self,
78
+ noisy_image_or_video: torch.Tensor,
79
+ conditional_dict: dict,
80
+ timestep: torch.Tensor,
81
+ ) -> torch.Tensor:
82
+ """
83
+ Run the classifier prediction branch on the generated image or video.
84
+ Input:
85
+ - image_or_video: a tensor with shape [B, F, C, H, W].
86
+ Output:
87
+ - cls_pred: a tensor with shape [B, 1, 1, 1, 1] representing the feature map for classification.
88
+ """
89
+ _, _, noisy_logit = self.fake_score(
90
+ noisy_image_or_video=noisy_image_or_video,
91
+ conditional_dict=conditional_dict,
92
+ timestep=timestep,
93
+ classify_mode=True,
94
+ concat_time_embeddings=self.concat_time_embeddings,
95
+ )
96
+
97
+ return noisy_logit
98
+
99
+ def generator_loss(
100
+ self,
101
+ image_or_video_shape,
102
+ conditional_dict: dict,
103
+ unconditional_dict: dict,
104
+ clean_latent: torch.Tensor,
105
+ initial_latent: torch.Tensor = None,
106
+ ) -> Tuple[torch.Tensor, dict]:
107
+ """
108
+ Generate image/videos from noise and compute the DMD loss.
109
+ The noisy input to the generator is backward simulated.
110
+ This removes the need of any datasets during distillation.
111
+ See Sec 4.5 of the DMD2 paper (https://arxiv.org/abs/2405.14867) for details.
112
+ Input:
113
+ - image_or_video_shape: a list containing the shape of the image or video [B, F, C, H, W].
114
+ - conditional_dict: a dictionary containing the conditional information (e.g. text embeddings, image embeddings).
115
+ - unconditional_dict: a dictionary containing the unconditional information (e.g. null/negative text embeddings, null/negative image embeddings).
116
+ - clean_latent: a tensor containing the clean latents [B, F, C, H, W]. Need to be passed when no backward simulation is used.
117
+ Output:
118
+ - loss: a scalar tensor representing the generator loss.
119
+ - generator_log_dict: a dictionary containing the intermediate tensors for logging.
120
+ """
121
+ # Step 1: Unroll generator to obtain fake videos
122
+ (
123
+ pred_image,
124
+ gradient_mask,
125
+ denoised_timestep_from,
126
+ denoised_timestep_to,
127
+ ) = self._run_generator(
128
+ image_or_video_shape=image_or_video_shape,
129
+ conditional_dict=conditional_dict,
130
+ initial_latent=initial_latent,
131
+ )
132
+
133
+ # Step 2: Get timestep and add noise to generated/real latents
134
+ min_timestep = (
135
+ denoised_timestep_to
136
+ if self.ts_schedule and denoised_timestep_to is not None
137
+ else self.min_score_timestep
138
+ )
139
+ max_timestep = (
140
+ denoised_timestep_from
141
+ if self.ts_schedule_max and denoised_timestep_from is not None
142
+ else self.num_train_timestep
143
+ )
144
+ critic_timestep = self._get_timestep(
145
+ min_timestep,
146
+ max_timestep,
147
+ image_or_video_shape[0],
148
+ image_or_video_shape[1],
149
+ self.num_frame_per_block,
150
+ uniform_timestep=True,
151
+ )
152
+
153
+ if self.critic_timestep_shift > 1:
154
+ critic_timestep = (
155
+ self.critic_timestep_shift
156
+ * (critic_timestep / 1000)
157
+ / (1 + (self.critic_timestep_shift - 1) * (critic_timestep / 1000))
158
+ * 1000
159
+ )
160
+
161
+ critic_timestep = critic_timestep.clamp(self.min_step, self.max_step)
162
+
163
+ critic_noise = torch.randn_like(pred_image)
164
+ noisy_fake_latent = self.scheduler.add_noise(
165
+ pred_image.flatten(0, 1),
166
+ critic_noise.flatten(0, 1),
167
+ critic_timestep.flatten(0, 1),
168
+ ).unflatten(0, image_or_video_shape[:2])
169
+
170
+ # Step 4: Compute the real GAN discriminator loss
171
+ real_image_or_video = clean_latent.clone()
172
+ critic_noise = torch.randn_like(real_image_or_video)
173
+ noisy_real_latent = self.scheduler.add_noise(
174
+ real_image_or_video.flatten(0, 1),
175
+ critic_noise.flatten(0, 1),
176
+ critic_timestep.flatten(0, 1),
177
+ ).unflatten(0, image_or_video_shape[:2])
178
+
179
+ conditional_dict["prompt_embeds"] = torch.concatenate(
180
+ (
181
+ conditional_dict["prompt_embeds"],
182
+ conditional_dict["prompt_embeds"],
183
+ ),
184
+ dim=0,
185
+ )
186
+ critic_timestep = torch.concatenate((critic_timestep, critic_timestep), dim=0)
187
+ noisy_latent = torch.concatenate((noisy_fake_latent, noisy_real_latent), dim=0)
188
+ _, _, noisy_logit = self.fake_score(
189
+ noisy_image_or_video=noisy_latent,
190
+ conditional_dict=conditional_dict,
191
+ timestep=critic_timestep,
192
+ classify_mode=True,
193
+ concat_time_embeddings=self.concat_time_embeddings,
194
+ )
195
+ noisy_fake_logit, noisy_real_logit = noisy_logit.chunk(2, dim=0)
196
+
197
+ if not self.relativistic_discriminator:
198
+ gan_G_loss = F.softplus(-noisy_fake_logit.float()).mean() * self.gan_g_weight
199
+ else:
200
+ relative_fake_logit = noisy_fake_logit - noisy_real_logit
201
+ gan_G_loss = F.softplus(-relative_fake_logit.float()).mean() * self.gan_g_weight
202
+
203
+ return gan_G_loss
204
+
205
+ def critic_loss(
206
+ self,
207
+ image_or_video_shape,
208
+ conditional_dict: dict,
209
+ unconditional_dict: dict,
210
+ clean_latent: torch.Tensor,
211
+ real_image_or_video: torch.Tensor,
212
+ initial_latent: torch.Tensor = None,
213
+ ) -> Tuple[torch.Tensor, dict]:
214
+ """
215
+ Generate image/videos from noise and train the critic with generated samples.
216
+ The noisy input to the generator is backward simulated.
217
+ This removes the need of any datasets during distillation.
218
+ See Sec 4.5 of the DMD2 paper (https://arxiv.org/abs/2405.14867) for details.
219
+ Input:
220
+ - image_or_video_shape: a list containing the shape of the image or video [B, F, C, H, W].
221
+ - conditional_dict: a dictionary containing the conditional information (e.g. text embeddings, image embeddings).
222
+ - unconditional_dict: a dictionary containing the unconditional information (e.g. null/negative text embeddings, null/negative image embeddings).
223
+ - clean_latent: a tensor containing the clean latents [B, F, C, H, W]. Need to be passed when no backward simulation is used.
224
+ Output:
225
+ - loss: a scalar tensor representing the generator loss.
226
+ - critic_log_dict: a dictionary containing the intermediate tensors for logging.
227
+ """
228
+
229
+ # Step 1: Run generator on backward simulated noisy input
230
+ with torch.no_grad():
231
+ (
232
+ generated_image,
233
+ _,
234
+ denoised_timestep_from,
235
+ denoised_timestep_to,
236
+ num_sim_steps,
237
+ ) = self._run_generator(
238
+ image_or_video_shape=image_or_video_shape,
239
+ conditional_dict=conditional_dict,
240
+ initial_latent=initial_latent,
241
+ )
242
+
243
+ # Step 2: Get timestep and add noise to generated/real latents
244
+ min_timestep = (
245
+ denoised_timestep_to
246
+ if self.ts_schedule and denoised_timestep_to is not None
247
+ else self.min_score_timestep
248
+ )
249
+ max_timestep = (
250
+ denoised_timestep_from
251
+ if self.ts_schedule_max and denoised_timestep_from is not None
252
+ else self.num_train_timestep
253
+ )
254
+ critic_timestep = self._get_timestep(
255
+ min_timestep,
256
+ max_timestep,
257
+ image_or_video_shape[0],
258
+ image_or_video_shape[1],
259
+ self.num_frame_per_block,
260
+ uniform_timestep=True,
261
+ )
262
+
263
+ if self.critic_timestep_shift > 1:
264
+ critic_timestep = (
265
+ self.critic_timestep_shift
266
+ * (critic_timestep / 1000)
267
+ / (1 + (self.critic_timestep_shift - 1) * (critic_timestep / 1000))
268
+ * 1000
269
+ )
270
+
271
+ critic_timestep = critic_timestep.clamp(self.min_step, self.max_step)
272
+
273
+ critic_noise = torch.randn_like(generated_image)
274
+ noisy_fake_latent = self.scheduler.add_noise(
275
+ generated_image.flatten(0, 1),
276
+ critic_noise.flatten(0, 1),
277
+ critic_timestep.flatten(0, 1),
278
+ ).unflatten(0, image_or_video_shape[:2])
279
+
280
+ # Step 4: Compute the real GAN discriminator loss
281
+ noisy_real_latent = self.scheduler.add_noise(
282
+ real_image_or_video.flatten(0, 1),
283
+ critic_noise.flatten(0, 1),
284
+ critic_timestep.flatten(0, 1),
285
+ ).unflatten(0, image_or_video_shape[:2])
286
+
287
+ conditional_dict_cloned = copy.deepcopy(conditional_dict)
288
+ conditional_dict_cloned["prompt_embeds"] = torch.concatenate(
289
+ (
290
+ conditional_dict_cloned["prompt_embeds"],
291
+ conditional_dict_cloned["prompt_embeds"],
292
+ ),
293
+ dim=0,
294
+ )
295
+ _, _, noisy_logit = self.fake_score(
296
+ noisy_image_or_video=torch.concatenate((noisy_fake_latent, noisy_real_latent), dim=0),
297
+ conditional_dict=conditional_dict_cloned,
298
+ timestep=torch.concatenate((critic_timestep, critic_timestep), dim=0),
299
+ classify_mode=True,
300
+ concat_time_embeddings=self.concat_time_embeddings,
301
+ )
302
+ noisy_fake_logit, noisy_real_logit = noisy_logit.chunk(2, dim=0)
303
+
304
+ if not self.relativistic_discriminator:
305
+ gan_D_loss = (
306
+ F.softplus(-noisy_real_logit.float()).mean()
307
+ + F.softplus(noisy_fake_logit.float()).mean()
308
+ )
309
+ else:
310
+ relative_real_logit = noisy_real_logit - noisy_fake_logit
311
+ gan_D_loss = F.softplus(-relative_real_logit.float()).mean()
312
+ gan_D_loss = gan_D_loss * self.gan_d_weight
313
+
314
+ # R1 regularization
315
+ if self.r1_weight > 0.0:
316
+ noisy_real_latent_perturbed = noisy_real_latent.clone()
317
+ epison_real = self.r1_sigma * torch.randn_like(noisy_real_latent_perturbed)
318
+ noisy_real_latent_perturbed = noisy_real_latent_perturbed + epison_real
319
+ noisy_real_logit_perturbed = self._run_cls_pred_branch(
320
+ noisy_image_or_video=noisy_real_latent_perturbed,
321
+ conditional_dict=conditional_dict,
322
+ timestep=critic_timestep,
323
+ )
324
+
325
+ r1_grad = (noisy_real_logit_perturbed - noisy_real_logit) / self.r1_sigma
326
+ r1_loss = self.r1_weight * torch.mean((r1_grad) ** 2)
327
+ else:
328
+ r1_loss = torch.zeros_like(gan_D_loss)
329
+
330
+ # R2 regularization
331
+ if self.r2_weight > 0.0:
332
+ noisy_fake_latent_perturbed = noisy_fake_latent.clone()
333
+ epison_generated = self.r2_sigma * torch.randn_like(noisy_fake_latent_perturbed)
334
+ noisy_fake_latent_perturbed = noisy_fake_latent_perturbed + epison_generated
335
+ noisy_fake_logit_perturbed = self._run_cls_pred_branch(
336
+ noisy_image_or_video=noisy_fake_latent_perturbed,
337
+ conditional_dict=conditional_dict,
338
+ timestep=critic_timestep,
339
+ )
340
+
341
+ r2_grad = (noisy_fake_logit_perturbed - noisy_fake_logit) / self.r2_sigma
342
+ r2_loss = self.r2_weight * torch.mean((r2_grad) ** 2)
343
+ else:
344
+ r2_loss = torch.zeros_like(r2_loss)
345
+
346
+ critic_log_dict = {
347
+ "critic_timestep": critic_timestep.detach(),
348
+ "noisy_real_logit": noisy_real_logit.detach(),
349
+ "noisy_fake_logit": noisy_fake_logit.detach(),
350
+ }
351
+
352
+ return (gan_D_loss, r1_loss, r2_loss), critic_log_dict
model/ode_regression.py ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch.nn.functional as F
2
+ from typing import Tuple
3
+ import torch
4
+
5
+ from model.base import BaseModel
6
+ from utils.wan_wrapper import WanDiffusionWrapper, WanTextEncoder, WanVAEWrapper
7
+
8
+
9
+ class ODERegression(BaseModel):
10
+ def __init__(
11
+ self,
12
+ args,
13
+ device,
14
+ ):
15
+ """
16
+ Initialize the ODERegression module.
17
+ This class is self-contained and compute generator losses
18
+ in the forward pass given precomputed ode solution pairs.
19
+ This class supports the ode regression loss for both causal and bidirectional models.
20
+ See Sec 4.3 of CausVid https://arxiv.org/abs/2412.07772 for details
21
+ """
22
+ super().__init__(args, device)
23
+
24
+ # Step 1: Initialize all models
25
+
26
+ self.generator = WanDiffusionWrapper(
27
+ **getattr(args, "model_kwargs", {}),
28
+ is_causal=True,
29
+ )
30
+ self.generator.model.requires_grad_(True)
31
+ if getattr(args, "generator_ckpt", False):
32
+ print(f"Loading pretrained generator from {args.generator_ckpt}")
33
+ state_dict = torch.load(args.generator_ckpt, map_location="cpu")["generator"]
34
+ self.generator.load_state_dict(state_dict, strict=True)
35
+
36
+ self.num_frame_per_block = getattr(args, "num_frame_per_block", 1)
37
+
38
+ if self.num_frame_per_block > 1:
39
+ self.generator.model.num_frame_per_block = self.num_frame_per_block
40
+
41
+ self.independent_first_frame = getattr(
42
+ args,
43
+ "independent_first_frame",
44
+ False,
45
+ )
46
+ if self.independent_first_frame:
47
+ self.generator.model.independent_first_frame = True
48
+ if args.gradient_checkpointing:
49
+ self.generator.enable_gradient_checkpointing()
50
+
51
+ # Step 2: Initialize all hyperparameters
52
+ self.timestep_shift = getattr(args, "timestep_shift", 1.0)
53
+
54
+ def _initialize_models(self, args):
55
+ self.generator = WanDiffusionWrapper(**getattr(args, "model_kwargs", {}), is_causal=True)
56
+ self.generator.model.requires_grad_(True)
57
+
58
+ self.text_encoder = WanTextEncoder()
59
+ self.text_encoder.requires_grad_(False)
60
+
61
+ self.vae = WanVAEWrapper()
62
+ self.vae.requires_grad_(False)
63
+
64
+ @torch.no_grad()
65
+ def _prepare_generator_input(
66
+ self,
67
+ ode_latent: torch.Tensor,
68
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
69
+ """
70
+ Given a tensor containing the whole ODE sampling trajectories,
71
+ randomly choose an intermediate timestep and return the latent as well as the corresponding timestep.
72
+ Input:
73
+ - ode_latent: a tensor containing the whole ODE sampling trajectories [batch_size, num_denoising_steps, num_frames, num_channels, height, width].
74
+ Output:
75
+ - noisy_input: a tensor containing the selected latent [batch_size, num_frames, num_channels, height, width].
76
+ - timestep: a tensor containing the corresponding timestep [batch_size].
77
+ """
78
+ (
79
+ batch_size,
80
+ num_denoising_steps,
81
+ num_frames,
82
+ num_channels,
83
+ height,
84
+ width,
85
+ ) = ode_latent.shape
86
+
87
+ # Step 1: Randomly choose a timestep for each frame
88
+ index = self._get_timestep(
89
+ 0,
90
+ len(self.denoising_step_list),
91
+ batch_size,
92
+ num_frames,
93
+ self.num_frame_per_block,
94
+ uniform_timestep=False,
95
+ )
96
+ if self.args.i2v:
97
+ index[:, 0] = len(self.denoising_step_list) - 1
98
+
99
+ noisy_input = torch.gather(
100
+ ode_latent,
101
+ dim=1,
102
+ index=index.reshape(batch_size, 1, num_frames, 1, 1, 1)
103
+ .expand(-1, -1, -1, num_channels, height, width)
104
+ .to(self.device),
105
+ ).squeeze(1)
106
+
107
+ timestep = self.denoising_step_list[index].to(self.device)
108
+
109
+ # if self.extra_noise_step > 0:
110
+ # random_timestep = torch.randint(0, self.extra_noise_step, [
111
+ # batch_size, num_frames], device=self.device, dtype=torch.long)
112
+ # perturbed_noisy_input = self.scheduler.add_noise(
113
+ # noisy_input.flatten(0, 1),
114
+ # torch.randn_like(noisy_input.flatten(0, 1)),
115
+ # random_timestep.flatten(0, 1)
116
+ # ).detach().unflatten(0, (batch_size, num_frames)).type_as(noisy_input)
117
+
118
+ # noisy_input[timestep == 0] = perturbed_noisy_input[timestep == 0]
119
+
120
+ return noisy_input, timestep
121
+
122
+ def generator_loss(
123
+ self,
124
+ ode_latent: torch.Tensor,
125
+ conditional_dict: dict,
126
+ ) -> Tuple[torch.Tensor, dict]:
127
+ """
128
+ Generate image/videos from noisy latents and compute the ODE regression loss.
129
+ Input:
130
+ - ode_latent: a tensor containing the ODE latents [batch_size, num_denoising_steps, num_frames, num_channels, height, width].
131
+ They are ordered from most noisy to clean latents.
132
+ - conditional_dict: a dictionary containing the conditional information (e.g. text embeddings, image embeddings).
133
+ Output:
134
+ - loss: a scalar tensor representing the generator loss.
135
+ - log_dict: a dictionary containing additional information for loss timestep breakdown.
136
+ """
137
+ # Step 1: Run generator on noisy latents
138
+ target_latent = ode_latent[:, -1]
139
+
140
+ noisy_input, timestep = self._prepare_generator_input(ode_latent=ode_latent)
141
+
142
+ _, pred_image_or_video = self.generator(
143
+ noisy_image_or_video=noisy_input,
144
+ conditional_dict=conditional_dict,
145
+ timestep=timestep,
146
+ )
147
+
148
+ # Step 2: Compute the regression loss
149
+ mask = timestep != 0
150
+
151
+ loss = F.mse_loss(pred_image_or_video[mask], target_latent[mask], reduction="mean")
152
+
153
+ log_dict = {
154
+ "unnormalized_loss": F.mse_loss(pred_image_or_video, target_latent, reduction="none")
155
+ .mean(dim=[1, 2, 3, 4])
156
+ .detach(),
157
+ "timestep": timestep.float().mean(dim=1).detach(),
158
+ "input": noisy_input.detach(),
159
+ "output": pred_image_or_video.detach(),
160
+ }
161
+
162
+ return loss, log_dict
model/sid.py ADDED
@@ -0,0 +1,318 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pipeline import SelfForcingTrainingPipeline
2
+ from typing import Optional, Tuple
3
+ import torch
4
+
5
+ from model.base import SelfForcingModel
6
+
7
+
8
+ class SiD(SelfForcingModel):
9
+ def __init__(
10
+ self,
11
+ args,
12
+ device,
13
+ ):
14
+ """
15
+ Initialize the DMD (Distribution Matching Distillation) module.
16
+ This class is self-contained and compute generator and fake score losses
17
+ in the forward pass.
18
+ """
19
+ super().__init__(args, device)
20
+ self.num_frame_per_block = getattr(args, "num_frame_per_block", 1)
21
+
22
+ if self.num_frame_per_block > 1:
23
+ self.generator.model.num_frame_per_block = self.num_frame_per_block
24
+
25
+ if args.gradient_checkpointing:
26
+ self.generator.enable_gradient_checkpointing()
27
+ self.fake_score.enable_gradient_checkpointing()
28
+ self.real_score.enable_gradient_checkpointing()
29
+
30
+ # this will be init later with fsdp-wrapped modules
31
+ self.inference_pipeline: SelfForcingTrainingPipeline = None
32
+
33
+ # Step 2: Initialize all dmd hyperparameters
34
+ self.num_train_timestep = args.num_train_timestep
35
+ self.min_step = int(0.02 * self.num_train_timestep)
36
+ self.max_step = int(0.98 * self.num_train_timestep)
37
+ if hasattr(args, "real_guidance_scale"):
38
+ self.real_guidance_scale = args.real_guidance_scale
39
+ else:
40
+ self.real_guidance_scale = args.guidance_scale
41
+ self.timestep_shift = getattr(args, "timestep_shift", 1.0)
42
+ self.sid_alpha = getattr(args, "sid_alpha", 1.0)
43
+ self.ts_schedule = getattr(args, "ts_schedule", True)
44
+ self.ts_schedule_max = getattr(args, "ts_schedule_max", False)
45
+
46
+ if getattr(self.scheduler, "alphas_cumprod", None) is not None:
47
+ self.scheduler.alphas_cumprod = self.scheduler.alphas_cumprod.to(device)
48
+ else:
49
+ self.scheduler.alphas_cumprod = None
50
+
51
+ def compute_distribution_matching_loss(
52
+ self,
53
+ image_or_video: torch.Tensor,
54
+ conditional_dict: dict,
55
+ unconditional_dict: dict,
56
+ gradient_mask: Optional[torch.Tensor] = None,
57
+ denoised_timestep_from: int = 0,
58
+ denoised_timestep_to: int = 0,
59
+ ) -> Tuple[torch.Tensor, dict]:
60
+ """
61
+ Compute the DMD loss (eq 7 in https://arxiv.org/abs/2311.18828).
62
+ Input:
63
+ - image_or_video: a tensor with shape [B, F, C, H, W] where the number of frame is 1 for images.
64
+ - conditional_dict: a dictionary containing the conditional information (e.g. text embeddings, image embeddings).
65
+ - unconditional_dict: a dictionary containing the unconditional information (e.g. null/negative text embeddings, null/negative image embeddings).
66
+ - gradient_mask: a boolean tensor with the same shape as image_or_video indicating which pixels to compute loss .
67
+ Output:
68
+ - dmd_loss: a scalar tensor representing the DMD loss.
69
+ - dmd_log_dict: a dictionary containing the intermediate tensors for logging.
70
+ """
71
+ original_latent = image_or_video
72
+
73
+ batch_size, num_frame = image_or_video.shape[:2]
74
+
75
+ # Step 1: Randomly sample timestep based on the given schedule and corresponding noise
76
+ min_timestep = (
77
+ denoised_timestep_to
78
+ if self.ts_schedule and denoised_timestep_to is not None
79
+ else self.min_score_timestep
80
+ )
81
+ max_timestep = (
82
+ denoised_timestep_from
83
+ if self.ts_schedule_max and denoised_timestep_from is not None
84
+ else self.num_train_timestep
85
+ )
86
+ timestep = self._get_timestep(
87
+ min_timestep,
88
+ max_timestep,
89
+ batch_size,
90
+ num_frame,
91
+ self.num_frame_per_block,
92
+ uniform_timestep=True,
93
+ )
94
+
95
+ if self.timestep_shift > 1:
96
+ timestep = (
97
+ self.timestep_shift
98
+ * (timestep / 1000)
99
+ / (1 + (self.timestep_shift - 1) * (timestep / 1000))
100
+ * 1000
101
+ )
102
+ timestep = timestep.clamp(self.min_step, self.max_step)
103
+
104
+ noise = torch.randn_like(image_or_video)
105
+ noisy_latent = self.scheduler.add_noise(
106
+ image_or_video.flatten(0, 1),
107
+ noise.flatten(0, 1),
108
+ timestep.flatten(0, 1),
109
+ ).unflatten(0, (batch_size, num_frame))
110
+
111
+ # Step 2: SiD (May be wrap it?)
112
+ noisy_image_or_video = noisy_latent
113
+ # Step 2.1: Compute the fake score
114
+ _, pred_fake_image = self.fake_score(
115
+ noisy_image_or_video=noisy_image_or_video,
116
+ conditional_dict=conditional_dict,
117
+ timestep=timestep,
118
+ )
119
+ # Step 2.2: Compute the real score
120
+ # We compute the conditional and unconditional prediction
121
+ # and add them together to achieve cfg (https://arxiv.org/abs/2207.12598)
122
+ # NOTE: This step may cause OOM issue, which can be addressed by the CFG-free technique
123
+
124
+ _, pred_real_image_cond = self.real_score(
125
+ noisy_image_or_video=noisy_image_or_video,
126
+ conditional_dict=conditional_dict,
127
+ timestep=timestep,
128
+ )
129
+
130
+ _, pred_real_image_uncond = self.real_score(
131
+ noisy_image_or_video=noisy_image_or_video,
132
+ conditional_dict=unconditional_dict,
133
+ timestep=timestep,
134
+ )
135
+
136
+ pred_real_image = (
137
+ pred_real_image_cond
138
+ + (pred_real_image_cond - pred_real_image_uncond) * self.real_guidance_scale
139
+ )
140
+
141
+ # Step 2.3: SiD Loss
142
+ # TODO: Add alpha
143
+ # TODO: Double?
144
+ sid_loss = (pred_real_image.double() - pred_fake_image.double()) * (
145
+ (pred_real_image.double() - original_latent.double())
146
+ - self.sid_alpha * (pred_real_image.double() - pred_fake_image.double())
147
+ )
148
+
149
+ # Step 2.4: Loss normalizer
150
+ with torch.no_grad():
151
+ p_real = original_latent - pred_real_image
152
+ normalizer = torch.abs(p_real).mean(dim=[1, 2, 3, 4], keepdim=True)
153
+ sid_loss = sid_loss / normalizer
154
+
155
+ sid_loss = torch.nan_to_num(sid_loss)
156
+ num_frame = sid_loss.shape[1]
157
+ sid_loss = sid_loss.mean()
158
+
159
+ sid_log_dict = {
160
+ "dmdtrain_gradient_norm": torch.zeros_like(sid_loss),
161
+ "timestep": timestep.detach(),
162
+ }
163
+
164
+ return sid_loss, sid_log_dict
165
+
166
+ def generator_loss(
167
+ self,
168
+ image_or_video_shape,
169
+ conditional_dict: dict,
170
+ unconditional_dict: dict,
171
+ clean_latent: torch.Tensor,
172
+ initial_latent: torch.Tensor = None,
173
+ ) -> Tuple[torch.Tensor, dict]:
174
+ """
175
+ Generate image/videos from noise and compute the DMD loss.
176
+ The noisy input to the generator is backward simulated.
177
+ This removes the need of any datasets during distillation.
178
+ See Sec 4.5 of the DMD2 paper (https://arxiv.org/abs/2405.14867) for details.
179
+ Input:
180
+ - image_or_video_shape: a list containing the shape of the image or video [B, F, C, H, W].
181
+ - conditional_dict: a dictionary containing the conditional information (e.g. text embeddings, image embeddings).
182
+ - unconditional_dict: a dictionary containing the unconditional information (e.g. null/negative text embeddings, null/negative image embeddings).
183
+ - clean_latent: a tensor containing the clean latents [B, F, C, H, W]. Need to be passed when no backward simulation is used.
184
+ Output:
185
+ - loss: a scalar tensor representing the generator loss.
186
+ - generator_log_dict: a dictionary containing the intermediate tensors for logging.
187
+ """
188
+ # Step 1: Unroll generator to obtain fake videos
189
+ (
190
+ pred_image,
191
+ gradient_mask,
192
+ denoised_timestep_from,
193
+ denoised_timestep_to,
194
+ ) = self._run_generator(
195
+ image_or_video_shape=image_or_video_shape,
196
+ conditional_dict=conditional_dict,
197
+ initial_latent=initial_latent,
198
+ )
199
+
200
+ # Step 2: Compute the DMD loss
201
+ dmd_loss, dmd_log_dict = self.compute_distribution_matching_loss(
202
+ image_or_video=pred_image,
203
+ conditional_dict=conditional_dict,
204
+ unconditional_dict=unconditional_dict,
205
+ gradient_mask=gradient_mask,
206
+ denoised_timestep_from=denoised_timestep_from,
207
+ denoised_timestep_to=denoised_timestep_to,
208
+ )
209
+
210
+ return dmd_loss, dmd_log_dict
211
+
212
+ def critic_loss(
213
+ self,
214
+ image_or_video_shape,
215
+ conditional_dict: dict,
216
+ unconditional_dict: dict,
217
+ clean_latent: torch.Tensor,
218
+ initial_latent: torch.Tensor = None,
219
+ ) -> Tuple[torch.Tensor, dict]:
220
+ """
221
+ Generate image/videos from noise and train the critic with generated samples.
222
+ The noisy input to the generator is backward simulated.
223
+ This removes the need of any datasets during distillation.
224
+ See Sec 4.5 of the DMD2 paper (https://arxiv.org/abs/2405.14867) for details.
225
+ Input:
226
+ - image_or_video_shape: a list containing the shape of the image or video [B, F, C, H, W].
227
+ - conditional_dict: a dictionary containing the conditional information (e.g. text embeddings, image embeddings).
228
+ - unconditional_dict: a dictionary containing the unconditional information (e.g. null/negative text embeddings, null/negative image embeddings).
229
+ - clean_latent: a tensor containing the clean latents [B, F, C, H, W]. Need to be passed when no backward simulation is used.
230
+ Output:
231
+ - loss: a scalar tensor representing the generator loss.
232
+ - critic_log_dict: a dictionary containing the intermediate tensors for logging.
233
+ """
234
+
235
+ # Step 1: Run generator on backward simulated noisy input
236
+ with torch.no_grad():
237
+ generated_image, _, denoised_timestep_from, denoised_timestep_to = self._run_generator(
238
+ image_or_video_shape=image_or_video_shape,
239
+ conditional_dict=conditional_dict,
240
+ initial_latent=initial_latent,
241
+ )
242
+
243
+ # Step 2: Compute the fake prediction
244
+ min_timestep = (
245
+ denoised_timestep_to
246
+ if self.ts_schedule and denoised_timestep_to is not None
247
+ else self.min_score_timestep
248
+ )
249
+ max_timestep = (
250
+ denoised_timestep_from
251
+ if self.ts_schedule_max and denoised_timestep_from is not None
252
+ else self.num_train_timestep
253
+ )
254
+ critic_timestep = self._get_timestep(
255
+ min_timestep,
256
+ max_timestep,
257
+ image_or_video_shape[0],
258
+ image_or_video_shape[1],
259
+ self.num_frame_per_block,
260
+ uniform_timestep=True,
261
+ )
262
+
263
+ if self.timestep_shift > 1:
264
+ critic_timestep = (
265
+ self.timestep_shift
266
+ * (critic_timestep / 1000)
267
+ / (1 + (self.timestep_shift - 1) * (critic_timestep / 1000))
268
+ * 1000
269
+ )
270
+
271
+ critic_timestep = critic_timestep.clamp(self.min_step, self.max_step)
272
+
273
+ critic_noise = torch.randn_like(generated_image)
274
+ noisy_generated_image = self.scheduler.add_noise(
275
+ generated_image.flatten(0, 1),
276
+ critic_noise.flatten(0, 1),
277
+ critic_timestep.flatten(0, 1),
278
+ ).unflatten(0, image_or_video_shape[:2])
279
+
280
+ _, pred_fake_image = self.fake_score(
281
+ noisy_image_or_video=noisy_generated_image,
282
+ conditional_dict=conditional_dict,
283
+ timestep=critic_timestep,
284
+ )
285
+
286
+ # Step 3: Compute the denoising loss for the fake critic
287
+ if self.args.denoising_loss_type == "flow":
288
+ from utils.wan_wrapper import WanDiffusionWrapper
289
+
290
+ flow_pred = WanDiffusionWrapper._convert_x0_to_flow_pred(
291
+ scheduler=self.scheduler,
292
+ x0_pred=pred_fake_image.flatten(0, 1),
293
+ xt=noisy_generated_image.flatten(0, 1),
294
+ timestep=critic_timestep.flatten(0, 1),
295
+ )
296
+ pred_fake_noise = None
297
+ else:
298
+ flow_pred = None
299
+ pred_fake_noise = self.scheduler.convert_x0_to_noise(
300
+ x0=pred_fake_image.flatten(0, 1),
301
+ xt=noisy_generated_image.flatten(0, 1),
302
+ timestep=critic_timestep.flatten(0, 1),
303
+ ).unflatten(0, image_or_video_shape[:2])
304
+
305
+ denoising_loss = self.denoising_loss_func(
306
+ x=generated_image.flatten(0, 1),
307
+ x_pred=pred_fake_image.flatten(0, 1),
308
+ noise=critic_noise.flatten(0, 1),
309
+ noise_pred=pred_fake_noise,
310
+ alphas_cumprod=self.scheduler.alphas_cumprod,
311
+ timestep=critic_timestep.flatten(0, 1),
312
+ flow_pred=flow_pred,
313
+ )
314
+
315
+ # Step 5: Debugging Log
316
+ critic_log_dict = {"critic_timestep": critic_timestep.detach()}
317
+
318
+ return denoising_loss, critic_log_dict
offline_run.py ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ from pathlib import Path
3
+
4
+ import torch
5
+ from hydra import compose, initialize
6
+ from hydra.core.global_hydra import GlobalHydra
7
+ from torchvision.io import write_video
8
+
9
+ from optimize_utils import MultiTrajectory
10
+ from stream_drag_inference_wrapper import StreamDragInferenceWrapper
11
+ from utils.misc import set_seed
12
+ from video_operations import run_optimization, save_videos
13
+
14
+
15
+ def build_stream_drag_inference(
16
+ config_dir: str,
17
+ config_name: str,
18
+ checkpoint_path: str,
19
+ total_generate_block_number: int,
20
+ use_ema: bool,
21
+ seed: int,
22
+ ) -> StreamDragInferenceWrapper:
23
+ if GlobalHydra.instance().is_initialized():
24
+ GlobalHydra.instance().clear()
25
+
26
+ with initialize(version_base=None, config_path=config_dir):
27
+ stream_config = compose(config_name=config_name)
28
+
29
+ return StreamDragInferenceWrapper(
30
+ stream_model_config=stream_config,
31
+ checkpoint_path=checkpoint_path,
32
+ total_generate_block_number=total_generate_block_number,
33
+ use_ema=use_ema,
34
+ seed=seed,
35
+ )
36
+
37
+
38
+ def main() -> None:
39
+ prompt_index = 4
40
+ trajectory_dir = "./saved_labels/self_forcing_dmd_vsink_stream_drag-seed42/0004-A_close-up_3D_animated_scene_of_a_short,_fluffy_mo"
41
+ start_block_index = 3
42
+ trajectory_prefix = "block_3_Animation"
43
+ config_dir = "configs"
44
+ config_name = "self_forcing_dmd_vsink_stream_drag"
45
+ checkpoint_path = "./checkpoints/self_forcing_dmd.pt"
46
+ total_generate_block_number = 36
47
+ seed = 42
48
+ fps = 8
49
+ output_dir = "outputs-editing/self_forcing_dmd_vsink_stream_drag-seed42"
50
+ use_ema = True
51
+
52
+ torch.set_grad_enabled(False)
53
+
54
+ trajectory = MultiTrajectory.load(
55
+ save_dir=trajectory_dir,
56
+ prefix=trajectory_prefix,
57
+ )
58
+
59
+ model = build_stream_drag_inference(
60
+ config_dir=config_dir,
61
+ config_name=config_name,
62
+ checkpoint_path=checkpoint_path,
63
+ total_generate_block_number=total_generate_block_number,
64
+ use_ema=use_ema,
65
+ seed=seed,
66
+ )
67
+
68
+ set_seed(seed)
69
+ model.reset()
70
+ model.inference(
71
+ start_block_index=0,
72
+ end_block_index=start_block_index,
73
+ prompt=trajectory.prompt,
74
+ )
75
+
76
+ all_video, current_video, end_block_index = run_optimization(
77
+ model=model,
78
+ trajectory=trajectory,
79
+ start_block_index=start_block_index,
80
+ )
81
+
82
+ saved_path = save_videos(
83
+ all_video=all_video,
84
+ current_video=current_video,
85
+ output_dir=Path(output_dir),
86
+ prompt_index=prompt_index,
87
+ prompt=trajectory.prompt,
88
+ start_block_index=start_block_index,
89
+ end_block_index=end_block_index,
90
+ mode=trajectory.drag_or_animation_select,
91
+ fps=fps,
92
+ )
93
+ print(str(saved_path))
94
+
95
+
96
+ if __name__ == "__main__":
97
+ main()
optimize_utils.py ADDED
@@ -0,0 +1,275 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from pathlib import Path
3
+ from attr import dataclass
4
+ import numpy as np
5
+ import torch
6
+
7
+ from tensor_utils import (
8
+ calculate_angle_from_points,
9
+ read_mask_from_file,
10
+ save_mask_to_file,
11
+ )
12
+
13
+
14
+ @dataclass
15
+ class Trajectory:
16
+ original_trajectory: dict[str, bool | list[torch.Tensor]] = None
17
+ """
18
+ trajectory is dict, keys include 'is_rotation', 'points', if translation also has 'control_points'
19
+ """
20
+ block_trajectories: list[dict[str, bool | list[torch.Tensor]]] = []
21
+ """block_num x trajectory
22
+ trajectory has keys 'is_rotation' 'deltas' 'start_point'
23
+ if is_rotation: trajectory also has 'rotation_center'
24
+ """
25
+ mask: np.ndarray = None
26
+ """
27
+ target mask for the trajectory
28
+ """
29
+
30
+ def __init__(
31
+ self,
32
+ original_trajectory: dict[str, bool | list[torch.Tensor]] = None,
33
+ mask: np.ndarray = None,
34
+ ):
35
+ self.original_trajectory = original_trajectory
36
+ self.mask = mask
37
+ if original_trajectory is not None:
38
+ self.block_trajectories = self.original_to_block_trajectories(original_trajectory)
39
+ else:
40
+ self.block_trajectories = []
41
+
42
+ @staticmethod
43
+ def original_to_block_trajectories(
44
+ original_trajectory: dict[str, bool | list[torch.Tensor]],
45
+ block_length: int = 3,
46
+ ) -> list[dict[str, bool | list[torch.Tensor]]]:
47
+ """Convert an original trajectory (with 'points') into per-block trajectories (with 'deltas').
48
+
49
+ For translation:
50
+ deltas[i] = points[i+1] - points[0] (displacement from start)
51
+ Each block gets `block_length` consecutive deltas.
52
+
53
+ For rotation:
54
+ points[0] is the rotation center.
55
+ deltas[i] = angle(center, points[1], points[i+2])
56
+ Each block gets `block_length` consecutive deltas,
57
+ plus 'rotation_center' and 'start_point'.
58
+ """
59
+ is_rotation = original_trajectory.get("is_rotation", False)
60
+ points = original_trajectory.get("points", [])
61
+
62
+ if is_rotation:
63
+ # points[0] = rotation center, points[1] = start arm, points[2:] = subsequent arms
64
+ if len(points) < 2:
65
+ return []
66
+ rotation_center = points[0]
67
+ start_point = points[1]
68
+ deltas = [
69
+ calculate_angle_from_points(
70
+ rotation_center,
71
+ start_point,
72
+ point,
73
+ )
74
+ for point in points[2:]
75
+ ]
76
+ else:
77
+ # Translation: points[0] = start, points[1:] = subsequent positions
78
+ if len(points) < 1:
79
+ return []
80
+ start_point = points[0]
81
+ deltas = [torch.Tensor(point) - torch.Tensor(start_point) for point in points[1:]]
82
+
83
+ block_trajectories = []
84
+ for i in range(0, len(deltas), block_length):
85
+ block_traj = {
86
+ "is_rotation": is_rotation,
87
+ "deltas": deltas[i : i + block_length],
88
+ "start_point": start_point,
89
+ }
90
+ if is_rotation:
91
+ block_traj["rotation_center"] = rotation_center
92
+ block_trajectories.append(block_traj)
93
+ return block_trajectories
94
+
95
+ def set_original_trajectory(
96
+ self,
97
+ original_trajectory: dict[str, bool | list[torch.Tensor]] = None,
98
+ ):
99
+ self.original_trajectory = original_trajectory
100
+ if original_trajectory is not None:
101
+ self.block_trajectories = self.original_to_block_trajectories(original_trajectory)
102
+ else:
103
+ self.block_trajectories = []
104
+
105
+ @staticmethod
106
+ def _serialize_value(
107
+ v,
108
+ ):
109
+ """Recursively serialize a value to JSON-compatible types."""
110
+ if isinstance(v, torch.Tensor):
111
+ return v.tolist()
112
+ elif isinstance(v, np.ndarray):
113
+ return v.tolist()
114
+ elif isinstance(v, dict):
115
+ return {k: Trajectory._serialize_value(val) for k, val in v.items()}
116
+ elif isinstance(v, list):
117
+ return [Trajectory._serialize_value(item) for item in v]
118
+ else:
119
+ return v
120
+
121
+ def to_dict(
122
+ self,
123
+ mask_filename: str = None,
124
+ ) -> dict:
125
+ """Convert the Trajectory to a JSON-serializable dictionary.
126
+
127
+ Args:
128
+ mask_filename: If provided, store this filename instead of the mask array.
129
+ """
130
+ result = {}
131
+
132
+ if self.original_trajectory is not None:
133
+ result["original_trajectory"] = self._serialize_value(self.original_trajectory)
134
+ else:
135
+ result["original_trajectory"] = None
136
+
137
+ result["block_trajectories"] = self._serialize_value(self.block_trajectories)
138
+
139
+ if mask_filename is not None:
140
+ result["mask_file"] = mask_filename
141
+
142
+ return result
143
+
144
+ def save_mask(
145
+ self,
146
+ save_path: Path,
147
+ ) -> None:
148
+ """Save the mask as a PNG image."""
149
+ if self.mask is not None:
150
+ save_mask_to_file(self.mask, save_path)
151
+
152
+ @staticmethod
153
+ def load(
154
+ data: dict,
155
+ save_dir: Path,
156
+ ) -> "Trajectory":
157
+ """Load a Trajectory from a dictionary and directory."""
158
+ traj = Trajectory()
159
+ traj.original_trajectory = data.get("original_trajectory", None)
160
+ traj.block_trajectories = data.get("block_trajectories", [])
161
+ mask_file = data.get("mask_file", None)
162
+ if mask_file is not None:
163
+ traj.mask = read_mask_from_file(save_dir / mask_file)
164
+ return traj
165
+
166
+
167
+ @dataclass
168
+ class MultiTrajectory:
169
+ block_number: int = 1
170
+ prompt: str = ""
171
+ drag_or_animation_select: str = "Drag"
172
+ trajectories: list[Trajectory] = []
173
+ """
174
+ multiple trajectories for a single prompt, each trajectory has its own mask
175
+ """
176
+ movable_mask: np.ndarray = None
177
+ """
178
+ the movable area mask for the whole image
179
+ """
180
+
181
+ def save(
182
+ self,
183
+ save_dir: str | Path,
184
+ prefix: str = "multi_traj",
185
+ ) -> Path:
186
+ """Save the MultiTrajectory to a directory.
187
+
188
+ Masks are saved as PNG images, and metadata is saved as a JSON file.
189
+
190
+ Args:
191
+ save_dir: Directory to save files into.
192
+ prefix: Filename prefix for all saved files.
193
+
194
+ Returns:
195
+ Path to the saved JSON file.
196
+ """
197
+ save_dir = Path(save_dir)
198
+ save_dir.mkdir(parents=True, exist_ok=True)
199
+
200
+ result = {
201
+ "block_number": self.block_number,
202
+ "prompt": self.prompt,
203
+ "drag_or_animation_select": self.drag_or_animation_select,
204
+ }
205
+
206
+ # Save movable_mask
207
+ if self.movable_mask is not None:
208
+ movable_mask_filename = f"{prefix}_movable_mask.png"
209
+ save_mask_to_file(self.movable_mask, save_dir / movable_mask_filename)
210
+ result["movable_area_mask_file"] = movable_mask_filename
211
+ else:
212
+ result["movable_area_mask_file"] = None
213
+
214
+ # Save each trajectory and its mask
215
+ traj_dicts = []
216
+ if self.trajectories is not None:
217
+ for i, traj in enumerate(self.trajectories):
218
+ mask_filename = None
219
+ if traj.mask is not None:
220
+ mask_filename = f"{prefix}_traj_{i}_mask.png"
221
+ traj.save_mask(save_dir / mask_filename)
222
+ traj_dicts.append(traj.to_dict(mask_filename=mask_filename))
223
+ result["trajectories"] = traj_dicts
224
+
225
+ # Write JSON
226
+ json_path = save_dir / f"{prefix}_trajectory.json"
227
+ with open(json_path, "w") as f:
228
+ json.dump(result, f, indent=2)
229
+
230
+ return json_path
231
+
232
+ @staticmethod
233
+ def load(
234
+ save_dir: str | Path,
235
+ prefix: str = "multi_traj",
236
+ ) -> "MultiTrajectory":
237
+ """Load a MultiTrajectory from a directory.
238
+
239
+ Args:
240
+ save_dir: Directory containing the saved files.
241
+ prefix: Filename prefix used when saving.
242
+
243
+ Returns:
244
+ The loaded MultiTrajectory instance.
245
+ """
246
+ save_dir = Path(save_dir)
247
+ json_path = save_dir / f"{prefix}_trajectory.json"
248
+
249
+ with open(json_path, "r") as f:
250
+ data = json.load(f)
251
+
252
+ mt = MultiTrajectory()
253
+ mt.block_number = data.get("block_number", 1)
254
+ mt.prompt = data.get("prompt", "")
255
+ mt.drag_or_animation_select = data.get("drag_or_animation_select", "Drag")
256
+ # Load movable_mask
257
+ movable_file = data.get("movable_area_mask_file", None)
258
+ if movable_file is not None:
259
+ mt.movable_mask = read_mask_from_file(save_dir / movable_file)
260
+
261
+ # Load trajectories
262
+ mt.trajectories = []
263
+ for traj_data in data.get("trajectories", []):
264
+ mt.trajectories.append(Trajectory.load(traj_data, save_dir))
265
+
266
+ return mt
267
+
268
+
269
+ def transpose_dict_2d(d):
270
+ """Transpose a 2D dict: dict[key1][key2] -> dict[key2][key1]."""
271
+ result = {}
272
+ for key1, inner in d.items():
273
+ for key2, item in inner.items():
274
+ result.setdefault(key2, {})[key1] = item
275
+ return result
palette.py ADDED
@@ -0,0 +1,774 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+
3
+ _palette = [
4
+ 0,
5
+ 0,
6
+ 0,
7
+ 128,
8
+ 0,
9
+ 0,
10
+ 0,
11
+ 128,
12
+ 0,
13
+ 128,
14
+ 128,
15
+ 0,
16
+ 0,
17
+ 0,
18
+ 128,
19
+ 128,
20
+ 0,
21
+ 128,
22
+ 0,
23
+ 128,
24
+ 128,
25
+ 128,
26
+ 128,
27
+ 128,
28
+ 64,
29
+ 0,
30
+ 0,
31
+ 191,
32
+ 0,
33
+ 0,
34
+ 64,
35
+ 128,
36
+ 0,
37
+ 191,
38
+ 128,
39
+ 0,
40
+ 64,
41
+ 0,
42
+ 128,
43
+ 191,
44
+ 0,
45
+ 128,
46
+ 64,
47
+ 128,
48
+ 128,
49
+ 191,
50
+ 128,
51
+ 128,
52
+ 0,
53
+ 64,
54
+ 0,
55
+ 128,
56
+ 64,
57
+ 0,
58
+ 0,
59
+ 191,
60
+ 0,
61
+ 128,
62
+ 191,
63
+ 0,
64
+ 0,
65
+ 64,
66
+ 128,
67
+ 128,
68
+ 64,
69
+ 128,
70
+ 22,
71
+ 22,
72
+ 22,
73
+ 23,
74
+ 23,
75
+ 23,
76
+ 24,
77
+ 24,
78
+ 24,
79
+ 25,
80
+ 25,
81
+ 25,
82
+ 26,
83
+ 26,
84
+ 26,
85
+ 27,
86
+ 27,
87
+ 27,
88
+ 28,
89
+ 28,
90
+ 28,
91
+ 29,
92
+ 29,
93
+ 29,
94
+ 30,
95
+ 30,
96
+ 30,
97
+ 31,
98
+ 31,
99
+ 31,
100
+ 32,
101
+ 32,
102
+ 32,
103
+ 33,
104
+ 33,
105
+ 33,
106
+ 34,
107
+ 34,
108
+ 34,
109
+ 35,
110
+ 35,
111
+ 35,
112
+ 36,
113
+ 36,
114
+ 36,
115
+ 37,
116
+ 37,
117
+ 37,
118
+ 38,
119
+ 38,
120
+ 38,
121
+ 39,
122
+ 39,
123
+ 39,
124
+ 40,
125
+ 40,
126
+ 40,
127
+ 41,
128
+ 41,
129
+ 41,
130
+ 42,
131
+ 42,
132
+ 42,
133
+ 43,
134
+ 43,
135
+ 43,
136
+ 44,
137
+ 44,
138
+ 44,
139
+ 45,
140
+ 45,
141
+ 45,
142
+ 46,
143
+ 46,
144
+ 46,
145
+ 47,
146
+ 47,
147
+ 47,
148
+ 48,
149
+ 48,
150
+ 48,
151
+ 49,
152
+ 49,
153
+ 49,
154
+ 50,
155
+ 50,
156
+ 50,
157
+ 51,
158
+ 51,
159
+ 51,
160
+ 52,
161
+ 52,
162
+ 52,
163
+ 53,
164
+ 53,
165
+ 53,
166
+ 54,
167
+ 54,
168
+ 54,
169
+ 55,
170
+ 55,
171
+ 55,
172
+ 56,
173
+ 56,
174
+ 56,
175
+ 57,
176
+ 57,
177
+ 57,
178
+ 58,
179
+ 58,
180
+ 58,
181
+ 59,
182
+ 59,
183
+ 59,
184
+ 60,
185
+ 60,
186
+ 60,
187
+ 61,
188
+ 61,
189
+ 61,
190
+ 62,
191
+ 62,
192
+ 62,
193
+ 63,
194
+ 63,
195
+ 63,
196
+ 64,
197
+ 64,
198
+ 64,
199
+ 65,
200
+ 65,
201
+ 65,
202
+ 66,
203
+ 66,
204
+ 66,
205
+ 67,
206
+ 67,
207
+ 67,
208
+ 68,
209
+ 68,
210
+ 68,
211
+ 69,
212
+ 69,
213
+ 69,
214
+ 70,
215
+ 70,
216
+ 70,
217
+ 71,
218
+ 71,
219
+ 71,
220
+ 72,
221
+ 72,
222
+ 72,
223
+ 73,
224
+ 73,
225
+ 73,
226
+ 74,
227
+ 74,
228
+ 74,
229
+ 75,
230
+ 75,
231
+ 75,
232
+ 76,
233
+ 76,
234
+ 76,
235
+ 77,
236
+ 77,
237
+ 77,
238
+ 78,
239
+ 78,
240
+ 78,
241
+ 79,
242
+ 79,
243
+ 79,
244
+ 80,
245
+ 80,
246
+ 80,
247
+ 81,
248
+ 81,
249
+ 81,
250
+ 82,
251
+ 82,
252
+ 82,
253
+ 83,
254
+ 83,
255
+ 83,
256
+ 84,
257
+ 84,
258
+ 84,
259
+ 85,
260
+ 85,
261
+ 85,
262
+ 86,
263
+ 86,
264
+ 86,
265
+ 87,
266
+ 87,
267
+ 87,
268
+ 88,
269
+ 88,
270
+ 88,
271
+ 89,
272
+ 89,
273
+ 89,
274
+ 90,
275
+ 90,
276
+ 90,
277
+ 91,
278
+ 91,
279
+ 91,
280
+ 92,
281
+ 92,
282
+ 92,
283
+ 93,
284
+ 93,
285
+ 93,
286
+ 94,
287
+ 94,
288
+ 94,
289
+ 95,
290
+ 95,
291
+ 95,
292
+ 96,
293
+ 96,
294
+ 96,
295
+ 97,
296
+ 97,
297
+ 97,
298
+ 98,
299
+ 98,
300
+ 98,
301
+ 99,
302
+ 99,
303
+ 99,
304
+ 100,
305
+ 100,
306
+ 100,
307
+ 101,
308
+ 101,
309
+ 101,
310
+ 102,
311
+ 102,
312
+ 102,
313
+ 103,
314
+ 103,
315
+ 103,
316
+ 104,
317
+ 104,
318
+ 104,
319
+ 105,
320
+ 105,
321
+ 105,
322
+ 106,
323
+ 106,
324
+ 106,
325
+ 107,
326
+ 107,
327
+ 107,
328
+ 108,
329
+ 108,
330
+ 108,
331
+ 109,
332
+ 109,
333
+ 109,
334
+ 110,
335
+ 110,
336
+ 110,
337
+ 111,
338
+ 111,
339
+ 111,
340
+ 112,
341
+ 112,
342
+ 112,
343
+ 113,
344
+ 113,
345
+ 113,
346
+ 114,
347
+ 114,
348
+ 114,
349
+ 115,
350
+ 115,
351
+ 115,
352
+ 116,
353
+ 116,
354
+ 116,
355
+ 117,
356
+ 117,
357
+ 117,
358
+ 118,
359
+ 118,
360
+ 118,
361
+ 119,
362
+ 119,
363
+ 119,
364
+ 120,
365
+ 120,
366
+ 120,
367
+ 121,
368
+ 121,
369
+ 121,
370
+ 122,
371
+ 122,
372
+ 122,
373
+ 123,
374
+ 123,
375
+ 123,
376
+ 124,
377
+ 124,
378
+ 124,
379
+ 125,
380
+ 125,
381
+ 125,
382
+ 126,
383
+ 126,
384
+ 126,
385
+ 127,
386
+ 127,
387
+ 127,
388
+ 128,
389
+ 128,
390
+ 128,
391
+ 129,
392
+ 129,
393
+ 129,
394
+ 130,
395
+ 130,
396
+ 130,
397
+ 131,
398
+ 131,
399
+ 131,
400
+ 132,
401
+ 132,
402
+ 132,
403
+ 133,
404
+ 133,
405
+ 133,
406
+ 134,
407
+ 134,
408
+ 134,
409
+ 135,
410
+ 135,
411
+ 135,
412
+ 136,
413
+ 136,
414
+ 136,
415
+ 137,
416
+ 137,
417
+ 137,
418
+ 138,
419
+ 138,
420
+ 138,
421
+ 139,
422
+ 139,
423
+ 139,
424
+ 140,
425
+ 140,
426
+ 140,
427
+ 141,
428
+ 141,
429
+ 141,
430
+ 142,
431
+ 142,
432
+ 142,
433
+ 143,
434
+ 143,
435
+ 143,
436
+ 144,
437
+ 144,
438
+ 144,
439
+ 145,
440
+ 145,
441
+ 145,
442
+ 146,
443
+ 146,
444
+ 146,
445
+ 147,
446
+ 147,
447
+ 147,
448
+ 148,
449
+ 148,
450
+ 148,
451
+ 149,
452
+ 149,
453
+ 149,
454
+ 150,
455
+ 150,
456
+ 150,
457
+ 151,
458
+ 151,
459
+ 151,
460
+ 152,
461
+ 152,
462
+ 152,
463
+ 153,
464
+ 153,
465
+ 153,
466
+ 154,
467
+ 154,
468
+ 154,
469
+ 155,
470
+ 155,
471
+ 155,
472
+ 156,
473
+ 156,
474
+ 156,
475
+ 157,
476
+ 157,
477
+ 157,
478
+ 158,
479
+ 158,
480
+ 158,
481
+ 159,
482
+ 159,
483
+ 159,
484
+ 160,
485
+ 160,
486
+ 160,
487
+ 161,
488
+ 161,
489
+ 161,
490
+ 162,
491
+ 162,
492
+ 162,
493
+ 163,
494
+ 163,
495
+ 163,
496
+ 164,
497
+ 164,
498
+ 164,
499
+ 165,
500
+ 165,
501
+ 165,
502
+ 166,
503
+ 166,
504
+ 166,
505
+ 167,
506
+ 167,
507
+ 167,
508
+ 168,
509
+ 168,
510
+ 168,
511
+ 169,
512
+ 169,
513
+ 169,
514
+ 170,
515
+ 170,
516
+ 170,
517
+ 171,
518
+ 171,
519
+ 171,
520
+ 172,
521
+ 172,
522
+ 172,
523
+ 173,
524
+ 173,
525
+ 173,
526
+ 174,
527
+ 174,
528
+ 174,
529
+ 175,
530
+ 175,
531
+ 175,
532
+ 176,
533
+ 176,
534
+ 176,
535
+ 177,
536
+ 177,
537
+ 177,
538
+ 178,
539
+ 178,
540
+ 178,
541
+ 179,
542
+ 179,
543
+ 179,
544
+ 180,
545
+ 180,
546
+ 180,
547
+ 181,
548
+ 181,
549
+ 181,
550
+ 182,
551
+ 182,
552
+ 182,
553
+ 183,
554
+ 183,
555
+ 183,
556
+ 184,
557
+ 184,
558
+ 184,
559
+ 185,
560
+ 185,
561
+ 185,
562
+ 186,
563
+ 186,
564
+ 186,
565
+ 187,
566
+ 187,
567
+ 187,
568
+ 188,
569
+ 188,
570
+ 188,
571
+ 189,
572
+ 189,
573
+ 189,
574
+ 190,
575
+ 190,
576
+ 190,
577
+ 191,
578
+ 191,
579
+ 191,
580
+ 192,
581
+ 192,
582
+ 192,
583
+ 193,
584
+ 193,
585
+ 193,
586
+ 194,
587
+ 194,
588
+ 194,
589
+ 195,
590
+ 195,
591
+ 195,
592
+ 196,
593
+ 196,
594
+ 196,
595
+ 197,
596
+ 197,
597
+ 197,
598
+ 198,
599
+ 198,
600
+ 198,
601
+ 199,
602
+ 199,
603
+ 199,
604
+ 200,
605
+ 200,
606
+ 200,
607
+ 201,
608
+ 201,
609
+ 201,
610
+ 202,
611
+ 202,
612
+ 202,
613
+ 203,
614
+ 203,
615
+ 203,
616
+ 204,
617
+ 204,
618
+ 204,
619
+ 205,
620
+ 205,
621
+ 205,
622
+ 206,
623
+ 206,
624
+ 206,
625
+ 207,
626
+ 207,
627
+ 207,
628
+ 208,
629
+ 208,
630
+ 208,
631
+ 209,
632
+ 209,
633
+ 209,
634
+ 210,
635
+ 210,
636
+ 210,
637
+ 211,
638
+ 211,
639
+ 211,
640
+ 212,
641
+ 212,
642
+ 212,
643
+ 213,
644
+ 213,
645
+ 213,
646
+ 214,
647
+ 214,
648
+ 214,
649
+ 215,
650
+ 215,
651
+ 215,
652
+ 216,
653
+ 216,
654
+ 216,
655
+ 217,
656
+ 217,
657
+ 217,
658
+ 218,
659
+ 218,
660
+ 218,
661
+ 219,
662
+ 219,
663
+ 219,
664
+ 220,
665
+ 220,
666
+ 220,
667
+ 221,
668
+ 221,
669
+ 221,
670
+ 222,
671
+ 222,
672
+ 222,
673
+ 223,
674
+ 223,
675
+ 223,
676
+ 224,
677
+ 224,
678
+ 224,
679
+ 225,
680
+ 225,
681
+ 225,
682
+ 226,
683
+ 226,
684
+ 226,
685
+ 227,
686
+ 227,
687
+ 227,
688
+ 228,
689
+ 228,
690
+ 228,
691
+ 229,
692
+ 229,
693
+ 229,
694
+ 230,
695
+ 230,
696
+ 230,
697
+ 231,
698
+ 231,
699
+ 231,
700
+ 232,
701
+ 232,
702
+ 232,
703
+ 233,
704
+ 233,
705
+ 233,
706
+ 234,
707
+ 234,
708
+ 234,
709
+ 235,
710
+ 235,
711
+ 235,
712
+ 236,
713
+ 236,
714
+ 236,
715
+ 237,
716
+ 237,
717
+ 237,
718
+ 238,
719
+ 238,
720
+ 238,
721
+ 239,
722
+ 239,
723
+ 239,
724
+ 240,
725
+ 240,
726
+ 240,
727
+ 241,
728
+ 241,
729
+ 241,
730
+ 242,
731
+ 242,
732
+ 242,
733
+ 243,
734
+ 243,
735
+ 243,
736
+ 244,
737
+ 244,
738
+ 244,
739
+ 245,
740
+ 245,
741
+ 245,
742
+ 246,
743
+ 246,
744
+ 246,
745
+ 247,
746
+ 247,
747
+ 247,
748
+ 248,
749
+ 248,
750
+ 248,
751
+ 249,
752
+ 249,
753
+ 249,
754
+ 250,
755
+ 250,
756
+ 250,
757
+ 251,
758
+ 251,
759
+ 251,
760
+ 252,
761
+ 252,
762
+ 252,
763
+ 253,
764
+ 253,
765
+ 253,
766
+ 254,
767
+ 254,
768
+ 254,
769
+ 255,
770
+ 255,
771
+ 255,
772
+ ]
773
+ _palette = np.array(_palette).reshape((-1, 3))
774
+ _palette = _palette.tolist()
pipeline/__init__.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .bidirectional_diffusion_inference import (
2
+ BidirectionalDiffusionInferencePipeline,
3
+ )
4
+ from .bidirectional_inference import BidirectionalInferencePipeline
5
+ from .causal_diffusion_inference import CausalDiffusionInferencePipeline
6
+ from .causal_inference import CausalInferencePipeline
7
+ from .self_forcing_training import SelfForcingTrainingPipeline
8
+
9
+ __all__ = [
10
+ "BidirectionalDiffusionInferencePipeline",
11
+ "BidirectionalInferencePipeline",
12
+ "CausalDiffusionInferencePipeline",
13
+ "CausalInferencePipeline",
14
+ "SelfForcingTrainingPipeline",
15
+ ]
pipeline/bidirectional_diffusion_inference.py ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from tqdm import tqdm
2
+ from typing import List
3
+ import torch
4
+
5
+ from wan.utils.fm_solvers import (
6
+ FlowDPMSolverMultistepScheduler,
7
+ get_sampling_sigmas,
8
+ retrieve_timesteps,
9
+ )
10
+ from wan.utils.fm_solvers_unipc import FlowUniPCMultistepScheduler
11
+ from utils.wan_wrapper import (
12
+ WanDiffusionWrapper,
13
+ WanTextEncoder,
14
+ WanVAEWrapper,
15
+ )
16
+
17
+
18
+ class BidirectionalDiffusionInferencePipeline(torch.nn.Module):
19
+ def __init__(
20
+ self,
21
+ args,
22
+ device,
23
+ generator=None,
24
+ text_encoder=None,
25
+ vae=None,
26
+ ):
27
+ super().__init__()
28
+ # Step 1: Initialize all models
29
+ self.generator = (
30
+ WanDiffusionWrapper(
31
+ **getattr(args, "model_kwargs", {}),
32
+ is_causal=False,
33
+ )
34
+ if generator is None
35
+ else generator
36
+ )
37
+ self.text_encoder = WanTextEncoder() if text_encoder is None else text_encoder
38
+ self.vae = WanVAEWrapper() if vae is None else vae
39
+
40
+ # Step 2: Initialize scheduler
41
+ self.num_train_timesteps = args.num_train_timestep
42
+ self.sampling_steps = 50
43
+ self.sample_solver = "unipc"
44
+ self.shift = 8.0
45
+
46
+ self.args = args
47
+
48
+ def inference(
49
+ self,
50
+ noise: torch.Tensor,
51
+ text_prompts: List[str],
52
+ return_latents=False,
53
+ ) -> torch.Tensor:
54
+ """
55
+ Perform inference on the given noise and text prompts.
56
+ Inputs:
57
+ noise (torch.Tensor): The input noise tensor of shape
58
+ (batch_size, num_frames, num_channels, height, width).
59
+ text_prompts (List[str]): The list of text prompts.
60
+ Outputs:
61
+ video (torch.Tensor): The generated video tensor of shape
62
+ (batch_size, num_frames, num_channels, height, width). It is normalized to be in the range [0, 1].
63
+ """
64
+
65
+ conditional_dict = self.text_encoder(text_prompts=text_prompts)
66
+ unconditional_dict = self.text_encoder(
67
+ text_prompts=[self.args.negative_prompt] * len(text_prompts)
68
+ )
69
+
70
+ latents = noise
71
+
72
+ sample_scheduler = self._initialize_sample_scheduler(noise)
73
+ for _, t in enumerate(tqdm(sample_scheduler.timesteps)):
74
+ latent_model_input = latents
75
+ timestep = t * torch.ones(
76
+ [latents.shape[0], 21], device=noise.device, dtype=torch.float32
77
+ )
78
+
79
+ flow_pred_cond, _ = self.generator(latent_model_input, conditional_dict, timestep)
80
+ flow_pred_uncond, _ = self.generator(latent_model_input, unconditional_dict, timestep)
81
+
82
+ flow_pred = flow_pred_uncond + self.args.guidance_scale * (
83
+ flow_pred_cond - flow_pred_uncond
84
+ )
85
+
86
+ temp_x0 = sample_scheduler.step(
87
+ flow_pred.unsqueeze(0),
88
+ t,
89
+ latents.unsqueeze(0),
90
+ return_dict=False,
91
+ )[0]
92
+ latents = temp_x0.squeeze(0)
93
+
94
+ x0 = latents
95
+ video = self.vae.decode_to_pixel(x0)
96
+ video = (video * 0.5 + 0.5).clamp(0, 1)
97
+
98
+ del sample_scheduler
99
+
100
+ if return_latents:
101
+ return video, latents
102
+ else:
103
+ return video
104
+
105
+ def _initialize_sample_scheduler(
106
+ self,
107
+ noise,
108
+ ):
109
+ if self.sample_solver == "unipc":
110
+ sample_scheduler = FlowUniPCMultistepScheduler(
111
+ num_train_timesteps=self.num_train_timesteps,
112
+ shift=1,
113
+ use_dynamic_shifting=False,
114
+ )
115
+ sample_scheduler.set_timesteps(
116
+ self.sampling_steps, device=noise.device, shift=self.shift
117
+ )
118
+ self.timesteps = sample_scheduler.timesteps
119
+ elif self.sample_solver == "dpm++":
120
+ sample_scheduler = FlowDPMSolverMultistepScheduler(
121
+ num_train_timesteps=self.num_train_timesteps,
122
+ shift=1,
123
+ use_dynamic_shifting=False,
124
+ )
125
+ sampling_sigmas = get_sampling_sigmas(self.sampling_steps, self.shift)
126
+ self.timesteps, _ = retrieve_timesteps(
127
+ sample_scheduler, device=noise.device, sigmas=sampling_sigmas
128
+ )
129
+ else:
130
+ raise NotImplementedError("Unsupported solver.")
131
+ return sample_scheduler
pipeline/bidirectional_inference.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List
2
+ import torch
3
+
4
+ from utils.wan_wrapper import WanDiffusionWrapper, WanTextEncoder, WanVAEWrapper
5
+
6
+
7
+ class BidirectionalInferencePipeline(torch.nn.Module):
8
+ def __init__(
9
+ self,
10
+ args,
11
+ device,
12
+ generator=None,
13
+ text_encoder=None,
14
+ vae=None,
15
+ ):
16
+ super().__init__()
17
+ # Step 1: Initialize all models
18
+ self.generator = (
19
+ WanDiffusionWrapper(**getattr(args, "model_kwargs", {}), is_causal=False)
20
+ if generator is None
21
+ else generator
22
+ )
23
+ self.text_encoder = WanTextEncoder() if text_encoder is None else text_encoder
24
+ self.vae = WanVAEWrapper() if vae is None else vae
25
+
26
+ # Step 2: Initialize all bidirectional wan hyperparmeters
27
+ self.scheduler = self.generator.get_scheduler()
28
+ self.denoising_step_list = torch.tensor(
29
+ args.denoising_step_list, dtype=torch.long, device=device
30
+ )
31
+ if self.denoising_step_list[-1] == 0:
32
+ self.denoising_step_list = self.denoising_step_list[
33
+ :-1
34
+ ] # remove the zero timestep for inference
35
+ if args.warp_denoising_step:
36
+ timesteps = torch.cat(
37
+ (
38
+ self.scheduler.timesteps.cpu(),
39
+ torch.tensor([0], dtype=torch.float32),
40
+ )
41
+ )
42
+ self.denoising_step_list = timesteps[1000 - self.denoising_step_list]
43
+
44
+ def inference(
45
+ self,
46
+ noise: torch.Tensor,
47
+ text_prompts: List[str],
48
+ ) -> torch.Tensor:
49
+ """
50
+ Perform inference on the given noise and text prompts.
51
+ Inputs:
52
+ noise (torch.Tensor): The input noise tensor of shape
53
+ (batch_size, num_frames, num_channels, height, width).
54
+ text_prompts (List[str]): The list of text prompts.
55
+ Outputs:
56
+ video (torch.Tensor): The generated video tensor of shape
57
+ (batch_size, num_frames, num_channels, height, width). It is normalized to be in the range [0, 1].
58
+ """
59
+ conditional_dict = self.text_encoder(text_prompts=text_prompts)
60
+
61
+ # initial point
62
+ noisy_image_or_video = noise
63
+
64
+ # use the last n-1 timesteps to simulate the generator's input
65
+ for index, current_timestep in enumerate(self.denoising_step_list[:-1]):
66
+ _, pred_image_or_video = self.generator(
67
+ noisy_image_or_video=noisy_image_or_video,
68
+ conditional_dict=conditional_dict,
69
+ timestep=torch.ones(noise.shape[:2], dtype=torch.long, device=noise.device)
70
+ * current_timestep,
71
+ ) # [B, F, C, H, W]
72
+
73
+ next_timestep = self.denoising_step_list[index + 1] * torch.ones(
74
+ noise.shape[:2], dtype=torch.long, device=noise.device
75
+ )
76
+
77
+ noisy_image_or_video = self.scheduler.add_noise(
78
+ pred_image_or_video.flatten(0, 1),
79
+ torch.randn_like(pred_image_or_video.flatten(0, 1)),
80
+ next_timestep.flatten(0, 1),
81
+ ).unflatten(0, noise.shape[:2])
82
+
83
+ video = self.vae.decode_to_pixel(pred_image_or_video)
84
+ video = (video * 0.5 + 0.5).clamp(0, 1)
85
+ return video
pipeline/causal_diffusion_inference.py ADDED
@@ -0,0 +1,402 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from tqdm import tqdm
2
+ from typing import List, Optional
3
+ import torch
4
+
5
+ from wan.utils.fm_solvers import (
6
+ FlowDPMSolverMultistepScheduler,
7
+ get_sampling_sigmas,
8
+ retrieve_timesteps,
9
+ )
10
+ from wan.utils.fm_solvers_unipc import FlowUniPCMultistepScheduler
11
+ from utils.wan_wrapper import (
12
+ WanDiffusionWrapper,
13
+ WanTextEncoder,
14
+ WanVAEWrapper,
15
+ )
16
+
17
+
18
+ class CausalDiffusionInferencePipeline(torch.nn.Module):
19
+ def __init__(
20
+ self,
21
+ args,
22
+ device,
23
+ generator=None,
24
+ text_encoder=None,
25
+ vae=None,
26
+ ):
27
+ super().__init__()
28
+ # Step 1: Initialize all models
29
+ self.generator = (
30
+ WanDiffusionWrapper(
31
+ **getattr(args, "model_kwargs", {}),
32
+ is_causal=True,
33
+ )
34
+ if generator is None
35
+ else generator
36
+ )
37
+ self.text_encoder = WanTextEncoder() if text_encoder is None else text_encoder
38
+ self.vae = WanVAEWrapper() if vae is None else vae
39
+
40
+ # Step 2: Initialize scheduler
41
+ self.num_train_timesteps = args.num_train_timestep
42
+ self.sampling_steps = 50
43
+ self.sample_solver = "unipc"
44
+ self.shift = args.timestep_shift
45
+
46
+ self.num_transformer_blocks = 30
47
+ self.frame_seq_length = 1560
48
+
49
+ self.kv_cache_pos = None
50
+ self.kv_cache_neg = None
51
+ self.crossattn_cache_pos = None
52
+ self.crossattn_cache_neg = None
53
+ self.args = args
54
+ self.num_frame_per_block = getattr(args, "num_frame_per_block", 1)
55
+ self.independent_first_frame = args.independent_first_frame
56
+ self.local_attn_size = self.generator.model.local_attn_size
57
+
58
+ print(f"KV inference with {self.num_frame_per_block} frames per block")
59
+
60
+ if self.num_frame_per_block > 1:
61
+ self.generator.model.num_frame_per_block = self.num_frame_per_block
62
+
63
+ def inference(
64
+ self,
65
+ noise: torch.Tensor,
66
+ text_prompts: List[str],
67
+ initial_latent: Optional[torch.Tensor] = None,
68
+ return_latents: bool = False,
69
+ start_frame_index: Optional[int] = 0,
70
+ ) -> torch.Tensor:
71
+ """
72
+ Perform inference on the given noise and text prompts.
73
+ Inputs:
74
+ noise (torch.Tensor): The input noise tensor of shape
75
+ (batch_size, num_output_frames, num_channels, height, width).
76
+ text_prompts (List[str]): The list of text prompts.
77
+ initial_latent (torch.Tensor): The initial latent tensor of shape
78
+ (batch_size, num_input_frames, num_channels, height, width).
79
+ If num_input_frames is 1, perform image to video.
80
+ If num_input_frames is greater than 1, perform video extension.
81
+ return_latents (bool): Whether to return the latents.
82
+ start_frame_index (int): In long video generation, where does the current window start?
83
+ Outputs:
84
+ video (torch.Tensor): The generated video tensor of shape
85
+ (batch_size, num_frames, num_channels, height, width). It is normalized to be in the range [0, 1].
86
+ """
87
+ batch_size, num_frames, num_channels, height, width = noise.shape
88
+ if not self.independent_first_frame or (
89
+ self.independent_first_frame and initial_latent is not None
90
+ ):
91
+ # If the first frame is independent and the first frame is provided, then the number of frames in the
92
+ # noise should still be a multiple of num_frame_per_block
93
+ assert num_frames % self.num_frame_per_block == 0
94
+ num_blocks = num_frames // self.num_frame_per_block
95
+ elif self.independent_first_frame and initial_latent is None:
96
+ # Using a [1, 4, 4, 4, 4, 4] model to generate a video without image conditioning
97
+ assert (num_frames - 1) % self.num_frame_per_block == 0
98
+ num_blocks = (num_frames - 1) // self.num_frame_per_block
99
+ num_input_frames = initial_latent.shape[1] if initial_latent is not None else 0
100
+ num_output_frames = num_frames + num_input_frames # add the initial latent frames
101
+ conditional_dict = self.text_encoder(text_prompts=text_prompts)
102
+ unconditional_dict = self.text_encoder(
103
+ text_prompts=[self.args.negative_prompt] * len(text_prompts)
104
+ )
105
+
106
+ output = torch.zeros(
107
+ [batch_size, num_output_frames, num_channels, height, width],
108
+ device=noise.device,
109
+ dtype=noise.dtype,
110
+ )
111
+
112
+ # Step 1: Initialize KV cache to all zeros
113
+ if self.kv_cache_pos is None:
114
+ self._initialize_kv_cache(batch_size=batch_size, dtype=noise.dtype, device=noise.device)
115
+ self._initialize_crossattn_cache(
116
+ batch_size=batch_size, dtype=noise.dtype, device=noise.device
117
+ )
118
+ else:
119
+ # reset cross attn cache
120
+ for block_index in range(self.num_transformer_blocks):
121
+ self.crossattn_cache_pos[block_index]["is_init"] = False
122
+ self.crossattn_cache_neg[block_index]["is_init"] = False
123
+ # reset kv cache
124
+ for block_index in range(len(self.kv_cache_pos)):
125
+ self.kv_cache_pos[block_index]["global_end_index"] = torch.tensor(
126
+ [0], dtype=torch.long, device=noise.device
127
+ )
128
+ self.kv_cache_pos[block_index]["local_end_index"] = torch.tensor(
129
+ [0], dtype=torch.long, device=noise.device
130
+ )
131
+ self.kv_cache_neg[block_index]["global_end_index"] = torch.tensor(
132
+ [0], dtype=torch.long, device=noise.device
133
+ )
134
+ self.kv_cache_neg[block_index]["local_end_index"] = torch.tensor(
135
+ [0], dtype=torch.long, device=noise.device
136
+ )
137
+
138
+ # Step 2: Cache context feature
139
+ current_start_frame = start_frame_index
140
+ cache_start_frame = 0
141
+ if initial_latent is not None:
142
+ timestep = torch.ones([batch_size, 1], device=noise.device, dtype=torch.int64) * 0
143
+ if self.independent_first_frame:
144
+ # Assume num_input_frames is 1 + self.num_frame_per_block * num_input_blocks
145
+ assert (num_input_frames - 1) % self.num_frame_per_block == 0
146
+ num_input_blocks = (num_input_frames - 1) // self.num_frame_per_block
147
+ output[:, :1] = initial_latent[:, :1]
148
+ self.generator(
149
+ noisy_image_or_video=initial_latent[:, :1],
150
+ conditional_dict=conditional_dict,
151
+ timestep=timestep * 0,
152
+ kv_cache=self.kv_cache_pos,
153
+ crossattn_cache=self.crossattn_cache_pos,
154
+ current_start=current_start_frame * self.frame_seq_length,
155
+ cache_start=cache_start_frame * self.frame_seq_length,
156
+ )
157
+ self.generator(
158
+ noisy_image_or_video=initial_latent[:, :1],
159
+ conditional_dict=unconditional_dict,
160
+ timestep=timestep * 0,
161
+ kv_cache=self.kv_cache_neg,
162
+ crossattn_cache=self.crossattn_cache_neg,
163
+ current_start=current_start_frame * self.frame_seq_length,
164
+ cache_start=cache_start_frame * self.frame_seq_length,
165
+ )
166
+ current_start_frame += 1
167
+ cache_start_frame += 1
168
+ else:
169
+ # Assume num_input_frames is self.num_frame_per_block * num_input_blocks
170
+ assert num_input_frames % self.num_frame_per_block == 0
171
+ num_input_blocks = num_input_frames // self.num_frame_per_block
172
+
173
+ for block_index in range(num_input_blocks):
174
+ current_ref_latents = initial_latent[
175
+ :,
176
+ cache_start_frame : cache_start_frame + self.num_frame_per_block,
177
+ ]
178
+ output[
179
+ :,
180
+ cache_start_frame : cache_start_frame + self.num_frame_per_block,
181
+ ] = current_ref_latents
182
+ self.generator(
183
+ noisy_image_or_video=current_ref_latents,
184
+ conditional_dict=conditional_dict,
185
+ timestep=timestep * 0,
186
+ kv_cache=self.kv_cache_pos,
187
+ crossattn_cache=self.crossattn_cache_pos,
188
+ current_start=current_start_frame * self.frame_seq_length,
189
+ cache_start=cache_start_frame * self.frame_seq_length,
190
+ )
191
+ self.generator(
192
+ noisy_image_or_video=current_ref_latents,
193
+ conditional_dict=unconditional_dict,
194
+ timestep=timestep * 0,
195
+ kv_cache=self.kv_cache_neg,
196
+ crossattn_cache=self.crossattn_cache_neg,
197
+ current_start=current_start_frame * self.frame_seq_length,
198
+ cache_start=cache_start_frame * self.frame_seq_length,
199
+ )
200
+ current_start_frame += self.num_frame_per_block
201
+ cache_start_frame += self.num_frame_per_block
202
+
203
+ # Step 3: Temporal denoising loop
204
+ all_num_frames = [self.num_frame_per_block] * num_blocks
205
+ if self.independent_first_frame and initial_latent is None:
206
+ all_num_frames = [1] + all_num_frames
207
+ for current_num_frames in all_num_frames:
208
+ noisy_input = noise[
209
+ :,
210
+ cache_start_frame
211
+ - num_input_frames : cache_start_frame
212
+ + current_num_frames
213
+ - num_input_frames,
214
+ ]
215
+ latents = noisy_input
216
+
217
+ # Step 3.1: Spatial denoising loop
218
+ sample_scheduler = self._initialize_sample_scheduler(noise)
219
+ for _, t in enumerate(tqdm(sample_scheduler.timesteps)):
220
+ latent_model_input = latents
221
+ timestep = t * torch.ones(
222
+ [batch_size, current_num_frames],
223
+ device=noise.device,
224
+ dtype=torch.float32,
225
+ )
226
+
227
+ flow_pred_cond, _ = self.generator(
228
+ noisy_image_or_video=latent_model_input,
229
+ conditional_dict=conditional_dict,
230
+ timestep=timestep,
231
+ kv_cache=self.kv_cache_pos,
232
+ crossattn_cache=self.crossattn_cache_pos,
233
+ current_start=current_start_frame * self.frame_seq_length,
234
+ cache_start=cache_start_frame * self.frame_seq_length,
235
+ )
236
+ flow_pred_uncond, _ = self.generator(
237
+ noisy_image_or_video=latent_model_input,
238
+ conditional_dict=unconditional_dict,
239
+ timestep=timestep,
240
+ kv_cache=self.kv_cache_neg,
241
+ crossattn_cache=self.crossattn_cache_neg,
242
+ current_start=current_start_frame * self.frame_seq_length,
243
+ cache_start=cache_start_frame * self.frame_seq_length,
244
+ )
245
+
246
+ flow_pred = flow_pred_uncond + self.args.guidance_scale * (
247
+ flow_pred_cond - flow_pred_uncond
248
+ )
249
+
250
+ temp_x0 = sample_scheduler.step(flow_pred, t, latents, return_dict=False)[0]
251
+ latents = temp_x0
252
+ print(f"kv_cache['local_end_index']: {self.kv_cache_pos[0]['local_end_index']}")
253
+ print(f"kv_cache['global_end_index']: {self.kv_cache_pos[0]['global_end_index']}")
254
+
255
+ # Step 3.2: record the model's output
256
+ output[:, cache_start_frame : cache_start_frame + current_num_frames] = latents
257
+
258
+ # Step 3.3: rerun with timestep zero to update KV cache using clean context
259
+ self.generator(
260
+ noisy_image_or_video=latents,
261
+ conditional_dict=conditional_dict,
262
+ timestep=timestep * 0,
263
+ kv_cache=self.kv_cache_pos,
264
+ crossattn_cache=self.crossattn_cache_pos,
265
+ current_start=current_start_frame * self.frame_seq_length,
266
+ cache_start=cache_start_frame * self.frame_seq_length,
267
+ )
268
+ self.generator(
269
+ noisy_image_or_video=latents,
270
+ conditional_dict=unconditional_dict,
271
+ timestep=timestep * 0,
272
+ kv_cache=self.kv_cache_neg,
273
+ crossattn_cache=self.crossattn_cache_neg,
274
+ current_start=current_start_frame * self.frame_seq_length,
275
+ cache_start=cache_start_frame * self.frame_seq_length,
276
+ )
277
+
278
+ # Step 3.4: update the start and end frame indices
279
+ current_start_frame += current_num_frames
280
+ cache_start_frame += current_num_frames
281
+
282
+ # Step 4: Decode the output
283
+ video = self.vae.decode_to_pixel(output)
284
+ video = (video * 0.5 + 0.5).clamp(0, 1)
285
+
286
+ if return_latents:
287
+ return video, output
288
+ else:
289
+ return video
290
+
291
+ def _initialize_kv_cache(
292
+ self,
293
+ batch_size,
294
+ dtype,
295
+ device,
296
+ ):
297
+ """
298
+ Initialize a Per-GPU KV cache for the Wan model.
299
+ """
300
+ kv_cache_pos = []
301
+ kv_cache_neg = []
302
+ if self.local_attn_size != -1:
303
+ # Use the local attention size to compute the KV cache size
304
+ kv_cache_size = self.local_attn_size * self.frame_seq_length
305
+ else:
306
+ # Use the default KV cache size
307
+ kv_cache_size = 32760
308
+
309
+ for _ in range(self.num_transformer_blocks):
310
+ kv_cache_pos.append(
311
+ {
312
+ "k": torch.zeros(
313
+ [batch_size, kv_cache_size, 12, 128],
314
+ dtype=dtype,
315
+ device=device,
316
+ ),
317
+ "v": torch.zeros(
318
+ [batch_size, kv_cache_size, 12, 128],
319
+ dtype=dtype,
320
+ device=device,
321
+ ),
322
+ "global_end_index": torch.tensor([0], dtype=torch.long, device=device),
323
+ "local_end_index": torch.tensor([0], dtype=torch.long, device=device),
324
+ }
325
+ )
326
+ kv_cache_neg.append(
327
+ {
328
+ "k": torch.zeros(
329
+ [batch_size, kv_cache_size, 12, 128],
330
+ dtype=dtype,
331
+ device=device,
332
+ ),
333
+ "v": torch.zeros(
334
+ [batch_size, kv_cache_size, 12, 128],
335
+ dtype=dtype,
336
+ device=device,
337
+ ),
338
+ "global_end_index": torch.tensor([0], dtype=torch.long, device=device),
339
+ "local_end_index": torch.tensor([0], dtype=torch.long, device=device),
340
+ }
341
+ )
342
+
343
+ self.kv_cache_pos = kv_cache_pos # always store the clean cache
344
+ self.kv_cache_neg = kv_cache_neg # always store the clean cache
345
+
346
+ def _initialize_crossattn_cache(
347
+ self,
348
+ batch_size,
349
+ dtype,
350
+ device,
351
+ ):
352
+ """
353
+ Initialize a Per-GPU cross-attention cache for the Wan model.
354
+ """
355
+ crossattn_cache_pos = []
356
+ crossattn_cache_neg = []
357
+ for _ in range(self.num_transformer_blocks):
358
+ crossattn_cache_pos.append(
359
+ {
360
+ "k": torch.zeros([batch_size, 512, 12, 128], dtype=dtype, device=device),
361
+ "v": torch.zeros([batch_size, 512, 12, 128], dtype=dtype, device=device),
362
+ "is_init": False,
363
+ }
364
+ )
365
+ crossattn_cache_neg.append(
366
+ {
367
+ "k": torch.zeros([batch_size, 512, 12, 128], dtype=dtype, device=device),
368
+ "v": torch.zeros([batch_size, 512, 12, 128], dtype=dtype, device=device),
369
+ "is_init": False,
370
+ }
371
+ )
372
+
373
+ self.crossattn_cache_pos = crossattn_cache_pos # always store the clean cache
374
+ self.crossattn_cache_neg = crossattn_cache_neg # always store the clean cache
375
+
376
+ def _initialize_sample_scheduler(
377
+ self,
378
+ noise,
379
+ ):
380
+ if self.sample_solver == "unipc":
381
+ sample_scheduler = FlowUniPCMultistepScheduler(
382
+ num_train_timesteps=self.num_train_timesteps,
383
+ shift=1,
384
+ use_dynamic_shifting=False,
385
+ )
386
+ sample_scheduler.set_timesteps(
387
+ self.sampling_steps, device=noise.device, shift=self.shift
388
+ )
389
+ self.timesteps = sample_scheduler.timesteps
390
+ elif self.sample_solver == "dpm++":
391
+ sample_scheduler = FlowDPMSolverMultistepScheduler(
392
+ num_train_timesteps=self.num_train_timesteps,
393
+ shift=1,
394
+ use_dynamic_shifting=False,
395
+ )
396
+ sampling_sigmas = get_sampling_sigmas(self.sampling_steps, self.shift)
397
+ self.timesteps, _ = retrieve_timesteps(
398
+ sample_scheduler, device=noise.device, sigmas=sampling_sigmas
399
+ )
400
+ else:
401
+ raise NotImplementedError("Unsupported solver.")
402
+ return sample_scheduler
pipeline/causal_inference.py ADDED
@@ -0,0 +1,1193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gc
2
+ import random
3
+ import time
4
+ from typing import List, Optional
5
+ import numpy as np
6
+ from omegaconf import DictConfig, OmegaConf
7
+ import torch
8
+
9
+ import torch.nn.functional as F
10
+ from torch.nn.utils import clip_grad_norm_
11
+ from tqdm import tqdm
12
+
13
+ from optimize_utils import transpose_dict_2d
14
+ from tensor_utils import (
15
+ build_anisotropic_gaussian_from_mask,
16
+ combine_gaussian_maps,
17
+ combine_masks_or,
18
+ normalize_tensor_to_match_tensor,
19
+ resize_tensor,
20
+ warp_tensor,
21
+ warp_tensor_sequence,
22
+ )
23
+ from utils.wan_wrapper import (
24
+ WanDiffusionWrapper,
25
+ WanTextEncoder,
26
+ WanVAEWrapper,
27
+ )
28
+
29
+ from demo_utils.memory import (
30
+ gpu,
31
+ get_cuda_free_memory_gb,
32
+ DynamicSwapInstaller,
33
+ move_model_to_device_with_memory_preservation,
34
+ )
35
+
36
+ IMAGE_HEIGHT = 480.0
37
+
38
+
39
+ def split_trajectories_segments(
40
+ trajectories: list[dict[str, bool | list[torch.Tensor]]],
41
+ translation_step: float,
42
+ rotation_step: float,
43
+ ) -> List[list[dict[str, bool | list[torch.Tensor]]]]:
44
+ """
45
+ Split drag trajectories into evenly spaced intermediate segments for
46
+ progressive (coarse-to-fine) optimization.
47
+
48
+ Given N trajectories (each with per-frame deltas), this function:
49
+ 1. Determines the maximum number of segments needed across all
50
+ trajectories based on the magnitude of their deltas and the
51
+ provided step sizes.
52
+ 2. Divides every trajectory's deltas uniformly into that many segments.
53
+ 3. Produces a list of cumulative intermediate trajectory snapshots,
54
+ where segment k contains deltas scaled by (k / max_segments).
55
+
56
+ :param trajectories:
57
+ N x trajectory dicts.
58
+ Each dict has keys:
59
+ - 'is_rotation' (bool): Whether this trajectory is a rotation.
60
+ - 'deltas' (list): Per-frame displacement values.
61
+ For translation: each delta is a 2D vector (dx, dy).
62
+ For rotation: each delta is a scalar angle.
63
+ - 'start_point': The starting pixel coordinate of the drag.
64
+ - 'rotation_center' (only if is_rotation): The center of rotation.
65
+
66
+ :param translation_step:
67
+ The pixel distance that defines one segment for translation
68
+ trajectories. Larger values produce fewer segments.
69
+
70
+ :param rotation_step:
71
+ The angle (in the same units as deltas) that defines one segment
72
+ for rotation trajectories. Larger values produce fewer segments.
73
+
74
+ :returns:
75
+ segment_num x N x trajectory dicts.
76
+ A list of length `max_segment_number`, where each element is a list
77
+ of N trajectory dicts. The k-th element (1-indexed) contains
78
+ trajectories whose deltas are scaled to (k / max_segment_number)
79
+ of the original deltas — i.e., cumulative intermediate waypoints.
80
+ """
81
+ # -------------------------------------------------------------------------
82
+ # Phase 1: Convert raw deltas to torch tensors (ensure uniform type)
83
+ # -------------------------------------------------------------------------
84
+ for trajectory in trajectories:
85
+ trajectory["deltas"] = [torch.tensor(delta, device="cpu") for delta in trajectory["deltas"]]
86
+
87
+ # -------------------------------------------------------------------------
88
+ # Phase 2: Determine the maximum number of segments across all trajectories.
89
+ # - For rotations: segment count = |angle_delta| // rotation_step
90
+ # - For translations: segment count = ||displacement_delta||₂ // translation_step
91
+ # - We take the global maximum so every trajectory is split into the
92
+ # same number of segments (ensuring synchronized progressive steps).
93
+ # -------------------------------------------------------------------------
94
+ max_segment_number = 1 # at least one segment
95
+ for trajectory in trajectories:
96
+ print(f"{trajectory['is_rotation'] = }")
97
+ for delta in trajectory["deltas"]:
98
+ if trajectory["is_rotation"]:
99
+ magnitude = abs(delta)
100
+ step = rotation_step
101
+ else:
102
+ magnitude = abs(torch.norm(delta))
103
+ step = translation_step
104
+ segment_number = int(magnitude // step)
105
+ print(f"{delta = } {magnitude = } {segment_number = }")
106
+ max_segment_number = max(max_segment_number, segment_number)
107
+ print(f"{max_segment_number = }")
108
+
109
+ # -------------------------------------------------------------------------
110
+ # Phase 3: Compute per-segment step sizes for each trajectory.
111
+ # Each trajectory's deltas are divided by max_segment_number to get
112
+ # the uniform per-segment increment.
113
+ # -------------------------------------------------------------------------
114
+ split_trajectory_steps = []
115
+ for trajectory in trajectories:
116
+ print(f"{trajectory['is_rotation'] = }")
117
+ # Divide each frame's delta by the total number of segments
118
+ trajectory_steps = [delta / float(max_segment_number) for delta in trajectory["deltas"]]
119
+ print(f"{trajectory_steps = }")
120
+ # Build the per-trajectory step metadata
121
+ split_trajectory_step = {
122
+ "is_rotation": trajectory["is_rotation"],
123
+ "steps": trajectory_steps, # per-segment increment per frame
124
+ "start_point": trajectory["start_point"],
125
+ }
126
+ if trajectory["is_rotation"]:
127
+ split_trajectory_step["rotation_center"] = trajectory["rotation_center"]
128
+ split_trajectory_steps.append(split_trajectory_step)
129
+
130
+ # -------------------------------------------------------------------------
131
+ # Phase 4: Build cumulative intermediate trajectory lists.
132
+ # For segment_index k (1-indexed from 1 to max_segment_number):
133
+ # delta_k = step * k
134
+ # This produces progressively larger displacements, enabling the
135
+ # optimizer to move features gradually toward the final target.
136
+ # -------------------------------------------------------------------------
137
+ new_trajectories_list = []
138
+ for segment_index in range(max_segment_number):
139
+ segment_index += 1 # 1-indexed: cumulative scale factor
140
+ new_trajectories = []
141
+ for trajectory_step in split_trajectory_steps:
142
+ new_trajectory = {
143
+ "is_rotation": trajectory_step["is_rotation"],
144
+ "deltas": [step * segment_index for step in trajectory_step["steps"]],
145
+ "start_point": trajectory_step["start_point"],
146
+ }
147
+ if trajectory_step["is_rotation"]:
148
+ new_trajectory["rotation_center"] = trajectory_step["rotation_center"]
149
+ new_trajectories.append(new_trajectory)
150
+ print(f"{new_trajectories = }")
151
+ new_trajectories_list.append(new_trajectories)
152
+
153
+ # Return: list of length max_segment_number, each containing N trajectory dicts
154
+ # with cumulatively scaled deltas (segment 1 = smallest, last = full original delta)
155
+ return new_trajectories_list
156
+
157
+
158
+ class CausalInferencePipeline(torch.nn.Module):
159
+ def __init__(
160
+ self,
161
+ args,
162
+ device,
163
+ generator=None,
164
+ text_encoder=None,
165
+ vae=None,
166
+ ):
167
+ super().__init__()
168
+ # Step 1: Initialize all models
169
+ self.generator = (
170
+ WanDiffusionWrapper(
171
+ **getattr(args, "model_kwargs", {}),
172
+ is_causal=True,
173
+ )
174
+ if generator is None
175
+ else generator
176
+ )
177
+ self.text_encoder = WanTextEncoder() if text_encoder is None else text_encoder
178
+ self.vae = WanVAEWrapper() if vae is None else vae
179
+
180
+ # Step 2: Initialize all causal hyperparmeters
181
+ self.scheduler = self.generator.get_scheduler()
182
+ self.denoising_step_list = torch.tensor(args.denoising_step_list, dtype=torch.long)
183
+ if args.warp_denoising_step:
184
+ timesteps = torch.cat(
185
+ (
186
+ self.scheduler.timesteps.cpu(),
187
+ torch.tensor([0], dtype=torch.float32),
188
+ )
189
+ )
190
+ self.denoising_step_list = timesteps[1000 - self.denoising_step_list]
191
+
192
+ self.num_transformer_blocks = 30
193
+ self.frame_seq_length = 1560
194
+
195
+ self.kv_cache1 = None
196
+ self.args = args
197
+ self.num_frame_per_block = getattr(args, "num_frame_per_block", 1)
198
+ self.independent_first_frame = args.independent_first_frame
199
+ self.local_attn_size = self.generator.model.local_attn_size
200
+
201
+ print(f"KV inference with {self.num_frame_per_block} frames per block")
202
+
203
+ if self.num_frame_per_block > 1:
204
+ self.generator.model.num_frame_per_block = self.num_frame_per_block
205
+
206
+ def stack_features(
207
+ self,
208
+ record_features: dict[int, torch.Tensor],
209
+ # dict[block_index] -> Tensor[1, 3, 30, 52, 1536]
210
+ ):
211
+ """
212
+ Reorder and stack per-block attention features into one tensor.
213
+
214
+ Input:
215
+ record_features: Mapping `block_index -> Tensor` with shape
216
+ `[1, 3, 30, 52, 1536]`.
217
+
218
+ Returns:
219
+ Tensor with shape `[1536 * L, 3, 30, 52]`,
220
+ where `L = len(record_features)`.
221
+ """
222
+ attention_values = {
223
+ k: v.permute(0, 4, 1, 2, 3).contiguous() for k, v in record_features.items()
224
+ }
225
+ # [ [1, 1536, 3, 30, 52] ]
226
+
227
+ features = []
228
+ for fet in attention_values.values():
229
+ # [1536, 3, 30, 52]
230
+ features.append(fet.squeeze(0).contiguous())
231
+ del fet
232
+
233
+ # Concatenate all features
234
+ feature = torch.cat(features, dim=0)
235
+ # [1536 * L, 3, 30, 52], L is the number of blocks
236
+ return feature
237
+
238
+ def generate_features(
239
+ self,
240
+ latents,
241
+ conditional_dict,
242
+ timestep,
243
+ kv_cache,
244
+ crossattn_cache,
245
+ current_start,
246
+ # below are for drag optimization
247
+ model_config: DictConfig = None,
248
+ ):
249
+ """
250
+ Run one generator forward pass and return prediction + stacked features.
251
+
252
+ Notes:
253
+ - KV cache is deep-cloned/detached before forward to avoid mutating
254
+ the caller's cache during optimization.
255
+ - Forward runs under CUDA bfloat16 autocast.
256
+ - Returned `record_features` are converted via `stack_features(...)`.
257
+
258
+ Returns:
259
+ denoised_pred:
260
+ Model denoised prediction tensor.
261
+ record_features:
262
+ Dict `variant_key -> Tensor[1536 * L, 3, 30, 52]`. L is the number of blocks.
263
+ """
264
+ temp_kv_cache = [
265
+ {
266
+ "k": kv_cache[block_index]["k"].clone().detach(),
267
+ "v": kv_cache[block_index]["v"].clone().detach(),
268
+ "global_end_index": kv_cache[block_index]["global_end_index"].clone().detach(),
269
+ "local_end_index": kv_cache[block_index]["local_end_index"].clone().detach(),
270
+ }
271
+ for block_index in range(self.num_transformer_blocks)
272
+ ]
273
+ # print(f"{temp_kv_cache[0]['k'].shape = }")
274
+ # Forward pass through the transformer with user-specified autocast dtype
275
+ with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
276
+ # Extract features during forward pass
277
+ _, denoised_pred = self.generator(
278
+ noisy_image_or_video=latents,
279
+ conditional_dict=conditional_dict,
280
+ timestep=timestep,
281
+ kv_cache=temp_kv_cache,
282
+ crossattn_cache=crossattn_cache,
283
+ current_start=current_start,
284
+ # below are for drag optimization
285
+ model_config=model_config,
286
+ )
287
+ denoised_pred, record_features = denoised_pred
288
+ # record_features: dict[block_index] -> dict[key -> Tensor] [1, 3, 30, 52, 1536]
289
+
290
+ record_features = transpose_dict_2d(record_features)
291
+ # record_features: Dict[key] -> Dict[block_index -> Tensor] [1, 3, 30, 52, 1536]
292
+ record_features = {k: self.stack_features(v) for k, v in record_features.items()}
293
+ # record_features: Dict[key] -> Tensor [1536 * L, 3, 30, 52]
294
+ return denoised_pred, record_features
295
+
296
+ @staticmethod
297
+ def precompute_conditions(
298
+ trajectories: list[dict[str, bool | list[torch.Tensor]]],
299
+ # N x trajectory
300
+ masks: list[np.ndarray], # N x mask
301
+ dtype: torch.dtype,
302
+ device: torch.device,
303
+ model_config: DictConfig,
304
+ downsample_movable_mask: torch.Tensor,
305
+ previous_record_features: dict[int, dict[str, torch.Tensor]],
306
+ latent_spatial_size: tuple[int, int], # (H_lat, W_lat)
307
+ ):
308
+ with torch.no_grad():
309
+ _any_prev_feat = next(iter(previous_record_features.values()))
310
+ feat_spatial_size = tuple(_any_prev_feat.shape[-2:]) # (Hf, Wf)
311
+
312
+ # 1) Warped masks in image space
313
+ warped_masks = [
314
+ warp_tensor_sequence(
315
+ tensor=torch.tensor(mask, device=device).float(),
316
+ is_rotation=trajectory["is_rotation"],
317
+ deltas=trajectory["deltas"],
318
+ rotation_center=trajectory.get("rotation_center", None),
319
+ original_height=IMAGE_HEIGHT,
320
+ mode="nearest",
321
+ cumulative=False,
322
+ )
323
+ for trajectory, mask in zip(trajectories, masks)
324
+ ] # N x frame x [H_img, W_img]
325
+ # print(f"{len(warped_masks) = }, {len(warped_masks[0]) = }, {warped_masks[0][0].shape = }")
326
+
327
+ # 2) Downsampled warped masks in feature space
328
+ down_warp_masks = [
329
+ [
330
+ resize_tensor(
331
+ warped_mask.detach(),
332
+ size=feat_spatial_size,
333
+ mode="nearest",
334
+ ).detach()
335
+ for warped_mask in traj_warped_masks
336
+ ]
337
+ for traj_warped_masks in warped_masks
338
+ ] # N x frame x [Hf, Wf]
339
+
340
+ # 3) Gaussian heatmaps per trajectory
341
+ gaussian_heatmaps_per_traj = [
342
+ [
343
+ build_anisotropic_gaussian_from_mask(
344
+ warped_mask,
345
+ padding_scale=model_config.drag_optim_config.gradient_gaussian_padding,
346
+ sigma_scale=model_config.drag_optim_config.gradient_gaussian_sigma,
347
+ ).detach()
348
+ for warped_mask in traj_warped_masks
349
+ ]
350
+ for traj_warped_masks in warped_masks
351
+ ] # N x frame x [H_img, W_img]
352
+
353
+ # 4) Combined downsampled movable mask (OR of all warped + original)
354
+ all_down_warp_masks = [
355
+ dwm for traj_down_masks in down_warp_masks for dwm in traj_down_masks
356
+ ]
357
+ all_down_warp_masks.append(downsample_movable_mask.clone())
358
+ combined_downsample_movable_mask = combine_masks_or(all_down_warp_masks)
359
+
360
+ # 5) Precompute warped attention values per variant
361
+ warped_for_prev: dict[str | int, List[List[torch.Tensor]]] = {
362
+ key: [
363
+ warp_tensor_sequence(
364
+ tensor=prev_feat.to(dtype=dtype, device=device),
365
+ is_rotation=trajectory["is_rotation"],
366
+ deltas=[
367
+ d.to(
368
+ dtype=dtype,
369
+ device=device,
370
+ )
371
+ for d in trajectory["deltas"]
372
+ ],
373
+ rotation_center=trajectory.get("rotation_center", None),
374
+ original_height=IMAGE_HEIGHT,
375
+ mode="nearest",
376
+ cumulative=False,
377
+ )
378
+ for trajectory in trajectories
379
+ ]
380
+ for key, prev_feat in previous_record_features.items()
381
+ }
382
+ # warped_for_prev: dict[key] -> list[traj_index] -> list[frame_index] -> Tensor [1536 * L, 30 * scaling, 52 * scaling]
383
+
384
+ # 6) Combined Gaussian heatmaps in latent space
385
+ combined_gaussian_heatmaps = None
386
+ num_frames = len(gaussian_heatmaps_per_traj[0])
387
+ num_trajs = len(gaussian_heatmaps_per_traj)
388
+ combined_gaussian_heatmaps = torch.stack(
389
+ [
390
+ combine_gaussian_maps(
391
+ [
392
+ gaussian_heatmaps_per_traj[traj_idx][frame_idx]
393
+ for traj_idx in range(num_trajs)
394
+ ]
395
+ ) # [H_img, W_img]
396
+ for frame_idx in range(num_frames)
397
+ ],
398
+ dim=0,
399
+ ).to(
400
+ device=device
401
+ ) # [F, H_img, W_img]
402
+
403
+ combined_gaussian_heatmaps = resize_tensor(
404
+ combined_gaussian_heatmaps,
405
+ size=latent_spatial_size,
406
+ mode="bilinear",
407
+ ).detach() # [F, H_lat, W_lat]
408
+ combined_gaussian_heatmaps = combined_gaussian_heatmaps.to(dtype=dtype)
409
+ return (
410
+ warped_masks,
411
+ down_warp_masks,
412
+ gaussian_heatmaps_per_traj,
413
+ combined_downsample_movable_mask,
414
+ warped_for_prev,
415
+ combined_gaussian_heatmaps,
416
+ )
417
+
418
+ def optimize_latent(
419
+ self,
420
+ latents,
421
+ conditional_dict,
422
+ timestep,
423
+ kv_cache,
424
+ crossattn_cache,
425
+ current_start,
426
+ # below are for drag optimization
427
+ trajectories: list[dict[str, bool | list[torch.Tensor]]],
428
+ # N x trajectory,
429
+ # trajectory has keys 'is_rotation' 'deltas' 'start_point'
430
+ # if is_rotation: trajectory also has 'rotation_center'
431
+ masks: list[np.ndarray], # N x mask
432
+ movable_mask: np.ndarray,
433
+ clean_previous_record_feature: dict[int, dict[str, torch.Tensor]],
434
+ # dict[block_index] -> dict[key -> Tensor] [1, 3, 30, 52, 1536]
435
+ noisy_previous_record_feature: dict[int, dict[str, torch.Tensor]],
436
+ # dict[block_index] -> dict[key -> Tensor] [1, 3, 30, 52, 1536]
437
+ model_config: DictConfig,
438
+ optimize_target_latent_index: int = -1,
439
+ ):
440
+ """
441
+ :param trajectories:
442
+ N x trajectory,
443
+ trajectory has keys 'is_rotation' 'deltas' 'start_point'
444
+ if is_rotation: trajectory also has 'rotation_center'
445
+ :param masks:
446
+ N x mask
447
+ """
448
+ assert isinstance(model_config.drag_optim_config.optimize_iter, int)
449
+ assert isinstance(model_config.drag_optim_config.optimize_lr, float)
450
+ assert len(model_config.drag_optim_config.record_feature_block_indexes) > 0
451
+ assert len(trajectories) == len(masks)
452
+ if len(trajectories) == 0:
453
+ return latents
454
+
455
+ print(f"{trajectories = }")
456
+ print(f"{len(masks) = }")
457
+
458
+ original_latents = latents.clone().detach()
459
+
460
+ original_denoised_pred = self.generate_features(
461
+ latents=latents,
462
+ conditional_dict=conditional_dict,
463
+ timestep=timestep,
464
+ kv_cache=kv_cache,
465
+ crossattn_cache=crossattn_cache,
466
+ current_start=current_start,
467
+ # below are for drag optimization
468
+ model_config=model_config,
469
+ )
470
+ original_denoised_pred, _ = original_denoised_pred
471
+
472
+ # Check if optimization is enabled
473
+ if model_config.drag_optim_config.optimize_iter <= 0:
474
+ return latents
475
+ # Cast latents to configured dtype for optimization
476
+ latent_original_dtype = latents.dtype
477
+ latents = latents.to(dtype=torch.bfloat16)
478
+ timestep_original_dtype = timestep.dtype
479
+ timestep = timestep.to(dtype=latents.dtype)
480
+
481
+ for param in self.generator.parameters():
482
+ generator_original_dtype = param.dtype
483
+ break
484
+ self.generator = self.generator.to(dtype=latents.dtype)
485
+ # self.generator.train(True)
486
+ for param in self.generator.parameters():
487
+ param.requires_grad = False
488
+
489
+ split_trajectories_list = split_trajectories_segments(
490
+ trajectories=trajectories,
491
+ translation_step=model_config.drag_optim_config.translation_step,
492
+ rotation_step=model_config.drag_optim_config.rotation_step,
493
+ )
494
+ # split_trajectories_list: list[segment_index] -> list[trajectory_index] -> trajectory dict
495
+
496
+ def _select_variant(feat):
497
+ if isinstance(feat, dict):
498
+ keys = list(feat.keys())
499
+ if not keys:
500
+ raise ValueError("Empty feature dict provided.")
501
+ non_orig = [k for k in keys if str(k) != "original"]
502
+ key = random.choice(keys)
503
+ print(f"Selected feature variant {key = } from {keys = }")
504
+ # key = "original" if "original" in feat else keys[0]
505
+ return feat[key]
506
+ return feat
507
+
508
+ def get_previous_last(
509
+ prev: dict[int, dict[str, torch.Tensor]],
510
+ # dict[block_index] -> dict[key -> Tensor] [1, 3, 30, 52, 1536]
511
+ ) -> dict[str, torch.Tensor]:
512
+ out = transpose_dict_2d(prev)
513
+ # out: dict[key] -> dict[block_index -> Tensor] [1, 3, 30, 52, 1536]
514
+ out = {k: self.stack_features(v) for k, v in out.items()}
515
+ # out: dict[key] -> Tensor [1536 * L, 3, 30, 52]
516
+ out = {k: v[:, -1, ...].detach() for k, v in out.items()}
517
+ # out: dict[key] -> Tensor [1536 * L, 30, 52]
518
+ out = {
519
+ k: resize_tensor(
520
+ v.detach(),
521
+ scale_factor=model_config.drag_optim_config.feature_scaling_factor,
522
+ mode="bilinear",
523
+ ).detach()
524
+ for k, v in out.items()
525
+ }
526
+ # out: dict[key] -> Tensor [1536 * L, 30 * scaling, 52 * scaling]
527
+ return out
528
+
529
+ previous_record_features: dict[str, torch.Tensor] = get_previous_last(
530
+ noisy_previous_record_feature
531
+ )
532
+ # previous_record_features: dict[key] -> Tensor [1536 * L, 30 * scaling, 52 * scaling]
533
+
534
+ movable_mask_torch = torch.tensor(movable_mask, device=latents.device).float()
535
+ downsample_movable_mask = resize_tensor(
536
+ movable_mask_torch.detach(),
537
+ size=tuple(original_denoised_pred.shape[-2:]),
538
+ mode="nearest",
539
+ ).detach()
540
+ # print(f"{downsample_movable_mask.shape = }") # [60, 104]
541
+
542
+ with torch.enable_grad():
543
+
544
+ latents.requires_grad_(True)
545
+ optimizer = torch.optim.AdamW(
546
+ [latents],
547
+ lr=model_config.drag_optim_config.optimize_lr,
548
+ )
549
+
550
+ for split_traj_idx, split_trajectories in enumerate(split_trajectories_list):
551
+ # split_trajectories: N x trajectory, list[trajectory_index] -> trajectory dict
552
+ (
553
+ warped_masks,
554
+ down_warp_masks,
555
+ gaussian_heatmaps_per_traj,
556
+ combined_downsample_movable_mask,
557
+ warped_previous_record_features,
558
+ combined_gaussian_heatmaps,
559
+ ) = CausalInferencePipeline.precompute_conditions(
560
+ trajectories=split_trajectories,
561
+ masks=masks,
562
+ dtype=latents.dtype,
563
+ device=latents.device,
564
+ model_config=model_config,
565
+ downsample_movable_mask=downsample_movable_mask,
566
+ previous_record_features=previous_record_features,
567
+ latent_spatial_size=tuple(latents.shape[-2:]),
568
+ )
569
+ # -------------------------
570
+ # Optimization iterations (reuse precomputed items)
571
+ # -------------------------
572
+ for optimize_iter_idx in range(model_config.drag_optim_config.optimize_iter):
573
+ print(f"{optimize_iter_idx = }")
574
+ print(f"{latents.mean((0, 2, 3, 4)) = }")
575
+ print(f"{latents.std((0, 2, 3, 4)) = }")
576
+
577
+ denoised_pred, record_features = self.generate_features(
578
+ latents=latents,
579
+ conditional_dict=conditional_dict,
580
+ timestep=timestep,
581
+ kv_cache=kv_cache,
582
+ crossattn_cache=crossattn_cache,
583
+ current_start=current_start,
584
+ model_config=model_config,
585
+ )
586
+ # denoised_pred.shape [1, 3, 16, 60, 104]
587
+ # record_features: dict[key] -> Tensor [1536 * L, 3, 30, 52]
588
+
589
+ print(f"selecting features for optimization")
590
+ record_features_selected = _select_variant(
591
+ record_features,
592
+ )
593
+ # print(f"{record_features_selected.shape = }")
594
+ # record_features_selected: Tensor [1536 * L, 3, 30, 52]
595
+ del record_features
596
+
597
+ if optimize_target_latent_index >= 0:
598
+ record_features_selected = record_features_selected[
599
+ :,
600
+ optimize_target_latent_index : optimize_target_latent_index + 1,
601
+ ]
602
+
603
+ record_features_selected = resize_tensor(
604
+ record_features_selected,
605
+ scale_factor=model_config.drag_optim_config.feature_scaling_factor,
606
+ mode="bilinear",
607
+ )
608
+ # print(
609
+ # f"{record_features_selected.shape = }"
610
+ # ) # [1536 * L, 3, 30 * scaling, 52 * scaling]
611
+
612
+ print(f"selecting warped previous features")
613
+ warped_previous_feature_selected = _select_variant(
614
+ warped_previous_record_features,
615
+ ) # list[traj_index] -> list[frame_index] -> Tensor [1536 * L, 30 * scaling, 52 * scaling]
616
+
617
+ loss = 0
618
+ loss_cnt = 0
619
+
620
+ # Iterate over each trajectory point
621
+ for trajectory_index, trajectory in enumerate(split_trajectories):
622
+ assert record_features_selected.shape[1] == len(trajectory["deltas"])
623
+
624
+ for frame_index in range(len(trajectory["deltas"])):
625
+ warped_attention_values = warped_previous_feature_selected[
626
+ trajectory_index
627
+ ][frame_index]
628
+
629
+ downsample_warped_mask = down_warp_masks[trajectory_index][frame_index]
630
+
631
+ pixel_wise_loss = F.mse_loss(
632
+ warped_attention_values * downsample_warped_mask,
633
+ record_features_selected[:, frame_index] * downsample_warped_mask,
634
+ reduction="none",
635
+ ).mean(dim=0)
636
+ # print(f"{pixel_wise_loss.shape = }") # [60, 104]
637
+
638
+ # Add weighted loss
639
+ loss = loss + (downsample_warped_mask * pixel_wise_loss).sum()
640
+ loss_cnt += downsample_warped_mask.sum()
641
+
642
+ print(f"{loss = } / {loss_cnt = }")
643
+ loss = loss / max(1e-8, loss_cnt)
644
+ print(f"{loss = }")
645
+
646
+ unchanged_mask = 1.0 - combined_downsample_movable_mask
647
+ unchanged_loss = F.mse_loss(
648
+ denoised_pred * unchanged_mask.detach(),
649
+ original_denoised_pred.detach() * unchanged_mask.detach(),
650
+ )
651
+ print(f"{unchanged_loss = }")
652
+ loss = loss + unchanged_loss * 1.0
653
+
654
+ # Update latents
655
+ self.generator.zero_grad()
656
+ optimizer.zero_grad()
657
+ if loss_cnt > 0:
658
+ loss.backward()
659
+ assert (
660
+ combined_gaussian_heatmaps.shape[0] == 1
661
+ or combined_gaussian_heatmaps.shape[0] == latents.shape[-4]
662
+ )
663
+ assert combined_gaussian_heatmaps.shape[-2:] == latents.shape[-2:]
664
+ latents.grad.mul_(combined_gaussian_heatmaps[:, None, :, :])
665
+ # Clip gradients
666
+ clip_grad_norm_(
667
+ [latents],
668
+ max_norm=1.0,
669
+ norm_type=2,
670
+ )
671
+ optimizer.step()
672
+ if model_config.drag_optim_config.normalize_latent_after_drag_optimize:
673
+ print(f"Normalizing latents after optimize iteration")
674
+ latents = (
675
+ normalize_tensor_to_match_tensor(
676
+ latents.detach().clone(),
677
+ dim=(0, 3, 4),
678
+ reference_tensor=original_latents.to(dtype=latents.dtype),
679
+ )
680
+ .detach()
681
+ .clone()
682
+ )
683
+ # latents = latents.clamp(
684
+ # min=latents_min,
685
+ # max=latents_max,
686
+ # ).detach().clone()
687
+ latents.requires_grad_(True)
688
+ optimizer = torch.optim.AdamW(
689
+ [latents],
690
+ lr=model_config.drag_optim_config.optimize_lr,
691
+ )
692
+ # Clean up to save memory
693
+ gc.collect()
694
+ torch.cuda.empty_cache()
695
+
696
+ latents = latents.detach().requires_grad_(False)
697
+
698
+ if model_config.drag_optim_config.normalize_latent_after_post_merge:
699
+ latents = (
700
+ normalize_tensor_to_match_tensor(
701
+ latents,
702
+ dim=None,
703
+ reference_tensor=original_latents,
704
+ )
705
+ .detach()
706
+ .clone()
707
+ )
708
+
709
+ # Convert back to original dtype
710
+ self.generator = self.generator.to(dtype=generator_original_dtype)
711
+ self.generator.train(False)
712
+ latents = latents.to(dtype=latent_original_dtype)
713
+ timestep = timestep.to(dtype=timestep_original_dtype)
714
+
715
+ # Detach latents and remove gradient
716
+ latents = latents.detach().requires_grad_(False)
717
+ return latents
718
+
719
+ def inference(
720
+ self,
721
+ noise: torch.Tensor,
722
+ text_prompts: List[str],
723
+ initial_latent: Optional[torch.Tensor] = None,
724
+ return_latents: bool = False,
725
+ profile: bool = False,
726
+ low_memory: bool = False,
727
+ do_not_decode_video: bool = False,
728
+ do_not_recompute_initial_latents: bool = False,
729
+ # below are for drag optimization
730
+ model_config: DictConfig = None,
731
+ previous_record_feature_list: dict[int, dict[int, dict[str, torch.Tensor]]] = None,
732
+ # dict[denoising_step] -> dict[block_index] -> dict[key -> Tensor] [1, 3, 30, 52, 1536]
733
+ is_drag_optimize: bool = False,
734
+ block_trajectories: list[list[dict[str, bool | list[torch.Tensor]]]] = [],
735
+ masks: list[np.ndarray] = [],
736
+ movable_mask: np.ndarray = None,
737
+ drag_optimize_target_latent_index: int = -1,
738
+ ) -> torch.Tensor:
739
+ """
740
+ Perform inference on the given noise and text prompts.
741
+ Inputs:
742
+ noise (torch.Tensor): The input noise tensor of shape
743
+ (batch_size, num_output_frames, num_channels, height, width).
744
+ text_prompts (List[str]): The list of text prompts.
745
+ initial_latent (torch.Tensor): The initial latent tensor of shape
746
+ (batch_size, num_input_frames, num_channels, height, width).
747
+ If num_input_frames is 1, perform image to video.
748
+ If num_input_frames is greater than 1, perform video extension.
749
+ return_latents (bool): Whether to return the latents.
750
+ :param block_trajectories:
751
+ block_num x N x trajectory,
752
+ trajectory has keys 'is_rotation' 'deltas' 'start_point'
753
+ if is_rotation: trajectory also has 'rotation_center'
754
+ :param masks:
755
+ N x mask
756
+ Outputs:
757
+ video (torch.Tensor): The generated video tensor of shape
758
+ (batch_size, num_output_frames, num_channels, height, width).
759
+ It is normalized to be in the range [0, 1].
760
+ """
761
+ batch_size, num_frames, num_channels, height, width = noise.shape
762
+ if not self.independent_first_frame or (
763
+ self.independent_first_frame and initial_latent is not None
764
+ ):
765
+ # If the first frame is independent and the first frame is provided, then the number of frames in the
766
+ # noise should still be a multiple of num_frame_per_block
767
+ assert num_frames % self.num_frame_per_block == 0
768
+ num_blocks = num_frames // self.num_frame_per_block
769
+ else:
770
+ # Using a [1, 4, 4, 4, 4, 4, ...] model to generate a video without image conditioning
771
+ assert (num_frames - 1) % self.num_frame_per_block == 0
772
+ num_blocks = (num_frames - 1) // self.num_frame_per_block
773
+ num_input_frames = initial_latent.shape[1] if initial_latent is not None else 0
774
+ num_output_frames = num_frames + num_input_frames # add the initial latent frames
775
+ conditional_dict = self.text_encoder(text_prompts=text_prompts)
776
+
777
+ if low_memory:
778
+ gpu_memory_preservation = get_cuda_free_memory_gb(gpu) + 5
779
+ move_model_to_device_with_memory_preservation(
780
+ self.text_encoder,
781
+ target_device=gpu,
782
+ preserved_memory_gb=gpu_memory_preservation,
783
+ )
784
+
785
+ output = torch.zeros(
786
+ [batch_size, num_output_frames, num_channels, height, width],
787
+ device=noise.device,
788
+ dtype=noise.dtype,
789
+ )
790
+
791
+ # Set up profiling if requested
792
+ if profile:
793
+ init_start = torch.cuda.Event(enable_timing=True)
794
+ init_end = torch.cuda.Event(enable_timing=True)
795
+ diffusion_start = torch.cuda.Event(enable_timing=True)
796
+ diffusion_end = torch.cuda.Event(enable_timing=True)
797
+ vae_start = torch.cuda.Event(enable_timing=True)
798
+ vae_end = torch.cuda.Event(enable_timing=True)
799
+ block_times = []
800
+ block_start = torch.cuda.Event(enable_timing=True)
801
+ block_end = torch.cuda.Event(enable_timing=True)
802
+ init_start.record()
803
+
804
+ # Step 1: Initialize KV cache to all zeros
805
+ if self.kv_cache1 is None:
806
+ self._initialize_kv_cache(batch_size=batch_size, dtype=noise.dtype, device=noise.device)
807
+ self._initialize_crossattn_cache(
808
+ batch_size=batch_size, dtype=noise.dtype, device=noise.device
809
+ )
810
+ else:
811
+ if do_not_recompute_initial_latents:
812
+ pass
813
+ else:
814
+ print(f"Resetting caches")
815
+ self._reset_crossattn_cache()
816
+ self._reset_kv_cache()
817
+
818
+ # Step 2: Cache context feature
819
+ current_start_frame = 0
820
+ if initial_latent is not None:
821
+ timestep = torch.ones([batch_size, 1], device=noise.device, dtype=torch.int64) * 0
822
+ if self.independent_first_frame:
823
+ # Assume num_input_frames is 1 + self.num_frame_per_block * num_input_blocks
824
+ assert (num_input_frames - 1) % self.num_frame_per_block == 0
825
+ num_input_blocks = (num_input_frames - 1) // self.num_frame_per_block
826
+ output[:, :1] = initial_latent[:, :1]
827
+ if do_not_recompute_initial_latents:
828
+ pass
829
+ else:
830
+ print(f"Recompute KV cache based on Initial Latents")
831
+ self.generator(
832
+ noisy_image_or_video=initial_latent[:, :1],
833
+ conditional_dict=conditional_dict,
834
+ timestep=timestep * 0,
835
+ kv_cache=self.kv_cache1,
836
+ crossattn_cache=self.crossattn_cache,
837
+ current_start=current_start_frame * self.frame_seq_length,
838
+ )
839
+ current_start_frame += 1
840
+ else:
841
+ # Assume num_input_frames is self.num_frame_per_block * num_input_blocks
842
+ assert num_input_frames % self.num_frame_per_block == 0
843
+ num_input_blocks = num_input_frames // self.num_frame_per_block
844
+
845
+ for _ in range(num_input_blocks):
846
+ current_ref_latents = initial_latent[
847
+ :,
848
+ current_start_frame : current_start_frame + self.num_frame_per_block,
849
+ ]
850
+ output[
851
+ :,
852
+ current_start_frame : current_start_frame + self.num_frame_per_block,
853
+ ] = current_ref_latents
854
+ if do_not_recompute_initial_latents:
855
+ pass
856
+ else:
857
+ print(f"Recompute KV cache based on Initial Latents")
858
+ self.generator(
859
+ noisy_image_or_video=current_ref_latents,
860
+ conditional_dict=conditional_dict,
861
+ timestep=timestep * 0,
862
+ kv_cache=self.kv_cache1,
863
+ crossattn_cache=self.crossattn_cache,
864
+ current_start=current_start_frame * self.frame_seq_length,
865
+ )
866
+ current_start_frame += self.num_frame_per_block
867
+
868
+ if profile:
869
+ init_end.record()
870
+ torch.cuda.synchronize()
871
+ diffusion_start.record()
872
+
873
+ # Step 3: Temporal denoising loop
874
+ all_num_frames = [self.num_frame_per_block] * num_blocks
875
+ if self.independent_first_frame and initial_latent is None:
876
+ all_num_frames = [1] + all_num_frames
877
+ for current_chunk_index, current_num_frames in enumerate(
878
+ tqdm(all_num_frames),
879
+ start=num_input_blocks if initial_latent is not None else 0,
880
+ ):
881
+ print(f"\n{current_chunk_index = } ; {current_start_frame = }")
882
+ if profile:
883
+ block_start.record()
884
+
885
+ noisy_input = noise[
886
+ :,
887
+ current_start_frame
888
+ - num_input_frames : current_start_frame
889
+ + current_num_frames
890
+ - num_input_frames,
891
+ ]
892
+
893
+ if model_config is not None and OmegaConf.select(
894
+ model_config, "drag_optim_config.record_feature_block_indexes"
895
+ ):
896
+ record_attention_values_list = {}
897
+ # Step 3.1: Spatial denoising loop
898
+ for time_step_index, current_timestep in enumerate(self.denoising_step_list):
899
+ print(f"{time_step_index = } ; {current_timestep = }")
900
+ # set current timestep
901
+ timestep = (
902
+ torch.ones(
903
+ [batch_size, current_num_frames],
904
+ device=noise.device,
905
+ dtype=torch.int64,
906
+ )
907
+ * current_timestep
908
+ )
909
+
910
+ if (
911
+ is_drag_optimize
912
+ and time_step_index
913
+ in model_config.drag_optim_config.optimize_denoising_steps_indexes
914
+ ):
915
+
916
+ noisy_input = self.optimize_latent(
917
+ latents=noisy_input,
918
+ conditional_dict=conditional_dict,
919
+ timestep=timestep,
920
+ kv_cache=self.kv_cache1,
921
+ crossattn_cache=self.crossattn_cache,
922
+ current_start=current_start_frame * self.frame_seq_length,
923
+ # below are for drag optimization
924
+ trajectories=block_trajectories[current_chunk_index - num_input_blocks],
925
+ masks=masks,
926
+ movable_mask=movable_mask,
927
+ clean_previous_record_feature=previous_record_feature_list[-1],
928
+ noisy_previous_record_feature=previous_record_feature_list[time_step_index],
929
+ model_config=model_config,
930
+ optimize_target_latent_index=drag_optimize_target_latent_index,
931
+ )
932
+ print(f"{noisy_input.mean() = }")
933
+ print(f"{noisy_input.std() = }")
934
+
935
+ _, denoised_pred = self.generator(
936
+ noisy_image_or_video=noisy_input,
937
+ conditional_dict=conditional_dict,
938
+ timestep=timestep,
939
+ kv_cache=self.kv_cache1,
940
+ crossattn_cache=self.crossattn_cache,
941
+ current_start=current_start_frame * self.frame_seq_length,
942
+ model_config=model_config,
943
+ )
944
+ if model_config is not None and OmegaConf.select(
945
+ model_config,
946
+ "drag_optim_config.record_feature_block_indexes",
947
+ ):
948
+ denoised_pred, record_features = denoised_pred
949
+ if (
950
+ time_step_index
951
+ in model_config.drag_optim_config.optimize_denoising_steps_indexes
952
+ ):
953
+ record_attention_values_list[time_step_index] = record_features
954
+
955
+ if (
956
+ model_config is not None
957
+ and OmegaConf.select(
958
+ model_config,
959
+ "drag_optim_config.dynamic_chunk_normalization_block_number",
960
+ default=0,
961
+ )
962
+ > 0
963
+ ):
964
+ num_norm_blocks = (
965
+ model_config.drag_optim_config.dynamic_chunk_normalization_block_number
966
+ )
967
+ # Exclude the first chunk (independent first frame latent) by starting no earlier than num_frame_per_block
968
+ dynamic_normalize_start_frame_index = max(
969
+ self.num_frame_per_block,
970
+ (current_chunk_index - num_norm_blocks) * self.num_frame_per_block,
971
+ )
972
+ # print(f"{dynamic_normalize_start_frame_index = }")
973
+ if dynamic_normalize_start_frame_index < current_start_frame:
974
+ reference_tensor = torch.cat(
975
+ [
976
+ output[
977
+ :,
978
+ dynamic_normalize_start_frame_index:current_start_frame,
979
+ ],
980
+ denoised_pred,
981
+ ],
982
+ dim=1,
983
+ )
984
+ denoised_pred = normalize_tensor_to_match_tensor(
985
+ denoised_pred,
986
+ dim=None,
987
+ reference_tensor=reference_tensor,
988
+ )
989
+ # print(f"{denoised_pred.mean() = }")
990
+ # print(f"{denoised_pred.std() = }")
991
+
992
+ if time_step_index < len(self.denoising_step_list) - 1:
993
+ next_timestep = self.denoising_step_list[time_step_index + 1]
994
+ noisy_input = self.scheduler.add_noise(
995
+ denoised_pred.flatten(0, 1),
996
+ torch.randn_like(denoised_pred.flatten(0, 1)),
997
+ next_timestep
998
+ * torch.ones(
999
+ [batch_size * current_num_frames],
1000
+ device=noise.device,
1001
+ dtype=torch.long,
1002
+ ),
1003
+ ).unflatten(0, denoised_pred.shape[:2])
1004
+
1005
+ # Step 3.2: record the model's output
1006
+ output[
1007
+ :,
1008
+ current_start_frame : current_start_frame + current_num_frames,
1009
+ ] = denoised_pred
1010
+
1011
+ # Step 3.3: rerun with timestep zero to update KV cache using clean context
1012
+ context_timestep = torch.ones_like(timestep) * self.args.context_noise
1013
+ _, denoised_pred = self.generator(
1014
+ noisy_image_or_video=denoised_pred,
1015
+ conditional_dict=conditional_dict,
1016
+ timestep=context_timestep,
1017
+ kv_cache=self.kv_cache1,
1018
+ crossattn_cache=self.crossattn_cache,
1019
+ current_start=current_start_frame * self.frame_seq_length,
1020
+ model_config=model_config,
1021
+ )
1022
+ if model_config is not None and OmegaConf.select(
1023
+ model_config, "drag_optim_config.record_feature_block_indexes"
1024
+ ):
1025
+ denoised_pred, record_features = denoised_pred
1026
+ record_attention_values_list[-1] = record_features
1027
+
1028
+ if profile:
1029
+ block_end.record()
1030
+ torch.cuda.synchronize()
1031
+ block_time = block_start.elapsed_time(block_end)
1032
+ block_times.append(block_time)
1033
+
1034
+ # Step 3.4: update the start and end frame indices
1035
+ current_start_frame += current_num_frames
1036
+
1037
+ if profile:
1038
+ # End diffusion timing and synchronize CUDA
1039
+ diffusion_end.record()
1040
+ torch.cuda.synchronize()
1041
+ diffusion_time = diffusion_start.elapsed_time(diffusion_end)
1042
+ init_time = init_start.elapsed_time(init_end)
1043
+ vae_start.record()
1044
+
1045
+ # Step 4: Decode the output
1046
+ if not do_not_decode_video:
1047
+ start_decode_time = time.time()
1048
+ video = self.vae.decode_to_pixel(output, use_cache=False)
1049
+ video = (video * 0.5 + 0.5).clamp(0, 1)
1050
+ print(
1051
+ f"{self.__class__.__name__}.inference() VAE decode time: {time.time() - start_decode_time:.2f} seconds"
1052
+ )
1053
+
1054
+ if profile:
1055
+ # End VAE timing and synchronize CUDA
1056
+ vae_end.record()
1057
+ torch.cuda.synchronize()
1058
+ vae_time = vae_start.elapsed_time(vae_end)
1059
+ total_time = init_time + diffusion_time + vae_time
1060
+
1061
+ print("Profiling results:")
1062
+ print(
1063
+ f" - Initialization/caching time: {init_time:.2f} ms ({100 * init_time / total_time:.2f}%)"
1064
+ )
1065
+ print(
1066
+ f" - Diffusion generation time: {diffusion_time:.2f} ms ({100 * diffusion_time / total_time:.2f}%)"
1067
+ )
1068
+ for i, block_time in enumerate(block_times):
1069
+ print(
1070
+ f" - Block {i} generation time: {block_time:.2f} ms ({100 * block_time / diffusion_time:.2f}% of diffusion)"
1071
+ )
1072
+ print(f" - VAE decoding time: {vae_time:.2f} ms ({100 * vae_time / total_time:.2f}%)")
1073
+ print(f" - Total time: {total_time:.2f} ms")
1074
+
1075
+ return_values = []
1076
+ if not do_not_decode_video:
1077
+ return_values.append(video)
1078
+ if return_latents:
1079
+ return_values.append(output)
1080
+ if model_config is not None and OmegaConf.select(
1081
+ model_config, "drag_optim_config.record_feature_block_indexes"
1082
+ ):
1083
+ return_values.append(record_attention_values_list)
1084
+
1085
+ if len(return_values) == 0:
1086
+ return
1087
+ elif len(return_values) == 1:
1088
+ return return_values[0]
1089
+ else:
1090
+ return tuple(return_values)
1091
+
1092
+ def _initialize_kv_cache(
1093
+ self,
1094
+ batch_size,
1095
+ dtype,
1096
+ device,
1097
+ ):
1098
+ """
1099
+ Initialize a Per-GPU KV cache for the Wan model.
1100
+ """
1101
+ print(
1102
+ f"""
1103
+ {type(self).__name__}._initialize_kv_cache
1104
+ {batch_size = }
1105
+ {dtype = }
1106
+ {device = }
1107
+ """
1108
+ )
1109
+ kv_cache1 = []
1110
+ if self.local_attn_size != -1:
1111
+ print(f"use {self.local_attn_size = }")
1112
+ # Use the local attention size to compute the KV cache size
1113
+ kv_cache_size = self.local_attn_size * self.frame_seq_length
1114
+ else:
1115
+ # Use the default KV cache size
1116
+ kv_cache_size = 32760
1117
+ print(f"{kv_cache_size = }")
1118
+
1119
+ for _ in range(self.num_transformer_blocks):
1120
+ kv_cache1.append(
1121
+ {
1122
+ "k": torch.zeros(
1123
+ [batch_size, kv_cache_size, 12, 128],
1124
+ dtype=dtype,
1125
+ device=device,
1126
+ ),
1127
+ "v": torch.zeros(
1128
+ [batch_size, kv_cache_size, 12, 128],
1129
+ dtype=dtype,
1130
+ device=device,
1131
+ ),
1132
+ "global_end_index": torch.tensor([0], dtype=torch.long, device=device),
1133
+ "local_end_index": torch.tensor([0], dtype=torch.long, device=device),
1134
+ }
1135
+ )
1136
+
1137
+ self.kv_cache1 = kv_cache1 # always store the clean cache
1138
+
1139
+ def _initialize_crossattn_cache(
1140
+ self,
1141
+ batch_size,
1142
+ dtype,
1143
+ device,
1144
+ ):
1145
+ """
1146
+ Initialize a Per-GPU cross-attention cache for the Wan model.
1147
+ """
1148
+ print(
1149
+ f"""
1150
+ {type(self).__name__}._initialize_crossattn_cache
1151
+ {batch_size = }
1152
+ {dtype = }
1153
+ {device = }
1154
+ """
1155
+ )
1156
+ crossattn_cache = []
1157
+
1158
+ for _ in range(self.num_transformer_blocks):
1159
+ crossattn_cache.append(
1160
+ {
1161
+ "k": torch.zeros([batch_size, 512, 12, 128], dtype=dtype, device=device),
1162
+ "v": torch.zeros([batch_size, 512, 12, 128], dtype=dtype, device=device),
1163
+ "is_init": False,
1164
+ }
1165
+ )
1166
+ self.crossattn_cache = crossattn_cache
1167
+
1168
+ def _reset_crossattn_cache(self):
1169
+ # reset cross attn cache
1170
+ print(f"{type(self).__name__}._reset_crossattn_cache")
1171
+ for block_index in range(self.num_transformer_blocks):
1172
+ self.crossattn_cache[block_index]["is_init"] = False
1173
+
1174
+ def _reset_kv_cache(self):
1175
+ # reset kv cache
1176
+ print(f"{type(self).__name__}._reset_kv_cache")
1177
+ for block_index in range(len(self.kv_cache1)):
1178
+ self.kv_cache1[block_index]["global_end_index"] = torch.tensor(
1179
+ [0],
1180
+ dtype=torch.long,
1181
+ device=self.kv_cache1[block_index]["global_end_index"].device,
1182
+ )
1183
+ self.kv_cache1[block_index]["local_end_index"] = torch.tensor(
1184
+ [0],
1185
+ dtype=torch.long,
1186
+ device=self.kv_cache1[block_index]["local_end_index"].device,
1187
+ )
1188
+
1189
+ def is_kv_cache_initialized(self):
1190
+ return hasattr(self, "kv_cache1") and self.kv_cache1 is not None
1191
+
1192
+ def is_crossattn_cache_initialized(self):
1193
+ return hasattr(self, "crossattn_cache") and self.crossattn_cache is not None
pipeline/self_forcing_training.py ADDED
@@ -0,0 +1,351 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from utils.wan_wrapper import WanDiffusionWrapper
2
+ from utils.scheduler import SchedulerInterface
3
+ from typing import List, Optional
4
+ import torch
5
+ import torch.distributed as dist
6
+
7
+
8
+ class SelfForcingTrainingPipeline:
9
+ def __init__(
10
+ self,
11
+ denoising_step_list: List[int],
12
+ scheduler: SchedulerInterface,
13
+ generator: WanDiffusionWrapper,
14
+ num_frame_per_block=3,
15
+ independent_first_frame: bool = False,
16
+ same_step_across_blocks: bool = False,
17
+ last_step_only: bool = False,
18
+ num_max_frames: int = 21,
19
+ context_noise: int = 0,
20
+ **kwargs
21
+ ):
22
+ super().__init__()
23
+ self.scheduler = scheduler
24
+ self.generator = generator
25
+ self.denoising_step_list = denoising_step_list
26
+ if self.denoising_step_list[-1] == 0:
27
+ self.denoising_step_list = self.denoising_step_list[
28
+ :-1
29
+ ] # remove the zero timestep for inference
30
+
31
+ # Wan specific hyperparameters
32
+ self.num_transformer_blocks = 30
33
+ self.frame_seq_length = 1560
34
+ self.num_frame_per_block = num_frame_per_block
35
+ self.context_noise = context_noise
36
+ self.i2v = False
37
+
38
+ self.kv_cache1 = None
39
+ self.kv_cache2 = None
40
+ self.independent_first_frame = independent_first_frame
41
+ self.same_step_across_blocks = same_step_across_blocks
42
+ self.last_step_only = last_step_only
43
+ self.kv_cache_size = num_max_frames * self.frame_seq_length
44
+
45
+ def generate_and_sync_list(
46
+ self,
47
+ num_blocks,
48
+ num_denoising_steps,
49
+ device,
50
+ ):
51
+ rank = dist.get_rank() if dist.is_initialized() else 0
52
+
53
+ if rank == 0:
54
+ # Generate random indices
55
+ indices = torch.randint(
56
+ low=0,
57
+ high=num_denoising_steps,
58
+ size=(num_blocks,),
59
+ device=device,
60
+ )
61
+ if self.last_step_only:
62
+ indices = torch.ones_like(indices) * (num_denoising_steps - 1)
63
+ else:
64
+ indices = torch.empty(num_blocks, dtype=torch.long, device=device)
65
+
66
+ dist.broadcast(indices, src=0) # Broadcast the random indices to all ranks
67
+ return indices.tolist()
68
+
69
+ def inference_with_trajectory(
70
+ self,
71
+ noise: torch.Tensor,
72
+ initial_latent: Optional[torch.Tensor] = None,
73
+ return_sim_step: bool = False,
74
+ **conditional_dict
75
+ ) -> torch.Tensor:
76
+ batch_size, num_frames, num_channels, height, width = noise.shape
77
+ if not self.independent_first_frame or (
78
+ self.independent_first_frame and initial_latent is not None
79
+ ):
80
+ # If the first frame is independent and the first frame is provided, then the number of frames in the
81
+ # noise should still be a multiple of num_frame_per_block
82
+ assert num_frames % self.num_frame_per_block == 0
83
+ num_blocks = num_frames // self.num_frame_per_block
84
+ else:
85
+ # Using a [1, 4, 4, 4, 4, 4, ...] model to generate a video without image conditioning
86
+ assert (num_frames - 1) % self.num_frame_per_block == 0
87
+ num_blocks = (num_frames - 1) // self.num_frame_per_block
88
+ num_input_frames = initial_latent.shape[1] if initial_latent is not None else 0
89
+ num_output_frames = num_frames + num_input_frames # add the initial latent frames
90
+ output = torch.zeros(
91
+ [batch_size, num_output_frames, num_channels, height, width],
92
+ device=noise.device,
93
+ dtype=noise.dtype,
94
+ )
95
+
96
+ # Step 1: Initialize KV cache to all zeros
97
+ self._initialize_kv_cache(batch_size=batch_size, dtype=noise.dtype, device=noise.device)
98
+ self._initialize_crossattn_cache(
99
+ batch_size=batch_size, dtype=noise.dtype, device=noise.device
100
+ )
101
+ # if self.kv_cache1 is None:
102
+ # self._initialize_kv_cache(
103
+ # batch_size=batch_size,
104
+ # dtype=noise.dtype,
105
+ # device=noise.device,
106
+ # )
107
+ # self._initialize_crossattn_cache(
108
+ # batch_size=batch_size,
109
+ # dtype=noise.dtype,
110
+ # device=noise.device
111
+ # )
112
+ # else:
113
+ # # reset cross attn cache
114
+ # for block_index in range(self.num_transformer_blocks):
115
+ # self.crossattn_cache[block_index]["is_init"] = False
116
+ # # reset kv cache
117
+ # for block_index in range(len(self.kv_cache1)):
118
+ # self.kv_cache1[block_index]["global_end_index"] = torch.tensor(
119
+ # [0], dtype=torch.long, device=noise.device)
120
+ # self.kv_cache1[block_index]["local_end_index"] = torch.tensor(
121
+ # [0], dtype=torch.long, device=noise.device)
122
+
123
+ # Step 2: Cache context feature
124
+ current_start_frame = 0
125
+ if initial_latent is not None:
126
+ timestep = torch.ones([batch_size, 1], device=noise.device, dtype=torch.int64) * 0
127
+ # Assume num_input_frames is 1 + self.num_frame_per_block * num_input_blocks
128
+ output[:, :1] = initial_latent
129
+ with torch.no_grad():
130
+ self.generator(
131
+ noisy_image_or_video=initial_latent,
132
+ conditional_dict=conditional_dict,
133
+ timestep=timestep * 0,
134
+ kv_cache=self.kv_cache1,
135
+ crossattn_cache=self.crossattn_cache,
136
+ current_start=current_start_frame * self.frame_seq_length,
137
+ )
138
+ current_start_frame += 1
139
+
140
+ # Step 3: Temporal denoising loop
141
+ all_num_frames = [self.num_frame_per_block] * num_blocks
142
+ if self.independent_first_frame and initial_latent is None:
143
+ all_num_frames = [1] + all_num_frames
144
+ num_denoising_steps = len(self.denoising_step_list)
145
+ exit_flags = self.generate_and_sync_list(
146
+ len(all_num_frames), num_denoising_steps, device=noise.device
147
+ )
148
+ start_gradient_frame_index = num_output_frames - 21
149
+
150
+ # for block_index in range(num_blocks):
151
+ for block_index, current_num_frames in enumerate(all_num_frames):
152
+ noisy_input = noise[
153
+ :,
154
+ current_start_frame
155
+ - num_input_frames : current_start_frame
156
+ + current_num_frames
157
+ - num_input_frames,
158
+ ]
159
+
160
+ # Step 3.1: Spatial denoising loop
161
+ for index, current_timestep in enumerate(self.denoising_step_list):
162
+ if self.same_step_across_blocks:
163
+ exit_flag = index == exit_flags[0]
164
+ else:
165
+ exit_flag = (
166
+ index == exit_flags[block_index]
167
+ ) # Only backprop at the randomly selected timestep (consistent across all ranks)
168
+ timestep = (
169
+ torch.ones(
170
+ [batch_size, current_num_frames],
171
+ device=noise.device,
172
+ dtype=torch.int64,
173
+ )
174
+ * current_timestep
175
+ )
176
+
177
+ if not exit_flag:
178
+ with torch.no_grad():
179
+ _, denoised_pred = self.generator(
180
+ noisy_image_or_video=noisy_input,
181
+ conditional_dict=conditional_dict,
182
+ timestep=timestep,
183
+ kv_cache=self.kv_cache1,
184
+ crossattn_cache=self.crossattn_cache,
185
+ current_start=current_start_frame * self.frame_seq_length,
186
+ )
187
+ next_timestep = self.denoising_step_list[index + 1]
188
+ noisy_input = self.scheduler.add_noise(
189
+ denoised_pred.flatten(0, 1),
190
+ torch.randn_like(denoised_pred.flatten(0, 1)),
191
+ next_timestep
192
+ * torch.ones(
193
+ [batch_size * current_num_frames],
194
+ device=noise.device,
195
+ dtype=torch.long,
196
+ ),
197
+ ).unflatten(0, denoised_pred.shape[:2])
198
+ else:
199
+ # for getting real output
200
+ # with torch.set_grad_enabled(current_start_frame >= start_gradient_frame_index):
201
+ if current_start_frame < start_gradient_frame_index:
202
+ with torch.no_grad():
203
+ _, denoised_pred = self.generator(
204
+ noisy_image_or_video=noisy_input,
205
+ conditional_dict=conditional_dict,
206
+ timestep=timestep,
207
+ kv_cache=self.kv_cache1,
208
+ crossattn_cache=self.crossattn_cache,
209
+ current_start=current_start_frame * self.frame_seq_length,
210
+ )
211
+ else:
212
+ _, denoised_pred = self.generator(
213
+ noisy_image_or_video=noisy_input,
214
+ conditional_dict=conditional_dict,
215
+ timestep=timestep,
216
+ kv_cache=self.kv_cache1,
217
+ crossattn_cache=self.crossattn_cache,
218
+ current_start=current_start_frame * self.frame_seq_length,
219
+ )
220
+ break
221
+
222
+ # Step 3.2: record the model's output
223
+ output[
224
+ :,
225
+ current_start_frame : current_start_frame + current_num_frames,
226
+ ] = denoised_pred
227
+
228
+ # Step 3.3: rerun with timestep zero to update the cache
229
+ context_timestep = torch.ones_like(timestep) * self.context_noise
230
+ # add context noise
231
+ denoised_pred = self.scheduler.add_noise(
232
+ denoised_pred.flatten(0, 1),
233
+ torch.randn_like(denoised_pred.flatten(0, 1)),
234
+ context_timestep
235
+ * torch.ones(
236
+ [batch_size * current_num_frames],
237
+ device=noise.device,
238
+ dtype=torch.long,
239
+ ),
240
+ ).unflatten(0, denoised_pred.shape[:2])
241
+ with torch.no_grad():
242
+ self.generator(
243
+ noisy_image_or_video=denoised_pred,
244
+ conditional_dict=conditional_dict,
245
+ timestep=context_timestep,
246
+ kv_cache=self.kv_cache1,
247
+ crossattn_cache=self.crossattn_cache,
248
+ current_start=current_start_frame * self.frame_seq_length,
249
+ )
250
+
251
+ # Step 3.4: update the start and end frame indices
252
+ current_start_frame += current_num_frames
253
+
254
+ # Step 3.5: Return the denoised timestep
255
+ if not self.same_step_across_blocks:
256
+ denoised_timestep_from, denoised_timestep_to = None, None
257
+ elif exit_flags[0] == len(self.denoising_step_list) - 1:
258
+ denoised_timestep_to = 0
259
+ denoised_timestep_from = (
260
+ 1000
261
+ - torch.argmin(
262
+ (
263
+ self.scheduler.timesteps.cuda()
264
+ - self.denoising_step_list[exit_flags[0]].cuda()
265
+ ).abs(),
266
+ dim=0,
267
+ ).item()
268
+ )
269
+ else:
270
+ denoised_timestep_to = (
271
+ 1000
272
+ - torch.argmin(
273
+ (
274
+ self.scheduler.timesteps.cuda()
275
+ - self.denoising_step_list[exit_flags[0] + 1].cuda()
276
+ ).abs(),
277
+ dim=0,
278
+ ).item()
279
+ )
280
+ denoised_timestep_from = (
281
+ 1000
282
+ - torch.argmin(
283
+ (
284
+ self.scheduler.timesteps.cuda()
285
+ - self.denoising_step_list[exit_flags[0]].cuda()
286
+ ).abs(),
287
+ dim=0,
288
+ ).item()
289
+ )
290
+
291
+ if return_sim_step:
292
+ return (
293
+ output,
294
+ denoised_timestep_from,
295
+ denoised_timestep_to,
296
+ exit_flags[0] + 1,
297
+ )
298
+
299
+ return output, denoised_timestep_from, denoised_timestep_to
300
+
301
+ def _initialize_kv_cache(
302
+ self,
303
+ batch_size,
304
+ dtype,
305
+ device,
306
+ ):
307
+ """
308
+ Initialize a Per-GPU KV cache for the Wan model.
309
+ """
310
+ kv_cache1 = []
311
+
312
+ for _ in range(self.num_transformer_blocks):
313
+ kv_cache1.append(
314
+ {
315
+ "k": torch.zeros(
316
+ [batch_size, self.kv_cache_size, 12, 128],
317
+ dtype=dtype,
318
+ device=device,
319
+ ),
320
+ "v": torch.zeros(
321
+ [batch_size, self.kv_cache_size, 12, 128],
322
+ dtype=dtype,
323
+ device=device,
324
+ ),
325
+ "global_end_index": torch.tensor([0], dtype=torch.long, device=device),
326
+ "local_end_index": torch.tensor([0], dtype=torch.long, device=device),
327
+ }
328
+ )
329
+
330
+ self.kv_cache1 = kv_cache1 # always store the clean cache
331
+
332
+ def _initialize_crossattn_cache(
333
+ self,
334
+ batch_size,
335
+ dtype,
336
+ device,
337
+ ):
338
+ """
339
+ Initialize a Per-GPU cross-attention cache for the Wan model.
340
+ """
341
+ crossattn_cache = []
342
+
343
+ for _ in range(self.num_transformer_blocks):
344
+ crossattn_cache.append(
345
+ {
346
+ "k": torch.zeros([batch_size, 512, 12, 128], dtype=dtype, device=device),
347
+ "v": torch.zeros([batch_size, 512, 12, 128], dtype=dtype, device=device),
348
+ "is_init": False,
349
+ }
350
+ )
351
+ self.crossattn_cache = crossattn_cache
prompts/MovieGenVideoBench.txt ADDED
The diff for this file is too large to render. See raw diff
 
prompts/MovieGenVideoBench_extended.txt ADDED
The diff for this file is too large to render. See raw diff
 
prompts/vbench/all_dimension.txt ADDED
@@ -0,0 +1,946 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ In a still frame, a stop sign
2
+ a toilet, frozen in time
3
+ a laptop, frozen in time
4
+ A tranquil tableau of alley
5
+ A tranquil tableau of bar
6
+ A tranquil tableau of barn
7
+ A tranquil tableau of bathroom
8
+ A tranquil tableau of bedroom
9
+ A tranquil tableau of cliff
10
+ In a still frame, courtyard
11
+ In a still frame, gas station
12
+ A tranquil tableau of house
13
+ indoor gymnasium, frozen in time
14
+ A tranquil tableau of indoor library
15
+ A tranquil tableau of kitchen
16
+ A tranquil tableau of palace
17
+ In a still frame, parking lot
18
+ In a still frame, phone booth
19
+ A tranquil tableau of restaurant
20
+ A tranquil tableau of tower
21
+ A tranquil tableau of a bowl
22
+ A tranquil tableau of an apple
23
+ A tranquil tableau of a bench
24
+ A tranquil tableau of a bed
25
+ A tranquil tableau of a chair
26
+ A tranquil tableau of a cup
27
+ A tranquil tableau of a dining table
28
+ In a still frame, a pear
29
+ A tranquil tableau of a bunch of grapes
30
+ A tranquil tableau of a bowl on the kitchen counter
31
+ A tranquil tableau of a beautiful, handcrafted ceramic bowl
32
+ A tranquil tableau of an antique bowl
33
+ A tranquil tableau of an exquisite mahogany dining table
34
+ A tranquil tableau of a wooden bench in the park
35
+ A tranquil tableau of a beautiful wrought-iron bench surrounded by blooming flowers
36
+ In a still frame, a park bench with a view of the lake
37
+ A tranquil tableau of a vintage rocking chair was placed on the porch
38
+ A tranquil tableau of the jail cell was small and dimly lit, with cold, steel bars
39
+ A tranquil tableau of the phone booth was tucked away in a quiet alley
40
+ a dilapidated phone booth stood as a relic of a bygone era on the sidewalk, frozen in time
41
+ A tranquil tableau of the old red barn stood weathered and iconic against the backdrop of the countryside
42
+ A tranquil tableau of a picturesque barn was painted a warm shade of red and nestled in a picturesque meadow
43
+ In a still frame, within the desolate desert, an oasis unfolded, characterized by the stoic presence of palm trees and a motionless, glassy pool of water
44
+ In a still frame, the Parthenon's majestic Doric columns stand in serene solitude atop the Acropolis, framed by the tranquil Athenian landscape
45
+ In a still frame, the Temple of Hephaestus, with its timeless Doric grace, stands stoically against the backdrop of a quiet Athens
46
+ In a still frame, the ornate Victorian streetlamp stands solemnly, adorned with intricate ironwork and stained glass panels
47
+ A tranquil tableau of the Stonehenge presented itself as an enigmatic puzzle, each colossal stone meticulously placed against the backdrop of tranquility
48
+ In a still frame, in the vast desert, an oasis nestled among dunes, featuring tall palm trees and an air of serenity
49
+ static view on a desert scene with an oasis, palm trees, and a clear, calm pool of water
50
+ A tranquil tableau of an ornate Victorian streetlamp standing on a cobblestone street corner, illuminating the empty night
51
+ A tranquil tableau of a tranquil lakeside cabin nestled among tall pines, its reflection mirrored perfectly in the calm water
52
+ In a still frame, a vintage gas lantern, adorned with intricate details, gracing a historic cobblestone square
53
+ In a still frame, a tranquil Japanese tea ceremony room, with tatami mats, a delicate tea set, and a bonsai tree in the corner
54
+ A tranquil tableau of the Parthenon stands resolute in its classical elegance, a timeless symbol of Athens' cultural legacy
55
+ A tranquil tableau of in the heart of Plaka, the neoclassical architecture of the old city harmonizes with the ancient ruins
56
+ A tranquil tableau of in the desolate beauty of the American Southwest, Chaco Canyon's ancient ruins whispered tales of an enigmatic civilization that once thrived amidst the arid landscapes
57
+ A tranquil tableau of at the edge of the Arabian Desert, the ancient city of Petra beckoned with its enigmatic rock-carved façades
58
+ In a still frame, amidst the cobblestone streets, an Art Nouveau lamppost stood tall
59
+ A tranquil tableau of in the quaint village square, a traditional wrought-iron streetlamp featured delicate filigree patterns and amber-hued glass panels
60
+ A tranquil tableau of the lampposts were adorned with Art Deco motifs, their geometric shapes and frosted glass creating a sense of vintage glamour
61
+ In a still frame, in the picturesque square, a Gothic-style lamppost adorned with intricate stone carvings added a touch of medieval charm to the setting
62
+ In a still frame, in the heart of the old city, a row of ornate lantern-style streetlamps bathed the narrow alleyway in a warm, welcoming light
63
+ A tranquil tableau of in the heart of the Utah desert, a massive sandstone arch spanned the horizon
64
+ A tranquil tableau of in the Arizona desert, a massive stone bridge arched across a rugged canyon
65
+ A tranquil tableau of in the corner of the minimalist tea room, a bonsai tree added a touch of nature's beauty to the otherwise simple and elegant space
66
+ In a still frame, amidst the hushed ambiance of the traditional tea room, a meticulously arranged tea set awaited, with porcelain cups, a bamboo whisk
67
+ In a still frame, nestled in the Zen garden, a rustic teahouse featured tatami seating and a traditional charcoal brazier
68
+ A tranquil tableau of a country estate's library featured elegant wooden shelves
69
+ A tranquil tableau of beneath the shade of a solitary oak tree, an old wooden park bench sat patiently
70
+ A tranquil tableau of beside a tranquil pond, a weeping willow tree draped its branches gracefully over the water's surface, creating a serene tableau of reflection and calm
71
+ A tranquil tableau of in the Zen garden, a perfectly raked gravel path led to a serene rock garden
72
+ In a still frame, a tranquil pond was fringed by weeping cherry trees, their blossoms drifting lazily onto the glassy surface
73
+ In a still frame, within the historic library's reading room, rows of antique leather chairs and mahogany tables offered a serene haven for literary contemplation
74
+ A tranquil tableau of a peaceful orchid garden showcased a variety of delicate blooms
75
+ A tranquil tableau of in the serene courtyard, a centuries-old stone well stood as a symbol of a bygone era, its mossy stones bearing witness to the passage of time
76
+ a bird and a cat
77
+ a cat and a dog
78
+ a dog and a horse
79
+ a horse and a sheep
80
+ a sheep and a cow
81
+ a cow and an elephant
82
+ an elephant and a bear
83
+ a bear and a zebra
84
+ a zebra and a giraffe
85
+ a giraffe and a bird
86
+ a chair and a couch
87
+ a couch and a potted plant
88
+ a potted plant and a tv
89
+ a tv and a laptop
90
+ a laptop and a remote
91
+ a remote and a keyboard
92
+ a keyboard and a cell phone
93
+ a cell phone and a book
94
+ a book and a clock
95
+ a clock and a backpack
96
+ a backpack and an umbrella
97
+ an umbrella and a handbag
98
+ a handbag and a tie
99
+ a tie and a suitcase
100
+ a suitcase and a vase
101
+ a vase and scissors
102
+ scissors and a teddy bear
103
+ a teddy bear and a frisbee
104
+ a frisbee and skis
105
+ skis and a snowboard
106
+ a snowboard and a sports ball
107
+ a sports ball and a kite
108
+ a kite and a baseball bat
109
+ a baseball bat and a baseball glove
110
+ a baseball glove and a skateboard
111
+ a skateboard and a surfboard
112
+ a surfboard and a tennis racket
113
+ a tennis racket and a bottle
114
+ a bottle and a chair
115
+ an airplane and a train
116
+ a train and a boat
117
+ a boat and an airplane
118
+ a bicycle and a car
119
+ a car and a motorcycle
120
+ a motorcycle and a bus
121
+ a bus and a traffic light
122
+ a traffic light and a fire hydrant
123
+ a fire hydrant and a stop sign
124
+ a stop sign and a parking meter
125
+ a parking meter and a truck
126
+ a truck and a bicycle
127
+ a toilet and a hair drier
128
+ a hair drier and a toothbrush
129
+ a toothbrush and a sink
130
+ a sink and a toilet
131
+ a wine glass and a chair
132
+ a cup and a couch
133
+ a fork and a potted plant
134
+ a knife and a tv
135
+ a spoon and a laptop
136
+ a bowl and a remote
137
+ a banana and a keyboard
138
+ an apple and a cell phone
139
+ a sandwich and a book
140
+ an orange and a clock
141
+ broccoli and a backpack
142
+ a carrot and an umbrella
143
+ a hot dog and a handbag
144
+ a pizza and a tie
145
+ a donut and a suitcase
146
+ a cake and a vase
147
+ an oven and scissors
148
+ a toaster and a teddy bear
149
+ a microwave and a frisbee
150
+ a refrigerator and skis
151
+ a bicycle and an airplane
152
+ a car and a train
153
+ a motorcycle and a boat
154
+ a person and a toilet
155
+ a person and a hair drier
156
+ a person and a toothbrush
157
+ a person and a sink
158
+ A person is riding a bike
159
+ A person is marching
160
+ A person is roller skating
161
+ A person is tasting beer
162
+ A person is clapping
163
+ A person is drawing
164
+ A person is petting animal (not cat)
165
+ A person is eating watermelon
166
+ A person is playing harp
167
+ A person is wrestling
168
+ A person is riding scooter
169
+ A person is sweeping floor
170
+ A person is skateboarding
171
+ A person is dunking basketball
172
+ A person is playing flute
173
+ A person is stretching leg
174
+ A person is tying tie
175
+ A person is skydiving
176
+ A person is shooting goal (soccer)
177
+ A person is playing piano
178
+ A person is finger snapping
179
+ A person is canoeing or kayaking
180
+ A person is laughing
181
+ A person is digging
182
+ A person is clay pottery making
183
+ A person is shooting basketball
184
+ A person is bending back
185
+ A person is shaking hands
186
+ A person is bandaging
187
+ A person is push up
188
+ A person is catching or throwing frisbee
189
+ A person is playing trumpet
190
+ A person is flying kite
191
+ A person is filling eyebrows
192
+ A person is shuffling cards
193
+ A person is folding clothes
194
+ A person is smoking
195
+ A person is tai chi
196
+ A person is squat
197
+ A person is playing controller
198
+ A person is throwing axe
199
+ A person is giving or receiving award
200
+ A person is air drumming
201
+ A person is taking a shower
202
+ A person is planting trees
203
+ A person is sharpening knives
204
+ A person is robot dancing
205
+ A person is rock climbing
206
+ A person is hula hooping
207
+ A person is writing
208
+ A person is bungee jumping
209
+ A person is pushing cart
210
+ A person is cleaning windows
211
+ A person is cutting watermelon
212
+ A person is cheerleading
213
+ A person is washing hands
214
+ A person is ironing
215
+ A person is cutting nails
216
+ A person is hugging
217
+ A person is trimming or shaving beard
218
+ A person is jogging
219
+ A person is making bed
220
+ A person is washing dishes
221
+ A person is grooming dog
222
+ A person is doing laundry
223
+ A person is knitting
224
+ A person is reading book
225
+ A person is baby waking up
226
+ A person is massaging legs
227
+ A person is brushing teeth
228
+ A person is crawling baby
229
+ A person is motorcycling
230
+ A person is driving car
231
+ A person is sticking tongue out
232
+ A person is shaking head
233
+ A person is sword fighting
234
+ A person is doing aerobics
235
+ A person is strumming guitar
236
+ A person is riding or walking with horse
237
+ A person is archery
238
+ A person is catching or throwing baseball
239
+ A person is playing chess
240
+ A person is rock scissors paper
241
+ A person is using computer
242
+ A person is arranging flowers
243
+ A person is bending metal
244
+ A person is ice skating
245
+ A person is climbing a rope
246
+ A person is crying
247
+ A person is dancing ballet
248
+ A person is getting a haircut
249
+ A person is running on treadmill
250
+ A person is kissing
251
+ A person is counting money
252
+ A person is barbequing
253
+ A person is peeling apples
254
+ A person is milking cow
255
+ A person is shining shoes
256
+ A person is making snowman
257
+ A person is sailing
258
+ a person swimming in ocean
259
+ a person giving a presentation to a room full of colleagues
260
+ a person washing the dishes
261
+ a person eating a burger
262
+ a person walking in the snowstorm
263
+ a person drinking coffee in a cafe
264
+ a person playing guitar
265
+ a bicycle leaning against a tree
266
+ a bicycle gliding through a snowy field
267
+ a bicycle slowing down to stop
268
+ a bicycle accelerating to gain speed
269
+ a car stuck in traffic during rush hour
270
+ a car turning a corner
271
+ a car slowing down to stop
272
+ a car accelerating to gain speed
273
+ a motorcycle cruising along a coastal highway
274
+ a motorcycle turning a corner
275
+ a motorcycle slowing down to stop
276
+ a motorcycle gliding through a snowy field
277
+ a motorcycle accelerating to gain speed
278
+ an airplane soaring through a clear blue sky
279
+ an airplane taking off
280
+ an airplane landing smoothly on a runway
281
+ an airplane accelerating to gain speed
282
+ a bus turning a corner
283
+ a bus stuck in traffic during rush hour
284
+ a bus accelerating to gain speed
285
+ a train speeding down the tracks
286
+ a train crossing over a tall bridge
287
+ a train accelerating to gain speed
288
+ a truck turning a corner
289
+ a truck anchored in a tranquil bay
290
+ a truck stuck in traffic during rush hour
291
+ a truck slowing down to stop
292
+ a truck accelerating to gain speed
293
+ a boat sailing smoothly on a calm lake
294
+ a boat slowing down to stop
295
+ a boat accelerating to gain speed
296
+ a bird soaring gracefully in the sky
297
+ a bird building a nest from twigs and leaves
298
+ a bird flying over a snowy forest
299
+ a cat grooming itself meticulously with its tongue
300
+ a cat playing in park
301
+ a cat drinking water
302
+ a cat running happily
303
+ a dog enjoying a peaceful walk
304
+ a dog playing in park
305
+ a dog drinking water
306
+ a dog running happily
307
+ a horse bending down to drink water from a river
308
+ a horse galloping across an open field
309
+ a horse taking a peaceful walk
310
+ a horse running to join a herd of its kind
311
+ a sheep bending down to drink water from a river
312
+ a sheep taking a peaceful walk
313
+ a sheep running to join a herd of its kind
314
+ a cow bending down to drink water from a river
315
+ a cow chewing cud while resting in a tranquil barn
316
+ a cow running to join a herd of its kind
317
+ an elephant spraying itself with water using its trunk to cool down
318
+ an elephant taking a peaceful walk
319
+ an elephant running to join a herd of its kind
320
+ a bear catching a salmon in its powerful jaws
321
+ a bear sniffing the air for scents of food
322
+ a bear climbing a tree
323
+ a bear hunting for prey
324
+ a zebra bending down to drink water from a river
325
+ a zebra running to join a herd of its kind
326
+ a zebra taking a peaceful walk
327
+ a giraffe bending down to drink water from a river
328
+ a giraffe taking a peaceful walk
329
+ a giraffe running to join a herd of its kind
330
+ a person
331
+ a bicycle
332
+ a car
333
+ a motorcycle
334
+ an airplane
335
+ a bus
336
+ a train
337
+ a truck
338
+ a boat
339
+ a traffic light
340
+ a fire hydrant
341
+ a stop sign
342
+ a parking meter
343
+ a bench
344
+ a bird
345
+ a cat
346
+ a dog
347
+ a horse
348
+ a sheep
349
+ a cow
350
+ an elephant
351
+ a bear
352
+ a zebra
353
+ a giraffe
354
+ a backpack
355
+ an umbrella
356
+ a handbag
357
+ a tie
358
+ a suitcase
359
+ a frisbee
360
+ skis
361
+ a snowboard
362
+ a sports ball
363
+ a kite
364
+ a baseball bat
365
+ a baseball glove
366
+ a skateboard
367
+ a surfboard
368
+ a tennis racket
369
+ a bottle
370
+ a wine glass
371
+ a cup
372
+ a fork
373
+ a knife
374
+ a spoon
375
+ a bowl
376
+ a banana
377
+ an apple
378
+ a sandwich
379
+ an orange
380
+ broccoli
381
+ a carrot
382
+ a hot dog
383
+ a pizza
384
+ a donut
385
+ a cake
386
+ a chair
387
+ a couch
388
+ a potted plant
389
+ a bed
390
+ a dining table
391
+ a toilet
392
+ a tv
393
+ a laptop
394
+ a remote
395
+ a keyboard
396
+ a cell phone
397
+ a microwave
398
+ an oven
399
+ a toaster
400
+ a sink
401
+ a refrigerator
402
+ a book
403
+ a clock
404
+ a vase
405
+ scissors
406
+ a teddy bear
407
+ a hair drier
408
+ a toothbrush
409
+ a red bicycle
410
+ a green bicycle
411
+ a blue bicycle
412
+ a yellow bicycle
413
+ an orange bicycle
414
+ a purple bicycle
415
+ a pink bicycle
416
+ a black bicycle
417
+ a white bicycle
418
+ a red car
419
+ a green car
420
+ a blue car
421
+ a yellow car
422
+ an orange car
423
+ a purple car
424
+ a pink car
425
+ a black car
426
+ a white car
427
+ a red bird
428
+ a green bird
429
+ a blue bird
430
+ a yellow bird
431
+ an orange bird
432
+ a purple bird
433
+ a pink bird
434
+ a black bird
435
+ a white bird
436
+ a black cat
437
+ a white cat
438
+ an orange cat
439
+ a yellow cat
440
+ a red umbrella
441
+ a green umbrella
442
+ a blue umbrella
443
+ a yellow umbrella
444
+ an orange umbrella
445
+ a purple umbrella
446
+ a pink umbrella
447
+ a black umbrella
448
+ a white umbrella
449
+ a red suitcase
450
+ a green suitcase
451
+ a blue suitcase
452
+ a yellow suitcase
453
+ an orange suitcase
454
+ a purple suitcase
455
+ a pink suitcase
456
+ a black suitcase
457
+ a white suitcase
458
+ a red bowl
459
+ a green bowl
460
+ a blue bowl
461
+ a yellow bowl
462
+ an orange bowl
463
+ a purple bowl
464
+ a pink bowl
465
+ a black bowl
466
+ a white bowl
467
+ a red chair
468
+ a green chair
469
+ a blue chair
470
+ a yellow chair
471
+ an orange chair
472
+ a purple chair
473
+ a pink chair
474
+ a black chair
475
+ a white chair
476
+ a red clock
477
+ a green clock
478
+ a blue clock
479
+ a yellow clock
480
+ an orange clock
481
+ a purple clock
482
+ a pink clock
483
+ a black clock
484
+ a white clock
485
+ a red vase
486
+ a green vase
487
+ a blue vase
488
+ a yellow vase
489
+ an orange vase
490
+ a purple vase
491
+ a pink vase
492
+ a black vase
493
+ a white vase
494
+ A beautiful coastal beach in spring, waves lapping on sand, Van Gogh style
495
+ A beautiful coastal beach in spring, waves lapping on sand, oil painting
496
+ A beautiful coastal beach in spring, waves lapping on sand by Hokusai, in the style of Ukiyo
497
+ A beautiful coastal beach in spring, waves lapping on sand, black and white
498
+ A beautiful coastal beach in spring, waves lapping on sand, pixel art
499
+ A beautiful coastal beach in spring, waves lapping on sand, in cyberpunk style
500
+ A beautiful coastal beach in spring, waves lapping on sand, animated style
501
+ A beautiful coastal beach in spring, waves lapping on sand, watercolor painting
502
+ A beautiful coastal beach in spring, waves lapping on sand, surrealism style
503
+ The bund Shanghai, Van Gogh style
504
+ The bund Shanghai, oil painting
505
+ The bund Shanghai by Hokusai, in the style of Ukiyo
506
+ The bund Shanghai, black and white
507
+ The bund Shanghai, pixel art
508
+ The bund Shanghai, in cyberpunk style
509
+ The bund Shanghai, animated style
510
+ The bund Shanghai, watercolor painting
511
+ The bund Shanghai, surrealism style
512
+ a shark is swimming in the ocean, Van Gogh style
513
+ a shark is swimming in the ocean, oil painting
514
+ a shark is swimming in the ocean by Hokusai, in the style of Ukiyo
515
+ a shark is swimming in the ocean, black and white
516
+ a shark is swimming in the ocean, pixel art
517
+ a shark is swimming in the ocean, in cyberpunk style
518
+ a shark is swimming in the ocean, animated style
519
+ a shark is swimming in the ocean, watercolor painting
520
+ a shark is swimming in the ocean, surrealism style
521
+ A panda drinking coffee in a cafe in Paris, Van Gogh style
522
+ A panda drinking coffee in a cafe in Paris, oil painting
523
+ A panda drinking coffee in a cafe in Paris by Hokusai, in the style of Ukiyo
524
+ A panda drinking coffee in a cafe in Paris, black and white
525
+ A panda drinking coffee in a cafe in Paris, pixel art
526
+ A panda drinking coffee in a cafe in Paris, in cyberpunk style
527
+ A panda drinking coffee in a cafe in Paris, animated style
528
+ A panda drinking coffee in a cafe in Paris, watercolor painting
529
+ A panda drinking coffee in a cafe in Paris, surrealism style
530
+ A cute happy Corgi playing in park, sunset, Van Gogh style
531
+ A cute happy Corgi playing in park, sunset, oil painting
532
+ A cute happy Corgi playing in park, sunset by Hokusai, in the style of Ukiyo
533
+ A cute happy Corgi playing in park, sunset, black and white
534
+ A cute happy Corgi playing in park, sunset, pixel art
535
+ A cute happy Corgi playing in park, sunset, in cyberpunk style
536
+ A cute happy Corgi playing in park, sunset, animated style
537
+ A cute happy Corgi playing in park, sunset, watercolor painting
538
+ A cute happy Corgi playing in park, sunset, surrealism style
539
+ Gwen Stacy reading a book, Van Gogh style
540
+ Gwen Stacy reading a book, oil painting
541
+ Gwen Stacy reading a book by Hokusai, in the style of Ukiyo
542
+ Gwen Stacy reading a book, black and white
543
+ Gwen Stacy reading a book, pixel art
544
+ Gwen Stacy reading a book, in cyberpunk style
545
+ Gwen Stacy reading a book, animated style
546
+ Gwen Stacy reading a book, watercolor painting
547
+ Gwen Stacy reading a book, surrealism style
548
+ A boat sailing leisurely along the Seine River with the Eiffel Tower in background, Van Gogh style
549
+ A boat sailing leisurely along the Seine River with the Eiffel Tower in background, oil painting
550
+ A boat sailing leisurely along the Seine River with the Eiffel Tower in background by Hokusai, in the style of Ukiyo
551
+ A boat sailing leisurely along the Seine River with the Eiffel Tower in background, black and white
552
+ A boat sailing leisurely along the Seine River with the Eiffel Tower in background, pixel art
553
+ A boat sailing leisurely along the Seine River with the Eiffel Tower in background, in cyberpunk style
554
+ A boat sailing leisurely along the Seine River with the Eiffel Tower in background, animated style
555
+ A boat sailing leisurely along the Seine River with the Eiffel Tower in background, watercolor painting
556
+ A boat sailing leisurely along the Seine River with the Eiffel Tower in background, surrealism style
557
+ A couple in formal evening wear going home get caught in a heavy downpour with umbrellas, Van Gogh style
558
+ A couple in formal evening wear going home get caught in a heavy downpour with umbrellas, oil painting
559
+ A couple in formal evening wear going home get caught in a heavy downpour with umbrellas by Hokusai, in the style of Ukiyo
560
+ A couple in formal evening wear going home get caught in a heavy downpour with umbrellas, black and white
561
+ A couple in formal evening wear going home get caught in a heavy downpour with umbrellas, pixel art
562
+ A couple in formal evening wear going home get caught in a heavy downpour with umbrellas, in cyberpunk style
563
+ A couple in formal evening wear going home get caught in a heavy downpour with umbrellas, animated style
564
+ A couple in formal evening wear going home get caught in a heavy downpour with umbrellas, watercolor painting
565
+ A couple in formal evening wear going home get caught in a heavy downpour with umbrellas, surrealism style
566
+ An astronaut flying in space, Van Gogh style
567
+ An astronaut flying in space, oil painting
568
+ An astronaut flying in space by Hokusai, in the style of Ukiyo
569
+ An astronaut flying in space, black and white
570
+ An astronaut flying in space, pixel art
571
+ An astronaut flying in space, in cyberpunk style
572
+ An astronaut flying in space, animated style
573
+ An astronaut flying in space, watercolor painting
574
+ An astronaut flying in space, surrealism style
575
+ Snow rocky mountains peaks canyon. snow blanketed rocky mountains surround and shadow deep canyons. the canyons twist and bend through the high elevated mountain peaks, Van Gogh style
576
+ Snow rocky mountains peaks canyon. snow blanketed rocky mountains surround and shadow deep canyons. the canyons twist and bend through the high elevated mountain peaks, oil painting
577
+ Snow rocky mountains peaks canyon. snow blanketed rocky mountains surround and shadow deep canyons. the canyons twist and bend through the high elevated mountain peaks by Hokusai, in the style of Ukiyo
578
+ Snow rocky mountains peaks canyon. snow blanketed rocky mountains surround and shadow deep canyons. the canyons twist and bend through the high elevated mountain peaks, black and white
579
+ Snow rocky mountains peaks canyon. snow blanketed rocky mountains surround and shadow deep canyons. the canyons twist and bend through the high elevated mountain peaks, pixel art
580
+ Snow rocky mountains peaks canyon. snow blanketed rocky mountains surround and shadow deep canyons. the canyons twist and bend through the high elevated mountain peaks, in cyberpunk style
581
+ Snow rocky mountains peaks canyon. snow blanketed rocky mountains surround and shadow deep canyons. the canyons twist and bend through the high elevated mountain peaks, animated style
582
+ Snow rocky mountains peaks canyon. snow blanketed rocky mountains surround and shadow deep canyons. the canyons twist and bend through the high elevated mountain peaks, watercolor painting
583
+ Snow rocky mountains peaks canyon. snow blanketed rocky mountains surround and shadow deep canyons. the canyons twist and bend through the high elevated mountain peaks, surrealism style
584
+ A beautiful coastal beach in spring, waves lapping on sand, in super slow motion
585
+ A beautiful coastal beach in spring, waves lapping on sand, zoom in
586
+ A beautiful coastal beach in spring, waves lapping on sand, zoom out
587
+ A beautiful coastal beach in spring, waves lapping on sand, pan left
588
+ A beautiful coastal beach in spring, waves lapping on sand, pan right
589
+ A beautiful coastal beach in spring, waves lapping on sand, tilt up
590
+ A beautiful coastal beach in spring, waves lapping on sand, tilt down
591
+ A beautiful coastal beach in spring, waves lapping on sand, with an intense shaking effect
592
+ A beautiful coastal beach in spring, waves lapping on sand, featuring a steady and smooth perspective
593
+ A beautiful coastal beach in spring, waves lapping on sand, racking focus
594
+ The bund Shanghai, in super slow motion
595
+ The bund Shanghai, zoom in
596
+ The bund Shanghai, zoom out
597
+ The bund Shanghai, pan left
598
+ The bund Shanghai, pan right
599
+ The bund Shanghai, tilt up
600
+ The bund Shanghai, tilt down
601
+ The bund Shanghai, with an intense shaking effect
602
+ The bund Shanghai, featuring a steady and smooth perspective
603
+ The bund Shanghai, racking focus
604
+ a shark is swimming in the ocean, in super slow motion
605
+ a shark is swimming in the ocean, zoom in
606
+ a shark is swimming in the ocean, zoom out
607
+ a shark is swimming in the ocean, pan left
608
+ a shark is swimming in the ocean, pan right
609
+ a shark is swimming in the ocean, tilt up
610
+ a shark is swimming in the ocean, tilt down
611
+ a shark is swimming in the ocean, with an intense shaking effect
612
+ a shark is swimming in the ocean, featuring a steady and smooth perspective
613
+ a shark is swimming in the ocean, racking focus
614
+ A panda drinking coffee in a cafe in Paris, in super slow motion
615
+ A panda drinking coffee in a cafe in Paris, zoom in
616
+ A panda drinking coffee in a cafe in Paris, zoom out
617
+ A panda drinking coffee in a cafe in Paris, pan left
618
+ A panda drinking coffee in a cafe in Paris, pan right
619
+ A panda drinking coffee in a cafe in Paris, tilt up
620
+ A panda drinking coffee in a cafe in Paris, tilt down
621
+ A panda drinking coffee in a cafe in Paris, with an intense shaking effect
622
+ A panda drinking coffee in a cafe in Paris, featuring a steady and smooth perspective
623
+ A panda drinking coffee in a cafe in Paris, racking focus
624
+ A cute happy Corgi playing in park, sunset, in super slow motion
625
+ A cute happy Corgi playing in park, sunset, zoom in
626
+ A cute happy Corgi playing in park, sunset, zoom out
627
+ A cute happy Corgi playing in park, sunset, pan left
628
+ A cute happy Corgi playing in park, sunset, pan right
629
+ A cute happy Corgi playing in park, sunset, tilt up
630
+ A cute happy Corgi playing in park, sunset, tilt down
631
+ A cute happy Corgi playing in park, sunset, with an intense shaking effect
632
+ A cute happy Corgi playing in park, sunset, featuring a steady and smooth perspective
633
+ A cute happy Corgi playing in park, sunset, racking focus
634
+ Gwen Stacy reading a book, in super slow motion
635
+ Gwen Stacy reading a book, zoom in
636
+ Gwen Stacy reading a book, zoom out
637
+ Gwen Stacy reading a book, pan left
638
+ Gwen Stacy reading a book, pan right
639
+ Gwen Stacy reading a book, tilt up
640
+ Gwen Stacy reading a book, tilt down
641
+ Gwen Stacy reading a book, with an intense shaking effect
642
+ Gwen Stacy reading a book, featuring a steady and smooth perspective
643
+ Gwen Stacy reading a book, racking focus
644
+ A boat sailing leisurely along the Seine River with the Eiffel Tower in background, in super slow motion
645
+ A boat sailing leisurely along the Seine River with the Eiffel Tower in background, zoom in
646
+ A boat sailing leisurely along the Seine River with the Eiffel Tower in background, zoom out
647
+ A boat sailing leisurely along the Seine River with the Eiffel Tower in background, pan left
648
+ A boat sailing leisurely along the Seine River with the Eiffel Tower in background, pan right
649
+ A boat sailing leisurely along the Seine River with the Eiffel Tower in background, tilt up
650
+ A boat sailing leisurely along the Seine River with the Eiffel Tower in background, tilt down
651
+ A boat sailing leisurely along the Seine River with the Eiffel Tower in background, with an intense shaking effect
652
+ A boat sailing leisurely along the Seine River with the Eiffel Tower in background, featuring a steady and smooth perspective
653
+ A boat sailing leisurely along the Seine River with the Eiffel Tower in background, racking focus
654
+ A couple in formal evening wear going home get caught in a heavy downpour with umbrellas, in super slow motion
655
+ A couple in formal evening wear going home get caught in a heavy downpour with umbrellas, zoom in
656
+ A couple in formal evening wear going home get caught in a heavy downpour with umbrellas, zoom out
657
+ A couple in formal evening wear going home get caught in a heavy downpour with umbrellas, pan left
658
+ A couple in formal evening wear going home get caught in a heavy downpour with umbrellas, pan right
659
+ A couple in formal evening wear going home get caught in a heavy downpour with umbrellas, tilt up
660
+ A couple in formal evening wear going home get caught in a heavy downpour with umbrellas, tilt down
661
+ A couple in formal evening wear going home get caught in a heavy downpour with umbrellas, with an intense shaking effect
662
+ A couple in formal evening wear going home get caught in a heavy downpour with umbrellas, featuring a steady and smooth perspective
663
+ A couple in formal evening wear going home get caught in a heavy downpour with umbrellas, racking focus
664
+ An astronaut flying in space, in super slow motion
665
+ An astronaut flying in space, zoom in
666
+ An astronaut flying in space, zoom out
667
+ An astronaut flying in space, pan left
668
+ An astronaut flying in space, pan right
669
+ An astronaut flying in space, tilt up
670
+ An astronaut flying in space, tilt down
671
+ An astronaut flying in space, with an intense shaking effect
672
+ An astronaut flying in space, featuring a steady and smooth perspective
673
+ An astronaut flying in space, racking focus
674
+ Snow rocky mountains peaks canyon. snow blanketed rocky mountains surround and shadow deep canyons. the canyons twist and bend through the high elevated mountain peaks, in super slow motion
675
+ Snow rocky mountains peaks canyon. snow blanketed rocky mountains surround and shadow deep canyons. the canyons twist and bend through the high elevated mountain peaks, zoom in
676
+ Snow rocky mountains peaks canyon. snow blanketed rocky mountains surround and shadow deep canyons. the canyons twist and bend through the high elevated mountain peaks, zoom out
677
+ Snow rocky mountains peaks canyon. snow blanketed rocky mountains surround and shadow deep canyons. the canyons twist and bend through the high elevated mountain peaks, pan left
678
+ Snow rocky mountains peaks canyon. snow blanketed rocky mountains surround and shadow deep canyons. the canyons twist and bend through the high elevated mountain peaks, pan right
679
+ Snow rocky mountains peaks canyon. snow blanketed rocky mountains surround and shadow deep canyons. the canyons twist and bend through the high elevated mountain peaks, tilt up
680
+ Snow rocky mountains peaks canyon. snow blanketed rocky mountains surround and shadow deep canyons. the canyons twist and bend through the high elevated mountain peaks, tilt down
681
+ Snow rocky mountains peaks canyon. snow blanketed rocky mountains surround and shadow deep canyons. the canyons twist and bend through the high elevated mountain peaks, with an intense shaking effect
682
+ Snow rocky mountains peaks canyon. snow blanketed rocky mountains surround and shadow deep canyons. the canyons twist and bend through the high elevated mountain peaks, featuring a steady and smooth perspective
683
+ Snow rocky mountains peaks canyon. snow blanketed rocky mountains surround and shadow deep canyons. the canyons twist and bend through the high elevated mountain peaks, racking focus
684
+ Close up of grapes on a rotating table.
685
+ Turtle swimming in ocean.
686
+ A storm trooper vacuuming the beach.
687
+ A panda standing on a surfboard in the ocean in sunset.
688
+ An astronaut feeding ducks on a sunny afternoon, reflection from the water.
689
+ Two pandas discussing an academic paper.
690
+ Sunset time lapse at the beach with moving clouds and colors in the sky.
691
+ A fat rabbit wearing a purple robe walking through a fantasy landscape.
692
+ A koala bear playing piano in the forest.
693
+ An astronaut flying in space.
694
+ Fireworks.
695
+ An animated painting of fluffy white clouds moving in sky.
696
+ Flying through fantasy landscapes.
697
+ A bigfoot walking in the snowstorm.
698
+ A squirrel eating a burger.
699
+ A cat wearing sunglasses and working as a lifeguard at a pool.
700
+ Snow rocky mountains peaks canyon. snow blanketed rocky mountains surround and shadow deep canyons. the canyons twist and bend through the high elevated mountain peaks.
701
+ Splash of turquoise water in extreme slow motion, alpha channel included.
702
+ an ice cream is melting on the table.
703
+ a drone flying over a snowy forest.
704
+ a shark is swimming in the ocean.
705
+ Aerial panoramic video from a drone of a fantasy land.
706
+ a teddy bear is swimming in the ocean.
707
+ time lapse of sunrise on mars.
708
+ golden fish swimming in the ocean.
709
+ An artist brush painting on a canvas close up.
710
+ A drone view of celebration with Christmas tree and fireworks, starry sky - background.
711
+ happy dog wearing a yellow turtleneck, studio, portrait, facing camera, dark background
712
+ Origami dancers in white paper, 3D render, on white background, studio shot, dancing modern dance.
713
+ Campfire at night in a snowy forest with starry sky in the background.
714
+ a fantasy landscape
715
+ A 3D model of a 1800s victorian house.
716
+ this is how I do makeup in the morning.
717
+ A raccoon that looks like a turtle, digital art.
718
+ Robot dancing in Times Square.
719
+ Busy freeway at night.
720
+ Balloon full of water exploding in extreme slow motion.
721
+ An astronaut is riding a horse in the space in a photorealistic style.
722
+ Macro slo-mo. Slow motion cropped closeup of roasted coffee beans falling into an empty bowl.
723
+ Sewing machine, old sewing machine working.
724
+ Motion colour drop in water, ink swirling in water, colourful ink in water, abstraction fancy dream cloud of ink.
725
+ Few big purple plums rotating on the turntable. water drops appear on the skin during rotation. isolated on the white background. close-up. macro.
726
+ Vampire makeup face of beautiful girl, red contact lenses.
727
+ Ashtray full of butts on table, smoke flowing on black background, close-up
728
+ Pacific coast, carmel by the sea ocean and waves.
729
+ A teddy bear is playing drum kit in NYC Times Square.
730
+ A corgi is playing drum kit.
731
+ An Iron man is playing the electronic guitar, high electronic guitar.
732
+ A raccoon is playing the electronic guitar.
733
+ A boat sailing leisurely along the Seine River with the Eiffel Tower in background by Vincent van Gogh
734
+ A corgi's head depicted as an explosion of a nebula
735
+ A fantasy landscape
736
+ A future where humans have achieved teleportation technology
737
+ A jellyfish floating through the ocean, with bioluminescent tentacles
738
+ A Mars rover moving on Mars
739
+ A panda drinking coffee in a cafe in Paris
740
+ A space shuttle launching into orbit, with flames and smoke billowing out from the engines
741
+ A steam train moving on a mountainside
742
+ A super cool giant robot in Cyberpunk Beijing
743
+ A tropical beach at sunrise, with palm trees and crystal-clear water in the foreground
744
+ Cinematic shot of Van Gogh's selfie, Van Gogh style
745
+ Gwen Stacy reading a book
746
+ Iron Man flying in the sky
747
+ The bund Shanghai, oil painting
748
+ Yoda playing guitar on the stage
749
+ A beautiful coastal beach in spring, waves lapping on sand by Hokusai, in the style of Ukiyo
750
+ A beautiful coastal beach in spring, waves lapping on sand by Vincent van Gogh
751
+ A boat sailing leisurely along the Seine River with the Eiffel Tower in background
752
+ A car moving slowly on an empty street, rainy evening
753
+ A cat eating food out of a bowl
754
+ A cat wearing sunglasses at a pool
755
+ A confused panda in calculus class
756
+ A cute fluffy panda eating Chinese food in a restaurant
757
+ A cute happy Corgi playing in park, sunset
758
+ A cute raccoon playing guitar in a boat on the ocean
759
+ A happy fuzzy panda playing guitar nearby a campfire, snow mountain in the background
760
+ A lightning striking atop of eiffel tower, dark clouds in the sky
761
+ A modern art museum, with colorful paintings
762
+ A panda cooking in the kitchen
763
+ A panda playing on a swing set
764
+ A polar bear is playing guitar
765
+ A raccoon dressed in suit playing the trumpet, stage background
766
+ A robot DJ is playing the turntable, in heavy raining futuristic tokyo rooftop cyberpunk night, sci-fi, fantasy
767
+ A shark swimming in clear Caribbean ocean
768
+ A super robot protecting city
769
+ A teddy bear washing the dishes
770
+ An epic tornado attacking above a glowing city at night, the tornado is made of smoke
771
+ An oil painting of a couple in formal evening wear going home get caught in a heavy downpour with umbrellas
772
+ Clown fish swimming through the coral reef
773
+ Hyper-realistic spaceship landing on Mars
774
+ The bund Shanghai, vibrant color
775
+ Vincent van Gogh is painting in the room
776
+ Yellow flowers swing in the wind
777
+ alley
778
+ amusement park
779
+ aquarium
780
+ arch
781
+ art gallery
782
+ bathroom
783
+ bakery shop
784
+ ballroom
785
+ bar
786
+ barn
787
+ basement
788
+ beach
789
+ bedroom
790
+ bridge
791
+ botanical garden
792
+ cafeteria
793
+ campsite
794
+ campus
795
+ carrousel
796
+ castle
797
+ cemetery
798
+ classroom
799
+ cliff
800
+ crosswalk
801
+ construction site
802
+ corridor
803
+ courtyard
804
+ desert
805
+ downtown
806
+ driveway
807
+ farm
808
+ food court
809
+ football field
810
+ forest road
811
+ fountain
812
+ gas station
813
+ glacier
814
+ golf course
815
+ indoor gymnasium
816
+ harbor
817
+ highway
818
+ hospital
819
+ house
820
+ iceberg
821
+ industrial area
822
+ jail cell
823
+ junkyard
824
+ kitchen
825
+ indoor library
826
+ lighthouse
827
+ laboratory
828
+ mansion
829
+ marsh
830
+ mountain
831
+ indoor movie theater
832
+ indoor museum
833
+ music studio
834
+ nursery
835
+ ocean
836
+ office
837
+ palace
838
+ parking lot
839
+ pharmacy
840
+ phone booth
841
+ raceway
842
+ restaurant
843
+ river
844
+ science museum
845
+ shower
846
+ ski slope
847
+ sky
848
+ skyscraper
849
+ baseball stadium
850
+ staircase
851
+ street
852
+ supermarket
853
+ indoor swimming pool
854
+ tower
855
+ outdoor track
856
+ train railway
857
+ train station platform
858
+ underwater coral reef
859
+ valley
860
+ volcano
861
+ waterfall
862
+ windmill
863
+ a bicycle on the left of a car, front view
864
+ a car on the right of a motorcycle, front view
865
+ a motorcycle on the left of a bus, front view
866
+ a bus on the right of a traffic light, front view
867
+ a traffic light on the left of a fire hydrant, front view
868
+ a fire hydrant on the right of a stop sign, front view
869
+ a stop sign on the left of a parking meter, front view
870
+ a parking meter on the right of a bench, front view
871
+ a bench on the left of a truck, front view
872
+ a truck on the right of a bicycle, front view
873
+ a bird on the left of a cat, front view
874
+ a cat on the right of a dog, front view
875
+ a dog on the left of a horse, front view
876
+ a horse on the right of a sheep, front view
877
+ a sheep on the left of a cow, front view
878
+ a cow on the right of an elephant, front view
879
+ an elephant on the left of a bear, front view
880
+ a bear on the right of a zebra, front view
881
+ a zebra on the left of a giraffe, front view
882
+ a giraffe on the right of a bird, front view
883
+ a bottle on the left of a wine glass, front view
884
+ a wine glass on the right of a cup, front view
885
+ a cup on the left of a fork, front view
886
+ a fork on the right of a knife, front view
887
+ a knife on the left of a spoon, front view
888
+ a spoon on the right of a bowl, front view
889
+ a bowl on the left of a bottle, front view
890
+ a potted plant on the left of a remote, front view
891
+ a remote on the right of a clock, front view
892
+ a clock on the left of a vase, front view
893
+ a vase on the right of scissors, front view
894
+ scissors on the left of a teddy bear, front view
895
+ a teddy bear on the right of a potted plant, front view
896
+ a frisbee on the left of a sports ball, front view
897
+ a sports ball on the right of a baseball bat, front view
898
+ a baseball bat on the left of a baseball glove, front view
899
+ a baseball glove on the right of a tennis racket, front view
900
+ a tennis racket on the left of a frisbee, front view
901
+ a toilet on the left of a hair drier, front view
902
+ a hair drier on the right of a toothbrush, front view
903
+ a toothbrush on the left of a sink, front view
904
+ a sink on the right of a toilet, front view
905
+ a chair on the left of a couch, front view
906
+ a couch on the right of a bed, front view
907
+ a bed on the left of a tv, front view
908
+ a tv on the right of a dining table, front view
909
+ a dining table on the left of a chair, front view
910
+ an airplane on the left of a train, front view
911
+ a train on the right of a boat, front view
912
+ a boat on the left of an airplane, front view
913
+ an oven on the top of a toaster, front view
914
+ an oven on the bottom of a toaster, front view
915
+ a toaster on the top of a microwave, front view
916
+ a toaster on the bottom of a microwave, front view
917
+ a microwave on the top of an oven, front view
918
+ a microwave on the bottom of an oven, front view
919
+ a banana on the top of an apple, front view
920
+ a banana on the bottom of an apple, front view
921
+ an apple on the top of a sandwich, front view
922
+ an apple on the bottom of a sandwich, front view
923
+ a sandwich on the top of an orange, front view
924
+ a sandwich on the bottom of an orange, front view
925
+ an orange on the top of a carrot, front view
926
+ an orange on the bottom of a carrot, front view
927
+ a carrot on the top of a hot dog, front view
928
+ a carrot on the bottom of a hot dog, front view
929
+ a hot dog on the top of a pizza, front view
930
+ a hot dog on the bottom of a pizza, front view
931
+ a pizza on the top of a donut, front view
932
+ a pizza on the bottom of a donut, front view
933
+ a donut on the top of broccoli, front view
934
+ a donut on the bottom of broccoli, front view
935
+ broccoli on the top of a banana, front view
936
+ broccoli on the bottom of a banana, front view
937
+ skis on the top of a snowboard, front view
938
+ skis on the bottom of a snowboard, front view
939
+ a snowboard on the top of a kite, front view
940
+ a snowboard on the bottom of a kite, front view
941
+ a kite on the top of a skateboard, front view
942
+ a kite on the bottom of a skateboard, front view
943
+ a skateboard on the top of a surfboard, front view
944
+ a skateboard on the bottom of a surfboard, front view
945
+ a surfboard on the top of skis, front view
946
+ a surfboard on the bottom of skis, front view
prompts/vbench/all_dimension_extended.txt ADDED
The diff for this file is too large to render. See raw diff
 
requirements.txt ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ torch>=2.4.0
2
+ torchvision>=0.19.0
3
+ opencv-python>=4.9.0.80
4
+ diffusers==0.31.0
5
+ transformers>=4.49.0
6
+ tokenizers>=0.20.3
7
+ accelerate>=1.1.1
8
+ tqdm
9
+ imageio
10
+ easydict
11
+ ftfy
12
+ dashscope
13
+ imageio-ffmpeg
14
+ numpy==1.24.4
15
+ wandb
16
+ omegaconf
17
+ einops
18
+ av==13.1.0
19
+ opencv-python
20
+ git+https://github.com/openai/CLIP.git
21
+ open_clip_torch
22
+ starlette
23
+ pycocotools
24
+ lmdb
25
+ matplotlib
26
+ sentencepiece
27
+ pydantic==2.10.6
28
+ scikit-image
29
+ huggingface_hub[cli]
30
+ dominate
31
+ nvidia-pyindex
32
+ nvidia-tensorrt
33
+ pycuda
34
+ onnx
35
+ onnxruntime
36
+ onnxscript
37
+ onnxconverter_common
38
+ flask
39
+ flask-socketio
40
+ torchao
41
+ hydra-core
42
+ git+https://github.com/facebookresearch/segment-anything.git
43
+
scripts/create_lmdb_14b_shards.py ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ python create_lmdb_14b_shards.py \
3
+ --data_path /mnt/localssd/wanx_14b_data \
4
+ --lmdb_path /mnt/localssd/wanx_14B_shift-3.0_cfg-5.0_lmdb
5
+ """
6
+
7
+ from tqdm import tqdm
8
+ import numpy as np
9
+ import argparse
10
+ import torch
11
+ import lmdb
12
+ import glob
13
+ import os
14
+
15
+ from utils.lmdb import store_arrays_to_lmdb, process_data_dict
16
+
17
+
18
+ def main():
19
+ """
20
+ Aggregate all ode pairs inside a folder into a lmdb dataset.
21
+ Each pt file should contain a (key, value) pair representing a
22
+ video's ODE trajectories.
23
+ """
24
+ parser = argparse.ArgumentParser()
25
+ parser.add_argument("--data_path", type=str, required=True, help="path to ode pairs")
26
+ parser.add_argument("--lmdb_path", type=str, required=True, help="path to lmdb")
27
+ parser.add_argument("--num_shards", type=int, default=16, help="num_shards")
28
+
29
+ args = parser.parse_args()
30
+
31
+ all_dirs = sorted(os.listdir(args.data_path))
32
+
33
+ # figure out the maximum map size needed
34
+ map_size = int(1e12) # adapt to your need, set to 1TB by default
35
+ os.makedirs(args.lmdb_path, exist_ok=True)
36
+ # 1) Open one LMDB env per shard
37
+ envs = []
38
+ num_shards = args.num_shards
39
+ for shard_id in range(num_shards):
40
+ print("shard_id ", shard_id)
41
+ path = os.path.join(args.lmdb_path, f"shard_{shard_id}")
42
+ env = lmdb.open(
43
+ path,
44
+ map_size=map_size,
45
+ subdir=True, # set to True if you want a directory per env
46
+ readonly=False,
47
+ metasync=True,
48
+ sync=True,
49
+ lock=True,
50
+ readahead=False,
51
+ meminit=False,
52
+ )
53
+ envs.append(env)
54
+
55
+ counters = [0] * num_shards
56
+ seen_prompts = set() # for deduplication
57
+ total_samples = 0
58
+ all_files = []
59
+
60
+ for part_dir in all_dirs:
61
+ all_files += sorted(glob.glob(os.path.join(args.data_path, part_dir, "*.pt")))
62
+
63
+ # 2) Prepare a write transaction for each shard
64
+ for idx, file in tqdm(enumerate(all_files)):
65
+ try:
66
+ data_dict = torch.load(file)
67
+ data_dict = process_data_dict(data_dict, seen_prompts)
68
+ except Exception as e:
69
+ print(f"Error processing {file}: {e}")
70
+ continue
71
+
72
+ if data_dict["latents"].shape != (1, 21, 16, 60, 104):
73
+ continue
74
+
75
+ shard_id = idx % num_shards
76
+ # write to lmdb file
77
+ store_arrays_to_lmdb(envs[shard_id], data_dict, start_index=counters[shard_id])
78
+ counters[shard_id] += len(data_dict["prompts"])
79
+ data_shape = data_dict["latents"].shape
80
+
81
+ total_samples += len(all_files)
82
+
83
+ print(len(seen_prompts))
84
+
85
+ # save each entry's shape to lmdb
86
+ for shard_id, env in enumerate(envs):
87
+ with env.begin(write=True) as txn:
88
+ for key, val in data_dict.items():
89
+ assert len(data_shape) == 5
90
+ array_shape = np.array(data_shape) # val.shape)
91
+ array_shape[0] = counters[shard_id]
92
+ shape_key = f"{key}_shape".encode()
93
+ print(shape_key, array_shape)
94
+ shape_str = " ".join(map(str, array_shape))
95
+ txn.put(shape_key, shape_str.encode())
96
+
97
+ print(
98
+ f"Finished writing {total_samples} examples into {num_shards} shards under {args.lmdb_path}"
99
+ )
100
+
101
+
102
+ if __name__ == "__main__":
103
+ main()
scripts/create_lmdb_iterative.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from tqdm import tqdm
2
+ import numpy as np
3
+ import argparse
4
+ import torch
5
+ import lmdb
6
+ import glob
7
+ import os
8
+
9
+ from utils.lmdb import store_arrays_to_lmdb, process_data_dict
10
+
11
+
12
+ def main():
13
+ """
14
+ Aggregate all ode pairs inside a folder into a lmdb dataset.
15
+ Each pt file should contain a (key, value) pair representing a
16
+ video's ODE trajectories.
17
+ """
18
+ parser = argparse.ArgumentParser()
19
+ parser.add_argument("--data_path", type=str, required=True, help="path to ode pairs")
20
+ parser.add_argument("--lmdb_path", type=str, required=True, help="path to lmdb")
21
+
22
+ args = parser.parse_args()
23
+
24
+ all_files = sorted(glob.glob(os.path.join(args.data_path, "*.pt")))
25
+
26
+ # figure out the maximum map size needed
27
+ total_array_size = 5000000000000 # adapt to your need, set to 5TB by default
28
+
29
+ env = lmdb.open(args.lmdb_path, map_size=total_array_size * 2)
30
+
31
+ counter = 0
32
+
33
+ seen_prompts = set() # for deduplication
34
+
35
+ for index, file in tqdm(enumerate(all_files)):
36
+ # read from disk
37
+ data_dict = torch.load(file)
38
+
39
+ data_dict = process_data_dict(data_dict, seen_prompts)
40
+
41
+ # write to lmdb file
42
+ store_arrays_to_lmdb(env, data_dict, start_index=counter)
43
+ counter += len(data_dict["prompts"])
44
+
45
+ # save each entry's shape to lmdb
46
+ with env.begin(write=True) as txn:
47
+ for key, val in data_dict.items():
48
+ print(key, val)
49
+ array_shape = np.array(val.shape)
50
+ array_shape[0] = counter
51
+
52
+ shape_key = f"{key}_shape".encode()
53
+ shape_str = " ".join(map(str, array_shape))
54
+ txn.put(shape_key, shape_str.encode())
55
+
56
+
57
+ if __name__ == "__main__":
58
+ main()
scripts/generate_ode_pairs.py ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from utils.distributed import launch_distributed_job
2
+ from utils.scheduler import FlowMatchScheduler
3
+ from utils.wan_wrapper import WanDiffusionWrapper, WanTextEncoder
4
+ from utils.dataset import TextDataset
5
+ import torch.distributed as dist
6
+ from tqdm import tqdm
7
+ import argparse
8
+ import torch
9
+ import math
10
+ import os
11
+
12
+
13
+ def init_model(
14
+ device,
15
+ ):
16
+ model = WanDiffusionWrapper().to(device).to(torch.float32)
17
+ encoder = WanTextEncoder().to(device).to(torch.float32)
18
+ model.model.requires_grad_(False)
19
+
20
+ scheduler = FlowMatchScheduler(shift=8.0, sigma_min=0.0, extra_one_step=True)
21
+ scheduler.set_timesteps(num_inference_steps=48, denoising_strength=1.0)
22
+ scheduler.sigmas = scheduler.sigmas.to(device)
23
+
24
+ sample_neg_prompt = "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走"
25
+
26
+ unconditional_dict = encoder(text_prompts=[sample_neg_prompt])
27
+
28
+ return model, encoder, scheduler, unconditional_dict
29
+
30
+
31
+ def main():
32
+ parser = argparse.ArgumentParser()
33
+ parser.add_argument("--local_rank", type=int, default=-1)
34
+ parser.add_argument("--output_folder", type=str)
35
+ parser.add_argument("--caption_path", type=str)
36
+ parser.add_argument("--guidance_scale", type=float, default=6.0)
37
+
38
+ args = parser.parse_args()
39
+
40
+ # launch_distributed_job()
41
+ launch_distributed_job()
42
+
43
+ device = torch.cuda.current_device()
44
+
45
+ torch.set_grad_enabled(False)
46
+ torch.backends.cuda.matmul.allow_tf32 = True
47
+ torch.backends.cudnn.allow_tf32 = True
48
+
49
+ model, encoder, scheduler, unconditional_dict = init_model(device=device)
50
+
51
+ dataset = TextDataset(args.caption_path)
52
+
53
+ # if global_rank == 0:
54
+ os.makedirs(args.output_folder, exist_ok=True)
55
+
56
+ for index in tqdm(
57
+ range(int(math.ceil(len(dataset) / dist.get_world_size()))),
58
+ disable=dist.get_rank() != 0,
59
+ ):
60
+ prompt_index = index * dist.get_world_size() + dist.get_rank()
61
+ if prompt_index >= len(dataset):
62
+ continue
63
+ prompt = dataset[prompt_index]
64
+
65
+ conditional_dict = encoder(text_prompts=prompt)
66
+
67
+ latents = torch.randn([1, 21, 16, 60, 104], dtype=torch.float32, device=device)
68
+
69
+ noisy_input = []
70
+
71
+ for progress_id, t in enumerate(tqdm(scheduler.timesteps)):
72
+ timestep = t * torch.ones([1, 21], device=device, dtype=torch.float32)
73
+
74
+ noisy_input.append(latents)
75
+
76
+ _, x0_pred_cond = model(latents, conditional_dict, timestep)
77
+
78
+ _, x0_pred_uncond = model(latents, unconditional_dict, timestep)
79
+
80
+ x0_pred = x0_pred_uncond + args.guidance_scale * (x0_pred_cond - x0_pred_uncond)
81
+
82
+ flow_pred = model._convert_x0_to_flow_pred(
83
+ scheduler=scheduler,
84
+ x0_pred=x0_pred.flatten(0, 1),
85
+ xt=latents.flatten(0, 1),
86
+ timestep=timestep.flatten(0, 1),
87
+ ).unflatten(0, x0_pred.shape[:2])
88
+
89
+ latents = scheduler.step(
90
+ flow_pred.flatten(0, 1),
91
+ scheduler.timesteps[progress_id]
92
+ * torch.ones([1, 21], device=device, dtype=torch.long).flatten(0, 1),
93
+ latents.flatten(0, 1),
94
+ ).unflatten(dim=0, sizes=flow_pred.shape[:2])
95
+
96
+ noisy_input.append(latents)
97
+
98
+ noisy_inputs = torch.stack(noisy_input, dim=1)
99
+
100
+ noisy_inputs = noisy_inputs[:, [0, 12, 24, 36, -1]]
101
+
102
+ stored_data = noisy_inputs
103
+
104
+ torch.save(
105
+ {prompt: stored_data.cpu().detach()},
106
+ os.path.join(args.output_folder, f"{prompt_index:05d}.pt"),
107
+ )
108
+
109
+ dist.barrier()
110
+
111
+
112
+ if __name__ == "__main__":
113
+ main()
setup.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ from setuptools import setup, find_packages
2
+
3
+ setup(
4
+ name="self_forcing",
5
+ version="0.0.1",
6
+ packages=find_packages(),
7
+ )
stream_drag_inference_wrapper.py ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import numpy as np
3
+ from omegaconf import DictConfig
4
+ from optimize_utils import MultiTrajectory
5
+ from stream_inference_wrapper import StreamInferenceWrapper
6
+
7
+
8
+ def _extract_block_trajectories(
9
+ multi_traj: MultiTrajectory,
10
+ ) -> tuple[
11
+ list[list[dict[str, bool | list[torch.Tensor]]]],
12
+ list[np.ndarray],
13
+ np.ndarray | None,
14
+ ]:
15
+ """Extract block_trajectories from a MultiTrajectory in the format expected by begin_optimize.
16
+
17
+ Returns:
18
+ block_trajectories: block_num x N x trajectory dict
19
+ Each trajectory dict has keys 'is_rotation', 'deltas', 'start_point',
20
+ and optionally 'rotation_center'.
21
+ masks: list of N masks corresponding to each trajectory
22
+ movable_mask: the movable area mask for the whole image
23
+ """
24
+ if multi_traj.trajectories is None or len(multi_traj.trajectories) == 0:
25
+ return [], [], None
26
+
27
+ movable_mask = multi_traj.movable_mask
28
+
29
+ # Collect per-trajectory masks
30
+ masks = [traj.mask for traj in multi_traj.trajectories]
31
+
32
+ # Find the maximum number of blocks across all trajectories
33
+ max_blocks = (
34
+ max(
35
+ len(traj.block_trajectories)
36
+ for traj in multi_traj.trajectories
37
+ if traj.block_trajectories
38
+ )
39
+ if any(traj.block_trajectories for traj in multi_traj.trajectories)
40
+ else 0
41
+ )
42
+
43
+ if max_blocks == 0:
44
+ return [], masks, movable_mask
45
+
46
+ block_trajectories = []
47
+ for block_idx in range(max_blocks):
48
+ block = []
49
+ for traj in multi_traj.trajectories:
50
+ if traj.block_trajectories and block_idx < len(traj.block_trajectories):
51
+ block.append(traj.block_trajectories[block_idx])
52
+ else:
53
+ # Provide an empty placeholder
54
+ block.append(
55
+ {
56
+ "is_rotation": False,
57
+ "deltas": [],
58
+ "start_point": (0, 0),
59
+ }
60
+ )
61
+ block_trajectories.append(block)
62
+
63
+ # Assert: the N of each block in block_trajectories should equal the length of masks
64
+ for block_idx, block in enumerate(block_trajectories):
65
+ assert len(block) == len(masks), (
66
+ f"Block {block_idx} has {len(block)} trajectories, " f"but there are {len(masks)} masks"
67
+ )
68
+
69
+ assert ((len(block_trajectories) == 0) and (movable_mask is None)) or (
70
+ (len(block_trajectories) > 0) and (movable_mask is not None)
71
+ ), "block_trajectories and movable_mask must both be present or both be absent"
72
+
73
+ return block_trajectories, masks, movable_mask
74
+
75
+
76
+ class StreamDragInferenceWrapper(StreamInferenceWrapper):
77
+ def __init__(
78
+ self,
79
+ stream_model_config: DictConfig,
80
+ checkpoint_path: str,
81
+ total_generate_block_number: int,
82
+ use_ema: bool = True,
83
+ seed: int = 0,
84
+ ):
85
+ super().__init__(
86
+ stream_model_config=stream_model_config,
87
+ checkpoint_path=checkpoint_path,
88
+ total_generate_block_number=total_generate_block_number,
89
+ use_ema=use_ema,
90
+ seed=seed,
91
+ )
92
+ self.previous_record_feature_list = None
93
+
94
+ def inference(
95
+ self,
96
+ start_block_index: int,
97
+ end_block_index: int,
98
+ prompt: str,
99
+ # below are for drag optimization
100
+ multiple_trajectory: MultiTrajectory = None,
101
+ ):
102
+ assert start_block_index >= 0
103
+ assert end_block_index > start_block_index
104
+ print(f"""
105
+ {self.__class__.__name__}.inference():
106
+ {start_block_index = } | {end_block_index = }
107
+ """)
108
+ sampled_noise = self.get_sampled_noise(start_block_index, end_block_index)
109
+ prompts = [prompt]
110
+
111
+ # Extract block_trajectories, masks, and movable_mask from multiple_trajectory
112
+ drag_optimize_target_latent_index = -1
113
+ if multiple_trajectory is not None:
114
+ block_trajectories, masks, movable_mask = _extract_block_trajectories(
115
+ multiple_trajectory
116
+ )
117
+ assert multiple_trajectory.drag_or_animation_select in [
118
+ "Drag",
119
+ "Animation",
120
+ ]
121
+ if multiple_trajectory.drag_or_animation_select == "Drag":
122
+ drag_optimize_target_latent_index = 2
123
+ else:
124
+ block_trajectories, masks, movable_mask = [], [], None
125
+
126
+ if len(block_trajectories) > 0:
127
+ is_drag_optimize = True
128
+ else:
129
+ is_drag_optimize = False
130
+
131
+ initial_latents = self.get_initial_latents(
132
+ start_block_index,
133
+ )
134
+ if initial_latents is not None:
135
+ print(f"{initial_latents.shape = }")
136
+
137
+ print(f"{block_trajectories = }")
138
+ print(f"{len(masks) = }")
139
+ latents_result = self.pipeline.inference(
140
+ noise=sampled_noise,
141
+ text_prompts=prompts,
142
+ return_latents=True,
143
+ initial_latent=initial_latents,
144
+ do_not_decode_video=True,
145
+ do_not_recompute_initial_latents=True,
146
+ # below are for drag optimization
147
+ model_config=self.stream_model_config,
148
+ previous_record_feature_list=self.previous_record_feature_list,
149
+ is_drag_optimize=is_drag_optimize,
150
+ block_trajectories=block_trajectories,
151
+ masks=masks,
152
+ movable_mask=movable_mask,
153
+ drag_optimize_target_latent_index=drag_optimize_target_latent_index,
154
+ )
155
+ if self.stream_model_config.drag_optim_config.record_feature_block_indexes:
156
+ latents, record_attention_values_list = latents_result
157
+ else:
158
+ latents = latents_result
159
+ record_attention_values_list = None
160
+ if self.recorded_latents is None:
161
+ self.recorded_latents = latents
162
+ else:
163
+ self.recorded_latents = torch.concat(
164
+ [
165
+ self.recorded_latents[:, :0],
166
+ latents,
167
+ ],
168
+ dim=1,
169
+ )
170
+
171
+ if record_attention_values_list is not None:
172
+
173
+ def dict_first_value(d: dict):
174
+ return next(iter(d.values()))
175
+
176
+ print(f"{record_attention_values_list.keys() = }") # denoising timesteps
177
+ print(
178
+ f"{dict_first_value(record_attention_values_list).keys() = }"
179
+ ) # attention block layers
180
+ print(
181
+ f"{dict_first_value(dict_first_value(record_attention_values_list)).keys() = }"
182
+ ) # attention types name
183
+ print(
184
+ f"{dict_first_value(dict_first_value(dict_first_value(record_attention_values_list))).shape = }"
185
+ ) # [1, 3, 30, 52, 1536]
186
+ else:
187
+ print(f"{record_attention_values_list = }")
188
+ self.previous_record_feature_list = record_attention_values_list
189
+
190
+ self.decode_and_update_video(start_block_index, end_block_index)
191
+
192
+ return (
193
+ self.video,
194
+ self.video[self.block_to_video_index(start_block_index) :],
195
+ )
196
+
197
+ def reset(
198
+ self,
199
+ ):
200
+ super().reset()
201
+ self.previous_record_feature_list = None
stream_inference.py ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ os.environ["CUDA_VISIBLE_DEVICES"] = "1"
4
+
5
+ from tqdm import tqdm
6
+ from torchvision.io import write_video
7
+ from torch.utils.data import DataLoader, SequentialSampler
8
+
9
+ from stream_inference_wrapper import StreamInferenceWrapper
10
+
11
+ # from stream_drag_inference_wrapper import StreamDragInferenceWrapper
12
+ from utils.dataset import TextDataset
13
+ from utils.misc import set_seed
14
+ from hydra import initialize, compose
15
+ from hydra.core.global_hydra import GlobalHydra
16
+
17
+ from demo_utils.memory import gpu, get_cuda_free_memory_gb
18
+
19
+
20
+ def main():
21
+
22
+ output_block_number = 27
23
+
24
+ config_dir = "configs"
25
+ stream_config_name = "self_forcing_dmd_vsink_stream"
26
+ # stream_config_name = "self_forcing_dmd_vsink_stream_drag"
27
+
28
+ data_path = "prompts/MovieGenVideoBench_extended.txt"
29
+
30
+ seed = 42
31
+ set_seed(seed)
32
+
33
+ output_folder = "outputs-stream"
34
+ output_folder = f"{output_folder}/blk{output_block_number}-{stream_config_name}-seed{seed}"
35
+
36
+ print(f"Free VRAM {get_cuda_free_memory_gb(gpu)} GB")
37
+ # low_memory = get_cuda_free_memory_gb(gpu) < 40
38
+
39
+ # Create dataset
40
+ dataset = TextDataset(prompt_path=data_path)
41
+ num_prompts = len(dataset)
42
+ print(f"Number of prompts: {num_prompts}")
43
+
44
+ sampler = SequentialSampler(dataset)
45
+ dataloader = DataLoader(dataset, batch_size=1, sampler=sampler, num_workers=0, drop_last=False)
46
+
47
+ os.makedirs(output_folder, exist_ok=True)
48
+
49
+ if GlobalHydra.instance().is_initialized():
50
+ GlobalHydra.instance().clear()
51
+
52
+ with initialize(version_base=None, config_path=config_dir):
53
+ stream_config = compose(config_name=stream_config_name)
54
+ print(f"{stream_config = }")
55
+
56
+ stream_inference = StreamInferenceWrapper(
57
+ stream_model_config=stream_config,
58
+ checkpoint_path="./checkpoints/self_forcing_dmd.pt",
59
+ total_generate_block_number=output_block_number,
60
+ use_ema=True,
61
+ seed=seed,
62
+ )
63
+
64
+ for i, batch_data in tqdm(enumerate(dataloader)):
65
+ idx = batch_data["idx"].item()
66
+ print(f"{idx = }")
67
+
68
+ # For DataLoader batch_size=1, the batch_data is already a single item, but in a batch container
69
+ # Unpack the batch data for convenience
70
+ if isinstance(batch_data, dict):
71
+ batch = batch_data
72
+ elif isinstance(batch_data, list):
73
+ batch = batch_data[0] # First (and only) item in the batch
74
+
75
+ # For text-to-video, batch is just the text prompt
76
+ prompt = batch["prompts"][0]
77
+ print(f"{prompt = }")
78
+ extended_prompt = batch["extended_prompts"][0] if "extended_prompts" in batch else None
79
+ print(f"{extended_prompt = }")
80
+
81
+ set_seed(seed)
82
+ stream_inference.reset()
83
+
84
+ current_block_index = 0
85
+ block_step = 3
86
+ while current_block_index < output_block_number:
87
+ end_block_index = current_block_index + block_step
88
+ all_video, current_video = stream_inference.inference(
89
+ start_block_index=current_block_index,
90
+ end_block_index=end_block_index,
91
+ prompt=prompt,
92
+ )
93
+
94
+ # Save the video if the current prompt is not a dummy prompt
95
+ if idx < num_prompts:
96
+ current_video_output_path = os.path.join(
97
+ output_folder,
98
+ f"{idx:04d}-{prompt[:50].replace(' ', '_')}-{current_block_index:02d}-{end_block_index:02d}.mp4",
99
+ )
100
+ write_video(current_video_output_path, current_video, fps=16)
101
+ all_video_output_path = os.path.join(
102
+ output_folder,
103
+ f"{idx:04d}-{prompt[:50].replace(' ', '_')}-{0:02d}-{end_block_index:02d}.mp4",
104
+ )
105
+ write_video(all_video_output_path, all_video, fps=16)
106
+
107
+ current_block_index = end_block_index
108
+
109
+
110
+ if __name__ == "__main__":
111
+ main()