Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -2,8 +2,6 @@ import openai
|
|
2 |
import gradio as gr
|
3 |
import fitz # PyMuPDF
|
4 |
from openai import OpenAI
|
5 |
-
import os
|
6 |
-
import tempfile
|
7 |
import traceback
|
8 |
|
9 |
# 全域變數
|
@@ -61,7 +59,7 @@ def extract_pdf_text(file_path):
|
|
61 |
text = ""
|
62 |
for page_num, page in enumerate(doc):
|
63 |
page_text = page.get_text()
|
64 |
-
if page_text.strip():
|
65 |
text += f"\n--- 第 {page_num + 1} 頁 ---\n"
|
66 |
text += page_text
|
67 |
doc.close()
|
@@ -80,20 +78,17 @@ def generate_summary(pdf_file):
|
|
80 |
return "❌ 請先上傳 PDF 文件"
|
81 |
|
82 |
try:
|
83 |
-
# 從 PDF 提取文字
|
84 |
pdf_text = extract_pdf_text(pdf_file.name)
|
85 |
|
86 |
if not pdf_text.strip():
|
87 |
return "⚠️ 無法解析 PDF 文字,可能為純圖片 PDF 或空白文件。"
|
88 |
|
89 |
-
|
90 |
-
max_chars = 8000 # 為系統提示留出空間
|
91 |
if len(pdf_text) > max_chars:
|
92 |
pdf_text_truncated = pdf_text[:max_chars] + "\n\n[文本已截斷,僅顯示前 8000 字符]"
|
93 |
else:
|
94 |
pdf_text_truncated = pdf_text
|
95 |
|
96 |
-
# 生成摘要
|
97 |
response = client.chat.completions.create(
|
98 |
model=selected_model,
|
99 |
messages=[
|
@@ -133,7 +128,6 @@ def ask_question(user_question):
|
|
133 |
return "❌ 請輸入問題"
|
134 |
|
135 |
try:
|
136 |
-
# 使用摘要和原始文本來提供更好的上下文
|
137 |
context = f"PDF 摘要:\n{summary_text}\n\n原始內容(部分):\n{pdf_text[:2000]}"
|
138 |
|
139 |
response = client.chat.completions.create(
|
@@ -172,633 +166,127 @@ def clear_all():
|
|
172 |
pdf_text = ""
|
173 |
return "", "", ""
|
174 |
|
175 |
-
#
|
176 |
-
with gr.Blocks(
|
177 |
-
|
178 |
-
title="PDF 摘要助手",
|
179 |
-
css="""
|
180 |
-
/* 修復頁面大小問題 */
|
181 |
-
.gradio-container {
|
182 |
-
max-width: none !important;
|
183 |
-
width: 100% !important;
|
184 |
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
|
185 |
-
min-height: 100vh !important;
|
186 |
-
padding: 0 !important;
|
187 |
-
margin: 0 !important;
|
188 |
-
}
|
189 |
|
190 |
-
|
191 |
-
|
192 |
-
background: rgba(255, 255, 255, 0.95) !important;
|
193 |
-
border-radius: 20px !important;
|
194 |
-
margin: 20px auto !important;
|
195 |
-
padding: 30px !important;
|
196 |
-
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1) !important;
|
197 |
-
backdrop-filter: blur(10px) !important;
|
198 |
-
max-width: 1400px !important;
|
199 |
-
width: calc(100% - 40px) !important;
|
200 |
-
}
|
201 |
-
|
202 |
-
/* 標題樣式 */
|
203 |
-
.main-header {
|
204 |
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
|
205 |
-
-webkit-background-clip: text !important;
|
206 |
-
-webkit-text-fill-color: transparent !important;
|
207 |
-
background-clip: text !important;
|
208 |
-
text-align: center !important;
|
209 |
-
font-size: 2.5em !important;
|
210 |
-
font-weight: bold !important;
|
211 |
-
margin-bottom: 20px !important;
|
212 |
-
}
|
213 |
-
|
214 |
-
/* 分頁導航 - 修復點擊問題 */
|
215 |
-
.gradio-tabs {
|
216 |
-
border-radius: 15px !important;
|
217 |
-
overflow: hidden !important;
|
218 |
-
}
|
219 |
-
|
220 |
-
.gradio-tabitem {
|
221 |
-
padding: 25px !important;
|
222 |
-
background: white !important;
|
223 |
-
border-radius: 0 0 15px 15px !important;
|
224 |
-
}
|
225 |
-
|
226 |
-
/* 輸入框樣式 - 修復交互問題 */
|
227 |
-
input[type="text"],
|
228 |
-
input[type="password"],
|
229 |
-
textarea {
|
230 |
-
border: 2px solid #e0e6ff !important;
|
231 |
-
border-radius: 12px !important;
|
232 |
-
padding: 15px !important;
|
233 |
-
font-size: 16px !important;
|
234 |
-
transition: all 0.3s ease !important;
|
235 |
-
width: 100% !important;
|
236 |
-
box-sizing: border-box !important;
|
237 |
-
}
|
238 |
-
|
239 |
-
input[type="text"]:focus,
|
240 |
-
input[type="password"]:focus,
|
241 |
-
textarea:focus {
|
242 |
-
border-color: #667eea !important;
|
243 |
-
box-shadow: 0 0 20px rgba(102, 126, 234, 0.3) !important;
|
244 |
-
outline: none !important;
|
245 |
-
}
|
246 |
-
|
247 |
-
/* 按鈕樣式 - 強力修復所有按鈕 */
|
248 |
-
button,
|
249 |
-
.gr-button,
|
250 |
-
input[type="submit"],
|
251 |
-
input[type="button"],
|
252 |
-
#summary-button,
|
253 |
-
#clear-button,
|
254 |
-
#question-button,
|
255 |
-
#api-key-button,
|
256 |
-
#model-button,
|
257 |
-
.action-button,
|
258 |
-
[data-testid*="button"] {
|
259 |
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
|
260 |
-
border: none !important;
|
261 |
-
border-radius: 12px !important;
|
262 |
-
color: white !important;
|
263 |
-
font-weight: 600 !important;
|
264 |
-
padding: 15px 30px !important;
|
265 |
-
font-size: 16px !important;
|
266 |
-
transition: all 0.3s ease !important;
|
267 |
-
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4) !important;
|
268 |
-
cursor: pointer !important;
|
269 |
-
pointer-events: auto !important;
|
270 |
-
z-index: 9999 !important;
|
271 |
-
position: relative !important;
|
272 |
-
display: inline-block !important;
|
273 |
-
min-height: 44px !important;
|
274 |
-
line-height: normal !important;
|
275 |
-
width: auto !important;
|
276 |
-
min-width: 120px !important;
|
277 |
-
text-align: center !important;
|
278 |
-
user-select: none !important;
|
279 |
-
margin: 10px 5px !important;
|
280 |
-
}
|
281 |
-
|
282 |
-
button:hover,
|
283 |
-
.gr-button:hover,
|
284 |
-
input[type="submit"]:hover,
|
285 |
-
input[type="button"]:hover,
|
286 |
-
#summary-button:hover,
|
287 |
-
#clear-button:hover,
|
288 |
-
#question-button:hover,
|
289 |
-
#api-key-button:hover,
|
290 |
-
#model-button:hover,
|
291 |
-
.action-button:hover,
|
292 |
-
[data-testid*="button"]:hover {
|
293 |
-
transform: translateY(-2px) !important;
|
294 |
-
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.6) !important;
|
295 |
-
background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%) !important;
|
296 |
-
}
|
297 |
-
|
298 |
-
button:active,
|
299 |
-
.gr-button:active,
|
300 |
-
input[type="submit"]:active,
|
301 |
-
input[type="button"]:active,
|
302 |
-
#summary-button:active,
|
303 |
-
#clear-button:active,
|
304 |
-
#question-button:active,
|
305 |
-
#api-key-button:active,
|
306 |
-
#model-button:active,
|
307 |
-
.action-button:active,
|
308 |
-
[data-testid*="button"]:active {
|
309 |
-
transform: translateY(0px) !important;
|
310 |
-
box-shadow: 0 3px 10px rgba(102, 126, 234, 0.4) !important;
|
311 |
-
}
|
312 |
-
|
313 |
-
/* 次要按鈕樣式 */
|
314 |
-
button[data-testid*="secondary"],
|
315 |
-
.gr-button.secondary,
|
316 |
-
button.secondary,
|
317 |
-
#clear-button,
|
318 |
-
#model-button,
|
319 |
-
.secondary-btn,
|
320 |
-
button[variant="secondary"] {
|
321 |
-
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%) !important;
|
322 |
-
}
|
323 |
-
|
324 |
-
button[data-testid*="secondary"]:hover,
|
325 |
-
.gr-button.secondary:hover,
|
326 |
-
button.secondary:hover,
|
327 |
-
#clear-button:hover,
|
328 |
-
#model-button:hover,
|
329 |
-
.secondary-btn:hover,
|
330 |
-
button[variant="secondary"]:hover {
|
331 |
-
background: linear-gradient(135deg, #e081e9 0%, #e3455a 100%) !important;
|
332 |
-
}
|
333 |
-
|
334 |
-
/* 修復 Gradio 特定的按鈕容器 */
|
335 |
-
.gr-form,
|
336 |
-
.gr-form > div,
|
337 |
-
.gr-button-group,
|
338 |
-
div[data-testid="button"],
|
339 |
-
div[data-testid*="button"] {
|
340 |
-
pointer-events: auto !important;
|
341 |
-
z-index: 9998 !important;
|
342 |
-
}
|
343 |
-
|
344 |
-
/* 確保按鈕內的文字和圖標不會阻擋點擊 */
|
345 |
-
button span,
|
346 |
-
button svg,
|
347 |
-
button i,
|
348 |
-
.gr-button span,
|
349 |
-
.gr-button svg,
|
350 |
-
.gr-button i,
|
351 |
-
.action-button span,
|
352 |
-
.action-button svg,
|
353 |
-
.action-button i {
|
354 |
-
pointer-events: none !important;
|
355 |
-
user-select: none !important;
|
356 |
-
}
|
357 |
-
|
358 |
-
/* 文件上傳區域 */
|
359 |
-
.file-upload-area {
|
360 |
-
border: 3px dashed #667eea !important;
|
361 |
-
border-radius: 15px !important;
|
362 |
-
background: rgba(102, 126, 234, 0.05) !important;
|
363 |
-
padding: 40px !important;
|
364 |
-
text-align: center !important;
|
365 |
-
transition: all 0.3s ease !important;
|
366 |
-
min-height: 120px !important;
|
367 |
-
}
|
368 |
-
|
369 |
-
.file-upload-area:hover {
|
370 |
-
background: rgba(102, 126, 234, 0.1) !important;
|
371 |
-
border-color: #764ba2 !important;
|
372 |
-
}
|
373 |
-
|
374 |
-
/* 單選按鈕容器 */
|
375 |
-
.radio-group {
|
376 |
-
background: rgba(102, 126, 234, 0.05) !important;
|
377 |
-
border-radius: 12px !important;
|
378 |
-
padding: 20px !important;
|
379 |
-
margin: 10px 0 !important;
|
380 |
-
}
|
381 |
-
|
382 |
-
/* 輸出文本區域 */
|
383 |
-
.output-text {
|
384 |
-
background: #f8f9ff !important;
|
385 |
-
border: 1px solid #e0e6ff !important;
|
386 |
-
border-radius: 12px !important;
|
387 |
-
padding: 20px !important;
|
388 |
-
min-height: 200px !important;
|
389 |
-
}
|
390 |
-
|
391 |
-
/* 隱藏 Gradio logo 和 footer */
|
392 |
-
footer,
|
393 |
-
.gradio-container footer,
|
394 |
-
div[class*="footer"],
|
395 |
-
div[class*="Footer"],
|
396 |
-
.gr-footer {
|
397 |
-
display: none !important;
|
398 |
-
}
|
399 |
-
|
400 |
-
/* 修復響應式問題 */
|
401 |
-
.gr-row {
|
402 |
-
display: flex !important;
|
403 |
-
gap: 20px !important;
|
404 |
-
width: 100% !important;
|
405 |
-
}
|
406 |
-
|
407 |
-
.gr-column {
|
408 |
-
flex: 1 !important;
|
409 |
-
min-width: 0 !important;
|
410 |
-
}
|
411 |
-
|
412 |
-
/* 確保所有交互元素正常工作 */
|
413 |
-
* {
|
414 |
-
pointer-events: auto !important;
|
415 |
-
}
|
416 |
-
|
417 |
-
/* 特殊修復:覆蓋可能的 Gradio 樣式衝突 */
|
418 |
-
.gradio-container * {
|
419 |
-
pointer-events: auto !important;
|
420 |
-
}
|
421 |
-
|
422 |
-
/* 修復單選按鈕 */
|
423 |
-
input[type="radio"] {
|
424 |
-
pointer-events: auto !important;
|
425 |
-
cursor: pointer !important;
|
426 |
-
z-index: 1000 !important;
|
427 |
-
position: relative !important;
|
428 |
-
}
|
429 |
-
|
430 |
-
/* 修復文件上傳 */
|
431 |
-
input[type="file"] {
|
432 |
-
pointer-events: auto !important;
|
433 |
-
cursor: pointer !important;
|
434 |
-
z-index: 1000 !important;
|
435 |
-
}
|
436 |
-
|
437 |
-
/* 添加 JavaScript 來確保按鈕響應 */
|
438 |
-
</style>
|
439 |
-
<script>
|
440 |
-
document.addEventListener('DOMContentLoaded', function() {
|
441 |
-
console.log('開始修復按鈕...');
|
442 |
|
443 |
-
|
444 |
-
|
445 |
-
|
446 |
-
|
447 |
-
|
448 |
-
'input[type="submit"]',
|
449 |
-
'input[type="button"]',
|
450 |
-
'.gr-button',
|
451 |
-
'[data-testid*="button"]',
|
452 |
-
'#summary-button',
|
453 |
-
'#clear-button',
|
454 |
-
'#question-button',
|
455 |
-
'#api-key-button',
|
456 |
-
'#model-button',
|
457 |
-
'.action-button',
|
458 |
-
'.primary-btn',
|
459 |
-
'.secondary-btn'
|
460 |
-
];
|
461 |
-
|
462 |
-
let buttonCount = 0;
|
463 |
-
selectors.forEach(selector => {
|
464 |
-
const elements = document.querySelectorAll(selector);
|
465 |
-
elements.forEach(btn => {
|
466 |
-
if (btn) {
|
467 |
-
// 強制設定樣式
|
468 |
-
btn.style.pointerEvents = 'auto';
|
469 |
-
btn.style.cursor = 'pointer';
|
470 |
-
btn.style.zIndex = '9999';
|
471 |
-
btn.style.position = 'relative';
|
472 |
-
btn.style.display = 'inline-block';
|
473 |
-
btn.style.minHeight = '44px';
|
474 |
-
btn.style.minWidth = '120px';
|
475 |
-
btn.style.padding = '15px 30px';
|
476 |
-
btn.style.borderRadius = '12px';
|
477 |
-
btn.style.border = 'none';
|
478 |
-
btn.style.fontWeight = '600';
|
479 |
-
btn.style.fontSize = '16px';
|
480 |
-
btn.style.color = 'white';
|
481 |
-
btn.style.userSelect = 'none';
|
482 |
-
btn.style.margin = '10px 5px';
|
483 |
-
btn.style.textAlign = 'center';
|
484 |
-
|
485 |
-
// 設定背景色
|
486 |
-
if (btn.id === 'clear-button' ||
|
487 |
-
btn.id === 'model-button' ||
|
488 |
-
btn.classList.contains('secondary-btn')) {
|
489 |
-
btn.style.background = 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)';
|
490 |
-
} else {
|
491 |
-
btn.style.background = 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)';
|
492 |
-
}
|
493 |
-
|
494 |
-
// 移除可能阻止點擊的屬性
|
495 |
-
btn.removeAttribute('disabled');
|
496 |
-
|
497 |
-
// 確保子元素不會阻止點擊
|
498 |
-
const children = btn.querySelectorAll('*');
|
499 |
-
children.forEach(child => {
|
500 |
-
child.style.pointerEvents = 'none';
|
501 |
-
child.style.userSelect = 'none';
|
502 |
-
});
|
503 |
-
|
504 |
-
buttonCount++;
|
505 |
-
}
|
506 |
-
});
|
507 |
-
});
|
508 |
-
|
509 |
-
console.log(`修復了 ${buttonCount} 個按鈕`);
|
510 |
-
}
|
511 |
|
512 |
-
|
513 |
-
|
514 |
|
515 |
-
|
516 |
-
|
|
|
|
|
|
|
517 |
|
518 |
-
|
519 |
-
const observer = new MutationObserver(function(mutations) {
|
520 |
-
let shouldFix = false;
|
521 |
-
mutations.forEach(function(mutation) {
|
522 |
-
if (mutation.type === 'childList') {
|
523 |
-
mutation.addedNodes.forEach(function(node) {
|
524 |
-
if (node.nodeType === 1) { // 元素節點
|
525 |
-
if (node.tagName === 'BUTTON' ||
|
526 |
-
node.querySelector && node.querySelector('button')) {
|
527 |
-
shouldFix = true;
|
528 |
-
}
|
529 |
-
}
|
530 |
-
});
|
531 |
-
}
|
532 |
-
});
|
533 |
-
|
534 |
-
if (shouldFix) {
|
535 |
-
setTimeout(fixAllButtons, 100);
|
536 |
-
}
|
537 |
-
});
|
538 |
|
539 |
-
|
540 |
-
|
541 |
-
|
542 |
-
|
543 |
-
|
544 |
-
});
|
545 |
|
546 |
-
|
547 |
-
|
548 |
-
console.log('點擊事件:', e.target);
|
549 |
-
}, true);
|
550 |
-
});
|
551 |
-
</script>
|
552 |
-
<style>
|
553 |
-
|
554 |
-
/* 響應式設計 */
|
555 |
-
@media (max-width: 768px) {
|
556 |
-
.main-content {
|
557 |
-
margin: 10px !important;
|
558 |
-
padding: 20px !important;
|
559 |
-
width: calc(100% - 20px) !important;
|
560 |
-
}
|
561 |
|
562 |
-
.
|
563 |
-
|
564 |
-
|
|
|
|
|
|
|
|
|
|
|
565 |
|
566 |
-
.
|
567 |
-
|
568 |
-
|
|
|
569 |
|
570 |
-
|
571 |
-
|
572 |
-
|
573 |
-
}
|
574 |
-
}
|
575 |
-
"""
|
576 |
-
) as demo:
|
577 |
-
with gr.Column(elem_classes="main-content"):
|
578 |
-
gr.HTML("""
|
579 |
-
<div class="main-header">📄 PDF 摘要 & 問答助手</div>
|
580 |
|
581 |
-
|
582 |
-
|
583 |
-
|
584 |
-
|
585 |
-
|
586 |
-
<strong>智能摘要生成</strong><br>
|
587 |
-
<span style="color: #666;">自動分析 PDF 內容並生成結構化摘要</span>
|
588 |
-
</div>
|
589 |
-
<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;">
|
590 |
-
<div style="font-size: 24px; margin-bottom: 10px;">🤖</div>
|
591 |
-
<strong>AI 問答系統</strong><br>
|
592 |
-
<span style="color: #666;">基於文檔內容回答您的問題</span>
|
593 |
-
</div>
|
594 |
-
<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;">
|
595 |
-
<div style="font-size: 24px; margin-bottom: 10px;">💡</div>
|
596 |
-
<strong>快速理解</strong><br>
|
597 |
-
<span style="color: #666;">快速掌握長篇文檔的核心內容</span>
|
598 |
-
</div>
|
599 |
-
</div>
|
600 |
-
<div style="background: rgba(255, 193, 7, 0.1); padding: 15px; border-radius: 10px; border-left: 4px solid #ffc107; margin-top: 20px;">
|
601 |
-
<strong style="color: #e65100;">⚠️ 重要提醒:</strong> 使用前請先在「🔧 設定」頁面輸入您的 OpenAI API Key
|
602 |
-
</div>
|
603 |
-
</div>
|
604 |
-
""")
|
605 |
-
|
606 |
-
with gr.Tab("🔧 設定", elem_classes="settings-tab"):
|
607 |
-
gr.Markdown("### ⚙️ 基本設定")
|
608 |
-
with gr.Row():
|
609 |
-
with gr.Column():
|
610 |
-
api_key_input = gr.Textbox(
|
611 |
-
label="🔑 輸入 OpenAI API Key",
|
612 |
-
type="password",
|
613 |
-
placeholder="請輸入您的 OpenAI API Key (sk-...)",
|
614 |
-
elem_classes="api-input"
|
615 |
-
)
|
616 |
-
# 添加確認按鈕
|
617 |
-
api_key_btn = gr.Button(
|
618 |
-
"✅ 確認設定 API Key",
|
619 |
-
variant="primary",
|
620 |
-
elem_id="api-key-button",
|
621 |
-
elem_classes="action-button primary-btn"
|
622 |
-
)
|
623 |
-
api_key_status = gr.Textbox(
|
624 |
-
label="📊 API 連接狀態",
|
625 |
-
interactive=False,
|
626 |
-
value="🔄 等待設定 API Key...",
|
627 |
-
elem_classes="status-display"
|
628 |
-
)
|
629 |
-
|
630 |
-
with gr.Column():
|
631 |
-
model_choice = gr.Radio(
|
632 |
-
choices=["gpt-4", "gpt-4.1", "gpt-4.5"],
|
633 |
-
label="🤖 選擇 AI 模型",
|
634 |
-
value="gpt-4",
|
635 |
-
interactive=True,
|
636 |
-
elem_classes="model-selector"
|
637 |
-
)
|
638 |
-
# 添加模型確認按鈕
|
639 |
-
model_btn = gr.Button(
|
640 |
-
"🎯 確認選擇模型",
|
641 |
-
variant="secondary",
|
642 |
-
elem_id="model-button",
|
643 |
-
elem_classes="action-button secondary-btn"
|
644 |
-
)
|
645 |
-
model_status = gr.Textbox(
|
646 |
-
label="🎯 當前模型",
|
647 |
-
interactive=False,
|
648 |
-
value="✅ 已選擇:gpt-4",
|
649 |
-
elem_classes="status-display"
|
650 |
-
)
|
651 |
-
|
652 |
-
# 添加使用說明和測試區域
|
653 |
-
gr.HTML("""
|
654 |
-
<div style='margin: 20px 0; padding: 15px; background: rgba(102, 126, 234, 0.1); border-radius: 10px;'>
|
655 |
-
<strong>📋 使用步驟:</strong>
|
656 |
-
<ol style='margin: 10px 0; padding-left: 20px;'>
|
657 |
-
<li>輸入您的 OpenAI API Key 並點擊「確認設定」</li>
|
658 |
-
<li>選擇 AI 模型並點擊「確認選擇」</li>
|
659 |
-
<li>前往「PDF 處理」頁面上傳文件開始使用!</li>
|
660 |
-
</ol>
|
661 |
-
<div style='margin-top: 15px; padding: 10px; background: rgba(255, 193, 7, 0.1); border-radius: 8px;'>
|
662 |
-
<strong>🔧 調試信息:</strong>
|
663 |
-
<br>• 如果按鈕沒反應,請檢查瀏覽器控制台
|
664 |
-
<br>• API Key 應該以 'sk-' 開頭
|
665 |
-
<br>• 模型選擇為單選,一次只能選一個
|
666 |
-
</div>
|
667 |
-
</div>
|
668 |
-
""")
|
669 |
-
|
670 |
-
# 添加測試按鈕
|
671 |
-
with gr.Row():
|
672 |
-
test_btn = gr.Button(
|
673 |
-
"🧪 測試按鈕功能",
|
674 |
-
variant="secondary",
|
675 |
-
elem_id="test-button"
|
676 |
-
)
|
677 |
-
test_output = gr.Textbox(
|
678 |
-
label="測試結果",
|
679 |
-
interactive=False,
|
680 |
-
placeholder="點擊測試按鈕查看是否正常工作"
|
681 |
-
)
|
682 |
-
|
683 |
-
def test_function():
|
684 |
-
return "✅ 按鈕功能正常!如果您看到這個訊息,表示按鈕點擊事件正常工作。"
|
685 |
-
|
686 |
-
test_btn.click(test_function, outputs=test_output)
|
687 |
-
|
688 |
-
with gr.Tab("📄 PDF 處理", elem_classes="pdf-tab"):
|
689 |
-
gr.Markdown("### 📁 文件上傳與摘要生成")
|
690 |
-
with gr.Row():
|
691 |
-
with gr.Column():
|
692 |
-
pdf_upload = gr.File(
|
693 |
-
label="📎 選擇 PDF 文件",
|
694 |
-
file_types=[".pdf"],
|
695 |
-
elem_classes="file-upload"
|
696 |
-
)
|
697 |
-
gr.Markdown("**支援格式**:PDF 文件 (建議 < 10MB)")
|
698 |
-
|
699 |
-
with gr.Row():
|
700 |
-
summary_btn = gr.Button(
|
701 |
-
"🔄 開始生成摘要",
|
702 |
-
variant="primary",
|
703 |
-
elem_id="summary-button",
|
704 |
-
elem_classes="action-button primary-btn"
|
705 |
-
)
|
706 |
-
clear_btn = gr.Button(
|
707 |
-
"🗑️ 清除所有資料",
|
708 |
-
variant="secondary",
|
709 |
-
elem_id="clear-button",
|
710 |
-
elem_classes="action-button secondary-btn"
|
711 |
-
)
|
712 |
-
|
713 |
-
with gr.Column():
|
714 |
-
summary_output = gr.Textbox(
|
715 |
-
label="📋 AI 生成的文檔摘要",
|
716 |
-
lines=15,
|
717 |
-
placeholder="📄 上傳 PDF 文件並點擊「開始生成摘要」按鈕,AI 將為您分析文檔內容...",
|
718 |
-
elem_classes="summary-output"
|
719 |
-
)
|
720 |
-
|
721 |
-
with gr.Tab("❓ 智能問答", elem_classes="qa-tab"):
|
722 |
-
gr.Markdown("### 💬 基於文檔內容的 AI 問答")
|
723 |
-
with gr.Row():
|
724 |
-
with gr.Column():
|
725 |
-
question_input = gr.Textbox(
|
726 |
-
label="💭 請輸入您的問題",
|
727 |
-
placeholder="例如:這份文件的主要結論是什麼?文中提到的關鍵數據有哪些?",
|
728 |
-
lines=3,
|
729 |
-
elem_classes="question-input"
|
730 |
-
)
|
731 |
-
question_btn = gr.Button(
|
732 |
-
"📤 發送問題",
|
733 |
-
variant="primary",
|
734 |
-
elem_id="question-button",
|
735 |
-
elem_classes="action-button primary-btn"
|
736 |
-
)
|
737 |
-
|
738 |
-
gr.Markdown("""
|
739 |
-
**💡 問題範例:**
|
740 |
-
- 這份文件討論的主要議題是什麼?
|
741 |
-
- 文中有哪些重要的統計數據?
|
742 |
-
- 作者的主要觀點和結論是什麼?
|
743 |
-
- 文件中提到的建議有哪些?
|
744 |
-
""")
|
745 |
-
|
746 |
-
with gr.Column():
|
747 |
-
answer_output = gr.Textbox(
|
748 |
-
label="🤖 AI 智能回答",
|
749 |
-
lines=12,
|
750 |
-
placeholder="🤖 請先上傳並生成 PDF 摘要��然後在左側輸入問題,AI 將基於文檔內容為您提供詳細回答...",
|
751 |
-
elem_classes="answer-output"
|
752 |
-
)
|
753 |
-
|
754 |
-
# 事件處理器 - 確保按鈕點擊有反應
|
755 |
-
# API Key 設定事件(雙重綁定確保有效)
|
756 |
-
def handle_api_key_click(api_key_value):
|
757 |
-
print(f"處理 API Key: {api_key_value[:10]}..." if api_key_value else "空值")
|
758 |
-
return set_api_key(api_key_value)
|
759 |
|
760 |
-
|
761 |
-
|
762 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
763 |
|
764 |
-
#
|
765 |
api_key_btn.click(
|
766 |
-
fn=
|
767 |
-
inputs=
|
768 |
-
outputs=
|
769 |
-
)
|
770 |
-
api_key_input.submit(
|
771 |
-
fn=handle_api_key_click,
|
772 |
-
inputs=[api_key_input],
|
773 |
-
outputs=[api_key_status]
|
774 |
)
|
775 |
|
776 |
-
# 模型選擇事件
|
777 |
model_btn.click(
|
778 |
-
fn=
|
779 |
-
inputs=
|
780 |
-
outputs=
|
781 |
)
|
782 |
-
|
783 |
-
|
784 |
-
|
785 |
-
|
|
|
786 |
)
|
787 |
|
788 |
-
|
789 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
790 |
|
791 |
-
#
|
792 |
-
|
793 |
-
|
|
|
|
|
|
|
794 |
|
795 |
-
|
796 |
-
|
|
|
|
|
|
|
797 |
|
798 |
if __name__ == "__main__":
|
799 |
-
# Hugging Face Spaces 部署設定
|
800 |
demo.launch(
|
801 |
-
share=False,
|
802 |
show_api=False,
|
803 |
show_error=True
|
804 |
)
|
|
|
2 |
import gradio as gr
|
3 |
import fitz # PyMuPDF
|
4 |
from openai import OpenAI
|
|
|
|
|
5 |
import traceback
|
6 |
|
7 |
# 全域變數
|
|
|
59 |
text = ""
|
60 |
for page_num, page in enumerate(doc):
|
61 |
page_text = page.get_text()
|
62 |
+
if page_text.strip():
|
63 |
text += f"\n--- 第 {page_num + 1} 頁 ---\n"
|
64 |
text += page_text
|
65 |
doc.close()
|
|
|
78 |
return "❌ 請先上傳 PDF 文件"
|
79 |
|
80 |
try:
|
|
|
81 |
pdf_text = extract_pdf_text(pdf_file.name)
|
82 |
|
83 |
if not pdf_text.strip():
|
84 |
return "⚠️ 無法解析 PDF 文字,可能為純圖片 PDF 或空白文件。"
|
85 |
|
86 |
+
max_chars = 8000
|
|
|
87 |
if len(pdf_text) > max_chars:
|
88 |
pdf_text_truncated = pdf_text[:max_chars] + "\n\n[文本已截斷,僅顯示前 8000 字符]"
|
89 |
else:
|
90 |
pdf_text_truncated = pdf_text
|
91 |
|
|
|
92 |
response = client.chat.completions.create(
|
93 |
model=selected_model,
|
94 |
messages=[
|
|
|
128 |
return "❌ 請輸入問題"
|
129 |
|
130 |
try:
|
|
|
131 |
context = f"PDF 摘要:\n{summary_text}\n\n原始內容(部分):\n{pdf_text[:2000]}"
|
132 |
|
133 |
response = client.chat.completions.create(
|
|
|
166 |
pdf_text = ""
|
167 |
return "", "", ""
|
168 |
|
169 |
+
# 創建最簡單的 Gradio 介面
|
170 |
+
with gr.Blocks(title="PDF 摘要助手") as demo:
|
171 |
+
gr.Markdown("# 📄 PDF 摘要 & 問答助手")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
172 |
|
173 |
+
with gr.Tab("🔧 設定"):
|
174 |
+
gr.Markdown("## API Key 設定")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
175 |
|
176 |
+
api_key_input = gr.Textbox(
|
177 |
+
label="輸入 OpenAI API Key",
|
178 |
+
type="password",
|
179 |
+
placeholder="請輸入您的 OpenAI API Key (sk-...)"
|
180 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
181 |
|
182 |
+
with gr.Row():
|
183 |
+
api_key_btn = gr.Button("確認設定 API Key", variant="primary")
|
184 |
|
185 |
+
api_key_status = gr.Textbox(
|
186 |
+
label="API 狀態",
|
187 |
+
interactive=False,
|
188 |
+
value="等待設定 API Key..."
|
189 |
+
)
|
190 |
|
191 |
+
gr.Markdown("## 模型選擇")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
192 |
|
193 |
+
model_choice = gr.Radio(
|
194 |
+
choices=["gpt-4", "gpt-4.1", "gpt-4.5"],
|
195 |
+
label="選擇 AI 模型",
|
196 |
+
value="gpt-4"
|
197 |
+
)
|
|
|
198 |
|
199 |
+
with gr.Row():
|
200 |
+
model_btn = gr.Button("確認選擇模型", variant="secondary")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
201 |
|
202 |
+
model_status = gr.Textbox(
|
203 |
+
label="當前模型",
|
204 |
+
interactive=False,
|
205 |
+
value="已選擇:gpt-4"
|
206 |
+
)
|
207 |
+
|
208 |
+
with gr.Tab("📄 PDF 處理"):
|
209 |
+
gr.Markdown("## 上傳 PDF 文件")
|
210 |
|
211 |
+
pdf_upload = gr.File(
|
212 |
+
label="選擇 PDF 文件",
|
213 |
+
file_types=[".pdf"]
|
214 |
+
)
|
215 |
|
216 |
+
with gr.Row():
|
217 |
+
summary_btn = gr.Button("生成摘要", variant="primary")
|
218 |
+
clear_btn = gr.Button("清除資料", variant="secondary")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
219 |
|
220 |
+
summary_output = gr.Textbox(
|
221 |
+
label="PDF 摘要",
|
222 |
+
lines=15,
|
223 |
+
placeholder="上傳 PDF 文件並點擊「生成摘要」按鈕"
|
224 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
225 |
|
226 |
+
with gr.Tab("❓ 問答"):
|
227 |
+
gr.Markdown("## AI 問答")
|
228 |
+
|
229 |
+
question_input = gr.Textbox(
|
230 |
+
label="請輸入您的問題",
|
231 |
+
placeholder="例如:這份文件的主要結論是什麼?",
|
232 |
+
lines=3
|
233 |
+
)
|
234 |
+
|
235 |
+
with gr.Row():
|
236 |
+
question_btn = gr.Button("發送問題", variant="primary")
|
237 |
+
|
238 |
+
answer_output = gr.Textbox(
|
239 |
+
label="AI 回答",
|
240 |
+
lines=10,
|
241 |
+
placeholder="AI 回答將顯示在這裡"
|
242 |
+
)
|
243 |
|
244 |
+
# 事件綁定 - 使用最簡單的方式
|
245 |
api_key_btn.click(
|
246 |
+
fn=set_api_key,
|
247 |
+
inputs=api_key_input,
|
248 |
+
outputs=api_key_status
|
|
|
|
|
|
|
|
|
|
|
249 |
)
|
250 |
|
|
|
251 |
model_btn.click(
|
252 |
+
fn=set_model,
|
253 |
+
inputs=model_choice,
|
254 |
+
outputs=model_status
|
255 |
)
|
256 |
+
|
257 |
+
summary_btn.click(
|
258 |
+
fn=generate_summary,
|
259 |
+
inputs=pdf_upload,
|
260 |
+
outputs=summary_output
|
261 |
)
|
262 |
|
263 |
+
question_btn.click(
|
264 |
+
fn=ask_question,
|
265 |
+
inputs=question_input,
|
266 |
+
outputs=answer_output
|
267 |
+
)
|
268 |
+
|
269 |
+
clear_btn.click(
|
270 |
+
fn=clear_all,
|
271 |
+
outputs=[summary_output, question_input, answer_output]
|
272 |
+
)
|
273 |
|
274 |
+
# 也支援 Enter 鍵
|
275 |
+
api_key_input.submit(
|
276 |
+
fn=set_api_key,
|
277 |
+
inputs=api_key_input,
|
278 |
+
outputs=api_key_status
|
279 |
+
)
|
280 |
|
281 |
+
question_input.submit(
|
282 |
+
fn=ask_question,
|
283 |
+
inputs=question_input,
|
284 |
+
outputs=answer_output
|
285 |
+
)
|
286 |
|
287 |
if __name__ == "__main__":
|
|
|
288 |
demo.launch(
|
289 |
+
share=False,
|
290 |
show_api=False,
|
291 |
show_error=True
|
292 |
)
|