Spaces:
Sleeping
Sleeping
# 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)} |