Spaces:
Sleeping
Sleeping
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() |