管理员上传并标注角色图片,为每个角色训练一个二分类小模型("是/不是该角色")。插件自动识别收到的图片和表情,命中时在对应图片后添加上下文提示;同时提供只读工具 recognize_character_image。角色名单在插件配置 characters 中维护(默认预置中V主要角色,可任意增删)。
本插件是 SengokuCola/MaiSelfPlugin(GPL-3.0)的泛化改造版:把"是否包含 Bot 自己"的单角色二分类,泛化为每角色一个二分类模型的通用角色识别。训练、版本管理、依赖覆盖层等工程链路与上游一致;与上游的具体差异见文末「与上游的差异」。
- 插件 ID:
org.civetc.char-recog - 数据目录:由宿主 SDK 授予(
ctx.paths.data_dir,默认data/plugins/org.civetc.char-recog/) - 插件目录名可以自取(宿主按
_manifest.json识别),本文档示例用plugins/char-recog
需要 MaiBot 1.3.6 – 1.3.x、SDK 2.11.0 – 2.13.x(已在宿主 1.3.6/1.3.7、SDK 2.11.0/2.13.0 上验证),以及支持 custom_pages、image_upload 的 WebUI。超出该范围的宿主会拒绝加载;若你已在更新的版本上跑通,可自行放宽 _manifest.json 的 max_version。
- 把插件目录放入
plugins/(例如plugins/char-recog)。 - 完整重启 MaiBot(热重载对含 manifest 的插件不生效)。
- 在 WebUI 插件管理页启用,出现「角色识别」工作区。
- 首次加载由宿主生成
config.toml;角色名单通过characters配置(默认预置中V主要角色)。 - 插件附带通用预训练底座,角色模型需要管理员自行训练(见「训练与模型」)。
用本地 SDK 开发源码调试主程序时:
$env:MAIBOT_PLUGIN_SDK_PATH = (Resolve-Path ../maibot-plugin-sdk).Path
$env:MAIBOT_WEBUI_USE_LOCAL_DASHBOARD = '1'
uv run bot.py(使用本地 WebUI 前先在 dashboard 目录执行 npm run build。已发布版本无需这两步。)
默认使用 MaiBot 的 Python,插件依赖安装在自己的覆盖层 dependencies/packages(位于插件数据目录内)。训练和推理优先加载覆盖层,其余包继承主环境。安装和卸载仅修改覆盖层,不改动 MaiBot 主环境。
自定义环境使用已有的 Python 解释器或环境目录,依赖由管理员管理。页面选择会持久保存,并优先于配置中的 worker_python;该配置留空时使用 MaiBot Python。更换底层 Python 大版本后需重新安装覆盖层。被指定的解释器会以该路径执行插件的训练与推理代码,请只指向可信环境。
GPU 版 PyTorch 默认选择 CUDA,也可选择 CPU;CPU 版使用 CPU;识别推理固定用 CPU。训练或推理运行期间锁定依赖操作,安装期间锁定工作进程启动。
安装先在临时目录完成,再核验导入路径、版本和 CUDA 运算,成功后切换到覆盖层,失败时保留原层。GPU 主包约 2.7GB,torchvision、配套依赖及解压文件需要额外空间。
下载进度、安装阶段和日志保存在插件数据目录。重载插件或重启 MaiBot 后,继续读取存活安装进程的进度,或恢复中断任务。下载支持断点续传;服务器拒绝续传或文件校验失败时重新下载,残留安装目录清理后重新安装。失败任务可手动重试。
停止推理后,通过手动开启或重新加载插件恢复。重载页面仅更新页面文件。
推理进程超时或崩溃不再永久关闭自动识别:首先熔断 30 秒,之后由后台自动重启;连续失败按 30/60/120/300 秒退避。第一条消息的冷启动(import torch)由后台预热线程完成,不占用下文的单条消息预算。手动停止是唯一粘性状态,只有手动开启或重载插件能解除。
这台机器上依赖分三层,出问题时先确认自己在哪一层:
| 层 | 内容 | 谁负责 | 位置 |
|---|---|---|---|
| 宿主环境 | Pillow、numpy |
MaiBot 自身依赖(_manifest.json 的 dependencies 会校验版本) |
MaiBot 的 Python |
| 插件覆盖层 | torch、torchvision、Pillow、numpy、psutil |
在 WebUI「运行环境」页点安装,插件自己下载并装入 | <数据目录>/dependencies/packages/ |
| 自定义环境 | 同上,全部 | 管理员自行准备 | 管理员指定的解释器/环境 |
- 宿主环境只装插件加载链路真正需要的东西:
recog/images.py顶层from PIL import Image,numpy在指纹计算时懒加载。这两个包在_manifest.json的dependencies里声明为Pillow >=11,<13、numpy >=2,<3;宿主版本不满足时会拒绝加载。 - 插件覆盖层里的
torch/torchvision/psutil不写进dependencies:体积太大(GPU 版约 2.7GB),不该让宿主预装。psutil只在训练 worker 里用于采样峰值内存。 - 自定义环境模式下插件不会安装任何东西,
torch、torchvision、psutil、Pillow、numpy必须已经存在;缺包时页面会提示「未安装 xxx」。 - 安装依赖需要
uv在 PATH 中,并且能访问download.pytorch.org(见「数据流向与网络访问」)。没有uv会明确报错,不会静默降级。
上传时为本批图片选择"是/不是该角色"。图片库支持修改标签、跨页批量操作、删除和恢复;「删除全部」仅删除当前角色的未删除图片,其他角色与已训练模型不受影响。历史训练任务引用的原图会保留。
支持 JPEG、PNG、WebP、GIF(含 APNG、动态 WebP)。动图统一取第一帧:识别、训练、缩略图和查重都只看首帧,不扫全帧。扩展名不可信——实测 2355 张素材里有 551 张 GIF 顶着 .jpg 后缀,格式一律按内容判定。宿主的 WebUI 上传仍只收静态图片(1.3.14/1.3.15 均如此),所以动图只能从聊天进来,插件自己的上传页拿不到。宿主按页面声明的 image_max_edge: 4000 把上传副本等比缩放到 4000 像素以内;插件侧再校验单张 20 MiB / 4000 万像素上限。
为什么只取首帧:509 张真实动图实测,首帧与 5 帧中位数的判定只差 11 张(2.2%),而「任一帧过线」会把负例误报从 32 张抬到 52 张。多帧聚合是拿精度换召回,不划算,因此在真实表情包样本证明首帧确实漏判之前不做(见 docs/decisions-animated-images.md D14/D19)。
动图入库时原始文件逐字节保留,首帧负责指纹、缩略图、审核展示与训练读取;精确查重认原始字节而不是首帧像素——否则「首帧相同、后续帧不同」的两个变体会被当成重复删掉。审核页只显示首帧静态缩略图,不播动图。
宿主侧的动图边界(1.3.14/1.3.15 源码核实):主程序在消息进入插件钩子之前,就会把超过 visual.max_image_size_mb(默认 16 MB)的图片压成首帧 JPEG(透明区域拍白底),且 RPC 单帧上限 16 MB、不分片。结果是——≤12 MB 的动图按动图识别;12–16 MB 可能整条钩子被跳过、没有标注;>16 MB 压缩成功时按静态首帧处理,压缩失败则插件看不到。这些都不影响消息正常处理,只是该图不产生提示。压缩到首帧之后,原始动画已无法恢复。
JPEG 解码走 libjpeg 的缩尺解码(Image.draft):训练时按该任务的输入尺寸解,推理时按最大的 448 解,省掉全尺寸 IDCT 与大图缩放。这对识别结果没有增益,只是更快——一张 12MP 图的预处理从约 300 ms 降到约 25 ms,训练每张约快 2.3 倍。非 JPEG 格式自动回退到完整解码;入库指纹与查重仍按原始尺寸计算,不受影响。
- 每批最多选择 100 个文件。
- 每页最多 24 张,单次批量操作最多 1000 张。查看范围可选全部 / 可训练 / 已排除 / 未标注 / 指定任务错判 / 已删除(可恢复)。
- 识别测试里判错的图可以直接「标为是/不是并入库」,不必先去图片库重新上传。
- 「查看错判」可以从训练任务页和模型版本页直接跳进图片网格,在那个视图里可以单张改标签(是 / 不是),改完立即从错判列表消失。批量、上传、删除、恢复仍然只在「图片标注」页做 —— 宿主按页放行 action,每页上限 20 个,把图片库的全部写操作复制到每个入口页会顶破这个上限(0.7.1 的处理方式,见 CHANGELOG)。
0.6.0 起不再需要给图片打标注。 旧版要求给每张图标「内容类型」(平涂/Q版、厚涂插画、实拍)、 负例标「它是什么」(其他角色 / 非角色内容)、以及「文字叠加」(有字 / 无字)。这三个字段从来不进 模型输入,也不影响训练与识别,只喂报告里三张分档表。移除的理由是标签本身不可靠:同一个人隔一周 标同一张图会给出不同答案(平涂与厚涂的界线本来就主观),而对不可靠标签做聚合,等于把噪声当诊断。 加上实际素材常把各种画风混在一起,按批标注不成立、只能逐张点,成本和收益倒挂。
「哪类图认不出」这个问题的替代答案是单图归因:报告里的「查看错判」直接列出被判错的图, 画风差异人眼一看便知,不需要事先给全库打标签才能聚合出这件事。
相同文件或像素完全一致的图片复用已有记录和标签。近重复图片按变体分组,每组保留一个训练代表。查重在上传后进行,管理员可核对排除原因和标签冲突。
每类至少需要 20 个独立图组才能开始训练,但这只是下限,不是建议值。受控实验(真实插画,见 docs/experiments-sample-size.md)在 0.60 判定阈值下测得:
| 每类正例 | 50 | 100 | 200 |
|---|---|---|---|
| 精确率(提示里正确的比例) | 0.64 | 0.66 | 0.84 |
20 组时精确率只有 0.52 —— 约一半的提示是错的。更省力的替代是加负例:50 组正例配 200 组「不是该角色」的图片,精确率就能到 0.80,比把正例堆到 100 组更好。训练页会按当前图组数给出档位与建议。
样本量不是唯一的决定因素。同一批数据里,厚涂同人插画的召回曾只有 2%,把它加进训练后升到 82% —— 差距来自「训练集有没有见过这种画法」,而不是数量。所以补图时要按你实际会遇到的画风补,只堆官方立绘会在同人图上几乎认不出。判断哪类图在掉,看报告里的「查看错判」:错判图本身就是那一类的样本。
报告给的是验证集、测试集和评估集三行整体指标(精确率及其分子分母、召回、F1、准确率、混淆矩阵),外加错判图片列表。
评估集是跨版本可比的依据:首次训练时按每类最多 25 组、以 (宽, 高) 整批挑出(同一批不会跨训练与评估,避免同源图虚高),之后每次训练都用同一批。它不参与训练与选轮;重建它会让历史报告不再可比,训练页有显式的「重建评估集」按钮,确认框会先告诉你它会占走多少张、分别来自训练/验证/测试集。已有训练历史的库不会自动建(那会改动既有划分),升级后需要点一次「重建评估集」。
图片自动分为训练、验证和测试集。后续图片保持已有划分;同图变体跟随原组,连接不同划分的图片暂时排除。
新任务的增广包含颜色抖动、随机裁剪(保留 60%–100% 面积)与水平翻转;0.5.0 之前创建后又暂停的任务保持原增广(只做颜色抖动),以免恢复时输入序列突变。
默认增量训练:加载当前启用模型,仅训练新增或标签已修改的图片。模型保存累计训练来源,图片库显示当前模型是否训练过该图片。首次训练和从头训练使用通用预训练底座(ImageNet MobileNetV3-Small,SHA256 见 recog/model.py 的 BASE_SHA)。
每个实例同时运行一个训练任务。任务开始时冻结数据和标签,训练期间的标注修改用于下一次任务。暂停保存训练状态,继续使用同一快照;最终核验阶段可取消。中断任务由管理员手动重试。
训练运行期间,推理 worker 自动降为 1 个 CPU 线程,训练结束后恢复;识别服务继续可用,聊天不被阻塞。
报告包含精确率及其分子分母、召回率、F1、准确率、混淆矩阵、错判图片和耗时。混淆矩阵行表示真值、列表示预测,顺序为"不是该角色/是该角色"。重复使用的测试集标为历史留出评测;留出标签或成员变化后,报告间的分数需要分别评估。指标默认按当前判定阈值重算,见「判定阈值」。
模型通过核验后由管理员启用,可回退上一启用版本。训练期间当前模型继续服务,版本切换前先完成加载和校验。
训练页可选择 224、320 或 448;插件配置 input_size 设置默认尺寸,默认为 320。设置用于新训练任务;识别和版本回退使用各模型保存的尺寸。
收到图片、表情和转发中的图片时,按该角色自己的判定阈值(默认 0.60)判定;同一张图只提示一个角色 —— 在通过各自阈值的角色里取分数最高的一个,若与第二名分差小于 0.02 则认为分不开,整图不提示(这个值经实测校准:0.08 会把 4.6% 的正确高分命中一起丢掉)。单条消息处理预算为 6 秒,超时保留原消息(fail-open)。auto_recognize=false 可关闭自动识别。
聊天捕获(chat_capture,默认关):打开后,模型在 0.4–0.9 这个「自己也不确定」的区间内的图会进入「待确认」页,但不带标签,确认前不参与训练。入库前有闸门:纯色、跨划分冲突、同组已有可用成员的图直接丢弃(只删临时文件,不写库),因为入库后也只会被自动排除,看不到又占盘。「待确认」默认把所有角色的队列放在一起,按捕获分数全局排序,页内的「角色范围」可切到单角色;确认、撤销、删除都用该图组自己的角色,不随头部选择器变化。队列按图组折叠,一组只问一次;按「是 / 不是」各一步,不再追问类型。积压上限是每角色 100 组(不是全库 100 组),满了只停止收集该角色的新捕获;页面里的「已自动处理」可按当前角色范围一次性清理旧数据。注意:这会把群成员发的图片保存到本地,开启前请确认对方知情。模型尚未就绪(冷启动中、熔断冷却中、已手动停止)时,该条消息直接不提示,不会阻塞聊天;后台预热完成后后续消息恢复。
识别测试保留最近 24 张图片及结果,可使用当前模型重新识别,显示判断、得分、版本和耗时。聊天图片与测试图片均独立于训练库。
工具接口:
recognize_character_image(message_id, image_index=0, character="")
工具读取当前聊天流中指定消息的图片或表情,image_index 为零基序号,character 留空则返回全部已启用角色的得分。未启用模型、消息或图片失效、索引错误及加载失败时返回具体状态。
判断门槛:得分 ≥ 该角色的判定阈值(默认 0.60)为"命中",≤ 阈值减 0.2 为"未命中",其余为"不确定"。得分表示模型输出,不是校准概率,身份仍需结合上下文核对。
每个角色有独立的线上判定阈值,默认 0.60,存在 settings 的 threshold:<角色> 键里。它只影响自动提示、工具判定与"错判"列表的成员,不改变模型权重;回滚模型也不会重置它。
- 训练报告的「判定阈值」面板会按验证集给出建议值:取「精确率 ≥ 90% 的最低阈值」,即同样精确率下召回最大的一点。搜索下限是 0.50 —— 低于它等于在模型自己倾向"不是"时仍判命中。
- 报告同时显示建议值的分子分母与 Wilson 95% 置信下限。每类只有 20 组时验证集仅 3/3,
3/3的置信下限只有 44%,因此会明确标注「样本不足,仅供参考」,不会显示裸的百分比。 - 支持一键采纳、手动输入、恢复默认(删除覆盖键,而不是写死 0.60)。
- 报告表格里的指标按当前阈值重算,每行标注所用阈值;
technical中保留训练时 0.50 的原始口径。因此调阈值会让报告数字与"错判"列表同时变化,这是刻意设计:报告要说的是"线上会怎样"。 - 建议值只在验证集上算。测试集是封存证据,用它调阈值会让"留出评测"失去意义。
持久数据全部写在宿主 SDK 授予的插件数据目录(默认 data/plugins/org.civetc.char-recog/),插件源码目录只放只读素材:
catalog.sqlite3:图片、标签、任务和版本记录。jobs表的device/input_size两列供任务列表使用,避免每次轮询都读取快照(旧库自动 ALTER,旧任务首次读取时回填)。settings键:active:<角色>/previous:<角色>/last_held_truth:<角色>/threshold:<角色>/eval_built:<角色>/characters/form_request_key/test_history/last_test/worker_mode/worker_override/ui_state(页面上次选择的角色、筛选与训练选项)。uploads/:宿主上传凭证落点(领取后即入库,不长期保留)。images/、thumbnails/:原图与缩略图缓存。test-images/:识别测试用图(只保留最近 24 张,超期即删)。jobs/<任务>/:训练快照、断点、进度与报告。在训练页手动清理任务记录时,先把report.json与snapshot.json复制到models/<任务>/,再连同快照、断点与日志一起删除。models/<版本>/weights.pt:角色模型。任务记录清理后,同一目录下还保留report.json与snapshot.json,模型页的「查看报告 / 查看错判」和下一次增量训练都靠它们;模型权重不受任务清理影响。在模型页删除版本时,整个目录连同这些留档一起移除(任务记录仍在时,任务页的报告不受影响)。dependencies/packages/:依赖覆盖层。dependencies/downloads/:下载缓存与续传文件。environment-status.json、environment.log、inference.log:安装与推理状态、日志。
发布插件时包含源码、页面、声明文件和通用预训练底座。运行数据、环境、配置和日志由各实例保管。
训练任务记录支持手动清理(0.5.25):只有已完成、失败、中断、取消或暂停的任务可删,运行中的一律拒绝;一次最多 50 条。删除是单向操作,页面会用确认框说明留档范围。
模型版本支持手动删除(0.5.26):使用中的版本、以及正被未结束训练任务当作父模型的版本不可删;批量里混入这类版本会整批拒绝。删除会移除 models/<版本>/ 整个目录并清空指向它的回退记录,但保留训练任务记录。版本卡片会显示目录占用,确认框按所选合计。
防护点(0.5.21):sqlite3.Error(库锁、IOERR 等)不属于 ValueError/OSError,此前会从所有兜底里穿过——聊天捕获线程的异常会落在无人消费的 executor future 上完全不可见,WebUI 动作会把裸异常抛给宿主。现在两类路径统一收口:捕获线程异常记 warning 日志,动作错误收敛为 {"success": false} 结构。
tools/migrate_from_maiself.py 可以把上游 maibot.mai-recog-self 数据目录整体迁移过来(整库归属到一个角色、保留划分与模型血统,幂等,支持 --dry-run)。运行它需要宿主 Python 或插件覆盖层里已有 Pillow + numpy。
- 不外发任何用户数据:不使用
httpx/requests/aiohttp,不向第三方上传聊天记录、用户 ID、群号、图片、配置或 token;没有用户可控 URL,因此也没有 SSRF 面。 - 唯一的出站访问是安装 PyTorch 依赖时的
download.pytorch.org(官方索引与官方 CDN),域名在代码里硬编码,不跟随索引返回值取主机。 - 每个 wheel 都用索引提供的 SHA256 强校验,校验失败即删除重下;安装包缓存拒绝符号链接、拒绝越出插件目录的路径。
- 安装动作由管理员在 WebUI「运行环境」页显式点击触发,不会在插件加载时自动联网。
- 训练与推理的模型权重只读本地文件:ImageNet 底座与已训练模型都做 SHA256 校验后才加载。
- 训练断点(
resume.pt)用torch.load恢复,仅由训练 worker 写入,并与resume-audit.json里的快照摘要 + 文件 SHA256 双向核对;用户上传的图片永远不会成为反序列化输入。
测试命令:
python -m pytest tests0.6.1 起不再需要 maibot_sdk 也能测插件层。 tests/conftest.py 在会话开始时把一个最小 SDK 桩装进 sys.modules,于是原先恒为「整模块跳过」的三个文件——test_plugin_loads.py、test_tool.py、test_annotation_policy.py——现在会真正执行。当前完整环境(含 torch)实测 412 collected / 409 passed / 3 skipped,其中 0.6.0 变更集口径为 235 collected / 234 passed / 1 skipped。
这次改动不是锦上添花:0.6.0 把
Store.ingest从 9 个参数砍到 6 个时漏改了_capture的调用点,整条聊天捕获链路静默失效,没有任何一条测试报出来——因为唯一会执行插件层的那三个文件全都是 0 收集。桩只提供plugin.py实际 import 的 8 个符号,不实现权限模型与ctx;需要宿主行为的测试仍应自带 fake ctx(见tests/test_release_gate.py)。
缺少 PyTorch 时,3 个需要真实训练/推理的测试模块会自动跳过(test_cpu_acceptance.py、test_input_size.py、test_resume.py),其余用例仍会执行;装上 CPU 版 PyTorch 后这三个模块会真实执行。另需 Pillow、numpy、psutil、pytest。test_tool.py 里「默认配置契约」一条另需本地 SDK 的 src.plugin_runtime 可导入(在 MaiBot 主程序目录下运行时满足),该条在纯开发机上跳过。
这三个模块自 0.3.0 起一直因缺 torch 而跳过,参数错误被掩盖到 0.4.1 才修。改完训练链路务必在有 torch 的环境跑一次全量,否则门禁只覆盖一半。
跑全量前先看一眼磁盘:C: 剩余空间低于约 2 GB 时 pytest 会给出大片假红(OSError: [Errno 28] No space left on device、torch 加载半截文件的 RuntimeError),与产品无关。清理只删 %TEMP%\pytest-of-<user>\garbage-*,不要动 pytest-NNN——那可能是别人正在跑的会话。
test_webui_contract.py 是静态契约测试,不需要任何三方依赖:它校验 webui.json 声明的 action 参数与 webui/index.html 里 operate(...) 的实参一致、生成产物与共享源同步。
页面不再声明同一套 action:宿主要求每页最多 20 条,所以每页只带自己区块能触发的动作,测试相应改为「每页覆盖本页触发面 + 并集覆盖全部调用点」。另有 test_webui_host_limits.py 固化宿主文档里的全部限额(页面数、每页 action/query 上限、参数数、uploads 必须绑定接收必填 upload_id 的 action、页面体积),这些限制违反任意一条都会让整份声明作废、插件直接不加载。
test_scores.py / test_thresholds.py / test_source_index.py / test_jobs_columns.py / test_annotation_policy.py 均为 torch-free:分别覆盖阈值数学与建议值、角色级阈值存储、SourceIndex 与旧线性扫描的等价性(含随机化对照)、jobs 表迁移与快照回填、以及「只提示一个角色」的聚合策略。
实测结果见 VALIDATION.md,合成素材测试用于验证工程流程,实际识别效果由训练数据决定。
共享源文件为 webui/index.html。修改后在插件目录执行:
uv run --no-sync python webui/build.py生成 webui/{images,capture,training,models,test,environment}.html 后点击"重载页面"。修改 webui.json 则需要重新加载插件。
宿主为每个侧边栏页加载独立文档,并且把 custom 页放进 sandbox="allow-scripts" 的 iframe(不透明源),localStorage 会抛 SecurityError。所以上次选择存在插件库的 settings.ui_state 里:页面启动时先执行 ui 查询恢复,再发第一轮业务查询;改动时通过 ui_save 串行回写(同一时刻最多一个请求,期间的新改动合并成一次)。保存角色、图片筛选(视图)、训练选项(模式/设备/输入尺寸)和「待确认」的角色范围;勾选的图片、跳过与浏览位置属于一次性状态,不保存。读写都经过白名单校验,坏数据回落到默认值。
相对 SengokuCola/MaiSelfPlugin,本插件做了这些改动(上游原文不再保留,避免与实现不一致):
| 项 | 上游 | 本插件 |
|---|---|---|
| 识别目标 | 单角色("是不是 Bot 自己") | 每角色一个二分类模型,characters 可增删 |
| 配置键 | character_name(单角色) |
characters(角色名单)+ input_size + worker_python |
| 工具名 | recognize_self_image(message_id, image_index=0) |
recognize_character_image(message_id, image_index=0, character="") |
| 判定文案 | "是自己/不是自己" | "命中/未命中/不确定" |
| 数据库 | 无角色维度 | images/jobs/models 增加 character 列,旧库自动 ALTER 迁移 |
| WebUI | 单角色页面 | 头部角色选择器,6 页按角色隔离查询与操作;「待确认」默认汇总全部角色,页内可切单角色 |
| 依赖管理 | 插件目录内 .venv |
共享覆盖层(数据目录内)+ 可选自定义环境 |
| 数据目录 | data/plugins/maibot.mai-recog-self/ |
宿主授予的 ctx.paths.data_dir |
未复制上游的任何个人图片、标签库、实验结果或已训练的身份模型;assets/mobilenet_v3_small-047dcff4.pth 是 PyTorch 官方 ImageNet 权重。
- 本仓库:GPL-3.0-or-later(见 LICENSE、NOTICE.md)。
- 改编自 SengokuCola/MaiSelfPlugin(GPL-3.0)与独立的
Mai_recog_self工程;整图补边、MobileNetV3-Small 二分类及训练选轮逻辑来自后者。 - MobileNetV3 实现由 torchvision 提供。