基于浏览器的收据扫描器LiteRT.js
谷歌最近推出了LiteRT.js,一款用于运行设备内AI推理的浏览器运行时,使用WebAssembly和WebGPU。

LiteRT.js将谷歌的LiteRT运行时(前身为TensorFlow Lite)带到了网络上。它无需采用JavaScript专用的模型格式,而是可以在浏览器中直接运行标准模型,同时利用现代浏览器硬件加速。.tflite
在本教程中,我们将了解LiteRT.js的工作原理,并构建端到端光学字符识别(OCR)收据扫描器。该应用程序将预处理收据照片,本地检测和识别文本,重建文档布局,并将提取的文本通过LiteRT-LM传递给设备上的Gemma模型以结构化结果。
整个流程在浏览器本地运行,因此收据数据无需发送到外部推理 API。
前提条件
你需要:
-
Node.js 20岁或更晚
-
一个React和TypeScript项目,最好用Vite创建
-
如果你想用GPU加速,可以推荐支持WebGPU的浏览器
在我们构建扫描仪之前,先看看在浏览器中运行机器学习模型时LiteRT.js有哪些变化。
理解LiteRT.js
TensorFlow Lite 最初是为移动和嵌入式系统设计的修复:iPhone 键盘上缺少麦克风图标 - 致知笔记 - 专注教程知识分享,而 TensorFlow.js 则专为网络设计。
然而,随着浏览器获得 WebAssembly SIMD 和 WebGPU 等功能,原生推理与基于浏览器的推理差距逐渐缩小。随后,谷歌将TensorFlow Lite重新命名为LiteRT,作为其更广泛的AI边缘工具的一部分。
LiteRT 既是一个模型运行时,也是更广泛转换流程的一部分。源自 TensorFlow、PyTorch 和 JAX 等框架的模型最终可以部署到该格式,而 LiteRT.js 则将该运行时带到浏览器中。.tflite
对于网页应用,重要的区别在于LiteRT.js可以使用现代浏览器计算API执行模型,而不必要求模型针对特定的JavaScript执行环境。.tflite
LiteRT.js后端
LiteRT.js可以针对多种执行路径:
| 后端 | 职责 | 最适合 |
|---|---|---|
| WebAssembly + XNNPACK | CPU 执行与备份 | 广泛兼容性与CPU推理 |
| WebGPU | GPU加速计算 | 并行工作负载,如神经网络推理 |
| WebNN | 新兴硬件抽象 | 直接访问可用的机器学习加速器和NPU |
WebAssembly 和 XNNPACK
WebAssembly 提供浏览器端的执行环境,而 XNNPACK 则提供优化的神经网络操作符。
借助 SIMD 和多线程等浏览器功能,修复:天气应用在 iPhone 上无法正常工作 - 致知笔记 - 专注教程知识分享这为LiteRT.js提供了比直接在 JavaScript 中实现相同数值操作更快的 CPU 路径。
WebGPU
WebGPU 为浏览器应用程序提供了通用 GPU 计算的访问权限。LiteRT.js可以利用该能力在用户GPU上执行高度并行的操作,如矩阵乘法。
这避免了将主要为图形设计的WebGL作为通用计算API的许多限制。
WebNN
WebNN是一种新兴的神经网络加速浏览器API。在支持的情况下,旨在提供对设备可用机器学习硬件的访问,包括GPU和NPU。
项目的建立
我们先初始化React应用并安装依赖。
申请主要分为两个阶段:
-
一个检测和识别文本的OCR流水线
-
一个将OCR输出转换为结构化收据数据的LLM流水线
如果你还没创建项目,可以用Vite搭建一个React和TypeScript应用:
npm create vite@latest document-scanner -- --template react-ts
然后安装LiteRT.js:
npm install @litertjs/core
我们将用来传递TensorFlow.js和LiteRT.js之间的张量:@litertjs/tfjs-interop
npm install @litertjs/tfjs-interop
安装TensorFlow.js:
npm install @tensorflow/tfjs
然后添加它的WebGPU后端:
npm install @tensorflow/tfjs-backend-webgpu
最后,安装 LiteRT-LM 用于设备内语言模型:
npm install --save @litert-lm/core
最终的堆栈如下:
| 包装 | 目的 |
|---|---|
| @litertjs/core | 加载和执行模型.tflite |
| @litertjs/tfjs-interop | TensorFlow.js与LiteRT.js共享张量 |
| @tensorflow/tfjs | 张量操作与支持图像操作 |
| @tensorflow/tfjs-backend-webgpu | WebGPU 后端支持TensorFlow.js |
| @litert-lm/core | 运行设备内语言模型 |
添加OCR和LLM模型
OCR流水线需要三个文件:
-
文本检测模型
-
文本识别模型
-
一个将识别输出类映射回字符的词典
你可以在GitHub仓库中找到OCR模型、词典和完整项目。
Gemma型号可在Hugging Face购买。
将所需的模型文件放入项目目录中。public/models/
本教程中使用的代码分布在 ,组织为三个主要目录:src/
src/
├── litert/
│ ├── runtime.ts
│ └── models.ts
├── ocr/
│ ├── preprocess.ts
│ ├── detect.ts
│ ├── ctc.ts
│ └── layout.ts
└── llm/
├── engine.ts
└── structureWithLlm.ts
这种分离使模型初始化、OCR处理和LLM推断保持独立。
构建本地LiteRT.js运行时
我们先从机器学习运行时本身说起。
在推理运行前,需要初始化两个部件:
-
浏览器计算后端
-
编译后的OCR模型
初始化GPU运行时
初始化机器学习后端成本相对较高。React组件可以频繁挂载、卸载和重新渲染,因此将运行时初始化直接绑定到组件生命周期可能会导致重复的GPU上下文和不必要的内存压力。
相反,我们将在模块层级缓存初始化,如何在 iPhone 上获取天气提醒和通知 - 致知笔记 - 专注教程知识分享并共享 。Promise
创作:runtime.ts
import {
loadLiteRt,
getWebGpuDevice,
isWebGPUSupported
} from '@litertjs/core';
import * as tf from '@tensorflow/tfjs';
import { WebGPUBackend } from '@tensorflow/tfjs-backend-webgpu';
import { WASM_PATH } from '../ocr/config';
export interface RuntimeInfo {
webgpu: boolean;
tfjsBackend: string;
}
let runtimePromise: Promise<RuntimeInfo> | null = null;
export function initRuntime(): Promise<RuntimeInfo> {
if (!runtimePromise) {
runtimePromise = doInit().catch((err) => {
runtimePromise = null;
throw err;
});
}
return runtimePromise;
}
async function doInit(): Promise<RuntimeInfo> {
if (isWebGPUSupported()) {
await tf.setBackend('webgpu');
await tf.ready();
await loadLiteRt(WASM_PATH);
const device = getWebGpuDevice();
if (device) {
tf.removeBackend('webgpu');
tf.registerBackend(
'webgpu',
() => new WebGPUBackend(device, device.adapterInfo)
);
await tf.setBackend('webgpu');
await tf.ready();
return {
webgpu: true,
tfjsBackend: tf.getBackend()
};
}
}
await loadLiteRt(WASM_PATH);
await tf.setBackend('cpu');
await tf.ready();
return {
webgpu: false,
tfjsBackend: tf.getBackend()
};
}
runtimePromise确保多个呼叫者共享相同的初始化工作。如果初始化失败,承诺会被重置,以便后续请求可以再次尝试。
当 WebGPU 可用时,应用程序会初始化 TensorFlow.js WebGPU 后端,并与 LiteRT.js 共享该 GPU 设备。否则,它会退回到CPU执行。
OCR模型的编译
运行时准备好后,我们可以编译模型图。.tflite
编译为特定执行后端的模型做准备。这里,我们先尝试首选的加速后端,如果编译失败再回退。
创作:models.ts
import { loadAndCompile } from '@litertjs/core';
import type {
CompiledModel,
TensorDetails
} from '@litertjs/core';
async function compileWithFallback(
url: string,
order: readonly Backend[]
): Promise<LoadedModel> {
let lastErr: unknown;
for (const accelerator of order) {
try {
const model = await loadAndCompile(url, { accelerator });
const toSpec = (d: TensorDetails) => ({
name: d.name,
dtype: d.dtype,
shape: Array.from(d.shape)
});
return {
model,
backend: accelerator,
inputs: model.getInputDetails().map(toSpec),
outputs: model.getOutputDetails().map(toSpec)
};
} catch (err) {
lastErr = err;
}
}
throw new Error(`Failed to compile ${url}: ${String(lastErr)}`);
}
完整实现并行加载检测器、识别器和字典。models.ts``Promise.all
预处理收据图像
在通过OCR模型发送图像之前,我们会预处理以提高识别可靠性。
管道:
-
放大图像
-
将图像转换为灰度
-
拉伸对比度,使文字更突出
相关部分看起来是这样的:preprocess.ts
export function preprocess(
source: CanvasImageSource,
srcW: number,
srcH: number
): Preprocessed {
const scale = downscaleFactor(srcW, srcH);
const w = Math.round(srcW * scale);
const h = Math.round(srcH * scale);
const img = toImageData(source, w, h);
const luma = grayscale(img);
contrastStretch(img, luma);
return {
image: img,
scale
};
}
downscaleFactor()将最长边限制在1600像素,防止不必要的大图像增加推理成本。
灰度转换将图像简化为亮度信息,而对比度拉伸则增加文本与背景之间的分离度。
本地运行OCR推理
OCR流程分为三个阶段:
-
检测文本区域
-
识别每个区域的文本
-
从检测到的坐标重建线和列
文本检测
检测模型会找到文本边界框。
当WebGPU可用时,我们希望避免在GPU和CPU内存之间反复复制张量。 提供 ,使TensorFlow.js张量直接进入LiteRT.js执行路径。@litertjs/tfjs-interop``runWithTfjsTensors
相关代码如下:detect.ts
import * as tf from '@tensorflow/tfjs';
import { runWithTfjsTensors } from '@litertjs/tfjs-interop';
export async function detect(
det: LoadedModel,
image: ImageData
): Promise<{ boxes: Box[] }> {
const inLayout = {
h: det.inputs[0].shape[2],
w: det.inputs[0].shape[3],
layout: 'nchw' as const
};
const input = buildInput(
image,
inLayout.w,
inLayout.h,
inLayout.layout
);
const outputs = await runWithTfjsTensors(det.model, [input]);
input.dispose();
const [region, affinity] = await Promise.all([
outputs[0].data(),
outputs[1].data()
]);
tf.dispose(outputs);
return {
boxes: decodeCraftHeatmaps(
region,
affinity,
inLayout.w,
inLayout.h
)
};
}
buildInput()将图像值归一化到模型预期的范围。
推断后,模型的热图通过连通分量分组法解码为包围框。
识别文本
一旦确定了文本区域,每个区域都会被裁剪并传递给识别模型。
识别器在每个时间步返回字符类的概率分布。然后我们用连接主义时间分类(CTC)解码该输出。
解码器看起来像这样:ctc.ts
export function ctcGreedyDecode(
logits: Float32Array,
T: number,
numClasses: number,
chars: string[]
) {
const path = new Int32Array(T);
const probs = new Float32Array(T);
for (let t = 0; t < T; t++) {
path[t] = getArgmax(
logits,
t * numClasses,
numClasses
);
probs[t] = getSoftmaxProbability(
logits,
t * numClasses,
numClasses
);
}
let out = '';
let prev = -1;
const blank = 0;
for (let t = 0; t < T; t++) {
const cls = path[t];
if (cls !== prev && cls !== blank) {
out += chars[cls] ?? '';
}
prev = cls;
}
return {
text: out,
confidence: calculateAvgConfidence(probs)
};
}
解码器会在每个时间步中选取最可能的字符,折叠重复的类,并移除CTC空白标记。
重建收据布局
原始OCR输出不足以获得收据。我们还需要保持物品名称及其价格出现在同一行的关系。
应用程序利用每个检测到的字的坐标重建该布局。
载于:layout.ts
export function reconstructLines(
words: Word[],
rowTolerance = 0.6
): Line[] {
const byY = [...words].sort(
(a, b) =>
(a.box.y + a.box.h / 2) -
(b.box.y + b.box.h / 2)
);
const rows: Row[] = [];
for (const word of byY) {
const matchedRow = findMatchingRow(
rows,
word,
rowTolerance
);
if (matchedRow) {
matchedRow.words.push(word);
} else {
rows.push({
words: [word],
centerSum: word.box.y + word.box.h / 2
});
}
}
return rows
.map(formatAndInsertSpaces)
.sort((a, b) => a.yCenter - b.yCenter);
}
该算法首先将垂直位置在容差范围内重叠的单词分组。然后它会水平排序单词,并根据彼此之间的物理距离插入空格。
这使得LLM比单纯的识别字符串列表更能用地表示收据。
使用 LiteRT-LM 结构化收据数据
OCR给我们提供文本。下一步是将这些文本转化为结构化应用数据。
对于收据来说,这可能意味着:
-
商人名称
-
各项项
-
价格
-
税务
-
总计
我们将使用设备内的 Gemma 模型,通过 LiteRT-LM 将重建后的 OCR 输出转换为 JSON。如果你对构建多回合AI代理的其他方法感兴趣,Genkit的代理API值得作为补充选项探索。
对LLM运行时间进行懒散加载
LLM运行时和模型资源相对较大,因此将它们作为初始应用包的一部分加载,即使是从未使用过该功能的用户,也会增加启动成本。
相反,只有当用户启用带有LLM的结构选项时,我们才会动态导入LiteRT-LM。
相关部分:engine.ts
import type { Engine } from '@litert-lm/core';
import {
LLM_MODEL_PATH,
LLM_MAX_TOKENS,
LLM_WASM_PATH
} from '../ocr/config';
let status: LlmStatus = 'unavailable';
let enginePromise: Promise<Engine | null> | null = null;
export function initLlmEngine(): Promise<Engine | null> {
if (!enginePromise) {
enginePromise = doInit().catch(() => {
enginePromise = null;
return null;
});
}
return enginePromise;
}
async function doInit(): Promise<Engine | null> {
const mod = await import('@litert-lm/core');
await mod.getOrLoadGlobalLiteRtLm(LLM_WASM_PATH);
return await mod.Engine.create({
model: LLM_MODEL_PATH,
mainExecutorSettings: {
maxNumTokens: LLM_MAX_TOKENS
}
});
}
模块级承诺的作用与主LiteRT.js运行时相同:一次只运行一次初始化。
这种动态还让捆绑者有机会将LLM运行时拆分为独立块,而不是包含在应用的初始JavaScript捆包中。这种懒惰的评估是管理昂贵资源的React应用中常见的模式。import()
运行LLM推理
接下来,我们提供了一个系统提示,定义预期的输出格式和目标 JSON 模式。
structureWithLlm.ts处理请求:
import { getLlmEngine } from './engine';
import {
SYSTEM_PROMPT,
buildUserPrompt
} from './prompt';
export async function structureWithLlm(
lines: Line[]
): Promise<ReceiptData> {
const engine = await getLlmEngine();
if (!engine) {
return structureFallback(lines);
}
try {
const convo = await engine.createConversation({
preface: {
messages: [
{
role: 'system',
content: SYSTEM_PROMPT
}
]
}
});
const prompt = buildUserPrompt(lines);
const raw = await convo.sendMessage(prompt);
await convo.delete();
const jsonText = extractJson(raw.content);
return parseAndMapOcrConfidence(
jsonText,
lines
);
} catch (err) {
return structureFallback(lines);
}
}
如果LLM无法初始化或推理失败,应用会退回到基于正则表达式的解析器,而不是整个扫描失败。
浏览器中的结果如下:
注:本次演示中使用的识别模型在作者测试中识别准确率不到50%。你可以用更准确的OCR模型替换它,而不会改变整体LiteRT.js流程。
LiteRT.js 与其他浏览器机器学习运行时的比较
LiteRT.js 是浏览器中运行机器学习工作负载的多种选项之一。
主要区别在于模型格式、执行架构以及每个运行时支持的硬件后端。
| 特色 | LiteRT.js | TensorFlow.js | ONNX 运行时网页 |
|---|---|---|---|
| 模型格式 | .tflite | TensorFlow.js模型格式 | .onnx |
| CPU 执行 | Wasm / 优化的原生内核 | JavaScript / Wasm 后端 | 瓦斯姆 |
| GPU执行 | WebGPU | WebGL / WebGPU,取决于后端 | WebGPU |
| 框架互操作性 | 模型可以通过转换来源于多个机器学习框架 | 与TensorFlow生态系统最强 | 广泛的ONNX生态系统 |
| WebNN路径 | 新生/依赖支持 | 依赖支持者 | 依赖支持者 |
最重要的选择通常是你已经使用的模型格式。
如果你已经有模型,或者针对同一模型流水线的Android、移动端、嵌入式和网页,LiteRT.js尤其有吸引力,因为浏览器可以使用相同的部署格式。.tflite
TensorFlow.js仍然拥有更广泛的JavaScript原生生态系统,当张量操作和模型执行主要都存在于JavaScript中时,它仍然非常有用。在评估这类机器学习项目的包管理器和依赖设置时,值得考虑每个工具如何影响安装时间和磁盘使用情况。
当你的模型流水线已经针对ONNX或你需要兼容多个训练框架的模型时,ONNX运行时Web是非常合适的选择。
结论
LiteRT.js为网页开发者提供了另一种实用的路径,可以直接在用户设备上运行机器学习模型。
在本教程中,我们构建了一个基于浏览器的收据处理流程,利用LiteRT.js进行OCR推断,重建识别文本的空间布局,并可选择性地将结果通过设备内的LiteRT-LM语言模型处理。
该架构还展示了LiteRT.js与现有浏览器工具的定位。TensorFlow.js 在这里仍然在张量处理和互操作性中发挥着有用作用,而 LiteRT.js 则负责模型执行。这使得LiteRT.js不再是全面TensorFlow.js替代品,而是围绕谷歌AI边缘模型生态系统构建的应用的新选择。.tflite
本地运行推理也会改变应用的部署模型。图像和提取后的文本可以保留在用户设备上,应用程序可以继续运行而无需往返推理 API,开发者也可以避免为每个模型请求建立专用后台。对于评估AI在其更广泛产品战略中定位的团队来说,随着这些设备端能力的成熟,了解AI知识产品经理的需求变得越来越重要。

浙公网安备 33010602011771号