Sandaru2's picture
Upload 2 files
6369537 verified
import gradio as gr
import pandas as pd
import io
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.base import MIMEBase
from email import encoders
import os
from datetime import datetime
from docx import Document
import tempfile
import subprocess
import platform
import zipfile
import shutil
import threading
import queue
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
# Global variables for email configuration and positioning
email_config = {
'smtp_server': '',
'smtp_port': 587,
'sender_email': '',
'sender_password': '',
'email_subject': 'Your Certificate of Participation',
'email_body': '''Dear {name},
Congratulations on successfully completing the webinar on "Modern Project Management Trends and Global Opportunities for Project Management"!
Please find your certificate of participation attached to this email.
This 2 PDU Programme is accepted by PMI-USA (PDU Code: 25675RL34G).
Best regards,
Pro Consultancy International (Pvt.) Ltd
PMI Authorized Training Partner'''
}
# Text replacement configuration
text_replacement_config = {
'placeholder_text': 'Name' # Text to replace in Word document
}
# Email rate limiting configuration
EMAIL_DELAY = 2 # Seconds between emails to avoid being flagged as spam
MAX_WORKERS = 3 # Maximum parallel threads for certificate generation
def setup_email_server(smtp_server, smtp_port, sender_email, sender_password, email_subject, email_body):
"""Configure email server settings"""
global email_config
email_config = {
'smtp_server': smtp_server,
'smtp_port': int(smtp_port),
'sender_email': sender_email,
'sender_password': sender_password,
'email_subject': email_subject,
'email_body': email_body
}
return "βœ… Email server configured successfully!"
def setup_text_replacement(placeholder_text):
"""Configure text replacement for Word documents"""
global text_replacement_config
text_replacement_config['placeholder_text'] = placeholder_text
return f"βœ… Placeholder text set to: '{placeholder_text}'"
def convert_docx_to_pdf(docx_path):
"""Convert Word document to PDF using LibreOffice or system tools"""
try:
output_pdf = docx_path.replace('.docx', '.pdf')
print(f"πŸ“„ Converting {os.path.basename(docx_path)} to PDF...")
# Try LibreOffice conversion (works on Linux/HuggingFace)
if platform.system() != 'Windows':
try:
print("🐧 Using LibreOffice (Linux)...")
result = subprocess.run([
'libreoffice', '--headless', '--convert-to', 'pdf',
'--outdir', os.path.dirname(docx_path), docx_path
], check=True, capture_output=True, timeout=30)
if os.path.exists(output_pdf):
print(f"βœ… PDF created: {os.path.basename(output_pdf)}")
return output_pdf
else:
print(f"⚠️ LibreOffice: PDF not created, using DOCX")
return docx_path
except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired) as e:
print(f"⚠️ LibreOffice not available or failed: {e}. Using DOCX file.")
return docx_path
else:
# For Windows, try using win32com with proper error handling
try:
print("πŸͺŸ Using win32com (Windows)...")
import win32com.client
import pythoncom
import time
pythoncom.CoInitialize()
word = None
try:
word = win32com.client.DispatchEx('Word.Application')
word.Visible = False
word.DisplayAlerts = 0
# Open document
abs_docx = os.path.abspath(docx_path)
abs_pdf = os.path.abspath(output_pdf)
print(f"Opening: {abs_docx}")
doc = word.Documents.Open(abs_docx)
# Save as PDF (wdFormatPDF = 17)
print(f"Saving as: {abs_pdf}")
doc.SaveAs2(abs_pdf, FileFormat=17)
# Close document
doc.Close(SaveChanges=False)
# Wait a bit for file to be written
time.sleep(0.5)
# Check if PDF was created
if os.path.exists(output_pdf):
print(f"βœ… PDF created: {os.path.basename(output_pdf)}")
return output_pdf
else:
print(f"❌ PDF not created: {output_pdf}")
return docx_path
finally:
if word:
word.Quit()
pythoncom.CoUninitialize()
except ImportError as ie:
print(f"⚠️ win32com not installed: {ie}. Returning DOCX file.")
return docx_path
except Exception as e:
print(f"❌ Win32com PDF conversion error: {e}")
# If conversion fails, return docx path
return docx_path
except Exception as e:
print(f"❌ PDF conversion error: {e}")
return docx_path
def replace_text_in_docx(docx_path, placeholder, replacement):
"""Replace placeholder text in Word document with student name"""
doc = Document(docx_path)
# Replace in paragraphs - handle runs properly
for paragraph in doc.paragraphs:
if placeholder in paragraph.text:
# Get the full text
full_text = paragraph.text
if placeholder in full_text:
# Replace in the full text
new_text = full_text.replace(placeholder, replacement)
# Clear all runs and add new text with first run's formatting
if paragraph.runs:
first_run = paragraph.runs[0]
# Store formatting
font_name = first_run.font.name
font_size = first_run.font.size
font_bold = first_run.font.bold
font_italic = first_run.font.italic
font_color = first_run.font.color.rgb if first_run.font.color.rgb else None
# Clear all runs
for run in paragraph.runs:
run.text = ""
# Set new text in first run
paragraph.runs[0].text = new_text
paragraph.runs[0].font.name = font_name
paragraph.runs[0].font.size = font_size
paragraph.runs[0].font.bold = font_bold
paragraph.runs[0].font.italic = font_italic
if font_color:
paragraph.runs[0].font.color.rgb = font_color
# Replace in tables
for table in doc.tables:
for row in table.rows:
for cell in row.cells:
for paragraph in cell.paragraphs:
if placeholder in paragraph.text:
# Get the full text
full_text = paragraph.text
if placeholder in full_text:
# Replace in the full text
new_text = full_text.replace(placeholder, replacement)
# Clear all runs and add new text with first run's formatting
if paragraph.runs:
first_run = paragraph.runs[0]
# Store formatting
font_name = first_run.font.name
font_size = first_run.font.size
font_bold = first_run.font.bold
font_italic = first_run.font.italic
font_color = first_run.font.color.rgb if first_run.font.color.rgb else None
# Clear all runs
for run in paragraph.runs:
run.text = ""
# Set new text in first run
paragraph.runs[0].text = new_text
paragraph.runs[0].font.name = font_name
paragraph.runs[0].font.size = font_size
paragraph.runs[0].font.bold = font_bold
paragraph.runs[0].font.italic = font_italic
if font_color:
paragraph.runs[0].font.color.rgb = font_color
return doc
def load_template(template_file):
"""Load Word document template"""
if template_file is None:
return None
file_ext = os.path.splitext(template_file)[1].lower()
if file_ext in ['.docx', '.doc']:
return template_file
else:
return None
def generate_certificate(template_path, name):
"""Generate certificate by replacing text in Word document"""
temp_dir = tempfile.mkdtemp()
output_docx = os.path.join(temp_dir, f"Certificate_{name.replace(' ', '_')}.docx")
# Replace text in Word document
doc = replace_text_in_docx(template_path, text_replacement_config['placeholder_text'], name)
doc.save(output_docx)
# Convert to PDF
output_pdf = convert_docx_to_pdf(output_docx)
return output_pdf
def preview_with_test_name(template_file, test_name, placeholder_text="[NAME]"):
"""Preview certificate with test name"""
try:
# Update placeholder text
setup_text_replacement(placeholder_text)
# Load template
template_path = load_template(template_file)
if template_path is None:
return f"❌ Please upload a valid Word document (.docx)"
# Generate preview certificate
preview_cert = generate_certificate(template_path, test_name)
return f"βœ… Certificate will be generated with '{test_name}' replacing '{placeholder_text}'\n\nFile will be saved as PDF when sent to students."
except Exception as e:
return f"❌ Error: {str(e)}"
def send_email_with_certificate(recipient_email, recipient_name, cert_file_path):
"""Send certificate via email as PDF or DOCX"""
try:
print(f"πŸ“§ Preparing email for {recipient_name} ({recipient_email})...")
# Create message
msg = MIMEMultipart()
msg['From'] = email_config['sender_email']
msg['To'] = recipient_email
msg['Subject'] = email_config.get('email_subject', 'Your Certificate of Participation')
# Email body - replace {name} placeholder with actual name
body = email_config.get('email_body', 'Dear {name},\n\nPlease find your certificate attached.').format(name=recipient_name)
msg.attach(MIMEText(body, 'plain'))
# Check if file exists and is a valid PDF or DOCX
if not os.path.exists(cert_file_path):
error_msg = f"Certificate file not found: {cert_file_path}"
print(f"❌ {error_msg}")
return False, error_msg
print(f"πŸ“Ž Attaching: {os.path.basename(cert_file_path)}")
# Determine file type and set appropriate MIME type
file_ext = os.path.splitext(cert_file_path)[1].lower()
if file_ext == '.pdf':
mime_type = 'application/pdf'
filename = f'Certificate_{recipient_name.replace(" ", "_")}.pdf'
elif file_ext == '.docx':
mime_type = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
filename = f'Certificate_{recipient_name.replace(" ", "_")}.docx'
else:
mime_type = 'application/octet-stream'
filename = f'Certificate_{recipient_name.replace(" ", "_")}{file_ext}'
# Attach certificate file
with open(cert_file_path, 'rb') as f:
part = MIMEBase('application', 'octet-stream')
part.set_payload(f.read())
encoders.encode_base64(part)
part.add_header('Content-Disposition', f'attachment; filename="{filename}"')
part.add_header('Content-Type', mime_type)
msg.attach(part)
print(f"πŸš€ Sending email to {recipient_email}...")
# Send email
with smtplib.SMTP(email_config['smtp_server'], email_config['smtp_port']) as server:
server.starttls()
server.login(email_config['sender_email'], email_config['sender_password'])
server.send_message(msg)
print(f"βœ… Email sent successfully to {recipient_name}")
return True, None
except Exception as e:
error_msg = f"Email error: {str(e)}"
print(f"❌ {error_msg}")
return False, error_msg
def send_email_with_retry(recipient_email, recipient_name, cert_file_path, max_retries=3):
"""Send email with retry logic and delay"""
for attempt in range(max_retries):
try:
success, error = send_email_with_certificate(recipient_email, recipient_name, cert_file_path)
if success:
# Add delay to prevent being flagged as spam
time.sleep(EMAIL_DELAY)
return True, None
else:
if attempt < max_retries - 1:
time.sleep(5) # Wait before retry
continue
return False, error
except Exception as e:
if attempt < max_retries - 1:
time.sleep(5)
continue
return False, str(e)
return False, "Max retries exceeded"
def generate_single_certificate(template_path, name, placeholder_text):
"""Generate a single certificate - thread-safe"""
try:
# Update placeholder for this thread
temp_dir = tempfile.mkdtemp()
output_docx = os.path.join(temp_dir, f"Certificate_{name.replace(' ', '_')}.docx")
# Replace text in Word document
doc = replace_text_in_docx(template_path, placeholder_text, name)
doc.save(output_docx)
# Convert to PDF
output_pdf = convert_docx_to_pdf(output_docx)
return output_pdf, None
except Exception as e:
return None, str(e)
def process_certificates(template_file, csv_file, send_emails, placeholder_text, progress=gr.Progress()):
"""Main function to process all certificates with threading"""
try:
print("\n" + "="*60)
print("πŸš€ Starting certificate processing...")
print(f"πŸ“‹ Send emails: {send_emails}")
print(f"πŸ”€ Placeholder text: '{placeholder_text}'")
print("="*60 + "\n")
# Validate email config if sending emails
if send_emails:
if not all([email_config.get('smtp_server'), email_config.get('sender_email'), email_config.get('sender_password')]):
error_msg = "❌ Please configure email server settings first!"
print(error_msg)
return error_msg, None
print(f"πŸ“§ Email configured:")
print(f" Server: {email_config['smtp_server']}:{email_config['smtp_port']}")
print(f" Sender: {email_config['sender_email']}")
print(f" Subject: {email_config.get('email_subject', 'Your Certificate')}")
# Update placeholder text
setup_text_replacement(placeholder_text)
# Load template
template_path = load_template(template_file)
if template_path is None:
error_msg = "❌ Could not load template file. Please upload a valid Word document (.docx)."
print(error_msg)
return error_msg, None
print(f"βœ… Template loaded: {os.path.basename(template_path)}\n")
# Read CSV
df = pd.read_csv(csv_file)
print(f"πŸ“Š CSV loaded: {len(df)} students found")
print(f" Columns: {', '.join(df.columns.tolist())}\n")
# Validate CSV columns
if 'Name' not in df.columns:
error_msg = "❌ CSV must have a 'Name' column!"
print(error_msg)
return error_msg, None
if send_emails and 'Email address' not in df.columns:
error_msg = "❌ CSV must have an 'Email address' column for sending emails!"
print(error_msg)
return error_msg, None
results = []
total = len(df)
successful = 0
failed = 0
generated_files = []
print(f"πŸ“„ Phase 1: Generating {total} certificates in parallel (max {MAX_WORKERS} workers)...\n")
# Phase 1: Generate all certificates in parallel with progress
cert_futures = {}
with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
# Submit all tasks
for idx, row in df.iterrows():
name = row['Name']
email = row.get('Email address', '')
print(f" πŸ“ Queued: {name}")
future = executor.submit(generate_single_certificate, template_path, name, placeholder_text)
cert_futures[future] = (name, email)
print(f"\nβš™οΈ Processing {len(cert_futures)} certificates...\n")
# Process completed futures with progress
completed_count = 0
for future in as_completed(cert_futures):
name, email = cert_futures[future]
completed_count += 1
try:
cert_file, error = future.result()
if cert_file and os.path.exists(cert_file):
file_ext = os.path.splitext(cert_file)[1]
print(f" βœ… [{completed_count}/{total}] Generated {file_ext} for: {name}")
generated_files.append((cert_file, name, email))
if not send_emails:
successful += 1
results.append(f"βœ… {name}")
else:
print(f" ❌ [{completed_count}/{total}] Failed for: {name} - {error if error else 'Unknown error'}")
failed += 1
results.append(f"❌ {name}: {error if error else 'Generation failed'}")
except Exception as e:
print(f" ❌ [{completed_count}/{total}] Exception for: {name} - {str(e)}")
failed += 1
results.append(f"❌ {name}: {str(e)}")
# Update progress
progress(completed_count / total, desc=f"πŸ“„ Generated {completed_count}/{total} certificates")
print(f"\nβœ… Phase 1 complete: {len(generated_files)} certificates generated\n")
# Phase 2: Send emails sequentially with rate limiting and progress
if send_emails and generated_files:
email_successful = 0
email_failed = 0
email_total = len(generated_files)
print(f"πŸ“§ Phase 2: Sending {email_total} emails sequentially...\n")
for idx, (cert_file, name, email) in enumerate(generated_files):
try:
print(f" πŸ“§ [{idx+1}/{email_total}] Sending to: {name} ({email})...")
success, error = send_email_with_retry(email, name, cert_file)
if success:
email_successful += 1
# Update existing result
for i in range(len(results)):
if name in results[i]:
results[i] = f"βœ… {name} ({email})"
break
else:
print(f" ⚠️ Email failed: {error}")
email_failed += 1
# Update existing result
for i in range(len(results)):
if name in results[i]:
results[i] = f"❌ {name} ({email}): {error}"
break
# Update progress (fix division to show correct percentage)
progress((idx + 1) / email_total, desc=f"πŸ“§ Sent {idx + 1}/{email_total} emails")
except Exception as e:
print(f" ⚠️ Email exception: {str(e)}")
email_failed += 1
for i in range(len(results)):
if name in results[i]:
results[i] = f"❌ {name} ({email}): {str(e)}"
break
successful = email_successful
failed = email_failed
print(f"\nβœ… Phase 2 complete: {email_successful} emails sent, {email_failed} failed\n")
# Create ZIP file with all certificates
zip_path = None
all_cert_files = [cert_file for cert_file, _, _ in generated_files] if generated_files else []
if all_cert_files:
try:
print(f"πŸ“¦ Creating ZIP file with {len(all_cert_files)} certificates...")
# Create a temporary directory for the ZIP
zip_dir = tempfile.mkdtemp()
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
zip_filename = f"Certificates_{timestamp}.zip"
zip_path = os.path.join(zip_dir, zip_filename)
# Create ZIP file
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
for cert_file in all_cert_files:
if os.path.exists(cert_file):
# Add file to ZIP with just the filename (not full path)
zipf.write(cert_file, os.path.basename(cert_file))
print(f"βœ… ZIP file created: {zip_filename} ({os.path.getsize(zip_path) / 1024:.1f} KB)\n")
except Exception as e:
print(f"❌ Error creating ZIP: {e}\n")
zip_path = None
# Create summary
print("="*60)
print(f"πŸŽ‰ Processing complete!")
print(f" Total: {total} | Success: {successful} | Failed: {failed}")
print("="*60 + "\n")
summary = f"Processed: {total} | Success: {successful} | Failed: {failed}\n\n"
summary += "\n".join(results)
return summary, zip_path
except Exception as e:
error_msg = f"❌ Error: {str(e)}"
print(error_msg)
import traceback
print(traceback.format_exc())
return error_msg, None
# Create Gradio Interface
with gr.Blocks(title="Certificate Generator & Email Sender", theme=gr.themes.Soft()) as app:
gr.Markdown("""
# πŸ“œ Certificate Generator & Email Sender
Generate personalized certificates and optionally send them via email to participants.
Β© 2025 @sandaruabey2025
""")
with gr.Tab("1️⃣ Email Configuration"):
gr.Markdown("### Configure your email server settings (required only if sending emails)")
with gr.Row():
smtp_server = gr.Textbox(
label="SMTP Server",
placeholder="smtp.gmail.com",
value="smtp.gmail.com"
)
smtp_port = gr.Number(
label="SMTP Port",
value=587
)
with gr.Row():
sender_email = gr.Textbox(
label="Sender Email",
placeholder="your-email@gmail.com"
)
sender_password = gr.Textbox(
label="App Password",
placeholder="Your app-specific password",
type="password"
)
email_subject = gr.Textbox(
label="Email Subject",
value="Your Certificate of Participation",
placeholder="Subject line for certificate emails"
)
email_body = gr.Textbox(
label="Email Body (use {name} for student name)",
value="""Dear {name},
Congratulations on successfully completing the webinar on "Modern Project Management Trends and Global Opportunities for Project Management"!
Please find your certificate of participation attached to this email.
This 2 PDU Programme is accepted by PMI-USA (PDU Code: 25675RL34G).
Best regards,
Pro Consultancy International (Pvt.) Ltd
PMI Authorized Training Partner""",
lines=12,
placeholder="Email body template"
)
config_btn = gr.Button("πŸ’Ύ Save Configuration", variant="primary")
config_status = gr.Textbox(label="Status", interactive=False)
config_btn.click(
fn=setup_email_server,
inputs=[smtp_server, smtp_port, sender_email, sender_password, email_subject, email_body],
outputs=config_status
)
gr.Markdown("""
**Note for Gmail users:**
1. Enable 2-Step Verification in your Google Account
2. Generate an App Password: [Google App Passwords](https://myaccount.google.com/apppasswords)
3. Use the generated 16-character password above
""")
with gr.Tab("2️⃣ Configure Certificate Template"):
gr.Markdown("""
### Configure your Word document certificate template
Upload your Word template and specify the placeholder text to replace
""")
template_preview = gr.File(
label="Upload Certificate Template (Word Document .docx only)",
file_count="single",
type="filepath"
)
with gr.Row():
with gr.Column():
test_name = gr.Textbox(
label="Test Name",
value="John Doe",
placeholder="Enter a test name"
)
placeholder_text_input = gr.Textbox(
label="Placeholder Text in Word Document",
value="Name",
placeholder="e.g., Name, [NAME], Your Name",
info="Exact text in Word document to replace with student name"
)
preview_btn = gr.Button("πŸ” Test Configuration", variant="secondary")
with gr.Column():
position_status = gr.Textbox(label="Status", interactive=False, lines=4)
preview_btn.click(
fn=preview_with_test_name,
inputs=[template_preview, test_name, placeholder_text_input],
outputs=position_status
)
gr.Markdown("""
**How to create your Word certificate:**
1. Design your certificate in Microsoft Word
2. Where you want the student name, type a placeholder like `[NAME]` or `Your Name`
3. Save as .docx format
4. Upload here and specify the exact placeholder text
**Example:** If your Word doc says "This certifies that [NAME] has completed...", use `[NAME]` as placeholder
""")
with gr.Tab("3️⃣ Generate & Send Certificates"):
gr.Markdown("### Upload certificate template and student data")
with gr.Row():
template_input = gr.File(
label="Certificate Template (Word Document .docx)",
file_count="single",
type="filepath"
)
csv_input = gr.File(
label="Student Data (CSV)",
file_count="single",
type="filepath"
)
placeholder_text_process = gr.Textbox(
label="Placeholder Text",
value="Name",
placeholder="e.g., Name, [NAME], Your Name",
info="Exact text in Word document to replace with student name"
)
send_email_checkbox = gr.Checkbox(
label="Send certificates via email (PDF if available, otherwise DOCX)",
value=False
)
gr.Markdown("""
**⚑ Performance Features:**
- βœ… Multi-threaded certificate generation (3 parallel workers)
- βœ… Smart email queue with 2-second delay between sends
- βœ… Automatic retry on email failures (3 attempts)
- βœ… Handles large CSV files efficiently
""")
process_btn = gr.Button("πŸš€ Generate Certificates", variant="primary", size="lg")
with gr.Row():
results_output = gr.Textbox(
label="Processing Results",
lines=20,
max_lines=25
)
download_output = gr.File(
label="πŸ“¦ Download All Certificates (ZIP)",
interactive=False
)
process_btn.click(
fn=process_certificates,
inputs=[template_input, csv_input, send_email_checkbox, placeholder_text_process],
outputs=[results_output, download_output]
)
gr.Markdown("""
**πŸ’‘ Tip:** After generating certificates, you can download all of them as a ZIP file using the download button above.
""")
with gr.Tab("ℹ️ Instructions"):
gr.Markdown("""
## How to Use This Application
### Step 1: Configure Email Settings (Optional)
- Only required if you want to send certificates via email
- For Gmail: Use an App Password, not your regular password
### Step 2: Create Word Certificate Template
1. Design your certificate in Microsoft Word
2. Where you want student name, type placeholder: `[NAME]`
3. Save as .docx format
4. Test configuration in Tab 2
### Step 3: Prepare Your CSV File
Required columns:
- `Name`: Participant's full name
- `Email address`: Participant's email (required only if sending emails)
Example CSV:
```
No,Name,Email address
1,John Doe,john@example.com
2,Jane Smith,jane@example.com
```
### Step 4: Generate & Send Certificates
- Upload Word template and CSV
- Enter placeholder text (e.g., `[NAME]`)
- Check "Send via email" to send as PDF
- Click "Generate Certificates"
### Supported File Formats
- **Certificate Template**: Word (.docx) only
- **Student Data**: CSV
### Tips
- Certificates are sent as PDF files via email
- Test with 1-2 students first
- Placeholder can appear multiple times in document
""")
# Launch the app
if __name__ == "__main__":
app.launch()