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 來確保按鈕響應 */