Spaces:
Running on Zero
Running on Zero
Images/audio/video tab order, expandable image slots, native attention for the VAEs
Browse files
app.py
CHANGED
|
@@ -66,6 +66,9 @@ MAX_UI_DURATION = 14
|
|
| 66 |
MIN_DURATION = 5
|
| 67 |
# A reference video shorter than 2 s gives the model almost no motion to read, and 15 s is the checkpoint's ceiling.
|
| 68 |
MIN_REFERENCE_VIDEO, MAX_REFERENCE_VIDEO = 2.0, 15.0
|
|
|
|
|
|
|
|
|
|
| 69 |
|
| 70 |
|
| 71 |
def snap_frames(seconds: float) -> int:
|
|
@@ -121,6 +124,23 @@ def load_models() -> str | None:
|
|
| 121 |
print(f"[ref2va] loading {[c.name for c in blocks.expected_components]} from {MODEL_REPO} ...", flush=True)
|
| 122 |
pipe = blocks.init_pipeline(MODEL_REPO, components_manager=manager, collection="h3")
|
| 123 |
pipe.load_components(dtype=torch.bfloat16, token=token)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
pipe.transformer_ref.set_attention_backend(ATTENTION)
|
| 125 |
|
| 126 |
# Still startup, still free: an AoTI package carries no weights and opens its compiled archive lazily inside
|
|
@@ -195,18 +215,19 @@ def probe(path: str) -> tuple[float | None, float | None]:
|
|
| 195 |
return video, audio
|
| 196 |
|
| 197 |
|
| 198 |
-
def collect(
|
| 199 |
"""The `(kind, path)` references of a request, **in the order the model reads them**.
|
| 200 |
|
| 201 |
That order is semantic rather than cosmetic: it numbers the labels of MiniMax-H3's prompt presentation and it
|
| 202 |
advances the shared audio/video rotary clock, so the same references in a different order are a different
|
| 203 |
-
request.
|
| 204 |
-
|
| 205 |
"""
|
| 206 |
-
ordered = [("
|
| 207 |
-
ordered += [("image", path) for path in (first_image_path, second_image_path) if path]
|
| 208 |
if audio_path:
|
| 209 |
ordered.append(("audio", audio_path))
|
|
|
|
|
|
|
| 210 |
return ordered
|
| 211 |
|
| 212 |
|
|
@@ -222,10 +243,13 @@ def audio_bearing(references: list[tuple[str, str]]) -> list[tuple[str, float]]:
|
|
| 222 |
return carried
|
| 223 |
|
| 224 |
|
| 225 |
-
def duration_controls(
|
| 226 |
-
"""Show the duration slider unless a single soundtrack can set it, which is when MiniMax-H3 lets it be left out.
|
|
|
|
|
|
|
|
|
|
| 227 |
try:
|
| 228 |
-
carried = audio_bearing(collect(
|
| 229 |
except Exception:
|
| 230 |
carried = []
|
| 231 |
# Exactly one soundtrack, and one long enough to be a duration MiniMax-H3 generates. Anything else and the
|
|
@@ -312,10 +336,17 @@ def _generate(prompt_embeds, text_token_tags, references, height, width, num_fra
|
|
| 312 |
|
| 313 |
def generate(
|
| 314 |
prompt,
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 318 |
audio_path=None,
|
|
|
|
| 319 |
canvas=DEFAULT_CANVAS,
|
| 320 |
match=True,
|
| 321 |
duration=5,
|
|
@@ -330,7 +361,8 @@ def generate(
|
|
| 330 |
|
| 331 |
from diffusers.utils import encode_video
|
| 332 |
|
| 333 |
-
|
|
|
|
| 334 |
check(prompt, references)
|
| 335 |
|
| 336 |
# `0` is "leave it to the references" over the wire, which MiniMax-H3 accepts when exactly one of them carries a
|
|
@@ -382,8 +414,8 @@ INTRO = """# MiniMax-H3
|
|
| 382 |
</div>
|
| 383 |
|
| 384 |
**MiniMax-H3** is a 33B parameter state of the art video generation model that produces video and a
|
| 385 |
-
fully synchronized soundtrack (ambience, foley, speech). Bring your own subject,
|
| 386 |
-
reference — they are read **in tab order**,
|
| 387 |
"""
|
| 388 |
|
| 389 |
CSS = """
|
|
@@ -391,7 +423,7 @@ CSS = """
|
|
| 391 |
.dark .gradio-container { color: var(--body-text-color); }
|
| 392 |
"""
|
| 393 |
|
| 394 |
-
with gr.Blocks(title="MiniMax-H3
|
| 395 |
gr.Markdown(INTRO)
|
| 396 |
|
| 397 |
with gr.Row():
|
|
@@ -404,14 +436,24 @@ with gr.Blocks(title="MiniMax-H3 references") as demo:
|
|
| 404 |
# One tab per modality, in the order the model reads them. A reference left in a tab that is not the
|
| 405 |
# open one is still part of the request — the tabs lay the slots out, they do not choose between them.
|
| 406 |
with gr.Tabs():
|
| 407 |
-
with gr.Tab("Video"):
|
| 408 |
-
video = gr.Video(label="Motion & camera, 2–15 s. Its soundtrack comes along.")
|
| 409 |
with gr.Tab("Images"):
|
|
|
|
|
|
|
| 410 |
with gr.Row():
|
| 411 |
-
|
| 412 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 413 |
with gr.Tab("Audio"):
|
| 414 |
audio = gr.Audio(label="A voice or a piece of music", type="filepath")
|
|
|
|
|
|
|
| 415 |
run = gr.Button("Generate", variant="primary")
|
| 416 |
with gr.Accordion("Advanced options", open=False):
|
| 417 |
canvas = gr.Dropdown(label="Canvas", choices=list(CANVASES), value=DEFAULT_CANVAS)
|
|
@@ -425,15 +467,26 @@ with gr.Blocks(title="MiniMax-H3 references") as demo:
|
|
| 425 |
with gr.Column():
|
| 426 |
result = gr.Video(label="Video + soundtrack")
|
| 427 |
|
| 428 |
-
|
| 429 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 430 |
control.change(
|
| 431 |
-
duration_controls, [
|
| 432 |
)
|
| 433 |
|
| 434 |
run.click(
|
| 435 |
generate,
|
| 436 |
-
[prompt, *
|
| 437 |
result,
|
| 438 |
api_name="generate",
|
| 439 |
)
|
|
|
|
| 66 |
MIN_DURATION = 5
|
| 67 |
# A reference video shorter than 2 s gives the model almost no motion to read, and 15 s is the checkpoint's ceiling.
|
| 68 |
MIN_REFERENCE_VIDEO, MAX_REFERENCE_VIDEO = 2.0, 15.0
|
| 69 |
+
# `MINIMAX_H3_MAX_REFERENCE_IMAGES`, hardcoded so the UI renders before `diffusers` is importable. The slots are all
|
| 70 |
+
# built up front and revealed one at a time, because a demo asking for two subjects should not open with nine boxes.
|
| 71 |
+
MAX_IMAGE_SLOTS, OPEN_IMAGE_SLOTS = 9, 2
|
| 72 |
|
| 73 |
|
| 74 |
def snap_frames(seconds: float) -> int:
|
|
|
|
| 124 |
print(f"[ref2va] loading {[c.name for c in blocks.expected_components]} from {MODEL_REPO} ...", flush=True)
|
| 125 |
pipe = blocks.init_pipeline(MODEL_REPO, components_manager=manager, collection="h3")
|
| 126 |
pipe.load_components(dtype=torch.bfloat16, token=token)
|
| 127 |
+
|
| 128 |
+
# Pin the two autoencoders to torch SDPA *before* the transformer takes cuDNN, and in that order.
|
| 129 |
+
#
|
| 130 |
+
# `set_attention_backend` does two things: it stamps the backend onto every attention processor of the model
|
| 131 |
+
# it is called on, and it sets the registry's **global** active backend, which every processor that was not
|
| 132 |
+
# stamped then falls through to. Both VAEs carry `AttentionModuleMixin` attention with `_attention_backend =
|
| 133 |
+
# None`, so stamping only the transformer leaves them inheriting cuDNN — and they are float32, for which
|
| 134 |
+
# cuDNN has no kernel:
|
| 135 |
+
#
|
| 136 |
+
# RuntimeError: No available kernel. Aborting execution. # audio_vae pre_block, is_causal=True
|
| 137 |
+
#
|
| 138 |
+
# It is `ref2va` that exposes this. The keyframe half only ever *decodes* audio, and the audio VAE's
|
| 139 |
+
# attention is on its encoder side, so nothing reached it until a reference brought a soundtrack along.
|
| 140 |
+
# Stamping the VAEs first leaves both explicitly on `native`; the transformer then stamps itself and takes
|
| 141 |
+
# the global with it, which no longer matters to anyone.
|
| 142 |
+
pipe.vae.set_attention_backend("native")
|
| 143 |
+
pipe.audio_vae.set_attention_backend("native")
|
| 144 |
pipe.transformer_ref.set_attention_backend(ATTENTION)
|
| 145 |
|
| 146 |
# Still startup, still free: an AoTI package carries no weights and opens its compiled archive lazily inside
|
|
|
|
| 215 |
return video, audio
|
| 216 |
|
| 217 |
|
| 218 |
+
def collect(image_paths, audio_path, video_path) -> list[tuple[str, str]]:
|
| 219 |
"""The `(kind, path)` references of a request, **in the order the model reads them**.
|
| 220 |
|
| 221 |
That order is semantic rather than cosmetic: it numbers the labels of MiniMax-H3's prompt presentation and it
|
| 222 |
advances the shared audio/video rotary clock, so the same references in a different order are a different
|
| 223 |
+
request. Images first, then a standalone audio clip, then the video — the order the tabs are laid out in, so
|
| 224 |
+
what the UI shows is what the model is handed.
|
| 225 |
"""
|
| 226 |
+
ordered = [("image", path) for path in image_paths if path]
|
|
|
|
| 227 |
if audio_path:
|
| 228 |
ordered.append(("audio", audio_path))
|
| 229 |
+
if video_path:
|
| 230 |
+
ordered.append(("video", video_path))
|
| 231 |
return ordered
|
| 232 |
|
| 233 |
|
|
|
|
| 243 |
return carried
|
| 244 |
|
| 245 |
|
| 246 |
+
def duration_controls(audio_path, video_path, match: bool):
|
| 247 |
+
"""Show the duration slider unless a single soundtrack can set it, which is when MiniMax-H3 lets it be left out.
|
| 248 |
+
|
| 249 |
+
Only the audio and video slots matter here: an image reference never carries a waveform.
|
| 250 |
+
"""
|
| 251 |
try:
|
| 252 |
+
carried = audio_bearing(collect([], audio_path, video_path))
|
| 253 |
except Exception:
|
| 254 |
carried = []
|
| 255 |
# Exactly one soundtrack, and one long enough to be a duration MiniMax-H3 generates. Anything else and the
|
|
|
|
| 336 |
|
| 337 |
def generate(
|
| 338 |
prompt,
|
| 339 |
+
image_1=None,
|
| 340 |
+
image_2=None,
|
| 341 |
+
image_3=None,
|
| 342 |
+
image_4=None,
|
| 343 |
+
image_5=None,
|
| 344 |
+
image_6=None,
|
| 345 |
+
image_7=None,
|
| 346 |
+
image_8=None,
|
| 347 |
+
image_9=None,
|
| 348 |
audio_path=None,
|
| 349 |
+
video_path=None,
|
| 350 |
canvas=DEFAULT_CANVAS,
|
| 351 |
match=True,
|
| 352 |
duration=5,
|
|
|
|
| 361 |
|
| 362 |
from diffusers.utils import encode_video
|
| 363 |
|
| 364 |
+
images = [image_1, image_2, image_3, image_4, image_5, image_6, image_7, image_8, image_9]
|
| 365 |
+
references = collect(images, audio_path, video_path)
|
| 366 |
check(prompt, references)
|
| 367 |
|
| 368 |
# `0` is "leave it to the references" over the wire, which MiniMax-H3 accepts when exactly one of them carries a
|
|
|
|
| 414 |
</div>
|
| 415 |
|
| 416 |
**MiniMax-H3** is a 33B parameter state of the art video generation model that produces video and a
|
| 417 |
+
fully synchronized soundtrack (ambience, foley, speech). Bring your own subject, voice or camera move as a
|
| 418 |
+
reference — they are read **in tab order**, images then audio then video.
|
| 419 |
"""
|
| 420 |
|
| 421 |
CSS = """
|
|
|
|
| 423 |
.dark .gradio-container { color: var(--body-text-color); }
|
| 424 |
"""
|
| 425 |
|
| 426 |
+
with gr.Blocks(title="MiniMax-H3 Reference") as demo:
|
| 427 |
gr.Markdown(INTRO)
|
| 428 |
|
| 429 |
with gr.Row():
|
|
|
|
| 436 |
# One tab per modality, in the order the model reads them. A reference left in a tab that is not the
|
| 437 |
# open one is still part of the request — the tabs lay the slots out, they do not choose between them.
|
| 438 |
with gr.Tabs():
|
|
|
|
|
|
|
| 439 |
with gr.Tab("Images"):
|
| 440 |
+
# One `gr.Row`, so gradio splits the width evenly and wraps once the slots hit `min_width`
|
| 441 |
+
# rather than leaving a hole where a hidden slot used to be.
|
| 442 |
with gr.Row():
|
| 443 |
+
images = [
|
| 444 |
+
gr.Image(
|
| 445 |
+
label="Subject, style or scene",
|
| 446 |
+
type="filepath",
|
| 447 |
+
min_width=180,
|
| 448 |
+
visible=index < OPEN_IMAGE_SLOTS,
|
| 449 |
+
)
|
| 450 |
+
for index in range(MAX_IMAGE_SLOTS)
|
| 451 |
+
]
|
| 452 |
+
add_image = gr.Button("+ Add another image", size="sm", variant="secondary")
|
| 453 |
with gr.Tab("Audio"):
|
| 454 |
audio = gr.Audio(label="A voice or a piece of music", type="filepath")
|
| 455 |
+
with gr.Tab("Video"):
|
| 456 |
+
video = gr.Video(label="Motion & camera, 2–15 s. Its soundtrack comes along.")
|
| 457 |
run = gr.Button("Generate", variant="primary")
|
| 458 |
with gr.Accordion("Advanced options", open=False):
|
| 459 |
canvas = gr.Dropdown(label="Canvas", choices=list(CANVASES), value=DEFAULT_CANVAS)
|
|
|
|
| 467 |
with gr.Column():
|
| 468 |
result = gr.Video(label="Video + soundtrack")
|
| 469 |
|
| 470 |
+
open_slots = gr.State(OPEN_IMAGE_SLOTS)
|
| 471 |
+
|
| 472 |
+
def reveal_image_slot(open_count):
|
| 473 |
+
open_count = min(open_count + 1, MAX_IMAGE_SLOTS)
|
| 474 |
+
return [
|
| 475 |
+
open_count,
|
| 476 |
+
*[gr.update(visible=index < open_count) for index in range(MAX_IMAGE_SLOTS)],
|
| 477 |
+
gr.update(visible=open_count < MAX_IMAGE_SLOTS),
|
| 478 |
+
]
|
| 479 |
+
|
| 480 |
+
add_image.click(reveal_image_slot, open_slots, [open_slots, *images, add_image], api_name=False)
|
| 481 |
+
|
| 482 |
+
for control in (audio, video, match):
|
| 483 |
control.change(
|
| 484 |
+
duration_controls, [audio, video, match], [match, duration], show_progress="hidden", api_name=False
|
| 485 |
)
|
| 486 |
|
| 487 |
run.click(
|
| 488 |
generate,
|
| 489 |
+
[prompt, *images, audio, video, canvas, match, duration, steps, seed],
|
| 490 |
result,
|
| 491 |
api_name="generate",
|
| 492 |
)
|