手把手帮你视频转文本(1-视频转音频)

2020-06-11 10:14:15 浏览数 (1)

本系列将介绍如何一步步实现将mp4视频中的语音对话,自动转换为文本,并输出到word文档中。这里第一篇,先完成视频转音频处理。本项目全部代码也已经全部开源到码云(https://gitee.com/coolpine/thomas),可直接下载试用。

总体技术架构

下图是整体转换流程:

  1. 先将mp4视频文件,通过ffmpeg工具库,批量转换为pcm音频文件(语音识别服务仅支持该格式)
  2. 基于百度云的技术,将pcm文件上传到百度对象存储BOS中,并将日志等记录到本地mysql数据库。
  3. pcm文件上传完毕后,调用免费的语音识别(录音转写)服务,创建离线录音转写任务。
  4. 查询转写成功的任务,并将相关转写结果存储到本地mysql库中。
  5. 基于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:

  1. 基于org.bytedeco的ffmpeg和ffmpeg-platform来实现用java调用ffmpeg。
  2. 因为每集视频的片头和片尾歌曲时长基本固定,但每集视频总时长不一样,通过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实现语音转录。

0 人点赞