Neural-ATARI / Dockerfile
AEUPH's picture
Create Dockerfile
72c977b verified
Raw
History Blame Contribute Delete
16.5 kB
# NEURAL ATARI GEN-CORE v1.0
FROM python:3.10-slim
WORKDIR /app
# 1. System Dependencies
# Fixed: libgl1-mesa-glx -> libgl1 (Debian Bookworm compatibility)
RUN apt-get update && apt-get install -y \
curl \
git \
libgomp1 \
libgl1 \
libglib2.0-0 \
&& rm -rf /var/lib/apt/lists/*
# 2. Python Environment
RUN pip install --upgrade pip
RUN pip install --no-cache-dir \
torch \
torchvision \
numpy \
flask \
flask-sock \
diffusers \
transformers \
accelerate \
peft \
pillow \
safetensors \
scipy \
sentencepiece \
diskcache
# 3. User Setup (Required for HF Spaces)
RUN useradd -m -u 1000 user
USER user
ENV HOME=/home/user \
PATH=/home/user/.local/bin:$PATH
# 4. The Monolithic Application (Embedded via HEREDOC)
COPY --chown=user <<'HYPER_EOF' app.py
import sys, os, io, base64, json, warnings, time, threading, random, re
import torch
import numpy as np
from flask import Flask
from flask_sock import Sock
from PIL import Image
from transformers import AutoModelForCausalLM, AutoTokenizer
from diffusers import StableDiffusionPipeline, LCMScheduler
import diskcache
warnings.filterwarnings("ignore")
# --- FRONTEND (React + Tailwind + Retro CSS) ---
HTML = r"""<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Neural Atari 2600</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=VT323&display=swap');
body { background: #050505; color: #eee; margin: 0; height: 100vh; display: flex; justify-content: center; align-items: center; font-family: 'VT323', monospace; overflow: hidden; }
.crt-container { position: relative; width: 640px; height: 480px; background: #000; border-radius: 20px; box-shadow: 0 0 50px rgba(0,0,0,0.8), inset 0 0 20px rgba(255,255,255,0.1); border: 20px solid #1a1a1a; }
.screen { width: 100%; height: 100%; position: relative; overflow: hidden; border-radius: 4px; }
.scanlines { position: absolute; inset: 0; background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.25) 50%), linear-gradient(90deg, rgba(255, 0, 0, 0.06), rgba(0, 255, 0, 0.02), rgba(0, 0, 255, 0.06)); background-size: 100% 4px, 6px 100%; pointer-events: none; z-index: 50; }
.glow { position: absolute; inset: 0; background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, rgba(0,0,0,0.4) 100%); pointer-events: none; z-index: 40; mix-blend-mode: screen; }
.flicker { animation: flicker 0.15s infinite; opacity: 0.05; position: absolute; inset: 0; background: white; pointer-events: none; z-index: 45; }
@keyframes flicker { 0% { opacity: 0.02; } 50% { opacity: 0.05; } 100% { opacity: 0.02; } }
canvas { image-rendering: pixelated; width: 100%; height: 100%; }
.wood-panel { position: absolute; bottom: -80px; left: -20px; right: -20px; height: 80px; background: #3e2723; border-top: 4px solid #1a1a1a; display: flex; align-items: center; justify-content: center; gap: 20px; }
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState, useEffect, useRef } = React;
function App() {
const [status, setStatus] = useState("BOOT"); // BOOT, INPUT, GENERATING, PLAYING, ERROR
const [logs, setLogs] = useState([]);
const [prompt, setPrompt] = useState("");
const [gameData, setGameData] = useState(null);
const socketRef = useRef(null);
const canvasRef = useRef(null);
const requestRef = useRef(null);
const gameStateRef = useRef({});
// --- LOGGING ---
const addLog = (msg) => setLogs(prev => [...prev.slice(-6), msg]);
// --- SOCKET CONNECTION ---
useEffect(() => {
const proto = window.location.protocol === 'https:' ? 'wss' : 'ws';
const ws = new WebSocket(`${proto}://${window.location.host}/atari`);
socketRef.current = ws;
ws.onopen = () => {
addLog("NEURAL CORE CONNECTED");
setTimeout(() => setStatus("INPUT"), 1500);
};
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
if (msg.type === 'log') addLog(msg.data);
if (msg.type === 'game_ready') {
setGameData(msg.data);
setStatus("PLAYING");
}
};
return () => ws.close();
}, []);
// --- GAME ENGINE ---
useEffect(() => {
if (status === 'PLAYING' && gameData && canvasRef.current) {
try {
// 1. Load Sprite Sheet
const img = new Image();
img.src = `data:image/png;base64,${gameData.sprites}`;
img.onload = () => {
// 2. Initialize Logic
// We use a safe-ish function constructor to interpret the generated code
// The backend sends a class body as string
const GameClass = new Function('ctx', 'width', 'height', 'sprites', gameData.code + '\nreturn Game;');
const ctx = canvasRef.current.getContext('2d');
// Instantiate Game
const gameInstance = new (GameClass())(ctx, 160, 192, img);
if(gameInstance.init) gameInstance.init();
gameStateRef.current = {
game: gameInstance,
keys: { ArrowUp: false, ArrowDown: false, ArrowLeft: false, ArrowRight: false, " ": false }
};
// 3. Start Loop
const loop = () => {
if(gameStateRef.current.game.update) {
gameStateRef.current.game.update(gameStateRef.current.keys);
}
if(gameStateRef.current.game.draw) {
// Clear Screen
ctx.fillStyle = gameData.bgColor || "#000";
ctx.fillRect(0, 0, 160, 192);
gameStateRef.current.game.draw();
}
requestRef.current = requestAnimationFrame(loop);
};
loop();
};
} catch (err) {
console.error(err);
addLog("ROM CORRUPTION ERROR");
setStatus("ERROR");
}
}
return () => cancelAnimationFrame(requestRef.current);
}, [status, gameData]);
// --- CONTROLS ---
useEffect(() => {
const handleDown = (e) => { if(gameStateRef.current.keys) gameStateRef.current.keys[e.key] = true; };
const handleUp = (e) => { if(gameStateRef.current.keys) gameStateRef.current.keys[e.key] = false; };
window.addEventListener('keydown', handleDown);
window.addEventListener('keyup', handleUp);
return () => { window.removeEventListener('keydown', handleDown); window.removeEventListener('keyup', handleUp); };
}, []);
const generateGame = () => {
if (!prompt) return;
setStatus("GENERATING");
socketRef.current.send(JSON.stringify({ type: 'generate', prompt: prompt }));
};
return (
<div className="crt-container">
<div className="scanlines"></div>
<div className="glow"></div>
<div className="flicker"></div>
<div className="screen bg-black relative flex flex-col items-center justify-center p-8 text-green-400">
{/* BOOT SCREEN */}
{status === 'BOOT' && (
<div className="text-left w-full animate-pulse">
<p>NEURAL BIOS v2.0</p>
<p>CHECKING MEMORY... OK</p>
<p>LOADING TENSOR CORES... OK</p>
{logs.map((l, i) => <p key={i}>{l}</p>)}
</div>
)}
{/* INPUT PROMPT */}
{status === 'INPUT' && (
<div className="w-full max-w-md text-center">
<h1 className="text-4xl mb-8 text-white drop-shadow-[0_0_10px_rgba(255,255,255,0.8)]">NEURAL ATARI</h1>
<p className="mb-2 text-green-600 uppercase">Insert Game Concept:</p>
<input
type="text"
className="w-full bg-black border-2 border-green-600 text-green-400 p-2 font-mono outline-none text-xl uppercase mb-4 focus:border-green-400 focus:shadow-[0_0_15px_rgba(0,255,0,0.5)]"
placeholder="E.G. SPACE CAT SHOOTER"
value={prompt}
onChange={e => setPrompt(e.target.value)}
onKeyDown={e => e.key === 'Enter' && generateGame()}
autoFocus
/>
<button
onClick={generateGame}
className="bg-green-700 text-black px-6 py-2 hover:bg-green-400 font-bold tracking-widest uppercase"
>
Generate ROM
</button>
</div>
)}
{/* GENERATING */}
{status === 'GENERATING' && (
<div className="text-center space-y-4">
<div className="w-16 h-16 border-4 border-green-500 border-t-transparent rounded-full animate-spin mx-auto"></div>
<h2 className="text-2xl animate-pulse">SYNTHESIZING CARTRIDGE...</h2>
<div className="text-sm h-32 overflow-hidden border border-green-900 p-2 text-left bg-black/50 w-full">
{logs.map((l, i) => <div key={i}>> {l}</div>)}
</div>
</div>
)}
{/* GAMEPLAY */}
{status === 'PLAYING' && (
<div className="w-full h-full relative group">
<canvas ref={canvasRef} width={160} height={192} />
<button
onClick={() => setStatus("INPUT")}
className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 bg-red-600 text-white px-2 py-1 text-xs"
>
EJECT
</button>
</div>
)}
</div>
<div className="wood-panel">
<div className="text-gray-400 text-sm">NEURAL SYSTEM</div>
<div className="w-24 h-4 bg-black rounded-full border border-gray-700"></div>
</div>
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>
"""
# --- BACKEND LOGIC ---
class NeuralSystem:
def __init__(self):
self.device = "cuda" if torch.cuda.is_available() else "cpu"
self.dtype = torch.float16 if self.device == "cuda" else torch.float32
print(f"⚡ Device: {self.device} | Type: {self.dtype}")
# 1. Load Coder LLM
print("🧠 Loading Logic Engine (Qwen)...")
self.model_id = "Qwen/Qwen2.5-Coder-1.5B-Instruct"
self.tokenizer = AutoTokenizer.from_pretrained(self.model_id)
self.llm = AutoModelForCausalLM.from_pretrained(self.model_id, torch_dtype=self.dtype).to(self.device)
# 2. Load Vision
print("🎨 Loading Vision Engine (SD-LCM)...")
self.pipe = StableDiffusionPipeline.from_pretrained(
"runwayml/stable-diffusion-v1-5",
torch_dtype=self.dtype,
safety_checker=None
)
if self.device == "cuda":
self.pipe = self.pipe.to("cuda")
self.pipe.load_lora_weights("latent-consistency/lcm-lora-sdv1-5")
self.pipe.scheduler = LCMScheduler.from_config(self.pipe.scheduler.config)
self.pipe.set_progress_bar_config(disable=True)
print("✓ SYSTEMS ONLINE")
def generate_code(self, prompt):
"""Generates JS Game Class"""
system_prompt = """You are an Atari 2600 developer. Write a Javascript Class named 'Game'.
The class MUST have:
1. constructor(ctx, width, height, spriteSheet)
2. init() - setup state
3. update(keys) - logic (keys.ArrowUp, etc)
4. draw() - rendering
Constraints:
- Canvas size is 160x192.
- Use ctx.fillStyle, ctx.fillRect, ctx.drawImage.
- The spriteSheet is an Image object. Assume it is a 2x2 grid (128x128 pixels total).
- Source sprites from the sheet: ctx.drawImage(this.spriteSheet, sx, sy, sw, sh, dx, dy, dw, dh).
- Keep logic simple (Pong, Space Invaders, Breakout style).
- Return ONLY the javascript code for the class, no markdown."""
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": f"Create a game about: {prompt}"}
]
text = self.tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
inputs = self.tokenizer([text], return_tensors="pt").to(self.device)
with torch.no_grad():
outputs = self.llm.generate(
inputs.input_ids,
max_new_tokens=1024,
temperature=0.2,
do_sample=True
)
code = self.tokenizer.decode(outputs[0][len(inputs.input_ids[0]):], skip_special_tokens=True)
# Cleanup markdown if present
code = re.sub(r'```javascript|```', '', code).strip()
return code
def generate_sprites(self, prompt):
"""Generates Sprite Sheet"""
# Enhance prompt for Atari style
style = "pixel art sprite sheet, atari 2600 style, 8-bit, black background, minimal colors, distinct shapes, white sprites, retro"
full_prompt = f"{style}, {prompt}"
with torch.no_grad():
image = self.pipe(
full_prompt,
num_inference_steps=4, # Fast LCM
guidance_scale=1.5,
width=256,
height=256
).images[0]
# Convert to Base64
buf = io.BytesIO()
image.save(buf, format="PNG")
return base64.b64encode(buf.getvalue()).decode()
# Init Global System
engine = NeuralSystem()
app = Flask(__name__)
sock = Sock(app)
@sock.route('/atari')
def atari_socket(ws):
ws.send(json.dumps({"type": "log", "data": "SYSTEM READY."}))
while True:
data = ws.receive()
if not data: break
msg = json.loads(data)
if msg['type'] == 'generate':
prompt = msg['prompt']
# 1. Generate Logic
ws.send(json.dumps({"type": "log", "data": "WRITING CODE..."}))
try:
code = engine.generate_code(prompt)
ws.send(json.dumps({"type": "log", "data": "CODE COMPILED."}))
# 2. Generate Assets
ws.send(json.dumps({"type": "log", "data": "DREAMING SPRITES..."}))
sprites = engine.generate_sprites(prompt)
ws.send(json.dumps({"type": "log", "data": "ASSETS LOADED."}))
# 3. Send Bundle
ws.send(json.dumps({
"type": "game_ready",
"data": {
"code": code,
"sprites": sprites,
"bgColor": "#000000"
}
}))
except Exception as e:
ws.send(json.dumps({"type": "log", "data": f"ERROR: {str(e)}"}))
@app.route('/')
def index():
return HTML
if __name__ == '__main__':
app.run(host='0.0.0.0', port=7860)
HYPER_EOF
# 5. Runtime Configuration
EXPOSE 7860
CMD ["python", "app.py"]