awacke1 commited on
Commit
1adce1c
ยท
verified ยท
1 Parent(s): 019da21

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +773 -0
app.py ADDED
@@ -0,0 +1,773 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import asyncio
3
+ import websockets
4
+ import uuid
5
+ from datetime import datetime
6
+ import os
7
+ import random
8
+ import hashlib
9
+ import base64
10
+ import edge_tts
11
+ import nest_asyncio
12
+ import re
13
+ import threading
14
+ import time
15
+ import json
16
+ import streamlit.components.v1 as components
17
+ from gradio_client import Client
18
+ from streamlit_marquee import streamlit_marquee
19
+ import folium
20
+ from streamlit_folium import folium_static
21
+ import glob
22
+ import pytz
23
+ from collections import defaultdict
24
+ import pandas as pd
25
+
26
+ # Patch asyncio for nesting
27
+ nest_asyncio.apply()
28
+
29
+ # Page Config
30
+ st.set_page_config(
31
+ layout="wide",
32
+ page_title="Rocky Mountain Quest 3D ๐Ÿ”๏ธ๐ŸŽฎ",
33
+ page_icon="๐ŸฆŒ",
34
+ initial_sidebar_state="auto",
35
+ menu_items={
36
+ 'Get Help': 'https://huggingface.co/awacke1',
37
+ 'Report a bug': 'https://huggingface.co/spaces/awacke1',
38
+ 'About': "Rocky Mountain Quest 3D ๐Ÿ”๏ธ๐ŸŽฎ"
39
+ }
40
+ )
41
+
42
+ # Game Config
43
+ GAME_NAME = "Rocky Mountain Quest 3D ๐Ÿ”๏ธ๐ŸŽฎ"
44
+ START_LOCATION = "Trailhead Camp โ›บ"
45
+ EDGE_TTS_VOICES = [
46
+ {"name": "Aria ๐ŸŒธ", "voice": "en-US-AriaNeural", "desc": "Elegant, creative storytelling", "color": 0xFF69B4},
47
+ {"name": "Guy ๐ŸŒŸ", "voice": "en-US-GuyNeural", "desc": "Authoritative, versatile", "color": 0x00FF00},
48
+ {"name": "Jenny ๐ŸŽถ", "voice": "en-US-JennyNeural", "desc": "Friendly, conversational", "color": 0xFFFF00},
49
+ {"name": "Sonia ๐ŸŒบ", "voice": "en-GB-SoniaNeural", "desc": "Bold, confident", "color": 0x0000FF},
50
+ {"name": "Ryan ๐Ÿ› ๏ธ", "voice": "en-GB-RyanNeural", "desc": "Approachable, casual", "color": 0x00FFFF},
51
+ {"name": "Natasha ๐ŸŒŒ", "voice": "en-AU-NatashaNeural", "desc": "Sophisticated, mysterious", "color": 0x800080},
52
+ {"name": "William ๐ŸŽป", "voice": "en-AU-WilliamNeural", "desc": "Classic, scholarly", "color": 0xFFA500},
53
+ {"name": "Clara ๐ŸŒท", "voice": "en-CA-ClaraNeural", "desc": "Cheerful, empathetic", "color": 0xFF4500},
54
+ {"name": "Liam ๐ŸŒŸ", "voice": "en-CA-LiamNeural", "desc": "Energetic, engaging", "color": 0xFFD700}
55
+ ]
56
+ FILE_EMOJIS = {"md": "๐Ÿ“œ", "mp3": "๐ŸŽต"}
57
+
58
+ # Prairie Simulator Locations
59
+ PRAIRIE_LOCATIONS = {
60
+ "Deadwood, SD": (44.3769, -103.7298),
61
+ "Wind Cave National Park": (43.6047, -103.4798),
62
+ "Wyoming Spring Creek": (41.6666, -106.6666)
63
+ }
64
+
65
+ # Directories and Files
66
+ for d in ["chat_logs", "audio_logs"]:
67
+ os.makedirs(d, exist_ok=True)
68
+
69
+ CHAT_DIR = "chat_logs"
70
+ AUDIO_DIR = "audio_logs"
71
+ STATE_FILE = "user_state.txt"
72
+ CHAT_FILE = os.path.join(CHAT_DIR, "quest_log.md")
73
+ GAME_STATE_FILE = "game_state.json"
74
+
75
+ # Cached Game State as "Resource"
76
+ @st.cache_resource
77
+ def load_game_state(_timestamp):
78
+ """Load or initialize the game state, treated as a cached resource."""
79
+ if os.path.exists(GAME_STATE_FILE):
80
+ with open(GAME_STATE_FILE, 'r') as f:
81
+ state = json.load(f)
82
+ else:
83
+ state = {
84
+ "players": {},
85
+ "treasures": [
86
+ {"id": str(uuid.uuid4()), "x": random.uniform(-40, 40), "z": random.uniform(-40, 40)},
87
+ {"id": str(uuid.uuid4()), "x": random.uniform(-40, 40), "z": random.uniform(-40, 40)},
88
+ {"id": str(uuid.uuid4()), "x": random.uniform(-40, 40), "z": random.uniform(-40, 40)},
89
+ {"id": str(uuid.uuid4()), "x": random.uniform(-40, 40), "z": random.uniform(-40, 40)},
90
+ {"id": str(uuid.uuid4()), "x": random.uniform(-40, 40), "z": random.uniform(-40, 40)}
91
+ ],
92
+ "history": []
93
+ }
94
+ with open(GAME_STATE_FILE, 'w') as f:
95
+ json.dump(state, f)
96
+ return state
97
+
98
+ def update_game_state(state):
99
+ """Update the game state and persist to file."""
100
+ state["timestamp"] = os.path.getmtime(GAME_STATE_FILE) if os.path.exists(GAME_STATE_FILE) else time.time()
101
+ with open(GAME_STATE_FILE, 'w') as f:
102
+ json.dump(state, f)
103
+ load_game_state.clear() # Clear cache to force reload
104
+ return load_game_state(time.time())
105
+
106
+ def reset_game_state():
107
+ """Reset the game state to initial conditions."""
108
+ state = {
109
+ "players": {},
110
+ "treasures": [
111
+ {"id": str(uuid.uuid4()), "x": random.uniform(-40, 40), "z": random.uniform(-40, 40)},
112
+ {"id": str(uuid.uuid4()), "x": random.uniform(-40, 40), "z": random.uniform(-40, 40)},
113
+ {"id": str(uuid.uuid4()), "x": random.uniform(-40, 40), "z": random.uniform(-40, 40)},
114
+ {"id": str(uuid.uuid4()), "x": random.uniform(-40, 40), "z": random.uniform(-40, 40)},
115
+ {"id": str(uuid.uuid4()), "x": random.uniform(-40, 40), "z": random.uniform(-40, 40)}
116
+ ],
117
+ "history": []
118
+ }
119
+ return update_game_state(state)
120
+
121
+ # Helpers
122
+ def format_timestamp(username=""):
123
+ now = datetime.now(pytz.timezone('US/Central')).strftime("%Y%m%d_%H%M%S")
124
+ return f"{now}-by-{username}"
125
+
126
+ def clean_text_for_tts(text):
127
+ return re.sub(r'[#*!\[\]]+', '', ' '.join(text.split()))[:200] or "No text"
128
+
129
+ def generate_filename(prompt, username, file_type="md"):
130
+ timestamp = format_timestamp(username)
131
+ cleaned_prompt = clean_text_for_tts(prompt)[:50].replace(" ", "_")
132
+ return f"{cleaned_prompt}_{timestamp}.{file_type}"
133
+
134
+ def create_file(prompt, username, file_type="md"):
135
+ filename = generate_filename(prompt, username, file_type)
136
+ with open(filename, 'w', encoding='utf-8') as f:
137
+ f.write(prompt)
138
+ return filename
139
+
140
+ def get_download_link(file, file_type="mp3"):
141
+ with open(file, "rb") as f:
142
+ b64 = base64.b64encode(f.read()).decode()
143
+ mime_types = {"mp3": "audio/mpeg", "md": "text/markdown"}
144
+ return f'<a href="data:{mime_types.get(file_type, "application/octet-stream")};base64,{b64}" download="{os.path.basename(file)}">{FILE_EMOJIS.get(file_type, "๐Ÿ“ฅ")} {os.path.basename(file)}</a>'
145
+
146
+ def save_username(username):
147
+ with open(STATE_FILE, 'w') as f:
148
+ f.write(username)
149
+
150
+ def load_username():
151
+ if os.path.exists(STATE_FILE):
152
+ with open(STATE_FILE, 'r') as f:
153
+ return f.read().strip()
154
+ return None
155
+
156
+ # Audio Processing
157
+ async def async_edge_tts_generate(text, voice, username):
158
+ cache_key = f"{text[:100]}_{voice}"
159
+ if cache_key in st.session_state['audio_cache']:
160
+ return st.session_state['audio_cache'][cache_key]
161
+ text = clean_text_for_tts(text)
162
+ filename = generate_filename(text, username, "mp3")
163
+ communicate = edge_tts.Communicate(text, voice)
164
+ await communicate.save(filename)
165
+ if os.path.exists(filename) and os.path.getsize(filename) > 0:
166
+ st.session_state['audio_cache'][cache_key] = filename
167
+ return filename
168
+ return None
169
+
170
+ def play_and_download_audio(file_path):
171
+ if file_path and os.path.exists(file_path):
172
+ st.audio(file_path, start_time=0)
173
+ st.markdown(get_download_link(file_path), unsafe_allow_html=True)
174
+
175
+ # WebSocket Broadcast
176
+ async def broadcast_message(message, room_id):
177
+ if room_id in st.session_state.active_connections:
178
+ disconnected = []
179
+ for client_id, ws in st.session_state.active_connections[room_id].items():
180
+ try:
181
+ await ws.send(message)
182
+ except websockets.ConnectionClosed:
183
+ disconnected.append(client_id)
184
+ for client_id in disconnected:
185
+ if client_id in st.session_state.active_connections[room_id]:
186
+ del st.session_state.active_connections[room_id][client_id]
187
+
188
+ # Chat and Quest Log
189
+ async def save_chat_entry(username, message, voice, is_markdown=False):
190
+ if not message.strip() or message == st.session_state.get('last_transcript', ''):
191
+ return None, None
192
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
193
+ entry = f"[{timestamp}] {username}: {message}" if not is_markdown else f"[{timestamp}] {username}:\n```markdown\n{message}\n```"
194
+ md_file = create_file(entry, username, "md")
195
+ with open(CHAT_FILE, 'a') as f:
196
+ f.write(f"{entry}\n")
197
+ audio_file = await async_edge_tts_generate(message, voice, username)
198
+ await broadcast_message(f"{username}|{message}", "quest")
199
+ st.session_state.chat_history.append({"username": username, "message": message, "audio": audio_file})
200
+ st.session_state.last_transcript = message
201
+ game_state = load_game_state(st.session_state.game_state_timestamp)
202
+ if username in game_state["players"]:
203
+ game_state["players"][username]["score"] += 10
204
+ game_state["players"][username]["treasures"] += 1
205
+ game_state["players"][username]["last_active"] = time.time()
206
+ game_state["history"].append(entry)
207
+ update_game_state(game_state)
208
+ return md_file, audio_file
209
+
210
+ async def load_chat():
211
+ if not os.path.exists(CHAT_FILE):
212
+ with open(CHAT_FILE, 'a') as f:
213
+ f.write(f"# {GAME_NAME} Log\n\nThe adventure begins at {START_LOCATION}! ๐Ÿ”๏ธ\n")
214
+ with open(CHAT_FILE, 'r') as f:
215
+ content = f.read().strip()
216
+ return content.split('\n')
217
+
218
+ # Session State Init
219
+ def init_session_state():
220
+ defaults = {
221
+ 'server_running': False, 'server_task': None, 'active_connections': {},
222
+ 'chat_history': [], 'audio_cache': {}, 'last_transcript': "",
223
+ 'username': None, 'score': 0, 'treasures': 0, 'location': START_LOCATION,
224
+ 'speech_processed': False, 'players': {}, 'last_update': time.time(),
225
+ 'update_interval': 1, 'x_pos': 0, 'z_pos': 0, 'move_left': False,
226
+ 'move_right': False, 'move_up': False, 'move_down': False,
227
+ 'prairie_players': {}, 'last_chat_update': 0, 'game_state_timestamp': time.time(),
228
+ 'timeout': 60, 'auto_refresh': 30, 'last_activity': time.time(),
229
+ 'marquee_settings': {"background": "#1E1E1E", "color": "#FFFFFF", "font-size": "14px", "animationDuration": "20s", "width": "100%", "lineHeight": "35px"},
230
+ 'operation_timings': {}, 'performance_metrics': defaultdict(list), 'download_link_cache': {}
231
+ }
232
+ for k, v in defaults.items():
233
+ if k not in st.session_state:
234
+ st.session_state[k] = v
235
+ if st.session_state.username is None:
236
+ saved_username = load_username()
237
+ if saved_username and any(c["name"] == saved_username for c in EDGE_TTS_VOICES):
238
+ st.session_state.username = saved_username
239
+ st.session_state.tts_voice = next(c["voice"] for c in EDGE_TTS_VOICES if c["name"] == saved_username)
240
+ else:
241
+ char = random.choice(EDGE_TTS_VOICES)
242
+ st.session_state.username = char["name"]
243
+ st.session_state.tts_voice = char["voice"]
244
+ asyncio.run(save_chat_entry(st.session_state.username, "๐Ÿ—บ๏ธ Joins the Rocky Mountain Quest!", char["voice"]))
245
+ save_username(st.session_state.username)
246
+ game_state = load_game_state(st.session_state.game_state_timestamp)
247
+ if st.session_state.username not in game_state["players"]:
248
+ char = next(c for c in EDGE_TTS_VOICES if c["name"] == st.session_state.username)
249
+ game_state["players"][st.session_state.username] = {
250
+ "x": random.uniform(-20, 20),
251
+ "z": random.uniform(-40, 40),
252
+ "color": char["color"],
253
+ "score": 0,
254
+ "treasures": 0,
255
+ "last_active": time.time()
256
+ }
257
+ update_game_state(game_state)
258
+
259
+ init_session_state()
260
+
261
+ # WebSocket for Multiplayer
262
+ async def websocket_handler(websocket, path):
263
+ client_id = str(uuid.uuid4())
264
+ room_id = "quest"
265
+ if room_id not in st.session_state.active_connections:
266
+ st.session_state.active_connections[room_id] = {}
267
+ st.session_state.active_connections[room_id][client_id] = websocket
268
+ username = st.session_state.username
269
+
270
+ game_state = load_game_state(st.session_state.game_state_timestamp)
271
+ if "prairie" in path:
272
+ char = next(c for c in EDGE_TTS_VOICES if c["name"] == username)
273
+ st.session_state.prairie_players[client_id] = {
274
+ "username": username,
275
+ "animal": "prairie_dog",
276
+ "location": PRAIRIE_LOCATIONS["Deadwood, SD"],
277
+ "color": char["color"]
278
+ }
279
+ await broadcast_message(f"System|{username} joins the prairie!", room_id)
280
+ else:
281
+ if username not in game_state["players"]:
282
+ char = next(c for c in EDGE_TTS_VOICES if c["name"] == username)
283
+ game_state["players"][username] = {
284
+ "x": random.uniform(-20, 20),
285
+ "z": random.uniform(-40, 40),
286
+ "color": char["color"],
287
+ "score": 0,
288
+ "treasures": 0,
289
+ "last_active": time.time()
290
+ }
291
+ update_game_state(game_state)
292
+ st.session_state.players[client_id] = game_state["players"][username]
293
+ await broadcast_message(f"System|{username} joins the quest!", room_id)
294
+
295
+ try:
296
+ async for message in websocket:
297
+ if '|' in message:
298
+ sender, content = message.split('|', 1)
299
+ voice = next(c["voice"] for c in EDGE_TTS_VOICES if c["name"] == sender)
300
+ game_state = load_game_state(st.session_state.game_state_timestamp)
301
+ if content.startswith("MOVE:"):
302
+ _, x, z = content.split(":")
303
+ x, z = float(x), float(z)
304
+ st.session_state.players[client_id]["x"] = x
305
+ st.session_state.players[client_id]["z"] = z
306
+ game_state["players"][sender]["x"] = x
307
+ game_state["players"][sender]["z"] = z
308
+ game_state["players"][sender]["last_active"] = time.time()
309
+ update_game_state(game_state)
310
+ elif content.startswith("SCORE:"):
311
+ score = int(content.split(":")[1])
312
+ st.session_state.players[client_id]["score"] = score
313
+ game_state["players"][sender]["score"] = score
314
+ game_state["players"][sender]["last_active"] = time.time()
315
+ update_game_state(game_state)
316
+ elif content.startswith("TREASURE:"):
317
+ treasures = int(content.split(":")[1])
318
+ st.session_state.players[client_id]["treasures"] = treasures
319
+ game_state["players"][sender]["treasures"] = treasures
320
+ game_state["players"][sender]["last_active"] = time.time()
321
+ for i, t in enumerate(game_state["treasures"]):
322
+ if abs(st.session_state.players[client_id]["x"] - t["x"]) < 2 and \
323
+ abs(st.session_state.players[client_id]["z"] - t["z"]) < 2:
324
+ game_state["treasures"].pop(i)
325
+ break
326
+ update_game_state(game_state)
327
+ elif content.startswith("PRAIRIE:"):
328
+ action, value = content.split(":", 1)
329
+ if action == "ANIMAL":
330
+ st.session_state.prairie_players[client_id]["animal"] = value
331
+ elif action == "MOVE":
332
+ target = PRAIRIE_LOCATIONS.get(value, PRAIRIE_LOCATIONS["Deadwood, SD"])
333
+ st.session_state.prairie_players[client_id]["location"] = target
334
+ action_msg = f"{sender} ({st.session_state.prairie_players[client_id]['animal']}) moves to {value}"
335
+ await save_chat_entry(sender, action_msg, voice)
336
+ else:
337
+ await save_chat_entry(sender, content, voice)
338
+ await perform_arxiv_search(content, sender)
339
+ except websockets.ConnectionClosed:
340
+ await broadcast_message(f"System|{username} leaves the quest!", room_id)
341
+ if client_id in st.session_state.players:
342
+ del st.session_state.players[client_id]
343
+ if client_id in st.session_state.prairie_players:
344
+ del st.session_state.prairie_players[client_id]
345
+ finally:
346
+ if room_id in st.session_state.active_connections and client_id in st.session_state.active_connections[room_id]:
347
+ del st.session_state.active_connections[room_id][client_id]
348
+
349
+ async def periodic_update():
350
+ while True:
351
+ if st.session_state.active_connections.get("quest"):
352
+ game_state = load_game_state(st.session_state.game_state_timestamp)
353
+ current_time = time.time()
354
+ inactive_players = [p for p, data in game_state["players"].items() if current_time - data["last_active"] > st.session_state.timeout]
355
+ for player in inactive_players:
356
+ del game_state["players"][player]
357
+ await broadcast_message(f"System|{player} timed out after 60 seconds!", "quest")
358
+ update_game_state(game_state)
359
+
360
+ player_list = ", ".join([p for p in game_state["players"].keys()]) or "No adventurers yet!"
361
+ message = f"๐Ÿ“ข Quest Update: Active Adventurers - {player_list}"
362
+ player_data = json.dumps(list(game_state["players"].values()))
363
+ await broadcast_message(f"MAP_UPDATE:{player_data}", "quest")
364
+
365
+ prairie_list = ", ".join([f"{p['username']} ({p['animal']})" for p in st.session_state.prairie_players.values()]) or "No animals yet!"
366
+ prairie_message = f"๐ŸŒพ Prairie Update: {prairie_list}"
367
+ prairie_data = json.dumps(list(st.session_state.prairie_players.values()))
368
+ await broadcast_message(f"PRAIRIE_UPDATE:{prairie_data}", "quest")
369
+
370
+ chat_content = await load_chat()
371
+ await broadcast_message(f"CHAT_UPDATE:{json.dumps(chat_content[-10:])}", "quest")
372
+ await broadcast_message(f"GAME_STATE:{json.dumps(game_state)}", "quest")
373
+ await save_chat_entry("System", f"{message}\n{prairie_message}", "en-US-AriaNeural")
374
+ await asyncio.sleep(st.session_state.update_interval)
375
+
376
+ async def run_websocket_server():
377
+ if not st.session_state.get('server_running', False):
378
+ server = await websockets.serve(websocket_handler, '0.0.0.0', 8765)
379
+ st.session_state['server_running'] = True
380
+ asyncio.create_task(periodic_update())
381
+ await server.wait_closed()
382
+
383
+ def start_websocket_server():
384
+ loop = asyncio.new_event_loop()
385
+ asyncio.set_event_loop(loop)
386
+ loop.run_until_complete(run_websocket_server())
387
+
388
+ # ArXiv Integration
389
+ async def perform_arxiv_search(query, username):
390
+ gradio_client = Client("awacke1/Arxiv-Paper-Search-And-QA-RAG-Pattern")
391
+ refs = gradio_client.predict(
392
+ query, 5, "Semantic Search", "mistralai/Mixtral-8x7B-Instruct-v0.1", api_name="/update_with_rag_md"
393
+ )[0]
394
+ result = f"๐Ÿ“š Ancient Rocky Knowledge:\n{refs}"
395
+ voice = next(c["voice"] for c in EDGE_TTS_VOICES if c["name"] == username)
396
+ md_file, audio_file = await save_chat_entry(username, result, voice, True)
397
+ return md_file, audio_file
398
+
399
+ # Sidebar Functions
400
+ def update_marquee_settings_ui():
401
+ st.sidebar.markdown("### ๐ŸŽฏ Marquee Settings")
402
+ cols = st.sidebar.columns(2)
403
+ with cols[0]:
404
+ bg_color = st.color_picker("๐ŸŽจ Background", st.session_state['marquee_settings']["background"], key="bg_color_picker")
405
+ text_color = st.color_picker("โœ๏ธ Text", st.session_state['marquee_settings']["color"], key="text_color_picker")
406
+ with cols[1]:
407
+ font_size = st.slider("๐Ÿ“ Size", 10, 24, 14, key="font_size_slider")
408
+ duration = st.slider("โฑ๏ธ Speed (secs)", 1, 20, 20, key="duration_slider")
409
+ st.session_state['marquee_settings'].update({
410
+ "background": bg_color,
411
+ "color": text_color,
412
+ "font-size": f"{font_size}px",
413
+ "animationDuration": f"{duration}s"
414
+ })
415
+
416
+ def display_file_history_in_sidebar():
417
+ st.sidebar.markdown("---")
418
+ st.sidebar.markdown("### ๐Ÿ“‚ Chat Gallery")
419
+ chat_files = sorted(glob.glob("*.mp3"), key=os.path.getmtime, reverse=True)
420
+ if not chat_files:
421
+ st.sidebar.write("No chat audio files found.")
422
+ return
423
+ for audio_file in chat_files[:10]: # Limit to last 10 for brevity
424
+ with st.sidebar.expander(os.path.basename(audio_file)):
425
+ st.write(f"**Said:** {os.path.splitext(os.path.basename(audio_file))[0].split('_')[0]}")
426
+ play_and_download_audio(audio_file)
427
+
428
+ def log_performance_metrics():
429
+ st.sidebar.markdown("### โฑ๏ธ Performance Metrics")
430
+ metrics = st.session_state['operation_timings']
431
+ if metrics:
432
+ total_time = sum(metrics.values())
433
+ st.sidebar.write(f"**Total Processing Time:** {total_time:.2f}s")
434
+ for operation, duration in metrics.items():
435
+ percentage = (duration / total_time) * 100
436
+ st.sidebar.write(f"**{operation}:** {duration:.2f}s ({percentage:.1f}%)")
437
+
438
+ # Enhanced 3D Game HTML
439
+ rocky_map_html = f"""
440
+ <!DOCTYPE html>
441
+ <html lang="en">
442
+ <head>
443
+ <meta charset="UTF-8">
444
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
445
+ <title>Rocky Mountain Quest 3D</title>
446
+ <style>
447
+ body {{ margin: 0; overflow: hidden; font-family: Arial, sans-serif; background: #000; }}
448
+ #gameContainer {{ width: 800px; height: 600px; position: relative; }}
449
+ canvas {{ width: 100%; height: 100%; display: block; }}
450
+ #ui {{
451
+ position: absolute; top: 10px; left: 10px; color: white;
452
+ background: rgba(0, 0, 0, 0.5); padding: 5px; border-radius: 5px;
453
+ }}
454
+ #chatBox {{
455
+ position: absolute; bottom: 60px; left: 10px; width: 300px; height: 150px;
456
+ background: rgba(0, 0, 0, 0.7); color: white; padding: 10px;
457
+ border-radius: 5px; overflow-y: auto;
458
+ }}
459
+ #controls {{
460
+ position: absolute; bottom: 10px; left: 10px; color: white;
461
+ background: rgba(0, 0, 0, 0.5); padding: 5px; border-radius: 5px;
462
+ }}
463
+ </style>
464
+ </head>
465
+ <body>
466
+ <div id="gameContainer">
467
+ <div id="ui">
468
+ <div id="players">Players: 1</div>
469
+ <div id="score">Score: 0</div>
470
+ <div id="treasures">Treasures: 0</div>
471
+ <div id="timeout">Timeout: 60s</div>
472
+ </div>
473
+ <div id="chatBox"></div>
474
+ <div id="controls">WASD/Arrows to move, Space to collect treasure</div>
475
+ </div>
476
+
477
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
478
+ <script>
479
+ const playerName = "{st.session_state.username}";
480
+ let ws = new WebSocket('ws://localhost:8765');
481
+ const scene = new THREE.Scene();
482
+ const camera = new THREE.PerspectiveCamera(75, 800 / 600, 0.1, 1000);
483
+ camera.position.set(0, 50, 50);
484
+ camera.lookAt(0, 0, 0);
485
+
486
+ const renderer = new THREE.WebGLRenderer({{ antialias: true }});
487
+ renderer.setSize(800, 600);
488
+ document.getElementById('gameContainer').appendChild(renderer.domElement);
489
+
490
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
491
+ scene.add(ambientLight);
492
+ const sunLight = new THREE.DirectionalLight(0xffddaa, 1);
493
+ sunLight.position.set(50, 50, 50);
494
+ sunLight.castShadow = true;
495
+ scene.add(sunLight);
496
+
497
+ const groundGeometry = new THREE.PlaneGeometry(100, 100);
498
+ const groundMaterial = new THREE.MeshStandardMaterial({{ color: 0x228B22 }});
499
+ const ground = new THREE.Mesh(groundGeometry, groundMaterial);
500
+ ground.rotation.x = -Math.PI / 2;
501
+ ground.receiveShadow = true;
502
+ scene.add(ground);
503
+
504
+ let players = {{}};
505
+ const playerMeshes = {{}};
506
+ let treasures = [];
507
+ const treasureMeshes = {{}};
508
+ let xPos = 0, zPos = 0, moveLeft = false, moveRight = false, moveUp = false, moveDown = false, collect = false;
509
+ let score = 0, treasureCount = 0, lastActive = performance.now() / 1000;
510
+
511
+ const playerGeometry = new THREE.BoxGeometry(2, 2, 2);
512
+ const playerMaterial = new THREE.MeshPhongMaterial({{ color: {next(c["color"] for c in EDGE_TTS_VOICES if c["name"] == st.session_state.username)} }});
513
+ const playerMesh = new THREE.Mesh(playerGeometry, playerMaterial);
514
+ playerMesh.position.set(xPos, 1, zPos);
515
+ playerMesh.castShadow = true;
516
+ scene.add(playerMesh);
517
+ players[playerName] = {{ mesh: playerMesh, score: 0, treasures: 0 }};
518
+
519
+ function updateTreasures(treasureData) {{
520
+ treasureData.forEach(t => {{
521
+ if (!treasureMeshes[t.id]) {{
522
+ const treasure = new THREE.Mesh(
523
+ new THREE.SphereGeometry(1, 8, 8),
524
+ new THREE.MeshPhongMaterial({{ color: 0xffff00 }})
525
+ );
526
+ treasure.position.set(t.x, 1, t.z);
527
+ treasure.castShadow = true;
528
+ treasureMeshes[t.id] = treasure;
529
+ treasures.push(treasure);
530
+ scene.add(treasure);
531
+ }} else {{
532
+ treasureMeshes[t.id].position.set(t.x, 1, t.z);
533
+ }}
534
+ }});
535
+ Object.keys(treasureMeshes).forEach(id => {{
536
+ if (!treasureData.some(t => t.id === id)) {{
537
+ scene.remove(treasureMeshes[id]);
538
+ treasures = treasures.filter(t => t !== treasureMeshes[id]);
539
+ delete treasureMeshes[id];
540
+ }}
541
+ }});
542
+ }}
543
+
544
+ document.addEventListener('keydown', (event) => {{
545
+ switch (event.code) {{
546
+ case 'ArrowLeft': case 'KeyA': moveLeft = true; break;
547
+ case 'ArrowRight': case 'KeyD': moveRight = true; break;
548
+ case 'ArrowUp': case 'KeyW': moveUp = true; break;
549
+ case 'ArrowDown': case 'KeyS': moveDown = true; break;
550
+ case 'Space': collect = true; break;
551
+ }}
552
+ lastActive = performance.now() / 1000;
553
+ }});
554
+ document.addEventListener('keyup', (event) => {{
555
+ switch (event.code) {{
556
+ case 'ArrowLeft': case 'KeyA': moveLeft = false; break;
557
+ case 'ArrowRight': case 'KeyD': moveRight = false; break;
558
+ case 'ArrowUp': case 'KeyW': moveUp = false; break;
559
+ case 'ArrowDown': case 'KeyS': moveDown = false; break;
560
+ case 'Space': collect = false; break;
561
+ }}
562
+ }});
563
+
564
+ function updatePlayer(delta) {{
565
+ const speed = 20;
566
+ if (moveLeft && xPos > -40) xPos -= speed * delta;
567
+ if (moveRight && xPos < 40) xPos += speed * delta;
568
+ if (moveUp && zPos > -40) zPos -= speed * delta;
569
+ if (moveDown && zPos < 40) zPos += speed * delta;
570
+ playerMesh.position.set(xPos, 1, zPos);
571
+ ws.send(`${{playerName}}|MOVE:${{xPos}}:${{zPos}}`);
572
+
573
+ if (collect) {{
574
+ for (let i = treasures.length - 1; i >= 0; i--) {{
575
+ if (playerMesh.position.distanceTo(treasures[i].position) < 2) {{
576
+ const id = Object.keys(treasureMeshes).find(key => treasureMeshes[key] === treasures[i]);
577
+ scene.remove(treasures[i]);
578
+ treasures.splice(i, 1);
579
+ delete treasureMeshes[id];
580
+ score += 50;
581
+ treasureCount += 1;
582
+ ws.send(`${{playerName}}|SCORE:${{score}}`);
583
+ ws.send(`${{playerName}}|TREASURE:${{treasureCount}}`);
584
+ }}
585
+ }}
586
+ }}
587
+
588
+ camera.position.set(xPos, 50, zPos + 50);
589
+ camera.lookAt(xPos, 0, zPos);
590
+ const timeout = 60 - (performance.now() / 1000 - lastActive);
591
+ document.getElementById('timeout').textContent = `Timeout: ${{Math.max(0, timeout.toFixed(0))}}s`;
592
+ }}
593
+
594
+ function updatePlayers(playerData) {{
595
+ playerData.forEach(player => {{
596
+ if (!playerMeshes[player.username]) {{
597
+ const mesh = new THREE.Mesh(playerGeometry, new THREE.MeshPhongMaterial({{ color: player.color }}));
598
+ mesh.castShadow = true;
599
+ scene.add(mesh);
600
+ playerMeshes[player.username] = mesh;
601
+ }}
602
+ const mesh = playerMeshes[player.username];
603
+ mesh.position.set(player.x, 1, player.z);
604
+ players[player.username] = {{ mesh: mesh, score: player.score, treasures: player.treasures }};
605
+ if (player.username === playerName) {{
606
+ xPos = player.x;
607
+ zPos = player.z;
608
+ score = player.score;
609
+ treasureCount = player.treasures;
610
+ playerMesh.position.set(xPos, 1, zPos);
611
+ }}
612
+ }});
613
+ document.getElementById('players').textContent = `Players: ${{Object.keys(playerMeshes).length}}`;
614
+ document.getElementById('score').textContent = `Score: ${{score}}`;
615
+ document.getElementById('treasures').textContent = `Treasures: ${{treasureCount}}`;
616
+ }}
617
+
618
+ ws.onmessage = function(event) {{
619
+ const data = event.data;
620
+ if (data.startsWith('MAP_UPDATE:')) {{
621
+ const playerData = JSON.parse(data.split('MAP_UPDATE:')[1]);
622
+ updatePlayers(playerData);
623
+ }} else if (data.startsWith('CHAT_UPDATE:')) {{
624
+ const chatData = JSON.parse(data.split('CHAT_UPDATE:')[1]);
625
+ const chatBox = document.getElementById('chatBox');
626
+ chatBox.innerHTML = chatData.map(line => `<p>${{line}}</p>`).join('');
627
+ chatBox.scrollTop = chatBox.scrollHeight;
628
+ }} else if (data.startsWith('GAME_STATE:')) {{
629
+ const gameState = JSON.parse(data.split('GAME_STATE:')[1]);
630
+ updatePlayers(gameState.players);
631
+ updateTreasures(gameState.treasures);
632
+ }} else if (!data.startsWith('PRAIRIE_UPDATE:')) {{
633
+ const [sender, message] = data.split('|');
634
+ const chatBox = document.getElementById('chatBox');
635
+ chatBox.innerHTML += `<p>${{sender}}: ${{message}}</p>`;
636
+ chatBox.scrollTop = chatBox.scrollHeight;
637
+ }}
638
+ }};
639
+
640
+ let lastTime = performance.now();
641
+ function animate() {{
642
+ requestAnimationFrame(animate);
643
+ const currentTime = performance.now();
644
+ const delta = (currentTime - lastTime) / 1000;
645
+ lastTime = currentTime;
646
+
647
+ updatePlayer(delta);
648
+ treasures.forEach(t => t.rotation.y += delta);
649
+ renderer.render(scene, camera);
650
+ }}
651
+
652
+ animate();
653
+ </script>
654
+ </body>
655
+ </html>
656
+ """
657
+
658
+ # Main Game Loop
659
+ def main():
660
+ update_marquee_settings_ui()
661
+ st.sidebar.title(f"๐ŸŽฎ {GAME_NAME}")
662
+ st.sidebar.subheader(f"๐ŸŒ„ {st.session_state.username}โ€™s Adventure - Score: {st.session_state.score} ๐Ÿ†")
663
+ st.sidebar.write(f"๐Ÿ“œ {next(c['desc'] for c in EDGE_TTS_VOICES if c['name'] == st.session_state.username)}")
664
+ st.sidebar.write(f"๐Ÿ“ Location: {st.session_state.location}")
665
+ st.sidebar.write(f"๐Ÿ… Score: {st.session_state.score}")
666
+ st.sidebar.write(f"๐ŸŽต Treasures: {st.session_state.treasures}")
667
+ st.sidebar.write(f"๐Ÿ‘ฅ Players: {', '.join([p['username'] for p in st.session_state.players.values()]) or 'None'}")
668
+
669
+ st.session_state.update_interval = st.sidebar.slider("Refresh Interval (seconds)", 1, 10, 1, step=1)
670
+ if st.sidebar.button("Reset World"):
671
+ reset_game_state()
672
+ st.session_state.game_state_timestamp = time.time()
673
+ st.rerun()
674
+
675
+ left_col, right_col = st.columns([2, 1])
676
+
677
+ with left_col:
678
+ components.html(rocky_map_html, width=800, height=600)
679
+ chat_content = asyncio.run(load_chat())
680
+ st.text_area("๐Ÿ“œ Quest Log", "\n".join(chat_content[-10:]), height=200, disabled=True)
681
+ message = st.text_input(f"๐Ÿ—จ๏ธ {st.session_state.username} says:", placeholder="Speak or type to chat! ๐ŸŒฒ")
682
+ if st.button("๐ŸŒŸ Send & Chat ๐ŸŽค"):
683
+ if message:
684
+ voice = next(c["voice"] for c in EDGE_TTS_VOICES if c["name"] == st.session_state.username)
685
+ md_file, audio_file = asyncio.run(save_chat_entry(st.session_state.username, message, voice))
686
+ if audio_file:
687
+ play_and_download_audio(audio_file)
688
+ st.session_state.last_activity = time.time()
689
+ st.success(f"๐ŸŒ„ +10 points! New Score: {st.session_state.score}")
690
+ uploaded_file = st.file_uploader("Upload a File", type=["txt", "md", "mp3"])
691
+ if uploaded_file:
692
+ with open(uploaded_file.name, "wb") as f:
693
+ f.write(uploaded_file.getbuffer())
694
+ game_state = load_game_state(st.session_state.game_state_timestamp)
695
+ if st.session_state.username in game_state["players"]:
696
+ game_state["players"][st.session_state.username]["score"] += 20
697
+ game_state["history"].append(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] {st.session_state.username}: Uploaded {uploaded_file.name}")
698
+ update_game_state(game_state)
699
+ st.session_state.last_activity = time.time()
700
+ st.success(f"File uploaded! +20 points! New Score: {st.session_state.score}")
701
+ mycomponent = components.declare_component("speech_component", path="./speech_component")
702
+ val = mycomponent(my_input_value="", key=f"speech_{st.session_state.get('speech_processed', False)}")
703
+ if val and val != st.session_state.last_transcript:
704
+ val_stripped = val.strip().replace('\n', ' ')
705
+ if val_stripped:
706
+ voice = next(c["voice"] for c in EDGE_TTS_VOICES if c["name"] == st.session_state.username)
707
+ st.session_state['speech_processed'] = True
708
+ md_file, audio_file = asyncio.run(save_chat_entry(st.session_state.username, val_stripped, voice))
709
+ if audio_file:
710
+ play_and_download_audio(audio_file)
711
+ st.session_state.last_activity = time.time()
712
+ st.rerun()
713
+
714
+ with right_col:
715
+ st.subheader("๐ŸŒพ Prairie Map")
716
+ prairie_map = folium.Map(location=[44.0, -103.0], zoom_start=8, tiles="CartoDB Positron")
717
+ for loc, (lat, lon) in PRAIRIE_LOCATIONS.items():
718
+ folium.Marker([lat, lon], popup=loc).add_to(prairie_map)
719
+ game_state = load_game_state(st.session_state.game_state_timestamp)
720
+ for player in game_state["players"].values():
721
+ folium.CircleMarker(
722
+ location=[44.0 + player["x"] * 0.01, -103.0 + player["z"] * 0.01],
723
+ radius=5,
724
+ color=f"#{player['color']:06x}",
725
+ fill=True,
726
+ fill_opacity=0.7,
727
+ popup=f"{player['username']} (Score: {player['score']}, Treasures: {player['treasures']})"
728
+ ).add_to(prairie_map)
729
+ for client_id, player in st.session_state.prairie_players.items():
730
+ folium.CircleMarker(
731
+ location=player['location'],
732
+ radius=5,
733
+ color=f"#{player['color']:06x}",
734
+ fill=True,
735
+ fill_opacity=0.7,
736
+ popup=f"{player['username']} ({player['animal']})"
737
+ ).add_to(prairie_map)
738
+ folium_static(prairie_map, width=600, height=400)
739
+
740
+ animal = st.selectbox("Choose Animal", ["prairie_dog", "deer", "sheep", "groundhog"])
741
+ location = st.selectbox("Move to", list(PRAIRIE_LOCATIONS.keys()))
742
+ if st.button("Move"):
743
+ asyncio.run(broadcast_message(f"{st.session_state.username}|PRAIRIE:ANIMAL:{animal}", "quest"))
744
+ asyncio.run(broadcast_message(f"{st.session_state.username}|PRAIRIE:MOVE:{location}", "quest"))
745
+ st.session_state.last_activity = time.time()
746
+ st.rerun()
747
+
748
+ elapsed = time.time() - st.session_state.last_update
749
+ remaining = max(0, st.session_state.update_interval - elapsed)
750
+ st.sidebar.markdown(f"โณ Next Update in: {int(remaining)}s")
751
+
752
+ if time.time() - st.session_state.last_activity > st.session_state.timeout:
753
+ st.sidebar.warning("Timed out! Refreshing in 5 seconds...")
754
+ time.sleep(5)
755
+ st.session_state.game_state_timestamp = time.time()
756
+ st.rerun()
757
+ elif time.time() - st.session_state.last_update > st.session_state.auto_refresh:
758
+ st.session_state.game_state_timestamp = time.time()
759
+ st.rerun()
760
+ elif remaining <= 0:
761
+ st.session_state.last_update = time.time()
762
+ st.session_state.game_state_timestamp = time.time()
763
+ st.rerun()
764
+
765
+ if not st.session_state.get('server_running', False):
766
+ st.session_state.server_task = threading.Thread(target=start_websocket_server, daemon=True)
767
+ st.session_state.server_task.start()
768
+
769
+ display_file_history_in_sidebar()
770
+ log_performance_metrics()
771
+
772
+ if __name__ == "__main__":
773
+ main()