Files
2026-07-13 13:16:54 +08:00

407 lines
20 KiB
Python

from __future__ import annotations
import base64
from typing import Callable, Optional
import gradio as gr
from .helpers import *
from .settings import *
from .styles import APP_CSS
def build_running_status_markdown() -> str:
return "Running..."
def get_logo_data_uri() -> str:
if not LANCE_LOGO_PATH.exists():
return ""
encoded_logo = base64.b64encode(LANCE_LOGO_PATH.read_bytes()).decode("ascii")
return f"data:image/webp;base64,{encoded_logo}"
def build_header_html() -> str:
logo_data_uri = get_logo_data_uri()
logo_html = (
f'<img class="lance-logo" src="{logo_data_uri}" alt="Lance logo">'
if logo_data_uri
else ""
)
return f"""
<div class="lance-hero">
{logo_html}
<h1 class="lance-title">Lance: Unified Multimodal Modeling by Multi-Task Synergy</h1>
<div class="lance-badges">
<a href="{LANCE_HOMEPAGE_URL}" target="_blank" rel="noopener noreferrer">
<img alt="Homepage" src="https://img.shields.io/badge/Homepage-Lance-2563eb?style=flat&labelColor=475569">
</a>
<a href="{LANCE_PAPER_URL}" target="_blank" rel="noopener noreferrer">
<img alt="Paper" src="https://img.shields.io/badge/Paper-arXiv-2563eb?style=flat&labelColor=475569&logo=arxiv">
</a>
<a href="{LANCE_HUGGING_FACE_URL}" target="_blank" rel="noopener noreferrer">
<img alt="Hugging Face" src="https://img.shields.io/badge/Model-HuggingFace-2563eb?style=flat&labelColor=475569&logo=huggingface">
</a>
<a href="{LANCE_GITHUB_URL}" target="_blank" rel="noopener noreferrer">
<img alt="GitHub" src="https://img.shields.io/badge/Code-GitHub-2563eb?style=flat&labelColor=475569&logo=github">
</a>
</div>
</div>
"""
def update_task_ui(task: str):
internal_task = normalize_task(task)
is_image_task = internal_task in IMAGE_TASKS
is_edit_task = internal_task in EDIT_TASKS
is_understanding_task = internal_task in UNDERSTANDING_TASKS
is_text_to_visual_task = internal_task in {TASK_T2V, TASK_T2I, TASK_I2V}
resolution_choices = get_resolution_choices_for_task(internal_task)
resolution_value = get_default_resolution_for_task(internal_task)
video_duration_value = get_default_video_duration_seconds(internal_task)
aspect_ratio_value = DEFAULT_IMAGE_ASPECT_RATIO if is_image_task else DEFAULT_VIDEO_ASPECT_RATIO
width_value, height_value = get_size_for_aspect_ratio(internal_task, aspect_ratio_value, resolution_value)
size_markdown = format_size_markdown(internal_task, width_value, height_value)
system_prompt_choices = get_understanding_system_prompt_choices(internal_task)
if is_text_to_visual_task:
text_label = "Prompt"
text_placeholder = "Describe what you want to generate..."
elif is_edit_task:
text_label = "Instruction"
text_placeholder = "Describe the edit you want..."
else:
text_label = "Question"
text_placeholder = "Ask a question about the input..."
if internal_task in {TASK_T2V, TASK_I2V, TASK_VIDEO_EDIT}:
output_label = "Output Video"
elif internal_task in {TASK_T2I, TASK_IMAGE_EDIT}:
output_label = "Output Image"
else:
output_label = "Output Text"
output_icon = "video" if output_label == "Output Video" else "image" if output_label == "Output Image" else "text"
show_aspect_ratio = internal_task in {TASK_T2V, TASK_T2I}
show_input_video = internal_task in {TASK_VIDEO_EDIT, TASK_X2T_VIDEO}
show_input_image = internal_task in {TASK_I2V, TASK_IMAGE_EDIT, TASK_X2T_IMAGE}
show_video_resolution_settings = internal_task in {TASK_T2V, TASK_I2V}
return (
gr.update(value=build_lance_label_html(text_label, "lance-prompt-label")),
gr.update(
label=text_label,
placeholder=text_placeholder,
visible=True,
value="",
),
gr.update(
choices=system_prompt_choices,
value=system_prompt_choices[0],
visible=False,
),
# Switching task pages should always start from a clean input state.
# Clear both visual input boxes even if one of them stays visible across tasks.
gr.update(label="Input Video", visible=show_input_video, value=None),
gr.update(label="Input Image", visible=show_input_image, value=None),
gr.update(visible=show_aspect_ratio),
gr.update(visible=False),
gr.update(visible=internal_task in {TASK_T2V, TASK_I2V}),
gr.update(visible=show_video_resolution_settings),
gr.update(choices=get_aspect_ratio_choices_for_task(internal_task), value=aspect_ratio_value, visible=show_aspect_ratio),
gr.update(value=height_value),
gr.update(value=width_value),
gr.update(choices=get_output_resolution_choices_for_task(internal_task, resolution_value), value=size_markdown, visible=False),
gr.update(visible=internal_task in {TASK_T2V, TASK_I2V}, value=video_duration_value),
gr.update(choices=resolution_choices, value=resolution_value, visible=show_video_resolution_settings),
gr.update(value=build_lance_icon_label_html(output_label, output_icon, "lance-output-label")),
gr.update(visible=internal_task in {TASK_T2V, TASK_I2V, TASK_VIDEO_EDIT}),
gr.update(visible=internal_task in {TASK_T2I, TASK_IMAGE_EDIT}),
gr.update(visible=is_understanding_task, value=""),
gr.update(visible=internal_task == TASK_T2V),
gr.update(visible=internal_task == TASK_I2V),
gr.update(visible=internal_task == TASK_VIDEO_EDIT),
gr.update(visible=internal_task == TASK_X2T_VIDEO),
gr.update(visible=internal_task == TASK_T2I),
gr.update(visible=internal_task == TASK_IMAGE_EDIT),
gr.update(visible=internal_task == TASK_X2T_IMAGE),
)
def build_demo(run_task_fn: Callable) -> gr.Blocks:
with gr.Blocks(title="Lance", css=APP_CSS) as demo:
gr.HTML(build_header_html())
with gr.Column(elem_classes=["lance-taskbar-wrap"]):
task = gr.Radio(
label="Task",
show_label=False,
choices=TASK_CHOICES,
value=TASK_LABEL_VIDEO_GENERATION,
elem_classes=["task-selector"],
)
with gr.Row(elem_classes=["lance-main-row"]):
with gr.Column(scale=1, elem_classes=["lance-main-column", "lance-input-column"]):
with gr.Column(elem_classes=["lance-panel", "lance-task-prompt-panel"]):
prompt_label = gr.HTML(build_lance_label_html("Prompt", "lance-prompt-label"), elem_classes=["lance-label-html"])
prompt = gr.Textbox(
label="Prompt",
show_label=False,
lines=6,
placeholder="Describe the video you want to generate...",
elem_classes=["main-prompt-control"],
)
with gr.Row(elem_classes=["prompt-options"]):
with gr.Group(elem_classes=["prompt-chip", "video-resolution-row"]) as video_resolution_row:
resolution = gr.Dropdown(
label="Video Resolution",
show_label=False,
choices=VIDEO_RESOLUTION_DISPLAY_CHOICES,
value=DEFAULT_RESOLUTION,
allow_custom_value=True,
elem_classes=["generation-control"],
)
with gr.Group(elem_classes=["prompt-chip", "aspect-ratio-row"]) as aspect_ratio_row:
aspect_ratio = gr.Dropdown(
label="Aspect Ratio",
show_label=False,
choices=get_aspect_ratio_choices_for_task(TASK_T2V),
value=DEFAULT_VIDEO_ASPECT_RATIO,
elem_classes=["generation-control"],
)
with gr.Group(elem_classes=["prompt-chip", "video-duration-row"]) as video_duration_row:
num_frames = gr.Dropdown(
label="Video Duration",
show_label=False,
choices=get_video_duration_choices(),
value=DEFAULT_VIDEO_DURATION_SECONDS,
elem_classes=["generation-control"],
)
with gr.Group(visible=False, elem_classes=["prompt-chip", "output-resolution-row"]) as output_resolution_row:
real_size = gr.Dropdown(
label="Output Resolution",
show_label=False,
choices=get_output_resolution_choices_for_task(TASK_T2V),
value=format_size_markdown(TASK_T2V, DEFAULT_WIDTH, DEFAULT_HEIGHT),
interactive=False,
visible=False,
allow_custom_value=True,
elem_classes=["generation-control"],
)
system_prompt = gr.Dropdown(
label="System Prompt",
choices=get_understanding_system_prompt_choices(TASK_X2T_VIDEO),
value=V2T_QA_SYSTEM_PROMPT,
visible=False,
allow_custom_value=True,
)
input_video = gr.Video(label="Input Video", visible=False, elem_classes=["lance-display-frame"])
input_image = gr.Image(label="Input Image", type="filepath", visible=False, elem_classes=["lance-display-frame"])
height = gr.Number(value=DEFAULT_HEIGHT, precision=0, visible=False)
width = gr.Number(value=DEFAULT_WIDTH, precision=0, visible=False)
with gr.Accordion("Advanced Parameters", open=False, elem_classes=["lance-advanced-accordion"]):
seed = gr.Number(label="Seed (-1 for random seed)", value=DEFAULT_BASIC_SEED, precision=0)
validation_num_timesteps = gr.Slider(
minimum=1,
maximum=50,
step=1,
value=DEFAULT_TIMESTEPS,
label="Validation Num Timesteps",
)
with gr.Row():
validation_timestep_shift = gr.Number(label="Validation Timestep Shift", value=DEFAULT_TIMESTEP_SHIFT)
cfg_text_scale = gr.Number(label="CFG Text Scale", value=DEFAULT_CFG_TEXT_SCALE)
with gr.Column(scale=1, elem_classes=["lance-main-column", "lance-output-column"]):
with gr.Column(elem_classes=["lance-panel", "lance-output-panel"]):
output_label = gr.HTML(
build_lance_icon_label_html("Output Video", "video", "lance-output-label"),
elem_classes=["lance-label-html"],
)
output_video = gr.Video(label="Output Video", show_label=False, elem_classes=["lance-display-frame", "output-media-control"])
output_image = gr.Image(label="Output Image", show_label=False, type="filepath", visible=False, elem_classes=["lance-display-frame", "output-media-control"])
output_text = gr.Textbox(label="Output Text", show_label=False, lines=3, visible=False, elem_classes=["lance-display-frame", "output-text-control"])
status = gr.Markdown("", elem_classes=["lance-run-status"])
run_button = gr.Button("🚀 Generate", variant="primary", elem_classes=["lance-run-button"])
def build_prompt_example_table(examples: list[list], media_type: Optional[str] = None):
"""Recommended example list with complete-fit reference media previews."""
example_buttons = []
with gr.Column(elem_classes=["prompt-example-full-table"]):
for row in examples:
example_prompt = str(row[0]) if row else ""
preview_video_path = input_video_path = None
preview_image_path = input_image_path = None
if media_type == "video":
preview_video_path = str(row[1]) if len(row) > 1 and row[1] else None
input_video_path = str(row[2]) if len(row) > 2 and row[2] else preview_video_path
elif media_type == "image":
preview_image_path = str(row[3]) if len(row) > 3 and row[3] else (str(row[2]) if len(row) > 2 and row[2] else None)
input_image_path = str(row[4]) if len(row) > 4 and row[4] else preview_image_path
button_label = example_prompt if len(example_prompt) <= 360 else f"{example_prompt[:357]}..."
if media_type in {"video", "image"}:
with gr.Row(elem_classes=["prompt-example-multimodal-row"]):
with gr.Column(elem_classes=["prompt-example-prompt-cell"]):
example_button = gr.Button(
button_label,
variant="secondary",
elem_classes=["prompt-example-row-button"],
)
with gr.Column(elem_classes=["prompt-example-media-cell"]):
if media_type == "video":
gr.Video(
value=preview_video_path,
show_label=False,
interactive=False,
elem_classes=["prompt-example-media-html", "prompt-example-video"],
)
else:
gr.HTML(
build_example_media_html(preview_image_path, "image"),
elem_classes=["prompt-example-media-html"],
)
else:
example_button = gr.Button(
button_label,
variant="secondary",
elem_classes=["prompt-example-row-button"],
)
example_buttons.append((example_button, example_prompt, input_video_path, input_image_path))
return example_buttons
def examples_section(title: str, examples: list[list], media_type: Optional[str] = None, visible: bool = False):
with gr.Column(visible=visible, elem_classes=["lance-recommended-section"]) as group:
gr.HTML(build_lance_label_html(title, "lance-section-label"), elem_classes=["lance-label-html"])
with gr.Group(elem_classes=["example-panel", "prompt-examples"]):
buttons = build_prompt_example_table(examples, media_type=media_type)
return group, buttons
video_generation_examples_group, video_generation_example_buttons = examples_section(
"Text-to-Video recommended cases", VIDEO_GENERATION_EXAMPLES, visible=True
)
image_to_video_examples_group, image_to_video_example_buttons = examples_section(
"Image-to-Video recommended cases", IMAGE_TO_VIDEO_EXAMPLES, media_type="image"
)
video_edit_examples_group, video_edit_example_buttons = examples_section(
"Video edit recommended cases", VIDEO_EDIT_EXAMPLES, media_type="video"
)
video_understanding_examples_group, video_understanding_example_buttons = examples_section(
"Video understanding recommended cases", VIDEO_UNDERSTANDING_EXAMPLES, media_type="video"
)
image_generation_examples_group, image_generation_example_buttons = examples_section(
"Text-to-Image recommended cases", IMAGE_GENERATION_EXAMPLES
)
image_edit_examples_group, image_edit_example_buttons = examples_section(
"Image edit recommended cases", IMAGE_EDIT_EXAMPLES, media_type="image"
)
image_understanding_examples_group, image_understanding_example_buttons = examples_section(
"Image understanding recommended cases", IMAGE_UNDERSTANDING_EXAMPLES, media_type="image"
)
task.change(
fn=update_task_ui,
inputs=[task],
outputs=[
prompt_label,
prompt,
system_prompt,
input_video,
input_image,
aspect_ratio_row,
output_resolution_row,
video_duration_row,
video_resolution_row,
aspect_ratio,
height,
width,
real_size,
num_frames,
resolution,
output_label,
output_video,
output_image,
output_text,
video_generation_examples_group,
image_to_video_examples_group,
video_edit_examples_group,
video_understanding_examples_group,
image_generation_examples_group,
image_edit_examples_group,
image_understanding_examples_group,
],
)
aspect_ratio.change(
fn=update_size_from_aspect_ratio,
inputs=[task, aspect_ratio, resolution],
outputs=[height, width, real_size],
queue=False,
show_api=False,
)
# real_size is hidden and derived from task/resolution/aspect_ratio.
# Do not attach a .change handler here: dynamic Dropdown choices can briefly
# contain 360p values while the selected value is 480p (or vice versa),
# which makes Gradio reject the stale value during preprocessing.
resolution.change(
fn=update_output_resolution_from_video_profile,
inputs=[task, aspect_ratio, resolution],
outputs=[real_size, height, width],
queue=False,
show_api=False,
)
for example_button, example_prompt, _, _ in video_generation_example_buttons + image_generation_example_buttons:
example_button.click(
fn=make_prompt_example_click_handler(example_prompt),
inputs=[task],
outputs=[prompt, system_prompt, aspect_ratio, height, width, num_frames, resolution, real_size],
queue=False,
show_api=False,
)
for example_button, example_prompt, example_video, example_image in (
image_to_video_example_buttons
+ video_edit_example_buttons
+ video_understanding_example_buttons
+ image_edit_example_buttons
+ image_understanding_example_buttons
):
example_button.click(
fn=make_media_prompt_example_click_handler(example_prompt, example_video, example_image),
inputs=[task],
outputs=[prompt, input_video, input_image, system_prompt, aspect_ratio, height, width, num_frames, resolution, real_size],
queue=False,
show_api=False,
)
run_button.click(
fn=build_running_status_markdown,
inputs=[],
outputs=[status],
queue=False,
show_api=False,
).then(
fn=run_task_fn,
inputs=[
task,
prompt,
system_prompt,
input_video,
input_image,
height,
width,
num_frames,
seed,
resolution,
validation_num_timesteps,
validation_timestep_shift,
cfg_text_scale,
],
outputs=[output_video, output_image, output_text, status],
show_progress="minimal",
)
return demo