Spaces:
Sleeping
Sleeping
update formate
Browse files- pages/1_π_Text_to_PPT.py +123 -57
pages/1_π_Text_to_PPT.py
CHANGED
@@ -1025,8 +1025,8 @@ import tempfile
|
|
1025 |
import os
|
1026 |
from PIL import Image
|
1027 |
import time
|
1028 |
-
import
|
1029 |
-
|
1030 |
|
1031 |
# Load the API key securely from environment variable
|
1032 |
api_key = os.getenv("GOOGLE_API_KEY")
|
@@ -1043,7 +1043,8 @@ DEFAULT_THEMES = {
|
|
1043 |
"text_color": RGBColor(200, 200, 200),
|
1044 |
"accent": RGBColor(0, 112, 192),
|
1045 |
"title_font": "Calibri",
|
1046 |
-
"text_font": "Calibri"
|
|
|
1047 |
},
|
1048 |
"Modern Green": {
|
1049 |
"background": RGBColor(22, 82, 66),
|
@@ -1051,7 +1052,8 @@ DEFAULT_THEMES = {
|
|
1051 |
"text_color": RGBColor(220, 220, 220),
|
1052 |
"accent": RGBColor(76, 175, 80),
|
1053 |
"title_font": "Arial",
|
1054 |
-
"text_font": "Arial"
|
|
|
1055 |
},
|
1056 |
"Light Corporate": {
|
1057 |
"background": RGBColor(255, 255, 255),
|
@@ -1059,7 +1061,8 @@ DEFAULT_THEMES = {
|
|
1059 |
"text_color": RGBColor(33, 33, 33),
|
1060 |
"accent": RGBColor(25, 118, 210),
|
1061 |
"title_font": "Segoe UI",
|
1062 |
-
"text_font": "Segoe UI"
|
|
|
1063 |
},
|
1064 |
"Dark Tech": {
|
1065 |
"background": RGBColor(33, 33, 33),
|
@@ -1067,7 +1070,8 @@ DEFAULT_THEMES = {
|
|
1067 |
"text_color": RGBColor(200, 200, 200),
|
1068 |
"accent": RGBColor(0, 150, 255),
|
1069 |
"title_font": "Consolas",
|
1070 |
-
"text_font": "Consolas"
|
|
|
1071 |
}
|
1072 |
}
|
1073 |
|
@@ -1097,6 +1101,7 @@ def extract_theme_from_pptx(uploaded_file):
|
|
1097 |
"accent": RGBColor(79, 129, 189), # Default blue
|
1098 |
"title_font": "Calibri",
|
1099 |
"text_font": "Calibri",
|
|
|
1100 |
"template_path": tmp_file_path # Store the template path for later use
|
1101 |
}
|
1102 |
|
@@ -1324,20 +1329,54 @@ def add_title_separator(slide, title_shape, accent_color):
|
|
1324 |
fill.fore_color.rgb = accent_color
|
1325 |
line.line.fill.background()
|
1326 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1327 |
def create_question_slide(prs, question, question_num, theme):
|
1328 |
-
"""Create a slide for a single question with proper
|
1329 |
# Try to use Title + Content layout, fallback to blank if not available
|
1330 |
try:
|
1331 |
slide = prs.slides.add_slide(prs.slide_layouts[1]) # Title + Content layout
|
1332 |
except IndexError:
|
1333 |
slide = prs.slides.add_slide(prs.slide_layouts[6]) # Blank layout
|
1334 |
|
1335 |
-
# Remove unused placeholders
|
1336 |
-
for shape in slide.shapes:
|
1337 |
-
if shape.has_text_frame and not shape.text.strip():
|
1338 |
-
sp = shape._element
|
1339 |
-
sp.getparent().remove(sp)
|
1340 |
-
|
1341 |
# Apply background if using custom theme
|
1342 |
if "template_path" not in theme:
|
1343 |
background = slide.background
|
@@ -1345,6 +1384,12 @@ def create_question_slide(prs, question, question_num, theme):
|
|
1345 |
fill.solid()
|
1346 |
fill.fore_color.rgb = theme["background"]
|
1347 |
|
|
|
|
|
|
|
|
|
|
|
|
|
1348 |
# Set title
|
1349 |
title = slide.shapes.title if hasattr(slide.shapes, 'title') else None
|
1350 |
if title:
|
@@ -1369,18 +1414,25 @@ def create_question_slide(prs, question, question_num, theme):
|
|
1369 |
tf = content_box.text_frame
|
1370 |
tf.word_wrap = True
|
1371 |
|
1372 |
-
# Add question text
|
1373 |
-
|
1374 |
-
|
1375 |
-
p.font.bold = True
|
1376 |
-
p.space_after = Pt(24)
|
1377 |
|
1378 |
-
#
|
1379 |
-
|
1380 |
-
|
1381 |
-
|
1382 |
-
|
1383 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1384 |
|
1385 |
# Add options with proper spacing
|
1386 |
for j, opt in enumerate(question['options']):
|
@@ -1393,7 +1445,7 @@ def create_question_slide(prs, question, question_num, theme):
|
|
1393 |
# Apply text formatting
|
1394 |
if "template_path" not in theme:
|
1395 |
p.font.color.rgb = theme["text_color"]
|
1396 |
-
p.font.size = Pt(
|
1397 |
if "text_font" in theme:
|
1398 |
p.font.name = theme["text_font"]
|
1399 |
|
@@ -1407,7 +1459,7 @@ def create_question_slide(prs, question, question_num, theme):
|
|
1407 |
return slide
|
1408 |
|
1409 |
def create_answer_key_slide(prs, questions, theme):
|
1410 |
-
"""Create answer key slide with proper
|
1411 |
# Try to use Title + Content layout, fallback to blank if not available
|
1412 |
try:
|
1413 |
slide = prs.slides.add_slide(prs.slide_layouts[1]) # Title + Content layout
|
@@ -1565,9 +1617,9 @@ def create_detailed_pptx(slides_data, questions, theme, branding_options=None, p
|
|
1565 |
|
1566 |
# Create left column
|
1567 |
left = Inches(0.5)
|
1568 |
-
top = Inches(1.
|
1569 |
width = prs.slide_width / 2 - Inches(1)
|
1570 |
-
height = prs.slide_height - Inches(
|
1571 |
left_box = slide.shapes.add_textbox(left, top, width, height)
|
1572 |
left_tf = left_box.text_frame
|
1573 |
|
@@ -1578,39 +1630,51 @@ def create_detailed_pptx(slides_data, questions, theme, branding_options=None, p
|
|
1578 |
|
1579 |
for content_part, tf in [(left_content, left_tf), (right_content, right_tf)]:
|
1580 |
for point in content_part:
|
1581 |
-
|
1582 |
-
|
1583 |
-
|
1584 |
-
|
1585 |
-
|
1586 |
-
|
1587 |
-
|
1588 |
-
p
|
1589 |
-
p.
|
1590 |
-
|
1591 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
1592 |
|
1593 |
elif layout != 'title-only': # Content slides
|
1594 |
# Create content area
|
1595 |
left = Inches(1)
|
1596 |
-
top = Inches(1.
|
1597 |
width = prs.slide_width - Inches(2)
|
1598 |
-
height = prs.slide_height - Inches(
|
1599 |
content_box = slide.shapes.add_textbox(left, top, width, height)
|
1600 |
tf = content_box.text_frame
|
1601 |
|
1602 |
for point in slide_info.get('content', []):
|
1603 |
-
|
1604 |
-
|
1605 |
-
|
1606 |
-
|
1607 |
-
|
1608 |
-
|
1609 |
-
|
1610 |
-
p
|
1611 |
-
p.
|
1612 |
-
|
1613 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
1614 |
|
1615 |
# Add notes if available
|
1616 |
if slide_info.get('notes') and hasattr(slide, 'notes_slide'):
|
@@ -1666,7 +1730,7 @@ def regenerate_slide(slide_index, topic, slide_count, theme, ppt_version):
|
|
1666 |
Please regenerate only slide number {slide_index+1} with the same structure as the others.
|
1667 |
The slide should have:
|
1668 |
- A clear title in [Title:] format
|
1669 |
-
- 3-5 detailed bullet points in [Content:] format
|
1670 |
- Optional speaker notes in [Notes:] format
|
1671 |
- Layout suggestion in [Layout:] format
|
1672 |
|
@@ -1674,11 +1738,11 @@ def regenerate_slide(slide_index, topic, slide_count, theme, ppt_version):
|
|
1674 |
[Title:] Slide Title
|
1675 |
[Layout:] layout-type
|
1676 |
[Content:]
|
1677 |
-
- Point 1
|
1678 |
-
- Point 2
|
1679 |
[Notes:] Optional notes
|
1680 |
|
1681 |
-
Make sure the content is
|
1682 |
"""
|
1683 |
|
1684 |
response = model.generate_content(prompt)
|
@@ -1762,6 +1826,7 @@ def main():
|
|
1762 |
accent_color = st.color_picker("Accent Color", "#0070C0")
|
1763 |
title_font = st.text_input("Title Font", "Calibri")
|
1764 |
text_font = st.text_input("Text Font", "Calibri")
|
|
|
1765 |
|
1766 |
if st.form_submit_button("Save Custom Theme"):
|
1767 |
if new_theme_name:
|
@@ -1771,7 +1836,8 @@ def main():
|
|
1771 |
"text_color": hex_to_rgb(text_color),
|
1772 |
"accent": hex_to_rgb(accent_color),
|
1773 |
"title_font": title_font,
|
1774 |
-
"text_font": text_font
|
|
|
1775 |
}
|
1776 |
st.success(f"Theme '{new_theme_name}' saved successfully!")
|
1777 |
else:
|
|
|
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 |
"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 |
"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 |
"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 |
"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 |
|
|
|
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 |
|
|
|
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 with proper text wrapping"""
|
1374 |
# Try to use Title + Content layout, fallback to blank if not available
|
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
|
|
|
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 if hasattr(slide.shapes, 'title') else None
|
1395 |
if title:
|
|
|
1414 |
tf = content_box.text_frame
|
1415 |
tf.word_wrap = True
|
1416 |
|
1417 |
+
# Add question text with smart wrapping
|
1418 |
+
question_text = question['text']
|
1419 |
+
wrapped_question = smart_text_wrap(question_text, 100)
|
|
|
|
|
1420 |
|
1421 |
+
# Calculate optimal font size
|
1422 |
+
font_size = calculate_optimal_font_size(question_text, width, height, 24)
|
1423 |
+
|
1424 |
+
for line in wrapped_question:
|
1425 |
+
p = tf.add_paragraph()
|
1426 |
+
p.text = line
|
1427 |
+
p.font.bold = True
|
1428 |
+
p.space_after = Pt(12)
|
1429 |
+
|
1430 |
+
# Apply text formatting
|
1431 |
+
if "template_path" not in theme:
|
1432 |
+
p.font.color.rgb = theme["text_color"]
|
1433 |
+
p.font.size = Pt(font_size)
|
1434 |
+
if "text_font" in theme:
|
1435 |
+
p.font.name = theme["text_font"]
|
1436 |
|
1437 |
# Add options with proper spacing
|
1438 |
for j, opt in enumerate(question['options']):
|
|
|
1445 |
# Apply text formatting
|
1446 |
if "template_path" not in theme:
|
1447 |
p.font.color.rgb = theme["text_color"]
|
1448 |
+
p.font.size = Pt(font_size - 4) # Slightly smaller for options
|
1449 |
if "text_font" in theme:
|
1450 |
p.font.name = theme["text_font"]
|
1451 |
|
|
|
1459 |
return slide
|
1460 |
|
1461 |
def create_answer_key_slide(prs, questions, theme):
|
1462 |
+
"""Create answer key slide with proper text wrapping"""
|
1463 |
# Try to use Title + Content layout, fallback to blank if not available
|
1464 |
try:
|
1465 |
slide = prs.slides.add_slide(prs.slide_layouts[1]) # Title + Content layout
|
|
|
1617 |
|
1618 |
# Create left column
|
1619 |
left = Inches(0.5)
|
1620 |
+
top = Inches(1.8) # Increased top margin
|
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 |
|
|
|
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 |
+
p.text = line
|
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(font_size)
|
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') and hasattr(slide, 'notes_slide'):
|
|
|
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 |
|
|
|
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)
|
|
|
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 |
"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:
|