项目地址:MartinDelophy/ai-video-editorundefined在线体验:Timeline Studio
传统的视频高清放大通常依赖云端 GPU:用户上传视频,服务端逐帧推理,完成后再下载结果。
这套方案成熟,但也带来几个问题:素材需要上传、等待时间不透明、服务器成本高,而且私人视频存在隐私顾虑。
在 Timeline Studio 中,我们尝试了另一条路线:把视频解码、AI 超分辨率推理、音轨保留和 MP4 合成全部放进浏览器,通过 NanoVSR、ONNX Runtime Web 和 WebGPU 完成 4× 高清修复。
整个过程中,视频画面不会上传到服务器。
本文不只介绍模型如何运行,也会重点讨论几个真正影响浏览器端视频 AI 产品体验的问题:
相关实现可以直接在仓库中查看:
最容易实现的方案,是把视频拆成图片,然后让单图超分模型独立处理每一帧。
问题在于,图片模型只知道“当前这一帧长什么样”,不知道上一帧和下一帧发生了什么。模型生成的纹理可能每帧略有不同,静态截图看起来很清晰,连续播放时却会出现:
因此,Timeline Studio 为图片和视频使用了不同的 ONNX 图:
场景 | 输入张量 | 输出张量 | 时间窗口 |
|---|---|---|---|
图片 |
|
| 1 帧 |
视频 |
|
| 5 帧 |
视频会以五帧为一组执行推理,让模型同时观察相邻画面。这样做不能消除所有时序问题,但相比五次互不相关的单帧推理,能明显改善连续播放时的稳定性。
项目使用的是 NanoVSR 644K 的 FP16 ONNX 导出版本。模型权重内部采用 FP16,而浏览器输入输出保持 FP32,以兼容 ONNX Runtime WebGPU 的执行路径。
模型说明和校验值也保存在仓库中:
Timeline Studio 的高清修复链路可以概括为:
视频片段
↓
按时间定位并解码帧
↓
每 5 帧组成一个时序窗口
↓
缩放并 contain 到 320×180
↓
转换为 NCTHW Float32 张量
↓
NanoVSR ONNX + WebGPU 推理
↓
得到 4× RGB 输出
↓
裁掉模型输入时产生的黑边
↓
每帧生成 PNG
↓
FFmpeg.wasm 编码 H.264
↓
截取并重新混入原始音轨
↓
生成新的 MP4 素材这里有一个重要的产品约束:高清修复不会直接覆盖原素材。
用户首先生成预览,通过前后对比分界线检查结果;只有明确点击“应用结果”,新视频才会进入“我的资产”并替换当前时间线片段。原始素材和处理前的片段信息仍然保留,可以随时关闭增强结果或恢复原片。
AI 推理如果直接运行在 React 主线程,会与预览播放、时间线拖动和界面渲染争抢执行时间。
Timeline Studio 因此把 ONNX Runtime 放进独立的 Web Worker:
worker = new Worker(
new URL("../workers/nanovsr.worker.js", import.meta.url),
{ type: "module" },
);主线程负责:
ImageBitmap;Worker 负责:
ImageBitmap 通过 transferable objects 传给 Worker,避免一次额外的像素数据复制:
activeWorker.postMessage(
{
type: "enhance",
requestId,
bitmaps,
outputCount,
modelSourcePreference,
},
bitmaps,
);完成渲染后,Worker 会主动关闭已经使用过的 Bitmap,并释放输出 Tensor:
bitmaps[index]?.close?.();
result.sr.dispose?.();浏览器视频处理中的很多崩溃并不来自模型本身,而是来自没有及时释放的帧对象、Canvas 或 GPU Tensor。对长视频来说,资源生命周期管理与推理速度同样重要。
项目默认以 12 fps 处理视频:
const totalFrames = Math.ceil(sourceDuration * 12);随后按五帧分组:
const nextGroup = Math.floor(index / 5) * 5;每次只执行以下步骤:
<video> 创建五个 ImageBitmap;最后一组不足五帧时,会用最后一张可用画面补齐模型的时间窗口,但只输出真实需要的帧数。
这是一种面向浏览器资源限制的折中:
12 fps 同样是一项工程取舍。它不等于原视频的播放帧率,而是当前高清修复管线的处理帧率。较低的采样率可以显著降低浏览器推理和编码压力,更适合本地原型与中短视频。
NanoVSR 模型的输入尺寸是 320×180,但用户素材可能是竖屏、方形、电影宽屏或其他比例。
如果直接拉伸到 320×180,人物和物体会发生几何变形。因此项目采用 contain fitting:
const scale = Math.min(
INPUT_WIDTH / width,
INPUT_HEIGHT / height,
);原图按比例缩放后居中绘制,空余区域填黑。模型输出 1280×720 后,再根据输入阶段记录的矩形裁掉黑边:
原始画面
↓ 等比缩放
320×180 模型画布
↓ 4× 推理
1280×720 模型输出
↓ 按原 contain 矩形裁切
保持原始宽高比的高清结果这种方法避免了画面变形,也不会为了适配模型而裁掉原素材的上下或左右内容。
4× 超分并不意味着所有输出都必须强制变成模型裁切区域的四倍。
如果源素材本身的分辨率已经高于模型能够有效恢复的尺寸,先缩小到 320×180,再输出 4×,反而可能损失原有细节。
Timeline Studio 会比较源图像素数量和模型有效输出区域:
const protectSourceResolution =
sourceBitmap.width * sourceBitmap.height >
modelWidth * modelHeight;当源素材更大时,结果会回到源分辨率,并把模型恢复结果与原始画面进行保守混合,同时加入少量由原图计算的高频细节:
target =
modelResult * 0.12
+ original * 0.88
+ protectedDetail;这不是单纯追求更强的锐化,而是在避免两个问题:
因此,这条管线更准确的描述是“面向低清素材的 4× 恢复,同时保护较大源素材”,而不是无条件把所有视频都输出成四倍尺寸。
浏览器 AI 的第一次体验,很大程度取决于模型是否能顺利下载。
Timeline Studio 同时支持 Hugging Face 和 ModelScope:
但镜像不能产生两份逻辑缓存。
无论模型从哪个站点下载,最终都使用与镜像 URL 无关的内部缓存键:
const cacheKey =
`/__model-cache__/haixin/timeline-studio-onnx-models/`
+ `${MODEL_REVISION}/${modelPath}`;这样即使用户第一次从 ModelScope 下载,下一次路由切换到 Hugging Face,浏览器仍然能够识别为同一个模型。
生产模型也没有指向可变的 main 分支,而是锁定到不可变 revision:
const MODEL_REVISION =
"d551be137b16ecdf12637387f2fb4776565e763f";固定版本可以避免远端文件更新后出现这些问题:
模型下载完成后会写入 Cache Storage。Worker 和两个 ONNX Session 也会继续保留:图片模型和视频模型分别初始化一次,后续重复生成不再表现为重新下载和重新初始化。
模型输出的只是图像帧,而用户期待的是一个可以继续编辑、预览和导出的视频文件。
Timeline Studio 会先把每一帧转换为 PNG Blob,然后交给 FFmpeg.wasm 编码:
-framerate 12
-i frame-%06d.png
-c:v libx264
-preset veryfast
-crf 18
-pix_fmt yuv420p
-movflags faststart如果原视频包含音频,还会读取原始 Blob,根据片段的 sourceStart 和 sourceDuration 截取对应音轨:
-ss <sourceStart>
-t <sourceDuration>
-i <original-video>
-map 0:v:0
-map 1:a?
-c:a aac
-b:a 192k
-shortest最终输出为:
yuv420p 像素格式;这一步解决了一个常见的“演示可用、产品不可用”问题:很多浏览器 AI Demo 能展示增强后的 Canvas,却不能把它变成一个带音频、可继续剪辑的标准视频文件。
视频高清修复包含多个耗时性质完全不同的阶段:
项目让 Worker 和编码层发送结构化进度消息:
{
progress,
phaseKey,
frameIndex,
totalFrames,
backend: "webgpu",
}界面既显示当前阶段,也显示真实帧数:
GPU 正在恢复画面细节
已处理 37 / 120 帧这比单独显示一个不断跳动的百分比更有解释力。特别是在第一次运行时,下载模型和编译 WebGPU 图可能长时间没有画面输出,如果没有阶段说明,用户很容易认为程序已经卡死。
所有阶段文案都通过国际化 key 输出,而不是把中文提示写死在处理库中。
浏览器端视频处理可能持续几十秒甚至更久,所以“取消”不能只是关闭弹窗。
Timeline Studio 使用 AbortController 贯穿任务:
const controller = new AbortController();取消后会同时发生:
需要说明的是,WebGPU 的单次 session.run() 通常不能从 JavaScript 中间抢占。取消发生在一次推理期间时,系统会等待当前 GPU 调用返回,但会丢弃其结果,不再开始下一组。
这也是为什么视频必须分组处理:较小的推理批次不仅控制内存,也让取消能够更快在组与组之间生效。
高清修复不是一个应该自动提交的操作。
模型可能改善低清纹理,也可能对文字、细线或已经锐化过的画面产生不理想结果。因此项目提供了同步的前后对比界面:
应用后,片段中会同时记录原始素材和处理结果:
enhancement: {
mode: "nanovsr-644k",
enabled: true,
original,
processed,
backend: "webgpu",
frameRate,
totalFrames,
}这让高清修复成为非破坏性编辑状态,而不是一次无法撤销的文件覆盖。
这套浏览器管线已经可以完成端到端的本地视频高清修复,但它仍有明确边界。
第一,模型固定在 320×180 的推理输入上。4× 描述的是模型内部缩放倍率,不代表任何源视频最终都能获得四倍于原尺寸的真实细节。
第二,目前默认以 12 fps 生成结果。对于高速运动、体育视频或高帧率素材,更低的处理帧率可能带来运动不够顺滑的问题。
第三,编码前会暂存生成的 PNG Blob,并写入 FFmpeg.wasm 的虚拟文件系统。这个方案实现直接、兼容性较好,但长视频会产生明显的内存压力。
更进一步的方向是:
VideoFrame 代替 PNG 作为中间表示;第四,当前要求浏览器支持 WebGPU。项目会给出明确错误,而不会偷偷切换到一个速度很慢、效果不同的 CPU 路径。
回头看,这个功能最值得复用的并不是某一段 NanoVSR 调用代码,而是整条浏览器媒体 AI 管线的分层方式:
React 产品层
├─ 预览、应用、撤销与资产管理
├─ 进度、错误和取消
└─ 前后对比
媒体调度层
├─ 视频定位与帧采样
├─ 五帧分组
├─ 音轨截取
└─ MP4 合成
AI Worker
├─ 模型下载与缓存
├─ ONNX Session 复用
├─ 张量预处理
├─ WebGPU 推理
└─ 输出帧渲染
模型分发层
├─ Hugging Face
├─ ModelScope
├─ 固定 revision
└─ 镜像无关的缓存身份这种结构也适合浏览器端去水印、抠图、降噪、插帧和人像增强:UI 不需要知道模型张量细节,Worker 不需要知道时间线如何保存,编码器也不需要理解 React 状态。
把视频超分辨率放进浏览器,真正困难的部分并不只是“让 ONNX 模型在 WebGPU 上跑起来”。
一个可以进入视频编辑器的完整功能,还需要同时解决:
Timeline Studio 目前已经把这条链路完整串了起来:视频留在本地,NanoVSR 在 WebGPU 上按五帧窗口运行,输出经过浏览器内编码重新变成带音轨的 MP4,用户确认后才进入时间线。
如果你也在做浏览器 AI、WebGPU、视频编辑或本地优先应用,欢迎查看源码、提交 Issue 或参与改进:
如果这个项目对你有帮助,也欢迎在 GitHub 点一个 Star。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。