webassembly009 transformers.js 网页端侧推理 whisper-web transcriber & ...

张裕  论坛元老 | 2025-4-23 12:20:45 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 1494|帖子 1494|积分 4482

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x
worker.js



  • 模型的团体推理结构仍与webassembly009 transformers.js 网页端侧推理 NLLB翻译模型类似。
worker.js 模型工厂类

  1. /* eslint-disable camelcase */
  2. import { pipeline, env } from "@xenova/transformers"; // 导入必要的模块
  3. // 禁用本地模型,确保仅使用在线资源
  4. env.allowLocalModels = false;
  5. // 定义模型工厂类
  6. class PipelineFactory {
  7.     static task = null; // 静态变量:任务类型
  8.     static model = null; // 静态变量:模型名称
  9.     static quantized = null; // 静态变量:是否量化模型
  10.     static instance = null; // 静态变量:模型实例
  11.     constructor(tokenizer, model, quantized) {
  12.         this.tokenizer = tokenizer;
  13.         this.model = model;
  14.         this.quantized = quantized;
  15.     }
  16.     // 获取单例模式的模型实例
  17.     static async getInstance(progress_callback = null) {
  18.         if (this.instance === null) {
  19.             // pipeline函数:默认情况下,模型将从 Hugging Face Hub 下载并存储在 浏览器缓存 中,有关更多信息,请参见https://hugging-face.cn/docs/transformers.js/custom_usage。
  20.             this.instance = pipeline(this.task, this.model, {
  21.                 quantized: this.quantized,
  22.                 progress_callback,
  23.                 // 对于中型模型,为了防止内存溢出,加载`no_attentions`版本
  24.                 revision: this.model.includes("/whisper-medium") ? "no_attentions" : "main"
  25.             });
  26.         }
  27.         return this.instance;
  28.     }
  29. }
  30. // 自动语音识别管道工厂类,继承自PipelineFactory
  31. class AutomaticSpeechRecognitionPipelineFactory extends PipelineFactory {
  32.     static task = "automatic-speech-recognition"; // 任务类型为自动语音识别
  33.     static model = null;
  34.     static quantized = null;
  35. }
复制代码
处置处罚来自主线程的消息

  1. // 监听消息事件,处理来自主线程的消息
  2. self.addEventListener("message", async (event) => {
  3.     const message = event.data;
  4.     // 根据接收到的消息数据执行转录操作
  5.     let transcript = await transcribe(
  6.         message.audio,
  7.         message.model,
  8.         message.multilingual,
  9.         message.quantized,
  10.         message.subtask,
  11.         message.language,
  12.     );
  13.     if (transcript === null) return;
  14.     // 将转录结果发送回主线程
  15.     self.postMessage({
  16.         status: "complete",
  17.         task: "automatic-speech-recognition",
  18.         data: transcript,
  19.     });
  20. });
复制代码
调用worker

  1. // 转录函数,根据提供的音频和其他参数进行转录
  2. const transcribe = async (
  3.     audio,
  4.     model,
  5.     multilingual,
  6.     quantized,
  7.     subtask,
  8.     language,
  9. ) => {
  10.     const isDistilWhisper = model.startsWith("distil-whisper/"); // 判断是否是特定模型
  11.     let modelName = model;
  12.     if (!isDistilWhisper && !multilingual) {
  13.         modelName += ".en"; // 如果不是多语言模型,则附加.en后缀
  14.     }
  15.     const p = AutomaticSpeechRecognitionPipelineFactory;
  16.     if (p.model !== modelName || p.quantized !== quantized) {
  17.         // 若模型或量化设置改变,则重新初始化
  18.         p.model = modelName;
  19.         p.quantized = quantized;
  20.         if (p.instance !== null) {
  21.             (await p.getInstance()).dispose();
  22.             p.instance = null;
  23.         }
  24.     }
  25.     // 加载转录器模型
  26.     let transcriber = await p.getInstance((data) => {
  27.         self.postMessage(data); // 发送进度更新
  28.     });
  29.     const time_precision =
  30.         transcriber.processor.feature_extractor.config.chunk_length /
  31.         transcriber.model.config.max_source_positions;
  32.     // 初始化待处理块列表
  33.     let chunks_to_process = [
  34.         {
  35.             tokens: [],
  36.             finalised: false,
  37.         },
  38.     ];
  39.     function chunk_callback(chunk) {
  40.         let last = chunks_to_process[chunks_to_process.length - 1];
  41.         // 更新最后一个块的信息,并在必要时创建新的块
  42.         Object.assign(last, chunk);
  43.         last.finalised = true;
  44.         if (!chunk.is_last) {
  45.             chunks_to_process.push({
  46.                 tokens: [],
  47.                 finalised: false,
  48.             });
  49.         }
  50.     }
  51.     // 回调函数,用于合并文本块
  52.     function callback_function(item) {
  53.         let last = chunks_to_process[chunks_to_process.length - 1];
  54.         last.tokens = [...item[0].output_token_ids]; // 更新最后块的token信息
  55.         // 解码并合并文本块
  56.         let data = transcriber.tokenizer._decode_asr(chunks_to_process, {
  57.             time_precision: time_precision,
  58.             return_timestamps: true,
  59.             force_full_sequences: false,
  60.         });
  61.         // 发送更新状态给主线程
  62.         self.postMessage({
  63.             status: "update",
  64.             task: "automatic-speech-recognition",
  65.             data: data,
  66.         });
  67.     }
  68.     // 执行实际的转录过程
  69.     let output = await transcriber(audio, {
  70.         top_k: 0, do_sample: false, // 使用贪婪搜索策略
  71.         chunk_length_s: isDistilWhisper ? 20 : 30, stride_length_s: isDistilWhisper ? 3 : 5, // 设置滑动窗口长度
  72.         language: language, task: subtask, // 设置语言和子任务
  73.         return_timestamps: true, force_full_sequences: false, // 返回时间戳
  74.         callback_function: callback_function, // 每个生成步骤后的回调
  75.         chunk_callback: chunk_callback, // 处理完每个块后的回调
  76.     }).catch((error) => {
  77.         // 错误处理
  78.         self.postMessage({
  79.             status: "error",
  80.             task: "automatic-speech-recognition",
  81.             data: error,
  82.         });
  83.         return null;
  84.     });
  85.     return output;
  86. };
复制代码
useWorker.ts



  • 代码利用React Hook useState来确保Web Worker仅被创建一次,并提供了一个自定义Hook useWorker,用于简化在React组件中利用Web Worker的过程。
  1. import { useState } from "react";
  2. export interface MessageEventHandler {
  3.     (event: MessageEvent): void;
  4. }
  5. export function useWorker(messageEventHandler: MessageEventHandler): Worker {
  6.     // Create new worker once and never again
  7.     const [worker] = useState(() => createWorker(messageEventHandler));
  8.     return worker;
  9. }
  10. function createWorker(messageEventHandler: MessageEventHandler): Worker {
  11.     const worker = new Worker(new URL("../worker.js", import.meta.url), {
  12.         type: "module",
  13.     });
  14.     // Listen for messages from the Web Worker
  15.     worker.addEventListener("message", messageEventHandler);
  16.     return worker;
  17. }
复制代码


  • 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 钩子的返回范例,定义了用于控制转录和管理其状态的各种函数和状态。
  1. import { useCallback, useMemo, useState } from "react"; // 导入 React 钩子函数
  2. import { useWorker } from "./useWorker"; // 导入自定义的 Web Worker 钩子
  3. import Constants from "../utils/Constants"; // 导入常量配置
  4. // 定义进度项接口,表示文件加载的进度
  5. interface ProgressItem {
  6.     file: string; // 文件名
  7.     loaded: number; // 已加载字节数
  8.     progress: number; // 加载进度百分比
  9.     total: number; // 总字节数
  10.     name: string; // 文件名
  11.     status: string; // 当前状态(如正在加载等)
  12. }
  13. // 定义转录更新数据接口,表示部分转录的更新数据
  14. interface TranscriberUpdateData {
  15.     data: [
  16.         string, // 当前转录的文本
  17.         { chunks: { text: string; timestamp: [number, number | null] }[] } // 文本分块和时间戳
  18.     ];
  19.     text: string; // 当前转录的完整文本
  20. }
  21. // 定义转录完成数据接口,表示转录完成时的完整数据
  22. interface TranscriberCompleteData {
  23.     data: {
  24.         text: string; // 完整转录的文本
  25.         chunks: { text: string; timestamp: [number, number | null] }[]; // 分块的文本和时间戳
  26.     };
  27. }
  28. // 定义转录数据接口,表示转录的状态和结果
  29. export interface TranscriberData {
  30.     isBusy: boolean; // 是否正在转录
  31.     text: string; // 当前转录的文本
  32.     chunks: { text: string; timestamp: [number, number | null] }[]; // 分块的文本和时间戳
  33. }
  34. // 定义转录器接口,提供一系列 API 用于控制转录过程
  35. export interface Transcriber {
  36.     onInputChange: () => void; // 输入变化时重置转录状态
  37.     isBusy: boolean; // 是否正在转录
  38.     isModelLoading: boolean; // 是否加载模型
  39.     progressItems: ProgressItem[]; // 文件加载的进度
  40.     start: (audioData: AudioBuffer | undefined) => void; // 启动转录
  41.     output?: TranscriberData; // 转录的结果
  42.     model: string; // 当前使用的模型
  43.     setModel: (model: string) => void; // 设置使用的模型
  44.     multilingual: boolean; // 是否启用多语言支持
  45.     setMultilingual: (model: boolean) => void; // 设置是否启用多语言
  46.     quantized: boolean; // 是否使用量化模型
  47.     setQuantized: (model: boolean) => void; // 设置是否启用量化
  48.     subtask: string; // 子任务(如语音识别等)
  49.     setSubtask: (subtask: string) => void; // 设置子任务
  50.     language?: string; // 语言设置
  51.     setLanguage: (language: string) => void; // 设置语言
  52. }
复制代码
状态 useTranscriber 钩子



  • 该钩子封装了转录逻辑。
  • postRequest 用于发送音频数据到 Web Worker 进行转录。它担当一个 AudioBuffer 对象,检查其是否为立体声(2 个声道),并将其合并为单声道。如果音频数据是单声道,则直接利用第一声道数据。然后,它将数据发送到 Web Worker,并通报须要的配置选项(如模型、是否利用多语言、量化选项、子任务等)。
  1. // 自定义 React 钩子 `useTranscriber` 用于管理转录过程,返回值类型是 Transcriber
  2. export function useTranscriber(): Transcriber {
  3.     const [transcript, setTranscript] = useState<TranscriberData | undefined>(undefined); // 存储转录结果
  4.     const [isBusy, setIsBusy] = useState(false); // 转录是否正在进行
  5.     const [isModelLoading, setIsModelLoading] = useState(false); // 是否正在加载模型
  6.     const [progressItems, setProgressItems] = useState<ProgressItem[]>([]); // 存储加载进度项
复制代码


  • useWorker 用来处置处罚与 Web Worker 的交互
  1.     // 使用 Web Worker 进行后台转录任务
  2.     const webWorker = useWorker((event) => {
  3.         const message = event.data; // 获取消息数据
  4.         // 根据消息的不同状态更新相应的状态
  5.         switch (message.status) {
  6.             case "progress":
  7.                 // 如果是文件加载进度更新
  8.                 setProgressItems((prev) =>
  9.                     prev.map((item) => {
  10.                         if (item.file === message.file) {
  11.                             return { ...item, progress: message.progress }; // 更新进度
  12.                         }
  13.                         return item; // 其他进度项不变
  14.                     }),
  15.                 );
  16.                 break;
  17.             case "update":
  18.                 // 如果是转录的部分更新
  19.                 const updateMessage = message as TranscriberUpdateData;
  20.                 setTranscript({
  21.                     isBusy: true,
  22.                     text: updateMessage.data[0],
  23.                     chunks: updateMessage.data[1].chunks,
  24.                 });
  25.                 break;
  26.             case "complete":
  27.                 // 如果是转录完成
  28.                 const completeMessage = message as TranscriberCompleteData;
  29.                 setTranscript({
  30.                     isBusy: false,
  31.                     text: completeMessage.data.text,
  32.                     chunks: completeMessage.data.chunks,
  33.                 });
  34.                 setIsBusy(false); // 标记不再忙碌
  35.                 break;
  36.             case "initiate":
  37.                 // 如果是开始加载模型文件
  38.                 setIsModelLoading(true);
  39.                 setProgressItems((prev) => [...prev, message]); // 添加进度项
  40.                 break;
  41.             case "ready":
  42.                 setIsModelLoading(false); // 模型加载完成
  43.                 break;
  44.             case "error":
  45.                 setIsBusy(false); // 发生错误,转录结束
  46.                 alert(
  47.                     `${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.`,
  48.                 );
  49.                 break;
  50.             case "done":
  51.                 // 如果模型加载完成,移除进度项
  52.                 setProgressItems((prev) =>
  53.                     prev.filter((item) => item.file !== message.file),
  54.                 );
  55.                 break;
  56.             default:
  57.                 // 默认处理其他消息
  58.                 break;
  59.         }
  60.     });
复制代码
  1.     // 初始化模型相关状态
  2.     const [model, setModel] = useState<string>(Constants.DEFAULT_MODEL); // 当前使用的模型
  3.     const [subtask, setSubtask] = useState<string>(Constants.DEFAULT_SUBTASK); // 当前子任务
  4.     const [quantized, setQuantized] = useState<boolean>(Constants.DEFAULT_QUANTIZED); // 是否使用量化模型
  5.     const [multilingual, setMultilingual] = useState<boolean>(Constants.DEFAULT_MULTILINGUAL); // 是否启用多语言支持
  6.     const [language, setLanguage] = useState<string>(Constants.DEFAULT_LANGUAGE); // 当前语言设置
  7.     // 当输入变化时,重置转录状态
  8.     const onInputChange = useCallback(() => {
  9.         setTranscript(undefined); // 清空当前转录数据
  10.     }, []);
  11.     // 发送音频数据到 Web Worker 进行处理
  12.     const postRequest = useCallback(
  13.         async (audioData: AudioBuffer | undefined) => {
  14.             if (audioData) {
  15.                 setTranscript(undefined); // 清空当前转录
  16.                 setIsBusy(true); // 设置转录为忙碌状态
  17.                 let audio;
  18.                 if (audioData.numberOfChannels === 2) {
  19.                     // 如果是立体声,合并两个声道为单声道
  20.                     const SCALING_FACTOR = Math.sqrt(2);
  21.                     let left = audioData.getChannelData(0); // 获取左声道数据
  22.                     let right = audioData.getChannelData(1); // 获取右声道数据
  23.                     audio = new Float32Array(left.length); // 创建新的单声道数据
  24.                     for (let i = 0; i < audioData.length; ++i) {
  25.                         audio[i] = SCALING_FACTOR * (left[i] + right[i]) / 2; // 合并并标准化数据
  26.                     }
  27.                 } else {
  28.                     // 如果是单声道音频,直接使用第一个声道
  29.                     audio = audioData.getChannelData(0);
  30.                 }
  31.                 // 向 Web Worker 发送消息,开始处理音频数据
  32.                 webWorker.postMessage({
  33.                     audio,
  34.                     model,
  35.                     multilingual,
  36.                     quantized,
  37.                     subtask: multilingual ? subtask : null, // 如果是多语言,传递子任务
  38.                     language: multilingual && language !== "auto" ? language : null, // 语言设置(如果启用了多语言)
  39.                 });
  40.             }
  41.         },
  42.         [webWorker, model, multilingual, quantized, subtask, language],
  43.     );
  44.     // 返回转录器对象,暴露相关 API
  45.     const transcriber = useMemo(() => {
  46.         return {
  47.             onInputChange,
  48.             isBusy,
  49.             isModelLoading,
  50.             progressItems,
  51.             start: postRequest,
  52.             output: transcript, // 转录结果
  53.             model,
  54.             setModel,
  55.             multilingual,
  56.             setMultilingual,
  57.             quantized,
  58.             setQuantized,
  59.             subtask,
  60.             setSubtask,
  61.             language,
  62.             setLanguage,
  63.         };
  64.     }, [
  65.         isBusy,
  66.         isModelLoading,
  67.         progressItems,
  68.         postRequest,
  69.         transcript,
  70.         model,
  71.         multilingual,
  72.         quantized,
  73.         subtask,
  74.         language,
  75.     ]);
  76.     return transcriber; // 返回转录器对象
  77. }
复制代码
CG



  • useMemo 和 useCallback 优化

    • useMemo:用于确保 transcriber 对象在依赖项(如 isBusy、model 等)变化时进行重新盘算,从而避免不须要的重新渲染。
    • useCallback:用于优化 onInputChange 和 postRequest 方法的重渲染性能,确保它们只在依赖项变化时重新创建。


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
继续阅读请点击广告
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

张裕

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表