import openai
import gradio as gr
import fitz # PyMuPDF
from openai import OpenAI
import os
import tempfile
import traceback
# 全域變數
api_key = ""
selected_model = "gpt-4"
summary_text = ""
client = None
pdf_text = ""
def set_api_key(user_api_key):
"""設定 OpenAI API Key 並初始化客戶端"""
global api_key, client
try:
api_key = user_api_key.strip()
if not api_key:
return "❌ API Key 不能為空"
client = OpenAI(api_key=api_key)
# 測試 API Key 是否有效
test_response = client.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": "你好"}],
max_tokens=5
)
return "✅ API Key 已設定並驗證成功"
except Exception as e:
return f"❌ API Key 設定失敗: {str(e)}"
def set_model(model_name):
"""設定選擇的模型"""
global selected_model
selected_model = model_name
return f"✅ 模型已選擇:{model_name}"
def extract_pdf_text(file_path):
"""從 PDF 文件中提取文字"""
try:
doc = fitz.open(file_path)
text = ""
for page_num, page in enumerate(doc):
page_text = page.get_text()
if page_text.strip(): # 只添加非空白頁面
text += f"\n--- 第 {page_num + 1} 頁 ---\n"
text += page_text
doc.close()
return text
except Exception as e:
return f"❌ PDF 解析錯誤: {str(e)}"
def generate_summary(pdf_file):
"""從 PDF 內容生成摘要"""
global summary_text, pdf_text
if not client:
return "❌ 請先設定 OpenAI API Key"
if not pdf_file:
return "❌ 請先上傳 PDF 文件"
try:
# 從 PDF 提取文字
pdf_text = extract_pdf_text(pdf_file.name)
if not pdf_text.strip():
return "⚠️ 無法解析 PDF 文字,可能為純圖片 PDF 或空白文件。"
# 檢查文字長度,必要時截斷
max_chars = 8000 # 為系統提示留出空間
if len(pdf_text) > max_chars:
pdf_text_truncated = pdf_text[:max_chars] + "\n\n[文本已截斷,僅顯示前 8000 字符]"
else:
pdf_text_truncated = pdf_text
# 生成摘要
response = client.chat.completions.create(
model=selected_model,
messages=[
{
"role": "system",
"content": """你是一個專業的文檔摘要助手。請將以下 PDF 內容整理為結構化的摘要:
1. 首先提供一個簡短的總體概述
2. 然後按照重要性列出主要重點(使用項目符號)
3. 如果有數據或統計信息,請特別標注
4. 如果有結論或建議,請單獨列出
請用繁體中文回答,保持專業且易於理解的語調。"""
},
{"role": "user", "content": pdf_text_truncated}
],
temperature=0.3
)
summary_text = response.choices[0].message.content
return summary_text
except Exception as e:
error_msg = f"❌ 摘要生成失敗: {str(e)}"
print(f"錯誤詳情: {traceback.format_exc()}")
return error_msg
def ask_question(user_question):
"""基於 PDF 內容回答問題"""
if not client:
return "❌ 請先設定 OpenAI API Key"
if not summary_text and not pdf_text:
return "❌ 請先生成 PDF 摘要"
if not user_question.strip():
return "❌ 請輸入問題"
try:
# 使用摘要和原始文本來提供更好的上下文
context = f"PDF 摘要:\n{summary_text}\n\n原始內容(部分):\n{pdf_text[:2000]}"
response = client.chat.completions.create(
model=selected_model,
messages=[
{
"role": "system",
"content": f"""你是一個專業的文檔問答助手。請基於提供的 PDF 內容回答用戶問題。
規則:
1. 只根據提供的文檔內容回答
2. 如果文檔中沒有相關信息,請明確說明
3. 引用具體的文檔內容來支持你的回答
4. 用繁體中文回答
5. 保持客觀和準確
文檔內容:
{context}"""
},
{"role": "user", "content": user_question}
],
temperature=0.2
)
return response.choices[0].message.content
except Exception as e:
error_msg = f"❌ 問答生成失敗: {str(e)}"
print(f"錯誤詳情: {traceback.format_exc()}")
return error_msg
def clear_all():
"""清除所有資料"""
global summary_text, pdf_text
summary_text = ""
pdf_text = ""
return "", "", ""
# 創建 Gradio 介面
with gr.Blocks(
theme=gr.themes.Soft(),
title="PDF 摘要助手",
css="""
/* 修復頁面大小問題 */
.gradio-container {
max-width: none !important;
width: 100% !important;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
min-height: 100vh !important;
padding: 0 !important;
margin: 0 !important;
}
/* 主要內容區域 - 修復大小問題 */
.main-content {
background: rgba(255, 255, 255, 0.95) !important;
border-radius: 20px !important;
margin: 20px auto !important;
padding: 30px !important;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1) !important;
backdrop-filter: blur(10px) !important;
max-width: 1400px !important;
width: calc(100% - 40px) !important;
}
/* 標題樣式 */
.main-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
-webkit-background-clip: text !important;
-webkit-text-fill-color: transparent !important;
background-clip: text !important;
text-align: center !important;
font-size: 2.5em !important;
font-weight: bold !important;
margin-bottom: 20px !important;
}
/* 分頁導航 - 修復點擊問題 */
.gradio-tabs {
border-radius: 15px !important;
overflow: hidden !important;
}
.gradio-tabitem {
padding: 25px !important;
background: white !important;
border-radius: 0 0 15px 15px !important;
}
/* 輸入框樣式 - 修復交互問題 */
input[type="text"],
input[type="password"],
textarea {
border: 2px solid #e0e6ff !important;
border-radius: 12px !important;
padding: 15px !important;
font-size: 16px !important;
transition: all 0.3s ease !important;
width: 100% !important;
box-sizing: border-box !important;
}
input[type="text"]:focus,
input[type="password"]:focus,
textarea:focus {
border-color: #667eea !important;
box-shadow: 0 0 20px rgba(102, 126, 234, 0.3) !important;
outline: none !important;
}
/* 按鈕樣式 - 修復點擊問題 */
button,
.gr-button,
input[type="submit"],
input[type="button"] {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
border: none !important;
border-radius: 12px !important;
color: white !important;
font-weight: 600 !important;
padding: 15px 30px !important;
font-size: 16px !important;
transition: all 0.3s ease !important;
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4) !important;
cursor: pointer !important;
pointer-events: auto !important;
z-index: 1000 !important;
position: relative !important;
display: inline-block !important;
min-height: 44px !important;
line-height: normal !important;
}
button:hover,
.gr-button:hover,
input[type="submit"]:hover,
input[type="button"]:hover {
transform: translateY(-2px) !important;
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.6) !important;
background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%) !important;
}
button:active,
.gr-button:active,
input[type="submit"]:active,
input[type="button"]:active {
transform: translateY(0px) !important;
box-shadow: 0 3px 10px rgba(102, 126, 234, 0.4) !important;
}
/* 次要按鈕樣式 */
button[data-testid*="secondary"],
.gr-button.secondary,
button.secondary {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%) !important;
}
button[data-testid*="secondary"]:hover,
.gr-button.secondary:hover,
button.secondary:hover {
background: linear-gradient(135deg, #e081e9 0%, #e3455a 100%) !important;
}
/* 修復 Gradio 特定的按鈕容器 */
.gr-form > div,
.gr-button-group,
div[data-testid="button"] {
pointer-events: auto !important;
z-index: 999 !important;
}
/* 確保按鈕內的文字可點擊 */
button span,
.gr-button span {
pointer-events: none !important;
user-select: none !important;
}
/* 文件上傳區域 */
.file-upload-area {
border: 3px dashed #667eea !important;
border-radius: 15px !important;
background: rgba(102, 126, 234, 0.05) !important;
padding: 40px !important;
text-align: center !important;
transition: all 0.3s ease !important;
min-height: 120px !important;
}
.file-upload-area:hover {
background: rgba(102, 126, 234, 0.1) !important;
border-color: #764ba2 !important;
}
/* 單選按鈕容器 */
.radio-group {
background: rgba(102, 126, 234, 0.05) !important;
border-radius: 12px !important;
padding: 20px !important;
margin: 10px 0 !important;
}
/* 輸出文本區域 */
.output-text {
background: #f8f9ff !important;
border: 1px solid #e0e6ff !important;
border-radius: 12px !important;
padding: 20px !important;
min-height: 200px !important;
}
/* 隱藏 Gradio logo 和 footer */
footer,
.gradio-container footer,
div[class*="footer"],
div[class*="Footer"],
.gr-footer {
display: none !important;
}
/* 修復響應式問題 */
.gr-row {
display: flex !important;
gap: 20px !important;
width: 100% !important;
}
.gr-column {
flex: 1 !important;
min-width: 0 !important;
}
/* 確保所有交互元素正常工作 */
* {
pointer-events: auto !important;
}
/* 特殊修復:覆蓋可能的 Gradio 樣式衝突 */
.gradio-container * {
pointer-events: auto !important;
}
/* 修復單選按鈕 */
input[type="radio"] {
pointer-events: auto !important;
cursor: pointer !important;
z-index: 1000 !important;
position: relative !important;
}
/* 修復文件上傳 */
input[type="file"] {
pointer-events: auto !important;
cursor: pointer !important;
z-index: 1000 !important;
}
/* 添加 JavaScript 來確保按鈕響應 */