Spaces:
Running
Running
Update app-BACKUP.py
Browse files- app-BACKUP.py +394 -188
app-BACKUP.py
CHANGED
@@ -10,6 +10,7 @@ import tempfile
|
|
10 |
import random
|
11 |
from typing import Dict, List, Tuple, Optional
|
12 |
import shutil
|
|
|
13 |
|
14 |
import gradio as gr
|
15 |
from loguru import logger
|
@@ -48,6 +49,28 @@ SERPHOUSE_API_KEY = os.getenv("SERPHOUSE_API_KEY", "")
|
|
48 |
if not SERPHOUSE_API_KEY:
|
49 |
logger.warning("SERPHOUSE_API_KEY not set. Web search functionality will be limited.")
|
50 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
51 |
##############################################################################
|
52 |
# AI Image Generation API Configuration - FLUX API
|
53 |
##############################################################################
|
@@ -863,89 +886,169 @@ def pdf_to_markdown(pdf_path: str) -> str:
|
|
863 |
return f"**[PDF File: {os.path.basename(pdf_path)}]**\n\n{full_text}"
|
864 |
|
865 |
##############################################################################
|
866 |
-
# AI Image Generation Functions using
|
867 |
##############################################################################
|
868 |
-
def
|
869 |
-
"""
|
870 |
|
871 |
# ์ฃผ์ ํค์๋ ์ถ์ถ
|
872 |
-
keywords = extract_keywords(
|
873 |
|
874 |
-
#
|
875 |
-
|
876 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
877 |
|
878 |
-
|
879 |
-
|
880 |
-
|
881 |
-
|
882 |
-
|
883 |
-
|
884 |
-
|
885 |
-
|
886 |
-
|
887 |
-
|
888 |
-
elif any(word in topic_lower for word in ['๊ธ์ต', 'finance', 'ํฌ์', 'investment']):
|
889 |
-
style = "financial professional"
|
890 |
|
891 |
-
#
|
892 |
-
|
|
|
893 |
|
894 |
-
|
895 |
-
|
896 |
-
|
897 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
898 |
|
899 |
-
#
|
900 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
901 |
|
902 |
-
|
903 |
-
conclusion_keywords = ['๊ฒฐ๋ก ', 'conclusion', '์์ฝ', 'summary', 'ํต์ฌ', 'key',
|
904 |
-
'์ค์', 'important', '๋ฏธ๋', 'future', '์ ๋ง', 'outlook']
|
905 |
|
906 |
-
#
|
907 |
-
|
908 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
909 |
else:
|
910 |
-
|
|
|
911 |
|
912 |
-
|
913 |
-
|
914 |
|
915 |
-
return
|
916 |
|
917 |
-
def
|
918 |
-
"""
|
919 |
|
920 |
-
# ์ฃผ์ ํค์๋ ์ถ์ถ
|
921 |
keywords = extract_keywords(f"{title} {content}", top_k=4).split()
|
922 |
|
923 |
-
#
|
924 |
-
|
925 |
-
|
926 |
-
style = "professional business"
|
927 |
-
if any(word in content_lower for word in ['data', '๋ฐ์ดํฐ', 'analysis', '๋ถ์', 'chart', '์ฐจํธ']):
|
928 |
-
style = "data visualization analytics"
|
929 |
-
elif any(word in content_lower for word in ['process', 'ํ๋ก์ธ์ค', 'workflow', '์ํฌํ๋ก์ฐ']):
|
930 |
-
style = "workflow process diagram"
|
931 |
-
elif any(word in content_lower for word in ['team', 'ํ', 'collaboration', 'ํ์
']):
|
932 |
-
style = "teamwork collaboration"
|
933 |
-
elif any(word in content_lower for word in ['growth', '์ฑ์ฅ', 'increase', '์ฆ๊ฐ']):
|
934 |
-
style = "growth success metrics"
|
935 |
|
936 |
-
|
937 |
-
|
|
|
|
|
|
|
|
|
938 |
|
939 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
940 |
|
941 |
def generate_flux_image_via_api(prompt: str) -> Optional[str]:
|
942 |
-
"""FLUX API๋ฅผ ํตํด ์ด๋ฏธ์ง ์์ฑ"""
|
943 |
if not FLUX_API_ENABLED or not flux_api_client:
|
944 |
-
logger.warning("FLUX API is not available")
|
945 |
return None
|
946 |
|
947 |
try:
|
948 |
-
logger.info(f"Generating FLUX image with prompt: {prompt[:100]}...")
|
949 |
|
950 |
result = flux_api_client.predict(
|
951 |
prompt=prompt,
|
@@ -964,7 +1067,6 @@ def generate_flux_image_via_api(prompt: str) -> Optional[str]:
|
|
964 |
if isinstance(result, tuple) and len(result) > 0:
|
965 |
image_path = result[0]
|
966 |
if image_path and os.path.exists(image_path):
|
967 |
-
# PNG๋ก ๋ณํ
|
968 |
with Image.open(image_path) as img:
|
969 |
png_tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".png")
|
970 |
img.save(png_tmp.name, format="PNG")
|
@@ -977,22 +1079,28 @@ def generate_flux_image_via_api(prompt: str) -> Optional[str]:
|
|
977 |
logger.error(f"Failed to generate FLUX image: {e}")
|
978 |
return None
|
979 |
|
980 |
-
def
|
981 |
-
"""
|
982 |
-
|
983 |
-
|
984 |
-
|
985 |
-
|
986 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
987 |
|
988 |
##############################################################################
|
989 |
# PPT Generation Functions - FIXED VERSION
|
990 |
##############################################################################
|
991 |
def parse_llm_ppt_response(response: str, layout_style: str = "consistent") -> list:
|
992 |
-
"""Parse LLM response to extract slide content -
|
993 |
slides = []
|
994 |
|
995 |
-
# Debug: ์ ์ฒด ์๋ต ํ์ธ
|
996 |
logger.info(f"Parsing LLM response, total length: {len(response)}")
|
997 |
logger.debug(f"First 500 chars: {response[:500]}")
|
998 |
|
@@ -1005,24 +1113,45 @@ def parse_llm_ppt_response(response: str, layout_style: str = "consistent") -> l
|
|
1005 |
except:
|
1006 |
pass
|
1007 |
|
1008 |
-
#
|
1009 |
-
#
|
1010 |
-
|
1011 |
-
|
1012 |
-
# ์ฌ๋ผ์ด๋
|
1013 |
-
|
1014 |
-
|
1015 |
-
|
1016 |
-
|
1017 |
-
|
1018 |
-
|
1019 |
-
|
1020 |
-
|
1021 |
-
|
1022 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1023 |
continue
|
1024 |
|
1025 |
-
logger.debug(f"Processing
|
1026 |
|
1027 |
slide = {
|
1028 |
'title': '',
|
@@ -1033,9 +1162,9 @@ def parse_llm_ppt_response(response: str, layout_style: str = "consistent") -> l
|
|
1033 |
}
|
1034 |
|
1035 |
# ์น์
๋ด์์ ์ ๋ชฉ, ๋ด์ฉ, ๋
ธํธ ์ถ์ถ
|
1036 |
-
lines = section.
|
1037 |
current_part = None
|
1038 |
-
|
1039 |
content_lines = []
|
1040 |
notes_lines = []
|
1041 |
|
@@ -1045,11 +1174,11 @@ def parse_llm_ppt_response(response: str, layout_style: str = "consistent") -> l
|
|
1045 |
continue
|
1046 |
|
1047 |
# ์ ๋ชฉ ์น์
๊ฐ์ง
|
1048 |
-
if line.startswith('์ ๋ชฉ:') or line.startswith('Title:'):
|
1049 |
current_part = 'title'
|
1050 |
title_text = line.split(':', 1)[1].strip() if ':' in line else ''
|
1051 |
-
|
1052 |
-
|
1053 |
# ๋ด์ฉ ์น์
๊ฐ์ง
|
1054 |
elif line.startswith('๋ด์ฉ:') or line.startswith('Content:'):
|
1055 |
current_part = 'content'
|
@@ -1064,57 +1193,91 @@ def parse_llm_ppt_response(response: str, layout_style: str = "consistent") -> l
|
|
1064 |
notes_lines.append(notes_text)
|
1065 |
# ํ์ฌ ์น์
์ ๋ฐ๋ผ ๋ด์ฉ ์ถ๊ฐ
|
1066 |
else:
|
1067 |
-
if current_part == 'title' and not
|
1068 |
-
|
1069 |
elif current_part == 'content':
|
1070 |
content_lines.append(line)
|
1071 |
elif current_part == 'notes':
|
1072 |
notes_lines.append(line)
|
1073 |
-
elif not
|
1074 |
# ์ฒซ ๋ฒ์งธ ์ค์ ์ ๋ชฉ์ผ๋ก
|
1075 |
-
|
1076 |
-
|
1077 |
-
|
|
|
|
|
1078 |
content_lines.append(line)
|
1079 |
|
1080 |
# ์ฌ๋ผ์ด๋ ๋ฐ์ดํฐ ์ค์
|
1081 |
-
slide['title'] = ' '.join(title_lines).strip()
|
1082 |
slide['content'] = '\n'.join(content_lines).strip()
|
1083 |
slide['notes'] = ' '.join(notes_lines).strip()
|
1084 |
|
1085 |
-
# ์ ๋ชฉ ์ ๋ฆฌ
|
1086 |
-
slide['title'] = re.sub(r'^(์ฌ๋ผ์ด๋|Slide)\s*\d+\s*[:๏ผ\-]?\s*', '', slide['title'], flags=re.IGNORECASE)
|
1087 |
-
slide['title'] = re.sub(r'^(์ ๋ชฉ|Title)\s*[:๏ผ]\s*', '', slide['title'], flags=re.IGNORECASE)
|
1088 |
-
|
1089 |
# ๋ด์ฉ์ด ์๋ ๊ฒฝ์ฐ์๋ง ์ถ๊ฐ
|
1090 |
if slide['title'] or slide['content']:
|
1091 |
logger.info(f"Slide {len(slides)+1}: Title='{slide['title'][:30]}...', Content length={len(slide['content'])}")
|
1092 |
slides.append(slide)
|
1093 |
|
1094 |
-
# ๋ง์ฝ ์ ๋ฐฉ๋ฒ์ผ๋ก ํ์ฑ์ด ์ ๋์๋ค๋ฉด, ๋
|
1095 |
-
if not slides:
|
1096 |
-
logger.warning("Primary parsing
|
|
|
|
|
|
|
|
|
1097 |
|
1098 |
-
# ๋๋ธ ๋ด๋ผ์ธ์ผ๋ก ๊ตฌ๋ถ
|
1099 |
-
sections = response.split('\n\n')
|
1100 |
for section in sections:
|
1101 |
-
|
1102 |
-
|
1103 |
-
slide = {
|
1104 |
-
'title': lines[0].strip(),
|
1105 |
-
'content': '\n'.join(lines[1:]).strip(),
|
1106 |
-
'notes': '',
|
1107 |
-
'layout': 'title_content',
|
1108 |
-
'chart_data': None
|
1109 |
-
}
|
1110 |
|
1111 |
-
|
1112 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1113 |
|
1114 |
-
if
|
1115 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1116 |
|
1117 |
logger.info(f"Total slides parsed: {len(slides)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
1118 |
return slides
|
1119 |
|
1120 |
def force_font_size(text_frame, font_size_pt: int, theme: Dict):
|
@@ -1380,14 +1543,14 @@ def create_advanced_ppt_from_content(
|
|
1380 |
# ๋ฐฐ๊ฒฝ ๊ทธ๋ผ๋์ธํธ
|
1381 |
add_gradient_background(slide, theme['colors']['primary'], theme['colors']['secondary'])
|
1382 |
|
1383 |
-
# ์ ๋ชฉ๊ณผ ๋ถ์ ๋ชฉ์
|
1384 |
title_shape = slide.shapes.title
|
1385 |
subtitle_shape = slide.placeholders[1] if len(slide.placeholders) > 1 else None
|
1386 |
|
1387 |
if title_shape:
|
1388 |
title_shape.left = Inches(0.5)
|
1389 |
title_shape.width = prs.slide_width - Inches(1)
|
1390 |
-
title_shape.top = Inches(0
|
1391 |
title_shape.height = Inches(1.2)
|
1392 |
|
1393 |
tf = title_shape.text_frame
|
@@ -1403,7 +1566,7 @@ def create_advanced_ppt_from_content(
|
|
1403 |
if subtitle_shape:
|
1404 |
subtitle_shape.left = Inches(0.5)
|
1405 |
subtitle_shape.width = prs.slide_width - Inches(1)
|
1406 |
-
subtitle_shape.top = Inches(2.
|
1407 |
subtitle_shape.height = Inches(0.9)
|
1408 |
|
1409 |
tf2 = subtitle_shape.text_frame
|
@@ -1415,18 +1578,27 @@ def create_advanced_ppt_from_content(
|
|
1415 |
p2.font.color.rgb = RGBColor(255, 255, 255)
|
1416 |
p2.alignment = PP_ALIGN.CENTER
|
1417 |
|
1418 |
-
# AI ์ด๋ฏธ์ง๋ฅผ
|
1419 |
-
if include_ai_image and FLUX_API_ENABLED:
|
1420 |
-
logger.info("Generating AI cover
|
1421 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1422 |
if ai_image_path and os.path.exists(ai_image_path):
|
1423 |
try:
|
1424 |
img = Image.open(ai_image_path)
|
1425 |
img_width, img_height = img.size
|
1426 |
|
1427 |
-
# ์ด๋ฏธ์ง๋ฅผ
|
1428 |
-
max_width = Inches(
|
1429 |
-
max_height = Inches(
|
1430 |
|
1431 |
ratio = img_height / img_width
|
1432 |
img_w = max_width
|
@@ -1436,9 +1608,9 @@ def create_advanced_ppt_from_content(
|
|
1436 |
img_h = max_height
|
1437 |
img_w = max_height / ratio
|
1438 |
|
1439 |
-
# ์ฐ์ธก ๋ฐฐ์น
|
1440 |
left = prs.slide_width - img_w - Inches(0.5)
|
1441 |
-
top =
|
1442 |
|
1443 |
pic = slide.shapes.add_picture(ai_image_path, left, top, width=img_w, height=img_h)
|
1444 |
pic.shadow.inherit = False
|
@@ -1447,9 +1619,9 @@ def create_advanced_ppt_from_content(
|
|
1447 |
pic.shadow.distance = Pt(8)
|
1448 |
pic.shadow.angle = 45
|
1449 |
|
1450 |
-
# ์ด๋ฏธ์ง
|
1451 |
caption_box = slide.shapes.add_textbox(
|
1452 |
-
left, top
|
1453 |
img_w, Inches(0.3)
|
1454 |
)
|
1455 |
caption_tf = caption_box.text_frame
|
@@ -1459,10 +1631,14 @@ def create_advanced_ppt_from_content(
|
|
1459 |
caption_p.font.color.rgb = RGBColor(255, 255, 255)
|
1460 |
caption_p.alignment = PP_ALIGN.CENTER
|
1461 |
|
1462 |
-
|
1463 |
-
|
1464 |
-
|
1465 |
-
|
|
|
|
|
|
|
|
|
1466 |
except Exception as e:
|
1467 |
logger.error(f"Failed to add cover image: {e}")
|
1468 |
|
@@ -1525,18 +1701,18 @@ def create_advanced_ppt_from_content(
|
|
1525 |
should_add_visual = False
|
1526 |
visual_type = None
|
1527 |
|
1528 |
-
# ๊ฒฐ๋ก ์ฌ๋ผ์ด๋๋ ํญ์
|
1529 |
-
if is_conclusion_slide and include_flux_images
|
1530 |
should_add_visual = True
|
1531 |
-
visual_type = ('
|
1532 |
elif include_diagrams:
|
1533 |
diagram_type = detect_diagram_type(slide_title, slide_content)
|
1534 |
if diagram_type:
|
1535 |
should_add_visual = True
|
1536 |
visual_type = ('diagram', diagram_type)
|
1537 |
-
elif not should_add_visual and include_flux_images and i %
|
1538 |
should_add_visual = True
|
1539 |
-
visual_type = ('
|
1540 |
|
1541 |
# ์๊ฐ์ ์์๊ฐ ์๋ ๊ฒฝ์ฐ ์ข-์ฐ ๋ ์ด์์ ์ ์ฉ
|
1542 |
if should_add_visual and layout_type not in ['section_header']:
|
@@ -1584,17 +1760,19 @@ def create_advanced_ppt_from_content(
|
|
1584 |
except Exception as e:
|
1585 |
logger.error(f"Failed to add diagram: {e}")
|
1586 |
|
1587 |
-
elif visual_type[0] == '
|
1588 |
-
# ๊ฒฐ๋ก ์ฌ๋ผ์ด๋์ฉ
|
1589 |
-
logger.info(f"Generating conclusion
|
1590 |
-
|
1591 |
-
|
|
|
|
|
|
|
1592 |
|
1593 |
-
if
|
1594 |
try:
|
1595 |
-
# FLUX ์ด๋ฏธ์ง ์ถ๊ฐ
|
1596 |
pic = slide.shapes.add_picture(
|
1597 |
-
|
1598 |
Inches(5.2), Inches(1.5),
|
1599 |
width=Inches(4.3), height=Inches(3.0)
|
1600 |
)
|
@@ -1611,31 +1789,45 @@ def create_advanced_ppt_from_content(
|
|
1611 |
caption_p.font.color.rgb = theme['colors']['secondary']
|
1612 |
caption_p.alignment = PP_ALIGN.CENTER
|
1613 |
|
1614 |
-
# ์์ ํ์ผ
|
1615 |
-
|
|
|
|
|
|
|
|
|
|
|
1616 |
except Exception as e:
|
1617 |
-
logger.error(f"Failed to add conclusion
|
1618 |
|
1619 |
-
elif visual_type[0] == '
|
1620 |
-
#
|
1621 |
-
logger.info(f"Generating
|
1622 |
-
|
1623 |
-
|
|
|
|
|
|
|
|
|
|
|
1624 |
|
1625 |
-
if
|
1626 |
try:
|
1627 |
-
# FLUX ์ด๋ฏธ์ง ์ถ๊ฐ
|
1628 |
pic = slide.shapes.add_picture(
|
1629 |
-
|
1630 |
Inches(5.2), Inches(1.5),
|
1631 |
width=Inches(4.3), height=Inches(3.0)
|
1632 |
)
|
1633 |
visual_added = True
|
1634 |
|
1635 |
-
# ์์ ํ์ผ
|
1636 |
-
|
|
|
|
|
|
|
|
|
|
|
1637 |
except Exception as e:
|
1638 |
-
logger.error(f"Failed to add
|
1639 |
|
1640 |
# ์๊ฐ์ ์์๊ฐ ์ถ๊ฐ๋์ง ์์ ๊ฒฝ์ฐ ํ๋ ์ด์คํ๋ ์ถ๊ฐ
|
1641 |
if not visual_added:
|
@@ -2052,14 +2244,25 @@ def generate_ppt(
|
|
2052 |
return None, "โ ์ฌ๋ผ์ด๋ ์๋ 3์ฅ ์ด์ 20์ฅ ์ดํ๋ก ์ค์ ํด์ฃผ์ธ์.", ""
|
2053 |
|
2054 |
try:
|
2055 |
-
#
|
2056 |
-
if include_ai_image and not
|
2057 |
-
yield None, "๐
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2058 |
if initialize_flux_api():
|
2059 |
yield None, "โ
FLUX API ์ฐ๊ฒฐ ์ฑ๊ณต!", ""
|
2060 |
else:
|
2061 |
-
include_ai_image
|
2062 |
-
|
|
|
|
|
2063 |
|
2064 |
# ๋ค์ด์ด๊ทธ๋จ API ์ด๊ธฐํ
|
2065 |
if include_diagrams and not DIAGRAM_API_ENABLED:
|
@@ -2070,15 +2273,6 @@ def generate_ppt(
|
|
2070 |
include_diagrams = False
|
2071 |
yield None, "โ ๏ธ ๋ค์ด์ด๊ทธ๋จ API ์ฐ๊ฒฐ ์คํจ. ๋ค์ด์ด๊ทธ๋จ ์์ด ์งํํฉ๋๋ค.", ""
|
2072 |
|
2073 |
-
# FLUX API ์ด๊ธฐํ (์ฌ๋ผ์ด๋ ์ด๋ฏธ์ง์ฉ)
|
2074 |
-
if include_flux_images and not FLUX_API_ENABLED:
|
2075 |
-
yield None, "๐ FLUX ์ด๋ฏธ์ง ์์ฑ API์ ์ฐ๊ฒฐํ๋ ์ค...", ""
|
2076 |
-
if initialize_flux_api():
|
2077 |
-
yield None, "โ
FLUX API ์ฐ๊ฒฐ ์ฑ๊ณต!", ""
|
2078 |
-
else:
|
2079 |
-
include_flux_images = False
|
2080 |
-
yield None, "โ ๏ธ FLUX API ์ฐ๊ฒฐ ์คํจ. ์ฌ๋ผ์ด๋ ์ด๋ฏธ์ง ์์ด ์งํํฉ๋๋ค.", ""
|
2081 |
-
|
2082 |
# Process reference files if provided
|
2083 |
additional_context = ""
|
2084 |
chart_data = None
|
@@ -2540,16 +2734,17 @@ with gr.Blocks(css=css, title="AI PPT Generator Pro") as demo:
|
|
2540 |
6. **์์ฑ ๋ฒํผ ํด๋ฆญ**: AI๊ฐ ์๋์ผ๋ก PPT ์์ฑ
|
2541 |
|
2542 |
### ๐จ ์๋ก์ด ๊ธฐ๋ฅ
|
2543 |
-
-
|
2544 |
-
-
|
2545 |
-
- **
|
2546 |
-
- **๊ฒฐ๋ก ์ฌ๋ผ์ด๋ ๊ฐ์กฐ**: ๊ฒฐ๋ก /์์ฝ ์ฌ๋ผ์ด๋์ ํน๋ณํ
|
2547 |
- **์ข-์ฐ ๋ ์ด์์**: ํ
์คํธ๋ ์ข์ธก, ์๊ฐ์ ์์๋ ์ฐ์ธก ๋ฐฐ์น
|
2548 |
|
2549 |
### ๐ก ๊ณ ๊ธ ํ
|
2550 |
-
-
|
|
|
|
|
2551 |
- ๊ฒฐ๋ก /์์ฝ ์ฌ๋ผ์ด๋๋ ์๋์ผ๋ก ๊ฐ์ง๋์ด ํน๋ณํ ์ด๋ฏธ์ง๊ฐ ์ถ๊ฐ๋ฉ๋๋ค
|
2552 |
-
- FLUX ์ด๋ฏธ์ง๋ ์ฌ๋ผ์ด๋ ๋ด์ฉ์ ๋ถ์ํ์ฌ ๊ด๋ จ ์ด๋ฏธ์ง๋ฅผ ์์ฑํฉ๋๋ค
|
2553 |
"""
|
2554 |
)
|
2555 |
|
@@ -2588,10 +2783,21 @@ with gr.Blocks(css=css, title="AI PPT Generator Pro") as demo:
|
|
2588 |
|
2589 |
# Initialize APIs on startup
|
2590 |
if __name__ == "__main__":
|
2591 |
-
# Try to initialize APIs
|
2592 |
-
|
2593 |
-
|
2594 |
-
|
2595 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2596 |
|
2597 |
demo.launch()
|
|
|
10 |
import random
|
11 |
from typing import Dict, List, Tuple, Optional
|
12 |
import shutil
|
13 |
+
import concurrent.futures
|
14 |
|
15 |
import gradio as gr
|
16 |
from loguru import logger
|
|
|
49 |
if not SERPHOUSE_API_KEY:
|
50 |
logger.warning("SERPHOUSE_API_KEY not set. Web search functionality will be limited.")
|
51 |
|
52 |
+
##############################################################################
|
53 |
+
# AI Image Generation API Configuration - 3D Style
|
54 |
+
##############################################################################
|
55 |
+
AI_IMAGE_API_URL = "http://211.233.58.201:7971/"
|
56 |
+
AI_IMAGE_ENABLED = False
|
57 |
+
ai_image_client = None
|
58 |
+
|
59 |
+
def initialize_ai_image_api():
|
60 |
+
"""AI ์ด๋ฏธ์ง ์์ฑ API ์ด๊ธฐํ (3D ์คํ์ผ)"""
|
61 |
+
global AI_IMAGE_ENABLED, ai_image_client
|
62 |
+
|
63 |
+
try:
|
64 |
+
logger.info("Connecting to AI image generation API (3D style)...")
|
65 |
+
ai_image_client = Client(AI_IMAGE_API_URL)
|
66 |
+
AI_IMAGE_ENABLED = True
|
67 |
+
logger.info("AI image generation API (3D style) connected successfully")
|
68 |
+
return True
|
69 |
+
except Exception as e:
|
70 |
+
logger.error(f"Failed to connect to AI image API: {e}")
|
71 |
+
AI_IMAGE_ENABLED = False
|
72 |
+
return False
|
73 |
+
|
74 |
##############################################################################
|
75 |
# AI Image Generation API Configuration - FLUX API
|
76 |
##############################################################################
|
|
|
886 |
return f"**[PDF File: {os.path.basename(pdf_path)}]**\n\n{full_text}"
|
887 |
|
888 |
##############################################################################
|
889 |
+
# AI Image Generation Functions using Multiple APIs
|
890 |
##############################################################################
|
891 |
+
def generate_diverse_prompt(title: str, content: str, slide_index: int) -> Tuple[str, str]:
|
892 |
+
"""์ฌ๋ผ์ด๋๋ณ๋ก ๋ค์ํ ํ๋กฌํํธ ์์ฑ - 3D์ ํฌํ ๋ฆฌ์ผ๋ฆฌ์คํฑ ๋ฒ์ """
|
893 |
|
894 |
# ์ฃผ์ ํค์๋ ์ถ์ถ
|
895 |
+
keywords = extract_keywords(f"{title} {content}", top_k=5).split()
|
896 |
|
897 |
+
# ์ฌ๋ผ์ด๋ ์ธ๋ฑ์ค์ ๋ฐ๋ผ ๋ค์ํ ์คํ์ผ ์ ์ฉ
|
898 |
+
styles_3d = [
|
899 |
+
"isometric 3D illustration",
|
900 |
+
"low poly 3D art",
|
901 |
+
"3D cartoon style",
|
902 |
+
"3D glass morphism",
|
903 |
+
"3D neon glow effect",
|
904 |
+
"3D paper cut art",
|
905 |
+
"3D clay render",
|
906 |
+
"3D geometric abstract"
|
907 |
+
]
|
908 |
|
909 |
+
styles_photo = [
|
910 |
+
"professional photography",
|
911 |
+
"cinematic shot",
|
912 |
+
"minimalist photography",
|
913 |
+
"aerial view photograph",
|
914 |
+
"macro photography",
|
915 |
+
"dramatic lighting photo",
|
916 |
+
"architectural photography",
|
917 |
+
"lifestyle photography"
|
918 |
+
]
|
|
|
|
|
919 |
|
920 |
+
# ๋ด์ฉ ๊ธฐ๋ฐ ์๊ฐ ๋ฉํํฌ ์ ํ
|
921 |
+
visual_metaphors = []
|
922 |
+
content_lower = (title + " " + content).lower()
|
923 |
|
924 |
+
if any(word in content_lower for word in ['์ฑ์ฅ', 'growth', '์ฆ๊ฐ', 'increase']):
|
925 |
+
visual_metaphors = ["ascending stairs", "growing tree", "rocket launch", "mountain peak", "rising graph"]
|
926 |
+
elif any(word in content_lower for word in ['ํ์ ', 'innovation', '์ฐฝ์', 'creative']):
|
927 |
+
visual_metaphors = ["lightbulb moment", "puzzle pieces connecting", "spark of genius", "breaking boundaries", "colorful explosion"]
|
928 |
+
elif any(word in content_lower for word in ['ํ์
', 'collaboration', 'ํ', 'team']):
|
929 |
+
visual_metaphors = ["hands joining together", "connected network", "team huddle", "bridge building", "interlocking gears"]
|
930 |
+
elif any(word in content_lower for word in ['๋ฐ์ดํฐ', 'data', '๋ถ์', 'analysis']):
|
931 |
+
visual_metaphors = ["data visualization", "digital dashboard", "flowing data streams", "analytical charts", "information network"]
|
932 |
+
elif any(word in content_lower for word in ['๋ฏธ๋', 'future', '์ ๋ง', 'vision']):
|
933 |
+
visual_metaphors = ["horizon view", "crystal ball", "futuristic cityscape", "pathway to tomorrow", "digital transformation"]
|
934 |
+
elif any(word in content_lower for word in ['ํ๋ก์ธ์ค', 'process', '๋จ๊ณ', 'step']):
|
935 |
+
visual_metaphors = ["flowing river", "assembly line", "domino effect", "clockwork mechanism", "journey path"]
|
936 |
+
elif any(word in content_lower for word in ['๋ชฉํ', 'goal', '์ฑ๊ณต', 'success']):
|
937 |
+
visual_metaphors = ["target with arrow", "trophy on pedestal", "finish line", "mountain summit", "golden key"]
|
938 |
+
else:
|
939 |
+
visual_metaphors = ["abstract shapes", "dynamic composition", "symbolic representation", "conceptual art", "modern design"]
|
940 |
+
|
941 |
+
# ์คํ์ผ๊ณผ ๋ฉํํฌ ์ ํ
|
942 |
+
style_3d = styles_3d[slide_index % len(styles_3d)]
|
943 |
+
style_photo = styles_photo[slide_index % len(styles_photo)]
|
944 |
+
metaphor = random.choice(visual_metaphors)
|
945 |
+
|
946 |
+
# ์์ ํ๋ ํธ ๋ค์ํ
|
947 |
+
color_palettes = [
|
948 |
+
"vibrant blue and orange",
|
949 |
+
"elegant purple and gold",
|
950 |
+
"fresh green and white",
|
951 |
+
"bold red and black",
|
952 |
+
"soft pastel tones",
|
953 |
+
"monochromatic blue",
|
954 |
+
"warm sunset colors",
|
955 |
+
"cool ocean palette"
|
956 |
+
]
|
957 |
+
colors = color_palettes[slide_index % len(color_palettes)]
|
958 |
|
959 |
+
# 3D ์คํ์ผ ํ๋กฌํํธ (ํ๊ธ)
|
960 |
+
prompt_3d = f"wbgmsst, {style_3d}, {metaphor} representing {' '.join(keywords[:3])}, {colors}, professional presentation slide, high quality, white background"
|
961 |
+
|
962 |
+
# ํฌํ ๋ฆฌ์ผ๋ฆฌ์คํฑ ํ๋กฌํํธ (์์ด)
|
963 |
+
prompt_photo = f"{style_photo} of {metaphor} symbolizing {' '.join(keywords[:3])}, {colors} color scheme, professional business context, clean composition, high resolution"
|
964 |
+
|
965 |
+
return prompt_3d, prompt_photo
|
966 |
+
|
967 |
+
def generate_cover_image_prompts(topic: str, slides_data: list) -> Tuple[str, str]:
|
968 |
+
"""ํ์ง์ฉ 3D์ ํฌํ ๋ฆฌ์ผ๋ฆฌ์คํฑ ํ๋กฌํํธ ์์ฑ"""
|
969 |
|
970 |
+
keywords = extract_keywords(topic, top_k=3).split()
|
|
|
|
|
971 |
|
972 |
+
# ์ฃผ์ ๋ณ ํนํ๋ ์๊ฐ ์์
|
973 |
+
topic_lower = topic.lower()
|
974 |
+
if any(word in topic_lower for word in ['๊ธฐ์ ', 'tech', 'ai', '์ธ๊ณต์ง๋ฅ']):
|
975 |
+
visual_3d = "futuristic 3D holographic interface"
|
976 |
+
visual_photo = "modern technology workspace with holographic displays"
|
977 |
+
elif any(word in topic_lower for word in ['๋น์ฆ๋์ค', 'business', '๊ฒฝ์']):
|
978 |
+
visual_3d = "3D corporate building with glass architecture"
|
979 |
+
visual_photo = "professional business meeting in modern office"
|
980 |
+
elif any(word in topic_lower for word in ['๊ต์ก', 'education', 'ํ์ต']):
|
981 |
+
visual_3d = "3D books transforming into knowledge symbols"
|
982 |
+
visual_photo = "inspiring educational environment with digital elements"
|
983 |
+
elif any(word in topic_lower for word in ['ํ๊ฒฝ', 'environment', '์์ฐ']):
|
984 |
+
visual_3d = "3D earth with renewable energy icons"
|
985 |
+
visual_photo = "pristine nature landscape with sustainable elements"
|
986 |
else:
|
987 |
+
visual_3d = "abstract 3D geometric composition"
|
988 |
+
visual_photo = "professional abstract photography"
|
989 |
|
990 |
+
prompt_3d = f"wbgmsst, {visual_3d}, {' '.join(keywords)} theme, premium 3D render, elegant composition, gradient background"
|
991 |
+
prompt_photo = f"{visual_photo} featuring {' '.join(keywords)}, cinematic lighting, professional presentation cover, high-end photography"
|
992 |
|
993 |
+
return prompt_3d, prompt_photo
|
994 |
|
995 |
+
def generate_conclusion_image_prompts(title: str, content: str) -> Tuple[str, str]:
|
996 |
+
"""๊ฒฐ๋ก ์ฌ๋ผ์ด๋์ฉ ํน๋ณํ ํ๋กฌํํธ ์์ฑ"""
|
997 |
|
|
|
998 |
keywords = extract_keywords(f"{title} {content}", top_k=4).split()
|
999 |
|
1000 |
+
# ๊ฒฐ๋ก ์คํ์ผ ๋น์ฃผ์ผ
|
1001 |
+
prompt_3d = f"wbgmsst, 3D trophy or achievement symbol, {' '.join(keywords[:2])} success visualization, golden lighting, celebration mood, premium quality"
|
1002 |
+
prompt_photo = f"inspirational sunrise or horizon view symbolizing {' '.join(keywords[:2])}, bright future ahead, professional photography, uplifting atmosphere"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1003 |
|
1004 |
+
return prompt_3d, prompt_photo
|
1005 |
+
|
1006 |
+
def generate_ai_image_via_3d_api(prompt: str) -> Optional[str]:
|
1007 |
+
"""3D ์คํ์ผ API๋ฅผ ํตํด ์ด๋ฏธ์ง ์์ฑ"""
|
1008 |
+
if not AI_IMAGE_ENABLED or not ai_image_client:
|
1009 |
+
return None
|
1010 |
|
1011 |
+
try:
|
1012 |
+
logger.info(f"Generating 3D style image with prompt: {prompt[:100]}...")
|
1013 |
+
|
1014 |
+
result = ai_image_client.predict(
|
1015 |
+
height=1024.0,
|
1016 |
+
width=1024.0,
|
1017 |
+
steps=8.0,
|
1018 |
+
scales=3.5,
|
1019 |
+
prompt=prompt,
|
1020 |
+
seed=float(random.randint(0, 1000000)),
|
1021 |
+
api_name="/process_and_save_image"
|
1022 |
+
)
|
1023 |
+
|
1024 |
+
# ๊ฒฐ๊ณผ ์ฒ๋ฆฌ
|
1025 |
+
image_path = None
|
1026 |
+
if isinstance(result, dict):
|
1027 |
+
image_path = result.get("path")
|
1028 |
+
elif isinstance(result, str):
|
1029 |
+
image_path = result
|
1030 |
+
|
1031 |
+
if image_path and os.path.exists(image_path):
|
1032 |
+
# PNG๋ก ๋ณํ
|
1033 |
+
with Image.open(image_path) as img:
|
1034 |
+
png_tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".png")
|
1035 |
+
img.save(png_tmp.name, format="PNG")
|
1036 |
+
logger.info(f"3D image generated and saved to {png_tmp.name}")
|
1037 |
+
return png_tmp.name
|
1038 |
+
|
1039 |
+
return None
|
1040 |
+
|
1041 |
+
except Exception as e:
|
1042 |
+
logger.error(f"Failed to generate 3D image: {e}")
|
1043 |
+
return None
|
1044 |
|
1045 |
def generate_flux_image_via_api(prompt: str) -> Optional[str]:
|
1046 |
+
"""FLUX API๋ฅผ ํตํด ํฌํ ๋ฆฌ์ผ๋ฆฌ์คํฑ ์ด๋ฏธ์ง ์์ฑ"""
|
1047 |
if not FLUX_API_ENABLED or not flux_api_client:
|
|
|
1048 |
return None
|
1049 |
|
1050 |
try:
|
1051 |
+
logger.info(f"Generating FLUX photorealistic image with prompt: {prompt[:100]}...")
|
1052 |
|
1053 |
result = flux_api_client.predict(
|
1054 |
prompt=prompt,
|
|
|
1067 |
if isinstance(result, tuple) and len(result) > 0:
|
1068 |
image_path = result[0]
|
1069 |
if image_path and os.path.exists(image_path):
|
|
|
1070 |
with Image.open(image_path) as img:
|
1071 |
png_tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".png")
|
1072 |
img.save(png_tmp.name, format="PNG")
|
|
|
1079 |
logger.error(f"Failed to generate FLUX image: {e}")
|
1080 |
return None
|
1081 |
|
1082 |
+
def generate_images_parallel(prompt_3d: str, prompt_photo: str) -> Tuple[Optional[str], Optional[str]]:
|
1083 |
+
"""๋ API๋ฅผ ๋ณ๋ ฌ๋ก ํธ์ถํ์ฌ ์ด๋ฏธ์ง ์์ฑ"""
|
1084 |
+
import concurrent.futures
|
1085 |
+
|
1086 |
+
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
|
1087 |
+
# ๋ API๋ฅผ ๋์์ ํธ์ถ
|
1088 |
+
future_3d = executor.submit(generate_ai_image_via_3d_api, prompt_3d)
|
1089 |
+
future_photo = executor.submit(generate_flux_image_via_api, prompt_photo)
|
1090 |
+
|
1091 |
+
# ๊ฒฐ๊ณผ ๋๊ธฐ
|
1092 |
+
image_3d = future_3d.result()
|
1093 |
+
image_photo = future_photo.result()
|
1094 |
+
|
1095 |
+
return image_3d, image_photo
|
1096 |
|
1097 |
##############################################################################
|
1098 |
# PPT Generation Functions - FIXED VERSION
|
1099 |
##############################################################################
|
1100 |
def parse_llm_ppt_response(response: str, layout_style: str = "consistent") -> list:
|
1101 |
+
"""Parse LLM response to extract slide content - FIXED VERSION"""
|
1102 |
slides = []
|
1103 |
|
|
|
1104 |
logger.info(f"Parsing LLM response, total length: {len(response)}")
|
1105 |
logger.debug(f"First 500 chars: {response[:500]}")
|
1106 |
|
|
|
1113 |
except:
|
1114 |
pass
|
1115 |
|
1116 |
+
# ๋ ์ ํํ ์ฌ๋ผ์ด๋ ๊ตฌ๋ถ ํจํด
|
1117 |
+
# "์ฌ๋ผ์ด๋ 1", "์ฌ๋ผ์ด๋ 2" ๋๋ "Slide 1", "Slide 2" ํ์์ ์ฐพ์
|
1118 |
+
slide_markers = []
|
1119 |
+
|
1120 |
+
# ์ฌ๋ผ์ด๋ ๋ง์ปค์ ์์น๋ฅผ ๋จผ์ ์ฐพ์
|
1121 |
+
for match in re.finditer(r'^(?:์ฌ๋ผ์ด๋|Slide)\s*(\d+)\s*$', response, re.MULTILINE):
|
1122 |
+
slide_markers.append({
|
1123 |
+
'index': int(match.group(1)),
|
1124 |
+
'start': match.start(),
|
1125 |
+
'end': match.end()
|
1126 |
+
})
|
1127 |
+
|
1128 |
+
logger.info(f"Found {len(slide_markers)} slide markers")
|
1129 |
+
|
1130 |
+
# ์ฌ๋ผ์ด๋ ๋ง์ปค๊ฐ ์์ผ๋ฉด ๋ค๋ฅธ ํจํด ์๋
|
1131 |
+
if not slide_markers:
|
1132 |
+
# ์ซ์๋ง์ผ๋ก ์์ํ๋ ํจํด๋ ์ฐพ๊ธฐ (์: "1.", "2." ๋ฑ)
|
1133 |
+
for match in re.finditer(r'^(\d+)[.)]\s*$', response, re.MULTILINE):
|
1134 |
+
slide_markers.append({
|
1135 |
+
'index': int(match.group(1)),
|
1136 |
+
'start': match.start(),
|
1137 |
+
'end': match.end()
|
1138 |
+
})
|
1139 |
+
|
1140 |
+
# ๊ฐ ์ฌ๋ผ์ด๋ ๋ง์ปค ์ฌ์ด์ ๋ด์ฉ์ ์ถ์ถ
|
1141 |
+
for i, marker in enumerate(slide_markers):
|
1142 |
+
# ํ์ฌ ์ฌ๋ผ์ด๋์ ์์๊ณผ ๋ ์์น
|
1143 |
+
start = marker['end']
|
1144 |
+
if i < len(slide_markers) - 1:
|
1145 |
+
end = slide_markers[i + 1]['start']
|
1146 |
+
else:
|
1147 |
+
end = len(response)
|
1148 |
+
|
1149 |
+
section = response[start:end].strip()
|
1150 |
+
|
1151 |
+
if not section:
|
1152 |
continue
|
1153 |
|
1154 |
+
logger.debug(f"Processing slide {marker['index']}: {section[:100]}...")
|
1155 |
|
1156 |
slide = {
|
1157 |
'title': '',
|
|
|
1162 |
}
|
1163 |
|
1164 |
# ์น์
๋ด์์ ์ ๋ชฉ, ๋ด์ฉ, ๋
ธํธ ์ถ์ถ
|
1165 |
+
lines = section.split('\n')
|
1166 |
current_part = None
|
1167 |
+
title_found = False
|
1168 |
content_lines = []
|
1169 |
notes_lines = []
|
1170 |
|
|
|
1174 |
continue
|
1175 |
|
1176 |
# ์ ๋ชฉ ์น์
๊ฐ์ง
|
1177 |
+
if (line.startswith('์ ๋ชฉ:') or line.startswith('Title:')) and not title_found:
|
1178 |
current_part = 'title'
|
1179 |
title_text = line.split(':', 1)[1].strip() if ':' in line else ''
|
1180 |
+
slide['title'] = title_text
|
1181 |
+
title_found = True
|
1182 |
# ๋ด์ฉ ์น์
๊ฐ์ง
|
1183 |
elif line.startswith('๋ด์ฉ:') or line.startswith('Content:'):
|
1184 |
current_part = 'content'
|
|
|
1193 |
notes_lines.append(notes_text)
|
1194 |
# ํ์ฌ ์น์
์ ๋ฐ๋ผ ๋ด์ฉ ์ถ๊ฐ
|
1195 |
else:
|
1196 |
+
if current_part == 'title' and not slide['title']:
|
1197 |
+
slide['title'] = line
|
1198 |
elif current_part == 'content':
|
1199 |
content_lines.append(line)
|
1200 |
elif current_part == 'notes':
|
1201 |
notes_lines.append(line)
|
1202 |
+
elif not title_found and not slide['title']:
|
1203 |
# ์ฒซ ๋ฒ์งธ ์ค์ ์ ๋ชฉ์ผ๋ก
|
1204 |
+
slide['title'] = line
|
1205 |
+
title_found = True
|
1206 |
+
current_part = 'content'
|
1207 |
+
elif current_part is None and title_found:
|
1208 |
+
current_part = 'content'
|
1209 |
content_lines.append(line)
|
1210 |
|
1211 |
# ์ฌ๋ผ์ด๋ ๋ฐ์ดํฐ ์ค์
|
|
|
1212 |
slide['content'] = '\n'.join(content_lines).strip()
|
1213 |
slide['notes'] = ' '.join(notes_lines).strip()
|
1214 |
|
|
|
|
|
|
|
|
|
1215 |
# ๋ด์ฉ์ด ์๋ ๊ฒฝ์ฐ์๋ง ์ถ๊ฐ
|
1216 |
if slide['title'] or slide['content']:
|
1217 |
logger.info(f"Slide {len(slides)+1}: Title='{slide['title'][:30]}...', Content length={len(slide['content'])}")
|
1218 |
slides.append(slide)
|
1219 |
|
1220 |
+
# ๋ง์ฝ ์ ๋ฐฉ๋ฒ์ผ๋ก ํ์ฑ์ด ์ ๋์๋ค๋ฉด, ๋ ์ ์ฐํ ๋ฐฉ๋ฒ ์๋
|
1221 |
+
if not slides or len(slides) < 3:
|
1222 |
+
logger.warning(f"Primary parsing resulted in only {len(slides)} slides, trying alternative method...")
|
1223 |
+
slides = []
|
1224 |
+
|
1225 |
+
# "์ ๋ชฉ:" ํจํด์ผ๋ก ์ฌ๋ผ์ด๋ ๊ตฌ๋ถ ์๋
|
1226 |
+
sections = re.split(r'\n(?=์ ๋ชฉ:|Title:)', response)
|
1227 |
|
|
|
|
|
1228 |
for section in sections:
|
1229 |
+
if not section.strip():
|
1230 |
+
continue
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1231 |
|
1232 |
+
slide = {
|
1233 |
+
'title': '',
|
1234 |
+
'content': '',
|
1235 |
+
'notes': '',
|
1236 |
+
'layout': 'title_content',
|
1237 |
+
'chart_data': None
|
1238 |
+
}
|
1239 |
+
|
1240 |
+
lines = section.strip().split('\n')
|
1241 |
+
current_part = None
|
1242 |
+
content_lines = []
|
1243 |
+
notes_lines = []
|
1244 |
+
|
1245 |
+
for line in lines:
|
1246 |
+
line = line.strip()
|
1247 |
+
if not line:
|
1248 |
+
continue
|
1249 |
|
1250 |
+
if line.startswith('์ ๋ชฉ:') or line.startswith('Title:'):
|
1251 |
+
slide['title'] = line.split(':', 1)[1].strip() if ':' in line else ''
|
1252 |
+
current_part = 'content'
|
1253 |
+
elif line.startswith('๋ด์ฉ:') or line.startswith('Content:'):
|
1254 |
+
current_part = 'content'
|
1255 |
+
elif line.startswith('๋
ธํธ:') or line.startswith('Notes:'):
|
1256 |
+
current_part = 'notes'
|
1257 |
+
notes_text = line.split(':', 1)[1].strip() if ':' in line else ''
|
1258 |
+
if notes_text:
|
1259 |
+
notes_lines.append(notes_text)
|
1260 |
+
elif current_part == 'content':
|
1261 |
+
content_lines.append(line)
|
1262 |
+
elif current_part == 'notes':
|
1263 |
+
notes_lines.append(line)
|
1264 |
+
|
1265 |
+
slide['content'] = '\n'.join(content_lines).strip()
|
1266 |
+
slide['notes'] = ' '.join(notes_lines).strip()
|
1267 |
+
|
1268 |
+
# ์ฌ๋ผ์ด๋ ๋ฒํธ ์ ๊ฑฐ
|
1269 |
+
slide['title'] = re.sub(r'^(์ฌ๋ผ์ด๋|Slide)\s*\d+\s*[:๏ผ\-]?\s*', '', slide['title'], flags=re.IGNORECASE)
|
1270 |
+
|
1271 |
+
if slide['title'] or slide['content']:
|
1272 |
+
slides.append(slide)
|
1273 |
|
1274 |
logger.info(f"Total slides parsed: {len(slides)}")
|
1275 |
+
|
1276 |
+
# ํ์ฑ ๊ฒฐ๊ณผ ๊ฒ์ฆ
|
1277 |
+
if len(slides) < 3:
|
1278 |
+
logger.error("Parsing resulted in too few slides. Raw response preview:")
|
1279 |
+
logger.error(response[:1000])
|
1280 |
+
|
1281 |
return slides
|
1282 |
|
1283 |
def force_font_size(text_frame, font_size_pt: int, theme: Dict):
|
|
|
1543 |
# ๋ฐฐ๊ฒฝ ๊ทธ๋ผ๋์ธํธ
|
1544 |
add_gradient_background(slide, theme['colors']['primary'], theme['colors']['secondary'])
|
1545 |
|
1546 |
+
# ์ ๋ชฉ๊ณผ ๋ถ์ ๋ชฉ์ ์ค์ ์๋จ์ ๋ฐฐ์น
|
1547 |
title_shape = slide.shapes.title
|
1548 |
subtitle_shape = slide.placeholders[1] if len(slide.placeholders) > 1 else None
|
1549 |
|
1550 |
if title_shape:
|
1551 |
title_shape.left = Inches(0.5)
|
1552 |
title_shape.width = prs.slide_width - Inches(1)
|
1553 |
+
title_shape.top = Inches(1.0) # ์๋จ์ ๋ฐฐ์น
|
1554 |
title_shape.height = Inches(1.2)
|
1555 |
|
1556 |
tf = title_shape.text_frame
|
|
|
1566 |
if subtitle_shape:
|
1567 |
subtitle_shape.left = Inches(0.5)
|
1568 |
subtitle_shape.width = prs.slide_width - Inches(1)
|
1569 |
+
subtitle_shape.top = Inches(2.2) # ์ ๋ชฉ ์๋์ ๋ฐฐ์น
|
1570 |
subtitle_shape.height = Inches(0.9)
|
1571 |
|
1572 |
tf2 = subtitle_shape.text_frame
|
|
|
1578 |
p2.font.color.rgb = RGBColor(255, 255, 255)
|
1579 |
p2.alignment = PP_ALIGN.CENTER
|
1580 |
|
1581 |
+
# AI ์ด๋ฏธ์ง๋ฅผ ์ฐ์ธก ํ๋จ์ ๋ฐฐ์น (๋ API ๋ณ๋ ฌ ์ฌ์ฉ)
|
1582 |
+
if include_ai_image and (AI_IMAGE_ENABLED or FLUX_API_ENABLED):
|
1583 |
+
logger.info("Generating AI cover images via parallel APIs...")
|
1584 |
+
|
1585 |
+
# ๋ ๊ฐ์ง ํ๋กฌํํธ ์์ฑ
|
1586 |
+
prompt_3d, prompt_photo = generate_cover_image_prompts(topic, slides_data)
|
1587 |
+
|
1588 |
+
# ๋ณ๋ ฌ๋ก ์ด๋ฏธ์ง ์์ฑ
|
1589 |
+
image_3d, image_photo = generate_images_parallel(prompt_3d, prompt_photo)
|
1590 |
+
|
1591 |
+
# ์ฑ๊ณตํ ์ด๋ฏธ์ง ์ค ํ๋ ์ ํ (3D ์ฐ์ )
|
1592 |
+
ai_image_path = image_3d or image_photo
|
1593 |
+
|
1594 |
if ai_image_path and os.path.exists(ai_image_path):
|
1595 |
try:
|
1596 |
img = Image.open(ai_image_path)
|
1597 |
img_width, img_height = img.size
|
1598 |
|
1599 |
+
# ์ด๋ฏธ์ง๋ฅผ ์ฐ์ธก ํ๋จ์ ๋ฐฐ์น
|
1600 |
+
max_width = Inches(3.5)
|
1601 |
+
max_height = Inches(2.5)
|
1602 |
|
1603 |
ratio = img_height / img_width
|
1604 |
img_w = max_width
|
|
|
1608 |
img_h = max_height
|
1609 |
img_w = max_height / ratio
|
1610 |
|
1611 |
+
# ์ฐ์ธก ํ๋จ ๋ฐฐ์น
|
1612 |
left = prs.slide_width - img_w - Inches(0.5)
|
1613 |
+
top = prs.slide_height - img_h - Inches(0.8)
|
1614 |
|
1615 |
pic = slide.shapes.add_picture(ai_image_path, left, top, width=img_w, height=img_h)
|
1616 |
pic.shadow.inherit = False
|
|
|
1619 |
pic.shadow.distance = Pt(8)
|
1620 |
pic.shadow.angle = 45
|
1621 |
|
1622 |
+
# ์ด๋ฏธ์ง ์์ ์์ ์บก์
์ถ๊ฐ
|
1623 |
caption_box = slide.shapes.add_textbox(
|
1624 |
+
left, top - Inches(0.3),
|
1625 |
img_w, Inches(0.3)
|
1626 |
)
|
1627 |
caption_tf = caption_box.text_frame
|
|
|
1631 |
caption_p.font.color.rgb = RGBColor(255, 255, 255)
|
1632 |
caption_p.alignment = PP_ALIGN.CENTER
|
1633 |
|
1634 |
+
# ์์ ํ์ผ ์ ๋ฆฌ
|
1635 |
+
for temp_path in [image_3d, image_photo]:
|
1636 |
+
if temp_path and os.path.exists(temp_path):
|
1637 |
+
try:
|
1638 |
+
os.unlink(temp_path)
|
1639 |
+
except:
|
1640 |
+
pass
|
1641 |
+
|
1642 |
except Exception as e:
|
1643 |
logger.error(f"Failed to add cover image: {e}")
|
1644 |
|
|
|
1701 |
should_add_visual = False
|
1702 |
visual_type = None
|
1703 |
|
1704 |
+
# ๊ฒฐ๋ก ์ฌ๋ผ์ด๋๋ ํญ์ ์ด๋ฏธ์ง ์ถ๊ฐ
|
1705 |
+
if is_conclusion_slide and include_flux_images:
|
1706 |
should_add_visual = True
|
1707 |
+
visual_type = ('conclusion_images', None)
|
1708 |
elif include_diagrams:
|
1709 |
diagram_type = detect_diagram_type(slide_title, slide_content)
|
1710 |
if diagram_type:
|
1711 |
should_add_visual = True
|
1712 |
visual_type = ('diagram', diagram_type)
|
1713 |
+
elif not should_add_visual and include_flux_images and i % 2 == 0: # ๋งค 2๋ฒ์งธ ์ฌ๋ผ์ด๋์ ์ด๋ฏธ์ง
|
1714 |
should_add_visual = True
|
1715 |
+
visual_type = ('diverse_images', None)
|
1716 |
|
1717 |
# ์๊ฐ์ ์์๊ฐ ์๋ ๊ฒฝ์ฐ ์ข-์ฐ ๋ ์ด์์ ์ ์ฉ
|
1718 |
if should_add_visual and layout_type not in ['section_header']:
|
|
|
1760 |
except Exception as e:
|
1761 |
logger.error(f"Failed to add diagram: {e}")
|
1762 |
|
1763 |
+
elif visual_type[0] == 'conclusion_images':
|
1764 |
+
# ๊ฒฐ๋ก ์ฌ๋ผ์ด๋์ฉ ์ด๋ฏธ์ง ์์ฑ (๋ API ๋ณ๋ ฌ)
|
1765 |
+
logger.info(f"Generating conclusion images for slide {i+1}")
|
1766 |
+
prompt_3d, prompt_photo = generate_conclusion_image_prompts(slide_title, slide_content)
|
1767 |
+
image_3d, image_photo = generate_images_parallel(prompt_3d, prompt_photo)
|
1768 |
+
|
1769 |
+
# ์ฑ๊ณตํ ์ด๋ฏธ์ง ์ค ํ๋ ์ ํ
|
1770 |
+
selected_image = image_photo or image_3d # ๊ฒฐ๋ก ์ ํฌํ ๋ฆฌ์ผ๋ฆฌ์คํฑ ์ฐ์
|
1771 |
|
1772 |
+
if selected_image and os.path.exists(selected_image):
|
1773 |
try:
|
|
|
1774 |
pic = slide.shapes.add_picture(
|
1775 |
+
selected_image,
|
1776 |
Inches(5.2), Inches(1.5),
|
1777 |
width=Inches(4.3), height=Inches(3.0)
|
1778 |
)
|
|
|
1789 |
caption_p.font.color.rgb = theme['colors']['secondary']
|
1790 |
caption_p.alignment = PP_ALIGN.CENTER
|
1791 |
|
1792 |
+
# ์์ ํ์ผ ์ ๋ฆฌ
|
1793 |
+
for temp_path in [image_3d, image_photo]:
|
1794 |
+
if temp_path and os.path.exists(temp_path):
|
1795 |
+
try:
|
1796 |
+
os.unlink(temp_path)
|
1797 |
+
except:
|
1798 |
+
pass
|
1799 |
except Exception as e:
|
1800 |
+
logger.error(f"Failed to add conclusion image: {e}")
|
1801 |
|
1802 |
+
elif visual_type[0] == 'diverse_images':
|
1803 |
+
# ๋ค์ํ ์ฌ๋ผ์ด๋ ์ด๋ฏธ์ง ์์ฑ (๋ API ๋ณ๋ ฌ)
|
1804 |
+
logger.info(f"Generating diverse images for slide {i+1}")
|
1805 |
+
prompt_3d, prompt_photo = generate_diverse_prompt(slide_title, slide_content, i)
|
1806 |
+
image_3d, image_photo = generate_images_parallel(prompt_3d, prompt_photo)
|
1807 |
+
|
1808 |
+
# ์ฌ๋ผ์ด๋ ์ธ๋ฑ์ค์ ๋ฐ๋ผ ๋ฒ๊ฐ์ ์ ํ
|
1809 |
+
selected_image = image_3d if i % 2 == 0 else image_photo
|
1810 |
+
if not selected_image: # ์คํจ์ ๋ค๋ฅธ ๊ฒ ์ ํ
|
1811 |
+
selected_image = image_photo if i % 2 == 0 else image_3d
|
1812 |
|
1813 |
+
if selected_image and os.path.exists(selected_image):
|
1814 |
try:
|
|
|
1815 |
pic = slide.shapes.add_picture(
|
1816 |
+
selected_image,
|
1817 |
Inches(5.2), Inches(1.5),
|
1818 |
width=Inches(4.3), height=Inches(3.0)
|
1819 |
)
|
1820 |
visual_added = True
|
1821 |
|
1822 |
+
# ์์ ํ์ผ ์ ๋ฆฌ
|
1823 |
+
for temp_path in [image_3d, image_photo]:
|
1824 |
+
if temp_path and os.path.exists(temp_path):
|
1825 |
+
try:
|
1826 |
+
os.unlink(temp_path)
|
1827 |
+
except:
|
1828 |
+
pass
|
1829 |
except Exception as e:
|
1830 |
+
logger.error(f"Failed to add slide image: {e}")
|
1831 |
|
1832 |
# ์๊ฐ์ ์์๊ฐ ์ถ๊ฐ๋์ง ์์ ๊ฒฝ์ฐ ํ๋ ์ด์คํ๋ ์ถ๊ฐ
|
1833 |
if not visual_added:
|
|
|
2244 |
return None, "โ ์ฌ๋ผ์ด๋ ์๋ 3์ฅ ์ด์ 20์ฅ ์ดํ๋ก ์ค์ ํด์ฃผ์ธ์.", ""
|
2245 |
|
2246 |
try:
|
2247 |
+
# 3D ์คํ์ผ API ์ด๊ธฐํ (ํ์ง ์ด๋ฏธ์ง์ฉ)
|
2248 |
+
if include_ai_image and not AI_IMAGE_ENABLED:
|
2249 |
+
yield None, "๐ 3D ์คํ์ผ ์ด๋ฏธ์ง ์์ฑ API์ ์ฐ๊ฒฐํ๋ ์ค...", ""
|
2250 |
+
if initialize_ai_image_api():
|
2251 |
+
yield None, "โ
3D ์คํ์ผ API ์ฐ๊ฒฐ ์ฑ๊ณต!", ""
|
2252 |
+
else:
|
2253 |
+
include_ai_image = False
|
2254 |
+
yield None, "โ ๏ธ 3D ์คํ์ผ API ์ฐ๊ฒฐ ์คํจ. AI ์ด๋ฏธ์ง ์์ด ์งํํฉ๋๋ค.", ""
|
2255 |
+
|
2256 |
+
# FLUX API ์ด๊ธฐํ (ํฌํ ๋ฆฌ์ผ๋ฆฌ์คํฑ ์ด๋ฏธ์ง์ฉ)
|
2257 |
+
if (include_ai_image or include_flux_images) and not FLUX_API_ENABLED:
|
2258 |
+
yield None, "๐ FLUX ํฌํ ๋ฆฌ์ผ๋ฆฌ์คํฑ API์ ์ฐ๊ฒฐํ๋ ์ค...", ""
|
2259 |
if initialize_flux_api():
|
2260 |
yield None, "โ
FLUX API ์ฐ๊ฒฐ ์ฑ๊ณต!", ""
|
2261 |
else:
|
2262 |
+
if include_ai_image and not AI_IMAGE_ENABLED:
|
2263 |
+
include_ai_image = False
|
2264 |
+
include_flux_images = False
|
2265 |
+
yield None, "โ ๏ธ FLUX API ์ฐ๊ฒฐ ์คํจ. ํฌํ ๋ฆฌ์ผ๋ฆฌ์คํฑ ์ด๋ฏธ์ง ์์ด ์งํํฉ๋๋ค.", ""
|
2266 |
|
2267 |
# ๋ค์ด์ด๊ทธ๋จ API ์ด๊ธฐํ
|
2268 |
if include_diagrams and not DIAGRAM_API_ENABLED:
|
|
|
2273 |
include_diagrams = False
|
2274 |
yield None, "โ ๏ธ ๋ค์ด์ด๊ทธ๋จ API ์ฐ๊ฒฐ ์คํจ. ๋ค์ด์ด๊ทธ๋จ ์์ด ์งํํฉ๋๋ค.", ""
|
2275 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2276 |
# Process reference files if provided
|
2277 |
additional_context = ""
|
2278 |
chart_data = None
|
|
|
2734 |
6. **์์ฑ ๋ฒํผ ํด๋ฆญ**: AI๊ฐ ์๋์ผ๋ก PPT ์์ฑ
|
2735 |
|
2736 |
### ๐จ ์๋ก์ด ๊ธฐ๋ฅ
|
2737 |
+
- **๋ณ๋ ฌ AI ์ด๋ฏธ์ง ์์ฑ**: 3D ์คํ์ผ๊ณผ ํฌํ ๋ฆฌ์ผ๋ฆฌ์คํฑ API๋ฅผ ๋์์ ์ฌ์ฉํ์ฌ ์์ฑ ์๊ฐ ๋จ์ถ
|
2738 |
+
- **๋ค์ํ ์ด๋ฏธ์ง ์คํ์ผ**: ๊ฐ ์ฌ๋ผ์ด๋๋ง๋ค ๋ค๋ฅธ ์คํ์ผ๊ณผ ๋ฉํํฌ๋ก ์ด๋ฏธ์ง ์์ฑ
|
2739 |
+
- **AI ํ์ง ์ด๋ฏธ์ง**: ์ฐ์ธก ํ๋จ ๋ฐฐ์น๋ก ํ
์คํธ์ ๊ฒน์น์ง ์์
|
2740 |
+
- **๊ฒฐ๋ก ์ฌ๋ผ์ด๋ ๊ฐ์กฐ**: ๊ฒฐ๋ก /์์ฝ ์ฌ๋ผ์ด๋์ ํน๋ณํ ์ด๋ฏธ์ง ์ถ๊ฐ
|
2741 |
- **์ข-์ฐ ๋ ์ด์์**: ํ
์คํธ๋ ์ข์ธก, ์๊ฐ์ ์์๋ ์ฐ์ธก ๋ฐฐ์น
|
2742 |
|
2743 |
### ๐ก ๊ณ ๊ธ ํ
|
2744 |
+
- ํ์ง ์ด๋ฏธ์ง๋ ์ฐ์ธก ํ๋จ์ ๋ฐฐ์น๋์ด ์ ๋ชฉ/๋ถ์ ์ ๊ฒน์น์ง ์์ต๋๋ค
|
2745 |
+
- 3D ์คํ์ผ๊ณผ ํฌํ ๋ฆฌ์ผ๋ฆฌ์คํฑ ์ด๋ฏธ์ง๊ฐ ๋ณ๋ ฌ๋ก ์์ฑ๋์ด ์๊ฐ์ด ์ ์ฝ๋ฉ๋๋ค
|
2746 |
+
- ๊ฐ ์ฌ๋ผ์ด๋๋ ๋ด์ฉ์ ๋ง๋ ๊ณ ์ ํ ์๊ฐ ๋ฉํํฌ์ ์คํ์ผ๋ก ์ด๋ฏธ์ง๊ฐ ์์ฑ๋ฉ๋๋ค
|
2747 |
- ๊ฒฐ๋ก /์์ฝ ์ฌ๋ผ์ด๋๋ ์๋์ผ๋ก ๊ฐ์ง๋์ด ํน๋ณํ ์ด๋ฏธ์ง๊ฐ ์ถ๊ฐ๋ฉ๋๋ค
|
|
|
2748 |
"""
|
2749 |
)
|
2750 |
|
|
|
2783 |
|
2784 |
# Initialize APIs on startup
|
2785 |
if __name__ == "__main__":
|
2786 |
+
# Try to initialize APIs in parallel
|
2787 |
+
with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
|
2788 |
+
futures = []
|
2789 |
+
if AI_IMAGE_API_URL:
|
2790 |
+
futures.append(executor.submit(initialize_ai_image_api))
|
2791 |
+
if FLUX_API_URL:
|
2792 |
+
futures.append(executor.submit(initialize_flux_api))
|
2793 |
+
if DIAGRAM_API_URL:
|
2794 |
+
futures.append(executor.submit(initialize_diagram_api))
|
2795 |
+
|
2796 |
+
# Wait for all to complete
|
2797 |
+
for future in concurrent.futures.as_completed(futures):
|
2798 |
+
try:
|
2799 |
+
future.result()
|
2800 |
+
except Exception as e:
|
2801 |
+
logger.error(f"API initialization failed: {e}")
|
2802 |
|
2803 |
demo.launch()
|