LeafCat79 commited on
Commit
cfa1bcb
·
verified ·
1 Parent(s): ce21919

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +220 -85
app.py CHANGED
@@ -1,51 +1,130 @@
1
  """
2
- Text-to-Game Generator - Groq Edition (Truly Free)
3
- - Code : llama-3.1-8b-instant via Groq API (free, rate-limited per minute not monthly)
4
- - Images: canvas-drawn graphics only (zero API calls, zero quota, zero cost)
5
 
6
- No HF token needed. No local models. No compilation. No OOM.
7
- Just add GROQ_API_KEY as a Space secret.
8
-
9
- Get a free Groq key (no credit card) at: console.groq.com
10
- Settings > Variables and secrets > New secret > Name: GROQ_API_KEY
11
  """
12
 
13
- import os
14
  import re
 
15
  import base64
16
  import traceback
17
 
 
 
18
  import gradio as gr
19
- from openai import OpenAI
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
 
21
  # ---------------------------------------------------------------------------
22
- # Groq client
23
  # ---------------------------------------------------------------------------
24
 
25
- CODE_MODEL = "llama-3.1-8b-instant"
26
- GROQ_API_KEY = os.environ.get("GROQ_API_KEY", "")
27
 
 
 
 
 
 
 
 
 
 
28
 
29
- def _check_key():
30
- if not GROQ_API_KEY:
31
- raise ValueError(
32
- "GROQ_API_KEY is not set.\n"
33
- "1. Sign up free (no credit card) at console.groq.com\n"
34
- "2. Create an API key\n"
35
- "3. Add it as a Space secret: Settings > Variables and secrets > Name: GROQ_API_KEY"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  )
37
 
 
 
 
 
 
38
 
39
- def get_client():
40
- _check_key()
41
- return OpenAI(
42
- base_url="https://api.groq.com/openai/v1",
43
- api_key=GROQ_API_KEY,
44
- )
45
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
 
47
  # ---------------------------------------------------------------------------
48
- # Game type configs — all use canvas drawing, no image files
49
  # ---------------------------------------------------------------------------
50
 
51
  GAME_TYPES = {
@@ -60,9 +139,10 @@ GAME_TYPES = {
60
  "- At least 5 platforms, 2 moving enemies, and a goal to reach.\n"
61
  "- Show score/lives on the canvas.\n"
62
  "- Use requestAnimationFrame for the game loop.\n"
63
- "- Draw ALL graphics using canvas 2D shapes and colors (fillRect, arc, fillStyle, etc).\n"
64
- "- Make colors, shapes and labels vividly match the theme: {theme}.\n"
65
- "- NO image files, NO external libraries, NO CDN links.\n"
 
66
  "Output ONLY the raw HTML. No explanation, no markdown fences."
67
  ),
68
  },
@@ -78,9 +158,9 @@ GAME_TYPES = {
78
  "- Display health, score, and wave on canvas.\n"
79
  "- Game-over screen with score and restart button.\n"
80
  "- Use requestAnimationFrame for the game loop.\n"
81
- "- Draw ALL graphics using canvas 2D shapes and colors only.\n"
82
- "- Make colors, shapes and labels vividly match the theme: {theme}.\n"
83
- "- NO image files, NO external libraries, NO CDN links.\n"
84
  "Output ONLY the raw HTML. No explanation, no markdown fences."
85
  ),
86
  },
@@ -95,9 +175,9 @@ GAME_TYPES = {
95
  "- At least 15x10 tile grid, collectible keys, and a locked exit.\n"
96
  "- Show a move counter or timer.\n"
97
  "- Win screen when player collects all keys and exits.\n"
98
- "- Draw ALL graphics using canvas 2D shapes and colors only.\n"
99
- "- Make colors, shapes and labels vividly match the theme: {theme}.\n"
100
- "- NO image files, NO external libraries, NO CDN links.\n"
101
  "Output ONLY the raw HTML. No explanation, no markdown fences."
102
  ),
103
  },
@@ -112,9 +192,9 @@ GAME_TYPES = {
112
  "- Collision ends the game; show time survived as score.\n"
113
  "- High score stored in a JS variable; restart button on game-over screen.\n"
114
  "- Use requestAnimationFrame for the game loop.\n"
115
- "- Draw ALL graphics using canvas 2D shapes and colors only.\n"
116
- "- Make colors, shapes and labels vividly match the theme: {theme}.\n"
117
- "- NO image files, NO external libraries, NO CDN links.\n"
118
  "Output ONLY the raw HTML. No explanation, no markdown fences."
119
  ),
120
  },
@@ -130,9 +210,9 @@ GAME_TYPES = {
130
  "- Clear win/lose conditions and a score display.\n"
131
  "- Game-over / win screen with a restart button.\n"
132
  "- Use requestAnimationFrame for the game loop.\n"
133
- "- Draw ALL graphics using canvas 2D shapes and colors only.\n"
134
- "- Make colors, shapes and labels vividly match the theme: {theme}.\n"
135
- "- NO image files, NO external libraries, NO CDN links.\n"
136
  "Output ONLY the raw HTML. No explanation, no markdown fences."
137
  ),
138
  },
@@ -149,7 +229,66 @@ THEME_EXAMPLES = {
149
  }
150
 
151
  # ---------------------------------------------------------------------------
152
- # Code generation
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
  # ---------------------------------------------------------------------------
154
 
155
  def generate_game_code(game_type: str, theme: str, temperature: float, max_new_tokens: int):
@@ -160,43 +299,41 @@ def generate_game_code(game_type: str, theme: str, temperature: float, max_new_t
160
  user_prompt = template.format(theme=theme.strip())
161
  system_msg = (
162
  "You are an expert HTML5 game developer. "
163
- "Write a complete, working, single-file HTML5 game using only canvas 2D drawing. "
164
- "Never use image files or external resources. "
165
- "Draw everything with canvas shapes, colors, and text. "
166
  "Output ONLY the raw HTML - no markdown fences, no explanation."
167
  )
 
168
 
169
  try:
170
- client = get_client()
171
- completion = client.chat.completions.create(
172
- model=CODE_MODEL,
173
- messages=[
174
- {"role": "system", "content": system_msg},
175
- {"role": "user", "content": user_prompt},
176
- ],
177
- max_tokens=int(max_new_tokens),
178
- temperature=float(temperature),
179
- )
180
- code = completion.choices[0].message.content.strip()
181
  code = re.sub(r"^```[a-zA-Z]*\n?", "", code).strip()
182
  code = re.sub(r"\n?```$", "", code).strip()
183
  if "<html" not in code.lower() and "<!doctype" not in code.lower():
184
  code = _wrap_in_html(code, theme)
185
 
186
- status = "Done! Game generated. Click Launch Game to play."
187
- return code, status, _build_preview(code)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
 
189
  except Exception as exc:
190
  traceback.print_exc()
191
- err = str(exc)
192
- if "invalid_api_key" in err.lower() or "401" in err:
193
- err = "Invalid GROQ_API_KEY. Check your key at console.groq.com."
194
- elif "429" in err or "rate" in err.lower():
195
- err = "Rate limited by Groq - wait a few seconds and try again."
196
- else:
197
- err = "Error: " + str(exc)
198
- return "", err, _placeholder_html(err)
199
-
200
 
201
  # ---------------------------------------------------------------------------
202
  # HTML helpers
@@ -237,7 +374,6 @@ def launch_game(code: str) -> str:
237
  return _placeholder_html("No game code yet - generate a game first.")
238
  return _build_preview(code)
239
 
240
-
241
  # ---------------------------------------------------------------------------
242
  # UI helpers
243
  # ---------------------------------------------------------------------------
@@ -249,21 +385,21 @@ def update_type_description(game_type: str) -> str:
249
  def get_first_theme(game_type: str) -> str:
250
  return THEME_EXAMPLES[game_type][0][0]
251
 
252
-
253
  # ---------------------------------------------------------------------------
254
  # Gradio UI
255
  # ---------------------------------------------------------------------------
256
 
257
  def build_ui():
258
- with gr.Blocks(title="Game Generator - Free") as demo:
259
 
260
  gr.Markdown(
261
- "# Game Generator (Truly Free)\n"
262
- "**Llama-3.1-8B** via Groq generates complete playable games instantly.\n"
263
- "All graphics are drawn with canvas — no image API, no quota, no cost.\n\n"
264
- "> **Setup:** Sign up free at [console.groq.com](https://console.groq.com) "
265
- "(no credit card needed), create an API key, then add it as a Space secret "
266
- "named `GROQ_API_KEY`."
 
267
  )
268
 
269
  with gr.Row():
@@ -297,11 +433,11 @@ def build_ui():
297
  label="Temperature - higher = more creative",
298
  )
299
  max_tokens_slider = gr.Slider(
300
- minimum=1000, maximum=6000, value=4000, step=500,
301
- label="Max tokens - more = longer / more complete game",
302
  )
303
 
304
- generate_btn = gr.Button("Generate Game", variant="primary")
305
  gen_status = gr.Markdown(value="_No game generated yet._")
306
 
307
  with gr.Column(scale=2, min_width=500):
@@ -309,7 +445,7 @@ def build_ui():
309
  gr.Markdown("## 3. Generated code (editable)")
310
 
311
  code_box = gr.Code(
312
- label="HTML source",
313
  language="html",
314
  lines=12,
315
  interactive=True,
@@ -336,15 +472,14 @@ def build_ui():
336
 
337
  gr.Markdown(
338
  "---\n"
339
- "**Why Groq is truly free:** Groq uses per-minute rate limits, not monthly credit quotas. "
340
- "If you hit a limit, wait 60 seconds — not 30 days. "
341
- "Games use only canvas drawing so no image API is ever called. "
342
  "Edit the HTML and click **Launch Game** to hot-reload."
343
  )
344
 
345
  return demo
346
 
347
-
348
  # ---------------------------------------------------------------------------
349
  # Entry point
350
  # ---------------------------------------------------------------------------
 
1
  """
2
+ Text-to-Game Generator - ZeroGPU Edition
3
+ - Code : Qwen/Qwen2.5-Coder-3B-Instruct (transformers, ZeroGPU)
4
+ - Images: deepseek-community/Janus-Pro-1B (transformers, ZeroGPU)
5
 
6
+ Space hardware: set to ZeroGPU in Space settings (free for public Spaces).
7
+ Models load to CPU at startup, GPU is borrowed on-demand via @spaces.GPU.
8
+ No API keys needed. No quota. Fully local.
 
 
9
  """
10
 
 
11
  import re
12
+ import io
13
  import base64
14
  import traceback
15
 
16
+ import torch
17
+ import spaces
18
  import gradio as gr
19
+ from transformers import AutoTokenizer, AutoModelForCausalLM
20
+ from transformers import JanusForConditionalGeneration, JanusProcessor
21
+ from PIL import Image
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # Load code model at startup (CPU)
25
+ # ---------------------------------------------------------------------------
26
+
27
+ CODE_MODEL_ID = "Qwen/Qwen2.5-Coder-3B-Instruct"
28
+
29
+ print("Loading Qwen2.5-Coder-3B tokenizer...")
30
+ code_tokenizer = AutoTokenizer.from_pretrained(CODE_MODEL_ID)
31
+ print("Loading Qwen2.5-Coder-3B model...")
32
+ code_model = AutoModelForCausalLM.from_pretrained(
33
+ CODE_MODEL_ID,
34
+ torch_dtype=torch.bfloat16,
35
+ device_map="cpu",
36
+ )
37
+ print("Code model ready.")
38
 
39
  # ---------------------------------------------------------------------------
40
+ # Load Janus-Pro-1B at startup (CPU)
41
  # ---------------------------------------------------------------------------
42
 
43
+ IMAGE_MODEL_ID = "deepseek-community/Janus-Pro-1B"
 
44
 
45
+ print("Loading Janus-Pro-1B processor...")
46
+ janus_processor = JanusProcessor.from_pretrained(IMAGE_MODEL_ID)
47
+ print("Loading Janus-Pro-1B model...")
48
+ janus_model = JanusForConditionalGeneration.from_pretrained(
49
+ IMAGE_MODEL_ID,
50
+ torch_dtype=torch.bfloat16,
51
+ device_map="cpu",
52
+ )
53
+ print("Image model ready.")
54
 
55
+ # ---------------------------------------------------------------------------
56
+ # Code generation — ZeroGPU
57
+ # ---------------------------------------------------------------------------
58
+
59
+ @spaces.GPU
60
+ def run_code_model(prompt: str, max_new_tokens: int, temperature: float) -> str:
61
+ """Generate HTML game code using Qwen2.5-Coder-3B on GPU."""
62
+ device = "cuda"
63
+ code_model.to(device)
64
+
65
+ messages = [{"role": "user", "content": prompt}]
66
+ inputs = code_tokenizer.apply_chat_template(
67
+ messages,
68
+ add_generation_prompt=True,
69
+ tokenize=True,
70
+ return_dict=True,
71
+ return_tensors="pt",
72
+ ).to(device)
73
+
74
+ with torch.no_grad():
75
+ outputs = code_model.generate(
76
+ **inputs,
77
+ max_new_tokens=int(max_new_tokens),
78
+ temperature=float(temperature),
79
+ do_sample=True,
80
+ top_p=0.95,
81
  )
82
 
83
+ generated = outputs[0][inputs["input_ids"].shape[-1]:]
84
+ result = code_tokenizer.decode(generated, skip_special_tokens=True)
85
+ code_model.to("cpu")
86
+ torch.cuda.empty_cache()
87
+ return result
88
 
89
+ # ---------------------------------------------------------------------------
90
+ # Image generation — ZeroGPU
91
+ # ---------------------------------------------------------------------------
 
 
 
92
 
93
+ @spaces.GPU
94
+ def run_image_model(prompt: str) -> Image.Image:
95
+ """Generate a sprite image using Janus-Pro-1B on GPU."""
96
+ device = "cuda"
97
+ janus_model.to(device)
98
+
99
+ messages = [{"role": "user", "content": [{"type": "text", "text": prompt}]}]
100
+ text_prompt = janus_processor.apply_chat_template(messages, add_generation_prompt=True)
101
+ inputs = janus_processor(
102
+ text=text_prompt,
103
+ generation_mode="image",
104
+ return_tensors="pt",
105
+ ).to(device, dtype=torch.bfloat16)
106
+
107
+ janus_model.generation_config.num_return_sequences = 1
108
+
109
+ with torch.no_grad():
110
+ outputs = janus_model.generate(
111
+ **inputs,
112
+ generation_mode="image",
113
+ do_sample=True,
114
+ use_cache=True,
115
+ )
116
+
117
+ decoded = janus_model.decode_image_tokens(outputs)
118
+ images = janus_processor.postprocess(
119
+ list(decoded.float()), return_tensors="PIL.Image.Image"
120
+ )
121
+ pil_img = images["pixel_values"][0]
122
+ janus_model.to("cpu")
123
+ torch.cuda.empty_cache()
124
+ return pil_img
125
 
126
  # ---------------------------------------------------------------------------
127
+ # Game type configs
128
  # ---------------------------------------------------------------------------
129
 
130
  GAME_TYPES = {
 
139
  "- At least 5 platforms, 2 moving enemies, and a goal to reach.\n"
140
  "- Show score/lives on the canvas.\n"
141
  "- Use requestAnimationFrame for the game loop.\n"
142
+ "- For every image asset use: const img = new Image(); img.src = 'sprite_NAME.png';\n"
143
+ " e.g. sprite_player.png, sprite_enemy.png, sprite_background.png. Max 3 sprites.\n"
144
+ "- No external libraries or CDN links.\n"
145
+ "- Theme all visuals to match: {theme}.\n"
146
  "Output ONLY the raw HTML. No explanation, no markdown fences."
147
  ),
148
  },
 
158
  "- Display health, score, and wave on canvas.\n"
159
  "- Game-over screen with score and restart button.\n"
160
  "- Use requestAnimationFrame for the game loop.\n"
161
+ "- For every image asset: const img = new Image(); img.src = 'sprite_NAME.png'; Max 3 sprites.\n"
162
+ "- No external libraries or CDN links.\n"
163
+ "- Theme everything to match: {theme}.\n"
164
  "Output ONLY the raw HTML. No explanation, no markdown fences."
165
  ),
166
  },
 
175
  "- At least 15x10 tile grid, collectible keys, and a locked exit.\n"
176
  "- Show a move counter or timer.\n"
177
  "- Win screen when player collects all keys and exits.\n"
178
+ "- For every image asset: const img = new Image(); img.src = 'sprite_NAME.png'; Max 3 sprites.\n"
179
+ "- No external libraries or CDN links.\n"
180
+ "- Theme everything to match: {theme}.\n"
181
  "Output ONLY the raw HTML. No explanation, no markdown fences."
182
  ),
183
  },
 
192
  "- Collision ends the game; show time survived as score.\n"
193
  "- High score stored in a JS variable; restart button on game-over screen.\n"
194
  "- Use requestAnimationFrame for the game loop.\n"
195
+ "- For every image asset: const img = new Image(); img.src = 'sprite_NAME.png'; Max 3 sprites.\n"
196
+ "- No external libraries or CDN links.\n"
197
+ "- Theme everything to match: {theme}.\n"
198
  "Output ONLY the raw HTML. No explanation, no markdown fences."
199
  ),
200
  },
 
210
  "- Clear win/lose conditions and a score display.\n"
211
  "- Game-over / win screen with a restart button.\n"
212
  "- Use requestAnimationFrame for the game loop.\n"
213
+ "- For every image asset: const img = new Image(); img.src = 'sprite_NAME.png'; Max 3 sprites.\n"
214
+ "- No external libraries or CDN links.\n"
215
+ "- Theme everything vividly to match: {theme}.\n"
216
  "Output ONLY the raw HTML. No explanation, no markdown fences."
217
  ),
218
  },
 
229
  }
230
 
231
  # ---------------------------------------------------------------------------
232
+ # Sprite helpers
233
+ # ---------------------------------------------------------------------------
234
+
235
+ def _pil_to_data_uri(pil_image: Image.Image, size: int = 64) -> str:
236
+ img = pil_image.resize((size, size), Image.LANCZOS)
237
+ buf = io.BytesIO()
238
+ img.save(buf, format="PNG")
239
+ return "data:image/png;base64," + base64.b64encode(buf.getvalue()).decode()
240
+
241
+
242
+ def _colored_placeholder(name: str) -> str:
243
+ colours = ["#e74c3c", "#3498db", "#2ecc71", "#f39c12", "#9b59b6", "#1abc9c"]
244
+ colour = colours[abs(hash(name)) % len(colours)]
245
+ label = name.replace("sprite_", "")[:6]
246
+ svg = (
247
+ '<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64">'
248
+ '<rect width="64" height="64" fill="' + colour + '" rx="8"/>'
249
+ '<text x="32" y="38" font-size="10" text-anchor="middle" fill="white">' + label + '</text>'
250
+ '</svg>'
251
+ )
252
+ return "data:image/svg+xml;base64," + base64.b64encode(svg.encode()).decode()
253
+
254
+
255
+ def _sprite_prompt(sprite_name: str, theme: str) -> str:
256
+ label = sprite_name.replace("sprite_", "").replace("_", " ")
257
+ return (
258
+ "Pixel-art game sprite of a " + label + " for a " + theme + " themed game. "
259
+ "Vibrant colours, clear silhouette, plain dark background, 64x64 style."
260
+ )
261
+
262
+
263
+ def _find_sprite_filenames(html_code: str) -> list:
264
+ pattern = r"""(?:src\s*=\s*['"]|\.src\s*=\s*['"])\s*(sprite_[a-zA-Z0-9_]+\.png)\s*['"]"""
265
+ return list(dict.fromkeys(re.findall(pattern, html_code)))[:3]
266
+
267
+
268
+ def _inject_sprites(html_code: str, sprite_map: dict) -> str:
269
+ for fname, data_uri in sprite_map.items():
270
+ html_code = html_code.replace('"' + fname + '"', '"' + data_uri + '"')
271
+ html_code = html_code.replace("'" + fname + "'", "'" + data_uri + "'")
272
+ return html_code
273
+
274
+
275
+ def generate_sprites(sprite_names: list, theme: str) -> dict:
276
+ if not sprite_names:
277
+ return {}
278
+ mapping = {}
279
+ for fname in sprite_names:
280
+ name_no_ext = fname.replace(".png", "")
281
+ prompt = _sprite_prompt(name_no_ext, theme)
282
+ try:
283
+ pil_img = run_image_model(prompt)
284
+ mapping[fname] = _pil_to_data_uri(pil_img, size=64)
285
+ except Exception as exc:
286
+ print("[Janus] Failed '" + fname + "': " + str(exc))
287
+ mapping[fname] = _colored_placeholder(name_no_ext)
288
+ return mapping
289
+
290
+ # ---------------------------------------------------------------------------
291
+ # Full generation pipeline
292
  # ---------------------------------------------------------------------------
293
 
294
  def generate_game_code(game_type: str, theme: str, temperature: float, max_new_tokens: int):
 
299
  user_prompt = template.format(theme=theme.strip())
300
  system_msg = (
301
  "You are an expert HTML5 game developer. "
302
+ "Write a complete, working, single-file HTML5 game using canvas and vanilla JavaScript. "
303
+ "Use new Image() with src='sprite_NAME.png' for every visual asset (max 3 sprites). "
 
304
  "Output ONLY the raw HTML - no markdown fences, no explanation."
305
  )
306
+ full_prompt = system_msg + "\n\n" + user_prompt
307
 
308
  try:
309
+ # Step 1: Generate HTML via Qwen2.5-Coder-3B
310
+ code = run_code_model(full_prompt, max_new_tokens, temperature)
 
 
 
 
 
 
 
 
 
311
  code = re.sub(r"^```[a-zA-Z]*\n?", "", code).strip()
312
  code = re.sub(r"\n?```$", "", code).strip()
313
  if "<html" not in code.lower() and "<!doctype" not in code.lower():
314
  code = _wrap_in_html(code, theme)
315
 
316
+ # Step 2: Detect sprites
317
+ sprite_names = _find_sprite_filenames(code)
318
+
319
+ # Step 3: Generate sprites via Janus-Pro-1B
320
+ sprite_map = generate_sprites(sprite_names, theme.strip())
321
+
322
+ # Step 4: Inject base64 images
323
+ final_code = _inject_sprites(code, sprite_map)
324
+
325
+ n_real = sum(1 for v in sprite_map.values() if "image/png" in v)
326
+ n_fallback = len(sprite_map) - n_real
327
+ status = (
328
+ "Done! " + str(len(sprite_names)) + " sprite(s) - "
329
+ + str(n_real) + " by Janus-Pro-1B, "
330
+ + str(n_fallback) + " fallback. Click Launch Game to play."
331
+ )
332
+ return final_code, status, _build_preview(final_code)
333
 
334
  except Exception as exc:
335
  traceback.print_exc()
336
+ return "", "Error: " + str(exc), _placeholder_html("Error: " + str(exc))
 
 
 
 
 
 
 
 
337
 
338
  # ---------------------------------------------------------------------------
339
  # HTML helpers
 
374
  return _placeholder_html("No game code yet - generate a game first.")
375
  return _build_preview(code)
376
 
 
377
  # ---------------------------------------------------------------------------
378
  # UI helpers
379
  # ---------------------------------------------------------------------------
 
385
  def get_first_theme(game_type: str) -> str:
386
  return THEME_EXAMPLES[game_type][0][0]
387
 
 
388
  # ---------------------------------------------------------------------------
389
  # Gradio UI
390
  # ---------------------------------------------------------------------------
391
 
392
  def build_ui():
393
+ with gr.Blocks(title="Game Generator - ZeroGPU") as demo:
394
 
395
  gr.Markdown(
396
+ "# Game Generator (ZeroGPU)\n"
397
+ "**Qwen2.5-Coder-3B** generates the game code. "
398
+ "**Janus-Pro-1B** generates every sprite. "
399
+ "Both run locally on ZeroGPU — no API keys, no quota.\n\n"
400
+ "> Make sure your Space hardware is set to **ZeroGPU** "
401
+ "(Space Settings → Hardware → ZeroGPU). "
402
+ "First run downloads models (~9GB total) — subsequent runs are faster."
403
  )
404
 
405
  with gr.Row():
 
433
  label="Temperature - higher = more creative",
434
  )
435
  max_tokens_slider = gr.Slider(
436
+ minimum=1000, maximum=4000, value=2500, step=500,
437
+ label="Max tokens - more = longer game",
438
  )
439
 
440
+ generate_btn = gr.Button("Generate Game + Sprites", variant="primary")
441
  gen_status = gr.Markdown(value="_No game generated yet._")
442
 
443
  with gr.Column(scale=2, min_width=500):
 
445
  gr.Markdown("## 3. Generated code (editable)")
446
 
447
  code_box = gr.Code(
448
+ label="HTML source (sprites embedded as base64)",
449
  language="html",
450
  lines=12,
451
  interactive=True,
 
472
 
473
  gr.Markdown(
474
  "---\n"
475
+ "**Code:** Qwen2.5-Coder-3B-Instruct (~6.2GB) via transformers + ZeroGPU. "
476
+ "**Images:** Janus-Pro-1B (~3.8GB) via transformers + ZeroGPU. "
477
+ "GPU is borrowed only during generation then released — no idle VRAM usage. "
478
  "Edit the HTML and click **Launch Game** to hot-reload."
479
  )
480
 
481
  return demo
482
 
 
483
  # ---------------------------------------------------------------------------
484
  # Entry point
485
  # ---------------------------------------------------------------------------