支持OCR和AI解釋的Web PDF閱讀器:解決大文檔閱讀難題

支持OCR和AI解釋的Web PDF閱讀器:解決大文檔閱讀難題

    • 一、背景:為什么需要這個工具?
      • 問題場景
      • 解決方案
    • 二、技術原理:如何實現這些功能?
      • 1、核心技術組件
      • 2、工作流程
      • 3、關鍵點
    • 三、操作指南
      • 1、環境準備
      • 2、生成Html代碼
      • 3、Web服務端
      • 4、啟動服務端
    • 四、效果

一、背景:為什么需要這個工具?

問題場景

當你在手機上閱讀掃描版PDF文檔(特別是超長文檔如2000頁的書籍)時,是否遇到過這些問題:

  1. 翻頁卡頓:越往后翻頁,加載速度越慢
  2. 文字識別失敗:嘗試復制文字時,OCR識別經常失敗或需要長時間等待
  3. 內容理解困難:專業術語或復雜段落難以理解,需要額外查詢

技術解釋:掃描版PDF本質上是圖片合集,手機自帶的OCR功能對長文檔處理能力有限,特別是:

  • 內存限制導致大文檔處理困難
  • 后臺進程被系統強制終止
  • 缺乏持續優化的大文檔處理機制

解決方案

為此我開發了這款Web版PDF閱讀器,核心功能包括:

  • 區域選擇識別:自由框選文檔任意區域進行OCR
  • 文字即時編輯:直接修改識別結果
  • AI智能解釋:一鍵獲取復雜內容的通俗解釋
  • 跨平臺使用:在電腦/手機瀏覽器中都能流暢運行

設計理念:將OCR和AI能力轉移到服務器端處理,突破移動設備性能限制,同時通過Web技術實現免安裝使用


二、技術原理:如何實現這些功能?

1、核心技術組件

組件功能使用技術
前端界面PDF渲染/用戶交互PDF.js + HTML5 Canvas
OCR引擎圖片轉文字百度文字識別API
AI解釋引擎文本內容解釋DeepSeek LLM大模型
服務端功能調度Python Flask框架

2、工作流程

用戶選擇PDF
前端渲染
框選區域
發送到服務端
OCR識別
返回識別文字
編輯文本
請求AI解釋
返回解釋結果

3、關鍵點

  1. 智能區域選擇

    • 自動適配不同分辨率設備
    • 支持觸摸屏手勢操作
    • 實時顯示選擇框效果
  2. 閱讀記憶功能

    • 自動記錄上次閱讀位置
    • 本地存儲閱讀進度
    • 翻頁進度可視化展示

三、操作指南

1、環境準備

cat > .env <<-'EOF'
APP_ID = '您的百度APPID'
API_KEY = '您的百度APIKEY'
SECRET_KEY = '您的百度SECRETKEY'
OPENAI_API_KEY = "您的DeepSeek密鑰"
OPENAI_BASE_URL = "https://api.deepseek.com"
EOF

注意

  1. 百度OCR服務需在AI開放平臺申請
  2. DeepSeek API可在官網獲取

2、生成Html代碼

mkdir templates
cd templates
cat > index.html <<-'EOF'
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>本地化PDF閱讀器 - OCR識別與文本解釋</title><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"><style>* {margin: 0;padding: 0;box-sizing: border-box;font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;touch-action: manipulation;}body {background: linear-gradient(135deg, #1a2a6c, #2a5298);min-height: 100vh;padding: 15px;color: #333;display: flex;flex-direction: column;align-items: center;overflow-x: hidden;}.container {width: 100%;max-width: 100%;background: white;border-radius: 12px;box-shadow: 0 10px 25px rgba(0, 0, 0, 0.35);overflow: hidden;display: flex;flex-direction: column;height: calc(100vh - 30px);}header {background: linear-gradient(to right, #2c3e50, #4a6491);color: white;padding: 15px 25px;display: flex;align-items: center;justify-content: space-between;}.logo {display: flex;align-items: center;gap: 12px;}.logo i {font-size: 30px;color: #4dabf7;animation: pulse 2s infinite;}@keyframes pulse {0%, 100% { transform: scale(1); }50% { transform: scale(1.1); }}.logo h1 {font-size: 24px;font-weight: 600;text-shadow: 1px 1px 3px rgba(0,0,0,0.3);}/* 修改開始:移除固定寬度,使用彈性布局 */.controls {display: flex;padding: 12px 15px;background: #f1f3f5;gap: 12px;border-bottom: 1px solid #dee2e6;align-items: center;width: 100%;overflow-x: auto;overflow-y: hidden;flex-wrap: nowrap;}/* 修改結束 */.file-controls, .progress-container {display: flex;align-items: center;gap: 10px;flex-shrink: 0;}.file-controls {flex: 1;min-width: 300px;}.progress-container {flex: 2;min-width: 400px;}button {padding: 9px 16px;border: none;border-radius: 6px;cursor: pointer;font-weight: 500;transition: all 0.2s ease;display: flex;align-items: center;gap: 6px;background: #339af0;color: white;box-shadow: 0 3px 5px rgba(0,0,0,0.1);flex-shrink: 0;}button:hover {background: #228be6;transform: translateY(-2px);box-shadow: 0 5px 10px rgba(0,0,0,0.15);}button:active {transform: translateY(1px);}button:disabled {background: #adb5bd;cursor: not-allowed;transform: none;box-shadow: none;}button i {font-size: 15px;}.page-info {font-weight: 500;background: #fff;padding: 7px 12px;border-radius: 6px;box-shadow: 0 2px 4px rgba(0,0,0,0.08);min-width: 110px;text-align: center;flex-shrink: 0;}.progress-bar {flex: 1;height: 8px;background: #e9ecef;border-radius: 4px;position: relative;overflow: hidden;box-shadow: inset 0 1px 2px rgba(0,0,0,0.1);}.progress-fill {height: 100%;background: linear-gradient(90deg, #4dabf7, #40c057);border-radius: 4px;width: 0%;transition: width 0.3s ease;}input[type="range"] {width: 100%;height: 8px;-webkit-appearance: none;background: transparent;flex: 1;}input[type="range"]::-webkit-slider-thumb {-webkit-appearance: none;width: 18px;height: 18px;border-radius: 50%;background: #339af0;cursor: pointer;box-shadow: 0 2px 6px rgba(0,0,0,0.25);border: 2px solid white;}.viewer-container {position: relative;flex: 1;background: #2c3e50;overflow: hidden;display: flex;justify-content: center;align-items: center;}#pdf-viewer {width: 100%;height: 100%;display: flex;justify-content: center;align-items: center;padding: 8px;overflow: auto;}.canvas-container {position: relative;display: flex;justify-content: center;align-items: center;margin: 0;box-shadow: 0 6px 15px rgba(0, 0, 0, 0.45);border: 1px solid #dee2e6;transition: transform 0.3s ease;max-width: 100%;max-height: 100%;overflow: hidden;}.canvas-container canvas {display: block;cursor: pointer;max-width: 100%;max-height: 100%;touch-action: none;}#selection-overlay {position: absolute;top: 0;left: 0;cursor: crosshair;border: 2px dashed rgba(77, 171, 247, 0.9);background: rgba(77, 171, 247, 0.2);pointer-events: none;z-index: 10;}.status-bar {background: #3d5a80;color: white;padding: 8px 15px;display: flex;justify-content: space-between;font-size: 13px;font-weight: 300;}.loading-overlay {position: absolute;top: 0;left: 0;width: 100%;height: 100%;background: rgba(0, 0, 0, 0.85);display: flex;flex-direction: column;justify-content: center;align-items: center;color: white;z-index: 100;}.spinner {width: 50px;height: 50px;border: 4px solid rgba(255, 255, 255, 0.3);border-radius: 50%;border-top: 4px solid #4dabf7;animation: spin 1s linear infinite;margin-bottom: 15px;}@keyframes spin {0% { transform: rotate(0deg); }100% { transform: rotate(360deg); }}.modal {position: fixed;top: 0;left: 0;width: 100%;height: 100%;background: rgba(0, 0, 0, 0.7);display: flex;justify-content: center;align-items: center;z-index: 1000;opacity: 0;visibility: hidden;transition: all 0.3s ease;}.modal.active {opacity: 1;visibility: visible;}.modal-content {background: white;border-radius: 10px;width: 85%;max-width: 550px;max-height: 85vh;overflow: hidden;box-shadow: 0 12px 35px rgba(0, 0, 0, 0.4);transform: translateY(-15px);transition: transform 0.3s ease;}.modal.active .modal-content {transform: translateY(0);}.modal-header {padding: 16px;background: linear-gradient(to right, #3d5a80, #4dabf7);color: white;display: flex;justify-content: space-between;align-items: center;}.modal-header h3 {font-size: 20px;font-weight: 600;}.close-btn {background: none;border: none;color: white;font-size: 22px;cursor: pointer;width: 32px;height: 32px;border-radius: 50%;display: flex;align-items: center;justify-content: center;transition: all 0.3s ease;}.close-btn:hover {background: rgba(255,255,255,0.2);}.modal-body {padding: 20px;overflow-y: auto;max-height: 55vh;}.modal-footer {padding: 16px;display: flex;justify-content: flex-end;gap: 12px;background: #f8f9fa;border-top: 1px solid #e9ecef;}.btn-secondary {background: #adb5bd;color: white;}.btn-primary {background: #339af0;color: white;}#ocr-text {width: 100%;min-height: 130px;padding: 12px;border: 1px solid #dee2e6;border-radius: 6px;font-size: 15px;line-height: 1.5;resize: vertical;margin-bottom: 15px;background: #f8f9fa;transition: border-color 0.3s;}#ocr-text:focus {border-color: #4dabf7;outline: none;box-shadow: 0 0 0 3px rgba(77, 171, 247, 0.2);}#deepseek-response {background: #f1f3f5;border-radius: 6px;border: 1px solid #e9ecef;padding: 16px;font-size: 14px;line-height: 1.5;max-height: 180px;overflow-y: auto;transition: all 0.3s ease;}.hidden {display: none;}.api-response {padding: 12px;background: #e7f5ff;border-left: 4px solid #4dabf7;border-radius: 4px;margin: 12px 0;animation: fadeIn 0.4s ease;}@keyframes fadeIn {from { opacity: 0; transform: translateY(8px); }to { opacity: 1; transform: translateY(0); }}.ocr-hint {text-align: center;color: #5c7cfa;font-style: italic;margin-top: 8px;padding: 8px;background: #f1f3f5;border-radius: 6px;margin-bottom: 12px;}.error-message {background: #ffe3e3;border: 1px solid #ff6b6b;border-radius: 8px;padding: 12px;margin: 0 auto 15px;text-align: center;max-width: 600px;display: none;}.api-status {display: flex;align-items: center;gap: 6px;margin-top: 8px;font-size: 13px;color: #495057;}.response-header {display: flex;justify-content: space-between;align-items: center;margin-bottom: 8px;}.api-tag {background: #4dabf7;color: white;padding: 3px 8px;border-radius: 4px;font-size: 11px;font-weight: bold;}.api-time {color: #868e96;font-size: 11px;}@media (max-width: 1024px) {.file-controls {min-width: 250px;}.progress-container {min-width: 350px;}}@media (max-width: 900px) {.controls {flex-wrap: wrap;padding: 10px;}.file-controls, .progress-container {min-width: 100%;}.progress-container {margin-top: 10px;}}@media (max-width: 768px) {body {padding: 10px;}.container {height: calc(100vh - 20px);}.logo h1 {font-size: 18px;}.status-bar {flex-direction: column;gap: 6px;text-align: center;}.modal-content {width: 95%;}button {padding: 10px;font-size: 14px;}.modal-footer {flex-wrap: wrap;justify-content: center;}.modal-footer button {flex: 1;min-width: 45%;margin-bottom: 8px;}.file-controls {gap: 6px;min-width: 100%;}.file-controls button {flex: 1;}}@media (max-width: 480px) {.page-info {min-width: auto;padding: 5px 8px;}.file-controls button span {display: none;}.file-controls button i {margin-right: 0;}}</style>
</head>
<body>    <div class="error-message" id="error-message"><i class="fas fa-exclamation-triangle"></i><span id="error-text">發生了錯誤,請查看控制臺獲取詳細信息</span></div><div class="container">        <div class="controls"><div class="file-controls"><button id="open-file"><i class="fas fa-folder-open"></i> 打開PDF</button><button id="prev-page"><i class="fas fa-arrow-left"></i> 上一頁</button><button id="next-page"><i class="fas fa-arrow-right"></i> 下一頁</button></div><div class="progress-container"><div class="page-info">頁碼: <span id="current-page">1</span> / <span id="total-pages">1</span></div><div class="progress-bar"><div class="progress-fill"></div></div><input type="range" id="page-slider" min="1" max="1" value="1"></div></div><div class="viewer-container"><div id="pdf-viewer"></div><div id="selection-overlay" class="hidden"></div><div id="loading-overlay" class="loading-overlay hidden"><div class="spinner"></div><p id="loading-text">加載中...</p></div></div><div class="status-bar"><div>狀態: <span id="ocr-status">準備就緒</span></div></div></div><!-- OCR模態框 --><div class="modal" id="ocr-modal"><div class="modal-content"><div class="modal-header"><h3><i class="fas fa-font"></i> OCR識別結果</h3><button class="close-btn" id="close-ocr-modal">&times;</button></div><div class="modal-body"><div class="ocr-hint"><i class="fas fa-lightbulb"></i> 您選擇了以下內容(可進行編輯):</div><textarea id="ocr-text" placeholder="識別內容將顯示在這里..."></textarea><div id="api-response-section" class="hidden"><div class="response-header"><p><strong><i class="fas fa-robot"></i> AI 響應:</strong></p><div class="api-time" id="api-time"></div></div><div id="deepseek-response">等待AI的回復...</div></div></div><div class="modal-footer"><button class="btn-secondary" id="copy-text"><i class="fas fa-copy"></i> 復制</button><button class="btn-primary" id="explain-text"><i class="fas fa-robot"></i> 解釋</button></div></div></div><!-- 使用本地文件 --><script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.16.105/pdf.min.js"></script><script>// 設置PDF.js工作環境pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.16.105/pdf.worker.min.js';// 常量const STORAGE_PREFIX = 'pdfReader_';// DOM元素const viewer = document.getElementById('pdf-viewer');const fileInput = document.createElement('input');fileInput.type = 'file';fileInput.accept = '.pdf';const openFileButton = document.getElementById('open-file');const prevPageButton = document.getElementById('prev-page');const nextPageButton = document.getElementById('next-page');const currentPageElement = document.getElementById('current-page');const totalPagesElement = document.getElementById('total-pages');const pageSlider = document.getElementById('page-slider');const progressFill = document.querySelector('.progress-fill');const loadingOverlay = document.getElementById('loading-overlay');const loadingText = document.getElementById('loading-text');const ocrStatus = document.getElementById('ocr-status');const ocrModal = document.getElementById('ocr-modal');const closeOcrModal = document.getElementById('close-ocr-modal');const ocrText = document.getElementById('ocr-text');const copyTextButton = document.getElementById('copy-text');const explainTextButton = document.getElementById('explain-text');const apiResponseSection = document.getElementById('api-response-section');const deepseekResponse = document.getElementById('deepseek-response');const selectionOverlay = document.getElementById('selection-overlay');const errorMessage = document.getElementById('error-message');const errorText = document.getElementById('error-text');const apiTimeElement = document.getElementById('api-time');// 全局變量let pdfDoc = null;let currentPage = 1;let currentScale = 1;let pageRendering = false;let pageNumPending = null;let fileName = null;let fileKey = null;let canvasMap = new Map();let selection = {};let currentCanvas = null;let currentCanvasRect = null;let dpr = window.devicePixelRatio || 1;let isMobile = /Mobi|Android/i.test(navigator.userAgent);let viewerContainer = document.querySelector('.viewer-container');// 初始化openFileButton.addEventListener('click', () => fileInput.click());fileInput.addEventListener('change', loadPDF);prevPageButton.addEventListener('click', () => gotoPage(currentPage - 1));nextPageButton.addEventListener('click', () => gotoPage(currentPage + 1));pageSlider.addEventListener('input', () => gotoPage(parseInt(pageSlider.value)));closeOcrModal.addEventListener('click', closeOCRModal);copyTextButton.addEventListener('click', copyOCRText);explainTextButton.addEventListener('click', explainTextWithAI);// 顯示錯誤信息function showError(message) {errorText.textContent = message;errorMessage.style.display = 'block';console.error(message);}// 隱藏錯誤信息function hideError() {errorMessage.style.display = 'none';}// 加載PDF文件function loadPDF(e) {const file = e.target.files[0];if (!file) return;if (file.type !== 'application/pdf') {alert('請選擇PDF文件');return;}fileName = file.name;fileKey = STORAGE_PREFIX + fileName;showLoading('加載PDF文件...');hideError();const fileReader = new FileReader();fileReader.onload = function() {const typedArray = new Uint8Array(this.result);try {// 加載PDF文檔pdfjsLib.getDocument(typedArray).promise.then(function(pdf) {pdfDoc = pdf;const numPages = pdf.numPages;// 顯示總頁數totalPagesElement.textContent = numPages;pageSlider.max = numPages;// 嘗試從本地存儲獲取閱讀位置const lastPage = localStorage.getItem(fileKey + '_page');const initPage = lastPage ? parseInt(lastPage) : 1;// 加載第一頁(或上次閱讀的頁面)gotoPage(initPage);// 清除畫布映射canvasMap.clear();// 移除加載狀態hideLoading();}).catch(function(error) {hideLoading();showError('加載PDF失敗: ' + error.message);});} catch (error) {hideLoading();showError('PDF.js初始化失敗: ' + error.message);}};fileReader.onerror = function() {hideLoading();showError('讀取文件失敗');};fileReader.readAsArrayBuffer(file);}// 渲染指定頁碼function renderPage(num) {if (!pdfDoc) return;pageRendering = true;showLoading(`渲染第 ${num} 頁...`);ocrStatus.textContent = '正在渲染頁面...';hideError();try {// 獲取頁面的promisepdfDoc.getPage(num).then(function(page) {const container = document.createElement('div');container.className = 'canvas-container';// 創建Canvasconst canvas = document.createElement('canvas');const ctx = canvas.getContext('2d', { willReadFrequently: true });// 獲取PDF頁面原始尺寸const viewport = page.getViewport({ scale: 1 });const originalWidth = viewport.width;const originalHeight = viewport.height;// 計算縮放比例以適應容器const viewerContainer = document.querySelector('.viewer-container');const viewerWidth = viewer.clientWidth - 20; // 減去內邊距const viewerHeight = viewer.clientHeight - 20;// 計算合適的縮放比例const widthScale = viewerWidth / originalWidth;const heightScale = viewerHeight / originalHeight;const scale = Math.min(widthScale, heightScale) * currentScale;const scaledViewport = page.getViewport({ scale: scale });// 設置Canvas尺寸(考慮設備像素比)const displayWidth = scaledViewport.width;const displayHeight = scaledViewport.height;const pixelWidth = Math.floor(displayWidth * dpr);const pixelHeight = Math.floor(displayHeight * dpr);canvas.width = pixelWidth;canvas.height = pixelHeight;canvas.style.width = displayWidth + 'px';canvas.style.height = displayHeight + 'px';// 縮放上下文以匹配設備像素比ctx.scale(dpr, dpr);container.appendChild(canvas);// 清空查看器并添加新容器viewer.innerHTML = '';viewer.appendChild(container);// 將Canvas存儲在映射中canvasMap.set(num, {canvas: canvas,rect: container.getBoundingClientRect(),viewport: scaledViewport,dpr: dpr});// 設置事件監聽器用于OCR選擇setupSelectionEvents(container);// 渲染PDF頁面到Canvasconst renderContext = {canvasContext: ctx,viewport: scaledViewport};const renderTask = page.render(renderContext);renderTask.promise.then(function() {if (pageNumPending !== null) {gotoPage(pageNumPending);pageNumPending = null;}pageRendering = false;hideLoading();updateStatus(`已渲染第 ${num}`);updateFileInfo();}).catch(function(error) {pageRendering = false;hideLoading();showError('渲染頁面失敗: ' + error.message);});}).catch(function(error) {hideLoading();showError('獲取PDF頁面失敗: ' + error.message);});} catch (error) {hideLoading();showError('渲染頁面時出錯: ' + error.message);}}// 設置選擇事件(同時支持鼠標和觸摸)function setupSelectionEvents(container) {container.addEventListener('mousedown', startSelection);container.addEventListener('touchstart', handleTouchStart, { passive: false });}// 處理觸摸開始事件function handleTouchStart(e) {if (e.touches.length === 1) {// 單指觸摸,開始選擇startSelection(e.touches[0]);}}// 處理觸摸移動事件function handleTouchMove(e) {if (e.touches.length === 1) {// 單指移動,調整選擇區域resizeSelection(e.touches[0]);}}// 處理觸摸結束事件function handleTouchEnd(e) {if (e.touches.length === 0) {// 所有手指離開,結束選擇finishSelection();}}// 跳轉到指定頁面function gotoPage(num) {if (!pdfDoc) return;if (pageRendering) {pageNumPending = num;return;}if (num < 1 || num > pdfDoc.numPages) return;currentPage = num;currentPageElement.textContent = num;pageSlider.value = num;// 更新進度條const percent = Math.round((num / pdfDoc.numPages) * 100);progressFill.style.width = percent + '%';// 保存當前頁到本地存儲if (fileKey) {localStorage.setItem(fileKey + '_page', num);}// 清空當前查看器內容viewer.innerHTML = '';selectionOverlay.classList.add('hidden');// 渲染該頁renderPage(num);updateFileInfo();}// 更新底部狀態欄信息function updateFileInfo() {}// 更新OCR狀態function updateStatus(message) {ocrStatus.textContent = message;}// 顯示加載狀態function showLoading(message) {loadingText.textContent = message;loadingOverlay.classList.remove('hidden');}// 隱藏加載狀態function hideLoading() {loadingOverlay.classList.add('hidden');}// OCR區域選擇function startSelection(e) {e.preventDefault();const container = e.currentTarget;if (!container) return;const canvas = container.querySelector('canvas');if (!canvas) return;// 存儲當前canvas和其邊界currentCanvas = canvas;currentCanvasRect = container.getBoundingClientRect();// 獲取事件坐標const clientX = e.clientX || e.pageX;const clientY = e.clientY || e.pageY;// 計算相對于容器的坐標(考慮滾動位置)const viewerRect = viewer.getBoundingClientRect();const containerRect = container.getBoundingClientRect();// 計算容器在viewer中的位置(考慮滾動)const containerXInViewer = containerRect.left - viewerRect.left + viewer.scrollLeft;const containerYInViewer = containerRect.top - viewerRect.top + viewer.scrollTop;// 計算事件在容器內的坐標const x = clientX - containerRect.left;const y = clientY - containerRect.top;// 初始化選擇框位置selectionOverlay.style.width = '0';selectionOverlay.style.height = '0';selectionOverlay.style.left = (containerXInViewer + x) + 'px';selectionOverlay.style.top = (containerYInViewer + y) + 'px';selectionOverlay.classList.remove('hidden');// 存儲初始位置(相對于容器)selection = {startX: x,startY: y,endX: x,endY: y};// 添加事件監聽if (isMobile) {document.addEventListener('touchmove', handleTouchMove, { passive: false });document.addEventListener('touchend', handleTouchEnd);} else {document.addEventListener('mousemove', resizeSelection);document.addEventListener('mouseup', finishSelection);}}// 調整選擇框大小function resizeSelection(e) {const container = document.querySelector('.canvas-container');if (!container) return;// 獲取事件坐標const clientX = e.clientX || e.pageX;const clientY = e.clientY || e.pageY;// 獲取容器和viewer的邊界矩形const viewerRect = viewer.getBoundingClientRect();const containerRect = container.getBoundingClientRect();// 計算容器在viewer中的位置(考慮滾動)const containerXInViewer = containerRect.left - viewerRect.left + viewer.scrollLeft;const containerYInViewer = containerRect.top - viewerRect.top + viewer.scrollTop;// 計算事件在容器內的坐標const x = clientX - containerRect.left;const y = clientY - containerRect.top;// 限制在畫布顯示范圍內const clampedX = Math.max(0, Math.min(x, containerRect.width));const clampedY = Math.max(0, Math.min(y, containerRect.height));// 更新選擇框尺寸const left = Math.min(selection.startX, clampedX);const top = Math.min(selection.startY, clampedY);const width = Math.abs(clampedX - selection.startX);const height = Math.abs(clampedY - selection.startY);// 設置選擇框在viewer中的位置selectionOverlay.style.left = (containerXInViewer + left) + 'px';selectionOverlay.style.top = (containerYInViewer + top) + 'px';selectionOverlay.style.width = width + 'px';selectionOverlay.style.height = height + 'px';// 更新結束位置selection.endX = clampedX;selection.endY = clampedY;}// 完成選擇并進行OCR識別function finishSelection() {// 移除事件監聽if (isMobile) {document.removeEventListener('touchmove', handleTouchMove);document.removeEventListener('touchend', handleTouchEnd);} else {document.removeEventListener('mousemove', resizeSelection);document.removeEventListener('mouseup', finishSelection);}// 檢查選擇區域是否有效const minArea = 20;const width = Math.abs(selection.endX - selection.startX);const height = Math.abs(selection.endY - selection.startY);if (width < minArea || height < minArea) {selectionOverlay.classList.add('hidden');return;}// 獲取當前頁的Canvasconst container = document.querySelector('.canvas-container');if (!container || !currentCanvas) return;const canvas = currentCanvas;const ctx = canvas.getContext('2d');// 計算畫布的實際像素與顯示尺寸的比率const scaleX = canvas.width / currentCanvasRect.width;const scaleY = canvas.height / currentCanvasRect.height;// 轉換為畫布的實際像素坐標const pixelX = selection.startX * scaleX;const pixelY = selection.startY * scaleY;const pixelW = width * scaleX;const pixelH = height * scaleY;try {// 獲取圖像數據const imageData = ctx.getImageData(Math.round(pixelX), Math.round(pixelY), Math.round(pixelW), Math.round(pixelH));// 創建臨時Canvas來存儲選擇區域的圖像const tempCanvas = document.createElement('canvas');tempCanvas.width = Math.round(pixelW);tempCanvas.height = Math.round(pixelH);const tempCtx = tempCanvas.getContext('2d');tempCtx.putImageData(imageData, 0, 0);// 顯示OCR模態框ocrModal.classList.add('active');ocrText.value = '';apiResponseSection.classList.add('hidden');deepseekResponse.innerHTML = '等待AI的回復...';updateStatus('準備進行OCR識別...');// 將圖像轉換為DataURLconst imageDataURL = tempCanvas.toDataURL('image/jpeg');// 發送到Flask服務端進行OCR識別fetch('/ocr', {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify({ image: imageDataURL })}).then(response => response.json()).then(data => {if (data.success) {ocrText.value = data.text.trim() || '未能識別到文字';updateStatus('OCR識別完成');} else {throw new Error(data.error || 'OCR識別失敗');}}).catch(err => {ocrText.value = 'OCR錯誤: ' + err.message;updateStatus('OCR識別失敗');showError('OCR識別失敗: ' + err.message);}).finally(() => {selectionOverlay.classList.add('hidden');});} catch (error) {showError('獲取圖像數據失敗: ' + error.message);selectionOverlay.classList.add('hidden');updateStatus('選擇區域錯誤');}}// 關閉OCR模態框function closeOCRModal() {ocrModal.classList.remove('active');}// 復制識別文本function copyOCRText() {ocrText.select();document.execCommand('copy');alert('文本已復制到剪貼板');}// 使用AI解釋文本 - 調用Flask服務function explainTextWithAI() {const text = ocrText.value.trim();if (!text) {alert('請先識別出文本內容');return;}apiResponseSection.classList.remove('hidden');updateStatus('正在使用AI解釋文本...');deepseekResponse.innerHTML = '<div class="api-response">正在分析文本內容...</div>';const startTime = new Date();// 調用Flask服務的/explain端點fetch('/explain', {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify({ text: text })}).then(response => {if (!response.ok) {throw new Error('服務器錯誤: ' + response.status);}return response.json();}).then(data => {const endTime = new Date();const timeTaken = (endTime - startTime) / 1000;deepseekResponse.innerHTML = `<div class="api-response"><div class="api-tag">解釋結果</div><p>${data.explanation || '未能獲取解釋內容'}</p><div class="api-status"><i class="fas fa-clock"></i> 本次分析耗時 ${timeTaken.toFixed(2)} 秒</div></div>`;updateStatus('AI解釋完成');apiTimeElement.textContent = `處理時間: ${timeTaken.toFixed(2)}`;}).catch(err => {deepseekResponse.innerHTML = `<div class="api-response" style="background:#ffecec;border-left-color:#ff6b6b;"><p>錯誤: ${err.message}</p><p>請檢查服務是否正常運行</p></div>`;updateStatus('AI解釋失敗');showError('調用解釋服務失敗: ' + err.message);});}// 顯示示例PDF加載window.addEventListener('load', function() {updateStatus('準備就緒 | 請打開PDF文件');});</script>
</body>
</html>
EOF
cd -

3、Web服務端

cat > main.py <<-'EOF'
import os
import base64
import io
import re
import logging
from logging.handlers import RotatingFileHandler
from flask import Flask, render_template, jsonify, request, send_from_directory
from PIL import Image
from aip import AipOcr
from dotenv import load_dotenv
import openai# 加載環境變量
load_dotenv()app = Flask(__name__)# 配置日志系統
def configure_logging():# 創建日志目錄log_dir = "logs"if not os.path.exists(log_dir):os.makedirs(log_dir)# 設置日志格式log_format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'formatter = logging.Formatter(log_format)# 文件日志處理器(滾動日志,最大10MB,保留3個備份)file_handler = RotatingFileHandler(os.path.join(log_dir, 'app.log'),maxBytes=10*1024*1024,backupCount=3)file_handler.setFormatter(formatter)file_handler.setLevel(logging.DEBUG)# 控制臺日志處理器console_handler = logging.StreamHandler()console_handler.setFormatter(formatter)console_handler.setLevel(logging.DEBUG)# 獲取應用日志器并添加處理器app.logger.setLevel(logging.DEBUG)app.logger.addHandler(file_handler)app.logger.addHandler(console_handler)# 禁用werkzeug的默認日志處理werkzeug_logger = logging.getLogger('werkzeug')werkzeug_logger.setLevel(logging.ERROR)werkzeug_logger.addHandler(file_handler)configure_logging()class OpenAILLM:"""OpenAI語言模型封裝類"""def __init__(self, model_name: str = "deepseek-chat"):self.model_name = model_nameself.client = openai.OpenAI()app.logger.info(f"初始化OpenAI模型: {model_name}")def predict(self, query: str) -> str:"""使用LLM生成解釋文本"""try:app.logger.debug(f"LLM查詢開始: {query[:100]}... (長度:{len(query)})")response = self.client.chat.completions.create(model=self.model_name,messages=[{"role": "system", "content": "請用簡潔且通俗易懂的方式解釋下面這句話:"},{"role": "user", "content": query}                ],temperature=0.7,)result = response.choices[0].message.content.strip()cleaned_result = re.sub(r'<think>.*?</think>', '', result, flags=re.DOTALL)app.logger.debug(f"LLM原始響應: {result[:200]}...")app.logger.debug(f"LLM清理后結果: {cleaned_result[:200]}...")return cleaned_resultexcept openai.APIError as api_err:app.logger.error(f"OpenAI API錯誤: {str(api_err)}", exc_info=True)return "API服務錯誤,請稍后再試"except openai.APIConnectionError as conn_err:app.logger.error(f"OpenAI連接錯誤: {str(conn_err)}", exc_info=True)return "網絡連接錯誤,請檢查網絡"except openai.RateLimitError as limit_err:app.logger.error(f"OpenAI限流錯誤: {str(limit_err)}", exc_info=True)return "請求過于頻繁,請稍后再試"except Exception as e:app.logger.exception("LLM處理未知錯誤")return "解釋生成失敗,請稍后再試"# 初始化全局模型實例
llm = OpenAILLM()@app.route('/')
def index():"""主頁面路由"""app.logger.info("訪問首頁")return render_template('index.html')@app.route('/ocr', methods=['POST'])
def ocr_processing():"""OCR文字識別接口"""try:app.logger.info("收到OCR請求")data = request.jsonimage_data = data.get('image', '')# 記錄圖像數據基本信息app.logger.debug(f"收到圖像數據: 長度={len(image_data)} 字符, 類型={type(image_data)}")# 提取Base64編碼數據if 'base64,' in image_data:image_data = image_data.split('base64,', 1)[1]app.logger.debug("已剝離Base64前綴")# 解碼圖像img_bytes = base64.b64decode(image_data)app.logger.debug(f"圖像解碼成功: {len(img_bytes)} 字節")# 使用百度OCR APIclient = AipOcr(os.getenv('APP_ID'), os.getenv('API_KEY'), os.getenv('SECRET_KEY'))app.logger.info("調用百度OCR API...")result = client.basicAccurate(img_bytes)# 檢查OCR結果if 'words_result' not in result:app.logger.warning(f"OCR返回異常結果: {result}")return jsonify(success=False, error="OCR識別失敗"), 500text = ' '.join(item['words'] for item in result.get('words_result', []))app.logger.info(f"OCR識別成功: 識別到{len(result['words_result'])}個文本塊")app.logger.debug(f"OCR識別結果: {text[:200]}...")return jsonify(success=True, text=text)except base64.binascii.Error as e:app.logger.error(f"Base64解碼失敗: {str(e)}", exc_info=True)return jsonify(success=False, error="無效的圖像數據"), 400except KeyError as e:app.logger.error(f"請求數據缺少必要字段: {str(e)}", exc_info=True)return jsonify(success=False, error="請求數據不完整"), 400except Exception as e:app.logger.exception("OCR處理未知錯誤")return jsonify(success=False, error="服務器內部錯誤"), 500@app.route('/explain', methods=['POST'])
def text_explanation():"""文本解釋接口"""try:app.logger.info("收到解釋請求")data = request.jsontext = data.get('text', '')if not text:app.logger.warning("解釋請求缺少文本數據")return jsonify(success=False, error='缺少文本數據'), 400app.logger.debug(f"待解釋文本: {text[:200]}... (長度:{len(text)})")explanation = llm.predict(text)app.logger.info("解釋生成成功")app.logger.debug(f"完整解釋結果: {explanation}")return jsonify(success=True, explanation=explanation)except KeyError as e:app.logger.error(f"請求數據缺少必要字段: {str(e)}", exc_info=True)return jsonify(success=False, error="請求數據不完整"), 400except Exception as e:app.logger.exception("解釋生成未知錯誤")return jsonify(success=False, error="服務器內部錯誤"), 500if __name__ == '__main__':app.run(debug=os.getenv('DEBUG_MODE', 'False').lower() == 'true')
EOF

4、啟動服務端

python main.py

四、效果

請添加圖片描述
請添加圖片描述
請添加圖片描述
請添加圖片描述
請添加圖片描述

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/pingmian/90293.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/90293.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/90293.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

研發過程都有哪些

產品規劃與定義 (Product Planning & Definition) 在詳細的需求調研之前&#xff0c;通常會進行市場分析、競品分析、確立產品目標和核心價值。這個階段決定了“我們要做什么”以及“為什么要做”。 系統設計與架構 (System & Architectural Design) 這是開發的“藍圖”…

舊物回收小程序系統開發——開啟綠色生活新篇章

在當今社會&#xff0c;環保已經成為全球關注的焦點話題。隨著人們生活水平的提高&#xff0c;消費能力不斷增強&#xff0c;各類物品的更新換代速度日益加快&#xff0c;大量舊物被隨意丟棄&#xff0c;不僅造成了資源的巨大浪費&#xff0c;還對環境產生了嚴重的污染。在這樣…

UE5 UI 水平框

文章目錄slot區分尺寸和對齊方式尺寸&#xff1a;自動模式尺寸&#xff1a;填充模式對齊常用設置所有按鈕大小一致&#xff0c;不受文本影響靠右排列和unity的HorizontalLayout不太一樣slot 以在水平框中放入帶文字的按鈕為例 UI如下布置 按鈕的大小受slot的尺寸、對齊和內部…

【Golang】Go語言變量

Go語言變量 文章目錄Go語言變量一、Go語言變量二、變量聲明2.1、第一種聲明方式2.2、第二種聲明方式2.3、第三種聲明方式2.4、多變量聲明2.5、打印變量占用字節一、Go語言變量 變量來源于數學&#xff0c;是計算機語言中能存儲計算結果或能表示值抽象的概念變量可以通過變量名…

Qt WebEngine Widgets的使用

一、Qt WebEngine基本概念Qt WebEngine中主要分為三個模塊&#xff1a;Qt WebEngine Widgets模塊&#xff0c;主要用于創建基于C Widgets部件的Web程序&#xff1b;Qt WebEngine模塊用來創建基于Qt Quick的Web程序&#xff1b;Qt WebEngine Core模塊用來與Chromeium交互。網頁玄…

【C++】標準模板庫(STL)—— 學習算法的利器

【C】標準模板庫&#xff08;STL&#xff09;—— 學習算法的利器學習 STL 需要注意的幾點及 STL 簡介一、什么是 STL&#xff1f;二、學習 STL 前的先修知識三、STL 常見容器特點對比四、學習 STL 的關鍵注意點五、STL 學習路線建議六、總結七、下一章 vector容器快速上手學習…

YOLO算法演進綜述:從YOLOv1到YOLOv13的技術突破與應用實踐,一文掌握YOLO家族全部算法!

引言&#xff1a;介紹目標檢測技術背景和YOLO算法的演進意義。YOLO算法發展歷程&#xff1a;使用階段劃分方式系統梳理各代YOLO的技術演進&#xff0c;包含早期奠基、效率優化、注意力機制和高階建模四個階段。YOLOv13的核心技術創新&#xff1a;詳細解析HyperACE機制、FullPAD…

快速將前端得依賴打為tar包(yarn.lock版本)并且推送至nexus私有依賴倉庫(筆記)

第一步創建js文件 文件名為downloadNpmPackage.jsprocess.env.NODE_TLS_REJECT_UNAUTHORIZED "0";const fs require("fs"); const path require("path"); const request require("request");// 設置依賴目錄 const downUrl "…

Unity VS Unreal Engine ,“電影像游戲的時代” 新手如何抉擇引擎?(結)

Unity VS Unreal Engine &#xff0c;“電影像游戲的時代” 新手如何抉擇引擎&#xff1f;(1)-CSDN博客 這是我的上一篇文章&#xff0c;如果你仍然困惑選擇引擎的事情&#xff0c;我們不妨從別的方面看看 注意&#xff1a;我們可能使用"UE5"來表示Unreal Engine系…

EVAL長度限制突破方法

EVAL長度限制突破方法 <?php $param $_REQUEST[param]; If (strlen($param) < 17 && stripos($param, eval) false && stripos($param, assert) false) //長度小于17&#xff0c;沒有eval和assert關鍵字 {eval($param); } //stripos — 查找字符串…

Linux部署.net Core 環境

我的環境 直接下載安裝就可以了 wget https://builds.dotnet.microsoft.com/dotnet/Sdk/8.0.315/dotnet-sdk-8.0.315-linux-x64.tar.gzmkdir -p $HOME/dotnet && tar zxf dotnet-sdk-8.0.315-linux-x64.tar.gz -C $HOME/dotnet export DOTNET_ROOT$HOME/dotnet expor…

ARM-定時器-PWM通道輸出

學習內容需求點亮4個燈&#xff0c;采用pwm的方式。定時器通道引腳AFLED序號T3CH0PD12AF2LED5CH1PD13AF2LED6CH2PD14AF2LED7CH3PD15AF2LED8實現LED5, LED6, LED7, LED8呼吸燈效果通用定時器多通道點亮T3定時器下的多個通道的燈。開發流程添加Timer依賴初始化PWM相關GPIO初始化P…

javaSE(List集合ArrayList實現類與LinkedList實現類)day15

目錄 List集合&#xff1a; 1、ArrayList類&#xff1a; &#xff08;1&#xff09;數據結構&#xff1a; &#xff08;2&#xff09;擴容機制 &#xff08;3&#xff09;ArrayList的初始化&#xff1a; &#xff08;4&#xff09;ArrayList的添加元素方法 &#xff08;5…

解決 WSL 中無法訪問 registry-1.docker.io/v2/,無法用 docker 拉取 image

文章目錄無法拉取docker鏡像補充遷移 WSL 位置Install Docker無法拉取docker鏡像 docker run hello-world Unable to find image hello-world:latest locally docker: Error response from daemon: Get "https://registry-1.docker.io/v2/": context deadline excee…

【C++】簡單學——list類

模擬實現之前需要了解的概念帶頭雙向鏈表&#xff08;double-linked&#xff09;&#xff0c;允許在任何位置進行插入區別相比vector和string&#xff0c;多了這個已經沒有下標[ ]了&#xff0c;因為迭代器其實才是主流&#xff08;要包頭文件<list>&#xff09;方法構造…

Qt 國際化與本地化完整解決方案

在全球化的今天&#xff0c;軟件支持多語言和本地化&#xff08;Internationalization & Localization&#xff0c;簡稱i18n & l10n&#xff09;已成為基本需求。Qt提供了一套完整的解決方案&#xff0c;幫助開發者輕松實現應用程序的國際化支持。本文將從原理到實踐&a…

MNIST 手寫數字識別模型分析

功能概述 這段代碼實現了一個基于TensorFlow和Keras的MNIST手寫數字識別模型。主要功能包括&#xff1a; 加載并預處理MNIST數據集構建一個簡單的全連接神經網絡模型訓練模型并評估其性能使用訓練好的模型進行預測保存和加載模型 代碼解析 1. 導入必要的庫 import matplot…

進階系統策略

該策略主要基于價格動態分析,結合多種技術指標和數學計算來生成交易信號。其核心邏輯包括: 1. 價格極值計算:首先,策略計算給定周期(由`Var3`定義)內的最高價和最低價,分別存儲在`Var12`和`Var13`中。這一步驟旨在捕捉價格的短期波動范圍。 2. 相對位置計算:接著,策…

【Linux內核】Linux驅動開發

推薦書籍&#xff1a; 《Linux內核探秘&#xff1a;深入解析文件系統和設備驅動的架構與設計》 知識點 x86的IO地址空間和內存地址空間是獨立的兩套地址空間&#xff0c;并且使用不同的指令訪問。MOV, IN, OUT。內存映射I/O可以將IO映射到內存。ARM等RISC采用統一編編址&#x…

MySQL用戶管理(15)

文章目錄前言一、用戶用戶信息創建用戶修改密碼刪除用戶二、數據庫的權限MySQL中的權限給用戶授權回收權限總結前言 其實與 Linux 操作系統類似&#xff0c;MySQL 中也有 超級用戶 和 普通用戶 之分 如果一個用戶只需要訪問 MySQL 中的某一個數據庫&#xff0c;甚至數據庫中的某…