Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -16,6 +16,7 @@ import asyncio
|
|
16 |
from datetime import datetime
|
17 |
from PyPDF2 import PdfReader
|
18 |
import threading
|
|
|
19 |
from PIL import Image
|
20 |
from streamlit_javascript import st_javascript
|
21 |
|
@@ -105,7 +106,7 @@ def generate_filename(content, username, extension):
|
|
105 |
def initialize_history_file():
|
106 |
ensure_dir(SAVED_WORLDS_DIR)
|
107 |
if not os.path.exists(WORLD_STATE_FILE):
|
108 |
-
initial_state = {"objects": {}, "players": {}}
|
109 |
with open(WORLD_STATE_FILE, 'w', encoding='utf-8') as f:
|
110 |
json.dump(initial_state, f, indent=2)
|
111 |
|
@@ -113,10 +114,15 @@ def read_history_file():
|
|
113 |
initialize_history_file()
|
114 |
try:
|
115 |
with open(WORLD_STATE_FILE, 'r', encoding='utf-8') as f:
|
116 |
-
|
|
|
|
|
|
|
|
|
|
|
117 |
except Exception as e:
|
118 |
print(f"Error reading history file: {e}")
|
119 |
-
return {"objects": {}, "players": {}}
|
120 |
|
121 |
def write_history_file(state):
|
122 |
with state_lock:
|
@@ -149,26 +155,57 @@ def update_player_state(username, position=None):
|
|
149 |
players[username]["last_action_timestamp"] = time.time()
|
150 |
state["players"] = players
|
151 |
write_history_file(state)
|
|
|
|
|
152 |
return state
|
153 |
|
154 |
-
|
155 |
-
|
156 |
-
|
157 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
158 |
write_history_file(state)
|
159 |
-
log_action(username, "place", obj_data)
|
160 |
-
return state
|
161 |
|
162 |
-
|
|
|
|
|
|
|
|
|
|
|
163 |
state = read_history_file()
|
164 |
state = prune_inactive_players(state)
|
165 |
-
|
166 |
-
|
167 |
-
|
168 |
-
|
169 |
-
|
170 |
-
|
171 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
172 |
|
173 |
def log_action(username, action_type, data):
|
174 |
timestamp = get_current_time_str()
|
@@ -192,6 +229,96 @@ def log_action(username, action_type, data):
|
|
192 |
except Exception as e:
|
193 |
print(f"Error writing to player history log {player_log_file}: {e}")
|
194 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
195 |
# ==============================================================================
|
196 |
# JavaScript Message Handling
|
197 |
# ==============================================================================
|
@@ -210,16 +337,15 @@ def handle_js_messages():
|
|
210 |
payload = data.get("payload", {})
|
211 |
username = payload.get("username", st.session_state.username)
|
212 |
if action == "place_object":
|
213 |
-
state =
|
214 |
st.session_state.world_state = state
|
215 |
st.rerun()
|
216 |
elif action == "delete_object":
|
217 |
-
state
|
218 |
st.session_state.world_state = state
|
219 |
st.rerun()
|
220 |
elif action == "move_player":
|
221 |
state = update_player_state(username, payload["position"])
|
222 |
-
log_action(username, "move", payload["position"])
|
223 |
st.session_state.world_state = state
|
224 |
st.rerun()
|
225 |
except json.JSONDecodeError:
|
@@ -253,6 +379,7 @@ def init_session_state():
|
|
253 |
'audio_cache': {},
|
254 |
'tts_voice': "en-US-AriaNeural",
|
255 |
'chat_history': [],
|
|
|
256 |
'enable_audio': True,
|
257 |
'download_link_cache': {},
|
258 |
'username': None,
|
@@ -261,7 +388,7 @@ def init_session_state():
|
|
261 |
'selected_object': 'None',
|
262 |
'paste_image_base64': "",
|
263 |
'new_world_name': "MyWorld",
|
264 |
-
'world_state': {"objects": {}, "players": {}}
|
265 |
}
|
266 |
for k, v in defaults.items():
|
267 |
if k not in st.session_state:
|
@@ -270,9 +397,17 @@ def init_session_state():
|
|
270 |
st.session_state.audio_cache = {}
|
271 |
if not isinstance(st.session_state.download_link_cache, dict):
|
272 |
st.session_state.download_link_cache = {}
|
|
|
|
|
273 |
if 'username' not in st.session_state or not st.session_state.username:
|
274 |
-
|
|
|
275 |
save_username(st.session_state.username)
|
|
|
|
|
|
|
|
|
|
|
276 |
update_player_state(st.session_state.username)
|
277 |
|
278 |
# ==============================================================================
|
@@ -324,34 +459,6 @@ def get_download_link(file_path, file_type="md"):
|
|
324 |
return f"<small>Err</small>"
|
325 |
return st.session_state.download_link_cache.get(cache_key, "<small>CacheErr</small>")
|
326 |
|
327 |
-
async def async_edge_tts_generate(text, voice, username):
|
328 |
-
if not text:
|
329 |
-
return None
|
330 |
-
cache_key = hashlib.md5(f"{text[:150]}_{voice}".encode()).hexdigest()
|
331 |
-
if 'audio_cache' not in st.session_state:
|
332 |
-
st.session_state.audio_cache = {}
|
333 |
-
cached_path = st.session_state.audio_cache.get(cache_key)
|
334 |
-
if cached_path and os.path.exists(cached_path):
|
335 |
-
return cached_path
|
336 |
-
text_cleaned = clean_text_for_tts(text)
|
337 |
-
if not text_cleaned or text_cleaned == "No text":
|
338 |
-
return None
|
339 |
-
filename_base = generate_filename(text_cleaned, username, "mp3")
|
340 |
-
save_path = os.path.join(AUDIO_DIR, filename_base)
|
341 |
-
ensure_dir(AUDIO_DIR)
|
342 |
-
try:
|
343 |
-
communicate = edge_tts.Communicate(text_cleaned, voice)
|
344 |
-
await communicate.save(save_path)
|
345 |
-
if os.path.exists(save_path) and os.path.getsize(save_path) > 0:
|
346 |
-
st.session_state.audio_cache[cache_key] = save_path
|
347 |
-
return save_path
|
348 |
-
else:
|
349 |
-
print(f"Audio file {save_path} failed generation.")
|
350 |
-
return None
|
351 |
-
except Exception as e:
|
352 |
-
print(f"Edge TTS Error: {e}")
|
353 |
-
return None
|
354 |
-
|
355 |
def play_and_download_audio(file_path):
|
356 |
if file_path and os.path.exists(file_path):
|
357 |
try:
|
@@ -361,24 +468,6 @@ def play_and_download_audio(file_path):
|
|
361 |
except Exception as e:
|
362 |
st.error(f"Audio display error for {os.path.basename(file_path)}: {e}")
|
363 |
|
364 |
-
async def save_chat_entry(username, message, voice, is_markdown=False):
|
365 |
-
if not message.strip():
|
366 |
-
return None, None
|
367 |
-
timestamp_str = get_current_time_str()
|
368 |
-
entry = f"[{timestamp_str}] {username} ({voice}): {message}" if not is_markdown else f"[{timestamp_str}] {username} ({voice}):\n```markdown\n{message}\n```"
|
369 |
-
md_filename_base = generate_filename(message, username, "md")
|
370 |
-
md_file_path = os.path.join(CHAT_DIR, md_filename_base)
|
371 |
-
md_file = create_file(entry, username, "md", save_path=md_file_path)
|
372 |
-
if 'chat_history' not in st.session_state:
|
373 |
-
st.session_state.chat_history = []
|
374 |
-
st.session_state.chat_history.append(entry)
|
375 |
-
audio_file = None
|
376 |
-
if st.session_state.get('enable_audio', True):
|
377 |
-
tts_message = message
|
378 |
-
audio_file = await async_edge_tts_generate(tts_message, voice, username)
|
379 |
-
log_action(username, "chat", {"message": message})
|
380 |
-
return md_file, audio_file
|
381 |
-
|
382 |
async def load_chat_history():
|
383 |
if 'chat_history' not in st.session_state:
|
384 |
st.session_state.chat_history = []
|
@@ -607,6 +696,7 @@ def render_sidebar():
|
|
607 |
st.header("๐พ World State")
|
608 |
st.caption("Manage the shared world state.")
|
609 |
|
|
|
610 |
state = read_history_file()
|
611 |
players = state.get("players", {})
|
612 |
st.subheader("Active Players")
|
@@ -616,6 +706,57 @@ def render_sidebar():
|
|
616 |
minutes_ago = (current_time - last_action) / 60
|
617 |
st.write(f"{username}: Last active {minutes_ago:.1f} minutes ago")
|
618 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
619 |
st.markdown("---")
|
620 |
st.header("๐๏ธ Build Tools")
|
621 |
st.caption("Select an object to place.")
|
@@ -658,6 +799,7 @@ def render_sidebar():
|
|
658 |
if st.session_state.selected_object != name:
|
659 |
st.session_state.selected_object = name
|
660 |
update_player_state(st.session_state.username)
|
|
|
661 |
st.rerun()
|
662 |
col_idx += 1
|
663 |
st.markdown("---")
|
@@ -665,6 +807,7 @@ def render_sidebar():
|
|
665 |
if st.session_state.selected_object != 'None':
|
666 |
st.session_state.selected_object = 'None'
|
667 |
update_player_state(st.session_state.username)
|
|
|
668 |
st.rerun()
|
669 |
|
670 |
st.markdown("---")
|
@@ -687,6 +830,7 @@ def render_sidebar():
|
|
687 |
st.session_state.tts_voice = FUN_USERNAMES[new_username]
|
688 |
save_username(st.session_state.username)
|
689 |
update_player_state(st.session_state.username)
|
|
|
690 |
st.rerun()
|
691 |
st.session_state['enable_audio'] = st.toggle("Enable TTS Audio", value=st.session_state.get('enable_audio', True))
|
692 |
|
@@ -698,8 +842,7 @@ def render_main_content():
|
|
698 |
with tab_world:
|
699 |
st.header("Shared 3D World")
|
700 |
st.caption("Click to place objects with the selected tool, or click to move player. Right-click to delete. State is saved in history.json.")
|
701 |
-
state =
|
702 |
-
st.session_state.world_state = state
|
703 |
html_file_path = 'index.html'
|
704 |
try:
|
705 |
with open(html_file_path, 'r', encoding='utf-8') as f:
|
@@ -740,7 +883,7 @@ def render_main_content():
|
|
740 |
if message_to_send.strip() and message_to_send != st.session_state.get('last_message', ''):
|
741 |
st.session_state.last_message = message_to_send
|
742 |
voice = FUN_USERNAMES.get(st.session_state.username, "en-US-AriaNeural")
|
743 |
-
asyncio.run(
|
744 |
update_player_state(st.session_state.username)
|
745 |
st.session_state.message_counter = st.session_state.get('message_counter', 0) + 1
|
746 |
st.rerun()
|
@@ -752,9 +895,10 @@ def render_main_content():
|
|
752 |
st.header("๐ Files & Settings")
|
753 |
st.subheader("๐พ World State Management")
|
754 |
if st.button("Clear World State", key="clear_world_state"):
|
755 |
-
state = {"objects": {}, "players": {}}
|
756 |
write_history_file(state)
|
757 |
st.session_state.world_state = state
|
|
|
758 |
st.success("World state cleared!")
|
759 |
st.rerun()
|
760 |
|
@@ -764,7 +908,7 @@ def render_main_content():
|
|
764 |
col_zip1, col_zip2, col_zip3 = st.columns(3)
|
765 |
with col_zip1:
|
766 |
if st.button("Zip Worlds"):
|
767 |
-
create_zip_of_files([WORLD_STATE_FILE], "Worlds")
|
768 |
with col_zip2:
|
769 |
if st.button("Zip Chats"):
|
770 |
create_zip_of_files(glob.glob(os.path.join(CHAT_DIR, "*.md")), "Chats")
|
|
|
16 |
from datetime import datetime
|
17 |
from PyPDF2 import PdfReader
|
18 |
import threading
|
19 |
+
import pandas as pd
|
20 |
from PIL import Image
|
21 |
from streamlit_javascript import st_javascript
|
22 |
|
|
|
106 |
def initialize_history_file():
|
107 |
ensure_dir(SAVED_WORLDS_DIR)
|
108 |
if not os.path.exists(WORLD_STATE_FILE):
|
109 |
+
initial_state = {"objects": {}, "players": {}, "action_history": []}
|
110 |
with open(WORLD_STATE_FILE, 'w', encoding='utf-8') as f:
|
111 |
json.dump(initial_state, f, indent=2)
|
112 |
|
|
|
114 |
initialize_history_file()
|
115 |
try:
|
116 |
with open(WORLD_STATE_FILE, 'r', encoding='utf-8') as f:
|
117 |
+
state = json.load(f)
|
118 |
+
# Ensure all expected keys exist
|
119 |
+
state.setdefault("objects", {})
|
120 |
+
state.setdefault("players", {})
|
121 |
+
state.setdefault("action_history", [])
|
122 |
+
return state
|
123 |
except Exception as e:
|
124 |
print(f"Error reading history file: {e}")
|
125 |
+
return {"objects": {}, "players": {}, "action_history": []}
|
126 |
|
127 |
def write_history_file(state):
|
128 |
with state_lock:
|
|
|
155 |
players[username]["last_action_timestamp"] = time.time()
|
156 |
state["players"] = players
|
157 |
write_history_file(state)
|
158 |
+
if position:
|
159 |
+
update_action_history(username, "move", {"position": position}, state)
|
160 |
return state
|
161 |
|
162 |
+
# ๐ Update Action History: Records actions in session state and history file
|
163 |
+
def update_action_history(username, action_type, data, state):
|
164 |
+
timestamp = get_current_time_str()
|
165 |
+
action_entry = {
|
166 |
+
"timestamp": timestamp,
|
167 |
+
"username": username,
|
168 |
+
"action": action_type,
|
169 |
+
"data": data
|
170 |
+
}
|
171 |
+
if 'action_history' not in st.session_state:
|
172 |
+
st.session_state.action_history = []
|
173 |
+
st.session_state.action_history.append(action_entry)
|
174 |
+
state["action_history"].append(action_entry)
|
175 |
write_history_file(state)
|
|
|
|
|
176 |
|
177 |
+
# ๐ ๏ธ Persist World Objects: Validates and saves object data to history.json
|
178 |
+
def persist_world_objects(obj_data, username, action_type):
|
179 |
+
if not obj_data or not isinstance(obj_data, dict) or 'obj_id' not in obj_data:
|
180 |
+
print(f"Invalid object data for {action_type}: {obj_data}")
|
181 |
+
return read_history_file()
|
182 |
+
|
183 |
state = read_history_file()
|
184 |
state = prune_inactive_players(state)
|
185 |
+
|
186 |
+
if action_type == "place":
|
187 |
+
if 'type' not in obj_data or obj_data['type'] not in PRIMITIVE_MAP.values():
|
188 |
+
print(f"Invalid object type: {obj_data.get('type', 'None')}")
|
189 |
+
return state
|
190 |
+
state["objects"][obj_data['obj_id']] = obj_data
|
191 |
+
update_action_history(username, "place", {
|
192 |
+
"obj_id": obj_data['obj_id'],
|
193 |
+
"type": obj_data['type'],
|
194 |
+
"position": obj_data['position']
|
195 |
+
}, state)
|
196 |
+
elif action_type == "delete":
|
197 |
+
if obj_data['obj_id'] in state["objects"]:
|
198 |
+
obj_info = state["objects"][obj_data['obj_id']]
|
199 |
+
del state["objects"][obj_data['obj_id']]
|
200 |
+
update_action_history(username, "delete", {
|
201 |
+
"obj_id": obj_data['obj_id'],
|
202 |
+
"type": obj_info['type'],
|
203 |
+
"position": obj_info['position']
|
204 |
+
}, state)
|
205 |
+
|
206 |
+
write_history_file(state)
|
207 |
+
log_action(username, action_type, obj_data)
|
208 |
+
return state
|
209 |
|
210 |
def log_action(username, action_type, data):
|
211 |
timestamp = get_current_time_str()
|
|
|
229 |
except Exception as e:
|
230 |
print(f"Error writing to player history log {player_log_file}: {e}")
|
231 |
|
232 |
+
# ๐ฉ Save and Log Chat: Saves chat message to chat_logs and logs to history_logs
|
233 |
+
async def save_and_log_chat(username, message, voice):
|
234 |
+
if not message.strip():
|
235 |
+
print("Empty chat message, skipping save.")
|
236 |
+
return None, None
|
237 |
+
|
238 |
+
timestamp_str = get_current_time_str()
|
239 |
+
entry = f"[{timestamp_str}] {username} ({voice}): {message}"
|
240 |
+
md_filename_base = generate_filename(message, username, "md")
|
241 |
+
md_file_path = os.path.join(CHAT_DIR, md_filename_base)
|
242 |
+
|
243 |
+
try:
|
244 |
+
ensure_dir(CHAT_DIR)
|
245 |
+
with open(md_file_path, 'w', encoding='utf-8') as f:
|
246 |
+
f.write(entry)
|
247 |
+
except Exception as e:
|
248 |
+
print(f"Error saving chat to {md_file_path}: {e}")
|
249 |
+
return None, None
|
250 |
+
|
251 |
+
if 'chat_history' not in st.session_state:
|
252 |
+
st.session_state.chat_history = []
|
253 |
+
st.session_state.chat_history.append(entry)
|
254 |
+
|
255 |
+
audio_file = None
|
256 |
+
if st.session_state.get('enable_audio', True):
|
257 |
+
text_cleaned = clean_text_for_tts(message)
|
258 |
+
if text_cleaned and text_cleaned != "No text":
|
259 |
+
filename_base = generate_filename(text_cleaned, username, "mp3")
|
260 |
+
save_path = os.path.join(AUDIO_DIR, filename_base)
|
261 |
+
ensure_dir(AUDIO_DIR)
|
262 |
+
try:
|
263 |
+
communicate = edge_tts.Communicate(text_cleaned, voice)
|
264 |
+
await communicate.save(save_path)
|
265 |
+
if os.path.exists(save_path) and os.path.getsize(save_path) > 0:
|
266 |
+
audio_file = save_path
|
267 |
+
else:
|
268 |
+
print(f"Audio file {save_path} failed generation.")
|
269 |
+
except Exception as e:
|
270 |
+
print(f"Edge TTS Error: {e}")
|
271 |
+
|
272 |
+
state = read_history_file()
|
273 |
+
update_action_history(username, "chat", {"message": message}, state)
|
274 |
+
return md_file_path, audio_file
|
275 |
+
|
276 |
+
# ๐พ Save World State: Saves current state to a named file
|
277 |
+
def save_world_state(world_name):
|
278 |
+
if not world_name.strip():
|
279 |
+
st.error("World name cannot be empty.")
|
280 |
+
return False
|
281 |
+
clean_name = clean_filename_part(world_name)
|
282 |
+
timestamp = get_current_time_str()
|
283 |
+
filename = f"world_{clean_name}_{timestamp}.json"
|
284 |
+
save_path = os.path.join(SAVED_WORLDS_DIR, filename)
|
285 |
+
|
286 |
+
state = read_history_file()
|
287 |
+
try:
|
288 |
+
with open(save_path, 'w', encoding='utf-8') as f:
|
289 |
+
json.dump(state, f, indent=2)
|
290 |
+
print(f"Saved world state to {save_path}")
|
291 |
+
st.success(f"Saved world as {filename}")
|
292 |
+
return True
|
293 |
+
except Exception as e:
|
294 |
+
print(f"Error saving world state to {save_path}: {e}")
|
295 |
+
st.error(f"Failed to save world: {e}")
|
296 |
+
return False
|
297 |
+
|
298 |
+
# ๐ Load World State: Loads a saved state from a file
|
299 |
+
def load_world_state(filename):
|
300 |
+
load_path = os.path.join(SAVED_WORLDS_DIR, filename)
|
301 |
+
if not os.path.exists(load_path):
|
302 |
+
st.error(f"World file not found: {filename}")
|
303 |
+
return False
|
304 |
+
try:
|
305 |
+
with open(load_path, 'r', encoding='utf-8') as f:
|
306 |
+
state = json.load(f)
|
307 |
+
state.setdefault("objects", {})
|
308 |
+
state.setdefault("players", {})
|
309 |
+
state.setdefault("action_history", [])
|
310 |
+
write_history_file(state)
|
311 |
+
st.session_state.world_state = state
|
312 |
+
st.session_state.action_history = state["action_history"]
|
313 |
+
print(f"Loaded world state from {load_path}")
|
314 |
+
st.success(f"Loaded world {filename}")
|
315 |
+
st.rerun()
|
316 |
+
return True
|
317 |
+
except Exception as e:
|
318 |
+
print(f"Error loading world state from {load_path}: {e}")
|
319 |
+
st.error(f"Failed to load world: {e}")
|
320 |
+
return False
|
321 |
+
|
322 |
# ==============================================================================
|
323 |
# JavaScript Message Handling
|
324 |
# ==============================================================================
|
|
|
337 |
payload = data.get("payload", {})
|
338 |
username = payload.get("username", st.session_state.username)
|
339 |
if action == "place_object":
|
340 |
+
state = persist_world_objects(payload["object_data"], username, "place")
|
341 |
st.session_state.world_state = state
|
342 |
st.rerun()
|
343 |
elif action == "delete_object":
|
344 |
+
state = persist_world_objects({"obj_id": payload["obj_id"]}, username, "delete")
|
345 |
st.session_state.world_state = state
|
346 |
st.rerun()
|
347 |
elif action == "move_player":
|
348 |
state = update_player_state(username, payload["position"])
|
|
|
349 |
st.session_state.world_state = state
|
350 |
st.rerun()
|
351 |
except json.JSONDecodeError:
|
|
|
379 |
'audio_cache': {},
|
380 |
'tts_voice': "en-US-AriaNeural",
|
381 |
'chat_history': [],
|
382 |
+
'action_history': [],
|
383 |
'enable_audio': True,
|
384 |
'download_link_cache': {},
|
385 |
'username': None,
|
|
|
388 |
'selected_object': 'None',
|
389 |
'paste_image_base64': "",
|
390 |
'new_world_name': "MyWorld",
|
391 |
+
'world_state': {"objects": {}, "players": {}, "action_history": []}
|
392 |
}
|
393 |
for k, v in defaults.items():
|
394 |
if k not in st.session_state:
|
|
|
397 |
st.session_state.audio_cache = {}
|
398 |
if not isinstance(st.session_state.download_link_cache, dict):
|
399 |
st.session_state.download_link_cache = {}
|
400 |
+
if not isinstance(st.session_state.action_history, list):
|
401 |
+
st.session_state.action_history = []
|
402 |
if 'username' not in st.session_state or not st.session_state.username:
|
403 |
+
saved_username = load_username()
|
404 |
+
st.session_state.username = saved_username if saved_username else random.choice(list(FUN_USERNAMES.keys()))
|
405 |
save_username(st.session_state.username)
|
406 |
+
|
407 |
+
# Load history on startup
|
408 |
+
state = read_history_file()
|
409 |
+
st.session_state.world_state = state
|
410 |
+
st.session_state.action_history = state.get("action_history", [])
|
411 |
update_player_state(st.session_state.username)
|
412 |
|
413 |
# ==============================================================================
|
|
|
459 |
return f"<small>Err</small>"
|
460 |
return st.session_state.download_link_cache.get(cache_key, "<small>CacheErr</small>")
|
461 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
462 |
def play_and_download_audio(file_path):
|
463 |
if file_path and os.path.exists(file_path):
|
464 |
try:
|
|
|
468 |
except Exception as e:
|
469 |
st.error(f"Audio display error for {os.path.basename(file_path)}: {e}")
|
470 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
471 |
async def load_chat_history():
|
472 |
if 'chat_history' not in st.session_state:
|
473 |
st.session_state.chat_history = []
|
|
|
696 |
st.header("๐พ World State")
|
697 |
st.caption("Manage the shared world state.")
|
698 |
|
699 |
+
# Active Players
|
700 |
state = read_history_file()
|
701 |
players = state.get("players", {})
|
702 |
st.subheader("Active Players")
|
|
|
706 |
minutes_ago = (current_time - last_action) / 60
|
707 |
st.write(f"{username}: Last active {minutes_ago:.1f} minutes ago")
|
708 |
|
709 |
+
# Action History Dataset
|
710 |
+
st.subheader("Action History")
|
711 |
+
history_data = []
|
712 |
+
for entry in st.session_state.action_history:
|
713 |
+
data = entry["data"]
|
714 |
+
if entry["action"] in ["place", "delete"]:
|
715 |
+
pos = data.get("position", {})
|
716 |
+
position_str = f"({pos.get('x', 0):.1f}, {pos.get('y', 0):.1f}, {pos.get('z', 0):.1f})"
|
717 |
+
history_data.append({
|
718 |
+
"Time": entry["timestamp"],
|
719 |
+
"Player": entry["username"],
|
720 |
+
"Action": entry["action"].capitalize(),
|
721 |
+
"Object Type": data.get("type", "N/A"),
|
722 |
+
"Position": position_str
|
723 |
+
})
|
724 |
+
elif entry["action"] == "move":
|
725 |
+
pos = data.get("position", {})
|
726 |
+
position_str = f"({pos.get('x', 0):.1f}, {pos.get('y', 0):.1f}, {pos.get('z', 0):.1f})"
|
727 |
+
history_data.append({
|
728 |
+
"Time": entry["timestamp"],
|
729 |
+
"Player": entry["username"],
|
730 |
+
"Action": "Move",
|
731 |
+
"Object Type": "Player",
|
732 |
+
"Position": position_str
|
733 |
+
})
|
734 |
+
elif entry["action"] == "chat":
|
735 |
+
history_data.append({
|
736 |
+
"Time": entry["timestamp"],
|
737 |
+
"Player": entry["username"],
|
738 |
+
"Action": "Chat",
|
739 |
+
"Object Type": "Message",
|
740 |
+
"Position": data.get("message", "N/A")[:50]
|
741 |
+
})
|
742 |
+
if history_data:
|
743 |
+
st.dataframe(pd.DataFrame(history_data), height=200, use_container_width=True)
|
744 |
+
else:
|
745 |
+
st.caption("No actions recorded yet.")
|
746 |
+
|
747 |
+
# Save World
|
748 |
+
st.subheader("Save World")
|
749 |
+
world_name = st.text_input("World Name", value="MyWorld", key="save_world_name")
|
750 |
+
if st.button("๐พ Save World", key="save_world"):
|
751 |
+
save_world_state(world_name)
|
752 |
+
|
753 |
+
# Load World
|
754 |
+
st.subheader("Load World")
|
755 |
+
saved_worlds = [os.path.basename(f) for f in glob.glob(os.path.join(SAVED_WORLDS_DIR, "world_*.json"))]
|
756 |
+
selected_world = st.selectbox("Select Saved World", ["None"] + saved_worlds, key="load_world_select")
|
757 |
+
if selected_world != "None" and st.button("๐ Load World", key="load_world"):
|
758 |
+
load_world_state(selected_world)
|
759 |
+
|
760 |
st.markdown("---")
|
761 |
st.header("๐๏ธ Build Tools")
|
762 |
st.caption("Select an object to place.")
|
|
|
799 |
if st.session_state.selected_object != name:
|
800 |
st.session_state.selected_object = name
|
801 |
update_player_state(st.session_state.username)
|
802 |
+
update_action_history(st.session_state.username, "tool_change", {"tool": name}, read_history_file())
|
803 |
st.rerun()
|
804 |
col_idx += 1
|
805 |
st.markdown("---")
|
|
|
807 |
if st.session_state.selected_object != 'None':
|
808 |
st.session_state.selected_object = 'None'
|
809 |
update_player_state(st.session_state.username)
|
810 |
+
update_action_history(st.session_state.username, "tool_change", {"tool": "None"}, read_history_file())
|
811 |
st.rerun()
|
812 |
|
813 |
st.markdown("---")
|
|
|
830 |
st.session_state.tts_voice = FUN_USERNAMES[new_username]
|
831 |
save_username(st.session_state.username)
|
832 |
update_player_state(st.session_state.username)
|
833 |
+
update_action_history(st.session_state.username, "rename", {"old_username": old_username, "new_username": new_username}, state)
|
834 |
st.rerun()
|
835 |
st.session_state['enable_audio'] = st.toggle("Enable TTS Audio", value=st.session_state.get('enable_audio', True))
|
836 |
|
|
|
842 |
with tab_world:
|
843 |
st.header("Shared 3D World")
|
844 |
st.caption("Click to place objects with the selected tool, or click to move player. Right-click to delete. State is saved in history.json.")
|
845 |
+
state = st.session_state.world_state
|
|
|
846 |
html_file_path = 'index.html'
|
847 |
try:
|
848 |
with open(html_file_path, 'r', encoding='utf-8') as f:
|
|
|
883 |
if message_to_send.strip() and message_to_send != st.session_state.get('last_message', ''):
|
884 |
st.session_state.last_message = message_to_send
|
885 |
voice = FUN_USERNAMES.get(st.session_state.username, "en-US-AriaNeural")
|
886 |
+
asyncio.run(save_and_log_chat(st.session_state.username, message_to_send, voice))
|
887 |
update_player_state(st.session_state.username)
|
888 |
st.session_state.message_counter = st.session_state.get('message_counter', 0) + 1
|
889 |
st.rerun()
|
|
|
895 |
st.header("๐ Files & Settings")
|
896 |
st.subheader("๐พ World State Management")
|
897 |
if st.button("Clear World State", key="clear_world_state"):
|
898 |
+
state = {"objects": {}, "players": {}, "action_history": []}
|
899 |
write_history_file(state)
|
900 |
st.session_state.world_state = state
|
901 |
+
st.session_state.action_history = []
|
902 |
st.success("World state cleared!")
|
903 |
st.rerun()
|
904 |
|
|
|
908 |
col_zip1, col_zip2, col_zip3 = st.columns(3)
|
909 |
with col_zip1:
|
910 |
if st.button("Zip Worlds"):
|
911 |
+
create_zip_of_files([WORLD_STATE_FILE] + glob.glob(os.path.join(SAVED_WORLDS_DIR, "world_*.json")), "Worlds")
|
912 |
with col_zip2:
|
913 |
if st.button("Zip Chats"):
|
914 |
create_zip_of_files(glob.glob(os.path.join(CHAT_DIR, "*.md")), "Chats")
|