dingrui17 commited on
Commit
0368bd3
·
1 Parent(s): 5d22fb2

Updated project with React framework.

Browse files
Files changed (4) hide show
  1. app.py +65 -37
  2. frontend/app.jsx +76 -0
  3. requirements.txt +3 -2
  4. start.sh +3 -0
app.py CHANGED
@@ -1,44 +1,72 @@
1
- import gradio as gr
2
- from PIL import Image, ImageDraw
3
 
4
- player_pos = [50, 50]
5
- canvas_size = (200, 200)
6
- step_size = 10
 
 
 
 
7
 
8
- def update_frame(action, prev_pos):
9
- x, y = prev_pos
10
- if action == "w": y -= step_size
11
- elif action == "s": y += step_size
12
- elif action == "a": x -= step_size
13
- elif action == "d": x += step_size
14
- x = max(0, min(canvas_size[0], x))
15
- y = max(0, min(canvas_size[1], y))
16
- img = Image.new("RGB", canvas_size, color="white")
17
- draw = ImageDraw.Draw(img)
18
- draw.ellipse((x-5, y-5, x+5, y+5), fill="blue")
19
- return img, [x, y]
20
 
21
- with gr.Blocks() as demo:
22
- state = gr.State([50, 50])
23
- img_output = gr.Image(type="pil", interactive=False)
24
- action_box = gr.Textbox(label="Press keys (WASD)", interactive=False)
 
 
25
 
26
- demo.load(update_frame, ["", state], [img_output, state]) # initial frame
 
 
 
 
 
 
 
 
27
 
28
- custom_js = """
29
- <script>
30
- document.addEventListener('keydown', function(event) {
31
- let key = event.key.toLowerCase();
32
- if (["w","a","s","d"].includes(key)) {
33
- document.querySelector('textarea[aria-label="Press keys (WASD)"]').value = key;
34
- document.querySelector('button').click();
35
- }
36
- });
37
- </script>
38
- """
 
39
 
40
- html_box = gr.HTML(custom_js)
41
- action_button = gr.Button("Update") # hidden trigger
42
- action_button.click(update_frame, [action_box, state], [img_output, state])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
 
44
- demo.launch()
 
 
 
1
 
2
+ # app.py
3
+ from fastapi import FastAPI, WebSocket, WebSocketDisconnect
4
+ from fastapi.responses import HTMLResponse
5
+ from PIL import Image, ImageDraw
6
+ import base64
7
+ import io
8
+ import asyncio
9
 
10
+ app = FastAPI()
 
 
 
 
 
 
 
 
 
 
 
11
 
12
+ # Simple in-memory game state per-connection
13
+ class GameState:
14
+ def __init__(self):
15
+ self.player_pos = [50, 50]
16
+ self.canvas_size = (200, 200)
17
+ self.step = 10
18
 
19
+ def apply_action(self, action):
20
+ x, y = self.player_pos
21
+ if action == 'w': y -= self.step
22
+ elif action == 's': y += self.step
23
+ elif action == 'a': x -= self.step
24
+ elif action == 'd': x += self.step
25
+ x = max(0, min(self.canvas_size[0], x))
26
+ y = max(0, min(self.canvas_size[1], y))
27
+ self.player_pos = [x, y]
28
 
29
+ def render_frame_b64(self):
30
+ img = Image.new('RGB', self.canvas_size, (255, 255, 255))
31
+ draw = ImageDraw.Draw(img)
32
+ x, y = self.player_pos
33
+ r = 6
34
+ draw.ellipse((x-r, y-r, x+r, y+r), fill=(40, 120, 255))
35
+ # scale up to 400x400 for client
36
+ img = img.resize((400, 400), Image.NEAREST)
37
+ buff = io.BytesIO()
38
+ img.save(buff, format='PNG')
39
+ b64 = base64.b64encode(buff.getvalue()).decode('ascii')
40
+ return b64
41
 
42
+ @app.websocket('/ws')
43
+ async def websocket_endpoint(ws: WebSocket):
44
+ await ws.accept()
45
+ state = GameState()
46
+ try:
47
+ # send initial frame
48
+ await ws.send_json({ 'type': 'frame', 'image_b64': state.render_frame_b64(), 'player_pos': state.player_pos })
49
+ while True:
50
+ data = await ws.receive_text()
51
+ msg = None
52
+ try:
53
+ import json
54
+ msg = json.loads(data)
55
+ except Exception:
56
+ continue
57
+ if not isinstance(msg, dict):
58
+ continue
59
+ mtype = msg.get('type')
60
+ if mtype == 'action':
61
+ action = msg.get('action')
62
+ state.apply_action(action)
63
+ # return next frame
64
+ await ws.send_json({ 'type': 'frame', 'image_b64': state.render_frame_b64(), 'player_pos': state.player_pos })
65
+ elif mtype == 'reset':
66
+ state = GameState()
67
+ await ws.send_json({ 'type': 'frame', 'image_b64': state.render_frame_b64(), 'player_pos': state.player_pos })
68
+ elif mtype == 'noop':
69
+ await ws.send_json({ 'type': 'status', 'text': 'noop-ok' })
70
+ except WebSocketDisconnect:
71
+ return
72
 
 
frontend/app.jsx ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect, useRef, useState } from "react";
2
+
3
+
4
+ export default function App() {
5
+ const canvasRef = useRef(null);
6
+ const wsRef = useRef(null);
7
+ const [connected, setConnected] = useState(false);
8
+ const [playerPos, setPlayerPos] = useState([50, 50]);
9
+ const [status, setStatus] = useState("Disconnected");
10
+
11
+
12
+ useEffect(() => {
13
+ // Connect to backend WebSocket. When deploying to HF Spaces,
14
+ // use the absolute URL or relative path ("/ws").
15
+ const proto = window.location.protocol === "https:" ? "wss" : "ws";
16
+ const host = window.location.host; // same host as Space
17
+ const wsUrl = `${proto}://${host}/ws`;
18
+
19
+
20
+ const ws = new WebSocket(wsUrl);
21
+ wsRef.current = ws;
22
+
23
+
24
+ ws.addEventListener("open", () => {
25
+ setConnected(true);
26
+ setStatus("Connected");
27
+ // ask for initial frame
28
+ ws.send(JSON.stringify({ type: "noop" }));
29
+ });
30
+
31
+
32
+ ws.addEventListener("message", (ev) => {
33
+ const msg = JSON.parse(ev.data);
34
+ if (msg.type === "frame") {
35
+ drawFrame(msg.image_b64);
36
+ if (Array.isArray(msg.player_pos)) setPlayerPos(msg.player_pos);
37
+ } else if (msg.type === "status") {
38
+ setStatus(msg.text);
39
+ }
40
+ });
41
+
42
+
43
+ ws.addEventListener("close", () => {
44
+ setConnected(false);
45
+ setStatus("Disconnected");
46
+ });
47
+
48
+
49
+ return () => {
50
+ ws.close();
51
+ };
52
+ }, []);
53
+
54
+
55
+ // draw base64 png on canvas
56
+ function drawFrame(image_b64) {
57
+ const canvas = canvasRef.current;
58
+ if (!canvas) return;
59
+ const ctx = canvas.getContext("2d");
60
+ const img = new Image();
61
+ img.onload = () => {
62
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
63
+ ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
64
+ };
65
+ img.src = `data:image/png;base64,${image_b64}`;
66
+ }
67
+
68
+
69
+ // send action down the websocket
70
+ function sendAction(action) {
71
+ if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) return;
72
+ wsRef.current.send(JSON.stringify({ type: "action", action }));
73
+ }
74
+
75
+
76
+ }
requirements.txt CHANGED
@@ -1,2 +1,3 @@
1
- gradio
2
- Pillow
 
 
1
+ fastapi
2
+ uvicorn[standard]
3
+ Pillow
start.sh ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ #!/bin/bash
2
+ # Run backend on port 7860 (Hugging Face Spaces exposes this port)
3
+ uvicorn app:app --host 0.0.0.0 --port 7860