Spaces:
Running
Running
import streamlit as st | |
from pptx import Presentation | |
from pptx.util import Inches, Pt | |
from pptx.dml.color import RGBColor | |
from pptx.enum.text import PP_ALIGN, PP_PARAGRAPH_ALIGNMENT | |
import io | |
import tempfile | |
import os | |
import requests | |
import re | |
# Initialize Streamlit | |
st.set_page_config(page_title="PPTX Smart Enhancer", layout="wide") | |
# Configuration | |
GROQ_API_KEY = os.environ.get("GROQ_API_KEY") | |
MODEL_NAME = "gemma2-9b-it" | |
# Style Options | |
FONT_CHOICES = { | |
"Arial": "Arial", | |
"Calibri": "Calibri", | |
"Times New Roman": "Times New Roman", | |
"Helvetica": "Helvetica", | |
"Verdana": "Verdana", | |
"Georgia": "Georgia", | |
"Courier New": "Courier New", | |
"Palatino": "Palatino", | |
"Garamond": "Garamond", | |
"Trebuchet MS": "Trebuchet MS" | |
} | |
COLOR_PALETTES = { | |
"Professional": {"primary": "#2B579A", "secondary": "#5B9BD5", "text": "#000000", "background": "#FFFFFF"}, | |
"Modern": {"primary": "#404040", "secondary": "#A5A5A5", "text": "#FFFFFF", "background": "#121212"}, | |
"Vibrant": {"primary": "#E53935", "secondary": "#FFCDD2", "text": "#212121", "background": "#F5F5F5"}, | |
"Corporate": {"primary": "#1976D2", "secondary": "#BBDEFB", "text": "#212121", "background": "#FAFAFA"}, | |
"Creative": {"primary": "#7B1FA2", "secondary": "#CE93D8", "text": "#FFFFFF", "background": "#212121"}, | |
"Nature": {"primary": "#388E3C", "secondary": "#A5D6A7", "text": "#212121", "background": "#F1F8E9"}, | |
"Elegant": {"primary": "#5D4037", "secondary": "#BCAAA4", "text": "#FFFFFF", "background": "#3E2723"}, | |
"Minimal": {"primary": "#000000", "secondary": "#E0E0E0", "text": "#212121", "background": "#FFFFFF"} | |
} | |
def get_custom_colors(): | |
"""Get custom colors from user input with background option""" | |
col1, col2, col3, col4 = st.columns(4) | |
with col1: | |
primary = st.color_picker("Primary Color", "#2B579A") | |
with col2: | |
secondary = st.color_picker("Secondary Color", "#5B9BD5") | |
with col3: | |
text = st.color_picker("Text Color", "#000000") | |
with col4: | |
background = st.color_picker("Background Color", "#FFFFFF") | |
return { | |
"primary": primary, | |
"secondary": secondary, | |
"text": text, | |
"background": background | |
} | |
def get_ai_response(prompt: str) -> str: | |
"""Get formatted content from Groq API.""" | |
headers = {"Authorization": f"Bearer {GROQ_API_KEY}"} | |
payload = { | |
"messages": [{ | |
"role": "user", | |
"content": f"{prompt}\n\nReturn well-structured content with clear headings, concise bullet points (max 5 per slide), and no markdown. Use bold/italic via formatting, not symbols." | |
}], | |
"model": MODEL_NAME, | |
"temperature": 0.7 | |
} | |
response = requests.post( | |
"https://api.groq.com/openai/v1/chat/completions", | |
headers=headers, | |
json=payload, | |
timeout=30 | |
) | |
return response.json()["choices"][0]["message"]["content"] | |
def extract_slide_text(slide) -> str: | |
"""Extract text from slide.""" | |
return "\n".join( | |
paragraph.text | |
for shape in slide.shapes | |
if hasattr(shape, "text_frame") | |
for paragraph in shape.text_frame.paragraphs | |
) | |
def split_formatted_runs(text): | |
"""Split text into formatted runs removing markdown.""" | |
formatted_segments = [] | |
bold_parts = re.split(r'(\*\*)', text) | |
bold = False | |
current_text = [] | |
for part in bold_parts: | |
if part == '**': | |
if current_text: | |
formatted_segments.append({'text': ''.join(current_text), 'bold': bold, 'italic': False}) | |
current_text = [] | |
bold = not bold | |
else: | |
italic_parts = re.split(r'(\*)', part) | |
italic = False | |
for italic_part in italic_parts: | |
if italic_part == '*': | |
if current_text: | |
formatted_segments.append({'text': ''.join(current_text), 'bold': bold, 'italic': italic}) | |
current_text = [] | |
italic = not italic | |
else: | |
current_text.append(italic_part) | |
if current_text: | |
formatted_segments.append({'text': ''.join(current_text), 'bold': bold, 'italic': italic}) | |
current_text = [] | |
return formatted_segments | |
def apply_formatting(text_frame, content, design_settings): | |
"""Apply proper formatting to text frame with markdown removal.""" | |
text_frame.clear() | |
paragraphs = [p for p in content.split('\n') if p.strip()] | |
for para in paragraphs: | |
clean_para = re.sub(r'^#+\s*', '', para).strip() | |
if re.match(r'^(What is|Understanding|What\'s|Drive Theory|Incentive Theory)', clean_para, re.IGNORECASE): | |
p = text_frame.add_paragraph() | |
p.text = clean_para | |
p.font.size = Pt(design_settings["heading_size"]) | |
p.font.bold = True | |
p.space_after = Pt(12) | |
continue | |
if re.match(r'^[\•\-\*] ', clean_para): | |
p = text_frame.add_paragraph() | |
p.level = 0 | |
content = clean_para[2:].strip() | |
segments = split_formatted_runs(content) | |
for seg in segments: | |
run = p.add_run() | |
run.text = seg['text'] | |
run.font.bold = seg['bold'] | |
run.font.italic = seg['italic'] | |
p.font.size = Pt(design_settings["body_size"]) | |
continue | |
p = text_frame.add_paragraph() | |
segments = split_formatted_runs(clean_para) | |
for seg in segments: | |
run = p.add_run() | |
run.text = seg['text'] | |
run.font.bold = seg['bold'] | |
run.font.italic = seg['italic'] | |
p.font.size = Pt(design_settings["body_size"]) | |
p.space_after = Pt(6) | |
def enhance_slide(slide, user_prompt, design_settings): | |
"""Enhance slide content with proper formatting.""" | |
original_text = extract_slide_text(slide) | |
if not original_text.strip(): | |
return | |
prompt = f"Improve this slide content:\n{original_text}\n\nInstructions: {user_prompt}" | |
enhanced_content = get_ai_response(prompt) | |
title_shape = getattr(slide.shapes, 'title', None) | |
shapes_to_remove = [s for s in slide.shapes if s != title_shape] | |
for shape in shapes_to_remove: | |
sp = shape._element | |
sp.getparent().remove(sp) | |
left = Inches(1) if title_shape else Inches(0.5) | |
top = Inches(1.5) if title_shape else Inches(0.5) | |
textbox = slide.shapes.add_textbox(left, top, Inches(8), Inches(5)) | |
text_frame = textbox.text_frame | |
text_frame.word_wrap = True | |
apply_formatting(text_frame, enhanced_content, design_settings) | |
apply_design(slide, design_settings) | |
def apply_design(slide, design_settings): | |
"""Apply visual design to slide.""" | |
colors = design_settings["colors"] | |
for shape in slide.shapes: | |
if hasattr(shape, "text_frame"): | |
for paragraph in shape.text_frame.paragraphs: | |
for run in paragraph.runs: | |
run.font.name = design_settings["font"] | |
run.font.color.rgb = RGBColor.from_string(colors["text"][1:]) | |
if shape == getattr(slide.shapes, 'title', None): | |
run.font.color.rgb = RGBColor.from_string(colors["primary"][1:]) | |
run.font.size = Pt(design_settings["title_size"]) | |
run.font.bold = True | |
if design_settings["set_background"]: | |
slide.background.fill.solid() | |
slide.background.fill.fore_color.rgb = RGBColor.from_string(colors["background"][1:]) | |
def process_presentation(uploaded_file, user_prompt, design_settings): | |
"""Process and enhance the entire presentation.""" | |
with tempfile.NamedTemporaryFile(delete=False, suffix=".pptx") as tmp_file: | |
tmp_file.write(uploaded_file.read()) | |
tmp_path = tmp_file.name | |
prs = Presentation(tmp_path) | |
with st.spinner("Enhancing slides..."): | |
for slide in prs.slides: | |
enhance_slide(slide, user_prompt, design_settings) | |
os.unlink(tmp_path) | |
return prs | |
def main(): | |
st.title("Professional PPTX Enhancer") | |
uploaded_file = st.file_uploader("Upload PowerPoint", type=["pptx"]) | |
user_prompt = st.text_area( | |
"Enhancement Instructions", | |
placeholder="Example: 'Make headings clear and add bullet points for key items'", | |
height=100 | |
) | |
with st.expander("🎨 Design Options", expanded=True): | |
tab1, tab2 = st.tabs(["Preset Themes", "Custom Colors"]) | |
with tab1: | |
col1, col2 = st.columns(2) | |
with col1: | |
font = st.selectbox("Font", list(FONT_CHOICES.keys()), index=1) | |
color_palette = st.selectbox("Color Scheme", list(COLOR_PALETTES.keys())) | |
with col2: | |
title_size = st.slider("Title Size", 12, 44, 32) | |
heading_size = st.slider("Heading Size", 12, 36, 24) | |
body_size = st.slider("Body Text Size", 8, 28, 18) | |
set_background = st.checkbox("Apply Background", True) | |
with tab2: | |
st.write("Create your own color scheme:") | |
custom_colors = get_custom_colors() | |
use_custom = st.checkbox("Use Custom Colors") | |
design_settings = { | |
"font": FONT_CHOICES[font], | |
"colors": custom_colors if use_custom else COLOR_PALETTES[color_palette], | |
"color_palette": "Custom" if use_custom else color_palette, | |
"title_size": title_size, | |
"heading_size": heading_size, | |
"body_size": body_size, | |
"set_background": set_background | |
} | |
if st.button("Enhance Presentation") and uploaded_file and user_prompt: | |
try: | |
if not GROQ_API_KEY: | |
st.error("GROQ API key not found! Please set it in Space settings.") | |
st.stop() | |
enhanced_prs = process_presentation(uploaded_file, user_prompt, design_settings) | |
buffer = io.BytesIO() | |
enhanced_prs.save(buffer) | |
buffer.seek(0) | |
st.success("Done!") | |
st.download_button( | |
"Download Enhanced PPTX", | |
data=buffer, | |
file_name="enhanced.pptx", | |
mime="application/vnd.openxmlformats-officedocument.presentationml.presentation" | |
) | |
except Exception as e: | |
st.error(f"Error: {str(e)}") | |
if __name__ == "__main__": | |
main() |