Spaces:
Running on Zero
Running on Zero
Upload 2 files
Browse files- app.py +119 -15
- lora_library.py +15 -10
app.py
CHANGED
|
@@ -1616,7 +1616,8 @@ def delete_profile(name):
|
|
| 1616 |
# ----------------------------------------------------------------------------------------------------------------
|
| 1617 |
|
| 1618 |
|
| 1619 |
-
def gpu_estimate(canvas, duration, steps, match, audio_path, video_path, identity_ref,
|
|
|
|
| 1620 |
"""What this request will reserve, by the same `budget()` the pre-flight check and `get_duration` use.
|
| 1621 |
The identity face counts too: it is sent as one extra picture, so it adds its rows."""
|
| 1622 |
images = list(rest[:MAX_IMAGE_SLOTS])
|
|
@@ -1661,7 +1662,9 @@ def gpu_estimate(canvas, duration, steps, match, audio_path, video_path, identit
|
|
| 1661 |
advice = f"Lower Steps to {room}." if room >= MIN_STEPS else "Lower the duration or pick a smaller canvas."
|
| 1662 |
head = f"🚫 **Wants ~{int(total)} s of GPU, ceiling is {MAX_GPU_DURATION} s.** {advice}"
|
| 1663 |
else:
|
| 1664 |
-
|
|
|
|
|
|
|
| 1665 |
|
| 1666 |
detail = (
|
| 1667 |
f"{width}x{height} · {seconds:.1f} s ({num_frames} frames) · {int(steps)} steps · "
|
|
@@ -1670,6 +1673,7 @@ def gpu_estimate(canvas, duration, steps, match, audio_path, video_path, identit
|
|
| 1670 |
)
|
| 1671 |
if derivable and match:
|
| 1672 |
detail += " · duration comes from the reference soundtrack"
|
|
|
|
| 1673 |
return f"{head} \n<sub>{detail}</sub>"
|
| 1674 |
|
| 1675 |
|
|
@@ -1821,6 +1825,63 @@ CIVITAI_API_HOSTS = [h for h in (os.environ.get("CIVITAI_API_HOST", "").strip(),
|
|
| 1821 |
H3_BASE_MODELS = ["MiniMax H3", "(any base model)"]
|
| 1822 |
|
| 1823 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1824 |
def civitai_search(query, base_model, want_nsfw, limit=20):
|
| 1825 |
"""Search CivitAI for lora. `/api/v1/models` embeds each model's versions, files and trigger words, so one call
|
| 1826 |
gives everything a slot needs. Returns `(readable list, dropdown update, {label: url})`."""
|
|
@@ -2524,7 +2585,8 @@ with gr.Blocks(title="MiniMax-H3 - Custom lora + CivitAI, structured prompts, GP
|
|
| 2524 |
elem_id="search-put")
|
| 2525 |
# Below the picker on purpose: a wall of results above it would push the
|
| 2526 |
# controls off the screen, which is exactly what happened the first time.
|
| 2527 |
-
|
|
|
|
| 2528 |
search_state = gr.State({})
|
| 2529 |
|
| 2530 |
lora_references, lora_scales = [], []
|
|
@@ -2548,7 +2610,8 @@ with gr.Blocks(title="MiniMax-H3 - Custom lora + CivitAI, structured prompts, GP
|
|
| 2548 |
with gr.Row():
|
| 2549 |
lora_identify_btn = gr.Button("🔎 name the links", variant="secondary",
|
| 2550 |
elem_id="lora-identify")
|
| 2551 |
-
|
|
|
|
| 2552 |
lora_upload = gr.File(
|
| 2553 |
label="Drop .safetensors here to fill the slots",
|
| 2554 |
file_count="multiple",
|
|
@@ -2613,19 +2676,40 @@ with gr.Blocks(title="MiniMax-H3 - Custom lora + CivitAI, structured prompts, GP
|
|
| 2613 |
elem_id="estimate-btn")
|
| 2614 |
with gr.Group(elem_classes="panel"):
|
| 2615 |
result = gr.Video(label="🎞️ Video + soundtrack", height=560)
|
| 2616 |
-
|
| 2617 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2618 |
gr.Markdown(
|
| 2619 |
-
"
|
| 2620 |
-
"
|
| 2621 |
-
"
|
| 2622 |
-
"**
|
|
|
|
|
|
|
|
|
|
| 2623 |
elem_classes="turbo-blurb",
|
| 2624 |
)
|
| 2625 |
# An output, so it can be revealed only for a request that asked for a rewrite.
|
| 2626 |
with gr.Accordion("Upsampled prompt", open=False, visible=False) as upsampled_panel:
|
| 2627 |
upsampled = gr.Textbox(show_label=False, lines=8, interactive=False)
|
| 2628 |
-
with gr.Accordion("🎞️
|
| 2629 |
gr.Markdown(
|
| 2630 |
"Add clips to a queue and they are joined into a single file, **soundtrack "
|
| 2631 |
"included**. Three clips make one long video for no extra GPU time. Everything "
|
|
@@ -2635,8 +2719,8 @@ with gr.Blocks(title="MiniMax-H3 - Custom lora + CivitAI, structured prompts, GP
|
|
| 2635 |
merge_name = gr.Textbox(label="File name (optional)", placeholder="my_scene",
|
| 2636 |
max_lines=1)
|
| 2637 |
auto_merge = gr.Checkbox(
|
| 2638 |
-
value=
|
| 2639 |
-
info="
|
| 2640 |
)
|
| 2641 |
with gr.Row():
|
| 2642 |
merge_add_btn = gr.Button("➕ Add the current video", variant="secondary")
|
|
@@ -2722,11 +2806,30 @@ with gr.Blocks(title="MiniMax-H3 - Custom lora + CivitAI, structured prompts, GP
|
|
| 2722 |
).then(
|
| 2723 |
generate_with_identity, request, [result, upsampled, upsampled_panel], api_name=False,
|
| 2724 |
).then(
|
| 2725 |
-
|
| 2726 |
[merged_video, merged_file, merge_queue, merge_status],
|
| 2727 |
show_progress="hidden", api_name=False,
|
| 2728 |
)
|
| 2729 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2730 |
# Searching CivitAI, and dropping a result into a slot.
|
| 2731 |
search_btn.click(
|
| 2732 |
civitai_search, [search_query, search_base, search_nsfw],
|
|
@@ -2804,7 +2907,8 @@ with gr.Blocks(title="MiniMax-H3 - Custom lora + CivitAI, structured prompts, GP
|
|
| 2804 |
for _field in lora_references:
|
| 2805 |
_field.submit(identify_loras, lora_references, lora_names, api_name=False)
|
| 2806 |
|
| 2807 |
-
estimate_inputs = [canvas, duration, steps, match, audio, video, identity_ref,
|
|
|
|
| 2808 |
# Recalculated on demand only. An automatic readout means an event on every slider
|
| 2809 |
# move, and a browser that never gets to rest.
|
| 2810 |
estimate_btn.click(gpu_estimate, estimate_inputs, estimate, show_progress="hidden")
|
|
|
|
| 1616 |
# ----------------------------------------------------------------------------------------------------------------
|
| 1617 |
|
| 1618 |
|
| 1619 |
+
def gpu_estimate(canvas, duration, steps, match, audio_path, video_path, identity_ref,
|
| 1620 |
+
clip_count=1, *rest):
|
| 1621 |
"""What this request will reserve, by the same `budget()` the pre-flight check and `get_duration` use.
|
| 1622 |
The identity face counts too: it is sent as one extra picture, so it adds its rows."""
|
| 1623 |
images = list(rest[:MAX_IMAGE_SLOTS])
|
|
|
|
| 1662 |
advice = f"Lower Steps to {room}." if room >= MIN_STEPS else "Lower the duration or pick a smaller canvas."
|
| 1663 |
head = f"🚫 **Wants ~{int(total)} s of GPU, ceiling is {MAX_GPU_DURATION} s.** {advice}"
|
| 1664 |
else:
|
| 1665 |
+
clips = max(1, int(clip_count or 1))
|
| 1666 |
+
head = (f"⏳ **GPU cost: ~{reserved} s** for one clip \n"
|
| 1667 |
+
f"🎬 whole scene, {clips} clips: **~{reserved * clips} s**")
|
| 1668 |
|
| 1669 |
detail = (
|
| 1670 |
f"{width}x{height} · {seconds:.1f} s ({num_frames} frames) · {int(steps)} steps · "
|
|
|
|
| 1673 |
)
|
| 1674 |
if derivable and match:
|
| 1675 |
detail += " · duration comes from the reference soundtrack"
|
| 1676 |
+
detail += " · joining the clips runs on the processor and costs nothing"
|
| 1677 |
return f"{head} \n<sub>{detail}</sub>"
|
| 1678 |
|
| 1679 |
|
|
|
|
| 1825 |
H3_BASE_MODELS = ["MiniMax H3", "(any base model)"]
|
| 1826 |
|
| 1827 |
|
| 1828 |
+
_SCENE_INDEX = {}
|
| 1829 |
+
|
| 1830 |
+
|
| 1831 |
+
def _scene_prompt_lines(text, total, fallback):
|
| 1832 |
+
"""One prompt per clip. A line the user left empty - or a line that is not there at all -
|
| 1833 |
+
means that clip runs on the main prompt, so the box can be left alone entirely."""
|
| 1834 |
+
lines = str(text or "").splitlines()
|
| 1835 |
+
out = []
|
| 1836 |
+
for index in range(total):
|
| 1837 |
+
line = lines[index].strip() if index < len(lines) else ""
|
| 1838 |
+
out.append(line or fallback)
|
| 1839 |
+
return out
|
| 1840 |
+
|
| 1841 |
+
|
| 1842 |
+
def make_scene(clip_count, scene_prompts, randomize, queue, name_hint, *request_values):
|
| 1843 |
+
"""One button, a whole scene. Generates `clip_count` clips back to back - each one starting on
|
| 1844 |
+
the last frame of the clip before it - and stitches them into a single video as it goes, with
|
| 1845 |
+
the soundtrack carried through. The identity face stays locked across the whole run, so the
|
| 1846 |
+
person does not drift from clip to clip."""
|
| 1847 |
+
values = list(request_values)
|
| 1848 |
+
idx_prompt = _SCENE_INDEX.get("prompt", 0)
|
| 1849 |
+
idx_image = _SCENE_INDEX.get("image", 1)
|
| 1850 |
+
idx_seed = _SCENE_INDEX.get("seed", 16)
|
| 1851 |
+
idx_identity = _SCENE_INDEX.get("identity", len(values) - 1)
|
| 1852 |
+
|
| 1853 |
+
if not values[idx_image]:
|
| 1854 |
+
raise gr.Error("Add a first reference image before making a scene.")
|
| 1855 |
+
|
| 1856 |
+
queue = list(queue or [])
|
| 1857 |
+
total = max(1, min(8, int(clip_count or 1)))
|
| 1858 |
+
prompts = _scene_prompt_lines(scene_prompts, total, values[idx_prompt])
|
| 1859 |
+
video_path = None
|
| 1860 |
+
refined = ""
|
| 1861 |
+
panel = gr.update()
|
| 1862 |
+
|
| 1863 |
+
# The face that is in view now is what every later clip is pulled back toward.
|
| 1864 |
+
if not values[idx_identity]:
|
| 1865 |
+
values[idx_identity] = values[idx_image]
|
| 1866 |
+
|
| 1867 |
+
for index in range(total):
|
| 1868 |
+
values[idx_prompt] = prompts[index]
|
| 1869 |
+
if index:
|
| 1870 |
+
values[idx_image] = last_frame_of(video_path)
|
| 1871 |
+
values[idx_seed] = roll_seed(randomize, values[idx_seed])
|
| 1872 |
+
|
| 1873 |
+
video_path, refined, panel = generate_with_identity(*values)
|
| 1874 |
+
if not video_path:
|
| 1875 |
+
raise gr.Error(f"Clip {index + 1} produced nothing.")
|
| 1876 |
+
|
| 1877 |
+
if not queue or queue[-1] != video_path:
|
| 1878 |
+
queue.append(video_path)
|
| 1879 |
+
merged = concat_videos(queue, name_hint) if len(queue) > 1 else queue[0]
|
| 1880 |
+
note = (_queue_status(len(queue), merged) if index + 1 == total
|
| 1881 |
+
else f"Clip {index + 1} of {total} done - {len(queue)} in the joined file.")
|
| 1882 |
+
yield (video_path, refined, panel, merged, merged, queue, note, values[idx_seed])
|
| 1883 |
+
|
| 1884 |
+
|
| 1885 |
def civitai_search(query, base_model, want_nsfw, limit=20):
|
| 1886 |
"""Search CivitAI for lora. `/api/v1/models` embeds each model's versions, files and trigger words, so one call
|
| 1887 |
gives everything a slot needs. Returns `(readable list, dropdown update, {label: url})`."""
|
|
|
|
| 2585 |
elem_id="search-put")
|
| 2586 |
# Below the picker on purpose: a wall of results above it would push the
|
| 2587 |
# controls off the screen, which is exactly what happened the first time.
|
| 2588 |
+
with gr.Accordion("📋 The results", open=False):
|
| 2589 |
+
search_results = gr.Markdown("No search yet.")
|
| 2590 |
search_state = gr.State({})
|
| 2591 |
|
| 2592 |
lora_references, lora_scales = [], []
|
|
|
|
| 2610 |
with gr.Row():
|
| 2611 |
lora_identify_btn = gr.Button("🔎 name the links", variant="secondary",
|
| 2612 |
elem_id="lora-identify")
|
| 2613 |
+
with gr.Accordion("📋 The named links", open=False):
|
| 2614 |
+
lora_names = gr.Markdown("Nothing in the slots yet.", elem_classes="turbo-blurb")
|
| 2615 |
lora_upload = gr.File(
|
| 2616 |
label="Drop .safetensors here to fill the slots",
|
| 2617 |
file_count="multiple",
|
|
|
|
| 2676 |
elem_id="estimate-btn")
|
| 2677 |
with gr.Group(elem_classes="panel"):
|
| 2678 |
result = gr.Video(label="🎞️ Video + soundtrack", height=560)
|
| 2679 |
+
gr.Markdown("### 🎬 Make it longer")
|
| 2680 |
+
chain_count = gr.Slider(
|
| 2681 |
+
2, 8, value=3, step=1, label="How many clips in a row",
|
| 2682 |
+
info="3 clips of 5 s ≈ a 15 second video.",
|
| 2683 |
+
)
|
| 2684 |
+
with gr.Accordion("✍️ A prompt per clip (optional)", open=False):
|
| 2685 |
+
scene_prompts = gr.Textbox(
|
| 2686 |
+
label="One line per clip", lines=8, max_lines=8,
|
| 2687 |
+
placeholder=("line 1 = clip 1, line 2 = clip 2, and so on\n"
|
| 2688 |
+
"she turns toward the window\n"
|
| 2689 |
+
"she smiles and looks down\n"
|
| 2690 |
+
"…"),
|
| 2691 |
+
info="Leave a line empty — or the whole box — and that clip uses the "
|
| 2692 |
+
"main prompt, unchanged.",
|
| 2693 |
+
)
|
| 2694 |
+
with gr.Row():
|
| 2695 |
+
chain_btn = gr.Button("🎬 Make the whole scene and join it",
|
| 2696 |
+
variant="primary", elem_id="extend-btn")
|
| 2697 |
+
chain_stop_btn = gr.Button("⏹ Stop", variant="stop")
|
| 2698 |
+
extend_btn = gr.Button("➕ Just one more clip", variant="secondary")
|
| 2699 |
gr.Markdown(
|
| 2700 |
+
"Press 🎬 once and leave it. Each clip starts on the last frame of the one "
|
| 2701 |
+
"before it and the joined video below grows as each clip lands — soundtrack "
|
| 2702 |
+
"included. Every clip costs one normal generation; the joining is free, and "
|
| 2703 |
+
"**the 🔒 identity face stays locked**, so the person never drifts between "
|
| 2704 |
+
"clips. \n"
|
| 2705 |
+
"➕ does the same thing one clip at a time, for when you want to change "
|
| 2706 |
+
"something in between.",
|
| 2707 |
elem_classes="turbo-blurb",
|
| 2708 |
)
|
| 2709 |
# An output, so it can be revealed only for a request that asked for a rewrite.
|
| 2710 |
with gr.Accordion("Upsampled prompt", open=False, visible=False) as upsampled_panel:
|
| 2711 |
upsampled = gr.Textbox(show_label=False, lines=8, interactive=False)
|
| 2712 |
+
with gr.Accordion("🎞️ The joined video (no GPU used)", open=True):
|
| 2713 |
gr.Markdown(
|
| 2714 |
"Add clips to a queue and they are joined into a single file, **soundtrack "
|
| 2715 |
"included**. Three clips make one long video for no extra GPU time. Everything "
|
|
|
|
| 2719 |
merge_name = gr.Textbox(label="File name (optional)", placeholder="my_scene",
|
| 2720 |
max_lines=1)
|
| 2721 |
auto_merge = gr.Checkbox(
|
| 2722 |
+
value=True, label="⚡ Add every new clip automatically",
|
| 2723 |
+
info="Leave this on. 🎬 and ➕ always join their clips whatever this says.",
|
| 2724 |
)
|
| 2725 |
with gr.Row():
|
| 2726 |
merge_add_btn = gr.Button("➕ Add the current video", variant="secondary")
|
|
|
|
| 2806 |
).then(
|
| 2807 |
generate_with_identity, request, [result, upsampled, upsampled_panel], api_name=False,
|
| 2808 |
).then(
|
| 2809 |
+
add_to_queue, [result, merge_queue, merge_name],
|
| 2810 |
[merged_video, merged_file, merge_queue, merge_status],
|
| 2811 |
show_progress="hidden", api_name=False,
|
| 2812 |
)
|
| 2813 |
|
| 2814 |
+
# One button, a whole scene. `make_scene` gets `request` as a flat tuple, so it is told here -
|
| 2815 |
+
# from the list itself - which slots hold the prompt, the first image, the seed and the
|
| 2816 |
+
# identity face, rather than counting positions by hand.
|
| 2817 |
+
_SCENE_INDEX.update(
|
| 2818 |
+
prompt=request.index(prompt),
|
| 2819 |
+
image=request.index(images[0]),
|
| 2820 |
+
seed=request.index(seed),
|
| 2821 |
+
identity=request.index(identity_ref),
|
| 2822 |
+
)
|
| 2823 |
+
_chain_event = chain_btn.click(
|
| 2824 |
+
fn=make_scene,
|
| 2825 |
+
inputs=[chain_count, scene_prompts, randomize_seed, merge_queue, merge_name, *request],
|
| 2826 |
+
outputs=[result, upsampled, upsampled_panel, merged_video, merged_file,
|
| 2827 |
+
merge_queue, merge_status, seed],
|
| 2828 |
+
api_name=False,
|
| 2829 |
+
)
|
| 2830 |
+
chain_stop_btn.click(fn=None, inputs=None, outputs=None, cancels=[_chain_event],
|
| 2831 |
+
api_name=False)
|
| 2832 |
+
|
| 2833 |
# Searching CivitAI, and dropping a result into a slot.
|
| 2834 |
search_btn.click(
|
| 2835 |
civitai_search, [search_query, search_base, search_nsfw],
|
|
|
|
| 2907 |
for _field in lora_references:
|
| 2908 |
_field.submit(identify_loras, lora_references, lora_names, api_name=False)
|
| 2909 |
|
| 2910 |
+
estimate_inputs = [canvas, duration, steps, match, audio, video, identity_ref,
|
| 2911 |
+
chain_count, *images, *lora_references]
|
| 2912 |
# Recalculated on demand only. An automatic readout means an event on every slider
|
| 2913 |
# move, and a browser that never gets to rest.
|
| 2914 |
estimate_btn.click(gpu_estimate, estimate_inputs, estimate, show_progress="hidden")
|
lora_library.py
CHANGED
|
@@ -723,16 +723,21 @@ def library_tab(lora_box=None, prompt_box=None, lora_slots=None, scale_slots=Non
|
|
| 723 |
normal_labels = gr.State(_labels(library["normal"]))
|
| 724 |
nsfw_labels = gr.State(_labels(library["nsfw"]))
|
| 725 |
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
)
|
| 735 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 736 |
|
| 737 |
with gr.Row():
|
| 738 |
add_button = gr.Button(
|
|
|
|
| 723 |
normal_labels = gr.State(_labels(library["normal"]))
|
| 724 |
nsfw_labels = gr.State(_labels(library["nsfw"]))
|
| 725 |
|
| 726 |
+
with gr.Accordion("📋 ordinary", open=False):
|
| 727 |
+
normal_pick = gr.CheckboxGroup(
|
| 728 |
+
choices=_labels(library["normal"]), value=[],
|
| 729 |
+
label="ordinary", show_label=False, interactive=True,
|
| 730 |
+
)
|
| 731 |
+
with gr.Accordion("🔗 the links", open=False):
|
| 732 |
+
normal_links = gr.Markdown(_links_markdown(library["normal"], "ordinary"))
|
| 733 |
+
|
| 734 |
+
with gr.Accordion("🔞 nsfw", open=False):
|
| 735 |
+
nsfw_pick = gr.CheckboxGroup(
|
| 736 |
+
choices=_labels(library["nsfw"]), value=[],
|
| 737 |
+
label="nsfw", show_label=False, interactive=True,
|
| 738 |
+
)
|
| 739 |
+
with gr.Accordion("🔗 the links", open=False):
|
| 740 |
+
nsfw_links = gr.Markdown(_links_markdown(library["nsfw"], "nsfw"))
|
| 741 |
|
| 742 |
with gr.Row():
|
| 743 |
add_button = gr.Button(
|