awacke1 commited on
Commit
b95d864
·
verified ·
1 Parent(s): 7eca515

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +359 -77
app.py CHANGED
@@ -10,7 +10,7 @@ import streamlit as st
10
  import pandas as pd
11
  from PIL import Image
12
  from reportlab.pdfgen import canvas
13
- from reportlab.lib.pagesizes import letter
14
  from reportlab.lib.utils import ImageReader
15
  import mistune
16
  from gtts import gTTS
@@ -19,50 +19,224 @@ from gtts import gTTS
19
  st.set_page_config(page_title="PDF & Code Interpreter", layout="wide", page_icon="🚀")
20
 
21
  def delete_asset(path):
 
22
  try:
23
  os.remove(path)
 
 
 
24
  except Exception as e:
25
  st.error(f"Error deleting file: {e}")
26
  st.rerun()
27
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  # Tabs setup
29
  tab1, tab2 = st.tabs(["📄 PDF Composer", "🧪 Code Interpreter"])
30
 
31
  with tab1:
32
  st.header("📄 PDF Composer & Voice Generator 🚀")
33
 
34
- # Sidebar PDF text settings
35
- columns = st.sidebar.slider("Text columns", 1, 3, 1)
36
- font_family = st.sidebar.selectbox("Font", ["Helvetica","Times-Roman","Courier"])
37
- font_size = st.sidebar.slider("Font size", 6, 24, 12)
38
-
39
- # Markdown input
40
- md_file = st.file_uploader("Upload Markdown (.md)", type=["md"])
 
 
 
 
41
  if md_file:
42
  md_text = md_file.getvalue().decode("utf-8")
43
- stem = Path(md_file.name).stem
 
44
  else:
45
- md_text = st.text_area("Or enter markdown text directly", height=200)
46
- stem = datetime.now().strftime('%Y%m%d_%H%M%S')
47
 
48
- # Convert Markdown to plain text
49
- # Using mistune to parse markdown to HTML, then stripping HTML tags
50
  renderer = mistune.HTMLRenderer()
51
  markdown = mistune.create_markdown(renderer=renderer)
52
  html = markdown(md_text or "")
53
- plain_text = re.sub(r'<[^>]+>', '', html) # Strip HTML tags
54
 
55
- # Voice settings
 
56
  languages = {"English (US)": "en", "English (UK)": "en-uk", "Spanish": "es"}
57
  voice_choice = st.selectbox("Voice Language", list(languages.keys()))
58
  voice_lang = languages[voice_choice]
59
  slow = st.checkbox("Slow Speech")
60
 
61
  if st.button("🔊 Generate & Download Voice MP3 from Text"):
62
- if plain_text.strip():
63
- voice_file = f"{stem}.mp3"
64
  try:
65
- tts = gTTS(text=plain_text, lang=voice_lang, slow=slow)
 
66
  tts.save(voice_file)
67
  st.audio(voice_file)
68
  with open(voice_file, 'rb') as mp3:
@@ -72,29 +246,33 @@ with tab1:
72
  else:
73
  st.warning("No text to generate voice from.")
74
 
75
- # Image uploads and ordering
76
- imgs = st.file_uploader("Upload Images for PDF", type=["png", "jpg", "jpeg"], accept_multiple_files=True)
77
- ordered_images = []
 
78
  if imgs:
79
  # Create a DataFrame for editing image order
80
  df_imgs = pd.DataFrame([{"name": f.name, "order": i} for i, f in enumerate(imgs)])
81
- edited = st.data_editor(df_imgs, use_container_width=True, num_rows="dynamic") # Use num_rows="dynamic" for better UI
82
- # Reconstruct the ordered list of file objects
 
83
  for _, row in edited.sort_values("order").iterrows():
84
  for f in imgs:
85
  if f.name == row['name']:
86
- ordered_images.append(f)
87
  break # Found the file object, move to the next row
88
 
89
- if st.button("🖋️ Generate PDF with Markdown & Images"):
90
- if not plain_text.strip() and not ordered_images:
91
- st.warning("Please provide some text or upload images to generate a PDF.")
 
 
92
  else:
93
  buf = io.BytesIO()
94
  c = canvas.Canvas(buf)
95
 
96
- # Render text with columns if there is text
97
- if plain_text.strip():
98
  page_w, page_h = letter
99
  margin = 40
100
  gutter = 20
@@ -104,101 +282,205 @@ with tab1:
104
  col = 0
105
  x = margin
106
  y = page_h - margin
107
- # Estimate wrap width based on font size and column width
108
- # This is an approximation and might need fine-tuning
109
- avg_char_width = font_size * 0.6 # Approximation
110
- wrap_width = int(col_w / avg_char_width) if avg_char_width > 0 else 100 # Prevent division by zero
111
-
112
- for paragraph in plain_text.split("\n"):
113
- # Handle empty lines by adding vertical space
114
- if not paragraph.strip():
115
- y -= line_height
116
- if y < margin:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  col += 1
118
  if col >= columns:
119
  c.showPage()
120
  c.setFont(font_family, font_size)
121
  col = 0
122
- x = margin + col*(col_w+gutter)
123
- y = page_h - margin
124
- continue # Skip to next paragraph
 
 
125
 
126
- for line in textwrap.wrap(paragraph, wrap_width):
127
- if y < margin:
128
- col += 1
129
- if col >= columns:
130
- c.showPage()
131
- c.setFont(font_family, font_size)
132
- col = 0
133
- x = margin + col*(col_w+gutter)
134
- y = page_h - margin
135
  c.drawString(x, y, line)
136
  y -= line_height
137
- y -= line_height # Add space between paragraphs
138
 
139
- # Autosize pages to each image
140
- for img_f in ordered_images:
 
 
 
 
 
 
 
 
141
  try:
142
- img = Image.open(img_f)
143
  w, h = img.size
144
- c.showPage() # Start a new page for each image
145
- # Set page size to image size
146
- c.setPageSize((w, h))
147
- # Draw image filling the page
148
- c.drawImage(ImageReader(img), 0, 0, w, h, preserveAspectRatio=False) # Use False to fill page exactly
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  except Exception as e:
150
- st.warning(f"Could not process image {img_f.name}: {e}")
151
  continue
152
 
 
 
 
 
 
 
153
  c.save()
154
  buf.seek(0)
155
- pdf_name = f"{stem}.pdf"
156
- st.download_button("⬇️ Download PDF", data=buf, file_name=pdf_name, mime="application/pdf")
157
 
158
  st.markdown("---")
159
  st.subheader("📂 Available Assets")
 
 
160
  # Get all files and filter out unwanted ones
161
  all_assets = glob.glob("*.*")
162
  excluded_extensions = ['.py', '.ttf']
163
- excluded_files = ['README.md', 'index.html']
164
 
165
  assets = sorted([
166
  a for a in all_assets
167
- if not (a.lower().endswith(tuple(excluded_extensions)) or a in excluded_files)
168
  ])
169
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
  if not assets:
171
  st.info("No available assets found.")
172
  else:
 
 
 
 
 
 
173
  for a in assets:
174
  ext = a.split('.')[-1].lower()
175
- cols = st.columns([3, 1, 1])
176
- cols[0].write(a)
 
 
 
 
177
 
178
- # Provide download/preview based on file type
 
 
 
179
  try:
180
  if ext == 'pdf':
181
  with open(a, 'rb') as fp:
182
- cols[1].download_button("📥", data=fp, file_name=a, mime="application/pdf")
183
  elif ext == 'mp3':
184
- # Streamlit can play audio directly from file path
185
- cols[1].audio(a)
186
  with open(a, 'rb') as mp3:
187
- cols[1].download_button("📥", data=mp3, file_name=a, mime="audio/mpeg")
188
  # Add more file types here if needed (e.g., images)
189
  elif ext in ['png', 'jpg', 'jpeg', 'gif']:
190
  # Can't preview image directly in this column, offer download
191
  with open(a, 'rb') as img_file:
192
- cols[1].download_button("⬇️", data=img_file, file_name=a, mime=f"image/{ext}")
 
 
 
 
193
  # Handle other file types - maybe just offer download
194
  else:
195
  with open(a, 'rb') as other_file:
196
- cols[1].download_button("⬇️", data=other_file, file_name=a) # Mime type is guessed by streamlit
197
 
198
- # Delete button
199
- cols[2].button("🗑️", key=f"del_{a}", on_click=delete_asset, args=(a,))
200
  except Exception as e:
201
- cols[2].error(f"Error handling file {a}: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
202
 
203
 
204
  with tab2:
 
10
  import pandas as pd
11
  from PIL import Image
12
  from reportlab.pdfgen import canvas
13
+ from reportlab.lib.pagesizes import letter # Using letter size for consistency
14
  from reportlab.lib.utils import ImageReader
15
  import mistune
16
  from gtts import gTTS
 
19
  st.set_page_config(page_title="PDF & Code Interpreter", layout="wide", page_icon="🚀")
20
 
21
  def delete_asset(path):
22
+ """Deletes a file asset and reruns the app."""
23
  try:
24
  os.remove(path)
25
+ # Also remove from session state selection if it exists
26
+ if 'selected_assets' in st.session_state and path in st.session_state.selected_assets:
27
+ del st.session_state.selected_assets[path]
28
  except Exception as e:
29
  st.error(f"Error deleting file: {e}")
30
  st.rerun()
31
 
32
+ # --- New Function to Generate Combined PDF ---
33
+ def generate_combined_pdf(selected_asset_paths):
34
+ """Generates a single PDF from selected markdown and image file paths."""
35
+ buf = io.BytesIO()
36
+ c = canvas.Canvas(buf)
37
+
38
+ # --- Process Markdown Files ---
39
+ all_plain_text = ""
40
+ md_count = 0
41
+ for path in selected_asset_paths:
42
+ # Process only markdown files first
43
+ if path.lower().endswith('.md'):
44
+ md_count += 1
45
+ try:
46
+ with open(path, 'r', encoding='utf-8') as f:
47
+ md_text = f.read()
48
+ # Convert Markdown to plain text using mistune (removes formatting but keeps content)
49
+ renderer = mistune.HTMLRenderer()
50
+ markdown = mistune.create_markdown(renderer=renderer)
51
+ html = markdown(md_text or "")
52
+ plain_text = re.sub(r'<[^>]+>', '', html) # Strip HTML tags
53
+
54
+ if all_plain_text:
55
+ all_plain_text += "\n\n---\n\n" # Add a separator between combined MD files
56
+ all_plain_text += plain_text
57
+
58
+ except Exception as e:
59
+ st.warning(f"Could not read or process markdown file {path}: {e}")
60
+ # Decide if you want to continue or stop if an MD fails
61
+
62
+ # Render combined markdown content if any was found
63
+ if all_plain_text.strip():
64
+ # --- Canvas Text Rendering (2 columns, 14pt font) ---
65
+ page_w, page_h = letter # Use standard letter size (8.5 x 11 inches, approx 612 x 792 points)
66
+ margin = 40 # Margin around the content area (points)
67
+ gutter = 15 # Space between columns (points)
68
+ num_columns = 2 # Fixed number of columns as requested
69
+
70
+ # Calculate available width for text and column width
71
+ available_text_width = page_w - 2 * margin
72
+ col_w = (available_text_width - (num_columns - 1) * gutter) / num_columns
73
+
74
+ font_family = "Helvetica" # A standard font available in ReportLab canvas
75
+ font_size = 14 # Font size as requested
76
+ c.setFont(font_family, font_size)
77
+
78
+ # Estimate line height and character width for text wrapping
79
+ # ReportLab units are points. Approximating char width for wrapping.
80
+ # A common approximation for average character width is font_size * 0.6
81
+ avg_char_width_points = font_size * 0.6
82
+ # wrap_width is the number of characters that fit in one line of a column
83
+ wrap_width = int(col_w / avg_char_width_points) if avg_char_width_points > 0 else 100 # Prevent division by zero
84
+
85
+ line_height = font_size * 1.3 # Line spacing (e.g., 1.3 times font size)
86
+
87
+ # Initialize column and vertical position
88
+ col = 0
89
+ x = margin + col * (col_w + gutter) # Starting x for the first column
90
+ y = page_h - margin # Starting y from the top margin
91
+
92
+ paragraphs = all_plain_text.split("\n")
93
+
94
+ for paragraph in paragraphs:
95
+ # Handle empty lines (add vertical space)
96
+ if not paragraph.strip():
97
+ y -= line_height / 2 # Add less space for blank lines compared to paragraphs
98
+ # Check for page/column break after adding vertical space
99
+ if y < margin:
100
+ col += 1
101
+ if col >= num_columns:
102
+ c.showPage() # Move to a new page
103
+ c.setFont(font_family, font_size) # Re-set font after new page
104
+ col = 0 # Reset to the first column
105
+ x = margin + col * (col_w + gutter) # Reset x position
106
+ y = page_h - margin # Reset y position to top margin
107
+ else:
108
+ # Move to the next column on the same page
109
+ x = margin + col * (col_w + gutter)
110
+ y = page_h - margin # Reset y position to top margin
111
+ continue # Move to the next paragraph
112
+
113
+ # Wrap the paragraph text into lines that fit the column width
114
+ lines = textwrap.wrap(paragraph, wrap_width)
115
+
116
+ for line in lines:
117
+ # Check for page/column break before drawing the line
118
+ if y < margin:
119
+ col += 1
120
+ if col >= num_columns:
121
+ c.showPage() # Move to a new page
122
+ c.setFont(font_family, font_size) # Re-set font after new page
123
+ col = 0 # Reset to the first column
124
+ x = margin + col * (col_w + gutter) # Reset x position
125
+ y = page_h - margin # Reset y position to top margin
126
+ else:
127
+ # Move to the next column on the same page
128
+ x = margin + col * (col_w + gutter)
129
+ y = page_h - margin # Reset y position to top margin
130
+
131
+ # Draw the line
132
+ c.drawString(x, y, line)
133
+ # Move y position down for the next line
134
+ y -= line_height
135
+
136
+ # Add extra space after a paragraph (except the last one)
137
+ if paragraph != paragraphs[-1] or lines: # Add space if it's not the very last line of the last paragraph
138
+ y -= line_height / 2
139
+
140
+ # After all markdown text, ensure subsequent images start on a new page
141
+ if all_plain_text.strip():
142
+ c.showPage() # Start images on a fresh page
143
+
144
+ # --- Process Image Files ---
145
+ image_count = 0
146
+ for path in selected_asset_paths:
147
+ # Process image files after markdown
148
+ if path.lower().endswith(('.png', '.jpg', '.jpeg', '.gif')): # Add other image types if needed
149
+ image_count += 1
150
+ try:
151
+ img = Image.open(path)
152
+ img_w, img_h = img.size
153
+
154
+ # Get current page size (should be letter if no text was added or after showPage)
155
+ page_w, page_h = letter
156
+ margin_img = 40 # Margin around the image on the page
157
+
158
+ # Calculate available space within margins on the page
159
+ available_w = page_w - 2 * margin_img
160
+ available_h = page_h - 2 * margin_img
161
+
162
+ # Calculate scaling factor to fit the image within the available space while preserving aspect ratio
163
+ scale = min(available_w / img_w, available_h / img_h)
164
+ draw_w = img_w * scale
165
+ draw_h = img_h * scale
166
+
167
+ # Calculate position to center the scaled image on the page
168
+ pos_x = margin_img + (available_w - draw_w) / 2
169
+ # Position from the bottom left corner
170
+ pos_y = margin_img + (available_h - draw_h) / 2
171
+
172
+ # Draw the image. Ensure it's on a new page.
173
+ # If this is the first image and no text was added, it will use the initial page.
174
+ # Otherwise, showPage() is called before drawing.
175
+ if image_count > 1 or all_plain_text.strip():
176
+ c.showPage() # Start a new page for this image
177
+
178
+ # Draw the image onto the current page
179
+ c.drawImage(path, pos_x, pos_y, width=draw_w, height=draw_h, preserveAspectRatio=True)
180
+
181
+ except Exception as e:
182
+ st.warning(f"Could not process image file {path}: {e}")
183
+ continue # Continue with other selected assets
184
+
185
+ # If no markdown or images were selected/processed
186
+ if not all_plain_text.strip() and image_count == 0:
187
+ page_w, page_h = letter
188
+ c.drawString(40, page_h - 40, "No selected markdown or image files to generate PDF.")
189
+
190
+ c.save() # Finalize the PDF
191
+ buf.seek(0) # Rewind the buffer to the beginning
192
+ return buf.getvalue() # Return the PDF bytes
193
+ # --- End of New Function ---
194
+
195
+
196
  # Tabs setup
197
  tab1, tab2 = st.tabs(["📄 PDF Composer", "🧪 Code Interpreter"])
198
 
199
  with tab1:
200
  st.header("📄 PDF Composer & Voice Generator 🚀")
201
 
202
+ # Sidebar settings for the original PDF composer
203
+ # These settings (columns, font size for the *first* PDF button) are separate
204
+ # from the settings for the combined PDF generation below.
205
+ st.sidebar.markdown("### Original PDF Composer Settings")
206
+ columns = st.sidebar.slider("Text columns (Original PDF)", 1, 3, 1)
207
+ font_family = st.sidebar.selectbox("Font (Original PDF)", ["Helvetica","Times-Roman","Courier"])
208
+ font_size = st.sidebar.slider("Font size (Original PDF)", 6, 24, 12)
209
+
210
+ # Markdown input for the original PDF composer
211
+ st.markdown("#### Original PDF Composer Input")
212
+ md_file = st.file_uploader("Upload Markdown (.md) for Original PDF", type=["md"])
213
  if md_file:
214
  md_text = md_file.getvalue().decode("utf-8")
215
+ # Use stem from uploaded file or timestamp if text area is used
216
+ original_pdf_stem = Path(md_file.name).stem
217
  else:
218
+ md_text = st.text_area("Or enter markdown text directly for Original PDF", height=200)
219
+ original_pdf_stem = datetime.now().strftime('%Y%m%d_%H%M%S')
220
 
221
+ # Convert Markdown to plain text for original PDF
 
222
  renderer = mistune.HTMLRenderer()
223
  markdown = mistune.create_markdown(renderer=renderer)
224
  html = markdown(md_text or "")
225
+ original_pdf_plain_text = re.sub(r'<[^>]+>', '', html) # Strip HTML tags
226
 
227
+ # Voice settings (Applies to the text entered above)
228
+ st.markdown("#### Voice Generation from Text Input")
229
  languages = {"English (US)": "en", "English (UK)": "en-uk", "Spanish": "es"}
230
  voice_choice = st.selectbox("Voice Language", list(languages.keys()))
231
  voice_lang = languages[voice_choice]
232
  slow = st.checkbox("Slow Speech")
233
 
234
  if st.button("🔊 Generate & Download Voice MP3 from Text"):
235
+ if original_pdf_plain_text.strip():
236
+ voice_file = f"{original_pdf_stem}.mp3"
237
  try:
238
+ # Using the plain text from the text area/uploaded MD for voice
239
+ tts = gTTS(text=original_pdf_plain_text, lang=voice_lang, slow=slow)
240
  tts.save(voice_file)
241
  st.audio(voice_file)
242
  with open(voice_file, 'rb') as mp3:
 
246
  else:
247
  st.warning("No text to generate voice from.")
248
 
249
+ # Image uploads and ordering for the original PDF composer
250
+ st.markdown("#### Images for Original PDF")
251
+ imgs = st.file_uploader("Upload Images for Original PDF", type=["png", "jpg", "jpeg"], accept_multiple_files=True)
252
+ ordered_images_original_pdf = []
253
  if imgs:
254
  # Create a DataFrame for editing image order
255
  df_imgs = pd.DataFrame([{"name": f.name, "order": i} for i, f in enumerate(imgs)])
256
+ # Use num_rows="dynamic" for better UI, though less relevant if not adding/deleting rows
257
+ edited = st.data_editor(df_imgs, use_container_width=True)
258
+ # Reconstruct the ordered list of file objects based on edited order
259
  for _, row in edited.sort_values("order").iterrows():
260
  for f in imgs:
261
  if f.name == row['name']:
262
+ ordered_images_original_pdf.append(f)
263
  break # Found the file object, move to the next row
264
 
265
+
266
+ # --- Original PDF Generation Button ---
267
+ if st.button("🖋️ Generate Original PDF with Markdown & Images"):
268
+ if not original_pdf_plain_text.strip() and not ordered_images_original_pdf:
269
+ st.warning("Please provide some text or upload images to generate the Original PDF.")
270
  else:
271
  buf = io.BytesIO()
272
  c = canvas.Canvas(buf)
273
 
274
+ # Render text using original settings and logic if text is provided
275
+ if original_pdf_plain_text.strip():
276
  page_w, page_h = letter
277
  margin = 40
278
  gutter = 20
 
282
  col = 0
283
  x = margin
284
  y = page_h - margin
285
+ # Estimate wrap width
286
+ avg_char_width = font_size * 0.6
287
+ wrap_width = int(col_w / avg_char_width) if avg_char_width > 0 else 100
288
+
289
+ for paragraph in original_pdf_plain_text.split("\n"):
290
+ if not paragraph.strip(): # Handle empty lines
291
+ y -= line_height / 2
292
+ if y < margin: # Check for column/page break
293
+ col += 1
294
+ if col >= columns:
295
+ c.showPage()
296
+ c.setFont(font_family, font_size)
297
+ col = 0
298
+ x = margin + col*(col_w+gutter)
299
+ y = page_h - margin
300
+ else:
301
+ x = margin + col*(col_w+gutter)
302
+ y = page_h - margin
303
+ continue
304
+
305
+ lines = textwrap.wrap(paragraph, wrap_width) if paragraph.strip() else [""]
306
+
307
+ for line in lines:
308
+ if y < margin: # Check for column/page break
309
  col += 1
310
  if col >= columns:
311
  c.showPage()
312
  c.setFont(font_family, font_size)
313
  col = 0
314
+ x = margin + col*(col_w+gutter)
315
+ y = page_h - margin
316
+ else:
317
+ x = margin + col*(col_w+gutter)
318
+ y = page_h - margin
319
 
 
 
 
 
 
 
 
 
 
320
  c.drawString(x, y, line)
321
  y -= line_height
 
322
 
323
+ y -= line_height / 2 # Space after paragraph
324
+
325
+ # Ensure images start on a new page if text was added
326
+ if original_pdf_plain_text.strip():
327
+ c.showPage()
328
+
329
+ # Autosize pages to each uploaded image
330
+ image_count = 0
331
+ for img_f in ordered_images_original_pdf:
332
+ image_count += 1
333
  try:
334
+ img = Image.open(img_f) # img_f is a file-like object from st.file_uploader
335
  w, h = img.size
336
+
337
+ # Start a new page for each image
338
+ if image_count > 1 or original_pdf_plain_text.strip():
339
+ c.showPage()
340
+
341
+ # Draw image scaled to fit a letter page within margins, centered
342
+ page_w, page_h = letter
343
+ margin_img = 40
344
+ available_w = page_w - 2 * margin_img
345
+ available_h = page_h - 2 * margin_img
346
+
347
+ scale = min(available_w / w, available_h / h)
348
+ draw_w = w * scale
349
+ draw_h = h * scale
350
+
351
+ pos_x = margin_img + (available_w - draw_w) / 2
352
+ pos_y = margin_img + (available_h - draw_h) / 2
353
+
354
+ # Use ImageReader for file-like objects
355
+ c.drawImage(ImageReader(img_f), pos_x, pos_y, width=draw_w, height=draw_h, preserveAspectRatio=True)
356
+
357
  except Exception as e:
358
+ st.warning(f"Could not process uploaded image {img_f.name}: {e}")
359
  continue
360
 
361
+ # If nothing was generated
362
+ if not original_pdf_plain_text.strip() and not ordered_images_original_pdf:
363
+ page_w, page_h = letter
364
+ c.drawString(40, page_h - 40, "No content to generate Original PDF.")
365
+
366
+
367
  c.save()
368
  buf.seek(0)
369
+ pdf_name = f"{original_pdf_stem}.pdf"
370
+ st.download_button("⬇️ Download Original PDF", data=buf, file_name=pdf_name, mime="application/pdf")
371
 
372
  st.markdown("---")
373
  st.subheader("📂 Available Assets")
374
+ st.markdown("Select assets below and click 'Generate Combined PDF'.")
375
+
376
  # Get all files and filter out unwanted ones
377
  all_assets = glob.glob("*.*")
378
  excluded_extensions = ['.py', '.ttf']
379
+ excluded_files = ['README.md', 'index.html'] # Added index.html here
380
 
381
  assets = sorted([
382
  a for a in all_assets
383
+ if not (a.lower().endswith(tuple(excluded_extensions)) or os.path.basename(a) in excluded_files)
384
  ])
385
 
386
+ # Initialize session state for selected assets if not already done
387
+ if 'selected_assets' not in st.session_state:
388
+ st.session_state.selected_assets = {}
389
+
390
+ # Ensure all current assets have an entry in session state, initialize to False if new
391
+ # Clean up session state from assets that no longer exist
392
+ current_asset_paths = [os.path.abspath(a) for a in assets]
393
+ st.session_state.selected_assets = {
394
+ k: v for k, v in st.session_state.selected_assets.items()
395
+ if os.path.abspath(k) in current_asset_paths # Keep only existing assets
396
+ }
397
+ for asset_path in assets:
398
+ if asset_path not in st.session_state.selected_assets:
399
+ st.session_state.selected_assets[asset_path] = False
400
+
401
+
402
+ # --- Display Assets with Checkboxes ---
403
  if not assets:
404
  st.info("No available assets found.")
405
  else:
406
+ # Header row for clarity
407
+ header_cols = st.columns([0.5, 3, 1, 1])
408
+ header_cols[1].write("**File**")
409
+ # header_cols[2].write("**Action**") # Optional header
410
+
411
+
412
  for a in assets:
413
  ext = a.split('.')[-1].lower()
414
+ cols = st.columns([0.5, 3, 1, 1])
415
+
416
+ # Checkbox in the first column, updating session state
417
+ # Use absolute path for robust keying in case of directory changes (less likely in Streamlit sharing, but good practice)
418
+ asset_key = os.path.abspath(a)
419
+ st.session_state.selected_assets[a] = cols[0].checkbox("", value=st.session_state.selected_assets.get(a, False), key=f"select_asset_{asset_key}")
420
 
421
+ # File name in the second column
422
+ cols[1].write(a)
423
+
424
+ # Provide download/preview based on file type in the third column
425
  try:
426
  if ext == 'pdf':
427
  with open(a, 'rb') as fp:
428
+ cols[2].download_button("📥", data=fp, file_name=a, mime="application/pdf", key=f"download_{a}")
429
  elif ext == 'mp3':
430
+ # Audio player takes up too much space here, just offer download
 
431
  with open(a, 'rb') as mp3:
432
+ cols[2].download_button("📥", data=mp3, file_name=a, mime="audio/mpeg", key=f"download_{a}")
433
  # Add more file types here if needed (e.g., images)
434
  elif ext in ['png', 'jpg', 'jpeg', 'gif']:
435
  # Can't preview image directly in this column, offer download
436
  with open(a, 'rb') as img_file:
437
+ cols[2].download_button("⬇️", data=img_file.read(), file_name=a, mime=f"image/{ext}", key=f"download_{a}")
438
+ elif ext == 'md':
439
+ # Offer download for markdown files
440
+ with open(a, 'r', encoding='utf-8') as md_file:
441
+ cols[2].download_button("⬇️", data=md_file.read(), file_name=a, mime="text/markdown", key=f"download_{a}")
442
  # Handle other file types - maybe just offer download
443
  else:
444
  with open(a, 'rb') as other_file:
445
+ cols[2].download_button("⬇️", data=other_file.read(), file_name=a, key=f"download_{a}") # Mime type is guessed by streamlit
446
 
447
+ # Delete button in the fourth column
448
+ cols[3].button("🗑️", key=f"del_{a}", on_click=delete_asset, args=(a,))
449
  except Exception as e:
450
+ cols[3].error(f"Error handling file {a}: {e}") # Place error in the delete column or add a separate status
451
+
452
+
453
+ # --- Combined PDF Generation Button ---
454
+ # Only show button if there are any assets listed
455
+ if assets:
456
+ if st.button("Generate Combined PDF from Selected Assets"):
457
+ # Get the list of selected asset paths
458
+ selected_asset_paths = [path for path, selected in st.session_state.selected_assets.items() if selected]
459
+
460
+ if not selected_asset_paths:
461
+ st.warning("Please select at least one asset.")
462
+ else:
463
+ with st.spinner("Generating combined PDF..."):
464
+ try:
465
+ # Call the new function to generate the combined PDF
466
+ combined_pdf_bytes = generate_combined_pdf(selected_asset_paths)
467
+
468
+ if combined_pdf_bytes: # Check if the function returned bytes (meaning content was added)
469
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
470
+ pdf_name = f"Combined_Assets_{timestamp}.pdf"
471
+ # Provide the generated PDF for download
472
+ st.download_button(
473
+ "⬇️ Download Combined PDF",
474
+ data=combined_pdf_bytes,
475
+ file_name=pdf_name,
476
+ mime="application/pdf"
477
+ )
478
+ st.success("Combined PDF generated!")
479
+ else:
480
+ st.warning("Generated PDF is empty. No valid markdown or image files were selected.")
481
+
482
+ except Exception as e:
483
+ st.error(f"An unexpected error occurred during PDF generation: {e}")
484
 
485
 
486
  with tab2: