reelforge 1.20.1 → 1.21.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.
- package/dist/commands/assets.js +1 -0
- package/dist/commands/create.js +3 -24
- package/dist/commands/vlog.js +83 -0
- package/dist/data/vlog/pet.md +155 -0
- package/dist/index.js +6 -2
- package/package.json +1 -1
package/dist/commands/assets.js
CHANGED
|
@@ -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");
|
package/dist/commands/create.js
CHANGED
|
@@ -154,8 +154,6 @@ function optsToBody(opts) {
|
|
|
154
154
|
out.asr_model = opts.asrModel;
|
|
155
155
|
if (opts.imageModel !== undefined)
|
|
156
156
|
out.image_model = opts.imageModel;
|
|
157
|
-
if (opts.promptPrefix !== undefined)
|
|
158
|
-
out.prompt_prefix = opts.promptPrefix;
|
|
159
157
|
if (opts.style !== undefined)
|
|
160
158
|
out.style = opts.style;
|
|
161
159
|
if (opts.characterRef !== undefined)
|
|
@@ -168,8 +166,6 @@ function optsToBody(opts) {
|
|
|
168
166
|
out.video_fps = opts.videoFps;
|
|
169
167
|
if (opts.quality !== undefined)
|
|
170
168
|
out.quality = opts.quality;
|
|
171
|
-
if (opts.crf !== undefined && Number.isFinite(opts.crf))
|
|
172
|
-
out.crf = opts.crf;
|
|
173
169
|
if (opts.videoBitrate !== undefined)
|
|
174
170
|
out.video_bitrate = opts.videoBitrate;
|
|
175
171
|
if (opts.motion !== undefined)
|
|
@@ -188,10 +184,6 @@ function optsToBody(opts) {
|
|
|
188
184
|
out.bgm_volume = opts.bgmVolume;
|
|
189
185
|
if (opts.subtitleStyle !== undefined)
|
|
190
186
|
out.subtitle_style = opts.subtitleStyle;
|
|
191
|
-
if (opts.subtitleColor !== undefined)
|
|
192
|
-
out.subtitle_color = opts.subtitleColor;
|
|
193
|
-
if (opts.subtitleBackground !== undefined)
|
|
194
|
-
out.subtitle_background = opts.subtitleBackground;
|
|
195
187
|
if (opts.subtitleBottom !== undefined && Number.isFinite(opts.subtitleBottom))
|
|
196
188
|
out.subtitle_bottom_px = opts.subtitleBottom;
|
|
197
189
|
if (opts.profile !== undefined && opts.profile.trim())
|
|
@@ -230,10 +222,6 @@ function optsToBody(opts) {
|
|
|
230
222
|
cover.base = opts.coverBase;
|
|
231
223
|
if (Object.keys(cover).length > 0)
|
|
232
224
|
out.cover = cover;
|
|
233
|
-
if (opts.segmentMinChars !== undefined)
|
|
234
|
-
out.segment_min_chars = opts.segmentMinChars;
|
|
235
|
-
if (opts.segmentMaxChars !== undefined)
|
|
236
|
-
out.segment_max_chars = opts.segmentMaxChars;
|
|
237
225
|
if (opts.previewOnly)
|
|
238
226
|
out.preview_only = true;
|
|
239
227
|
return out;
|
|
@@ -283,7 +271,6 @@ export function registerCreate(program) {
|
|
|
283
271
|
.option("-d, --duration <sec>", "target video duration in seconds (generate mode only; default 45). LLM aims for ~duration × 5 chars of narration.", (v) => parseInt(v, 10))
|
|
284
272
|
.option("-p, --pace <pace>", "visual rhythm hint passed to the LLM: slow | normal | fast (default normal). LLM still decides the actual scene count from semantic structure.")
|
|
285
273
|
.option("--image-model <id>", "image model (rx-image-z | rx-image-flux | rx-image-qwen | rx-image-qwen-edit). Auto-switches to the edit-capable model when --character-ref is set.")
|
|
286
|
-
.option("--prompt-prefix <text>", "raw style prefix prepended to every image prompt (overrides --style)")
|
|
287
274
|
.option("--style <preset>", "image style preset id — server-expanded. 用户不知道选哪个时,agent 应该先跑 `rf styles list --json` 把每个风格的 preview_url(同一灯塔在 17 种风格下的对比图)给用户看,让用户照图选。仅看 id 列表用户和你都猜不准实际出图长啥样")
|
|
288
275
|
.option("--character-ref <urlOrPath>", "reference image of the main character — locks identity across scenes. URL, data: URI, or local png/jpg/webp path (auto-encoded). Auto-enables the edit-capable image model.")
|
|
289
276
|
.option("--motion <preset>", "per-scene image animation intensity. See 'Motion presets' below. Default: lite.")
|
|
@@ -294,8 +281,6 @@ export function registerCreate(program) {
|
|
|
294
281
|
.option("--bgm-volume <0..1>", "BGM mix volume (0=silent, 1=full). Default 0.15 keeps narration intelligible.", (v) => Number(v))
|
|
295
282
|
.option("--no-bgm", "disable background music for this render")
|
|
296
283
|
.option("--subtitle-style <preset>", "subtitle visual style. See 'Subtitle styles' below. Default: plate.")
|
|
297
|
-
.option("--subtitle-color <css>", "override subtitle text color, e.g. '#ffeb3b'. Omit for preset default.")
|
|
298
|
-
.option("--subtitle-background <css>", "override plate-preset background, e.g. 'rgba(20,30,80,0.75)'. Other presets ignore.")
|
|
299
284
|
.option("--subtitle-bottom <px>", "override the subtitle bottom offset in px (default 400, the lower-third position). blur-bg/letterbox images move up to clear it.", (v) => parseInt(v, 10))
|
|
300
285
|
.option("--profile <id>", "视频的「场景」(垂类):plain(默认,纯 AI 图,无组件)| finance(财经/金融,数据组件 + 鎏金深蓝)| mindset(财商认知,认知组件 + 暖纸黏土)| general(通用组件,中性皮)。选了 profile 即带出它的组件、配色、配图风、封面与字幕默认,可被其他 flag 覆盖。")
|
|
301
286
|
.option("--subtitle-translate <lang>", "双语字幕:把每条字幕翻译成指定语言,以小一号文字显示在主字幕下方,如 'en'(英文)/ 'ja'(日文)。开启后字幕分段自动变短(主字幕保持单行)。默认:开(en);传 'off' 关闭(也可覆盖 recipe 设置)")
|
|
@@ -317,12 +302,9 @@ export function registerCreate(program) {
|
|
|
317
302
|
.option("--llm-model <id>", "override the LLM model used for scene-plan")
|
|
318
303
|
.option("--tts-model <id>", "override the TTS model (defaults to the server's)")
|
|
319
304
|
.option("--asr-model <id>", "override the ASR model (defaults to the server's)")
|
|
320
|
-
.option("--segment-min-chars <N>", "subtitle SEGMENT (分段) min chars (default 10). A segment is one on-screen subtitle unit, not a visual line — visual wrapping is automatic.", (v) => parseInt(v, 10))
|
|
321
|
-
.option("--segment-max-chars <N>", "subtitle SEGMENT (分段) max chars (default 28 ≈ 2 visual lines at 820px safe width). HTML clamps display to 2 lines + ellipsis.", (v) => parseInt(v, 10))
|
|
322
305
|
.option("--video-fps <n>", "output video fps (default 24 — cinema-standard, ~20% faster render vs 30; pass 30 if you want smoother motion)", (v) => parseInt(v, 10))
|
|
323
|
-
.option("--quality <preset>", "encoder quality preset: draft | standard (default) | high. draft shrinks output ~3-4× vs standard (~3 Mbps)
|
|
324
|
-
.option("--
|
|
325
|
-
.option("--video-bitrate <rate>", "目标视频码率,如 '750k' / '1M' / '2M'。用于命中文件大小预算。和 --crf 二选一。")
|
|
306
|
+
.option("--quality <preset>", "encoder quality preset: draft | standard (default) | high. draft shrinks output ~3-4× vs standard (~3 Mbps); high is the opposite. Mutually exclusive with --video-bitrate.")
|
|
307
|
+
.option("--video-bitrate <rate>", "目标视频码率,如 '750k' / '1M' / '2M'。用于命中文件大小预算。")
|
|
326
308
|
.option("--recipe <file>", "load defaults from a JSON recipe file (CLI flags still override)")
|
|
327
309
|
.option("--redo", "replay last successful create from ~/.reelforge/last-create.json")
|
|
328
310
|
.option("--dry-run", "print the final request body + estimated units; do NOT submit")
|
|
@@ -385,8 +367,6 @@ export function registerCreate(program) {
|
|
|
385
367
|
" plate semi-transparent black plate + white text (CapCut default; safest readability)",
|
|
386
368
|
" stroke bold white text with black stroke + shadow, no plate (抖音网红风)",
|
|
387
369
|
" cinema bottom black gradient backdrop + lighter text (film / documentary look)",
|
|
388
|
-
" · use --subtitle-color / --subtitle-background to override the preset's colors",
|
|
389
|
-
" e.g. --subtitle-color '#ffeb3b' --subtitle-background 'rgba(20,30,80,0.75)'",
|
|
390
370
|
"",
|
|
391
371
|
"Brand chrome (--brand-* flags or config.video.default_brand):",
|
|
392
372
|
" Adds a constant @handle / slogan / logo block in a frame corner.",
|
|
@@ -402,8 +382,7 @@ export function registerCreate(program) {
|
|
|
402
382
|
"",
|
|
403
383
|
"Image style presets (--style <preset>) — server expands id → prompt prefix:",
|
|
404
384
|
formatStylePresetsList(),
|
|
405
|
-
" ·
|
|
406
|
-
" · Omit both to let the LLM pick a per-video style automatically.",
|
|
385
|
+
" · Omit --style to let the LLM pick a per-video style automatically.",
|
|
407
386
|
" · Run `rf styles list` to query the server's live catalog.",
|
|
408
387
|
"",
|
|
409
388
|
"Output behavior:",
|
|
@@ -0,0 +1,83 @@
|
|
|
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
|
+
function loadPlaybook(id) {
|
|
14
|
+
const candidates = [
|
|
15
|
+
path.resolve(__dirname, "../data/vlog", `${id}.md`),
|
|
16
|
+
path.resolve(__dirname, "../../../docs/vlog", `${id}.md`),
|
|
17
|
+
];
|
|
18
|
+
for (const p of candidates) {
|
|
19
|
+
try {
|
|
20
|
+
return fs.readFileSync(p, "utf8");
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
function indexText() {
|
|
28
|
+
const lines = [
|
|
29
|
+
"rf vlog — 用素材做视频的「赛道剧本」(给 agent 读的 use-case SOP)",
|
|
30
|
+
"",
|
|
31
|
+
"底层能力是 `rf assets describe`(看素材) + `rf compose`(渲染);本命名空间只给",
|
|
32
|
+
"「怎么把一条赛道落地」的引导。写旁白是 agent 的活,引擎不替写。",
|
|
33
|
+
"",
|
|
34
|
+
"可用赛道:",
|
|
35
|
+
];
|
|
36
|
+
for (const v of VERTICALS) {
|
|
37
|
+
lines.push(` ${v.id.padEnd(8)} ${v.label}`);
|
|
38
|
+
lines.push(` ${" ".repeat(8)} ${v.blurb}`);
|
|
39
|
+
}
|
|
40
|
+
lines.push("");
|
|
41
|
+
lines.push("用法:");
|
|
42
|
+
lines.push(" rf vlog pet 打印「宠物赛道」完整剧本(SOP)");
|
|
43
|
+
lines.push(" rf vlog pet | less 分页浏览");
|
|
44
|
+
return lines.join("\n");
|
|
45
|
+
}
|
|
46
|
+
export function registerVlog(program) {
|
|
47
|
+
const cmd = program
|
|
48
|
+
.command("vlog")
|
|
49
|
+
.description("素材→视频 垂类剧本 (给 agent 读的 use-case SOP;底层用 rf assets describe + rf compose)")
|
|
50
|
+
.helpOption("-h, --help", "show help")
|
|
51
|
+
.action(() => {
|
|
52
|
+
process.stdout.write(indexText() + "\n");
|
|
53
|
+
});
|
|
54
|
+
for (const v of VERTICALS) {
|
|
55
|
+
cmd
|
|
56
|
+
.command(v.id)
|
|
57
|
+
.description(`打印「${v.label}」完整剧本 (SOP) 到 stdout`)
|
|
58
|
+
.helpOption("-h, --help", "show help")
|
|
59
|
+
.addHelpText("after", ["", `不带 -h 直接运行 \`rf vlog ${v.id}\` 打印完整剧本。`, "适合 agent 启动时一次性读入。"].join("\n"))
|
|
60
|
+
.action(() => {
|
|
61
|
+
const text = loadPlaybook(v.id);
|
|
62
|
+
if (text) {
|
|
63
|
+
process.stdout.write(text);
|
|
64
|
+
if (!text.endsWith("\n"))
|
|
65
|
+
process.stdout.write("\n");
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
process.stdout.write([
|
|
69
|
+
`# rf vlog · ${v.label}`,
|
|
70
|
+
"",
|
|
71
|
+
"(本机未找到完整剧本文件;可能是旧版安装。)",
|
|
72
|
+
"核心流程:",
|
|
73
|
+
" 1. rf assets describe ./素材目录/ -o assets.json # 让 agent 看懂素材",
|
|
74
|
+
" 2. agent 基于真实画面写第一人称旁白(不要虚构画面没发生的动作)",
|
|
75
|
+
" 3. 写 compose.v3 spec,音色查 `rf tts voices --provider relayx --model vox/index-tts-2`",
|
|
76
|
+
" 4. rf compose spec.json -o ./final.mp4",
|
|
77
|
+
"",
|
|
78
|
+
"spec 字段见 `rf compose -h`,封面模板见 `rf cover templates`。",
|
|
79
|
+
"",
|
|
80
|
+
].join("\n"));
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
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
|
+
手机竖拍照片常带旋转标记,直接喂可能是横躺的。喂前先摆正、抽帧确认方向:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
ffmpeg -noautorotate -i in.jpg -vf "transpose=1" up.jpg
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## 提交前自查
|
|
151
|
+
- [ ] 跑过 `rf assets describe`?
|
|
152
|
+
- [ ] 旁白每一句都对得上画面真实动作?(过一遍反虚构铁律)
|
|
153
|
+
- [ ] 给用户看的是**人话故事板**,不是裸 JSON?
|
|
154
|
+
- [ ] 照片朝向已摆正?
|
|
155
|
+
- [ ] 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,10 @@ program.addHelpText("after", [
|
|
|
111
112
|
" rf history list 看之前生成的视频",
|
|
112
113
|
" rf tasks list --status running 看进行中的任务",
|
|
113
114
|
"",
|
|
114
|
-
"
|
|
115
|
-
" rf
|
|
115
|
+
"做 vlog?(用户拍的素材 → 视频,如宠物号)先看赛道剧本:",
|
|
116
|
+
" rf vlog 列出可用赛道(宠物等)",
|
|
117
|
+
" rf vlog pet 打印「宠物第一视角搞笑」完整 SOP",
|
|
118
|
+
" → 拿到 SOP 后照着走:rf assets describe(看素材) → 写旁白 → rf compose(渲染)",
|
|
116
119
|
].join("\n"));
|
|
117
120
|
registerAuth(program);
|
|
118
121
|
registerCreate(program);
|
|
@@ -142,6 +145,7 @@ registerExtract(program);
|
|
|
142
145
|
registerMedia(program);
|
|
143
146
|
registerScript(program);
|
|
144
147
|
registerCompose(program);
|
|
148
|
+
registerVlog(program);
|
|
145
149
|
async function main() {
|
|
146
150
|
if (process.argv.length <= 2) {
|
|
147
151
|
program.outputHelp();
|