amisima commited on
Commit
b5c5a4a
·
verified ·
1 Parent(s): f024248

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +119 -15
  2. 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, *rest):
 
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
- head = f"⏳ **GPU cost: ~{reserved} s**"
 
 
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
- search_results = gr.Markdown("No search yet.")
 
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
- lora_names = gr.Markdown("Nothing in the slots yet.", elem_classes="turbo-blurb")
 
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
- extend_btn = gr.Button(" Continue the scene (one more clip, joined on)",
2617
- variant="secondary", elem_id="extend-btn")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2618
  gr.Markdown(
2619
- "Takes the last frame of the clip above, makes it the first reference, generates again with "
2620
- "the settings untouched, and joins the two into one file soundtrack included. Press it again "
2621
- "for a third clip. Each press costs one normal generation; the joining is free. "
2622
- "**The 🔒 identity face stays locked**, so the person never drifts between clips.",
 
 
 
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("🎞️ Stitch clips into one video (no GPU used)", open=False):
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=False, label="⚡ Add every new generation automatically",
2639
- info="Generate, generate, generate the joined file grows on its own.",
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
- auto_queue, [result, auto_merge, merge_queue, merge_name],
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, *images, *lora_references]
 
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
- normal_pick = gr.CheckboxGroup(
727
- choices=_labels(library["normal"]), value=[],
728
- label="ordinary", interactive=True,
729
- )
730
- normal_links = gr.Markdown(_links_markdown(library["normal"], "ordinary"))
731
- nsfw_pick = gr.CheckboxGroup(
732
- choices=_labels(library["nsfw"]), value=[],
733
- label="nsfw", interactive=True,
734
- )
735
- nsfw_links = gr.Markdown(_links_markdown(library["nsfw"], "nsfw"))
 
 
 
 
 
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(