File size: 45,173 Bytes
09c9f6f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
# gemini_ppt_generator.py
import os
import json
import requests
import tempfile
from io import BytesIO
from PIL import Image
import gradio as gr
import google.generativeai as genai
from pptx import Presentation
from pptx.util import Inches, Pt
from pptx.enum.text import PP_ALIGN
from pptx.dml.color import RGBColor
from slide_themes import SlideThemeManager
from ppt_analyzer import PPTAnalyzer

class GeminiPPTGenerator:
    def __init__(self):
        self.pexels_headers = {}
        self.gemini_model = None
        self.config_file = "config.json"
        
        # 載入已保存的API金鑰
        self.load_config()
        
        # 初始化版型管理器
        self.theme_manager = SlideThemeManager()
        
        # 16:9 簡報尺寸 (單位:英吋)
        self.slide_width = self.theme_manager.slide_width
        self.slide_height = self.theme_manager.slide_height
        
        # 圖片風格
        self.image_styles = self.theme_manager.image_styles
    
    def load_config(self):
        """從config.json載入API金鑰"""
        try:
            if os.path.exists(self.config_file):
                with open(self.config_file, 'r', encoding='utf-8') as f:
                    config = json.load(f)
                    gemini_key = config.get('gemini_api_key', '')
                    pexels_key = config.get('pexels_api_key', '')
                    
                    if gemini_key and pexels_key:
                        self.setup_apis(gemini_key, pexels_key)
                        return gemini_key, pexels_key
        except Exception as e:
            print(f"載入配置錯誤: {e}")
        return '', ''
    
    def save_config(self, gemini_api_key, pexels_api_key):
        """保存API金鑰到config.json"""
        try:
            config = {
                'gemini_api_key': gemini_api_key,
                'pexels_api_key': pexels_api_key
            }
            with open(self.config_file, 'w', encoding='utf-8') as f:
                json.dump(config, f, ensure_ascii=False, indent=2)
            return True
        except Exception as e:
            print(f"保存配置錯誤: {e}")
            return False
    
    def get_saved_keys(self):
        """獲取已保存的API金鑰"""
        try:
            if os.path.exists(self.config_file):
                with open(self.config_file, 'r', encoding='utf-8') as f:
                    config = json.load(f)
                    return config.get('gemini_api_key', ''), config.get('pexels_api_key', '')
        except:
            pass
        return '', ''
    
    def setup_apis(self, gemini_api_key, pexels_api_key):
        """設定 API 金鑰"""
        try:
            # 設定 Gemini API
            if gemini_api_key:
                genai.configure(api_key=gemini_api_key)
                self.gemini_model = genai.GenerativeModel('gemini-2.5-flash-preview-05-20')
            
            # 設定 Pexels API
            if pexels_api_key:
                self.pexels_headers = {
                    "Authorization": pexels_api_key
                }
            
            return True, "✅ API 設定成功"
        except Exception as e:
            return False, f"❌ API 設定失敗:{str(e)}"
    
    def generate_content_with_gemini(self, topic, slide_count=5):
        """使用 Gemini 生成簡報內容"""
        
        prompt = f"""

        請為主題「{topic}」製作一個 {slide_count} 頁的簡報大綱,以 JSON 格式回傳。



        格式要求:

        {{

            "title": "簡報主標題",

            "subtitle": "簡報副標題",

            "title_keywords": "主題相關的英文關鍵字,用於搜尋標題頁和結尾頁圖片",

            "slides": [

                {{

                    "title": "投影片標題",

                    "content": [

                        "重點1",

                        "重點2", 

                        "重點3"

                    ],

                    "image_keywords": "英文關鍵字,用於搜尋相關圖片"

                }}

            ]

        }}



        要求:

        1. 內容要專業且有邏輯性

        2. 每頁 3-4 個重點

        3. title_keywords 要用英文,描述主題相關的圖片搜尋關鍵字(3-5個詞)

        4. image_keywords 要用英文,描述該投影片適合的圖片內容

        5. 關鍵字要具體明確,例如 "business meeting", "technology innovation", "data analysis"

        6. 使用繁體中文(除了 title_keywords 和 image_keywords)

        7. 第一頁是概述介紹,最後一頁是結論總結

        8. 請直接回傳 JSON,不要包含其他文字說明,也不可以有"**"等不必要的markdown符號

        """
        
        try:
            if self.gemini_model:
                response = self.gemini_model.generate_content(prompt)
                content = response.text
                
                # 清理回應內容,提取 JSON
                content = content.strip()
                if content.startswith('```json'):
                    content = content[7:]
                if content.endswith('```'):
                    content = content[:-3]
                
                # 尋找 JSON 開始和結束位置
                start = content.find('{')
                end = content.rfind('}') + 1
                
                if start != -1 and end > start:
                    json_str = content[start:end]
                    return json.loads(json_str)
                else:
                    raise ValueError("無法在回應中找到有效的 JSON")
                    
            else:
                return self.get_default_structure_with_images(topic)
                
        except Exception as e:
            print(f"Gemini API 錯誤: {e}")
            return self.get_default_structure_with_images(topic)
    
    def get_default_structure_with_images(self, topic):
        """預設簡報結構(含圖片關鍵字)"""
        # 生成簡單的英文關鍵字
        title_keywords = "business presentation professional meeting"
        if "科技" in topic or "技術" in topic:
            title_keywords = "technology innovation digital development"
        elif "教育" in topic or "學習" in topic:
            title_keywords = "education learning academic study"
        elif "醫療" in topic or "健康" in topic:
            title_keywords = "healthcare medical health wellness"
        elif "環境" in topic or "環保" in topic:
            title_keywords = "environment sustainability green nature"
        elif "經濟" in topic or "金融" in topic:
            title_keywords = "economics finance business economy"
        
        return {
            "title": f"{topic} 簡報",
            "subtitle": "由 AI 自動生成",
            "title_keywords": title_keywords,
            "slides": [
                {
                    "title": "簡介與背景",
                    "content": [
                        "主題背景介紹",
                        "研究目的與範圍", 
                        "簡報架構說明"
                    ],
                    "image_keywords": "presentation introduction business"
                },
                {
                    "title": "主要內容分析",
                    "content": [
                        "核心概念說明",
                        "重要特點分析",
                        "相關案例討論"
                    ],
                    "image_keywords": "analysis data research content"
                },
                {
                    "title": "深入探討",
                    "content": [
                        "優勢與機會識別",
                        "挑戰與問題分析",
                        "影響因素評估"
                    ],
                    "image_keywords": "strategy planning discussion"
                },
                {
                    "title": "解決方案與建議",
                    "content": [
                        "策略建議提出",
                        "實施方法規劃",
                        "預期效果評估"
                    ],
                    "image_keywords": "solution implementation strategy"
                },
                {
                    "title": "結論與展望",
                    "content": [
                        "重點總結回顧",
                        "未來發展趨勢", 
                        "行動建議提出"
                    ],
                    "image_keywords": "conclusion future success"
                }
            ]
        }
    
    def search_pexels_with_style(self, keywords, image_style="professional", per_page=10):
        """根據風格搜尋 Pexels 圖片"""
        if not self.pexels_headers:
            return None
        
        # 先嘗試純主題關鍵字搜尋
        url = "https://api.pexels.com/v1/search"
        params = {
            "query": keywords,
            "per_page": per_page,
            "orientation": "landscape",
            "size": "medium"
        }
        
        try:
            response = requests.get(url, headers=self.pexels_headers, params=params)
            if response.status_code == 200:
                data = response.json()
                if data["photos"] and len(data["photos"]) >= 3:
                    return data["photos"]
            
            # 如果純主題搜尋結果不足,再組合風格關鍵字
            style_modifier = self.image_styles.get(image_style, "")
            enhanced_keywords = f"{keywords} {style_modifier}"
            
            params["query"] = enhanced_keywords
            response = requests.get(url, headers=self.pexels_headers, params=params)
            if response.status_code == 200:
                data = response.json()
                return data["photos"] if data["photos"] else None
            
            return None
        except Exception as e:
            print(f"Pexels API 錯誤: {e}")
            return None
    
    def select_best_image(self, photos, slide_title=""):
        """從多張圖片中選擇最適合的"""
        if not photos:
            return None
        
        # 選擇解析度較高的圖片
        best_photo = photos[0]
        for photo in photos[:3]:
            if photo["width"] * photo["height"] > best_photo["width"] * best_photo["height"]:
                best_photo = photo
        
        return best_photo["src"]["medium"]
    
    def download_image(self, image_url):
        """下載圖片並返回檔案路徑"""
        if not image_url:
            return None
            
        try:
            response = requests.get(image_url)
            if response.status_code == 200:
                temp_dir = tempfile.mkdtemp()
                image_path = os.path.join(temp_dir, "slide_image.jpg")
                
                # 處理圖片
                image = Image.open(BytesIO(response.content))
                
                # 調整圖片大小
                max_size = (800, 600)
                image.thumbnail(max_size, Image.Resampling.LANCZOS)
                
                # 轉換並儲存
                if image.mode in ("RGBA", "P"):
                    image = image.convert("RGB")
                image.save(image_path, "JPEG", quality=85)
                
                return image_path
            return None
        except Exception as e:
            print(f"圖片下載錯誤: {e}")
            return None
    
    def add_image_to_slide(self, slide, image_path, theme):
        """將圖片添加到投影片,保持比例避免變形"""
        if not image_path or not os.path.exists(image_path):
            return
        
        try:
            image_area = theme["image_area"]
            
            # 目標區域
            target_left = Inches(image_area["left"])
            target_top = Inches(image_area["top"])
            target_width = Inches(image_area["width"])
            target_height = Inches(image_area["height"])
            
            # 載入圖片獲取原始尺寸
            from PIL import Image as PILImage
            with PILImage.open(image_path) as img:
                original_width, original_height = img.size
                original_ratio = original_width / original_height
            
            # 計算目標比例
            target_ratio = target_width.inches / target_height.inches
            
            # 根據比例計算實際顯示尺寸,保持圖片比例
            if original_ratio > target_ratio:
                # 圖片較寬,以寬度為準
                actual_width = target_width
                actual_height = Inches(target_width.inches / original_ratio)
                # 垂直置中
                actual_top = Inches(target_top.inches + (target_height.inches - actual_height.inches) / 2)
                actual_left = target_left
            else:
                # 圖片較高,以高度為準
                actual_height = target_height
                actual_width = Inches(target_height.inches * original_ratio)
                # 水平置中
                actual_left = Inches(target_left.inches + (target_width.inches - actual_width.inches) / 2)
                actual_top = target_top
            
            # 添加圖片
            picture = slide.shapes.add_picture(image_path, actual_left, actual_top, actual_width, actual_height)
                
        except Exception as e:
            print(f"添加圖片錯誤: {e}")
            # 降級處理:如果計算失敗,使用原來的方式
            try:
                left = Inches(image_area["left"])
                top = Inches(image_area["top"])
                width = Inches(image_area["width"])
                height = Inches(image_area["height"])
                slide.shapes.add_picture(image_path, left, top, width, height)
            except:
                pass
    
    def setup_slide_content(self, slide, slide_data, theme):
        """設定投影片內容,使用正確的位置"""
        try:
            # 設置背景和裝飾元素
            self.theme_manager.setup_slide_background_and_layout(slide, theme)
            
            # 設定標題
            title_shape = slide.shapes.title
            title_shape.text = slide_data["title"]
            
            # 調整標題位置和尺寸
            title_area = theme["title_area"]
            title_shape.left = Inches(title_area["left"])
            title_shape.top = Inches(title_area["top"])
            title_shape.width = Inches(title_area["width"])
            title_shape.height = Inches(title_area["height"])
            
            self.theme_manager.format_title(title_shape, theme, 34, self.get_font_name)
            
            # 移除預設內容佔位符(如果存在)
            shapes_to_remove = []
            for shape in slide.shapes:
                try:
                    if hasattr(shape, 'placeholder_format') and shape.placeholder_format is not None:
                        if shape.placeholder_format.type == 2:  # 內容佔位符
                            shapes_to_remove.append(shape)
                except:
                    continue
            
            for shape in shapes_to_remove:
                try:
                    sp = shape.element
                    sp.getparent().remove(sp)
                except:
                    continue
            
            # 設定內容區域
            content_area = theme["content_area"]
            
            # 創建帶背景的內容框
            self.theme_manager.create_content_box_with_background(slide, theme, content_area)
            
            # 創建新的內容文字框
            left = Inches(content_area["left"])
            top = Inches(content_area["top"])
            width = Inches(content_area["width"])
            height = Inches(content_area["height"])
            
            textbox = slide.shapes.add_textbox(left, top, width, height)
            text_frame = textbox.text_frame
            
            # 設定文字框屬性
            text_frame.margin_left = Inches(0.15)
            text_frame.margin_right = Inches(0.15)
            text_frame.margin_top = Inches(0.1)
            text_frame.margin_bottom = Inches(0.1)
            text_frame.word_wrap = True
            text_frame.auto_size = None  # 不自動調整大小
            
            # 清除預設文字
            text_frame.clear()
            
            # 添加內容
            for i, point in enumerate(slide_data["content"]):
                if i == 0:
                    p = text_frame.paragraphs[0]
                else:
                    p = text_frame.add_paragraph()
                
                p.text = f"• {point}"
                p.level = 0
                p.space_after = Pt(10)  # 段落間距
                self.theme_manager.format_content(p, theme, 22, self.get_font_name)
                
        except Exception as e:
            print(f"設定投影片內容錯誤詳細: {str(e)}")
            import traceback
            print(f"錯誤追蹤: {traceback.format_exc()}")
    
    def adjust_content_layout(self, slide, layout_type):
        """這個方法已被 setup_slide_content 取代,保留以免錯誤"""
        pass
    
    def get_font_name(self):
        """獲取中文字型名稱"""
        # 檢查是否有自定義中文字型檔案
        font_path = os.path.join(os.path.dirname(__file__), "cht.ttf")
        if os.path.exists(font_path):
            return "cht"  # 使用自定義字型
        else:
            # 備用字型選擇
            return "Arial Unicode MS"  # 通用 Unicode 字型
    
    def format_title_with_shadow(self, shape, theme, font_size):
        """格式化標題並添加陰影效果以提高可讀性"""
        self.theme_manager.format_title(shape, theme, font_size, self.get_font_name)
        try:
            paragraph = shape.text_frame.paragraphs[0]
            paragraph.font.color.rgb = RGBColor(255, 255, 255)  # 白色文字在深色背景上更清楚
            paragraph.alignment = PP_ALIGN.CENTER
            paragraph.font.bold = True
        except:
            pass
    
    def create_presentation_with_images(self, topic, theme_name="商務專業", 

                                      slide_count=5, image_style="professional"):
        """建立包含圖片的簡報"""
        
        # 生成內容結構
        structure = self.generate_content_with_gemini(topic, slide_count)
        theme = self.theme_manager.get_theme(theme_name)
        
        # 建立 16:9 簡報
        prs = Presentation()
        prs.slide_width = self.slide_width
        prs.slide_height = self.slide_height
        
        # 建立標題頁
        title_slide = prs.slides.add_slide(prs.slide_layouts[0])
        title_shape = title_slide.shapes.title
        subtitle_shape = title_slide.placeholders[1]
        
        title_shape.text = structure["title"]
        subtitle_shape.text = structure["subtitle"]
        
        # 調整標題頁版面 (16:9)
        title_shape.left = Inches(1.0)
        title_shape.top = Inches(2.0)
        title_shape.width = Inches(11.333)
        title_shape.height = Inches(1.5)
        
        subtitle_shape.left = Inches(1.0)
        subtitle_shape.top = Inches(4.0)
        subtitle_shape.width = Inches(11.333)
        subtitle_shape.height = Inches(1.0)
        
        # 格式化標題頁 - 加強文字可讀性
        self.format_title_with_shadow(title_shape, theme, 54)
        self.format_title_with_shadow(subtitle_shape, theme, 32)
        
        # 為標題頁添加主題相關圖片 - 使用AI生成的英文關鍵字
        main_keywords = structure.get("title_keywords", f"{topic} introduction overview")
        title_photos = self.search_pexels_with_style(main_keywords, image_style, per_page=15)
        if title_photos:
            title_image_url = self.select_best_image(title_photos, structure["title"])
            if title_image_url:
                title_image_path = self.download_image(title_image_url)
                if title_image_path:
                    # 標題頁使用半透明背景
                    self.add_title_background_with_overlay(title_slide, title_image_path, theme)
        
        # 建立內容頁
        for i, slide_data in enumerate(structure["slides"]):
            slide = prs.slides.add_slide(prs.slide_layouts[1])
            
            # 設定內容和版面
            self.setup_slide_content(slide, slide_data, theme)
            
            # 搜尋並添加圖片
            keywords = slide_data.get("image_keywords", f"{topic} slide {i+1}")
            photos = self.search_pexels_with_style(keywords, image_style)
            
            if photos:
                image_url = self.select_best_image(photos, slide_data["title"])
                if image_url:
                    image_path = self.download_image(image_url)
                    if image_path:
                        self.add_image_to_slide(slide, image_path, theme)
        
        # 建立感謝頁
        self.add_thank_you_slide(prs, theme, image_style, topic, structure)
        
        return prs, structure
    
    def search_pexels_image_for_title(self, keywords, topic, per_page=10):
        """專門為標題頁搜尋圖片,優先考慮主題相關性"""
        if not self.pexels_headers:
            return None
        
        # 先嘗試純主題搜尋
        topic_keywords = f"{topic} background"
        
        url = "https://api.pexels.com/v1/search"
        params = {
            "query": topic_keywords,
            "per_page": per_page,
            "orientation": "landscape",
            "size": "medium"
        }
        
        try:
            response = requests.get(url, headers=self.pexels_headers, params=params)
            if response.status_code == 200:
                data = response.json()
                if data["photos"]:
                    return data["photos"]
            
            # 如果主題搜尋沒結果,使用通用關鍵字
            fallback_params = {
                "query": "professional presentation background",
                "per_page": per_page,
                "orientation": "landscape",
                "size": "medium"
            }
            
            response = requests.get(url, headers=self.pexels_headers, params=fallback_params)
            if response.status_code == 200:
                data = response.json()
                return data["photos"] if data["photos"] else None
                
            return None
        except Exception as e:
            print(f"Pexels API 錯誤: {e}")
            return None
    
    def add_title_background_with_overlay(self, slide, image_path, theme):
        """為標題頁添加帶有文字背景框的背景圖片"""
        try:
            # 添加背景圖片
            picture = slide.shapes.add_picture(
                image_path, 
                Inches(0), 
                Inches(0), 
                self.slide_width, 
                self.slide_height
            )
            # 移到背景層
            picture.element.getparent().remove(picture.element)
            slide.shapes._spTree.insert(2, picture.element)            
            
        except Exception as e:
            print(f"添加標題背景錯誤: {e}")
            # 降級處理:直接添加背景圖片
            try:
                picture = slide.shapes.add_picture(
                    image_path, 
                    Inches(0), 
                    Inches(0), 
                    self.slide_width, 
                    self.slide_height
                )
                picture.element.getparent().remove(picture.element)
                slide.shapes._spTree.insert(2, picture.element)
            except:
                pass
    
    def add_title_background(self, slide, image_path):
        """為標題頁添加背景圖片(保留原方法以免錯誤)"""
        self.add_title_background_with_overlay(slide, image_path, None)
    
    def add_thank_you_slide(self, prs, theme, image_style, topic, structure):
        """添加感謝頁"""
        thank_slide = prs.slides.add_slide(prs.slide_layouts[5])
        
        # 感謝頁使用主題關鍵字加上結尾相關詞彙
        title_keywords = structure.get("title_keywords", f"{topic} success conclusion achievement")
        thank_keywords = f"{title_keywords} success conclusion achievement"
        thank_photos = self.search_pexels_with_style(thank_keywords, image_style, per_page=12)
        if thank_photos:
            thank_image_url = self.select_best_image(thank_photos)
            if thank_image_url:
                thank_image_path = self.download_image(thank_image_url)
                if thank_image_path:
                    self.add_title_background_with_overlay(thank_slide, thank_image_path, theme)
        
        # 添加感謝文字背景框
        text_bg = thank_slide.shapes.add_shape(
            1,  # 矩形
            Inches(2.5),
            Inches(2.0),
            Inches(8.333),
            Inches(3.5)
        )
        
        fill = text_bg.fill
        fill.solid()
        fill.fore_color.rgb = RGBColor(255, 255, 255)  # 白色背景
        text_bg.line.color.rgb = theme["accent_color"] if theme else RGBColor(79, 129, 189)
        text_bg.line.width = Pt(3)
        
        # 添加感謝文字 (16:9 居中位置)
        left = Inches(3.0)
        top = Inches(2.5)
        width = Inches(7.333)
        height = Inches(2.5)
        
        textbox = thank_slide.shapes.add_textbox(left, top, width, height)
        text_frame = textbox.text_frame
        text_frame.text = "謝謝聆聽\nThank You"
        
        for paragraph in text_frame.paragraphs:
            paragraph.font.name = self.get_font_name()
            paragraph.font.size = Pt(60)
            paragraph.font.color.rgb = theme["title_color"] if theme else RGBColor(31, 73, 125)
            paragraph.alignment = PP_ALIGN.CENTER
            paragraph.font.bold = True
    

    def save_presentation(self, prs, filename):
        """儲存簡報"""
        temp_dir = tempfile.mkdtemp()
        filepath = os.path.join(temp_dir, filename)
        prs.save(filepath)
        return filepath
    
    
    def generate_preview_text(self, structure):
        """生成簡報預覽文字"""
        preview = f"📊 {structure['title']}\n"
        preview += f"   {structure['subtitle']}\n\n"
        
        for i, slide in enumerate(structure['slides'], 1):
            preview += f"{i}. {slide['title']}\n"
            for point in slide['content'][:2]:  # 只顯示前兩個重點
                preview += f"   • {point}\n"
            if len(slide['content']) > 2:
                preview += f"   • ...(共 {len(slide['content'])} 個重點)\n"
            preview += "\n"
        
        return preview

def analyze_and_restyle_ppt(gemini_api_key, pexels_api_key, uploaded_file, theme_name, image_style):
    """分析並重新設計上傳的簡報"""
    
    if not uploaded_file:
        return None, "", "❌ 請上傳PPT文件"
    
    generator = GeminiPPTGenerator()
    
    # 如果API金鑰為空,嘗試從已保存的配置載入
    if not gemini_api_key.strip() or not pexels_api_key.strip():
        saved_gemini, saved_pexels = generator.get_saved_keys()
        if not gemini_api_key.strip():
            gemini_api_key = saved_gemini
        if not pexels_api_key.strip():
            pexels_api_key = saved_pexels
    
    # 檢查輸入
    if not gemini_api_key.strip():
        return None, "", "❌ 請輸入 Gemini API 金鑰"
    
    if not pexels_api_key.strip():
        return None, "", "❌ 請輸入 Pexels API 金鑰"
    
    try:
        # 設定 API
        success, message = generator.setup_apis(gemini_api_key, pexels_api_key)
        if not success:
            return None, "", message
        
        # 創建分析器
        analyzer = PPTAnalyzer(
            gemini_model=generator.gemini_model,
            pexels_headers=generator.pexels_headers,
            image_styles=generator.image_styles
        )
        
        # 分析上傳的PPT
        analysis_result = analyzer.analyze_ppt_file(uploaded_file.name)
        if not analysis_result:
            return None, "", "❌ 無法分析PPT文件,請確認文件格式正確"
        
        # 套用新主題和添加圖片
        processed_prs, processed_slides = analyzer.apply_theme_to_presentation(
            uploaded_file.name, theme_name, image_style, analysis_result
        )
        
        if not processed_prs:
            return None, "", "❌ 處理PPT文件時發生錯誤"
        
        # 生成分析報告
        report = analyzer.generate_analysis_report(analysis_result, processed_slides)
        
        # 儲存處理後的簡報
        original_name = os.path.splitext(os.path.basename(uploaded_file.name))[0]
        filename = f"{original_name}_{theme_name}_{image_style}_restyled.pptx"
        output_path = analyzer.save_processed_presentation(processed_prs, filename)
        
        if not output_path:
            return None, "", "❌ 儲存處理後的簡報時發生錯誤"
        
        success_msg = f"✅ 成功重新設計《{original_name}》!\n"
        success_msg += f"🎨 套用主題:{theme_name}\n"
        success_msg += f"🖼️ 圖片風格:{image_style}\n"
        success_msg += f"📄 處理了 {len(processed_slides)} 張投影片"
        
        return output_path, report, success_msg
        
    except Exception as e:
        import traceback
        error_details = traceback.format_exc()
        print(f"詳細錯誤: {error_details}")
        return None, "", f"❌ 處理失敗:{str(e)}"
    
    

def generate_ppt_with_gemini(gemini_api_key, pexels_api_key, topic, theme, slide_count, image_style):
    """生成簡報的主要函數"""
    
    generator = GeminiPPTGenerator()
    
    # 如果API金鑰為空,嘗試從已保存的配置載入
    if not gemini_api_key.strip() or not pexels_api_key.strip():
        saved_gemini, saved_pexels = generator.get_saved_keys()
        if not gemini_api_key.strip():
            gemini_api_key = saved_gemini
        if not pexels_api_key.strip():
            pexels_api_key = saved_pexels
    
    # 檢查輸入
    if not gemini_api_key.strip():
        return None, "", "❌ 請輸入 Gemini API 金鑰"
    
    if not pexels_api_key.strip():
        return None, "", "❌ 請輸入 Pexels API 金鑰"
    
    if not topic.strip():
        return None, "", "❌ 請輸入簡報主題"
    
    try:
        # 設定 API
        success, message = generator.setup_apis(gemini_api_key, pexels_api_key)
        if not success:
            return None, "", message
        
        # 保存API金鑰到配置檔案
        generator.save_config(gemini_api_key, pexels_api_key)
        
        # 生成簡報
        prs, structure = generator.create_presentation_with_images(
            topic, theme, slide_count, image_style
        )
        
        # 生成預覽
        preview = generator.generate_preview_text(structure)
        
        # 儲存檔案
        filename = f"{topic.replace(' ', '_')}_{image_style}_簡報.pptx"
        filepath = generator.save_presentation(prs, filename)
        
        success_msg = f"✅ 成功生成《{topic}{image_style}風格簡報!({slide_count} 頁,含圖片)"
        
        return filepath, preview, success_msg
        
    except Exception as e:
        import traceback
        error_details = traceback.format_exc()
        print(f"詳細錯誤: {error_details}")
        return None, "", f"❌ 生成失敗:{str(e)}"

# Gradio 介面
def create_gemini_interface():
    """建立 Gradio 介面"""
    
    # 檢查是否已有保存的API金鑰
    generator = GeminiPPTGenerator()
    saved_gemini, saved_pexels = generator.get_saved_keys()
    keys_exist = bool(saved_gemini and saved_pexels)
    
    with gr.Blocks(title="Gemini AI 圖文簡報生成器", theme=gr.themes.Soft()) as iface:
        gr.Markdown("# 🤖 Gemini AI 智能圖文簡報生成器")
        gr.Markdown("**使用 Google Gemini 2.0 + Pexels 圖庫**,智能生成專業圖文簡報,或改造現有簡報")
        
        # API 設定區域(共用)
        if keys_exist:
            with gr.Group():
                gr.Markdown("### ✅ API 金鑰已配置")
                gr.Markdown("API 金鑰已從 config.json 載入,可直接使用。如需更新金鑰,請刪除 config.json 檔案後重新啟動。")
                # 隱藏的輸入框,用於傳遞已保存的金鑰
                gemini_api_input = gr.Textbox(value=saved_gemini, visible=False)
                pexels_api_input = gr.Textbox(value=saved_pexels, visible=False)
        else:
            with gr.Group():
                gr.Markdown("### 🔑 API 設定")
                with gr.Row():
                    gemini_api_input = gr.Textbox(
                        label="🤖 Gemini API Key",
                        placeholder="請輸入你的 Gemini API 金鑰",
                        type="password",
                        info="免費額度,前往 https://ai.google.dev/ 獲取"
                    )
                    pexels_api_input = gr.Textbox(
                        label="📸 Pexels API Key", 
                        placeholder="請輸入你的 Pexels API 金鑰",
                        type="password",
                        info="免費 200次/月,前往 https://www.pexels.com/api/ 獲取"
                    )
        
        # 選項卡
        with gr.Tabs():
            # 原有的生成功能
            with gr.TabItem("🆕 創建新簡報"):
                # 主要設定區域
                with gr.Row():
                    with gr.Column(scale=2):
                        topic_input = gr.Textbox(
                            label="📝 簡報主題",
                            placeholder="請輸入具體的簡報主題...",
                            value="人工智慧在現代教育中的應用與挑戰"
                        )
                        
                        with gr.Row():
                            # 從主題管理器獲取所有主題名稱
                            generator = GeminiPPTGenerator()
                            theme_dropdown = gr.Dropdown(
                                choices=generator.theme_manager.get_all_theme_names(),
                                value="商務專業",
                                label="🎨 版型風格"
                            )
                            
                            image_style_dropdown = gr.Dropdown(
                                choices=["professional", "creative", "minimalist", "modern", "natural", "technology"],
                                value="professional",
                                label="🖼️ 圖片風格"
                            )
                        
                        slide_count = gr.Slider(
                            minimum=3,
                            maximum=20,
                            value=6,
                            step=1,
                            label="📄 投影片數量"
                        )
                        
                        generate_btn = gr.Button("🚀 生成專業簡報", variant="primary", size="lg")
                    
                    with gr.Column(scale=1):
                        status_output = gr.Textbox(label="📊 生成狀態", interactive=False)
                        file_output = gr.File(label="📁 下載簡報")
                        
                        # 預覽區域
                        with gr.Group():
                            gr.Markdown("### 📋 簡報預覽")
                            preview_output = gr.Textbox(
                                label="內容大綱",
                                placeholder="生成後將顯示簡報大綱...",
                                lines=8,
                                interactive=False
                            )
            
            # 新增的簡報改造功能
            with gr.TabItem("🔄 改造現有簡報"):
                gr.Markdown("### 📤 上傳並改造您的簡報")
                gr.Markdown("上傳現有的PPT文件,AI將分析內容並套用新的版型設計,自動為每頁添加相關圖片")
                
                with gr.Row():
                    with gr.Column(scale=2):
                        # 文件上傳
                        upload_file = gr.File(
                            label="📎 上傳PPT文件",
                            file_types=[".pptx", ".ppt"],
                            type="filepath"
                        )
                        
                        with gr.Row():
                            # 主題選擇
                            upload_theme_dropdown = gr.Dropdown(
                                choices=generator.theme_manager.get_all_theme_names(),
                                value="商務專業",
                                label="🎨 套用版型風格"
                            )
                            
                            upload_image_style_dropdown = gr.Dropdown(
                                choices=["professional", "creative", "minimalist", "modern", "natural", "technology"],
                                value="professional",
                                label="🖼️ 圖片風格"
                            )
                        
                        analyze_btn = gr.Button("🔍 分析並改造簡報", variant="primary", size="lg")
                        
                    with gr.Column(scale=1):
                        upload_status_output = gr.Textbox(label="📊 處理狀態", interactive=False)
                        upload_file_output = gr.File(label="📁 下載改造後簡報")
                        
                        # 分析報告區域
                        with gr.Group():
                            gr.Markdown("### 📋 分析報告")
                            analysis_report = gr.Textbox(
                                label="處理詳情",
                                placeholder="上傳並處理後將顯示分析報告...",
                                lines=8,
                                interactive=False
                            )
        
        # 說明區域
        with gr.Accordion("📖 使用說明與功能特色", open=False):
            gr.Markdown("""

            ### 🌟 核心特色

            

            #### 🤖 Google Gemini 2.0 Flash

            - **最新模型**:使用 Gemini 2.0 Flash Preview 版本

            - **免費額度**:Google 提供慷慨的免費使用額度

            - **中文優化**:對繁體中文有優秀的理解和生成能力

            - **結構化輸出**:精確生成 JSON 格式的簡報結構

            - **內容分析**:智能分析現有簡報內容,生成適合的圖片搜尋關鍵字

            

            #### 📸 Pexels 圖片整合

            - **百萬圖庫**:Pexels 提供高品質免費圖片

            - **智能匹配**:AI 為每頁生成最適合的搜尋關鍵字

            - **風格選擇**:6 種圖片風格滿足不同需求

            - **自動配圖**:每張投影片自動配上相關圖片

            - **智能避重**:首頁和結尾使用不同關鍵字避免重複圖片

            

            #### 🎨 專業版面設計

            - **8 種版型**:商務、科技、創意、學術、簡約、橙色、紫色、藍綠風格

            - **智能排版**:根據版型自動調整圖文位置

            - **色彩搭配**:專業的色彩主題設計,高對比度確保文字清晰

            - **中文字型**:完美支援繁體中文顯示

            - **背景漸變**:精美的漸變背景和裝飾元素

            

            #### 🔄 簡報改造功能

            - **檔案分析**:智能分析上傳的PPT文件結構和內容

            - **表格檢測**:自動識別包含表格的投影片,只套用配色不添加圖片

            - **版型套用**:將現有簡報套用全新的專業版型設計

            - **AI配圖**:為每頁內容生成專屬的圖片搜尋關鍵字並自動配圖

            - **空間計算**:智能計算可用空間,合理放置圖片避免覆蓋原有內容

            

            ### 📋 使用步驟

            

            #### 🆕 創建新簡報

            1. **獲取 API 金鑰**:

               - Gemini API:前往 [Google AI Studio](https://ai.google.dev/) 免費申請

               - Pexels API:前往 [Pexels API](https://www.pexels.com/api/) 免費申請(200次/日)

            

            2. **輸入 API 金鑰**:在上方輸入框中填入你的 API 金鑰(僅需輸入一次,會自動保存到 config.json)

            

            3. **設定簡報參數**:

               - 輸入具體明確的簡報主題

               - 選擇適合的版型和圖片風格

               - 設定所需的投影片數量

            

            4. **生成簡報**:點擊生成按鈕,系統將自動完成所有工作

            

            5. **下載使用**:獲得完整的 .pptx 檔案,可直接在 PowerPoint 中使用

            

            #### 🔄 改造現有簡報

            1. **上傳PPT文件**:支援 .pptx 和 .ppt 格式

            

            2. **選擇版型風格**:從8種專業版型中選擇適合的風格

            

            3. **選擇圖片風格**:選擇與內容匹配的圖片風格

            

            4. **開始分析改造**:AI將自動分析每頁內容並套用新設計

            

            5. **查看分析報告**:了解每頁的處理詳情和圖片添加情況

            

            6. **下載改造後簡報**:獲得全新設計的簡報文件

            

            ### 💡 專業建議

            - **主題要具體**:「AI在醫療診斷的應用」比「人工智慧」效果更好

            - **選對風格**:商務場合用「professional」,創意展示用「creative」

            - **適當頁數**:建議 5-8 頁,內容豐富但不冗長

            - **測試 API**:第一次使用建議先測試 API 連接是否正常

            

            ### 🔧 技術特點

            - **純 Python 實現**:不需要安裝 Microsoft Office

            - **即時生成**:通常 30-60 秒完成整個簡報

            - **高品質輸出**:生成的 .pptx 檔案完全相容 PowerPoint

            - **跨平台支援**:Windows、macOS、Linux 都能正常使用

            """)
        
        # 事件綁定
        generate_btn.click(
            fn=generate_ppt_with_gemini,
            inputs=[
                gemini_api_input, 
                pexels_api_input, 
                topic_input, 
                theme_dropdown, 
                slide_count, 
                image_style_dropdown
            ],
            outputs=[file_output, preview_output, status_output]
        )
        
        analyze_btn.click(
            fn=analyze_and_restyle_ppt,
            inputs=[
                gemini_api_input,
                pexels_api_input,
                upload_file,
                upload_theme_dropdown,
                upload_image_style_dropdown
            ],
            outputs=[upload_file_output, analysis_report, upload_status_output]
        )
    
    return iface

if __name__ == "__main__":
    # 啟動應用
    iface = create_gemini_interface()
    iface.launch(
        server_name="127.0.0.1", 
        server_port=7860,
        share=False,
        inbrowser=True
    )