3a05chatgpt's picture
Update app.py
c0edcf5 verified
raw
history blame
16.4 kB
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: 100vw !important;
height: 100vh !important;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
margin: 0 !important;
padding: 0 !important;
}
/* 主要內容區域 - 全螢幕 */
.main-content {
background: rgba(255, 255, 255, 0.95) !important;
border-radius: 20px !important;
margin: 15px !important;
padding: 30px !important;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1) !important;
backdrop-filter: blur(10px) !important;
width: calc(100vw - 30px) !important;
min-height: calc(100vh - 30px) !important;
box-sizing: border-box !important;
}
/* 標題樣式 */
.main-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
-webkit-background-clip: text !important;
-webkit-text-fill-color: transparent !important;
text-align: center !important;
font-size: 2.8em !important;
font-weight: bold !important;
margin-bottom: 25px !important;
}
/* 分頁導航 */
.gradio-tabs {
border-radius: 15px !important;
overflow: hidden !important;
width: 100% !important;
}
.gradio-tabitem {
padding: 30px !important;
background: white !important;
border-radius: 0 0 15px 15px !important;
width: 100% !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 {
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;
min-height: 50px !important;
margin: 8px 4px !important;
}
button:hover {
transform: translateY(-2px) !important;
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.6) !important;
}
/* 次要按鈕 */
button[variant="secondary"] {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%) !important;
}
button[variant="secondary"]:hover {
background: linear-gradient(135deg, #e081e9 0%, #e3455a 100%) !important;
}
/* 文件上傳區域 */
.file-upload {
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;
width: 100% !important;
}
.file-upload:hover {
background: rgba(102, 126, 234, 0.1) !important;
border-color: #764ba2 !important;
}
/* 行和列的布局 */
.gr-row {
display: flex !important;
gap: 20px !important;
width: 100% !important;
margin: 15px 0 !important;
}
.gr-column {
flex: 1 !important;
min-width: 0 !important;
}
/* 文字輸出區域 */
.gr-textbox {
width: 100% !important;
}
/* 隱藏所有 footer 和 logo */
footer,
.gradio-container footer,
div[class*="footer"],
div[class*="Footer"],
.gr-footer,
.gradio-footer {
display: none !important;
}
/* 響應式設計 */
@media (max-width: 768px) {
.main-content {
margin: 5px !important;
padding: 15px !important;
width: calc(100vw - 10px) !important;
}
.main-header {
font-size: 2em !important;
}
.gr-row {
flex-direction: column !important;
}
button {
width: 100% !important;
margin: 5px 0 !important;
}
}
"""
) as demo:
with gr.Column(elem_classes="main-content"):
gr.HTML("""
<div class="main-header">📄 PDF 摘要 & 問答助手</div>
<div style="text-align: center; margin-bottom: 30px; padding: 25px; background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%); border-radius: 15px; border-left: 5px solid #667eea;">
<h3 style="color: #667eea; margin-bottom: 15px;">🚀 歡迎使用 PDF 智能分析工具!</h3>
<div style="display: flex; justify-content: space-around; flex-wrap: wrap; margin: 20px 0;">
<div style="margin: 10px; padding: 15px; background: white; border-radius: 10px; box-shadow: 0 3px 10px rgba(0,0,0,0.1); min-width: 200px; flex: 1; max-width: 300px;">
<div style="font-size: 24px; margin-bottom: 10px;">📋</div>
<strong>智能摘要生成</strong><br>
<span style="color: #666;">自動分析 PDF 內容並生成結構化摘要</span>
</div>
<div style="margin: 10px; padding: 15px; background: white; border-radius: 10px; box-shadow: 0 3px 10px rgba(0,0,0,0.1); min-width: 200px; flex: 1; max-width: 300px;">
<div style="font-size: 24px; margin-bottom: 10px;">🤖</div>
<strong>AI 問答系統</strong><br>
<span style="color: #666;">基於文檔內容回答您的問題</span>
</div>
<div style="margin: 10px; padding: 15px; background: white; border-radius: 10px; box-shadow: 0 3px 10px rgba(0,0,0,0.1); min-width: 200px; flex: 1; max-width: 300px;">
<div style="font-size: 24px; margin-bottom: 10px;">💡</div>
<strong>快速理解</strong><br>
<span style="color: #666;">快速掌握長篇文檔的核心內容</span>
</div>
</div>
<div style="background: rgba(255, 193, 7, 0.1); padding: 15px; border-radius: 10px; border-left: 4px solid #ffc107; margin-top: 20px;">
<strong style="color: #e65100;">⚠️ 重要提醒:</strong> 使用前請先在「🔧 設定」頁面輸入您的 OpenAI API Key
</div>
</div>
""")
with gr.Tab("🔧 設定"):
gr.Markdown("### ⚙️ 基本設定")
with gr.Row():
with gr.Column():
api_key_input = gr.Textbox(
label="🔑 輸入 OpenAI API Key",
type="password",
placeholder="請輸入您的 OpenAI API Key (sk-...)"
)
api_key_status = gr.Textbox(
label="📊 API 連接狀態",
interactive=False,
value="🔄 等待設定 API Key..."
)
with gr.Column():
model_choice = gr.Radio(
["gpt-4", "gpt-4.1", "gpt-4.5"],
label="🤖 選擇 AI 模型",
value="gpt-4"
)
model_status = gr.Textbox(
label="🎯 當前模型",
interactive=False,
value="✅ 已選擇:gpt-4"
)
with gr.Tab("📄 PDF 處理"):
gr.Markdown("### 📁 文件上傳與摘要生成")
with gr.Row():
with gr.Column():
pdf_upload = gr.File(
label="📎 選擇 PDF 文件",
file_types=[".pdf"]
)
gr.Markdown("**支援格式**:PDF 文件 (建議 < 10MB)")
with gr.Row():
summary_btn = gr.Button(
"🔄 開始生成摘要",
variant="primary"
)
clear_btn = gr.Button(
"🗑️ 清除所有資料",
variant="secondary"
)
with gr.Column():
summary_output = gr.Textbox(
label="📋 AI 生成的文檔摘要",
lines=20,
placeholder="📄 上傳 PDF 文件並點擊「開始生成摘要」按鈕,AI 將為您分析文檔內容..."
)
with gr.Tab("❓ 智能問答"):
gr.Markdown("### 💬 基於文檔內容的 AI 問答")
with gr.Row():
with gr.Column():
question_input = gr.Textbox(
label="💭 請輸入您的問題",
placeholder="例如:這份文件的主要結論是什麼?文中提到的關鍵數據有哪些?",
lines=4
)
question_btn = gr.Button(
"📤 發送問題",
variant="primary"
)
gr.Markdown("""
**💡 問題範例:**
- 這份文件討論的主要議題是什麼?
- 文中有哪些重要的統計數據?
- 作者的主要觀點和結論是什麼?
- 文件中提到的建議有哪些?
""")
with gr.Column():
answer_output = gr.Textbox(
label="🤖 AI 智能回答",
lines=18,
placeholder="🤖 請先上傳並生成 PDF 摘要,然後在左側輸入問題,AI 將基於文檔內容為您提供詳細回答..."
)
# 事件處理器
api_key_input.submit(set_api_key, inputs=api_key_input, outputs=api_key_status)
model_choice.change(set_model, inputs=model_choice, outputs=model_status)
summary_btn.click(generate_summary, inputs=pdf_upload, outputs=summary_output)
question_btn.click(ask_question, inputs=question_input, outputs=answer_output)
question_input.submit(ask_question, inputs=question_input, outputs=answer_output)
clear_btn.click(clear_all, outputs=[summary_output, question_input, answer_output])
if __name__ == "__main__":
demo.launch(
share=False,
show_api=False,
show_error=True
)