用程式碼做配音影片
概覽
一條影片 = 定稿文案 → 逐句 TTS → 逐句 ASR 驗證 → 時間軸 → HTML 動畫 → 逐幀截圖 → ffmpeg 合成。
核心原則:時間軸來自音訊。 先把每一句配好、驗證好,測出每個字幕段的真實起止時間,再把所有畫面動畫掛在這些時間點上。畫面不要寫死秒數,配音改了重跑時間軸即可;渲染是對頁面逐幀 __seek(t) 截圖,結果確定、可重複。
適用:講解/教學(2–5 分鐘)、產品宣傳短片(20–40 秒)、帶字幕和章節條的知識影片。 不適用:真人出鏡剪輯、需要實拍素材的影片、純音樂 MV。
流程
每一步都有關口,前一步沒過不要往下走。
| # | 步驟 | 產出 | 關口 | 參考 |
|---|---|---|---|---|
| 0 | 初始化 | 工作目錄 | bash scripts/init.sh <dir> 成功 | 下文 |
| 1 | 選題、核實、文案 | script.md、audio/make_script.py | 事實已核實(要實驗的先跑);使用者明確同意文案,才能開始配音 | references/script.md |
| 2 | 配音 | audio/raw/NN.wav → audio/clips/NN.wav(1.3x) | 每句 ASR 過一遍,問題句重錄 | references/voice.md |
| 3 | 時間軸 | build/narration.wav、build/timeline.json | 字幕切分對上語音 | references/timeline.md |
| 4 | 素材 | 真實界面截圖、真實終端輸出 | 和口播一致,範例網域用 example.com | references/capture.md |
| 5 | 合成 | comp/index.html | 抽幀拼圖逐張看過 | references/composition.md |
| 6 | 渲染 | dist/video.mp4 | 時長 = 時間軸,響度 -14 LUFS | references/render.md |
| 7 | 封面、文案 | dist/cover-bili.png、dist/cover-douyin.png、dist/copy.txt | 封面在各平台裁切安全區內 | references/publish.md |
| 8 | 交付、清理 | 發給使用者;歸檔原始檔 | 使用者確認已儲存後才清理 | 下文 |
初始化
bash scripts/init.sh ~/work/<專案>/video-<期名> 把 template/ 複製成工作目錄、把 scripts/ 複製到 tools/、安裝好 playwright-core。範本是一條能直接跑通的 30 秒示範,開工前替換這些示範內容:
script.md、audio/make_script.py:本期文案;章節只在make_script.py的CHAPTERS裡定義一次,章節間停頓、章節條、章節時間戳都從它推導。audio/direction.txt:TTS 導演提示的【場景】。asr-prompt.txt:本期術語表(規範寫法,必要時附讀法)。comp/index.html的場景和動畫、comp/cover-*.html的封面內容。
常用命令
在工作目錄裡執行:
| 命令 | 作用 |
|---|---|
python3 audio/make_script.py | 生成 audio/script.json 和 audio/lines.json |
python3 tools/tts.py audio/lines.json audio/raw | 逐句配音;依文字複用已有音訊,改稿後直接重跑 |
bash tools/speed.sh audio/raw audio/clips 1.3 | 加速不變調 |
python3 tools/asr.py audio/clips --en | 逐句辨識:plain / 術語提示 / 英文模式 |
python3 tools/takes.py 3,7 3 | 第 3、7 句各錄 3 條並評分 |
python3 tools/takes.py audio/var.json 2 | 同一句的幾種改寫各錄 2 條 |
python3 tools/tts.py audio/lines.json audio/raw --adopt 3 audio/takes/03-0-1.raw.wav | 採用某條 take |
python3 tools/build_audio.py audio/clips build audio/script.json | 拼接配音,生成時間軸 |
python3 tools/align_chunks.py audio/clips build/timeline.json audio/script.json | 字幕段對齊到實際讀音 |
node tools/render.mjs --timeline=build/timeline.json --stills=2,8.5,20 | 抽幀到 build/stills(每次先清空) |
bash tools/contact.sh build/stills build/sheets | 2x2 拼圖,用 Read 看圖 |
bash tools/render-par.sh build/timeline.json build/narration.wav dist/video.mp4 8 | 8 程序並行渲染整片並混音 |
node tools/render.mjs --html=comp/cover-4x3.html --width=1440 --height=1080 --png=dist/cover-bili.png | 出封面 |
python3 tools/bili_kw.py "nginx 配置ssl证书" "宝塔 https" | 選題:B站關鍵字前 6 條播放量 |
依賴:ffmpeg、Node 18+、本機 Google Chrome、Python 3 + requests + openai-whisper(small 模型)。TTS 用小米 MiMo,Key 從環境變數 MIMO_API_KEY 或 MIMO_ENV_FILE 指向的檔案讀取,都沒有就問使用者要;換別家 TTS 只要保證每句一個 WAV。
最容易出錯的地方
- 沒審批就配音。 文案先發使用者;"繼續/開始/可以"算同意。為了讀音改了措辭,交付時逐條告訴使用者。
- 字幕和配音寫成一份。 字幕給人看(
Nginx、443、1.3),配音給 TTS 讀(Engine X、四四三、一點三)。make_script.py裡用P(字幕, 讀法)分開寫,各段讀法拼起來要等於整句 TTS 文字。 - 只聽整段混音。 整段辨識會把英文術語認錯,必須逐句辨識。選 take 只看 plain 分數和
--en結果;帶術語提示的 prime 會把讀錯的也"糾正"對,判定規則見 voice.md。 - 把秒數寫死在動畫裡。 第一句開始之後的動畫一律寫
T(句, 段);加速、重錄後時間都會變。 - 頁面開頭空著。 卡片出現後 2–3 秒還是空的,看起來像卡住了;內容在本章第一個字幕段就要出來。
- 同一元素多段
fill: both動畫、重複 id。 WAAPI 不回報錯誤,元素會整段消失或停在錯誤狀態,見 composition.md。 - 用
file://打開頁面渲染。 CSS mask 等會失效;render.mjs自帶本地 HTTP 服務。 - 封面被平台裁掉。 B站一張圖同時裁 4:3 和 16:9,抖音 9:16 會被裁成 3:4,內容只放安全區。
- 違規導流。 畫面、封面、文案裡不放指向真實站點的網址、QR碼、聯絡方式,不寫"可以看看/搜一下";
example.com這類保留範例網域可以出現在示範界面和終端裡。跨平台發布的影片不拿某個內容平台當範例;宣傳片不講價格。 - 先報成功再看結果。 發進度訊息要等命令跑完、看到輸出之後。
交付和清理
- 交付:成片、B站 4:3 封面、抖音 9:16 封面、
dist/copy.txt(標題、標籤、簡介、章節時間戳)。B站影片預設也會發到抖音等平台,所以豎版封面和抖音文案一起出;使用者明確只發一個平台時只出那一套。列出所有為讀音改過的措辭,以及沒把握的時間點請使用者聽一下。 - 檔案超過聊天工具上限(常見 30MB)時,先發
crf 22預覽版,並說明高畫質版位置。大檔案傳送逾時重發後,提醒使用者可能收到兩份。 - 使用者確認素材已儲存後:把可複用原始檔(
comp/及用到的素材、audio/make_script.py、變體 spec、script.md、copy.txt、direction.txt、asr-prompt.txt、截圖腳本、lab/)歸檔到使用者的範本庫(沒有約定就問使用者放哪,例如~/work/<專案>/video-template/<期名>/),diff -r逐個比對一致後,再刪工作目錄、暫存瀏覽器 profile、容器、開發伺服器(按 PID 殺,先確認命令列)和只為截圖裝的前端依賴。whisper 模型快取和 Docker 映像檔保留。