Upload folder using huggingface_hub
Browse files- DEPLOY.md +42 -0
- Dockerfile +19 -0
- README.md +29 -0
- app.py +476 -0
- requirements.txt +8 -0
- static/css/app.css +391 -0
- static/index.html +224 -0
- 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} · ${(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();
|