WebGPU ,在浏览器里跑跑算法

首先 WebGPU 的兼容性要求比较高,桌面端依赖 Chrome 113 等以上版本,具体的兼容性列表见:WebGPU_API#浏览器兼容性

如何使用

在大部分情况下,我们只需要在前端调用前人已经训练好的模型就可以了,感谢 HuggingFace 和 Onnx,我们可以基于一套通用的框架来实现对模型的调用。

也许你使用过或者听说过 Transformers.js,不过目前他的正式版本(2.7.x)还没有正式支持 WebGPU,如果想要使用,你需要安装 npm i @xenova/transformers#v3,这会直接把对应的仓库分支安装到你的项目下。

如果你想要通过 external 的形式导入,那么你需要手动 clone xenova/transformers.js,然后在本地切换到 v3 分支,并且执行 npm install && npm run build

这会在 dist 目录下产生打包后的文件,只需要把 transformers.min.js 拿出来使用即可。

跑个例子

目前 huggingFace 或者其他地方已经有许多人开始将热门的模型搬运到 Web 上,可以参考

或者直接看 transformers.js 的官方仓库,也有非常多的例子,记得切换到 v3 分支。

又或者可以看看我这两天的学习成果,也是基于 RMBG-1.4 的一个例子,代码在 Github, 使用了 Worker 来做异步计算。

注意事项

  1. 网络问题
    虽然我们可以通过env.allowLocalModels = false;直接加载 HuggingFace 的模型资源,但是 HuggingFace 位于国外,下载成功率较低,因此在大规模使用的情况下,建议将模型文件下载下来托管到自己的服务器或者CDN上。
  2. 下载速度问题
    模型在首次下载后将会缓存到浏览器的 Cache,二次加载时不会重复下载,因此下载速度主要影响用户首次进入的加载耗时。
  3. 算法速度问题
    尽管目前新版的浏览器已经有相当大的比例已经支持 WebGPU,但是还是会有环境存在不支持的情况,在这些场景下可以退化到 wasm 来保证基础的体验,速度的话可能慢几百倍,视具体的模型和任务而定。
  4. 手机能用吗
    Safari 桌面端和移动端均不支持,对于 Chrome Android 121 / WebView Android 121 以上都支持,如果你的移动设备在持续更新,那么多半是支持的(不包括手Q内置浏览器等第三方内核)。

前端下载文件的几种方式

阶段性总结一下前端可以用于文件下载的几种方法

1. a[href=*] 右键另存为

最原始的方式之一,在很多远古的网站上会存在,一般需要搭配后端对文件请求的 reponseHeader 设置 Content-Disposition ,否则用户直接点击的话可能会打开预览而不是文件下载。

如果服务器实现了 Content-Disposition 的话,前端也可以使用这些方式直接去打开对应的url:

  • location.href
  • window.open
  • iframe[src]
  • a[href].click

2. a[download='filename'] 使用 download 属性

现代 HTML5 规范提供了一种直接在链接声明文件可下载的方式,使用 a 标签的 download 属性,属性值将会作为下载文件的文件名,使用这个方法,结合 ObjectUrl,首次将前端生成文件并直接在网页上触发下载变成了可能。

在这个API的基础上终于能够做到直接在前端生成并下载 文本文件、excel 等文件,而无需后端服务参与。

上述两种方式都有一个比较基础和典型的实现:https://github.com/eligrey/FileSaver.js

3. ServiceWorker

ServiceWorker 对于全局请求的劫持,结合 Stream API, 使前端流式创建/下载文件变成了可能,在前端技术栈上可以做到流式下载一个超大文件,例如几个 G 大小的视频。

一个比较经典的实现来自: https://github.com/jimmywarting/StreamSaver.js

4. showSaveFilePicker

Chrome>86 的才可以使用的一个API,同样可以用于保存文件,属于 浏览器 FileSystemApi 的一部分,同样可以流式写入,缺点是不会产生浏览器原生的下载进度和记录,需要自己实现下载进度的相关交互界面。

QQ音乐-付费音乐包开通办法 (已失效)


至2024年7月23日,该文章已失效。


截至 2023年10月11日 , QQ Music 已经下线了包括 web、移动端在内的所有 付费音乐包 的开通链接(仅当前处于音乐包套餐的用户能通过移动端续费)。

新用户无法购买 8 元的音乐包,绿钻用户也无法单独续费音乐包。

不过经过研究,目前尚有方法能够拉起音乐包开通,且开且珍惜。

- 阅读剩余部分 -

使用 SVG 实现 Canvas 响应式缩放

很多时候我们需要做一个响应式的页面,如果页面中存在一个Canvas,调整尺寸的时候一般就需要重绘它。

在大部分情况下,操作 Canvas 并重绘并非不可接受,但是在一些场景中,我们既需要保持高分辨率的 Canvas,又需要它能适应屏幕的宽度,比如我们创建了一个 Canvas 用于捕捉屏幕上的视频流或者图片流,并且可能启动了一个 MediaRecorder 录制该 Canvas,在这个时候,强行修改Canvas的尺寸会影响正在录制的 MediaRecorder ,即便不在录制过程中,也会降低画面的分辨率,那么有什么其他方法吗?

做法一

使用一个新的 Canvas 来进行渲染,与原本的录制 Canvas 隔离。

我们可以将录制或者相关的业务逻辑放在另一个 Canvas中,可能是 offscreenCanvas。
然后我们在 requestAnimationFrame 或类似的时机,将 offscreenCanvas的内容绘制到实际显示的 Canvas 中。

这个方法的优势在于整体的数据流向不会变,缺点是额外绘制一个 Canvas 总是一种性能损耗,并且对上屏的 Canvas,我们依然要去监听 resize 并且重设 Canvas 的宽高。

做法二

仔细回顾一下我们的需求,如果要做到最完美的版本,即:我们需要有一个方案,他能够让 Canvas 全尺寸绘制并且不影响所有Canvas 相关的API,与此同时,它还要支持 CSS 的响应式逻辑。

没错,SVG。

SVG能够在 DOM 中创造一块独立的渲染逻辑,其中由 SVG 标签的 viewBOX 定义了整个渲染区域的坐标和尺寸,而在 DOM 中,它就像一个图片一样,可以使用 object-fit 或者 width 等 CSS 属性来实际控制它的尺寸和渲染方式。

于是在一个 Vue Template 中,我们可以这样写一个 Canvas:

<svg :viewBox="`0 0 ${loadedArea?.width} ${loadedArea?.height}`" xmlns="http://www.w3.org/2000/svg" class="w-full">
  <foreignObject x="0" y="0" :width="loadedArea?.width" :height="loadedArea?.height">
    <canvas ref="ImageCanvas" :height="loadedArea?.height" :width="loadedArea?.width" class="canvas"></canvas>
  </foreignObject>
</svg>
/src/tools/radar-chart/index.vue#L323-L328

使用 foreignObject ,并且把 SVG 的 viewbox 设成和 Canvas 相同的尺寸,就可以使 Canvas 完全铺满 SVG 的渲染空间。

最后,原有的逻辑完全不需要修改,依然可以通过 JS 去操作 Canvas的 API ,可以说是一种比较完美的解决方法。