目錄

利用Whisper打造UE5台詞自動化工作流

動機與背景

之前在公司做上一個專案的時候,整個期程只有10個月,但無論劇本怎麼刪減,演出的部份還是有四、五十場之多(用到動補的場次約二十多場)。當專案進入後期之後,影音同仁的工作量相當大,想說看看能否幫他們減輕一些重複的手工作業,因此做了這個工具。

[註1] 由於這個工具跟公司內部的其他工具有較高的相依性,且大部份是在上班時間進行開發,因此沒有辦法將其開源。寫下這篇的目的主要當作自己的一些紀錄,分享工具的演算法及實作過程中的想法、嘗試等種種。或許也可以作為一些idea發想上的參考。

[註2] 文章底部有詞彙表,列出文章內的常用詞彙及簡單說明,先行看過對加速理解本文內容應該有所幫助,可以利用目錄(TOC)跳轉過去。

目標

為Unreal Engine的Sequence自動建立台詞的工具。

(由於我沒有參與動補方案細節的討論跟拍攝,所以不清楚廠商是否還有其他方案能提供更多的內容,總之我們選擇的方案只有全身動態捕捉,並沒有包含臉部跟其他。如果廠商方有、且費用及效果都符合專案要求的話,就不需要自己土砲一個了。)

現有作業流程

這邊只說明Sequence製作中跟最後目標有關,即台詞的部份。

影音同仁會在引擎內為每一場演出的Sequence,用公司內部自製的plugin(下述)為演出角色新增台詞Track,並且在時間軸上正確的時間點加上台詞Section,填入台詞字串編號。

工作量大致為:
含有台詞的全部演出場數
  └ 每場演出的角色各需要一個Track
          └ 角色的每句台詞各需要一個Section

手邊可用材料

  1. 拍攝每一個動補場次時的側拍影片。
    影片內容包含: 開始 -> 預留時間 -> 3, 2, 1, Action -> 實際演出 -> cut -> 預留時間 -> 結束

  2. 台詞字串表
    字串表內包含所有演出場次名稱(編號)、台詞及角色名稱。

  3. 同事製作的UE plugin
    能在Sequence上增加字幕專用的Track,Track上可用一句台詞為單位建立Section,Section可以填入台詞字串編號,台詞字串編號會引用到字串表內的台詞字串。當Track中的Section被執行到的時候,會呼叫方法把字串編號引用到的台詞字串顯示在UI上。

開發環境

  • Windows 10
  • UE 5.5.1
  • Python 3

核心流程

整體流程可以拆分成三個階段,而每個階段可再細切成幾項步驟:

階段一:資料預處理 — 從影片到原始逐字稿
  1. ffmpeg轉檔
  2. 音訊轉譯

階段二:正規化 — 時間軸校正與文字內容對映
  3. 時間軸校正
  4. 台詞mapping
  5. (Optional) 場次合併
  6. 場次資料整理合併

階段三:自動化生成 — 編寫並使用BP腳本生成台詞
  7. 編寫EditorUtilityWidget腳本
  8. 台詞生成

[註] 步驟1~4都是針對各個場次個別做處理。

接下來會針對各個階段的每個步驟做說明。


階段一:資料預處理

1. ffmpeg轉檔

用ffmpeg將所有動補場次的側拍影片mts轉成wav。

2. 音訊轉譯

WhisperDesktop的CLI工具將wav轉譯成中文,以srt格式輸出。

這邊使用的是ggml-large-v2模型。也嘗試過large-v3,但無法正確轉譯,不確定是否跟GPU規格有關;而其他版本的模型則是轉譯效果較差(以人工檢視確認各個場次的轉譯結果)。
(模型可以由此下載)。

由於台詞為中文,Whisper轉譯的時候指定輸出為中文(預設為英文),但輸出有時為繁體、有時為簡體,效果不穩定。不僅如此,wav檔中包含有Action與Cut等詞,某些時候會被Whisper強制翻譯成中文(並非每次,規則不明),影響後續時間軸校正的困難度。

此外,為了得到較好的轉譯效果,也曾嘗試過:

  • ffmpeg轉檔的同時做降噪處理。
  • 其他對wav檔去噪或人聲加強等工具。

都可以提昇音檔品質(就人耳而言)。

  • 或是其他Whisper-base的工具(如WhisperX)。

但對轉譯準確度的提昇沒有實際幫助。最後發現直接在Whisper轉譯時,帶入以下prompt作為參數,產出的結果不錯。

這是一段拍攝現場的錄音, 其內容與順序會包含:

  1. 倒數
  2. Action
  3. 一到多句的台詞
  4. Cut
  5. A-Pose

這個階段結束後,會產出所有場次的台詞的srt檔,包含時間及台詞內容。但轉譯出來會有兩大問題需要處理:

  • 時間 - srt的時間軸(包含預留時間)跟動補內容(只有Action到Cut的部份)時間長短不同。
  • 文字內容 - 轉譯出的文字(為動補演員唸出的台詞)跟劇本上的不完全一致(意思相同但用字不同或字數不同)、輸出為簡體中文、同音異字母跟斷句不同等問題。

階段二:正規化

3. 時間軸校正

這個階段要處理的是時間長短不同的問題,流程如下:

  1. 將srt內容整理為一個json陣列,一句台詞為一筆entity,其結構為: 開始時間、結束時間跟文字內容。
  2. 找出陣列中,文字內容為Action跟Cut,紀錄其陣列索引值及時間,之後用來作時間軸的校正。
  3. 如果沒有找到Action一詞,輸出一個前綴為場次名稱的dummy file作為標記,之後以手動處理;表示Whisper轉譯沒有正確識別出Action/Cut。
  4. 刪除json陣列中喊Cut以後的段落。
  5. 把每句台詞的開始與結束時間減掉Action的時間,往前平移。
  6. 刪除喊Action(含)以前的段落。
  7. 以場次為檔名,存成json格式。

結束後,可以得到校正時間後的轉譯文字。

4. 台詞mapping

因為存在斷句跟劇本上不同的問題,所以這個階段要處理的是將轉譯出的文字,以正確的斷句mapping回字串表的台詞。

Python中的difflib模組有SequenceMatcher工具可以用,雖然有支援中文字串的比較,但是無法處理同音異字的狀況,而用字不同的問題也會影響比較結果。後來突發奇想,將兩者先轉成羅馬拼音後再作比較。不但可以避免無法處理中文同音異字的狀況,受到用字/字數不同的影響也在可接受的範圍內。

  1. 從字串表中找到要處理的場次的區段,取出該區段的所有台詞存為一個結構陣列(字串編號/台詞內容/角色名稱)。
  2. 以該場次的台詞為主,跟校正後的轉譯文字作逐句比較,紀錄應該要為同一句台詞的開始/結束index。
  3. 以1.跟2.的結果整理出每句台詞的時間及角色
  4. 如果開始/結束時間相同,則加上0.5秒,避免在UE內的Sequencer上Section範圍過小點不到。
  5. 輸出成json格式。

第2點的文字比較細節可以參考下面的範例

(之前曾經用Ollama自己架LLM,所以也有試過拿來代替ChatGPT或Gemini這類AI服務,做台詞內容的兩者比較。但由於VRAM的限制,大概只能執行14B以下的模型,因此得到的效果奇差、執行效率也不好。)

舉例說明

借用Youtube上九日的專訪內容來作例子。

台詞字串表:

編號台詞角色
0728然後我們繼續還是在做覺得有趣的事情Vincent
0729也是謝謝,就是IGA有繼續在頒這些獎項,讓我們這些在遊戲圈打滾很多年的人,就偶爾有一些認可Vincent
0730哦我做東西有人覺得還不錯,可以得獎Vincent

校正後的轉譯文字資料:

  ...,
  { "line": 259, "start": "00:09:45,000", "end": "00:09:49,933", "text": "還是在做覺得有趣的事情" },
  { "line": 260, "start": "00:09:50,000", "end": "00:09:51,066", "text": "也是謝謝" },
  { "line": 261, "start": "00:09:51,166", "end": "00:09:55,366", "text": "就是IGA有繼續在頒這些獎項" },
  { "line": 262, "start": "00:09:55,466", "end": "00:09:57,800", "text": "讓我們這些在遊戲圈打滾很多年的人" },
  { "line": 263, "start": "00:09:57,800", "end": "00:10:01,333", "text": "就偶爾有一些認可" },
  { "line": 264, "start": "00:10:01,333", "end": "00:10:05,333", "text": "哦我做東西有人覺得還不錯" },
  ...

當要為編號0729的台詞做mapping時候,設定相似度初始值為0,並且將台詞內容轉成拼音字串S1。
首先將line 259的text轉成拼音字串S2,用SequenceMatcher比較S1與S2的相似度。
相似度為0或下降,判斷為不同句,改以line 260重新開始做台詞內容比較。
將line 260的text轉成拼音字串S2後,比較S1與S2的相似度。
相似度上升,判斷可能為同句,繼續往下處理。
再將line 261的text轉成拼音字串後,與S2串連成S3,比較S1與S3的相似度。
相似度上升,同樣判斷可能為同句。
接著重複字串串連、比較相似度的過程,直到相似度下降為止。
所以會得到line 260~263串連的text相似度為最高;而再加上line 264的話,則會開始下降。
即可將台詞0729與line 260~263視為一組mapping。
而該句台詞的開始/結束時間即是line 260的start與line 263的end。

這階段結束後,輸出的會是單一場次所有台詞的台詞編號、內容、開始/結束時間跟說話角色。

5. (Optional) 場次合併

其中有少數場次的演出被拆成兩場動補內容,所以在這個階段把資料依照順序合併、並且校正時間軸。
因為這種案例的數量很少,所以是用手動執行合併腳本。

6. 場次資料整理合併

將上述所有場次個別處理完之後所得到的結果,重新整理成一個單一json檔,結構大致為:
(結構並非一定如此,依照專案需求調整)

[
  {
    "場次名稱": "場次編號",
    "該場次所有角色": [
      {
        "角色": "角色編號",
        "台詞": [
          {
            "台詞編號": 編號,
            "開始時間": 秒數,
            "結束時間": 秒數,
          },
          其他台詞...
        ]
      },
      其他角色...
    ]
  },
  其他場次...
]

便可以將其匯入引擎成DataTable供後續使用。


階段三:自動化生成

7. 編寫EditorUtilityWidget腳本

這邊參考岡田和也先生提供的教學,利用EditorUtilityWidget編寫BP腳本的邏輯。
讀取匯入引擎後的DataTable資料,對每個場次的Sequence建立演出角色的Track,並把台詞作為Section,依照開始、結束時間建立在Track上,並且填入台詞字串編號。

8. 台詞生成

EditorUtilityWidget腳本完成後,如此便可以在引擎內,直接透過EUW的UI執行內寫好的腳本,把所有使用動補演出的場次,一鍵打上全部的台詞了。
這個步驟我是交由負責影音的同仁來動手,因為Sequence相關料件原本就是由他們負責,而且這樣可以讓他們直接感受到這個工具是否有實際上的幫助。


後記 - 可優化方向

因為Whisper轉譯出每句話的結束時間會是下一句的開始時間,在不做任何調整的情況下,直接用轉譯輸出的時間來建立Sequence的台詞時間軸,會變成每句台詞Section的頭尾相連,難以選到正確的Section加以拖動。

有試著簡單找過一些VAD(Voice Activity Detection)工具看看是否能夠有效的把時間軸上台詞之間的空白(靜音)段落給去除,但並沒有取得很好的結果。

另外就是,沒有辦法保證從側拍影片擷取出的台詞時間資訊能夠完美對上Sequence在實際編排動作之後的時間軸,所以即便使用了這個工具,後續還是會因為動作、語音等等而需要透過手工調整台詞Section位置及長短的狀況,但至少減少了一定程度的重複工作量。

由於還是以專案進度的推進為主,因此並沒有繼續往下深究其相關技術跟工具的細節。當初預計幫影音同仁減少工作量的目標也算達成,所以就在這個階段止步,如果未來真的有需要,則再行繼續吧。

補充: 因為畢竟是遊戲演出,所以碰到兩個以上角色同時間說話的狀況相當少(至少在這個專案跟上一個專案都是),否則由Whisper轉譯出來的內容應該會慘不忍賭。


詞彙表

詞彙說明
台詞劇本中,每位角色的對白。
Track即UE Sequencer中的Track。
Section即UE Sequencer中的Section。
字串表一個包含遊戲內所有文字內容的表格。我們是用Excel來整理、編輯內容,其匯入引擎後即為String Table。
字串編號每個字串內容在字串表上的編號。
演出遊戲中用於講述劇情的表演,此時玩家無法進行操作。
側拍影片由定點錄影機拍攝動補全景,且動作演員身上裝有麥克風。
場次獨立的一場演出;每一場演出都有各自可識別的編號。

參考資料