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

worker.js

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[chunks_to_process.length - 1];

        // 更新最后一个块的信息,并在必要时创建新的块
        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[chunks_to_process.length - 1];
        last.tokens = [...item[0].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 [worker] = 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: [number, number | null] }[] } // 文本分块和时间戳
    ];
    text: string; // 当前转录的完整文本
}

// 定义转录完成数据接口,表示转录完成时的完整数据
interface TranscriberCompleteData {
    data: {
        text: string; // 完整转录的文本
        chunks: { text: string; timestamp: [number, number | null] }[]; // 分块的文本和时间戳
    };
}

// 定义转录数据接口,表示转录的状态和结果
export interface TranscriberData {
    isBusy: boolean; // 是否正在转录
    text: string; // 当前转录的文本
    chunks: { text: string; timestamp: [number, number | null] }[]; // 分块的文本和时间戳
}

// 定义转录器接口,提供一系列 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 [transcript, setTranscript] = useState<TranscriberData | undefined>(undefined); // 存储转录结果
    const [isBusy, setIsBusy] = useState(false); // 转录是否正在进行
    const [isModelLoading, setIsModelLoading] = useState(false); // 是否正在加载模型

    const [progressItems, setProgressItems] = 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[0],
                    chunks: updateMessage.data[1].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 [model, setModel] = useState<string>(Constants.DEFAULT_MODEL); // 当前使用的模型
    const [subtask, setSubtask] = useState<string>(Constants.DEFAULT_SUBTASK); // 当前子任务
    const [quantized, setQuantized] = useState<boolean>(Constants.DEFAULT_QUANTIZED); // 是否使用量化模型
    const [multilingual, setMultilingual] = useState<boolean>(Constants.DEFAULT_MULTILINGUAL); // 是否启用多语言支持
    const [language, setLanguage] = 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[i] = SCALING_FACTOR * (left[i] + right[i]) / 2; // 合并并标准化数据
                    }
                } else {
                    // 如果是单声道音频,直接使用第一个声道
                    audio = audioData.getChannelData(0);
                }

                // 向 Web Worker 发送消息,开始处理音频数据
                webWorker.postMessage({
                    audio,
                    model,
                    multilingual,
                    quantized,
                    subtask: multilingual ? subtask : null, // 如果是多语言,传递子任务
                    language: multilingual && language !== "auto" ? language : null, // 语言设置(如果启用了多语言)
                });
            }
        },
        [webWorker, model, multilingual, quantized, subtask, language],
    );

    // 返回转录器对象,暴露相关 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 对象在依赖项(如 isBusymodel 等)变化时进行重新计算,从而避免不必要的重新渲染。
    • useCallback:用于优化 onInputChangepostRequest 方法的重渲染性能,确保它们只在依赖项变化时重新创建。
### 安装 transformers 4.36.0 的 `.whl` 文件 要下载和安装 `transformers` 4.36.0 的 `.whl` 文件,可以通过以下几种方式实现: #### 1. 使用 `pip download` 命令下载 `.whl` 文件 使用 `pip` 的 `download` 命令可以下载指定版本的 `.whl` 文件及其依赖项。执行以下命令: ```bash pip download transformers==4.36.0 ``` 此命令会将 `transformers-4.36.0-py3-none-any.whl` 文件下载到当前目录。如果需要为特定平台或 Python 版本下载,可以使用 `--platform` 和 `--python-version` 参数指定目标环境。 #### 2. 从 PyPI 手动下载 访问 [transformers 4.36.0 的 PyPI 页面](https://pypi.org/project/transformers/4.36.0/),滚动到页面底部,在 &quot;Download files&quot; 部分找到 `.whl` 文件并手动下载。 #### 3. 使用镜像站点加速下载 如果使用国内网络,可以考虑使用镜像站点加速下载。例如,使用清华大学的 PyPI 镜像: ```bash pip download transformers==4.36.0 -i https://pypi.tuna.tsinghua.edu.cn/simple ``` #### 4. 安装本地 `.whl` 文件 下载完成后,可以使用 `pip install` 安装本地的 `.whl` 文件: ```bash pip install transformers-4.36.0-py3-none-any.whl ``` 此方法适用于离线安装或在特定环境中部署。 #### 5. 安装时指定版本号 如果在安装过程中遇到问题,可以尝试指定具体版本号以避免冲突。例如: ```bash pip install transformers==4.36.0 ``` 此方法可以避免因版本不兼容导致的安装失败问题,特别是在依赖项冲突的情况下[^1]。 #### 6. 处理安装失败问题 如果在安装过程中出现类似 `Failed building wheel for safetensors` 的错误,可以尝试先安装依赖项,或者直接下载 `.whl` 文件进行离线安装: ```bash pip install safetensors ``` 或者从 PyPI 下载 `safetensors` 的 `.whl` 文件并手动安装。 #### 7. 示例代码:使用 `transformers` 加载模型 以下是一个简单的代码示例,展示如何加载并使用 `transformers` 库中的模型: ```python from transformers import BertTokenizer, BertModel # 加载预训练的 BERT 模型和对应的 tokenizer tokenizer = BertTokenizer.from_pretrained(&#39;bert-base-uncased&#39;) model = BertModel.from_pretrained(&#39;bert-base-uncased&#39;) # 对输入文本进行编码 inputs = tokenizer(&quot;Hello, this is a test sentence.&quot;, return_tensors=&quot;pt&quot;) # 获取模型输出 outputs = model(**inputs) print(outputs.last_hidden_state.shape) ``` --- ### 相关问题 1. 如何为特定平台下载 Python 库的 `.whl` 文件? 2. 如何在离线环境中安装 Python 库? 3. 如何使用 `pip download` 下载依赖包? 4. 如何查看 `.whl` 文件支持的 Python 版本和平台? 5. 如何手动下载并安装 Hugging Face 的 `transformers` 库?
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值