ginipick commited on
Commit
8c7b732
·
verified ·
1 Parent(s): 7a610d2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +537 -23
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
- color: white;
833
- text-shadow: 2px 2px 4px rgba(0,0,0,0.8);
 
 
 
834
  ">
835
- <h1 style="font-size: 48px; margin-bottom: 20px;">{slide_data.get('topic', '')}</h1>
836
- <h2 style="font-size: 24px;">{subtitle}</h2>
 
 
 
 
 
 
 
 
 
 
 
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(0, 0, 0)
994
- title_bg.fill.transparency = 0.5
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(255, 255, 255)
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(255, 255, 255)
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(0, 0, 0)
1052
- thanks_bg.fill.transparency = 0.5
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(255, 255, 255)
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
- preview_html += create_slide_preview_html(slide_data)
 
 
 
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 전체 생성 (텍스트 + 이미지 + 발표노트)", variant="primary", size="lg")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
  # 환경 변수 확인