File size: 10,604 Bytes
1811aab
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
"""Stream open-world examples -- the released ones (code-world-model release/examples/example_XX: Picture 1, a
Depth + Semantic-ID proxy, per-window captions) or open-code-world-model takes (examples/gallery.py: a released
first frame, a proxy compiled by the same renderer and rig as our CARLA proxies). There is no ground truth: a
qualitative test outside CARLA.

    python -m rtcwm.eval.open_world prepare --examples DIR/example_01 [...] [--chunks 24]    # one GPU
    python -m rtcwm.eval.open_world generate --lora LORA --out DIR --examples example_01 [...] [--chunks 24]
    python -m rtcwm.eval.open_world release --out DIR --examples example_01 [...] [--chunks 24]

prepare writes each example in the training-cache layout (train_cache/ow_example_XX/), so
rtcwm.train.dataset.load_window streams it like a CARLA clip:
    stream.safetensors {"duv"}   proxy latents of the first --chunks 17-frame clips
    anchorn_c00 / staticn_c00    Picture 1 at 1344x768 (the release's first target frame) and the Qwen states
                                 of the static caption
The static caption is the example's first-window "Appearance and scene" section -- the starting state --
without its action timeline (real time: the text tells nothing of the future).
generate writes <example>_latents.pt and <example>_stream.mp4: generated | proxy (Semantic-ID colours), full
resolution.
release writes DIR/<example>/config.json: the example's own release configuration cut to the windows that
cover --chunks chunks, for the released (bidirectional, windowed) CWM sampler -- python -m
cwm_h3_inference prepare / generate --config ...; it keeps the full per-window captions, timeline included.
"""
from __future__ import annotations

import argparse
import json
from pathlib import Path

import numpy as np
import torch

from cwm_h3_inference.constants import TARGET_HEIGHT, TARGET_WIDTH

from .. import paths
from ..data.anchors import cache_paths
from ..model.layout import CLIP_FRAMES

TIMELINE = "Protagonist action timeline:"
PALETTE = np.array([[0, 0, 0], [135, 206, 235], [30, 90, 200], [150, 120, 80], [90, 90, 90], [60, 160, 60],
                    [200, 120, 80], [220, 200, 60], [230, 40, 40], [200, 80, 200], [40, 220, 220], [250, 250, 250]],
                   dtype=np.uint8)                                 # Semantic-ID classes 0..11 (INFERENCE.md)


def cache_name(example: str) -> str:
    return "ow_" + Path(example).name


def static_caption(prompt: str) -> str:
    """A window prompt cut to its "Appearance and scene:" section -- the starting state, no timeline (and no
    instruction preamble, which open-code-world-model's captions carry)."""
    head = "Appearance and scene:"
    return (head + prompt.split(head, 1)[1] if head in prompt else prompt).split(TIMELINE)[0].strip()


def example_inputs(ex: Path):
    """(Picture 1, condition directory, first-window prompt) of a released example (config.json) or of an
    open-code-world-model take (first_frame.png, conditions/, prompts/000.txt)."""
    if (ex / "config.json").exists():
        cfg = json.loads((ex / "config.json").read_text())
        return ex / cfg["anchor_image"], ex / cfg["condition_root"], cfg["windows"][0]["prompt"]
    return ex / "first_frame.png", ex / "conditions", (ex / "prompts" / "000.txt").read_text()


def prepare(examples: list[Path], chunks: int):
    from PIL import Image
    from musubi_tuner.minimax_h3.text_encoder import load_h3_processor, load_h3_text_encoder
    from musubi_tuner.minimax_h3.video_vae import encode_video_condition, load_video_vae
    from musubi_tuner.minimax_h3_generate_video import _prepare_pixels
    from cwm_h3_inference.cache import _load_anchor
    from cwm_h3_inference.config import _canonical_caption
    from cwm_h3_inference.duv import load_duv_frame
    from ..train.preencode import atomic_save, static_text_states
    dev = torch.device("cuda:0")
    vae = load_video_vae(paths.VIDEO_VAE, device=dev, dtype=torch.float16, disable_mmap=False).eval().requires_grad_(False)
    proc = load_h3_processor(str(paths.PROCESSOR))
    qwen = load_h3_text_encoder(str(paths.TEXT_ENCODER), processor_path=str(paths.PROCESSOR), device=dev,
                                dtype=torch.bfloat16).eval()
    for ex in examples:
        anchor, cond, prompt = example_inputs(ex)
        name = cache_name(ex)
        (paths.CACHE / name).mkdir(parents=True, exist_ok=True)
        k = min(chunks, len(list(cond.glob("*.semantic_id.png"))) // CLIP_FRAMES)
        duv = np.stack([load_duv_frame(cond, i, for_qwen=False) for i in range(k * CLIP_FRAMES)])
        duv = np.concatenate([duv, np.repeat(duv[-1:], CLIP_FRAMES, 0)], 0)     # the encoder drops 3 latents
        with torch.inference_mode():
            ref = encode_video_condition(vae, _prepare_pixels(torch.from_numpy(duv)).to(dev, torch.float16))[0]
        atomic_save(paths.CACHE / name / "stream.safetensors", {"duv": ref.float().cpu()[:, :5 * k].contiguous()},
                    {"clip_frames": str(CLIP_FRAMES), "vae_clips": str(k), "source": str(ex)})
        cp = cache_paths(name, 0, "native")
        picture1 = _load_anchor(anchor, size=(TARGET_WIDTH, TARGET_HEIGHT))
        Image.fromarray(picture1).save(cp["png"])
        with torch.inference_mode():
            lat = encode_video_condition(vae, _prepare_pixels(torch.from_numpy(picture1).unsqueeze(0)).to(dev, torch.float16))
        atomic_save(cp["latent"], {"latents_ref_000_image": lat[0].float().cpu()})
        caption = _canonical_caption(static_caption(prompt), f"{name}/static_c00")
        hidden, tags = static_text_states(proc, qwen, picture1, cp["png"], cond, 0, caption)
        atomic_save(cp["text"], {"hidden": hidden.to("cpu", torch.bfloat16), "tags": tags.to("cpu", torch.int64)},
                    {"system_prompt": "w0", "duv": "start frame repeated", "caption": "first window, no timeline"})
        print(f"{name}: {k} chunks, caption {len(caption.split())} words, Qwen rows {hidden.shape[0]}", flush=True)


def proxy_frames(example: Path, n: int) -> np.ndarray:
    from PIL import Image
    cond = example_inputs(example)[1]
    sem = [np.asarray(Image.open(cond / f"{i:06d}.semantic_id.png")) for i in range(n)]
    out = PALETTE[np.clip(np.stack(sem), 0, 11)]
    return np.stack([np.asarray(Image.fromarray(f).resize((TARGET_WIDTH, TARGET_HEIGHT), Image.NEAREST)) for f in out])


def generate(a):
    from cwm_h3_inference.constants import VIDEO_SHIFT
    from ..model.loading import load_for_inference
    from ..model.sampler import generate_stream
    from ..train.dataset import load_window, silence, stream_chunks
    from .video import decode, load_vae, write_mp4
    dev = torch.device("cuda", 0)
    a.out.mkdir(parents=True, exist_ok=True)
    from .evaluate import check_span
    check_span(a.lora)
    from ..model import sampler
    sampler.HISTORY_FROM_LAST_STEP = a.history_from_last_step
    model = load_for_inference(a.lora, dev, fp8=a.fp8)
    audio, results = silence(), {}
    for ex in a.examples:
        name = cache_name(ex)
        n = min(a.chunks, stream_chunks(name))
        lat = generate_stream(model, lambda j: load_window(name, j, audio, start=0, with_target=False), n, a.steps,
                              torch.Generator().manual_seed(a.seed), dev, a.rule, a.shift if a.shift else VIDEO_SHIFT,
                              history_noise=a.history_noise, kv_reuse=a.kv_reuse, on_chunk=lambda k, s: print(f"{name} chunk {k + 1}/{n} {s:.1f}s", flush=True))
        results[ex] = lat.cpu()
    del model
    torch.cuda.empty_cache()
    vae = load_vae(dev)
    for ex, lat in results.items():
        torch.save(lat, a.out / f"{ex}_latents.pt")
        gen = decode(vae, lat, dev)
        write_mp4(a.out / f"{ex}_stream.mp4", np.concatenate([gen, proxy_frames(Path(a.root) / ex, len(gen))], 2), crf=20)
        print("wrote", a.out / f"{ex}_stream.mp4", flush=True)


def release_configs(a):
    from cwm_h3_inference.constants import STRIDE_FRAMES, WINDOW_FRAMES
    windows = 1 + max(0, -(-(a.chunks * CLIP_FRAMES - WINDOW_FRAMES) // STRIDE_FRAMES))
    for e in a.examples:
        ex = Path(a.root) / e
        cfg = json.loads((ex / "config.json").read_text())
        d = (a.out / e).resolve()
        d.mkdir(parents=True, exist_ok=True)
        absolute = lambda v: str((ex / v).resolve())
        cfg.update(anchor_image=absolute(cfg["anchor_image"]), condition_root=absolute(cfg["condition_root"]),
                   models={k: absolute(v) for k, v in cfg["models"].items()}, windows=cfg["windows"][:windows],
                   cache_dir=str(d / "cache"), output=str(d / "output.mp4"), blocks_to_swap=16)
        (d / "config.json").write_text(json.dumps(cfg, indent=1))
        print(f"{e}: {windows} windows, {WINDOW_FRAMES + STRIDE_FRAMES * (windows - 1)} frames -> {d / 'config.json'}")


def main():
    ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    ap.add_argument("stage", choices=["prepare", "generate", "release"])
    ap.add_argument("--examples", nargs="+", required=True, help="prepare: example directories; generate: their names")
    ap.add_argument("--root", default=str(Path.home() / "projects/code-world-model/release/examples"))
    ap.add_argument("--chunks", type=int, default=24)
    ap.add_argument("--lora", type=Path)
    ap.add_argument("--out", type=Path)
    ap.add_argument("--steps", type=int, default=35)
    ap.add_argument("--seed", type=int, default=0)
    ap.add_argument("--fp8", action="store_true", help="the blocks' large linears in FP8 (Hopper)")
    ap.add_argument("--rule", choices=["euler", "x0"], default="euler", help="x0: few-step (distilled) sampling")
    ap.add_argument("--shift", type=float, default=0.0, help="sampling flow shift (0: the released 12)")
    ap.add_argument("--kv-reuse", action="store_true", help="carry the history's keys/values across chunks")
    ap.add_argument("--history-from-last-step", action=argparse.BooleanOptionalAction, default=True,
                    help="kv-reuse: cache each chunk from its last denoising step (one forward fewer per chunk; "
                         "--no-history-from-last-step: one more clean forward)")
    ap.add_argument("--history-noise", type=float, default=0.0, help="noise level of the history chunks")
    a = ap.parse_args()
    if a.stage == "prepare":
        prepare([Path(e) if Path(e).is_dir() else Path(a.root) / e for e in a.examples], a.chunks)
    elif a.stage == "release":
        release_configs(a)
    else:
        generate(a)


if __name__ == "__main__":
    main()