体验了开源的离线语音识别模型 wenetspeech,准确度很高。分享一些使用心得。

体验了开源的离线语音识别模型 wenetspeech,准确度很高。分享一些使用心得。

关于wenetspeech的公众号文章

Leaderboard 新增 WenetSpeech 数据集预训练 WeNet 模型

https://mp.weixin.qq.com/s/8L6vZpH1-uiDR3J5fzw-9Q

TIOBE 测试集开源计划(关于如何下载wenetspeech预训练模型文件)

https://mp.weixin.qq.com/s?__biz=MzA4NTAwNjkzMA&mid=2247484328&idx=1&sn=ffd2650be8e7d73e7bf2856f0db584a0

认识wenetspeech

官方网站:https://wenet-e2e.github.io/WenetSpeech

使用超过一万小时的高质量语音数据进行训练,准确率(可信度)可达95%以上。实际使用下来,使用2021-11-02发布的预训练模型,确实准确率很高。

注意:这个网站上的“Download”,是指的下载一万+小时的训练素材,并不是指下载训练好的模型。

如何开始使用

wenetspeech是一个预训练的模型,也就是说它并不是一个可执行软件,要使用它,可以使用wenet(https://github.com/wenet-e2e/wenet)。

Wenet的GitHub介绍是“Production First and Production Ready End-to-End Speech Recognition Toolkit(翻译:生产第一和生产就绪的端到端语音识别工具包)”,可以用它来进行模型训练,同时,项目下的runtime(https://github.com/wenet-e2e/wenet/tree/main/runtime)也提供了android端和x86服务端的运行时,可以结合模型进行语音识别。

尝试x86运行时

x86提供了 Docker 的方式安装,简单方便。详情:https://github.com/wenet-e2e/wenet/blob/main/runtime/server/x86/README_CN.md

识别wav文件的命令如下

/home/wenet/runtime/server/x86/build/decoder_main --chunk_size -1 --model_path /path/to/final.zip --dict_path /path/to/words.txt --wav_path /path/to/you/file.wav

不过,当前(2021-12-1)该docker里用的是 aishell2/20210602 的模型,尝试了几次识别,准确性较低。
而且,官方也没有提供如何下载wenetspeech预训练模型的方式。下面就是摸索如何获得这个95%准确度的模型。

下载wenetspeech预训练模型

上面的文章《TIOBE 测试集开源计划》里,提到了如何下载预训练模型(model),这里总结一下:

  • 打开https://github.com/SpeechColab/Leaderboard,在readme里的“3. Model Zoo”里,展开“Local Engine”,有该项目目前公开的语音引擎(也就是预训练模型),里面有“wenet_wenetspeech”
  • 将该项目clone到本地(windows下使用不便,因为要使用阿里云的oss cli,可以选择wsl)
  • 根据readme,在项目根目录下运行“ops/pull model wenet_wenetspeech”。
    • 如果是第一次运行,会提示先运行“utils/install_aliyun_oss_client.sh”安装oss cli,运行完在utils下会多一个名为“oss”的文件。
    • 继续运行pull,会提示需要oss.cfg,根据提示发送邮件即可,对方会自动回复oss.cfg的内容,内容类似
[Credentials]
language=EN
endpoint=oss-cn-hangzhou.aliyuncs.com
accessKeyID=xxxxxxxxxxxx
accessKeySecret=xxxxxxxxxxxxxxxxxxxxxx
* 将该文本保存到“credentials/aliyun_oss.cfg”里,初始化配置就完成了。

* 运行pull命令之后,会开始下载模型文件,我下载的是2021-11-02的模型,总共513MB。
* 下载完了,就可以在“models/wenet_wenetspeech/assets”下看到熟悉(观察decoder_main的命令)的“final.zip”和“words.txt”了。

使用wenetspeech预训练模型

这里官方在docker hub上发布的“mobvoiwenet/wenet:0.5.0”镜像,无法直接使用wenetspeech的模型文件,估计是代码比较旧、模型比较新的原因,具体没有深究,因为我这里放弃了使用官方docker,而是自己重新编译docker image。

创建wenetspeech docker image

在上面下载完模型后,其实也同样下载了 models/wenet_wenetspeech/docker/Dockerfile 文件,可以直接通过这个Dockerfile来创建自己的image,具体方式查看docker文档即可。

提示:我在国内,这 Dockerfile 里面的 git clone 经常失败,所以,我是直接使用 ubuntu 镜像,在里面一条条执行 Dockerfile 里的命令来编译的(其实命令不多,四五条命令就完了,就是最后一步cmake的耗时比较长)。在container里编译完后,记得 docker commit 保存为镜像。

体验wenetspeech识别

命令和文档里的没有区别,只要让docker能访问到模型文件和wav文件即可(我用-v做磁盘映射)

以下是我在windows下运行的命令样例:

# 我把模型文件和wav文件都存放在D:\ffmpeg目录下
# 我自己创建的image,命名为wenetspeech:timestamp
docker run --rm -it -v d:/ffmpeg/:/data/ffmpeg wenetspeech:timestamp /home/wenet/runtime/server/x86/build/decoder_main --chunk_size -1 --model_path /data/ffmpeg/wenet/wenet_wenetspeech/assets/final.zip --dict_path /data/ffmpeg/wenet/wenet_wenetspeech/assets/words.txt --wav_path /data/ffmpeg/wenet/test_wav/xxx16k10s.wav

问题及解决方案

websocket的准确度较低

官方还有websocket的样例及server端 websocket_server_main ,经过体验,同样的wav文件,使用 websocket_server_main,准确度大大低于使用 decoder_main ,所以,除非你有很高的实时性要求必须使用websocket,不然建议使用decoder_main

长语音文件识别

decoder_main 无法一次识别时长太长的文件(经测试180秒没问题,官方说最大300秒但是我测试的250秒也会报错),这个问题可以使用ffmpeg将长语音切成多个180秒的短语音文件,分别识别即可。

时间戳

decoder_main 的识别结果不带时间戳。这个问题参考这个issue(https://github.com/wenet-e2e/wenet/issues/604)。需要修改源码(改动不大,我这个不会c语言的都能改)。

程序本身识别的时候,对每个字都会有一个开始-结束时间标记,只不过decoder_main.cc里面没有把这些信息输出。同样参考上面issue里提到的一个文件 https://github.com/wenet-e2e/wenet/blob/604231391c81efdf06454dbc99406bbc06cb030d/runtime/core/bin/label_checker_main.cc#L212-L218

修改的地方:

    std::string final_result;
    std::string timestamp_str;//增加一个变量存储时间戳的内容

原来的位置:

      if (state == wenet::DecodeState::kEndpoint) {
        decoder.Rescoring();
        final_result.append(decoder.result()[0].sentence);
        decoder.ResetContinuousDecoding();
      }

增加代码后:

      if (state == wenet::DecodeState::kEndpoint) {
        decoder.Rescoring();
        final_result.append(decoder.result()[0].sentence);
        const wenet::DecodeResult &result = decoder.result()[0];
        std::stringstream ss;
        for (const auto &w : result.word_pieces) {
          ss << " " << w.word << " " << w.start << " " << w.end;
        }
        timestamp_str.append(ss.str());
        decoder.ResetContinuousDecoding();
      }

原来110行左右的位置:

    if (decoder.DecodedSomething()) {
      final_result.append(decoder.result()[0].sentence);
    }

增加代码后:

    if (decoder.DecodedSomething()) {
      final_result.append(decoder.result()[0].sentence);
      const wenet::DecodeResult &result = decoder.result()[0];
      std::stringstream ss;
      for (const auto &w : result.word_pieces) {
        ss << " " << w.word << " " << w.start << " " << w.end;
      }
      timestamp_str.append(ss.str());
    }

原先输出的位置:

      buffer << wav.first << " " << final_result << std::endl;

增加一行输出:

      buffer << wav.first << " " << final_result << std::endl;
//我这里按照我的习惯输出,你也可以修改为任意其他形式。不输出wav.first,在一次识别多个文件(-wav_scp)的时候,可能不好识别属于哪个文件,我不用这个所以就去掉了
      buffer << "TIMESTAMP " << timestamp_str << std::endl;

到这里,源码的修改的结束了,重新编译也很简单:

cd ${x86dir}/build
cmake --build .

重新识别就能看到打印出来的带时间戳的内容了。(以下是无穷小亮某一期https://www.bilibili.com/video/BV1Vh411i7DB的语音识别结果)

test 这都是蜘蛛见过这么好看的吗很多人都害怕蜘蛛但是上回有一个小姑娘跟我说说我特别喜欢蜘蛛那姑娘才七八岁我说我特别高兴因为你能够发现蜘蛛的美为了让大家了解蜘蛛扭转对蜘蛛的物解博物杂志做了这一期蜘蛛专体这一期里边有简单的内容比如告诉你身边常见的几种蜘蛛都叫什么你像这个白鹅高脚猪白垩巨蟹猪还有这个屋里边经常出现的一些小蜘蛛还有花园里边你看这个就是路边花园里边经常出现的还有这个应该都很熟悉吧还有很萌的一类蜘蛛叫跳珠他们那个眼睛特别大特别可爱可以扭转很多人对蜘蛛的恐惧而且他还会转眼珠跳珠也是非常适合饲养的这个告诉你应该怎么养一只跳珠另外呢还有进阶版就是你如果成为一个蜘蛛的研究者你会怎么研究蜘蛛还有包括蜘蛛网是怎么织的各种文章反正以蜘蛛为专题并且做的这么专业这么详细的我还没听说国内哪个杂志这么做过另外这一期还有一些寻找梦幻觉的科考经理还有丘砂鸭的知识欧洲恐龙的故事各国的火绳枪手这天文里也有我们插画是孟凡蒙画的一些火枪手的手绘点我视频下方链接你会得到这期杂志和一张火枪手的大海报这俩搁一块十八块钱包邮大家玩的命买
TIMESTAMP  这 0 600 都 600 720 是 720 880 蜘 880 1040 蛛 1040 1440 见 1440 1800 过 1800 1920 这 1920 2040 么 2040 2120 好 2120 2240 看 2240 2360 的 2360 2480 吗 2480 2720 很 2720 3000 多 3000 3120 人 3120 3200 都 3200 3360 害 3360 3520 怕 3520 3680 蜘 3680 3840 蛛 3840 4080 但 4080 4320 是 4320 4440 上 4440 4600 回 4600 4800 有 4800 5000 一 5000 5120 个 5120 5240 小 5240 5400 姑 5400 5560 娘 5560 5680 ...

时间戳的小问题

通过以上的样例可以看到,首先输出的不是“词”,而是“字”。其次,静音的部分在输出结果中不会被跳过,举个例子,两句话之间假设间隔4秒,则上一句话最后一个字的end time,会被延长2秒,下一句话第一个字的start time 也是往前延长2秒。看起来这两个字都持续了2秒多,如果对时间戳比较严肃的场景就不太适用。不过我目前看不懂源码,不清楚是runtime的代码没优化,还是识别的部分没优化,我个人更倾向于是runtime没优化。

识别效率

测试机器i5 10400@2.9GHz,耗时大概在原语音时长的½到⅓之间,还可以接受。CPU占用25%左右,内存占用比较高,比如上面无穷小亮的那个音频,占用了4G内存。

整体还是可以接受的,如果是部署到服务器端的话可以通过多进程和分布式,提高整体的识别效率。

总结

这个模型的准确度目前真的是出乎我的意料。据网站介绍,2.0版本也在准备中( preparing ),可以期待2.0的表现。目前这个模型,应付一下语音转写,然后做人工校对,应该很有帮助。

Comments are closed.