Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -17,6 +17,7 @@ from datetime import datetime
|
|
| 17 |
from PyPDF2 import PdfReader
|
| 18 |
import threading
|
| 19 |
from PIL import Image
|
|
|
|
| 20 |
|
| 21 |
# ==============================================================================
|
| 22 |
# Configuration & Constants
|
|
@@ -148,19 +149,26 @@ def update_player_state(username, position=None):
|
|
| 148 |
players[username]["last_action_timestamp"] = time.time()
|
| 149 |
state["players"] = players
|
| 150 |
write_history_file(state)
|
|
|
|
| 151 |
|
| 152 |
-
def add_object_to_state(obj_data):
|
| 153 |
state = read_history_file()
|
| 154 |
state = prune_inactive_players(state)
|
| 155 |
state["objects"][obj_data["obj_id"]] = obj_data
|
| 156 |
write_history_file(state)
|
|
|
|
|
|
|
| 157 |
|
| 158 |
-
def remove_object_from_state(obj_id):
|
| 159 |
state = read_history_file()
|
| 160 |
state = prune_inactive_players(state)
|
| 161 |
if obj_id in state["objects"]:
|
|
|
|
| 162 |
del state["objects"][obj_id]
|
| 163 |
write_history_file(state)
|
|
|
|
|
|
|
|
|
|
| 164 |
|
| 165 |
def log_action(username, action_type, data):
|
| 166 |
timestamp = get_current_time_str()
|
|
@@ -184,6 +192,41 @@ def log_action(username, action_type, data):
|
|
| 184 |
except Exception as e:
|
| 185 |
print(f"Error writing to player history log {player_log_file}: {e}")
|
| 186 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
# ==============================================================================
|
| 188 |
# User State & Session Init
|
| 189 |
# ==============================================================================
|
|
@@ -206,7 +249,7 @@ def load_username():
|
|
| 206 |
|
| 207 |
def init_session_state():
|
| 208 |
defaults = {
|
| 209 |
-
'
|
| 210 |
'audio_cache': {},
|
| 211 |
'tts_voice': "en-US-AriaNeural",
|
| 212 |
'chat_history': [],
|
|
@@ -217,7 +260,8 @@ def init_session_state():
|
|
| 217 |
'last_message': "",
|
| 218 |
'selected_object': 'None',
|
| 219 |
'paste_image_base64': "",
|
| 220 |
-
'new_world_name': "MyWorld"
|
|
|
|
| 221 |
}
|
| 222 |
for k, v in defaults.items():
|
| 223 |
if k not in st.session_state:
|
|
@@ -332,6 +376,7 @@ async def save_chat_entry(username, message, voice, is_markdown=False):
|
|
| 332 |
if st.session_state.get('enable_audio', True):
|
| 333 |
tts_message = message
|
| 334 |
audio_file = await async_edge_tts_generate(tts_message, voice, username)
|
|
|
|
| 335 |
return md_file, audio_file
|
| 336 |
|
| 337 |
async def load_chat_history():
|
|
@@ -613,12 +658,14 @@ def render_sidebar():
|
|
| 613 |
if st.session_state.selected_object != name:
|
| 614 |
st.session_state.selected_object = name
|
| 615 |
update_player_state(st.session_state.username)
|
|
|
|
| 616 |
col_idx += 1
|
| 617 |
st.markdown("---")
|
| 618 |
if st.button("🚫 Clear Tool", key="clear_tool", use_container_width=True):
|
| 619 |
if st.session_state.selected_object != 'None':
|
| 620 |
st.session_state.selected_object = 'None'
|
| 621 |
update_player_state(st.session_state.username)
|
|
|
|
| 622 |
|
| 623 |
st.markdown("---")
|
| 624 |
st.header("🗣️ Voice & User")
|
|
@@ -650,8 +697,9 @@ def render_main_content():
|
|
| 650 |
|
| 651 |
with tab_world:
|
| 652 |
st.header("Shared 3D World")
|
| 653 |
-
st.caption("Click to place objects with the selected tool. Right-click to delete. State is saved in history.json.")
|
| 654 |
state = read_history_file()
|
|
|
|
| 655 |
html_file_path = 'index.html'
|
| 656 |
try:
|
| 657 |
with open(html_file_path, 'r', encoding='utf-8') as f:
|
|
@@ -666,6 +714,7 @@ def render_main_content():
|
|
| 666 |
</script>"""
|
| 667 |
html_content_with_state = html_template.replace('</head>', js_injection_script + '\n</head>', 1)
|
| 668 |
components.html(html_content_with_state, height=700, scrolling=False)
|
|
|
|
| 669 |
except FileNotFoundError:
|
| 670 |
st.error(f"CRITICAL ERROR: Could not find '{html_file_path}'.")
|
| 671 |
except Exception as e:
|
|
@@ -682,7 +731,7 @@ def render_main_content():
|
|
| 682 |
else:
|
| 683 |
st.caption("No chat messages yet.")
|
| 684 |
|
| 685 |
-
message_value = st.text_input("Your Message:", key="
|
| 686 |
send_button_clicked = st.button("Send Chat 💬", key="send_chat_button")
|
| 687 |
should_autosend = st.session_state.get('autosend', False) and message_value
|
| 688 |
|
|
@@ -693,7 +742,7 @@ def render_main_content():
|
|
| 693 |
voice = FUN_USERNAMES.get(st.session_state.username, "en-US-AriaNeural")
|
| 694 |
asyncio.run(save_chat_entry(st.session_state.username, message_to_send, voice))
|
| 695 |
update_player_state(st.session_state.username)
|
| 696 |
-
st.session_state.
|
| 697 |
st.rerun()
|
| 698 |
elif send_button_clicked:
|
| 699 |
st.toast("Message empty or same as last.")
|
|
@@ -705,6 +754,7 @@ def render_main_content():
|
|
| 705 |
if st.button("Clear World State", key="clear_world_state"):
|
| 706 |
state = {"objects": {}, "players": {}}
|
| 707 |
write_history_file(state)
|
|
|
|
| 708 |
st.success("World state cleared!")
|
| 709 |
st.rerun()
|
| 710 |
|
|
|
|
| 17 |
from PyPDF2 import PdfReader
|
| 18 |
import threading
|
| 19 |
from PIL import Image
|
| 20 |
+
from streamlit_javascript import st_javascript
|
| 21 |
|
| 22 |
# ==============================================================================
|
| 23 |
# Configuration & Constants
|
|
|
|
| 149 |
players[username]["last_action_timestamp"] = time.time()
|
| 150 |
state["players"] = players
|
| 151 |
write_history_file(state)
|
| 152 |
+
return state
|
| 153 |
|
| 154 |
+
def add_object_to_state(obj_data, username):
|
| 155 |
state = read_history_file()
|
| 156 |
state = prune_inactive_players(state)
|
| 157 |
state["objects"][obj_data["obj_id"]] = obj_data
|
| 158 |
write_history_file(state)
|
| 159 |
+
log_action(username, "place", obj_data)
|
| 160 |
+
return state
|
| 161 |
|
| 162 |
+
def remove_object_from_state(obj_id, username):
|
| 163 |
state = read_history_file()
|
| 164 |
state = prune_inactive_players(state)
|
| 165 |
if obj_id in state["objects"]:
|
| 166 |
+
obj_data = state["objects"][obj_id]
|
| 167 |
del state["objects"][obj_id]
|
| 168 |
write_history_file(state)
|
| 169 |
+
log_action(username, "delete", {"obj_id": obj_id})
|
| 170 |
+
return state, obj_data
|
| 171 |
+
return state, None
|
| 172 |
|
| 173 |
def log_action(username, action_type, data):
|
| 174 |
timestamp = get_current_time_str()
|
|
|
|
| 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 |
+
# ==============================================================================
|
| 198 |
+
|
| 199 |
+
def handle_js_messages():
|
| 200 |
+
message = st_javascript("""
|
| 201 |
+
window.addEventListener('message', (event) => {
|
| 202 |
+
return JSON.stringify(event.data);
|
| 203 |
+
}, {once: true});
|
| 204 |
+
return null;
|
| 205 |
+
""")
|
| 206 |
+
if message:
|
| 207 |
+
try:
|
| 208 |
+
data = json.loads(message)
|
| 209 |
+
action = data.get("type")
|
| 210 |
+
payload = data.get("payload", {})
|
| 211 |
+
username = payload.get("username", st.session_state.username)
|
| 212 |
+
if action == "place_object":
|
| 213 |
+
state = add_object_to_state(payload["object_data"], username)
|
| 214 |
+
st.session_state.world_state = state
|
| 215 |
+
st.rerun()
|
| 216 |
+
elif action == "delete_object":
|
| 217 |
+
state, obj_data = remove_object_from_state(payload["obj_id"], username)
|
| 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:
|
| 226 |
+
print(f"Invalid JS message: {message}")
|
| 227 |
+
except Exception as e:
|
| 228 |
+
print(f"Error handling JS message: {e}")
|
| 229 |
+
|
| 230 |
# ==============================================================================
|
| 231 |
# User State & Session Init
|
| 232 |
# ==============================================================================
|
|
|
|
| 249 |
|
| 250 |
def init_session_state():
|
| 251 |
defaults = {
|
| 252 |
+
'message_counter': 0,
|
| 253 |
'audio_cache': {},
|
| 254 |
'tts_voice': "en-US-AriaNeural",
|
| 255 |
'chat_history': [],
|
|
|
|
| 260 |
'last_message': "",
|
| 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:
|
|
|
|
| 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():
|
|
|
|
| 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("---")
|
| 664 |
if st.button("🚫 Clear Tool", key="clear_tool", use_container_width=True):
|
| 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("---")
|
| 671 |
st.header("🗣️ Voice & User")
|
|
|
|
| 697 |
|
| 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 = read_history_file()
|
| 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:
|
|
|
|
| 714 |
</script>"""
|
| 715 |
html_content_with_state = html_template.replace('</head>', js_injection_script + '\n</head>', 1)
|
| 716 |
components.html(html_content_with_state, height=700, scrolling=False)
|
| 717 |
+
handle_js_messages()
|
| 718 |
except FileNotFoundError:
|
| 719 |
st.error(f"CRITICAL ERROR: Could not find '{html_file_path}'.")
|
| 720 |
except Exception as e:
|
|
|
|
| 731 |
else:
|
| 732 |
st.caption("No chat messages yet.")
|
| 733 |
|
| 734 |
+
message_value = st.text_input("Your Message:", key=f"message_input_{st.session_state.get('message_counter', 0)}", label_visibility="collapsed")
|
| 735 |
send_button_clicked = st.button("Send Chat 💬", key="send_chat_button")
|
| 736 |
should_autosend = st.session_state.get('autosend', False) and message_value
|
| 737 |
|
|
|
|
| 742 |
voice = FUN_USERNAMES.get(st.session_state.username, "en-US-AriaNeural")
|
| 743 |
asyncio.run(save_chat_entry(st.session_state.username, message_to_send, voice))
|
| 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()
|
| 747 |
elif send_button_clicked:
|
| 748 |
st.toast("Message empty or same as last.")
|
|
|
|
| 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 |
|