reelforge 0.7.1 → 0.9.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/README.md +262 -240
- package/dist/commands/create.js +109 -71
- package/dist/commands/pipelines.js +35 -24
- package/package.json +51 -51
package/README.md
CHANGED
|
@@ -1,240 +1,262 @@
|
|
|
1
|
-
# reelforge
|
|
2
|
-
|
|
3
|
-
> CLI for [ReelForge Studio](https://github.com/puke3615/ReelForge) — every REST API exposed as a command, with `--help` available at every level.
|
|
4
|
-
|
|
5
|
-
## Install
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npm install -g reelforge
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
Or use directly without install:
|
|
12
|
-
|
|
13
|
-
```bash
|
|
14
|
-
npx reelforge <command>
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
After install, two binaries are on your `PATH` — `reelforge` and the short alias `rf`. Both behave identically; the docs use `rf` from here on.
|
|
18
|
-
|
|
19
|
-
```bash
|
|
20
|
-
rf --version # same as `reelforge --version`
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
## Quick start
|
|
24
|
-
|
|
25
|
-
The CLI ships pointing at the hosted instance (`https://reelforge.timor419.com`). Log in once, then call:
|
|
26
|
-
|
|
27
|
-
```bash
|
|
28
|
-
npm install -g reelforge
|
|
29
|
-
rf login # opens browser; headless? rf login <api_key>
|
|
30
|
-
rf whoami # balance + api_keys
|
|
31
|
-
rf create "为什么我们还没找到外星文明?" # auto-saves to ./<title>-<id>.mp4 in cwd
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
That's the whole story — no server to run.
|
|
35
|
-
|
|
36
|
-
### Output behavior
|
|
37
|
-
|
|
38
|
-
| invocation | result |
|
|
39
|
-
|---|---|
|
|
40
|
-
| `rf create "..."` | Saves to `./<sanitized-title>-<task_id_short>.mp4`, prints the path |
|
|
41
|
-
| `rf create "..." -o ./videos/space.mp4` | Saves to that exact path (must include filename, not just a directory) |
|
|
42
|
-
| `rf create "..." --no-download` | Skips local save, prints JSON result with `video_url` |
|
|
43
|
-
| `rf create "..." \| jq .video_url` | When stdout is piped, download is skipped automatically |
|
|
44
|
-
|
|
45
|
-
### Self-hosting
|
|
46
|
-
|
|
47
|
-
If you want to run your own ReelForge Studio (own RelayX key, your own pricing) clone the upstream repo, `pnpm dev`, then point the CLI at it:
|
|
48
|
-
|
|
49
|
-
```bash
|
|
50
|
-
rf --server http://localhost:8501 health
|
|
51
|
-
# or persist:
|
|
52
|
-
export REELFORGE_SERVER=http://localhost:8501
|
|
53
|
-
# or via `rf login <key> --server http://localhost:8501`
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
## Global options
|
|
57
|
-
|
|
58
|
-
| flag | description |
|
|
59
|
-
|---|---|
|
|
60
|
-
| `-s, --server <url>` | ReelForge server URL (overrides `$REELFORGE_SERVER`; default `https://reelforge.timor419.com`) |
|
|
61
|
-
| `-k, --api-key <key>` | API key (overrides `$REELFORGE_API_KEY` and `reelforge login` saved key) |
|
|
62
|
-
| `--json` | Output raw JSON instead of pretty text — pipe-friendly |
|
|
63
|
-
| `--quiet` | Suppress informational messages on stderr |
|
|
64
|
-
| `-v, --version` | Show CLI version |
|
|
65
|
-
| `-h, --help` | Show help (works on every sub-command) |
|
|
66
|
-
|
|
67
|
-
## Command map
|
|
68
|
-
|
|
69
|
-
Run `rf <command> --help` for full details on any of these.
|
|
70
|
-
|
|
71
|
-
### Core capabilities
|
|
72
|
-
|
|
73
|
-
| command | what it does |
|
|
74
|
-
|---|---|
|
|
75
|
-
| `llm chat -p <text>` | Send one prompt to the configured LLM (RelayX gateway by default) |
|
|
76
|
-
| `llm presets` | List built-in RelayX model presets |
|
|
77
|
-
| `tts edge -t <text> -o out.mp3` | Local Edge TTS synthesis (free) |
|
|
78
|
-
| `tts relayx -t <text> -o out.mp3` | RelayX TTS (vox/index-tts-2, 149 built-in voices) |
|
|
79
|
-
| `tts voices [--locale zh]` | List supported Edge TTS voices |
|
|
80
|
-
| `images generate -p <prompt> -m rx-image-flux` | Image generation via RelayX (rx-image-z / rx-image-flux / rx-image-qwen) |
|
|
81
|
-
|
|
82
|
-
### Content / audio / subtitle atomics
|
|
83
|
-
|
|
84
|
-
| command | what it does |
|
|
85
|
-
|---|---|
|
|
86
|
-
| `content scene-plan -t <topic>` | Single LLM call: title + master script + per-scene image prompts (replaces the old narration / split / image-prompts / title trio) |
|
|
87
|
-
| `content scene-plan --script <text-or-@file>` | Same, but the user supplies the script verbatim — LLM only segments and writes image prompts |
|
|
88
|
-
| `audio transcribe -f <file>` / `--url <url>` | RelayX paraformer-v2 ASR with word + segment timestamps |
|
|
89
|
-
| `subtitles split -t <text-or-@file>` | Deterministic tiered-punctuation subtitle line splitter (pure function, zero billing) |
|
|
90
|
-
|
|
91
|
-
### Composition
|
|
92
|
-
|
|
93
|
-
| command | what it does |
|
|
94
|
-
|---|---|
|
|
95
|
-
| `templates list [--size 1080x1920] [--type image]` | List HTML frame templates |
|
|
96
|
-
| `templates preview <keyOrPath> [-o out.png]` | Render a preview from a preset key **or your own local .html file** |
|
|
97
|
-
| `templates show <key> [-o file.html]` | Print or save the source HTML of any preset — copy it as a starting point for a custom template |
|
|
98
|
-
| `frames render -t <keyOrPath> --title ... --text ...` | Render a single composed frame to PNG. `-t` accepts a preset key **or a local .html path** |
|
|
99
|
-
| `compositions concat <v1> <v2> -o out.mp4` | FFmpeg concat (+ optional BGM) |
|
|
100
|
-
| `compositions bgm -i video.mp4 --bgm bgm.mp3 -o out.mp4` | Add background music |
|
|
101
|
-
| `compositions image-to-video -i img.png -a aud.mp3 -o out.mp4` | Build video from image + audio |
|
|
102
|
-
| `compositions overlay -v video.mp4 --overlay overlay.png -o out.mp4` | Overlay PNG on video |
|
|
103
|
-
|
|
104
|
-
### End-to-end pipelines
|
|
105
|
-
|
|
106
|
-
All `pipelines *` commands submit an **async task** and (by default) poll until it finishes with a live progress indicator on stderr. Use `--no-wait` to return immediately with a `task_id`, then `rf tasks wait <id>` later.
|
|
107
|
-
|
|
108
|
-
The standard pipeline is **audio-first**: scene-plan → one-shot TTS → ASR alignment → per-scene image generation → per-subtitle-line frame rendering → ffmpeg mux. One continuous master audio track; image cuts at scene boundaries; subtitle cuts at line boundaries.
|
|
109
|
-
|
|
110
|
-
| command | what it does |
|
|
111
|
-
|---|---|
|
|
112
|
-
| `pipelines standard -t <topic>` (or `--script <text>`) | Audio-first pipeline; `-d/--duration` and `-p/--pace` are the two main knobs |
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
rf
|
|
163
|
-
|
|
164
|
-
#
|
|
165
|
-
rf
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
rf
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
#
|
|
178
|
-
|
|
179
|
-
rf
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
rf
|
|
183
|
-
rf
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
rf
|
|
192
|
-
rf
|
|
193
|
-
rf
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
1
|
+
# reelforge
|
|
2
|
+
|
|
3
|
+
> CLI for [ReelForge Studio](https://github.com/puke3615/ReelForge) — every REST API exposed as a command, with `--help` available at every level.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g reelforge
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or use directly without install:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx reelforge <command>
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
After install, two binaries are on your `PATH` — `reelforge` and the short alias `rf`. Both behave identically; the docs use `rf` from here on.
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
rf --version # same as `reelforge --version`
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick start
|
|
24
|
+
|
|
25
|
+
The CLI ships pointing at the hosted instance (`https://reelforge.timor419.com`). Log in once, then call:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install -g reelforge
|
|
29
|
+
rf login # opens browser; headless? rf login <api_key>
|
|
30
|
+
rf whoami # balance + api_keys
|
|
31
|
+
rf create "为什么我们还没找到外星文明?" # auto-saves to ./<title>-<id>.mp4 in cwd
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
That's the whole story — no server to run.
|
|
35
|
+
|
|
36
|
+
### Output behavior
|
|
37
|
+
|
|
38
|
+
| invocation | result |
|
|
39
|
+
|---|---|
|
|
40
|
+
| `rf create "..."` | Saves to `./<sanitized-title>-<task_id_short>.mp4`, prints the path |
|
|
41
|
+
| `rf create "..." -o ./videos/space.mp4` | Saves to that exact path (must include filename, not just a directory) |
|
|
42
|
+
| `rf create "..." --no-download` | Skips local save, prints JSON result with `video_url` |
|
|
43
|
+
| `rf create "..." \| jq .video_url` | When stdout is piped, download is skipped automatically |
|
|
44
|
+
|
|
45
|
+
### Self-hosting
|
|
46
|
+
|
|
47
|
+
If you want to run your own ReelForge Studio (own RelayX key, your own pricing) clone the upstream repo, `pnpm dev`, then point the CLI at it:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
rf --server http://localhost:8501 health
|
|
51
|
+
# or persist:
|
|
52
|
+
export REELFORGE_SERVER=http://localhost:8501
|
|
53
|
+
# or via `rf login <key> --server http://localhost:8501`
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Global options
|
|
57
|
+
|
|
58
|
+
| flag | description |
|
|
59
|
+
|---|---|
|
|
60
|
+
| `-s, --server <url>` | ReelForge server URL (overrides `$REELFORGE_SERVER`; default `https://reelforge.timor419.com`) |
|
|
61
|
+
| `-k, --api-key <key>` | API key (overrides `$REELFORGE_API_KEY` and `reelforge login` saved key) |
|
|
62
|
+
| `--json` | Output raw JSON instead of pretty text — pipe-friendly |
|
|
63
|
+
| `--quiet` | Suppress informational messages on stderr |
|
|
64
|
+
| `-v, --version` | Show CLI version |
|
|
65
|
+
| `-h, --help` | Show help (works on every sub-command) |
|
|
66
|
+
|
|
67
|
+
## Command map
|
|
68
|
+
|
|
69
|
+
Run `rf <command> --help` for full details on any of these.
|
|
70
|
+
|
|
71
|
+
### Core capabilities
|
|
72
|
+
|
|
73
|
+
| command | what it does |
|
|
74
|
+
|---|---|
|
|
75
|
+
| `llm chat -p <text>` | Send one prompt to the configured LLM (RelayX gateway by default) |
|
|
76
|
+
| `llm presets` | List built-in RelayX model presets |
|
|
77
|
+
| `tts edge -t <text> -o out.mp3` | Local Edge TTS synthesis (free) |
|
|
78
|
+
| `tts relayx -t <text> -o out.mp3` | RelayX TTS (vox/index-tts-2, 149 built-in voices) |
|
|
79
|
+
| `tts voices [--locale zh]` | List supported Edge TTS voices |
|
|
80
|
+
| `images generate -p <prompt> -m rx-image-flux` | Image generation via RelayX (rx-image-z / rx-image-flux / rx-image-qwen) |
|
|
81
|
+
|
|
82
|
+
### Content / audio / subtitle atomics
|
|
83
|
+
|
|
84
|
+
| command | what it does |
|
|
85
|
+
|---|---|
|
|
86
|
+
| `content scene-plan -t <topic>` | Single LLM call: title + master script + per-scene image prompts (replaces the old narration / split / image-prompts / title trio) |
|
|
87
|
+
| `content scene-plan --script <text-or-@file>` | Same, but the user supplies the script verbatim — LLM only segments and writes image prompts |
|
|
88
|
+
| `audio transcribe -f <file>` / `--url <url>` | RelayX paraformer-v2 ASR with word + segment timestamps |
|
|
89
|
+
| `subtitles split -t <text-or-@file>` | Deterministic tiered-punctuation subtitle line splitter (pure function, zero billing) |
|
|
90
|
+
|
|
91
|
+
### Composition
|
|
92
|
+
|
|
93
|
+
| command | what it does |
|
|
94
|
+
|---|---|
|
|
95
|
+
| `templates list [--size 1080x1920] [--type image]` | List HTML frame templates |
|
|
96
|
+
| `templates preview <keyOrPath> [-o out.png]` | Render a preview from a preset key **or your own local .html file** |
|
|
97
|
+
| `templates show <key> [-o file.html]` | Print or save the source HTML of any preset — copy it as a starting point for a custom template |
|
|
98
|
+
| `frames render -t <keyOrPath> --title ... --text ...` | Render a single composed frame to PNG. `-t` accepts a preset key **or a local .html path** |
|
|
99
|
+
| `compositions concat <v1> <v2> -o out.mp4` | FFmpeg concat (+ optional BGM) |
|
|
100
|
+
| `compositions bgm -i video.mp4 --bgm bgm.mp3 -o out.mp4` | Add background music |
|
|
101
|
+
| `compositions image-to-video -i img.png -a aud.mp3 -o out.mp4` | Build video from image + audio |
|
|
102
|
+
| `compositions overlay -v video.mp4 --overlay overlay.png -o out.mp4` | Overlay PNG on video |
|
|
103
|
+
|
|
104
|
+
### End-to-end pipelines
|
|
105
|
+
|
|
106
|
+
All `pipelines *` commands submit an **async task** and (by default) poll until it finishes with a live progress indicator on stderr. Use `--no-wait` to return immediately with a `task_id`, then `rf tasks wait <id>` later.
|
|
107
|
+
|
|
108
|
+
The standard pipeline is **audio-first**: scene-plan → one-shot TTS → ASR alignment → per-scene image generation → per-subtitle-line frame rendering → ffmpeg mux. One continuous master audio track; image cuts at scene boundaries; subtitle cuts at line boundaries.
|
|
109
|
+
|
|
110
|
+
| command | what it does |
|
|
111
|
+
|---|---|
|
|
112
|
+
| `pipelines standard -t <topic>` (or `--script <text>`) | Audio-first pipeline; `-d/--duration` and `-p/--pace` are the two main knobs |
|
|
113
|
+
|
|
114
|
+
#### Composition knobs
|
|
115
|
+
|
|
116
|
+
Three independent axes — mix and match as you like:
|
|
117
|
+
|
|
118
|
+
| flag | values | default | what changes |
|
|
119
|
+
|---|---|---|---|
|
|
120
|
+
| `--motion` | `off` / `lite` / `max` | `lite` | per-scene zoompan + crossfade intensity |
|
|
121
|
+
| `--layout` | `full` / `blur-bg` / `letterbox` | `full` | how the image sits in the canvas |
|
|
122
|
+
| `--subtitle-style` | `plate` / `stroke` / `cinema` | `plate` | subtitle look |
|
|
123
|
+
|
|
124
|
+
**Layout presets**:
|
|
125
|
+
- `full` — image fills the whole 1080×1920 canvas. Generates a 1080×1920 image. High-impact; best for human portraits, landscapes, 9:16-native content.
|
|
126
|
+
- `blur-bg` — image at 1080×1080 centered, top/bottom is a gaussian-blurred copy of the **same image** moving in sync with the foreground. Generates a 1080×1080 image (cheaper + no wasted pixels). Best for charts, screenshots, non-9:16 source content (小红书 / 抖音 style).
|
|
127
|
+
- `letterbox` — image at 1080×1080 centered, top/bottom is a solid matte (CSS color). Generates a 1080×1080 image. Cinematic / calm. Customize the matte with `--layout-matte-color "#1a1a1a"` (default `black`).
|
|
128
|
+
|
|
129
|
+
Examples:
|
|
130
|
+
```bash
|
|
131
|
+
rf create "财经日报" --layout blur-bg # 小红书 / 抖音
|
|
132
|
+
rf create "纪录片片段" --layout letterbox --motion max # 电影感
|
|
133
|
+
rf create "..." --layout letterbox --layout-matte-color "#1a1a1a" # 柔和黑
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Resources
|
|
137
|
+
|
|
138
|
+
| command | what it does |
|
|
139
|
+
|---|---|
|
|
140
|
+
| `bgm list / upload <file> / delete <name>` | Manage background music |
|
|
141
|
+
| `files list / upload <file> / download <path> / delete <path>` | Manage user assets |
|
|
142
|
+
|
|
143
|
+
### System
|
|
144
|
+
|
|
145
|
+
| command | what it does |
|
|
146
|
+
|---|---|
|
|
147
|
+
| `config get` | Read server config (keys masked) |
|
|
148
|
+
| `config set <key> <value>` | Update a dotted-path setting (e.g. `llm.api_key sk-xxx`) |
|
|
149
|
+
| `config patch <file>` | Apply a JSON-merge patch |
|
|
150
|
+
| `tasks list [--status running]` | List recent tasks |
|
|
151
|
+
| `tasks get <id>` / `tasks wait <id>` / `tasks cancel <id>` | Task lifecycle |
|
|
152
|
+
| `history list / get <id> / delete <id>` | Browse / delete completed runs |
|
|
153
|
+
| `health` | Server health + capability check |
|
|
154
|
+
|
|
155
|
+
## Examples
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
# 1. One-click out a video (45s default, AI writes the script)
|
|
159
|
+
rf create "为什么我们还没找到外星文明?"
|
|
160
|
+
|
|
161
|
+
# 2. Longer video with a slower visual rhythm
|
|
162
|
+
rf create "深夜便利店的灯光" -d 90 -p slow
|
|
163
|
+
|
|
164
|
+
# 3. Your own script — no narration-splitting on your side, the pipeline handles it
|
|
165
|
+
rf create --script @./my-script.txt
|
|
166
|
+
rf create --script "雨水缓缓滑落在玻璃窗上,像是无声的泪珠。"
|
|
167
|
+
|
|
168
|
+
# 4. Pick a built-in visual style preset
|
|
169
|
+
rf create "美食教程" --style photorealistic
|
|
170
|
+
|
|
171
|
+
# 5. Pipeline form with explicit output path
|
|
172
|
+
rf pipelines standard \
|
|
173
|
+
--script @./script.txt \
|
|
174
|
+
--frame-template 1080x1920/image_default.html \
|
|
175
|
+
-p normal -o smoke.mp4
|
|
176
|
+
|
|
177
|
+
# 6. Inspect existing tasks & redownload a finished video
|
|
178
|
+
rf tasks list --limit 5
|
|
179
|
+
rf history get <task-id> --download recovered.mp4
|
|
180
|
+
|
|
181
|
+
# 7. Atomics for stand-alone use
|
|
182
|
+
rf content scene-plan -t "雨天的玻璃窗" -d 45 --json | jq .scenes
|
|
183
|
+
rf audio transcribe -f narration.mp3 --json | jq '.words[:5]'
|
|
184
|
+
rf subtitles split -t @./narration.txt --min 10 --hard-max 24
|
|
185
|
+
|
|
186
|
+
# 8. JSON pipe for automation
|
|
187
|
+
rf llm presets --json | jq '.[].defaultModel'
|
|
188
|
+
|
|
189
|
+
# 9. Configure & test LLM (self-hosted)
|
|
190
|
+
rf config set llm.api_key rx-xxxxx # RelayX key (or your own provider key)
|
|
191
|
+
rf config set llm.base_url https://relayx.timor419.com/v1
|
|
192
|
+
rf config set llm.model anthropic/claude-4-7-sonnet
|
|
193
|
+
rf llm chat -p 'one-sentence summary of antifragile'
|
|
194
|
+
|
|
195
|
+
# 10. Use your own HTML template (no PR/release needed)
|
|
196
|
+
# Any --frame-template that points to a local .html file is read and sent
|
|
197
|
+
# inline. Declare size inside the file via
|
|
198
|
+
# <meta name="template:width" content="1080">
|
|
199
|
+
# <meta name="template:height" content="1920">
|
|
200
|
+
# or pass --frame-template-size 1080x1920.
|
|
201
|
+
rf templates show 1080x1920/image_default.html -o my-brand.html # copy a preset
|
|
202
|
+
# ...edit my-brand.html to suit your style...
|
|
203
|
+
rf templates preview ./my-brand.html --title "Hello" -o preview.png
|
|
204
|
+
rf frames render -t ./my-brand.html --values '{"author":"Alice"}' -o frame.png
|
|
205
|
+
rf pipelines standard -t "宠物" --frame-template ./my-brand.html -o final.mp4
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Custom HTML templates
|
|
209
|
+
|
|
210
|
+
Easiest way to start: grab a preset as a reference.
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
rf templates list # see all keys
|
|
214
|
+
rf templates show 1080x1920/static_default.html # print to stdout
|
|
215
|
+
rf templates show 1080x1920/image_default.html -o my-brand.html # save and edit
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
`{{title}}`, `{{text}}`, `{{image}}`, `{{index}}`, `{{total}}` are reserved built-ins auto-injected by the pipeline; everything else uses the `{{name:type=default}}` DSL (`type` ∈ `text|number|color|bool`). Pass extras through `--values '{"author":"Alice"}'` (or `template_params` on the pipeline API).
|
|
219
|
+
|
|
220
|
+
- `{{index}}` — current scene number, 1-based
|
|
221
|
+
- `{{total}}` — scene count the LLM actually produced (use this for "scene N of M" badges; don't hardcode in `template_params`, the scene count is decided at runtime)
|
|
222
|
+
|
|
223
|
+
#### Template type — does the pipeline generate an AI image per scene?
|
|
224
|
+
|
|
225
|
+
When you ship an inline template through `rf create` / `rf pipelines standard`, ReelForge needs to know whether each scene should kick off RelayX image generation. Resolution priority (high → low):
|
|
226
|
+
|
|
227
|
+
1. Explicit flag — `--frame-template-type image|static|asset` (or `frame_template_type` in the API body).
|
|
228
|
+
2. Inside the HTML — `<meta name="template:type" content="image">` (or `static` / `asset`).
|
|
229
|
+
3. **Default: `image`** — best practice for zero-config users. If your template doesn't reference scene imagery (pure-text card, etc.), declare `static` explicitly to skip image generation and its cost.
|
|
230
|
+
|
|
231
|
+
The placeholder `{{image}}` no longer doubles as a type signal — declare type explicitly.
|
|
232
|
+
|
|
233
|
+
Limits and safety:
|
|
234
|
+
|
|
235
|
+
- Max 2 MB per inline HTML.
|
|
236
|
+
- The render sandbox blocks `file://`, loopback / private / link-local IPs, CGNAT range, cloud-metadata, and `*.local` / `*.internal` hostnames. So your template can only reference public `https`/`http` resources or `data:` URIs.
|
|
237
|
+
- If the CLI is talking to a hosted server, local-path `--image` won't reach the server; either upload to `rf files upload` first or use an HTTPS URL / data: URI.
|
|
238
|
+
|
|
239
|
+
#### API field reference
|
|
240
|
+
|
|
241
|
+
| endpoint | inline HTML field | size field | type field |
|
|
242
|
+
|---|---|---|---|
|
|
243
|
+
| `POST /api/v1/frames/render` | `template_html` | `size` | — (n/a, no image generation) |
|
|
244
|
+
| `POST /api/v1/templates/preview` | `template_html` | `size` | — |
|
|
245
|
+
| `POST /api/v1/pipelines/standard` | `frame_template_inline` | `frame_template_size` | `frame_template_type` |
|
|
246
|
+
|
|
247
|
+
The pipeline endpoint uses the `frame_template_*` prefix because it already has a `frame_template` field (preset key). The single-frame endpoints use the shorter `template_html` because they don't.
|
|
248
|
+
|
|
249
|
+
## Tip — getting unstuck
|
|
250
|
+
|
|
251
|
+
Every level has `--help`:
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
rf --help # top-level overview
|
|
255
|
+
rf pipelines --help # list of pipelines
|
|
256
|
+
rf pipelines standard --help # full option reference
|
|
257
|
+
rf tts edge --help # one specific command
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## License
|
|
261
|
+
|
|
262
|
+
Apache-2.0
|
package/dist/commands/create.js
CHANGED
|
@@ -15,27 +15,8 @@ const IMAGE_UNITS = 3;
|
|
|
15
15
|
const CHARS_PER_SEC_ZH = 5;
|
|
16
16
|
const TARGET_SEC_PER_SCENE = 8;
|
|
17
17
|
function estimateUnits(body) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if (body.frame_template_type) {
|
|
21
|
-
tplType = body.frame_template_type;
|
|
22
|
-
}
|
|
23
|
-
else {
|
|
24
|
-
const m = body.frame_template_inline.match(/<meta[^>]+name=["']template:type["'][^>]+content=["']([a-z]+)["']/i);
|
|
25
|
-
const v = m?.[1].toLowerCase();
|
|
26
|
-
tplType = v === "static" || v === "asset" || v === "image" ? v : "image";
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
else {
|
|
30
|
-
const tplKey = body.frame_template || "1080x1920/image_default.html";
|
|
31
|
-
const tplBase = (tplKey.split("/").pop() || "").toLowerCase();
|
|
32
|
-
tplType = tplBase.startsWith("static_")
|
|
33
|
-
? "static"
|
|
34
|
-
: tplBase.startsWith("asset_")
|
|
35
|
-
? "asset"
|
|
36
|
-
: "image";
|
|
37
|
-
}
|
|
38
|
-
// Estimated scene count: from script length (fixed) or from duration (generate).
|
|
18
|
+
// The standard pipeline now always generates one AI image per scene (the
|
|
19
|
+
// old static_/asset_ template-type carveouts are gone).
|
|
39
20
|
let estimatedScenes;
|
|
40
21
|
if (body.script) {
|
|
41
22
|
const estSec = body.script.length / CHARS_PER_SEC_ZH;
|
|
@@ -45,19 +26,9 @@ function estimateUnits(body) {
|
|
|
45
26
|
const dur = body.duration ?? 45;
|
|
46
27
|
estimatedScenes = Math.max(2, Math.round(dur / TARGET_SEC_PER_SCENE));
|
|
47
28
|
}
|
|
48
|
-
|
|
49
|
-
return PLAN_UNITS + TTS_UNITS + ASR_UNITS + imageUnits;
|
|
29
|
+
return PLAN_UNITS + TTS_UNITS + ASR_UNITS + estimatedScenes * IMAGE_UNITS;
|
|
50
30
|
}
|
|
51
31
|
// ── Helpers ─────────────────────────────────────────────────────
|
|
52
|
-
function looksLikeLocalHtmlPath(value) {
|
|
53
|
-
if (/^[.~]|^\//.test(value))
|
|
54
|
-
return true;
|
|
55
|
-
if (value.includes("\\"))
|
|
56
|
-
return true;
|
|
57
|
-
if (value.endsWith(".html") && fsSync.existsSync(value))
|
|
58
|
-
return true;
|
|
59
|
-
return false;
|
|
60
|
-
}
|
|
61
32
|
/** `@file` prefix → load file contents; raw text → return as-is. */
|
|
62
33
|
async function resolveTextOrFile(input) {
|
|
63
34
|
if (input.startsWith("@")) {
|
|
@@ -190,24 +161,34 @@ function optsToBody(opts) {
|
|
|
190
161
|
out.tts_speed = opts.ttsSpeed;
|
|
191
162
|
if (opts.videoFps !== undefined)
|
|
192
163
|
out.video_fps = opts.videoFps;
|
|
193
|
-
if (opts.
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
if (opts.
|
|
210
|
-
|
|
164
|
+
if (opts.motion !== undefined)
|
|
165
|
+
out.motion = opts.motion;
|
|
166
|
+
if (opts.layout !== undefined)
|
|
167
|
+
out.layout = opts.layout;
|
|
168
|
+
if (opts.layoutMatteColor !== undefined)
|
|
169
|
+
out.layout_matte_color = opts.layoutMatteColor;
|
|
170
|
+
if (opts.subtitleStyle !== undefined)
|
|
171
|
+
out.subtitle_style = opts.subtitleStyle;
|
|
172
|
+
if (opts.subtitleColor !== undefined)
|
|
173
|
+
out.subtitle_color = opts.subtitleColor;
|
|
174
|
+
if (opts.subtitleBackground !== undefined)
|
|
175
|
+
out.subtitle_background = opts.subtitleBackground;
|
|
176
|
+
// Brand: collect flat --brand-* flags into a nested brand object only if
|
|
177
|
+
// at least one is set. The pipeline-side field-level merge with
|
|
178
|
+
// config.video.default_brand will fill in unset fields.
|
|
179
|
+
const brand = {};
|
|
180
|
+
if (opts.brandHandle !== undefined)
|
|
181
|
+
brand.handle = opts.brandHandle;
|
|
182
|
+
if (opts.brandSlogan !== undefined)
|
|
183
|
+
brand.slogan = opts.brandSlogan;
|
|
184
|
+
if (opts.brandLogo !== undefined)
|
|
185
|
+
brand.logo_url = opts.brandLogo;
|
|
186
|
+
if (opts.brandPosition !== undefined)
|
|
187
|
+
brand.position = opts.brandPosition;
|
|
188
|
+
if (opts.brandColor !== undefined)
|
|
189
|
+
brand.color = opts.brandColor;
|
|
190
|
+
if (Object.keys(brand).length > 0)
|
|
191
|
+
out.brand = brand;
|
|
211
192
|
if (opts.subtitleMinChars !== undefined)
|
|
212
193
|
out.subtitle_min_chars = opts.subtitleMinChars;
|
|
213
194
|
if (opts.subtitleHardMax !== undefined)
|
|
@@ -334,13 +315,22 @@ export function registerCreate(program) {
|
|
|
334
315
|
.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))
|
|
335
316
|
.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.")
|
|
336
317
|
// --- Visual ---
|
|
337
|
-
.option("--frame-template <keyOrPath>", "HTML frame template: preset key (e.g. 1080x1920/image_default.html) OR path to a local .html (auto-sent inline)")
|
|
338
|
-
.option("--frame-template-size <wxh>", "size for inline HTML when the file lacks <meta template:width|height>, e.g. 1080x1920")
|
|
339
|
-
.option("--frame-template-type <type>", "inline template type: image (default) | static | asset. Controls whether AI image generation runs per scene.")
|
|
340
318
|
.option("--image-model <id>", "RelayX image model (rx-image-z | rx-image-flux | rx-image-qwen | rx-image-qwen-edit). Auto-switches to rx-image-qwen-edit when --character-ref is set.")
|
|
341
319
|
.option("--prompt-prefix <text>", "raw style prefix prepended to every image prompt (overrides --style)")
|
|
342
320
|
.option("--style <preset>", "image style preset — shortcut for --prompt-prefix; see 'Style presets' below")
|
|
343
321
|
.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 rx-image-qwen-edit.")
|
|
322
|
+
.option("--motion <preset>", "per-scene image animation intensity. See 'Motion presets' below. Default: lite.")
|
|
323
|
+
.option("--layout <preset>", "image layout within canvas: full (default) | blur-bg | letterbox. See 'Layout presets' below.")
|
|
324
|
+
.option("--layout-matte-color <css>", "letterbox matte color (CSS string, e.g. 'black', '#1a1a1a', '#2d3748'). Ignored unless --layout letterbox. Default: black.")
|
|
325
|
+
.option("--subtitle-style <preset>", "subtitle visual style. See 'Subtitle styles' below. Default: plate.")
|
|
326
|
+
.option("--subtitle-color <css>", "override subtitle text color, e.g. '#ffeb3b'. Omit for preset default.")
|
|
327
|
+
.option("--subtitle-background <css>", "override plate-preset background, e.g. 'rgba(20,30,80,0.75)'. Other presets ignore.")
|
|
328
|
+
// --- Brand chrome (corner @handle / slogan / logo) ---
|
|
329
|
+
.option("--brand-handle <text>", "creator handle shown in corner, e.g. '@大灰狼'")
|
|
330
|
+
.option("--brand-slogan <text>", "creator slogan/tagline shown under handle")
|
|
331
|
+
.option("--brand-logo <urlOrPath>", "logo/avatar URL or local file path (PNG/JPG/WebP)")
|
|
332
|
+
.option("--brand-position <pos>", "where to render brand chrome: top-left | top-right | bottom-left | bottom-right")
|
|
333
|
+
.option("--brand-color <css>", "brand text color, e.g. '#ffffff' (default)")
|
|
344
334
|
// --- Audio (TTS) ---
|
|
345
335
|
.option("--voice-id <id>", "RelayX TTS voice id (default 专业解说); see `rf tts voices`")
|
|
346
336
|
.option("--tts-speed <n>", "speech speed 0.5..2 (default 1.0)", parseFloat)
|
|
@@ -353,14 +343,6 @@ export function registerCreate(program) {
|
|
|
353
343
|
.option("--subtitle-hard-max <N>", "subtitle line absolute max chars (default 24)", (v) => parseInt(v, 10))
|
|
354
344
|
// --- Output / extra ---
|
|
355
345
|
.option("--video-fps <n>", "output video fps (default 30)", (v) => parseInt(v, 10))
|
|
356
|
-
.option("--template-params <json>", "extra template placeholders as JSON string", (v) => {
|
|
357
|
-
try {
|
|
358
|
-
return JSON.parse(v);
|
|
359
|
-
}
|
|
360
|
-
catch {
|
|
361
|
-
throw new Error(`--template-params: invalid JSON: ${v}`);
|
|
362
|
-
}
|
|
363
|
-
})
|
|
364
346
|
// --- Runtime / workflow ---
|
|
365
347
|
.option("--recipe <file>", "load defaults from a JSON recipe file (CLI flags still override)")
|
|
366
348
|
.option("--redo", "replay last successful create from ~/.reelforge/last-create.json")
|
|
@@ -382,9 +364,41 @@ export function registerCreate(program) {
|
|
|
382
364
|
" fast split long semantic chunks into multiple shots for variety",
|
|
383
365
|
"",
|
|
384
366
|
"Defaults:",
|
|
385
|
-
" duration=45s · pace=normal ·
|
|
367
|
+
" duration=45s · pace=normal · motion=lite · layout=full · subtitle-style=plate · tts-speed=1.0",
|
|
386
368
|
"",
|
|
387
|
-
"
|
|
369
|
+
"Motion presets (--motion <preset>) — per-scene image animation intensity:",
|
|
370
|
+
" off no motion, hard cuts between scenes — PPT / slideshow mode",
|
|
371
|
+
" lite 6% zoom + 0.3s crossfade, 4 sub-anims (default; safe + tasteful)",
|
|
372
|
+
" max 20% zoom/pan + 0.5s crossfade, 10 sub-anims (cinematic, more dramatic)",
|
|
373
|
+
" · sub-animations are Fisher-Yates shuffled per task_id, so every video",
|
|
374
|
+
" cycles a different order — no two videos feel identical.",
|
|
375
|
+
"",
|
|
376
|
+
"Layout presets (--layout <preset>) — how the AI image sits in the 1080×1920 canvas:",
|
|
377
|
+
" full image fills the whole canvas (default; high-impact, attention-grabbing).",
|
|
378
|
+
" best for human portraits, landscapes, 9:16-native content.",
|
|
379
|
+
" blur-bg image at 1080×1080 centered, top/bottom is a gaussian-blurred copy",
|
|
380
|
+
" of the same image — moves in sync with the foreground (小红书/抖音 style).",
|
|
381
|
+
" best for charts, screenshots, infographics, non-9:16 source content.",
|
|
382
|
+
" letterbox image at 1080×1080 centered, top/bottom is a solid matte (cinematic).",
|
|
383
|
+
" use --layout-matte-color to change (default 'black'; try '#1a1a1a' for less harsh).",
|
|
384
|
+
" · The image generator is asked for the actual on-screen size (1080×1920 for full,",
|
|
385
|
+
" 1080×1080 for blur-bg/letterbox), so you don't pay for pixels that get cropped.",
|
|
386
|
+
"",
|
|
387
|
+
"Subtitle styles (--subtitle-style <preset>):",
|
|
388
|
+
" plate semi-transparent black plate + white text (CapCut default; safest readability)",
|
|
389
|
+
" stroke bold white text with black stroke + shadow, no plate (抖音网红风)",
|
|
390
|
+
" cinema bottom black gradient backdrop + lighter text (film / documentary look)",
|
|
391
|
+
" · use --subtitle-color / --subtitle-background to override the preset's colors",
|
|
392
|
+
" e.g. --subtitle-color '#ffeb3b' --subtitle-background 'rgba(20,30,80,0.75)'",
|
|
393
|
+
"",
|
|
394
|
+
"Brand chrome (--brand-* flags or config.video.default_brand):",
|
|
395
|
+
" Adds a constant @handle / slogan / logo block in a frame corner.",
|
|
396
|
+
" --brand-position bottom-right --brand-handle '@大灰狼' --brand-slogan '财经科普'",
|
|
397
|
+
" --brand-logo accepts URL / data: URI / local png/jpg/webp path.",
|
|
398
|
+
" All fields optional; missing handle/slogan/logo are individually hidden.",
|
|
399
|
+
" Per-request flags merge over config defaults field by field.",
|
|
400
|
+
"",
|
|
401
|
+
"Image style presets (--style <preset>) — quick shortcut for --prompt-prefix:",
|
|
388
402
|
formatStylePresetsList(),
|
|
389
403
|
" · Pass --prompt-prefix to override (raw string always wins).",
|
|
390
404
|
" · Omit both to use the server's configured default style (if any).",
|
|
@@ -406,16 +420,23 @@ export function registerCreate(program) {
|
|
|
406
420
|
" rf create --script @./script.txt",
|
|
407
421
|
' rf create --script "整段文案文本..."',
|
|
408
422
|
"",
|
|
409
|
-
" #
|
|
410
|
-
" rf create '...' --frame-template ./my-brand.html",
|
|
411
|
-
"",
|
|
412
|
-
" # Pick a built-in style preset",
|
|
423
|
+
" # Pick a built-in image style preset",
|
|
413
424
|
' rf create "..." --style cinematic',
|
|
414
425
|
"",
|
|
415
426
|
" # Cross-scene character consistency (auto-switches image model)",
|
|
416
427
|
' rf create "主角小女孩的一天" --character-ref ./hero.png',
|
|
417
428
|
' rf create "..." --character-ref https://example.com/hero.png',
|
|
418
429
|
"",
|
|
430
|
+
" # Motion + subtitle style combos",
|
|
431
|
+
' rf create "..." --motion max --subtitle-style stroke # 抖音网红风',
|
|
432
|
+
' rf create "..." --motion lite --subtitle-style cinema # 文艺纪录片',
|
|
433
|
+
' rf create "..." --motion off # PPT 模式(旧行为)',
|
|
434
|
+
"",
|
|
435
|
+
" # Layout combos",
|
|
436
|
+
' rf create "财经日报" --layout blur-bg # 小红书 / 抖音 风(图表内容首选)',
|
|
437
|
+
' rf create "纪录片片段" --layout letterbox --motion max # 电影感',
|
|
438
|
+
' rf create "..." --layout letterbox --layout-matte-color "#1a1a1a" # 柔和黑',
|
|
439
|
+
"",
|
|
419
440
|
" # Recipe + replay last",
|
|
420
441
|
" rf create --recipe ./space.recipe.json",
|
|
421
442
|
" rf create --redo # replay last successful create",
|
|
@@ -444,6 +465,18 @@ export function registerCreate(program) {
|
|
|
444
465
|
if (opts.pace && !["slow", "normal", "fast"].includes(opts.pace)) {
|
|
445
466
|
throw new Error(`--pace must be one of slow|normal|fast (got: ${opts.pace})`);
|
|
446
467
|
}
|
|
468
|
+
if (opts.motion && !["off", "lite", "max"].includes(opts.motion)) {
|
|
469
|
+
throw new Error(`--motion must be one of off|lite|max (got: ${opts.motion})`);
|
|
470
|
+
}
|
|
471
|
+
if (opts.layout && !["full", "blur-bg", "letterbox"].includes(opts.layout)) {
|
|
472
|
+
throw new Error(`--layout must be one of full|blur-bg|letterbox (got: ${opts.layout})`);
|
|
473
|
+
}
|
|
474
|
+
if (opts.subtitleStyle && !["plate", "stroke", "cinema"].includes(opts.subtitleStyle)) {
|
|
475
|
+
throw new Error(`--subtitle-style must be one of plate|stroke|cinema (got: ${opts.subtitleStyle})`);
|
|
476
|
+
}
|
|
477
|
+
if (opts.brandPosition && !["top-left", "top-right", "bottom-left", "bottom-right"].includes(opts.brandPosition)) {
|
|
478
|
+
throw new Error(`--brand-position must be one of top-left|top-right|bottom-left|bottom-right (got: ${opts.brandPosition})`);
|
|
479
|
+
}
|
|
447
480
|
// 1. Layer defaults: --redo → --recipe → CLI opts → positional topic
|
|
448
481
|
let body = {};
|
|
449
482
|
if (opts.redo) {
|
|
@@ -484,6 +517,14 @@ export function registerCreate(program) {
|
|
|
484
517
|
body.character_ref = resolvedChar;
|
|
485
518
|
else
|
|
486
519
|
delete body.character_ref;
|
|
520
|
+
// Brand logo: same URL / data: / local-file rules as character ref.
|
|
521
|
+
if (body.brand?.logo_url) {
|
|
522
|
+
const resolvedLogo = await resolveRefImage(body.brand.logo_url, "--brand-logo");
|
|
523
|
+
if (resolvedLogo !== undefined)
|
|
524
|
+
body.brand.logo_url = resolvedLogo;
|
|
525
|
+
else
|
|
526
|
+
delete body.brand.logo_url;
|
|
527
|
+
}
|
|
487
528
|
// Validate content mode
|
|
488
529
|
const hasTopic = typeof body.topic === "string" && body.topic.trim().length > 0;
|
|
489
530
|
const hasScript = typeof body.script === "string" && body.script.trim().length > 0;
|
|
@@ -493,11 +534,8 @@ export function registerCreate(program) {
|
|
|
493
534
|
if (hasTopic && hasScript) {
|
|
494
535
|
throw new Error("--topic and --script are mutually exclusive (pick one mode).");
|
|
495
536
|
}
|
|
496
|
-
// 3. Final body
|
|
537
|
+
// 3. Final body
|
|
497
538
|
const finalBody = { ...body };
|
|
498
|
-
if (finalBody.frame_template_inline && finalBody.frame_template) {
|
|
499
|
-
delete finalBody.frame_template;
|
|
500
|
-
}
|
|
501
539
|
// 4. Estimate cost
|
|
502
540
|
const estimate = estimateUnits(finalBody);
|
|
503
541
|
if (opts.dryRun) {
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
|
-
import fsSync from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
2
|
import { post } from "../client.js";
|
|
5
3
|
import { waitForTask } from "../utils/task-waiter.js";
|
|
6
4
|
import { downloadTo } from "../utils/download.js";
|
|
@@ -47,11 +45,13 @@ export function registerPipelines(program) {
|
|
|
47
45
|
.option("--script <text>", "your own master script text (mode=fixed). Use @file to read from disk.")
|
|
48
46
|
.option("-d, --duration <sec>", "target video duration in seconds (generate mode; default 45)", (v) => parseInt(v, 10))
|
|
49
47
|
.option("-p, --pace <pace>", "visual rhythm hint: slow | normal | fast (default normal)")
|
|
50
|
-
.option("--
|
|
51
|
-
.option("--
|
|
52
|
-
.option("--
|
|
53
|
-
.option("--
|
|
48
|
+
.option("--motion <preset>", "per-scene image animation: off | lite (default) | max")
|
|
49
|
+
.option("--layout <preset>", "image layout: full (default) | blur-bg | letterbox. See below.")
|
|
50
|
+
.option("--layout-matte-color <css>", "letterbox matte color (CSS). Ignored unless --layout letterbox. Default: black.")
|
|
51
|
+
.option("--subtitle-style <preset>", "subtitle visual style: plate (default) | stroke | cinema")
|
|
52
|
+
.option("--image-model <id>", "RelayX image model (rx-image-z | rx-image-flux | rx-image-qwen | rx-image-qwen-edit)")
|
|
54
53
|
.option("--prompt-prefix <text>", "style prefix prepended to every image prompt")
|
|
54
|
+
.option("--character-ref <urlOrPath>", "main character ref for cross-scene identity lock")
|
|
55
55
|
.option("--voice-id <id>", "RelayX TTS voice id (default 专业解说); see `rf tts voices`")
|
|
56
56
|
.option("--tts-speed <n>", "speech speed (0.5..2; default 1.0)", parseFloat)
|
|
57
57
|
.option("--video-fps <n>", "output video fps (default 30)", (v) => parseInt(v, 10))
|
|
@@ -64,11 +64,24 @@ export function registerPipelines(program) {
|
|
|
64
64
|
" fixed You supply the script. --script <text-or-@file>",
|
|
65
65
|
"",
|
|
66
66
|
"Pace (LLM visual rhythm hint): slow | normal | fast",
|
|
67
|
+
"Motion (per-scene animation): off | lite | max",
|
|
68
|
+
"Subtitle style: plate | stroke | cinema",
|
|
69
|
+
"",
|
|
70
|
+
"Layout — how the AI image sits in the 1080×1920 canvas:",
|
|
71
|
+
" full image fills the whole canvas (default; punchy, 9:16-native content).",
|
|
72
|
+
" blur-bg image at 1080×1080 centered + same image scaled/blurred as background",
|
|
73
|
+
" (小红书 / 抖音 style; best for charts, screenshots, non-9:16 source).",
|
|
74
|
+
" letterbox image at 1080×1080 centered + solid matte top/bottom (cinematic).",
|
|
75
|
+
" tweak with --layout-matte-color (default 'black').",
|
|
76
|
+
" · Image is generated at the actual on-screen size, so you don't pay for cropped pixels.",
|
|
67
77
|
"",
|
|
68
78
|
"Examples:",
|
|
69
79
|
" rf pipelines standard -t 'why we explore space' -d 60 -o space.mp4",
|
|
70
|
-
" rf pipelines standard --script @script.txt -p slow -o out.mp4",
|
|
71
|
-
" rf pipelines standard -t '
|
|
80
|
+
" rf pipelines standard --script @script.txt -p slow --motion max -o out.mp4",
|
|
81
|
+
" rf pipelines standard -t '财经日报' --layout blur-bg --subtitle-style plate -o out.mp4",
|
|
82
|
+
" rf pipelines standard -t '纪录片' --layout letterbox --motion max -o film.mp4",
|
|
83
|
+
"",
|
|
84
|
+
"Tip: `rf create` is a more ergonomic wrapper around the same endpoint.",
|
|
72
85
|
].join("\n"))).action(async (opts) => {
|
|
73
86
|
const hasTopic = typeof opts.topic === "string" && opts.topic.length > 0;
|
|
74
87
|
const hasScript = typeof opts.script === "string" && opts.script.length > 0;
|
|
@@ -81,24 +94,33 @@ export function registerPipelines(program) {
|
|
|
81
94
|
if (opts.pace && !["slow", "normal", "fast"].includes(opts.pace)) {
|
|
82
95
|
throw new Error(`--pace must be one of slow|normal|fast (got: ${opts.pace})`);
|
|
83
96
|
}
|
|
97
|
+
if (opts.motion && !["off", "lite", "max"].includes(opts.motion)) {
|
|
98
|
+
throw new Error(`--motion must be one of off|lite|max (got: ${opts.motion})`);
|
|
99
|
+
}
|
|
100
|
+
if (opts.layout && !["full", "blur-bg", "letterbox"].includes(opts.layout)) {
|
|
101
|
+
throw new Error(`--layout must be one of full|blur-bg|letterbox (got: ${opts.layout})`);
|
|
102
|
+
}
|
|
103
|
+
if (opts.subtitleStyle && !["plate", "stroke", "cinema"].includes(opts.subtitleStyle)) {
|
|
104
|
+
throw new Error(`--subtitle-style must be one of plate|stroke|cinema (got: ${opts.subtitleStyle})`);
|
|
105
|
+
}
|
|
84
106
|
let topic = opts.topic;
|
|
85
107
|
let script = opts.script;
|
|
86
108
|
if (topic?.startsWith("@"))
|
|
87
109
|
topic = await fs.readFile(topic.slice(1), "utf-8");
|
|
88
110
|
if (script?.startsWith("@"))
|
|
89
111
|
script = await fs.readFile(script.slice(1), "utf-8");
|
|
90
|
-
const tpl = opts.frameTemplate
|
|
91
|
-
? resolveTemplateArg(opts.frameTemplate, opts.frameTemplateSize)
|
|
92
|
-
: {};
|
|
93
112
|
await submitAndMaybeWait("/api/v1/pipelines/standard", {
|
|
94
113
|
topic,
|
|
95
114
|
script,
|
|
96
115
|
duration: opts.duration,
|
|
97
116
|
pace: opts.pace,
|
|
98
|
-
|
|
99
|
-
|
|
117
|
+
motion: opts.motion,
|
|
118
|
+
layout: opts.layout,
|
|
119
|
+
layout_matte_color: opts.layoutMatteColor,
|
|
120
|
+
subtitle_style: opts.subtitleStyle,
|
|
100
121
|
image_model: opts.imageModel,
|
|
101
122
|
prompt_prefix: opts.promptPrefix,
|
|
123
|
+
character_ref: opts.characterRef,
|
|
102
124
|
voice_id: opts.voiceId,
|
|
103
125
|
tts_speed: opts.ttsSpeed,
|
|
104
126
|
video_fps: opts.videoFps,
|
|
@@ -107,14 +129,3 @@ export function registerPipelines(program) {
|
|
|
107
129
|
}, { wait: opts.wait, output: opts.output, pollMs: opts.pollMs, timeoutMs: opts.timeoutMs });
|
|
108
130
|
});
|
|
109
131
|
}
|
|
110
|
-
function resolveTemplateArg(value, size) {
|
|
111
|
-
if (/^[.~]|^\//.test(value) || value.includes("\\") || (value.endsWith(".html") && fsSync.existsSync(value))) {
|
|
112
|
-
const abs = path.resolve(value);
|
|
113
|
-
if (!fsSync.existsSync(abs)) {
|
|
114
|
-
throw new Error(`Local template not found: ${abs}`);
|
|
115
|
-
}
|
|
116
|
-
const html = fsSync.readFileSync(abs, "utf-8");
|
|
117
|
-
return size ? { frame_template_inline: html, frame_template_size: size } : { frame_template_inline: html };
|
|
118
|
-
}
|
|
119
|
-
return { frame_template: value };
|
|
120
|
-
}
|
package/package.json
CHANGED
|
@@ -1,51 +1,51 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "reelforge",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "CLI for ReelForge Studio — AI video engine. Installs as both `reelforge` and the short alias `rf`. Every REST API exposed as a command, with --help on every level.",
|
|
5
|
-
"license": "Apache-2.0",
|
|
6
|
-
"type": "module",
|
|
7
|
-
"bin": {
|
|
8
|
-
"reelforge": "./bin/reelforge.js",
|
|
9
|
-
"rf": "./bin/reelforge.js"
|
|
10
|
-
},
|
|
11
|
-
"files": [
|
|
12
|
-
"bin",
|
|
13
|
-
"dist",
|
|
14
|
-
"README.md"
|
|
15
|
-
],
|
|
16
|
-
"engines": {
|
|
17
|
-
"node": ">=18.17"
|
|
18
|
-
},
|
|
19
|
-
"scripts": {
|
|
20
|
-
"build": "tsc -p tsconfig.json",
|
|
21
|
-
"dev": "tsc -p tsconfig.json --watch",
|
|
22
|
-
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
23
|
-
"clean": "rimraf dist",
|
|
24
|
-
"prepublishOnly": "npm run clean && npm run build"
|
|
25
|
-
},
|
|
26
|
-
"dependencies": {
|
|
27
|
-
"commander": "^12.1.0",
|
|
28
|
-
"kleur": "^4.1.5"
|
|
29
|
-
},
|
|
30
|
-
"devDependencies": {
|
|
31
|
-
"@types/node": "^20.14.0",
|
|
32
|
-
"rimraf": "^6.0.1",
|
|
33
|
-
"typescript": "^5.5.0"
|
|
34
|
-
},
|
|
35
|
-
"keywords": [
|
|
36
|
-
"reelforge",
|
|
37
|
-
"ai-video",
|
|
38
|
-
"relayx",
|
|
39
|
-
"tts",
|
|
40
|
-
"edge-tts",
|
|
41
|
-
"ffmpeg",
|
|
42
|
-
"playwright",
|
|
43
|
-
"cli"
|
|
44
|
-
],
|
|
45
|
-
"repository": {
|
|
46
|
-
"type": "git",
|
|
47
|
-
"url": "https://github.com/puke3615/ReelForge.git",
|
|
48
|
-
"directory": "cli"
|
|
49
|
-
},
|
|
50
|
-
"homepage": "https://github.com/puke3615/ReelForge"
|
|
51
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "reelforge",
|
|
3
|
+
"version": "0.9.0",
|
|
4
|
+
"description": "CLI for ReelForge Studio — AI video engine. Installs as both `reelforge` and the short alias `rf`. Every REST API exposed as a command, with --help on every level.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"reelforge": "./bin/reelforge.js",
|
|
9
|
+
"rf": "./bin/reelforge.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"bin",
|
|
13
|
+
"dist",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=18.17"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc -p tsconfig.json",
|
|
21
|
+
"dev": "tsc -p tsconfig.json --watch",
|
|
22
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
23
|
+
"clean": "rimraf dist",
|
|
24
|
+
"prepublishOnly": "npm run clean && npm run build"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"commander": "^12.1.0",
|
|
28
|
+
"kleur": "^4.1.5"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^20.14.0",
|
|
32
|
+
"rimraf": "^6.0.1",
|
|
33
|
+
"typescript": "^5.5.0"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"reelforge",
|
|
37
|
+
"ai-video",
|
|
38
|
+
"relayx",
|
|
39
|
+
"tts",
|
|
40
|
+
"edge-tts",
|
|
41
|
+
"ffmpeg",
|
|
42
|
+
"playwright",
|
|
43
|
+
"cli"
|
|
44
|
+
],
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "https://github.com/puke3615/ReelForge.git",
|
|
48
|
+
"directory": "cli"
|
|
49
|
+
},
|
|
50
|
+
"homepage": "https://github.com/puke3615/ReelForge"
|
|
51
|
+
}
|