awacke1 commited on
Commit
5978ca5
Β·
verified Β·
1 Parent(s): 0ddf72d

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +371 -0
app.py ADDED
@@ -0,0 +1,371 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ from contextlib import redirect_stdout
9
+
10
+ import streamlit as st
11
+ import pandas as pd
12
+ from PIL import Image
13
+ from reportlab.pdfgen import canvas
14
+ from reportlab.lib.pagesizes import letter
15
+ from reportlab.lib.utils import ImageReader
16
+ import mistune
17
+ from gtts import gTTS
18
+
19
+ # --- Core Utility Functions ---
20
+
21
+ # πŸͺ„ Now you see it, now you don't.
22
+ def delete_asset(path: str):
23
+ if os.path.exists(path):
24
+ os.remove(path)
25
+ st.rerun()
26
+
27
+ # πŸ•΅οΈβ€β™‚οΈ On the hunt for related files in the digital jungle.
28
+ def get_project_files(pattern: str = "*_*.*") -> dict:
29
+ projects = {}
30
+ for f in glob.glob(pattern):
31
+ stem = Path(f).stem
32
+ project_name = '_'.join(stem.split('_')[:-1])
33
+ if project_name not in projects:
34
+ projects[project_name] = {'md': [], 'images': []}
35
+
36
+ ext = Path(f).suffix.lower()
37
+ if ext == '.md':
38
+ projects[project_name]['md'].append(f)
39
+ elif ext in ['.png', '.jpg', '.jpeg']:
40
+ projects[project_name]['images'].append(f)
41
+ return projects
42
+
43
+ # ✨ Turning markdown into its simple, unadorned soul.
44
+ def md_to_plain_text(md_text: str) -> str:
45
+ if not md_text:
46
+ return ""
47
+ html = mistune.html(md_text)
48
+ return re.sub(r'<[^>]+>', '', html).strip()
49
+
50
+ # --- PDF & Audio Generation ---
51
+
52
+ # πŸŽ™οΈ Lending a golden voice to your silent words.
53
+ def generate_audio(text: str, filename_stem: str, lang: str, slow: bool) -> str | None:
54
+ if not text:
55
+ st.warning("No text provided to generate audio.")
56
+ return None
57
+
58
+ voice_file = f"{filename_stem}.mp3"
59
+ try:
60
+ tts = gTTS(text=text, lang=lang, slow=slow)
61
+ tts.save(voice_file)
62
+ return voice_file
63
+ except Exception as e:
64
+ st.error(f"Failed to generate audio: {e}")
65
+ return None
66
+
67
+ # ✍️ Weaving words into PDF poetry.
68
+ def render_pdf_text(c: canvas.Canvas, text: str, settings: dict):
69
+ page_w, page_h = letter
70
+ margin = 40
71
+ gutter = 20
72
+ col_w = (page_w - 2 * margin - (settings['columns'] - 1) * gutter) / settings['columns']
73
+
74
+ c.setFont(settings['font_family'], settings['font_size'])
75
+ line_height = settings['font_size'] * 1.2
76
+ wrap_width = int(col_w / (settings['font_size'] * 0.6))
77
+
78
+ y = page_h - margin
79
+ col = 0
80
+ x = margin
81
+
82
+ for paragraph in text.split("\n"):
83
+ wrapped_lines = textwrap.wrap(paragraph, wrap_width, replace_whitespace=False, drop_whitespace=False)
84
+ if not wrapped_lines:
85
+ y -= line_height
86
+ if y < margin:
87
+ col, x, y = new_pdf_page_or_column(c, settings, col, margin, col_w, gutter, page_h)
88
+
89
+ for line in wrapped_lines:
90
+ if y < margin:
91
+ col, x, y = new_pdf_page_or_column(c, settings, col, margin, col_w, gutter, page_h)
92
+
93
+ c.drawString(x, y, line)
94
+ y -= line_height
95
+
96
+ # πŸ“„ Time to turn the page, or at least scoot over.
97
+ def new_pdf_page_or_column(c, settings, col, margin, col_w, gutter, page_h):
98
+ col += 1
99
+ if col >= settings['columns']:
100
+ c.showPage()
101
+ c.setFont(settings['font_family'], settings['font_size'])
102
+ col = 0
103
+
104
+ x = margin + col * (col_w + gutter)
105
+ y = page_h - margin
106
+ return col, x, y
107
+
108
+ # πŸ–ΌοΈ Arranging your pixels perfectly on the page.
109
+ def render_pdf_images(c: canvas.Canvas, image_files: list):
110
+ for img_file in image_files:
111
+ try:
112
+ img = Image.open(img_file)
113
+ w, h = img.size
114
+ c.showPage()
115
+ c.setPageSize((w, h))
116
+ c.drawImage(ImageReader(img), 0, 0, w, h, preserveAspectRatio=True, mask='auto')
117
+ except Exception as e:
118
+ st.warning(f"Could not process image {img_file.name}: {e}")
119
+ continue
120
+
121
+ # πŸ“œ The grand finale: text and images join forces in a PDF.
122
+ def generate_pdf_from_content(text: str, images: list, settings: dict, filename_stem: str) -> bytes:
123
+ buf = io.BytesIO()
124
+ c = canvas.Canvas(buf, pagesize=letter)
125
+
126
+ if text:
127
+ render_pdf_text(c, text, settings)
128
+
129
+ if images:
130
+ render_pdf_images(c, images)
131
+
132
+ c.save()
133
+ buf.seek(0)
134
+ return buf.getvalue()
135
+
136
+ # --- Streamlit UI Components ---
137
+
138
+ # πŸŽ›οΈ Organizing the mission control for your creative genius.
139
+ def setup_sidebar(projects: dict):
140
+ st.sidebar.header("🎨 PDF Style Settings")
141
+ settings = {
142
+ 'columns': st.sidebar.slider("Text columns", 1, 3, 1),
143
+ 'font_family': st.sidebar.selectbox("Font Family", ["Helvetica", "Times-Roman", "Courier"]),
144
+ 'font_size': st.sidebar.slider("Font Size", 6, 24, 12)
145
+ }
146
+
147
+ st.sidebar.header("πŸ“‚ Project Files")
148
+ st.sidebar.caption("Files matching the `Name_Date` pattern.")
149
+ if not projects:
150
+ st.sidebar.info("No projects found. Upload files with a `_` in the name.")
151
+ else:
152
+ sorted_projects = sorted(projects.items())
153
+ for name, files in sorted_projects:
154
+ with st.sidebar.expander(f"Project: {name}"):
155
+ if files['md']:
156
+ st.write("πŸ“„ " + ", ".join(Path(f).name for f in files['md']))
157
+ if files['images']:
158
+ st.write("πŸ–ΌοΈ " + ", ".join(Path(f).name for f in files['images']))
159
+
160
+ return settings
161
+
162
+ # πŸ† Putting your magnificent creations on display.
163
+ def display_local_assets():
164
+ st.markdown("---")
165
+ st.subheader("πŸ“‚ Available Assets")
166
+ assets = sorted(glob.glob("*.pdf") + glob.glob("*.mp3"))
167
+ if not assets:
168
+ st.info("No PDFs or MP3s generated yet.")
169
+ return
170
+
171
+ for asset_path in assets:
172
+ ext = Path(asset_path).suffix.lower()
173
+ cols = st.columns([4, 2, 1])
174
+ cols[0].write(f"`{asset_path}`")
175
+
176
+ with open(asset_path, 'rb') as f:
177
+ file_bytes = f.read()
178
+
179
+ if ext == '.pdf':
180
+ cols[1].download_button("⬇️ Download PDF", data=file_bytes, file_name=asset_path, mime="application/pdf")
181
+ elif ext == '.mp3':
182
+ cols[1].audio(file_bytes, format='audio/mpeg')
183
+
184
+ cols[2].button("πŸ—‘οΈ Delete", key=f"del_{asset_path}", on_click=delete_asset, args=(asset_path,))
185
+
186
+ # 🎭 The main stage for our PDF and Voice show.
187
+ def pdf_composer_tab(projects: dict):
188
+ st.header("πŸ“„ PDF Composer & Voice Generator πŸš€")
189
+
190
+ col1, col2 = st.columns(2)
191
+ with col1:
192
+ input_method = st.radio(
193
+ "Choose your content source:",
194
+ ["Select an existing project", "Upload new files or paste text"],
195
+ horizontal=True,
196
+ label_visibility="collapsed"
197
+ )
198
+
199
+ md_text = ""
200
+ selected_images = []
201
+ filename_stem = datetime.now().strftime('%Y%m%d_%H%M%S')
202
+
203
+ if input_method == "Select an existing project" and projects:
204
+ sorted_project_names = sorted(projects.keys())
205
+ chosen_project = st.selectbox("Select Project", sorted_project_names)
206
+
207
+ md_files = projects[chosen_project]['md']
208
+ if md_files:
209
+ md_path = md_files[0]
210
+ filename_stem = Path(md_path).stem
211
+ with open(md_path, 'r', encoding='utf-8') as f:
212
+ md_text = f.read()
213
+ st.text_area("Markdown Content", value=md_text, height=250, key="md_from_project")
214
+
215
+ image_files = projects[chosen_project]['images']
216
+ if image_files:
217
+ st.info(f"Found {len(image_files)} related image(s):")
218
+ for img in image_files:
219
+ st.image(img, width=150, caption=Path(img).name)
220
+ with open(img, 'rb') as f:
221
+ bytes_io = io.BytesIO(f.read())
222
+ bytes_io.name = Path(img).name
223
+ selected_images.append(bytes_io)
224
+
225
+ else:
226
+ st.info("Upload a Markdown file, or just paste your text below.")
227
+ uploaded_files = st.file_uploader(
228
+ "Upload Markdown (.md) and Image files (.png, .jpg)",
229
+ type=["md", "png", "jpg", "jpeg"],
230
+ accept_multiple_files=True
231
+ )
232
+
233
+ md_from_upload = [f for f in uploaded_files if f.type == "text/markdown"]
234
+ images_from_upload = [f for f in uploaded_files if f.type != "text/markdown"]
235
+
236
+ if md_from_upload:
237
+ md_file = md_from_upload[0]
238
+ md_text = md_file.getvalue().decode("utf-8")
239
+ filename_stem = Path(md_file.name).stem
240
+
241
+ md_text = st.text_area("Markdown Text", value=md_text, height=250, key="md_from_paste")
242
+ selected_images.extend(images_from_upload)
243
+
244
+ if selected_images:
245
+ st.subheader("πŸ–ΌοΈ Arrange Images")
246
+ st.caption("Drag rows to reorder the images for the PDF.")
247
+ df_imgs = pd.DataFrame([{"order": i + 1, "name": f.name, "preview": f} for i, f in enumerate(selected_images)])
248
+
249
+ edited_df = st.data_editor(
250
+ df_imgs,
251
+ column_config={"preview": st.column_config.ImageColumn("Preview")},
252
+ hide_index=True,
253
+ use_container_width=True,
254
+ num_rows="dynamic"
255
+ )
256
+ ordered_names = edited_df['name'].tolist()
257
+ selected_images.sort(key=lambda x: ordered_names.index(x.name))
258
+
259
+ plain_text = md_to_plain_text(md_text)
260
+
261
+ st.markdown("---")
262
+ st.subheader("🎬 Generate Your Files")
263
+
264
+ pdf_settings = setup_sidebar(projects)
265
+
266
+ pdf_col, voice_col = st.columns(2)
267
+
268
+ with pdf_col:
269
+ if st.button("πŸ–‹οΈ Generate PDF", use_container_width=True, type="primary"):
270
+ if not plain_text and not selected_images:
271
+ st.error("Cannot generate an empty PDF. Please add some text or images.")
272
+ else:
273
+ with st.spinner("Crafting your PDF..."):
274
+ pdf_bytes = generate_pdf_from_content(plain_text, selected_images, pdf_settings, filename_stem)
275
+ st.download_button(
276
+ label="⬇��� Download PDF",
277
+ data=pdf_bytes,
278
+ file_name=f"{filename_stem}.pdf",
279
+ mime="application/pdf",
280
+ use_container_width=True
281
+ )
282
+ st.success("PDF is ready for download!")
283
+
284
+ with voice_col:
285
+ st.markdown("<h6>Voice Generation</h6>", unsafe_allow_html=True)
286
+ languages = {"English (US)": "en", "English (UK)": "en-gb", "Spanish": "es"}
287
+ voice_choice = st.selectbox("Voice Language", list(languages.keys()))
288
+ slow_speech = st.checkbox("Slow Speech")
289
+
290
+ if st.button("πŸ”Š Generate MP3", use_container_width=True):
291
+ with st.spinner("Converting text to speech..."):
292
+ audio_file = generate_audio(plain_text, filename_stem, languages[voice_choice], slow_speech)
293
+ if audio_file:
294
+ st.success("MP3 generated!")
295
+ with open(audio_file, 'rb') as mp3:
296
+ st.download_button("πŸ“₯ Download MP3", data=mp3, file_name=audio_file, mime="audio/mpeg", use_container_width=True)
297
+ st.audio(audio_file)
298
+
299
+ display_local_assets()
300
+
301
+ # --- Code Interpreter Section ---
302
+
303
+ # πŸ‘» Catching code spirits in a bottle (or a buffer).
304
+ def execute_code(code: str) -> tuple[str | None, str | None]:
305
+ buf = io.StringIO()
306
+ try:
307
+ with redirect_stdout(buf):
308
+ exec(code, {})
309
+ return buf.getvalue(), None
310
+ except Exception as e:
311
+ return None, str(e)
312
+
313
+ # 🐍 Finding the sneaky Python hidden in the markdown grass.
314
+ def extract_python_code(md: str) -> list[str]:
315
+ return re.findall(r"```python\s*(.*?)```", md, re.DOTALL)
316
+
317
+ # πŸ§ͺ A safe lab for your wild Python experiments.
318
+ def code_interpreter_tab():
319
+ st.header("🐍 Python Code Executor")
320
+ st.info("Execute Python code snippets. Note: This runs within the Streamlit environment.")
321
+
322
+ DEFAULT_CODE = 'import streamlit as st\n\nst.balloons()\nst.write("Hello from the code interpreter!")'
323
+
324
+ if 'code' not in st.session_state:
325
+ st.session_state.code = DEFAULT_CODE
326
+
327
+ uploaded_file = st.file_uploader("Upload .py or .md file with Python code", type=['py', 'md'])
328
+
329
+ if uploaded_file:
330
+ text = uploaded_file.getvalue().decode()
331
+ if uploaded_file.type == 'text/markdown':
332
+ codes = extract_python_code(text)
333
+ st.session_state.code = codes[0] if codes else ''
334
+ else:
335
+ st.session_state.code = text
336
+
337
+ st.session_state.code = st.text_area("Code Editor", value=st.session_state.code, height=300, key="code_editor")
338
+
339
+ run_col, clear_col = st.columns(2)
340
+ if run_col.button("▢️ Run Code", use_container_width=True, type="primary"):
341
+ output, error = execute_code(st.session_state.code)
342
+ if error:
343
+ st.error(f"Execution Failed:\n\n{error}")
344
+ elif output:
345
+ st.subheader("Output")
346
+ st.code(output, language=None)
347
+ else:
348
+ st.success("βœ… Executed successfully with no output.")
349
+
350
+ if clear_col.button("πŸ—‘οΈ Clear Code", use_container_width=True):
351
+ st.session_state.code = ''
352
+ st.rerun()
353
+
354
+ # --- Main App ---
355
+
356
+ # 🎬 Lights, camera, action! Kicking off the whole show.
357
+ def main():
358
+ st.set_page_config(page_title="PDF & Code Interpreter", layout="wide", page_icon="πŸš€")
359
+
360
+ project_files = get_project_files()
361
+
362
+ tab1, tab2 = st.tabs(["πŸ“„ PDF Composer", "πŸ§ͺ Code Interpreter"])
363
+
364
+ with tab1:
365
+ pdf_composer_tab(project_files)
366
+
367
+ with tab2:
368
+ code_interpreter_tab()
369
+
370
+ if __name__ == "__main__":
371
+ main()