dseditor commited on
Commit
09c9f6f
·
verified ·
1 Parent(s): 857d2c6

CommitForLocalAndCloud

Browse files
Files changed (5) hide show
  1. app.py +1002 -867
  2. localapp.py +1056 -0
  3. ppt_analyzer.py +678 -0
  4. setup_and_run.bat +70 -0
  5. slide_themes.py +321 -0
app.py CHANGED
@@ -1,868 +1,1003 @@
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
- # 16:9 簡報尺寸 (單位:英吋)
21
- self.slide_width = Inches(13.333) # 16:9 寬度
22
- self.slide_height = Inches(7.5) # 16:9 高度
23
-
24
- # 版型配置 - 重新設計,避免重疊和變形
25
- self.themes = {
26
- "商務專業": {
27
- "bg_color": RGBColor(255, 255, 255),
28
- "title_color": RGBColor(31, 73, 125),
29
- "text_color": RGBColor(68, 68, 68),
30
- "accent_color": RGBColor(79, 129, 189),
31
- "layout": "image_right",
32
- # 標題:橫跨整個投影片上方
33
- "title_area": {"left": 0.5, "top": 0.3, "width": 12.3, "height": 1.0},
34
- # 內容:左側,給圖片留出右側空間
35
- "content_area": {"left": 0.5, "top": 1.8, "width": 6.5, "height": 5.2},
36
- # 圖片:右側,保持比例
37
- "image_area": {"left": 7.5, "top": 1.8, "width": 5.3, "height": 4.0}
38
- },
39
- "科技創新": {
40
- "bg_color": RGBColor(240, 240, 240),
41
- "title_color": RGBColor(0, 102, 204),
42
- "text_color": RGBColor(51, 51, 51),
43
- "accent_color": RGBColor(255, 102, 0),
44
- "layout": "image_bottom",
45
- # 標題:橫跨整個投影片上方
46
- "title_area": {"left": 0.5, "top": 0.3, "width": 12.3, "height": 1.0},
47
- # 內容:上半部,給圖片留出下方空間
48
- "content_area": {"left": 0.5, "top": 1.8, "width": 12.3, "height": 2.8},
49
- # 圖片:下方,橫向展示
50
- "image_area": {"left": 0.5, "top": 5.0, "width": 12.3, "height": 2.2}
51
- },
52
- "創意設計": {
53
- "bg_color": RGBColor(255, 250, 250),
54
- "title_color": RGBColor(220, 20, 60),
55
- "text_color": RGBColor(70, 70, 70),
56
- "accent_color": RGBColor(255, 140, 0),
57
- "layout": "image_left",
58
- # 標題:橫跨整個投影片上方
59
- "title_area": {"left": 0.5, "top": 0.3, "width": 12.3, "height": 1.0},
60
- # 內容:右側,給圖片留出左側空間
61
- "content_area": {"left": 6.3, "top": 1.8, "width": 6.5, "height": 5.2},
62
- # 圖片:左側,保持比例
63
- "image_area": {"left": 0.5, "top": 1.8, "width": 5.3, "height": 4.0}
64
- },
65
- "教育學術": {
66
- "bg_color": RGBColor(255, 255, 255),
67
- "title_color": RGBColor(102, 51, 153),
68
- "text_color": RGBColor(85, 85, 85),
69
- "accent_color": RGBColor(153, 204, 0),
70
- "layout": "image_top",
71
- # 標題:橫跨整個投影片上方
72
- "title_area": {"left": 0.5, "top": 0.3, "width": 12.3, "height": 1.0},
73
- # 內容:下半部,給圖片留出上方空間
74
- "content_area": {"left": 0.5, "top": 4.2, "width": 12.3, "height": 2.8},
75
- # 圖片:上方,橫向展示
76
- "image_area": {"left": 0.5, "top": 1.8, "width": 12.3, "height": 2.2}
77
- }
78
- }
79
-
80
- # 圖片風格
81
- self.image_styles = {
82
- "professional": "business professional corporate clean",
83
- "creative": "creative artistic colorful vibrant",
84
- "minimalist": "minimal clean simple white space",
85
- "modern": "modern contemporary sleek design",
86
- "natural": "natural outdoor organic environment",
87
- "technology": "technology digital modern tech innovation"
88
- }
89
-
90
- def setup_apis(self, gemini_api_key, pexels_api_key):
91
- """設定 API 金鑰"""
92
- try:
93
- # 設定 Gemini API
94
- if gemini_api_key:
95
- genai.configure(api_key=gemini_api_key)
96
- self.gemini_model = genai.GenerativeModel('gemini-2.5-flash-preview-05-20')
97
-
98
- # 設定 Pexels API
99
- if pexels_api_key:
100
- self.pexels_headers = {
101
- "Authorization": pexels_api_key
102
- }
103
-
104
- return True, "✅ API 設定成功"
105
- except Exception as e:
106
- return False, f"❌ API 設定失敗:{str(e)}"
107
-
108
- def generate_content_with_gemini(self, topic, slide_count=5):
109
- """使用 Gemini 生成簡報內容"""
110
-
111
- prompt = f"""
112
- 請為主題「{topic}」製作一個 {slide_count} 頁的簡報大綱,以 JSON 格��回傳。
113
-
114
- 格式要求:
115
- {{
116
- "title": "簡報主標題",
117
- "subtitle": "簡報副標題",
118
- "slides": [
119
- {{
120
- "title": "投影片標題",
121
- "content": [
122
- "重點1",
123
- "重點2",
124
- "重點3"
125
- ],
126
- "image_keywords": "英文關鍵字,用於搜尋相關圖片"
127
- }}
128
- ]
129
- }}
130
-
131
- 要求:
132
- 1. 內容要專業且有邏輯性
133
- 2. 每頁 3-4 個重點
134
- 3. image_keywords 要用英文,描述該投影片適合的圖片內容
135
- 4. 關鍵字要具體明確,例如 "business meeting", "technology innovation", "data analysis"
136
- 5. 使用繁體中文(除了 image_keywords)
137
- 6. 第一頁是概述介紹,最後一頁是結論總結
138
- 7. 請直接回傳 JSON,不要包含其他文字說明
139
- """
140
-
141
- try:
142
- if self.gemini_model:
143
- response = self.gemini_model.generate_content(prompt)
144
- content = response.text
145
-
146
- # 清理回應內容,提取 JSON
147
- content = content.strip()
148
- if content.startswith('```json'):
149
- content = content[7:]
150
- if content.endswith('```'):
151
- content = content[:-3]
152
-
153
- # 尋找 JSON 開始和結束位置
154
- start = content.find('{')
155
- end = content.rfind('}') + 1
156
-
157
- if start != -1 and end > start:
158
- json_str = content[start:end]
159
- return json.loads(json_str)
160
- else:
161
- raise ValueError("無法在回應中找到有效的 JSON")
162
-
163
- else:
164
- return self.get_default_structure_with_images(topic)
165
-
166
- except Exception as e:
167
- print(f"Gemini API 錯誤: {e}")
168
- return self.get_default_structure_with_images(topic)
169
-
170
- def get_default_structure_with_images(self, topic):
171
- """預設簡報結構(含圖片關鍵字)"""
172
- return {
173
- "title": f"{topic} 簡報",
174
- "subtitle": "由 AI 自動生成",
175
- "slides": [
176
- {
177
- "title": "簡介與背景",
178
- "content": [
179
- "主題背景介紹",
180
- "研究目的與範圍",
181
- "簡報架構說明"
182
- ],
183
- "image_keywords": "presentation introduction business"
184
- },
185
- {
186
- "title": "主要內容分析",
187
- "content": [
188
- "核心概念說明",
189
- "重要特點分析",
190
- "相關案例討論"
191
- ],
192
- "image_keywords": "analysis data research content"
193
- },
194
- {
195
- "title": "深入探討",
196
- "content": [
197
- "優勢與機會識別",
198
- "挑戰與問題分析",
199
- "影響因素評估"
200
- ],
201
- "image_keywords": "strategy planning discussion"
202
- },
203
- {
204
- "title": "解決方案與建議",
205
- "content": [
206
- "策略建議提出",
207
- "實施方法規劃",
208
- "預期效果評估"
209
- ],
210
- "image_keywords": "solution implementation strategy"
211
- },
212
- {
213
- "title": "結論與展望",
214
- "content": [
215
- "重點總結回顧",
216
- "未來發展趨勢",
217
- "行動建議提出"
218
- ],
219
- "image_keywords": "conclusion future success"
220
- }
221
- ]
222
- }
223
-
224
- def search_pexels_with_style(self, keywords, image_style="professional", per_page=10):
225
- """根據風格搜尋 Pexels 圖片"""
226
- if not self.pexels_headers:
227
- return None
228
-
229
- # 組合關鍵字
230
- style_modifier = self.image_styles.get(image_style, "")
231
- enhanced_keywords = f"{keywords} {style_modifier}"
232
-
233
- url = "https://api.pexels.com/v1/search"
234
- params = {
235
- "query": enhanced_keywords,
236
- "per_page": per_page,
237
- "orientation": "landscape",
238
- "size": "medium"
239
- }
240
-
241
- try:
242
- response = requests.get(url, headers=self.pexels_headers, params=params)
243
- if response.status_code == 200:
244
- data = response.json()
245
- return data["photos"] if data["photos"] else None
246
- return None
247
- except Exception as e:
248
- print(f"Pexels API 錯誤: {e}")
249
- return None
250
-
251
- def select_best_image(self, photos, slide_title=""):
252
- """從多張圖片中選擇最適合的"""
253
- if not photos:
254
- return None
255
-
256
- # 選擇解析度較高的圖片
257
- best_photo = photos[0]
258
- for photo in photos[:3]:
259
- if photo["width"] * photo["height"] > best_photo["width"] * best_photo["height"]:
260
- best_photo = photo
261
-
262
- return best_photo["src"]["medium"]
263
-
264
- def download_image(self, image_url):
265
- """下載圖片並返回檔案路徑"""
266
- if not image_url:
267
- return None
268
-
269
- try:
270
- response = requests.get(image_url)
271
- if response.status_code == 200:
272
- temp_dir = tempfile.mkdtemp()
273
- image_path = os.path.join(temp_dir, "slide_image.jpg")
274
-
275
- # 處理圖片
276
- image = Image.open(BytesIO(response.content))
277
-
278
- # 調整圖片大小
279
- max_size = (800, 600)
280
- image.thumbnail(max_size, Image.Resampling.LANCZOS)
281
-
282
- # 轉換並儲存
283
- if image.mode in ("RGBA", "P"):
284
- image = image.convert("RGB")
285
- image.save(image_path, "JPEG", quality=85)
286
-
287
- return image_path
288
- return None
289
- except Exception as e:
290
- print(f"圖片下載錯誤: {e}")
291
- return None
292
-
293
- def add_image_to_slide(self, slide, image_path, theme):
294
- """將圖片添加到投影片,保持比例避免變形"""
295
- if not image_path or not os.path.exists(image_path):
296
- return
297
-
298
- try:
299
- image_area = theme["image_area"]
300
-
301
- # 目標區域
302
- target_left = Inches(image_area["left"])
303
- target_top = Inches(image_area["top"])
304
- target_width = Inches(image_area["width"])
305
- target_height = Inches(image_area["height"])
306
-
307
- # 載入圖片獲取原始尺寸
308
- from PIL import Image as PILImage
309
- with PILImage.open(image_path) as img:
310
- original_width, original_height = img.size
311
- original_ratio = original_width / original_height
312
-
313
- # 計算目標比例
314
- target_ratio = target_width.inches / target_height.inches
315
-
316
- # 根據比例計算實際顯示尺寸,保持圖片比例
317
- if original_ratio > target_ratio:
318
- # 圖片較寬,以寬度為準
319
- actual_width = target_width
320
- actual_height = Inches(target_width.inches / original_ratio)
321
- # 垂直置中
322
- actual_top = Inches(target_top.inches + (target_height.inches - actual_height.inches) / 2)
323
- actual_left = target_left
324
- else:
325
- # 圖片較高,以高度為準
326
- actual_height = target_height
327
- actual_width = Inches(target_height.inches * original_ratio)
328
- # 水平置中
329
- actual_left = Inches(target_left.inches + (target_width.inches - actual_width.inches) / 2)
330
- actual_top = target_top
331
-
332
- # 添加圖片
333
- picture = slide.shapes.add_picture(image_path, actual_left, actual_top, actual_width, actual_height)
334
-
335
- except Exception as e:
336
- print(f"添加圖片錯誤: {e}")
337
- # 降級處理:如果計算失敗,使用原來的方式
338
- try:
339
- left = Inches(image_area["left"])
340
- top = Inches(image_area["top"])
341
- width = Inches(image_area["width"])
342
- height = Inches(image_area["height"])
343
- slide.shapes.add_picture(image_path, left, top, width, height)
344
- except:
345
- pass
346
-
347
- def setup_slide_content(self, slide, slide_data, theme):
348
- """設定投影片內容,使用正確的位置"""
349
- try:
350
- # 設定標題
351
- title_shape = slide.shapes.title
352
- title_shape.text = slide_data["title"]
353
-
354
- # 調整標題位置和尺寸
355
- title_area = theme["title_area"]
356
- title_shape.left = Inches(title_area["left"])
357
- title_shape.top = Inches(title_area["top"])
358
- title_shape.width = Inches(title_area["width"])
359
- title_shape.height = Inches(title_area["height"])
360
-
361
- self.format_title(title_shape, theme, 32)
362
-
363
- # 移除預設內容佔位符(如果存在)
364
- shapes_to_remove = []
365
- for shape in slide.shapes:
366
- try:
367
- if hasattr(shape, 'placeholder_format') and shape.placeholder_format is not None:
368
- if shape.placeholder_format.type == 2: # 內容佔位符
369
- shapes_to_remove.append(shape)
370
- except:
371
- continue
372
-
373
- for shape in shapes_to_remove:
374
- try:
375
- sp = shape.element
376
- sp.getparent().remove(sp)
377
- except:
378
- continue
379
-
380
- # 設定內容區域
381
- content_area = theme["content_area"]
382
-
383
- # 創建新的內容文字框
384
- left = Inches(content_area["left"])
385
- top = Inches(content_area["top"])
386
- width = Inches(content_area["width"])
387
- height = Inches(content_area["height"])
388
-
389
- textbox = slide.shapes.add_textbox(left, top, width, height)
390
- text_frame = textbox.text_frame
391
-
392
- # 設定文字框屬性
393
- text_frame.margin_left = Inches(0.2)
394
- text_frame.margin_right = Inches(0.2)
395
- text_frame.margin_top = Inches(0.1)
396
- text_frame.margin_bottom = Inches(0.1)
397
- text_frame.word_wrap = True
398
- text_frame.auto_size = None # 不自動調整大小
399
-
400
- # 清除預設文字
401
- text_frame.clear()
402
-
403
- # 添加內容
404
- for i, point in enumerate(slide_data["content"]):
405
- if i == 0:
406
- p = text_frame.paragraphs[0]
407
- else:
408
- p = text_frame.add_paragraph()
409
-
410
- p.text = f"• {point}"
411
- p.level = 0
412
- p.space_after = Pt(8) # 段落間距
413
- self.format_content(p, theme, 18)
414
-
415
- except Exception as e:
416
- print(f"設定投影片內容錯誤詳細: {str(e)}")
417
- import traceback
418
- print(f"錯誤追蹤: {traceback.format_exc()}")
419
-
420
- def adjust_content_layout(self, slide, layout_type):
421
- """這個方法已被 setup_slide_content 取代,保留以免錯誤"""
422
- pass
423
-
424
- def get_font_name(self):
425
- """獲取中文字型名稱"""
426
- # 檢查是否有自定義中文字型檔案
427
- font_path = os.path.join(os.path.dirname(__file__), "cht.ttf")
428
- if os.path.exists(font_path):
429
- return "cht" # 使用自定義字型
430
- else:
431
- # 備用字型選擇
432
- return "Arial Unicode MS" # 通用 Unicode 字型
433
-
434
- def format_title(self, shape, theme, font_size):
435
- """格式化標題"""
436
- paragraph = shape.text_frame.paragraphs[0]
437
- paragraph.font.name = self.get_font_name()
438
- paragraph.font.size = Pt(font_size)
439
- paragraph.font.color.rgb = theme["title_color"]
440
- paragraph.alignment = PP_ALIGN.CENTER
441
-
442
- def format_title_with_shadow(self, shape, theme, font_size):
443
- """格式化標題並添加陰影效果以提高可讀性"""
444
- paragraph = shape.text_frame.paragraphs[0]
445
- paragraph.font.name = self.get_font_name()
446
- paragraph.font.size = Pt(font_size)
447
- paragraph.font.color.rgb = RGBColor(255, 255, 255) # 白色文字在深色背景上更清楚
448
- paragraph.alignment = PP_ALIGN.CENTER
449
-
450
- # 添加文字陰影效果
451
- try:
452
- # 為文字添加邊框,增強可讀性
453
- paragraph.font.bold = True
454
- except:
455
- pass
456
-
457
- def create_presentation_with_images(self, topic, theme_name="商務專業",
458
- slide_count=5, image_style="professional"):
459
- """建立包含圖片的簡報"""
460
-
461
- # 生成內容結構
462
- structure = self.generate_content_with_gemini(topic, slide_count)
463
- theme = self.themes[theme_name]
464
-
465
- # 建立 16:9 簡報
466
- prs = Presentation()
467
- prs.slide_width = self.slide_width
468
- prs.slide_height = self.slide_height
469
-
470
- # 建立標題頁
471
- title_slide = prs.slides.add_slide(prs.slide_layouts[0])
472
- title_shape = title_slide.shapes.title
473
- subtitle_shape = title_slide.placeholders[1]
474
-
475
- title_shape.text = structure["title"]
476
- subtitle_shape.text = structure["subtitle"]
477
-
478
- # 調整標題頁版面 (16:9)
479
- title_shape.left = Inches(1.0)
480
- title_shape.top = Inches(2.0)
481
- title_shape.width = Inches(11.333)
482
- title_shape.height = Inches(1.5)
483
-
484
- subtitle_shape.left = Inches(1.0)
485
- subtitle_shape.top = Inches(4.0)
486
- subtitle_shape.width = Inches(11.333)
487
- subtitle_shape.height = Inches(1.0)
488
-
489
- # 格式化標題頁 - 加強文字可讀性
490
- self.format_title_with_shadow(title_shape, theme, 44)
491
- self.format_title_with_shadow(subtitle_shape, theme, 24)
492
-
493
- # 為標題頁添加主題相關圖片
494
- main_keywords = f"{topic} cover background" # 更具體的主題關鍵字
495
- # 不加入風格修飾詞,讓主題主導搜尋結果
496
- title_photos = self.search_pexels_image_for_title(main_keywords, topic)
497
- if title_photos:
498
- title_image_url = self.select_best_image(title_photos, structure["title"])
499
- if title_image_url:
500
- title_image_path = self.download_image(title_image_url)
501
- if title_image_path:
502
- # 標題頁使用半透明背景
503
- self.add_title_background_with_overlay(title_slide, title_image_path, theme)
504
-
505
- # 建立內容頁
506
- for i, slide_data in enumerate(structure["slides"]):
507
- slide = prs.slides.add_slide(prs.slide_layouts[1])
508
-
509
- # 設定內容和版面
510
- self.setup_slide_content(slide, slide_data, theme)
511
-
512
- # 搜尋並添加圖片
513
- keywords = slide_data.get("image_keywords", f"{topic} slide {i+1}")
514
- photos = self.search_pexels_with_style(keywords, image_style)
515
-
516
- if photos:
517
- image_url = self.select_best_image(photos, slide_data["title"])
518
- if image_url:
519
- image_path = self.download_image(image_url)
520
- if image_path:
521
- self.add_image_to_slide(slide, image_path, theme)
522
-
523
- # 建立感謝頁
524
- self.add_thank_you_slide(prs, theme, image_style, topic)
525
-
526
- return prs, structure
527
-
528
- def search_pexels_image_for_title(self, keywords, topic, per_page=10):
529
- """專門為標題頁搜尋圖片,優先考慮主題相關性"""
530
- if not self.pexels_headers:
531
- return None
532
-
533
- # 先嘗試純主題搜尋
534
- topic_keywords = f"{topic} background"
535
-
536
- url = "https://api.pexels.com/v1/search"
537
- params = {
538
- "query": topic_keywords,
539
- "per_page": per_page,
540
- "orientation": "landscape",
541
- "size": "medium"
542
- }
543
-
544
- try:
545
- response = requests.get(url, headers=self.pexels_headers, params=params)
546
- if response.status_code == 200:
547
- data = response.json()
548
- if data["photos"]:
549
- return data["photos"]
550
-
551
- # 如果主題搜尋沒結果,使用通用關鍵字
552
- fallback_params = {
553
- "query": "professional presentation background",
554
- "per_page": per_page,
555
- "orientation": "landscape",
556
- "size": "medium"
557
- }
558
-
559
- response = requests.get(url, headers=self.pexels_headers, params=fallback_params)
560
- if response.status_code == 200:
561
- data = response.json()
562
- return data["photos"] if data["photos"] else None
563
-
564
- return None
565
- except Exception as e:
566
- print(f"Pexels API 錯誤: {e}")
567
- return None
568
-
569
- def add_title_background_with_overlay(self, slide, image_path, theme):
570
- """為標題頁添加帶有文字背景框的背景圖片"""
571
- try:
572
- # 添加背景圖片
573
- picture = slide.shapes.add_picture(
574
- image_path,
575
- Inches(0),
576
- Inches(0),
577
- self.slide_width,
578
- self.slide_height
579
- )
580
- # 移到背景層
581
- picture.element.getparent().remove(picture.element)
582
- slide.shapes._spTree.insert(2, picture.element)
583
-
584
- except Exception as e:
585
- print(f"添加標題背景錯誤: {e}")
586
- # 降級處理:直接添加背景圖片
587
- try:
588
- picture = slide.shapes.add_picture(
589
- image_path,
590
- Inches(0),
591
- Inches(0),
592
- self.slide_width,
593
- self.slide_height
594
- )
595
- picture.element.getparent().remove(picture.element)
596
- slide.shapes._spTree.insert(2, picture.element)
597
- except:
598
- pass
599
-
600
- def add_title_background(self, slide, image_path):
601
- """為標題頁添加背景圖片(保留原方法以免錯誤)"""
602
- self.add_title_background_with_overlay(slide, image_path, None)
603
-
604
- def add_thank_you_slide(self, prs, theme, image_style, topic):
605
- """添加感謝頁"""
606
- thank_slide = prs.slides.add_slide(prs.slide_layouts[5])
607
-
608
- # 感謝頁也優先考慮主題相關性
609
- thank_keywords = f"{topic} success completion"
610
- thank_photos = self.search_pexels_image_for_title(thank_keywords, topic)
611
- if thank_photos:
612
- thank_image_url = self.select_best_image(thank_photos)
613
- if thank_image_url:
614
- thank_image_path = self.download_image(thank_image_url)
615
- if thank_image_path:
616
- self.add_title_background_with_overlay(thank_slide, thank_image_path, theme)
617
-
618
- # 添加感謝文字背景框
619
- text_bg = thank_slide.shapes.add_shape(
620
- 1, # 矩形
621
- Inches(2.5),
622
- Inches(2.0),
623
- Inches(8.333),
624
- Inches(3.5)
625
- )
626
-
627
- fill = text_bg.fill
628
- fill.solid()
629
- fill.fore_color.rgb = RGBColor(255, 255, 255) # 白色背景
630
- text_bg.line.color.rgb = theme["accent_color"] if theme else RGBColor(79, 129, 189)
631
- text_bg.line.width = Pt(3)
632
-
633
- # 添加感謝文字 (16:9 居中位置)
634
- left = Inches(3.0)
635
- top = Inches(2.5)
636
- width = Inches(7.333)
637
- height = Inches(2.5)
638
-
639
- textbox = thank_slide.shapes.add_textbox(left, top, width, height)
640
- text_frame = textbox.text_frame
641
- text_frame.text = "謝謝聆聽\nThank You"
642
-
643
- for paragraph in text_frame.paragraphs:
644
- paragraph.font.name = self.get_font_name()
645
- paragraph.font.size = Pt(48)
646
- paragraph.font.color.rgb = theme["title_color"] if theme else RGBColor(31, 73, 125)
647
- paragraph.alignment = PP_ALIGN.CENTER
648
-
649
- def format_content(self, paragraph, theme, font_size):
650
- """格式化內容"""
651
- paragraph.font.name = self.get_font_name()
652
- paragraph.font.size = Pt(font_size)
653
- paragraph.font.color.rgb = theme["text_color"]
654
-
655
- def save_presentation(self, prs, filename):
656
- """儲存簡報"""
657
- temp_dir = tempfile.mkdtemp()
658
- filepath = os.path.join(temp_dir, filename)
659
- prs.save(filepath)
660
- return filepath
661
-
662
-
663
- def generate_preview_text(self, structure):
664
- """生成簡報預覽文字"""
665
- preview = f"📊 {structure['title']}\n"
666
- preview += f" {structure['subtitle']}\n\n"
667
-
668
- for i, slide in enumerate(structure['slides'], 1):
669
- preview += f"{i}. {slide['title']}\n"
670
- for point in slide['content'][:2]: # 只顯示前兩個重點
671
- preview += f" • {point}\n"
672
- if len(slide['content']) > 2:
673
- preview += f" • ...(共 {len(slide['content'])} 個重點)\n"
674
- preview += "\n"
675
-
676
- return preview
677
-
678
-
679
-
680
- def generate_ppt_with_gemini(gemini_api_key, pexels_api_key, topic, theme, slide_count, image_style):
681
- """生成簡報的主要函數"""
682
-
683
- # 檢查輸入
684
- if not gemini_api_key.strip():
685
- return None, "", "❌ 請輸入 Gemini API 金鑰"
686
-
687
- if not pexels_api_key.strip():
688
- return None, "", "❌ 請輸入 Pexels API 金鑰"
689
-
690
- if not topic.strip():
691
- return None, "", "❌ 請輸入簡報主題"
692
-
693
- generator = GeminiPPTGenerator()
694
-
695
- try:
696
- # 設定 API
697
- success, message = generator.setup_apis(gemini_api_key, pexels_api_key)
698
- if not success:
699
- return None, "", message
700
-
701
- # 生成簡報
702
- prs, structure = generator.create_presentation_with_images(
703
- topic, theme, slide_count, image_style
704
- )
705
-
706
- # 生成預覽
707
- preview = generator.generate_preview_text(structure)
708
-
709
- # 儲存檔案
710
- filename = f"{topic.replace(' ', '_')}_{image_style}_簡報.pptx"
711
- filepath = generator.save_presentation(prs, filename)
712
-
713
- success_msg = f"✅ 成功生成《{topic}》{image_style}風格簡報!({slide_count} 頁,含圖片)"
714
-
715
- return filepath, preview, success_msg
716
-
717
- except Exception as e:
718
- import traceback
719
- error_details = traceback.format_exc()
720
- print(f"詳細錯誤: {error_details}")
721
- return None, "", f"❌ 生成失敗:{str(e)}"
722
-
723
- # Gradio 介面
724
- def create_gemini_interface():
725
- """建立 Gradio 介面"""
726
- with gr.Blocks(title="Gemini AI 圖文簡報生成器", theme=gr.themes.Soft()) as iface:
727
- gr.Markdown("# 🤖 Gemini AI 智能圖文簡報生成器")
728
- gr.Markdown("**使用 Google Gemini 2.0 + Pexels 圖庫**,智能生成專業圖文簡報")
729
-
730
- # API 設定區域
731
- with gr.Group():
732
- gr.Markdown("### 🔑 API 設定")
733
- with gr.Row():
734
- gemini_api_input = gr.Textbox(
735
- label="🤖 Gemini API Key",
736
- placeholder="請輸入你的 Gemini API 金鑰",
737
- type="password",
738
- info="免費額度,前往 https://ai.google.dev/ 獲取"
739
- )
740
- pexels_api_input = gr.Textbox(
741
- label="📸 Pexels API Key",
742
- placeholder="請輸入你的 Pexels API 金鑰",
743
- type="password",
744
- info="免費 200次/月,前往 https://www.pexels.com/api/ 獲取"
745
- )
746
-
747
- # 主要設定區域
748
- with gr.Row():
749
- with gr.Column(scale=2):
750
- topic_input = gr.Textbox(
751
- label="📝 簡報主題",
752
- placeholder="請輸入具體的簡報主題...",
753
- value="人工智慧在現代教育中的應用與挑戰"
754
- )
755
-
756
- with gr.Row():
757
- theme_dropdown = gr.Dropdown(
758
- choices=["商務專業", "科技創新", "創意設計", "教育學術"],
759
- value="商務專業",
760
- label="🎨 版型風格"
761
- )
762
-
763
- image_style_dropdown = gr.Dropdown(
764
- choices=["professional", "creative", "minimalist", "modern", "natural", "technology"],
765
- value="professional",
766
- label="🖼️ 圖片風格"
767
- )
768
-
769
- slide_count = gr.Slider(
770
- minimum=3,
771
- maximum=10,
772
- value=6,
773
- step=1,
774
- label="📄 投影片數量"
775
- )
776
-
777
- generate_btn = gr.Button("🚀 生成專業簡報", variant="primary", size="lg")
778
-
779
- with gr.Column(scale=1):
780
- status_output = gr.Textbox(label="📊 生成狀態", interactive=False)
781
- file_output = gr.File(label="📁 下載簡報")
782
-
783
- # 預覽區域
784
- with gr.Group():
785
- gr.Markdown("### 📋 簡報預覽")
786
- preview_output = gr.Textbox(
787
- label="內容大綱",
788
- placeholder="生成後將顯示簡報大綱...",
789
- lines=8,
790
- interactive=False
791
- )
792
-
793
- # 說明區域
794
- with gr.Accordion("📖 使用說明與功能特色", open=False):
795
- gr.Markdown("""
796
- ### 🌟 核心特色
797
-
798
- #### 🤖 Google Gemini 2.0 Flash
799
- - **最新模型**:使用 Gemini 2.0 Flash Preview 版本
800
- - **免費額度**:Google 提供慷慨的免費使用額度
801
- - **中文優化**:對繁體中文有優秀的理解和生成能力
802
- - **結構化輸出**:精確生成 JSON 格式的簡報結構
803
-
804
- #### 📸 Pexels 圖片整合
805
- - **百萬圖庫**:Pexels 提供高品質免費圖片
806
- - **智能匹配**:AI 為每頁生成最適合的搜尋關鍵字
807
- - **風格選擇**:6 種圖片風格滿足不同需求
808
- - **自動配圖**:每張投影片自動配上相關圖片
809
-
810
- #### 🎨 專業版面設計
811
- - **4 種版型**:商務、科技、創意、學術風格
812
- - **智能排版**:根據版型自動調整圖文位置
813
- - **色彩搭配**:專業的色彩主題設計
814
- - **中文字型**:完美支援繁體中文顯示
815
-
816
- ### 📋 使用步驟
817
- 1. **獲取 API 金鑰**:
818
- - Gemini API:前往 [Google AI Studio](https://ai.google.dev/) 免費申請
819
- - Pexels API:前往 [Pexels API](https://www.pexels.com/api/) 免費申請(200次/月)
820
-
821
- 2. **輸入 API 金鑰**:在上方輸入框中填入你的 API 金鑰
822
-
823
- 3. **設定簡報參數**:
824
- - 輸入具體明確的簡報主題
825
- - 選擇適合的版型和圖片風格
826
- - 設定所需的投影片數量
827
-
828
- 4. **生成簡報**:點擊生成按鈕,系統將自動完成所有工作
829
-
830
- 5. **下載使用**:獲得完整的 .pptx 檔案,可直接在 PowerPoint 中使用
831
-
832
- ### 💡 專業建議
833
- - **主題要具體**:「AI在醫療診斷的應用」比「人工智慧」效果更好
834
- - **選對風格**:商務場合用「professional」,創意展示用「creative」
835
- - **適當頁數**:建議 5-8 頁,內容豐富但不冗長
836
- - **測試 API**:第一次使用建議先測試 API 連接是否正常
837
-
838
- ### 🔧 技術特點
839
- - **純 Python 實現**:不需要安裝 Microsoft Office
840
- - **即時生成**:通常 30-60 秒完成整個簡報
841
- - **高品質輸出**:生成的 .pptx 檔案完全相容 PowerPoint
842
- - **跨平台支援**:Windows、macOS、Linux 都能正常使用
843
- """)
844
-
845
- # 事件綁定
846
- generate_btn.click(
847
- fn=generate_ppt_with_gemini,
848
- inputs=[
849
- gemini_api_input,
850
- pexels_api_input,
851
- topic_input,
852
- theme_dropdown,
853
- slide_count,
854
- image_style_dropdown
855
- ],
856
- outputs=[file_output, preview_output, status_output]
857
- )
858
-
859
- return iface
860
-
861
- if __name__ == "__main__":
862
- # 啟動應用
863
- iface = create_gemini_interface()
864
- iface.launch(
865
- server_name="0.0.0.0",
866
- server_port=7860,
867
- share=True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
868
  )
 
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
+ from slide_themes import SlideThemeManager
15
+ from ppt_analyzer import PPTAnalyzer
16
+
17
+ class GeminiPPTGenerator:
18
+ def __init__(self):
19
+ self.pexels_headers = {}
20
+ self.gemini_model = None
21
+ self.config_file = "config.json"
22
+
23
+ # 載入已保存的API金鑰
24
+ self.load_config()
25
+
26
+ # 初始化版型管理器
27
+ self.theme_manager = SlideThemeManager()
28
+
29
+ # 16:9 簡報尺寸 (單位:英吋)
30
+ self.slide_width = self.theme_manager.slide_width
31
+ self.slide_height = self.theme_manager.slide_height
32
+
33
+ # 圖片風格
34
+ self.image_styles = self.theme_manager.image_styles
35
+
36
+ def load_config(self):
37
+ """雲端版本不讀取配置檔案"""
38
+ return '', ''
39
+
40
+ def save_config(self, gemini_api_key, pexels_api_key):
41
+ """雲端版本不保存配置檔案"""
42
+ return False
43
+
44
+ def get_saved_keys(self):
45
+ """雲端版本不讀取已保存的金鑰"""
46
+ return '', ''
47
+
48
+ def setup_apis(self, gemini_api_key, pexels_api_key):
49
+ """設定 API 金鑰"""
50
+ try:
51
+ # 設定 Gemini API
52
+ if gemini_api_key:
53
+ genai.configure(api_key=gemini_api_key)
54
+ self.gemini_model = genai.GenerativeModel('gemini-2.5-flash-preview-05-20')
55
+
56
+ # 設定 Pexels API
57
+ if pexels_api_key:
58
+ self.pexels_headers = {
59
+ "Authorization": pexels_api_key
60
+ }
61
+
62
+ return True, "✅ API 設定成功"
63
+ except Exception as e:
64
+ return False, f"❌ API 設定失敗:{str(e)}"
65
+
66
+ def generate_content_with_gemini(self, topic, slide_count=5):
67
+ """使用 Gemini 生成簡報內容"""
68
+
69
+ prompt = f"""
70
+ 請為主題「{topic}」製作一個 {slide_count} 頁的簡報大綱,以 JSON 格式回傳。
71
+
72
+ 格式要求:
73
+ {{
74
+ "title": "簡報主標題",
75
+ "subtitle": "簡報副標題",
76
+ "title_keywords": "主題相關的英文關鍵字,用於搜尋標題頁和結尾頁圖片",
77
+ "slides": [
78
+ {{
79
+ "title": "投影片標題",
80
+ "content": [
81
+ "重點1",
82
+ "重點2",
83
+ "重點3"
84
+ ],
85
+ "image_keywords": "英文關鍵字,用於搜尋相關圖片"
86
+ }}
87
+ ]
88
+ }}
89
+
90
+ 要求:
91
+ 1. 內容要專業且有邏輯性
92
+ 2. 每頁 3-4 個重點
93
+ 3. title_keywords 要用英文,描述主題相關的圖片搜尋關鍵字(3-5個詞)
94
+ 4. image_keywords 要用英文,描述該投影片適合的圖片內容
95
+ 5. 關鍵字要具體明確,例如 "business meeting", "technology innovation", "data analysis"
96
+ 6. 使用繁體中文(除了 title_keywords 和 image_keywords)
97
+ 7. 第一頁是概述介紹,最後一頁是結論總結
98
+ 8. 請直接回傳 JSON,不要包含其他文字說明,也不可以有"**"等不必要的markdown符號
99
+ """
100
+
101
+ try:
102
+ if self.gemini_model:
103
+ response = self.gemini_model.generate_content(prompt)
104
+ content = response.text
105
+
106
+ # 清理回應內容,提取 JSON
107
+ content = content.strip()
108
+ if content.startswith('```json'):
109
+ content = content[7:]
110
+ if content.endswith('```'):
111
+ content = content[:-3]
112
+
113
+ # 尋找 JSON 開始和結束位置
114
+ start = content.find('{')
115
+ end = content.rfind('}') + 1
116
+
117
+ if start != -1 and end > start:
118
+ json_str = content[start:end]
119
+ return json.loads(json_str)
120
+ else:
121
+ raise ValueError("無法在回應中找到有效的 JSON")
122
+
123
+ else:
124
+ return self.get_default_structure_with_images(topic)
125
+
126
+ except Exception as e:
127
+ print(f"Gemini API 錯誤: {e}")
128
+ return self.get_default_structure_with_images(topic)
129
+
130
+ def get_default_structure_with_images(self, topic):
131
+ """預設簡報結構(含圖片關鍵字)"""
132
+ # 生成簡單的英文關鍵字
133
+ title_keywords = "business presentation professional meeting"
134
+ if "科技" in topic or "技術" in topic:
135
+ title_keywords = "technology innovation digital development"
136
+ elif "教育" in topic or "學習" in topic:
137
+ title_keywords = "education learning academic study"
138
+ elif "醫療" in topic or "健康" in topic:
139
+ title_keywords = "healthcare medical health wellness"
140
+ elif "環境" in topic or "環保" in topic:
141
+ title_keywords = "environment sustainability green nature"
142
+ elif "經濟" in topic or "金融" in topic:
143
+ title_keywords = "economics finance business economy"
144
+
145
+ return {
146
+ "title": f"{topic} 簡報",
147
+ "subtitle": "由 AI 自動生成",
148
+ "title_keywords": title_keywords,
149
+ "slides": [
150
+ {
151
+ "title": "簡介與背景",
152
+ "content": [
153
+ "主題背景介紹",
154
+ "研究目的與範圍",
155
+ "簡報架構說明"
156
+ ],
157
+ "image_keywords": "presentation introduction business"
158
+ },
159
+ {
160
+ "title": "主要內容分析",
161
+ "content": [
162
+ "核心概念說明",
163
+ "重要特點分析",
164
+ "相關案例討論"
165
+ ],
166
+ "image_keywords": "analysis data research content"
167
+ },
168
+ {
169
+ "title": "深入探討",
170
+ "content": [
171
+ "優勢與機會識別",
172
+ "挑戰與問題分析",
173
+ "影響因素評估"
174
+ ],
175
+ "image_keywords": "strategy planning discussion"
176
+ },
177
+ {
178
+ "title": "解決方案與建議",
179
+ "content": [
180
+ "策略建議提出",
181
+ "實施方法規劃",
182
+ "預期效果評估"
183
+ ],
184
+ "image_keywords": "solution implementation strategy"
185
+ },
186
+ {
187
+ "title": "結論與展望",
188
+ "content": [
189
+ "重點總結回顧",
190
+ "未來發展趨勢",
191
+ "行動建議提出"
192
+ ],
193
+ "image_keywords": "conclusion future success"
194
+ }
195
+ ]
196
+ }
197
+
198
+ def search_pexels_with_style(self, keywords, image_style="professional", per_page=10):
199
+ """根據風格搜尋 Pexels 圖片"""
200
+ if not self.pexels_headers:
201
+ return None
202
+
203
+ # 先嘗試純主題關鍵字搜尋
204
+ url = "https://api.pexels.com/v1/search"
205
+ params = {
206
+ "query": keywords,
207
+ "per_page": per_page,
208
+ "orientation": "landscape",
209
+ "size": "medium"
210
+ }
211
+
212
+ try:
213
+ response = requests.get(url, headers=self.pexels_headers, params=params)
214
+ if response.status_code == 200:
215
+ data = response.json()
216
+ if data["photos"] and len(data["photos"]) >= 3:
217
+ return data["photos"]
218
+
219
+ # 如果純主題搜尋結果不足,再組合風格關鍵字
220
+ style_modifier = self.image_styles.get(image_style, "")
221
+ enhanced_keywords = f"{keywords} {style_modifier}"
222
+
223
+ params["query"] = enhanced_keywords
224
+ response = requests.get(url, headers=self.pexels_headers, params=params)
225
+ if response.status_code == 200:
226
+ data = response.json()
227
+ return data["photos"] if data["photos"] else None
228
+
229
+ return None
230
+ except Exception as e:
231
+ print(f"Pexels API 錯誤: {e}")
232
+ return None
233
+
234
+ def select_best_image(self, photos, slide_title=""):
235
+ """從多張圖片中選擇最適合的"""
236
+ if not photos:
237
+ return None
238
+
239
+ # 選擇解析度較高的圖片
240
+ best_photo = photos[0]
241
+ for photo in photos[:3]:
242
+ if photo["width"] * photo["height"] > best_photo["width"] * best_photo["height"]:
243
+ best_photo = photo
244
+
245
+ return best_photo["src"]["medium"]
246
+
247
+ def download_image(self, image_url):
248
+ """下載圖片並返回檔案路徑"""
249
+ if not image_url:
250
+ return None
251
+
252
+ try:
253
+ response = requests.get(image_url)
254
+ if response.status_code == 200:
255
+ temp_dir = tempfile.mkdtemp()
256
+ image_path = os.path.join(temp_dir, "slide_image.jpg")
257
+
258
+ # 處理圖片
259
+ image = Image.open(BytesIO(response.content))
260
+
261
+ # 調整圖片大小
262
+ max_size = (800, 600)
263
+ image.thumbnail(max_size, Image.Resampling.LANCZOS)
264
+
265
+ # 轉換並儲存
266
+ if image.mode in ("RGBA", "P"):
267
+ image = image.convert("RGB")
268
+ image.save(image_path, "JPEG", quality=85)
269
+
270
+ return image_path
271
+ return None
272
+ except Exception as e:
273
+ print(f"圖片下載錯誤: {e}")
274
+ return None
275
+
276
+ def add_image_to_slide(self, slide, image_path, theme):
277
+ """將圖片添加到投影片,保持比例避免變形"""
278
+ if not image_path or not os.path.exists(image_path):
279
+ return
280
+
281
+ try:
282
+ image_area = theme["image_area"]
283
+
284
+ # 目標區域
285
+ target_left = Inches(image_area["left"])
286
+ target_top = Inches(image_area["top"])
287
+ target_width = Inches(image_area["width"])
288
+ target_height = Inches(image_area["height"])
289
+
290
+ # 載入圖片獲取原始尺寸
291
+ from PIL import Image as PILImage
292
+ with PILImage.open(image_path) as img:
293
+ original_width, original_height = img.size
294
+ original_ratio = original_width / original_height
295
+
296
+ # 計算目標比例
297
+ target_ratio = target_width.inches / target_height.inches
298
+
299
+ # 根據比例計算實際顯示尺寸,保持圖片比例
300
+ if original_ratio > target_ratio:
301
+ # 圖片較寬,以寬度為準
302
+ actual_width = target_width
303
+ actual_height = Inches(target_width.inches / original_ratio)
304
+ # 垂直置中
305
+ actual_top = Inches(target_top.inches + (target_height.inches - actual_height.inches) / 2)
306
+ actual_left = target_left
307
+ else:
308
+ # 圖片較高,以高度為準
309
+ actual_height = target_height
310
+ actual_width = Inches(target_height.inches * original_ratio)
311
+ # 水平置中
312
+ actual_left = Inches(target_left.inches + (target_width.inches - actual_width.inches) / 2)
313
+ actual_top = target_top
314
+
315
+ # 添加圖片
316
+ picture = slide.shapes.add_picture(image_path, actual_left, actual_top, actual_width, actual_height)
317
+
318
+ except Exception as e:
319
+ print(f"添加圖片錯誤: {e}")
320
+ # 降級處理:如果計算失敗,使用原來的方式
321
+ try:
322
+ left = Inches(image_area["left"])
323
+ top = Inches(image_area["top"])
324
+ width = Inches(image_area["width"])
325
+ height = Inches(image_area["height"])
326
+ slide.shapes.add_picture(image_path, left, top, width, height)
327
+ except:
328
+ pass
329
+
330
+ def setup_slide_content(self, slide, slide_data, theme):
331
+ """設定投影片內容,使用正確的位置"""
332
+ try:
333
+ # 設置背景和裝飾元素
334
+ self.theme_manager.setup_slide_background_and_layout(slide, theme)
335
+
336
+ # 設定標題
337
+ title_shape = slide.shapes.title
338
+ title_shape.text = slide_data["title"]
339
+
340
+ # 調整標題位置和尺寸
341
+ title_area = theme["title_area"]
342
+ title_shape.left = Inches(title_area["left"])
343
+ title_shape.top = Inches(title_area["top"])
344
+ title_shape.width = Inches(title_area["width"])
345
+ title_shape.height = Inches(title_area["height"])
346
+
347
+ self.theme_manager.format_title(title_shape, theme, 34, self.get_font_name)
348
+
349
+ # 移除預設內容佔位符(如果存在)
350
+ shapes_to_remove = []
351
+ for shape in slide.shapes:
352
+ try:
353
+ if hasattr(shape, 'placeholder_format') and shape.placeholder_format is not None:
354
+ if shape.placeholder_format.type == 2: # 內容佔位符
355
+ shapes_to_remove.append(shape)
356
+ except:
357
+ continue
358
+
359
+ for shape in shapes_to_remove:
360
+ try:
361
+ sp = shape.element
362
+ sp.getparent().remove(sp)
363
+ except:
364
+ continue
365
+
366
+ # 設定內容區域
367
+ content_area = theme["content_area"]
368
+
369
+ # 創建帶背景的內容框
370
+ self.theme_manager.create_content_box_with_background(slide, theme, content_area)
371
+
372
+ # 創建新的內容文字框
373
+ left = Inches(content_area["left"])
374
+ top = Inches(content_area["top"])
375
+ width = Inches(content_area["width"])
376
+ height = Inches(content_area["height"])
377
+
378
+ textbox = slide.shapes.add_textbox(left, top, width, height)
379
+ text_frame = textbox.text_frame
380
+
381
+ # 設定文字框屬性
382
+ text_frame.margin_left = Inches(0.15)
383
+ text_frame.margin_right = Inches(0.15)
384
+ text_frame.margin_top = Inches(0.1)
385
+ text_frame.margin_bottom = Inches(0.1)
386
+ text_frame.word_wrap = True
387
+ text_frame.auto_size = None # 不自動調整大小
388
+
389
+ # 清除預設文字
390
+ text_frame.clear()
391
+
392
+ # 添加內容
393
+ for i, point in enumerate(slide_data["content"]):
394
+ if i == 0:
395
+ p = text_frame.paragraphs[0]
396
+ else:
397
+ p = text_frame.add_paragraph()
398
+
399
+ p.text = f"• {point}"
400
+ p.level = 0
401
+ p.space_after = Pt(10) # 段落間距
402
+ self.theme_manager.format_content(p, theme, 22, self.get_font_name)
403
+
404
+ except Exception as e:
405
+ print(f"設定投影片內容錯誤詳細: {str(e)}")
406
+ import traceback
407
+ print(f"錯誤追蹤: {traceback.format_exc()}")
408
+
409
+ def adjust_content_layout(self, slide, layout_type):
410
+ """這個方法已被 setup_slide_content 取代,保留以免錯誤"""
411
+ pass
412
+
413
+ def get_font_name(self):
414
+ """獲取中文字型名稱"""
415
+ # 檢查是否有自定義中文字型檔案
416
+ font_path = os.path.join(os.path.dirname(__file__), "cht.ttf")
417
+ if os.path.exists(font_path):
418
+ return "cht" # 使用自定義字型
419
+ else:
420
+ # 備用字型選擇
421
+ return "Arial Unicode MS" # 通用 Unicode 字型
422
+
423
+ def format_title_with_shadow(self, shape, theme, font_size):
424
+ """格式化標題並添加陰影效果以提高可讀性"""
425
+ self.theme_manager.format_title(shape, theme, font_size, self.get_font_name)
426
+ try:
427
+ paragraph = shape.text_frame.paragraphs[0]
428
+ paragraph.font.color.rgb = RGBColor(255, 255, 255) # 白色文字在深色背景上更清楚
429
+ paragraph.alignment = PP_ALIGN.CENTER
430
+ paragraph.font.bold = True
431
+ except:
432
+ pass
433
+
434
+ def create_presentation_with_images(self, topic, theme_name="商務專業",
435
+ slide_count=5, image_style="professional"):
436
+ """建立包含圖片的簡報"""
437
+
438
+ # 生成內容結構
439
+ structure = self.generate_content_with_gemini(topic, slide_count)
440
+ theme = self.theme_manager.get_theme(theme_name)
441
+
442
+ # 建立 16:9 簡報
443
+ prs = Presentation()
444
+ prs.slide_width = self.slide_width
445
+ prs.slide_height = self.slide_height
446
+
447
+ # 建立標題頁
448
+ title_slide = prs.slides.add_slide(prs.slide_layouts[0])
449
+ title_shape = title_slide.shapes.title
450
+ subtitle_shape = title_slide.placeholders[1]
451
+
452
+ title_shape.text = structure["title"]
453
+ subtitle_shape.text = structure["subtitle"]
454
+
455
+ # 調整標題頁版面 (16:9)
456
+ title_shape.left = Inches(1.0)
457
+ title_shape.top = Inches(2.0)
458
+ title_shape.width = Inches(11.333)
459
+ title_shape.height = Inches(1.5)
460
+
461
+ subtitle_shape.left = Inches(1.0)
462
+ subtitle_shape.top = Inches(4.0)
463
+ subtitle_shape.width = Inches(11.333)
464
+ subtitle_shape.height = Inches(1.0)
465
+
466
+ # 格式化標題頁 - 加強文字可讀性
467
+ self.format_title_with_shadow(title_shape, theme, 54)
468
+ self.format_title_with_shadow(subtitle_shape, theme, 32)
469
+
470
+ # 為標題頁添加主題相關圖片 - 使用AI生成的英文關鍵字
471
+ main_keywords = structure.get("title_keywords", f"{topic} introduction overview")
472
+ title_photos = self.search_pexels_with_style(main_keywords, image_style, per_page=15)
473
+ if title_photos:
474
+ title_image_url = self.select_best_image(title_photos, structure["title"])
475
+ if title_image_url:
476
+ title_image_path = self.download_image(title_image_url)
477
+ if title_image_path:
478
+ # 標題頁使用半透明背景
479
+ self.add_title_background_with_overlay(title_slide, title_image_path, theme)
480
+
481
+ # 建立內容頁
482
+ for i, slide_data in enumerate(structure["slides"]):
483
+ slide = prs.slides.add_slide(prs.slide_layouts[1])
484
+
485
+ # 設定內容和版面
486
+ self.setup_slide_content(slide, slide_data, theme)
487
+
488
+ # 搜尋並添加圖片
489
+ keywords = slide_data.get("image_keywords", f"{topic} slide {i+1}")
490
+ photos = self.search_pexels_with_style(keywords, image_style)
491
+
492
+ if photos:
493
+ image_url = self.select_best_image(photos, slide_data["title"])
494
+ if image_url:
495
+ image_path = self.download_image(image_url)
496
+ if image_path:
497
+ self.add_image_to_slide(slide, image_path, theme)
498
+
499
+ # 建立感謝頁
500
+ self.add_thank_you_slide(prs, theme, image_style, topic, structure)
501
+
502
+ return prs, structure
503
+
504
+ def search_pexels_image_for_title(self, keywords, topic, per_page=10):
505
+ """專門為標題頁搜尋圖片,優先考慮主題相關性"""
506
+ if not self.pexels_headers:
507
+ return None
508
+
509
+ # 先嘗試純主題搜尋
510
+ topic_keywords = f"{topic} background"
511
+
512
+ url = "https://api.pexels.com/v1/search"
513
+ params = {
514
+ "query": topic_keywords,
515
+ "per_page": per_page,
516
+ "orientation": "landscape",
517
+ "size": "medium"
518
+ }
519
+
520
+ try:
521
+ response = requests.get(url, headers=self.pexels_headers, params=params)
522
+ if response.status_code == 200:
523
+ data = response.json()
524
+ if data["photos"]:
525
+ return data["photos"]
526
+
527
+ # 如果主題搜尋沒結果,使用通用關鍵字
528
+ fallback_params = {
529
+ "query": "professional presentation background",
530
+ "per_page": per_page,
531
+ "orientation": "landscape",
532
+ "size": "medium"
533
+ }
534
+
535
+ response = requests.get(url, headers=self.pexels_headers, params=fallback_params)
536
+ if response.status_code == 200:
537
+ data = response.json()
538
+ return data["photos"] if data["photos"] else None
539
+
540
+ return None
541
+ except Exception as e:
542
+ print(f"Pexels API 錯誤: {e}")
543
+ return None
544
+
545
+ def add_title_background_with_overlay(self, slide, image_path, theme):
546
+ """為標題頁添加帶有文字背景框的背景圖片"""
547
+ try:
548
+ # 添加背景圖片
549
+ picture = slide.shapes.add_picture(
550
+ image_path,
551
+ Inches(0),
552
+ Inches(0),
553
+ self.slide_width,
554
+ self.slide_height
555
+ )
556
+ # 移到背景層
557
+ picture.element.getparent().remove(picture.element)
558
+ slide.shapes._spTree.insert(2, picture.element)
559
+
560
+ except Exception as e:
561
+ print(f"添加標題背景錯誤: {e}")
562
+ # 降級處理:直接添加背景圖片
563
+ try:
564
+ picture = slide.shapes.add_picture(
565
+ image_path,
566
+ Inches(0),
567
+ Inches(0),
568
+ self.slide_width,
569
+ self.slide_height
570
+ )
571
+ picture.element.getparent().remove(picture.element)
572
+ slide.shapes._spTree.insert(2, picture.element)
573
+ except:
574
+ pass
575
+
576
+ def add_title_background(self, slide, image_path):
577
+ """為標題頁添加背景圖片(保留原方法以免錯誤)"""
578
+ self.add_title_background_with_overlay(slide, image_path, None)
579
+
580
+ def add_thank_you_slide(self, prs, theme, image_style, topic, structure):
581
+ """添加感謝頁"""
582
+ thank_slide = prs.slides.add_slide(prs.slide_layouts[5])
583
+
584
+ # 感謝頁使用主題關鍵字加上結尾相關詞彙
585
+ title_keywords = structure.get("title_keywords", f"{topic} success conclusion achievement")
586
+ thank_keywords = f"{title_keywords} success conclusion achievement"
587
+ thank_photos = self.search_pexels_with_style(thank_keywords, image_style, per_page=12)
588
+ if thank_photos:
589
+ thank_image_url = self.select_best_image(thank_photos)
590
+ if thank_image_url:
591
+ thank_image_path = self.download_image(thank_image_url)
592
+ if thank_image_path:
593
+ self.add_title_background_with_overlay(thank_slide, thank_image_path, theme)
594
+
595
+ # 添加感謝文字背景框
596
+ text_bg = thank_slide.shapes.add_shape(
597
+ 1, # 矩形
598
+ Inches(2.5),
599
+ Inches(2.0),
600
+ Inches(8.333),
601
+ Inches(3.5)
602
+ )
603
+
604
+ fill = text_bg.fill
605
+ fill.solid()
606
+ fill.fore_color.rgb = RGBColor(255, 255, 255) # 白色背景
607
+ text_bg.line.color.rgb = theme["accent_color"] if theme else RGBColor(79, 129, 189)
608
+ text_bg.line.width = Pt(3)
609
+
610
+ # 添加感謝文字 (16:9 居中位置)
611
+ left = Inches(3.0)
612
+ top = Inches(2.5)
613
+ width = Inches(7.333)
614
+ height = Inches(2.5)
615
+
616
+ textbox = thank_slide.shapes.add_textbox(left, top, width, height)
617
+ text_frame = textbox.text_frame
618
+ text_frame.text = "謝謝聆聽\nThank You"
619
+
620
+ for paragraph in text_frame.paragraphs:
621
+ paragraph.font.name = self.get_font_name()
622
+ paragraph.font.size = Pt(60)
623
+ paragraph.font.color.rgb = theme["title_color"] if theme else RGBColor(31, 73, 125)
624
+ paragraph.alignment = PP_ALIGN.CENTER
625
+ paragraph.font.bold = True
626
+
627
+
628
+ def save_presentation(self, prs, filename):
629
+ """儲存簡報"""
630
+ temp_dir = tempfile.mkdtemp()
631
+ filepath = os.path.join(temp_dir, filename)
632
+ prs.save(filepath)
633
+ return filepath
634
+
635
+
636
+ def generate_preview_text(self, structure):
637
+ """生成簡報預覽文字"""
638
+ preview = f"📊 {structure['title']}\n"
639
+ preview += f" {structure['subtitle']}\n\n"
640
+
641
+ for i, slide in enumerate(structure['slides'], 1):
642
+ preview += f"{i}. {slide['title']}\n"
643
+ for point in slide['content'][:2]: # 只顯示前兩個重點
644
+ preview += f" • {point}\n"
645
+ if len(slide['content']) > 2:
646
+ preview += f" ...(共 {len(slide['content'])} 個重點)\n"
647
+ preview += "\n"
648
+
649
+ return preview
650
+
651
+ def analyze_and_restyle_ppt(gemini_api_key, pexels_api_key, uploaded_file, theme_name, image_style):
652
+ """分析並重新設計上傳的簡報"""
653
+
654
+ if not uploaded_file:
655
+ return None, "", "❌ 請上傳PPT文件"
656
+
657
+ generator = GeminiPPTGenerator()
658
+
659
+ # 雲端版本不載入已保存的配置
660
+
661
+ # 檢查輸入
662
+ if not gemini_api_key.strip():
663
+ return None, "", "❌ 請輸入 Gemini API 金鑰"
664
+
665
+ if not pexels_api_key.strip():
666
+ return None, "", "❌ 請輸入 Pexels API 金鑰"
667
+
668
+ try:
669
+ # 設定 API
670
+ success, message = generator.setup_apis(gemini_api_key, pexels_api_key)
671
+ if not success:
672
+ return None, "", message
673
+
674
+ # 創建分析器
675
+ analyzer = PPTAnalyzer(
676
+ gemini_model=generator.gemini_model,
677
+ pexels_headers=generator.pexels_headers,
678
+ image_styles=generator.image_styles
679
+ )
680
+
681
+ # 分析上傳的PPT
682
+ analysis_result = analyzer.analyze_ppt_file(uploaded_file.name)
683
+ if not analysis_result:
684
+ return None, "", "❌ 無法分析PPT文件,請確認文件格式正確"
685
+
686
+ # 套用新主題和添加圖片
687
+ processed_prs, processed_slides = analyzer.apply_theme_to_presentation(
688
+ uploaded_file.name, theme_name, image_style, analysis_result
689
+ )
690
+
691
+ if not processed_prs:
692
+ return None, "", "❌ 處理PPT文件時發生錯誤"
693
+
694
+ # 生成分析報告
695
+ report = analyzer.generate_analysis_report(analysis_result, processed_slides)
696
+
697
+ # 儲存處理後的簡報
698
+ original_name = os.path.splitext(os.path.basename(uploaded_file.name))[0]
699
+ filename = f"{original_name}_{theme_name}_{image_style}_restyled.pptx"
700
+ output_path = analyzer.save_processed_presentation(processed_prs, filename)
701
+
702
+ if not output_path:
703
+ return None, "", "❌ 儲存處理後的簡報時發生錯誤"
704
+
705
+ success_msg = f"✅ 成功重新設計《{original_name}》!\n"
706
+ success_msg += f"🎨 套用主題:{theme_name}\n"
707
+ success_msg += f"🖼️ 圖片風格:{image_style}\n"
708
+ success_msg += f"📄 處理了 {len(processed_slides)} 張投影片"
709
+
710
+ return output_path, report, success_msg
711
+
712
+ except Exception as e:
713
+ import traceback
714
+ error_details = traceback.format_exc()
715
+ print(f"詳細錯誤: {error_details}")
716
+ return None, "", f"❌ 處理失敗:{str(e)}"
717
+
718
+
719
+
720
+ def generate_ppt_with_gemini(gemini_api_key, pexels_api_key, topic, theme, slide_count, image_style):
721
+ """生成簡報的主要函數"""
722
+
723
+ generator = GeminiPPTGenerator()
724
+
725
+ # 雲端版本不載入已保存的配置
726
+
727
+ # 檢查輸入
728
+ if not gemini_api_key.strip():
729
+ return None, "", "❌ 請輸入 Gemini API 金鑰"
730
+
731
+ if not pexels_api_key.strip():
732
+ return None, "", "❌ 請輸入 Pexels API 金鑰"
733
+
734
+ if not topic.strip():
735
+ return None, "", "❌ 請輸入簡報主題"
736
+
737
+ try:
738
+ # 設定 API
739
+ success, message = generator.setup_apis(gemini_api_key, pexels_api_key)
740
+ if not success:
741
+ return None, "", message
742
+
743
+ # 雲端版本不保存配置檔案
744
+
745
+ # 生成簡報
746
+ prs, structure = generator.create_presentation_with_images(
747
+ topic, theme, slide_count, image_style
748
+ )
749
+
750
+ # 生成預覽
751
+ preview = generator.generate_preview_text(structure)
752
+
753
+ # 儲存檔案
754
+ filename = f"{topic.replace(' ', '_')}_{image_style}_簡報.pptx"
755
+ filepath = generator.save_presentation(prs, filename)
756
+
757
+ success_msg = f"✅ 成功生成《{topic}》{image_style}風格簡報!({slide_count} 頁,含圖片)"
758
+
759
+ return filepath, preview, success_msg
760
+
761
+ except Exception as e:
762
+ import traceback
763
+ error_details = traceback.format_exc()
764
+ print(f"詳細錯誤: {error_details}")
765
+ return None, "", f"❌ 生成失敗:{str(e)}"
766
+
767
+ # Gradio 介面
768
+ def create_gemini_interface():
769
+ """建立 Gradio 介面"""
770
+
771
+ # 雲端版本不檢查已保存的金鑰
772
+
773
+ with gr.Blocks(title="Gemini AI 圖文簡報生成器", theme=gr.themes.Soft()) as iface:
774
+ gr.Markdown("# 🤖 Gemini AI 智能圖文簡報生成器")
775
+ gr.Markdown("**使用 Google Gemini 2.0 + Pexels 圖庫**,智能生成專業圖文簡報,或改造現有簡報")
776
+
777
+ # API 設定區域(雲端版本總是要求輸入)
778
+ with gr.Group():
779
+ gr.Markdown("### 🔑 API 設定")
780
+ with gr.Row():
781
+ gemini_api_input = gr.Textbox(
782
+ label="🤖 Gemini API Key",
783
+ placeholder="請輸入你的 Gemini API 金鑰",
784
+ type="password",
785
+ info="免費額度,前往 https://ai.google.dev/ 獲取"
786
+ )
787
+ pexels_api_input = gr.Textbox(
788
+ label="📸 Pexels API Key",
789
+ placeholder="請輸入你的 Pexels API 金鑰",
790
+ type="password",
791
+ info="免費 200次/月,前往 https://www.pexels.com/api/ 獲取"
792
+ )
793
+
794
+ # 選項卡
795
+ with gr.Tabs():
796
+ # 原有的生成功能
797
+ with gr.TabItem("🆕 創建新簡報"):
798
+ # 主要設定區域
799
+ with gr.Row():
800
+ with gr.Column(scale=2):
801
+ topic_input = gr.Textbox(
802
+ label="📝 簡報主題",
803
+ placeholder="請輸入具體的簡報主題...",
804
+ value="人工智慧在現代教育中的應用與挑戰"
805
+ )
806
+
807
+ with gr.Row():
808
+ # 從主題管理器獲取所有主題名稱
809
+ generator = GeminiPPTGenerator()
810
+ theme_dropdown = gr.Dropdown(
811
+ choices=generator.theme_manager.get_all_theme_names(),
812
+ value="商務專業",
813
+ label="🎨 版型風格"
814
+ )
815
+
816
+ image_style_dropdown = gr.Dropdown(
817
+ choices=["professional", "creative", "minimalist", "modern", "natural", "technology"],
818
+ value="professional",
819
+ label="🖼️ 圖片風格"
820
+ )
821
+
822
+ slide_count = gr.Slider(
823
+ minimum=3,
824
+ maximum=15,
825
+ value=6,
826
+ step=1,
827
+ label="📄 投影片數量"
828
+ )
829
+
830
+ generate_btn = gr.Button("🚀 生成專業簡報", variant="primary", size="lg")
831
+
832
+ with gr.Column(scale=1):
833
+ status_output = gr.Textbox(label="📊 生成狀態", interactive=False)
834
+ file_output = gr.File(label="📁 下載簡報")
835
+
836
+ # 預覽區域
837
+ with gr.Group():
838
+ gr.Markdown("### 📋 簡報預覽")
839
+ preview_output = gr.Textbox(
840
+ label="內容大綱",
841
+ placeholder="生成後將顯示簡報大綱...",
842
+ lines=8,
843
+ interactive=False
844
+ )
845
+
846
+ # 新增的簡報改造功能
847
+ with gr.TabItem("🔄 改造現有簡報"):
848
+ gr.Markdown("### 📤 上傳並改造���的簡報")
849
+ gr.Markdown("上傳現有的PPT文件,AI將分析內容並套用新的版型設計,自動為每頁添加相關圖片")
850
+
851
+ with gr.Row():
852
+ with gr.Column(scale=2):
853
+ # 文件上傳
854
+ upload_file = gr.File(
855
+ label="📎 上傳PPT文件",
856
+ file_types=[".pptx", ".ppt"],
857
+ type="filepath"
858
+ )
859
+
860
+ with gr.Row():
861
+ # 主題選擇
862
+ upload_theme_dropdown = gr.Dropdown(
863
+ choices=generator.theme_manager.get_all_theme_names(),
864
+ value="商務專業",
865
+ label="🎨 套用版型風格"
866
+ )
867
+
868
+ upload_image_style_dropdown = gr.Dropdown(
869
+ choices=["professional", "creative", "minimalist", "modern", "natural", "technology"],
870
+ value="professional",
871
+ label="🖼️ 圖片風格"
872
+ )
873
+
874
+ analyze_btn = gr.Button("🔍 分析並改造簡報", variant="primary", size="lg")
875
+
876
+ with gr.Column(scale=1):
877
+ upload_status_output = gr.Textbox(label="📊 處理狀態", interactive=False)
878
+ upload_file_output = gr.File(label="📁 下載改造後簡報")
879
+
880
+ # 分析報告區域
881
+ with gr.Group():
882
+ gr.Markdown("### 📋 分析報告")
883
+ analysis_report = gr.Textbox(
884
+ label="處理詳情",
885
+ placeholder="上傳並處理後將顯示分析報告...",
886
+ lines=8,
887
+ interactive=False
888
+ )
889
+
890
+ # 說明區域
891
+ with gr.Accordion("📖 使用說明與功能特色", open=False):
892
+ gr.Markdown("""
893
+ ### 🌟 核心特色
894
+
895
+ #### 🤖 Google Gemini 2.5 Flash
896
+ - **最新模型**:使用 Gemini 2.5 Flash Preview 版本
897
+ - **免費額度**:Google 提供免費使用額度
898
+ - **中文優化**:對繁體中文有優秀的理解和生成能力
899
+ - **結構化輸出**:精確生成 JSON 格式的簡報結構
900
+ - **內容分析**:智能分析現有簡報內容,生成適合的圖片搜尋關鍵字
901
+
902
+ #### 📸 Pexels 圖片整合
903
+ - **百萬圖庫**:Pexels 提供高品質免費圖片
904
+ - **智能匹配**:AI 為每頁生成最適合的搜尋關鍵字
905
+ - **風格選擇**:6 種圖片風格滿足不同需求
906
+ - **自動配圖**:每張投影片自動配上相關圖片
907
+ - **智能避重**:首頁和結尾使用不同關鍵字避免重複圖片
908
+
909
+ #### 🎨 專業版面設計
910
+ - **8 種版型**:商務、科技、創意、學術、簡約、橙色、紫色、藍綠風格
911
+ - **智能排版**:根據版型自動調整圖文位置
912
+ - **色彩搭配**:專業的色彩主題設計,高對比度確保文字清晰
913
+ - **中文字型**:完美支援繁體中文顯示
914
+ - **背景漸變**:精美的漸變背景和裝飾元素
915
+
916
+ #### 🔄 簡報改造功能
917
+ - **檔案分析**:智能分析上傳的PPT文件結構和內容
918
+ - **表格檢測**:自動識別包含表格的投影片,只套用配色不添加圖片
919
+ - **版型套用**:將現有簡報套用全新的專業版型設計
920
+ - **AI配圖**:為每頁內容生成專屬的圖片搜尋關鍵字並自動配圖
921
+ - **空間計算**:智能計算可用空間,合理放置圖片避免覆蓋原有內容
922
+
923
+ ### 📋 使用步驟
924
+
925
+ #### 🆕 創建新簡報
926
+ 1. **獲取 API 金鑰**:
927
+ - Gemini API:前往 [Google AI Studio](https://ai.google.dev/) 免費申請
928
+ - Pexels API:前往 [Pexels API](https://www.pexels.com/api/) 免費申請(200次/日)
929
+
930
+ 2. **輸入 API 金鑰**:在上方輸入框中填入你的 API 金鑰(每次使用都需要輸入)
931
+
932
+ 3. **設定簡報參數**:
933
+ - 輸入具體明確的簡報主題
934
+ - 選擇適合的版型和圖片風格
935
+ - 設定所需的投影片數量
936
+
937
+ 4. **生成簡報**:點擊生成按鈕,系統將自動完成所有工作
938
+
939
+ 5. **下載使用**:獲得完整的 .pptx 檔案,可直接在 PowerPoint 中使用
940
+
941
+ #### 🔄 改造現有簡報
942
+ 1. **上傳PPT文件**:支援 .pptx 和 .ppt 格式
943
+
944
+ 2. **選擇版型風格**:從8種專業版型中選擇適合的風格
945
+
946
+ 3. **選擇圖片風格**:選擇與內容匹配的圖片風格
947
+
948
+ 4. **開始分析改造**:AI將自動分析每頁內容並套用新設計
949
+
950
+ 5. **查看分析報告**:了解每頁的處理詳情和圖片添加情況
951
+
952
+ 6. **下載改造後簡報**:獲得全新設計的簡報文件
953
+
954
+ ### 💡 專業建議
955
+ - **主題要具體**:「AI在醫療診斷的應用」比「人工智慧」效果更好
956
+ - **選對風格**:商務場合用「professional」,創意展示用「creative」
957
+ - **適當頁數**:建議 5-8 頁,內容豐富但不冗長
958
+ - **測試 API**:第一次使用建議先測試 API 連接是否正常
959
+
960
+ ### 🔧 技術特點
961
+ - **純 Python 實現**:不需要安裝 Microsoft Office
962
+ - **即時生成**:通常 30-60 秒完成整個簡報
963
+ - **高品質輸出**:生成的 .pptx 檔案完全相容 PowerPoint
964
+ - **跨平台支援**:Windows、macOS、Linux 都能正常使用
965
+ """)
966
+
967
+ # 事件綁定
968
+ generate_btn.click(
969
+ fn=generate_ppt_with_gemini,
970
+ inputs=[
971
+ gemini_api_input,
972
+ pexels_api_input,
973
+ topic_input,
974
+ theme_dropdown,
975
+ slide_count,
976
+ image_style_dropdown
977
+ ],
978
+ outputs=[file_output, preview_output, status_output]
979
+ )
980
+
981
+ analyze_btn.click(
982
+ fn=analyze_and_restyle_ppt,
983
+ inputs=[
984
+ gemini_api_input,
985
+ pexels_api_input,
986
+ upload_file,
987
+ upload_theme_dropdown,
988
+ upload_image_style_dropdown
989
+ ],
990
+ outputs=[upload_file_output, analysis_report, upload_status_output]
991
+ )
992
+
993
+ return iface
994
+
995
+ if __name__ == "__main__":
996
+ # 啟動應用
997
+ iface = create_gemini_interface()
998
+ iface.launch(
999
+ server_name="0.0.0.0",
1000
+ server_port=7860,
1001
+ share=True,
1002
+ inbrowser=False
1003
  )
localapp.py ADDED
@@ -0,0 +1,1056 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ from slide_themes import SlideThemeManager
15
+ from ppt_analyzer import PPTAnalyzer
16
+
17
+ class GeminiPPTGenerator:
18
+ def __init__(self):
19
+ self.pexels_headers = {}
20
+ self.gemini_model = None
21
+ self.config_file = "config.json"
22
+
23
+ # 載入已保存的API金鑰
24
+ self.load_config()
25
+
26
+ # 初始化版型管理器
27
+ self.theme_manager = SlideThemeManager()
28
+
29
+ # 16:9 簡報尺寸 (單位:英吋)
30
+ self.slide_width = self.theme_manager.slide_width
31
+ self.slide_height = self.theme_manager.slide_height
32
+
33
+ # 圖片風格
34
+ self.image_styles = self.theme_manager.image_styles
35
+
36
+ def load_config(self):
37
+ """從config.json載入API金鑰"""
38
+ try:
39
+ if os.path.exists(self.config_file):
40
+ with open(self.config_file, 'r', encoding='utf-8') as f:
41
+ config = json.load(f)
42
+ gemini_key = config.get('gemini_api_key', '')
43
+ pexels_key = config.get('pexels_api_key', '')
44
+
45
+ if gemini_key and pexels_key:
46
+ self.setup_apis(gemini_key, pexels_key)
47
+ return gemini_key, pexels_key
48
+ except Exception as e:
49
+ print(f"載入配置錯誤: {e}")
50
+ return '', ''
51
+
52
+ def save_config(self, gemini_api_key, pexels_api_key):
53
+ """保存API金鑰到config.json"""
54
+ try:
55
+ config = {
56
+ 'gemini_api_key': gemini_api_key,
57
+ 'pexels_api_key': pexels_api_key
58
+ }
59
+ with open(self.config_file, 'w', encoding='utf-8') as f:
60
+ json.dump(config, f, ensure_ascii=False, indent=2)
61
+ return True
62
+ except Exception as e:
63
+ print(f"保存配置錯誤: {e}")
64
+ return False
65
+
66
+ def get_saved_keys(self):
67
+ """獲取已保存的API金鑰"""
68
+ try:
69
+ if os.path.exists(self.config_file):
70
+ with open(self.config_file, 'r', encoding='utf-8') as f:
71
+ config = json.load(f)
72
+ return config.get('gemini_api_key', ''), config.get('pexels_api_key', '')
73
+ except:
74
+ pass
75
+ return '', ''
76
+
77
+ def setup_apis(self, gemini_api_key, pexels_api_key):
78
+ """設定 API 金鑰"""
79
+ try:
80
+ # 設定 Gemini API
81
+ if gemini_api_key:
82
+ genai.configure(api_key=gemini_api_key)
83
+ self.gemini_model = genai.GenerativeModel('gemini-2.5-flash-preview-05-20')
84
+
85
+ # 設定 Pexels API
86
+ if pexels_api_key:
87
+ self.pexels_headers = {
88
+ "Authorization": pexels_api_key
89
+ }
90
+
91
+ return True, "✅ API 設定成功"
92
+ except Exception as e:
93
+ return False, f"❌ API 設定失敗:{str(e)}"
94
+
95
+ def generate_content_with_gemini(self, topic, slide_count=5):
96
+ """使用 Gemini 生成簡報內容"""
97
+
98
+ prompt = f"""
99
+ 請為主題「{topic}」製作一個 {slide_count} 頁的簡報大綱,以 JSON 格式回傳。
100
+
101
+ 格式要求:
102
+ {{
103
+ "title": "簡報主標題",
104
+ "subtitle": "簡報副標題",
105
+ "title_keywords": "主題相關的英文關鍵字,用於搜尋標題頁和結尾頁圖片",
106
+ "slides": [
107
+ {{
108
+ "title": "投影片標題",
109
+ "content": [
110
+ "重點1",
111
+ "重點2",
112
+ "重點3"
113
+ ],
114
+ "image_keywords": "英文關鍵字,用於搜尋相關圖片"
115
+ }}
116
+ ]
117
+ }}
118
+
119
+ 要求:
120
+ 1. 內容要專業且有邏輯性
121
+ 2. 每頁 3-4 個重點
122
+ 3. title_keywords 要用英文,描述主題相關的圖片搜尋關鍵字(3-5個詞)
123
+ 4. image_keywords 要用英文,描述該投影片適合的圖片內容
124
+ 5. 關鍵字要具體明確,例如 "business meeting", "technology innovation", "data analysis"
125
+ 6. 使用繁體中文(除了 title_keywords 和 image_keywords)
126
+ 7. 第一頁是概述介紹,最後一頁是結論總結
127
+ 8. 請直接回傳 JSON,不要包含其他文字說明,也不可以有"**"等不必要的markdown符號
128
+ """
129
+
130
+ try:
131
+ if self.gemini_model:
132
+ response = self.gemini_model.generate_content(prompt)
133
+ content = response.text
134
+
135
+ # 清理回應內容,提取 JSON
136
+ content = content.strip()
137
+ if content.startswith('```json'):
138
+ content = content[7:]
139
+ if content.endswith('```'):
140
+ content = content[:-3]
141
+
142
+ # 尋找 JSON 開始和結束位置
143
+ start = content.find('{')
144
+ end = content.rfind('}') + 1
145
+
146
+ if start != -1 and end > start:
147
+ json_str = content[start:end]
148
+ return json.loads(json_str)
149
+ else:
150
+ raise ValueError("無法在回應中找到有效的 JSON")
151
+
152
+ else:
153
+ return self.get_default_structure_with_images(topic)
154
+
155
+ except Exception as e:
156
+ print(f"Gemini API 錯誤: {e}")
157
+ return self.get_default_structure_with_images(topic)
158
+
159
+ def get_default_structure_with_images(self, topic):
160
+ """預設簡報結構(含圖片關鍵字)"""
161
+ # 生成簡單的英文關鍵字
162
+ title_keywords = "business presentation professional meeting"
163
+ if "科技" in topic or "技術" in topic:
164
+ title_keywords = "technology innovation digital development"
165
+ elif "教育" in topic or "學習" in topic:
166
+ title_keywords = "education learning academic study"
167
+ elif "醫療" in topic or "健康" in topic:
168
+ title_keywords = "healthcare medical health wellness"
169
+ elif "環境" in topic or "環保" in topic:
170
+ title_keywords = "environment sustainability green nature"
171
+ elif "經濟" in topic or "金融" in topic:
172
+ title_keywords = "economics finance business economy"
173
+
174
+ return {
175
+ "title": f"{topic} 簡報",
176
+ "subtitle": "由 AI 自動生成",
177
+ "title_keywords": title_keywords,
178
+ "slides": [
179
+ {
180
+ "title": "簡介與背景",
181
+ "content": [
182
+ "主題背景介紹",
183
+ "研究目的與範圍",
184
+ "簡報架構說明"
185
+ ],
186
+ "image_keywords": "presentation introduction business"
187
+ },
188
+ {
189
+ "title": "主要內容分析",
190
+ "content": [
191
+ "核心概念說明",
192
+ "重要特點分析",
193
+ "相關案例討論"
194
+ ],
195
+ "image_keywords": "analysis data research content"
196
+ },
197
+ {
198
+ "title": "深入探討",
199
+ "content": [
200
+ "優勢與機會識別",
201
+ "挑戰與問題分析",
202
+ "影響因素評估"
203
+ ],
204
+ "image_keywords": "strategy planning discussion"
205
+ },
206
+ {
207
+ "title": "解決方案與建議",
208
+ "content": [
209
+ "策略建議提出",
210
+ "實施方法規劃",
211
+ "預期效果評估"
212
+ ],
213
+ "image_keywords": "solution implementation strategy"
214
+ },
215
+ {
216
+ "title": "結論與展望",
217
+ "content": [
218
+ "重點總結回顧",
219
+ "未來發展趨勢",
220
+ "行動建議提出"
221
+ ],
222
+ "image_keywords": "conclusion future success"
223
+ }
224
+ ]
225
+ }
226
+
227
+ def search_pexels_with_style(self, keywords, image_style="professional", per_page=10):
228
+ """根據風格搜尋 Pexels 圖片"""
229
+ if not self.pexels_headers:
230
+ return None
231
+
232
+ # 先嘗試純主題關鍵字搜尋
233
+ url = "https://api.pexels.com/v1/search"
234
+ params = {
235
+ "query": keywords,
236
+ "per_page": per_page,
237
+ "orientation": "landscape",
238
+ "size": "medium"
239
+ }
240
+
241
+ try:
242
+ response = requests.get(url, headers=self.pexels_headers, params=params)
243
+ if response.status_code == 200:
244
+ data = response.json()
245
+ if data["photos"] and len(data["photos"]) >= 3:
246
+ return data["photos"]
247
+
248
+ # 如果純主題搜尋結果不足,再組合風格關鍵字
249
+ style_modifier = self.image_styles.get(image_style, "")
250
+ enhanced_keywords = f"{keywords} {style_modifier}"
251
+
252
+ params["query"] = enhanced_keywords
253
+ response = requests.get(url, headers=self.pexels_headers, params=params)
254
+ if response.status_code == 200:
255
+ data = response.json()
256
+ return data["photos"] if data["photos"] else None
257
+
258
+ return None
259
+ except Exception as e:
260
+ print(f"Pexels API 錯誤: {e}")
261
+ return None
262
+
263
+ def select_best_image(self, photos, slide_title=""):
264
+ """從多張圖片中選擇最適合的"""
265
+ if not photos:
266
+ return None
267
+
268
+ # 選擇解析度較高的圖片
269
+ best_photo = photos[0]
270
+ for photo in photos[:3]:
271
+ if photo["width"] * photo["height"] > best_photo["width"] * best_photo["height"]:
272
+ best_photo = photo
273
+
274
+ return best_photo["src"]["medium"]
275
+
276
+ def download_image(self, image_url):
277
+ """下載圖片並返回檔案路徑"""
278
+ if not image_url:
279
+ return None
280
+
281
+ try:
282
+ response = requests.get(image_url)
283
+ if response.status_code == 200:
284
+ temp_dir = tempfile.mkdtemp()
285
+ image_path = os.path.join(temp_dir, "slide_image.jpg")
286
+
287
+ # 處理圖片
288
+ image = Image.open(BytesIO(response.content))
289
+
290
+ # 調整圖片大小
291
+ max_size = (800, 600)
292
+ image.thumbnail(max_size, Image.Resampling.LANCZOS)
293
+
294
+ # 轉換並儲存
295
+ if image.mode in ("RGBA", "P"):
296
+ image = image.convert("RGB")
297
+ image.save(image_path, "JPEG", quality=85)
298
+
299
+ return image_path
300
+ return None
301
+ except Exception as e:
302
+ print(f"圖片下載錯誤: {e}")
303
+ return None
304
+
305
+ def add_image_to_slide(self, slide, image_path, theme):
306
+ """將圖片添加到投影片,保持比例避免變形"""
307
+ if not image_path or not os.path.exists(image_path):
308
+ return
309
+
310
+ try:
311
+ image_area = theme["image_area"]
312
+
313
+ # 目標區域
314
+ target_left = Inches(image_area["left"])
315
+ target_top = Inches(image_area["top"])
316
+ target_width = Inches(image_area["width"])
317
+ target_height = Inches(image_area["height"])
318
+
319
+ # 載入圖片獲取原始尺寸
320
+ from PIL import Image as PILImage
321
+ with PILImage.open(image_path) as img:
322
+ original_width, original_height = img.size
323
+ original_ratio = original_width / original_height
324
+
325
+ # 計算目標比例
326
+ target_ratio = target_width.inches / target_height.inches
327
+
328
+ # 根據比例計算實際顯示尺寸,保持圖片比例
329
+ if original_ratio > target_ratio:
330
+ # 圖片較寬,以寬度為準
331
+ actual_width = target_width
332
+ actual_height = Inches(target_width.inches / original_ratio)
333
+ # 垂直置中
334
+ actual_top = Inches(target_top.inches + (target_height.inches - actual_height.inches) / 2)
335
+ actual_left = target_left
336
+ else:
337
+ # 圖片較高,以高度為準
338
+ actual_height = target_height
339
+ actual_width = Inches(target_height.inches * original_ratio)
340
+ # 水平置中
341
+ actual_left = Inches(target_left.inches + (target_width.inches - actual_width.inches) / 2)
342
+ actual_top = target_top
343
+
344
+ # 添加圖片
345
+ picture = slide.shapes.add_picture(image_path, actual_left, actual_top, actual_width, actual_height)
346
+
347
+ except Exception as e:
348
+ print(f"添加圖片錯誤: {e}")
349
+ # 降級處理:如果計算失敗,使用原來的方式
350
+ try:
351
+ left = Inches(image_area["left"])
352
+ top = Inches(image_area["top"])
353
+ width = Inches(image_area["width"])
354
+ height = Inches(image_area["height"])
355
+ slide.shapes.add_picture(image_path, left, top, width, height)
356
+ except:
357
+ pass
358
+
359
+ def setup_slide_content(self, slide, slide_data, theme):
360
+ """設定投影片內容,使用正確的位置"""
361
+ try:
362
+ # 設置背景和裝飾元素
363
+ self.theme_manager.setup_slide_background_and_layout(slide, theme)
364
+
365
+ # 設定標題
366
+ title_shape = slide.shapes.title
367
+ title_shape.text = slide_data["title"]
368
+
369
+ # 調整標題位置和尺寸
370
+ title_area = theme["title_area"]
371
+ title_shape.left = Inches(title_area["left"])
372
+ title_shape.top = Inches(title_area["top"])
373
+ title_shape.width = Inches(title_area["width"])
374
+ title_shape.height = Inches(title_area["height"])
375
+
376
+ self.theme_manager.format_title(title_shape, theme, 34, self.get_font_name)
377
+
378
+ # 移除預設內容佔位符(如果存在)
379
+ shapes_to_remove = []
380
+ for shape in slide.shapes:
381
+ try:
382
+ if hasattr(shape, 'placeholder_format') and shape.placeholder_format is not None:
383
+ if shape.placeholder_format.type == 2: # 內容佔位符
384
+ shapes_to_remove.append(shape)
385
+ except:
386
+ continue
387
+
388
+ for shape in shapes_to_remove:
389
+ try:
390
+ sp = shape.element
391
+ sp.getparent().remove(sp)
392
+ except:
393
+ continue
394
+
395
+ # 設定內容區域
396
+ content_area = theme["content_area"]
397
+
398
+ # 創建帶背景的內容框
399
+ self.theme_manager.create_content_box_with_background(slide, theme, content_area)
400
+
401
+ # 創建新的內容文字框
402
+ left = Inches(content_area["left"])
403
+ top = Inches(content_area["top"])
404
+ width = Inches(content_area["width"])
405
+ height = Inches(content_area["height"])
406
+
407
+ textbox = slide.shapes.add_textbox(left, top, width, height)
408
+ text_frame = textbox.text_frame
409
+
410
+ # 設定文字框屬性
411
+ text_frame.margin_left = Inches(0.15)
412
+ text_frame.margin_right = Inches(0.15)
413
+ text_frame.margin_top = Inches(0.1)
414
+ text_frame.margin_bottom = Inches(0.1)
415
+ text_frame.word_wrap = True
416
+ text_frame.auto_size = None # 不自動調整大小
417
+
418
+ # 清除預設文字
419
+ text_frame.clear()
420
+
421
+ # 添加內容
422
+ for i, point in enumerate(slide_data["content"]):
423
+ if i == 0:
424
+ p = text_frame.paragraphs[0]
425
+ else:
426
+ p = text_frame.add_paragraph()
427
+
428
+ p.text = f"• {point}"
429
+ p.level = 0
430
+ p.space_after = Pt(10) # 段落間距
431
+ self.theme_manager.format_content(p, theme, 22, self.get_font_name)
432
+
433
+ except Exception as e:
434
+ print(f"設定投影片內容錯誤詳細: {str(e)}")
435
+ import traceback
436
+ print(f"錯誤追蹤: {traceback.format_exc()}")
437
+
438
+ def adjust_content_layout(self, slide, layout_type):
439
+ """這個方法已被 setup_slide_content 取代,保留以免錯誤"""
440
+ pass
441
+
442
+ def get_font_name(self):
443
+ """獲取中文字型名稱"""
444
+ # 檢查是否有自定義中文字型檔案
445
+ font_path = os.path.join(os.path.dirname(__file__), "cht.ttf")
446
+ if os.path.exists(font_path):
447
+ return "cht" # 使用自定義字型
448
+ else:
449
+ # 備用字型選擇
450
+ return "Arial Unicode MS" # 通用 Unicode 字型
451
+
452
+ def format_title_with_shadow(self, shape, theme, font_size):
453
+ """格式化標題並添加陰影效果以提高可讀性"""
454
+ self.theme_manager.format_title(shape, theme, font_size, self.get_font_name)
455
+ try:
456
+ paragraph = shape.text_frame.paragraphs[0]
457
+ paragraph.font.color.rgb = RGBColor(255, 255, 255) # 白色文字在深色背景上更清楚
458
+ paragraph.alignment = PP_ALIGN.CENTER
459
+ paragraph.font.bold = True
460
+ except:
461
+ pass
462
+
463
+ def create_presentation_with_images(self, topic, theme_name="商務專業",
464
+ slide_count=5, image_style="professional"):
465
+ """建立包含圖片的簡報"""
466
+
467
+ # 生成內容結構
468
+ structure = self.generate_content_with_gemini(topic, slide_count)
469
+ theme = self.theme_manager.get_theme(theme_name)
470
+
471
+ # 建立 16:9 簡報
472
+ prs = Presentation()
473
+ prs.slide_width = self.slide_width
474
+ prs.slide_height = self.slide_height
475
+
476
+ # 建立標題頁
477
+ title_slide = prs.slides.add_slide(prs.slide_layouts[0])
478
+ title_shape = title_slide.shapes.title
479
+ subtitle_shape = title_slide.placeholders[1]
480
+
481
+ title_shape.text = structure["title"]
482
+ subtitle_shape.text = structure["subtitle"]
483
+
484
+ # 調整標題頁版面 (16:9)
485
+ title_shape.left = Inches(1.0)
486
+ title_shape.top = Inches(2.0)
487
+ title_shape.width = Inches(11.333)
488
+ title_shape.height = Inches(1.5)
489
+
490
+ subtitle_shape.left = Inches(1.0)
491
+ subtitle_shape.top = Inches(4.0)
492
+ subtitle_shape.width = Inches(11.333)
493
+ subtitle_shape.height = Inches(1.0)
494
+
495
+ # 格式化標題頁 - 加強文字可讀性
496
+ self.format_title_with_shadow(title_shape, theme, 54)
497
+ self.format_title_with_shadow(subtitle_shape, theme, 32)
498
+
499
+ # 為標題頁添加主題相關圖片 - 使用AI生成的英文關鍵字
500
+ main_keywords = structure.get("title_keywords", f"{topic} introduction overview")
501
+ title_photos = self.search_pexels_with_style(main_keywords, image_style, per_page=15)
502
+ if title_photos:
503
+ title_image_url = self.select_best_image(title_photos, structure["title"])
504
+ if title_image_url:
505
+ title_image_path = self.download_image(title_image_url)
506
+ if title_image_path:
507
+ # 標題頁使用半透明背景
508
+ self.add_title_background_with_overlay(title_slide, title_image_path, theme)
509
+
510
+ # 建立內容頁
511
+ for i, slide_data in enumerate(structure["slides"]):
512
+ slide = prs.slides.add_slide(prs.slide_layouts[1])
513
+
514
+ # 設定內容和版面
515
+ self.setup_slide_content(slide, slide_data, theme)
516
+
517
+ # 搜尋並添加圖片
518
+ keywords = slide_data.get("image_keywords", f"{topic} slide {i+1}")
519
+ photos = self.search_pexels_with_style(keywords, image_style)
520
+
521
+ if photos:
522
+ image_url = self.select_best_image(photos, slide_data["title"])
523
+ if image_url:
524
+ image_path = self.download_image(image_url)
525
+ if image_path:
526
+ self.add_image_to_slide(slide, image_path, theme)
527
+
528
+ # 建立感謝頁
529
+ self.add_thank_you_slide(prs, theme, image_style, topic, structure)
530
+
531
+ return prs, structure
532
+
533
+ def search_pexels_image_for_title(self, keywords, topic, per_page=10):
534
+ """專門為標題頁搜尋圖片,優先考慮主題相關性"""
535
+ if not self.pexels_headers:
536
+ return None
537
+
538
+ # 先嘗試純主題搜尋
539
+ topic_keywords = f"{topic} background"
540
+
541
+ url = "https://api.pexels.com/v1/search"
542
+ params = {
543
+ "query": topic_keywords,
544
+ "per_page": per_page,
545
+ "orientation": "landscape",
546
+ "size": "medium"
547
+ }
548
+
549
+ try:
550
+ response = requests.get(url, headers=self.pexels_headers, params=params)
551
+ if response.status_code == 200:
552
+ data = response.json()
553
+ if data["photos"]:
554
+ return data["photos"]
555
+
556
+ # 如果主題搜尋沒結果,使用通用關鍵字
557
+ fallback_params = {
558
+ "query": "professional presentation background",
559
+ "per_page": per_page,
560
+ "orientation": "landscape",
561
+ "size": "medium"
562
+ }
563
+
564
+ response = requests.get(url, headers=self.pexels_headers, params=fallback_params)
565
+ if response.status_code == 200:
566
+ data = response.json()
567
+ return data["photos"] if data["photos"] else None
568
+
569
+ return None
570
+ except Exception as e:
571
+ print(f"Pexels API 錯誤: {e}")
572
+ return None
573
+
574
+ def add_title_background_with_overlay(self, slide, image_path, theme):
575
+ """為標題頁添加帶有文字背景框的背景圖片"""
576
+ try:
577
+ # 添加背景圖片
578
+ picture = slide.shapes.add_picture(
579
+ image_path,
580
+ Inches(0),
581
+ Inches(0),
582
+ self.slide_width,
583
+ self.slide_height
584
+ )
585
+ # 移到背景層
586
+ picture.element.getparent().remove(picture.element)
587
+ slide.shapes._spTree.insert(2, picture.element)
588
+
589
+ except Exception as e:
590
+ print(f"添加標題背景錯誤: {e}")
591
+ # 降級處理:直接添加背景圖片
592
+ try:
593
+ picture = slide.shapes.add_picture(
594
+ image_path,
595
+ Inches(0),
596
+ Inches(0),
597
+ self.slide_width,
598
+ self.slide_height
599
+ )
600
+ picture.element.getparent().remove(picture.element)
601
+ slide.shapes._spTree.insert(2, picture.element)
602
+ except:
603
+ pass
604
+
605
+ def add_title_background(self, slide, image_path):
606
+ """為標題頁添加背景圖片(保留原方法以免錯誤)"""
607
+ self.add_title_background_with_overlay(slide, image_path, None)
608
+
609
+ def add_thank_you_slide(self, prs, theme, image_style, topic, structure):
610
+ """添��感謝頁"""
611
+ thank_slide = prs.slides.add_slide(prs.slide_layouts[5])
612
+
613
+ # 感謝頁使用主題關鍵字加上結尾相關詞彙
614
+ title_keywords = structure.get("title_keywords", f"{topic} success conclusion achievement")
615
+ thank_keywords = f"{title_keywords} success conclusion achievement"
616
+ thank_photos = self.search_pexels_with_style(thank_keywords, image_style, per_page=12)
617
+ if thank_photos:
618
+ thank_image_url = self.select_best_image(thank_photos)
619
+ if thank_image_url:
620
+ thank_image_path = self.download_image(thank_image_url)
621
+ if thank_image_path:
622
+ self.add_title_background_with_overlay(thank_slide, thank_image_path, theme)
623
+
624
+ # 添加感謝文字背景框
625
+ text_bg = thank_slide.shapes.add_shape(
626
+ 1, # 矩形
627
+ Inches(2.5),
628
+ Inches(2.0),
629
+ Inches(8.333),
630
+ Inches(3.5)
631
+ )
632
+
633
+ fill = text_bg.fill
634
+ fill.solid()
635
+ fill.fore_color.rgb = RGBColor(255, 255, 255) # 白色背景
636
+ text_bg.line.color.rgb = theme["accent_color"] if theme else RGBColor(79, 129, 189)
637
+ text_bg.line.width = Pt(3)
638
+
639
+ # 添加感謝文字 (16:9 居中位置)
640
+ left = Inches(3.0)
641
+ top = Inches(2.5)
642
+ width = Inches(7.333)
643
+ height = Inches(2.5)
644
+
645
+ textbox = thank_slide.shapes.add_textbox(left, top, width, height)
646
+ text_frame = textbox.text_frame
647
+ text_frame.text = "謝謝聆聽\nThank You"
648
+
649
+ for paragraph in text_frame.paragraphs:
650
+ paragraph.font.name = self.get_font_name()
651
+ paragraph.font.size = Pt(60)
652
+ paragraph.font.color.rgb = theme["title_color"] if theme else RGBColor(31, 73, 125)
653
+ paragraph.alignment = PP_ALIGN.CENTER
654
+ paragraph.font.bold = True
655
+
656
+
657
+ def save_presentation(self, prs, filename):
658
+ """儲存簡報"""
659
+ temp_dir = tempfile.mkdtemp()
660
+ filepath = os.path.join(temp_dir, filename)
661
+ prs.save(filepath)
662
+ return filepath
663
+
664
+
665
+ def generate_preview_text(self, structure):
666
+ """生成簡報預覽文字"""
667
+ preview = f"📊 {structure['title']}\n"
668
+ preview += f" {structure['subtitle']}\n\n"
669
+
670
+ for i, slide in enumerate(structure['slides'], 1):
671
+ preview += f"{i}. {slide['title']}\n"
672
+ for point in slide['content'][:2]: # 只顯示前兩個重點
673
+ preview += f" • {point}\n"
674
+ if len(slide['content']) > 2:
675
+ preview += f" • ...(共 {len(slide['content'])} 個重點)\n"
676
+ preview += "\n"
677
+
678
+ return preview
679
+
680
+ def analyze_and_restyle_ppt(gemini_api_key, pexels_api_key, uploaded_file, theme_name, image_style):
681
+ """分析並重新設計上傳的簡報"""
682
+
683
+ if not uploaded_file:
684
+ return None, "", "❌ 請上傳PPT文件"
685
+
686
+ generator = GeminiPPTGenerator()
687
+
688
+ # 如果API金鑰為空,嘗試從已保存的配置載入
689
+ if not gemini_api_key.strip() or not pexels_api_key.strip():
690
+ saved_gemini, saved_pexels = generator.get_saved_keys()
691
+ if not gemini_api_key.strip():
692
+ gemini_api_key = saved_gemini
693
+ if not pexels_api_key.strip():
694
+ pexels_api_key = saved_pexels
695
+
696
+ # 檢查輸入
697
+ if not gemini_api_key.strip():
698
+ return None, "", "❌ 請輸入 Gemini API 金鑰"
699
+
700
+ if not pexels_api_key.strip():
701
+ return None, "", "❌ 請輸入 Pexels API 金鑰"
702
+
703
+ try:
704
+ # 設定 API
705
+ success, message = generator.setup_apis(gemini_api_key, pexels_api_key)
706
+ if not success:
707
+ return None, "", message
708
+
709
+ # 創建分析器
710
+ analyzer = PPTAnalyzer(
711
+ gemini_model=generator.gemini_model,
712
+ pexels_headers=generator.pexels_headers,
713
+ image_styles=generator.image_styles
714
+ )
715
+
716
+ # 分析上傳的PPT
717
+ analysis_result = analyzer.analyze_ppt_file(uploaded_file.name)
718
+ if not analysis_result:
719
+ return None, "", "❌ 無法分析PPT文件,請確認文件格式正確"
720
+
721
+ # 套用新主題和添加圖片
722
+ processed_prs, processed_slides = analyzer.apply_theme_to_presentation(
723
+ uploaded_file.name, theme_name, image_style, analysis_result
724
+ )
725
+
726
+ if not processed_prs:
727
+ return None, "", "❌ 處理PPT文件時發生錯誤"
728
+
729
+ # 生成分析報告
730
+ report = analyzer.generate_analysis_report(analysis_result, processed_slides)
731
+
732
+ # 儲存處理後的簡報
733
+ original_name = os.path.splitext(os.path.basename(uploaded_file.name))[0]
734
+ filename = f"{original_name}_{theme_name}_{image_style}_restyled.pptx"
735
+ output_path = analyzer.save_processed_presentation(processed_prs, filename)
736
+
737
+ if not output_path:
738
+ return None, "", "❌ 儲存處理後的簡報時發生錯誤"
739
+
740
+ success_msg = f"✅ 成功重新設計《{original_name}》!\n"
741
+ success_msg += f"🎨 套用主題:{theme_name}\n"
742
+ success_msg += f"🖼️ 圖片風格:{image_style}\n"
743
+ success_msg += f"📄 處理了 {len(processed_slides)} 張投影片"
744
+
745
+ return output_path, report, success_msg
746
+
747
+ except Exception as e:
748
+ import traceback
749
+ error_details = traceback.format_exc()
750
+ print(f"詳細錯誤: {error_details}")
751
+ return None, "", f"❌ 處理失敗:{str(e)}"
752
+
753
+
754
+
755
+ def generate_ppt_with_gemini(gemini_api_key, pexels_api_key, topic, theme, slide_count, image_style):
756
+ """生成簡報的主要函數"""
757
+
758
+ generator = GeminiPPTGenerator()
759
+
760
+ # 如果API金鑰為空,嘗試從已保存的配置載入
761
+ if not gemini_api_key.strip() or not pexels_api_key.strip():
762
+ saved_gemini, saved_pexels = generator.get_saved_keys()
763
+ if not gemini_api_key.strip():
764
+ gemini_api_key = saved_gemini
765
+ if not pexels_api_key.strip():
766
+ pexels_api_key = saved_pexels
767
+
768
+ # 檢查輸入
769
+ if not gemini_api_key.strip():
770
+ return None, "", "❌ 請輸入 Gemini API 金鑰"
771
+
772
+ if not pexels_api_key.strip():
773
+ return None, "", "❌ 請輸入 Pexels API 金鑰"
774
+
775
+ if not topic.strip():
776
+ return None, "", "❌ 請輸入簡報主題"
777
+
778
+ try:
779
+ # 設定 API
780
+ success, message = generator.setup_apis(gemini_api_key, pexels_api_key)
781
+ if not success:
782
+ return None, "", message
783
+
784
+ # 保存API金鑰到配置檔案
785
+ generator.save_config(gemini_api_key, pexels_api_key)
786
+
787
+ # 生成簡報
788
+ prs, structure = generator.create_presentation_with_images(
789
+ topic, theme, slide_count, image_style
790
+ )
791
+
792
+ # 生成預覽
793
+ preview = generator.generate_preview_text(structure)
794
+
795
+ # 儲存檔案
796
+ filename = f"{topic.replace(' ', '_')}_{image_style}_簡報.pptx"
797
+ filepath = generator.save_presentation(prs, filename)
798
+
799
+ success_msg = f"✅ 成功生成《{topic}》{image_style}風格簡報!({slide_count} 頁,含圖片)"
800
+
801
+ return filepath, preview, success_msg
802
+
803
+ except Exception as e:
804
+ import traceback
805
+ error_details = traceback.format_exc()
806
+ print(f"詳細錯誤: {error_details}")
807
+ return None, "", f"❌ 生成失敗:{str(e)}"
808
+
809
+ # Gradio 介面
810
+ def create_gemini_interface():
811
+ """建立 Gradio 介面"""
812
+
813
+ # 檢查是否已有保存的API金鑰
814
+ generator = GeminiPPTGenerator()
815
+ saved_gemini, saved_pexels = generator.get_saved_keys()
816
+ keys_exist = bool(saved_gemini and saved_pexels)
817
+
818
+ with gr.Blocks(title="Gemini AI 圖文簡報生成器", theme=gr.themes.Soft()) as iface:
819
+ gr.Markdown("# 🤖 Gemini AI 智能圖文簡報生成器")
820
+ gr.Markdown("**使用 Google Gemini 2.0 + Pexels 圖庫**,智能生成專業圖文簡報,或改造現有簡報")
821
+
822
+ # API 設定區域(共用)
823
+ if keys_exist:
824
+ with gr.Group():
825
+ gr.Markdown("### ✅ API 金鑰已配置")
826
+ gr.Markdown("API 金鑰已從 config.json 載入,可直接使用。如需更新金鑰,請刪除 config.json 檔案後重新啟動。")
827
+ # 隱藏的輸入框,用於傳遞已保存的金鑰
828
+ gemini_api_input = gr.Textbox(value=saved_gemini, visible=False)
829
+ pexels_api_input = gr.Textbox(value=saved_pexels, visible=False)
830
+ else:
831
+ with gr.Group():
832
+ gr.Markdown("### 🔑 API 設定")
833
+ with gr.Row():
834
+ gemini_api_input = gr.Textbox(
835
+ label="🤖 Gemini API Key",
836
+ placeholder="請輸入你的 Gemini API 金鑰",
837
+ type="password",
838
+ info="免費額度,前往 https://ai.google.dev/ 獲取"
839
+ )
840
+ pexels_api_input = gr.Textbox(
841
+ label="📸 Pexels API Key",
842
+ placeholder="請輸入你的 Pexels API 金鑰",
843
+ type="password",
844
+ info="免費 200次/月,前往 https://www.pexels.com/api/ 獲取"
845
+ )
846
+
847
+ # 選項卡
848
+ with gr.Tabs():
849
+ # 原有的生成功能
850
+ with gr.TabItem("🆕 創建新簡報"):
851
+ # 主要設定區域
852
+ with gr.Row():
853
+ with gr.Column(scale=2):
854
+ topic_input = gr.Textbox(
855
+ label="📝 簡報主題",
856
+ placeholder="請輸入具體的簡報主題...",
857
+ value="人工智慧在現代教育中的應用與挑戰"
858
+ )
859
+
860
+ with gr.Row():
861
+ # 從主題管理器獲取所有主題名稱
862
+ generator = GeminiPPTGenerator()
863
+ theme_dropdown = gr.Dropdown(
864
+ choices=generator.theme_manager.get_all_theme_names(),
865
+ value="商務專業",
866
+ label="🎨 版型風格"
867
+ )
868
+
869
+ image_style_dropdown = gr.Dropdown(
870
+ choices=["professional", "creative", "minimalist", "modern", "natural", "technology"],
871
+ value="professional",
872
+ label="🖼️ 圖片風格"
873
+ )
874
+
875
+ slide_count = gr.Slider(
876
+ minimum=3,
877
+ maximum=20,
878
+ value=6,
879
+ step=1,
880
+ label="📄 投影片數量"
881
+ )
882
+
883
+ generate_btn = gr.Button("🚀 生成專業簡報", variant="primary", size="lg")
884
+
885
+ with gr.Column(scale=1):
886
+ status_output = gr.Textbox(label="📊 生成狀態", interactive=False)
887
+ file_output = gr.File(label="📁 下載簡報")
888
+
889
+ # 預覽區域
890
+ with gr.Group():
891
+ gr.Markdown("### 📋 簡報預覽")
892
+ preview_output = gr.Textbox(
893
+ label="內容大綱",
894
+ placeholder="生成後將顯示簡報大綱...",
895
+ lines=8,
896
+ interactive=False
897
+ )
898
+
899
+ # 新增的簡報改造功能
900
+ with gr.TabItem("🔄 改造現有簡報"):
901
+ gr.Markdown("### 📤 上傳並改造您的簡報")
902
+ gr.Markdown("上傳現有的PPT文件,AI將分析內容並套用新的版型設計,自動為每頁添加相關圖片")
903
+
904
+ with gr.Row():
905
+ with gr.Column(scale=2):
906
+ # 文件上傳
907
+ upload_file = gr.File(
908
+ label="📎 上傳PPT文件",
909
+ file_types=[".pptx", ".ppt"],
910
+ type="filepath"
911
+ )
912
+
913
+ with gr.Row():
914
+ # 主題選擇
915
+ upload_theme_dropdown = gr.Dropdown(
916
+ choices=generator.theme_manager.get_all_theme_names(),
917
+ value="商務專業",
918
+ label="🎨 套用版型風格"
919
+ )
920
+
921
+ upload_image_style_dropdown = gr.Dropdown(
922
+ choices=["professional", "creative", "minimalist", "modern", "natural", "technology"],
923
+ value="professional",
924
+ label="🖼️ 圖片風格"
925
+ )
926
+
927
+ analyze_btn = gr.Button("🔍 分析並改造簡報", variant="primary", size="lg")
928
+
929
+ with gr.Column(scale=1):
930
+ upload_status_output = gr.Textbox(label="📊 處理狀態", interactive=False)
931
+ upload_file_output = gr.File(label="📁 下載改造後簡報")
932
+
933
+ # 分析報告區域
934
+ with gr.Group():
935
+ gr.Markdown("### 📋 分析報告")
936
+ analysis_report = gr.Textbox(
937
+ label="處理詳情",
938
+ placeholder="上傳並處理後將顯示分析報告...",
939
+ lines=8,
940
+ interactive=False
941
+ )
942
+
943
+ # 說明區域
944
+ with gr.Accordion("📖 使用說明與功能特色", open=False):
945
+ gr.Markdown("""
946
+ ### 🌟 核心特色
947
+
948
+ #### 🤖 Google Gemini 2.0 Flash
949
+ - **最新模型**:使用 Gemini 2.0 Flash Preview 版本
950
+ - **免費額度**:Google 提供慷慨的免費使用額度
951
+ - **中文優化**:對繁體中文有優秀的理解和生成能力
952
+ - **結構化輸出**:精確生成 JSON 格式的簡報結構
953
+ - **內容分析**:智能分析現有簡報內容,生成適合的圖片搜尋關鍵字
954
+
955
+ #### 📸 Pexels 圖片整合
956
+ - **百萬圖庫**:Pexels 提供高品質免費圖片
957
+ - **智能匹配**:AI 為每頁生成最適合的搜尋關鍵字
958
+ - **風格選擇**:6 種圖片風格滿足不同需求
959
+ - **自動配圖**:每張投影片自動配上相關圖片
960
+ - **智能避重**:首頁和結尾使用不同關鍵字避免重複圖片
961
+
962
+ #### 🎨 專業版面設計
963
+ - **8 種版型**:商務、科技、創意、學術、簡約、橙色、紫色、藍綠風格
964
+ - **智能排版**:根據版型自動調整圖文位置
965
+ - **色彩搭配**:專業的色彩主題設計,高對比度確保文字清晰
966
+ - **中文字型**:完美支援繁體中文顯示
967
+ - **背景漸變**:精美的漸變背景和裝飾元素
968
+
969
+ #### 🔄 簡報改造功能
970
+ - **檔案分析**:智能分析上傳的PPT文件結構和內容
971
+ - **表格檢測**:自動識別包含表格的投影片,只套用配色不添加圖片
972
+ - **版型套用**:將現有簡報套用全新的專業版型設計
973
+ - **AI配圖**:為每頁內容生成專屬的圖片搜尋關鍵字並自動配圖
974
+ - **空間計算**:智能計算可用空間,合理放置圖片避免覆蓋原有內容
975
+
976
+ ### 📋 使用步驟
977
+
978
+ #### 🆕 創建新簡報
979
+ 1. **獲取 API 金鑰**:
980
+ - Gemini API:前往 [Google AI Studio](https://ai.google.dev/) 免費申請
981
+ - Pexels API:前往 [Pexels API](https://www.pexels.com/api/) 免費申請(200次/日)
982
+
983
+ 2. **輸入 API 金鑰**:在上方輸入框中填入你的 API 金鑰(僅需輸入一次,會自動保存到 config.json)
984
+
985
+ 3. **設定簡報參數**:
986
+ - 輸入具體明確的簡報主題
987
+ - 選擇適合的版型和圖片風格
988
+ - 設定所需的投影片數量
989
+
990
+ 4. **生成簡報**:點擊生成按鈕,系統將自動完成所有工作
991
+
992
+ 5. **下載使用**:獲得完整的 .pptx 檔案,可直接在 PowerPoint 中使用
993
+
994
+ #### 🔄 改造現有簡報
995
+ 1. **上傳PPT文件**:支援 .pptx 和 .ppt 格式
996
+
997
+ 2. **選擇版型風格**:從8種專業版型中選擇適合的風格
998
+
999
+ 3. **選擇圖片風格**:選擇與內容匹配的圖片風格
1000
+
1001
+ 4. **開始分析改造**:AI將自動分析每頁內容並套用新設計
1002
+
1003
+ 5. **查看分析報告**:了解每頁的處理詳情和圖片添加情況
1004
+
1005
+ 6. **下載改造後簡報**:獲得全新設計的簡報文件
1006
+
1007
+ ### 💡 專業建議
1008
+ - **主題要具體**:「AI在醫療診斷的應用」比「人工智慧」效果更好
1009
+ - **選對風格**:商務場合用「professional」,創意展示用「creative」
1010
+ - **適當頁數**:建議 5-8 頁,內容豐富但不冗長
1011
+ - **測試 API**:第一次使用建議先測試 API 連接是否正常
1012
+
1013
+ ### 🔧 技術特點
1014
+ - **純 Python 實現**:不需要安裝 Microsoft Office
1015
+ - **即時生成**:通常 30-60 秒完成整個簡報
1016
+ - **高品質輸出**:生成的 .pptx 檔案完全相容 PowerPoint
1017
+ - **跨平台支援**:Windows、macOS、Linux 都能正常使用
1018
+ """)
1019
+
1020
+ # 事件綁定
1021
+ generate_btn.click(
1022
+ fn=generate_ppt_with_gemini,
1023
+ inputs=[
1024
+ gemini_api_input,
1025
+ pexels_api_input,
1026
+ topic_input,
1027
+ theme_dropdown,
1028
+ slide_count,
1029
+ image_style_dropdown
1030
+ ],
1031
+ outputs=[file_output, preview_output, status_output]
1032
+ )
1033
+
1034
+ analyze_btn.click(
1035
+ fn=analyze_and_restyle_ppt,
1036
+ inputs=[
1037
+ gemini_api_input,
1038
+ pexels_api_input,
1039
+ upload_file,
1040
+ upload_theme_dropdown,
1041
+ upload_image_style_dropdown
1042
+ ],
1043
+ outputs=[upload_file_output, analysis_report, upload_status_output]
1044
+ )
1045
+
1046
+ return iface
1047
+
1048
+ if __name__ == "__main__":
1049
+ # 啟動應用
1050
+ iface = create_gemini_interface()
1051
+ iface.launch(
1052
+ server_name="127.0.0.1",
1053
+ server_port=7860,
1054
+ share=False,
1055
+ inbrowser=True
1056
+ )
ppt_analyzer.py ADDED
@@ -0,0 +1,678 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ppt_analyzer.py
2
+ import os
3
+ import json
4
+ import tempfile
5
+ from io import BytesIO
6
+ from pptx import Presentation
7
+ from pptx.util import Inches, Pt
8
+ from pptx.enum.shapes import MSO_SHAPE_TYPE
9
+ from pptx.enum.text import PP_ALIGN
10
+ from pptx.dml.color import RGBColor
11
+ import google.generativeai as genai
12
+ from slide_themes import SlideThemeManager
13
+
14
+ class PPTAnalyzer:
15
+ def __init__(self, gemini_model=None, pexels_headers=None, image_styles=None):
16
+ self.gemini_model = gemini_model
17
+ self.pexels_headers = pexels_headers
18
+ self.theme_manager = SlideThemeManager()
19
+ self.image_styles = image_styles or {
20
+ "professional": "business professional corporate clean",
21
+ "creative": "creative artistic colorful vibrant",
22
+ "minimalist": "minimal clean simple white space",
23
+ "modern": "modern contemporary sleek design",
24
+ "natural": "natural outdoor organic environment",
25
+ "technology": "technology digital modern tech innovation"
26
+ }
27
+
28
+ def analyze_ppt_file(self, ppt_file_path):
29
+ """分析上傳的PPT文件"""
30
+ try:
31
+ prs = Presentation(ppt_file_path)
32
+ slides_info = []
33
+
34
+ for i, slide in enumerate(prs.slides):
35
+ slide_info = {
36
+ "slide_number": i + 1,
37
+ "title": "",
38
+ "content": [],
39
+ "has_table": False,
40
+ "has_chart": False,
41
+ "has_image": False,
42
+ "layout_type": slide.slide_layout.name if hasattr(slide.slide_layout, 'name') else "Unknown"
43
+ }
44
+
45
+ # 提取文字內容和檢測對象類型
46
+ for shape in slide.shapes:
47
+ # 檢測表格
48
+ if shape.shape_type == MSO_SHAPE_TYPE.TABLE:
49
+ slide_info["has_table"] = True
50
+
51
+ # 檢測圖表
52
+ elif shape.shape_type == MSO_SHAPE_TYPE.CHART:
53
+ slide_info["has_chart"] = True
54
+
55
+ # 檢測圖片
56
+ elif shape.shape_type == MSO_SHAPE_TYPE.PICTURE:
57
+ slide_info["has_image"] = True
58
+
59
+ # 提取文字內容
60
+ elif hasattr(shape, "text_frame") and shape.text_frame:
61
+ text_content = shape.text_frame.text.strip()
62
+ if text_content:
63
+ # 判斷是否為標題(通常是第一個有內容的文字框或字體較大)
64
+ if not slide_info["title"] and len(text_content) < 100:
65
+ slide_info["title"] = text_content
66
+ else:
67
+ # 分割多行內容
68
+ lines = [line.strip() for line in text_content.split('\n') if line.strip()]
69
+ slide_info["content"].extend(lines)
70
+
71
+ # 如果沒有找到標題,使用第一行內容作為標題
72
+ if not slide_info["title"] and slide_info["content"]:
73
+ slide_info["title"] = slide_info["content"].pop(0)
74
+
75
+ slides_info.append(slide_info)
76
+
77
+ return {
78
+ "total_slides": len(slides_info),
79
+ "slides": slides_info,
80
+ "original_size": {
81
+ "width": prs.slide_width,
82
+ "height": prs.slide_height
83
+ }
84
+ }
85
+
86
+ except Exception as e:
87
+ print(f"分析PPT文件錯誤: {e}")
88
+ return None
89
+
90
+ def generate_image_keywords_with_ai(self, slide_info):
91
+ """使用AI分析投影片內容生成圖片搜尋關鍵字"""
92
+ if not self.gemini_model:
93
+ print("Gemini模型不可用,使用回退關鍵字")
94
+ return self.generate_fallback_keywords(slide_info)
95
+
96
+ # 構建分析提示
97
+ title = slide_info.get("title", "")
98
+ content = slide_info.get("content", [])
99
+ content_text = " ".join(content[:3]) # 只取前3行內容避免太長
100
+
101
+ print(f"AI分析輸入 - 標題: {title}, 內容: {content_text}")
102
+
103
+ prompt = f"""
104
+ 請分析以下投影片內容,生成適合的英文圖片搜尋關鍵字:
105
+
106
+ 標題:{title}
107
+ 內容:{content_text}
108
+
109
+ 要求:
110
+ 1. 先理解中文內容的核心概念
111
+ 2. 將核心概念轉換為相應的英文關鍵字
112
+ 3. 生成3-5個英文關鍵字,用空格分隔
113
+ 4. 關鍵字要與內容主題相關,具體明確
114
+ 5. 避免過於抽象的詞彙
115
+ 6. 適合用於圖片搜尋
116
+ 7. 只回傳關鍵字,不要其他說明
117
+
118
+ 例如:
119
+ - 如果內容是關於"商業會議",回傳:business meeting office professional
120
+ - 如果內容是關於"技術創新",回傳:technology innovation digital development
121
+ - 如果內容是關於"數據分析",回傳:data analysis statistics chart
122
+ """
123
+
124
+ try:
125
+ response = self.gemini_model.generate_content(prompt)
126
+ keywords = response.text.strip()
127
+ print(f"AI生成的原始關鍵字: {keywords}")
128
+
129
+ # 清理回應,只保留英文字母和空格
130
+ keywords = ''.join(c if c.isalnum() or c.isspace() else ' ' for c in keywords)
131
+ keywords = ' '.join(keywords.split()) # 移除多餘空格
132
+
133
+ # 如果關鍵字太短或為空,使用回退方案
134
+ if len(keywords.strip()) < 3:
135
+ print("AI生成的關鍵字太短,使用回退方案")
136
+ return self.generate_fallback_keywords(slide_info)
137
+
138
+ final_keywords = keywords[:100] # 限制長度
139
+ print(f"最終關鍵字: {final_keywords}")
140
+ return final_keywords
141
+
142
+ except Exception as e:
143
+ print(f"AI分析錯誤: {e}")
144
+ return self.generate_fallback_keywords(slide_info)
145
+
146
+ def generate_fallback_keywords(self, slide_info):
147
+ """當AI不可用時的回退關鍵字生成"""
148
+ title = slide_info.get("title", "").lower()
149
+ content = " ".join(slide_info.get("content", [])).lower()
150
+
151
+ print(f"回退關鍵字生成 - 標題: {title}, 內容: {content[:100]}...")
152
+
153
+ # 基於關鍵詞映射生成搜尋詞(中英文混合)
154
+ keyword_mapping = {
155
+ # 英文關鍵字
156
+ "business": "business professional meeting",
157
+ "technology": "technology innovation digital",
158
+ "data": "data analysis statistics chart",
159
+ "marketing": "marketing strategy advertising",
160
+ "finance": "finance money investment",
161
+ "education": "education learning school",
162
+ "health": "health medical healthcare",
163
+ "environment": "environment nature green",
164
+ "team": "team collaboration teamwork",
165
+ "strategy": "strategy planning business",
166
+ "innovation": "innovation creative technology",
167
+ "growth": "growth success achievement",
168
+ "research": "research study academic",
169
+ "development": "development progress building",
170
+ "management": "management leadership office",
171
+ "analysis": "analysis review examination",
172
+ "solution": "solution problem solving",
173
+ "project": "project work planning",
174
+ "system": "system network infrastructure",
175
+ "process": "process workflow method",
176
+ "quality": "quality standard excellence",
177
+ "performance": "performance improvement results",
178
+ "customer": "customer service client",
179
+ "market": "market industry commercial",
180
+ "product": "product design manufacturing",
181
+ "service": "service support assistance",
182
+ # 中文關鍵字
183
+ "商業": "business professional meeting",
184
+ "企業": "business corporate company",
185
+ "科技": "technology innovation digital",
186
+ "技術": "technology digital development",
187
+ "數據": "data analysis statistics",
188
+ "資料": "data information analytics",
189
+ "分析": "analysis research examination",
190
+ "行銷": "marketing advertising strategy",
191
+ "市場": "market industry commercial",
192
+ "金融": "finance money investment",
193
+ "財務": "finance accounting money",
194
+ "教育": "education learning school",
195
+ "學習": "learning study education",
196
+ "健康": "health medical wellness",
197
+ "醫療": "medical healthcare health",
198
+ "環境": "environment nature sustainability",
199
+ "環保": "environment green sustainability",
200
+ "團隊": "team collaboration teamwork",
201
+ "策略": "strategy planning business",
202
+ "創新": "innovation creative development",
203
+ "成長": "growth success achievement",
204
+ "研究": "research study academic",
205
+ "開發": "development programming building",
206
+ "管理": "management leadership office",
207
+ "解決": "solution problem solving",
208
+ "專案": "project work planning",
209
+ "系統": "system network infrastructure",
210
+ "流程": "process workflow method",
211
+ "品質": "quality standard excellence",
212
+ "效能": "performance improvement results",
213
+ "客戶": "customer service client",
214
+ "產品": "product design manufacturing",
215
+ "服務": "service support assistance",
216
+ "會議": "meeting conference business",
217
+ "報告": "report presentation business",
218
+ "簡報": "presentation business professional"
219
+ }
220
+
221
+ found_keywords = []
222
+ text_to_search = f"{title} {content}"
223
+
224
+ for key, value in keyword_mapping.items():
225
+ if key in text_to_search:
226
+ found_keywords.append(value)
227
+ print(f"找到關鍵字映射: {key} -> {value}")
228
+
229
+ if found_keywords:
230
+ result = " ".join(found_keywords[:2]) # 最多使用2組關鍵字
231
+ else:
232
+ result = "business presentation professional meeting"
233
+
234
+ print(f"回退關鍵字結果: {result}")
235
+ return result
236
+
237
+ def apply_theme_to_presentation(self, original_ppt_path, theme_name, image_style, analysis_result):
238
+ """將主題套用到現有簡報"""
239
+ try:
240
+ # 載入原始簡報
241
+ prs = Presentation(original_ppt_path)
242
+ theme = self.theme_manager.get_theme(theme_name)
243
+
244
+ # 設定新的16:9尺寸
245
+ prs.slide_width = self.theme_manager.slide_width
246
+ prs.slide_height = self.theme_manager.slide_height
247
+
248
+ processed_slides = []
249
+
250
+ for i, slide_info in enumerate(analysis_result["slides"]):
251
+ if i >= len(prs.slides):
252
+ break
253
+
254
+ slide = prs.slides[i]
255
+
256
+ # 應用背景和裝飾
257
+ self.theme_manager.setup_slide_background_and_layout(slide, theme)
258
+
259
+ # 重新格式化所有文字
260
+ self.reformat_slide_text(slide, theme)
261
+
262
+ # 決定是否添加圖片
263
+ should_add_image = not (slide_info["has_table"] or slide_info["has_chart"])
264
+
265
+ if should_add_image and self.pexels_headers:
266
+ # 生成圖片搜尋關鍵字
267
+ keywords = self.generate_image_keywords_with_ai(slide_info)
268
+
269
+ # 搜尋和添加圖片
270
+ image_added = self.add_image_to_existing_slide(slide, keywords, image_style, theme)
271
+ slide_info["image_added"] = image_added
272
+ slide_info["search_keywords"] = keywords
273
+ else:
274
+ slide_info["image_added"] = False
275
+ slide_info["skip_reason"] = "含有表格或圖表"
276
+
277
+ processed_slides.append(slide_info)
278
+
279
+ return prs, processed_slides
280
+
281
+ except Exception as e:
282
+ print(f"套用主題錯誤: {e}")
283
+ return None, []
284
+
285
+ def reformat_slide_text(self, slide, theme):
286
+ """重新格式化投影片中的所有文字"""
287
+ try:
288
+ for shape in slide.shapes:
289
+ if hasattr(shape, "text_frame") and shape.text_frame:
290
+ # 判斷是否為標題(通常在上方且文字較少)
291
+ is_title = (shape.top < Inches(2) and
292
+ len(shape.text_frame.text) < 100 and
293
+ shape.text_frame.text.strip())
294
+
295
+ for paragraph in shape.text_frame.paragraphs:
296
+ if paragraph.text.strip():
297
+ if is_title:
298
+ # 格式化為標題
299
+ paragraph.font.name = self.theme_manager.get_font_name()
300
+ paragraph.font.size = Pt(36)
301
+ paragraph.font.color.rgb = theme["title_color"]
302
+ paragraph.font.bold = True
303
+ paragraph.alignment = PP_ALIGN.LEFT
304
+ else:
305
+ # 格式化為內容
306
+ paragraph.font.name = self.theme_manager.get_font_name()
307
+ paragraph.font.size = Pt(24)
308
+ paragraph.font.color.rgb = theme["text_color"]
309
+ paragraph.space_before = Pt(8)
310
+ paragraph.space_after = Pt(8)
311
+ paragraph.line_spacing = 1.3
312
+ except Exception as e:
313
+ print(f"重新格式化文字錯誤: {e}")
314
+
315
+ def add_image_to_existing_slide(self, slide, keywords, image_style, theme):
316
+ """為現有投影片添加圖片"""
317
+ try:
318
+ print(f"開始為投影片添加圖片,關鍵字: {keywords}")
319
+
320
+ # 搜尋圖片
321
+ photos = self.search_pexels_with_style(keywords, image_style)
322
+ if not photos:
323
+ print(f"未找到相關圖片,關鍵字: {keywords}")
324
+ return False
325
+
326
+ print(f"找到 {len(photos)} 張圖片")
327
+
328
+ # 選擇最佳圖片
329
+ image_url = self.select_best_image(photos)
330
+ if not image_url:
331
+ print("無法選擇最佳圖片")
332
+ return False
333
+
334
+ print(f"選中圖片URL: {image_url}")
335
+
336
+ # 下載圖片
337
+ image_path = self.download_image(image_url)
338
+ if not image_path:
339
+ print("圖片下載失敗")
340
+ return False
341
+
342
+ print(f"圖片下載成功: {image_path}")
343
+
344
+ # 計算可用空間並添加圖片
345
+ available_area = self.calculate_available_space(slide)
346
+ if available_area:
347
+ print(f"找到可用空間: {available_area}")
348
+ self.add_image_to_available_space(slide, image_path, available_area)
349
+ print("圖片添加成功")
350
+ return True
351
+ else:
352
+ print("未找到可用空間,嘗試背景圖片模式")
353
+ # 如果找不到理想空間,將圖片作為背景放置,但在文字底下
354
+ self.add_background_image_to_slide(slide, image_path)
355
+ return True
356
+
357
+ except Exception as e:
358
+ print(f"添加圖片錯誤: {e}")
359
+ import traceback
360
+ print(f"詳細錯誤: {traceback.format_exc()}")
361
+ return False
362
+
363
+ def calculate_available_space(self, slide):
364
+ """計算投影片中的可用空間"""
365
+ try:
366
+ slide_width = self.theme_manager.slide_width.inches
367
+ slide_height = self.theme_manager.slide_height.inches
368
+
369
+ print(f"投影片尺寸: {slide_width} x {slide_height}")
370
+
371
+ # 收集所有現有形狀的位置
372
+ occupied_areas = []
373
+ shape_count = 0
374
+ for shape in slide.shapes:
375
+ if hasattr(shape, 'left') and hasattr(shape, 'top'):
376
+ area = {
377
+ 'left': shape.left.inches,
378
+ 'top': shape.top.inches,
379
+ 'right': shape.left.inches + shape.width.inches,
380
+ 'bottom': shape.top.inches + shape.height.inches
381
+ }
382
+ occupied_areas.append(area)
383
+ shape_count += 1
384
+ print(f"形狀 {shape_count}: {area}")
385
+
386
+ # 定義可能的圖片位置區域(更大的尺寸)
387
+ possible_areas = [
388
+ # 右側區域 - 更大
389
+ {'left': slide_width * 0.5, 'top': slide_height * 0.1,
390
+ 'width': slide_width * 0.45, 'height': slide_height * 0.8},
391
+ # 下方區域 - 更大
392
+ {'left': slide_width * 0.05, 'top': slide_height * 0.55,
393
+ 'width': slide_width * 0.9, 'height': slide_height * 0.4},
394
+ # 左側區域 - 更大
395
+ {'left': slide_width * 0.05, 'top': slide_height * 0.1,
396
+ 'width': slide_width * 0.45, 'height': slide_height * 0.8},
397
+ # 中央下方區域 - 更大
398
+ {'left': slide_width * 0.2, 'top': slide_height * 0.65,
399
+ 'width': slide_width * 0.6, 'height': slide_height * 0.3},
400
+ # 右上區域 - 更大
401
+ {'left': slide_width * 0.6, 'top': slide_height * 0.05,
402
+ 'width': slide_width * 0.35, 'height': slide_height * 0.5}
403
+ ]
404
+
405
+ print(f"檢查 {len(possible_areas)} 個可能區域")
406
+
407
+ # 找到最大的可用區域
408
+ for i, area in enumerate(possible_areas):
409
+ print(f"檢查區域 {i+1}: {area}")
410
+ if self.is_area_available(area, occupied_areas):
411
+ print(f"區域 {i+1} 可用")
412
+ return area
413
+ else:
414
+ print(f"區域 {i+1} 被占用")
415
+
416
+ print("所有預定義區域都被占用")
417
+ return None
418
+
419
+ except Exception as e:
420
+ print(f"計算可用空間錯誤: {e}")
421
+ import traceback
422
+ print(f"詳細錯誤: {traceback.format_exc()}")
423
+ return None
424
+
425
+ def add_background_image_to_slide(self, slide, image_path):
426
+ """將圖片作為背景添加到投影片,確保在文字底下"""
427
+ try:
428
+ print(f"添加背景圖片: {image_path}")
429
+
430
+ # 計算較大的圖片尺寸,覆蓋更多區域
431
+ slide_width = self.theme_manager.slide_width.inches
432
+ slide_height = self.theme_manager.slide_height.inches
433
+
434
+ # 使用更大的圖片��寸,稍微偏移以不完全覆蓋標題
435
+ img_left = slide_width * 0.1 # 10% 邊距
436
+ img_top = slide_height * 0.2 # 20% 邊距,避開標題
437
+ img_width = slide_width * 0.8 # 80% 寬度
438
+ img_height = slide_height * 0.7 # 70% 高度
439
+
440
+ # 計算圖片比例並調整尺寸
441
+ from PIL import Image as PILImage
442
+ with PILImage.open(image_path) as img:
443
+ img_width_px, img_height_px = img.size
444
+ img_ratio = img_width_px / img_height_px
445
+
446
+ # 調整尺寸以保持比例
447
+ if img_ratio > (img_width / img_height):
448
+ # 圖片較寬,以寬度為準
449
+ actual_width = img_width
450
+ actual_height = img_width / img_ratio
451
+ actual_top = img_top + (img_height - actual_height) / 2
452
+ actual_left = img_left
453
+ else:
454
+ # 圖片較高,以高度為準
455
+ actual_height = img_height
456
+ actual_width = img_height * img_ratio
457
+ actual_left = img_left + (img_width - actual_width) / 2
458
+ actual_top = img_top
459
+
460
+ print(f"背景圖片尺寸: left={actual_left:.2f}, top={actual_top:.2f}, width={actual_width:.2f}, height={actual_height:.2f}")
461
+
462
+ # 添加圖片
463
+ picture = slide.shapes.add_picture(
464
+ image_path,
465
+ Inches(actual_left),
466
+ Inches(actual_top),
467
+ Inches(actual_width),
468
+ Inches(actual_height)
469
+ )
470
+
471
+ # 將圖片移到最底層(在所有文字和形狀之下)
472
+ picture.element.getparent().remove(picture.element)
473
+ slide.shapes._spTree.insert(2, picture.element)
474
+
475
+ print("背景圖片添加成功並移至底層")
476
+
477
+ except Exception as e:
478
+ print(f"添加背景圖片錯誤: {e}")
479
+ import traceback
480
+ print(f"詳細錯誤: {traceback.format_exc()}")
481
+
482
+ def is_area_available(self, area, occupied_areas):
483
+ """檢查區域是否可用(允許少量重疊)"""
484
+ area_right = area['left'] + area['width']
485
+ area_bottom = area['top'] + area['height']
486
+
487
+ # 計算重疊程度的閾值(允許10%的重疊)
488
+ overlap_threshold = 0.1
489
+
490
+ for occupied in occupied_areas:
491
+ # 計算重疊區域
492
+ overlap_left = max(area['left'], occupied['left'])
493
+ overlap_top = max(area['top'], occupied['top'])
494
+ overlap_right = min(area_right, occupied['right'])
495
+ overlap_bottom = min(area_bottom, occupied['bottom'])
496
+
497
+ # 如果有重疊
498
+ if overlap_left < overlap_right and overlap_top < overlap_bottom:
499
+ overlap_width = overlap_right - overlap_left
500
+ overlap_height = overlap_bottom - overlap_top
501
+ overlap_area = overlap_width * overlap_height
502
+
503
+ # 計算相對於目標區域的重疊比例
504
+ target_area = area['width'] * area['height']
505
+ overlap_ratio = overlap_area / target_area
506
+
507
+ print(f"重疊比例: {overlap_ratio:.2f}")
508
+
509
+ # 如果重疊超過閾值,則認為不可用
510
+ if overlap_ratio > overlap_threshold:
511
+ return False
512
+
513
+ return True
514
+
515
+ def add_image_to_available_space(self, slide, image_path, area):
516
+ """在可用空間添加圖片"""
517
+ try:
518
+ print(f"準備在區域添加圖片: {area}")
519
+ print(f"圖片路徑: {image_path}")
520
+
521
+ left = Inches(area['left'])
522
+ top = Inches(area['top'])
523
+ width = Inches(area['width'])
524
+ height = Inches(area['height'])
525
+
526
+ print(f"目標位置: left={left.inches}, top={top.inches}, width={width.inches}, height={height.inches}")
527
+
528
+ # 計算圖片比例並調整尺寸
529
+ from PIL import Image as PILImage
530
+ with PILImage.open(image_path) as img:
531
+ img_width, img_height = img.size
532
+ img_ratio = img_width / img_height
533
+ area_ratio = area['width'] / area['height']
534
+
535
+ print(f"圖片原始尺寸: {img_width} x {img_height}, 比例: {img_ratio:.2f}")
536
+ print(f"目標區域比例: {area_ratio:.2f}")
537
+
538
+ if img_ratio > area_ratio:
539
+ # 圖片較寬,以寬度為準
540
+ actual_width = width
541
+ actual_height = Inches(width.inches / img_ratio)
542
+ actual_top = Inches(top.inches + (height.inches - actual_height.inches) / 2)
543
+ actual_left = left
544
+ else:
545
+ # 圖片較高,以高度為準
546
+ actual_height = height
547
+ actual_width = Inches(height.inches * img_ratio)
548
+ actual_left = Inches(left.inches + (width.inches - actual_width.inches) / 2)
549
+ actual_top = top
550
+
551
+ print(f"最終尺寸: left={actual_left.inches:.2f}, top={actual_top.inches:.2f}, width={actual_width.inches:.2f}, height={actual_height.inches:.2f}")
552
+
553
+ # 添加圖片
554
+ picture = slide.shapes.add_picture(image_path, actual_left, actual_top,
555
+ actual_width, actual_height)
556
+ print(f"圖片添加成功,picture對象: {picture}")
557
+
558
+ except Exception as e:
559
+ print(f"在可用空間添加圖片錯誤: {e}")
560
+ import traceback
561
+ print(f"詳細錯誤: {traceback.format_exc()}")
562
+
563
+ def search_pexels_with_style(self, keywords, image_style, per_page=10):
564
+ """搜尋Pexels圖片"""
565
+ if not self.pexels_headers:
566
+ return None
567
+
568
+ import requests
569
+
570
+ # 組合關鍵字
571
+ style_modifier = self.image_styles.get(image_style, "")
572
+ enhanced_keywords = f"{keywords} {style_modifier}"
573
+
574
+ url = "https://api.pexels.com/v1/search"
575
+ params = {
576
+ "query": enhanced_keywords,
577
+ "per_page": per_page,
578
+ "orientation": "landscape",
579
+ "size": "medium"
580
+ }
581
+
582
+ try:
583
+ response = requests.get(url, headers=self.pexels_headers, params=params)
584
+ if response.status_code == 200:
585
+ data = response.json()
586
+ return data["photos"] if data["photos"] else None
587
+ return None
588
+ except Exception as e:
589
+ print(f"Pexels API 錯誤: {e}")
590
+ return None
591
+
592
+ def select_best_image(self, photos):
593
+ """選擇最佳圖片"""
594
+ if not photos:
595
+ return None
596
+
597
+ # 選擇解析度較高的圖片
598
+ best_photo = photos[0]
599
+ for photo in photos[:3]:
600
+ if photo["width"] * photo["height"] > best_photo["width"] * best_photo["height"]:
601
+ best_photo = photo
602
+
603
+ return best_photo["src"]["medium"]
604
+
605
+ def download_image(self, image_url):
606
+ """下載圖片"""
607
+ if not image_url:
608
+ return None
609
+
610
+ import requests
611
+ from PIL import Image
612
+
613
+ try:
614
+ response = requests.get(image_url)
615
+ if response.status_code == 200:
616
+ temp_dir = tempfile.mkdtemp()
617
+ image_path = os.path.join(temp_dir, "slide_image.jpg")
618
+
619
+ # 處理圖片
620
+ image = Image.open(BytesIO(response.content))
621
+
622
+ # 調整圖片大小
623
+ max_size = (800, 600)
624
+ image.thumbnail(max_size, Image.Resampling.LANCZOS)
625
+
626
+ # 轉換並儲存
627
+ if image.mode in ("RGBA", "P"):
628
+ image = image.convert("RGB")
629
+ image.save(image_path, "JPEG", quality=85)
630
+
631
+ return image_path
632
+ return None
633
+ except Exception as e:
634
+ print(f"圖片下載錯誤: {e}")
635
+ return None
636
+
637
+ def save_processed_presentation(self, prs, filename):
638
+ """儲存處理後的簡報"""
639
+ try:
640
+ temp_dir = tempfile.mkdtemp()
641
+ filepath = os.path.join(temp_dir, filename)
642
+ prs.save(filepath)
643
+ return filepath
644
+ except Exception as e:
645
+ print(f"儲存簡報錯誤: {e}")
646
+ return None
647
+
648
+ def generate_analysis_report(self, analysis_result, processed_slides):
649
+ """生成分析報告"""
650
+ report = f"📊 簡報分析報告\n"
651
+ report += f"總投影片數:{analysis_result['total_slides']}\n\n"
652
+
653
+ for i, slide_info in enumerate(processed_slides, 1):
654
+ report += f"{i}. {slide_info.get('title', f'投影片 {i}')}\n"
655
+
656
+ # 內容類型
657
+ content_types = []
658
+ if slide_info.get('has_table'):
659
+ content_types.append("表格")
660
+ if slide_info.get('has_chart'):
661
+ content_types.append("圖表")
662
+ if slide_info.get('has_image'):
663
+ content_types.append("原有圖片")
664
+
665
+ if content_types:
666
+ report += f" 包含:{', '.join(content_types)}\n"
667
+
668
+ # 圖片處���結果
669
+ if slide_info.get('image_added'):
670
+ report += f" ✅ 已添加圖片 (關鍵字: {slide_info.get('search_keywords', 'N/A')})\n"
671
+ elif slide_info.get('skip_reason'):
672
+ report += f" ⏭️ 跳過添加圖片 ({slide_info['skip_reason']})\n"
673
+ else:
674
+ report += f" ❌ 未能添加圖片\n"
675
+
676
+ report += "\n"
677
+
678
+ return report
setup_and_run.bat ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @echo off
2
+ echo =====================================
3
+ echo PPT Creator 本地環境設置與啟動腳本
4
+ echo =====================================
5
+ echo.
6
+
7
+ REM 檢查Python是否安裝
8
+ python --version >nul 2>&1
9
+ if %errorlevel% neq 0 (
10
+ echo 錯誤: 未找到Python安裝,請先安裝Python 3.8+
11
+ echo 下載地址: https://www.python.org/downloads/
12
+ pause
13
+ exit /b 1
14
+ )
15
+
16
+ echo ✓ Python已安裝
17
+ echo.
18
+
19
+ REM 創建虛擬環境(如果不存在)
20
+ if not exist "venv" (
21
+ echo 正在創建Python虛擬環境...
22
+ python -m venv venv
23
+ if %errorlevel% neq 0 (
24
+ echo 錯誤: 無法創建虛擬環境
25
+ pause
26
+ exit /b 1
27
+ )
28
+ echo ✓ 虛擬環境創建完成
29
+ ) else (
30
+ echo ✓ 虛擬環境已存在
31
+ )
32
+
33
+ echo.
34
+
35
+ REM 激活虛擬環境
36
+ echo 正在激活虛擬環境...
37
+ call venv\Scripts\activate.bat
38
+ if %errorlevel% neq 0 (
39
+ echo 錯誤: 無法激活虛擬環境
40
+ pause
41
+ exit /b 1
42
+ )
43
+
44
+ echo ✓ 虛擬環境已激活
45
+ echo.
46
+
47
+ REM 升級pip
48
+ echo 正在升級pip...
49
+ python -m pip install --upgrade pip
50
+
51
+ REM 安裝依賴套件
52
+ echo 正在安裝依賴套件...
53
+ pip install -r requirements.txt
54
+ if %errorlevel% neq 0 (
55
+ echo 錯誤: 安裝依賴套件失敗
56
+ pause
57
+ exit /b 1
58
+ )
59
+
60
+ echo ✓ 所有依賴套件安裝完成
61
+ echo.
62
+
63
+ REM 啟動應用
64
+ echo 正在啟動PPT Creator應用...
65
+ echo 請在瀏覽器中打開 http://localhost:7860
66
+ echo 按 Ctrl+C 可停止應用
67
+ echo.
68
+ python localapp.py
69
+
70
+ pause
slide_themes.py ADDED
@@ -0,0 +1,321 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # slide_themes.py
2
+ from pptx.util import Inches, Pt
3
+ from pptx.dml.color import RGBColor
4
+ from pptx.enum.text import PP_ALIGN
5
+
6
+ class SlideThemeManager:
7
+ def __init__(self):
8
+ # 16:9 簡報尺寸 (單位:英吋)
9
+ self.slide_width = Inches(13.333) # 16:9 寬度
10
+ self.slide_height = Inches(7.5) # 16:9 高度
11
+
12
+ # 版型配置 - 重新設計,減少留白,增加多樣化配色,改善文字對比度
13
+ self.themes = {
14
+ "商務專業": {
15
+ "bg_color": RGBColor(248, 252, 255), # 更淺的藍白背景
16
+ "title_color": RGBColor(8, 47, 91), # 更深的藍色標題
17
+ "text_color": RGBColor(33, 37, 41), # 深黑文字,提高對比度
18
+ "accent_color": RGBColor(52, 144, 220), # 藍色強調
19
+ "secondary_color": RGBColor(255, 255, 255), # 純白輔助背景
20
+ "content_bg_color": RGBColor(255, 255, 255), # 內容框背景色
21
+ "layout": "image_right",
22
+ "has_gradient": True,
23
+ "gradient_start": RGBColor(248, 252, 255),
24
+ "gradient_end": RGBColor(235, 245, 255),
25
+ # 縮小邊距,擴大內容區域
26
+ "title_area": {"left": 0.2, "top": 0.1, "width": 12.9, "height": 1.0},
27
+ "content_area": {"left": 0.2, "top": 1.3, "width": 6.8, "height": 5.9},
28
+ "image_area": {"left": 7.2, "top": 1.3, "width": 5.9, "height": 4.8}
29
+ },
30
+ "科技創新": {
31
+ "bg_color": RGBColor(18, 28, 42), # 深藍黑背景
32
+ "title_color": RGBColor(120, 255, 235), # 更亮的青綠標題
33
+ "text_color": RGBColor(245, 248, 252), # 更亮的淺色文字
34
+ "accent_color": RGBColor(255, 107, 107), # 紅色強調
35
+ "secondary_color": RGBColor(35, 47, 62), # 深灰輔助
36
+ "content_bg_color": RGBColor(35, 47, 62), # 內容框背景色
37
+ "layout": "image_bottom",
38
+ "has_gradient": True,
39
+ "gradient_start": RGBColor(18, 28, 42),
40
+ "gradient_end": RGBColor(35, 47, 62),
41
+ "title_area": {"left": 0.2, "top": 0.1, "width": 12.9, "height": 1.0},
42
+ "content_area": {"left": 0.2, "top": 1.3, "width": 12.9, "height": 3.2},
43
+ "image_area": {"left": 0.2, "top": 4.7, "width": 12.9, "height": 2.6}
44
+ },
45
+ "創意設計": {
46
+ "bg_color": RGBColor(255, 245, 250), # 更淺的粉色背景
47
+ "title_color": RGBColor(136, 14, 79), # 更深的紫色標題
48
+ "text_color": RGBColor(33, 33, 33), # 深黑文字,提高對比度
49
+ "accent_color": RGBColor(255, 152, 0), # 橙色強調
50
+ "secondary_color": RGBColor(255, 255, 255), # 純白輔助
51
+ "content_bg_color": RGBColor(255, 255, 255), # 內容框背景色
52
+ "layout": "image_left",
53
+ "has_gradient": True,
54
+ "gradient_start": RGBColor(255, 245, 250),
55
+ "gradient_end": RGBColor(250, 224, 235),
56
+ "title_area": {"left": 0.2, "top": 0.1, "width": 12.9, "height": 1.0},
57
+ "content_area": {"left": 6.0, "top": 1.3, "width": 7.1, "height": 5.9},
58
+ "image_area": {"left": 0.2, "top": 1.3, "width": 5.6, "height": 4.8}
59
+ },
60
+ "教育學術": {
61
+ "bg_color": RGBColor(252, 255, 252), # 更淺的綠白背景
62
+ "title_color": RGBColor(46, 125, 50), # 更深的綠色標題
63
+ "text_color": RGBColor(33, 37, 41), # 深黑文字
64
+ "accent_color": RGBColor(255, 193, 7), # 黃色強調
65
+ "secondary_color": RGBColor(255, 255, 255), # 純白輔助
66
+ "content_bg_color": RGBColor(255, 255, 255), # 內容框背景色
67
+ "layout": "image_top",
68
+ "has_gradient": True,
69
+ "gradient_start": RGBColor(252, 255, 252),
70
+ "gradient_end": RGBColor(240, 248, 240),
71
+ "title_area": {"left": 0.2, "top": 0.1, "width": 12.9, "height": 1.0},
72
+ "content_area": {"left": 0.2, "top": 4.0, "width": 12.9, "height": 3.3},
73
+ "image_area": {"left": 0.2, "top": 1.3, "width": 12.9, "height": 2.5}
74
+ },
75
+ "現代簡約": {
76
+ "bg_color": RGBColor(255, 255, 255), # 純白背景
77
+ "title_color": RGBColor(33, 37, 41), # 深黑標題
78
+ "text_color": RGBColor(33, 37, 41), # 深黑文字,提高對比度
79
+ "accent_color": RGBColor(0, 123, 255), # 藍色強調
80
+ "secondary_color": RGBColor(248, 249, 250), # 淺灰輔助
81
+ "content_bg_color": RGBColor(248, 249, 250), # 內容框背景色
82
+ "layout": "image_right",
83
+ "has_gradient": False,
84
+ "title_area": {"left": 0.2, "top": 0.1, "width": 12.9, "height": 1.0},
85
+ "content_area": {"left": 0.2, "top": 1.3, "width": 6.8, "height": 5.9},
86
+ "image_area": {"left": 7.2, "top": 1.3, "width": 5.9, "height": 4.8}
87
+ },
88
+ "溫暖橙色": {
89
+ "bg_color": RGBColor(255, 252, 245), # 更淺的橙白背景
90
+ "title_color": RGBColor(191, 54, 12), # 更深的橙色標題
91
+ "text_color": RGBColor(33, 37, 41), # 深黑文字,提高對比度
92
+ "accent_color": RGBColor(255, 138, 101), # 淺橙強調
93
+ "secondary_color": RGBColor(255, 255, 255), # 純白輔助
94
+ "content_bg_color": RGBColor(255, 255, 255), # 內容框背景色
95
+ "layout": "image_bottom",
96
+ "has_gradient": True,
97
+ "gradient_start": RGBColor(255, 252, 245),
98
+ "gradient_end": RGBColor(255, 243, 224),
99
+ "title_area": {"left": 0.2, "top": 0.1, "width": 12.9, "height": 1.0},
100
+ "content_area": {"left": 0.2, "top": 1.3, "width": 12.9, "height": 3.2},
101
+ "image_area": {"left": 0.2, "top": 4.7, "width": 12.9, "height": 2.6}
102
+ },
103
+ "深邃紫色": {
104
+ "bg_color": RGBColor(67, 18, 125), # 深紫背景
105
+ "title_color": RGBColor(224, 164, 234), # 更亮的淺紫標題
106
+ "text_color": RGBColor(255, 255, 255), # 純白文字
107
+ "accent_color": RGBColor(255, 204, 0), # 金黃強調
108
+ "secondary_color": RGBColor(98, 26, 142), # 中紫輔助
109
+ "content_bg_color": RGBColor(98, 26, 142), # 內容框背景色
110
+ "layout": "image_left",
111
+ "has_gradient": True,
112
+ "gradient_start": RGBColor(67, 18, 125),
113
+ "gradient_end": RGBColor(118, 30, 152),
114
+ "title_area": {"left": 0.2, "top": 0.1, "width": 12.9, "height": 1.0},
115
+ "content_area": {"left": 6.0, "top": 1.3, "width": 7.1, "height": 5.9},
116
+ "image_area": {"left": 0.2, "top": 1.3, "width": 5.6, "height": 4.8}
117
+ },
118
+ "清新藍綠": {
119
+ "bg_color": RGBColor(240, 255, 252), # 更淺的藍綠背景
120
+ "title_color": RGBColor(0, 105, 92), # 更深的藍綠標題
121
+ "text_color": RGBColor(33, 37, 41), # 深黑文字
122
+ "accent_color": RGBColor(255, 111, 97), # 珊瑚紅強調
123
+ "secondary_color": RGBColor(255, 255, 255), # 純白輔助
124
+ "content_bg_color": RGBColor(255, 255, 255), # 內容框背景色
125
+ "layout": "image_top",
126
+ "has_gradient": True,
127
+ "gradient_start": RGBColor(240, 255, 252),
128
+ "gradient_end": RGBColor(224, 242, 235),
129
+ "title_area": {"left": 0.2, "top": 0.1, "width": 12.9, "height": 1.0},
130
+ "content_area": {"left": 0.2, "top": 4.0, "width": 12.9, "height": 3.3},
131
+ "image_area": {"left": 0.2, "top": 1.3, "width": 12.9, "height": 2.5}
132
+ }
133
+ }
134
+
135
+ # 圖片風格
136
+ self.image_styles = {
137
+ "professional": "business professional corporate clean",
138
+ "creative": "creative artistic colorful vibrant",
139
+ "minimalist": "minimal clean simple white space",
140
+ "modern": "modern contemporary sleek design",
141
+ "natural": "natural outdoor organic environment",
142
+ "technology": "technology digital modern tech innovation"
143
+ }
144
+
145
+ def get_theme(self, theme_name):
146
+ """獲取指定主題"""
147
+ return self.themes.get(theme_name, self.themes["商務專業"])
148
+
149
+ def get_all_theme_names(self):
150
+ """獲取所有主題名稱"""
151
+ return list(self.themes.keys())
152
+
153
+ def apply_background(self, slide, theme):
154
+ """應用背景色彩或漸變"""
155
+ try:
156
+ # 獲取投影片背景
157
+ background = slide.background
158
+ fill = background.fill
159
+
160
+ if theme.get("has_gradient", False):
161
+ # 應用漸變背景
162
+ fill.gradient()
163
+ fill.gradient_angle = 45.0 # 45度角漸變
164
+
165
+ # 設置漸變色彩停靠點
166
+ gradient_stops = fill.gradient_stops
167
+ gradient_stops[0].color.rgb = theme["gradient_start"]
168
+ gradient_stops[1].color.rgb = theme["gradient_end"]
169
+ else:
170
+ # 應用純色背景
171
+ fill.solid()
172
+ fill.fore_color.rgb = theme["bg_color"]
173
+ except Exception as e:
174
+ print(f"應用背景錯誤: {e}")
175
+
176
+ def add_decorative_elements(self, slide, theme):
177
+ """添加裝飾性元素(邊框、圖形等)"""
178
+ try:
179
+ # 根據主題��加裝飾線條或圖形
180
+ if theme.get("layout") == "image_right":
181
+ # 在左側內容區域添加垂直裝飾線
182
+ line = slide.shapes.add_connector(
183
+ connector_type=1, # 直線
184
+ begin_x=Inches(0.1),
185
+ begin_y=Inches(1.3),
186
+ end_x=Inches(0.1),
187
+ end_y=Inches(7.2)
188
+ )
189
+ line.line.color.rgb = theme["accent_color"]
190
+ line.line.width = Pt(4)
191
+
192
+ elif theme.get("layout") == "image_bottom":
193
+ # 在標題下方添加水平裝飾線
194
+ line = slide.shapes.add_connector(
195
+ connector_type=1, # 直線
196
+ begin_x=Inches(0.2),
197
+ begin_y=Inches(1.2),
198
+ end_x=Inches(13.1),
199
+ end_y=Inches(1.2)
200
+ )
201
+ line.line.color.rgb = theme["accent_color"]
202
+ line.line.width = Pt(3)
203
+
204
+ except Exception as e:
205
+ print(f"添加裝飾元素錯誤: {e}")
206
+
207
+ def format_title(self, shape, theme, font_size, font_getter=None):
208
+ """格式化標題"""
209
+ try:
210
+ paragraph = shape.text_frame.paragraphs[0]
211
+ if font_getter:
212
+ paragraph.font.name = font_getter()
213
+ else:
214
+ paragraph.font.name = self.get_font_name()
215
+ paragraph.font.size = Pt(font_size)
216
+ paragraph.font.color.rgb = theme["title_color"]
217
+ paragraph.alignment = PP_ALIGN.LEFT
218
+ paragraph.font.bold = True
219
+
220
+ # 為深色背景的主題添加陰影效果
221
+ if self.is_dark_background(theme):
222
+ try:
223
+ # 添加文字陰影增強可讀性
224
+ paragraph.font.color.rgb = RGBColor(255, 255, 255)
225
+ except:
226
+ pass
227
+ except Exception as e:
228
+ print(f"格式化標題錯誤: {e}")
229
+
230
+ def format_content(self, paragraph, theme, font_size, font_getter=None):
231
+ """格式化內容"""
232
+ try:
233
+ if font_getter:
234
+ paragraph.font.name = font_getter()
235
+ else:
236
+ paragraph.font.name = self.get_font_name()
237
+ paragraph.font.size = Pt(font_size)
238
+ paragraph.font.color.rgb = theme["text_color"]
239
+ paragraph.space_before = Pt(6)
240
+ paragraph.space_after = Pt(6)
241
+ paragraph.line_spacing = 1.2
242
+ except Exception as e:
243
+ print(f"格式化內容錯誤: {e}")
244
+
245
+ def get_font_name(self):
246
+ """獲取中文字型名稱"""
247
+ import os
248
+ # 檢查是否有自定義中文字型檔案
249
+ font_path = os.path.join(os.path.dirname(__file__), "cht.ttf")
250
+ if os.path.exists(font_path):
251
+ return "cht"
252
+ else:
253
+ return "Arial Unicode MS"
254
+
255
+ def is_dark_background(self, theme):
256
+ """判斷是否為深色背景"""
257
+ bg_color = theme["bg_color"]
258
+ # 計算亮度(簡單的RGB平均值判斷)
259
+ # RGBColor 物件沒有 .r 屬性,需要使用 ._color_val 或直接從 RGB 值計算
260
+ try:
261
+ # 嘗試獲取 RGB 值
262
+ r = bg_color._color_val & 0xFF
263
+ g = (bg_color._color_val >> 8) & 0xFF
264
+ b = (bg_color._color_val >> 16) & 0xFF
265
+ brightness = (r + g + b) / 3
266
+ return brightness < 128
267
+ except:
268
+ # 如果無法獲取,使用保守判斷
269
+ # 檢查是否為已知的深色主題
270
+ dark_themes = ["科技創新", "深邃紫色"]
271
+ theme_name = getattr(theme, 'name', '')
272
+ return theme_name in dark_themes
273
+
274
+ def setup_slide_background_and_layout(self, slide, theme):
275
+ """設置投影片背景和基本布局"""
276
+ # 應用背景
277
+ self.apply_background(slide, theme)
278
+
279
+ # 添加裝飾元素
280
+ self.add_decorative_elements(slide, theme)
281
+
282
+ def create_content_box_with_background(self, slide, theme, content_area):
283
+ """創建帶背景的內容框"""
284
+ try:
285
+ # 在內容區域添加背景框
286
+ bg_shape = slide.shapes.add_shape(
287
+ 1, # 矩形
288
+ Inches(content_area["left"] - 0.1),
289
+ Inches(content_area["top"] - 0.1),
290
+ Inches(content_area["width"] + 0.2),
291
+ Inches(content_area["height"] + 0.2)
292
+ )
293
+
294
+ # 設置背景框樣式
295
+ fill = bg_shape.fill
296
+ fill.solid()
297
+
298
+ # 使用主題中定義的內容背景色
299
+ if "content_bg_color" in theme:
300
+ fill.fore_color.rgb = theme["content_bg_color"]
301
+ else:
302
+ # 回退邏輯
303
+ if self.is_dark_background(theme):
304
+ fill.fore_color.rgb = RGBColor(40, 40, 40) # 深色背景用深灰框
305
+ else:
306
+ fill.fore_color.rgb = RGBColor(255, 255, 255) # 淺色背景用白色框
307
+
308
+ # 設置邊框
309
+ bg_shape.line.color.rgb = theme["accent_color"]
310
+ bg_shape.line.width = Pt(2)
311
+
312
+ # 對於深色背景,增加透明度
313
+ if self.is_dark_background(theme):
314
+ fill.transparency = 0.2 # 20% 透明度
315
+ else:
316
+ fill.transparency = 0.05 # 5% 透明度
317
+
318
+ return bg_shape
319
+ except Exception as e:
320
+ print(f"創建內容背景框錯誤: {e}")
321
+ return None