awacke1 commited on
Commit
b33fc5b
·
verified ·
1 Parent(s): 2b8d57c

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +1037 -0
app.py ADDED
@@ -0,0 +1,1037 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import anthropic
3
+ import openai
4
+ import base64
5
+ import cv2
6
+ import glob
7
+ import json
8
+ import math
9
+ import os
10
+ import pytz
11
+ import random
12
+ import re
13
+ import requests
14
+ import time
15
+ import zipfile
16
+ import plotly.graph_objects as go
17
+ import streamlit.components.v1 as components
18
+ from datetime import datetime
19
+ from audio_recorder_streamlit import audio_recorder
20
+ from bs4 import BeautifulSoup
21
+ from collections import defaultdict, deque, Counter
22
+ from dotenv import load_dotenv
23
+ from gradio_client import Client
24
+ from huggingface_hub import InferenceClient
25
+ from io import BytesIO
26
+ from PIL import Image
27
+ from PyPDF2 import PdfReader
28
+ from urllib.parse import quote
29
+ from xml.etree import ElementTree as ET
30
+ from openai import OpenAI
31
+ import extra_streamlit_components as stx
32
+ from streamlit.runtime.scriptrunner import get_script_run_ctx
33
+ import asyncio
34
+ import edge_tts
35
+ from streamlit_marquee import streamlit_marquee
36
+ from typing import Tuple, Optional
37
+ import pandas as pd
38
+
39
+ # Patch the asyncio event loop to allow nested use of asyncio.run()
40
+ import nest_asyncio
41
+ nest_asyncio.apply()
42
+
43
+ # ─────────────────────────────────────────────────────────
44
+ # 1. CORE CONFIGURATION & SETUP
45
+ # ─────────────────────────────────────────────────────────
46
+
47
+ st.set_page_config(
48
+ page_title="🚲TalkingAIResearcher🏆",
49
+ page_icon="🚲🏆",
50
+ layout="wide",
51
+ initial_sidebar_state="auto",
52
+ menu_items={
53
+ 'Get Help': 'https://huggingface.co/awacke1',
54
+ 'Report a bug': 'https://huggingface.co/spaces/awacke1',
55
+ 'About': "🚲TalkingAIResearcher🏆"
56
+ }
57
+ )
58
+ load_dotenv()
59
+
60
+ # ▶ Available English voices for Edge TTS
61
+ EDGE_TTS_VOICES = [
62
+ "en-US-AriaNeural",
63
+ "en-US-GuyNeural",
64
+ "en-US-JennyNeural",
65
+ "en-GB-SoniaNeural",
66
+ "en-GB-RyanNeural",
67
+ "en-AU-NatashaNeural",
68
+ "en-AU-WilliamNeural",
69
+ "en-CA-ClaraNeural",
70
+ "en-CA-LiamNeural"
71
+ ]
72
+
73
+ # ▶ Initialize Session State
74
+ if 'marquee_settings' not in st.session_state:
75
+ st.session_state['marquee_settings'] = {
76
+ "background": "#1E1E1E",
77
+ "color": "#FFFFFF",
78
+ "font-size": "14px",
79
+ "animationDuration": "20s",
80
+ "width": "100%",
81
+ "lineHeight": "35px"
82
+ }
83
+ if 'tts_voice' not in st.session_state:
84
+ st.session_state['tts_voice'] = EDGE_TTS_VOICES[0]
85
+ if 'audio_format' not in st.session_state:
86
+ st.session_state['audio_format'] = 'mp3'
87
+ if 'transcript_history' not in st.session_state:
88
+ st.session_state['transcript_history'] = []
89
+ if 'chat_history' not in st.session_state:
90
+ st.session_state['chat_history'] = []
91
+ if 'openai_model' not in st.session_state:
92
+ st.session_state['openai_model'] = "gpt-4o-2024-05-13"
93
+ if 'messages' not in st.session_state:
94
+ st.session_state['messages'] = []
95
+ if 'last_voice_input' not in st.session_state:
96
+ st.session_state['last_voice_input'] = ""
97
+ if 'editing_file' not in st.session_state:
98
+ st.session_state['editing_file'] = None
99
+ if 'edit_new_name' not in st.session_state:
100
+ st.session_state['edit_new_name'] = ""
101
+ if 'edit_new_content' not in st.session_state:
102
+ st.session_state['edit_new_content'] = ""
103
+ if 'viewing_prefix' not in st.session_state:
104
+ st.session_state['viewing_prefix'] = None
105
+ if 'should_rerun' not in st.session_state:
106
+ st.session_state['should_rerun'] = False
107
+ if 'old_val' not in st.session_state:
108
+ st.session_state['old_val'] = None
109
+ if 'last_query' not in st.session_state:
110
+ st.session_state['last_query'] = ""
111
+ if 'marquee_content' not in st.session_state:
112
+ st.session_state['marquee_content'] = "🚀 Welcome to TalkingAIResearcher | 🤖 Your Research Assistant"
113
+
114
+ # ▶ Additional keys for performance, caching, etc.
115
+ if 'audio_cache' not in st.session_state:
116
+ st.session_state['audio_cache'] = {}
117
+ if 'download_link_cache' not in st.session_state:
118
+ st.session_state['download_link_cache'] = {}
119
+ if 'operation_timings' not in st.session_state:
120
+ st.session_state['operation_timings'] = {}
121
+ if 'performance_metrics' not in st.session_state:
122
+ st.session_state['performance_metrics'] = defaultdict(list)
123
+ if 'enable_audio' not in st.session_state:
124
+ st.session_state['enable_audio'] = True # Turn TTS on/off
125
+
126
+ # ▶ API Keys
127
+ openai_api_key = os.getenv('OPENAI_API_KEY', "")
128
+ anthropic_key = os.getenv('ANTHROPIC_API_KEY_3', "")
129
+ xai_key = os.getenv('xai', "")
130
+ if 'OPENAI_API_KEY' in st.secrets:
131
+ openai_api_key = st.secrets['OPENAI_API_KEY']
132
+ if 'ANTHROPIC_API_KEY' in st.secrets:
133
+ anthropic_key = st.secrets["ANTHROPIC_API_KEY"]
134
+
135
+ openai.api_key = openai_api_key
136
+ openai_client = OpenAI(api_key=openai.api_key, organization=os.getenv('OPENAI_ORG_ID'))
137
+ HF_KEY = os.getenv('HF_KEY')
138
+ API_URL = os.getenv('API_URL')
139
+
140
+ # ▶ Helper constants
141
+ FILE_EMOJIS = {
142
+ "md": "📝",
143
+ "mp3": "🎵",
144
+ "wav": "🔊"
145
+ }
146
+
147
+ # ─────────────────────────────────────────────────────────
148
+ # 2. PERFORMANCE MONITORING & TIMING
149
+ # ─────────────────────────────────────────────────────────
150
+
151
+ class PerformanceTimer:
152
+ def __init__(self, operation_name: str):
153
+ self.operation_name = operation_name
154
+ self.start_time = None
155
+
156
+ def __enter__(self):
157
+ self.start_time = time.time()
158
+ return self
159
+
160
+ def __exit__(self, exc_type, exc_val, exc_tb):
161
+ if not exc_type: # Only log if no exception occurred
162
+ duration = time.time() - self.start_time
163
+ st.session_state['operation_timings'][self.operation_name] = duration
164
+ st.session_state['performance_metrics'][self.operation_name].append(duration)
165
+
166
+ def log_performance_metrics():
167
+ st.sidebar.markdown("### ⏱️ Performance Metrics")
168
+ metrics = st.session_state['operation_timings']
169
+ if metrics:
170
+ total_time = sum(metrics.values())
171
+ st.sidebar.write(f"**Total Processing Time:** {total_time:.2f}s")
172
+ for operation, duration in metrics.items():
173
+ percentage = (duration / total_time) * 100
174
+ st.sidebar.write(f"**{operation}:** {duration:.2f}s ({percentage:.1f}%)")
175
+ history_data = []
176
+ for op, times in st.session_state['performance_metrics'].items():
177
+ if times:
178
+ avg_time = sum(times) / len(times)
179
+ history_data.append({"Operation": op, "Avg Time (s)": avg_time})
180
+ if history_data:
181
+ st.sidebar.markdown("### 📊 Timing History (Avg)")
182
+ chart_data = pd.DataFrame(history_data)
183
+ st.sidebar.bar_chart(chart_data.set_index("Operation"))
184
+
185
+ # ─────────────────────────────────────────────────────────
186
+ # 3. HELPER FUNCTIONS (FILENAMES, LINKS, MARQUEE, ETC.)
187
+ # ─────────────────────────────────────────────────────────
188
+
189
+ def get_central_time():
190
+ central = pytz.timezone('US/Central')
191
+ return datetime.now(central)
192
+
193
+ def format_timestamp_prefix():
194
+ ct = get_central_time()
195
+ return ct.strftime("%Y%m%d_%H%M%S")
196
+
197
+ def initialize_marquee_settings():
198
+ if 'marquee_settings' not in st.session_state:
199
+ st.session_state['marquee_settings'] = {
200
+ "background": "#1E1E1E",
201
+ "color": "#FFFFFF",
202
+ "font-size": "14px",
203
+ "animationDuration": "20s",
204
+ "width": "100%",
205
+ "lineHeight": "35px"
206
+ }
207
+
208
+ def get_marquee_settings():
209
+ initialize_marquee_settings()
210
+ return st.session_state['marquee_settings']
211
+
212
+ def update_marquee_settings_ui():
213
+ st.sidebar.markdown("### 🎯 Marquee Settings")
214
+ cols = st.sidebar.columns(2)
215
+ with cols[0]:
216
+ bg_color = st.color_picker("🎨 Background",
217
+ st.session_state['marquee_settings']["background"],
218
+ key="bg_color_picker")
219
+ text_color = st.color_picker("✍️ Text",
220
+ st.session_state['marquee_settings']["color"],
221
+ key="text_color_picker")
222
+ with cols[1]:
223
+ font_size = st.slider("📏 Size", 10, 24, 14, key="font_size_slider")
224
+ duration = st.slider("⏱️ Speed (secs)", 1, 20, 20, key="duration_slider")
225
+
226
+ st.session_state['marquee_settings'].update({
227
+ "background": bg_color,
228
+ "color": text_color,
229
+ "font-size": f"{font_size}px",
230
+ "animationDuration": f"{duration}s"
231
+ })
232
+
233
+ def display_marquee(text, settings, key_suffix=""):
234
+ truncated_text = text[:280] + "..." if len(text) > 280 else text
235
+ streamlit_marquee(
236
+ content=truncated_text,
237
+ **settings,
238
+ key=f"marquee_{key_suffix}"
239
+ )
240
+ st.write("")
241
+
242
+ def get_high_info_terms(text: str, top_n=10) -> list:
243
+ stop_words = set(['the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with'])
244
+ words = re.findall(r'\b\w+(?:-\w+)*\b', text.lower())
245
+ bi_grams = [' '.join(pair) for pair in zip(words, words[1:])]
246
+ combined = words + bi_grams
247
+ filtered = [term for term in combined if term not in stop_words and len(term.split()) <= 2]
248
+ counter = Counter(filtered)
249
+ return [term for term, freq in counter.most_common(top_n)]
250
+
251
+ def clean_text_for_filename(text: str) -> str:
252
+ text = text.lower()
253
+ text = re.sub(r'[^\w\s-]', '', text)
254
+ words = text.split()
255
+ stop_short = set(['the', 'and', 'for', 'with', 'this', 'that', 'ai', 'library'])
256
+ filtered = [w for w in words if len(w) > 3 and w not in stop_short]
257
+ return '_'.join(filtered)[:200]
258
+
259
+ def generate_filename(prompt, response, file_type="md", max_length=200):
260
+ prefix = format_timestamp_prefix() + "_"
261
+ combined_text = (prompt + " " + response)[:200]
262
+ info_terms = get_high_info_terms(combined_text, top_n=5)
263
+ snippet = (prompt[:40] + " " + response[:40]).strip()
264
+ snippet_cleaned = clean_text_for_filename(snippet)
265
+
266
+ name_parts = info_terms + [snippet_cleaned]
267
+ seen = set()
268
+ unique_parts = []
269
+ for part in name_parts:
270
+ if part not in seen:
271
+ seen.add(part)
272
+ unique_parts.append(part)
273
+
274
+ wct = len(prompt.split())
275
+ sw = len(response.split())
276
+ estimated_duration = round((wct + sw) / 2.5)
277
+
278
+ base_name = '_'.join(unique_parts).strip('_')
279
+ extra_tokens = f"_wct{wct}_sw{sw}_dur{estimated_duration}"
280
+ leftover_chars = max_length - len(prefix) - len(file_type) - 1
281
+ if len(base_name) + len(extra_tokens) > leftover_chars:
282
+ base_name = base_name[:leftover_chars - len(extra_tokens)]
283
+ full_name = base_name + extra_tokens
284
+
285
+ return f"{prefix}{full_name}.{file_type}"
286
+
287
+ def create_file(prompt, response, file_type="md"):
288
+ filename = generate_filename(prompt.strip(), response.strip(), file_type)
289
+ with open(filename, 'w', encoding='utf-8') as f:
290
+ f.write(prompt + "\n\n" + response)
291
+ return filename
292
+
293
+ def get_download_link(file, file_type="zip"):
294
+ with open(file, "rb") as f:
295
+ b64 = base64.b64encode(f.read()).decode()
296
+ if file_type == "zip":
297
+ return f'<a href="data:application/zip;base64,{b64}" download="{os.path.basename(file)}">📂 Download {os.path.basename(file)}</a>'
298
+ elif file_type == "mp3":
299
+ return f'<a href="data:audio/mpeg;base64,{b64}" download="{os.path.basename(file)}">🎵 Download {os.path.basename(file)}</a>'
300
+ elif file_type == "wav":
301
+ return f'<a href="data:audio/wav;base64,{b64}" download="{os.path.basename(file)}">🔊 Download {os.path.basename(file)}</a>'
302
+ elif file_type == "md":
303
+ return f'<a href="data:text/markdown;base64,{b64}" download="{os.path.basename(file)}">📝 Download {os.path.basename(file)}</a>'
304
+ else:
305
+ return f'<a href="data:application/octet-stream;base64,{b64}" download="{os.path.basename(file)}">Download {os.path.basename(file)}</a>'
306
+
307
+ def clean_for_speech(text: str) -> str:
308
+ text = text.replace("\n", " ")
309
+ text = text.replace("</s>", " ")
310
+ text = text.replace("#", "")
311
+ text = re.sub(r"\(https?:\/\/[^\)]+\)", "", text)
312
+ text = re.sub(r"\s+", " ", text).strip()
313
+ return text
314
+
315
+ # ─────────────────────────────────────────────────────────
316
+ # 5 MINUTE RESEARCH PAPER FEATURE
317
+ # ─────────────────────────────────────────────────────────
318
+
319
+ def generate_pdf_link(url: str) -> str:
320
+ if "abs" in url:
321
+ pdf_url = url.replace("abs", "pdf")
322
+ if not pdf_url.endswith(".pdf"):
323
+ pdf_url += ".pdf"
324
+ return pdf_url
325
+ return url
326
+
327
+ def generate_5min_feature_markdown(paper: dict) -> str:
328
+ title = paper.get('title', '')
329
+ summary = paper.get('summary', '')
330
+ authors = paper.get('authors', '')
331
+ date = paper.get('date', '')
332
+ url = paper.get('url', '')
333
+ pdf_link = generate_pdf_link(url)
334
+ title_wc = len(title.split())
335
+ summary_wc = len(summary.split())
336
+ high_info_terms = get_high_info_terms(summary, top_n=15)
337
+ terms_str = ", ".join(high_info_terms)
338
+ rouge_score = round((len(high_info_terms) / max(len(summary.split()), 1)) * 100, 2)
339
+
340
+ mermaid_code = "```mermaid\nflowchart TD\n"
341
+ for i in range(len(high_info_terms) - 1):
342
+ mermaid_code += f' T{i+1}["{high_info_terms[i]}"] --> T{i+2}["{high_info_terms[i+1]}"]\n'
343
+ mermaid_code += "```"
344
+
345
+ md = f"""
346
+ ## 📄 {title}
347
+
348
+ **Authors:** {authors}
349
+ **Date:** {date}
350
+ **Word Count (Title):** {title_wc} | **Word Count (Summary):** {summary_wc}
351
+
352
+ **Links:** [Abstract]({url}) | [PDF]({pdf_link})
353
+
354
+ **High Info Terms:** {terms_str}
355
+ **ROUGE Score:** {rouge_score}%
356
+
357
+ ### 🎤 TTF Read Aloud
358
+ - **Title:** {title}
359
+ - **Key Terms:** {terms_str}
360
+ - **ROUGE:** {rouge_score}%
361
+
362
+ #### Mermaid Graph of Key Concepts
363
+ {mermaid_code}
364
+
365
+ ---
366
+ """
367
+ return md
368
+
369
+ def create_detailed_paper_md(papers: list) -> str:
370
+ md_parts = ["# Detailed Research Paper Summary\n"]
371
+ for idx, paper in enumerate(papers, start=1):
372
+ md_parts.append(generate_5min_feature_markdown(paper))
373
+ return "\n".join(md_parts)
374
+
375
+ # ─────────────────────────────────────────────────────────
376
+ # 4. OPTIMIZED AUDIO GENERATION
377
+ # ─────────────────────────────────────────────────────────
378
+
379
+ async def async_edge_tts_generate(
380
+ text: str,
381
+ voice: str,
382
+ rate: int = 0,
383
+ pitch: int = 0,
384
+ file_format: str = "mp3"
385
+ ) -> Tuple[Optional[str], float]:
386
+ with PerformanceTimer("tts_generation") as timer:
387
+ text = clean_for_speech(text)
388
+ if not text.strip():
389
+ return None, 0
390
+
391
+ cache_key = f"{text[:100]}_{voice}_{rate}_{pitch}_{file_format}"
392
+ if cache_key in st.session_state['audio_cache']:
393
+ return st.session_state['audio_cache'][cache_key], 0
394
+
395
+ try:
396
+ rate_str = f"{rate:+d}%"
397
+ pitch_str = f"{pitch:+d}Hz"
398
+ communicate = edge_tts.Communicate(text, voice, rate=rate_str, pitch=pitch_str)
399
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
400
+ filename = f"audio_{timestamp}_{random.randint(1000, 9999)}.{file_format}"
401
+ await communicate.save(filename)
402
+ st.session_state['audio_cache'][cache_key] = filename
403
+ return filename, time.time() - timer.start_time
404
+
405
+ except Exception as e:
406
+ st.error(f"❌ Error generating audio: {str(e)}")
407
+ return None, 0
408
+
409
+ def speak_with_edge_tts(text, voice="en-US-AriaNeural", rate=0, pitch=0, file_format="mp3"):
410
+ result = asyncio.run(async_edge_tts_generate(text, voice, rate, pitch, file_format))
411
+ if isinstance(result, tuple):
412
+ return result[0]
413
+ return result
414
+
415
+ async def async_save_qa_with_audio(
416
+ question: str,
417
+ answer: str,
418
+ voice: Optional[str] = None
419
+ ) -> Tuple[str, Optional[str], float, float]:
420
+ voice = voice or st.session_state['tts_voice']
421
+
422
+ with PerformanceTimer("qa_save") as timer:
423
+ md_start = time.time()
424
+ md_file = create_file(question, answer, "md")
425
+ md_time = time.time() - md_start
426
+
427
+ audio_file = None
428
+ audio_time = 0
429
+ if st.session_state['enable_audio']:
430
+ audio_text = f"{question}\n\nAnswer: {answer}"
431
+ audio_file, audio_time = await async_edge_tts_generate(
432
+ audio_text,
433
+ voice=voice,
434
+ file_format=st.session_state['audio_format']
435
+ )
436
+
437
+ return md_file, audio_file, md_time, audio_time
438
+
439
+ def save_qa_with_audio(question, answer, voice=None):
440
+ if not voice:
441
+ voice = st.session_state['tts_voice']
442
+
443
+ md_file = create_file(question, answer, "md")
444
+ audio_text = f"{question}\n\nAnswer: {answer}"
445
+ audio_file = speak_with_edge_tts(
446
+ audio_text,
447
+ voice=voice,
448
+ file_format=st.session_state['audio_format']
449
+ )
450
+ return md_file, audio_file
451
+
452
+ def create_download_link_with_cache(file_path: str, file_type: str = "mp3") -> str:
453
+ with PerformanceTimer("download_link_generation"):
454
+ cache_key = f"dl_{file_path}"
455
+ if cache_key in st.session_state['download_link_cache']:
456
+ return st.session_state['download_link_cache'][cache_key]
457
+
458
+ try:
459
+ with open(file_path, "rb") as f:
460
+ b64 = base64.b64encode(f.read()).decode()
461
+ filename = os.path.basename(file_path)
462
+
463
+ if file_type == "mp3":
464
+ link = f'<a href="data:audio/mpeg;base64,{b64}" download="{filename}">🎵 Download {filename}</a>'
465
+ elif file_type == "wav":
466
+ link = f'<a href="data:audio/wav;base64,{b64}" download="{filename}">🔊 Download {filename}</a>'
467
+ elif file_type == "md":
468
+ link = f'<a href="data:text/markdown;base64,{b64}" download="{filename}">📝 Download {filename}</a>'
469
+ else:
470
+ link = f'<a href="data:application/octet-stream;base64,{b64}" download="{filename}">⬇️ Download {filename}</a>'
471
+
472
+ st.session_state['download_link_cache'][cache_key] = link
473
+ return link
474
+
475
+ except Exception as e:
476
+ st.error(f"❌ Error creating download link: {str(e)}")
477
+ return ""
478
+
479
+ def play_and_download_audio(file_path, file_type="mp3"):
480
+ if file_path and isinstance(file_path, str) and os.path.exists(file_path):
481
+ st.audio(file_path)
482
+ dl_link = get_download_link(file_path, file_type=file_type)
483
+ st.markdown(dl_link, unsafe_allow_html=True)
484
+
485
+ # ─────────────────────────────────────────────────────────
486
+ # 5. RESEARCH / ARXIV FUNCTIONS
487
+ # ─────────────────────────────────────────────────────────
488
+
489
+ def parse_arxiv_refs(ref_text: str):
490
+ if not ref_text:
491
+ return []
492
+ results = []
493
+ current_paper = {}
494
+ lines = ref_text.split('\n')
495
+
496
+ for i, line in enumerate(lines):
497
+ if line.count('|') == 2:
498
+ if current_paper:
499
+ results.append(current_paper)
500
+ if len(results) >= 20:
501
+ break
502
+ try:
503
+ header_parts = line.strip('* ').split('|')
504
+ date = header_parts[0].strip()
505
+ title = header_parts[1].strip()
506
+ url_match = re.search(r'(https://arxiv.org/\S+)', line)
507
+ url = url_match.group(1) if url_match else f"paper_{len(results)}"
508
+
509
+ current_paper = {
510
+ 'date': date,
511
+ 'title': title,
512
+ 'url': url,
513
+ 'authors': '',
514
+ 'summary': '',
515
+ 'full_audio': None,
516
+ 'download_base64': '',
517
+ }
518
+ except Exception as e:
519
+ st.warning(f"⚠️ Error parsing paper header: {str(e)}")
520
+ current_paper = {}
521
+ continue
522
+ elif current_paper:
523
+ if not current_paper['authors']:
524
+ current_paper['authors'] = line.strip('* ')
525
+ else:
526
+ if current_paper['summary']:
527
+ current_paper['summary'] += ' ' + line.strip()
528
+ else:
529
+ current_paper['summary'] = line.strip()
530
+
531
+ if current_paper:
532
+ results.append(current_paper)
533
+
534
+ return results[:20]
535
+
536
+ def create_paper_links_md(papers):
537
+ lines = ["# Paper Links\n"]
538
+ for i, p in enumerate(papers, start=1):
539
+ lines.append(f"{i}. **{p['title']}** — [Arxiv Link]({p['url']})")
540
+ return "\n".join(lines)
541
+
542
+ async def create_paper_audio_files(papers, input_question):
543
+ for paper in papers:
544
+ try:
545
+ audio_text = f"{paper['title']} by {paper['authors']}. {paper['summary']}"
546
+ audio_text = clean_for_speech(audio_text)
547
+ file_format = st.session_state['audio_format']
548
+ audio_file, _ = await async_edge_tts_generate(
549
+ audio_text,
550
+ voice=st.session_state['tts_voice'],
551
+ file_format=file_format
552
+ )
553
+ paper['full_audio'] = audio_file
554
+
555
+ if audio_file:
556
+ ext = file_format
557
+ download_link = create_download_link_with_cache(audio_file, file_type=ext)
558
+ paper['download_base64'] = download_link
559
+
560
+ except Exception as e:
561
+ st.warning(f"⚠️ Error processing paper {paper['title']}: {str(e)}")
562
+ paper['full_audio'] = None
563
+ paper['download_base64'] = ''
564
+
565
+ def display_papers(papers, marquee_settings):
566
+ st.write("## 🔎 Research Papers")
567
+ for i, paper in enumerate(papers, start=1):
568
+ marquee_text = f"📄 {paper['title']} | 👤 {paper['authors'][:120]} | 📝 {paper['summary'][:200]}"
569
+ display_marquee(marquee_text, marquee_settings, key_suffix=f"paper_{i}")
570
+
571
+ with st.expander(f"{i}. 📄 {paper['title']}", expanded=True):
572
+ st.markdown(f"**{paper['date']} | {paper['title']}** — [Arxiv Link]({paper['url']})")
573
+ pdf_link = generate_pdf_link(paper['url'])
574
+ st.markdown(f"**PDF Link:** [PDF]({pdf_link})")
575
+ st.markdown(f"*Authors:* {paper['authors']}")
576
+ st.markdown(paper['summary'])
577
+ st.markdown(generate_5min_feature_markdown(paper))
578
+ if paper.get('full_audio'):
579
+ st.write("📚 **Paper Audio**")
580
+ st.audio(paper['full_audio'])
581
+ if paper['download_base64']:
582
+ st.markdown(paper['download_base64'], unsafe_allow_html=True)
583
+
584
+ def display_papers_in_sidebar(papers):
585
+ st.sidebar.title("🎶 Papers & Audio")
586
+ for i, paper in enumerate(papers, start=1):
587
+ with st.sidebar.expander(f"{i}. {paper['title']}"):
588
+ st.markdown(f"**Arxiv:** [Link]({paper['url']})")
589
+ pdf_link = generate_pdf_link(paper['url'])
590
+ st.markdown(f"**PDF:** [PDF]({pdf_link})")
591
+ if paper['full_audio']:
592
+ st.audio(paper['full_audio'])
593
+ if paper['download_base64']:
594
+ st.markdown(paper['download_base64'], unsafe_allow_html=True)
595
+ st.markdown(f"**Authors:** {paper['authors']}")
596
+ if paper['summary']:
597
+ st.markdown(f"**Summary:** {paper['summary'][:300]}...")
598
+ st.markdown(generate_5min_feature_markdown(paper))
599
+
600
+ # ─────────────────────────────────────────────────────────
601
+ # 6. ZIP FUNCTION
602
+ # ─────────────────────────────────────────────────────────
603
+
604
+ def create_zip_of_files(md_files, mp3_files, wav_files, input_question):
605
+ md_files = [f for f in md_files if os.path.basename(f).lower() != 'readme.md']
606
+ all_files = md_files + mp3_files + wav_files
607
+ if not all_files:
608
+ return None
609
+
610
+ all_content = []
611
+ for f in all_files:
612
+ if f.endswith('.md'):
613
+ with open(f, "r", encoding='utf-8') as file:
614
+ all_content.append(file.read())
615
+ elif f.endswith('.mp3') or f.endswith('.wav'):
616
+ basename = os.path.splitext(os.path.basename(f))[0]
617
+ words = basename.replace('_', ' ')
618
+ all_content.append(words)
619
+
620
+ all_content.append(input_question)
621
+ combined_content = " ".join(all_content)
622
+ info_terms = get_high_info_terms(combined_content, top_n=10)
623
+
624
+ timestamp = format_timestamp_prefix()
625
+ name_text = '-'.join(term for term in info_terms[:5])
626
+ short_zip_name = (timestamp + "_" + name_text)[:20] + ".zip"
627
+
628
+ with zipfile.ZipFile(short_zip_name, 'w') as z:
629
+ for f in all_files:
630
+ z.write(f)
631
+ return short_zip_name
632
+
633
+ # ─────────────────────────────────────────────────────────
634
+ # 7. MAIN AI LOGIC: LOOKUP & TAB HANDLERS
635
+ # ─────────────────────────────────────────────────────────
636
+
637
+ def perform_ai_lookup(q, vocal_summary=True, extended_refs=False,
638
+ titles_summary=True, full_audio=False, useArxiv=True, useArxivAudio=False):
639
+ """Main routine that uses Anthropic (Claude) + Gradio ArXiv RAG pipeline."""
640
+ start = time.time()
641
+
642
+ # Input validation
643
+ if not q or not q.strip():
644
+ st.error("❌ Please provide a valid question with non-whitespace text.")
645
+ return None
646
+
647
+ # Initialize Anthropic client
648
+ client = anthropic.Anthropic(api_key=anthropic_key)
649
+
650
+ # --- 1) Claude API Call ---
651
+ try:
652
+ response = client.messages.create(
653
+ model="claude-3-5-sonnet-20240620", # Updated to a newer model
654
+ max_tokens=1000,
655
+ messages=[
656
+ {"role": "user", "content": q.strip()}
657
+ ]
658
+ )
659
+ st.write("Claude's reply 🧠:")
660
+ result = response.content[0].text
661
+ st.markdown(result)
662
+
663
+ # Save & produce audio
664
+ md_file, audio_file = save_qa_with_audio(q, result)
665
+ st.subheader("📝 Main Response Audio")
666
+ play_and_download_audio(audio_file, st.session_state['audio_format'])
667
+
668
+ except anthropic.BadRequestError as e:
669
+ st.error(f"❌ Anthropic API error: {str(e)}")
670
+ return None
671
+ except Exception as e:
672
+ st.error(f"❌ Unexpected error during Claude API call: {str(e)}")
673
+ return None
674
+
675
+ # --- 2) ArXiv RAG Integration ---
676
+ if useArxiv:
677
+ try:
678
+ q_with_result = q + " " + result # Fortify prompt with Claude's answer
679
+ st.write('Running Arxiv RAG with Claude inputs.')
680
+ gradio_client = Client("awacke1/Arxiv-Paper-Search-And-QA-RAG-Pattern")
681
+ refs = gradio_client.predict(
682
+ q_with_result,
683
+ 10,
684
+ "Semantic Search",
685
+ "mistralai/Mixtral-8x7B-Instruct-v0.1",
686
+ api_name="/update_with_rag_md"
687
+ )[0]
688
+
689
+ result = f"🔎 {q}\n\n{refs}"
690
+ md_file, audio_file = save_qa_with_audio(q, result)
691
+ st.subheader("📝 Main Response Audio with ArXiv")
692
+ play_and_download_audio(audio_file, st.session_state['audio_format'])
693
+
694
+ # --- 3) Parse + Handle Papers ---
695
+ papers = parse_arxiv_refs(refs)
696
+ if papers:
697
+ paper_links = create_paper_links_md(papers)
698
+ links_file = create_file(q, paper_links, "md")
699
+ st.markdown(paper_links)
700
+
701
+ detailed_md = create_detailed_paper_md(papers)
702
+ detailed_file = create_file(q, detailed_md, "md")
703
+ st.markdown(detailed_md)
704
+
705
+ if useArxivAudio:
706
+ asyncio.run(create_paper_audio_files(papers, input_question=q))
707
+
708
+ display_papers(papers, get_marquee_settings())
709
+ display_papers_in_sidebar(papers)
710
+ else:
711
+ st.warning("No papers found in the response.")
712
+
713
+ except Exception as e:
714
+ st.error(f"❌ Error during ArXiv processing: {str(e)}")
715
+
716
+ # --- 4) Claude API with ArXiv Papers for App Generation ---
717
+ try:
718
+ user_input = (q + '\n\n' +
719
+ 'Use the reference papers below to answer the question by creating a '
720
+ 'Python Streamlit app.py and requirements.txt with Python libraries '
721
+ 'for creating a single app.py application that answers the questions '
722
+ 'with working code to demonstrate.\n\n' + (result or ""))
723
+ response = client.messages.create(
724
+ model="claude-3-5-sonnet-20240620", # Updated model
725
+ max_tokens=1000,
726
+ messages=[
727
+ {"role": "user", "content": user_input}
728
+ ]
729
+ )
730
+ r2 = response.content[0].text
731
+ st.write("Claude's reply with app code 🧠:")
732
+ st.markdown(r2)
733
+
734
+ # Save the app code response
735
+ md_file, audio_file = save_qa_with_audio(q, r2)
736
+ st.subheader("📝 App Code Response Audio")
737
+ play_and_download_audio(audio_file, st.session_state['audio_format'])
738
+
739
+ except anthropic.BadRequestError as e:
740
+ st.error(f"❌ Anthropic API error during app code generation: {str(e)}")
741
+ except Exception as e:
742
+ st.error(f"❌ Unexpected error during app code generation: {str(e)}")
743
+
744
+ elapsed = time.time() - start
745
+ st.write(f"**Total Elapsed:** {elapsed:.2f} s")
746
+ return result
747
+
748
+ async def process_voice_input(text):
749
+ if not text:
750
+ return
751
+ st.subheader("🔍 Search Results")
752
+
753
+ result = perform_ai_lookup(
754
+ text,
755
+ vocal_summary=True,
756
+ extended_refs=False,
757
+ titles_summary=True,
758
+ full_audio=True
759
+ )
760
+
761
+ if result:
762
+ md_file, audio_file, md_time, audio_time = await async_save_qa_with_audio(text, result)
763
+ st.subheader("📝 Generated Files")
764
+ st.write(f"**Markdown:** {md_file} (saved in {md_time:.2f}s)")
765
+ if audio_file:
766
+ st.write(f"**Audio:** {audio_file} (generated in {audio_time:.2f}s)")
767
+ st.audio(audio_file)
768
+ dl_link = create_download_link_with_cache(audio_file, file_type=st.session_state['audio_format'])
769
+ st.markdown(dl_link, unsafe_allow_html=True)
770
+
771
+ def display_voice_tab():
772
+ st.sidebar.markdown("### 🎤 Voice Settings")
773
+ caption_female = 'Top: 🌸 **Aria** – 🎶 **Jenny** – 🌺 **Sonia** – 🌌 **Natasha** – 🌷 **Clara**'
774
+ caption_male = 'Bottom: 🌟 **Guy** – 🛠️ **Ryan** – 🎻 **William** – 🌟 **Liam**'
775
+
776
+ try:
777
+ st.sidebar.image('Group Picture - Voices.png', caption=caption_female + ' | ' + caption_male)
778
+ except:
779
+ st.sidebar.write('.')
780
+
781
+ selected_voice = st.sidebar.selectbox(
782
+ "👄 Select TTS Voice:",
783
+ options=EDGE_TTS_VOICES,
784
+ index=EDGE_TTS_VOICES.index(st.session_state['tts_voice'])
785
+ )
786
+
787
+ st.sidebar.markdown("""
788
+ # 🎙️ Voice Character Agent Selector 🎭
789
+ *Female Voices*:
790
+ - 🌸 **Aria** – Elegant, creative storytelling
791
+ - 🎶 **Jenny** – Friendly, conversational
792
+ - 🌺 **Sonia** – Bold, confident
793
+ - 🌌 **Natasha** – Sophisticated, mysterious
794
+ - 🌷 **Clara** – Cheerful, empathetic
795
+
796
+ *Male Voices*:
797
+ - 🌟 **Guy** – Authoritative, versatile
798
+ - 🛠️ **Ryan** – Approachable, casual
799
+ - 🎻 **William** – Classic, scholarly
800
+ - 🌟 **Liam** – Energetic, engaging
801
+ """)
802
+
803
+ st.markdown("### 🔊 Audio Format")
804
+ selected_format = st.radio(
805
+ "Choose Audio Format:",
806
+ options=["MP3", "WAV"],
807
+ index=0
808
+ )
809
+
810
+ if selected_voice != st.session_state['tts_voice']:
811
+ st.session_state['tts_voice'] = selected_voice
812
+ st.rerun()
813
+ if selected_format.lower() != st.session_state['audio_format']:
814
+ st.session_state['audio_format'] = selected_format.lower()
815
+ st.rerun()
816
+
817
+ user_text = st.text_area("💬 Message:", height=100)
818
+ user_text = user_text.strip().replace('\n', ' ')
819
+
820
+ if st.button("📨 Send"):
821
+ asyncio.run(process_voice_input(user_text))
822
+
823
+ st.subheader("📜 Chat History")
824
+ for c in st.session_state.chat_history:
825
+ st.write("**You:**", c["user"])
826
+ st.write("**Response:**", c["claude"])
827
+
828
+ def display_file_history_in_sidebar():
829
+ st.sidebar.markdown("---")
830
+ st.sidebar.markdown("### 📂 File History")
831
+
832
+ md_files = glob.glob("*.md")
833
+ mp3_files = glob.glob("*.mp3")
834
+ wav_files = glob.glob("*.wav")
835
+ all_files = md_files + mp3_files + wav_files
836
+
837
+ if not all_files:
838
+ st.sidebar.write("No files found.")
839
+ return
840
+
841
+ all_files = sorted(all_files, key=os.path.getmtime, reverse=True)
842
+
843
+ grouped_files = {}
844
+ for f in all_files:
845
+ fname = os.path.basename(f)
846
+ prefix = '_'.join(fname.split('_')[:6])
847
+ if prefix not in grouped_files:
848
+ grouped_files[prefix] = {'md': [], 'audio': [], 'loaded': False}
849
+
850
+ ext = os.path.splitext(fname)[1].lower()
851
+ if ext == '.md':
852
+ grouped_files[prefix]['md'].append(f)
853
+ elif ext in ['.mp3', '.wav']:
854
+ grouped_files[prefix]['audio'].append(f)
855
+
856
+ sorted_groups = sorted(grouped_files.items(), key=lambda x: x[0], reverse=True)
857
+
858
+ col1, col4 = st.sidebar.columns(2)
859
+ with col1:
860
+ if st.button("🗑 Delete All"):
861
+ for f in all_files:
862
+ os.remove(f)
863
+ st.rerun()
864
+ with col4:
865
+ if st.button("⬇️ Zip All"):
866
+ zip_name = create_zip_of_files(md_files, mp3_files, wav_files,
867
+ st.session_state.get('last_query', ''))
868
+ if zip_name:
869
+ st.sidebar.markdown(get_download_link(zip_name, "zip"),
870
+ unsafe_allow_html=True)
871
+
872
+ for prefix, files in sorted_groups:
873
+ preview = ""
874
+ if files['md']:
875
+ with open(files['md'][0], "r", encoding="utf-8") as f:
876
+ preview = f.read(200).replace("\n", " ")
877
+ if len(preview) > 200:
878
+ preview += "..."
879
+ group_key = f"group_{prefix}"
880
+ if group_key not in st.session_state:
881
+ st.session_state[group_key] = False
882
+
883
+ with st.sidebar.expander(f"📑 Query Group: {prefix}"):
884
+ st.write("**Preview:**")
885
+ st.write(preview)
886
+
887
+ if st.button("📖 View Full Content", key=f"btn_{prefix}"):
888
+ st.session_state[group_key] = True
889
+
890
+ if st.session_state[group_key]:
891
+ for md_file in files['md']:
892
+ with open(md_file, "r", encoding="utf-8") as f:
893
+ content = f.read()
894
+ st.markdown("**Full Content:**")
895
+ st.markdown(content)
896
+ st.markdown(get_download_link(md_file, file_type="md"),
897
+ unsafe_allow_html=True)
898
+
899
+ for audio_file in files['audio']:
900
+ ext = os.path.splitext(audio_file)[1].replace('.', '')
901
+ st.audio(audio_file)
902
+ st.markdown(get_download_link(audio_file, file_type=ext),
903
+ unsafe_allow_html=True)
904
+
905
+ def main():
906
+ update_marquee_settings_ui()
907
+ marquee_settings = get_marquee_settings()
908
+
909
+ display_marquee(
910
+ st.session_state['marquee_content'],
911
+ {**marquee_settings, "font-size": "28px", "lineHeight": "50px"},
912
+ key_suffix="welcome"
913
+ )
914
+
915
+ tab_main = st.radio("Action:", ["🎤 Voice", "📸 Media", "🔍 ArXiv", "📝 Editor"],
916
+ horizontal=True)
917
+
918
+ useArxiv = st.checkbox("Search Arxiv for Research Paper Answers", value=True)
919
+ useArxivAudio = st.checkbox("Generate Audio File for Research Paper Answers", value=False)
920
+
921
+ mycomponent = components.declare_component("mycomponent", path="mycomponent")
922
+ val = mycomponent(my_input_value="Hello from MyComponent")
923
+
924
+ if val:
925
+ val_stripped = val.replace('\\n', ' ')
926
+ edited_input = st.text_area("✏️ Edit Input:", value=val_stripped, height=100)
927
+ run_option = st.selectbox("Model:", ["Arxiv", "Other (demo)"])
928
+ col1, col2 = st.columns(2)
929
+ with col1:
930
+ autorun = st.checkbox("⚙ AutoRun", value=True)
931
+ with col2:
932
+ full_audio = st.checkbox("📚FullAudio", value=False)
933
+
934
+ input_changed = (val != st.session_state.old_val)
935
+
936
+ if autorun and input_changed:
937
+ st.session_state.old_val = val
938
+ st.session_state.last_query = edited_input
939
+ perform_ai_lookup(edited_input,
940
+ vocal_summary=True,
941
+ extended_refs=False,
942
+ titles_summary=True,
943
+ full_audio=full_audio, useArxiv=useArxiv, useArxivAudio=useArxivAudio)
944
+ else:
945
+ if st.button("▶ Run"):
946
+ st.session_state.old_val = val
947
+ st.session_state.last_query = edited_input
948
+ perform_ai_lookup(edited_input,
949
+ vocal_summary=True,
950
+ extended_refs=False,
951
+ titles_summary=True,
952
+ full_audio=full_audio, useArxiv=useArxiv, useArxivAudio=useArxivAudio)
953
+
954
+ if tab_main == "🔍 ArXiv":
955
+ st.subheader("🔍 Query ArXiv")
956
+ q = st.text_input("🔍 Query:", key="arxiv_query")
957
+
958
+ st.markdown("### 🎛 Options")
959
+ vocal_summary = st.checkbox("🎙ShortAudio", value=True, key="option_vocal_summary")
960
+ extended_refs = st.checkbox("📜LongRefs", value=False, key="option_extended_refs")
961
+ titles_summary = st.checkbox("🔖TitlesOnly", value=True, key="option_titles_summary")
962
+ full_audio = st.checkbox("📚FullAudio", value=False, key="option_full_audio")
963
+ full_transcript = st.checkbox("🧾FullTranscript", value=False, key="option_full_transcript")
964
+
965
+ if q and st.button("🔍Run"):
966
+ st.session_state.last_query = q
967
+ result = perform_ai_lookup(q,
968
+ vocal_summary=vocal_summary,
969
+ extended_refs=extended_refs,
970
+ titles_summary=titles_summary,
971
+ full_audio=full_audio, useArxiv=useArxiv, useArxivAudio=useArxivAudio)
972
+ if full_transcript and result:
973
+ create_file(q, result, "md")
974
+
975
+ elif tab_main == "🎤 Voice":
976
+ display_voice_tab()
977
+
978
+ elif tab_main == "📸 Media":
979
+ st.header("📸 Media Gallery")
980
+ tabs = st.tabs(["🎵 Audio", "🖼 Images", "🎥 Video"])
981
+
982
+ with tabs[0]:
983
+ st.subheader("🎵 Audio Files")
984
+ audio_files = glob.glob("*.mp3") + glob.glob("*.wav")
985
+ if audio_files:
986
+ for a in audio_files:
987
+ with st.expander(os.path.basename(a)):
988
+ st.audio(a)
989
+ ext = os.path.splitext(a)[1].replace('.', '')
990
+ dl_link = get_download_link(a, file_type=ext)
991
+ st.markdown(dl_link, unsafe_allow_html=True)
992
+ else:
993
+ st.write("No audio files found.")
994
+
995
+ with tabs[1]:
996
+ st.subheader("🖼 Image Files")
997
+ imgs = glob.glob("*.png") + glob.glob("*.jpg") + glob.glob("*.jpeg")
998
+ if imgs:
999
+ c = st.slider("Cols", 1, 5, 3, key="cols_images")
1000
+ cols = st.columns(c)
1001
+ for i, f in enumerate(imgs):
1002
+ with cols[i % c]:
1003
+ st.image(Image.open(f), use_container_width=True)
1004
+ else:
1005
+ st.write("No images found.")
1006
+
1007
+ with tabs[2]:
1008
+ st.subheader("🎥 Video Files")
1009
+ vids = glob.glob("*.mp4") + glob.glob("*.mov") + glob.glob("*.avi")
1010
+ if vids:
1011
+ for v in vids:
1012
+ with st.expander(os.path.basename(v)):
1013
+ st.video(v)
1014
+ else:
1015
+ st.write("No videos found.")
1016
+
1017
+ elif tab_main == "📝 Editor":
1018
+ st.write("### 📝 File Editor (Minimal Demo)")
1019
+ st.write("Select or create a file to edit. More advanced features can be added as needed.")
1020
+
1021
+ display_file_history_in_sidebar()
1022
+ log_performance_metrics()
1023
+
1024
+ st.markdown("""
1025
+ <style>
1026
+ .main { background: linear-gradient(to right, #1a1a1a, #2d2d2d); color: #fff; }
1027
+ .stMarkdown { font-family: 'Helvetica Neue', sans-serif; }
1028
+ .stButton>button { margin-right: 0.5rem; }
1029
+ </style>
1030
+ """, unsafe_allow_html=True)
1031
+
1032
+ if st.session_state.should_rerun:
1033
+ st.session_state.should_rerun = False
1034
+ st.rerun()
1035
+
1036
+ if __name__ == "__main__":
1037
+ main()