awacke1 commited on
Commit
1ebfa27
Β·
verified Β·
1 Parent(s): fb13a36

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +843 -0
app.py ADDED
@@ -0,0 +1,843 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import asyncio
3
+ import websockets
4
+ import uuid
5
+ import argparse
6
+ from datetime import datetime
7
+ import os
8
+ import random
9
+ import time
10
+ import hashlib
11
+ from PIL import Image
12
+ import glob
13
+ import base64
14
+ import io
15
+ import streamlit.components.v1 as components
16
+ import edge_tts
17
+ from audio_recorder_streamlit import audio_recorder
18
+ import nest_asyncio
19
+ import re
20
+ from streamlit_paste_button import paste_image_button
21
+ import pytz
22
+ import shutil
23
+
24
+ # Patch for nested async - sneaky fix! 🐍✨
25
+ nest_asyncio.apply()
26
+
27
+ # Static config - constants rule! πŸ“πŸ‘‘
28
+ icons = 'πŸ€–πŸ§ πŸ”¬πŸ“'
29
+ START_ROOM = "Sector 🌌"
30
+
31
+ # Page setup - dressing up the window! πŸ–ΌοΈπŸŽ€
32
+ st.set_page_config(
33
+ page_title="πŸ€–πŸ§ MMO Chat BrainπŸ“πŸ”¬",
34
+ page_icon=icons,
35
+ layout="wide",
36
+ initial_sidebar_state="auto"
37
+ )
38
+
39
+ # Funky usernames - who’s who in the zoo with unique voices! πŸŽ­πŸΎπŸŽ™οΈ
40
+ FUN_USERNAMES = {
41
+ "CosmicJester 🌌": "en-US-AriaNeural",
42
+ "PixelPanda 🐼": "en-US-JennyNeural",
43
+ "QuantumQuack πŸ¦†": "en-GB-SoniaNeural",
44
+ "StellarSquirrel 🐿️": "en-AU-NatashaNeural",
45
+ "GizmoGuru βš™οΈ": "en-CA-ClaraNeural",
46
+ "NebulaNinja 🌠": "en-US-GuyNeural",
47
+ "ByteBuster πŸ’Ύ": "en-GB-RyanNeural",
48
+ "GalacticGopher 🌍": "en-AU-WilliamNeural",
49
+ "RocketRaccoon πŸš€": "en-CA-LiamNeural",
50
+ "EchoElf 🧝": "en-US-AnaNeural",
51
+ "PhantomFox 🦊": "en-US-BrandonNeural",
52
+ "WittyWizard πŸ§™": "en-GB-ThomasNeural",
53
+ "LunarLlama πŸŒ™": "en-AU-FreyaNeural",
54
+ "SolarSloth β˜€οΈ": "en-CA-LindaNeural",
55
+ "AstroAlpaca πŸ¦™": "en-US-ChristopherNeural",
56
+ "CyberCoyote 🐺": "en-GB-ElliotNeural",
57
+ "MysticMoose 🦌": "en-AU-JamesNeural",
58
+ "GlitchGnome 🧚": "en-CA-EthanNeural",
59
+ "VortexViper 🐍": "en-US-AmberNeural",
60
+ "ChronoChimp πŸ’": "en-GB-LibbyNeural"
61
+ }
62
+
63
+ # Folders galore - organizing chaos! πŸ“‚πŸŒ€
64
+ CHAT_DIR = "chat_logs"
65
+ VOTE_DIR = "vote_logs"
66
+ STATE_FILE = "user_state.txt"
67
+ AUDIO_DIR = "audio_logs"
68
+ HISTORY_DIR = "history_logs"
69
+ MEDIA_DIR = "media_files"
70
+ os.makedirs(CHAT_DIR, exist_ok=True)
71
+ os.makedirs(VOTE_DIR, exist_ok=True)
72
+ os.makedirs(AUDIO_DIR, exist_ok=True)
73
+ os.makedirs(HISTORY_DIR, exist_ok=True)
74
+ os.makedirs(MEDIA_DIR, exist_ok=True)
75
+
76
+ CHAT_FILE = os.path.join(CHAT_DIR, "global_chat.md")
77
+ QUOTE_VOTES_FILE = os.path.join(VOTE_DIR, "quote_votes.md")
78
+ MEDIA_VOTES_FILE = os.path.join(VOTE_DIR, "media_votes.md")
79
+ HISTORY_FILE = os.path.join(HISTORY_DIR, "chat_history.md")
80
+
81
+ # Fancy digits - numbers got style! πŸ”’πŸ’ƒ
82
+ UNICODE_DIGITS = {i: f"{i}\uFE0F⃣" for i in range(10)}
83
+
84
+ # Massive font collection - typography bonanza! πŸ–‹οΈπŸŽ¨
85
+ UNICODE_FONTS = [
86
+ ("Normal", lambda x: x),
87
+ ("Bold", lambda x: "".join(chr(ord(c) + 0x1D400 - 0x41) if 'A' <= c <= 'Z' else chr(ord(c) + 0x1D41A - 0x61) if 'a' <= c <= 'z' else c for c in x)),
88
+ ("Italic", lambda x: "".join(chr(ord(c) + 0x1D434 - 0x41) if 'A' <= c <= 'Z' else chr(ord(c) + 0x1D44E - 0x61) if 'a' <= c <= 'z' else c for c in x)),
89
+ ("Bold Italic", lambda x: "".join(chr(ord(c) + 0x1D468 - 0x41) if 'A' <= c <= 'Z' else chr(ord(c) + 0x1D482 - 0x61) if 'a' <= c <= 'z' else c for c in x)),
90
+ ("Script", lambda x: "".join(chr(ord(c) + 0x1D49C - 0x41) if 'A' <= c <= 'Z' else chr(ord(c) + 0x1D4B6 - 0x61) if 'a' <= c <= 'z' else c for c in x)),
91
+ ("Bold Script", lambda x: "".join(chr(ord(c) + 0x1D4D0 - 0x41) if 'A' <= c <= 'Z' else chr(ord(c) + 0x1D4EA - 0x61) if 'a' <= c <= 'z' else c for c in x)),
92
+ ("Fraktur", lambda x: "".join(chr(ord(c) + 0x1D504 - 0x41) if 'A' <= c <= 'Z' else chr(ord(c) + 0x1D51E - 0x61) if 'a' <= c <= 'z' else c for c in x)),
93
+ ("Bold Fraktur", lambda x: "".join(chr(ord(c) + 0x1D56C - 0x41) if 'A' <= c <= 'Z' else chr(ord(c) + 0x1D586 - 0x61) if 'a' <= c <= 'z' else c for c in x)),
94
+ ("Double Struck", lambda x: "".join(chr(ord(c) + 0x1D538 - 0x41) if 'A' <= c <= 'Z' else chr(ord(c) + 0x1D552 - 0x61) if 'a' <= c <= 'z' else c for c in x)),
95
+ ("Sans Serif", lambda x: "".join(chr(ord(c) + 0x1D5A0 - 0x41) if 'A' <= c <= 'Z' else chr(ord(c) + 0x1D5BA - 0x61) if 'a' <= c <= 'z' else c for c in x)),
96
+ ("Sans Serif Bold", lambda x: "".join(chr(ord(c) + 0x1D5D4 - 0x41) if 'A' <= c <= 'Z' else chr(ord(c) + 0x1D5EE - 0x61) if 'a' <= c <= 'z' else c for c in x)),
97
+ ("Sans Serif Italic", lambda x: "".join(chr(ord(c) + 0x1D608 - 0x41) if 'A' <= c <= 'Z' else chr(ord(c) + 0x1D622 - 0x61) if 'a' <= c <= 'z' else c for c in x)),
98
+ ("Sans Serif Bold Italic", lambda x: "".join(chr(ord(c) + 0x1D63C - 0x41) if 'A' <= c <= 'Z' else chr(ord(c) + 0x1D656 - 0x61) if 'a' <= c <= 'z' else c for c in x)),
99
+ ("Monospace", lambda x: "".join(chr(ord(c) + 0x1D670 - 0x41) if 'A' <= c <= 'Z' else chr(ord(c) + 0x1D68A - 0x61) if 'a' <= c <= 'z' else c for c in x)),
100
+ ("Circled", lambda x: "".join(chr(ord(c) - 0x41 + 0x24B6) if 'A' <= c <= 'Z' else chr(ord(c) - 0x61 + 0x24D0) if 'a' <= c <= 'z' else c for c in x)),
101
+ ("Squared", lambda x: "".join(chr(ord(c) - 0x41 + 0x1F130) if 'A' <= c <= 'Z' else c for c in x)),
102
+ ("Negative Circled", lambda x: "".join(chr(ord(c) - 0x41 + 0x1F150) if 'A' <= c <= 'Z' else c for c in x)),
103
+ ("Negative Squared", lambda x: "".join(chr(ord(c) - 0x41 + 0x1F170) if 'A' <= c <= 'Z' else c for c in x)),
104
+ ("Regional Indicator", lambda x: "".join(chr(ord(c) - 0x41 + 0x1F1E6) if 'A' <= c <= 'Z' else c for c in x)),
105
+ ]
106
+
107
+ # Global state - keeping tabs! πŸŒπŸ“‹
108
+ if 'server_running' not in st.session_state:
109
+ st.session_state.server_running = False
110
+ if 'server_task' not in st.session_state:
111
+ st.session_state.server_task = None
112
+ if 'active_connections' not in st.session_state:
113
+ st.session_state.active_connections = {}
114
+ if 'media_notifications' not in st.session_state:
115
+ st.session_state.media_notifications = []
116
+ if 'last_chat_update' not in st.session_state:
117
+ st.session_state.last_chat_update = 0
118
+ if 'displayed_chat_lines' not in st.session_state:
119
+ st.session_state.displayed_chat_lines = []
120
+ if 'old_val' not in st.session_state:
121
+ st.session_state.old_val = ""
122
+ if 'last_query' not in st.session_state:
123
+ st.session_state.last_query = ""
124
+ if 'message_text' not in st.session_state:
125
+ st.session_state.message_text = ""
126
+ if 'audio_cache' not in st.session_state:
127
+ st.session_state.audio_cache = {}
128
+ if 'pasted_image_data' not in st.session_state:
129
+ st.session_state.pasted_image_data = None
130
+ if 'quote_line' not in st.session_state:
131
+ st.session_state.quote_line = None
132
+ if 'refresh_rate' not in st.session_state:
133
+ st.session_state.refresh_rate = 5
134
+ if 'base64_cache' not in st.session_state:
135
+ st.session_state.base64_cache = {}
136
+ if 'transcript_history' not in st.session_state:
137
+ st.session_state.transcript_history = []
138
+ if 'last_transcript' not in st.session_state:
139
+ st.session_state.last_transcript = ""
140
+ if 'image_hashes' not in st.session_state:
141
+ st.session_state.image_hashes = set()
142
+
143
+ # Timestamp wizardry - clock ticks with flair! ⏰🎩
144
+ def format_timestamp_prefix(username):
145
+ central = pytz.timezone('US/Central')
146
+ now = datetime.now(central)
147
+ return f"{now.strftime('%I-%M-%p-ct-%m-%d-%Y')}-by-{username}"
148
+
149
+ # Compute image hash from binary data
150
+ def compute_image_hash(image_data):
151
+ if isinstance(image_data, Image.Image):
152
+ img_byte_arr = io.BytesIO()
153
+ image_data.save(img_byte_arr, format='PNG')
154
+ img_bytes = img_byte_arr.getvalue()
155
+ else:
156
+ img_bytes = image_data
157
+ return hashlib.md5(img_bytes).hexdigest()[:8]
158
+
159
+ # Node naming - christening the beast! 🌐🍼
160
+ def get_node_name():
161
+ parser = argparse.ArgumentParser(description='Start a chat node with a specific name')
162
+ parser.add_argument('--node-name', type=str, default=None)
163
+ parser.add_argument('--port', type=int, default=8501)
164
+ args = parser.parse_args()
165
+ username = st.session_state.get('username', 'System 🌟')
166
+ log_action(username, "🌐🍼 - Node naming - christening the beast!")
167
+ return args.node_name or f"node-{uuid.uuid4().hex[:8]}", args.port
168
+
169
+ # Action logger - spying on deeds! πŸ•΅οΈπŸ“œ
170
+ def log_action(username, action):
171
+ if 'action_log' not in st.session_state:
172
+ st.session_state.action_log = {}
173
+ user_log = st.session_state.action_log.setdefault(username, {})
174
+ current_time = time.time()
175
+ user_log = {k: v for k, v in user_log.items() if current_time - v < 10}
176
+ st.session_state.action_log[username] = user_log
177
+ if action not in user_log:
178
+ central = pytz.timezone('US/Central')
179
+ with open(HISTORY_FILE, 'a') as f:
180
+ f.write(f"[{datetime.now(central).strftime('%Y-%m-%d %H:%M:%S')}] {username}: {action}\n")
181
+ user_log[action] = current_time
182
+
183
+ # Clean text - strip the fancy stuff! πŸ§ΉπŸ“
184
+ def clean_text_for_tts(text):
185
+ cleaned = re.sub(r'[#*!\[\]]+', '', text)
186
+ cleaned = ' '.join(cleaned.split())
187
+ return cleaned[:200] if cleaned else "No text to speak"
188
+
189
+ # Chat saver - words locked tight! πŸ’¬πŸ”’
190
+ async def save_chat_entry(username, message, is_markdown=False):
191
+ await asyncio.to_thread(log_action, username, "πŸ’¬πŸ”’ - Chat saver - words locked tight!")
192
+ central = pytz.timezone('US/Central')
193
+ timestamp = datetime.now(central).strftime("%Y-%m-%d %H:%M:%S")
194
+ if is_markdown:
195
+ entry = f"[{timestamp}] {username}:\n```markdown\n{message}\n```"
196
+ else:
197
+ entry = f"[{timestamp}] {username}: {message}"
198
+ await asyncio.to_thread(lambda: open(CHAT_FILE, 'a').write(f"{entry}\n"))
199
+ voice = FUN_USERNAMES.get(username, "en-US-AriaNeural")
200
+ cleaned_message = clean_text_for_tts(message)
201
+ audio_file = await async_edge_tts_generate(cleaned_message, voice)
202
+ if audio_file:
203
+ with open(HISTORY_FILE, 'a') as f:
204
+ f.write(f"[{timestamp}] {username}: Audio generated - {audio_file}\n")
205
+ await broadcast_message(f"{username}|{message}", "chat")
206
+ st.session_state.last_chat_update = time.time()
207
+ return audio_file
208
+
209
+ # Chat loader - history unleashed! πŸ“œπŸš€
210
+ async def load_chat():
211
+ username = st.session_state.get('username', 'System 🌟')
212
+ await asyncio.to_thread(log_action, username, "πŸ“œπŸš€ - Chat loader - history unleashed!")
213
+ if not os.path.exists(CHAT_FILE):
214
+ await asyncio.to_thread(lambda: open(CHAT_FILE, 'a').write(f"# {START_ROOM} Chat\n\nWelcome to the cosmic hub - start chatting! 🎀\n"))
215
+ with open(CHAT_FILE, 'r') as f:
216
+ content = await asyncio.to_thread(f.read)
217
+ return content
218
+
219
+ # User lister - who’s in the gang! πŸ‘₯πŸŽ‰
220
+ async def get_user_list(chat_content):
221
+ username = st.session_state.get('username', 'System 🌟')
222
+ await asyncio.to_thread(log_action, username, "πŸ‘₯πŸŽ‰ - User lister - who’s in the gang!")
223
+ users = set()
224
+ for line in chat_content.split('\n'):
225
+ if line.strip() and ': ' in line:
226
+ user = line.split(': ')[1].split(' ')[0]
227
+ users.add(user)
228
+ return sorted(list(users))
229
+
230
+ # Join checker - been here before? πŸšͺπŸ”
231
+ async def has_joined_before(client_id, chat_content):
232
+ username = st.session_state.get('username', 'System 🌟')
233
+ await asyncio.to_thread(log_action, username, "πŸšͺπŸ” - Join checker - been here before?")
234
+ return any(f"Client-{client_id}" in line for line in chat_content.split('\n'))
235
+
236
+ # Suggestion maker - old quips resurface! πŸ’‘πŸ“
237
+ async def get_message_suggestions(chat_content, prefix):
238
+ username = st.session_state.get('username', 'System 🌟')
239
+ await asyncio.to_thread(log_action, username, "πŸ’‘πŸ“ - Suggestion maker - old quips resurface!")
240
+ lines = chat_content.split('\n')
241
+ messages = [line.split(': ', 1)[1] for line in lines if ': ' in line and line.strip()]
242
+ return [msg for msg in messages if msg.lower().startswith(prefix.lower())][:5]
243
+
244
+ # Vote saver - cheers recorded! πŸ‘πŸ“Š
245
+ async def save_vote(file, item, user_hash, username, comment=""):
246
+ await asyncio.to_thread(log_action, username, "πŸ‘πŸ“Š - Vote saver - cheers recorded!")
247
+ central = pytz.timezone('US/Central')
248
+ timestamp = datetime.now(central).strftime("%Y-%m-%d %H:%M:%S")
249
+ entry = f"[{timestamp}] {user_hash} voted for {item}"
250
+ await asyncio.to_thread(lambda: open(file, 'a').write(f"{entry}\n"))
251
+ await asyncio.to_thread(lambda: open(HISTORY_FILE, "a").write(f"- {timestamp} - User {user_hash} voted for {item}\n"))
252
+ chat_message = f"{username} upvoted: \"{item}\""
253
+ if comment:
254
+ chat_message += f" - {comment}"
255
+ await save_chat_entry(username, chat_message)
256
+
257
+ # Vote counter - tallying the love! πŸ†πŸ“ˆ
258
+ async def load_votes(file):
259
+ username = st.session_state.get('username', 'System 🌟')
260
+ await asyncio.to_thread(log_action, username, "πŸ†πŸ“ˆ - Vote counter - tallying the love!")
261
+ if not os.path.exists(file):
262
+ await asyncio.to_thread(lambda: open(file, 'w').write("# Vote Tally\n\nNo votes yet - get clicking! πŸ–±οΈ\n"))
263
+ with open(file, 'r') as f:
264
+ content = await asyncio.to_thread(f.read)
265
+ lines = content.strip().split('\n')[2:]
266
+ votes = {}
267
+ user_votes = set()
268
+ for line in lines:
269
+ if line.strip() and 'voted for' in line:
270
+ user_hash = line.split('] ')[1].split(' voted for ')[0]
271
+ item = line.split('voted for ')[1]
272
+ vote_key = f"{user_hash}-{item}"
273
+ if vote_key not in user_votes:
274
+ votes[item] = votes.get(item, 0) + 1
275
+ user_votes.add(vote_key)
276
+ return votes
277
+
278
+ # Hash generator - secret codes ahoy! πŸ”‘πŸ•΅οΈ
279
+ async def generate_user_hash():
280
+ username = st.session_state.get('username', 'System 🌟')
281
+ await asyncio.to_thread(log_action, username, "πŸ”‘πŸ•΅οΈ - Hash generator - secret codes ahoy!")
282
+ if 'user_hash' not in st.session_state:
283
+ st.session_state.user_hash = hashlib.md5(str(random.getrandbits(128)).encode()).hexdigest()[:8]
284
+ return st.session_state.user_hash
285
+
286
+ # Audio maker - voices come alive! 🎢🌟
287
+ async def async_edge_tts_generate(text, voice, rate=0, pitch=0, file_format="mp3"):
288
+ username = st.session_state.get('username', 'System 🌟')
289
+ await asyncio.to_thread(log_action, username, "🎢🌟 - Audio maker - voices come alive!")
290
+ timestamp = format_timestamp_prefix(username)
291
+ filename = f"{timestamp}.{file_format}"
292
+ filepath = os.path.join(AUDIO_DIR, filename)
293
+ communicate = edge_tts.Communicate(text, voice, rate=f"{rate:+d}%", pitch=f"{pitch:+d}Hz")
294
+ try:
295
+ await communicate.save(filepath)
296
+ return filepath if os.path.exists(filepath) else None
297
+ except edge_tts.exceptions.NoAudioReceived:
298
+ with open(HISTORY_FILE, 'a') as f:
299
+ central = pytz.timezone('US/Central')
300
+ f.write(f"[{datetime.now(central).strftime('%Y-%m-%d %H:%M:%S')}] {username}: Audio failed - No audio received for '{text}'\n")
301
+ return None
302
+
303
+ # Audio player - tunes blast off! πŸ”ŠπŸš€
304
+ def play_and_download_audio(file_path):
305
+ if file_path and os.path.exists(file_path):
306
+ st.audio(file_path)
307
+ if file_path not in st.session_state.base64_cache:
308
+ with open(file_path, "rb") as f:
309
+ b64 = base64.b64encode(f.read()).decode()
310
+ st.session_state.base64_cache[file_path] = b64
311
+ b64 = st.session_state.base64_cache[file_path]
312
+ dl_link = f'<a href="data:audio/mpeg;base64,{b64}" download="{os.path.basename(file_path)}">🎡 Download {os.path.basename(file_path)}</a>'
313
+ st.markdown(dl_link, unsafe_allow_html=True)
314
+
315
+ # Image saver - pics preserved with naming! πŸ“ΈπŸ’Ύ
316
+ async def save_pasted_image(image, username):
317
+ await asyncio.to_thread(log_action, username, "πŸ“ΈπŸ’Ύ - Image saver - pics preserved!")
318
+ img_hash = compute_image_hash(image)
319
+ if img_hash in st.session_state.image_hashes:
320
+ return None
321
+ timestamp = format_timestamp_prefix(username)
322
+ filename = f"{timestamp}-{img_hash}.png"
323
+ filepath = os.path.join(MEDIA_DIR, filename)
324
+ await asyncio.to_thread(image.save, filepath, "PNG")
325
+ st.session_state.image_hashes.add(img_hash)
326
+ return filepath
327
+
328
+ # Video renderer - movies roll with autoplay! πŸŽ₯🎬
329
+ async def get_video_html(video_path, width="100%"):
330
+ username = st.session_state.get('username', 'System 🌟')
331
+ await asyncio.to_thread(log_action, username, "πŸŽ₯🎬 - Video renderer - movies roll!")
332
+ with open(video_path, 'rb') as f:
333
+ video_data = await asyncio.to_thread(f.read)
334
+ video_url = f"data:video/mp4;base64,{base64.b64encode(video_data).decode()}"
335
+ return f'<video width="{width}" controls autoplay><source src="{video_url}" type="video/mp4">Your browser does not support the video tag.</video>'
336
+
337
+ # Audio renderer - sounds soar! 🎢✈️
338
+ async def get_audio_html(audio_path, width="100%"):
339
+ username = st.session_state.get('username', 'System 🌟')
340
+ await asyncio.to_thread(log_action, username, "🎢✈️ - Audio renderer - sounds soar!")
341
+ audio_url = f"data:audio/mpeg;base64,{base64.b64encode(await asyncio.to_thread(open, audio_path, 'rb').read()).decode()}"
342
+ return f'<audio controls style="width: {width};"><source src="{audio_url}" type="audio/mpeg">Your browser does not support the audio element.</audio>'
343
+
344
+ # Websocket handler - chat links up! πŸŒπŸ”—
345
+ async def websocket_handler(websocket, path):
346
+ username = st.session_state.get('username', 'System 🌟')
347
+ await asyncio.to_thread(log_action, username, "πŸŒπŸ”— - Websocket handler - chat links up!")
348
+ try:
349
+ client_id = str(uuid.uuid4())
350
+ room_id = "chat"
351
+ st.session_state.active_connections.setdefault(room_id, {})[client_id] = websocket
352
+ chat_content = await load_chat()
353
+ username = st.session_state.get('username', random.choice(list(FUN_USERNAMES.keys())))
354
+ if not await has_joined_before(client_id, chat_content):
355
+ await save_chat_entry(f"Client-{client_id}", f"{username} has joined {START_ROOM}!")
356
+ async for message in websocket:
357
+ parts = message.split('|', 1)
358
+ if len(parts) == 2:
359
+ username, content = parts
360
+ await save_chat_entry(username, content)
361
+ except websockets.ConnectionClosed:
362
+ pass
363
+ finally:
364
+ if room_id in st.session_state.active_connections and client_id in st.session_state.active_connections[room_id]:
365
+ del st.session_state.active_connections[room_id][client_id]
366
+
367
+ # Message broadcaster - words fly far! πŸ“’βœˆοΈ
368
+ async def broadcast_message(message, room_id):
369
+ username = st.session_state.get('username', 'System 🌟')
370
+ await asyncio.to_thread(log_action, username, "πŸ“’βœˆοΈ - Message broadcaster - words fly far!")
371
+ if room_id in st.session_state.active_connections:
372
+ disconnected = []
373
+ for client_id, ws in st.session_state.active_connections[room_id].items():
374
+ try:
375
+ await ws.send(message)
376
+ except websockets.ConnectionClosed:
377
+ disconnected.append(client_id)
378
+ for client_id in disconnected:
379
+ del st.session_state.active_connections[room_id][client_id]
380
+
381
+ # Server starter - web spins up! πŸ–₯οΈπŸŒ€
382
+ async def run_websocket_server():
383
+ username = st.session_state.get('username', 'System 🌟')
384
+ await asyncio.to_thread(log_action, username, "πŸ–₯οΈπŸŒ€ - Server starter - web spins up!")
385
+ if not st.session_state.server_running:
386
+ server = await websockets.serve(websocket_handler, '0.0.0.0', 8765)
387
+ st.session_state.server_running = True
388
+ await server.wait_closed()
389
+
390
+ # Voice processor - speech to text! πŸŽ€πŸ“
391
+ async def process_voice_input(audio_bytes):
392
+ username = st.session_state.get('username', 'System 🌟')
393
+ await asyncio.to_thread(log_action, username, "πŸŽ€πŸ“ - Voice processor - speech to text!")
394
+ if audio_bytes:
395
+ text = "Voice input simulation"
396
+ await save_chat_entry(username, text)
397
+
398
+ # Dummy AI lookup function (replace with actual implementation)
399
+ async def perform_ai_lookup(query, vocal_summary=True, extended_refs=False, titles_summary=True, full_audio=False, useArxiv=True, useArxivAudio=False):
400
+ username = st.session_state.get('username', 'System 🌟')
401
+ result = f"AI Lookup Result for '{query}' (Arxiv: {useArxiv}, Audio: {useArxivAudio})"
402
+ await save_chat_entry(username, result)
403
+ if useArxivAudio:
404
+ audio_file = await async_edge_tts_generate(result, FUN_USERNAMES.get(username, "en-US-AriaNeural"))
405
+ if audio_file:
406
+ st.audio(audio_file)
407
+
408
+ # Delete all user files function
409
+ def delete_user_files():
410
+ protected_files = {'app.py', 'requirements.txt', 'README.md'}
411
+ deleted_files = []
412
+
413
+ # Directories to clear
414
+ directories = [MEDIA_DIR, AUDIO_DIR, CHAT_DIR, VOTE_DIR, HISTORY_DIR]
415
+
416
+ for directory in directories:
417
+ if os.path.exists(directory):
418
+ for root, _, files in os.walk(directory):
419
+ for file in files:
420
+ file_path = os.path.join(root, file)
421
+ if os.path.basename(file_path) not in protected_files:
422
+ try:
423
+ os.remove(file_path)
424
+ deleted_files.append(file_path)
425
+ except Exception as e:
426
+ st.error(f"Failed to delete {file_path}: {e}")
427
+ # Remove empty directories
428
+ try:
429
+ shutil.rmtree(directory, ignore_errors=True)
430
+ os.makedirs(directory, exist_ok=True) # Recreate empty directory
431
+ except Exception as e:
432
+ st.error(f"Failed to remove directory {directory}: {e}")
433
+
434
+ # Clear session state caches
435
+ st.session_state.image_hashes.clear()
436
+ st.session_state.audio_cache.clear()
437
+ st.session_state.base64_cache.clear()
438
+ st.session_state.displayed_chat_lines.clear()
439
+
440
+ return deleted_files
441
+
442
+ # ASR Component HTML
443
+ ASR_HTML = """
444
+ <html>
445
+ <head>
446
+ <title>Continuous Speech Demo</title>
447
+ <style>
448
+ body {
449
+ font-family: sans-serif;
450
+ padding: 20px;
451
+ max-width: 800px;
452
+ margin: 0 auto;
453
+ }
454
+ button {
455
+ padding: 10px 20px;
456
+ margin: 10px 5px;
457
+ font-size: 16px;
458
+ }
459
+ #status {
460
+ margin: 10px 0;
461
+ padding: 10px;
462
+ background: #e8f5e9;
463
+ border-radius: 4px;
464
+ }
465
+ #output {
466
+ white-space: pre-wrap;
467
+ padding: 15px;
468
+ background: #f5f5f5;
469
+ border-radius: 4px;
470
+ margin: 10px 0;
471
+ min-height: 100px;
472
+ max-height: 400px;
473
+ overflow-y: auto;
474
+ }
475
+ .controls {
476
+ margin: 10px 0;
477
+ }
478
+ </style>
479
+ </head>
480
+ <body>
481
+ <div class="controls">
482
+ <button id="start">Start Listening</button>
483
+ <button id="stop" disabled>Stop Listening</button>
484
+ <button id="clear">Clear Text</button>
485
+ </div>
486
+ <div id="status">Ready</div>
487
+ <div id="output"></div>
488
+
489
+ <script>
490
+ if (!('webkitSpeechRecognition' in window)) {
491
+ alert('Speech recognition not supported');
492
+ } else {
493
+ const recognition = new webkitSpeechRecognition();
494
+ const startButton = document.getElementById('start');
495
+ const stopButton = document.getElementById('stop');
496
+ const clearButton = document.getElementById('clear');
497
+ const status = document.getElementById('status');
498
+ const output = document.getElementById('output');
499
+ let fullTranscript = '';
500
+ let lastUpdateTime = Date.now();
501
+
502
+ recognition.continuous = true;
503
+ recognition.interimResults = true;
504
+
505
+ const startRecognition = () => {
506
+ try {
507
+ recognition.start();
508
+ status.textContent = 'Listening...';
509
+ startButton.disabled = true;
510
+ stopButton.disabled = false;
511
+ } catch (e) {
512
+ console.error(e);
513
+ status.textContent = 'Error: ' + e.message;
514
+ }
515
+ };
516
+
517
+ window.addEventListener('load', () => {
518
+ setTimeout(startRecognition, 1000);
519
+ });
520
+
521
+ startButton.onclick = startRecognition;
522
+
523
+ stopButton.onclick = () => {
524
+ recognition.stop();
525
+ status.textContent = 'Stopped';
526
+ startButton.disabled = false;
527
+ stopButton.disabled = true;
528
+ };
529
+
530
+ clearButton.onclick = () => {
531
+ fullTranscript = '';
532
+ output.textContent = '';
533
+ sendDataToPython({value: '', dataType: "json"});
534
+ };
535
+
536
+ recognition.onresult = (event) => {
537
+ let interimTranscript = '';
538
+ let finalTranscript = '';
539
+
540
+ for (let i = event.resultIndex; i < event.results.length; i++) {
541
+ const transcript = event.results[i][0].transcript;
542
+ if (event.results[i].isFinal) {
543
+ finalTranscript += transcript + '\\n';
544
+ } else {
545
+ interimTranscript += transcript;
546
+ }
547
+ }
548
+
549
+ if (finalTranscript || (Date.now() - lastUpdateTime > 5000)) {
550
+ if (finalTranscript) {
551
+ fullTranscript += finalTranscript;
552
+ }
553
+ lastUpdateTime = Date.now();
554
+ output.textContent = fullTranscript + (interimTranscript ? '... ' + interimTranscript : '');
555
+ output.scrollTop = output.scrollHeight;
556
+ sendDataToPython({value: fullTranscript, dataType: "json"});
557
+ }
558
+ };
559
+
560
+ recognition.onend = () => {
561
+ if (!stopButton.disabled) {
562
+ try {
563
+ recognition.start();
564
+ console.log('Restarted recognition');
565
+ } catch (e) {
566
+ console.error('Failed to restart recognition:', e);
567
+ status.textContent = 'Error restarting: ' + e.message;
568
+ startButton.disabled = false;
569
+ stopButton.disabled = true;
570
+ }
571
+ }
572
+ };
573
+
574
+ recognition.onerror = (event) => {
575
+ console.error('Recognition error:', event.error);
576
+ status.textContent = 'Error: ' + event.error;
577
+ if (event.error === 'not-allowed' || event.error === 'service-not-allowed') {
578
+ startButton.disabled = false;
579
+ stopButton.disabled = true;
580
+ }
581
+ };
582
+ }
583
+
584
+ function sendDataToPython(data) {
585
+ window.parent.postMessage({
586
+ isStreamlitMessage: true,
587
+ type: "streamlit:setComponentValue",
588
+ ...data
589
+ }, "*");
590
+ }
591
+
592
+ window.addEventListener('load', function() {
593
+ window.setTimeout(function() {
594
+ window.parent.postMessage({
595
+ isStreamlitMessage: true,
596
+ type: "streamlit:setFrameHeight",
597
+ height: document.documentElement.clientHeight
598
+ }, "*");
599
+ }, 0);
600
+ });
601
+ </script>
602
+ </body>
603
+ </html>
604
+ """
605
+
606
+ # Main execution - let’s roll! πŸŽ²πŸš€
607
+ def main():
608
+ NODE_NAME, port = get_node_name()
609
+
610
+ loop = asyncio.new_event_loop()
611
+ asyncio.set_event_loop(loop)
612
+
613
+ async def async_interface():
614
+ if 'username' not in st.session_state:
615
+ chat_content = await load_chat()
616
+ available_names = [name for name in FUN_USERNAMES if not any(f"{name} has joined" in line for line in chat_content.split('\n'))]
617
+ st.session_state.username = random.choice(available_names) if available_names else random.choice(list(FUN_USERNAMES.keys()))
618
+ st.session_state.voice = FUN_USERNAMES[st.session_state.username]
619
+ st.markdown(f"**πŸŽ™οΈ Voice Selected**: {st.session_state.voice} πŸ—£οΈ for {st.session_state.username}")
620
+
621
+ st.title(f"πŸ€–πŸ§ MMO {st.session_state.username}πŸ“πŸ”¬")
622
+ st.markdown(f"Welcome to {START_ROOM} - chat, vote, upload, paste images, and enjoy quoting! πŸŽ‰")
623
+
624
+ if not st.session_state.server_task:
625
+ st.session_state.server_task = loop.create_task(run_websocket_server())
626
+
627
+ audio_bytes = audio_recorder()
628
+ if audio_bytes:
629
+ await process_voice_input(audio_bytes)
630
+ st.rerun()
631
+
632
+ # Continuous Speech Input (ASR)
633
+ st.subheader("🎀 Continuous Speech Input")
634
+ asr_component = components.html(ASR_HTML, height=400)
635
+ if asr_component and isinstance(asr_component, dict) and 'value' in asr_component:
636
+ transcript = asr_component['value'].strip()
637
+ if transcript and transcript != st.session_state.last_transcript:
638
+ st.session_state.transcript_history.append(transcript)
639
+ await save_chat_entry(st.session_state.username, transcript, is_markdown=True)
640
+ st.session_state.last_transcript = transcript
641
+ st.rerun()
642
+
643
+ # Load and display chat
644
+ st.subheader(f"{START_ROOM} Chat πŸ’¬")
645
+ chat_content = await load_chat()
646
+ chat_lines = chat_content.split('\n')
647
+ chat_votes = await load_votes(QUOTE_VOTES_FILE)
648
+
649
+ current_time = time.time()
650
+ if current_time - st.session_state.last_chat_update > 1 or not st.session_state.displayed_chat_lines:
651
+ new_lines = [line for line in chat_lines if line.strip() and ': ' in line and line not in st.session_state.displayed_chat_lines and not line.startswith('#')]
652
+ st.session_state.displayed_chat_lines.extend(new_lines)
653
+ st.session_state.last_chat_update = current_time
654
+
655
+ for i, line in enumerate(st.session_state.displayed_chat_lines):
656
+ col1, col2, col3, col4 = st.columns([3, 1, 1, 2])
657
+ with col1:
658
+ if "```markdown" in line:
659
+ markdown_content = re.search(r'```markdown\n(.*?)```', line, re.DOTALL)
660
+ if markdown_content:
661
+ st.markdown(markdown_content.group(1))
662
+ else:
663
+ st.markdown(line)
664
+ else:
665
+ st.markdown(line)
666
+ if "Pasted image:" in line or "Uploaded media:" in line:
667
+ file_path = line.split(': ')[-1].strip()
668
+ if os.path.exists(file_path):
669
+ if file_path not in st.session_state.base64_cache:
670
+ with open(file_path, "rb") as f:
671
+ b64 = base64.b64encode(f.read()).decode()
672
+ st.session_state.base64_cache[file_path] = b64
673
+ b64 = st.session_state.base64_cache[file_path]
674
+ mime_type = "image/png" if file_path.endswith(('.png', '.jpg')) else "video/mp4" if file_path.endswith('.mp4') else "audio/mpeg"
675
+ if file_path.endswith(('.png', '.jpg')):
676
+ st.image(file_path, use_container_width=True)
677
+ dl_link = f'<a href="data:{mime_type};base64,{b64}" download="{os.path.basename(file_path)}">πŸ“₯ Download {os.path.basename(file_path)}</a>'
678
+ st.markdown(dl_link, unsafe_allow_html=True)
679
+ elif file_path.endswith('.mp4'):
680
+ st.markdown(await get_video_html(file_path), unsafe_allow_html=True)
681
+ dl_link = f'<a href="data:{mime_type};base64,{b64}" download="{os.path.basename(file_path)}">πŸ“₯ Download {os.path.basename(file_path)}</a>'
682
+ st.markdown(dl_link, unsafe_allow_html=True)
683
+ elif file_path.endswith('.mp3'):
684
+ st.markdown(await get_audio_html(file_path), unsafe_allow_html=True)
685
+ dl_link = f'<a href="data:{mime_type};base64,{b64}" download="{os.path.basename(file_path)}">πŸ“₯ Download {os.path.basename(file_path)}</a>'
686
+ st.markdown(dl_link, unsafe_allow_html=True)
687
+ with col2:
688
+ vote_count = chat_votes.get(line.split('. ')[1] if '. ' in line else line, 0)
689
+ if st.button(f"πŸ‘ {vote_count}", key=f"chat_vote_{i}"):
690
+ comment = st.session_state.message_text
691
+ await save_vote(QUOTE_VOTES_FILE, line.split('. ')[1] if '. ' in line else line, await generate_user_hash(), st.session_state.username, comment)
692
+ st.session_state.message_text = ''
693
+ st.rerun()
694
+ with col3:
695
+ if st.button("πŸ“’ Quote", key=f"quote_{i}"):
696
+ st.session_state.quote_line = line
697
+ st.rerun()
698
+ with col4:
699
+ username = line.split(': ')[1].split(' ')[0]
700
+ cache_key = f"{line}_{FUN_USERNAMES.get(username, 'en-US-AriaNeural')}"
701
+ if cache_key not in st.session_state.audio_cache:
702
+ cleaned_text = clean_text_for_tts(line.split(': ', 1)[1])
703
+ audio_file = await async_edge_tts_generate(cleaned_text, FUN_USERNAMES.get(username, "en-US-AriaNeural"))
704
+ st.session_state.audio_cache[cache_key] = audio_file
705
+ audio_file = st.session_state.audio_cache.get(cache_key)
706
+ if audio_file:
707
+ play_and_download_audio(audio_file)
708
+
709
+ if st.session_state.quote_line:
710
+ st.markdown(f"### Quoting: {st.session_state.quote_line}")
711
+ quote_response = st.text_area("Add your response", key="quote_response", value=st.session_state.message_text)
712
+ paste_result_quote = paste_image_button("πŸ“‹ Paste Image or Text with Quote", key="paste_button_quote")
713
+ if paste_result_quote.image_data is not None:
714
+ if isinstance(paste_result_quote.image_data, str):
715
+ st.session_state.message_text = paste_result_quote.image_data
716
+ st.text_area("Add your response", key="quote_response", value=st.session_state.message_text)
717
+ else:
718
+ st.image(paste_result_quote.image_data, caption="Received Image for Quote")
719
+ filename = await save_pasted_image(paste_result_quote.image_data, st.session_state.username)
720
+ if filename:
721
+ st.session_state.pasted_image_data = filename
722
+ if st.button("Send Quote πŸš€", key="send_quote"):
723
+ markdown_response = f"### Quote Response\n- **Original**: {st.session_state.quote_line}\n- **{st.session_state.username} Replies**: {quote_response}"
724
+ if st.session_state.pasted_image_data:
725
+ markdown_response += f"\n- **Image**: ![Pasted Image]({st.session_state.pasted_image_data})"
726
+ await save_chat_entry(st.session_state.username, f"Pasted image: {st.session_state.pasted_image_data}")
727
+ st.session_state.pasted_image_data = None
728
+ await save_chat_entry(st.session_state.username, markdown_response, is_markdown=True)
729
+ st.session_state.quote_line = None
730
+ st.session_state.message_text = ''
731
+ st.rerun()
732
+
733
+ current_selection = st.session_state.username if st.session_state.username in FUN_USERNAMES else ""
734
+ new_username = st.selectbox("Change Name and Voice", [""] + list(FUN_USERNAMES.keys()), index=(list(FUN_USERNAMES.keys()).index(current_selection) + 1 if current_selection else 0), format_func=lambda x: f"{x} ({FUN_USERNAMES.get(x, 'No Voice')})" if x else "Select a name")
735
+ if new_username and new_username != st.session_state.username:
736
+ await save_chat_entry("System 🌟", f"{st.session_state.username} changed name to {new_username}")
737
+ st.session_state.username = new_username
738
+ st.session_state.voice = FUN_USERNAMES[new_username]
739
+ st.markdown(f"**πŸŽ™οΈ Voice Changed**: {st.session_state.voice} πŸ—£οΈ for {st.session_state.username}")
740
+ st.rerun()
741
+
742
+ message = st.text_input(f"Message as {st.session_state.username} (Voice: {st.session_state.voice})", key="message_input", value=st.session_state.message_text)
743
+ paste_result_msg = paste_image_button("πŸ“‹ Paste Image or Text with Message", key="paste_button_msg")
744
+ if paste_result_msg.image_data is not None:
745
+ if isinstance(paste_result_msg.image_data, str):
746
+ st.session_state.message_text = paste_result_msg.image_data
747
+ st.text_input(f"Message as {st.session_state.username} (Voice: {st.session_state.voice})", key="message_input_paste", value=st.session_state.message_text)
748
+ else:
749
+ st.image(paste_result_msg.image_data, caption="Received Image for Message")
750
+ filename = await save_pasted_image(paste_result_msg.image_data, st.session_state.username)
751
+ if filename:
752
+ st.session_state.pasted_image_data = filename
753
+ if st.button("Send πŸš€", key="send_button") and (message.strip() or st.session_state.pasted_image_data):
754
+ if message.strip():
755
+ audio_file = await save_chat_entry(st.session_state.username, message, is_markdown=True)
756
+ if audio_file:
757
+ st.session_state.audio_cache[f"{message}_{FUN_USERNAMES[st.session_state.username]}"] = audio_file
758
+ if st.session_state.pasted_image_data:
759
+ await save_chat_entry(st.session_state.username, f"Pasted image: {st.session_state.pasted_image_data}")
760
+ st.session_state.pasted_image_data = None
761
+ st.session_state.message_text = ''
762
+ st.rerun()
763
+
764
+ tab_main = st.radio("Action:", ["🎀 Voice", "πŸ“Έ Media", "πŸ” ArXiv", "πŸ“ Editor"], horizontal=True)
765
+ useArxiv = st.checkbox("Search Arxiv for Research Paper Answers", value=True)
766
+ useArxivAudio = st.checkbox("Generate Audio File for Research Paper Answers", value=False)
767
+
768
+ st.subheader("Upload Media 🎨🎢πŸŽ₯")
769
+ uploaded_file = st.file_uploader("Upload Media", type=['png', 'jpg', 'mp4', 'mp3'])
770
+ if uploaded_file:
771
+ timestamp = format_timestamp_prefix(st.session_state.username)
772
+ username = st.session_state.username
773
+ ext = uploaded_file.name.split('.')[-1]
774
+ file_hash = hashlib.md5(uploaded_file.getbuffer()).hexdigest()[:8]
775
+ if file_hash not in st.session_state.image_hashes:
776
+ filename = f"{timestamp}-{file_hash}.{ext}"
777
+ file_path = os.path.join(MEDIA_DIR, filename)
778
+ await asyncio.to_thread(lambda: open(file_path, 'wb').write(uploaded_file.getbuffer()))
779
+ st.success(f"Uploaded {filename}")
780
+ await save_chat_entry(username, f"Uploaded media: {file_path}")
781
+ st.session_state.image_hashes.add(file_hash)
782
+ if file_path.endswith('.mp4'):
783
+ st.session_state.media_notifications.append(file_path)
784
+
785
+ # Big Red Delete Button
786
+ st.subheader("πŸ›‘ Danger Zone")
787
+ if st.button("Try Not To Delete It All On Your First Day", key="delete_all", help="Deletes all user-added files!", type="primary", use_container_width=True):
788
+ deleted_files = delete_user_files()
789
+ if deleted_files:
790
+ st.markdown("### πŸ—‘οΈ Deleted Files:\n" + "\n".join([f"- `{file}`" for file in deleted_files]))
791
+ else:
792
+ st.markdown("### πŸ—‘οΈ Nothing to Delete!")
793
+ st.session_state.image_hashes.clear()
794
+ st.session_state.audio_cache.clear()
795
+ st.session_state.base64_cache.clear()
796
+ st.session_state.displayed_chat_lines.clear()
797
+ st.rerun()
798
+
799
+ st.subheader("Media Gallery 🎨🎢πŸŽ₯")
800
+ media_files = glob.glob(f"{MEDIA_DIR}/*.png") + glob.glob(f"{MEDIA_DIR}/*.jpg") + glob.glob(f"{MEDIA_DIR}/*.mp4") + glob.glob(f"{MEDIA_DIR}/*.mp3")
801
+ if media_files:
802
+ media_votes = await load_votes(MEDIA_VOTES_FILE)
803
+ st.write("### All Media Uploads")
804
+ seen_files = set()
805
+ for media_file in sorted(media_files, key=os.path.getmtime, reverse=True):
806
+ if media_file not in seen_files:
807
+ seen_files.add(media_file)
808
+ filename = os.path.basename(media_file)
809
+ vote_count = media_votes.get(media_file, 0)
810
+ col1, col2 = st.columns([3, 1])
811
+ with col1:
812
+ st.markdown(f"**{filename}**")
813
+ if media_file.endswith(('.png', '.jpg')):
814
+ st.image(media_file, use_container_width=True)
815
+ elif media_file.endswith('.mp4'):
816
+ st.markdown(await get_video_html(media_file), unsafe_allow_html=True)
817
+ elif media_file.endswith('.mp3'):
818
+ st.markdown(await get_audio_html(media_file), unsafe_allow_html=True)
819
+ with col2:
820
+ if st.button(f"πŸ‘ {vote_count}", key=f"media_vote_{media_file}"):
821
+ await save_vote(MEDIA_VOTES_FILE, media_file, await generate_user_hash(), st.session_state.username)
822
+ st.rerun()
823
+
824
+ st.subheader("Refresh ⏳")
825
+ refresh_rate = st.slider("Refresh Rate", 1, 300, st.session_state.refresh_rate)
826
+ st.session_state.refresh_rate = refresh_rate
827
+ timer_placeholder = st.empty()
828
+ for i in range(st.session_state.refresh_rate, -1, -1):
829
+ font_name, font_func = random.choice(UNICODE_FONTS)
830
+ countdown_str = "".join(UNICODE_DIGITS[int(d)] for d in str(i)) if i < 10 else font_func(str(i))
831
+ timer_placeholder.markdown(f"<p class='timer'>⏳ {font_func('Refresh in:')} {countdown_str}</p>", unsafe_allow_html=True)
832
+ time.sleep(1)
833
+ st.rerun()
834
+
835
+ st.sidebar.subheader("Chat History πŸ“œ")
836
+ with open(HISTORY_FILE, 'r') as f:
837
+ history_content = f.read()
838
+ st.sidebar.markdown(history_content)
839
+
840
+ loop.run_until_complete(async_interface())
841
+
842
+ if __name__ == "__main__":
843
+ main()