Spaces:
Running on Zero
Running on Zero
Add lightx2v 8-step v1.0 LoRA as lightx8; fix step-count off-by-one
Browse fileslightx8 loads minimax_h3_fl2v_turbo_8step_v1.0_bf16.safetensors through
the same diffusers-native PEFT mapping (rank 128, alpha 8, scale 0.0625),
suggested 8 steps, H3_LIGHTX8=off skips it.
MiniMaxH3Scheduler reads num_inference_steps as sigma grid points
including the terminal zero, so N evaluations need N + 1 points (as the
ModelTC inference script does). Pass steps + 1 so the UI's step count
matches the actual NFE count.
- README.md +6 -1
- app.py +7 -4
- h3_lora.py +12 -7
README.md
CHANGED
|
@@ -55,6 +55,10 @@ and the low-rank factors stay resident so switching is an in-place unfold/fold t
|
|
| 55 |
`H3_LORA` selects the larry file (`off` skips it), `H3_LIGHTX=off` skips lightx, `H3_LORA_DEFAULT` picks which set
|
| 56 |
starts folded, and `H3_LORA_STRENGTH` scales the larry update (the card's sharpness/artifact dial).
|
| 57 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
A third, non-turbo set is [`fal/MiniMax-H3-Realism-People-LoRA`](https://huggingface.co/fal/MiniMax-H3-Realism-People-LoRA)
|
| 59 |
(`realism` in the dropdown, trigger word `r34l1sm`): realistic people at the full 28 steps. It ships in the same
|
| 60 |
reference-tree layout as larry under a `diffusion_model.` prefix, so it folds through the same key mapping.
|
|
@@ -191,7 +195,8 @@ one-time `PIPE.to("cuda")` is inside the first row's 339 s and does not reappear
|
|
| 191 |
| `H3_LORA` | `minimax_h3_turbo_v4_step600_ema.safetensors` | Turbo LoRA file folded into the transformer at startup. `off` disables. |
|
| 192 |
| `H3_LORA_REPO` | `larryvrh/MiniMax-H3-Turbo-Lora` | Hub repo the LoRA is fetched from. |
|
| 193 |
| `H3_LORA_STRENGTH` | `1.0` | Scales the larry LoRA delta (sharpness/artifact trade-off). |
|
| 194 |
-
| `H3_LIGHTX` | `on` | Set to `off` to skip loading the lightx2v LoRA set. |
|
|
|
|
| 195 |
| `H3_REALISM` | `on` | Set to `off` to skip loading the fal realism-people LoRA set. |
|
| 196 |
| `H3_JOYFOX` | `on` | Set to `off` to skip loading the joyfox 4-step turbo LoRA set. |
|
| 197 |
| `H3_LORA_DEFAULT` | `larry` | Which loaded LoRA set starts folded (`larry` / `lightx` / `realism` / `joyfox`). |
|
|
|
|
| 55 |
`H3_LORA` selects the larry file (`off` skips it), `H3_LIGHTX=off` skips lightx, `H3_LORA_DEFAULT` picks which set
|
| 56 |
starts folded, and `H3_LORA_STRENGTH` scales the larry update (the card's sharpness/artifact dial).
|
| 57 |
|
| 58 |
+
The lightx set also comes as `lightx8` — the 8-step v1.0 diffusers checkpoint
|
| 59 |
+
(`minimax_h3_fl2v_turbo_8step_v1.0_bf16.safetensors`), same PEFT layout and scale, suggested 8 steps.
|
| 60 |
+
`H3_LIGHTX8=off` skips it.
|
| 61 |
+
|
| 62 |
A third, non-turbo set is [`fal/MiniMax-H3-Realism-People-LoRA`](https://huggingface.co/fal/MiniMax-H3-Realism-People-LoRA)
|
| 63 |
(`realism` in the dropdown, trigger word `r34l1sm`): realistic people at the full 28 steps. It ships in the same
|
| 64 |
reference-tree layout as larry under a `diffusion_model.` prefix, so it folds through the same key mapping.
|
|
|
|
| 195 |
| `H3_LORA` | `minimax_h3_turbo_v4_step600_ema.safetensors` | Turbo LoRA file folded into the transformer at startup. `off` disables. |
|
| 196 |
| `H3_LORA_REPO` | `larryvrh/MiniMax-H3-Turbo-Lora` | Hub repo the LoRA is fetched from. |
|
| 197 |
| `H3_LORA_STRENGTH` | `1.0` | Scales the larry LoRA delta (sharpness/artifact trade-off). |
|
| 198 |
+
| `H3_LIGHTX` | `on` | Set to `off` to skip loading the lightx2v 4-step LoRA set. |
|
| 199 |
+
| `H3_LIGHTX8` | `on` | Set to `off` to skip loading the lightx2v 8-step v1.0 LoRA set. |
|
| 200 |
| `H3_REALISM` | `on` | Set to `off` to skip loading the fal realism-people LoRA set. |
|
| 201 |
| `H3_JOYFOX` | `on` | Set to `off` to skip loading the joyfox 4-step turbo LoRA set. |
|
| 202 |
| `H3_LORA_DEFAULT` | `larry` | Which loaded LoRA set starts folded (`larry` / `lightx` / `realism` / `joyfox`). |
|
app.py
CHANGED
|
@@ -254,6 +254,8 @@ def _generate(prompt_embeds, text_token_tags, image, last_image, height, width,
|
|
| 254 |
|
| 255 |
# The modular denoise loop has no callback hook, so count video scheduler steps through a wrapper: one call per
|
| 256 |
# denoise step. When the count reaches the total, what remains inside `PIPE(...)` is the two decoders.
|
|
|
|
|
|
|
| 257 |
steps = int(steps)
|
| 258 |
PROGRESS.update(phase="denoise", step=0, steps=steps)
|
| 259 |
original_step = PIPE.scheduler.step
|
|
@@ -282,7 +284,7 @@ def _generate(prompt_embeds, text_token_tags, image, last_image, height, width,
|
|
| 282 |
height=height,
|
| 283 |
width=width,
|
| 284 |
num_frames=num_frames,
|
| 285 |
-
num_inference_steps=steps,
|
| 286 |
generator=torch.Generator("cpu").manual_seed(int(seed)),
|
| 287 |
)
|
| 288 |
finally:
|
|
@@ -326,8 +328,8 @@ def _fit_keyframe(image_path, current_canvas):
|
|
| 326 |
|
| 327 |
|
| 328 |
def _resolve_lora(lora, use_lora) -> str:
|
| 329 |
-
"""`lora` (`larry` / `lightx` / `realism` / `joyfox` / `off`) wins; the legacy bool maps onto `larry` / `off`."""
|
| 330 |
-
if isinstance(lora, str) and lora in ("larry", "lightx", "realism", "joyfox", "off"):
|
| 331 |
return lora
|
| 332 |
return "larry" if use_lora else "off"
|
| 333 |
|
|
@@ -452,7 +454,8 @@ def studio_config():
|
|
| 452 |
# The LoRA dropdown: value -> {label, suggested steps}.
|
| 453 |
"loras": {
|
| 454 |
**{
|
| 455 |
-
name: {"label": spec["label"],
|
|
|
|
| 456 |
for name, spec in sets.items()
|
| 457 |
},
|
| 458 |
"off": {"label": "off (base model)", "steps": 28},
|
|
|
|
| 254 |
|
| 255 |
# The modular denoise loop has no callback hook, so count video scheduler steps through a wrapper: one call per
|
| 256 |
# denoise step. When the count reaches the total, what remains inside `PIPE(...)` is the two decoders.
|
| 257 |
+
# `MiniMaxH3Scheduler` reads `num_inference_steps` as sigma grid points, terminal zero included, so N model
|
| 258 |
+
# evaluations need N + 1 points (the lightx/ModelTC inference script does the same).
|
| 259 |
steps = int(steps)
|
| 260 |
PROGRESS.update(phase="denoise", step=0, steps=steps)
|
| 261 |
original_step = PIPE.scheduler.step
|
|
|
|
| 284 |
height=height,
|
| 285 |
width=width,
|
| 286 |
num_frames=num_frames,
|
| 287 |
+
num_inference_steps=steps + 1,
|
| 288 |
generator=torch.Generator("cpu").manual_seed(int(seed)),
|
| 289 |
)
|
| 290 |
finally:
|
|
|
|
| 328 |
|
| 329 |
|
| 330 |
def _resolve_lora(lora, use_lora) -> str:
|
| 331 |
+
"""`lora` (`larry` / `lightx` / `lightx8` / `realism` / `joyfox` / `off`) wins; the legacy bool maps onto `larry` / `off`."""
|
| 332 |
+
if isinstance(lora, str) and lora in ("larry", "lightx", "lightx8", "realism", "joyfox", "off"):
|
| 333 |
return lora
|
| 334 |
return "larry" if use_lora else "off"
|
| 335 |
|
|
|
|
| 454 |
# The LoRA dropdown: value -> {label, suggested steps}.
|
| 455 |
"loras": {
|
| 456 |
**{
|
| 457 |
+
name: {"label": spec["label"],
|
| 458 |
+
"steps": {"larry": 6, "lightx": 4, "lightx8": 8, "realism": 28, "joyfox": 4}.get(name, 6)}
|
| 459 |
for name, spec in sets.items()
|
| 460 |
},
|
| 461 |
"off": {"label": "off (base model)", "steps": 28},
|
h3_lora.py
CHANGED
|
@@ -17,9 +17,10 @@ The two supported LoRAs ship in different layouts:
|
|
| 17 |
`final_layer.adaln_proj.linear` -> `norm_out.linear`. The row transforms are applied to `lora_B` directly
|
| 18 |
(rows of `B @ A` are rows of `B`), so no full delta is ever materialized at load.
|
| 19 |
|
| 20 |
-
* `lightx` (`lightx2v/Minimax-h3-Turbo`)
|
| 21 |
`transformer_blocks.N.attn.to_q.lora_A.default.weight` and friends — rank 128, `alpha == 8`, so the fold scale is
|
| 22 |
`8 / 128 = 0.0625` (matching `set_adapters(weights=1.0)` in their inference script). Keys map name-for-name.
|
|
|
|
| 23 |
|
| 24 |
* `realism` (`fal/MiniMax-H3-Realism-People-LoRA`) is a *style* LoRA, not a turbo one — realistic people, trigger
|
| 25 |
word `r34l1sm`. Same reference tree as larry under a `diffusion_model.` prefix, attention only (`qkv_proj` /
|
|
@@ -35,8 +36,8 @@ The two supported LoRAs ship in different layouts:
|
|
| 35 |
(`[96768, 2688]`), so those deltas have no counterpart to fold into.
|
| 36 |
|
| 37 |
`H3_LORA` selects the larry file (`off` skips loading it), `H3_LIGHTX=off` skips lightx, `H3_REALISM=off` skips
|
| 38 |
-
realism, `H3_JOYFOX=off` skips joyfox, `
|
| 39 |
-
the larry card's sharpness/artifact dial.
|
| 40 |
"""
|
| 41 |
|
| 42 |
from __future__ import annotations
|
|
@@ -51,6 +52,7 @@ LARRY_REPO = os.environ.get("H3_LORA_REPO", "larryvrh/MiniMax-H3-Turbo-Lora")
|
|
| 51 |
LARRY_FILE = os.environ.get("H3_LORA", "minimax_h3_turbo_v4_step600_ema.safetensors")
|
| 52 |
LIGHTX_REPO = os.environ.get("H3_LIGHTX_REPO", "lightx2v/Minimax-h3-Turbo")
|
| 53 |
LIGHTX_FILE = os.environ.get("H3_LIGHTX_FILE", "minimax_h3_fl2v_turbo_4step_v0.1.safetensors")
|
|
|
|
| 54 |
LIGHTX_ALPHA = 8
|
| 55 |
REALISM_REPO = os.environ.get("H3_REALISM_REPO", "fal/MiniMax-H3-Realism-People-LoRA")
|
| 56 |
REALISM_FILE = os.environ.get("H3_REALISM_FILE", "h3-realism-people-t2v-i2v-r2v.safetensors")
|
|
@@ -107,19 +109,20 @@ def _load_larry(inner_dim: int) -> dict:
|
|
| 107 |
}
|
| 108 |
|
| 109 |
|
| 110 |
-
def _load_lightx() -> dict:
|
|
|
|
| 111 |
from huggingface_hub import hf_hub_download
|
| 112 |
from safetensors.torch import load_file
|
| 113 |
|
| 114 |
-
lora = load_file(hf_hub_download(LIGHTX_REPO,
|
| 115 |
suffix_a, suffix_b = ".lora_A.default.weight", ".lora_B.default.weight"
|
| 116 |
bases = sorted({key[: -len(suffix_a)] for key in lora if key.endswith(suffix_a)})
|
| 117 |
ranks = {lora[f"{name}{suffix_a}"].shape[0] for name in bases}
|
| 118 |
if len(ranks) != 1:
|
| 119 |
-
raise ValueError(f"Mixed LoRA ranks in {
|
| 120 |
entries = [(f"{name}.weight", lora[f"{name}{suffix_a}"], lora[f"{name}{suffix_b}"]) for name in bases]
|
| 121 |
return {
|
| 122 |
-
"label": f"{LIGHTX_REPO}/{
|
| 123 |
"scale": LIGHTX_ALPHA / ranks.pop(),
|
| 124 |
"entries": entries,
|
| 125 |
}
|
|
@@ -195,6 +198,8 @@ def apply_lora(transformer) -> str | None:
|
|
| 195 |
sets["larry"] = _load_larry(inner_dim)
|
| 196 |
if os.environ.get("H3_LIGHTX", "on").lower() not in ("", "off", "none"):
|
| 197 |
sets["lightx"] = _load_lightx()
|
|
|
|
|
|
|
| 198 |
if os.environ.get("H3_REALISM", "on").lower() not in ("", "off", "none"):
|
| 199 |
sets["realism"] = _load_realism(inner_dim)
|
| 200 |
if os.environ.get("H3_JOYFOX", "on").lower() not in ("", "off", "none"):
|
|
|
|
| 17 |
`final_layer.adaln_proj.linear` -> `norm_out.linear`. The row transforms are applied to `lora_B` directly
|
| 18 |
(rows of `B @ A` are rows of `B`), so no full delta is ever materialized at load.
|
| 19 |
|
| 20 |
+
* `lightx` / `lightx8` (`lightx2v/Minimax-h3-Turbo`) are PEFT checkpoints against the diffusers tree itself —
|
| 21 |
`transformer_blocks.N.attn.to_q.lora_A.default.weight` and friends — rank 128, `alpha == 8`, so the fold scale is
|
| 22 |
`8 / 128 = 0.0625` (matching `set_adapters(weights=1.0)` in their inference script). Keys map name-for-name.
|
| 23 |
+
`lightx` is the 4-step file, `lightx8` the 8-step v1.0 file.
|
| 24 |
|
| 25 |
* `realism` (`fal/MiniMax-H3-Realism-People-LoRA`) is a *style* LoRA, not a turbo one — realistic people, trigger
|
| 26 |
word `r34l1sm`. Same reference tree as larry under a `diffusion_model.` prefix, attention only (`qkv_proj` /
|
|
|
|
| 36 |
(`[96768, 2688]`), so those deltas have no counterpart to fold into.
|
| 37 |
|
| 38 |
`H3_LORA` selects the larry file (`off` skips loading it), `H3_LIGHTX=off` skips lightx, `H3_REALISM=off` skips
|
| 39 |
+
realism, `H3_JOYFOX=off` skips joyfox, `H3_LIGHTX8=off` skips the lightx 8-step file, `H3_LORA_DEFAULT` picks which
|
| 40 |
+
set starts folded, and `H3_LORA_STRENGTH` is the larry card's sharpness/artifact dial.
|
| 41 |
"""
|
| 42 |
|
| 43 |
from __future__ import annotations
|
|
|
|
| 52 |
LARRY_FILE = os.environ.get("H3_LORA", "minimax_h3_turbo_v4_step600_ema.safetensors")
|
| 53 |
LIGHTX_REPO = os.environ.get("H3_LIGHTX_REPO", "lightx2v/Minimax-h3-Turbo")
|
| 54 |
LIGHTX_FILE = os.environ.get("H3_LIGHTX_FILE", "minimax_h3_fl2v_turbo_4step_v0.1.safetensors")
|
| 55 |
+
LIGHTX8_FILE = os.environ.get("H3_LIGHTX8_FILE", "minimax_h3_fl2v_turbo_8step_v1.0_bf16.safetensors")
|
| 56 |
LIGHTX_ALPHA = 8
|
| 57 |
REALISM_REPO = os.environ.get("H3_REALISM_REPO", "fal/MiniMax-H3-Realism-People-LoRA")
|
| 58 |
REALISM_FILE = os.environ.get("H3_REALISM_FILE", "h3-realism-people-t2v-i2v-r2v.safetensors")
|
|
|
|
| 109 |
}
|
| 110 |
|
| 111 |
|
| 112 |
+
def _load_lightx(file: str = LIGHTX_FILE) -> dict:
|
| 113 |
+
"""A diffusers-native PEFT checkpoint from `lightx2v/Minimax-h3-Turbo` — keys map name-for-name."""
|
| 114 |
from huggingface_hub import hf_hub_download
|
| 115 |
from safetensors.torch import load_file
|
| 116 |
|
| 117 |
+
lora = load_file(hf_hub_download(LIGHTX_REPO, file))
|
| 118 |
suffix_a, suffix_b = ".lora_A.default.weight", ".lora_B.default.weight"
|
| 119 |
bases = sorted({key[: -len(suffix_a)] for key in lora if key.endswith(suffix_a)})
|
| 120 |
ranks = {lora[f"{name}{suffix_a}"].shape[0] for name in bases}
|
| 121 |
if len(ranks) != 1:
|
| 122 |
+
raise ValueError(f"Mixed LoRA ranks in {file}: {sorted(ranks)}")
|
| 123 |
entries = [(f"{name}.weight", lora[f"{name}{suffix_a}"], lora[f"{name}{suffix_b}"]) for name in bases]
|
| 124 |
return {
|
| 125 |
+
"label": f"{LIGHTX_REPO}/{file}",
|
| 126 |
"scale": LIGHTX_ALPHA / ranks.pop(),
|
| 127 |
"entries": entries,
|
| 128 |
}
|
|
|
|
| 198 |
sets["larry"] = _load_larry(inner_dim)
|
| 199 |
if os.environ.get("H3_LIGHTX", "on").lower() not in ("", "off", "none"):
|
| 200 |
sets["lightx"] = _load_lightx()
|
| 201 |
+
if os.environ.get("H3_LIGHTX8", "on").lower() not in ("", "off", "none"):
|
| 202 |
+
sets["lightx8"] = _load_lightx(LIGHTX8_FILE)
|
| 203 |
if os.environ.get("H3_REALISM", "on").lower() not in ("", "off", "none"):
|
| 204 |
sets["realism"] = _load_realism(inner_dim)
|
| 205 |
if os.environ.get("H3_JOYFOX", "on").lower() not in ("", "off", "none"):
|