Spaces:
Running on Zero
Running on Zero
Update app.py
Browse files
app.py
CHANGED
|
@@ -421,23 +421,19 @@ def encode_remote(prompt, references, canvas, num_frames, rewrite_prompt=False):
|
|
| 421 |
|
| 422 |
|
| 423 |
@spaces.GPU(duration=get_duration, size=GPU_SIZE)
|
|
|
|
| 424 |
def _generate(prompt_embeds, text_token_tags, references, height, width, num_frames, steps, seed, loras=()):
|
| 425 |
"""The only thing on GPU time: the two reference encoders, the packed-sequence denoise loop and the decoders.
|
| 426 |
-
|
| 427 |
References cross as paths and are decoded here; only the three generated outputs come back. A `@spaces.GPU`
|
| 428 |
argument crosses a process boundary by pickling, a 5 s 1344x768 reference video is 370 MB of expanded frames, and
|
| 429 |
the full `PipelineState` still holds the packed latents and the rotary grid on the card.
|
| 430 |
-
|
| 431 |
The adapters are attached here rather than in the caller: `spaces` runs this body in its own worker, so the
|
| 432 |
transformer the request sees is the one that has to carry them.
|
| 433 |
"""
|
| 434 |
import torch
|
| 435 |
-
|
| 436 |
if PLACEMENT == "lazy":
|
| 437 |
PIPE.to("cuda")
|
| 438 |
-
|
| 439 |
-
apply_loras(PIPE.transformer_ref, loras or ())
|
| 440 |
-
|
| 441 |
state = PIPE(
|
| 442 |
prompt_embeds=prompt_embeds.to("cuda"),
|
| 443 |
text_token_tags=text_token_tags,
|
|
@@ -452,8 +448,6 @@ def _generate(prompt_embeds, text_token_tags, references, height, width, num_fra
|
|
| 452 |
|
| 453 |
|
| 454 |
def generate(
|
| 455 |
-
# Every parameter after `prompt` has a default, and the newest ones sit at the end, so a positional API client
|
| 456 |
-
# written against an older signature keeps working.
|
| 457 |
prompt,
|
| 458 |
image_1=None,
|
| 459 |
audio_path=None,
|
|
@@ -472,11 +466,9 @@ def generate(
|
|
| 472 |
steps=28,
|
| 473 |
seed=42,
|
| 474 |
upsample=False,
|
| 475 |
-
*lora_fields,
|
| 476 |
progress=gr.Progress(track_tqdm=True),
|
| 477 |
):
|
| 478 |
-
"""One request.
|
| 479 |
-
unaffected. `lora_fields` arrives as `reference, strength, reference, strength, ...`."""
|
| 480 |
if LOAD_ERROR:
|
| 481 |
raise gr.Error(LOAD_ERROR)
|
| 482 |
if PIPE is None:
|
|
@@ -493,8 +485,6 @@ def generate(
|
|
| 493 |
derivable = len(audio_bearing(references)) == 1
|
| 494 |
requested = 0 if (match and derivable) else snap_frames(duration)
|
| 495 |
|
| 496 |
-
loras, lora_labels = collect_loras(lora_fields, progress)
|
| 497 |
-
|
| 498 |
progress(0.0, desc="Upsampling the prompt ..." if upsample else "Reading the prompt and references ...")
|
| 499 |
conditioned = time.time()
|
| 500 |
try:
|
|
@@ -518,7 +508,7 @@ def generate(
|
|
| 518 |
progress(0.1, desc=f"Generating {num_frames / FPS:.1f} s at {width}x{height} ...")
|
| 519 |
started = time.time()
|
| 520 |
frames, audio, sampling_rate = _generate(
|
| 521 |
-
prompt_embeds, text_token_tags, references, height, width, num_frames, steps, seed
|
| 522 |
)
|
| 523 |
generate_seconds = time.time() - started
|
| 524 |
|
|
@@ -532,8 +522,7 @@ def generate(
|
|
| 532 |
f"({num_frames / FPS:.3f} s), {int(steps)} steps · conditioner {condition_seconds:.0f}s "
|
| 533 |
f"({plan['num_text_tokens']} tokens{', upsampled' if refined else ''}) · "
|
| 534 |
f"denoise + decode {generate_seconds:.0f}s "
|
| 535 |
-
f"({generate_seconds / int(steps):.1f} s/step) · seed {int(seed)}"
|
| 536 |
-
f"{' · LoRA ' + ', '.join(lora_labels) if lora_labels else ''}",
|
| 537 |
flush=True,
|
| 538 |
)
|
| 539 |
return path, refined, gr.update(visible=bool(refined))
|
|
@@ -547,11 +536,7 @@ def generate(
|
|
| 547 |
# is gone by the next visit, so a saved path would restore as a dead file rather than as the image.
|
| 548 |
|
| 549 |
SETTINGS_VERSION = 1
|
| 550 |
-
SETTINGS_KEYS =
|
| 551 |
-
["prompt", "upsample", "canvas", "match", "duration", "steps", "seed"]
|
| 552 |
-
+ [f"lora_{slot + 1}" for slot in range(LORA_SLOTS)]
|
| 553 |
-
+ [f"lora_{slot + 1}_scale" for slot in range(LORA_SLOTS)]
|
| 554 |
-
)
|
| 555 |
|
| 556 |
|
| 557 |
def save_settings(*values):
|
|
@@ -591,41 +576,9 @@ def load_settings(path):
|
|
| 591 |
return updates
|
| 592 |
|
| 593 |
|
| 594 |
-
def _fill_lora_slots(files, *current):
|
| 595 |
-
"""Drop `.safetensors` files on the uploader and their paths land in the first free slots, so a local adapter
|
| 596 |
-
needs no typing at all."""
|
| 597 |
-
slots = list(current)
|
| 598 |
-
for path in files or []:
|
| 599 |
-
for index, value in enumerate(slots):
|
| 600 |
-
if not (value or "").strip():
|
| 601 |
-
slots[index] = path
|
| 602 |
-
break
|
| 603 |
-
return [gr.update(value=value) for value in slots]
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
def _add_preset_lora(preset, *current):
|
| 607 |
-
"""Fill the first free LoRA slot with a preset adapter, set its strength to 1.0 and move the steps slider to the
|
| 608 |
-
preset's recommended count.
|
| 609 |
-
|
| 610 |
-
The Turbo presets are tuned for a specific step range, so the steps slider is moved along with the slot — it is the
|
| 611 |
-
one output beyond the LoRA fields. A slot already holding the same reference is a no-op, so the button can be
|
| 612 |
-
pressed twice without duplicating, and a full set of slots is left untouched.
|
| 613 |
-
"""
|
| 614 |
-
reference, steps, _ = LORA_PRESETS[preset]
|
| 615 |
-
slots = list(current[:LORA_SLOTS])
|
| 616 |
-
scales = list(current[LORA_SLOTS:])
|
| 617 |
-
if reference not in [(value or "").strip() for value in slots]:
|
| 618 |
-
for index, value in enumerate(slots):
|
| 619 |
-
if not (value or "").strip():
|
| 620 |
-
slots[index] = reference
|
| 621 |
-
scales[index] = 1.0
|
| 622 |
-
break
|
| 623 |
-
return [*slots, *scales, steps]
|
| 624 |
-
|
| 625 |
-
|
| 626 |
load_models()
|
| 627 |
|
| 628 |
-
INTRO = """# MiniMax-H3 Reference
|
| 629 |
|
| 630 |
<div align="center">
|
| 631 |
<a href="https://huggingface.co/MiniMaxAI/MiniMax-H3" target="_blank" rel="noopener"><strong>[ model ]</strong></a>
|
|
@@ -636,28 +589,6 @@ INTRO = """# MiniMax-H3 Reference Custom Lora
|
|
| 636 |
**MiniMax-H3** is a 33B parameter state of the art video generation model that produces video and a
|
| 637 |
fully synchronized soundtrack (ambience, foley, speech). Bring your own subject, voice or camera move as a
|
| 638 |
reference.
|
| 639 |
-
|
| 640 |
-
This Space ships with **pre-wired Turbo LoRA presets** from
|
| 641 |
-
[`larryvrh/MiniMax-H3-Turbo-Lora`](https://huggingface.co/larryvrh/MiniMax-H3-Turbo-Lora) that render joint video +
|
| 642 |
-
soundtrack in **4–8 steps** instead of the usual ~20 (a ~5× speedup). It also accepts **ComfyUI-trained LoRAs** —
|
| 643 |
-
adapters in the ComfyUI checkpoint naming are remapped to diffusers' module names on the fly, so a `.safetensors`
|
| 644 |
-
trained against the ComfyUI base works without a separate conversion step.
|
| 645 |
-
"""
|
| 646 |
-
|
| 647 |
-
LORA_HELP = """Each slot takes a Hugging Face repo (`owner/repo`), a file inside one
|
| 648 |
-
(`owner/repo/name.safetensors`), a file URL, or a local path — or just drop the files below. A strength of `0`
|
| 649 |
-
switches a slot off without clearing it. Adapters have to be trained against the `transformer_ref/` partition.
|
| 650 |
-
|
| 651 |
-
**Turbo LoRA presets** — from
|
| 652 |
-
[`larryvrh/MiniMax-H3-Turbo-Lora`](https://huggingface.co/larryvrh/MiniMax-H3-Turbo-Lora), a few-step distillation that
|
| 653 |
-
renders joint video + soundtrack in **4–8 steps** instead of the usual ~20 (a ~5× speedup). Pick one from the dropdown
|
| 654 |
-
and **Add to a free slot** to fill a slot, set its strength to `1.0` and move the steps slider to the recommended
|
| 655 |
-
count. Keep strength at `1.0`; only nudge it if a specific clip misbehaves (smear → up, over-sharp → down).
|
| 656 |
-
"""
|
| 657 |
-
|
| 658 |
-
SETTINGS_HELP = """Saves the prompt, the canvas, the sliders and the LoRA slots — everything typed rather than
|
| 659 |
-
uploaded. Images, audio and video are not saved: gradio keeps them in a temporary folder that is gone by the next
|
| 660 |
-
visit, so a saved path would come back as a dead file.
|
| 661 |
"""
|
| 662 |
|
| 663 |
CSS = """
|
|
@@ -700,40 +631,6 @@ with gr.Blocks(title="MiniMax-H3 Reference Custom Lora") as demo:
|
|
| 700 |
with gr.Tab("Video"):
|
| 701 |
video = gr.Video(label="Motion & camera, 2–15 s. Its soundtrack comes along.")
|
| 702 |
run = gr.Button("Generate", variant="primary")
|
| 703 |
-
|
| 704 |
-
with gr.Accordion("LoRA", open=False):
|
| 705 |
-
gr.Markdown(LORA_HELP)
|
| 706 |
-
with gr.Row():
|
| 707 |
-
lora_preset = gr.Dropdown(
|
| 708 |
-
label="Turbo LoRA presets",
|
| 709 |
-
choices=list(LORA_PRESETS),
|
| 710 |
-
value=list(LORA_PRESETS)[0],
|
| 711 |
-
scale=4,
|
| 712 |
-
)
|
| 713 |
-
lora_preset_add = gr.Button("Add to a free slot", size="sm", variant="secondary", scale=1)
|
| 714 |
-
lora_references, lora_scales = [], []
|
| 715 |
-
for slot in range(LORA_SLOTS):
|
| 716 |
-
with gr.Row():
|
| 717 |
-
lora_references.append(
|
| 718 |
-
gr.Textbox(label=f"LoRA {slot + 1}", placeholder="owner/repo", scale=3)
|
| 719 |
-
)
|
| 720 |
-
lora_scales.append(
|
| 721 |
-
gr.Slider(
|
| 722 |
-
label="Strength",
|
| 723 |
-
minimum=LORA_MIN_SCALE,
|
| 724 |
-
maximum=LORA_MAX_SCALE,
|
| 725 |
-
step=0.05,
|
| 726 |
-
value=1.0,
|
| 727 |
-
scale=2,
|
| 728 |
-
)
|
| 729 |
-
)
|
| 730 |
-
lora_upload = gr.File(
|
| 731 |
-
label="Drop .safetensors here to fill the slots",
|
| 732 |
-
file_count="multiple",
|
| 733 |
-
file_types=[".safetensors"],
|
| 734 |
-
type="filepath",
|
| 735 |
-
)
|
| 736 |
-
|
| 737 |
with gr.Accordion("Advanced options", open=False):
|
| 738 |
canvas = gr.Dropdown(label="Canvas", choices=list(CANVASES), value=DEFAULT_CANVAS)
|
| 739 |
match = gr.Checkbox(label="Match the reference soundtrack", value=True, visible=False)
|
|
@@ -790,16 +687,13 @@ with gr.Blocks(title="MiniMax-H3 Reference Custom Lora") as demo:
|
|
| 790 |
)
|
| 791 |
|
| 792 |
# Same order as `SETTINGS_KEYS`.
|
| 793 |
-
settings_fields = [prompt, upsample, canvas, match, duration, steps, seed
|
| 794 |
save.click(save_settings, settings_fields, settings_download, api_name=False)
|
| 795 |
settings_upload.upload(load_settings, settings_upload, settings_fields, api_name=False)
|
| 796 |
-
|
| 797 |
-
# Same order as `generate`'s signature: the five leading columns first, then the remaining image slots, then the
|
| 798 |
-
# LoRA fields the `*lora_fields` tail collects.
|
| 799 |
request = [
|
| 800 |
-
prompt, images[0], audio, video, canvas, *images[1:], match, duration, steps, seed, upsample
|
| 801 |
]
|
| 802 |
-
|
| 803 |
run.click(generate, request, [result, upsampled, upsampled_panel], api_name="generate")
|
| 804 |
|
| 805 |
|
|
|
|
| 421 |
|
| 422 |
|
| 423 |
@spaces.GPU(duration=get_duration, size=GPU_SIZE)
|
| 424 |
+
@spaces.GPU(duration=get_duration, size=GPU_SIZE)
|
| 425 |
def _generate(prompt_embeds, text_token_tags, references, height, width, num_frames, steps, seed, loras=()):
|
| 426 |
"""The only thing on GPU time: the two reference encoders, the packed-sequence denoise loop and the decoders.
|
|
|
|
| 427 |
References cross as paths and are decoded here; only the three generated outputs come back. A `@spaces.GPU`
|
| 428 |
argument crosses a process boundary by pickling, a 5 s 1344x768 reference video is 370 MB of expanded frames, and
|
| 429 |
the full `PipelineState` still holds the packed latents and the rotary grid on the card.
|
|
|
|
| 430 |
The adapters are attached here rather than in the caller: `spaces` runs this body in its own worker, so the
|
| 431 |
transformer the request sees is the one that has to carry them.
|
| 432 |
"""
|
| 433 |
import torch
|
|
|
|
| 434 |
if PLACEMENT == "lazy":
|
| 435 |
PIPE.to("cuda")
|
| 436 |
+
apply_loras(PIPE.transformer_ref, loras or ())
|
|
|
|
|
|
|
| 437 |
state = PIPE(
|
| 438 |
prompt_embeds=prompt_embeds.to("cuda"),
|
| 439 |
text_token_tags=text_token_tags,
|
|
|
|
| 448 |
|
| 449 |
|
| 450 |
def generate(
|
|
|
|
|
|
|
| 451 |
prompt,
|
| 452 |
image_1=None,
|
| 453 |
audio_path=None,
|
|
|
|
| 466 |
steps=28,
|
| 467 |
seed=42,
|
| 468 |
upsample=False,
|
|
|
|
| 469 |
progress=gr.Progress(track_tqdm=True),
|
| 470 |
):
|
| 471 |
+
"""One request."""
|
|
|
|
| 472 |
if LOAD_ERROR:
|
| 473 |
raise gr.Error(LOAD_ERROR)
|
| 474 |
if PIPE is None:
|
|
|
|
| 485 |
derivable = len(audio_bearing(references)) == 1
|
| 486 |
requested = 0 if (match and derivable) else snap_frames(duration)
|
| 487 |
|
|
|
|
|
|
|
| 488 |
progress(0.0, desc="Upsampling the prompt ..." if upsample else "Reading the prompt and references ...")
|
| 489 |
conditioned = time.time()
|
| 490 |
try:
|
|
|
|
| 508 |
progress(0.1, desc=f"Generating {num_frames / FPS:.1f} s at {width}x{height} ...")
|
| 509 |
started = time.time()
|
| 510 |
frames, audio, sampling_rate = _generate(
|
| 511 |
+
prompt_embeds, text_token_tags, references, height, width, num_frames, steps, seed
|
| 512 |
)
|
| 513 |
generate_seconds = time.time() - started
|
| 514 |
|
|
|
|
| 522 |
f"({num_frames / FPS:.3f} s), {int(steps)} steps · conditioner {condition_seconds:.0f}s "
|
| 523 |
f"({plan['num_text_tokens']} tokens{', upsampled' if refined else ''}) · "
|
| 524 |
f"denoise + decode {generate_seconds:.0f}s "
|
| 525 |
+
f"({generate_seconds / int(steps):.1f} s/step) · seed {int(seed)}",
|
|
|
|
| 526 |
flush=True,
|
| 527 |
)
|
| 528 |
return path, refined, gr.update(visible=bool(refined))
|
|
|
|
| 536 |
# is gone by the next visit, so a saved path would restore as a dead file rather than as the image.
|
| 537 |
|
| 538 |
SETTINGS_VERSION = 1
|
| 539 |
+
SETTINGS_KEYS = ["prompt", "upsample", "canvas", "match", "duration", "steps", "seed"]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 540 |
|
| 541 |
|
| 542 |
def save_settings(*values):
|
|
|
|
| 576 |
return updates
|
| 577 |
|
| 578 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 579 |
load_models()
|
| 580 |
|
| 581 |
+
INTRO = """# MiniMax-H3 Reference
|
| 582 |
|
| 583 |
<div align="center">
|
| 584 |
<a href="https://huggingface.co/MiniMaxAI/MiniMax-H3" target="_blank" rel="noopener"><strong>[ model ]</strong></a>
|
|
|
|
| 589 |
**MiniMax-H3** is a 33B parameter state of the art video generation model that produces video and a
|
| 590 |
fully synchronized soundtrack (ambience, foley, speech). Bring your own subject, voice or camera move as a
|
| 591 |
reference.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 592 |
"""
|
| 593 |
|
| 594 |
CSS = """
|
|
|
|
| 631 |
with gr.Tab("Video"):
|
| 632 |
video = gr.Video(label="Motion & camera, 2–15 s. Its soundtrack comes along.")
|
| 633 |
run = gr.Button("Generate", variant="primary")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 634 |
with gr.Accordion("Advanced options", open=False):
|
| 635 |
canvas = gr.Dropdown(label="Canvas", choices=list(CANVASES), value=DEFAULT_CANVAS)
|
| 636 |
match = gr.Checkbox(label="Match the reference soundtrack", value=True, visible=False)
|
|
|
|
| 687 |
)
|
| 688 |
|
| 689 |
# Same order as `SETTINGS_KEYS`.
|
| 690 |
+
settings_fields = [prompt, upsample, canvas, match, duration, steps, seed]
|
| 691 |
save.click(save_settings, settings_fields, settings_download, api_name=False)
|
| 692 |
settings_upload.upload(load_settings, settings_upload, settings_fields, api_name=False)
|
| 693 |
+
# Same order as `generate`'s signature: the five leading columns first, then the remaining image slots.
|
|
|
|
|
|
|
| 694 |
request = [
|
| 695 |
+
prompt, images[0], audio, video, canvas, *images[1:], match, duration, steps, seed, upsample
|
| 696 |
]
|
|
|
|
| 697 |
run.click(generate, request, [result, upsampled, upsampled_panel], api_name="generate")
|
| 698 |
|
| 699 |
|