Flutter开发Mac桌面应用实现自动提取生成视频字幕文件
作者:loongwind 发布时间:2023-05-11 05:25:58
前言
前段时间准备做一个视频,最后需要添加字幕,手动添加太麻烦了就想在网上找一个能自动提取字幕的软件或服务,确实是找到了,但是免费版基本上都有诸多限制,比如现在视频时长等等,后来在 Github 找到一个开源的版本是使用云平台的语音识别实现的,云服务的语音识别是有免费的额度的,对于个人使用来说一般是够用了,项目地址:video-srt-windows ,大致实现流程如下:
使用 ffmpeg 提取视频的音频文件
将音频文件上传到云平台的对象存储
调用云平台的语音识别 api 进行文字识别
生成字幕文件
下载 release 版本测试了一下效果还可以,只需要修改个别识别有误的词就行,功能完全满足我的需求;但是遗憾的是该项目只提供了 Windows 版本,而没有 Mac 版本的 ,虽然作者也提供了一个 CLI 命令行版本可以在 Mac 上使用,但是对于普通用户来说使用起来还是不是很方便,于是产生了开发一个 Mac 版。
思路
该开源项目作者是用 Go 语言写的,我本人擅长的是 Flutter 开发,所以首先想到的就是通过 Flutter 开发一个 Mac 版的桌面应用,将 CLI 项目通过 Go 编译成 Mac 的可执行文件内置到 Flutter 项目中,再通过 Dart 调用 shell 命令进行执行从而实现软件的功能。
效果
实现
下面就来看看整个项目是如何一步步最终实现上面的效果的。
编译 Mac 版可执行文件
首先将 CLI 项目 clone 到本地,然后使用 go build
命令编译对应平台的可执行文件,如下:
# Mac M1/M2 Arm 架构 CPU
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -o video-srt-arm64 main.go
# Mac Amd 架构 CPU
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o video-srt-amd64 main.go
执行以上文件分别生成 arm
和 amd
架构的可执行文件 video-srt-arm64 和 video-srt-amd64。
内置可执行文件和 ffmpeg
将上一步生成的对应平台的可执行文件修改为 video-srt
和配置文件 config.ini
以及 ffmpeg
文件放到一个文件夹中打包成 video-srt.zip
压缩包减少包体积。
因为项目需要使用到 ffmpeg ,所以需要把 ffmpeg 也内置到项目中
通过 Xcode 将 video-srt.zip
文件添加到项目的 Resources 文件夹下
然后就是通过代码在程序启动时将内置的压缩包解压到指定位置,这里解压使用了 archive
库,核心代码如下:
// 目录名称
const String VIDEO_SRT = "video-srt";
class ZipRepository{
static Future<void> unzip(String zipFile, String targetDir) async{
final inputStream = InputFileStream(zipFile);
final archive = ZipDecoder().decodeBuffer(inputStream);
extractArchiveToDisk(archive, targetDir);
return;
}
static Future<void> unzipVideoSrt() async{
var workDirPath = await PathUtils.getWorkDirPath();
// 创建工作目录下的 video-srt 目录
var videoSrtFile = Directory("$workDirPath/$VIDEO_SRT");
// 如果已经存在则不重复解压
if(await videoSrtFile.exists()){
return;
}
// 解压
await unzip(VIDEO_SRT_ZIP_PATH, "$workDirPath");
return;
}
}
这里还用到了 path_provider
库用于获取相关目录:
// 工作目录名称
const String WORK_DIR_NAME = "videoSrt";
class PathUtils{
static String? workDirPath;
static Future<String> getWorkDirPath() async{
if(workDirPath != null){
return workDirPath!;
}
// 获取 library 目录
Directory tempDir = await getLibraryDirectory();
var workDir = "${tempDir.path}/$WORK_DIR_NAME";
var dir = Directory(workDir);
if(! (await dir.exists())){
await dir.create();
}
workDirPath = workDir;
return workDir;
}
}
在应用启动时调用解压将内置的 video-srt.zip
内容解压到系统 library 下的 videoSrt 目录下。
设置配置信息
video-srt
的配置是用的 config.ini
文件存储的,所以在代码里需要读写 ini 文件,这里使用了一个 ini
的三方库,config.ini
里包含如下配置内容:
#字幕相关设置
[srt]
#智能分段处理:true(开启) false(关闭)
intelligent_block=true
#阿里云Oss对象服务配置
#文档:https://help.aliyun.com/document_detail/31827.html?spm=a2c4g.11186623.6.582.4e7858a85Dr5pA
[aliyunOss]
# OSS 对外服务的访问域名
endpoint=
# 存储空间(Bucket)名称
bucketName=
# 存储空间(Bucket 域名)地址
bucketDomain=
accessKeyId=
accessKeySecret=
#阿里云语音识别配置
#文档:
[aliyunClound]
# 在管控台中创建的项目Appkey,项目的唯一标识
appKey=
accessKeyId=
accessKeySecret=
这里创建一个 ConfigModel
用于存放相关配置,然后使用 ini 库的 Config 进行读写封装,代码如下 :
// 读取配置数据
static Future<ConfigModel> readIniData() async{
var workDir = await PathUtils.getWorkDirPath();
var iniPath = "$workDir/$VIDEO_SRT/$CONFIG_NAME";
Completer<ConfigModel> completer = Completer();
File(iniPath).readAsLines()
.then((lines) => Config.fromStrings(lines))
.then((Config config){
var iniModel = ConfigModel();
iniModel.intelligent_block = (config.get("srt", "intelligent_block") ?? "true").toLowerCase() == "true";
iniModel.oss_endpoint = config.get("aliyunOss", "endpoint");
iniModel.oss_bucketName = config.get("aliyunOss", "bucketName") ;
iniModel.oss_bucketDomain = config.get("aliyunOss", "bucketDomain") ;
iniModel.oss_accessKeyId = config.get("aliyunOss", "accessKeyId") ;
iniModel.oss_accessKeySecret = config.get("aliyunOss", "accessKeySecret") ;
iniModel.voice_appKey = config.get("aliyunClound", "appKey") ;
iniModel.voice_accessKeyId = config.get("aliyunClound", "accessKeyId") ;
iniModel.voice_accessKeySecret = config.get("aliyunClound", "accessKeySecret") ;
iniModel.go_path = config.get("go", "goPath") ;
completer.complete(iniModel);
});
return completer.future;
}
// 写配置数据
static Future<void> writeIniData(ConfigModel iniModel) async{
Config config = Config();
config.addSection("srt");
config.set("srt", "intelligent_block", iniModel.intelligent_block.toString());
config.addSection("aliyunOss");
config.set("aliyunOss", "endpoint", iniModel.oss_endpoint ?? "");
config.set("aliyunOss", "bucketName", iniModel.oss_bucketName ?? "");
config.set("aliyunOss", "bucketDomain", iniModel.oss_bucketDomain ?? "");
config.set("aliyunOss", "accessKeyId", iniModel.oss_accessKeyId ?? "");
config.set("aliyunOss", "accessKeySecret", iniModel.oss_accessKeySecret ?? "");
config.addSection("aliyunClound");
config.set("aliyunClound", "appKey", iniModel.voice_appKey ?? "");
config.set("aliyunClound", "accessKeyId", iniModel.voice_accessKeyId ?? "");
config.set("aliyunClound", "accessKeySecret", iniModel.voice_accessKeySecret ?? "");
config.addSection("go");
config.set("go", "goPath", iniModel.go_path ?? "");
var workDir = await PathUtils.getWorkDirPath();
var iniPath = "$workDir/$VIDEO_SRT/$CONFIG_NAME";
await File(iniPath).writeAsString(config.toString());
return;
}
执行命令
配置也写好了,接下来就需要执行编译好的 video-srt 命令来提取视频字幕,这里使用 shell 命令来执行,用到了 process_run
库,核心代码如下:
static Future<void> runVideoSrt(String targetFilePath, Function(String) callback) async{
if(targetFilePath.isEmpty){
return;
}
// 获取工作目录
var workDir = await PathUtils.getWorkDirPath();
var controller = ShellLinesController();
var shell = Shell(stdout: controller.sink, verbose: false);
// 切换路径到工作目录下的 video-srt 下
shell = shell.pushd("$workDir/$VIDEO_SRT");
try {
// 给 ffmpeg 添加执行权限
await shell.run("chmod +x ffmpeg");
// 给 video-srt 添加执行权限
await shell.run("chmod +x video-srt");
} on ShellException catch (_) {
// We might get a shell exception
}
// 监听执行结果
controller.stream.listen((event) {
callback(event);
});
try {
// 执行视频提取字幕命令
await shell.run("./video-srt $targetFilePath");
} on ShellException catch (_) {
// We might get a shell exception
}
shell = shell.popd();
return;
}
UI 实现
核心功能实现了,接下来就是完成界面的开发,让我们可以方便的进行相关配置和选择要生成字幕的视频文件。
为了实现 Mac 风格的界面,这里使用了 macos_ui
库,可以让我们更快捷的实现相关界面。
界面分成两部分,左边菜单和右边内容展示区域,效果如下:
代码如下:
class MainView extends StatefulWidget {
const MainView({super.key});
@override
State<MainView> createState() => _MainViewState();
}
class _MainViewState extends State<MainView> {
int _pageIndex = 0;
@override
Widget build(BuildContext context) {
return PlatformMenuBar(
menus: const [
PlatformMenu(
label: 'VideoSrtMacos',
menus: [
// 状态栏左上角退出按钮
PlatformProvidedMenuItem(
type: PlatformProvidedMenuItemType.quit,
),
],
),
],
child: MacosWindow(
sidebar: Sidebar(
minWidth: 200,
builder: (context, scrollController) => SidebarItems(
currentIndex: _pageIndex,
onChanged: (index) {
setState(() => _pageIndex = index);
},
items: const [
SidebarItem(leading: MacosIcon(CupertinoIcons.home),label: Text('首页'),),
SidebarItem(leading: MacosIcon(CupertinoIcons.settings),label: Text('配置'),),
SidebarItem(leading: MacosIcon(CupertinoIcons.helm),label: Text('帮助'),),
SidebarItem(leading: MacosIcon(CupertinoIcons.info),label: Text('关于'),),
],
),
),
child: IndexedStack(
index: _pageIndex,
children: const [
// 主页
HomePage(),
// 配置页面
ConfigView(),
HelpView(),
AboutView()
],
),
),
);
}
}
然后分别实现对应的子界面即可实现整个完整的功能,这部分就是纯粹的 flutter 界面开发的内容了,这里就不过多赘述了。
最后
虽然使用 Flutter 进行开发已经很久了,但是更多还是进行 Android、iOS 的开发,桌面端虽然也写过一些Demo,但是还未真正使用 Flutter 去开发一个桌面应用,虽然这个项目功能很简单但也算是一个不错的练手项目。
Github 地址:video-srt-mac
来源:https://juejin.cn/post/7203249440500596794


猜你喜欢
- Spring Security和Shiro的区别相同点1、认证功能2、授权功能3、加密功能4、会话管理5、缓存支持6、rememberMe功
- FeignClient重试机制造成的接口幂等性Feign源码分析,其实现类在 SynchronousMethodHandler,实现方法是p
- 本文实例讲述了Android编程设计模式之原型模式。分享给大家供大家参考,具体如下:一、介绍原型模式是一个创建型的模式。原型二字表明了该模型
- 文章来源:aspcn 作者:孙雯重复和并发服务器这个应用程序被当作一个重复的服务器.因为它只有在处理完一个进程以后才会接受另一个连接.更多的
- 权限提升方法:一种方法:1、在AndroidManifest.xml中的manifest节点中添加android:sharedUserId=
- 本文实例讲述了Android开发使用自定义View将圆角矩形绘制在Canvas上的方法。分享给大家供大家参考,具体如下:前几天,公司一个项目
- 第一种方法:一、测试如下,直接设置小圆点不是图标二、准备工作1.在drawable创建dot.xml,设置小圆点,比较方便<?xml
- 内存泄漏:是指内存得不到GC的及时回收,从而造成内存占用过多,从而导致程序Crash,也就是常说的OOM。 一、static 先来看下面一段
- 目录事件最基本的用法理解路由事件WPF中使用路由事件升级了传统应用开发中的事件,在WPF中使用路由事件能更好的处理事件相关的逻辑,我们从这篇
- 身为一名开发人员,大家都知道,我们经常会在项目中大量的编写许多重复的代码,比如说public Entity find(String id);
- ubuntu 安装jdk 的两种方法总结:1:通过ppa(源) 方式安装.2:通过官网下载安装包安装.这里推荐第1种,因为可以通过 apt-
- 本文主要介绍了关于c#和java base64不一致的解决方法,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧不一致的问题不
- 本文实例讲述了C#中foreach语句使用break暂停遍历的方法。分享给大家供大家参考。具体分析如下:下面的代码演示了在C#中使用fore
- 有些时候我们做的程序需要进度条,而vs提供的控件不是我们想要的。先看效果图:进度条闪烁动画,当然背景可设为Transparent之前想手绘进
- 本文实例为大家分享了Javaweb统计在线人数示的具体代码,供大家参考,具体内容如下1. 实现功能统计在线人数显示每个人的sessionId
- 本文实例讲述了JAVA中的final关键字用法。分享给大家供大家参考,具体如下:根据上下文环境,java的关键字final也存在着细微的区别
- 前言最近在优化自己之前基于Spring AOP的统一响应体的实现方案。什么是统一响应体呢?在目前的前后端分离架构下,后端主要是一个RESTf
- 终日惶惶,不知归路;一日写起代码,突发奇想,若是在运行时发现自定义上下文的数据丢失,我们该如何解决处理数据丢失的问题?问题复现一下,大家看下
- 这篇文章主要介绍了java多线程关键字final和static详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价
- 本文实例为大家分享了android利用handler实现倒计时的具体代码,供大家参考,具体内容如下xml<?xml version=&