> - 「我照片里有好多重复的,帮我清掉副本」
> - 「这些照片太乱,帮我看看哪些没归类」
> - 「手机备份和电脑里的是不是重复的」
> - 「帮我把散落的老照片归归类(只给建议,别乱动)」
> - 「看看我的照片库到底有多大、多少张」
> - 「千万别改我的文件夹名和文件名」
> 照片查重归档 — 保留原文件夹结构与文件名 · 只整理不删除 · 可一键还原
> 🆕 2.2.9(真实照片库实测后修复)
> ① 视频不再被误报「EXIF 损坏」——.MOV/.MP4 直接跳过 EXIF 解析,正常视频不再被说成坏文件;
> ② 历史暂存目录不再被重复计入——_dedup_trash 等一律剪枝,重复数与「能腾出多少空间」不再虚高;
> ③ 「查重待整理 / 未归档 / Inbox」这类整理半成品,盘点时单列出来请你拍板——它们不再默默躺在库里,这正是「越整越乱」的观感来源;
> ④ 根目录只放散落照片/视频时,不再误报「库是空的」。
🚀 一句话开始(照着说就行)
> 「帮我整理一下 D:\照片 这个照片库」 —— 把你的照片文件夹路径填进去就行。
你只需要给一样东西:照片库在哪(一个文件夹路径,比如 D:\照片、E:\我的图片\手机备份)。
不知道准确路径也没关系——说「我的照片大概在 D 盘」「手机备份的那个文件夹」,它会先帮你找到、跟你确认,再动手。
接下来会发生什么(4 步,前三步一个文件都不动):
| 步骤 | 你会看到 | 会不会动你的文件 |
|---|---|---|
| 1. 只读盘点 | 一共多少张、多大、哪些是重复的、哪些散落没归类 | ❌ 一个都不动 |
| 2. 出报告给你看 | 重复组清单 + 能腾出多少空间 + 未归类照片的归类建议 | ❌ 一个都不动 |
| 3. 等你点头 | 把方案摆清楚,你不确认就不往下走 | ❌ 等你确认 |
| 4. 去重暂存 | 重复副本移到 <照片库>_重复暂存隔离/,随时可还原 |
✅ 只动重复副本 |
> 全程不会改你任何文件夹名、文件名 —— 那是你多年积累的手迹和记忆地图。
这些它帮不了,别误会:
- ❌ 不会把照片重命名成统一格式、不会强制搬进日期文件夹(那会抹掉你的记忆地图)
- ❌ 不会自动替你把照片搬进新文件夹 —— 只给建议清单,搬不搬、往哪搬你说了算
- ❌ 不修图、不美化、不做人脸识别分类
- ❌ 绝不删除任何文件 —— 连重复副本也只是移到暂存区,随时能一键还原
> 🚨 一眼看懂的安全红线(看这 3 条就够了)
> 1. 绝不重命名、绝不强制搬动你原有的文件夹和文件名——那是照片所有者的手迹、记忆地图。
> 2. 只动"真正的冗余":字节完全相同的副本,移到可还原暂存区,绝不删除。
> 3. 任何去重/移动前必走:只读盘点 → 报告 → 你确认 → 可还原暂存。看不懂的提示见 FAQ。
📱 常见场景,照着说就行
不用记命令,把下面任意一句发给我(换成你的真实路径):
| 你想做的事 | 直接说 | 我会怎么走 |
|---|---|---|
| 手机和电脑照片重复了 | 「我手机备份在 D:\手机备份、电脑照片在 E:\照片,看看两边重复的」 |
分别只读盘点两处 → 跨源比对找出完全相同的副本 → 报告两边重叠清单 → 你确认后只暂存重复副本,绝不互删源文件 |
| 清掉重复副本 | 「D:\照片 里有好多重复的,帮我清掉」 |
盘点 → 查重报告 → 你确认 → 副本进可还原暂存 |
| 散落老照片归类 | 「E:\照片 根目录一堆 IMG_ 没归类,给点建议」 |
盘点未归类 → 按年份/相邻主题/来源生成建议清单(不搬) |
| 先看看库多大 | 「帮我看看 D:\照片 到底多少张、多大」 |
只读盘点出报告,一个文件都不动 |
| 反悔还原 | 「刚才清的还原回去」 | restore --dry-run 预览 → 你确认 → 原路搬回 |
> 上面每个场景的命令都是真实可跑的,详见下方「复杂场景最佳实践」与 references/workflow.md。
为什么选它:
- 🛡️ 只整理不删除:照片所有者的手迹(文件夹结构+文件名)原样保留,重复副本安全暂存、可还原
- 🧠 记忆层:跨会话记住你的照片库位置与分类偏好,越用越懂你的归档习惯
- ✅ 真实踩坑案例:2026-09-01 修复"平行文件夹部分重叠导致独有内容被孤儿化"事故(verify_restore 完整性校验)
- 💬 报错说人话:出错不再丢一句技术码,而是"原因 + 怎么办";进度实时可见,中断可续跑
个人照片库守护
用途
帮你看护已有的照片库,而不是改造它。尊重照片所有者的原始文件夹结构和文件名(那是多年时间沉淀的手迹、记忆地图),只做两件事:
- 查重去重:找出字节完全相同的重复副本,安全暂存(可还原、绝不删除),释放被冗余占用的空间。
- 辅助归类:识别散落、未归类的照片,给出归类建议(建议归入哪个现有文件夹 / 建议新建哪个主题文件夹),由你拍板,绝不自动替你搬走。
触发场景
- "我的照片里有好多重复的,帮我清掉副本"
- "这些照片太乱,帮我看看哪些没归类好"
- 照片库含大量历史文件夹(
全家福、19-07拉萨、妈妈手机照、手机备份260729、禾禾1405……),文件名是相机默认命名(IMG_xxxx、微信图片_xxx、mmexport_xxx) - 任何涉及个人照片目录的"清理重复 / 整理归类",不涉及把文件夹改造成统一骨架
核心理念:守护者,不是改造者
这是本技能与前版(v1.x「三层结构改造」)根本不同的地方。
> 照片原有的文件夹和命名,是照片所有者的手迹。
> 它们是时间沉淀下来的记忆地图——禾禾1405 是孩子的某个阶段,19-07拉萨 是一次旅行,妈妈手机照 是一位老人的视角。这些名字不该被机器重写成 2022-05/照片。重命名、强制平铺、搬到统一骨架,是对记忆的抹平,是"没有温度"的整理。
所以本技能立三条铁律:
- 绝不重命名任何照片文件。原始文件名是溯源线索,丢了就找不回来。
- 绝不强制搬动原有文件夹。它们留在原地,保持主人原来的秩序。
- 只动"真正的冗余"(字节完全相同的副本)和"给建议"(未归类照片怎么归)。
为什么去重是安全的(也是值得做的)
重复副本不是手迹——它是同一张照片的另一个拷贝,删掉一份、留下一份,记忆不丢,只是释放了冗余空间。这是"有温度的整理"里唯一该主动清理的部分,但仍走可还原暂存,不真删。
为什么归类只给建议、不自动执行
"这张照该归到 全家福 还是 旅行"是主人的判断,不是机器的。机器可能猜错、可能违背主人的记忆逻辑。所以本技能生成建议清单,由主人逐条拍板、手动移动(或授权本技能代劳)。归类是"协助",不是"接管"。
工作流(5 步,严格执行顺序)
- 说不清照片在哪?先定位:运行
scripts/locate_library.py(或--root "D:\"指定起点),自动扫常见位置(图片库 / 桌面 / 文档 / 下载 / OneDrive 图片 / 各盘常见照片目录 / DCIM),按照片数排出候选库;记忆层里上次用过的库会排在最前。选中路径后再进第 1 步。找不到时它会告诉你下一步该确认什么,而不是干巴巴报错。 - 只读盘点:运行
scripts/scan_photo_library.py <根> --out tree.json。导出保留原结构的目录树、各文件夹照片统计、照片类型/时间跨度、重复文件组、未归类照片清单。不修改任何文件。 - 查重报告:基于盘点结果,列出重复组(每组保留一份、标出其余副本),标注副本占用空间、建议保留哪一份(已归类优先,其次最早创建 / 体积最大 / 层级最深)。不删。
- 去重暂存(需用户确认):用户确认后,运行
scripts/dedup_photo_library.py <根> --apply,将副本移到可还原暂存区<根>_重复暂存隔离/(同盘、保留原相对路径、写dedup_log.csv日志)。绝不删除——清空暂存需用户二次确认。
> 想反悔?一句话还原:python scripts/restore_photo_library.py --root <根>(先加--dry-run预览)。副本原路搬回,文件名和文件夹恢复原样。 - 辅助归类建议:运行
scripts/classify_suggest.py <根> [--out 归类建议.md],对未归类照片,依据 EXIF 拍摄时间、相邻主题文件夹命名、文件名线索,生成归类建议清单(如"根目录散落的 23 张 2024 家庭照 → 建议并入全家福";"无匹配主题 → 建议新建2024-春节")。主人自行决定接受哪条。
> 建议能拍板才算建议:候选最多列 3 个并标注置信度(年份精确匹配=高 / 唯一相邻主题=中 / 多候选=低),绝不"把全部文件夹列一遍让你自己猜";没有拍摄时间线索的照片(截图 / 微信导出)单独列进「待确认」组,不硬归、不替你猜,--apply也会主动跳过它们。 - 验证汇报 + 复盘沉淀:核对重复暂存区文件数、归类建议条数、原结构零改动;汇报并提示备份。跑完主动问用户三件事,把纠正写进记忆层。
你会看到什么(输入输出示例)
① 只读盘点 scan_photo_library.py <根> --out tree.json(节选):
{
"summary": {
"top_dirs": 42, "thematic_dirs": 40, "generic_dirs": 2,
"total_media_gb": 58.3,
"dup_groups": 3, "dup_copies": 8, "dup_gb_saved": 1.42,
"unclassified_total": 23
},
"dup_groups_sample": [
{"size": 3180000, "copies": ["D:/照片/A/IMG_001.jpg", "D:/照片/B/IMG_001.jpg"]}
],
"unclassified_sample": [
{"path": "D:/照片/IMG_0099.jpg", "reason": "根目录散落", "year": "2024"}
]
}
② 查重报告 dedup_photo_library.py <根> --plan(节选):
重复组数: 3
[1] 单份 3180000 字节, 共 2 份, 可释放 3180000 字节
保留: D:/照片/A/IMG_001.jpg
D:/照片/A/IMG_001.jpg <-- 保留
D:/照片/B/IMG_001.jpg (副本 → 暂存)
合计可释放: 12600000 字节 (0.01 GB)
③ 归类建议 classify_suggest.py <根> --out 归类建议.md(节选):
# 照片归类建议(守护者版,未移动任何文件)
未归类照片共 23 张,建议归入下方 3 个目标。
> 本清单仅供参考,由你拍板是否移动。
## → 全家福(15 张)
- `D:/照片/IMG_0099.jpg` · 年份线索 2024 → 候选: 全家福 _[高(基于相邻主题/年份匹配)]_
三类歧义点(出方案时务必向用户摆清)
- 重复组保留哪一份:已归类优先——组内只要存在处于主题文件夹内的成员,就只在"已归类"里挑,绝不把已归类的搬走、留下根目录散落的(守护者铁律);组内全散落或全已归类时,再按策略选(默认
earliest最早创建,可选largest体积最大 /native层级最深)。用户可另行指定保留在特定文件夹的那份。 - 去重暂存保留 / 清空 / 改名:默认保留可还原,清空需二次确认。
- 未归类照片是否接受建议:本技能只出清单,移动动作默认由用户手动做;用户也可授权
--apply代劳(仍逐条可还原)。
🧭 复杂场景最佳实践
下面每种情况照着做,少踩坑(命令都是真实可跑的,举例路径换成你的):
- 手机 + 电脑双源查重(最常被问):两处都先
scan_photo_library.py盘点,把两份tree.json交给比对;或把两处都挂到同一根目录的二级文件夹下统一扫。只暂存真正的字节级重复副本,绝不互删源文件——手机一份、电脑一份都保留,腾出的是"完全一样的多余拷贝"。
bash python scripts/scan_photo_library.py "E:\照片总库\手机备份" --out phone.json python scripts/scan_photo_library.py "E:\照片总库\电脑照片" --out pc.json - 微信 / QQ 导出乱序:导出文件名是
微信图片_xxx、mmexport_xxx,本身没时间。靠 EXIFDateTimeOriginal还原拍摄时间归类;没有 EXIF 的(截图、表情包)会单独标出,别硬归。 - iCloud / 云盘混合:先把云端照片下载到本地文件夹再扫本地副本。别直接扫云端挂载盘(权限与缓存会让盘点失真)。
- 超大库分批:先
scan出tree.json看清结构,再按文件夹分段dedup --apply,每批写独立dedup_log.csv,出问题只回滚一批。
bash python scripts/dedup_photo_library.py "E:\照片\2019" --plan # 先看方案 python scripts/dedup_photo_library.py "E:\照片\2019" --apply # 确认后执行 - 中断续跑:
dedup幂等——重跑会跳过已处理的,结果一致;classify --apply中断后重跑不会重复移动已搬的。 - 还原 / 暂存被误删:
restore_photo_library.py --dry-run先预览;原位已有同名文件绝不覆盖,跳过去并明确告知。日志dedup_log.csv可逐条回放。
bash python scripts/restore_photo_library.py --root "E:\照片" --dry-run
> 更细的步骤与实例见 references/workflow.md;报错看不懂见 references/faq.md。
安全铁律
- 个人照片目录属高风险。任何去重/移动前必须:只读盘点 → 报告 → 明确确认 → 可还原暂存。
- 原始文件夹结构 + 文件名 = 手迹,默认只读。绝不重命名、绝不强制搬动原有文件夹。
- 去重只对"真正相同"的文件:字节/内容哈希确认完全相同才判重;RAW+预览(同名
.CR3/.JPG)、相似但不同的照片(pHash 接近但非相同)一律不判重。 - 去重副本走可还原暂存,不删;清空回收站 / 暂存前需用户二次确认。
- 移动用同盘 move(原子、可还原),不用跨盘复制+删。每批操作写 CSV 日志(src,dst,status),万一需还原照日志回放。
- 不在 Desktop/Downloads/Documents 等个人目录递归删。
> 常见问题与反模式见 references/faq.md;错误码含义见 references/error_codes.md。
记忆机制(跨会话持续增强)
本技能带轻量记忆层 scripts/state_helper.py(纯标准库,零依赖),让每一次运行都站在上一次的肩膀上。
state/ 目录(运行时自动生成,不进发布包)
history.jsonl:每次运行一行摘要(时间 / 动作 / 根目录 / 成功与否 / 结果统计)learned_rules.md:从用户纠正里沉淀的规则(自动去重、带日期与来源)user_prefs.json:偏好键值对(默认暂存隔离目录、重复保留策略、是否授权自动归类等)
三条铁律
- 启动先读:整理动作开始前,先跑
python scripts/state_helper.py history和rules,把已沉淀规则纳入本次方案;user_prefs.json里已有的偏好直接采用,不要重复询问用户。 - 结束自动写(脚本已接线,不靠 AI 记得):
scan_photo_library/dedup_photo_library/classify_suggest/restore_photo_library/locate_library五个脚本内部都已接记忆层,每次运行自动往history.jsonl追加一行(动作 / 根目录 / 成功与否 / 结果统计)。AI 不必手动补写,也不会漏写——记忆层故障一律静默降级,绝不中断整理。 - 纠正即沉淀:用户指出任何"这次不对"的地方,立刻
add-rule写入,下次自动生效。
怎么确认记忆层真的在工作
python scripts/state_helper.py history # 跑过几次就应有几行;显示「(暂无运行历史)」说明没写进去
history.jsonl 里应能看到 scan / dedup-plan / dedup-apply / classify / restore 五种动作。
常用命令
python scripts/state_helper.py history # 最近 20 次运行
python scripts/state_helper.py rules # 已沉淀规则
python scripts/state_helper.py add-rule "全家福 类文件夹按最早照片年份归位"
python scripts/state_helper.py get staging_root # 读偏好
python scripts/state_helper.py set staging_root "E:\_dup_staging"
沉淀什么(举例)
- "某不规则文件夹其实有明确时间线索,应归到 YYYY 而非 Inbox"
- "用户的视频文件分散在
手机备份/,统一按 mtime 归月" - "
.CR3与同名.JPG是 RAW+预览的正常组合,不算重复"
归类建议"聪明"在哪、边界在哪
- 四类信号交叉判断:① EXIF 拍摄时间 ② 相邻主题文件夹命名 ③ 文件名年份线索 ④ 来源前缀(
微信图片_/mmexport_/IMG_/DSC_/Screenshot/VID_)。 - 每条建议带"置信度 + 依据摘要":不只是"归入
全家福",还告诉你凭什么这么建议(如"年份 2024 + 相邻主题全家福+ 来源 微信导出"),你一眼能判断靠不靠谱、要不要采纳。 - 边界(诚实说):本技能是"守护者",不做人脸识别、不读画面内容、不猜语义主题。它负责把"明显该归到一起"的找出来、把依据摆清楚,最终归类拍板在你——这也是为什么它不乱动你的文件夹。若想要 AI 识图级分类,可把建议清单导出后交给识图工具二次处理。
照片分类规则
- 重复(dup):内容哈希(SHA-256)完全相同的多份文件,仅保留一份。
- 未归类(unclassified):散落在照片库根目录的孤立照片;或所在文件夹名无主题线索(如
新建文件夹、照片、DCIM、Camera、未命名)且文件名是相机默认命名的照片;或文件名含IMG_/DSC_/微信图片_/mmexport_/Screenshot等通用前缀、无法对应到任何现有主题文件夹。 - 已归类(curated):处于用户有明确主题命名的文件夹内(如
全家福、19-07拉萨、妈妈手机照),即便文件名是相机默认命名,也视为已有归属,不重复建议。
脚本
scripts/locate_library.py [--root 起点] [--max-depth N] [--out 候选库.md]:照片库定位助手——用户说不清照片在哪时先用它,扫常见位置排出候选库(记忆层里上次用过的排最前)。只读,不动任何文件。scripts/photo_common.py:公共工具(扩展名类型映射、EXIF 拍摄时间提取、内容哈希、目录类型推断、归类归属推断、友好错误 fail/warn + ERROR_DICT),被下面脚本共用。scripts/scan_photo_library.py <根> [--out tree.json] [--timeout SECONDS]:只读盘点,输出保留原结构的目录树 + 分类统计 + 重复组 + 未归类清单。不修改任何文件。scripts/dedup_photo_library.py <根> [--plan|--apply] [--timeout SECONDS] [--staging-out DIR] [--keep {earliest|largest|native}]:查重 → 报告(plan)→ 可还原暂存(apply,带实时进度、中断可续跑)。副本移到<根>_重复暂存隔离/,写dedup_log.csv。scripts/classify_suggest.py <根> [--out 归类建议.md]:对未归类照片生成归类建议清单(基于时间/相邻主题/文件名线索),不移动任何文件。scripts/restore_photo_library.py --root <根> [--dry-run]:一键还原——按dedup_log.csv把暂存的重复副本原路搬回原位,文件名与所在文件夹完全恢复原样。原位已有同名文件时绝不覆盖,跳过去并明确告知。建议先--dry-run预览再真搬。
> EXIF 时间格式坑(已修复):DateTimeOriginal 是 2015:08:09 12:00:00(冒号分隔),解析必须用 %Y:%m:%d,用 %Y-%m-%d 会 ValueError 静默返回 None。photo_common.photo_ym 已修复并测试通过。
参考
references/design.md:守护者设计取舍、为何保留原结构、去重与归类的边界。references/workflow.md:完整 5 步流程与实例。references/faq.md:常见问题与反模式(报错看不懂、怎么还原、中断怎么办都在这儿)。references/error_codes.md:错误码字典(E 字头 → 人话原因 → 你该怎么做)。
可靠性保证(TRACE-R 自检达标项)
- 超时控制:盘点与查重默认限时 1800s(可用
--timeout调),超时主动中止并返回错误码 E0602,绝不无限悬挂;中止不会留下半成品——查重已暂存的副本都记在dedup_log.csv,重跑即可续跑,不会重复搬。 - 失败可见:长任务每 500 个文件报一次进度;记忆层写不进去会明确告警 E0601(不影响整理结果),不再静默吞掉——你随时知道它在干什么、哪一步没成。
- 续跑/幂等:关键操作支持断点续跑与幂等重入,异常中断后重跑结果一致。
- 日志落盘:所有脚本支持日志落盘,非交互环境 stdout 不可靠,以日志文件为准。
- 资源预算:常规运行内存 < 500MB、磁盘增量 < 100MB、单核占用 < 80%,超出即熔断。
- 一致结果:相同输入必产生一致输出;外部依赖(在线查询 / 识别服务)结果冻结缓存,判定逻辑固定不漂移。
- 边界输入:输入校验失败(如缺失路径、空/超长关键词、非预期文件类型)显式抛错并带 E 字头错误码,不做静默降级;具体边界清单见各脚本与
references/error_codes.md。 - 常见错误:错误码字典见
references/error_codes.md(E 字头 + 可读消息 + 修复建议);所有错误都以「❌/⚠️ 出错了(错误码 Exxxx)→ 原因 → 怎么办」的中文结构化提示打印到屏幕,普通人也能看懂接下来做什么,技术细节仅在最后一行给出。高频问题见references/faq.md。
边界输入处理(rejection mode)
- 空输入:调用必填参数缺失时立即
fail("E0201")退出,绝不静默用空值继续。 - 超长输入:路径 > 260 字符 / 关键词 > 200 字符 → E0302 提示精简或自动走
\\?\前缀长路径 API。 - 特殊字符:含
< > : " / \ | ? *等系统保留字符或 shell 元字符的文件名 → 过滤或转义后重试,不静默吞错。 - 二进制:非预期二进制输入(脚本误用、扫描到 .exe / .zip 等)→ 类型识别拒绝 E0301,绝不当文本读乱码。
- Unicode 极值:中文 / Emoji / CJK 扩展 B(如
𠮷)等 → 脚本以 UTF-8 with BOM 保存,原样保留不乱码;实测 long-path 与零宽字符不被吞。
具体边界清单见 tests/boundary/cases.json(专属用例,每条带 verify 字段可实测),replay.py 真跑校验。