matteuscruz commited on
Commit
df1fde2
·
1 Parent(s): 765cb06

feat: add multi-game selector (all 5 games) with suite fallback

Browse files
Files changed (1) hide show
  1. app.py +76 -84
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
- SUITE = "sonic"
30
- DATASET = "SONIC"
 
 
 
 
 
 
31
 
32
  # ---------------------------------------------------------------------------
33
  # Asset download helpers
34
  # ---------------------------------------------------------------------------
35
 
36
- def _download_suite_checkpoints(suite: str = SUITE) -> Path:
37
- """Download all .pt checkpoint files for a given suite from HF model repo."""
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": ["latent_actions", "lam", "actions", "action_tokenizer"],
68
- "dynamics": ["dynamics"],
69
  }
70
  names = aliases.get(model_name, [model_name])
71
-
72
- candidates = []
73
- for state_file in suite_dir.rglob("model_state_dict.pt"):
74
- parent = state_file.parent
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 directory for '{model_name}' in {suite_dir}")
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 _ensure_dataset_h5(h5_filename: str) -> None:
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} from HF dataset repo…")
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 (loaded once, reused across requests)
105
  # ---------------------------------------------------------------------------
106
 
107
  _model_cache: dict = {}
108
 
109
 
110
- def get_models():
111
- if SUITE in _model_cache:
112
- return _model_cache[SUITE]
113
 
114
- print("Downloading / loading model checkpoints…")
115
- suite_dir = _download_suite_checkpoints(SUITE)
 
 
 
 
 
 
116
 
117
- vt_path = _find_ckpt_dir(suite_dir, "video_tokenizer")
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[SUITE] = (vt, lam, dyn)
124
- print(f"Models loaded on {DEVICE}.")
125
- return _model_cache[SUITE]
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
- if (DATASET, num_frames) in _dataset_cache:
137
- return _dataset_cache[(DATASET, num_frames)]
 
138
 
139
- _ensure_dataset_h5("sonic_frames.h5")
 
140
 
141
  from datasets.data_utils import load_data_and_data_loaders # noqa: PLC0415
142
  _, val_data, _, _, _ = load_data_and_data_loaders(
143
- dataset=DATASET, batch_size=1, num_frames=num_frames, preload_ratio=0.15
144
  )
145
- _dataset_cache[(DATASET, num_frames)] = val_data
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
- print("Loading models…")
177
- video_tokenizer, latent_action_model, dynamics_model = get_models()
 
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) # [1, T, C, H, W]
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 = latent_action_model.quantizer(pad_enc)
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 = torch.clamp((gt_frames[:, : gen.shape[1]].detach().cpu() + 1) / 2, 0, 1)
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 = (gt[0, t].permute(1, 2, 0).numpy() * 255).astype(np.uint8)
243
- gn_f = (gen[0, t].permute(1, 2, 0).numpy() * 255).astype(np.uint8)
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 &nbsp;|&nbsp; **Right half**: model predictions
265
 
266
- > First generation takes longer — models and dataset are downloaded on first run.
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, 1 = max diversity)"
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 &nbsp;|&nbsp; **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
  )