ginipick commited on
Commit
393e229
Β·
verified Β·
1 Parent(s): 8c7b732

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +525 -204
app.py CHANGED
@@ -1,4 +1,3 @@
1
- import gradio as gr
2
  import replicate
3
  import requests
4
  import os
@@ -18,13 +17,14 @@ from pptx.enum.shapes import MSO_SHAPE
18
  import PyPDF2
19
  import pandas as pd
20
  import chardet
 
21
 
22
  # 예제 ν…œν”Œλ¦Ώ μ •μ˜
23
  EXAMPLE_TOPICS = {
24
- "λΉ„μ¦ˆλ‹ˆμŠ€ μ œμ•ˆμ„œ": "AI 기반 고객 μ„œλΉ„μŠ€ μžλ™ν™” ν”Œλž«νΌ",
25
- "μ œν’ˆ μ†Œκ°œ": "슀마트 ν™ˆ IoT λ³΄μ•ˆ μ‹œμŠ€ν…œ",
26
- "ν”„λ‘œμ νŠΈ 보고": "λ””μ§€ν„Έ μ „ν™˜ ν”„λ‘œμ νŠΈ 3λΆ„κΈ° μ„±κ³Ό",
27
- "μ „λž΅ 기획": "2025λ…„ κΈ€λ‘œλ²Œ μ‹œμž₯ μ§„μΆœ μ „λž΅"
28
  }
29
 
30
  # ν™˜κ²½ λ³€μˆ˜μ—μ„œ 토큰 κ°€μ Έμ˜€κΈ°
@@ -116,10 +116,10 @@ STYLE_TEMPLATES = {
116
  "example": "A dramatic wide-angle view of a modern glass skyscraper reaching into clouds with golden sunset lighting, symbolizing growth and ambition. Ultra-realistic photography style, cinematic composition, lens flare, professional corporate aesthetic"
117
  },
118
  "Thank You Slide": {
119
- "name": "Thank You",
120
- "description": "Elegant closing slide design",
121
- "use_case": "λ§ˆμ§€λ§‰ 인사",
122
- "example": "Abstract elegant background with soft gradient from deep blue to purple, golden particles floating like celebration confetti, subtle light rays, with space for 'Thank You' text. Minimalist, professional, warm feeling"
123
  },
124
  "3D Style (Pixar-like)": {
125
  "name": "3D Style",
@@ -396,14 +396,18 @@ def generate_presentation_notes(topic: str, slide_title: str, content: Dict) ->
396
 
397
  system_prompt = """You are a professional presentation coach who creates natural, conversational speaker notes.
398
 
 
 
399
  Create speaker notes that:
400
  1. Sound natural and conversational, as if speaking to an audience
401
- 2. Include transitions and engagement phrases
402
- 3. Reference the slide content but expand with additional context
403
  4. Use a warm, professional tone
404
  5. Be 100-150 words long
405
  6. Include pauses and emphasis markers where appropriate
406
 
 
 
407
  Format:
408
  - Use conversational language
409
  - Include transition phrases
@@ -414,10 +418,10 @@ Format:
414
  user_message = f"""Topic: {topic}
415
  Slide Title: {slide_title}
416
  Subtitle: {content.get('subtitle', '')}
417
- Key Points:
418
  {bullet_text}
419
 
420
- Create natural speaker notes for presenting this slide."""
421
 
422
  payload = {
423
  "model": "dep89a2fld32mcm",
@@ -444,7 +448,7 @@ Create natural speaker notes for presenting this slide."""
444
 
445
  # ν•œκΈ€ 주제인 경우 λ²ˆμ—­
446
  if any(ord('κ°€') <= ord(char) <= ord('힣') for char in topic):
447
- notes = translate_content_to_korean(notes)
448
 
449
  return notes
450
  else:
@@ -469,18 +473,21 @@ 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}
@@ -488,7 +495,7 @@ 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]}"
@@ -530,7 +537,19 @@ Rewrite the content following the instruction above."""
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,
@@ -539,15 +558,142 @@ Rewrite the content following the instruction above."""
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} ν…μŠ€νŠΈ 생성 쀑...")
@@ -562,24 +708,36 @@ def generate_slide_content(topic: str, slide_title: str, slide_context: str, upl
562
 
563
  Your task is to create:
564
  1. A compelling subtitle (max 10 words)
565
- 2. Exactly 5 bullet points, each being a complete, concise sentence
566
- 3. Each bullet point should be 10-15 words
 
 
 
 
 
 
 
 
567
 
568
- Guidelines:
569
- - Be specific and actionable
570
- - Use professional business language
571
- - Include relevant data points or metrics when appropriate
572
- - Ensure content aligns with the slide's purpose
573
- - Make each point distinct and valuable
574
- - Use active voice and strong verbs
 
 
 
 
575
 
576
  Output format:
577
  Subtitle: [subtitle here]
578
- β€’ [Point 1]
579
- β€’ [Point 2]
580
- β€’ [Point 3]
581
- β€’ [Point 4]
582
- β€’ [Point 5]"""
583
 
584
  user_message = f"""Topic: {topic}
585
  Slide Title: {slide_title}
@@ -596,7 +754,7 @@ Context: {slide_context}"""
596
  search_context += f"- {result['title']}: {result['description']}\n"
597
  user_message += search_context
598
 
599
- user_message += "\n\nCreate compelling content for this presentation slide."
600
 
601
  payload = {
602
  "model": "dep89a2fld32mcm",
@@ -636,7 +794,19 @@ Context: {slide_context}"""
636
  # ν•œκΈ€λ‘œ λ²ˆμ—­μ΄ ν•„μš”ν•œ 경우
637
  if any(ord('κ°€') <= ord(char) <= ord('힣') for char in topic):
638
  subtitle = translate_content_to_korean(subtitle)
639
- bullet_points = [translate_content_to_korean(point) for point in bullet_points]
 
 
 
 
 
 
 
 
 
 
 
 
640
 
641
  return {
642
  "subtitle": subtitle,
@@ -645,17 +815,17 @@ Context: {slide_context}"""
645
  else:
646
  return {
647
  "subtitle": slide_title,
648
- "bullet_points": ["λ‚΄μš©μ„ 생성할 수 μ—†μŠ΅λ‹ˆλ‹€."] * 5
649
  }
650
  except Exception as e:
651
  print(f"[μŠ¬λΌμ΄λ“œ λ‚΄μš©] 였λ₯˜: {str(e)}")
652
  return {
653
  "subtitle": slide_title,
654
- "bullet_points": ["λ‚΄μš©μ„ 생성할 수 μ—†μŠ΅λ‹ˆλ‹€."] * 5
655
  }
656
 
657
  def translate_content_to_korean(text: str) -> str:
658
- """μ˜μ–΄ ν…μŠ€νŠΈλ₯Ό ν•œκΈ€λ‘œ λ²ˆμ—­"""
659
  url = "https://api.friendli.ai/dedicated/v1/chat/completions"
660
  headers = {
661
  "Authorization": f"Bearer {FRIENDLI_TOKEN}",
@@ -689,6 +859,83 @@ def translate_content_to_korean(text: str) -> str:
689
  except Exception as e:
690
  return text
691
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
692
  def generate_prompt_with_llm(topic: str, style_example: str = None, slide_context: str = None, uploaded_content: str = None) -> str:
693
  """μ£Όμ œμ™€ μŠ€νƒ€μΌ 예제λ₯Ό λ°›μ•„μ„œ LLM을 μ‚¬μš©ν•΄ 이미지 ν”„λ‘¬ν”„νŠΈλ₯Ό 생성"""
694
  print(f"[LLM] ν”„λ‘¬ν”„νŠΈ 생성 μ‹œμž‘: {slide_context}")
@@ -938,22 +1185,22 @@ def create_slide_preview_html(slide_data: Dict) -> str:
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 += """
@@ -992,6 +1239,7 @@ def create_slide_preview_html(slide_data: Dict) -> str:
992
  """
993
 
994
  for point in bullet_points:
 
995
  html += f"""
996
  <li style="
997
  margin-bottom: 16px;
@@ -1007,7 +1255,7 @@ def create_slide_preview_html(slide_data: Dict) -> str:
1007
  color: #007bff;
1008
  font-size: 18px;
1009
  ">β€’</span>
1010
- {point.replace('β€’', '').strip()}
1011
  </li>
1012
  """
1013
 
@@ -1123,46 +1371,46 @@ def create_pptx_file(results: List[Dict], topic: str, template_name: str, theme_
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),
1146
- Inches(12), Inches(3)
1147
  )
1148
  title_frame = title_box.text_frame
1149
  title_frame.text = topic
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
- # λΆ€μ œλͺ© μΆ”κ°€
1157
  subtitle_box = slide.shapes.add_textbox(
1158
- Inches(2), Inches(6),
1159
- Inches(12), Inches(2)
1160
  )
1161
  subtitle_frame = subtitle_box.text_frame
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 μŠ¬λΌμ΄λ“œ
@@ -1190,34 +1438,34 @@ def create_pptx_file(results: List[Dict], topic: str, template_name: str, theme_
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),
1213
- Inches(12), Inches(2)
1214
  )
1215
  thanks_frame = thanks_box.text_frame
1216
- thanks_frame.text = "Thank You"
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
  # 일반 μŠ¬λΌμ΄λ“œ
@@ -1303,12 +1551,17 @@ def create_pptx_file(results: List[Dict], topic: str, template_name: str, theme_
1303
  bullet_points = slide_data.get('bullet_points', [])
1304
  for point in bullet_points:
1305
  p = text_frame.add_paragraph()
1306
- p.text = point.replace('β€’', '').strip()
 
 
1307
  p.font.size = Pt(16)
1308
  p.font.color.rgb = theme["text_color"]
 
1309
  p.level = 0
1310
  p.space_after = Pt(12)
1311
  p.line_spacing = 1.5
 
 
1312
 
1313
  # 뢈릿 색상
1314
  p.font.color.rgb = theme["text_color"]
@@ -1406,11 +1659,11 @@ def generate_dynamic_slides(topic: str, template: Dict, slide_count: int) -> Lis
1406
  slides.extend(selected_slides)
1407
 
1408
  # Thank You μŠ¬λΌμ΄λ“œ μΆ”κ°€ (맨 λ’€)
1409
- slides.append({"title": "Thank You", "style": "Thank You Slide", "prompt_hint": "감사 인사"})
1410
 
1411
  return slides
1412
 
1413
- def create_editable_slide_interface(slide_data: Dict, slide_index: int) -> str:
1414
  """νŽΈμ§‘ κ°€λŠ₯ν•œ μŠ¬λΌμ΄λ“œ μΈν„°νŽ˜μ΄μŠ€ 생성"""
1415
 
1416
  # 이미지λ₯Ό base64둜 인코딩
@@ -1430,7 +1683,7 @@ def create_editable_slide_interface(slide_data: Dict, slide_index: int) -> str:
1430
 
1431
  # HTML 생성
1432
  html = f"""
1433
- <div class="slide-container" style="
1434
  width: 100%;
1435
  max-width: 1200px;
1436
  margin: 20px auto;
@@ -1450,7 +1703,7 @@ def create_editable_slide_interface(slide_data: Dict, slide_index: int) -> str:
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;
@@ -1481,7 +1734,7 @@ def create_editable_slide_interface(slide_data: Dict, slide_index: int) -> str:
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;
@@ -1497,6 +1750,7 @@ def create_editable_slide_interface(slide_data: Dict, slide_index: int) -> str:
1497
  """
1498
 
1499
  for point in bullet_points:
 
1500
  html += f"""
1501
  <li style="
1502
  margin-bottom: 16px;
@@ -1512,7 +1766,7 @@ def create_editable_slide_interface(slide_data: Dict, slide_index: int) -> str:
1512
  color: #007bff;
1513
  font-size: 18px;
1514
  ">β€’</span>
1515
- {point.replace('β€’', '').strip()}
1516
  </li>
1517
  """
1518
 
@@ -1536,6 +1790,7 @@ def create_editable_slide_interface(slide_data: Dict, slide_index: int) -> str:
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="
@@ -1545,7 +1800,7 @@ def create_editable_slide_interface(slide_data: Dict, slide_index: int) -> str:
1545
  border: 1px solid #e9ecef;
1546
  border-radius: 5px;
1547
  font-size: 16px;
1548
- " placeholder="포인트 {i+1}">
1549
  """
1550
 
1551
  html += f"""
@@ -1567,8 +1822,8 @@ def create_editable_slide_interface(slide_data: Dict, slide_index: int) -> str:
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;
@@ -1581,7 +1836,7 @@ def create_editable_slide_interface(slide_data: Dict, slide_index: int) -> str:
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;
@@ -1590,7 +1845,7 @@ def create_editable_slide_interface(slide_data: Dict, slide_index: int) -> str:
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;
@@ -1633,53 +1888,100 @@ def create_editable_slide_interface(slide_data: Dict, slide_index: int) -> str:
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
 
@@ -1707,7 +2009,7 @@ def generate_ppt_with_content(topic: str, template_name: str, custom_slides: Lis
1707
  if template_name == "μ‚¬μš©μž μ •μ˜" and custom_slides:
1708
  slides = [{"title": "ν‘œοΏ½οΏ½", "style": "Title Slide (Hero)", "prompt_hint": "ν”„λ ˆμ  ν…Œμ΄μ…˜ ν‘œμ§€"}]
1709
  slides.extend(custom_slides)
1710
- slides.append({"title": "Thank You", "style": "Thank You Slide", "prompt_hint": "감사 인사"})
1711
  else:
1712
  template = PPT_TEMPLATES[template_name]
1713
  slides = generate_dynamic_slides(topic, template, slide_count)
@@ -1749,13 +2051,23 @@ def generate_ppt_with_content(topic: str, template_name: str, custom_slides: Lis
1749
 
1750
  # ν…μŠ€νŠΈ λ‚΄μš© 생성
1751
  slide_context = f"{slide['title']} - {slide.get('prompt_hint', '')}"
1752
- content = generate_slide_content(
1753
- topic, slide['title'], slide_context,
1754
- uploaded_content, web_search_results
1755
- )
1756
 
1757
- # λ°œν‘œμž λ…ΈνŠΈ 생성
1758
- speaker_notes = generate_presentation_notes(topic, slide['title'], content)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1759
 
1760
  # ν”„λ‘¬ν”„νŠΈ 생성 및 이미지 생성
1761
  style_key = slide["style"]
@@ -1784,7 +2096,7 @@ def generate_ppt_with_content(topic: str, template_name: str, custom_slides: Lis
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
 
@@ -1886,17 +2198,26 @@ with gr.Blocks(title="PPT 이미지 생성기", theme=gr.themes.Soft(), css="""
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
  - πŸ“‹ **예제 ν…œν”Œλ¦Ώ**: λ²„νŠΌ 클릭으둜 예제 주제 뢈러였기
@@ -2002,10 +2323,22 @@ with gr.Blocks(title="PPT 이미지 생성기", theme=gr.themes.Soft(), css="""
2002
  4. **AI μž¬μž‘μ„±**: μ§€μ‹œμ‚¬ν•­ μž…λ ₯ ν›„ AI둜 λ‹€μ‹œ μž‘μ„±
2003
  5. **μ΅œμ’… λ‹€μš΄λ‘œλ“œ**: νŽΈμ§‘ μ™„λ£Œ ν›„ λ‹€μš΄λ‘œλ“œ
2004
 
2005
- ### πŸ’‘ νŽΈμ§‘ 팁:
2006
- - 각 μŠ¬λΌμ΄λ“œμ˜ νŽΈμ§‘ λ²„νŠΌμ„ ν΄λ¦­ν•˜μ—¬ ν…μŠ€νŠΈ μˆ˜μ •
2007
- - AIμ—κ²Œ "더 κ°„λ‹¨ν•˜κ²Œ", "숫자 포함", "μ „λ¬Έμ μœΌλ‘œ" λ“± μ§€μ‹œ
2008
- - νŽΈμ§‘ ν›„ μ €μž₯ λ²„νŠΌμ„ 클릭해야 반영됨
 
 
 
 
 
 
 
 
 
 
 
 
2009
  """)
2010
 
2011
  # PPTX λ‹€μš΄λ‘œλ“œ μ˜μ—­
@@ -2150,85 +2483,73 @@ with gr.Blocks(title="PPT 이미지 생성기", theme=gr.themes.Soft(), css="""
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],
2162
  outputs=[theme_preview]
2163
  )
2164
-
2165
  template_select.change(
2166
  fn=update_template_info,
2167
  inputs=[template_select, slide_count],
2168
  outputs=[template_info]
2169
  )
2170
-
2171
  slide_count.change(
2172
  fn=update_template_info,
2173
  inputs=[template_select, slide_count],
2174
  outputs=[template_info]
2175
  )
2176
-
2177
  custom_slide_count.change(
2178
  fn=update_custom_slides_visibility,
2179
  inputs=[custom_slide_count],
2180
- outputs=[comp for slide in custom_slides_components for comp in [slide["title"], slide["style"], slide["hint"]]]
 
 
 
 
2181
  )
2182
-
2183
- # μ‚¬μš©μž μ •μ˜ μž…λ ₯ μˆ˜μ§‘
2184
  all_custom_inputs = []
2185
- for slide_components in custom_slides_components:
2186
- all_custom_inputs.extend([
2187
- slide_components["title"],
2188
- slide_components["style"],
2189
- slide_components["hint"]
2190
- ])
2191
-
2192
  generate_btn.click(
2193
  fn=generate_ppt_handler,
2194
- inputs=[topic_input, template_select, theme_select, slide_count, seed_input,
2195
- file_upload, use_web_search, custom_slide_count] + all_custom_inputs,
2196
- outputs=[preview_output, status_output, download_file]
 
 
 
 
 
 
 
 
 
2197
  )
2198
-
2199
- # 초기 ν…œν”Œλ¦Ώ 정보 ν‘œμ‹œ
2200
  demo.load(
2201
  fn=lambda: (
2202
- update_template_info("λΉ„μ¦ˆλ‹ˆμŠ€ μ œμ•ˆμ„œ", 8),
2203
- update_theme_preview("λ―Έλ‹ˆλ©€ 라이트")
2204
  ),
2205
  inputs=[],
2206
- outputs=[template_info, theme_preview]
2207
  )
2208
 
2209
- # μ•± μ‹€ν–‰
2210
  if __name__ == "__main__":
2211
- print("\n" + "="*50)
2212
- print("πŸš€ PPT 톡합 생성기 (νŽΈμ§‘ κ°€λŠ₯ 버전) μ‹œμž‘!")
2213
- print("="*50)
2214
-
2215
- # ν™˜κ²½ λ³€μˆ˜ 확인
2216
- if not REPLICATE_API_TOKEN:
2217
- print("⚠️ κ²½κ³ : RAPI_TOKEN ν™˜κ²½ λ³€μˆ˜κ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.")
2218
- else:
2219
- print("βœ… RAPI_TOKEN 확인됨")
2220
-
2221
- if not FRIENDLI_TOKEN:
2222
- print("⚠️ κ²½κ³ : FRIENDLI_TOKEN ν™˜κ²½ λ³€μˆ˜κ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.")
2223
- else:
2224
- print("βœ… FRIENDLI_TOKEN 확인됨")
2225
-
2226
- if not BRAVE_API_TOKEN:
2227
- print("ℹ️ BAPI_TOKEN이 μ—†μ–΄ μ›Ή 검색 κΈ°λŠ₯이 λΉ„ν™œμ„±ν™”λ©λ‹ˆλ‹€.")
2228
- else:
2229
- print("βœ… BAPI_TOKEN 확인됨")
2230
-
2231
- print("="*50 + "\n")
2232
-
2233
- # SSR λͺ¨λ“œ λΉ„ν™œμ„±ν™”ν•˜μ—¬ μ‹€ν–‰
2234
- demo.launch(ssr_mode=False)
 
 
1
  import replicate
2
  import requests
3
  import os
 
17
  import PyPDF2
18
  import pandas as pd
19
  import chardet
20
+ import gradio as gr
21
 
22
  # 예제 ν…œν”Œλ¦Ώ μ •μ˜
23
  EXAMPLE_TOPICS = {
24
+ "λΉ„μ¦ˆλ‹ˆμŠ€ μ œμ•ˆμ„œ": "AI 기반 고객 μ„œλΉ„μŠ€ μžλ™ν™” ν”Œλž«νΌ 투자 μ œμ•ˆ",
25
+ "μ œν’ˆ μ†Œκ°œ": "슀마트 ν™ˆ IoT λ³΄μ•ˆ μ‹œμŠ€ν…œ μ‹ μ œν’ˆ 런칭",
26
+ "ν”„λ‘œμ νŠΈ 보고": "λ””μ§€ν„Έ μ „ν™˜ ν”„λ‘œμ νŠΈ 3λΆ„κΈ° μ„±κ³Ό 보고",
27
+ "μ „λž΅ 기획": "2025λ…„ κΈ€λ‘œλ²Œ μ‹œμž₯ μ§„μΆœ μ „λž΅ 수립"
28
  }
29
 
30
  # ν™˜κ²½ λ³€μˆ˜μ—μ„œ 토큰 κ°€μ Έμ˜€κΈ°
 
116
  "example": "A dramatic wide-angle view of a modern glass skyscraper reaching into clouds with golden sunset lighting, symbolizing growth and ambition. Ultra-realistic photography style, cinematic composition, lens flare, professional corporate aesthetic"
117
  },
118
  "Thank You Slide": {
119
+ "name": "Closing Slide",
120
+ "description": "Elegant closing slide design with conclusion",
121
+ "use_case": "ν”„λ ˆμ  ν…Œμ΄μ…˜ 마무리",
122
+ "example": "Abstract elegant background with soft gradient from deep blue to purple, golden particles floating like celebration confetti, subtle light rays, with space for conclusion text. Minimalist, professional, warm feeling"
123
  },
124
  "3D Style (Pixar-like)": {
125
  "name": "3D Style",
 
396
 
397
  system_prompt = """You are a professional presentation coach who creates natural, conversational speaker notes.
398
 
399
+ The slide content uses concise noun-ending style, but your speaker notes should be natural and conversational.
400
+
401
  Create speaker notes that:
402
  1. Sound natural and conversational, as if speaking to an audience
403
+ 2. Expand on the concise bullet points with additional context
404
+ 3. Include transitions and engagement phrases
405
  4. Use a warm, professional tone
406
  5. Be 100-150 words long
407
  6. Include pauses and emphasis markers where appropriate
408
 
409
+ Note: The bullet points may include emojis - incorporate their meaning into your speech.
410
+
411
  Format:
412
  - Use conversational language
413
  - Include transition phrases
 
418
  user_message = f"""Topic: {topic}
419
  Slide Title: {slide_title}
420
  Subtitle: {content.get('subtitle', '')}
421
+ Key Points (concise style with emojis):
422
  {bullet_text}
423
 
424
+ Create natural speaker notes that expand on these concise points for presenting this slide."""
425
 
426
  payload = {
427
  "model": "dep89a2fld32mcm",
 
448
 
449
  # ν•œκΈ€ 주제인 경우 λ²ˆμ—­
450
  if any(ord('κ°€') <= ord(char) <= ord('힣') for char in topic):
451
+ notes = translate_content_to_korean_natural(notes)
452
 
453
  return notes
454
  else:
 
473
 
474
  Your task is to create:
475
  1. A compelling subtitle (max 10 words)
476
+ 2. Exactly 5 bullet points with emojis
477
+ 3. Each bullet point should be 8-12 words
478
+ 4. Use noun-ending style (λͺ…μ‚¬ν˜• μ’…κ²°) for Korean or concise fragments for English
479
 
480
+ IMPORTANT Style Guidelines:
481
+ - End sentences with nouns or concise phrases
482
+ - Avoid long verb endings like "μž…λ‹ˆλ‹€", "μŠ΅λ‹ˆλ‹€"
483
+ - Use short endings like "μž„", "함" or noun forms
484
+ - Start each bullet with a relevant emoji
485
+ - Follow the user's instructions while maintaining this style
486
 
487
+ Emoji Guidelines:
488
+ - Professional emojis: πŸ“Š 🎯 πŸ’‘ πŸš€ ⚑ πŸ” πŸ“ˆ πŸ’° πŸ† πŸ”§ 🌐 πŸ” ⭐ 🎨 πŸ“± 🀝 πŸ“ πŸŽͺ πŸ—οΈ 🌱
489
+ - Match emoji to content meaning
490
+ - Use different emojis for variety"""
 
 
 
491
 
492
  user_message = f"""Topic: {topic}
493
  Slide Title: {slide_title}
 
495
 
496
  User's specific instruction: {user_instruction}
497
 
498
+ Rewrite the content following the instruction above while maintaining concise, noun-ending style with emojis."""
499
 
500
  if uploaded_content:
501
  user_message += f"\n\nReference Material:\n{uploaded_content[:1000]}"
 
537
  # ν•œκΈ€λ‘œ λ²ˆμ—­μ΄ ν•„μš”ν•œ 경우
538
  if any(ord('κ°€') <= ord(char) <= ord('힣') for char in topic):
539
  subtitle = translate_content_to_korean(subtitle)
540
+ # 뢈릿 ν¬μΈνŠΈλŠ” 이λͺ¨μ§€λ₯Ό μœ μ§€ν•˜λ©΄μ„œ λ²ˆμ—­
541
+ translated_bullets = []
542
+ for point in bullet_points:
543
+ # 이λͺ¨μ§€μ™€ ν…μŠ€νŠΈ 뢄리
544
+ parts = point.split(' ', 2)
545
+ if len(parts) >= 3 and len(parts[1]) <= 2: # 이λͺ¨μ§€κ°€ μžˆλŠ” 경우
546
+ emoji = parts[1]
547
+ text = ' '.join(parts[2:])
548
+ translated_text = translate_content_to_korean_concise(text)
549
+ translated_bullets.append(f"β€’ {emoji} {translated_text}")
550
+ else:
551
+ translated_bullets.append(translate_content_to_korean_concise(point))
552
+ bullet_points = translated_bullets
553
 
554
  return {
555
  "subtitle": subtitle,
 
558
  else:
559
  return {
560
  "subtitle": slide_title,
561
+ "bullet_points": ["β€’ ❌ μž¬μƒμ„± μ‹€νŒ¨"] * 5
562
  }
563
  except Exception as e:
564
  print(f"[AI μž¬μž‘μ„±] 였λ₯˜: {str(e)}")
565
  return {
566
  "subtitle": slide_title,
567
+ "bullet_points": ["β€’ ❌ μž¬μƒμ„± 였λ₯˜"] * 5
568
  }
569
 
570
+ def generate_closing_notes(topic: str, conclusion_phrase: str) -> str:
571
+ """λ§ˆμ§€λ§‰ μŠ¬λΌμ΄λ“œμ˜ λ°œν‘œμž λ…ΈνŠΈ 생성"""
572
+ print(f"[마무리 λ…ΈνŠΈ] 생성 쀑...")
573
+
574
+ url = "https://api.friendli.ai/dedicated/v1/chat/completions"
575
+ headers = {
576
+ "Authorization": f"Bearer {FRIENDLI_TOKEN}",
577
+ "Content-Type": "application/json"
578
+ }
579
+
580
+ system_prompt = """You are a professional presentation coach creating closing speaker notes.
581
+
582
+ Create natural closing remarks that:
583
+ 1. Thank the audience warmly
584
+ 2. Briefly summarize the key message of the presentation
585
+ 3. Reference the conclusion phrase naturally
586
+ 4. End with an invitation for questions or next steps
587
+ 5. Be 80-100 words long
588
+ 6. Sound conversational and warm
589
+ 7. NO stage directions or parentheses - only spoken words
590
+
591
+ Write only what the speaker would say out loud."""
592
+
593
+ user_message = f"""Presentation topic: {topic}
594
+ Conclusion phrase on screen: {conclusion_phrase}
595
+
596
+ Create natural closing speaker notes that wrap up the presentation effectively."""
597
+
598
+ payload = {
599
+ "model": "dep89a2fld32mcm",
600
+ "messages": [
601
+ {
602
+ "role": "system",
603
+ "content": system_prompt
604
+ },
605
+ {
606
+ "role": "user",
607
+ "content": user_message
608
+ }
609
+ ],
610
+ "max_tokens": 200,
611
+ "temperature": 0.8,
612
+ "stream": False
613
+ }
614
+
615
+ try:
616
+ response = requests.post(url, json=payload, headers=headers, timeout=30)
617
+ if response.status_code == 200:
618
+ result = response.json()
619
+ notes = result['choices'][0]['message']['content'].strip()
620
+
621
+ # ν•œκΈ€ 주제인 경우 λ²ˆμ—­
622
+ if any(ord('κ°€') <= ord(char) <= ord('힣') for char in topic):
623
+ notes = translate_content_to_korean_natural(notes)
624
+
625
+ return notes
626
+ else:
627
+ return "였늘 λ°œν‘œλ₯Ό λ§ˆλ¬΄λ¦¬ν•˜λ©° κ°μ‚¬μ˜ 말씀을 λ“œλ¦½λ‹ˆλ‹€. 질문이 μžˆμœΌμ‹œλ©΄ νŽΈν•˜κ²Œ 말씀해 μ£Όμ„Έμš”."
628
+ except Exception as e:
629
+ print(f"[마무리 λ…ΈνŠΈ] 였λ₯˜: {str(e)}")
630
+ return "였늘 λ°œν‘œλ₯Ό λ§ˆλ¬΄λ¦¬ν•˜λ©° κ°μ‚¬μ˜ 말씀을 λ“œλ¦½λ‹ˆλ‹€. 질문이 μžˆμœΌμ‹œλ©΄ νŽΈν•˜κ²Œ 말씀해 μ£Όμ„Έμš”."
631
+
632
+ def generate_conclusion_phrase(topic: str) -> str:
633
+ """ν”„λ ˆμ  ν…Œμ΄μ…˜μ˜ 핡심을 담은 κ²°λ‘  문ꡬ 생성"""
634
+ print(f"[κ²°λ‘  문ꡬ] 생성 쀑...")
635
+
636
+ url = "https://api.friendli.ai/dedicated/v1/chat/completions"
637
+ headers = {
638
+ "Authorization": f"Bearer {FRIENDLI_TOKEN}",
639
+ "Content-Type": "application/json"
640
+ }
641
+
642
+ system_prompt = """You are a professional copywriter creating powerful closing statements for presentations.
643
+
644
+ Create a concise, impactful closing phrase that:
645
+ 1. Captures the essence of the presentation topic
646
+ 2. Is memorable and inspirational
647
+ 3. Maximum 5-7 words
648
+ 4. Uses powerful, action-oriented language
649
+ 5. Leaves a lasting impression
650
+
651
+ Examples:
652
+ - "Innovation Starts Today"
653
+ - "Together We Transform"
654
+ - "Future Begins Now"
655
+ - "Excellence Through Innovation"
656
+
657
+ Output only the phrase, no explanation."""
658
+
659
+ user_message = f"""Presentation topic: {topic}
660
+
661
+ Create a powerful closing phrase that encapsulates the main message."""
662
+
663
+ payload = {
664
+ "model": "dep89a2fld32mcm",
665
+ "messages": [
666
+ {
667
+ "role": "system",
668
+ "content": system_prompt
669
+ },
670
+ {
671
+ "role": "user",
672
+ "content": user_message
673
+ }
674
+ ],
675
+ "max_tokens": 50,
676
+ "temperature": 0.9,
677
+ "stream": False
678
+ }
679
+
680
+ try:
681
+ response = requests.post(url, json=payload, headers=headers, timeout=30)
682
+ if response.status_code == 200:
683
+ result = response.json()
684
+ phrase = result['choices'][0]['message']['content'].strip()
685
+
686
+ # ν•œκΈ€ 주제인 경우 λ²ˆμ—­
687
+ if any(ord('κ°€') <= ord(char) <= ord('힣') for char in topic):
688
+ phrase = translate_content_to_korean_concise(phrase)
689
+
690
+ return phrase
691
+ else:
692
+ return "ν•¨κ»˜ λ§Œλ“œλŠ” 미래"
693
+ except Exception as e:
694
+ print(f"[κ²°λ‘  문ꡬ] 였λ₯˜: {str(e)}")
695
+ return "ν•¨κ»˜ λ§Œλ“œλŠ” 미래"
696
+
697
  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]:
698
  """각 μŠ¬λΌμ΄λ“œμ˜ ν…μŠ€νŠΈ λ‚΄μš© 생성"""
699
  print(f"[μŠ¬λΌμ΄λ“œ λ‚΄μš©] {slide_title} ν…μŠ€νŠΈ 생성 쀑...")
 
708
 
709
  Your task is to create:
710
  1. A compelling subtitle (max 10 words)
711
+ 2. Exactly 5 bullet points with emojis
712
+ 3. Each bullet point should be 8-12 words
713
+ 4. Use noun-ending style (λͺ…μ‚¬ν˜• μ’…κ²°) for Korean or concise fragments for English
714
+
715
+ IMPORTANT Style Guidelines:
716
+ - End sentences with nouns or concise phrases (예: "μ „λž΅μ  ν™•λŒ€", "핡심 과제", "μ£Όμš” λͺ©ν‘œ")
717
+ - Avoid long verb endings like "μž…λ‹ˆλ‹€", "μŠ΅λ‹ˆλ‹€"
718
+ - Use short endings like "μž„", "함" or noun forms
719
+ - Start each bullet with a relevant emoji that represents the content
720
+ - Be extremely concise and impactful
721
 
722
+ Emoji Guidelines:
723
+ - Use professional emojis that match the content
724
+ - Common emojis:
725
+ πŸ“Š (data/analysis), 🎯 (goals/targets), πŸ’‘ (innovation/ideas),
726
+ πŸš€ (growth/launch), ⚑ (speed/efficiency), πŸ” (research/analysis),
727
+ πŸ“ˆ (increase/growth), πŸ’° (finance/revenue), πŸ† (achievement/success),
728
+ πŸ”§ (tools/solutions), 🌐 (global/network), πŸ” (security/safety),
729
+ ⭐ (excellence/quality), 🎨 (creative/design), πŸ“± (mobile/tech),
730
+ 🀝 (partnership/collaboration), πŸ“ (location/focus), πŸŽ–οΈ (strategy),
731
+ πŸ—οΈ (building/development), 🌱 (sustainability/growth)
732
+ - Each bullet should have a different, relevant emoji
733
 
734
  Output format:
735
  Subtitle: [subtitle here]
736
+ β€’ 🎯 [Point 1 - noun ending or fragment]
737
+ β€’ πŸ“Š [Point 2 - noun ending or fragment]
738
+ β€’ πŸ’‘ [Point 3 - noun ending or fragment]
739
+ β€’ πŸš€ [Point 4 - noun ending or fragment]
740
+ β€’ ⚑ [Point 5 - noun ending or fragment]"""
741
 
742
  user_message = f"""Topic: {topic}
743
  Slide Title: {slide_title}
 
754
  search_context += f"- {result['title']}: {result['description']}\n"
755
  user_message += search_context
756
 
757
+ user_message += "\n\nCreate compelling content for this presentation slide. Remember to use emojis and concise noun-ending style."
758
 
759
  payload = {
760
  "model": "dep89a2fld32mcm",
 
794
  # ν•œκΈ€λ‘œ λ²ˆμ—­μ΄ ν•„μš”ν•œ 경우
795
  if any(ord('κ°€') <= ord(char) <= ord('힣') for char in topic):
796
  subtitle = translate_content_to_korean(subtitle)
797
+ # 뢈릿 ν¬μΈνŠΈλŠ” 이λͺ¨μ§€λ₯Ό μœ μ§€ν•˜λ©΄μ„œ λ²ˆμ—­
798
+ translated_bullets = []
799
+ for point in bullet_points:
800
+ # 이λͺ¨μ§€μ™€ ν…μŠ€νŠΈ 뢄리
801
+ parts = point.split(' ', 2)
802
+ if len(parts) >= 3 and len(parts[1]) <= 2: # 이λͺ¨μ§€κ°€ μžˆλŠ” 경우
803
+ emoji = parts[1]
804
+ text = ' '.join(parts[2:])
805
+ translated_text = translate_content_to_korean_concise(text)
806
+ translated_bullets.append(f"β€’ {emoji} {translated_text}")
807
+ else:
808
+ translated_bullets.append(translate_content_to_korean_concise(point))
809
+ bullet_points = translated_bullets
810
 
811
  return {
812
  "subtitle": subtitle,
 
815
  else:
816
  return {
817
  "subtitle": slide_title,
818
+ "bullet_points": ["β€’ πŸ“Œ λ‚΄μš© 생성 λΆˆκ°€"] * 5
819
  }
820
  except Exception as e:
821
  print(f"[μŠ¬λΌμ΄λ“œ λ‚΄μš©] 였λ₯˜: {str(e)}")
822
  return {
823
  "subtitle": slide_title,
824
+ "bullet_points": ["β€’ ❌ λ‚΄μš© 생성 였λ₯˜"] * 5
825
  }
826
 
827
  def translate_content_to_korean(text: str) -> str:
828
+ """μ˜μ–΄ ν…μŠ€νŠΈλ₯Ό ν•œκΈ€λ‘œ λ²ˆμ—­ (일반용)"""
829
  url = "https://api.friendli.ai/dedicated/v1/chat/completions"
830
  headers = {
831
  "Authorization": f"Bearer {FRIENDLI_TOKEN}",
 
859
  except Exception as e:
860
  return text
861
 
862
+ def translate_content_to_korean_concise(text: str) -> str:
863
+ """μ˜μ–΄ ν…μŠ€νŠΈλ₯Ό κ°„κ²°ν•œ ν•œκΈ€λ‘œ λ²ˆμ—­ (λͺ…μ‚¬ν˜• μ’…κ²°)"""
864
+ url = "https://api.friendli.ai/dedicated/v1/chat/completions"
865
+ headers = {
866
+ "Authorization": f"Bearer {FRIENDLI_TOKEN}",
867
+ "Content-Type": "application/json"
868
+ }
869
+
870
+ payload = {
871
+ "model": "dep89a2fld32mcm",
872
+ "messages": [
873
+ {
874
+ "role": "system",
875
+ "content": """You are a translator specializing in concise Korean business presentations.
876
+ Translate to Korean using noun-ending style (λͺ…μ‚¬ν˜• μ’…κ²°μ–΄λ―Έ).
877
+ End with "μž„", "함", or noun forms instead of "μž…λ‹ˆλ‹€", "μŠ΅λ‹ˆλ‹€".
878
+ Keep it extremely concise and professional.
879
+ Examples: "μ „λž΅μ  ν™•λŒ€", "핡심 과제 λ„μΆœ", "μ‹œμž₯ 선도 μ „λž΅ 수립"
880
+ Only return the translation without any explanation."""
881
+ },
882
+ {
883
+ "role": "user",
884
+ "content": text
885
+ }
886
+ ],
887
+ "max_tokens": 200,
888
+ "top_p": 0.8,
889
+ "stream": False
890
+ }
891
+
892
+ try:
893
+ response = requests.post(url, json=payload, headers=headers, timeout=30)
894
+ if response.status_code == 200:
895
+ result = response.json()
896
+ return result['choices'][0]['message']['content'].strip()
897
+ else:
898
+ return text
899
+ except Exception as e:
900
+ return text
901
+
902
+ def translate_content_to_korean_natural(text: str) -> str:
903
+ """μ˜μ–΄ ν…μŠ€νŠΈλ₯Ό μžμ—°μŠ€λŸ¬μš΄ ν•œκΈ€λ‘œ λ²ˆμ—­ (λ°œν‘œ λ…ΈνŠΈμš©)"""
904
+ url = "https://api.friendli.ai/dedicated/v1/chat/completions"
905
+ headers = {
906
+ "Authorization": f"Bearer {FRIENDLI_TOKEN}",
907
+ "Content-Type": "application/json"
908
+ }
909
+
910
+ payload = {
911
+ "model": "dep89a2fld32mcm",
912
+ "messages": [
913
+ {
914
+ "role": "system",
915
+ "content": """You are a translator. Translate the given English text to natural, conversational Korean.
916
+ Use polite spoken language suitable for presentations.
917
+ Only return the translation without any explanation."""
918
+ },
919
+ {
920
+ "role": "user",
921
+ "content": text
922
+ }
923
+ ],
924
+ "max_tokens": 300,
925
+ "top_p": 0.8,
926
+ "stream": False
927
+ }
928
+
929
+ try:
930
+ response = requests.post(url, json=payload, headers=headers, timeout=30)
931
+ if response.status_code == 200:
932
+ result = response.json()
933
+ return result['choices'][0]['message']['content'].strip()
934
+ else:
935
+ return text
936
+ except Exception as e:
937
+ return text
938
+
939
  def generate_prompt_with_llm(topic: str, style_example: str = None, slide_context: str = None, uploaded_content: str = None) -> str:
940
  """μ£Όμ œμ™€ μŠ€νƒ€μΌ 예제λ₯Ό λ°›μ•„μ„œ LLM을 μ‚¬μš©ν•΄ 이미지 ν”„λ‘¬ν”„νŠΈλ₯Ό 생성"""
941
  print(f"[LLM] ν”„λ‘¬ν”„νŠΈ 생성 μ‹œμž‘: {slide_context}")
 
1185
  left: 50%;
1186
  transform: translate(-50%, -50%);
1187
  text-align: center;
1188
+ background: rgba(255, 255, 255, 0.9);
1189
+ padding: 30px 60px;
1190
  border-radius: 20px;
1191
+ box-shadow: 0 10px 40px rgba(0,0,0,0.3);
1192
  backdrop-filter: blur(10px);
1193
  ">
1194
  """
1195
 
1196
  if slide_data.get('title') == 'ν‘œμ§€':
1197
  html += f"""
1198
+ <h1 style="font-size: 48px; margin-bottom: 10px; color: #000000; font-weight: 700;">{slide_data.get('topic', '')}</h1>
1199
+ <h2 style="font-size: 24px; margin-top: 0; color: #212529; font-weight: 400;">{subtitle}</h2>
1200
  """
1201
  else: # Thank You
1202
  html += f"""
1203
+ <h1 style="font-size: 42px; color: #000000; font-weight: 700; line-height: 1.2;">{subtitle}</h1>
1204
  """
1205
 
1206
  html += """
 
1239
  """
1240
 
1241
  for point in bullet_points:
1242
+ clean_point = point.replace('β€’', '').strip()
1243
  html += f"""
1244
  <li style="
1245
  margin-bottom: 16px;
 
1255
  color: #007bff;
1256
  font-size: 18px;
1257
  ">β€’</span>
1258
+ {clean_point}
1259
  </li>
1260
  """
1261
 
 
1371
  # 제λͺ© λ°°κ²½ λ°•μŠ€ (반투λͺ… - 더 밝고 투λͺ…ν•˜κ²Œ)
1372
  title_bg = slide.shapes.add_shape(
1373
  MSO_SHAPE.ROUNDED_RECTANGLE,
1374
+ Inches(2), Inches(2.8),
1375
+ Inches(12), Inches(3.2)
1376
  )
1377
  title_bg.fill.solid()
1378
  title_bg.fill.fore_color.rgb = RGBColor(255, 255, 255) # 흰색 λ°°κ²½
1379
+ title_bg.fill.transparency = 0.2 # 20% 투λͺ…도 (80% 뢈투λͺ…)
1380
  title_bg.line.fill.background()
1381
 
1382
+ # 그림자 효과 μΆ”κ°€ (더 κ°•ν•˜κ²Œ)
1383
  shadow = title_bg.shadow
1384
  shadow.visible = True
1385
+ shadow.distance = Pt(8)
1386
+ shadow.size = 120
1387
+ shadow.blur_radius = Pt(15)
1388
+ shadow.transparency = 0.3
1389
  shadow.angle = 45
1390
 
1391
  # 제λͺ© ν…μŠ€νŠΈ μΆ”κ°€
1392
  title_box = slide.shapes.add_textbox(
1393
+ Inches(2), Inches(3.2),
1394
+ Inches(12), Inches(1.5)
1395
  )
1396
  title_frame = title_box.text_frame
1397
  title_frame.text = topic
1398
  title_para = title_frame.paragraphs[0]
1399
  title_para.font.size = Pt(48)
1400
  title_para.font.bold = True
1401
+ title_para.font.color.rgb = RGBColor(0, 0, 0) # μ™„μ „ν•œ 검정색
1402
  title_para.alignment = PP_ALIGN.CENTER
1403
 
1404
+ # λΆ€μ œλͺ© μΆ”κ°€ (제λͺ©κ³Ό κ°€κΉŒμ΄)
1405
  subtitle_box = slide.shapes.add_textbox(
1406
+ Inches(2), Inches(4.3),
1407
+ Inches(12), Inches(1.0)
1408
  )
1409
  subtitle_frame = subtitle_box.text_frame
1410
  subtitle_frame.text = slide_data.get('subtitle', f'{template_name} - AI ν”„λ ˆμ  ν…Œμ΄μ…˜')
1411
  subtitle_para = subtitle_frame.paragraphs[0]
1412
+ subtitle_para.font.size = Pt(28) # 24μ—μ„œ 28둜 증가
1413
+ subtitle_para.font.color.rgb = RGBColor(33, 37, 41) # μ§„ν•œ νšŒμƒ‰
1414
  subtitle_para.alignment = PP_ALIGN.CENTER
1415
 
1416
  # Thank You μŠ¬λΌμ΄λ“œ
 
1438
  # Thank You λ°°κ²½ λ°•μŠ€ (반투λͺ… - 더 밝고 투λͺ…ν•˜κ²Œ)
1439
  thanks_bg = slide.shapes.add_shape(
1440
  MSO_SHAPE.ROUNDED_RECTANGLE,
1441
+ Inches(2), Inches(3.5),
1442
+ Inches(12), Inches(2.5)
1443
  )
1444
  thanks_bg.fill.solid()
1445
  thanks_bg.fill.fore_color.rgb = RGBColor(255, 255, 255) # 흰색 λ°°κ²½
1446
+ thanks_bg.fill.transparency = 0.2 # 20% 투λͺ…도
1447
  thanks_bg.line.fill.background()
1448
 
1449
+ # 그림자 효과 μΆ”κ°€ (더 κ°•ν•˜κ²Œ)
1450
  shadow = thanks_bg.shadow
1451
  shadow.visible = True
1452
+ shadow.distance = Pt(8)
1453
+ shadow.size = 120
1454
+ shadow.blur_radius = Pt(15)
1455
+ shadow.transparency = 0.3
1456
  shadow.angle = 45
1457
 
1458
+ # Thank You ν…μŠ€νŠΈ (κ²°λ‘  문ꡬ)
1459
  thanks_box = slide.shapes.add_textbox(
1460
+ Inches(2), Inches(4),
1461
+ Inches(12), Inches(1.5)
1462
  )
1463
  thanks_frame = thanks_box.text_frame
1464
+ thanks_frame.text = slide_data.get('subtitle', 'Thank You') # κ²°λ‘  문ꡬ μ‚¬μš©
1465
  thanks_para = thanks_frame.paragraphs[0]
1466
+ thanks_para.font.size = Pt(42) # 크기λ₯Ό 쑰금 μ€„μž„
1467
  thanks_para.font.bold = True
1468
+ thanks_para.font.color.rgb = RGBColor(0, 0, 0) # μ™„μ „ν•œ 검정색
1469
  thanks_para.alignment = PP_ALIGN.CENTER
1470
 
1471
  # 일반 μŠ¬λΌμ΄λ“œ
 
1551
  bullet_points = slide_data.get('bullet_points', [])
1552
  for point in bullet_points:
1553
  p = text_frame.add_paragraph()
1554
+ # β€’ μ œκ±°ν•˜κ³  ν…μŠ€νŠΈλ§Œ μΆ”κ°€ (이λͺ¨μ§€λŠ” μœ μ§€)
1555
+ clean_text = point.replace('β€’', '').strip()
1556
+ p.text = clean_text
1557
  p.font.size = Pt(16)
1558
  p.font.color.rgb = theme["text_color"]
1559
+ # 뢈릿 없이 λ“€μ—¬μ“°κΈ°λ§Œ
1560
  p.level = 0
1561
  p.space_after = Pt(12)
1562
  p.line_spacing = 1.5
1563
+ # μ™Όμͺ½ μ—¬λ°± μ„€μ •
1564
+ p.left_indent = Pt(0)
1565
 
1566
  # 뢈릿 색상
1567
  p.font.color.rgb = theme["text_color"]
 
1659
  slides.extend(selected_slides)
1660
 
1661
  # Thank You μŠ¬λΌμ΄λ“œ μΆ”κ°€ (맨 λ’€)
1662
+ slides.append({"title": "Thank You", "style": "Thank You Slide", "prompt_hint": "ν”„λ ˆμ  ν…Œμ΄μ…˜ λ§ˆλ¬΄λ¦¬μ™€ 핡심 λ©”μ‹œμ§€"})
1663
 
1664
  return slides
1665
 
1666
+ def create_editable_slide_interface(slide_data: Dict, slide_index: int, topic: str) -> str:
1667
  """νŽΈμ§‘ κ°€λŠ₯ν•œ μŠ¬λΌμ΄λ“œ μΈν„°νŽ˜μ΄μŠ€ 생성"""
1668
 
1669
  # 이미지λ₯Ό base64둜 인코딩
 
1683
 
1684
  # HTML 생성
1685
  html = f"""
1686
+ <div class="slide-container" id="slide_container_{slide_index}" style="
1687
  width: 100%;
1688
  max-width: 1200px;
1689
  margin: 20px auto;
 
1703
  align-items: center;
1704
  ">
1705
  <span>μŠ¬λΌμ΄λ“œ {slide_data.get('slide_number', '')}: {slide_data.get('title', '')}</span>
1706
+ <button id="edit_btn_{slide_index}" style="
1707
  background: #3498db;
1708
  color: white;
1709
  border: none;
 
1734
  border-radius: 12px;
1735
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.08);
1736
  ">
1737
+ <div id="view_mode_{slide_index}" style="display: block;">
1738
  <h2 style="
1739
  color: #212529;
1740
  font-size: 28px;
 
1750
  """
1751
 
1752
  for point in bullet_points:
1753
+ clean_point = point.replace('β€’', '').strip()
1754
  html += f"""
1755
  <li style="
1756
  margin-bottom: 16px;
 
1766
  color: #007bff;
1767
  font-size: 18px;
1768
  ">β€’</span>
1769
+ {clean_point}
1770
  </li>
1771
  """
1772
 
 
1790
  """
1791
 
1792
  for i, point in enumerate(bullet_points):
1793
+ # 이λͺ¨μ§€λ₯Ό ν¬ν•¨ν•œ 전체 ν…μŠ€νŠΈ (β€’ 제거)
1794
  clean_point = point.replace('β€’', '').strip()
1795
  html += f"""
1796
  <input type="text" id="bullet_{slide_index}_{i}" value="{clean_point}" style="
 
1800
  border: 1px solid #e9ecef;
1801
  border-radius: 5px;
1802
  font-size: 16px;
1803
+ " placeholder="예: 🎯 포인트 {i+1}">
1804
  """
1805
 
1806
  html += f"""
 
1822
  font-size: 14px;
1823
  resize: vertical;
1824
  background: white;
1825
+ " placeholder="예: 더 μ „λ¬Έμ μœΌλ‘œ, μˆ«μžμ™€ 톡계 포함, 더 κ°„λ‹¨ν•˜κ²Œ, μž„νŒ©νŠΈ 있게"></textarea>
1826
+ <button id="ai_btn_{slide_index}" style="
1827
  background: #9b59b6;
1828
  color: white;
1829
  border: none;
 
1836
  </div>
1837
 
1838
  <div style="margin-top: 20px;">
1839
+ <button id="save_btn_{slide_index}" style="
1840
  background: #27ae60;
1841
  color: white;
1842
  border: none;
 
1845
  cursor: pointer;
1846
  margin-right: 10px;
1847
  ">μ €μž₯</button>
1848
+ <button id="cancel_btn_{slide_index}" style="
1849
  background: #95a5a6;
1850
  color: white;
1851
  border: none;
 
1888
  </div>
1889
 
1890
  <script>
1891
+ // μ¦‰μ‹œ μ‹€ν–‰ ν•¨μˆ˜λ‘œ μŠ€μ½”ν”„ 뢄리
1892
+ (function() {{
1893
+ const slideIndex = {slide_index};
1894
+
1895
+ // DOM이 μ€€λΉ„λ˜λ©΄ μ‹€ν–‰
1896
+ setTimeout(function() {{
1897
+ const viewMode = document.getElementById('view_mode_' + slideIndex);
1898
+ const editMode = document.getElementById('edit_mode_' + slideIndex);
1899
+ const editBtn = document.getElementById('edit_btn_' + slideIndex);
1900
+ const saveBtn = document.getElementById('save_btn_' + slideIndex);
1901
+ const cancelBtn = document.getElementById('cancel_btn_' + slideIndex);
1902
+ const aiBtn = document.getElementById('ai_btn_' + slideIndex);
1903
+
1904
+ if (editBtn) {{
1905
+ editBtn.onclick = function(e) {{
1906
+ e.preventDefault();
1907
+ e.stopPropagation();
1908
+ viewMode.style.display = 'none';
1909
+ editMode.style.display = 'block';
1910
+ return false;
1911
+ }};
1912
+ }}
1913
+
1914
+ if (saveBtn) {{
1915
+ saveBtn.onclick = function(e) {{
1916
+ e.preventDefault();
1917
+ e.stopPropagation();
1918
+ const subtitle = document.getElementById('subtitle_' + slideIndex).value;
1919
+ const bullets = [];
1920
+ for (let i = 0; i < 5; i++) {{
1921
+ const bullet = document.getElementById('bullet_' + slideIndex + '_' + i);
1922
+ if (bullet && bullet.value) {{
1923
+ let bulletText = bullet.value.trim();
1924
+ if (!bulletText.startsWith('β€’')) {{
1925
+ bulletText = 'β€’ ' + bulletText;
1926
+ }}
1927
+ bullets.push(bulletText);
1928
+ }}
1929
+ }}
1930
+
1931
+ window.savedSlideData = window.savedSlideData || {{}};
1932
+ window.savedSlideData[slideIndex] = {{
1933
+ subtitle: subtitle,
1934
+ bullet_points: bullets
1935
+ }};
1936
+
1937
+ // λ·° λͺ¨λ“œ μ—…λ°μ΄νŠΈ
1938
+ const h2 = viewMode.querySelector('h2');
1939
+ if (h2) h2.textContent = subtitle;
1940
+
1941
+ const ul = viewMode.querySelector('ul');
1942
+ if (ul) {{
1943
+ ul.innerHTML = bullets.map(b => `
1944
+ <li style="margin-bottom: 16px; padding-left: 28px; position: relative; color: #495057; font-size: 16px; line-height: 1.6;">
1945
+ <span style="position: absolute; left: 0; color: #007bff; font-size: 18px;">β€’</span>
1946
+ ${{b.replace('β€’', '').trim()}}
1947
+ </li>
1948
+ `).join('');
1949
+ }}
1950
+
1951
+ viewMode.style.display = 'block';
1952
+ editMode.style.display = 'none';
1953
+ return false;
1954
+ }};
1955
+ }}
1956
+
1957
+ if (cancelBtn) {{
1958
+ cancelBtn.onclick = function(e) {{
1959
+ e.preventDefault();
1960
+ e.stopPropagation();
1961
+ viewMode.style.display = 'block';
1962
+ editMode.style.display = 'none';
1963
+ return false;
1964
+ }};
1965
+ }}
1966
+
1967
+ if (aiBtn) {{
1968
+ aiBtn.onclick = function(e) {{
1969
+ e.preventDefault();
1970
+ e.stopPropagation();
1971
+ const instruction = document.getElementById('instruction_' + slideIndex).value;
1972
+ if (instruction) {{
1973
+ window.aiRegenerateRequest = {{
1974
+ slideIndex: slideIndex,
1975
+ instruction: instruction,
1976
+ topic: "{topic}"
1977
+ }};
1978
+ alert('AI μž¬μƒμ„±μ΄ μš”μ²­λ˜μ—ˆμŠ΅λ‹ˆλ‹€. μ‹€μ œ κ΅¬ν˜„μ—μ„œλŠ” μ„œλ²„μ™€ ν†΅μ‹ ν•©λ‹ˆλ‹€.');
1979
+ }}
1980
+ return false;
1981
+ }};
1982
+ }}
1983
+ }}, 100);
1984
+ }})();
1985
  </script>
1986
  """
1987
 
 
2009
  if template_name == "μ‚¬μš©μž μ •μ˜" and custom_slides:
2010
  slides = [{"title": "ν‘œοΏ½οΏ½", "style": "Title Slide (Hero)", "prompt_hint": "ν”„λ ˆμ  ν…Œμ΄μ…˜ ν‘œμ§€"}]
2011
  slides.extend(custom_slides)
2012
+ slides.append({"title": "Thank You", "style": "Thank You Slide", "prompt_hint": "ν”„λ ˆμ  ν…Œμ΄μ…˜ λ§ˆλ¬΄λ¦¬μ™€ 핡심 λ©”μ‹œμ§€"})
2013
  else:
2014
  template = PPT_TEMPLATES[template_name]
2015
  slides = generate_dynamic_slides(topic, template, slide_count)
 
2051
 
2052
  # ν…μŠ€νŠΈ λ‚΄μš© 생성
2053
  slide_context = f"{slide['title']} - {slide.get('prompt_hint', '')}"
 
 
 
 
2054
 
2055
+ # Thank You μŠ¬λΌμ΄λ“œλŠ” κ²°λ‘  문ꡬ 생성
2056
+ if slide['title'] == 'Thank You':
2057
+ conclusion_phrase = generate_conclusion_phrase(topic)
2058
+ content = {
2059
+ "subtitle": conclusion_phrase,
2060
+ "bullet_points": []
2061
+ }
2062
+ # Thank You μŠ¬λΌμ΄λ“œμš© νŠΉλ³„ λ…ΈνŠΈ
2063
+ speaker_notes = generate_closing_notes(topic, conclusion_phrase)
2064
+ else:
2065
+ content = generate_slide_content(
2066
+ topic, slide['title'], slide_context,
2067
+ uploaded_content, web_search_results
2068
+ )
2069
+ # 일반 μŠ¬λΌμ΄λ“œ λ°œν‘œμž λ…ΈνŠΈ
2070
+ speaker_notes = generate_presentation_notes(topic, slide['title'], content)
2071
 
2072
  # ν”„λ‘¬ν”„νŠΈ 생성 및 이미지 생성
2073
  style_key = slide["style"]
 
2096
 
2097
  # 프리뷰 HTML 생성 (νŽΈμ§‘ κ°€λŠ₯ν•œ μΈν„°νŽ˜μ΄μŠ€)
2098
  if slide_data.get('title') not in ['ν‘œμ§€', 'Thank You']:
2099
+ preview_html += create_editable_slide_interface(slide_data, i, topic)
2100
  else:
2101
  preview_html += create_slide_preview_html(slide_data)
2102
 
 
2198
  .slide-container:hover { transform: translateY(-2px); box-shadow: 0 12px 24px rgba(0, 0, 0, 0.15) !important; }
2199
  input[type="text"], textarea { transition: all 0.3s ease; }
2200
  input[type="text"]:focus, textarea:focus { border-color: #3498db !important; outline: none; box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.2); }
2201
+ button {
2202
+ transition: all 0.3s ease;
2203
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
2204
+ cursor: pointer !important;
2205
+ user-select: none;
2206
+ -webkit-user-select: none;
2207
+ }
2208
  button:hover { transform: translateY(-1px); box-shadow: 0 4px 8px rgba(0,0,0,0.2); opacity: 0.9; }
2209
+ button:active { transform: translateY(0); }
2210
  .edit-mode { background: #f8f9fa; padding: 20px; border-radius: 8px; }
2211
  .ai-instruction { background: #f3e5f5; padding: 15px; border-radius: 8px; margin-top: 15px; }
2212
  """) as demo:
2213
  gr.Markdown("""
2214
+ # 🎯 AI 기반 PPT 톡합 생성기 (κ°„κ²°ν•œ μŠ€νƒ€μΌ 버전)
2215
 
2216
  ### ν…μŠ€νŠΈμ™€ 이미지가 μ™„λ²½ν•˜κ²Œ μ‘°ν™”λœ ν”„λ ˆμ  ν…Œμ΄μ…˜μ„ μžλ™μœΌλ‘œ μƒμ„±ν•˜κ³  νŽΈμ§‘ ν›„ λ‹€μš΄λ‘œλ“œν•˜μ„Έμš”!
2217
 
2218
  #### πŸ†• μƒˆλ‘œμš΄ κΈ°λŠ₯:
2219
+ - πŸ“ **κ°„κ²°ν•œ λͺ…μ‚¬ν˜• ν…μŠ€νŠΈ**: "~μž„", "~함" μŠ€νƒ€μΌμ˜ ν”„λ‘œνŽ˜μ…”λ„ν•œ 문체
2220
+ - 🎨 **이λͺ¨μ§€ μžλ™ μΆ”κ°€**: 각 ν¬μΈνŠΈλ³„ μ μ ˆν•œ 이λͺ¨μ§€λ‘œ μ‹œκ°μ  ꡬ뢄
2221
  - ✏️ **μŠ¬λΌμ΄λ“œ ν…μŠ€νŠΈ νŽΈμ§‘**: μƒμ„±λœ 각 μŠ¬λΌμ΄λ“œμ˜ ν…μŠ€νŠΈλ₯Ό 직접 μˆ˜μ •
2222
  - πŸ€– **AI μž¬μž‘μ„±**: μ§€μ‹œμ‚¬ν•­μ„ μž…λ ₯ν•˜λ©΄ AIκ°€ ν…μŠ€νŠΈλ₯Ό λ‹€μ‹œ μž‘μ„±
2223
  - πŸ“‹ **예제 ν…œν”Œλ¦Ώ**: λ²„νŠΌ 클릭으둜 예제 주제 뢈러였기
 
2323
  4. **AI μž¬μž‘μ„±**: μ§€μ‹œμ‚¬ν•­ μž…λ ₯ ν›„ AI둜 λ‹€μ‹œ μž‘μ„±
2324
  5. **μ΅œμ’… λ‹€μš΄λ‘œλ“œ**: νŽΈμ§‘ μ™„λ£Œ ν›„ λ‹€μš΄λ‘œλ“œ
2325
 
2326
+ ### πŸ’‘ ν…μŠ€νŠΈ μŠ€νƒ€μΌ νŠΉμ§•:
2327
+ - **κ°„κ²°ν•œ λͺ…μ‚¬ν˜• μ’…κ²°**: "~μž„", "~함" λ˜λŠ” λͺ…μ‚¬λ‘œ 끝남
2328
+ - **이λͺ¨μ§€ ν™œμš©**: 각 포인트 μ•žμ— λ‚΄μš© κ΄€λ ¨ 이λͺ¨μ§€ μžλ™ οΏ½οΏ½κ°€
2329
+ - **8-12단어**: 각 뢈릿 ν¬μΈνŠΈλŠ” 맀우 κ°„κ²°ν•˜κ²Œ ꡬ성
2330
+
2331
+ **μŠ€νƒ€μΌ μ˜ˆμ‹œ:**
2332
+ - ❌ "μ‹œμž₯ μ μœ μœ¨μ„ ν™•λŒ€ν•˜κΈ° μœ„ν•œ μ „λž΅μ„ μˆ˜λ¦½ν•©λ‹ˆλ‹€"
2333
+ - βœ… "🎯 μ‹œμž₯ 점유율 ν™•λŒ€ μ „λž΅ 수립"
2334
+ - ❌ "AI κΈ°μˆ μ„ ν™œμš©ν•˜μ—¬ νš¨μœ¨μ„±μ„ ν–₯μƒμ‹œν‚΅λ‹ˆλ‹€"
2335
+ - βœ… "πŸš€ AI 기술 ν™œμš©ν•œ νš¨μœ¨μ„± κ·ΉλŒ€ν™”"
2336
+
2337
+ ### πŸ€– AI μž¬μž‘μ„± 팁:
2338
+ - "더 κ°„λ‹¨ν•˜κ²Œ": ν•΅μ‹¬λ§Œ 남기고 μΆ•μ•½
2339
+ - "숫자 포함": ꡬ체적 μˆ˜μΉ˜λ‚˜ 톡계 μΆ”κ°€
2340
+ - "μ „λ¬Έμ μœΌλ‘œ": 업계 μ „λ¬Έ μš©μ–΄ μ‚¬μš©
2341
+ - "μž„νŒ©νŠΈ 있게": 강쑰점과 핡심 μ„±κ³Ό 뢀각
2342
  """)
2343
 
2344
  # PPTX λ‹€μš΄λ‘œλ“œ μ˜μ—­
 
2483
  yield preview, status, pptx_file
2484
 
2485
  # 이벀트 μ—°κ²°
2486
+ # --- 이벀트 μ—°κ²° -------------------------------------------------
2487
  example_btn.click(
2488
  fn=load_example,
2489
  inputs=[template_select],
2490
  outputs=[topic_input]
2491
  )
2492
+
2493
  theme_select.change(
2494
  fn=update_theme_preview,
2495
  inputs=[theme_select],
2496
  outputs=[theme_preview]
2497
  )
2498
+
2499
  template_select.change(
2500
  fn=update_template_info,
2501
  inputs=[template_select, slide_count],
2502
  outputs=[template_info]
2503
  )
2504
+
2505
  slide_count.change(
2506
  fn=update_template_info,
2507
  inputs=[template_select, slide_count],
2508
  outputs=[template_info]
2509
  )
2510
+
2511
  custom_slide_count.change(
2512
  fn=update_custom_slides_visibility,
2513
  inputs=[custom_slide_count],
2514
+ outputs=[
2515
+ comp
2516
+ for slide in custom_slides_components
2517
+ for comp in (slide["title"], slide["style"], slide["hint"])
2518
+ ]
2519
  )
2520
+
2521
+ # μ‚¬μš©μž μ •μ˜ μŠ¬λΌμ΄λ“œ μž…λ ₯ μ»΄ν¬λ„ŒνŠΈ 평탄화
2522
  all_custom_inputs = []
2523
+ for slide in custom_slides_components:
2524
+ all_custom_inputs.extend([slide["title"], slide["style"], slide["hint"]])
2525
+
 
 
 
 
2526
  generate_btn.click(
2527
  fn=generate_ppt_handler,
2528
+ inputs=[
2529
+ topic_input,
2530
+ template_select,
2531
+ theme_select,
2532
+ slide_count,
2533
+ seed_input,
2534
+ file_upload,
2535
+ use_web_search,
2536
+ custom_slide_count,
2537
+ ]
2538
+ + all_custom_inputs,
2539
+ outputs=[preview_output, status_output, download_file],
2540
  )
2541
+
2542
+ # 초기 λ‘œλ“œμ‹œ ν…œν”Œλ¦Ώ/ν…Œλ§ˆ 정보 ν‘œμ‹œ
2543
  demo.load(
2544
  fn=lambda: (
2545
+ update_template_info(template_select.value, slide_count.value),
2546
+ update_theme_preview(theme_select.value),
2547
  ),
2548
  inputs=[],
2549
+ outputs=[template_info, theme_preview],
2550
  )
2551
 
2552
+ # --- μ•± μ‹€ν–‰ --------------------------------------------------------
2553
  if __name__ == "__main__":
2554
+ demo.launch(ssr_mode=False)
2555
+