This commit is contained in:
lychang
2026-07-19 23:56:28 +08:00
parent 34bcd67682
commit 36969a4fa0
16 changed files with 1925 additions and 350 deletions

View 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
View 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 → 全量解析 + 人物合并