multimodalart HF Staff commited on
Commit
0c68bcd
·
verified ·
1 Parent(s): e02bb85

Upload folder using huggingface_hub

Browse files
.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: Leaptalk Talking Head
3
- emoji: 👀
4
- colorFrom: green
5
- colorTo: purple
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
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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

  • SHA256: 384abd202f7dae45fdbbab3b6a7777cd7c8a64625fb19217a45d056f2162c624
  • Pointer size: 131 Bytes
  • Size of remote file: 357 kB
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

  • SHA256: 7fd43bef3e2a1e31df9f31f38c71640560363f2764c4e9480ca4ac76ab5547b0
  • Pointer size: 131 Bytes
  • Size of remote file: 237 kB
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