首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >把 4× 视频高清修复搬进浏览器:Timeline Studio 的 NanoVSR + WebGPU 实践

把 4× 视频高清修复搬进浏览器:Timeline Studio 的 NanoVSR + WebGPU 实践

原创
作者头像
用户5557817
发布2026-07-28 14:12:09
发布2026-07-28 14:12:09
1450
举报

把 4× 视频高清修复搬进浏览器:Timeline Studio 的 NanoVSR + WebGPU 实践

项目地址:MartinDelophy/ai-video-editorundefined在线体验:Timeline Studio

传统的视频高清放大通常依赖云端 GPU:用户上传视频,服务端逐帧推理,完成后再下载结果。

这套方案成熟,但也带来几个问题:素材需要上传、等待时间不透明、服务器成本高,而且私人视频存在隐私顾虑。

在 Timeline Studio 中,我们尝试了另一条路线:把视频解码、AI 超分辨率推理、音轨保留和 MP4 合成全部放进浏览器,通过 NanoVSR、ONNX Runtime Web 和 WebGPU 完成 4× 高清修复。

整个过程中,视频画面不会上传到服务器。

本文不只介绍模型如何运行,也会重点讨论几个真正影响浏览器端视频 AI 产品体验的问题:

  • 如何处理视频,而不只是放大一张图片;
  • 如何利用相邻帧减少画面闪烁;
  • 如何控制显存、内存和主线程压力;
  • 如何缓存模型,避免用户每次重新下载;
  • 如何保留原始音轨并重新生成 MP4;
  • 如何让取消、进度和前后对比真正可用。

相关实现可以直接在仓库中查看:

为什么视频超分不能简单地逐帧调用图片模型

最容易实现的方案,是把视频拆成图片,然后让单图超分模型独立处理每一帧。

问题在于,图片模型只知道“当前这一帧长什么样”,不知道上一帧和下一帧发生了什么。模型生成的纹理可能每帧略有不同,静态截图看起来很清晰,连续播放时却会出现:

  • 纹理闪烁;
  • 细线抖动;
  • 字体和边缘忽明忽暗;
  • 压缩噪点被不一致地放大;
  • 人脸、头发等细节缺少时间连续性。

因此,Timeline Studio 为图片和视频使用了不同的 ONNX 图:

场景

输入张量

输出张量

时间窗口

图片

[1, 1, 3, 180, 320]

[1, 1, 3, 720, 1280]

1 帧

视频

[1, 5, 3, 180, 320]

[1, 5, 3, 720, 1280]

5 帧

视频会以五帧为一组执行推理,让模型同时观察相邻画面。这样做不能消除所有时序问题,但相比五次互不相关的单帧推理,能明显改善连续播放时的稳定性。

项目使用的是 NanoVSR 644K 的 FP16 ONNX 导出版本。模型权重内部采用 FP16,而浏览器输入输出保持 FP32,以兼容 ONNX Runtime WebGPU 的执行路径。

模型说明和校验值也保存在仓库中:

NanoVSR 浏览器模型说明

浏览器里的完整处理链路

Timeline Studio 的高清修复链路可以概括为:

代码语言:plaintext
复制
视频片段
  ↓
按时间定位并解码帧
  ↓
每 5 帧组成一个时序窗口
  ↓
缩放并 contain 到 320×180
  ↓
转换为 NCTHW Float32 张量
  ↓
NanoVSR ONNX + WebGPU 推理
  ↓
得到 4× RGB 输出
  ↓
裁掉模型输入时产生的黑边
  ↓
每帧生成 PNG
  ↓
FFmpeg.wasm 编码 H.264
  ↓
截取并重新混入原始音轨
  ↓
生成新的 MP4 素材

这里有一个重要的产品约束:高清修复不会直接覆盖原素材。

用户首先生成预览,通过前后对比分界线检查结果;只有明确点击“应用结果”,新视频才会进入“我的资产”并替换当前时间线片段。原始素材和处理前的片段信息仍然保留,可以随时关闭增强结果或恢复原片。

一、在主线程之外运行 WebGPU 推理

AI 推理如果直接运行在 React 主线程,会与预览播放、时间线拖动和界面渲染争抢执行时间。

Timeline Studio 因此把 ONNX Runtime 放进独立的 Web Worker:

代码语言:js
复制
worker = new Worker(
  new URL("../workers/nanovsr.worker.js", import.meta.url),
  { type: "module" },
);

主线程负责:

  • 读取当前片段;
  • 定位视频时间;
  • 创建 ImageBitmap
  • 接收进度;
  • 管理取消信号;
  • 保存最终素材。

Worker 负责:

  • 下载和缓存 ONNX 模型;
  • 创建 WebGPU 推理会话;
  • 图像预处理;
  • 张量构建;
  • 执行 NanoVSR;
  • 把输出转换为 PNG Blob。

ImageBitmap 通过 transferable objects 传给 Worker,避免一次额外的像素数据复制:

代码语言:js
复制
activeWorker.postMessage(
  {
    type: "enhance",
    requestId,
    bitmaps,
    outputCount,
    modelSourcePreference,
  },
  bitmaps,
);

完成渲染后,Worker 会主动关闭已经使用过的 Bitmap,并释放输出 Tensor:

代码语言:js
复制
bitmaps[index]?.close?.();
result.sr.dispose?.();

浏览器视频处理中的很多崩溃并不来自模型本身,而是来自没有及时释放的帧对象、Canvas 或 GPU Tensor。对长视频来说,资源生命周期管理与推理速度同样重要。

二、五帧一组,而不是一次把整段视频塞进显存

项目默认以 12 fps 处理视频:

代码语言:js
复制
const totalFrames = Math.ceil(sourceDuration * 12);

随后按五帧分组:

代码语言:js
复制
const nextGroup = Math.floor(index / 5) * 5;

每次只执行以下步骤:

  1. 定位到这一组中每一帧对应的时间;
  2. <video> 创建五个 ImageBitmap
  3. 把五帧送入 Worker;
  4. 运行一次时序模型;
  5. 返回这一组的 PNG;
  6. 继续处理下一组。

最后一组不足五帧时,会用最后一张可用画面补齐模型的时间窗口,但只输出真实需要的帧数。

这是一种面向浏览器资源限制的折中:

  • 一帧一帧处理,时序稳定性不足;
  • 整段一次处理,显存和内存不可控;
  • 五帧窗口能够利用时间信息,同时把峰值资源占用限制在较小范围内。

12 fps 同样是一项工程取舍。它不等于原视频的播放帧率,而是当前高清修复管线的处理帧率。较低的采样率可以显著降低浏览器推理和编码压力,更适合本地原型与中短视频。

三、兼容任意宽高比,而不是强制裁成 16:9

NanoVSR 模型的输入尺寸是 320×180,但用户素材可能是竖屏、方形、电影宽屏或其他比例。

如果直接拉伸到 320×180,人物和物体会发生几何变形。因此项目采用 contain fitting:

代码语言:js
复制
const scale = Math.min(
  INPUT_WIDTH / width,
  INPUT_HEIGHT / height,
);

原图按比例缩放后居中绘制,空余区域填黑。模型输出 1280×720 后,再根据输入阶段记录的矩形裁掉黑边:

代码语言:plaintext
复制
原始画面
  ↓ 等比缩放
320×180 模型画布
  ↓ 4× 推理
1280×720 模型输出
  ↓ 按原 contain 矩形裁切
保持原始宽高比的高清结果

这种方法避免了画面变形,也不会为了适配模型而裁掉原素材的上下或左右内容。

四、为什么还要保护已经足够大的源素材

4× 超分并不意味着所有输出都必须强制变成模型裁切区域的四倍。

如果源素材本身的分辨率已经高于模型能够有效恢复的尺寸,先缩小到 320×180,再输出 4×,反而可能损失原有细节。

Timeline Studio 会比较源图像素数量和模型有效输出区域:

代码语言:js
复制
const protectSourceResolution =
  sourceBitmap.width * sourceBitmap.height >
  modelWidth * modelHeight;

当源素材更大时,结果会回到源分辨率,并把模型恢复结果与原始画面进行保守混合,同时加入少量由原图计算的高频细节:

代码语言:js
复制
target =
  modelResult * 0.12
  + original * 0.88
  + protectedDetail;

这不是单纯追求更强的锐化,而是在避免两个问题:

  • 高分辨率素材被模型的固定输入尺寸降质;
  • AI 生成纹理过强,覆盖原图中本来就存在的真实细节。

因此,这条管线更准确的描述是“面向低清素材的 4× 恢复,同时保护较大源素材”,而不是无条件把所有视频都输出成四倍尺寸。

五、模型下载:双镜像、固定版本和统一缓存身份

浏览器 AI 的第一次体验,很大程度取决于模型是否能顺利下载。

Timeline Studio 同时支持 Hugging Face 和 ModelScope:

  • 中文界面或国内会话优先 ModelScope;
  • 其他环境默认优先 Hugging Face;
  • 首选源不可用时自动尝试另一个镜像;
  • 还会通过短时 HEAD 请求探测当前更快的模型源。

但镜像不能产生两份逻辑缓存。

无论模型从哪个站点下载,最终都使用与镜像 URL 无关的内部缓存键:

代码语言:js
复制
const cacheKey =
  `/__model-cache__/haixin/timeline-studio-onnx-models/`
  + `${MODEL_REVISION}/${modelPath}`;

这样即使用户第一次从 ModelScope 下载,下一次路由切换到 Hugging Face,浏览器仍然能够识别为同一个模型。

生产模型也没有指向可变的 main 分支,而是锁定到不可变 revision:

代码语言:js
复制
const MODEL_REVISION =
  "d551be137b16ecdf12637387f2fb4776565e763f";

固定版本可以避免远端文件更新后出现这些问题:

  • 缓存中是旧模型,代码却按新模型解释;
  • 输入输出节点或形状突然改变;
  • 无法复现用户反馈;
  • 两个镜像在同步过程中短暂不一致。

模型下载完成后会写入 Cache Storage。Worker 和两个 ONNX Session 也会继续保留:图片模型和视频模型分别初始化一次,后续重复生成不再表现为重新下载和重新初始化。

六、从 PNG 帧重新合成带声音的 MP4

模型输出的只是图像帧,而用户期待的是一个可以继续编辑、预览和导出的视频文件。

Timeline Studio 会先把每一帧转换为 PNG Blob,然后交给 FFmpeg.wasm 编码:

代码语言:bash
复制
-framerate 12
-i frame-%06d.png
-c:v libx264
-preset veryfast
-crf 18
-pix_fmt yuv420p
-movflags faststart

如果原视频包含音频,还会读取原始 Blob,根据片段的 sourceStartsourceDuration 截取对应音轨:

代码语言:bash
复制
-ss <sourceStart>
-t <sourceDuration>
-i <original-video>
-map 0:v:0
-map 1:a?
-c:a aac
-b:a 192k
-shortest

最终输出为:

  • H.264 视频;
  • AAC 192 kbps 音频;
  • yuv420p 像素格式;
  • faststart MP4;
  • 时长以修复后的视频帧为准。

这一步解决了一个常见的“演示可用、产品不可用”问题:很多浏览器 AI Demo 能展示增强后的 Canvas,却不能把它变成一个带音频、可继续剪辑的标准视频文件。

七、进度不能只是一个虚假的百分比

视频高清修复包含多个耗时性质完全不同的阶段:

  1. 准备视频帧;
  2. 下载模型;
  3. 读取缓存;
  4. 初始化 WebGPU;
  5. 执行模型推理;
  6. 生成高清帧;
  7. 加载浏览器编码器;
  8. 合成视频;
  9. 创建新素材。

项目让 Worker 和编码层发送结构化进度消息:

代码语言:js
复制
{
  progress,
  phaseKey,
  frameIndex,
  totalFrames,
  backend: "webgpu",
}

界面既显示当前阶段,也显示真实帧数:

代码语言:plaintext
复制
GPU 正在恢复画面细节
已处理 37 / 120 帧

这比单独显示一个不断跳动的百分比更有解释力。特别是在第一次运行时,下载模型和编译 WebGPU 图可能长时间没有画面输出,如果没有阶段说明,用户很容易认为程序已经卡死。

所有阶段文案都通过国际化 key 输出,而不是把中文提示写死在处理库中。

八、取消操作必须真正释放资源

浏览器端视频处理可能持续几十秒甚至更久,所以“取消”不能只是关闭弹窗。

Timeline Studio 使用 AbortController 贯穿任务:

代码语言:js
复制
const controller = new AbortController();

取消后会同时发生:

  • 主线程停止继续解码视频帧;
  • Worker 收到当前任务的取消标记;
  • 后续进度不再发送;
  • 已有 Bitmap 被关闭;
  • 结果不再写入资产库;
  • 如果已经进入 FFmpeg 编码阶段,直接终止 FFmpeg 实例;
  • 原始片段保持不变。

需要说明的是,WebGPU 的单次 session.run() 通常不能从 JavaScript 中间抢占。取消发生在一次推理期间时,系统会等待当前 GPU 调用返回,但会丢弃其结果,不再开始下一组。

这也是为什么视频必须分组处理:较小的推理批次不仅控制内存,也让取消能够更快在组与组之间生效。

九、先比较,再应用

高清修复不是一个应该自动提交的操作。

模型可能改善低清纹理,也可能对文字、细线或已经锐化过的画面产生不理想结果。因此项目提供了同步的前后对比界面:

  • 原视频和结果视频保持相同播放时间;
  • 拖动中间分界线查看同一帧;
  • 时间滑块可以定位到具体位置;
  • 图片和视频使用相同的前后对比逻辑;
  • 只有点击“应用结果”才创建新资产并替换片段。

应用后,片段中会同时记录原始素材和处理结果:

代码语言:js
复制
enhancement: {
  mode: "nanovsr-644k",
  enabled: true,
  original,
  processed,
  backend: "webgpu",
  frameRate,
  totalFrames,
}

这让高清修复成为非破坏性编辑状态,而不是一次无法撤销的文件覆盖。

当前实现的边界

这套浏览器管线已经可以完成端到端的本地视频高清修复,但它仍有明确边界。

第一,模型固定在 320×180 的推理输入上。4× 描述的是模型内部缩放倍率,不代表任何源视频最终都能获得四倍于原尺寸的真实细节。

第二,目前默认以 12 fps 生成结果。对于高速运动、体育视频或高帧率素材,更低的处理帧率可能带来运动不够顺滑的问题。

第三,编码前会暂存生成的 PNG Blob,并写入 FFmpeg.wasm 的虚拟文件系统。这个方案实现直接、兼容性较好,但长视频会产生明显的内存压力。

更进一步的方向是:

  • 使用 VideoFrame 代替 PNG 作为中间表示;
  • 通过 WebCodecs 流式编码;
  • 推理完成一帧就送入编码器;
  • 对解码、推理和编码建立有界队列;
  • 根据 GPU 能力动态调整时间窗口和处理帧率;
  • 用 tile 推理支持更高分辨率输入;
  • 引入重叠区域融合,减少分块边缘;
  • 加入光流或帧插值,恢复到更高输出帧率。

第四,当前要求浏览器支持 WebGPU。项目会给出明确错误,而不会偷偷切换到一个速度很慢、效果不同的 CPU 路径。

可以复用的架构经验

回头看,这个功能最值得复用的并不是某一段 NanoVSR 调用代码,而是整条浏览器媒体 AI 管线的分层方式:

代码语言:plaintext
复制
React 产品层
  ├─ 预览、应用、撤销与资产管理
  ├─ 进度、错误和取消
  └─ 前后对比

媒体调度层
  ├─ 视频定位与帧采样
  ├─ 五帧分组
  ├─ 音轨截取
  └─ MP4 合成

AI Worker
  ├─ 模型下载与缓存
  ├─ ONNX Session 复用
  ├─ 张量预处理
  ├─ WebGPU 推理
  └─ 输出帧渲染

模型分发层
  ├─ Hugging Face
  ├─ ModelScope
  ├─ 固定 revision
  └─ 镜像无关的缓存身份

这种结构也适合浏览器端去水印、抠图、降噪、插帧和人像增强:UI 不需要知道模型张量细节,Worker 不需要知道时间线如何保存,编码器也不需要理解 React 状态。

结语

把视频超分辨率放进浏览器,真正困难的部分并不只是“让 ONNX 模型在 WebGPU 上跑起来”。

一个可以进入视频编辑器的完整功能,还需要同时解决:

  • 时序一致性;
  • 视频帧解码;
  • GPU 和内存资源释放;
  • 模型分发与缓存;
  • 音视频重新合成;
  • 进度与取消;
  • 前后对比;
  • 非破坏性应用;
  • 浏览器兼容边界。

Timeline Studio 目前已经把这条链路完整串了起来:视频留在本地,NanoVSR 在 WebGPU 上按五帧窗口运行,输出经过浏览器内编码重新变成带音轨的 MP4,用户确认后才进入时间线。

如果你也在做浏览器 AI、WebGPU、视频编辑或本地优先应用,欢迎查看源码、提交 Issue 或参与改进:

如果这个项目对你有帮助,也欢迎在 GitHub 点一个 Star。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 把 4× 视频高清修复搬进浏览器:Timeline Studio 的 NanoVSR + WebGPU 实践
    • 为什么视频超分不能简单地逐帧调用图片模型
    • 浏览器里的完整处理链路
    • 一、在主线程之外运行 WebGPU 推理
    • 二、五帧一组,而不是一次把整段视频塞进显存
    • 三、兼容任意宽高比,而不是强制裁成 16:9
    • 四、为什么还要保护已经足够大的源素材
    • 五、模型下载:双镜像、固定版本和统一缓存身份
    • 六、从 PNG 帧重新合成带声音的 MP4
    • 七、进度不能只是一个虚假的百分比
    • 八、取消操作必须真正释放资源
    • 九、先比较,再应用
    • 当前实现的边界
    • 可以复用的架构经验
    • 结语
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档