Reubencf commited on
Commit
7b3900c
·
verified ·
1 Parent(s): 1ce16e0

Deploy: Godot Web build + HF Inference NPC dialogue (drop AWS)

Browse files
Files changed (2) hide show
  1. README.md +1 -1
  2. app.py +41 -134
README.md CHANGED
@@ -49,7 +49,7 @@ As newcomers to Godot, we faced a steep learning curve, especially with our seme
49
  - Elevenlabs for audio generation
50
  - Notebook LM to quickly grasp concepts, even using an 8-hour YouTube video as input
51
  - Suno AI for creating background music
52
- - **Hugging Face Inference (Qwen2.5-7B-Instruct)** for NPC dialogue
53
  - **Ziva.sh** (in-editor AI agent) and **AWS Q Developer** to help write GDScript code
54
  - **Hugging Face Spaces** to host both the game and the AI dialogue endpoint
55
 
 
49
  - Elevenlabs for audio generation
50
  - Notebook LM to quickly grasp concepts, even using an 8-hour YouTube video as input
51
  - Suno AI for creating background music
52
+ - **Hugging Face Inference (Qwen2.5-1.5B-Instruct)** for fast NPC dialogue
53
  - **Ziva.sh** (in-editor AI agent) and **AWS Q Developer** to help write GDScript code
54
  - **Hugging Face Spaces** to host both the game and the AI dialogue endpoint
55
 
app.py CHANGED
@@ -1,162 +1,74 @@
1
  import os
2
  import time
 
3
 
4
- import gradio as gr
5
  from fastapi import FastAPI, Request
6
- from fastapi.responses import JSONResponse
7
  from fastapi.staticfiles import StaticFiles
8
- from dotenv import load_dotenv
9
  from huggingface_hub import InferenceClient
10
 
 
11
  load_dotenv()
12
 
13
- # --- Configuration -----------------------------------------------------------
14
- # Set HF_TOKEN as a Space secret (Settings -> Repository secrets). The token only
15
- # needs "Make calls to Inference Providers" permission. Without it the app falls
16
- # back to a mock response so the deployment still builds and runs.
17
  HF_TOKEN = os.getenv("HF_TOKEN")
18
-
19
- # Cheapest reliably-served conversational model with good quality-per-cost.
20
- # Swap this for Qwen/Qwen2.5-1.5B-Instruct (cheaper) or any chat model served by
21
- # Hugging Face Inference Providers: https://huggingface.co/models?inference_provider=all
22
- MODEL_ID = "Qwen/Qwen2.5-7B-Instruct"
23
-
24
  GAME_DIR = "game"
25
 
 
 
 
 
26
  client = InferenceClient(token=HF_TOKEN) if HF_TOKEN else None
27
 
 
 
28
 
29
- def generate_reply(prompt: str) -> str:
30
- """Call Hugging Face Inference (OpenAI-compatible router) for NPC dialogue."""
31
- full_prompt = prompt + ". Strictly respond in plain text, limited to 4 sentences."
32
 
 
 
33
  if client is None:
34
- time.sleep(0.5)
35
- return (
36
- "Mocked response. The wasteland is quiet today, traveler. "
37
- "(Set the HF_TOKEN secret to enable real AI dialogue. "
38
- "Get a token at https://huggingface.co/settings/tokens)"
39
- )
40
-
41
- print(f"Calling Hugging Face Inference: model={MODEL_ID}")
42
- start_time = time.time()
43
 
 
44
  completion = client.chat.completions.create(
45
  model=MODEL_ID,
46
- messages=[{"role": "user", "content": full_prompt}],
47
- max_tokens=200,
48
- temperature=0.6,
49
- top_p=0.9,
 
 
 
 
 
 
 
 
 
50
  )
51
-
52
- print(f"Took={time.time() - start_time:.2f}s")
53
  return completion.choices[0].message.content.strip()
54
 
55
 
56
- def get_ai_response(prompt):
57
- """Gradio tester-tab handler. Returns the same shape the game expects."""
58
- try:
59
- if not prompt:
60
- return {"error": "Missing prompt"}
61
- return {"output": generate_reply(prompt)}
62
- except Exception as e:
63
- return {"error": f"Inference error: {str(e)}"}
64
-
65
 
66
- # --- Gradio UI ---------------------------------------------------------------
67
- with gr.Blocks(title="Shadows of Tomorrow - AWS Game Hackathon") as demo:
68
- gr.Markdown(
69
- """
70
- # 🎮 Shadows of Tomorrow
71
- ### An RPG Adventure Set in Post-Nuclear 2200
72
 
73
- Welcome to Magnus Province! Built with Godot 4.3, with AI-powered NPC
74
- dialogue served by **Hugging Face Inference** (Qwen2.5-7B-Instruct).
 
 
75
 
76
- **Game Controls:**
77
- - WASD / Arrow Keys: Move
78
- - E: Interact
79
- - Left Mouse Click: Shoot
80
- - Tab: Toggle Inventory
81
- """
82
- )
83
 
84
- with gr.Tab("🎮 Play Game"):
85
- gr.HTML(
86
- """
87
- <div style="display: flex; justify-content: center; align-items: center; padding: 20px;">
88
- <iframe
89
- src="/game/index.html"
90
- width="1920"
91
- height="1080"
92
- style="border: 2px solid #333; border-radius: 8px; background: #000; max-width: 100%; max-height: 80vh;"
93
- allow="autoplay; fullscreen"
94
- allowfullscreen>
95
- </iframe>
96
- </div>
97
- """
98
- )
99
- gr.Markdown(
100
- "### 📝 Note\nIf the game doesn't load, the Godot Web export hasn't been "
101
- "placed in the `game/` folder yet. Click inside the game area to capture keyboard input."
102
- )
103
-
104
- with gr.Tab("🤖 AI Dialogue Tester"):
105
- gr.Markdown(
106
- """
107
- ### Test the NPC Dialogue System
108
- This is the same AI system used for NPC conversations in the game,
109
- powered by Hugging Face Inference (Qwen2.5-7B-Instruct).
110
- """
111
- )
112
- with gr.Row():
113
- with gr.Column():
114
- prompt_input = gr.Textbox(
115
- label="Enter NPC Prompt",
116
- placeholder="Example: You are a farmer in Magnus Province. Discuss renewable energy...",
117
- lines=5,
118
- )
119
- submit_btn = gr.Button("Generate Response", variant="primary")
120
- with gr.Column():
121
- response_output = gr.JSON(label="AI Response")
122
-
123
- submit_btn.click(fn=get_ai_response, inputs=[prompt_input], outputs=[response_output])
124
-
125
- gr.Examples(
126
- examples=[
127
- ["You are Delano, a farmer. Discuss innovative farming methods in a post-nuclear world."],
128
- ["You are a Nova Force officer. Explain how food security is maintained in Magnus Province."],
129
- ["You are a scientist. Discuss methods to combat contaminated soil after the nuclear disaster."],
130
- ],
131
- inputs=[prompt_input],
132
- )
133
-
134
- with gr.Tab("📖 About"):
135
- gr.Markdown(
136
- """
137
- ## Technologies
138
- - **Godot 4.3** — Game engine
139
- - **Hugging Face Inference (Qwen2.5-7B-Instruct)** — NPC dialogue
140
- - **Hugging Face Spaces** — Free hosting for the game + AI endpoint
141
- - **FLUX-dev / ElevenLabs / Suno AI** — Image, audio, and music generation
142
-
143
- ## How the AI works
144
- NPC prompts are POSTed to this Space's `/ai-response` endpoint, which proxies
145
- the request to Hugging Face Inference using the server-side `HF_TOKEN` secret —
146
- so no API keys are ever exposed in the browser game.
147
-
148
- [GitHub Repository](https://github.com/Reubencfernandes/aws-game-hackathon)
149
- """
150
- )
151
-
152
-
153
- # --- FastAPI app: AI endpoint + static game + mounted Gradio ------------------
154
- app = FastAPI()
155
 
156
 
157
  @app.post("/ai-response")
158
  async def ai_response(request: Request):
159
- """Endpoint the Godot game POSTs to. Contract: {"prompt": str} -> {"output": str}."""
160
  try:
161
  data = await request.json()
162
  except Exception:
@@ -169,19 +81,14 @@ async def ai_response(request: Request):
169
  try:
170
  return {"output": generate_reply(prompt)}
171
  except Exception as e:
172
- print(f"Error: {str(e)}")
173
  return JSONResponse({"error": str(e)}, status_code=500)
174
 
175
 
176
- # Serve the exported Godot Web build (game/index.html, *.wasm, *.pck, ...).
177
- # html=True makes /game/ resolve to index.html.
178
  if os.path.isdir(GAME_DIR):
179
  app.mount("/game", StaticFiles(directory=GAME_DIR, html=True), name="game")
180
  else:
181
- print(f"WARNING: '{GAME_DIR}/' not found export the Godot Web build there before deploying.")
182
-
183
- # Mount the Gradio UI at the root. Registered last so /ai-response and /game win.
184
- app = gr.mount_gradio_app(app, demo, path="/")
185
 
186
 
187
  if __name__ == "__main__":
 
1
  import os
2
  import time
3
+ import mimetypes
4
 
5
+ from dotenv import load_dotenv
6
  from fastapi import FastAPI, Request
7
+ from fastapi.responses import JSONResponse, RedirectResponse
8
  from fastapi.staticfiles import StaticFiles
 
9
  from huggingface_hub import InferenceClient
10
 
11
+
12
  load_dotenv()
13
 
 
 
 
 
14
  HF_TOKEN = os.getenv("HF_TOKEN")
15
+ MODEL_ID = os.getenv("MODEL_ID", "Qwen/Qwen2.5-1.5B-Instruct")
 
 
 
 
 
16
  GAME_DIR = "game"
17
 
18
+ MAX_TOKENS = 80
19
+ TEMPERATURE = 0.5
20
+ TOP_P = 0.85
21
+
22
  client = InferenceClient(token=HF_TOKEN) if HF_TOKEN else None
23
 
24
+ mimetypes.add_type("application/wasm", ".wasm")
25
+ mimetypes.add_type("application/octet-stream", ".pck")
26
 
 
 
 
27
 
28
+ def generate_reply(prompt: str) -> str:
29
+ """Generate short NPC dialogue for the Godot game."""
30
  if client is None:
31
+ time.sleep(0.2)
32
+ return "The air is calm today. Bring what you find, and we will make it useful."
 
 
 
 
 
 
 
33
 
34
+ start_time = time.time()
35
  completion = client.chat.completions.create(
36
  model=MODEL_ID,
37
+ messages=[
38
+ {
39
+ "role": "system",
40
+ "content": (
41
+ "You are an NPC in the post-nuclear RPG Shadows of Tomorrow. "
42
+ "Answer in plain text only, with 1 or 2 short natural sentences."
43
+ ),
44
+ },
45
+ {"role": "user", "content": prompt},
46
+ ],
47
+ max_tokens=MAX_TOKENS,
48
+ temperature=TEMPERATURE,
49
+ top_p=TOP_P,
50
  )
51
+ print(f"HF inference model={MODEL_ID} took={time.time() - start_time:.2f}s")
 
52
  return completion.choices[0].message.content.strip()
53
 
54
 
55
+ app = FastAPI()
 
 
 
 
 
 
 
 
56
 
 
 
 
 
 
 
57
 
58
+ @app.get("/", include_in_schema=False)
59
+ async def root():
60
+ """Make the Space open straight into the fullscreen Godot web export."""
61
+ return RedirectResponse(url="/game/index.html", status_code=307)
62
 
 
 
 
 
 
 
 
63
 
64
+ @app.get("/healthz")
65
+ async def healthz():
66
+ return {"ok": True, "model": MODEL_ID}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
 
68
 
69
  @app.post("/ai-response")
70
  async def ai_response(request: Request):
71
+ """Contract used by Godot: {"prompt": str} -> {"output": str}."""
72
  try:
73
  data = await request.json()
74
  except Exception:
 
81
  try:
82
  return {"output": generate_reply(prompt)}
83
  except Exception as e:
84
+ print(f"Inference error: {e}")
85
  return JSONResponse({"error": str(e)}, status_code=500)
86
 
87
 
 
 
88
  if os.path.isdir(GAME_DIR):
89
  app.mount("/game", StaticFiles(directory=GAME_DIR, html=True), name="game")
90
  else:
91
+ print(f"WARNING: '{GAME_DIR}/' not found. Export the Godot Web build before deploying.")
 
 
 
92
 
93
 
94
  if __name__ == "__main__":