update app
Browse files
app.py
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
import math
|
|
@@ -7,16 +14,10 @@ import subprocess
|
|
| 7 |
import tempfile
|
| 8 |
import time
|
| 9 |
import traceback
|
| 10 |
-
import base64
|
| 11 |
-
import json
|
| 12 |
from functools import cache
|
| 13 |
-
from io import BytesIO
|
| 14 |
-
from PIL import Image
|
| 15 |
|
| 16 |
import spaces
|
| 17 |
import gradio as gr
|
| 18 |
-
from gradio import Server
|
| 19 |
-
from fastapi.responses import HTMLResponse
|
| 20 |
|
| 21 |
MODEL_REPO = os.environ.get("H3_MODEL_REPO", "MiniMaxAI/MiniMax-H3")
|
| 22 |
CONDITIONER_SPACE = os.environ.get("H3_CONDITIONER", "multimodalart/qwen3vl-conditioner")
|
|
@@ -52,8 +53,9 @@ PIPELINE_MAX_DURATION = MAX_UI_DURATION + 1.0
|
|
| 52 |
CHUNK_SECONDS = float(os.environ.get("H3_CHUNK_SECONDS", "14"))
|
| 53 |
CONDITIONER_MAX_FRAMES = int(os.environ.get("H3_CONDITIONER_MAX_FRAMES", "345"))
|
| 54 |
|
|
|
|
| 55 |
GPU_DURATION_CHOICES = (60, 90, 120, 150, 180, 250)
|
| 56 |
-
DEFAULT_GPU_DURATION =
|
| 57 |
|
| 58 |
OUTPUT_DIR = os.path.join(tempfile.gettempdir(), "h3-outputs")
|
| 59 |
|
|
@@ -75,6 +77,7 @@ def snap_frames(seconds: float) -> int:
|
|
| 75 |
|
| 76 |
def patch_duration_window(minimum: float, maximum: float) -> None:
|
| 77 |
from diffusers.modular_pipelines.minimax_h3.modular_pipeline import MiniMaxH3ModularPipeline
|
|
|
|
| 78 |
MiniMaxH3ModularPipeline.min_duration = property(lambda self: float(minimum))
|
| 79 |
MiniMaxH3ModularPipeline.max_duration = property(lambda self: float(maximum))
|
| 80 |
|
|
@@ -82,17 +85,19 @@ def patch_duration_window(minimum: float, maximum: float) -> None:
|
|
| 82 |
def _apply_lora(transformer) -> str | None:
|
| 83 |
try:
|
| 84 |
import h3_lora
|
|
|
|
| 85 |
return h3_lora.apply_lora(transformer)
|
| 86 |
except ImportError:
|
| 87 |
-
return "⚠️ `h3_lora.py` is missing — running without the Turbo LoRA."
|
| 88 |
except Exception as error:
|
| 89 |
traceback.print_exc()
|
| 90 |
-
return f"⚠️ LoRA loading failed ({type(error).__name__}: {error})"
|
| 91 |
|
| 92 |
|
| 93 |
def _set_lora(transformer, name: str) -> str:
|
| 94 |
try:
|
| 95 |
import h3_lora
|
|
|
|
| 96 |
return h3_lora.set_active(transformer, name)
|
| 97 |
except Exception:
|
| 98 |
return "off"
|
|
@@ -100,6 +105,7 @@ def _set_lora(transformer, name: str) -> str:
|
|
| 100 |
|
| 101 |
def load_models() -> str | None:
|
| 102 |
global PIPE, MUTE_PIPE, MANAGER, LOAD_ERROR, MUTE_ERROR, LOADED_IN, LORA_STATUS
|
|
|
|
| 103 |
if PIPE is not None or LOAD_ERROR is not None:
|
| 104 |
return LOAD_ERROR
|
| 105 |
|
|
@@ -107,6 +113,7 @@ def load_models() -> str | None:
|
|
| 107 |
try:
|
| 108 |
import torch
|
| 109 |
from diffusers import ComponentsManager
|
|
|
|
| 110 |
from h3_split_blocks import MiniMaxH3GeneratorBlocks
|
| 111 |
|
| 112 |
patch_duration_window(MIN_UI_DURATION, PIPELINE_MAX_DURATION)
|
|
@@ -144,6 +151,7 @@ def load_models() -> str | None:
|
|
| 144 |
def _build_mute_pipe(pipe):
|
| 145 |
try:
|
| 146 |
from h3_split_blocks import MiniMaxH3MuteGeneratorBlocks
|
|
|
|
| 147 |
blocks = MiniMaxH3MuteGeneratorBlocks()
|
| 148 |
mute = blocks.init_pipeline(MODEL_REPO)
|
| 149 |
shared = {}
|
|
@@ -178,6 +186,7 @@ def _arm_decode_hooks(pipe):
|
|
| 178 |
@cache
|
| 179 |
def conditioner():
|
| 180 |
from gradio_client import Client
|
|
|
|
| 181 |
return Client(CONDITIONER_SPACE)
|
| 182 |
|
| 183 |
|
|
@@ -185,6 +194,7 @@ def conditioner_client(ip_token):
|
|
| 185 |
if not ip_token:
|
| 186 |
return conditioner()
|
| 187 |
from gradio_client import Client
|
|
|
|
| 188 |
return Client(CONDITIONER_SPACE, headers={"x-ip-token": ip_token})
|
| 189 |
|
| 190 |
|
|
@@ -228,8 +238,13 @@ def estimate_seconds(height, width, num_frames, steps, num_keyframes=0, with_aud
|
|
| 228 |
|
| 229 |
def get_duration(
|
| 230 |
prompt_embeds, text_token_tags, image, last_image, height, width, num_frames, steps, seed,
|
| 231 |
-
lora="
|
| 232 |
):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 233 |
if gpu_duration:
|
| 234 |
try:
|
| 235 |
value = int(gpu_duration)
|
|
@@ -244,10 +259,12 @@ def get_duration(
|
|
| 244 |
@spaces.GPU(duration=get_duration, size=GPU_SIZE)
|
| 245 |
def _generate(
|
| 246 |
prompt_embeds, text_token_tags, image, last_image, height, width, num_frames, steps, seed,
|
| 247 |
-
lora="
|
| 248 |
):
|
| 249 |
import torch
|
|
|
|
| 250 |
active_lora = _set_lora(PIPE.transformer, lora)
|
|
|
|
| 251 |
pipe = PIPE if (with_audio or MUTE_PIPE is None) else MUTE_PIPE
|
| 252 |
decoded_audio = with_audio or MUTE_PIPE is None
|
| 253 |
|
|
@@ -277,6 +294,7 @@ def _generate(
|
|
| 277 |
|
| 278 |
def _fit_keyframe(image_path, current_canvas):
|
| 279 |
from PIL import Image as _Image
|
|
|
|
| 280 |
img = _Image.open(image_path)
|
| 281 |
aspect = img.width / img.height
|
| 282 |
fastest = {}
|
|
@@ -311,7 +329,7 @@ LORA_NAMES = ("larry", "lightx", "lightx8", "realism", "joyfox", "H3-Facial-Real
|
|
| 311 |
|
| 312 |
def _resolve_lora(lora) -> str:
|
| 313 |
if not isinstance(lora, str) or not lora.strip():
|
| 314 |
-
return "
|
| 315 |
value = lora.strip().lower()
|
| 316 |
lowered = {name.lower(): name for name in LORA_NAMES}
|
| 317 |
if value in lowered:
|
|
@@ -322,8 +340,15 @@ def _resolve_lora(lora) -> str:
|
|
| 322 |
raise gr.Error(f"Unknown LoRA `{lora}`. Allowed: {', '.join(LORA_NAMES)}.")
|
| 323 |
|
| 324 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 325 |
def keyframe(path):
|
| 326 |
from PIL import Image, ImageOps
|
|
|
|
| 327 |
return ImageOps.exif_transpose(Image.open(path)).convert("RGB") if path else None
|
| 328 |
|
| 329 |
|
|
@@ -332,6 +357,7 @@ def _render_clip(prompt, first, last, canvas, duration, steps, seed, upsample, l
|
|
| 332 |
from diffusers.utils import encode_video
|
| 333 |
|
| 334 |
num_frames = snap_frames(duration)
|
|
|
|
| 335 |
conditioned = time.time()
|
| 336 |
prompt_embeds, text_token_tags, metadata, plan = encode_remote(
|
| 337 |
prompt, first, last, canvas, num_frames, rewrite_prompt=upsample, ip_token=ip_token
|
|
@@ -399,8 +425,8 @@ CHAIN_SUFFIX = (
|
|
| 399 |
|
| 400 |
|
| 401 |
def generate(
|
| 402 |
-
prompt, first_frame=None, last_frame=None, canvas=DEFAULT_CANVAS, duration=
|
| 403 |
-
steps=
|
| 404 |
gpu_duration=DEFAULT_GPU_DURATION,
|
| 405 |
):
|
| 406 |
if LOAD_ERROR:
|
|
@@ -416,6 +442,7 @@ def generate(
|
|
| 416 |
duration = max(MIN_UI_DURATION, min(float(duration), MAX_UI_DURATION))
|
| 417 |
with_audio = bool(with_audio)
|
| 418 |
|
|
|
|
| 419 |
try:
|
| 420 |
gpu_duration = int(gpu_duration)
|
| 421 |
except (TypeError, ValueError):
|
|
@@ -423,7 +450,7 @@ def generate(
|
|
| 423 |
if gpu_duration not in GPU_DURATION_CHOICES:
|
| 424 |
gpu_duration = min(GPU_DURATION_CHOICES, key=lambda v: abs(v - gpu_duration))
|
| 425 |
|
| 426 |
-
first, last = first_frame, last_frame
|
| 427 |
if first:
|
| 428 |
first, canvas = _fit_keyframe(first, canvas)
|
| 429 |
if last:
|
|
@@ -482,67 +509,129 @@ def generate(
|
|
| 482 |
shutil.rmtree(work, ignore_errors=True)
|
| 483 |
|
| 484 |
|
| 485 |
-
def
|
| 486 |
-
if
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
else
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
return temp_file.name
|
| 496 |
-
|
| 497 |
-
# ── Gradio Server (Server mode): FastAPI + Gradio queue/API engine ────────────
|
| 498 |
-
app = Server(title="MiniMax-H3-Turbo-LoRA-Fast")
|
| 499 |
-
|
| 500 |
-
@app.mcp.tool(name="generate_video")
|
| 501 |
-
@app.api(name="generate_video")
|
| 502 |
-
def infer(
|
| 503 |
-
prompt: str,
|
| 504 |
-
images_b64_json: str,
|
| 505 |
-
canvas: str,
|
| 506 |
-
duration: float,
|
| 507 |
-
mode: str,
|
| 508 |
-
steps: int,
|
| 509 |
-
seed: int,
|
| 510 |
-
upsample: bool,
|
| 511 |
-
lora: str,
|
| 512 |
-
with_audio: bool,
|
| 513 |
-
gpu_duration: int,
|
| 514 |
-
) -> dict:
|
| 515 |
-
"""Generates a video using MiniMax-H3-Turbo-LoRA-Fast."""
|
| 516 |
-
images = json.loads(images_b64_json) if images_b64_json else []
|
| 517 |
-
first_frame = save_b64_image(images[0]) if len(images) > 0 else None
|
| 518 |
-
last_frame = save_b64_image(images[1]) if len(images) > 1 else None
|
| 519 |
-
|
| 520 |
-
final_video_path, summary, refined = generate(
|
| 521 |
prompt, first_frame, last_frame, canvas, duration, mode, steps, seed, upsample, lora, with_audio,
|
| 522 |
-
ip_token=
|
| 523 |
)
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
"
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 544 |
|
| 545 |
load_models()
|
| 546 |
|
| 547 |
if __name__ == "__main__":
|
| 548 |
-
|
|
|
|
| 1 |
+
"""MiniMax-H3-Turbo-LoRA-Fast — the denoising half of a split MiniMax-H3 deployment.
|
| 2 |
+
|
| 3 |
+
The 62 GiB Qwen3-VL text encoder runs in a separate conditioner Space; this Space loads the transformer and the
|
| 4 |
+
VAEs, folds the selected Turbo/Style LoRA into the bf16 weights, and serves first/last-frame video generation up
|
| 5 |
+
to 20 seconds with optional audio.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
from __future__ import annotations
|
| 9 |
|
| 10 |
import math
|
|
|
|
| 14 |
import tempfile
|
| 15 |
import time
|
| 16 |
import traceback
|
|
|
|
|
|
|
| 17 |
from functools import cache
|
|
|
|
|
|
|
| 18 |
|
| 19 |
import spaces
|
| 20 |
import gradio as gr
|
|
|
|
|
|
|
| 21 |
|
| 22 |
MODEL_REPO = os.environ.get("H3_MODEL_REPO", "MiniMaxAI/MiniMax-H3")
|
| 23 |
CONDITIONER_SPACE = os.environ.get("H3_CONDITIONER", "multimodalart/qwen3vl-conditioner")
|
|
|
|
| 53 |
CHUNK_SECONDS = float(os.environ.get("H3_CHUNK_SECONDS", "14"))
|
| 54 |
CONDITIONER_MAX_FRAMES = int(os.environ.get("H3_CONDITIONER_MAX_FRAMES", "345"))
|
| 55 |
|
| 56 |
+
# Allowed GPU-duration choices (seconds) exposed in the Advanced panel.
|
| 57 |
GPU_DURATION_CHOICES = (60, 90, 120, 150, 180, 250)
|
| 58 |
+
DEFAULT_GPU_DURATION = 90
|
| 59 |
|
| 60 |
OUTPUT_DIR = os.path.join(tempfile.gettempdir(), "h3-outputs")
|
| 61 |
|
|
|
|
| 77 |
|
| 78 |
def patch_duration_window(minimum: float, maximum: float) -> None:
|
| 79 |
from diffusers.modular_pipelines.minimax_h3.modular_pipeline import MiniMaxH3ModularPipeline
|
| 80 |
+
|
| 81 |
MiniMaxH3ModularPipeline.min_duration = property(lambda self: float(minimum))
|
| 82 |
MiniMaxH3ModularPipeline.max_duration = property(lambda self: float(maximum))
|
| 83 |
|
|
|
|
| 85 |
def _apply_lora(transformer) -> str | None:
|
| 86 |
try:
|
| 87 |
import h3_lora
|
| 88 |
+
|
| 89 |
return h3_lora.apply_lora(transformer)
|
| 90 |
except ImportError:
|
| 91 |
+
return "⚠️ `h3_lora.py` is missing — running without the Turbo LoRA. Raise the steps or add the file (README)"
|
| 92 |
except Exception as error:
|
| 93 |
traceback.print_exc()
|
| 94 |
+
return f"⚠️ LoRA loading failed ({type(error).__name__}: {error}) — running without"
|
| 95 |
|
| 96 |
|
| 97 |
def _set_lora(transformer, name: str) -> str:
|
| 98 |
try:
|
| 99 |
import h3_lora
|
| 100 |
+
|
| 101 |
return h3_lora.set_active(transformer, name)
|
| 102 |
except Exception:
|
| 103 |
return "off"
|
|
|
|
| 105 |
|
| 106 |
def load_models() -> str | None:
|
| 107 |
global PIPE, MUTE_PIPE, MANAGER, LOAD_ERROR, MUTE_ERROR, LOADED_IN, LORA_STATUS
|
| 108 |
+
|
| 109 |
if PIPE is not None or LOAD_ERROR is not None:
|
| 110 |
return LOAD_ERROR
|
| 111 |
|
|
|
|
| 113 |
try:
|
| 114 |
import torch
|
| 115 |
from diffusers import ComponentsManager
|
| 116 |
+
|
| 117 |
from h3_split_blocks import MiniMaxH3GeneratorBlocks
|
| 118 |
|
| 119 |
patch_duration_window(MIN_UI_DURATION, PIPELINE_MAX_DURATION)
|
|
|
|
| 151 |
def _build_mute_pipe(pipe):
|
| 152 |
try:
|
| 153 |
from h3_split_blocks import MiniMaxH3MuteGeneratorBlocks
|
| 154 |
+
|
| 155 |
blocks = MiniMaxH3MuteGeneratorBlocks()
|
| 156 |
mute = blocks.init_pipeline(MODEL_REPO)
|
| 157 |
shared = {}
|
|
|
|
| 186 |
@cache
|
| 187 |
def conditioner():
|
| 188 |
from gradio_client import Client
|
| 189 |
+
|
| 190 |
return Client(CONDITIONER_SPACE)
|
| 191 |
|
| 192 |
|
|
|
|
| 194 |
if not ip_token:
|
| 195 |
return conditioner()
|
| 196 |
from gradio_client import Client
|
| 197 |
+
|
| 198 |
return Client(CONDITIONER_SPACE, headers={"x-ip-token": ip_token})
|
| 199 |
|
| 200 |
|
|
|
|
| 238 |
|
| 239 |
def get_duration(
|
| 240 |
prompt_embeds, text_token_tags, image, last_image, height, width, num_frames, steps, seed,
|
| 241 |
+
lora="larry", with_audio=True, gpu_duration=DEFAULT_GPU_DURATION, *a, **k
|
| 242 |
):
|
| 243 |
+
"""Duration callback for `@spaces.GPU`.
|
| 244 |
+
|
| 245 |
+
Honors an explicit user override (`gpu_duration`) when it is one of the
|
| 246 |
+
allowed choices; otherwise falls back to the empirical estimate.
|
| 247 |
+
"""
|
| 248 |
if gpu_duration:
|
| 249 |
try:
|
| 250 |
value = int(gpu_duration)
|
|
|
|
| 259 |
@spaces.GPU(duration=get_duration, size=GPU_SIZE)
|
| 260 |
def _generate(
|
| 261 |
prompt_embeds, text_token_tags, image, last_image, height, width, num_frames, steps, seed,
|
| 262 |
+
lora="larry", with_audio=True, gpu_duration=DEFAULT_GPU_DURATION,
|
| 263 |
):
|
| 264 |
import torch
|
| 265 |
+
|
| 266 |
active_lora = _set_lora(PIPE.transformer, lora)
|
| 267 |
+
|
| 268 |
pipe = PIPE if (with_audio or MUTE_PIPE is None) else MUTE_PIPE
|
| 269 |
decoded_audio = with_audio or MUTE_PIPE is None
|
| 270 |
|
|
|
|
| 294 |
|
| 295 |
def _fit_keyframe(image_path, current_canvas):
|
| 296 |
from PIL import Image as _Image
|
| 297 |
+
|
| 298 |
img = _Image.open(image_path)
|
| 299 |
aspect = img.width / img.height
|
| 300 |
fastest = {}
|
|
|
|
| 329 |
|
| 330 |
def _resolve_lora(lora) -> str:
|
| 331 |
if not isinstance(lora, str) or not lora.strip():
|
| 332 |
+
return "larry"
|
| 333 |
value = lora.strip().lower()
|
| 334 |
lowered = {name.lower(): name for name in LORA_NAMES}
|
| 335 |
if value in lowered:
|
|
|
|
| 340 |
raise gr.Error(f"Unknown LoRA `{lora}`. Allowed: {', '.join(LORA_NAMES)}.")
|
| 341 |
|
| 342 |
|
| 343 |
+
def _as_path(value):
|
| 344 |
+
if isinstance(value, dict):
|
| 345 |
+
value = value.get("path") or (value.get("url") or "").removeprefix("/gradio_api/file=")
|
| 346 |
+
return value or None
|
| 347 |
+
|
| 348 |
+
|
| 349 |
def keyframe(path):
|
| 350 |
from PIL import Image, ImageOps
|
| 351 |
+
|
| 352 |
return ImageOps.exif_transpose(Image.open(path)).convert("RGB") if path else None
|
| 353 |
|
| 354 |
|
|
|
|
| 357 |
from diffusers.utils import encode_video
|
| 358 |
|
| 359 |
num_frames = snap_frames(duration)
|
| 360 |
+
|
| 361 |
conditioned = time.time()
|
| 362 |
prompt_embeds, text_token_tags, metadata, plan = encode_remote(
|
| 363 |
prompt, first, last, canvas, num_frames, rewrite_prompt=upsample, ip_token=ip_token
|
|
|
|
| 425 |
|
| 426 |
|
| 427 |
def generate(
|
| 428 |
+
prompt, first_frame=None, last_frame=None, canvas=DEFAULT_CANVAS, duration=5.0, mode="single",
|
| 429 |
+
steps=6, seed=42, upsample=False, lora="larry", with_audio=True, ip_token=None, progress=None,
|
| 430 |
gpu_duration=DEFAULT_GPU_DURATION,
|
| 431 |
):
|
| 432 |
if LOAD_ERROR:
|
|
|
|
| 442 |
duration = max(MIN_UI_DURATION, min(float(duration), MAX_UI_DURATION))
|
| 443 |
with_audio = bool(with_audio)
|
| 444 |
|
| 445 |
+
# Normalize GPU-duration override to one of the allowed choices.
|
| 446 |
try:
|
| 447 |
gpu_duration = int(gpu_duration)
|
| 448 |
except (TypeError, ValueError):
|
|
|
|
| 450 |
if gpu_duration not in GPU_DURATION_CHOICES:
|
| 451 |
gpu_duration = min(GPU_DURATION_CHOICES, key=lambda v: abs(v - gpu_duration))
|
| 452 |
|
| 453 |
+
first, last = _as_path(first_frame), _as_path(last_frame)
|
| 454 |
if first:
|
| 455 |
first, canvas = _fit_keyframe(first, canvas)
|
| 456 |
if last:
|
|
|
|
| 509 |
shutil.rmtree(work, ignore_errors=True)
|
| 510 |
|
| 511 |
|
| 512 |
+
def _caller_ip_token(request: gr.Request | None) -> str | None:
|
| 513 |
+
if request is None:
|
| 514 |
+
return None
|
| 515 |
+
headers = getattr(request, "headers", None)
|
| 516 |
+
return headers.get("x-ip-token") if headers is not None else None
|
| 517 |
+
|
| 518 |
+
|
| 519 |
+
def run(prompt, first_frame, last_frame, canvas, duration, mode, steps, seed, upsample, lora, with_audio,
|
| 520 |
+
gpu_duration, request: gr.Request = None, progress=gr.Progress()):
|
| 521 |
+
return generate(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 522 |
prompt, first_frame, last_frame, canvas, duration, mode, steps, seed, upsample, lora, with_audio,
|
| 523 |
+
ip_token=_caller_ip_token(request), progress=progress, gpu_duration=gpu_duration,
|
| 524 |
)
|
| 525 |
+
|
| 526 |
+
|
| 527 |
+
def cost_hint(canvas, duration, mode, steps, first_frame, last_frame, with_audio, gpu_duration):
|
| 528 |
+
try:
|
| 529 |
+
height, width = CANVASES.get(canvas, CANVASES[DEFAULT_CANVAS])
|
| 530 |
+
keyframes = int(bool(first_frame)) + int(bool(last_frame))
|
| 531 |
+
durations = [float(duration)] if mode == "single" else plan_clips(float(duration), CHUNK_SECONDS)
|
| 532 |
+
estimated = sum(
|
| 533 |
+
estimate_seconds(height, width, snap_frames(d), int(steps), keyframes, bool(with_audio))
|
| 534 |
+
for d in durations
|
| 535 |
+
)
|
| 536 |
+
try:
|
| 537 |
+
gd = int(gpu_duration)
|
| 538 |
+
except (TypeError, ValueError):
|
| 539 |
+
gd = DEFAULT_GPU_DURATION
|
| 540 |
+
booking = gd * len(durations) if gd in GPU_DURATION_CHOICES else estimated
|
| 541 |
+
frames = sum(snap_frames(d) for d in durations)
|
| 542 |
+
parts = [
|
| 543 |
+
f"{width}x{height}",
|
| 544 |
+
f"{frames} frames ({frames / FPS:.2f}s)",
|
| 545 |
+
f"{len(durations)} pass(es)",
|
| 546 |
+
f"~{booking}s GPU booking" + (f" ({gd}s/clip override)" if gd in GPU_DURATION_CHOICES else " (auto-estimate)"),
|
| 547 |
+
]
|
| 548 |
+
if not with_audio:
|
| 549 |
+
parts.append("audio VAE stays on the host")
|
| 550 |
+
if mode == "single" and float(duration) > 15:
|
| 551 |
+
parts.append("⚠️ >15s extrapolates")
|
| 552 |
+
return " · ".join(parts)
|
| 553 |
+
except Exception as error:
|
| 554 |
+
return f"(estimate unavailable: {error})"
|
| 555 |
+
|
| 556 |
+
|
| 557 |
+
INTRO = f"""# MiniMax-H3-Turbo-LoRA-Fast
|
| 558 |
+
|
| 559 |
+
Denoising half of a split deployment: the 62 GiB Qwen3-VL text encoder runs in
|
| 560 |
+
[`{CONDITIONER_SPACE}`](https://huggingface.co/spaces/{CONDITIONER_SPACE}); this Space holds only the transformer
|
| 561 |
+
and the VAEs.
|
| 562 |
+
|
| 563 |
+
* **First/Last Frame** — both optional and usable individually. A last frame alone means "generate *towards* this image".
|
| 564 |
+
* **Length** — `single` is one pass with a raised ceiling (H3 is trained up to 15 s; beyond that is experimental).
|
| 565 |
+
`chain` cuts the video into clips of at most {CHUNK_SECONDS:g}s and hands the last frame of each clip to the next.
|
| 566 |
+
* **Audio** — when disabled, the audio VAE is never declared, never moved to the GPU and never decoded. The audio
|
| 567 |
+
rows themselves are always denoised by H3; they are part of the same packed sequence.
|
| 568 |
+
* **GPU Duration** — override the ZeroGPU booking per clip in the Advanced panel
|
| 569 |
+
(choices: {', '.join(str(v) for v in GPU_DURATION_CHOICES)} s; default {DEFAULT_GPU_DURATION} s).
|
| 570 |
+
"""
|
| 571 |
+
|
| 572 |
+
|
| 573 |
+
with gr.Blocks(title="MiniMax-H3-Turbo-LoRA-Fast") as demo:
|
| 574 |
+
gr.Markdown(INTRO)
|
| 575 |
+
|
| 576 |
+
with gr.Row():
|
| 577 |
+
with gr.Column(scale=3):
|
| 578 |
+
prompt = gr.Textbox(
|
| 579 |
+
label="Prompt",
|
| 580 |
+
lines=4,
|
| 581 |
+
value="An extreme close-up shot focuses on a young Asian woman with auburn-tinted hair and silver ear piercings, gazing intensely toward the camera. She subtly blinks and shifts her deep brown eyes with a contemplative expression as soft, warm ambient light highlights fine skin texture, natural blemishes, and delicate facial micro-movements.",
|
| 582 |
+
)
|
| 583 |
+
with gr.Row():
|
| 584 |
+
first_frame = gr.Image(label="First Frame (optional)", type="filepath", height=200)
|
| 585 |
+
last_frame = gr.Image(label="Last Frame (optional)", type="filepath", height=200)
|
| 586 |
+
with gr.Row():
|
| 587 |
+
canvas = gr.Dropdown(label="Canvas", choices=list(CANVASES), value=DEFAULT_CANVAS)
|
| 588 |
+
mode = gr.Radio(
|
| 589 |
+
label="Duration mode",
|
| 590 |
+
choices=[("single (1 pass)", "single"), (f"chain (clips ≤ {CHUNK_SECONDS:g}s)", "chain")],
|
| 591 |
+
value="single",
|
| 592 |
+
)
|
| 593 |
+
duration = gr.Slider(
|
| 594 |
+
MIN_UI_DURATION, MAX_UI_DURATION, value=4, step=0.5,
|
| 595 |
+
label="Length (s)", info="Rounded up to the next 17n+5 at 24 fps.",
|
| 596 |
+
)
|
| 597 |
+
with gr.Row():
|
| 598 |
+
with_audio = gr.Checkbox(label="Generate audio", value=False)
|
| 599 |
+
upsample = gr.Checkbox(label="Prompt upsampling (+15–50s)", value=False)
|
| 600 |
+
with gr.Accordion("Advanced", open=True):
|
| 601 |
+
steps = gr.Slider(2, 12, value=6, step=1, label="Steps")
|
| 602 |
+
lora = gr.Dropdown(label="Turbo/Style-LoRA", choices=list(LORA_NAMES), value="lightx")
|
| 603 |
+
seed = gr.Number(label="Seed (clip i = seed + i)", value=42, precision=0)
|
| 604 |
+
gpu_duration = gr.Slider(
|
| 605 |
+
minimum=GPU_DURATION_CHOICES[0],
|
| 606 |
+
maximum=GPU_DURATION_CHOICES[-1],
|
| 607 |
+
value=DEFAULT_GPU_DURATION,
|
| 608 |
+
step=10,
|
| 609 |
+
label="GPU Duration (s per clip)",
|
| 610 |
+
info=f"Allowed: {', '.join(str(v) for v in GPU_DURATION_CHOICES)} s · "
|
| 611 |
+
f"default {DEFAULT_GPU_DURATION} s · overrides ZeroGPU booking per clip.",
|
| 612 |
+
)
|
| 613 |
+
cost = gr.Markdown()
|
| 614 |
+
run_button = gr.Button("Generate video", variant="primary")
|
| 615 |
+
|
| 616 |
+
with gr.Column(scale=2):
|
| 617 |
+
output_video = gr.Video(label="Result", format="mp4", autoplay=True)
|
| 618 |
+
report = gr.Markdown(label="Report")
|
| 619 |
+
refined = gr.Textbox(label="Refined Prompt", lines=4)
|
| 620 |
+
|
| 621 |
+
cost_inputs = [canvas, duration, mode, steps, first_frame, last_frame, with_audio, gpu_duration]
|
| 622 |
+
for control in cost_inputs:
|
| 623 |
+
control.change(cost_hint, cost_inputs, cost)
|
| 624 |
+
demo.load(cost_hint, cost_inputs, cost)
|
| 625 |
+
|
| 626 |
+
run_button.click(
|
| 627 |
+
run,
|
| 628 |
+
[prompt, first_frame, last_frame, canvas, duration, mode, steps, seed, upsample, lora, with_audio, gpu_duration],
|
| 629 |
+
[output_video, report, refined],
|
| 630 |
+
api_name="generate",
|
| 631 |
+
)
|
| 632 |
+
|
| 633 |
|
| 634 |
load_models()
|
| 635 |
|
| 636 |
if __name__ == "__main__":
|
| 637 |
+
demo.queue(default_concurrency_limit=1, max_size=8).launch(theme=gr.themes.Citrus(), show_error=True, allowed_paths=[OUTPUT_DIR])
|