"""MiniMax-H3 Turbo LoRAs - a video+audio studio with a stackable LoRA rack.
The UI is the whole of this module: request orchestration lives in `h3_engine`, the LoRA rack in `lora_stack`.
No prompt is inspected, rewritten or refused anywhere in this Space - the only rejected request is an empty one,
which the model itself does not accept.
"""
from __future__ import annotations
import random
import gradio as gr
import h3_engine
import lora_stack
SLOTS = 5
MAX_SEED = 2**32 - 1
DEFAULT_PROMPT = (
"A lighthouse keeper climbs the spiral stairs at dawn, lantern swinging, waves booming against the rocks below"
)
# ---------------------------------------------------------------------------
# Handlers
# ---------------------------------------------------------------------------
def run(
prompt,
first_frame,
last_frame,
canvas,
duration,
steps,
seed,
randomize,
*slot_values,
request: gr.Request = None,
):
"""One Generate press. `slot_values` is `key, url, weight` per slot, in slot order."""
if not (prompt or "").strip():
raise gr.Error("MiniMax-H3 always takes a prompt, keyframes or not.")
if randomize:
seed = random.randint(0, MAX_SEED)
slots = [tuple(slot_values[index * 3 : index * 3 + 3]) for index in range(SLOTS)]
try:
stack = lora_stack.build_stack(slots)
except ValueError as error:
raise gr.Error(str(error)) from error
# The visitor's ZeroGPU token, so the conditioner's encode is booked against their quota and not this Space's
# shared IP allowance.
ip_token = request.headers.get("x-ip-token") if request is not None else None
try:
path, report, facts = h3_engine.generate(
prompt,
first_frame,
last_frame,
canvas,
duration,
steps,
int(seed),
stack,
ip_token=ip_token,
)
except gr.Error:
raise
except Exception as error:
raise gr.Error(f"{type(error).__name__}: {error}") from error
return path, str(facts["seed"]), facts["geometry"], facts["timing"], facts["loras"], report
def frame_readout(duration):
frames = h3_engine.snap_frames(duration)
return (
f'
{frames} frames · '
f"{frames / h3_engine.FPS:.2f} s at {h3_engine.FPS} fps · "
f"snapped to the VAE grid (17n+5)
"
)
def step_hint(*keys):
steps = lora_stack.suggested_steps(keys)
if steps is None:
return 'No turbo LoRA in the rack - the base checkpoint wants roughly 28 steps.
'
return f'Turbo LoRA in the rack - it was distilled for about {steps} steps.
'
def slot_card(key):
"""The blurb under a slot: what the LoRA does, how to drive it, where it lives."""
about, hint, url = lora_stack.about(key)
if not about:
return ""
link = f' model card ↗' if url else ""
return f'{about}{link}
{hint}
'
def show_url(key):
return gr.Textbox(visible=key == lora_stack.CUSTOM_CHOICE)
def quick_add(preset):
"""A chip fills the first free slot, and does nothing when the preset is already racked."""
def handler(*keys):
keys = list(keys)
if preset in keys:
return tuple(keys)
free = keys.index(lora_stack.NONE_CHOICE) if lora_stack.NONE_CHOICE in keys else 0
keys[free] = preset
return tuple(keys)
return handler
# ---------------------------------------------------------------------------
# Theme + CSS
# ---------------------------------------------------------------------------
THEME = gr.themes.Base(
primary_hue=gr.themes.colors.amber,
secondary_hue=gr.themes.colors.teal,
neutral_hue=gr.themes.colors.gray,
radius_size=gr.themes.sizes.radius_sm,
font=[gr.themes.GoogleFont("Space Grotesk"), "ui-sans-serif", "system-ui"],
)
CSS = """
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap');
* { font-family: 'Space Grotesk', system-ui, sans-serif !important; }
code, .mono, .readout, .badge, .rack-index, .meta-tile textarea, #report textarea,
input[type=number], .slot-weight .tab-like-container input { font-family: 'IBM Plex Mono', monospace !important; }
/* ---- shell ---------------------------------------------------------- */
body, .dark { background: #07090c !important; }
.gradio-container {
background:
radial-gradient(900px 520px at 12% -10%, rgba(255,180,84,.10) 0%, rgba(7,9,12,0) 60%),
radial-gradient(900px 520px at 88% -6%, rgba(79,214,200,.10) 0%, rgba(7,9,12,0) 62%),
#07090c !important;
max-width: none !important; overflow: visible !important; color: #dfe5ea !important; }
.app { max-width: 1420px !important; width: 100% !important; margin: 0 auto !important;
padding: 0 22px 48px !important; }
main.contain { padding: 0 !important; gap: 0 !important; }
footer.svelte-zxu34v, gradio-app > .gradio-container > .main + footer { display: none !important; }
::selection { background: rgba(255,180,84,.30); }
::-webkit-scrollbar { width: 9px; height: 9px; }
::-webkit-scrollbar-track { background: #07090c; }
::-webkit-scrollbar-thumb { background: #1f262d; border-radius: 6px; border: 2px solid #07090c; }
* { scrollbar-width: thin; scrollbar-color: #1f262d #07090c; }
/* ---- top bar -------------------------------------------------------- */
.topbar { display: flex; align-items: center; justify-content: space-between; gap: 16px;
padding: 22px 0 18px; border-bottom: 1px solid #161b21; margin-bottom: 22px; flex-wrap: wrap; }
.brand { display: flex; align-items: center; gap: 14px; }
.brand-mark { width: 40px; height: 40px; border-radius: 10px; display: grid; place-items: center;
background: linear-gradient(140deg, #ffb454, #ff7a45 55%, #4fd6c8); color: #0a0d11;
font-weight: 700; font-size: 17px; letter-spacing: -.5px; }
.brand h1 { margin: 0; font-size: 21px; font-weight: 700; letter-spacing: -.4px; color: #f2f5f7; }
.brand p { margin: 2px 0 0; font-size: 12.5px; color: #7a848e; }
.badge { font-size: 10.5px; letter-spacing: .10em; text-transform: uppercase; color: #ffb454;
border: 1px solid rgba(255,180,84,.35); border-radius: 999px; padding: 3px 9px; margin-left: 10px; }
.state { display: flex; align-items: center; gap: 9px; font-size: 12px; color: #99a3ad;
background: #0d1116; border: 1px solid #1a2028; border-radius: 999px; padding: 7px 14px; max-width: 640px; }
.state .dot { width: 7px; height: 7px; border-radius: 50%; background: #4fd6c8; flex: 0 0 auto;
box-shadow: 0 0 0 3px rgba(79,214,200,.16); }
.state.busy .dot { background: #ffb454; box-shadow: 0 0 0 3px rgba(255,180,84,.16); }
.state.bad .dot { background: #ff6b6b; box-shadow: 0 0 0 3px rgba(255,107,107,.16); }
.state code { color: #cfd6dd; background: none; padding: 0; font-size: 11.5px; }
/* ---- columns -------------------------------------------------------- */
#main { gap: 20px !important; align-items: flex-start !important; }
#console { gap: 16px !important; min-width: 0 !important; }
#preview { position: sticky !important; top: 18px !important; align-self: flex-start !important;
min-width: 0 !important; }
#dock { position: sticky !important; bottom: 14px !important; z-index: 6; min-width: 0 !important; }
@media (max-width: 960px) {
#main { flex-direction: column !important; }
#main > * { width: 100% !important; }
#preview { position: static !important; }
}
/* ---- panels --------------------------------------------------------- */
.panel { background: #0b0e13 !important; border: 1px solid #1b2129 !important; border-radius: 14px !important;
padding: 16px !important; gap: 12px !important; }
.panel .block, .panel .form { background: transparent !important; border: none !important;
box-shadow: none !important; padding: 0 !important; }
.panel-head { display: flex; align-items: baseline; gap: 10px; margin-bottom: 2px; }
.panel-head h2 { margin: 0; font-size: 11px; font-weight: 600; letter-spacing: .13em; text-transform: uppercase;
color: #6d7883; }
.panel-head span { font-size: 11.5px; color: #4e5761; }
.panel.tabs { padding-bottom: 66px !important; }
.panel.hero { border-color: #24303a !important;
background: linear-gradient(180deg, #10151b 0%, #0c0f14 100%) !important; }
.note { font-size: 11.5px; color: #5f6a75; margin: 0; }
.hint { font-size: 11.5px; color: #6d7883; padding: 8px 10px; border-left: 2px solid #232b34;
background: #0a0d11; border-radius: 0 8px 8px 0; }
.hint.accent { border-left-color: #ffb454; color: #b9a389; }
.readout { font-size: 11.5px; color: #7d8791; letter-spacing: .02em; }
.readout span { color: #4fd6c8; font-weight: 600; }
/* ---- inputs --------------------------------------------------------- */
textarea, input[type=text], input[type=number] {
background: #090c10 !important; border: 1px solid #1c232b !important; color: #e6ebf0 !important;
border-radius: 10px !important; }
textarea:focus, input[type=text]:focus { border-color: #ffb454 !important;
box-shadow: 0 0 0 3px rgba(255,180,84,.10) !important; }
#prompt textarea { font-size: 14.5px !important; line-height: 1.55 !important; min-height: 96px !important; }
span[data-testid="block-info"] { font-size: 11px !important; color: #6d7883 !important;
letter-spacing: .06em !important; text-transform: uppercase !important; }
.panel input[type=range] { accent-color: #ffb454 !important; }
/* keyframe drop zones */
.frame-slot { background: #090c10 !important; border: 1px dashed #232b34 !important; border-radius: 12px !important; }
.frame-slot:hover { border-color: #3a4650 !important; }
/* canvas chips */
#canvas .wrap { gap: 6px !important; flex-wrap: wrap !important; }
#canvas label { background: #0a0e12 !important; border: 1px solid #1b222a !important; border-radius: 8px !important;
padding: 6px 10px !important; font-size: 11.5px !important; color: #939da7 !important; }
#canvas label:has(input:checked) { border-color: #ffb454 !important; color: #ffce8a !important;
background: rgba(255,180,84,.08) !important; }
#canvas input { display: none !important; }
/* tabs */
#tabs button { font-size: 12.5px !important; color: #7a848e !important; }
#tabs button.selected { color: #ffce8a !important; }
#tabs .tab-nav, #tabs .tab-container { border-bottom: 1px solid #171d24 !important; }
/* ---- lora rack ------------------------------------------------------ */
.chips { gap: 6px !important; flex-wrap: wrap !important; }
.chip { background: rgba(79,214,200,.08) !important; border: 1px solid rgba(79,214,200,.28) !important;
color: #8fe3d8 !important; font-size: 11.5px !important; border-radius: 999px !important;
padding: 5px 11px !important; flex: 0 1 auto !important; width: auto !important; min-width: 0 !important; }
.chip:hover { background: rgba(79,214,200,.18) !important; color: #d5fbf5 !important; }
.slot { background: #090c10 !important; border: 1px solid #171d24 !important; border-radius: 12px !important;
padding: 12px !important; gap: 9px !important; }
.slot:has(.slot-card) { border-color: #232b34 !important; }
.rack-index { font-size: 10px; letter-spacing: .16em; color: #4e5761; text-transform: uppercase; }
.slot .row { gap: 10px !important; align-items: center !important; }
.slot-weight .min_value, .slot-weight .max_value { display: none !important; }
.slot-card { font-size: 11.5px; color: #7d8791; line-height: 1.5; padding: 9px 11px; background: #0b0f14;
border: 1px solid #161c23; border-radius: 9px; }
.slot-card b { color: #b8c2cb; font-weight: 600; }
.slot-card a { color: #4fd6c8; text-decoration: none; }
.slot-url textarea { font-size: 11.5px !important; }
/* ---- generate ------------------------------------------------------- */
#generate { background: linear-gradient(120deg, #ffb454, #ff8a4c) !important; color: #100b04 !important;
border: none !important; font-weight: 700 !important; font-size: 15px !important; letter-spacing: .02em;
padding: 15px !important; border-radius: 12px !important;
box-shadow: 0 14px 34px -14px rgba(255,140,70,.75) !important; }
#generate:hover { filter: brightness(1.06); transform: translateY(-1px); }
/* ---- preview -------------------------------------------------------- */
#clip video { border-radius: 12px !important; background: #05070a !important; }
#meta { gap: 8px !important; }
.meta-tile textarea { background: #090c10 !important; border: 1px solid #171d24 !important; color: #cfd6dd !important;
font-size: 11.5px !important; text-align: center !important; }
.meta-tile span[data-testid="block-info"] { font-size: 9.5px !important; }
#report textarea { background: #090c10 !important; border: 1px solid #171d24 !important; color: #8b959f !important;
font-size: 11px !important; line-height: 1.6 !important; }
.footer { border-top: 1px solid #161b21; margin-top: 26px; padding-top: 14px; font-size: 11.5px; color: #4e5761;
display: flex; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.footer span { color: #6d7883; }
"""
def topbar_html() -> str:
"""Title, badge and the engine's own status line - re-rendered whenever a page opens."""
if h3_engine.LOAD_ERROR:
tone = "bad"
elif not h3_engine.is_ready():
tone = "busy"
else:
tone = ""
return (
''
'
'
'
H3
'
"
"
'
MiniMax H3 Turbo LoRAsvideo + stereo audio
'
"
One packed sequence, 24 fps, sound generated with the picture - and a rack of stackable LoRAs.
"
"
"
"
"
f'
{h3_engine.status()}
'
"
"
)
# ---------------------------------------------------------------------------
# UI
# ---------------------------------------------------------------------------
with gr.Blocks(title="MiniMax H3 Turbo LoRAs") as demo:
# The bar is a component, not static markup: the 77 GB load finishes after the page is built, and every visitor
# gets the state as of the moment they opened it.
topbar = gr.HTML(topbar_html())
with gr.Row(equal_height=False, elem_id="main"):
# ---- console -----------------------------------------------------
with gr.Column(scale=105, min_width=340, elem_id="console"):
with gr.Column(elem_classes="panel"):
gr.HTML(
'Keyframes
optional'
'First frame, last frame, or neither. Uploads are cover-cropped onto the '
"closest supported canvas.
"
)
with gr.Row():
first_frame = gr.Image(
type="filepath", label="First frame", height=176, elem_classes="frame-slot"
)
last_frame = gr.Image(type="filepath", label="Last frame", height=176, elem_classes="frame-slot")
with gr.Column(elem_classes="panel hero"):
gr.HTML('Prompt
picture and soundtrack')
prompt = gr.Textbox(
show_label=False,
lines=4,
elem_id="prompt",
value=DEFAULT_PROMPT,
placeholder="Describe the shot - and what it sounds like. Dialogue in quotes is spoken.",
)
with gr.Column(elem_classes="panel tabs"):
with gr.Tabs(elem_id="tabs"):
with gr.Tab("Shot"):
gr.HTML('Canvas
the label the model generates on')
canvas = gr.Radio(
choices=list(h3_engine.CANVASES),
value=h3_engine.DEFAULT_CANVAS,
show_label=False,
elem_id="canvas",
)
duration = gr.Slider(
h3_engine.MIN_UI_DURATION,
h3_engine.MAX_UI_DURATION,
value=5,
step=0.5,
label="Duration (seconds)",
)
frames_readout = gr.HTML(frame_readout(5))
with gr.Tab("Sampling"):
steps = gr.Slider(1, 30, value=6, step=1, label="Steps")
steps_hint = gr.HTML(step_hint())
with gr.Row():
seed = gr.Number(value=42, precision=0, label="Seed", interactive=False)
randomize = gr.Checkbox(value=True, label="Random seed")
gr.HTML(
'The checkpoint is guidance-distilled: no negative prompt, no CFG '
"scale, one forward pass per step.
"
)
with gr.Tab("LoRA rack"):
gr.HTML('Quick add
fills the first free slot')
with gr.Row(elem_classes="chips"):
chips = [
(gr.Button(key, size="sm", elem_classes="chip", min_width=40), key)
for key in lora_stack.CATALOG
]
slot_keys, slot_urls, slot_weights, slot_cards = [], [], [], []
for index in range(SLOTS):
with gr.Column(elem_classes="slot"):
gr.HTML(f'Slot {index + 1}
')
with gr.Row():
key = gr.Dropdown(
choices=lora_stack.CHOICES,
value=lora_stack.NONE_CHOICE,
show_label=False,
scale=3,
)
weight = gr.Slider(
0.0,
2.0,
value=1.0,
step=0.05,
label="Weight",
scale=2,
min_width=112,
elem_classes="slot-weight",
)
url = gr.Textbox(
visible=False,
show_label=False,
lines=2,
elem_classes="slot-url",
placeholder=(
"HF: user/repo/file.safetensors · CivitAI: "
"civitai.com/models/X?modelVersionId=Y · https://…/lora.safetensors"
),
)
card = gr.HTML("")
slot_keys.append(key)
slot_urls.append(url)
slot_weights.append(weight)
slot_cards.append(card)
gr.HTML(
'Slots stack: every filled one is loaded as its own adapter and '
"activated with its weight. Weights change for free - nothing is baked into the "
"checkpoint.
"
)
with gr.Column(elem_id="dock"):
generate = gr.Button("Generate clip", variant="primary", elem_id="generate")
# ---- preview -----------------------------------------------------
with gr.Column(scale=110, min_width=340, elem_id="preview"):
with gr.Column(elem_classes="panel"):
gr.HTML('Clip
mp4 · 24 fps · stereo 32 kHz')
clip = gr.Video(show_label=False, height=380, elem_id="clip", interactive=False)
with gr.Row(elem_id="meta"):
used_seed = gr.Textbox(label="Seed", value="—", interactive=False, elem_classes="meta-tile")
geometry = gr.Textbox(label="Geometry", value="—", interactive=False, elem_classes="meta-tile")
with gr.Row(elem_id="meta"):
timing = gr.Textbox(label="Timing", value="—", interactive=False, elem_classes="meta-tile")
stack_used = gr.Textbox(label="Stack", value="—", interactive=False, elem_classes="meta-tile")
report = gr.Textbox(label="Report", value="", lines=4, interactive=False, elem_id="report")
gr.HTML(
'"
)
# ---- events ----------------------------------------------------------
for chip, preset in chips:
chip.click(fn=quick_add(preset), inputs=slot_keys, outputs=slot_keys)
for key, url, weight, card in zip(slot_keys, slot_urls, slot_weights, slot_cards):
key.change(fn=show_url, inputs=[key], outputs=[url])
key.change(fn=slot_card, inputs=[key], outputs=[card])
key.change(fn=lora_stack.default_weight, inputs=[key], outputs=[weight])
key.change(fn=step_hint, inputs=slot_keys, outputs=[steps_hint])
duration.change(fn=frame_readout, inputs=[duration], outputs=[frames_readout])
randomize.change(fn=lambda on: gr.Number(interactive=not on), inputs=[randomize], outputs=[seed])
demo.load(fn=topbar_html, outputs=[topbar])
generate.click(
fn=run,
inputs=[
prompt,
first_frame,
last_frame,
canvas,
duration,
steps,
seed,
randomize,
*[widget for slot in zip(slot_keys, slot_urls, slot_weights) for widget in slot],
],
outputs=[clip, used_seed, geometry, timing, stack_used, report],
)
if __name__ == "__main__":
h3_engine.load_engine()
# allowed_paths: the file route only serves whitelisted directories, and the clips live outside the repo.
# Gradio 6 takes the theme and the stylesheet at launch, not on the Blocks constructor.
demo.launch(theme=THEME, css=CSS, show_error=True, allowed_paths=[h3_engine.OUTPUT_DIR])