Instructions to use teawhite/ActionRoPE-internal with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Wan2.2
How to use teawhite/ActionRoPE-internal with Wan2.2:
# No code snippets available yet for this library. # To use this model, check the repository files and the library's documentation. # Want to help? PRs adding snippets are welcome at: # https://github.com/huggingface/huggingface.js
- Notebooks
- Google Colab
- Kaggle
File size: 14,100 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 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 | # ActionRoPE 实现契约(所有模块必须遵守)
复现目标(来自旧机器的两份报告 `恶意不息世界画布` / `世界RoPE·四科判决`):
在 **Wan2.2-TI2V-5B** 上做动作条件世界模型,**动作不走文本、走 RoPE**:
1. **世界坐标 RoPE(ARoPE,零新增参数)**:每帧 token 的 (h, w) 位置编码 =
窗口内格坐标 + 该帧相机累计偏移(换算成 token 单位)。同一世界位置 ⇒ 同一编码。
2. **known / new 支持掩码**:首帧足迹是全局唯一"真"区域;某帧某 token 的世界位置落在
首帧足迹外 ⇒ `new`。new 区 loss ×2(`new_weight`),并可选把 mask 作为一个额外输入通道
经 **零初始化 conv** 加到 patch embedding 上(`mask_channel`,默认开;关掉即严格零参数版)。
3. 文本只承载场景(动作从句已剥离,`data/latent/text_table_actionrope.pt`);
转场语料的 trigger 从句按 cell 变化 ⇒ **逐 cell 文本条件**(每帧 token 只 cross-attend
自己 cell 的文本;所有 cell 文本相同时与原模型逐位等价)。
4. 对照臂 `plain`:普通 RoPE + 文本带动作,即报告里的 "Wan + Prompt"。**数据集的动作词是镜像的**
(日志→屏幕矩阵 M 符号翻转,2026-09-04 人工看视频确认:"moving left" 的 clip 玩家实际向右走),
plain 臂训练/推理默认用 `prompts.mirror_action` 把动作词翻回真实屏幕方向,文本表用补齐了镜像串的
`text_table_eybx_mirror.pt`(`actionrope/build_mirror_table.py` 生成,原 1,253 键 + 530 条镜像转场串)。
## 常量与几何
| 量 | 值 |
|---|---|
| 画幅 | 832×480(W×H),16 fps,81 帧 |
| latent | `[48, 21, 30, 52]`(C, F, H_lat, W_lat),latent 像素 = 16 px |
| token | patch (1,2,2) ⇒ 每帧 15×26 token,token = **32 px** |
| cell ↔ 帧 | cell 0 = 帧 0;cell k (k≥1) = 帧 4k−3 … 4k(含) |
| 屏幕坐标 | u 向右、v 向下,单位 px(832×480 画幅) |
### 相机 / 世界 → 屏幕
日志 `data/logs/<session>/state.npz` 是 20 Hz 玩家世界坐标 `(vt, x, y, z)`。
`offsets.json`:`video_t = log_vt + offset(log_vt)`(分段常数),`M_px1280` 是 3×2 矩阵,
行对应 (x, y, z),列对应 1280×720 画幅下的 (u, v):
```
p_px1280 = [x, y, z] @ M_px1280 # 玩家的"屏幕投影",含高度项
p_px832 = p_px1280 * [832/1280, 480/720]
```
相机刚性钉在玩家上(无平滑无滞后),所以**背景相对屏幕的位移 = −(玩家屏幕投影位移)**。
### 相机偏移(sidecar 的核心量)
对 clip 的第 i 帧(video_t_i = video_t0 + i/16,日志按 video_t 最近邻采样,与 `make_clips.py` 一致):
```
frame_offset_px[i] = p_px832(frame i) − p_px832(frame 0) # (dx, dy),frame 0 恒为 (0,0)
offset_px[k] = mean_{i ∈ cell k} frame_offset_px[i] # [21, 2],cell 0 = (0,0)
```
**世界 token 坐标**:cell k 中屏幕格 (i_h, j_w) 的 token 的世界坐标为
`(i_h + offset_px[k].dy / 32, j_w + offset_px[k].dx / 32)`(允许小数,RoPE 是连续的)。
理由:玩家向右走 D px ⇒ 背景整体左移 D px ⇒ 现在屏幕位置 s 上的内容,在首帧位于 s + D。
**known / new**(token 分辨率):token 的世界坐标中心落在首帧足迹
`[−0.5, 14.5] × [−0.5, 25.5]`(h × w)内 ⇒ known,否则 new。
latent 分辨率同理(30×52,偏移除以 16)。
**转场 clip**(`kind ∈ {burn, hardcut}`,帧 < n_pre 来自 `src_a` 的同下标帧,帧 ≥ n_pre 来自 `src_b`
的同下标帧,burn 段是两者混合):
```
i < n_pre : off[i] = offA[i]
i >= n_pre : off[i] = offA[n_pre−1] + (offB[i] − offB[n_pre−1]) # 累计位移保持连续
```
转场 clip 的 known / new 掩码**只按上面的几何定义**:B 段(帧 ≥ n_pre)的内容来自另一个场景,
首帧看不到,但其 token 若几何上落在首帧足迹内仍标 known(loss 权重 1、mask 通道 = 1)。
这是有意的简化(模型靠逐 cell 文本知道换了场景),不是缺陷;要按语义把 B 段整体标 new 需改此处。
### 增益校验(报告里的教训)
旧报告说 manifest 常数被逐帧小位移法的亚像素零偏压低 ~11%,真值应为横 27.5 / 纵 18.7 px/米;
本机 `offsets.json` 的 M 折算后为横 26.6 / 纵 16.9。**不要照抄任何一边**:sidecar 必须附带
对视频的实测校验(大基线相位相关/模板匹配,抽样 ≥200 个高速直行 clip),报告拟合增益比
`gain_fit = (实测位移) / (日志预测位移)` 及 R²。若两 session 一致且 R² ≥ 0.9,则把
`gain_correction`(分 x/y)写进 sidecar meta 并在生成 `offset_px` 时**已乘上**;否则保持 1.0 并在 meta 里说明。
(2026-09-04 人定例外:纵向 R² 0.92/0.84 未达门槛但 step-5000 推理证实 1.0 欠标定 27%,改用实测 1.12。)
## 文件契约
### `data/latent/arope_sidecar.pt`(由 `actionrope/sidecar.py` 生成)
```python
{
"meta": {
"version": "arope_sidecar_v1",
"M_px832": [[..],[..],[..]], # 3x2,已换算到 832x480
"gain_correction": [gx, gy], # 已应用到 offset_px 的修正倍率,默认 [1.0, 1.0]
"gain_fit": {...}, # 校验统计:每 session 的拟合斜率 / R² / 样本数
"walk_speed_px_s": [sx, sy], # 直行 clip 里的中位步行速度(|dx|/s, |dy|/s),供推理造指令用
"n_clips": int, "n_valid": int, "notes": str,
},
"clips": {
"<clip_id>": {
"offset_px": torch.float32[21, 2], # (dx, dy),cell 0 = 0
"frame_offset_px": torch.float32[81, 2],
"valid": bool, # 日志缺口 / 采样异常 ⇒ False(训练时剔除)
"kind": "plain" | "burn" | "hardcut",
}, ...
}
}
```
覆盖 `data/latent/pool/` 全部 22,782 个 clip。
### 文本表
`data/latent/text_table_actionrope.pt`:`dict[str, bf16[512, 4096]]`,键 = `strip_action(prompt)`
(`actionrope/prompts.py`)。含空条件串 `NULL_PROMPT`。`plain` 臂用 `text_table_eybx_mirror.pt`
(键 = `mirror_action(原 prompt)`,是 `text_table_eybx.pt` 的超集);`--no-mirror_labels` 旧约定才用原表、原串。
### 几何工具 `actionrope/geometry.py`(sidecar 与推理共用,避免两处各写一套)
```python
FRAMES, CELLS, W, H = 81, 21, 832, 480
LAT_H, LAT_W, TOK_H, TOK_W = 30, 52, 15, 26
PX_PER_TOKEN, PX_PER_LATENT = 32, 16
def cell_frames(k) -> list[int]
def frames_to_cells(frame_offset_px: np.ndarray[81,2]) -> np.ndarray[21,2]
def load_session_tracks(logs_dir, session) -> dict # 排序去重后的 vt/x/y/z + offset 分段 + M_px832
def project_px832(tracks, x, y, z) -> (u, v)
def sample_frame_offsets(tracks, video_t0, n_frames=81) -> np.ndarray[81,2] # 含 valid 判定
def known_mask_tokens(offset_px[F,2]) -> np.bool_[F, 15, 26]
def known_mask_latent(offset_px[F,2]) -> np.bool_[F, 30, 52]
```
## 模型契约 `actionrope/arope.py`
基于 `diffsynth.models.wan_video_dit.WanModel`(TI2V-5B 配置:dim 3072, 24 头, head_dim 128,
30 层, patch (1,2,2), in/out 48, `seperated_timestep=True`, `fuse_vae_embedding_in_latents=True`)。
RoPE 分量:`precompute_freqs_cis_3d(128)` ⇒ f 轴 44 维、h 轴 42 维、w 轴 42 维(`dit.freqs` 实际是 complex128,theta 1e4)。
```python
def build_world_freqs(dit, f, h, w, offset_tok=None, device=None) -> torch.complex128
# offset_tok: [b, f, 2] (dx_tok, dy_tok) 或 None(None ⇒ 与 dit.freqs 查表结果逐位相同)
# 返回 [b, f*h*w, 1, 64](offset_tok=None 时可返回 [f*h*w, 1, 64])
# 位置 = (frame_idx, i_h + dy_tok[f], j_w + dx_tok[f]),h/w 轴用 polar(1, pos * inv_freq) 现算
def known_mask(offset_tok, h, w) -> torch.bool [b, f, h, w]
def loss_weight_map(offset_px, lat_h=30, lat_w=52, new_weight=2.0) -> torch.float32 [b, 1, f, lat_h, lat_w]
def install_arope(dit, mask_channel: bool = True):
# mask_channel ⇒ 新增 dit.arope_mask_embedding = nn.Conv3d(1, dim, patch, stride=patch),零初始化
# 不改动任何已有参数名;plain / 关闭时 ckpt 键集与原模型完全一致
def arope_forward(dit, latents, timestep, context, *,
offset_px=None, # [b, f, 2],None ⇒ 普通 RoPE
first_frame_cond=True, # 帧 0 为干净条件帧:timestep 置 0(seperated_timestep 路径)
mask_input=None, # [b, 1, f, 30, 52] float,known=1 / new=0;None ⇒ 不加
use_gradient_checkpointing=False,
use_gradient_checkpointing_offload=False) -> torch.Tensor [b, 48, f, 30, 52]
# context: [b, 512, 4096](整条一致)或 [b, f, 512, 4096](逐 cell)。逐 cell 时 cross-attn 按帧分组:
# q: (b f) (h w) c,k/v: (b f) L c。所有 cell 相同 ⇒ 结果与整条模式逐位等价(bf16 容差)。
# text_embedding 只对去重后的串算一次。
# 等价性要求:offset_px=None, mask_input=None, context 整条 ⇒ 与 diffsynth.pipelines.wan_video.model_fn_wan_video(
# dit=dit, latents=..., timestep=..., context=..., fuse_vae_embedding_in_latents=True) 输出一致(bf16 容差 ~1e-2 相对)。
```
时间步处理照抄 `model_fn_wan_video` 的 `seperated_timestep and fuse_vae_embedding_in_latents` 分支
(帧 0 的 t=0,其余帧 t=timestep,t_mod 逐 token)。block 调用照抄 `WanModel.forward`(含梯度检查点)。
## 训练契约 `actionrope/train.py`
* 只加载 DiT(`ModelConfig(path=[三个 safetensors 的绝对路径])`,`redirect_common_files=False`),
bf16;不挂 T5/VAE。latent 与文本都是预计算的。
* 数据:`data/latent/train_eybx/*.pt`(软链)+ sidecar + 文本表;`valid=False` 的 clip 剔除;
**转场 clip 的 `src_a` / `src_b` 若落在 val 目录也剔除**(val 的帧会原样出现在转场 clip 里,划分脚本没按 src 归属划);
sidecar 的 valid 规则:最近邻日志样本时间差 ≤ **0.5 s**(原 0.15 s;2026-09-04 视频实测 0.15–0.5 s 缺口的 clip
偏移精度与 valid 基线一致,残差 5–7 px,人定放宽)。世界降速另用 meta 的 `rt_ratio` 闸:
训练 `--min_rt_ratio 0.8`(默认),val / 评测 `--val_min_rt_ratio 0.9`(默认)。
样本 = `{input_latents [48,21,30,52] bf16, context [21,512,4096] bf16, offset_px [21,2], clip_id}`。
场景 dropout:以 `--scene_dropout p`(默认 0.1)把整条 21 个 cell 都换成 `NULL_PROMPT`
(plain 臂换成 .pt 自带的 `prompts_bossdrop` 裸动作串并同样镜像,`NULL_PROMPT` 不在 eybx 表里)。
随机数按 rank 错开(`set_seed(seed, device_specific=True)`):噪声 ε、dropout 抽签各卡独立,数据分片由 accelerate 同步。
* loss(与 DiffSynth `FlowMatchSFTLoss` 一致,加权重):
`timestep` 从 `FlowMatchScheduler("Wan").set_timesteps(1000, training=True)` 的 1000 个时间步里均匀抽;
`x_t = (1−σ) x0 + σ ε`,目标 `ε − x0`;帧 0 用干净 x0 替换(TI2V 首帧条件)并在 loss 中剔除;
`loss = Σ w·(pred−target)² / Σ w`(w 来自 `loss_weight_map`,帧 0 不计),再乘 `scheduler.training_weight(timestep)`。
* 分布式:`accelerate launch --config_file configs/accelerate_zero2.yaml`,DeepSpeed ZeRO-2,bf16,
每卡 batch 1,梯度检查点开,AdamW(lr 默认 1e-5, wd 0.01),线性 warmup 可选。
`--grad_clip` 写进 DeepSpeed 的 `gradient_clipping`(日志 `grad_norm` 是裁剪前的全局范数)。
`--save_state` 写 `state_A` / `state_B` 两槽轮转,`<output>/state` 软链指向最新完整槽。
* 参数:`--arm {arope,plain}`、`--mask_channel/--no_mask_channel`、`--new_weight`、`--text_table`、
`--scene_dropout`、`--lr`、`--max_steps`、`--save_every`、`--val_every --val_n`、`--output`、
`--resume`、`--limit`(smoke 用)、`--seed`、`--num_workers`。
* 输出 `outputs/<run>/`:`step-N.safetensors`(只含 DiT 权重,键名与原模型一致 + 可选 `arope_mask_embedding.*`),
`train_log.csv` / tensorboard,`config.json`(含 arm、gain_correction 等复现信息)。
* plain 臂 = `offset_px=None` + 无 mask + 权重全 1 + 文本带动作从句(默认 `--mirror_labels`:动作词翻回真实方向)。
## 推理契约 `actionrope/infer.py`
* 输入:首帧(png / mp4 首帧 / `clip_id`,取 clip mp4 的第 0 帧)、场景从句、动作脚本、ckpt。
* 动作脚本 → `frame_offset_px[81,2]`(用 sidecar meta 的 `walk_speed_px_s` × 倍率 × 方向单位向量,逐帧累计),
再 `frames_to_cells` ⇒ `offset_px[21,2]`。`--replay <clip_id>` 直接用 sidecar 真值(可与 GT clip 逐帧比较 PSNR)。
* 采样:DiffSynth `FlowMatchScheduler("Wan")`,50 步,shift 5,帧 0 = VAE 编码的首帧 latent(fp32 编码后存 bf16,
与数据集 latent 同精度)并每步保持;CFG 可选(负条件:arope 臂 `NULL_PROMPT`;plain 臂逐 cell 裸动作串
`In a 2.5D top down view, the player is {action}.`,即该臂训练 dropout 用的串)。
VAE:`models/Wan2.2-TI2V-5B/Wan2.2_VAE.pth`(`WanVideoVAE38`)。
* 输出 mp4(832×480,16 fps)+ 一行文本(实测位移:对生成视频做相位相关得到的累计 dx/dy,与指令对比)。
## 环境与纪律
* 解释器:`/opt/dlami/nvme/zhiyangdeng/ActionRoPE/.venv/bin/python`(3.12,torch 2.11+cu128,8×H200)。
允许往 venv 里 `pip install` 缺的包(如 `opencv-python-headless`、`scipy`)。
* 所有路径用绝对路径;只在 `/opt/dlami/nvme/zhiyangdeng/ActionRoPE` 内读写;大文件(sidecar、ckpt、样例视频)
只能落在 `data/` / `outputs/`。临时文件放 scratchpad,不要写到 `/`、`/root`、`/tmp` 之外。
* **不要读取、修改任何 AWS 鉴权信息(ak/sk、`~/.aws`、环境变量凭据)**。
* 不要启动超过 smoke 规模的训练(≤ 数十步)。GPU 分配以任务指令为准,避免互相抢卡。
|