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
猜你喜欢
- Thread parameterThread_t = null; private void Print_DetailForm_S
- dom4j是一个非常优秀的Java XML API,具有性能优异、功能强大和极端易用使用的特点,同时它也是一个开放源工具。可以在这个地址ht
- 一、layui.use1、LayUI的官方使用文档:https://www.layui.com/doc/2、layui的内置模块不是默认就加
- 1、servlet层package com.ycz.controller;import com.alibaba.fastjson.JSON;
- 前言Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据
- 我们知道,值类型的变量是在堆栈上分配内存的,而引用类型包括System.Object的对象是在堆上分配内存的,基于这一特点,当值类型被类型转
- 简单说一下(定义)什么是原型模式:原型模式是用于创建重复的对象,同时又能保证性能。用一个已经创建的实例作为原型,通过复制该原型对象来创建一个
- 一、堆的概念堆的定义:n个元素的序列{k1 , k2 , … , kn}称之为堆,当且仅当满足以下条件时:(1)ki
- 运行本实例,将显示一个用户登录界面,输入用户名(hpuacm)和密码(1111)后,单击"登录"按钮,将弹出如下图所示的
- 环境IDEA :2020.1Maven:3.5.6SpringBoot: 2.0.9 (与此前整合的版本2.3.3 不同,版本适配问题,为配
- 弃用内容先来纠正一个误区。主要之前在版本更新介绍的时候,存在一些表述上的问题。导致部分读者认为这次的更新是Datasource本身初始化的调
- 学习过java基础,最近趁着大量课余时间想学习Android开发。百度很多资料Android studio,由Google开发的开发工具,那
- 方式一: 配置文件 application.propertiesserver.port=7788方式二: java启动命令# 以应用参数的方
- 实例如下所示:package test;import java.util.ArrayList;import java.util.Collec
- 一、队列的结构队列:一种操作受限的线性表,只允许在线性表的一端进行插入,另一端进行删除,插入的一端称为队尾,删除的一端称为队头通过 动态顺序
- 一、问题最近在做代码重构,代码工程采用了Controller/Service/Dao分层架构,Dao层使用了Mybatis-Plus框架。在
- @Value从Nacos配置中心获取值并自动刷新在使用Nacos作为配置中心时,除了@NacosValue可以做到自动刷新外,nacos-s
- 1、问题起因最近做项目时遇到了需要多用户之间通信的问题,涉及到了WebSocket握手请求,以及集群中WebSocket Session共享
- 1、long long 和 __int64在C++ Primer当中提到的64位的int只有long long,但是在实际各种各样的C++编
- 使用区间使用 in 运算符来检测某个数字是否在指定区间内,区间格式为x..y:实例fun main(args: Array<Strin