Files
invoice-master-poc-v2/README.md
2026-02-03 22:03:44 +01:00

650 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Invoice Master POC v2
自动发票字段提取系统 - 使用 YOLOv11 + PaddleOCR 从瑞典 PDF 发票中提取结构化数据。
## 项目概述
本项目实现了一个完整的发票字段自动提取流程:
1. **自动标注**: 利用已有 CSV 结构化数据 + OCR 自动生成 YOLO 训练标注
2. **模型训练**: 使用 YOLOv11 训练字段检测模型,支持数据增强
3. **推理提取**: 检测字段区域 -> OCR 提取文本 -> 字段规范化
4. **Web 管理**: React 前端 + FastAPI 后端,支持文档管理、数据集构建、模型训练和版本管理
### 架构
项目采用 **monorepo + 三包分离** 架构,训练和推理可独立部署:
```
packages/
├── shared/ # 共享库 (PDF, OCR, 规范化, 匹配, 存储, 训练)
├── training/ # 训练服务 (GPU, 按需启动)
└── backend/ # 后端服务 (Web API + 推理, 常驻运行)
frontend/ # React 前端 (Vite + TypeScript + TailwindCSS)
```
| 服务 | 部署目标 | GPU | 生命周期 |
|------|---------|-----|---------|
| **Frontend** | Vercel / Nginx | 否 | 常驻 |
| **Backend** | Azure App Service / AWS | 可选 | 常驻 7x24 |
| **Training** | Azure ACI / AWS ECS | 必需 | 按需启动/销毁 |
两个服务通过共享 PostgreSQL 数据库通信。推理服务通过 API 触发训练任务,训练服务从数据库拾取任务执行。
### 当前进度
| 指标 | 数值 |
|------|------|
| **已标注文档** | 9,738 (9,709 成功) |
| **总体字段匹配率** | 94.8% (82,604/87,121) |
| **测试** | 2,058 passed |
| **测试覆盖率** | 60% |
| **模型 mAP@0.5** | 93.5% |
**各字段匹配率:**
| 字段 | 匹配率 | 说明 |
|------|--------|------|
| supplier_accounts(Bankgiro) | 100.0% | 供应商 Bankgiro |
| supplier_accounts(Plusgiro) | 100.0% | 供应商 Plusgiro |
| Plusgiro | 99.4% | 支付 Plusgiro |
| OCR | 99.1% | OCR 参考号 |
| Bankgiro | 99.0% | 支付 Bankgiro |
| InvoiceNumber | 98.9% | 发票号码 |
| InvoiceDueDate | 95.9% | 到期日期 |
| InvoiceDate | 95.5% | 发票日期 |
| Amount | 91.3% | 金额 |
| supplier_organisation_number | 78.2% | 供应商组织号 (CSV 数据质量问题) |
## 运行环境
本项目**必须**在 **WSL + Conda** 环境中运行。
### 系统要求
| 环境 | 要求 |
|------|------|
| **WSL** | WSL 2 + Ubuntu 22.04 (或 24.04 for RTX 50 系列) |
| **Conda** | Miniconda 或 Anaconda |
| **Python** | 3.11+ (通过 Conda 管理), 3.10 for RTX 50 系列 |
| **GPU** | NVIDIA GPU + CUDA 12.x (RTX 50 系列见 SM120 章节) |
| **数据库** | PostgreSQL (存储标注结果) |
## 安装
```bash
# 1. 进入 WSL
wsl -d Ubuntu-22.04
# 2. 创建 Conda 环境
conda create -n invoice-sm120 python=3.11 -y
conda activate invoice-sm120
# 3. 进入项目目录
cd /mnt/c/Users/yaoji/git/ColaCoder/invoice-master-poc-v2
# 4. 安装三个包 (editable mode)
pip install -e packages/shared
pip install -e packages/training
pip install -e packages/backend
```
## RTX 5080 (Blackwell SM 120) GPU 设置
RTX 50 系列 (Blackwell 架构) 使用 SM 120 计算能力,官方 PaddlePaddle 仅支持到 SM 90。需要使用社区编译的 SM120 wheel。
### 系统要求
| 要求 | 版本 |
|------|------|
| **WSL** | Ubuntu 24.04 (glibc 2.39+) |
| **Python** | 3.10 (wheel 限制) |
| **CUDA** | 13.0+ (通过 pip nvidia 包) |
### 升级 WSL 到 Ubuntu 24.04
```bash
# 检查当前版本
lsb_release -a
# 如果是 22.04,需要升级
sudo sed -i 's/Prompt=lts/Prompt=normal/g' /etc/update-manager/release-upgrades
sudo apt update && sudo apt upgrade -y
sudo do-release-upgrade
```
### 创建 SM120 环境
```bash
# 1. 创建 Python 3.10 环境
conda create -n invoice-sm120 python=3.10 -y
conda activate invoice-sm120
# 2. 安装 SM120 PaddlePaddle wheel
pip install https://github.com/horhe-dvlp/paddlepaddle-sm120-wheels/releases/download/v3.0.0/paddlepaddle_gpu-3.0.0-cp310-cp310-linux_x86_64.whl
# 3. 安装项目依赖
cd /mnt/c/Users/yaoji/git/ColaCoder/invoice-master-poc-v2
pip install -e packages/shared
pip install -e packages/training
pip install -e packages/backend
```
### 配置环境变量
`~/.bashrc` 中添加:
```bash
# PaddlePaddle SM120 (RTX 50 series) environment
export PADDLE_SM120_LIBS=/home/kai/.local/lib/python3.10/site-packages/nvidia
alias activate-sm120='export LD_LIBRARY_PATH=$PADDLE_SM120_LIBS/cublas/lib:$PADDLE_SM120_LIBS/cudnn/lib:$PADDLE_SM120_LIBS/cuda_runtime/lib:/usr/lib/wsl/lib:$LD_LIBRARY_PATH && export PADDLE_PDX_DISABLE_MODEL_SOURCE_CHECK=True && source ~/miniconda3/etc/profile.d/conda.sh && conda activate invoice-sm120'
```
### 使用
```bash
# 激活 SM120 环境
source ~/.bashrc
activate-sm120
# 验证 GPU
python -c "import paddle; paddle.utils.run_check()"
# 运行服务
cd /mnt/c/Users/yaoji/git/ColaCoder/invoice-master-poc-v2
python run_server.py --port 8000
```
### 故障排除
| 错误 | 解决方案 |
|------|---------|
| `GLIBCXX_3.4.32 not found` | 升级到 Ubuntu 24.04 |
| `GLIBC_2.38 not found` | 升级到 Ubuntu 24.04 |
| `cublasLtCreate` 失败 | 检查 LD_LIBRARY_PATH 包含 nvidia 库路径 |
| `Mismatched GPU Architecture` | 使用 SM120 wheel不要用官方 paddle |
### 云部署
Azure/AWS GPU 实例 (A100, H100, T4, V100) 使用官方 PaddlePaddle无需 SM120 wheel。
## 项目结构
```
invoice-master-poc-v2/
├── packages/
│ ├── shared/ # 共享库
│ │ ├── setup.py
│ │ └── shared/
│ │ ├── pdf/ # PDF 处理 (提取, 渲染, 检测)
│ │ ├── ocr/ # PaddleOCR 封装 + 机器码解析
│ │ ├── normalize/ # 字段规范化 (10 种 normalizer)
│ │ ├── matcher/ # 字段匹配 (精确/子串/模糊)
│ │ ├── storage/ # 存储抽象层 (Local/Azure/S3)
│ │ ├── training/ # 共享训练组件 (YOLOTrainer)
│ │ ├── augmentation/ # 数据增强 (DatasetAugmenter)
│ │ ├── utils/ # 工具 (验证, 清理, 模糊匹配)
│ │ ├── data/ # DocumentDB, CSVLoader
│ │ ├── config.py # 全局配置 (数据库, 路径, DPI)
│ │ └── exceptions.py # 异常定义
│ │
│ ├── training/ # 训练服务 (GPU, 按需)
│ │ ├── setup.py
│ │ ├── Dockerfile
│ │ ├── run_training.py # 入口 (--task-id 或 --poll)
│ │ └── training/
│ │ ├── cli/ # train, autolabel, analyze_*, validate
│ │ ├── yolo/ # db_dataset, annotation_generator
│ │ ├── processing/ # CPU/GPU worker pool, task dispatcher
│ │ └── data/ # training_db, autolabel_report
│ │
│ └── backend/ # 后端服务 (Web API + 推理, 常驻)
│ ├── setup.py
│ ├── Dockerfile
│ ├── run_server.py # Web 服务器入口
│ └── backend/
│ ├── cli/ # infer, serve
│ ├── pipeline/ # YOLO 检测, 字段提取, 解析器
│ ├── web/ # FastAPI 应用
│ │ ├── api/v1/ # REST API (admin, public, batch)
│ │ ├── schemas/ # Pydantic 数据模型
│ │ ├── services/ # 业务逻辑
│ │ ├── core/ # 认证, 调度器, 限流
│ │ └── workers/ # 后台任务队列
│ ├── validation/ # LLM 验证器
│ ├── data/ # AdminDB, AsyncRequestDB, Models
│ └── azure/ # ACI 训练触发器
├── frontend/ # React 前端 (Vite + TypeScript + TailwindCSS)
│ ├── src/
│ │ ├── api/ # API 客户端 (axios + react-query)
│ │ ├── components/ # UI 组件
│ │ │ ├── Dashboard.tsx # 文档管理面板
│ │ │ ├── Training.tsx # 训练管理 (数据集/任务)
│ │ │ ├── Models.tsx # 模型版本管理
│ │ │ ├── DatasetDetail.tsx # 数据集详情
│ │ │ └── InferenceDemo.tsx # 推理演示
│ │ └── hooks/ # React Query hooks
│ └── package.json
├── migrations/ # 数据库迁移 (SQL)
│ ├── 003_training_tasks.sql
│ ├── 004_training_datasets.sql
│ ├── 005_add_group_key.sql
│ ├── 006_model_versions.sql
│ ├── 007_training_tasks_extra_columns.sql
│ ├── 008_fix_model_versions_fk.sql
│ ├── 009_add_document_category.sql
│ └── 010_add_dataset_training_status.sql
├── tests/ # 测试 (1,601 tests)
├── docker-compose.yml # 本地开发 (postgres + inference + training)
├── run_server.py # 快捷启动脚本
└── runs/train/ # 训练输出 (weights, curves)
```
## 支持的字段
| 类别 ID | 字段名 | 说明 |
|---------|--------|------|
| 0 | invoice_number | 发票号码 |
| 1 | invoice_date | 发票日期 |
| 2 | invoice_due_date | 到期日期 |
| 3 | ocr_number | OCR 参考号 (瑞典支付系统) |
| 4 | bankgiro | Bankgiro 号码 |
| 5 | plusgiro | Plusgiro 号码 |
| 6 | amount | 金额 |
| 7 | supplier_organisation_number | 供应商组织号 |
| 8 | payment_line | 支付行 (机器可读格式) |
| 9 | customer_number | 客户编号 |
## 快速开始
### 1. 自动标注
```bash
# 使用双池模式 (CPU + GPU)
python -m training.cli.autolabel \
--dual-pool \
--cpu-workers 3 \
--gpu-workers 1
# 单线程模式
python -m training.cli.autolabel --workers 4
```
### 2. 训练模型
```bash
# 从预训练模型开始训练
python -m training.cli.train \
--model yolo11n.pt \
--epochs 100 \
--batch 16 \
--name invoice_fields \
--dpi 150
# 低内存模式
python -m training.cli.train \
--model yolo11n.pt \
--epochs 100 \
--name invoice_fields \
--low-memory
# 从检查点恢复训练
python -m training.cli.train \
--model runs/train/invoice_fields/weights/last.pt \
--epochs 100 \
--name invoice_fields \
--resume
```
### 3. 推理
```bash
# 命令行推理
python -m backend.cli.infer \
--model runs/train/invoice_fields/weights/best.pt \
--input path/to/invoice.pdf \
--output result.json \
--gpu
```
### 4. Web 应用
```bash
# 从 Windows PowerShell 启动
wsl bash -c "source ~/miniconda3/etc/profile.d/conda.sh && conda activate invoice-sm120 && cd /mnt/c/Users/yaoji/git/ColaCoder/invoice-master-poc-v2 && python run_server.py --port 8000"
# 启动前端
cd frontend && npm install && npm run dev
# 访问 http://localhost:5173
```
### 5. Docker 本地开发
```bash
docker-compose up
# inference: http://localhost:8000
# training: 轮询模式自动拾取任务
```
## 训练触发流程
推理服务通过 API 触发训练,训练在独立的 GPU 实例上执行:
```
Inference API PostgreSQL Training (ACI)
| | |
POST /admin/training/trigger | |
|-> INSERT training_tasks ------>| status=pending |
|-> Azure SDK: create ACI --------------------------------> 启动
| | |
| |<-- SELECT pending -----+
| |--- UPDATE running -----+
| | 执行训练...
| |<-- UPDATE completed ---+
| | + model_path |
| | + metrics 自动关机
| | |
GET /admin/training/{id} | |
|-> SELECT training_tasks ------>| |
+-- return status + metrics | |
```
## Web API 端点
**Public API:**
| 方法 | 端点 | 描述 |
|------|------|------|
| GET | `/api/v1/health` | 健康检查 |
| POST | `/api/v1/infer` | 上传文件并推理 |
| GET | `/api/v1/results/{filename}` | 获取可视化图片 |
| POST | `/api/v1/async/infer` | 异步推理 |
| GET | `/api/v1/async/status/{task_id}` | 查询异步任务状态 |
**Admin API** (需要 `X-Admin-Token` header):
| 方法 | 端点 | 描述 |
|------|------|------|
| POST | `/api/v1/admin/auth/login` | 管理员登录 |
| GET | `/api/v1/admin/documents` | 文档列表 |
| POST | `/api/v1/admin/documents/upload` | 上传 PDF |
| GET | `/api/v1/admin/documents/{id}` | 文档详情 |
| PATCH | `/api/v1/admin/documents/{id}/status` | 更新文档状态 |
| PATCH | `/api/v1/admin/documents/{id}/category` | 更新文档分类 |
| GET | `/api/v1/admin/documents/categories` | 获取分类列表 |
| POST | `/api/v1/admin/documents/{id}/annotations` | 创建标注 |
**Training API:**
| 方法 | 端点 | 描述 |
|------|------|------|
| POST | `/api/v1/admin/training/datasets` | 创建数据集 |
| GET | `/api/v1/admin/training/datasets` | 数据集列表 |
| GET | `/api/v1/admin/training/datasets/{id}` | 数据集详情 |
| DELETE | `/api/v1/admin/training/datasets/{id}` | 删除数据集 |
| POST | `/api/v1/admin/training/tasks` | 创建训练任务 |
| GET | `/api/v1/admin/training/tasks` | 任务列表 |
| GET | `/api/v1/admin/training/tasks/{id}` | 任务详情 |
| GET | `/api/v1/admin/training/tasks/{id}/logs` | 训练日志 |
**Model Versions API:**
| 方法 | 端点 | 描述 |
|------|------|------|
| GET | `/api/v1/admin/models` | 模型版本列表 |
| GET | `/api/v1/admin/models/{id}` | 模型详情 |
| POST | `/api/v1/admin/models/{id}/activate` | 激活模型 |
| POST | `/api/v1/admin/models/{id}/archive` | 归档模型 |
| DELETE | `/api/v1/admin/models/{id}` | 删除模型 |
## Python API
```python
from backend.pipeline import InferencePipeline
# 初始化
pipeline = InferencePipeline(
model_path='runs/train/invoice_fields/weights/best.pt',
confidence_threshold=0.25,
use_gpu=True,
dpi=150,
enable_fallback=True
)
# 处理 PDF
result = pipeline.process_pdf('invoice.pdf')
print(result.fields)
# {'InvoiceNumber': '12345', 'Amount': '1234.56', ...}
print(result.confidence)
# {'InvoiceNumber': 0.95, 'Amount': 0.92, ...}
# 交叉验证
if result.cross_validation:
print(f"OCR match: {result.cross_validation.ocr_match}")
```
```python
from backend.pipeline.payment_line_parser import PaymentLineParser
from backend.pipeline.customer_number_parser import CustomerNumberParser
# Payment Line 解析
parser = PaymentLineParser()
result = parser.parse("# 94228110015950070 # 15658 00 8 > 48666036#14#")
print(f"OCR: {result.ocr_number}, Amount: {result.amount}")
# Customer Number 解析
parser = CustomerNumberParser()
result = parser.parse("Said, Shakar Umj 436-R Billo")
print(f"Customer Number: {result}") # "UMJ 436-R"
```
## DPI 配置
系统所有组件统一使用 **150 DPI**。DPI 必须在训练和推理时保持一致。
| 组件 | 配置位置 |
|------|---------|
| 全局常量 | `packages/shared/shared/config.py` -> `DEFAULT_DPI = 150` |
| Web 推理 | `packages/backend/backend/web/config.py` -> `ModelConfig.dpi` |
| CLI 推理 | `python -m backend.cli.infer --dpi 150` |
| 自动标注 | `packages/shared/shared/config.py` -> `AUTOLABEL['dpi']` |
## 数据库架构
| 数据库 | 用途 | 存储内容 |
|--------|------|----------|
| **PostgreSQL** | 主数据库 | 文档、标注、训练任务、数据集、模型版本 |
### 主要表
| 表名 | 说明 |
|------|------|
| `admin_documents` | 文档管理 (PDF 元数据, 状态, 分类) |
| `admin_annotations` | 标注数据 (YOLO 格式边界框) |
| `training_tasks` | 训练任务 (状态, 配置, 指标) |
| `training_datasets` | 数据集 (train/val/test 分割) |
| `dataset_documents` | 数据集-文档关联 |
| `model_versions` | 模型版本管理 (激活/归档) |
| `admin_tokens` | 管理员认证令牌 |
| `async_requests` | 异步推理请求 |
### 数据集状态
| 状态 | 说明 |
|------|------|
| `building` | 正在构建数据集 |
| `ready` | 数据集就绪,可开始训练 |
| `trained` | 已完成训练 |
| `failed` | 构建失败 |
| `archived` | 已归档 |
### 训练状态
| 状态 | 说明 |
|------|------|
| `pending` | 等待执行 |
| `scheduled` | 已计划 |
| `running` | 正在训练 |
| `completed` | 训练完成 |
| `failed` | 训练失败 |
| `cancelled` | 已取消 |
## 测试
```bash
# 运行所有测试
DB_PASSWORD=xxx pytest tests/ -q
# 运行并查看覆盖率
DB_PASSWORD=xxx pytest tests/ --cov=packages --cov-report=term-missing
```
| 指标 | 数值 |
|------|------|
| **测试总数** | 2,058 |
| **通过率** | 100% |
| **覆盖率** | 60% |
## 存储抽象层
统一的文件存储接口,支持多后端切换:
| 后端 | 用途 | 安装 |
|------|------|------|
| **Local** | 本地开发/测试 | 默认 |
| **Azure Blob** | Azure 云部署 | `pip install -e "packages/shared[azure]"` |
| **AWS S3** | AWS 云部署 | `pip install -e "packages/shared[s3]"` |
### 配置文件 (storage.yaml)
```yaml
backend: ${STORAGE_BACKEND:-local}
presigned_url_expiry: 3600
local:
base_path: ${STORAGE_BASE_PATH:-./data/storage}
azure:
connection_string: ${AZURE_STORAGE_CONNECTION_STRING}
container_name: ${AZURE_STORAGE_CONTAINER:-documents}
s3:
bucket_name: ${AWS_S3_BUCKET}
region_name: ${AWS_REGION:-us-east-1}
```
### 使用示例
```python
from shared.storage import get_storage_backend
# 从配置文件加载
storage = get_storage_backend("storage.yaml")
# 上传文件
storage.upload(Path("local.pdf"), "documents/invoice.pdf")
# 获取预签名 URL (前端访问)
url = storage.get_presigned_url("documents/invoice.pdf", expires_in_seconds=3600)
```
### 环境变量
| 变量 | 后端 | 说明 |
|------|------|------|
| `STORAGE_BACKEND` | 全部 | `local`, `azure_blob`, `s3` |
| `STORAGE_BASE_PATH` | Local | 本地存储路径 |
| `AZURE_STORAGE_CONNECTION_STRING` | Azure | 连接字符串 |
| `AZURE_STORAGE_CONTAINER` | Azure | 容器名称 |
| `AWS_S3_BUCKET` | S3 | 存储桶名称 |
| `AWS_REGION` | S3 | 区域 (默认: us-east-1) |
## 数据增强
训练时支持多种数据增强策略:
| 增强类型 | 说明 |
|----------|------|
| `perspective_warp` | 透视变换 (模拟扫描角度) |
| `wrinkle` | 皱纹效果 |
| `edge_damage` | 边缘损坏 |
| `stain` | 污渍效果 |
| `lighting_variation` | 光照变化 |
| `shadow` | 阴影效果 |
| `gaussian_blur` | 高斯模糊 |
| `motion_blur` | 运动模糊 |
| `gaussian_noise` | 高斯噪声 |
| `salt_pepper` | 椒盐噪声 |
| `paper_texture` | 纸张纹理 |
| `scanner_artifacts` | 扫描伪影 |
增强配置示例:
```json
{
"augmentation": {
"gaussian_blur": { "enabled": true, "kernel_size": 5 },
"perspective_warp": { "enabled": true, "intensity": 0.1 }
},
"augmentation_multiplier": 2
}
```
## 前端功能
React 前端提供以下功能模块:
| 模块 | 功能 |
|------|------|
| **Dashboard** | 文档列表、上传、标注状态管理、分类筛选 |
| **Training** | 数据集创建/管理、训练任务配置、增强设置 |
| **Models** | 模型版本管理、激活/归档、指标查看 |
| **Inference Demo** | 实时推理演示、结果可视化 |
### 启动前端
```bash
cd frontend
npm install
npm run dev
# 访问 http://localhost:5173
```
## 技术栈
| 组件 | 技术 |
|------|------|
| **目标检测** | YOLOv11 (Ultralytics) |
| **OCR 引擎** | PaddleOCR v5 (PP-OCRv5) |
| **PDF 处理** | PyMuPDF (fitz) |
| **数据库** | PostgreSQL + SQLModel |
| **Web 框架** | FastAPI + Uvicorn |
| **前端** | React + TypeScript + Vite + TailwindCSS |
| **状态管理** | React Query (TanStack Query) |
| **深度学习** | PyTorch + CUDA 12.x |
| **部署** | Docker + Azure/AWS (训练) / App Service (推理) |
## 环境变量
| 变量 | 必需 | 说明 |
|------|------|------|
| `DB_PASSWORD` | 是 | PostgreSQL 密码 |
| `DB_HOST` | 否 | 数据库主机 (默认: localhost) |
| `DB_PORT` | 否 | 数据库端口 (默认: 5432) |
| `DB_NAME` | 否 | 数据库名 (默认: docmaster) |
| `DB_USER` | 否 | 数据库用户 (默认: docmaster) |
| `STORAGE_BASE_PATH` | 否 | 存储路径 (默认: ~/invoice-data/data) |
| `MODEL_PATH` | 否 | 模型路径 |
| `CONFIDENCE_THRESHOLD` | 否 | 置信度阈值 (默认: 0.5) |
| `SERVER_HOST` | 否 | 服务器主机 (默认: 0.0.0.0) |
| `SERVER_PORT` | 否 | 服务器端口 (默认: 8000) |
## 许可证
MIT License