Web端如何实现语音识别? 我用 WebRTC + Whisper 找到了答案

大家好,欢迎来到前端研习圈。

前言

随着 AI 的不断发展,越来越多的应用都开始引入 AI 来优化原本的 ⌈功能流程,用户体验⌋ 等,对于一名前端工程师来说,该如何融入到 AI 这个大环境中呢,如果你没有头绪,那不妨跟着本系列的步伐,我们一起去探探路~

上期我分享了 openAI 的语音识别模型 whisper 的使用体验,本期我们就来实践一下,在 web端 来实现一个 语音识别 功能。

功能分析

首先我们可以先基于 web端语音识别 这个功能点,提出两个问题

  1. 语音如何采集。即在程序中,我们如何获取到我们说的话(音频数据)
  2. 如何传输语音 。如何将采集到的音频数据交给 whisper 进行识别

这两个问题其实也很简单,都有成熟的解决方案

对于 语音采集 ,在 web端 我们可以用浏览器基于 WebRTC 技术提供的流媒体相关接口调用麦克风来完成

对于 语音传输 ,当然还是走传统的 http,架设一个服务端提供 语音识别 接口来供前端调用

技术选型

基于上述简要的分析,web应用部分比较简单

  • 构建工具:vite
  • 框架:React
  • 组件库:antd
  • 语音采集:recordrtc, webm-to-wav-converter

由于 whisper 需要通过 python 调用,因此服务端基于 python 技术栈来做

  • 服务器框架:uvicornfastapi
  • 音频处理:librosa
  • 字词转换:zhconv

实现过程

做好了相关技术栈的选定后,我们就开始着手搭建环境和编码

web应用

为了测试 语音识别 这个功能,我们简单设计一下 UI

  • 做一个 按钮 来控制 开始录音结束录音
  • 做一个 列表 来展示每次录音的 识别结果

OK,有了基本的设计,我们用 vitereact-ts 模板来初始化项目

sh 复制代码
pnpm create vite whisper-demo-for-web --template react-ts

创建好项目之后,安装依赖,启动环境这些过程就不赘述了,我们直接进入编码阶段

首先来实现按钮部分,我们设计一个 RecordButtom 组件

  • WebRTC 的调用以及 语音识别接口 的调用一并封装在内
  • 提供一个 onResult 回调,用于返回识别结果
jsx 复制代码
import React, { useState } from 'react';
import { Button, Spin } from 'antd';
import AudioRTC from "../sdk/AudioRTC";
import AudioAI from '../sdk/AudioAI';

// 定义识别结果
export type Result = {
    // 识别内容 or 错误信息
    text: string
    // 识别耗时
    transcribe_time?: number
}

type Props = {
    // 识别完成事件
    onResult?: (result: Result) => void;
}

// 状态枚举
enum Status {
    // 空闲
    IDLE = 'idle',
    // 记录中
    RECORDING = 'recording',
}

// 文本映射
const labelMapper = {
    [Status.IDLE]: '开始录音',
    [Status.RECORDING]: '停止录音',
}

// 过程转换映射
const processStatusMapper = {
    [Status.IDLE]: Status.RECORDING,
    [Status.RECORDING]: Status.IDLE,
}

// 初始化AudioAI
const audioAI = new AudioAI(); 
// 初始化AudioRTC
const audioRTC = new AudioRTC(); 

export default function RecordButton(props: Props) {
    const [status, setStatus] = useState(Status.IDLE);
    const [loading, setLoading] = useState(false);

    // 点击事件
    const onClick = async () => {
        if (status === Status.IDLE) {
            // 开始录制
            audioRTC.startRecording();
        }

        if (status === Status.RECORDING) {
            // 结束录制
            await audioRTC.stopRecording();
            // 获取 wav 格式的 blob
            const waveBlob = await audioRTC.getWaveBlob();

            try {
                setLoading(true);

                // 调用接口 - 语音识别
                const response = await audioAI.toText(waveBlob)

                props.onResult?.(response);
            } catch (error) {
                props.onResult?.({ text: `${error}` });
            } finally {
                setLoading(false)
            }
        }

        setStatus(processStatusMapper[status]);
    }

    return (
        <>
            <Button onClick={onClick}>{labelMapper[status]}</Button>
            {
                loading &&
                <Spin tip="识别中..." size="small">
                    <span className="content" />
                </Spin>
            }
        </>
    )
}

在按钮的实现过程中,我们又抽象了两个模块出来

  • AudioAI 提供 AI 的处理能力,由于功能比较简单,只实现了一个 toText 方法,本质上是调用服务端接口,获取识别结果后返回
ts 复制代码
// AudioAI.ts
import { fetchAudioToText } from "./service";

export default class AudioAI {
    async toText(audio: Blob) {
        const response = await fetchAudioToText(audio)

        return response.data
    }
}
ts 复制代码
// service.ts
import axios from 'axios'

const api = axios.create({
    baseURL: '/api',
})

export const fetchAudioToText = async (audio: Blob) => {
    const formData = new FormData()

    formData.append('audio', audio)
    formData.append('timestamp', String(+new Date()))

    return api.post('/audioToText', formData)
}

  • AudioRTC 封装调用流媒体的操作,提供开始录制,结束录制,获取音频流等相关 API
ts 复制代码
// AudioRTC.ts
import RecordRTC from 'recordrtc';
import { getWaveBlob } from 'webm-to-wav-converter'

export default class AudioRTC {
    stream!: MediaStream;

    recorder!: RecordRTC

    /**
     * 开始录制
     */
    async startRecording() {
        if (!this.recorder) {
            this.stream = await navigator.mediaDevices.getUserMedia({ audio: true })

            this.recorder = new RecordRTC(this.stream, {
                type: 'audio'
            })
        }

        this.recorder.startRecording()
    }

    /**
     * 结束录制
     * @returns 
     */
    stopRecording(): Promise<Blob> {
        if (!this.recorder) {
            return Promise.reject('Recorder is not initialized')
        }

        return new Promise((resolve) => {
            this.recorder.stopRecording(() => {
                const blob = this.recorder.getBlob()

                resolve(blob)
            })
        })
    }

    /**
     * 获取 blob
     * @returns 
     */
    getWaveBlob() {
        const blob = this.recorder.getBlob()

        return getWaveBlob(blob, false);
    }
}

OK,按钮部分的编码至此结束,接下来就是列表部分的实现,由于比较简单,我们就直接写在 根组件 中,大致逻辑就是

  • 存储一个 识别结果 列表
  • 注册一个 onResult 事件,在收到识别结果时将其推入列表
  • 渲染列表,列表项中展示序号,识别耗时及内容等信息
jsx 复制代码
import { useState } from 'react'
import './App.css'
import RecordButton, { Result } from './components/RecordButton'
import { Divider, List, Typography } from 'antd'

function App() {
    const [list, setList] = useState<Result[]>([])

    const onResult = (result: Result) => {
        setList((prev) => [...prev, result])
    }

    return (
        <div className="App">
            <RecordButton onResult={onResult} />

            <Divider orientation="left">识别记录</Divider>
            <List
                bordered
                dataSource={list}
                renderItem={(item, index) => (
                    <List.Item style={{ justifyContent: 'flex-start' }}>
                        {/* 序号 */}
                        <span>[{index + 1}]</span>
                        {/* 耗时 */}
                        <Typography.Text mark style={{ margin: '0 6px' }}>
                            识别耗时:{item.transcribe_time}s
                        </Typography.Text>
                        {/* 内容 */}
                        <span style={{ margin: '0 6px' }}>{item.text}</span>
                    </List.Item>
                )}
            />
            
        </div>
    )
}

export default App

以上就是 web应用 的主要实现逻辑,接下来我们看下实际效果

现在我们已经完成了一个简单页面,接下来就到了 服务端 的部分了

服务端

我们用 python 的 FastAPI 来快速实现一个 /audioToText 接口,这个接口具体需要做哪些工作呢?我们来分析一下

  • 参数部分,接收一个 audio 参数,类型是文件流;接收一个 timestamp 参数,是前端通过 FormData 传递的时间戳
  • 功能部分,从文件流中读取到音频数据,交给 whisper 去识别,然后将识别出的文本中的繁体字转为简体字,同时我们再分别记录一下这两步的耗时,提供一个参考指标

繁体转简体也有现成的库zhconv,所以这个接口的实现就很简单了,只需要把我们的业务流程组装起来即可

接下来我们开始编码

py 复制代码
# main.py
import time
import uvicorn
import io
import librosa
import zhconv

from fastapi import FastAPI, File, Form, UploadFile
from whisper_client import WhisperClient

app = FastAPI()

whisper = WhisperClient("medium")

@app.post('/audioToText')
def create_audio_to_text(
    timestamp: str = Form(),
    audio: UploadFile = File(...)
):
    # 读取字节流
    bt = audio.file.read()
    # 转为 BinaryIO
    memory_file = io.BytesIO(bt)
    # 获取音频数据
    data, sample_rate = librosa.load(memory_file, sr=None)

    resample_data = librosa.resample(data, orig_sr=sample_rate, target_sr=16000)

    transcribe_start_time = time.time()
    # 语音识别
    text = whisper.transcribe(resample_data)
    transcribe_end_time = time.time()

    convert_start_time = time.time()
    # 繁体转简体
    text = zhconv.convert(text, 'zh-hans')
    convert_end_time = time.time()

    return {
        'status': 'ok',
        'text': text,
        'transcribe_time': transcribe_end_time - transcribe_start_time,
        'convert_time': convert_end_time - convert_start_time
    }

uvicorn.run(app, host="0.0.0.0", port=9090)

编码完成后,在终端启动一下服务来测试一下效果

css 复制代码
python main.py

效果演示

这里分别录入了两句话,都正确识别了

耗时方面则和机器的性能有关,在我们使用的这台测试机上 识别耗时 平均3s左右,体验稍差,这也说明进步空间

完整代码整理后会上传到 github 上,有兴趣的同学可以留意一下

github.com/Mumujiangua...

结尾

本期我们使用 WebRTC + Whisper 实现了一个简单 语音识别 功能,但实际应用中,语音识别 往往会结合 语音唤醒,降噪 等需求一起考虑,所以我们目前仅仅是完成了第一步

其实对于前端同学来说,python 几乎没有什么学习成本,结合 python 丰富的生态,快速开发一个web系统还是很轻松的,下期我们再继续分享~

OK,那么本期就到这里,我们下期见~

相关推荐
麻雀飞吧5 分钟前
最新量化学习路径,交易认知和技术实现要并行
人工智能·python
北辰alk8 分钟前
我用Seed Evolving做了个“代码遗产抢救者”和“旅行规划师”——一个开发者的双面实战日记
人工智能
腾渊信息科技公司13 分钟前
Spring Boot对接MES实战:视觉检测数据自动同步方案
java·人工智能·spring boot·后端·计算机视觉·ai·软件需求
一点一木13 分钟前
从60首歌到1个网站:输入你的故事,还你一首歌
前端·github
人间凡尔赛14 分钟前
WAIC 2026 落幕,AI Agent 开发已进入 Harness 架构时代
人工智能·架构
-XWB-34 分钟前
【LLM】Agent Planning 完全指南:8 种纯 LLM 范式 + 8 种混合规划模式详解(二)
人工智能·经验分享·aigc·学习方法·ai编程
微硬创新43 分钟前
耐达讯自动化 CC-Link IE 转 PROFINET网关在污水厂智能化升级实战
人工智能·物联网·网络协议·自动化·信息与通信
Smoothcloud润云1 小时前
国内GPU算力租赁平台横向测评:资源、成本、稳定性三维对比
人工智能·ai·云计算·gpu算力·gpu
点PY1 小时前
一种从CTA图像中去除扫描床的方法及装置
人工智能·计算机视觉