Spaces:
Paused
Paused
Update app.py
Browse files
app.py
CHANGED
|
@@ -275,6 +275,93 @@ def get_duration(
|
|
| 275 |
return 80
|
| 276 |
else:
|
| 277 |
return 120
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 278 |
|
| 279 |
@spaces.GPU(duration=get_duration)
|
| 280 |
def generate_video(
|
|
@@ -290,7 +377,6 @@ def generate_video(
|
|
| 290 |
):
|
| 291 |
"""
|
| 292 |
Generate a short cinematic video from a text prompt and optional input image using the LTX-2 distilled pipeline.
|
| 293 |
-
|
| 294 |
Args:
|
| 295 |
input_image: Optional input image for image-to-video. If provided, it is injected at frame 0 to guide motion.
|
| 296 |
prompt: Text description of the scene, motion, and cinematic style to generate.
|
|
@@ -301,12 +387,10 @@ def generate_video(
|
|
| 301 |
height: Output video height in pixels.
|
| 302 |
width: Output video width in pixels.
|
| 303 |
progress: Gradio progress tracker.
|
| 304 |
-
|
| 305 |
Returns:
|
| 306 |
A tuple of:
|
| 307 |
- output_path: Path to the generated MP4 video file.
|
| 308 |
- seed: The seed used for generation.
|
| 309 |
-
|
| 310 |
Notes:
|
| 311 |
- Uses a fixed frame rate of 24 FPS.
|
| 312 |
- Prompt embeddings are generated externally to avoid reloading the text encoder.
|
|
@@ -484,7 +568,55 @@ css = """
|
|
| 484 |
}
|
| 485 |
"""
|
| 486 |
|
| 487 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 488 |
gr.HTML(
|
| 489 |
"""
|
| 490 |
<div style="text-align: center;">
|
|
@@ -511,6 +643,7 @@ with gr.Blocks(css=css, title="LTX-2 Video Distilled 🎥🔈") as demo:
|
|
| 511 |
with gr.Column(elem_id="col-container"):
|
| 512 |
with gr.Row():
|
| 513 |
with gr.Column(elem_id="step-column"):
|
|
|
|
| 514 |
input_image = gr.Image(
|
| 515 |
label="Input Image (Optional)",
|
| 516 |
type="pil",
|
|
@@ -547,31 +680,26 @@ with gr.Blocks(css=css, title="LTX-2 Video Distilled 🎥🔈") as demo:
|
|
| 547 |
|
| 548 |
randomize_seed = gr.Checkbox(label="Randomize Seed", value=True)
|
| 549 |
|
| 550 |
-
with gr.Row():
|
| 551 |
-
width = gr.Number(label="Width", value=DEFAULT_1_STAGE_WIDTH, precision=0)
|
| 552 |
-
height = gr.Number(label="Height", value=DEFAULT_1_STAGE_HEIGHT, precision=0)
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
with gr.Column(elem_id="step-column"):
|
| 556 |
|
|
|
|
|
|
|
| 557 |
output_video = gr.Video(label="Generated Video", autoplay=True, height=512)
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
show_label=False,
|
| 561 |
choices=["768x512", "512x512", "512x768"],
|
| 562 |
value=f"{DEFAULT_1_STAGE_WIDTH}x{DEFAULT_1_STAGE_HEIGHT}",
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
|
|
|
|
|
|
|
| 568 |
|
| 569 |
-
|
| 570 |
-
|
|
|
|
| 571 |
fn=apply_resolution,
|
| 572 |
-
inputs=
|
| 573 |
outputs=[width, height],
|
| 574 |
-
show_api=False
|
| 575 |
)
|
| 576 |
|
| 577 |
generate_btn.click(
|
|
@@ -619,4 +747,4 @@ with gr.Blocks(css=css, title="LTX-2 Video Distilled 🎥🔈") as demo:
|
|
| 619 |
|
| 620 |
|
| 621 |
if __name__ == "__main__":
|
| 622 |
-
demo.launch(ssr_mode=False, mcp_server=True)
|
|
|
|
| 275 |
return 80
|
| 276 |
else:
|
| 277 |
return 120
|
| 278 |
+
|
| 279 |
+
class RadioAnimated(gr.HTML):
|
| 280 |
+
"""
|
| 281 |
+
Animated segmented radio (like iOS pill selector).
|
| 282 |
+
Outputs: selected option string, e.g. "768x512"
|
| 283 |
+
"""
|
| 284 |
+
def __init__(self, choices, value=None, **kwargs):
|
| 285 |
+
if not choices or len(choices) < 2:
|
| 286 |
+
raise ValueError("RadioAnimated requires at least 2 choices.")
|
| 287 |
+
if value is None:
|
| 288 |
+
value = choices[0]
|
| 289 |
+
|
| 290 |
+
# Build labels/inputs HTML
|
| 291 |
+
inputs_html = "\n".join(
|
| 292 |
+
f"""
|
| 293 |
+
<input class="ra-input" type="radio" name="ra" id="ra-{i}" value="{c}">
|
| 294 |
+
<label class="ra-label" for="ra-{i}">{c}</label>
|
| 295 |
+
"""
|
| 296 |
+
for i, c in enumerate(choices)
|
| 297 |
+
)
|
| 298 |
+
|
| 299 |
+
html_template = f"""
|
| 300 |
+
<div class="ra-wrap" id="ra-wrap">
|
| 301 |
+
<div class="ra-inner" id="ra-inner">
|
| 302 |
+
<div class="ra-highlight" id="ra-highlight"></div>
|
| 303 |
+
{inputs_html}
|
| 304 |
+
</div>
|
| 305 |
+
</div>
|
| 306 |
+
"""
|
| 307 |
+
|
| 308 |
+
js_on_load = r"""
|
| 309 |
+
(() => {
|
| 310 |
+
const wrap = element.querySelector('#ra-wrap');
|
| 311 |
+
const inner = element.querySelector('#ra-inner');
|
| 312 |
+
const highlight = element.querySelector('#ra-highlight');
|
| 313 |
+
const inputs = Array.from(element.querySelectorAll('.ra-input'));
|
| 314 |
+
const labels = Array.from(element.querySelectorAll('.ra-label'));
|
| 315 |
+
|
| 316 |
+
if (!inputs.length) return;
|
| 317 |
+
|
| 318 |
+
const choices = inputs.map(i => i.value);
|
| 319 |
+
|
| 320 |
+
function setHighlightByIndex(idx) {
|
| 321 |
+
const n = choices.length;
|
| 322 |
+
const pct = 100 / n;
|
| 323 |
+
highlight.style.width = `calc(${pct}% - 6px)`;
|
| 324 |
+
highlight.style.transform = `translateX(${idx * 100}%)`;
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
function setCheckedByValue(val, shouldTrigger=false) {
|
| 328 |
+
const idx = Math.max(0, choices.indexOf(val));
|
| 329 |
+
inputs.forEach((inp, i) => { inp.checked = (i === idx); });
|
| 330 |
+
setHighlightByIndex(idx);
|
| 331 |
+
|
| 332 |
+
// Update props + fire change if requested
|
| 333 |
+
props.value = choices[idx];
|
| 334 |
+
if (shouldTrigger) trigger('change', props.value);
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
// Init from props.value
|
| 338 |
+
setCheckedByValue(props.value ?? choices[0], false);
|
| 339 |
+
|
| 340 |
+
// Click handlers
|
| 341 |
+
inputs.forEach((inp) => {
|
| 342 |
+
inp.addEventListener('change', () => {
|
| 343 |
+
setCheckedByValue(inp.value, true);
|
| 344 |
+
});
|
| 345 |
+
});
|
| 346 |
+
|
| 347 |
+
// Watch for python-side value updates
|
| 348 |
+
let last = props.value;
|
| 349 |
+
setInterval(() => {
|
| 350 |
+
if (props.value !== last) {
|
| 351 |
+
last = props.value;
|
| 352 |
+
setCheckedByValue(props.value ?? choices[0], false);
|
| 353 |
+
}
|
| 354 |
+
}, 100);
|
| 355 |
+
})();
|
| 356 |
+
"""
|
| 357 |
+
|
| 358 |
+
super().__init__(
|
| 359 |
+
value=value,
|
| 360 |
+
html_template=html_template,
|
| 361 |
+
js_on_load=js_on_load,
|
| 362 |
+
**kwargs
|
| 363 |
+
)
|
| 364 |
+
|
| 365 |
|
| 366 |
@spaces.GPU(duration=get_duration)
|
| 367 |
def generate_video(
|
|
|
|
| 377 |
):
|
| 378 |
"""
|
| 379 |
Generate a short cinematic video from a text prompt and optional input image using the LTX-2 distilled pipeline.
|
|
|
|
| 380 |
Args:
|
| 381 |
input_image: Optional input image for image-to-video. If provided, it is injected at frame 0 to guide motion.
|
| 382 |
prompt: Text description of the scene, motion, and cinematic style to generate.
|
|
|
|
| 387 |
height: Output video height in pixels.
|
| 388 |
width: Output video width in pixels.
|
| 389 |
progress: Gradio progress tracker.
|
|
|
|
| 390 |
Returns:
|
| 391 |
A tuple of:
|
| 392 |
- output_path: Path to the generated MP4 video file.
|
| 393 |
- seed: The seed used for generation.
|
|
|
|
| 394 |
Notes:
|
| 395 |
- Uses a fixed frame rate of 24 FPS.
|
| 396 |
- Prompt embeddings are generated externally to avoid reloading the text encoder.
|
|
|
|
| 568 |
}
|
| 569 |
"""
|
| 570 |
|
| 571 |
+
css += """
|
| 572 |
+
/* ---- radioanimated ---- */
|
| 573 |
+
.ra-wrap{
|
| 574 |
+
width: fit-content;
|
| 575 |
+
}
|
| 576 |
+
.ra-inner{
|
| 577 |
+
position: relative;
|
| 578 |
+
display: inline-flex;
|
| 579 |
+
align-items: center;
|
| 580 |
+
gap: 0;
|
| 581 |
+
padding: 6px;
|
| 582 |
+
background: #0b0b0b;
|
| 583 |
+
border-radius: 9999px;
|
| 584 |
+
overflow: hidden;
|
| 585 |
+
user-select: none;
|
| 586 |
+
}
|
| 587 |
+
.ra-input{
|
| 588 |
+
display: none;
|
| 589 |
+
}
|
| 590 |
+
.ra-label{
|
| 591 |
+
position: relative;
|
| 592 |
+
z-index: 2;
|
| 593 |
+
padding: 10px 18px;
|
| 594 |
+
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Arial;
|
| 595 |
+
font-size: 14px;
|
| 596 |
+
font-weight: 600;
|
| 597 |
+
color: rgba(255,255,255,0.7);
|
| 598 |
+
cursor: pointer;
|
| 599 |
+
transition: color 180ms ease;
|
| 600 |
+
white-space: nowrap;
|
| 601 |
+
}
|
| 602 |
+
.ra-highlight{
|
| 603 |
+
position: absolute;
|
| 604 |
+
z-index: 1;
|
| 605 |
+
top: 6px;
|
| 606 |
+
left: 6px;
|
| 607 |
+
height: calc(100% - 12px);
|
| 608 |
+
border-radius: 9999px;
|
| 609 |
+
background: #8bff97; /* green knob */
|
| 610 |
+
transition: transform 200ms ease, width 200ms ease;
|
| 611 |
+
}
|
| 612 |
+
/* selected label becomes darker like your screenshot */
|
| 613 |
+
.ra-input:checked + .ra-label{
|
| 614 |
+
color: rgba(0,0,0,0.75);
|
| 615 |
+
}
|
| 616 |
+
"""
|
| 617 |
+
|
| 618 |
+
|
| 619 |
+
with gr.Blocks(title="LTX-2 Video Distilled 🎥🔈") as demo:
|
| 620 |
gr.HTML(
|
| 621 |
"""
|
| 622 |
<div style="text-align: center;">
|
|
|
|
| 643 |
with gr.Column(elem_id="col-container"):
|
| 644 |
with gr.Row():
|
| 645 |
with gr.Column(elem_id="step-column"):
|
| 646 |
+
|
| 647 |
input_image = gr.Image(
|
| 648 |
label="Input Image (Optional)",
|
| 649 |
type="pil",
|
|
|
|
| 680 |
|
| 681 |
randomize_seed = gr.Checkbox(label="Randomize Seed", value=True)
|
| 682 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 683 |
|
| 684 |
+
|
| 685 |
+
with gr.Column(elem_id="step-column"):
|
| 686 |
output_video = gr.Video(label="Generated Video", autoplay=True, height=512)
|
| 687 |
+
|
| 688 |
+
radioanimated = RadioAnimated(
|
|
|
|
| 689 |
choices=["768x512", "512x512", "512x768"],
|
| 690 |
value=f"{DEFAULT_1_STAGE_WIDTH}x{DEFAULT_1_STAGE_HEIGHT}",
|
| 691 |
+
elem_id="radioanimated"
|
| 692 |
+
)
|
|
|
|
|
|
|
| 693 |
|
| 694 |
+
width = gr.Number(label="Width", value=DEFAULT_1_STAGE_WIDTH, precision=0, visible=False)
|
| 695 |
+
height = gr.Number(label="Height", value=DEFAULT_1_STAGE_HEIGHT, precision=0, visible=False)
|
| 696 |
|
| 697 |
+
generate_btn = gr.Button("🤩 Generate Video", variant="primary", elem_classes="button-gradient")
|
| 698 |
+
|
| 699 |
+
radioanimated.change(
|
| 700 |
fn=apply_resolution,
|
| 701 |
+
inputs=radioanimated,
|
| 702 |
outputs=[width, height],
|
|
|
|
| 703 |
)
|
| 704 |
|
| 705 |
generate_btn.click(
|
|
|
|
| 747 |
|
| 748 |
|
| 749 |
if __name__ == "__main__":
|
| 750 |
+
demo.launch(ssr_mode=False, mcp_server=True, css=css)
|