""" Motion data handler for loading and managing SMPL motion pickle files. Handles .pkl files containing dance motion data with SMPL format. """ import pickle import numpy as np from typing import Dict, Optional, Tuple class MotionHandler: """Handles loading and accessing motion data from pickle files.""" def __init__(self, fps: int = 30): """ Initialize motion handler. Args: fps: Frames per second for the motion data (default: 30) """ self.fps = fps self.motion_data: Optional[Dict] = None self.full_pose: Optional[np.ndarray] = None self.num_frames: int = 0 self.duration: float = 0.0 def load_pickle(self, file_bytes: bytes) -> Dict: """ Load motion data from pickle file bytes. Args: file_bytes: Raw bytes from uploaded pickle file Returns: Dictionary containing motion metadata Raises: ValueError: If pickle file format is invalid """ try: # Load pickle data self.motion_data = pickle.loads(file_bytes) # Validate required keys if "full_pose" not in self.motion_data: raise ValueError("Pickle file must contain 'full_pose' field") # Extract full_pose: (num_frames, 24, 3) self.full_pose = self.motion_data["full_pose"] # Validate shape if ( len(self.full_pose.shape) != 3 or self.full_pose.shape[1] != 24 or self.full_pose.shape[2] != 3 ): raise ValueError( f"Invalid full_pose shape: {self.full_pose.shape}. " f"Expected (num_frames, 24, 3)" ) # Calculate metadata self.num_frames = self.full_pose.shape[0] self.duration = self.num_frames / self.fps return { "num_frames": int(self.num_frames), "duration": float(self.duration), "fps": self.fps, "num_joints": 24, "shape": list(self.full_pose.shape), } except pickle.UnpicklingError as e: raise ValueError(f"Failed to unpickle file: {str(e)}") except Exception as e: raise ValueError(f"Error loading motion data: {str(e)}") def get_frame_at_timestamp(self, timestamp: float) -> Optional[np.ndarray]: """ Get joint positions for a specific timestamp. Args: timestamp: Time in seconds Returns: Joint positions array of shape (24, 3) or None if timestamp is out of bounds """ if self.full_pose is None: return None # Calculate frame index frame_idx = int(timestamp * self.fps) # Clamp to valid range if frame_idx < 0: frame_idx = 0 elif frame_idx >= self.num_frames: frame_idx = self.num_frames - 1 # Return frame data: (24, 3) return self.full_pose[frame_idx] def get_frame_by_index(self, frame_idx: int) -> Optional[np.ndarray]: """ Get joint positions for a specific frame index. Args: frame_idx: Frame index (0-based) Returns: Joint positions array of shape (24, 3) or None if invalid index """ if self.full_pose is None or frame_idx < 0 or frame_idx >= self.num_frames: return None return self.full_pose[frame_idx] def is_loaded(self) -> bool: """Check if motion data is loaded.""" return self.full_pose is not None # SMPL Joint Names (24 joints) SMPL_JOINT_NAMES = [ "root", # 0 "lhip", # 1 "rhip", # 2 "belly", # 3 "lknee", # 4 "rknee", # 5 "spine", # 6 "lankle", # 7 "rankle", # 8 "chest", # 9 "ltoes", # 10 "rtoes", # 11 "neck", # 12 "linshoulder", # 13 "rinshoulder", # 14 "head", # 15 "lshoulder", # 16 "rshoulder", # 17 "lelbow", # 18 "relbow", # 19 "lwrist", # 20 "rwrist", # 21 "lhand", # 22 "rhand", # 23 ]