awacke1 commited on
Commit
5eb1418
ยท
verified ยท
1 Parent(s): 3a2870f

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +590 -0
app.py ADDED
@@ -0,0 +1,590 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import io
2
+ import os
3
+ import re
4
+ import glob
5
+ import textwrap
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+ import streamlit as st
9
+ import pandas as pd
10
+ from PIL import Image
11
+ from reportlab.pdfgen import canvas
12
+ from reportlab.lib.pagesizes import letter, A4
13
+ from reportlab.lib.utils import ImageReader
14
+ from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
15
+ from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
16
+ from reportlab.lib import colors
17
+ from reportlab.pdfbase import pdfmetrics
18
+ from reportlab.pdfbase.ttfonts import TTFont
19
+ import mistune
20
+ from gtts import gTTS
21
+ import fitz
22
+ import edge_tts
23
+ import asyncio
24
+ import base64
25
+ from urllib.parse import quote
26
+
27
+ # Page config
28
+ st.set_page_config(page_title="PDF & Code Interpreter", layout="wide", page_icon="๐Ÿš€")
29
+
30
+ def delete_asset(path):
31
+ try:
32
+ os.remove(path)
33
+ except Exception as e:
34
+ st.error(f"Error deleting file: {e}")
35
+ st.rerun()
36
+
37
+ async def generate_audio(text, voice, filename):
38
+ communicate = edge_tts.Communicate(text, voice)
39
+ await communicate.save(filename)
40
+ return filename
41
+
42
+ def clean_for_speech(text):
43
+ text = text.replace("#", "")
44
+ emoji_pattern = re.compile(
45
+ r"[\U0001F300-\U0001F5FF"
46
+ r"\U0001F600-\U0001F64F"
47
+ r"\U0001F680-\U0001F6FF"
48
+ r"\U0001F700-\U0001F77F"
49
+ r"\U0001F780-\U0001F7FF"
50
+ r"\U0001F800-\U0001F8FF"
51
+ r"\U0001F900-\U0001F9FF"
52
+ r"\U0001FA00-\U0001FA6F"
53
+ r"\U0001FA70-\U0001FAFF"
54
+ r"\u2600-\u26FF"
55
+ r"\u2700-\u27BF]+", flags=re.UNICODE)
56
+ return emoji_pattern.sub('', text)
57
+
58
+ def detect_and_convert_links(text):
59
+ md_link_pattern = re.compile(r'\[(.*?)\]\((https?://[^\s\[\]()<>{}]+)\)')
60
+ text = md_link_pattern.sub(r'<a href="\2" color="blue">\1</a>', text)
61
+ url_pattern = re.compile(r'(?<!href=")(https?://[^\s<>{}]+)', re.IGNORECASE)
62
+ text = url_pattern.sub(r'<a href="\1" color="blue">\1</a>', text)
63
+ return text
64
+
65
+ def apply_emoji_font(text, emoji_font):
66
+ tag_pattern = re.compile(r'(<[^>]+>)')
67
+ segments = tag_pattern.split(text)
68
+ result = []
69
+ emoji_pattern = re.compile(
70
+ r"([\U0001F300-\U0001F5FF"
71
+ r"\U0001F600-\U0001F64F"
72
+ r"\U0001F680-\U0001F6FF"
73
+ r"\U0001F700-\U0001F77F"
74
+ r"\U0001F780-\U0001F7FF"
75
+ r"\U0001F800-\U0001F8FF"
76
+ r"\U0001F900-\U0001F9FF"
77
+ r"\U0001FAD0-\U0001FAD9"
78
+ r"\U0001FA00-\U0001FA6F"
79
+ r"\U0001FA70-\U0001FAFF"
80
+ r"\u2600-\u26FF"
81
+ r"\u2700-\u27BF]+)"
82
+ )
83
+ def replace_emoji(match):
84
+ emoji = match.group(1)
85
+ return f'<font face="{emoji_font}">{emoji}</font>'
86
+ for segment in segments:
87
+ if tag_pattern.match(segment):
88
+ result.append(segment)
89
+ else:
90
+ parts = []
91
+ last_pos = 0
92
+ for match in emoji_pattern.finditer(segment):
93
+ start, end = match.span()
94
+ if last_pos < start:
95
+ parts.append(f'<font face="DejaVuSans">{segment[last_pos:start]}</font>')
96
+ parts.append(replace_emoji(match))
97
+ last_pos = end
98
+ if last_pos < len(segment):
99
+ parts.append(f'<font face="DejaVuSans">{segment[last_pos:]}</font>')
100
+ result.append(''.join(parts))
101
+ return ''.join(result)
102
+
103
+ def markdown_to_pdf_content(markdown_text, add_space_before_numbered, headings_to_fonts):
104
+ lines = markdown_text.strip().split('\n')
105
+ pdf_content = []
106
+ number_pattern = re.compile(r'^\d+(\.\d+)*\.\s')
107
+ heading_pattern = re.compile(r'^(#{1,4})\s+(.+)$')
108
+ first_numbered_seen = False
109
+ for line in lines:
110
+ line = line.strip()
111
+ if not line:
112
+ continue
113
+ if headings_to_fonts and line.startswith('#'):
114
+ heading_match = heading_pattern.match(line)
115
+ if heading_match:
116
+ level = len(heading_match.group(1))
117
+ heading_text = heading_match.group(2).strip()
118
+ formatted_heading = f"<h{level}>{heading_text}</h{level}>"
119
+ pdf_content.append(formatted_heading)
120
+ continue
121
+ is_numbered_line = number_pattern.match(line) is not None
122
+ if add_space_before_numbered and is_numbered_line:
123
+ if first_numbered_seen and not line.startswith("1."):
124
+ pdf_content.append("")
125
+ if not first_numbered_seen:
126
+ first_numbered_seen = True
127
+ line = detect_and_convert_links(line)
128
+ line = re.sub(r'\*\*(.+?)\*\*', r'<b>\1</b>', line)
129
+ line = re.sub(r'\*([^*]+?)\*', r'<b>\1</b>', line)
130
+ pdf_content.append(line)
131
+ total_lines = len(pdf_content)
132
+ return pdf_content, total_lines
133
+
134
+ def create_pdf(markdown_texts, image_files, base_font_size=14, num_columns=2, add_space_before_numbered=True, headings_to_fonts=True, doc_title="Combined Document"):
135
+ if not markdown_texts and not image_files:
136
+ return None
137
+ buffer = io.BytesIO()
138
+ page_width = A4[0] * 2
139
+ page_height = A4[1]
140
+ doc = SimpleDocTemplate(
141
+ buffer,
142
+ pagesize=(page_width, page_height),
143
+ leftMargin=36,
144
+ rightMargin=36,
145
+ topMargin=36,
146
+ bottomMargin=36,
147
+ title=doc_title
148
+ )
149
+ styles = getSampleStyleSheet()
150
+ spacer_height = 10
151
+ try:
152
+ pdfmetrics.registerFont(TTFont("DejaVuSans", "DejaVuSans.ttf"))
153
+ pdfmetrics.registerFont(TTFont("NotoEmoji-Bold", "NotoEmoji-Bold.ttf"))
154
+ except Exception as e:
155
+ st.error(f"Font registration error: {e}")
156
+ return None
157
+ story = []
158
+ for markdown_text in markdown_texts:
159
+ pdf_content, total_lines = markdown_to_pdf_content(markdown_text, add_space_before_numbered, headings_to_fonts)
160
+ total_chars = sum(len(line) for line in pdf_content)
161
+ hierarchy_weight = sum(1.5 if line.startswith("<b>") else 1 for line in pdf_content)
162
+ longest_line_words = max(len(line.split()) for line in pdf_content) if pdf_content else 0
163
+ content_density = total_lines * hierarchy_weight + total_chars / 50
164
+ usable_height = page_height - 72 - spacer_height
165
+ usable_width = page_width - 72
166
+ avg_line_chars = total_chars / total_lines if total_lines > 0 else 50
167
+ col_width = usable_width / num_columns
168
+ min_font_size = 5
169
+ max_font_size = 16
170
+ lines_per_col = total_lines / num_columns if num_columns > 0 else total_lines
171
+ target_height_per_line = usable_height / lines_per_col if lines_per_col > 0 else usable_height
172
+ estimated_font_size = int(target_height_per_line / 1.5)
173
+ adjusted_font_size = max(min_font_size, min(max_font_size, estimated_font_size))
174
+ if avg_line_chars > col_width / adjusted_font_size * 10:
175
+ adjusted_font_size = int(col_width / (avg_line_chars / 10))
176
+ adjusted_font_size = max(min_font_size, adjusted_font_size)
177
+ if longest_line_words > 17 or lines_per_col > 20:
178
+ font_scale = min(17 / max(longest_line_words, 17), 60 / max(lines_per_col, 20))
179
+ adjusted_font_size = max(min_font_size, int(base_font_size * font_scale))
180
+ item_style = ParagraphStyle(
181
+ 'ItemStyle', parent=styles['Normal'], fontName="DejaVuSans",
182
+ fontSize=adjusted_font_size, leading=adjusted_font_size * 1.15, spaceAfter=1,
183
+ linkUnderline=True
184
+ )
185
+ numbered_bold_style = ParagraphStyle(
186
+ 'NumberedBoldStyle', parent=styles['Normal'], fontName="NotoEmoji-Bold",
187
+ fontSize=adjusted_font_size, leading=adjusted_font_size * 1.15, spaceAfter=1,
188
+ linkUnderline=True
189
+ )
190
+ section_style = ParagraphStyle(
191
+ 'SectionStyle', parent=styles['Heading2'], fontName="DejaVuSans",
192
+ textColor=colors.darkblue, fontSize=adjusted_font_size * 1.1, leading=adjusted_font_size * 1.32, spaceAfter=2,
193
+ linkUnderline=True
194
+ )
195
+ columns = [[] for _ in range(num_columns)]
196
+ lines_per_column = total_lines / num_columns if num_columns > 0 else total_lines
197
+ current_line_count = 0
198
+ current_column = 0
199
+ number_pattern = re.compile(r'^\d+(\.\d+)*\.\s')
200
+ for item in pdf_content:
201
+ if current_line_count >= lines_per_column and current_column < num_columns - 1:
202
+ current_column += 1
203
+ current_line_count = 0
204
+ columns[current_column].append(item)
205
+ current_line_count += 1
206
+ column_cells = [[] for _ in range(num_columns)]
207
+ for col_idx, column in enumerate(columns):
208
+ for item in column:
209
+ if isinstance(item, str):
210
+ heading_match = re.match(r'<h(\d)>(.*?)</h\1>', item) if headings_to_fonts else None
211
+ if heading_match:
212
+ level = int(heading_match.group(1))
213
+ heading_text = heading_match.group(2)
214
+ heading_style = ParagraphStyle(
215
+ f'Heading{level}Style',
216
+ parent=styles['Heading1'],
217
+ fontName="DejaVuSans",
218
+ textColor=colors.darkblue if level == 1 else (colors.black if level > 2 else colors.blue),
219
+ fontSize=adjusted_font_size * (1.6 - (level-1)*0.15),
220
+ leading=adjusted_font_size * (1.8 - (level-1)*0.15),
221
+ spaceAfter=4 - (level-1),
222
+ spaceBefore=6 - (level-1),
223
+ linkUnderline=True
224
+ )
225
+ column_cells[col_idx].append(Paragraph(apply_emoji_font(heading_text, "NotoEmoji-Bold"), heading_style))
226
+ elif item.startswith("<b>") and item.endswith("</b>"):
227
+ content = item[3:-4].strip()
228
+ if number_pattern.match(content):
229
+ column_cells[col_idx].append(Paragraph(apply_emoji_font(content, "NotoEmoji-Bold"), numbered_bold_style))
230
+ else:
231
+ column_cells[col_idx].append(Paragraph(apply_emoji_font(content, "NotoEmoji-Bold"), section_style))
232
+ else:
233
+ column_cells[col_idx].append(Paragraph(apply_emoji_font(item, "NotoEmoji-Bold"), item_style))
234
+ else:
235
+ column_cells[col_idx].append(Paragraph(apply_emoji_font(str(item), "NotoEmoji-Bold"), item_style))
236
+ max_cells = max(len(cells) for cells in column_cells) if column_cells else 0
237
+ for cells in column_cells:
238
+ cells.extend([Paragraph("", item_style)] * (max_cells - len(cells)))
239
+ table_data = list(zip(*column_cells)) if column_cells else [[]]
240
+ table = Table(table_data, colWidths=[col_width] * num_columns, hAlign='CENTER')
241
+ table.setStyle(TableStyle([
242
+ ('VALIGN', (0, 0), (-1, -1), 'TOP'),
243
+ ('ALIGN', (0, 0), (-1, -1), 'LEFT'),
244
+ ('BACKGROUND', (0, 0), (-1, -1), colors.white),
245
+ ('GRID', (0, 0), (-1, -1), 0, colors.white),
246
+ ('LINEAFTER', (0, 0), (num_columns-1, -1), 0.5, colors.grey),
247
+ ('LEFTPADDING', (0, 0), (-1, -1), 2),
248
+ ('RIGHTPADDING', (0, 0), (-1, -1), 2),
249
+ ('TOPPADDING', (0, 0), (-1, -1), 1),
250
+ ('BOTTOMPADDING', (0, 0), (-1, -1), 1),
251
+ ]))
252
+ story.append(Spacer(1, spacer_height))
253
+ story.append(table)
254
+ story.append(Spacer(1, spacer_height * 2))
255
+ for img_path in image_files:
256
+ try:
257
+ img = Image.open(img_path)
258
+ img_width, img_height = img.size
259
+ page_width, page_height = A4
260
+ scale = min((page_width - 40) / img_width, (page_height - 40) / img_height)
261
+ new_width = img_width * scale
262
+ new_height = img_height * scale
263
+ x = (page_width - new_width) / 2
264
+ y = (page_height - new_height) / 2
265
+ buffer_img = io.BytesIO()
266
+ c = canvas.Canvas(buffer_img, pagesize=A4)
267
+ c.drawImage(img_path, x, y, new_width, new_height)
268
+ c.showPage()
269
+ c.save()
270
+ buffer_img.seek(0)
271
+ img_pdf = fitz.open(stream=buffer_img, filetype="pdf")
272
+ story.append(fitz.open(stream=buffer_img, filetype="pdf").load_page(0))
273
+ except Exception as e:
274
+ st.warning(f"Could not process image {img_path}: {e}")
275
+ continue
276
+ doc.build(story)
277
+ buffer.seek(0)
278
+ return buffer.getvalue()
279
+
280
+ def pdf_to_image(pdf_bytes):
281
+ if pdf_bytes is None:
282
+ return None
283
+ try:
284
+ doc = fitz.open(stream=pdf_bytes, filetype="pdf")
285
+ images = []
286
+ for page in doc:
287
+ pix = page.get_pixmap(matrix=fitz.Matrix(2.0, 2.0))
288
+ img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
289
+ images.append(img)
290
+ doc.close()
291
+ return images
292
+ except Exception as e:
293
+ st.error(f"Failed to render PDF preview: {e}")
294
+ return None
295
+
296
+ def get_video_html(video_path, width="100%"):
297
+ try:
298
+ video_url = f"data:video/mp4;base64,{base64.b64encode(open(video_path, 'rb').read()).decode()}"
299
+ return f'''
300
+ <video width="{width}" controls autoplay muted loop>
301
+ <source src="{video_url}" type="video/mp4">
302
+ Your browser does not support the video tag.
303
+ </video>
304
+ '''
305
+ except Exception as e:
306
+ st.warning(f"Could not load video {video_path}: {e}")
307
+ return ""
308
+
309
+ def display_glossary_entity(k):
310
+ search_urls = {
311
+ "๐Ÿš€๐ŸŒŒArXiv": lambda k: f"https://arxiv.org/search/?query={quote(k)}&searchtype=all",
312
+ "๐Ÿ“–": lambda k: f"https://en.wikipedia.org/wiki/{quote(k)}",
313
+ "๐Ÿ”": lambda k: f"https://www.google.com/search?q={quote(k)}",
314
+ "๐ŸŽฅ": lambda k: f"https://www.youtube.com/results?search_query={quote(k)}",
315
+ }
316
+ links_md = ' '.join([f"[{emoji}]({url(k)})" for emoji, url in search_urls.items()])
317
+ st.markdown(f"**{k}** <small>{links_md}</small>", unsafe_allow_html=True)
318
+
319
+ # Tabs setup
320
+ tab1, tab2 = st.tabs(["๐Ÿ“„ PDF Composer", "๐Ÿงช Code Interpreter"])
321
+
322
+ with tab1:
323
+ st.header("๐Ÿ“„ PDF Composer & Voice Generator ๐Ÿš€")
324
+ # Sidebar PDF text settings
325
+ columns = st.sidebar.slider("Text columns", 1, 3, 2)
326
+ font_family = st.sidebar.selectbox("Font", ["Helvetica", "Times-Roman", "Courier", "DejaVuSans"])
327
+ font_size = st.sidebar.slider("Font size", 6, 24, 14)
328
+ # Markdown input
329
+ md_file = st.file_uploader("Upload Markdown (.md)", type=["md"])
330
+ if md_file:
331
+ md_text = md_file.getvalue().decode("utf-8")
332
+ stem = Path(md_file.name).stem
333
+ else:
334
+ md_text = st.text_area("Or enter markdown text directly", height=200)
335
+ stem = datetime.now().strftime('%Y%m%d_%H%M%S')
336
+ # Convert Markdown to plain text
337
+ renderer = mistune.HTMLRenderer()
338
+ markdown = mistune.create_markdown(renderer=renderer)
339
+ html = markdown(md_text or "")
340
+ plain_text = re.sub(r'<[^>]+>', '', html)
341
+ # Voice settings
342
+ languages = {"English (US)": "en", "English (UK)": "en-uk", "Spanish": "es"}
343
+ voice_choice = st.selectbox("Voice Language", list(languages.keys()))
344
+ voice_lang = languages[voice_choice]
345
+ slow = st.checkbox("Slow Speech")
346
+ VOICES = ["en-US-AriaNeural", "en-US-JennyNeural", "en-GB-SoniaNeural", "en-US-GuyNeural", "en-US-AnaNeural"]
347
+ selected_voice = st.selectbox("Select Voice for TTS", options=VOICES, index=0)
348
+ if st.button("๐Ÿ”Š Generate & Download Voice MP3 from Text"):
349
+ if plain_text.strip():
350
+ voice_file = f"{stem}_{selected_voice}.mp3"
351
+ try:
352
+ cleaned_text = clean_for_speech(plain_text)
353
+ audio_file = asyncio.run(generate_audio(cleaned_text, selected_voice, voice_file))
354
+ st.audio(audio_file)
355
+ with open(audio_file, 'rb') as mp3:
356
+ st.download_button("๐Ÿ“ฅ Download MP3", data=mp3, file_name=voice_file, mime="audio/mpeg")
357
+ except Exception as e:
358
+ st.error(f"Error generating voice: {e}")
359
+ else:
360
+ st.warning("No text to generate voice from.")
361
+ # Image uploads and ordering
362
+ imgs = st.file_uploader("Upload Images for PDF", type=["png", "jpg", "jpeg"], accept_multiple_files=True)
363
+ ordered_images = []
364
+ if imgs:
365
+ df_imgs = pd.DataFrame([{"name": f.name, "order": i} for i, f in enumerate(imgs)])
366
+ edited = st.data_editor(df_imgs, use_container_width=True, num_rows="dynamic")
367
+ for _, row in edited.sort_values("order").iterrows():
368
+ for f in imgs:
369
+ if f.name == row['name']:
370
+ ordered_images.append(f)
371
+ break
372
+ if st.button("๐Ÿ–‹๏ธ Generate PDF with Markdown & Images"):
373
+ if not plain_text.strip() and not ordered_images:
374
+ st.warning("Please provide some text or upload images to generate a PDF.")
375
+ else:
376
+ buf = io.BytesIO()
377
+ c = canvas.Canvas(buf)
378
+ if plain_text.strip():
379
+ page_w, page_h = letter
380
+ margin = 40
381
+ gutter = 20
382
+ col_w = (page_w - 2*margin - (columns-1)*gutter) / columns
383
+ c.setFont(font_family, font_size)
384
+ line_height = font_size * 1.2
385
+ col = 0
386
+ x = margin
387
+ y = page_h - margin
388
+ avg_char_width = font_size * 0.6
389
+ wrap_width = int(col_w / avg_char_width) if avg_char_width > 0 else 100
390
+ for paragraph in plain_text.split("\n"):
391
+ if not paragraph.strip():
392
+ y -= line_height
393
+ if y < margin:
394
+ col += 1
395
+ if col >= columns:
396
+ c.showPage()
397
+ c.setFont(font_family, font_size)
398
+ col = 0
399
+ x = margin + col*(col_w+gutter)
400
+ y = page_h - margin
401
+ continue
402
+ for line in textwrap.wrap(paragraph, wrap_width):
403
+ if y < margin:
404
+ col += 1
405
+ if col >= columns:
406
+ c.showPage()
407
+ c.setFont(font_family, font_size)
408
+ col = 0
409
+ x = margin + col*(col_w+gutter)
410
+ y = page_h - margin
411
+ c.drawString(x, y, line)
412
+ y -= line_height
413
+ y -= line_height
414
+ for img_f in ordered_images:
415
+ try:
416
+ img = Image.open(img_f)
417
+ w, h = img.size
418
+ c.showPage()
419
+ c.setPageSize((w, h))
420
+ c.drawImage(ImageReader(img), 0, 0, w, h, preserveAspectRatio=False)
421
+ except Exception as e:
422
+ st.warning(f"Could not process image {img_f.name}: {e}")
423
+ continue
424
+ c.save()
425
+ buf.seek(0)
426
+ pdf_name = f"{stem}.pdf"
427
+ st.download_button("โฌ‡๏ธ Download PDF", data=buf, file_name=pdf_name, mime="application/pdf")
428
+ st.markdown("---")
429
+ st.subheader("๐Ÿ“‚ Available Assets")
430
+ all_assets = glob.glob("*.*")
431
+ excluded_extensions = ['.py', '.ttf', '.txt']
432
+ excluded_files = ['README.md', 'index.html']
433
+ assets = sorted([
434
+ a for a in all_assets
435
+ if not (a.lower().endswith(tuple(excluded_extensions)) or a in excluded_files)
436
+ and a.lower().endswith(('.md', '.png', '.jpg', '.jpeg'))
437
+ ])
438
+ if 'selected_assets' not in st.session_state:
439
+ st.session_state.selected_assets = []
440
+ if not assets:
441
+ st.info("No available assets found.")
442
+ else:
443
+ for a in assets:
444
+ ext = a.split('.')[-1].lower()
445
+ cols = st.columns([1, 3, 1, 1])
446
+ with cols[0]:
447
+ is_selected = st.checkbox("", key=f"select_{a}", value=a in st.session_state.selected_assets)
448
+ if is_selected and a not in st.session_state.selected_assets:
449
+ st.session_state.selected_assets.append(a)
450
+ elif not is_selected and a in st.session_state.selected_assets:
451
+ st.session_state.selected_assets.remove(a)
452
+ cols[1].write(a)
453
+ try:
454
+ if ext == 'md':
455
+ with open(a, 'r', encoding='utf-8') as f:
456
+ cols[2].download_button("๐Ÿ“ฅ", data=f.read(), file_name=a, mime="text/markdown")
457
+ elif ext in ['png', 'jpg', 'jpeg']:
458
+ with open(a, 'rb') as img_file:
459
+ cols[2].download_button("โฌ‡๏ธ", data=img_file, file_name=a, mime=f"image/{ext}")
460
+ cols[3].button("๐Ÿ—‘๏ธ", key=f"del_{a}", on_click=delete_asset, args=(a,))
461
+ except Exception as e:
462
+ cols[3].error(f"Error handling file {a}: {e}")
463
+ if st.button("๐Ÿ“‘ Generate PDF from Selected Assets"):
464
+ if not st.session_state.selected_assets:
465
+ st.warning("Please select at least one asset to generate a PDF.")
466
+ else:
467
+ markdown_texts = []
468
+ image_files = []
469
+ for a in st.session_state.selected_assets:
470
+ ext = a.split('.')[-1].lower()
471
+ if ext == 'md':
472
+ with open(a, 'r', encoding='utf-8') as f:
473
+ markdown_texts.append(f.read())
474
+ elif ext in ['png', 'jpg', 'jpeg']:
475
+ image_files.append(a)
476
+ with st.spinner("Generating PDF from selected assets..."):
477
+ pdf_bytes = create_pdf(
478
+ markdown_texts=markdown_texts,
479
+ image_files=image_files,
480
+ base_font_size=14,
481
+ num_columns=2,
482
+ add_space_before_numbered=True,
483
+ headings_to_fonts=True,
484
+ doc_title="Combined_Selected_Assets"
485
+ )
486
+ if pdf_bytes:
487
+ pdf_images = pdf_to_image(pdf_bytes)
488
+ if pdf_images:
489
+ st.subheader("Preview of Generated PDF")
490
+ for i, img in enumerate(pdf_images):
491
+ st.image(img, caption=f"Page {i+1}", use_container_width=True)
492
+ prefix = datetime.now().strftime("%Y%m%d_%H%M%S")
493
+ st.download_button(
494
+ label="๐Ÿ’พ Download Combined PDF",
495
+ data=pdf_bytes,
496
+ file_name=f"{prefix}_combined.pdf",
497
+ mime="application/pdf"
498
+ )
499
+ else:
500
+ st.error("Failed to generate PDF.")
501
+ st.markdown("---")
502
+ st.subheader("๐Ÿ–ผ Image Gallery")
503
+ image_files = glob.glob("*.png") + glob.glob("*.jpg") + glob.glob("*.jpeg")
504
+ image_cols = st.slider("Gallery Columns ๐Ÿ–ผ", min_value=1, max_value=15, value=5, key="image_cols")
505
+ if image_files:
506
+ cols = st.columns(image_cols)
507
+ for idx, image_file in enumerate(image_files):
508
+ with cols[idx % image_cols]:
509
+ try:
510
+ img = Image.open(image_file)
511
+ st.image(img, caption=image_file, use_container_width=True)
512
+ display_glossary_entity(os.path.splitext(image_file)[0])
513
+ except Exception as e:
514
+ st.warning(f"Could not load image {image_file}: {e}")
515
+ else:
516
+ st.info("No images found in the current directory.")
517
+ st.markdown("---")
518
+ st.subheader("๐ŸŽฅ Video Gallery")
519
+ video_files = glob.glob("*.mp4")
520
+ video_cols = st.slider("Gallery Columns ๐ŸŽฌ", min_value=1, max_value=5, value=3, key="video_cols")
521
+ if video_files:
522
+ cols = st.columns(video_cols)
523
+ for idx, video_file in enumerate(video_files):
524
+ with cols[idx % video_cols]:
525
+ st.markdown(get_video_html(video_file, width="100%"), unsafe_allow_html=True)
526
+ display_glossary_entity(os.path.splitext(video_file)[0])
527
+ else:
528
+ st.info("No videos found in the current directory.")
529
+
530
+ with tab2:
531
+ st.header("๐Ÿงช Python Code Executor & Demo")
532
+ import io, sys
533
+ from contextlib import redirect_stdout
534
+ DEFAULT_CODE = '''import streamlit as st
535
+ import random
536
+ st.title("๐Ÿ“Š Demo App")
537
+ st.markdown("Random number and color demo")
538
+ col1, col2 = st.columns(2)
539
+ with col1:
540
+ num = st.number_input("Number:", 1, 100, 10)
541
+ mul = st.slider("Multiplier:", 1, 10, 2)
542
+ if st.button("Calc"):
543
+ st.write(num * mul)
544
+ with col2:
545
+ color = st.color_picker("Pick color","#ff0000")
546
+ st.markdown(f'<div style="background:{color};padding:10px;">Color</div>', unsafe_allow_html=True)
547
+ '''
548
+ def extract_python_code(md: str) -> list:
549
+ return re.findall(r"```python\s*(.*?)```", md, re.DOTALL)
550
+ def execute_code(code: str) -> tuple:
551
+ buf = io.StringIO(); local_vars = {}
552
+ try:
553
+ with redirect_stdout(buf):
554
+ exec(code, {}, local_vars)
555
+ return buf.getvalue(), None
556
+ except Exception as e:
557
+ return None, str(e)
558
+ up = st.file_uploader("Upload .py or .md", type=['py', 'md'])
559
+ if 'code' not in st.session_state:
560
+ st.session_state.code = DEFAULT_CODE
561
+ if up:
562
+ text = up.getvalue().decode()
563
+ if up.type == 'text/markdown':
564
+ codes = extract_python_code(text)
565
+ if codes:
566
+ st.session_state.code = codes[0].strip()
567
+ else:
568
+ st.warning("No Python code block found in the markdown file.")
569
+ st.session_state.code = ''
570
+ else:
571
+ st.session_state.code = text.strip()
572
+ st.code(st.session_state.code, language='python')
573
+ else:
574
+ st.session_state.code = st.text_area("๐Ÿ’ป Code Editor", value=st.session_state.code, height=400)
575
+ c1, c2 = st.columns([1, 1])
576
+ if c1.button("โ–ถ๏ธ Run Code"):
577
+ if st.session_state.code.strip():
578
+ out, err = execute_code(st.session_state.code)
579
+ if err:
580
+ st.error(f"Execution Error:\n{err}")
581
+ elif out:
582
+ st.subheader("Output:")
583
+ st.code(out)
584
+ else:
585
+ st.success("Executed with no standard output.")
586
+ else:
587
+ st.warning("No code to run.")
588
+ if c2.button("๐Ÿ—‘๏ธ Clear Code"):
589
+ st.session_state.code = ''
590
+ st.rerun()