From 5cf4010c9b745b6d3ad3898e44f9c1c2471a7090 Mon Sep 17 00:00:00 2001 From: egg Date: Tue, 18 Nov 2025 12:13:25 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=BE=A9=E5=A4=9A=E9=A0=81PDF?= =?UTF-8?q?=E9=A0=81=E7=A2=BC=E5=88=86=E9=85=8D=E9=8C=AF=E8=AA=A4=E5=92=8C?= =?UTF-8?q?logging=E9=85=8D=E7=BD=AE=E5=95=8F=E9=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Critical Bug #1: 多頁PDF頁碼分配錯誤 問題: - 在處理多頁PDF時,雖然text_regions有正確的頁碼標記 - 但layout_data.elements(表格)和images_metadata(圖片)都保持page=0 - 導致所有頁面的表格和圖片都被錯誤地繪製在第1頁 - 造成嚴重的版面錯誤、元素重疊和位置錯誤 根本原因: - ocr_service.py (第359-372行) 在累積多頁結果時 - text_regions有添加頁碼:region['page'] = page_num - 但images_metadata和layout_data.elements沒有更新頁碼 - 它們保持單頁處理時的默認值page=0 修復方案: - backend/app/services/ocr_service.py (第359-372行) - 為layout_data.elements中的每個元素添加正確的頁碼 - 為images_metadata中的每個圖片添加正確的頁碼 - 確保多頁PDF的每個元素都有正確的page標記 Critical Bug #2: Logging配置被uvicorn覆蓋 問題: - uvicorn啟動時會設置自己的logging配置 - 這會覆蓋應用程式的logging.basicConfig() - 導致應用層的INFO/WARNING/ERROR log完全消失 - 只能看到uvicorn的HTTP請求log和第三方庫的DEBUG log - 無法診斷PDF生成過程中的問題 修復方案: - backend/app/main.py (第17-36行) - 添加force=True參數強制重新配置logging (Python 3.8+) - 顯式設置root logger的level - 配置app-specific loggers (app.services.pdf_generator_service等) - 啟用log propagation確保訊息能傳遞到root logger 其他修復: - backend/app/services/pdf_generator_service.py - 將重要的debug logging改為info level (第371, 379, 490, 613行) 原因:預設log level是INFO,debug log不會顯示 - 修復max_cols UnboundLocalError (第507-509行) 將logger.info()移到max_cols定義之後 - 移除危險的.get('page', 0)默認值 (第762行) 改為.get('page'),沒有page的元素會被正確跳過 影響: ✅ 多頁PDF的表格和圖片現在會正確分配到對應頁面 ✅ 詳細的PDF生成log現在可以正確顯示(座標轉換、縮放比例等) ✅ 能夠診斷文字擠壓、間距和位置錯誤的問題 測試建議: 1. 重新啟動後端清除Python cache 2. 上傳多頁PDF進行OCR處理 3. 檢查生成的JSON中每個元素是否有正確的page標記 4. 檢查終端log是否顯示詳細的PDF生成過程 5. 驗證生成的PDF中每頁的元素位置是否正確 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .claude/settings.local.json | 73 +------------------ backend/app/main.py | 14 +++- backend/app/services/ocr_service.py | 13 +++- backend/app/services/pdf_generator_service.py | 14 ++-- 4 files changed, 31 insertions(+), 83 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 6c2542e..49b8e00 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -1,78 +1,7 @@ { "permissions": { "allow": [ - "Bash(openspec validate:*)", - "Bash(openspec list:*)", - "Bash(openspec show:*)", - "Bash(conda env:*)", - "Bash(alembic init:*)", - "Bash(alembic revision:*)", - "Bash(python -m alembic revision:*)", - "Bash(python test_services.py:*)", - "Bash(source ~/.zshrc)", - "Bash(conda activate:*)", - "Bash(brew install:*)", - "Bash(/opt/homebrew/bin/brew install libmagic)", - "Bash(python:*)", - "Bash(/opt/homebrew/bin/brew install pango gdk-pixbuf libffi)", - "Bash(export DYLD_LIBRARY_PATH:*)", - "Bash(pip install:*)", - "Bash(timeout 5 python:*)", - "Bash(curl:*)", - "Bash(pkill:*)", - "Bash(bash -c \"source ~/.zshrc && conda activate tool_ocr && export DYLD_LIBRARY_PATH=/opt/homebrew/lib:$DYLD_LIBRARY_PATH && python -m app.main > /tmp/tool_ocr_startup.log 2>&1 &\")", - "Bash(TOKEN=\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjMsInVzZXJuYW1lIjoiYWRtaW4iLCJleHAiOjE3NjI4ODM1NDF9.sm7zPq7ShErFg3UfBSrzGWxC5m5MgC_L0owKJb7Q4J4\":*)", - "Bash(/tmp/login_response.json)", - "Bash(cat:*)", - "Bash(conda run:*)", - "Bash(alembic upgrade:*)", - "Bash(lsof:*)", - "Bash(xargs kill:*)", - "Bash(brew list:*)", - "Bash(echo:*)", - "Bash(bash -c \"source ~/.zshrc && conda activate tool_ocr && cd /Users/egg/Projects/Tool_OCR/backend && pip list | grep pytest\")", - "Bash(bash -c:*)", - "Bash(find:*)", - "Bash(TOKEN=\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjMsInVzZXJuYW1lIjoiYWRtaW4iLCJleHAiOjE3NjI5MTczMzl9.x5FYcKYpF8rp1M7M7pQsDGwJS1EeQ6RdgRxtNbA2W5E\")", - "Bash(TOKEN=\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjMsInVzZXJuYW1lIjoiYWRtaW4iLCJleHAiOjE3NjI5MTczOTN9.oNPbj-SvIl_becIlulXb4DOJ6uHF70hnwlqI-Zfqs1g\")", - "Bash(TOKEN=\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIzIiwidXNlcm5hbWUiOiJhZG1pbiIsImV4cCI6MTc2MjkxNzQ1NH0.wtLv3n8bR_whzkuYILehy87IBDI_ph8FWEFd7laASEU\")", - "Bash(python3:*)", - "Bash(TOKEN=\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIzIiwidXNlcm5hbWUiOiJhZG1pbiIsImV4cCI6MTc2MjkyMDUzMn0.e_uG5pRTHsnsCEO3yVZDCR4vXXne81Evkw99VDGVZQU\")", - "Bash(unzip:*)", - "Bash(TOKEN=\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIzIiwidXNlcm5hbWUiOiJhZG1pbiIsImV4cCI6MTc2MjkyMDc0OH0.zOpB_2lTi-nVf5B7VMMB9GPeanuo0i-m6iauzjyhCno\")", - "Bash(TOKEN=\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIzIiwidXNlcm5hbWUiOiJhZG1pbiIsImV4cCI6MTc2MjkyMTExM30.q81VbDDIvQkL3VLl5sCvDEJlha3Rm4hkWMDQmWJyurs\")", - "Bash(TOKEN=\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIzIiwidXNlcm5hbWUiOiJhZG1pbiIsImV4cCI6MTc2MjkyMTI3OH0.7CQ9NMj5yekdtaRg4v0jHYQmfsbajTZ8aK8kKOo7ixQ\")", - "Bash(/Applications/LibreOffice.app/Contents/MacOS/soffice --headless --convert-to docx test_document.html --outdir .)", - "Bash(env)", - "Bash(node --version:*)", - "Bash(npm:*)", - "Bash(npx tailwindcss init -p)", - "Bash(sqlite3:*)", - "Bash(TOKEN=\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIzIiwidXNlcm5hbWUiOiJhZG1pbiIsImV4cCI6MTc2Mjk1ODUzOX0.S1JjFxVVmifdkN5F_dORt5jTRdTFN9MKJ8UJKuYacA8\")", - "Bash(tree:*)", - "Bash(done)", - "Bash(git add:*)", - "Bash(git commit:*)", - "Bash(git push)", - "Bash(docker --version:*)", - "Bash(dpkg:*)", - "Bash(pip3:*)", - "Bash(chmod:*)", - "Bash(sudo apt install:*)", - "Bash(/usr/bin/soffice:*)", - "Bash(git config:*)", - "Bash(source:*)", - "Bash(pip uninstall:*)", - "Bash(nvidia-smi:*)", - "Bash(journalctl:*)", - "Bash(ss:*)", - "Bash(pip index:*)", - "Bash(timeout 10 python:*)", - "Bash(alembic current:*)", - "Bash(git clean:*)", - "Bash(npx tsc:*)", - "Bash(./node_modules/.bin/tsc:*)", - "Bash(export LD_LIBRARY_PATH=/usr/lib/wsl/lib:$LD_LIBRARY_PATH echo \"Updated LD_LIBRARY_PATH:\" echo \"$LD_LIBRARY_PATH\" echo \"\" echo \"Testing CUDA library loading:\" ldconfig -p)" + "Bash(git commit:*)" ], "deny": [], "ask": [] diff --git a/backend/app/main.py b/backend/app/main.py index 7f273da..e888224 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -14,7 +14,7 @@ from app.core.config import settings # Ensure log directory exists before configuring logging Path(settings.log_file).parent.mkdir(parents=True, exist_ok=True) -# Configure logging +# Configure logging - Force configuration to override uvicorn's settings logging.basicConfig( level=getattr(logging, settings.log_level), format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", @@ -22,7 +22,19 @@ logging.basicConfig( logging.FileHandler(settings.log_file), logging.StreamHandler(), ], + force=True # Force reconfiguration (Python 3.8+) ) + +# Also explicitly configure root logger and app loggers +root_logger = logging.getLogger() +root_logger.setLevel(getattr(logging, settings.log_level)) + +# Configure app-specific loggers +for logger_name in ['app', 'app.services', 'app.services.pdf_generator_service', 'app.services.ocr_service']: + app_logger = logging.getLogger(logger_name) + app_logger.setLevel(getattr(logging, settings.log_level)) + app_logger.propagate = True # Ensure logs propagate to root logger + logger = logging.getLogger(__name__) diff --git a/backend/app/services/ocr_service.py b/backend/app/services/ocr_service.py index 7cefec5..32a6056 100644 --- a/backend/app/services/ocr_service.py +++ b/backend/app/services/ocr_service.py @@ -356,12 +356,19 @@ class OCRService: total_confidence_sum += page_result['average_confidence'] * page_result['total_text_regions'] total_valid_regions += page_result['total_text_regions'] - # Accumulate layout data + # Accumulate layout data and update page numbers if page_result.get('layout_data'): - all_layout_data.append(page_result['layout_data']) + layout_data = page_result['layout_data'] + # Update page number for all layout elements + if layout_data.get('elements'): + for element in layout_data['elements']: + element['page'] = page_num + all_layout_data.append(layout_data) - # Accumulate images metadata + # Accumulate images metadata and update page numbers if page_result.get('images_metadata'): + for img_meta in page_result['images_metadata']: + img_meta['page'] = page_num # Update page number for multi-page PDFs all_images_metadata.extend(page_result['images_metadata']) # Store OCR dimensions for each page diff --git a/backend/app/services/pdf_generator_service.py b/backend/app/services/pdf_generator_service.py index 9825984..116cf89 100644 --- a/backend/app/services/pdf_generator_service.py +++ b/backend/app/services/pdf_generator_service.py @@ -368,7 +368,7 @@ class PDFGeneratorService: ocr_x_right = bbox[2][0] # Right X ocr_y_bottom = bbox[2][1] # Bottom Y in OCR coordinates - logger.debug(f"[文字] '{text[:20]}...' OCR原始座標: L={ocr_x_left:.0f}, T={ocr_y_top:.0f}, R={ocr_x_right:.0f}, B={ocr_y_bottom:.0f}") + logger.info(f"[文字] '{text[:20]}...' OCR原始座標: L={ocr_x_left:.0f}, T={ocr_y_top:.0f}, R={ocr_x_right:.0f}, B={ocr_y_bottom:.0f}") # Apply scale factors to convert from OCR space to PDF space scaled_x_left = ocr_x_left * scale_w @@ -376,7 +376,7 @@ class PDFGeneratorService: scaled_x_right = ocr_x_right * scale_w scaled_y_bottom = ocr_y_bottom * scale_h - logger.debug(f"[文字] '{text[:20]}...' 縮放後(scale={scale_w:.3f},{scale_h:.3f}): L={scaled_x_left:.1f}, T={scaled_y_top:.1f}, R={scaled_x_right:.1f}, B={scaled_y_bottom:.1f}") + logger.info(f"[文字] '{text[:20]}...' 縮放後(scale={scale_w:.3f},{scale_h:.3f}): L={scaled_x_left:.1f}, T={scaled_y_top:.1f}, R={scaled_x_right:.1f}, B={scaled_y_bottom:.1f}") # Calculate bbox dimensions (after scaling) bbox_width = abs(scaled_x_right - scaled_x_left) @@ -487,7 +487,7 @@ class PDFGeneratorService: ocr_x_right_raw = table_bbox[2][0] ocr_y_bottom_raw = table_bbox[2][1] - logger.debug(f"[表格] OCR原始座標: L={ocr_x_left_raw:.0f}, T={ocr_y_top_raw:.0f}, R={ocr_x_right_raw:.0f}, B={ocr_y_bottom_raw:.0f}") + logger.info(f"[表格] OCR原始座標: L={ocr_x_left_raw:.0f}, T={ocr_y_top_raw:.0f}, R={ocr_x_right_raw:.0f}, B={ocr_y_bottom_raw:.0f}") # Apply scaling ocr_x_left = ocr_x_left_raw * scale_w @@ -502,11 +502,11 @@ class PDFGeneratorService: pdf_x = ocr_x_left pdf_y = page_height - ocr_y_bottom - logger.info(f"[表格] {len(rows)}行x{max_cols}列 → PDF位置: ({pdf_x:.1f}, {pdf_y:.1f}), 寬x高: {table_width:.0f}x{table_height:.0f}") - # Build table data for ReportLab # Convert parsed structure to simple 2D array max_cols = max(len(row['cells']) for row in rows) + + logger.info(f"[表格] {len(rows)}行x{max_cols}列 → PDF位置: ({pdf_x:.1f}, {pdf_y:.1f}), 寬x高: {table_width:.0f}x{table_height:.0f}") reportlab_data = [] for row in rows: @@ -610,7 +610,7 @@ class PDFGeneratorService: ocr_x_right_raw = bbox[2][0] ocr_y_bottom_raw = bbox[2][1] - logger.debug(f"[圖片] '{image_path_str}' OCR原始座標: L={ocr_x_left_raw:.0f}, T={ocr_y_top_raw:.0f}, R={ocr_x_right_raw:.0f}, B={ocr_y_bottom_raw:.0f}") + logger.info(f"[圖片] '{image_path_str}' OCR原始座標: L={ocr_x_left_raw:.0f}, T={ocr_y_top_raw:.0f}, R={ocr_x_right_raw:.0f}, B={ocr_y_bottom_raw:.0f}") # Apply scaling ocr_x_left = ocr_x_left_raw * scale_w @@ -759,7 +759,7 @@ class PDFGeneratorService: self.draw_text_region(pdf_canvas, region, target_height, scale_w, scale_h) # Draw tables for this page - page_tables = [t for t in table_elements if t.get('page', 0) == page_num - 1] + page_tables = [t for t in table_elements if t.get('page') == page_num - 1] logger.info(f"第 {page_num} 頁: 繪製 {len(page_tables)} 個表格") for table_elem in page_tables: self.draw_table_region(pdf_canvas, table_elem, images_metadata, target_height, scale_w, scale_h)