amisima commited on
Commit
e92d4f3
·
verified ·
1 Parent(s): f938043

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +183 -3
app.py CHANGED
@@ -1456,6 +1456,124 @@ def identify_loras(*references):
1456
  return " \n".join(lines)
1457
 
1458
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1459
  def _fill_lora_slots(files, *current):
1460
  """Drop `.safetensors` files on the uploader and their paths land in the first free slots, so a local adapter
1461
  needs no typing at all."""
@@ -1653,7 +1771,7 @@ CSS = """
1653
  border-radius: 14px !important;
1654
  box-shadow: 0 8px 22px rgba(29, 78, 216, .25);
1655
  }
1656
- #lora-identify, #profile-refresh, #seed-dice, #turbo-btn, #profile-save, #profile-load, #profile-delete {
1657
  min-height: 42px !important; border-radius: 12px !important; font-weight: 600 !important;
1658
  }
1659
 
@@ -1684,7 +1802,7 @@ THEME = gr.themes.Soft(primary_hue="blue", secondary_hue="cyan", neutral_hue="sl
1684
 
1685
  HERO = """
1686
  <div id="h3-hero">
1687
- <h1>MiniMax-H3 &middot; reference &rarr; video + soundtrack &middot; Custom lora + CivitAI links, GPU cost, profiles, clip stitching</h1>
1688
  <p>33B model generating video and a fully synchronized soundtrack (ambience, foley, speech) from your own subject,
1689
  voice or camera move.
1690
  <a href="https://huggingface.co/MiniMaxAI/MiniMax-H3" target="_blank" rel="noopener">model</a> &middot;
@@ -1692,7 +1810,7 @@ HERO = """
1692
  <a href="https://huggingface.co/spaces/multimodalart/minimax-h3" target="_blank" rel="noopener">text / image to video</a></p>
1693
  <div class="pills">
1694
  <span>33B</span><span>joint video + audio</span><span>Turbo lora: 4&ndash;8 steps</span>
1695
- <span>ComfyUI lora accepted</span><span>up to 9 references</span><span>5 custom lora slots</span><span>named profiles</span><span>CivitAI links</span><span>kohya + LoKr auto-convert</span><span>GPU cost estimate</span><span>clip stitching with audio</span>
1696
  </div>
1697
  </div>
1698
  """
@@ -1807,6 +1925,28 @@ with gr.Blocks(title="MiniMax-H3 - Custom lora + CivitAI, GPU cost, profiles, cl
1807
  with gr.Tabs():
1808
  with gr.Tab(f"⭐ Custom lora ({LORA_SLOTS} slots)"):
1809
  gr.Markdown(LORA_HELP)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1810
  lora_references, lora_scales = [], []
1811
  for slot in range(LORA_SLOTS):
1812
  with gr.Row():
@@ -1889,6 +2029,14 @@ with gr.Blocks(title="MiniMax-H3 - Custom lora + CivitAI, GPU cost, profiles, cl
1889
  with gr.Column(scale=6):
1890
  with gr.Group(elem_classes="panel"):
1891
  result = gr.Video(label="🎞️ Video + soundtrack", height=560)
 
 
 
 
 
 
 
 
1892
  # An output, so it can be revealed only for a request that asked for a rewrite.
1893
  with gr.Accordion("Upsampled prompt", open=False, visible=False) as upsampled_panel:
1894
  upsampled = gr.Textbox(show_label=False, lines=8, interactive=False)
@@ -1974,6 +2122,38 @@ with gr.Blocks(title="MiniMax-H3 - Custom lora + CivitAI, GPU cost, profiles, cl
1974
  show_progress="hidden", api_name=False,
1975
  )
1976
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1977
  merge_add_btn.click(
1978
  add_to_queue, [result, merge_queue, merge_name],
1979
  [merged_video, merged_file, merge_queue, merge_status], api_name=False,
 
1456
  return " \n".join(lines)
1457
 
1458
 
1459
+ # ------------------------------------------------------------------------------------------------------------------
1460
+ # Continuing a scene, and searching CivitAI
1461
+ # ------------------------------------------------------------------------------------------------------------------
1462
+
1463
+
1464
+ def last_frame_of(video_path) -> str:
1465
+ """Write the final frame of a clip to a PNG and return its path, so it can be dropped straight into an image
1466
+ slot. A few frames back from the very end on purpose: the last frame is often a duplicate or half-written, and a
1467
+ soft still starts the next clip badly."""
1468
+ if not video_path or not os.path.exists(str(video_path)):
1469
+ raise gr.Error("Generate a video first - there is nothing to continue from.")
1470
+
1471
+ import av
1472
+ from PIL import Image
1473
+
1474
+ frames = []
1475
+ with av.open(str(video_path)) as container:
1476
+ stream = container.streams.video[0]
1477
+ stream.thread_type = "AUTO"
1478
+ for frame in container.decode(stream):
1479
+ frames.append(frame)
1480
+ if len(frames) > 4:
1481
+ frames.pop(0)
1482
+
1483
+ if not frames:
1484
+ raise gr.Error("That video has no readable frames.")
1485
+
1486
+ chosen = frames[max(0, len(frames) - 3)]
1487
+ directory = os.path.join(tempfile.gettempdir(), "continuations")
1488
+ os.makedirs(directory, exist_ok=True)
1489
+ path = os.path.join(directory, f"frame_{int(time.time() * 1000)}.png")
1490
+ Image.fromarray(chosen.to_ndarray(format="rgb24")).save(path)
1491
+ return path
1492
+
1493
+
1494
+ def stage_extension(video_path, queue, name_hint):
1495
+ """Before a continuation runs: park the finished clip in the merge queue and hand its last frame back as the new
1496
+ first reference."""
1497
+ frame = last_frame_of(video_path)
1498
+ merged, merged_file, queue, status = add_to_queue(video_path, queue, name_hint)
1499
+ return frame, None, merged, merged_file, queue, status
1500
+
1501
+
1502
+ H3_BASE_MODELS = ["MiniMax H3", "(any base model)"]
1503
+
1504
+
1505
+ def civitai_search(query, base_model, want_nsfw, limit=20):
1506
+ """Search CivitAI for lora. `/api/v1/models` embeds each model's versions, files and trigger words, so one call
1507
+ gives everything a slot needs. Returns `(readable list, dropdown update, {label: url})`."""
1508
+ import requests
1509
+
1510
+ query = (query or "").strip()
1511
+ if not query:
1512
+ return "Type something to search for.", gr.update(choices=[], value=None), {}
1513
+
1514
+ params = {"query": query, "types": "LORA", "limit": int(limit), "sort": "Most Downloaded"}
1515
+ if base_model and base_model != "(any base model)":
1516
+ params["baseModels"] = base_model
1517
+ if want_nsfw:
1518
+ params["nsfw"] = "true"
1519
+
1520
+ headers = {"User-Agent": "Mozilla/5.0"}
1521
+ token = os.environ.get("CIVITAI_TOKEN", "").strip()
1522
+ if token:
1523
+ headers["Authorization"] = f"Bearer {token}"
1524
+
1525
+ try:
1526
+ response = requests.get("https://civitai.com/api/v1/models", params=params, headers=headers, timeout=30)
1527
+ response.raise_for_status()
1528
+ items = response.json().get("items") or []
1529
+ except Exception as error: # noqa: BLE001
1530
+ return f"Search failed: `{type(error).__name__}: {error}`", gr.update(choices=[], value=None), {}
1531
+
1532
+ if not items:
1533
+ return ("Nothing found. Try fewer words, or set the base model to *(any base model)*.",
1534
+ gr.update(choices=[], value=None), {})
1535
+
1536
+ mapping, lines, choices = {}, [], []
1537
+ for item in items:
1538
+ model_name = item.get("name") or "?"
1539
+ creator = (item.get("creator") or {}).get("username") or "?"
1540
+ downloads = (item.get("stats") or {}).get("downloadCount") or 0
1541
+
1542
+ for version in (item.get("modelVersions") or [])[:3]:
1543
+ version_name = version.get("name") or ""
1544
+ words = [w for w in (version.get("trainedWords") or []) if w]
1545
+ for entry in (version.get("files") or []):
1546
+ name = entry.get("name") or ""
1547
+ if not name.lower().endswith(".safetensors"):
1548
+ continue
1549
+ url = f"https://civitai.com/api/download/models/{version.get('id')}?fileId={entry.get('id')}"
1550
+ size = float(entry.get("sizeKB") or 0) / 1024
1551
+ label = f"{model_name} · {version_name} · {name}"[:150]
1552
+ mapping[label] = url
1553
+ choices.append(label)
1554
+ line = f"**{model_name}** · {version_name} · {name} \n<sub>{size:.0f} MB · {downloads} downloads · by {creator}"
1555
+ if words:
1556
+ line += f" · triggers: {', '.join(words[:5])}"
1557
+ lines.append(line + "</sub>")
1558
+
1559
+ if not choices:
1560
+ return "Found models, but none with a `.safetensors` file.", gr.update(choices=[], value=None), {}
1561
+
1562
+ return " \n".join(lines[:40]), gr.update(choices=choices, value=choices[0]), mapping
1563
+
1564
+
1565
+ def put_in_slot(label, mapping, slot):
1566
+ """Write the chosen result's download link into one lora slot, leaving the others alone."""
1567
+ updates = [gr.update() for _ in range(LORA_SLOTS)]
1568
+ url = (mapping or {}).get(label)
1569
+ if not url:
1570
+ return [*updates, "Search and pick a file first."]
1571
+
1572
+ index = int(str(slot).split()[-1]) - 1
1573
+ updates[index] = gr.update(value=url)
1574
+ return [*updates, f"**Put into {slot}** — {label}"]
1575
+
1576
+
1577
  def _fill_lora_slots(files, *current):
1578
  """Drop `.safetensors` files on the uploader and their paths land in the first free slots, so a local adapter
1579
  needs no typing at all."""
 
1771
  border-radius: 14px !important;
1772
  box-shadow: 0 8px 22px rgba(29, 78, 216, .25);
1773
  }
1774
+ #search-btn, #search-put, #extend-btn, #lora-identify, #profile-refresh, #seed-dice, #turbo-btn, #profile-save, #profile-load, #profile-delete {
1775
  min-height: 42px !important; border-radius: 12px !important; font-weight: 600 !important;
1776
  }
1777
 
 
1802
 
1803
  HERO = """
1804
  <div id="h3-hero">
1805
+ <h1>MiniMax-H3 &middot; reference &rarr; video + soundtrack &middot; Custom lora + CivitAI search, scene continuation, GPU cost, profiles, clip stitching</h1>
1806
  <p>33B model generating video and a fully synchronized soundtrack (ambience, foley, speech) from your own subject,
1807
  voice or camera move.
1808
  <a href="https://huggingface.co/MiniMaxAI/MiniMax-H3" target="_blank" rel="noopener">model</a> &middot;
 
1810
  <a href="https://huggingface.co/spaces/multimodalart/minimax-h3" target="_blank" rel="noopener">text / image to video</a></p>
1811
  <div class="pills">
1812
  <span>33B</span><span>joint video + audio</span><span>Turbo lora: 4&ndash;8 steps</span>
1813
+ <span>ComfyUI lora accepted</span><span>up to 9 references</span><span>5 custom lora slots</span><span>named profiles</span><span>CivitAI search + links</span><span>scene continuation</span><span>kohya + LoKr auto-convert</span><span>GPU cost estimate</span><span>clip stitching with audio</span>
1814
  </div>
1815
  </div>
1816
  """
 
1925
  with gr.Tabs():
1926
  with gr.Tab(f"⭐ Custom lora ({LORA_SLOTS} slots)"):
1927
  gr.Markdown(LORA_HELP)
1928
+ with gr.Accordion("🔍 Search CivitAI", open=False):
1929
+ gr.Markdown(
1930
+ "Search CivitAI without leaving the Space, then drop a result straight into a slot. "
1931
+ "Each hit shows its size, downloads and trigger words."
1932
+ )
1933
+ with gr.Row():
1934
+ search_query = gr.Textbox(label="Search", placeholder="e.g. dance, rain, camera move",
1935
+ scale=3)
1936
+ search_base = gr.Dropdown(H3_BASE_MODELS, value=H3_BASE_MODELS[0], label="Base model",
1937
+ allow_custom_value=True, scale=2)
1938
+ with gr.Row():
1939
+ search_nsfw = gr.Checkbox(value=True, label="Include NSFW results")
1940
+ search_btn = gr.Button("🔍 Search", variant="secondary", elem_id="search-btn")
1941
+ search_results = gr.Markdown("No search yet.")
1942
+ search_pick = gr.Dropdown(choices=[], label="Pick a file")
1943
+ with gr.Row():
1944
+ search_slot = gr.Dropdown([f"lora {i}" for i in range(1, LORA_SLOTS + 1)],
1945
+ value="lora 1", label="Into slot", scale=2)
1946
+ search_put_btn = gr.Button("⬇️ Put it in", variant="primary", scale=1,
1947
+ elem_id="search-put")
1948
+ search_state = gr.State({})
1949
+
1950
  lora_references, lora_scales = [], []
1951
  for slot in range(LORA_SLOTS):
1952
  with gr.Row():
 
2029
  with gr.Column(scale=6):
2030
  with gr.Group(elem_classes="panel"):
2031
  result = gr.Video(label="🎞️ Video + soundtrack", height=560)
2032
+ extend_btn = gr.Button("➕ Continue the scene (one more clip, joined on)",
2033
+ variant="secondary", elem_id="extend-btn")
2034
+ gr.Markdown(
2035
+ "Takes the last frame of the clip above, makes it the first reference, generates again with "
2036
+ "the settings untouched, and joins the two into one file — soundtrack included. Press it again "
2037
+ "for a third clip. Each press costs one normal generation; the joining is free.",
2038
+ elem_classes="turbo-blurb",
2039
+ )
2040
  # An output, so it can be revealed only for a request that asked for a rewrite.
2041
  with gr.Accordion("Upsampled prompt", open=False, visible=False) as upsampled_panel:
2042
  upsampled = gr.Textbox(show_label=False, lines=8, interactive=False)
 
2122
  show_progress="hidden", api_name=False,
2123
  )
2124
 
2125
+ # Continuing a scene: park the finished clip, hand its last frame to the first image slot, generate from it,
2126
+ # then join the new clip on. `request` starts with the prompt and the first image, so the second step reads
2127
+ # the still the first step just wrote.
2128
+ extend_btn.click(
2129
+ fn=stage_extension,
2130
+ inputs=[result, merge_queue, merge_name],
2131
+ outputs=[images[0], result, merged_video, merged_file, merge_queue, merge_status],
2132
+ api_name=False,
2133
+ ).then(
2134
+ roll_seed, [randomize_seed, seed], seed, show_progress="hidden", api_name=False,
2135
+ ).then(
2136
+ generate, request, [result, upsampled, upsampled_panel], api_name=False,
2137
+ ).then(
2138
+ auto_queue, [result, auto_merge, merge_queue, merge_name],
2139
+ [merged_video, merged_file, merge_queue, merge_status],
2140
+ show_progress="hidden", api_name=False,
2141
+ )
2142
+
2143
+ # Searching CivitAI, and dropping a result into a slot.
2144
+ search_btn.click(
2145
+ civitai_search, [search_query, search_base, search_nsfw],
2146
+ [search_results, search_pick, search_state], api_name=False,
2147
+ )
2148
+ search_query.submit(
2149
+ civitai_search, [search_query, search_base, search_nsfw],
2150
+ [search_results, search_pick, search_state], api_name=False,
2151
+ )
2152
+ search_put_btn.click(
2153
+ put_in_slot, [search_pick, search_state, search_slot],
2154
+ [*lora_references, search_results], api_name=False,
2155
+ )
2156
+
2157
  merge_add_btn.click(
2158
  add_to_queue, [result, merge_queue, merge_name],
2159
  [merged_video, merged_file, merge_queue, merge_status], api_name=False,