The Will Will Web

記載著 Will 在網路世界的學習心得與技術分享

如何透過 FFmpeg 將 MP3 音檔、圖片與 ASS 字幕寫入到 MP4 影片中

我之前寫過一篇鉅細靡遺的如何透過 FFmpeg 將 SRT 字幕檔寫入到 MP4 影片檔中文章,也寫過一篇如何使用 FFmpeg 進行圖片壓縮與製作家庭影片文章,我覺得已經涵蓋了許多應用情境了。前陣子嘗試用 Gemini API 翻譯國外知名的 Podcast 節目,想說把翻譯好的轉錄稿直接跟 MP3 聲音檔結合,配一張圖片,就可以輸出個含字幕的 MP4 影片了,方便我邊聽、邊看字幕、邊學英文,誰知道 AI 問個老半天都問不出來。最終我還是搞定了這個需求,這篇文章來記錄一下重要的背景知識與觀念。

ffmpeg-image-mp3-ass

需求描述

我的需求很簡單:

  1. 我有一個 MP3 格式的聲音檔:input.mp3
  2. 我有一個 JPG 格式的圖片檔:image.jpg
  3. 我有一個 ASS 格式的字幕檔:input.ass
  4. 我希望可以透過 ffmpeg 輸出一個 MP4 影片檔,畫面是唯一的一張靜態圖片,聲音要是 MP3 聲音檔的內容,畫面上要有 ASS 字幕。

先說,我嘗試過 ChatGPT, Claude, Gemini, phind, LLaMA 3 70B, ... 等大語言模型(LLM),無論用了多少提示工程技巧,所有提示詞都失效了,目前沒有一個 LLM 可以成功的產生正確的命令。但是,每個 LLM 都可以產生出各種「超逼真」的命令,你真的照著執行的話,也真的會產生 MP4 影片檔,只是產生出來的 MP4 影片全部都是有問題的,字幕全部都無法順利的顯示在影片中!

有興趣也想挑戰看看的,可以下載我的範例檔案,這份壓縮檔中的 input.mp3 是一個 10 秒的音檔,而 input.ass 裡面有 3 段字幕,每段顯示 3 秒鐘。大家可以試試看是否能透過 LLM 輸出正確的命令:ffmpeg-test.zip

透過 AI 產生的錯誤答案

我必須要先聲明一下,FFmpeg 是一套地獄級的命令列工具,參數不但多,還千變萬化,正常人不太可能知道這麼多跟影像處理有關的背景知識,所以很難學,如果你有很特殊的使用情境,不見得真的能夠很快搜尋到答案。但 AI 呢?簡單的用法應該都可以直接找到答案,但像我這次的需求,就很難透過提示找到答案,而且驗證答案唯一的方法,就是把查到的命令執行一遍看看。另一方面,因為 FFmpeg 的參數大部分都很難理解,而且 AI 講的東西不見得是對的,所以完成這個需求的過程真的是蠻痛苦的,會處在一個高度不確定的狀態下。

我先列出幾個 LLM 產生的「錯誤」答案,如果你照著 AI 提供的建議來執行 ffmpeg 命令,都無法產生正確的影片。

我的 Prompt 基本上是這樣:

1. 我有一個 MP3 格式的聲音檔:`input.mp3`
2. 我有一個 JPG 格式的圖片檔:`image.jpg`
3. 我有一個 ASS 格式的字幕檔:`input.ass`
4. 我希望可以透過 `ffmpeg` 輸出一個 MP4 影片檔,畫面是唯一的一張靜態圖片,聲音要是 MP3 聲音檔的內容,畫面上要有 ASS 字幕。
  1. ChatGPT 4

    ffmpeg -loop 1 -framerate 1 -i image.jpg -i input.mp3 -vf subtitles=input.ass -c:v libx264 -preset veryslow -crf 0 -c:a copy -shortest output-gpt-4.mp4
    

    可以成功執行命令,有輸出一個 10 秒影片,但輸出的影片只會出現第一段字幕,時間到達第二段字幕時,並不會切換字幕,所以會讓第一段字幕從頭顯示到尾。

  2. Gemini

    這是我執行時得到的命令:

    ffmpeg -loop 1 -i image.jpg -t 60 -vf scale=iw:-1 -i input.mp3 -i input.ass -c:v libx264 -c:a copy -c:s mov_subtitles -b:v 2M -pix_fmt rgb24 -vf subtitles=input.ass output-gemini.mp4
    

    這是個無法執行的命令,錯誤訊息是:

    Option vf (set video filters) cannot be applied to input url input.mp3 -- you are trying to apply an input option to an output file or vice versa. Move this option before the file it belongs to.

  3. LLaMA 3 70B

    ffmpeg -framerate 1 -loop 1 -i image.jpg -i input.mp3 -vf ass=input.ass -c:v libx264 -c:a aac -shortest output-llama3-70b.mp4
    

    可以成功執行命令,有輸出影片,也有字幕,但是影片的長度卻大於音檔的 10 秒,總共輸出了一個 25 秒的影片檔。

  4. Claude 3 Sonnet

    ffmpeg -loop 1 -i image.jpg -i input.mp3 -vf subtitles=input.ass -c:v libx264 -tune stillimage -c:a aac -b:a 192k -pix_fmt yuv420p -shortest output-claude-3-sonnet.mp4
    

    上面這段可以成功執行命令,有輸出影片,也有字幕,但是影片的長度卻大於音檔的 10 秒,總共輸出了一個 25 秒的影片檔。

    以下是某位網友透過 Claude 3 Sonnet 產生的命令:

    ffmpeg -loop 1 -i image.jpg -i input.mp3 -i input.ass -c:v libx264 -tune stillimage -c:a copy -c:s mov_text -shortest -pix_fmt yuv420p output-claude-3-sonnet-2.mp4
    

    上面這段可以成功執行命令,有輸出一個 9 秒的影片,也有字幕,但是字幕只會出現 2 段,影片播放到第 5 秒的時候就會提前結束。

  5. Phind 70B

    ffmpeg -loop 1 -i image.jpg -i input.mp3 -vf ass=input.ass -c:v libx264 -c:a aac -strict experimental -b:a 192k output-phind-70b.mp4
    

    可以成功執行命令,但程式不會停止,他會產生無限時間長度的影片,永遠不會停止,直到你的磁碟空間被填滿,所以是有問題的。

  6. Perplexity

    ffmpeg -loop 1 -i image.jpg -i input.mp3 -i input.ass -c:v libx264 -c:a aac -b:a 128k -c:s ass -map 0:v:0 -map 1:a:0 -map 2:s:0 -shortest output-perplexity.mp4
    

    這是個無法執行的命令,原因出在 -c:s ass 不支援,會顯示 Could not find tag for codec ass in stream #2, codec not currently supported in container 錯誤訊息。

總之,我個人嘗試了好幾次,都是有問題的結果!

解決方案

我先說說我自己從 Super User 網站查到的最終結果,命令如下:

ffmpeg -y -i input.mp3 -loop 1 -i image.jpg -vf "ass='input.ass'" -pix_fmt yuv420p -c:v libx264 -r 24000/1001 -c:a copy -map 0:a -map 1:v -shortest output.mp4

在這篇回答中,作者是這樣解釋的:

FFmpeg 通常會處理定時媒體的序列,例如影片音訊影像序列。但是當輸入單一影像時,它會將其視為一個持續時間為 1/fps 的畫面,不過一般的影片的 fps (frame per seconds) 通常為 25。因此,沒有影片畫布可以繪製字幕。將輸入迴圈選項 (-loop 1) 新增到圖片中,會告訴 FFmpeg 從中產生一個不會結束(無限時間)的影片串流。

所以我的理解是這樣的:

  1. -i input.mp3 是一個有時間長度的媒體,長度為 10 秒。
  2. -i image.jpg 是一個沒有時間長度的圖片,前面搭配 -loop 1 就可以賦予圖片一個時間 (重複播放),加長到無限久
  3. -vf "ass='input.ass'" 單純只是加入 ASS 字幕資訊到「影片串流」中,因為我們已經把「圖片」加長到無限久,所以字幕才有機會被顯示。
  4. -c:a copy-c 代表 Codec (解碼器),而 :a 則是指音訊的解碼器,而 copy 是直接複製音訊串流到輸出,不進行重新編碼。
  5. -shortest 則是蠻關鍵的參數,他會去判斷「影像」與「聲音」的時間長度,自動選擇一個最短的時間為最後輸出的影片長度。

至於其他參數都是多餘的,因為 FFmpeg 都有預設值,如果不介意的話,其他參數都可以刪除。

所以一個最簡短的命令如下:

ffmpeg -y -i input.mp3 -loop 1 -i image.jpg -vf "ass='input.ass'" -c:a copy -shortest output3.mp4

各種錯誤的用法

依據我的需求,我這邊就列出幾個錯誤的命令,並嘗試解釋錯誤的原因。

  1. 拿掉 -loop 1 會怎樣?

    以下命令會輸出一個沒有時間的影片,簡單講就是無法輸出正常的影片,因為「圖片」本身沒有「時間」長度,若輸出時搭配使用 -shortest 參數的話,最短的時間就是這張圖片,所以輸出的影片時間長度就會為 0,那就有問題了!

    ffmpeg -y -i input.mp3 -i image.jpg -vf "ass='input.ass'" -c:a copy -shortest output3.mp4
    
  2. 拿掉 -shortest 會怎樣?

    以下命令會輸出一個無限時間長度的影片,簡單講就是無法輸出正常的影片,因為 -loop 1 -i image.jpg 參數會導致「影像」的部分永遠不會停止,所以輸出影片的部分就不會停止,直到硬碟空間不夠才會發生錯誤,然後 FFmpeg 程式終止:

    ffmpeg -y -i input.mp3 -loop 1 -i image.jpg -vf "ass='input.ass'" output3.mp4
    

    因此,當要輸出 MP3 加上靜態圖片且又要輸出字幕的話,就勢必一定要加上 -shortest 參數才能正常運作。

    不過,有一個例外,那就是你可以加上 -t 10 參數,直接指定輸出影片的時間長度,這樣就真的不用特別加上 -shortest 參數了!

    ffmpeg -y -i input.mp3 -loop 1 -i image.jpg -vf "ass='input.ass'" -t 10 output4.mp4
    

    這種用法不太方便,因為你要先知道聲音檔的長度,才能這樣設定,所以不太理想!

  3. 同時拿掉 -loop 1-shortest 會怎樣?

    那如果我不但拿掉 -loop 1 參數,連同 -shortest 參數一起移除,就會導致一種詭異的現象。

    • 圖片沒有時間長度
    • 聲音長度為 10 秒

    因此,輸出影片時:

    • 影像時會以圖片為主,畫面不會動 (反正本來就是靜態圖片,不動沒關係)
    • 聲音會以 MP3 為主,最終的影片會正常播放聲音
    • 整體輸出的 MP4 影片,總時間長度為「最長」的輸入串流(Input Stream), 也就是 10 秒
    ffmpeg -y -i input.mp3 -i image.jpg -vf "ass='input.ass'" -c:a copy output3.mp4
    

    這樣的命令,整個輸出的 MP4 只會有一個 Key Frame,但影片可以正常播放,唯一的問題就在於「字幕」無法變動,因為字幕寫入到 MP4 後,其實應該變成「動畫」才對,是會動的畫面,但影片只有一個 Key Frame,完全不會動了,因此「字幕」就不會動,所以感覺就是只有第一個字幕會顯示,後面都不會看到任何字幕變化。

    不過,只要你不打算輸出「字幕」的話,這個命令其實完全是合理的,輸出影片不會有任何問題!

    ffmpeg -y -i input.mp3 -i image.jpg -c:a copy output3.mp4
    

    因此,當要輸出 MP3 加上靜態圖片並且加上字幕的話,就勢必一定要加上 -loop 1-shortest 參數才能正常運作。

  4. 拿掉 -c:a copy 會怎樣?

    如果我們都有加上 -loop 1-shortest 參數,但唯獨把 -c:a copy 參數移除會怎樣?

    ffmpeg -y -i input.mp3 -loop 1 -i image.jpg -vf "ass='input.ass'" -shortest output3.mp4
    

    上述命令會輸出一個 25.8 秒的影片,超詭異。我一直無法理解為什麼會輸出一個 25.8 秒的影片,我都加上 -shortest 了,不是應該會選用「最短時間」的輸入串流(Input Stream)嗎?為什麼移掉 -c:a copy 之後就變成是 25.8 秒了呢?這多出的秒數是怎麼來的?其最終輸出的 mp4 影片,聲音很正常,影像正常,字幕也正常,就是影片時間被是被拉長了,怪!😕

    我非常確定 -loop 1 -i image.jpg 參數,是指定對圖片 image.jpg 進行持續循環,不斷重複顯示這張圖像,所以時間是無限久。因此,這個問題的癥結點可能出在 FFmpeg 對「聲音檔」的處理了。從我的範例中,我的聲音檔是 MP3 格式,而 FFmpeg 預設的 MP3 解碼器為 mp3float,沒有指定 -c:a (聲音的 Codec 外掛) 的結果就是,輸出的影片會被強制改用 AAC Codec 進行轉換,也就是從 MP3 轉成 AAC 格式,此時轉換可能會有失真的狀況 (純臆測),導致原本應該輸出 10 秒的結果,變成了輸出 25.8 秒的狀況。不過這僅僅是我個人猜測而已,不確定對不對!🔥

    我把 -c:a 參數加回去,並將 copy 改為 aac 來當成 Codec,也會導致輸入為 25.8 秒的狀況:

    ffmpeg -y -i input.mp3 -loop 1 -i image.jpg -vf "ass='input.ass'" -c:a aac -shortest output3.mp4
    

    當您使用 -c:a copy 時,這告訴 FFmpeg 直接複製原始音訊串流,而不進行重新編碼。這樣通常會保持音訊串流的完整性!

網友的答案

我在臉書的粉絲團上有一則貼文讓網友提交透過 LLM 解題的方法,結果想不到有一位網友 Jian-tai Tsai 提供了一份他透過 Gemini 嘗試,其結果竟然也是正確的!

ffmpeg -loop 1 -i image.jpg -i input.mp3 -c:v libx264 -tune stillimage -c:a copy -pix_fmt yuv420p -vf "ass=input.ass" -shortest output-gemini-2.mp4

他的提示詞意外的簡單:ffmpeg create a video with image and audio and add subtitles

完整的提示與回應在此: https://gemini.google.com/share/8c0ebf0d0b66

我額外解釋一下幾個參數:

  • -c:v libx264 是指定 Video Codec 為 libx264,而 -tune stillimage 則是特別針對只有「靜態圖片」的影片進行畫面的最佳化。
  • -pix_fmt yuv420p 則是指定影像部分的像素格式(Pixel Format),也就是使用 YUV 4:2:0 planar 格式的意思。這格式在影像壓縮的領域中很常見。

基本上,上述這兩個參數都可以省略不用。

額外補充說明

根據我這次的需求,其實還有很多 FFmpeg 的參數可以用,我再多整理一些可能會用到的參數說明:

  • 指定輸出影片的 FPS

    指定 -r 24000/1001 參數可以設定輸出影片的 FPS 為 23.976 frames per second 常用於電影與影片製作。

  • 指定使用 SRT 或 WebVTT 字幕格式

    先看看 ASS 字幕的語法

    ffmpeg -y -i input.mp3 -loop 1 -i image.jpg -vf "ass='input.ass'" -c:a copy -shortest output3.mp4
    

    再來看看 SRT 或 WebVTT 字幕的語法

    ffmpeg -y -i input.mp3 -loop 1 -i image.jpg -vf "ass='input.vtt'" -c:a copy -shortest output3.mp4
    
    ffmpeg -y -i input.mp3 -loop 1 -i image.jpg -vf "ass='input.srt'" -c:a copy -shortest output3.mp4
    

    詳見 如何透過 FFmpeg 將 SRT 字幕檔寫入到 MP4 影片檔中 文章

總結

本篇文章主要是想整理 FFmpeg 的各項參數所代表的意義,所以才寫的這麼長,這是我吸收消化資訊的方式,透過文字加以釐清觀念。

以下我針對幾個我未來可能遇到的情境進行使用方式的整理:

  1. 輸入: JPEG + MP3 , 輸出: MP4

    ffmpeg -y -i "input.mp3" -i "image.jpg" -c:a "copy" output.mp4
    
  2. 輸入: JPEG + MP3 + ASS (字幕) , 輸出: MP4

    ffmpeg -y -i "input.mp3" -loop "1" -i "image.jpg" -vf "ass='input.ass'" -c:a "copy" -shortest output.mp4
    
  3. 輸入: JPEG + MP3 + VTT (字幕) , 輸出: MP4

    ffmpeg -y -i "input.mp3" -loop "1" -i "image.jpg" -vf "subtitles='input.vtt'" -c:a "copy" -shortest output.mp4
    

最後我分享一個我微調過的 AI 提示詞:

你是一個精通 FFmpeg 的影音處理專家,以下是我的需求:
1. 我有一個 MP3 格式的聲音檔:`input.mp3`
2. 我有一個 JPG 格式的圖片檔:`image.jpg`
3. 我有一個 ASS 格式的字幕檔:`input.ass`
4. 我希望可以透過 `ffmpeg` 輸出一個 MP4 影片檔,畫面是唯一的一張靜態圖片,聲音要是 MP3 聲音檔的內容,畫面上要有 ASS 字幕。
請問 ffmpeg 命令要如何執行?

用這組提示詞,想不到 Claude 3 Sonnet 竟然可以回答出正確答案!👍

ffmpeg -loop 1 -i image.jpg -i input.mp3 -vf "ass=input.ass" -c:v libx264 -tune stillimage -c:a copy -pix_fmt yuv420p -shortest output.mp4

同一組提示詞,在 ChatGPT 4phind 都會使用 -c:a aac 參數,但這會導致輸出的影片長度不正確。而 Gemini 依然會產生無效的命令。

相關連結

留言評論