- name
- cinematic-music-video
- description
- 把一首歌 + 歌词制作成电影级音乐 MV。先把主角按七个维度定全(三视图 / 五官特征 / 发型结构 / 服装层次 / 表情库 / 标志动作 / 场景气质),再把反复出现的地点定义成场景资产(scenes[] + 无人空镜锚点图),用 agnes-image-2.5-flash 生成「角色资产设定表」(大特写 + 正/侧/背三视图 + 表情带)与逐镜头首帧,用 agnes-3.0-flash 的视觉能力从真实图片反读结构化参数并做人物/场景一致性质检与自动重画,然后用 agnes-video-2.5-flash 图生视频,最后用 ffmpeg 按帧精确拼接并混入原声。保证人物一致性、地点一致性、情绪可控、原声一致性、不丢帧、图音同步、画布统一、镜头有真实运动。所有产物与中间产物统一输出到当前会话工作目录下的 out/(process.cwd()/out,不写回技能安装目录,可用 CMV_OUT_DIR 改根),源码不含任何机器相关绝对路径。Invoke when the user asks to make a music video / MV / 音乐视频 / 歌曲 MV from a song and lyrics.
- agent_created
- true
# 电影级音乐 MV 制作 (cinematic-music-video)
把「一首歌 + 歌词」变成一支电影质感的音乐 MV。整条流水线用 Agnes 三个模型分工:
**agnes-3.0-flash 当导演兼质检**(分镜 + 人物视觉核验)、**agnes-image-2.5-flash 当摄影**
(角色设定表 + 逐镜首帧)、**agnes-video-2.5-flash 让画面动起来**(图生视频),最后 **ffmpeg 做剪辑与混音**。
> **本版本的三处核心升级。**
>
> **其一:人物一致性从"靠祈祷"变成"有门禁"。**
> 旧做法是「一张半身定妆照 → 派生全身照 → 逐镜头生成」,结果实测出现**首帧和定妆照明显不是同一个人**。
> 原因有四:半身照没有全身信息(模型只能编)、派生链是"复制品的复制品"(每过一手都掉精度)、
> 镜头是 16:9 远景而参考是 1:1 半身(尺度差太大)、身份描述被埋在长提示词中间(权重被稀释)。
> 现在改为:**一张角色设定表锁定真源 → 单独反读结构化参数 → 派生多角度白底参考图 →
> 每张含主角的首帧都过一遍视觉一致性核验,不达标自动带着"差异清单"重画。**
>
> **其二:人物"先定清楚再进分镜"。**
> 旧做法是规划器一边编剧情一边临时决定这个人穿什么、什么表情、在什么场景里——
> 结果"同一个人在每个镜头里都像刚被重新介绍了一遍"。
> 现在要求**在写任何镜头之前**,把主角按**七个维度**全部定死:
> **三视图、五官特征、发型结构、服装层次、表情库、标志动作、场景气质**。
> 定完之后分镜就退化成"把已经定好的人放进已经定好的场景",跨镜头一致性也从"靠模型自觉"
> 变成"靠结构保证"。详见 **1.1 七维度角色档案**。
>
> **其三:地点也是资产,且身份被切成"不可变 / 本场可变"两桶。**
> 三个镜头写在"同一个雨夜街头",就是三次独立生成——巷子宽窄、灯位、积水颜色都会漂,
> 观众不会说"巷子变了",只会觉得"这不像一个片子"。现在**地点先在 `scenes[]` 里定义成资产**,
> 系统为它生成一张**无人空镜锚点图**,该地点的所有镜头(含空镜)都挂上它(见 **2.4**)。
> 同时角色卡被拆成 **不可变身份**(骨相 / 发构 / 肤色)与 **本场可变**(服装 / 淋湿 / 擦伤 / 速度)两桶,
> 分块注入——因为把"架构"和"天气"混成一段,就是换脸的起点(见 **2.2**)。
> **一致性 = 共享可复用资产 + 显式引用**,而不是"在提示词里重新描述一遍"。
>
> **外加一条工程约束:产物与机器环境解耦。**
> **所有结果与中间产物只写进当前会话工作目录下的 `out/`(`process.cwd()/out`),不写回技能安装目录**;
> 源码内不含任何绝对路径、盘符、用户名或 `home` 目录,歌曲/歌词会被复制进项目使项目可整体搬走。
> 详见 **输出契约** 一节,由 `test/paths.js` 强制守住。
## 何时调用
用户出现以下意图时调用本 Skill:
- "给这首歌做个 MV / 音乐视频 / 音乐短片"
- "根据歌词做一支电影感的视频"
- 提供了歌曲文件 + 歌词,要求生成带音乐的成片
- 要求"图生视频 + 保持人物一致 + 不丢帧 + 图音同步"的 MV 类交付
## 运行环境
| 依赖 | 要求 | 检查方式 |
|---|---|---|
| Node.js | ≥ 18(本机已有 22.x) | `node -v` |
| ffmpeg / ffprobe | 必须可用,含 libx264 | `ffmpeg -version` |
| Agnes API Key | 至少 1 个,推荐 2 个以上用于轮询 | 见下 |
> 脚本**零 npm 依赖**,只用 Node 内置模块。
> Windows 下 ffmpeg 通常已在 PATH;若未安装:`winget install Gyan.FFmpeg`。
> ffmpeg 不在 PATH 时可用环境变量指定绝对路径:`FFMPEG=...` / `FFPROBE=...`。
> 若 bash 里 `cp/ls/head/tail/wc/sed/basename` 报 `command not found`(本机实测过的一种 PATH 劣化),
> 执行前先补回 PATH(注意 `/c/Windows` 也要在,否则 `cp` 仍找不到):
> `export PATH="/usr/bin:/bin:/c/Windows/System32:/c/Windows:$PATH"`
## 输出契约:一切都在当前会话的 `<cwd>/out/` 下
**这是硬约束,不是约定。** 所有结果输出与中间产物**只**写到**运行命令时的当前工作目录**下的 `out/`,
不写回技能安装目录;同一流水线的各阶段必须在**同一个 cwd** 下执行(或用 `CMV_OUT_DIR` 固定输出根)。
```
<cwd>/out/ ← 唯一的输出根(跟随会话),可整体删除或打包
├── .state/ # 跨项目共享的运行时状态
│ ├── .agnes-key-state.json # Key 轮询游标
│ └── vision-cache/ # 喂给视觉模型的降采样图缓存
├── mv-<歌名>/ # 每首歌一个项目目录
│ └── 00_source/ … 05_output/ # 见「目录结构」
```
**为什么这样设计**(对应两条硬性要求):
| 要求 | 实现 |
|---|---|
| 产物集中、可整体清理 | 所有路径由 `lib/paths.js` 统一推导,输出根锚定当前会话 cwd(`<cwd>/out`) |
| **不依赖本机环境**(绝对路径 / 用户名 / 盘符) | 源码内**零**硬编码绝对路径;`process.cwd()` 只允许出现在 `lib/paths.js`(锚定输出根),`test/paths.js` 会扫描源码强制这条 |
想换盘或挂到大容量目录时,**不要改代码**,用环境变量覆盖即可:
```bash
CMV_OUT_DIR="/d/mv-out" node "<skill-dir>/scripts/run.js" --song "夜曲.mp3" --keys "sk-aaa"
```
> **项目目录现在是可搬走的。** `plan.js` 会把歌曲(以及歌词文件)复制进 `00_source/`,
> 并在 `project.json` 里记录**相对项目目录**的路径。因此项目复制到另一台机器后,
> `assemble.js` / `verify.js` 仍能找到原声,不会因为"用户把歌挪走了"而失败。
## API Key 与轮询
Key 按优先级解析,**支持多个 Key 自动轮询**:
1. `--keys "sk-a,sk-b,sk-c"`(逗号/分号/空格分隔)
2. `--keys-file keys.txt`(每行一个,`#` 开头为注释)
3. `--api-key sk-x`(单个)
4. 环境变量 `AGNES_API_KEYS` → `agnes-api-key` → `AGNES_API_KEY`
轮询行为:**round-robin 游标持久化**在当前会话的 `out/.state/.agnes-key-state.json`,遇到 `401/403` 或 `429`
会把该 Key 打入冷却队列并自动切到下一个(冷却时长指数递增,上限 5 分钟);多个 Key 全部冷却时
会等待最早恢复的那个。每个阶段结束会打印各 Key 的成功调用次数。
游标是**跨项目共享**的,所以连续做多首歌时请求会持续在 Key 之间分散,而不是每首歌都从第 1 个 Key 重新开始。
> **安全**:不要把 Key 写进任何文件或日志。脚本只通过参数/环境变量读取,日志里只输出掩码。
## 四阶段流水线
```
plan.js → images.js → videos.js → assemble.js
七维度角色档案 设定表→反读参数→多角度参考→逐镜首帧 图生视频 精确拼接+混原声
+ 地点资产库 + 地点空镜锚点 + 就绪预检
+ 分镜铁律 (过身份门禁 + 身份/本场分块注入) + 一镜多拍选优
↑___________________agnes-3.0-flash 视觉质检___________________↑
```
**三类资产,全部"先行":** 角色(设定表 + 反读参数 + 多角度参考)、**地点**(无人空镜锚点)、
风格(基调板)。所有镜头只负责**引用**这些资产,而不是在提示词里重新描述一遍——
这是一致性从"靠模型自觉"转为"靠结构保证"的关键。
一步跑完(可断点续跑,已完成的阶段自动跳过)。**无需指定输出目录**——默认就是
当前会话工作目录下的 `out/mv-<歌名>/`:
```bash
node "<skill-dir>/scripts/run.js" \
--song "夜曲.mp3" \
--lyrics "夜曲.lrc" \
--brief "城市里两个人在雨夜重逢又告别" \
--style "霓虹雨夜,胶片质感,青橙色调" \
--aspect 16:9 --shots 26 \
--video-mode keyframe --motion-min 1.2 --motion-retries 2 \
--transition dissolve --fade --normalize-audio \
--concurrency 3 \
--keys "sk-aaa,sk-bbb"
```
也可以分阶段执行,便于逐段审查(**推荐**,见下方"标准工作流")。
---
## 阶段 1:分镜规划 `plan.js`
```bash
# 默认输出到 <cwd>/out/mv-夜曲/;要换位置加 --out <dir>
node "<skill-dir>/scripts/plan.js" \
--song "夜曲.mp3" \
--lyrics "夜曲.lrc" \
--brief "创作要求:一段关于城市夜晚重逢的故事" \
--style "赛博朋克霓虹" \
--aspect 16:9 --shots 26 \
--keys "sk-aaa,sk-bbb"
```
**它做什么**
1. **把输入收进项目**:歌曲(以及歌词文件,若来自文件)复制到 `00_source/`,
`project.json` 只记录**相对项目目录**的路径。这样项目搬走后 `assemble.js` 仍能找到原声,
不会因为原文件被移动、改名或换盘而失败。内联歌词本就已写进 `storyboard.json`。
2. 用 ffprobe 读出歌曲**真实时长**(后续所有时间轴的锚点)。
3. 读取歌词:支持纯文本、内联文本、`auto`(自动找同名 `.lrc`)。
**LRC 带时间戳时**会解析出每句的真实时间点,分镜会尽量对齐,并在成片时把歌词挂到对应镜头上——这是最强的时间同步手段。
4. 让 `agnes-3.0-flash` 以「顶级 MV 导演」身份输出严格 JSON 分镜。**但它必须先交出一份完整的角色档案**:
七个维度(三视图所需的体型比例、五官特征、发型结构、服装层次、表情库、标志动作、场景气质)
全部定完之后,才开始写镜头。详见下面 **1.1**。
5. **校验 + 自动修复**:校验器会检查镜头时长是否在 4–12s、`video_prompt` 是否同时包含「摄影机运动 + 主体动作」、是否缺少必填字段、镜头数是否与时长匹配,以及**角色档案七个维度是否定全**。不合格就把**具体问题**回传给模型重写(默认最多 3 次)。档案维度缺失默认只警告并兜底;`--strict-profile` 时升级为硬失败。
6. **时间轴拟合** `fitTimeline()`:把所有镜头时长缩放到与歌曲时长**精确相等**,处理超长拆分、过短合并、余量兜底。每个镜头输出 `api_seconds`(向上取整到 4–12 的整数,供 API 使用)。
**产出**
- `00_source/` —— 歌曲(+ 歌词)副本,使项目自包含
- `01_plan/storyboard.json` —— 机器可读,后续阶段都读它
- `01_plan/storyboard.md` —— 人类可读分镜表(镜头表 + 逐镜头详案 + 提示词全文)
> **关键**:`plan.js` 结束后**必须用 Read 工具打开 `storyboard.md` 给用户过目**,确认故事方向、
> 人物设定、风格是否正确,再进入生图。这一步是整条流水线里最省时间的返工点。
### 1.1 先把人物"定清楚":七维度角色档案
**这是本版最重要的方法升级。** 人物漂移的深层原因不是"模型画不准",而是**人设根本没定全**——
规划器一边想剧情一边临时决定这个人穿什么、什么表情、在什么场景里,每个镜头各拍一次脑袋,
结果就是"同一个人在每个镜头里都像刚被重新介绍过一遍"。
正确顺序是:**先把一个人从"长什么样"一路拆到"怎么动、穿什么、适合出现在哪里",再把这个人放进分镜。**
定完之后分镜工作就退化成"把已经定好的人放进已经定好的场景"。
七个维度、对应的字段、以及它被谁消费:
| # | 维度 | 字段 | 定什么 | 由谁填 | 被谁用 |
|---|---|---|---|---|---|
| ① | **三视图** | (由 params 自动生成) | 正面/侧面/背面全身 + 大特写 + 表情带 | 系统生成 | 身份真源 |
| ② | **五官特征** | `face_shape` `eye_shape` `iris_color` `eye_spacing` `eyebrows` `nose` `lips` `jaw_chin` `skin_tone` | 脸的可复现形体结构 | 规划器写 → 视觉反读覆盖 | 每张首帧的身份块 |
| ③ | **发型结构** | `hair_structure` `hair_color` | **长度 + 分缝 + 刘海 + 卷度 + 层次**(不许只写 "long hair") | 同上 | 身份块 + 身份尾 |
| ④ | **服装层次** | `wardrobe_layers{inner,outer,bottoms,footwear,accessories,materials}` `garment_palette` `wardrobe` | 按穿着层次逐层拆(不许压扁成一句) | 同上 | 身份块(逐层进入提示词) |
| ⑤ | **表情** | `expression_bank[4]{name,en}` + 每镜 `shot.expression` | 4 个具名表情 + 各自的英文执行描述 | **规划器独有**(设定表是中性的,视觉读不出表情) | 首帧的独立 `EXPRESSION` 块 |
| ⑥ | **动作** | `signature_actions[3]` | 这个人特有的标志性动作/体态 | **规划器独有** | 镜头 `subject_action` 优先复用 |
| ⑦ | **场景气质** | `scene_affinity` | 适合的空间类型、材质与光线 | **规划器独有** | 含主角镜头的选景约束 |
**为什么③④要从图片反读,而⑤⑥⑦不能?**
设定表是一张**中性的、纯影棚的、单一服装的**图。它能告诉你头发怎么分层、衣服怎么穿,
但它**不包含**这个人的情绪范围、习惯动作、以及他属于什么样的世界。让视觉模型去"读"这三样,
它只会编。所以字段分两类,各归各的:
```
VISUAL_FIELDS 五官 / 发型结构 / 服装层次 / 色板 → 视觉模型从设定表真实反读(文字与画面必须一致)
CREATIVE_FIELDS 表情库 / 标志动作 / 场景气质 / 气质 → 规划器拥有,原样透传(不参与反读)
```
这条分工写死在 `lib/assets.js`(`VISUAL_FIELDS` / `CREATIVE_FIELDS`),有离线测试守着。
**表情为什么要单独走一条通道(而不是写在 `keyframe_prompt` 里)?**
因为"情绪"和"长相"是两种信息,混在一句里会互相污染。本版把表情提成独立字段:
规划器给每种表情一句英文执行描述,渲染时**单独注入一个 `EXPRESSION` 块**,并明确告诉图像模型:
> 表情只改变面部肌肉;骨相、眼型与眼距、鼻、唇、下颌线、肤色、发型必须与参考图完全一致。
于是"同一个人在哭"和"同一个人在笑"不会再变成"两个长得像的人"。空镜写 `none`,
主角刻意中性写 `neutral`——两者都不会注入任何表情块。
**约束强度可控**:默认七维度缺失只给**警告并用默认值兜底**(缺失的表情库自动补 4 个默认表情,
流水线照常跑完);加 `--strict-profile` 则升级为**硬失败**,规划器必须补齐才能通过。
日常建议先用默认跑,看到警告后再针对性修正;要对交付质量有硬保证时再开 strict。
### 1.2 地点也要"先定清楚":`scenes[]` 地点资产库
和人物一样,**地点也必须先定义成资产、再写镜头**。规划器要交出 2-4 个反复出现的地点:
```json
"scenes": [
{ "id": "SC1", "name": "雨夜霓虹街头",
"description": "a narrow rain-soaked alley lined with shuttered storefronts…",
"architecture": "wet asphalt, glazed brick walls, corrugated shutters",
"lighting": "sodium street lamps overhead throwing warm pools onto the wet ground",
"used_by": ["S01", "S03", "S07"] }
]
```
然后每个镜头用 `scene_id` 引用它。产出会在 `storyboard.md` 里渲染成一张**地点资产表**,
`引用镜头` 一列如果显示"无镜头引用",说明这个场景白定义了。
校验器会报三类问题:`scene_id` 引用了不存在的场景(**硬失败**)、场景字段写得太粗、
以及场景没人引用。完整机制见 **2.4**。
### 1.3 三条分镜铁律(都是为了绕开模型的短板)
| 铁律 | 问题 | 做法 | 校验 |
|---|---|---|---|
| **动作安全区** | 多人肢体交互(拥抱/打斗/递物/牵手)是**穿模与肢体融合的重灾区**,模型会把两条手臂融在一起 | 改用**景别切换 + 反应镜头**:特写手指停住、特写对方表情、特写两只手之间那段没碰到的距离。真要接触就压在特写/中近景里完成,别用远景交代全身交互 | 多人同框 + 动作含交互词 → 自动警告并给出替代建议 |
| **情绪落点用稳镜头** | 运镜越花,越看不清脸;而全片最需要看清脸的就是那句关键歌词 | 副歌第一句 / Bridge 最高音配**固定机位或极缓推移**(`motion_intensity = 1`);环境镜头、转场、鼓点密集处才放开到 3 | 写入规划约束(软性创作建议) |
| **地点要成资产** | 同一个地点在多个镜头里各自生成 → 巷子宽窄、灯位、积水颜色都会漂 | 见 1.2 / 2.4 | `scene_id` 引用有效性 |
另外 `shot.transient_state` 与 `shot.speed` 也是本版新增:前者是"天气"专用通道(淋湿的头发、
敞开的衣领),后者控制整段拍摄速度(`normal / slow-motion / fast-motion`)。
两者都进首帧的 `SHOT-LOCAL STATE` 块,**绝不混进身份描述**。
---
## 阶段 2:角色素材 + 生图 `images.js`
```bash
node "<skill-dir>/scripts/images.js" --concurrency 3 \
--sheet-candidates 2 --identity-min 70 --identity-retries 2 --keys "sk-aaa,sk-bbb"
```
> **`--project` 是可选的。** 省略时自动选取当前会话 `out/` 下**唯一**的项目;
> 若有多个项目,脚本会**列出候选并要求你显式指定**,而不是猜一个——静默选错片子比报错更贵。
> 阶段 2/3/4 与 `verify.js` 都是这个规则。
### 2.1 角色资产:三步走,每一步都依赖上一步
| 顺序 | 文件 | 作用 |
|---|---|---|
| 1 | `02_images/ref_charsheet.png` | **角色资产设定表** —— 全片人物身份的唯一真源 |
| 2 | `02_images/character_lock.json` | **结构化身份参数 + 创意档案** —— 视觉反读的五官/发型/服装层次 + 规划器的表情库/动作/场景气质 |
| 3 | `02_images/ref_face.png`、`ref_body_<角度>.png` | 面部特写 + 各角度白底全身参考图(均由**面部特写**派生,见铁律一) |
| 4 | `02_images/ref_style.png` | 风格基调板(无人物),锁定色彩/光线/质感 |
| 5 | `02_images/ref_scene_<id>.png` | **地点锚点**:每个反复出现的地点一张**无人空镜**,锁定空间/材质/光位/色调(见 2.4) |
| 6 | `02_images/shot_S01.png` … | 逐镜头首帧(含主角的会过身份核验,并单独注入表情块) |
**第 1 步:为什么是"一张设定表"而不是"定妆照"**
设定表 = **一次生成**里按上下两带排布:
```
┌──────────────────────────────────────────────────┐
│ 上带 ~62% │ │
│ ┌──────────────┐ │ 正面 / 侧面 / 背面 │
│ │ 大胸像特写 │ │ 三个全身站立视图 │
│ └──────────────┘ │ │
├──────────────────────────────────────────────────┤
│ 下带 ~30% 微笑 │ 害羞 │ 委屈 │ 开心 │
│ 四个表情头像(同一张脸,同框同构图) │
└──────────────────────────────────────────────────┘
GitHub에서 보기