【项目管理】Java离线版语音识别-语音转文字

系统:Win10
Java:1.8.0_333
IDEA:2020.3.4
Gitee:https://gitee.com/lijinjiang01/SpeechRecognition

1.项目前言

最近在做一个鬼畜视频的时候,需要处理大量语音文件,全部都是 wav 格式的,然后我想把这些语音转成文字,不过这些语音有几千条,这时候我就想能不能用 Java 实现。
不过现在主流的语音识别像百度。讯飞好像都不支持 Java 离线版,在查找一些资料后,我准备使用 Vosk

2.Vosk介绍

Vosk 官网:https://alphacephei.com/vosk/
Vosk 是言语识别工具包,Vosk 最大的优点是:

  1. 支持二十+种语言 - 中文,英语,印度英语,德语,法语,西班牙语,葡萄牙语,俄语,土耳其语,越南语,意大利语,荷兰人,加泰罗尼亚语,阿拉伯, 希腊语, 波斯语, 菲律宾语,乌克兰语, 哈萨克语, 瑞典语, 日语, 世界语, 印地语, 捷克语, 波兰语
  2. 移动设备上脱机工作-Raspberry Pi,Android,iOS
  3. 使用简单的 pip3 install vosk 安装
  4. 每种语言的手提式模型只有是 50Mb, 但还有更大的服务器模型可用
  5. 提供流媒体 API,以提供最佳用户体验(与流行的语音识别 python 包不同)
  6. 还有用于不同编程语言的包装器-java / csharp / javascript等
  7. 可以快速重新配置词汇以实现最佳准确性
  8. 支持说话人识别

至于选择 Vosk 的原因,我想大概因为他们是 Apache-2.0 开源项目吧,而且他们还提供了中文模型,这省了很多事不是么

3.项目开发

3.1 项目准备

这里的项目准备只做一个 wav 语音识别,能够供自己使用就行了
首先,我们需要新建一个 Maven Java 项目,然后导入相关的依赖

<!-- 获取音频信息 -->
<dependency>
    <groupId>org</groupId>
    <artifactId>jaudiotagger</artifactId>
    <version>2.0.3</version>
</dependency>

<!-- 语音识别 -->
<dependency>
    <groupId>net.java.dev.jna</groupId>
    <artifactId>jna</artifactId>
    <version>5.7.0</version>
</dependency>
<dependency>
    <groupId>com.alphacephei</groupId>
    <artifactId>vosk</artifactId>
    <version>0.3.32</version>
</dependency>

这里除了 vosk 相关依赖,我还导入了 jaudiotagger 这个获取音频信息的依赖,因为等会我们需要自动获取音频的采样率(SampleRate),有兴趣的小伙伴可以看一下我另一篇文章:Java获取Wav文件的采样率SampleRate

那么为什么我需要获取音频的采样率呢?这里我们看下 Vosk 官方给的示例代码:
https://github.com/alphacep/vosk-api/blob/master/java/demo/src/main/java/org/vosk/demo/DecoderDemo.java

package org.vosk.demo;

import java.io.FileInputStream;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;

import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.UnsupportedAudioFileException;

import org.vosk.LogLevel;
import org.vosk.Recognizer;
import org.vosk.LibVosk;
import org.vosk.Model;

public class DecoderDemo {
    
    

    public static void main(String[] argv) throws IOException, UnsupportedAudioFileException {
    
    
        LibVosk.setLogLevel(LogLevel.DEBUG);

        try (Model model = new Model("model");
                    InputStream ais = AudioSystem.getAudioInputStream(new BufferedInputStream(new FileInputStream("../../python/example/test.wav")));
                    Recognizer recognizer = new Recognizer(model, 16000)) {
    
    

            int nbytes;
            byte[] b = new byte[4096];
            while ((nbytes = ais.read(b)) >= 0) {
    
    
                if (recognizer.acceptWaveForm(b, nbytes)) {
    
    
                    System.out.println(recognizer.getResult());
                } else {
    
    
                    System.out.println(recognizer.getPartialResult());
                }
            }

            System.out.println(recognizer.getFinalResult());
        }
    }
}

这个示例代码里有两个重要点:

  1. model:也就是 new Model(“model”) 这里,这里需要我们指定模型位置
  2. sampleRate:也就是 new Recognizer(model, 16000) 这里,他这里的示例代码写死了 sampleRate 为 16000 Hz,不过每个音频的采样率不可能都一样,我需要识别的音频采样率基本都是 44100 Hz,所以这里我们需要将他改为自动识别

3.2 model 准备

我们需要实现离线语音识别,那么就得将模型下载到本地电脑。下载地址为官网的 Models 模块:https://alphacephei.com/vosk/models
我们直接找到 Chinese 分类,这里有 2 个模型,上面较小的 40 多M的是轻量级模型,适用于手机等移动设备;下面 1 个多G的适用于服务器的,很明显模型越大识别语音正确率越高
在这里插入图片描述
这里我们两个都下载,等会对比下正确率和速率,下载下来是两个压缩包,直接解压到 D 盘,等会选择路径方便(怎么方便怎么来)。
在这里插入图片描述
解压之后如下
在这里插入图片描述

3.3 测试音频准备

音频下载地址:https://download.csdn.net/download/qq_35132089/86723883
测试音频已经上传到 CSDN 的资源库,设置下载积分为0,有兴趣的小伙伴可以下载测试玩玩
这里一共准备了 8 段音频,共 62 个字

01.wav: 保家卫国
02.wav: 这个世界需要希望
03.wav: 我们的勇气绝对不能动摇
04.wav: 德玛西亚
05.wav: 正义要靠法律要么靠武力
06.wav: 为了那些不能作战的人而战
07.wav: 勇往直前
08.wav: 生命不息战斗不止

3.4 代码实现

捋清楚思路,接下来实现就比较简单了,我这里写一个 Swing 的项目,准备到时候选择 wav 文件直接语音识别,或者选择一个文件夹,解析该目录下所有的 wav 音频文件
关键代码:

import com.lijinjiang.beautyeye.ch3_button.BEButtonUI;
import org.jaudiotagger.audio.AudioFile;
import org.jaudiotagger.audio.wav.WavFileReader;
import org.vosk.Model;
import org.vosk.Recognizer;
import javax.sound.sampled.AudioSystem;
import javax.swing.*;
import javax.swing.filechooser.FileNameExtensionFilter;
import javax.swing.filechooser.FileSystemView;
import java.awt.*;
import java.io.*;

public class MainFrame {
    
    
    private JFrame mainFrame; // 主界面
    private final JPanel contentPanel = new JPanel(null); // 内容面板
    private String modelPath; // 模型位置
    private File chooseFile; // 选择的文件夹或文件
    private JTextField pathField; // 模型位置文本框
    private JTextField fileField; // 文件路径文本框
    private JTextArea displayArea; // 展示区域

    private JLabel timeLabel; // 显示耗时标签

    public MainFrame() {
    
    
        modelPath = System.getProperty("user.dir") + "/src/main/resources/vosk-model-small-cn-0.22"; // 初始化模型
        System.out.println(modelPath);
        createFrame();
    }

    /**
     * 创建主窗口
     */
    private void createFrame() {
    
    
        mainFrame = new JFrame();
        mainFrame.setTitle("语音识别");
        createOperatePanel();
        createDisplayPane();
        createTimeLabel();
        mainFrame.add(contentPanel);
        mainFrame.setSize(new Dimension(800, 600));
        mainFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        mainFrame.setLocationRelativeTo(null);
        mainFrame.setVisible(true);
    }

    /**
     * 创建操作面板
     */
    private void createOperatePanel() {
    
    
        JButton pathBtn = new JButton("选择模型");
        pathBtn.setLocation(10, 10);
        pathBtn.setSize(new Dimension(80, 36));
        pathBtn.setFocusable(false); // 不绘制焦点
        pathBtn.addActionListener(e -> showChoosePathDialog());


        pathField = new JTextField();
        pathField.setEditable(false);
        pathField.setLocation(100, 10);
        pathField.setSize(new Dimension(250, 36));


        JButton fileBtn = new JButton("选择文件");
        fileBtn.setFocusable(false); // 不绘制焦点
        fileBtn.addActionListener(e -> showChooseFileDialog());
        fileBtn.setLocation(360, 10);
        fileBtn.setSize(new Dimension(80, 36));


        fileField = new JTextField();
        fileField.setEditable(false);
        fileField.setLocation(450, 10);
        fileField.setSize(new Dimension(250, 36));

        // 开始执行按钮
        JButton startBtn = new JButton("执行");
        startBtn.addActionListener(e -> execute());
        startBtn.setUI(new BEButtonUI().setNormalColor(BEButtonUI.NormalColor.green));
        startBtn.setFocusable(false); // 不绘制焦点
        startBtn.setLocation(710, 10);
        startBtn.setSize(new Dimension(70, 36));


        contentPanel.add(pathBtn);
        contentPanel.add(pathField);
        contentPanel.add(fileBtn);
        contentPanel.add(fileField);
        contentPanel.add(startBtn);
    }

    /**
     * 创建展示面板
     */
    private void createDisplayPane() {
    
    
        JScrollPane scrollPane = new JScrollPane();
        displayArea = new JTextArea();
        scrollPane.setViewportView(displayArea);
        displayArea.setEditable(false);
        displayArea.setBorder(null);
        scrollPane.setSize(new Dimension(775, 480));
        scrollPane.setLocation(8, 56);
        contentPanel.add(scrollPane);
    }

    private void createTimeLabel() {
    
    
        timeLabel = new JLabel();
        timeLabel.setHorizontalAlignment(SwingConstants.RIGHT); // 文本靠右对齐
        timeLabel.setSize(new Dimension(100, 36));
        timeLabel.setLocation(680, 530);
        contentPanel.add(timeLabel);
    }

    /**
     * 选择模型位置
     */
    private void showChoosePathDialog() {
    
    
        JFileChooser fileChooser = new JFileChooser(); // 初始化一个文件选择器
        String pathValue = pathField.getText().trim();
        if (pathValue.length() == 0) {
    
    
            FileSystemView fsv = fileChooser.getFileSystemView(); // 获取文件系统网关
            fileChooser.setCurrentDirectory(fsv.getHomeDirectory()); // 设置桌面为当前文件路径
        } else {
    
    
            // 设置上一次选择路径为当前文件路径
            File file = new File(pathValue);
            File parentFile = file.getParentFile();
            if (parentFile == null) {
    
    
                fileChooser.setCurrentDirectory(file);
            } else {
    
    
                fileChooser.setCurrentDirectory(parentFile);
            }
        }
        fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); // 可选文件夹和文件
        fileChooser.setMultiSelectionEnabled(false); // 设置可多选
        int result = fileChooser.showOpenDialog(mainFrame);
        if (result == JFileChooser.APPROVE_OPTION) {
    
    
            File file = fileChooser.getSelectedFile();
            modelPath = file.getAbsolutePath();
            pathField.setText(modelPath); // 将选择的文件路径写入到文本框
        }
    }

    /**
     * 选择需要转换成文字的文件夹或者文件
     * 文件夹:表示该目录下一层所有 wav 都需要转成文字
     * 文件:表示只需要将该文件转换成文字即可
     */
    private void showChooseFileDialog() {
    
    
        JFileChooser fileChooser = new JFileChooser(); // 初始化一个文件选择器
        String fileValue = fileField.getText().trim();
        if (fileValue.length() == 0) {
    
    
            FileSystemView fsv = fileChooser.getFileSystemView();
            fileChooser.setCurrentDirectory(fsv.getHomeDirectory()); // 设置桌面为当前文件路径
        } else {
    
    
            // 设置上一次选择路径为当前文件路径
            File file = new File(fileValue);
            File parentFile = file.getParentFile();
            if (parentFile == null) {
    
    
                fileChooser.setCurrentDirectory(file);
            } else {
    
    
                fileChooser.setCurrentDirectory(parentFile);
            }
        }
        fileChooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES); // 可选文件夹和文件
        fileChooser.setMultiSelectionEnabled(false); // 设置可多选
        fileChooser.removeChoosableFileFilter(fileChooser.getAcceptAllFileFilter()); // 不显示所有文件的下拉选
        fileChooser.addChoosableFileFilter(new FileNameExtensionFilter("wav", "wav"));
        int result = fileChooser.showOpenDialog(mainFrame);
        if (result == JFileChooser.APPROVE_OPTION) {
    
    
            chooseFile = fileChooser.getSelectedFile();
            fileField.setText(chooseFile.getAbsolutePath()); // 将选择的文件路径写入到文本框
        }
    }

    /**
     * 开始执行操作
     */
    private void execute() {
    
    
        displayArea.setText(""); // 执行后清空显示面板
        if (modelPath == null || 0 == modelPath.length()) {
    
    
            JOptionPane.showMessageDialog(mainFrame, "模型位置不能为空", "错误", JOptionPane.ERROR_MESSAGE);
            return;
        }
        if (chooseFile == null) {
    
    
            JOptionPane.showMessageDialog(mainFrame, "未选择文件夹或者音频文件", "错误", JOptionPane.ERROR_MESSAGE);
            return;
        }
        long startTime = System.currentTimeMillis();

        // 用于测试进度条的线程
        Thread thread = new Thread() {
    
    

            public void run() {
    
    
                if (chooseFile.isDirectory()) {
    
     // 如果是文件夹,则遍历里面每个文件
                    File[] files = chooseFile.listFiles(pathname -> pathname.getName().endsWith(".wav"));
                    if (files != null) {
    
    
                        for (File childFile : files) processFile(childFile);
                    }
                } else {
    
    
                    processFile(chooseFile);
                }

            }

        };
        //显示进度条测试对话框
        ProgressBar.show((Frame) null, thread, "语音正在识别中,请稍后...", "执行结束", "取消");
        // 否则直接处理该文件
        long endTime = System.currentTimeMillis();
        String msg = "耗时:" + (endTime - startTime) + "ms";
        timeLabel.setText(msg);
    }

    /**
     * 处理文件:语音转文字
     */
    private void processFile(File file) {
    
    
        try (Model model = new Model(modelPath);//该段是模型地址
             InputStream ais = AudioSystem.getAudioInputStream(new BufferedInputStream(new FileInputStream(file))); //该段是要转的语言文件,仅支持wav
             Recognizer recognizer = new Recognizer(model, getSampleRate(file))) {
    
     //该段中12000是语言频率(Hz),需要大于8000,可以自行调整

            int bytes;
            byte[] b = new byte[4096];
            while ((bytes = ais.read(b)) >= 0) {
    
    
                recognizer.acceptWaveForm(b, bytes);
            }
            displayArea.append(file.getName() + " ");
            displayArea.append(recognizer.getFinalResult() + System.lineSeparator());
        } catch (Exception e) {
    
    
            JOptionPane.showMessageDialog(mainFrame, e.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
            e.printStackTrace();
        }
    }

    /**
     * 获取音频文件的采样率
     */
    private Float getSampleRate(File file) throws Exception {
    
    
        WavFileReader fileReader = new WavFileReader();
        AudioFile audioFile = fileReader.read(file);
        String sampleRate = audioFile.getAudioHeader().getSampleRate();
        return Float.parseFloat(sampleRate);
    }
}

4.效果演示

4.1 界面效果

界面效果就是如下图所示

  1. 点击选择模型,就可以指定模型文件夹路径
  2. 点击选择文件,就可以指定需要识别的语音或文件夹
  3. 最后点击执行即可开始语音识别
  4. 识别成功,会将对应音频文件名和识别的文字写在下面的文本域中
  5. 最后还会将使用时间显示在界面右下角

在这里插入图片描述

4.2 单个文件语音识别

4.2.1 轻量模型

这里选择模型选择轻量模型,文件只识别第一个文件
在这里插入图片描述

4.2.2 通用模型

这里模型替换为了通用模型,语音文件不变,然后执行
在这里插入图片描述

4.2.3 两者对比

单个文件语音识别 轻量模型 通用模型
正确率 100% 100%
消耗时间 2021 ms 21093 ms
这里我们可以发现同一个文件,轻量模型只用了 1/10 的时间就识别完成,且成功率均为 100%,不过这也存在这段语言比较简单的原因

4.3 多个语音文件识别

4.3.1 轻量模型

这里的模型还是选择轻量模型,语音文件选择整个语音文件夹
在这里插入图片描述

4.3.2 通用模型

这里模型替换为了通用模型,语音文件不变,然后执行;因为这里耗费时间太长了,GIF 做了抽帧处理
在这里插入图片描述

4.3.3 两者对比

多个文件语音识别 轻量模型 通用模型
正确率 71.43% 84.12%
消耗时间 14332 ms 176040 ms
这里我们可以发现同一个文件,轻量模型只用了 8% 的时间就识别完成,不过这里的正确率只有 71% 左右,通用模型的正确率却有 84% 左右,当然因为其中存在一些专有名词如德玛西亚,两次都没有识别出来

5.项目总结

综合比较下来我们基本可以得出结论:如果对正确率要求没那么高的情况下,轻量模型完全符合我们的要求;而且通用模型的时间消耗确实太大了,可能需要一些方法来减少时间的消耗。

猜你喜欢

转载自blog.csdn.net/qq_35132089/article/details/127069618