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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +423 -744
app.py CHANGED
@@ -1,9 +1,9 @@
 
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
@@ -17,26 +17,35 @@ 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",
@@ -48,326 +57,202 @@ FUN_USERNAMES = {
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():
@@ -376,468 +261,262 @@ async def broadcast_message(message, room_id):
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()
 
1
+ # 🚀 Main App - TalkingAIResearcher with Chat, Voice, Media, ArXiv, and More
2
  import streamlit as st
3
  import asyncio
4
  import websockets
5
  import uuid
6
  import argparse
 
7
  import os
8
  import random
9
  import time
 
17
  from audio_recorder_streamlit import audio_recorder
18
  import nest_asyncio
19
  import re
 
20
  import pytz
21
  import shutil
22
+ import anthropic
23
+ import openai
24
+ from PyPDF2 import PdfReader
25
+ import threading
26
+ import json
27
+ import zipfile
28
+ from gradio_client import Client
29
+ from dotenv import load_dotenv
30
+ from streamlit_marquee import streamlit_marquee
31
+ from datetime import datetime
32
+ from collections import defaultdict, Counter
33
+ import pandas as pd
34
 
35
+ # 🛠️ Patch asyncio for nesting glory
36
  nest_asyncio.apply()
37
 
38
+ # 🎨 Page Config
 
 
 
 
39
  st.set_page_config(
40
+ page_title="🚲TalkingAIResearcher🏆",
41
+ page_icon="🚲🏆",
42
  layout="wide",
43
  initial_sidebar_state="auto"
44
  )
45
 
46
+ # 🌟 Static Config
47
+ icons = '🤖🧠🔬📝'
48
+ START_ROOM = "Sector 🌌"
49
  FUN_USERNAMES = {
50
  "CosmicJester 🌌": "en-US-AriaNeural",
51
  "PixelPanda 🐼": "en-US-JennyNeural",
 
57
  "GalacticGopher 🌍": "en-AU-WilliamNeural",
58
  "RocketRaccoon 🚀": "en-CA-LiamNeural",
59
  "EchoElf 🧝": "en-US-AnaNeural",
 
 
 
 
 
 
 
 
 
 
60
  }
61
+ EDGE_TTS_VOICES = list(set(FUN_USERNAMES.values())) # 🎙️ Voice options
62
+ FILE_EMOJIS = {"md": "📝", "mp3": "🎵", "wav": "🔊"}
63
+
64
+ # 📁 Directories
65
+ for d in ["chat_logs", "vote_logs", "audio_logs", "history_logs", "media_files", "audio_cache"]:
66
+ os.makedirs(d, exist_ok=True)
67
 
68
+ CHAT_FILE = "chat_logs/global_chat.md"
69
+ HISTORY_FILE = "history_logs/chat_history.md"
 
 
 
 
70
  MEDIA_DIR = "media_files"
71
+ AUDIO_CACHE_DIR = "audio_cache"
72
+ AUDIO_DIR = "audio_logs" # New dir for MP3s
73
+
74
+ # 🔑 API Keys
75
+ load_dotenv()
76
+ anthropic_key = os.getenv('ANTHROPIC_API_KEY', st.secrets.get('ANTHROPIC_API_KEY', ""))
77
+ openai_api_key = os.getenv('OPENAI_API_KEY', st.secrets.get('OPENAI_API_KEY', ""))
78
+ openai_client = openai.OpenAI(api_key=openai_api_key)
79
+
80
+ # 🕒 Timestamp Helper
81
+ def format_timestamp_prefix(username=""):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  central = pytz.timezone('US/Central')
83
  now = datetime.now(central)
84
+ return f"{now.strftime('%Y%m%d_%H%M%S')}-by-{username}"
85
+
86
+ # 📈 Performance Timer
87
+ class PerformanceTimer:
88
+ def __init__(self, name): self.name, self.start = name, None
89
+ def __enter__(self):
90
+ self.start = time.time()
91
+ return self
92
+ def __exit__(self, *args):
93
+ duration = time.time() - self.start
94
+ st.session_state['operation_timings'][self.name] = duration
95
+ st.session_state['performance_metrics'][self.name].append(duration)
96
+
97
+ # 🎛️ Session State Init
98
+ def init_session_state():
99
+ defaults = {
100
+ 'server_running': False, 'server_task': None, 'active_connections': {},
101
+ 'media_notifications': [], 'last_chat_update': 0, 'displayed_chat_lines': [],
102
+ 'message_text': "", 'audio_cache': {}, 'pasted_image_data': None,
103
+ 'quote_line': None, 'refresh_rate': 5, 'base64_cache': {},
104
+ 'transcript_history': [], 'last_transcript': "", 'image_hashes': set(),
105
+ 'tts_voice': "en-US-AriaNeural", 'chat_history': [], 'marquee_settings': {
106
+ "background": "#1E1E1E", "color": "#FFFFFF", "font-size": "14px",
107
+ "animationDuration": "20s", "width": "100%", "lineHeight": "35px"
108
+ }, 'operation_timings': {}, 'performance_metrics': defaultdict(list),
109
+ 'enable_audio': True, 'download_link_cache': {}, 'username': None,
110
+ 'autosend': True, 'autosearch': True, 'last_message': "", 'last_query': "",
111
+ 'mp3_files': {} # Store MP3s with chat lines
112
+ }
113
+ for k, v in defaults.items():
114
+ if k not in st.session_state: st.session_state[k] = v
115
+
116
+ # 🖌️ Marquee Helpers
117
+ def update_marquee_settings_ui():
118
+ # 🎨 Sidebar marquee controls
119
+ st.sidebar.markdown("### 🎯 Marquee Settings")
120
+ cols = st.sidebar.columns(2)
121
+ with cols[0]:
122
+ st.session_state['marquee_settings']['background'] = st.color_picker("🎨 Background", "#1E1E1E")
123
+ st.session_state['marquee_settings']['color'] = st.color_picker("✍️ Text", "#FFFFFF")
124
+ with cols[1]:
125
+ st.session_state['marquee_settings']['font-size'] = f"{st.slider('📏 Size', 10, 24, 14)}px"
126
+ st.session_state['marquee_settings']['animationDuration'] = f"{st.slider('⏱️ Speed', 1, 20, 20)}s"
127
+
128
+ def display_marquee(text, settings, key_suffix=""):
129
+ # 🌈 Show marquee with truncation
130
+ truncated = text[:280] + "..." if len(text) > 280 else text
131
+ streamlit_marquee(content=truncated, **settings, key=f"marquee_{key_suffix}")
132
+ st.write("")
133
+
134
+ # 📝 Text & File Helpers
135
+ def clean_text_for_tts(text): return re.sub(r'[#*!\[\]]+', '', ' '.join(text.split()))[:200] or "No text"
136
+ def clean_text_for_filename(text): return '_'.join(re.sub(r'[^\w\s-]', '', text.lower()).split())[:200]
137
+ def get_high_info_terms(text, top_n=10):
138
+ stop_words = {'the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with'}
139
+ words = re.findall(r'\b\w+(?:-\w+)*\b', text.lower())
140
+ bi_grams = [' '.join(pair) for pair in zip(words, words[1:])]
141
+ filtered = [t for t in words + bi_grams if t not in stop_words and len(t.split()) <= 2]
142
+ return [t for t, _ in Counter(filtered).most_common(top_n)]
143
+
144
+ def generate_filename(prompt, response, file_type="md"):
145
+ # 📁 Smart filename with info terms
146
+ prefix = format_timestamp_prefix()
147
+ terms = get_high_info_terms(prompt + " " + response, 5)
148
+ snippet = clean_text_for_filename(prompt[:40] + " " + response[:40])
149
+ wct, sw = len(prompt.split()), len(response.split())
150
+ dur = round((wct + sw) / 2.5)
151
+ base = '_'.join(list(dict.fromkeys(terms + [snippet])))[:200 - len(prefix) - len(f"_wct{wct}_sw{sw}_dur{dur}.{file_type}")]
152
+ return f"{prefix}{base}_wct{wct}_sw{sw}_dur{dur}.{file_type}"
153
+
154
+ def create_file(prompt, response, file_type="md"):
155
+ # 📝 Save file with Q&A
156
+ filename = generate_filename(prompt, response, file_type)
157
+ with open(filename, 'w', encoding='utf-8') as f: f.write(prompt + "\n\n" + response)
158
+ return filename
159
+
160
+ def get_download_link(file, file_type="mp3"):
161
+ # ⬇️ Cached download link
162
+ cache_key = f"dl_{file}"
163
+ if cache_key not in st.session_state['download_link_cache']:
164
+ with open(file, "rb") as f:
165
+ b64 = base64.b64encode(f.read()).decode()
166
+ st.session_state['download_link_cache'][cache_key] = f'<a href="data:audio/mpeg;base64,{b64}" download="{os.path.basename(file)}">{FILE_EMOJIS.get(file_type, "Download")} Download {os.path.basename(file)}</a>'
167
+ return st.session_state['download_link_cache'][cache_key]
168
+
169
+ # 🎶 Audio Processing
170
+ async def async_edge_tts_generate(text, voice, username, rate=0, pitch=0, file_format="mp3"):
171
+ # 🎵 Async TTS with caching and .md generation
172
+ cache_key = f"{text[:100]}_{voice}_{rate}_{pitch}_{file_format}"
173
+ if cache_key in st.session_state['audio_cache']: return st.session_state['audio_cache'][cache_key], 0
174
+ start_time = time.time()
175
+ text = clean_text_for_tts(text)
176
+ if not text: return None, 0
177
+ filename = f"{AUDIO_DIR}/{format_timestamp_prefix(username)}_{voice}.{file_format}"
178
+ communicate = edge_tts.Communicate(text, voice, rate=f"{rate:+d}%", pitch=f"{pitch:+d}Hz")
179
+ await communicate.save(filename)
180
+ st.session_state['audio_cache'][cache_key] = filename
181
+
182
+ # Generate .md file
183
+ md_filename = filename.replace(".mp3", ".md")
184
+ md_content = f"# Chat Audio Log\n\n**Player:** {username}\n**Voice:** {voice}\n**Text:**\n```markdown\n{text}\n```"
185
+ with open(md_filename, 'w', encoding='utf-8') as f: f.write(md_content)
186
+
187
+ return filename, time.time() - start_time
188
+
189
+ def play_and_download_audio(file_path):
190
+ # 🔊 Play + download
191
+ if file_path and os.path.exists(file_path):
192
+ st.audio(file_path)
193
+ st.markdown(get_download_link(file_path), unsafe_allow_html=True)
194
+
195
+ def load_mp3_viewer():
196
+ # 🎵 Load all MP3s at startup
197
+ mp3_files = glob.glob(f"{AUDIO_DIR}/*.mp3")
198
+ for mp3 in mp3_files:
199
+ filename = os.path.basename(mp3)
200
+ if filename not in st.session_state['mp3_files']:
201
+ st.session_state['mp3_files'][filename] = mp3
202
+
203
  async def save_chat_entry(username, message, is_markdown=False):
204
+ # 💬 Save chat with multicast broadcast and audio
205
  central = pytz.timezone('US/Central')
206
  timestamp = datetime.now(central).strftime("%Y-%m-%d %H:%M:%S")
207
+ entry = f"[{timestamp}] {username}: {message}" if not is_markdown else f"[{timestamp}] {username}:\n```markdown\n{message}\n```"
208
+ with open(CHAT_FILE, 'a') as f: f.write(f"{entry}\n")
 
 
 
209
  voice = FUN_USERNAMES.get(username, "en-US-AriaNeural")
210
+ audio_file, _ = await async_edge_tts_generate(message, voice, username)
 
211
  if audio_file:
212
+ with open(HISTORY_FILE, 'a') as f: f.write(f"[{timestamp}] {username}: Audio - {audio_file}\n")
213
+ st.session_state['mp3_files'][os.path.basename(audio_file)] = audio_file
214
  await broadcast_message(f"{username}|{message}", "chat")
215
  st.session_state.last_chat_update = time.time()
216
+ st.session_state.chat_history.append(entry)
217
  return audio_file
218
 
 
219
  async def load_chat():
220
+ # 📜 Load chat history - Numbered
 
221
  if not os.path.exists(CHAT_FILE):
222
+ with open(CHAT_FILE, 'a') as f: f.write(f"# {START_ROOM} Chat\n\nWelcome to the cosmic hub! 🎤\n")
223
+ with open(CHAT_FILE, 'r') as f:
224
+ content = f.read().strip()
225
+ lines = content.split('\n')
226
+ numbered_content = "\n".join(f"{i+1}. {line}" for i, line in enumerate(lines) if line.strip())
227
+ return numbered_content
228
+
229
+ # 🌐 WebSocket Handling
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
  async def websocket_handler(websocket, path):
231
+ # 🤝 Handle WebSocket clients
232
+ client_id = str(uuid.uuid4())
233
+ room_id = "chat"
234
+ if room_id not in st.session_state.active_connections:
235
+ st.session_state.active_connections[room_id] = {}
236
+ st.session_state.active_connections[room_id][client_id] = websocket
237
+ username = st.session_state.get('username', random.choice(list(FUN_USERNAMES.keys())))
238
+ chat_content = await load_chat()
239
+ if not any(f"Client-{client_id}" in line for line in chat_content.split('\n')):
240
+ await save_chat_entry("System 🌟", f"{username} has joined {START_ROOM}!")
241
  try:
 
 
 
 
 
 
 
242
  async for message in websocket:
243
+ if '|' in message:
244
+ username, content = message.split('|', 1)
 
245
  await save_chat_entry(username, content)
246
+ else:
247
+ await websocket.send("ERROR|Message format: username|content")
248
  except websockets.ConnectionClosed:
249
+ await save_chat_entry("System 🌟", f"{username} has left {START_ROOM}!")
250
  finally:
251
  if room_id in st.session_state.active_connections and client_id in st.session_state.active_connections[room_id]:
252
  del st.session_state.active_connections[room_id][client_id]
253
 
 
254
  async def broadcast_message(message, room_id):
255
+ # 📢 Broadcast to all clients
 
256
  if room_id in st.session_state.active_connections:
257
  disconnected = []
258
  for client_id, ws in st.session_state.active_connections[room_id].items():
 
261
  except websockets.ConnectionClosed:
262
  disconnected.append(client_id)
263
  for client_id in disconnected:
264
+ if client_id in st.session_state.active_connections[room_id]:
265
+ del st.session_state.active_connections[room_id][client_id]
266
 
 
267
  async def run_websocket_server():
268
+ # 🖥️ Start WebSocket server
 
269
  if not st.session_state.server_running:
270
  server = await websockets.serve(websocket_handler, '0.0.0.0', 8765)
271
  st.session_state.server_running = True
272
  await server.wait_closed()
273
 
274
+ # 📚 PDF to Audio
275
+ class AudioProcessor:
276
+ def __init__(self):
277
+ self.cache_dir = AUDIO_CACHE_DIR
278
+ os.makedirs(self.cache_dir, exist_ok=True)
279
+ self.metadata = json.load(open(f"{self.cache_dir}/metadata.json")) if os.path.exists(f"{self.cache_dir}/metadata.json") else {}
280
+
281
+ def _save_metadata(self):
282
+ with open(f"{self.cache_dir}/metadata.json", 'w') as f: json.dump(self.metadata, f)
283
+
284
+ async def create_audio(self, text, voice='en-US-AriaNeural'):
285
+ # 🎶 Generate cached audio
286
+ cache_key = hashlib.md5(f"{text}:{voice}".encode()).hexdigest()
287
+ cache_path = f"{self.cache_dir}/{cache_key}.mp3"
288
+ if cache_key in self.metadata and os.path.exists(cache_path):
289
+ return open(cache_path, 'rb').read()
290
+ text = clean_text_for_tts(text)
291
+ if not text: return None
292
+ communicate = edge_tts.Communicate(text, voice)
293
+ await communicate.save(cache_path)
294
+ self.metadata[cache_key] = {'timestamp': datetime.now().isoformat(), 'text_length': len(text), 'voice': voice}
295
+ self._save_metadata()
296
+ return open(cache_path, 'rb').read()
297
+
298
+ def process_pdf(pdf_file, max_pages, voice, audio_processor):
299
+ # 📄 Convert PDF to audio
300
+ reader = PdfReader(pdf_file)
301
+ total_pages = min(len(reader.pages), max_pages)
302
+ texts, audios = [], {}
303
+ async def process_page(i, text): audios[i] = await audio_processor.create_audio(text, voice)
304
+ for i in range(total_pages):
305
+ text = reader.pages[i].extract_text()
306
+ texts.append(text)
307
+ threading.Thread(target=lambda: asyncio.run(process_page(i, text))).start()
308
+ return texts, audios, total_pages
309
+
310
+ # 🔍 ArXiv & AI Lookup
311
+ def parse_arxiv_refs(ref_text):
312
+ # 📜 Parse ArXiv refs into dicts
313
+ if not ref_text: return []
314
+ papers = []
315
+ current = {}
316
+ for line in ref_text.split('\n'):
317
+ if line.count('|') == 2:
318
+ if current: papers.append(current)
319
+ date, title, *_ = line.strip('* ').split('|')
320
+ url = re.search(r'(https://arxiv.org/\S+)', line).group(1) if re.search(r'(https://arxiv.org/\S+)', line) else f"paper_{len(papers)}"
321
+ current = {'date': date, 'title': title, 'url': url, 'authors': '', 'summary': '', 'full_audio': None, 'download_base64': ''}
322
+ elif current:
323
+ if not current['authors']: current['authors'] = line.strip('* ')
324
+ else: current['summary'] += ' ' + line.strip() if current['summary'] else line.strip()
325
+ if current: papers.append(current)
326
+ return papers[:20]
327
+
328
+ def generate_5min_feature_markdown(paper):
329
+ # ✨ 5-min research paper feature
330
+ title, summary, authors, date, url = paper['title'], paper['summary'], paper['authors'], paper['date'], paper['url']
331
+ pdf_url = url.replace("abs", "pdf") + (".pdf" if not url.endswith(".pdf") else "")
332
+ wct, sw = len(title.split()), len(summary.split())
333
+ terms = get_high_info_terms(summary, 15)
334
+ rouge = round((len(terms) / max(sw, 1)) * 100, 2)
335
+ mermaid = "```mermaid\nflowchart TD\n" + "\n".join(f' T{i+1}["{t}"] --> T{i+2}["{terms[i+1]}"]' for i in range(len(terms)-1)) + "\n```"
336
+ return f"""
337
+ ## 📄 {title}
338
+ **Authors:** {authors} | **Date:** {date} | **Words:** Title: {wct}, Summary: {sw}
339
+ **Links:** [Abstract]({url}) | [PDF]({pdf_url})
340
+ **Terms:** {', '.join(terms)} | **ROUGE:** {rouge}%
341
+ ### 🎤 TTF Read Aloud
342
+ - **Title:** {title} | **Terms:** {', '.join(terms)} | **ROUGE:** {rouge}%
343
+ #### Concepts Graph
344
+ {mermaid}
345
+ ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
346
  """
347
 
348
+ def create_detailed_paper_md(papers): return "# Detailed Summary\n" + "\n".join(generate_5min_feature_markdown(p) for p in papers)
349
+
350
+ async def create_paper_audio_files(papers, query):
351
+ # 🎧 Generate paper audio
352
+ for p in papers:
353
+ audio_text = clean_text_for_tts(f"{p['title']} by {p['authors']}. {p['summary']}")
354
+ p['full_audio'], _ = await async_edge_tts_generate(audio_text, st.session_state['tts_voice'], p['authors'])
355
+ if p['full_audio']: p['download_base64'] = get_download_link(p['full_audio'])
356
+
357
+ async def perform_ai_lookup(q, useArxiv=True, useArxivAudio=False):
358
+ # 🔮 AI-powered research
359
+ client = anthropic.Anthropic(api_key=anthropic_key)
360
+ response = client.messages.create(model="claude-3-sonnet-20240229", max_tokens=1000, messages=[{"role": "user", "content": q}])
361
+ result = response.content[0].text
362
+ st.markdown("### Claude's Reply 🧠\n" + result)
363
+ md_file = create_file(q, result)
364
+ audio_file, _ = await async_edge_tts_generate(result, st.session_state['tts_voice'], "System")
365
+ play_and_download_audio(audio_file)
366
+
367
+ if useArxiv:
368
+ q += result
369
+ gradio_client = Client("awacke1/Arxiv-Paper-Search-And-QA-RAG-Pattern")
370
+ refs = gradio_client.predict(q, 10, "Semantic Search", "mistralai/Mixtral-8x7B-Instruct-v0.1", api_name="/update_with_rag_md")[0]
371
+ result = f"🔎 {q}\n\n{refs}"
372
+ md_file, audio_file = create_file(q, result), (await async_edge_tts_generate(result, st.session_state['tts_voice'], "System"))[0]
373
+ play_and_download_audio(audio_file)
374
+ papers = parse_arxiv_refs(refs)
375
+ if papers and useArxivAudio: await create_paper_audio_files(papers, q)
376
+ return result, papers
377
+ return result, []
378
+
379
+ # 📦 Zip Files
380
+ def create_zip_of_files(md_files, mp3_files, query):
381
+ # 📦 Zip it up
382
+ all_files = md_files + mp3_files
383
+ if not all_files: return None
384
+ terms = get_high_info_terms(" ".join([open(f, 'r', encoding='utf-8').read() if f.endswith('.md') else os.path.splitext(os.path.basename(f))[0].replace('_', ' ') for f in all_files] + [query]), 5)
385
+ zip_name = f"{format_timestamp_prefix()}_{'-'.join(terms)[:20]}.zip"
386
+ with zipfile.ZipFile(zip_name, 'w') as z: [z.write(f) for f in all_files]
387
+ return zip_name
388
+
389
+ # 🎮 Main Interface
390
+ async def async_interface():
391
+ init_session_state()
392
+ load_mp3_viewer() # Load MP3s at startup
393
+ if not st.session_state.username:
394
+ available = [n for n in FUN_USERNAMES if not any(f"{n} has joined" in l for l in (await load_chat()).split('\n'))]
395
+ st.session_state.username = random.choice(available or list(FUN_USERNAMES.keys()))
396
+ st.session_state.tts_voice = FUN_USERNAMES[st.session_state.username]
397
+ await save_chat_entry("System 🌟", f"{st.session_state.username} has joined {START_ROOM}!")
398
+
399
+ st.title(f"🤖🧠MMO Chat & Research for {st.session_state.username}📝🔬")
400
+ update_marquee_settings_ui()
401
+ display_marquee(f"🚀 Welcome to {START_ROOM} | 🤖 {st.session_state.username}", st.session_state['marquee_settings'], "welcome")
402
+
403
+ if not st.session_state.server_task:
404
+ st.session_state.server_task = asyncio.create_task(run_websocket_server())
405
+
406
+ tab_main = st.radio("Action:", ["🎤 Chat & Voice", "📸 Media", "🔍 ArXiv", "📚 PDF to Audio"], horizontal=True)
407
+ useArxiv, useArxivAudio = st.checkbox("Search ArXiv", True), st.checkbox("ArXiv Audio", False)
408
+ st.session_state.autosend = st.checkbox("Autosend Chat", value=True)
409
+ st.session_state.autosearch = st.checkbox("Autosearch ArXiv", value=True)
410
+
411
+ # 🎤 Chat & Voice
412
+ if tab_main == "🎤 Chat & Voice":
413
  st.subheader(f"{START_ROOM} Chat 💬")
414
  chat_content = await load_chat()
415
+ chat_container = st.container()
416
+ with chat_container:
417
+ lines = chat_content.split('\n')
418
+ for i, line in enumerate(lines):
419
+ if line.strip():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
420
  st.markdown(line)
421
+ # Match MP3 to line by timestamp and username
422
+ for mp3_name, mp3_path in st.session_state['mp3_files'].items():
423
+ if line.strip() in mp3_name and st.session_state.username in mp3_name:
424
+ st.audio(mp3_path, key=f"audio_{i}_{mp3_name}")
425
+ break
426
+
427
+ message = st.text_input(f"Message as {st.session_state.username}", key="message_input")
428
+ if message and message != st.session_state.last_message:
429
+ st.session_state.last_message = message
430
+ if st.session_state.autosend or st.button("Send 🚀"):
431
+ await save_chat_entry(st.session_state.username, message, True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
432
  st.rerun()
433
 
434
+ st.subheader("🎤 Speech-to-Chat")
435
+ speech_component = components.declare_component("speech_component", path="mycomponent")
436
+ transcript_data = speech_component(default_value=st.session_state.get('last_transcript', ''))
437
+ if transcript_data and 'value' in transcript_data:
438
+ transcript = transcript_data['value'].strip()
439
+ st.write(f"🎙️ You said: {transcript}")
440
+ if transcript and transcript != st.session_state.last_transcript:
441
+ st.session_state.last_transcript = transcript
442
+ if st.session_state.autosend:
443
+ await save_chat_entry(st.session_state.username, transcript, True)
444
+ st.rerun()
445
+ elif st.button("Send to Chat"):
446
+ await save_chat_entry(st.session_state.username, transcript, True)
447
+ st.rerun()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
448
 
449
+ # 📸 Media
450
+ elif tab_main == "📸 Media":
451
+ st.header("📸 Media Gallery")
452
+ tabs = st.tabs(["🎵 Audio", "🖼 Images", "🎥 Video"])
453
+ with tabs[0]:
454
+ for a in glob.glob(f"{MEDIA_DIR}/*.mp3"):
455
+ with st.expander(os.path.basename(a)): play_and_download_audio(a)
456
+ with tabs[1]:
457
+ imgs = glob.glob(f"{MEDIA_DIR}/*.png") + glob.glob(f"{MEDIA_DIR}/*.jpg")
458
+ if imgs:
459
+ cols = st.columns(3)
460
+ for i, f in enumerate(imgs): cols[i % 3].image(f, use_container_width=True)
461
+ with tabs[2]:
462
+ for v in glob.glob(f"{MEDIA_DIR}/*.mp4"):
463
+ with st.expander(os.path.basename(v)): st.video(v)
464
 
 
465
  uploaded_file = st.file_uploader("Upload Media", type=['png', 'jpg', 'mp4', 'mp3'])
466
  if uploaded_file:
467
+ filename = f"{format_timestamp_prefix(st.session_state.username)}-{hashlib.md5(uploaded_file.getbuffer()).hexdigest()[:8]}.{uploaded_file.name.split('.')[-1]}"
468
+ with open(f"{MEDIA_DIR}/{filename}", 'wb') as f: f.write(uploaded_file.getbuffer())
469
+ await save_chat_entry(st.session_state.username, f"Uploaded: {filename}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
470
  st.rerun()
471
 
472
+ # 🔍 ArXiv
473
+ elif tab_main == "🔍 ArXiv":
474
+ q = st.text_input("🔍 Query:", key="arxiv_query")
475
+ if q and q != st.session_state.last_query:
476
+ st.session_state.last_query = q
477
+ if st.session_state.autosearch or st.button("🔍 Run"):
478
+ result, papers = await perform_ai_lookup(q, useArxiv, useArxivAudio)
479
+ for i, p in enumerate(papers, 1):
480
+ with st.expander(f"{i}. 📄 {p['title']}"):
481
+ st.markdown(f"**{p['date']} | {p['title']}** — [Link]({p['url']})")
482
+ st.markdown(generate_5min_feature_markdown(p))
483
+ if p.get('full_audio'): play_and_download_audio(p['full_audio'])
484
+
485
+ # 📚 PDF to Audio
486
+ elif tab_main == "📚 PDF to Audio":
487
+ audio_processor = AudioProcessor()
488
+ pdf_file = st.file_uploader("Choose PDF", "pdf")
489
+ max_pages = st.slider('Pages', 1, 100, 10)
490
+ if pdf_file:
491
+ with st.spinner('Processing...'):
492
+ texts, audios, total = process_pdf(pdf_file, max_pages, st.session_state['tts_voice'], audio_processor)
493
+ for i, text in enumerate(texts):
494
+ with st.expander(f"Page {i+1}"):
495
+ st.markdown(text)
496
+ while i not in audios: time.sleep(0.1)
497
+ if audios[i]:
498
+ st.audio(audios[i], format='audio/mp3')
499
+ st.markdown(get_download_link(io.BytesIO(audios[i]), "mp3"), unsafe_allow_html=True)
500
+
501
+ # 🗂️ Sidebar
502
+ st.sidebar.subheader("Voice Settings")
503
+ new_username = st.sidebar.selectbox("Change Name/Voice", list(FUN_USERNAMES.keys()), index=list(FUN_USERNAMES.keys()).index(st.session_state.username))
504
+ if new_username != st.session_state.username:
505
+ await save_chat_entry("System 🌟", f"{st.session_state.username} changed to {new_username}")
506
+ st.session_state.username, st.session_state.tts_voice = new_username, FUN_USERNAMES[new_username]
507
  st.rerun()
508
 
509
+ md_files, mp3_files = glob.glob("*.md"), glob.glob(f"{AUDIO_DIR}/*.mp3")
510
+ st.sidebar.markdown("### 📂 File History")
511
+ for f in sorted(md_files + mp3_files, key=os.path.getmtime, reverse=True)[:10]:
512
+ st.sidebar.write(f"{FILE_EMOJIS.get(f.split('.')[-1], '📄')} {os.path.basename(f)}")
513
+ if st.sidebar.button("⬇️ Zip All"):
514
+ zip_name = create_zip_of_files(md_files, mp3_files, "latest_query")
515
+ if zip_name: st.sidebar.markdown(get_download_link(zip_name, "zip"), unsafe_allow_html=True)
516
 
517
+ def main():
518
+ # 🎉 Kick it off
519
+ asyncio.run(async_interface())
520
 
521
  if __name__ == "__main__":
522
  main()