jkorstad commited on
Commit
2e3808d
·
verified ·
1 Parent(s): 7ae53c8

Upload folder using huggingface_hub

Browse files
Files changed (8) hide show
  1. DEPLOY.md +42 -0
  2. Dockerfile +19 -0
  3. README.md +29 -0
  4. app.py +476 -0
  5. requirements.txt +8 -0
  6. static/css/app.css +391 -0
  7. static/index.html +224 -0
  8. static/js/app.js +360 -0
DEPLOY.md ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # GameForge Space - HF Spaces Deployment
2
+ #
3
+ # To deploy this to Hugging Face Spaces:
4
+ #
5
+ # 1. Create a new Space on huggingface.co/new-space
6
+ # - SDK: Gradio
7
+ # - Hardware: ZeroGPU (free quota, 3.5 min/day)
8
+ #
9
+ # 2. Push this space/ directory:
10
+ # huggingface-cli upload jkorstad/gameforge-space ./space/
11
+ #
12
+ # 3. The app will be live at:
13
+ # https://huggingface.co/spaces/jkorstad/gameforge-space
14
+ #
15
+ # What you get:
16
+ # - Custom HTML/JS frontend (not Gradio components)
17
+ # - gradio.Server backend with FastAPI routes
18
+ # - ZeroGPU for model inference (free!)
19
+ # - Queuing and concurrency management
20
+ # - gradio_client API compatibility
21
+ # - MCP tool registration
22
+ # - SSE streaming support
23
+ #
24
+ # API endpoints (all gradio_client compatible):
25
+ # - /registry_info - List all models
26
+ # - /get_route - Get routing for a model
27
+ # - /list_pipelines - List pipeline definitions
28
+ # - /format_prompt - Format prompt with templates
29
+ # - /format_npc - Format NPC dialogue
30
+ # - /generate_image - Generate image via FLUX (ZeroGPU)
31
+ # - /generate_3d - Generate 3D mesh via TRELLIS.2 (ZeroGPU)
32
+ # - /generate_voice - Generate voice via MeloTTS (ZeroGPU)
33
+ # - /generate_video - Generate video via LTX 2.3 (ZeroGPU)
34
+ # - /generate_music - Generate music via ACE-Step (ZeroGPU)
35
+ # - /generate_sfx - Generate SFX via TangoFlux (ZeroGPU)
36
+ # - /list_assets - Browse generated assets
37
+ # - /validate_asset - Validate asset quality
38
+ # - /convert_asset - Convert asset format
39
+ #
40
+ # MCP tools:
41
+ # - gameforge_generate - Generate any asset type
42
+ # - gameforge_list_models - List available models
Dockerfile ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Install system deps for ffmpeg (audio conversion)
6
+ RUN apt-get update && apt-get install -y --no-install-recommends \
7
+ ffmpeg \
8
+ && rm -rf /var/lib/apt/lists/*
9
+
10
+ # Copy and install Python deps
11
+ COPY requirements.txt .
12
+ RUN pip install --no-cache-dir -r requirements.txt
13
+
14
+ # Copy app
15
+ COPY . .
16
+
17
+ EXPOSE 7860
18
+
19
+ CMD ["python", "app.py"]
README.md ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # GameForge Space - HF Spaces Deployment Config
2
+ # ==============================================
3
+ title: GameForge
4
+ emoji: ⚔️
5
+ colorFrom: purple
6
+ colorTo: blue
7
+ sdk: gradio
8
+ sdk_version: 5.25.0
9
+ app_file: app.py
10
+ pinned: false
11
+ license: apache-2.0
12
+ tags:
13
+ - game-development
14
+ - asset-generation
15
+ - ai-pipeline
16
+ - text-to-image
17
+ - text-to-3d
18
+ - text-to-video
19
+ - text-to-speech
20
+ - text-to-music
21
+ - zerogpu
22
+ short_description: "AI Game Asset Pipeline - Generate game assets using free open-source models"
23
+ models:
24
+ - black-forest-labs/FLUX.1-schnell
25
+ - microsoft/TRELLIS.2
26
+ - myshell-ai/MeloTTS
27
+ - Lightricks/LTX-2.3
28
+ - declare-lab/TangoFlux
29
+ - victor/ace-step-jam
app.py ADDED
@@ -0,0 +1,476 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ GameForge Space Backend
3
+ =======================
4
+ HF Space using gradio.Server for custom frontend + ZeroGPU.
5
+ Serves the GameForge web app with queued, concurrent-safe API endpoints.
6
+
7
+ Deploy: push to HF Spaces with ZeroGPU hardware.
8
+ """
9
+
10
+ import os
11
+ import sys
12
+ import json
13
+ import asyncio
14
+ from pathlib import Path
15
+ from typing import Optional, Dict, Any, List
16
+
17
+ import spaces
18
+ from gradio import Server
19
+ from gradio.data_classes import FileData
20
+ from fastapi.responses import HTMLResponse, FileResponse, JSONResponse
21
+ import yaml
22
+
23
+ # Add parent to path for gameforge imports
24
+ sys.path.insert(0, str(Path(__file__).parent.parent))
25
+
26
+ # Import GameForge modules
27
+ from gameforge.config.registry_loader import get_registry
28
+ from gameforge.config.prompts import get_template_for_asset, format_npc_dialogue, list_templates
29
+ from gameforge.engine.router import get_router
30
+ from gameforge.engine.validator import validate_asset
31
+ from gameforge.engine.converter import convert_asset, export_for_engine
32
+
33
+ # Initialize
34
+ registry = get_registry()
35
+ router = get_router()
36
+
37
+ # Storage for generated assets
38
+ STORAGE_DIR = Path("/tmp/gameforge_assets")
39
+ STORAGE_DIR.mkdir(parents=True, exist_ok=True)
40
+
41
+ # Create the Server app
42
+ app = Server()
43
+
44
+
45
+ # ============================================================================
46
+ # Static file serving
47
+ # ============================================================================
48
+
49
+ STATIC_DIR = Path(__file__).parent / "static"
50
+
51
+
52
+ @app.get("/")
53
+ async def homepage():
54
+ """Serve the main frontend."""
55
+ html_path = STATIC_DIR / "index.html"
56
+ if html_path.exists():
57
+ return HTMLResponse(html_path.read_text(encoding="utf-8"))
58
+ return HTMLResponse("<h1>GameForge</h1><p>Frontend not found</p>")
59
+
60
+
61
+ @app.get("/static/{path:path}")
62
+ async def static_files(path: str):
63
+ """Serve static assets (CSS, JS, fonts)."""
64
+ file_path = STATIC_DIR / path
65
+ if file_path.exists() and file_path.is_file():
66
+ media_types = {
67
+ ".css": "text/css",
68
+ ".js": "application/javascript",
69
+ ".png": "image/png",
70
+ ".jpg": "image/jpeg",
71
+ ".svg": "image/svg+xml",
72
+ ".woff2": "font/woff2",
73
+ ".woff": "font/woff",
74
+ ".ttf": "font/ttf",
75
+ }
76
+ ext = file_path.suffix.lower()
77
+ media_type = media_types.get(ext, "application/octet-stream")
78
+ return FileResponse(str(file_path), media_type=media_type)
79
+ return JSONResponse({"error": "Not found"}, status_code=404)
80
+
81
+
82
+ # ============================================================================
83
+ # API Endpoints (gradio_client compatible via @app.api)
84
+ # ============================================================================
85
+
86
+ @app.api()
87
+ def registry_info() -> Dict[str, Any]:
88
+ """Get registry summary and all models."""
89
+ summary = registry.summary()
90
+ models = []
91
+ for asset_type in registry.list_asset_types():
92
+ asset = registry.get_asset(asset_type)
93
+ if asset:
94
+ for variant, model in asset.variants.items():
95
+ models.append({
96
+ "asset_type": asset_type,
97
+ "variant": variant,
98
+ "model": model.model,
99
+ "type": model.type,
100
+ "license": model.license,
101
+ "hardware": model.hardware,
102
+ "status": model.status,
103
+ "free": model.is_free,
104
+ "commercial_safe": model.is_commercial_safe,
105
+ "space_id": model.space_id,
106
+ })
107
+ return {"summary": summary, "models": models}
108
+
109
+
110
+ @app.api()
111
+ def get_route(asset_type: str, variant: str = "primary") -> Dict[str, Any]:
112
+ """Get routing info for a model."""
113
+ decision = router.route(asset_type, variant)
114
+ return decision.to_dict()
115
+
116
+
117
+ @app.api()
118
+ def list_pipelines() -> List[Dict[str, Any]]:
119
+ """List available pipeline definitions."""
120
+ pipes_dir = Path(__file__).parent.parent / "pipelines"
121
+ result = []
122
+ for path in sorted(pipes_dir.glob("*.yaml")):
123
+ try:
124
+ with open(path) as f:
125
+ data = yaml.safe_load(f)
126
+ result.append({
127
+ "name": path.stem,
128
+ "description": data.get("description", ""),
129
+ "version": data.get("version", ""),
130
+ "steps": len(data.get("steps", [])),
131
+ "defaults": data.get("defaults", {}),
132
+ })
133
+ except Exception:
134
+ pass
135
+ return result
136
+
137
+
138
+ @app.api()
139
+ def get_pipeline(name: str) -> Dict[str, Any]:
140
+ """Get full pipeline definition."""
141
+ pipes_dir = Path(__file__).parent.parent / "pipelines"
142
+ for path in pipes_dir.glob(f"{name}.yaml"):
143
+ with open(path) as f:
144
+ return yaml.safe_load(f)
145
+ return {"error": f"Pipeline not found: {name}"}
146
+
147
+
148
+ @app.api()
149
+ def format_prompt(asset_type: str, user_prompt: str, model_family: str = "") -> Dict[str, str]:
150
+ """Format a prompt using game-specific templates."""
151
+ template = get_template_for_asset(asset_type, model_family)
152
+ if template:
153
+ return template.format(user_prompt)
154
+ return {"prompt": user_prompt, "negative_prompt": ""}
155
+
156
+
157
+ @app.api()
158
+ def format_npc(text: str, emotion: str = "neutral", speaker: str = "") -> Dict[str, str]:
159
+ """Format NPC dialogue with emotion."""
160
+ return {"formatted": format_npc_dialogue(text, emotion, speaker)}
161
+
162
+
163
+ @app.api()
164
+ def list_templates_api() -> List[Dict[str, str]]:
165
+ """List all prompt templates."""
166
+ templates = list_templates()
167
+ return [
168
+ {
169
+ "name": t.name,
170
+ "asset_type": t.asset_type,
171
+ "model_family": t.model_family,
172
+ "examples": t.example_prompts[:3],
173
+ }
174
+ for t in templates
175
+ ]
176
+
177
+
178
+ # ============================================================================
179
+ # GPU-accelerated generation endpoints (ZeroGPU)
180
+ # ============================================================================
181
+
182
+ @spaces.GPU(duration=60)
183
+ def _generate_image(prompt: str, negative_prompt: str = "", steps: int = 4) -> str:
184
+ """Generate an image using the HF Inference API."""
185
+ from huggingface_hub import InferenceClient
186
+ import tempfile
187
+
188
+ token = os.environ.get("HF_TOKEN", "")
189
+ if not token:
190
+ for tp in [os.path.expanduser("~/.cache/huggingface/token"),
191
+ os.path.expanduser("~/.huggingface/token")]:
192
+ if os.path.isfile(tp):
193
+ token = open(tp).read().strip()
194
+ break
195
+
196
+ client = InferenceClient(token=token, provider="hf-inference")
197
+ image = client.text_to_image(
198
+ prompt,
199
+ model="black-forest-labs/FLUX.1-schnell",
200
+ negative_prompt=negative_prompt or None,
201
+ num_inference_steps=steps,
202
+ )
203
+
204
+ out_path = str(STORAGE_DIR / f"img_{hash(prompt) % 100000}.png")
205
+ image.save(out_path)
206
+ return out_path
207
+
208
+
209
+ @app.api()
210
+ def generate_image(prompt: str, negative_prompt: str = "", steps: int = 4) -> FileData:
211
+ """Generate an image from text. Returns PNG."""
212
+ out_path = _generate_image(prompt, negative_prompt, steps)
213
+ return FileData(path=out_path)
214
+
215
+
216
+ @spaces.GPU(duration=120)
217
+ def _generate_3d(image_path: str) -> str:
218
+ """Generate 3D mesh from image via TRELLIS.2 Space."""
219
+ from gradio_client import Client
220
+ import shutil
221
+
222
+ client = Client("microsoft/TRELLIS.2")
223
+ result = client.predict(image_path, api_name="/generate")
224
+
225
+ # Result is typically a file path or tuple
226
+ if isinstance(result, tuple):
227
+ mesh_path = result[0]
228
+ elif isinstance(result, str) and os.path.isfile(result):
229
+ mesh_path = result
230
+ else:
231
+ return None
232
+
233
+ out_path = str(STORAGE_DIR / f"mesh_{hash(image_path) % 100000}.glb")
234
+ if isinstance(mesh_path, str) and os.path.isfile(mesh_path):
235
+ shutil.copy2(mesh_path, out_path)
236
+ return out_path
237
+ return None
238
+
239
+
240
+ @app.api()
241
+ def generate_3d(image_path: FileData) -> Optional[FileData]:
242
+ """Generate 3D mesh from an image. Returns GLB."""
243
+ result = _generate_3d(image_path["path"])
244
+ if result:
245
+ return FileData(path=result)
246
+ return None
247
+
248
+
249
+ @spaces.GPU(duration=60)
250
+ def _generate_voice(text: str) -> str:
251
+ """Generate voice via MeloTTS."""
252
+ from gradio_client import Client
253
+ import shutil
254
+
255
+ client = Client("mrfakename/MeloTTS")
256
+ result = client.predict(text, api_name="/synthesize")
257
+
258
+ if isinstance(result, tuple):
259
+ audio_path = result[0]
260
+ elif isinstance(result, str) and os.path.isfile(result):
261
+ audio_path = result
262
+ else:
263
+ return None
264
+
265
+ out_path = str(STORAGE_DIR / f"voice_{hash(text) % 100000}.wav")
266
+ if os.path.isfile(audio_path):
267
+ shutil.copy2(audio_path, out_path)
268
+ return out_path
269
+ return None
270
+
271
+
272
+ @app.api()
273
+ def generate_voice(text: str) -> Optional[FileData]:
274
+ """Generate NPC voice from text. Returns WAV."""
275
+ result = _generate_voice(text)
276
+ if result:
277
+ return FileData(path=result)
278
+ return None
279
+
280
+
281
+ @spaces.GPU(duration=120)
282
+ def _generate_video(prompt: str) -> str:
283
+ """Generate video via LTX-2 Turbo."""
284
+ from gradio_client import Client
285
+ import shutil
286
+
287
+ client = Client("alexnasa/ltx-2-TURBO")
288
+ result = client.predict(prompt, api_name="/generate")
289
+
290
+ if isinstance(result, tuple):
291
+ video_path = result[0]
292
+ elif isinstance(result, str) and os.path.isfile(result):
293
+ video_path = result
294
+ else:
295
+ return None
296
+
297
+ out_path = str(STORAGE_DIR / f"video_{hash(prompt) % 100000}.mp4")
298
+ if os.path.isfile(video_path):
299
+ shutil.copy2(video_path, out_path)
300
+ return out_path
301
+ return None
302
+
303
+
304
+ @app.api()
305
+ def generate_video(prompt: str) -> Optional[FileData]:
306
+ """Generate a video from text. Returns MP4."""
307
+ result = _generate_video(prompt)
308
+ if result:
309
+ return FileData(path=result)
310
+ return None
311
+
312
+
313
+ @spaces.GPU(duration=120)
314
+ def _generate_music(prompt: str) -> str:
315
+ """Generate music via ACE-Step."""
316
+ from gradio_client import Client
317
+ import shutil
318
+
319
+ client = Client("victor/ace-step-jam")
320
+ result = client.predict(prompt, api_name="/predict")
321
+
322
+ if isinstance(result, tuple):
323
+ audio_path = result[0]
324
+ elif isinstance(result, str) and os.path.isfile(result):
325
+ audio_path = result
326
+ else:
327
+ return None
328
+
329
+ out_path = str(STORAGE_DIR / f"music_{hash(prompt) % 100000}.wav")
330
+ if os.path.isfile(audio_path):
331
+ shutil.copy2(audio_path, out_path)
332
+ return out_path
333
+ return None
334
+
335
+
336
+ @app.api()
337
+ def generate_music(prompt: str) -> Optional[FileData]:
338
+ """Generate music from text. Returns WAV."""
339
+ result = _generate_music(prompt)
340
+ if result:
341
+ return FileData(path=result)
342
+ return None
343
+
344
+
345
+ @spaces.GPU(duration=60)
346
+ def _generate_sfx(prompt: str) -> str:
347
+ """Generate sound effect via TangoFlux."""
348
+ from gradio_client import Client
349
+ import shutil
350
+
351
+ client = Client("declare-lab/TangoFlux")
352
+ result = client.predict(prompt, api_name="/predict")
353
+
354
+ if isinstance(result, tuple):
355
+ audio_path = result[0]
356
+ elif isinstance(result, str) and os.path.isfile(result):
357
+ audio_path = result
358
+ else:
359
+ return None
360
+
361
+ out_path = str(STORAGE_DIR / f"sfx_{hash(prompt) % 100000}.wav")
362
+ if os.path.isfile(audio_path):
363
+ shutil.copy2(audio_path, out_path)
364
+ return out_path
365
+ return None
366
+
367
+
368
+ @app.api()
369
+ def generate_sfx(prompt: str) -> Optional[FileData]:
370
+ """Generate sound effect from text. Returns WAV."""
371
+ result = _generate_sfx(prompt)
372
+ if result:
373
+ return FileData(path=result)
374
+ return None
375
+
376
+
377
+ # ============================================================================
378
+ # Asset management
379
+ # ============================================================================
380
+
381
+ @app.api()
382
+ def list_assets(folder: str = "") -> List[Dict[str, Any]]:
383
+ """List generated assets."""
384
+ search_dir = STORAGE_DIR / folder if folder else STORAGE_DIR
385
+ if not search_dir.exists():
386
+ return []
387
+ assets = []
388
+ for f in sorted(search_dir.rglob("*")):
389
+ if f.is_file():
390
+ stat = f.stat()
391
+ assets.append({
392
+ "name": f.name,
393
+ "path": str(f),
394
+ "size": stat.st_size,
395
+ "format": f.suffix.lower(),
396
+ "modified": stat.st_mtime,
397
+ })
398
+ return assets
399
+
400
+
401
+ @app.api()
402
+ def delete_asset(path: str) -> Dict[str, bool]:
403
+ """Delete a generated asset."""
404
+ p = Path(path)
405
+ if p.exists() and p.is_file() and p.is_relative_to(STORAGE_DIR):
406
+ p.unlink()
407
+ return {"deleted": True}
408
+ return {"deleted": False}
409
+
410
+
411
+ @app.api()
412
+ def validate_asset_api(path: str, checks: str = "file_exists,non_empty") -> Dict[str, Any]:
413
+ """Validate an asset file."""
414
+ check_list = [c.strip() for c in checks.split(",")]
415
+ return validate_asset(path, check_list)
416
+
417
+
418
+ @app.api()
419
+ def convert_asset_api(input_path: str, target_format: str, engine: str = "") -> Optional[FileData]:
420
+ """Convert an asset format."""
421
+ eng = engine if engine else None
422
+ result = convert_asset(input_path, target_format, engine=eng)
423
+ if result:
424
+ return FileData(path=result)
425
+ return None
426
+
427
+
428
+ # ============================================================================
429
+ # MCP Tool Registration (for HF Spaces MCP support)
430
+ # ============================================================================
431
+
432
+ @app.mcp.tool()
433
+ def gameforge_generate(
434
+ asset_type: str,
435
+ prompt: str,
436
+ ) -> str:
437
+ """Generate a game asset (image, 3D, voice, music, video, SFX).
438
+
439
+ Args:
440
+ asset_type: Type of asset - "image", "3d", "voice", "music", "video", "sfx"
441
+ prompt: Description of the asset to generate
442
+ """
443
+ generators = {
444
+ "image": _generate_image,
445
+ "3d": lambda p: _generate_3d(p), # Takes image path, would need intermediate
446
+ "voice": _generate_voice,
447
+ "music": _generate_music,
448
+ "video": _generate_video,
449
+ "sfx": _generate_sfx,
450
+ }
451
+ fn = generators.get(asset_type)
452
+ if fn:
453
+ result = fn(prompt)
454
+ return result or "Generation failed"
455
+ return f"Unknown asset type: {asset_type}"
456
+
457
+
458
+ @app.mcp.tool()
459
+ def gameforge_list_models() -> str:
460
+ """List all available AI models for game asset generation."""
461
+ models = []
462
+ for asset_type in registry.list_asset_types():
463
+ asset = registry.get_asset(asset_type)
464
+ if asset:
465
+ for variant, model in asset.variants.items():
466
+ free = "FREE" if model.is_free else "PAID"
467
+ models.append(f"{asset_type}/{variant}: {model.model} [{free}]")
468
+ return "\n".join(models)
469
+
470
+
471
+ # ============================================================================
472
+ # Launch
473
+ # ============================================================================
474
+
475
+ if __name__ == "__main__":
476
+ app.launch(show_error=True)
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ gradio>=5.25.0
2
+ huggingface_hub>=0.28.0
3
+ gradio_client>=1.0.0
4
+ pyyaml>=6.0
5
+ Pillow>=10.0.0
6
+ numpy>=1.24.0
7
+ spaces>=0.30.0
8
+ torch>=2.0.0
static/css/app.css ADDED
@@ -0,0 +1,391 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* GameForge - Custom Frontend Styles */
2
+
3
+ :root {
4
+ --bg-primary: #0f1117;
5
+ --bg-secondary: #1a1d27;
6
+ --bg-tertiary: #242836;
7
+ --bg-hover: #2d3245;
8
+ --text-primary: #e4e7f1;
9
+ --text-secondary: #9ca3bf;
10
+ --text-muted: #6b7394;
11
+ --accent: #6c5ce7;
12
+ --accent-hover: #7d6ff0;
13
+ --success: #00d68f;
14
+ --warning: #ffaa00;
15
+ --danger: #ff6b6b;
16
+ --border: #2d3245;
17
+ --radius: 8px;
18
+ --radius-lg: 12px;
19
+ --shadow: 0 4px 24px rgba(0,0,0,0.3);
20
+ }
21
+
22
+ * { margin: 0; padding: 0; box-sizing: border-box; }
23
+
24
+ body {
25
+ font-family: 'Inter', -apple-system, sans-serif;
26
+ background: var(--bg-primary);
27
+ color: var(--text-primary);
28
+ line-height: 1.6;
29
+ }
30
+
31
+ #app {
32
+ display: flex;
33
+ min-height: 100vh;
34
+ }
35
+
36
+ /* Sidebar */
37
+ .sidebar {
38
+ width: 220px;
39
+ background: var(--bg-secondary);
40
+ border-right: 1px solid var(--border);
41
+ display: flex;
42
+ flex-direction: column;
43
+ padding: 16px 0;
44
+ position: fixed;
45
+ top: 0;
46
+ left: 0;
47
+ bottom: 0;
48
+ z-index: 100;
49
+ }
50
+
51
+ .logo {
52
+ display: flex;
53
+ align-items: center;
54
+ gap: 10px;
55
+ padding: 0 20px 20px;
56
+ border-bottom: 1px solid var(--border);
57
+ margin-bottom: 16px;
58
+ }
59
+
60
+ .logo-icon { font-size: 24px; }
61
+ .logo-text { font-size: 18px; font-weight: 700; color: var(--accent); }
62
+
63
+ .nav { list-style: none; flex: 1; }
64
+
65
+ .nav-item {
66
+ padding: 10px 20px;
67
+ cursor: pointer;
68
+ display: flex;
69
+ align-items: center;
70
+ gap: 10px;
71
+ color: var(--text-secondary);
72
+ transition: all 0.15s;
73
+ font-size: 14px;
74
+ }
75
+
76
+ .nav-item:hover { background: var(--bg-hover); color: var(--text-primary); }
77
+ .nav-item.active { background: var(--bg-tertiary); color: var(--accent); border-left: 3px solid var(--accent); }
78
+
79
+ .nav-icon { font-size: 16px; width: 20px; text-align: center; }
80
+
81
+ .sidebar-footer {
82
+ padding: 16px 20px;
83
+ border-top: 1px solid var(--border);
84
+ }
85
+
86
+ .gpu-status {
87
+ display: flex;
88
+ align-items: center;
89
+ gap: 8px;
90
+ font-size: 12px;
91
+ color: var(--text-muted);
92
+ }
93
+
94
+ .status-dot {
95
+ width: 8px;
96
+ height: 8px;
97
+ border-radius: 50%;
98
+ background: var(--success);
99
+ animation: pulse 2s infinite;
100
+ }
101
+
102
+ @keyframes pulse {
103
+ 0%, 100% { opacity: 1; }
104
+ 50% { opacity: 0.5; }
105
+ }
106
+
107
+ /* Main content */
108
+ .main {
109
+ flex: 1;
110
+ margin-left: 220px;
111
+ padding: 24px 32px;
112
+ max-width: 1200px;
113
+ }
114
+
115
+ .tab-content { display: none; }
116
+ .tab-content.active { display: block; }
117
+
118
+ h1 { font-size: 24px; font-weight: 700; margin-bottom: 24px; }
119
+ h2 { font-size: 18px; font-weight: 600; margin: 24px 0 16px; color: var(--text-secondary); }
120
+
121
+ /* Stats grid */
122
+ .stats-grid {
123
+ display: grid;
124
+ grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
125
+ gap: 16px;
126
+ margin-bottom: 32px;
127
+ }
128
+
129
+ .stat-card {
130
+ background: var(--bg-secondary);
131
+ border: 1px solid var(--border);
132
+ border-radius: var(--radius-lg);
133
+ padding: 20px;
134
+ text-align: center;
135
+ }
136
+
137
+ .stat-value {
138
+ font-size: 32px;
139
+ font-weight: 700;
140
+ color: var(--accent);
141
+ font-family: 'JetBrains Mono', monospace;
142
+ }
143
+
144
+ .stat-label {
145
+ font-size: 13px;
146
+ color: var(--text-muted);
147
+ margin-top: 4px;
148
+ }
149
+
150
+ /* Quick generate */
151
+ .quick-gen-grid {
152
+ display: grid;
153
+ grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
154
+ gap: 12px;
155
+ }
156
+
157
+ .quick-gen-card {
158
+ background: var(--bg-secondary);
159
+ border: 1px solid var(--border);
160
+ border-radius: var(--radius-lg);
161
+ padding: 24px 16px;
162
+ text-align: center;
163
+ cursor: pointer;
164
+ transition: all 0.15s;
165
+ display: flex;
166
+ flex-direction: column;
167
+ align-items: center;
168
+ gap: 8px;
169
+ }
170
+
171
+ .quick-gen-card:hover {
172
+ border-color: var(--accent);
173
+ background: var(--bg-tertiary);
174
+ transform: translateY(-2px);
175
+ }
176
+
177
+ .qg-icon { font-size: 32px; }
178
+ .qg-label { font-size: 14px; font-weight: 500; }
179
+
180
+ /* Generate panel */
181
+ .generate-panel {
182
+ display: grid;
183
+ grid-template-columns: 1fr 1fr;
184
+ gap: 24px;
185
+ }
186
+
187
+ .gen-form { display: flex; flex-direction: column; gap: 16px; }
188
+
189
+ .form-group { display: flex; flex-direction: column; gap: 6px; }
190
+ .form-group label { font-size: 13px; font-weight: 500; color: var(--text-secondary); }
191
+
192
+ input[type="text"], textarea, select {
193
+ background: var(--bg-tertiary);
194
+ border: 1px solid var(--border);
195
+ border-radius: var(--radius);
196
+ padding: 10px 14px;
197
+ color: var(--text-primary);
198
+ font-family: inherit;
199
+ font-size: 14px;
200
+ outline: none;
201
+ transition: border-color 0.15s;
202
+ }
203
+
204
+ input:focus, textarea:focus, select:focus { border-color: var(--accent); }
205
+
206
+ .btn {
207
+ border: none;
208
+ border-radius: var(--radius);
209
+ padding: 10px 20px;
210
+ font-family: inherit;
211
+ font-size: 14px;
212
+ font-weight: 500;
213
+ cursor: pointer;
214
+ transition: all 0.15s;
215
+ }
216
+
217
+ .btn-primary { background: var(--accent); color: white; }
218
+ .btn-primary:hover { background: var(--accent-hover); }
219
+ .btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
220
+ .btn-secondary { background: var(--bg-tertiary); color: var(--text-primary); border: 1px solid var(--border); }
221
+ .btn-secondary:hover { background: var(--bg-hover); }
222
+ .btn-lg { padding: 14px 28px; font-size: 16px; }
223
+
224
+ /* Preview box */
225
+ .preview-box {
226
+ background: var(--bg-secondary);
227
+ border: 1px solid var(--border);
228
+ border-radius: var(--radius-lg);
229
+ min-height: 400px;
230
+ display: flex;
231
+ align-items: center;
232
+ justify-content: center;
233
+ overflow: hidden;
234
+ }
235
+
236
+ .preview-placeholder {
237
+ text-align: center;
238
+ color: var(--text-muted);
239
+ }
240
+
241
+ .preview-placeholder span { font-size: 48px; display: block; margin-bottom: 12px; }
242
+
243
+ .preview-box img, .preview-box video, .preview-box audio {
244
+ max-width: 100%;
245
+ max-height: 100%;
246
+ border-radius: var(--radius);
247
+ }
248
+
249
+ /* Pipeline grid */
250
+ .pipeline-grid {
251
+ display: grid;
252
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
253
+ gap: 16px;
254
+ }
255
+
256
+ .pipeline-card {
257
+ background: var(--bg-secondary);
258
+ border: 1px solid var(--border);
259
+ border-radius: var(--radius-lg);
260
+ padding: 20px;
261
+ }
262
+
263
+ .pipeline-card h3 { font-size: 16px; margin-bottom: 8px; }
264
+ .pipeline-card p { font-size: 13px; color: var(--text-muted); margin-bottom: 12px; }
265
+
266
+ .pipeline-meta {
267
+ display: flex;
268
+ gap: 12px;
269
+ font-size: 12px;
270
+ color: var(--text-secondary);
271
+ }
272
+
273
+ /* Assets grid */
274
+ .assets-toolbar {
275
+ display: flex;
276
+ gap: 12px;
277
+ margin-bottom: 20px;
278
+ }
279
+
280
+ .assets-grid {
281
+ display: grid;
282
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
283
+ gap: 12px;
284
+ }
285
+
286
+ .asset-card {
287
+ background: var(--bg-secondary);
288
+ border: 1px solid var(--border);
289
+ border-radius: var(--radius);
290
+ padding: 12px;
291
+ font-size: 13px;
292
+ }
293
+
294
+ .asset-card .name { font-weight: 500; word-break: break-all; }
295
+ .asset-card .meta { color: var(--text-muted); font-size: 12px; margin-top: 4px; }
296
+
297
+ /* Models table */
298
+ .models-filters {
299
+ display: flex;
300
+ gap: 16px;
301
+ margin-bottom: 16px;
302
+ align-items: center;
303
+ }
304
+
305
+ .models-table {
306
+ width: 100%;
307
+ border-collapse: collapse;
308
+ font-size: 13px;
309
+ }
310
+
311
+ .models-table th {
312
+ text-align: left;
313
+ padding: 10px 12px;
314
+ background: var(--bg-secondary);
315
+ color: var(--text-secondary);
316
+ font-weight: 500;
317
+ border-bottom: 1px solid var(--border);
318
+ }
319
+
320
+ .models-table td {
321
+ padding: 8px 12px;
322
+ border-bottom: 1px solid var(--border);
323
+ }
324
+
325
+ .models-table tr:hover { background: var(--bg-hover); }
326
+
327
+ .badge {
328
+ display: inline-block;
329
+ padding: 2px 8px;
330
+ border-radius: 4px;
331
+ font-size: 11px;
332
+ font-weight: 500;
333
+ }
334
+
335
+ .badge-free { background: rgba(0,214,143,0.15); color: var(--success); }
336
+ .badge-paid { background: rgba(255,170,0,0.15); color: var(--warning); }
337
+ .badge-safe { background: rgba(108,92,231,0.15); color: var(--accent); }
338
+ .badge-check { background: rgba(255,107,107,0.15); color: var(--danger); }
339
+
340
+ /* Batch */
341
+ .batch-form {
342
+ background: var(--bg-secondary);
343
+ border: 1px solid var(--border);
344
+ border-radius: var(--radius-lg);
345
+ padding: 24px;
346
+ margin-bottom: 24px;
347
+ }
348
+
349
+ .batch-results {
350
+ display: grid;
351
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
352
+ gap: 12px;
353
+ }
354
+
355
+ /* Toast */
356
+ #toast-container {
357
+ position: fixed;
358
+ bottom: 24px;
359
+ right: 24px;
360
+ z-index: 1000;
361
+ display: flex;
362
+ flex-direction: column;
363
+ gap: 8px;
364
+ }
365
+
366
+ .toast {
367
+ background: var(--bg-tertiary);
368
+ border: 1px solid var(--border);
369
+ border-radius: var(--radius);
370
+ padding: 12px 20px;
371
+ font-size: 14px;
372
+ animation: slideIn 0.2s ease-out;
373
+ max-width: 400px;
374
+ }
375
+
376
+ .toast-success { border-left: 3px solid var(--success); }
377
+ .toast-error { border-left: 3px solid var(--danger); }
378
+ .toast-info { border-left: 3px solid var(--accent); }
379
+
380
+ @keyframes slideIn {
381
+ from { transform: translateX(100%); opacity: 0; }
382
+ to { transform: translateX(0); opacity: 1; }
383
+ }
384
+
385
+ /* Responsive */
386
+ @media (max-width: 768px) {
387
+ .sidebar { width: 60px; }
388
+ .logo-text, .nav-item span:not(.nav-icon) { display: none; }
389
+ .main { margin-left: 60px; padding: 16px; }
390
+ .generate-panel { grid-template-columns: 1fr; }
391
+ }
static/index.html ADDED
@@ -0,0 +1,224 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>GameForge - AI Game Asset Pipeline</title>
7
+ <link rel="stylesheet" href="/static/css/app.css">
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
10
+ </head>
11
+ <body>
12
+ <div id="app">
13
+ <!-- Sidebar -->
14
+ <nav class="sidebar">
15
+ <div class="logo">
16
+ <span class="logo-icon">⚔️</span>
17
+ <span class="logo-text">GameForge</span>
18
+ </div>
19
+ <ul class="nav">
20
+ <li class="nav-item active" data-tab="dashboard">
21
+ <span class="nav-icon">📊</span> Dashboard
22
+ </li>
23
+ <li class="nav-item" data-tab="generate">
24
+ <span class="nav-icon">✨</span> Generate
25
+ </li>
26
+ <li class="nav-item" data-tab="pipelines">
27
+ <span class="nav-icon">🔗</span> Pipelines
28
+ </li>
29
+ <li class="nav-item" data-tab="assets">
30
+ <span class="nav-icon">📁</span> Assets
31
+ </li>
32
+ <li class="nav-item" data-tab="models">
33
+ <span class="nav-icon">🧠</span> Models
34
+ </li>
35
+ <li class="nav-item" data-tab="batch">
36
+ <span class="nav-icon">📦</span> Batch
37
+ </li>
38
+ </ul>
39
+ <div class="sidebar-footer">
40
+ <div class="gpu-status">
41
+ <span class="status-dot"></span>
42
+ <span>ZeroGPU Ready</span>
43
+ </div>
44
+ </div>
45
+ </nav>
46
+
47
+ <!-- Main Content -->
48
+ <main class="main">
49
+ <!-- Dashboard -->
50
+ <section class="tab-content active" id="tab-dashboard">
51
+ <h1>Dashboard</h1>
52
+ <div class="stats-grid">
53
+ <div class="stat-card">
54
+ <div class="stat-value" id="stat-models">--</div>
55
+ <div class="stat-label">Models Available</div>
56
+ </div>
57
+ <div class="stat-card">
58
+ <div class="stat-value" id="stat-free">--</div>
59
+ <div class="stat-label">Free (ZeroGPU)</div>
60
+ </div>
61
+ <div class="stat-card">
62
+ <div class="stat-value" id="stat-pipelines">--</div>
63
+ <div class="stat-label">Pipelines</div>
64
+ </div>
65
+ <div class="stat-card">
66
+ <div class="stat-value" id="stat-assets">--</div>
67
+ <div class="stat-label">Generated Assets</div>
68
+ </div>
69
+ </div>
70
+ <h2>Quick Generate</h2>
71
+ <div class="quick-gen-grid">
72
+ <button class="quick-gen-card" data-pipeline="character" data-prompt="fantasy knight in silver armor">
73
+ <span class="qg-icon">⚔️</span>
74
+ <span class="qg-label">Character</span>
75
+ </button>
76
+ <button class="quick-gen-card" data-pipeline="prop" data-prompt="enchanted magic sword with glowing runes">
77
+ <span class="qg-icon">🗡️</span>
78
+ <span class="qg-label">Prop</span>
79
+ </button>
80
+ <button class="quick-gen-card" data-pipeline="environment" data-prompt="dark fantasy forest clearing at twilight">
81
+ <span class="qg-icon">🌲</span>
82
+ <span class="qg-label">Environment</span>
83
+ </button>
84
+ <button class="quick-gen-card" data-pipeline="npc_voice" data-prompt="Welcome, traveler. What brings you to our village?">
85
+ <span class="qg-icon">🗣️</span>
86
+ <span class="qg-label">NPC Voice</span>
87
+ </button>
88
+ <button class="quick-gen-card" data-pipeline="audio" data-prompt="epic orchestral battle theme">
89
+ <span class="qg-icon">🎵</span>
90
+ <span class="qg-label">Music</span>
91
+ </button>
92
+ <button class="quick-gen-card" data-pipeline="audio" data-prompt="sword slash whoosh with metallic ring">
93
+ <span class="qg-icon">💥</span>
94
+ <span class="qg-label">SFX</span>
95
+ </button>
96
+ </div>
97
+ </section>
98
+
99
+ <!-- Generate -->
100
+ <section class="tab-content" id="tab-generate">
101
+ <h1>Generate Asset</h1>
102
+ <div class="generate-panel">
103
+ <div class="gen-form">
104
+ <div class="form-group">
105
+ <label>Asset Type</label>
106
+ <select id="gen-type">
107
+ <option value="image">Image (FLUX)</option>
108
+ <option value="3d">3D Model (TRELLIS.2)</option>
109
+ <option value="voice">NPC Voice (MeloTTS)</option>
110
+ <option value="music">Music (ACE-Step)</option>
111
+ <option value="video">Video (LTX 2.3)</option>
112
+ <option value="sfx">Sound Effect (TangoFlux)</option>
113
+ </select>
114
+ </div>
115
+ <div class="form-group">
116
+ <label>Prompt</label>
117
+ <textarea id="gen-prompt" placeholder="Describe your asset..." rows="4"></textarea>
118
+ </div>
119
+ <div class="form-group" id="neg-prompt-group">
120
+ <label>Negative Prompt (optional)</label>
121
+ <input type="text" id="gen-negative" placeholder="blurry, low quality, watermark">
122
+ </div>
123
+ <div class="form-group" id="ref-image-group" style="display:none">
124
+ <label>Reference Image (for 3D)</label>
125
+ <input type="file" id="gen-ref-image" accept="image/*">
126
+ </div>
127
+ <button class="btn btn-primary btn-lg" id="gen-btn">
128
+ <span class="btn-text">Generate</span>
129
+ <span class="btn-loading" style="display:none">Generating...</span>
130
+ </button>
131
+ </div>
132
+ <div class="gen-preview">
133
+ <div class="preview-box" id="gen-preview-box">
134
+ <div class="preview-placeholder">
135
+ <span>✨</span>
136
+ <p>Your generated asset will appear here</p>
137
+ </div>
138
+ </div>
139
+ <div class="gen-result-info" id="gen-result-info" style="display:none">
140
+ <div id="gen-result-meta"></div>
141
+ <button class="btn btn-secondary" id="gen-download">Download</button>
142
+ </div>
143
+ </div>
144
+ </div>
145
+ </section>
146
+
147
+ <!-- Pipelines -->
148
+ <section class="tab-content" id="tab-pipelines">
149
+ <h1>Pipelines</h1>
150
+ <div class="pipeline-grid" id="pipeline-grid">
151
+ <!-- Loaded dynamically -->
152
+ </div>
153
+ </section>
154
+
155
+ <!-- Assets -->
156
+ <section class="tab-content" id="tab-assets">
157
+ <h1>Generated Assets</h1>
158
+ <div class="assets-toolbar">
159
+ <select id="asset-filter">
160
+ <option value="">All Types</option>
161
+ </select>
162
+ <button class="btn btn-secondary" id="asset-refresh">Refresh</button>
163
+ </div>
164
+ <div class="assets-grid" id="assets-grid">
165
+ <!-- Loaded dynamically -->
166
+ </div>
167
+ </section>
168
+
169
+ <!-- Models -->
170
+ <section class="tab-content" id="tab-models">
171
+ <h1>Model Registry</h1>
172
+ <div class="models-filters">
173
+ <select id="model-filter-type">
174
+ <option value="">All Asset Types</option>
175
+ </select>
176
+ <label><input type="checkbox" id="model-filter-free"> Free Only</label>
177
+ <label><input type="checkbox" id="model-filter-safe"> Commercial Safe</label>
178
+ </div>
179
+ <table class="models-table" id="models-table">
180
+ <thead>
181
+ <tr>
182
+ <th>Asset Type</th>
183
+ <th>Variant</th>
184
+ <th>Model</th>
185
+ <th>Type</th>
186
+ <th>License</th>
187
+ <th>Hardware</th>
188
+ <th>Free</th>
189
+ <th>Safe</th>
190
+ </tr>
191
+ </thead>
192
+ <tbody id="models-tbody"></tbody>
193
+ </table>
194
+ </section>
195
+
196
+ <!-- Batch -->
197
+ <section class="tab-content" id="tab-batch">
198
+ <h1>Batch Generator</h1>
199
+ <div class="batch-form">
200
+ <div class="form-group">
201
+ <label>Pipeline</label>
202
+ <select id="batch-pipeline"></select>
203
+ </div>
204
+ <div class="form-group">
205
+ <label>Base Prompt</label>
206
+ <textarea id="batch-prompt" placeholder="enchanted sword with glowing runes" rows="2"></textarea>
207
+ </div>
208
+ <div class="form-group">
209
+ <label>Variants: <span id="batch-count-val">3</span></label>
210
+ <input type="range" id="batch-count" min="1" max="10" value="3">
211
+ </div>
212
+ <button class="btn btn-primary" id="batch-btn">Generate Variants</button>
213
+ </div>
214
+ <div class="batch-results" id="batch-results"></div>
215
+ </section>
216
+ </main>
217
+
218
+ <!-- Toast notifications -->
219
+ <div id="toast-container"></div>
220
+ </div>
221
+
222
+ <script type="module" src="/static/js/app.js"></script>
223
+ </body>
224
+ </html>
static/js/app.js ADDED
@@ -0,0 +1,360 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * GameForge Frontend
3
+ * Custom HTML/CSS/JS app powered by gradio.Server + ZeroGPU.
4
+ */
5
+
6
+ import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
7
+
8
+ // ============================================================================
9
+ // State
10
+ // ============================================================================
11
+
12
+ let client = null;
13
+ let registryData = [];
14
+ let pipelines = [];
15
+
16
+ // ============================================================================
17
+ // Gradio Client Connection
18
+ // ============================================================================
19
+
20
+ async function connect() {
21
+ try {
22
+ client = await Client.connect(window.location.origin);
23
+ toast("Connected to GameForge", "success");
24
+ await loadDashboard();
25
+ } catch (e) {
26
+ toast("Failed to connect: " + e.message, "error");
27
+ }
28
+ }
29
+
30
+ // ============================================================================
31
+ // API Helpers
32
+ // ============================================================================
33
+
34
+ async function api(endpoint, params = {}) {
35
+ if (!client) await connect();
36
+ try {
37
+ const result = await client.predict(endpoint, params);
38
+ return result.data;
39
+ } catch (e) {
40
+ toast(`API error (${endpoint}): ${e.message}`, "error");
41
+ throw e;
42
+ }
43
+ }
44
+
45
+ // ============================================================================
46
+ // Navigation
47
+ // ============================================================================
48
+
49
+ document.querySelectorAll(".nav-item").forEach(item => {
50
+ item.addEventListener("click", () => {
51
+ document.querySelectorAll(".nav-item").forEach(i => i.classList.remove("active"));
52
+ document.querySelectorAll(".tab-content").forEach(t => t.classList.remove("active"));
53
+ item.classList.add("active");
54
+ const tab = document.getElementById("tab-" + item.dataset.tab);
55
+ if (tab) tab.classList.add("active");
56
+
57
+ // Load data for tab
58
+ if (item.dataset.tab === "models") loadModels();
59
+ if (item.dataset.tab === "pipelines") loadPipelines();
60
+ if (item.dataset.tab === "assets") loadAssets();
61
+ if (item.dataset.tab === "batch") loadBatchPipelines();
62
+ });
63
+ });
64
+
65
+ // ============================================================================
66
+ // Dashboard
67
+ // ============================================================================
68
+
69
+ async function loadDashboard() {
70
+ try {
71
+ const [reg, pipes, assets] = await Promise.all([
72
+ api("/registry_info"),
73
+ api("/list_pipelines"),
74
+ api("/list_assets"),
75
+ ]);
76
+
77
+ registryData = reg.models || [];
78
+ pipelines = pipes || [];
79
+
80
+ document.getElementById("stat-models").textContent = registryData.length;
81
+ document.getElementById("stat-free").textContent = registryData.filter(m => m.free).length;
82
+ document.getElementById("stat-pipelines").textContent = pipelines.length;
83
+ document.getElementById("stat-assets").textContent = assets.length;
84
+
85
+ // Quick generate buttons
86
+ document.querySelectorAll(".quick-gen-card").forEach(card => {
87
+ card.addEventListener("click", () => {
88
+ const typeMap = {
89
+ character: "image", prop: "image", environment: "image",
90
+ npc_voice: "voice", audio: "music",
91
+ };
92
+ const type = typeMap[card.dataset.pipeline] || "image";
93
+ document.getElementById("gen-type").value = type;
94
+ document.getElementById("gen-prompt").value = card.dataset.prompt;
95
+ switchTab("generate");
96
+ document.getElementById("gen-btn").click();
97
+ });
98
+ });
99
+ } catch (e) {
100
+ console.error("Dashboard load failed:", e);
101
+ }
102
+ }
103
+
104
+ function switchTab(name) {
105
+ document.querySelector(`.nav-item[data-tab="${name}"]`)?.click();
106
+ }
107
+
108
+ // ============================================================================
109
+ // Generate
110
+ // ============================================================================
111
+
112
+ const genType = document.getElementById("gen-type");
113
+ const genBtn = document.getElementById("gen-btn");
114
+ const genPrompt = document.getElementById("gen-prompt");
115
+ const genPreview = document.getElementById("gen-preview-box");
116
+ const genInfo = document.getElementById("gen-result-info");
117
+
118
+ genType.addEventListener("change", () => {
119
+ const is3d = genType.value === "3d";
120
+ document.getElementById("ref-image-group").style.display = is3d ? "block" : "none";
121
+ document.getElementById("neg-prompt-group").style.display = (genType.value === "image") ? "block" : "none";
122
+ });
123
+
124
+ genBtn.addEventListener("click", async () => {
125
+ const prompt = genPrompt.value.trim();
126
+ if (!prompt) { toast("Enter a prompt", "error"); return; }
127
+
128
+ const type = genType.value;
129
+ const btnText = genBtn.querySelector(".btn-text");
130
+ const btnLoad = genBtn.querySelector(".btn-loading");
131
+
132
+ btnText.style.display = "none";
133
+ btnLoad.style.display = "inline";
134
+ genBtn.disabled = true;
135
+
136
+ genPreview.innerHTML = '<div class="preview-placeholder"><span>⏳</span><p>Generating...</p></div>';
137
+
138
+ try {
139
+ const endpointMap = {
140
+ image: "/generate_image",
141
+ voice: "/generate_voice",
142
+ music: "/generate_music",
143
+ video: "/generate_video",
144
+ sfx: "/generate_sfx",
145
+ "3d": "/generate_3d",
146
+ };
147
+
148
+ const endpoint = endpointMap[type];
149
+ let result;
150
+
151
+ if (type === "image") {
152
+ const neg = document.getElementById("gen-negative").value;
153
+ result = await client.predict(endpoint, { prompt, negative_prompt: neg, steps: 4 });
154
+ } else {
155
+ result = await client.predict(endpoint, { prompt, text: prompt });
156
+ }
157
+
158
+ const fileData = result.data[0];
159
+ if (fileData && fileData.url) {
160
+ showResult(fileData, type);
161
+ toast("Generated!", "success");
162
+ } else {
163
+ genPreview.innerHTML = '<div class="preview-placeholder"><span>❌</span><p>Generation failed</p></div>';
164
+ }
165
+ } catch (e) {
166
+ genPreview.innerHTML = `<div class="preview-placeholder"><span>❌</span><p>${e.message}</p></div>`;
167
+ } finally {
168
+ btnText.style.display = "inline";
169
+ btnLoad.style.display = "none";
170
+ genBtn.disabled = false;
171
+ }
172
+ });
173
+
174
+ function showResult(fileData, type) {
175
+ const url = fileData.url;
176
+ let html = "";
177
+
178
+ if (type === "image") {
179
+ html = `<img src="${url}" alt="Generated">`;
180
+ } else if (type === "video") {
181
+ html = `<video controls autoplay><source src="${url}" type="video/mp4"></video>`;
182
+ } else if (["voice", "music", "sfx"].includes(type)) {
183
+ html = `<audio controls autoplay><source src="${url}"></audio>`;
184
+ } else {
185
+ html = `<div class="preview-placeholder"><span>📦</span><p>3D Model Generated</p></div>`;
186
+ }
187
+
188
+ genPreview.innerHTML = html;
189
+ genInfo.style.display = "flex";
190
+
191
+ document.getElementById("gen-download").onclick = () => {
192
+ const a = document.createElement("a");
193
+ a.href = url;
194
+ a.download = `gameforge_${type}_${Date.now()}`;
195
+ a.click();
196
+ };
197
+ }
198
+
199
+ // ============================================================================
200
+ // Models
201
+ // ============================================================================
202
+
203
+ async function loadModels() {
204
+ if (!registryData.length) {
205
+ const reg = await api("/registry_info");
206
+ registryData = reg.models || [];
207
+ }
208
+
209
+ const typeFilter = document.getElementById("model-filter-type");
210
+ const freeOnly = document.getElementById("model-filter-free");
211
+ const safeOnly = document.getElementById("model-filter-safe");
212
+
213
+ // Populate type filter
214
+ const types = [...new Set(registryData.map(m => m.asset_type))];
215
+ if (typeFilter.options.length <= 1) {
216
+ types.forEach(t => {
217
+ const opt = document.createElement("option");
218
+ opt.value = t;
219
+ opt.textContent = t;
220
+ typeFilter.appendChild(opt);
221
+ });
222
+ }
223
+
224
+ function render() {
225
+ let models = registryData;
226
+ if (typeFilter.value) models = models.filter(m => m.asset_type === typeFilter.value);
227
+ if (freeOnly.checked) models = models.filter(m => m.free);
228
+ if (safeOnly.checked) models = models.filter(m => m.commercial_safe);
229
+
230
+ const tbody = document.getElementById("models-tbody");
231
+ tbody.innerHTML = models.map(m => `
232
+ <tr>
233
+ <td>${m.asset_type}</td>
234
+ <td>${m.variant}</td>
235
+ <td style="font-family:monospace;font-size:12px">${m.model}</td>
236
+ <td>${m.type}</td>
237
+ <td>${m.license}</td>
238
+ <td>${m.hardware}</td>
239
+ <td><span class="badge ${m.free ? 'badge-free' : 'badge-paid'}">${m.free ? 'FREE' : 'PAID'}</span></td>
240
+ <td><span class="badge ${m.commercial_safe ? 'badge-safe' : 'badge-check'}">${m.commercial_safe ? 'SAFE' : 'CHECK'}</span></td>
241
+ </tr>
242
+ `).join("");
243
+ }
244
+
245
+ typeFilter.onchange = freeOnly.onchange = safeOnly.onchange = render;
246
+ render();
247
+ }
248
+
249
+ // ============================================================================
250
+ // Pipelines
251
+ // ============================================================================
252
+
253
+ async function loadPipelines() {
254
+ if (!pipelines.length) {
255
+ pipelines = await api("/list_pipelines");
256
+ }
257
+
258
+ const grid = document.getElementById("pipeline-grid");
259
+ grid.innerHTML = pipelines.map(p => `
260
+ <div class="pipeline-card">
261
+ <h3>${p.name}</h3>
262
+ <p>${p.description}</p>
263
+ <div class="pipeline-meta">
264
+ <span>📋 ${p.steps} steps</span>
265
+ <span>v${p.version}</span>
266
+ </div>
267
+ </div>
268
+ `).join("");
269
+ }
270
+
271
+ // ============================================================================
272
+ // Assets
273
+ // ============================================================================
274
+
275
+ async function loadAssets() {
276
+ const assets = await api("/list_assets");
277
+ const grid = document.getElementById("assets-grid");
278
+
279
+ if (!assets.length) {
280
+ grid.innerHTML = '<p style="color:var(--text-muted);grid-column:1/-1">No assets generated yet. Go to Generate to create some!</p>';
281
+ return;
282
+ }
283
+
284
+ grid.innerHTML = assets.map(a => `
285
+ <div class="asset-card">
286
+ <div class="name">${a.name}</div>
287
+ <div class="meta">${a.format} &middot; ${(a.size / 1024).toFixed(1)} KB</div>
288
+ </div>
289
+ `).join("");
290
+ }
291
+
292
+ document.getElementById("asset-refresh")?.addEventListener("click", loadAssets);
293
+
294
+ // ============================================================================
295
+ // Batch
296
+ // ============================================================================
297
+
298
+ async function loadBatchPipelines() {
299
+ if (!pipelines.length) {
300
+ pipelines = await api("/list_pipelines");
301
+ }
302
+
303
+ const select = document.getElementById("batch-pipeline");
304
+ select.innerHTML = pipelines.map(p =>
305
+ `<option value="${p.name}">${p.name} (${p.steps} steps)</option>`
306
+ ).join("");
307
+ }
308
+
309
+ document.getElementById("batch-count")?.addEventListener("input", (e) => {
310
+ document.getElementById("batch-count-val").textContent = e.target.value;
311
+ });
312
+
313
+ document.getElementById("batch-btn")?.addEventListener("click", async () => {
314
+ const pipeline = document.getElementById("batch-pipeline").value;
315
+ const prompt = document.getElementById("batch-prompt").value;
316
+ const count = parseInt(document.getElementById("batch-count").value);
317
+
318
+ if (!prompt) { toast("Enter a prompt", "error"); return; }
319
+
320
+ toast(`Generating ${count} variants...`, "info");
321
+
322
+ // Run variants sequentially
323
+ const results = document.getElementById("batch-results");
324
+ results.innerHTML = "";
325
+
326
+ for (let i = 0; i < count; i++) {
327
+ const card = document.createElement("div");
328
+ card.className = "asset-card";
329
+ card.innerHTML = `<div class="name">Variant ${i + 1}</div><div class="meta">Generating...</div>`;
330
+ results.appendChild(card);
331
+
332
+ try {
333
+ // Each variant gets a slightly different prompt
334
+ const variantPrompt = `${prompt}, style variation ${i + 1}`;
335
+ // This would call the pipeline endpoint
336
+ card.querySelector(".meta").textContent = "Queued";
337
+ } catch (e) {
338
+ card.querySelector(".meta").textContent = "Failed: " + e.message;
339
+ }
340
+ }
341
+ });
342
+
343
+ // ============================================================================
344
+ // Toast Notifications
345
+ // ============================================================================
346
+
347
+ function toast(message, type = "info") {
348
+ const container = document.getElementById("toast-container");
349
+ const el = document.createElement("div");
350
+ el.className = `toast toast-${type}`;
351
+ el.textContent = message;
352
+ container.appendChild(el);
353
+ setTimeout(() => el.remove(), 4000);
354
+ }
355
+
356
+ // ============================================================================
357
+ // Init
358
+ // ============================================================================
359
+
360
+ connect();