feat: 色情识别模型本地化(lib/320n.onnx),缺失时自动下载并回退自带模型

This commit is contained in:
2025-06-11 21:15:24 +08:00
parent d4f9dc362e
commit 01a268663f
5 changed files with 85 additions and 6 deletions
+70 -1
View File
@@ -4,6 +4,12 @@
只依赖 Python 标准库 + nudenet 包,模型在进程里只加载一次。
收到 /detect 就把图片喂给 NudeDetector,返回结果 JSON。
模型加载优先级(模型放在 <程序运行目录>/lib/320n.onnx):
1. 本地已有该文件 → 直接加载(离线可用,适合随程序分发);
2. 本地没有 → 自动下载到 lib/320n.onnx 再加载(仅第一次需要网络);
3. 下载失败 → 回退用 nudenet 包自带的模型。
下载地址是 NudeNet 官方 release 的 320n.onnx(约 11.6MB)。
Windows 超长路径说明:OpenCV 的 imread 走老式 C API,路径超过 260 字符就
打不开(报 can't open/read file)。所以这里改用 Python 的 open() 读字节,
再把字节直接交给 NudeNet(v3 的 detect 支持 bytes)——从内存解码,彻底
@@ -13,6 +19,7 @@ import json
import os
import sys
import tempfile
import urllib.request
from http.server import BaseHTTPRequestHandler, HTTPServer
from nudenet import NudeDetector
@@ -33,13 +40,72 @@ SENSITIVE = {
"FEMALE_BREAST_EXPOSED",
}
# 模型文件与官方下载地址
MODEL_FILENAME = "320n.onnx"
MODEL_URL = "https://github.com/notAI-tech/NudeNet/releases/download/v3.4-weights/320n.onnx"
# 模型目录由 Go 侧作为第二个参数传入(程序运行目录下的 lib);没传则回退用包自带模型
MODEL_DIR = sys.argv[2] if len(sys.argv) > 2 else ""
_detector = None
def _looks_like_model(path):
"""粗略校验文件是不是真 ONNX 模型:开头不是 HTML、大小不小于 1MB(320n 模型约 11.6MB)。
防止 GitHub 被墙时把"登录网页/占位页"当成模型存下来,下次加载直接报错。
"""
try:
with open(path, "rb") as f:
head = f.read(4)
if not head or head.startswith(b"<"):
return False
return os.path.getsize(path) > 1_000_000
except OSError:
return False
def _ensure_model(model_dir):
"""返回要加载的模型路径;返回 None 表示用 nudenet 包自带的模型。"""
if not model_dir:
return None
local = os.path.join(model_dir, MODEL_FILENAME)
if os.path.isfile(local) and _looks_like_model(local):
print("=== 使用本地模型: %s" % local, flush=True)
return local
# 本地没有(或文件无效)→ 下载到 lib 目录(仅第一次需要网络)
try:
os.makedirs(model_dir, exist_ok=True)
print("=== 本地未找到有效模型, 正在下载: %s" % local, flush=True)
urllib.request.urlretrieve(MODEL_URL, local)
if _looks_like_model(local):
print("=== 模型下载完成: %s" % local, flush=True)
return local
# 下载回来的是网页/占位文件,不是真模型:删掉免得下次继续踩坑
print("=== 下载的文件无效(可能被墙返回了网页), 删除并回退用包自带模型", flush=True)
try:
os.remove(local)
except OSError:
pass
except Exception as exc: # noqa: BLE001 - 下载失败就回退,别让服务起不来
print("=== 模型下载失败, 回退用包自带模型: %s" % exc, flush=True)
return None
def get_detector():
global _detector
if _detector is None:
_detector = NudeDetector()
mp = _ensure_model(MODEL_DIR)
try:
_detector = NudeDetector(model_path=mp)
except Exception as exc: # noqa: BLE001 - 指定路径加载失败就回退包自带模型,别崩
print("=== 本地模型加载失败(%s), 回退用包自带模型" % exc, flush=True)
if mp:
try:
os.remove(mp)
except OSError:
pass
_detector = NudeDetector()
return _detector
@@ -136,4 +202,7 @@ class Handler(BaseHTTPRequestHandler):
if __name__ == "__main__":
port = int(sys.argv[1]) if len(sys.argv) > 1 else 0
# 启动前先把模型准备好(本地加载 / 下载 / 回退),/health 就绪即代表模型可用,
# 这样 Go 侧 waitReady 等到的就是"模型已就绪"的服务。
get_detector()
HTTPServer(("127.0.0.1", port), Handler).serve_forever()