系統(tǒng):Win10
Java:1.8.0_333
IDEA:2020.3.4
Gitee:https://gitee.com/lijinjiang01/SpeechRecognition
1.項(xiàng)目前言
最近在做一個(gè)鬼畜視頻的時(shí)候,需要處理大量語(yǔ)音文件,全部都是 wav 格式的,然后我想把這些語(yǔ)音轉(zhuǎn)成文字,不過這些語(yǔ)音有幾千條,這時(shí)候我就想能不能用 Java 實(shí)現(xiàn)。
不過現(xiàn)在主流的語(yǔ)音識(shí)別像百度。訊飛好像都不支持 Java 離線版,在查找一些資料后,我準(zhǔn)備使用 Vosk
2.Vosk介紹
Vosk 官網(wǎng):https://alphacephei.com/vosk/
Vosk 是言語(yǔ)識(shí)別工具包,Vosk 最大的優(yōu)點(diǎn)是:
- 支持二十+種語(yǔ)言 - 中文,英語(yǔ),印度英語(yǔ),德語(yǔ),法語(yǔ),西班牙語(yǔ),葡萄牙語(yǔ),俄語(yǔ),土耳其語(yǔ),越南語(yǔ),意大利語(yǔ),荷蘭人,加泰羅尼亞語(yǔ),阿拉伯, 希臘語(yǔ), 波斯語(yǔ), 菲律賓語(yǔ),烏克蘭語(yǔ), 哈薩克語(yǔ), 瑞典語(yǔ), 日語(yǔ), 世界語(yǔ), 印地語(yǔ), 捷克語(yǔ), 波蘭語(yǔ)
- 移動(dòng)設(shè)備上脫機(jī)工作-Raspberry Pi,Android,iOS
- 使用簡(jiǎn)單的 pip3 install vosk 安裝
- 每種語(yǔ)言的手提式模型只有是 50Mb, 但還有更大的服務(wù)器模型可用
- 提供流媒體 API,以提供最佳用戶體驗(yàn)(與流行的語(yǔ)音識(shí)別 python 包不同)
- 還有用于不同編程語(yǔ)言的包裝器-java / csharp / javascript等
- 可以快速重新配置詞匯以實(shí)現(xiàn)最佳準(zhǔn)確性
- 支持說話人識(shí)別
至于選擇 Vosk 的原因,我想大概因?yàn)樗麄兪?Apache-2.0 開源項(xiàng)目吧,而且他們還提供了中文模型,這省了很多事不是么
3.項(xiàng)目開發(fā)
3.1 項(xiàng)目準(zhǔn)備
這里的項(xiàng)目準(zhǔn)備只做一個(gè) wav 語(yǔ)音識(shí)別,能夠供自己使用就行了
首先,我們需要新建一個(gè) Maven Java 項(xiàng)目,然后導(dǎo)入相關(guān)的依賴
<!-- 獲取音頻信息 -->
<dependency>
<groupId>org</groupId>
<artifactId>jaudiotagger</artifactId>
<version>2.0.3</version>
</dependency>
<!-- 語(yǔ)音識(shí)別 -->
<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 相關(guān)依賴,我還導(dǎo)入了 jaudiotagger 這個(gè)獲取音頻信息的依賴,因?yàn)榈葧?huì)我們需要自動(dòng)獲取音頻的采樣率(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());
}
}
}
這個(gè)示例代碼里有兩個(gè)重要點(diǎn):
- model:也就是 new Model(“model”) 這里,這里需要我們指定模型位置
- sampleRate:也就是 new Recognizer(model, 16000) 這里,他這里的示例代碼寫死了 sampleRate 為 16000 Hz,不過每個(gè)音頻的采樣率不可能都一樣,我需要識(shí)別的音頻采樣率基本都是 44100 Hz,所以這里我們需要將他改為自動(dòng)識(shí)別
3.2 model 準(zhǔn)備
我們需要實(shí)現(xiàn)離線語(yǔ)音識(shí)別,那么就得將模型下載到本地電腦。下載地址為官網(wǎng)的 Models 模塊:https://alphacephei.com/vosk/models
我們直接找到 Chinese 分類,這里有 2 個(gè)模型,上面較小的 40 多M的是輕量級(jí)模型,適用于手機(jī)等移動(dòng)設(shè)備;下面 1 個(gè)多G的適用于服務(wù)器的,很明顯模型越大識(shí)別語(yǔ)音正確率越高
這里我們兩個(gè)都下載,等會(huì)對(duì)比下正確率和速率,下載下來是兩個(gè)壓縮包,直接解壓到 D 盤,等會(huì)選擇路徑方便(怎么方便怎么來)。
解壓之后如下
3.3 測(cè)試音頻準(zhǔn)備
音頻下載地址:https://download.csdn.net/download/qq_35132089/86723883
測(cè)試音頻已經(jīng)上傳到 CSDN 的資源庫(kù),設(shè)置下載積分為0,有興趣的小伙伴可以下載測(cè)試玩玩
這里一共準(zhǔn)備了 8 段音頻,共 62 個(gè)字
01.wav: 保家衛(wèi)國(guó)
02.wav: 這個(gè)世界需要希望
03.wav: 我們的勇氣絕對(duì)不能動(dòng)搖
04.wav: 德瑪西亞
05.wav: 正義要靠法律要么靠武力
06.wav: 為了那些不能作戰(zhàn)的人而戰(zhàn)
07.wav: 勇往直前
08.wav: 生命不息戰(zhàn)斗不止
3.4 代碼實(shí)現(xiàn)
捋清楚思路,接下來實(shí)現(xiàn)就比較簡(jiǎn)單了,我這里寫一個(gè) Swing 的項(xiàng)目,準(zhǔn)備到時(shí)候選擇 wav 文件直接語(yǔ)音識(shí)別,或者選擇一個(gè)文件夾,解析該目錄下所有的 wav 音頻文件
關(guān)鍵代碼:
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); // 內(nèi)容面板
private String modelPath; // 模型位置
private File chooseFile; // 選擇的文件夾或文件
private JTextField pathField; // 模型位置文本框
private JTextField fileField; // 文件路徑文本框
private JTextArea displayArea; // 展示區(qū)域
private JLabel timeLabel; // 顯示耗時(shí)標(biāo)簽
public MainFrame() {
modelPath = System.getProperty("user.dir") + "/src/main/resources/vosk-model-small-cn-0.22"; // 初始化模型
System.out.println(modelPath);
createFrame();
}
/**
* 創(chuàng)建主窗口
*/
private void createFrame() {
mainFrame = new JFrame();
mainFrame.setTitle("語(yǔ)音識(shí)別");
createOperatePanel();
createDisplayPane();
createTimeLabel();
mainFrame.add(contentPanel);
mainFrame.setSize(new Dimension(800, 600));
mainFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
mainFrame.setLocationRelativeTo(null);
mainFrame.setVisible(true);
}
/**
* 創(chuàng)建操作面板
*/
private void createOperatePanel() {
JButton pathBtn = new JButton("選擇模型");
pathBtn.setLocation(10, 10);
pathBtn.setSize(new Dimension(80, 36));
pathBtn.setFocusable(false); // 不繪制焦點(diǎn)
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); // 不繪制焦點(diǎn)
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));
// 開始執(zhí)行按鈕
JButton startBtn = new JButton("執(zhí)行");
startBtn.addActionListener(e -> execute());
startBtn.setUI(new BEButtonUI().setNormalColor(BEButtonUI.NormalColor.green));
startBtn.setFocusable(false); // 不繪制焦點(diǎn)
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);
}
/**
* 創(chuàng)建展示面板
*/
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); // 文本靠右對(duì)齊
timeLabel.setSize(new Dimension(100, 36));
timeLabel.setLocation(680, 530);
contentPanel.add(timeLabel);
}
/**
* 選擇模型位置
*/
private void showChoosePathDialog() {
JFileChooser fileChooser = new JFileChooser(); // 初始化一個(gè)文件選擇器
String pathValue = pathField.getText().trim();
if (pathValue.length() == 0) {
FileSystemView fsv = fileChooser.getFileSystemView(); // 獲取文件系統(tǒng)網(wǎng)關(guān)
fileChooser.setCurrentDirectory(fsv.getHomeDirectory()); // 設(shè)置桌面為當(dāng)前文件路徑
} else {
// 設(shè)置上一次選擇路徑為當(dāng)前文件路徑
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); // 設(shè)置可多選
int result = fileChooser.showOpenDialog(mainFrame);
if (result == JFileChooser.APPROVE_OPTION) {
File file = fileChooser.getSelectedFile();
modelPath = file.getAbsolutePath();
pathField.setText(modelPath); // 將選擇的文件路徑寫入到文本框
}
}
/**
* 選擇需要轉(zhuǎn)換成文字的文件夾或者文件
* 文件夾:表示該目錄下一層所有 wav 都需要轉(zhuǎn)成文字
* 文件:表示只需要將該文件轉(zhuǎn)換成文字即可
*/
private void showChooseFileDialog() {
JFileChooser fileChooser = new JFileChooser(); // 初始化一個(gè)文件選擇器
String fileValue = fileField.getText().trim();
if (fileValue.length() == 0) {
FileSystemView fsv = fileChooser.getFileSystemView();
fileChooser.setCurrentDirectory(fsv.getHomeDirectory()); // 設(shè)置桌面為當(dāng)前文件路徑
} else {
// 設(shè)置上一次選擇路徑為當(dāng)前文件路徑
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); // 設(shè)置可多選
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()); // 將選擇的文件路徑寫入到文本框
}
}
/**
* 開始執(zhí)行操作
*/
private void execute() {
displayArea.setText(""); // 執(zhí)行后清空顯示面板
if (modelPath == null || 0 == modelPath.length()) {
JOptionPane.showMessageDialog(mainFrame, "模型位置不能為空", "錯(cuò)誤", JOptionPane.ERROR_MESSAGE);
return;
}
if (chooseFile == null) {
JOptionPane.showMessageDialog(mainFrame, "未選擇文件夾或者音頻文件", "錯(cuò)誤", JOptionPane.ERROR_MESSAGE);
return;
}
long startTime = System.currentTimeMillis();
// 用于測(cè)試進(jìn)度條的線程
Thread thread = new Thread() {
public void run() {
if (chooseFile.isDirectory()) { // 如果是文件夾,則遍歷里面每個(gè)文件
File[] files = chooseFile.listFiles(pathname -> pathname.getName().endsWith(".wav"));
if (files != null) {
for (File childFile : files) processFile(childFile);
}
} else {
processFile(chooseFile);
}
}
};
//顯示進(jìn)度條測(cè)試對(duì)話框
ProgressBar.show((Frame) null, thread, "語(yǔ)音正在識(shí)別中,請(qǐng)稍后...", "執(zhí)行結(jié)束", "取消");
// 否則直接處理該文件
long endTime = System.currentTimeMillis();
String msg = "耗時(shí):" + (endTime - startTime) + "ms";
timeLabel.setText(msg);
}
/**
* 處理文件:語(yǔ)音轉(zhuǎn)文字
*/
private void processFile(File file) {
try (Model model = new Model(modelPath);//該段是模型地址
InputStream ais = AudioSystem.getAudioInputStream(new BufferedInputStream(new FileInputStream(file))); //該段是要轉(zhuǎn)的語(yǔ)言文件,僅支持wav
Recognizer recognizer = new Recognizer(model, getSampleRate(file))) { //該段中12000是語(yǔ)言頻率(Hz),需要大于8000,可以自行調(diào)整
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(), "錯(cuò)誤", 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 界面效果
界面效果就是如下圖所示
- 點(diǎn)擊選擇模型,就可以指定模型文件夾路徑
- 點(diǎn)擊選擇文件,就可以指定需要識(shí)別的語(yǔ)音或文件夾
- 最后點(diǎn)擊執(zhí)行即可開始語(yǔ)音識(shí)別
- 識(shí)別成功,會(huì)將對(duì)應(yīng)音頻文件名和識(shí)別的文字寫在下面的文本域中
- 最后還會(huì)將使用時(shí)間顯示在界面右下角
4.2 單個(gè)文件語(yǔ)音識(shí)別
4.2.1 輕量模型
這里選擇模型選擇輕量模型,文件只識(shí)別第一個(gè)文件
4.2.2 通用模型
這里模型替換為了通用模型,語(yǔ)音文件不變,然后執(zhí)行
4.2.3 兩者對(duì)比
單個(gè)文件語(yǔ)音識(shí)別 | 輕量模型 | 通用模型 |
---|---|---|
正確率 | 100% | 100% |
消耗時(shí)間 | 2021 ms | 21093 ms |
4.3 多個(gè)語(yǔ)音文件識(shí)別
4.3.1 輕量模型
這里的模型還是選擇輕量模型,語(yǔ)音文件選擇整個(gè)語(yǔ)音文件夾
4.3.2 通用模型
這里模型替換為了通用模型,語(yǔ)音文件不變,然后執(zhí)行;因?yàn)檫@里耗費(fèi)時(shí)間太長(zhǎng)了,GIF 做了抽幀處理文章來源:http://www.zghlxwxcb.cn/news/detail-443058.html
4.3.3 兩者對(duì)比
多個(gè)文件語(yǔ)音識(shí)別 | 輕量模型 | 通用模型 |
---|---|---|
正確率 | 71.43% | 84.12% |
消耗時(shí)間 | 14332 ms | 176040 ms |
5.項(xiàng)目總結(jié)
綜合比較下來我們基本可以得出結(jié)論:如果對(duì)正確率要求沒那么高的情況下,輕量模型完全符合我們的要求;而且通用模型的時(shí)間消耗確實(shí)太大了,可能需要一些方法來減少時(shí)間的消耗。文章來源地址http://www.zghlxwxcb.cn/news/detail-443058.html
到了這里,關(guān)于【項(xiàng)目管理】Java離線版語(yǔ)音識(shí)別-語(yǔ)音轉(zhuǎn)文字的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!