LeafCat79 commited on
Commit
7f2f0ed
·
verified ·
1 Parent(s): 561c134

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +67 -160
app.py CHANGED
@@ -1,70 +1,51 @@
1
  """
2
- Text-to-Game Generator
3
- - Code : WithinUsAI/Opus4.7-GODs.Ghost.Codex-4B.GGuF (local GGUF, CPU, ~2.5GB)
4
- - Images: black-forest-labs/FLUX.1-schnell via nscale (free HF Inference API, 0MB disk)
5
 
6
- Total disk: ~2.5GB well within 16GB free Space limit.
7
- No ZeroGPU needed code runs on CPU, images run on HF servers.
8
 
9
- Setup: add HF_TOKEN as a Space secret:
10
- Settings > Variables and secrets > New secret > Name: HF_TOKEN
11
  """
12
 
13
  import os
14
  import re
15
- import io
16
  import base64
17
  import traceback
18
 
19
  import gradio as gr
20
- from llama_cpp import Llama
21
- from huggingface_hub import InferenceClient
22
- from PIL import Image
23
 
24
  # ---------------------------------------------------------------------------
25
- # Code model — loaded locally on CPU at startup
26
  # ---------------------------------------------------------------------------
27
 
28
- CODE_REPO = "WithinUsAI/Opus4.7-GODs.Ghost.Codex-4B.GGuF"
29
- CODE_FILENAME = "Opus4.7-Distill-GODsGhost-Codex-4B-Q4_K_M.gguf"
30
 
31
- print("Loading Opus4.7-GODs.Ghost.Codex-4B...")
32
- from huggingface_hub import hf_hub_download
33
 
34
- _model_path = hf_hub_download(
35
- repo_id=CODE_REPO,
36
- filename=CODE_FILENAME,
37
- )
38
- print("Model cached at:", _model_path)
39
-
40
- code_llm = Llama(
41
- model_path=_model_path,
42
- n_ctx=4096,
43
- n_threads=4,
44
- n_gpu_layers=0,
45
- verbose=False,
46
- )
47
- print("Code model ready.")
48
-
49
- # ---------------------------------------------------------------------------
50
- # Image model — FLUX.1-schnell via nscale (HF Inference API, free)
51
- # ---------------------------------------------------------------------------
52
-
53
- IMAGE_MODEL = "black-forest-labs/FLUX.1-schnell"
54
- HF_TOKEN = os.environ.get("HF_TOKEN", "")
55
-
56
-
57
- def get_image_client():
58
- if not HF_TOKEN:
59
  raise ValueError(
60
- "HF_TOKEN is not set. Add it as a Space secret: "
61
- "Settings > Variables and secrets > New secret > Name: HF_TOKEN"
 
 
62
  )
63
- return InferenceClient(provider="nscale", api_key=HF_TOKEN)
 
 
 
 
 
 
 
64
 
65
 
66
  # ---------------------------------------------------------------------------
67
- # Game type configs
68
  # ---------------------------------------------------------------------------
69
 
70
  GAME_TYPES = {
@@ -79,10 +60,9 @@ GAME_TYPES = {
79
  "- At least 5 platforms, 2 moving enemies, and a goal to reach.\n"
80
  "- Show score/lives on the canvas.\n"
81
  "- Use requestAnimationFrame for the game loop.\n"
82
- "- For every image asset use: const img = new Image(); img.src = 'sprite_NAME.png';\n"
83
- " e.g. sprite_player.png, sprite_enemy.png, sprite_background.png. Max 3 sprites.\n"
84
- "- No external libraries or CDN links.\n"
85
- "- Theme all visuals to match: {theme}.\n"
86
  "Output ONLY the raw HTML. No explanation, no markdown fences."
87
  ),
88
  },
@@ -98,9 +78,9 @@ GAME_TYPES = {
98
  "- Display health, score, and wave on canvas.\n"
99
  "- Game-over screen with score and restart button.\n"
100
  "- Use requestAnimationFrame for the game loop.\n"
101
- "- For every image asset: const img = new Image(); img.src = 'sprite_NAME.png'; Max 3 sprites.\n"
102
- "- No external libraries or CDN links.\n"
103
- "- Theme everything to match: {theme}.\n"
104
  "Output ONLY the raw HTML. No explanation, no markdown fences."
105
  ),
106
  },
@@ -115,9 +95,9 @@ GAME_TYPES = {
115
  "- At least 15x10 tile grid, collectible keys, and a locked exit.\n"
116
  "- Show a move counter or timer.\n"
117
  "- Win screen when player collects all keys and exits.\n"
118
- "- For every image asset: const img = new Image(); img.src = 'sprite_NAME.png'; Max 3 sprites.\n"
119
- "- No external libraries or CDN links.\n"
120
- "- Theme everything to match: {theme}.\n"
121
  "Output ONLY the raw HTML. No explanation, no markdown fences."
122
  ),
123
  },
@@ -132,9 +112,9 @@ GAME_TYPES = {
132
  "- Collision ends the game; show time survived as score.\n"
133
  "- High score stored in a JS variable; restart button on game-over screen.\n"
134
  "- Use requestAnimationFrame for the game loop.\n"
135
- "- For every image asset: const img = new Image(); img.src = 'sprite_NAME.png'; Max 3 sprites.\n"
136
- "- No external libraries or CDN links.\n"
137
- "- Theme everything to match: {theme}.\n"
138
  "Output ONLY the raw HTML. No explanation, no markdown fences."
139
  ),
140
  },
@@ -150,9 +130,9 @@ GAME_TYPES = {
150
  "- Clear win/lose conditions and a score display.\n"
151
  "- Game-over / win screen with a restart button.\n"
152
  "- Use requestAnimationFrame for the game loop.\n"
153
- "- For every image asset: const img = new Image(); img.src = 'sprite_NAME.png'; Max 3 sprites.\n"
154
- "- No external libraries or CDN links.\n"
155
- "- Theme everything vividly to match: {theme}.\n"
156
  "Output ONLY the raw HTML. No explanation, no markdown fences."
157
  ),
158
  },
@@ -168,67 +148,6 @@ THEME_EXAMPLES = {
168
  "Surprise Me!": [["A sentient library that rearranges itself"], ["Deep sea bioluminescent creatures"], ["Retro space diner on a comet"]],
169
  }
170
 
171
- # ---------------------------------------------------------------------------
172
- # Image helpers
173
- # ---------------------------------------------------------------------------
174
-
175
- def _pil_to_data_uri(pil_image: Image.Image, size: int = 64) -> str:
176
- img = pil_image.resize((size, size), Image.LANCZOS)
177
- buf = io.BytesIO()
178
- img.save(buf, format="PNG")
179
- return "data:image/png;base64," + base64.b64encode(buf.getvalue()).decode()
180
-
181
-
182
- def _colored_placeholder(name: str) -> str:
183
- colours = ["#e74c3c", "#3498db", "#2ecc71", "#f39c12", "#9b59b6", "#1abc9c"]
184
- colour = colours[abs(hash(name)) % len(colours)]
185
- label = name.replace("sprite_", "")[:6]
186
- svg = (
187
- '<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64">'
188
- '<rect width="64" height="64" fill="' + colour + '" rx="8"/>'
189
- '<text x="32" y="38" font-size="10" text-anchor="middle" fill="white">' + label + '</text>'
190
- '</svg>'
191
- )
192
- return "data:image/svg+xml;base64," + base64.b64encode(svg.encode()).decode()
193
-
194
-
195
- def _sprite_prompt(sprite_name: str, theme: str) -> str:
196
- label = sprite_name.replace("sprite_", "").replace("_", " ")
197
- return (
198
- "Pixel-art game sprite: " + label + ". Theme: " + theme + ". "
199
- "Vibrant colours, clear silhouette, plain dark background, 64x64 pixel style."
200
- )
201
-
202
-
203
- def _find_sprite_filenames(html_code: str) -> list:
204
- pattern = r"""(?:src\s*=\s*['"]|\.src\s*=\s*['"])\s*(sprite_[a-zA-Z0-9_]+\.png)\s*['"]"""
205
- return list(dict.fromkeys(re.findall(pattern, html_code)))[:3]
206
-
207
-
208
- def _inject_sprites(html_code: str, sprite_map: dict) -> str:
209
- for fname, data_uri in sprite_map.items():
210
- html_code = html_code.replace('"' + fname + '"', '"' + data_uri + '"')
211
- html_code = html_code.replace("'" + fname + "'", "'" + data_uri + "'")
212
- return html_code
213
-
214
-
215
- def generate_sprites(sprite_names: list, theme: str) -> dict:
216
- if not sprite_names:
217
- return {}
218
- mapping = {}
219
- for fname in sprite_names:
220
- name_no_ext = fname.replace(".png", "")
221
- prompt = _sprite_prompt(name_no_ext, theme)
222
- try:
223
- client = get_image_client()
224
- pil_img = client.text_to_image(prompt, model=IMAGE_MODEL)
225
- mapping[fname] = _pil_to_data_uri(pil_img, size=64)
226
- except Exception as exc:
227
- print("[FLUX] Failed '" + fname + "': " + str(exc))
228
- mapping[fname] = _colored_placeholder(name_no_ext)
229
- return mapping
230
-
231
-
232
  # ---------------------------------------------------------------------------
233
  # Code generation
234
  # ---------------------------------------------------------------------------
@@ -241,14 +160,16 @@ def generate_game_code(game_type: str, theme: str, temperature: float, max_new_t
241
  user_prompt = template.format(theme=theme.strip())
242
  system_msg = (
243
  "You are an expert HTML5 game developer. "
244
- "Write a complete, working, single-file HTML5 game using canvas and vanilla JavaScript. "
245
- "Use new Image() with src='sprite_NAME.png' for every visual asset (max 3 sprites). "
 
246
  "Output ONLY the raw HTML - no markdown fences, no explanation."
247
  )
248
 
249
  try:
250
- # Step 1: Generate HTML via Opus4.7 Codex (local llama-cpp, CPU)
251
- response = code_llm.create_chat_completion(
 
252
  messages=[
253
  {"role": "system", "content": system_msg},
254
  {"role": "user", "content": user_prompt},
@@ -256,37 +177,22 @@ def generate_game_code(game_type: str, theme: str, temperature: float, max_new_t
256
  max_tokens=int(max_new_tokens),
257
  temperature=float(temperature),
258
  )
259
- code = response["choices"][0]["message"]["content"].strip()
260
  code = re.sub(r"^```[a-zA-Z]*\n?", "", code).strip()
261
  code = re.sub(r"\n?```$", "", code).strip()
262
  if "<html" not in code.lower() and "<!doctype" not in code.lower():
263
  code = _wrap_in_html(code, theme)
264
 
265
- # Step 2: Detect sprites (max 3)
266
- sprite_names = _find_sprite_filenames(code)
267
-
268
- # Step 3: Generate sprites via FLUX.1-schnell (nscale, free API)
269
- sprite_map = generate_sprites(sprite_names, theme.strip())
270
-
271
- # Step 4: Inject base64 images into HTML
272
- final_code = _inject_sprites(code, sprite_map)
273
-
274
- n_real = sum(1 for v in sprite_map.values() if "image/png" in v)
275
- n_fallback = len(sprite_map) - n_real
276
- status = (
277
- "Done! " + str(len(sprite_names)) + " sprite(s) - "
278
- + str(n_real) + " by FLUX.1-schnell, "
279
- + str(n_fallback) + " fallback. Click Launch Game to play."
280
- )
281
- return final_code, status, _build_preview(final_code)
282
 
283
  except Exception as exc:
284
  traceback.print_exc()
285
  err = str(exc)
286
- if "401" in err or "unauthorized" in err.lower():
287
- err = "Unauthorized - make sure HF_TOKEN is added as a Space secret."
288
  elif "429" in err or "rate" in err.lower():
289
- err = "Rate limited - wait a moment and try again."
290
  else:
291
  err = "Error: " + str(exc)
292
  return "", err, _placeholder_html(err)
@@ -349,15 +255,15 @@ def get_first_theme(game_type: str) -> str:
349
  # ---------------------------------------------------------------------------
350
 
351
  def build_ui():
352
- with gr.Blocks(title="Game Generator") as demo:
353
 
354
  gr.Markdown(
355
- "# Game Generator\n"
356
- "**Opus4.7-GODs.Ghost.Codex-4B** (local, CPU) writes the game code. "
357
- "**FLUX.1-schnell** (nscale API, free) generates the sprites.\n\n"
358
- "> Add your **HF_TOKEN** as a Space secret "
359
- "(Settings > Variables and secrets > New secret > Name: HF_TOKEN). "
360
- "First run downloads the code model (~2.5GB) — subsequent runs are instant."
361
  )
362
 
363
  with gr.Row():
@@ -391,11 +297,11 @@ def build_ui():
391
  label="Temperature - higher = more creative",
392
  )
393
  max_tokens_slider = gr.Slider(
394
- minimum=1000, maximum=4000, value=2500, step=500,
395
- label="Max tokens - more = longer game",
396
  )
397
 
398
- generate_btn = gr.Button("Generate Game + Sprites", variant="primary")
399
  gen_status = gr.Markdown(value="_No game generated yet._")
400
 
401
  with gr.Column(scale=2, min_width=500):
@@ -403,7 +309,7 @@ def build_ui():
403
  gr.Markdown("## 3. Generated code (editable)")
404
 
405
  code_box = gr.Code(
406
- label="HTML source (sprites embedded as base64)",
407
  language="html",
408
  lines=12,
409
  interactive=True,
@@ -430,8 +336,9 @@ def build_ui():
430
 
431
  gr.Markdown(
432
  "---\n"
433
- "**Code:** Opus4.7-GODs.Ghost.Codex-4B Q4_K_M GGUF (~2.5GB) via llama-cpp on CPU. "
434
- "**Images:** FLUX.1-schnell via nscale provider (free with HF token, 0MB disk). "
 
435
  "Edit the HTML and click **Launch Game** to hot-reload."
436
  )
437
 
 
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
  "- 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
  "- 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
  "- 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
  "- 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
  "- 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
  },
 
148
  "Surprise Me!": [["A sentient library that rearranges itself"], ["Deep sea bioluminescent creatures"], ["Retro space diner on a comet"]],
149
  }
150
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  # ---------------------------------------------------------------------------
152
  # Code generation
153
  # ---------------------------------------------------------------------------
 
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},
 
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)
 
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
  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
  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
 
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