Flutter開發(fā)Mac桌面應(yīng)用實(shí)現(xiàn)自動提取生成視頻字幕文件
前言
前段時間準(zhǔn)備做一個視頻,最后需要添加字幕,手動添加太麻煩了就想在網(wǎng)上找一個能自動提取字幕的軟件或服務(wù),確實(shí)是找到了,但是免費(fèi)版基本上都有諸多限制,比如現(xiàn)在視頻時長等等,后來在 Github 找到一個開源的版本是使用云平臺的語音識別實(shí)現(xiàn)的,云服務(wù)的語音識別是有免費(fèi)的額度的,對于個人使用來說一般是夠用了,項(xiàng)目地址:video-srt-windows ,大致實(shí)現(xiàn)流程如下:
- 使用 ffmpeg 提取視頻的音頻文件
- 將音頻文件上傳到云平臺的對象存儲
- 調(diào)用云平臺的語音識別 api 進(jìn)行文字識別
- 生成字幕文件
下載 release 版本測試了一下效果還可以,只需要修改個別識別有誤的詞就行,功能完全滿足我的需求;但是遺憾的是該項(xiàng)目只提供了 Windows 版本,而沒有 Mac 版本的 ,雖然作者也提供了一個 CLI 命令行版本可以在 Mac 上使用,但是對于普通用戶來說使用起來還是不是很方便,于是產(chǎn)生了開發(fā)一個 Mac 版。
思路
該開源項(xiàng)目作者是用 Go 語言寫的,我本人擅長的是 Flutter 開發(fā),所以首先想到的就是通過 Flutter 開發(fā)一個 Mac 版的桌面應(yīng)用,將 CLI 項(xiàng)目通過 Go 編譯成 Mac 的可執(zhí)行文件內(nèi)置到 Flutter 項(xiàng)目中,再通過 Dart 調(diào)用 shell 命令進(jìn)行執(zhí)行從而實(shí)現(xiàn)軟件的功能。
效果
實(shí)現(xiàn)
下面就來看看整個項(xiàng)目是如何一步步最終實(shí)現(xiàn)上面的效果的。
編譯 Mac 版可執(zhí)行文件
首先將 CLI 項(xiàng)目 clone 到本地,然后使用 go build
命令編譯對應(yīng)平臺的可執(zhí)行文件,如下:
# Mac M1/M2 Arm 架構(gòu) CPU CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -o video-srt-arm64 main.go # Mac Amd 架構(gòu) CPU CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o video-srt-amd64 main.go
執(zhí)行以上文件分別生成 arm
和 amd
架構(gòu)的可執(zhí)行文件 video-srt-arm64 和 video-srt-amd64。
內(nèi)置可執(zhí)行文件和 ffmpeg
將上一步生成的對應(yīng)平臺的可執(zhí)行文件修改為 video-srt
和配置文件 config.ini
以及 ffmpeg
文件放到一個文件夾中打包成 video-srt.zip
壓縮包減少包體積。
因?yàn)轫?xiàng)目需要使用到 ffmpeg ,所以需要把 ffmpeg 也內(nèi)置到項(xiàng)目中
通過 Xcode 將 video-srt.zip
文件添加到項(xiàng)目的 Resources 文件夾下
然后就是通過代碼在程序啟動時將內(nèi)置的壓縮包解壓到指定位置,這里解壓使用了 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(); // 創(chuàng)建工作目錄下的 video-srt 目錄 var videoSrtFile = Directory("$workDirPath/$VIDEO_SRT"); // 如果已經(jīng)存在則不重復(fù)解壓 if(await videoSrtFile.exists()){ return; } // 解壓 await unzip(VIDEO_SRT_ZIP_PATH, "$workDirPath"); return; } }
這里還用到了 path_provider
庫用于獲取相關(guān)目錄:
// 工作目錄名稱 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; } }
在應(yīng)用啟動時調(diào)用解壓將內(nèi)置的 video-srt.zip
內(nèi)容解壓到系統(tǒng) library 下的 videoSrt 目錄下。
設(shè)置配置信息
video-srt
的配置是用的 config.ini
文件存儲的,所以在代碼里需要讀寫 ini 文件,這里使用了一個 ini
的三方庫,config.ini
里包含如下配置內(nèi)容:
#字幕相關(guān)設(shè)置 [srt] #智能分段處理:true(開啟) false(關(guān)閉) intelligent_block=true #阿里云Oss對象服務(wù)配置 #文檔:https://help.aliyun.com/document_detail/31827.html?spm=a2c4g.11186623.6.582.4e7858a85Dr5pA [aliyunOss] # OSS 對外服務(wù)的訪問域名 endpoint= # 存儲空間(Bucket)名稱 bucketName= # 存儲空間(Bucket 域名)地址 bucketDomain= accessKeyId= accessKeySecret= #阿里云語音識別配置 #文檔: [aliyunClound] # 在管控臺中創(chuàng)建的項(xiàng)目Appkey,項(xiàng)目的唯一標(biāo)識 appKey= accessKeyId= accessKeySecret=
這里創(chuàng)建一個 ConfigModel
用于存放相關(guān)配置,然后使用 ini 庫的 Config 進(jìn)行讀寫封裝,代碼如下 :
// 讀取配置數(shù)據(jù) 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; } // 寫配置數(shù)據(jù) 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; }
執(zhí)行命令
配置也寫好了,接下來就需要執(zhí)行編譯好的 video-srt 命令來提取視頻字幕,這里使用 shell 命令來執(zhí)行,用到了 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 添加執(zhí)行權(quán)限 await shell.run("chmod +x ffmpeg"); // 給 video-srt 添加執(zhí)行權(quán)限 await shell.run("chmod +x video-srt"); } on ShellException catch (_) { // We might get a shell exception } // 監(jiān)聽執(zhí)行結(jié)果 controller.stream.listen((event) { callback(event); }); try { // 執(zhí)行視頻提取字幕命令 await shell.run("./video-srt $targetFilePath"); } on ShellException catch (_) { // We might get a shell exception } shell = shell.popd(); return; }
UI 實(shí)現(xiàn)
核心功能實(shí)現(xiàn)了,接下來就是完成界面的開發(fā),讓我們可以方便的進(jìn)行相關(guān)配置和選擇要生成字幕的視頻文件。
為了實(shí)現(xiàn) Mac 風(fēng)格的界面,這里使用了 macos_ui
庫,可以讓我們更快捷的實(shí)現(xiàn)相關(guān)界面。
界面分成兩部分,左邊菜單和右邊內(nèi)容展示區(qū)域,效果如下:
代碼如下:
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: [ // 狀態(tài)欄左上角退出按鈕 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('關(guān)于'),), ], ), ), child: IndexedStack( index: _pageIndex, children: const [ // 主頁 HomePage(), // 配置頁面 ConfigView(), HelpView(), AboutView() ], ), ), ); } }
然后分別實(shí)現(xiàn)對應(yīng)的子界面即可實(shí)現(xiàn)整個完整的功能,這部分就是純粹的 flutter 界面開發(fā)的內(nèi)容了,這里就不過多贅述了。
最后
雖然使用 Flutter 進(jìn)行開發(fā)已經(jīng)很久了,但是更多還是進(jìn)行 Android、iOS 的開發(fā),桌面端雖然也寫過一些Demo,但是還未真正使用 Flutter 去開發(fā)一個桌面應(yīng)用,雖然這個項(xiàng)目功能很簡單但也算是一個不錯的練手項(xiàng)目。
Github 地址:video-srt-mac
以上就是Flutter開發(fā)Mac桌面應(yīng)用實(shí)現(xiàn)自動提取生成視頻字幕文件的詳細(xì)內(nèi)容,更多關(guān)于Flutter Mac提取視頻字幕的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
android完美實(shí)現(xiàn) 拍照 選擇圖片 剪裁等代碼分享
本文給大家分享了2個安卓實(shí)現(xiàn)實(shí)現(xiàn) 拍照 選擇圖片 剪裁等的代碼,都是從正式項(xiàng)目中提取出來了,非常實(shí)用,有需要的小伙伴可以參考下。2016-01-01android ListView和GridView拖拽移位實(shí)現(xiàn)代碼
有些朋友對android中ListView和GridView拖拽移位功能的實(shí)現(xiàn)不是很了解,接下來將詳細(xì)介紹,需要了解的朋友可以參考下2012-12-12Android studio 連接手機(jī)調(diào)試操作步驟
這篇文章主要介紹了Android studio 連接手機(jī)調(diào)試操作步驟,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-05-05Android實(shí)現(xiàn)仿通訊錄側(cè)邊欄滑動SiderBar效果代碼
這篇文章主要介紹了Android實(shí)現(xiàn)仿通訊錄側(cè)邊欄滑動SiderBar效果代碼,實(shí)例分析了通訊錄側(cè)邊欄滑動效果的實(shí)現(xiàn)技巧,并附帶完整實(shí)例代碼供讀者下載參考,需要的朋友可以參考下2015-10-10詳解Flutter桌面應(yīng)用如何進(jìn)行多分辨率適配
這篇文章主要為大家介紹了Flutter桌面應(yīng)用如何進(jìn)行多分辨率適配的方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-02-02Android開發(fā)實(shí)現(xiàn)ImageView寬度頂邊顯示,高度保持比例的方法
這篇文章主要介紹了Android開發(fā)實(shí)現(xiàn)ImageView寬度頂邊顯示,高度保持比例的方法,結(jié)合實(shí)例形式分析了Android ImageView界面布局及元素屬性動態(tài)操作兩種功能實(shí)現(xiàn)技巧,需要的朋友可以參考下2018-02-02Android AnalogClock簡單使用方法實(shí)例
這篇文章主要介紹了Android AnalogClock簡單使用方法,結(jié)合實(shí)例形式簡單分析了AnalogClock的布局調(diào)用技巧,需要的朋友可以參考下2016-01-01