diff --git a/backend/tools/pdf_acceptance_gate.py b/backend/tools/pdf_acceptance_gate.py index 742328b1..5925913a 100644 --- a/backend/tools/pdf_acceptance_gate.py +++ b/backend/tools/pdf_acceptance_gate.py @@ -9,7 +9,7 @@ from pathlib import Path from typing import Callable, Dict, List, Optional import httpx -from PIL import Image, ImageDraw +from PIL import Image, ImageDraw, ImageFont from reportlab.lib.utils import ImageReader from reportlab.pdfbase import pdfmetrics from reportlab.pdfbase.cidfonts import UnicodeCIDFont @@ -133,6 +133,44 @@ def _make_image_contract(low_res: bool) -> bytes: return pdf_buffer.getvalue() +def _load_cjk_font(size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont: + font_candidates = [ + Path("C:/Windows/Fonts/msyh.ttc"), + Path("C:/Windows/Fonts/simsun.ttc"), + Path("C:/Windows/Fonts/simhei.ttf"), + ] + for font_path in font_candidates: + if font_path.exists(): + try: + return ImageFont.truetype(str(font_path), size=size) + except OSError: + continue + return ImageFont.load_default() + + +def make_cn_image_contract() -> bytes: + size = (900, 560) + image = Image.new("RGB", size, "white") + draw = ImageDraw.Draw(image) + font = _load_cjk_font(30) + lines = [ + "采购合同扫描样本", + "甲方应在验收合格并收到发票后十五个工作日内付款。", + "乙方应对商业秘密和技术资料承担保密义务。", + "如乙方延期交付,双方应按约承担违约责任。", + ] + y = 48 + for line in lines: + draw.text((40, y), line, fill="black", font=font) + y += 86 + + pdf_buffer = io.BytesIO() + pdf = canvas.Canvas(pdf_buffer) + pdf.drawImage(ImageReader(image), 36, 280, width=540, height=340) + pdf.save() + return pdf_buffer.getvalue() + + def make_image_only_pdf() -> bytes: return _make_image_contract(low_res=False) @@ -211,6 +249,17 @@ def build_cases(sample_contract_path: Optional[str]) -> List[PdfCase]: require_message_contains="OCR 识别质量不足", notes="OCR 第一批实现后,这类样本允许 WARN 或 BLOCK,但不能伪装成高质量 PASS。", ), + PdfCase( + case_id="C-003", + category="C", + label="中文扫描合同 PDF(OCR第一批)", + expectation="ocr_supported", + file_name="gate-cn-image.pdf", + build_bytes=make_cn_image_contract, + allow_ocr_statuses=["PASS", "WARN"], + require_preview=["采购合同扫描样本", "甲方应在验收合格并收到发票后十五个工作日内付款。"], + notes="OCR 第一批实现后,中文扫描合同应至少进入正式审查,并暴露 OCR 门禁结果。", + ), ] if sample_contract_path: diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 2da7b4df..d7c77143 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -2168,6 +2168,43 @@ a { border: 1px solid rgba(22, 119, 255, 0.12); } +.review-detail-ocr-panel { + margin-top: 18px; + padding: 16px 18px; + border-radius: 18px; + border: 1px solid rgba(148, 163, 184, 0.16); + background: rgba(248, 250, 252, 0.92); + display: flex; + flex-direction: column; + gap: 12px; +} + +.review-detail-ocr-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 12px; +} + +.review-detail-ocr-card { + padding: 12px 14px; + border-radius: 14px; + background: #fff; + border: 1px solid rgba(148, 163, 184, 0.14); + display: flex; + flex-direction: column; + gap: 6px; +} + +.review-detail-ocr-card span { + color: #64748b; + font-size: 12px; +} + +.review-detail-ocr-card strong { + color: #0f172a; + font-size: 15px; +} + .review-detail-download { margin-top: 12px; } @@ -4046,6 +4083,7 @@ body::selection { .review-detail-hero-stats, .review-detail-decision-strip, .review-detail-group-grid, + .review-detail-ocr-grid, .chat-review-meta-grid, .chat-answer-summary-grid, .chat-action-card-grid, diff --git a/frontend/src/app/reviews/[id]/page.tsx b/frontend/src/app/reviews/[id]/page.tsx index 1a2ce421..8c189014 100644 --- a/frontend/src/app/reviews/[id]/page.tsx +++ b/frontend/src/app/reviews/[id]/page.tsx @@ -111,6 +111,43 @@ function formatSourcePreview(value: string | null | undefined) { return value.length > 180 ? `${value.slice(0, 180)}...` : value; } +function formatDocumentKind(value: string | null | undefined) { + if (!value) return "-"; + const kindMap: Record = { + text_pdf: "文本型 PDF", + scanned_pdf: "扫描件 PDF", + mixed_pdf: "混合型 PDF", + unknown_pdf: "未知 PDF", + platform_review_report_pdf: "平台审查报告 PDF", + }; + return kindMap[value] ?? value; +} + +function formatExtractionMode(value: string | null | undefined) { + if (!value) return "-"; + const modeMap: Record = { + text: "文本抽取", + ocr: "OCR 抽取", + reject_report_pdf: "报告型 PDF 拦截", + ocr_disabled: "OCR 未启用", + ocr_runtime_missing: "OCR 运行时缺失", + ocr_parse_failed: "OCR 前解析失败", + }; + return modeMap[value] ?? value; +} + +function formatOcrGateStatus(value: string | null | undefined) { + if (!value) return "-"; + return value; +} + +function getOcrGateColor(value: string | null | undefined) { + if (value === "PASS") return "green"; + if (value === "WARN") return "gold"; + if (value === "BLOCK") return "red"; + return "default"; +} + function formatClauseContent(value: string | undefined) { if (!value) return "暂无条款内容"; return value.length > 180 ? `${value.slice(0, 180)}...` : value; @@ -336,6 +373,8 @@ export default function ReviewDetailPage({ params }: { params: Promise<{ id: str const overallRisk = formatRisk(result?.risk_summary.overall); const reviewOpinion = result?.review_opinion ?? "-"; const recommendation = formatRecommendation(result?.recommendation); + const documentAnalysis = result?.confidence_summary?.document_analysis ?? null; + const ocrQuality = documentAnalysis?.ocr_quality ?? null; const detailAnchors = [ { href: "#review-risk-overview", label: "风险总览" }, @@ -644,7 +683,6 @@ export default function ReviewDetailPage({ params }: { params: Promise<{ id: str 审查报告: { event.preventDefault(); void handleDownloadReport(); @@ -708,6 +746,41 @@ export default function ReviewDetailPage({ params }: { params: Promise<{ id: str {detail?.report_status === "partial" ? 报告部分完成 : null} {detail?.can_retry ? 可重试 : null} + {documentAnalysis ? ( +
+ OCR Quality Gate + 扫描件识别与质量门禁 +
+
+ 文档类型 + {formatDocumentKind(documentAnalysis.document_kind)} +
+
+ 抽取模式 + {formatExtractionMode(documentAnalysis.extraction_mode)} +
+
+ OCR 门禁 + {formatOcrGateStatus(documentAnalysis.ocr_gate_status)} +
+
+ 平均置信度 + {ocrQuality?.mean_confidence == null ? "-" : `${ocrQuality.mean_confidence}`} +
+
+ 识别字符数 + {ocrQuality?.char_count ?? "-"} +
+
+ 噪声比例 + {ocrQuality?.noise_ratio == null ? "-" : `${ocrQuality.noise_ratio}`} +
+
+ {ocrQuality?.failure_message ? ( +
OCR 提示:{ocrQuality.failure_message}
+ ) : null} +
+ ) : null} {formatLifecycleStatus(detail?.parse_status)} {formatLifecycleStatus(detail?.review_status)} diff --git a/frontend/src/services/review.ts b/frontend/src/services/review.ts index 9a11064b..8567da60 100644 --- a/frontend/src/services/review.ts +++ b/frontend/src/services/review.ts @@ -71,6 +71,26 @@ export type ReviewResult = { }>; review_opinion: string; recommendation: string; + confidence_summary?: { + analysis_mode?: string; + provider_mode?: string; + router_status?: string; + document_analysis?: { + document_kind?: string; + extraction_mode?: string; + ocr_gate_status?: string; + ocr_quality?: { + status?: string; + char_count?: number; + token_count?: number; + line_count?: number; + printable_ratio?: number; + noise_ratio?: number; + mean_confidence?: number | null; + failure_message?: string | null; + }; + }; + }; }; export type ReviewReport = { @@ -80,7 +100,9 @@ export type ReviewReport = { }; export async function createReview(formData: FormData) { - const response = await http.post>("/reviews", formData); + const response = await http.post>("/reviews", formData, { + timeout: 180000, + }); return response.data; } diff --git a/project-docs/OCR与扫描件审查增强方案-2026-05-04.md b/project-docs/OCR与扫描件审查增强方案-2026-05-04.md index ac32e54c..5fd95b2a 100644 --- a/project-docs/OCR与扫描件审查增强方案-2026-05-04.md +++ b/project-docs/OCR与扫描件审查增强方案-2026-05-04.md @@ -83,12 +83,29 @@ - 当前仍由 OCR 质量门禁拦截 - 属于受控失败,不再伪成功 +3. `C-003` 中文扫描合同 PDF + - 已升级为可正式支持 + - 当前可达到 `WARN` + +4. **前端显示 OCR 门禁状态** + - 审查详情页已显示: + - 文档类型 + - 抽取模式 + - OCR 门禁状态 + - 平均置信度 + - 识别字符数 + - 噪声比例 + +5. **中文扫描合同样本库清单** + - 已新增: + - `project-docs/中文扫描合同样本库清单-2026-05-04.csv` + ## 4. 未在第一批实现的内容 1. 扫描件中文复杂版式专项优化 2. 表格型扫描 PDF 专项优化 3. 多页扫描件跨页段落拼接优化 -4. OCR 结果前端可视化展示 +4. OCR 结果前端更多场景可视化展示 5. OCR 失败原因的人机交互优化 ## 5. 当前验收口径 diff --git a/project-docs/PDF样本验收分级清单-2026-05-04.md b/project-docs/PDF样本验收分级清单-2026-05-04.md index 76f378a5..1f75e2bb 100644 --- a/project-docs/PDF样本验收分级清单-2026-05-04.md +++ b/project-docs/PDF样本验收分级清单-2026-05-04.md @@ -226,6 +226,7 @@ OCR上线后验收标准: - `C-001` 图片型 PDF(OCR第一批) - `C-002` 低清晰度图片型 PDF(OCR第一批) +- `C-003` 中文扫描合同 PDF(OCR第一批) 当前门禁判定规则: @@ -233,5 +234,6 @@ OCR上线后验收标准: 2. B类全部必须被正确拦截 3. `C-001` 这类清晰图片型 PDF 在 OCR 第一批中应至少达到 `PASS/WARN` 4. `C-002` 这类低清晰度图片型 PDF 在 OCR 第一批中允许 `WARN/BLOCK`,但不能伪装成高质量 `PASS` +5. `C-003` 这类中文扫描合同 PDF 在 OCR 第一批中应至少达到 `PASS/WARN` 如果未来 OCR 继续增强,应继续在这支门禁里把更多 C类样本逐个升级为 `PASS / WARN / BLOCK` 判定。 diff --git a/project-docs/中文扫描合同样本库清单-2026-05-04.csv b/project-docs/中文扫描合同样本库清单-2026-05-04.csv new file mode 100644 index 00000000..764a7ecd --- /dev/null +++ b/project-docs/中文扫描合同样本库清单-2026-05-04.csv @@ -0,0 +1,6 @@ +sample_id,title,document_type,language,current_stage,current_expectation,gate_case,source_mode,notes +C-003,中文扫描采购合同(清晰),scanned_pdf,zh-CN,ocr_first_batch,PASS/WARN,C-003,dynamic_generated,当前已接入 PDF 验收门禁;用于验证中文 OCR 进入正式审查链路 +C-004,中文扫描采购合同(低清晰度),scanned_pdf,zh-CN,planned_second_batch,WARN/BLOCK,,planned_fixture,下一批用于验证中文低质量扫描件的 OCR 质量门禁 +C-005,中文扫描服务合同(多页),scanned_pdf,zh-CN,planned_second_batch,PASS/WARN,,planned_fixture,下一批用于验证多页扫描件、跨页拼接与页眉页脚清洗 +C-006,中文扫描劳动合同(盖章页),scanned_pdf,zh-CN,planned_second_batch,PASS/WARN,,planned_fixture,下一批用于验证盖章、签章页与正文分离 +C-007,中文扫描框架协议(表格较多),scanned_pdf,zh-CN,planned_second_batch,WARN/BLOCK,,planned_fixture,下一批用于验证表格型扫描 PDF 的 OCR 与后清洗