Spaces:
Running
Running
Update pages/1_π_Text_to_PPT.py
Browse files- pages/1_π_Text_to_PPT.py +163 -491
pages/1_π_Text_to_PPT.py
CHANGED
@@ -1010,6 +1010,7 @@
|
|
1010 |
|
1011 |
|
1012 |
###################################################################################################################################################################
|
|
|
1013 |
import streamlit as st
|
1014 |
import google.generativeai as genai
|
1015 |
from pptx import Presentation
|
@@ -1017,16 +1018,11 @@ from pptx.util import Inches, Pt
|
|
1017 |
from pptx.dml.color import RGBColor
|
1018 |
from pptx.enum.text import PP_ALIGN, PP_PARAGRAPH_ALIGNMENT
|
1019 |
from pptx.enum.text import MSO_AUTO_SIZE
|
1020 |
-
from pptx.enum.shapes import MSO_SHAPE
|
1021 |
from pptx.oxml.xmlchemy import OxmlElement
|
1022 |
import io
|
1023 |
import re
|
1024 |
import tempfile
|
1025 |
import os
|
1026 |
-
from PIL import Image
|
1027 |
-
import time
|
1028 |
-
import textwrap
|
1029 |
-
import math
|
1030 |
|
1031 |
# Load the API key securely from environment variable
|
1032 |
api_key = os.getenv("GOOGLE_API_KEY")
|
@@ -1043,8 +1039,7 @@ DEFAULT_THEMES = {
|
|
1043 |
"text_color": RGBColor(200, 200, 200),
|
1044 |
"accent": RGBColor(0, 112, 192),
|
1045 |
"title_font": "Calibri",
|
1046 |
-
"text_font": "Calibri"
|
1047 |
-
"min_font_size": 14
|
1048 |
},
|
1049 |
"Modern Green": {
|
1050 |
"background": RGBColor(22, 82, 66),
|
@@ -1052,8 +1047,7 @@ DEFAULT_THEMES = {
|
|
1052 |
"text_color": RGBColor(220, 220, 220),
|
1053 |
"accent": RGBColor(76, 175, 80),
|
1054 |
"title_font": "Arial",
|
1055 |
-
"text_font": "Arial"
|
1056 |
-
"min_font_size": 14
|
1057 |
},
|
1058 |
"Light Corporate": {
|
1059 |
"background": RGBColor(255, 255, 255),
|
@@ -1061,8 +1055,7 @@ DEFAULT_THEMES = {
|
|
1061 |
"text_color": RGBColor(33, 33, 33),
|
1062 |
"accent": RGBColor(25, 118, 210),
|
1063 |
"title_font": "Segoe UI",
|
1064 |
-
"text_font": "Segoe UI"
|
1065 |
-
"min_font_size": 14
|
1066 |
},
|
1067 |
"Dark Tech": {
|
1068 |
"background": RGBColor(33, 33, 33),
|
@@ -1070,18 +1063,10 @@ DEFAULT_THEMES = {
|
|
1070 |
"text_color": RGBColor(200, 200, 200),
|
1071 |
"accent": RGBColor(0, 150, 255),
|
1072 |
"title_font": "Consolas",
|
1073 |
-
"text_font": "Consolas"
|
1074 |
-
"min_font_size": 14
|
1075 |
}
|
1076 |
}
|
1077 |
|
1078 |
-
# PowerPoint compatibility options
|
1079 |
-
PPT_VERSIONS = {
|
1080 |
-
"Modern (2013+)": {"aspect_ratio": (13.33, 7.5)},
|
1081 |
-
"Standard (2007-2013)": {"aspect_ratio": (10, 7.5)},
|
1082 |
-
"Legacy (2003)": {"aspect_ratio": (10, 7.5)}
|
1083 |
-
}
|
1084 |
-
|
1085 |
def hex_to_rgb(hex_color):
|
1086 |
"""Convert hex color to RGBColor"""
|
1087 |
hex_color = hex_color.lstrip('#')
|
@@ -1101,7 +1086,6 @@ def extract_theme_from_pptx(uploaded_file):
|
|
1101 |
"accent": RGBColor(79, 129, 189), # Default blue
|
1102 |
"title_font": "Calibri",
|
1103 |
"text_font": "Calibri",
|
1104 |
-
"min_font_size": 14,
|
1105 |
"template_path": tmp_file_path # Store the template path for later use
|
1106 |
}
|
1107 |
|
@@ -1142,12 +1126,8 @@ def extract_theme_from_pptx(uploaded_file):
|
|
1142 |
|
1143 |
return theme
|
1144 |
|
1145 |
-
|
1146 |
-
|
1147 |
-
"""Generate slide content with caching"""
|
1148 |
-
if _model is None:
|
1149 |
-
_model = genai.GenerativeModel('gemini-2.0-flash')
|
1150 |
-
|
1151 |
prompt = f"""Create a comprehensive presentation on '{topic}' with exactly {slide_count} slides.
|
1152 |
For each slide, provide:
|
1153 |
1. A clear title in [Title:] format
|
@@ -1218,7 +1198,7 @@ def generate_slide_content(topic, slide_count, _model=None):
|
|
1218 |
Begin the content generation now.
|
1219 |
"""
|
1220 |
|
1221 |
-
response =
|
1222 |
return response.text
|
1223 |
|
1224 |
def parse_slide_content(slide_text):
|
@@ -1315,212 +1295,100 @@ def parse_slide_content(slide_text):
|
|
1315 |
|
1316 |
return slides, questionnaire, answer_key
|
1317 |
|
1318 |
-
def add_title_separator(slide, title_shape, accent_color):
|
1319 |
-
"""Add a decorative line under the title"""
|
1320 |
-
line = slide.shapes.add_shape(
|
1321 |
-
MSO_SHAPE.RECTANGLE,
|
1322 |
-
title_shape.left,
|
1323 |
-
title_shape.top + title_shape.height - Pt(5),
|
1324 |
-
title_shape.width,
|
1325 |
-
Pt(3)
|
1326 |
-
)
|
1327 |
-
fill = line.fill
|
1328 |
-
fill.solid()
|
1329 |
-
fill.fore_color.rgb = accent_color
|
1330 |
-
line.line.fill.background()
|
1331 |
-
|
1332 |
-
def smart_text_wrap(text, max_width=100):
|
1333 |
-
"""Wrap text intelligently at sentence boundaries when possible"""
|
1334 |
-
if len(text) <= max_width:
|
1335 |
-
return [text]
|
1336 |
-
|
1337 |
-
# Try to split at sentence boundaries
|
1338 |
-
sentences = re.split(r'(?<=[.!?]) +', text)
|
1339 |
-
lines = []
|
1340 |
-
current_line = ""
|
1341 |
-
|
1342 |
-
for sentence in sentences:
|
1343 |
-
if len(current_line) + len(sentence) + 1 <= max_width:
|
1344 |
-
current_line += (sentence + " ") if current_line else sentence
|
1345 |
-
else:
|
1346 |
-
if current_line:
|
1347 |
-
lines.append(current_line.strip())
|
1348 |
-
current_line = sentence
|
1349 |
-
|
1350 |
-
if current_line:
|
1351 |
-
lines.append(current_line.strip())
|
1352 |
-
|
1353 |
-
# If we didn't split into sentences, do regular word wrap
|
1354 |
-
if len(lines) == 1 and len(text) > max_width:
|
1355 |
-
return textwrap.wrap(text, max_width)
|
1356 |
-
|
1357 |
-
return lines
|
1358 |
-
|
1359 |
-
def calculate_optimal_font_size(text, max_width, max_height, initial_size=18):
|
1360 |
-
"""Calculate optimal font size to fit text in specified dimensions"""
|
1361 |
-
# Estimate based on character count
|
1362 |
-
char_count = len(text)
|
1363 |
-
if char_count < 100:
|
1364 |
-
return initial_size
|
1365 |
-
elif char_count < 200:
|
1366 |
-
return max(initial_size - 2, 14)
|
1367 |
-
elif char_count < 300:
|
1368 |
-
return max(initial_size - 4, 12)
|
1369 |
-
else:
|
1370 |
-
return max(initial_size - 6, 10)
|
1371 |
-
|
1372 |
def create_question_slide(prs, question, question_num, theme):
|
1373 |
-
"""Create a slide for a single question
|
1374 |
-
|
1375 |
-
try:
|
1376 |
-
slide = prs.slides.add_slide(prs.slide_layouts[1]) # Title + Content layout
|
1377 |
-
except IndexError:
|
1378 |
-
slide = prs.slides.add_slide(prs.slide_layouts[6]) # Blank layout
|
1379 |
-
|
1380 |
-
# Apply background if using custom theme
|
1381 |
-
if "template_path" not in theme:
|
1382 |
-
background = slide.background
|
1383 |
-
fill = background.fill
|
1384 |
-
fill.solid()
|
1385 |
-
fill.fore_color.rgb = theme["background"]
|
1386 |
-
|
1387 |
-
# Remove unused placeholders
|
1388 |
-
for shape in slide.shapes:
|
1389 |
-
if shape.has_text_frame and not shape.text.strip():
|
1390 |
-
sp = shape._element
|
1391 |
-
sp.getparent().remove(sp)
|
1392 |
|
1393 |
# Set title
|
1394 |
-
title = slide.shapes.title
|
1395 |
-
|
1396 |
-
title.text = f"Question {question_num}"
|
1397 |
-
if "template_path" not in theme:
|
1398 |
-
title.text_frame.paragraphs[0].font.color.rgb = theme["accent"]
|
1399 |
-
title.text_frame.paragraphs[0].font.size = Pt(36)
|
1400 |
-
title.text_frame.paragraphs[0].font.bold = True
|
1401 |
-
if "title_font" in theme:
|
1402 |
-
title.text_frame.paragraphs[0].font.name = theme["title_font"]
|
1403 |
-
|
1404 |
-
# Add decorative line under title
|
1405 |
-
add_title_separator(slide, title, theme["accent"])
|
1406 |
-
|
1407 |
-
# Create content area with proper spacing
|
1408 |
-
left = Inches(1)
|
1409 |
-
top = Inches(2) if title else Inches(0.5)
|
1410 |
-
width = prs.slide_width - Inches(2)
|
1411 |
-
height = prs.slide_height - top - Inches(1) # Leave 1 inch at bottom
|
1412 |
-
|
1413 |
-
content_box = slide.shapes.add_textbox(left, top, width, height)
|
1414 |
-
tf = content_box.text_frame
|
1415 |
-
tf.word_wrap = True
|
1416 |
|
1417 |
-
#
|
1418 |
-
|
1419 |
-
|
1420 |
-
|
1421 |
-
|
1422 |
-
|
1423 |
-
|
1424 |
-
|
1425 |
-
|
1426 |
-
|
1427 |
-
|
1428 |
-
|
1429 |
-
|
1430 |
-
|
1431 |
-
|
1432 |
-
|
1433 |
-
|
1434 |
-
|
1435 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1436 |
|
1437 |
-
# Add options
|
1438 |
for j, opt in enumerate(question['options']):
|
1439 |
p = tf.add_paragraph()
|
1440 |
p.text = f"{chr(65+j)}. {opt}"
|
1441 |
p.level = 0
|
1442 |
-
p.space_before = Pt(12)
|
1443 |
p.space_after = Pt(12)
|
1444 |
|
1445 |
-
#
|
1446 |
if "template_path" not in theme:
|
1447 |
p.font.color.rgb = theme["text_color"]
|
1448 |
-
p.font.size = Pt(
|
1449 |
if "text_font" in theme:
|
1450 |
p.font.name = theme["text_font"]
|
1451 |
|
1452 |
# Add correct answer to speaker notes
|
1453 |
-
|
1454 |
-
|
1455 |
-
|
1456 |
-
if hasattr(notes_slide, 'notes_text_frame'):
|
1457 |
-
notes_slide.notes_text_frame.text = notes_text
|
1458 |
|
1459 |
return slide
|
1460 |
|
1461 |
def create_answer_key_slide(prs, questions, theme):
|
1462 |
-
"""Create answer key slide with
|
1463 |
-
|
1464 |
-
try:
|
1465 |
-
slide = prs.slides.add_slide(prs.slide_layouts[1]) # Title + Content layout
|
1466 |
-
except IndexError:
|
1467 |
-
slide = prs.slides.add_slide(prs.slide_layouts[6]) # Blank layout
|
1468 |
-
|
1469 |
-
# Apply background if using custom theme
|
1470 |
-
if "template_path" not in theme:
|
1471 |
-
background = slide.background
|
1472 |
-
fill = background.fill
|
1473 |
-
fill.solid()
|
1474 |
-
fill.fore_color.rgb = theme["background"]
|
1475 |
-
|
1476 |
-
# Remove unused placeholders
|
1477 |
-
for shape in slide.shapes:
|
1478 |
-
if shape.has_text_frame and not shape.text.strip():
|
1479 |
-
sp = shape._element
|
1480 |
-
sp.getparent().remove(sp)
|
1481 |
|
1482 |
# Set title
|
1483 |
-
title = slide.shapes.title
|
1484 |
-
|
1485 |
-
title.text = "Answer Key"
|
1486 |
-
if "template_path" not in theme:
|
1487 |
-
title.text_frame.paragraphs[0].font.color.rgb = theme["accent"]
|
1488 |
-
title.text_frame.paragraphs[0].font.size = Pt(36)
|
1489 |
-
title.text_frame.paragraphs[0].font.bold = True
|
1490 |
-
if "title_font" in theme:
|
1491 |
-
title.text_frame.paragraphs[0].font.name = theme["title_font"]
|
1492 |
-
|
1493 |
-
# Add decorative line under title
|
1494 |
-
add_title_separator(slide, title, theme["accent"])
|
1495 |
|
1496 |
-
#
|
1497 |
-
|
1498 |
-
|
1499 |
-
|
1500 |
-
|
|
|
|
|
1501 |
|
1502 |
-
|
1503 |
-
|
1504 |
-
tf
|
|
|
1505 |
|
1506 |
# Add answer key
|
1507 |
for i, q in enumerate(questions):
|
1508 |
p = tf.add_paragraph()
|
1509 |
p.text = f"Question {i+1}: {q['correct']}"
|
1510 |
p.level = 0
|
1511 |
-
p.space_after = Pt(
|
1512 |
|
1513 |
-
#
|
1514 |
if "template_path" not in theme:
|
1515 |
p.font.color.rgb = theme["text_color"]
|
1516 |
p.font.size = Pt(24)
|
1517 |
-
p.font.bold = True
|
1518 |
if "text_font" in theme:
|
1519 |
p.font.name = theme["text_font"]
|
1520 |
|
1521 |
return slide
|
1522 |
|
1523 |
-
def create_detailed_pptx(slides_data, questions, theme, branding_options=None
|
1524 |
"""Create PowerPoint using the uploaded template"""
|
1525 |
# Use the template if one was uploaded
|
1526 |
if "template_path" in theme and os.path.exists(theme["template_path"]):
|
@@ -1528,10 +1396,9 @@ def create_detailed_pptx(slides_data, questions, theme, branding_options=None, p
|
|
1528 |
else:
|
1529 |
prs = Presentation()
|
1530 |
|
1531 |
-
# Set
|
1532 |
-
|
1533 |
-
prs.
|
1534 |
-
prs.slide_height = Inches(aspect_ratio[1])
|
1535 |
|
1536 |
# Layout mapping
|
1537 |
layout_indices = {
|
@@ -1549,7 +1416,7 @@ def create_detailed_pptx(slides_data, questions, theme, branding_options=None, p
|
|
1549 |
default_layout_idx = available_layouts.get('title-content', 0)
|
1550 |
|
1551 |
# Create main slides
|
1552 |
-
for
|
1553 |
layout = slide_info.get('layout', 'title-content').lower()
|
1554 |
layout_idx = available_layouts.get(layout, default_layout_idx)
|
1555 |
|
@@ -1558,153 +1425,87 @@ def create_detailed_pptx(slides_data, questions, theme, branding_options=None, p
|
|
1558 |
except IndexError:
|
1559 |
slide = prs.slides.add_slide(prs.slide_layouts[default_layout_idx])
|
1560 |
|
1561 |
-
#
|
|
|
|
|
|
|
|
|
|
|
1562 |
if "template_path" not in theme:
|
|
|
1563 |
background = slide.background
|
1564 |
fill = background.fill
|
1565 |
fill.solid()
|
1566 |
fill.fore_color.rgb = theme["background"]
|
1567 |
-
|
1568 |
-
# Remove unused placeholders
|
1569 |
-
for shape in slide.shapes:
|
1570 |
-
if shape.has_text_frame and not shape.text.strip():
|
1571 |
-
sp = shape._element
|
1572 |
-
sp.getparent().remove(sp)
|
1573 |
-
|
1574 |
-
# Try to get the title placeholder
|
1575 |
-
title_placeholder = slide.shapes.title if hasattr(slide.shapes, 'title') else None
|
1576 |
-
|
1577 |
-
# Set title if placeholder exists
|
1578 |
-
if title_placeholder is not None:
|
1579 |
-
title_placeholder.text = slide_info['title']
|
1580 |
-
title_placeholder.text_frame.paragraphs[0].alignment = PP_ALIGN.CENTER
|
1581 |
|
1582 |
-
#
|
1583 |
-
|
1584 |
-
|
1585 |
-
|
1586 |
-
title_placeholder.text_frame.paragraphs[0].font.size = Pt(36)
|
1587 |
-
title_placeholder.text_frame.paragraphs[0].font.bold = True
|
1588 |
-
if "title_font" in theme:
|
1589 |
-
title_placeholder.text_frame.paragraphs[0].font.name = theme["title_font"]
|
1590 |
-
|
1591 |
-
# Add decorative line under title
|
1592 |
-
add_title_separator(slide, title_placeholder, theme["accent"])
|
1593 |
-
else:
|
1594 |
-
# Add title manually if no placeholder
|
1595 |
-
left = Inches(1)
|
1596 |
-
top = Inches(0.5)
|
1597 |
-
width = prs.slide_width - Inches(2)
|
1598 |
-
height = Inches(1)
|
1599 |
-
title_box = slide.shapes.add_textbox(left, top, width, height)
|
1600 |
-
title_tf = title_box.text_frame
|
1601 |
-
p = title_tf.add_paragraph()
|
1602 |
-
p.text = slide_info['title']
|
1603 |
-
p.alignment = PP_ALIGN.CENTER
|
1604 |
-
p.font.bold = True
|
1605 |
-
p.font.size = Pt(36)
|
1606 |
if "title_font" in theme:
|
1607 |
-
|
1608 |
-
|
1609 |
-
|
|
|
|
|
|
|
1610 |
|
1611 |
# Set content based on layout
|
1612 |
-
if
|
1613 |
content = slide_info.get('content', [])
|
1614 |
mid_point = len(content) // 2
|
1615 |
left_content = content[:mid_point]
|
1616 |
right_content = content[mid_point:]
|
1617 |
|
1618 |
-
|
1619 |
-
|
1620 |
-
|
1621 |
-
width = prs.slide_width / 2 - Inches(1)
|
1622 |
-
height = prs.slide_height - top - Inches(1) # Leave space at bottom
|
1623 |
-
left_box = slide.shapes.add_textbox(left, top, width, height)
|
1624 |
-
left_tf = left_box.text_frame
|
1625 |
|
1626 |
-
|
1627 |
-
|
1628 |
-
|
1629 |
-
right_tf = right_box.text_frame
|
1630 |
|
1631 |
for content_part, tf in [(left_content, left_tf), (right_content, right_tf)]:
|
1632 |
for point in content_part:
|
1633 |
-
# Wrap text to prevent overflow
|
1634 |
-
wrapped_text = smart_text_wrap(point, 100)
|
1635 |
-
|
1636 |
-
# Calculate optimal font size
|
1637 |
-
font_size = calculate_optimal_font_size(point, width, height, 18)
|
1638 |
-
|
1639 |
-
for line in wrapped_text:
|
1640 |
-
p = tf.add_paragraph()
|
1641 |
-
p.text = line
|
1642 |
-
p.level = 0
|
1643 |
-
p.alignment = PP_ALIGN.JUSTIFY
|
1644 |
-
p.space_after = Pt(8) # Reduced space between lines
|
1645 |
-
if "template_path" not in theme: # Only apply custom formatting if no template
|
1646 |
-
p.font.color.rgb = theme["text_color"]
|
1647 |
-
p.font.size = Pt(font_size)
|
1648 |
-
if "text_font" in theme:
|
1649 |
-
p.font.name = theme["text_font"]
|
1650 |
-
|
1651 |
-
elif layout != 'title-only': # Content slides
|
1652 |
-
# Create content area
|
1653 |
-
left = Inches(1)
|
1654 |
-
top = Inches(1.8) # Increased top margin
|
1655 |
-
width = prs.slide_width - Inches(2)
|
1656 |
-
height = prs.slide_height - top - Inches(1) # Leave space at bottom
|
1657 |
-
content_box = slide.shapes.add_textbox(left, top, width, height)
|
1658 |
-
tf = content_box.text_frame
|
1659 |
-
|
1660 |
-
for point in slide_info.get('content', []):
|
1661 |
-
# Wrap text to prevent overflow
|
1662 |
-
wrapped_text = smart_text_wrap(point, 100)
|
1663 |
-
|
1664 |
-
# Calculate optimal font size
|
1665 |
-
font_size = calculate_optimal_font_size(point, width, height, 18)
|
1666 |
-
|
1667 |
-
for line in wrapped_text:
|
1668 |
p = tf.add_paragraph()
|
1669 |
-
|
|
|
1670 |
p.level = 0
|
1671 |
-
p.alignment = PP_ALIGN.JUSTIFY
|
1672 |
-
p.space_after = Pt(8) # Reduced space between lines
|
1673 |
if "template_path" not in theme: # Only apply custom formatting if no template
|
1674 |
p.font.color.rgb = theme["text_color"]
|
1675 |
-
p.font.size = Pt(
|
1676 |
if "text_font" in theme:
|
1677 |
p.font.name = theme["text_font"]
|
1678 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1679 |
# Add notes if available
|
1680 |
-
if slide_info.get('notes')
|
1681 |
notes_slide = slide.notes_slide
|
1682 |
-
|
1683 |
-
notes_slide.notes_text_frame.text = slide_info['notes']
|
1684 |
|
1685 |
-
# Add question slides (one per question)
|
1686 |
if questions:
|
1687 |
-
# Add section header
|
1688 |
-
|
1689 |
-
|
1690 |
-
|
1691 |
-
section_slide = prs.slides.add_slide(prs.slide_layouts[0]) # Fallback to title slide
|
1692 |
-
|
1693 |
-
# Apply background if using custom theme
|
1694 |
-
if "template_path" not in theme:
|
1695 |
-
background = section_slide.background
|
1696 |
-
fill = background.fill
|
1697 |
-
fill.solid()
|
1698 |
-
fill.fore_color.rgb = theme["background"]
|
1699 |
-
|
1700 |
-
title_placeholder = section_slide.shapes.title if hasattr(section_slide.shapes, 'title') else None
|
1701 |
-
if title_placeholder is not None:
|
1702 |
-
title_placeholder.text = "Knowledge Check"
|
1703 |
-
|
1704 |
-
# Format section header
|
1705 |
-
if "template_path" not in theme:
|
1706 |
-
title_placeholder.text_frame.paragraphs[0].font.color.rgb = theme["accent"]
|
1707 |
-
title_placeholder.text_frame.paragraphs[0].font.size = Pt(44)
|
1708 |
|
1709 |
# Add each question on a separate slide
|
1710 |
for i, question in enumerate(questions, 1):
|
@@ -1723,53 +1524,6 @@ def create_detailed_pptx(slides_data, questions, theme, branding_options=None, p
|
|
1723 |
|
1724 |
return pptx_io
|
1725 |
|
1726 |
-
def regenerate_slide(slide_index, topic, slide_count, theme, ppt_version):
|
1727 |
-
"""Regenerate a specific slide using AI"""
|
1728 |
-
model = genai.GenerativeModel('gemini-2.0-flash')
|
1729 |
-
prompt = f"""We are creating a presentation on '{topic}' with {slide_count} slides.
|
1730 |
-
Please regenerate only slide number {slide_index+1} with the same structure as the others.
|
1731 |
-
The slide should have:
|
1732 |
-
- A clear title in [Title:] format
|
1733 |
-
- 3-5 detailed bullet points in [Content:] format (each point should be concise, 1-2 lines max)
|
1734 |
-
- Optional speaker notes in [Notes:] format
|
1735 |
-
- Layout suggestion in [Layout:] format
|
1736 |
-
|
1737 |
-
Format:
|
1738 |
-
[Title:] Slide Title
|
1739 |
-
[Layout:] layout-type
|
1740 |
-
[Content:]
|
1741 |
-
- Point 1: Concise point
|
1742 |
-
- Point 2: Another concise point
|
1743 |
-
[Notes:] Optional notes
|
1744 |
-
|
1745 |
-
Make sure the content is concise and fits on a single slide. Avoid long paragraphs.
|
1746 |
-
"""
|
1747 |
-
|
1748 |
-
response = model.generate_content(prompt)
|
1749 |
-
new_slide_text = response.text
|
1750 |
-
|
1751 |
-
# Parse the new slide
|
1752 |
-
new_slide = {'title': '', 'content': [], 'notes': '', 'layout': 'title-content'}
|
1753 |
-
for line in new_slide_text.split('\n'):
|
1754 |
-
line = line.strip()
|
1755 |
-
if line.startswith('[Title:]'):
|
1756 |
-
new_slide['title'] = line.replace('[Title:]', '').strip()
|
1757 |
-
elif line.startswith('[Content:]'):
|
1758 |
-
pass # Content follows on subsequent lines
|
1759 |
-
elif line.startswith('[Notes:]'):
|
1760 |
-
new_slide['notes'] = line.replace('[Notes:]', '').strip()
|
1761 |
-
elif line.startswith('[Layout:]'):
|
1762 |
-
layout = line.replace('[Layout:]', '').strip().lower()
|
1763 |
-
valid_layouts = ['title-only', 'title-content', 'two-column', 'section-header']
|
1764 |
-
new_slide['layout'] = layout if layout in valid_layouts else 'title-content'
|
1765 |
-
elif line.startswith('-'):
|
1766 |
-
point = line[1:].strip()
|
1767 |
-
if ':' in point:
|
1768 |
-
point = point.split(':')[0].strip()
|
1769 |
-
new_slide['content'].append(point)
|
1770 |
-
|
1771 |
-
return new_slide
|
1772 |
-
|
1773 |
def main():
|
1774 |
st.set_page_config(page_title="Advanced PPTX Generator", layout="wide")
|
1775 |
|
@@ -1780,18 +1534,6 @@ def main():
|
|
1780 |
if 'custom_themes' not in st.session_state:
|
1781 |
st.session_state.custom_themes = {}
|
1782 |
|
1783 |
-
# Initialize session state for presentation data
|
1784 |
-
if 'slides_data' not in st.session_state:
|
1785 |
-
st.session_state.slides_data = []
|
1786 |
-
if 'questionnaire' not in st.session_state:
|
1787 |
-
st.session_state.questionnaire = []
|
1788 |
-
if 'answer_key' not in st.session_state:
|
1789 |
-
st.session_state.answer_key = []
|
1790 |
-
if 'pptx_file' not in st.session_state:
|
1791 |
-
st.session_state.pptx_file = None
|
1792 |
-
if 'regenerate_map' not in st.session_state:
|
1793 |
-
st.session_state.regenerate_map = {}
|
1794 |
-
|
1795 |
# Combine default and custom themes
|
1796 |
ALL_THEMES = {**DEFAULT_THEMES, **st.session_state.custom_themes}
|
1797 |
col1, col2 = st.columns([3, 1])
|
@@ -1826,7 +1568,6 @@ def main():
|
|
1826 |
accent_color = st.color_picker("Accent Color", "#0070C0")
|
1827 |
title_font = st.text_input("Title Font", "Calibri")
|
1828 |
text_font = st.text_input("Text Font", "Calibri")
|
1829 |
-
min_font_size = st.slider("Minimum Font Size", 10, 18, 14)
|
1830 |
|
1831 |
if st.form_submit_button("Save Custom Theme"):
|
1832 |
if new_theme_name:
|
@@ -1836,8 +1577,7 @@ def main():
|
|
1836 |
"text_color": hex_to_rgb(text_color),
|
1837 |
"accent": hex_to_rgb(accent_color),
|
1838 |
"title_font": title_font,
|
1839 |
-
"text_font": text_font
|
1840 |
-
"min_font_size": min_font_size
|
1841 |
}
|
1842 |
st.success(f"Theme '{new_theme_name}' saved successfully!")
|
1843 |
else:
|
@@ -1852,14 +1592,6 @@ def main():
|
|
1852 |
st.success("Theme extracted from template!")
|
1853 |
else:
|
1854 |
st.info("Please upload a PowerPoint file to extract its theme")
|
1855 |
-
|
1856 |
-
# PowerPoint version compatibility
|
1857 |
-
ppt_version = st.selectbox(
|
1858 |
-
"PowerPoint Version Compatibility:",
|
1859 |
-
list(PPT_VERSIONS.keys()),
|
1860 |
-
index=0
|
1861 |
-
)
|
1862 |
-
st.caption("Modern (2013+): Widescreen (16:9)\nStandard (2007-2013): Standard (4:3)\nLegacy (2003): Standard (4:3)")
|
1863 |
|
1864 |
with col2:
|
1865 |
st.markdown("### Instructions")
|
@@ -1867,117 +1599,57 @@ def main():
|
|
1867 |
st.markdown("2. Select slide count and theme")
|
1868 |
st.markdown("3. Click 'Generate Presentation'")
|
1869 |
st.markdown("4. Download your PowerPoint file")
|
1870 |
-
st.markdown("5. Use 'Regenerate' buttons to improve specific slides")
|
1871 |
|
1872 |
-
|
1873 |
-
if generate_btn:
|
1874 |
if not topic:
|
1875 |
st.warning("Please enter a topic first!")
|
1876 |
elif theme_option == "Example-Based Theme" and not uploaded_file:
|
1877 |
st.warning("Please upload a PowerPoint template file first")
|
1878 |
else:
|
1879 |
-
with st.spinner("
|
1880 |
-
|
1881 |
-
|
1882 |
-
|
1883 |
-
# Generate slide content
|
1884 |
-
status_text.text("Generating content with AI...")
|
1885 |
-
progress_bar.progress(20)
|
1886 |
-
model = genai.GenerativeModel('gemini-2.0-flash')
|
1887 |
-
slide_text = generate_slide_content(topic, slide_count, _model=model)
|
1888 |
-
|
1889 |
-
# Parse slide content
|
1890 |
-
status_text.text("Parsing slide content...")
|
1891 |
-
progress_bar.progress(40)
|
1892 |
-
slides_data, questionnaire, answer_key = parse_slide_content(slide_text)
|
1893 |
-
|
1894 |
-
# Store in session state
|
1895 |
-
st.session_state.slides_data = slides_data
|
1896 |
-
st.session_state.questionnaire = questionnaire
|
1897 |
-
st.session_state.answer_key = answer_key
|
1898 |
-
st.session_state.regenerate_map = {i: False for i in range(len(slides_data))}
|
1899 |
-
|
1900 |
-
# Create PowerPoint
|
1901 |
-
status_text.text("Creating PowerPoint file...")
|
1902 |
-
progress_bar.progress(70)
|
1903 |
-
pptx_file = create_detailed_pptx(slides_data, questionnaire, theme, ppt_version=ppt_version)
|
1904 |
-
st.session_state.pptx_file = pptx_file
|
1905 |
-
|
1906 |
-
progress_bar.progress(100)
|
1907 |
-
status_text.text("Done!")
|
1908 |
-
time.sleep(1)
|
1909 |
-
progress_bar.empty()
|
1910 |
-
status_text.empty()
|
1911 |
-
|
1912 |
-
st.success("Presentation generated successfully!")
|
1913 |
-
|
1914 |
-
# Show slide overview with regeneration options
|
1915 |
-
if st.session_state.slides_data:
|
1916 |
-
with st.expander("Slide Overview (Click to Expand)", expanded=True):
|
1917 |
-
st.markdown(f"**Total Slides:** {len(st.session_state.slides_data)} content slides + "
|
1918 |
-
f"{len(st.session_state.questionnaire)} question slides + 1 answer key slide")
|
1919 |
-
|
1920 |
-
for i, slide in enumerate(st.session_state.slides_data):
|
1921 |
-
col1, col2 = st.columns([4, 1])
|
1922 |
-
with col1:
|
1923 |
-
st.subheader(f"Slide {i+1}: {slide['title']}")
|
1924 |
-
st.markdown("**Content:**")
|
1925 |
-
for point in slide.get('content', []):
|
1926 |
-
st.markdown(f"- {point}")
|
1927 |
-
if slide.get('notes'):
|
1928 |
-
st.markdown(f"**Notes:** {slide['notes']}")
|
1929 |
-
|
1930 |
-
with col2:
|
1931 |
-
if st.button(f"Regenerate", key=f"reg_{i}"):
|
1932 |
-
with st.spinner(f"Regenerating slide {i+1}..."):
|
1933 |
-
new_slide = regenerate_slide(i, topic, slide_count, theme, ppt_version)
|
1934 |
-
st.session_state.slides_data[i] = new_slide
|
1935 |
-
st.session_state.regenerate_map[i] = True
|
1936 |
-
st.rerun()
|
1937 |
|
1938 |
-
# Show
|
1939 |
-
|
1940 |
-
|
1941 |
-
|
1942 |
-
|
1943 |
-
|
1944 |
-
|
1945 |
-
|
1946 |
-
|
1947 |
-
|
1948 |
-
|
1949 |
-
|
1950 |
-
|
1951 |
-
|
1952 |
-
|
1953 |
-
|
1954 |
-
|
1955 |
-
|
1956 |
-
|
1957 |
-
|
1958 |
-
|
1959 |
-
|
1960 |
-
|
1961 |
-
|
1962 |
-
|
1963 |
-
|
1964 |
-
|
1965 |
-
|
1966 |
-
|
1967 |
-
|
1968 |
-
|
1969 |
-
|
1970 |
-
|
1971 |
-
|
1972 |
-
|
1973 |
-
|
1974 |
-
|
1975 |
-
|
1976 |
-
|
1977 |
-
data=st.session_state.pptx_file,
|
1978 |
-
file_name=f"{topic.replace(' ', '_') if topic else 'presentation'}_presentation.pptx",
|
1979 |
-
mime="application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
1980 |
-
)
|
1981 |
|
1982 |
if __name__ == "__main__":
|
1983 |
main()
|
|
|
1010 |
|
1011 |
|
1012 |
###################################################################################################################################################################
|
1013 |
+
|
1014 |
import streamlit as st
|
1015 |
import google.generativeai as genai
|
1016 |
from pptx import Presentation
|
|
|
1018 |
from pptx.dml.color import RGBColor
|
1019 |
from pptx.enum.text import PP_ALIGN, PP_PARAGRAPH_ALIGNMENT
|
1020 |
from pptx.enum.text import MSO_AUTO_SIZE
|
|
|
1021 |
from pptx.oxml.xmlchemy import OxmlElement
|
1022 |
import io
|
1023 |
import re
|
1024 |
import tempfile
|
1025 |
import os
|
|
|
|
|
|
|
|
|
1026 |
|
1027 |
# Load the API key securely from environment variable
|
1028 |
api_key = os.getenv("GOOGLE_API_KEY")
|
|
|
1039 |
"text_color": RGBColor(200, 200, 200),
|
1040 |
"accent": RGBColor(0, 112, 192),
|
1041 |
"title_font": "Calibri",
|
1042 |
+
"text_font": "Calibri"
|
|
|
1043 |
},
|
1044 |
"Modern Green": {
|
1045 |
"background": RGBColor(22, 82, 66),
|
|
|
1047 |
"text_color": RGBColor(220, 220, 220),
|
1048 |
"accent": RGBColor(76, 175, 80),
|
1049 |
"title_font": "Arial",
|
1050 |
+
"text_font": "Arial"
|
|
|
1051 |
},
|
1052 |
"Light Corporate": {
|
1053 |
"background": RGBColor(255, 255, 255),
|
|
|
1055 |
"text_color": RGBColor(33, 33, 33),
|
1056 |
"accent": RGBColor(25, 118, 210),
|
1057 |
"title_font": "Segoe UI",
|
1058 |
+
"text_font": "Segoe UI"
|
|
|
1059 |
},
|
1060 |
"Dark Tech": {
|
1061 |
"background": RGBColor(33, 33, 33),
|
|
|
1063 |
"text_color": RGBColor(200, 200, 200),
|
1064 |
"accent": RGBColor(0, 150, 255),
|
1065 |
"title_font": "Consolas",
|
1066 |
+
"text_font": "Consolas"
|
|
|
1067 |
}
|
1068 |
}
|
1069 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1070 |
def hex_to_rgb(hex_color):
|
1071 |
"""Convert hex color to RGBColor"""
|
1072 |
hex_color = hex_color.lstrip('#')
|
|
|
1086 |
"accent": RGBColor(79, 129, 189), # Default blue
|
1087 |
"title_font": "Calibri",
|
1088 |
"text_font": "Calibri",
|
|
|
1089 |
"template_path": tmp_file_path # Store the template path for later use
|
1090 |
}
|
1091 |
|
|
|
1126 |
|
1127 |
return theme
|
1128 |
|
1129 |
+
def generate_slide_content(topic, slide_count):
|
1130 |
+
model = genai.GenerativeModel('gemini-2.0-flash')
|
|
|
|
|
|
|
|
|
1131 |
prompt = f"""Create a comprehensive presentation on '{topic}' with exactly {slide_count} slides.
|
1132 |
For each slide, provide:
|
1133 |
1. A clear title in [Title:] format
|
|
|
1198 |
Begin the content generation now.
|
1199 |
"""
|
1200 |
|
1201 |
+
response = model.generate_content(prompt)
|
1202 |
return response.text
|
1203 |
|
1204 |
def parse_slide_content(slide_text):
|
|
|
1295 |
|
1296 |
return slides, questionnaire, answer_key
|
1297 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1298 |
def create_question_slide(prs, question, question_num, theme):
|
1299 |
+
"""Create a slide for a single question"""
|
1300 |
+
slide = prs.slides.add_slide(prs.slide_layouts[1]) # Title + Content layout
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1301 |
|
1302 |
# Set title
|
1303 |
+
title = slide.shapes.title
|
1304 |
+
title.text = f"Question {question_num}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1305 |
|
1306 |
+
# Only apply custom formatting if not using a template
|
1307 |
+
if "template_path" not in theme:
|
1308 |
+
title.text_frame.paragraphs[0].font.color.rgb = theme["accent"]
|
1309 |
+
title.text_frame.paragraphs[0].font.size = Pt(36)
|
1310 |
+
title.text_frame.paragraphs[0].font.bold = True
|
1311 |
+
if "title_font" in theme:
|
1312 |
+
title.text_frame.paragraphs[0].font.name = theme["title_font"]
|
1313 |
+
|
1314 |
+
# Create content
|
1315 |
+
body = slide.placeholders[1]
|
1316 |
+
tf = body.text_frame
|
1317 |
+
tf.clear()
|
1318 |
+
|
1319 |
+
# Add question text
|
1320 |
+
p = tf.add_paragraph()
|
1321 |
+
p.text = question['text']
|
1322 |
+
p.level = 0
|
1323 |
+
p.font.bold = True
|
1324 |
+
p.space_after = Pt(24)
|
1325 |
+
|
1326 |
+
# Only apply custom formatting if not using a template
|
1327 |
+
if "template_path" not in theme:
|
1328 |
+
p.font.color.rgb = theme["text_color"]
|
1329 |
+
p.font.size = Pt(24)
|
1330 |
+
if "text_font" in theme:
|
1331 |
+
p.font.name = theme["text_font"]
|
1332 |
|
1333 |
+
# Add options
|
1334 |
for j, opt in enumerate(question['options']):
|
1335 |
p = tf.add_paragraph()
|
1336 |
p.text = f"{chr(65+j)}. {opt}"
|
1337 |
p.level = 0
|
|
|
1338 |
p.space_after = Pt(12)
|
1339 |
|
1340 |
+
# Only apply custom formatting if not using a template
|
1341 |
if "template_path" not in theme:
|
1342 |
p.font.color.rgb = theme["text_color"]
|
1343 |
+
p.font.size = Pt(20)
|
1344 |
if "text_font" in theme:
|
1345 |
p.font.name = theme["text_font"]
|
1346 |
|
1347 |
# Add correct answer to speaker notes
|
1348 |
+
notes_slide = slide.notes_slide
|
1349 |
+
notes_text = f"Correct Answer: {question['correct']}\n"
|
1350 |
+
notes_slide.notes_text_frame.text = notes_text
|
|
|
|
|
1351 |
|
1352 |
return slide
|
1353 |
|
1354 |
def create_answer_key_slide(prs, questions, theme):
|
1355 |
+
"""Create answer key slide with all correct answers"""
|
1356 |
+
slide = prs.slides.add_slide(prs.slide_layouts[1]) # Title + Content layout
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1357 |
|
1358 |
# Set title
|
1359 |
+
title = slide.shapes.title
|
1360 |
+
title.text = "Answer Key"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1361 |
|
1362 |
+
# Only apply custom formatting if not using a template
|
1363 |
+
if "template_path" not in theme:
|
1364 |
+
title.text_frame.paragraphs[0].font.color.rgb = theme["accent"]
|
1365 |
+
title.text_frame.paragraphs[0].font.size = Pt(36)
|
1366 |
+
title.text_frame.paragraphs[0].font.bold = True
|
1367 |
+
if "title_font" in theme:
|
1368 |
+
title.text_frame.paragraphs[0].font.name = theme["title_font"]
|
1369 |
|
1370 |
+
# Create content
|
1371 |
+
body = slide.placeholders[1]
|
1372 |
+
tf = body.text_frame
|
1373 |
+
tf.clear()
|
1374 |
|
1375 |
# Add answer key
|
1376 |
for i, q in enumerate(questions):
|
1377 |
p = tf.add_paragraph()
|
1378 |
p.text = f"Question {i+1}: {q['correct']}"
|
1379 |
p.level = 0
|
1380 |
+
p.space_after = Pt(12)
|
1381 |
|
1382 |
+
# Only apply custom formatting if not using a template
|
1383 |
if "template_path" not in theme:
|
1384 |
p.font.color.rgb = theme["text_color"]
|
1385 |
p.font.size = Pt(24)
|
|
|
1386 |
if "text_font" in theme:
|
1387 |
p.font.name = theme["text_font"]
|
1388 |
|
1389 |
return slide
|
1390 |
|
1391 |
+
def create_detailed_pptx(slides_data, questions, theme, branding_options=None):
|
1392 |
"""Create PowerPoint using the uploaded template"""
|
1393 |
# Use the template if one was uploaded
|
1394 |
if "template_path" in theme and os.path.exists(theme["template_path"]):
|
|
|
1396 |
else:
|
1397 |
prs = Presentation()
|
1398 |
|
1399 |
+
# Set widescreen layout (16:9 aspect ratio)
|
1400 |
+
prs.slide_width = Inches(13.33)
|
1401 |
+
prs.slide_height = Inches(7.5)
|
|
|
1402 |
|
1403 |
# Layout mapping
|
1404 |
layout_indices = {
|
|
|
1416 |
default_layout_idx = available_layouts.get('title-content', 0)
|
1417 |
|
1418 |
# Create main slides
|
1419 |
+
for slide_info in slides_data:
|
1420 |
layout = slide_info.get('layout', 'title-content').lower()
|
1421 |
layout_idx = available_layouts.get(layout, default_layout_idx)
|
1422 |
|
|
|
1425 |
except IndexError:
|
1426 |
slide = prs.slides.add_slide(prs.slide_layouts[default_layout_idx])
|
1427 |
|
1428 |
+
# Set title
|
1429 |
+
title = slide.shapes.title
|
1430 |
+
title.text = slide_info['title']
|
1431 |
+
title.text_frame.paragraphs[0].alignment = PP_ALIGN.CENTER
|
1432 |
+
|
1433 |
+
# Only apply custom formatting if not using a template
|
1434 |
if "template_path" not in theme:
|
1435 |
+
# Apply background
|
1436 |
background = slide.background
|
1437 |
fill = background.fill
|
1438 |
fill.solid()
|
1439 |
fill.fore_color.rgb = theme["background"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1440 |
|
1441 |
+
# Format title
|
1442 |
+
title.text_frame.paragraphs[0].font.color.rgb = theme["title_color"]
|
1443 |
+
title.text_frame.paragraphs[0].font.size = Pt(36)
|
1444 |
+
title.text_frame.paragraphs[0].font.bold = True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1445 |
if "title_font" in theme:
|
1446 |
+
title.text_frame.paragraphs[0].font.name = theme["title_font"]
|
1447 |
+
|
1448 |
+
# Add logo if provided
|
1449 |
+
if branding_options and branding_options.get('logo_path'):
|
1450 |
+
add_logo_to_slide(slide, branding_options['logo_path'],
|
1451 |
+
branding_options.get('logo_position', 'top-right'))
|
1452 |
|
1453 |
# Set content based on layout
|
1454 |
+
if layout_idx == 3: # Two column layout
|
1455 |
content = slide_info.get('content', [])
|
1456 |
mid_point = len(content) // 2
|
1457 |
left_content = content[:mid_point]
|
1458 |
right_content = content[mid_point:]
|
1459 |
|
1460 |
+
left_body = slide.placeholders[1]
|
1461 |
+
left_tf = left_body.text_frame
|
1462 |
+
left_tf.clear()
|
|
|
|
|
|
|
|
|
1463 |
|
1464 |
+
right_body = slide.placeholders[2]
|
1465 |
+
right_tf = right_body.text_frame
|
1466 |
+
right_tf.clear()
|
|
|
1467 |
|
1468 |
for content_part, tf in [(left_content, left_tf), (right_content, right_tf)]:
|
1469 |
for point in content_part:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1470 |
p = tf.add_paragraph()
|
1471 |
+
point_text = point.replace('- ', '').strip()
|
1472 |
+
p.text = point_text
|
1473 |
p.level = 0
|
1474 |
+
p.alignment = PP_ALIGN.JUSTIFY
|
|
|
1475 |
if "template_path" not in theme: # Only apply custom formatting if no template
|
1476 |
p.font.color.rgb = theme["text_color"]
|
1477 |
+
p.font.size = Pt(18)
|
1478 |
if "text_font" in theme:
|
1479 |
p.font.name = theme["text_font"]
|
1480 |
|
1481 |
+
elif layout_idx != 0: # Not title-only
|
1482 |
+
body = slide.placeholders[1]
|
1483 |
+
tf = body.text_frame
|
1484 |
+
tf.clear()
|
1485 |
+
|
1486 |
+
for point in slide_info.get('content', []):
|
1487 |
+
p = tf.add_paragraph()
|
1488 |
+
point_text = point.replace('- ', '').strip()
|
1489 |
+
p.text = point_text
|
1490 |
+
p.level = 0
|
1491 |
+
p.alignment = PP_ALIGN.JUSTIFY
|
1492 |
+
if "template_path" not in theme: # Only apply custom formatting if no template
|
1493 |
+
p.font.color.rgb = theme["text_color"]
|
1494 |
+
p.font.size = Pt(18)
|
1495 |
+
if "text_font" in theme:
|
1496 |
+
p.font.name = theme["text_font"]
|
1497 |
+
|
1498 |
# Add notes if available
|
1499 |
+
if slide_info.get('notes'):
|
1500 |
notes_slide = slide.notes_slide
|
1501 |
+
notes_slide.notes_text_frame.text = slide_info['notes']
|
|
|
1502 |
|
1503 |
+
# Add question slides (one per question)
|
1504 |
if questions:
|
1505 |
+
# Add section header
|
1506 |
+
section_slide = prs.slides.add_slide(prs.slide_layouts[2]) # Section header layout
|
1507 |
+
section_title = section_slide.shapes.title
|
1508 |
+
section_title.text = "Knowledge Check"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1509 |
|
1510 |
# Add each question on a separate slide
|
1511 |
for i, question in enumerate(questions, 1):
|
|
|
1524 |
|
1525 |
return pptx_io
|
1526 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1527 |
def main():
|
1528 |
st.set_page_config(page_title="Advanced PPTX Generator", layout="wide")
|
1529 |
|
|
|
1534 |
if 'custom_themes' not in st.session_state:
|
1535 |
st.session_state.custom_themes = {}
|
1536 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1537 |
# Combine default and custom themes
|
1538 |
ALL_THEMES = {**DEFAULT_THEMES, **st.session_state.custom_themes}
|
1539 |
col1, col2 = st.columns([3, 1])
|
|
|
1568 |
accent_color = st.color_picker("Accent Color", "#0070C0")
|
1569 |
title_font = st.text_input("Title Font", "Calibri")
|
1570 |
text_font = st.text_input("Text Font", "Calibri")
|
|
|
1571 |
|
1572 |
if st.form_submit_button("Save Custom Theme"):
|
1573 |
if new_theme_name:
|
|
|
1577 |
"text_color": hex_to_rgb(text_color),
|
1578 |
"accent": hex_to_rgb(accent_color),
|
1579 |
"title_font": title_font,
|
1580 |
+
"text_font": text_font
|
|
|
1581 |
}
|
1582 |
st.success(f"Theme '{new_theme_name}' saved successfully!")
|
1583 |
else:
|
|
|
1592 |
st.success("Theme extracted from template!")
|
1593 |
else:
|
1594 |
st.info("Please upload a PowerPoint file to extract its theme")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1595 |
|
1596 |
with col2:
|
1597 |
st.markdown("### Instructions")
|
|
|
1599 |
st.markdown("2. Select slide count and theme")
|
1600 |
st.markdown("3. Click 'Generate Presentation'")
|
1601 |
st.markdown("4. Download your PowerPoint file")
|
|
|
1602 |
|
1603 |
+
if st.button("Generate Presentation", type="primary", key="generate_btn"):
|
|
|
1604 |
if not topic:
|
1605 |
st.warning("Please enter a topic first!")
|
1606 |
elif theme_option == "Example-Based Theme" and not uploaded_file:
|
1607 |
st.warning("Please upload a PowerPoint template file first")
|
1608 |
else:
|
1609 |
+
with st.spinner(f"Creating {slide_count}-slide presentation about '{topic}'..."):
|
1610 |
+
try:
|
1611 |
+
slide_text = generate_slide_content(topic, slide_count)
|
1612 |
+
slides_data, questionnaire, answer_key = parse_slide_content(slide_text)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1613 |
|
1614 |
+
# Show slide overview with detailed content
|
1615 |
+
with st.expander("Slide Overview (Detailed)"):
|
1616 |
+
for i, slide in enumerate(slides_data, 1):
|
1617 |
+
st.subheader(f"Slide {i}: {slide['title']}")
|
1618 |
+
st.markdown("**Content:**")
|
1619 |
+
for point in slide.get('content', []):
|
1620 |
+
st.markdown(f"- {point}")
|
1621 |
+
if slide.get('notes'):
|
1622 |
+
st.markdown(f"**Notes:** {slide['notes']}")
|
1623 |
+
st.markdown("---")
|
1624 |
+
|
1625 |
+
if questionnaire:
|
1626 |
+
st.subheader("Questionnaire")
|
1627 |
+
for i, q in enumerate(questionnaire, 1):
|
1628 |
+
st.subheader(f"Question {i}")
|
1629 |
+
st.markdown(f"**{q['text']}**")
|
1630 |
+
for j, opt in enumerate(q['options']):
|
1631 |
+
st.markdown(f"{chr(65+j)}. {opt}")
|
1632 |
+
st.markdown(f"*Correct: {q['correct']}*")
|
1633 |
+
st.markdown("---")
|
1634 |
+
|
1635 |
+
if answer_key:
|
1636 |
+
st.subheader("Answer Key")
|
1637 |
+
for i, ans in enumerate(answer_key, 1):
|
1638 |
+
st.markdown(f"{i}. {ans}")
|
1639 |
+
|
1640 |
+
pptx_file = create_detailed_pptx(slides_data, questionnaire, theme)
|
1641 |
+
|
1642 |
+
st.success("Presentation generated successfully!")
|
1643 |
+
|
1644 |
+
st.download_button(
|
1645 |
+
label="Download PowerPoint",
|
1646 |
+
data=pptx_file,
|
1647 |
+
file_name=f"{topic.replace(' ', '_')}_presentation.pptx",
|
1648 |
+
mime="application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
1649 |
+
)
|
1650 |
+
|
1651 |
+
except Exception as e:
|
1652 |
+
st.error(f"An error occurred: {str(e)}")
|
|
|
|
|
|
|
|
|
1653 |
|
1654 |
if __name__ == "__main__":
|
1655 |
main()
|