家里那台PS5买回来半年后我第一个受不了的不是游戏容量而是截图和录像的管理。官方相册只能按时间倒序看想搜某一场Boss战、某个游戏的地标截图只能一页页翻。更头疼的是我习惯把素材备份到家里的NAS导出来以后文件名全是IMG_0001这种根本分不清来源。后来实在忍不了就花了一个月断断续续写了个小工具取名AnyPS5。意思很直白不管手边是PS5、PS5 Slim还是Pro只要在同一局域网里都能用同一套逻辑把主机上的截图、录像、游戏记录自动整理成网页画廊。AnyPS5不是一个模拟器也不碰系统内部数据更不涉及任何在线账号的敏感信息。它做的事情很简单扫描你指定的媒体目录比如从PS5拷出来或者NAS里挂载的文件夹识别每个文件属于哪个游戏去除重复截图然后生成一个可以手机、电脑随时看的网页。如果后面你还想找素材打开浏览器搜索一下关键词就行。适合谁看如果你手上有一台PS5又喜欢截图、录像或者家里有NAS想搞一个游戏媒体库这篇文章适合你如果你是刚开始接触FastAPI或者图片识别的新手也可以照着我的思路搭一个自己用的工具。接下来我会从需求梳理讲到技术选型再到实际代码和踩坑记录尽量把每个“为什么这么做”都说清楚。1. 项目动机与整体思路1.1 为什么不做串流而是做素材管家很多人一听PS5相关工具第一反应是远程串流。串流确实是刚需但官方客户端已经做得不错而且我不是搞协议分析的料更不想去碰主机系统权限那套灰色地带。我真正恼火的场景很日常打完一个章节想回看一下自己拍的截图主机相册里只有时间排序没有搜索没有标签跨设备浏览更是想都别想素材导出到电脑后文件名全是IMG_20250503_213456这种和游戏名毫无关系玩家经常连拍好几张差不多的图一次Boss战能拍出几十张高度重复的照片想按游戏建文件夹但时间久了根本记不清某张模糊截图是哪个游戏里的。所以我把AnyPS5的定位收窄成一个“游戏素材管家”专注管理PS5产生的截图、短视频和游玩记录不做串流、不做系统监控、不做在线账号同步。边界清晰之后很多技术选型就变得简单了。1.2 核心功能清单和产品边界经过半个多月的使用和迭代最终保留的功能如下自动扫描指定目录兼容jpg、png、webp、gif、mp4、mov、webm等常见媒体格式用感知哈希对图片去重保留清晰度更高的一张重复项自动归档用OCR识别截图里的游戏名结合本地游戏别名表把素材自动归入对应游戏生成一个网页画廊支持按游戏、时间、文件类型、是否重复筛选支持批量下载、批量打标签、批量删除所有操作都走本地接口做一个简单的统计面板展示素材总量、存储占用、各游戏占比。我不做登录鉴权因为是在家庭局域网内使用的不做云端同步因为NAS本来就承担了同步职责不做PSN账号登录因为我只需要读取本地文件不想碰任何账号体系。这个取舍让整个项目的安全体积小很多。1.3 技术选型背后的取舍逻辑技术栈最终定为Python FastAPI SQLite Vue 3加ffmpeg和PaddleOCR作为辅助工具。选Python的原因很简单文件处理和图片识别生态太成熟了Pillow、imagehash、watchdog、ffmpeg-python这些库都能省掉大量底层工作。FastAPI是因为它自带异步支持和OpenAPI文档前端联调非常方便不需要额外写接口文档。前端用Vue 3 Vite纯粹是为了组件化开发舒服画廊页面的筛选栏、卡片网格、懒加载实现起来都很顺手。数据库用SQLite因为素材量在几万个文件以内时SQLite完全够用而且零配置文件备份也方便。如果用PostgreSQL反而增加了部署复杂度。ffmpeg是用来处理视频缩略图的PaddleOCR是用来识别截图文字的。这两个工具单独拎出来说是因为它们在整个项目里承担的是“脏活累活”后面实战部分我会重点讲。2. 核心模块拆解文件发现、去重和识别2.1 文件发现定时扫描加目录监听AnyPS5面对的第一个问题是怎么知道新素材进来了我试过纯定时扫描缺点是响应慢而且全量扫描很耗CPU我也试过纯监听缺点是程序第一次启动时看不到历史文件。最后采用了“初始全量扫描 watchdog监听”的双轨方案。初始化时服务端会对配置的媒体根目录做一次深度扫描把已有文件的路径、大小、修改时间记录到数据库。之后交给watchdog监听目录事件新增文件会立刻进入处理队列。为了避免重复处理我在数据库里用“文件路径文件大小修改时间”组成指纹字段同一个文件不管触发多少次事件都只处理一次。这里有个容易踩的坑PS5通过U盘或局域网拷贝文件时可能是先写一个临时文件再重命名成正式文件。如果监听rename事件不加以判断很容易把半截文件当成完整文件处理。我的做法是收到事件后不立刻处理而是等5秒再检查文件大小是否稳定稳定了才入队。核心扫描代码类似这样from watchdog.observers import Observer from watchdog.events import FileSystemEventHandler class MediaHandler(FileSystemEventHandler): def on_created(self, event): if event.is_directory: return self.schedule(event.src_path) def on_modified(self, event): if event.is_directory: return self.schedule(event.src_path) def schedule(self, path): # 延迟5秒等待文件写入完成 self.pending[path] time.time() def flush_pending(self): # 定时器里调用比对文件大小是否稳定 for path, t in list(self.pending.items()): if time.time() - t 5: size os.path.getsize(path) if size self.last_size.get(path): process_queue.add(path) self.last_size[path] size这个延迟逻辑也适用于NAS挂载目录毕竟网络文件系统的事件触发比本地更不稳定。2.2 感知哈希去重用指纹代替肉眼去重是我第一个实现的功能因为连拍带来的重复图片实在太多了。去重方案我对比过MD5、感知哈希和神经网络特征最后选了感知哈希。MD5只对一模一样的文件有效玩家截图只要缩放、压缩过就认不出来。神经网络特征准确但重跑在一个常驻后台服务里太浪费。感知哈希的优点是速度快、对轻微缩放和压缩不敏感适合截图这种“内容相同但编码不同”的情况。我用的是pHash具体做法是先把图片缩放成32x32转灰度做离散余弦变换取左上角8x8低频区域计算它的均值然后逐位比较得到64位哈希值。from PIL import Image import imagehash def dhash(image_path, hash_size8): img Image.open(image_path).convert(L) return imagehash.phash(img, hash_size) def hamming_distance(h1, h2): return h1 - h2 # imagehash实现了汉明距离去重判断规则是这样的两张图的汉明距离小于等于5就认为是重复如果一组图里有不同分辨率保留分辨率最高的那张其余标记为“重复”可一键清理。实际测试下来这个阈值在截图场景下表现相当不错。默认阈值5既能识别“同一场景连拍”又不会把两张Boss战不同阶段的图误判成重复。如果素材全是UI界面、页面背景比较单调阈值可以降到3避免误杀。2.3 游戏信息识别PaddleOCR加人工校正去重之后的素材还需要知道它是哪个游戏的。第一版我打算靠文件名识别但PS5导出的文件名只有时间戳行不通。后来发现截图里通常带着游戏UI比如左上角的任务名、游戏内菜单标题这些文字信息非常适合用OCR提取。我选了PaddleOCR的中文模型识别准确率比传统OCR高不少但直接扔原图进去效果一般。主要问题是截图里UI文字太小、有半透明背景、还有抗锯齿。我的预处理流程是先放大两倍转灰度再用自适应阈值去背景最后才交给PaddleOCR。import cv2 from paddleocr import PaddleOCR ocr PaddleOCR(use_angle_clsTrue, langch) def extract_game_name(image_path): img cv2.imread(image_path) img cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) img cv2.resize(img, None, fx2, fy2, interpolationcv2.INTER_CUBIC) img cv2.adaptiveThreshold(img, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2) result ocr.ocr(img, clsTrue) # 拼接所有识别到的文字和游戏别名表做匹配 text .join(line[1][0] for line in result[0]) return match_game_by_text(text)识别出来的文本未必是游戏名比如“设置”“地图”这种通用词汇会出现。我的办法是维护一个游戏别名表把常见游戏的中英文名、缩写、以及本地化译名都放进去。OCR文本先和别名表做模糊匹配命中就返回游戏名没命中的进入待人工校正队列。使用一段时间后这个表越来越可靠识别率从最初的58%能提升到90%以上。2.4 视频为什么不走同一套流程视频文件没法像图片一样直接OCR需要先从视频里抽帧。最开始我抽第一帧结果发现很多视频第一帧是黑屏或者加载画面导致识别结果一团糟。后来改成用ffmpeg在视频时间轴上均匀抽三帧分别OCR取识别置信度最高的结果。如果三帧都识别不到内容就用中间帧生成一张缩略图放到“待分类”文件夹里不强行猜测。ffmpeg -i input.mp4 -ss 00:00:02 -frames:v 1 frame_1.jpg ffmpeg -i input.mp4 -ss 00:01:30 -frames:v 1 frame_2.jpg ffmpeg -i input.mp4 -ss 00:03:00 -frames:v 1 frame_3.jpg抽帧和OCR都是耗时操作我放在一个独立的处理进程里用队列控制并发数为4。并发太高的话CPU会被占满主机上同时玩游戏会卡顿。3. 实操过程从空文件夹到网页画廊3.1 先搭一个最小的服务端骨架AnyPS5的后端目录结构很简单没有搞微服务那套毕竟就是家庭自用工具anyps5/ ├── app/ │ ├── main.py # FastAPI入口 │ ├── scanner.py # 目录扫描和文件监听 │ ├── hash_utils.py # 感知哈希去重 │ ├── ocr_engine.py # OCR识别 │ ├── models.py # SQLAlchemy模型 │ └── api/ │ ├── media.py # 素材列表接口 │ └── tasks.py # 批量操作接口 ├── frontend/ │ └── src/ # Vue3源码 └── data/ └── anyps5.db # SQLite数据库FastAPI入口文件只做三件事挂载静态文件、注册子路由、启动后台扫描任务。from fastapi import FastAPI from fastapi.staticfiles import StaticFiles from .api import media, tasks from .scanner import start_scan_once app FastAPI(titleAnyPS5) app.include_router(media.router, prefix/api/media, tags[media]) app.include_router(tasks.router, prefix/api/tasks, tags[tasks]) app.mount(/static, StaticFiles(directory../frontend/dist), namestatic) app.on_event(startup) async def startup_event(): start_scan_once()3.2 扫描入库流程把文件变成记录扫描模块是把目录下的文件信息写入数据库。表结构关键字段有原始路径、文件名、文件类型、大小、修改时间、感知哈希、OCR原始文本、游戏ID、去重状态。入库逻辑要注意一个细节不要把感知哈希计算放在扫描主流程里。因为几百个文件同时计算哈希会让接口长时间无响应。我让扫描器先把文件元数据入库状态设置为“待处理”后台工作线程再从数据库里捞“待处理”记录逐个计算哈希和识别游戏名。INSERT INTO media (path, name, type, size, mtime, status) VALUES (?, ?, ?, ?, ?, pending);这样前端画廊可以立刻看到文件变多识别结果则是一点一点回填的体验上更自然。3.3 OCR识别与元数据回填让文件有名字处理线程从数据库取出一条待处理记录先判断文件类型。图片走pHash OCR视频走ffmpeg抽帧再OCR。识别到的游戏名通过别名表映射成统一名称同时把文本结果单独存一列方便人工搜索。为了提升速度识别结果做了缓存。同一个文件如果OCR文本相同就直接沿用之前的游戏归属避免重复跑模型。缓存键是“文件大小修改时间OCR文本前50字”实测能减少大约35%的重复识别请求。回填时的判断优先级是这样的OCR命中游戏别名表 目录名包含游戏关键字 待人工分类。因为PS5素材导出时用户可能已经手动建了以游戏命名的文件夹这种目录信息比OCR更可靠。3.4 前端画廊筛选、搜索和无限滚动前端核心是一个单页画廊打开就是瀑布流卡片每张卡片显示缩略图、游戏名、拍摄时间和文件类型。顶部是筛选栏支持选择游戏、时间范围、图片/视频/全部、只看重复项。接口设计成支持游标分页避免大数据量下普通页数跳跃导致重复。GET /api/media?game_id12typeimagecursorxxxlimit50返回结果里带一个next_cursor前端滚动到页面底部时自动请求下一页。这样即使素材量达到几万张滚动加载也不会有明显卡顿。卡片点击后进入预览大图模式支持键盘左右切换。预览时显示源文件路径、分辨率、是否重复。重复项会提供“查看相似图组”把同一个哈希组的图片全部列出来方便手动决定留哪张。3.5 批量下载zip打包的后台任务批量下载看起来简单但直接同步打包几个GB视频很危险很容易超时。我改成异步任务方案前端提交一批文件ID后端创建一条任务记录后台用zipfile把文件流式写入临时文件执行完成后通过轮询接口通知前端。import zipfile def build_zip(file_paths, output_path): with zipfile.ZipFile(output_path, w, zipfile.ZIP_STORED) as zf: for fp in file_paths: # 用项目内相对路径作为压缩包内文件路径 arcname fPS5素材/{os.path.basename(fp)} zf.write(fp, arcname)这里使用ZIP_STORED而不是ZIP_DEFLATED因为视频和图片本身已经是压缩格式再压缩只会浪费CPU速度还慢。如果是文本文件或者记录文件才建议用DEFLATED。4. 常见问题与排查实录4.1 OCR识别率上不去怎么办这是被问得最多的问题。我实验下来OCR识别率低通常不是模型问题而是预处理没做到位。PS5截图里的文字经常叠加在复杂背景上比如角色身后的天空、战斗特效直接把原图丢给OCR结果会有大量乱码。解决办法是先把目标区域放大两倍再灰度化同时用自适应阈值过滤背景。另外要在别名表里放足够多的“同义词”比如“战神的黄昏”“战神5”“God of War Ragnarok”要映射到同一个规范名称。我的映射表一开始只有60个词条后来加到300多识别率才稳定下来。如果截图里的文字确实很少比如风景照、人物特写没有任何UI元素不要强行分类老老实实放到“未分类”列表里靠人工轻轻点一下鼠标就行。4.2 视频缩略图生成太慢视频文件比图片难处理是因为ffmpeg解码整个文件很耗时。我最初的做法是每个视频抽三帧并行处理结果资源消耗很大。后来发现大部分视频只需要首尾和中段各抽一帧就够了而且用ffmpeg的thumbnail滤镜能自动选出有代表性的帧比固定时间戳更聪明。ffmpeg -i input.mp4 -vf thumbnail100,scale320:-1 -frames:v 1 thumb.jpg这个滤镜会在100帧里选一张质量最高的通常不会选中黑屏或者纯字幕帧识别率比固定抽帧还要高。另外一定要注意限制并发。ffmpeg抽帧非常吃CPU家里主机如果正在跑游戏并发数过高会导致画面卡顿。我的经验是把处理队列的最大并发控制在CPU核心数的一半比如8核机器就开4个worker。4.3 去重阈值怎么调才合适感知哈希阈值不是越大越好。阈值设成10时同一游戏的不同截图经常被误判成重复大量正常素材被归档。阈值设成0时重命名过的文件、重新压缩过的图片又没法去重。我建议先用3到5之间的值跑一批样本统计一下重复组数量和人工抽查结果。如果重复组里出现了明显不相干的图片就调小阈值如果同样场景的连拍图没被识别出来就调大一点。对PS5截图这种来源比较固定的素材5已经足够稳定。还有一个细节视频缩略图和原图之间不要做跨类型去重。一张是jpg截图、一张是mp4抽出来的缩略图内容可能很像但一个是源文件一个是派生文件混在一起会让你误删源素材。4.4 NAS/SMB挂载权限和文件名乱码我把媒体目录放在NAS上通过SMB挂载到Linux服务器。一开始遇到两个问题文件权限不足导致扫描器读不了某些目录中文文件名在数据库里变成乱码。权限问题好解决挂载时指定正确的uid/gid并给目录设置755权限。乱码问题要注意SMB挂载参数要加上iocharsetutf8确保内核把文件名转成UTF-8。mount -t cifs //nas/ps5_media /mnt/ps5 -o usernamexxx,passwordxxx,uid1000,gid1000,iocharsetutf8,noatime另外SQLite对并发写支持比较弱多个worker同时写数据库容易报“database is locked”。解决方法是连接字符串里加上?modero给只读接口写操作集中在单线程队列或者开启WAL模式sqlite3.connect(anyps5.db, timeout20) # 执行一次 PRAGMA journal_modeWAL;4.5 数据库结构升级的迁移方案项目迭代过程中肯定会加字段。我的做法是用Alembic做迁移脚本虽然对自用小工具来说有点重但总比改坏了几万条数据重来要好。每次升级前先备份SQLite文件。备份时不要直接copy文件要先用sqlite3 .backup命令否则可能在写入过程中得到不一致的副本。迁移脚本尽量小步走一次只加一个字段方便回滚。如果只是想加一个筛选标签不需要改表结构的话也可以直接新建一张关联表保存“文件ID标签名”完全避开ALTER TABLE带来的兼容问题。现在AnyPS5已经在我那台NAS上连续跑了两周积压了快半年的截图和录像被整理归位找素材的效率比之前高了一个量级。我自己最大的感受工具不怕小怕的是不做。最后分享一个实用设置如果你也用SMB挂载建议打开file_mode0755,dir_mode0755不然每次处理完后的文件属性会乱影响后续要不要重新识别。后续我还在考虑把游戏奖杯时间线和视频高光片段自动拼接加进来不过那要重新梳理任务队列等这个版本再跑稳一点再说。