Update app.py
Browse files
app.py
CHANGED
|
@@ -19,11 +19,25 @@ import PyPDF2
|
|
| 19 |
import pandas as pd
|
| 20 |
import chardet
|
| 21 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
# 환경 변수에서 토큰 가져오기
|
| 23 |
REPLICATE_API_TOKEN = os.getenv("RAPI_TOKEN")
|
| 24 |
FRIENDLI_TOKEN = os.getenv("FRIENDLI_TOKEN")
|
| 25 |
BRAVE_API_TOKEN = os.getenv("BAPI_TOKEN")
|
| 26 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
# 디자인 테마 정의
|
| 28 |
DESIGN_THEMES = {
|
| 29 |
"미니멀 라이트": {
|
|
@@ -439,6 +453,101 @@ Create natural speaker notes for presenting this slide."""
|
|
| 439 |
print(f"[발표자 노트] 오류: {str(e)}")
|
| 440 |
return "이 슬라이드에서는 핵심 내용을 설명해 주세요."
|
| 441 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 442 |
def generate_slide_content(topic: str, slide_title: str, slide_context: str, uploaded_content: str = None, web_search_results: List[Dict] = None) -> Dict[str, str]:
|
| 443 |
"""각 슬라이드의 텍스트 내용 생성"""
|
| 444 |
print(f"[슬라이드 내용] {slide_title} 텍스트 생성 중...")
|
|
@@ -829,11 +938,25 @@ def create_slide_preview_html(slide_data: Dict) -> str:
|
|
| 829 |
left: 50%;
|
| 830 |
transform: translate(-50%, -50%);
|
| 831 |
text-align: center;
|
| 832 |
-
|
| 833 |
-
|
|
|
|
|
|
|
|
|
|
| 834 |
">
|
| 835 |
-
|
| 836 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 837 |
</div>
|
| 838 |
"""
|
| 839 |
|
|
@@ -938,6 +1061,20 @@ def create_slide_preview_html(slide_data: Dict) -> str:
|
|
| 938 |
|
| 939 |
return html
|
| 940 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 941 |
def create_pptx_file(results: List[Dict], topic: str, template_name: str, theme_name: str = "미니멀 라이트") -> str:
|
| 942 |
"""생성된 결과를 PPTX 파일로 변환 (발표자 노트 포함)"""
|
| 943 |
print(f"[PPTX] 파일 생성 시작... 테마: {theme_name}")
|
|
@@ -983,17 +1120,26 @@ def create_pptx_file(results: List[Dict], topic: str, template_name: str, theme_
|
|
| 983 |
except Exception as e:
|
| 984 |
print(f"[PPTX] 표지 이미지 추가 실패: {str(e)}")
|
| 985 |
|
| 986 |
-
# 제목 배경 박스 (반투명)
|
| 987 |
title_bg = slide.shapes.add_shape(
|
| 988 |
MSO_SHAPE.ROUNDED_RECTANGLE,
|
| 989 |
Inches(2), Inches(2.5),
|
| 990 |
Inches(12), Inches(4)
|
| 991 |
)
|
| 992 |
title_bg.fill.solid()
|
| 993 |
-
title_bg.fill.fore_color.rgb = RGBColor(
|
| 994 |
-
title_bg.fill.transparency = 0.
|
| 995 |
title_bg.line.fill.background()
|
| 996 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 997 |
# 제목 텍스트 추가
|
| 998 |
title_box = slide.shapes.add_textbox(
|
| 999 |
Inches(2), Inches(3),
|
|
@@ -1004,7 +1150,7 @@ def create_pptx_file(results: List[Dict], topic: str, template_name: str, theme_
|
|
| 1004 |
title_para = title_frame.paragraphs[0]
|
| 1005 |
title_para.font.size = Pt(48)
|
| 1006 |
title_para.font.bold = True
|
| 1007 |
-
title_para.font.color.rgb = RGBColor(
|
| 1008 |
title_para.alignment = PP_ALIGN.CENTER
|
| 1009 |
|
| 1010 |
# 부제목 추가
|
|
@@ -1016,7 +1162,7 @@ def create_pptx_file(results: List[Dict], topic: str, template_name: str, theme_
|
|
| 1016 |
subtitle_frame.text = slide_data.get('subtitle', f'{template_name} - AI 프레젠테이션')
|
| 1017 |
subtitle_para = subtitle_frame.paragraphs[0]
|
| 1018 |
subtitle_para.font.size = Pt(24)
|
| 1019 |
-
subtitle_para.font.color.rgb = RGBColor(
|
| 1020 |
subtitle_para.alignment = PP_ALIGN.CENTER
|
| 1021 |
|
| 1022 |
# Thank You 슬라이드
|
|
@@ -1041,17 +1187,26 @@ def create_pptx_file(results: List[Dict], topic: str, template_name: str, theme_
|
|
| 1041 |
except Exception as e:
|
| 1042 |
print(f"[PPTX] Thank You 이미지 추가 실패: {str(e)}")
|
| 1043 |
|
| 1044 |
-
# Thank You 배경 박스
|
| 1045 |
thanks_bg = slide.shapes.add_shape(
|
| 1046 |
MSO_SHAPE.ROUNDED_RECTANGLE,
|
| 1047 |
Inches(3), Inches(3),
|
| 1048 |
Inches(10), Inches(3)
|
| 1049 |
)
|
| 1050 |
thanks_bg.fill.solid()
|
| 1051 |
-
thanks_bg.fill.fore_color.rgb = RGBColor(
|
| 1052 |
-
thanks_bg.fill.transparency = 0.
|
| 1053 |
thanks_bg.line.fill.background()
|
| 1054 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1055 |
# Thank You 텍스트
|
| 1056 |
thanks_box = slide.shapes.add_textbox(
|
| 1057 |
Inches(2), Inches(3.5),
|
|
@@ -1062,7 +1217,7 @@ def create_pptx_file(results: List[Dict], topic: str, template_name: str, theme_
|
|
| 1062 |
thanks_para = thanks_frame.paragraphs[0]
|
| 1063 |
thanks_para.font.size = Pt(60)
|
| 1064 |
thanks_para.font.bold = True
|
| 1065 |
-
thanks_para.font.color.rgb = RGBColor(
|
| 1066 |
thanks_para.alignment = PP_ALIGN.CENTER
|
| 1067 |
|
| 1068 |
# 일반 슬라이드
|
|
@@ -1255,6 +1410,281 @@ def generate_dynamic_slides(topic: str, template: Dict, slide_count: int) -> Lis
|
|
| 1255 |
|
| 1256 |
return slides
|
| 1257 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1258 |
def generate_ppt_with_content(topic: str, template_name: str, custom_slides: List[Dict],
|
| 1259 |
slide_count: int, seed: int, uploaded_file,
|
| 1260 |
use_web_search: bool, theme_name: str = "미니멀 라이트",
|
|
@@ -1352,8 +1782,11 @@ def generate_ppt_with_content(topic: str, template_name: str, custom_slides: Lis
|
|
| 1352 |
"topic": topic # 표지용
|
| 1353 |
}
|
| 1354 |
|
| 1355 |
-
# 프리뷰 HTML 생성
|
| 1356 |
-
|
|
|
|
|
|
|
|
|
|
| 1357 |
|
| 1358 |
# 현재까지의 상태 업데이트
|
| 1359 |
yield preview_html + "</div>", f"### 🔄 {slide_info} 생성 중...", None
|
|
@@ -1371,15 +1804,54 @@ def generate_ppt_with_content(topic: str, template_name: str, custom_slides: Lis
|
|
| 1371 |
except Exception as e:
|
| 1372 |
print(f"[PPTX] 파일 생성 오류: {str(e)}")
|
| 1373 |
|
| 1374 |
-
# 최종
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1375 |
preview_html += "</div>"
|
| 1376 |
progress(1.0, "완료!")
|
| 1377 |
successful = sum(1 for r in results if r["success"])
|
| 1378 |
final_status = f"### 🎉 생성 완료! 총 {total_slides}개 슬라이드 중 {successful}개 성공"
|
| 1379 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1380 |
if pptx_path:
|
| 1381 |
-
final_status += f"\n\n### 📥 PPTX 파일이 준비되었습니다!
|
| 1382 |
-
final_status += f"\n\n💡
|
|
|
|
| 1383 |
|
| 1384 |
yield preview_html, final_status, pptx_path
|
| 1385 |
|
|
@@ -1410,20 +1882,31 @@ def create_custom_slides_ui():
|
|
| 1410 |
# Gradio 인터페이스 생성
|
| 1411 |
with gr.Blocks(title="PPT 이미지 생성기", theme=gr.themes.Soft(), css="""
|
| 1412 |
.preview-container { max-width: 1400px; margin: 0 auto; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1413 |
""") as demo:
|
| 1414 |
gr.Markdown("""
|
| 1415 |
-
# 🎯 AI 기반 PPT 통합 생성기 (
|
| 1416 |
|
| 1417 |
-
### 텍스트와 이미지가 완벽하게 조화된 프레젠테이션을 자동으로 생성하고 다운로드하세요!
|
| 1418 |
|
| 1419 |
#### 🆕 새로운 기능:
|
|
|
|
|
|
|
|
|
|
| 1420 |
- 🎨 **디자인 테마**: 5가지 전문적인 디자인 테마 선택 가능
|
|
|
|
| 1421 |
- 📊 **표지와 Thank You 슬라이드** 자동 추가
|
| 1422 |
- 📝 **발표자 노트** 자동 생성 (구어체)
|
| 1423 |
- 📁 **파일 업로드** 지원 (PDF/CSV/TXT)
|
| 1424 |
- 🔍 **웹 검색** 기능 (Brave Search)
|
| 1425 |
- 🎚️ **슬라이드 수 조절** (6-20장)
|
| 1426 |
-
- 💎 **입체감 있는 디자인**: 둥근 모서리와 그림자 효과
|
| 1427 |
""")
|
| 1428 |
|
| 1429 |
# API 토큰 상태 확인
|
|
@@ -1440,6 +1923,10 @@ with gr.Blocks(title="PPT 이미지 생성기", theme=gr.themes.Soft(), css="""
|
|
| 1440 |
|
| 1441 |
with gr.Row():
|
| 1442 |
with gr.Column(scale=1):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1443 |
# 기본 입력
|
| 1444 |
topic_input = gr.Textbox(
|
| 1445 |
label="프레젠테이션 주제",
|
|
@@ -1503,7 +1990,23 @@ with gr.Blocks(title="PPT 이미지 생성기", theme=gr.themes.Soft(), css="""
|
|
| 1503 |
label="시드 값"
|
| 1504 |
)
|
| 1505 |
|
| 1506 |
-
generate_btn = gr.Button("🚀 PPT
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1507 |
|
| 1508 |
# PPTX 다운��드 영역
|
| 1509 |
with gr.Row():
|
|
@@ -1540,6 +2043,11 @@ with gr.Blocks(title="PPT 이미지 생성기", theme=gr.themes.Soft(), css="""
|
|
| 1540 |
)
|
| 1541 |
|
| 1542 |
# 이벤트 핸들러
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1543 |
def update_theme_preview(theme_name):
|
| 1544 |
"""테마 미리보기 HTML 생성"""
|
| 1545 |
theme = DESIGN_THEMES.get(theme_name, DESIGN_THEMES["미니멀 라이트"])
|
|
@@ -1642,6 +2150,12 @@ with gr.Blocks(title="PPT 이미지 생성기", theme=gr.themes.Soft(), css="""
|
|
| 1642 |
yield preview, status, pptx_file
|
| 1643 |
|
| 1644 |
# 이벤트 연결
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1645 |
theme_select.change(
|
| 1646 |
fn=update_theme_preview,
|
| 1647 |
inputs=[theme_select],
|
|
@@ -1695,7 +2209,7 @@ with gr.Blocks(title="PPT 이미지 생성기", theme=gr.themes.Soft(), css="""
|
|
| 1695 |
# 앱 실행
|
| 1696 |
if __name__ == "__main__":
|
| 1697 |
print("\n" + "="*50)
|
| 1698 |
-
print("🚀 PPT 통합 생성기 (
|
| 1699 |
print("="*50)
|
| 1700 |
|
| 1701 |
# 환경 변수 확인
|
|
|
|
| 19 |
import pandas as pd
|
| 20 |
import chardet
|
| 21 |
|
| 22 |
+
# 예제 템플릿 정의
|
| 23 |
+
EXAMPLE_TOPICS = {
|
| 24 |
+
"비즈니스 제안서": "AI 기반 고객 서비스 자동화 플랫폼",
|
| 25 |
+
"제품 소개": "스마트 홈 IoT 보안 시스템",
|
| 26 |
+
"프로젝트 보고": "디지털 전환 프로젝트 3분기 성과",
|
| 27 |
+
"전략 기획": "2025년 글로벌 시장 진출 전략"
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
# 환경 변수에서 토큰 가져오기
|
| 31 |
REPLICATE_API_TOKEN = os.getenv("RAPI_TOKEN")
|
| 32 |
FRIENDLI_TOKEN = os.getenv("FRIENDLI_TOKEN")
|
| 33 |
BRAVE_API_TOKEN = os.getenv("BAPI_TOKEN")
|
| 34 |
|
| 35 |
+
# 전역 변수로 현재 슬라이드 데이터 저장
|
| 36 |
+
current_slides_data = []
|
| 37 |
+
current_topic = ""
|
| 38 |
+
current_template = ""
|
| 39 |
+
current_theme = ""
|
| 40 |
+
|
| 41 |
# 디자인 테마 정의
|
| 42 |
DESIGN_THEMES = {
|
| 43 |
"미니멀 라이트": {
|
|
|
|
| 453 |
print(f"[발표자 노트] 오류: {str(e)}")
|
| 454 |
return "이 슬라이드에서는 핵심 내용을 설명해 주세요."
|
| 455 |
|
| 456 |
+
def regenerate_slide_text(topic: str, slide_title: str, slide_context: str,
|
| 457 |
+
user_instruction: str, uploaded_content: str = None) -> Dict[str, str]:
|
| 458 |
+
"""사용자 지시사항에 따라 슬라이드 텍스트 재생성"""
|
| 459 |
+
print(f"[AI 재작성] {slide_title} - 지시사항: {user_instruction}")
|
| 460 |
+
|
| 461 |
+
url = "https://api.friendli.ai/dedicated/v1/chat/completions"
|
| 462 |
+
headers = {
|
| 463 |
+
"Authorization": f"Bearer {FRIENDLI_TOKEN}",
|
| 464 |
+
"Content-Type": "application/json"
|
| 465 |
+
}
|
| 466 |
+
|
| 467 |
+
system_prompt = """You are a professional presentation content writer.
|
| 468 |
+
Follow the user's specific instructions to rewrite slide content.
|
| 469 |
+
|
| 470 |
+
Your task is to create:
|
| 471 |
+
1. A compelling subtitle (max 10 words)
|
| 472 |
+
2. Exactly 5 bullet points, each being a complete, concise sentence
|
| 473 |
+
3. Each bullet point should be 10-15 words
|
| 474 |
+
|
| 475 |
+
Follow the user's instructions carefully while maintaining professional quality.
|
| 476 |
+
|
| 477 |
+
Output format:
|
| 478 |
+
Subtitle: [subtitle here]
|
| 479 |
+
• [Point 1]
|
| 480 |
+
• [Point 2]
|
| 481 |
+
• [Point 3]
|
| 482 |
+
• [Point 4]
|
| 483 |
+
• [Point 5]"""
|
| 484 |
+
|
| 485 |
+
user_message = f"""Topic: {topic}
|
| 486 |
+
Slide Title: {slide_title}
|
| 487 |
+
Context: {slide_context}
|
| 488 |
+
|
| 489 |
+
User's specific instruction: {user_instruction}
|
| 490 |
+
|
| 491 |
+
Rewrite the content following the instruction above."""
|
| 492 |
+
|
| 493 |
+
if uploaded_content:
|
| 494 |
+
user_message += f"\n\nReference Material:\n{uploaded_content[:1000]}"
|
| 495 |
+
|
| 496 |
+
payload = {
|
| 497 |
+
"model": "dep89a2fld32mcm",
|
| 498 |
+
"messages": [
|
| 499 |
+
{
|
| 500 |
+
"role": "system",
|
| 501 |
+
"content": system_prompt
|
| 502 |
+
},
|
| 503 |
+
{
|
| 504 |
+
"role": "user",
|
| 505 |
+
"content": user_message
|
| 506 |
+
}
|
| 507 |
+
],
|
| 508 |
+
"max_tokens": 300,
|
| 509 |
+
"temperature": 0.8,
|
| 510 |
+
"stream": False
|
| 511 |
+
}
|
| 512 |
+
|
| 513 |
+
try:
|
| 514 |
+
response = requests.post(url, json=payload, headers=headers, timeout=30)
|
| 515 |
+
if response.status_code == 200:
|
| 516 |
+
result = response.json()
|
| 517 |
+
content = result['choices'][0]['message']['content'].strip()
|
| 518 |
+
|
| 519 |
+
# Parse content
|
| 520 |
+
lines = content.split('\n')
|
| 521 |
+
subtitle = ""
|
| 522 |
+
bullet_points = []
|
| 523 |
+
|
| 524 |
+
for line in lines:
|
| 525 |
+
if line.startswith("Subtitle:"):
|
| 526 |
+
subtitle = line.replace("Subtitle:", "").strip()
|
| 527 |
+
elif line.strip().startswith("•"):
|
| 528 |
+
bullet_points.append(line.strip())
|
| 529 |
+
|
| 530 |
+
# 한글로 번역이 필요한 경우
|
| 531 |
+
if any(ord('가') <= ord(char) <= ord('힣') for char in topic):
|
| 532 |
+
subtitle = translate_content_to_korean(subtitle)
|
| 533 |
+
bullet_points = [translate_content_to_korean(point) for point in bullet_points]
|
| 534 |
+
|
| 535 |
+
return {
|
| 536 |
+
"subtitle": subtitle,
|
| 537 |
+
"bullet_points": bullet_points[:5]
|
| 538 |
+
}
|
| 539 |
+
else:
|
| 540 |
+
return {
|
| 541 |
+
"subtitle": slide_title,
|
| 542 |
+
"bullet_points": ["재생성할 수 없습니다."] * 5
|
| 543 |
+
}
|
| 544 |
+
except Exception as e:
|
| 545 |
+
print(f"[AI 재작성] 오류: {str(e)}")
|
| 546 |
+
return {
|
| 547 |
+
"subtitle": slide_title,
|
| 548 |
+
"bullet_points": ["재생성할 수 없습니다."] * 5
|
| 549 |
+
}
|
| 550 |
+
|
| 551 |
def generate_slide_content(topic: str, slide_title: str, slide_context: str, uploaded_content: str = None, web_search_results: List[Dict] = None) -> Dict[str, str]:
|
| 552 |
"""각 슬라이드의 텍스트 내용 생성"""
|
| 553 |
print(f"[슬라이드 내용] {slide_title} 텍스트 생성 중...")
|
|
|
|
| 938 |
left: 50%;
|
| 939 |
transform: translate(-50%, -50%);
|
| 940 |
text-align: center;
|
| 941 |
+
background: rgba(255, 255, 255, 0.85);
|
| 942 |
+
padding: 40px 60px;
|
| 943 |
+
border-radius: 20px;
|
| 944 |
+
box-shadow: 0 8px 32px rgba(0,0,0,0.2);
|
| 945 |
+
backdrop-filter: blur(10px);
|
| 946 |
">
|
| 947 |
+
"""
|
| 948 |
+
|
| 949 |
+
if slide_data.get('title') == '표지':
|
| 950 |
+
html += f"""
|
| 951 |
+
<h1 style="font-size: 48px; margin-bottom: 20px; color: #212529; font-weight: 700;">{slide_data.get('topic', '')}</h1>
|
| 952 |
+
<h2 style="font-size: 24px; color: #495057; font-weight: 400;">{subtitle}</h2>
|
| 953 |
+
"""
|
| 954 |
+
else: # Thank You
|
| 955 |
+
html += f"""
|
| 956 |
+
<h1 style="font-size: 60px; color: #212529; font-weight: 700;">Thank You</h1>
|
| 957 |
+
"""
|
| 958 |
+
|
| 959 |
+
html += """
|
| 960 |
</div>
|
| 961 |
"""
|
| 962 |
|
|
|
|
| 1061 |
|
| 1062 |
return html
|
| 1063 |
|
| 1064 |
+
def create_final_pptx_with_edits(results: List[Dict], topic: str, template_name: str,
|
| 1065 |
+
theme_name: str, edited_data: Dict = None) -> str:
|
| 1066 |
+
"""편집된 내용을 반영한 최종 PPTX 파일 생성"""
|
| 1067 |
+
print("[PPTX] 편집된 내용으로 최종 파일 생성 중...")
|
| 1068 |
+
|
| 1069 |
+
# 편집된 데이터가 있으면 결과에 반영
|
| 1070 |
+
if edited_data:
|
| 1071 |
+
for slide_index, edits in edited_data.items():
|
| 1072 |
+
if slide_index < len(results):
|
| 1073 |
+
results[slide_index]["slide_data"].update(edits)
|
| 1074 |
+
|
| 1075 |
+
# 기존 create_pptx_file 함수 호출
|
| 1076 |
+
return create_pptx_file(results, topic, template_name, theme_name)
|
| 1077 |
+
|
| 1078 |
def create_pptx_file(results: List[Dict], topic: str, template_name: str, theme_name: str = "미니멀 라이트") -> str:
|
| 1079 |
"""생성된 결과를 PPTX 파일로 변환 (발표자 노트 포함)"""
|
| 1080 |
print(f"[PPTX] 파일 생성 시작... 테마: {theme_name}")
|
|
|
|
| 1120 |
except Exception as e:
|
| 1121 |
print(f"[PPTX] 표지 이미지 추가 실패: {str(e)}")
|
| 1122 |
|
| 1123 |
+
# 제목 배경 박스 (반투명 - 더 밝고 투명하게)
|
| 1124 |
title_bg = slide.shapes.add_shape(
|
| 1125 |
MSO_SHAPE.ROUNDED_RECTANGLE,
|
| 1126 |
Inches(2), Inches(2.5),
|
| 1127 |
Inches(12), Inches(4)
|
| 1128 |
)
|
| 1129 |
title_bg.fill.solid()
|
| 1130 |
+
title_bg.fill.fore_color.rgb = RGBColor(255, 255, 255) # 흰색 배경
|
| 1131 |
+
title_bg.fill.transparency = 0.3 # 30% 투명도 (70% 불투명)
|
| 1132 |
title_bg.line.fill.background()
|
| 1133 |
|
| 1134 |
+
# 그림자 효과 추가
|
| 1135 |
+
shadow = title_bg.shadow
|
| 1136 |
+
shadow.visible = True
|
| 1137 |
+
shadow.distance = Pt(5)
|
| 1138 |
+
shadow.size = 100
|
| 1139 |
+
shadow.blur_radius = Pt(10)
|
| 1140 |
+
shadow.transparency = 0.5
|
| 1141 |
+
shadow.angle = 45
|
| 1142 |
+
|
| 1143 |
# 제목 텍스트 추가
|
| 1144 |
title_box = slide.shapes.add_textbox(
|
| 1145 |
Inches(2), Inches(3),
|
|
|
|
| 1150 |
title_para = title_frame.paragraphs[0]
|
| 1151 |
title_para.font.size = Pt(48)
|
| 1152 |
title_para.font.bold = True
|
| 1153 |
+
title_para.font.color.rgb = RGBColor(33, 37, 41) # 진한 회색
|
| 1154 |
title_para.alignment = PP_ALIGN.CENTER
|
| 1155 |
|
| 1156 |
# 부제목 추가
|
|
|
|
| 1162 |
subtitle_frame.text = slide_data.get('subtitle', f'{template_name} - AI 프레젠테이션')
|
| 1163 |
subtitle_para = subtitle_frame.paragraphs[0]
|
| 1164 |
subtitle_para.font.size = Pt(24)
|
| 1165 |
+
subtitle_para.font.color.rgb = RGBColor(52, 58, 64) # 중간 회색
|
| 1166 |
subtitle_para.alignment = PP_ALIGN.CENTER
|
| 1167 |
|
| 1168 |
# Thank You 슬라이드
|
|
|
|
| 1187 |
except Exception as e:
|
| 1188 |
print(f"[PPTX] Thank You 이미지 추가 실패: {str(e)}")
|
| 1189 |
|
| 1190 |
+
# Thank You 배경 박스 (반투명 - 더 밝고 투명하게)
|
| 1191 |
thanks_bg = slide.shapes.add_shape(
|
| 1192 |
MSO_SHAPE.ROUNDED_RECTANGLE,
|
| 1193 |
Inches(3), Inches(3),
|
| 1194 |
Inches(10), Inches(3)
|
| 1195 |
)
|
| 1196 |
thanks_bg.fill.solid()
|
| 1197 |
+
thanks_bg.fill.fore_color.rgb = RGBColor(255, 255, 255) # 흰색 배경
|
| 1198 |
+
thanks_bg.fill.transparency = 0.3 # 30% 투명도
|
| 1199 |
thanks_bg.line.fill.background()
|
| 1200 |
|
| 1201 |
+
# 그림자 효과 추가
|
| 1202 |
+
shadow = thanks_bg.shadow
|
| 1203 |
+
shadow.visible = True
|
| 1204 |
+
shadow.distance = Pt(5)
|
| 1205 |
+
shadow.size = 100
|
| 1206 |
+
shadow.blur_radius = Pt(10)
|
| 1207 |
+
shadow.transparency = 0.5
|
| 1208 |
+
shadow.angle = 45
|
| 1209 |
+
|
| 1210 |
# Thank You 텍스트
|
| 1211 |
thanks_box = slide.shapes.add_textbox(
|
| 1212 |
Inches(2), Inches(3.5),
|
|
|
|
| 1217 |
thanks_para = thanks_frame.paragraphs[0]
|
| 1218 |
thanks_para.font.size = Pt(60)
|
| 1219 |
thanks_para.font.bold = True
|
| 1220 |
+
thanks_para.font.color.rgb = RGBColor(33, 37, 41) # 진한 회색
|
| 1221 |
thanks_para.alignment = PP_ALIGN.CENTER
|
| 1222 |
|
| 1223 |
# 일반 슬라이드
|
|
|
|
| 1410 |
|
| 1411 |
return slides
|
| 1412 |
|
| 1413 |
+
def create_editable_slide_interface(slide_data: Dict, slide_index: int) -> str:
|
| 1414 |
+
"""편집 가능한 슬라이드 인터페이스 생성"""
|
| 1415 |
+
|
| 1416 |
+
# 이미지를 base64로 인코딩
|
| 1417 |
+
img_base64 = ""
|
| 1418 |
+
if slide_data.get("image"):
|
| 1419 |
+
buffered = BytesIO()
|
| 1420 |
+
slide_data["image"].save(buffered, format="PNG")
|
| 1421 |
+
img_base64 = base64.b64encode(buffered.getvalue()).decode()
|
| 1422 |
+
|
| 1423 |
+
# 텍스트 내용 가져오기
|
| 1424 |
+
subtitle = slide_data.get("subtitle", "")
|
| 1425 |
+
bullet_points = slide_data.get("bullet_points", [])
|
| 1426 |
+
|
| 1427 |
+
# 표지와 Thank You는 편집 불가
|
| 1428 |
+
if slide_data.get('title') in ['표지', 'Thank You']:
|
| 1429 |
+
return create_slide_preview_html(slide_data)
|
| 1430 |
+
|
| 1431 |
+
# HTML 생성
|
| 1432 |
+
html = f"""
|
| 1433 |
+
<div class="slide-container" style="
|
| 1434 |
+
width: 100%;
|
| 1435 |
+
max-width: 1200px;
|
| 1436 |
+
margin: 20px auto;
|
| 1437 |
+
background: white;
|
| 1438 |
+
border-radius: 12px;
|
| 1439 |
+
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
|
| 1440 |
+
overflow: hidden;
|
| 1441 |
+
">
|
| 1442 |
+
<div class="slide-header" style="
|
| 1443 |
+
background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%);
|
| 1444 |
+
color: white;
|
| 1445 |
+
padding: 15px 30px;
|
| 1446 |
+
font-size: 18px;
|
| 1447 |
+
font-weight: bold;
|
| 1448 |
+
display: flex;
|
| 1449 |
+
justify-content: space-between;
|
| 1450 |
+
align-items: center;
|
| 1451 |
+
">
|
| 1452 |
+
<span>슬라이드 {slide_data.get('slide_number', '')}: {slide_data.get('title', '')}</span>
|
| 1453 |
+
<button onclick="toggleEdit_{slide_index}()" style="
|
| 1454 |
+
background: #3498db;
|
| 1455 |
+
color: white;
|
| 1456 |
+
border: none;
|
| 1457 |
+
padding: 5px 15px;
|
| 1458 |
+
border-radius: 5px;
|
| 1459 |
+
cursor: pointer;
|
| 1460 |
+
font-size: 14px;
|
| 1461 |
+
">✏️ 편집</button>
|
| 1462 |
+
</div>
|
| 1463 |
+
|
| 1464 |
+
<div class="slide-content" style="
|
| 1465 |
+
display: flex;
|
| 1466 |
+
min-height: 400px;
|
| 1467 |
+
background: #fafbfc;
|
| 1468 |
+
position: relative;
|
| 1469 |
+
">
|
| 1470 |
+
<div class="slide-inner" style="
|
| 1471 |
+
width: 100%;
|
| 1472 |
+
display: flex;
|
| 1473 |
+
padding: 20px;
|
| 1474 |
+
gap: 20px;
|
| 1475 |
+
">
|
| 1476 |
+
<!-- 텍스트 영역 (좌측) -->
|
| 1477 |
+
<div class="text-area" style="
|
| 1478 |
+
flex: 1;
|
| 1479 |
+
padding: 30px;
|
| 1480 |
+
background: rgba(255, 255, 255, 0.95);
|
| 1481 |
+
border-radius: 12px;
|
| 1482 |
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.08);
|
| 1483 |
+
">
|
| 1484 |
+
<div id="view_mode_{slide_index}">
|
| 1485 |
+
<h2 style="
|
| 1486 |
+
color: #212529;
|
| 1487 |
+
font-size: 28px;
|
| 1488 |
+
margin-bottom: 30px;
|
| 1489 |
+
font-weight: 600;
|
| 1490 |
+
">{subtitle}</h2>
|
| 1491 |
+
|
| 1492 |
+
<ul style="
|
| 1493 |
+
list-style: none;
|
| 1494 |
+
padding: 0;
|
| 1495 |
+
margin: 0;
|
| 1496 |
+
">
|
| 1497 |
+
"""
|
| 1498 |
+
|
| 1499 |
+
for point in bullet_points:
|
| 1500 |
+
html += f"""
|
| 1501 |
+
<li style="
|
| 1502 |
+
margin-bottom: 16px;
|
| 1503 |
+
padding-left: 28px;
|
| 1504 |
+
position: relative;
|
| 1505 |
+
color: #495057;
|
| 1506 |
+
font-size: 16px;
|
| 1507 |
+
line-height: 1.6;
|
| 1508 |
+
">
|
| 1509 |
+
<span style="
|
| 1510 |
+
position: absolute;
|
| 1511 |
+
left: 0;
|
| 1512 |
+
color: #007bff;
|
| 1513 |
+
font-size: 18px;
|
| 1514 |
+
">•</span>
|
| 1515 |
+
{point.replace('•', '').strip()}
|
| 1516 |
+
</li>
|
| 1517 |
+
"""
|
| 1518 |
+
|
| 1519 |
+
html += f"""
|
| 1520 |
+
</ul>
|
| 1521 |
+
</div>
|
| 1522 |
+
|
| 1523 |
+
<!-- 편집 모드 -->
|
| 1524 |
+
<div id="edit_mode_{slide_index}" style="display: none;">
|
| 1525 |
+
<input type="text" id="subtitle_{slide_index}" value="{subtitle}" style="
|
| 1526 |
+
width: 100%;
|
| 1527 |
+
font-size: 24px;
|
| 1528 |
+
font-weight: 600;
|
| 1529 |
+
margin-bottom: 20px;
|
| 1530 |
+
padding: 10px;
|
| 1531 |
+
border: 2px solid #e9ecef;
|
| 1532 |
+
border-radius: 5px;
|
| 1533 |
+
" placeholder="소제목 입력">
|
| 1534 |
+
|
| 1535 |
+
<div style="margin-bottom: 20px;">
|
| 1536 |
+
"""
|
| 1537 |
+
|
| 1538 |
+
for i, point in enumerate(bullet_points):
|
| 1539 |
+
clean_point = point.replace('•', '').strip()
|
| 1540 |
+
html += f"""
|
| 1541 |
+
<input type="text" id="bullet_{slide_index}_{i}" value="{clean_point}" style="
|
| 1542 |
+
width: 100%;
|
| 1543 |
+
margin-bottom: 10px;
|
| 1544 |
+
padding: 8px 8px 8px 25px;
|
| 1545 |
+
border: 1px solid #e9ecef;
|
| 1546 |
+
border-radius: 5px;
|
| 1547 |
+
font-size: 16px;
|
| 1548 |
+
" placeholder="포인트 {i+1}">
|
| 1549 |
+
"""
|
| 1550 |
+
|
| 1551 |
+
html += f"""
|
| 1552 |
+
</div>
|
| 1553 |
+
|
| 1554 |
+
<div style="
|
| 1555 |
+
background: #f3e5f5;
|
| 1556 |
+
border-radius: 8px;
|
| 1557 |
+
padding: 15px;
|
| 1558 |
+
margin-top: 20px;
|
| 1559 |
+
">
|
| 1560 |
+
<h4 style="margin-bottom: 10px; color: #7b1fa2;">🤖 AI로 다시 작성하기</h4>
|
| 1561 |
+
<textarea id="instruction_{slide_index}" style="
|
| 1562 |
+
width: 100%;
|
| 1563 |
+
height: 60px;
|
| 1564 |
+
padding: 10px;
|
| 1565 |
+
border: 2px solid #e1bee7;
|
| 1566 |
+
border-radius: 5px;
|
| 1567 |
+
font-size: 14px;
|
| 1568 |
+
resize: vertical;
|
| 1569 |
+
background: white;
|
| 1570 |
+
" placeholder="AI에게 지시사항 입력 (예: 더 전문적으로, 숫자 포함, 간단하게 등)"></textarea>
|
| 1571 |
+
<button onclick="regenerateText_{slide_index}()" style="
|
| 1572 |
+
background: #9b59b6;
|
| 1573 |
+
color: white;
|
| 1574 |
+
border: none;
|
| 1575 |
+
padding: 8px 20px;
|
| 1576 |
+
border-radius: 5px;
|
| 1577 |
+
cursor: pointer;
|
| 1578 |
+
margin-top: 10px;
|
| 1579 |
+
font-size: 14px;
|
| 1580 |
+
">🔄 AI 새로 작성</button>
|
| 1581 |
+
</div>
|
| 1582 |
+
|
| 1583 |
+
<div style="margin-top: 20px;">
|
| 1584 |
+
<button onclick="saveEdit_{slide_index}()" style="
|
| 1585 |
+
background: #27ae60;
|
| 1586 |
+
color: white;
|
| 1587 |
+
border: none;
|
| 1588 |
+
padding: 8px 20px;
|
| 1589 |
+
border-radius: 5px;
|
| 1590 |
+
cursor: pointer;
|
| 1591 |
+
margin-right: 10px;
|
| 1592 |
+
">저장</button>
|
| 1593 |
+
<button onclick="cancelEdit_{slide_index}()" style="
|
| 1594 |
+
background: #95a5a6;
|
| 1595 |
+
color: white;
|
| 1596 |
+
border: none;
|
| 1597 |
+
padding: 8px 20px;
|
| 1598 |
+
border-radius: 5px;
|
| 1599 |
+
cursor: pointer;
|
| 1600 |
+
">취소</button>
|
| 1601 |
+
</div>
|
| 1602 |
+
</div>
|
| 1603 |
+
</div>
|
| 1604 |
+
|
| 1605 |
+
<!-- 이미지 영역 (우측) -->
|
| 1606 |
+
<div class="image-area" style="
|
| 1607 |
+
flex: 1;
|
| 1608 |
+
background: rgba(248, 249, 250, 0.9);
|
| 1609 |
+
border-radius: 12px;
|
| 1610 |
+
display: flex;
|
| 1611 |
+
align-items: center;
|
| 1612 |
+
justify-content: center;
|
| 1613 |
+
padding: 20px;
|
| 1614 |
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05);
|
| 1615 |
+
">
|
| 1616 |
+
"""
|
| 1617 |
+
|
| 1618 |
+
if img_base64:
|
| 1619 |
+
html += f"""
|
| 1620 |
+
<img src="data:image/png;base64,{img_base64}" style="
|
| 1621 |
+
max-width: 100%;
|
| 1622 |
+
max-height: 100%;
|
| 1623 |
+
object-fit: contain;
|
| 1624 |
+
border-radius: 8px;
|
| 1625 |
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
| 1626 |
+
" alt="Slide Image">
|
| 1627 |
+
"""
|
| 1628 |
+
|
| 1629 |
+
html += f"""
|
| 1630 |
+
</div>
|
| 1631 |
+
</div>
|
| 1632 |
+
</div>
|
| 1633 |
+
</div>
|
| 1634 |
+
|
| 1635 |
+
<script>
|
| 1636 |
+
function toggleEdit_{slide_index}() {{
|
| 1637 |
+
var viewMode = document.getElementById('view_mode_{slide_index}');
|
| 1638 |
+
var editMode = document.getElementById('edit_mode_{slide_index}');
|
| 1639 |
+
|
| 1640 |
+
if (viewMode.style.display === 'none') {{
|
| 1641 |
+
viewMode.style.display = 'block';
|
| 1642 |
+
editMode.style.display = 'none';
|
| 1643 |
+
}} else {{
|
| 1644 |
+
viewMode.style.display = 'none';
|
| 1645 |
+
editMode.style.display = 'block';
|
| 1646 |
+
}}
|
| 1647 |
+
}}
|
| 1648 |
+
|
| 1649 |
+
function saveEdit_{slide_index}() {{
|
| 1650 |
+
// 편집된 내용을 저장하는 로직
|
| 1651 |
+
var subtitle = document.getElementById('subtitle_{slide_index}').value;
|
| 1652 |
+
var bullets = [];
|
| 1653 |
+
for (var i = 0; i < 5; i++) {{
|
| 1654 |
+
var bullet = document.getElementById('bullet_{slide_index}_' + i);
|
| 1655 |
+
if (bullet) bullets.push(bullet.value);
|
| 1656 |
+
}}
|
| 1657 |
+
|
| 1658 |
+
// 저장 후 뷰 모드로 전환
|
| 1659 |
+
toggleEdit_{slide_index}();
|
| 1660 |
+
|
| 1661 |
+
// 실제 저장은 Gradio 이벤트로 처리
|
| 1662 |
+
window.savedSlideData = window.savedSlideData || {{}};
|
| 1663 |
+
window.savedSlideData[{slide_index}] = {{
|
| 1664 |
+
subtitle: subtitle,
|
| 1665 |
+
bullet_points: bullets.map(b => '• ' + b)
|
| 1666 |
+
}};
|
| 1667 |
+
}}
|
| 1668 |
+
|
| 1669 |
+
function cancelEdit_{slide_index}() {{
|
| 1670 |
+
toggleEdit_{slide_index}();
|
| 1671 |
+
}}
|
| 1672 |
+
|
| 1673 |
+
function regenerateText_{slide_index}() {{
|
| 1674 |
+
var instruction = document.getElementById('instruction_{slide_index}').value;
|
| 1675 |
+
if (instruction) {{
|
| 1676 |
+
// AI 재생성 요청
|
| 1677 |
+
window.regenerateRequest = {{
|
| 1678 |
+
slideIndex: {slide_index},
|
| 1679 |
+
instruction: instruction
|
| 1680 |
+
}};
|
| 1681 |
+
}}
|
| 1682 |
+
}}
|
| 1683 |
+
</script>
|
| 1684 |
+
"""
|
| 1685 |
+
|
| 1686 |
+
return html
|
| 1687 |
+
|
| 1688 |
def generate_ppt_with_content(topic: str, template_name: str, custom_slides: List[Dict],
|
| 1689 |
slide_count: int, seed: int, uploaded_file,
|
| 1690 |
use_web_search: bool, theme_name: str = "미니멀 라이트",
|
|
|
|
| 1782 |
"topic": topic # 표지용
|
| 1783 |
}
|
| 1784 |
|
| 1785 |
+
# 프리뷰 HTML 생성 (편집 가능한 인터페이스)
|
| 1786 |
+
if slide_data.get('title') not in ['표지', 'Thank You']:
|
| 1787 |
+
preview_html += create_editable_slide_interface(slide_data, i)
|
| 1788 |
+
else:
|
| 1789 |
+
preview_html += create_slide_preview_html(slide_data)
|
| 1790 |
|
| 1791 |
# 현재까지의 상태 업데이트
|
| 1792 |
yield preview_html + "</div>", f"### 🔄 {slide_info} 생성 중...", None
|
|
|
|
| 1804 |
except Exception as e:
|
| 1805 |
print(f"[PPTX] 파일 생성 오류: {str(e)}")
|
| 1806 |
|
| 1807 |
+
# 최종 결과에 편집 가능한 슬라이드 데이터 저장
|
| 1808 |
+
preview_html += """
|
| 1809 |
+
<div style="margin: 40px auto; max-width: 1200px; text-align: center; padding: 30px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 15px; box-shadow: 0 10px 30px rgba(0,0,0,0.3);">
|
| 1810 |
+
<h3 style="color: white; margin-bottom: 20px;">✨ 모든 편집이 완료되었나요?</h3>
|
| 1811 |
+
<button id="finalDownloadBtn" onclick="prepareFinalDownload()" style="
|
| 1812 |
+
background: white;
|
| 1813 |
+
color: #667eea;
|
| 1814 |
+
border: none;
|
| 1815 |
+
padding: 18px 50px;
|
| 1816 |
+
border-radius: 50px;
|
| 1817 |
+
cursor: pointer;
|
| 1818 |
+
font-size: 20px;
|
| 1819 |
+
font-weight: bold;
|
| 1820 |
+
box-shadow: 0 5px 15px rgba(0,0,0,0.3);
|
| 1821 |
+
transition: all 0.3s ease;
|
| 1822 |
+
" onmouseover="this.style.transform='scale(1.05)'" onmouseout="this.style.transform='scale(1)'">
|
| 1823 |
+
💾 편집 내용 반영하여 최종 다운로드
|
| 1824 |
+
</button>
|
| 1825 |
+
<p style="color: white; margin-top: 15px; font-size: 14px;">
|
| 1826 |
+
💡 편집한 내용이 최종 PPT에 반영됩니다
|
| 1827 |
+
</p>
|
| 1828 |
+
</div>
|
| 1829 |
+
|
| 1830 |
+
<script>
|
| 1831 |
+
function prepareFinalDownload() {
|
| 1832 |
+
// 편집된 모든 데이터 수집
|
| 1833 |
+
alert('편집된 내용을 반영한 최종 PPT를 다운로드합니다.');
|
| 1834 |
+
// 실제 구현에서는 Gradio 이벤트로 처리
|
| 1835 |
+
}
|
| 1836 |
+
</script>
|
| 1837 |
+
"""
|
| 1838 |
+
|
| 1839 |
preview_html += "</div>"
|
| 1840 |
progress(1.0, "완료!")
|
| 1841 |
successful = sum(1 for r in results if r["success"])
|
| 1842 |
final_status = f"### 🎉 생성 완료! 총 {total_slides}개 슬라이드 중 {successful}개 성공"
|
| 1843 |
|
| 1844 |
+
# 편집 가능한 슬라이드 데이터를 전역 변수로 저장
|
| 1845 |
+
global current_slides_data, current_topic, current_template, current_theme
|
| 1846 |
+
current_slides_data = results
|
| 1847 |
+
current_topic = topic
|
| 1848 |
+
current_template = template_name
|
| 1849 |
+
current_theme = theme_name
|
| 1850 |
+
|
| 1851 |
if pptx_path:
|
| 1852 |
+
final_status += f"\n\n### 📥 PPTX 파일이 준비되었습니다!"
|
| 1853 |
+
final_status += f"\n\n💡 **각 슬라이드를 편집한 후 '편집 완료 후 최종 다운로드' 버튼을 클릭하세요**"
|
| 1854 |
+
final_status += f"\n\n🎤 **발표자 노트가 각 슬라이드에 포함되어 있습니다!**"
|
| 1855 |
|
| 1856 |
yield preview_html, final_status, pptx_path
|
| 1857 |
|
|
|
|
| 1882 |
# Gradio 인터페이스 생성
|
| 1883 |
with gr.Blocks(title="PPT 이미지 생성기", theme=gr.themes.Soft(), css="""
|
| 1884 |
.preview-container { max-width: 1400px; margin: 0 auto; }
|
| 1885 |
+
.slide-container { transition: all 0.3s ease; }
|
| 1886 |
+
.slide-container:hover { transform: translateY(-2px); box-shadow: 0 12px 24px rgba(0, 0, 0, 0.15) !important; }
|
| 1887 |
+
input[type="text"], textarea { transition: all 0.3s ease; }
|
| 1888 |
+
input[type="text"]:focus, textarea:focus { border-color: #3498db !important; outline: none; box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.2); }
|
| 1889 |
+
button { transition: all 0.3s ease; }
|
| 1890 |
+
button:hover { transform: translateY(-1px); box-shadow: 0 4px 8px rgba(0,0,0,0.2); opacity: 0.9; }
|
| 1891 |
+
.edit-mode { background: #f8f9fa; padding: 20px; border-radius: 8px; }
|
| 1892 |
+
.ai-instruction { background: #f3e5f5; padding: 15px; border-radius: 8px; margin-top: 15px; }
|
| 1893 |
""") as demo:
|
| 1894 |
gr.Markdown("""
|
| 1895 |
+
# 🎯 AI 기반 PPT 통합 생성기 (편집 가능 버전)
|
| 1896 |
|
| 1897 |
+
### 텍스트와 이미지가 완벽하게 조화된 프레젠테이션을 자동으로 생성하고 편집 후 다운로드하세요!
|
| 1898 |
|
| 1899 |
#### 🆕 새로운 기능:
|
| 1900 |
+
- ✏️ **슬라이드 텍스트 편집**: 생성된 각 슬라이드의 텍스트를 직접 수정
|
| 1901 |
+
- 🤖 **AI 재작성**: 지시사항을 입력하면 AI가 텍스트를 다시 작성
|
| 1902 |
+
- 📋 **예제 템플릿**: 버튼 클릭으로 예제 주제 불러오기
|
| 1903 |
- 🎨 **디자인 테마**: 5가지 전문적인 디자인 테마 선택 가능
|
| 1904 |
+
- 💎 **개선된 표지 디자인**: 반투명 배경과 선명한 텍스트
|
| 1905 |
- 📊 **표지와 Thank You 슬라이드** 자동 추가
|
| 1906 |
- 📝 **발표자 노트** 자동 생성 (구어체)
|
| 1907 |
- 📁 **파일 업로드** 지원 (PDF/CSV/TXT)
|
| 1908 |
- 🔍 **웹 검색** 기능 (Brave Search)
|
| 1909 |
- 🎚️ **슬라이드 수 조절** (6-20장)
|
|
|
|
| 1910 |
""")
|
| 1911 |
|
| 1912 |
# API 토큰 상태 확인
|
|
|
|
| 1923 |
|
| 1924 |
with gr.Row():
|
| 1925 |
with gr.Column(scale=1):
|
| 1926 |
+
# 예제 선택
|
| 1927 |
+
with gr.Row():
|
| 1928 |
+
example_btn = gr.Button("📋 예제 불러오기", size="sm", variant="secondary")
|
| 1929 |
+
|
| 1930 |
# 기본 입력
|
| 1931 |
topic_input = gr.Textbox(
|
| 1932 |
label="프레젠테이션 주제",
|
|
|
|
| 1990 |
label="시드 값"
|
| 1991 |
)
|
| 1992 |
|
| 1993 |
+
generate_btn = gr.Button("🚀 PPT 생성 시작 (AI가 모든 내용을 자동 생성)", variant="primary", size="lg")
|
| 1994 |
+
|
| 1995 |
+
# 사용 방법 안내
|
| 1996 |
+
with gr.Accordion("📖 사용 방법", open=False):
|
| 1997 |
+
gr.Markdown("""
|
| 1998 |
+
### 🔄 작업 순서:
|
| 1999 |
+
1. **예제 불러오기** 버튼으로 샘플 주제를 로드하거나 직접 입력
|
| 2000 |
+
2. **템플릿과 테마 선택** 후 생성 버튼 클릭
|
| 2001 |
+
3. **각 슬라이드 편집**: ✏️ 편집 버튼으로 텍스트 수정
|
| 2002 |
+
4. **AI 재작성**: 지시사항 입력 후 AI로 다시 작성
|
| 2003 |
+
5. **최종 다운로드**: 편집 완료 후 다운로드
|
| 2004 |
+
|
| 2005 |
+
### 💡 편집 팁:
|
| 2006 |
+
- 각 슬라이드의 편집 버튼을 클릭하여 텍스트 수정
|
| 2007 |
+
- AI에게 "더 간단하게", "숫자 포함", "전문적으로" 등 지시
|
| 2008 |
+
- 편집 후 저장 버튼을 클릭해야 반영됨
|
| 2009 |
+
""")
|
| 2010 |
|
| 2011 |
# PPTX 다운��드 영역
|
| 2012 |
with gr.Row():
|
|
|
|
| 2043 |
)
|
| 2044 |
|
| 2045 |
# 이벤트 핸들러
|
| 2046 |
+
def load_example(template_name):
|
| 2047 |
+
"""템플릿에 맞는 예제 주제 로드"""
|
| 2048 |
+
example_topic = EXAMPLE_TOPICS.get(template_name, "")
|
| 2049 |
+
return example_topic
|
| 2050 |
+
|
| 2051 |
def update_theme_preview(theme_name):
|
| 2052 |
"""테마 미리보기 HTML 생성"""
|
| 2053 |
theme = DESIGN_THEMES.get(theme_name, DESIGN_THEMES["미니멀 라이트"])
|
|
|
|
| 2150 |
yield preview, status, pptx_file
|
| 2151 |
|
| 2152 |
# 이벤트 연결
|
| 2153 |
+
example_btn.click(
|
| 2154 |
+
fn=load_example,
|
| 2155 |
+
inputs=[template_select],
|
| 2156 |
+
outputs=[topic_input]
|
| 2157 |
+
)
|
| 2158 |
+
|
| 2159 |
theme_select.change(
|
| 2160 |
fn=update_theme_preview,
|
| 2161 |
inputs=[theme_select],
|
|
|
|
| 2209 |
# 앱 실행
|
| 2210 |
if __name__ == "__main__":
|
| 2211 |
print("\n" + "="*50)
|
| 2212 |
+
print("🚀 PPT 통합 생성기 (편집 가능 버전) 시작!")
|
| 2213 |
print("="*50)
|
| 2214 |
|
| 2215 |
# 환경 변수 확인
|