Spaces:
Running on Zero
Running on Zero
Commit ·
df1fde2
1
Parent(s): 765cb06
feat: add multi-game selector (all 5 games) with suite fallback
Browse files
app.py
CHANGED
|
@@ -2,10 +2,9 @@
|
|
| 2 |
Gradio Space for TinyWorlds — Autoregressive World Model demo.
|
| 3 |
Deploy at: https://huggingface.co/spaces
|
| 4 |
"""
|
| 5 |
-
import os
|
| 6 |
-
import sys
|
| 7 |
-
import random
|
| 8 |
import re
|
|
|
|
|
|
|
| 9 |
from pathlib import Path
|
| 10 |
|
| 11 |
import cv2
|
|
@@ -25,16 +24,23 @@ MODEL_CACHE_DIR.mkdir(exist_ok=True)
|
|
| 25 |
MODELS_REPO_ID = "AlmondGod/tinyworlds-models"
|
| 26 |
DATASET_REPO_ID = "AlmondGod/tinyworlds"
|
| 27 |
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
|
|
|
|
| 28 |
|
| 29 |
-
|
| 30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
|
| 32 |
# ---------------------------------------------------------------------------
|
| 33 |
# Asset download helpers
|
| 34 |
# ---------------------------------------------------------------------------
|
| 35 |
|
| 36 |
-
def _download_suite_checkpoints(suite: str
|
| 37 |
-
"""Download
|
| 38 |
suite_dir = MODEL_CACHE_DIR / suite
|
| 39 |
suite_dir.mkdir(exist_ok=True)
|
| 40 |
|
|
@@ -42,10 +48,7 @@ def _download_suite_checkpoints(suite: str = SUITE) -> Path:
|
|
| 42 |
suite_files = [f for f in all_files if f.startswith(f"{suite}/") and f.endswith(".pt")]
|
| 43 |
|
| 44 |
if not suite_files:
|
| 45 |
-
raise FileNotFoundError(
|
| 46 |
-
f"No checkpoints found for suite '{suite}' in {MODELS_REPO_ID}. "
|
| 47 |
-
"Make sure the HF model repo has files under the suite folder."
|
| 48 |
-
)
|
| 49 |
|
| 50 |
for fname in suite_files:
|
| 51 |
local = MODEL_CACHE_DIR / fname
|
|
@@ -61,36 +64,29 @@ def _download_suite_checkpoints(suite: str = SUITE) -> Path:
|
|
| 61 |
|
| 62 |
|
| 63 |
def _find_ckpt_dir(suite_dir: Path, model_name: str) -> Path:
|
| 64 |
-
"""Return the checkpoint directory with the highest step number for a model."""
|
| 65 |
aliases = {
|
| 66 |
"video_tokenizer": ["video_tokenizer"],
|
| 67 |
-
"latent_actions":
|
| 68 |
-
"dynamics":
|
| 69 |
}
|
| 70 |
names = aliases.get(model_name, [model_name])
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
if any(n in str(parent) for n in names):
|
| 76 |
-
candidates.append(parent)
|
| 77 |
-
|
| 78 |
if not candidates:
|
| 79 |
-
raise FileNotFoundError(f"No checkpoint
|
| 80 |
-
|
| 81 |
def _step(p: Path) -> int:
|
| 82 |
m = re.search(r"_step_(\d+)", str(p))
|
| 83 |
return int(m.group(1)) if m else 0
|
| 84 |
-
|
| 85 |
return max(candidates, key=_step)
|
| 86 |
|
| 87 |
|
| 88 |
-
def
|
| 89 |
-
"""Download dataset h5 file from HF if not already present."""
|
| 90 |
dest = DATA_DIR / h5_filename
|
| 91 |
if dest.exists():
|
| 92 |
return
|
| 93 |
-
print(f"Downloading {h5_filename}
|
| 94 |
hf_hub_download(
|
| 95 |
repo_id=DATASET_REPO_ID,
|
| 96 |
filename=h5_filename,
|
|
@@ -101,28 +97,34 @@ def _ensure_dataset_h5(h5_filename: str) -> None:
|
|
| 101 |
|
| 102 |
|
| 103 |
# ---------------------------------------------------------------------------
|
| 104 |
-
# Model cache
|
| 105 |
# ---------------------------------------------------------------------------
|
| 106 |
|
| 107 |
_model_cache: dict = {}
|
| 108 |
|
| 109 |
|
| 110 |
-
def get_models():
|
| 111 |
-
if
|
| 112 |
-
return _model_cache[
|
| 113 |
|
| 114 |
-
|
| 115 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
|
| 117 |
-
vt_path
|
| 118 |
lam_path = _find_ckpt_dir(suite_dir, "latent_actions")
|
| 119 |
dyn_path = _find_ckpt_dir(suite_dir, "dynamics")
|
| 120 |
|
| 121 |
from utils.inference_utils import load_models # noqa: PLC0415
|
| 122 |
vt, lam, dyn = load_models(str(vt_path), str(lam_path), str(dyn_path), DEVICE, use_actions=True)
|
| 123 |
-
_model_cache[
|
| 124 |
-
print(f"Models loaded on {DEVICE}.")
|
| 125 |
-
return _model_cache[
|
| 126 |
|
| 127 |
|
| 128 |
# ---------------------------------------------------------------------------
|
|
@@ -132,17 +134,19 @@ def get_models():
|
|
| 132 |
_dataset_cache: dict = {}
|
| 133 |
|
| 134 |
|
| 135 |
-
def get_val_dataset(num_frames: int):
|
| 136 |
-
|
| 137 |
-
|
|
|
|
| 138 |
|
| 139 |
-
|
|
|
|
| 140 |
|
| 141 |
from datasets.data_utils import load_data_and_data_loaders # noqa: PLC0415
|
| 142 |
_, val_data, _, _, _ = load_data_and_data_loaders(
|
| 143 |
-
dataset=
|
| 144 |
)
|
| 145 |
-
_dataset_cache[
|
| 146 |
return val_data
|
| 147 |
|
| 148 |
|
|
@@ -150,37 +154,21 @@ def get_val_dataset(num_frames: int):
|
|
| 150 |
# Inference
|
| 151 |
# ---------------------------------------------------------------------------
|
| 152 |
|
| 153 |
-
def _frames_to_mp4(frames_01: torch.Tensor, path: str, fps: int = 4) -> None:
|
| 154 |
-
"""Save [1, T, C, H, W] float tensor (values in [0,1]) as an MP4 file."""
|
| 155 |
-
B, T, C, H, W = frames_01.shape
|
| 156 |
-
fourcc = cv2.VideoWriter_fourcc(*"mp4v")
|
| 157 |
-
writer = cv2.VideoWriter(path, fourcc, fps, (W, H))
|
| 158 |
-
for t in range(T):
|
| 159 |
-
frame = (frames_01[0, t].permute(1, 2, 0).numpy() * 255).astype(np.uint8)
|
| 160 |
-
if frame.shape[2] == 1:
|
| 161 |
-
frame = np.repeat(frame, 3, axis=2)
|
| 162 |
-
writer.write(cv2.cvtColor(frame, cv2.COLOR_RGB2BGR))
|
| 163 |
-
writer.release()
|
| 164 |
-
|
| 165 |
-
|
| 166 |
@spaces.GPU(duration=120)
|
| 167 |
-
def run_inference(
|
| 168 |
-
n_steps: int,
|
| 169 |
-
temperature: float,
|
| 170 |
-
use_random_actions: bool,
|
| 171 |
-
) -> str:
|
| 172 |
context_window = 2
|
| 173 |
prediction_horizon = 1
|
| 174 |
frames_to_load = context_window + n_steps * prediction_horizon
|
| 175 |
|
| 176 |
-
|
| 177 |
-
|
|
|
|
| 178 |
|
| 179 |
-
print("Loading dataset sample…")
|
| 180 |
-
val_data = get_val_dataset(frames_to_load)
|
| 181 |
|
| 182 |
idx = random.randint(0, len(val_data) - 1)
|
| 183 |
-
gt_frames = val_data[idx][0].unsqueeze(0).to(DEVICE)
|
| 184 |
|
| 185 |
context_frames = gt_frames[:, :context_window]
|
| 186 |
generated_frames = context_frames.clone()
|
|
@@ -190,7 +178,7 @@ def run_inference(
|
|
| 190 |
|
| 191 |
with torch.no_grad():
|
| 192 |
for i in range(n_steps):
|
| 193 |
-
print(f"Generating frame {i + 1}/{n_steps}…")
|
| 194 |
|
| 195 |
ctx = generated_frames[:, -context_window:]
|
| 196 |
video_indices = video_tokenizer.tokenize(ctx)
|
|
@@ -199,17 +187,15 @@ def run_inference(
|
|
| 199 |
action_latent = None
|
| 200 |
if use_random_actions:
|
| 201 |
from utils.inference_utils import sample_random_action # noqa: PLC0415
|
| 202 |
-
|
| 203 |
action_idx = sample_random_action(n_actions)
|
| 204 |
inferred_actions.append(action_idx)
|
| 205 |
recent = inferred_actions[-context_window:]
|
| 206 |
recent_t = repeat(torch.tensor(recent, device=DEVICE), "i -> 1 i")
|
| 207 |
action_latent = latent_action_model.quantizer.get_latents_from_indices(recent_t)
|
| 208 |
-
|
| 209 |
if len(recent) < context_window:
|
| 210 |
pad_len = context_window - len(recent) + 1
|
| 211 |
pad_enc = latent_action_model.encode(ctx[:, :pad_len])
|
| 212 |
-
pad_q
|
| 213 |
action_latent = torch.cat([pad_q, action_latent], dim=1)
|
| 214 |
|
| 215 |
def idx_to_latents(idx):
|
|
@@ -223,24 +209,23 @@ def run_inference(
|
|
| 223 |
conditioning=action_latent,
|
| 224 |
temperature=temperature,
|
| 225 |
)
|
| 226 |
-
|
| 227 |
next_frames = video_tokenizer.detokenize(next_latents)
|
| 228 |
-
generated_frames = torch.cat(
|
| 229 |
-
[generated_frames, next_frames[:, -prediction_horizon:]], dim=1
|
| 230 |
-
)
|
| 231 |
-
|
| 232 |
-
print("Rendering output…")
|
| 233 |
|
|
|
|
| 234 |
gen = torch.clamp((generated_frames.detach().cpu() + 1) / 2, 0, 1)
|
| 235 |
-
gt
|
| 236 |
|
| 237 |
B, T, C, H, W = gen.shape
|
|
|
|
|
|
|
|
|
|
|
|
|
| 238 |
fourcc = cv2.VideoWriter_fourcc(*"mp4v")
|
| 239 |
-
out_path = str(BASE_DIR / "demo_output.mp4")
|
| 240 |
writer = cv2.VideoWriter(out_path, fourcc, 4, (W * 2, H))
|
| 241 |
for t in range(T):
|
| 242 |
-
gt_f
|
| 243 |
-
gn_f
|
| 244 |
combined = np.concatenate([gt_f, gn_f], axis=1)
|
| 245 |
writer.write(cv2.cvtColor(combined, cv2.COLOR_RGB2BGR))
|
| 246 |
writer.release()
|
|
@@ -253,26 +238,33 @@ def run_inference(
|
|
| 253 |
# Gradio UI
|
| 254 |
# ---------------------------------------------------------------------------
|
| 255 |
|
|
|
|
|
|
|
| 256 |
with gr.Blocks(title="TinyWorlds — World Model Demo") as demo:
|
| 257 |
gr.Markdown(
|
| 258 |
"""
|
| 259 |
# TinyWorlds — Autoregressive World Model
|
| 260 |
|
| 261 |
A minimal implementation of [Google DeepMind's Genie architecture](https://arxiv.org/pdf/2402.15391).
|
| 262 |
-
The model learns to predict future game frames **without any action labels** — actions are inferred unsupervised.
|
| 263 |
|
| 264 |
**Left half**: ground truth | **Right half**: model predictions
|
| 265 |
|
| 266 |
-
> First
|
| 267 |
"""
|
| 268 |
)
|
| 269 |
|
| 270 |
with gr.Row():
|
| 271 |
with gr.Column(scale=1):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 272 |
n_steps = gr.Slider(2, 15, value=8, step=1, label="Frames to generate")
|
| 273 |
temperature = gr.Slider(
|
| 274 |
0.0, 1.0, value=0.5, step=0.05,
|
| 275 |
-
label="Temperature (0 = deterministic
|
| 276 |
)
|
| 277 |
use_actions = gr.Checkbox(value=True, label="Condition on random actions")
|
| 278 |
btn = gr.Button("Generate", variant="primary", size="lg")
|
|
@@ -285,7 +277,7 @@ The model learns to predict future game frames **without any action labels** —
|
|
| 285 |
|
| 286 |
btn.click(
|
| 287 |
fn=run_inference,
|
| 288 |
-
inputs=[n_steps, temperature, use_actions],
|
| 289 |
outputs=video_out,
|
| 290 |
api_name=False,
|
| 291 |
)
|
|
|
|
| 2 |
Gradio Space for TinyWorlds — Autoregressive World Model demo.
|
| 3 |
Deploy at: https://huggingface.co/spaces
|
| 4 |
"""
|
|
|
|
|
|
|
|
|
|
| 5 |
import re
|
| 6 |
+
import random
|
| 7 |
+
import tempfile
|
| 8 |
from pathlib import Path
|
| 9 |
|
| 10 |
import cv2
|
|
|
|
| 24 |
MODELS_REPO_ID = "AlmondGod/tinyworlds-models"
|
| 25 |
DATASET_REPO_ID = "AlmondGod/tinyworlds"
|
| 26 |
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
|
| 27 |
+
FALLBACK_SUITE = "sonic"
|
| 28 |
|
| 29 |
+
# Per-game config: dataset key → (suite name on HF, h5 filename, display label)
|
| 30 |
+
GAME_CONFIG: dict[str, dict] = {
|
| 31 |
+
"SONIC": {"suite": "sonic", "h5": "sonic_frames.h5", "label": "Sonic the Hedgehog"},
|
| 32 |
+
"PONG": {"suite": "pong", "h5": "pong_frames.h5", "label": "Pong"},
|
| 33 |
+
"ZELDA": {"suite": "zelda", "h5": "zelda_frames.h5", "label": "Zelda OoT 2D"},
|
| 34 |
+
"POLE_POSITION": {"suite": "pole_position","h5": "pole_position_frames.h5","label": "Pole Position"},
|
| 35 |
+
"PICODOOM": {"suite": "picodoom", "h5": "picodoom_frames.h5", "label": "PicoDoom"},
|
| 36 |
+
}
|
| 37 |
|
| 38 |
# ---------------------------------------------------------------------------
|
| 39 |
# Asset download helpers
|
| 40 |
# ---------------------------------------------------------------------------
|
| 41 |
|
| 42 |
+
def _download_suite_checkpoints(suite: str) -> Path:
|
| 43 |
+
"""Download .pt files for a suite from HF. Returns suite_dir."""
|
| 44 |
suite_dir = MODEL_CACHE_DIR / suite
|
| 45 |
suite_dir.mkdir(exist_ok=True)
|
| 46 |
|
|
|
|
| 48 |
suite_files = [f for f in all_files if f.startswith(f"{suite}/") and f.endswith(".pt")]
|
| 49 |
|
| 50 |
if not suite_files:
|
| 51 |
+
raise FileNotFoundError(f"No checkpoints for suite '{suite}' in {MODELS_REPO_ID}")
|
|
|
|
|
|
|
|
|
|
| 52 |
|
| 53 |
for fname in suite_files:
|
| 54 |
local = MODEL_CACHE_DIR / fname
|
|
|
|
| 64 |
|
| 65 |
|
| 66 |
def _find_ckpt_dir(suite_dir: Path, model_name: str) -> Path:
|
|
|
|
| 67 |
aliases = {
|
| 68 |
"video_tokenizer": ["video_tokenizer"],
|
| 69 |
+
"latent_actions": ["latent_actions", "lam", "actions", "action_tokenizer"],
|
| 70 |
+
"dynamics": ["dynamics"],
|
| 71 |
}
|
| 72 |
names = aliases.get(model_name, [model_name])
|
| 73 |
+
candidates = [
|
| 74 |
+
f.parent for f in suite_dir.rglob("model_state_dict.pt")
|
| 75 |
+
if any(n in str(f.parent) for n in names)
|
| 76 |
+
]
|
|
|
|
|
|
|
|
|
|
| 77 |
if not candidates:
|
| 78 |
+
raise FileNotFoundError(f"No checkpoint for '{model_name}' in {suite_dir}")
|
|
|
|
| 79 |
def _step(p: Path) -> int:
|
| 80 |
m = re.search(r"_step_(\d+)", str(p))
|
| 81 |
return int(m.group(1)) if m else 0
|
|
|
|
| 82 |
return max(candidates, key=_step)
|
| 83 |
|
| 84 |
|
| 85 |
+
def _ensure_h5(h5_filename: str) -> None:
|
|
|
|
| 86 |
dest = DATA_DIR / h5_filename
|
| 87 |
if dest.exists():
|
| 88 |
return
|
| 89 |
+
print(f"Downloading {h5_filename}…")
|
| 90 |
hf_hub_download(
|
| 91 |
repo_id=DATASET_REPO_ID,
|
| 92 |
filename=h5_filename,
|
|
|
|
| 97 |
|
| 98 |
|
| 99 |
# ---------------------------------------------------------------------------
|
| 100 |
+
# Model cache
|
| 101 |
# ---------------------------------------------------------------------------
|
| 102 |
|
| 103 |
_model_cache: dict = {}
|
| 104 |
|
| 105 |
|
| 106 |
+
def get_models(suite: str):
|
| 107 |
+
if suite in _model_cache:
|
| 108 |
+
return _model_cache[suite]
|
| 109 |
|
| 110 |
+
# Try game-specific suite first, fall back to sonic
|
| 111 |
+
actual_suite = suite
|
| 112 |
+
try:
|
| 113 |
+
suite_dir = _download_suite_checkpoints(suite)
|
| 114 |
+
except FileNotFoundError:
|
| 115 |
+
print(f"No checkpoints for '{suite}', falling back to '{FALLBACK_SUITE}'")
|
| 116 |
+
actual_suite = FALLBACK_SUITE
|
| 117 |
+
suite_dir = _download_suite_checkpoints(FALLBACK_SUITE)
|
| 118 |
|
| 119 |
+
vt_path = _find_ckpt_dir(suite_dir, "video_tokenizer")
|
| 120 |
lam_path = _find_ckpt_dir(suite_dir, "latent_actions")
|
| 121 |
dyn_path = _find_ckpt_dir(suite_dir, "dynamics")
|
| 122 |
|
| 123 |
from utils.inference_utils import load_models # noqa: PLC0415
|
| 124 |
vt, lam, dyn = load_models(str(vt_path), str(lam_path), str(dyn_path), DEVICE, use_actions=True)
|
| 125 |
+
_model_cache[suite] = (vt, lam, dyn)
|
| 126 |
+
print(f"Models for '{actual_suite}' loaded on {DEVICE}.")
|
| 127 |
+
return _model_cache[suite]
|
| 128 |
|
| 129 |
|
| 130 |
# ---------------------------------------------------------------------------
|
|
|
|
| 134 |
_dataset_cache: dict = {}
|
| 135 |
|
| 136 |
|
| 137 |
+
def get_val_dataset(game: str, num_frames: int):
|
| 138 |
+
key = (game, num_frames)
|
| 139 |
+
if key in _dataset_cache:
|
| 140 |
+
return _dataset_cache[key]
|
| 141 |
|
| 142 |
+
h5_file = GAME_CONFIG[game]["h5"]
|
| 143 |
+
_ensure_h5(h5_file)
|
| 144 |
|
| 145 |
from datasets.data_utils import load_data_and_data_loaders # noqa: PLC0415
|
| 146 |
_, val_data, _, _, _ = load_data_and_data_loaders(
|
| 147 |
+
dataset=game, batch_size=1, num_frames=num_frames, preload_ratio=0.15
|
| 148 |
)
|
| 149 |
+
_dataset_cache[key] = val_data
|
| 150 |
return val_data
|
| 151 |
|
| 152 |
|
|
|
|
| 154 |
# Inference
|
| 155 |
# ---------------------------------------------------------------------------
|
| 156 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
@spaces.GPU(duration=120)
|
| 158 |
+
def run_inference(game: str, n_steps: int, temperature: float, use_random_actions: bool) -> str:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
context_window = 2
|
| 160 |
prediction_horizon = 1
|
| 161 |
frames_to_load = context_window + n_steps * prediction_horizon
|
| 162 |
|
| 163 |
+
suite = GAME_CONFIG[game]["suite"]
|
| 164 |
+
print(f"[{game}] Loading models (suite={suite})…")
|
| 165 |
+
video_tokenizer, latent_action_model, dynamics_model = get_models(suite)
|
| 166 |
|
| 167 |
+
print(f"[{game}] Loading dataset sample…")
|
| 168 |
+
val_data = get_val_dataset(game, frames_to_load)
|
| 169 |
|
| 170 |
idx = random.randint(0, len(val_data) - 1)
|
| 171 |
+
gt_frames = val_data[idx][0].unsqueeze(0).to(DEVICE)
|
| 172 |
|
| 173 |
context_frames = gt_frames[:, :context_window]
|
| 174 |
generated_frames = context_frames.clone()
|
|
|
|
| 178 |
|
| 179 |
with torch.no_grad():
|
| 180 |
for i in range(n_steps):
|
| 181 |
+
print(f"[{game}] Generating frame {i + 1}/{n_steps}…")
|
| 182 |
|
| 183 |
ctx = generated_frames[:, -context_window:]
|
| 184 |
video_indices = video_tokenizer.tokenize(ctx)
|
|
|
|
| 187 |
action_latent = None
|
| 188 |
if use_random_actions:
|
| 189 |
from utils.inference_utils import sample_random_action # noqa: PLC0415
|
|
|
|
| 190 |
action_idx = sample_random_action(n_actions)
|
| 191 |
inferred_actions.append(action_idx)
|
| 192 |
recent = inferred_actions[-context_window:]
|
| 193 |
recent_t = repeat(torch.tensor(recent, device=DEVICE), "i -> 1 i")
|
| 194 |
action_latent = latent_action_model.quantizer.get_latents_from_indices(recent_t)
|
|
|
|
| 195 |
if len(recent) < context_window:
|
| 196 |
pad_len = context_window - len(recent) + 1
|
| 197 |
pad_enc = latent_action_model.encode(ctx[:, :pad_len])
|
| 198 |
+
pad_q = latent_action_model.quantizer(pad_enc)
|
| 199 |
action_latent = torch.cat([pad_q, action_latent], dim=1)
|
| 200 |
|
| 201 |
def idx_to_latents(idx):
|
|
|
|
| 209 |
conditioning=action_latent,
|
| 210 |
temperature=temperature,
|
| 211 |
)
|
|
|
|
| 212 |
next_frames = video_tokenizer.detokenize(next_latents)
|
| 213 |
+
generated_frames = torch.cat([generated_frames, next_frames[:, -prediction_horizon:]], dim=1)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
|
| 215 |
+
print("Rendering…")
|
| 216 |
gen = torch.clamp((generated_frames.detach().cpu() + 1) / 2, 0, 1)
|
| 217 |
+
gt = torch.clamp((gt_frames[:, :gen.shape[1]].detach().cpu() + 1) / 2, 0, 1)
|
| 218 |
|
| 219 |
B, T, C, H, W = gen.shape
|
| 220 |
+
tmp = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False)
|
| 221 |
+
out_path = tmp.name
|
| 222 |
+
tmp.close()
|
| 223 |
+
|
| 224 |
fourcc = cv2.VideoWriter_fourcc(*"mp4v")
|
|
|
|
| 225 |
writer = cv2.VideoWriter(out_path, fourcc, 4, (W * 2, H))
|
| 226 |
for t in range(T):
|
| 227 |
+
gt_f = (gt [0, t].permute(1, 2, 0).numpy() * 255).astype(np.uint8)
|
| 228 |
+
gn_f = (gen[0, t].permute(1, 2, 0).numpy() * 255).astype(np.uint8)
|
| 229 |
combined = np.concatenate([gt_f, gn_f], axis=1)
|
| 230 |
writer.write(cv2.cvtColor(combined, cv2.COLOR_RGB2BGR))
|
| 231 |
writer.release()
|
|
|
|
| 238 |
# Gradio UI
|
| 239 |
# ---------------------------------------------------------------------------
|
| 240 |
|
| 241 |
+
GAME_CHOICES = [(cfg["label"], key) for key, cfg in GAME_CONFIG.items()]
|
| 242 |
+
|
| 243 |
with gr.Blocks(title="TinyWorlds — World Model Demo") as demo:
|
| 244 |
gr.Markdown(
|
| 245 |
"""
|
| 246 |
# TinyWorlds — Autoregressive World Model
|
| 247 |
|
| 248 |
A minimal implementation of [Google DeepMind's Genie architecture](https://arxiv.org/pdf/2402.15391).
|
| 249 |
+
The model learns to predict future game frames **without any action labels** — actions are inferred unsupervised from video alone.
|
| 250 |
|
| 251 |
**Left half**: ground truth | **Right half**: model predictions
|
| 252 |
|
| 253 |
+
> First run downloads models + dataset (~1–2 GB). Subsequent runs use cache.
|
| 254 |
"""
|
| 255 |
)
|
| 256 |
|
| 257 |
with gr.Row():
|
| 258 |
with gr.Column(scale=1):
|
| 259 |
+
game_selector = gr.Radio(
|
| 260 |
+
choices=GAME_CHOICES,
|
| 261 |
+
value="SONIC",
|
| 262 |
+
label="Game",
|
| 263 |
+
)
|
| 264 |
n_steps = gr.Slider(2, 15, value=8, step=1, label="Frames to generate")
|
| 265 |
temperature = gr.Slider(
|
| 266 |
0.0, 1.0, value=0.5, step=0.05,
|
| 267 |
+
label="Temperature (0 = deterministic · 1 = max diversity)",
|
| 268 |
)
|
| 269 |
use_actions = gr.Checkbox(value=True, label="Condition on random actions")
|
| 270 |
btn = gr.Button("Generate", variant="primary", size="lg")
|
|
|
|
| 277 |
|
| 278 |
btn.click(
|
| 279 |
fn=run_inference,
|
| 280 |
+
inputs=[game_selector, n_steps, temperature, use_actions],
|
| 281 |
outputs=video_out,
|
| 282 |
api_name=False,
|
| 283 |
)
|