# app.py (Merged Version - World Versioning) import streamlit as st import asyncio import websockets import uuid from datetime import datetime import os import random import time import hashlib # from PIL import Image import glob import base64 import io import streamlit.components.v1 as components import edge_tts # from audio_recorder_streamlit import audio_recorder import nest_asyncio import re import pytz import shutil # import anthropic # import openai from PyPDF2 import PdfReader import threading import json import zipfile # from gradio_client import Client from dotenv import load_dotenv from streamlit_marquee import streamlit_marquee from collections import defaultdict, Counter import pandas as pd from streamlit_js_eval import streamlit_js_eval from PIL import Image # Needed for paste_image_component # πŸ› οΈ Patch asyncio for nesting nest_asyncio.apply() # 🎨 Page Config st.set_page_config( page_title="πŸ€–πŸ—οΈ Shared World Builder πŸ†", page_icon="πŸ—οΈ", layout="wide", initial_sidebar_state="expanded" ) # --- Constants --- # General icons = 'πŸ€–πŸ—οΈπŸ—£οΈπŸ’Ύ' Site_Name = 'πŸ€–πŸ—οΈ Shared World Builder πŸ—£οΈ' START_ROOM = "World Lobby 🌍" MEDIA_DIR = "." # Base directory for general files STATE_FILE = "user_state.txt" # For remembering username # User/Chat FUN_USERNAMES = { "BuilderBot πŸ€–": "en-US-AriaNeural", "WorldWeaver πŸ•ΈοΈ": "en-US-JennyNeural", "Terraformer 🌱": "en-GB-SoniaNeural", "SkyArchitect ☁️": "en-AU-NatashaNeural", "PixelPainter 🎨": "en-CA-ClaraNeural", "VoxelVortex πŸŒͺ️": "en-US-GuyNeural", "CosmicCrafter ✨": "en-GB-RyanNeural", "GeoGuru πŸ—ΊοΈ": "en-AU-WilliamNeural", "BlockBard 🧱": "en-CA-LiamNeural", "SoundSculptor πŸ”Š": "en-US-AnaNeural", } EDGE_TTS_VOICES = list(set(FUN_USERNAMES.values())) CHAT_DIR = "chat_logs" # Audio AUDIO_CACHE_DIR = "audio_cache" AUDIO_DIR = "audio_logs" # World Builder SAVED_WORLDS_DIR = "saved_worlds" # Directory for MD world files PLOT_WIDTH = 50.0 # Needed for JS injection PLOT_DEPTH = 50.0 # Needed for JS injection # CSV_COLUMNS = [...] # No longer needed if not using CSVs # File Emojis FILE_EMOJIS = {"md": "πŸ“", "mp3": "🎡", "png": "πŸ–ΌοΈ", "mp4": "πŸŽ₯", "zip": "πŸ“¦", "json": "πŸ“„"} # --- Directories --- for d in [CHAT_DIR, AUDIO_DIR, AUDIO_CACHE_DIR, SAVED_WORLDS_DIR]: os.makedirs(d, exist_ok=True) # --- API Keys (Placeholder) --- load_dotenv() # --- Global State & Lock --- world_objects_lock = threading.Lock() world_objects = defaultdict(dict) # In-memory world state {obj_id: data} connected_clients = set() # Holds client_id strings # --- Helper Functions --- def get_current_time_str(tz='US/Central'): """Gets formatted timestamp string.""" try: timezone = pytz.timezone(tz) except pytz.UnknownTimeZoneError: timezone = pytz.utc return datetime.now(timezone).strftime('%Y%m%d_%H%M%S') def clean_filename_part(text, max_len=30): """Cleans a string part for use in a filename.""" text = re.sub(r'\s+', '_', text) # Replace spaces text = re.sub(r'[^\w\-.]', '', text) # Keep word chars, hyphen, period return text[:max_len] def generate_world_save_filename(name="World"): """Generates a filename for saving world state MD files.""" timestamp = get_current_time_str() clean_name = clean_filename_part(name) # Add short random hash to prevent collisions further rand_hash = hashlib.md5(str(time.time()).encode()).hexdigest()[:6] return f"🌍_{clean_name}_{timestamp}_{rand_hash}.md" def parse_world_filename(filename): """Extracts info from filename if possible, otherwise returns defaults.""" basename = os.path.basename(filename) parts = basename.split('_') if len(parts) >= 4 and parts[0] == '🌍' and parts[-1].endswith('.md'): name = " ".join(parts[1:-2]) # Join parts between emoji and date timestamp_str = parts[-2] try: # Attempt to parse timestamp for sorting/display dt_obj = datetime.strptime(timestamp_str, '%Y%m%d_%H%M%S') except ValueError: dt_obj = None # Could not parse timestamp return {"name": name, "timestamp": timestamp_str, "dt": dt_obj, "filename": filename} else: # Fallback for unknown format return {"name": basename.replace('.md',''), "timestamp": "Unknown", "dt": None, "filename": filename} # --- World State MD File Handling --- def save_world_state_to_md(target_filename): """Saves the current in-memory world state to a specific MD file.""" global world_objects save_path = os.path.join(SAVED_WORLDS_DIR, target_filename) print(f"Saving {len(world_objects)} objects to MD file: {save_path}...") # Prepare JSON data (acquire lock) with world_objects_lock: world_data_dict = dict(world_objects) # Format Markdown content parsed_info = parse_world_filename(target_filename) timestamp = get_current_time_str() # Current time for header md_content = f"""# World State: {parsed_info['name']} * **Saved:** {timestamp} * **Original Timestamp:** {parsed_info['timestamp']} * **Objects:** {len(world_data_dict)} ```json {json.dumps(world_data_dict, indent=2)}