webassembly009 transformers.js 网页端侧推理 whisper-web transcriber & useTranscrib
worker.js[*]模型的团体推理结构仍与webassembly009 transformers.js 网页端侧推理 NLLB翻译模型类似。
worker.js 模型工厂类
/* eslint-disable camelcase */
import { pipeline, env } from "@xenova/transformers"; // 导入必要的模块
// 禁用本地模型,确保仅使用在线资源
env.allowLocalModels = false;
// 定义模型工厂类
class PipelineFactory {
static task = null; // 静态变量:任务类型
static model = null; // 静态变量:模型名称
static quantized = null; // 静态变量:是否量化模型
static instance = null; // 静态变量:模型实例
constructor(tokenizer, model, quantized) {
this.tokenizer = tokenizer;
this.model = model;
this.quantized = quantized;
}
// 获取单例模式的模型实例
static async getInstance(progress_callback = null) {
if (this.instance === null) {
// pipeline函数:默认情况下,模型将从 Hugging Face Hub 下载并存储在 浏览器缓存 中,有关更多信息,请参见https://hugging-face.cn/docs/transformers.js/custom_usage。
this.instance = pipeline(this.task, this.model, {
quantized: this.quantized,
progress_callback,
// 对于中型模型,为了防止内存溢出,加载`no_attentions`版本
revision: this.model.includes("/whisper-medium") ? "no_attentions" : "main"
});
}
return this.instance;
}
}
// 自动语音识别管道工厂类,继承自PipelineFactory
class AutomaticSpeechRecognitionPipelineFactory extends PipelineFactory {
static task = "automatic-speech-recognition"; // 任务类型为自动语音识别
static model = null;
static quantized = null;
}
处置处罚来自主线程的消息
// 监听消息事件,处理来自主线程的消息
self.addEventListener("message", async (event) => {
const message = event.data;
// 根据接收到的消息数据执行转录操作
let transcript = await transcribe(
message.audio,
message.model,
message.multilingual,
message.quantized,
message.subtask,
message.language,
);
if (transcript === null) return;
// 将转录结果发送回主线程
self.postMessage({
status: "complete",
task: "automatic-speech-recognition",
data: transcript,
});
});
调用worker
// 转录函数,根据提供的音频和其他参数进行转录
const transcribe = async (
audio,
model,
multilingual,
quantized,
subtask,
language,
) => {
const isDistilWhisper = model.startsWith("distil-whisper/"); // 判断是否是特定模型
let modelName = model;
if (!isDistilWhisper && !multilingual) {
modelName += ".en"; // 如果不是多语言模型,则附加.en后缀
}
const p = AutomaticSpeechRecognitionPipelineFactory;
if (p.model !== modelName || p.quantized !== quantized) {
// 若模型或量化设置改变,则重新初始化
p.model = modelName;
p.quantized = quantized;
if (p.instance !== null) {
(await p.getInstance()).dispose();
p.instance = null;
}
}
// 加载转录器模型
let transcriber = await p.getInstance((data) => {
self.postMessage(data); // 发送进度更新
});
const time_precision =
transcriber.processor.feature_extractor.config.chunk_length /
transcriber.model.config.max_source_positions;
// 初始化待处理块列表
let chunks_to_process = [
{
tokens: [],
finalised: false,
},
];
function chunk_callback(chunk) {
let last = chunks_to_process;
// 更新最后一个块的信息,并在必要时创建新的块
Object.assign(last, chunk);
last.finalised = true;
if (!chunk.is_last) {
chunks_to_process.push({
tokens: [],
finalised: false,
});
}
}
// 回调函数,用于合并文本块
function callback_function(item) {
let last = chunks_to_process;
last.tokens = [...item.output_token_ids]; // 更新最后块的token信息
// 解码并合并文本块
let data = transcriber.tokenizer._decode_asr(chunks_to_process, {
time_precision: time_precision,
return_timestamps: true,
force_full_sequences: false,
});
// 发送更新状态给主线程
self.postMessage({
status: "update",
task: "automatic-speech-recognition",
data: data,
});
}
// 执行实际的转录过程
let output = await transcriber(audio, {
top_k: 0, do_sample: false, // 使用贪婪搜索策略
chunk_length_s: isDistilWhisper ? 20 : 30, stride_length_s: isDistilWhisper ? 3 : 5, // 设置滑动窗口长度
language: language, task: subtask, // 设置语言和子任务
return_timestamps: true, force_full_sequences: false, // 返回时间戳
callback_function: callback_function, // 每个生成步骤后的回调
chunk_callback: chunk_callback, // 处理完每个块后的回调
}).catch((error) => {
// 错误处理
self.postMessage({
status: "error",
task: "automatic-speech-recognition",
data: error,
});
return null;
});
return output;
};
useWorker.ts
[*]代码利用React Hook useState来确保Web Worker仅被创建一次,并提供了一个自定义Hook useWorker,用于简化在React组件中利用Web Worker的过程。
import { useState } from "react";
export interface MessageEventHandler {
(event: MessageEvent): void;
}
export function useWorker(messageEventHandler: MessageEventHandler): Worker {
// Create new worker once and never again
const = useState(() => createWorker(messageEventHandler));
return worker;
}
function createWorker(messageEventHandler: MessageEventHandler): Worker {
const worker = new Worker(new URL("../worker.js", import.meta.url), {
type: "module",
});
// Listen for messages from the Web Worker
worker.addEventListener("message", messageEventHandler);
return worker;
}
[*] useWorker 用来处置处罚与 Web Worker 的交互,Web Worker 的职责是处置处罚耗时的音频转录工作,避免壅闭主线程。通过监听来自 Worker 的消息,更新转录状态。
[*]“progress”:更新文件加载进度,修改 progressItems 状态。
[*]“update”:更新部分转录文本,将数据更新到 transcript。
[*]“complete”:转录完成,更新 transcript,并标记为不忙碌。
[*]“initiate”:开始加载模型文件,添加进度项。
[*]“ready”:模型加载完毕。
[*]“error”:错误处置处罚,提示用户。
[*]“done”:模型文件加载完成,移除进度项。
useTranscriber.ts
[*]代码实现了一个自定义的 React 钩子 useTranscriber,用于处置处罚音频转录过程,结合 Web Worker 和后台模型进行推理。
定义的接口
[*]ProgressItem:用于表示文件加载的进度信息。包含文件名、加载的字节数、总字节数、进度百分比和状态。
[*]TranscriberUpdateData:表示部分转录更新数据,包含已经转录的文本和时间戳的 chunks。
[*]TranscriberCompleteData:表示完备转录数据,包含最终的转录文本和时间戳的 chunks。
[*]TranscriberData:表示转录的数据,包罗是否忙碌、转录的文本以及文本的时间戳 chunks。
[*]Transcriber:是 useTranscriber 钩子的返回范例,定义了用于控制转录和管理其状态的各种函数和状态。
import { useCallback, useMemo, useState } from "react"; // 导入 React 钩子函数
import { useWorker } from "./useWorker"; // 导入自定义的 Web Worker 钩子
import Constants from "../utils/Constants"; // 导入常量配置
// 定义进度项接口,表示文件加载的进度
interface ProgressItem {
file: string; // 文件名
loaded: number; // 已加载字节数
progress: number; // 加载进度百分比
total: number; // 总字节数
name: string; // 文件名
status: string; // 当前状态(如正在加载等)
}
// 定义转录更新数据接口,表示部分转录的更新数据
interface TranscriberUpdateData {
data: [
string, // 当前转录的文本
{ chunks: { text: string; timestamp: }[] } // 文本分块和时间戳
];
text: string; // 当前转录的完整文本
}
// 定义转录完成数据接口,表示转录完成时的完整数据
interface TranscriberCompleteData {
data: {
text: string; // 完整转录的文本
chunks: { text: string; timestamp: }[]; // 分块的文本和时间戳
};
}
// 定义转录数据接口,表示转录的状态和结果
export interface TranscriberData {
isBusy: boolean; // 是否正在转录
text: string; // 当前转录的文本
chunks: { text: string; timestamp: }[]; // 分块的文本和时间戳
}
// 定义转录器接口,提供一系列 API 用于控制转录过程
export interface Transcriber {
onInputChange: () => void; // 输入变化时重置转录状态
isBusy: boolean; // 是否正在转录
isModelLoading: boolean; // 是否加载模型
progressItems: ProgressItem[]; // 文件加载的进度
start: (audioData: AudioBuffer | undefined) => void; // 启动转录
output?: TranscriberData; // 转录的结果
model: string; // 当前使用的模型
setModel: (model: string) => void; // 设置使用的模型
multilingual: boolean; // 是否启用多语言支持
setMultilingual: (model: boolean) => void; // 设置是否启用多语言
quantized: boolean; // 是否使用量化模型
setQuantized: (model: boolean) => void; // 设置是否启用量化
subtask: string; // 子任务(如语音识别等)
setSubtask: (subtask: string) => void; // 设置子任务
language?: string; // 语言设置
setLanguage: (language: string) => void; // 设置语言
}
状态 useTranscriber 钩子
[*] 该钩子封装了转录逻辑。
[*] postRequest 用于发送音频数据到 Web Worker 进行转录。它担当一个 AudioBuffer 对象,检查其是否为立体声(2 个声道),并将其合并为单声道。如果音频数据是单声道,则直接利用第一声道数据。然后,它将数据发送到 Web Worker,并通报须要的配置选项(如模型、是否利用多语言、量化选项、子任务等)。
// 自定义 React 钩子 `useTranscriber` 用于管理转录过程,返回值类型是 Transcriber
export function useTranscriber(): Transcriber {
const = useState<TranscriberData | undefined>(undefined); // 存储转录结果
const = useState(false); // 转录是否正在进行
const = useState(false); // 是否正在加载模型
const = useState<ProgressItem[]>([]); // 存储加载进度项
[*]useWorker 用来处置处罚与 Web Worker 的交互
// 使用 Web Worker 进行后台转录任务
const webWorker = useWorker((event) => {
const message = event.data; // 获取消息数据
// 根据消息的不同状态更新相应的状态
switch (message.status) {
case "progress":
// 如果是文件加载进度更新
setProgressItems((prev) =>
prev.map((item) => {
if (item.file === message.file) {
return { ...item, progress: message.progress }; // 更新进度
}
return item; // 其他进度项不变
}),
);
break;
case "update":
// 如果是转录的部分更新
const updateMessage = message as TranscriberUpdateData;
setTranscript({
isBusy: true,
text: updateMessage.data,
chunks: updateMessage.data.chunks,
});
break;
case "complete":
// 如果是转录完成
const completeMessage = message as TranscriberCompleteData;
setTranscript({
isBusy: false,
text: completeMessage.data.text,
chunks: completeMessage.data.chunks,
});
setIsBusy(false); // 标记不再忙碌
break;
case "initiate":
// 如果是开始加载模型文件
setIsModelLoading(true);
setProgressItems((prev) => [...prev, message]); // 添加进度项
break;
case "ready":
setIsModelLoading(false); // 模型加载完成
break;
case "error":
setIsBusy(false); // 发生错误,转录结束
alert(
`${message.data.message} This is most likely because you are using Safari on an M1/M2 Mac. Please try again from Chrome, Firefox, or Edge.\n\nIf this is not the case, please file a bug report.`,
);
break;
case "done":
// 如果模型加载完成,移除进度项
setProgressItems((prev) =>
prev.filter((item) => item.file !== message.file),
);
break;
default:
// 默认处理其他消息
break;
}
});
// 初始化模型相关状态
const = useState<string>(Constants.DEFAULT_MODEL); // 当前使用的模型
const = useState<string>(Constants.DEFAULT_SUBTASK); // 当前子任务
const = useState<boolean>(Constants.DEFAULT_QUANTIZED); // 是否使用量化模型
const = useState<boolean>(Constants.DEFAULT_MULTILINGUAL); // 是否启用多语言支持
const = useState<string>(Constants.DEFAULT_LANGUAGE); // 当前语言设置
// 当输入变化时,重置转录状态
const onInputChange = useCallback(() => {
setTranscript(undefined); // 清空当前转录数据
}, []);
// 发送音频数据到 Web Worker 进行处理
const postRequest = useCallback(
async (audioData: AudioBuffer | undefined) => {
if (audioData) {
setTranscript(undefined); // 清空当前转录
setIsBusy(true); // 设置转录为忙碌状态
let audio;
if (audioData.numberOfChannels === 2) {
// 如果是立体声,合并两个声道为单声道
const SCALING_FACTOR = Math.sqrt(2);
let left = audioData.getChannelData(0); // 获取左声道数据
let right = audioData.getChannelData(1); // 获取右声道数据
audio = new Float32Array(left.length); // 创建新的单声道数据
for (let i = 0; i < audioData.length; ++i) {
audio = SCALING_FACTOR * (left + right) / 2; // 合并并标准化数据
}
} else {
// 如果是单声道音频,直接使用第一个声道
audio = audioData.getChannelData(0);
}
// 向 Web Worker 发送消息,开始处理音频数据
webWorker.postMessage({
audio,
model,
multilingual,
quantized,
subtask: multilingual ? subtask : null, // 如果是多语言,传递子任务
language: multilingual && language !== "auto" ? language : null, // 语言设置(如果启用了多语言)
});
}
},
,
);
// 返回转录器对象,暴露相关 API
const transcriber = useMemo(() => {
return {
onInputChange,
isBusy,
isModelLoading,
progressItems,
start: postRequest,
output: transcript, // 转录结果
model,
setModel,
multilingual,
setMultilingual,
quantized,
setQuantized,
subtask,
setSubtask,
language,
setLanguage,
};
}, [
isBusy,
isModelLoading,
progressItems,
postRequest,
transcript,
model,
multilingual,
quantized,
subtask,
language,
]);
return transcriber; // 返回转录器对象
}
CG
[*] useMemo 和 useCallback 优化
[*]useMemo:用于确保 transcriber 对象在依赖项(如 isBusy、model 等)变化时进行重新盘算,从而避免不须要的重新渲染。
[*]useCallback:用于优化 onInputChange 和 postRequest 方法的重渲染性能,确保它们只在依赖项变化时重新创建。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页:
[1]