|
import io |
|
import os |
|
import re |
|
import glob |
|
import textwrap |
|
import base64 |
|
import sys |
|
from datetime import datetime |
|
from pathlib import Path |
|
from contextlib import redirect_stdout |
|
|
|
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 |
|
from reportlab.pdfbase import pdfmetrics |
|
from reportlab.pdfbase.ttfonts import TTFont |
|
import mistune |
|
from gtts import gTTS |
|
|
|
|
|
|
|
|
|
def delete_asset(path): |
|
"""Safely deletes a file if it exists and reruns the Streamlit app.""" |
|
try: |
|
os.remove(path) |
|
except OSError as e: |
|
st.error(f"Error deleting file {path}: {e}") |
|
st.rerun() |
|
|
|
|
|
def get_text_input(file_uploader_label, accepted_types, text_area_label): |
|
""" |
|
Provides UI for uploading a text file or entering text manually. |
|
Returns the text content and a filename stem. |
|
""" |
|
md_text = "" |
|
stem = datetime.now().strftime('%Y%m%d_%H%M%S') |
|
|
|
uploaded_file = st.file_uploader(file_uploader_label, type=accepted_types) |
|
if uploaded_file: |
|
md_text = uploaded_file.getvalue().decode("utf-8") |
|
stem = Path(uploaded_file.name).stem |
|
else: |
|
md_text = st.text_area(text_area_label, height=200, value="## Your Markdown Here\n\nEnter your markdown text, or upload a file.") |
|
|
|
|
|
renderer = mistune.HTMLRenderer() |
|
markdown = mistune.create_markdown(renderer=renderer) |
|
html = markdown(md_text or "") |
|
plain_text = re.sub(r'<[^>]+>', '', html) |
|
|
|
return plain_text, stem |
|
|
|
|
|
def generate_voice_file(text, lang, is_slow, filename): |
|
""" |
|
Creates an MP3 from text, saves it, and provides it for playback and download. |
|
""" |
|
if not text.strip(): |
|
st.warning("No text to generate voice from.") |
|
return |
|
|
|
voice_file_path = f"{filename}.mp3" |
|
try: |
|
tts = gTTS(text=text, lang=lang, slow=is_slow) |
|
tts.save(voice_file_path) |
|
st.audio(voice_file_path) |
|
with open(voice_file_path, 'rb') as fp: |
|
st.download_button("π₯ Download MP3", data=fp, file_name=voice_file_path, mime="audio/mpeg") |
|
except Exception as e: |
|
st.error(f"Failed to generate audio: {e}") |
|
|
|
|
|
def generate_pdf(text_content, images, pdf_params): |
|
""" |
|
Generates a PDF buffer from text and a list of images based on specified parameters. |
|
""" |
|
buf = io.BytesIO() |
|
|
|
|
|
for font_path in pdf_params.get('ttf_files', []): |
|
try: |
|
font_name = Path(font_path).stem |
|
pdfmetrics.registerFont(TTFont(font_name, font_path)) |
|
except Exception as e: |
|
st.warning(f"Could not register font {font_path}: {e}") |
|
|
|
c = canvas.Canvas(buf) |
|
page_w, page_h = letter |
|
margin = 40 |
|
gutter = 20 |
|
col_w = (page_w - 2 * margin - (pdf_params['columns'] - 1) * gutter) / pdf_params['columns'] |
|
|
|
|
|
font_name_to_use = Path(pdf_params['font_family']).stem if ".ttf" in pdf_params['font_family'] else pdf_params['font_family'] |
|
c.setFont(font_name_to_use, pdf_params['font_size']) |
|
|
|
line_height = pdf_params['font_size'] * 1.2 |
|
|
|
|
|
wrap_width = int(col_w / (pdf_params['font_size'] * 0.6)) if pdf_params['font_size'] > 0 else 50 |
|
|
|
|
|
y = page_h - margin |
|
col_idx = 0 |
|
|
|
|
|
for paragraph in text_content.split("\n"): |
|
wrapped_lines = textwrap.wrap(paragraph, wrap_width) if paragraph.strip() else [""] |
|
for line in wrapped_lines: |
|
if y < margin: |
|
col_idx += 1 |
|
if col_idx >= pdf_params['columns']: |
|
c.showPage() |
|
c.setFont(font_name_to_use, pdf_params['font_size']) |
|
col_idx = 0 |
|
y = page_h - margin |
|
|
|
x = margin + col_idx * (col_w + gutter) |
|
c.drawString(x, y, line) |
|
y -= line_height |
|
y -= line_height |
|
|
|
|
|
for img_file in images: |
|
try: |
|
|
|
img = Image.open(img_file) |
|
w, h = img.size |
|
c.showPage() |
|
c.setPageSize((w, h)) |
|
c.drawImage(ImageReader(img), 0, 0, w, h, preserveAspectRatio=True, mask='auto') |
|
except Exception as e: |
|
img_name = img_file.name if hasattr(img_file, 'name') else img_file |
|
st.warning(f"Could not process image {img_name}: {e}") |
|
continue |
|
|
|
c.save() |
|
buf.seek(0) |
|
return buf |
|
|
|
|
|
def show_asset_manager(): |
|
"""Scans for local files and displays them with management controls.""" |
|
st.markdown("---") |
|
st.subheader("π Available Assets") |
|
assets = sorted(glob.glob("*.*")) |
|
if not assets: |
|
st.info("No assets generated yet.") |
|
return |
|
|
|
for asset_path in assets: |
|
|
|
if asset_path.endswith('.py'): |
|
continue |
|
|
|
ext = Path(asset_path).suffix.lower() |
|
cols = st.columns([3, 1, 1]) |
|
cols[0].write(f"`{asset_path}`") |
|
|
|
try: |
|
with open(asset_path, 'rb') as fp: |
|
file_bytes = fp.read() |
|
|
|
if ext == '.pdf': |
|
cols[1].download_button("π₯", data=file_bytes, file_name=asset_path, mime="application/pdf", key=f"dl_{asset_path}") |
|
elif ext == '.mp3': |
|
cols[1].audio(file_bytes) |
|
elif ext in ['.png', '.jpg', '.jpeg']: |
|
cols[1].image(file_bytes, width=60) |
|
except Exception as e: |
|
cols[1].error("Error") |
|
|
|
cols[2].button("ποΈ", key=f"del_{asset_path}", on_click=delete_asset, args=(asset_path,)) |
|
|
|
|
|
def show_batch_processing_demo(pdf_params): |
|
"""Finds local files and shows how to process them.""" |
|
st.markdown("---") |
|
st.subheader("π§© Batch Processing Demo") |
|
st.info("This section demonstrates how you could call the PDF generation function programmatically with lists of files.") |
|
|
|
md_files = glob.glob("*.md") |
|
img_files = glob.glob("*.png") + glob.glob("*.jpg") |
|
|
|
if not md_files or not img_files: |
|
st.warning("To run the demo, please ensure there is at least one `.md` file and one image (`.png`/`.jpg`) in the directory.") |
|
return |
|
|
|
st.write("Found the following files to use for the demo:") |
|
st.write(f"**Markdown file:** `{md_files[0]}`") |
|
st.write(f"**Image files:** `{', '.join(img_files)}`") |
|
|
|
if st.button("π§ͺ Run Demo with Above Files"): |
|
md_file_str = md_files[0] |
|
img_files_str = ",".join(img_files) |
|
|
|
|
|
|
|
with open(md_file_str, 'r') as f: |
|
text_content = f.read() |
|
|
|
|
|
image_objects = img_files |
|
|
|
|
|
pdf_buffer = generate_pdf(text_content, image_objects, pdf_params) |
|
|
|
|
|
st.download_button( |
|
"β¬οΈ Download Batch Demo PDF", |
|
data=pdf_buffer, |
|
file_name="batch_demo_output.pdf", |
|
mime="application/pdf" |
|
) |
|
st.success("Batch demo PDF generated!") |
|
|
|
|
|
|
|
def render_code_interpreter(): |
|
"""Sets up the UI and execution logic for the code interpreter tab.""" |
|
st.header("π§ͺ Python Code Executor & Demo") |
|
|
|
|
|
def extract_python_code(md_text): |
|
return re.findall(r"```python\s*(.*?)```", md_text, re.DOTALL) |
|
|
|
def execute_code(code_str): |
|
output_buffer = io.StringIO() |
|
try: |
|
|
|
exec_globals = { |
|
"st": st, |
|
"glob": glob, |
|
"base64": base64, |
|
"io": io, |
|
"canvas": canvas, |
|
"letter": letter |
|
} |
|
with redirect_stdout(output_buffer): |
|
exec(code_str, exec_globals) |
|
return output_buffer.getvalue(), None |
|
except Exception as e: |
|
return None, str(e) |
|
|
|
|
|
DEFAULT_CODE = """ |
|
import streamlit as st |
|
import glob |
|
import base64 |
|
import io |
|
from reportlab.pdfgen import canvas |
|
from reportlab.lib.pagesizes import letter |
|
|
|
st.title("π Enhanced Demo App") |
|
st.markdown("This demo shows file galleries and base64 PDF downloads.") |
|
|
|
# --- Image Gallery --- |
|
with st.expander("πΌοΈ Show Image Files in Directory"): |
|
image_files = glob.glob("*.png") + glob.glob("*.jpg") |
|
if not image_files: |
|
st.write("No image files found.") |
|
else: |
|
st.image(image_files) |
|
|
|
# --- PDF Gallery --- |
|
with st.expander("π Show PDF Files in Directory"): |
|
pdf_files = glob.glob("*.pdf") |
|
if not pdf_files: |
|
st.write("No PDF files found.") |
|
else: |
|
st.write(pdf_files) |
|
|
|
# --- PDF Generation and Download --- |
|
if st.button("Generate Demo PDF & Download Link"): |
|
# 1. Create PDF in memory |
|
buffer = io.BytesIO() |
|
p = canvas.Canvas(buffer, pagesize=letter) |
|
p.drawString(100, 750, "This is a demo PDF generated from the code interpreter.") |
|
p.showPage() |
|
p.save() |
|
|
|
# 2. Encode PDF to Base64 |
|
b64 = base64.b64encode(buffer.getvalue()).decode() |
|
|
|
# 3. Create download link |
|
href = f'<a href="data:application/pdf;base64,{b64}" download="demo_from_code.pdf">Download Generated PDF</a>' |
|
st.markdown(href, unsafe_allow_html=True) |
|
st.success("PDF generated! Click the link above to download.") |
|
""" |
|
if 'code' not in st.session_state: |
|
st.session_state.code = DEFAULT_CODE |
|
|
|
uploaded_file = st.file_uploader("Upload .py or .md", type=['py', 'md'], key="code_uploader") |
|
if uploaded_file: |
|
file_content = uploaded_file.getvalue().decode() |
|
if uploaded_file.type == 'text/markdown': |
|
codes = extract_python_code(file_content) |
|
st.session_state.code = codes[0] if codes else '' |
|
else: |
|
st.session_state.code = file_content |
|
|
|
st.session_state.code = st.text_area("π» Code Editor", value=st.session_state.code, height=400) |
|
|
|
c1, c2 = st.columns(2) |
|
if c1.button("βΆοΈ Run Code", use_container_width=True): |
|
output, err = execute_code(st.session_state.code) |
|
st.subheader("Output") |
|
if err: |
|
st.error(err) |
|
if output: |
|
st.code(output, language='text') |
|
if not err: |
|
st.success("Executed successfully.") |
|
|
|
if c2.button("ποΈ Clear Code", use_container_width=True): |
|
st.session_state.code = '' |
|
st.rerun() |
|
|
|
|
|
def main(): |
|
"""Main function to run the Streamlit application.""" |
|
st.set_page_config(page_title="PDF & Code Interpreter", layout="wide", page_icon="π") |
|
|
|
tab1, tab2 = st.tabs(["π PDF Composer", "π§ͺ Code Interpreter"]) |
|
|
|
with tab1: |
|
st.header("π PDF Composer & Voice Generator π") |
|
|
|
|
|
st.sidebar.title("PDF Settings") |
|
|
|
|
|
ttf_files = glob.glob("*.ttf") |
|
standard_fonts = ["Helvetica", "Times-Roman", "Courier"] |
|
available_fonts = ttf_files + standard_fonts |
|
default_font_index = 0 if ttf_files else 0 |
|
|
|
pdf_params = { |
|
'columns': st.sidebar.slider("Text columns", 1, 3, 1), |
|
'font_family': st.sidebar.selectbox("Font", available_fonts, index=default_font_index), |
|
'font_size': st.sidebar.slider("Font size", 6, 48, 12), |
|
'ttf_files': ttf_files |
|
} |
|
|
|
|
|
plain_text, filename_stem = get_text_input( |
|
"Upload Markdown (.md)", ["md"], "Or enter markdown text directly" |
|
) |
|
|
|
st.subheader("π£οΈ Voice Generation") |
|
languages = {"English (US)": "en", "English (UK)": "en-uk", "Spanish": "es"} |
|
voice_choice = st.selectbox("Voice Language", list(languages.keys())) |
|
slow_speech = st.checkbox("Slow Speech") |
|
|
|
if st.button("π Generate Voice MP3"): |
|
generate_voice_file(plain_text, languages[voice_choice], slow_speech, filename_stem) |
|
|
|
st.subheader("πΌοΈ Image Upload") |
|
uploaded_images = st.file_uploader( |
|
"Upload Images for PDF", type=["png", "jpg", "jpeg"], accept_multiple_files=True |
|
) |
|
|
|
ordered_images = [] |
|
if uploaded_images: |
|
df_imgs = pd.DataFrame([{"name": f.name, "order": i} for i, f in enumerate(uploaded_images)]) |
|
edited_df = st.data_editor(df_imgs, use_container_width=True, key="img_order_editor") |
|
|
|
image_map = {f.name: f for f in uploaded_images} |
|
|
|
for _, row in edited_df.sort_values("order").iterrows(): |
|
if row['name'] in image_map: |
|
ordered_images.append(image_map[row['name']]) |
|
|
|
st.subheader("ποΈ PDF Generation") |
|
if st.button("Generate PDF from UI"): |
|
if not plain_text.strip() and not ordered_images: |
|
st.warning("Please provide some text or images to generate a PDF.") |
|
else: |
|
pdf_buffer = generate_pdf(plain_text, ordered_images, pdf_params) |
|
st.download_button( |
|
"β¬οΈ Download PDF", |
|
data=pdf_buffer, |
|
file_name=f"{filename_stem}.pdf", |
|
mime="application/pdf" |
|
) |
|
|
|
show_asset_manager() |
|
show_batch_processing_demo(pdf_params) |
|
|
|
with tab2: |
|
render_code_interpreter() |
|
|
|
if __name__ == "__main__": |
|
main() |
|
|