update
This commit is contained in:
226
docs/pipeline-architecture.md
Normal file
226
docs/pipeline-architecture.md
Normal file
@@ -0,0 +1,226 @@
|
||||
# 流云图库 — 元数据管线架构文档
|
||||
|
||||
> 版本:v2.0 · 更新:2026-07-19
|
||||
|
||||
---
|
||||
|
||||
## 总览
|
||||
|
||||
流云图库是一个 Rust 编写的图片管理应用,核心功能是从相册标题中自动提取**人物名称**和**分类标签**,并建立人物-相册关联关系。所有提取和关联逻辑由一套三阶段的**处理管线(Pipeline)** 驱动。
|
||||
|
||||
```
|
||||
相册标题(如 "水淼Aqua cosplay Zani – Wuthering Waves 69 photos")
|
||||
│
|
||||
▼
|
||||
┌─ Pipeline ──────────────────────────────────────┐
|
||||
│ Phase 1 全量解析 提取人名 + 提取标签 │
|
||||
│ Phase 2 人物合并 按中文核心合并重复人物 │
|
||||
│ Phase 3 缓存刷新 重新加载内存缓存 │
|
||||
└──────────────────────────────────────────────────┘
|
||||
│
|
||||
├──→ albums 表(person_name_original, tags 等)
|
||||
├──→ persons 表(人物主表)
|
||||
├──→ person_aliases 表(别名关联)
|
||||
└──→ album_tags 表(标签关联)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 启动流程与触发时机
|
||||
|
||||
应用启动后,管线任务按顺序串行执行三个阶段:
|
||||
|
||||
```
|
||||
启动管线(单次,后台任务 A)
|
||||
│
|
||||
├── ① 扫描导入新相册(import)
|
||||
├── ② 全量解析 + 人物合并(run_pipeline)
|
||||
└── ③ 刷新 AlbumsCache
|
||||
```
|
||||
|
||||
- **启动管线**:import → run_pipeline → refreshCache,三件事**顺序串行**,不并发
|
||||
- **定时管线**:每 30 分钟触发一次,执行 run_pipeline → refreshCache(不重复 import)
|
||||
- **API 触发**:POST /api/pipeline/run 可手动触发 run_pipeline
|
||||
|
||||
---
|
||||
|
||||
## Phase 1:全量解析
|
||||
|
||||
**入口函数:** `phase_parse_all(db)` → `(person_updated, tag_updated)`
|
||||
|
||||
对数据库中**每一个相册**独立执行以下流程:
|
||||
|
||||
```
|
||||
for each album:
|
||||
│
|
||||
├── jieba 分词(词性标注)
|
||||
│ ├──→ 提取推荐人名(词性 nr/nrfg/nrt/nz)
|
||||
│ └──→ 提取推荐标签(词性 n/nz/ns/eng/x)
|
||||
│
|
||||
├── 确定最佳人名
|
||||
│ ├── 有人物库匹配?→ 用库匹配结果
|
||||
│ └── 否则 → 用 jieba 推荐
|
||||
│
|
||||
├── 写入 DB(强制覆盖,不比对旧值)
|
||||
│
|
||||
├── 提取标签
|
||||
│ ├── 三步提取:分类词库 → jieba → 正则兜底
|
||||
│ ├── unique_tags() 去重+排序+坏标签过滤
|
||||
│ └── 写入 DB(强制覆盖,不比对旧值)
|
||||
│
|
||||
└── 下一个相册
|
||||
```
|
||||
|
||||
### 人名提取策略
|
||||
|
||||
按优先级依次尝试:
|
||||
|
||||
1. **人物库精确匹配** — `person_merger::match_title()`,在已有 persons 表中查询标题子串
|
||||
2. **jieba 词性标注** — `extract_person_from_tags()`,优先取 nr(人名)类词
|
||||
3. **标题模式匹配** — `extract_person_name_from_title()`,尝试 cosplay 格式、分隔符切分、正则扫描
|
||||
|
||||
### 人名处理规则
|
||||
|
||||
- **禁止中英混杂**:人名主体为纯中文或纯英文(中文优先级更高)
|
||||
- 中英文混合(如"水淼Aqua")由 `split_person_name_with_aliases` 拆分为:主名="水淼" + 别名="Aqua"
|
||||
- 别名保存到 `person_aliases` 表,用于后续搜索匹配
|
||||
|
||||
### 标签分类体系
|
||||
|
||||
| 分类 | 标识 | 示例词 |
|
||||
|------|------|--------|
|
||||
| 风格 | style | 日系、韩系、复古、甜美、sexy |
|
||||
| 服装 | outfit | 比基尼、旗袍、制服、丝袜 |
|
||||
| 场景 | scene | 室内、户外、街拍、浴室、beach |
|
||||
| 身体 | body | 美腿、翘臀、玉足、美背 |
|
||||
| 情绪 | mood | 清纯、诱惑、唯美、冷艳 |
|
||||
| 主题 | topic | 写真、私房、旅拍、cosplay |
|
||||
| 名词 | noun | jieba 提取的普通名词 |
|
||||
| 专名 | proper | jieba 提取的专有名词 |
|
||||
| 地名 | location | jieba 提取的地名 |
|
||||
| 其他 | other | 正则兜底提取的剩余关键词 |
|
||||
|
||||
### 标签过滤链(从外到内共 5 层)
|
||||
|
||||
```
|
||||
1. is_punctuation_only() → 检测不含任何字/字母/数字,则跳过
|
||||
2. 停用词表过滤 → photos、cosplay、美女、模特 等 50+ 词
|
||||
3. 人名排除 → 排除标签提取时确定的人名
|
||||
4. 词性排除 → 形容词(a)、动词(v)、名动词(vn)、机构(nt) 等不产出标签
|
||||
5. unique_tags() 最终过滤 → 调用 is_bad_tag() 二次确认
|
||||
```
|
||||
|
||||
### 强制覆盖写入
|
||||
|
||||
每次管线执行都是**完全重新解析**,提取到人名就写入覆盖、提取到标签就写入覆盖。不做"新旧好坏"比对:
|
||||
|
||||
- **人名**:jieba/fuzzy 提取到 → 直接写入 `albums.person_name_*`,创建/更新 `persons` 记录及别名关联
|
||||
- **标签**:经过 5 层过滤链后仍有结果 → 直接写入 `albums.tags`,重建 `album_tags` 表
|
||||
|
||||
这样每次启动或定时触发时,标签和人名都会用最新的解析逻辑重新计算并强制覆盖,解析逻辑改进后老数据自动受益。
|
||||
|
||||
---
|
||||
|
||||
## Phase 2:人物合并
|
||||
|
||||
**入口函数:** `phase_merge_persons(db)` → `merged_count`
|
||||
|
||||
按**中文核心**自动合并重复人物:
|
||||
|
||||
```
|
||||
1. 扫描所有 persons,提取其中文核心(开头的连续中文字符)
|
||||
2. 将中文核心相同的 person 分为一组
|
||||
3. 每组选相册数最多的作为目标(dst)
|
||||
4. 其余合并到目标:
|
||||
- 移动所有相册关联
|
||||
- 移动所有别名
|
||||
- 源人物原名也作为目标别名
|
||||
- 删除源人物
|
||||
5. 更新目标的统计计数
|
||||
```
|
||||
|
||||
示例:`杨晨晨Yome` 和 `杨晨晨Sugar` 的中文核心都是 "杨晨晨" → 合并为一个人物
|
||||
|
||||
---
|
||||
|
||||
## 核心数据表结构
|
||||
|
||||
### albums 表(关键字段)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | TEXT | UUID |
|
||||
| title | TEXT | 相册标题,来源是文件夹名或 index.json |
|
||||
| person_name_original | TEXT | 提取的原始人名(可能带英文后缀) |
|
||||
| person_name_cleaned | TEXT | 清洗后的人名(仅中文主名) |
|
||||
| tags | TEXT | JSON 数组,如 `["比基尼","室内","清纯"]` |
|
||||
| all_words | TEXT | JSON 数组,如 `[["比基尼","outfit"],["室内","scene"]]` |
|
||||
| processed_at | TEXT | 最后处理时间 |
|
||||
|
||||
### persons 表
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | INTEGER | 自增主键 |
|
||||
| name | TEXT | 人物名(通常是中文主名) |
|
||||
| display_name | TEXT | 显示名 |
|
||||
| total_albums | INTEGER | 关联相册数 |
|
||||
| total_images | INTEGER | 关联图片数 |
|
||||
|
||||
### person_aliases 表
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | INTEGER | 自增主键 |
|
||||
| person_id | INTEGER | 关联 persons.id |
|
||||
| alias_type | TEXT | 别名类型(english/nickname/chinese/original) |
|
||||
| alias_name | TEXT | 别名文本 |
|
||||
|
||||
### album_tags 表
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | INTEGER | 自增主键 |
|
||||
| album_id | TEXT | 关联 albums.id |
|
||||
| tag | TEXT | 标签文本 |
|
||||
|
||||
---
|
||||
|
||||
## 定时任务
|
||||
|
||||
每 30 分钟触发一次,顺序执行:
|
||||
|
||||
```
|
||||
1. run_pipeline() → Phase 1(全量解析)+ Phase 2(人物合并)
|
||||
2. 刷新 AlbumsCache(内存缓存,为 API 提供快速查询)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API 接口
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| POST | /api/pipeline/run | 手动触发一次完整管线 |
|
||||
|
||||
返回:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"persons_updated": 12,
|
||||
"tags_updated": 45,
|
||||
"persons_merged": 3,
|
||||
"message": "管线执行完毕:更新人名 12 个,更新标签 45 个,合并人物 3 人"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 关键设计原则
|
||||
|
||||
1. **幂等性**:管线可反复执行,每次都是完整重新解析 + 强制覆盖写入
|
||||
2. **渐进改进**:解析逻辑改进后,老数据在下次管线执行时自动受益
|
||||
3. **强制覆盖**:提取到即写入,不做新旧比对,确保数据始终使用最新逻辑
|
||||
4. **中文优先**:人名提取优先中文,中英文混合被拆分为主名+别名
|
||||
5. **多层过滤**:标签经过 5 层过滤链,确保质量
|
||||
6. **纯中文/纯英文**:人名主体不允许中英混杂
|
||||
124
docs/pipeline-spec.md
Normal file
124
docs/pipeline-spec.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# 元数据管线 — 开发规格
|
||||
|
||||
> 2026-07-19
|
||||
|
||||
---
|
||||
|
||||
## 1. 需求概述
|
||||
|
||||
系统在启动和运行时,需要自动完成三个任务:
|
||||
|
||||
| # | 任务 | 说明 |
|
||||
|---|------|------|
|
||||
| 1 | **新相册解析** | 扫描磁盘导入的新相册 → 提取人名+标签 → 写入 DB |
|
||||
| 2 | **老数据重解析** | 已有数据的相册用最新逻辑重新提取,强制覆盖 |
|
||||
| 3 | **人物合并** | 按中文核心合并同一人的不同记录 |
|
||||
|
||||
这三件事通过**同一个管道**完成,管道在**启动时跑一次** + **定时每 30 分钟跑一次**。
|
||||
|
||||
---
|
||||
|
||||
## 2. 启动流程(时序)
|
||||
|
||||
当前问题:import 和 pipeline 是两个并发的 `tokio::spawn`,pipeline 固定等 3s,import 可能耗时更长,导致刚导入的相册没被解析。
|
||||
|
||||
**目标时序:**
|
||||
|
||||
```
|
||||
服务启动
|
||||
│
|
||||
├── DB 初始化
|
||||
│
|
||||
├─── 后台任务 A:启动管线(单次)─────┐
|
||||
│ ├── ① 扫描导入新相册 │
|
||||
│ ├── ② Phase 1:全量解析(强制覆盖) │
|
||||
│ ├── ③ Phase 2:人物合并 │
|
||||
│ └── ④ 刷新内存缓存 │
|
||||
│ │
|
||||
├─── 后台任务 B:定时管线(每 30 分钟)──┤
|
||||
│ ├── ① Phase 1:全量解析(强制覆盖) │
|
||||
│ ├── ② Phase 2:人物合并 │
|
||||
│ └── ③ 刷新内存缓存 │
|
||||
│ │
|
||||
└─── HTTP 服务启动 │
|
||||
│
|
||||
注:后台任务 A 和 B 互斥,不会同时执行
|
||||
```
|
||||
|
||||
### 变化要点
|
||||
|
||||
- **启动管线 = import + parse + merge + refreshCache** 依次串行
|
||||
- **定时管线 = parse + merge + refreshCache**(不重复 import)
|
||||
- 管线内部顺序固定不可并发
|
||||
|
||||
---
|
||||
|
||||
## 3. 管线内部结构
|
||||
|
||||
```
|
||||
run_pipeline(db, with_import: bool)
|
||||
│
|
||||
├── [with_import=true] 扫描导入新相册
|
||||
│
|
||||
├── Phase 1:全量解析
|
||||
│ for each album:
|
||||
│ ├── jieba 分词 → 候选人名 + 候选标签
|
||||
│ ├── 人物库匹配 / jieba → 最佳人名
|
||||
│ ├── 强制写入 albums.person_name_* + persons + aliases
|
||||
│ ├── 标签三步提取(词库→jieba→正则)
|
||||
│ ├── unique_tags(去重+排序+坏标签过滤)
|
||||
│ └── 强制写入 albums.tags + album_tags
|
||||
│
|
||||
├── Phase 2:人物合并
|
||||
│ ├── 提取所有人名的中文核心
|
||||
│ ├── 分组 → 每组选最多相册的为目标
|
||||
│ └── 合并相册关联 / 别名,删除源
|
||||
│
|
||||
└── Phase 3:刷新缓存
|
||||
└── 重新加载 albums 到 AlbumsCache
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 模块职责
|
||||
|
||||
| 模块 | 文件 | 职责 |
|
||||
|------|------|------|
|
||||
| Pipeline 引擎 | `engine/pipeline.rs` | `phase_parse_all()`、`phase_merge_persons()`、`run_pipeline()` |
|
||||
| 标题解析 | `engine/title_parser.rs` | jieba 分词、人名提取、中英文拆分、别名生成 |
|
||||
| 标签提取 | `engine/tag_extractor.rs` | 分类词库匹配、jieba 名词提取、标点/停用词/坏标签过滤 |
|
||||
| 人物合并 | `engine/person_merger.rs` | 中文核心提取、分组分析、合并执行 |
|
||||
| 导入 | `services/import.rs` | 扫描磁盘目录、创建相册和图片记录 |
|
||||
| DB | `db/metadata.rs` | persons、aliases、album_tags 表的读写 |
|
||||
| 缓存 | `cache/mod.rs` | AlbumsCache + TagCache + PersonCache |
|
||||
|
||||
---
|
||||
|
||||
## 5. 关键设计约束
|
||||
|
||||
1. **强制覆盖**:每次管线执行都是完整重解,直接覆盖 DB 数据
|
||||
2. **幂等**:管线可反复执行,不影响正确性
|
||||
3. **串行**:管线内部各阶段顺序执行,不并发
|
||||
4. **多级过滤**:标签经过 5 层过滤才入库,确保质量
|
||||
5. **中文优先**:人名优先中文,中英混杂拆为"主名+别名"
|
||||
6. **纯中/纯英**:人名主体不允许中英混杂
|
||||
|
||||
---
|
||||
|
||||
## 6. 实现清单
|
||||
|
||||
### 6.1 Pipeline 引擎
|
||||
|
||||
- [x] `phase_parse_all()` — 全量解析
|
||||
- [x] `phase_merge_persons()` — 人物合并
|
||||
- [x] `phase_refresh_cache()` — 刷新缓存(在 main.rs 实现)
|
||||
- [x] `run_pipeline(db)` — 整合解析+合并
|
||||
|
||||
### 6.2 main.rs 启动逻辑
|
||||
|
||||
- [x] 后台任务 A:顺序执行 import → run_pipeline → cache refresh
|
||||
- [x] 后台任务 B:定时每 30 分钟 run_pipeline → cache refresh
|
||||
|
||||
### 6.3 API
|
||||
|
||||
- [x] POST /api/pipeline/run → 全量解析 + 人物合并
|
||||
Reference in New Issue
Block a user