展示影片
將功能轉換成一部簡短影片:在真實瀏覽器中進行實際互動、解釋畫面上內容的語音旁白,以及字幕。所有步驟都在本機執行:agent-browser 負責錄製,Kokoro 負責朗讀,ffmpeg 和 Pillow 負責組裝。
讓這一切運作的關鍵概念是:旁白即時鐘。 先渲染語音,測量每個場景的長度,然後將每個瀏覽器動作對齊到談論它的字詞。絕不讓計時去配合錄影的節奏;錄影的節奏會飄移,但 WAV 檔不會。錄影本身不會等待旁白:動作會一個接一個地執行,而組裝時會在旁白需要更多時間的地方凍結畫面。
工具
此技能的腳本存放在其基礎目錄下的 scripts/ 中(即載入時顯示的「此技能的基礎目錄」);範例情境則放在該目錄下的 references/ 中。每個 Shell 命令都是全新啟動的,因此在某個命令中匯出的變數在下一個命令中會消失:每個需要使用這些變數的命令都必須以 export SCRIPTS=<基礎目錄>/scripts DEMO=<展示目錄>; 開頭,如下方的預檢指令:
export SCRIPTS=<基礎目錄>/scripts; bash $SCRIPTS/preflight.sh
它會檢查 agent-browser 及其所需的 Chrome 瀏覽器、支援 libx264 的 ffmpeg/ffprobe,以及 Kokoro 的 Python 環境與模型:可以是預先安裝好的(設定了 DEMO_VIDEO_VENV 環境變數),或者是使用 uv(它會以 Python 3.12 執行 Kokoro 和 Pillow,無論系統 Python 版本為何)並能存取 Hugging Face。如果此指令以非零狀態碼結束,請停止:告訴使用者缺少什麼以及如何安裝,並且不要開始進行腳本、資料或錄製。未經詢問,請勿自行替換為其他 TTS 引擎、錄製工具或無聲影片——這會讓結果不符使用者的要求。其警告(缺少 espeak-ng、缺少系統字型)不會阻礙流程;請提一下這些警告。
$SCRIPTS/python 會在此技能 uv.lock 鎖定的環境中執行下列 Python 腳本;情境腳本也會讀取 $SCRIPTS 變數。將所有工作檔案存放在暫存區中的一個展示目錄(DEMO,絕對路徑)裡,該目錄位於儲存庫外部。如果你只能寫入儲存庫內部,或者影片將附加到該儲存庫的拉取請求中,則改用一個 Git 會忽略的資料夾:在 git check-ignore -q tmp/demo-video 成功時使用 tmp/demo-video/<名稱>,否則使用其他它確認被忽略的資料夾,以確保其中的內容不會被提交。未經詢問,請勿為此編輯 .gitignore。
工作流程
1. 理解功能並選擇故事
閱讀分支差異、計畫或 PR 描述,以及功能的使用者面向文案(i18n 檔案、工具提示、幫助文字)。工具提示通常最能定義欄位和選項——旁白應逐字與它們對數字含義的描述保持一致。
選擇一個主角(一個會真正使用此功能的虛構企業)以及該功能為他們解答的一個問題。一個從問題到解答的具體情境展示,勝過一個對所有控制項的巡禮。
僅在使用者做出會改變影片的選擇時才詢問:觀眾(客戶或團隊)、目標長度、語音,以及他們是否希望在錄製任何內容之前先閱讀腳本。預設值:客戶、約 3 分鐘、af_heart。跳過使用者已回答或已無關的問題——對於要求從頭到尾一次完成、或顯然無法回答問題的人,使用預設值並省略腳本審查。
2. 製作能講述故事的展示資料
空的或隨機的資料會讓功能看起來有問題。建立一個資料集,其數字能讓螢幕上的每個要點都一目瞭然:邊界情況在第一個畫面就可見,當選項切換時總和會發生明顯變化。
- 隔離資料。 建立一個獨立的資料庫,並在單獨的連接埠上執行應用程式;切勿重複使用使用者的開發資料。優先查閱專案自身的慣例(CLAUDE.md、關於 worktree 和開發伺服器的指南、種子目錄)。如果專案有種子框架,請在其中撰寫一個可重複使用的、按行業區分的展示種子(例如
db/seeds/demos/bike_shop.rb),而非一次性腳本——這樣下一次展示就沒有成本了。在提交前先詢問。 - 所有日期都相對於今天,這樣展示就不會過時。
- 歷史記錄需要時間旅行。 應用程式會拒絕在過去建立訂單或預訂;請在記錄真正應該被建立的時刻建立每筆記錄(例如 Rails 的
travel_to)。留意那些忽略時間旅行時鐘的狀態:在時間旅行之前載入的實例上快取的值,以及在建立時計算的狀態欄位。在時間旅行區塊內重新載入並刷新它們。 - 快照空的資料庫結構(例如使用
createdb -T),以便在修復後重新播種時只需刪除並複製,而無需執行遷移。 - 然後開啟頁面並檢查你計畫提及的每個數字。
3. 撰寫腳本
撰寫 DEMO/scenes.json:[{"id": "01_intro", "text": "..."}, ...]。每個場景一個想法,共 10–15 個場景。Id 按播放順序排序。
- 長度: Kokoro 的語速約為每分鐘 150 個單詞,每個場景會增加約 1–3 秒的動作和停頓。對於 3 分鐘的影片,撰寫約 450 個單詞;420 個單詞的影片長度約為 2 分 40 秒。
- 說出觀眾正在看的內容,按照他們看到的順序。使用螢幕上的標籤來命名控制項。
- 為聽覺而寫:使用簡短的句子,將數字拼寫出來(如果直接讀起來很奇怪,例如「百分之二十五」)。
- 在旁白中遵循與程式碼中相同的專案詞彙規則(詞彙表、保留字)。
- 在螢幕上顯示內容時,才說出它的名稱。 在觀眾能看到篩選條件值、選單選項或記錄之前,不要先說出它們(例如,在工作人員列表尚未開啟時就說「Sam 賣的所有東西」)。將開啟選單的詞語放在名稱之前,這樣情境腳本可以先開啟選單,並在說出名稱的同時將指標停在選項上。
- 以功能自身的問題開場。 第一句話必須是只有此功能才能回答的問題。「我們這個月賣了多少?」這個問題也可以用來描述相鄰的報表;「哪家商店賣出了最多的入門套件?」則不會。在確定之前,先檢查應用程式的同級功能(其他報表、其他設定頁面)。
- 為指定的觀眾而寫。 客戶展示展示他們能做什麼;給行銷或客戶管理部門的內部展示則需涵蓋它如何決定事情、如何開啟、其限制,以及當客戶詢問時該怎麼說。
如果使用者要求審查腳本,請在此處停止,並在渲染音訊之前向他們展示:先提供一個簡短的大綱(每個場景一行:顯示的內容及其提出的一個主張),再加上你不確定的任何主張,下面則提供全文給需要的人。之後更改一句話意味著重新渲染該場景、重新調整情境腳本的提示點並重新錄製該次錄影,因此這是改變故事成本最低的時刻。否則繼續進行;在最終報告中提到腳本位於 DEMO/scenes.json 中。
4. 渲染語音
export SCRIPTS=… DEMO=…; bash $SCRIPTS/python $SCRIPTS/narrate.py $DEMO
會寫入 tts/<id>.wav 和包含每個單詞時間戳記的 tts/<id>.words.json,並印出每個場景的長度。使用 --only <id> ... 重新渲染單一場景。語音選項:af_heart(最自然的美式)、am_michael、bf_emma(英式)。語音的第一個字母選擇語言。
5. 預演每個互動
在錄製之前,使用 agent-browser 執行每個步驟一次並擷取螢幕截圖。這可以在攝影機面前找出會出錯的地方:錯誤的路由、被覆蓋層遮擋的元素、關閉時會提交的表單、在徽章出現時會變動的版面配置。快照引用(@e12)在每次頁面載入時都會改變,因此請在情境腳本中使用穩定的 CSS 選擇器(id、dom_id、name=)。優先使用它們而非 demo.click_role:輔助函數會將指標滑動到選擇器的元素後再動作,但 agent-browser 無法為角色提供位置,因此點擊角色會使指標跳躍。
隱藏僅開發用的覆蓋層(分析器、除錯列),並透過在錄影開始前載入頁面兩次(demo.prime(url))來清除一次性閃現,例如「已登入為…」。
6. 撰寫情境腳本並錄製
情境腳本是 DEMO 中的一個簡短 Python 腳本,使用 SCRIPTS/recorder.py。從 references/example_scenario.py 開始。使用 s.at("單詞") 將動作對齊到字詞。標點符號是獨立的記號,因此提示點應使用 "October" 而非 "October,",當單詞出現兩次時使用 nth=。
僅透過輔助函數移動。 原始的 agent-browser 指標命令會瞬間移動指標,而滑鼠滾輪會跳躍頁面,這在鏡頭上讀起來像是一個剪輯。demo.move、demo.hover 和 demo.click 會將指標平滑地移動到那裡,並在指標到達後返回;demo.scroll 會以動畫方式滾動,並在頁面靜止後返回。因此,每個動作都會完整地呈現在錄影中,然後下一個定格才會凍結畫面。
讓觀眾能閱讀每個選單。 一個在 1 秒內開啟又關閉的列表什麼也看不到。使用輔助函數,它們會在開啟的列表上停留,並在選擇選項之前將指標停在該選項上:
demo.pick(open_trigger, option_selector)用於自訂 HTML 選單。- 傳入
scene=s, cue="<單詞>"給任一輔助函數,可以在旁白說出該名稱時,精確地將指標停在選項上;選單大約會在兩秒前開啟。這需要在同一場景中,提示單詞之前有兩秒鐘的旁白,且其間沒有慢動作——如果無法達成,組裝時會印出warning: … runs …s late,而解決方法在於腳本(例如使用「那個選單中的第三個選項將它拆分…」而非「或者拆分它…」),或者將慢動作移動到前一個場景。 demo.pick_native(select_selector, label)用於原生的<select>。在無頭錄製中,原生選擇項目的彈出視窗永遠不會繪製出來,因此此函數會從 select 的真實選項中繪製一個替代列表,指向該選擇,然後選取它。
每次錄影使用單一視埠。 視埠在錄影中途改變的錄影會變形。將手機片段錄製為自己的錄影(demo.viewport(390, 844));組裝時會將它放到一個畫布上。在其之後的一個簡短結尾錄影會回到桌面。
錄製每次錄影:export SCRIPTS=… DEMO=…; python3 -I $DEMO/scenario.py desktop(以及 phone、outro 等)。錄製是無計時的:s.at() 和 s.until() 不會停頓,而是記錄一個定格點,因此錄影只會像其動作所需時間一樣長(一個 2 分鐘的影片可在 1 分鐘內錄製完成),並且損壞的選擇器或旁白中缺少的提示單詞會在數秒內失敗。這使得單獨的預演變得沒有必要;直接迭代錄影即可。每次錄影會寫入 <take>.mp4 和 <take>.holds.json;組裝時會在每個定格點凍結畫面,直到旁白到達其提示單詞。如果在一次錄影中途步驟失敗,請修正情境腳本並重新執行整個錄影;定格點必須來自一次乾淨的執行。
每個動作都必須在下次提示之前讓頁面穩定下來,因為該最後一個畫面就是將被凍結的畫面:在重新載入或提交的表單之後保留 time.sleep,否則定格的畫面會顯示一個載入到一半的頁面。任何依賴自身時鐘執行的東西(例如淡出的提示訊息、旋轉器、倒數計時器)只會在情境腳本執行時推進,因此定格會在半途中凍結它。為了讓它不出現在攝影機前,請在下一次提示之前讓它穩定完成,就像 prime 對登入通知所做的那樣。為了顯示它,例如顯示一個「已儲存」的提示訊息出現並淡出,請在動作之後加入一個涵蓋整個效果的 time.sleep,使其在下一次定格之前播放完畢。對於一個在閒置時會持續動畫的頁面,請使用 DEMO_TIMED=1 進行計時錄製,它會在錄製時暫停直到每個提示單詞,並寫入 <take>.timeline.json 取代定格點。
7. 組裝
先關閉瀏覽器(agent-browser close):Chrome 和 ffmpeg 的編碼器一起使用可能會超出小型機器或容器的記憶體容量。
export SCRIPTS=… DEMO=…; bash $SCRIPTS/python $SCRIPTS/assemble.py $DEMO --takes desktop phone outro --out <名稱>
產生 DEMO/build/<名稱>.mp4,內容包含內嵌字幕、一個軟字幕軌道和旁白,以及 <名稱>.srt。
8. 交付前審查
你可以看到影片,但聽不到它。檢查你能檢查的部分:
export DEMO=…; ffmpeg -v error -y -i $DEMO/build/<名稱>.mp4 -vf "fps=1/6,scale=480:-1,tile=4x8" -frames:v 1 $DEMO/build/grid.png # 閱讀它
export DEMO=…; ffmpeg -i $DEMO/build/<名稱>.mp4 -map 0:a -af volumedetect -f null - 2>&1 | grep volume # 確認不是靜音
在網格圖中,確認每個場景都顯示了其旁白所描述的內容、字幕從未遮蓋正在指向的東西,並且沒有畫面捕捉到載入到一半的頁面或錯誤。在提示時間點(-ss <t>)擷取重要的單幀畫面。
然後停止開發伺服器並交付影片:在使用者自己的機器上,將其複製到他們能找到的位置並 open 它;在其他情況下,提供其路徑,或者如果你使用的工具支援,將其附加到拉取請求中。當你回報時,說明你無法驗證的內容(藉由聽覺檢查音訊)並列出任何採取的捷徑,例如替代的選單。
已經解決的陷阱
- 當頁面仍在穩定時,
agent-browser click會因「被覆蓋」而失敗。recorder.ab會重試;在重試後仍然失敗的問題是真實的,因此請修正選擇器。 - 在關閉時會提交的表單,例如在按下 Escape 時提交的核取清單,需要幾秒鐘重新載入。在下一次點擊之前等待它完成,否則那次點擊會落在一個移動過的按鈕上。
- Overmind 在存在過時的
.overmind.sock時會拒絕啟動。僅在確認沒有東西在該 socket 上回應後(overmind status)才移除該 socket。 - 過長的暫存區路徑會超出 Unix socket 長度限制;不要將 socket 指向那裡。
- Kokoro 會將長文字分割成多個區塊;
narrate.py會偏移每個區塊的字詞時間戳記,因此請根據字詞來設定提示點,切勿根據猜測的秒數。