Spaces:
Running on Zero
Running on Zero
| """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'<div class="readout"><span>{frames}</span> frames · ' | |
| f"<span>{frames / h3_engine.FPS:.2f}</span> s at {h3_engine.FPS} fps · " | |
| f"snapped to the VAE grid (17n+5)</div>" | |
| ) | |
| def step_hint(*keys): | |
| steps = lora_stack.suggested_steps(keys) | |
| if steps is None: | |
| return '<div class="hint">No turbo LoRA in the rack - the base checkpoint wants roughly 28 steps.</div>' | |
| return f'<div class="hint accent">Turbo LoRA in the rack - it was distilled for about {steps} steps.</div>' | |
| 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' <a href="{url}" target="_blank" rel="noopener">model card ↗</a>' if url else "" | |
| return f'<div class="slot-card"><b>{about}</b>{link}<br>{hint}</div>' | |
| 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 ( | |
| '<div class="topbar">' | |
| ' <div class="brand">' | |
| ' <div class="brand-mark">H3</div>' | |
| " <div>" | |
| ' <h1>MiniMax H3 Turbo LoRAs<span class="badge">video + stereo audio</span></h1>' | |
| " <p>One packed sequence, 24 fps, sound generated with the picture - and a rack of stackable LoRAs.</p>" | |
| " </div>" | |
| " </div>" | |
| f' <div class="state {tone}"><span class="dot"></span><code>{h3_engine.status()}</code></div>' | |
| "</div>" | |
| ) | |
| # --------------------------------------------------------------------------- | |
| # 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( | |
| '<div class="panel-head"><h2>Keyframes</h2><span>optional</span></div>' | |
| '<p class="note">First frame, last frame, or neither. Uploads are cover-cropped onto the ' | |
| "closest supported canvas.</p>" | |
| ) | |
| 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('<div class="panel-head"><h2>Prompt</h2><span>picture and soundtrack</span></div>') | |
| 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('<div class="panel-head"><h2>Canvas</h2><span>the label the model generates on</span></div>') | |
| 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( | |
| '<p class="note">The checkpoint is guidance-distilled: no negative prompt, no CFG ' | |
| "scale, one forward pass per step.</p>" | |
| ) | |
| with gr.Tab("LoRA rack"): | |
| gr.HTML('<div class="panel-head"><h2>Quick add</h2><span>fills the first free slot</span></div>') | |
| 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'<div class="rack-index">Slot {index + 1}</div>') | |
| 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( | |
| '<p class="note">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.</p>" | |
| ) | |
| 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('<div class="panel-head"><h2>Clip</h2><span>mp4 · 24 fps · stereo 32 kHz</span></div>') | |
| 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( | |
| '<div class="footer"><span>MiniMaxAI/MiniMax-H3 · denoiser here, Qwen3-VL conditioning next door</span>' | |
| "<span>Built with Gradio</span></div>" | |
| ) | |
| # ---- 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]) | |