openfree commited on
Commit
078c7e7
ยท
verified ยท
1 Parent(s): b3fc026

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +197 -127
app.py CHANGED
@@ -47,6 +47,11 @@ except ImportError as e:
47
  DIAGRAM_GENERATORS_AVAILABLE = False
48
  logger.warning(f"๋‹ค์ด์–ด๊ทธ๋žจ ์ƒ์„ฑ๊ธฐ ๋ชจ๋“ˆ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: {e}")
49
 
 
 
 
 
 
50
  ##############################################################################
51
  # API Configuration
52
  ##############################################################################
@@ -106,11 +111,6 @@ def initialize_flux_api():
106
  FLUX_API_ENABLED = False
107
  return False
108
 
109
- ##############################################################################
110
- # Diagram Generation API Configuration - REMOVED (using local generators)
111
- ##############################################################################
112
- # ๊ธฐ์กด Diagram API ๊ด€๋ จ ์ฝ”๋“œ ์ œ๊ฑฐํ•˜๊ณ  ๋กœ์ปฌ ์ƒ์„ฑ๊ธฐ ์‚ฌ์šฉ
113
-
114
  ##############################################################################
115
  # Design Themes and Color Schemes
116
  ##############################################################################
@@ -350,41 +350,70 @@ def get_emoji_for_content(text: str) -> str:
350
  return 'โ–ถ๏ธ'
351
 
352
  ##############################################################################
353
- # Diagram Type Detection - UPDATED for 6 types
354
  ##############################################################################
355
- def detect_diagram_type(title: str, content: str) -> Optional[str]:
356
- """์Šฌ๋ผ์ด๋“œ ๋‚ด์šฉ์„ ๋ถ„์„ํ•˜์—ฌ ์ ์ ˆํ•œ ๋‹ค์ด์–ด๊ทธ๋žจ ํƒ€์ž… ๊ฒฐ์ •"""
357
  combined_text = f"{title} {content}".lower()
358
 
359
- # Process Flow keywords
360
- if any(word in combined_text for word in ['ํ”„๋กœ์„ธ์Šค', 'process', '์ ˆ์ฐจ', 'procedure', '๋‹จ๊ณ„', 'step', 'flow', 'ํ๋ฆ„', '์›Œํฌํ”Œ๋กœ์šฐ', 'workflow', '์ˆœ์„œ', 'sequence']):
361
- return "Process Flow"
362
-
363
- # WBS keywords
364
- elif any(word in combined_text for word in ['wbs', '์ž‘์—…๋ถ„ํ•ด', 'ํ”„๋กœ์ ํŠธ', 'project', '์—…๋ฌด๋ถ„ํ•ด', 'breakdown', '๊ตฌ์กฐ๋„', '์ž‘์—…๊ตฌ์กฐ', 'work structure']):
365
- return "WBS Diagram"
366
-
367
- # Concept Map keywords
368
- elif any(word in combined_text for word in ['๊ฐœ๋…', 'concept', '๊ด€๊ณ„', 'relationship', '์—ฐ๊ด€', 'connection', '๋งˆ์ธ๋“œ๋งต', 'mindmap', '๊ตฌ์กฐ', 'structure', '์ฒด๊ณ„', 'system']):
369
- return "Concept Map"
370
-
371
- # Radial Diagram keywords
372
- elif any(word in combined_text for word in ['์ค‘์‹ฌ', 'central', '๋ฐฉ์‚ฌํ˜•', 'radial', 'ํ•ต์‹ฌ', 'core', '์ฃผ์š”', 'main', '์ค‘์•™', 'center']):
373
- return "Radial Diagram"
374
-
375
- # Synoptic Chart keywords
376
- elif any(word in combined_text for word in ['๊ฐœ์š”', 'overview', '์ „์ฒด', 'overall', '์š”์•ฝ', 'summary', '์‹œ๋†‰ํ‹ฑ', 'synoptic', '์ด๊ด„', 'comprehensive']):
377
- return "Synoptic Chart"
378
 
379
- # Default: ํ‚ค์›Œ๋“œ๊ฐ€ ์—†์œผ๋ฉด ๋‚ด์šฉ์˜ ํŠน์„ฑ์— ๋”ฐ๋ผ ๊ฒฐ์ •
380
- # ๋ฆฌ์ŠคํŠธ๋‚˜ ํ•ญ๋ชฉ์ด ๋งŽ์œผ๋ฉด Concept Map
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
381
  if content.count('\n-') > 3 or content.count('\nโ€ข') > 3:
382
- return "Concept Map"
383
- # ์ˆซ์ž๊ฐ€ ์žˆ์œผ๋ฉด Process Flow
384
- elif any(char in content for char in ['1.', '2.', '3.', 'โ‘ ', 'โ‘ก', 'โ‘ข']):
385
- return "Process Flow"
386
 
387
- return None
 
 
 
 
 
 
 
 
388
 
389
  ##############################################################################
390
  # Generate Diagram JSON using LLM
@@ -394,7 +423,7 @@ def generate_diagram_json(title: str, content: str, diagram_type: str) -> Option
394
  if not FRIENDLI_TOKEN:
395
  return None
396
 
397
- # ๋‹ค์ด์–ด๊ทธ๋žจ ํƒ€์ž…๋ณ„ JSON ๊ตฌ์กฐ ๊ฐ€์ด๋“œ - paste-2.txt์˜ ๊ตฌ์กฐ์™€ ๋™์ผํ•˜๊ฒŒ
398
  json_guides = {
399
  "Concept Map": """Generate a JSON for a concept map with the EXACT following structure:
400
  {
@@ -526,15 +555,18 @@ Important rules:
526
  return None
527
 
528
  ##############################################################################
529
- # Generate Diagram using Local Generators - NEW
530
  ##############################################################################
531
  def generate_diagram_locally(json_data: str, diagram_type: str, output_format: str = "png") -> Optional[str]:
532
- """๋กœ์ปฌ ์ƒ์„ฑ๊ธฐ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋‹ค์ด์–ด๊ทธ๋žจ ์ƒ์„ฑ"""
533
  if not DIAGRAM_GENERATORS_AVAILABLE:
534
  logger.error("๋‹ค์ด์–ด๊ทธ๋žจ ์ƒ์„ฑ๊ธฐ ๋ชจ๋“ˆ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค")
535
  return None
536
 
537
  try:
 
 
 
538
  # ์ ์ ˆํ•œ ์ƒ์„ฑ๊ธฐ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋‹ค์ด์–ด๊ทธ๋žจ ์ƒ์„ฑ
539
  if diagram_type == "Concept Map":
540
  result = generate_concept_map(json_data, output_format)
@@ -1067,7 +1099,7 @@ def generate_images_parallel(prompt_3d: str, prompt_photo: str) -> Tuple[Optiona
1067
  return image_3d, image_photo
1068
 
1069
  ##############################################################################
1070
- # PPT Generation Functions - FIXED VERSION
1071
  ##############################################################################
1072
  def parse_llm_ppt_response(response: str, layout_style: str = "consistent") -> list:
1073
  """Parse LLM response to extract slide content - FIXED VERSION"""
@@ -1495,7 +1527,7 @@ def create_advanced_ppt_from_content(
1495
  include_diagrams: bool = False,
1496
  include_flux_images: bool = False
1497
  ) -> str:
1498
- """Create advanced PPT file with consistent visual design and AI-generated visuals"""
1499
  if not PPTX_AVAILABLE:
1500
  raise ImportError("python-pptx library is required")
1501
 
@@ -1506,8 +1538,32 @@ def create_advanced_ppt_from_content(
1506
  prs.slide_width = Inches(10)
1507
  prs.slide_height = Inches(5.625)
1508
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1509
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1510
- # 1) ์ œ๋ชฉ ์Šฌ๋ผ์ด๋“œ(ํ‘œ์ง€) ์ƒ์„ฑ - ์ˆ˜์ •๋œ ๋ ˆ์ด์•„์›ƒ
1511
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1512
  title_slide_layout = prs.slide_layouts[0]
1513
  slide = prs.slides.add_slide(title_slide_layout)
@@ -1522,7 +1578,7 @@ def create_advanced_ppt_from_content(
1522
  if title_shape:
1523
  title_shape.left = Inches(0.5)
1524
  title_shape.width = prs.slide_width - Inches(1)
1525
- title_shape.top = Inches(1.0) # ์ƒ๋‹จ์— ๋ฐฐ์น˜
1526
  title_shape.height = Inches(1.2)
1527
 
1528
  tf = title_shape.text_frame
@@ -1538,7 +1594,7 @@ def create_advanced_ppt_from_content(
1538
  if subtitle_shape:
1539
  subtitle_shape.left = Inches(0.5)
1540
  subtitle_shape.width = prs.slide_width - Inches(1)
1541
- subtitle_shape.top = Inches(2.2) # ์ œ๋ชฉ ์•„๋ž˜์— ๋ฐฐ์น˜
1542
  subtitle_shape.height = Inches(0.9)
1543
 
1544
  tf2 = subtitle_shape.text_frame
@@ -1550,25 +1606,27 @@ def create_advanced_ppt_from_content(
1550
  p2.font.color.rgb = RGBColor(255, 255, 255)
1551
  p2.alignment = PP_ALIGN.CENTER
1552
 
1553
- # AI ์ด๋ฏธ์ง€๋ฅผ ์šฐ์ธก ํ•˜๋‹จ์— ๋ฐฐ์น˜ (๋‘ API ๋ณ‘๋ ฌ ์‚ฌ์šฉ)
1554
  if include_ai_image and (AI_IMAGE_ENABLED or FLUX_API_ENABLED):
1555
  logger.info("Generating AI cover images via parallel APIs...")
1556
 
1557
- # ๋‘ ๊ฐ€์ง€ ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ
1558
  prompt_3d, prompt_photo = generate_cover_image_prompts(topic, slides_data)
1559
-
1560
- # ๋ณ‘๋ ฌ๋กœ ์ด๋ฏธ์ง€ ์ƒ์„ฑ
1561
  image_3d, image_photo = generate_images_parallel(prompt_3d, prompt_photo)
1562
 
1563
- # ์„ฑ๊ณตํ•œ ์ด๋ฏธ์ง€ ์ค‘ ํ•˜๋‚˜ ์„ ํƒ (3D ์šฐ์„ )
1564
- ai_image_path = image_3d or image_photo
 
 
 
 
 
 
1565
 
1566
  if ai_image_path and os.path.exists(ai_image_path):
1567
  try:
1568
  img = Image.open(ai_image_path)
1569
  img_width, img_height = img.size
1570
 
1571
- # ์ด๋ฏธ์ง€๋ฅผ ์šฐ์ธก ํ•˜๋‹จ์— ๋ฐฐ์น˜
1572
  max_width = Inches(3.5)
1573
  max_height = Inches(2.5)
1574
 
@@ -1580,7 +1638,6 @@ def create_advanced_ppt_from_content(
1580
  img_h = max_height
1581
  img_w = max_height / ratio
1582
 
1583
- # ์šฐ์ธก ํ•˜๋‹จ ๋ฐฐ์น˜
1584
  left = prs.slide_width - img_w - Inches(0.5)
1585
  top = prs.slide_height - img_h - Inches(0.8)
1586
 
@@ -1591,7 +1648,6 @@ def create_advanced_ppt_from_content(
1591
  pic.shadow.distance = Pt(8)
1592
  pic.shadow.angle = 45
1593
 
1594
- # ์ด๋ฏธ์ง€ ์œ„์— ์ž‘์€ ์บก์…˜ ์ถ”๊ฐ€
1595
  caption_box = slide.shapes.add_textbox(
1596
  left, top - Inches(0.3),
1597
  img_w, Inches(0.3)
@@ -1614,14 +1670,14 @@ def create_advanced_ppt_from_content(
1614
  except Exception as e:
1615
  logger.error(f"Failed to add cover image: {e}")
1616
 
1617
- # ์žฅ์‹ ์š”์†Œ
1618
  add_decorative_shapes(slide, theme)
1619
 
1620
- # Add content slides with consistent design
 
 
1621
  for i, slide_data in enumerate(slides_data):
1622
  layout_type = slide_data.get('layout', 'title_content')
1623
 
1624
- # Log slide creation
1625
  logger.info(f"Creating slide {i+1}: {slide_data.get('title', 'No title')}")
1626
  logger.debug(f"Content length: {len(slide_data.get('content', ''))}")
1627
 
@@ -1643,24 +1699,23 @@ def create_advanced_ppt_from_content(
1643
  # Set title
1644
  if slide.shapes.title:
1645
  slide.shapes.title.text = slide_data.get('title', '์ œ๋ชฉ ์—†์Œ')
1646
- # IMMEDIATELY set title font size after setting text
1647
  try:
1648
  title_text_frame = slide.shapes.title.text_frame
1649
  if title_text_frame and title_text_frame.paragraphs:
1650
  for paragraph in title_text_frame.paragraphs:
1651
  if layout_type == 'section_header':
1652
- paragraph.font.size = Pt(28) # Increased from 20
1653
  paragraph.font.color.rgb = RGBColor(255, 255, 255)
1654
  paragraph.alignment = PP_ALIGN.CENTER
1655
  else:
1656
- paragraph.font.size = Pt(24) # Increased from 18
1657
  paragraph.font.color.rgb = theme['colors']['primary']
1658
  paragraph.font.bold = True
1659
  paragraph.font.name = theme['fonts']['title']
1660
  except Exception as e:
1661
  logger.warning(f"Title font sizing failed: {e}")
1662
 
1663
- # Detect if this slide should have a diagram or image
1664
  slide_title = slide_data.get('title', '')
1665
  slide_content = slide_data.get('content', '')
1666
 
@@ -1669,20 +1724,22 @@ def create_advanced_ppt_from_content(
1669
  ['๊ฒฐ๋ก ', 'conclusion', '์š”์•ฝ', 'summary', 'ํ•ต์‹ฌ', 'key',
1670
  '๋งˆ๋ฌด๋ฆฌ', 'closing', '์ •๋ฆฌ', 'takeaway', '์‹œ์‚ฌ์ ', 'implication'])
1671
 
1672
- # ๋‹ค์ด์–ด๊ทธ๋žจ ๋˜๋Š” ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์—ฌ๋ถ€ ๊ฒฐ์ •
1673
  should_add_visual = False
1674
  visual_type = None
1675
 
1676
- # ๊ฒฐ๋ก  ์Šฌ๋ผ์ด๋“œ๋Š” ํ•ญ์ƒ ์ด๋ฏธ์ง€ ์ถ”๊ฐ€
1677
- if is_conclusion_slide and include_flux_images:
 
 
 
 
 
 
1678
  should_add_visual = True
1679
  visual_type = ('conclusion_images', None)
1680
- elif include_diagrams:
1681
- diagram_type = detect_diagram_type(slide_title, slide_content)
1682
- if diagram_type:
1683
- should_add_visual = True
1684
- visual_type = ('diagram', diagram_type)
1685
- elif not should_add_visual and include_flux_images and i % 2 == 0: # ๋งค 2๋ฒˆ์งธ ์Šฌ๋ผ์ด๋“œ์— ์ด๋ฏธ์ง€
1686
  should_add_visual = True
1687
  visual_type = ('diverse_images', None)
1688
 
@@ -1712,14 +1769,13 @@ def create_advanced_ppt_from_content(
1712
 
1713
  if visual_type[0] == 'diagram' and DIAGRAM_GENERATORS_AVAILABLE:
1714
  # ๋‹ค์ด์–ด๊ทธ๋žจ ์ƒ์„ฑ
1715
- logger.info(f"Generating {visual_type[1]} for slide {i+1}")
1716
  diagram_json = generate_diagram_json(slide_title, slide_content, visual_type[1])
1717
 
1718
  if diagram_json:
1719
  diagram_path = generate_diagram_locally(diagram_json, visual_type[1], "png")
1720
  if diagram_path and os.path.exists(diagram_path):
1721
  try:
1722
- # ๋‹ค์ด์–ด๊ทธ๋žจ ์ด๋ฏธ์ง€ ์ถ”๊ฐ€
1723
  pic = slide.shapes.add_picture(
1724
  diagram_path,
1725
  Inches(5.2), Inches(1.5),
@@ -1727,7 +1783,6 @@ def create_advanced_ppt_from_content(
1727
  )
1728
  visual_added = True
1729
 
1730
- # ๋‹ค์ด์–ด๊ทธ๋žจ ์บก์…˜ ์ถ”๊ฐ€
1731
  caption_box = slide.shapes.add_textbox(
1732
  Inches(5.2), Inches(4.6), Inches(4.3), Inches(0.3)
1733
  )
@@ -1738,7 +1793,6 @@ def create_advanced_ppt_from_content(
1738
  caption_p.font.color.rgb = theme['colors']['secondary']
1739
  caption_p.alignment = PP_ALIGN.CENTER
1740
 
1741
- # ์ž„์‹œ ํŒŒ์ผ ์‚ญ์ œ
1742
  try:
1743
  os.unlink(diagram_path)
1744
  except:
@@ -1747,13 +1801,26 @@ def create_advanced_ppt_from_content(
1747
  logger.error(f"Failed to add diagram: {e}")
1748
 
1749
  elif visual_type[0] == 'conclusion_images':
1750
- # ๊ฒฐ๋ก  ์Šฌ๋ผ์ด๋“œ์šฉ ์ด๋ฏธ์ง€ ์ƒ์„ฑ (๋‘ API ๋ณ‘๋ ฌ)
1751
  logger.info(f"Generating conclusion images for slide {i+1}")
1752
  prompt_3d, prompt_photo = generate_conclusion_image_prompts(slide_title, slide_content)
1753
- image_3d, image_photo = generate_images_parallel(prompt_3d, prompt_photo)
1754
 
1755
- # ์„ฑ๊ณตํ•œ ์ด๋ฏธ์ง€ ์ค‘ ํ•˜๋‚˜ ์„ ํƒ
1756
- selected_image = image_photo or image_3d # ๊ฒฐ๋ก ์€ ํฌํ† ๋ฆฌ์–ผ๋ฆฌ์Šคํ‹ฑ ์šฐ์„ 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1757
 
1758
  if selected_image and os.path.exists(selected_image):
1759
  try:
@@ -1764,7 +1831,6 @@ def create_advanced_ppt_from_content(
1764
  )
1765
  visual_added = True
1766
 
1767
- # ์ด๋ฏธ์ง€ ์บก์…˜ ์ถ”๊ฐ€
1768
  caption_box = slide.shapes.add_textbox(
1769
  Inches(5.2), Inches(4.6), Inches(4.3), Inches(0.3)
1770
  )
@@ -1775,7 +1841,6 @@ def create_advanced_ppt_from_content(
1775
  caption_p.font.color.rgb = theme['colors']['secondary']
1776
  caption_p.alignment = PP_ALIGN.CENTER
1777
 
1778
- # ์ž„์‹œ ํŒŒ์ผ ์ •๋ฆฌ
1779
  for temp_path in [image_3d, image_photo]:
1780
  if temp_path and os.path.exists(temp_path):
1781
  try:
@@ -1786,15 +1851,23 @@ def create_advanced_ppt_from_content(
1786
  logger.error(f"Failed to add conclusion image: {e}")
1787
 
1788
  elif visual_type[0] == 'diverse_images':
1789
- # ๋‹ค์–‘ํ•œ ์Šฌ๋ผ์ด๋“œ ์ด๋ฏธ์ง€ ์ƒ์„ฑ (๋‘ API ๋ณ‘๋ ฌ)
1790
- logger.info(f"Generating diverse images for slide {i+1}")
1791
  prompt_3d, prompt_photo = generate_diverse_prompt(slide_title, slide_content, i)
1792
- image_3d, image_photo = generate_images_parallel(prompt_3d, prompt_photo)
1793
 
1794
- # ์Šฌ๋ผ์ด๋“œ ์ธ๋ฑ์Šค์— ๋”ฐ๋ผ ๋ฒˆ๊ฐˆ์•„ ์„ ํƒ
1795
- selected_image = image_3d if i % 2 == 0 else image_photo
1796
- if not selected_image: # ์‹คํŒจ์‹œ ๋‹ค๋ฅธ ๊ฒƒ ์„ ํƒ
1797
- selected_image = image_photo if i % 2 == 0 else image_3d
 
 
 
 
 
 
 
 
 
1798
 
1799
  if selected_image and os.path.exists(selected_image):
1800
  try:
@@ -1805,13 +1878,8 @@ def create_advanced_ppt_from_content(
1805
  )
1806
  visual_added = True
1807
 
1808
- # ์ž„์‹œ ํŒŒ์ผ ์ •๋ฆฌ
1809
- for temp_path in [image_3d, image_photo]:
1810
- if temp_path and os.path.exists(temp_path):
1811
- try:
1812
- os.unlink(temp_path)
1813
- except:
1814
- pass
1815
  except Exception as e:
1816
  logger.error(f"Failed to add slide image: {e}")
1817
 
@@ -1829,7 +1897,6 @@ def create_advanced_ppt_from_content(
1829
  else:
1830
  # ๊ธฐ๋ณธ ๋ ˆ์ด์•„์›ƒ (์‹œ๊ฐ์  ์š”์†Œ ์—†์Œ)
1831
  if layout_type == 'section_header':
1832
- # Section header content handling
1833
  content = slide_data.get('content', '')
1834
  if content:
1835
  logger.info(f"Adding content to section header slide {i+1}: {content[:50]}...")
@@ -1847,7 +1914,6 @@ def create_advanced_ppt_from_content(
1847
  paragraph.font.color.rgb = RGBColor(255, 255, 255)
1848
  paragraph.alignment = PP_ALIGN.CENTER
1849
 
1850
- # Add decorative line
1851
  line = slide.shapes.add_shape(
1852
  MSO_SHAPE.RECTANGLE, Inches(3), Inches(3.2), Inches(4), Pt(4)
1853
  )
@@ -1874,7 +1940,6 @@ def create_advanced_ppt_from_content(
1874
  left_tf.word_wrap = True
1875
  force_font_size(left_tf, 14, theme)
1876
 
1877
- # Apply emoji bullets
1878
  for paragraph in left_tf.paragraphs:
1879
  text = paragraph.text.strip()
1880
  if text and text.startswith(('-', 'โ€ข', 'โ—')) and not has_emoji(text):
@@ -1895,7 +1960,6 @@ def create_advanced_ppt_from_content(
1895
  right_tf.word_wrap = True
1896
  force_font_size(right_tf, 14, theme)
1897
 
1898
- # Apply emoji bullets
1899
  for paragraph in right_tf.paragraphs:
1900
  text = paragraph.text.strip()
1901
  if text and text.startswith(('-', 'โ€ข', 'โ—')) and not has_emoji(text):
@@ -1915,10 +1979,7 @@ def create_advanced_ppt_from_content(
1915
 
1916
  if content and content.strip():
1917
  textbox = slide.shapes.add_textbox(
1918
- Inches(0.5), # left
1919
- Inches(1.5), # top
1920
- Inches(9), # width
1921
- Inches(3.5) # height
1922
  )
1923
 
1924
  tf = textbox.text_frame
@@ -1969,7 +2030,7 @@ def create_advanced_ppt_from_content(
1969
  except Exception as e:
1970
  logger.warning(f"Failed to add slide notes: {e}")
1971
 
1972
- # Add slide number with better design
1973
  slide_number_bg = slide.shapes.add_shape(
1974
  MSO_SHAPE.ROUNDED_RECTANGLE,
1975
  Inches(8.3), Inches(5.0), Inches(1.5), Inches(0.5)
@@ -1984,12 +2045,11 @@ def create_advanced_ppt_from_content(
1984
  )
1985
  slide_number_frame = slide_number_box.text_frame
1986
  slide_number_frame.text = f"{i + 1} / {len(slides_data)}"
1987
- slide_number_frame.paragraphs[0].font.size = Pt(10) # Increased from 8
1988
  slide_number_frame.paragraphs[0].font.color.rgb = RGBColor(255, 255, 255)
1989
  slide_number_frame.paragraphs[0].font.bold = False
1990
  slide_number_frame.paragraphs[0].alignment = PP_ALIGN.CENTER
1991
 
1992
- # Add subtle design element on alternating slides
1993
  if i % 2 == 0:
1994
  accent_shape = slide.shapes.add_shape(
1995
  MSO_SHAPE.OVAL,
@@ -2000,31 +2060,32 @@ def create_advanced_ppt_from_content(
2000
  accent_shape.fill.fore_color.rgb = theme['colors']['accent']
2001
  accent_shape.line.fill.background()
2002
 
2003
- # Add thank you slide with consistent design
 
 
 
2004
  thank_you_layout = prs.slide_layouts[5] if len(prs.slide_layouts) > 5 else prs.slide_layouts[0]
2005
  thank_you_slide = prs.slides.add_slide(thank_you_layout)
2006
 
2007
- # Apply gradient background
2008
  add_gradient_background(thank_you_slide, theme['colors']['secondary'], theme['colors']['primary'])
2009
 
2010
  if thank_you_slide.shapes.title:
2011
  thank_you_slide.shapes.title.text = "๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค"
2012
  try:
2013
  if thank_you_slide.shapes.title.text_frame and thank_you_slide.shapes.title.text_frame.paragraphs:
2014
- thank_you_slide.shapes.title.text_frame.paragraphs[0].font.size = Pt(36) # Increased from 28
2015
  thank_you_slide.shapes.title.text_frame.paragraphs[0].font.bold = True
2016
  thank_you_slide.shapes.title.text_frame.paragraphs[0].font.color.rgb = RGBColor(255, 255, 255)
2017
  thank_you_slide.shapes.title.text_frame.paragraphs[0].alignment = PP_ALIGN.CENTER
2018
  except Exception as e:
2019
  logger.warning(f"Thank you slide styling failed: {e}")
2020
 
2021
- # Add contact or additional info placeholder
2022
  info_box = thank_you_slide.shapes.add_textbox(
2023
  Inches(2), Inches(3.5), Inches(6), Inches(1)
2024
  )
2025
  info_tf = info_box.text_frame
2026
  info_tf.text = "AI๋กœ ์ƒ์„ฑ๋œ ํ”„๋ ˆ์  ํ…Œ์ด์…˜"
2027
- info_tf.paragraphs[0].font.size = Pt(18) # Increased from 14
2028
  info_tf.paragraphs[0].font.color.rgb = RGBColor(255, 255, 255)
2029
  info_tf.paragraphs[0].alignment = PP_ALIGN.CENTER
2030
 
@@ -2255,6 +2316,10 @@ def generate_ppt(
2255
  yield None, "โš ๏ธ ๋‹ค์ด์–ด๊ทธ๋žจ ์ƒ์„ฑ๊ธฐ ๋ชจ๋“ˆ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๋‹ค์ด์–ด๊ทธ๋žจ ์—†์ด ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค.", ""
2256
  include_diagrams = False
2257
 
 
 
 
 
2258
  # Process reference files if provided
2259
  additional_context = ""
2260
  chart_data = None
@@ -2327,12 +2392,12 @@ def generate_ppt(
2327
 
2328
  # AI ์ด๋ฏธ์ง€ ๋ฐ ๋‹ค์ด์–ด๊ทธ๋žจ ์ƒ์„ฑ ์•Œ๋ฆผ
2329
  visual_features = []
2330
- if include_ai_image and FLUX_API_ENABLED:
2331
  visual_features.append("AI 3D ํ‘œ์ง€ ์ด๋ฏธ์ง€")
2332
  if include_diagrams and DIAGRAM_GENERATORS_AVAILABLE:
2333
- visual_features.append("๋‹ค์ด์–ด๊ทธ๋žจ")
2334
  if include_flux_images and FLUX_API_ENABLED:
2335
- visual_features.append("AI ์ƒ์„ฑ ์ด๋ฏธ์ง€")
2336
 
2337
  if visual_features:
2338
  yield None, f"๐Ÿ“ ์Šฌ๋ผ์ด๋“œ ์ƒ์„ฑ ์™„๋ฃŒ!\n\n๐ŸŽจ ์ƒ์„ฑ ์ค‘: {', '.join(visual_features)}... (์‹œ๊ฐ„์ด ์†Œ์š”๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค)", llm_response
@@ -2354,12 +2419,12 @@ def generate_ppt(
2354
  success_msg += f"๐ŸŽจ ๋””์ž์ธ ํ…Œ๋งˆ: {DESIGN_THEMES[design_theme]['name']}\n"
2355
  success_msg += f"๐Ÿ“ ๋ ˆ์ด์•„์›ƒ ์Šคํƒ€์ผ: {layout_style}\n"
2356
 
2357
- if include_ai_image and FLUX_API_ENABLED:
2358
  success_msg += f"๐Ÿ–ผ๏ธ AI ์ƒ์„ฑ ํ‘œ์ง€ ์ด๋ฏธ์ง€ ํฌํ•จ\n"
2359
  if include_diagrams and DIAGRAM_GENERATORS_AVAILABLE:
2360
- success_msg += f"๐Ÿ“Š AI ์ƒ์„ฑ ๋‹ค์ด์–ด๊ทธ๋žจ ํฌํ•จ\n"
2361
  if include_flux_images and FLUX_API_ENABLED:
2362
- success_msg += f"๐ŸŽจ AI ์ƒ์„ฑ ์Šฌ๋ผ์ด๋“œ ์ด๋ฏธ์ง€ ํฌํ•จ\n"
2363
 
2364
  success_msg += f"๐Ÿ“ ์ƒ์„ฑ๋œ ์Šฌ๋ผ์ด๋“œ:\n"
2365
 
@@ -2677,13 +2742,13 @@ with gr.Blocks(css=css, title="AI PPT Generator Pro") as demo:
2677
  include_diagrams = gr.Checkbox(
2678
  label="๐Ÿ“Š AI ๋‹ค์ด์–ด๊ทธ๋žจ",
2679
  value=False,
2680
- info="6๊ฐ€์ง€ ํƒ€์ž…์˜ ๋‹ค์ด์–ด๊ทธ๋žจ ์ž๋™ ์ƒ์„ฑ"
2681
  )
2682
 
2683
  include_flux_images = gr.Checkbox(
2684
  label="๐ŸŽจ ์Šฌ๋ผ์ด๋“œ ์ด๋ฏธ์ง€",
2685
  value=False,
2686
- info="์ผ๋ถ€ ์Šฌ๋ผ์ด๋“œ์— FLUX ์ด๋ฏธ์ง€ ์ถ”๊ฐ€"
2687
  )
2688
 
2689
  reference_files = gr.File(
@@ -2732,19 +2797,17 @@ with gr.Blocks(css=css, title="AI PPT Generator Pro") as demo:
2732
  5. **์ฐธ๊ณ  ์ž๋ฃŒ ์—…๋กœ๋“œ**: PDF, CSV, TXT ํŒŒ์ผ ์ง€์›
2733
  6. **์ƒ์„ฑ ๋ฒ„ํŠผ ํด๋ฆญ**: AI๊ฐ€ ์ž๋™์œผ๋กœ PPT ์ƒ์„ฑ
2734
 
2735
- ### ๐ŸŽจ ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ - 6๊ฐ€์ง€ ๋‹ค์ด์–ด๊ทธ๋žจ ํƒ€์ž…
2736
- - **Process Flow**: ํ”„๋กœ์„ธ์Šค์™€ ์›Œํฌํ”Œ๋กœ์šฐ๋ฅผ ์‹œ๊ฐํ™”
2737
- - **Concept Map**: ๊ฐœ๋…๊ณผ ๊ด€๊ณ„๋ฅผ ๊ตฌ์กฐํ™”ํ•˜์—ฌ ํ‘œํ˜„
2738
- - **WBS Diagram**: ํ”„๋กœ์ ํŠธ ์ž‘์—… ๋ถ„ํ•ด ๊ตฌ์กฐ๋„
2739
- - **Radial Diagram**: ์ค‘์‹ฌ ๊ฐœ๋…๊ณผ ๊ด€๋ จ ์š”์†Œ๋“ค์˜ ๋ฐฉ์‚ฌํ˜• ํ‘œํ˜„
2740
- - **Synoptic Chart**: ์ „์ฒด ๊ฐœ์š”์™€ ์š”์•ฝ์„ ํ•œ๋ˆˆ์— ๋ณด์—ฌ์ฃผ๋Š” ์ฐจํŠธ
2741
- - **์ž๋™ ๊ฐ์ง€**: ์Šฌ๋ผ์ด๋“œ ๋‚ด์šฉ์„ ๋ถ„์„ํ•˜์—ฌ ์ ์ ˆํ•œ ๋‹ค์ด์–ด๊ทธ๋žจ ํƒ€์ž… ์ž๋™ ์„ ํƒ
2742
 
2743
  ### ๐Ÿ’ก ๊ณ ๊ธ‰ ํŒ
2744
- - **๋‹ค์ด์–ด๊ทธ๋žจ ์ž๋™ ์ƒ์„ฑ**: ์Šฌ๋ผ์ด๋“œ ๋‚ด์šฉ์— ๋”ฐ๋ผ 6๊ฐ€์ง€ ๋‹ค์ด์–ด๊ทธ๋žจ ์ค‘ ๊ฐ€์žฅ ์ ํ•ฉํ•œ ํƒ€์ž…์ด ์ž๋™์œผ๋กœ ์„ ํƒ๋ฉ๋‹ˆ๋‹ค
2745
- - **๋ณ‘๋ ฌ ์ด๋ฏธ์ง€ ์ƒ์„ฑ**: 3D ์Šคํƒ€์ผ๊ณผ ํฌํ† ๋ฆฌ์–ผ๋ฆฌ์Šคํ‹ฑ ์ด๋ฏธ์ง€๊ฐ€ ๋™์‹œ์— ์ƒ์„ฑ๋˜์–ด ์‹œ๊ฐ„์ด ์ ˆ์•ฝ๋ฉ๋‹ˆ๋‹ค
2746
- - **์Šค๋งˆํŠธ ๋ฐฐ์น˜**: ๋‹ค์ด์–ด๊ทธ๋žจ๊ณผ ์ด๋ฏธ์ง€๋Š” ํ…์ŠคํŠธ์™€ ๊ฒน์น˜์ง€ ์•Š๋„๋ก ์šฐ์ธก์— ์ž๋™ ๋ฐฐ์น˜๋ฉ๋‹ˆ๋‹ค
2747
- - **ํ‚ค์›Œ๋“œ ๊ธฐ๋ฐ˜ ๊ฐ์ง€**: ํ”„๋กœ์„ธ์Šค, ๊ฐœ๋…, ํ”„๋กœ์ ํŠธ, ์ค‘์‹ฌ, ๊ฐœ์š” ๋“ฑ์˜ ํ‚ค์›Œ๋“œ๋ฅผ ๊ฐ์ง€ํ•˜์—ฌ ๋‹ค์ด์–ด๊ทธ๋žจ ํƒ€์ž…์„ ๊ฒฐ์ •ํ•ฉ๋‹ˆ๋‹ค
2748
  """
2749
  )
2750
 
@@ -2785,6 +2848,13 @@ with gr.Blocks(css=css, title="AI PPT Generator Pro") as demo:
2785
 
2786
  # Initialize APIs on startup
2787
  if __name__ == "__main__":
 
 
 
 
 
 
 
2788
  # Try to initialize APIs in parallel
2789
  with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
2790
  futures = []
@@ -2807,4 +2877,4 @@ if __name__ == "__main__":
2807
  else:
2808
  logger.warning("โš ๏ธ ๋‹ค์ด์–ด๊ทธ๋žจ ์ƒ์„ฑ๊ธฐ ๋ชจ๋“ˆ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๋‹ค์ด์–ด๊ทธ๋žจ ๊ธฐ๋Šฅ์ด ๋น„ํ™œ์„ฑํ™”๋ฉ๋‹ˆ๋‹ค.")
2809
 
2810
- demo.launch()
 
47
  DIAGRAM_GENERATORS_AVAILABLE = False
48
  logger.warning(f"๋‹ค์ด์–ด๊ทธ๋žจ ์ƒ์„ฑ๊ธฐ ๋ชจ๋“ˆ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: {e}")
49
 
50
+ # ํ•œ๊ธ€ ํฐํŠธ ๊ฒฝ๋กœ ์„ค์ •
51
+ KOREAN_FONT_PATH = os.path.join(os.path.dirname(__file__), "NanumGothic-Regular.ttf")
52
+ if not os.path.exists(KOREAN_FONT_PATH):
53
+ logger.warning(f"ํ•œ๊ธ€ ํฐํŠธ ํŒŒ์ผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: {KOREAN_FONT_PATH}")
54
+
55
  ##############################################################################
56
  # API Configuration
57
  ##############################################################################
 
111
  FLUX_API_ENABLED = False
112
  return False
113
 
 
 
 
 
 
114
  ##############################################################################
115
  # Design Themes and Color Schemes
116
  ##############################################################################
 
350
  return 'โ–ถ๏ธ'
351
 
352
  ##############################################################################
353
+ # Diagram Type Detection with Priority Score
354
  ##############################################################################
355
+ def detect_diagram_type_with_score(title: str, content: str) -> Tuple[Optional[str], float]:
356
+ """์Šฌ๋ผ์ด๋“œ ๋‚ด์šฉ์„ ๋ถ„์„ํ•˜์—ฌ ์ ์ ˆํ•œ ๋‹ค์ด์–ด๊ทธ๋žจ ํƒ€์ž…๊ณผ ํ•„์š”๋„ ์ ์ˆ˜ ๊ฒฐ์ •"""
357
  combined_text = f"{title} {content}".lower()
358
 
359
+ # ๊ฐ ๋‹ค์ด์–ด๊ทธ๋žจ ํƒ€์ž…๋ณ„ ํ‚ค์›Œ๋“œ์™€ ๊ฐ€์ค‘์น˜
360
+ diagram_scores = {
361
+ "Process Flow": 0,
362
+ "WBS Diagram": 0,
363
+ "Concept Map": 0,
364
+ "Radial Diagram": 0,
365
+ "Synoptic Chart": 0
366
+ }
 
 
 
 
 
 
 
 
 
 
 
367
 
368
+ # Process Flow keywords with weights
369
+ if any(word in combined_text for word in ['ํ”„๋กœ์„ธ์Šค', 'process', '์ ˆ์ฐจ', 'procedure']):
370
+ diagram_scores["Process Flow"] += 3
371
+ if any(word in combined_text for word in ['๋‹จ๊ณ„', 'step', 'flow', 'ํ๋ฆ„']):
372
+ diagram_scores["Process Flow"] += 2
373
+ if any(word in combined_text for word in ['์›Œํฌํ”Œ๋กœ์šฐ', 'workflow', '์ˆœ์„œ', 'sequence']):
374
+ diagram_scores["Process Flow"] += 2
375
+
376
+ # WBS keywords with weights
377
+ if any(word in combined_text for word in ['wbs', '์ž‘์—…๋ถ„ํ•ด', 'ํ”„๋กœ์ ํŠธ', 'project']):
378
+ diagram_scores["WBS Diagram"] += 3
379
+ if any(word in combined_text for word in ['์—…๋ฌด๋ถ„ํ•ด', 'breakdown', '๊ตฌ์กฐ๋„', '์ž‘์—…๊ตฌ์กฐ']):
380
+ diagram_scores["WBS Diagram"] += 2
381
+
382
+ # Concept Map keywords with weights
383
+ if any(word in combined_text for word in ['๊ฐœ๋…', 'concept', '๊ด€๊ณ„', 'relationship']):
384
+ diagram_scores["Concept Map"] += 3
385
+ if any(word in combined_text for word in ['์—ฐ๊ด€', 'connection', '๋งˆ์ธ๋“œ๋งต', 'mindmap']):
386
+ diagram_scores["Concept Map"] += 2
387
+ if any(word in combined_text for word in ['๊ตฌ์กฐ', 'structure', '์ฒด๊ณ„', 'system']):
388
+ diagram_scores["Concept Map"] += 1
389
+
390
+ # Radial Diagram keywords with weights
391
+ if any(word in combined_text for word in ['์ค‘์‹ฌ', 'central', '๋ฐฉ์‚ฌํ˜•', 'radial']):
392
+ diagram_scores["Radial Diagram"] += 3
393
+ if any(word in combined_text for word in ['ํ•ต์‹ฌ', 'core', '์ฃผ์š”', 'main']):
394
+ diagram_scores["Radial Diagram"] += 2
395
+
396
+ # Synoptic Chart keywords with weights
397
+ if any(word in combined_text for word in ['๊ฐœ์š”', 'overview', '์ „์ฒด', 'overall']):
398
+ diagram_scores["Synoptic Chart"] += 3
399
+ if any(word in combined_text for word in ['์š”์•ฝ', 'summary', '์‹œ๋†‰ํ‹ฑ', 'synoptic']):
400
+ diagram_scores["Synoptic Chart"] += 2
401
+
402
+ # ์ถ”๊ฐ€ ์ ์ˆ˜ ๋ถ€์—ฌ: ๋ฆฌ์ŠคํŠธ๋‚˜ ๊ตฌ์กฐํ™”๋œ ๋‚ด์šฉ์ด ๋งŽ์€ ๊ฒฝ์šฐ
403
  if content.count('\n-') > 3 or content.count('\nโ€ข') > 3:
404
+ diagram_scores["Concept Map"] += 1
405
+ if any(char in content for char in ['1.', '2.', '3.', 'โ‘ ', 'โ‘ก', 'โ‘ข']):
406
+ diagram_scores["Process Flow"] += 1
 
407
 
408
+ # ๊ฐ€์žฅ ๋†’์€ ์ ์ˆ˜์˜ ๋‹ค์ด์–ด๊ทธ๋žจ ํƒ€์ž… ์„ ํƒ
409
+ max_score = max(diagram_scores.values())
410
+ if max_score > 0:
411
+ best_type = max(diagram_scores.items(), key=lambda x: x[1])[0]
412
+ # ํ•„์š”๋„ ์ ์ˆ˜ ๊ณ„์‚ฐ (0-1 ๋ฒ”์œ„)
413
+ necessity_score = min(max_score / 5.0, 1.0) # ์ตœ๋Œ€ 5์ ์„ 1.0์œผ๋กœ ์ •๊ทœํ™”
414
+ return best_type, necessity_score
415
+
416
+ return None, 0.0
417
 
418
  ##############################################################################
419
  # Generate Diagram JSON using LLM
 
423
  if not FRIENDLI_TOKEN:
424
  return None
425
 
426
+ # ๋‹ค์ด์–ด๊ทธ๋žจ ํƒ€์ž…๋ณ„ JSON ๊ตฌ์กฐ ๊ฐ€์ด๋“œ
427
  json_guides = {
428
  "Concept Map": """Generate a JSON for a concept map with the EXACT following structure:
429
  {
 
555
  return None
556
 
557
  ##############################################################################
558
+ # Generate Diagram using Local Generators with Korean Font
559
  ##############################################################################
560
  def generate_diagram_locally(json_data: str, diagram_type: str, output_format: str = "png") -> Optional[str]:
561
+ """๋กœ์ปฌ ์ƒ์„ฑ๊ธฐ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋‹ค์ด์–ด๊ทธ๋žจ ์ƒ์„ฑ (ํ•œ๊ธ€ ํฐํŠธ ์ ์šฉ)"""
562
  if not DIAGRAM_GENERATORS_AVAILABLE:
563
  logger.error("๋‹ค์ด์–ด๊ทธ๋žจ ์ƒ์„ฑ๊ธฐ ๋ชจ๋“ˆ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค")
564
  return None
565
 
566
  try:
567
+ # ํ•œ๊ธ€ ํฐํŠธ ๊ฒฝ๋กœ๋ฅผ ํ™˜๊ฒฝ๋ณ€์ˆ˜๋กœ ์„ค์ •
568
+ os.environ['KOREAN_FONT_PATH'] = KOREAN_FONT_PATH
569
+
570
  # ์ ์ ˆํ•œ ์ƒ์„ฑ๊ธฐ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋‹ค์ด์–ด๊ทธ๋žจ ์ƒ์„ฑ
571
  if diagram_type == "Concept Map":
572
  result = generate_concept_map(json_data, output_format)
 
1099
  return image_3d, image_photo
1100
 
1101
  ##############################################################################
1102
+ # PPT Generation Functions - FIXED WITH LIMITED DIAGRAMS AND IMAGES
1103
  ##############################################################################
1104
  def parse_llm_ppt_response(response: str, layout_style: str = "consistent") -> list:
1105
  """Parse LLM response to extract slide content - FIXED VERSION"""
 
1527
  include_diagrams: bool = False,
1528
  include_flux_images: bool = False
1529
  ) -> str:
1530
+ """Create advanced PPT file with limited visuals (max 6 images + 2 diagrams)"""
1531
  if not PPTX_AVAILABLE:
1532
  raise ImportError("python-pptx library is required")
1533
 
 
1538
  prs.slide_width = Inches(10)
1539
  prs.slide_height = Inches(5.625)
1540
 
1541
+ # ์ด๋ฏธ์ง€์™€ ๋‹ค์ด์–ด๊ทธ๋žจ ์นด์šดํ„ฐ ๋ฐ ์ถ”์ 
1542
+ image_count_3d = 0
1543
+ image_count_flux = 0
1544
+ diagram_count = 0
1545
+ max_images_per_api = 3
1546
+ max_diagrams = 2
1547
+
1548
+ # ๋‹ค์ด์–ด๊ทธ๋žจ์ด ํ•„์š”ํ•œ ์Šฌ๋ผ์ด๋“œ๋ฅผ ๋ฏธ๋ฆฌ ๋ถ„์„
1549
+ diagram_candidates = []
1550
+ if include_diagrams:
1551
+ for i, slide_data in enumerate(slides_data):
1552
+ title = slide_data.get('title', '')
1553
+ content = slide_data.get('content', '')
1554
+ diagram_type, score = detect_diagram_type_with_score(title, content)
1555
+ if diagram_type and score > 0:
1556
+ diagram_candidates.append((i, diagram_type, score))
1557
+
1558
+ # ํ•„์š”๋„ ์ ์ˆ˜๊ฐ€ ๋†’์€ ์ˆœ์œผ๋กœ ์ •๋ ฌํ•˜๊ณ  ์ƒ์œ„ 2๊ฐœ๋งŒ ์„ ํƒ
1559
+ diagram_candidates.sort(key=lambda x: x[2], reverse=True)
1560
+ diagram_candidates = diagram_candidates[:max_diagrams]
1561
+ diagram_slide_indices = [x[0] for x in diagram_candidates]
1562
+ else:
1563
+ diagram_slide_indices = []
1564
+
1565
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1566
+ # 1) ์ œ๋ชฉ ์Šฌ๋ผ์ด๋“œ(ํ‘œ์ง€) ์ƒ์„ฑ
1567
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1568
  title_slide_layout = prs.slide_layouts[0]
1569
  slide = prs.slides.add_slide(title_slide_layout)
 
1578
  if title_shape:
1579
  title_shape.left = Inches(0.5)
1580
  title_shape.width = prs.slide_width - Inches(1)
1581
+ title_shape.top = Inches(1.0)
1582
  title_shape.height = Inches(1.2)
1583
 
1584
  tf = title_shape.text_frame
 
1594
  if subtitle_shape:
1595
  subtitle_shape.left = Inches(0.5)
1596
  subtitle_shape.width = prs.slide_width - Inches(1)
1597
+ subtitle_shape.top = Inches(2.2)
1598
  subtitle_shape.height = Inches(0.9)
1599
 
1600
  tf2 = subtitle_shape.text_frame
 
1606
  p2.font.color.rgb = RGBColor(255, 255, 255)
1607
  p2.alignment = PP_ALIGN.CENTER
1608
 
1609
+ # ํ‘œ์ง€ ์ด๋ฏธ์ง€ (์นด์šดํŠธ์— ํฌํ•จ)
1610
  if include_ai_image and (AI_IMAGE_ENABLED or FLUX_API_ENABLED):
1611
  logger.info("Generating AI cover images via parallel APIs...")
1612
 
 
1613
  prompt_3d, prompt_photo = generate_cover_image_prompts(topic, slides_data)
 
 
1614
  image_3d, image_photo = generate_images_parallel(prompt_3d, prompt_photo)
1615
 
1616
+ # 3D ์šฐ์„  ์„ ํƒ
1617
+ ai_image_path = None
1618
+ if image_3d and image_count_3d < max_images_per_api:
1619
+ ai_image_path = image_3d
1620
+ image_count_3d += 1
1621
+ elif image_photo and image_count_flux < max_images_per_api:
1622
+ ai_image_path = image_photo
1623
+ image_count_flux += 1
1624
 
1625
  if ai_image_path and os.path.exists(ai_image_path):
1626
  try:
1627
  img = Image.open(ai_image_path)
1628
  img_width, img_height = img.size
1629
 
 
1630
  max_width = Inches(3.5)
1631
  max_height = Inches(2.5)
1632
 
 
1638
  img_h = max_height
1639
  img_w = max_height / ratio
1640
 
 
1641
  left = prs.slide_width - img_w - Inches(0.5)
1642
  top = prs.slide_height - img_h - Inches(0.8)
1643
 
 
1648
  pic.shadow.distance = Pt(8)
1649
  pic.shadow.angle = 45
1650
 
 
1651
  caption_box = slide.shapes.add_textbox(
1652
  left, top - Inches(0.3),
1653
  img_w, Inches(0.3)
 
1670
  except Exception as e:
1671
  logger.error(f"Failed to add cover image: {e}")
1672
 
 
1673
  add_decorative_shapes(slide, theme)
1674
 
1675
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1676
+ # 2) ์ปจํ…์ธ  ์Šฌ๋ผ์ด๋“œ ์ƒ์„ฑ
1677
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1678
  for i, slide_data in enumerate(slides_data):
1679
  layout_type = slide_data.get('layout', 'title_content')
1680
 
 
1681
  logger.info(f"Creating slide {i+1}: {slide_data.get('title', 'No title')}")
1682
  logger.debug(f"Content length: {len(slide_data.get('content', ''))}")
1683
 
 
1699
  # Set title
1700
  if slide.shapes.title:
1701
  slide.shapes.title.text = slide_data.get('title', '์ œ๋ชฉ ์—†์Œ')
 
1702
  try:
1703
  title_text_frame = slide.shapes.title.text_frame
1704
  if title_text_frame and title_text_frame.paragraphs:
1705
  for paragraph in title_text_frame.paragraphs:
1706
  if layout_type == 'section_header':
1707
+ paragraph.font.size = Pt(28)
1708
  paragraph.font.color.rgb = RGBColor(255, 255, 255)
1709
  paragraph.alignment = PP_ALIGN.CENTER
1710
  else:
1711
+ paragraph.font.size = Pt(24)
1712
  paragraph.font.color.rgb = theme['colors']['primary']
1713
  paragraph.font.bold = True
1714
  paragraph.font.name = theme['fonts']['title']
1715
  except Exception as e:
1716
  logger.warning(f"Title font sizing failed: {e}")
1717
 
1718
+ # ์Šฌ๋ผ์ด๋“œ ์ •๋ณด
1719
  slide_title = slide_data.get('title', '')
1720
  slide_content = slide_data.get('content', '')
1721
 
 
1724
  ['๊ฒฐ๋ก ', 'conclusion', '์š”์•ฝ', 'summary', 'ํ•ต์‹ฌ', 'key',
1725
  '๋งˆ๋ฌด๋ฆฌ', 'closing', '์ •๋ฆฌ', 'takeaway', '์‹œ์‚ฌ์ ', 'implication'])
1726
 
1727
+ # ์‹œ๊ฐ์  ์š”์†Œ ์ถ”๊ฐ€ ์—ฌ๋ถ€ ๊ฒฐ์ •
1728
  should_add_visual = False
1729
  visual_type = None
1730
 
1731
+ # 1. ๋‹ค์ด์–ด๊ทธ๋žจ ์šฐ์„  ํ™•์ธ
1732
+ if i in diagram_slide_indices and diagram_count < max_diagrams:
1733
+ should_add_visual = True
1734
+ diagram_info = next(x for x in diagram_candidates if x[0] == i)
1735
+ visual_type = ('diagram', diagram_info[1])
1736
+ diagram_count += 1
1737
+ # 2. ๊ฒฐ๋ก  ์Šฌ๋ผ์ด๋“œ๋Š” ์ด๋ฏธ์ง€ ์ถ”๊ฐ€ (์ด๋ฏธ์ง€ ์ œํ•œ ๋‚ด์—์„œ)
1738
+ elif is_conclusion_slide and include_flux_images and (image_count_3d + image_count_flux) < (max_images_per_api * 2):
1739
  should_add_visual = True
1740
  visual_type = ('conclusion_images', None)
1741
+ # 3. ์ผ๋ฐ˜ ์Šฌ๋ผ์ด๋“œ ์ด๋ฏธ์ง€ (์ด๋ฏธ์ง€ ์ œํ•œ ๋‚ด์—์„œ, 3~4์žฅ๋งˆ๋‹ค ํ•˜๋‚˜์”ฉ)
1742
+ elif include_flux_images and (image_count_3d + image_count_flux) < (max_images_per_api * 2) and i % 3 == 0:
 
 
 
 
1743
  should_add_visual = True
1744
  visual_type = ('diverse_images', None)
1745
 
 
1769
 
1770
  if visual_type[0] == 'diagram' and DIAGRAM_GENERATORS_AVAILABLE:
1771
  # ๋‹ค์ด์–ด๊ทธ๋žจ ์ƒ์„ฑ
1772
+ logger.info(f"Generating {visual_type[1]} for slide {i+1} (Diagram {diagram_count}/{max_diagrams})")
1773
  diagram_json = generate_diagram_json(slide_title, slide_content, visual_type[1])
1774
 
1775
  if diagram_json:
1776
  diagram_path = generate_diagram_locally(diagram_json, visual_type[1], "png")
1777
  if diagram_path and os.path.exists(diagram_path):
1778
  try:
 
1779
  pic = slide.shapes.add_picture(
1780
  diagram_path,
1781
  Inches(5.2), Inches(1.5),
 
1783
  )
1784
  visual_added = True
1785
 
 
1786
  caption_box = slide.shapes.add_textbox(
1787
  Inches(5.2), Inches(4.6), Inches(4.3), Inches(0.3)
1788
  )
 
1793
  caption_p.font.color.rgb = theme['colors']['secondary']
1794
  caption_p.alignment = PP_ALIGN.CENTER
1795
 
 
1796
  try:
1797
  os.unlink(diagram_path)
1798
  except:
 
1801
  logger.error(f"Failed to add diagram: {e}")
1802
 
1803
  elif visual_type[0] == 'conclusion_images':
1804
+ # ๊ฒฐ๋ก  ์Šฌ๋ผ์ด๋“œ์šฉ ์ด๋ฏธ์ง€ ์ƒ์„ฑ
1805
  logger.info(f"Generating conclusion images for slide {i+1}")
1806
  prompt_3d, prompt_photo = generate_conclusion_image_prompts(slide_title, slide_content)
 
1807
 
1808
+ image_3d = None
1809
+ image_photo = None
1810
+
1811
+ if image_count_3d < max_images_per_api:
1812
+ image_3d = generate_ai_image_via_3d_api(prompt_3d)
1813
+ if image_count_flux < max_images_per_api:
1814
+ image_photo = generate_flux_image_via_api(prompt_photo)
1815
+
1816
+ # ์„ ํƒ
1817
+ selected_image = None
1818
+ if image_photo and image_count_flux < max_images_per_api:
1819
+ selected_image = image_photo
1820
+ image_count_flux += 1
1821
+ elif image_3d and image_count_3d < max_images_per_api:
1822
+ selected_image = image_3d
1823
+ image_count_3d += 1
1824
 
1825
  if selected_image and os.path.exists(selected_image):
1826
  try:
 
1831
  )
1832
  visual_added = True
1833
 
 
1834
  caption_box = slide.shapes.add_textbox(
1835
  Inches(5.2), Inches(4.6), Inches(4.3), Inches(0.3)
1836
  )
 
1841
  caption_p.font.color.rgb = theme['colors']['secondary']
1842
  caption_p.alignment = PP_ALIGN.CENTER
1843
 
 
1844
  for temp_path in [image_3d, image_photo]:
1845
  if temp_path and os.path.exists(temp_path):
1846
  try:
 
1851
  logger.error(f"Failed to add conclusion image: {e}")
1852
 
1853
  elif visual_type[0] == 'diverse_images':
1854
+ # ๋‹ค์–‘ํ•œ ์Šฌ๋ผ์ด๋“œ ์ด๋ฏธ์ง€ ์ƒ์„ฑ (API๋ณ„ ์ œํ•œ ํ™•์ธ)
1855
+ logger.info(f"Generating diverse images for slide {i+1} (3D: {image_count_3d}/{max_images_per_api}, FLUX: {image_count_flux}/{max_images_per_api})")
1856
  prompt_3d, prompt_photo = generate_diverse_prompt(slide_title, slide_content, i)
 
1857
 
1858
+ selected_image = None
1859
+
1860
+ # API๋ณ„ ์ œํ•œ์— ๋”ฐ๋ผ ์ด๋ฏธ์ง€ ์ƒ์„ฑ
1861
+ if i % 2 == 0 and image_count_3d < max_images_per_api:
1862
+ image_3d = generate_ai_image_via_3d_api(prompt_3d)
1863
+ if image_3d:
1864
+ selected_image = image_3d
1865
+ image_count_3d += 1
1866
+ elif image_count_flux < max_images_per_api:
1867
+ image_photo = generate_flux_image_via_api(prompt_photo)
1868
+ if image_photo:
1869
+ selected_image = image_photo
1870
+ image_count_flux += 1
1871
 
1872
  if selected_image and os.path.exists(selected_image):
1873
  try:
 
1878
  )
1879
  visual_added = True
1880
 
1881
+ if selected_image:
1882
+ os.unlink(selected_image)
 
 
 
 
 
1883
  except Exception as e:
1884
  logger.error(f"Failed to add slide image: {e}")
1885
 
 
1897
  else:
1898
  # ๊ธฐ๋ณธ ๋ ˆ์ด์•„์›ƒ (์‹œ๊ฐ์  ์š”์†Œ ์—†์Œ)
1899
  if layout_type == 'section_header':
 
1900
  content = slide_data.get('content', '')
1901
  if content:
1902
  logger.info(f"Adding content to section header slide {i+1}: {content[:50]}...")
 
1914
  paragraph.font.color.rgb = RGBColor(255, 255, 255)
1915
  paragraph.alignment = PP_ALIGN.CENTER
1916
 
 
1917
  line = slide.shapes.add_shape(
1918
  MSO_SHAPE.RECTANGLE, Inches(3), Inches(3.2), Inches(4), Pt(4)
1919
  )
 
1940
  left_tf.word_wrap = True
1941
  force_font_size(left_tf, 14, theme)
1942
 
 
1943
  for paragraph in left_tf.paragraphs:
1944
  text = paragraph.text.strip()
1945
  if text and text.startswith(('-', 'โ€ข', 'โ—')) and not has_emoji(text):
 
1960
  right_tf.word_wrap = True
1961
  force_font_size(right_tf, 14, theme)
1962
 
 
1963
  for paragraph in right_tf.paragraphs:
1964
  text = paragraph.text.strip()
1965
  if text and text.startswith(('-', 'โ€ข', 'โ—')) and not has_emoji(text):
 
1979
 
1980
  if content and content.strip():
1981
  textbox = slide.shapes.add_textbox(
1982
+ Inches(0.5), Inches(1.5), Inches(9), Inches(3.5)
 
 
 
1983
  )
1984
 
1985
  tf = textbox.text_frame
 
2030
  except Exception as e:
2031
  logger.warning(f"Failed to add slide notes: {e}")
2032
 
2033
+ # Add slide number
2034
  slide_number_bg = slide.shapes.add_shape(
2035
  MSO_SHAPE.ROUNDED_RECTANGLE,
2036
  Inches(8.3), Inches(5.0), Inches(1.5), Inches(0.5)
 
2045
  )
2046
  slide_number_frame = slide_number_box.text_frame
2047
  slide_number_frame.text = f"{i + 1} / {len(slides_data)}"
2048
+ slide_number_frame.paragraphs[0].font.size = Pt(10)
2049
  slide_number_frame.paragraphs[0].font.color.rgb = RGBColor(255, 255, 255)
2050
  slide_number_frame.paragraphs[0].font.bold = False
2051
  slide_number_frame.paragraphs[0].alignment = PP_ALIGN.CENTER
2052
 
 
2053
  if i % 2 == 0:
2054
  accent_shape = slide.shapes.add_shape(
2055
  MSO_SHAPE.OVAL,
 
2060
  accent_shape.fill.fore_color.rgb = theme['colors']['accent']
2061
  accent_shape.line.fill.background()
2062
 
2063
+ # ์ด๋ฏธ์ง€ ์ƒ์„ฑ ๋กœ๊ทธ
2064
+ logger.info(f"Total images generated - 3D: {image_count_3d}, FLUX: {image_count_flux}, Diagrams: {diagram_count}")
2065
+
2066
+ # Add thank you slide
2067
  thank_you_layout = prs.slide_layouts[5] if len(prs.slide_layouts) > 5 else prs.slide_layouts[0]
2068
  thank_you_slide = prs.slides.add_slide(thank_you_layout)
2069
 
 
2070
  add_gradient_background(thank_you_slide, theme['colors']['secondary'], theme['colors']['primary'])
2071
 
2072
  if thank_you_slide.shapes.title:
2073
  thank_you_slide.shapes.title.text = "๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค"
2074
  try:
2075
  if thank_you_slide.shapes.title.text_frame and thank_you_slide.shapes.title.text_frame.paragraphs:
2076
+ thank_you_slide.shapes.title.text_frame.paragraphs[0].font.size = Pt(36)
2077
  thank_you_slide.shapes.title.text_frame.paragraphs[0].font.bold = True
2078
  thank_you_slide.shapes.title.text_frame.paragraphs[0].font.color.rgb = RGBColor(255, 255, 255)
2079
  thank_you_slide.shapes.title.text_frame.paragraphs[0].alignment = PP_ALIGN.CENTER
2080
  except Exception as e:
2081
  logger.warning(f"Thank you slide styling failed: {e}")
2082
 
 
2083
  info_box = thank_you_slide.shapes.add_textbox(
2084
  Inches(2), Inches(3.5), Inches(6), Inches(1)
2085
  )
2086
  info_tf = info_box.text_frame
2087
  info_tf.text = "AI๋กœ ์ƒ์„ฑ๋œ ํ”„๋ ˆ์  ํ…Œ์ด์…˜"
2088
+ info_tf.paragraphs[0].font.size = Pt(18)
2089
  info_tf.paragraphs[0].font.color.rgb = RGBColor(255, 255, 255)
2090
  info_tf.paragraphs[0].alignment = PP_ALIGN.CENTER
2091
 
 
2316
  yield None, "โš ๏ธ ๋‹ค์ด์–ด๊ทธ๋žจ ์ƒ์„ฑ๊ธฐ ๋ชจ๋“ˆ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๋‹ค์ด์–ด๊ทธ๋žจ ์—†์ด ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค.", ""
2317
  include_diagrams = False
2318
 
2319
+ # ํ•œ๊ธ€ ํฐํŠธ ํ™•์ธ
2320
+ if include_diagrams and not os.path.exists(KOREAN_FONT_PATH):
2321
+ yield None, f"โš ๏ธ ํ•œ๊ธ€ ํฐํŠธ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: {KOREAN_FONT_PATH}\n๋‹ค์ด์–ด๊ทธ๋žจ์— ํ•œ๊ธ€์ด ๊นจ์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.", ""
2322
+
2323
  # Process reference files if provided
2324
  additional_context = ""
2325
  chart_data = None
 
2392
 
2393
  # AI ์ด๋ฏธ์ง€ ๋ฐ ๋‹ค์ด์–ด๊ทธ๋žจ ์ƒ์„ฑ ์•Œ๋ฆผ
2394
  visual_features = []
2395
+ if include_ai_image and (AI_IMAGE_ENABLED or FLUX_API_ENABLED):
2396
  visual_features.append("AI 3D ํ‘œ์ง€ ์ด๋ฏธ์ง€")
2397
  if include_diagrams and DIAGRAM_GENERATORS_AVAILABLE:
2398
+ visual_features.append("๋‹ค์ด์–ด๊ทธ๋žจ (์ตœ๋Œ€ 2๊ฐœ)")
2399
  if include_flux_images and FLUX_API_ENABLED:
2400
+ visual_features.append("AI ์ƒ์„ฑ ์ด๋ฏธ์ง€ (๊ฐ API๋ณ„ ์ตœ๋Œ€ 3๊ฐœ)")
2401
 
2402
  if visual_features:
2403
  yield None, f"๐Ÿ“ ์Šฌ๋ผ์ด๋“œ ์ƒ์„ฑ ์™„๋ฃŒ!\n\n๐ŸŽจ ์ƒ์„ฑ ์ค‘: {', '.join(visual_features)}... (์‹œ๊ฐ„์ด ์†Œ์š”๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค)", llm_response
 
2419
  success_msg += f"๐ŸŽจ ๋””์ž์ธ ํ…Œ๋งˆ: {DESIGN_THEMES[design_theme]['name']}\n"
2420
  success_msg += f"๐Ÿ“ ๋ ˆ์ด์•„์›ƒ ์Šคํƒ€์ผ: {layout_style}\n"
2421
 
2422
+ if include_ai_image and (AI_IMAGE_ENABLED or FLUX_API_ENABLED):
2423
  success_msg += f"๐Ÿ–ผ๏ธ AI ์ƒ์„ฑ ํ‘œ์ง€ ์ด๋ฏธ์ง€ ํฌํ•จ\n"
2424
  if include_diagrams and DIAGRAM_GENERATORS_AVAILABLE:
2425
+ success_msg += f"๐Ÿ“Š AI ์ƒ์„ฑ ๋‹ค์ด์–ด๊ทธ๋žจ ํฌํ•จ (์ตœ๋Œ€ 2๊ฐœ)\n"
2426
  if include_flux_images and FLUX_API_ENABLED:
2427
+ success_msg += f"๐ŸŽจ AI ์ƒ์„ฑ ์Šฌ๋ผ์ด๋“œ ์ด๋ฏธ์ง€ ํฌํ•จ (API๋ณ„ ์ตœ๋Œ€ 3๊ฐœ)\n"
2428
 
2429
  success_msg += f"๐Ÿ“ ์ƒ์„ฑ๋œ ์Šฌ๋ผ์ด๋“œ:\n"
2430
 
 
2742
  include_diagrams = gr.Checkbox(
2743
  label="๐Ÿ“Š AI ๋‹ค์ด์–ด๊ทธ๋žจ",
2744
  value=False,
2745
+ info="ํ•„์š”๋„๊ฐ€ ๋†’์€ 2๊ฐœ ์Šฌ๋ผ์ด๋“œ์— ๋‹ค์ด์–ด๊ทธ๋žจ ์ž๋™ ์ƒ์„ฑ"
2746
  )
2747
 
2748
  include_flux_images = gr.Checkbox(
2749
  label="๐ŸŽจ ์Šฌ๋ผ์ด๋“œ ์ด๋ฏธ์ง€",
2750
  value=False,
2751
+ info="์ฃผ์š” ์Šฌ๋ผ์ด๋“œ์— FLUX ์ด๋ฏธ์ง€ ์ถ”๊ฐ€ (API๋ณ„ ์ตœ๋Œ€ 3๊ฐœ)"
2752
  )
2753
 
2754
  reference_files = gr.File(
 
2797
  5. **์ฐธ๊ณ  ์ž๋ฃŒ ์—…๋กœ๋“œ**: PDF, CSV, TXT ํŒŒ์ผ ์ง€์›
2798
  6. **์ƒ์„ฑ ๋ฒ„ํŠผ ํด๋ฆญ**: AI๊ฐ€ ์ž๋™์œผ๋กœ PPT ์ƒ์„ฑ
2799
 
2800
+ ### ๐ŸŽจ ๊ฐœ์„ ๋œ ๊ธฐ๋Šฅ - ์ตœ์ ํ™”๋œ ์‹œ๊ฐ ์š”์†Œ
2801
+ - **๋‹ค์ด์–ด๊ทธ๋žจ ๊ฐœ์ˆ˜ ์ œํ•œ**: ์ „์ฒด ์Šฌ๋ผ์ด๋“œ ์ค‘ ํ•„์š”๋„๊ฐ€ ๊ฐ€์žฅ ๋†’์€ 2๊ฐœ ์Šฌ๋ผ์ด๋“œ์—๋งŒ ๋‹ค์ด์–ด๊ทธ๋žจ ์ƒ์„ฑ
2802
+ - **์ด๋ฏธ์ง€ ๊ฐœ์ˆ˜ ์ œํ•œ**: ๊ฐ API๋ณ„๋กœ ์ตœ๋Œ€ 3๊ฐœ์”ฉ, ์ด 6๊ฐœ์˜ AI ์ƒ์„ฑ ์ด๋ฏธ์ง€
2803
+ - **ํ•œ๊ธ€ ํฐํŠธ ์ง€์›**: NanumGothic-Regular.ttf๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋‹ค์ด์–ด๊ทธ๋žจ์˜ ํ•œ๊ธ€ ๊นจ์ง ๋ฌธ์ œ ํ•ด๊ฒฐ
2804
+ - **์Šค๋งˆํŠธ ๋ฐฐ์น˜**: ๋‹ค์ด์–ด๊ทธ๋žจ๊ณผ ์ด๋ฏธ์ง€๋Š” ํ…์ŠคํŠธ์™€ ๊ฒน์น˜์ง€ ์•Š๋„๋ก ์šฐ์ธก์— ์ž๋™ ๋ฐฐ์น˜
 
 
2805
 
2806
  ### ๐Ÿ’ก ๊ณ ๊ธ‰ ํŒ
2807
+ - **๋‹ค์ด์–ด๊ทธ๋žจ ์šฐ์„ ๏ฟฝ๏ฟฝ์œ„**: ํ”„๋กœ์„ธ์Šค, WBS, ๊ฐœ๋…๋„ ๋“ฑ์˜ ํ‚ค์›Œ๋“œ๊ฐ€ ์žˆ๋Š” ์Šฌ๋ผ์ด๋“œ๊ฐ€ ์šฐ์„  ์„ ํƒ๋ฉ๋‹ˆ๋‹ค
2808
+ - **์ด๋ฏธ์ง€ ๋ถ„๋ฐฐ**: 3D ์Šคํƒ€์ผ๊ณผ ํฌํ† ๋ฆฌ์–ผ๋ฆฌ์Šคํ‹ฑ ์ด๋ฏธ์ง€๊ฐ€ ๊ท ํ˜•์žˆ๊ฒŒ ๋ถ„๋ฐฐ๋ฉ๋‹ˆ๋‹ค
2809
+ - **ํ‘œ์ง€ ์ด๋ฏธ์ง€**: ์ฃผ์ œ์— ๋งž๋Š” ํ”„๋ฆฌ๋ฏธ์—„ ํ‘œ์ง€ ์ด๋ฏธ์ง€๊ฐ€ ์ž๋™ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค
2810
+ - **ํ•œ๊ธ€ ์ง€์›**: app.py์™€ ๊ฐ™์€ ๊ฒฝ๋กœ์— NanumGothic-Regular.ttf ํŒŒ์ผ์„ ๋ฐฐ์น˜ํ•˜์„ธ์š”
2811
  """
2812
  )
2813
 
 
2848
 
2849
  # Initialize APIs on startup
2850
  if __name__ == "__main__":
2851
+ # ํ•œ๊ธ€ ํฐํŠธ ํ™•์ธ
2852
+ if os.path.exists(KOREAN_FONT_PATH):
2853
+ logger.info(f"โœ… ํ•œ๊ธ€ ํฐํŠธ ํŒŒ์ผ์„ ์ฐพ์•˜์Šต๋‹ˆ๋‹ค: {KOREAN_FONT_PATH}")
2854
+ else:
2855
+ logger.warning(f"โš ๏ธ ํ•œ๊ธ€ ํฐํŠธ ํŒŒ์ผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: {KOREAN_FONT_PATH}")
2856
+ logger.warning("๋‹ค์ด์–ด๊ทธ๋žจ์—์„œ ํ•œ๊ธ€์ด ๊นจ์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. NanumGothic-Regular.ttf ํŒŒ์ผ์„ app.py์™€ ๊ฐ™์€ ๊ฒฝ๋กœ์— ๋ฐฐ์น˜ํ•˜์„ธ์š”.")
2857
+
2858
  # Try to initialize APIs in parallel
2859
  with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
2860
  futures = []
 
2877
  else:
2878
  logger.warning("โš ๏ธ ๋‹ค์ด์–ด๊ทธ๋žจ ์ƒ์„ฑ๊ธฐ ๋ชจ๋“ˆ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๋‹ค์ด์–ด๊ทธ๋žจ ๊ธฐ๋Šฅ์ด ๋น„ํ™œ์„ฑํ™”๋ฉ๋‹ˆ๋‹ค.")
2879
 
2880
+ demo.launch()