356 lines
7.2 KiB
Markdown
356 lines
7.2 KiB
Markdown
# PDF 解析系统
|
||
|
||
提供统一接口支持多种 PDF 解析提供商。
|
||
|
||
## 支持的提供商
|
||
|
||
### 1. unpdf (内置)
|
||
|
||
- **成本**: 免费,内置
|
||
- **特性**: 基础文本提取、图片提取
|
||
- **要求**: 无
|
||
- **使用**: 直接上传 PDF 文件
|
||
|
||
### 2. MinerU (本地部署)
|
||
|
||
- **成本**: 免费(需要自己部署)
|
||
- **特性**:
|
||
- 高级文本提取(保留 Markdown 布局)
|
||
- 表格识别
|
||
- 公式提取(LaTeX)
|
||
- 更好的 OCR 支持
|
||
- 多种输出格式(markdown, JSON, docx, html, latex)
|
||
- **要求**:
|
||
- 部署 MinerU 服务(Docker 或源码)
|
||
- 配置服务器地址
|
||
- **优势**: 数据隐私、无文件大小限制
|
||
|
||
## 快速开始
|
||
|
||
### 部署 MinerU(可选)
|
||
|
||
```bash
|
||
# Docker 部署(推荐)
|
||
docker pull opendatalab/mineru:latest
|
||
docker run -d --name mineru -p 8080:8080 opendatalab/mineru:latest
|
||
|
||
# 验证
|
||
curl http://localhost:8080/api/health
|
||
```
|
||
|
||
### API 使用
|
||
|
||
#### 使用 unpdf(文件上传)
|
||
|
||
```typescript
|
||
const formData = new FormData();
|
||
formData.append('pdf', pdfFile);
|
||
formData.append('providerId', 'unpdf');
|
||
|
||
const response = await fetch('/api/parse-pdf', {
|
||
method: 'POST',
|
||
body: formData,
|
||
});
|
||
|
||
const result = await response.json();
|
||
// result.data: ParsedPdfContent
|
||
```
|
||
|
||
#### 使用 MinerU(本地服务)
|
||
|
||
```typescript
|
||
const formData = new FormData();
|
||
formData.append('pdf', pdfFile);
|
||
formData.append('providerId', 'mineru');
|
||
formData.append('baseUrl', 'http://localhost:8080');
|
||
|
||
const response = await fetch('/api/parse-pdf', {
|
||
method: 'POST',
|
||
body: formData,
|
||
});
|
||
|
||
const result = await response.json();
|
||
// result.data: ParsedPdfContent with imageMapping
|
||
```
|
||
|
||
## 响应格式
|
||
|
||
```typescript
|
||
interface ParsedPdfContent {
|
||
text: string; // 提取的文本(MinerU 为 Markdown)
|
||
images: string[]; // Base64 图片数组
|
||
|
||
// 扩展特性(MinerU)
|
||
tables?: Array<{
|
||
page: number;
|
||
data: string[][];
|
||
caption?: string;
|
||
}>;
|
||
|
||
formulas?: Array<{
|
||
page: number;
|
||
latex: string;
|
||
position?: { x: number; y: number; width: number; height: number };
|
||
}>;
|
||
|
||
layout?: Array<{
|
||
page: number;
|
||
type: 'title' | 'text' | 'image' | 'table' | 'formula';
|
||
content: string;
|
||
position?: { x: number; y: number; width: number; height: number };
|
||
}>;
|
||
|
||
metadata?: {
|
||
pageCount: number;
|
||
parser: 'unpdf' | 'mineru';
|
||
fileName?: string;
|
||
fileSize?: number;
|
||
processingTime?: number;
|
||
|
||
// 用于内容生成流程(MinerU)
|
||
imageMapping?: Record<string, string>; // img_1 -> base64 URL
|
||
pdfImages?: Array<{
|
||
id: string; // img_1, img_2, etc.
|
||
src: string; // base64 data URL
|
||
pageNumber: number; // PDF 页码
|
||
description?: string; // 图片描述
|
||
}>;
|
||
};
|
||
}
|
||
```
|
||
|
||
## 与内容生成集成
|
||
|
||
MinerU 解析器与内容生成流程无缝集成:
|
||
|
||
```typescript
|
||
// 1. 解析 PDF
|
||
const parseResult = await parsePDF(
|
||
{
|
||
providerId: 'mineru',
|
||
baseUrl: 'http://localhost:8080',
|
||
},
|
||
buffer,
|
||
);
|
||
|
||
// 2. 提取数据
|
||
const pdfText = parseResult.text; // Markdown(含 img_1 引用)
|
||
const pdfImages = parseResult.metadata.pdfImages; // 图片数组
|
||
const imageMapping = parseResult.metadata.imageMapping; // 图片映射
|
||
|
||
// 3. 生成场景大纲
|
||
await generateSceneOutlinesFromRequirements(
|
||
requirements,
|
||
pdfText, // Markdown 内容
|
||
pdfImages, // 带页码的图片
|
||
aiCall,
|
||
);
|
||
|
||
// 4. 生成场景(含图片)
|
||
await buildSceneFromOutline(
|
||
outline,
|
||
aiCall,
|
||
stageId,
|
||
assignedImages, // 从 pdfImages 筛选
|
||
imageMapping, // 用于解析 img_1 到实际 URL
|
||
);
|
||
```
|
||
|
||
## 图片处理流程
|
||
|
||
MinerU 的图片处理:
|
||
|
||
1. **提取**: PDF → MinerU → Markdown + 图片
|
||
2. **转换**: `` → ``
|
||
3. **映射**: 创建 `{ "img_1": "data:image/png;base64,..." }`
|
||
4. **生成**: AI 使用 `img_1` 引用生成幻灯片
|
||
5. **解析**: `resolveImageIds()` 替换为实际 URL
|
||
6. **渲染**: 幻灯片显示图片
|
||
|
||
## 配置
|
||
|
||
### 全局设置
|
||
|
||
```typescript
|
||
import { useSettingsStore } from '@/lib/store/settings';
|
||
|
||
useSettingsStore.setState({
|
||
pdfProviderId: 'mineru',
|
||
pdfProvidersConfig: {
|
||
mineru: {
|
||
baseUrl: 'http://localhost:8080',
|
||
apiKey: 'optional-if-needed',
|
||
},
|
||
},
|
||
});
|
||
```
|
||
|
||
### 请求级配置
|
||
|
||
```typescript
|
||
// 在 API 调用时覆盖全局设置
|
||
formData.append('providerId', 'mineru');
|
||
formData.append('baseUrl', 'http://your-server:8080');
|
||
formData.append('apiKey', 'optional');
|
||
```
|
||
|
||
## 添加新的提供商
|
||
|
||
### 1. 定义提供商
|
||
|
||
`lib/pdf/constants.ts`:
|
||
|
||
```typescript
|
||
export const PDF_PROVIDERS = {
|
||
myProvider: {
|
||
id: 'myProvider',
|
||
name: 'My Provider',
|
||
requiresApiKey: true,
|
||
features: ['text', 'images'],
|
||
},
|
||
};
|
||
```
|
||
|
||
### 2. 实现解析器
|
||
|
||
`lib/pdf/pdf-providers.ts`:
|
||
|
||
```typescript
|
||
async function parseWithMyProvider(
|
||
config: PDFParserConfig,
|
||
pdfBuffer: Buffer
|
||
): Promise<ParsedPdfContent> {
|
||
// 实现解析逻辑
|
||
return {
|
||
text: '...',
|
||
images: [...],
|
||
metadata: {
|
||
pageCount: 0,
|
||
parser: 'myProvider',
|
||
},
|
||
};
|
||
}
|
||
```
|
||
|
||
### 3. 添加到路由
|
||
|
||
```typescript
|
||
switch (config.providerId) {
|
||
case 'unpdf':
|
||
result = await parseWithUnpdf(pdfBuffer);
|
||
break;
|
||
case 'mineru':
|
||
result = await parseWithMinerU(config, pdfBuffer);
|
||
break;
|
||
case 'myProvider':
|
||
result = await parseWithMyProvider(config, pdfBuffer);
|
||
break;
|
||
}
|
||
```
|
||
|
||
## 调试工具
|
||
|
||
访问 http://localhost:3000/debug/pdf-parser 测试解析功能:
|
||
|
||
- 切换提供商(unpdf/MinerU)
|
||
- 上传 PDF 文件
|
||
- 配置服务器地址
|
||
- 查看解析结果
|
||
- 检查图片映射
|
||
|
||
## 常见问题
|
||
|
||
### Q: MinerU 服务无法连接?
|
||
|
||
**A**: 检查:
|
||
|
||
```bash
|
||
# 服务状态
|
||
docker ps | grep mineru
|
||
|
||
# 网络连通性
|
||
curl http://localhost:8080/api/health
|
||
|
||
# 日志
|
||
docker logs mineru
|
||
```
|
||
|
||
### Q: 图片不显示?
|
||
|
||
**A**: 确保:
|
||
|
||
1. `imageMapping` 正确传递到 scene-stream API
|
||
2. 图片 ID 格式正确(img_1, img_2)
|
||
3. Base64 编码完整
|
||
|
||
### Q: 解析速度慢?
|
||
|
||
**A**: 优化:
|
||
|
||
```bash
|
||
# 增加 Docker 资源
|
||
docker run -d \
|
||
--name mineru \
|
||
-p 8080:8080 \
|
||
--memory=4g \
|
||
--cpus=2 \
|
||
opendatalab/mineru:latest
|
||
```
|
||
|
||
### Q: unpdf vs MinerU 如何选择?
|
||
|
||
**A**: 选择建议:
|
||
|
||
| 场景 | 推荐 |
|
||
| ------------------ | ------ |
|
||
| 简单 PDF(纯文本) | unpdf |
|
||
| 包含表格、公式 | MinerU |
|
||
| 需要保留布局 | MinerU |
|
||
| 快速测试 | unpdf |
|
||
| 生产环境 | MinerU |
|
||
| 无法部署服务 | unpdf |
|
||
|
||
## 性能建议
|
||
|
||
### MinerU 并发处理
|
||
|
||
```typescript
|
||
const files = [file1, file2, file3];
|
||
|
||
const results = await Promise.all(
|
||
files.map((file) => {
|
||
const formData = new FormData();
|
||
formData.append('pdf', file);
|
||
formData.append('providerId', 'mineru');
|
||
return fetch('/api/parse-pdf', {
|
||
method: 'POST',
|
||
body: formData,
|
||
}).then((r) => r.json());
|
||
}),
|
||
);
|
||
```
|
||
|
||
### 结果缓存
|
||
|
||
```typescript
|
||
// 考虑缓存解析结果
|
||
const cacheKey = `pdf_${fileHash}`;
|
||
const cached = localStorage.getItem(cacheKey);
|
||
if (cached) {
|
||
return JSON.parse(cached);
|
||
}
|
||
```
|
||
|
||
## 参考资源
|
||
|
||
- **MinerU GitHub**: https://github.com/opendatalab/MinerU
|
||
- **快速开始**: `/MINERU_QUICKSTART.md`
|
||
- **变更说明**: `/MINERU_LOCAL_DEPLOYMENT.md`
|
||
- **调试工具**: http://localhost:3000/debug/pdf-parser
|
||
|
||
---
|
||
|
||
**最后更新**: 2026-02-11
|
||
**模式**: 本地自托管
|
||
**状态**: 生产就绪
|