看着手机上剪映、美图秀秀里的视频修复功能,几秒钟就能把糊成马赛克的老视频“变清晰”,很多人以为这就是模型在本地跑出来的。实际上,Wink 这类应用里的“超清修复”走的是云端大模型路线——视频传上去,远程显卡跑完再传回来。效果确实顶尖,但流量、延迟、隐私的账一算,问题就出来了。正好最近我在做一个灵狐风格的端侧轻量化修复工具,核心目标就是把视频修复模型塞进浏览器,让用户在本地、在网页里直接完成画质增强。这篇文章就把我踩过的坑、验证过的方案、以及从云端大模型落地到端侧推理的完整思路整理出来。
先说结论:这条路完全走得通,但没你想得那么“傻瓜”。浏览器里跑视频修复模型,本质上是把“空间域大模型 + 时序后处理”压成“轻量级超分网络 + WebGPU 并行推理”,中间要过模型转换、量化、算子适配、帧管线编排这几道坎。适合正在做视频增强、老片修复、或者想把 AI 能力嵌入网页应用的人参考,下面全程是我的实际经验。
1. 内容整体设计与思路拆解
1.1 云端大模型的算力账与延迟账
Wink 的云端修复方案,底层通常是 Restormer、Stable Diffusion 类复原模型或它们的高精度变体。这类模型在 NVIDIA A10/A100 上处理一段 1080p 视频,单帧推理时间在 50ms 到 300ms 之间,主观效果好,纹理重建相当自然。但云端方案的成本不只是“GPU 跑一次”,而是要把整个视频编解码链路都搬到服务端:
- 客户端上传原始视频,经过 WebRTC 或 HTTP 分片。
- 服务端解码(通常是 H.264/H.265 硬解)。
- 逐帧送入大模型推理,可能要配合光流做时序稳定性处理。
- 服务器端重新编码修复后的视频。
- 用户下载或流式播放,前端再跟上播放对接。
这一套下来,单条 30 秒的 720p 视频,云端处理耗时通常要 2 到 4 分钟。更关键的是带宽——上传几十 MB 到几百 MB 的视频,在移动网络下体验非常差。而且用户一旦意识到“我的视频要传到别人服务器上”,隐私这一关就很难过去。
我在调研阶段测过一组数据:一段 10 秒 480p 的视频,码率 2Mbps,体积约 2.5MB。走云端修复,从上传到拿到结果,在 5G 环境下大约要 50 秒(其中排队时间 20 秒,GPU 推理 15 秒,编解码和网络传输 15 秒)。同样的素材,端侧基线的轻量化模型在浏览器里跑是 11 秒,还是在 WebGPU 没有充分利用的情况下。这账一算,端侧方案的吸引力就出来了。
1.2 端侧方案的三大核心价值
把模型塞进浏览器,核心是在拍板架构之前,你要先想清楚用户感知层面的三个收益,否则后面都是白折腾。
第一是延迟。端侧推理没有“上传 → 排队 → 下载”的链路,帧数据直接从 VideoFrame 对象进推理管线。对于超分这种单帧无状态操作,首个可见结果可以在 500ms 内出现,整段视频的处理时间基本等于“帧数 × 单帧推理耗时”,带宽归零。
第二是隐私。这是老片修复场景里极其敏感的一环。家里的老照片、旧结婚录像、去世长辈的影像,用户天然不愿意传云端。端侧跑意味着视频数据从头到尾不出设备,这也让合规成本大幅降低——不需要做数据出境评估,不需要写隐私协议里“我们会收集您的视频用于处理”这类劝退文案。
第三是成本。云端大模型按 GPU 时长计费,如果有 1 万用户同时排队处理视频,后台必须买断或按量付费租卡。端侧部署模型的成本只发生在开发期(训练+调试),用户浏览器就是免费的分发网络。
不过端侧方案有一个必须接受的现实:浏览器里的可用算力远低于服务器 GPU。桌面端 Chrome 的 WebGPU 能调用的计算单元大约等于一块入门级独显,移动端则更紧张。所以模型不能照搬云端那一套,必须做轻量化改造。这也是我这个项目命名里“灵狐”的含义——它是一套经过蒸馏和量化后的轻量级修复模型,效果和云端有差距,但性价比极高。
1.3 为什么选择浏览器作为端侧载体
端侧部署的平台选择上,很多人第一反应是:为什么不做成原生 App 或小程序,非要塞浏览器?
我的理由是“分发成本”。原生 App 需要下载安装、需要适配 Android/iOS 两套生态,小程序要过审核且不能自由使用 WebGPU。浏览器方案只要打出一个 URL,用户点开即用。而且随着 WebGPU 在 Chrome、Edge、Firefox 的逐步稳定,2024 年之后浏览器内的 GPU 通用计算能力已经达到“可做实时图像处理”的水平——矩阵乘法、卷积、池化这些神经网络底层操作,WebGPU 的 compute shader 都能高效实现。
再加上 WebCodecs 提供了 VideoDecoder / VideoEncoder,浏览器不再只能靠<video>标签读帧,而是可以直接解码出 VideoFrame 并交给 GPU 纹理。这意味着整个“解码 -> 推理 -> 编码 -> 播放”链路都可以在浏览器内部闭环。这套组合拳在 2023 年之前是做不到的,所以直到今天,浏览器端视频修复仍然是少数人尝试过的方向,但窗口期已经打开。
2. 核心细节解析与实操要点
2.1 模型选型:从 Real-ESRGAN 到灵狐轻量网络
云端大模型往往采用百 MB 甚至 GB 级别的参数。举个例子,一个通用的视频复原 Transformer 模型,如果输入 patch 是 128×128,参数量轻松到 5000 万以上;当输入扩展到 512×512 时,Self-Attention 的计算量是平方级增长,浏览器直接跑这种模型,单帧推理时间会到分钟级,完全不可接受。
灵狐方案采用的是Real-ESRGAN 的轻量化蒸馏版本,配合几个针对性改造:
- 主干从 RRDB(Residual in Residual Dense Block)换成改进型ESPCN 残差结构,把 4 倍上采样放到末端,中间层只处理低分辨率特征图。
- 去掉 4 倍下采样的前端,直接接一个 3×3 卷积把 RGB 映射到 64 通道特征空间。
- 引入PixelUnshuffle 前置降采样,把 4K 输入压到 1080p 级别再进网络,降低计算量的同时保留一定的高频信息。
- 最后用亚像素卷积(Pixel Shuffle)实现从低分辨率到目标分辨率的直接跳动。
这样改造完的轻量版模型参数量在 200 万到 400 万之间,单帧 1080p 推理在 WebGPU 下能压到 100ms 到 300ms 区间。
选模型时还要注意一点:超分模型不等于修复模型。老视频常常有噪点、压缩块效应、模糊、色偏,单纯做分辨率放大会把噪声也放大。所以我在训练阶段混合了三个目标:超分重建(L1 + Perceptual Loss)、去噪(添加高斯/泊松噪声模拟)、去压缩伪影(用真实压缩视频做配对数据)。最终模型在主观效果上“去糊 + 去块”并存,而不是只拉分辨率。
2.2 量化方案:INT8 与 FP16 的工程取舍
模型塞进浏览器,除了结构上的轻量化,数值精度也是关键。WebGPU 的浮点能力分两档:f32 在所有设备都支持,f16 是可选特性,INT8 则需要通过算子融合变相模拟。我在实践中做了两组对比。
| 精度 | 模型体积(ONNX) | 单帧推理耗时(1080p,RTX 3060) | PSNR 损失(相对 FP32) | 说明 |
|---|---|---|---|---|
| FP32 | 15.3 MB | 320ms | 基准 | 体积大,速度最慢 |
| FP16 | 7.7 MB | 150ms | 0.02dB 以内 | 精度几乎无损,体积减半 |
| INT8 | 3.9 MB | 90ms | 0.3~0.5dB | 体积最小,速度快,但需要量化校准 |
实际部署时我推荐主体保持 FP16,敏感层(最后几层卷积和亚像素上采样层)保持 FP32的混合精度策略。原因很简单:INT8 在量化敏感层时会造成纹理细节丢失,视频里的人物皮肤和文字边缘最容易出现“死白”或“马赛克化”。FP16 在 WebGPU 上如果硬件支持(hasFeature('float16-filterable')),几乎无损。
校准数据一般准备 300 到 500 张有代表性的老照片/视频帧,用原始模型跑一遍输出,统计每层激活值的 min/max,再按对称量化计算 scale 和 zero point。这个步骤不能省,直接拿默认参数量化,模型输出经常会整片发绿或变暗。
注意:INT8 量化完成后,务必在浏览器里用真实摄像头/视频帧做一轮端到端验证,不要只跑 1 张测试图。我遇到过某层量化后数值漂移,单张图片效果正常,但连续帧会出现闪烁的情况。
2.3 模型导出:PyTorch -> ONNX -> WebGPU
把 PyTorch 权重变成浏览器可运行的模型,链路是:
# 步骤1:PyTorch 导出 ONNX python export_onnx.py \ --weights linghu_final.pt \ --output linghu.onnx \ --opset 18 \ --input-size 1x3x270x480 # 步骤2:用 onnxsim 做常量折叠和结构精简 python -m onnxsim linghu.onnx linghu_sim.onnx # 步骤3:转换为 ORT 格式(WebGPU 后端更友好) python -m onnxruntime.tools.convert_onnx_models_to_ort \ --input linghu_sim.onnx \ --output_dir ./ort_models \ --optimization_level 99这里的重点是--opset 18。WebGPU 后端对 ONNX 算子支持以 opset 18 为兼容基线,太老或太新的版本都容易出算子缺失问题。另一个经验是:导出 ONNX 之前,一定要把模型的training状态关掉,并把 BN(BatchNorm)层全部折叠到卷积层。否则 ONNX 文件里会残留一批BatchNormalization节点,WebGPU 后端虽然能跑,但性能和数值稳定性差不少。
ONNX Runtime Web 有两种运行方式:WASM 和 WebGPU。WASM 走 CPU 多线程,适合低端设备兜底;WebGPU 走 GPU,是主力路径。我在工程里做了一个自动降级逻辑:优先ep = 'webgpu',如果报GPU is not supported就切'wasm',再不行就提示用户换浏览器。
前端初始化代码大致是这样:
import * as ort from 'onnxruntime-web'; ort.env.wasm.wasmPaths = '/wasm/'; // 首选 WebGPU,失败则退回 WASM let session; try { const webgpuAvailable = navigator.gpu && (await navigator.gpu.requestAdapter()) !== null; session = webgpuAvailable ? await ort.InferenceSession.create('/models/linghu_fp16.ort', { executionProviders: ['webgpu'] }) : await ort.InferenceSession.create('/models/linghu_quant.onnx', { executionProviders: ['wasm'] }); } catch (e) { // 最后兜底:WASM + 降采样输入 session = await ort.InferenceSession.create('/models/linghu_quant.onnx', { executionProviders: ['wasm'] }); }3. 实操过程与核心环节实现
3.1 浏览器侧的视频帧读取链路
视频修复不是把一张图放大,而是对连续帧做处理。所以第一步是搭建“视频 -> 帧 -> 纹理”的管线。我推荐用 WebCodecs,而不是传统 canvas +drawImage。后者会把视频帧先拷贝到 Canvas 2D 上下文,再getImageData读出 RGBA,链路长、内存复制多,而且拿不到 GPU 纹理,性能损失明显。
用 WebCodecs 拉帧的流程如下:
const decoder = new VideoDecoder({ output: (frame) => { // frame 是一个 VideoFrame 对象 handleVideoFrame(frame); }, error: (e) => console.error('解码错误', e) }); decoder.configure({ codec: 'avc1.42001f', // H.264 High Profile Level 3.1 optimizeForLatency: false, hardwareAcceleration: 'prefer-hardware' }); // 把 ArrayBuffer 数据 feed 给解码器 const chunk = new EncodedVideoChunk({ type: 'key', timestamp: 0, duration: 33333, // 微秒 data: encodedVideoBuffer }); decoder.decode(chunk);这里有个容易踩的坑:视频的编码格式直接影响解码器的可用性。Chrome 的 VideoDecoder 不支持所有 H.264 Profile,比如 10bit 的 H.264 就不行。实际项目里我用 FFmpeg 转码作为前置步骤,把用户上传的所有视频统一转成 H.264 High Profile 8bit 4:2:0,YUV420 格式。这个转码本身也可以在浏览器里做——用mp4box或ffmpeg.wasm,但那又是另一个大坑,后面再说。
3.2 神经网络推理前的预处理细节
拿到 VideoFrame 之后,模型吃的是 [1, 3, H, W] 的 RGB float 张量。需要做:
- 从 VideoFrame 读取 RGBA 数据(用
frame.copyTo()到 ArrayBuffer)。 - 去掉 Alpha 通道,RGBA -> RGB。
- 像素值从 [0, 255] 归一化到 [-1, 1]。
- 根据网络输入尺寸做居中裁剪或等比缩放。
关键点是第 3 步的归一化。大多数 PyTorch 训练代码用的是mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]的 ImageNet 统计。但我的灵狐模型在训练时直接把输入归一化到了 [-1, 1],所以推理时应当用(pixel / 127.5) - 1.0,不该套用 ImageNet 均值。这类“训练/推理不一致”的问题,排查起来非常隐蔽——图像看起来整体偏暗或偏色,但网络结构完全没问题,最后才发现在归一化上栽了。
Float 数据送到 OnnxRuntime 时,推荐用Tensor 的 location 直接指到 GPU 纹理,而不是 CPU 数组再 upload。ONNX Runtime Web 的Tensor构造支持gpuBuffer和download两种模式。如果用 CPU 数组,每次推理都要复制一遍数据,带宽瓶颈比算力还明显。下面这段代码演示了如何把 WebGPU 纹理作为输入:
import { Tensor } from 'onnxruntime-web'; // 假设 existingWebGPUBuffer 是从纹理导出的 GPU Buffer const inputTensor = new Tensor('float32', [1, 3, height, width], { gpuBuffer: existingWebGPUBuffer, download: false // 关键:告诉 ORT 不要先把数据拉回 CPU });3.3 分块推理解决显存不足问题
浏览器里处理 4K 视频帧时,显存是一个大问题。WebGPU 的默认 buffer 大小限制因设备而异,但通常不建议把整张 4K 帧一次性塞进模型。我的方案是重叠分块推理:
- 把一帧 3840×2160 的图像切成 4×4,共 16 个 960×540 的 patch。
- 每个 patch 跟相邻 patch 保留 16px 的重叠区域(overlap)。
- 推理完成后,丢弃重叠区的边缘部分(每侧 8px),只取中心块。
- 最后按原坐标拼回一张完整输出图。
重叠区的作用是消除块与块之间的接缝。如果完全不重叠,修复后的图像在块边界会有明显的亮度跳变或纹理断裂,就像马赛克拼图。16px 的经验值来自我的实测试验:低于 8px 接缝明显,高于 32px 计算浪费太大。
代码结构示意:
const PATCH_SIZE = 960; const OVERLAP = 16; function splitIntoTiles(frameBuffer, width, height) { const tiles = []; for (let y = 0; y < height; y += PATCH_SIZE - OVERLAP) { for (let x = 0; x < width; x += PATCH_SIZE - OVERLAP) { const patchW = Math.min(PATCH_SIZE, width - x); const patchH = Math.min(PATCH_SIZE, height - y); tiles.push({ x, y, data: extractPatch(frameBuffer, x, y, patchW, patchH) }); } } return tiles; }分块推理其实还有一个额外收益:因为每个 patch 的尺寸固定,模型的输入尺寸固定,ONNX Runtime 可以提前编译 shader,不用在推理时做动态 shape 推导,启动速度能快 30% 左右。
3.4 时序稳定性:防止视频闪烁的关键
单帧超分模型单独处理每一帧时,会出现一个严重问题——帧间闪烁(flickering)。表现为静止区域轻微抖动、纹理细节忽隐忽现、边缘线条“爬动”。原因很简单:模型对同一场景的每一帧都独立做增强,噪声和压缩伪影在不同帧上被放大程度不一致。
我试过三种解决方案,效果从差到优排列:
- 置信度掩膜:让模型额外输出一张“修复置信度图”,在时序上对低置信度区域做平滑。实现难度高,训练成本大,效果不稳定。
- 帧间光流对齐:用轻量光流网络(如 PWC-Net 精简版)把前一帧的输出 warp 到当前帧,然后和当前帧输出做加权平均。效果很好,但光流网络本身在浏览器里又是一笔算力开销,只在关键帧上做才行。
- 滑动窗口时域滤波:对连续 N 帧的输出取加权平均,权重按时间距离衰减。实现最简单,内存占用小,实际效果能减少 80% 的闪烁问题。
我最终采用的是方案 3 的改进版——多细节层次融合(multi-detail fusion)。对当前帧输出和上一帧的 warp 结果做拉普拉斯金字塔分解,只在高频层做融合,低频层保留当前帧原始输出。这样既保留了当前帧的纹理细节,又不让运动区域产生拖影。
function temporalStabilize(prevWarped, currentOutput) { const l1Prev = laplacianDecompose(prevWarped, 3).high; const l1Curr = laplacianDecompose(currentOutput, 3).high; const fused = l1Curr.map((h, i) => h * 0.7 + l1Prev[i] * 0.3); return reconstructFromLaplacian( laplacianDecompose(currentOutput, 3).low, fused, 3 ); }这个函数的实际效果在人物说话的视频上特别明显——嘴巴和眼睛周围不再有高频闪烁,而静态背景则保持了极高的稳定度。
4. 常见问题与排查技巧实录
4.1 解码器报错:Unsupported codec or format
这是踩得最多的坑。Chrome 的 VideoDecoder 能力受平台硬解能力影响,同一段视频在 Windows 上能解,在 Linux 或 Android 上就可能报错。而且不同版本的 Chrome,能力差异也很大。
排查思路分三步:
- 先用
VideoDecoder.isConfigSupported()检测配置是否支持,不支持就降级用<video>+ canvas 读帧。 - 不要信任用户上传的原始容器格式。
webm(VP9)和mov(ProRes)在部分浏览器上支持不稳定,统一转 H.264 MP4 是当前最稳的路线。 - 转码用
ffmpeg.wasm是重路子,体积大、转码慢。我尝试过直接用浏览器播放视频并逐帧截图,虽然效率低,但胜在零依赖,适合低优先级的 repr 流程。
4.2 WebGPU 设备丢失或报错输出乱码
WebGPU 设备不是永久可用的,切换显卡、休眠唤醒、多标签页抢占都可能导致设备丢失(device.lost事件)。如果不处理,推理过程会突然中断,表现为输出图像一团乱码或直接白屏。
我加了两层保护:
- 监听
device.lost,触发时重建 session,并提示用户“GPU 资源被释放,正在重启推理引擎”。 - 在每次推理调用前检测
session.hostContext()的gpuStatus,如果异常就主动释放并重建。
这个保护逻辑本质上是把“GPU 不可用”当成常态来设计,而不是例外。移动端浏览器上,GPU 被后台抢占的几率远高于桌面端,所以不能假设设备始终可用。
4.3 首帧推理特别慢,怎么优化
WebGPU 首次推理会比后续慢很多,常见原因有两个:
- Shader 编译:WebGPU 需要把 kernel 函数编译成平台 shader(HLSL/SPIR-V),首次编译耗时可能 2 到 5 秒。
- 权重上传:模型权重需要从 CPU 内存上传到 GPU buffer,这个步骤在 session 创建时发生。
优化手段:
- 用
session.startProfiling()跑一次预热推理,在页面加载后、用户点击“开始修复”按钮前完成。 - 把常用的几个静态 shape 提前编译好,避免推理时动态 shape 引起的重新编译。
// 预热 const warmUpInput = new Tensor('float32', new Float32Array(1*3*270*480), [1,3,270,480]); await session.run({ input: warmUpInput });预热前后的对比,我在实测中看到:1080p 视频第一帧推理耗时从 1.8s 降到 220ms,效果非常可观。
4.4 内存爆炸:连续处理长视频的噩梦
如果用户上传一段 10 分钟的视频,逐帧处理不释放内存,Chrome 会被 OOM 杀进程。浏览器环境的内存管理不像 Node.js,没有全局 GC 可以依赖。我处理这个问题的核心思路是流式处理 + 主动释放。
具体做法:
- 不把整段视频一次性解码成所有帧,而是按 100 帧为一个批次处理。
- 处理完一批,就把原始 VideoFrame 调用
close(),输出的修复帧也用videoEncoder.encode()及时写入文件。 - 使用
ArrayBuffer时,用完立刻realloc或让引用归零,不要保留长生命周期的大数组。
async function processBatch(frames) { const results = []; for (const frame of frames) { const output = await enhanceFrame(frame); results.push(output); frame.close(); // 关键:主动释放底层资源 } // 将 results 编码为视频片段 const encoded = await encodeFrames(results); return encoded; }这段看起来简单,但“忘了 close”是新手最容易犯的错。VideoFrame 不 close,底层的 GPU 纹理和内存就不会被回收,10 分钟视频跑到第 3 分钟就把页面干崩溃了。
5. 性能实测:灵狐工具与云端方案的量化对比
5.1 测试环境与方案说明
测试设备是三组有代表性的环境:
| 设备 | GPU | 浏览器 | 备注 |
|---|---|---|---|
| 桌面 Windows | RTX 3060 12GB | Chrome 124 | 主力测试 |
| 桌面 Mac | Apple Silicon M2 | Safari 17 | WebGPU 兼容性需注意 |
| 手机 | Adreno 730 | Chrome 移动版 | 移动端 WebGPU 表现 |
测试素材是一段 10 秒的 480p 老视频,H.264 编码,码率 1.5Mbps,包含人物说话、镜头平移、轻微手抖。
对比对象:
- A:Wink 云端标准修复模式(服务端模型未知,视为大模型高性能方案)
- B:灵狐端侧方案(FP16 混合精度,WebGPU,分块推理 + 时域稳定)
5.2 耗时与体积对比结果
| 指标 | Wink 云端 | 灵狐端侧 |
|---|---|---|
| 端到端耗时(10s 视频) | 55 秒 | 19 秒 |
| 上传下载数据量 | 约 12MB | 0 |
| 单帧峰值显存占用 | 服务端不关心 | 约 1.2GB |
| 主观画质(5 分制) | 4.5 | 3.8 |
| PSNR(相对原始 1080p 参考) | 27.8dB | 26.4dB |
端侧方案单帧推理耗时约 180ms,1080p 输入,输出 4K 级分辨率。考虑到没有网络排队和传输,实际体验“点开就用”的感受非常强。
主观画质差距主要在极低质量素材上体现:当原视频有严重块效应或较强噪声时,灵狐的去块能力弱于云端大模型。但在“轻微模糊 + 轻度压缩”的日常老片场景中,两者观感差距不大,灵狐的细节锐度甚至略好(因为端侧分块推理减少了全局纹理过度平滑)。
5.3 有意思的发现:WebGPU 在移动端的潜力
实测里有个意料之外的结论:Adreno 730 上的 WebGPU 推理速度超过预期。10 秒视频在手机上端到端处理是 26 秒,桌面端是 19 秒,差距没有想象中大。原因可能是移动端 WebGPU 驱动的调度对 compute shader 的优化比较激进,而且灵狐模型本身的计算量不算大(约 2 GFLOPs 每帧),GPU 频率的劣势被小分辨率掩盖了。
这给了一个启发:端侧轻量化模型的最大受益场景其实不是桌面,而是手机。毕竟大多数老视频的原始素材就是手机拍的,在手机上完成修复,省去文件传电脑这步,体验闭环了。
6. 踩坑实录与其他经验补充
6.1 别碰的坑:动态分辨率输入
早期版本我想让模型接受任意分辨率输入,省去预处理缩放。结果 ONNX Runtime Web 的 WebGPU 后端对动态 shape 支持极差,每次输入尺寸变化都会触发 shader 重新编译,推理性能直接下降一个数量级。后面我固定成 480p 输入 + 2 倍超分输出,问题才消失。如果你确实要让用户输入任意尺寸,建议服务端和端侧都做一次“等比例缩放到最近支持尺寸”的预处理。
6.2 别忽视的细节:浏览器标签页后台休眠
当用户把标签页切到后台,浏览器会暂停requestAnimationFrame和定时器,但 WebGPU 的计算任务通常不受影响。本来这是好事,但测试时发现一个诡异现象:切换标签页后重新回来,页面的视频处理居然卡住不动了。排查后发现是我用requestAnimationFrame驱动事件循环,而它被后台挂起了。解决方案是改用setInterval加document.visibilityState检查,或者干脆用 Web Worker 里的MessageChannel驱动。
6.3 输出视频的编码参数选择
修复完的视频最终还是要导出。我在编码时走了不少弯路,最终固定为:
--codec=h264 --profile=high --level=4.1 --crf=18 --preset=slowCRF 18 是视觉无损阈值;preset=slow编码时间略长,但能显著减少色带(banding)和块效应。如果用户对体积有严格要求,可以放宽到 CRF 23,但我不建议再高,否则修复出来的细节会被二次压缩吃掉,等于白做。
6.4 ONNX 算子替代的实战案例
WebGPU 后端对 ONNX 算子的支持正在逐步完善,但仍有一些算子缺失。我遇到最多的三个:
GridSample:光流 warp 的标配算子,WebGPU 后端不支持,用ConvTranspose2d + affine_grid替代或者改写为Resize。ScatterND:某些通道混合场景用到,可以用分步Split + Concat绕开。InstanceNormalization:部分模型架构里有,但 WebGPU 后端支持不全,尽量在导出前把 IN 层 fold 掉。
遇到算子不支持,最粗暴的办法是逐个替换并重新导出 ONNX,但更高效的方式是在设计网络结构时就避开这些算子。所以灵狐模型在最初设计时,就不包含这些我在导出阶段踩过坑的层。
做完整套折腾,我个人的体会是:视频修复模型进浏览器,技术天花板是真实存在的,但它已经足够落地,不再是 demo 级别的玩具。云端大模型保住了上限画质,端侧轻量化则把可用性拉到了另一个维度。如果你也在做类似的事,建议先扎实解决模型轻量化的问题,别急着碰 WebGPU 的炫技功能。你平时是怎么处理 WebCodecs 和视频帧管线的?欢迎在评论区聊聊,踩过的坑一起排一排。