Update app.py
Browse files
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": "
|
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
|
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.
|
402 |
-
3.
|
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 =
|
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
|
473 |
-
3. Each bullet point should be
|
|
|
474 |
|
475 |
-
|
|
|
|
|
|
|
|
|
|
|
476 |
|
477 |
-
|
478 |
-
|
479 |
-
|
480 |
-
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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": ["
|
543 |
}
|
544 |
except Exception as e:
|
545 |
print(f"[AI μ¬μμ±] μ€λ₯: {str(e)}")
|
546 |
return {
|
547 |
"subtitle": slide_title,
|
548 |
-
"bullet_points": ["
|
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
|
566 |
-
3. Each bullet point should be
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
567 |
|
568 |
-
Guidelines:
|
569 |
-
-
|
570 |
-
-
|
571 |
-
|
572 |
-
|
573 |
-
|
574 |
-
|
|
|
|
|
|
|
|
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
640 |
|
641 |
return {
|
642 |
"subtitle": subtitle,
|
@@ -645,17 +815,17 @@ Context: {slide_context}"""
|
|
645 |
else:
|
646 |
return {
|
647 |
"subtitle": slide_title,
|
648 |
-
"bullet_points": ["
|
649 |
}
|
650 |
except Exception as e:
|
651 |
print(f"[μ¬λΌμ΄λ λ΄μ©] μ€λ₯: {str(e)}")
|
652 |
return {
|
653 |
"subtitle": slide_title,
|
654 |
-
"bullet_points": ["
|
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.
|
942 |
-
padding:
|
943 |
border-radius: 20px;
|
944 |
-
box-shadow: 0
|
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:
|
952 |
-
<h2 style="font-size: 24px; color: #
|
953 |
"""
|
954 |
else: # Thank You
|
955 |
html += f"""
|
956 |
-
<h1 style="font-size:
|
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 |
-
{
|
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.
|
1127 |
-
Inches(12), Inches(
|
1128 |
)
|
1129 |
title_bg.fill.solid()
|
1130 |
title_bg.fill.fore_color.rgb = RGBColor(255, 255, 255) # ν°μ λ°°κ²½
|
1131 |
-
title_bg.fill.transparency = 0.
|
1132 |
title_bg.line.fill.background()
|
1133 |
|
1134 |
-
# κ·Έλ¦Όμ ν¨κ³Ό μΆκ°
|
1135 |
shadow = title_bg.shadow
|
1136 |
shadow.visible = True
|
1137 |
-
shadow.distance = Pt(
|
1138 |
-
shadow.size =
|
1139 |
-
shadow.blur_radius = Pt(
|
1140 |
-
shadow.transparency = 0.
|
1141 |
shadow.angle = 45
|
1142 |
|
1143 |
# μ λͺ© ν
μ€νΈ μΆκ°
|
1144 |
title_box = slide.shapes.add_textbox(
|
1145 |
-
Inches(2), Inches(3),
|
1146 |
-
Inches(12), Inches(
|
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(
|
1154 |
title_para.alignment = PP_ALIGN.CENTER
|
1155 |
|
1156 |
-
# λΆμ λͺ© μΆκ°
|
1157 |
subtitle_box = slide.shapes.add_textbox(
|
1158 |
-
Inches(2), Inches(
|
1159 |
-
Inches(12), Inches(
|
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(
|
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(
|
1194 |
-
Inches(
|
1195 |
)
|
1196 |
thanks_bg.fill.solid()
|
1197 |
thanks_bg.fill.fore_color.rgb = RGBColor(255, 255, 255) # ν°μ λ°°κ²½
|
1198 |
-
thanks_bg.fill.transparency = 0.
|
1199 |
thanks_bg.line.fill.background()
|
1200 |
|
1201 |
-
# κ·Έλ¦Όμ ν¨κ³Ό μΆκ°
|
1202 |
shadow = thanks_bg.shadow
|
1203 |
shadow.visible = True
|
1204 |
-
shadow.distance = Pt(
|
1205 |
-
shadow.size =
|
1206 |
-
shadow.blur_radius = Pt(
|
1207 |
-
shadow.transparency = 0.
|
1208 |
shadow.angle = 45
|
1209 |
|
1210 |
-
# Thank You ν
μ€νΈ
|
1211 |
thanks_box = slide.shapes.add_textbox(
|
1212 |
-
Inches(2), Inches(
|
1213 |
-
Inches(12), Inches(
|
1214 |
)
|
1215 |
thanks_frame = thanks_box.text_frame
|
1216 |
-
thanks_frame.text =
|
1217 |
thanks_para = thanks_frame.paragraphs[0]
|
1218 |
-
thanks_para.font.size = Pt(
|
1219 |
thanks_para.font.bold = True
|
1220 |
-
thanks_para.font.color.rgb = RGBColor(
|
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 |
-
|
|
|
|
|
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
|
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 |
-
{
|
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="
|
1571 |
-
<button
|
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
|
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
|
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 |
-
|
1637 |
-
|
1638 |
-
|
1639 |
-
|
1640 |
-
|
1641 |
-
|
1642 |
-
|
1643 |
-
|
1644 |
-
|
1645 |
-
|
1646 |
-
|
1647 |
-
|
1648 |
-
|
1649 |
-
|
1650 |
-
|
1651 |
-
|
1652 |
-
|
1653 |
-
|
1654 |
-
|
1655 |
-
|
1656 |
-
|
1657 |
-
|
1658 |
-
|
1659 |
-
|
1660 |
-
|
1661 |
-
|
1662 |
-
|
1663 |
-
|
1664 |
-
|
1665 |
-
|
1666 |
-
|
1667 |
-
|
1668 |
-
|
1669 |
-
|
1670 |
-
|
1671 |
-
|
1672 |
-
|
1673 |
-
|
1674 |
-
|
1675 |
-
|
1676 |
-
|
1677 |
-
|
1678 |
-
|
1679 |
-
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 {
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
-
|
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=[
|
|
|
|
|
|
|
|
|
2181 |
)
|
2182 |
-
|
2183 |
-
# μ¬μ©μ μ μ μ
λ ₯
|
2184 |
all_custom_inputs = []
|
2185 |
-
for
|
2186 |
-
all_custom_inputs.extend([
|
2187 |
-
|
2188 |
-
slide_components["style"],
|
2189 |
-
slide_components["hint"]
|
2190 |
-
])
|
2191 |
-
|
2192 |
generate_btn.click(
|
2193 |
fn=generate_ppt_handler,
|
2194 |
-
inputs=[
|
2195 |
-
|
2196 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2197 |
)
|
2198 |
-
|
2199 |
-
# μ΄κΈ°
|
2200 |
demo.load(
|
2201 |
fn=lambda: (
|
2202 |
-
update_template_info(
|
2203 |
-
update_theme_preview(
|
2204 |
),
|
2205 |
inputs=[],
|
2206 |
-
outputs=[template_info, theme_preview]
|
2207 |
)
|
2208 |
|
2209 |
-
# μ± μ€ν
|
2210 |
if __name__ == "__main__":
|
2211 |
-
|
2212 |
-
|
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 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|