| """ |
| Public VoxelMind inference — no Model.py / Affectors.py required. |
| |
| Load the TorchScript artifact exported with scripts/export_scripted.py: |
| python scripts/export_scripted.py |
| """ |
|
|
| from __future__ import annotations |
|
|
| import argparse |
| from pathlib import Path |
| from typing import Sequence |
|
|
| import cv2 |
| import numpy as np |
| import torch |
| import torch.nn.functional as F |
|
|
| TARGET_FPS = 22 |
| TARGET_SIZE = (64, 64) |
|
|
| CLASS_NAMES: list[str] = [ |
| "s", |
| "d", |
| "idle", |
| "mouse_up", |
| "jump", |
| "mouse_right", |
| "mouse_down", |
| "w", |
| "a", |
| "mouse_left", |
| "drop", |
| ] |
|
|
|
|
| def frame_to_gray(frame_bgr: np.ndarray, target_size: tuple[int, int] = TARGET_SIZE) -> np.ndarray: |
| resized = cv2.resize(frame_bgr, target_size, interpolation=cv2.INTER_AREA) |
| return cv2.cvtColor(resized, cv2.COLOR_BGR2GRAY) |
|
|
|
|
| def read_video_gray(path: str | Path, target_size: tuple[int, int] = TARGET_SIZE) -> list[np.ndarray]: |
| cap = cv2.VideoCapture(str(path)) |
| frames: list[np.ndarray] = [] |
| while cap.isOpened(): |
| ok, frame = cap.read() |
| if not ok: |
| break |
| frames.append(frame_to_gray(frame, target_size)) |
| cap.release() |
| if not frames: |
| raise ValueError(f"no frames in video: {path}") |
| return frames |
|
|
|
|
| def crop_temporal(frames: Sequence[np.ndarray], target_fps: int = TARGET_FPS) -> list[np.ndarray]: |
| n = len(frames) |
| if n >= target_fps: |
| start = (n - target_fps) // 2 |
| return list(frames[start : start + target_fps]) |
| padded = list(frames) |
| while len(padded) < target_fps: |
| padded.append(frames[-1]) |
| return padded[:target_fps] |
|
|
|
|
| def frames_to_tensor( |
| frames_gray: Sequence[np.ndarray], |
| target_fps: int = TARGET_FPS, |
| target_size: tuple[int, int] = TARGET_SIZE, |
| ) -> torch.Tensor: |
| video = torch.stack([torch.from_numpy(f) for f in frames_gray]).float() / 255.0 |
| video = video.unsqueeze(0).unsqueeze(0) |
| video = F.interpolate( |
| video, |
| size=(len(frames_gray), target_size[0], target_size[1]), |
| mode="trilinear", |
| align_corners=False, |
| ) |
| return video.squeeze(0) |
|
|
|
|
| def load_model(path: str | Path, device: str | torch.device = "cpu") -> torch.jit.ScriptModule: |
| model = torch.jit.load(str(path), map_location=device) |
| model.eval() |
| return model |
|
|
|
|
| def predict_tensor( |
| model: torch.jit.ScriptModule, |
| clip: torch.Tensor, |
| device: str | torch.device = "cpu", |
| topk: int = 3, |
| ) -> list[tuple[str, float]]: |
| """clip: [1, 22, 64, 64] or [1, 1, 22, 64, 64]""" |
| if clip.dim() == 4: |
| clip = clip.unsqueeze(0) |
| clip = clip.to(device) |
| with torch.inference_mode(): |
| logits = model(clip) |
| probs = torch.softmax(logits, dim=-1).squeeze(0) |
| k = min(topk, probs.numel()) |
| values, indices = probs.topk(k) |
| return [(CLASS_NAMES[i], float(v)) for v, i in zip(values, indices)] |
|
|
|
|
| def predict_video( |
| model: torch.jit.ScriptModule, |
| video_path: str | Path, |
| device: str | torch.device = "cpu", |
| topk: int = 3, |
| ) -> list[tuple[str, float]]: |
| frames = crop_temporal(read_video_gray(video_path)) |
| clip = frames_to_tensor(frames) |
| return predict_tensor(model, clip, device=device, topk=topk) |
|
|
|
|
| def main() -> None: |
| p = argparse.ArgumentParser(description="VoxelMind inference (TorchScript, no source model code)") |
| p.add_argument("--model", default="models/voxel_scripted.pt", help="TorchScript artifact") |
| p.add_argument("--video", required=True, help="Input .mp4 clip") |
| p.add_argument("--device", default="cpu", help="cpu | cuda") |
| p.add_argument("--topk", type=int, default=3) |
| args = p.parse_args() |
|
|
| model = load_model(args.model, args.device) |
| preds = predict_video(model, args.video, device=args.device, topk=args.topk) |
| for name, prob in preds: |
| print(f"{name:12s} {prob * 100:5.1f}%") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|