|
import io |
|
import os |
|
import re |
|
import glob |
|
import textwrap |
|
from datetime import datetime |
|
from pathlib import Path |
|
|
|
import streamlit as st |
|
import pandas as pd |
|
from PIL import Image |
|
from reportlab.pdfgen import canvas |
|
from reportlab.lib.pagesizes import letter |
|
from reportlab.lib.utils import ImageReader |
|
import mistune |
|
from gtts import gTTS |
|
|
|
|
|
st.set_page_config(page_title="PDF & Code Interpreter", layout="wide", page_icon="๐") |
|
|
|
def delete_asset(path): |
|
"""Deletes a file asset and reruns the app.""" |
|
try: |
|
os.remove(path) |
|
|
|
if 'selected_assets' in st.session_state and path in st.session_state.selected_assets: |
|
del st.session_state.selected_assets[path] |
|
except Exception as e: |
|
st.error(f"Error deleting file: {e}") |
|
st.rerun() |
|
|
|
|
|
def generate_combined_pdf(selected_asset_paths): |
|
"""Generates a single PDF from selected markdown and image file paths.""" |
|
buf = io.BytesIO() |
|
c = canvas.Canvas(buf) |
|
|
|
|
|
all_plain_text = "" |
|
md_count = 0 |
|
for path in selected_asset_paths: |
|
|
|
if path.lower().endswith('.md'): |
|
md_count += 1 |
|
try: |
|
with open(path, 'r', encoding='utf-8') as f: |
|
md_text = f.read() |
|
|
|
renderer = mistune.HTMLRenderer() |
|
markdown = mistune.create_markdown(renderer=renderer) |
|
html = markdown(md_text or "") |
|
plain_text = re.sub(r'<[^>]+>', '', html) |
|
|
|
if all_plain_text: |
|
all_plain_text += "\n\n---\n\n" |
|
all_plain_text += plain_text |
|
|
|
except Exception as e: |
|
st.warning(f"Could not read or process markdown file {path}: {e}") |
|
|
|
|
|
|
|
if all_plain_text.strip(): |
|
|
|
page_w, page_h = letter |
|
margin = 40 |
|
gutter = 15 |
|
num_columns = 2 |
|
|
|
|
|
available_text_width = page_w - 2 * margin |
|
col_w = (available_text_width - (num_columns - 1) * gutter) / num_columns |
|
|
|
font_family = "Helvetica" |
|
font_size = 14 |
|
c.setFont(font_family, font_size) |
|
|
|
|
|
|
|
|
|
avg_char_width_points = font_size * 0.6 |
|
|
|
wrap_width = int(col_w / avg_char_width_points) if avg_char_width_points > 0 else 100 |
|
|
|
line_height = font_size * 1.3 |
|
|
|
|
|
col = 0 |
|
x = margin + col * (col_w + gutter) |
|
y = page_h - margin |
|
|
|
paragraphs = all_plain_text.split("\n") |
|
|
|
for paragraph in paragraphs: |
|
|
|
if not paragraph.strip(): |
|
y -= line_height / 2 |
|
|
|
if y < margin: |
|
col += 1 |
|
if col >= num_columns: |
|
c.showPage() |
|
c.setFont(font_family, font_size) |
|
col = 0 |
|
x = margin + col * (col_w + gutter) |
|
y = page_h - margin |
|
else: |
|
|
|
x = margin + col * (col_w + gutter) |
|
y = page_h - margin |
|
continue |
|
|
|
|
|
lines = textwrap.wrap(paragraph, wrap_width) |
|
|
|
for line in lines: |
|
|
|
if y < margin: |
|
col += 1 |
|
if col >= num_columns: |
|
c.showPage() |
|
c.setFont(font_family, font_size) |
|
col = 0 |
|
x = margin + col * (col_w + gutter) |
|
y = page_h - margin |
|
else: |
|
|
|
x = margin + col * (col_w + gutter) |
|
y = page_h - margin |
|
|
|
|
|
c.drawString(x, y, line) |
|
|
|
y -= line_height |
|
|
|
|
|
if paragraph != paragraphs[-1] or lines: |
|
y -= line_height / 2 |
|
|
|
|
|
if all_plain_text.strip(): |
|
c.showPage() |
|
|
|
|
|
image_count = 0 |
|
for path in selected_asset_paths: |
|
|
|
if path.lower().endswith(('.png', '.jpg', '.jpeg', '.gif')): |
|
image_count += 1 |
|
try: |
|
img = Image.open(path) |
|
img_w, img_h = img.size |
|
|
|
|
|
page_w, page_h = letter |
|
margin_img = 40 |
|
|
|
|
|
available_w = page_w - 2 * margin_img |
|
available_h = page_h - 2 * margin_img |
|
|
|
|
|
scale = min(available_w / img_w, available_h / img_h) |
|
draw_w = img_w * scale |
|
draw_h = img_h * scale |
|
|
|
|
|
pos_x = margin_img + (available_w - draw_w) / 2 |
|
|
|
pos_y = margin_img + (available_h - draw_h) / 2 |
|
|
|
|
|
|
|
|
|
if image_count > 1 or all_plain_text.strip(): |
|
c.showPage() |
|
|
|
|
|
|
|
c.drawImage(path, pos_x, pos_y, width=draw_w, height=draw_h, preserveAspectRatio=True) |
|
|
|
except Exception as e: |
|
st.warning(f"Could not process image file {path}: {e}") |
|
continue |
|
|
|
|
|
if not all_plain_text.strip() and image_count == 0: |
|
page_w, page_h = letter |
|
c.drawString(40, page_h - 40, "No selected markdown or image files to generate PDF.") |
|
|
|
c.save() |
|
buf.seek(0) |
|
return buf.getvalue() |
|
|
|
|
|
|
|
|
|
tab1, tab2 = st.tabs(["๐ PDF Composer", "๐งช Code Interpreter"]) |
|
|
|
with tab1: |
|
st.header("๐ PDF Composer & Voice Generator ๐") |
|
|
|
|
|
|
|
|
|
st.sidebar.markdown("### Original PDF Composer Settings") |
|
columns = st.sidebar.slider("Text columns (Original PDF)", 1, 3, 1) |
|
font_family = st.sidebar.selectbox("Font (Original PDF)", ["Helvetica","Times-Roman","Courier"]) |
|
font_size = st.sidebar.slider("Font size (Original PDF)", 6, 24, 12) |
|
|
|
|
|
st.markdown("#### Original PDF Composer Input") |
|
md_file = st.file_uploader("Upload Markdown (.md) for Original PDF", type=["md"]) |
|
if md_file: |
|
md_text = md_file.getvalue().decode("utf-8") |
|
|
|
original_pdf_stem = Path(md_file.name).stem |
|
else: |
|
md_text = st.text_area("Or enter markdown text directly for Original PDF", height=200) |
|
original_pdf_stem = datetime.now().strftime('%Y%m%d_%H%M%S') |
|
|
|
|
|
renderer = mistune.HTMLRenderer() |
|
markdown = mistune.create_markdown(renderer=renderer) |
|
html = markdown(md_text or "") |
|
original_pdf_plain_text = re.sub(r'<[^>]+>', '', html) |
|
|
|
|
|
st.markdown("#### Voice Generation from Text Input") |
|
languages = {"English (US)": "en", "English (UK)": "en-uk", "Spanish": "es"} |
|
voice_choice = st.selectbox("Voice Language", list(languages.keys())) |
|
voice_lang = languages[voice_choice] |
|
slow = st.checkbox("Slow Speech") |
|
|
|
if st.button("๐ Generate & Download Voice MP3 from Text"): |
|
if original_pdf_plain_text.strip(): |
|
voice_file = f"{original_pdf_stem}.mp3" |
|
try: |
|
|
|
tts = gTTS(text=original_pdf_plain_text, lang=voice_lang, slow=slow) |
|
tts.save(voice_file) |
|
st.audio(voice_file) |
|
with open(voice_file, 'rb') as mp3: |
|
st.download_button("๐ฅ Download MP3", data=mp3, file_name=voice_file, mime="audio/mpeg") |
|
except Exception as e: |
|
st.error(f"Error generating voice: {e}") |
|
else: |
|
st.warning("No text to generate voice from.") |
|
|
|
|
|
st.markdown("#### Images for Original PDF") |
|
imgs = st.file_uploader("Upload Images for Original PDF", type=["png", "jpg", "jpeg"], accept_multiple_files=True) |
|
ordered_images_original_pdf = [] |
|
if imgs: |
|
|
|
df_imgs = pd.DataFrame([{"name": f.name, "order": i} for i, f in enumerate(imgs)]) |
|
|
|
edited = st.data_editor(df_imgs, use_container_width=True) |
|
|
|
for _, row in edited.sort_values("order").iterrows(): |
|
for f in imgs: |
|
if f.name == row['name']: |
|
ordered_images_original_pdf.append(f) |
|
break |
|
|
|
|
|
|
|
if st.button("๐๏ธ Generate Original PDF with Markdown & Images"): |
|
if not original_pdf_plain_text.strip() and not ordered_images_original_pdf: |
|
st.warning("Please provide some text or upload images to generate the Original PDF.") |
|
else: |
|
buf = io.BytesIO() |
|
c = canvas.Canvas(buf) |
|
|
|
|
|
if original_pdf_plain_text.strip(): |
|
page_w, page_h = letter |
|
margin = 40 |
|
gutter = 20 |
|
col_w = (page_w - 2*margin - (columns-1)*gutter) / columns |
|
c.setFont(font_family, font_size) |
|
line_height = font_size * 1.2 |
|
col = 0 |
|
x = margin |
|
y = page_h - margin |
|
|
|
avg_char_width = font_size * 0.6 |
|
wrap_width = int(col_w / avg_char_width) if avg_char_width > 0 else 100 |
|
|
|
for paragraph in original_pdf_plain_text.split("\n"): |
|
if not paragraph.strip(): |
|
y -= line_height / 2 |
|
if y < margin: |
|
col += 1 |
|
if col >= columns: |
|
c.showPage() |
|
c.setFont(font_family, font_size) |
|
col = 0 |
|
x = margin + col*(col_w+gutter) |
|
y = page_h - margin |
|
else: |
|
x = margin + col*(col_w+gutter) |
|
y = page_h - margin |
|
continue |
|
|
|
lines = textwrap.wrap(paragraph, wrap_width) if paragraph.strip() else [""] |
|
|
|
for line in lines: |
|
if y < margin: |
|
col += 1 |
|
if col >= columns: |
|
c.showPage() |
|
c.setFont(font_family, font_size) |
|
col = 0 |
|
x = margin + col*(col_w+gutter) |
|
y = page_h - margin |
|
else: |
|
x = margin + col*(col_w+gutter) |
|
y = page_h - margin |
|
|
|
c.drawString(x, y, line) |
|
y -= line_height |
|
|
|
y -= line_height / 2 |
|
|
|
|
|
if original_pdf_plain_text.strip(): |
|
c.showPage() |
|
|
|
|
|
image_count = 0 |
|
for img_f in ordered_images_original_pdf: |
|
image_count += 1 |
|
try: |
|
img = Image.open(img_f) |
|
w, h = img.size |
|
|
|
|
|
if image_count > 1 or original_pdf_plain_text.strip(): |
|
c.showPage() |
|
|
|
|
|
page_w, page_h = letter |
|
margin_img = 40 |
|
available_w = page_w - 2 * margin_img |
|
available_h = page_h - 2 * margin_img |
|
|
|
scale = min(available_w / w, available_h / h) |
|
draw_w = w * scale |
|
draw_h = h * scale |
|
|
|
pos_x = margin_img + (available_w - draw_w) / 2 |
|
pos_y = margin_img + (available_h - draw_h) / 2 |
|
|
|
|
|
c.drawImage(ImageReader(img_f), pos_x, pos_y, width=draw_w, height=draw_h, preserveAspectRatio=True) |
|
|
|
except Exception as e: |
|
st.warning(f"Could not process uploaded image {img_f.name}: {e}") |
|
continue |
|
|
|
|
|
if not original_pdf_plain_text.strip() and not ordered_images_original_pdf: |
|
page_w, page_h = letter |
|
c.drawString(40, page_h - 40, "No content to generate Original PDF.") |
|
|
|
|
|
c.save() |
|
buf.seek(0) |
|
pdf_name = f"{original_pdf_stem}.pdf" |
|
st.download_button("โฌ๏ธ Download Original PDF", data=buf, file_name=pdf_name, mime="application/pdf") |
|
|
|
st.markdown("---") |
|
st.subheader("๐ Available Assets") |
|
st.markdown("Select assets below to include in a combined PDF.") |
|
|
|
|
|
all_assets = glob.glob("*.*") |
|
|
|
excluded_extensions = ['.py', '.ttf'] |
|
excluded_files = ['README.md', 'index.html'] |
|
|
|
assets = sorted([ |
|
a for a in all_assets |
|
|
|
if not (a.lower().endswith(tuple(excluded_extensions)) or os.path.basename(a) in excluded_files) |
|
]) |
|
|
|
|
|
if 'selected_assets' not in st.session_state: |
|
st.session_state.selected_assets = {} |
|
|
|
|
|
|
|
current_asset_paths = [os.path.abspath(a) for a in assets] |
|
st.session_state.selected_assets = { |
|
k: v for k, v in st.session_state.selected_assets.items() |
|
if os.path.abspath(k) in current_asset_paths |
|
} |
|
for asset_path in assets: |
|
if asset_path not in st.session_state.selected_assets: |
|
st.session_state.selected_assets[asset_path] = False |
|
|
|
|
|
|
|
if not assets: |
|
st.info("No available assets found.") |
|
else: |
|
|
|
header_cols = st.columns([0.5, 3, 1, 1]) |
|
header_cols[1].write("**File**") |
|
|
|
|
|
|
|
for a in assets: |
|
ext = a.split('.')[-1].lower() |
|
cols = st.columns([0.5, 3, 1, 1]) |
|
|
|
|
|
|
|
asset_key = os.path.abspath(a) |
|
st.session_state.selected_assets[a] = cols[0].checkbox("", value=st.session_state.selected_assets.get(a, False), key=f"select_asset_{asset_key}") |
|
|
|
|
|
cols[1].write(a) |
|
|
|
|
|
try: |
|
if ext == 'pdf': |
|
with open(a, 'rb') as fp: |
|
cols[2].download_button("๐ฅ", data=fp, file_name=a, mime="application/pdf", key=f"download_{a}") |
|
elif ext == 'mp3': |
|
|
|
with open(a, 'rb') as mp3: |
|
cols[2].download_button("๐ฅ", data=mp3, file_name=a, mime="audio/mpeg", key=f"download_{a}") |
|
|
|
elif ext in ['md', 'txt', 'csv', 'json', 'xml', 'log']: |
|
with open(a, 'r', encoding='utf-8') as text_file: |
|
cols[2].download_button("โฌ๏ธ", data=text_file.read(), file_name=a, mime="text/plain", key=f"download_{a}") |
|
|
|
elif ext in ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'tiff']: |
|
with open(a, 'rb') as img_file: |
|
cols[2].download_button("โฌ๏ธ", data=img_file.read(), file_name=a, mime=f"image/{ext}", key=f"download_{a}") |
|
|
|
elif ext in ['mp4', 'webm', 'ogg', 'avi', 'mov']: |
|
with open(a, 'rb') as video_file: |
|
cols[2].download_button("โฌ๏ธ", data=video_file.read(), file_name=a, mime=f"video/{ext}", key=f"download_{a}") |
|
|
|
else: |
|
with open(a, 'rb') as other_file: |
|
cols[2].download_button("โฌ๏ธ", data=other_file.read(), file_name=a, key=f"download_{a}") |
|
|
|
|
|
|
|
cols[3].button("๐๏ธ", key=f"del_{a}", on_click=delete_asset, args=(a,)) |
|
except Exception as e: |
|
|
|
cols[3].error(f"Error: {e}") |
|
|
|
|
|
|
|
|
|
if assets: |
|
if st.button("Generate Combined PDF from Selected Assets"): |
|
|
|
selected_asset_paths = [path for path, selected in st.session_state.selected_assets.items() if selected] |
|
|
|
if not selected_asset_paths: |
|
st.warning("Please select at least one asset.") |
|
else: |
|
with st.spinner("Generating combined PDF..."): |
|
try: |
|
|
|
combined_pdf_bytes = generate_combined_pdf(selected_asset_paths) |
|
|
|
if combined_pdf_bytes: |
|
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') |
|
pdf_name = f"Combined_Assets_{timestamp}.pdf" |
|
|
|
st.download_button( |
|
"โฌ๏ธ Download Combined PDF", |
|
data=combined_pdf_bytes, |
|
file_name=pdf_name, |
|
mime="application/pdf" |
|
) |
|
st.success("Combined PDF generated!") |
|
else: |
|
|
|
st.warning("Generated PDF is empty. Check selected files or console for errors.") |
|
|
|
except Exception as e: |
|
st.error(f"An unexpected error occurred during PDF generation: {e}") |
|
|
|
|
|
st.markdown("---") |
|
st.subheader("๐ผ๏ธ Image Gallery") |
|
|
|
image_files = sorted(glob.glob("*.png") + glob.glob("*.jpg") + glob.glob("*.jpeg") + glob.glob("*.gif") + glob.glob("*.bmp") + glob.glob("*.tiff")) |
|
|
|
if not image_files: |
|
st.info("No image files found in the directory.") |
|
else: |
|
|
|
image_cols = st.slider("Image Gallery Columns", min_value=1, max_value=10, value=5) |
|
|
|
image_cols = max(1, image_cols) |
|
|
|
|
|
cols = st.columns(image_cols) |
|
for idx, image_file in enumerate(image_files): |
|
with cols[idx % image_cols]: |
|
try: |
|
img = Image.open(image_file) |
|
st.image(img, caption=os.path.basename(image_file), use_container_width=True) |
|
except Exception as e: |
|
st.warning(f"Could not display image {image_file}: {e}") |
|
|
|
|
|
|
|
st.markdown("---") |
|
st.subheader("๐ฅ Video Gallery") |
|
|
|
video_files = sorted(glob.glob("*.mp4") + glob.glob("*.webm") + glob.glob("*.ogg") + glob.glob("*.avi") + glob.glob("*.mov")) |
|
|
|
if not video_files: |
|
st.info("No video files found in the directory.") |
|
else: |
|
|
|
video_cols = st.slider("Video Gallery Columns", min_value=1, max_value=5, value=3) |
|
|
|
video_cols = max(1, video_cols) |
|
|
|
|
|
|
|
cols = st.columns(video_cols) |
|
for idx, video_file in enumerate(video_files): |
|
with cols[idx % video_cols]: |
|
try: |
|
|
|
st.video(video_file, caption=os.path.basename(video_file)) |
|
except Exception as e: |
|
st.warning(f"Could not display video {video_file}: {e}") |
|
|
|
|
|
with tab2: |
|
st.header("๐งช Python Code Executor & Demo") |
|
import io, sys |
|
from contextlib import redirect_stdout |
|
|
|
DEFAULT_CODE = '''import streamlit as st |
|
import random |
|
|
|
st.title("๐ Demo App") |
|
st.markdown("Random number and color demo") |
|
|
|
col1, col2 = st.columns(2) |
|
with col1: |
|
num = st.number_input("Number:", 1, 100, 10) |
|
mul = st.slider("Multiplier:", 1, 10, 2) |
|
if st.button("Calc"): |
|
st.write(num * mul) |
|
with col2: |
|
color = st.color_picker("Pick color","#ff0000") |
|
st.markdown(f'<div style="background:{color};padding:10px;">Color</div>', unsafe_allow_html=True) |
|
''' |
|
|
|
def extract_python_code(md: str) -> list: |
|
|
|
return re.findall(r"```python\s*(.*?)```", md, re.DOTALL) |
|
|
|
def execute_code(code: str) -> tuple: |
|
buf = io.StringIO(); local_vars = {} |
|
|
|
try: |
|
with redirect_stdout(buf): |
|
|
|
|
|
exec(code, {}, local_vars) |
|
return buf.getvalue(), None |
|
except Exception as e: |
|
return None, str(e) |
|
|
|
up = st.file_uploader("Upload .py or .md", type=['py', 'md']) |
|
|
|
if 'code' not in st.session_state: |
|
st.session_state.code = DEFAULT_CODE |
|
|
|
if up: |
|
text = up.getvalue().decode() |
|
if up.type == 'text/markdown': |
|
codes = extract_python_code(text) |
|
if codes: |
|
|
|
st.session_state.code = codes[0].strip() |
|
else: |
|
st.warning("No Python code block found in the markdown file.") |
|
st.session_state.code = '' |
|
else: |
|
st.session_state.code = text.strip() |
|
|
|
|
|
st.code(st.session_state.code, language='python') |
|
else: |
|
|
|
st.session_state.code = st.text_area("๐ป Code Editor", value=st.session_state.code, height=400) |
|
|
|
c1, c2 = st.columns([1, 1]) |
|
if c1.button("โถ๏ธ Run Code"): |
|
if st.session_state.code.strip(): |
|
out, err = execute_code(st.session_state.code) |
|
if err: |
|
st.error(f"Execution Error:\n{err}") |
|
elif out: |
|
st.subheader("Output:") |
|
st.code(out) |
|
else: |
|
st.success("Executed with no standard output.") |
|
else: |
|
st.warning("No code to run.") |
|
|
|
if c2.button("๐๏ธ Clear Code"): |
|
st.session_state.code = '' |
|
st.rerun() |