Spaces:
Running
Running
CommitForLocalAndCloud
Browse files- app.py +1002 -867
- localapp.py +1056 -0
- ppt_analyzer.py +678 -0
- setup_and_run.bat +70 -0
- 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 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
self.
|
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 |
-
text_frame
|
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 |
-
def
|
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 |
-
self.
|
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 |
)
|
|
|
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
|