Spaces:
Running on Zero
Running on Zero
Download multics/data/dataset.py from hugging-apps/multics-multiplayer-video: direct link, hf CLI and curl.
- Browser
- Download file 52.3 kB
-
https://huggingface.co/spaces/hugging-apps/multics-multiplayer-video/resolve/main/multics/data/dataset.py
- Command line
-
hf download hf://spaces/hugging-apps/multics-multiplayer-video/multics/data/dataset.py
-
curl -L -o dataset.py https://huggingface.co/spaces/hugging-apps/multics-multiplayer-video/resolve/main/multics/data/dataset.py
52.3 kB
| import os, re, random, warnings | |
| import torch | |
| import numpy as np | |
| import pandas as pd | |
| from pathlib import Path | |
| _REPO_DIR = str(Path(__file__).resolve().parents[2] / "asset") | |
| _BLANK_PROMPT_EMB_FILENAME = "blank_string_prompt_emb.pth" | |
| def _default_blank_prompt_emb_path() -> str: | |
| return os.path.join(_REPO_DIR, _BLANK_PROMPT_EMB_FILENAME) | |
| def _parse_int_csv(value, *, default=None, name="value"): | |
| if value is None or value == "": | |
| return default | |
| if isinstance(value, (list, tuple)): | |
| return [int(v) for v in value] | |
| return [int(v.strip()) for v in str(value).split(",") if v.strip()] | |
| def _prompt_emb_to_cpu(prompt_emb: dict) -> dict: | |
| if not isinstance(prompt_emb, dict): | |
| raise TypeError(f"prompt emb should be dict, got {type(prompt_emb)}") | |
| return { | |
| k: v.detach().cpu() if torch.is_tensor(v) else v for k, v in prompt_emb.items() | |
| } | |
| def _save_prompt_emb_atomic(prompt_emb: dict, path: str) -> None: | |
| os.makedirs(os.path.dirname(path) or ".", exist_ok=True) | |
| rank = os.environ.get("RANK", "0") | |
| tmp_path = f"{path}.tmp.rank{rank}.pid{os.getpid()}" | |
| try: | |
| torch.save(prompt_emb, tmp_path) | |
| os.replace(tmp_path, path) | |
| finally: | |
| if os.path.exists(tmp_path): | |
| os.remove(tmp_path) | |
| def _resolve_path(base_path, p): | |
| """CSV 路径可为绝对路径;否则相对 dataset_path。""" | |
| p = str(p) | |
| return p if os.path.isabs(p) else os.path.join(base_path, p) | |
| def _camera12_from_absolute_c2w( | |
| c2w_abs: torch.Tensor, pos_scale: float | |
| ) -> torch.Tensor: | |
| """Convert absolute cameras to the model's shared-relative representation. | |
| Args: | |
| c2w_abs: Float tensor ``[P,T,4,4]`` in the shared CS world frame. | |
| pos_scale: Positive translation normalization in metres. | |
| Returns: | |
| Float32 tensor ``[P,T,12]``, anchored at player 0/frame 0. | |
| """ | |
| if c2w_abs.ndim != 4 or tuple(c2w_abs.shape[-2:]) != (4, 4): | |
| raise ValueError(f"absolute c2w 应为 [P,T,4,4],got {tuple(c2w_abs.shape)}") | |
| if c2w_abs.shape[0] < 1 or c2w_abs.shape[1] < 1: | |
| raise ValueError(f"absolute c2w 的 P/T 必须为正,got {tuple(c2w_abs.shape)}") | |
| if not torch.isfinite(c2w_abs).all(): | |
| raise ValueError("absolute c2w 包含 NaN/Inf") | |
| if not np.isfinite(pos_scale) or float(pos_scale) <= 0: | |
| raise ValueError(f"camera pos_scale 必须为正,got {pos_scale}") | |
| c2w = c2w_abs.to(dtype=torch.float64) | |
| expected_bottom = torch.tensor( | |
| [0.0, 0.0, 0.0, 1.0], dtype=c2w.dtype, device=c2w.device | |
| ) | |
| if not torch.allclose(c2w[..., 3, :], expected_bottom, atol=1e-09, rtol=0.0): | |
| raise ValueError("absolute c2w 齐次矩阵最后一行不是 [0,0,0,1]") | |
| rotations = c2w[..., :3, :3] | |
| eye3 = torch.eye(3, dtype=c2w.dtype, device=c2w.device) | |
| if not torch.allclose( | |
| rotations.transpose(-1, -2) @ rotations, eye3, atol=2e-07, rtol=0.0 | |
| ): | |
| raise ValueError("absolute c2w rotation 不是正交矩阵") | |
| det = torch.linalg.det(rotations) | |
| if not torch.allclose(det, torch.ones_like(det), atol=2e-07, rtol=0.0): | |
| raise ValueError("absolute c2w rotation determinant 不是 +1") | |
| anchor = c2w[0, 0] | |
| anchor_w2c = torch.eye(4, dtype=c2w.dtype, device=c2w.device) | |
| anchor_w2c[:3, :3] = anchor[:3, :3].transpose(0, 1) | |
| anchor_w2c[:3, 3] = -(anchor[:3, :3].transpose(0, 1) @ anchor[:3, 3]) | |
| c2w_rel = anchor_w2c.view(1, 1, 4, 4) @ c2w | |
| c2w_rel[..., :3, 3] /= float(pos_scale) | |
| return c2w_rel[..., :3, :].reshape(c2w.shape[0], c2w.shape[1], 12).float() | |
| def _ensure_prompt_batch_dim(t: torch.Tensor, *, path: str, key: str) -> torch.Tensor: | |
| if not torch.is_tensor(t): | |
| raise TypeError(f"prompt emb {key!r} 不是 tensor: {path}") | |
| if t.ndim == 2: | |
| return t.unsqueeze(0) | |
| if t.ndim >= 3: | |
| return t | |
| raise ValueError(f"prompt emb {key!r} 形状异常 {tuple(t.shape)}: {path}") | |
| def _load_prompt_emb_many(paths: list[str]) -> dict: | |
| """Load per-player prompt embeddings and stack on P. | |
| Each file is expected to contain one player's context, e.g. [1,S,4096]. | |
| Return shape per key: [P,S,4096] before DataLoader adds B -> [B,P,S,4096]. | |
| """ | |
| embs = [torch.load(p, weights_only=True, map_location="cpu") for p in paths] | |
| if any((not isinstance(e, dict) for e in embs)): | |
| raise TypeError(f"prompt emb 文件应为 dict: {paths}") | |
| keys = list(embs[0].keys()) | |
| out = {} | |
| for k in keys: | |
| vals = [] | |
| for path, emb in zip(paths, embs): | |
| if k not in emb: | |
| raise KeyError(f"prompt emb key {k!r} 不存在于 {path}") | |
| v = _ensure_prompt_batch_dim(emb[k], path=path, key=k) | |
| if v.shape[0] != 1: | |
| raise ValueError( | |
| f"prompt_emb_path* 应各自只含 1 条 player emb,但 key {k!r} 在 {path} 的形状为 {tuple(v.shape)}" | |
| ) | |
| if vals and tuple(v.shape[1:]) != tuple(vals[0].shape[1:]): | |
| raise ValueError( | |
| f"prompt emb key {k!r} 除 player 维外形状不一致: {tuple(v.shape)} vs {tuple(vals[0].shape)} ({path})" | |
| ) | |
| vals.append(v) | |
| out[k] = torch.cat(vals, dim=0) | |
| return out | |
| def consecutive_precomputed_start_rows( | |
| metadata, consecutive_rows: int, num_players: int | |
| ) -> list[int]: | |
| """Return CSV row starts whose following rows are the same continuous clip.""" | |
| consecutive_rows = int(consecutive_rows) | |
| if consecutive_rows < 1: | |
| raise ValueError(f"consecutive_rows must be >= 1, got {consecutive_rows}") | |
| n_rows = len(metadata) | |
| if consecutive_rows == 1: | |
| return list(range(n_rows)) | |
| required = ["start_frame"] | |
| for pidx in range(1, int(num_players) + 1): | |
| required.extend((f"video_path{pidx}", f"video{pidx}_frame")) | |
| missing = [column for column in required if column not in metadata.columns] | |
| if missing: | |
| raise ValueError( | |
| f"consecutive precomputed rows require CSV columns {missing} to verify continuity" | |
| ) | |
| valid = [] | |
| for start in range(0, n_rows - consecutive_rows + 1): | |
| ok = True | |
| for row in range(start, start + consecutive_rows - 1): | |
| lengths = [ | |
| int(metadata.iloc[row][f"video{pidx}_frame"]) | |
| for pidx in range(1, num_players + 1) | |
| ] | |
| if min(lengths) <= 0 or len(set(lengths)) != 1: | |
| ok = False | |
| break | |
| if ( | |
| int(metadata.iloc[row + 1]["start_frame"]) | |
| != int(metadata.iloc[row]["start_frame"]) + lengths[0] | |
| ): | |
| ok = False | |
| break | |
| for pidx in range(1, num_players + 1): | |
| current_dir = os.path.dirname( | |
| str(metadata.iloc[row][f"video_path{pidx}"]).replace("\\", "/") | |
| ) | |
| next_dir = os.path.dirname( | |
| str(metadata.iloc[row + 1][f"video_path{pidx}"]).replace("\\", "/") | |
| ) | |
| if current_dir != next_dir: | |
| ok = False | |
| break | |
| if not ok: | |
| break | |
| if ok: | |
| valid.append(start) | |
| return valid | |
| class PrecomputedLatentDataset(torch.utils.data.Dataset): | |
| """直接从预处理好的 CSV 读取压好的 VAE latent + action(跳过在线读视频 + VAE 编码)。 | |
| CSV 列(路径相对 base_path=preprocess root;绝对路径则原样用): | |
| latent_path1..latent_pathP(或 vae_path1..P), action_path1..action_pathP | |
| camera pose(仅 --use_camera_pose 时读取): | |
| - 只读预计算好的 camera_pose_path/camera12_path,或 per-player camera_pose_path1..P; | |
| - per-player 文件可为旧 [T,12],也可为 canonical absolute c2w [T,4,4];后者在 | |
| P 路全部加载后,以 compact player 0 / frame 0 统一计算 shared-relative camera12; | |
| - 训练时不再从 json_file1/2 现场生成 camera12,避免 dataloader 反复读 action JSON。 | |
| 每段(CSV 一行)对应的物理文件: | |
| *.latent.pt : torch.save 的张量 [C=48, T_lat, h, w](bf16) | |
| *.action.pt : torch.save({"keyboard": float[T,23], "mouse": float[T,2]})(mouse 已 compress_mouse_linear) | |
| 返回的 batch key 与 TensorDataset 默认路径下游一致: | |
| latents[P,48,T_lat,h,w]、keyboard[P,T,23]、mouse[P,T,2]、prompt_emb[P,S,4096]。 | |
| 为兼容旧 2-player 路径,当 P=2 时也会额外返回 latents1/latents2。 | |
| latent 与 action 由同一 VAE 和同一切片离线算出。 | |
| """ | |
| def __init__( | |
| self, | |
| base_path, | |
| metadata_path, | |
| steps_per_epoch=2000, | |
| num_players=2, | |
| target_rgb_frames=165, | |
| blank_prompt_emb_path=None, | |
| vae_latent_channels=48, | |
| debug_no_retry=False, | |
| num_frame_per_block=3, | |
| local_attn_size=24, | |
| use_camera_pose=False, | |
| use_group_labels=False, | |
| num_groups=1, | |
| group_path_column="group_path", | |
| group_label_player_ids=None, | |
| hier_group_mode="csv", | |
| hier_group_seed=0, | |
| hier_random_preserve_sizes=True, | |
| hier_group_format="labels", | |
| consecutive_rows=1, | |
| grouping_sample_indices=None, | |
| grouping_cache_dir="outputs/grouping_cache", | |
| grouping_model_profile=(5, 6, 1.0, 3), | |
| allow_first_frame_latents=False, | |
| ): | |
| metadata = pd.read_csv(metadata_path) | |
| self.num_players = int(num_players) | |
| if self.num_players < 2: | |
| raise ValueError(f"num_players must be >= 2, got {self.num_players}") | |
| latent_cols = [] | |
| action_cols = [] | |
| missing = [] | |
| for pidx in range(1, self.num_players + 1): | |
| latent_col = None | |
| for candidate in (f"latent_path{pidx}", f"vae_path{pidx}"): | |
| if candidate in metadata.columns: | |
| latent_col = candidate | |
| break | |
| if latent_col is None: | |
| missing.append(f"latent_path{pidx}/vae_path{pidx}") | |
| latent_cols.append(latent_col) | |
| action_col = f"action_path{pidx}" | |
| if action_col not in metadata.columns: | |
| missing.append(action_col) | |
| action_cols.append(action_col) | |
| if missing: | |
| raise ValueError( | |
| f"[PrecomputedLatentDataset] CSV 缺少预处理列 {missing};该 CSV 似乎不是 *.preprocessed.csv。现有列:{list(metadata.columns)}" | |
| ) | |
| self.metadata = metadata | |
| self.base_path = base_path | |
| self.latent_column_names = latent_cols | |
| self.action_column_names = action_cols | |
| self.latent_paths = [ | |
| [_resolve_path(base_path, x) for x in metadata[col]] | |
| for col in self.latent_column_names | |
| ] | |
| self.action_paths = [ | |
| [_resolve_path(base_path, x) for x in metadata[col]] | |
| for col in self.action_column_names | |
| ] | |
| self.latent_path1 = self.latent_paths[0] | |
| self.latent_path2 = self.latent_paths[1] | |
| self.action_path1 = self.action_paths[0] | |
| self.action_path2 = self.action_paths[1] | |
| self.use_camera_pose = bool(use_camera_pose) | |
| self.camera_pose_path = None | |
| self.camera_pose_paths = None | |
| combined_camera_col = next( | |
| (c for c in ("camera_pose_path", "camera12_path") if c in metadata.columns), | |
| None, | |
| ) | |
| pair_camera_cols = next( | |
| ( | |
| prefix | |
| for prefix in ("camera_pose_path", "camera12_path") | |
| if all( | |
| ( | |
| f"{prefix}{pidx}" in metadata.columns | |
| for pidx in range(1, self.num_players + 1) | |
| ) | |
| ) | |
| ), | |
| None, | |
| ) | |
| if combined_camera_col is not None and pair_camera_cols is not None: | |
| raise ValueError( | |
| "CSV camera pose 列不能同时使用 combined 和 per-player 两种格式" | |
| ) | |
| for prefix in ("camera_pose_path", "camera12_path"): | |
| present = [ | |
| f"{prefix}{pidx}" in metadata.columns | |
| for pidx in range(1, self.num_players + 1) | |
| ] | |
| if any(present) and (not all(present)): | |
| raise ValueError( | |
| f"CSV 若使用 per-player camera pose,必须同时包含 {prefix}1..{prefix}{self.num_players}" | |
| ) | |
| if combined_camera_col is not None: | |
| self.camera_pose_path = [ | |
| _resolve_path(base_path, x) for x in metadata[combined_camera_col] | |
| ] | |
| elif pair_camera_cols is not None: | |
| self.camera_pose_paths = [ | |
| [ | |
| _resolve_path(base_path, x) | |
| for x in metadata[f"{pair_camera_cols}{pidx}"] | |
| ] | |
| for pidx in range(1, self.num_players + 1) | |
| ] | |
| if ( | |
| self.use_camera_pose | |
| and self.camera_pose_path is None | |
| and (self.camera_pose_paths is None) | |
| ): | |
| raise ValueError( | |
| f"--use_camera_pose requires camera_pose_path/camera12_path, or per-player camera_pose_path1..{self.num_players} / camera12_path1..{self.num_players}. Camera poses must already be precomputed." | |
| ) | |
| self.prompt_emb_paths = None | |
| prompt_cols = [ | |
| f"prompt_emb_path{pidx}" for pidx in range(1, self.num_players + 1) | |
| ] | |
| prompt_present = [c in metadata.columns for c in prompt_cols] | |
| if any(prompt_present) and (not all(prompt_present)): | |
| raise ValueError( | |
| f"CSV 若使用 prompt emb,必须同时包含 prompt_emb_path1..prompt_emb_path{self.num_players}" | |
| ) | |
| if all(prompt_present): | |
| self.prompt_emb_paths = [ | |
| [_resolve_path(base_path, x) for x in metadata[col]] | |
| for col in prompt_cols | |
| ] | |
| self.prompt_emb_path1 = ( | |
| self.prompt_emb_paths[0] if self.prompt_emb_paths else None | |
| ) | |
| self.prompt_emb_path2 = ( | |
| self.prompt_emb_paths[1] if self.prompt_emb_paths else None | |
| ) | |
| self.captions = None | |
| caption_cols = [f"caption{pidx}" for pidx in range(1, self.num_players + 1)] | |
| caption_present = [c in metadata.columns for c in caption_cols] | |
| if any(caption_present) and (not all(caption_present)): | |
| raise ValueError( | |
| f"CSV 若使用 caption,必须同时包含 caption1..caption{self.num_players}" | |
| ) | |
| if all(caption_present): | |
| self.captions = [ | |
| [str(v).strip() if pd.notna(v) else "" for v in metadata[col]] | |
| for col in caption_cols | |
| ] | |
| print( | |
| f"[PrecomputedLatentDataset] 使用 caption1..caption{self.num_players} 作为 validation prompt。" | |
| ) | |
| self.caption1 = self.captions[0] if self.captions else None | |
| self.caption2 = self.captions[1] if self.captions else None | |
| self.use_group_labels = bool(use_group_labels) | |
| self.num_groups = int(num_groups) | |
| self.group_path_column = str(group_path_column) | |
| self.hier_group_mode = str(hier_group_mode) | |
| self.hier_group_seed = int(hier_group_seed) | |
| self.hier_random_preserve_sizes = bool(hier_random_preserve_sizes) | |
| self.hier_group_format = str(hier_group_format) | |
| if self.hier_group_mode not in ("csv", "random_static", "random_block"): | |
| raise ValueError( | |
| f"hier_group_mode must be csv/random_static/random_block, got {self.hier_group_mode!r}" | |
| ) | |
| if self.hier_group_format not in ("labels", "membership"): | |
| raise ValueError( | |
| f"hier_group_format must be labels or membership, got {self.hier_group_format!r}" | |
| ) | |
| if self.hier_group_format == "membership" and self.hier_group_mode != "csv": | |
| raise ValueError( | |
| f"hier_group_format=membership does not support hier_group_mode={self.hier_group_mode!r}; use csv. Random modes only support the labels format." | |
| ) | |
| self.group_label_player_ids = _parse_int_csv( | |
| group_label_player_ids, | |
| default=list(range(1, self.num_players + 1)), | |
| name="group_label_player_ids", | |
| ) | |
| if len(self.group_label_player_ids) != self.num_players: | |
| raise ValueError( | |
| f"group_label_player_ids length {len(self.group_label_player_ids)} must equal num_players={self.num_players}: {self.group_label_player_ids}" | |
| ) | |
| if any((pid < 1 or pid > 10 for pid in self.group_label_player_ids)): | |
| raise ValueError( | |
| f"group_label_player_ids must be 1-based ids in [1,10], got {self.group_label_player_ids}" | |
| ) | |
| self.group_paths = None | |
| self.source_n_rows = len(metadata) | |
| self.consecutive_rows = int(consecutive_rows) | |
| if self.consecutive_rows < 1: | |
| raise ValueError( | |
| f"consecutive_rows must be >= 1, got {self.consecutive_rows}" | |
| ) | |
| self.sample_start_rows = consecutive_precomputed_start_rows( | |
| metadata, self.consecutive_rows, self.num_players | |
| ) | |
| self.n_rows = len(self.sample_start_rows) | |
| if self.n_rows == 0: | |
| raise ValueError( | |
| f"[PrecomputedLatentDataset] no valid {self.consecutive_rows}-row continuous windows in {metadata_path}" | |
| ) | |
| self.allow_first_frame_latents = bool(allow_first_frame_latents) | |
| if self.allow_first_frame_latents and self.consecutive_rows != 1: | |
| raise ValueError("First-frame latents require consecutive_rows=1") | |
| self.steps_per_epoch = steps_per_epoch | |
| self.target_rgb_frames = int(target_rgb_frames) | |
| self.vae_latent_channels = int(vae_latent_channels) | |
| self.num_frame_per_block = int(num_frame_per_block) | |
| self.local_attn_size = int(local_attn_size) | |
| self.blank_prompt_emb_path = ( | |
| blank_prompt_emb_path or _default_blank_prompt_emb_path() | |
| ) | |
| self.blank_prompt_emb = None | |
| self.debug_no_retry = debug_no_retry | |
| if self.use_group_labels: | |
| from multics.grouping.prepare import prepare_dataset_groups | |
| prepare_dataset_groups( | |
| self, grouping_sample_indices, grouping_cache_dir, | |
| (*grouping_model_profile[:3], self.num_frame_per_block), | |
| ) | |
| print( | |
| f"[PrecomputedLatentDataset] rows={self.n_rows} target_rgb_frames={self.target_rgb_frames} latent_channels={self.vae_latent_channels} use_camera_pose={self.use_camera_pose} camera_source={('path' if self.camera_pose_path or self.camera_pose_paths else 'none')} prompt_emb={self.prompt_emb_paths is not None} num_players={self.num_players} use_group_labels={self.use_group_labels} group_label_player_ids={(self.group_label_player_ids if self.use_group_labels else 'none')} hier_group_mode={(self.hier_group_mode if self.use_group_labels else 'none')} hier_group_format={(self.hier_group_format if self.use_group_labels else 'none')} hier_group_seed={self.hier_group_seed} hier_random_preserve_sizes={self.hier_random_preserve_sizes} consecutive_rows={self.consecutive_rows} valid_windows={self.n_rows}" | |
| ) | |
| def __len__(self): | |
| return max(self.n_rows, self.steps_per_epoch) | |
| def _load_action(self, path): | |
| d = torch.load(path, weights_only=True, map_location="cpu") | |
| return (d["keyboard"].float(), d["mouse"].float()) | |
| def _load_camera_pose_file(self, path: str): | |
| ext = os.path.splitext(path)[1].lower() | |
| if ext == ".npy": | |
| obj = np.load(path) | |
| elif ext == ".npz": | |
| with np.load(path) as npz: | |
| if "c2w_abs" in npz and ( | |
| not any( | |
| ( | |
| key in npz | |
| for key in ("camera_pose", "camera12", "pose", "camera") | |
| ) | |
| ) | |
| ): | |
| obj = {key: np.array(npz[key], copy=True) for key in npz.files} | |
| else: | |
| key = next( | |
| ( | |
| k | |
| for k in ("camera_pose", "camera12", "pose", "camera") | |
| if k in npz | |
| ), | |
| npz.files[0], | |
| ) | |
| obj = np.array(npz[key], copy=True) | |
| else: | |
| obj = torch.load(path, weights_only=True, map_location="cpu") | |
| if isinstance(obj, dict): | |
| if "c2w_abs" in obj and ( | |
| not any( | |
| ( | |
| key in obj | |
| for key in ("camera_pose", "camera12", "pose", "camera") | |
| ) | |
| ) | |
| ): | |
| return obj | |
| key = next( | |
| (k for k in ("camera_pose", "camera12", "pose", "camera") if k in obj), | |
| None, | |
| ) | |
| if key is None: | |
| raise KeyError( | |
| f"camera pose 文件缺少 camera_pose/camera12/pose/camera key: {path}" | |
| ) | |
| obj = obj[key] | |
| if not torch.is_tensor(obj): | |
| obj = torch.as_tensor(obj) | |
| return obj.float() | |
| def _camera_payload_tensor(payload: dict, key: str, *, path: str) -> torch.Tensor: | |
| """Read one required sidecar field as a tensor. | |
| Args: | |
| payload: Loaded numeric per-video camera dictionary. | |
| key: Required dictionary field name. | |
| path: Source path included in validation errors. | |
| Returns: | |
| The field as a torch tensor without changing its dtype. | |
| """ | |
| if key not in payload: | |
| raise KeyError(f"per-video camera 文件缺少 {key!r}: {path}") | |
| value = payload[key] | |
| return value if torch.is_tensor(value) else torch.as_tensor(value) | |
| def _camera12_from_per_video_absolute( | |
| self, row: int, payloads: list[dict] | |
| ) -> torch.Tensor: | |
| """Stack one row's physical-video sidecars and choose a shared anchor. | |
| Args: | |
| row: Metadata row index used for CSV/path consistency checks. | |
| payloads: Per-video absolute-camera dictionaries in compact CSV order. | |
| Returns: | |
| Float32 ``camera12[P,T_lat,12]`` anchored at the first compact | |
| player's first latent frame. Physical ``player_key`` is checked | |
| against each corresponding ``video_pathN``. | |
| """ | |
| paths = [player_paths[row] for player_paths in self.camera_pose_paths] | |
| c2w_players = [] | |
| reference_ticks = None | |
| reference_indices = None | |
| reference_start = None | |
| reference_stride = None | |
| reference_scale = None | |
| reference_unit = None | |
| csv_start = None | |
| if "start_frame" in self.metadata.columns: | |
| csv_start = int(self.metadata.iloc[row]["start_frame"]) | |
| for player_index, (payload, path) in enumerate(zip(payloads, paths), start=1): | |
| version = int( | |
| self._camera_payload_tensor(payload, "format_version", path=path) | |
| .reshape(-1)[0] | |
| .item() | |
| ) | |
| if version not in (2, 3): | |
| raise ValueError( | |
| f"per-video absolute camera format_version 应为 2/3,got {version}: {path}" | |
| ) | |
| c2w = self._camera_payload_tensor(payload, "c2w_abs", path=path) | |
| if c2w.ndim != 3 or tuple(c2w.shape[-2:]) != (4, 4): | |
| raise ValueError( | |
| f"per-video c2w_abs 应为 [T,4,4],got {tuple(c2w.shape)}: {path}" | |
| ) | |
| c2w_players.append(c2w) | |
| ticks = self._camera_payload_tensor( | |
| payload, "latent_ticks", path=path | |
| ).long() | |
| indices = self._camera_payload_tensor( | |
| payload, "latent_rgb_indices", path=path | |
| ).long() | |
| start = int( | |
| self._camera_payload_tensor(payload, "start_frame", path=path) | |
| .reshape(-1)[0] | |
| .item() | |
| ) | |
| stride = int( | |
| self._camera_payload_tensor(payload, "latent_stride", path=path) | |
| .reshape(-1)[0] | |
| .item() | |
| ) | |
| scale = float( | |
| self._camera_payload_tensor(payload, "pos_scale", path=path) | |
| .reshape(-1)[0] | |
| .item() | |
| ) | |
| unit = float( | |
| self._camera_payload_tensor(payload, "world_unit_to_metre", path=path) | |
| .reshape(-1)[0] | |
| .item() | |
| ) | |
| player_key = int( | |
| self._camera_payload_tensor(payload, "player_key", path=path) | |
| .reshape(-1)[0] | |
| .item() | |
| ) | |
| video_col = f"video_path{player_index}" | |
| if video_col in self.metadata.columns: | |
| video_path = str(self.metadata.iloc[row][video_col]).replace("\\", "/") | |
| match = _CAMERA_PLAYER_PART.search(video_path) | |
| if match is not None and int(match.group(1)) != player_key: | |
| raise ValueError( | |
| f"camera player_key={player_key:02d} 与 {video_col} 中的 player={int(match.group(1)):02d} 不一致: {path}" | |
| ) | |
| if csv_start is not None and start != csv_start: | |
| raise ValueError( | |
| f"camera start_frame={start} 与 CSV start_frame={csv_start} 不一致: {path}" | |
| ) | |
| if reference_ticks is None: | |
| reference_ticks = ticks | |
| reference_indices = indices | |
| reference_start = start | |
| reference_stride = stride | |
| reference_scale = scale | |
| reference_unit = unit | |
| else: | |
| if not torch.equal(ticks, reference_ticks): | |
| raise ValueError( | |
| f"不同 player 的 camera latent_ticks 不一致: {path}" | |
| ) | |
| if not torch.equal(indices, reference_indices): | |
| raise ValueError( | |
| f"不同 player 的 camera latent_rgb_indices 不一致: {path}" | |
| ) | |
| if start != reference_start or stride != reference_stride: | |
| raise ValueError( | |
| f"不同 player 的 camera start/stride 不一致: {path}" | |
| ) | |
| if scale != reference_scale or unit != reference_unit: | |
| raise ValueError( | |
| f"不同 player 的 camera unit/pos_scale 不一致: {path}" | |
| ) | |
| if len({tuple(c2w.shape) for c2w in c2w_players}) != 1: | |
| raise ValueError( | |
| f"不同 player 的 per-video c2w_abs shape 不一致: {[tuple(c2w.shape) for c2w in c2w_players]}" | |
| ) | |
| c2w_abs = torch.stack(c2w_players, dim=0) | |
| return _camera12_from_absolute_c2w(c2w_abs, float(reference_scale)) | |
| def _load_camera_pose(self, row: int, t_lat: int) -> torch.Tensor | None: | |
| if self.camera_pose_path is not None: | |
| cam = self._load_camera_pose_file(self.camera_pose_path[row]) | |
| if isinstance(cam, dict): | |
| raise ValueError( | |
| "combined camera_pose_path 不能指向 per-video absolute c2w;请使用 camera_pose_path1..N" | |
| ) | |
| elif self.camera_pose_paths is not None: | |
| cams = [ | |
| self._load_camera_pose_file(paths[row]) | |
| for paths in self.camera_pose_paths | |
| ] | |
| absolute = [isinstance(cam_i, dict) for cam_i in cams] | |
| if any(absolute) and (not all(absolute)): | |
| raise ValueError( | |
| "per-player camera 文件不能混用 absolute c2w 与旧 camera12 格式" | |
| ) | |
| if all(absolute): | |
| cam = self._camera12_from_per_video_absolute(row, cams) | |
| else: | |
| if any((cam_i.dim() != 2 for cam_i in cams)): | |
| raise ValueError("旧 per-player camera pose 文件应为 [T,12]") | |
| cam = torch.stack(cams, dim=0) | |
| else: | |
| return None | |
| if cam.dim() != 3: | |
| raise ValueError( | |
| f"camera_pose 应为 [P,T,12] 或 [T,P,12],got {tuple(cam.shape)}" | |
| ) | |
| if cam.shape[0] != self.num_players and cam.shape[1] == self.num_players: | |
| cam = cam.permute(1, 0, 2).contiguous() | |
| if cam.shape[0] != self.num_players or cam.shape[-1] != 12: | |
| raise ValueError( | |
| f"camera_pose 形状应为 [P={self.num_players},T,12],got {tuple(cam.shape)}" | |
| ) | |
| if cam.shape[1] < t_lat: | |
| raise ValueError( | |
| f"camera_pose latent T={cam.shape[1]} 短于 latent T={t_lat}" | |
| ) | |
| return cam[:, :t_lat, :].contiguous() | |
| def _randomize_group_labels( | |
| self, group_labels: torch.Tensor, row: int | |
| ) -> torch.Tensor: | |
| """Apply a deterministic random grouping while preserving [T,P].""" | |
| if self.hier_group_mode == "csv": | |
| return group_labels | |
| t_lat, num_players = group_labels.shape | |
| n_block = max(1, int(self.num_frame_per_block)) | |
| generator = torch.Generator() | |
| seed = (self.hier_group_seed + int(row) * 1000003) % ((1 << 63) - 1) | |
| generator.manual_seed(seed) | |
| if self.hier_random_preserve_sizes: | |
| if self.hier_group_mode == "random_static": | |
| perm = torch.randperm(num_players, generator=generator) | |
| return group_labels.index_select(1, perm).contiguous() | |
| randomized = torch.empty_like(group_labels) | |
| for start in range(0, t_lat, n_block): | |
| end = min(start + n_block, t_lat) | |
| perm = torch.randperm(num_players, generator=generator) | |
| assignment = group_labels[start].index_select(0, perm) | |
| randomized[start:end] = assignment | |
| return randomized | |
| randomized = torch.empty_like(group_labels) | |
| if self.hier_group_mode == "random_static": | |
| assignment = torch.randint( | |
| self.num_groups, (num_players,), generator=generator | |
| ) | |
| randomized[:] = assignment | |
| else: | |
| for start in range(0, t_lat, n_block): | |
| end = min(start + n_block, t_lat) | |
| assignment = torch.randint( | |
| self.num_groups, (num_players,), generator=generator | |
| ) | |
| randomized[start:end] = assignment | |
| return randomized | |
| def _select_group_player_columns( | |
| self, tensor: torch.Tensor, player_dim: int, path: str, name: str | |
| ) -> torch.Tensor: | |
| """Select compact player columns using 1-based group_label_player_ids. | |
| tensor player axis may be the full 10-player roster or already-compact P. | |
| """ | |
| cols = [pid - 1 for pid in self.group_label_player_ids] | |
| n_src = int(tensor.shape[player_dim]) | |
| max_id = max(self.group_label_player_ids) | |
| if n_src < max_id: | |
| already_compact = ( | |
| n_src == self.num_players | |
| and self.group_label_player_ids == list(range(1, self.num_players + 1)) | |
| ) | |
| if not already_compact: | |
| raise ValueError( | |
| f"{name} player dim {n_src} cannot cover ids {self.group_label_player_ids} at {path}" | |
| ) | |
| return tensor | |
| return tensor.index_select(player_dim, torch.tensor(cols, dtype=torch.long)) | |
| def _assert_block_constant( | |
| tensor: torch.Tensor, n_block: int, path: str, name: str | |
| ) -> None: | |
| """Grouping must be constant inside each latent temporal block (n=3).""" | |
| if n_block <= 1: | |
| return | |
| t_lat = int(tensor.shape[0]) | |
| for start in range(0, t_lat, n_block): | |
| block = tensor[start : start + n_block] | |
| if block.numel() and (not torch.equal(block, block[:1].expand_as(block))): | |
| raise ValueError( | |
| f"{name} must be constant inside each latent temporal block; path={path} block=[{start},{min(start + n_block, t_lat)})" | |
| ) | |
| def _load_group_labels(self, row: int, t_lat: int) -> dict: | |
| """Load grouping tensors for one CSV row. | |
| labels mode returns: | |
| group_labels [T,P] long in [0,N), active_group [T,N] bool | |
| membership mode additionally returns: | |
| membership [T,N,P] bool, global_only [T,P] bool | |
| group_labels is debug-only (argmax; -1 = not in any local group) | |
| eligible stays in the .pt and is only asserted, never returned. | |
| """ | |
| if self.group_paths is None: | |
| raise RuntimeError( | |
| "use_group_labels=True but group_paths is not initialized" | |
| ) | |
| path = self.group_paths[row] | |
| d = torch.load(path, weights_only=True, map_location="cpu") | |
| n_block = max(1, int(self.num_frame_per_block)) | |
| if self.hier_group_format == "membership": | |
| if not isinstance(d, dict) or "membership" not in d: | |
| raise ValueError( | |
| f"hier_group_format=membership requires key 'membership' in {path}; refusing to silently fall back to labels (anchors / orphans would be wrong)" | |
| ) | |
| membership = d["membership"] | |
| if not torch.is_tensor(membership): | |
| membership = torch.as_tensor(membership) | |
| if membership.dim() != 3: | |
| raise ValueError( | |
| f"membership must be [T,N,P] bool, got {tuple(membership.shape)} at {path}" | |
| ) | |
| if membership.shape[0] < t_lat: | |
| raise ValueError( | |
| f"membership T={membership.shape[0]} shorter than latent T={t_lat}: {path}" | |
| ) | |
| if int(membership.shape[1]) != self.num_groups: | |
| raise ValueError( | |
| f"membership N={int(membership.shape[1])} must equal num_groups={self.num_groups} at {path}" | |
| ) | |
| membership = membership[:t_lat].to(dtype=torch.bool) | |
| membership = self._select_group_player_columns( | |
| membership, player_dim=2, path=path, name="membership" | |
| ) | |
| if membership.shape[2] != self.num_players: | |
| raise ValueError( | |
| f"membership P={membership.shape[2]} must equal num_players={self.num_players} at {path}" | |
| ) | |
| if "global_only" not in d: | |
| raise ValueError( | |
| f"hier_group_format=membership requires key 'global_only' in {path}" | |
| ) | |
| global_only = d["global_only"] | |
| if not torch.is_tensor(global_only): | |
| global_only = torch.as_tensor(global_only) | |
| if global_only.dim() != 2: | |
| raise ValueError( | |
| f"global_only must be [T,P] bool, got {tuple(global_only.shape)} at {path}" | |
| ) | |
| if global_only.shape[0] < t_lat: | |
| raise ValueError( | |
| f"global_only T={global_only.shape[0]} shorter than latent T={t_lat}: {path}" | |
| ) | |
| global_only = global_only[:t_lat].to(dtype=torch.bool) | |
| global_only = self._select_group_player_columns( | |
| global_only, player_dim=1, path=path, name="global_only" | |
| ) | |
| if global_only.shape[1] != self.num_players: | |
| raise ValueError( | |
| f"global_only P={global_only.shape[1]} must equal num_players={self.num_players} at {path}" | |
| ) | |
| if "eligible" not in d: | |
| raise ValueError( | |
| f"hier_group_format=membership requires key 'eligible' [P] in {path}" | |
| ) | |
| eligible = d["eligible"] | |
| if not torch.is_tensor(eligible): | |
| eligible = torch.as_tensor(eligible) | |
| eligible = eligible.to(dtype=torch.bool).reshape(-1) | |
| eligible = self._select_group_player_columns( | |
| eligible.unsqueeze(0), player_dim=1, path=path, name="eligible" | |
| ).squeeze(0) | |
| if eligible.numel() != self.num_players: | |
| raise ValueError( | |
| f"eligible P={eligible.numel()} must equal num_players={self.num_players} at {path}" | |
| ) | |
| if not bool(eligible.all().item()): | |
| raise ValueError( | |
| f"eligible must be all True for this training CSV (not used in the mask); got {eligible.tolist()} at {path}" | |
| ) | |
| self._assert_block_constant(membership, n_block, path, "membership") | |
| self._assert_block_constant(global_only, n_block, path, "global_only") | |
| active_group = membership.any(dim=-1).contiguous() | |
| if "labels" in d: | |
| labels = d["labels"] | |
| if not torch.is_tensor(labels): | |
| labels = torch.as_tensor(labels) | |
| group_labels = labels[:t_lat].to(dtype=torch.long) | |
| group_labels = self._select_group_player_columns( | |
| group_labels, player_dim=1, path=path, name="labels" | |
| ).contiguous() | |
| else: | |
| any_g = membership.any(dim=1) | |
| group_labels = membership.to(dtype=torch.long).argmax(dim=1) | |
| group_labels = torch.where( | |
| any_g, group_labels, torch.full_like(group_labels, -1) | |
| ) | |
| return { | |
| "group_labels": group_labels, | |
| "active_group": active_group, | |
| "membership": membership.contiguous(), | |
| "global_only": global_only.contiguous(), | |
| } | |
| if isinstance(d, dict) and "labels" not in d: | |
| raise ValueError( | |
| f"hier_group_format=labels requires key 'labels' in {path}; this looks like a membership-only file. Pass --hier_group_format membership." | |
| ) | |
| labels = d["labels"] if isinstance(d, dict) else d | |
| if not torch.is_tensor(labels): | |
| labels = torch.as_tensor(labels) | |
| if labels.dim() != 2 or labels.shape[1] < max(self.group_label_player_ids): | |
| already_compact = ( | |
| labels.dim() == 2 | |
| and labels.shape[1] == self.num_players | |
| and ( | |
| self.group_label_player_ids == list(range(1, self.num_players + 1)) | |
| ) | |
| ) | |
| if not already_compact: | |
| raise ValueError( | |
| f"group labels should be [T,10] and cover ids {self.group_label_player_ids}, got {tuple(labels.shape)} at {path}" | |
| ) | |
| if labels.shape[0] < t_lat: | |
| raise ValueError( | |
| f"group labels T={labels.shape[0]} shorter than latent T={t_lat}: {path}" | |
| ) | |
| group_labels = labels[:t_lat].to(dtype=torch.long) | |
| group_labels = self._select_group_player_columns( | |
| group_labels, player_dim=1, path=path, name="labels" | |
| ).contiguous() | |
| if group_labels.numel() and ( | |
| int(group_labels.min().item()) < 0 | |
| or int(group_labels.max().item()) >= self.num_groups | |
| ): | |
| raise ValueError( | |
| f"group_labels values out of range [0,{self.num_groups}) at {path}: min={int(group_labels.min().item())} max={int(group_labels.max().item())}" | |
| ) | |
| self._assert_block_constant(group_labels, n_block, path, "group_labels") | |
| group_labels = self._randomize_group_labels(group_labels, row) | |
| active_group = torch.zeros(t_lat, self.num_groups, dtype=torch.bool) | |
| active_group.scatter_(1, group_labels, True) | |
| return {"group_labels": group_labels, "active_group": active_group} | |
| def _load_camera_pose_rows( | |
| self, rows: list[int], latent_lengths: list[int], target_t_lat: int | |
| ) -> torch.Tensor | None: | |
| """Load consecutive camera segments in one shared relative frame. | |
| Multi-row inference requires canonical per-player absolute c2w files. | |
| Concatenating legacy camera12 tensors would silently reset the relative | |
| anchor at every CSV boundary, so that ambiguous case is rejected. | |
| """ | |
| if len(rows) == 1: | |
| return self._load_camera_pose(rows[0], target_t_lat) | |
| if self.camera_pose_paths is None: | |
| raise ValueError( | |
| "consecutive_rows>1 with camera pose requires per-player canonical camera_pose_path1..P containing absolute c2w" | |
| ) | |
| segments = [] | |
| reference_scale = None | |
| for row, t_lat in zip(rows, latent_lengths): | |
| payloads = [ | |
| self._load_camera_pose_file(paths[row]) | |
| for paths in self.camera_pose_paths | |
| ] | |
| if not all((isinstance(payload, dict) for payload in payloads)): | |
| raise ValueError( | |
| "consecutive_rows>1 cannot concatenate legacy relative camera12; use canonical per-player absolute c2w sidecars" | |
| ) | |
| self._camera12_from_per_video_absolute(row, payloads) | |
| scales = [ | |
| float( | |
| self._camera_payload_tensor(payload, "pos_scale", path=paths[row]) | |
| .reshape(-1)[0] | |
| .item() | |
| ) | |
| for payload, paths in zip(payloads, self.camera_pose_paths) | |
| ] | |
| if len(set(scales)) != 1: | |
| raise ValueError( | |
| f"camera pos_scale differs between players at CSV row {row}: {scales}" | |
| ) | |
| if reference_scale is None: | |
| reference_scale = scales[0] | |
| elif scales[0] != reference_scale: | |
| raise ValueError( | |
| f"camera pos_scale differs across consecutive rows: {reference_scale} vs {scales[0]}" | |
| ) | |
| c2w = torch.stack( | |
| [ | |
| self._camera_payload_tensor(payload, "c2w_abs", path=paths[row]) | |
| for payload, paths in zip(payloads, self.camera_pose_paths) | |
| ], | |
| dim=0, | |
| ) | |
| if c2w.shape[1] < t_lat: | |
| raise ValueError( | |
| f"camera latent T={c2w.shape[1]} shorter than row latent T={t_lat} at CSV row {row}" | |
| ) | |
| segments.append(c2w[:, :t_lat]) | |
| absolute = torch.cat(segments, dim=1) | |
| if absolute.shape[1] < target_t_lat: | |
| raise ValueError( | |
| f"combined camera latent T={absolute.shape[1]} shorter than target {target_t_lat}" | |
| ) | |
| return _camera12_from_absolute_c2w( | |
| absolute[:, :target_t_lat], float(reference_scale) | |
| ).contiguous() | |
| def _load_group_rows( | |
| self, rows: list[int], latent_lengths: list[int], target_t_lat: int | |
| ) -> dict: | |
| parts = [ | |
| self._load_group_labels(row, t_lat) | |
| for row, t_lat in zip(rows, latent_lengths) | |
| ] | |
| keys = set(parts[0]) | |
| if any((set(part) != keys for part in parts[1:])): | |
| raise ValueError("group tensor keys differ across consecutive CSV rows") | |
| result = {key: torch.cat([part[key] for part in parts], dim=0) for key in keys} | |
| for key, tensor in result.items(): | |
| if tensor.shape[0] < target_t_lat: | |
| raise ValueError( | |
| f"combined {key} T={tensor.shape[0]} shorter than target latent T={target_t_lat}" | |
| ) | |
| result[key] = tensor[:target_t_lat].contiguous() | |
| return result | |
| def __getitem__(self, index): | |
| for _ in range(20): | |
| sample_index = index % self.n_rows | |
| row = self.sample_start_rows[sample_index] | |
| if self.use_group_labels and any( | |
| r not in self.prepared_group_rows | |
| for r in range(row, row + self.consecutive_rows) | |
| ): | |
| raise ValueError(f"Grouping was not prepared for sample {sample_index}") | |
| try: | |
| rows = list(range(row, row + self.consecutive_rows)) | |
| row_latents = [ | |
| [ | |
| torch.load(paths[r], weights_only=True, map_location="cpu") | |
| for paths in self.latent_paths | |
| ] | |
| for r in rows | |
| ] | |
| row_actions = [ | |
| [self._load_action(paths[r]) for paths in self.action_paths] | |
| for r in rows | |
| ] | |
| latent_lengths = [] | |
| for segment_idx, ( | |
| source_row, | |
| segment_latents, | |
| segment_actions, | |
| ) in enumerate(zip(rows, row_latents, row_actions)): | |
| for pidx, lat in enumerate(segment_latents, start=1): | |
| if lat.dim() != 4 or lat.shape[0] != self.vae_latent_channels: | |
| raise RuntimeError( | |
| f"latent{pidx} shape {tuple(lat.shape)}, expected [{self.vae_latent_channels}, T_lat, h, w]: {self.latent_paths[pidx - 1][source_row]}" | |
| ) | |
| if tuple(lat.shape) != tuple(segment_latents[0].shape): | |
| raise RuntimeError( | |
| f"latent{pidx} shape {tuple(lat.shape)} differs from latent1 {tuple(segment_latents[0].shape)} at CSV row {source_row}" | |
| ) | |
| segment_rgb_t = int(segment_actions[0][0].shape[0]) | |
| expected_latent_t = (segment_rgb_t - 1) // 4 + 1 | |
| first_frame_only = ( | |
| self.allow_first_frame_latents | |
| and int(segment_latents[0].shape[1]) == 1 | |
| ) | |
| if ( | |
| not first_frame_only | |
| and int(segment_latents[0].shape[1]) != expected_latent_t | |
| ): | |
| raise RuntimeError( | |
| f"CSV row {source_row} latent T={segment_latents[0].shape[1]} does not match action T={segment_rgb_t} (expected {expected_latent_t})" | |
| ) | |
| for pidx, (keyboard_i, mouse_i) in enumerate( | |
| segment_actions, start=1 | |
| ): | |
| if keyboard_i.shape != (segment_rgb_t, 23) or mouse_i.shape != ( | |
| segment_rgb_t, | |
| 2, | |
| ): | |
| raise RuntimeError( | |
| f"action{pidx} shape invalid at CSV row {source_row}: keyboard={tuple(keyboard_i.shape)} mouse={tuple(mouse_i.shape)}" | |
| ) | |
| latent_lengths.append(expected_latent_t) | |
| target_t_lat = (self.target_rgb_frames - 1) // 4 + 1 | |
| latents_per_player = [ | |
| torch.cat([segment[pidx] for segment in row_latents], dim=1)[ | |
| :, :target_t_lat | |
| ] | |
| for pidx in range(self.num_players) | |
| ] | |
| keyboards = [ | |
| torch.cat([segment[pidx][0] for segment in row_actions], dim=0)[ | |
| : self.target_rgb_frames | |
| ] | |
| for pidx in range(self.num_players) | |
| ] | |
| mice = [ | |
| torch.cat([segment[pidx][1] for segment in row_actions], dim=0)[ | |
| : self.target_rgb_frames | |
| ] | |
| for pidx in range(self.num_players) | |
| ] | |
| got_T = int(keyboards[0].shape[0]) | |
| if got_T < self.target_rgb_frames: | |
| warnings.warn( | |
| f"[PrecomputedLatentDataset] action 比 target 短:got={got_T} want={self.target_rgb_frames} (row={row}, {self.action_paths[0][row]})。" | |
| ) | |
| for pidx, (kb, ms) in enumerate(zip(keyboards, mice), start=1): | |
| if kb.dim() != 2 or ms.dim() != 2: | |
| raise RuntimeError( | |
| f"action{pidx} 形状异常 keyboard={tuple(kb.shape)} mouse={tuple(ms.shape)}" | |
| ) | |
| if kb.shape[0] != got_T or ms.shape[0] != got_T: | |
| raise RuntimeError( | |
| f"action{pidx} T 与 action1 不一致: keyboard={tuple(kb.shape)} mouse={tuple(ms.shape)}, action1_T={got_T}" | |
| ) | |
| if kb.shape[-1] != 23 or ms.shape[-1] != 2: | |
| raise RuntimeError( | |
| f"action{pidx} feature dim 异常,期望 keyboard[...,23] / mouse[...,2],got {tuple(kb.shape)} / {tuple(ms.shape)}" | |
| ) | |
| exp_T_lat = (got_T - 1) // 4 + 1 | |
| if latents_per_player[0].shape[1] != exp_T_lat and not ( | |
| self.allow_first_frame_latents | |
| and latents_per_player[0].shape[1] == 1 | |
| ): | |
| raise RuntimeError( | |
| f"latent T({latents_per_player[0].shape[1]}) 与 action T({got_T}) 不一致:期望 latent_T=(T-1)//4+1={exp_T_lat}({self.latent_paths[0][row]})" | |
| ) | |
| latents = torch.stack(latents_per_player, dim=0) | |
| keyboard = torch.stack(keyboards, dim=0) | |
| mouse = torch.stack(mice, dim=0) | |
| data = { | |
| "latents": latents, | |
| "keyboard": keyboard, | |
| "mouse": mouse, | |
| "source_rows": torch.tensor(rows, dtype=torch.long), | |
| } | |
| if self.num_players == 2: | |
| data["latents1"] = latents[0] | |
| data["latents2"] = latents[1] | |
| if self.use_camera_pose: | |
| camera_pose = self._load_camera_pose_rows( | |
| rows, latent_lengths, exp_T_lat | |
| ) | |
| if camera_pose is None: | |
| raise RuntimeError( | |
| "use_camera_pose=True 但 CSV 没有可读取的 camera_pose_path/camera12_path" | |
| ) | |
| data["camera_pose"] = camera_pose | |
| if self.use_group_labels: | |
| data.update( | |
| self._load_group_rows(rows, latent_lengths, exp_T_lat) | |
| ) | |
| if self.prompt_emb_paths is not None: | |
| data["prompt_emb"] = _load_prompt_emb_many( | |
| [paths[row] for paths in self.prompt_emb_paths] | |
| ) | |
| else: | |
| data["prompt_emb"] = {} | |
| if self.captions is not None: | |
| data["prompt"] = [caps[row] for caps in self.captions] | |
| return data | |
| except Exception as e: | |
| print( | |
| f"ERROR WHEN LOADING (precomputed) sample={sample_index} source_row={row}: {e}" | |
| ) | |
| if self.debug_no_retry: | |
| raise | |
| if self.use_group_labels: | |
| index = self.grouping_sample_indices[ | |
| random.randrange(len(self.grouping_sample_indices)) | |
| ] | |
| else: | |
| index = random.randrange(self.n_rows) | |
| raise RuntimeError("PrecomputedLatentDataset: 连续加载失败") | |
| _CAMERA_PLAYER_PART = re.compile("(?:^|/)player=(\\d+)(?:/|$)") | |