File size: 5,163 Bytes
880dff9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
#!/usr/bin/env python3
"""建 ActionRoPE 的 UMT5 文本表(动作从句已剥离)。

数据集自带的 text_table_eybx.pt 是 1,253 键,每一条都含 "the player is {动作}"
—— 那是动作走文本的老方案。ActionRoPE 的动作走 RoPE,文本只留场景,剥完
只剩 360 条不同的串(18 纯场景 + 342 转场),所以要重建一张表。

编码走 DiffSynth 自己的 tokenizer + WanTextEncoder,与训练/推理时
WanVideoUnit_PromptEmbedder 的路径逐字一致:
  · HuggingfaceTokenizer(seq_len=512, clean='whitespace')
  · prompt_emb[:, seq_len:] = 0   (尾部 pad 清零)
产物与老表同构:dict[str, bf16[512, 4096]],训练时直接查表,不必挂 11 GB 的 UMT5。

    python -m actionrope.build_text_table --out data/latent/text_table_actionrope.pt
"""
from __future__ import annotations

import argparse
import glob
import json
import os
import sys
import time

import torch

sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from actionrope.prompts import NULL_PROMPT, strip_action  # noqa: E402

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DEFAULT_CKPT = os.path.join(ROOT, "models", "Wan2.2-TI2V-5B")


def collect_keys(meta_glob: str, null_prompt: str) -> list[str]:
    """从 meta/*.jsonl 收全部逐 cell prompt,剥掉动作从句后去重。

    走 meta 而不是 22,782 个 .pt:meta 只有 65 MB,.pt 要读 72 GB,
    两者的 prompts 字段是同一份(encode_latents.py 直接从 meta 抄进 sidecar)。
    """
    keys, n_rows, n_cells = set(), 0, 0
    files = sorted(glob.glob(meta_glob))
    if not files:
        raise FileNotFoundError(f"没有 meta 文件: {meta_glob}")
    for f in files:
        with open(f, encoding="utf-8") as fh:
            for line in fh:
                r = json.loads(line)
                n_rows += 1
                for p in r["prompts"]:
                    n_cells += 1
                    keys.add(strip_action(p))
    print(f"扫过 {n_rows:,} 个 clip / {n_cells:,} 个 cell -> {len(keys):,} 个不同串")
    # 场景 dropout / CFG 用的空条件串。剥离后 prompts_bossdrop 的 21 条会全部
    # 塌成同一个「没有场景」的串,这里显式加进表里,训练侧才有得查。
    if null_prompt not in keys:
        keys.add(null_prompt)
        print(f"追加空条件串: {null_prompt!r}")
    return sorted(keys)


def main():
    ap = argparse.ArgumentParser(description="ActionRoPE UMT5 文本表")
    ap.add_argument("--meta", default=os.path.join(ROOT, "data/meta/*.jsonl"))
    ap.add_argument("--out", default=os.path.join(ROOT, "data/latent/text_table_actionrope.pt"))
    ap.add_argument("--ckpt", default=DEFAULT_CKPT, help="Wan2.2-TI2V-5B 本地目录")
    ap.add_argument("--null_prompt", default=NULL_PROMPT,
                    help="场景 dropout / CFG 的空条件串")
    ap.add_argument("--batch", type=int, default=32)
    ap.add_argument("--device", default="cuda")
    ap.add_argument("--dry_run", action="store_true", help="只收串、不加载 UMT5")
    args = ap.parse_args()

    keys = collect_keys(args.meta, args.null_prompt)
    plain = [k for k in keys if "burning away into" not in k and "cutting to" not in k]
    print(f"  纯场景 {len(plain)} 条 / 转场 {len(keys) - len(plain)} 条")
    if args.dry_run:
        for k in plain:
            print("   ", k)
        return

    from diffsynth.pipelines.wan_video import WanVideoPipeline, ModelConfig

    # 只加载 text_encoder:DiT 和 VAE 这一步用不上,省 15 GB 显存。
    # redirect_common_files=False —— 本地已有 .pth,不要去 ModelScope 拉 safetensors。
    pipe = WanVideoPipeline.from_pretrained(
        torch_dtype=torch.bfloat16,
        device=args.device,
        model_configs=[ModelConfig(path=os.path.join(args.ckpt, "models_t5_umt5-xxl-enc-bf16.pth"))],
        tokenizer_config=ModelConfig(path=os.path.join(args.ckpt, "google/umt5-xxl")),
        redirect_common_files=False,
    )
    pipe.load_models_to_device(["text_encoder"])

    table, t0 = {}, time.time()
    for i in range(0, len(keys), args.batch):
        chunk = keys[i:i + args.batch]
        ids, mask = pipe.tokenizer(chunk, return_mask=True, add_special_tokens=True)
        ids, mask = ids.to(args.device), mask.to(args.device)
        seq_lens = mask.gt(0).sum(dim=1).long()
        with torch.no_grad():
            emb = pipe.text_encoder(ids, mask)
        # 与 WanVideoUnit_PromptEmbedder 一致:有效长度之后一律清零
        for j, v in enumerate(seq_lens):
            emb[j, v:] = 0
        for k, e, v in zip(chunk, emb, seq_lens):
            table[k] = e.to(torch.bfloat16).cpu().clone()
        print(f"  {min(i + args.batch, len(keys)):4d}/{len(keys)} · {time.time() - t0:.0f}s", flush=True)

    shape = next(iter(table.values())).shape
    os.makedirs(os.path.dirname(args.out), exist_ok=True)
    torch.save(table, args.out)
    print(f"DONE  {len(table):,} 键 · 每键 {tuple(shape)} bf16 · "
          f"{os.path.getsize(args.out) / 1e6:.0f} MB -> {args.out}")


if __name__ == "__main__":
    main()