awacke1's picture
Create app.py
7eca515 verified
raw
history blame
11.4 kB
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
# Page config
st.set_page_config(page_title="PDF & Code Interpreter", layout="wide", page_icon="🚀")
def delete_asset(path):
try:
os.remove(path)
except Exception as e:
st.error(f"Error deleting file: {e}")
st.rerun()
# Tabs setup
tab1, tab2 = st.tabs(["📄 PDF Composer", "🧪 Code Interpreter"])
with tab1:
st.header("📄 PDF Composer & Voice Generator 🚀")
# Sidebar PDF text settings
columns = st.sidebar.slider("Text columns", 1, 3, 1)
font_family = st.sidebar.selectbox("Font", ["Helvetica","Times-Roman","Courier"])
font_size = st.sidebar.slider("Font size", 6, 24, 12)
# Markdown input
md_file = st.file_uploader("Upload Markdown (.md)", type=["md"])
if md_file:
md_text = md_file.getvalue().decode("utf-8")
stem = Path(md_file.name).stem
else:
md_text = st.text_area("Or enter markdown text directly", height=200)
stem = datetime.now().strftime('%Y%m%d_%H%M%S')
# Convert Markdown to plain text
# Using mistune to parse markdown to HTML, then stripping HTML tags
renderer = mistune.HTMLRenderer()
markdown = mistune.create_markdown(renderer=renderer)
html = markdown(md_text or "")
plain_text = re.sub(r'<[^>]+>', '', html) # Strip HTML tags
# Voice settings
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 plain_text.strip():
voice_file = f"{stem}.mp3"
try:
tts = gTTS(text=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.")
# Image uploads and ordering
imgs = st.file_uploader("Upload Images for PDF", type=["png", "jpg", "jpeg"], accept_multiple_files=True)
ordered_images = []
if imgs:
# Create a DataFrame for editing image order
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, num_rows="dynamic") # Use num_rows="dynamic" for better UI
# Reconstruct the ordered list of file objects
for _, row in edited.sort_values("order").iterrows():
for f in imgs:
if f.name == row['name']:
ordered_images.append(f)
break # Found the file object, move to the next row
if st.button("🖋️ Generate PDF with Markdown & Images"):
if not plain_text.strip() and not ordered_images:
st.warning("Please provide some text or upload images to generate a PDF.")
else:
buf = io.BytesIO()
c = canvas.Canvas(buf)
# Render text with columns if there is text
if 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
# Estimate wrap width based on font size and column width
# This is an approximation and might need fine-tuning
avg_char_width = font_size * 0.6 # Approximation
wrap_width = int(col_w / avg_char_width) if avg_char_width > 0 else 100 # Prevent division by zero
for paragraph in plain_text.split("\n"):
# Handle empty lines by adding vertical space
if not paragraph.strip():
y -= line_height
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
continue # Skip to next paragraph
for line in textwrap.wrap(paragraph, wrap_width):
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
c.drawString(x, y, line)
y -= line_height
y -= line_height # Add space between paragraphs
# Autosize pages to each image
for img_f in ordered_images:
try:
img = Image.open(img_f)
w, h = img.size
c.showPage() # Start a new page for each image
# Set page size to image size
c.setPageSize((w, h))
# Draw image filling the page
c.drawImage(ImageReader(img), 0, 0, w, h, preserveAspectRatio=False) # Use False to fill page exactly
except Exception as e:
st.warning(f"Could not process image {img_f.name}: {e}")
continue
c.save()
buf.seek(0)
pdf_name = f"{stem}.pdf"
st.download_button("⬇️ Download PDF", data=buf, file_name=pdf_name, mime="application/pdf")
st.markdown("---")
st.subheader("📂 Available Assets")
# Get all files and filter out unwanted ones
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 a in excluded_files)
])
if not assets:
st.info("No available assets found.")
else:
for a in assets:
ext = a.split('.')[-1].lower()
cols = st.columns([3, 1, 1])
cols[0].write(a)
# Provide download/preview based on file type
try:
if ext == 'pdf':
with open(a, 'rb') as fp:
cols[1].download_button("📥", data=fp, file_name=a, mime="application/pdf")
elif ext == 'mp3':
# Streamlit can play audio directly from file path
cols[1].audio(a)
with open(a, 'rb') as mp3:
cols[1].download_button("📥", data=mp3, file_name=a, mime="audio/mpeg")
# Add more file types here if needed (e.g., images)
elif ext in ['png', 'jpg', 'jpeg', 'gif']:
# Can't preview image directly in this column, offer download
with open(a, 'rb') as img_file:
cols[1].download_button("⬇️", data=img_file, file_name=a, mime=f"image/{ext}")
# Handle other file types - maybe just offer download
else:
with open(a, 'rb') as other_file:
cols[1].download_button("⬇️", data=other_file, file_name=a) # Mime type is guessed by streamlit
# Delete button
cols[2].button("🗑️", key=f"del_{a}", on_click=delete_asset, args=(a,))
except Exception as e:
cols[2].error(f"Error handling file {a}: {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)
''' # noqa
def extract_python_code(md: str) -> list:
# Find all blocks starting with ```python and ending with ```
return re.findall(r"```python\s*(.*?)```", md, re.DOTALL)
def execute_code(code: str) -> tuple:
buf = io.StringIO(); local_vars = {}
# Redirect stdout to capture print statements
try:
with redirect_stdout(buf):
# Use exec to run the code. locals() and globals() are needed.
# Passing empty dicts might limit some functionalities but provides isolation.
exec(code, {}, local_vars)
return buf.getvalue(), None # Return captured output
except Exception as e:
return None, str(e) # Return error message
up = st.file_uploader("Upload .py or .md", type=['py', 'md'])
# Initialize session state for code if it doesn't exist
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:
# Take the first python code block found
st.session_state.code = codes[0].strip()
else:
st.warning("No Python code block found in the markdown file.")
st.session_state.code = '' # Clear code if no block found
else: # .py file
st.session_state.code = text.strip()
# Display the code after upload
st.code(st.session_state.code, language='python')
else:
# Text area for code editing if no file is uploaded or after processing upload
st.session_state.code = st.text_area("💻 Code Editor", value=st.session_state.code, height=400) # Increased height
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()