图片超分这类工具,模型早就够用了,卡住普通人的是环境:Python、PyTorch、CUDA 版本、一长串命令行参数。 Final2x 做的事情是把这堆东西塞进一个六百多像素宽的小窗口——拖图进去、选模型、点开始。 让我想写一篇的不是它的界面,是它划进程边界的方式:界面这边一行推理代码都没有,真正干活的是另一个进程、另一门语言,两边靠三样东西通信。

定位

跨平台的图片超分辨率工具,Windows、macOS、Linux 都有发行物,BSD 3-Clause。当前是 v4.0.0,推理后端换成了 cccv,开始支持挂自定义权重。

它不训练模型也不实现算法,做的是把一堆现成的超分模型包装成能双击的东西。内置的权重列表覆盖 Real-ESRGAN、Real-CUGAN、DAT、HAT、SwinIR、SCUNet、EDSR、SRCNN 八个系列七十多项,里头还有 APISR、AnimeJaNai、Ani4K 这些动漫向的微调版本,通用照片和二次元插画各有各的选择。

仓库结构是这篇想讲的重点。GUI 这边是 TypeScript 加 Vue,从主进程到界面组件,没有任何推理代码。所有算法在一个叫 Final2x-core 的独立项目里,Python 写的,能单独 pip 装、单独当命令行工具用。桌面应用只是它的一个前端。

上手

Windows 和 macOS 下安装包就行。macOS 没走签名公证,第一次要手动放行:

bash
xattr -cr /Applications/Final2x.app

Linux 得自己先备好环境,因为安装包里不带推理内核:

bash
pip install Final2x-core
Final2x-core -h
apt install -y libomp5 xdg-utils

打开是个固定大小的窗口,把图拖进去、选模型和倍率、点开始。输出目录不填的话,它会从第一张输入图的所在目录推一个。

进程边界

一次超分的进程往返一次超分的进程往返:配置 base64 后当命令行参数下去,stdout 文本流上来,界面靠三个哨兵正则解析进度

交互版,可缩放、可播放调用轨迹。)

一次超分的完整往返是这样:界面把设置攒成一个配置对象,通过 IPC 交给主进程;主进程把它序列化、base64、拼成命令行参数,起一个子进程;子进程一边跑一边往 stdout 打日志,主进程原样转发回界面;跑完给一个退出码。

两个进程之间的契约就这三样:一个 base64 过的 JSON、一条 stdout 文本流、一个退出码。没有 socket,没有本地 HTTP 服务,没有 gRPC,也没有反向通道——子进程起来之后,界面唯一还能对它做的事就是把它杀掉。

base64 那一步值得单说。配置里有引号,路径里有空格,中文路径也常见,而 Windows 的 cmd 和 POSIX shell 的转义规则完全不是一回事。编码之后参数里只剩 A-Za-z0-9+/=,引号地狱一次性消失。这招土,但比自己写一套跨平台的 shell 转义靠谱得多。

杀进程那头也有个坑。子进程是带 shell 起的,直接对拿到的 pid 发信号只会杀掉 shell,Python 那层变成孤儿继续占着显存。所以它按 pid 树整棵杀,并且在应用退出前先拦下 quit 事件,等杀干净了再放行。

哨兵协议

进度条怎么来的?正则匹配 stdout。

ts
const skipImageRegex = /______Skip_Image______:(.+)/
const processingRegex = /Processing------\[ ([\d.]+)% /
const srSuccessRegex = /______SR_COMPLETED______/

界面每收到一段输出就拿这三条过一遍:匹配到百分比就更新进度条,匹配到跳过就弹一条提示,匹配到那个下划线包起来的完成标记就把成功标志置上。子进程退出时如果这个标志还是假,直接弹失败对话框——退出码为零不算成功,见过完成哨兵才算

脆弱是明摆着的,内核那边改一句日志格式,进度条就静默失效。但它换来一件事:内核的输出对人也是可读的。同一份日志既喂给界面解析,也原样显示在日志抽屉里,不需要维护「给人看的」和「给程序看的」两套。人和程序读同一份输出,这个取舍我觉得是划算的。

内核的来源

推理内核从哪来推理内核从哪来:构建期拉一份打进安装包,运行期先探测系统里有没有,两条路汇到同一句 spawn

交互版

「到底用哪个内核」这件事有三段逻辑。

先探测:直接跑一次 Final2x-core -h,退出码为零就说明系统里有 pip 装的版本,那就直接用命令名调用。探不到才回退到安装包里带的那份,位置是可执行文件目录再上跳一级——打包配置里那份内核是以额外资源的身份放在 asar 归档旁边的,不进归档。

包里那份是构建期下载来的:CI 打包前先从内核项目的 Release 拉一个压缩包解开,再交给打包器塞进去。Linux 这一步直接跳过,因为 PyTorch 加上权重的体积不适合放进安装包,所以 Linux 的发行物文件名里干脆写着 pip。

还有个细节:界面里那个七十多项的模型下拉框不是手抄的,文件头上写着由 Final2x-core 自动生成、不要手改。跨语言的枚举同步交给代码生成,比两边各维护一份靠谱。而且这个下拉框是可输入的,v4 挂自定义权重就是从这儿输名字进去。

能力

模型系列 —— Real-ESRGAN、Real-CUGAN、DAT、HAT、SwinIR、SCUNet、EDSR、SRCNN,含 APISR、AnimeJaNai、Ani4K 等动漫向微调权重 目标倍率 —— 可以不按模型固有倍率走,填一个目标缩放值,由内核自己组合放大与重采样 设备选择 —— Auto、CUDA、MPS、CPU 四挡,默认 Auto 分块推理 —— 显存不够时切块跑,默认开着 批量与格式 —— 一次拖多张,输出可选 PNG、JPG、WebP、TIFF 权重代理 —— 权重要从 GitHub 拉,可以填一个加速前缀 自定义权重 —— v4 起模型框可以直接输入名字

设置是持久化的,关掉再开还在。

边界与限制

  • Linux 不带内核。 得自己 pip 装,还得先有 Python 3.9+ 和 PyTorch 2.0+。这不算隐藏条款,发行物名字里就写着 pip。
  • Intel Mac 没有官方构建。 发布矩阵里 macOS 只有 arm64;下载脚本里那条 x64 分支查不到地址,会落到「无效平台」直接返回。Windows arm64 拿到的则是 x64 内核,靠系统转译跑。
  • 协议是文本。 内核改日志格式,进度条就哑了,而且这类耦合不会在编译期报错。
  • 窗口尺寸锁死。 宽度限死在 670 到 870 之间,高度 470 到 670,拉不开。
  • 硬件加速被关掉了。 主进程启动时就调了 disableHardwareAcceleration,注释写的理由是 Windows 兼容性。
  • 没签名公证。 macOS 首次运行要手动放行。README 给的办法是把 Gatekeeper 整个关掉,这条我个人不太赞成,先试 xattr -cr 通常就够了。

取舍

把 GUI 和推理拆成两个进程、两门语言,是这类工具的常见形态,但拆到什么程度差别很大。多数做法是嵌一个本地服务:起个 HTTP 或者 gRPC,双向通道,结构化消息,进度、日志、取消各走各的路。Final2x 选了最土的那一档,单向命令行加 stdout 文本。

代价上面写了。收益有两条,我认为都成立。

一是内核有了 GUI 之外的生命。它本来就是个 pip 包,装完直接是命令行工具,能进脚本、进批处理、进别人的流水线。如果协议是私有的 socket,内核就退化成桌面应用的附属品了。

二是两边可以完全独立发版。界面不需要知道内核的内部结构,内核也不需要为界面留接口。运行期那套「先看系统里有没有」的逻辑,正是因为协议足够薄才敢写——用户自己装的版本可能比包里那份新,但只要命令行参数没变就能直接顶上。

判断一个本地 AI 工具的外壳做得薄不薄,我现在会看一条:把 GUI 删掉,内核还能不能独立活着。能活的,外壳大概率是对的。

尾声

这类工具最容易走的一条路是:为了界面写起来顺手,把推理逻辑也拉进同一个进程,最后得到一个既不好用也不好改的整体。Final2x 反过来,把外壳压到只剩窗口、参数拼装和一个进度条正则,别的全推给一个能独立存在的命令行程序。

代码在 EutropicAI/Final2x,BSD 3-Clause,内核在 Final2x-core