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
File size: 10,200 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 | """几何工具:帧/cell 映射、日志 → 屏幕偏移采样、known/new 掩码。
sidecar 生成(actionrope/sidecar.py)和推理造指令(actionrope/infer.py)共用这一套,
避免两处各写一套时符号或分组规则悄悄不一致。
坐标约定(与 SPEC 一致):
屏幕 u 向右、v 向下,单位 px(832×480 画幅)
p_px1280 = [x, y, z] @ M_px1280 玩家的"屏幕投影",含高度项 y
p_px832 = p_px1280 * [832/1280, 480/720]
frame_offset_px[i] = p_px832(frame i) − p_px832(frame 0)
相机刚性钉在玩家上,所以背景相对屏幕的位移 = −frame_offset_px。这个符号由
actionrope/verify_sidecar.py 对视频实测,不是照抄日志。
"""
from __future__ import annotations
import json
import os
import numpy as np
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
FPS = 16.0
# 日志里的 M 是 1280×720 画幅下解出来的;clip 是直接 scale 到 832×480(不裁不补),
# 所以横纵各自按比例缩,横向 0.65、纵向 0.6667 —— 2.5% 的横向压缩要保留。
SCALE_1280_TO_832 = np.array([W / 1280.0, H / 720.0], np.float64)
# 采样有效性判定。日志 20 Hz,最近邻误差正常 ≤ 0.025 s;超过 0.15 s 说明日志断档。
# 玩家跑动上限 ~2.5 m/s × 27 px/m ≈ 70 px/s ≈ 4 px/帧,单帧 200 px 只可能是传送。
MAX_SAMPLE_DT = 0.15
MAX_STEP_PX = 200.0
# 相邻两帧所选日志样本的 vt 差:正常 ≤ 1/16 + 2×max_sample_dt(0.15 s 规则下 ≈ 0.36 s)。
# 远超这个量(或倒退)只会发生在 offsets.json 的 offset 台阶处(20260823 的 vt=14400,
# vid_t 局部非单调),那里的最近邻会跨过 4 s 的日志跳到另一侧,逐帧偏移出现 ~150 px 的
# 假跳变但没到 MAX_STEP_PX。阈值取 max(MAX_LOG_JUMP_S, 1/16 + 2×max_sample_dt + 0.1),
# 放宽 max_sample_dt 时随之放宽,台阶的 4 s 总是被抓住。
MAX_LOG_JUMP_S = 0.5
# 9 类动作 → 屏幕单位方向 (du, dv),索引与 data/code/scenes.py 的 ACTIONS 一致:
# 0 idle, 1 up, 2 up-left, 3 left, 4 down-left, 5 down, 6 down-right, 7 right, 8 up-right。
_S = np.sqrt(0.5)
ACTION_DIRS = np.array([
[0.0, 0.0], [0.0, -1.0], [-_S, -_S], [-1.0, 0.0], [-_S, _S],
[0.0, 1.0], [_S, _S], [1.0, 0.0], [_S, -_S],
], np.float64)
def cell_frames(k: int) -> list[int]:
"""cell k 覆盖的帧下标:cell 0 = 帧 0;cell k = 帧 4k−3 … 4k(Wan2.2 因果 VAE 的分组)。"""
if not 0 <= k < CELLS:
raise ValueError(f"cell 下标越界: {k}")
return [0] if k == 0 else list(range(4 * k - 3, 4 * k + 1))
def frames_to_cells(frame_offset_px: np.ndarray) -> np.ndarray:
"""逐帧偏移 [81,2] → 逐 cell 偏移 [21,2]:cell 内各帧取均值。cell 0 就是帧 0。"""
off = np.asarray(frame_offset_px, np.float64)
if off.shape != (FRAMES, 2):
raise ValueError(f"frame_offset_px 形状应为 ({FRAMES}, 2),得到 {off.shape}")
return np.stack([off[cell_frames(k)].mean(0) for k in range(CELLS)], 0)
def load_session_tracks(logs_dir: str, session: str) -> dict:
"""读一个 session 的日志:按 vt 排序去重的世界轨迹 + offset 分段 + 两种画幅下的 M。
与 data/code/make_clips.py 的 build_tracks 同一套处理:日志有重复时间戳,
不去重的话 gradient / 最近邻都会出问题;这里只保留 diff(vt) > 1e-6 的样本。
make_clips 用的是默认 argsort(不稳定排序),这里用 stable —— 两个 session 上核对过
保留下来的样本下标逐一相同(重复 vt 的样本本来就按写入顺序排在一起)。
"""
d = os.path.join(logs_dir, session)
st = np.load(os.path.join(d, "state.npz"))
o = np.argsort(st["vt"], kind="stable")
vt = st["vt"][o].astype(np.float64)
x = st["x"][o].astype(np.float64)
y = st["y"][o].astype(np.float64)
z = st["z"][o].astype(np.float64)
keep = np.concatenate([[True], np.diff(vt) > 1e-6])
vt, x, y, z = vt[keep], x[keep], y[keep], z[keep]
with open(os.path.join(d, "offsets.json"), encoding="utf-8") as fh:
align = json.load(fh)
segs = align["segments"]
off_los = np.array([s["vt_lo"] for s in segs], np.float64)
off_vals = np.array([s["offset"] for s in segs], np.float64)
if align.get("M_px1280") is None:
raise ValueError(f"{session} 的 offsets.json 没有 M_px1280")
M_px1280 = np.array(align["M_px1280"], np.float64)
if M_px1280.shape != (3, 2):
raise ValueError(f"M_px1280 形状应为 (3, 2),得到 {M_px1280.shape}")
M_px832 = M_px1280 * SCALE_1280_TO_832[None, :]
tracks = dict(session=session, vt=vt, x=x, y=y, z=z,
off_los=off_los, off_vals=off_vals,
M_px1280=M_px1280, M_px832=M_px832)
# video_t = vt + offset(vt)。20260823 在 vt=14400 有 −4.13 s 的台阶,vid_t 在那里
# 局部不单调;make_clips 就是在这条数组上 searchsorted 的,为了逐帧真值一致这里照做,
# 落在台阶附近的 clip 靠 MAX_SAMPLE_DT / MAX_LOG_JUMP_S 判 invalid。
tracks["vid_t"] = vt + offset_at(tracks, vt)
return tracks
def offset_at(tracks: dict, vt):
"""分段常数 offset(vt)。段以 vt_lo 为界,右闭左开与 make_clips 一致。"""
i = np.clip(np.searchsorted(tracks["off_los"], vt, side="right") - 1,
0, tracks["off_vals"].size - 1)
return tracks["off_vals"][i]
def project_px832(tracks: dict, x, y, z):
"""世界坐标 → 832×480 屏幕投影 (u, v)。支持标量或同形数组。"""
M = tracks["M_px832"]
x = np.asarray(x, np.float64); y = np.asarray(y, np.float64); z = np.asarray(z, np.float64)
u = x * M[0, 0] + y * M[1, 0] + z * M[2, 0]
v = x * M[0, 1] + y * M[1, 1] + z * M[2, 1]
return u, v
def nearest_log_index(tracks: dict, video_t: np.ndarray) -> np.ndarray:
"""按 video_t 取最近邻日志样本下标(与 make_clips.process_segment 逐帧一致)。"""
vid_t = tracks["vid_t"]
i = np.searchsorted(vid_t, video_t)
i = np.clip(i, 1, vid_t.size - 1)
prev = i - 1
return np.where(np.abs(vid_t[i] - video_t) <= np.abs(vid_t[prev] - video_t), i, prev)
def sample_frame_offsets(tracks: dict, video_t0: float, n_frames: int = FRAMES,
return_valid: bool = False, max_sample_dt: float = MAX_SAMPLE_DT):
"""clip 的逐帧相机偏移 [n_frames, 2](dx, dy),帧 0 恒为 (0, 0)。
帧 i 的 video_t = video_t0 + i/16,日志按 video_t 最近邻取样。
valid 判定(任一条不满足 ⇒ False):
· 每帧最近邻日志样本的时间差 ≤ max_sample_dt(否则是日志断档)
· 坐标无 NaN
· 所选日志样本的 vt 单调且相邻差 ≤ max(MAX_LOG_JUMP_S, 1/16 + 2·max_sample_dt + 0.1)
(否则跨过了 offset 台阶)
· 单帧位移 ≤ MAX_STEP_PX(否则是传送)
默认只返回偏移数组,且 invalid 时整个数组填 NaN —— 调用方不检查也用不到脏值。
return_valid=True 时返回 (offsets_raw, valid, reason),raw 里保留实际算出的值供排查。
"""
want_t = float(video_t0) + np.arange(n_frames, dtype=np.float64) / FPS
idx = nearest_log_index(tracks, want_t)
dt = np.abs(tracks["vid_t"][idx] - want_t)
u, v = project_px832(tracks, tracks["x"][idx], tracks["y"][idx], tracks["z"][idx])
off = np.stack([u - u[0], v - v[0]], 1)
dvt = np.diff(tracks["vt"][idx]) if n_frames > 1 else np.zeros(1)
reason = ""
if not np.isfinite(off).all():
reason = "nan"
elif dt.max() > max_sample_dt:
reason = f"log_gap dt={dt.max():.3f}s"
elif n_frames > 1 and (dvt.min() < 0 or dvt.max() > max(MAX_LOG_JUMP_S, 1.0 / FPS + 2.0 * max_sample_dt + 0.1)):
reason = f"log_jump dvt=[{dvt.min():.3f},{dvt.max():.3f}]s"
else:
step = np.hypot(*np.diff(off, axis=0).T)
if step.size and step.max() > MAX_STEP_PX:
reason = f"teleport step={step.max():.1f}px"
valid = reason == ""
if return_valid:
return off, valid, reason
return off if valid else np.full_like(off, np.nan)
def transition_frame_offsets(off_a: np.ndarray, off_b: np.ndarray, n_pre: int) -> np.ndarray:
"""转场 clip 的逐帧偏移:帧 < n_pre 抄 src_a,之后接 src_b 的增量,累计位移保持连续。
i < n_pre : off[i] = offA[i]
i >= n_pre : off[i] = offA[n_pre−1] + (offB[i] − offB[n_pre−1])
"""
off_a = np.asarray(off_a, np.float64); off_b = np.asarray(off_b, np.float64)
if off_a.shape != off_b.shape or off_a.shape[1] != 2:
raise ValueError(f"src_a/src_b 偏移形状不一致: {off_a.shape} vs {off_b.shape}")
if not 1 <= n_pre <= off_a.shape[0]:
raise ValueError(f"n_pre 越界: {n_pre}")
out = off_a.copy()
out[n_pre:] = off_a[n_pre - 1] + (off_b[n_pre:] - off_b[n_pre - 1])
return out
def _known_mask(offset_px: np.ndarray, h: int, w: int, px_per_unit: float) -> np.ndarray:
"""cell k 中格 (i_h, j_w) 的世界坐标 = (i_h + dy/px, j_w + dx/px);落在首帧足迹
[−0.5, h−0.5] × [−0.5, w−0.5] 内 ⇒ known。边界取闭区间,偏移恰为半格时仍算 known。"""
off = np.asarray(offset_px, np.float64)
if off.ndim != 2 or off.shape[1] != 2:
raise ValueError(f"offset_px 形状应为 (F, 2),得到 {off.shape}")
ih = np.arange(h, dtype=np.float64)[None, :, None] + off[:, 1, None, None] / px_per_unit
jw = np.arange(w, dtype=np.float64)[None, None, :] + off[:, 0, None, None] / px_per_unit
ok_h = (ih >= -0.5) & (ih <= h - 0.5)
ok_w = (jw >= -0.5) & (jw <= w - 0.5)
return ok_h & ok_w
def known_mask_tokens(offset_px: np.ndarray) -> np.ndarray:
"""token 分辨率 known 掩码 [F, 15, 26](token = 32 px)。"""
return _known_mask(offset_px, TOK_H, TOK_W, PX_PER_TOKEN)
def known_mask_latent(offset_px: np.ndarray) -> np.ndarray:
"""latent 分辨率 known 掩码 [F, 30, 52](latent 像素 = 16 px)。"""
return _known_mask(offset_px, LAT_H, LAT_W, PX_PER_LATENT)
|