| <!doctype html> |
| <html> |
| <head> |
| <meta charset="utf-8" /> |
| <title>Pong</title> |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> |
| |
| <script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script> |
| <style> |
| html, body { margin:0; height:100%; background:#111; color:#eee; font-family: system-ui, sans-serif; } |
| #overlay { |
| position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; |
| background: rgba(0,0,0,0.8); z-index: 9999; transition: opacity 200ms ease; |
| } |
| #overlay.hidden { opacity: 0; pointer-events: none; } |
| .spinner { |
| width: 64px; height: 64px; border: 6px solid #444; border-top-color: #09f; border-radius: 50%; |
| animation: spin 0.9s linear infinite; |
| } |
| @keyframes spin { to { transform: rotate(360deg); } } |
| #statusText { margin-top: 12px; color: #aaa; text-align: center; font-size: 14px; white-space: pre-line; } |
| #app { padding: 16px; } |
| button { padding: 8px 12px; background:#09f; color:#fff; border:none; border-radius:8px; cursor:pointer; } |
| button:disabled { opacity: .5; cursor: not-allowed; } |
| img#frame { image-rendering: pixelated; width: 240px; height: 240px; background:#222; display:block; margin-top:12px; } |
| </style> |
| </head> |
| <body> |
| <div id="overlay"> |
| <div> |
| <div class="spinner"></div> |
| <div id="statusText">Loading model…</div> |
| </div> |
| </div> |
|
|
| <div id="app"> |
| <h1>Pong</h1> |
| <div style="margin-bottom: 12px;"> |
| <label style="display: block; margin-bottom: 8px;"> |
| FPS: <input type="number" id="fpsInput" value="12" min="1" max="30" step="1" style="width: 60px; padding: 4px; margin-left: 8px;" /> |
| <span style="color: #aaa; font-size: 12px; margin-left: 8px;">frames per second</span> |
| </label> |
| <label style="display: block; margin-bottom: 8px;"> |
| Steps: <input type="number" id="stepsInput" value="4" min="1" max="10" step="1" style="width: 60px; padding: 4px; margin-left: 8px;" /> |
| <span style="color: #aaa; font-size: 12px; margin-left: 8px;">diffusion steps</span> |
| </label> |
| </div> |
| <div> |
| <button id="startBtn" disabled>Start Stream</button> |
| <button id="stopBtn" disabled>Stop Stream</button> |
| </div> |
| <img id="frame" alt="Latest frame" /> |
| <div id="actionDisplay" style="margin-top: 12px; font-size: 16px; font-family: monospace;"> |
| Action: <span id="actionValue">-</span> |
| </div> |
| <div id="fpsDisplay" style="margin-top: 8px; font-size: 16px; font-family: monospace;"> |
| Achieved FPS: <span id="fpsValue">-</span> |
| </div> |
| <div id="waitingMessage" style="margin-top: 12px; padding: 8px; background: #333; border-radius: 4px; display: none; color: #ffa500;"> |
| ⏳ Another player is currently using the stream. Please wait for them to finish. |
| </div> |
| <div style="margin-top: 12px; padding: 8px; background: #222; border-radius: 4px; border-left: 3px solid #09f; color: #ccc; font-size: 13px;"> |
| 💡 <strong>Tip:</strong> Click anywhere on this page to enable keyboard controls. Use <strong>↑/↓ Arrow Keys</strong> or <strong>W/S</strong> to control the paddle. |
| </div> |
| <div style="margin-top: 12px; font-size: 14px; color: #aaa; line-height: 1.5;"> |
| This demo uses a small transformer model trained with rectified flow matching to simulate Pong game frames conditioned on user inputs. The model generates 24×24 pixel frames in real-time using diffusion sampling with configurable steps. Performance targets ~16 FPS with 4 diffusion steps on GPU hardware. |
| </div> |
| </div> |
|
|
| <script> |
| |
| const socket = io({ transports: ['websocket', 'polling'] }); |
| |
| const overlay = document.getElementById('overlay'); |
| const statusText = document.getElementById('statusText'); |
| const startBtn = document.getElementById('startBtn'); |
| const stopBtn = document.getElementById('stopBtn'); |
| const frameImg = document.getElementById('frame'); |
| |
| function setStatus(isReady) { |
| if (!isReady) { |
| |
| overlay.classList.remove('hidden'); |
| startBtn.disabled = true; |
| stopBtn.disabled = true; |
| statusText.textContent = 'Loading model…'; |
| } else { |
| |
| overlay.classList.add('hidden'); |
| startBtn.disabled = false; |
| stopBtn.disabled = false; |
| statusText.textContent = 'Ready'; |
| } |
| } |
| |
| |
| setStatus(false); |
| |
| socket.on('connect', () => { |
| |
| console.log('connected'); |
| }); |
| |
| |
| socket.on('server_status', (payload) => { |
| const ready = !!(payload && payload.ready); |
| console.log('Server status:', { ready }); |
| setStatus(ready); |
| }); |
| |
| |
| startBtn.addEventListener('click', () => { |
| const fps = parseInt(document.getElementById('fpsInput').value) || 16; |
| const n_steps = parseInt(document.getElementById('stepsInput').value) || 1; |
| socket.emit('start_stream', { n_steps: n_steps, cfg: 0.0, fps: fps, clamp: true }); |
| }); |
| stopBtn.addEventListener('click', () => { |
| socket.emit('stop_stream'); |
| }); |
| |
| const actionValue = document.getElementById('actionValue'); |
| const fpsValue = document.getElementById('fpsValue'); |
| const waitingMessage = document.getElementById('waitingMessage'); |
| |
| |
| socket.on('stream_busy', (data) => { |
| console.log('Stream is busy:', data); |
| waitingMessage.style.display = 'block'; |
| startBtn.disabled = true; |
| }); |
| |
| socket.on('stream_available', (data) => { |
| console.log('Stream is available:', data); |
| waitingMessage.style.display = 'none'; |
| startBtn.disabled = false; |
| }); |
| |
| |
| socket.on('frame', ({ frame, frame_index, action, fps }) => { |
| frameImg.src = `data:image/png;base64,${frame}`; |
| |
| const actionLabels = ['START','NOOP', 'UP', 'DOWN']; |
| actionValue.textContent = `${action} (${actionLabels[action] || 'UNKNOWN'})`; |
| |
| if (fps !== undefined) { |
| fpsValue.textContent = fps.toFixed(1); |
| } |
| }); |
| |
| socket.on('error', (e) => { |
| console.warn('server error', e); |
| if (e && e.message) { |
| console.error('Server error message:', e.message); |
| |
| if (e.message.includes('Another player') || e.message.includes('not the current player')) { |
| waitingMessage.style.display = 'block'; |
| waitingMessage.textContent = '⏳ ' + e.message; |
| startBtn.disabled = true; |
| } else { |
| alert('Error: ' + e.message); |
| } |
| } |
| }); |
| |
| |
| |
| document.addEventListener('keydown', (e) => { |
| let action = null; |
| if (e.key === 'ArrowUp' || e.key === 'w' || e.key === 'W') { |
| action = 2; |
| } else if (e.key === 'ArrowDown' || e.key === 's' || e.key === 'S') { |
| action = 3; |
| } |
| if (action !== null) { |
| socket.emit('action', { action }); |
| e.preventDefault(); |
| } |
| }); |
| |
| document.addEventListener('keyup', (e) => { |
| if (['ArrowUp', 'ArrowDown', 'w', 'W', 's', 'S'].includes(e.key)) { |
| socket.emit('action', { action: 1 }); |
| e.preventDefault(); |
| } |
| }); |
| </script> |
| </body> |
| </html> |
|
|