3a05chatgpt commited on
Commit
228df30
·
verified ·
1 Parent(s): 2232f84

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +98 -610
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
- # 創建 Gradio 介面
176
- with gr.Blocks(
177
- theme=gr.themes.Soft(),
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
- .main-content {
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
- function fixAllButtons() {
445
- // 所有可能的按鈕選擇器
446
- const selectors = [
447
- 'button',
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
- fixAllButtons();
514
 
515
- // 每秒檢查一次
516
- setInterval(fixAllButtons, 1000);
 
 
 
517
 
518
- // 監聽 DOM 變化
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
- observer.observe(document.body, {
540
- childList: true,
541
- subtree: true,
542
- attributes: true,
543
- attributeFilter: ['style', 'class']
544
- });
545
 
546
- // 添加全域點擊事件監聽
547
- document.addEventListener('click', function(e) {
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
- .main-header {
563
- font-size: 1.8em !important;
564
- }
 
 
 
 
 
565
 
566
- .gr-row {
567
- flex-direction: column !important;
568
- }
 
569
 
570
- button {
571
- width: 100% !important;
572
- margin: 5px 0 !important;
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
- <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;">
582
- <h3 style="color: #667eea; margin-bottom: 15px;">🚀 歡迎使用 PDF 智能分析工具!</h3>
583
- <div style="display: flex; justify-content: space-around; flex-wrap: wrap; margin: 20px 0;">
584
- <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;">
585
- <div style="font-size: 24px; margin-bottom: 10px;">📋</div>
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
- def handle_model_click(model_value):
761
- print(f"處理模型選擇: {model_value}")
762
- return set_model(model_value)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
763
 
764
- # 綁定事件
765
  api_key_btn.click(
766
- fn=handle_api_key_click,
767
- inputs=[api_key_input],
768
- outputs=[api_key_status]
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=handle_model_click,
779
- inputs=[model_choice],
780
- outputs=[model_status]
781
  )
782
- model_choice.change(
783
- fn=handle_model_click,
784
- inputs=[model_choice],
785
- outputs=[model_status]
 
786
  )
787
 
788
- # PDF 處理事件
789
- summary_btn.click(generate_summary, inputs=pdf_upload, outputs=summary_output)
 
 
 
 
 
 
 
 
790
 
791
- # 問答事件
792
- question_btn.click(ask_question, inputs=question_input, outputs=answer_output)
793
- question_input.submit(ask_question, inputs=question_input, outputs=answer_output)
 
 
 
794
 
795
- # 清除功能
796
- clear_btn.click(clear_all, outputs=[summary_output, question_input, answer_output])
 
 
 
797
 
798
  if __name__ == "__main__":
799
- # Hugging Face Spaces 部署設定
800
  demo.launch(
801
- share=False, # Hugging Face Spaces 不需要 share
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
  )