Skip to content

Commit 6346fb9

Browse files
committed
[feat]: add Lucy Edit inference scaffold
1 parent 30c4562 commit 6346fb9

11 files changed

Lines changed: 283 additions & 11 deletions

File tree

docs/inference/support_matrix.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ pipeline initialization and sampling.
5858
| FastWan2.1 T2V 1.3B | `FastVideo/FastWan2.1-T2V-1.3B-Diffusers` | 480P ||||||
5959
| FastWan2.2 TI2V 5B Full Attn* | `FastVideo/FastWan2.2-TI2V-5B-FullAttn-Diffusers` | 720P ||||||
6060
| Wan2.2 TI2V 5B | `Wan-AI/Wan2.2-TI2V-5B-Diffusers` | 720P ||||||
61+
| Lucy Edit Dev 5B*** | `decart-ai/Lucy-Edit-Dev` | 480P ||||||
6162
| Wan2.2 T2V A14B | `Wan-AI/Wan2.2-T2V-A14B-Diffusers` | 480P<br>720P ||||||
6263
| Wan2.2 I2V A14B | `Wan-AI/Wan2.2-I2V-A14B-Diffusers` | 480P<br>720P ||||||
6364
| HunyuanVideo | `hunyuanvideo-community/HunyuanVideo` | 720px1280p<br>544px960p ||||||
@@ -77,6 +78,9 @@ pipeline initialization and sampling.
7778

7879
**Note**: Wan2.2 TI2V 5B has some quality issues when performing I2V generation. We are working on fixing this issue.
7980

81+
***Lucy Edit Dev uses a non-commercial model license. FastVideo support is
82+
focused on inference integration for video editing workflows.
83+
8084
`Sliding Tile Attn (Legacy Branch)` entries refer to the archived
8185
`sta_do_not_delete` branch workflow, not active `main` inference wiring.
8286

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from fastvideo import VideoGenerator
2+
3+
OUTPUT_PATH = "video_samples_lucy_edit"
4+
5+
6+
def main():
7+
generator = VideoGenerator.from_pretrained(
8+
"decart-ai/Lucy-Edit-Dev",
9+
num_gpus=1,
10+
use_fsdp_inference=False,
11+
dit_cpu_offload=True,
12+
vae_cpu_offload=False,
13+
text_encoder_cpu_offload=True,
14+
pin_cpu_memory=True,
15+
)
16+
17+
prompt = ("Change the apron and blouse to a classic clown costume: satin "
18+
"polka-dot jumpsuit in bright primary colors, ruffled white collar, "
19+
"oversized pom-pom buttons, white gloves, oversized red shoes, red "
20+
"foam nose; soft window light from left, eye-level medium shot.")
21+
video_path = "https://d2drjpuinn46lb.cloudfront.net/painter_original_edit.mp4"
22+
23+
generator.generate_video(
24+
prompt,
25+
negative_prompt="",
26+
video_path=video_path,
27+
output_path=OUTPUT_PATH,
28+
save_video=True,
29+
height=480,
30+
width=832,
31+
num_frames=81,
32+
fps=24,
33+
guidance_scale=5.0,
34+
)
35+
36+
37+
if __name__ == "__main__":
38+
main()

fastvideo/configs/pipelines/__init__.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@
77
from fastvideo.configs.pipelines.hyworld import HYWorldConfig
88
from fastvideo.pipelines.basic.ltx2.pipeline_configs import LTX2T2VConfig
99
from fastvideo.registry import get_pipeline_config_cls_from_name
10-
from fastvideo.configs.pipelines.wan import (SelfForcingWanT2V480PConfig, WanI2V480PConfig, WanI2V720PConfig,
11-
WanT2V480PConfig, WanT2V720PConfig)
10+
from fastvideo.configs.pipelines.wan import (LucyEditDevConfig, SelfForcingWanT2V480PConfig, WanI2V480PConfig,
11+
WanI2V720PConfig, WanT2V480PConfig, WanT2V720PConfig)
1212

1313
__all__ = [
1414
"HunyuanConfig", "FastHunyuanConfig", "HunyuanGameCraftPipelineConfig", "PipelineConfig", "Hunyuan15T2V480PConfig",
1515
"Hunyuan15T2V720PConfig", "WanT2V480PConfig", "WanI2V480PConfig", "WanT2V720PConfig", "WanI2V720PConfig",
16-
"SelfForcingWanT2V480PConfig", "CosmosConfig", "Cosmos25Config", "LTX2T2VConfig", "HYWorldConfig",
17-
"get_pipeline_config_cls_from_name"
16+
"SelfForcingWanT2V480PConfig", "LucyEditDevConfig", "CosmosConfig", "Cosmos25Config", "LTX2T2VConfig",
17+
"HYWorldConfig", "get_pipeline_config_cls_from_name"
1818
]

fastvideo/configs/pipelines/base.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,9 @@ class PipelineConfig:
6464
# DMD parameters
6565
dmd_denoising_steps: list[int] | None = field(default=None)
6666

67-
# Wan2.2 TI2V parameters
67+
# Wan2.2 task modifiers
6868
ti2v_task: bool = False
69+
lucy_edit_task: bool = False
6970
boundary_ratio: float | None = None
7071

7172
# Compilation

fastvideo/configs/pipelines/wan.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66

77
from fastvideo.configs.models import DiTConfig, EncoderConfig, VAEConfig
88
from fastvideo.configs.models.dits import WanVideoConfig
9+
from fastvideo.configs.models.dits.wanvideo import WanVideoArchConfig
910
from fastvideo.configs.models.dits.matrixgame import MatrixGameWanVideoConfig
1011
from fastvideo.configs.models.encoders import (BaseEncoderOutput, CLIPVisionConfig, T5Config,
1112
WAN2_1ControlCLIPVisionConfig)
1213
from fastvideo.configs.models.vaes import WanVAEConfig
14+
from fastvideo.configs.models.vaes.wanvae import WanVAEArchConfig
1315
from fastvideo.configs.pipelines.base import PipelineConfig
1416

1517

@@ -126,6 +128,60 @@ def __post_init__(self) -> None:
126128
self.dit_config.expand_timesteps = self.expand_timesteps
127129

128130

131+
@dataclass
132+
class LucyEditDevConfig(Wan2_2_TI2V_5B_Config):
133+
"""Configuration for Decart Lucy Edit Dev video editing."""
134+
135+
dit_config: DiTConfig = field(default_factory=lambda: WanVideoConfig(arch_config=WanVideoArchConfig(
136+
num_attention_heads=24,
137+
in_channels=96,
138+
out_channels=48,
139+
ffn_dim=14336,
140+
num_layers=30,
141+
)))
142+
vae_config: VAEConfig = field(default_factory=lambda: WanVAEConfig(arch_config=WanVAEArchConfig(
143+
base_dim=160,
144+
decoder_base_dim=256,
145+
z_dim=48,
146+
in_channels=12,
147+
out_channels=12,
148+
scale_factor_spatial=16,
149+
patch_size=2,
150+
is_residual=True,
151+
clip_output=False,
152+
latents_mean=(
153+
-0.2289, -0.0052, -0.1323, -0.2339, -0.2799, 0.0174,
154+
0.1838, 0.1557, -0.1382, 0.0542, 0.2813, 0.0891,
155+
0.1570, -0.0098, 0.0375, -0.1825, -0.2246, -0.1207,
156+
-0.0698, 0.5109, 0.2665, -0.2108, -0.2158, 0.2502,
157+
-0.2055, -0.0322, 0.1109, 0.1567, -0.0729, 0.0899,
158+
-0.2799, -0.1230, -0.0313, -0.1649, 0.0117, 0.0723,
159+
-0.2839, -0.2083, -0.0520, 0.3748, 0.0152, 0.1957,
160+
0.1433, -0.2944, 0.3573, -0.0548, -0.1681, -0.0667,
161+
),
162+
latents_std=(
163+
0.4765, 1.0364, 0.4514, 1.1677, 0.5313, 0.4990,
164+
0.4818, 0.5013, 0.8158, 1.0344, 0.5894, 1.0901,
165+
0.6885, 0.6165, 0.8454, 0.4978, 0.5759, 0.3523,
166+
0.7135, 0.6804, 0.5833, 1.4146, 0.8986, 0.5659,
167+
0.7069, 0.5338, 0.4889, 0.4917, 0.4069, 0.4999,
168+
0.6866, 0.4093, 0.5709, 0.6065, 0.6415, 0.4944,
169+
0.5726, 1.2042, 0.5458, 1.6887, 0.3971, 1.0600,
170+
0.3943, 0.5537, 0.5444, 0.4089, 0.7468, 0.7744,
171+
),
172+
)))
173+
ti2v_task: bool = False
174+
lucy_edit_task: bool = True
175+
176+
def __post_init__(self) -> None:
177+
# Lucy uses Wan2.2's enhanced 48-channel VAE latents. Denoising
178+
# concatenates noise + video latents, matching the 96-channel
179+
# transformer input declared above.
180+
self.vae_config.load_encoder = True
181+
self.vae_config.load_decoder = True
182+
self.dit_config.expand_timesteps = self.expand_timesteps
183+
184+
129185
@dataclass
130186
class FastWan2_2_TI2V_5B_Config(Wan2_2_TI2V_5B_Config):
131187
flow_shift: float | None = 5.0
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
"""Lucy Edit video editing pipeline.
3+
4+
Lucy Edit uses a Wan2.2 5B transformer with an input video latent appended to
5+
the noisy latent channels. The stage topology is therefore closest to Wan V2V,
6+
but the model repo does not include CLIP image-encoder components.
7+
"""
8+
9+
from fastvideo.fastvideo_args import FastVideoArgs
10+
from fastvideo.logger import init_logger
11+
from fastvideo.pipelines.basic.wan.wan_v2v_pipeline import WanVideoToVideoPipeline
12+
from fastvideo.pipelines.stages import (
13+
ConditioningStage,
14+
DecodingStage,
15+
DenoisingStage,
16+
InputValidationStage,
17+
LatentPreparationStage,
18+
TextEncodingStage,
19+
TimestepPreparationStage,
20+
VideoVAEEncodingStage,
21+
)
22+
23+
logger = init_logger(__name__)
24+
25+
26+
class LucyEditPipeline(WanVideoToVideoPipeline):
27+
"""FastVideo pipeline for decart-ai/Lucy-Edit-Dev."""
28+
29+
_required_config_modules = [
30+
"text_encoder",
31+
"tokenizer",
32+
"vae",
33+
"transformer",
34+
"scheduler",
35+
]
36+
37+
def create_pipeline_stages(self, fastvideo_args: FastVideoArgs):
38+
self.add_stage(stage_name="input_validation_stage", stage=InputValidationStage())
39+
40+
self.add_stage(
41+
stage_name="prompt_encoding_stage",
42+
stage=TextEncodingStage(
43+
text_encoders=[self.get_module("text_encoder")],
44+
tokenizers=[self.get_module("tokenizer")],
45+
),
46+
)
47+
48+
self.add_stage(stage_name="conditioning_stage", stage=ConditioningStage())
49+
50+
self.add_stage(
51+
stage_name="timestep_preparation_stage",
52+
stage=TimestepPreparationStage(scheduler=self.get_module("scheduler")),
53+
)
54+
55+
self.add_stage(
56+
stage_name="latent_preparation_stage",
57+
stage=LatentPreparationStage(
58+
scheduler=self.get_module("scheduler"),
59+
transformer=self.get_module("transformer"),
60+
),
61+
)
62+
63+
self.add_stage(
64+
stage_name="video_latent_preparation_stage",
65+
stage=VideoVAEEncodingStage(vae=self.get_module("vae")),
66+
)
67+
68+
self.add_stage(
69+
stage_name="denoising_stage",
70+
stage=DenoisingStage(
71+
transformer=self.get_module("transformer"),
72+
transformer_2=self.get_module("transformer_2"),
73+
scheduler=self.get_module("scheduler"),
74+
),
75+
)
76+
77+
self.add_stage(stage_name="decoding_stage", stage=DecodingStage(vae=self.get_module("vae")))
78+
79+
80+
EntryClass = LucyEditPipeline

fastvideo/pipelines/basic/wan/presets.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,23 @@
268268
},
269269
)
270270

271+
LUCY_EDIT_DEV = InferencePreset(
272+
name="lucy_edit_dev",
273+
version=1,
274+
model_family="wan",
275+
description="Lucy Edit Dev 5B video editing",
276+
stage_schemas=(_DENOISE_STAGE, ),
277+
defaults={
278+
"height": 480,
279+
"width": 832,
280+
"num_frames": 81,
281+
"fps": 24,
282+
"guidance_scale": 5.0,
283+
"num_inference_steps": 50,
284+
"negative_prompt": "",
285+
},
286+
)
287+
271288
# -------------------------------------------------------------------
272289
# Self-Forcing (causal) presets
273290
# -------------------------------------------------------------------
@@ -341,6 +358,7 @@
341358
FAST_WAN_T2V_480P,
342359
WAN_2_2_TI2V_5B,
343360
FAST_WAN_2_2_TI2V_5B,
361+
LUCY_EDIT_DEV,
344362
SF_WAN_T2V_1_3B,
345363
SF_WAN_2_2_T2V_A14B,
346364
SF_WAN_2_2_I2V_A14B,

fastvideo/pipelines/stages/denoising.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@ def forward(
211211
# Initialize lists for ODE trajectory
212212
trajectory_timesteps: list[torch.Tensor] = []
213213
trajectory_latents: list[torch.Tensor] = []
214+
is_lucy_edit = fastvideo_args.pipeline_config.lucy_edit_task
214215

215216
# Hoisted out of the per-step loop: depends only on inputs that
216217
# are constant across denoising steps.
@@ -228,6 +229,13 @@ def forward(
228229
# latent_model_input. Shape is fixed by latents and is never
229230
# written to, so we allocate once.
230231
v2v_zero_pad = torch.zeros_like(latents) if batch.video_latent is not None else None
232+
lucy_timestep_seq_len = None
233+
if is_lucy_edit:
234+
patch_size = fastvideo_args.pipeline_config.dit_config.arch_config.patch_size
235+
assert patch_size[0] == 1, "Lucy Edit timestep expansion assumes temporal patch size 1"
236+
lucy_timestep_seq_len = (
237+
latents.shape[2] * (latents.shape[3] // patch_size[1]) * (latents.shape[4] // patch_size[2])
238+
)
231239

232240
# Run denoising loop
233241
with self.progress_bar(total=num_inference_steps) as progress_bar:
@@ -265,14 +273,25 @@ def forward(
265273
# Expand latents for V2V/I2V
266274
latent_model_input = latents.to(target_dtype)
267275
if batch.video_latent is not None:
268-
latent_model_input = torch.cat([latent_model_input, batch.video_latent, v2v_zero_pad],
269-
dim=1).to(target_dtype)
276+
if is_lucy_edit:
277+
latent_model_input = torch.cat(
278+
[latent_model_input, batch.video_latent],
279+
dim=1,
280+
).to(target_dtype)
281+
else:
282+
latent_model_input = torch.cat(
283+
[latent_model_input, batch.video_latent, v2v_zero_pad],
284+
dim=1,
285+
).to(target_dtype)
270286
elif batch.image_latent is not None:
271287
assert not fastvideo_args.pipeline_config.ti2v_task, "image latents should not be provided for TI2V task"
272288
latent_model_input = torch.cat([latent_model_input, batch.image_latent], dim=1).to(target_dtype)
273289

274290
assert not torch.isnan(latent_model_input).any(), "latent_model_input contains nan"
275-
if fastvideo_args.pipeline_config.ti2v_task and batch.pil_image is not None:
291+
if is_lucy_edit:
292+
assert lucy_timestep_seq_len is not None
293+
t_expand = t.repeat(latent_model_input.shape[0], lucy_timestep_seq_len)
294+
elif fastvideo_args.pipeline_config.ti2v_task and batch.pil_image is not None:
276295
timestep = torch.stack([t]).to(get_local_torch_device())
277296
temp_ts = (mask2[0][0][:, ::2, ::2] * timestep).flatten()
278297
temp_ts = torch.cat([temp_ts, temp_ts.new_ones(seq_len - temp_ts.size(0)) * timestep])

fastvideo/pipelines/stages/image_encoding.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -629,9 +629,10 @@ def forward(
629629
encoder_output = self.vae.encode(video_condition)
630630

631631
generator = batch.generator
632-
if generator is None:
633-
raise ValueError("Generator must be provided")
634-
latent_condition = self.retrieve_latents(encoder_output, generator)
632+
sample_mode = "argmax" if fastvideo_args.pipeline_config.lucy_edit_task else "sample"
633+
if sample_mode == "sample" and generator is None:
634+
raise ValueError("Generator must be provided for sampled video VAE encoding")
635+
latent_condition = self.retrieve_latents(encoder_output, generator, sample_mode=sample_mode)
635636

636637
if (hasattr(self.vae, "shift_factor") and self.vae.shift_factor is not None):
637638
if isinstance(self.vae.shift_factor, torch.Tensor):

fastvideo/registry.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
from fastvideo.configs.pipelines.wan import (
3939
FastWan2_1_T2V_480P_Config,
4040
FastWan2_2_TI2V_5B_Config,
41+
LucyEditDevConfig,
4142
MatrixGameI2V480PConfig,
4243
SelfForcingWan2_2_T2V480PConfig,
4344
SelfForcingWanT2V480PConfig,
@@ -682,6 +683,18 @@ def _register_configs() -> None:
682683
model_family="wan",
683684
default_preset="fast_wan_2_2_ti2v_5b",
684685
)
686+
register_configs(
687+
sampling_param_cls=None,
688+
pipeline_config_cls=LucyEditDevConfig,
689+
workload_types=(),
690+
hf_model_paths=[
691+
"decart-ai/Lucy-Edit-Dev",
692+
"decart-ai/Lucy-Edit-1.1-Dev",
693+
],
694+
model_detectors=[lambda path: "lucy-edit" in path.lower()],
695+
model_family="wan",
696+
default_preset="lucy_edit_dev",
697+
)
685698
register_configs(
686699
sampling_param_cls=None,
687700
pipeline_config_cls=Wan2_2_T2V_A14B_Config,

0 commit comments

Comments
 (0)