尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

浏览器内置AI实战:六天开发视频翻译与网页摘要插件

浏览器内置AI实战:六天开发视频翻译与网页摘要插件 浏览器内置AI能力开放之后我第一时间想到的不是做聊天机器人而是解决两个每天都在消耗我时间的问题看外语视频要来回切窗口查词以及读长文要自己划重点。于是就有了这个从零到上架的插件项目——视频翻译加网页摘要全程六天。下面把整个过程中的技术选型、踩坑记录和实操细节完整拆出来给同样想动手的人一个可复现的参考。1. 为什么我盯上了浏览器内置AI而不是自己搭后端1.1 一个反直觉的结论省掉的不只是服务器钱大多数人做翻译或摘要类插件第一反应是接云端大模型API。我一开始也是这么想的但算了一笔账之后改了主意。云端方案意味着你要处理密钥管理、请求转发、跨域、限流、计费还要考虑用户隐私——视频字幕和网页正文都是相当敏感的内容用户凭什么信任你把整页文字传到你的服务器浏览器内置AIChrome 138 和 Edge 稳定版都已支持把模型跑在本地通过window.ai或LanguageModel接口调用。这意味着三件事同时成立零服务器成本、零数据外传、零网络延迟。对于翻译和摘要这种单次输入几千字以内的任务本地小模型的质量已经够用尤其是摘要任务用户要的是帮我抓住重点不是写出完美文风。提示内置AI的可用性依赖浏览器版本和硬件。Chrome 需要 138 以上且开启相关 flag 或处于稳定通道Edge 在 153 版本之后对 Copilot 侧边栏做了调整但底层 AI API 依然可用。开发前先用一行代码探测能力别假设它一定存在。1.2 内置AI到底能做什么、不能做什么内置AI接口目前主要提供几类能力文本摘要Summarizer、语言检测LanguageDetector、翻译Translator、以及通用的语言模型对话LanguageModel / Prompt API。我实测下来摘要和翻译这两个专用接口比通用对话接口更稳因为它们内部做了针对性的提示词封装和输出约束。不能做的事也很明确它不适合长上下文推理不适合需要外部知识的问题也不适合高并发批量处理。所以我的插件定位很清晰——单次、短文本、本地、即时。视频翻译走的是提取字幕→分段→逐段翻译网页摘要走的是提取正文→截断→摘要都是典型的短任务。1.3 六天时间线是怎么分配的很多人好奇六天够不够。我的实际分配是这样的第一天做能力探测和原型验证确认内置AI在目标浏览器上能跑通第二天搭插件骨架manifest v3、popup、content script 三件套第三天做视频字幕提取这是最耗时的一块第四天接翻译和摘要逻辑第五天做UI和错误处理第六天打包、写说明、提交商店。真正写业务逻辑的时间其实只有两天剩下都在处理浏览器兼容和边界情况。2. 插件骨架搭建manifest v3 下最容易忽略的三个细节2.1 权限声明要克制否则审核和用户都劝退manifest v3 的权限模型比 v2 严格得多。我最初图省事写了host_permissions: [all_urls]结果本地调试时浏览器一直弹权限警告用户体验极差。后来改成按需申请只在用户点击插件图标时才通过chrome.permissions.request动态申请当前标签页的访问权限。{ manifest_version: 3, name: 视频翻译与网页摘要, version: 1.0.0, permissions: [activeTab, scripting, storage], action: { default_popup: popup.html }, background: { service_worker: background.js } }activeTab这个权限很关键它让你在用户主动触发时临时获得当前标签页的访问权不需要声明全站权限。审核通过率会高很多用户安装时看到的权限提示也温和得多。2.2 service worker 的生命周期是个坑manifest v3 把后台页换成了 service worker它会在空闲约30秒后被浏览器回收。我第一版把翻译状态存在全局变量里结果用户切个标签页回来状态全丢了。正确做法是把所有需要持久化的状态写进chrome.storage.session或chrome.storage.local。// 错误示范全局变量会被回收 let translationCache {}; // 正确做法写入 session storage async function cacheTranslation(key, value) { const store await chrome.storage.session.get(cache) || { cache: {} }; store.cache[key] value; await chrome.storage.session.set(store); }chrome.storage.session是内存存储浏览器关闭就清空适合放临时状态chrome.storage.local是持久化的适合放用户配置。分清楚这两个能省掉大量为什么我的数据没了的调试时间。2.3 content script 注入时机决定成败视频网站的字幕元素往往是异步加载的如果你在document_idle时注入 content script 就去抓字幕大概率抓不到。我的做法是注入后先做一次探测如果没找到目标元素就用MutationObserver监听 DOM 变化等字幕容器出现再执行提取。function waitForElement(selector, timeout 10000) { return new Promise((resolve, reject) { const existing document.querySelector(selector); if (existing) return resolve(existing); const observer new MutationObserver(() { const el document.querySelector(selector); if (el) { observer.disconnect(); resolve(el); } }); observer.observe(document.body, { childList: true, subtree: true }); setTimeout(() { observer.disconnect(); reject(new Error(超时)); }, timeout); }); }这个waitForElement工具函数后来被我复用了无数次几乎所有涉及动态页面的抓取都靠它。超时时间设10秒是个经验值太短容易误判太长用户等得烦躁。3. 视频字幕提取比想象中麻烦得多的那一环3.1 字幕来源有三条路各有各的坑视频字幕的获取方式主要有三种一是页面已经渲染出来的字幕DOM节点二是视频文件内嵌的字幕轨道三是通过语音识别Whisper从音频生成。我三条路都试过最后插件里主要走第一条第二条作为补充第三条留给没有字幕的视频。DOM提取最简单直接读字幕容器的文本就行但问题是很多网站的字幕是逐句替换的你需要持续监听才能收集完整。内嵌字幕轨道可以通过video.textTracks拿到但跨域视频往往拿不到 cue 数据。Whisper 方案质量最好但最重需要本地跑模型不适合塞进插件里。3.2 用 MutationObserver 收集滚动字幕的完整实现以常见的逐句字幕为例字幕容器里的文本会不断被替换。我的策略是监听容器的characterData和childList变化每次变化时读取当前文本和上一条比对不同就追加到数组里。function collectSubtitles(container) { const lines []; let last ; const observer new MutationObserver(() { const text container.textContent.trim(); if (text text ! last) { lines.push(text); last text; } }); observer.observe(container, { childList: true, subtree: true, characterData: true }); return { lines, stop: () observer.disconnect() }; }这里有个细节字幕经常会有短暂的空白帧如果不过滤空字符串收集到的数组里会夹杂大量空行翻译时白白浪费调用次数。加一个text 判断就能解决。3.3 时间轴对齐为什么我最后放弃了精确同步理想情况下翻译后的字幕应该和原视频时间轴精确对齐原字幕出现时译文同步出现。我尝试过读取每条字幕的时间戳但不同网站的时间戳格式五花八门有的用>async function translateText(text, sourceLang, targetLang) { const availability await Translator.availability({ sourceLanguage: sourceLang, targetLanguage: targetLang }); if (availability unavailable) { throw new Error(该语言对不支持); } const translator await Translator.create({ sourceLanguage: sourceLang, targetLanguage: targetLang, monitor(m) { m.addEventListener(downloadprogress, (e) { console.log(模型下载中${Math.round(e.loaded * 100)}%); }); } }); return await translator.translate(text); }第一次使用某个语言对时浏览器需要下载对应的语言模型可能几十到几百MB。downloadprogress事件一定要监听否则用户会以为插件卡死了。我在UI上做了进度条体验好很多。4.2 分段翻译为什么不能整段丢进去内置翻译接口对单次输入长度有限制而且长文本翻译质量会下降。我的做法是按标点符号把字幕切成句子每句单独翻译再拼回去。切分时要注意保留标点否则译文会丢失语气。function splitSentences(text) { return text .split(/(?[.!?。])\s*/) .map(s s.trim()) .filter(Boolean); }这个正则用了后行断言按句末标点切分且保留标点。实测下来单句翻译的准确率明显高于整段翻译尤其是中英互译时长句容易翻得前言不搭后语。4.3 Summarizer 接口的参数调优摘要接口有几个关键参数typetldr / key-points / teaser / headline、lengthshort / medium / long、formatplain-text / markdown。我默认用key-pointsmediummarkdown输出的是带项目符号的要点列表最符合快速抓重点的需求。const summarizer await Summarizer.create({ type: key-points, length: medium, format: markdown, sharedContext: 这是一篇技术文章请提取核心技术要点 }); const summary await summarizer.summarize(pageText);sharedContext这个参数很多人忽略但它对摘要质量影响很大。给模型一个上下文提示比如这是技术文章或这是新闻报道摘要的侧重点会明显不同。4.4 网页正文提取Readability 算法还是自己写摘要的前提是拿到干净的正文。我直接用了 Mozilla 的 Readability 库就是 Firefox 阅读模式背后的那个它能自动识别文章主体去掉导航、广告、评论。比自己写正则靠谱得多。import { Readability } from mozilla/readability; const article new Readability(document.cloneNode(true)).parse(); const cleanText article ? article.textContent : document.body.innerText;注意要传document.cloneNode(true)因为 Readability 会修改 DOM直接传原文档会破坏页面。这个坑我踩过页面被改得面目全非排查了半天。5. 调试与兼容那些文档里不会写的问题5.1 内置AI接口在不同浏览器上的差异Chrome 和 Edge 虽然都基于 Chromium但内置AI的接口暴露情况不完全一致。Chrome 上window.ai和顶层Translator、Summarizer都可能存在Edge 上则可能只暴露部分。我的做法是写一个能力探测函数运行时动态判断而不是硬编码。async function detectCapabilities() { const caps { translator: false, summarizer: false, languageModel: false }; if (typeof Translator ! undefined) { caps.translator (await Translator.availability({ sourceLanguage: en, targetLanguage: zh })) ! unavailable; } if (typeof Summarizer ! undefined) { caps.summarizer (await Summarizer.availability()) ! unavailable; } return caps; }探测结果决定UI上哪些按钮可用。不支持的功能直接置灰并给出提示比让用户点了报错体验好得多。5.2 扩展加载失败的常见原因排查开发阶段用chrome://extensions/或edge://extensions/加载未打包扩展时最常见的失败原因是 manifest 格式错误和文件路径不对。我的排查顺序是先看扩展页面的错误提示再看 service worker 的控制台最后看目标页面的控制台。一个特别隐蔽的坑manifest 里引用的图标文件如果不存在扩展会直接加载失败但错误提示很模糊。建议图标先用占位图跑通之后再替换。5.3 内存占用与性能优化内置AI模型加载后会占用一定内存Edge 本身内存占用就不低加上模型之后更明显。我的优化策略是翻译器和摘要器用完就释放不要常驻。// 用完销毁释放内存 await translator.destroy(); await summarizer.destroy();另外摘要任务不要对整页文本直接跑先用 Readability 提取正文再截断到合理长度我设的是8000字符超出部分丢弃。这样既省内存又快。6. 从本地跑通到提交商店的最后一公里6.1 打包前的自检清单提交之前我列了一个清单逐项过manifest 版本号、图标尺寸16/48/128三档、权限是否最小化、是否有硬编码的调试代码、隐私政策说明因为涉及内容处理必须声明数据不外传。隐私政策这块很多人偷懒不写但涉及文本处理的插件基本都会被要求提供。6.2 商店审核被拒的两个真实原因我第一次提交被拒原因是权限说明不充分。审核方要求解释每个权限的用途activeTab和scripting都要写清楚为什么需要。第二次被拒是因为描述里用了翻译这个词但没说明翻译在本地完成被怀疑数据外传。补充说明所有处理均在本地浏览器内完成不上传任何数据之后就通过了。提示涉及AI处理的插件审核时一定要主动声明数据处理位置。本地处理是你的卖点也是过审的关键。6.3 用户反馈里最有价值的一条上架后收到一条反馈让我印象很深用户说希望摘要结果能一键复制成纯文本。我原本输出的是 markdown复制到某些编辑器里会带格式符号。后来加了一个复制纯文本按钮把 markdown 符号去掉再复制。这种细节文档里不会写只有真实用户会告诉你。7. 如果重来一次我会怎么调整内置AI这条路走下来最大的感受是它适合做轻量、即时、隐私敏感的任务不适合做重推理。视频翻译和网页摘要恰好卡在这个甜区里。如果重来我会更早做能力探测而不是先写业务逻辑再发现接口不支持也会更早把状态管理从全局变量改成 storage省掉那些莫名其妙的丢数据问题。另外Whisper 本地识别这块我目前只是留了入口后续想做成半自动流程——插件检测到无字幕视频时自动提取音频并给出本地处理指引用户处理完把结果贴回来。这样既保持了插件轻量又补上了无字幕视频的短板。字幕时间轴同步我也没完全放弃打算先针对一两个主流站点做深度适配验证效果再考虑扩展。
返回列表