ashish-doing commited on
Commit
ea809e6
·
1 Parent(s): 67226a3

fix: FastAPI serves index.html at root, Gradio API at /gradio/run/*

Browse files
Files changed (3) hide show
  1. app.py +92 -141
  2. index.html +1 -1
  3. requirements.txt +2 -1
app.py CHANGED
@@ -1,10 +1,7 @@
1
- """app.py — SnapQuest backend.
2
-
3
- Architecture:
4
- - Gradio exposes named API endpoints only (no UI components)
5
- - index.html serves the full game UI as a static file
6
- - JS in index.html calls /run/upload, /run/action etc. directly
7
- - Zero gr.HTML, zero SSE timing issues, zero iframe sandbox problems
8
  """
9
  import base64
10
  import json
@@ -12,6 +9,9 @@ import os
12
  import tempfile
13
 
14
  import gradio as gr
 
 
 
15
 
16
  from engine_photo import start_photo_game, take_photo_action
17
  from dungeon import current_room, can_advance
@@ -19,17 +19,14 @@ from game_data import roll_loot
19
  from vision import analyze_scene
20
  from voice import clean_for_speech, speak, transcribe_audio
21
 
 
22
 
23
- # ── Session store (keyed by session_hash) ───────────────────────────────────
24
- _SESSIONS: dict[str, dict] = {}
25
 
26
  def _get(sid: str) -> dict:
27
  if sid not in _SESSIONS:
28
- _SESSIONS[sid] = {
29
- "photos": [],
30
- "selected_class": None,
31
- "game": {},
32
- }
33
  return _SESSIONS[sid]
34
 
35
  def _save_b64(data_url: str) -> str:
@@ -47,31 +44,60 @@ def _fmt_story(state: dict) -> str:
47
  room = current_room(state)
48
  diff = room.get("difficulty", "").upper()
49
  sym = {"EASY": "◆", "MEDIUM": "◈", "HARD": "⬡"}.get(diff, "◆")
50
- lines = [
51
- f"{'─'*5} {sym} {room.get('scene_name','Unknown')} {sym} {'─'*5}",
52
- "",
53
- room.get("scene_description", ""),
54
- ]
55
  for entry in state.get("history", [])[-8:]:
56
  lines.append(f"\n▷ {entry.get('action','')}")
57
  st = entry.get("response", {}).get("story", "")
58
- if st:
59
- lines.append(st)
60
  if can_advance(state):
61
  lines.append("\n\n[ Room cleared — click DESCEND DEEPER ]")
62
  return "\n".join(str(l) for l in lines if l is not None)
63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
 
65
- # ── API endpoints ─────────────────────────────────────────────────────────────
66
 
67
  def api_upload(sid: str, slot: int, data_url: str, name: str) -> str:
68
  app = _get(sid)
69
  photos = list(app.get("photos", []))
70
- while len(photos) <= slot:
71
- photos.append({})
72
  if not data_url:
73
- photos[slot] = {}
74
- app["photos"] = photos
75
  return json.dumps({"ok": True, "photos": _photos_summary(photos)})
76
  try:
77
  path = _save_b64(data_url)
@@ -83,22 +109,14 @@ def api_upload(sid: str, slot: int, data_url: str, name: str) -> str:
83
  app["photos"] = photos
84
  return json.dumps({"ok": True, "photos": _photos_summary(photos)})
85
 
86
- def _photos_summary(photos):
87
- return [
88
- {"has": bool(p.get("path")), "objects": p.get("objects", []), "name": p.get("name", "")}
89
- for p in photos
90
- ]
91
-
92
  def api_start(sid: str, character_class: str) -> str:
93
  app = _get(sid)
94
- photos = app.get("photos", [])
95
- paths = [p["path"] for p in photos if p.get("path")]
96
  if not paths:
97
  return json.dumps({"ok": False, "error": "No photos uploaded"})
98
  try:
99
  gs = start_photo_game(paths, character_class)
100
- app["game"] = gs
101
- app["selected_class"] = character_class
102
  return json.dumps({"ok": True, "state": _serialize_state(gs)})
103
  except Exception as exc:
104
  return json.dumps({"ok": False, "error": str(exc)})
@@ -124,18 +142,14 @@ def api_action(sid: str, text: str) -> str:
124
  try:
125
  txt = clean_for_speech(parsed)
126
  if txt:
127
- path = speak(txt)
128
- with open(path, "rb") as f:
129
  audio_b64 = base64.b64encode(f.read()).decode()
130
  except Exception:
131
  pass
132
- return json.dumps({
133
- "ok": True,
134
- "state": _serialize_state(new_gs),
135
- "loot": loot,
136
- "audio_b64": audio_b64,
137
- "story": parsed.get("story", ""),
138
- })
139
  except Exception as exc:
140
  return json.dumps({"ok": False, "error": str(exc)})
141
 
@@ -145,8 +159,7 @@ def api_voice(sid: str, audio_b64: str) -> str:
145
  try:
146
  raw = base64.b64decode(audio_b64)
147
  fd, path = tempfile.mkstemp(suffix=".wav", dir="/tmp")
148
- with os.fdopen(fd, "wb") as f:
149
- f.write(raw)
150
  text = transcribe_audio(path)
151
  if not text:
152
  return json.dumps({"ok": False, "text": ""})
@@ -156,105 +169,43 @@ def api_voice(sid: str, audio_b64: str) -> str:
156
  except Exception as exc:
157
  return json.dumps({"ok": False, "error": str(exc), "text": ""})
158
 
159
- def api_state(sid: str) -> str:
160
- app = _get(sid)
161
- gs = app.get("game", {})
162
- return json.dumps({
163
- "ok": True,
164
- "photos": _photos_summary(app.get("photos", [])),
165
- "selected_class": app.get("selected_class"),
166
- "has_game": bool(gs.get("rooms")),
167
- "state": _serialize_state(gs) if gs.get("rooms") else {},
168
- })
169
 
170
- def _serialize_state(gs: dict) -> dict:
171
- rooms = gs.get("rooms", [])
172
- idx = gs.get("room_index", 0)
173
- room = rooms[min(idx, len(rooms)-1)] if rooms else {}
174
- boss = room.get("boss") if room.get("is_boss") else None
175
- return {
176
- "hp": gs.get("hp", 100),
177
- "max_hp": gs.get("max_hp", 100),
178
- "xp": gs.get("xp", 0),
179
- "level": 1 + gs.get("xp", 0) // 100,
180
- "inventory": gs.get("inventory", []),
181
- "character_class": gs.get("character_class", ""),
182
- "room_index": idx,
183
- "total_rooms": len(rooms),
184
- "room": {
185
- "scene_name": room.get("scene_name", ""),
186
- "scene_description": room.get("scene_description", ""),
187
- "atmosphere": room.get("atmosphere", ""),
188
- "objects_found": room.get("objects_found", []),
189
- "difficulty": room.get("difficulty", "easy"),
190
- "is_boss": room.get("is_boss", False),
191
- "cleared": room.get("cleared", False),
192
- "enemy_alive": room.get("enemy_alive", True),
193
- "enemy_hp": room.get("enemy_hp", 30),
194
- "enemy_max_hp": room.get("enemy_max_hp", 30),
195
- },
196
- "boss": {
197
- "name": boss.get("name", "") if boss else "",
198
- "hp": boss.get("hp", 0) if boss else 0,
199
- "max_hp": boss.get("max_hp", 100) if boss else 100,
200
- "alive": boss.get("alive", True) if boss else False,
201
- } if boss else None,
202
- "choices": gs.get("current_choices", ["Look around", "Move forward", "Hold position"]),
203
- "story": _fmt_story(gs),
204
- "rooms_summary": [
205
- {"cleared": r.get("cleared", False), "is_boss": r.get("is_boss", False), "scene_name": r.get("scene_name","")}
206
- for r in rooms
207
- ],
208
- }
209
-
210
-
211
- # ── Gradio app — API only, no visible UI ────────────────────────────────────
212
-
213
- with gr.Blocks(title="SnapQuest API") as demo:
214
- gr.Markdown("## SnapQuest API — open the App tab to play")
215
-
216
- with gr.Row():
217
- sid_box = gr.Textbox(label="sid", visible=False)
218
- result_box = gr.Textbox(label="result", visible=False)
219
-
220
- # Upload endpoint
221
  with gr.Row(visible=False):
222
- up_sid = gr.Textbox(); up_slot = gr.Number(); up_data = gr.Textbox(); up_name = gr.Textbox()
223
- up_out = gr.Textbox()
224
- up_btn = gr.Button("upload", elem_id="sq-api-upload")
225
- up_btn.click(api_upload, inputs=[up_sid, up_slot, up_data, up_name], outputs=[up_out], api_name="upload")
226
-
227
- # Start endpoint
228
  with gr.Row(visible=False):
229
- st_sid = gr.Textbox(); st_cls = gr.Textbox()
230
- st_out = gr.Textbox()
231
- st_btn = gr.Button("start", elem_id="sq-api-start")
232
- st_btn.click(api_start, inputs=[st_sid, st_cls], outputs=[st_out], api_name="start")
233
-
234
- # Action endpoint
235
  with gr.Row(visible=False):
236
- ac_sid = gr.Textbox(); ac_txt = gr.Textbox()
237
- ac_out = gr.Textbox()
238
- ac_btn = gr.Button("action", elem_id="sq-api-action")
239
- ac_btn.click(api_action, inputs=[ac_sid, ac_txt], outputs=[ac_out], api_name="action")
240
-
241
- # State endpoint
242
  with gr.Row(visible=False):
243
- gs_sid = gr.Textbox()
244
- gs_out = gr.Textbox()
245
- gs_btn = gr.Button("getstate", elem_id="sq-api-state")
246
- gs_btn.click(api_state, inputs=[gs_sid], outputs=[gs_out], api_name="getstate")
247
 
248
- # Voice endpoint
249
- with gr.Row(visible=False):
250
- vo_sid = gr.Textbox(); vo_data = gr.Textbox()
251
- vo_out = gr.Textbox()
252
- vo_btn = gr.Button("voice", elem_id="sq-api-voice")
253
- vo_btn.click(api_voice, inputs=[vo_sid, vo_data], outputs=[vo_out], api_name="voice")
 
 
 
 
 
 
 
254
 
 
 
255
 
256
- demo.queue().launch(
257
- show_api=True,
258
- ssr_mode=False,
259
- allowed_paths=["/tmp", tempfile.gettempdir()],
260
- )
 
1
+ """app.py — SnapQuest.
2
+ FastAPI serves index.html at / and /game.
3
+ Gradio mounts at /gradio — its API endpoints at /gradio/run/*.
4
+ index.html calls /gradio/run/<endpoint>.
 
 
 
5
  """
6
  import base64
7
  import json
 
9
  import tempfile
10
 
11
  import gradio as gr
12
+ from fastapi import FastAPI
13
+ from fastapi.responses import FileResponse
14
+ from fastapi.middleware.cors import CORSMiddleware
15
 
16
  from engine_photo import start_photo_game, take_photo_action
17
  from dungeon import current_room, can_advance
 
19
  from vision import analyze_scene
20
  from voice import clean_for_speech, speak, transcribe_audio
21
 
22
+ _HERE = os.path.dirname(os.path.abspath(__file__))
23
 
24
+ # ── Session store ─────────────────────────────────────────────────────────────
25
+ _SESSIONS: dict = {}
26
 
27
  def _get(sid: str) -> dict:
28
  if sid not in _SESSIONS:
29
+ _SESSIONS[sid] = {"photos": [], "selected_class": None, "game": {}}
 
 
 
 
30
  return _SESSIONS[sid]
31
 
32
  def _save_b64(data_url: str) -> str:
 
44
  room = current_room(state)
45
  diff = room.get("difficulty", "").upper()
46
  sym = {"EASY": "◆", "MEDIUM": "◈", "HARD": "⬡"}.get(diff, "◆")
47
+ lines = [f"{'─'*5} {sym} {room.get('scene_name','Unknown')} {sym} {'─'*5}", "",
48
+ room.get("scene_description", "")]
 
 
 
49
  for entry in state.get("history", [])[-8:]:
50
  lines.append(f"\n▷ {entry.get('action','')}")
51
  st = entry.get("response", {}).get("story", "")
52
+ if st: lines.append(st)
 
53
  if can_advance(state):
54
  lines.append("\n\n[ Room cleared — click DESCEND DEEPER ]")
55
  return "\n".join(str(l) for l in lines if l is not None)
56
 
57
+ def _photos_summary(photos):
58
+ return [{"has": bool(p.get("path")), "objects": p.get("objects", []), "name": p.get("name", "")}
59
+ for p in photos]
60
+
61
+ def _serialize_state(gs: dict) -> dict:
62
+ rooms = gs.get("rooms", [])
63
+ idx = gs.get("room_index", 0)
64
+ room = rooms[min(idx, len(rooms)-1)] if rooms else {}
65
+ boss = room.get("boss") if room.get("is_boss") else None
66
+ return {
67
+ "hp": gs.get("hp", 100), "max_hp": gs.get("max_hp", 100),
68
+ "xp": gs.get("xp", 0), "level": 1 + gs.get("xp", 0) // 100,
69
+ "inventory": gs.get("inventory", []),
70
+ "character_class": gs.get("character_class", ""),
71
+ "room_index": idx, "total_rooms": len(rooms),
72
+ "room": {
73
+ "scene_name": room.get("scene_name", ""),
74
+ "scene_description": room.get("scene_description", ""),
75
+ "atmosphere": room.get("atmosphere", ""),
76
+ "objects_found": room.get("objects_found", []),
77
+ "difficulty": room.get("difficulty", "easy"),
78
+ "is_boss": room.get("is_boss", False),
79
+ "cleared": room.get("cleared", False),
80
+ "enemy_alive": room.get("enemy_alive", True),
81
+ "enemy_hp": room.get("enemy_hp", 30),
82
+ "enemy_max_hp": room.get("enemy_max_hp", 30),
83
+ },
84
+ "boss": {"name": boss.get("name",""), "hp": boss.get("hp",0),
85
+ "max_hp": boss.get("max_hp",100), "alive": boss.get("alive",True)
86
+ } if boss else None,
87
+ "choices": gs.get("current_choices", ["Look around","Move forward","Hold position"]),
88
+ "story": _fmt_story(gs),
89
+ "rooms_summary": [{"cleared": r.get("cleared",False), "is_boss": r.get("is_boss",False),
90
+ "scene_name": r.get("scene_name","")} for r in rooms],
91
+ }
92
 
93
+ # ── Gradio API functions ──────────────────────────────────────────────────────
94
 
95
  def api_upload(sid: str, slot: int, data_url: str, name: str) -> str:
96
  app = _get(sid)
97
  photos = list(app.get("photos", []))
98
+ while len(photos) <= slot: photos.append({})
 
99
  if not data_url:
100
+ photos[slot] = {}; app["photos"] = photos
 
101
  return json.dumps({"ok": True, "photos": _photos_summary(photos)})
102
  try:
103
  path = _save_b64(data_url)
 
109
  app["photos"] = photos
110
  return json.dumps({"ok": True, "photos": _photos_summary(photos)})
111
 
 
 
 
 
 
 
112
  def api_start(sid: str, character_class: str) -> str:
113
  app = _get(sid)
114
+ paths = [p["path"] for p in app.get("photos", []) if p.get("path")]
 
115
  if not paths:
116
  return json.dumps({"ok": False, "error": "No photos uploaded"})
117
  try:
118
  gs = start_photo_game(paths, character_class)
119
+ app["game"] = gs; app["selected_class"] = character_class
 
120
  return json.dumps({"ok": True, "state": _serialize_state(gs)})
121
  except Exception as exc:
122
  return json.dumps({"ok": False, "error": str(exc)})
 
142
  try:
143
  txt = clean_for_speech(parsed)
144
  if txt:
145
+ apath = speak(txt)
146
+ with open(apath, "rb") as f:
147
  audio_b64 = base64.b64encode(f.read()).decode()
148
  except Exception:
149
  pass
150
+ return json.dumps({"ok": True, "state": _serialize_state(new_gs),
151
+ "loot": loot, "audio_b64": audio_b64,
152
+ "story": parsed.get("story", "")})
 
 
 
 
153
  except Exception as exc:
154
  return json.dumps({"ok": False, "error": str(exc)})
155
 
 
159
  try:
160
  raw = base64.b64decode(audio_b64)
161
  fd, path = tempfile.mkstemp(suffix=".wav", dir="/tmp")
162
+ with os.fdopen(fd, "wb") as f: f.write(raw)
 
163
  text = transcribe_audio(path)
164
  if not text:
165
  return json.dumps({"ok": False, "text": ""})
 
169
  except Exception as exc:
170
  return json.dumps({"ok": False, "error": str(exc), "text": ""})
171
 
172
+ # ── Gradio Blocks ─────────────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
173
 
174
+ with gr.Blocks(title="SnapQuest") as demo:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
  with gr.Row(visible=False):
176
+ gr.Button().click(api_upload,
177
+ inputs=[gr.Textbox(),gr.Number(),gr.Textbox(),gr.Textbox()],
178
+ outputs=[gr.Textbox()], api_name="upload")
 
 
 
179
  with gr.Row(visible=False):
180
+ gr.Button().click(api_start,
181
+ inputs=[gr.Textbox(),gr.Textbox()],
182
+ outputs=[gr.Textbox()], api_name="start")
 
 
 
183
  with gr.Row(visible=False):
184
+ gr.Button().click(api_action,
185
+ inputs=[gr.Textbox(),gr.Textbox()],
186
+ outputs=[gr.Textbox()], api_name="action")
 
 
 
187
  with gr.Row(visible=False):
188
+ gr.Button().click(api_voice,
189
+ inputs=[gr.Textbox(),gr.Textbox()],
190
+ outputs=[gr.Textbox()], api_name="voice")
 
191
 
192
+ # ── FastAPI: / and /game → index.html, /gradio → Gradio API ──────────────────
193
+
194
+ fastapi_app = FastAPI()
195
+ fastapi_app.add_middleware(CORSMiddleware, allow_origins=["*"],
196
+ allow_methods=["*"], allow_headers=["*"])
197
+
198
+ @fastapi_app.get("/")
199
+ async def root():
200
+ return FileResponse(os.path.join(_HERE, "index.html"))
201
+
202
+ @fastapi_app.get("/game")
203
+ async def game():
204
+ return FileResponse(os.path.join(_HERE, "index.html"))
205
 
206
+ # Mount Gradio at /gradio — API at /gradio/run/upload etc.
207
+ fastapi_app = gr.mount_gradio_app(fastapi_app, demo, path="/gradio")
208
 
209
+ if __name__ == "__main__":
210
+ import uvicorn
211
+ uvicorn.run(fastapi_app, host="0.0.0.0", port=7860)
 
 
index.html CHANGED
@@ -421,7 +421,7 @@ const LOOT_TIERS = {
421
  // API CALLS — Gradio /run/<api_name> endpoints
422
  // ════════════════════════════════════════════════════════════════════════════
423
  async function callAPI(endpoint, data) {
424
- const res = await fetch(`/run/${endpoint}`, {
425
  method:'POST',
426
  headers:{'Content-Type':'application/json'},
427
  body: JSON.stringify({data}),
 
421
  // API CALLS — Gradio /run/<api_name> endpoints
422
  // ════════════════════════════════════════════════════════════════════════════
423
  async function callAPI(endpoint, data) {
424
+ const res = await fetch(`/gradio/run/${endpoint}`, {
425
  method:'POST',
426
  headers:{'Content-Type':'application/json'},
427
  body: JSON.stringify({data}),
requirements.txt CHANGED
@@ -3,4 +3,5 @@ edge-tts
3
  openai-whisper
4
  Pillow
5
  huggingface_hub
6
- faster-whisper
 
 
3
  openai-whisper
4
  Pillow
5
  huggingface_hub
6
+ faster-whisper
7
+ uvicorn