Add Letter Generator Page

This commit is contained in:
BuffTechTalk
2025-03-31 19:53:36 -05:00
parent 3ca20eaaab
commit 6ff2fc1edb
29 changed files with 240 additions and 13 deletions

4
app.py
View File

@@ -17,6 +17,10 @@ if page_label == "Homepage":
elif page_label == "BuffBot":
pg.buffbot()
elif page_label == "Letter Generator":
pg.letter_generator()
elif page_label == "Outstanding Members":
pg.outstanding_members()

BIN
images/02-05-2025-AI101.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

View File

@@ -8,6 +8,7 @@ from .reference import reference
from .pythonx_lessons_pages.pythonx_homepage import pythonx_homepage
from .pythonx_lessons_pages.pythonx_introduction import pythonx_introduction
from .buff_bot import buffbot
from .bufftools_pages.letter_generator import letter_generator
from .pythonx_lessons_pages.pythonx_finance import pythonx_finance
from .pythonx_lessons_pages.pythonx_geomap import pythonx_geomap
from .pythonx_lessons_pages.pythonx_wordcloud import pythonx_wordcloud

View File

@@ -0,0 +1,207 @@
import os
import tempfile
import streamlit as st
from docx import Document
from io import BytesIO
import base64
import re
from zipfile import ZipFile
import subprocess
import shutil
def sanitize_filename(name):
"""Convert name to a safe filename"""
name = re.sub(r'[^\w\s-]', '', str(name)).strip()
return re.sub(r'[-\s]+', '_', name)
def check_pandoc_installed():
try:
subprocess.run(["pandoc", "--version"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
return True
except (subprocess.CalledProcessError, FileNotFoundError):
return False
def check_libreoffice_installed():
try:
subprocess.run(["libreoffice", "--version"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
return True
except (subprocess.CalledProcessError, FileNotFoundError):
return False
def convert_with_libreoffice(docx_path, pdf_path):
"""Convert using LibreOffice (better for complex docs with images)"""
try:
cmd = [
"libreoffice",
"--headless",
"--convert-to", "pdf",
"--outdir", os.path.dirname(pdf_path),
docx_path
]
result = subprocess.run(cmd, check=True)
# LibreOffice names output as input file but with .pdf extension
expected_path = os.path.splitext(docx_path)[0] + ".pdf"
if os.path.exists(expected_path):
if expected_path != pdf_path:
shutil.move(expected_path, pdf_path)
return True
return False
except subprocess.CalledProcessError as e:
st.error(f"LibreOffice conversion failed: {str(e)}")
return False
def convert_to_pdf(docx_path, pdf_path):
"""Try multiple conversion methods to preserve images"""
# First try LibreOffice if available
if check_libreoffice_installed():
if convert_with_libreoffice(docx_path, pdf_path):
return True
# Fallback to Pandoc if LibreOffice fails or isn't available
if check_pandoc_installed():
try:
cmd = [
"pandoc",
docx_path,
"-o", pdf_path,
"--pdf-engine=xelatex",
"--resource-path", os.path.dirname(docx_path),
"--extract-media", os.path.dirname(docx_path)
]
subprocess.run(cmd, check=True)
return True
except subprocess.CalledProcessError as e:
st.error(f"Pandoc conversion failed: {str(e)}")
return False
def letter_generator():
st.title("📄 Generate Letters for Recipient")
st.markdown("""
Generate documents based on recipient names in Word/PDF files.
This is useful for creating personalized letters or certificates based on given templates.
""")
# Check requirements
if not (check_pandoc_installed() or check_libreoffice_installed()):
st.error("""
**Required tools missing**:
- Install [LibreOffice](https://www.libreoffice.org/) for best PDF conversion
- Or install [Pandoc](https://pandoc.org/installing.html) as fallback
""")
st.stop()
st.image("./images/ButtTools-SampleTemplatePlaceHolder.jpg", width=400, caption="Sample Template with Placeholder")
# File upload
st.subheader("1. Upload Template")
template_file = st.file_uploader("Word template (.docx)", type=["docx"])
# Placeholder config
st.subheader("2. Configure Placeholders")
placeholder = st.text_input("Placeholder to replace (e.g., [NAME])", "[NAME]")
# Data input
st.subheader("3. Enter Values (Names)")
names = st.text_area("List values (one per line)", height=150).split('\n')
# Output options
st.subheader("4. Output Format")
output_format = st.radio("", ["Word", "PDF"], index=1)
if st.button("✨ Generate Documents"):
if not template_file:
st.error("Please upload a template file")
return
names = [n.strip() for n in names if n.strip()]
if not names:
st.error("Please enter at least one value")
return
generate_documents(template_file, names, placeholder, output_format)
def generate_documents(template_file, names, placeholder, output_format):
with st.spinner(f"Generating {len(names)} {output_format} files..."):
with tempfile.TemporaryDirectory() as temp_dir:
# Save template
template_path = os.path.join(temp_dir, "template.docx")
with open(template_path, "wb") as f:
f.write(template_file.getbuffer())
results = []
progress_bar = st.progress(0)
for i, name in enumerate(names):
progress = (i + 1) / len(names)
progress_bar.progress(progress)
try:
# Customize document
doc = Document(template_path)
for p in doc.paragraphs:
if placeholder in p.text:
for r in p.runs:
if placeholder in r.text:
r.text = r.text.replace(placeholder, name)
for table in doc.tables:
for row in table.rows:
for cell in row.cells:
if placeholder in cell.text:
for p in cell.paragraphs:
for r in p.runs:
if placeholder in r.text:
r.text = r.text.replace(placeholder, name)
# Save output
safe_name = sanitize_filename(name)
if output_format == "Word":
output_path = os.path.join(temp_dir, f"output_{safe_name}.docx")
doc.save(output_path)
results.append(output_path)
else:
word_path = os.path.join(temp_dir, f"temp_{safe_name}.docx")
pdf_path = os.path.join(temp_dir, f"output_{safe_name}.pdf")
doc.save(word_path)
if convert_to_pdf(word_path, pdf_path):
results.append(pdf_path)
os.unlink(word_path)
except Exception as e:
st.error(f"Error processing {name}: {str(e)}")
progress_bar.empty()
if results:
# Create download package
zip_buffer = BytesIO()
with ZipFile(zip_buffer, "w") as zipf:
for file_path in results:
with open(file_path, "rb") as f:
ext = ".pdf" if output_format == "PDF" else ".docx"
name = os.path.basename(file_path).replace("temp_", "").replace("output_", "")
zipf.writestr(f"document_{name}{ext}", f.read())
# Show results
st.success(f"Generated {len(results)} documents")
st.download_button(
"📥 Download All",
data=zip_buffer.getvalue(),
file_name=f"documents_{output_format.lower()}.zip",
mime="application/zip"
)
# Preview first PDF
if output_format == "PDF" and results:
with open(results[0], "rb") as f:
st.subheader("First Document Preview")
base64_pdf = base64.b64encode(f.read()).decode('utf-8')
st.markdown(
f'<iframe src="data:application/pdf;base64,{base64_pdf}" '
'width="100%" height="600px" style="border:1px solid #eee;"></iframe>',
unsafe_allow_html=True
)

View File

@@ -2,9 +2,9 @@ import streamlit as st
def coreteks_homepage():
# load pythonx logo
st.image("./images/CoreTeksPicture.png")
st.image("./images/CoreTeksPicture.png", use_container_width =True)
st.header(" :clap: Welcome to CoreTeks")
st.subheader(" :clap: Welcome to CoreTeks")
st.markdown(
"""
@@ -18,18 +18,30 @@ def coreteks_homepage():
st.divider()
st.header(" :paperclip: CoreTeks Presentations")
# for streamlit talk
st.subheader(" :one: Introduction to Streamlit")
st.image("./images/11-05-2024-Streamlit.jpg", width= 700)
st.link_button(label="Watch Video Recording", use_container_width=True, type="primary", url="https://wtamu0-my.sharepoint.com/:v:/g/personal/czhang_wtamu_edu/EZBXalcLWaxHquMkZxOWXz8BV2yBo_A1OURZin0ZM0XliQ?nav=eyJyZWZlcnJhbEluZm8iOnsicmVmZXJyYWxBcHAiOiJPbmVEcml2ZUZvckJ1c2luZXNzIiwicmVmZXJyYWxBcHBQbGF0Zm9ybSI6IldlYiIsInJlZmVycmFsTW9kZSI6InZpZXciLCJyZWZlcnJhbFZpZXciOiJNeUZpbGVzTGlua0NvcHkifX0&e=SJD0A4", )
st.divider()
st.subheader(" :paperclip: CoreTeks Presentations")
with st.expander("**:one: 11/05/2024: Introduction to Streamlit**"):
# for streamlit talk
st.image("./images/11-05-2024-Streamlit.jpg", use_container_width =True)
st.link_button(label="Watch Video Recording", use_container_width=True, type="primary", url="https://wtamu0-my.sharepoint.com/:v:/g/personal/czhang_wtamu_edu/EZBXalcLWaxHquMkZxOWXz8BV2yBo_A1OURZin0ZM0XliQ?nav=eyJyZWZlcnJhbEluZm8iOnsicmVmZXJyYWxBcHAiOiJPbmVEcml2ZUZvckJ1c2luZXNzIiwicmVmZXJyYWxBcHBQbGF0Zm9ybSI6IldlYiIsInJlZmVycmFsTW9kZSI6InZpZXciLCJyZWZlcnJhbFZpZXciOiJNeUZpbGVzTGlua0NvcHkifX0&e=SJD0A4", )
st.divider()
with st.expander("**:two: 11/19/2024: Linux Security and Web Server Setup**"):
# for linux talk
st.subheader(" :two: Linux Security and Web Server Setup")
st.image("./images/11-19-2024-CaddyAndLinux.jpg", width= 700)
st.link_button(label="Watch Video Recording", use_container_width=True, type="primary", url="https://wtamu0-my.sharepoint.com/:v:/g/personal/czhang_wtamu_edu/EdgZHrnkqihNrOZGYAT6O2MBSRBOBMv3czD_uIE21KgsWw?nav=eyJyZWZlcnJhbEluZm8iOnsicmVmZXJyYWxBcHAiOiJPbmVEcml2ZUZvckJ1c2luZXNzIiwicmVmZXJyYWxBcHBQbGF0Zm9ybSI6IldlYiIsInJlZmVycmFsTW9kZSI6InZpZXciLCJyZWZlcnJhbFZpZXciOiJNeUZpbGVzTGlua0NvcHkifX0&e=obIoyY", )
st.link_button(label="Download PDF Instruction", use_container_width=True, type="primary", url="https://wtamu0-my.sharepoint.com/:b:/g/personal/czhang_wtamu_edu/Eaygqoc_FqxNgYj3BlEOXhsBzOj-BWTbNKwkpJEYSDBwgA?e=K7qxSU", )
st.divider()
# st.subheader(" :two: Linux Security and Web Server Setup")
st.image("./images/11-19-2024-CaddyAndLinux.jpg", use_container_width =True)
st.link_button(label="Watch Video Recording", use_container_width=True, type="primary", url="https://wtamu0-my.sharepoint.com/:v:/g/personal/czhang_wtamu_edu/EdgZHrnkqihNrOZGYAT6O2MBSRBOBMv3czD_uIE21KgsWw?nav=eyJyZWZlcnJhbEluZm8iOnsicmVmZXJyYWxBcHAiOiJPbmVEcml2ZUZvckJ1c2luZXNzIiwicmVmZXJyYWxBcHBQbGF0Zm9ybSI6IldlYiIsInJlZmVycmFsTW9kZSI6InZpZXciLCJyZWZlcnJhbFZpZXciOiJNeUZpbGVzTGlua0NvcHkifX0&e=obIoyY", )
st.link_button(label="Download PDF Instruction", use_container_width=True, type="primary", url="https://wtamu0-my.sharepoint.com/:b:/g/personal/czhang_wtamu_edu/Eaygqoc_FqxNgYj3BlEOXhsBzOj-BWTbNKwkpJEYSDBwgA?e=K7qxSU", )
st.divider()
with st.expander("**:three: 02/05/2025: AI101: An Introduction to AI in Business**"):
st.image("./images/02-05-2025-AI101.jpg", use_container_width =True)
st.link_button(label="Watch Video Recording", use_container_width=True, type="primary", url="https://discord.com/channels/1015379966780780655/1151982114825318400/1337853863176179843", )
st.link_button(label="Download PDF Instruction", use_container_width=True, type="primary", url="https://discord.com/channels/1015379966780780655/1151982114825318400/1337853863176179843", )
st.divider()
with st.expander("**:four: 04/08/2025: The State of Web Development in 2025**"):
st.image("./images/04-08-2025-ModernWebDevelopment.jpg", use_container_width =True)

View File

@@ -14,6 +14,9 @@ def navigation_bar():
page_label = sac.menu([
sac.MenuItem('Homepage', icon='house'),
sac.MenuItem('BuffBot', icon='robot'),
sac.MenuItem('BuffTools', icon='boxes', children=[
sac.MenuItem('Letter Generator', icon='bi bi-file-word'),
]),
sac.MenuItem('Outstanding Members', icon='award'),
sac.MenuItem("Join Us", icon='person-add'),
sac.MenuItem('BuffTeks Project', icon='bi bi-laptop'),