dragonlimited commited on
Commit
7e7101e
·
verified ·
1 Parent(s): 927d9fe

Legend of Valor: Act I rendered, notebook viewer + LTX-2 load fixes

Browse files
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("""