PPTCreator / localapp.py
dseditor's picture
CommitForLocalAndCloud
09c9f6f verified
# gemini_ppt_generator.py
import os
import json
import requests
import tempfile
from io import BytesIO
from PIL import Image
import gradio as gr
import google.generativeai as genai
from pptx import Presentation
from pptx.util import Inches, Pt
from pptx.enum.text import PP_ALIGN
from pptx.dml.color import RGBColor
from slide_themes import SlideThemeManager
from ppt_analyzer import PPTAnalyzer
class GeminiPPTGenerator:
def __init__(self):
self.pexels_headers = {}
self.gemini_model = None
self.config_file = "config.json"
# 載入已保存的API金鑰
self.load_config()
# 初始化版型管理器
self.theme_manager = SlideThemeManager()
# 16:9 簡報尺寸 (單位:英吋)
self.slide_width = self.theme_manager.slide_width
self.slide_height = self.theme_manager.slide_height
# 圖片風格
self.image_styles = self.theme_manager.image_styles
def load_config(self):
"""從config.json載入API金鑰"""
try:
if os.path.exists(self.config_file):
with open(self.config_file, 'r', encoding='utf-8') as f:
config = json.load(f)
gemini_key = config.get('gemini_api_key', '')
pexels_key = config.get('pexels_api_key', '')
if gemini_key and pexels_key:
self.setup_apis(gemini_key, pexels_key)
return gemini_key, pexels_key
except Exception as e:
print(f"載入配置錯誤: {e}")
return '', ''
def save_config(self, gemini_api_key, pexels_api_key):
"""保存API金鑰到config.json"""
try:
config = {
'gemini_api_key': gemini_api_key,
'pexels_api_key': pexels_api_key
}
with open(self.config_file, 'w', encoding='utf-8') as f:
json.dump(config, f, ensure_ascii=False, indent=2)
return True
except Exception as e:
print(f"保存配置錯誤: {e}")
return False
def get_saved_keys(self):
"""獲取已保存的API金鑰"""
try:
if os.path.exists(self.config_file):
with open(self.config_file, 'r', encoding='utf-8') as f:
config = json.load(f)
return config.get('gemini_api_key', ''), config.get('pexels_api_key', '')
except:
pass
return '', ''
def setup_apis(self, gemini_api_key, pexels_api_key):
"""設定 API 金鑰"""
try:
# 設定 Gemini API
if gemini_api_key:
genai.configure(api_key=gemini_api_key)
self.gemini_model = genai.GenerativeModel('gemini-2.5-flash-preview-05-20')
# 設定 Pexels API
if pexels_api_key:
self.pexels_headers = {
"Authorization": pexels_api_key
}
return True, "✅ API 設定成功"
except Exception as e:
return False, f"❌ API 設定失敗:{str(e)}"
def generate_content_with_gemini(self, topic, slide_count=5):
"""使用 Gemini 生成簡報內容"""
prompt = f"""
請為主題「{topic}」製作一個 {slide_count} 頁的簡報大綱,以 JSON 格式回傳。
格式要求:
{{
"title": "簡報主標題",
"subtitle": "簡報副標題",
"title_keywords": "主題相關的英文關鍵字,用於搜尋標題頁和結尾頁圖片",
"slides": [
{{
"title": "投影片標題",
"content": [
"重點1",
"重點2",
"重點3"
],
"image_keywords": "英文關鍵字,用於搜尋相關圖片"
}}
]
}}
要求:
1. 內容要專業且有邏輯性
2. 每頁 3-4 個重點
3. title_keywords 要用英文,描述主題相關的圖片搜尋關鍵字(3-5個詞)
4. image_keywords 要用英文,描述該投影片適合的圖片內容
5. 關鍵字要具體明確,例如 "business meeting", "technology innovation", "data analysis"
6. 使用繁體中文(除了 title_keywords 和 image_keywords)
7. 第一頁是概述介紹,最後一頁是結論總結
8. 請直接回傳 JSON,不要包含其他文字說明,也不可以有"**"等不必要的markdown符號
"""
try:
if self.gemini_model:
response = self.gemini_model.generate_content(prompt)
content = response.text
# 清理回應內容,提取 JSON
content = content.strip()
if content.startswith('```json'):
content = content[7:]
if content.endswith('```'):
content = content[:-3]
# 尋找 JSON 開始和結束位置
start = content.find('{')
end = content.rfind('}') + 1
if start != -1 and end > start:
json_str = content[start:end]
return json.loads(json_str)
else:
raise ValueError("無法在回應中找到有效的 JSON")
else:
return self.get_default_structure_with_images(topic)
except Exception as e:
print(f"Gemini API 錯誤: {e}")
return self.get_default_structure_with_images(topic)
def get_default_structure_with_images(self, topic):
"""預設簡報結構(含圖片關鍵字)"""
# 生成簡單的英文關鍵字
title_keywords = "business presentation professional meeting"
if "科技" in topic or "技術" in topic:
title_keywords = "technology innovation digital development"
elif "教育" in topic or "學習" in topic:
title_keywords = "education learning academic study"
elif "醫療" in topic or "健康" in topic:
title_keywords = "healthcare medical health wellness"
elif "環境" in topic or "環保" in topic:
title_keywords = "environment sustainability green nature"
elif "經濟" in topic or "金融" in topic:
title_keywords = "economics finance business economy"
return {
"title": f"{topic} 簡報",
"subtitle": "由 AI 自動生成",
"title_keywords": title_keywords,
"slides": [
{
"title": "簡介與背景",
"content": [
"主題背景介紹",
"研究目的與範圍",
"簡報架構說明"
],
"image_keywords": "presentation introduction business"
},
{
"title": "主要內容分析",
"content": [
"核心概念說明",
"重要特點分析",
"相關案例討論"
],
"image_keywords": "analysis data research content"
},
{
"title": "深入探討",
"content": [
"優勢與機會識別",
"挑戰與問題分析",
"影響因素評估"
],
"image_keywords": "strategy planning discussion"
},
{
"title": "解決方案與建議",
"content": [
"策略建議提出",
"實施方法規劃",
"預期效果評估"
],
"image_keywords": "solution implementation strategy"
},
{
"title": "結論與展望",
"content": [
"重點總結回顧",
"未來發展趨勢",
"行動建議提出"
],
"image_keywords": "conclusion future success"
}
]
}
def search_pexels_with_style(self, keywords, image_style="professional", per_page=10):
"""根據風格搜尋 Pexels 圖片"""
if not self.pexels_headers:
return None
# 先嘗試純主題關鍵字搜尋
url = "https://api.pexels.com/v1/search"
params = {
"query": keywords,
"per_page": per_page,
"orientation": "landscape",
"size": "medium"
}
try:
response = requests.get(url, headers=self.pexels_headers, params=params)
if response.status_code == 200:
data = response.json()
if data["photos"] and len(data["photos"]) >= 3:
return data["photos"]
# 如果純主題搜尋結果不足,再組合風格關鍵字
style_modifier = self.image_styles.get(image_style, "")
enhanced_keywords = f"{keywords} {style_modifier}"
params["query"] = enhanced_keywords
response = requests.get(url, headers=self.pexels_headers, params=params)
if response.status_code == 200:
data = response.json()
return data["photos"] if data["photos"] else None
return None
except Exception as e:
print(f"Pexels API 錯誤: {e}")
return None
def select_best_image(self, photos, slide_title=""):
"""從多張圖片中選擇最適合的"""
if not photos:
return None
# 選擇解析度較高的圖片
best_photo = photos[0]
for photo in photos[:3]:
if photo["width"] * photo["height"] > best_photo["width"] * best_photo["height"]:
best_photo = photo
return best_photo["src"]["medium"]
def download_image(self, image_url):
"""下載圖片並返回檔案路徑"""
if not image_url:
return None
try:
response = requests.get(image_url)
if response.status_code == 200:
temp_dir = tempfile.mkdtemp()
image_path = os.path.join(temp_dir, "slide_image.jpg")
# 處理圖片
image = Image.open(BytesIO(response.content))
# 調整圖片大小
max_size = (800, 600)
image.thumbnail(max_size, Image.Resampling.LANCZOS)
# 轉換並儲存
if image.mode in ("RGBA", "P"):
image = image.convert("RGB")
image.save(image_path, "JPEG", quality=85)
return image_path
return None
except Exception as e:
print(f"圖片下載錯誤: {e}")
return None
def add_image_to_slide(self, slide, image_path, theme):
"""將圖片添加到投影片,保持比例避免變形"""
if not image_path or not os.path.exists(image_path):
return
try:
image_area = theme["image_area"]
# 目標區域
target_left = Inches(image_area["left"])
target_top = Inches(image_area["top"])
target_width = Inches(image_area["width"])
target_height = Inches(image_area["height"])
# 載入圖片獲取原始尺寸
from PIL import Image as PILImage
with PILImage.open(image_path) as img:
original_width, original_height = img.size
original_ratio = original_width / original_height
# 計算目標比例
target_ratio = target_width.inches / target_height.inches
# 根據比例計算實際顯示尺寸,保持圖片比例
if original_ratio > target_ratio:
# 圖片較寬,以寬度為準
actual_width = target_width
actual_height = Inches(target_width.inches / original_ratio)
# 垂直置中
actual_top = Inches(target_top.inches + (target_height.inches - actual_height.inches) / 2)
actual_left = target_left
else:
# 圖片較高,以高度為準
actual_height = target_height
actual_width = Inches(target_height.inches * original_ratio)
# 水平置中
actual_left = Inches(target_left.inches + (target_width.inches - actual_width.inches) / 2)
actual_top = target_top
# 添加圖片
picture = slide.shapes.add_picture(image_path, actual_left, actual_top, actual_width, actual_height)
except Exception as e:
print(f"添加圖片錯誤: {e}")
# 降級處理:如果計算失敗,使用原來的方式
try:
left = Inches(image_area["left"])
top = Inches(image_area["top"])
width = Inches(image_area["width"])
height = Inches(image_area["height"])
slide.shapes.add_picture(image_path, left, top, width, height)
except:
pass
def setup_slide_content(self, slide, slide_data, theme):
"""設定投影片內容,使用正確的位置"""
try:
# 設置背景和裝飾元素
self.theme_manager.setup_slide_background_and_layout(slide, theme)
# 設定標題
title_shape = slide.shapes.title
title_shape.text = slide_data["title"]
# 調整標題位置和尺寸
title_area = theme["title_area"]
title_shape.left = Inches(title_area["left"])
title_shape.top = Inches(title_area["top"])
title_shape.width = Inches(title_area["width"])
title_shape.height = Inches(title_area["height"])
self.theme_manager.format_title(title_shape, theme, 34, self.get_font_name)
# 移除預設內容佔位符(如果存在)
shapes_to_remove = []
for shape in slide.shapes:
try:
if hasattr(shape, 'placeholder_format') and shape.placeholder_format is not None:
if shape.placeholder_format.type == 2: # 內容佔位符
shapes_to_remove.append(shape)
except:
continue
for shape in shapes_to_remove:
try:
sp = shape.element
sp.getparent().remove(sp)
except:
continue
# 設定內容區域
content_area = theme["content_area"]
# 創建帶背景的內容框
self.theme_manager.create_content_box_with_background(slide, theme, content_area)
# 創建新的內容文字框
left = Inches(content_area["left"])
top = Inches(content_area["top"])
width = Inches(content_area["width"])
height = Inches(content_area["height"])
textbox = slide.shapes.add_textbox(left, top, width, height)
text_frame = textbox.text_frame
# 設定文字框屬性
text_frame.margin_left = Inches(0.15)
text_frame.margin_right = Inches(0.15)
text_frame.margin_top = Inches(0.1)
text_frame.margin_bottom = Inches(0.1)
text_frame.word_wrap = True
text_frame.auto_size = None # 不自動調整大小
# 清除預設文字
text_frame.clear()
# 添加內容
for i, point in enumerate(slide_data["content"]):
if i == 0:
p = text_frame.paragraphs[0]
else:
p = text_frame.add_paragraph()
p.text = f"• {point}"
p.level = 0
p.space_after = Pt(10) # 段落間距
self.theme_manager.format_content(p, theme, 22, self.get_font_name)
except Exception as e:
print(f"設定投影片內容錯誤詳細: {str(e)}")
import traceback
print(f"錯誤追蹤: {traceback.format_exc()}")
def adjust_content_layout(self, slide, layout_type):
"""這個方法已被 setup_slide_content 取代,保留以免錯誤"""
pass
def get_font_name(self):
"""獲取中文字型名稱"""
# 檢查是否有自定義中文字型檔案
font_path = os.path.join(os.path.dirname(__file__), "cht.ttf")
if os.path.exists(font_path):
return "cht" # 使用自定義字型
else:
# 備用字型選擇
return "Arial Unicode MS" # 通用 Unicode 字型
def format_title_with_shadow(self, shape, theme, font_size):
"""格式化標題並添加陰影效果以提高可讀性"""
self.theme_manager.format_title(shape, theme, font_size, self.get_font_name)
try:
paragraph = shape.text_frame.paragraphs[0]
paragraph.font.color.rgb = RGBColor(255, 255, 255) # 白色文字在深色背景上更清楚
paragraph.alignment = PP_ALIGN.CENTER
paragraph.font.bold = True
except:
pass
def create_presentation_with_images(self, topic, theme_name="商務專業",
slide_count=5, image_style="professional"):
"""建立包含圖片的簡報"""
# 生成內容結構
structure = self.generate_content_with_gemini(topic, slide_count)
theme = self.theme_manager.get_theme(theme_name)
# 建立 16:9 簡報
prs = Presentation()
prs.slide_width = self.slide_width
prs.slide_height = self.slide_height
# 建立標題頁
title_slide = prs.slides.add_slide(prs.slide_layouts[0])
title_shape = title_slide.shapes.title
subtitle_shape = title_slide.placeholders[1]
title_shape.text = structure["title"]
subtitle_shape.text = structure["subtitle"]
# 調整標題頁版面 (16:9)
title_shape.left = Inches(1.0)
title_shape.top = Inches(2.0)
title_shape.width = Inches(11.333)
title_shape.height = Inches(1.5)
subtitle_shape.left = Inches(1.0)
subtitle_shape.top = Inches(4.0)
subtitle_shape.width = Inches(11.333)
subtitle_shape.height = Inches(1.0)
# 格式化標題頁 - 加強文字可讀性
self.format_title_with_shadow(title_shape, theme, 54)
self.format_title_with_shadow(subtitle_shape, theme, 32)
# 為標題頁添加主題相關圖片 - 使用AI生成的英文關鍵字
main_keywords = structure.get("title_keywords", f"{topic} introduction overview")
title_photos = self.search_pexels_with_style(main_keywords, image_style, per_page=15)
if title_photos:
title_image_url = self.select_best_image(title_photos, structure["title"])
if title_image_url:
title_image_path = self.download_image(title_image_url)
if title_image_path:
# 標題頁使用半透明背景
self.add_title_background_with_overlay(title_slide, title_image_path, theme)
# 建立內容頁
for i, slide_data in enumerate(structure["slides"]):
slide = prs.slides.add_slide(prs.slide_layouts[1])
# 設定內容和版面
self.setup_slide_content(slide, slide_data, theme)
# 搜尋並添加圖片
keywords = slide_data.get("image_keywords", f"{topic} slide {i+1}")
photos = self.search_pexels_with_style(keywords, image_style)
if photos:
image_url = self.select_best_image(photos, slide_data["title"])
if image_url:
image_path = self.download_image(image_url)
if image_path:
self.add_image_to_slide(slide, image_path, theme)
# 建立感謝頁
self.add_thank_you_slide(prs, theme, image_style, topic, structure)
return prs, structure
def search_pexels_image_for_title(self, keywords, topic, per_page=10):
"""專門為標題頁搜尋圖片,優先考慮主題相關性"""
if not self.pexels_headers:
return None
# 先嘗試純主題搜尋
topic_keywords = f"{topic} background"
url = "https://api.pexels.com/v1/search"
params = {
"query": topic_keywords,
"per_page": per_page,
"orientation": "landscape",
"size": "medium"
}
try:
response = requests.get(url, headers=self.pexels_headers, params=params)
if response.status_code == 200:
data = response.json()
if data["photos"]:
return data["photos"]
# 如果主題搜尋沒結果,使用通用關鍵字
fallback_params = {
"query": "professional presentation background",
"per_page": per_page,
"orientation": "landscape",
"size": "medium"
}
response = requests.get(url, headers=self.pexels_headers, params=fallback_params)
if response.status_code == 200:
data = response.json()
return data["photos"] if data["photos"] else None
return None
except Exception as e:
print(f"Pexels API 錯誤: {e}")
return None
def add_title_background_with_overlay(self, slide, image_path, theme):
"""為標題頁添加帶有文字背景框的背景圖片"""
try:
# 添加背景圖片
picture = slide.shapes.add_picture(
image_path,
Inches(0),
Inches(0),
self.slide_width,
self.slide_height
)
# 移到背景層
picture.element.getparent().remove(picture.element)
slide.shapes._spTree.insert(2, picture.element)
except Exception as e:
print(f"添加標題背景錯誤: {e}")
# 降級處理:直接添加背景圖片
try:
picture = slide.shapes.add_picture(
image_path,
Inches(0),
Inches(0),
self.slide_width,
self.slide_height
)
picture.element.getparent().remove(picture.element)
slide.shapes._spTree.insert(2, picture.element)
except:
pass
def add_title_background(self, slide, image_path):
"""為標題頁添加背景圖片(保留原方法以免錯誤)"""
self.add_title_background_with_overlay(slide, image_path, None)
def add_thank_you_slide(self, prs, theme, image_style, topic, structure):
"""添加感謝頁"""
thank_slide = prs.slides.add_slide(prs.slide_layouts[5])
# 感謝頁使用主題關鍵字加上結尾相關詞彙
title_keywords = structure.get("title_keywords", f"{topic} success conclusion achievement")
thank_keywords = f"{title_keywords} success conclusion achievement"
thank_photos = self.search_pexels_with_style(thank_keywords, image_style, per_page=12)
if thank_photos:
thank_image_url = self.select_best_image(thank_photos)
if thank_image_url:
thank_image_path = self.download_image(thank_image_url)
if thank_image_path:
self.add_title_background_with_overlay(thank_slide, thank_image_path, theme)
# 添加感謝文字背景框
text_bg = thank_slide.shapes.add_shape(
1, # 矩形
Inches(2.5),
Inches(2.0),
Inches(8.333),
Inches(3.5)
)
fill = text_bg.fill
fill.solid()
fill.fore_color.rgb = RGBColor(255, 255, 255) # 白色背景
text_bg.line.color.rgb = theme["accent_color"] if theme else RGBColor(79, 129, 189)
text_bg.line.width = Pt(3)
# 添加感謝文字 (16:9 居中位置)
left = Inches(3.0)
top = Inches(2.5)
width = Inches(7.333)
height = Inches(2.5)
textbox = thank_slide.shapes.add_textbox(left, top, width, height)
text_frame = textbox.text_frame
text_frame.text = "謝謝聆聽\nThank You"
for paragraph in text_frame.paragraphs:
paragraph.font.name = self.get_font_name()
paragraph.font.size = Pt(60)
paragraph.font.color.rgb = theme["title_color"] if theme else RGBColor(31, 73, 125)
paragraph.alignment = PP_ALIGN.CENTER
paragraph.font.bold = True
def save_presentation(self, prs, filename):
"""儲存簡報"""
temp_dir = tempfile.mkdtemp()
filepath = os.path.join(temp_dir, filename)
prs.save(filepath)
return filepath
def generate_preview_text(self, structure):
"""生成簡報預覽文字"""
preview = f"📊 {structure['title']}\n"
preview += f" {structure['subtitle']}\n\n"
for i, slide in enumerate(structure['slides'], 1):
preview += f"{i}. {slide['title']}\n"
for point in slide['content'][:2]: # 只顯示前兩個重點
preview += f" • {point}\n"
if len(slide['content']) > 2:
preview += f" • ...(共 {len(slide['content'])} 個重點)\n"
preview += "\n"
return preview
def analyze_and_restyle_ppt(gemini_api_key, pexels_api_key, uploaded_file, theme_name, image_style):
"""分析並重新設計上傳的簡報"""
if not uploaded_file:
return None, "", "❌ 請上傳PPT文件"
generator = GeminiPPTGenerator()
# 如果API金鑰為空,嘗試從已保存的配置載入
if not gemini_api_key.strip() or not pexels_api_key.strip():
saved_gemini, saved_pexels = generator.get_saved_keys()
if not gemini_api_key.strip():
gemini_api_key = saved_gemini
if not pexels_api_key.strip():
pexels_api_key = saved_pexels
# 檢查輸入
if not gemini_api_key.strip():
return None, "", "❌ 請輸入 Gemini API 金鑰"
if not pexels_api_key.strip():
return None, "", "❌ 請輸入 Pexels API 金鑰"
try:
# 設定 API
success, message = generator.setup_apis(gemini_api_key, pexels_api_key)
if not success:
return None, "", message
# 創建分析器
analyzer = PPTAnalyzer(
gemini_model=generator.gemini_model,
pexels_headers=generator.pexels_headers,
image_styles=generator.image_styles
)
# 分析上傳的PPT
analysis_result = analyzer.analyze_ppt_file(uploaded_file.name)
if not analysis_result:
return None, "", "❌ 無法分析PPT文件,請確認文件格式正確"
# 套用新主題和添加圖片
processed_prs, processed_slides = analyzer.apply_theme_to_presentation(
uploaded_file.name, theme_name, image_style, analysis_result
)
if not processed_prs:
return None, "", "❌ 處理PPT文件時發生錯誤"
# 生成分析報告
report = analyzer.generate_analysis_report(analysis_result, processed_slides)
# 儲存處理後的簡報
original_name = os.path.splitext(os.path.basename(uploaded_file.name))[0]
filename = f"{original_name}_{theme_name}_{image_style}_restyled.pptx"
output_path = analyzer.save_processed_presentation(processed_prs, filename)
if not output_path:
return None, "", "❌ 儲存處理後的簡報時發生錯誤"
success_msg = f"✅ 成功重新設計《{original_name}》!\n"
success_msg += f"🎨 套用主題:{theme_name}\n"
success_msg += f"🖼️ 圖片風格:{image_style}\n"
success_msg += f"📄 處理了 {len(processed_slides)} 張投影片"
return output_path, report, success_msg
except Exception as e:
import traceback
error_details = traceback.format_exc()
print(f"詳細錯誤: {error_details}")
return None, "", f"❌ 處理失敗:{str(e)}"
def generate_ppt_with_gemini(gemini_api_key, pexels_api_key, topic, theme, slide_count, image_style):
"""生成簡報的主要函數"""
generator = GeminiPPTGenerator()
# 如果API金鑰為空,嘗試從已保存的配置載入
if not gemini_api_key.strip() or not pexels_api_key.strip():
saved_gemini, saved_pexels = generator.get_saved_keys()
if not gemini_api_key.strip():
gemini_api_key = saved_gemini
if not pexels_api_key.strip():
pexels_api_key = saved_pexels
# 檢查輸入
if not gemini_api_key.strip():
return None, "", "❌ 請輸入 Gemini API 金鑰"
if not pexels_api_key.strip():
return None, "", "❌ 請輸入 Pexels API 金鑰"
if not topic.strip():
return None, "", "❌ 請輸入簡報主題"
try:
# 設定 API
success, message = generator.setup_apis(gemini_api_key, pexels_api_key)
if not success:
return None, "", message
# 保存API金鑰到配置檔案
generator.save_config(gemini_api_key, pexels_api_key)
# 生成簡報
prs, structure = generator.create_presentation_with_images(
topic, theme, slide_count, image_style
)
# 生成預覽
preview = generator.generate_preview_text(structure)
# 儲存檔案
filename = f"{topic.replace(' ', '_')}_{image_style}_簡報.pptx"
filepath = generator.save_presentation(prs, filename)
success_msg = f"✅ 成功生成《{topic}{image_style}風格簡報!({slide_count} 頁,含圖片)"
return filepath, preview, success_msg
except Exception as e:
import traceback
error_details = traceback.format_exc()
print(f"詳細錯誤: {error_details}")
return None, "", f"❌ 生成失敗:{str(e)}"
# Gradio 介面
def create_gemini_interface():
"""建立 Gradio 介面"""
# 檢查是否已有保存的API金鑰
generator = GeminiPPTGenerator()
saved_gemini, saved_pexels = generator.get_saved_keys()
keys_exist = bool(saved_gemini and saved_pexels)
with gr.Blocks(title="Gemini AI 圖文簡報生成器", theme=gr.themes.Soft()) as iface:
gr.Markdown("# 🤖 Gemini AI 智能圖文簡報生成器")
gr.Markdown("**使用 Google Gemini 2.0 + Pexels 圖庫**,智能生成專業圖文簡報,或改造現有簡報")
# API 設定區域(共用)
if keys_exist:
with gr.Group():
gr.Markdown("### ✅ API 金鑰已配置")
gr.Markdown("API 金鑰已從 config.json 載入,可直接使用。如需更新金鑰,請刪除 config.json 檔案後重新啟動。")
# 隱藏的輸入框,用於傳遞已保存的金鑰
gemini_api_input = gr.Textbox(value=saved_gemini, visible=False)
pexels_api_input = gr.Textbox(value=saved_pexels, visible=False)
else:
with gr.Group():
gr.Markdown("### 🔑 API 設定")
with gr.Row():
gemini_api_input = gr.Textbox(
label="🤖 Gemini API Key",
placeholder="請輸入你的 Gemini API 金鑰",
type="password",
info="免費額度,前往 https://ai.google.dev/ 獲取"
)
pexels_api_input = gr.Textbox(
label="📸 Pexels API Key",
placeholder="請輸入你的 Pexels API 金鑰",
type="password",
info="免費 200次/月,前往 https://www.pexels.com/api/ 獲取"
)
# 選項卡
with gr.Tabs():
# 原有的生成功能
with gr.TabItem("🆕 創建新簡報"):
# 主要設定區域
with gr.Row():
with gr.Column(scale=2):
topic_input = gr.Textbox(
label="📝 簡報主題",
placeholder="請輸入具體的簡報主題...",
value="人工智慧在現代教育中的應用與挑戰"
)
with gr.Row():
# 從主題管理器獲取所有主題名稱
generator = GeminiPPTGenerator()
theme_dropdown = gr.Dropdown(
choices=generator.theme_manager.get_all_theme_names(),
value="商務專業",
label="🎨 版型風格"
)
image_style_dropdown = gr.Dropdown(
choices=["professional", "creative", "minimalist", "modern", "natural", "technology"],
value="professional",
label="🖼️ 圖片風格"
)
slide_count = gr.Slider(
minimum=3,
maximum=20,
value=6,
step=1,
label="📄 投影片數量"
)
generate_btn = gr.Button("🚀 生成專業簡報", variant="primary", size="lg")
with gr.Column(scale=1):
status_output = gr.Textbox(label="📊 生成狀態", interactive=False)
file_output = gr.File(label="📁 下載簡報")
# 預覽區域
with gr.Group():
gr.Markdown("### 📋 簡報預覽")
preview_output = gr.Textbox(
label="內容大綱",
placeholder="生成後將顯示簡報大綱...",
lines=8,
interactive=False
)
# 新增的簡報改造功能
with gr.TabItem("🔄 改造現有簡報"):
gr.Markdown("### 📤 上傳並改造您的簡報")
gr.Markdown("上傳現有的PPT文件,AI將分析內容並套用新的版型設計,自動為每頁添加相關圖片")
with gr.Row():
with gr.Column(scale=2):
# 文件上傳
upload_file = gr.File(
label="📎 上傳PPT文件",
file_types=[".pptx", ".ppt"],
type="filepath"
)
with gr.Row():
# 主題選擇
upload_theme_dropdown = gr.Dropdown(
choices=generator.theme_manager.get_all_theme_names(),
value="商務專業",
label="🎨 套用版型風格"
)
upload_image_style_dropdown = gr.Dropdown(
choices=["professional", "creative", "minimalist", "modern", "natural", "technology"],
value="professional",
label="🖼️ 圖片風格"
)
analyze_btn = gr.Button("🔍 分析並改造簡報", variant="primary", size="lg")
with gr.Column(scale=1):
upload_status_output = gr.Textbox(label="📊 處理狀態", interactive=False)
upload_file_output = gr.File(label="📁 下載改造後簡報")
# 分析報告區域
with gr.Group():
gr.Markdown("### 📋 分析報告")
analysis_report = gr.Textbox(
label="處理詳情",
placeholder="上傳並處理後將顯示分析報告...",
lines=8,
interactive=False
)
# 說明區域
with gr.Accordion("📖 使用說明與功能特色", open=False):
gr.Markdown("""
### 🌟 核心特色
#### 🤖 Google Gemini 2.0 Flash
- **最新模型**:使用 Gemini 2.0 Flash Preview 版本
- **免費額度**:Google 提供慷慨的免費使用額度
- **中文優化**:對繁體中文有優秀的理解和生成能力
- **結構化輸出**:精確生成 JSON 格式的簡報結構
- **內容分析**:智能分析現有簡報內容,生成適合的圖片搜尋關鍵字
#### 📸 Pexels 圖片整合
- **百萬圖庫**:Pexels 提供高品質免費圖片
- **智能匹配**:AI 為每頁生成最適合的搜尋關鍵字
- **風格選擇**:6 種圖片風格滿足不同需求
- **自動配圖**:每張投影片自動配上相關圖片
- **智能避重**:首頁和結尾使用不同關鍵字避免重複圖片
#### 🎨 專業版面設計
- **8 種版型**:商務、科技、創意、學術、簡約、橙色、紫色、藍綠風格
- **智能排版**:根據版型自動調整圖文位置
- **色彩搭配**:專業的色彩主題設計,高對比度確保文字清晰
- **中文字型**:完美支援繁體中文顯示
- **背景漸變**:精美的漸變背景和裝飾元素
#### 🔄 簡報改造功能
- **檔案分析**:智能分析上傳的PPT文件結構和內容
- **表格檢測**:自動識別包含表格的投影片,只套用配色不添加圖片
- **版型套用**:將現有簡報套用全新的專業版型設計
- **AI配圖**:為每頁內容生成專屬的圖片搜尋關鍵字並自動配圖
- **空間計算**:智能計算可用空間,合理放置圖片避免覆蓋原有內容
### 📋 使用步驟
#### 🆕 創建新簡報
1. **獲取 API 金鑰**:
- Gemini API:前往 [Google AI Studio](https://ai.google.dev/) 免費申請
- Pexels API:前往 [Pexels API](https://www.pexels.com/api/) 免費申請(200次/日)
2. **輸入 API 金鑰**:在上方輸入框中填入你的 API 金鑰(僅需輸入一次,會自動保存到 config.json)
3. **設定簡報參數**:
- 輸入具體明確的簡報主題
- 選擇適合的版型和圖片風格
- 設定所需的投影片數量
4. **生成簡報**:點擊生成按鈕,系統將自動完成所有工作
5. **下載使用**:獲得完整的 .pptx 檔案,可直接在 PowerPoint 中使用
#### 🔄 改造現有簡報
1. **上傳PPT文件**:支援 .pptx 和 .ppt 格式
2. **選擇版型風格**:從8種專業版型中選擇適合的風格
3. **選擇圖片風格**:選擇與內容匹配的圖片風格
4. **開始分析改造**:AI將自動分析每頁內容並套用新設計
5. **查看分析報告**:了解每頁的處理詳情和圖片添加情況
6. **下載改造後簡報**:獲得全新設計的簡報文件
### 💡 專業建議
- **主題要具體**:「AI在醫療診斷的應用」比「人工智慧」效果更好
- **選對風格**:商務場合用「professional」,創意展示用「creative」
- **適當頁數**:建議 5-8 頁,內容豐富但不冗長
- **測試 API**:第一次使用建議先測試 API 連接是否正常
### 🔧 技術特點
- **純 Python 實現**:不需要安裝 Microsoft Office
- **即時生成**:通常 30-60 秒完成整個簡報
- **高品質輸出**:生成的 .pptx 檔案完全相容 PowerPoint
- **跨平台支援**:Windows、macOS、Linux 都能正常使用
""")
# 事件綁定
generate_btn.click(
fn=generate_ppt_with_gemini,
inputs=[
gemini_api_input,
pexels_api_input,
topic_input,
theme_dropdown,
slide_count,
image_style_dropdown
],
outputs=[file_output, preview_output, status_output]
)
analyze_btn.click(
fn=analyze_and_restyle_ppt,
inputs=[
gemini_api_input,
pexels_api_input,
upload_file,
upload_theme_dropdown,
upload_image_style_dropdown
],
outputs=[upload_file_output, analysis_report, upload_status_output]
)
return iface
if __name__ == "__main__":
# 啟動應用
iface = create_gemini_interface()
iface.launch(
server_name="127.0.0.1",
server_port=7860,
share=False,
inbrowser=True
)