本系列将介绍如何一步步实现将mp4视频中的语音对话,自动转换为文本,并输出到word文档中。这里第一篇,先完成视频转音频处理。本项目全部代码也已经全部开源到码云(https://gitee.com/coolpine/thomas),可直接下载试用。
总体技术架构
下图是整体转换流程:
- 先将mp4视频文件,通过ffmpeg工具库,批量转换为pcm音频文件(语音识别服务仅支持该格式)
- 基于百度云的技术,将pcm文件上传到百度对象存储BOS中,并将日志等记录到本地mysql数据库。
- pcm文件上传完毕后,调用免费的语音识别(录音转写)服务,创建离线录音转写任务。
- 查询转写成功的任务,并将相关转写结果存储到本地mysql库中。
- 基于docx4j库,将数据库中的录音转写结果,导出为规范化的word文档。
转换结果示例
我们这里实现的是将 《托马斯和他的朋友们第18季》20集MP4视频,最终转换为一个word故事文档:
下面是第一集具体对话文本表格:
视频转音频
视频转音频基于ffmpeg库来实现。ffmpeg是一个强大的跨平台音视频记录、转换方案(官网说法:A complete, cross-platform solution to record, convert and stream audio and video)
ffmpeg主要是以命令行模式来实现音视频转换和处理,我们这里实现的功能有:
- 将mp4文件中片头和片尾音乐剔除,截取中间片段。
- 将截取后的mp4文件,转换为pcm文件。
- 基于ffplay验证pcm可播放情况。
截取mp4文件中间片段的命令基本格式为:
代码语言:javascript复制ffmpeg -ss [start] -i [input] -t [duration] -c copy [output]
ffmpeg -ss [start] -i [input] -to [end] -c copy [output]
# 例如,以下是将t1801.mp4文件,截取从第30秒开始,截止到524秒,并保存为c1-1801.mp4文件:
ffmpeg -y -ss 30 -i t1801.mp4 -to 524 -c copy c1-1801.mp4
将mp4文件转换为pcm音频文件命令参数:
代码语言:javascript复制-i 输入文件
-an 去除音频流
-vn 去除视频流
-acodec 设置音频编码
-f 强制指定输入或输出文件的编码
-ac 设置音频轨道数
-ar 设置音频采用频率
-y 不经过确认,直接覆盖同名文件
# 例如,以下是将t1801.mp4文件,去除视频流并用pcm_s16le进行音频编码,输出文件也采用s16le编码,同时音轨为1且采样频率为16000:
ffmpeg -i t1801.mp4 -vn -acodec pcm_s16le -f s16le -ac 1 -ar 16000 t1801.pcm
用ffplay播放pcm文件:
代码语言:javascript复制ffplay -ar 16000 -ac 1 -f s16le -i t1801.pcm
更多ffmpeg命令使用,参见官方文档:https://ffmpeg.org/ffmpeg.html
Java音视频处理
以上只是验证了在命令行模式下,基于ffmpeg进行基本音视频操作。因为要进行批量处理,我们还需要用编程的方式来调用ffmpeg:
- 基于org.bytedeco的ffmpeg和ffmpeg-platform来实现用java调用ffmpeg。
- 因为每集视频的片头和片尾歌曲时长基本固定,但每集视频总时长不一样,通过org.mp4parser的isoparser库实现读取每集总时长,动态拼装转换命令。
以下是引入的基本依赖:
代码语言:javascript复制<!--实现对视频文件读取-->
<dependency>
<groupId>org.mp4parser</groupId>
<artifactId>isoparser</artifactId>
<version>1.9.41</version>
</dependency>
<!--实现对ffmpeg的操作-->
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>ffmpeg</artifactId>
<version>4.2.2-1.5.3</version>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>ffmpeg-platform</artifactId>
<version>4.2.2-1.5.3</version>
</dependency>
以下是基于isoparser,读取MP4文件的总时长(秒数):
代码语言:javascript复制public long readDuration(Path mp4Path) {
if (Files.notExists(mp4Path) || !Files.isReadable(mp4Path)) {
log.warn("文件路径不存在或不可读 {}", mp4Path);
return 0;
}
try {
IsoFile isoFile = new IsoFile(mp4Path.toFile());
long duration = isoFile.getMovieBox().getMovieHeaderBox().getDuration();
long timescale = isoFile.getMovieBox().getMovieHeaderBox().getTimescale();
return duration / timescale;
} catch (IOException e) {
log.error("读取MP4文件时长出错", e);
return 0;
}
}
以下是将MP4文件进行截取,并转换为PCM文件:
代码语言:javascript复制/**
* 将单个PM4文件进行片头和片尾歌曲删除后,转换为PCM文件
*
* @param mp4Path
* @param pcmDir
* @return 转换完成后的pcm文件路径
*/
public Optional<String> convertMP4toPCM(Path mp4Path, Path pcmDir) {
long seconds = readDuration(mp4Path);
if (seconds == 0) {
log.warn("文件总时长为0");
return Optional.empty();
}
String ffmpeg = Loader.load(org.bytedeco.ffmpeg.ffmpeg.class);
String endTime = String.valueOf(seconds - 100 - 30);
File src = mp4Path.toFile();
//在当前源mp4文件目录下生成临时文件
String mp4TempFile = src.getParent() "\" System.currentTimeMillis() ".mp4";
//基于ffmpeg进行截取
ProcessBuilder cutBuilder = new ProcessBuilder(ffmpeg, "-ss", "30", "-i", mp4Path.toAbsolutePath().toString(),
"-to", endTime, "-c", "copy", mp4TempFile);
try {
cutBuilder.inheritIO().start().waitFor();
} catch (InterruptedException | IOException e) {
log.error("ffmpeg截取MP4文件出错", e);
return Optional.empty();
}
// 基于ffmpeg进行pcm转换
// 基于输入路径的md5值来命名,也可以基于系统时间戳来命名
String pcmFile = pcmDir.resolve(DigestUtils.md5Hex(mp4Path.toString()) ".pcm").toString();
ProcessBuilder pcmBuilder = new ProcessBuilder(ffmpeg, "-y", "-i", mp4TempFile, "-vn", "-acodec", "pcm_s16le",
"-f", "s16le", "-ac", "1", "-ar", "16000", pcmFile);
try {
//inheritIO是指将 子流程的IO与当前java流程的IO设置为相同
pcmBuilder.inheritIO().start().waitFor();
} catch (InterruptedException | IOException e) {
log.error("ffmpeg将mp4转换为pcm时出错", e);
return Optional.empty();
}
// 删除MP4临时文件
try {
Files.deleteIfExists(Paths.get(mp4TempFile));
} catch (IOException e) {
log.error("删除mp4临时文件出错", e);
}
//返回pcm文件路径
return Optional.of(pcmFile);
}
调用上述单个文件的处理方法,实现批量文件处理和转换:
代码语言:javascript复制/**
* 批量将MP4文件转换为PCM文件
*
* @param rootDir
* @param pcmDir
* @return 成功转换的PCM文件数
*/
public int batchConvertMP4toPCM(Path rootDir, Path pcmDir) {
if (Files.notExists(rootDir) || !Files.isDirectory(rootDir)) {
log.warn("mp4文件目录{}不存在", rootDir);
return 0;
}
if (Files.notExists(pcmDir)) {
//级联创建目录
try {
Files.createDirectories(pcmDir);
} catch (IOException e) {
log.error("创建文件夹出错", e);
}
}
AtomicInteger pcmCount = new AtomicInteger(0);
//遍历rootdir,获取所有目录下子目录和文件
try {
Files.list(rootDir).forEach(path -> {
if (Files.isDirectory(path)) {
//递归遍历下级目录
pcmCount.getAndAdd(batchConvertMP4toPCM(path, pcmDir));
}
if (Files.isRegularFile(path) && Files.isReadable(path) && path.getFileName()
.toString()
.endsWith("mp4")) {
Optional<String> pcmFile = this.convertMP4toPCM(path, pcmDir);
if (pcmFile.isPresent()) {
pcmCount.getAndIncrement();
}
}
});
} catch (IOException e) {
log.error("批量将MP4文件转换为PCM文件出错", e);
}
return pcmCount.get();
}
单个文件转换调用测试:
代码语言:javascript复制@Test
void cutTest() {
String file = "D:\dev2\project\thomas\local\videos\t1801.mp4";
String pcmdir = "D:\dev2\project\thomas\local\videos\pcm";
Path path = Paths.get(file);
util.convertMP4toPCM(path, Paths.get(pcmdir));
}
批量文件转换测试:
代码语言:javascript复制@Test
void batchTest() {
Path root = Paths.get("D:\dev2\project\thomas\local\videos\第18季");
Path pcmDir = Paths.get("D:\dev2\project\thomas\local\videos\pcm");
int pcmFiles = util.batchConvertMP4toPCM(root, pcmDir);
log.info("转换出PCM文件数{}", pcmFiles);
}
至此,读取mp4文件,转换为pcm文件并剔除片头和片尾,就基本完成了,接下来将为你介绍如何基于百度云SDK和API实现语音转录。