650 lines
22 KiB
HTML
650 lines
22 KiB
HTML
<!doctype html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<title>OCR 测试页面 - Mistral & MinerU</title>
|
||
<style>
|
||
body {
|
||
font-family: sans-serif;
|
||
padding: 40px;
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
}
|
||
h1 {
|
||
text-align: center;
|
||
margin-bottom: 30px;
|
||
}
|
||
.section {
|
||
border: 1px solid #ddd;
|
||
border-radius: 8px;
|
||
padding: 20px;
|
||
margin-bottom: 30px;
|
||
background-color: #f9f9f9;
|
||
}
|
||
.section h2 {
|
||
margin-top: 0;
|
||
padding-bottom: 10px;
|
||
border-bottom: 2px solid #ddd;
|
||
}
|
||
.section.mistral h2 {
|
||
color: #2196f3;
|
||
border-bottom-color: #2196f3;
|
||
}
|
||
.section.mineru h2 {
|
||
color: #ff9800;
|
||
border-bottom-color: #ff9800;
|
||
}
|
||
button {
|
||
padding: 12px 24px;
|
||
font-size: 16px;
|
||
cursor: pointer;
|
||
margin-right: 10px;
|
||
margin-bottom: 10px;
|
||
border: none;
|
||
border-radius: 4px;
|
||
transition: background-color 0.2s;
|
||
}
|
||
.btn-primary {
|
||
background-color: #4caf50;
|
||
color: white;
|
||
}
|
||
.btn-primary:hover {
|
||
background-color: #45a049;
|
||
}
|
||
.btn-secondary {
|
||
background-color: #2196f3;
|
||
color: white;
|
||
}
|
||
.btn-secondary:hover {
|
||
background-color: #1976d2;
|
||
}
|
||
.btn-mineru-primary {
|
||
background-color: #ff9800;
|
||
color: white;
|
||
}
|
||
.btn-mineru-primary:hover {
|
||
background-color: #f57c00;
|
||
}
|
||
.btn-mineru-secondary {
|
||
background-color: #795548;
|
||
color: white;
|
||
}
|
||
.btn-mineru-secondary:hover {
|
||
background-color: #5d4037;
|
||
}
|
||
.status {
|
||
margin-top: 15px;
|
||
padding: 10px;
|
||
background-color: #fff;
|
||
border-radius: 4px;
|
||
min-height: 50px;
|
||
color: #555;
|
||
font-family: monospace;
|
||
font-size: 14px;
|
||
}
|
||
.error {
|
||
color: red;
|
||
}
|
||
.success {
|
||
color: green;
|
||
}
|
||
.info {
|
||
color: #666;
|
||
font-size: 14px;
|
||
margin-top: 10px;
|
||
}
|
||
</style>
|
||
|
||
<!-- 引入必要的库 -->
|
||
<script src="js/lib/jszip.min.js"></script>
|
||
<script src="js/storage/storage.js"></script>
|
||
<script src="js/api/api.js"></script>
|
||
<script src="js/ui/ocr-settings.js"></script>
|
||
<script src="js/process/ocr-manager.js"></script>
|
||
<script src="js/process/ocr-adapters/mistral-adapter.js"></script>
|
||
<script src="js/process/ocr-adapters/mineru-adapter.js"></script>
|
||
<script src="js/process/mineru-structured-translation.js"></script>
|
||
<!-- 通过 index.js 动态加载所有处理脚本(包含 translation.js 和 main.js) -->
|
||
<script src="js/process/index.js"></script>
|
||
<script src="js/history/history.js"></script>
|
||
|
||
<script>
|
||
// 由于已引入 ocr-settings.js,不再需要手动创建 window.ocrSettingsManager
|
||
// ocr-settings.js 会在 DOMContentLoaded 时自动初始化
|
||
|
||
// ============ 工具函数 ============
|
||
|
||
function addLog(elementId, msg, type = "info") {
|
||
const statusDiv = document.getElementById(elementId);
|
||
if (!statusDiv) {
|
||
console.warn(`[addLog] Element ${elementId} not found`);
|
||
return;
|
||
}
|
||
const p = document.createElement("div");
|
||
p.textContent = `[${new Date().toLocaleTimeString()}] ${msg}`;
|
||
if (type === "error") p.className = "error";
|
||
if (type === "success") p.className = "success";
|
||
statusDiv.appendChild(p);
|
||
statusDiv.scrollTop = statusDiv.scrollHeight;
|
||
}
|
||
|
||
// 兼容 main.js 可能调用的全局 addProgressLog
|
||
window.addProgressLog = function (msg) {
|
||
// 尝试找到当前活动的状态区域
|
||
const activeStatus = document.querySelector(".status");
|
||
if (activeStatus) {
|
||
const p = document.createElement("div");
|
||
p.textContent = msg;
|
||
activeStatus.appendChild(p);
|
||
activeStatus.scrollTop = activeStatus.scrollHeight;
|
||
}
|
||
console.log("[addProgressLog]", msg);
|
||
};
|
||
|
||
function clearStatus(elementId) {
|
||
const el = document.getElementById(elementId);
|
||
if (el) el.innerHTML = "";
|
||
}
|
||
|
||
// 读取 input 文件夹第一个文件
|
||
async function fetchFirstInputFile() {
|
||
const response = await fetch(
|
||
"http://localhost:3456/api/local/read-first-input",
|
||
);
|
||
if (!response.ok) {
|
||
const errText = await response.text();
|
||
throw new Error(`获取文件失败:${response.status} ${errText}`);
|
||
}
|
||
|
||
// 解析文件名
|
||
const contentDisposition =
|
||
response.headers.get("Content-Disposition") || "";
|
||
let filename = "unknown.pdf";
|
||
const filenameMatch = contentDisposition.match(/filename="?([^"]+)"?/);
|
||
if (filenameMatch) {
|
||
filename = decodeURIComponent(filenameMatch[1]);
|
||
}
|
||
|
||
const blob = await response.blob();
|
||
const file = new File([blob], filename, {
|
||
type: blob.type || "application/pdf",
|
||
});
|
||
|
||
return { file, blob };
|
||
}
|
||
|
||
// 创建历史记录并跳转(使用原系统保存到数据库的逻辑)
|
||
async function createHistoryRecordAndNavigate(file, blob, statusDivId) {
|
||
const processedAt = new Date().toISOString();
|
||
const recordId = `${file.name}_${file.size}`;
|
||
|
||
// 读取 blob 为 base64
|
||
const arrayBuffer = await blob.arrayBuffer();
|
||
const base64 = arrayBufferToBase64(arrayBuffer);
|
||
|
||
// 构造与 processSinglePdf 一致的记录格式
|
||
const record = {
|
||
id: recordId,
|
||
name: file.name,
|
||
size: file.size,
|
||
time: processedAt,
|
||
ocr: "", // 没有 OCR 内容
|
||
translation: "", // 没有翻译内容
|
||
images: [],
|
||
ocrChunks: [],
|
||
translatedChunks: [],
|
||
fileType: file.name.split(".").pop().toLowerCase(),
|
||
targetLanguage: "zh-CN",
|
||
relativePath: file.name,
|
||
sourceArchive: null,
|
||
originalContent: null,
|
||
originalEncoding: null,
|
||
originalBinary: base64, // 保存文件内容
|
||
originalExtension: file.name.split(".").pop().toLowerCase(),
|
||
// OCR/翻译元信息
|
||
ocrEngine: "none",
|
||
ocrSource: null,
|
||
translationModelName: "none",
|
||
translationModelCustomName: null,
|
||
translationModelId: null,
|
||
batchId: null,
|
||
batchOrder: null,
|
||
batchTotal: null,
|
||
batchTemplate: null,
|
||
batchFormats: null,
|
||
batchStartedAt: null,
|
||
batchOutputLanguage: null,
|
||
batchOriginalIndex: null,
|
||
batchAttempt: null,
|
||
batchZip: null,
|
||
// MinerU 结构化翻译元数据(无)
|
||
metadata: {
|
||
originalPdfBase64: base64, // 用于历史详情页查看原始 PDF
|
||
},
|
||
};
|
||
|
||
if (typeof window.saveResultToDB === "function") {
|
||
await window.saveResultToDB(record);
|
||
addLog(statusDivId, `已创建历史记录,ID: ${recordId}`, "success");
|
||
}
|
||
|
||
// 保存当前文档 ID
|
||
try {
|
||
localStorage.setItem("pbx_current_doc_id", recordId);
|
||
} catch (e) {
|
||
console.warn("Failed to save doc id to localStorage", e);
|
||
}
|
||
|
||
// 打开历史详情页
|
||
const historyUrl = `views/history/history_detail.html?id=${encodeURIComponent(recordId)}`;
|
||
window.open(historyUrl, "_blank");
|
||
|
||
return recordId;
|
||
}
|
||
|
||
// ============ Mistral 相关函数 ============
|
||
|
||
// Mistral: 读取并跳转(不做 OCR)
|
||
async function mistralOpenHistory() {
|
||
const btn = document.getElementById("mistralOpenBtn");
|
||
const statusId = "mistralStatus";
|
||
btn.disabled = true;
|
||
clearStatus(statusId);
|
||
addLog(statusId, "正在读取 input 文件夹的第一个文件...");
|
||
|
||
try {
|
||
const { file, blob } = await fetchFirstInputFile();
|
||
addLog(
|
||
statusId,
|
||
`成功读取文件:${file.name} (${(file.size / 1024).toFixed(2)} KB)`,
|
||
);
|
||
|
||
addLog(statusId, "正在创建历史记录并跳转...");
|
||
await createHistoryRecordAndNavigate(file, blob, statusId);
|
||
|
||
addLog(statusId, "✅ 已打开历史详情页", "success");
|
||
} catch (err) {
|
||
addLog(statusId, `❌ 错误:${err.message}`, "error");
|
||
} finally {
|
||
btn.disabled = false;
|
||
}
|
||
}
|
||
|
||
// Mistral: 处理并跳转(执行 OCR)
|
||
async function mistralProcessAndNavigate() {
|
||
const btn = document.getElementById("mistralProcessBtn");
|
||
const statusId = "mistralStatus";
|
||
btn.disabled = true;
|
||
clearStatus(statusId);
|
||
addLog(statusId, "正在读取 input 文件夹的第一个文件...");
|
||
|
||
try {
|
||
const { file, blob } = await fetchFirstInputFile();
|
||
addLog(
|
||
statusId,
|
||
`成功读取文件:${file.name} (${(file.size / 1024).toFixed(2)} KB)`,
|
||
);
|
||
addLog(statusId, "开始 Mistral OCR 处理...");
|
||
|
||
// 设置 localStorage 中的 OCR 配置
|
||
localStorage.setItem("ocrEngine", "mistral");
|
||
|
||
// 调用处理主逻辑
|
||
const result = await processSinglePdf(
|
||
file,
|
||
null, // mistral key 使用后端
|
||
null, // translation key
|
||
"none", // 翻译模型
|
||
null, // config
|
||
4000, // 默认 token 限制
|
||
"zh-CN", // 目标语言
|
||
async () => {}, // slot acquire
|
||
() => {}, // slot release
|
||
"", // sys prompt
|
||
"", // user prompt
|
||
false, // use custom
|
||
null, // batchContext
|
||
(f) => {
|
||
addLog(statusId, `处理成功: ${f.name}`, "success");
|
||
},
|
||
);
|
||
|
||
if (result.error) {
|
||
throw new Error(result.error);
|
||
}
|
||
|
||
addLog(statusId, "OCR 完成,正在跳转到详情页...");
|
||
|
||
const recordId = result.id || `${file.name}_${file.size}`;
|
||
|
||
// 等待 showHistoryDetail 函数可用
|
||
const waitForShowHistoryDetail = (callback, retries = 10) => {
|
||
if (typeof window.showHistoryDetail === "function") {
|
||
callback();
|
||
} else if (retries > 0) {
|
||
setTimeout(
|
||
() => waitForShowHistoryDetail(callback, retries - 1),
|
||
100,
|
||
);
|
||
} else {
|
||
window.location.href = `views/history/history_detail.html?id=${recordId}`;
|
||
}
|
||
};
|
||
|
||
waitForShowHistoryDetail(() => {
|
||
window.showHistoryDetail(recordId);
|
||
});
|
||
} catch (err) {
|
||
addLog(statusId, `❌ 错误:${err.message}`, "error");
|
||
} finally {
|
||
btn.disabled = false;
|
||
}
|
||
}
|
||
|
||
// ============ MinerU 相关函数 ============
|
||
|
||
// MinerU: 读取并跳转(不做 OCR)
|
||
async function mineruOpenHistory() {
|
||
const btn = document.getElementById("mineruOpenBtn");
|
||
const statusId = "mineruStatus";
|
||
btn.disabled = true;
|
||
clearStatus(statusId);
|
||
addLog(statusId, "正在读取 input 文件夹的第一个文件...");
|
||
|
||
try {
|
||
const { file, blob } = await fetchFirstInputFile();
|
||
addLog(
|
||
statusId,
|
||
`成功读取文件:${file.name} (${(file.size / 1024).toFixed(2)} KB)`,
|
||
);
|
||
|
||
addLog(statusId, "正在创建历史记录并跳转...");
|
||
await createHistoryRecordAndNavigate(file, blob, statusId);
|
||
|
||
addLog(statusId, "✅ 已打开历史详情页", "success");
|
||
} catch (err) {
|
||
addLog(statusId, `❌ 错误:${err.message}`, "error");
|
||
} finally {
|
||
btn.disabled = false;
|
||
}
|
||
}
|
||
|
||
// MinerU: 处理并跳转(执行 OCR)
|
||
async function mineruProcessAndNavigate() {
|
||
const btn = document.getElementById("mineruProcessBtn");
|
||
const statusId = "mineruStatus";
|
||
btn.disabled = true;
|
||
clearStatus(statusId);
|
||
addLog(statusId, "正在读取 input 文件夹的第一个文件...");
|
||
|
||
try {
|
||
const { file, blob } = await fetchFirstInputFile();
|
||
addLog(
|
||
statusId,
|
||
`成功读取文件:${file.name} (${(file.size / 1024).toFixed(2)} KB)`,
|
||
);
|
||
|
||
// 设置 localStorage 中的 OCR 配置
|
||
localStorage.setItem("ocrEngine", "mineru");
|
||
localStorage.setItem("ocrMinerUWorkerUrl", "http://localhost:3456");
|
||
localStorage.setItem("ocrMinerUEnableOcr", "true");
|
||
localStorage.setItem("ocrMinerUEnableFormula", "true");
|
||
localStorage.setItem("ocrMinerUEnableTable", "true");
|
||
localStorage.setItem("ocrMinerUTokenMode", "backend");
|
||
|
||
addLog(statusId, "开始 MinerU OCR 处理...");
|
||
|
||
// 调用处理主逻辑
|
||
const result = await processSinglePdf(
|
||
file,
|
||
null, // mineru key 使用后端
|
||
null, // translation key
|
||
"none", // 翻译模型
|
||
null, // config
|
||
4000, // 默认 token 限制
|
||
"zh-CN", // 目标语言
|
||
async () => {}, // slot acquire
|
||
() => {}, // slot release
|
||
"", // sys prompt
|
||
"", // user prompt
|
||
false, // use custom
|
||
null, // batchContext
|
||
(f) => {
|
||
addLog(statusId, `处理成功: ${f.name}`, "success");
|
||
},
|
||
);
|
||
|
||
if (result.error) {
|
||
throw new Error(result.error);
|
||
}
|
||
|
||
addLog(statusId, "OCR 完成,正在跳转到详情页...");
|
||
|
||
const recordId = result.id || `${file.name}_${file.size}`;
|
||
|
||
// 等待 showHistoryDetail 函数可用
|
||
const waitForShowHistoryDetail = (callback, retries = 10) => {
|
||
if (typeof window.showHistoryDetail === "function") {
|
||
callback();
|
||
} else if (retries > 0) {
|
||
setTimeout(
|
||
() => waitForShowHistoryDetail(callback, retries - 1),
|
||
100,
|
||
);
|
||
} else {
|
||
window.location.href = `views/history/history_detail.html?id=${recordId}`;
|
||
}
|
||
};
|
||
|
||
waitForShowHistoryDetail(() => {
|
||
window.showHistoryDetail(recordId);
|
||
});
|
||
} catch (err) {
|
||
addLog(statusId, `❌ 错误:${err.message}`, "error");
|
||
} finally {
|
||
btn.disabled = false;
|
||
}
|
||
}
|
||
|
||
// 从 localStorage 读取 API Key
|
||
|
||
// 通义百炼:结构化翻译测试
|
||
async function tongyiStructuredTranslation() {
|
||
const btn = document.getElementById("tongyiStructuredBtn");
|
||
const statusId = "tongyiStructuredStatus";
|
||
btn.disabled = true;
|
||
clearStatus(statusId);
|
||
addLog(statusId, "正在读取 input 文件夹的第一个文件...");
|
||
|
||
try {
|
||
// 等待 processModule 初始化完成
|
||
if (
|
||
typeof processModule === "undefined" ||
|
||
typeof processModule.translateMarkdown !== "function"
|
||
) {
|
||
addLog(statusId, "正在等待处理模块初始化...", "info");
|
||
await new Promise((resolve) => {
|
||
const checkInterval = setInterval(() => {
|
||
if (
|
||
typeof processModule !== "undefined" &&
|
||
typeof processModule.translateMarkdown === "function"
|
||
) {
|
||
clearInterval(checkInterval);
|
||
resolve();
|
||
}
|
||
}, 100);
|
||
});
|
||
addLog(statusId, "处理模块已初始化", "success");
|
||
}
|
||
|
||
const { file, blob } = await fetchFirstInputFile();
|
||
addLog(
|
||
statusId,
|
||
`成功读取文件:${file.name} (${(file.size / 1024).toFixed(2)} KB)`,
|
||
);
|
||
addLog(statusId, "开始 MinerU OCR 处理(结构化模式)...");
|
||
|
||
// 设置 localStorage 中的 OCR 配置
|
||
localStorage.setItem("ocrEngine", "mineru");
|
||
localStorage.setItem("ocrMinerUWorkerUrl", "http://localhost:3456");
|
||
localStorage.setItem("ocrMinerUEnableOcr", "true");
|
||
localStorage.setItem("ocrMinerUEnableFormula", "true");
|
||
localStorage.setItem("ocrMinerUEnableTable", "true");
|
||
localStorage.setItem("ocrMinerUTokenMode", "backend");
|
||
|
||
addLog(statusId, "开始 OCR 处理...");
|
||
|
||
// 由于使用后端代理,无需前端API Key
|
||
const targetLanguageSelect = document.getElementById("targetLanguageSelect");
|
||
const targetLanguage = targetLanguageSelect ? targetLanguageSelect.value : "zh-CN"; // 默认为中文简体
|
||
|
||
// 调用处理主逻辑(包含结构化翻译)
|
||
const result = await processSinglePdf(
|
||
file,
|
||
null, // mineru key 使用后端
|
||
{
|
||
id: "tongyi-key",
|
||
value: "sk-proxy", // 使用占位符密钥,实际调用由后端处理
|
||
},
|
||
"proxy", // 使用 proxy 进行翻译
|
||
null, // config
|
||
4000, // 默认 token 限制
|
||
targetLanguage, // 目标语言
|
||
async () => {}, // slot acquire
|
||
() => {}, // slot release
|
||
"", // sys prompt
|
||
"", // user prompt
|
||
false, // use custom
|
||
null, // batchContext
|
||
(f) => {
|
||
addLog(statusId, `处理成功: ${f.name}`, "success");
|
||
},
|
||
);
|
||
|
||
if (result.error) {
|
||
throw new Error(result.error);
|
||
}
|
||
|
||
addLog(statusId, "✅ OCR + 翻译完成,正在跳转到详情页...");
|
||
|
||
const recordId = result.id || `${file.name}_${file.size}`;
|
||
|
||
// 等待 showHistoryDetail 函数可用
|
||
const waitForShowHistoryDetail = (callback, retries = 10) => {
|
||
if (typeof window.showHistoryDetail === "function") {
|
||
callback();
|
||
} else if (retries > 0) {
|
||
setTimeout(
|
||
() => waitForShowHistoryDetail(callback, retries - 1),
|
||
100,
|
||
);
|
||
} else {
|
||
window.location.href = `views/history/history_detail.html?id=${recordId}`;
|
||
}
|
||
};
|
||
|
||
waitForShowHistoryDetail(() => {
|
||
window.showHistoryDetail(recordId);
|
||
});
|
||
} catch (err) {
|
||
addLog(statusId, `❌ 错误:${err.message}`, "error");
|
||
} finally {
|
||
btn.disabled = false;
|
||
}
|
||
}
|
||
</script>
|
||
</head>
|
||
<body>
|
||
<h1>OCR 测试页面</h1>
|
||
|
||
<!-- Mistral 测试区域 -->
|
||
<div class="section mistral">
|
||
<h2>🔷 Mistral OCR 测试</h2>
|
||
<div class="info">
|
||
代理后端: http://localhost:3456/api/llm/mistral/...<br />
|
||
测试功能:读取 input 文件夹第一个 PDF 文件
|
||
</div>
|
||
<div style="margin-top: 15px">
|
||
<button
|
||
id="mistralOpenBtn"
|
||
class="btn-primary"
|
||
onclick="mistralOpenHistory()"
|
||
>
|
||
📂 读取并打开历史界面
|
||
</button>
|
||
<button
|
||
id="mistralProcessBtn"
|
||
class="btn-secondary"
|
||
onclick="mistralProcessAndNavigate()"
|
||
>
|
||
⚙️ 处理并跳转 (OCR)
|
||
</button>
|
||
</div>
|
||
<div id="mistralStatus" class="status"></div>
|
||
</div>
|
||
|
||
<!-- MinerU 测试区域 -->
|
||
<div class="section mineru">
|
||
<h2>🔶 MinerU OCR 测试</h2>
|
||
<div class="info">
|
||
代理后端: http://localhost:3456/mineru/...<br />
|
||
测试功能:读取 input 文件夹第一个 PDF 文件
|
||
</div>
|
||
<div style="margin-top: 15px">
|
||
<button
|
||
id="mineruOpenBtn"
|
||
class="btn-mineru-primary"
|
||
onclick="mineruOpenHistory()"
|
||
>
|
||
📂 读取并打开历史界面(仅跳转,不会OCR)我平常使用这个按钮测试这个功能
|
||
</button>
|
||
<button
|
||
id="mineruProcessBtn"
|
||
class="btn-mineru-secondary"
|
||
onclick="mineruProcessAndNavigate()"
|
||
>
|
||
⚙️ 处理并跳转 (OCR)
|
||
</button>
|
||
</div>
|
||
<div id="mineruStatus" class="status"></div>
|
||
|
||
<div
|
||
style="margin-top: 20px; padding-top: 20px; border-top: 2px dashed #ccc"
|
||
>
|
||
<h3 style="margin-top: 0; color: #ff9800">📋 MinerU + Qwen 翻译</h3>
|
||
|
||
<div style="margin-top: 10px">
|
||
<label for="targetLanguageSelect" style="display: inline-block; width: 120px; margin-right: 10px;">目标语言:</label>
|
||
<select id="targetLanguageSelect" style="padding: 5px; border: 1px solid #ccc; border-radius: 4px;">
|
||
<option value="zh-CN">中文简体</option>
|
||
<option value="zh-TW">中文繁體</option>
|
||
<option value="en">English</option>
|
||
<option value="ja">日本語</option>
|
||
<option value="ko">한국어</option>
|
||
<option value="fr">Français</option>
|
||
<option value="de">Deutsch</option>
|
||
<option value="es">Español</option>
|
||
<option value="ru">Русский</option>
|
||
<option value="ar">العربية</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div style="margin-top: 10px">
|
||
<button
|
||
id="tongyiStructuredBtn"
|
||
class="btn-mineru-primary"
|
||
onclick="tongyiStructuredTranslation()"
|
||
>
|
||
🔤 执行结构化翻译
|
||
</button>
|
||
</div>
|
||
<div
|
||
id="tongyiStructuredStatus"
|
||
class="status"
|
||
style="margin-top: 10px"
|
||
></div>
|
||
</div>
|
||
</div>
|
||
</body>
|
||
</html>
|