227 lines
7.9 KiB
Markdown
227 lines
7.9 KiB
Markdown
# 流云图库 — 元数据管线架构文档
|
||
|
||
> 版本: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. **纯中文/纯英文**:人名主体不允许中英混杂
|