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/geometry.py from teawhite/ActionRoPE: direct link, hf CLI and curl.
- Browser
- Download file 10.2 kB
-
https://huggingface.co/teawhite/ActionRoPE/resolve/main/code/actionrope/geometry.py
- Command line
-
hf download hf://teawhite/ActionRoPE/code/actionrope/geometry.py
-
curl -L -o geometry.py https://huggingface.co/teawhite/ActionRoPE/resolve/main/code/actionrope/geometry.py
10.2 kB
| """几何工具:帧/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) | |