Apr 28, 2025
2 min read
Rust,
pytorch,
candle,
typescript,
wasm,

从零开始构建手写输入法:修成正果篇

我们通过 wasm-pack 编译出带 wasm 与模型权重的 npm 包后,就可以应用在前端项目上了。

这里主要讲一些关键的细节。

由于 模型加载和模型推理是比较耗时的操作,因此我们需要把这两个操作放到 web worker 进程里去做。

同时,我们还需要一个单例,让模型只在第一次加载。

import init, { Model } from "ochw-wasm"
// 实现一个 HandingWrite 类的单例
export class HandingWrite {
    private static instance: Model | undefined;
    private constructor() { }
    public static async getInstance(): Promise<Model> {
        if (!this.instance) {
            await init(wasmUrl)
            self.postMessage({ status: `loading model` });
            this.instance = Model.new()
            return this.instance;
        } else {
            return this.instance;
        }
    }
}

然后再监听来自主线程的事件,接收图片数据,并进行模型推理:

// 监听来自主线程的消息事件
self.addEventListener("message", async (event: MessageEvent) => {
    try {
        // 获取 HandingWrite 类的单例实例,确保模型已加载
        const model = await HandingWrite.getInstance();

        // 从消息事件中提取 uint8Array 数据,用于模型预测
        const { uint8Array } = event.data;

        // 使用模型对输入的 uint8Array 数据进行预测
        const res = model.predict(uint8Array);

        // 将预测结果解析为 JSON 格式,并通过 postMessage 返回给主线程
        self.postMessage({
            status: "complete", // 标记任务完成
            output: JSON.parse(res), // 返回预测结果
        });
    } catch (e) {
        // 如果发生错误,捕获异常并通过 postMessage 返回错误信息
        self.postMessage({ error: `worker error: ${e}` });
    }
});

同时在 web 页面上,初始化这个 worker

const worker = new Worker(new URL("./worker.js", import.meta.url), {
  type: "module",
});

用户每写一次,都在画布上获取图片,发送到工作进程进行推理:

	const stage = e.target.getStage();
    if (stage) {
      const blob: Blob = (await stage.toBlob()) as Blob;
      const arrayBuffer = await blob.arrayBuffer();
      const uint8Array = new Uint8Array(arrayBuffer);
      // console.log(uint8Array)
      worker.postMessage({
        uint8Array,
        width: stage.width(),
        height: stage.height(),
      });
      //  debug:下载图片
      // const dataURL = stage.toDataURL({
      //   pixelRatio: 1, // double resolution
      // });

      // create link to download
      // const link = document.createElement("a");
      // link.download = "stage.png";
      // link.href = dataURL;
      // document.body.appendChild(link);
      // link.click();
      // document.body.removeChild(link);
    }

我们还要获取 web worker 的结果:

  const [candidateWords, setCandidateWords] = useState<CandidateWord[]>([]);

  useEffect(() => {
    worker.onmessage = (e) => {
      // console.log(e.data);
      if (e.data && e.data.status =='complete') {
        setCandidateWords(e.data.output);
      }
    };
  }, []);

问题

目前此模型还有一些问题。

比如,一些简单的笔划的字反而无法识别, 说明数据集的处理有一些问题。还需要做一些消融实验去改进识别效果。

如果你对《从零开始构建手写输入法》 代码感兴趣,可以访问 : https://github.com/ximeiorg/ochw , 麻烦给个星。

如果你想访问效果,请访问: https://ochw.ximei.me

如果你在 PC 访问发现识别此变差,那么有可能是因为,画布因为 PC 变形,长宽比不一样,导致输入模型的尺寸差异问题而精度下降。