Spaces:
Running
Running
Upload 3 files
Browse files- .gitattributes +1 -0
- app.py +632 -0
- cht.ttf +3 -0
- requirements.txt +5 -0
.gitattributes
CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
36 |
+
cht.ttf filter=lfs diff=lfs merge=lfs -text
|
app.py
ADDED
@@ -0,0 +1,632 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# gemini_ppt_generator.py
|
2 |
+
import os
|
3 |
+
import json
|
4 |
+
import requests
|
5 |
+
import tempfile
|
6 |
+
from io import BytesIO
|
7 |
+
from PIL import Image
|
8 |
+
import gradio as gr
|
9 |
+
import google.generativeai as genai
|
10 |
+
from pptx import Presentation
|
11 |
+
from pptx.util import Inches, Pt
|
12 |
+
from pptx.enum.text import PP_ALIGN
|
13 |
+
from pptx.dml.color import RGBColor
|
14 |
+
|
15 |
+
class GeminiPPTGenerator:
|
16 |
+
def __init__(self):
|
17 |
+
self.pexels_headers = {}
|
18 |
+
self.gemini_model = None
|
19 |
+
|
20 |
+
# 版型配置
|
21 |
+
self.themes = {
|
22 |
+
"商務專業": {
|
23 |
+
"bg_color": RGBColor(255, 255, 255),
|
24 |
+
"title_color": RGBColor(31, 73, 125),
|
25 |
+
"text_color": RGBColor(68, 68, 68),
|
26 |
+
"accent_color": RGBColor(79, 129, 189),
|
27 |
+
"layout": "image_right"
|
28 |
+
},
|
29 |
+
"科技創新": {
|
30 |
+
"bg_color": RGBColor(240, 240, 240),
|
31 |
+
"title_color": RGBColor(0, 102, 204),
|
32 |
+
"text_color": RGBColor(51, 51, 51),
|
33 |
+
"accent_color": RGBColor(255, 102, 0),
|
34 |
+
"layout": "image_background"
|
35 |
+
},
|
36 |
+
"創意設計": {
|
37 |
+
"bg_color": RGBColor(255, 250, 250),
|
38 |
+
"title_color": RGBColor(220, 20, 60),
|
39 |
+
"text_color": RGBColor(70, 70, 70),
|
40 |
+
"accent_color": RGBColor(255, 140, 0),
|
41 |
+
"layout": "image_left"
|
42 |
+
},
|
43 |
+
"教育學術": {
|
44 |
+
"bg_color": RGBColor(255, 255, 255),
|
45 |
+
"title_color": RGBColor(102, 51, 153),
|
46 |
+
"text_color": RGBColor(85, 85, 85),
|
47 |
+
"accent_color": RGBColor(153, 204, 0),
|
48 |
+
"layout": "image_top"
|
49 |
+
}
|
50 |
+
}
|
51 |
+
|
52 |
+
# 圖片風格
|
53 |
+
self.image_styles = {
|
54 |
+
"professional": "business professional corporate clean",
|
55 |
+
"creative": "creative artistic colorful vibrant",
|
56 |
+
"minimalist": "minimal clean simple white space",
|
57 |
+
"modern": "modern contemporary sleek design",
|
58 |
+
"natural": "natural outdoor organic environment",
|
59 |
+
"technology": "technology digital modern tech innovation"
|
60 |
+
}
|
61 |
+
|
62 |
+
def setup_apis(self, gemini_api_key, pexels_api_key):
|
63 |
+
"""設定 API 金鑰"""
|
64 |
+
try:
|
65 |
+
# 設定 Gemini API
|
66 |
+
if gemini_api_key:
|
67 |
+
genai.configure(api_key=gemini_api_key)
|
68 |
+
self.gemini_model = genai.GenerativeModel('gemini-2.5-flash-preview-05-20')
|
69 |
+
|
70 |
+
# 設定 Pexels API
|
71 |
+
if pexels_api_key:
|
72 |
+
self.pexels_headers = {
|
73 |
+
"Authorization": pexels_api_key
|
74 |
+
}
|
75 |
+
|
76 |
+
return True, "✅ API 設定成功"
|
77 |
+
except Exception as e:
|
78 |
+
return False, f"❌ API 設定失敗:{str(e)}"
|
79 |
+
|
80 |
+
def generate_content_with_gemini(self, topic, slide_count=5):
|
81 |
+
"""使用 Gemini 生成簡報內容"""
|
82 |
+
|
83 |
+
prompt = f"""
|
84 |
+
請為主題「{topic}」製作一個 {slide_count} 頁的簡報大綱,以 JSON 格式回傳。
|
85 |
+
|
86 |
+
格式要求:
|
87 |
+
{{
|
88 |
+
"title": "簡報主標題",
|
89 |
+
"subtitle": "簡報副標題",
|
90 |
+
"slides": [
|
91 |
+
{{
|
92 |
+
"title": "投影片標題",
|
93 |
+
"content": [
|
94 |
+
"重點1",
|
95 |
+
"重點2",
|
96 |
+
"重點3"
|
97 |
+
],
|
98 |
+
"image_keywords": "英文關鍵字,用於搜尋相關圖片"
|
99 |
+
}}
|
100 |
+
]
|
101 |
+
}}
|
102 |
+
|
103 |
+
要求:
|
104 |
+
1. 內容要專業且有邏輯性
|
105 |
+
2. 每頁 3-4 個重點
|
106 |
+
3. image_keywords 要用英文,描述該投影片適合的圖片內容
|
107 |
+
4. 關鍵字要具體明確,例如 "business meeting", "technology innovation", "data analysis"
|
108 |
+
5. 使用繁體中文(除了 image_keywords)
|
109 |
+
6. 第一頁是概述介紹,最後一頁是結論總結
|
110 |
+
7. 請直接回傳 JSON,不要包含其他文字說明
|
111 |
+
"""
|
112 |
+
|
113 |
+
try:
|
114 |
+
if self.gemini_model:
|
115 |
+
response = self.gemini_model.generate_content(prompt)
|
116 |
+
content = response.text
|
117 |
+
|
118 |
+
# 清理回應內容,提取 JSON
|
119 |
+
content = content.strip()
|
120 |
+
if content.startswith('```json'):
|
121 |
+
content = content[7:]
|
122 |
+
if content.endswith('```'):
|
123 |
+
content = content[:-3]
|
124 |
+
|
125 |
+
# 尋找 JSON 開始和結束位置
|
126 |
+
start = content.find('{')
|
127 |
+
end = content.rfind('}') + 1
|
128 |
+
|
129 |
+
if start != -1 and end > start:
|
130 |
+
json_str = content[start:end]
|
131 |
+
return json.loads(json_str)
|
132 |
+
else:
|
133 |
+
raise ValueError("無法在回應中找到有效的 JSON")
|
134 |
+
|
135 |
+
else:
|
136 |
+
return self.get_default_structure_with_images(topic)
|
137 |
+
|
138 |
+
except Exception as e:
|
139 |
+
print(f"Gemini API 錯誤: {e}")
|
140 |
+
return self.get_default_structure_with_images(topic)
|
141 |
+
|
142 |
+
def get_default_structure_with_images(self, topic):
|
143 |
+
"""預設簡報結構(含圖片關鍵字)"""
|
144 |
+
return {
|
145 |
+
"title": f"{topic} 簡報",
|
146 |
+
"subtitle": "由 AI 自動生成",
|
147 |
+
"slides": [
|
148 |
+
{
|
149 |
+
"title": "簡介與背景",
|
150 |
+
"content": [
|
151 |
+
"主題背景介紹",
|
152 |
+
"研究目的與範圍",
|
153 |
+
"簡報架構說明"
|
154 |
+
],
|
155 |
+
"image_keywords": "presentation introduction business"
|
156 |
+
},
|
157 |
+
{
|
158 |
+
"title": "主要內容分析",
|
159 |
+
"content": [
|
160 |
+
"核心概念說明",
|
161 |
+
"重要特點分析",
|
162 |
+
"相關案例討論"
|
163 |
+
],
|
164 |
+
"image_keywords": "analysis data research content"
|
165 |
+
},
|
166 |
+
{
|
167 |
+
"title": "深入探討",
|
168 |
+
"content": [
|
169 |
+
"優勢與機會識別",
|
170 |
+
"挑戰與問題分析",
|
171 |
+
"影響因素評估"
|
172 |
+
],
|
173 |
+
"image_keywords": "strategy planning discussion"
|
174 |
+
},
|
175 |
+
{
|
176 |
+
"title": "解決方案與建議",
|
177 |
+
"content": [
|
178 |
+
"策略建議提出",
|
179 |
+
"實施方法規劃",
|
180 |
+
"預期效果評估"
|
181 |
+
],
|
182 |
+
"image_keywords": "solution implementation strategy"
|
183 |
+
},
|
184 |
+
{
|
185 |
+
"title": "結論與展望",
|
186 |
+
"content": [
|
187 |
+
"重點總結回顧",
|
188 |
+
"未來發展趨勢",
|
189 |
+
"行動建議提出"
|
190 |
+
],
|
191 |
+
"image_keywords": "conclusion future success"
|
192 |
+
}
|
193 |
+
]
|
194 |
+
}
|
195 |
+
|
196 |
+
def search_pexels_with_style(self, keywords, image_style="professional", per_page=10):
|
197 |
+
"""根據風格搜尋 Pexels 圖片"""
|
198 |
+
if not self.pexels_headers:
|
199 |
+
return None
|
200 |
+
|
201 |
+
# 組合關鍵字
|
202 |
+
style_modifier = self.image_styles.get(image_style, "")
|
203 |
+
enhanced_keywords = f"{keywords} {style_modifier}"
|
204 |
+
|
205 |
+
url = "https://api.pexels.com/v1/search"
|
206 |
+
params = {
|
207 |
+
"query": enhanced_keywords,
|
208 |
+
"per_page": per_page,
|
209 |
+
"orientation": "landscape",
|
210 |
+
"size": "medium"
|
211 |
+
}
|
212 |
+
|
213 |
+
try:
|
214 |
+
response = requests.get(url, headers=self.pexels_headers, params=params)
|
215 |
+
if response.status_code == 200:
|
216 |
+
data = response.json()
|
217 |
+
return data["photos"] if data["photos"] else None
|
218 |
+
return None
|
219 |
+
except Exception as e:
|
220 |
+
print(f"Pexels API 錯誤: {e}")
|
221 |
+
return None
|
222 |
+
|
223 |
+
def select_best_image(self, photos, slide_title=""):
|
224 |
+
"""從多張圖片中選擇最適合的"""
|
225 |
+
if not photos:
|
226 |
+
return None
|
227 |
+
|
228 |
+
# 選擇解析度較高的圖片
|
229 |
+
best_photo = photos[0]
|
230 |
+
for photo in photos[:3]:
|
231 |
+
if photo["width"] * photo["height"] > best_photo["width"] * best_photo["height"]:
|
232 |
+
best_photo = photo
|
233 |
+
|
234 |
+
return best_photo["src"]["medium"]
|
235 |
+
|
236 |
+
def download_image(self, image_url):
|
237 |
+
"""下載圖片並返回檔案路徑"""
|
238 |
+
if not image_url:
|
239 |
+
return None
|
240 |
+
|
241 |
+
try:
|
242 |
+
response = requests.get(image_url)
|
243 |
+
if response.status_code == 200:
|
244 |
+
temp_dir = tempfile.mkdtemp()
|
245 |
+
image_path = os.path.join(temp_dir, "slide_image.jpg")
|
246 |
+
|
247 |
+
# 處理圖片
|
248 |
+
image = Image.open(BytesIO(response.content))
|
249 |
+
|
250 |
+
# 調整圖片大小
|
251 |
+
max_size = (800, 600)
|
252 |
+
image.thumbnail(max_size, Image.Resampling.LANCZOS)
|
253 |
+
|
254 |
+
# 轉換並儲存
|
255 |
+
if image.mode in ("RGBA", "P"):
|
256 |
+
image = image.convert("RGB")
|
257 |
+
image.save(image_path, "JPEG", quality=85)
|
258 |
+
|
259 |
+
return image_path
|
260 |
+
return None
|
261 |
+
except Exception as e:
|
262 |
+
print(f"圖片下載錯誤: {e}")
|
263 |
+
return None
|
264 |
+
|
265 |
+
def add_image_to_slide(self, slide, image_path, layout_type="image_right"):
|
266 |
+
"""將圖片添加到投影片"""
|
267 |
+
if not image_path or not os.path.exists(image_path):
|
268 |
+
return
|
269 |
+
|
270 |
+
try:
|
271 |
+
if layout_type == "image_right":
|
272 |
+
left, top, width, height = Inches(5.5), Inches(1.5), Inches(4), Inches(3)
|
273 |
+
elif layout_type == "image_left":
|
274 |
+
left, top, width, height = Inches(0.5), Inches(1.5), Inches(4), Inches(3)
|
275 |
+
elif layout_type == "image_top":
|
276 |
+
left, top, width, height = Inches(1), Inches(1), Inches(8), Inches(2.5)
|
277 |
+
elif layout_type == "image_background":
|
278 |
+
left, top, width, height = Inches(0), Inches(0), Inches(10), Inches(7.5)
|
279 |
+
|
280 |
+
picture = slide.shapes.add_picture(image_path, left, top, width, height)
|
281 |
+
|
282 |
+
# 背景圖片移到後層
|
283 |
+
if layout_type == "image_background":
|
284 |
+
picture.element.getparent().remove(picture.element)
|
285 |
+
slide.shapes._spTree.insert(2, picture.element)
|
286 |
+
|
287 |
+
except Exception as e:
|
288 |
+
print(f"添加圖片錯誤: {e}")
|
289 |
+
|
290 |
+
def adjust_content_layout(self, slide, layout_type):
|
291 |
+
"""根據圖片位置調整文字內容位置"""
|
292 |
+
try:
|
293 |
+
content_shape = slide.placeholders[1]
|
294 |
+
|
295 |
+
if layout_type == "image_right":
|
296 |
+
content_shape.left = Inches(0.5)
|
297 |
+
content_shape.width = Inches(4.5)
|
298 |
+
elif layout_type == "image_left":
|
299 |
+
content_shape.left = Inches(5)
|
300 |
+
content_shape.width = Inches(4.5)
|
301 |
+
elif layout_type == "image_top":
|
302 |
+
content_shape.top = Inches(4)
|
303 |
+
content_shape.height = Inches(3)
|
304 |
+
|
305 |
+
except Exception as e:
|
306 |
+
print(f"調整版面錯誤: {e}")
|
307 |
+
|
308 |
+
def format_title(self, shape, theme, font_size):
|
309 |
+
"""格式化標題"""
|
310 |
+
paragraph = shape.text_frame.paragraphs[0]
|
311 |
+
paragraph.font.name = "Microsoft JhengHei"
|
312 |
+
paragraph.font.size = Pt(font_size)
|
313 |
+
paragraph.font.color.rgb = theme["title_color"]
|
314 |
+
paragraph.alignment = PP_ALIGN.CENTER
|
315 |
+
|
316 |
+
def format_content(self, paragraph, theme, font_size):
|
317 |
+
"""格式化內容"""
|
318 |
+
paragraph.font.name = "Microsoft JhengHei"
|
319 |
+
paragraph.font.size = Pt(font_size)
|
320 |
+
paragraph.font.color.rgb = theme["text_color"]
|
321 |
+
|
322 |
+
def create_presentation_with_images(self, topic, theme_name="商務專業",
|
323 |
+
slide_count=5, image_style="professional"):
|
324 |
+
"""建立包含圖片的簡報"""
|
325 |
+
|
326 |
+
# 生成內容結構
|
327 |
+
structure = self.generate_content_with_gemini(topic, slide_count)
|
328 |
+
theme = self.themes[theme_name]
|
329 |
+
layout_type = theme["layout"]
|
330 |
+
|
331 |
+
# 建立簡報
|
332 |
+
prs = Presentation()
|
333 |
+
|
334 |
+
# 建立標題頁
|
335 |
+
title_slide = prs.slides.add_slide(prs.slide_layouts[0])
|
336 |
+
title_shape = title_slide.shapes.title
|
337 |
+
subtitle_shape = title_slide.placeholders[1]
|
338 |
+
|
339 |
+
title_shape.text = structure["title"]
|
340 |
+
subtitle_shape.text = structure["subtitle"]
|
341 |
+
|
342 |
+
# 格式化標題頁
|
343 |
+
self.format_title(title_shape, theme, 44)
|
344 |
+
self.format_title(subtitle_shape, theme, 24)
|
345 |
+
|
346 |
+
# 為標題頁添加主題相關圖片
|
347 |
+
main_keywords = f"{topic} presentation cover"
|
348 |
+
title_photos = self.search_pexels_with_style(main_keywords, image_style)
|
349 |
+
if title_photos:
|
350 |
+
title_image_url = self.select_best_image(title_photos, structure["title"])
|
351 |
+
if title_image_url:
|
352 |
+
title_image_path = self.download_image(title_image_url)
|
353 |
+
if title_image_path:
|
354 |
+
self.add_image_to_slide(title_slide, title_image_path, "image_background")
|
355 |
+
|
356 |
+
# 建立內容頁
|
357 |
+
for i, slide_data in enumerate(structure["slides"]):
|
358 |
+
slide = prs.slides.add_slide(prs.slide_layouts[1])
|
359 |
+
|
360 |
+
# 設定標題
|
361 |
+
title_shape = slide.shapes.title
|
362 |
+
title_shape.text = slide_data["title"]
|
363 |
+
self.format_title(title_shape, theme, 32)
|
364 |
+
|
365 |
+
# 根據版面類型調整內容位置
|
366 |
+
self.adjust_content_layout(slide, layout_type)
|
367 |
+
|
368 |
+
# 設定內容
|
369 |
+
content_shape = slide.placeholders[1]
|
370 |
+
text_frame = content_shape.text_frame
|
371 |
+
text_frame.clear()
|
372 |
+
|
373 |
+
for j, point in enumerate(slide_data["content"]):
|
374 |
+
if j == 0:
|
375 |
+
p = text_frame.paragraphs[0]
|
376 |
+
else:
|
377 |
+
p = text_frame.add_paragraph()
|
378 |
+
|
379 |
+
p.text = point
|
380 |
+
p.level = 0
|
381 |
+
self.format_content(p, theme, 18)
|
382 |
+
|
383 |
+
# 搜尋並添加風格化圖片
|
384 |
+
keywords = slide_data.get("image_keywords", f"{topic} slide {i+1}")
|
385 |
+
photos = self.search_pexels_with_style(keywords, image_style)
|
386 |
+
|
387 |
+
if photos:
|
388 |
+
image_url = self.select_best_image(photos, slide_data["title"])
|
389 |
+
if image_url:
|
390 |
+
image_path = self.download_image(image_url)
|
391 |
+
if image_path:
|
392 |
+
self.add_image_to_slide(slide, image_path, layout_type)
|
393 |
+
|
394 |
+
# 建立感謝頁
|
395 |
+
self.add_thank_you_slide(prs, theme, image_style)
|
396 |
+
|
397 |
+
return prs, structure
|
398 |
+
|
399 |
+
def add_thank_you_slide(self, prs, theme, image_style):
|
400 |
+
"""添加感謝頁"""
|
401 |
+
thank_slide = prs.slides.add_slide(prs.slide_layouts[5])
|
402 |
+
|
403 |
+
# 添加感謝圖片
|
404 |
+
thank_keywords = "thank you appreciation success celebration"
|
405 |
+
thank_photos = self.search_pexels_with_style(thank_keywords, image_style)
|
406 |
+
if thank_photos:
|
407 |
+
thank_image_url = self.select_best_image(thank_photos)
|
408 |
+
if thank_image_url:
|
409 |
+
thank_image_path = self.download_image(thank_image_url)
|
410 |
+
if thank_image_path:
|
411 |
+
self.add_image_to_slide(thank_slide, thank_image_path, "image_background")
|
412 |
+
|
413 |
+
# 添加感謝文字
|
414 |
+
left, top, width, height = Inches(1), Inches(3), Inches(8), Inches(2)
|
415 |
+
textbox = thank_slide.shapes.add_textbox(left, top, width, height)
|
416 |
+
text_frame = textbox.text_frame
|
417 |
+
text_frame.text = "謝謝聆聽\nThank You"
|
418 |
+
|
419 |
+
for paragraph in text_frame.paragraphs:
|
420 |
+
paragraph.font.name = "Microsoft JhengHei"
|
421 |
+
paragraph.font.size = Pt(48)
|
422 |
+
paragraph.font.color.rgb = theme["title_color"]
|
423 |
+
paragraph.alignment = PP_ALIGN.CENTER
|
424 |
+
|
425 |
+
def save_presentation(self, prs, filename):
|
426 |
+
"""儲存簡報"""
|
427 |
+
temp_dir = tempfile.mkdtemp()
|
428 |
+
filepath = os.path.join(temp_dir, filename)
|
429 |
+
prs.save(filepath)
|
430 |
+
return filepath
|
431 |
+
|
432 |
+
def generate_preview_text(self, structure):
|
433 |
+
"""生成簡報預覽文字"""
|
434 |
+
preview = f"📊 {structure['title']}\n"
|
435 |
+
preview += f" {structure['subtitle']}\n\n"
|
436 |
+
|
437 |
+
for i, slide in enumerate(structure['slides'], 1):
|
438 |
+
preview += f"{i}. {slide['title']}\n"
|
439 |
+
for point in slide['content'][:2]:
|
440 |
+
preview += f" • {point}\n"
|
441 |
+
if len(slide['content']) > 2:
|
442 |
+
preview += f" • ...(共 {len(slide['content'])} 個重點)\n"
|
443 |
+
preview += "\n"
|
444 |
+
|
445 |
+
return preview
|
446 |
+
|
447 |
+
def generate_ppt_with_gemini(gemini_api_key, pexels_api_key, topic, theme, slide_count, image_style):
|
448 |
+
"""生成簡報的主要函數"""
|
449 |
+
|
450 |
+
# 檢查輸入
|
451 |
+
if not gemini_api_key.strip():
|
452 |
+
return None, "", "❌ 請輸入 Gemini API 金鑰"
|
453 |
+
|
454 |
+
if not pexels_api_key.strip():
|
455 |
+
return None, "", "❌ 請輸入 Pexels API 金鑰"
|
456 |
+
|
457 |
+
if not topic.strip():
|
458 |
+
return None, "", "❌ 請輸入簡報主題"
|
459 |
+
|
460 |
+
generator = GeminiPPTGenerator()
|
461 |
+
|
462 |
+
try:
|
463 |
+
# 設定 API
|
464 |
+
success, message = generator.setup_apis(gemini_api_key, pexels_api_key)
|
465 |
+
if not success:
|
466 |
+
return None, "", message
|
467 |
+
|
468 |
+
# 生成簡報
|
469 |
+
prs, structure = generator.create_presentation_with_images(
|
470 |
+
topic, theme, slide_count, image_style
|
471 |
+
)
|
472 |
+
|
473 |
+
# 生成預覽
|
474 |
+
preview = generator.generate_preview_text(structure)
|
475 |
+
|
476 |
+
# 儲存檔案
|
477 |
+
filename = f"{topic.replace(' ', '_')}_{image_style}_簡報.pptx"
|
478 |
+
filepath = generator.save_presentation(prs, filename)
|
479 |
+
|
480 |
+
success_msg = f"✅ 成功生成《{topic}》{image_style}風格簡報!({slide_count} 頁,含圖片)"
|
481 |
+
|
482 |
+
return filepath, preview, success_msg
|
483 |
+
|
484 |
+
except Exception as e:
|
485 |
+
return None, "", f"❌ 生成失敗:{str(e)}"
|
486 |
+
|
487 |
+
# Gradio 介面
|
488 |
+
def create_gemini_interface():
|
489 |
+
"""建立 Gradio 介面"""
|
490 |
+
with gr.Blocks(title="Gemini AI 圖文簡報生成器", theme=gr.themes.Soft()) as iface:
|
491 |
+
gr.Markdown("# 🤖 Gemini AI 智能圖文簡報生成器")
|
492 |
+
gr.Markdown("**使用 Google Gemini 2.0 + Pexels 圖庫**,智能生成專業圖文簡報")
|
493 |
+
|
494 |
+
# API 設定區域
|
495 |
+
with gr.Group():
|
496 |
+
gr.Markdown("### 🔑 API 設定")
|
497 |
+
with gr.Row():
|
498 |
+
gemini_api_input = gr.Textbox(
|
499 |
+
label="🤖 Gemini API Key",
|
500 |
+
placeholder="請輸入你的 Gemini API 金鑰",
|
501 |
+
type="password",
|
502 |
+
info="免費額度,前往 https://ai.google.dev/ 獲取"
|
503 |
+
)
|
504 |
+
pexels_api_input = gr.Textbox(
|
505 |
+
label="📸 Pexels API Key",
|
506 |
+
placeholder="請輸入你的 Pexels API 金鑰",
|
507 |
+
type="password",
|
508 |
+
info="免費 200次/月,前往 https://www.pexels.com/api/ 獲取"
|
509 |
+
)
|
510 |
+
|
511 |
+
# 主要設定區域
|
512 |
+
with gr.Row():
|
513 |
+
with gr.Column(scale=2):
|
514 |
+
topic_input = gr.Textbox(
|
515 |
+
label="📝 簡報主題",
|
516 |
+
placeholder="請輸入具體的簡報主題...",
|
517 |
+
value="人工智慧在現代教育中的應用與挑戰"
|
518 |
+
)
|
519 |
+
|
520 |
+
with gr.Row():
|
521 |
+
theme_dropdown = gr.Dropdown(
|
522 |
+
choices=["商務專業", "科技創新", "創意設計", "教育學術"],
|
523 |
+
value="商務專業",
|
524 |
+
label="🎨 版型風格"
|
525 |
+
)
|
526 |
+
|
527 |
+
image_style_dropdown = gr.Dropdown(
|
528 |
+
choices=["professional", "creative", "minimalist", "modern", "natural", "technology"],
|
529 |
+
value="professional",
|
530 |
+
label="🖼️ 圖片風格"
|
531 |
+
)
|
532 |
+
|
533 |
+
slide_count = gr.Slider(
|
534 |
+
minimum=3,
|
535 |
+
maximum=10,
|
536 |
+
value=6,
|
537 |
+
step=1,
|
538 |
+
label="📄 投影片數量"
|
539 |
+
)
|
540 |
+
|
541 |
+
generate_btn = gr.Button("🚀 生成專業簡報", variant="primary", size="lg")
|
542 |
+
|
543 |
+
with gr.Column(scale=1):
|
544 |
+
status_output = gr.Textbox(label="📊 生成狀態", interactive=False)
|
545 |
+
file_output = gr.File(label="📁 下載簡報")
|
546 |
+
|
547 |
+
# 預覽區域
|
548 |
+
with gr.Group():
|
549 |
+
gr.Markdown("### 📋 簡報預覽")
|
550 |
+
preview_output = gr.Textbox(
|
551 |
+
label="內容大綱",
|
552 |
+
placeholder="生成後將顯示簡報大綱...",
|
553 |
+
lines=8,
|
554 |
+
interactive=False
|
555 |
+
)
|
556 |
+
|
557 |
+
# 說明區域
|
558 |
+
with gr.Accordion("📖 使用說明與功能特色", open=False):
|
559 |
+
gr.Markdown("""
|
560 |
+
### 🌟 核心特色
|
561 |
+
|
562 |
+
#### 🤖 Google Gemini 2.0 Flash
|
563 |
+
- **最新模型**:使用 Gemini 2.0 Flash Preview 版本
|
564 |
+
- **免費額度**:Google 提供慷慨的免費使用額度
|
565 |
+
- **中文優化**:對繁體中文有優秀的理解和生成能力
|
566 |
+
- **結構化輸出**:精確生成 JSON 格式的簡報結構
|
567 |
+
|
568 |
+
#### 📸 Pexels 圖片整合
|
569 |
+
- **百萬圖庫**:Pexels 提供高品質免費圖片
|
570 |
+
- **智能匹配**:AI 為每頁生成最適合的搜尋關鍵字
|
571 |
+
- **風格選擇**:6 種圖片風格滿足不同需求
|
572 |
+
- **自動配圖**:每張投影片自動配上相關圖片
|
573 |
+
|
574 |
+
#### 🎨 專業版面設計
|
575 |
+
- **4 種版型**:商務、科技、創意、學術風格
|
576 |
+
- **智能排版**:根據版型自動調整圖文位置
|
577 |
+
- **色彩搭配**:專業的色彩主題設計
|
578 |
+
- **中文字型**:完美支援繁體中文顯示
|
579 |
+
|
580 |
+
### 📋 使用步驟
|
581 |
+
1. **獲取 API 金鑰**:
|
582 |
+
- Gemini API:前往 [Google AI Studio](https://ai.google.dev/) 免費申請
|
583 |
+
- Pexels API:前往 [Pexels API](https://www.pexels.com/api/) 免費申請(200次/月)
|
584 |
+
|
585 |
+
2. **輸入 API 金鑰**:在上方輸入框中填入你的 API 金鑰
|
586 |
+
|
587 |
+
3. **設定簡報參數**:
|
588 |
+
- 輸入具體明確的簡報主題
|
589 |
+
- 選擇適合的版型和圖片風格
|
590 |
+
- 設定所需的投影片數量
|
591 |
+
|
592 |
+
4. **生成簡報**:點擊生成按鈕,系統將自動完成所有工作
|
593 |
+
|
594 |
+
5. **下載使用**:獲得完整的 .pptx 檔案,可直接在 PowerPoint 中使用
|
595 |
+
|
596 |
+
### 💡 專業建議
|
597 |
+
- **主題要具體**:「AI在醫療診斷的應用」比「人工智慧」效果更好
|
598 |
+
- **選對風格**:商務場合用「professional」,創意展示用「creative」
|
599 |
+
- **適當頁數**:建議 5-8 頁,內容豐富但不冗長
|
600 |
+
- **測試 API**:第一次使用建議先測試 API 連接是否正常
|
601 |
+
|
602 |
+
### 🔧 技術特點
|
603 |
+
- **純 Python 實現**:不需要安裝 Microsoft Office
|
604 |
+
- **即時生成**:通常 30-60 秒完成整個簡報
|
605 |
+
- **高品質輸出**:生成的 .pptx 檔案完全相容 PowerPoint
|
606 |
+
- **跨平台支援**:Windows、macOS、Linux 都能正常使用
|
607 |
+
""")
|
608 |
+
|
609 |
+
# 事件綁定
|
610 |
+
generate_btn.click(
|
611 |
+
fn=generate_ppt_with_gemini,
|
612 |
+
inputs=[
|
613 |
+
gemini_api_input,
|
614 |
+
pexels_api_input,
|
615 |
+
topic_input,
|
616 |
+
theme_dropdown,
|
617 |
+
slide_count,
|
618 |
+
image_style_dropdown
|
619 |
+
],
|
620 |
+
outputs=[file_output, preview_output, status_output]
|
621 |
+
)
|
622 |
+
|
623 |
+
return iface
|
624 |
+
|
625 |
+
if __name__ == "__main__":
|
626 |
+
# 啟動應用
|
627 |
+
iface = create_gemini_interface()
|
628 |
+
iface.launch(
|
629 |
+
server_name="0.0.0.0",
|
630 |
+
server_port=7860,
|
631 |
+
share=True
|
632 |
+
)
|
cht.ttf
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:9d5bf4932d31fe94c18cd8cfddc98bc1b14ce10f4e354c682179db290a99c825
|
3 |
+
size 4911464
|
requirements.txt
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
gradio>=4.0.0
|
2 |
+
python-pptx>=0.6.21
|
3 |
+
Pillow>=9.0.0
|
4 |
+
requests>=2.28.0
|
5 |
+
google-generativeai>=0.3.0
|