1228 lines
38 KiB
Markdown
1228 lines
38 KiB
Markdown
# 文档图片质量校验模块第3版接口与落地清单
|
||
|
||
## 1. 目标边界
|
||
|
||
本模块用于对案卷文档中的拍照图片、扫描图片、附件图片做“图片清晰度预警检测”。
|
||
|
||
本模块的边界固定如下:
|
||
|
||
- 与现有 OCR 抽取、评查主流程完全独立。
|
||
- 即使检测出图片模糊,也不能阻断上传、不能阻断 OCR、不能阻断评查。
|
||
- 结果只做预警展示和留痕,分三档:
|
||
- `pass`:通过
|
||
- `review`:疑似模糊待人工确认
|
||
- `reject`:不通过需重拍
|
||
- 第一版优先支持:
|
||
- 图片文件
|
||
- PDF / 扫描 PDF
|
||
- 图片附件
|
||
- PDF 附件
|
||
- `doc/docx/wps` 第一版允许降级为“正文第 N 张图”定位,不强承诺精确页码。
|
||
|
||
## 2. 建议代码落点
|
||
|
||
### 2.1 后端目录
|
||
|
||
建议新增如下目录与文件:
|
||
|
||
- `fastapi_modules/fastapi_leaudit/domian/vo/imageQualityVo.py`
|
||
- `fastapi_modules/fastapi_leaudit/services/imageQualityService.py`
|
||
- `fastapi_modules/fastapi_leaudit/services/impl/imageQualityServiceImpl.py`
|
||
- `fastapi_modules/fastapi_leaudit/controllers/imageQualityController.py`
|
||
- `fastapi_modules/fastapi_leaudit/image_quality/tasks.py`
|
||
- `fastapi_modules/fastapi_leaudit/image_quality/runner.py`
|
||
- `fastapi_modules/fastapi_leaudit/image_quality/storage_adapter.py`
|
||
- `fastapi_modules/fastapi_leaudit/image_quality/input_resolver.py`
|
||
- `fastapi_modules/fastapi_leaudit/image_quality/extractors.py`
|
||
- `fastapi_modules/fastapi_leaudit/image_quality/detector.py`
|
||
- `fastapi_modules/fastapi_leaudit/image_quality/config_resolver.py`
|
||
|
||
### 2.2 现有文件改动点
|
||
|
||
- `fastapi_modules/fastapi_leaudit/controllers/documentController.py`
|
||
- `fastapi_modules/fastapi_leaudit/services/documentService.py`
|
||
- `fastapi_modules/fastapi_leaudit/services/impl/documentServiceImpl.py`
|
||
- `fastapi_modules/fastapi_leaudit/domian/vo/documentVo.py`
|
||
- `fastapi_admin/config/_settings.py`
|
||
- `fastapi_admin/config/__init__.pyi`
|
||
|
||
## 3. 新增 VO 建议
|
||
|
||
按当前仓库 `documentVo.py / auditVo.py / reviewPointVo.py` 风格,建议单独新增 `imageQualityVo.py`。
|
||
|
||
### 3.1 配置相关 VO
|
||
|
||
```python
|
||
from pydantic import BaseModel, Field
|
||
|
||
|
||
class ImageQualityConfigItemVO(BaseModel):
|
||
"""图片质量校验配置项。"""
|
||
|
||
id: int = Field(..., description="配置ID")
|
||
scopeType: str = Field(..., description="作用域类型:global/doc_type/region/doc_type_region")
|
||
documentTypeId: int | None = Field(None, description="文档类型ID")
|
||
region: str | None = Field(None, description="地区")
|
||
enabled: bool = Field(..., description="是否启用")
|
||
warnThreshold: float | None = Field(None, description="疑似模糊阈值")
|
||
rejectThreshold: float | None = Field(None, description="不通过阈值")
|
||
maxImagesPerDoc: int | None = Field(None, description="单文档最大检测图片数")
|
||
maxConcurrency: int | None = Field(None, description="单任务并发数")
|
||
updatedAt: str | None = Field(None, description="更新时间")
|
||
|
||
|
||
class ImageQualityConfigUpsertDTO(BaseModel):
|
||
"""图片质量校验配置新增/更新请求。"""
|
||
|
||
scopeType: str = Field(..., description="作用域类型")
|
||
documentTypeId: int | None = Field(None, description="文档类型ID")
|
||
region: str | None = Field(None, description="地区")
|
||
enabled: bool = Field(..., description="是否启用")
|
||
warnThreshold: float | None = Field(None, description="疑似模糊阈值")
|
||
rejectThreshold: float | None = Field(None, description="不通过阈值")
|
||
maxImagesPerDoc: int | None = Field(None, description="单文档最大检测图片数")
|
||
maxConcurrency: int | None = Field(None, description="单任务并发数")
|
||
```
|
||
|
||
### 3.2 明细与摘要相关 VO
|
||
|
||
```python
|
||
class ImageQualityItemVO(BaseModel):
|
||
"""单张图片质检明细。"""
|
||
|
||
itemId: int = Field(..., description="明细ID")
|
||
runId: int = Field(..., description="质检运行ID")
|
||
documentId: int = Field(..., description="文档ID")
|
||
documentFileId: int | None = Field(None, description="文档文件ID")
|
||
sourceKind: str = Field(..., description="来源类型")
|
||
sourceFileName: str | None = Field(None, description="来源文件名")
|
||
sourcePageNum: int | None = Field(None, description="来源页码")
|
||
imageIndexInPage: int | None = Field(None, description="页内图片序号")
|
||
imageIndexInFile: int | None = Field(None, description="文件内图片序号")
|
||
bbox: dict | list | None = Field(None, description="图片定位框")
|
||
qualityStatus: str = Field(..., description="pass/review/reject")
|
||
qualityScore: float | None = Field(None, description="清晰度分值")
|
||
reasonCode: str | None = Field(None, description="原因编码")
|
||
reasonText: str | None = Field(None, description="原因说明")
|
||
cropOssUrl: str | None = Field(None, description="裁剪图OSS地址")
|
||
displayText: str | None = Field(None, description="展示文案,例如第12页第1张图片模糊")
|
||
|
||
|
||
class ImageQualitySummaryVO(BaseModel):
|
||
"""文档图片质检摘要。"""
|
||
|
||
runId: int | None = Field(None, description="最新运行ID")
|
||
runStatus: str | None = Field(None, description="queued/running/completed/failed/skipped")
|
||
summaryStatus: str | None = Field(None, description="pass/review/reject")
|
||
skipReason: str | None = Field(None, description="跳过原因")
|
||
totalImages: int = Field(0, description="总图片数")
|
||
passCount: int = Field(0, description="通过数")
|
||
reviewCount: int = Field(0, description="待人工确认数")
|
||
rejectCount: int = Field(0, description="需重拍数")
|
||
warningText: str | None = Field(None, description="摘要提示文案")
|
||
finishedAt: str | None = Field(None, description="完成时间")
|
||
|
||
|
||
class ImageQualityDetailVO(BaseModel):
|
||
"""文档图片质检详情。"""
|
||
|
||
summary: ImageQualitySummaryVO = Field(..., description="质检摘要")
|
||
items: list[ImageQualityItemVO] = Field(default_factory=list, description="问题图片列表")
|
||
```
|
||
|
||
### 3.3 批量状态与重检相关 VO
|
||
|
||
```python
|
||
class ImageQualityStatusItemVO(BaseModel):
|
||
"""文档图片质检状态项。"""
|
||
|
||
documentId: int = Field(..., description="文档ID")
|
||
runId: int | None = Field(None, description="最新运行ID")
|
||
runStatus: str | None = Field(None, description="运行状态")
|
||
summaryStatus: str | None = Field(None, description="摘要状态")
|
||
rejectCount: int = Field(0, description="需重拍数")
|
||
reviewCount: int = Field(0, description="待确认数")
|
||
updatedAt: str | None = Field(None, description="更新时间")
|
||
|
||
|
||
class ImageQualityRecheckVO(BaseModel):
|
||
"""手工重检响应。"""
|
||
|
||
runId: int = Field(..., description="新质检运行ID")
|
||
documentId: int = Field(..., description="文档ID")
|
||
status: str = Field(..., description="queued")
|
||
message: str = Field("", description="提示信息")
|
||
```
|
||
|
||
## 4. 对现有 Document VO 的扩展建议
|
||
|
||
建议直接扩充 `documentVo.py`,这样上传页、列表页、详情页可以少开新接口或少拼数据。
|
||
|
||
### 4.1 `DocumentUploadVO` 增加字段
|
||
|
||
```python
|
||
imageQualityEnabled: bool = Field(False, description="当前文档是否启用图片质量校验")
|
||
imageQualityRunId: int | None = Field(None, description="图片质量校验运行ID")
|
||
imageQualityRunStatus: str | None = Field(None, description="图片质量校验运行状态")
|
||
imageQualitySummaryStatus: str | None = Field(None, description="图片质量校验摘要状态")
|
||
```
|
||
|
||
### 4.2 `DocumentStatusItemVO` 增加字段
|
||
|
||
```python
|
||
imageQualityRunId: int | None = Field(None, description="图片质量校验运行ID")
|
||
imageQualityRunStatus: str | None = Field(None, description="图片质量校验运行状态")
|
||
imageQualitySummaryStatus: str | None = Field(None, description="图片质量校验摘要状态")
|
||
imageQualityRejectCount: int = Field(0, description="需重拍图片数")
|
||
imageQualityReviewCount: int = Field(0, description="待人工确认图片数")
|
||
```
|
||
|
||
### 4.3 `DocumentListItemVO` 增加字段
|
||
|
||
```python
|
||
imageQualityRunId: int | None = Field(None, description="图片质量校验运行ID")
|
||
imageQualityRunStatus: str | None = Field(None, description="图片质量校验运行状态")
|
||
imageQualitySummaryStatus: str | None = Field(None, description="图片质量校验摘要状态")
|
||
imageQualityIssueCount: int = Field(0, description="问题图片数")
|
||
imageQualityWarningText: str | None = Field(None, description="图片质量提示文案")
|
||
```
|
||
|
||
### 4.4 `DocumentDetailVO` 增加字段
|
||
|
||
```python
|
||
imageQualitySummary: ImageQualitySummaryVO | None = Field(None, description="图片质量摘要")
|
||
```
|
||
|
||
## 5. 新增 Service 接口签名
|
||
|
||
建议新建 `services/imageQualityService.py`,不要污染 `IDocumentService` 主职责。
|
||
|
||
```python
|
||
from abc import ABC, abstractmethod
|
||
|
||
from fastapi_modules.fastapi_leaudit.domian.vo.imageQualityVo import (
|
||
ImageQualityConfigItemVO,
|
||
ImageQualityConfigUpsertDTO,
|
||
ImageQualityDetailVO,
|
||
ImageQualityRecheckVO,
|
||
ImageQualityStatusItemVO,
|
||
ImageQualitySummaryVO,
|
||
)
|
||
|
||
|
||
class IImageQualityService(ABC):
|
||
"""图片质量校验服务接口。"""
|
||
|
||
@abstractmethod
|
||
async def DispatchForDocument(
|
||
self,
|
||
DocumentId: int,
|
||
TriggerUserId: int | None = None,
|
||
Force: bool = False,
|
||
Speed: str = "normal",
|
||
) -> ImageQualityRecheckVO | None:
|
||
"""按文档触发图片质量校验任务。"""
|
||
...
|
||
|
||
@abstractmethod
|
||
async def GetDocumentSummary(
|
||
self,
|
||
CurrentUserId: int,
|
||
DocumentId: int,
|
||
) -> ImageQualitySummaryVO:
|
||
"""获取文档图片质检摘要。"""
|
||
...
|
||
|
||
@abstractmethod
|
||
async def GetDocumentDetail(
|
||
self,
|
||
CurrentUserId: int,
|
||
DocumentId: int,
|
||
) -> ImageQualityDetailVO:
|
||
"""获取文档图片质检详情。"""
|
||
...
|
||
|
||
@abstractmethod
|
||
async def GetDocumentsStatus(
|
||
self,
|
||
CurrentUserId: int,
|
||
Ids: list[int],
|
||
) -> list[ImageQualityStatusItemVO]:
|
||
"""批量获取文档图片质检状态。"""
|
||
...
|
||
|
||
@abstractmethod
|
||
async def RecheckDocument(
|
||
self,
|
||
CurrentUserId: int,
|
||
DocumentId: int,
|
||
Speed: str = "normal",
|
||
) -> ImageQualityRecheckVO:
|
||
"""手工重跑文档图片质量校验。"""
|
||
...
|
||
|
||
@abstractmethod
|
||
async def ListConfigs(self) -> list[ImageQualityConfigItemVO]:
|
||
"""获取图片质量校验配置列表。"""
|
||
...
|
||
|
||
@abstractmethod
|
||
async def UpsertConfig(self, Body: ImageQualityConfigUpsertDTO) -> ImageQualityConfigItemVO:
|
||
"""新增或更新图片质量校验配置。"""
|
||
...
|
||
|
||
@abstractmethod
|
||
async def DeleteConfig(self, Id: int) -> None:
|
||
"""删除图片质量校验配置。"""
|
||
...
|
||
```
|
||
|
||
## 6. 对现有 DocumentService 的接入点建议
|
||
|
||
建议只在现有 `IDocumentService / DocumentServiceImpl` 中补“触发点”,不要把查询逻辑也塞进去。
|
||
|
||
### 6.1 `DocumentServiceImpl.Upload`
|
||
|
||
在上传主文件和附件落库完成后,补一段:
|
||
|
||
```python
|
||
await self.ImageQualityService.DispatchForDocument(
|
||
DocumentId=document.Id,
|
||
TriggerUserId=CreatedBy,
|
||
Force=False,
|
||
Speed=Speed,
|
||
)
|
||
```
|
||
|
||
要求:
|
||
|
||
- try/except 包裹
|
||
- 失败只记 warning log
|
||
- 不影响现有上传成功返回
|
||
|
||
### 6.2 `DocumentServiceImpl.AppendAttachments`
|
||
|
||
在追加附件成功后,补一段:
|
||
|
||
```python
|
||
await self.ImageQualityService.DispatchForDocument(
|
||
DocumentId=Id,
|
||
TriggerUserId=CurrentUserId,
|
||
Force=True,
|
||
Speed="normal",
|
||
)
|
||
```
|
||
|
||
原因:
|
||
|
||
- 附件变化后,图片质检结果已过期
|
||
- 需要按最新附件重新跑
|
||
|
||
## 7. Controller 路由建议
|
||
|
||
建议新建 `controllers/imageQualityController.py`,风格保持与 `documentController.py / auditController.py` 一致。
|
||
|
||
### 7.1 文档级路由
|
||
|
||
```python
|
||
@self.router.get("/documents/{DocumentId}/image-quality/summary", response_model=Result[ImageQualitySummaryVO])
|
||
async def GetDocumentImageQualitySummary(
|
||
DocumentId: int,
|
||
payload: dict[str, Any] = Depends(verify_access_token),
|
||
):
|
||
"""获取单个文档图片质量校验摘要。"""
|
||
|
||
|
||
@self.router.get("/documents/{DocumentId}/image-quality", response_model=Result[ImageQualityDetailVO])
|
||
async def GetDocumentImageQualityDetail(
|
||
DocumentId: int,
|
||
payload: dict[str, Any] = Depends(verify_access_token),
|
||
):
|
||
"""获取单个文档图片质量校验详情。"""
|
||
|
||
|
||
@self.router.post("/documents/{DocumentId}/image-quality/recheck", response_model=Result[ImageQualityRecheckVO])
|
||
async def RecheckDocumentImageQuality(
|
||
DocumentId: int,
|
||
speed: str = Form("normal", description="执行速度档位:urgent/normal"),
|
||
payload: dict[str, Any] = Depends(verify_access_token),
|
||
):
|
||
"""手工重跑单个文档图片质量校验。"""
|
||
```
|
||
|
||
### 7.2 批量状态路由
|
||
|
||
```python
|
||
@self.router.get("/documents/image-quality/status", response_model=Result[list[ImageQualityStatusItemVO]])
|
||
async def GetDocumentsImageQualityStatus(
|
||
ids: str = Query(..., description="逗号分隔的文档ID列表"),
|
||
payload: dict[str, Any] = Depends(verify_access_token),
|
||
):
|
||
"""批量获取文档图片质量校验状态。"""
|
||
```
|
||
|
||
### 7.3 配置管理路由
|
||
|
||
如果第一期不做后台配置页,可以先只保留 service,不先开放 controller。
|
||
|
||
如果要开接口,建议:
|
||
|
||
```python
|
||
@self.router.get("/v3/image-quality/configs", response_model=Result[list[ImageQualityConfigItemVO]])
|
||
async def ListImageQualityConfigs():
|
||
"""获取图片质量校验配置。"""
|
||
|
||
|
||
@self.router.post("/v3/image-quality/configs", response_model=Result[ImageQualityConfigItemVO])
|
||
async def UpsertImageQualityConfig(Body: ImageQualityConfigUpsertDTO):
|
||
"""新增或更新图片质量校验配置。"""
|
||
|
||
|
||
@self.router.delete("/v3/image-quality/configs/{ConfigId}", response_model=Result[None])
|
||
async def DeleteImageQualityConfig(ConfigId: int):
|
||
"""删除图片质量校验配置。"""
|
||
```
|
||
|
||
## 8. Celery 任务与函数签名建议
|
||
|
||
新增文件:`fastapi_modules/fastapi_leaudit/image_quality/tasks.py`
|
||
|
||
```python
|
||
def resolve_image_quality_queue(speed: str = "normal") -> str:
|
||
"""根据优先级返回图片质量校验队列名。"""
|
||
|
||
|
||
def dispatch_image_quality_task(
|
||
run_id: int,
|
||
*,
|
||
speed: str = "normal",
|
||
trigger_user_id: int | None = None,
|
||
) -> Any:
|
||
"""投递图片质量校验任务。"""
|
||
|
||
|
||
@celery_app.task(
|
||
bind=True,
|
||
name="leaudit.image_quality.process_document",
|
||
acks_late=True,
|
||
)
|
||
def image_quality_process_document_task(
|
||
self,
|
||
run_id: int,
|
||
trigger_user_id: int | None = None,
|
||
) -> dict[str, Any]:
|
||
"""Celery worker 入口。"""
|
||
```
|
||
|
||
新增文件:`fastapi_modules/fastapi_leaudit/image_quality/runner.py`
|
||
|
||
```python
|
||
class ImageQualityRunner:
|
||
"""图片质量校验执行器。"""
|
||
|
||
async def Execute(
|
||
self,
|
||
RunId: int,
|
||
TriggerUserId: int | None = None,
|
||
) -> dict[str, Any]:
|
||
"""执行一次完整的图片质量校验。"""
|
||
```
|
||
|
||
## 9. SQL 草案文件命名建议
|
||
|
||
按你当前仓库习惯,建议直接放到:
|
||
|
||
- `scripts/创建sql/`
|
||
|
||
建议拆 3 个 SQL 草案文件。
|
||
|
||
### 9.1 表结构 SQL
|
||
|
||
文件名:
|
||
|
||
- `scripts/创建sql/schema_add_image_quality_module.sql`
|
||
|
||
内容范围:
|
||
|
||
- `leaudit_image_quality_configs`
|
||
- `leaudit_image_quality_runs`
|
||
- `leaudit_image_quality_items`
|
||
- 索引
|
||
- 唯一约束
|
||
- 软删除字段
|
||
|
||
### 9.2 权限点 SQL
|
||
|
||
文件名:
|
||
|
||
- `scripts/创建sql/seed_image_quality_permissions.sql`
|
||
|
||
内容范围:
|
||
|
||
- 图片质量校验查询权限
|
||
- 图片质量校验重检权限
|
||
- 图片质量配置管理权限
|
||
|
||
### 9.3 路由/入口 SQL
|
||
|
||
文件名:
|
||
|
||
- `scripts/创建sql/seed_image_quality_routes.sql`
|
||
|
||
内容范围:
|
||
|
||
- 新增接口路由资源
|
||
- 如需配置后台菜单,再补菜单项
|
||
|
||
## 10. 建议表结构草案
|
||
|
||
### 10.1 `leaudit_image_quality_configs`
|
||
|
||
建议字段:
|
||
|
||
- `id BIGSERIAL PRIMARY KEY`
|
||
- `scope_type VARCHAR(32) NOT NULL`
|
||
- `document_type_id BIGINT NULL`
|
||
- `region VARCHAR(64) NULL`
|
||
- `enabled BOOLEAN NOT NULL DEFAULT TRUE`
|
||
- `warn_threshold NUMERIC(10,4) NULL`
|
||
- `reject_threshold NUMERIC(10,4) NULL`
|
||
- `max_images_per_doc INTEGER NULL`
|
||
- `max_concurrency INTEGER NULL`
|
||
- `created_at TIMESTAMP NOT NULL DEFAULT NOW()`
|
||
- `updated_at TIMESTAMP NOT NULL DEFAULT NOW()`
|
||
- `deleted_at TIMESTAMP NULL`
|
||
|
||
建议索引:
|
||
|
||
- `idx_leaudit_image_quality_configs_scope`
|
||
- `idx_leaudit_image_quality_configs_doc_type`
|
||
- `idx_leaudit_image_quality_configs_region`
|
||
|
||
### 10.2 `leaudit_image_quality_runs`
|
||
|
||
建议字段:
|
||
|
||
- `id BIGSERIAL PRIMARY KEY`
|
||
- `document_id BIGINT NOT NULL`
|
||
- `document_file_id BIGINT NULL`
|
||
- `status VARCHAR(32) NOT NULL DEFAULT 'queued'`
|
||
- `skip_reason VARCHAR(64) NULL`
|
||
- `summary_status VARCHAR(32) NULL`
|
||
- `total_images INTEGER NOT NULL DEFAULT 0`
|
||
- `pass_count INTEGER NOT NULL DEFAULT 0`
|
||
- `review_count INTEGER NOT NULL DEFAULT 0`
|
||
- `reject_count INTEGER NOT NULL DEFAULT 0`
|
||
- `task_id VARCHAR(255) NULL`
|
||
- `error_message TEXT NULL`
|
||
- `started_at TIMESTAMP NULL`
|
||
- `finished_at TIMESTAMP NULL`
|
||
- `created_by BIGINT NULL`
|
||
- `created_at TIMESTAMP NOT NULL DEFAULT NOW()`
|
||
- `updated_at TIMESTAMP NOT NULL DEFAULT NOW()`
|
||
- `deleted_at TIMESTAMP NULL`
|
||
|
||
建议索引:
|
||
|
||
- `idx_leaudit_image_quality_runs_document_id`
|
||
- `idx_leaudit_image_quality_runs_status`
|
||
- `idx_leaudit_image_quality_runs_document_created_at`
|
||
|
||
### 10.3 `leaudit_image_quality_items`
|
||
|
||
建议字段:
|
||
|
||
- `id BIGSERIAL PRIMARY KEY`
|
||
- `run_id BIGINT NOT NULL`
|
||
- `document_id BIGINT NOT NULL`
|
||
- `document_file_id BIGINT NULL`
|
||
- `source_kind VARCHAR(64) NOT NULL`
|
||
- `source_file_name VARCHAR(255) NULL`
|
||
- `source_page_num INTEGER NULL`
|
||
- `image_index_in_page INTEGER NULL`
|
||
- `image_index_in_file INTEGER NULL`
|
||
- `bbox_json JSONB NULL`
|
||
- `image_key VARCHAR(255) NULL`
|
||
- `parent_image_key VARCHAR(255) NULL`
|
||
- `crop_oss_url TEXT NULL`
|
||
- `quality_status VARCHAR(32) NOT NULL`
|
||
- `quality_score NUMERIC(10,4) NULL`
|
||
- `reason_code VARCHAR(64) NULL`
|
||
- `reason_text TEXT NULL`
|
||
- `extra_json JSONB NULL`
|
||
- `created_at TIMESTAMP NOT NULL DEFAULT NOW()`
|
||
- `updated_at TIMESTAMP NOT NULL DEFAULT NOW()`
|
||
- `deleted_at TIMESTAMP NULL`
|
||
|
||
建议索引:
|
||
|
||
- `idx_leaudit_image_quality_items_run_id`
|
||
- `idx_leaudit_image_quality_items_document_id`
|
||
- `idx_leaudit_image_quality_items_quality_status`
|
||
- `idx_leaudit_image_quality_items_source_page_num`
|
||
|
||
## 11. 配置项建议
|
||
|
||
在 `fastapi_admin/config/_settings.py` 的 `LeauditSettings` 增加:
|
||
|
||
```python
|
||
LEAUDIT_IMAGE_QUALITY_ENABLED: bool = False
|
||
LEAUDIT_IMAGE_QUALITY_WARN_THRESHOLD: float = 0.45
|
||
LEAUDIT_IMAGE_QUALITY_REJECT_THRESHOLD: float = 0.30
|
||
LEAUDIT_IMAGE_QUALITY_MAX_IMAGES_PER_DOC: int = 80
|
||
LEAUDIT_IMAGE_QUALITY_MAX_CONCURRENCY: int = 4
|
||
LEAUDIT_IMAGE_QUALITY_QUEUE_NORMAL: str = "leaudit.image_quality.normal"
|
||
LEAUDIT_IMAGE_QUALITY_QUEUE_URGENT: str = "leaudit.image_quality.urgent"
|
||
LEAUDIT_IMAGE_QUALITY_TIMEOUT: int = 120
|
||
```
|
||
|
||
并同步补到 `fastapi_admin/config/__init__.pyi`。
|
||
|
||
## 12. 前端接口命名建议
|
||
|
||
建议前端 API 文件新增:
|
||
|
||
- `legal-platform-frontend/lib/api/legacy/files/image-quality.ts`
|
||
|
||
建议函数:
|
||
|
||
```ts
|
||
export async function getDocumentImageQualitySummary(documentId: number)
|
||
export async function getDocumentImageQualityDetail(documentId: number)
|
||
export async function getDocumentsImageQualityStatus(ids: number[])
|
||
export async function recheckDocumentImageQuality(documentId: number, speed?: string)
|
||
```
|
||
|
||
## 13. 第一版实施顺序
|
||
|
||
建议按下面顺序做:
|
||
|
||
1. 先补 SQL 与后端表模型
|
||
2. 再补 `imageQualityVo.py`
|
||
3. 再补 `IImageQualityService / ImageQualityServiceImpl`
|
||
4. 再补 `tasks.py / runner.py`
|
||
5. 再在 `DocumentServiceImpl.Upload / AppendAttachments` 挂触发
|
||
6. 再补 `imageQualityController.py`
|
||
7. 最后接前端上传页、列表页、详情页
|
||
|
||
## 14. 结论
|
||
|
||
按当前仓库风格,这个模块最稳妥的落地方式是:
|
||
|
||
- 新开独立 `VO + Service + Controller + Celery task + SQL`
|
||
- 只在 `DocumentServiceImpl` 做触发,不侵入现有 `AuditServiceImpl`
|
||
- 对外暴露文档级摘要、明细、批量状态、手工重检四类接口
|
||
- SQL 先拆成结构、权限、路由三个脚本,方便分阶段上线
|
||
|
||
这个拆法能保证图片质量校验模块后续可单独演进,不会把现有 OCR / 评查主链路拖乱。
|
||
|
||
## 15. 页码定位策略修正版
|
||
|
||
这一节用于修正第一版里对 `doc/docx/wps` 页码定位的表述,避免研发误以为可以直接靠当前 `python-docx` 兜底逻辑拿到真实页码。
|
||
|
||
### 15.1 现有 OCR / 评查链路里的页码定位是怎么来的
|
||
|
||
当前系统里,评查详情页能展示字段页码、部分情况下还能做定位高亮,主来源并不是“原始 Word 文档页码”,而是下面两层能力:
|
||
|
||
- 第一层:OCR/归一化结果自带页结构
|
||
- `ocr_result.pages[].page_num`
|
||
- OCR chunk 的 `bbox`
|
||
- `field_positions` 里的 `pageNum / bbox / matchPosition`
|
||
- 第二层:详情页兜底文本匹配
|
||
- 对 PDF 重新逐页抽文本,再把字段文本匹配回页码
|
||
- 这是兜底逻辑,不是主定位来源
|
||
|
||
也就是说,当前评查模块“真正稳定可复用”的页码语义,主要来自 OCR / normalized document,而不是详情页自己的回推逻辑。
|
||
|
||
### 15.2 当前项目里 `docx` 的兜底页码能力并不是真分页
|
||
|
||
当前 `documentServiceImpl.py` 里的 `_extract_page_texts_from_docx()` 实现,实际是:
|
||
|
||
- 读取段落和表格文本
|
||
- 拼成整篇文档文本
|
||
- 最终只返回 `[(1, text)]`
|
||
|
||
这意味着:
|
||
|
||
- 当前详情页兜底逻辑对 `docx` 并没有真实分页能力
|
||
- 它只是把整篇文档退化成“第 1 页”
|
||
- 因此不能把这条逻辑当成 `docx` 图片定位的基础
|
||
|
||
结论要固定为:
|
||
|
||
- 当前系统里 `docx` 不是完全不能定位
|
||
- 但不能依赖现有 `python-docx` 兜底逻辑做精确页码
|
||
- 如果要把 `docx` 图片定位做准,必须优先复用 OCR / 归一化后的页结构
|
||
|
||
### 15.3 图片质量模块对 `docx` 的正确做法
|
||
|
||
图片质量模块在处理 `doc/docx/wps` 时,页码定位策略建议按优先级分层:
|
||
|
||
1. 优先复用 OCR 结果中的页结构和视觉对象定位
|
||
2. 其次复用 `visual_manifest` 中已有的视觉对象元数据
|
||
3. 如果 OCR 已能给出该图片或父图像所在页,则直接采用该页码
|
||
4. 如果拿不到稳定页码,则降级为“正文第 N 张图”或“附件第 N 张图”
|
||
|
||
推荐优先使用的字段包括:
|
||
|
||
- `ocr_result.pages[].page_num`
|
||
- `ocr_result.visual_manifest`
|
||
- `visual_manifest` 中对象的 `page_num`
|
||
- `visual_manifest` 中对象的 `bbox`
|
||
- `image_key / parent_image_key`
|
||
|
||
这样做的本质是:
|
||
|
||
- 不自己重新发明 Word 分页
|
||
- 而是复用现有 OCR pipeline 已经归一化出来的页概念
|
||
|
||
### 15.4 为什么图片质量模块不能直接复用详情页 `docx` 兜底逻辑
|
||
|
||
因为那条逻辑只适合“文本字段没有页码时,尽量补一个最小可用页码”,不适合图片质量场景。
|
||
|
||
图片质量场景要解决的是:
|
||
|
||
- 哪一张图片模糊
|
||
- 它在第几页
|
||
- 它在页内第几张
|
||
- 最好还能给出 bbox 或裁剪图
|
||
|
||
而当前 `docx` 兜底逻辑没有:
|
||
|
||
- 真分页
|
||
- 图片级索引
|
||
- 页内图片序号
|
||
- 图片 bbox
|
||
|
||
所以如果直接复用它,只会得到一个看似有页码、其实不可用的假定位。
|
||
|
||
### 15.5 图片质量模块的页码定位分级策略
|
||
|
||
建议在方案里明确写死:
|
||
|
||
- `PDF / 扫描PDF / 图片文件 / 图片附件 / PDF附件`
|
||
- 第一优先级支持精确定位
|
||
- 目标输出:`source_page_num + image_index_in_page + bbox`
|
||
- `doc/docx/wps`
|
||
- 第一优先级复用 OCR / visual_manifest 的页码与 bbox
|
||
- 第二优先级降级为 `image_index_in_file`
|
||
- 第一版不承诺所有 Office 文档都能稳定精确到预览页码
|
||
|
||
也就是说,`docx` 可以做定位,但实现前提不是当前 `python-docx` 的退化逻辑,而是 OCR 归一化之后的页结构。
|
||
|
||
### 15.6 对图片质量模块的实现修正
|
||
|
||
基于上面的分析,图片质量模块实现时建议补一个约束:
|
||
|
||
- 对 `doc/docx/wps`,图片抽取和质量判断可以独立做
|
||
- 但页码回指优先放在“抽图后结合 OCR visual metadata 回填”这一层完成
|
||
- 不允许直接调用当前 `_extract_page_texts_from_docx()` 来生成图片页码
|
||
|
||
更具体地说:
|
||
|
||
- `extractors.py` 负责抽图和生成图片级唯一索引
|
||
- `detector.py` 负责清晰度三档判断
|
||
- `runner.py` 在汇总结果前,优先尝试将图片索引和 OCR/visual_manifest 做关联
|
||
- 关联成功则写入 `source_page_num / bbox`
|
||
- 关联失败则写入 `image_index_in_file`,前端展示“正文第 N 张图”
|
||
|
||
### 15.7 最终结论
|
||
|
||
本项目当前“评查页码定位”对 `docx` 的可用性,主要来自 OCR 归一化页结构,而不是 `python-docx` 的原生分页能力。
|
||
|
||
因此文档图片质量校验模块如果要把 `docx` 也做准,正确方向是:
|
||
|
||
- 参考现有 OCR / 评查流程的页结构设计
|
||
- 复用 `ocr_result.pages / visual_manifest / bbox / image_key`
|
||
- 避免把当前 `docx` 文本兜底逻辑误当成图片级定位能力
|
||
|
||
这一点在后续实施时应作为明确技术约束,不建议再走“直接解析 Word 文本推页码”的路线。
|
||
|
||
## 16. 图片索引表与 OCR visual_manifest 关联策略
|
||
|
||
这一节用于把图片质量模块里最关键的“抽图记录如何回绑到 OCR 页码与 bbox”讲清楚,尤其是 `doc/docx/wps` 场景。
|
||
|
||
### 16.1 为什么必须做关联层
|
||
|
||
图片质量模块自己的 `extractors.py` 会先把图片从原始文档里抽出来,并落到 `leaudit_image_quality_items`。
|
||
|
||
但抽图本身只能保证下面这些信息:
|
||
|
||
- 这张图片来自哪个文档文件
|
||
- 它是主文件还是附件
|
||
- 它是文件内第几张图
|
||
- 它在抽图阶段的原始宽高、字节、hash
|
||
|
||
仅靠这些信息,还不够支持前端稳定回指到:
|
||
|
||
- 第几页
|
||
- 页内第几张
|
||
- 具体 bbox
|
||
|
||
所以必须增加一层“图片索引表与 OCR visual metadata 的关联层”,把抽图记录和 OCR/visual_manifest 里的页码、坐标、父图像标识打通。
|
||
|
||
### 16.2 建议增加的落库字段
|
||
|
||
在 `leaudit_image_quality_items` 里,建议补充或明确以下字段:
|
||
|
||
- `image_sha256`
|
||
- 抽图后二进制 hash
|
||
- `image_width`
|
||
- 抽图宽度
|
||
- `image_height`
|
||
- 抽图高度
|
||
- `source_ext`
|
||
- 来源文件扩展名
|
||
- `ocr_page_num`
|
||
- 关联到 OCR 后得到的页码
|
||
- `ocr_bbox_json`
|
||
- 关联到 OCR 后得到的 bbox
|
||
- `ocr_image_key`
|
||
- 关联到 OCR visual object 的 image_key
|
||
- `ocr_parent_image_key`
|
||
- 关联到 OCR visual object 的 parent_image_key
|
||
- `ocr_match_mode`
|
||
- 关联命中方式
|
||
- `ocr_match_score`
|
||
- 关联命中置信度
|
||
|
||
说明:
|
||
|
||
- `source_page_num` 表示模块自己理解的来源页码
|
||
- `ocr_page_num` 表示从 OCR / visual_manifest 里回填的页码
|
||
- 第一版前端展示时,优先取 `ocr_page_num`,取不到再回退到 `source_page_num`
|
||
|
||
### 16.3 关联优先级建议
|
||
|
||
建议在 `runner.py` 里做一段统一的关联流程,优先级从高到低如下:
|
||
|
||
1. `image_key / parent_image_key` 直接命中
|
||
2. 图片二进制 hash 命中
|
||
3. 图片尺寸 + 裁剪相似度命中
|
||
4. 页级候选范围内按 bbox / area 比例匹配
|
||
5. 全部失败则降级为文件内序号定位
|
||
|
||
这套顺序的原因是:
|
||
|
||
- `image_key / parent_image_key` 一旦能命中,最稳定
|
||
- hash 命中次稳,但前提是 OCR side 和抽图 side 用的是同一图像字节
|
||
- 尺寸和相似度可以做弱匹配
|
||
- bbox/area 更适合已经知道页候选时做局部定位
|
||
|
||
### 16.4 各类文件的推荐关联策略
|
||
|
||
#### 16.4.1 PDF / 扫描PDF / PDF附件
|
||
|
||
推荐策略:
|
||
|
||
- 抽图阶段直接记录 `source_page_num`
|
||
- OCR 若返回同页 visual object,则用页内 bbox 做二次确认
|
||
- 最终定位以 `source_page_num + image_index_in_page + bbox` 为准
|
||
|
||
这一类文件通常不需要过度依赖 `visual_manifest`,因为自身抽图时就有稳定页概念。
|
||
|
||
#### 16.4.2 图片文件 / 图片附件
|
||
|
||
推荐策略:
|
||
|
||
- `source_page_num` 固定记为 `1`
|
||
- `image_index_in_page = 1`
|
||
- 若 OCR 输出 visual object,则仅作为 bbox 或 `image_key` 补充
|
||
|
||
这类文件定位最简单,本身就是单页单图。
|
||
|
||
#### 16.4.3 `doc/docx/wps`
|
||
|
||
推荐策略:
|
||
|
||
- 抽图阶段只保证:
|
||
- `image_index_in_file`
|
||
- `image_sha256`
|
||
- `image_width / image_height`
|
||
- `source_file_name`
|
||
- 页码不要在抽图阶段硬算
|
||
- 等 OCR/visual_manifest 结果出来后,再做回填
|
||
|
||
这一类文件的关联优先级建议是:
|
||
|
||
1. `parent_image_key` 命中
|
||
2. `image_key` 命中
|
||
3. hash 命中
|
||
4. 尺寸与内容相似度命中
|
||
5. 命不中则只展示“正文第 N 张图”
|
||
|
||
### 16.5 建议的 `ocr_match_mode` 枚举
|
||
|
||
建议在 `ocr_match_mode` 里固定以下值,方便后续排查:
|
||
|
||
- `parent_image_key_exact`
|
||
- `image_key_exact`
|
||
- `image_sha256_exact`
|
||
- `size_similarity_match`
|
||
- `bbox_overlap_match`
|
||
- `page_candidate_match`
|
||
- `file_index_fallback`
|
||
- `no_match`
|
||
|
||
这样后面如果业务反馈“页码不准”,可以直接按命中模式排查是哪一层出了问题。
|
||
|
||
### 16.6 `visual_manifest` 关联的实现建议
|
||
|
||
建议把这块逻辑放在 `image_quality/runner.py` 或单独的 `image_quality/locator.py` 中,职责清晰一点:
|
||
|
||
- `extractors.py`
|
||
- 只负责抽图
|
||
- 不负责猜页码
|
||
- `detector.py`
|
||
- 只负责判断图片清晰度
|
||
- `locator.py` 或 `runner.py`
|
||
- 负责把图片索引记录与 OCR `visual_manifest` 做关联
|
||
- 回填 `ocr_page_num / ocr_bbox_json / ocr_match_mode`
|
||
|
||
也就是说,页码定位不是抽图逻辑的一部分,而是“抽图后 + OCR结果可用后”的补全逻辑。
|
||
|
||
### 16.7 为什么建议先落图片索引,再做关联
|
||
|
||
因为如果把“抽图、关联、检测”混在一起,会有两个问题:
|
||
|
||
- 当 OCR 结果未就绪时,图片质量模块会被迫等待主流程
|
||
- 当 OCR side 结构调整时,图片质量模块会被连带影响
|
||
|
||
正确拆法是:
|
||
|
||
1. 先独立抽图并持久化图片索引
|
||
2. 独立跑清晰度检测
|
||
3. 如果 OCR 结果已就绪,则补做 visual_manifest 关联
|
||
4. 如果 OCR 结果尚未就绪,则先以弱定位展示,后续可异步补齐
|
||
|
||
这样既保持了两条 pipeline 独立,又能最大化复用 OCR 页结构。
|
||
|
||
### 16.8 页面展示的取值优先级
|
||
|
||
前端展示“第几页/第几张图”时,建议按下面顺序取值:
|
||
|
||
1. `ocr_page_num + image_index_in_page`
|
||
2. `source_page_num + image_index_in_page`
|
||
3. `image_index_in_file`
|
||
4. 仅展示来源文件名
|
||
|
||
示例文案:
|
||
|
||
- `主文件第 12 页第 1 张图片模糊`
|
||
- `附件《现场照片2.pdf》第 3 页第 2 张图片疑似模糊`
|
||
- `正文第 5 张内嵌图片模糊`
|
||
- `附件《取证照片.jpg》图片模糊`
|
||
|
||
### 16.9 对第一版实施的建议收口
|
||
|
||
为了控制复杂度,第一版建议这样收:
|
||
|
||
- `PDF / 扫描PDF / 图片文件 / 图片附件 / PDF附件`
|
||
- 做强定位
|
||
- `doc/docx/wps`
|
||
- 先把 OCR `visual_manifest` 关联能力做出来
|
||
- 关联成功就展示页码
|
||
- 关联失败就降级到“正文第 N 张图”
|
||
|
||
不要第一版就要求所有 Word/WPS 内嵌图都 100% 精确到预览页码,否则开发成本和误判风险都会明显上升。
|
||
|
||
### 16.10 最终结论
|
||
|
||
图片质量模块能不能把 `docx` 场景做准,关键不在“能不能把图抽出来”,而在“抽出来以后,能不能与 OCR visual metadata 建立稳定关联”。
|
||
|
||
所以第一版技术路线应明确为:
|
||
|
||
- 先做图片索引表
|
||
- 再做 visual_manifest 关联层
|
||
- 最后才是页码/坐标展示
|
||
|
||
这比直接依赖 `python-docx` 或手工猜 Word 分页要稳得多,也更贴合当前项目已有的 OCR / 评查体系。
|
||
|
||
## 17. `extractors.py` 职责拆分与 `runner.py` 执行时序
|
||
|
||
这一节用于把模块内部分层再落细一点,方便研发直接按文件拆任务。
|
||
|
||
### 17.1 模块职责分层建议
|
||
|
||
建议图片质量模块按下面几层拆,不要把所有逻辑堆在一个 service 或一个 task 文件里。
|
||
|
||
#### 17.1.1 `input_resolver.py`
|
||
|
||
职责:
|
||
|
||
- 读取文档主文件与附件
|
||
- 统一返回原始文件输入列表
|
||
- 不做抽图
|
||
- 不做检测
|
||
- 不做 OCR 关联
|
||
|
||
建议输出结构:
|
||
|
||
- `document_id`
|
||
- `document_file_id`
|
||
- `file_role`
|
||
- `file_name`
|
||
- `file_ext`
|
||
- `mime_type`
|
||
- `source_type`
|
||
- `source_path`
|
||
- `file_bytes`
|
||
|
||
#### 17.1.2 `extractors.py`
|
||
|
||
职责:
|
||
|
||
- 针对不同文件类型抽图
|
||
- 生成图片级索引记录
|
||
- 记录图片最基础元数据
|
||
- 不负责模糊判断
|
||
- 不负责最终页码回指策略
|
||
|
||
建议内部再拆为:
|
||
|
||
- `extract_images_from_pdf()`
|
||
- `extract_images_from_image_file()`
|
||
- `extract_images_from_docx()`
|
||
- `extract_images_from_attachment()`
|
||
- `persist_image_index_items()`
|
||
|
||
建议统一输出的数据结构:
|
||
|
||
- `documentId`
|
||
- `documentFileId`
|
||
- `sourceKind`
|
||
- `sourceFileName`
|
||
- `sourcePageNum`
|
||
- `imageIndexInPage`
|
||
- `imageIndexInFile`
|
||
- `imageSha256`
|
||
- `imageWidth`
|
||
- `imageHeight`
|
||
- `imageBytes`
|
||
- `extraJson`
|
||
|
||
其中:
|
||
|
||
- `PDF / 扫描PDF / 图片文件 / 图片附件` 可以在这里直接给出 `sourcePageNum`
|
||
- `doc/docx/wps` 这里不强行给精确页码,允许先留空
|
||
|
||
#### 17.1.3 `detector.py`
|
||
|
||
职责:
|
||
|
||
- 对抽出的图片执行清晰度检测
|
||
- 返回三档结果
|
||
- 输出原因码和说明
|
||
- 不负责页码定位
|
||
|
||
建议统一返回:
|
||
|
||
- `qualityStatus`
|
||
- `qualityScore`
|
||
- `reasonCode`
|
||
- `reasonText`
|
||
|
||
建议原因码先收敛为有限集合:
|
||
|
||
- `blur_detected`
|
||
- `low_resolution`
|
||
- `over_exposure`
|
||
- `under_exposure`
|
||
- `motion_blur`
|
||
- `text_unreadable`
|
||
- `detector_timeout`
|
||
- `detector_failed`
|
||
|
||
#### 17.1.4 `locator.py`
|
||
|
||
职责:
|
||
|
||
- 读取 OCR / visual_manifest
|
||
- 把图片索引记录与 visual object 做关联
|
||
- 回填 `ocr_page_num / ocr_bbox_json / ocr_match_mode`
|
||
- 不负责清晰度打分
|
||
|
||
如果第一期不想单独开 `locator.py`,也可以先内聚到 `runner.py`,但职责概念上仍然建议独立。
|
||
|
||
#### 17.1.5 `storage_adapter.py`
|
||
|
||
职责:
|
||
|
||
- 创建 run
|
||
- 批量写入 image items
|
||
- 更新图片检测结果
|
||
- 更新关联结果
|
||
- 汇总 run 主状态
|
||
|
||
不要把业务判断写进这里,这里只做存取。
|
||
|
||
### 17.2 `extractors.py` 建议的实现规则
|
||
|
||
#### 17.2.1 PDF 抽图规则
|
||
|
||
建议:
|
||
|
||
- 按页遍历
|
||
- 同页内按出现顺序编号 `image_index_in_page`
|
||
- 全文件维度编号 `image_index_in_file`
|
||
- 能拿到 bbox 就记录 bbox
|
||
- 原始图片字节计算 `sha256`
|
||
|
||
最终目标:
|
||
|
||
- PDF 在抽图阶段就拿到强定位能力
|
||
|
||
#### 17.2.2 图片文件抽图规则
|
||
|
||
建议:
|
||
|
||
- 单文件直接视为 1 页 1 图
|
||
- `sourcePageNum = 1`
|
||
- `imageIndexInPage = 1`
|
||
- `imageIndexInFile = 1`
|
||
|
||
#### 17.2.3 `doc/docx/wps` 抽图规则
|
||
|
||
建议:
|
||
|
||
- 只抽出内嵌图片
|
||
- 保证文件内顺序稳定
|
||
- 第一版不在这里做页码推断
|
||
- 重点保留:
|
||
- `image_sha256`
|
||
- `image_width`
|
||
- `image_height`
|
||
- `image_index_in_file`
|
||
|
||
原因:
|
||
|
||
- 这类文件的难点不在抽图,而在“抽图后如何挂回 OCR 页结构”
|
||
|
||
#### 17.2.4 附件抽图规则
|
||
|
||
建议:
|
||
|
||
- 每个附件独立视为一个来源文件
|
||
- 不要沿用主文件页码空间
|
||
- `source_file_name` 必填
|
||
- 前端展示时允许出现:
|
||
- `附件《xxx.pdf》第 2 页第 1 张图片模糊`
|
||
- `附件《xxx.jpg》图片模糊`
|
||
|
||
### 17.3 `runner.py` 建议执行时序
|
||
|
||
建议整条图片质量任务按下面时序执行:
|
||
|
||
1. 创建或确认 `image_quality_run`
|
||
2. 更新 run 状态为 `running`
|
||
3. 调用 `input_resolver.py` 读取主文件与附件
|
||
4. 调用 `extractors.py` 抽图
|
||
5. 将抽图结果落表 `leaudit_image_quality_items`
|
||
6. 判断是否无图
|
||
7. 若无图则 run 标记 `skipped/no_images`
|
||
8. 若有图则进入并发检测
|
||
9. 调用 `detector.py` 批量检测,回写每条 item 的质量结果
|
||
10. 尝试读取 OCR / visual_manifest
|
||
11. 若可读,则做 `locator` 关联并回填页码/bbox
|
||
12. 汇总 `pass/review/reject` 计数
|
||
13. 更新 run 的 `summary_status`
|
||
14. 标记 run `completed` 或 `partial_failed`
|
||
|
||
### 17.4 推荐时序图口径
|
||
|
||
可以把执行理解成下面这条线:
|
||
|
||
1. 上传成功
|
||
2. 文档主流程正常投递 OCR/评查
|
||
3. 图片质量任务独立投递
|
||
4. 图片质量任务先抽图、先检测
|
||
5. OCR 若先完成,则图片质量任务顺手做 visual_manifest 关联
|
||
6. OCR 若未完成,则图片质量任务先给弱定位结果
|
||
7. 后续可通过补偿任务把弱定位升级成强定位
|
||
|
||
这意味着:
|
||
|
||
- 图片质量任务不依赖 OCR 主任务完成后才能启动
|
||
- 但它可以在 OCR 结果可用时变得更准
|
||
|
||
### 17.5 是否需要补偿任务
|
||
|
||
建议保留“可选补偿任务”设计,但第一版不强求一定实现。
|
||
|
||
补偿任务适合处理:
|
||
|
||
- 图片质检先完成,但 OCR 尚未完成
|
||
- 首次只拿到了 `image_index_in_file`
|
||
- OCR 完成后再补齐 `ocr_page_num / bbox`
|
||
|
||
如果要做,建议命名:
|
||
|
||
- `leaudit.image_quality.relocate_by_ocr`
|
||
|
||
但第一版也可以先不单独开 task,而是在详情查询时做一次懒补偿,或者在 OCR 主流程结束后顺手触发一次回填。
|
||
|
||
### 17.6 `runner.py` 的失败处理原则
|
||
|
||
建议明确:
|
||
|
||
- 抽图失败:
|
||
- 当前 run 标记 `failed`
|
||
- 不影响文档主流程
|
||
- 单张图片检测失败:
|
||
- 当前 item 标记失败原因
|
||
- run 最终允许为 `partial_failed`
|
||
- OCR 关联失败:
|
||
- 不算质检失败
|
||
- 只算定位降级
|
||
- 全部图片检测完成但都无法定位页码:
|
||
- run 仍可 `completed`
|
||
- 只是展示层回退到“第 N 张图”
|
||
|
||
也就是说,质量检测失败和定位失败要分开统计,不能混成一类。
|
||
|
||
### 17.7 推荐的汇总规则
|
||
|
||
建议 `runner.py` 最终汇总时按下面规则判断:
|
||
|
||
- 只要存在至少 1 张 `reject`,`summary_status = reject`
|
||
- 否则只要存在至少 1 张 `review`,`summary_status = review`
|
||
- 否则若全部 `pass`,`summary_status = pass`
|
||
- 若任务本身没跑起来,则 `status = failed`
|
||
- 若任务跑完但部分图片检测失败,则 `status = partial_failed`
|
||
- 若命中无图、纯文本、开关关闭,则 `status = skipped`
|
||
|
||
### 17.8 第一版最小可交付实现
|
||
|
||
为了避免第一版过重,建议把可交付标准压到下面这一级:
|
||
|
||
- `extractors.py`
|
||
- 先支持 PDF、图片、docx 内嵌图、附件
|
||
- `detector.py`
|
||
- 先给出稳定三档判断
|
||
- `runner.py`
|
||
- 先能完成抽图、检测、汇总
|
||
- `locator`
|
||
- 先支持 OCR `visual_manifest` 存在时的强关联
|
||
- 不存在时允许降级
|
||
|
||
只要能做到:
|
||
|
||
- 不影响主流程
|
||
- 能把问题图片列出来
|
||
- 大多数 PDF/图片类文档能精确到页
|
||
- `docx` 至少能稳定定位到“正文第 N 张图”
|
||
|
||
第一版就已经是可上线状态。
|
||
|
||
### 17.9 最终建议
|
||
|
||
研发拆任务时,建议直接按下面方式分:
|
||
|
||
- A 同学:`SQL + VO + Controller + Service`
|
||
- B 同学:`input_resolver + extractors`
|
||
- C 同学:`detector + runner`
|
||
- D 同学:`locator / OCR visual_manifest 关联`
|
||
- 前端同学:`上传页 + 列表页 + 详情页展示`
|
||
|
||
这样拆分写入面冲突最小,也最贴合当前仓库结构。
|