Text-to-Video
Diffusers
Safetensors
LTX2Pipeline
video-generation
audio-generation
audio-visual
ltx-2
marimo
molab
Instructions to use dragonlimited/LTX2-marimo with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Diffusers
How to use dragonlimited/LTX2-marimo with Diffusers:
pip install -U diffusers transformers accelerate
import torch from diffusers import DiffusionPipeline # switch to "mps" for apple devices pipe = DiffusionPipeline.from_pretrained("dragonlimited/LTX2-marimo", dtype=torch.bfloat16, device_map="cuda") prompt = "Astronaut in a jungle, cold color palette, muted colors, detailed, 8k" image = pipe(prompt).images[0] - Notebooks
- Google Colab
- Kaggle
Legend of Valor: Act I rendered, notebook viewer + LTX-2 load fixes
Browse files- film/legend-of-valor/README.md +45 -0
- logs/legend-of-valor-film.log +8 -0
- notebook.py +100 -0
film/legend-of-valor/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Legend of Valor
|
| 2 |
+
|
| 3 |
+
A 107.6-second vertical short film — 576 x 1024 at 24 fps, 5 acts, 15 shots, 40 denoising steps each —
|
| 4 |
+
rendered with `LTX2ConditionPipeline` from [Lightricks/LTX-2](https://huggingface.co/Lightricks/LTX-2).
|
| 5 |
+
|
| 6 |
+
## The story
|
| 7 |
+
|
| 8 |
+
Assembled from *Arena of Valor*'s documented lore (TiMi Studio Group / Level Infinite), not invented:
|
| 9 |
+
|
| 10 |
+
- **Volkath, The Black Apostate** — *"Light is fleeting, Darkness is forever."* Lord of **Lokheim** and the
|
| 11 |
+
demons. His recorded story opens on **Mt. Orphean**: a golden-haired boy with crimson eyes asks his teacher
|
| 12 |
+
*"What is the meaning of life without freedom?"* and is told, *"True freedom is not to do whatever you want;
|
| 13 |
+
it is to be able to refuse what you do not want to do."*
|
| 14 |
+
- **Ilumia, The Seer** — Goddess of Light in **Veda**, succeeding **Edras**, the Holy Light Master. Also born
|
| 15 |
+
on Mt. Orphean. The Temple was founded on belief in her.
|
| 16 |
+
- **Antaris Battlefield**, its lanes named for the **Dark Slayer** and the **Abyssal Dragon**; **Horizon
|
| 17 |
+
Valley**, where the Dark Slayer holds the **Drake Mondester** sealed inside its body.
|
| 18 |
+
|
| 19 |
+
The spine is canon: the light and the dark came from the same mountain.
|
| 20 |
+
|
| 21 |
+
| Act | Shots | Files |
|
| 22 |
+
|---|---|---|
|
| 23 |
+
| I — The Peak | 3 | `p1-s1` .. `p1-s3` |
|
| 24 |
+
| II — The Schism | 3 | `p2-s1` .. `p2-s3` |
|
| 25 |
+
| III — Lokheim Marches | 4 | `p3-s1` .. `p3-s4` |
|
| 26 |
+
| IV — The Beasts | 2 | `p4-s1`, `p4-s2` |
|
| 27 |
+
| V — The Crystal | 3 | `p5-s1` .. `p5-s3` |
|
| 28 |
+
|
| 29 |
+
## Files
|
| 30 |
+
|
| 31 |
+
- `film.json` — the shot list this film was rendered from (full prompts, seeds, durations).
|
| 32 |
+
- `make_film.py` — the renderer. Chains each shot on the previous shot's last frame *within* an act, so motion
|
| 33 |
+
is continuous; dissolves shots together; writes a sidecar per shot.
|
| 34 |
+
- `notebook.py` — the marimo notebook that drives it.
|
| 35 |
+
|
| 36 |
+
## Rendering notes
|
| 37 |
+
|
| 38 |
+
Each shot is conditioned on the last frame of the shot before it, which keeps motion continuous inside an act
|
| 39 |
+
— but faces drift *between* acts. Locking a character's look needs one still per character used as the opening
|
| 40 |
+
condition of every shot they appear in.
|
| 41 |
+
|
| 42 |
+
## Licence
|
| 43 |
+
|
| 44 |
+
Volkath, Ilumia, Edras, Maloch and the rest are Tencent / TiMi Studio Group characters, and the LTX-2 weights
|
| 45 |
+
carry Lightricks' own licence. Personal use is one thing; publishing at reach is another.
|
logs/legend-of-valor-film.log
CHANGED
|
@@ -1,3 +1,11 @@
|
|
| 1 |
18:00:30 film 'legend-of-valor': 5 pieces, 15 shots, 108s total
|
| 2 |
18:00:30 skip p1s1 (already rendered)
|
| 3 |
18:00:32 p1s2: 32.0 GB free on the GPU
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
18:00:30 film 'legend-of-valor': 5 pieces, 15 shots, 108s total
|
| 2 |
18:00:30 skip p1s1 (already rendered)
|
| 3 |
18:00:32 p1s2: 32.0 GB free on the GPU
|
| 4 |
+
18:06:48 backed up p1-s2.mp4 -> dragonlimited/LTX2-marimo
|
| 5 |
+
18:06:49 backed up p1-s2.json -> dragonlimited/LTX2-marimo
|
| 6 |
+
18:06:49 rendered p1s2 (193 frames) in 379s total
|
| 7 |
+
18:06:51 p1s3: 32.0 GB free on the GPU
|
| 8 |
+
18:11:28 backed up p1-s3.mp4 -> dragonlimited/LTX2-marimo
|
| 9 |
+
18:11:29 backed up p1-s3.json -> dragonlimited/LTX2-marimo
|
| 10 |
+
18:11:29 rendered p1s3 (145 frames) in 659s total
|
| 11 |
+
18:11:31 p2s1: 32.0 GB free on the GPU
|
notebook.py
CHANGED
|
@@ -1194,6 +1194,106 @@ def film_render(film_spec, make_film, pipe, render_film):
|
|
| 1194 |
return
|
| 1195 |
|
| 1196 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1197 |
@app.cell
|
| 1198 |
def tips(mo):
|
| 1199 |
mo.md("""
|
|
|
|
| 1194 |
return
|
| 1195 |
|
| 1196 |
|
| 1197 |
+
@app.cell
|
| 1198 |
+
def refresh(mo):
|
| 1199 |
+
# A timer so the viewer below stays current while a film renders. It sits in its own cell because marimo
|
| 1200 |
+
# will not let a cell read a widget it created — the same reason the film button is split across two cells.
|
| 1201 |
+
refresh_timer = mo.ui.refresh(
|
| 1202 |
+
options=["10s", "30s", "1m", "5m"],
|
| 1203 |
+
default_interval="30s",
|
| 1204 |
+
label="Auto-refresh",
|
| 1205 |
+
)
|
| 1206 |
+
refresh_timer
|
| 1207 |
+
|
| 1208 |
+
return (refresh_timer,)
|
| 1209 |
+
|
| 1210 |
+
|
| 1211 |
+
@app.cell
|
| 1212 |
+
def watch(OUTPUT_DIR, film_spec, mo, refresh_timer):
|
| 1213 |
+
# The one place to watch everything this notebook generates.
|
| 1214 |
+
#
|
| 1215 |
+
# Section 4 shows the single-shot render and section 5 the *finished* films (`outputs/*.mp4`). The takes of a
|
| 1216 |
+
# film being rendered live in `outputs/film/<title>/`, and nothing surfaced those — so a render could run for
|
| 1217 |
+
# an hour while the notebook looked empty. This cell is that viewer.
|
| 1218 |
+
#
|
| 1219 |
+
# It reads `refresh_timer`, so it re-runs on the timer and new takes appear on their own.
|
| 1220 |
+
_refreshed_at = refresh_timer.value
|
| 1221 |
+
|
| 1222 |
+
_film_dir = OUTPUT_DIR / "film" / film_spec["title"] if film_spec else None
|
| 1223 |
+
_takes = sorted(_film_dir.glob("p*-s*.mp4")) if _film_dir and _film_dir.is_dir() else []
|
| 1224 |
+
_films = sorted(OUTPUT_DIR.glob("*.mp4"), key=lambda _p: _p.stat().st_mtime, reverse=True)
|
| 1225 |
+
|
| 1226 |
+
_planned = (
|
| 1227 |
+
[
|
| 1228 |
+
(f"p{_p}-s{_s}", _piece.get("name", f"piece-{_p}"), _shot["seconds"])
|
| 1229 |
+
for _p, _piece in enumerate(film_spec["pieces"], 1)
|
| 1230 |
+
for _s, _shot in enumerate(_piece["shots"], 1)
|
| 1231 |
+
]
|
| 1232 |
+
if film_spec
|
| 1233 |
+
else []
|
| 1234 |
+
)
|
| 1235 |
+
_done = {_take.stem for _take in _takes}
|
| 1236 |
+
|
| 1237 |
+
|
| 1238 |
+
def _player(_path, _caption):
|
| 1239 |
+
"""A generated video with its own download button."""
|
| 1240 |
+
return mo.vstack(
|
| 1241 |
+
[
|
| 1242 |
+
mo.video(_path, width=200, muted=True),
|
| 1243 |
+
mo.md(_caption),
|
| 1244 |
+
mo.download(
|
| 1245 |
+
lambda _p=_path: _p.read_bytes(),
|
| 1246 |
+
filename=_path.name,
|
| 1247 |
+
mimetype="video/mp4",
|
| 1248 |
+
label="⬇ .mp4",
|
| 1249 |
+
),
|
| 1250 |
+
]
|
| 1251 |
+
)
|
| 1252 |
+
|
| 1253 |
+
|
| 1254 |
+
_blocks = [mo.md(f"## Watch — refreshed {_refreshed_at}")]
|
| 1255 |
+
|
| 1256 |
+
if _films:
|
| 1257 |
+
_blocks.append(mo.md(f"### Finished films — {len(_films)} in `outputs/`"))
|
| 1258 |
+
_blocks.append(
|
| 1259 |
+
mo.hstack(
|
| 1260 |
+
[_player(_f, f"**`{_f.name}`** — {_f.stat().st_size / 1e6:.1f} MB") for _f in _films[:6]],
|
| 1261 |
+
wrap=True,
|
| 1262 |
+
align="start",
|
| 1263 |
+
)
|
| 1264 |
+
)
|
| 1265 |
+
|
| 1266 |
+
if not film_spec:
|
| 1267 |
+
_blocks.append(mo.md("_No `film.json`, so no film is planned._"))
|
| 1268 |
+
else:
|
| 1269 |
+
_blocks.append(mo.md(f"### Film takes — **{len(_done)} of {len(_planned)}** rendered"))
|
| 1270 |
+
_blocks.append(
|
| 1271 |
+
mo.hstack(
|
| 1272 |
+
[
|
| 1273 |
+
_player(_take, f"**`{_take.stem}`** — {_seconds:g}s · {_take.stat().st_size / 1e6:.2f} MB")
|
| 1274 |
+
for _take in _takes
|
| 1275 |
+
for _name, _act, _seconds in _planned
|
| 1276 |
+
if _name == _take.stem
|
| 1277 |
+
],
|
| 1278 |
+
wrap=True,
|
| 1279 |
+
align="start",
|
| 1280 |
+
)
|
| 1281 |
+
if _takes
|
| 1282 |
+
else mo.md("_Nothing rendered yet._")
|
| 1283 |
+
)
|
| 1284 |
+
_pending = [f"`{_n}` — {_a} ({_s:g}s)" for _n, _a, _s in _planned if _n not in _done]
|
| 1285 |
+
_blocks.append(
|
| 1286 |
+
mo.accordion({"Still to render": mo.md("\n\n".join(f"- {_item}" for _item in _pending))})
|
| 1287 |
+
if _pending
|
| 1288 |
+
else mo.md("_**All shots rendered.** The joined film lands in `outputs/` once assembly runs._")
|
| 1289 |
+
)
|
| 1290 |
+
|
| 1291 |
+
video_viewer = mo.vstack(_blocks)
|
| 1292 |
+
video_viewer
|
| 1293 |
+
|
| 1294 |
+
return
|
| 1295 |
+
|
| 1296 |
+
|
| 1297 |
@app.cell
|
| 1298 |
def tips(mo):
|
| 1299 |
mo.md("""
|