张裕 发表于 2025-4-23 12:20:45

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]
查看完整版本: webassembly009 transformers.js 网页端侧推理 whisper-web transcriber & useTranscrib