docs: 重写 README,清理老版本残留并同步当前功能

This commit is contained in:
2025-06-17 21:17:12 +08:00
parent f849b0fc21
commit ea134afa27
+122 -148
View File
@@ -1,143 +1,93 @@
# kenaito-media-picker
媒体分辨率分类整理工具:一键扫描目录下所有图片与视频,按横屏 / 竖屏 / 等比和分辨率自动归类到对应子目录。
媒体文件自动分类整理工具:一键扫描目录下的图片 / 视频 / 音频,按 **方向(横屏 / 竖屏 / 等比)+ 分辨率 + 明暗** 自动归入对应子目录,并提供 Web 管理界面(实时进度、运行日志、媒体库、统计看板、可选色情识别)。
## 功能特性
- **图片 / 视频 / 音频**:可指定任意目录进行扫描(递归),自动识别图片、视频与音频文件;不填目录时默认扫描程序当前运行目录
- **标准分辨率归类**:720P、1080P、2K、2.5K、影院 2K、DCI 4K、8K 及带鱼屏(UltraWide 2K/3K/4K)等标准规格单独建目录
- **非标分辨率归类**:按像素区间(每 1024px 一档)自动归入 1K ~ 19K / 普通 / 原图(原画质)目录
- **方向区分**:横屏、竖屏、等比(正方形)分开整理,GIF 单独处理
- **EXIF 旋转修正**:手机/相机竖拍的图(文件里横着存、带 EXIF 旋转标记)自动识别并转正——找到 ffmpeg 时用 ffmpeg 把像素真正转正;找不到时至少按"显示方向"(宽高对调)归类,不会再误入横屏目录
- **明暗分类**:基于 ITU-R BT.601 亮度公式分析每张图片明暗程度,暗色(亮度 < 80)像素占比为 darkScore、亮色(亮度 > 175)像素占比为 lightScore;以两者差幅判定亮暗主导,在分辨率目录下追加 `dark_N` / `mid` / `bright_N` 子目录(N 为占比四舍五入到 20% 的档位,20/40/60/80/100 共 5 档)
- **音频归类**:音频文件单独归入「音频」目录
- **异常兜底**:读取失败、不支持的格式自动归入「读取异常」「已忽略」目录
- **自动整理**:无需人工干预,扫描 → 分析 → 移动一次完成,跨磁盘自动降级为复制后删除
- **全格式支持**:视频/音频以**内嵌的 ffprobe**(Windows 版)作为最终判定依据,**凡是 ffmpeg 支持的格式都能识别**,不再依赖扩展名白名单
- **媒体信息入库**:自动将文件的 MD5 与完整媒体信息(类型/宽高/时长/编码/容器等)存入根目录下的 `media-info.db`(SQLite,xorm 驱动),支持按字段查询与去重
- **全类型识别**:图片、视频、音频均可扫描(递归);视频/音频以内嵌 **ffprobe** 探测媒体流为准,**凡 ffmpeg 支持的格式都能识别**,不依赖扩展名白名单
- **方向区分**:横屏、竖屏、等比(正方形)分开整理
- **标准分辨率归类**:720P、1D5KP、1080P、2DKP、2D5KP、UltraWide 2K/3K/4K、4KP、4KHP、8KP 等精确匹配单独建目录
- **非标分辨率归类**:按长边每 1024px 一档归入 `1K` ~ `19K`,不足一档归「普通」,超出归「原图」(视频为「原画质」)
- **明暗分类**:基于 ITU-R BT.601 亮度公式,在分辨率目录下追加 `dark_N` / `mid` / `bright_N` 子目录(N 取 20/40/60/80/100 共 5 档)
- **EXIF 旋转修正**:带旋转标记的竖拍图自动处理——有 ffmpeg 时物理转正像素,否则至少按显示方向(宽高对调)归类;可在配置中整体关闭
- **色情识别(可选)**:本机 Python + NudeNet 离线鉴黄,命中的图片移入 `图片_敏感_*` 目录并记录检测分,Web 端可逐张预览复核
- **异常兜底**:读取失败、不支持格式自动归入「读取异常」「已忽略」目录;GIF、PSD 单独归类
- **媒体信息入库**:自动把文件 MD5 与完整媒体信息(类型 / 宽高 / 时长 / 编码 / 容器等)写入 `media-info.db`(SQLite),支持筛选查询与去重
- **Web 管理界面**:页面点击触发扫描整理,WebSocket 实时推送进度与运行日志;支持中止
- **并行处理**:探测 / 归类 / 移动各阶段并发 worker 数可配置(默认 4)
## 支持的格式
| 类型 | 格式 |
|------|------|
| 图片 | JPG / JPEG / JPE / JFIF / PNG / GIF / BMP / WEBP / PSB,PSD 单独归入「图片_PSD」;Go 无法解码的图片格式(如 TIFF)由 ffprobe 兜底 |
| 视频 | 凡 ffmpeg 支持的容器/编码均可识别(MP4 / MKV / AVI / MOV / TS / FLV / WMV / RM / RMVB / WEBM / M2TS 等),以 ffprobe 探测结果为准 |
| 音频 | 凡 ffmpeg 支持的音频(MP3 / WAV / FLAC / AAC / M4A / OGG / WMA 等)归入「音频」目录 |
| 图片 | JPG / JPEG / JPE / JFIF / PNG / GIF / BMP / WEBP / PSD 等;Go 无法解码的格式由 ffprobe 兜底 |
| 视频 | 凡 ffmpeg 支持的容器/编码(MP4 / MKV / AVI / MOV / TS / FLV / WMV / RM / RMVB / WEBM / M2TS 等),以 ffprobe 探测结果为准 |
| 音频 | 凡 ffmpeg 支持的音频(MP3 / WAV / FLAC / AAC / M4A / OGG / WMA 等),统一归入「音频」目录 |
> 判断依据:图片按文件 MIME + 扩展名识别(扩展名大小写不敏感);视频/音频由 ffprobe 读取媒体流判定,支持范围与 ffmpeg 一致。
> 图片按 MIME + 扩展名识别(大小写不敏感);视频/音频由 ffprobe 读取媒体流判定。
## 构建编译
## 快速开始
```shell
sh build.sh
```
- 依赖 Go 1.25.0+(`go.mod` 指定 1.25.0)
- 构建时内嵌 **Windows 版 ffprobe**:`internal/probe/bin/ffprobe.exe`,需提前放置(未入库,见 `.gitignore`)
- `build.sh` 在 **Windows** 下先构建前端、再 `go build` + `upx` 压缩,输出 `kenaito-media-picker.exe`(Web 服务 + 内嵌前端)
- 编译所需工具(ffmpeg / upx)压缩包已放在 `doc/` 目录,按需解压使用(upx 放置于项目根目录 `upx`)
## 使用方法
### 运行
```text
# 双击有风险, 双击需谨慎
将 kenaito-media-picker.exe 拖拽到需要整理的文件夹内,双击运行即可
运行编译出的 exe(如 kenaito-media-picker.exe),程序启动后自动打开浏览器进入
Web 管理界面(默认 http://localhost:8080)。
```
程序启动后自动打开浏览器进入 **Web 管理界面**(默认 http://localhost:8080),
媒体整理不再是自动执行,而是在页面上点击 **【扫描整理】** 后开始(整理会移动文件,请先备份)。
整理不再自动执行——在页面上点击 **【扫描整理】** 后开始(会移动文件,请先备份)。
## Web 管理界面
项目提供 **Web 管理界面**(Vue 2 + ElementUI,内嵌进程序二进制),负责触发扫描整理与浏览 `media-info.db` 媒体库。
### 运行服务
```shell
# 需要 Node.js 的机器上先构建前端(dist 会被 go:embed 打入程序)
cd frontend
npm install && npm run build
# 回到项目根目录编译(Windows 下输出 kenaito-media-picker.exe)
go build .
./kenaito-media-picker.exe
```
- 默认监听 `:8080`,启动后自动打开浏览器(配置 `server.autoOpen=false` 可关闭)
- 端口 / 数据库路径 / 自动打开浏览器可在 `application.properties` 配置:`server.port` / `database.dsn`(相对路径相对服务启动目录)/ `server.autoOpen`
### 开发模式(无需重复构建前端)
日常开发用 **Vite 开发服务器 + Go 源码运行**,前端改动实时热更新,不需要每次 `npm run build`:
```shell
# 终端 1:启动后端(源码运行,端口 8080)
go run .
# 终端 2:启动前端开发服务器(端口 3000,/api 自动代理到 8080)
cd frontend
npm install && npm run dev
```
- 浏览器访问 **http://localhost:3000** 即可联调
- 改前端文件 → Vite 热更新自动生效;改后端 → 重启 `go run .`
**只有发布时才需要重新构建前端**:执行 `npm run build` 把产物打进 `frontend/dist`,再 `go build .` 重新编译,前端改动才会进入二进制。
### 页面功能
### Web 页面
| 页面 | 功能 |
|------|------|
| 扫描整理 | 点击「选择目录」浏览服务器磁盘(只列目录),也可手动输入;点击【开始扫描】触发扫描归类,实时显示运行日志与进度 |
| 统计看板 | 媒体总数/总体积/重复文件、类型/方向/分辨率/编码/格式分布、体积 TOP10 |
| 媒体库 | 按类型/方向/分辨率/编码/格式/文件名筛选,分页 + 列排序 |
| 扫描整理 | 选择/输入扫描目录与存放目录,点击开始扫描,实时查看进度、运行日志,可中止 |
| 统计看板 | 媒体总数 / 总体积 / 平均·最大·最小文件 / 总时长,以及类型、方向、分辨率、编码、格式、扩展名等分布与体积 TOP10 |
| 媒体库 | 按类型 / 方向 / 分辨率 / 编码 / 格式 / 文件名筛选,分页 + 列排序 |
| 色情识别 | 展示最近一次开启识别扫描命中的图片:检测分 + 完整路径 + 缩略图预览,便于人工复核 |
> 分辨率统计与筛选复用归类规则(标准规格 / 1K~19K / 普通 / 原图),与归类结果一致。
## 配置(application.properties)
程序运行目录下的 `application.properties`,用记事本即可修改:
| 配置项 | 默认值 | 说明 |
|--------|--------|------|
| `server.port` | `8080` | Web 服务端口 |
| `server.autoOpen` | `true` | 启动后是否自动打开浏览器 |
| `database.dsn` | `media-info.db` | 媒体数据库路径(相对路径相对运行目录) |
| `ffmpeg.path` | 空 | ffmpeg 路径,用于 EXIF 旋转转正;留空自动在 PATH 找 |
| `organize.rotateImages` | `true` | 是否处理 EXIF 旋转:`true` 转正/按显示方向归类;`false` 完全不处理,按存储方向原样归类 |
| `organize.workers` | `4` | 探测/归类/移动的并发 worker 数(`0` = 用满全部 CPU 核) |
| `nsfw.enabled` | `false` | 是否开启色情图片识别 |
| `nsfw.python` | 空 | python 可执行文件路径;留空自动在 PATH 找 |
| `nsfw.threshold` | `0.5` | 判定阈值(0~1),敏感部位检测分达到即判为色情 |
## 整理规则
执行后,会在当前目录生成一批以 UUID 开头命名的子目录,文件按规则移动(或复制)进去:
### 图片
整理结果统一放进输出目录下的 **`KMP已整理`** 目录(默认在扫描目录下创建,也可在页面指定存放目录)。文件名会加一长串随机前缀(如 `a1b2..._照片.jpg`),防止重名覆盖。
```text
{uid}-图片_横屏_720P/dark_100 ← 1280×720,暗色主导
{uid}-图片_横屏_1D5KP/mid ← 1600×900,明暗均衡
{uid}-图片_横屏_1080P/bright_100 ← 1920×1080,亮色主导
{uid}-图片_横屏_2DKP/dark_80 ← 2048×1080(影院 2K)
{uid}-图片_竖屏_... ← 竖屏按同样规则(普通 / 1K~19K / 原图)
{uid}-图片_等比_... ← 等比(正方形)按同样规则
{uid}-图片_GIF ← 所有 GIF 图片
{uid}-图片_PSD ← 所有 PSD 文件
{uid}-图片_读取异常 ← 无法读取尺寸的图片
{uid}-图片_已忽略 ← 不支持格式的图片
KMP已整理/
├── 图片_横屏_1080P/
│ ├── bright_20/ ← 偏亮
│ ├── mid/ ← 明暗均衡
│ └── dark_40/ ← 偏暗
├── 图片_竖屏_2K/
├── 图片_等比_普通/ ← 非标分辨率:普通 / 1K~19K / 原图
├── 图片_横屏_UltraWide3KP/
├── 图片_GIF/
├── 图片_PSD/ ← PSD 设计稿原样收走
├── 图片_读取异常/ 图片_已忽略/
├── 图片_敏感_横屏_1080P/bright_20/ ← 判为色情的图片(明暗子目录照常)
├── 视频_横屏_1080P/ 视频_竖屏_1K/ 视频_等比_原画质/
├── 视频_横屏_GIF/ 视频_竖屏_GIF/ 视频_等比_GIF/
├── 视频_读取异常/ 视频_已忽略/
└── 音频/
```
### 视频 / 音频
标准分辨率档位:`720P / 1D5KP / 1080P / 2DKP / 2D5KP / UltraWide2KP / UltraWide3KP / UltraWide4KP / 4KP / 4KHP / 8KP`。
```text
{uid}-视频_横屏_720P / 1080P / 2DKP / 2D5KP / 4KP / 4KHP / 8KP
{uid}-视频_横屏_UltraWide2KP / UltraWide3KP / UltraWide4KP
{uid}-视频_横屏_1k ~ 19k / 普通 / 原画质
{uid}-视频_竖屏_1k ~ 19k / 普通 / 原画质
{uid}-视频_等比_1k ~ 19k / 普通 / 原画质
{uid}-视频_横屏_GIF / 视频_竖屏_GIF / 视频_等比_GIF
音频 ← 所有音频文件(ffmpeg 可识别的格式)
视频_读取异常 ← mime 为视频但 ffprobe 读取失败
视频_已忽略 ← 非媒体文件或无法识别
```
### 重复文件
```text
重复文件 ← 与库中已有记录 MD5 相同的图片/视频/音频(不去重入库,单独存放)
```
> 文件按 MD5 去重:已入库的同一内容文件(不论图片、视频还是音频)统一移入「重复文件」目录。
> 视频时长与分辨率通过内嵌 ffprobe 读取(处理时会临时释放到当前目录、完成后自动删除);凡 ffmpeg 支持的视频/音频格式均可识别。
## 明暗评分算法
### 明暗评分算法
```text
brightness = 0.299×R + 0.587×G + 0.114×B (ITU-R BT.601)
@@ -145,53 +95,77 @@ darkScore = 亮度 < 80 的像素占比
lightScore = 亮度 > 175 的像素占比
```
- **判定规则**:`lightScore` 高于 `darkScore` 15 个百分点以上归 `bright_N` 子目录,反之归 `dark_N` 子目录;差幅不足 15% 视为中性归 `mid`(不强制二选一),明暗名作为分辨率目录下的子目录(如 `图片_横屏_1080P/bright_20`)
- **档位命名**:N 为占比四舍五入到 20% 的档位(20~100,共 5 档),如 darkScore 0.70 → `dark_80`,表示占比命中 [N-10%, N+10%) 区间
- **透明处理**:完全透明的像素(alpha=0)不计入统计,全透明图片归 `-mid`
- **性能**:大图(>100 万像素)先等比缩放为最长边 512px 的缩略图(纯内存)再统计明暗,内存占用与耗时大幅下降;超 4000 万像素的图片直接跳过明暗分析(按中性 mid 归类)避免 OOM
- **判定规则**:`lightScore` 高于 `darkScore` 15 个百分点以上归 `bright_N`,反之归 `dark_N`,差幅不足 15% 归 `mid`
- **档位命名**:N 为占比四舍五入到 20% 的档位(20/40/60/80/100 共 5 档),如 darkScore 0.70 → `dark_80`
- **透明处理**:完全透明的像素(alpha=0)不计入统计,全透明图片归 `mid`
- **性能**:超过 25 万像素的图先等比缩放为最长边 256px 的缩略图(纯内存)再统计;超过 4000 万像素直接跳过明暗分析(按 `mid` 归类),避免内存暴涨
### 去重
与数据库中已有记录 **MD5 相同且源路径相同** 的文件视为"同一文件被重复扫到",原地忽略(不移动、不重复入库)。MD5 相同但路径不同视为不同文件,照常整理。
### 色情图片识别(可选)
基于本机 **Python + NudeNet** 的离线鉴黄,判为色情的图片移入 `图片_敏感_*` 分类目录(明暗子目录照常保留,如 `图片_敏感_横屏_1080P/bright_20`),并可在 Web「色情识别」页面查看每张图的检测分、路径与预览。
- **开启**:`application.properties` 设 `nsfw.enabled=true`
- **依赖**:本机安装 Python 并 `pip install nudenet`
- **模型**:优先加载程序运行目录下 `lib/320n.onnx`(本仓库已附带,约 11.6MB,离线可用);本地缺失时自动下载到该位置,下载失败再回退用 nudenet 包自带模型
- **可调**:`nsfw.python`(python 路径)、`nsfw.threshold`(判定阈值)
- **敏感类**:`FEMALE_GENITALIA_EXPOSED` / `MALE_GENITALIA_EXPOSED` / `ANUS_EXPOSED` / `FEMALE_BREAST_EXPOSED`,任一分数达标即判定
- **降级**:未装 python / nudenet 时打印警告并跳过识别,图片按普通流程整理,不影响其它功能
## 构建编译
```shell
sh build.sh
```
- 依赖 Go 1.25.0+(`go.mod` 指定)
- `build.sh` 先构建前端,再 `go build` 输出 `kenaito-media-picker-release.exe`(upx 压缩已注释,可自行启用)
- 前端产物(`frontend/dist`)由 `go:embed` 打进二进制;`internal/probe/bin/ffprobe.exe`(Windows 版,约 145MB)已随仓库入库并内嵌
### 前端开发模式(无需重复构建)
```shell
# 终端 1:后端源码运行(端口 8080)
go run .
# 终端 2:前端开发服务器(端口 3000,/api 代理到 8080,热更新)
cd frontend
npm install && npm run dev
```
浏览器访问 http://localhost:3000 联调。**只有发布时才需要** `npm run build` 重新构建前端。
## 目录结构
```text
├── main.go # 程序入口(Web 服务:Gin + 内嵌前端产物)
├── main.go # 程序入口(Gin + 内嵌前端产物)
├── application.properties # 运行配置(端口 / 数据库 / 旋转开关 / 色情识别)
├── lib/320n.onnx # 色情识别模型(随仓库分发,离线可用)
├── build.sh # 编译脚本
├── internal/
│ ├── scanner/ # 文件扫描、MIME 读取、图片/媒体候选过滤
│ ├── probe/ # 媒体探测:图片尺寸 + ffprobe 解析 + 内嵌解码器释放
│ ├── classify/ # 纯函数归类规则(方向 + 标准分辨率 + K档 + 明暗等级)
│ ├── organize/ # 整理编排:探测→归类→收集→移动→记录去向
│ ├── mover/ # 文件移动(rename / 跨盘 copy+delete)
│ ├── store/ # SQLite 媒体信息持久化 + 查询/统计
│ ├── service/ # 后台任务编排(扫描整理任务 + 状态/日志)
│ ├── config/ # Web 服务配置加载(application.properties)
│ ├── nsfw/ # 色情图片识别(本机 python + NudeNet 本地服务)
│ ├── api/ # HTTP 接口层(统一响应 / 媒体列表 / 统计 / 扫描触发)
│ └── digest/ # 文件 MD5 / 指纹采样
├── frontend/ # 前端工程(Vue 2 + ElementUI + Vite,dist 内嵌进程序)
├── application.properties # Web 服务配置(端口 / 数据库路径)
├── doc/ # 构建依赖(ffmpeg / upx 压缩包)
└── build.sh # 编译脚本(upx 压缩)
│ ├── scanner/ # 文件扫描、MIME 读取、候选过滤
│ ├── probe/ # 媒体探测:图片尺寸 / ffprobe / EXIF / 明暗分析(含内嵌 ffprobe)
│ ├── classify/ # 纯函数归类规则(方向 + 标准分辨率 + K档 + 明暗等级)
│ ├── organize/ # 整理编排:探测 → 归类 → 收集 → 移动 → 入库
│ ├── mover/ # 文件移动(rename / 跨盘 copy+delete)
│ ├── store/ # SQLite 媒体信息 / 色情记录持久化 + 查询/统计
│ ├── service/ # 后台任务编排(扫描整理任务 + 状态/日志/WebSocket)
│ ├── nsfw/ # 色情图片识别(内嵌 python 服务 + NudeNet 本地 HTTP)
│ ├── api/ # HTTP 接口层(扫描 / 媒体库 / 统计 / 色情识别 / 目录浏览)
│ └── digest/ # 文件 MD5 / 指纹采样
└── frontend/ # 前端工程(Vue 2 + ElementUI + Vite)
```
## 注意事项
- 默认使用**移动**模式,原文件会从源目录移走;同名文件跨磁盘时自动降级为复制 + 删除
- 已整理文件全部移动到 `KMP已整理` 白名单目录内,重复运行会跳过该目录,并将新文件并入已有的分类目录
- 扫描整理由页面点击触发,整理期间服务会短暂占用较多 CPU/磁盘,属正常现象
- 同一时间只允许一次扫描整理;重复点击会提示「正在运行中」
- GIF 图片单独归入「图片_GIF」;GIF 视频按方向归入「视频_*_GIF」
- ffprobe 内嵌(Windows),运行前释放到当前目录、结束后自动删除
- EXIF 旋转转正需要 ffmpeg:配置 `ffmpeg.path` 指定路径,或让 ffmpeg 在 PATH 里;找不到 ffmpeg 时不转正像素,仍按显示方向归类。想完全关闭旋转处理(不转正、也不按显示方向调宽高,图片按存储方向原样归类)可在 `application.properties` 里设 `organize.rotateImages=false`
## 色情图片识别(可选)
基于本机 **Python + NudeNet** 的离线鉴黄,判为色情的图片移入 `图片_敏感_*` 分类目录(明暗子目录照常保留,如 `图片_敏感_横屏_1080P/bright_20`),便于事后人工复核。
- **开启**:`application.properties` 里设 `nsfw.enabled=true`(默认关闭)
- **依赖**:本机安装 Python 并 `pip install nudenet`
- **模型(推荐分发方式)**:把模型文件放到程序运行目录的 `lib/320n.onnx`(官方地址 `https://github.com/notAI-tech/NudeNet/releases/download/v3.4-weights/320n.onnx`,约 11.6MB,需自带或下载后随程序分发)——放好后完全离线可用。`lib` 里没有时程序会自动下载一次到该位置;下载失败再回退用 nudenet 包自带的模型
- **可调**:`nsfw.python`(python 路径,留空自动在 PATH 找)、`nsfw.threshold`(判定阈值,默认 0.5)
- **敏感类**:`FEMALE_GENITALIA_EXPOSED` / `MALE_GENITALIA_EXPOSED` / `ANUS_EXPOSED` / `FEMALE_BREAST_EXPOSED`,任一分数达标即判定
- **降级**:未装 python / nudenet 时打印警告并跳过识别,图片按普通流程整理,不影响其它功能
- 默认使用**移动**模式,原文件会从源目录移走;跨磁盘时自动降级为复制 + 删除
- 已整理文件全部进入 `KMP已整理` 白名单目录,重复运行会跳过它,并把新文件并入已有分类目录
- 扫描会自动跳过 `KMP已整理` 输出目录、`media-info.db` 数据库与 ffprobe 临时文件
- 同一时间只允许一次扫描整理;整理期间会占用较多 CPU/磁盘,属正常现象
- EXIF 旋转转正需要 ffmpeg:配置 `ffmpeg.path` 指定路径,或让 ffmpeg 在 PATH 里;找不到 ffmpeg 时不转正像素,仍按显示方向归类。完全关闭旋转处理(也不按显示方向调宽高)可在配置里设 `organize.rotateImages=false`
## License