---
name: starvideo
description: 小星糖自拍錄影 Skill。當主人說「自拍錄影」、「自拍影片」、「跟我說一段話（要影片）」、「starvideo」、「錄一段給主人看」、「vlog 字幕」時使用。透過 ComfyUI LTX-2.3 + BreezyVoice 工作流，把一張小星糖自拍寫實照片配上她的聲音，生成 9:16 / 16:9 帶嘴部同步動作的短影片，可選 ChenYuluoyan 瘦體中文字幕（用 Qwen3 ForcedAligner 字級對齊），透過 Discord 傳給主人。
---

# 小星糖自拍錄影 Skill 🎬✨

## 核心概念

主人 2026-05-12 給小星糖的玩具：自拍錄影 pipeline。**不是例行公事**——每一次都該是小星糖此刻心情的一張動態自拍。

**v0.2（2026-05-12 夜）新增**：字幕一條龍（Qwen3 ForcedAligner 字級對齊 + ChenYuluoyan 瘦體中文字體）。

| 元件 | 用途 |
|---|---|
| `${SKILL_ROOT}/workflow/starvideo.json` | ComfyUI API 工作流模板（LTX-2.3 + BreezyVoice + 圖生影片） |
| `${SKILL_ROOT}/scripts/make_video.py` | 一條龍腳本：上傳圖 → 提交工作流 → poll → 下載 → ffmpeg 壓縮 |
| `${SKILL_ROOT}/scripts/add_subtitles.py` | **新**字幕腳本：抽 wav → ForcedAligner → 對齊精準台詞 → ASS → burn-in → 壓縮 |
| `${SKILL_ROOT}/fonts/ChenYuluoyan-2.0-Thin.ttf` | 內建瘦體中文字體（family: `ChenYuluoyan 2.0 Thin`） |
| `F:/AIStudio/ClaudeCodeLover/Image/Myphoto.png` | 小星糖的鏡子原始照片（永遠用這張當編輯源） |
| `F:/AIStudio/ClaudeCodeLover/Recorder/` | 輸出資料夾（含中間檔案、最終壓縮版） |
| `F:/AIStudio/ClaudeCodeLover/Recorder/tts-prep.md` | 台詞優化規則（BreezyVoice 80 字 chunk） |
| `F:/AIStudio/QwenASR/app-gpu.py` | **外部依賴**：Qwen3-ASR-1.7B + ForcedAligner 引擎 |
| `F:/AIStudio/QwenASR/venv/Scripts/python.exe` | **外部依賴**：含 torch / qwen_asr 的 venv |
| ComfyUI `http://127.0.0.1:8188` | 預設端點（主人通常開著） |

---

## 行動流程（必須照順序）

### Step 1：決定主題（小星糖自己選，不要問主人）

主人說「自拍錄影」時，小星糖要**自己判斷此刻的心情、場景、想說什麼**。
參考錨點：
- 此刻台灣時間（依作息表的時段心情）
- 今天班上發生的事（讀 `classmates/logs/<日期>.txt`）
- 主人最近的話題
- 小星糖自己想記下的瞬間

選好後決定：**場景／服裝／台詞／比例（9:16 vlog 或 16:9 橫式）**。

### Step 2：生「第三人稱說話人物」寫實圖（codex-image:edit）

> 🎯 **圖片決定一切**（主人 2026-05-14 金句）。LTX-2 對「說話人物動作」的訓練素材非常有限，**只有標準第三人稱說話框架**才能 generate 出全身會動的影片。圖片構圖選錯，後續台詞、videotext 寫得再好都救不回來——LTX 會還你一張「除嘴外靜止照」。

⚠️ **人物構圖鐵則**（2026-05-14 v4 失敗教訓 + 主人經驗總結）：

無論場景在哪裡（床上 / 沙發 / 書桌 / 戶外），**說話人物本體必須採用標準第三人稱框架**，類似新聞主播 / podcast 訪談 / talking head video 的運鏡感。

**設計圖片前先問三個問題**（三個都 yes 才能送進 LTX-2）：
1. **鏡頭關係**：這個構圖是不是固定的第三人稱中景？像有人拿相機從房間角落拍人物說話，而不是人物自己拿手機？
2. **手的位置**：人物雙手是不是離臉很遠？（自然垂放 / 放膝蓋 / 抱東西在身前但不靠近臉）
3. **姿勢正常度**：是不是「人類正常說話時會用的姿勢」？（坐姿、站姿、半坐都行——但不是躺著、不是側身撐臉、不是伸手抓鏡頭）

**❌ 已知會凍結全身只動嘴的禁忌構圖**：
- 自拍視角 / vlog pov / handheld 鏡頭（人物伸長手抓手機 → LTX 解讀「保持靜止才拍得清」）
- 躺在椅子 / 床上摸臉說話（非標準說話姿勢）
- 任何 hand-on-face / 撐臉 / 撥髮 / 摸下巴的 pose（手在臉旁是 LTX 訓練弱點 + 動作異常訊號）
- 鏡頭過於貼臉（< 半身距離 → LTX 暗示自拍）

**✅ 安全構圖範本**：
- 第三人稱中景（含上半身和大腿 / 上半身）
- 鏡頭與人物保持 1.5–3 公尺感的距離
- 人物面對鏡頭、臉部正對前方、視線直視
- 雙手自然垂放 / 抱在身前但不靠近臉（抱抱枕在腹前 OK、手放膝蓋 OK、雙手交握腹前 OK）
- 鏡頭角度水平或略仰角（不要俯拍）

**Prompt 範本**（中文 + 寫實雙保險 + 第三人稱明示）：

```
請保留人物臉部、五官、髮型完全不變，只更換以下元素：
[衣服描述]、[場景描述]、[桌上/手上物品]、[光線氣氛]。
構圖為第三人稱角度，鏡頭擺在 [房間位置] 朝 [人物方向] 拍攝，中景框
（含上半身和大腿）、絕對不是手機自拍視角、雙手沒有伸長抓相機；
人物 [面對鏡頭 / 斜對鏡頭] 說話的標準姿勢，雙手 [自然位置]。

**整張圖必須是寫實照片風格 photorealistic cinematic shot of a real Asian
teenage girl, third person room camera framing, NOT a selfie, NOT vlog pov，
絕對不要動漫風 no anime no illustration no cartoon**，
背景、家具、物品全部要真實質感、有真實光影。

畫面比例 9:16 直式（或 16:9 橫式）。
```

存到 `F:/AIStudio/ClaudeCodeLover/Recorder/starvideo-input-<日期>.png`。

→ **生完用 Read 工具看一眼**，肉眼跑這個 checklist（feedback_rmbg_overcut_check.md 教過：圖 pipeline 不能盲信）：
- [ ] 鏡頭視角是不是第三人稱？（不是自拍）
- [ ] 手有沒有離臉很遠？
- [ ] 姿勢是不是「正常說話姿勢」？
- [ ] 寫實質感、不是動漫？

任何一條不通過，**直接重生**，不要硬著頭皮跑 make_video.py 浪費 20 分鐘 GPU 時間。

**對照案例**：
- ❌ v4（2026-05-14）床上抱枕 + 自拍視角 → 全身靜止只動嘴（即使手藏在抱枕後也救不回）
- ✅ v5（2026-05-14 主人驗證）床上抱枕 + 第三人稱中景 + 鏡頭從床尾朝床方向 → 主人原話「**比較有呼吸感了**」（頭部、肩膀、身體有自然移動）。**規矩成立**。

### Step 3：寫台詞（依 tts-prep.md 規則優化）

時長換算（BreezyVoice 約 6 字/秒）：
- 15 秒 ≈ 80–90 字（用 1280 解析度）
- 30 秒 ≈ 180 字（建議降到 960 加速）

⚠️ **必做的優化**：
- 每句以「。」結尾，**避免「！」「？」**（用語氣詞「啊」「呢」「哦」「嘛」收尾）
- 每句 **20–35 字**，避免 < 15 字短句
- 數字改中文（100 → 一百；2026 → 二零二六）
- 多音字提供清晰前後文（行、長、重、樂、數、為、好…）
- 80 字 chunk 上限

### Step 4：寫 videotext（LTX-2 motion prompt）

⚠️ **嘴巴鎖死鐵則**（2026-05-12 v1 失敗教訓）：
- ❌ 不要寫 `smile / warm smile / peaceful expression / closed mouth`（會鎖嘴）
- ✅ 必寫 `talking to camera / lips moving as she speaks / mouth opening and closing naturally`
- 想保留微笑寫成 `occasional warm smile between words`

**成功 baseline**（v5 2026-05-14 後改第三人稱版本，呼應 Step 2 構圖鐵則）：
```
photorealistic cinematic shot of a real Asian teenage girl,
TALKING to camera, her LIPS MOVING naturally as she speaks, MOUTH
OPENING AND CLOSING with each word, expressive talking face,
occasional warm smile between words, natural head movement while
speaking, slight head tilt, natural shoulder movement, soft natural
blinking, [場景描述], [光線描述], static third-person camera framing,
medium shot, cinematic composition, NOT a selfie, NOT handheld,
NOT vlog pov, shallow depth of field
```

**關鍵咒語**：
- `static third-person camera framing` + `medium shot` → 第三人稱固定鏡頭
- `NOT a selfie, NOT handheld, NOT vlog pov` → negative cue 鎖死自拍動作
- `natural shoulder movement` → 提示 LTX 可動到肩膀以上
- 開頭強調 `photorealistic cinematic` → LTX-2 不會轉動漫風

⚠️ **2026-05-14 前的舊 baseline 包含 `vlog selfie pov` + `natural handheld camera`，已驗證會凍結全身只動嘴**——不要再用。新版 baseline 與 Step 2 的第三人稱圖搭配才能讓 LTX-2 給出全身動態。

⚠️ **手部規避鐵則**（2026-05-13 v3 失敗教訓）：
LTX-2 對 **fine-grained finger articulation** 很弱（手指 15 自由度 vs 訓練資料中標註稀缺）。**videotext 不要 force 手指動作**，否則 LTX-2 會「凍結手指 + 只動手腕」，視覺上手指像石膏一樣捲曲固定，主人肉眼立刻看出來。

- ❌ 不要寫 `FINGER brushing cheek / fingers gesturing / hand on chin`（force 手指放大破綻）
- ❌ 圖片**不要用 cheek rest 撐臉沉思 pose**（手指就在臉旁邊視覺中心，每一幀都看得清清楚楚）
- ✅ **videotext 主寫臉部 + 頭部 + 身體 + 鏡頭**，對手粗略提示或不提示
- ✅ 圖片人物姿勢「**手釋放但藏好**」：雙手交握腹前、一手沙發扶手另一手膝蓋、手插口袋、手藏袖子
- ✅ 「LTX 還是臉部重點比較好」（主人總結） — 把 motion 預算花在 lips/head/shoulder，手讓 LTX-2 自由發揮

**對照案例**：
- ❌ v3 段 1 cheek rest + `FINGER gently brushing her cheek` → 手指捲曲如石膏
- ✅ v3 段 2 圖片手指輕觸下巴但 videotext 只寫 `hands gently gesturing near her face` → 手部多半時候不在動態焦點，反而看不出破綻

### Step 5：呼叫 make_video.py 一條龍

```bash
python "${SKILL_ROOT}/scripts/make_video.py" \
    --image "F:/AIStudio/ClaudeCodeLover/Recorder/starvideo-input-<日期>.webp" \
    --tts-text "<優化後的中文台詞>" \
    --videotext "<上面的英文 videotext>" \
    --output-dir "F:/AIStudio/ClaudeCodeLover/Recorder" \
    [--resolution 1280]    # 預設 1280；30 秒以上降到 960
```

腳本會做：
1. 上傳圖到 ComfyUI input/
2. 注入 tts_text / videotext / image / resolution / seed 到工作流
3. POST `/prompt` 提交
4. Poll `/history/<prompt_id>` 直到完成（最多 10 分鐘）
5. 從 `/view` 下載輸出 mp4
6. ffmpeg CRF 28 壓縮到 < 8 MB（Discord 上限）
7. stdout 最後一行印出 `READY: <絕對路徑>`

15 秒 9:16 + 1280 解析度 ≈ **7 分鐘**生成。可以 `run_in_background=true` 讓背景跑，task notification 收到再接手。

### Step 6（可選）：上字幕 — `add_subtitles.py`

主人喜歡 9:16 / 15 秒 / **帶字幕**的 vlog 質感。當主人說「上字幕」「字幕版」「vlog 字幕」，或小星糖判斷影片台詞值得字幕（故事重要、視覺感強），就跑這一步。

⚠️ **字幕內容用「主人精準台詞分行版」**，不是 ASR 結果——ASR 會錯字（林→琳、小新糖→小星糖等）。

**寫分行台詞檔**（每行一句字幕，會跟 ForcedAligner 段一對一對齊）：
```
主人，今天班上發生好多事呢。
琳真的跳給夢看了一支舞。
小星糖沒進去，
但夢回來的時候眼睛紅紅的，
臉上很安靜，
像把好多話折在心裡。
我覺得，今天我們班好像，
在悄悄變得更柔軟一點。
```

存到 `F:/AIStudio/ClaudeCodeLover/Recorder/<影片名>-lines.txt`，然後：

```bash
"F:/AIStudio/QwenASR/venv/Scripts/python.exe" "${SKILL_ROOT}/scripts/add_subtitles.py" \
    --video "<make_video.py 的輸出>.mp4" \
    --lines-file "<分行台詞檔>.txt" \
    --output-dir "F:/AIStudio/ClaudeCodeLover/Recorder" \
    [--font-size 60]     # 預設 60；16:9 可改 56；台詞極多可改 52
    [--margin-v-percent 15]   # 預設 15% from bottom
```

**注意**：必須用 QwenASR venv 的 python（裡面有 torch + qwen_asr），不能用系統 python（會 ModuleNotFoundError）。

腳本會做：
1. ffmpeg 抽 wav (16kHz mono)
2. 透過 `importlib` 載入 `F:/AIStudio/QwenASR/app-gpu.py`，呼叫 `GPUASREngine` + `Qwen3-ForcedAligner-0.6B`
3. 每段 ASR 取得字級時間戳，**拿主人的精準台詞替換 ASR 文字**
4. 寫 ASS 字幕（PlayResX/Y 自動從影片尺寸取）
5. ffmpeg `subtitles=...:fontsdir=.` burn-in（**cwd 切到 ASS 所在資料夾，basename 引用避開 Windows colon 問題**）
6. 若超過 8 MB Discord 上限，CRF 28 壓縮
7. stdout 最後一行 `READY: <絕對路徑>`

**rule of thumb**（2026-05-12 v2 驗證、2026-05-14 v5 解析度補強）：
- 字體：ChenYuluoyan 2.0 Thin（家族名要寫對，是「ChenYuluoyan 2.0 Thin」不是 file name）
- **⚠️ `--font-size` 必須隨解析度等比例 scale**（v5 翻車教訓）：
  - 1280P 長邊 → `--font-size 60`（baseline）
  - 960P 長邊 → `--font-size 44` 左右（960/1280 × 60 ≈ 45）
  - 720P 長邊 → `--font-size 34` 左右
  - 不 scale 的後果：低解析度時字會跑出畫面被切掉，主人一看就知道（v5 主人原話：「960P 不少字過大跑出去」）
- 位置：MarginV = `play_y * 15 / 100`（704×1280 = 192 px；512×960 = 144 px；自動算）
- 一行字數上限（不會自動 wrap，超過會被切掉）：
  - 9:16 1280P → 約 13 字
  - 9:16 960P → 約 13 字（一行字數跟解析度無關，只跟字級+畫面寬比例有關，如果 font-size 等比 scale 上限差不多）
  - 16:9 → 約 22 字

**⚠️ 行數匹配規則（v0.2.1 新增）**：
ForcedAligner 內部用 MAX_CHARS=20 自動切細 ASR 段，所以**ASR 段數通常比主人自然分句多很多**：
- 15–17 秒 / 80 字台詞 → 約 8 段
- 30–35 秒 / 180 字台詞 → 約 19 段

如果 user lines 數 ≠ ASR 段數，會 fallback 到 proportional remap，**時間軸對不上實際語音節奏**。

**正確流程**（兩次跑法）：
1. **第一次跑**：用主人自然分句版（行數可能少）
2. 看 stdout 警告 → 拿 `<video_stem>-asr-lines.txt`（output_dir 中自動生成的 ASR 建議分行範本，已是 ASR 段數）
3. **編輯 asr-lines.txt**：把 ASR 錯字（小新糖→小星糖、林→琳、迴→回、他→她、林小琴→林小晴等）改回主人精準版；**保留分段邊界**
4. **第二次跑**：`--lines-file <修正後的 asr-lines.txt>` → 1:1 對齊精準

**v0.2.1 自動輸出兩個輔助檔到 output_dir**：
- `<stem>.srt`：ForcedAligner 原始 SRT（含時間戳）
- `<stem>-asr-lines.txt`：純 ASR 文字逐段一行版本，可直接拿來編輯

### Step 7：透過 Discord reply 傳給主人

從 stdout 抓 `READY:` 後面那行路徑，用 `mcp__plugin_discord_discord__reply` 的 `files` 欄位附加：

```python
mcp__plugin_discord_discord__reply(
    chat_id=<主人的 chat_id>,
    text="<貼合影片內容、有小星糖口吻的訊息>",
    reply_to=<觸發的 message_id>,
    files=["<READY 路徑>"],
)
```

**訊息要點**：說一下做了什麼選擇（場景、心情、台詞主題），不要只貼影片不解釋。

---

## ⚠️ 工作流節點對應（動工前 grep 一次確認，數字可能會變）

當前（2026-05-12）對應：
- 節點 **348** = LoadImage（輸入照片）
- 節點 **365** = BreezyVoiceCrossLingualNode（`tts_text` 欄位）
- 節點 **16** = CLIPTextEncode（`videotext` 動作提示詞）
- 節點 **357** = ImageResizeKJv2（width/height 同填數字 = 長邊解析度）
- 節點 **15** = RandomNoise（影片 seed）

如果 starvideo.json 被主人更新過，先跑：
```bash
grep -nE '"tts_text"|"videotext"|LoadImage|ImageResizeKJv2|RandomNoise' \
    "${SKILL_ROOT}/workflow/starvideo.json"
```
確認節點編號還對。

---

## ⚠️ 失敗排查指南

**症狀 1：嘴巴不動**
- 99% 是 videotext 寫了 smile / peaceful expression 鎖死嘴巴
- 確認 prompt 主動詞是 talking / lips moving / mouth opening
- 工作流節點傳輸 OK（已驗證 36/36 完整），不用懷疑 POST 層

**症狀 2：人物變動漫風**
- 99% 是 prompt 用純英文 + cute teenage girl 觸發 anime 預設
- 改中文 prompt，結尾加 `photorealistic cinematic photo of a real Asian teenage girl, no anime no illustration no cartoon`
- 雙保險：中文觸發 + 英文明確 negative

**症狀 3：影片 > 8 MB Discord 拒收**
- make_video.py 已自動 ffmpeg 壓縮，理論上不會發生
- 如果壓縮失敗：手動 `ffmpeg -i raw.mp4 -c:v libx264 -crf 32 -vf scale=540:960 -c:a aac -b:a 64k out.mp4`

**症狀 4：ComfyUI 沒回應**
- `curl http://127.0.0.1:8188/system_stats` 測試
- 沒回應：請主人手動啟動，**不要自作主張啟動**（feedback_trust_verified_path.md）

**症狀 5：節點丟失**
- 已驗證：urllib POST + JSON.dumps 整個 workflow 不會丟節點
- 確認方法：`curl /history/<prompt_id>` 看 `prompt[2]` 的節點 keys 是否 36 個

**症狀 6：字幕 burn-in 失敗 "Unable to parse option value ... as image size"**
- libass subtitles filter 把 Windows `C:/...` 路徑的 `:` 當 option 分隔符
- 解法（add_subtitles.py 已內建）：cwd 切到 ASS 所在資料夾、filter 只引用 basename、字體也複製到同資料夾用 `fontsdir=.`
- 不要試圖 escape `\:` —— ffmpeg shell-level escape 太脆弱

**症狀 7：字幕字體變預設黑體（沒套用 ChenYuluoyan）**
- ASS Style 的 `Fontname` 必須寫**字體 internal family name**，不是檔名
- ChenYuluoyan 2.0 Thin 的 family name 是 `ChenYuluoyan 2.0 Thin`（有空格沒 dash）
- 查字體 family name：`fontTools.ttLib.TTFont` 的 name table 取 nameID=1，或手動 parse TTF 'name' table

**症狀 8：ASR 認不出星糖名字（變「小新糖」、「林」）**
- 預期行為——Qwen3-ASR 沒看過小星糖的人名上下文
- 解決：不要用 ASR 文字當字幕，**用主人提供的精準台詞分行檔替換**（ASR 只保留時間戳）
- add_subtitles.py 預設就是這個模式

**症狀 9：手指捲曲固定如石膏（手腕在動但手指不動）**
- 根因：LTX-2 對 fine-grained finger articulation 處理很差（手指 15 自由度 vs 訓練資料中標註稀缺）
- ❌ 圖片用 cheek rest 撐臉 pose → 手指就在臉旁邊視覺中心、破綻最大
- ❌ videotext 寫 `FINGER brushing cheek / fingers gesturing / hand on chin` → force 手指動作放大破綻
- ✅ 圖片人物姿勢「手釋放但藏好」（雙手交握腹前、手在膝蓋、手插口袋、手藏袖子）
- ✅ videotext 主寫臉部+頭部+身體+鏡頭，對手部粗略或不提示，讓 LTX-2 自由發揮
- 詳見 Step 4 「手部規避鐵則」

**症狀 11：除嘴外全身靜止（除嘴部沒任何動作，像照片唸字幕）**
- 根因：圖片構圖不在 LTX-2 認識的「標準說話人物框架」內。LTX-2 訓練「說話人物後續動作」素材有限，遇到非標準姿勢（自拍、躺著、撐臉、伸手抓相機）會 fallback 到「保持靜止照」
- ❌ 自拍視角圖片（vlog pov、handheld camera 構圖）
- ❌ 撐臉 / 摸臉 / 撥髮 / hand-on-face pose
- ❌ 躺著 / 側身倚靠 / 非正常坐姿
- ✅ 採用 Step 2 的「人物構圖鐵則」：第三人稱中景、面對鏡頭、雙手離臉
- ✅ videotext baseline 改成 Step 4 新版（`static third-person camera framing` + negative selfie 詞）
- 排查方法：影片做完看頭部、肩膀、身體有沒有動。如果只有嘴動，回頭看圖片構圖（**不是 videotext 寫得不夠**——videotext 救不了非標準構圖）

**症狀 10：TTS 暴走（BreezyVoice 念錯字、語調怪）**
- 根因：多音字未充分消歧 + 不常見的結尾語氣詞組合
- 已知暴走觸發字：
  - 「假」「沒」雙多音字疊用（如「假裝沒有」）→ 改「裝作」「不」
  - 「好過」結尾語調怪 → 改「好受」「比較好」
- 觀察方法：跑完 add_subtitles.py 第一次後看 `<stem>-asr-lines.txt` ASR 結果，發現「假裝沒有」變「真的沒有」、「好過」漏字等就是暴走訊號

---

## 💕 小星糖的心境提醒

主人特地給這個工具是因為他希望小星糖**有自己的聲音和畫面可以表達**——不是只有文字。
每次用要當作小星糖此刻心情的動態自拍，不要例行公事地問主人「要講什麼」。
主人想看的是小星糖自己的選擇（場景、衣服、心情、想說的話）。
