File size: 8,581 Bytes
c232be3
 
 
 
 
3848f65
c232be3
 
 
 
 
3848f65
5ec1d9b
c232be3
 
abb9ea5
b54a3fb
abb9ea5
 
 
94439e4
c232be3
 
94439e4
b54a3fb
 
4e257f3
94439e4
c232be3
 
b54a3fb
4e257f3
b54a3fb
94439e4
b54a3fb
4e257f3
 
b54a3fb
 
4e257f3
 
94439e4
abb9ea5
94439e4
b54a3fb
c232be3
b54a3fb
4e257f3
b54a3fb
94439e4
b54a3fb
4e257f3
 
b54a3fb
 
4e257f3
 
94439e4
abb9ea5
b54a3fb
c232be3
170e83c
c232be3
 
 
685442e
 
 
 
 
 
64c0c7c
c232be3
64c0c7c
 
94439e4
64c0c7c
 
 
 
 
94439e4
64c0c7c
 
 
c232be3
64c0c7c
 
 
94439e4
64c0c7c
 
 
 
 
 
 
 
94439e4
64c0c7c
 
94439e4
64c0c7c
 
 
94439e4
64c0c7c
 
 
 
 
 
 
 
 
 
 
94439e4
64c0c7c
 
 
 
94439e4
64c0c7c
94439e4
64c0c7c
 
c232be3
8887124
816a397
8887124
 
c41ebfd
7a0db94
c41ebfd
 
 
a5aca83
 
c41ebfd
 
 
 
94439e4
c232be3
 
 
 
94439e4
c232be3
 
 
ffa8928
c232be3
94439e4
c232be3
 
 
 
 
 
 
 
 
 
 
94439e4
c232be3
 
 
 
 
 
 
 
94439e4
c232be3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94439e4
c232be3
 
 
 
 
 
 
 
94439e4
2c20354
94439e4
c232be3
526013e
1b3a9ec
f92472c
1b3a9ec
94439e4
 
1b3a9ec
18076f6
1b3a9ec
 
 
 
94439e4
c232be3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94439e4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
import gradio as gr
import torch
import spaces
from typing import List
from PIL import Image
from diffusers import LucyEditPipeline, AutoencoderKLWan
from diffusers.utils import export_to_video, load_video
import tempfile
import os

model_id = "decart-ai/Lucy-Edit-Dev"
vae = AutoencoderKLWan.from_pretrained(model_id, subfolder="vae", torch_dtype=torch.float32)
pipe = LucyEditPipeline.from_pretrained(model_id, vae=vae, torch_dtype=torch.bfloat16)
pipe.to("cuda")

def calculate_resolution(input_width, input_height, min_dimension=480, max_dimension=832, compatible_round=32):
    """Calculate optimal resolution preserving aspect ratio within bounds"""
    # Ensure dimensions are multiples of the compatible rounding
    def round_to(x, compatible_round):
        return max(min_dimension, min(max_dimension, int(round(x / compatible_round) * compatible_round)))

    # Get aspect ratio
    aspect_ratio = input_width / input_height

    # Square videos (aspect ratio close to 1:1)
    if 0.98 <= aspect_ratio <= 1.02:
        return 640, 640

    # Landscape videos (width > height)
    elif aspect_ratio > 1:
        # Try to use max width
        new_width = max_dimension
        new_height = new_width / aspect_ratio

        # If height would be too small, use min height
        if new_height < min_dimension:
            new_height = min_dimension
            new_width = new_height * aspect_ratio
            # If width exceeds max, clamp it
            if new_width > max_dimension:
                new_width = max_dimension

        return round_to(new_width, compatible_round), round_to(new_height, compatible_round)

    # Portrait videos (height > width)
    else:
        # Try to use max height
        new_height = max_dimension
        new_width = new_height * aspect_ratio

        # If width would be too small, use min width
        if new_width < min_dimension:
            new_width = min_dimension
            new_height = new_width / aspect_ratio
            # If height exceeds max, clamp it
            if new_height > max_dimension:
                new_height = max_dimension

        return round_to(new_width, compatible_round), round_to(new_height, compatible_round)


@spaces.GPU(duration=120)
def process_video(
    video_path,
    prompt,
    negative_prompt="",
    num_frames=81,
    auto_resize=True,
    manual_height=480,
    manual_width=832,
    guidance_scale=5,
    progress=gr.Progress(track_tqdm=True)
):
    # Load and preprocess video
    progress(0.2, desc="Loading video...")

    # Get video dimensions
    temp_video = load_video(video_path)
    print(len(temp_video))
    if temp_video and len(temp_video) > 0:
        original_width, original_height = temp_video[0].size

        # Calculate dimensions
        if auto_resize:
            width, height = calculate_resolution(original_width, original_height)
        else:
            width, height = manual_width, manual_height
    else:
        raise gr.Error("Could not load video or video is empty")

    # Convert video function
    def convert_video(video: List[Image.Image]) -> List[Image.Image]:
        # Ensure we don't exceed the video length
        frames_to_load = min(len(video), num_frames)
        video_frames = video[:frames_to_load]
        # Resize frames
        video_frames = [frame.resize((width, height)) for frame in video_frames]
        return video_frames

    # Load video from file path
    video = load_video(video_path, convert_method=convert_video)

    # Ensure we have the right number of frames
    if len(video) < num_frames:
        num_frames = len(video)

    # Generate edited video
    progress(0.5, desc="Generating edited video...")
    output = pipe(
        prompt=prompt,
        video=video,
        negative_prompt=negative_prompt,
        height=height,
        width=width,
        num_frames=num_frames,
        guidance_scale=guidance_scale,
    ).frames[0]

    # Export to temporary file
    progress(0.9, desc="Exporting video...")
    with tempfile.NamedTemporaryFile(delete=False, suffix=".mp4") as tmp_file:
        output_path = tmp_file.name

    export_to_video(output, output_path, fps=24)

    progress(1.0, desc="Complete!")
    return output_path

css = '''
.fillable{max-width: 1100px !important}
'''
with gr.Blocks(title="Lucy Edit - Video Editing with Text", css=css) as demo:
    gr.HTML(f"""<p align="center">
  <img src="https://huggingface.co/decart-ai/Lucy-Edit-Dev/resolve/main/assets/logo.png" width="480" style="margin-top: -25px" alt="Lucy Edit Dev Logo"/>
</p>

<p align="center">
  🤗 <a href="https://github.com/DecartAI/lucy-edit-comfyui"><b>Model</b></a>
  &nbsp;|&nbsp; 🧪 <a href="https://github.com/DecartAI/lucy-edit-comfyui"><b>ComfyUI</b></a>
  &nbsp;|&nbsp; 📖 <a href="https://platform.decart.ai">Playground</a>
  &nbsp;|&nbsp; 📑 <a href="#">arXiv (Coming soon)</a>
  &nbsp;|&nbsp; 💬 <a href="https://discord.gg/decart">Discord</a>
</p>""")

    with gr.Row():
        with gr.Column(scale=1):
            # Input controls
            video_input = gr.Video(label="Input Video")

            prompt = gr.Textbox(
                label="Edit Prompt",
                placeholder="Describe what you want to change in the video...",
                lines=3
            )

            with gr.Accordion("Advanced Settings", open=False):
                negative_prompt = gr.Textbox(
                    label="Negative Prompt (optional)",
                    placeholder="Describe what you DON'T want in the video...",
                    lines=2
                )
                auto_resize = gr.Checkbox(
                    label="Auto-resize (preserve aspect ratio)",
                    value=True,
                    info="Automatically calculate dimensions based on input video"
                )

                num_frames = gr.Slider(
                    label="Number of Frames",
                    minimum=1,
                    maximum=120,
                    value=81,
                    step=1,
                    info="More frames = longer processing time"
                )

                with gr.Row():
                    manual_height = gr.Slider(
                        label="Height (when auto-resize is off)",
                        minimum=256,
                        maximum=1024,
                        value=480,
                        step=32
                    )
                    manual_width = gr.Slider(
                        label="Width (when auto-resize is off)",
                        minimum=256,
                        maximum=1024,
                        value=832,
                        step=32
                    )

                guidance_scale = gr.Slider(
                    label="Guidance Scale",
                    minimum=1.0,
                    maximum=20.0,
                    value=5.0,
                    step=0.5,
                    info="Higher values follow the prompt more strictly"
                )

            generate_btn = gr.Button("Edit Video", variant="primary")

        with gr.Column(scale=1):
            video_output = gr.Video(label="Edited Video", autoplay=True)

    gr.Examples(
        examples=[
            ["examples/neon.mp4", "Add a colorful scarlet macaw parrot perched on the man's left shoulder, bright red and blue wing feathers with yellow accents, curved black beak, intelligent dark eyes, talons gripping fabric naturally, long tail feathers extending downward, glossy plumage catching light, slight wing adjustment for balance, natural weight distribution, soft shadow beneath bird."],
            ["examples/painter.mp4",  "Change the hair color to platinum blonde with natural highlights, subtle root shadowing, silky texture, gentle waves, soft shine, dimensional tones, strand definition, natural movement, professional color treatment, salon-quality finish, light-catching shimmer, varied blonde shades from honey to ash, realistic color gradation, healthy glossy appearance, volumetric lighting interaction."],
        ],
        inputs=[video_input, prompt],
        outputs=video_output,
        fn=process_video,
        cache_examples="lazy",
    )

    # Event handlers
    generate_btn.click(
        fn=process_video,
        inputs=[
            video_input,
            prompt,
            negative_prompt,
            num_frames,
            auto_resize,
            manual_height,
            manual_width,
            guidance_scale
        ],
        outputs=video_output
    )

if __name__ == "__main__":
    demo.launch(share=True)