网页端语音识别技术使用方案

144 阅读2分钟

需求:项目是一个支持PC和手机端的 ai 对话平台,需要支持用户语音输入问题,ai 回答。

方案a :浏览器提供 webkitSpeechRecognition 可以直接识别用户语音并转换文字。

使用:


// 检测是否支持 Web Speech API ===
const isWebSpeechSupported = typeof window !== 'undefined' && !!(window.SpeechRecognition || window.webkitSpeechRecognition);
if(!isWebSpeechSupported) return alert("不支持语音识别!")
recognition = new webkitSpeechRecognition();
recognition.lang = 'zh-CN'; // 设置语音识别的语言为中文
recognition.continuous = true

recognition.onresult = (event) => {
  const speechResult = event.results[0][0].transcript;
  console.log("语音识别结果:", speechResult);
  eventBus.emit('senMsg', speechResult);  // 自己的业务代码
};
recognition.onerror = (event) => {
  console.log("发生错误:", event.error);
};

recognition.start() 开始录音

recognition.stop() 结束录音 -- 会执行 onresult 回调函数返回识别语音文字

recognition.abort() 终止所有操作 - 不会执行 onresult 回调函数

缺陷:SpeechRecognition 不支持IOS平台。

图片来源:ai

方案b:前端录音 + 后端 ASR(语音转文字)

用户点击“开始说话” → 前端调用麦克风录制音频(WAV/MP3) → 上传音频到服务器 → 服务器调用 ASR 引擎(如阿里云、腾讯云、百度、Google Cloud) → 返回识别文本给前端

使用:这里使用百度 ASR,音频流需要转换 PCM 二进制

pcm和blob的区别:pcm是原始音频数据的编码格式, Blob(Binary Large Object)是浏览器中用于封装二进制数据的容器对象

图片来源 :ai

相关代码:

try {
  let recognition = null
  let chunks: Blob[] = [];
  // 请求麦克风权限准备录音
  let stream = await navigator.mediaDevices.getUserMedia({ audio: true });
  recognition = new MediaRecorder(stream);
  // 监听录音数据 并收集到 chunks 二进制数组
  recognition.ondataavailable = (e) => {
    chunks = []
    chunks.length = 0
    if (e.data.size > 0) {
      chunks.push(e.data);
    }
  };
  // 3. 准备接收录音数据
  // 录音结束时处理完整音频
  recognition.onstop = async () => {
    // ✅ 使用真实的 mimeType
    const mimeType = recognition!.mimeType || 'audio/mp4';
    const blob = new Blob(chunks, { type: mimeType });   // 

    // 1. 转 PCM / 看自己的 ASR 云服务是否需要
    const pcmData = await audioBlobToPcm(blob);
    console.log('PCM 长度:', pcmData.byteLength, '字节');

    // 2. 调用 百度 ASR 接口
    const result = await fetchAsrBaiduPro(pcmData, "自己的token");
    console.log('语音识别返回', result);


    if (result.err_no === 0 && result.result?.[0]) {
      const text = result.result[0];
      console.log('识别结果:', text);
      eventBus.emit('senMsg', text);  // 处理自己的业务
    } else {
      console.log('ASR 错误:', result.err_msg);
      toast.fail("时间太短,未识别到语音")
      // alert(`识别失败: ${result.err_msg}`);
    }
    // 直接下载音频查看----用于测试音频代码
    // const url = URL.createObjectURL(blob);
    // const a = document.createElement('a');
    // a.href = url;
    // a.download = `recording-${Date.now()}.${getExt(mimeType)}`;
    // document.body.appendChild(a);
    // a.click();
    // document.body.removeChild(a);

    // // 释放内存(延迟一点,确保下载完成)
    // setTimeout(() => URL.revokeObjectURL(url), 1000);
  };

} catch (error) {
  console.log(error);  // 这里监听不同报错处理业务
  toast.fail('找不到符合条件的音视频设备')
}

blob 转 PCM 函数封装

// utils/audioToPcm.ts

/**
 * 将音频 Blob(如 audio/mp4)解码为 16kHz/16bit/单声道 PCM ArrayBuffer
 * @param audioBlob - 录音得到的音频 Blob
 * @returns PCM 格式的 ArrayBuffer  
 */
export async function audioBlobToPcm(audioBlob: Blob): Promise<ArrayBuffer> {
    return new Promise((resolve, reject) => {
        const fileReader = new FileReader();
        fileReader.onload = async () => {
            try {
                const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)();
                const audioData = await audioContext.decodeAudioData(fileReader.result as ArrayBuffer);

                // 重采样到 16kHz(可选,但百度要求 16k)
                const sampleRate = 16000;
                let offlineContext = new OfflineAudioContext(1, audioData.duration * sampleRate, sampleRate);
                let source = offlineContext.createBufferSource();
                source.buffer = audioData;
                source.connect(offlineContext.destination);
                source.start();

                const renderedBuffer = await offlineContext.startRendering();

                // 转为 16-bit PCM
                const float32Array = renderedBuffer.getChannelData(0); // 单声道
                const int16Array = new Int16Array(float32Array.length);
                for (let i = 0; i < float32Array.length; i++) {
                    // 浮点 [-1, 1] → 16位整数 [-32768, 32767]
                    const s = Math.max(-1, Math.min(1, float32Array[i]));
                    int16Array[i] = s < 0 ? s * 0x8000 : s * 0x7FFF;
                }

                resolve(int16Array.buffer);
            } catch (err) {
                reject(err);
            }
        };
        fileReader.onerror = () => reject(fileReader.error);
        fileReader.readAsArrayBuffer(audioBlob);
    });
}

fetch调用 百度 ASR 函数封装:

// utils/asrBaiduPro.ts

interface AsrBaiduProResult {
    err_no: number;
    err_msg: string;
    result?: string[];
    sn?: string;
}

/**
 * 调用百度语音识别 Pro API(需传入 PCM 音频数据)
 * @param pcmData - PCM 音频的 ArrayBuffer(16kHz, 16bit, 单声道)
 * @param token - 百度 OAuth2.0 access_token
 * @param cuid - 设备唯一标识(建议固定值或生成)
 * @returns 识别结果文本数组(result[0] 为主结果)
 */
export async function fetchAsrBaiduPro(
    pcmData: ArrayBuffer,
    token: string,
    cuid: string = 'web-client-' + Date.now()
): Promise<AsrBaiduProResult> {
    const rate = 16000;
    const channel = 1;
    const format = 'pcm';
    const devPid = 80001; // 普通话 + 带标点 + 数字识别

    // 将 PCM 转为 Base64
    const base64Speech = arrayBufferToBase64(pcmData);
    const len = pcmData.byteLength;

    const payload = {
        format,
        rate,
        channel,
        cuid:'自己的标识',
        dev_pid: devPid,
        token,
        len,
        speech: base64Speech,
    };

    const url = 'https://vop.baidu.com/pro_api';

    try {
        console.log('【ASR】开始请求百度 Pro API...');
        const t0 = performance.now();
        // xixi 是 vite 的 proxy 代理 https://vop.baidu.com/pro_api
        const response = await fetch("/xixi", {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json; charset=utf-8',
                Accept: 'application/json',
            },
            body: JSON.stringify(payload),
        });

        const t1 = performance.now();
        console.log(`【ASR】请求耗时: ${(t1 - t0).toFixed(2)} ms`);

        if (!response.ok) {
            throw new Error(`HTTP ${response.status}: ${response.statusText}`);
        }

        const data: AsrBaiduProResult = await response.json();
        return data;
    } catch (error) {
        console.error('【ASR】请求失败:', error);
        throw error;
    }
}

// 辅助函数:ArrayBuffer → Base64
function arrayBufferToBase64(buffer: ArrayBuffer): string {
    let binary = '';
    const bytes = new Uint8Array(buffer);
    for (let i = 0; i < bytes.length; i++) {
        binary += String.fromCharCode(bytes[i]);
    }
    return btoa(binary);
}

方案c:微信提供的录音 api

主要针对低版本的 IOS 和 微信对 getUserMedia 不支持。

正常使用 getUserMedia必须满足:新版本的 iOS(≥14.3) + 较新版本的微信(≥8.0.0) + 用户主动触发了录音(如点击按钮) + 页面是 HTTPS

developers.weixin.qq.com/doc/service…

方案b和c区别:

来源 ai

兼容性差异:

来源 ai

低版本IOS兼容:

function startRecording() {
  // 1. 优先尝试 Web 录音(适合 Android/PC)
  if (isWechat() && isIOS()) {
    // 2. iOS 微信 → 强制使用 wx.startRecord()
    useWechatRecord();
  } else {
    // 3. 其他环境 → 尝试 Web 录音
    useWebRecord().catch(() => {
      // 4. 失败则降级到 wx.startRecord()(如果是微信)
      if (isWechat()) useWechatRecord();
      else alert('当前环境不支持录音');
    });
  }
}