Spaces:
Running on Zero
Running on Zero
Upload folder using huggingface_hub
Browse files- .gitattributes +4 -0
- LICENSE +201 -0
- README.md +50 -6
- app.py +515 -0
- examples/girl.png +3 -0
- examples/news.wav +3 -0
- examples/podcast_sichuan.wav +3 -0
- examples/portrait.jpg +3 -0
- flash_head/audio_analysis/torch_utils.py +20 -0
- flash_head/audio_analysis/wav2vec2.py +125 -0
- flash_head/src/modules/flash_head_model.py +589 -0
- flash_head/src/pipeline/flash_head_pipeline.py +379 -0
- flash_head/utils/cpu_face_handler.py +55 -0
- flash_head/utils/facecrop.py +110 -0
- flash_head/utils/utils.py +222 -0
- flash_head/wan/modules/__init__.py +7 -0
- flash_head/wan/modules/tae.py +304 -0
- flash_head/wan/modules/vae.py +1598 -0
- leaptalk_inference.py +1156 -0
- requirements.txt +18 -0
- vibt/scheduler.py +59 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,7 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
examples/girl.png filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
examples/news.wav filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
examples/podcast_sichuan.wav filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
examples/portrait.jpg filter=lfs diff=lfs merge=lfs -text
|
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.md
CHANGED
|
@@ -1,13 +1,57 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.22.0
|
| 8 |
-
python_version: '3.12'
|
| 9 |
app_file: app.py
|
|
|
|
|
|
|
|
|
|
| 10 |
pinned: false
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: LeapTalk
|
| 3 |
+
emoji: 🗣️
|
| 4 |
+
colorFrom: pink
|
| 5 |
+
colorTo: yellow
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.22.0
|
|
|
|
| 8 |
app_file: app.py
|
| 9 |
+
short_description: One-step audio-driven talking head video generation
|
| 10 |
+
python_version: "3.12"
|
| 11 |
+
startup_duration_timeout: 45m
|
| 12 |
pinned: false
|
| 13 |
+
license: apache-2.0
|
| 14 |
+
models:
|
| 15 |
+
- z-rx/leaptalk
|
| 16 |
+
- Soul-AILab/SoulX-FlashHead-1_3B
|
| 17 |
+
- facebook/wav2vec2-base-960h
|
| 18 |
---
|
| 19 |
|
| 20 |
+
# LeapTalk — real-time talking-head generation
|
| 21 |
+
|
| 22 |
+
Demo for [**z-rx/leaptalk**](https://huggingface.co/z-rx/leaptalk): give it a portrait photo
|
| 23 |
+
and a speech clip, and it generates a lip-synced talking-head video.
|
| 24 |
+
|
| 25 |
+
LeapTalk reformulates talking-head generation as a *data-to-data* Brownian-bridge transport
|
| 26 |
+
(**Bridge Forcing**), which lets the distilled model synthesize each video chunk with a single
|
| 27 |
+
network evaluation (1 NFE) while keeping identity and style stable over long autoregressive
|
| 28 |
+
rollouts.
|
| 29 |
+
|
| 30 |
+
- Paper: [LeapTalk: Breaking the Latency-Quality Trade-off in Talking Head Generation](https://huggingface.co/papers/2608.00079)
|
| 31 |
+
- Project page: https://zhangrongxiang.github.io/leaptalk-page/
|
| 32 |
+
- Code: https://github.com/zhangrongxiang/LeapTalk
|
| 33 |
+
|
| 34 |
+
## Pipeline
|
| 35 |
+
|
| 36 |
+
This Space reproduces the authors' reference streaming pipeline
|
| 37 |
+
(`inference.py --model_type pro --lite --num_inference_steps 1 --audio_encode_mode stream`):
|
| 38 |
+
|
| 39 |
+
| component | weights |
|
| 40 |
+
|---|---|
|
| 41 |
+
| base video DiT | `Soul-AILab/SoulX-FlashHead-1_3B` (`Model_Pro`) |
|
| 42 |
+
| LeapTalk LoRA (merged) + audio projector | `z-rx/leaptalk` |
|
| 43 |
+
| audio encoder | `facebook/wav2vec2-base-960h` |
|
| 44 |
+
| Lite TAE video autoencoder | `taew2_1.pth` from `z-rx/leaptalk` |
|
| 45 |
+
| sampler | ViBT Brownian-bridge scheduler (1 step / chunk) |
|
| 46 |
+
|
| 47 |
+
Video is produced chunk-by-chunk (33 frames per chunk, 5 overlapping motion frames) at
|
| 48 |
+
512x512, 25 fps, with the SoulX-style VAE round-trip history update and reference-colour
|
| 49 |
+
correction.
|
| 50 |
+
|
| 51 |
+
## Notes
|
| 52 |
+
|
| 53 |
+
- Runs on ZeroGPU; the multi-GPU USP (xFuser sequence-parallel) path from the reference
|
| 54 |
+
implementation is disabled since the Space is single-GPU.
|
| 55 |
+
- Example assets come from the [LeapTalk](https://github.com/zhangrongxiang/LeapTalk) and
|
| 56 |
+
[SoulX-FlashHead](https://github.com/Soul-AILab/SoulX-FlashHead) repositories (Apache-2.0);
|
| 57 |
+
the audio clips were trimmed to 9 seconds.
|
app.py
ADDED
|
@@ -0,0 +1,515 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""LeapTalk — real-time audio-driven talking-head generation on ZeroGPU.
|
| 2 |
+
|
| 3 |
+
Faithful port of the official reference implementation
|
| 4 |
+
(https://github.com/zhangrongxiang/LeapTalk, `inference.py` streaming path):
|
| 5 |
+
|
| 6 |
+
SoulX-FlashHead-1_3B (Model_Pro) + LeapTalk LoRA (merged)
|
| 7 |
+
+ LeapTalk audio projector + wav2vec2-base-960h audio encoder
|
| 8 |
+
+ Lite TAE (taew2_1) VAE + ViBT Brownian-bridge scheduler
|
| 9 |
+
|
| 10 |
+
Everything (chunking, audio windowing, bridge sampling, motion-frame
|
| 11 |
+
round-trip, colour correction) mirrors the authors' `--lite` / `--model_type pro`
|
| 12 |
+
/ `--audio_encode_mode stream` defaults from `inf.sh`.
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
import os
|
| 16 |
+
|
| 17 |
+
os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
|
| 18 |
+
|
| 19 |
+
import spaces # noqa: E402 — must precede any torch / CUDA-touching import
|
| 20 |
+
|
| 21 |
+
import math # noqa: E402
|
| 22 |
+
import shutil # noqa: E402
|
| 23 |
+
import subprocess # noqa: E402
|
| 24 |
+
import sys # noqa: E402
|
| 25 |
+
import tempfile # noqa: E402
|
| 26 |
+
import time # noqa: E402
|
| 27 |
+
import wave # noqa: E402
|
| 28 |
+
from collections import deque # noqa: E402
|
| 29 |
+
|
| 30 |
+
import gradio as gr # noqa: E402
|
| 31 |
+
import imageio # noqa: E402
|
| 32 |
+
import librosa # noqa: E402
|
| 33 |
+
import numpy as np # noqa: E402
|
| 34 |
+
import torch # noqa: E402
|
| 35 |
+
from huggingface_hub import hf_hub_download, snapshot_download # noqa: E402
|
| 36 |
+
from loguru import logger # noqa: E402
|
| 37 |
+
from peft import PeftModel # noqa: E402
|
| 38 |
+
|
| 39 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 40 |
+
|
| 41 |
+
import flash_head.src.pipeline.flash_head_pipeline as fh_pipe_mod # noqa: E402
|
| 42 |
+
|
| 43 |
+
# torch.compile is disabled: the released LoRA was not saved from a compiled base
|
| 44 |
+
# (`--compile off` in the reference `inf.sh`), and TAEHV uses Python-level loops.
|
| 45 |
+
fh_pipe_mod.COMPILE_MODEL = False
|
| 46 |
+
fh_pipe_mod.COMPILE_VAE = False
|
| 47 |
+
|
| 48 |
+
from flash_head.src.pipeline.flash_head_pipeline import FlashHeadPipeline # noqa: E402
|
| 49 |
+
from leaptalk_inference import ( # noqa: E402
|
| 50 |
+
StreamParams,
|
| 51 |
+
_audio_context_from_embeddings_range,
|
| 52 |
+
_bridge_sample_one_chunk,
|
| 53 |
+
_build_infer_timesteps,
|
| 54 |
+
_decode_to_cthw,
|
| 55 |
+
_encode_motion_prefix_from_decoded,
|
| 56 |
+
_get_inner_flashhead_model,
|
| 57 |
+
_maybe_apply_color_correction,
|
| 58 |
+
)
|
| 59 |
+
from vibt.scheduler import ViBTScheduler # noqa: E402
|
| 60 |
+
|
| 61 |
+
# --------------------------------------------------------------------------------------
|
| 62 |
+
# Fixed inference configuration (reference defaults)
|
| 63 |
+
# --------------------------------------------------------------------------------------
|
| 64 |
+
DEVICE = "cuda"
|
| 65 |
+
DTYPE = torch.bfloat16
|
| 66 |
+
HEIGHT = WIDTH = 512
|
| 67 |
+
FPS = 25
|
| 68 |
+
SAMPLE_RATE = 16000
|
| 69 |
+
FRAME_NUM = 33
|
| 70 |
+
MOTION_FRAMES_LATENT_NUM = 2
|
| 71 |
+
CACHED_AUDIO_DURATION = 8
|
| 72 |
+
SHIFT_GAMMA = 5.0
|
| 73 |
+
NOISE_SCALE = 1.0
|
| 74 |
+
COLOR_CORRECTION_STRENGTH = 1.0
|
| 75 |
+
MAX_SECONDS_CAP = 20
|
| 76 |
+
|
| 77 |
+
# --------------------------------------------------------------------------------------
|
| 78 |
+
# Weights
|
| 79 |
+
# --------------------------------------------------------------------------------------
|
| 80 |
+
logger.info("Downloading weights…")
|
| 81 |
+
CKPT_DIR = snapshot_download(
|
| 82 |
+
"Soul-AILab/SoulX-FlashHead-1_3B", allow_patterns=["Model_Pro/*"]
|
| 83 |
+
)
|
| 84 |
+
WAV2VEC_DIR = snapshot_download(
|
| 85 |
+
"facebook/wav2vec2-base-960h",
|
| 86 |
+
allow_patterns=["*.json", "*.txt", "*.safetensors", "pytorch_model.bin"],
|
| 87 |
+
)
|
| 88 |
+
LEAPTALK_DIR = snapshot_download("z-rx/leaptalk")
|
| 89 |
+
LORA_DIR = os.path.join(LEAPTALK_DIR, "lora")
|
| 90 |
+
TAE_PATH = os.path.join(LEAPTALK_DIR, "taew2_1.pth")
|
| 91 |
+
AUDIO_PROJ_PATH = os.path.join(LEAPTALK_DIR, "audio_proj_step_10400.pt")
|
| 92 |
+
|
| 93 |
+
# --------------------------------------------------------------------------------------
|
| 94 |
+
# Pipeline assembly (module scope, eagerly moved to CUDA)
|
| 95 |
+
# --------------------------------------------------------------------------------------
|
| 96 |
+
logger.info("Building FlashHead pipeline…")
|
| 97 |
+
# Built on CPU first so the LoRA merge / projector load happen on real tensors,
|
| 98 |
+
# then the whole stack is moved to CUDA eagerly (ZeroGPU packs it from there).
|
| 99 |
+
pipeline = FlashHeadPipeline(
|
| 100 |
+
checkpoint_dir=CKPT_DIR,
|
| 101 |
+
model_type="pro",
|
| 102 |
+
wav2vec_dir=WAV2VEC_DIR,
|
| 103 |
+
device="cpu",
|
| 104 |
+
param_dtype=DTYPE,
|
| 105 |
+
use_usp=False,
|
| 106 |
+
use_tae=True,
|
| 107 |
+
tae_path=TAE_PATH,
|
| 108 |
+
tae_model_type="wan21",
|
| 109 |
+
)
|
| 110 |
+
|
| 111 |
+
logger.info("Merging LeapTalk LoRA…")
|
| 112 |
+
pipeline.model = PeftModel.from_pretrained(pipeline.model, LORA_DIR, is_trainable=False)
|
| 113 |
+
pipeline.model = pipeline.model.merge_and_unload()
|
| 114 |
+
pipeline.model.eval().requires_grad_(False)
|
| 115 |
+
|
| 116 |
+
logger.info("Loading LeapTalk audio projector…")
|
| 117 |
+
_audio_proj_state = torch.load(AUDIO_PROJ_PATH, map_location="cpu", weights_only=True)
|
| 118 |
+
_get_inner_flashhead_model(pipeline.model).audio_proj.load_state_dict(
|
| 119 |
+
_audio_proj_state, strict=True
|
| 120 |
+
)
|
| 121 |
+
del _audio_proj_state
|
| 122 |
+
|
| 123 |
+
pipeline.device = DEVICE
|
| 124 |
+
pipeline.model.to(DEVICE)
|
| 125 |
+
pipeline.vae.device = DEVICE
|
| 126 |
+
pipeline.vae.model.to(DEVICE)
|
| 127 |
+
pipeline.audio_encoder.to(DEVICE)
|
| 128 |
+
pipeline.audio_encoder.eval().requires_grad_(False)
|
| 129 |
+
|
| 130 |
+
STREAM = StreamParams(
|
| 131 |
+
frame_num=FRAME_NUM,
|
| 132 |
+
motion_frames_latent_num=MOTION_FRAMES_LATENT_NUM,
|
| 133 |
+
tgt_fps=FPS,
|
| 134 |
+
sample_rate=SAMPLE_RATE,
|
| 135 |
+
cached_audio_duration=CACHED_AUDIO_DURATION,
|
| 136 |
+
).init_with_stride(int(pipeline.config.vae_stride[0]))
|
| 137 |
+
SLICE_SAMPLES = STREAM.slice_len * SAMPLE_RATE // FPS
|
| 138 |
+
logger.info(
|
| 139 |
+
f"Ready. frame_num={STREAM.frame_num} motion_frames={STREAM.motion_frames_num} "
|
| 140 |
+
f"slice_len={STREAM.slice_len} ({SLICE_SAMPLES} samples/chunk)"
|
| 141 |
+
)
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
# --------------------------------------------------------------------------------------
|
| 145 |
+
# Video helpers
|
| 146 |
+
# --------------------------------------------------------------------------------------
|
| 147 |
+
def _ffmpeg_exe() -> str:
|
| 148 |
+
exe = shutil.which("ffmpeg")
|
| 149 |
+
if exe:
|
| 150 |
+
return exe
|
| 151 |
+
import imageio_ffmpeg
|
| 152 |
+
|
| 153 |
+
return imageio_ffmpeg.get_ffmpeg_exe()
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def _write_wav(path: str, audio: np.ndarray, sample_rate: int = SAMPLE_RATE) -> str:
|
| 157 |
+
pcm = (np.clip(audio, -1.0, 1.0) * 32767.0).astype(np.int16)
|
| 158 |
+
with wave.open(path, "wb") as wf:
|
| 159 |
+
wf.setnchannels(1)
|
| 160 |
+
wf.setsampwidth(2)
|
| 161 |
+
wf.setframerate(sample_rate)
|
| 162 |
+
wf.writeframes(pcm.tobytes())
|
| 163 |
+
return path
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
def _mux(video_path: str, audio_path: str, out_path: str) -> str:
|
| 167 |
+
cmd = [
|
| 168 |
+
_ffmpeg_exe(), "-y",
|
| 169 |
+
"-i", video_path,
|
| 170 |
+
"-i", audio_path,
|
| 171 |
+
"-c:v", "copy",
|
| 172 |
+
"-c:a", "aac", "-b:a", "128k",
|
| 173 |
+
"-shortest",
|
| 174 |
+
"-movflags", "+faststart",
|
| 175 |
+
out_path,
|
| 176 |
+
]
|
| 177 |
+
proc = subprocess.run(cmd, capture_output=True)
|
| 178 |
+
if proc.returncode != 0 or not os.path.exists(out_path):
|
| 179 |
+
logger.warning(f"ffmpeg mux failed: {proc.stderr.decode()[-800:]}")
|
| 180 |
+
shutil.copy(video_path, out_path)
|
| 181 |
+
return out_path
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
def _num_chunks_for(seconds: float) -> int:
|
| 185 |
+
samples = max(int(seconds * SAMPLE_RATE), FRAME_NUM * SAMPLE_RATE // FPS)
|
| 186 |
+
return max(1, math.ceil(samples / SLICE_SAMPLES))
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
def _estimate_duration(
|
| 190 |
+
portrait_image=None,
|
| 191 |
+
speech_audio=None,
|
| 192 |
+
max_seconds: float = 9.0,
|
| 193 |
+
num_inference_steps: int = 1,
|
| 194 |
+
guidance_scale: float = 1.0,
|
| 195 |
+
*args,
|
| 196 |
+
**kwargs,
|
| 197 |
+
) -> int:
|
| 198 |
+
"""ZeroGPU time budget: weight streaming + per-chunk cost."""
|
| 199 |
+
try:
|
| 200 |
+
chunks = _num_chunks_for(float(max_seconds))
|
| 201 |
+
nfe = max(1, int(num_inference_steps)) * (2 if float(guidance_scale) != 1.0 else 1)
|
| 202 |
+
except Exception:
|
| 203 |
+
chunks, nfe = _num_chunks_for(MAX_SECONDS_CAP), 1
|
| 204 |
+
return int(min(180, 25 + chunks * (0.9 + 0.55 * nfe)))
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
# --------------------------------------------------------------------------------------
|
| 208 |
+
# Inference
|
| 209 |
+
# --------------------------------------------------------------------------------------
|
| 210 |
+
@spaces.GPU(duration=_estimate_duration)
|
| 211 |
+
def generate(
|
| 212 |
+
portrait_image: str,
|
| 213 |
+
speech_audio: str,
|
| 214 |
+
max_seconds: float = 9.0,
|
| 215 |
+
num_inference_steps: int = 1,
|
| 216 |
+
guidance_scale: float = 1.0,
|
| 217 |
+
seed: int = 42,
|
| 218 |
+
auto_crop_face: bool = True,
|
| 219 |
+
progress=gr.Progress(track_tqdm=True),
|
| 220 |
+
):
|
| 221 |
+
"""Animate a portrait photo so that it speaks the given audio.
|
| 222 |
+
|
| 223 |
+
Args:
|
| 224 |
+
portrait_image: Path to a portrait photo (a single, roughly front-facing face).
|
| 225 |
+
speech_audio: Path to a speech audio file that drives lip and head motion.
|
| 226 |
+
max_seconds: Maximum number of seconds of the audio to animate.
|
| 227 |
+
num_inference_steps: Bridge-sampler steps per chunk. LeapTalk is distilled for 1.
|
| 228 |
+
guidance_scale: Audio classifier-free guidance. 1.0 disables it (2x faster).
|
| 229 |
+
seed: Random seed for the Brownian-bridge noise.
|
| 230 |
+
auto_crop_face: Detect and crop around the face before generating.
|
| 231 |
+
|
| 232 |
+
Returns:
|
| 233 |
+
A tuple of (path to the generated talking-head mp4, a short speed report).
|
| 234 |
+
"""
|
| 235 |
+
if not portrait_image:
|
| 236 |
+
raise gr.Error("Please provide a portrait image.")
|
| 237 |
+
if not speech_audio:
|
| 238 |
+
raise gr.Error("Please provide a speech audio file.")
|
| 239 |
+
|
| 240 |
+
num_inference_steps = max(1, int(num_inference_steps))
|
| 241 |
+
guidance_scale = float(guidance_scale)
|
| 242 |
+
seed = int(seed)
|
| 243 |
+
max_seconds = float(np.clip(max_seconds, 1.0, MAX_SECONDS_CAP))
|
| 244 |
+
|
| 245 |
+
workdir = tempfile.mkdtemp(prefix="leaptalk_")
|
| 246 |
+
progress(0.02, desc="Preparing reference portrait…")
|
| 247 |
+
|
| 248 |
+
# ---- reference image -> anchor latent X0 (same call as inference.py) --------------
|
| 249 |
+
pipeline.prepare_params(
|
| 250 |
+
cond_image_path_or_dir=portrait_image,
|
| 251 |
+
target_size=(HEIGHT, WIDTH),
|
| 252 |
+
frame_num=STREAM.frame_num,
|
| 253 |
+
motion_frames_num=0,
|
| 254 |
+
sampling_steps=num_inference_steps,
|
| 255 |
+
seed=seed,
|
| 256 |
+
shift=SHIFT_GAMMA,
|
| 257 |
+
color_correction_strength=COLOR_CORRECTION_STRENGTH,
|
| 258 |
+
use_face_crop=bool(auto_crop_face),
|
| 259 |
+
)
|
| 260 |
+
X0 = pipeline.ref_img_latent.to(device=DEVICE, dtype=DTYPE)
|
| 261 |
+
|
| 262 |
+
# ---- scheduler -------------------------------------------------------------------
|
| 263 |
+
scheduler = ViBTScheduler(num_train_timesteps=1000)
|
| 264 |
+
scheduler.timesteps = _build_infer_timesteps(
|
| 265 |
+
step_list=None,
|
| 266 |
+
num_inference_steps=num_inference_steps,
|
| 267 |
+
shift_gamma=SHIFT_GAMMA,
|
| 268 |
+
device=DEVICE,
|
| 269 |
+
num_timesteps=1000,
|
| 270 |
+
)
|
| 271 |
+
scheduler.num_inference_steps = int(scheduler.timesteps.numel())
|
| 272 |
+
scheduler.set_parameters(noise_scale=NOISE_SCALE, shift_gamma=SHIFT_GAMMA, seed=seed)
|
| 273 |
+
|
| 274 |
+
# ---- audio (streaming ring buffer, exactly as inference.py --audio_encode_mode stream)
|
| 275 |
+
progress(0.06, desc="Loading audio…")
|
| 276 |
+
audio_all, _ = librosa.load(speech_audio, sr=SAMPLE_RATE, mono=True)
|
| 277 |
+
audio_all = audio_all[: int(max_seconds * SAMPLE_RATE)]
|
| 278 |
+
if audio_all.size == 0:
|
| 279 |
+
raise gr.Error("The audio file appears to be empty.")
|
| 280 |
+
|
| 281 |
+
frame_window_samples = STREAM.frame_num * SAMPLE_RATE // FPS
|
| 282 |
+
remainder = len(audio_all) % SLICE_SAMPLES
|
| 283 |
+
if remainder > 0:
|
| 284 |
+
audio_all = np.concatenate(
|
| 285 |
+
[audio_all, np.zeros(SLICE_SAMPLES - remainder, dtype=audio_all.dtype)]
|
| 286 |
+
)
|
| 287 |
+
if len(audio_all) < frame_window_samples:
|
| 288 |
+
audio_all = np.concatenate(
|
| 289 |
+
[audio_all, np.zeros(frame_window_samples - len(audio_all), dtype=audio_all.dtype)]
|
| 290 |
+
)
|
| 291 |
+
remainder = len(audio_all) % SLICE_SAMPLES
|
| 292 |
+
if remainder != 0:
|
| 293 |
+
audio_all = np.concatenate(
|
| 294 |
+
[audio_all, np.zeros(SLICE_SAMPLES - remainder, dtype=audio_all.dtype)]
|
| 295 |
+
)
|
| 296 |
+
|
| 297 |
+
slices = audio_all.reshape(-1, SLICE_SAMPLES)
|
| 298 |
+
num_chunks = int(slices.shape[0])
|
| 299 |
+
|
| 300 |
+
cached_len = SAMPLE_RATE * STREAM.cached_audio_duration
|
| 301 |
+
audio_end_idx = STREAM.cached_audio_duration * FPS
|
| 302 |
+
audio_start_idx = audio_end_idx - STREAM.frame_num
|
| 303 |
+
audio_dq = deque([0.0] * cached_len, maxlen=cached_len)
|
| 304 |
+
|
| 305 |
+
latent_motion_frames = X0[:, :1].unsqueeze(0).clone()
|
| 306 |
+
clamp_latent_len = int(latent_motion_frames.shape[2])
|
| 307 |
+
|
| 308 |
+
generated: list[np.ndarray] = []
|
| 309 |
+
gen_seconds = 0.0
|
| 310 |
+
gen_frames = 0
|
| 311 |
+
|
| 312 |
+
for chunk_idx in range(num_chunks):
|
| 313 |
+
progress(
|
| 314 |
+
0.08 + 0.88 * chunk_idx / num_chunks,
|
| 315 |
+
desc=f"Generating chunk {chunk_idx + 1}/{num_chunks}…",
|
| 316 |
+
)
|
| 317 |
+
torch.cuda.synchronize()
|
| 318 |
+
t0 = time.perf_counter()
|
| 319 |
+
|
| 320 |
+
audio_dq.extend(slices[chunk_idx].tolist())
|
| 321 |
+
audio_cache = np.array(audio_dq, dtype=np.float32)
|
| 322 |
+
audio_emb = pipeline.preprocess_audio(audio_cache, sr=SAMPLE_RATE, fps=FPS)
|
| 323 |
+
if audio_emb is None:
|
| 324 |
+
raise gr.Error("Failed to extract audio embeddings.")
|
| 325 |
+
audio_emb = audio_emb.to(device=DEVICE, dtype=DTYPE)
|
| 326 |
+
audio_ctx = _audio_context_from_embeddings_range(
|
| 327 |
+
audio_emb,
|
| 328 |
+
start_idx=audio_start_idx,
|
| 329 |
+
end_idx=audio_end_idx,
|
| 330 |
+
device=DEVICE,
|
| 331 |
+
dtype=DTYPE,
|
| 332 |
+
)
|
| 333 |
+
|
| 334 |
+
x_final = _bridge_sample_one_chunk(
|
| 335 |
+
pipeline,
|
| 336 |
+
scheduler=scheduler,
|
| 337 |
+
ref_latent=X0,
|
| 338 |
+
audio_context=audio_ctx,
|
| 339 |
+
guidance_scale=guidance_scale,
|
| 340 |
+
latent_motion_frames=latent_motion_frames,
|
| 341 |
+
clamp_latent_len=clamp_latent_len,
|
| 342 |
+
device=DEVICE,
|
| 343 |
+
dtype=DTYPE,
|
| 344 |
+
)
|
| 345 |
+
decoded_cthw = _decode_to_cthw(pipeline, x_final)
|
| 346 |
+
decoded_cthw = _maybe_apply_color_correction(pipeline, decoded_cthw)
|
| 347 |
+
|
| 348 |
+
# SoulX-style VAE round-trip history update (reference default)
|
| 349 |
+
latent_motion_frames = _encode_motion_prefix_from_decoded(
|
| 350 |
+
pipeline,
|
| 351 |
+
decoded_video_cthw=decoded_cthw,
|
| 352 |
+
motion_frames_num=STREAM.motion_frames_num,
|
| 353 |
+
device=DEVICE,
|
| 354 |
+
dtype=DTYPE,
|
| 355 |
+
).unsqueeze(0)
|
| 356 |
+
clamp_latent_len = int(latent_motion_frames.shape[2])
|
| 357 |
+
|
| 358 |
+
decoded_cthw = decoded_cthw[:, STREAM.motion_frames_num:]
|
| 359 |
+
video_thwc = (
|
| 360 |
+
((decoded_cthw + 1.0) / 2.0)
|
| 361 |
+
.permute(1, 2, 3, 0)
|
| 362 |
+
.clamp(0.0, 1.0)
|
| 363 |
+
.mul(255.0)
|
| 364 |
+
.contiguous()
|
| 365 |
+
)
|
| 366 |
+
torch.cuda.synchronize()
|
| 367 |
+
chunk_seconds = time.perf_counter() - t0
|
| 368 |
+
|
| 369 |
+
frames_np = video_thwc.to(torch.float32).cpu().numpy().astype(np.uint8)
|
| 370 |
+
generated.append(frames_np)
|
| 371 |
+
gen_frames += int(frames_np.shape[0])
|
| 372 |
+
gen_seconds += chunk_seconds
|
| 373 |
+
logger.info(
|
| 374 |
+
f"chunk {chunk_idx + 1}/{num_chunks}: {chunk_seconds:.3f}s "
|
| 375 |
+
f"({frames_np.shape[0] / max(chunk_seconds, 1e-6):.1f} FPS)"
|
| 376 |
+
)
|
| 377 |
+
|
| 378 |
+
progress(0.97, desc="Encoding video…")
|
| 379 |
+
silent_path = os.path.join(workdir, "silent.mp4")
|
| 380 |
+
with imageio.get_writer(
|
| 381 |
+
silent_path,
|
| 382 |
+
format="mp4",
|
| 383 |
+
mode="I",
|
| 384 |
+
fps=FPS,
|
| 385 |
+
codec="h264",
|
| 386 |
+
pixelformat="yuv420p",
|
| 387 |
+
ffmpeg_params=["-bf", "0"],
|
| 388 |
+
) as writer:
|
| 389 |
+
for frames_np in generated:
|
| 390 |
+
for frame in frames_np:
|
| 391 |
+
writer.append_data(frame)
|
| 392 |
+
|
| 393 |
+
wav_path = _write_wav(os.path.join(workdir, "track.wav"), audio_all)
|
| 394 |
+
out_path = _mux(silent_path, wav_path, os.path.join(workdir, "leaptalk.mp4"))
|
| 395 |
+
|
| 396 |
+
video_seconds = gen_frames / FPS
|
| 397 |
+
report = (
|
| 398 |
+
f"**{gen_frames} frames** ({video_seconds:.1f}s of video) in "
|
| 399 |
+
f"**{gen_seconds:.2f}s** of GPU time — "
|
| 400 |
+
f"**{gen_frames / max(gen_seconds, 1e-6):.1f} FPS** generation throughput "
|
| 401 |
+
f"({gen_frames / max(gen_seconds, 1e-6) / FPS:.2f}× real time) over "
|
| 402 |
+
f"{num_chunks} streaming chunks at {num_inference_steps} step"
|
| 403 |
+
f"{'s' if num_inference_steps > 1 else ''}/chunk."
|
| 404 |
+
)
|
| 405 |
+
return out_path, report
|
| 406 |
+
|
| 407 |
+
|
| 408 |
+
# --------------------------------------------------------------------------------------
|
| 409 |
+
# UI
|
| 410 |
+
# --------------------------------------------------------------------------------------
|
| 411 |
+
CSS = """
|
| 412 |
+
#col-container { margin: 0 auto; max-width: 1180px; }
|
| 413 |
+
"""
|
| 414 |
+
|
| 415 |
+
with gr.Blocks(theme=gr.themes.Citrus(), css=CSS, title="LeapTalk") as demo:
|
| 416 |
+
with gr.Column(elem_id="col-container"):
|
| 417 |
+
gr.Markdown(
|
| 418 |
+
"""
|
| 419 |
+
# 🗣️ LeapTalk — real-time talking heads
|
| 420 |
+
|
| 421 |
+
Animate a **portrait photo** with a **speech clip**. LeapTalk reformulates talking-head
|
| 422 |
+
generation as a Brownian-bridge transport (*Bridge Forcing*), which lets it synthesize each
|
| 423 |
+
video chunk in a **single sampling step** while keeping identity stable over long rollouts.
|
| 424 |
+
|
| 425 |
+
[Model](https://huggingface.co/z-rx/leaptalk) · [Paper](https://huggingface.co/papers/2608.00079)
|
| 426 |
+
· [Project page](https://zhangrongxiang.github.io/leaptalk-page/)
|
| 427 |
+
· [Code](https://github.com/zhangrongxiang/LeapTalk)
|
| 428 |
+
· built on [SoulX-FlashHead-1.3B](https://huggingface.co/Soul-AILab/SoulX-FlashHead-1_3B)
|
| 429 |
+
"""
|
| 430 |
+
)
|
| 431 |
+
|
| 432 |
+
with gr.Row():
|
| 433 |
+
with gr.Column():
|
| 434 |
+
portrait_image = gr.Image(
|
| 435 |
+
label="Portrait", type="filepath", height=320, sources=["upload", "webcam", "clipboard"]
|
| 436 |
+
)
|
| 437 |
+
speech_audio = gr.Audio(
|
| 438 |
+
label="Speech audio", type="filepath", sources=["upload", "microphone"]
|
| 439 |
+
)
|
| 440 |
+
run_btn = gr.Button("Generate talking head", variant="primary")
|
| 441 |
+
with gr.Column():
|
| 442 |
+
video_out = gr.Video(
|
| 443 |
+
label="Result", height=460, autoplay=True, show_download_button=True
|
| 444 |
+
)
|
| 445 |
+
report_out = gr.Markdown()
|
| 446 |
+
|
| 447 |
+
with gr.Accordion("Advanced options", open=False):
|
| 448 |
+
with gr.Row():
|
| 449 |
+
max_seconds = gr.Slider(
|
| 450 |
+
label="Max audio length (seconds)",
|
| 451 |
+
minimum=1,
|
| 452 |
+
maximum=MAX_SECONDS_CAP,
|
| 453 |
+
step=1,
|
| 454 |
+
value=9,
|
| 455 |
+
)
|
| 456 |
+
num_inference_steps = gr.Slider(
|
| 457 |
+
label="Sampling steps per chunk",
|
| 458 |
+
minimum=1,
|
| 459 |
+
maximum=4,
|
| 460 |
+
step=1,
|
| 461 |
+
value=1,
|
| 462 |
+
info="LeapTalk is distilled for 1-step (1 NFE) generation.",
|
| 463 |
+
)
|
| 464 |
+
with gr.Row():
|
| 465 |
+
guidance_scale = gr.Slider(
|
| 466 |
+
label="Audio guidance scale",
|
| 467 |
+
minimum=1.0,
|
| 468 |
+
maximum=3.0,
|
| 469 |
+
step=0.1,
|
| 470 |
+
value=1.0,
|
| 471 |
+
info="1.0 disables audio CFG; higher strengthens lip motion but doubles compute.",
|
| 472 |
+
)
|
| 473 |
+
seed = gr.Number(label="Seed", value=42, precision=0)
|
| 474 |
+
auto_crop_face = gr.Checkbox(
|
| 475 |
+
label="Auto-crop to face",
|
| 476 |
+
value=True,
|
| 477 |
+
info="Detects the face and crops around it; falls back to a centre crop.",
|
| 478 |
+
)
|
| 479 |
+
|
| 480 |
+
gr.Examples(
|
| 481 |
+
examples=[
|
| 482 |
+
["examples/portrait.jpg", "examples/news.wav"],
|
| 483 |
+
["examples/girl.png", "examples/podcast_sichuan.wav"],
|
| 484 |
+
],
|
| 485 |
+
inputs=[portrait_image, speech_audio],
|
| 486 |
+
outputs=[video_out, report_out],
|
| 487 |
+
fn=generate,
|
| 488 |
+
cache_examples=True,
|
| 489 |
+
cache_mode="lazy",
|
| 490 |
+
)
|
| 491 |
+
|
| 492 |
+
gr.Markdown(
|
| 493 |
+
"Example assets: portrait + news clip from the "
|
| 494 |
+
"[LeapTalk](https://github.com/zhangrongxiang/LeapTalk) repository, portrait + podcast "
|
| 495 |
+
"clip from [SoulX-FlashHead](https://github.com/Soul-AILab/SoulX-FlashHead) "
|
| 496 |
+
"(both Apache-2.0). Audio clips were trimmed to 9 seconds."
|
| 497 |
+
)
|
| 498 |
+
|
| 499 |
+
gr.on(
|
| 500 |
+
triggers=[run_btn.click],
|
| 501 |
+
fn=generate,
|
| 502 |
+
inputs=[
|
| 503 |
+
portrait_image,
|
| 504 |
+
speech_audio,
|
| 505 |
+
max_seconds,
|
| 506 |
+
num_inference_steps,
|
| 507 |
+
guidance_scale,
|
| 508 |
+
seed,
|
| 509 |
+
auto_crop_face,
|
| 510 |
+
],
|
| 511 |
+
outputs=[video_out, report_out],
|
| 512 |
+
)
|
| 513 |
+
|
| 514 |
+
if __name__ == "__main__":
|
| 515 |
+
demo.launch(mcp_server=True)
|
examples/girl.png
ADDED
|
Git LFS Details
|
examples/news.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:08e71b2901fb19c33d02dc4c96a118d432f476a155ac849bcbf121602fdb3829
|
| 3 |
+
size 1587644
|
examples/podcast_sichuan.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:2dc8315e8f2a6a616baa467706769ca3a563696d280829fad68310357db60614
|
| 3 |
+
size 288044
|
examples/portrait.jpg
ADDED
|
Git LFS Details
|
flash_head/audio_analysis/torch_utils.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import torch.nn.functional as F
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
def get_mask_from_lengths(lengths, max_len=None):
|
| 6 |
+
lengths = lengths.to(torch.long)
|
| 7 |
+
if max_len is None:
|
| 8 |
+
max_len = torch.max(lengths).item()
|
| 9 |
+
|
| 10 |
+
ids = torch.arange(0, max_len).unsqueeze(0).expand(lengths.shape[0], -1).to(lengths.device)
|
| 11 |
+
mask = ids < lengths.unsqueeze(1).expand(-1, max_len)
|
| 12 |
+
|
| 13 |
+
return mask
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def linear_interpolation(features, seq_len):
|
| 17 |
+
features = features.transpose(1, 2)
|
| 18 |
+
output_features = F.interpolate(features, size=seq_len, align_corners=True, mode='linear')
|
| 19 |
+
return output_features.transpose(1, 2)
|
| 20 |
+
|
flash_head/audio_analysis/wav2vec2.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from transformers import Wav2Vec2Config, Wav2Vec2Model
|
| 2 |
+
from transformers.modeling_outputs import BaseModelOutput
|
| 3 |
+
|
| 4 |
+
from .torch_utils import linear_interpolation
|
| 5 |
+
|
| 6 |
+
# the implementation of Wav2Vec2Model is borrowed from
|
| 7 |
+
# https://github.com/huggingface/transformers/blob/HEAD/src/transformers/models/wav2vec2/modeling_wav2vec2.py
|
| 8 |
+
# initialize our encoder with the pre-trained wav2vec 2.0 weights.
|
| 9 |
+
class Wav2Vec2Model(Wav2Vec2Model):
|
| 10 |
+
def __init__(self, config: Wav2Vec2Config):
|
| 11 |
+
super().__init__(config)
|
| 12 |
+
|
| 13 |
+
def forward(
|
| 14 |
+
self,
|
| 15 |
+
input_values,
|
| 16 |
+
seq_len,
|
| 17 |
+
attention_mask=None,
|
| 18 |
+
mask_time_indices=None,
|
| 19 |
+
output_attentions=None,
|
| 20 |
+
output_hidden_states=None,
|
| 21 |
+
return_dict=None,
|
| 22 |
+
):
|
| 23 |
+
self.config.output_attentions = False
|
| 24 |
+
|
| 25 |
+
output_hidden_states = (
|
| 26 |
+
output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states
|
| 27 |
+
)
|
| 28 |
+
return_dict = return_dict if return_dict is not None else self.config.use_return_dict
|
| 29 |
+
|
| 30 |
+
extract_features = self.feature_extractor(input_values)
|
| 31 |
+
extract_features = extract_features.transpose(1, 2)
|
| 32 |
+
extract_features = linear_interpolation(extract_features, seq_len=seq_len)
|
| 33 |
+
|
| 34 |
+
if attention_mask is not None:
|
| 35 |
+
# compute reduced attention_mask corresponding to feature vectors
|
| 36 |
+
attention_mask = self._get_feature_vector_attention_mask(
|
| 37 |
+
extract_features.shape[1], attention_mask, add_adapter=False
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
hidden_states, extract_features = self.feature_projection(extract_features)
|
| 41 |
+
hidden_states = self._mask_hidden_states(
|
| 42 |
+
hidden_states, mask_time_indices=mask_time_indices, attention_mask=attention_mask
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
encoder_outputs = self.encoder(
|
| 46 |
+
hidden_states,
|
| 47 |
+
attention_mask=attention_mask,
|
| 48 |
+
output_attentions=output_attentions,
|
| 49 |
+
output_hidden_states=output_hidden_states,
|
| 50 |
+
return_dict=return_dict,
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
hidden_states = encoder_outputs[0]
|
| 54 |
+
|
| 55 |
+
if self.adapter is not None:
|
| 56 |
+
hidden_states = self.adapter(hidden_states)
|
| 57 |
+
|
| 58 |
+
if not return_dict:
|
| 59 |
+
return (hidden_states, ) + encoder_outputs[1:]
|
| 60 |
+
return BaseModelOutput(
|
| 61 |
+
last_hidden_state=hidden_states,
|
| 62 |
+
hidden_states=encoder_outputs.hidden_states,
|
| 63 |
+
attentions=encoder_outputs.attentions,
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def feature_extract(
|
| 68 |
+
self,
|
| 69 |
+
input_values,
|
| 70 |
+
seq_len,
|
| 71 |
+
):
|
| 72 |
+
extract_features = self.feature_extractor(input_values)
|
| 73 |
+
extract_features = extract_features.transpose(1, 2)
|
| 74 |
+
extract_features = linear_interpolation(extract_features, seq_len=seq_len)
|
| 75 |
+
|
| 76 |
+
return extract_features
|
| 77 |
+
|
| 78 |
+
def encode(
|
| 79 |
+
self,
|
| 80 |
+
extract_features,
|
| 81 |
+
attention_mask=None,
|
| 82 |
+
mask_time_indices=None,
|
| 83 |
+
output_attentions=None,
|
| 84 |
+
output_hidden_states=None,
|
| 85 |
+
return_dict=None,
|
| 86 |
+
):
|
| 87 |
+
self.config.output_attentions = False
|
| 88 |
+
|
| 89 |
+
output_hidden_states = (
|
| 90 |
+
output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states
|
| 91 |
+
)
|
| 92 |
+
return_dict = return_dict if return_dict is not None else self.config.use_return_dict
|
| 93 |
+
|
| 94 |
+
if attention_mask is not None:
|
| 95 |
+
# compute reduced attention_mask corresponding to feature vectors
|
| 96 |
+
attention_mask = self._get_feature_vector_attention_mask(
|
| 97 |
+
extract_features.shape[1], attention_mask, add_adapter=False
|
| 98 |
+
)
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
hidden_states, extract_features = self.feature_projection(extract_features)
|
| 102 |
+
hidden_states = self._mask_hidden_states(
|
| 103 |
+
hidden_states, mask_time_indices=mask_time_indices, attention_mask=attention_mask
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
encoder_outputs = self.encoder(
|
| 107 |
+
hidden_states,
|
| 108 |
+
attention_mask=attention_mask,
|
| 109 |
+
output_attentions=output_attentions,
|
| 110 |
+
output_hidden_states=output_hidden_states,
|
| 111 |
+
return_dict=return_dict,
|
| 112 |
+
)
|
| 113 |
+
|
| 114 |
+
hidden_states = encoder_outputs[0]
|
| 115 |
+
|
| 116 |
+
if self.adapter is not None:
|
| 117 |
+
hidden_states = self.adapter(hidden_states)
|
| 118 |
+
|
| 119 |
+
if not return_dict:
|
| 120 |
+
return (hidden_states, ) + encoder_outputs[1:]
|
| 121 |
+
return BaseModelOutput(
|
| 122 |
+
last_hidden_state=hidden_states,
|
| 123 |
+
hidden_states=encoder_outputs.hidden_states,
|
| 124 |
+
attentions=encoder_outputs.attentions,
|
| 125 |
+
)
|
flash_head/src/modules/flash_head_model.py
ADDED
|
@@ -0,0 +1,589 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import torch
|
| 3 |
+
import torch.nn as nn
|
| 4 |
+
import torch.nn.functional as F
|
| 5 |
+
import math
|
| 6 |
+
from typing import Tuple, Optional
|
| 7 |
+
from einops import rearrange
|
| 8 |
+
from diffusers import ModelMixin
|
| 9 |
+
from diffusers.configuration_utils import ConfigMixin, register_to_config
|
| 10 |
+
import torch.distributed as dist
|
| 11 |
+
|
| 12 |
+
# `torch.cuda.amp` is deprecated; fall back to the equivalent `torch.amp` API on
|
| 13 |
+
# newer torch releases where the alias has been removed.
|
| 14 |
+
try:
|
| 15 |
+
import torch.cuda.amp as amp
|
| 16 |
+
except (ImportError, AttributeError): # pragma: no cover
|
| 17 |
+
from torch import amp as _torch_amp
|
| 18 |
+
|
| 19 |
+
class amp: # noqa: N801 - drop-in shim for torch.cuda.amp
|
| 20 |
+
@staticmethod
|
| 21 |
+
def autocast(*args, **kwargs):
|
| 22 |
+
return _torch_amp.autocast("cuda", *args, **kwargs)
|
| 23 |
+
|
| 24 |
+
# xFuser is only needed for the multi-GPU USP (sequence-parallel) path. This Space
|
| 25 |
+
# always runs single-GPU, so the import is made optional to keep the environment slim.
|
| 26 |
+
try:
|
| 27 |
+
from xfuser.core.distributed import (
|
| 28 |
+
get_sequence_parallel_rank,
|
| 29 |
+
get_sequence_parallel_world_size,
|
| 30 |
+
get_sp_group,
|
| 31 |
+
)
|
| 32 |
+
from xfuser.core.long_ctx_attention import xFuserLongContextAttention
|
| 33 |
+
except ImportError: # single-GPU: USP is never enabled (see _maybe_enable_usp below)
|
| 34 |
+
get_sequence_parallel_rank = None
|
| 35 |
+
get_sequence_parallel_world_size = None
|
| 36 |
+
get_sp_group = None
|
| 37 |
+
xFuserLongContextAttention = None
|
| 38 |
+
try:
|
| 39 |
+
import flash_attn_interface
|
| 40 |
+
FLASH_ATTN_3_AVAILABLE = True
|
| 41 |
+
except ModuleNotFoundError:
|
| 42 |
+
FLASH_ATTN_3_AVAILABLE = False
|
| 43 |
+
|
| 44 |
+
try:
|
| 45 |
+
import flash_attn
|
| 46 |
+
FLASH_ATTN_2_AVAILABLE = True
|
| 47 |
+
except ModuleNotFoundError:
|
| 48 |
+
FLASH_ATTN_2_AVAILABLE = False
|
| 49 |
+
|
| 50 |
+
try:
|
| 51 |
+
from sageattention import sageattn
|
| 52 |
+
SAGE_ATTN_AVAILABLE = True
|
| 53 |
+
except ModuleNotFoundError:
|
| 54 |
+
SAGE_ATTN_AVAILABLE = False
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def _maybe_enable_usp() -> tuple[bool, int, int]:
|
| 58 |
+
"""Return (use_usp, sp_size, sp_rank).
|
| 59 |
+
|
| 60 |
+
The model code supports xFuser's USP/sequence-parallel path, but this
|
| 61 |
+
requires xFuser model-parallel groups to be initialized.
|
| 62 |
+
|
| 63 |
+
In plain torchrun DDP training we typically only initialize the default
|
| 64 |
+
torch.distributed process group; in that case xFuser groups are absent.
|
| 65 |
+
To keep DDP usable, we gracefully fall back to non-USP behavior.
|
| 66 |
+
"""
|
| 67 |
+
|
| 68 |
+
if not dist.is_initialized():
|
| 69 |
+
return False, 1, 0
|
| 70 |
+
|
| 71 |
+
try:
|
| 72 |
+
# These call into xFuser parallel_state and will assert if the group
|
| 73 |
+
# is not initialized.
|
| 74 |
+
sp_size = int(get_sequence_parallel_world_size())
|
| 75 |
+
sp_rank = int(get_sequence_parallel_rank())
|
| 76 |
+
if sp_size <= 1:
|
| 77 |
+
return False, 1, 0
|
| 78 |
+
return True, sp_size, sp_rank
|
| 79 |
+
except Exception:
|
| 80 |
+
return False, 1, 0
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def flash_attention(q: torch.Tensor, k: torch.Tensor, v: torch.Tensor, num_heads: int, compatibility_mode=False):
|
| 84 |
+
if compatibility_mode:
|
| 85 |
+
q = rearrange(q, "b s (n d) -> b n s d", n=num_heads)
|
| 86 |
+
k = rearrange(k, "b s (n d) -> b n s d", n=num_heads)
|
| 87 |
+
v = rearrange(v, "b s (n d) -> b n s d", n=num_heads)
|
| 88 |
+
x = F.scaled_dot_product_attention(q, k, v)
|
| 89 |
+
x = rearrange(x, "b n s d -> b s (n d)", n=num_heads)
|
| 90 |
+
elif SAGE_ATTN_AVAILABLE:
|
| 91 |
+
q = rearrange(q, "b s (n d) -> b n s d", n=num_heads)
|
| 92 |
+
k = rearrange(k, "b s (n d) -> b n s d", n=num_heads)
|
| 93 |
+
v = rearrange(v, "b s (n d) -> b n s d", n=num_heads)
|
| 94 |
+
x = sageattn(q, k, v)
|
| 95 |
+
x = rearrange(x, "b n s d -> b s (n d)", n=num_heads)
|
| 96 |
+
elif FLASH_ATTN_3_AVAILABLE:
|
| 97 |
+
q = rearrange(q, "b s (n d) -> b s n d", n=num_heads)
|
| 98 |
+
k = rearrange(k, "b s (n d) -> b s n d", n=num_heads)
|
| 99 |
+
v = rearrange(v, "b s (n d) -> b s n d", n=num_heads)
|
| 100 |
+
x = flash_attn_interface.flash_attn_func(q, k, v)
|
| 101 |
+
x = rearrange(x, "b s n d -> b s (n d)", n=num_heads)
|
| 102 |
+
elif FLASH_ATTN_2_AVAILABLE:
|
| 103 |
+
q = rearrange(q, "b s (n d) -> b s n d", n=num_heads)
|
| 104 |
+
k = rearrange(k, "b s (n d) -> b s n d", n=num_heads)
|
| 105 |
+
v = rearrange(v, "b s (n d) -> b s n d", n=num_heads)
|
| 106 |
+
x = flash_attn.flash_attn_func(q, k, v)
|
| 107 |
+
x = rearrange(x, "b s n d -> b s (n d)", n=num_heads)
|
| 108 |
+
else:
|
| 109 |
+
q = rearrange(q, "b s (n d) -> b n s d", n=num_heads)
|
| 110 |
+
k = rearrange(k, "b s (n d) -> b n s d", n=num_heads)
|
| 111 |
+
v = rearrange(v, "b s (n d) -> b n s d", n=num_heads)
|
| 112 |
+
x = F.scaled_dot_product_attention(q, k, v)
|
| 113 |
+
x = rearrange(x, "b n s d -> b s (n d)", n=num_heads)
|
| 114 |
+
return x
|
| 115 |
+
|
| 116 |
+
def sinusoidal_embedding_1d(dim, position):
|
| 117 |
+
sinusoid = torch.outer(position.type(torch.float64), torch.pow(
|
| 118 |
+
10000, -torch.arange(dim//2, dtype=torch.float64, device=position.device).div(dim//2)))
|
| 119 |
+
x = torch.cat([torch.cos(sinusoid), torch.sin(sinusoid)], dim=1)
|
| 120 |
+
return x.to(position.dtype)
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def precompute_freqs_cis_3d(dim: int, end: int = 1024, theta: float = 10000.0):
|
| 124 |
+
# 3d rope precompute
|
| 125 |
+
f_freqs_cis = precompute_freqs_cis(dim - 2 * (dim // 3), end, theta)
|
| 126 |
+
h_freqs_cis = precompute_freqs_cis(dim // 3, end, theta)
|
| 127 |
+
w_freqs_cis = precompute_freqs_cis(dim // 3, end, theta)
|
| 128 |
+
return torch.cat([f_freqs_cis, h_freqs_cis, w_freqs_cis], dim=1)
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
def precompute_freqs_cis(dim: int, end: int = 1024, theta: float = 10000.0):
|
| 132 |
+
# 1d rope precompute
|
| 133 |
+
freqs = 1.0 / (theta ** (torch.arange(0, dim, 2)
|
| 134 |
+
[: (dim // 2)].double() / dim))
|
| 135 |
+
freqs = torch.outer(torch.arange(end, device=freqs.device), freqs)
|
| 136 |
+
freqs_cis = torch.polar(torch.ones_like(freqs), freqs) # complex64
|
| 137 |
+
return freqs_cis
|
| 138 |
+
|
| 139 |
+
def pad_freqs(original_tensor, target_len):
|
| 140 |
+
seq_len, s1, s2 = original_tensor.shape
|
| 141 |
+
pad_size = target_len - seq_len
|
| 142 |
+
padding_tensor = torch.ones(
|
| 143 |
+
pad_size,
|
| 144 |
+
s1,
|
| 145 |
+
s2,
|
| 146 |
+
dtype=original_tensor.dtype,
|
| 147 |
+
device=original_tensor.device)
|
| 148 |
+
padded_tensor = torch.cat([original_tensor, padding_tensor], dim=0)
|
| 149 |
+
return padded_tensor
|
| 150 |
+
|
| 151 |
+
def rope_apply(x, freqs, grid_sizes, use_usp=False, sp_size=1, sp_rank=0):
|
| 152 |
+
"""
|
| 153 |
+
x: [B, L, N, C].
|
| 154 |
+
grid_sizes: [B, 3].
|
| 155 |
+
freqs: [M, C // 2].
|
| 156 |
+
"""
|
| 157 |
+
s, n, c = x.size(1), x.size(2), x.size(3) // 2
|
| 158 |
+
# split freqs
|
| 159 |
+
freqs = freqs.split([c - 2 * (c // 3), c // 3, c // 3], dim=1) # [[N, head_dim/2], [N, head_dim/2], [N, head_dim/2]] # T H W 极坐标
|
| 160 |
+
|
| 161 |
+
# loop over samples
|
| 162 |
+
|
| 163 |
+
(f, h, w) = grid_sizes
|
| 164 |
+
seq_len = f * h * w
|
| 165 |
+
|
| 166 |
+
# precompute multipliers
|
| 167 |
+
x_i = torch.view_as_complex(x[0, :s].to(torch.float64).reshape(
|
| 168 |
+
s, n, -1, 2)) # [L, N, C/2] # 极坐标
|
| 169 |
+
freqs_i = torch.cat([
|
| 170 |
+
freqs[0][:f].view(f, 1, 1, -1).expand(f, h, w, -1),
|
| 171 |
+
freqs[1][:h].view(1, h, 1, -1).expand(f, h, w, -1),
|
| 172 |
+
freqs[2][:w].view(1, 1, w, -1).expand(f, h, w, -1)
|
| 173 |
+
],
|
| 174 |
+
dim=-1).reshape(seq_len, 1, -1) # seq_lens, 1, 3 * dim / 2 (T H W)
|
| 175 |
+
|
| 176 |
+
if use_usp:
|
| 177 |
+
# apply rotary embedding
|
| 178 |
+
freqs_i = pad_freqs(freqs_i, s * sp_size)
|
| 179 |
+
s_per_rank = s
|
| 180 |
+
freqs_i_rank = freqs_i[(sp_rank * s_per_rank):((sp_rank + 1) *
|
| 181 |
+
s_per_rank), :, :]
|
| 182 |
+
x_i = torch.view_as_real(x_i * freqs_i_rank).flatten(2)
|
| 183 |
+
x_i = torch.cat([x_i, x[0, s:]])
|
| 184 |
+
else:
|
| 185 |
+
x_i = torch.view_as_real(x_i * freqs_i).flatten(2)
|
| 186 |
+
x_i = torch.cat([x_i, x[0, seq_len:]])
|
| 187 |
+
return x_i.unsqueeze(0).to(x.dtype)
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
class RMSNorm(nn.Module):
|
| 191 |
+
def __init__(self, dim, eps=1e-5):
|
| 192 |
+
super().__init__()
|
| 193 |
+
self.eps = eps
|
| 194 |
+
self.weight = nn.Parameter(torch.ones(dim))
|
| 195 |
+
|
| 196 |
+
def norm(self, x):
|
| 197 |
+
return x * torch.rsqrt(x.pow(2).mean(dim=-1, keepdim=True) + self.eps)
|
| 198 |
+
|
| 199 |
+
def forward(self, x):
|
| 200 |
+
dtype = x.dtype
|
| 201 |
+
return self.norm(x.float()).to(dtype) * self.weight
|
| 202 |
+
|
| 203 |
+
class SelfAttention(nn.Module):
|
| 204 |
+
def __init__(self, dim: int, num_heads: int, eps: float = 1e-6):
|
| 205 |
+
super().__init__()
|
| 206 |
+
self.dim = dim
|
| 207 |
+
self.num_heads = num_heads
|
| 208 |
+
self.head_dim = dim // num_heads
|
| 209 |
+
|
| 210 |
+
self.q = nn.Linear(dim, dim)
|
| 211 |
+
self.k = nn.Linear(dim, dim)
|
| 212 |
+
self.v = nn.Linear(dim, dim)
|
| 213 |
+
self.o = nn.Linear(dim, dim)
|
| 214 |
+
self.norm_q = RMSNorm(dim, eps=eps)
|
| 215 |
+
self.norm_k = RMSNorm(dim, eps=eps)
|
| 216 |
+
|
| 217 |
+
self.use_usp, self.sp_size, self.sp_rank = _maybe_enable_usp()
|
| 218 |
+
|
| 219 |
+
def forward(self, x, freqs, grid_sizes):
|
| 220 |
+
b, s, n, d = *x.shape[:2], self.num_heads, self.head_dim
|
| 221 |
+
q = self.norm_q(self.q(x)).view(b, s, n, d)
|
| 222 |
+
k = self.norm_k(self.k(x)).view(b, s, n, d)
|
| 223 |
+
v = self.v(x)
|
| 224 |
+
|
| 225 |
+
if self.use_usp:
|
| 226 |
+
from yunchang.kernels import AttnType
|
| 227 |
+
if SAGE_ATTN_AVAILABLE:
|
| 228 |
+
attn_type = AttnType.SAGE_AUTO
|
| 229 |
+
else:
|
| 230 |
+
attn_type = AttnType.FA
|
| 231 |
+
|
| 232 |
+
x = xFuserLongContextAttention(attn_type=attn_type)(
|
| 233 |
+
None,
|
| 234 |
+
query=rope_apply(q, freqs, grid_sizes, self.use_usp, self.sp_size, self.sp_rank),
|
| 235 |
+
key=rope_apply(k, freqs, grid_sizes, self.use_usp, self.sp_size, self.sp_rank),
|
| 236 |
+
value=v.view(b, s, n, d),
|
| 237 |
+
).flatten(2)
|
| 238 |
+
else:
|
| 239 |
+
x = flash_attention(
|
| 240 |
+
q=rope_apply(q, freqs, grid_sizes).flatten(2),
|
| 241 |
+
k=rope_apply(k, freqs, grid_sizes).flatten(2),
|
| 242 |
+
v=v,
|
| 243 |
+
num_heads=self.num_heads
|
| 244 |
+
)
|
| 245 |
+
return self.o(x)
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
class CrossAttention(nn.Module):
|
| 249 |
+
def __init__(self, dim: int, num_heads: int, eps: float = 1e-6, has_image_input: bool = False):
|
| 250 |
+
super().__init__()
|
| 251 |
+
self.dim = dim
|
| 252 |
+
self.num_heads = num_heads
|
| 253 |
+
self.head_dim = dim // num_heads
|
| 254 |
+
|
| 255 |
+
self.q = nn.Linear(dim, dim)
|
| 256 |
+
self.k = nn.Linear(dim, dim)
|
| 257 |
+
self.v = nn.Linear(dim, dim)
|
| 258 |
+
self.o = nn.Linear(dim, dim)
|
| 259 |
+
self.norm_q = RMSNorm(dim, eps=eps)
|
| 260 |
+
self.norm_k = RMSNorm(dim, eps=eps)
|
| 261 |
+
self.has_image_input = has_image_input
|
| 262 |
+
if has_image_input:
|
| 263 |
+
self.k_img = nn.Linear(dim, dim)
|
| 264 |
+
self.v_img = nn.Linear(dim, dim)
|
| 265 |
+
self.norm_k_img = RMSNorm(dim, eps=eps)
|
| 266 |
+
|
| 267 |
+
def forward(self, x: torch.Tensor, y: torch.Tensor):
|
| 268 |
+
if self.has_image_input:
|
| 269 |
+
img = y[:, :257]
|
| 270 |
+
ctx = y[:, 257:]
|
| 271 |
+
else:
|
| 272 |
+
ctx = y
|
| 273 |
+
q = self.norm_q(self.q(x))
|
| 274 |
+
k = self.norm_k(self.k(ctx))
|
| 275 |
+
v = self.v(ctx)
|
| 276 |
+
x = flash_attention(q, k, v, num_heads=self.num_heads)
|
| 277 |
+
if self.has_image_input:
|
| 278 |
+
k_img = self.norm_k_img(self.k_img(img))
|
| 279 |
+
v_img = self.v_img(img)
|
| 280 |
+
y = flash_attention(q, k_img, v_img, num_heads=self.num_heads)
|
| 281 |
+
x = x + y
|
| 282 |
+
return self.o(x)
|
| 283 |
+
|
| 284 |
+
class DiTAudioBlock(nn.Module):
|
| 285 |
+
def __init__(self, has_image_input: bool, dim: int, num_heads: int, ffn_dim: int, eps: float = 1e-6, i=0, num_layers=0):
|
| 286 |
+
super().__init__()
|
| 287 |
+
self.dim = dim
|
| 288 |
+
self.num_heads = num_heads
|
| 289 |
+
self.ffn_dim = ffn_dim
|
| 290 |
+
self.i = i
|
| 291 |
+
self.num_layers = num_layers
|
| 292 |
+
|
| 293 |
+
self.self_attn = SelfAttention(dim, num_heads, eps)
|
| 294 |
+
self.cross_attn = CrossAttention(
|
| 295 |
+
dim, num_heads, eps, has_image_input=has_image_input)
|
| 296 |
+
self.norm1 = nn.LayerNorm(dim, eps=eps, elementwise_affine=False)
|
| 297 |
+
self.norm2 = nn.LayerNorm(dim, eps=eps, elementwise_affine=False)
|
| 298 |
+
self.norm3 = nn.LayerNorm(dim, eps=eps)
|
| 299 |
+
self.ffn = nn.Sequential(nn.Linear(dim, ffn_dim), nn.GELU(
|
| 300 |
+
approximate='tanh'), nn.Linear(ffn_dim, dim))
|
| 301 |
+
self.modulation = nn.Parameter(torch.randn(1, 6, dim) / dim**0.5)
|
| 302 |
+
|
| 303 |
+
self.use_usp, self.sp_size, self.sp_rank = _maybe_enable_usp()
|
| 304 |
+
|
| 305 |
+
def forward(self, x, context, t_mod, freqs, grid_sizes):
|
| 306 |
+
e = (self.modulation.to(dtype=t_mod.dtype, device=t_mod.device) + t_mod).chunk(6, dim=1)
|
| 307 |
+
|
| 308 |
+
y = self.self_attn(
|
| 309 |
+
self.norm1(x) * (1 + e[1]) + e[0], freqs, grid_sizes)
|
| 310 |
+
|
| 311 |
+
x = x + y * e[2]
|
| 312 |
+
|
| 313 |
+
x_1 = rearrange(self.norm3(x), 'b (f l) c -> (b f) l c', f=context.shape[1])
|
| 314 |
+
context_1 = context.squeeze(0)
|
| 315 |
+
|
| 316 |
+
if self.use_usp:
|
| 317 |
+
context_1 = context_1.unsqueeze(1).repeat(1, self.sp_size, 1, 1).flatten(0,1)
|
| 318 |
+
context_1 = torch.chunk(context_1, self.sp_size, dim=0)[self.sp_rank]
|
| 319 |
+
|
| 320 |
+
x = x + self.cross_attn(x_1, context_1).flatten(0, 1).unsqueeze(0)
|
| 321 |
+
|
| 322 |
+
y = self.ffn(self.norm2(x) * (1 + e[4]) + e[3])
|
| 323 |
+
x = x + y * e[5]
|
| 324 |
+
|
| 325 |
+
return x
|
| 326 |
+
|
| 327 |
+
class MLP(torch.nn.Module):
|
| 328 |
+
def __init__(self, in_dim, out_dim):
|
| 329 |
+
super().__init__()
|
| 330 |
+
self.proj = torch.nn.Sequential(
|
| 331 |
+
nn.LayerNorm(in_dim),
|
| 332 |
+
nn.Linear(in_dim, in_dim),
|
| 333 |
+
nn.GELU(),
|
| 334 |
+
nn.Linear(in_dim, out_dim),
|
| 335 |
+
nn.LayerNorm(out_dim)
|
| 336 |
+
)
|
| 337 |
+
|
| 338 |
+
def forward(self, x):
|
| 339 |
+
return self.proj(x)
|
| 340 |
+
|
| 341 |
+
|
| 342 |
+
class Head(nn.Module):
|
| 343 |
+
def __init__(self, dim: int, out_dim: int, patch_size: Tuple[int, int, int], eps: float):
|
| 344 |
+
super().__init__()
|
| 345 |
+
self.dim = dim
|
| 346 |
+
self.patch_size = patch_size
|
| 347 |
+
self.norm = nn.LayerNorm(dim, eps=eps, elementwise_affine=False)
|
| 348 |
+
self.head = nn.Linear(dim, out_dim * math.prod(patch_size))
|
| 349 |
+
self.modulation = nn.Parameter(torch.randn(1, 2, dim) / dim**0.5)
|
| 350 |
+
|
| 351 |
+
def forward(self, x, t_mod):
|
| 352 |
+
r"""
|
| 353 |
+
Args:
|
| 354 |
+
x(Tensor): Shape [B, L1, C]
|
| 355 |
+
t_mod(Tensor): Shape [B*21, C]
|
| 356 |
+
"""
|
| 357 |
+
B, L, D = x.shape
|
| 358 |
+
F = t_mod.shape[0] // B
|
| 359 |
+
shift, scale = (self.modulation.to(dtype=t_mod.dtype, device=t_mod.device).unsqueeze(1) + t_mod.unflatten(dim=0, sizes=(B, t_mod.shape[0]//B)).unsqueeze(2)).chunk(2, dim=2)
|
| 360 |
+
|
| 361 |
+
x = rearrange(x, 'b (f l) d -> b f l d', f=F)
|
| 362 |
+
x = (self.head(self.norm(x) * (1 + scale) + shift))
|
| 363 |
+
x = rearrange(x, 'b f l d -> b (f l) d')
|
| 364 |
+
return x
|
| 365 |
+
|
| 366 |
+
class WanModelAudioProject(ModelMixin, ConfigMixin):
|
| 367 |
+
_no_split_modules = ['DiTAudioBlock']
|
| 368 |
+
@register_to_config
|
| 369 |
+
def __init__(
|
| 370 |
+
self,
|
| 371 |
+
dim: int,
|
| 372 |
+
in_dim: int,
|
| 373 |
+
ffn_dim: int,
|
| 374 |
+
out_dim: int,
|
| 375 |
+
text_dim: int,
|
| 376 |
+
freq_dim: int,
|
| 377 |
+
eps: float,
|
| 378 |
+
vae_stride: Tuple[int, int, int],
|
| 379 |
+
patch_size: Tuple[int, int, int],
|
| 380 |
+
num_heads: int,
|
| 381 |
+
num_layers: int,
|
| 382 |
+
has_image_input: bool,
|
| 383 |
+
**kwargs,
|
| 384 |
+
):
|
| 385 |
+
super().__init__()
|
| 386 |
+
self.dim = dim
|
| 387 |
+
self.freq_dim = freq_dim
|
| 388 |
+
self.has_image_input = has_image_input
|
| 389 |
+
self.patch_size = patch_size
|
| 390 |
+
|
| 391 |
+
self.patch_embedding = nn.Conv3d(
|
| 392 |
+
in_dim, dim, kernel_size=patch_size, stride=patch_size)
|
| 393 |
+
self.text_embedding = nn.Sequential(
|
| 394 |
+
nn.Linear(text_dim, dim),
|
| 395 |
+
nn.GELU(approximate='tanh'),
|
| 396 |
+
nn.Linear(dim, dim)
|
| 397 |
+
)
|
| 398 |
+
self.time_embedding = nn.Sequential(
|
| 399 |
+
nn.Linear(freq_dim, dim),
|
| 400 |
+
nn.SiLU(),
|
| 401 |
+
nn.Linear(dim, dim)
|
| 402 |
+
)
|
| 403 |
+
self.time_projection = nn.Sequential(
|
| 404 |
+
nn.SiLU(), nn.Linear(dim, dim * 6))
|
| 405 |
+
self.blocks = nn.ModuleList([
|
| 406 |
+
DiTAudioBlock(has_image_input, dim, num_heads, ffn_dim, eps, i, num_layers)
|
| 407 |
+
for i in range(num_layers)
|
| 408 |
+
])
|
| 409 |
+
self.head = Head(dim, out_dim, patch_size, eps)
|
| 410 |
+
head_dim = dim // num_heads
|
| 411 |
+
self.freqs = precompute_freqs_cis_3d(head_dim)
|
| 412 |
+
|
| 413 |
+
self.audio_emb = MLP(768, dim)
|
| 414 |
+
|
| 415 |
+
if has_image_input:
|
| 416 |
+
self.img_emb = MLP(1280, dim)
|
| 417 |
+
|
| 418 |
+
# init audio adapter
|
| 419 |
+
audio_window = 5
|
| 420 |
+
vae_scale = vae_stride[0]
|
| 421 |
+
intermediate_dim = 512
|
| 422 |
+
output_dim = 1536
|
| 423 |
+
context_tokens = 32
|
| 424 |
+
norm_output_audio = True
|
| 425 |
+
self.audio_window = audio_window
|
| 426 |
+
self.vae_scale = vae_scale
|
| 427 |
+
self.audio_proj = AudioProjModel(
|
| 428 |
+
seq_len=audio_window,
|
| 429 |
+
seq_len_vf=audio_window+vae_scale-1,
|
| 430 |
+
intermediate_dim=intermediate_dim,
|
| 431 |
+
output_dim=output_dim,
|
| 432 |
+
context_tokens=context_tokens,
|
| 433 |
+
norm_output_audio=norm_output_audio,
|
| 434 |
+
)
|
| 435 |
+
|
| 436 |
+
self.use_usp, self.sp_size, self.sp_rank = _maybe_enable_usp()
|
| 437 |
+
|
| 438 |
+
def patchify(self, x: torch.Tensor):
|
| 439 |
+
x = self.patch_embedding(x)
|
| 440 |
+
grid_size = x.shape[2:]
|
| 441 |
+
x = rearrange(x, 'b c f h w -> b (f h w) c').contiguous()
|
| 442 |
+
return x, grid_size # x, grid_size: (f, h, w)
|
| 443 |
+
|
| 444 |
+
def unpatchify(self, x: torch.Tensor, grid_size: torch.Tensor):
|
| 445 |
+
return rearrange(
|
| 446 |
+
x, 'b (f h w) (x y z c) -> b c (f x) (h y) (w z)',
|
| 447 |
+
f=grid_size[0], h=grid_size[1], w=grid_size[2],
|
| 448 |
+
x=self.patch_size[0], y=self.patch_size[1], z=self.patch_size[2]
|
| 449 |
+
)
|
| 450 |
+
|
| 451 |
+
def forward(self,
|
| 452 |
+
x: torch.Tensor, #(1, 16, 9, 64, 64))
|
| 453 |
+
timestep: torch.Tensor, #(9,)
|
| 454 |
+
context: torch.Tensor, #(5, 33, 12, 768)
|
| 455 |
+
y: Optional[torch.Tensor] = None, #(1, 16, 9, 64, 64)
|
| 456 |
+
use_gradient_checkpointing: bool = False,
|
| 457 |
+
use_gradient_checkpointing_offload: bool = False,
|
| 458 |
+
**kwargs,
|
| 459 |
+
):
|
| 460 |
+
|
| 461 |
+
if self.freqs.device != x.device:
|
| 462 |
+
self.freqs = self.freqs.to(x.device)
|
| 463 |
+
|
| 464 |
+
x = torch.cat([x, y], dim=1) # (1, 32, 9, 64, 64)
|
| 465 |
+
x, grid_sizes = self.patchify(x)
|
| 466 |
+
t = self.time_embedding(
|
| 467 |
+
sinusoidal_embedding_1d(self.freq_dim, timestep.to(dtype=x.dtype)))
|
| 468 |
+
t_mod = self.time_projection(t).unflatten(1, (6, self.dim)) # (bsz, 6, 1536)
|
| 469 |
+
|
| 470 |
+
# ==================== 音频条件处理 ====================
|
| 471 |
+
# 输入: context (bsz, 81, 5, 12, 768)
|
| 472 |
+
# - 81 帧 = 1 (第一帧) + 80 (后续帧, 每4帧对应VAE压缩后的1帧)
|
| 473 |
+
# - 5 是音频窗口大小 (audio_window)
|
| 474 |
+
# - 12 是音频特征的 blocks
|
| 475 |
+
# - 768 是音频特征维度
|
| 476 |
+
|
| 477 |
+
audio_cond = context.to(device=x.device, dtype=x.dtype)
|
| 478 |
+
|
| 479 |
+
# 1. 第一帧:直接使用完整的5帧音频窗口
|
| 480 |
+
first_frame_audio = audio_cond[:, :1, ...] # (bsz, 1, 5, 12, 768)
|
| 481 |
+
|
| 482 |
+
# 2. 后续帧:需要根据帧位置选择不同的音频窗口
|
| 483 |
+
# 将 32 帧重排为 (8 个 VAE latent, 每个4帧)
|
| 484 |
+
latter_frames_audio = rearrange(
|
| 485 |
+
audio_cond[:, 1:, ...],
|
| 486 |
+
"b (n_latent n_frame) w s c -> b n_latent n_frame w s c",
|
| 487 |
+
n_frame=self.vae_scale # vae_scale=4
|
| 488 |
+
) # (bsz, 8, 4, 5, 12, 768)
|
| 489 |
+
|
| 490 |
+
mid_idx = self.audio_window // 2 # 窗口中心索引: 5//2=2
|
| 491 |
+
|
| 492 |
+
# 为每个 latent 的4帧选择合适的音频窗口:
|
| 493 |
+
# - 第1帧 (帧索引0): 无过去,取前3帧窗口 [:mid_idx+1] = [:3]
|
| 494 |
+
# - 中间帧 (帧索引1-2): 取中心1帧 [mid_idx:mid_idx+1] = [2:3]
|
| 495 |
+
# - 第4帧 (帧索引3): 无未来,取后3帧窗口 [mid_idx:] = [2:]
|
| 496 |
+
|
| 497 |
+
first_of_group = latter_frames_audio[:, :, :1, :mid_idx+1, ...] # (bsz, 8, 1, 3, 12, 768)
|
| 498 |
+
middle_of_group = latter_frames_audio[:, :, 1:-1, mid_idx:mid_idx+1, ...] # (bsz, 8, 2, 1, 12, 768)
|
| 499 |
+
last_of_group = latter_frames_audio[:, :, -1:, mid_idx:, ...] # (bsz, 8, 1, 3, 12, 768)
|
| 500 |
+
|
| 501 |
+
# 合并并展平窗口维度: (n_frame, window) -> (n_frame * window)
|
| 502 |
+
latter_frames_audio_processed = torch.cat([
|
| 503 |
+
rearrange(first_of_group, "b n_latent n_f w s c -> b n_latent (n_f w) s c"),
|
| 504 |
+
rearrange(middle_of_group, "b n_latent n_f w s c -> b n_latent (n_f w) s c"),
|
| 505 |
+
rearrange(last_of_group, "b n_latent n_f w s c -> b n_latent (n_f w) s c"),
|
| 506 |
+
], dim=2) # (bsz, 8, 1*3 + 2*1 + 1*3, 12, 768) = (bsz, 8, 8, 12, 768)
|
| 507 |
+
|
| 508 |
+
# 3. 通过 AudioProjModel 投影到 DiT 所需的特征空间
|
| 509 |
+
context = self.audio_proj(
|
| 510 |
+
first_frame_audio,
|
| 511 |
+
latter_frames_audio_processed
|
| 512 |
+
).to(x.dtype) # (bsz, 9, 32, 1536)
|
| 513 |
+
|
| 514 |
+
if self.use_usp:
|
| 515 |
+
x = torch.chunk(x, self.sp_size, dim=1)[self.sp_rank]
|
| 516 |
+
|
| 517 |
+
for block in self.blocks:
|
| 518 |
+
x = block(x, context, t_mod, self.freqs, grid_sizes)
|
| 519 |
+
x = self.head(x, t) # (bsz, 9*32*32, 64)
|
| 520 |
+
if self.use_usp:
|
| 521 |
+
x = get_sp_group().all_gather(x, dim=1)
|
| 522 |
+
x = self.unpatchify(x, grid_sizes) # (bsz, 16, 21, 64, 64)
|
| 523 |
+
return x
|
| 524 |
+
|
| 525 |
+
|
| 526 |
+
class AudioProjModel(ModelMixin, ConfigMixin):
|
| 527 |
+
def __init__(
|
| 528 |
+
self,
|
| 529 |
+
seq_len=5,
|
| 530 |
+
seq_len_vf=12,
|
| 531 |
+
blocks=12,
|
| 532 |
+
channels=768,
|
| 533 |
+
intermediate_dim=512,
|
| 534 |
+
output_dim=768,
|
| 535 |
+
context_tokens=32,
|
| 536 |
+
norm_output_audio=False,
|
| 537 |
+
):
|
| 538 |
+
super().__init__()
|
| 539 |
+
|
| 540 |
+
self.seq_len = seq_len
|
| 541 |
+
self.blocks = blocks
|
| 542 |
+
self.channels = channels
|
| 543 |
+
self.input_dim = seq_len * blocks * channels
|
| 544 |
+
self.input_dim_vf = seq_len_vf * blocks * channels
|
| 545 |
+
self.intermediate_dim = intermediate_dim
|
| 546 |
+
self.context_tokens = context_tokens
|
| 547 |
+
self.output_dim = output_dim
|
| 548 |
+
|
| 549 |
+
# define multiple linear layers
|
| 550 |
+
self.proj1 = nn.Linear(self.input_dim, intermediate_dim)
|
| 551 |
+
self.proj1_vf = nn.Linear(self.input_dim_vf, intermediate_dim)
|
| 552 |
+
self.proj2 = nn.Linear(intermediate_dim, intermediate_dim)
|
| 553 |
+
self.proj3 = nn.Linear(intermediate_dim, context_tokens * output_dim)
|
| 554 |
+
self.norm = nn.LayerNorm(output_dim) if norm_output_audio else nn.Identity()
|
| 555 |
+
|
| 556 |
+
def forward(self, audio_embeds, audio_embeds_vf):
|
| 557 |
+
video_length = audio_embeds.shape[1] + audio_embeds_vf.shape[1]
|
| 558 |
+
B, _, _, S, C = audio_embeds.shape
|
| 559 |
+
|
| 560 |
+
# process audio of first frame
|
| 561 |
+
audio_embeds = rearrange(audio_embeds, "bz f w b c -> (bz f) w b c")
|
| 562 |
+
batch_size, window_size, blocks, channels = audio_embeds.shape
|
| 563 |
+
audio_embeds = audio_embeds.view(batch_size, window_size * blocks * channels)
|
| 564 |
+
|
| 565 |
+
# process audio of latter frame
|
| 566 |
+
audio_embeds_vf = rearrange(audio_embeds_vf, "bz f w b c -> (bz f) w b c")
|
| 567 |
+
batch_size_vf, window_size_vf, blocks_vf, channels_vf = audio_embeds_vf.shape
|
| 568 |
+
audio_embeds_vf = audio_embeds_vf.view(batch_size_vf, window_size_vf * blocks_vf * channels_vf)
|
| 569 |
+
|
| 570 |
+
# first projection
|
| 571 |
+
audio_embeds = torch.relu(self.proj1(audio_embeds))
|
| 572 |
+
audio_embeds_vf = torch.relu(self.proj1_vf(audio_embeds_vf))
|
| 573 |
+
audio_embeds = rearrange(audio_embeds, "(bz f) c -> bz f c", bz=B)
|
| 574 |
+
audio_embeds_vf = rearrange(audio_embeds_vf, "(bz f) c -> bz f c", bz=B)
|
| 575 |
+
audio_embeds_c = torch.concat([audio_embeds, audio_embeds_vf], dim=1)
|
| 576 |
+
batch_size_c, N_t, C_a = audio_embeds_c.shape
|
| 577 |
+
audio_embeds_c = audio_embeds_c.view(batch_size_c*N_t, C_a)
|
| 578 |
+
|
| 579 |
+
# second projection
|
| 580 |
+
audio_embeds_c = torch.relu(self.proj2(audio_embeds_c))
|
| 581 |
+
|
| 582 |
+
context_tokens = self.proj3(audio_embeds_c).reshape(batch_size_c*N_t, self.context_tokens, self.output_dim)
|
| 583 |
+
|
| 584 |
+
# normalization and reshape
|
| 585 |
+
with amp.autocast(dtype=torch.float32):
|
| 586 |
+
context_tokens = self.norm(context_tokens)
|
| 587 |
+
context_tokens = rearrange(context_tokens, "(bz f) m c -> bz f m c", f=video_length)
|
| 588 |
+
|
| 589 |
+
return context_tokens
|
flash_head/src/pipeline/flash_head_pipeline.py
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024-2025 The Alibaba Wan Team Authors. All rights reserved.
|
| 2 |
+
import os
|
| 3 |
+
from PIL import Image
|
| 4 |
+
from loguru import logger
|
| 5 |
+
import time
|
| 6 |
+
import numpy as np
|
| 7 |
+
import torch
|
| 8 |
+
import torch.distributed as dist
|
| 9 |
+
from einops import rearrange
|
| 10 |
+
|
| 11 |
+
from transformers import Wav2Vec2FeatureExtractor
|
| 12 |
+
|
| 13 |
+
from flash_head.src.modules.flash_head_model import WanModelAudioProject
|
| 14 |
+
from flash_head.audio_analysis.wav2vec2 import Wav2Vec2Model
|
| 15 |
+
from flash_head.utils.utils import match_and_blend_colors_torch, resize_and_centercrop
|
| 16 |
+
from flash_head.utils.facecrop import process_image
|
| 17 |
+
|
| 18 |
+
# compile models to speedup inference
|
| 19 |
+
COMPILE_MODEL = True
|
| 20 |
+
COMPILE_VAE = True
|
| 21 |
+
# use parallel vae to speedup decode/encode, only support WanVAE
|
| 22 |
+
USE_PARALLEL_VAE = True
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class _TAEAdapter:
|
| 26 |
+
"""Adapter to make TAEHV look like WanVAE (encode/decode signatures).
|
| 27 |
+
|
| 28 |
+
FlashHead code expects:
|
| 29 |
+
- encode(video[B=1,C,T,H,W] in [-1,1]) -> latent[C_lat,T_lat,h,w]
|
| 30 |
+
- decode(latent[C_lat,T_lat,h,w]) -> video[B=1,C,T,H,W] in [-1,1]
|
| 31 |
+
|
| 32 |
+
TAEHV expects NTCHW layout and operates in [0,1] pixel space.
|
| 33 |
+
"""
|
| 34 |
+
|
| 35 |
+
def __init__(self, tae, dtype, device):
|
| 36 |
+
self.model = tae.eval().requires_grad_(False).to(device=device, dtype=dtype)
|
| 37 |
+
self.dtype = dtype
|
| 38 |
+
self.device = device
|
| 39 |
+
|
| 40 |
+
@torch.no_grad()
|
| 41 |
+
def encode(self, video, world_size_h=None, world_size_w=None):
|
| 42 |
+
if video is None or video.dim() != 5:
|
| 43 |
+
raise ValueError(f"TAE encode expects [1,C,T,H,W], got {None if video is None else tuple(video.shape)}")
|
| 44 |
+
|
| 45 |
+
# [1,C,T,H,W] in [-1,1] -> [1,T,C,H,W] in [0,1]
|
| 46 |
+
x = video.to(device=self.device, dtype=self.dtype)
|
| 47 |
+
x = (x + 1.0) / 2.0
|
| 48 |
+
x = x.permute(0, 2, 1, 3, 4).contiguous()
|
| 49 |
+
|
| 50 |
+
z = self.model.encode_video(x, parallel=True, show_progress_bar=False) # [1,T_lat,C_lat,h,w]
|
| 51 |
+
z = z.permute(0, 2, 1, 3, 4).contiguous().squeeze(0) # [C_lat,T_lat,h,w]
|
| 52 |
+
return z
|
| 53 |
+
|
| 54 |
+
@torch.no_grad()
|
| 55 |
+
def decode(self, zs):
|
| 56 |
+
if zs is None or zs.dim() != 4:
|
| 57 |
+
raise ValueError(f"TAE decode expects [C_lat,T_lat,h,w], got {None if zs is None else tuple(zs.shape)}")
|
| 58 |
+
|
| 59 |
+
z = zs.to(device=self.device, dtype=self.dtype)
|
| 60 |
+
z = z.unsqueeze(0).permute(0, 2, 1, 3, 4).contiguous() # [1,T_lat,C_lat,h,w]
|
| 61 |
+
|
| 62 |
+
x = self.model.decode_video(z, parallel=True, show_progress_bar=False) # [1,T,C,H,W] in [0,1]
|
| 63 |
+
x = x.permute(0, 2, 1, 3, 4).contiguous() # [1,C,T,H,W]
|
| 64 |
+
x = x * 2.0 - 1.0
|
| 65 |
+
return x.clamp_(-1, 1)
|
| 66 |
+
|
| 67 |
+
def get_cond_image_dict(cond_image_path_or_dir, use_face_crop):
|
| 68 |
+
def get_image(cond_image_path, use_face_crop):
|
| 69 |
+
if use_face_crop:
|
| 70 |
+
try:
|
| 71 |
+
image = process_image(cond_image_path)
|
| 72 |
+
return image
|
| 73 |
+
except Exception as e:
|
| 74 |
+
logger.error(f"Error processing {cond_image_path}: {e}")
|
| 75 |
+
return Image.open(cond_image_path).convert("RGB")
|
| 76 |
+
|
| 77 |
+
if os.path.isdir(cond_image_path_or_dir):
|
| 78 |
+
import glob
|
| 79 |
+
cond_image_list = glob.glob(os.path.join(cond_image_path_or_dir, "*.png"))
|
| 80 |
+
cond_image_list.sort()
|
| 81 |
+
cond_image_dict = {cond_image.split("/")[-1].split(".")[0]: get_image(cond_image, use_face_crop) for cond_image in cond_image_list}
|
| 82 |
+
else:
|
| 83 |
+
cond_image_dict = {cond_image_path_or_dir.split("/")[-1].split(".")[0]: get_image(cond_image_path_or_dir, use_face_crop)}
|
| 84 |
+
return cond_image_dict
|
| 85 |
+
|
| 86 |
+
def timestep_transform(
|
| 87 |
+
t,
|
| 88 |
+
shift=5.0,
|
| 89 |
+
num_timesteps=1000,
|
| 90 |
+
):
|
| 91 |
+
t = t / num_timesteps
|
| 92 |
+
# shift the timestep based on ratio
|
| 93 |
+
new_t = shift * t / (1 + (shift - 1) * t)
|
| 94 |
+
new_t = new_t * num_timesteps
|
| 95 |
+
return new_t
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
class FlashHeadPipeline:
|
| 99 |
+
def __init__(
|
| 100 |
+
self,
|
| 101 |
+
checkpoint_dir,
|
| 102 |
+
model_type,
|
| 103 |
+
wav2vec_dir,
|
| 104 |
+
device="cuda",
|
| 105 |
+
param_dtype=torch.bfloat16,
|
| 106 |
+
use_usp=False,
|
| 107 |
+
num_timesteps=1000,
|
| 108 |
+
use_timestep_transform=True,
|
| 109 |
+
use_tae=False,
|
| 110 |
+
tae_path=None,
|
| 111 |
+
tae_model_type="wan21",
|
| 112 |
+
):
|
| 113 |
+
r"""
|
| 114 |
+
Initializes the image-to-video generation model components.
|
| 115 |
+
Args:
|
| 116 |
+
checkpoint_dir (`str`):
|
| 117 |
+
Path to directory containing model checkpoints
|
| 118 |
+
wav2vec_dir (`str`):
|
| 119 |
+
Path to directory containing wav2vec checkpoints
|
| 120 |
+
use_usp (`bool`, *optional*, defaults to False):
|
| 121 |
+
Enable distribution strategy of USP.
|
| 122 |
+
"""
|
| 123 |
+
self.param_dtype = param_dtype
|
| 124 |
+
self.device = device
|
| 125 |
+
self.rank = dist.get_rank() if dist.is_initialized() else 0
|
| 126 |
+
self.use_usp = use_usp and dist.is_initialized()
|
| 127 |
+
self.model_type = model_type
|
| 128 |
+
self.use_ltx = model_type == "lite"
|
| 129 |
+
|
| 130 |
+
if self.use_ltx:
|
| 131 |
+
model_dir = os.path.join(checkpoint_dir, "Model_Lite")
|
| 132 |
+
vae_dir = os.path.join(checkpoint_dir, "VAE_LTX")
|
| 133 |
+
|
| 134 |
+
from flash_head.ltx_video.ltx_vae import LtxVAE
|
| 135 |
+
self.vae = LtxVAE(
|
| 136 |
+
pretrained_model_type_or_path=vae_dir,
|
| 137 |
+
dtype=self.param_dtype,
|
| 138 |
+
device=self.device,
|
| 139 |
+
)
|
| 140 |
+
else:
|
| 141 |
+
vae_path = os.path.join(checkpoint_dir, "VAE_Wan/Wan2.1_VAE.pth")
|
| 142 |
+
|
| 143 |
+
if use_tae:
|
| 144 |
+
if tae_path is None:
|
| 145 |
+
tae_path = os.path.join(checkpoint_dir, "VAE_Wan/taew2_1.pth")
|
| 146 |
+
from flash_head.wan.modules.tae import TAEHV
|
| 147 |
+
|
| 148 |
+
logger.info(f"Using TAEHV for VAE encode/decode: {tae_path}")
|
| 149 |
+
if USE_PARALLEL_VAE and self.use_usp:
|
| 150 |
+
logger.warning("USE_PARALLEL_VAE is enabled but TAEHV does not support VAE parallel sharding; falling back to single-rank VAE.")
|
| 151 |
+
|
| 152 |
+
tae = TAEHV(checkpoint_path=tae_path, model_type=str(tae_model_type))
|
| 153 |
+
self.vae = _TAEAdapter(tae, dtype=self.param_dtype, device=self.device)
|
| 154 |
+
self.use_tae = True
|
| 155 |
+
else:
|
| 156 |
+
from flash_head.wan.modules import WanVAE
|
| 157 |
+
self.vae = WanVAE(
|
| 158 |
+
vae_path=vae_path,
|
| 159 |
+
dtype=self.param_dtype,
|
| 160 |
+
device=self.device,
|
| 161 |
+
parallel=(USE_PARALLEL_VAE and self.use_usp),
|
| 162 |
+
)
|
| 163 |
+
self.use_tae = False
|
| 164 |
+
|
| 165 |
+
if self.model_type == "pretrained":
|
| 166 |
+
self.audio_guide_scale = 3.0
|
| 167 |
+
model_dir = os.path.join(checkpoint_dir, "teacher")
|
| 168 |
+
elif self.model_type == "pro":
|
| 169 |
+
model_dir = os.path.join(checkpoint_dir, "Model_Pro")
|
| 170 |
+
|
| 171 |
+
self.model = WanModelAudioProject.from_pretrained(model_dir)
|
| 172 |
+
self.model.eval().requires_grad_(False)
|
| 173 |
+
self.model.to(device=self.device, dtype=self.param_dtype)
|
| 174 |
+
|
| 175 |
+
self.config = self.model.config
|
| 176 |
+
|
| 177 |
+
if use_usp:
|
| 178 |
+
from xfuser.core.distributed import get_sequence_parallel_world_size
|
| 179 |
+
self.sp_size = get_sequence_parallel_world_size()
|
| 180 |
+
else:
|
| 181 |
+
self.sp_size = 1
|
| 182 |
+
|
| 183 |
+
if dist.is_initialized():
|
| 184 |
+
dist.barrier()
|
| 185 |
+
|
| 186 |
+
self.num_timesteps = num_timesteps
|
| 187 |
+
self.use_timestep_transform = use_timestep_transform
|
| 188 |
+
|
| 189 |
+
if COMPILE_MODEL:
|
| 190 |
+
self.model = torch.compile(self.model)
|
| 191 |
+
if COMPILE_VAE:
|
| 192 |
+
if self.use_ltx:
|
| 193 |
+
self.vae.model.encode = torch.compile(self.vae.model.encode)
|
| 194 |
+
self.vae.model.decode = torch.compile(self.vae.model.decode)
|
| 195 |
+
else:
|
| 196 |
+
if getattr(self, "use_tae", False):
|
| 197 |
+
logger.warning("COMPILE_VAE is enabled, but TAEHV backend is selected; skipping torch.compile for VAE.")
|
| 198 |
+
else:
|
| 199 |
+
self.vae.encode = torch.compile(self.vae.encode)
|
| 200 |
+
self.vae.decode = torch.compile(self.vae.decode)
|
| 201 |
+
|
| 202 |
+
self.audio_encoder = Wav2Vec2Model.from_pretrained(wav2vec_dir, local_files_only=True).to(self.device)
|
| 203 |
+
self.audio_encoder.feature_extractor._freeze_parameters()
|
| 204 |
+
self.wav2vec_feature_extractor = Wav2Vec2FeatureExtractor.from_pretrained(wav2vec_dir, local_files_only=True)
|
| 205 |
+
|
| 206 |
+
@torch.no_grad()
|
| 207 |
+
def prepare_params(self,
|
| 208 |
+
cond_image_path_or_dir,
|
| 209 |
+
target_size,
|
| 210 |
+
frame_num,
|
| 211 |
+
motion_frames_num,
|
| 212 |
+
sampling_steps,
|
| 213 |
+
seed=None,
|
| 214 |
+
shift=5.0,
|
| 215 |
+
color_correction_strength=0.0,
|
| 216 |
+
use_face_crop=False,
|
| 217 |
+
):
|
| 218 |
+
self.cond_image_dict = get_cond_image_dict(cond_image_path_or_dir, use_face_crop)
|
| 219 |
+
|
| 220 |
+
self.frame_num = frame_num
|
| 221 |
+
self.motion_frames_num = motion_frames_num
|
| 222 |
+
self.color_correction_strength = color_correction_strength
|
| 223 |
+
|
| 224 |
+
self.target_h, self.target_w = target_size
|
| 225 |
+
self.lat_h, self.lat_w = self.target_h // self.config.vae_stride[1], self.target_w // self.config.vae_stride[2]
|
| 226 |
+
|
| 227 |
+
self.generator = torch.Generator(device=self.device).manual_seed(seed)
|
| 228 |
+
|
| 229 |
+
# prepare timesteps
|
| 230 |
+
if sampling_steps == 2:
|
| 231 |
+
timesteps = [1000, 500]
|
| 232 |
+
elif sampling_steps == 4:
|
| 233 |
+
timesteps = [1000, 750, 500, 250]
|
| 234 |
+
else:
|
| 235 |
+
timesteps = list(np.linspace(self.num_timesteps, 1, sampling_steps, dtype=np.float32))
|
| 236 |
+
|
| 237 |
+
timesteps.append(0.)
|
| 238 |
+
timesteps = [torch.tensor([t], device=self.device) for t in timesteps]
|
| 239 |
+
if self.use_timestep_transform:
|
| 240 |
+
timesteps = [timestep_transform(t, shift=shift, num_timesteps=self.num_timesteps) for t in timesteps]
|
| 241 |
+
self.timesteps = timesteps
|
| 242 |
+
|
| 243 |
+
self.cond_image_tensor_dict = {}
|
| 244 |
+
self.ref_img_latent_dict = {}
|
| 245 |
+
for i, (person_name, cond_image_pil) in enumerate(self.cond_image_dict.items()):
|
| 246 |
+
cond_image_tensor = resize_and_centercrop(cond_image_pil, (self.target_h, self.target_w)).to(self.device, dtype=self.param_dtype) # 1 C 1 H W
|
| 247 |
+
cond_image_tensor = (cond_image_tensor / 255 - 0.5) * 2
|
| 248 |
+
|
| 249 |
+
self.cond_image_tensor_dict[person_name] = cond_image_tensor
|
| 250 |
+
|
| 251 |
+
video_frames = cond_image_tensor.repeat(1, 1, self.frame_num, 1, 1)
|
| 252 |
+
self.ref_img_latent_dict[person_name] = self.vae.encode(video_frames) # (16, 9, 64, 64) / (128, 5, 16, 16)
|
| 253 |
+
if i == 0:
|
| 254 |
+
self.reset_person_name(person_name)
|
| 255 |
+
|
| 256 |
+
return
|
| 257 |
+
|
| 258 |
+
@torch.no_grad()
|
| 259 |
+
def reset_person_name(self, person_name=None):
|
| 260 |
+
if person_name is None or person_name not in self.cond_image_dict:
|
| 261 |
+
pass
|
| 262 |
+
else:
|
| 263 |
+
self.person_name = person_name
|
| 264 |
+
self.original_color_reference = self.cond_image_tensor_dict[self.person_name]
|
| 265 |
+
self.ref_img_latent = self.ref_img_latent_dict[self.person_name]
|
| 266 |
+
self.latent_motion_frames = self.ref_img_latent[:, :1].clone()
|
| 267 |
+
|
| 268 |
+
@torch.no_grad()
|
| 269 |
+
def preprocess_audio(self, speech_array, sr=16000, fps=25):
|
| 270 |
+
video_length = len(speech_array) * fps / sr
|
| 271 |
+
|
| 272 |
+
# wav2vec_feature_extractor
|
| 273 |
+
audio_feature = np.squeeze(
|
| 274 |
+
self.wav2vec_feature_extractor(speech_array, sampling_rate=sr).input_values
|
| 275 |
+
)
|
| 276 |
+
audio_feature = torch.from_numpy(audio_feature).float().to(device=self.device)
|
| 277 |
+
audio_feature = audio_feature.unsqueeze(0)
|
| 278 |
+
|
| 279 |
+
# audio encoder
|
| 280 |
+
with torch.no_grad():
|
| 281 |
+
embeddings = self.audio_encoder(audio_feature, seq_len=int(video_length), output_hidden_states=True)
|
| 282 |
+
|
| 283 |
+
if len(embeddings) == 0:
|
| 284 |
+
logger.error("Fail to extract audio embedding")
|
| 285 |
+
return None
|
| 286 |
+
|
| 287 |
+
audio_emb = torch.stack(embeddings.hidden_states[1:], dim=1).squeeze(0)
|
| 288 |
+
audio_emb = rearrange(audio_emb, "b s d -> s b d")
|
| 289 |
+
return audio_emb
|
| 290 |
+
|
| 291 |
+
@torch.no_grad()
|
| 292 |
+
def generate(self, audio_embedding):
|
| 293 |
+
# evaluation mode
|
| 294 |
+
with torch.no_grad():
|
| 295 |
+
|
| 296 |
+
# sample videos
|
| 297 |
+
noise = torch.randn(
|
| 298 |
+
self.config.out_dim,
|
| 299 |
+
(self.frame_num - 1) // self.config.vae_stride[0] + 1,
|
| 300 |
+
self.lat_h,
|
| 301 |
+
self.lat_w,
|
| 302 |
+
dtype=self.param_dtype,
|
| 303 |
+
device=self.device,
|
| 304 |
+
generator=self.generator)
|
| 305 |
+
|
| 306 |
+
for i in range(len(self.timesteps)-1):
|
| 307 |
+
torch.cuda.synchronize()
|
| 308 |
+
start_time = time.time()
|
| 309 |
+
|
| 310 |
+
noise[:, :self.latent_motion_frames.shape[1]] = self.latent_motion_frames
|
| 311 |
+
|
| 312 |
+
flow_pred = self.model(
|
| 313 |
+
x=noise.unsqueeze(0),
|
| 314 |
+
timestep=self.timesteps[i],
|
| 315 |
+
context=audio_embedding,
|
| 316 |
+
y=self.ref_img_latent.unsqueeze(0),
|
| 317 |
+
)[0]
|
| 318 |
+
|
| 319 |
+
if self.model_type == "pretrained":
|
| 320 |
+
flow_pred_drop_audio = self.model(
|
| 321 |
+
x=noise.unsqueeze(0),
|
| 322 |
+
timestep=self.timesteps[i],
|
| 323 |
+
context=torch.zeros_like(audio_embedding),
|
| 324 |
+
y=self.ref_img_latent.unsqueeze(0),
|
| 325 |
+
)[0]
|
| 326 |
+
flow_pred = flow_pred_drop_audio + self.audio_guide_scale * (flow_pred - flow_pred_drop_audio)
|
| 327 |
+
|
| 328 |
+
# update latent
|
| 329 |
+
dt = self.timesteps[i] - self.timesteps[i + 1]
|
| 330 |
+
dt = (dt / self.num_timesteps).to(self.param_dtype)
|
| 331 |
+
noise = noise - flow_pred * dt[:, None, None, None]
|
| 332 |
+
|
| 333 |
+
else:
|
| 334 |
+
# update latent
|
| 335 |
+
t_i = (self.timesteps[i][:, None, None, None] / self.num_timesteps).to(self.param_dtype)
|
| 336 |
+
t_i_1 = (self.timesteps[i+1][:, None, None, None] / self.num_timesteps).to(self.param_dtype)
|
| 337 |
+
x_0 = noise - flow_pred * t_i
|
| 338 |
+
|
| 339 |
+
noise = (1 - t_i_1) * x_0 + t_i_1 * torch.randn(x_0.size(), dtype=x_0.dtype, device=self.device, generator=self.generator)
|
| 340 |
+
|
| 341 |
+
torch.cuda.synchronize()
|
| 342 |
+
end_time = time.time()
|
| 343 |
+
if self.rank == 0:
|
| 344 |
+
print(f'[generate] model denoise per step: {end_time - start_time}s')
|
| 345 |
+
|
| 346 |
+
noise[:, :self.latent_motion_frames.shape[1]] = self.latent_motion_frames
|
| 347 |
+
|
| 348 |
+
torch.cuda.synchronize()
|
| 349 |
+
start_decode_time = time.time()
|
| 350 |
+
|
| 351 |
+
videos = self.vae.decode(noise)
|
| 352 |
+
|
| 353 |
+
torch.cuda.synchronize()
|
| 354 |
+
end_decode_time = time.time()
|
| 355 |
+
if self.rank == 0:
|
| 356 |
+
print(f'[generate] decode video frames: {end_decode_time - start_decode_time}s')
|
| 357 |
+
|
| 358 |
+
torch.cuda.synchronize()
|
| 359 |
+
start_color_correction_time = time.time()
|
| 360 |
+
if self.color_correction_strength > 0.0:
|
| 361 |
+
videos = match_and_blend_colors_torch(videos, self.original_color_reference, self.color_correction_strength)
|
| 362 |
+
|
| 363 |
+
cond_frame = videos[:, :, -self.motion_frames_num:].to(self.device)
|
| 364 |
+
torch.cuda.synchronize()
|
| 365 |
+
end_color_correction_time = time.time()
|
| 366 |
+
if self.rank == 0:
|
| 367 |
+
print(f'[generate] color correction: {end_color_correction_time - start_color_correction_time}s')
|
| 368 |
+
|
| 369 |
+
torch.cuda.synchronize()
|
| 370 |
+
start_encode_time = time.time()
|
| 371 |
+
self.latent_motion_frames = self.vae.encode(cond_frame)
|
| 372 |
+
torch.cuda.synchronize()
|
| 373 |
+
end_encode_time = time.time()
|
| 374 |
+
if self.rank == 0:
|
| 375 |
+
print(f'[generate] encode motion frames: {end_encode_time - start_encode_time}s')
|
| 376 |
+
|
| 377 |
+
gen_video_samples = videos #[:, :, self.motion_frames_num:]
|
| 378 |
+
|
| 379 |
+
return gen_video_samples[0].to(torch.float32)
|
flash_head/utils/cpu_face_handler.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import mediapipe as mp
|
| 2 |
+
import numpy as np
|
| 3 |
+
from typing import Tuple, List
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class CPUFaceHandler:
|
| 7 |
+
"""Handler for CPU-based face detection using MediaPipe.
|
| 8 |
+
(2 ms/frame)
|
| 9 |
+
This handler provides a simple interface for face detection using MediaPipe's
|
| 10 |
+
face detection model. It's optimized for CPU usage and provides basic face
|
| 11 |
+
detection functionality.
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
def __init__(self, model_selection: int = 1, min_detection_confidence: float = 0.0):
|
| 15 |
+
"""Initialize the face detection handler."""
|
| 16 |
+
self.detector = mp.solutions.face_detection.FaceDetection(
|
| 17 |
+
model_selection=model_selection,
|
| 18 |
+
min_detection_confidence=min_detection_confidence,
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
def detect(self, image: np.ndarray) -> Tuple[int, List[int]]:
|
| 22 |
+
"""Detect faces in the given image.
|
| 23 |
+
|
| 24 |
+
Args:
|
| 25 |
+
image (np.ndarray): RGB image array.
|
| 26 |
+
|
| 27 |
+
Returns:
|
| 28 |
+
Tuple[int, List[int]]: A tuple containing:
|
| 29 |
+
- Number of faces detected (int)
|
| 30 |
+
- Bounding box coordinates [x1, y1, x2, y2] if exactly one face is detected,
|
| 31 |
+
empty list otherwise
|
| 32 |
+
"""
|
| 33 |
+
bboxs, scores = [], []
|
| 34 |
+
results = self.detector.process(image)
|
| 35 |
+
detection_result = results.detections
|
| 36 |
+
if detection_result is None:
|
| 37 |
+
return bboxs, scores
|
| 38 |
+
for detection in detection_result:
|
| 39 |
+
bboxC = detection.location_data.relative_bounding_box
|
| 40 |
+
x, y, w, h = bboxC.xmin, bboxC.ymin, bboxC.width, bboxC.height
|
| 41 |
+
x1, y1, x2, y2 = x, y, x + w, y + h
|
| 42 |
+
bboxs.append([x1, y1, x2, y2])
|
| 43 |
+
scores.append(detection.score[0])
|
| 44 |
+
return bboxs, scores
|
| 45 |
+
|
| 46 |
+
def __call__(self, image: np.ndarray) -> Tuple[int, List[int]]:
|
| 47 |
+
"""Make the handler callable.
|
| 48 |
+
|
| 49 |
+
Args:
|
| 50 |
+
image (np.ndarray): RGB image array.
|
| 51 |
+
|
| 52 |
+
Returns:
|
| 53 |
+
Tuple[int, List[int]]: Same as detect() method.
|
| 54 |
+
"""
|
| 55 |
+
return self.detect(image)
|
flash_head/utils/facecrop.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
人脸裁剪处理脚本
|
| 4 |
+
从单张图像中检测人脸,裁剪并调整大小到指定尺寸
|
| 5 |
+
"""
|
| 6 |
+
import os
|
| 7 |
+
from PIL import Image
|
| 8 |
+
import numpy as np
|
| 9 |
+
|
| 10 |
+
from flash_head.utils.cpu_face_handler import CPUFaceHandler
|
| 11 |
+
|
| 12 |
+
def get_scaled_bbox(
|
| 13 |
+
bbox, img_w, img_h, ratio: float = 1.0, face_image: Image.Image = None
|
| 14 |
+
):
|
| 15 |
+
"""
|
| 16 |
+
根据人脸边界框计算缩放后的裁剪区域
|
| 17 |
+
|
| 18 |
+
Args:
|
| 19 |
+
bbox: 人脸边界框 [x1, y1, x2, y2]
|
| 20 |
+
img_w: 图像宽度
|
| 21 |
+
img_h: 图像高度
|
| 22 |
+
ratio: 缩放比例,数值越大,人脸在画面中的比例越小(周围留白越多)
|
| 23 |
+
face_image: PIL Image 对象
|
| 24 |
+
|
| 25 |
+
Returns:
|
| 26 |
+
裁剪后的人脸图像
|
| 27 |
+
"""
|
| 28 |
+
x1, y1, x2, y2 = bbox
|
| 29 |
+
|
| 30 |
+
# Calculate center point
|
| 31 |
+
center_x = (x1 + x2) / 2
|
| 32 |
+
center_y = (y1 + y2) / 2
|
| 33 |
+
|
| 34 |
+
# Calculate width and height
|
| 35 |
+
width = x2 - x1
|
| 36 |
+
|
| 37 |
+
# Scale width and height
|
| 38 |
+
new_width = width * ratio
|
| 39 |
+
new_height = new_width
|
| 40 |
+
|
| 41 |
+
# tile pix
|
| 42 |
+
dis_x_left = new_width * 0.5
|
| 43 |
+
dis_x_right = new_width - dis_x_left # 0.5new_width
|
| 44 |
+
dis_y_up = new_height * 0.55
|
| 45 |
+
dis_y_down = new_height - dis_y_up # 0.45new_height
|
| 46 |
+
|
| 47 |
+
# Calculate new coordinates
|
| 48 |
+
new_x1 = int(max(0, center_x - dis_x_left))
|
| 49 |
+
new_y1 = int(max(0, center_y - dis_y_up))
|
| 50 |
+
new_x2 = int(min(img_w, center_x + dis_x_right))
|
| 51 |
+
new_y2 = int(min(img_h, center_y + dis_y_down))
|
| 52 |
+
scaled_bbox = [new_x1, new_y1, new_x2, new_y2]
|
| 53 |
+
crop_face = face_image.crop(scaled_bbox)
|
| 54 |
+
return crop_face
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def process_image(
|
| 58 |
+
input_path,
|
| 59 |
+
face_ratio=2.0,
|
| 60 |
+
target_size=(512, 512),
|
| 61 |
+
):
|
| 62 |
+
"""
|
| 63 |
+
处理单张图像,进行人脸检测和裁剪
|
| 64 |
+
|
| 65 |
+
Args:
|
| 66 |
+
input_path: 输入图像路径
|
| 67 |
+
face_ratio: 人脸缩放比例,建议范围:1.5-3.0,默认2.0
|
| 68 |
+
target_size: 输出图像尺寸,默认(512, 512)
|
| 69 |
+
|
| 70 |
+
Returns:
|
| 71 |
+
imgae: 处理后的图像
|
| 72 |
+
"""
|
| 73 |
+
# 初始化人脸检测器
|
| 74 |
+
face_detector = CPUFaceHandler()
|
| 75 |
+
|
| 76 |
+
# 验证输入文件
|
| 77 |
+
if not os.path.isfile(input_path):
|
| 78 |
+
raise ValueError(f"File not found: {input_path}")
|
| 79 |
+
|
| 80 |
+
try:
|
| 81 |
+
# 读取图像
|
| 82 |
+
image = Image.open(input_path)
|
| 83 |
+
image = image.convert("RGB")
|
| 84 |
+
image_rgb = np.array(image)
|
| 85 |
+
img_h, img_w = image_rgb.shape[:2]
|
| 86 |
+
|
| 87 |
+
# 检测人脸
|
| 88 |
+
boxes, scores = face_detector(image_rgb)
|
| 89 |
+
|
| 90 |
+
if len(boxes) == 0:
|
| 91 |
+
raise ValueError("No face detected")
|
| 92 |
+
|
| 93 |
+
# 转换边界框坐标(从相对坐标转为绝对坐标)
|
| 94 |
+
boxes_abs = [
|
| 95 |
+
boxes[0][0] * img_w,
|
| 96 |
+
boxes[0][1] * img_h,
|
| 97 |
+
boxes[0][2] * img_w,
|
| 98 |
+
boxes[0][3] * img_h
|
| 99 |
+
]
|
| 100 |
+
|
| 101 |
+
# 裁剪人脸
|
| 102 |
+
crop_face = get_scaled_bbox(boxes_abs, img_w, img_h, face_ratio, image)
|
| 103 |
+
|
| 104 |
+
# 调整大小
|
| 105 |
+
crop_face = crop_face.resize(target_size)
|
| 106 |
+
|
| 107 |
+
return crop_face
|
| 108 |
+
|
| 109 |
+
except Exception as e:
|
| 110 |
+
raise ValueError(f"Error processing {input_path}: {e}")
|
flash_head/utils/utils.py
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import torch.nn as nn
|
| 3 |
+
import numpy as np
|
| 4 |
+
import math
|
| 5 |
+
from PIL import Image
|
| 6 |
+
import torchvision.transforms as transforms
|
| 7 |
+
import torch.nn as nn
|
| 8 |
+
import pyloudnorm as pyln
|
| 9 |
+
|
| 10 |
+
def rgb_to_lab_torch(rgb: torch.Tensor) -> torch.Tensor:
|
| 11 |
+
"""
|
| 12 |
+
PyTorch GPU版本:RGB转Lab颜色空间(输入范围[0,1],张量形状任意,最后一维为通道数)
|
| 13 |
+
参考CIE 1931标准转换公式
|
| 14 |
+
"""
|
| 15 |
+
# 转换为线性RGB(sRGB伽马校正逆过程)
|
| 16 |
+
linear_rgb = torch.where(
|
| 17 |
+
rgb > 0.04045,
|
| 18 |
+
((rgb + 0.055) / 1.055) ** 2.4,
|
| 19 |
+
rgb / 12.92
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
# 线性RGB转XYZ(使用sRGB标准白点D65)
|
| 23 |
+
xyz_from_rgb = torch.tensor([
|
| 24 |
+
[0.4124564, 0.3575761, 0.1804375],
|
| 25 |
+
[0.2126729, 0.7151522, 0.0721750],
|
| 26 |
+
[0.0193339, 0.1191920, 0.9503041]
|
| 27 |
+
], dtype=rgb.dtype, device=rgb.device)
|
| 28 |
+
|
| 29 |
+
# 维度适配:确保输入为(B, ..., C),矩阵乘法后保持空间维度
|
| 30 |
+
shape = linear_rgb.shape
|
| 31 |
+
linear_rgb_flat = linear_rgb.reshape(-1, 3) # (N, 3),N=B*T*H*W
|
| 32 |
+
xyz_flat = linear_rgb_flat @ xyz_from_rgb.T # (N, 3)
|
| 33 |
+
xyz = xyz_flat.reshape(shape) # 恢复原形状
|
| 34 |
+
|
| 35 |
+
# XYZ转Lab(使用D65白点参数)
|
| 36 |
+
xyz_ref = torch.tensor([0.95047, 1.0, 1.08883], dtype=rgb.dtype, device=rgb.device)
|
| 37 |
+
xyz_normalized = xyz / xyz_ref[None, None, None, None, :] # 广播适配(B, C, T, H, W)
|
| 38 |
+
|
| 39 |
+
# 应用Lab转换公式
|
| 40 |
+
epsilon = 0.008856
|
| 41 |
+
kappa = 903.3
|
| 42 |
+
xyz_normalized = torch.clamp(xyz_normalized, 1e-8, 1.0) # 避免log(0)
|
| 43 |
+
|
| 44 |
+
f_xyz = torch.where(
|
| 45 |
+
xyz_normalized > epsilon,
|
| 46 |
+
xyz_normalized ** (1/3),
|
| 47 |
+
(kappa * xyz_normalized + 16) / 116
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
L = 116 * f_xyz[..., 1] - 16 # Y通道对应亮度
|
| 51 |
+
a = 500 * (f_xyz[..., 0] - f_xyz[..., 1]) # X-Y对应红绿
|
| 52 |
+
b = 200 * (f_xyz[..., 1] - f_xyz[..., 2]) # Y-Z对应蓝黄
|
| 53 |
+
|
| 54 |
+
lab = torch.stack([L, a, b], dim=-1) # 最后一维拼接为Lab通道
|
| 55 |
+
return lab
|
| 56 |
+
|
| 57 |
+
def lab_to_rgb_torch(lab: torch.Tensor) -> torch.Tensor:
|
| 58 |
+
"""
|
| 59 |
+
PyTorch GPU版本:Lab转RGB颜色空间(输出范围[0,1],张量形状任意,最后一维为通道数)
|
| 60 |
+
"""
|
| 61 |
+
# Lab分离通道
|
| 62 |
+
L = lab[..., 0]
|
| 63 |
+
a = lab[..., 1]
|
| 64 |
+
b = lab[..., 2]
|
| 65 |
+
|
| 66 |
+
# Lab转XYZ
|
| 67 |
+
f_y = (L + 16) / 116
|
| 68 |
+
f_x = (a / 500) + f_y
|
| 69 |
+
f_z = f_y - (b / 200)
|
| 70 |
+
|
| 71 |
+
epsilon = 0.008856
|
| 72 |
+
kappa = 903.3
|
| 73 |
+
|
| 74 |
+
x = torch.where(f_x ** 3 > epsilon, f_x ** 3, (116 * f_x - 16) / kappa)
|
| 75 |
+
y = torch.where(L > kappa * epsilon, ((L + 16) / 116) ** 3, L / kappa)
|
| 76 |
+
z = torch.where(f_z ** 3 > epsilon, f_z ** 3, (116 * f_z - 16) / kappa)
|
| 77 |
+
|
| 78 |
+
# 乘以D65白点参数
|
| 79 |
+
xyz_ref = torch.tensor([0.95047, 1.0, 1.08883], dtype=lab.dtype, device=lab.device)
|
| 80 |
+
xyz = torch.stack([x, y, z], dim=-1) * xyz_ref[None, None, None, None, :]
|
| 81 |
+
|
| 82 |
+
# XYZ转线性RGB
|
| 83 |
+
rgb_from_xyz = torch.tensor([
|
| 84 |
+
[3.2404542, -1.5371385, -0.4985314],
|
| 85 |
+
[-0.9692660, 1.8760108, 0.0415560],
|
| 86 |
+
[0.0556434, -0.2040259, 1.0572252]
|
| 87 |
+
], dtype=lab.dtype, device=lab.device)
|
| 88 |
+
|
| 89 |
+
# 维度适配:矩阵乘法
|
| 90 |
+
shape = xyz.shape
|
| 91 |
+
xyz_flat = xyz.reshape(-1, 3) # (N, 3)
|
| 92 |
+
linear_rgb_flat = xyz_flat @ rgb_from_xyz.T # (N, 3)
|
| 93 |
+
linear_rgb = linear_rgb_flat.reshape(shape) # 恢复原形状
|
| 94 |
+
|
| 95 |
+
# 线性RGB转sRGB(伽马校正)
|
| 96 |
+
rgb = torch.where(
|
| 97 |
+
linear_rgb > 0.0031308,
|
| 98 |
+
1.055 * (linear_rgb ** (1/2.4)) - 0.055,
|
| 99 |
+
12.92 * linear_rgb
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
# 确保输出在[0,1]范围内
|
| 103 |
+
rgb = torch.clamp(rgb, 0.0, 1.0)
|
| 104 |
+
return rgb
|
| 105 |
+
|
| 106 |
+
def match_and_blend_colors_torch(
|
| 107 |
+
source_chunk: torch.Tensor,
|
| 108 |
+
reference_image: torch.Tensor,
|
| 109 |
+
strength: float
|
| 110 |
+
) -> torch.Tensor:
|
| 111 |
+
"""
|
| 112 |
+
全GPU批量运算版本:将视频chunk的颜色匹配到参考图像并混合(支持B>1、T帧并行)
|
| 113 |
+
|
| 114 |
+
Args:
|
| 115 |
+
source_chunk (torch.Tensor): 视频chunk (B, C, T, H, W),范围[-1, 1]
|
| 116 |
+
reference_image (torch.Tensor): 参考图像 (B, C, 1, H, W),范围[-1, 1](B需与source_chunk一致)
|
| 117 |
+
strength (float): 颜色校正强度 (0.0-1.0),0.0无校正,1.0完全校正
|
| 118 |
+
|
| 119 |
+
Returns:
|
| 120 |
+
torch.Tensor: 颜色校正后的视频chunk (B, C, T, H, W),范围[-1, 1]
|
| 121 |
+
"""
|
| 122 |
+
# 强度为0直接返回原图
|
| 123 |
+
if strength <= 0.0:
|
| 124 |
+
return source_chunk.clone()
|
| 125 |
+
|
| 126 |
+
# 验证强度范围
|
| 127 |
+
if not 0.0 <= strength <= 1.0:
|
| 128 |
+
raise ValueError(f"Strength必须在0.0-1.0之间,当前值:{strength}")
|
| 129 |
+
|
| 130 |
+
# 验证输入形状(确保B一致,参考图T=1)
|
| 131 |
+
B, C, T, H, W = source_chunk.shape
|
| 132 |
+
assert reference_image.shape == (B, C, 1, H, W), \
|
| 133 |
+
f"参考图像形状需为(B, C, 1, H, W),当前为{reference_image.shape}"
|
| 134 |
+
assert C == 3, f"仅支持3通道RGB图像,当前通道数:{C}"
|
| 135 |
+
|
| 136 |
+
# 保持设备和数据类型一致
|
| 137 |
+
device = source_chunk.device
|
| 138 |
+
dtype = source_chunk.dtype
|
| 139 |
+
reference_image = reference_image.to(device=device, dtype=dtype)
|
| 140 |
+
|
| 141 |
+
# 1. 从[-1,1]转换到[0,1](GPU上直接运算)
|
| 142 |
+
source_01 = (source_chunk + 1.0) / 2.0
|
| 143 |
+
ref_01 = (reference_image + 1.0) / 2.0
|
| 144 |
+
|
| 145 |
+
# 2. 调整维度顺序:(B, C, T, H, W) → (B, T, H, W, C)(适配颜色空间转换)
|
| 146 |
+
# 参考图:(B, C, 1, H, W) → (B, 1, H, W, C)
|
| 147 |
+
source_permuted = source_01.permute(0, 2, 3, 4, 1) # 通道移到最后一维
|
| 148 |
+
ref_permuted = ref_01.permute(0, 2, 3, 4, 1)
|
| 149 |
+
|
| 150 |
+
# 3. RGB转Lab(批量处理所有帧)
|
| 151 |
+
source_lab = rgb_to_lab_torch(source_permuted)
|
| 152 |
+
ref_lab = rgb_to_lab_torch(ref_permuted) # (B, 1, H, W, 3)
|
| 153 |
+
|
| 154 |
+
# 4. 批量颜色迁移:匹配L/a/b通道的均值和标准差(核心逻辑)
|
| 155 |
+
# 计算参考图各通道的均值和标准差(对H、W维度求统计,保持B维度)
|
| 156 |
+
ref_mean = ref_lab.mean(dim=[2, 3], keepdim=True) # (B, 1, 1, 1, 3)
|
| 157 |
+
ref_std = ref_lab.std(dim=[2, 3], keepdim=True, unbiased=False) # (B, 1, 1, 1, 3)
|
| 158 |
+
|
| 159 |
+
# 计算源视频各通道的均值和标准差(对H、W维度求统计,保持B、T维度)
|
| 160 |
+
source_mean = source_lab.mean(dim=[2, 3], keepdim=True) # (B, T, 1, 1, 3)
|
| 161 |
+
source_std = source_lab.std(dim=[2, 3], keepdim=True, unbiased=False) # (B, T, 1, 1, 3)
|
| 162 |
+
|
| 163 |
+
# 避免标准差为0的除法错误(用1.0替代0)
|
| 164 |
+
source_std_safe = torch.where(source_std < 1e-8, torch.ones_like(source_std), source_std)
|
| 165 |
+
|
| 166 |
+
# 颜色迁移公式:(源 - 源均值) * (参考标准差/源标准差) + 参考均值
|
| 167 |
+
corrected_lab = (source_lab - source_mean) * (ref_std / source_std_safe) + ref_mean
|
| 168 |
+
|
| 169 |
+
# 5. Lab转RGB(批量转换所有校正后的帧)
|
| 170 |
+
corrected_rgb_01 = lab_to_rgb_torch(corrected_lab)
|
| 171 |
+
|
| 172 |
+
# 6. 批量混合原始帧和校正帧(按强度加权)
|
| 173 |
+
blended_rgb_01 = (1 - strength) * source_permuted + strength * corrected_rgb_01
|
| 174 |
+
|
| 175 |
+
# 7. 还原维度顺序和数值范围:(B, T, H, W, C) → (B, C, T, H, W),范围[0,1]→[-1,1]
|
| 176 |
+
blended_rgb_01 = blended_rgb_01.permute(0, 4, 1, 2, 3) # 通道移回第二维
|
| 177 |
+
blended_rgb_minus1_1 = (blended_rgb_01 * 2.0) - 1.0
|
| 178 |
+
|
| 179 |
+
# 8. 确保输出格式正确(连续内存布局)
|
| 180 |
+
output = blended_rgb_minus1_1.contiguous().to(device=device, dtype=dtype)
|
| 181 |
+
|
| 182 |
+
return output
|
| 183 |
+
|
| 184 |
+
def resize_and_centercrop(cond_image, target_size):
|
| 185 |
+
"""
|
| 186 |
+
Resize image or tensor to the target size without padding.
|
| 187 |
+
"""
|
| 188 |
+
|
| 189 |
+
# Get the original size
|
| 190 |
+
if isinstance(cond_image, torch.Tensor):
|
| 191 |
+
_, orig_h, orig_w = cond_image.shape
|
| 192 |
+
else:
|
| 193 |
+
orig_h, orig_w = cond_image.height, cond_image.width
|
| 194 |
+
|
| 195 |
+
target_h, target_w = target_size
|
| 196 |
+
|
| 197 |
+
# Calculate the scaling factor for resizing
|
| 198 |
+
scale_h = target_h / orig_h
|
| 199 |
+
scale_w = target_w / orig_w
|
| 200 |
+
|
| 201 |
+
# Compute the final size
|
| 202 |
+
scale = max(scale_h, scale_w)
|
| 203 |
+
final_h = math.ceil(scale * orig_h)
|
| 204 |
+
final_w = math.ceil(scale * orig_w)
|
| 205 |
+
|
| 206 |
+
# Resize
|
| 207 |
+
if isinstance(cond_image, torch.Tensor):
|
| 208 |
+
if len(cond_image.shape) == 3:
|
| 209 |
+
cond_image = cond_image[None]
|
| 210 |
+
resized_tensor = nn.functional.interpolate(cond_image, size=(final_h, final_w), mode='nearest').contiguous()
|
| 211 |
+
# crop
|
| 212 |
+
cropped_tensor = transforms.functional.center_crop(resized_tensor, target_size)
|
| 213 |
+
cropped_tensor = cropped_tensor.squeeze(0)
|
| 214 |
+
else:
|
| 215 |
+
resized_image = cond_image.resize((final_w, final_h), resample=Image.BILINEAR)
|
| 216 |
+
resized_image = np.array(resized_image)
|
| 217 |
+
# tensor and crop
|
| 218 |
+
resized_tensor = torch.from_numpy(resized_image)[None, ...].permute(0, 3, 1, 2).contiguous()
|
| 219 |
+
cropped_tensor = transforms.functional.center_crop(resized_tensor, target_size)
|
| 220 |
+
cropped_tensor = cropped_tensor[:, :, None, :, :]
|
| 221 |
+
|
| 222 |
+
return cropped_tensor
|
flash_head/wan/modules/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .vae import WanVAE
|
| 2 |
+
from .tae import TAEHV
|
| 3 |
+
|
| 4 |
+
__all__ = [
|
| 5 |
+
'WanVAE',
|
| 6 |
+
'TAEHV',
|
| 7 |
+
]
|
flash_head/wan/modules/tae.py
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 os
|
| 8 |
+
from collections import namedtuple
|
| 9 |
+
|
| 10 |
+
import torch
|
| 11 |
+
import torch.nn as nn
|
| 12 |
+
import torch.nn.functional as F
|
| 13 |
+
from safetensors.torch import load_file
|
| 14 |
+
from tqdm.auto import tqdm
|
| 15 |
+
|
| 16 |
+
DecoderResult = namedtuple("DecoderResult", ("frame", "memory"))
|
| 17 |
+
TWorkItem = namedtuple("TWorkItem", ("input_tensor", "block_index"))
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def conv(n_in, n_out, **kwargs):
|
| 21 |
+
return nn.Conv2d(n_in, n_out, 3, padding=1, **kwargs)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class Clamp(nn.Module):
|
| 25 |
+
def forward(self, x):
|
| 26 |
+
return torch.tanh(x / 3) * 3
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class MemBlock(nn.Module):
|
| 30 |
+
def __init__(self, n_in, n_out, act_func):
|
| 31 |
+
super().__init__()
|
| 32 |
+
self.conv = nn.Sequential(conv(n_in * 2, n_out), act_func, conv(n_out, n_out), act_func, conv(n_out, n_out))
|
| 33 |
+
self.skip = nn.Conv2d(n_in, n_out, 1, bias=False) if n_in != n_out else nn.Identity()
|
| 34 |
+
self.act = act_func
|
| 35 |
+
|
| 36 |
+
def forward(self, x, past):
|
| 37 |
+
return self.act(self.conv(torch.cat([x, past], 1)) + self.skip(x))
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class TPool(nn.Module):
|
| 41 |
+
def __init__(self, n_f, stride):
|
| 42 |
+
super().__init__()
|
| 43 |
+
self.stride = stride
|
| 44 |
+
self.conv = nn.Conv2d(n_f * stride, n_f, 1, bias=False)
|
| 45 |
+
|
| 46 |
+
def forward(self, x):
|
| 47 |
+
_NT, C, H, W = x.shape
|
| 48 |
+
return self.conv(x.reshape(-1, self.stride * C, H, W))
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
class TGrow(nn.Module):
|
| 52 |
+
def __init__(self, n_f, stride):
|
| 53 |
+
super().__init__()
|
| 54 |
+
self.stride = stride
|
| 55 |
+
self.conv = nn.Conv2d(n_f, n_f * stride, 1, bias=False)
|
| 56 |
+
|
| 57 |
+
def forward(self, x):
|
| 58 |
+
_NT, C, H, W = x.shape
|
| 59 |
+
x = self.conv(x)
|
| 60 |
+
return x.reshape(-1, C, H, W)
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def apply_model_with_memblocks(model, x, parallel, show_progress_bar):
|
| 64 |
+
"""
|
| 65 |
+
Apply a sequential model with memblocks to the given input.
|
| 66 |
+
Args:
|
| 67 |
+
- model: nn.Sequential of blocks to apply
|
| 68 |
+
- x: input data, of dimensions NTCHW
|
| 69 |
+
- parallel: if True, parallelize over timesteps (fast but uses O(T) memory)
|
| 70 |
+
if False, each timestep will be processed sequentially (slow but uses O(1) memory)
|
| 71 |
+
- show_progress_bar: if True, enables tqdm progressbar display
|
| 72 |
+
|
| 73 |
+
Returns NTCHW tensor of output data.
|
| 74 |
+
"""
|
| 75 |
+
assert x.ndim == 5, f"TAEHV operates on NTCHW tensors, but got {x.ndim}-dim tensor"
|
| 76 |
+
N, T, C, H, W = x.shape
|
| 77 |
+
if parallel:
|
| 78 |
+
x = x.reshape(N * T, C, H, W)
|
| 79 |
+
# parallel over input timesteps, iterate over blocks
|
| 80 |
+
for b in tqdm(model, disable=not show_progress_bar):
|
| 81 |
+
if isinstance(b, MemBlock):
|
| 82 |
+
NT, C, H, W = x.shape
|
| 83 |
+
T = NT // N
|
| 84 |
+
_x = x.reshape(N, T, C, H, W)
|
| 85 |
+
mem = F.pad(_x, (0, 0, 0, 0, 0, 0, 1, 0), value=0)[:, :T].reshape(x.shape)
|
| 86 |
+
x = b(x, mem)
|
| 87 |
+
else:
|
| 88 |
+
x = b(x)
|
| 89 |
+
NT, C, H, W = x.shape
|
| 90 |
+
T = NT // N
|
| 91 |
+
x = x.view(N, T, C, H, W)
|
| 92 |
+
else:
|
| 93 |
+
# TODO(oboerbohan): at least on macos this still gradually uses more memory during decode...
|
| 94 |
+
# need to fix :(
|
| 95 |
+
out = []
|
| 96 |
+
# iterate over input timesteps and also iterate over blocks.
|
| 97 |
+
# because of the cursed TPool/TGrow blocks, this is not a nested loop,
|
| 98 |
+
# it's actually a ***graph traversal*** problem! so let's make a queue
|
| 99 |
+
work_queue = [TWorkItem(xt, 0) for t, xt in enumerate(x.reshape(N, T * C, H, W).chunk(T, dim=1))]
|
| 100 |
+
# in addition to manually managing our queue, we also need to manually manage our progressbar.
|
| 101 |
+
# we'll update it for every source node that we consume.
|
| 102 |
+
progress_bar = tqdm(range(T), disable=not show_progress_bar)
|
| 103 |
+
# we'll also need a separate addressable memory per node as well
|
| 104 |
+
mem = [None] * len(model)
|
| 105 |
+
while work_queue:
|
| 106 |
+
xt, i = work_queue.pop(0)
|
| 107 |
+
if i == 0:
|
| 108 |
+
# new source node consumed
|
| 109 |
+
progress_bar.update(1)
|
| 110 |
+
if i == len(model):
|
| 111 |
+
# reached end of the graph, append result to output list
|
| 112 |
+
out.append(xt)
|
| 113 |
+
else:
|
| 114 |
+
# fetch the block to process
|
| 115 |
+
b = model[i]
|
| 116 |
+
if isinstance(b, MemBlock):
|
| 117 |
+
# mem blocks are simple since we're visiting the graph in causal order
|
| 118 |
+
if mem[i] is None:
|
| 119 |
+
xt_new = b(xt, xt * 0)
|
| 120 |
+
mem[i] = xt
|
| 121 |
+
else:
|
| 122 |
+
xt_new = b(xt, mem[i])
|
| 123 |
+
mem[i].copy_(xt) # inplace might reduce mysterious pytorch memory allocations? doesn't help though
|
| 124 |
+
# add successor to work queue
|
| 125 |
+
work_queue.insert(0, TWorkItem(xt_new, i + 1))
|
| 126 |
+
elif isinstance(b, TPool):
|
| 127 |
+
# pool blocks are miserable
|
| 128 |
+
if mem[i] is None:
|
| 129 |
+
mem[i] = [] # pool memory is itself a queue of inputs to pool
|
| 130 |
+
mem[i].append(xt)
|
| 131 |
+
if len(mem[i]) > b.stride:
|
| 132 |
+
# pool mem is in invalid state, we should have pooled before this
|
| 133 |
+
raise ValueError("???")
|
| 134 |
+
elif len(mem[i]) < b.stride:
|
| 135 |
+
# pool mem is not yet full, go back to processing the work queue
|
| 136 |
+
pass
|
| 137 |
+
else:
|
| 138 |
+
# pool mem is ready, run the pool block
|
| 139 |
+
N, C, H, W = xt.shape
|
| 140 |
+
xt = b(torch.cat(mem[i], 1).view(N * b.stride, C, H, W))
|
| 141 |
+
# reset the pool mem
|
| 142 |
+
mem[i] = []
|
| 143 |
+
# add successor to work queue
|
| 144 |
+
work_queue.insert(0, TWorkItem(xt, i + 1))
|
| 145 |
+
elif isinstance(b, TGrow):
|
| 146 |
+
xt = b(xt)
|
| 147 |
+
NT, C, H, W = xt.shape
|
| 148 |
+
# each tgrow has multiple successor nodes
|
| 149 |
+
for xt_next in reversed(xt.view(N, b.stride * C, H, W).chunk(b.stride, 1)):
|
| 150 |
+
# add successor to work queue
|
| 151 |
+
work_queue.insert(0, TWorkItem(xt_next, i + 1))
|
| 152 |
+
else:
|
| 153 |
+
# normal block with no funny business
|
| 154 |
+
xt = b(xt)
|
| 155 |
+
# add successor to work queue
|
| 156 |
+
work_queue.insert(0, TWorkItem(xt, i + 1))
|
| 157 |
+
progress_bar.close()
|
| 158 |
+
x = torch.stack(out, 1)
|
| 159 |
+
return x
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
class TAEHV(nn.Module):
|
| 163 |
+
def __init__(self, checkpoint_path="taehv.pth", decoder_time_upscale=(True, True), decoder_space_upscale=(True, True, True), patch_size=1, latent_channels=16, model_type="wan21"):
|
| 164 |
+
"""Initialize pretrained TAEHV from the given checkpoint.
|
| 165 |
+
|
| 166 |
+
Arg:
|
| 167 |
+
checkpoint_path: path to weight file to load. taehv.pth for Hunyuan, taew2_1.pth for Wan 2.1.
|
| 168 |
+
decoder_time_upscale: whether temporal upsampling is enabled for each block. upsampling can be disabled for a cheaper preview.
|
| 169 |
+
decoder_space_upscale: whether spatial upsampling is enabled for each block. upsampling can be disabled for a cheaper preview.
|
| 170 |
+
patch_size: input/output pixelshuffle patch-size for this model.
|
| 171 |
+
latent_channels: number of latent channels (z dim) for this model.
|
| 172 |
+
"""
|
| 173 |
+
super().__init__()
|
| 174 |
+
self.patch_size = patch_size
|
| 175 |
+
self.latent_channels = latent_channels
|
| 176 |
+
self.image_channels = 3
|
| 177 |
+
self.is_cogvideox = checkpoint_path is not None and "taecvx" in checkpoint_path
|
| 178 |
+
# if checkpoint_path is not None and "taew2_2" in checkpoint_path:
|
| 179 |
+
# self.patch_size, self.latent_channels = 2, 48
|
| 180 |
+
self.model_type = model_type
|
| 181 |
+
if model_type == "wan22":
|
| 182 |
+
self.patch_size, self.latent_channels = 2, 48
|
| 183 |
+
if model_type == "hy15":
|
| 184 |
+
act_func = nn.LeakyReLU(0.2, inplace=True)
|
| 185 |
+
else:
|
| 186 |
+
act_func = nn.ReLU(inplace=True)
|
| 187 |
+
|
| 188 |
+
self.encoder = nn.Sequential(
|
| 189 |
+
conv(self.image_channels * self.patch_size**2, 64),
|
| 190 |
+
act_func,
|
| 191 |
+
TPool(64, 2),
|
| 192 |
+
conv(64, 64, stride=2, bias=False),
|
| 193 |
+
MemBlock(64, 64, act_func),
|
| 194 |
+
MemBlock(64, 64, act_func),
|
| 195 |
+
MemBlock(64, 64, act_func),
|
| 196 |
+
TPool(64, 2),
|
| 197 |
+
conv(64, 64, stride=2, bias=False),
|
| 198 |
+
MemBlock(64, 64, act_func),
|
| 199 |
+
MemBlock(64, 64, act_func),
|
| 200 |
+
MemBlock(64, 64, act_func),
|
| 201 |
+
TPool(64, 1),
|
| 202 |
+
conv(64, 64, stride=2, bias=False),
|
| 203 |
+
MemBlock(64, 64, act_func),
|
| 204 |
+
MemBlock(64, 64, act_func),
|
| 205 |
+
MemBlock(64, 64, act_func),
|
| 206 |
+
conv(64, self.latent_channels),
|
| 207 |
+
)
|
| 208 |
+
n_f = [256, 128, 64, 64]
|
| 209 |
+
self.frames_to_trim = 2 ** sum(decoder_time_upscale) - 1
|
| 210 |
+
self.decoder = nn.Sequential(
|
| 211 |
+
Clamp(),
|
| 212 |
+
conv(self.latent_channels, n_f[0]),
|
| 213 |
+
act_func,
|
| 214 |
+
MemBlock(n_f[0], n_f[0], act_func),
|
| 215 |
+
MemBlock(n_f[0], n_f[0], act_func),
|
| 216 |
+
MemBlock(n_f[0], n_f[0], act_func),
|
| 217 |
+
nn.Upsample(scale_factor=2 if decoder_space_upscale[0] else 1),
|
| 218 |
+
TGrow(n_f[0], 1),
|
| 219 |
+
conv(n_f[0], n_f[1], bias=False),
|
| 220 |
+
MemBlock(n_f[1], n_f[1], act_func),
|
| 221 |
+
MemBlock(n_f[1], n_f[1], act_func),
|
| 222 |
+
MemBlock(n_f[1], n_f[1], act_func),
|
| 223 |
+
nn.Upsample(scale_factor=2 if decoder_space_upscale[1] else 1),
|
| 224 |
+
TGrow(n_f[1], 2 if decoder_time_upscale[0] else 1),
|
| 225 |
+
conv(n_f[1], n_f[2], bias=False),
|
| 226 |
+
MemBlock(n_f[2], n_f[2], act_func),
|
| 227 |
+
MemBlock(n_f[2], n_f[2], act_func),
|
| 228 |
+
MemBlock(n_f[2], n_f[2], act_func),
|
| 229 |
+
nn.Upsample(scale_factor=2 if decoder_space_upscale[2] else 1),
|
| 230 |
+
TGrow(n_f[2], 2 if decoder_time_upscale[1] else 1),
|
| 231 |
+
conv(n_f[2], n_f[3], bias=False),
|
| 232 |
+
act_func,
|
| 233 |
+
conv(n_f[3], self.image_channels * self.patch_size**2),
|
| 234 |
+
)
|
| 235 |
+
if checkpoint_path is not None:
|
| 236 |
+
ext = os.path.splitext(checkpoint_path)[1].lower()
|
| 237 |
+
|
| 238 |
+
if ext == ".pth":
|
| 239 |
+
state_dict = torch.load(checkpoint_path, map_location="cpu", weights_only=True)
|
| 240 |
+
elif ext == ".safetensors":
|
| 241 |
+
state_dict = load_file(checkpoint_path, device="cpu")
|
| 242 |
+
else:
|
| 243 |
+
raise ValueError(f"Unsupported checkpoint format: {ext}. Supported formats: .pth, .safetensors")
|
| 244 |
+
|
| 245 |
+
self.load_state_dict(self.patch_tgrow_layers(state_dict))
|
| 246 |
+
|
| 247 |
+
def patch_tgrow_layers(self, sd):
|
| 248 |
+
"""Patch TGrow layers to use a smaller kernel if needed.
|
| 249 |
+
|
| 250 |
+
Args:
|
| 251 |
+
sd: state dict to patch
|
| 252 |
+
"""
|
| 253 |
+
new_sd = self.state_dict()
|
| 254 |
+
for i, layer in enumerate(self.decoder):
|
| 255 |
+
if isinstance(layer, TGrow):
|
| 256 |
+
key = f"decoder.{i}.conv.weight"
|
| 257 |
+
if sd[key].shape[0] > new_sd[key].shape[0]:
|
| 258 |
+
# take the last-timestep output channels
|
| 259 |
+
sd[key] = sd[key][-new_sd[key].shape[0] :]
|
| 260 |
+
return sd
|
| 261 |
+
|
| 262 |
+
def encode_video(self, x, parallel=True, show_progress_bar=True):
|
| 263 |
+
"""Encode a sequence of frames.
|
| 264 |
+
|
| 265 |
+
Args:
|
| 266 |
+
x: input NTCHW RGB (C=3) tensor with values in [0, 1].
|
| 267 |
+
parallel: if True, all frames will be processed at once.
|
| 268 |
+
(this is faster but may require more memory).
|
| 269 |
+
if False, frames will be processed sequentially.
|
| 270 |
+
Returns NTCHW latent tensor with ~Gaussian values.
|
| 271 |
+
"""
|
| 272 |
+
if self.patch_size > 1:
|
| 273 |
+
x = F.pixel_unshuffle(x, self.patch_size)
|
| 274 |
+
if x.shape[1] % 4 != 0:
|
| 275 |
+
# pad at end to multiple of 4
|
| 276 |
+
n_pad = 4 - x.shape[1] % 4
|
| 277 |
+
padding = x[:, -1:].repeat_interleave(n_pad, dim=1)
|
| 278 |
+
x = torch.cat([x, padding], 1)
|
| 279 |
+
return apply_model_with_memblocks(self.encoder, x, parallel, show_progress_bar)
|
| 280 |
+
|
| 281 |
+
def decode_video(self, x, parallel=True, show_progress_bar=True):
|
| 282 |
+
"""Decode a sequence of frames.
|
| 283 |
+
|
| 284 |
+
Args:
|
| 285 |
+
x: input NTCHW latent (C=12) tensor with ~Gaussian values.
|
| 286 |
+
parallel: if True, all frames will be processed at once.
|
| 287 |
+
(this is faster but may require more memory).
|
| 288 |
+
if False, frames will be processed sequentially.
|
| 289 |
+
Returns NTCHW RGB tensor with ~[0, 1] values.
|
| 290 |
+
"""
|
| 291 |
+
skip_trim = self.is_cogvideox and x.shape[1] % 2 == 0
|
| 292 |
+
x = apply_model_with_memblocks(self.decoder, x, parallel, show_progress_bar)
|
| 293 |
+
if self.model_type == "hy15":
|
| 294 |
+
x = x.clamp_(-1, 1)
|
| 295 |
+
else:
|
| 296 |
+
x = x.clamp_(0, 1)
|
| 297 |
+
if self.patch_size > 1:
|
| 298 |
+
x = F.pixel_shuffle(x, self.patch_size)
|
| 299 |
+
if skip_trim:
|
| 300 |
+
# skip trimming for cogvideox to make frame counts match.
|
| 301 |
+
# this still doesn't have correct temporal alignment for certain frame counts
|
| 302 |
+
# (cogvideox seems to pad at the start?), but for multiple-of-4 it's fine.
|
| 303 |
+
return x
|
| 304 |
+
return x[:, self.frames_to_trim :]
|
flash_head/wan/modules/vae.py
ADDED
|
@@ -0,0 +1,1598 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024-2025 The Alibaba Wan Team Authors. All rights reserved.
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
import torch.distributed as dist
|
| 5 |
+
import torch.nn as nn
|
| 6 |
+
import torch.nn.functional as F
|
| 7 |
+
from einops import rearrange
|
| 8 |
+
from loguru import logger
|
| 9 |
+
|
| 10 |
+
__all__ = [
|
| 11 |
+
"WanVAE",
|
| 12 |
+
]
|
| 13 |
+
|
| 14 |
+
CACHE_T = 2
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class CausalConv3d(nn.Conv3d):
|
| 18 |
+
"""
|
| 19 |
+
Causal 3d convolusion.
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
def __init__(self, *args, **kwargs):
|
| 23 |
+
super().__init__(*args, **kwargs)
|
| 24 |
+
self._padding = (
|
| 25 |
+
self.padding[2],
|
| 26 |
+
self.padding[2],
|
| 27 |
+
self.padding[1],
|
| 28 |
+
self.padding[1],
|
| 29 |
+
2 * self.padding[0],
|
| 30 |
+
0,
|
| 31 |
+
)
|
| 32 |
+
self.padding = (0, 0, 0)
|
| 33 |
+
|
| 34 |
+
def forward(self, x, cache_x=None):
|
| 35 |
+
padding = list(self._padding)
|
| 36 |
+
if cache_x is not None and self._padding[4] > 0:
|
| 37 |
+
cache_x = cache_x.to(x.device)
|
| 38 |
+
x = torch.cat([cache_x, x], dim=2)
|
| 39 |
+
padding[4] -= cache_x.shape[2]
|
| 40 |
+
x = F.pad(x, padding)
|
| 41 |
+
|
| 42 |
+
return super().forward(x)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
class RMS_norm(nn.Module):
|
| 46 |
+
def __init__(self, dim, channel_first=True, images=True, bias=False):
|
| 47 |
+
super().__init__()
|
| 48 |
+
broadcastable_dims = (1, 1, 1) if not images else (1, 1)
|
| 49 |
+
shape = (dim, *broadcastable_dims) if channel_first else (dim,)
|
| 50 |
+
|
| 51 |
+
self.channel_first = channel_first
|
| 52 |
+
self.scale = dim**0.5
|
| 53 |
+
self.gamma = nn.Parameter(torch.ones(shape))
|
| 54 |
+
self.bias = nn.Parameter(torch.zeros(shape)) if bias else 0.0
|
| 55 |
+
|
| 56 |
+
def forward(self, x):
|
| 57 |
+
return (
|
| 58 |
+
F.normalize(x, dim=(1 if self.channel_first else -1))
|
| 59 |
+
* self.scale
|
| 60 |
+
* self.gamma
|
| 61 |
+
+ self.bias
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
class Upsample(nn.Upsample):
|
| 66 |
+
def forward(self, x):
|
| 67 |
+
"""
|
| 68 |
+
Fix bfloat16 support for nearest neighbor interpolation.
|
| 69 |
+
"""
|
| 70 |
+
return super().forward(x)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
class Resample(nn.Module):
|
| 74 |
+
def __init__(self, dim, mode):
|
| 75 |
+
assert mode in (
|
| 76 |
+
"none",
|
| 77 |
+
"upsample2d",
|
| 78 |
+
"upsample3d",
|
| 79 |
+
"downsample2d",
|
| 80 |
+
"downsample3d",
|
| 81 |
+
)
|
| 82 |
+
super().__init__()
|
| 83 |
+
self.dim = dim
|
| 84 |
+
self.mode = mode
|
| 85 |
+
|
| 86 |
+
# layers
|
| 87 |
+
if mode == "upsample2d":
|
| 88 |
+
self.resample = nn.Sequential(
|
| 89 |
+
Upsample(scale_factor=(2.0, 2.0), mode="nearest-exact"),
|
| 90 |
+
nn.Conv2d(dim, dim // 2, 3, padding=1),
|
| 91 |
+
)
|
| 92 |
+
elif mode == "upsample3d":
|
| 93 |
+
self.resample = nn.Sequential(
|
| 94 |
+
Upsample(scale_factor=(2.0, 2.0), mode="nearest-exact"),
|
| 95 |
+
nn.Conv2d(dim, dim // 2, 3, padding=1),
|
| 96 |
+
)
|
| 97 |
+
self.time_conv = CausalConv3d(dim, dim * 2, (3, 1, 1), padding=(1, 0, 0))
|
| 98 |
+
|
| 99 |
+
elif mode == "downsample2d":
|
| 100 |
+
self.resample = nn.Sequential(
|
| 101 |
+
nn.ZeroPad2d((0, 1, 0, 1)), nn.Conv2d(dim, dim, 3, stride=(2, 2))
|
| 102 |
+
)
|
| 103 |
+
elif mode == "downsample3d":
|
| 104 |
+
self.resample = nn.Sequential(
|
| 105 |
+
nn.ZeroPad2d((0, 1, 0, 1)), nn.Conv2d(dim, dim, 3, stride=(2, 2))
|
| 106 |
+
)
|
| 107 |
+
self.time_conv = CausalConv3d(
|
| 108 |
+
dim, dim, (3, 1, 1), stride=(2, 1, 1), padding=(0, 0, 0)
|
| 109 |
+
)
|
| 110 |
+
|
| 111 |
+
else:
|
| 112 |
+
self.resample = nn.Identity()
|
| 113 |
+
|
| 114 |
+
def forward(self, x, feat_cache=None, feat_idx=[0]):
|
| 115 |
+
b, c, t, h, w = x.size()
|
| 116 |
+
if self.mode == "upsample3d":
|
| 117 |
+
if feat_cache is not None:
|
| 118 |
+
idx = feat_idx[0]
|
| 119 |
+
if feat_cache[idx] is None:
|
| 120 |
+
feat_cache[idx] = "Rep"
|
| 121 |
+
feat_idx[0] += 1
|
| 122 |
+
else:
|
| 123 |
+
cache_x = x[:, :, -CACHE_T:, :, :].clone()
|
| 124 |
+
if (
|
| 125 |
+
cache_x.shape[2] < 2
|
| 126 |
+
and feat_cache[idx] is not None
|
| 127 |
+
and feat_cache[idx] != "Rep"
|
| 128 |
+
):
|
| 129 |
+
# cache last frame of last two chunk
|
| 130 |
+
cache_x = torch.cat(
|
| 131 |
+
[
|
| 132 |
+
feat_cache[idx][:, :, -1, :, :]
|
| 133 |
+
.unsqueeze(2)
|
| 134 |
+
.to(cache_x.device),
|
| 135 |
+
cache_x,
|
| 136 |
+
],
|
| 137 |
+
dim=2,
|
| 138 |
+
)
|
| 139 |
+
if (
|
| 140 |
+
cache_x.shape[2] < 2
|
| 141 |
+
and feat_cache[idx] is not None
|
| 142 |
+
and feat_cache[idx] == "Rep"
|
| 143 |
+
):
|
| 144 |
+
cache_x = torch.cat(
|
| 145 |
+
[torch.zeros_like(cache_x).to(cache_x.device), cache_x],
|
| 146 |
+
dim=2,
|
| 147 |
+
)
|
| 148 |
+
if feat_cache[idx] == "Rep":
|
| 149 |
+
x = self.time_conv(x)
|
| 150 |
+
else:
|
| 151 |
+
x = self.time_conv(x, feat_cache[idx])
|
| 152 |
+
feat_cache[idx] = cache_x
|
| 153 |
+
feat_idx[0] += 1
|
| 154 |
+
|
| 155 |
+
x = x.reshape(b, 2, c, t, h, w)
|
| 156 |
+
x = torch.stack((x[:, 0, :, :, :, :], x[:, 1, :, :, :, :]), 3)
|
| 157 |
+
x = x.reshape(b, c, t * 2, h, w)
|
| 158 |
+
t = x.shape[2]
|
| 159 |
+
x = rearrange(x, "b c t h w -> (b t) c h w")
|
| 160 |
+
x = self.resample(x)
|
| 161 |
+
x = rearrange(x, "(b t) c h w -> b c t h w", t=t)
|
| 162 |
+
|
| 163 |
+
if self.mode == "downsample3d":
|
| 164 |
+
if feat_cache is not None:
|
| 165 |
+
idx = feat_idx[0]
|
| 166 |
+
if feat_cache[idx] is None:
|
| 167 |
+
feat_cache[idx] = x.clone()
|
| 168 |
+
feat_idx[0] += 1
|
| 169 |
+
else:
|
| 170 |
+
cache_x = x[:, :, -1:, :, :].clone()
|
| 171 |
+
# if cache_x.shape[2] < 2 and feat_cache[idx] is not None and feat_cache[idx]!='Rep':
|
| 172 |
+
# # cache last frame of last two chunk
|
| 173 |
+
# cache_x = torch.cat([feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(cache_x.device), cache_x], dim=2)
|
| 174 |
+
|
| 175 |
+
x = self.time_conv(
|
| 176 |
+
torch.cat([feat_cache[idx][:, :, -1:, :, :], x], 2)
|
| 177 |
+
)
|
| 178 |
+
feat_cache[idx] = cache_x
|
| 179 |
+
feat_idx[0] += 1
|
| 180 |
+
return x
|
| 181 |
+
|
| 182 |
+
def init_weight(self, conv):
|
| 183 |
+
conv_weight = conv.weight
|
| 184 |
+
nn.init.zeros_(conv_weight)
|
| 185 |
+
c1, c2, t, h, w = conv_weight.size()
|
| 186 |
+
one_matrix = torch.eye(c1, c2)
|
| 187 |
+
init_matrix = one_matrix
|
| 188 |
+
nn.init.zeros_(conv_weight)
|
| 189 |
+
# conv_weight.data[:,:,-1,1,1] = init_matrix * 0.5
|
| 190 |
+
conv_weight.data[:, :, 1, 0, 0] = init_matrix # * 0.5
|
| 191 |
+
conv.weight.data.copy_(conv_weight)
|
| 192 |
+
nn.init.zeros_(conv.bias.data)
|
| 193 |
+
|
| 194 |
+
def init_weight2(self, conv):
|
| 195 |
+
conv_weight = conv.weight.data
|
| 196 |
+
nn.init.zeros_(conv_weight)
|
| 197 |
+
c1, c2, t, h, w = conv_weight.size()
|
| 198 |
+
init_matrix = torch.eye(c1 // 2, c2)
|
| 199 |
+
# init_matrix = repeat(init_matrix, 'o ... -> (o 2) ...').permute(1,0,2).contiguous().reshape(c1,c2)
|
| 200 |
+
conv_weight[: c1 // 2, :, -1, 0, 0] = init_matrix
|
| 201 |
+
conv_weight[c1 // 2 :, :, -1, 0, 0] = init_matrix
|
| 202 |
+
conv.weight.data.copy_(conv_weight)
|
| 203 |
+
nn.init.zeros_(conv.bias.data)
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
class ResidualBlock(nn.Module):
|
| 207 |
+
def __init__(self, in_dim, out_dim, dropout=0.0):
|
| 208 |
+
super().__init__()
|
| 209 |
+
self.in_dim = in_dim
|
| 210 |
+
self.out_dim = out_dim
|
| 211 |
+
|
| 212 |
+
# layers
|
| 213 |
+
self.residual = nn.Sequential(
|
| 214 |
+
RMS_norm(in_dim, images=False),
|
| 215 |
+
nn.SiLU(),
|
| 216 |
+
CausalConv3d(in_dim, out_dim, 3, padding=1),
|
| 217 |
+
RMS_norm(out_dim, images=False),
|
| 218 |
+
nn.SiLU(),
|
| 219 |
+
nn.Dropout(dropout),
|
| 220 |
+
CausalConv3d(out_dim, out_dim, 3, padding=1),
|
| 221 |
+
)
|
| 222 |
+
self.shortcut = (
|
| 223 |
+
CausalConv3d(in_dim, out_dim, 1) if in_dim != out_dim else nn.Identity()
|
| 224 |
+
)
|
| 225 |
+
|
| 226 |
+
def forward(self, x, feat_cache=None, feat_idx=[0]):
|
| 227 |
+
h = self.shortcut(x)
|
| 228 |
+
for layer in self.residual:
|
| 229 |
+
if isinstance(layer, CausalConv3d) and feat_cache is not None:
|
| 230 |
+
idx = feat_idx[0]
|
| 231 |
+
cache_x = x[:, :, -CACHE_T:, :, :].clone()
|
| 232 |
+
if cache_x.shape[2] < 2 and feat_cache[idx] is not None:
|
| 233 |
+
# cache last frame of last two chunk
|
| 234 |
+
cache_x = torch.cat(
|
| 235 |
+
[
|
| 236 |
+
feat_cache[idx][:, :, -1, :, :]
|
| 237 |
+
.unsqueeze(2)
|
| 238 |
+
.to(cache_x.device),
|
| 239 |
+
cache_x,
|
| 240 |
+
],
|
| 241 |
+
dim=2,
|
| 242 |
+
)
|
| 243 |
+
x = layer(x, feat_cache[idx])
|
| 244 |
+
feat_cache[idx] = cache_x
|
| 245 |
+
feat_idx[0] += 1
|
| 246 |
+
else:
|
| 247 |
+
x = layer(x)
|
| 248 |
+
return x + h
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
class AttentionBlock(nn.Module):
|
| 252 |
+
"""
|
| 253 |
+
Causal self-attention with a single head.
|
| 254 |
+
"""
|
| 255 |
+
|
| 256 |
+
def __init__(self, dim):
|
| 257 |
+
super().__init__()
|
| 258 |
+
self.dim = dim
|
| 259 |
+
|
| 260 |
+
# layers
|
| 261 |
+
self.norm = RMS_norm(dim)
|
| 262 |
+
self.to_qkv = nn.Conv2d(dim, dim * 3, 1)
|
| 263 |
+
self.proj = nn.Conv2d(dim, dim, 1)
|
| 264 |
+
|
| 265 |
+
# zero out the last layer params
|
| 266 |
+
nn.init.zeros_(self.proj.weight)
|
| 267 |
+
|
| 268 |
+
def forward(self, x):
|
| 269 |
+
identity = x
|
| 270 |
+
b, c, t, h, w = x.size()
|
| 271 |
+
x = rearrange(x, "b c t h w -> (b t) c h w")
|
| 272 |
+
x = self.norm(x)
|
| 273 |
+
# compute query, key, value
|
| 274 |
+
q, k, v = (
|
| 275 |
+
self.to_qkv(x)
|
| 276 |
+
.reshape(b * t, 1, c * 3, -1)
|
| 277 |
+
.permute(0, 1, 3, 2)
|
| 278 |
+
.contiguous()
|
| 279 |
+
.chunk(3, dim=-1)
|
| 280 |
+
)
|
| 281 |
+
|
| 282 |
+
# apply attention
|
| 283 |
+
x = F.scaled_dot_product_attention(
|
| 284 |
+
q,
|
| 285 |
+
k,
|
| 286 |
+
v,
|
| 287 |
+
)
|
| 288 |
+
x = x.squeeze(1).permute(0, 2, 1).reshape(b * t, c, h, w)
|
| 289 |
+
|
| 290 |
+
# output
|
| 291 |
+
x = self.proj(x)
|
| 292 |
+
x = rearrange(x, "(b t) c h w-> b c t h w", t=t)
|
| 293 |
+
return x + identity
|
| 294 |
+
|
| 295 |
+
|
| 296 |
+
class Encoder3d(nn.Module):
|
| 297 |
+
def __init__(
|
| 298 |
+
self,
|
| 299 |
+
dim=128,
|
| 300 |
+
z_dim=4,
|
| 301 |
+
dim_mult=[1, 2, 4, 4],
|
| 302 |
+
num_res_blocks=2,
|
| 303 |
+
attn_scales=[],
|
| 304 |
+
temperal_downsample=[True, True, False],
|
| 305 |
+
dropout=0.0,
|
| 306 |
+
):
|
| 307 |
+
super().__init__()
|
| 308 |
+
self.dim = dim
|
| 309 |
+
self.z_dim = z_dim
|
| 310 |
+
self.dim_mult = dim_mult
|
| 311 |
+
self.num_res_blocks = num_res_blocks
|
| 312 |
+
self.attn_scales = attn_scales
|
| 313 |
+
self.temperal_downsample = temperal_downsample
|
| 314 |
+
|
| 315 |
+
# dimensions
|
| 316 |
+
dims = [dim * u for u in [1] + dim_mult]
|
| 317 |
+
scale = 1.0
|
| 318 |
+
|
| 319 |
+
# init block
|
| 320 |
+
self.conv1 = CausalConv3d(3, dims[0], 3, padding=1)
|
| 321 |
+
|
| 322 |
+
# downsample blocks
|
| 323 |
+
downsamples = []
|
| 324 |
+
for i, (in_dim, out_dim) in enumerate(zip(dims[:-1], dims[1:])):
|
| 325 |
+
# residual (+attention) blocks
|
| 326 |
+
for _ in range(num_res_blocks):
|
| 327 |
+
downsamples.append(ResidualBlock(in_dim, out_dim, dropout))
|
| 328 |
+
if scale in attn_scales:
|
| 329 |
+
downsamples.append(AttentionBlock(out_dim))
|
| 330 |
+
in_dim = out_dim
|
| 331 |
+
|
| 332 |
+
# downsample block
|
| 333 |
+
if i != len(dim_mult) - 1:
|
| 334 |
+
mode = "downsample3d" if temperal_downsample[i] else "downsample2d"
|
| 335 |
+
downsamples.append(Resample(out_dim, mode=mode))
|
| 336 |
+
scale /= 2.0
|
| 337 |
+
self.downsamples = nn.Sequential(*downsamples)
|
| 338 |
+
|
| 339 |
+
# middle blocks
|
| 340 |
+
self.middle = nn.Sequential(
|
| 341 |
+
ResidualBlock(out_dim, out_dim, dropout),
|
| 342 |
+
AttentionBlock(out_dim),
|
| 343 |
+
ResidualBlock(out_dim, out_dim, dropout),
|
| 344 |
+
)
|
| 345 |
+
|
| 346 |
+
# output blocks
|
| 347 |
+
self.head = nn.Sequential(
|
| 348 |
+
RMS_norm(out_dim, images=False),
|
| 349 |
+
nn.SiLU(),
|
| 350 |
+
CausalConv3d(out_dim, z_dim, 3, padding=1),
|
| 351 |
+
)
|
| 352 |
+
|
| 353 |
+
def forward(self, x, feat_cache=None, feat_idx=[0]):
|
| 354 |
+
if feat_cache is not None:
|
| 355 |
+
idx = feat_idx[0]
|
| 356 |
+
cache_x = x[:, :, -CACHE_T:, :, :].clone()
|
| 357 |
+
if cache_x.shape[2] < 2 and feat_cache[idx] is not None:
|
| 358 |
+
# cache last frame of last two chunk
|
| 359 |
+
cache_x = torch.cat(
|
| 360 |
+
[
|
| 361 |
+
feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(cache_x.device),
|
| 362 |
+
cache_x,
|
| 363 |
+
],
|
| 364 |
+
dim=2,
|
| 365 |
+
)
|
| 366 |
+
x = self.conv1(x, feat_cache[idx])
|
| 367 |
+
feat_cache[idx] = cache_x
|
| 368 |
+
feat_idx[0] += 1
|
| 369 |
+
else:
|
| 370 |
+
x = self.conv1(x)
|
| 371 |
+
|
| 372 |
+
## downsamples
|
| 373 |
+
for layer in self.downsamples:
|
| 374 |
+
if feat_cache is not None:
|
| 375 |
+
x = layer(x, feat_cache, feat_idx)
|
| 376 |
+
else:
|
| 377 |
+
x = layer(x)
|
| 378 |
+
|
| 379 |
+
## middle
|
| 380 |
+
for layer in self.middle:
|
| 381 |
+
if isinstance(layer, ResidualBlock) and feat_cache is not None:
|
| 382 |
+
x = layer(x, feat_cache, feat_idx)
|
| 383 |
+
else:
|
| 384 |
+
x = layer(x)
|
| 385 |
+
|
| 386 |
+
## head
|
| 387 |
+
for layer in self.head:
|
| 388 |
+
if isinstance(layer, CausalConv3d) and feat_cache is not None:
|
| 389 |
+
idx = feat_idx[0]
|
| 390 |
+
cache_x = x[:, :, -CACHE_T:, :, :].clone()
|
| 391 |
+
if cache_x.shape[2] < 2 and feat_cache[idx] is not None:
|
| 392 |
+
# cache last frame of last two chunk
|
| 393 |
+
cache_x = torch.cat(
|
| 394 |
+
[
|
| 395 |
+
feat_cache[idx][:, :, -1, :, :]
|
| 396 |
+
.unsqueeze(2)
|
| 397 |
+
.to(cache_x.device),
|
| 398 |
+
cache_x,
|
| 399 |
+
],
|
| 400 |
+
dim=2,
|
| 401 |
+
)
|
| 402 |
+
x = layer(x, feat_cache[idx])
|
| 403 |
+
feat_cache[idx] = cache_x
|
| 404 |
+
feat_idx[0] += 1
|
| 405 |
+
else:
|
| 406 |
+
x = layer(x)
|
| 407 |
+
return x
|
| 408 |
+
|
| 409 |
+
|
| 410 |
+
class Decoder3d(nn.Module):
|
| 411 |
+
def __init__(
|
| 412 |
+
self,
|
| 413 |
+
dim=128,
|
| 414 |
+
z_dim=4,
|
| 415 |
+
dim_mult=[1, 2, 4, 4],
|
| 416 |
+
num_res_blocks=2,
|
| 417 |
+
attn_scales=[],
|
| 418 |
+
temperal_upsample=[False, True, True],
|
| 419 |
+
dropout=0.0,
|
| 420 |
+
):
|
| 421 |
+
super().__init__()
|
| 422 |
+
self.dim = dim
|
| 423 |
+
self.z_dim = z_dim
|
| 424 |
+
self.dim_mult = dim_mult
|
| 425 |
+
self.num_res_blocks = num_res_blocks
|
| 426 |
+
self.attn_scales = attn_scales
|
| 427 |
+
self.temperal_upsample = temperal_upsample
|
| 428 |
+
|
| 429 |
+
# dimensions
|
| 430 |
+
dims = [dim * u for u in [dim_mult[-1]] + dim_mult[::-1]]
|
| 431 |
+
|
| 432 |
+
scale = 1.0 / 2 ** (len(dim_mult) - 2)
|
| 433 |
+
|
| 434 |
+
# init block
|
| 435 |
+
self.conv1 = CausalConv3d(z_dim, dims[0], 3, padding=1)
|
| 436 |
+
|
| 437 |
+
# middle blocks
|
| 438 |
+
self.middle = nn.Sequential(
|
| 439 |
+
ResidualBlock(dims[0], dims[0], dropout),
|
| 440 |
+
AttentionBlock(dims[0]),
|
| 441 |
+
ResidualBlock(dims[0], dims[0], dropout),
|
| 442 |
+
)
|
| 443 |
+
|
| 444 |
+
# upsample blocks
|
| 445 |
+
upsamples = []
|
| 446 |
+
for i, (in_dim, out_dim) in enumerate(zip(dims[:-1], dims[1:])):
|
| 447 |
+
# residual (+attention) blocks
|
| 448 |
+
if i == 1 or i == 2 or i == 3:
|
| 449 |
+
in_dim = in_dim // 2
|
| 450 |
+
for _ in range(num_res_blocks + 1):
|
| 451 |
+
upsamples.append(ResidualBlock(in_dim, out_dim, dropout))
|
| 452 |
+
if scale in attn_scales:
|
| 453 |
+
upsamples.append(AttentionBlock(out_dim))
|
| 454 |
+
in_dim = out_dim
|
| 455 |
+
|
| 456 |
+
# upsample block
|
| 457 |
+
if i != len(dim_mult) - 1:
|
| 458 |
+
mode = "upsample3d" if temperal_upsample[i] else "upsample2d"
|
| 459 |
+
upsamples.append(Resample(out_dim, mode=mode))
|
| 460 |
+
scale *= 2.0
|
| 461 |
+
self.upsamples = nn.Sequential(*upsamples)
|
| 462 |
+
|
| 463 |
+
# output blocks
|
| 464 |
+
self.head = nn.Sequential(
|
| 465 |
+
RMS_norm(out_dim, images=False),
|
| 466 |
+
nn.SiLU(),
|
| 467 |
+
CausalConv3d(out_dim, 3, 3, padding=1),
|
| 468 |
+
)
|
| 469 |
+
|
| 470 |
+
def forward(self, x, feat_cache=None, feat_idx=[0]):
|
| 471 |
+
## conv1
|
| 472 |
+
if feat_cache is not None:
|
| 473 |
+
idx = feat_idx[0]
|
| 474 |
+
cache_x = x[:, :, -CACHE_T:, :, :].clone()
|
| 475 |
+
if cache_x.shape[2] < 2 and feat_cache[idx] is not None:
|
| 476 |
+
# cache last frame of last two chunk
|
| 477 |
+
cache_x = torch.cat(
|
| 478 |
+
[
|
| 479 |
+
feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(cache_x.device),
|
| 480 |
+
cache_x,
|
| 481 |
+
],
|
| 482 |
+
dim=2,
|
| 483 |
+
)
|
| 484 |
+
x = self.conv1(x, feat_cache[idx])
|
| 485 |
+
feat_cache[idx] = cache_x
|
| 486 |
+
feat_idx[0] += 1
|
| 487 |
+
else:
|
| 488 |
+
x = self.conv1(x)
|
| 489 |
+
|
| 490 |
+
## middle
|
| 491 |
+
for layer in self.middle:
|
| 492 |
+
if isinstance(layer, ResidualBlock) and feat_cache is not None:
|
| 493 |
+
x = layer(x, feat_cache, feat_idx)
|
| 494 |
+
else:
|
| 495 |
+
x = layer(x)
|
| 496 |
+
|
| 497 |
+
## upsamples
|
| 498 |
+
for layer in self.upsamples:
|
| 499 |
+
if feat_cache is not None:
|
| 500 |
+
x = layer(x, feat_cache, feat_idx)
|
| 501 |
+
else:
|
| 502 |
+
x = layer(x)
|
| 503 |
+
|
| 504 |
+
## head
|
| 505 |
+
for layer in self.head:
|
| 506 |
+
if isinstance(layer, CausalConv3d) and feat_cache is not None:
|
| 507 |
+
idx = feat_idx[0]
|
| 508 |
+
cache_x = x[:, :, -CACHE_T:, :, :].clone()
|
| 509 |
+
if cache_x.shape[2] < 2 and feat_cache[idx] is not None:
|
| 510 |
+
# cache last frame of last two chunk
|
| 511 |
+
cache_x = torch.cat(
|
| 512 |
+
[
|
| 513 |
+
feat_cache[idx][:, :, -1, :, :]
|
| 514 |
+
.unsqueeze(2)
|
| 515 |
+
.to(cache_x.device),
|
| 516 |
+
cache_x,
|
| 517 |
+
],
|
| 518 |
+
dim=2,
|
| 519 |
+
)
|
| 520 |
+
x = layer(x, feat_cache[idx])
|
| 521 |
+
feat_cache[idx] = cache_x
|
| 522 |
+
feat_idx[0] += 1
|
| 523 |
+
else:
|
| 524 |
+
x = layer(x)
|
| 525 |
+
return x
|
| 526 |
+
|
| 527 |
+
|
| 528 |
+
def count_conv3d(model):
|
| 529 |
+
count = 0
|
| 530 |
+
for m in model.modules():
|
| 531 |
+
if isinstance(m, CausalConv3d):
|
| 532 |
+
count += 1
|
| 533 |
+
return count
|
| 534 |
+
|
| 535 |
+
|
| 536 |
+
class WanVAE_(nn.Module):
|
| 537 |
+
def __init__(
|
| 538 |
+
self,
|
| 539 |
+
dim=128,
|
| 540 |
+
z_dim=4,
|
| 541 |
+
dim_mult=[1, 2, 4, 4],
|
| 542 |
+
num_res_blocks=2,
|
| 543 |
+
attn_scales=[],
|
| 544 |
+
temperal_downsample=[True, True, False],
|
| 545 |
+
dropout=0.0,
|
| 546 |
+
):
|
| 547 |
+
super().__init__()
|
| 548 |
+
self.dim = dim
|
| 549 |
+
self.z_dim = z_dim
|
| 550 |
+
self.dim_mult = dim_mult
|
| 551 |
+
self.num_res_blocks = num_res_blocks
|
| 552 |
+
self.attn_scales = attn_scales
|
| 553 |
+
self.temperal_downsample = temperal_downsample
|
| 554 |
+
self.temperal_upsample = temperal_downsample[::-1]
|
| 555 |
+
self.spatial_compression_ratio = 2 ** len(self.temperal_downsample)
|
| 556 |
+
|
| 557 |
+
# The minimal tile height and width for spatial tiling to be used
|
| 558 |
+
self.tile_sample_min_height = 256
|
| 559 |
+
self.tile_sample_min_width = 256
|
| 560 |
+
|
| 561 |
+
# The minimal distance between two spatial tiles
|
| 562 |
+
self.tile_sample_stride_height = 192
|
| 563 |
+
self.tile_sample_stride_width = 192
|
| 564 |
+
# modules
|
| 565 |
+
self.encoder = Encoder3d(
|
| 566 |
+
dim,
|
| 567 |
+
z_dim * 2,
|
| 568 |
+
dim_mult,
|
| 569 |
+
num_res_blocks,
|
| 570 |
+
attn_scales,
|
| 571 |
+
self.temperal_downsample,
|
| 572 |
+
dropout,
|
| 573 |
+
)
|
| 574 |
+
self.conv1 = CausalConv3d(z_dim * 2, z_dim * 2, 1)
|
| 575 |
+
self.conv2 = CausalConv3d(z_dim, z_dim, 1)
|
| 576 |
+
self.decoder = Decoder3d(
|
| 577 |
+
dim,
|
| 578 |
+
z_dim,
|
| 579 |
+
dim_mult,
|
| 580 |
+
num_res_blocks,
|
| 581 |
+
attn_scales,
|
| 582 |
+
self.temperal_upsample,
|
| 583 |
+
dropout,
|
| 584 |
+
)
|
| 585 |
+
|
| 586 |
+
def forward(self, x):
|
| 587 |
+
mu, log_var = self.encode(x)
|
| 588 |
+
z = self.reparameterize(mu, log_var)
|
| 589 |
+
x_recon = self.decode(z)
|
| 590 |
+
return x_recon, mu, log_var
|
| 591 |
+
|
| 592 |
+
def blend_v(self, a, b, blend_extent):
|
| 593 |
+
blend_extent = min(a.shape[-2], b.shape[-2], blend_extent)
|
| 594 |
+
for y in range(blend_extent):
|
| 595 |
+
b[:, :, :, y, :] = a[:, :, :, -blend_extent + y, :] * (
|
| 596 |
+
1 - y / blend_extent
|
| 597 |
+
) + b[:, :, :, y, :] * (y / blend_extent)
|
| 598 |
+
return b
|
| 599 |
+
|
| 600 |
+
def blend_h(self, a, b, blend_extent):
|
| 601 |
+
blend_extent = min(a.shape[-1], b.shape[-1], blend_extent)
|
| 602 |
+
for x in range(blend_extent):
|
| 603 |
+
b[:, :, :, :, x] = a[:, :, :, :, -blend_extent + x] * (
|
| 604 |
+
1 - x / blend_extent
|
| 605 |
+
) + b[:, :, :, :, x] * (x / blend_extent)
|
| 606 |
+
return b
|
| 607 |
+
|
| 608 |
+
def tiled_encode(self, x, scale):
|
| 609 |
+
_, _, num_frames, height, width = x.shape
|
| 610 |
+
latent_height = height // self.spatial_compression_ratio
|
| 611 |
+
latent_width = width // self.spatial_compression_ratio
|
| 612 |
+
|
| 613 |
+
tile_latent_min_height = (
|
| 614 |
+
self.tile_sample_min_height // self.spatial_compression_ratio
|
| 615 |
+
)
|
| 616 |
+
tile_latent_min_width = (
|
| 617 |
+
self.tile_sample_min_width // self.spatial_compression_ratio
|
| 618 |
+
)
|
| 619 |
+
tile_latent_stride_height = (
|
| 620 |
+
self.tile_sample_stride_height // self.spatial_compression_ratio
|
| 621 |
+
)
|
| 622 |
+
tile_latent_stride_width = (
|
| 623 |
+
self.tile_sample_stride_width // self.spatial_compression_ratio
|
| 624 |
+
)
|
| 625 |
+
|
| 626 |
+
blend_height = tile_latent_min_height - tile_latent_stride_height
|
| 627 |
+
blend_width = tile_latent_min_width - tile_latent_stride_width
|
| 628 |
+
|
| 629 |
+
# Split x into overlapping tiles and encode them separately.
|
| 630 |
+
# The tiles have an overlap to avoid seams between tiles.
|
| 631 |
+
rows = []
|
| 632 |
+
for i in range(0, height, self.tile_sample_stride_height):
|
| 633 |
+
row = []
|
| 634 |
+
for j in range(0, width, self.tile_sample_stride_width):
|
| 635 |
+
self.clear_cache()
|
| 636 |
+
time = []
|
| 637 |
+
frame_range = 1 + (num_frames - 1) // 4
|
| 638 |
+
for k in range(frame_range):
|
| 639 |
+
self._enc_conv_idx = [0]
|
| 640 |
+
if k == 0:
|
| 641 |
+
tile = x[
|
| 642 |
+
:,
|
| 643 |
+
:,
|
| 644 |
+
:1,
|
| 645 |
+
i : i + self.tile_sample_min_height,
|
| 646 |
+
j : j + self.tile_sample_min_width,
|
| 647 |
+
]
|
| 648 |
+
else:
|
| 649 |
+
tile = x[
|
| 650 |
+
:,
|
| 651 |
+
:,
|
| 652 |
+
1 + 4 * (k - 1) : 1 + 4 * k,
|
| 653 |
+
i : i + self.tile_sample_min_height,
|
| 654 |
+
j : j + self.tile_sample_min_width,
|
| 655 |
+
]
|
| 656 |
+
tile = self.encoder(
|
| 657 |
+
tile, feat_cache=self._enc_feat_map, feat_idx=self._enc_conv_idx
|
| 658 |
+
)
|
| 659 |
+
mu, log_var = self.conv1(tile).chunk(2, dim=1)
|
| 660 |
+
if isinstance(scale[0], torch.Tensor):
|
| 661 |
+
mu = (mu - scale[0].view(1, self.z_dim, 1, 1, 1)) * scale[
|
| 662 |
+
1
|
| 663 |
+
].view(1, self.z_dim, 1, 1, 1)
|
| 664 |
+
else:
|
| 665 |
+
mu = (mu - scale[0]) * scale[1]
|
| 666 |
+
|
| 667 |
+
time.append(mu)
|
| 668 |
+
|
| 669 |
+
row.append(torch.cat(time, dim=2))
|
| 670 |
+
rows.append(row)
|
| 671 |
+
self.clear_cache()
|
| 672 |
+
|
| 673 |
+
result_rows = []
|
| 674 |
+
for i, row in enumerate(rows):
|
| 675 |
+
result_row = []
|
| 676 |
+
for j, tile in enumerate(row):
|
| 677 |
+
# blend the above tile and the left tile
|
| 678 |
+
# to the current tile and add the current tile to the result row
|
| 679 |
+
if i > 0:
|
| 680 |
+
tile = self.blend_v(rows[i - 1][j], tile, blend_height)
|
| 681 |
+
if j > 0:
|
| 682 |
+
tile = self.blend_h(row[j - 1], tile, blend_width)
|
| 683 |
+
result_row.append(
|
| 684 |
+
tile[:, :, :, :tile_latent_stride_height, :tile_latent_stride_width]
|
| 685 |
+
)
|
| 686 |
+
result_rows.append(torch.cat(result_row, dim=-1))
|
| 687 |
+
|
| 688 |
+
enc = torch.cat(result_rows, dim=3)[:, :, :, :latent_height, :latent_width]
|
| 689 |
+
return enc
|
| 690 |
+
|
| 691 |
+
def tiled_decode(self, z, scale):
|
| 692 |
+
if isinstance(scale[0], torch.Tensor):
|
| 693 |
+
z = z / scale[1].view(1, self.z_dim, 1, 1, 1) + scale[0].view(
|
| 694 |
+
1, self.z_dim, 1, 1, 1
|
| 695 |
+
)
|
| 696 |
+
else:
|
| 697 |
+
z = z / scale[1] + scale[0]
|
| 698 |
+
|
| 699 |
+
_, _, num_frames, height, width = z.shape
|
| 700 |
+
sample_height = height * self.spatial_compression_ratio
|
| 701 |
+
sample_width = width * self.spatial_compression_ratio
|
| 702 |
+
|
| 703 |
+
tile_latent_min_height = (
|
| 704 |
+
self.tile_sample_min_height // self.spatial_compression_ratio
|
| 705 |
+
)
|
| 706 |
+
tile_latent_min_width = (
|
| 707 |
+
self.tile_sample_min_width // self.spatial_compression_ratio
|
| 708 |
+
)
|
| 709 |
+
tile_latent_stride_height = (
|
| 710 |
+
self.tile_sample_stride_height // self.spatial_compression_ratio
|
| 711 |
+
)
|
| 712 |
+
tile_latent_stride_width = (
|
| 713 |
+
self.tile_sample_stride_width // self.spatial_compression_ratio
|
| 714 |
+
)
|
| 715 |
+
|
| 716 |
+
blend_height = self.tile_sample_min_height - self.tile_sample_stride_height
|
| 717 |
+
blend_width = self.tile_sample_min_width - self.tile_sample_stride_width
|
| 718 |
+
|
| 719 |
+
# Split z into overlapping tiles and decode them separately.
|
| 720 |
+
# The tiles have an overlap to avoid seams between tiles.
|
| 721 |
+
rows = []
|
| 722 |
+
for i in range(0, height, tile_latent_stride_height):
|
| 723 |
+
row = []
|
| 724 |
+
for j in range(0, width, tile_latent_stride_width):
|
| 725 |
+
self.clear_cache()
|
| 726 |
+
time = []
|
| 727 |
+
for k in range(num_frames):
|
| 728 |
+
self._conv_idx = [0]
|
| 729 |
+
tile = z[
|
| 730 |
+
:,
|
| 731 |
+
:,
|
| 732 |
+
k : k + 1,
|
| 733 |
+
i : i + tile_latent_min_height,
|
| 734 |
+
j : j + tile_latent_min_width,
|
| 735 |
+
]
|
| 736 |
+
tile = self.conv2(tile)
|
| 737 |
+
decoded = self.decoder(
|
| 738 |
+
tile, feat_cache=self._feat_map, feat_idx=self._conv_idx
|
| 739 |
+
)
|
| 740 |
+
time.append(decoded)
|
| 741 |
+
row.append(torch.cat(time, dim=2))
|
| 742 |
+
rows.append(row)
|
| 743 |
+
self.clear_cache()
|
| 744 |
+
|
| 745 |
+
result_rows = []
|
| 746 |
+
for i, row in enumerate(rows):
|
| 747 |
+
result_row = []
|
| 748 |
+
for j, tile in enumerate(row):
|
| 749 |
+
# blend the above tile and the left tile
|
| 750 |
+
# to the current tile and add the current tile to the result row
|
| 751 |
+
if i > 0:
|
| 752 |
+
tile = self.blend_v(rows[i - 1][j], tile, blend_height)
|
| 753 |
+
if j > 0:
|
| 754 |
+
tile = self.blend_h(row[j - 1], tile, blend_width)
|
| 755 |
+
result_row.append(
|
| 756 |
+
tile[
|
| 757 |
+
:,
|
| 758 |
+
:,
|
| 759 |
+
:,
|
| 760 |
+
: self.tile_sample_stride_height,
|
| 761 |
+
: self.tile_sample_stride_width,
|
| 762 |
+
]
|
| 763 |
+
)
|
| 764 |
+
result_rows.append(torch.cat(result_row, dim=-1))
|
| 765 |
+
|
| 766 |
+
dec = torch.cat(result_rows, dim=3)[:, :, :, :sample_height, :sample_width]
|
| 767 |
+
|
| 768 |
+
return dec
|
| 769 |
+
|
| 770 |
+
def encode(self, x, scale, return_mu=False):
|
| 771 |
+
self.clear_cache()
|
| 772 |
+
## cache
|
| 773 |
+
t = x.shape[2]
|
| 774 |
+
iter_ = 1 + (t - 1) // 4
|
| 775 |
+
for i in range(iter_):
|
| 776 |
+
self._enc_conv_idx = [0]
|
| 777 |
+
if i == 0:
|
| 778 |
+
out = self.encoder(
|
| 779 |
+
x[:, :, :1, :, :],
|
| 780 |
+
feat_cache=self._enc_feat_map,
|
| 781 |
+
feat_idx=self._enc_conv_idx,
|
| 782 |
+
)
|
| 783 |
+
else:
|
| 784 |
+
out_ = self.encoder(
|
| 785 |
+
x[:, :, 1 + 4 * (i - 1) : 1 + 4 * i, :, :],
|
| 786 |
+
feat_cache=self._enc_feat_map,
|
| 787 |
+
feat_idx=self._enc_conv_idx,
|
| 788 |
+
)
|
| 789 |
+
out = torch.cat([out, out_], 2)
|
| 790 |
+
mu, log_var = self.conv1(out).chunk(2, dim=1)
|
| 791 |
+
if isinstance(scale[0], torch.Tensor):
|
| 792 |
+
mu = (mu - scale[0].view(1, self.z_dim, 1, 1, 1)) * scale[1].view(
|
| 793 |
+
1, self.z_dim, 1, 1, 1
|
| 794 |
+
)
|
| 795 |
+
else:
|
| 796 |
+
mu = (mu - scale[0]) * scale[1]
|
| 797 |
+
|
| 798 |
+
self.clear_cache()
|
| 799 |
+
if return_mu:
|
| 800 |
+
return mu, log_var
|
| 801 |
+
else:
|
| 802 |
+
return mu
|
| 803 |
+
|
| 804 |
+
def decode(self, z, scale):
|
| 805 |
+
self.clear_cache()
|
| 806 |
+
|
| 807 |
+
# z: [b,c,t,h,w]
|
| 808 |
+
if isinstance(scale[0], torch.Tensor):
|
| 809 |
+
z = z / scale[1].view(1, self.z_dim, 1, 1, 1) + scale[0].view(
|
| 810 |
+
1, self.z_dim, 1, 1, 1
|
| 811 |
+
)
|
| 812 |
+
else:
|
| 813 |
+
z = z / scale[1] + scale[0]
|
| 814 |
+
iter_ = z.shape[2]
|
| 815 |
+
x = self.conv2(z)
|
| 816 |
+
for i in range(iter_):
|
| 817 |
+
self._conv_idx = [0]
|
| 818 |
+
if i == 0:
|
| 819 |
+
out = self.decoder(
|
| 820 |
+
x[:, :, i : i + 1, :, :],
|
| 821 |
+
feat_cache=self._feat_map,
|
| 822 |
+
feat_idx=self._conv_idx,
|
| 823 |
+
)
|
| 824 |
+
else:
|
| 825 |
+
out_ = self.decoder(
|
| 826 |
+
x[:, :, i : i + 1, :, :],
|
| 827 |
+
feat_cache=self._feat_map,
|
| 828 |
+
feat_idx=self._conv_idx,
|
| 829 |
+
)
|
| 830 |
+
out = torch.cat([out, out_], 2)
|
| 831 |
+
|
| 832 |
+
self.clear_cache()
|
| 833 |
+
return out
|
| 834 |
+
|
| 835 |
+
def decode_stream(self, z, scale):
|
| 836 |
+
self.clear_cache()
|
| 837 |
+
|
| 838 |
+
# z: [b,c,t,h,w]
|
| 839 |
+
if isinstance(scale[0], torch.Tensor):
|
| 840 |
+
z = z / scale[1].view(1, self.z_dim, 1, 1, 1) + scale[0].view(
|
| 841 |
+
1, self.z_dim, 1, 1, 1
|
| 842 |
+
)
|
| 843 |
+
else:
|
| 844 |
+
z = z / scale[1] + scale[0]
|
| 845 |
+
iter_ = z.shape[2]
|
| 846 |
+
x = self.conv2(z)
|
| 847 |
+
for i in range(iter_):
|
| 848 |
+
self._conv_idx = [0]
|
| 849 |
+
out = self.decoder(
|
| 850 |
+
x[:, :, i : i + 1, :, :],
|
| 851 |
+
feat_cache=self._feat_map,
|
| 852 |
+
feat_idx=self._conv_idx,
|
| 853 |
+
)
|
| 854 |
+
yield out
|
| 855 |
+
|
| 856 |
+
def cached_decode(self, z, scale):
|
| 857 |
+
# z: [b,c,t,h,w]
|
| 858 |
+
if isinstance(scale[0], torch.Tensor):
|
| 859 |
+
z = z / scale[1].view(1, self.z_dim, 1, 1, 1) + scale[0].view(
|
| 860 |
+
1, self.z_dim, 1, 1, 1
|
| 861 |
+
)
|
| 862 |
+
else:
|
| 863 |
+
z = z / scale[1] + scale[0]
|
| 864 |
+
iter_ = z.shape[2]
|
| 865 |
+
x = self.conv2(z)
|
| 866 |
+
for i in range(iter_):
|
| 867 |
+
self._conv_idx = [0]
|
| 868 |
+
if i == 0:
|
| 869 |
+
out = self.decoder(
|
| 870 |
+
x[:, :, i : i + 1, :, :],
|
| 871 |
+
feat_cache=self._feat_map,
|
| 872 |
+
feat_idx=self._conv_idx,
|
| 873 |
+
)
|
| 874 |
+
else:
|
| 875 |
+
out_ = self.decoder(
|
| 876 |
+
x[:, :, i : i + 1, :, :],
|
| 877 |
+
feat_cache=self._feat_map,
|
| 878 |
+
feat_idx=self._conv_idx,
|
| 879 |
+
)
|
| 880 |
+
out = torch.cat([out, out_], 2)
|
| 881 |
+
return out
|
| 882 |
+
|
| 883 |
+
def reparameterize(self, mu, log_var):
|
| 884 |
+
std = torch.exp(0.5 * log_var)
|
| 885 |
+
eps = torch.randn_like(std)
|
| 886 |
+
return eps * std + mu
|
| 887 |
+
|
| 888 |
+
def sample(self, imgs, deterministic=False, scale=[0, 1]):
|
| 889 |
+
mu, log_var = self.encode(imgs, scale, return_mu=True)
|
| 890 |
+
if deterministic:
|
| 891 |
+
return mu
|
| 892 |
+
std = torch.exp(0.5 * log_var.clamp(-30.0, 20.0))
|
| 893 |
+
return mu + std * torch.randn_like(std), mu, log_var
|
| 894 |
+
|
| 895 |
+
def clear_cache(self):
|
| 896 |
+
self._conv_num = count_conv3d(self.decoder)
|
| 897 |
+
self._conv_idx = [0]
|
| 898 |
+
self._feat_map = [None] * self._conv_num
|
| 899 |
+
# cache encode
|
| 900 |
+
self._enc_conv_num = count_conv3d(self.encoder)
|
| 901 |
+
self._enc_conv_idx = [0]
|
| 902 |
+
self._enc_feat_map = [None] * self._enc_conv_num
|
| 903 |
+
|
| 904 |
+
def encode_video(self, x, scale=[0, 1]):
|
| 905 |
+
assert x.ndim == 5 # NTCHW
|
| 906 |
+
assert x.shape[2] % 3 == 0
|
| 907 |
+
x = x.transpose(1, 2)
|
| 908 |
+
y = x.mul(2).sub_(1)
|
| 909 |
+
y, mu, log_var = self.sample(y, scale=scale)
|
| 910 |
+
return y.transpose(1, 2).to(x), mu, log_var
|
| 911 |
+
|
| 912 |
+
def decode_video(self, x, scale=[0, 1]):
|
| 913 |
+
assert x.ndim == 5 # NTCHW
|
| 914 |
+
assert x.shape[2] % self.z_dim == 0
|
| 915 |
+
x = x.transpose(1, 2)
|
| 916 |
+
# B, C, T, H, W
|
| 917 |
+
y = x
|
| 918 |
+
y = self.decode(y, scale).clamp_(-1, 1)
|
| 919 |
+
y = y.mul_(0.5).add_(0.5).clamp_(0, 1) # NCTHW
|
| 920 |
+
return y.transpose(1, 2).to(x)
|
| 921 |
+
|
| 922 |
+
|
| 923 |
+
def _video_vae(
|
| 924 |
+
pretrained_path=None,
|
| 925 |
+
z_dim=None,
|
| 926 |
+
device="cpu",
|
| 927 |
+
dtype=torch.float,
|
| 928 |
+
**kwargs,
|
| 929 |
+
):
|
| 930 |
+
"""
|
| 931 |
+
Autoencoder3d adapted from Stable Diffusion 1.x, 2.x and XL.
|
| 932 |
+
"""
|
| 933 |
+
# params
|
| 934 |
+
cfg = dict(
|
| 935 |
+
dim=96,
|
| 936 |
+
z_dim=z_dim,
|
| 937 |
+
dim_mult=[1, 2, 4, 4],
|
| 938 |
+
num_res_blocks=2,
|
| 939 |
+
attn_scales=[],
|
| 940 |
+
temperal_downsample=[False, True, True],
|
| 941 |
+
dropout=0.0,
|
| 942 |
+
)
|
| 943 |
+
cfg.update(**kwargs)
|
| 944 |
+
|
| 945 |
+
# init model
|
| 946 |
+
with torch.device("meta"):
|
| 947 |
+
model = WanVAE_(**cfg)
|
| 948 |
+
|
| 949 |
+
# load checkpoint
|
| 950 |
+
model.load_state_dict(torch.load(pretrained_path, map_location=device), assign=True)
|
| 951 |
+
|
| 952 |
+
return model
|
| 953 |
+
|
| 954 |
+
class WanVAE:
|
| 955 |
+
def __init__(
|
| 956 |
+
self,
|
| 957 |
+
z_dim=16,
|
| 958 |
+
vae_path="cache/vae_step_411000.pth",
|
| 959 |
+
dtype=torch.float,
|
| 960 |
+
device="cuda",
|
| 961 |
+
parallel=False,
|
| 962 |
+
use_tiling=False,
|
| 963 |
+
use_2d_split=True,
|
| 964 |
+
):
|
| 965 |
+
self.dtype = dtype
|
| 966 |
+
self.device = device
|
| 967 |
+
self.parallel = parallel
|
| 968 |
+
self.use_tiling = use_tiling
|
| 969 |
+
self.use_2d_split = use_2d_split
|
| 970 |
+
|
| 971 |
+
mean = [
|
| 972 |
+
-0.7571,
|
| 973 |
+
-0.7089,
|
| 974 |
+
-0.9113,
|
| 975 |
+
0.1075,
|
| 976 |
+
-0.1745,
|
| 977 |
+
0.9653,
|
| 978 |
+
-0.1517,
|
| 979 |
+
1.5508,
|
| 980 |
+
0.4134,
|
| 981 |
+
-0.0715,
|
| 982 |
+
0.5517,
|
| 983 |
+
-0.3632,
|
| 984 |
+
-0.1922,
|
| 985 |
+
-0.9497,
|
| 986 |
+
0.2503,
|
| 987 |
+
-0.2921,
|
| 988 |
+
]
|
| 989 |
+
std = [
|
| 990 |
+
2.8184,
|
| 991 |
+
1.4541,
|
| 992 |
+
2.3275,
|
| 993 |
+
2.6558,
|
| 994 |
+
1.2196,
|
| 995 |
+
1.7708,
|
| 996 |
+
2.6052,
|
| 997 |
+
2.0743,
|
| 998 |
+
3.2687,
|
| 999 |
+
2.1526,
|
| 1000 |
+
2.8652,
|
| 1001 |
+
1.5579,
|
| 1002 |
+
1.6382,
|
| 1003 |
+
1.1253,
|
| 1004 |
+
2.8251,
|
| 1005 |
+
1.9160,
|
| 1006 |
+
]
|
| 1007 |
+
self.mean = torch.tensor(mean, dtype=dtype, device=device)
|
| 1008 |
+
self.inv_std = 1.0 / torch.tensor(std, dtype=dtype, device=device)
|
| 1009 |
+
self.scale = [self.mean, self.inv_std]
|
| 1010 |
+
|
| 1011 |
+
# (height, width, world_size) -> (world_size_h, world_size_w)
|
| 1012 |
+
self.grid_table = {
|
| 1013 |
+
# world_size = 2
|
| 1014 |
+
(60, 104, 2): (1, 2),
|
| 1015 |
+
(68, 120, 2): (1, 2),
|
| 1016 |
+
(90, 160, 2): (1, 2),
|
| 1017 |
+
(60, 60, 2): (1, 2),
|
| 1018 |
+
(72, 72, 2): (1, 2),
|
| 1019 |
+
(88, 88, 2): (1, 2),
|
| 1020 |
+
(120, 120, 2): (1, 2),
|
| 1021 |
+
(104, 60, 2): (2, 1),
|
| 1022 |
+
(120, 68, 2): (2, 1),
|
| 1023 |
+
(160, 90, 2): (2, 1),
|
| 1024 |
+
# world_size = 4
|
| 1025 |
+
(60, 104, 4): (2, 2),
|
| 1026 |
+
(68, 120, 4): (2, 2),
|
| 1027 |
+
(90, 160, 4): (2, 2),
|
| 1028 |
+
(60, 60, 4): (2, 2),
|
| 1029 |
+
(72, 72, 4): (2, 2),
|
| 1030 |
+
(88, 88, 4): (2, 2),
|
| 1031 |
+
(120, 120, 4): (2, 2),
|
| 1032 |
+
(104, 60, 4): (2, 2),
|
| 1033 |
+
(120, 68, 4): (2, 2),
|
| 1034 |
+
(160, 90, 4): (2, 2),
|
| 1035 |
+
# world_size = 8
|
| 1036 |
+
(60, 104, 8): (2, 4),
|
| 1037 |
+
(68, 120, 8): (2, 4),
|
| 1038 |
+
(90, 160, 8): (2, 4),
|
| 1039 |
+
(60, 60, 8): (2, 4),
|
| 1040 |
+
(72, 72, 8): (2, 4),
|
| 1041 |
+
(88, 88, 8): (2, 4),
|
| 1042 |
+
(120, 120, 8): (2, 4),
|
| 1043 |
+
(104, 60, 8): (4, 2),
|
| 1044 |
+
(120, 68, 8): (4, 2),
|
| 1045 |
+
(160, 90, 8): (4, 2),
|
| 1046 |
+
}
|
| 1047 |
+
|
| 1048 |
+
# init model
|
| 1049 |
+
self.model = (
|
| 1050 |
+
_video_vae(
|
| 1051 |
+
pretrained_path=vae_path,
|
| 1052 |
+
z_dim=z_dim,
|
| 1053 |
+
dtype=dtype,
|
| 1054 |
+
)
|
| 1055 |
+
.eval()
|
| 1056 |
+
.requires_grad_(False)
|
| 1057 |
+
.to(device)
|
| 1058 |
+
.to(dtype)
|
| 1059 |
+
)
|
| 1060 |
+
|
| 1061 |
+
def _calculate_2d_grid(self, latent_height, latent_width, world_size):
|
| 1062 |
+
if (latent_height, latent_width, world_size) in self.grid_table:
|
| 1063 |
+
best_h, best_w = self.grid_table[(latent_height, latent_width, world_size)]
|
| 1064 |
+
# logger.info(f"Vae using cached 2D grid: {best_h}x{best_w} grid for {latent_height}x{latent_width} latent")
|
| 1065 |
+
return best_h, best_w
|
| 1066 |
+
|
| 1067 |
+
best_h, best_w = 1, world_size
|
| 1068 |
+
min_aspect_diff = float("inf")
|
| 1069 |
+
|
| 1070 |
+
for h in range(1, world_size + 1):
|
| 1071 |
+
if world_size % h == 0:
|
| 1072 |
+
w = world_size // h
|
| 1073 |
+
if latent_height % h == 0 and latent_width % w == 0:
|
| 1074 |
+
# Calculate how close this grid is to square
|
| 1075 |
+
aspect_diff = abs((latent_height / h) - (latent_width / w))
|
| 1076 |
+
if aspect_diff < min_aspect_diff:
|
| 1077 |
+
min_aspect_diff = aspect_diff
|
| 1078 |
+
best_h, best_w = h, w
|
| 1079 |
+
# logger.info(f"Vae using 2D grid & Update cache: {best_h}x{best_w} grid for {latent_height}x{latent_width} latent")
|
| 1080 |
+
self.grid_table[(latent_height, latent_width, world_size)] = (best_h, best_w)
|
| 1081 |
+
return best_h, best_w
|
| 1082 |
+
|
| 1083 |
+
def current_device(self):
|
| 1084 |
+
return next(self.model.parameters()).device
|
| 1085 |
+
|
| 1086 |
+
def encode_dist(self, video, world_size, cur_rank, split_dim):
|
| 1087 |
+
spatial_ratio = 8
|
| 1088 |
+
|
| 1089 |
+
if split_dim == 3:
|
| 1090 |
+
total_latent_len = video.shape[3] // spatial_ratio
|
| 1091 |
+
elif split_dim == 4:
|
| 1092 |
+
total_latent_len = video.shape[4] // spatial_ratio
|
| 1093 |
+
else:
|
| 1094 |
+
raise ValueError(f"Unsupported split_dim: {split_dim}")
|
| 1095 |
+
|
| 1096 |
+
splited_chunk_len = total_latent_len // world_size
|
| 1097 |
+
padding_size = 1
|
| 1098 |
+
|
| 1099 |
+
video_chunk_len = splited_chunk_len * spatial_ratio
|
| 1100 |
+
video_padding_len = padding_size * spatial_ratio
|
| 1101 |
+
|
| 1102 |
+
if cur_rank == 0:
|
| 1103 |
+
if split_dim == 3:
|
| 1104 |
+
video_chunk = video[
|
| 1105 |
+
:, :, :, : video_chunk_len + 2 * video_padding_len, :
|
| 1106 |
+
].contiguous()
|
| 1107 |
+
elif split_dim == 4:
|
| 1108 |
+
video_chunk = video[
|
| 1109 |
+
:, :, :, :, : video_chunk_len + 2 * video_padding_len
|
| 1110 |
+
].contiguous()
|
| 1111 |
+
elif cur_rank == world_size - 1:
|
| 1112 |
+
if split_dim == 3:
|
| 1113 |
+
video_chunk = video[
|
| 1114 |
+
:, :, :, -(video_chunk_len + 2 * video_padding_len) :, :
|
| 1115 |
+
].contiguous()
|
| 1116 |
+
elif split_dim == 4:
|
| 1117 |
+
video_chunk = video[
|
| 1118 |
+
:, :, :, :, -(video_chunk_len + 2 * video_padding_len) :
|
| 1119 |
+
].contiguous()
|
| 1120 |
+
else:
|
| 1121 |
+
start_idx = cur_rank * video_chunk_len - video_padding_len
|
| 1122 |
+
end_idx = (cur_rank + 1) * video_chunk_len + video_padding_len
|
| 1123 |
+
if split_dim == 3:
|
| 1124 |
+
video_chunk = video[:, :, :, start_idx:end_idx, :].contiguous()
|
| 1125 |
+
elif split_dim == 4:
|
| 1126 |
+
video_chunk = video[:, :, :, :, start_idx:end_idx].contiguous()
|
| 1127 |
+
|
| 1128 |
+
if self.use_tiling:
|
| 1129 |
+
encoded_chunk = self.model.tiled_encode(video_chunk, self.scale)
|
| 1130 |
+
else:
|
| 1131 |
+
encoded_chunk = self.model.encode(video_chunk, self.scale)
|
| 1132 |
+
|
| 1133 |
+
if cur_rank == 0:
|
| 1134 |
+
if split_dim == 3:
|
| 1135 |
+
encoded_chunk = encoded_chunk[
|
| 1136 |
+
:, :, :, :splited_chunk_len, :
|
| 1137 |
+
].contiguous()
|
| 1138 |
+
elif split_dim == 4:
|
| 1139 |
+
encoded_chunk = encoded_chunk[
|
| 1140 |
+
:, :, :, :, :splited_chunk_len
|
| 1141 |
+
].contiguous()
|
| 1142 |
+
elif cur_rank == world_size - 1:
|
| 1143 |
+
if split_dim == 3:
|
| 1144 |
+
encoded_chunk = encoded_chunk[
|
| 1145 |
+
:, :, :, -splited_chunk_len:, :
|
| 1146 |
+
].contiguous()
|
| 1147 |
+
elif split_dim == 4:
|
| 1148 |
+
encoded_chunk = encoded_chunk[
|
| 1149 |
+
:, :, :, :, -splited_chunk_len:
|
| 1150 |
+
].contiguous()
|
| 1151 |
+
else:
|
| 1152 |
+
if split_dim == 3:
|
| 1153 |
+
encoded_chunk = encoded_chunk[
|
| 1154 |
+
:, :, :, padding_size:-padding_size, :
|
| 1155 |
+
].contiguous()
|
| 1156 |
+
elif split_dim == 4:
|
| 1157 |
+
encoded_chunk = encoded_chunk[
|
| 1158 |
+
:, :, :, :, padding_size:-padding_size
|
| 1159 |
+
].contiguous()
|
| 1160 |
+
|
| 1161 |
+
full_encoded = [torch.empty_like(encoded_chunk) for _ in range(world_size)]
|
| 1162 |
+
dist.all_gather(full_encoded, encoded_chunk)
|
| 1163 |
+
|
| 1164 |
+
torch.cuda.synchronize()
|
| 1165 |
+
|
| 1166 |
+
encoded = torch.cat(full_encoded, dim=split_dim)
|
| 1167 |
+
|
| 1168 |
+
return encoded.squeeze(0)
|
| 1169 |
+
|
| 1170 |
+
def encode_dist_2d(self, video, world_size_h, world_size_w, cur_rank_h, cur_rank_w):
|
| 1171 |
+
spatial_ratio = 8
|
| 1172 |
+
|
| 1173 |
+
# Calculate chunk sizes for both dimensions
|
| 1174 |
+
total_latent_h = video.shape[3] // spatial_ratio
|
| 1175 |
+
total_latent_w = video.shape[4] // spatial_ratio
|
| 1176 |
+
|
| 1177 |
+
chunk_h = total_latent_h // world_size_h
|
| 1178 |
+
chunk_w = total_latent_w // world_size_w
|
| 1179 |
+
|
| 1180 |
+
padding_size = 1
|
| 1181 |
+
video_chunk_h = chunk_h * spatial_ratio
|
| 1182 |
+
video_chunk_w = chunk_w * spatial_ratio
|
| 1183 |
+
video_padding_h = padding_size * spatial_ratio
|
| 1184 |
+
video_padding_w = padding_size * spatial_ratio
|
| 1185 |
+
|
| 1186 |
+
# Calculate H dimension slice
|
| 1187 |
+
if cur_rank_h == 0:
|
| 1188 |
+
h_start = 0
|
| 1189 |
+
h_end = video_chunk_h + 2 * video_padding_h
|
| 1190 |
+
elif cur_rank_h == world_size_h - 1:
|
| 1191 |
+
h_start = video.shape[3] - (video_chunk_h + 2 * video_padding_h)
|
| 1192 |
+
h_end = video.shape[3]
|
| 1193 |
+
else:
|
| 1194 |
+
h_start = cur_rank_h * video_chunk_h - video_padding_h
|
| 1195 |
+
h_end = (cur_rank_h + 1) * video_chunk_h + video_padding_h
|
| 1196 |
+
|
| 1197 |
+
# Calculate W dimension slice
|
| 1198 |
+
if cur_rank_w == 0:
|
| 1199 |
+
w_start = 0
|
| 1200 |
+
w_end = video_chunk_w + 2 * video_padding_w
|
| 1201 |
+
elif cur_rank_w == world_size_w - 1:
|
| 1202 |
+
w_start = video.shape[4] - (video_chunk_w + 2 * video_padding_w)
|
| 1203 |
+
w_end = video.shape[4]
|
| 1204 |
+
else:
|
| 1205 |
+
w_start = cur_rank_w * video_chunk_w - video_padding_w
|
| 1206 |
+
w_end = (cur_rank_w + 1) * video_chunk_w + video_padding_w
|
| 1207 |
+
|
| 1208 |
+
# Extract the video chunk for this process
|
| 1209 |
+
video_chunk = video[:, :, :, h_start:h_end, w_start:w_end].contiguous()
|
| 1210 |
+
|
| 1211 |
+
# Encode the chunk
|
| 1212 |
+
if self.use_tiling:
|
| 1213 |
+
encoded_chunk = self.model.tiled_encode(video_chunk, self.scale)
|
| 1214 |
+
else:
|
| 1215 |
+
encoded_chunk = self.model.encode(video_chunk, self.scale)
|
| 1216 |
+
|
| 1217 |
+
# Remove padding from encoded chunk
|
| 1218 |
+
if cur_rank_h == 0:
|
| 1219 |
+
encoded_h_start = 0
|
| 1220 |
+
encoded_h_end = chunk_h
|
| 1221 |
+
elif cur_rank_h == world_size_h - 1:
|
| 1222 |
+
encoded_h_start = encoded_chunk.shape[3] - chunk_h
|
| 1223 |
+
encoded_h_end = encoded_chunk.shape[3]
|
| 1224 |
+
else:
|
| 1225 |
+
encoded_h_start = padding_size
|
| 1226 |
+
encoded_h_end = encoded_chunk.shape[3] - padding_size
|
| 1227 |
+
|
| 1228 |
+
if cur_rank_w == 0:
|
| 1229 |
+
encoded_w_start = 0
|
| 1230 |
+
encoded_w_end = chunk_w
|
| 1231 |
+
elif cur_rank_w == world_size_w - 1:
|
| 1232 |
+
encoded_w_start = encoded_chunk.shape[4] - chunk_w
|
| 1233 |
+
encoded_w_end = encoded_chunk.shape[4]
|
| 1234 |
+
else:
|
| 1235 |
+
encoded_w_start = padding_size
|
| 1236 |
+
encoded_w_end = encoded_chunk.shape[4] - padding_size
|
| 1237 |
+
|
| 1238 |
+
encoded_chunk = encoded_chunk[
|
| 1239 |
+
:, :, :, encoded_h_start:encoded_h_end, encoded_w_start:encoded_w_end
|
| 1240 |
+
].contiguous()
|
| 1241 |
+
|
| 1242 |
+
# Gather all chunks
|
| 1243 |
+
total_processes = world_size_h * world_size_w
|
| 1244 |
+
full_encoded = [torch.empty_like(encoded_chunk) for _ in range(total_processes)]
|
| 1245 |
+
|
| 1246 |
+
dist.all_gather(full_encoded, encoded_chunk)
|
| 1247 |
+
|
| 1248 |
+
torch.cuda.synchronize()
|
| 1249 |
+
|
| 1250 |
+
# Reconstruct the full encoded tensor
|
| 1251 |
+
encoded_rows = []
|
| 1252 |
+
for h_idx in range(world_size_h):
|
| 1253 |
+
encoded_cols = []
|
| 1254 |
+
for w_idx in range(world_size_w):
|
| 1255 |
+
process_idx = h_idx * world_size_w + w_idx
|
| 1256 |
+
encoded_cols.append(full_encoded[process_idx])
|
| 1257 |
+
encoded_rows.append(torch.cat(encoded_cols, dim=4))
|
| 1258 |
+
|
| 1259 |
+
encoded = torch.cat(encoded_rows, dim=3)
|
| 1260 |
+
|
| 1261 |
+
return encoded.squeeze(0)
|
| 1262 |
+
|
| 1263 |
+
def encode(self, video, world_size_h=None, world_size_w=None):
|
| 1264 |
+
"""
|
| 1265 |
+
video: one video with shape [1, C, T, H, W].
|
| 1266 |
+
"""
|
| 1267 |
+
if self.parallel:
|
| 1268 |
+
world_size = dist.get_world_size()
|
| 1269 |
+
cur_rank = dist.get_rank()
|
| 1270 |
+
height, width = video.shape[3], video.shape[4]
|
| 1271 |
+
|
| 1272 |
+
if self.use_2d_split:
|
| 1273 |
+
if world_size_h is None or world_size_w is None:
|
| 1274 |
+
world_size_h, world_size_w = self._calculate_2d_grid(
|
| 1275 |
+
height // 8, width // 8, world_size
|
| 1276 |
+
)
|
| 1277 |
+
cur_rank_h = cur_rank // world_size_w
|
| 1278 |
+
cur_rank_w = cur_rank % world_size_w
|
| 1279 |
+
out = self.encode_dist_2d(
|
| 1280 |
+
video, world_size_h, world_size_w, cur_rank_h, cur_rank_w
|
| 1281 |
+
)
|
| 1282 |
+
else:
|
| 1283 |
+
# Original 1D splitting logic
|
| 1284 |
+
if width % world_size == 0:
|
| 1285 |
+
out = self.encode_dist(video, world_size, cur_rank, split_dim=4)
|
| 1286 |
+
elif height % world_size == 0:
|
| 1287 |
+
out = self.encode_dist(video, world_size, cur_rank, split_dim=3)
|
| 1288 |
+
else:
|
| 1289 |
+
logger.info("Fall back to naive encode mode")
|
| 1290 |
+
if self.use_tiling:
|
| 1291 |
+
out = self.model.tiled_encode(video, self.scale).squeeze(0)
|
| 1292 |
+
else:
|
| 1293 |
+
out = self.model.encode(video, self.scale).squeeze(0)
|
| 1294 |
+
else:
|
| 1295 |
+
if self.use_tiling:
|
| 1296 |
+
out = self.model.tiled_encode(video, self.scale).squeeze(0)
|
| 1297 |
+
else:
|
| 1298 |
+
out = self.model.encode(video, self.scale).squeeze(0)
|
| 1299 |
+
|
| 1300 |
+
return out
|
| 1301 |
+
|
| 1302 |
+
def decode_dist(self, zs, world_size, cur_rank, split_dim):
|
| 1303 |
+
splited_total_len = zs.shape[split_dim]
|
| 1304 |
+
splited_chunk_len = splited_total_len // world_size
|
| 1305 |
+
padding_size = 1
|
| 1306 |
+
|
| 1307 |
+
if cur_rank == 0:
|
| 1308 |
+
if split_dim == 2:
|
| 1309 |
+
zs = zs[:, :, : splited_chunk_len + 2 * padding_size, :].contiguous()
|
| 1310 |
+
elif split_dim == 3:
|
| 1311 |
+
zs = zs[:, :, :, : splited_chunk_len + 2 * padding_size].contiguous()
|
| 1312 |
+
elif cur_rank == world_size - 1:
|
| 1313 |
+
if split_dim == 2:
|
| 1314 |
+
zs = zs[:, :, -(splited_chunk_len + 2 * padding_size) :, :].contiguous()
|
| 1315 |
+
elif split_dim == 3:
|
| 1316 |
+
zs = zs[:, :, :, -(splited_chunk_len + 2 * padding_size) :].contiguous()
|
| 1317 |
+
else:
|
| 1318 |
+
if split_dim == 2:
|
| 1319 |
+
zs = zs[
|
| 1320 |
+
:,
|
| 1321 |
+
:,
|
| 1322 |
+
cur_rank * splited_chunk_len - padding_size : (cur_rank + 1)
|
| 1323 |
+
* splited_chunk_len
|
| 1324 |
+
+ padding_size,
|
| 1325 |
+
:,
|
| 1326 |
+
].contiguous()
|
| 1327 |
+
elif split_dim == 3:
|
| 1328 |
+
zs = zs[
|
| 1329 |
+
:,
|
| 1330 |
+
:,
|
| 1331 |
+
:,
|
| 1332 |
+
cur_rank * splited_chunk_len - padding_size : (cur_rank + 1)
|
| 1333 |
+
* splited_chunk_len
|
| 1334 |
+
+ padding_size,
|
| 1335 |
+
].contiguous()
|
| 1336 |
+
|
| 1337 |
+
decode_func = self.model.tiled_decode if self.use_tiling else self.model.decode
|
| 1338 |
+
images = decode_func(zs.unsqueeze(0), self.scale).clamp_(-1, 1)
|
| 1339 |
+
|
| 1340 |
+
if cur_rank == 0:
|
| 1341 |
+
if split_dim == 2:
|
| 1342 |
+
images = images[:, :, :, : splited_chunk_len * 8, :].contiguous()
|
| 1343 |
+
elif split_dim == 3:
|
| 1344 |
+
images = images[:, :, :, :, : splited_chunk_len * 8].contiguous()
|
| 1345 |
+
elif cur_rank == world_size - 1:
|
| 1346 |
+
if split_dim == 2:
|
| 1347 |
+
images = images[:, :, :, -splited_chunk_len * 8 :, :].contiguous()
|
| 1348 |
+
elif split_dim == 3:
|
| 1349 |
+
images = images[:, :, :, :, -splited_chunk_len * 8 :].contiguous()
|
| 1350 |
+
else:
|
| 1351 |
+
if split_dim == 2:
|
| 1352 |
+
images = images[
|
| 1353 |
+
:, :, :, 8 * padding_size : -8 * padding_size, :
|
| 1354 |
+
].contiguous()
|
| 1355 |
+
elif split_dim == 3:
|
| 1356 |
+
images = images[
|
| 1357 |
+
:, :, :, :, 8 * padding_size : -8 * padding_size
|
| 1358 |
+
].contiguous()
|
| 1359 |
+
|
| 1360 |
+
full_images = [torch.empty_like(images) for _ in range(world_size)]
|
| 1361 |
+
dist.all_gather(full_images, images)
|
| 1362 |
+
|
| 1363 |
+
torch.cuda.synchronize()
|
| 1364 |
+
|
| 1365 |
+
images = torch.cat(full_images, dim=split_dim + 1)
|
| 1366 |
+
|
| 1367 |
+
return images
|
| 1368 |
+
|
| 1369 |
+
def decode_dist_2d(self, zs, world_size_h, world_size_w, cur_rank_h, cur_rank_w):
|
| 1370 |
+
total_h = zs.shape[2]
|
| 1371 |
+
total_w = zs.shape[3]
|
| 1372 |
+
|
| 1373 |
+
chunk_h = total_h // world_size_h
|
| 1374 |
+
chunk_w = total_w // world_size_w
|
| 1375 |
+
|
| 1376 |
+
padding_size = 2
|
| 1377 |
+
|
| 1378 |
+
# Calculate H dimension slice
|
| 1379 |
+
if cur_rank_h == 0:
|
| 1380 |
+
h_start = 0
|
| 1381 |
+
h_end = chunk_h + 2 * padding_size
|
| 1382 |
+
elif cur_rank_h == world_size_h - 1:
|
| 1383 |
+
h_start = total_h - (chunk_h + 2 * padding_size)
|
| 1384 |
+
h_end = total_h
|
| 1385 |
+
else:
|
| 1386 |
+
h_start = cur_rank_h * chunk_h - padding_size
|
| 1387 |
+
h_end = (cur_rank_h + 1) * chunk_h + padding_size
|
| 1388 |
+
|
| 1389 |
+
# Calculate W dimension slice
|
| 1390 |
+
if cur_rank_w == 0:
|
| 1391 |
+
w_start = 0
|
| 1392 |
+
w_end = chunk_w + 2 * padding_size
|
| 1393 |
+
elif cur_rank_w == world_size_w - 1:
|
| 1394 |
+
w_start = total_w - (chunk_w + 2 * padding_size)
|
| 1395 |
+
w_end = total_w
|
| 1396 |
+
else:
|
| 1397 |
+
w_start = cur_rank_w * chunk_w - padding_size
|
| 1398 |
+
w_end = (cur_rank_w + 1) * chunk_w + padding_size
|
| 1399 |
+
|
| 1400 |
+
# Extract the latent chunk for this process
|
| 1401 |
+
zs_chunk = zs[:, :, h_start:h_end, w_start:w_end].contiguous()
|
| 1402 |
+
|
| 1403 |
+
# Decode the chunk
|
| 1404 |
+
decode_func = self.model.tiled_decode if self.use_tiling else self.model.decode
|
| 1405 |
+
images_chunk = decode_func(zs_chunk.unsqueeze(0), self.scale).clamp_(-1, 1)
|
| 1406 |
+
|
| 1407 |
+
# Remove padding from decoded chunk
|
| 1408 |
+
spatial_ratio = 8
|
| 1409 |
+
if cur_rank_h == 0:
|
| 1410 |
+
decoded_h_start = 0
|
| 1411 |
+
decoded_h_end = chunk_h * spatial_ratio
|
| 1412 |
+
elif cur_rank_h == world_size_h - 1:
|
| 1413 |
+
decoded_h_start = images_chunk.shape[3] - chunk_h * spatial_ratio
|
| 1414 |
+
decoded_h_end = images_chunk.shape[3]
|
| 1415 |
+
else:
|
| 1416 |
+
decoded_h_start = padding_size * spatial_ratio
|
| 1417 |
+
decoded_h_end = images_chunk.shape[3] - padding_size * spatial_ratio
|
| 1418 |
+
|
| 1419 |
+
if cur_rank_w == 0:
|
| 1420 |
+
decoded_w_start = 0
|
| 1421 |
+
decoded_w_end = chunk_w * spatial_ratio
|
| 1422 |
+
elif cur_rank_w == world_size_w - 1:
|
| 1423 |
+
decoded_w_start = images_chunk.shape[4] - chunk_w * spatial_ratio
|
| 1424 |
+
decoded_w_end = images_chunk.shape[4]
|
| 1425 |
+
else:
|
| 1426 |
+
decoded_w_start = padding_size * spatial_ratio
|
| 1427 |
+
decoded_w_end = images_chunk.shape[4] - padding_size * spatial_ratio
|
| 1428 |
+
|
| 1429 |
+
images_chunk = images_chunk[
|
| 1430 |
+
:, :, :, decoded_h_start:decoded_h_end, decoded_w_start:decoded_w_end
|
| 1431 |
+
].contiguous()
|
| 1432 |
+
|
| 1433 |
+
# Gather all chunks
|
| 1434 |
+
total_processes = world_size_h * world_size_w
|
| 1435 |
+
full_images = [torch.empty_like(images_chunk) for _ in range(total_processes)]
|
| 1436 |
+
|
| 1437 |
+
dist.all_gather(full_images, images_chunk)
|
| 1438 |
+
|
| 1439 |
+
torch.cuda.synchronize()
|
| 1440 |
+
|
| 1441 |
+
# Reconstruct the full image tensor
|
| 1442 |
+
image_rows = []
|
| 1443 |
+
for h_idx in range(world_size_h):
|
| 1444 |
+
image_cols = []
|
| 1445 |
+
for w_idx in range(world_size_w):
|
| 1446 |
+
process_idx = h_idx * world_size_w + w_idx
|
| 1447 |
+
image_cols.append(full_images[process_idx])
|
| 1448 |
+
image_rows.append(torch.cat(image_cols, dim=4))
|
| 1449 |
+
|
| 1450 |
+
images = torch.cat(image_rows, dim=3)
|
| 1451 |
+
|
| 1452 |
+
return images
|
| 1453 |
+
|
| 1454 |
+
def decode_dist_2d_stream(
|
| 1455 |
+
self, zs, world_size_h, world_size_w, cur_rank_h, cur_rank_w
|
| 1456 |
+
):
|
| 1457 |
+
total_h = zs.shape[2]
|
| 1458 |
+
total_w = zs.shape[3]
|
| 1459 |
+
|
| 1460 |
+
chunk_h = total_h // world_size_h
|
| 1461 |
+
chunk_w = total_w // world_size_w
|
| 1462 |
+
|
| 1463 |
+
padding_size = 2
|
| 1464 |
+
|
| 1465 |
+
# Calculate H dimension slice
|
| 1466 |
+
if cur_rank_h == 0:
|
| 1467 |
+
h_start = 0
|
| 1468 |
+
h_end = chunk_h + 2 * padding_size
|
| 1469 |
+
elif cur_rank_h == world_size_h - 1:
|
| 1470 |
+
h_start = total_h - (chunk_h + 2 * padding_size)
|
| 1471 |
+
h_end = total_h
|
| 1472 |
+
else:
|
| 1473 |
+
h_start = cur_rank_h * chunk_h - padding_size
|
| 1474 |
+
h_end = (cur_rank_h + 1) * chunk_h + padding_size
|
| 1475 |
+
|
| 1476 |
+
# Calculate W dimension slice
|
| 1477 |
+
if cur_rank_w == 0:
|
| 1478 |
+
w_start = 0
|
| 1479 |
+
w_end = chunk_w + 2 * padding_size
|
| 1480 |
+
elif cur_rank_w == world_size_w - 1:
|
| 1481 |
+
w_start = total_w - (chunk_w + 2 * padding_size)
|
| 1482 |
+
w_end = total_w
|
| 1483 |
+
else:
|
| 1484 |
+
w_start = cur_rank_w * chunk_w - padding_size
|
| 1485 |
+
w_end = (cur_rank_w + 1) * chunk_w + padding_size
|
| 1486 |
+
|
| 1487 |
+
# Extract the latent chunk for this process
|
| 1488 |
+
zs_chunk = zs[:, :, h_start:h_end, w_start:w_end].contiguous()
|
| 1489 |
+
|
| 1490 |
+
for image in self.model.decode_stream(zs_chunk.unsqueeze(0), self.scale):
|
| 1491 |
+
images_chunk = image.clamp_(-1, 1)
|
| 1492 |
+
# Remove padding from decoded chunk
|
| 1493 |
+
spatial_ratio = 8
|
| 1494 |
+
if cur_rank_h == 0:
|
| 1495 |
+
decoded_h_start = 0
|
| 1496 |
+
decoded_h_end = chunk_h * spatial_ratio
|
| 1497 |
+
elif cur_rank_h == world_size_h - 1:
|
| 1498 |
+
decoded_h_start = images_chunk.shape[3] - chunk_h * spatial_ratio
|
| 1499 |
+
decoded_h_end = images_chunk.shape[3]
|
| 1500 |
+
else:
|
| 1501 |
+
decoded_h_start = padding_size * spatial_ratio
|
| 1502 |
+
decoded_h_end = images_chunk.shape[3] - padding_size * spatial_ratio
|
| 1503 |
+
|
| 1504 |
+
if cur_rank_w == 0:
|
| 1505 |
+
decoded_w_start = 0
|
| 1506 |
+
decoded_w_end = chunk_w * spatial_ratio
|
| 1507 |
+
elif cur_rank_w == world_size_w - 1:
|
| 1508 |
+
decoded_w_start = images_chunk.shape[4] - chunk_w * spatial_ratio
|
| 1509 |
+
decoded_w_end = images_chunk.shape[4]
|
| 1510 |
+
else:
|
| 1511 |
+
decoded_w_start = padding_size * spatial_ratio
|
| 1512 |
+
decoded_w_end = images_chunk.shape[4] - padding_size * spatial_ratio
|
| 1513 |
+
|
| 1514 |
+
images_chunk = images_chunk[
|
| 1515 |
+
:, :, :, decoded_h_start:decoded_h_end, decoded_w_start:decoded_w_end
|
| 1516 |
+
].contiguous()
|
| 1517 |
+
|
| 1518 |
+
# Gather all chunks
|
| 1519 |
+
total_processes = world_size_h * world_size_w
|
| 1520 |
+
full_images = [
|
| 1521 |
+
torch.empty_like(images_chunk) for _ in range(total_processes)
|
| 1522 |
+
]
|
| 1523 |
+
|
| 1524 |
+
dist.all_gather(full_images, images_chunk)
|
| 1525 |
+
|
| 1526 |
+
torch.cuda.synchronize()
|
| 1527 |
+
|
| 1528 |
+
# Reconstruct the full image tensor
|
| 1529 |
+
image_rows = []
|
| 1530 |
+
for h_idx in range(world_size_h):
|
| 1531 |
+
image_cols = []
|
| 1532 |
+
for w_idx in range(world_size_w):
|
| 1533 |
+
process_idx = h_idx * world_size_w + w_idx
|
| 1534 |
+
image_cols.append(full_images[process_idx])
|
| 1535 |
+
image_rows.append(torch.cat(image_cols, dim=4))
|
| 1536 |
+
|
| 1537 |
+
images = torch.cat(image_rows, dim=3)
|
| 1538 |
+
|
| 1539 |
+
yield images
|
| 1540 |
+
|
| 1541 |
+
def decode(self, zs):
|
| 1542 |
+
if self.parallel:
|
| 1543 |
+
world_size = dist.get_world_size()
|
| 1544 |
+
cur_rank = dist.get_rank()
|
| 1545 |
+
latent_height, latent_width = zs.shape[2], zs.shape[3]
|
| 1546 |
+
|
| 1547 |
+
if self.use_2d_split:
|
| 1548 |
+
world_size_h, world_size_w = self._calculate_2d_grid(
|
| 1549 |
+
latent_height, latent_width, world_size
|
| 1550 |
+
)
|
| 1551 |
+
cur_rank_h = cur_rank // world_size_w
|
| 1552 |
+
cur_rank_w = cur_rank % world_size_w
|
| 1553 |
+
images = self.decode_dist_2d(
|
| 1554 |
+
zs, world_size_h, world_size_w, cur_rank_h, cur_rank_w
|
| 1555 |
+
)
|
| 1556 |
+
else:
|
| 1557 |
+
# Original 1D splitting logic
|
| 1558 |
+
if latent_width % world_size == 0:
|
| 1559 |
+
images = self.decode_dist(zs, world_size, cur_rank, split_dim=3)
|
| 1560 |
+
elif latent_height % world_size == 0:
|
| 1561 |
+
images = self.decode_dist(zs, world_size, cur_rank, split_dim=2)
|
| 1562 |
+
else:
|
| 1563 |
+
logger.info("Fall back to naive decode mode")
|
| 1564 |
+
images = self.model.decode(zs.unsqueeze(0), self.scale).clamp_(
|
| 1565 |
+
-1, 1
|
| 1566 |
+
)
|
| 1567 |
+
else:
|
| 1568 |
+
decode_func = (
|
| 1569 |
+
self.model.tiled_decode if self.use_tiling else self.model.decode
|
| 1570 |
+
)
|
| 1571 |
+
images = decode_func(zs.unsqueeze(0), self.scale).clamp_(-1, 1)
|
| 1572 |
+
|
| 1573 |
+
return images
|
| 1574 |
+
|
| 1575 |
+
def decode_stream(self, zs):
|
| 1576 |
+
if self.parallel:
|
| 1577 |
+
world_size = dist.get_world_size()
|
| 1578 |
+
cur_rank = dist.get_rank()
|
| 1579 |
+
latent_height, latent_width = zs.shape[2], zs.shape[3]
|
| 1580 |
+
|
| 1581 |
+
world_size_h, world_size_w = self._calculate_2d_grid(
|
| 1582 |
+
latent_height, latent_width, world_size
|
| 1583 |
+
)
|
| 1584 |
+
cur_rank_h = cur_rank // world_size_w
|
| 1585 |
+
cur_rank_w = cur_rank % world_size_w
|
| 1586 |
+
for images in self.decode_dist_2d_stream(
|
| 1587 |
+
zs, world_size_h, world_size_w, cur_rank_h, cur_rank_w
|
| 1588 |
+
):
|
| 1589 |
+
yield images
|
| 1590 |
+
else:
|
| 1591 |
+
for image in self.model.decode_stream(zs.unsqueeze(0), self.scale):
|
| 1592 |
+
yield image.clamp_(-1, 1)
|
| 1593 |
+
|
| 1594 |
+
def encode_video(self, vid):
|
| 1595 |
+
return self.model.encode_video(vid)
|
| 1596 |
+
|
| 1597 |
+
def decode_video(self, vid_enc):
|
| 1598 |
+
return self.model.decode_video(vid_enc)
|
leaptalk_inference.py
ADDED
|
@@ -0,0 +1,1156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
import argparse
|
| 6 |
+
import contextlib
|
| 7 |
+
import glob
|
| 8 |
+
import os
|
| 9 |
+
import subprocess
|
| 10 |
+
import sys
|
| 11 |
+
import time
|
| 12 |
+
from dataclasses import dataclass
|
| 13 |
+
|
| 14 |
+
import imageio
|
| 15 |
+
import librosa
|
| 16 |
+
import numpy as np
|
| 17 |
+
import torch
|
| 18 |
+
import torch.distributed as dist
|
| 19 |
+
from loguru import logger
|
| 20 |
+
from peft import PeftModel
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
# `flash_head` and `vibt` are vendored next to this file inside the Space repo.
|
| 24 |
+
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 25 |
+
VIBT_ROOT = THIS_DIR
|
| 26 |
+
if THIS_DIR not in sys.path:
|
| 27 |
+
sys.path.insert(0, THIS_DIR)
|
| 28 |
+
|
| 29 |
+
from flash_head.src.pipeline.flash_head_pipeline import FlashHeadPipeline, timestep_transform # noqa: E402
|
| 30 |
+
from flash_head.utils.utils import match_and_blend_colors_torch # noqa: E402
|
| 31 |
+
from vibt.scheduler import ViBTScheduler # noqa: E402
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def _build_infer_timesteps(
|
| 35 |
+
*,
|
| 36 |
+
step_list: list[int] | None,
|
| 37 |
+
num_inference_steps: int,
|
| 38 |
+
shift_gamma: float,
|
| 39 |
+
device: str,
|
| 40 |
+
num_timesteps: int = 1000,
|
| 41 |
+
) -> torch.Tensor:
|
| 42 |
+
"""Return 1D float tensor of timesteps for ViBTScheduler.
|
| 43 |
+
|
| 44 |
+
Mirrors training-style warping with `timestep_transform(shift=shift_gamma)`.
|
| 45 |
+
Does not include a terminal 0 step to avoid an extra Euler update at t=0.
|
| 46 |
+
"""
|
| 47 |
+
|
| 48 |
+
if step_list is not None and len(step_list) > 0:
|
| 49 |
+
base_steps = [int(x) for x in step_list if int(x) > 0]
|
| 50 |
+
if not base_steps:
|
| 51 |
+
raise ValueError("--step_list must contain at least one positive timestep")
|
| 52 |
+
is_strict_desc = all(base_steps[i] > base_steps[i + 1] for i in range(len(base_steps) - 1))
|
| 53 |
+
if not is_strict_desc:
|
| 54 |
+
base_steps = sorted(set(base_steps), reverse=True)
|
| 55 |
+
logger.warning(f"step_list is not strictly descending; using sorted unique list: {base_steps}")
|
| 56 |
+
else:
|
| 57 |
+
n = int(num_inference_steps)
|
| 58 |
+
if n <= 0:
|
| 59 |
+
raise ValueError("--num_inference_steps must be > 0")
|
| 60 |
+
if n == 2:
|
| 61 |
+
base_steps = [1000, 500]
|
| 62 |
+
elif n == 4:
|
| 63 |
+
base_steps = [1000, 750, 500, 250]
|
| 64 |
+
else:
|
| 65 |
+
base_steps = list(np.linspace(num_timesteps, 1, n, dtype=np.float32))
|
| 66 |
+
base_steps = [int(round(float(x))) for x in base_steps]
|
| 67 |
+
base_steps = [x for x in base_steps if x > 0]
|
| 68 |
+
base_steps = sorted(set(base_steps), reverse=True)
|
| 69 |
+
if not base_steps:
|
| 70 |
+
raise ValueError("Derived empty timestep list; check --num_inference_steps")
|
| 71 |
+
logger.info(f"Using timesteps: {base_steps} (derived from num_inference_steps={num_inference_steps})")
|
| 72 |
+
ts = [torch.tensor([float(t)], device=device) for t in base_steps]
|
| 73 |
+
ts = [timestep_transform(t, shift=float(shift_gamma), num_timesteps=num_timesteps) for t in ts]
|
| 74 |
+
values = [t.reshape(-1)[0].to(device=device, dtype=torch.float32) for t in ts]
|
| 75 |
+
return torch.stack(values, dim=0)
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def _pick_latest_audio_proj(path_or_dir: str | None) -> str | None:
|
| 79 |
+
if not path_or_dir:
|
| 80 |
+
return None
|
| 81 |
+
if os.path.isfile(path_or_dir):
|
| 82 |
+
return path_or_dir
|
| 83 |
+
candidates = glob.glob(os.path.join(path_or_dir, "audio_proj_step_*.pt"))
|
| 84 |
+
if not candidates:
|
| 85 |
+
return None
|
| 86 |
+
|
| 87 |
+
def _step_num(p: str) -> int:
|
| 88 |
+
base = os.path.basename(p)
|
| 89 |
+
try:
|
| 90 |
+
return int(base.split("audio_proj_step_")[-1].split(".pt")[0])
|
| 91 |
+
except Exception:
|
| 92 |
+
return -1
|
| 93 |
+
|
| 94 |
+
candidates.sort(key=_step_num)
|
| 95 |
+
return candidates[-1]
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def _pick_latest_lora_dir(path_or_dir: str | None) -> str | None:
|
| 99 |
+
if not path_or_dir:
|
| 100 |
+
return None
|
| 101 |
+
if os.path.isfile(os.path.join(path_or_dir, "adapter_config.json")):
|
| 102 |
+
return path_or_dir
|
| 103 |
+
|
| 104 |
+
release_lora_dir = os.path.join(path_or_dir, "lora")
|
| 105 |
+
if os.path.isfile(os.path.join(release_lora_dir, "adapter_config.json")):
|
| 106 |
+
return release_lora_dir
|
| 107 |
+
|
| 108 |
+
candidates = glob.glob(os.path.join(path_or_dir, "lora_step_*"))
|
| 109 |
+
candidates = [p for p in candidates if os.path.isfile(os.path.join(p, "adapter_config.json"))]
|
| 110 |
+
if not candidates:
|
| 111 |
+
return None
|
| 112 |
+
|
| 113 |
+
def _step_num(p: str) -> int:
|
| 114 |
+
base = os.path.basename(os.path.normpath(p))
|
| 115 |
+
try:
|
| 116 |
+
return int(base.split("lora_step_")[-1])
|
| 117 |
+
except Exception:
|
| 118 |
+
return -1
|
| 119 |
+
|
| 120 |
+
candidates.sort(key=_step_num)
|
| 121 |
+
return candidates[-1]
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def _resolve_lite_tae_path(
|
| 125 |
+
*,
|
| 126 |
+
lora_dir: str,
|
| 127 |
+
resolved_lora_dir: str,
|
| 128 |
+
ckpt_dir: str,
|
| 129 |
+
explicit_tae_path: str | None,
|
| 130 |
+
) -> str:
|
| 131 |
+
if explicit_tae_path:
|
| 132 |
+
if os.path.isfile(explicit_tae_path):
|
| 133 |
+
return explicit_tae_path
|
| 134 |
+
raise SystemExit(f"TAE checkpoint not found: {explicit_tae_path}")
|
| 135 |
+
|
| 136 |
+
search_roots = [
|
| 137 |
+
lora_dir,
|
| 138 |
+
resolved_lora_dir,
|
| 139 |
+
os.path.dirname(os.path.abspath(resolved_lora_dir)),
|
| 140 |
+
os.path.join(ckpt_dir, "VAE_Wan"),
|
| 141 |
+
]
|
| 142 |
+
seen = set()
|
| 143 |
+
for root in search_roots:
|
| 144 |
+
if not root:
|
| 145 |
+
continue
|
| 146 |
+
root = os.path.abspath(root)
|
| 147 |
+
if root in seen:
|
| 148 |
+
continue
|
| 149 |
+
seen.add(root)
|
| 150 |
+
for name in ("taew2_1.pth", "taew2_1.safetensors", "taew2_2.pth", "taew2_2.safetensors"):
|
| 151 |
+
candidate = os.path.join(root, name)
|
| 152 |
+
if os.path.isfile(candidate):
|
| 153 |
+
return candidate
|
| 154 |
+
|
| 155 |
+
raise SystemExit(
|
| 156 |
+
"Lite mode requires a TAE checkpoint. Expected taew2_1.pth under --lora_dir "
|
| 157 |
+
"or its parent directory after downloading https://huggingface.co/z-rx/leaptalk."
|
| 158 |
+
)
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
def _get_inner_flashhead_model(model):
|
| 162 |
+
# After PEFT wrapping, training code uses model.base_model.model
|
| 163 |
+
if hasattr(model, "base_model") and hasattr(model.base_model, "model"):
|
| 164 |
+
return model.base_model.model
|
| 165 |
+
return model
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
def _read_num_heads_from_infer_params(vibt_root: str) -> int:
|
| 169 |
+
"""Best-effort parse of flash_head/configs/infer_params.yaml without requiring PyYAML."""
|
| 170 |
+
cfg_path = os.path.join(vibt_root, "flash_head", "configs", "infer_params.yaml")
|
| 171 |
+
try:
|
| 172 |
+
with open(cfg_path, "r", encoding="utf-8") as f:
|
| 173 |
+
for line in f:
|
| 174 |
+
line = line.strip()
|
| 175 |
+
if not line or line.startswith("#"):
|
| 176 |
+
continue
|
| 177 |
+
if line.startswith("num_heads"):
|
| 178 |
+
# e.g. "num_heads: 12"
|
| 179 |
+
_, value = line.split(":", 1)
|
| 180 |
+
return int(value.strip())
|
| 181 |
+
except Exception:
|
| 182 |
+
pass
|
| 183 |
+
return 12
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
def _lora_checkpoint_needs_compiled_base(lora_dir: str) -> bool:
|
| 187 |
+
"""Detect whether adapter weights were saved from a torch.compile()'d base model."""
|
| 188 |
+
weights_path = os.path.join(lora_dir, "adapter_model.safetensors")
|
| 189 |
+
if not os.path.exists(weights_path):
|
| 190 |
+
return False
|
| 191 |
+
|
| 192 |
+
try:
|
| 193 |
+
from safetensors.torch import safe_open
|
| 194 |
+
|
| 195 |
+
with safe_open(weights_path, framework="pt", device="cpu") as f:
|
| 196 |
+
for k in f.keys():
|
| 197 |
+
if "._orig_mod." in k:
|
| 198 |
+
return True
|
| 199 |
+
return False
|
| 200 |
+
except Exception:
|
| 201 |
+
# Best-effort: if we cannot inspect, do not force compile.
|
| 202 |
+
return False
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
@dataclass
|
| 206 |
+
class StreamParams:
|
| 207 |
+
frame_num: int = 33
|
| 208 |
+
motion_frames_latent_num: int = 2
|
| 209 |
+
tgt_fps: int = 25
|
| 210 |
+
sample_rate: int = 16000
|
| 211 |
+
cached_audio_duration: int = 8
|
| 212 |
+
|
| 213 |
+
def init_with_stride(self, vae_stride_t: int) -> "InitializedStreamParams":
|
| 214 |
+
# motion_frames_num is in *pixel frames*.
|
| 215 |
+
motion_frames_num = (self.motion_frames_latent_num - 1) * vae_stride_t + 1
|
| 216 |
+
return InitializedStreamParams(
|
| 217 |
+
frame_num=self.frame_num,
|
| 218 |
+
motion_frames_latent_num=self.motion_frames_latent_num,
|
| 219 |
+
tgt_fps=self.tgt_fps,
|
| 220 |
+
sample_rate=self.sample_rate,
|
| 221 |
+
cached_audio_duration=self.cached_audio_duration,
|
| 222 |
+
motion_frames_num=motion_frames_num,
|
| 223 |
+
)
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
@dataclass
|
| 227 |
+
class InitializedStreamParams:
|
| 228 |
+
frame_num: int
|
| 229 |
+
motion_frames_latent_num: int
|
| 230 |
+
tgt_fps: int
|
| 231 |
+
sample_rate: int
|
| 232 |
+
cached_audio_duration: int
|
| 233 |
+
motion_frames_num: int
|
| 234 |
+
|
| 235 |
+
@property
|
| 236 |
+
def slice_len(self) -> int:
|
| 237 |
+
return self.frame_num - self.motion_frames_num
|
| 238 |
+
|
| 239 |
+
|
| 240 |
+
def _open_video_writer(tmp_mp4: str, fps: int):
|
| 241 |
+
os.makedirs(os.path.dirname(os.path.abspath(tmp_mp4)) or ".", exist_ok=True)
|
| 242 |
+
return imageio.get_writer(
|
| 243 |
+
tmp_mp4,
|
| 244 |
+
format="mp4",
|
| 245 |
+
mode="I",
|
| 246 |
+
fps=fps,
|
| 247 |
+
codec="h264",
|
| 248 |
+
ffmpeg_params=["-bf", "0"],
|
| 249 |
+
)
|
| 250 |
+
|
| 251 |
+
|
| 252 |
+
@torch.no_grad()
|
| 253 |
+
def _preprocess_audio_embeddings(
|
| 254 |
+
pipeline: FlashHeadPipeline,
|
| 255 |
+
audio_array: np.ndarray,
|
| 256 |
+
sr: int,
|
| 257 |
+
fps: int,
|
| 258 |
+
device: str,
|
| 259 |
+
dtype: torch.dtype,
|
| 260 |
+
) -> torch.Tensor:
|
| 261 |
+
"""Return per-frame wav2vec2 embeddings.
|
| 262 |
+
|
| 263 |
+
Output: [T, 12, 768] where T ~= len(audio_array) * fps / sr.
|
| 264 |
+
"""
|
| 265 |
+
video_len = int(len(audio_array) * fps / sr)
|
| 266 |
+
if video_len <= 0:
|
| 267 |
+
raise RuntimeError("audio too short: computed video_len <= 0")
|
| 268 |
+
|
| 269 |
+
inputs = pipeline.wav2vec_feature_extractor(audio_array, sampling_rate=sr, return_tensors="pt", padding=True)
|
| 270 |
+
audio_values = inputs.input_values.to(device)
|
| 271 |
+
audio_out = pipeline.audio_encoder(audio_values, seq_len=video_len, output_hidden_states=True)
|
| 272 |
+
|
| 273 |
+
if not getattr(audio_out, "hidden_states", None):
|
| 274 |
+
raise RuntimeError("wav2vec2 returned no hidden_states")
|
| 275 |
+
|
| 276 |
+
hs = audio_out.hidden_states[-12:]
|
| 277 |
+
audio_emb = torch.stack(hs, dim=2).squeeze(0) # [T, 12, 768]
|
| 278 |
+
return audio_emb.to(device=device, dtype=dtype)
|
| 279 |
+
|
| 280 |
+
|
| 281 |
+
@torch.no_grad()
|
| 282 |
+
def _audio_context_from_embeddings_range(
|
| 283 |
+
audio_emb: torch.Tensor,
|
| 284 |
+
start_idx: int,
|
| 285 |
+
end_idx: int,
|
| 286 |
+
device: str,
|
| 287 |
+
dtype: torch.dtype,
|
| 288 |
+
) -> torch.Tensor:
|
| 289 |
+
"""Build [1, F, 5, 12, 768] from audio_emb [T, 12, 768] for [start_idx, end_idx).
|
| 290 |
+
|
| 291 |
+
This mirrors the official implementation in SoulX-FlashHead (vectorized gather)
|
| 292 |
+
and avoids Python-side loops.
|
| 293 |
+
"""
|
| 294 |
+
|
| 295 |
+
if audio_emb.dim() != 3:
|
| 296 |
+
raise RuntimeError(f"Expected audio_emb [T,12,768], got shape: {tuple(audio_emb.shape)}")
|
| 297 |
+
|
| 298 |
+
T = int(audio_emb.shape[0])
|
| 299 |
+
start_idx = int(start_idx)
|
| 300 |
+
end_idx = int(end_idx)
|
| 301 |
+
if end_idx <= start_idx:
|
| 302 |
+
raise RuntimeError(f"Invalid audio index range: start={start_idx}, end={end_idx}")
|
| 303 |
+
|
| 304 |
+
# Clamp to valid range.
|
| 305 |
+
start_idx = max(0, start_idx)
|
| 306 |
+
end_idx = min(T, end_idx)
|
| 307 |
+
F = end_idx - start_idx
|
| 308 |
+
if F <= 0:
|
| 309 |
+
raise RuntimeError(f"Invalid clamped range: start={start_idx}, end={end_idx}, T={T}")
|
| 310 |
+
|
| 311 |
+
# Official: indices = [-2,-1,0,1,2]
|
| 312 |
+
dev = audio_emb.device
|
| 313 |
+
base = torch.arange(start_idx, end_idx, device=dev, dtype=torch.long) # [F]
|
| 314 |
+
offsets = torch.arange(-2, 3, device=dev, dtype=torch.long) # [5]
|
| 315 |
+
center = base.unsqueeze(1) + offsets.unsqueeze(0) # [F,5]
|
| 316 |
+
|
| 317 |
+
# Match official clamping behavior (max to end_idx-1).
|
| 318 |
+
center = torch.clamp(center, min=0, max=end_idx - 1)
|
| 319 |
+
ctx = audio_emb[center] # [F,5,12,768]
|
| 320 |
+
ctx = ctx.unsqueeze(0).contiguous() # [1,F,5,12,768]
|
| 321 |
+
return ctx.to(device=device, dtype=dtype)
|
| 322 |
+
|
| 323 |
+
|
| 324 |
+
@torch.no_grad()
|
| 325 |
+
def _decode_to_cthw(pipeline: FlashHeadPipeline, latents_cthw: torch.Tensor) -> torch.Tensor:
|
| 326 |
+
decoded = pipeline.vae.decode(latents_cthw)
|
| 327 |
+
if isinstance(decoded, (tuple, list)):
|
| 328 |
+
decoded = decoded[0]
|
| 329 |
+
if decoded.dim() == 5:
|
| 330 |
+
decoded = decoded[0]
|
| 331 |
+
if decoded.dim() != 4:
|
| 332 |
+
raise RuntimeError(f"Unexpected decoded video shape: {tuple(decoded.shape)}")
|
| 333 |
+
|
| 334 |
+
# Expect [C,T,H,W]
|
| 335 |
+
if decoded.shape[0] not in (1, 3, 4):
|
| 336 |
+
raise RuntimeError(f"Cannot interpret decoded layout as [C,T,H,W]: {tuple(decoded.shape)}")
|
| 337 |
+
return decoded
|
| 338 |
+
|
| 339 |
+
|
| 340 |
+
@torch.no_grad()
|
| 341 |
+
def _maybe_apply_color_correction(
|
| 342 |
+
pipeline: FlashHeadPipeline,
|
| 343 |
+
decoded_video_cthw: torch.Tensor,
|
| 344 |
+
) -> torch.Tensor:
|
| 345 |
+
strength = float(getattr(pipeline, "color_correction_strength", 0.0))
|
| 346 |
+
if strength <= 0.0:
|
| 347 |
+
return decoded_video_cthw
|
| 348 |
+
|
| 349 |
+
reference = getattr(pipeline, "original_color_reference", None)
|
| 350 |
+
if reference is None:
|
| 351 |
+
return decoded_video_cthw
|
| 352 |
+
|
| 353 |
+
corrected = match_and_blend_colors_torch(
|
| 354 |
+
decoded_video_cthw.unsqueeze(0),
|
| 355 |
+
reference,
|
| 356 |
+
strength,
|
| 357 |
+
)
|
| 358 |
+
return corrected[0]
|
| 359 |
+
|
| 360 |
+
|
| 361 |
+
@torch.no_grad()
|
| 362 |
+
def _encode_motion_prefix_from_decoded(
|
| 363 |
+
pipeline: FlashHeadPipeline,
|
| 364 |
+
decoded_video_cthw: torch.Tensor,
|
| 365 |
+
motion_frames_num: int,
|
| 366 |
+
device: str,
|
| 367 |
+
dtype: torch.dtype,
|
| 368 |
+
) -> torch.Tensor:
|
| 369 |
+
# decoded_video_cthw: [C,T,H,W] in [-1,1]
|
| 370 |
+
cond = decoded_video_cthw[:, -motion_frames_num:]
|
| 371 |
+
cond = cond.unsqueeze(0).to(device=device, dtype=dtype) # [1,C,T,H,W]
|
| 372 |
+
return pipeline.vae.encode(cond).squeeze(0)
|
| 373 |
+
|
| 374 |
+
|
| 375 |
+
@torch.no_grad()
|
| 376 |
+
def _motion_prefix_from_latent_tail(latent_chunk_cthw: torch.Tensor, motion_frames_latent_num: int) -> torch.Tensor:
|
| 377 |
+
if motion_frames_latent_num <= 0:
|
| 378 |
+
raise RuntimeError(f"motion_frames_latent_num must be positive, got {motion_frames_latent_num}")
|
| 379 |
+
total_latent_len = int(latent_chunk_cthw.shape[1])
|
| 380 |
+
prefix_len = min(int(motion_frames_latent_num), total_latent_len)
|
| 381 |
+
return latent_chunk_cthw[:, -prefix_len:].contiguous()
|
| 382 |
+
|
| 383 |
+
|
| 384 |
+
@torch.no_grad()
|
| 385 |
+
def _source_suffix_from_ref_latent(ref_latent: torch.Tensor, prefix_len: int, suffix_len: int) -> torch.Tensor:
|
| 386 |
+
if suffix_len <= 0:
|
| 387 |
+
return ref_latent[:, 0:0, :, :].contiguous()
|
| 388 |
+
return ref_latent[:, prefix_len:prefix_len + suffix_len, :, :].contiguous()
|
| 389 |
+
|
| 390 |
+
|
| 391 |
+
@torch.no_grad()
|
| 392 |
+
def _bridge_sample_one_chunk(
|
| 393 |
+
pipeline: FlashHeadPipeline,
|
| 394 |
+
scheduler: ViBTScheduler,
|
| 395 |
+
ref_latent: torch.Tensor,
|
| 396 |
+
audio_context: torch.Tensor,
|
| 397 |
+
guidance_scale: float,
|
| 398 |
+
latent_motion_frames: torch.Tensor | None,
|
| 399 |
+
clamp_latent_len: int,
|
| 400 |
+
device: str,
|
| 401 |
+
dtype: torch.dtype,
|
| 402 |
+
) -> torch.Tensor:
|
| 403 |
+
"""Run one chunk with the same source construction used in training.
|
| 404 |
+
|
| 405 |
+
Training defines the source suffix from the fixed initial reference latent, while
|
| 406 |
+
keeping the history prefix hard-clamped for autoregressive continuity.
|
| 407 |
+
"""
|
| 408 |
+
guidance_scale = float(guidance_scale)
|
| 409 |
+
y = ref_latent.unsqueeze(0).to(device=device, dtype=dtype)
|
| 410 |
+
|
| 411 |
+
if latent_motion_frames is None or clamp_latent_len <= 0:
|
| 412 |
+
x_t = ref_latent.unsqueeze(0).to(device=device, dtype=dtype).clone()
|
| 413 |
+
else:
|
| 414 |
+
history = latent_motion_frames[:, :, :clamp_latent_len].to(device=device, dtype=dtype)
|
| 415 |
+
total_latent_len = int(ref_latent.shape[1])
|
| 416 |
+
suffix_len = total_latent_len - clamp_latent_len
|
| 417 |
+
if suffix_len < 0:
|
| 418 |
+
raise RuntimeError(f"Invalid latent lengths: total={total_latent_len}, clamp={clamp_latent_len}")
|
| 419 |
+
|
| 420 |
+
if suffix_len > 0:
|
| 421 |
+
source_suffix = _source_suffix_from_ref_latent(ref_latent, clamp_latent_len, suffix_len).unsqueeze(0).to(device=device, dtype=dtype)
|
| 422 |
+
x_t = torch.cat([history, source_suffix], dim=2)
|
| 423 |
+
else:
|
| 424 |
+
x_t = history.clone()
|
| 425 |
+
|
| 426 |
+
for t in scheduler.timesteps:
|
| 427 |
+
if latent_motion_frames is not None and clamp_latent_len > 0:
|
| 428 |
+
x_t[:, :, :clamp_latent_len] = latent_motion_frames[:, :, :clamp_latent_len]
|
| 429 |
+
timestep = t.unsqueeze(0).to(device=device, dtype=x_t.dtype)
|
| 430 |
+
v_pred_cond = pipeline.model(x=x_t, timestep=timestep, context=audio_context, y=y)
|
| 431 |
+
if isinstance(v_pred_cond, (tuple, list)):
|
| 432 |
+
v_pred_cond = v_pred_cond[0]
|
| 433 |
+
|
| 434 |
+
if guidance_scale == 1.0:
|
| 435 |
+
v_pred = v_pred_cond
|
| 436 |
+
else:
|
| 437 |
+
# Audio-driven classifier-free guidance (CFG)
|
| 438 |
+
# v_final = v_uncond + s * (v_cond - v_uncond)
|
| 439 |
+
uncond_audio_context = torch.zeros_like(audio_context)
|
| 440 |
+
v_pred_uncond = pipeline.model(x=x_t, timestep=timestep, context=uncond_audio_context, y=y)
|
| 441 |
+
if isinstance(v_pred_uncond, (tuple, list)):
|
| 442 |
+
v_pred_uncond = v_pred_uncond[0]
|
| 443 |
+
v_pred = v_pred_uncond + guidance_scale * (v_pred_cond - v_pred_uncond)
|
| 444 |
+
x_t = scheduler.step(v_pred, t, x_t)[0]
|
| 445 |
+
|
| 446 |
+
if latent_motion_frames is not None and clamp_latent_len > 0:
|
| 447 |
+
x_t[:, :, :clamp_latent_len] = latent_motion_frames[:, :, :clamp_latent_len]
|
| 448 |
+
|
| 449 |
+
return x_t[0]
|
| 450 |
+
|
| 451 |
+
|
| 452 |
+
def _mux_audio(tmp_mp4: str, audio_path: str, out_path: str):
|
| 453 |
+
cmd = [
|
| 454 |
+
"ffmpeg",
|
| 455 |
+
"-i",
|
| 456 |
+
tmp_mp4,
|
| 457 |
+
"-i",
|
| 458 |
+
audio_path,
|
| 459 |
+
"-c:v",
|
| 460 |
+
"copy",
|
| 461 |
+
"-c:a",
|
| 462 |
+
"mp3",
|
| 463 |
+
"-shortest",
|
| 464 |
+
out_path,
|
| 465 |
+
"-y",
|
| 466 |
+
]
|
| 467 |
+
subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False)
|
| 468 |
+
|
| 469 |
+
|
| 470 |
+
def main():
|
| 471 |
+
parser = argparse.ArgumentParser(description="Streaming inference for ViBT-finetuned FlashHead (chunked AR).")
|
| 472 |
+
parser.add_argument("--ckpt_dir", type=str, default="")
|
| 473 |
+
parser.add_argument("--wav2vec_dir", type=str, default="")
|
| 474 |
+
parser.add_argument("--lora_dir", type=str, default="")
|
| 475 |
+
parser.add_argument(
|
| 476 |
+
"--audio_proj",
|
| 477 |
+
type=str,
|
| 478 |
+
default="",
|
| 479 |
+
help="Path to audio_proj_step_*.pt (or directory containing them). If omitted, picks latest in the LeapTalk model directory.",
|
| 480 |
+
)
|
| 481 |
+
|
| 482 |
+
parser.add_argument("--cond_image", type=str, default="")
|
| 483 |
+
parser.add_argument("--audio_path", type=str, default="")
|
| 484 |
+
parser.add_argument("--out", type=str, default="outputs/leaptalk.mp4")
|
| 485 |
+
|
| 486 |
+
parser.add_argument("--device", type=str, default="cuda")
|
| 487 |
+
parser.add_argument("--dtype", type=str, default="bf16", choices=["bf16", "fp16", "fp32"])
|
| 488 |
+
parser.add_argument(
|
| 489 |
+
"--compile",
|
| 490 |
+
type=str,
|
| 491 |
+
default="off",
|
| 492 |
+
choices=["auto", "on", "off"],
|
| 493 |
+
help="torch.compile base model: auto uses LoRA key inspection.",
|
| 494 |
+
)
|
| 495 |
+
|
| 496 |
+
parser.add_argument(
|
| 497 |
+
"--usp",
|
| 498 |
+
type=str,
|
| 499 |
+
default="on",
|
| 500 |
+
choices=["auto", "on", "off"],
|
| 501 |
+
help="Enable USP multi-GPU (same forward split across ranks). auto enables when WORLD_SIZE>1.",
|
| 502 |
+
)
|
| 503 |
+
|
| 504 |
+
parser.add_argument("--height", type=int, default=512)
|
| 505 |
+
parser.add_argument("--width", type=int, default=512)
|
| 506 |
+
parser.add_argument("--fps", type=int, default=25)
|
| 507 |
+
parser.add_argument("--sample_rate", type=int, default=16000)
|
| 508 |
+
|
| 509 |
+
# Stream params (match SoulX defaults)
|
| 510 |
+
parser.add_argument("--frame_num", type=int, default=33)
|
| 511 |
+
parser.add_argument("--motion_frames_latent_num", type=int, default=2)
|
| 512 |
+
parser.add_argument("--cached_audio_duration", type=int, default=8)
|
| 513 |
+
parser.add_argument("--audio_encode_mode", type=str, default="stream", choices=["stream", "once"])
|
| 514 |
+
parser.add_argument("--max_chunks", type=int, default=0, help="0 means run all chunks.")
|
| 515 |
+
parser.add_argument(
|
| 516 |
+
"--history_update_mode",
|
| 517 |
+
type=str,
|
| 518 |
+
default="roundtrip",
|
| 519 |
+
choices=["roundtrip", "latent"],
|
| 520 |
+
help="How to update next-chunk history: SoulX-style VAE round-trip or direct latent tail reuse.",
|
| 521 |
+
)
|
| 522 |
+
|
| 523 |
+
# Bridge sampler params
|
| 524 |
+
parser.add_argument("--num_inference_steps", type=int, default=1)
|
| 525 |
+
parser.add_argument(
|
| 526 |
+
"--step_list",
|
| 527 |
+
type=int,
|
| 528 |
+
nargs="+",
|
| 529 |
+
default=None,
|
| 530 |
+
help="Explicit discrete timesteps (e.g. --step_list 1000 750 500 250). Overrides --num_inference_steps.",
|
| 531 |
+
)
|
| 532 |
+
parser.add_argument("--noise_scale", type=float, default=1.0)
|
| 533 |
+
parser.add_argument("--shift_gamma", type=float, default=5.0)
|
| 534 |
+
parser.add_argument("--seed", type=int, default=42)
|
| 535 |
+
parser.add_argument(
|
| 536 |
+
"--guidance_scale",
|
| 537 |
+
type=float,
|
| 538 |
+
default=1.0,
|
| 539 |
+
help="Audio CFG guidance scale; 1.0 disables guidance.",
|
| 540 |
+
)
|
| 541 |
+
|
| 542 |
+
parser.add_argument("--use_face_crop", action="store_true")
|
| 543 |
+
parser.add_argument(
|
| 544 |
+
"--color_correction_strength",
|
| 545 |
+
type=float,
|
| 546 |
+
default=1.0,
|
| 547 |
+
help="Blend decoded chunk colors toward the reference image; 0 disables correction and 1 applies full correction.",
|
| 548 |
+
)
|
| 549 |
+
parser.add_argument(
|
| 550 |
+
"--model_type",
|
| 551 |
+
type=str,
|
| 552 |
+
default="pro"
|
| 553 |
+
)
|
| 554 |
+
|
| 555 |
+
# Lite mode uses the TAE backend for fast VAE encode/decode.
|
| 556 |
+
parser.add_argument(
|
| 557 |
+
"--lite",
|
| 558 |
+
dest="lite",
|
| 559 |
+
action="store_true",
|
| 560 |
+
default=True,
|
| 561 |
+
help="Use the TAE checkpoint shipped with the LeapTalk model directory for VAE encode/decode.",
|
| 562 |
+
)
|
| 563 |
+
parser.add_argument(
|
| 564 |
+
"--no_lite",
|
| 565 |
+
dest="lite",
|
| 566 |
+
action="store_false",
|
| 567 |
+
help="Use WanVAE instead of the Lite TAE backend.",
|
| 568 |
+
)
|
| 569 |
+
|
| 570 |
+
# Backward-compatible aliases for older evaluation scripts.
|
| 571 |
+
parser.add_argument(
|
| 572 |
+
"--use_tae",
|
| 573 |
+
dest="lite",
|
| 574 |
+
action="store_true",
|
| 575 |
+
help=argparse.SUPPRESS,
|
| 576 |
+
)
|
| 577 |
+
parser.add_argument(
|
| 578 |
+
"--no_tae",
|
| 579 |
+
dest="lite",
|
| 580 |
+
action="store_false",
|
| 581 |
+
help=argparse.SUPPRESS,
|
| 582 |
+
)
|
| 583 |
+
parser.add_argument(
|
| 584 |
+
"--tae_path",
|
| 585 |
+
type=str,
|
| 586 |
+
default=None,
|
| 587 |
+
help=argparse.SUPPRESS,
|
| 588 |
+
)
|
| 589 |
+
parser.add_argument(
|
| 590 |
+
"--tae_model_type",
|
| 591 |
+
type=str,
|
| 592 |
+
default="wan21",
|
| 593 |
+
choices=["wan21", "wan22", "hy15"],
|
| 594 |
+
help="TAEHV model_type (affects latent_channels/patch_size/clamp behavior).",
|
| 595 |
+
)
|
| 596 |
+
args = parser.parse_args()
|
| 597 |
+
|
| 598 |
+
missing_paths = [
|
| 599 |
+
name
|
| 600 |
+
for name in ("ckpt_dir", "wav2vec_dir", "lora_dir", "cond_image", "audio_path")
|
| 601 |
+
if not getattr(args, name)
|
| 602 |
+
]
|
| 603 |
+
if missing_paths:
|
| 604 |
+
raise SystemExit("Missing required path argument(s): " + ", ".join(f"--{name}" for name in missing_paths))
|
| 605 |
+
|
| 606 |
+
resolved_lora_dir = _pick_latest_lora_dir(args.lora_dir)
|
| 607 |
+
if not resolved_lora_dir:
|
| 608 |
+
raise SystemExit(
|
| 609 |
+
"LoRA weights not found; pass --lora_dir /path/to/leaptalk, "
|
| 610 |
+
"/path/to/leaptalk/lora, or a directory containing lora_step_*."
|
| 611 |
+
)
|
| 612 |
+
|
| 613 |
+
tae_path = _resolve_lite_tae_path(
|
| 614 |
+
lora_dir=args.lora_dir,
|
| 615 |
+
resolved_lora_dir=resolved_lora_dir,
|
| 616 |
+
ckpt_dir=args.ckpt_dir,
|
| 617 |
+
explicit_tae_path=args.tae_path,
|
| 618 |
+
) if args.lite else None
|
| 619 |
+
|
| 620 |
+
# USP init must happen before pipeline construction.
|
| 621 |
+
world_size_env = int(os.environ.get("WORLD_SIZE", "1"))
|
| 622 |
+
use_usp = (args.usp == "on") or (args.usp == "auto" and world_size_env > 1)
|
| 623 |
+
did_init_dist = False
|
| 624 |
+
if use_usp and world_size_env > 1:
|
| 625 |
+
if not torch.cuda.is_available():
|
| 626 |
+
raise SystemExit("USP requested but CUDA is not available")
|
| 627 |
+
|
| 628 |
+
if dist.is_initialized():
|
| 629 |
+
logger.info("torch.distributed already initialized; reusing existing process group.")
|
| 630 |
+
else:
|
| 631 |
+
num_heads = _read_num_heads_from_infer_params(VIBT_ROOT)
|
| 632 |
+
from flash_head.src.distributed.usp_device import get_device, get_parallel_degree
|
| 633 |
+
|
| 634 |
+
ulysses_degree, ring_degree = get_parallel_degree(world_size_env, num_heads)
|
| 635 |
+
logger.info(
|
| 636 |
+
f"Initializing USP: WORLD_SIZE={world_size_env}, num_heads={num_heads}, "
|
| 637 |
+
f"ulysses_degree={ulysses_degree}, ring_degree={ring_degree}"
|
| 638 |
+
)
|
| 639 |
+
args.device = str(get_device(ulysses_degree, ring_degree))
|
| 640 |
+
did_init_dist = True
|
| 641 |
+
else:
|
| 642 |
+
use_usp = False
|
| 643 |
+
|
| 644 |
+
rank = dist.get_rank() if dist.is_initialized() else 0
|
| 645 |
+
is_rank0 = rank == 0
|
| 646 |
+
|
| 647 |
+
# Under torchrun/USP, every rank executes the same script.
|
| 648 |
+
# To avoid duplicated progress logs and stdout prints, keep rank0 verbose
|
| 649 |
+
# and silence non-rank0 stdout while still surfacing errors on stderr.
|
| 650 |
+
if not is_rank0:
|
| 651 |
+
try:
|
| 652 |
+
sys.stdout = open(os.devnull, "w")
|
| 653 |
+
except Exception:
|
| 654 |
+
pass
|
| 655 |
+
try:
|
| 656 |
+
logger.remove()
|
| 657 |
+
logger.add(sys.stderr, level="ERROR")
|
| 658 |
+
except Exception:
|
| 659 |
+
pass
|
| 660 |
+
|
| 661 |
+
if use_usp and args.history_update_mode != "roundtrip":
|
| 662 |
+
logger.warning("USP enabled: forcing --history_update_mode=roundtrip to match official behavior.")
|
| 663 |
+
args.history_update_mode = "roundtrip"
|
| 664 |
+
|
| 665 |
+
if args.dtype == "bf16":
|
| 666 |
+
amp_dtype = torch.bfloat16
|
| 667 |
+
elif args.dtype == "fp16":
|
| 668 |
+
amp_dtype = torch.float16
|
| 669 |
+
else:
|
| 670 |
+
amp_dtype = torch.float32
|
| 671 |
+
|
| 672 |
+
# Decide compile mode BEFORE creating pipeline (pipeline module reads globals)
|
| 673 |
+
needs_compiled = _lora_checkpoint_needs_compiled_base(resolved_lora_dir)
|
| 674 |
+
if args.compile == "auto":
|
| 675 |
+
compile_model = bool(needs_compiled)
|
| 676 |
+
elif args.compile == "on":
|
| 677 |
+
compile_model = True
|
| 678 |
+
else:
|
| 679 |
+
compile_model = False
|
| 680 |
+
|
| 681 |
+
# If checkpoint was saved from a compiled base (keys contain '._orig_mod.'), we must compile
|
| 682 |
+
# BEFORE loading LoRA so module names match. Otherwise, compiling before loading may
|
| 683 |
+
# introduce a prefix mismatch; compile after loading is safer.
|
| 684 |
+
compile_before_lora = bool(needs_compiled)
|
| 685 |
+
compile_after_lora = bool(compile_model and not needs_compiled)
|
| 686 |
+
|
| 687 |
+
import flash_head.src.pipeline.flash_head_pipeline as fh_pipe_mod
|
| 688 |
+
|
| 689 |
+
fh_pipe_mod.COMPILE_MODEL = bool(compile_before_lora)
|
| 690 |
+
# TAEHV uses Python-level block loops; torch.compile often provides little benefit and can break.
|
| 691 |
+
fh_pipe_mod.COMPILE_VAE = bool(not args.lite)
|
| 692 |
+
|
| 693 |
+
logger.info(
|
| 694 |
+
f"Init pipeline: device={args.device}, dtype={args.dtype}, compile_model={fh_pipe_mod.COMPILE_MODEL}, "
|
| 695 |
+
f"usp={use_usp}, lite={args.lite}, rank={rank}"
|
| 696 |
+
)
|
| 697 |
+
pipeline = FlashHeadPipeline(
|
| 698 |
+
checkpoint_dir=args.ckpt_dir,
|
| 699 |
+
model_type=args.model_type,
|
| 700 |
+
wav2vec_dir=args.wav2vec_dir,
|
| 701 |
+
device=args.device,
|
| 702 |
+
param_dtype=amp_dtype,
|
| 703 |
+
use_usp=use_usp,
|
| 704 |
+
use_tae=bool(args.lite),
|
| 705 |
+
tae_path=tae_path,
|
| 706 |
+
tae_model_type=args.tae_model_type,
|
| 707 |
+
)
|
| 708 |
+
logger.info(f"Pipeline initialized. Model dtype: {args.model_type}")
|
| 709 |
+
# Load LoRA
|
| 710 |
+
logger.info(f"Loading LoRA: {resolved_lora_dir}")
|
| 711 |
+
pipeline.model = PeftModel.from_pretrained(pipeline.model, resolved_lora_dir, is_trainable=False)
|
| 712 |
+
# Merge LoRA weights into base model to remove PEFT overhead during inference
|
| 713 |
+
pipeline.model = pipeline.model.merge_and_unload()
|
| 714 |
+
pipeline.model.eval().requires_grad_(False)
|
| 715 |
+
|
| 716 |
+
if compile_after_lora:
|
| 717 |
+
logger.info("Compiling model after loading LoRA (--compile on; LoRA keys do not require compiled base).")
|
| 718 |
+
pipeline.model = torch.compile(pipeline.model)
|
| 719 |
+
|
| 720 |
+
# Load audio_proj weights
|
| 721 |
+
audio_proj_path = (
|
| 722 |
+
_pick_latest_audio_proj(args.audio_proj)
|
| 723 |
+
or _pick_latest_audio_proj(args.lora_dir)
|
| 724 |
+
or _pick_latest_audio_proj(resolved_lora_dir)
|
| 725 |
+
or _pick_latest_audio_proj(os.path.dirname(resolved_lora_dir))
|
| 726 |
+
)
|
| 727 |
+
if not audio_proj_path:
|
| 728 |
+
raise SystemExit("audio_proj weights not found; pass --audio_proj /path/to/audio_proj_step_XXXX.pt")
|
| 729 |
+
|
| 730 |
+
logger.info(f"Loading audio_proj: {audio_proj_path}")
|
| 731 |
+
audio_proj_state = torch.load(audio_proj_path, map_location="cpu")
|
| 732 |
+
inner = _get_inner_flashhead_model(pipeline.model)
|
| 733 |
+
inner.audio_proj.load_state_dict(audio_proj_state, strict=True)
|
| 734 |
+
|
| 735 |
+
# Stream params derived from VAE stride
|
| 736 |
+
vae_stride_t = int(pipeline.config.vae_stride[0])
|
| 737 |
+
sp = StreamParams(
|
| 738 |
+
frame_num=int(args.frame_num),
|
| 739 |
+
motion_frames_latent_num=int(args.motion_frames_latent_num),
|
| 740 |
+
tgt_fps=int(args.fps),
|
| 741 |
+
sample_rate=int(args.sample_rate),
|
| 742 |
+
cached_audio_duration=int(args.cached_audio_duration),
|
| 743 |
+
).init_with_stride(vae_stride_t)
|
| 744 |
+
|
| 745 |
+
frame_num = sp.frame_num
|
| 746 |
+
motion_frames_num = sp.motion_frames_num
|
| 747 |
+
slice_len = sp.slice_len
|
| 748 |
+
|
| 749 |
+
if slice_len <= 0:
|
| 750 |
+
raise SystemExit(f"Invalid streaming params: frame_num={frame_num}, motion_frames_num={motion_frames_num}")
|
| 751 |
+
|
| 752 |
+
logger.info(f"Streaming: frame_num={frame_num}, motion_frames_num={motion_frames_num}, slice_len={slice_len}")
|
| 753 |
+
logger.info(f"History update mode: {args.history_update_mode}")
|
| 754 |
+
|
| 755 |
+
# Build X0 using pipeline preprocessing (resizes to H/W and repeats to frame_num)
|
| 756 |
+
pipeline.prepare_params(
|
| 757 |
+
cond_image_path_or_dir=args.cond_image,
|
| 758 |
+
target_size=(args.height, args.width),
|
| 759 |
+
frame_num=frame_num,
|
| 760 |
+
motion_frames_num=0,
|
| 761 |
+
sampling_steps=4,
|
| 762 |
+
seed=args.seed,
|
| 763 |
+
shift=args.shift_gamma,
|
| 764 |
+
color_correction_strength=args.color_correction_strength,
|
| 765 |
+
use_face_crop=args.use_face_crop,
|
| 766 |
+
)
|
| 767 |
+
X0 = pipeline.ref_img_latent.to(device=args.device, dtype=amp_dtype) # [C_lat,T_lat,h,w]
|
| 768 |
+
logger.info(f"Initialized static anchor latent with shape: {tuple(X0.shape)}")
|
| 769 |
+
|
| 770 |
+
# Scheduler
|
| 771 |
+
scheduler = ViBTScheduler(num_train_timesteps=1000)
|
| 772 |
+
scheduler.timesteps = _build_infer_timesteps(
|
| 773 |
+
step_list=args.step_list,
|
| 774 |
+
num_inference_steps=args.num_inference_steps,
|
| 775 |
+
shift_gamma=args.shift_gamma,
|
| 776 |
+
device=args.device,
|
| 777 |
+
num_timesteps=1000,
|
| 778 |
+
)
|
| 779 |
+
scheduler.num_inference_steps = int(scheduler.timesteps.numel())
|
| 780 |
+
scheduler.set_parameters(noise_scale=args.noise_scale, shift_gamma=args.shift_gamma, seed=args.seed)
|
| 781 |
+
|
| 782 |
+
# Init motion prefix (match pipeline.reset_person_name): only 1 latent frame at start
|
| 783 |
+
latent_motion_frames = X0[:, :1].unsqueeze(0).clone() # [1,C_lat,1,h,w]
|
| 784 |
+
clamp_latent_len = int(latent_motion_frames.shape[2])
|
| 785 |
+
# Audio preparation
|
| 786 |
+
audio_all, _ = librosa.load(args.audio_path, sr=args.sample_rate, mono=True)
|
| 787 |
+
human_speech_array_slice_len = slice_len * args.sample_rate // args.fps
|
| 788 |
+
human_speech_array_frame_num = frame_num * args.sample_rate // args.fps
|
| 789 |
+
|
| 790 |
+
# pad audio with silence to avoid re
|
| 791 |
+
if args.audio_encode_mode == "once":
|
| 792 |
+
remainder = (len(audio_all) - human_speech_array_frame_num) % human_speech_array_slice_len
|
| 793 |
+
else:
|
| 794 |
+
remainder = len(audio_all) % human_speech_array_slice_len
|
| 795 |
+
if remainder > 0:
|
| 796 |
+
pad_len = human_speech_array_slice_len - remainder
|
| 797 |
+
audio_all = np.concatenate([audio_all, np.zeros(pad_len, dtype=audio_all.dtype)])
|
| 798 |
+
|
| 799 |
+
# ensure audio is long enough for at least one window
|
| 800 |
+
if len(audio_all) < human_speech_array_frame_num:
|
| 801 |
+
audio_all = np.concatenate([audio_all, np.zeros(human_speech_array_frame_num - len(audio_all), dtype=audio_all.dtype)])
|
| 802 |
+
|
| 803 |
+
tmp_mp4 = args.out.replace(".mp4", "_tmp.mp4")
|
| 804 |
+
# Match official generate_video.py: cache per-chunk video tensors on CPU,
|
| 805 |
+
# then write tmp mp4 once and mux audio at the end.
|
| 806 |
+
generated_list: list[torch.Tensor] = []
|
| 807 |
+
|
| 808 |
+
total_frames = 0
|
| 809 |
+
total_time = 0.0
|
| 810 |
+
total_chunks = 0
|
| 811 |
+
|
| 812 |
+
try:
|
| 813 |
+
if args.audio_encode_mode == "once":
|
| 814 |
+
# Match official path: use pipeline.preprocess_audio + vectorized gather windowing.
|
| 815 |
+
audio_emb_all = pipeline.preprocess_audio(audio_all, sr=args.sample_rate, fps=args.fps)
|
| 816 |
+
if audio_emb_all is None:
|
| 817 |
+
raise RuntimeError("Failed to extract audio embeddings")
|
| 818 |
+
audio_emb_all = audio_emb_all.to(device=args.device, dtype=amp_dtype)
|
| 819 |
+
total_frames = int(audio_emb_all.shape[0])
|
| 820 |
+
audio_ctx_all = _audio_context_from_embeddings_range(
|
| 821 |
+
audio_emb_all,
|
| 822 |
+
start_idx=0,
|
| 823 |
+
end_idx=total_frames,
|
| 824 |
+
device=args.device,
|
| 825 |
+
dtype=amp_dtype,
|
| 826 |
+
)
|
| 827 |
+
|
| 828 |
+
# Always run at least one chunk.
|
| 829 |
+
num_chunks = 1 + max(0, (total_frames - frame_num) // slice_len)
|
| 830 |
+
if args.max_chunks and args.max_chunks > 0:
|
| 831 |
+
num_chunks = min(num_chunks, int(args.max_chunks))
|
| 832 |
+
|
| 833 |
+
for chunk_idx in range(num_chunks):
|
| 834 |
+
use_cuda_timing = args.device.startswith("cuda") and torch.cuda.is_available()
|
| 835 |
+
if use_cuda_timing:
|
| 836 |
+
torch.cuda.synchronize()
|
| 837 |
+
chunk_start = time.perf_counter()
|
| 838 |
+
start = chunk_idx * slice_len
|
| 839 |
+
end = start + frame_num
|
| 840 |
+
if end > total_frames:
|
| 841 |
+
# For the last partial chunk, clamp; audio windows will pad at edges.
|
| 842 |
+
end = total_frames
|
| 843 |
+
audio_ctx = audio_ctx_all[:, start:end].contiguous()
|
| 844 |
+
|
| 845 |
+
if use_cuda_timing:
|
| 846 |
+
denoise_evt_s = torch.cuda.Event(enable_timing=True)
|
| 847 |
+
denoise_evt_e = torch.cuda.Event(enable_timing=True)
|
| 848 |
+
denoise_evt_s.record()
|
| 849 |
+
x_final = _bridge_sample_one_chunk(
|
| 850 |
+
pipeline,
|
| 851 |
+
scheduler=scheduler,
|
| 852 |
+
ref_latent=X0,
|
| 853 |
+
audio_context=audio_ctx,
|
| 854 |
+
guidance_scale=args.guidance_scale,
|
| 855 |
+
latent_motion_frames=latent_motion_frames,
|
| 856 |
+
clamp_latent_len=clamp_latent_len,
|
| 857 |
+
device=args.device,
|
| 858 |
+
dtype=amp_dtype,
|
| 859 |
+
)
|
| 860 |
+
if use_cuda_timing:
|
| 861 |
+
denoise_evt_e.record()
|
| 862 |
+
|
| 863 |
+
if use_cuda_timing:
|
| 864 |
+
decode_evt_s = torch.cuda.Event(enable_timing=True)
|
| 865 |
+
decode_evt_e = torch.cuda.Event(enable_timing=True)
|
| 866 |
+
decode_evt_s.record()
|
| 867 |
+
decoded_cthw = _decode_to_cthw(pipeline, x_final)
|
| 868 |
+
if use_cuda_timing:
|
| 869 |
+
decode_evt_e.record()
|
| 870 |
+
|
| 871 |
+
if use_cuda_timing:
|
| 872 |
+
color_evt_s = torch.cuda.Event(enable_timing=True)
|
| 873 |
+
color_evt_e = torch.cuda.Event(enable_timing=True)
|
| 874 |
+
color_evt_s.record()
|
| 875 |
+
decoded_cthw = _maybe_apply_color_correction(pipeline, decoded_cthw)
|
| 876 |
+
if use_cuda_timing:
|
| 877 |
+
color_evt_e.record()
|
| 878 |
+
|
| 879 |
+
if args.history_update_mode == "roundtrip":
|
| 880 |
+
if use_cuda_timing:
|
| 881 |
+
hist_evt_s = torch.cuda.Event(enable_timing=True)
|
| 882 |
+
hist_evt_e = torch.cuda.Event(enable_timing=True)
|
| 883 |
+
hist_evt_s.record()
|
| 884 |
+
latent_motion_frames = _encode_motion_prefix_from_decoded(
|
| 885 |
+
pipeline,
|
| 886 |
+
decoded_video_cthw=decoded_cthw,
|
| 887 |
+
motion_frames_num=motion_frames_num,
|
| 888 |
+
device=args.device,
|
| 889 |
+
dtype=amp_dtype,
|
| 890 |
+
).unsqueeze(0)
|
| 891 |
+
if use_cuda_timing:
|
| 892 |
+
hist_evt_e.record()
|
| 893 |
+
else:
|
| 894 |
+
if use_cuda_timing:
|
| 895 |
+
hist_evt_s = torch.cuda.Event(enable_timing=True)
|
| 896 |
+
hist_evt_e = torch.cuda.Event(enable_timing=True)
|
| 897 |
+
hist_evt_s.record()
|
| 898 |
+
latent_motion_frames = _motion_prefix_from_latent_tail(
|
| 899 |
+
x_final,
|
| 900 |
+
motion_frames_latent_num=sp.motion_frames_latent_num,
|
| 901 |
+
).unsqueeze(0)
|
| 902 |
+
if use_cuda_timing:
|
| 903 |
+
hist_evt_e.record()
|
| 904 |
+
clamp_latent_len = int(latent_motion_frames.shape[2])
|
| 905 |
+
|
| 906 |
+
# Write frames: drop overlap on later chunks
|
| 907 |
+
if chunk_idx != 0:
|
| 908 |
+
decoded_cthw = decoded_cthw[:, motion_frames_num:]
|
| 909 |
+
|
| 910 |
+
# Match official run_pipeline(): build THWC frames in [0,255] on GPU.
|
| 911 |
+
video_thwc = (
|
| 912 |
+
((decoded_cthw + 1.0) / 2.0)
|
| 913 |
+
.permute(1, 2, 3, 0)
|
| 914 |
+
.clamp(0.0, 1.0)
|
| 915 |
+
.mul(255.0)
|
| 916 |
+
.contiguous()
|
| 917 |
+
)
|
| 918 |
+
|
| 919 |
+
# End chunk_time at GPU boundary (match official timing semantics).
|
| 920 |
+
if use_cuda_timing:
|
| 921 |
+
torch.cuda.synchronize()
|
| 922 |
+
chunk_time = time.perf_counter() - chunk_start
|
| 923 |
+
|
| 924 |
+
# Post-processing / IO time (excluded from chunk_time)
|
| 925 |
+
io_start = time.perf_counter()
|
| 926 |
+
video_cpu = video_thwc.detach().cpu()
|
| 927 |
+
if is_rank0:
|
| 928 |
+
generated_list.append(video_cpu)
|
| 929 |
+
io_time = time.perf_counter() - io_start
|
| 930 |
+
|
| 931 |
+
if chunk_idx >= 2:
|
| 932 |
+
total_frames += int(video_thwc.shape[0])
|
| 933 |
+
total_time += float(chunk_time)
|
| 934 |
+
total_chunks += 1
|
| 935 |
+
|
| 936 |
+
if use_cuda_timing:
|
| 937 |
+
denoise_time = float(denoise_evt_s.elapsed_time(denoise_evt_e)) / 1000.0
|
| 938 |
+
decode_time = float(decode_evt_s.elapsed_time(decode_evt_e)) / 1000.0
|
| 939 |
+
color_time = float(color_evt_s.elapsed_time(color_evt_e)) / 1000.0
|
| 940 |
+
hist_time = float(hist_evt_s.elapsed_time(hist_evt_e)) / 1000.0
|
| 941 |
+
else:
|
| 942 |
+
denoise_time = float("nan")
|
| 943 |
+
decode_time = float("nan")
|
| 944 |
+
color_time = float("nan")
|
| 945 |
+
hist_time = float("nan")
|
| 946 |
+
|
| 947 |
+
if is_rank0:
|
| 948 |
+
logger.info(
|
| 949 |
+
f"Chunk {chunk_idx + 1}/{num_chunks} timings | "
|
| 950 |
+
f"denoise={denoise_time:.3f}s | decode={decode_time:.3f}s | color={color_time:.3f}s | "
|
| 951 |
+
f"hist={hist_time:.3f}s | io={io_time:.3f}s"
|
| 952 |
+
)
|
| 953 |
+
logger.info(f"Chunk {chunk_idx + 1}/{num_chunks} done")
|
| 954 |
+
|
| 955 |
+
else:
|
| 956 |
+
# stream: cached ring buffer, recompute embeddings for cached audio each chunk
|
| 957 |
+
from collections import deque
|
| 958 |
+
|
| 959 |
+
cached_len = args.sample_rate * sp.cached_audio_duration
|
| 960 |
+
audio_end_idx = sp.cached_audio_duration * args.fps
|
| 961 |
+
audio_start_idx = audio_end_idx - frame_num
|
| 962 |
+
|
| 963 |
+
if human_speech_array_slice_len <= 0:
|
| 964 |
+
raise SystemExit("Invalid audio slice length")
|
| 965 |
+
|
| 966 |
+
# Ensure divisible for reshape
|
| 967 |
+
remainder = len(audio_all) % human_speech_array_slice_len
|
| 968 |
+
if remainder != 0:
|
| 969 |
+
audio_all = np.concatenate([audio_all, np.zeros(human_speech_array_slice_len - remainder, dtype=audio_all.dtype)])
|
| 970 |
+
|
| 971 |
+
slices = audio_all.reshape(-1, human_speech_array_slice_len)
|
| 972 |
+
if slices.shape[0] == 0:
|
| 973 |
+
raise SystemExit("audio too short after padding")
|
| 974 |
+
|
| 975 |
+
audio_dq = deque([0.0] * cached_len, maxlen=cached_len)
|
| 976 |
+
|
| 977 |
+
num_chunks = int(slices.shape[0])
|
| 978 |
+
if args.max_chunks and args.max_chunks > 0:
|
| 979 |
+
num_chunks = min(num_chunks, int(args.max_chunks))
|
| 980 |
+
|
| 981 |
+
for chunk_idx in range(num_chunks):
|
| 982 |
+
use_cuda_timing = args.device.startswith("cuda") and torch.cuda.is_available()
|
| 983 |
+
if use_cuda_timing:
|
| 984 |
+
torch.cuda.synchronize()
|
| 985 |
+
chunk_start = time.perf_counter()
|
| 986 |
+
audio_dq.extend(slices[chunk_idx].tolist())
|
| 987 |
+
audio_cache = np.array(audio_dq, dtype=np.float32)
|
| 988 |
+
audio_emb_cache = pipeline.preprocess_audio(audio_cache, sr=args.sample_rate, fps=args.fps)
|
| 989 |
+
if audio_emb_cache is None:
|
| 990 |
+
raise RuntimeError("Failed to extract audio embeddings")
|
| 991 |
+
audio_emb_cache = audio_emb_cache.to(device=args.device, dtype=amp_dtype)
|
| 992 |
+
audio_ctx = _audio_context_from_embeddings_range(
|
| 993 |
+
audio_emb_cache,
|
| 994 |
+
start_idx=audio_start_idx,
|
| 995 |
+
end_idx=audio_end_idx,
|
| 996 |
+
device=args.device,
|
| 997 |
+
dtype=amp_dtype,
|
| 998 |
+
)
|
| 999 |
+
|
| 1000 |
+
if use_cuda_timing:
|
| 1001 |
+
denoise_evt_s = torch.cuda.Event(enable_timing=True)
|
| 1002 |
+
denoise_evt_e = torch.cuda.Event(enable_timing=True)
|
| 1003 |
+
denoise_evt_s.record()
|
| 1004 |
+
x_final = _bridge_sample_one_chunk(
|
| 1005 |
+
pipeline,
|
| 1006 |
+
scheduler=scheduler,
|
| 1007 |
+
ref_latent=X0,
|
| 1008 |
+
audio_context=audio_ctx,
|
| 1009 |
+
guidance_scale=args.guidance_scale,
|
| 1010 |
+
latent_motion_frames=latent_motion_frames,
|
| 1011 |
+
clamp_latent_len=clamp_latent_len,
|
| 1012 |
+
device=args.device,
|
| 1013 |
+
dtype=amp_dtype,
|
| 1014 |
+
)
|
| 1015 |
+
if use_cuda_timing:
|
| 1016 |
+
denoise_evt_e.record()
|
| 1017 |
+
|
| 1018 |
+
if use_cuda_timing:
|
| 1019 |
+
decode_evt_s = torch.cuda.Event(enable_timing=True)
|
| 1020 |
+
decode_evt_e = torch.cuda.Event(enable_timing=True)
|
| 1021 |
+
decode_evt_s.record()
|
| 1022 |
+
decoded_cthw = _decode_to_cthw(pipeline, x_final)
|
| 1023 |
+
if use_cuda_timing:
|
| 1024 |
+
decode_evt_e.record()
|
| 1025 |
+
|
| 1026 |
+
if use_cuda_timing:
|
| 1027 |
+
color_evt_s = torch.cuda.Event(enable_timing=True)
|
| 1028 |
+
color_evt_e = torch.cuda.Event(enable_timing=True)
|
| 1029 |
+
color_evt_s.record()
|
| 1030 |
+
decoded_cthw = _maybe_apply_color_correction(pipeline, decoded_cthw)
|
| 1031 |
+
if use_cuda_timing:
|
| 1032 |
+
color_evt_e.record()
|
| 1033 |
+
|
| 1034 |
+
if args.history_update_mode == "roundtrip":
|
| 1035 |
+
if use_cuda_timing:
|
| 1036 |
+
hist_evt_s = torch.cuda.Event(enable_timing=True)
|
| 1037 |
+
hist_evt_e = torch.cuda.Event(enable_timing=True)
|
| 1038 |
+
hist_evt_s.record()
|
| 1039 |
+
latent_motion_frames = _encode_motion_prefix_from_decoded(
|
| 1040 |
+
pipeline,
|
| 1041 |
+
decoded_video_cthw=decoded_cthw,
|
| 1042 |
+
motion_frames_num=motion_frames_num,
|
| 1043 |
+
device=args.device,
|
| 1044 |
+
dtype=amp_dtype,
|
| 1045 |
+
).unsqueeze(0)
|
| 1046 |
+
if use_cuda_timing:
|
| 1047 |
+
hist_evt_e.record()
|
| 1048 |
+
else:
|
| 1049 |
+
if use_cuda_timing:
|
| 1050 |
+
hist_evt_s = torch.cuda.Event(enable_timing=True)
|
| 1051 |
+
hist_evt_e = torch.cuda.Event(enable_timing=True)
|
| 1052 |
+
hist_evt_s.record()
|
| 1053 |
+
latent_motion_frames = _motion_prefix_from_latent_tail(
|
| 1054 |
+
x_final,
|
| 1055 |
+
motion_frames_latent_num=sp.motion_frames_latent_num,
|
| 1056 |
+
).unsqueeze(0)
|
| 1057 |
+
if use_cuda_timing:
|
| 1058 |
+
hist_evt_e.record()
|
| 1059 |
+
clamp_latent_len = int(latent_motion_frames.shape[2])
|
| 1060 |
+
|
| 1061 |
+
# In stream mode, match generate_video.py: always drop overlap
|
| 1062 |
+
decoded_cthw = decoded_cthw[:, motion_frames_num:]
|
| 1063 |
+
|
| 1064 |
+
# Match official run_pipeline(): build THWC frames in [0,255] on GPU.
|
| 1065 |
+
video_thwc = (
|
| 1066 |
+
((decoded_cthw + 1.0) / 2.0)
|
| 1067 |
+
.permute(1, 2, 3, 0)
|
| 1068 |
+
.clamp(0.0, 1.0)
|
| 1069 |
+
.mul(255.0)
|
| 1070 |
+
.contiguous()
|
| 1071 |
+
)
|
| 1072 |
+
|
| 1073 |
+
# End chunk_time at GPU boundary (match official timing semantics).
|
| 1074 |
+
if use_cuda_timing:
|
| 1075 |
+
torch.cuda.synchronize()
|
| 1076 |
+
chunk_time = time.perf_counter() - chunk_start
|
| 1077 |
+
|
| 1078 |
+
# Post-processing / IO time (excluded from chunk_time)
|
| 1079 |
+
io_start = time.perf_counter()
|
| 1080 |
+
video_cpu = video_thwc.detach().cpu()
|
| 1081 |
+
if is_rank0:
|
| 1082 |
+
generated_list.append(video_cpu)
|
| 1083 |
+
io_time = time.perf_counter() - io_start
|
| 1084 |
+
|
| 1085 |
+
chunk_frames = int(video_thwc.shape[0])
|
| 1086 |
+
chunk_fps = chunk_frames / chunk_time
|
| 1087 |
+
|
| 1088 |
+
if chunk_idx >= 2:
|
| 1089 |
+
total_frames += chunk_frames
|
| 1090 |
+
total_time += chunk_time
|
| 1091 |
+
total_chunks += 1
|
| 1092 |
+
|
| 1093 |
+
if use_cuda_timing:
|
| 1094 |
+
denoise_time = float(denoise_evt_s.elapsed_time(denoise_evt_e)) / 1000.0
|
| 1095 |
+
decode_time = float(decode_evt_s.elapsed_time(decode_evt_e)) / 1000.0
|
| 1096 |
+
color_time = float(color_evt_s.elapsed_time(color_evt_e)) / 1000.0
|
| 1097 |
+
hist_time = float(hist_evt_s.elapsed_time(hist_evt_e)) / 1000.0
|
| 1098 |
+
else:
|
| 1099 |
+
denoise_time = float("nan")
|
| 1100 |
+
decode_time = float("nan")
|
| 1101 |
+
color_time = float("nan")
|
| 1102 |
+
hist_time = float("nan")
|
| 1103 |
+
|
| 1104 |
+
logger.info(
|
| 1105 |
+
f"Chunk {chunk_idx+1}/{num_chunks} | "
|
| 1106 |
+
f"time={chunk_time:.3f}s | "
|
| 1107 |
+
f"frames={chunk_frames} | "
|
| 1108 |
+
f"FPS={chunk_fps:.2f} | "
|
| 1109 |
+
f"denoise={denoise_time:.3f}s | decode={decode_time:.3f}s | color={color_time:.3f}s | "
|
| 1110 |
+
f"hist={hist_time:.3f}s | io={io_time:.3f}s"
|
| 1111 |
+
)
|
| 1112 |
+
logger.info(f"Chunk {chunk_idx + 1}/{num_chunks} done")
|
| 1113 |
+
|
| 1114 |
+
finally:
|
| 1115 |
+
pass
|
| 1116 |
+
|
| 1117 |
+
if dist.is_initialized():
|
| 1118 |
+
dist.barrier()
|
| 1119 |
+
|
| 1120 |
+
if is_rank0:
|
| 1121 |
+
os.makedirs(os.path.dirname(os.path.abspath(args.out)) or ".", exist_ok=True)
|
| 1122 |
+
|
| 1123 |
+
# Save video once (official-style) then mux audio.
|
| 1124 |
+
with _open_video_writer(tmp_mp4, fps=args.fps) as writer:
|
| 1125 |
+
for video_cpu in generated_list:
|
| 1126 |
+
# Match official save_video(): numpy().astype(np.uint8) at write time.
|
| 1127 |
+
# NumPy does not support bfloat16; cast at write time (outside chunk_time).
|
| 1128 |
+
if video_cpu.dtype == torch.bfloat16:
|
| 1129 |
+
video_cpu = video_cpu.to(torch.float16)
|
| 1130 |
+
frames_np = video_cpu.numpy().astype(np.uint8)
|
| 1131 |
+
for fr in frames_np:
|
| 1132 |
+
writer.append_data(fr)
|
| 1133 |
+
_mux_audio(tmp_mp4, args.audio_path, args.out)
|
| 1134 |
+
try:
|
| 1135 |
+
os.remove(tmp_mp4)
|
| 1136 |
+
except OSError:
|
| 1137 |
+
pass
|
| 1138 |
+
|
| 1139 |
+
if total_time > 0:
|
| 1140 |
+
avg_fps = total_frames / total_time
|
| 1141 |
+
avg_chunk_time = total_time / total_chunks if total_chunks > 0 else float("nan")
|
| 1142 |
+
logger.info(
|
| 1143 |
+
f"Average generation FPS (excl first 2 chunks): {avg_fps:.2f} "
|
| 1144 |
+
f"(total_frames={total_frames}, total_time={total_time:.2f}s)"
|
| 1145 |
+
)
|
| 1146 |
+
logger.info(f"Average chunk time (excl first 2 chunks): {avg_chunk_time:.3f}s")
|
| 1147 |
+
logger.info(f"Saved: {args.out}")
|
| 1148 |
+
|
| 1149 |
+
if dist.is_initialized():
|
| 1150 |
+
dist.barrier()
|
| 1151 |
+
if did_init_dist:
|
| 1152 |
+
dist.destroy_process_group()
|
| 1153 |
+
|
| 1154 |
+
|
| 1155 |
+
if __name__ == "__main__":
|
| 1156 |
+
main()
|
requirements.txt
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
torchvision
|
| 2 |
+
diffusers==0.38.0
|
| 3 |
+
transformers==4.57.3
|
| 4 |
+
tokenizers
|
| 5 |
+
peft==0.19.1
|
| 6 |
+
accelerate
|
| 7 |
+
safetensors
|
| 8 |
+
einops
|
| 9 |
+
numpy
|
| 10 |
+
pillow
|
| 11 |
+
librosa
|
| 12 |
+
soundfile
|
| 13 |
+
imageio
|
| 14 |
+
imageio-ffmpeg
|
| 15 |
+
loguru
|
| 16 |
+
pyloudnorm
|
| 17 |
+
mediapipe==0.10.35
|
| 18 |
+
tqdm
|
vibt/scheduler.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from diffusers.schedulers import UniPCMultistepScheduler
|
| 2 |
+
import torch
|
| 3 |
+
import time
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class ViBTScheduler(UniPCMultistepScheduler):
|
| 7 |
+
def __init__(self, **kwargs):
|
| 8 |
+
super().__init__(**{**kwargs, "use_flow_sigmas": True})
|
| 9 |
+
self.set_parameters()
|
| 10 |
+
|
| 11 |
+
def set_parameters(self, noise_scale=1.0, shift_gamma=5.0, seed=None):
|
| 12 |
+
self.noise_scale = noise_scale
|
| 13 |
+
self.config.flow_shift = shift_gamma
|
| 14 |
+
self.generator = (
|
| 15 |
+
None if seed is None else torch.Generator("cuda").manual_seed(seed)
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
def step(self, model_output, timestep, sample, ode=False, **kwargs):
|
| 19 |
+
t_val = float(timestep)
|
| 20 |
+
|
| 21 |
+
if not hasattr(self, "_t_list") or len(self._t_list) != len(self.timesteps):
|
| 22 |
+
self._t_list = self.timesteps.cpu().tolist()
|
| 23 |
+
|
| 24 |
+
idx = -1
|
| 25 |
+
for i, val in enumerate(self._t_list):
|
| 26 |
+
if abs(val - t_val) < 1e-4:
|
| 27 |
+
idx = i
|
| 28 |
+
break
|
| 29 |
+
|
| 30 |
+
if idx != -1 and idx + 1 < len(self._t_list):
|
| 31 |
+
next_t = self._t_list[idx + 1]
|
| 32 |
+
else:
|
| 33 |
+
next_t = -1.0
|
| 34 |
+
|
| 35 |
+
delta_t = (next_t - t_val) / 1000.0
|
| 36 |
+
current_t = (t_val + 1.0) / 1000.0
|
| 37 |
+
|
| 38 |
+
eta_radicand = -delta_t * (current_t + delta_t) / current_t
|
| 39 |
+
eta = (max(0.0, eta_radicand)) ** 0.5
|
| 40 |
+
|
| 41 |
+
noise = torch.randn(
|
| 42 |
+
sample.shape,
|
| 43 |
+
generator=self.generator,
|
| 44 |
+
device=sample.device,
|
| 45 |
+
dtype=sample.dtype,
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
latents = sample + delta_t * model_output + eta * self.noise_scale * noise
|
| 49 |
+
|
| 50 |
+
return (latents,)
|
| 51 |
+
|
| 52 |
+
@classmethod
|
| 53 |
+
def from_scheduler(
|
| 54 |
+
cls, scheduler: UniPCMultistepScheduler, noise_scale=1.0, shift_gamma=5.0
|
| 55 |
+
):
|
| 56 |
+
obj = cls.__new__(cls)
|
| 57 |
+
obj.__dict__ = scheduler.__dict__.copy()
|
| 58 |
+
obj.set_parameters(noise_scale, shift_gamma)
|
| 59 |
+
return obj
|