reelforge 0.6.0 → 0.7.1
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 +240 -237
- package/dist/commands/create.js +47 -1
- package/dist/commands/images.js +50 -4
- package/package.json +51 -51
package/README.md
CHANGED
|
@@ -1,237 +1,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
|
-
### Resources
|
|
115
|
-
|
|
116
|
-
| command | what it does |
|
|
117
|
-
|---|---|
|
|
118
|
-
| `bgm list / upload <file> / delete <name>` | Manage background music |
|
|
119
|
-
| `files list / upload <file> / download <path> / delete <path>` | Manage user assets |
|
|
120
|
-
|
|
121
|
-
### System
|
|
122
|
-
|
|
123
|
-
| command | what it does |
|
|
124
|
-
|---|---|
|
|
125
|
-
| `config get` | Read server config (keys masked) |
|
|
126
|
-
| `config set <key> <value>` | Update a dotted-path setting (e.g. `llm.api_key sk-xxx`) |
|
|
127
|
-
| `config patch <file>` | Apply a JSON-merge patch |
|
|
128
|
-
| `tasks list [--status running]` | List recent tasks |
|
|
129
|
-
| `tasks get <id>` / `tasks wait <id>` / `tasks cancel <id>` | Task lifecycle |
|
|
130
|
-
| `history list / get <id> / delete <id>` | Browse / delete completed runs |
|
|
131
|
-
| `health` | Server health + capability check |
|
|
132
|
-
|
|
133
|
-
## Examples
|
|
134
|
-
|
|
135
|
-
```bash
|
|
136
|
-
# 1. One-click out a video (45s default, AI writes the script)
|
|
137
|
-
rf create "为什么我们还没找到外星文明?"
|
|
138
|
-
|
|
139
|
-
# 2. Longer video with a slower visual rhythm
|
|
140
|
-
rf create "深夜便利店的灯光" -d 90 -p slow
|
|
141
|
-
|
|
142
|
-
# 3. Your own script — no narration-splitting on your side, the pipeline handles it
|
|
143
|
-
rf create --script @./my-script.txt
|
|
144
|
-
rf create --script "雨水缓缓滑落在玻璃窗上,像是无声的泪珠。"
|
|
145
|
-
|
|
146
|
-
# 4. Pick a built-in visual style preset
|
|
147
|
-
rf create "美食教程" --style photorealistic
|
|
148
|
-
|
|
149
|
-
# 5. Pipeline form with explicit output path
|
|
150
|
-
rf pipelines standard \
|
|
151
|
-
--script @./script.txt \
|
|
152
|
-
--frame-template 1080x1920/image_default.html \
|
|
153
|
-
-p normal -o smoke.mp4
|
|
154
|
-
|
|
155
|
-
# 6. Inspect existing tasks & redownload a finished video
|
|
156
|
-
rf tasks list --limit 5
|
|
157
|
-
rf history get <task-id> --download recovered.mp4
|
|
158
|
-
|
|
159
|
-
# 7. Atomics for stand-alone use
|
|
160
|
-
rf content scene-plan -t "雨天的玻璃窗" -d 45 --json | jq .scenes
|
|
161
|
-
rf audio transcribe -f narration.mp3 --json | jq '.words[:5]'
|
|
162
|
-
rf subtitles split -t @./narration.txt --min 10 --hard-max 24
|
|
163
|
-
|
|
164
|
-
# 8. JSON pipe for automation
|
|
165
|
-
rf llm presets --json | jq '.[].defaultModel'
|
|
166
|
-
|
|
167
|
-
# 9. Configure & test LLM (self-hosted)
|
|
168
|
-
rf config set llm.api_key rx-xxxxx # RelayX key (or your own provider key)
|
|
169
|
-
rf config set llm.base_url https://relayx.timor419.com/v1
|
|
170
|
-
rf config set llm.model anthropic/claude-4-7-sonnet
|
|
171
|
-
rf llm chat -p 'one-sentence summary of antifragile'
|
|
172
|
-
|
|
173
|
-
# 10. Use your own HTML template (no PR/release needed)
|
|
174
|
-
# Any --frame-template that points to a local .html file is read and sent
|
|
175
|
-
# inline. Declare size inside the file via
|
|
176
|
-
# <meta name="template:width" content="1080">
|
|
177
|
-
# <meta name="template:height" content="1920">
|
|
178
|
-
# or pass --frame-template-size 1080x1920.
|
|
179
|
-
rf templates show 1080x1920/image_default.html -o my-brand.html # copy a preset
|
|
180
|
-
# ...edit my-brand.html to suit your style...
|
|
181
|
-
rf templates preview ./my-brand.html --title "Hello" -o preview.png
|
|
182
|
-
rf frames render -t ./my-brand.html --values '{"author":"Alice"}' -o frame.png
|
|
183
|
-
rf pipelines standard -t "宠物" --frame-template ./my-brand.html -o final.mp4
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
### Custom HTML templates
|
|
187
|
-
|
|
188
|
-
Easiest way to start: grab a preset as a reference.
|
|
189
|
-
|
|
190
|
-
```bash
|
|
191
|
-
rf templates list # see all keys
|
|
192
|
-
rf templates show 1080x1920/static_default.html # print to stdout
|
|
193
|
-
rf templates show 1080x1920/image_default.html -o my-brand.html # save and edit
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
`{{title}}`, `{{text}}`, `{{image}}`, `{{index}}` are reserved built-ins; 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).
|
|
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
|
-
rf
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
+
### Resources
|
|
115
|
+
|
|
116
|
+
| command | what it does |
|
|
117
|
+
|---|---|
|
|
118
|
+
| `bgm list / upload <file> / delete <name>` | Manage background music |
|
|
119
|
+
| `files list / upload <file> / download <path> / delete <path>` | Manage user assets |
|
|
120
|
+
|
|
121
|
+
### System
|
|
122
|
+
|
|
123
|
+
| command | what it does |
|
|
124
|
+
|---|---|
|
|
125
|
+
| `config get` | Read server config (keys masked) |
|
|
126
|
+
| `config set <key> <value>` | Update a dotted-path setting (e.g. `llm.api_key sk-xxx`) |
|
|
127
|
+
| `config patch <file>` | Apply a JSON-merge patch |
|
|
128
|
+
| `tasks list [--status running]` | List recent tasks |
|
|
129
|
+
| `tasks get <id>` / `tasks wait <id>` / `tasks cancel <id>` | Task lifecycle |
|
|
130
|
+
| `history list / get <id> / delete <id>` | Browse / delete completed runs |
|
|
131
|
+
| `health` | Server health + capability check |
|
|
132
|
+
|
|
133
|
+
## Examples
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
# 1. One-click out a video (45s default, AI writes the script)
|
|
137
|
+
rf create "为什么我们还没找到外星文明?"
|
|
138
|
+
|
|
139
|
+
# 2. Longer video with a slower visual rhythm
|
|
140
|
+
rf create "深夜便利店的灯光" -d 90 -p slow
|
|
141
|
+
|
|
142
|
+
# 3. Your own script — no narration-splitting on your side, the pipeline handles it
|
|
143
|
+
rf create --script @./my-script.txt
|
|
144
|
+
rf create --script "雨水缓缓滑落在玻璃窗上,像是无声的泪珠。"
|
|
145
|
+
|
|
146
|
+
# 4. Pick a built-in visual style preset
|
|
147
|
+
rf create "美食教程" --style photorealistic
|
|
148
|
+
|
|
149
|
+
# 5. Pipeline form with explicit output path
|
|
150
|
+
rf pipelines standard \
|
|
151
|
+
--script @./script.txt \
|
|
152
|
+
--frame-template 1080x1920/image_default.html \
|
|
153
|
+
-p normal -o smoke.mp4
|
|
154
|
+
|
|
155
|
+
# 6. Inspect existing tasks & redownload a finished video
|
|
156
|
+
rf tasks list --limit 5
|
|
157
|
+
rf history get <task-id> --download recovered.mp4
|
|
158
|
+
|
|
159
|
+
# 7. Atomics for stand-alone use
|
|
160
|
+
rf content scene-plan -t "雨天的玻璃窗" -d 45 --json | jq .scenes
|
|
161
|
+
rf audio transcribe -f narration.mp3 --json | jq '.words[:5]'
|
|
162
|
+
rf subtitles split -t @./narration.txt --min 10 --hard-max 24
|
|
163
|
+
|
|
164
|
+
# 8. JSON pipe for automation
|
|
165
|
+
rf llm presets --json | jq '.[].defaultModel'
|
|
166
|
+
|
|
167
|
+
# 9. Configure & test LLM (self-hosted)
|
|
168
|
+
rf config set llm.api_key rx-xxxxx # RelayX key (or your own provider key)
|
|
169
|
+
rf config set llm.base_url https://relayx.timor419.com/v1
|
|
170
|
+
rf config set llm.model anthropic/claude-4-7-sonnet
|
|
171
|
+
rf llm chat -p 'one-sentence summary of antifragile'
|
|
172
|
+
|
|
173
|
+
# 10. Use your own HTML template (no PR/release needed)
|
|
174
|
+
# Any --frame-template that points to a local .html file is read and sent
|
|
175
|
+
# inline. Declare size inside the file via
|
|
176
|
+
# <meta name="template:width" content="1080">
|
|
177
|
+
# <meta name="template:height" content="1920">
|
|
178
|
+
# or pass --frame-template-size 1080x1920.
|
|
179
|
+
rf templates show 1080x1920/image_default.html -o my-brand.html # copy a preset
|
|
180
|
+
# ...edit my-brand.html to suit your style...
|
|
181
|
+
rf templates preview ./my-brand.html --title "Hello" -o preview.png
|
|
182
|
+
rf frames render -t ./my-brand.html --values '{"author":"Alice"}' -o frame.png
|
|
183
|
+
rf pipelines standard -t "宠物" --frame-template ./my-brand.html -o final.mp4
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Custom HTML templates
|
|
187
|
+
|
|
188
|
+
Easiest way to start: grab a preset as a reference.
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
rf templates list # see all keys
|
|
192
|
+
rf templates show 1080x1920/static_default.html # print to stdout
|
|
193
|
+
rf templates show 1080x1920/image_default.html -o my-brand.html # save and edit
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
`{{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).
|
|
197
|
+
|
|
198
|
+
- `{{index}}` — current scene number, 1-based
|
|
199
|
+
- `{{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)
|
|
200
|
+
|
|
201
|
+
#### Template type — does the pipeline generate an AI image per scene?
|
|
202
|
+
|
|
203
|
+
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):
|
|
204
|
+
|
|
205
|
+
1. Explicit flag — `--frame-template-type image|static|asset` (or `frame_template_type` in the API body).
|
|
206
|
+
2. Inside the HTML — `<meta name="template:type" content="image">` (or `static` / `asset`).
|
|
207
|
+
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.
|
|
208
|
+
|
|
209
|
+
The placeholder `{{image}}` no longer doubles as a type signal — declare type explicitly.
|
|
210
|
+
|
|
211
|
+
Limits and safety:
|
|
212
|
+
|
|
213
|
+
- Max 2 MB per inline HTML.
|
|
214
|
+
- 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.
|
|
215
|
+
- 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.
|
|
216
|
+
|
|
217
|
+
#### API field reference
|
|
218
|
+
|
|
219
|
+
| endpoint | inline HTML field | size field | type field |
|
|
220
|
+
|---|---|---|---|
|
|
221
|
+
| `POST /api/v1/frames/render` | `template_html` | `size` | — (n/a, no image generation) |
|
|
222
|
+
| `POST /api/v1/templates/preview` | `template_html` | `size` | — |
|
|
223
|
+
| `POST /api/v1/pipelines/standard` | `frame_template_inline` | `frame_template_size` | `frame_template_type` |
|
|
224
|
+
|
|
225
|
+
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.
|
|
226
|
+
|
|
227
|
+
## Tip — getting unstuck
|
|
228
|
+
|
|
229
|
+
Every level has `--help`:
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
rf --help # top-level overview
|
|
233
|
+
rf pipelines --help # list of pipelines
|
|
234
|
+
rf pipelines standard --help # full option reference
|
|
235
|
+
rf tts edge --help # one specific command
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## License
|
|
239
|
+
|
|
240
|
+
Apache-2.0
|
package/dist/commands/create.js
CHANGED
|
@@ -66,6 +66,37 @@ async function resolveTextOrFile(input) {
|
|
|
66
66
|
}
|
|
67
67
|
return input;
|
|
68
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Reference-image resolver. Accepts a public URL, a data: URI, or a local
|
|
71
|
+
* file path. Local files are base64-encoded into a data: URI so RelayX can
|
|
72
|
+
* receive them in a pure-JSON body (no upload endpoint needed on our side).
|
|
73
|
+
*
|
|
74
|
+
* Returns undefined when input is missing/blank so the caller can branch on
|
|
75
|
+
* "user actually provided this knob".
|
|
76
|
+
*/
|
|
77
|
+
async function resolveRefImage(input, flagName) {
|
|
78
|
+
if (input === undefined)
|
|
79
|
+
return undefined;
|
|
80
|
+
const t = input.trim();
|
|
81
|
+
if (!t)
|
|
82
|
+
return undefined;
|
|
83
|
+
if (/^https?:\/\//i.test(t) || t.startsWith("data:"))
|
|
84
|
+
return t;
|
|
85
|
+
const abs = path.resolve(t);
|
|
86
|
+
if (!fsSync.existsSync(abs)) {
|
|
87
|
+
throw new Error(`${flagName}: local file not found: ${abs}`);
|
|
88
|
+
}
|
|
89
|
+
const ext = path.extname(abs).toLowerCase();
|
|
90
|
+
const mime = ext === ".jpg" || ext === ".jpeg" ? "image/jpeg" :
|
|
91
|
+
ext === ".webp" ? "image/webp" :
|
|
92
|
+
ext === ".png" ? "image/png" :
|
|
93
|
+
null;
|
|
94
|
+
if (!mime) {
|
|
95
|
+
throw new Error(`${flagName}: unsupported extension ${ext} (use png/jpg/jpeg/webp)`);
|
|
96
|
+
}
|
|
97
|
+
const buf = await fs.readFile(abs);
|
|
98
|
+
return `data:${mime};base64,${buf.toString("base64")}`;
|
|
99
|
+
}
|
|
69
100
|
async function loadRecipe(recipePath) {
|
|
70
101
|
const raw = await fs.readFile(recipePath, "utf-8");
|
|
71
102
|
const parsed = JSON.parse(raw);
|
|
@@ -151,6 +182,8 @@ function optsToBody(opts) {
|
|
|
151
182
|
out.image_model = opts.imageModel;
|
|
152
183
|
if (opts.promptPrefix !== undefined)
|
|
153
184
|
out.prompt_prefix = opts.promptPrefix;
|
|
185
|
+
if (opts.characterRef !== undefined)
|
|
186
|
+
out.character_ref = opts.characterRef;
|
|
154
187
|
if (opts.voiceId !== undefined)
|
|
155
188
|
out.voice_id = opts.voiceId;
|
|
156
189
|
if (opts.ttsSpeed !== undefined)
|
|
@@ -304,9 +337,10 @@ export function registerCreate(program) {
|
|
|
304
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)")
|
|
305
338
|
.option("--frame-template-size <wxh>", "size for inline HTML when the file lacks <meta template:width|height>, e.g. 1080x1920")
|
|
306
339
|
.option("--frame-template-type <type>", "inline template type: image (default) | static | asset. Controls whether AI image generation runs per scene.")
|
|
307
|
-
.option("--image-model <id>", "RelayX image model (rx-image-z | rx-image-flux | rx-image-qwen)")
|
|
340
|
+
.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.")
|
|
308
341
|
.option("--prompt-prefix <text>", "raw style prefix prepended to every image prompt (overrides --style)")
|
|
309
342
|
.option("--style <preset>", "image style preset — shortcut for --prompt-prefix; see 'Style presets' below")
|
|
343
|
+
.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.")
|
|
310
344
|
// --- Audio (TTS) ---
|
|
311
345
|
.option("--voice-id <id>", "RelayX TTS voice id (default 专业解说); see `rf tts voices`")
|
|
312
346
|
.option("--tts-speed <n>", "speech speed 0.5..2 (default 1.0)", parseFloat)
|
|
@@ -378,6 +412,10 @@ export function registerCreate(program) {
|
|
|
378
412
|
" # Pick a built-in style preset",
|
|
379
413
|
' rf create "..." --style cinematic',
|
|
380
414
|
"",
|
|
415
|
+
" # Cross-scene character consistency (auto-switches image model)",
|
|
416
|
+
' rf create "主角小女孩的一天" --character-ref ./hero.png',
|
|
417
|
+
' rf create "..." --character-ref https://example.com/hero.png',
|
|
418
|
+
"",
|
|
381
419
|
" # Recipe + replay last",
|
|
382
420
|
" rf create --recipe ./space.recipe.json",
|
|
383
421
|
" rf create --redo # replay last successful create",
|
|
@@ -438,6 +476,14 @@ export function registerCreate(program) {
|
|
|
438
476
|
if (typeof body.script === "string") {
|
|
439
477
|
body.script = await resolveTextOrFile(body.script);
|
|
440
478
|
}
|
|
479
|
+
// Resolve character ref: local file path → data: URI (RelayX accepts
|
|
480
|
+
// both https:// and data: in image_urls). Done after layering so a
|
|
481
|
+
// recipe can carry the ref by path too.
|
|
482
|
+
const resolvedChar = await resolveRefImage(body.character_ref, "--character-ref");
|
|
483
|
+
if (resolvedChar !== undefined)
|
|
484
|
+
body.character_ref = resolvedChar;
|
|
485
|
+
else
|
|
486
|
+
delete body.character_ref;
|
|
441
487
|
// Validate content mode
|
|
442
488
|
const hasTopic = typeof body.topic === "string" && body.topic.trim().length > 0;
|
|
443
489
|
const hasScript = typeof body.script === "string" && body.script.trim().length > 0;
|
package/dist/commands/images.js
CHANGED
|
@@ -1,6 +1,35 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import fsSync from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
1
4
|
import { post } from "../client.js";
|
|
2
5
|
import { downloadTo } from "../utils/download.js";
|
|
3
6
|
import { print, success } from "../utils/output.js";
|
|
7
|
+
/**
|
|
8
|
+
* Reference image resolver. Accepts an https:// URL, a data: URI, or a local
|
|
9
|
+
* file path. Local files are read + base64-encoded into a data: URI so the
|
|
10
|
+
* server can forward them inside the JSON body — no file-hosting step needed.
|
|
11
|
+
*/
|
|
12
|
+
async function resolveImageRef(input) {
|
|
13
|
+
const t = input.trim();
|
|
14
|
+
if (!t)
|
|
15
|
+
throw new Error("--image: empty value");
|
|
16
|
+
if (/^https?:\/\//i.test(t) || t.startsWith("data:"))
|
|
17
|
+
return t;
|
|
18
|
+
const abs = path.resolve(t);
|
|
19
|
+
if (!fsSync.existsSync(abs)) {
|
|
20
|
+
throw new Error(`--image: local file not found: ${abs}`);
|
|
21
|
+
}
|
|
22
|
+
const ext = path.extname(abs).toLowerCase();
|
|
23
|
+
const mime = ext === ".jpg" || ext === ".jpeg" ? "image/jpeg" :
|
|
24
|
+
ext === ".webp" ? "image/webp" :
|
|
25
|
+
ext === ".png" ? "image/png" :
|
|
26
|
+
null;
|
|
27
|
+
if (!mime) {
|
|
28
|
+
throw new Error(`--image: unsupported extension ${ext} (use png/jpg/jpeg/webp)`);
|
|
29
|
+
}
|
|
30
|
+
const buf = await fs.readFile(abs);
|
|
31
|
+
return `data:${mime};base64,${buf.toString("base64")}`;
|
|
32
|
+
}
|
|
4
33
|
export function registerImages(program) {
|
|
5
34
|
const images = program
|
|
6
35
|
.command("images")
|
|
@@ -8,19 +37,30 @@ export function registerImages(program) {
|
|
|
8
37
|
.helpOption("-h, --help", "show help");
|
|
9
38
|
images
|
|
10
39
|
.command("generate")
|
|
11
|
-
.description("Generate an image via RelayX")
|
|
40
|
+
.description("Generate an image via RelayX (text-to-image or ref-image, depending on model + --image)")
|
|
12
41
|
.helpOption("-h, --help", "show help")
|
|
13
|
-
.requiredOption("-p, --prompt <text>", "text prompt")
|
|
14
|
-
.option("-m, --model <id>", "RelayX image model id (rx-image-z | rx-image-flux | rx-image-qwen)")
|
|
42
|
+
.requiredOption("-p, --prompt <text>", "text prompt. With --image, reference each ref via 「图 N」/「Image N」.")
|
|
43
|
+
.option("-m, --model <id>", "RelayX image model id (rx-image-z | rx-image-flux | rx-image-qwen | rx-image-qwen-edit)")
|
|
15
44
|
.option("--width <n>", "image width", parseInt)
|
|
16
45
|
.option("--height <n>", "image height", parseInt)
|
|
46
|
+
.option("-i, --image <urlOrPath>", "reference image, repeatable up to 3 times. URL / data: URI / local png/jpg/webp path (auto-base64'd). Index N-1 maps to 「图 N」in the prompt. Only honored by edit-capable SKUs (rx-image-qwen-edit).", (val, prev) => [...prev, val], [])
|
|
17
47
|
.option("-o, --output <file>", "download first image to this local path")
|
|
18
48
|
.option("--all-output <dir>", "download ALL generated images into this directory")
|
|
19
49
|
.addHelpText("after", [
|
|
20
50
|
"",
|
|
21
51
|
"Examples:",
|
|
52
|
+
" # plain text-to-image",
|
|
22
53
|
" reelforge images generate -p 'a cat' -m rx-image-flux --width 1024 --height 1024 -o cat.png",
|
|
23
|
-
"
|
|
54
|
+
"",
|
|
55
|
+
" # ref-image edit (Qwen-Image-Edit), 1 ref local file",
|
|
56
|
+
" reelforge images generate -m rx-image-qwen-edit \\",
|
|
57
|
+
" -p '保持图1人物,把背景改成雪山黄昏' \\",
|
|
58
|
+
" -i ./hero.png -o out.png",
|
|
59
|
+
"",
|
|
60
|
+
" # ref-image edit, 2 refs (URL + local), 「图1」+「图2」",
|
|
61
|
+
" reelforge images generate -m rx-image-qwen-edit \\",
|
|
62
|
+
" -p '让图1的人物穿上图2的衣服' \\",
|
|
63
|
+
" -i https://example.com/person.jpg -i ./outfit.png -o composite.png",
|
|
24
64
|
].join("\n"))
|
|
25
65
|
.action(async (opts) => {
|
|
26
66
|
const body = { prompt: opts.prompt };
|
|
@@ -30,6 +70,12 @@ export function registerImages(program) {
|
|
|
30
70
|
body.width = opts.width;
|
|
31
71
|
if (opts.height !== undefined)
|
|
32
72
|
body.height = opts.height;
|
|
73
|
+
if (opts.image && Array.isArray(opts.image) && opts.image.length > 0) {
|
|
74
|
+
if (opts.image.length > 3) {
|
|
75
|
+
throw new Error(`--image accepts at most 3 refs (got ${opts.image.length})`);
|
|
76
|
+
}
|
|
77
|
+
body.image_urls = await Promise.all(opts.image.map(resolveImageRef));
|
|
78
|
+
}
|
|
33
79
|
const r = await post("/api/v1/images/generate", body);
|
|
34
80
|
if (opts.output && r.images?.[0]) {
|
|
35
81
|
await downloadTo(r.images[0], opts.output);
|
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.7.1",
|
|
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
|
+
}
|