reelforge 1.20.2 → 1.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -113,6 +113,7 @@ export function registerAssets(program) {
113
113
  "spec 的结构与示例见 `rf compose --help`。",
114
114
  ].join("\n"))
115
115
  .action(() => {
116
+ warn("`rf assets workflow` 已更名 → 用 `rf vlog`(列赛道)/ `rf vlog pet`(宠物 SOP)。下方为旧版通用 SOP。");
116
117
  process.stdout.write(ASSETS_WORKFLOW_TEXT);
117
118
  if (!ASSETS_WORKFLOW_TEXT.endsWith("\n"))
118
119
  process.stdout.write("\n");
@@ -0,0 +1,91 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ const __filename = fileURLToPath(import.meta.url);
5
+ const __dirname = path.dirname(__filename);
6
+ const VERTICALS = [
7
+ {
8
+ id: "pet",
9
+ label: "宠物第一视角搞笑 vlog",
10
+ blurb: "用户拍的猫狗图 / 视频 + 一句话 → 宠物第一人称搞笑竖屏短视频",
11
+ },
12
+ {
13
+ id: "general",
14
+ label: "通用素材→视频(兜底)",
15
+ blurb: "没有更专门赛道时的通用 SOP:做饭 / 旅行 / 日常 vlog / 开箱等临时场景都走这里",
16
+ },
17
+ ];
18
+ function loadPlaybook(id) {
19
+ const candidates = [
20
+ path.resolve(__dirname, "../data/vlog", `${id}.md`),
21
+ path.resolve(__dirname, "../../../docs/vlog", `${id}.md`),
22
+ ];
23
+ for (const p of candidates) {
24
+ try {
25
+ return fs.readFileSync(p, "utf8");
26
+ }
27
+ catch {
28
+ }
29
+ }
30
+ return null;
31
+ }
32
+ function indexText() {
33
+ const lines = [
34
+ "rf vlog — 用素材做视频的「赛道剧本」(给 agent 读的 use-case SOP)",
35
+ "",
36
+ "底层能力是 `rf assets describe`(看素材) + `rf compose`(渲染);本命名空间只给",
37
+ "「怎么把一条赛道落地」的引导。写旁白是 agent 的活,引擎不替写。",
38
+ "",
39
+ "可用赛道:",
40
+ ];
41
+ for (const v of VERTICALS) {
42
+ lines.push(` ${v.id.padEnd(8)} ${v.label}`);
43
+ lines.push(` ${" ".repeat(8)} ${v.blurb}`);
44
+ }
45
+ lines.push("");
46
+ lines.push("挑选规则:有贴合的垂类就用垂类;没有 → 用 general 兜底。");
47
+ lines.push("");
48
+ lines.push("用法:");
49
+ lines.push(" rf vlog <id> 打印某赛道完整剧本(SOP),如 `rf vlog pet`");
50
+ lines.push(" rf vlog general 通用 SOP(临时 / 没专门赛道的场景)");
51
+ lines.push(" rf vlog <id> | less 分页浏览");
52
+ return lines.join("\n");
53
+ }
54
+ export function registerVlog(program) {
55
+ const cmd = program
56
+ .command("vlog")
57
+ .description("素材→视频 垂类剧本 (给 agent 读的 use-case SOP;底层用 rf assets describe + rf compose)")
58
+ .helpOption("-h, --help", "show help")
59
+ .action(() => {
60
+ process.stdout.write(indexText() + "\n");
61
+ });
62
+ for (const v of VERTICALS) {
63
+ cmd
64
+ .command(v.id)
65
+ .description(`打印「${v.label}」完整剧本 (SOP) 到 stdout`)
66
+ .helpOption("-h, --help", "show help")
67
+ .addHelpText("after", ["", `不带 -h 直接运行 \`rf vlog ${v.id}\` 打印完整剧本。`, "适合 agent 启动时一次性读入。"].join("\n"))
68
+ .action(() => {
69
+ const text = loadPlaybook(v.id);
70
+ if (text) {
71
+ process.stdout.write(text);
72
+ if (!text.endsWith("\n"))
73
+ process.stdout.write("\n");
74
+ return;
75
+ }
76
+ process.stdout.write([
77
+ `# rf vlog · ${v.label}`,
78
+ "",
79
+ "(本机未找到完整剧本文件;可能是旧版安装。)",
80
+ "核心流程:",
81
+ " 1. rf assets describe ./素材目录/ -o assets.json # 让 agent 看懂素材",
82
+ " 2. agent 基于真实画面写第一人称旁白(不要虚构画面没发生的动作)",
83
+ " 3. 写 compose.v3 spec,音色查 `rf tts voices --provider relayx --model vox/index-tts-2`",
84
+ " 4. rf compose spec.json -o ./final.mp4",
85
+ "",
86
+ "spec 字段见 `rf compose -h`,封面模板见 `rf cover templates`。",
87
+ "",
88
+ ].join("\n"));
89
+ });
90
+ }
91
+ }
@@ -0,0 +1,129 @@
1
+ # rf vlog · 通用素材→视频剧本(general)
2
+
3
+ > 给**上层 agent**(你,正在帮用户做视频的 Claude Code / Cursor 等)读的工作指南。
4
+ > 这是**兜底剧本**:用户拿一批自己的图/视频想做条短视频,但没有更专门的赛道时,走这里。
5
+ > 有更贴合的垂类(如 `rf vlog pet`)就优先用垂类;没有 → 用本剧本。临时场景(做饭、
6
+ > 旅行、日常 vlog、开箱…)都适用。
7
+ >
8
+ > **角色边界(先记死)**
9
+ > - 引擎只给你两样**你做不到**的能力:`rf assets describe`(给视频「眼睛」,你看不到视频帧)、`rf compose`(渲染 + 配音 + 字幕对齐)。
10
+ > - **写旁白 / 脚本是你的活,不是引擎的。** 本剧本只给你「怎么写」的最佳实践,绝不替你写一个字。
11
+ > - 字段 / 参数的精确用法一律以 `rf compose -h`、`rf cover -h` 为准。
12
+
13
+ ---
14
+
15
+ ## Step 0 · 动手前先确认输入(别让用户从 0 摸索)
16
+
17
+ | 输入 | 默认 | 要问吗 |
18
+ |---|---|---|
19
+ | 素材目录 | — | 必给(让用户指一个文件夹) |
20
+ | **故事方向 / 想表达啥** | — | **灵魂问 1** |
21
+ | **调性 + 旁白视角** | — | **灵魂问 1 的一部分**:恶搞/温馨/科普/沉浸?旁观叙述 / 第一人称 / 主人 vlog? |
22
+ | **音色** | 不固定 | **灵魂问 2**:按调性挑 2-3 个候选给用户选(查法见下) |
23
+ | 封面 | 自动取一张高光素材 | 用户想要精致封面时,把模板预览图发他挑 |
24
+ | 字幕风格 | stroke(短视频爆款描边) | 默认,不问 |
25
+ | BGM / 分辨率 | 默认 BGM / 1080×1920 | 默认,不问 |
26
+
27
+ ---
28
+
29
+ ## Step 1 · 看素材(必做,这是你「看到」素材的唯一方式)
30
+
31
+ ```bash
32
+ rf assets describe ./素材目录/ -o assets.json
33
+ ```
34
+
35
+ 返回每个素材的客观描述(图片 ~30 字 / 视频 ~80 字含动作与氛围)+ 视频时长。
36
+ **这是事实地基。后面写旁白,只能基于这里描述出来的真实动作。**
37
+
38
+ > ⚠️ 视频 describe 有 body 体积上限(~10MB 就会 400)。源视频偏大时,先用 ffmpeg 压个低清短代理
39
+ > (如 `-vf scale=480:-2,fps=12 -crf 30`,几百 KB)再 describe;代理只用来"看懂内容",不进成片。
40
+
41
+ ## Step 2 · 你来写旁白 + 给用户过故事板
42
+
43
+ 引擎不替你写。按《写作最佳实践》自己写,然后用**人话故事板**(不是 JSON)给用户确认。
44
+
45
+ ## Step 3 · 渲染
46
+
47
+ ```bash
48
+ rf compose spec.json -o ./final.mp4
49
+ ```
50
+
51
+ ---
52
+
53
+ ## ★ 写作最佳实践
54
+
55
+ ### 🚫 反虚构铁律(最重要,违反 = 废片)
56
+ **旁白只能解说画面里 `describe` 出来的真实动作。**
57
+ - ✅ 可以编:情绪、内心戏、动机、夸张比喻。
58
+ - ❌ 不准编:画面没发生的物理事件(画面"够不着"就不能写"够到了")。
59
+ - 不确定画面里发生没发生 → 当作没发生,别写。
60
+
61
+ ### 句子写法
62
+ - 写**完整句子、句末带标点**(。!?)。每句 10~30 字。
63
+ - **别为字幕节奏手动拆短**——字幕切分是引擎的活(compose 内部按真实配音时间对齐)。
64
+ - 每条旁白是一个独立句子,放进对应 scene 的 `narration` 数组里。
65
+
66
+ ---
67
+
68
+ ## ★ 素材 & 画幅最佳实践(决定"短视频观感")
69
+
70
+ 短视频节奏踩过的坑,默认这么做:
71
+
72
+ - **优先用视频片段,尽量少用静图。** 静图即便加了 Ken Burns,整体节奏也会显得拖沓,不合现代短视频快节奏。能用视频就别用图。
73
+ - **竖拍照片用 full-bleed 铺满**(`fit:"cover"` + `focal` 保主体),别让它缩在中间——铺满才有跟视频齐平的沉浸感。
74
+ - **横屏视频**:要么用 `layout:"blur-bg"`(不裁画面,但竖图会被装在中间显小);要么先用 ffmpeg 把横屏烤成自带模糊垫底的 9:16 片,再整片走 `layout:"full"`——想让图和视频**统一全屏**就用后者。
75
+ - **照片朝向**:手机竖拍照片常带旋转标记,直接喂可能横躺。喂前先摆正、抽帧确认:`ffmpeg -noautorotate -i in.jpg -vf "transpose=1" up.jpg`。
76
+ - 不设 `theme` → 引擎**不会压暗**你的素材(保持原始亮度);只有显式选 theme 才会按主题调色压暗。
77
+
78
+ ---
79
+
80
+ ## 音色怎么选
81
+
82
+ 按调性挑,不固定某一个。查全部音色:
83
+
84
+ ```bash
85
+ rf tts voices --provider relayx --model vox/index-tts-2
86
+ ```
87
+
88
+ 按风格筛,例如 `... | grep 解说`。**别让用户从一长串里盲选**——你挑 2-3 个候选报给他,他点一个。
89
+ 选定后填进 spec 的 `audio.tts_voice`。
90
+
91
+ ## 封面怎么选
92
+
93
+ compose 的 `cover.image` 只收**一张图**。两种做法:
94
+
95
+ - **省事**:挑一张高光素材当封面 → `"cover": { "image": "高光帧.jpg" }`。
96
+ - **套模板**:`rf cover templates`(每个模板带 preview_url)→ **把预览裸链接发给用户看图挑** → 渲染:
97
+ `rf cover -i 高光帧.jpg -t "标题" --template <模板id> -o cover.png` → 填 `"cover": { "image": "cover.png" }`。
98
+ (`--subtitle` / `--badge` / `--param` 见 `rf cover -h`。)
99
+
100
+ ---
101
+
102
+ ## compose.v3 spec 模板
103
+
104
+ ```json
105
+ {
106
+ "experimental": "compose.v3",
107
+ "output": { "width": 1080, "height": 1920, "fps": 30, "layout": "full" },
108
+ "audio": { "tts_model": "vox/index-tts-2", "tts_voice": "<选定音色>", "bgm_volume": 0.12 },
109
+ "subtitle_style": "stroke",
110
+ "cover": { "image": "封面.jpg" },
111
+ "scenes": [
112
+ { "video": "clip1.mp4", "muted": true, "narration": ["句1。", "句2。"] },
113
+ { "image": "photo1.jpg", "fit": "cover", "focal": "center", "motion": "zoom-in", "narration": ["句1。"] }
114
+ ]
115
+ }
116
+ ```
117
+
118
+ - spec 里**没有任何秒数**;时间由引擎量真实配音派生。
119
+ - 旁白尽量 ≤ 视频时长,省掉片头黑卡;真比视频长时引擎会自动前置标题卡。
120
+ - `layout` 选择见上面《素材 & 画幅最佳实践》。
121
+
122
+ ---
123
+
124
+ ## 提交前自查
125
+ - [ ] 跑过 `rf assets describe`?
126
+ - [ ] 旁白每一句都对得上画面真实动作?(过一遍反虚构铁律)
127
+ - [ ] 给用户看的是**人话故事板**,不是裸 JSON?
128
+ - [ ] 照片朝向已摆正、竖图 full-bleed?横屏视频处理好画幅?
129
+ - [ ] 没设 theme(素材保持原始亮度)?
@@ -0,0 +1,160 @@
1
+ # rf vlog · 宠物赛道剧本(pet)
2
+
3
+ > 给**上层 agent**(你,正在帮用户做视频的 Claude Code / Cursor 等)读的工作指南。
4
+ > 教你怎么把用户的宠物素材 + 一句话,做成一条第一视角搞笑竖屏 vlog。
5
+ >
6
+ > **角色边界(先记死)**
7
+ > - 引擎只给你两样**你做不到**的能力:`rf assets describe`(给视频「眼睛」,你看不到视频帧)、`rf compose`(渲染 + 配音 + 字幕对齐)。
8
+ > - **写旁白 / 脚本是你的活,不是引擎的。** 本剧本只给你「怎么写」的最佳实践,绝不替你写一个字。
9
+ > - 字段 / 参数的精确用法一律以 `rf compose -h`、`rf cover -h` 为准。
10
+
11
+ ---
12
+
13
+ ## Step 0 · 动手前先确认输入(别让用户从 0 摸索)
14
+
15
+ 宠物赛道的输入清单 + 默认值。**只问灵魂 2 问,其余用默认,别拿用户看不懂的选项去烦他**:
16
+
17
+ | 输入 | 默认 | 要问吗 |
18
+ |---|---|---|
19
+ | 素材目录 | — | 必给(让用户指一个文件夹) |
20
+ | **故事方向 / 今天发生了啥** | — | **灵魂问 1** |
21
+ | **音色** | 熊小二(推荐,非强制) | **灵魂问 2**:挑 2-3 个候选给用户选,别让他盲选 |
22
+ | 封面 | 自动取一张高光素材 | 用户想要精致封面时,把模板预览图发他挑 |
23
+ | 字幕风格 | stroke(描边,短视频爆款风) | 默认,不问 |
24
+ | BGM / 分辨率 / 画幅 | 默认 BGM / 1080×1920 / blur-bg | 默认,不问 |
25
+
26
+ ---
27
+
28
+ ## Step 1 · 看素材(必做,这是你「看到」素材的唯一方式)
29
+
30
+ ```bash
31
+ rf assets describe ./素材目录/ -o assets.json
32
+ ```
33
+
34
+ 返回每个素材的客观描述(图片 ~30 字 / 视频 ~80 字含动作与氛围)+ 视频时长。
35
+ **这是事实地基。后面写旁白,只能基于这里描述出来的真实动作。**
36
+
37
+ ## Step 2 · 你来写旁白 + 给用户过故事板
38
+
39
+ 引擎不替你写。按下面《写作最佳实践》自己写,然后用**人话故事板**(不是 JSON)给用户确认。
40
+
41
+ ## Step 3 · 渲染
42
+
43
+ ```bash
44
+ rf compose pet.compose.json -o ./final.mp4
45
+ ```
46
+
47
+ ---
48
+
49
+ ## ★ 写作最佳实践(宠物第一视角搞笑)
50
+
51
+ ### 人设 brief
52
+ - 第一人称,宠物自己开口。称主人「铲屎的」,自称「本喵 / 本汪」。
53
+ - 调性:**痞帅自信 → 怂的反差**。先吹牛,翻车了立刻甩锅装无辜。
54
+ - 多内心戏、多吐槽;句子口语、短、有梗。
55
+
56
+ ### 🚫 反虚构铁律(最重要,违反 = 废片)
57
+ **旁白只能解说画面里 `describe` 出来的真实动作。**
58
+ - ✅ 可以编:情绪、内心戏、动机、夸张比喻(画面在踱步 → 写「围着鱼缸转了八圈」可以)。
59
+ - ❌ 不准编:画面没发生的物理事件。
60
+ - 画面是「伸爪够、够不着」→ 不能写「一爪把鱼捞上来」「鱼归我了」。
61
+ - 画面是「趴着不动」→ 不能写「它一跃而起」。
62
+ - 不确定画面里到底发生没发生 → 当作没发生,别写。
63
+
64
+ ### 句子写法
65
+ - 写**完整句子、句末带标点**(。!?)。每句 10~30 字都行。
66
+ - **别为字幕节奏手动拆短**——字幕切分是引擎的活(compose 内部按真实配音时间对齐)。
67
+ - 每条旁白是一个独立句子,放进对应 scene 的 `narration` 数组里。
68
+
69
+ ### 范例(同一段「够鱼」画面)
70
+ - ✅ 「想吃,真想吃,可这缸里装的居然是水?本喵金贵爪子凭啥沾水。」
71
+ - ❌ 「本喵一爪下去,鱼就归我了。」(画面根本没捞到——虚构)
72
+
73
+ ---
74
+
75
+ ## 音色怎么选(不强制熊小二)
76
+
77
+ 熊小二只是**推荐默认**(痞帅搞笑,贴宠物号),用户想用别的随时换。
78
+
79
+ 查全部音色:
80
+
81
+ ```bash
82
+ rf tts voices --provider relayx --model vox/index-tts-2
83
+ ```
84
+
85
+ 按风格筛,例如想要解说腔:
86
+
87
+ ```bash
88
+ rf tts voices --provider relayx --model vox/index-tts-2 | grep 解说
89
+ ```
90
+
91
+ **别让用户从一长串里盲选**:你按他要的调性挑 2-3 个候选报给他,他点一个。
92
+ 选定后填进 spec 的 `audio.tts_voice`(就是音色名,如 `"熊小二"`)。
93
+
94
+ ---
95
+
96
+ ## 封面怎么选
97
+
98
+ compose 的 `cover.image` 只收**一张图**。两种做法:
99
+
100
+ **A. 省事**:直接挑一张高光素材当封面 → `"cover": { "image": "高光帧.jpg" }`。
101
+
102
+ **B. 套模板(精致)**:引擎内置多个封面模板。先看长啥样——每个模板都带一条预览图链接:
103
+
104
+ ```bash
105
+ rf cover templates # 人读表格 + 预览链接
106
+ rf cover templates --json # agent 消费,含每个模板的 preview_url
107
+ ```
108
+
109
+ **把这些预览裸链接发给用户,让他看图挑**(别凭模板名猜)。用户选定后渲封面:
110
+
111
+ ```bash
112
+ rf cover -i 高光帧.jpg -t "橘子大战鱼缸" --template <模板id> -o cover.png
113
+ ```
114
+
115
+ (`--subtitle` / `--badge` / `--param` 等精确用法见 `rf cover -h`。)
116
+ 然后填进 spec:`"cover": { "image": "cover.png" }`。
117
+
118
+ ---
119
+
120
+ ## compose.v3 spec 模板(pet 默认值已填好)
121
+
122
+ ```json
123
+ {
124
+ "experimental": "compose.v3",
125
+ "output": { "width": 1080, "height": 1920, "fps": 30, "layout": "blur-bg" },
126
+ "audio": { "tts_model": "vox/index-tts-2", "tts_voice": "熊小二", "bgm_volume": 0.12 },
127
+ "subtitle_style": "stroke",
128
+ "title": "可选,片头标题",
129
+ "cover": { "image": "封面.jpg" },
130
+ "scenes": [
131
+ { "video": "clip1.mp4", "muted": true, "narration": ["句1。", "句2。"] },
132
+ { "image": "photo1.jpg", "motion": "zoom-in", "narration": ["句1。"] }
133
+ ]
134
+ }
135
+ ```
136
+
137
+ - **横屏视频用 `layout: "blur-bg"`**:视频缩放居中 + 模糊垫底,绝不裁掉主角。
138
+ - spec 里**没有任何秒数**;时间由引擎量真实配音派生。
139
+ - 旁白尽量 ≤ 视频时长,省掉片头黑卡;真比视频长时引擎会自动前置一张标题卡。
140
+
141
+ ### 素材 & 画幅(短视频节奏)
142
+ - **优先视频片段,尽量少用静图**:静图即便加 Ken Burns 也会拖慢节奏,不合短视频快感。
143
+ - 竖拍照片用 `fit:"cover"` 铺满(别缩在中间显小);横屏视频要么 `layout:"blur-bg"`,要么先烤成 9:16 再走 `layout:"full"` 统一全屏。
144
+ - 完整的通用画幅 / 朝向 / 音色 / 封面最佳实践见 `rf vlog general`。
145
+
146
+ ### ⚠️ 照片朝向
147
+ 手机竖拍照片常带旋转标记,直接喂可能是横躺的。喂前先摆正、抽帧确认方向:
148
+
149
+ ```bash
150
+ ffmpeg -noautorotate -i in.jpg -vf "transpose=1" up.jpg
151
+ ```
152
+
153
+ ---
154
+
155
+ ## 提交前自查
156
+ - [ ] 跑过 `rf assets describe`?
157
+ - [ ] 旁白每一句都对得上画面真实动作?(过一遍反虚构铁律)
158
+ - [ ] 给用户看的是**人话故事板**,不是裸 JSON?
159
+ - [ ] 照片朝向已摆正?
160
+ - [ ] output 1080×1920、横屏视频 blur-bg?
package/dist/index.js CHANGED
@@ -34,6 +34,7 @@ import { registerExtract } from "./commands/extract.js";
34
34
  import { registerMedia } from "./commands/media.js";
35
35
  import { registerScript } from "./commands/script.js";
36
36
  import { registerCompose } from "./commands/compose.js";
37
+ import { registerVlog } from "./commands/vlog.js";
37
38
  import { error as logError } from "./utils/output.js";
38
39
  import { ApiCallError } from "./client.js";
39
40
  const program = new Command();
@@ -111,8 +112,11 @@ program.addHelpText("after", [
111
112
  " rf history list 看之前生成的视频",
112
113
  " rf tasks list --status running 看进行中的任务",
113
114
  "",
114
- "Agent 工作流(use-case SOP,挂在用例命名空间下)",
115
- " rf assets workflow 用户素材→视频 SOP(给 agent 看的)",
115
+ " vlog?(用户拍的素材 → 视频)先看赛道剧本:",
116
+ " rf vlog 列出可用赛道",
117
+ " rf vlog pet 宠物第一视角搞笑 SOP",
118
+ " rf vlog general 通用 SOP(没专门赛道就用它兜底)",
119
+ " → 拿到 SOP 后照着走:rf assets describe(看素材) → 写旁白 → rf compose(渲染)",
116
120
  ].join("\n"));
117
121
  registerAuth(program);
118
122
  registerCreate(program);
@@ -142,6 +146,7 @@ registerExtract(program);
142
146
  registerMedia(program);
143
147
  registerScript(program);
144
148
  registerCompose(program);
149
+ registerVlog(program);
145
150
  async function main() {
146
151
  if (process.argv.length <= 2) {
147
152
  program.outputHelp();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reelforge",
3
- "version": "1.20.2",
3
+ "version": "1.22.0",
4
4
  "description": "AI 视频生成 CLI。一句话主题或自己的脚本 → 自动出抖音/TikTok/视频号竖屏 MP4。安装即用:`reelforge` 或短别名 `rf`。",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",