Spaces:
Running on Zero
Running on Zero
File size: 17,700 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 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 | """Pre-encode rendered clips into training windows, exactly as the released CWM inference builds them.
Per clip, in paths.CACHE/<clip>/:
anchor.safetensors latents_ref_000_image Picture 1 (frame 0, short edge 2048)
first_frame.safetensors first_frame [24,1,48,84] frame 0 at 1344x768 (window-0 keyframe)
wNN.visual.safetensors target [24,37,48,84] VAE latent of the window's 124 GT frames
duv [24,37,12,21] coarse-proxy reference latent
prefix [24,10,48,84] (NN>0) GT frames 0..33 re-encoded (Retake34)
wNN.text.safetensors hidden [L,5120] bf16, tags [L] Qwen conditioning (w0 / wn system prompt)
stream.safetensors target [24,5K,48,84] the whole clip as one latent stream: its first K
17-frame VAE clips (918 of 934 frames), 5 latents each
duv [24,5K,12,21] the coarse proxy, encoded the same way
anchor_cMM.png / .safetensors, static_cMM.text.safetensors
per start frame m (rtcwm.data.anchors): its Picture 1 latent and the Qwen states
of its static caption (that frame and its proxy only, w0 system prompt)
paths.CACHE/silence_audio.safetensors audio [32,2,207]
python -m rtcwm.train.preencode --stage visual|text|stream|static_text [--shard] [--reverse] [CLIP_DIR ...]
Without CLIP_DIRs every clip of the dataset with a complete prompt set is taken; --shard splits them
over SLURM tasks (task i takes clips i, i+n, ...); --reverse walks from the end (a helper job meeting
the main one in the middle). Existing files are skipped; writes are atomic per process.
"""
from __future__ import annotations
import argparse
import gc
import os
import time
from pathlib import Path
import av
import numpy as np
import torch
from safetensors.torch import save_file
from cwm_h3_inference.cache import _load_anchor
from cwm_h3_inference.config import _canonical_caption
from cwm_h3_inference.constants import (AUDIO_LATENT_SHAPE, FPS, OVERLAP_FRAMES, QWEN_OFFSETS, REFERENCE_LATENT_SHAPE,
STRIDE_FRAMES, TARGET_HEIGHT, TARGET_WIDTH, VIDEO_LATENT_SHAPE,
VIDEO_PREFIX_LATENTS, WINDOW_AUDIO_SAMPLES, WINDOW_FRAMES)
from cwm_h3_inference.duv import load_duv_window
from cwm_h3_inference.presentation import build_ref2va_presentation
from .. import paths
from ..data.clip import has_prompts, n_windows, qc_pass, read_meta
def windows_of(clip: Path) -> int:
return min(n_windows(read_meta(clip)["frames"]), int(os.environ.get("MAX_WINDOWS", 99)))
def read_frames(path: Path) -> np.ndarray:
with av.open(str(path)) as c:
return np.stack([f.to_ndarray(format="rgb24") for f in c.decode(video=0)])
def atomic_save(path: Path, tensors: dict, meta: dict | None = None):
tmp = path.with_name(f"{path.name}.{os.getpid()}.tmp") # unique: a helper job may write the same file
save_file({k: v.contiguous() for k, v in tensors.items()}, str(tmp), metadata=meta or {})
tmp.replace(path)
def stage_visual(clips):
from musubi_tuner.minimax_h3.audio_vae import load_audio_vae
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 PIL import Image
dev = torch.device("cuda:0")
vae = load_video_vae(paths.VIDEO_VAE, device=dev, dtype=torch.float16, disable_mmap=False).eval().requires_grad_(False)
sil = paths.CACHE / "silence_audio.safetensors"
if not sil.exists():
avae = load_audio_vae(paths.AUDIO_VAE, device=dev, dtype=torch.float32, disable_mmap=False).eval()
with torch.inference_mode():
a = avae.encode(torch.zeros(1, 2, WINDOW_AUDIO_SAMPLES, device=dev)).float().cpu()[0]
assert tuple(a.shape) == AUDIO_LATENT_SHAPE[1:], a.shape
paths.CACHE.mkdir(parents=True, exist_ok=True)
atomic_save(sil, {"audio": a})
del avae
def enc(frames_uint8: np.ndarray) -> torch.Tensor:
px = _prepare_pixels(torch.from_numpy(np.ascontiguousarray(frames_uint8))).to(dev, torch.float16)
with torch.inference_mode():
return vae.encode(px).float().cpu()[0]
for clip in clips:
out = paths.CACHE / clip.name
out.mkdir(parents=True, exist_ok=True)
W = windows_of(clip)
if all((out / f"w{w:02d}.visual.safetensors").exists() for w in range(W)) and (out / "anchor.safetensors").exists():
continue
frames = read_frames(clip / "rgb.mp4")
assert frames.shape[1:3] == (TARGET_HEIGHT, TARGET_WIDTH), frames.shape
anchor_png = out / "first_frame.png"
Image.fromarray(frames[0]).save(anchor_png)
if not (out / "anchor.safetensors").exists():
big = _load_anchor(anchor_png)
px = _prepare_pixels(torch.from_numpy(big).unsqueeze(0)).to(dev, torch.float16)
with torch.inference_mode():
lat = encode_video_condition(vae, px)[0].cpu().float()
atomic_save(out / "anchor.safetensors", {"latents_ref_000_image": lat})
ff = enc(frames[:1])
assert ff.shape[1] == 1
atomic_save(out / "first_frame.safetensors", {"first_frame": ff})
for w in range(W):
p = out / f"w{w:02d}.visual.safetensors"
if p.exists():
continue
s = STRIDE_FRAMES * w
target = enc(frames[s:s + WINDOW_FRAMES])
assert tuple(target.shape) == VIDEO_LATENT_SHAPE[1:], target.shape
duv = load_duv_window(clip / "conditions_coarse", s, for_qwen=False)
dpx = _prepare_pixels(torch.from_numpy(duv.copy())).to(dev, torch.float16)
with torch.inference_mode():
ref = encode_video_condition(vae, dpx)[0].cpu().float()
assert tuple(ref.shape) == REFERENCE_LATENT_SHAPE, ref.shape
t = {"target": target, "duv": ref}
if w > 0:
tail = frames[s:s + OVERLAP_FRAMES]
padded = np.concatenate([tail, np.repeat(tail[-1:], WINDOW_FRAMES - OVERLAP_FRAMES, 0)], 0)
t["prefix"] = enc(padded)[:, :VIDEO_PREFIX_LATENTS]
atomic_save(p, t)
gc.collect(); torch.cuda.empty_cache()
print("visual", clip.name, W, "windows", flush=True)
def stage_text(clips):
from musubi_tuner.minimax_h3.media import H3AudioSource, H3Record, H3Reference
from musubi_tuner.minimax_h3.text_encoder import (H3TextVisual, build_presentation, encode_h3_presentation,
load_h3_processor, load_h3_text_encoder)
dev = torch.device("cuda:0")
proc = load_h3_processor(str(paths.PROCESSOR))
model = load_h3_text_encoder(str(paths.TEXT_ENCODER), processor_path=str(paths.PROCESSOR), device=dev,
dtype=torch.bfloat16).eval()
for clip in clips:
out = paths.CACHE / clip.name
anchor_png = out / "first_frame.png"
anchor = torch.from_numpy(np.ascontiguousarray(_load_anchor(anchor_png))).unsqueeze(0)
cond = clip / "conditions_coarse"
for w in range(windows_of(clip)):
p = out / f"w{w:02d}.text.safetensors"
if p.exists():
continue
caption = _canonical_caption((clip / "prompts" / f"{w:03d}.txt").read_text(), f"{clip.name}/w{w}")
s = STRIDE_FRAMES * w
duv = torch.from_numpy(np.ascontiguousarray(load_duv_window(cond, s, for_qwen=True)))
image_key, video_key = anchor_png, cond / f"{s:06d}.semantic_id.png"
placeholder = cond / ".h3-presentation-placeholder"
record = H3Record(video_path=placeholder, caption=caption,
target_audio=H3AudioSource(path=placeholder, embedded=False),
references=(H3Reference(type="image", path=image_key),
H3Reference(type="video", path=video_key, duration_seconds=WINDOW_FRAMES / FPS)),
jsonl_line=0)
plain = build_presentation(record, "ref2va", {image_key: H3TextVisual(anchor),
video_key: H3TextVisual(duv, tuple(o / FPS for o in QWEN_OFFSETS))})
pres = build_ref2va_presentation(proc, plain, w, caption) # w0 / wn system prompt by index
with torch.inference_mode():
hidden, tags = encode_h3_presentation(proc, model, pres)
atomic_save(p, {"hidden": hidden.to("cpu", torch.bfloat16), "tags": tags.to("cpu", torch.int64)},
{"system_prompt": "w0" if w == 0 else "wn"})
gc.collect(); torch.cuda.empty_cache()
print("text", clip.name, flush=True)
CLIP_FRAMES = 17 # the video VAE encodes independent 17-frame clips into 5 latents each
def stage_stream(clips):
"""The whole clip as a continuous latent stream (streaming generation). The VAE encodes 17-frame clips
independently, so the stream is the concatenation of per-clip latents; one dummy clip is appended
because the encoder drops the last 3 latents of whatever it is given."""
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.duv import load_duv_frame
dev = torch.device("cuda:0")
vae = load_video_vae(paths.VIDEO_VAE, device=dev, dtype=torch.float16, disable_mmap=False).eval().requires_grad_(False)
def padded(frames: np.ndarray) -> torch.Tensor:
frames = np.concatenate([frames, np.repeat(frames[-1:], CLIP_FRAMES, 0)], 0)
return _prepare_pixels(torch.from_numpy(np.ascontiguousarray(frames))).to(dev, torch.float16)
for clip in clips:
p = paths.CACHE / clip.name / "stream.safetensors"
if p.exists():
continue
frames = read_frames(clip / "rgb.mp4")
k = len(frames) // CLIP_FRAMES
n = k * CLIP_FRAMES
duv = np.stack([load_duv_frame(clip / "conditions_coarse", i, for_qwen=False) for i in range(n)])
with torch.inference_mode():
target = vae.encode(padded(frames[:n])).float().cpu()[0, :, :5 * k]
ref = encode_video_condition(vae, padded(duv))[0].float().cpu()[:, :5 * k]
assert target.shape[1] == ref.shape[1] == 5 * k, (target.shape, ref.shape)
p.parent.mkdir(parents=True, exist_ok=True)
atomic_save(p, {"target": target, "duv": ref}, {"clip_frames": str(CLIP_FRAMES), "vae_clips": str(k)})
gc.collect(); torch.cuda.empty_cache()
print("stream", clip.name, k, "VAE clips", flush=True)
def stage_static_text(clips, follow_job: str = "", res: str = "native"):
"""Per start frame (rtcwm.data.anchors): the Picture 1 latent of that frame (short edge 2048, as the
released CWM encodes its anchor) and the Qwen states of its static caption. The text must not carry
anything from later frames: Qwen sees Picture 1 and, as the Ref2VA video reference, the proxy of the
start frame repeated (not a window's proxy video), with the first-window system prompt."""
from musubi_tuner.minimax_h3.text_encoder import load_h3_processor, load_h3_text_encoder
from musubi_tuner.minimax_h3.video_vae import load_video_vae
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))
model = load_h3_text_encoder(str(paths.TEXT_ENCODER), processor_path=str(paths.PROCESSOR), device=dev,
dtype=torch.bfloat16).eval()
while True:
running = bool(follow_job) and _job_running(follow_job) # captions still being written?
n = _static_text_pass(clips, vae, proc, model, dev, res)
if not running:
break
if not n:
time.sleep(60)
def _static_text_pass(clips, vae, proc, model, dev, res: str = "native") -> int:
from PIL import Image
from musubi_tuner.minimax_h3.video_vae import encode_video_condition
from musubi_tuner.minimax_h3_generate_video import _prepare_pixels
from ..data.anchors import anchor_frame, cache_paths, caption_path, clip_anchors
done = 0
for clip in clips:
todo = [m for m in clip_anchors(clip) if caption_path(clip, m).exists() and not cache_paths(clip.name, m, res)["text"].exists()]
if not todo:
continue
frames = read_frames(clip / "rgb.mp4")
(paths.CACHE / clip.name).mkdir(parents=True, exist_ok=True)
for m in todo:
cp = cache_paths(clip.name, m, res)
Image.fromarray(frames[anchor_frame(m)]).save(cp["png"])
big = _load_anchor(cp["png"]) if res == "2048" else np.ascontiguousarray(frames[anchor_frame(m)])
with torch.inference_mode():
lat = encode_video_condition(vae, _prepare_pixels(torch.from_numpy(big).unsqueeze(0)).to(dev, torch.float16))[0].cpu().float()
atomic_save(cp["latent"], {"latents_ref_000_image": lat})
caption = _canonical_caption(caption_path(clip, m).read_text(), f"{clip.name}/static_c{m:02d}")
hidden, tags = static_text_states(proc, model, big, cp["png"], clip / "conditions_coarse", anchor_frame(m), caption)
atomic_save(cp["text"], {"hidden": hidden.to("cpu", torch.bfloat16), "tags": tags.to("cpu", torch.int64)},
{"system_prompt": "w0", "duv": "start frame repeated", "start_chunk": str(m)})
gc.collect(); torch.cuda.empty_cache()
done += len(todo)
print("static_text", clip.name, len(todo), "start frames", flush=True)
return done
def static_text_states(proc, model, picture1: np.ndarray, picture1_png: Path, cond: Path, frame: int, caption: str):
"""Qwen states [L,5120] and token tags of a static caption: Qwen sees Picture 1 ([H,W,3] uint8, saved at
picture1_png) and, as the Ref2VA video reference, the proxy of `frame` repeated -- nothing of later
frames -- with the first-window system prompt."""
from musubi_tuner.minimax_h3.media import H3AudioSource, H3Record, H3Reference
from musubi_tuner.minimax_h3.text_encoder import H3TextVisual, build_presentation, encode_h3_presentation
from cwm_h3_inference.duv import load_duv_frame
anchor = torch.from_numpy(np.ascontiguousarray(picture1)).unsqueeze(0)
f0 = load_duv_frame(cond, frame, for_qwen=True)
duv = torch.from_numpy(np.ascontiguousarray(np.stack([f0] * len(QWEN_OFFSETS))))
video_key = cond / f"{frame:06d}.semantic_id.png"
placeholder = cond / ".h3-presentation-placeholder"
record = H3Record(video_path=placeholder, caption=caption, target_audio=H3AudioSource(path=placeholder, embedded=False),
references=(H3Reference(type="image", path=picture1_png),
H3Reference(type="video", path=video_key, duration_seconds=WINDOW_FRAMES / FPS)),
jsonl_line=0)
plain = build_presentation(record, "ref2va", {picture1_png: H3TextVisual(anchor),
video_key: H3TextVisual(duv, tuple(o / FPS for o in QWEN_OFFSETS))})
with torch.inference_mode():
return encode_h3_presentation(proc, model, build_ref2va_presentation(proc, plain, 0, caption))
def _job_running(job: str) -> bool:
import subprocess
return bool(subprocess.run(["squeue", "-h", "-j", job], capture_output=True, text=True).stdout.strip())
def text_done(clip: Path) -> bool:
return all((paths.CACHE / clip.name / f"w{w:02d}.text.safetensors").exists() for w in range(windows_of(clip)))
def select_clips(shard: bool, reverse: bool, only_incomplete: bool):
clips = [d for d in sorted(paths.DATASET.glob("clip_*")) if d.is_dir() and qc_pass(d) and has_prompts(d)]
if only_incomplete:
clips = [c for c in clips if not text_done(c)]
if reverse:
clips = clips[::-1]
if shard:
i, n = int(os.environ["SLURM_PROCID"]), int(os.environ["SLURM_NTASKS"])
clips = clips[i::n]
return clips
def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--stage", choices=["visual", "text", "both", "stream", "static_text"], required=True)
ap.add_argument("clips", nargs="*", type=Path)
ap.add_argument("--shard", action="store_true", help="take this SLURM task's share of the dataset")
ap.add_argument("--reverse", action="store_true")
ap.add_argument("--only-incomplete", action="store_true", help="skip clips whose text stage is done")
ap.add_argument("--follow-job", default="", help="static_text: keep encoding new captions while this SLURM job runs")
ap.add_argument("--picture1", choices=["native", "2048"], default="native", help="static_text: Picture 1 resolution")
a = ap.parse_args()
clips = [c.resolve() for c in a.clips] or select_clips(a.shard, a.reverse, a.only_incomplete)
print(f"{len(clips)} clips", flush=True)
if a.stage in ("visual", "both"):
stage_visual(clips)
if a.stage in ("text", "both"):
stage_text(clips)
if a.stage == "stream":
stage_stream(clips)
if a.stage == "static_text":
stage_static_text(clips, a.follow_job, a.picture1)
if __name__ == "__main__":
main()
|