影音檔 Whisper 轉逐字稿之檔案批次處理工具
| | 3 | | 904 |
前陣子介紹了用 Whisper API 將 MP3 轉逐字稿,不需準備高檔顯卡,用一顆茶葉蛋的錢,一小時 MP3 轉逐字稿大約兩分鐘可完成,速度跟品質都頗令人滿意。
實測過幾次,發現免不了需要一些轉檔、併檔、拆檔,基本上靠萬能的 ffmpeg 都能解決,但每次遇到要查指令敲指令很沒效率,是時侯寫成共用腳本檔或程式庫了。這篇會用 PowerShell 示範,但關鍵在 ffmpeg 參數,大家可視需要改寫成自己慣用的語言。
整理我常用的檔案批次作業:
- 錄影或錄音檔有多個,想合併成一個
- 來源為 .mp4,想轉成 .mp3
- Whisper 有單檔 25MB 限制,以 20 分鐘為單位將 .mp3 拆成多個檔案
- 將長時間 .mp3 自動拆檔 Whisper 轉換再組裝成單一 .mp3 (檔案間加上分隔註記方便整理校正)
為此,我寫了三個函式:Az-MergeMp4s、Az-ConvertMp4ToMp3、Az-SplitMp3、Az-TranscibeLongMp3,程式邏輯不難,說穿了就是組裝好參數,召喚 ffmpeg 登場大顯神威罷了。只有幾個小地方值得一提:
- 合併檔案需將動態產生檔案清單文字檔,合併完成後刪除
- PowerShell 內建的 Resolve-Path 無法解析不存在路徑,要取得絕對路徑需額外處理
- ffmpeg 的 -hide_banner 參數可隱藏又臭又長的版本、版權聲明及支援格式資訊 -v warning 只顯示警告等級以上訊息 -stats 顯示進及流計資訊
- 拆分檔案時,用 -ss 指定開始秒數或 mm:ss 時間值、-t 指定長度(我習慣加 60 秒與下段重疊)
function Resolve-AbsPath($relPath) {
return $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($relPath)
}
function Az-MergeMp4s(
[Parameter(Mandatory=$true)][string[]]$mp4Paths,
[Parameter(Mandatory=$true)][string]$outPath) {
# 寫入檔案清單暫存檔
$tmpListPath = [IO.Path]::GetTempFileName() + '.txt'
$mp4Paths | ForEach-Object { "file '$(Resolve-Path $_)'" } | Set-Content -Path $tmpListPath
# 將路徑解析成絕對路徑 https://blog.darkthread.net/blog/ps-resolve-non-existing-path/
$outPath = Resolve-AbsPath $outPath
& ffmpeg -f concat -safe 0 -i $tmpListPath -c copy -hide_banner -stats -v warning $outPath
Remove-Item -Path $tmpListPath
Write-Host "$outPath created." -ForegroundColor Green
}
function Az-ConvertMp4ToMp3($mp4Path, $outDir = '.') {
$mp4Path = Resolve-Path $mp4Path
$outDir = Resolve-AbsPath $outDir
[IO.Directory]::CreateDirectory($outDir) | Out-Null
$mp3Path = [System.IO.Path]::ChangeExtension($mp4Path, '.mp3')
& ffmpeg -i $mp4Path -hide_banner -stats -v warning $mp3Path
Write-Host "$mp3Path created." -ForegroundColor Green
}
function Az-SplitMp3($mp3Path, $outDir = '.', $sliceLen = 1200, $overlap = 60)
{
$mp3Path = Resolve-Path $mp3Path
[int]$duration = & ffprobe -i $mp3Path -show_entries "format=duration" -v quiet -of "csv=p=0"
$count = [math]::Ceiling($duration / $sliceLen)
for ($i = 0; $i -lt $count; $i++) {
$start = $i * $sliceLen
$timeSpan = [TimeSpan]::FromSeconds($start)
$outFile = [System.IO.Path]::Combine($outDir, [System.IO.Path]::GetFileNameWithoutExtension($mp3Path) + "-$($i+1).mp3")
& ffmpeg -i $mp3Path -ss $start -t ($sliceLen + $overlap) -hide_banner -stats -v warning -c copy $outFile
Write-Host "Split $timeSpan to $outFile" -ForegroundColor Cyan
}
}
function Az-TranscibeLongMp3(
[Parameter(Mandatory=$true)]$mp3Path,
[Parameter(Mandatory=$true)]$prompt,
$outDir = '.\temp', $sliceLen = 1200,
$whisperDelpoy = $whisperDeployName,
$gptDeploy = $gptDeployName
)
{
$outDir = Resolve-AbsPath $outDir
[IO.Directory]::CreateDirectory($outDir) | Out-Null
$mp3Path = Resolve-Path $mp3Path
Az-SplitMp3 $mp3Path $outDir $sliceLen
$files = Get-ChildItem -Path $outDir -Filter '*.mp3'
$transcripts = @()
$toRemove = @()
foreach ($file in $files) {
$transcripts += "==== $($file.Name) ===="
$transcript = Az-WhisperTranscribe $file.FullName -language 'zh' -prompt $prompt -deployName $whisperDelpoy
$transcriptPath = [System.IO.Path]::ChangeExtension($file.FullName, '.txt')
$transcript | Out-File -Path $transcriptPath -Encoding utf8 # backup
$transcripts += $transcript
$toRemove += $file.FullName
$toRemove += $transcriptPath
}
$txtPath = [System.IO.Path]::ChangeExtension($mp3Path, '.txt')
$transcripts | Out-File -Path $txtPath -Encoding utf8
$toRemove | ForEach-Object { Remove-Item -Path $_ -Force }
Write-Host "Transcript saved. [$txtPath]" -ForegroundColor Green
}
假設我有一堂課程錄影,分為四個檔案:
分別執行併檔、MP4 轉 MP3、一小時 44 分的 MP3 以 20 分鐘為單位拆成六個檔:
比較檔案大小時,發現一處可疑,.mp4 長度為 1:41:59,但轉換成 .mp3 後長度為 1:44:36。但檢查 part2.mp3 的起始時間確實有對上 .mp4 的 20:00 整。故推測是 Windows 跟 ffmpeg MP3 解碼器的差異造成,不影響結果。
而自動長 .mp3 拆檔呼叫 Whisper 再組裝回單一 .txt 的簡易實測如下,轉 20 分鐘大約要 1'10":
有了以上工具,轉換長檔就簡單多了。
Comments
# by arick
按長度分割再轉文字. 如何處理句子不連貫問題?
# by Jeffrey
to arick, RAG 處理長文件也有相同問題,一般的做法是在切割處向前或向後延伸,讓兩個分割檔有部分重複。我在這的做法是多抓 60 秒,若起首的句子被切到只剩半截,有機會在前一段的結尾找到完整版。
# by Python路過吾好錯過
等待複核中,留言將在稍後顯示 / The comment is awaiting review.