Instructions to use teawhite/ActionRoPE with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Wan2.2
How to use teawhite/ActionRoPE 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
Download code/actionrope/SPEC.md from teawhite/ActionRoPE: direct link, hf CLI and curl.
- Browser
- Download file 14.1 kB
-
https://huggingface.co/teawhite/ActionRoPE/resolve/main/code/actionrope/SPEC.md
- Command line
-
hf download hf://teawhite/ActionRoPE/code/actionrope/SPEC.md
-
curl -L -o SPEC.md https://huggingface.co/teawhite/ActionRoPE/resolve/main/code/actionrope/SPEC.md
ActionRoPE 实现契约(所有模块必须遵守)
复现目标(来自旧机器的两份报告 恶意不息世界画布 / 世界RoPE·四科判决):
在 Wan2.2-TI2V-5B 上做动作条件世界模型,动作不走文本、走 RoPE:
- 世界坐标 RoPE(ARoPE,零新增参数):每帧 token 的 (h, w) 位置编码 = 窗口内格坐标 + 该帧相机累计偏移(换算成 token 单位)。同一世界位置 ⇒ 同一编码。
- known / new 支持掩码:首帧足迹是全局唯一"真"区域;某帧某 token 的世界位置落在
首帧足迹外 ⇒
new。new 区 loss ×2(new_weight),并可选把 mask 作为一个额外输入通道 经 零初始化 conv 加到 patch embedding 上(mask_channel,默认开;关掉即严格零参数版)。 - 文本只承载场景(动作从句已剥离,
data/latent/text_table_actionrope.pt); 转场语料的 trigger 从句按 cell 变化 ⇒ 逐 cell 文本条件(每帧 token 只 cross-attend 自己 cell 的文本;所有 cell 文本相同时与原模型逐位等价)。 - 对照臂
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 生成)
{
"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 与推理共用,避免两处各写一套)
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)。
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 分配以任务指令为准,避免互相抢卡。