reelforge 1.22.0 → 1.24.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 +295 -295
- package/bin/reelforge.js +8 -8
- package/dist/commands/clip.js +139 -0
- package/dist/commands/compose.js +28 -28
- package/dist/commands/dub.js +201 -0
- package/dist/index.js +5 -0
- package/package.json +44 -44
package/README.md
CHANGED
|
@@ -1,295 +1,295 @@
|
|
|
1
|
-
# reelforge
|
|
2
|
-
|
|
3
|
-
> Turn a topic or script into a finished vertical video — narration, visuals, and subtitles, assembled for you. Every capability is a command, with `--help` 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 service. 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
|
-
## Global options
|
|
46
|
-
|
|
47
|
-
| flag | description |
|
|
48
|
-
|---|---|
|
|
49
|
-
| `-s, --server <url>` | Server URL (overrides `$REELFORGE_SERVER`; defaults to the hosted service) |
|
|
50
|
-
| `-k, --api-key <key>` | API key (overrides `$REELFORGE_API_KEY` and `reelforge login` saved key) |
|
|
51
|
-
| `--json` | Output raw JSON instead of pretty text — pipe-friendly |
|
|
52
|
-
| `--quiet` | Suppress informational messages on stderr |
|
|
53
|
-
| `-v, --version` | Show CLI version |
|
|
54
|
-
| `-h, --help` | Show help (works on every sub-command) |
|
|
55
|
-
|
|
56
|
-
## Command map
|
|
57
|
-
|
|
58
|
-
Run `rf <command> --help` for full details on any of these.
|
|
59
|
-
|
|
60
|
-
### Core capabilities
|
|
61
|
-
|
|
62
|
-
| command | what it does |
|
|
63
|
-
|---|---|
|
|
64
|
-
| `llm chat -p <text>` | Send one prompt to the configured model |
|
|
65
|
-
| `llm presets` | List built-in model presets |
|
|
66
|
-
| `tts ... -t <text> -o out.mp3` | Text-to-speech (free local + cloud voice options) |
|
|
67
|
-
| `tts voices [--locale zh]` | List supported voices |
|
|
68
|
-
| `images generate -p <prompt>` | Image generation |
|
|
69
|
-
|
|
70
|
-
### Content / audio / subtitle atomics
|
|
71
|
-
|
|
72
|
-
| command | what it does |
|
|
73
|
-
|---|---|
|
|
74
|
-
| `content scene-plan -t <topic>` | Single LLM call: title + master script + per-scene image prompts (replaces the old narration / split / image-prompts / title trio) |
|
|
75
|
-
| `content scene-plan --script <text-or-@file>` | Same, but the user supplies the script verbatim — LLM only segments and writes image prompts |
|
|
76
|
-
| `audio transcribe -f <file>` / `--url <url>` | Speech-to-text with word + segment timestamps |
|
|
77
|
-
| `subtitles split -t <text-or-@file>` | Deterministic tiered-punctuation subtitle line splitter (pure function, zero billing) |
|
|
78
|
-
|
|
79
|
-
### Composition
|
|
80
|
-
|
|
81
|
-
| command | what it does |
|
|
82
|
-
|---|---|
|
|
83
|
-
| `templates list [--size 1080x1920] [--type image]` | List HTML frame templates |
|
|
84
|
-
| `templates preview <keyOrPath> [-o out.png]` | Render a preview from a preset key **or your own local .html file** |
|
|
85
|
-
| `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 |
|
|
86
|
-
| `frames render -t <keyOrPath> --title ... --text ...` | Render a single composed frame to PNG. `-t` accepts a preset key **or a local .html path** |
|
|
87
|
-
| `compositions concat <v1> <v2> -o out.mp4` | FFmpeg concat (+ optional BGM) |
|
|
88
|
-
| `compositions bgm -i video.mp4 --bgm bgm.mp3 -o out.mp4` | Add background music |
|
|
89
|
-
| `compositions image-to-video -i img.png -a aud.mp3 -o out.mp4` | Build video from image + audio |
|
|
90
|
-
| `compositions overlay -v video.mp4 --overlay overlay.png -o out.mp4` | Overlay PNG on video |
|
|
91
|
-
|
|
92
|
-
### End-to-end pipelines
|
|
93
|
-
|
|
94
|
-
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.
|
|
95
|
-
|
|
96
|
-
The standard pipeline turns a topic or script into a finished vertical video — narration, visuals, and subtitles are generated and assembled for you. One continuous narration track; visuals cut at scene boundaries; subtitles cut at line boundaries.
|
|
97
|
-
|
|
98
|
-
| command | what it does |
|
|
99
|
-
|---|---|
|
|
100
|
-
| `pipelines standard -t <topic>` (or `--script <text>`) | Audio-first pipeline; `-d/--duration` and `-p/--pace` are the two main knobs |
|
|
101
|
-
|
|
102
|
-
#### Composition knobs
|
|
103
|
-
|
|
104
|
-
Three independent axes — mix and match as you like:
|
|
105
|
-
|
|
106
|
-
| flag | values | default | what changes |
|
|
107
|
-
|---|---|---|---|
|
|
108
|
-
| `--motion` | `off` / `lite` / `max` | `lite` | per-scene zoompan + crossfade intensity |
|
|
109
|
-
| `--layout` | `full` / `blur-bg` / `letterbox` | `full` | how the image sits in the canvas |
|
|
110
|
-
| `--subtitle-style` | `plate` / `stroke` / `cinema` | `plate` | subtitle look |
|
|
111
|
-
|
|
112
|
-
**Layout presets**:
|
|
113
|
-
- `full` — image fills the whole 1080×1920 canvas. Generates a 1080×1920 image. High-impact; best for human portraits, landscapes, 9:16-native content.
|
|
114
|
-
- `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).
|
|
115
|
-
- `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`).
|
|
116
|
-
|
|
117
|
-
Examples:
|
|
118
|
-
```bash
|
|
119
|
-
rf create "财经日报" --layout blur-bg # 小红书 / 抖音
|
|
120
|
-
rf create "纪录片片段" --layout letterbox --motion max # 电影感
|
|
121
|
-
rf create "..." --layout letterbox --layout-matte-color "#1a1a1a" # 柔和黑
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
### Resources
|
|
125
|
-
|
|
126
|
-
| command | what it does |
|
|
127
|
-
|---|---|
|
|
128
|
-
| `bgm list / upload <file> / delete <name>` | Manage background music |
|
|
129
|
-
| `files list / upload <file> / download <path> / delete <path>` | Manage user assets |
|
|
130
|
-
|
|
131
|
-
### System
|
|
132
|
-
|
|
133
|
-
| command | what it does |
|
|
134
|
-
|---|---|
|
|
135
|
-
| `config get` | Read server config (keys masked) |
|
|
136
|
-
| `config set <key> <value>` | Update a dotted-path setting (e.g. `llm.api_key sk-xxx`) |
|
|
137
|
-
| `config patch <file>` | Apply a JSON-merge patch |
|
|
138
|
-
| `tasks list [--status running]` | List recent tasks |
|
|
139
|
-
| `tasks get <id>` / `tasks wait <id>` / `tasks cancel <id>` | Task lifecycle |
|
|
140
|
-
| `history list / get <id> / delete <id>` | Browse / delete completed runs |
|
|
141
|
-
| `health` | Server health + capability check |
|
|
142
|
-
|
|
143
|
-
### Heavy brand customization (custom overlay templates)
|
|
144
|
-
|
|
145
|
-
`--motion` / `--layout` / `--subtitle-*` / `--brand-*` cover the common cases. For full visual identity ownership (custom path bar, accent decorations, light theme, footer block, etc.) pass a custom overlay HTML via `--frame-template <local.html | preset_key>`:
|
|
146
|
-
|
|
147
|
-
```bash
|
|
148
|
-
rf templates show 1080x1920/default.html -o ./my-brand.html
|
|
149
|
-
# ...edit my-brand.html (change colors, add structure, etc.)...
|
|
150
|
-
rf create "我的视频" --frame-template ./my-brand.html
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
**Contract for custom HTML**:
|
|
154
|
-
|
|
155
|
-
- Canvas is **1080×1920** (pipeline-fixed; don't declare a different size in `<meta>`).
|
|
156
|
-
- Background must be **transparent** — the scene image is composited by the renderer outside the HTML layer.
|
|
157
|
-
- **`{{image}}` no longer exists.** Using `<img src="{{image}}">` is a hard error at submit time. The image is composited by the renderer.
|
|
158
|
-
- The pipeline injects these placeholders for you:
|
|
159
|
-
|
|
160
|
-
| Placeholder | What it is |
|
|
161
|
-
|---|---|
|
|
162
|
-
| `{{title}}` `{{text}}` | per-frame content |
|
|
163
|
-
| `{{index}}` `{{total}}` | "scene N of M" — `{{total}}` is the LLM-decided scene count, don't hardcode |
|
|
164
|
-
| `{{layout}}` | `"full"` / `"blur-bg"` / `"letterbox"` — react via `body[data-layout="..."]` CSS to put title/subtitle in the matte zones when layout ≠ full |
|
|
165
|
-
| `{{subtitle_style}}` `{{subtitle_color}}` `{{subtitle_background}}` | subtitle preset + overrides |
|
|
166
|
-
| `{{brand_position}}` `{{brand_handle}}` `{{brand_slogan}}` `{{brand_logo}}` `{{brand_color}}` | brand-chrome inputs (use or ignore as you like) |
|
|
167
|
-
|
|
168
|
-
Inline HTML is hard-capped at 2 MB. The audio + motion + character-ref + scene-plan stages all keep working identically — only the overlay layer is yours.
|
|
169
|
-
|
|
170
|
-
**Publishing to short-video platforms (Douyin / TikTok / WeChat Channels)** — the default composition renders to the full 1080×1920 canvas, but those apps overlay UI on top/bottom/right and cover-crop ~96-180px on each side on taller phones. ReelForge does NOT bake platform-specific padding into the renderer. Look up reference safe-zone numbers + recommended `--media-anchor-y` values per platform:
|
|
171
|
-
|
|
172
|
-
```bash
|
|
173
|
-
rf platform # overview table for all platforms
|
|
174
|
-
rf platform 抖音 # detail + anchor variants (alias of douyin)
|
|
175
|
-
rf platform tiktok
|
|
176
|
-
rf platform wechat # 视频号
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
## Examples
|
|
180
|
-
|
|
181
|
-
```bash
|
|
182
|
-
# 1. One-click out a video (45s default, AI writes the script)
|
|
183
|
-
rf create "为什么我们还没找到外星文明?"
|
|
184
|
-
|
|
185
|
-
# 2. Longer video with a slower visual rhythm
|
|
186
|
-
rf create "深夜便利店的灯光" -d 90 -p slow
|
|
187
|
-
|
|
188
|
-
# 3. Your own script — no narration-splitting on your side, the pipeline handles it
|
|
189
|
-
rf create --script @./my-script.txt
|
|
190
|
-
rf create --script "雨水缓缓滑落在玻璃窗上,像是无声的泪珠。"
|
|
191
|
-
|
|
192
|
-
# 4. Preview-first workflow: scrub the storyboard in the browser
|
|
193
|
-
# before paying for the MP4 render. Finalize with `rf render` when satisfied.
|
|
194
|
-
rf create "..." --preview-only # opens browser studio
|
|
195
|
-
rf render <task-id> # produce the MP4
|
|
196
|
-
|
|
197
|
-
# 5. Push the image square up to grow the bottom matte (抖音 long-description
|
|
198
|
-
# case); subtitle position follows the image automatically.
|
|
199
|
-
rf create "..." --layout blur-bg --media-anchor-y 0.40
|
|
200
|
-
# See `rf platform 抖音` for the safe range and per-scenario recommendations.
|
|
201
|
-
|
|
202
|
-
# 6. Encoder budget — pick a quality preset or set an exact bitrate.
|
|
203
|
-
rf create "..." --quality draft # ~1 Mbps, ~3-4× smaller than default
|
|
204
|
-
rf create "..." --video-bitrate 750k # exact bitrate
|
|
205
|
-
rf create "..." --crf 28 # fine-grained control
|
|
206
|
-
|
|
207
|
-
# 7. Pick a built-in visual style preset
|
|
208
|
-
rf create "美食教程" --style photorealistic
|
|
209
|
-
|
|
210
|
-
# 5. Pipeline form with explicit output path
|
|
211
|
-
rf pipelines standard \
|
|
212
|
-
--script @./script.txt \
|
|
213
|
-
--frame-template 1080x1920/image_default.html \
|
|
214
|
-
-p normal -o smoke.mp4
|
|
215
|
-
|
|
216
|
-
# 6. Inspect existing tasks & redownload a finished video
|
|
217
|
-
rf tasks list --limit 5
|
|
218
|
-
rf history get <task-id> --download recovered.mp4
|
|
219
|
-
|
|
220
|
-
# 7. Atomics for stand-alone use
|
|
221
|
-
rf content scene-plan -t "雨天的玻璃窗" -d 45 --json | jq .scenes
|
|
222
|
-
rf audio transcribe -f narration.mp3 --json | jq '.words[:5]'
|
|
223
|
-
rf subtitles split -t @./narration.txt --min 10 --hard-max 24
|
|
224
|
-
|
|
225
|
-
# 8. JSON pipe for automation
|
|
226
|
-
rf llm presets --json | jq '.[].defaultModel'
|
|
227
|
-
|
|
228
|
-
# 9. Use your own HTML template (no PR/release needed)
|
|
229
|
-
# Any --frame-template that points to a local .html file is read and sent
|
|
230
|
-
# inline. Declare size inside the file via
|
|
231
|
-
# <meta name="template:width" content="1080">
|
|
232
|
-
# <meta name="template:height" content="1920">
|
|
233
|
-
# or pass --frame-template-size 1080x1920.
|
|
234
|
-
rf templates show 1080x1920/image_default.html -o my-brand.html # copy a preset
|
|
235
|
-
# ...edit my-brand.html to suit your style...
|
|
236
|
-
rf templates preview ./my-brand.html --title "Hello" -o preview.png
|
|
237
|
-
rf frames render -t ./my-brand.html --values '{"author":"Alice"}' -o frame.png
|
|
238
|
-
rf pipelines standard -t "宠物" --frame-template ./my-brand.html -o final.mp4
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
### Custom HTML templates
|
|
242
|
-
|
|
243
|
-
Easiest way to start: grab a preset as a reference.
|
|
244
|
-
|
|
245
|
-
```bash
|
|
246
|
-
rf templates list # see all keys
|
|
247
|
-
rf templates show 1080x1920/static_default.html # print to stdout
|
|
248
|
-
rf templates show 1080x1920/image_default.html -o my-brand.html # save and edit
|
|
249
|
-
```
|
|
250
|
-
|
|
251
|
-
`{{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).
|
|
252
|
-
|
|
253
|
-
- `{{index}}` — current scene number, 1-based
|
|
254
|
-
- `{{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)
|
|
255
|
-
|
|
256
|
-
#### Template type — does the pipeline generate an AI image per scene?
|
|
257
|
-
|
|
258
|
-
When you ship an inline template through `rf create` / `rf pipelines standard`, ReelForge needs to know whether each scene should kick off image generation. Resolution priority (high → low):
|
|
259
|
-
|
|
260
|
-
1. Explicit flag — `--frame-template-type image|static|asset` (or `frame_template_type` in the API body).
|
|
261
|
-
2. Inside the HTML — `<meta name="template:type" content="image">` (or `static` / `asset`).
|
|
262
|
-
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.
|
|
263
|
-
|
|
264
|
-
The placeholder `{{image}}` no longer doubles as a type signal — declare type explicitly.
|
|
265
|
-
|
|
266
|
-
Limits and safety:
|
|
267
|
-
|
|
268
|
-
- Max 2 MB per inline HTML.
|
|
269
|
-
- 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.
|
|
270
|
-
- 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.
|
|
271
|
-
|
|
272
|
-
#### API field reference
|
|
273
|
-
|
|
274
|
-
| endpoint | inline HTML field | size field | type field |
|
|
275
|
-
|---|---|---|---|
|
|
276
|
-
| `POST /api/v1/frames/render` | `template_html` | `size` | — (n/a, no image generation) |
|
|
277
|
-
| `POST /api/v1/templates/preview` | `template_html` | `size` | — |
|
|
278
|
-
| `POST /api/v1/pipelines/standard` | `frame_template_inline` | `frame_template_size` | `frame_template_type` |
|
|
279
|
-
|
|
280
|
-
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.
|
|
281
|
-
|
|
282
|
-
## Tip — getting unstuck
|
|
283
|
-
|
|
284
|
-
Every level has `--help`:
|
|
285
|
-
|
|
286
|
-
```bash
|
|
287
|
-
rf --help # top-level overview
|
|
288
|
-
rf pipelines --help # list of pipelines
|
|
289
|
-
rf pipelines standard --help # full option reference
|
|
290
|
-
rf tts edge --help # one specific command
|
|
291
|
-
```
|
|
292
|
-
|
|
293
|
-
## License
|
|
294
|
-
|
|
295
|
-
Apache-2.0
|
|
1
|
+
# reelforge
|
|
2
|
+
|
|
3
|
+
> Turn a topic or script into a finished vertical video — narration, visuals, and subtitles, assembled for you. Every capability is a command, with `--help` 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 service. 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
|
+
## Global options
|
|
46
|
+
|
|
47
|
+
| flag | description |
|
|
48
|
+
|---|---|
|
|
49
|
+
| `-s, --server <url>` | Server URL (overrides `$REELFORGE_SERVER`; defaults to the hosted service) |
|
|
50
|
+
| `-k, --api-key <key>` | API key (overrides `$REELFORGE_API_KEY` and `reelforge login` saved key) |
|
|
51
|
+
| `--json` | Output raw JSON instead of pretty text — pipe-friendly |
|
|
52
|
+
| `--quiet` | Suppress informational messages on stderr |
|
|
53
|
+
| `-v, --version` | Show CLI version |
|
|
54
|
+
| `-h, --help` | Show help (works on every sub-command) |
|
|
55
|
+
|
|
56
|
+
## Command map
|
|
57
|
+
|
|
58
|
+
Run `rf <command> --help` for full details on any of these.
|
|
59
|
+
|
|
60
|
+
### Core capabilities
|
|
61
|
+
|
|
62
|
+
| command | what it does |
|
|
63
|
+
|---|---|
|
|
64
|
+
| `llm chat -p <text>` | Send one prompt to the configured model |
|
|
65
|
+
| `llm presets` | List built-in model presets |
|
|
66
|
+
| `tts ... -t <text> -o out.mp3` | Text-to-speech (free local + cloud voice options) |
|
|
67
|
+
| `tts voices [--locale zh]` | List supported voices |
|
|
68
|
+
| `images generate -p <prompt>` | Image generation |
|
|
69
|
+
|
|
70
|
+
### Content / audio / subtitle atomics
|
|
71
|
+
|
|
72
|
+
| command | what it does |
|
|
73
|
+
|---|---|
|
|
74
|
+
| `content scene-plan -t <topic>` | Single LLM call: title + master script + per-scene image prompts (replaces the old narration / split / image-prompts / title trio) |
|
|
75
|
+
| `content scene-plan --script <text-or-@file>` | Same, but the user supplies the script verbatim — LLM only segments and writes image prompts |
|
|
76
|
+
| `audio transcribe -f <file>` / `--url <url>` | Speech-to-text with word + segment timestamps |
|
|
77
|
+
| `subtitles split -t <text-or-@file>` | Deterministic tiered-punctuation subtitle line splitter (pure function, zero billing) |
|
|
78
|
+
|
|
79
|
+
### Composition
|
|
80
|
+
|
|
81
|
+
| command | what it does |
|
|
82
|
+
|---|---|
|
|
83
|
+
| `templates list [--size 1080x1920] [--type image]` | List HTML frame templates |
|
|
84
|
+
| `templates preview <keyOrPath> [-o out.png]` | Render a preview from a preset key **or your own local .html file** |
|
|
85
|
+
| `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 |
|
|
86
|
+
| `frames render -t <keyOrPath> --title ... --text ...` | Render a single composed frame to PNG. `-t` accepts a preset key **or a local .html path** |
|
|
87
|
+
| `compositions concat <v1> <v2> -o out.mp4` | FFmpeg concat (+ optional BGM) |
|
|
88
|
+
| `compositions bgm -i video.mp4 --bgm bgm.mp3 -o out.mp4` | Add background music |
|
|
89
|
+
| `compositions image-to-video -i img.png -a aud.mp3 -o out.mp4` | Build video from image + audio |
|
|
90
|
+
| `compositions overlay -v video.mp4 --overlay overlay.png -o out.mp4` | Overlay PNG on video |
|
|
91
|
+
|
|
92
|
+
### End-to-end pipelines
|
|
93
|
+
|
|
94
|
+
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.
|
|
95
|
+
|
|
96
|
+
The standard pipeline turns a topic or script into a finished vertical video — narration, visuals, and subtitles are generated and assembled for you. One continuous narration track; visuals cut at scene boundaries; subtitles cut at line boundaries.
|
|
97
|
+
|
|
98
|
+
| command | what it does |
|
|
99
|
+
|---|---|
|
|
100
|
+
| `pipelines standard -t <topic>` (or `--script <text>`) | Audio-first pipeline; `-d/--duration` and `-p/--pace` are the two main knobs |
|
|
101
|
+
|
|
102
|
+
#### Composition knobs
|
|
103
|
+
|
|
104
|
+
Three independent axes — mix and match as you like:
|
|
105
|
+
|
|
106
|
+
| flag | values | default | what changes |
|
|
107
|
+
|---|---|---|---|
|
|
108
|
+
| `--motion` | `off` / `lite` / `max` | `lite` | per-scene zoompan + crossfade intensity |
|
|
109
|
+
| `--layout` | `full` / `blur-bg` / `letterbox` | `full` | how the image sits in the canvas |
|
|
110
|
+
| `--subtitle-style` | `plate` / `stroke` / `cinema` | `plate` | subtitle look |
|
|
111
|
+
|
|
112
|
+
**Layout presets**:
|
|
113
|
+
- `full` — image fills the whole 1080×1920 canvas. Generates a 1080×1920 image. High-impact; best for human portraits, landscapes, 9:16-native content.
|
|
114
|
+
- `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).
|
|
115
|
+
- `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`).
|
|
116
|
+
|
|
117
|
+
Examples:
|
|
118
|
+
```bash
|
|
119
|
+
rf create "财经日报" --layout blur-bg # 小红书 / 抖音
|
|
120
|
+
rf create "纪录片片段" --layout letterbox --motion max # 电影感
|
|
121
|
+
rf create "..." --layout letterbox --layout-matte-color "#1a1a1a" # 柔和黑
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Resources
|
|
125
|
+
|
|
126
|
+
| command | what it does |
|
|
127
|
+
|---|---|
|
|
128
|
+
| `bgm list / upload <file> / delete <name>` | Manage background music |
|
|
129
|
+
| `files list / upload <file> / download <path> / delete <path>` | Manage user assets |
|
|
130
|
+
|
|
131
|
+
### System
|
|
132
|
+
|
|
133
|
+
| command | what it does |
|
|
134
|
+
|---|---|
|
|
135
|
+
| `config get` | Read server config (keys masked) |
|
|
136
|
+
| `config set <key> <value>` | Update a dotted-path setting (e.g. `llm.api_key sk-xxx`) |
|
|
137
|
+
| `config patch <file>` | Apply a JSON-merge patch |
|
|
138
|
+
| `tasks list [--status running]` | List recent tasks |
|
|
139
|
+
| `tasks get <id>` / `tasks wait <id>` / `tasks cancel <id>` | Task lifecycle |
|
|
140
|
+
| `history list / get <id> / delete <id>` | Browse / delete completed runs |
|
|
141
|
+
| `health` | Server health + capability check |
|
|
142
|
+
|
|
143
|
+
### Heavy brand customization (custom overlay templates)
|
|
144
|
+
|
|
145
|
+
`--motion` / `--layout` / `--subtitle-*` / `--brand-*` cover the common cases. For full visual identity ownership (custom path bar, accent decorations, light theme, footer block, etc.) pass a custom overlay HTML via `--frame-template <local.html | preset_key>`:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
rf templates show 1080x1920/default.html -o ./my-brand.html
|
|
149
|
+
# ...edit my-brand.html (change colors, add structure, etc.)...
|
|
150
|
+
rf create "我的视频" --frame-template ./my-brand.html
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
**Contract for custom HTML**:
|
|
154
|
+
|
|
155
|
+
- Canvas is **1080×1920** (pipeline-fixed; don't declare a different size in `<meta>`).
|
|
156
|
+
- Background must be **transparent** — the scene image is composited by the renderer outside the HTML layer.
|
|
157
|
+
- **`{{image}}` no longer exists.** Using `<img src="{{image}}">` is a hard error at submit time. The image is composited by the renderer.
|
|
158
|
+
- The pipeline injects these placeholders for you:
|
|
159
|
+
|
|
160
|
+
| Placeholder | What it is |
|
|
161
|
+
|---|---|
|
|
162
|
+
| `{{title}}` `{{text}}` | per-frame content |
|
|
163
|
+
| `{{index}}` `{{total}}` | "scene N of M" — `{{total}}` is the LLM-decided scene count, don't hardcode |
|
|
164
|
+
| `{{layout}}` | `"full"` / `"blur-bg"` / `"letterbox"` — react via `body[data-layout="..."]` CSS to put title/subtitle in the matte zones when layout ≠ full |
|
|
165
|
+
| `{{subtitle_style}}` `{{subtitle_color}}` `{{subtitle_background}}` | subtitle preset + overrides |
|
|
166
|
+
| `{{brand_position}}` `{{brand_handle}}` `{{brand_slogan}}` `{{brand_logo}}` `{{brand_color}}` | brand-chrome inputs (use or ignore as you like) |
|
|
167
|
+
|
|
168
|
+
Inline HTML is hard-capped at 2 MB. The audio + motion + character-ref + scene-plan stages all keep working identically — only the overlay layer is yours.
|
|
169
|
+
|
|
170
|
+
**Publishing to short-video platforms (Douyin / TikTok / WeChat Channels)** — the default composition renders to the full 1080×1920 canvas, but those apps overlay UI on top/bottom/right and cover-crop ~96-180px on each side on taller phones. ReelForge does NOT bake platform-specific padding into the renderer. Look up reference safe-zone numbers + recommended `--media-anchor-y` values per platform:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
rf platform # overview table for all platforms
|
|
174
|
+
rf platform 抖音 # detail + anchor variants (alias of douyin)
|
|
175
|
+
rf platform tiktok
|
|
176
|
+
rf platform wechat # 视频号
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Examples
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
# 1. One-click out a video (45s default, AI writes the script)
|
|
183
|
+
rf create "为什么我们还没找到外星文明?"
|
|
184
|
+
|
|
185
|
+
# 2. Longer video with a slower visual rhythm
|
|
186
|
+
rf create "深夜便利店的灯光" -d 90 -p slow
|
|
187
|
+
|
|
188
|
+
# 3. Your own script — no narration-splitting on your side, the pipeline handles it
|
|
189
|
+
rf create --script @./my-script.txt
|
|
190
|
+
rf create --script "雨水缓缓滑落在玻璃窗上,像是无声的泪珠。"
|
|
191
|
+
|
|
192
|
+
# 4. Preview-first workflow: scrub the storyboard in the browser
|
|
193
|
+
# before paying for the MP4 render. Finalize with `rf render` when satisfied.
|
|
194
|
+
rf create "..." --preview-only # opens browser studio
|
|
195
|
+
rf render <task-id> # produce the MP4
|
|
196
|
+
|
|
197
|
+
# 5. Push the image square up to grow the bottom matte (抖音 long-description
|
|
198
|
+
# case); subtitle position follows the image automatically.
|
|
199
|
+
rf create "..." --layout blur-bg --media-anchor-y 0.40
|
|
200
|
+
# See `rf platform 抖音` for the safe range and per-scenario recommendations.
|
|
201
|
+
|
|
202
|
+
# 6. Encoder budget — pick a quality preset or set an exact bitrate.
|
|
203
|
+
rf create "..." --quality draft # ~1 Mbps, ~3-4× smaller than default
|
|
204
|
+
rf create "..." --video-bitrate 750k # exact bitrate
|
|
205
|
+
rf create "..." --crf 28 # fine-grained control
|
|
206
|
+
|
|
207
|
+
# 7. Pick a built-in visual style preset
|
|
208
|
+
rf create "美食教程" --style photorealistic
|
|
209
|
+
|
|
210
|
+
# 5. Pipeline form with explicit output path
|
|
211
|
+
rf pipelines standard \
|
|
212
|
+
--script @./script.txt \
|
|
213
|
+
--frame-template 1080x1920/image_default.html \
|
|
214
|
+
-p normal -o smoke.mp4
|
|
215
|
+
|
|
216
|
+
# 6. Inspect existing tasks & redownload a finished video
|
|
217
|
+
rf tasks list --limit 5
|
|
218
|
+
rf history get <task-id> --download recovered.mp4
|
|
219
|
+
|
|
220
|
+
# 7. Atomics for stand-alone use
|
|
221
|
+
rf content scene-plan -t "雨天的玻璃窗" -d 45 --json | jq .scenes
|
|
222
|
+
rf audio transcribe -f narration.mp3 --json | jq '.words[:5]'
|
|
223
|
+
rf subtitles split -t @./narration.txt --min 10 --hard-max 24
|
|
224
|
+
|
|
225
|
+
# 8. JSON pipe for automation
|
|
226
|
+
rf llm presets --json | jq '.[].defaultModel'
|
|
227
|
+
|
|
228
|
+
# 9. Use your own HTML template (no PR/release needed)
|
|
229
|
+
# Any --frame-template that points to a local .html file is read and sent
|
|
230
|
+
# inline. Declare size inside the file via
|
|
231
|
+
# <meta name="template:width" content="1080">
|
|
232
|
+
# <meta name="template:height" content="1920">
|
|
233
|
+
# or pass --frame-template-size 1080x1920.
|
|
234
|
+
rf templates show 1080x1920/image_default.html -o my-brand.html # copy a preset
|
|
235
|
+
# ...edit my-brand.html to suit your style...
|
|
236
|
+
rf templates preview ./my-brand.html --title "Hello" -o preview.png
|
|
237
|
+
rf frames render -t ./my-brand.html --values '{"author":"Alice"}' -o frame.png
|
|
238
|
+
rf pipelines standard -t "宠物" --frame-template ./my-brand.html -o final.mp4
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Custom HTML templates
|
|
242
|
+
|
|
243
|
+
Easiest way to start: grab a preset as a reference.
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
rf templates list # see all keys
|
|
247
|
+
rf templates show 1080x1920/static_default.html # print to stdout
|
|
248
|
+
rf templates show 1080x1920/image_default.html -o my-brand.html # save and edit
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
`{{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).
|
|
252
|
+
|
|
253
|
+
- `{{index}}` — current scene number, 1-based
|
|
254
|
+
- `{{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)
|
|
255
|
+
|
|
256
|
+
#### Template type — does the pipeline generate an AI image per scene?
|
|
257
|
+
|
|
258
|
+
When you ship an inline template through `rf create` / `rf pipelines standard`, ReelForge needs to know whether each scene should kick off image generation. Resolution priority (high → low):
|
|
259
|
+
|
|
260
|
+
1. Explicit flag — `--frame-template-type image|static|asset` (or `frame_template_type` in the API body).
|
|
261
|
+
2. Inside the HTML — `<meta name="template:type" content="image">` (or `static` / `asset`).
|
|
262
|
+
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.
|
|
263
|
+
|
|
264
|
+
The placeholder `{{image}}` no longer doubles as a type signal — declare type explicitly.
|
|
265
|
+
|
|
266
|
+
Limits and safety:
|
|
267
|
+
|
|
268
|
+
- Max 2 MB per inline HTML.
|
|
269
|
+
- 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.
|
|
270
|
+
- 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.
|
|
271
|
+
|
|
272
|
+
#### API field reference
|
|
273
|
+
|
|
274
|
+
| endpoint | inline HTML field | size field | type field |
|
|
275
|
+
|---|---|---|---|
|
|
276
|
+
| `POST /api/v1/frames/render` | `template_html` | `size` | — (n/a, no image generation) |
|
|
277
|
+
| `POST /api/v1/templates/preview` | `template_html` | `size` | — |
|
|
278
|
+
| `POST /api/v1/pipelines/standard` | `frame_template_inline` | `frame_template_size` | `frame_template_type` |
|
|
279
|
+
|
|
280
|
+
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.
|
|
281
|
+
|
|
282
|
+
## Tip — getting unstuck
|
|
283
|
+
|
|
284
|
+
Every level has `--help`:
|
|
285
|
+
|
|
286
|
+
```bash
|
|
287
|
+
rf --help # top-level overview
|
|
288
|
+
rf pipelines --help # list of pipelines
|
|
289
|
+
rf pipelines standard --help # full option reference
|
|
290
|
+
rf tts edge --help # one specific command
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
## License
|
|
294
|
+
|
|
295
|
+
Apache-2.0
|
package/bin/reelforge.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Thin shim so `reelforge` (alias `rf`) is invokable globally. The actual
|
|
3
|
-
// implementation is bundled to dist/ by `npm run build`.
|
|
4
|
-
import("../dist/index.js").catch((err) => {
|
|
5
|
-
// eslint-disable-next-line no-console
|
|
6
|
-
console.error(err?.stack || err);
|
|
7
|
-
process.exit(1);
|
|
8
|
-
});
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Thin shim so `reelforge` (alias `rf`) is invokable globally. The actual
|
|
3
|
+
// implementation is bundled to dist/ by `npm run build`.
|
|
4
|
+
import("../dist/index.js").catch((err) => {
|
|
5
|
+
// eslint-disable-next-line no-console
|
|
6
|
+
console.error(err?.stack || err);
|
|
7
|
+
process.exit(1);
|
|
8
|
+
});
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { post } from "../client.js";
|
|
4
|
+
import { waitForTask } from "../utils/task-waiter.js";
|
|
5
|
+
import { downloadTo } from "../utils/download.js";
|
|
6
|
+
import { formatUsd, humanDuration, info, isJson, print, success } from "../utils/output.js";
|
|
7
|
+
async function loadSpec(raw) {
|
|
8
|
+
let text = raw;
|
|
9
|
+
if (raw.startsWith("@")) {
|
|
10
|
+
text = await fs.readFile(path.resolve(raw.slice(1)), "utf8");
|
|
11
|
+
}
|
|
12
|
+
let parsed;
|
|
13
|
+
try {
|
|
14
|
+
parsed = JSON.parse(text);
|
|
15
|
+
}
|
|
16
|
+
catch (e) {
|
|
17
|
+
throw new Error(`--spec is not valid JSON: ${e instanceof Error ? e.message : e}`);
|
|
18
|
+
}
|
|
19
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
20
|
+
throw new Error("--spec must be a JSON object (the component's render spec)");
|
|
21
|
+
}
|
|
22
|
+
return parsed;
|
|
23
|
+
}
|
|
24
|
+
export function registerClip(program) {
|
|
25
|
+
program
|
|
26
|
+
.command("clip")
|
|
27
|
+
.description("一个 scene 组件 → 独立竖屏 MP4 (可选配音; 给 `rf dub --append` 拼片尾用)")
|
|
28
|
+
.helpOption("-h, --help", "show help")
|
|
29
|
+
.requiredOption("--component <id>", "组件 kind id (如 kinetic-text / quote-card / index-board)")
|
|
30
|
+
.requiredOption("--spec <json>", "组件 spec,内联 JSON 或 @文件路径")
|
|
31
|
+
.option("--theme <id>", "主题皮肤 id (缺省 = 组件所属 profile;general 组件用 general)")
|
|
32
|
+
.option("--narration <text>", "配音文案 (传了才有声音;不传 = 无声片段)")
|
|
33
|
+
.option("--voice <id>", "配音音色 (index-tts-2 音色名, 默认 熊小二)", "熊小二")
|
|
34
|
+
.option("--tts-model <id>", "TTS 模型 (默认 vox/index-tts-2)", "vox/index-tts-2")
|
|
35
|
+
.option("--duration <sec>", "片段时长 (秒);缺省 = 组件自然时长(无声)或配音时长(有声)", (v) => parseFloat(v))
|
|
36
|
+
.option("--subtitles", "烧录字幕 (默认不烧;组件通常自带文字)")
|
|
37
|
+
.option("--subtitle-style <style>", "字幕样式: plate | stroke | cinema (默认 plate)", "plate")
|
|
38
|
+
.option("-o, --output <file>", "输出 MP4 路径 (默认: clip-<组件>.mp4, 放在当前目录)")
|
|
39
|
+
.option("--preview-only", "只生成 preview, 不渲染最终 MP4")
|
|
40
|
+
.option("--poll-ms <ms>", "轮询间隔", (v) => parseInt(v, 10), 1500)
|
|
41
|
+
.option("--timeout-ms <ms>", "最长等待时间", (v) => parseInt(v, 10))
|
|
42
|
+
.addHelpText("after", [
|
|
43
|
+
"",
|
|
44
|
+
"它是什么:",
|
|
45
|
+
" 把单个 scene 组件渲染成一段独立的 1080×1920 竖屏 MP4。通用命令 —— 任何",
|
|
46
|
+
" 想单独跑的组件都走这里;最常见用途是给 `rf dub --append` 做固定片尾(关注卡 /",
|
|
47
|
+
" 赞助板 / 口播尾板),生成一次反复拼,省去每条视频重复 TTS。",
|
|
48
|
+
"",
|
|
49
|
+
"配音 (可选):",
|
|
50
|
+
" · 不传 --narration → 无声片段,时长 = 组件自然时长 (或 --duration)。",
|
|
51
|
+
" · 传 --narration → 熊小二配音 (可 --voice 换),时长 = max(配音, 组件自然时长),",
|
|
52
|
+
" 即配音短也不会截断动画;--subtitles 可另烧字幕。",
|
|
53
|
+
"",
|
|
54
|
+
"组件 (--component) + spec (--spec):",
|
|
55
|
+
" · --component 传组件的 kind id。常用通用组件: kinetic-text(一句大字) /",
|
|
56
|
+
" quote-card(金句) / comparison-vs(对比) / stat-counter(数字) /",
|
|
57
|
+
" bullet-list(要点) / timeline(时间线)。finance / mindset 垂直组件也可,",
|
|
58
|
+
" 会自动套对应主题皮肤 (也可 --theme 指定)。",
|
|
59
|
+
" · --spec 是该组件的渲染字段 (JSON 对象),内联或 @文件。字段随组件而定,",
|
|
60
|
+
" 例: kinetic-text = {text, emphasis};quote-card = {quote, author, context}。",
|
|
61
|
+
"",
|
|
62
|
+
"认证: 需要 API key — `rf login <key>` / $REELFORGE_API_KEY / --api-key。",
|
|
63
|
+
"可用音色: `rf tts voices`。",
|
|
64
|
+
"",
|
|
65
|
+
"示例:",
|
|
66
|
+
" # 无声金句卡 (用组件自然时长)",
|
|
67
|
+
' rf clip --component quote-card --spec \'{"quote":"慢即是快","author":"老张"}\'',
|
|
68
|
+
"",
|
|
69
|
+
" # 带配音的关注片尾,再拼到 dub 后面 (典型用法)",
|
|
70
|
+
' rf clip --component kinetic-text --spec \'{"text":"关注 机械制造师","emphasis":"机械制造师"}\' \\',
|
|
71
|
+
' --narration "关注机械制造师官方账号,每天一个离谱发明" -o outro.mp4',
|
|
72
|
+
" rf dub gameplay.mp4 --append outro.mp4",
|
|
73
|
+
"",
|
|
74
|
+
" # 指定时长 + 字幕",
|
|
75
|
+
' rf clip --component kinetic-text --spec \'{"text":"上车!"}\' --narration "都给我上车" --subtitles --duration 4',
|
|
76
|
+
"",
|
|
77
|
+
" # 财经组件 (spec 从文件读, 自动用 finance 皮肤)",
|
|
78
|
+
" rf clip --component index-board --spec @board.json",
|
|
79
|
+
].join("\n"))
|
|
80
|
+
.action(async (opts) => {
|
|
81
|
+
const spec = await loadSpec(opts.spec);
|
|
82
|
+
const subtitleStyle = (opts.subtitleStyle || "plate").toLowerCase();
|
|
83
|
+
if (!["plate", "stroke", "cinema"].includes(subtitleStyle)) {
|
|
84
|
+
throw new Error(`--subtitle-style must be plate | stroke | cinema (got ${opts.subtitleStyle})`);
|
|
85
|
+
}
|
|
86
|
+
if (opts.duration !== undefined && (!Number.isFinite(opts.duration) || opts.duration <= 0)) {
|
|
87
|
+
throw new Error(`--duration must be a positive number (got ${opts.duration})`);
|
|
88
|
+
}
|
|
89
|
+
const body = {
|
|
90
|
+
component: opts.component,
|
|
91
|
+
spec,
|
|
92
|
+
voice: opts.voice || "熊小二",
|
|
93
|
+
tts_model: opts.ttsModel || "vox/index-tts-2",
|
|
94
|
+
subtitles: opts.subtitles === true,
|
|
95
|
+
subtitle_style: subtitleStyle,
|
|
96
|
+
};
|
|
97
|
+
if (opts.theme)
|
|
98
|
+
body.theme = opts.theme;
|
|
99
|
+
if (opts.narration)
|
|
100
|
+
body.narration = opts.narration;
|
|
101
|
+
if (opts.duration !== undefined)
|
|
102
|
+
body.duration_sec = opts.duration;
|
|
103
|
+
if (opts.previewOnly)
|
|
104
|
+
body.preview_only = true;
|
|
105
|
+
info(`Rendering component "${opts.component}"${opts.narration ? " (voiced)" : " (silent)"}...`);
|
|
106
|
+
const submitted = await post("/api/v1/clip", body);
|
|
107
|
+
info(`Submitted task: ${submitted.task_id} — 渲染中, 请稍候...`);
|
|
108
|
+
const t = await waitForTask(submitted.task_id, { pollMs: opts.pollMs, timeoutMs: opts.timeoutMs });
|
|
109
|
+
if (t.status !== "completed") {
|
|
110
|
+
throw new Error(t.error || `Task ended with status ${t.status}`);
|
|
111
|
+
}
|
|
112
|
+
const result = t.result;
|
|
113
|
+
if (result?.render_pending) {
|
|
114
|
+
info(`MP4 render skipped (--preview-only). Run \`rf render ${t.id}\` to finalize.`);
|
|
115
|
+
}
|
|
116
|
+
else if (result?.video_url) {
|
|
117
|
+
const stdoutIsPipe = !process.stdout.isTTY;
|
|
118
|
+
const skipDownload = stdoutIsPipe && !opts.output;
|
|
119
|
+
let savedPath;
|
|
120
|
+
if (opts.output)
|
|
121
|
+
savedPath = path.resolve(opts.output);
|
|
122
|
+
else if (!skipDownload)
|
|
123
|
+
savedPath = path.resolve(`clip-${opts.component}.mp4`);
|
|
124
|
+
if (savedPath) {
|
|
125
|
+
await downloadTo(result.video_url, savedPath);
|
|
126
|
+
success(`Saved → ${savedPath}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (result?.generation_ms != null) {
|
|
130
|
+
const playback = result.duration != null ? `, ${result.duration.toFixed(1)}s` : "";
|
|
131
|
+
const skin = result.theme ? `, theme ${result.theme}` : "";
|
|
132
|
+
info(`generated in ${humanDuration(result.generation_ms)}${playback}${skin}`);
|
|
133
|
+
}
|
|
134
|
+
if (result?.price_usd != null)
|
|
135
|
+
info(`charge: ${formatUsd(result.price_usd)}`);
|
|
136
|
+
if (isJson())
|
|
137
|
+
print({ task_id: t.id, status: t.status, ...result });
|
|
138
|
+
});
|
|
139
|
+
}
|
package/dist/commands/compose.js
CHANGED
|
@@ -59,35 +59,35 @@ async function readAsDataUri(specPath, baseDir) {
|
|
|
59
59
|
const buf = await fs.readFile(abs);
|
|
60
60
|
return { dataUri: `data:${mime};base64,${buf.toString("base64")}`, bytes: buf.byteLength };
|
|
61
61
|
}
|
|
62
|
-
const EXAMPLE_SPEC_HINT = `{
|
|
63
|
-
"experimental": "compose.v2",
|
|
64
|
-
"audio": { "narration": "./narration.mp3" },
|
|
65
|
-
"scenes": [
|
|
66
|
-
{ "start_sec": 0, "end_sec": 5, "image": "./s1.png", "motion": "zoom-in" },
|
|
67
|
-
{ "start_sec": 5, "end_sec": 11, "video": "./clip.mp4", "muted": true }
|
|
68
|
-
],
|
|
69
|
-
"subtitles": [
|
|
70
|
-
{ "start_sec": 0, "end_sec": 5, "text": "为什么咖啡因让人精神" },
|
|
71
|
-
{ "start_sec": 5, "end_sec": 11, "text": "原来它跟睡眠物质是同一把钥匙" }
|
|
72
|
-
],
|
|
73
|
-
"title": "咖啡因的真相"
|
|
62
|
+
const EXAMPLE_SPEC_HINT = `{
|
|
63
|
+
"experimental": "compose.v2",
|
|
64
|
+
"audio": { "narration": "./narration.mp3" },
|
|
65
|
+
"scenes": [
|
|
66
|
+
{ "start_sec": 0, "end_sec": 5, "image": "./s1.png", "motion": "zoom-in" },
|
|
67
|
+
{ "start_sec": 5, "end_sec": 11, "video": "./clip.mp4", "muted": true }
|
|
68
|
+
],
|
|
69
|
+
"subtitles": [
|
|
70
|
+
{ "start_sec": 0, "end_sec": 5, "text": "为什么咖啡因让人精神" },
|
|
71
|
+
{ "start_sec": 5, "end_sec": 11, "text": "原来它跟睡眠物质是同一把钥匙" }
|
|
72
|
+
],
|
|
73
|
+
"title": "咖啡因的真相"
|
|
74
74
|
}`;
|
|
75
|
-
const EXAMPLE_SPEC_V3_HINT = `{
|
|
76
|
-
"experimental": "compose.v3",
|
|
77
|
-
"audio": { "tts_voice": "熊小二" },
|
|
78
|
-
"scenes": [
|
|
79
|
-
{
|
|
80
|
-
"video": "./s1.mp4", "muted": true,
|
|
81
|
-
"narration": ["今天热得不行", "我跑厕所睡觉"],
|
|
82
|
-
"intro_title": "热到躲厕所"
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
"image": "./splash.jpg",
|
|
86
|
-
"narration": ["主人你找拖鞋去吧"]
|
|
87
|
-
}
|
|
88
|
-
],
|
|
89
|
-
"subtitle_style": "stroke",
|
|
90
|
-
"title": "贼喵被抓现行"
|
|
75
|
+
const EXAMPLE_SPEC_V3_HINT = `{
|
|
76
|
+
"experimental": "compose.v3",
|
|
77
|
+
"audio": { "tts_voice": "熊小二" },
|
|
78
|
+
"scenes": [
|
|
79
|
+
{
|
|
80
|
+
"video": "./s1.mp4", "muted": true,
|
|
81
|
+
"narration": ["今天热得不行", "我跑厕所睡觉"],
|
|
82
|
+
"intro_title": "热到躲厕所"
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"image": "./splash.jpg",
|
|
86
|
+
"narration": ["主人你找拖鞋去吧"]
|
|
87
|
+
}
|
|
88
|
+
],
|
|
89
|
+
"subtitle_style": "stroke",
|
|
90
|
+
"title": "贼喵被抓现行"
|
|
91
91
|
}`;
|
|
92
92
|
export function registerCompose(program) {
|
|
93
93
|
program
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import fsSync from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { post } from "../client.js";
|
|
5
|
+
import { waitForTask } from "../utils/task-waiter.js";
|
|
6
|
+
import { downloadTo } from "../utils/download.js";
|
|
7
|
+
import { formatUsd, humanDuration, info, isJson, print, success } from "../utils/output.js";
|
|
8
|
+
const VIDEO_EXT_TO_MIME = {
|
|
9
|
+
".mp4": "video/mp4",
|
|
10
|
+
".mov": "video/quicktime",
|
|
11
|
+
".webm": "video/webm",
|
|
12
|
+
".mkv": "video/x-matroska",
|
|
13
|
+
".m4v": "video/mp4",
|
|
14
|
+
};
|
|
15
|
+
const MAX_BYTES = 55 * 1024 * 1024;
|
|
16
|
+
function fmtSize(bytes) {
|
|
17
|
+
if (!bytes)
|
|
18
|
+
return "0";
|
|
19
|
+
if (bytes < 1024)
|
|
20
|
+
return `${bytes} B`;
|
|
21
|
+
if (bytes < 1024 * 1024)
|
|
22
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
23
|
+
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
24
|
+
}
|
|
25
|
+
export function registerDub(program) {
|
|
26
|
+
program
|
|
27
|
+
.command("dub <video>")
|
|
28
|
+
.description("⚠️ EXPERIMENTAL · 一个原始视频 → 自动反推内容 + 写解说 + 熊小二配音 + 字幕,输出成片")
|
|
29
|
+
.helpOption("-h, --help", "show help")
|
|
30
|
+
.option("-o, --output <file>", "输出 MP4 路径 (默认: <视频名>-dub.mp4, 放在当前目录)")
|
|
31
|
+
.option("--voice <id>", "配音音色 id (index-tts-2 音色名, 默认 熊小二)", "熊小二")
|
|
32
|
+
.option("--tts-model <id>", "TTS 模型 (默认 vox/index-tts-2)", "vox/index-tts-2")
|
|
33
|
+
.option("--game-volume <n>", "原视频自带声音的音量 0..1 (默认 0.6; 设 0 = 完全静音原声, 只留配音)", (v) => parseFloat(v), 0.6)
|
|
34
|
+
.option("--no-subtitles", "不烧录字幕 (默认烧录)")
|
|
35
|
+
.option("--subtitle-style <style>", "字幕样式: plate | stroke | cinema (默认 plate)", "plate")
|
|
36
|
+
.option("--no-cover", "不加封面 (默认自动生成封面)")
|
|
37
|
+
.option("--cover-template <id>", "封面模板 (默认 paper-band; 见 `rf cover templates`)", "paper-band")
|
|
38
|
+
.option("--cover-title <text>", "封面标题 (默认自动生成 ≤10 字钩子)")
|
|
39
|
+
.option("--cover-subtitle <text>", "封面副标题 (默认无)")
|
|
40
|
+
.option("--cover-badge <text>", "封面角标 (默认无)")
|
|
41
|
+
.option("--append <file>", "在成片末尾拼接一段视频 (如 `rf clip` 出的关注片尾; 可多次传, 按顺序拼)", (val, acc) => {
|
|
42
|
+
acc.push(val);
|
|
43
|
+
return acc;
|
|
44
|
+
}, [])
|
|
45
|
+
.option("--theme <id>", "可选主题 id (影响字幕底板 / 配色风格)")
|
|
46
|
+
.option("--vision-model <id>", "反推用的视觉模型 (默认 qwen/qwen3-vl-flash)")
|
|
47
|
+
.option("--script-model <id>", "写解说脚本用的 LLM (默认服务端配置模型)")
|
|
48
|
+
.option("--preview-only", "只生成 preview, 不渲染最终 MP4 (更快更省, 用于试效果)")
|
|
49
|
+
.option("--poll-ms <ms>", "轮询间隔", (v) => parseInt(v, 10), 1500)
|
|
50
|
+
.option("--timeout-ms <ms>", "最长等待时间", (v) => parseInt(v, 10))
|
|
51
|
+
.addHelpText("after", [
|
|
52
|
+
"",
|
|
53
|
+
"它做了什么 (全部在服务端完成):",
|
|
54
|
+
" 1. 反推视频内容 — 详细、带时间段的分镜 + 屏幕文字",
|
|
55
|
+
" 2. 根据视频时长写一段贴合画面的解说脚本 (卡住时长, 画面演到哪讲到哪)",
|
|
56
|
+
" 3. 用熊小二音色合成配音 (可 --voice 指定其它音色)",
|
|
57
|
+
" 4. 把配音叠到原视频上, 原视频自带声音压到 60% 垫底 (可 --game-volume 调)",
|
|
58
|
+
" 5. 烧录字幕 (复用 compose 的字幕样式 + 切分逻辑; --no-subtitles 关闭)",
|
|
59
|
+
" 6. 自动生成封面 — 选画面最炸的一帧当底图 + LLM 钩子标题, 套 paper-band 模板",
|
|
60
|
+
" (--no-cover 关; --cover-template / --cover-title 自定; 见 `rf cover templates`)",
|
|
61
|
+
" 7. (可选) 在末尾拼接片尾 — --append <clip.mp4>, 可多次, 按顺序拼",
|
|
62
|
+
" (片尾用 `rf clip` 先生成, 比如固定的「关注」引导卡, 生成一次反复用)",
|
|
63
|
+
"",
|
|
64
|
+
"认证:",
|
|
65
|
+
" 需要 API key — `rf login <key>` 或 $REELFORGE_API_KEY 或 --api-key <key>。",
|
|
66
|
+
" 没有 key 服务端会拒绝 (401)。",
|
|
67
|
+
"",
|
|
68
|
+
"支持的输入扩展名: .mp4 .mov .webm .mkv .m4v (单文件 ≤ 55MB)",
|
|
69
|
+
"",
|
|
70
|
+
"可用音色: `rf tts voices` (熊小二 / 激情熊大 / 专业解说 等 149 个)",
|
|
71
|
+
"",
|
|
72
|
+
"示例:",
|
|
73
|
+
" # 最简单: 一个视频进, 配音+字幕成片出 (默认熊小二, 原声 60%)",
|
|
74
|
+
" rf dub ./clip.mp4",
|
|
75
|
+
"",
|
|
76
|
+
" # 指定输出路径 + 换个音色 + 原声调小到 30%",
|
|
77
|
+
" rf dub ./clip.mp4 -o ./out.mp4 --voice 激情熊大 --game-volume 0.3",
|
|
78
|
+
"",
|
|
79
|
+
" # 不要字幕",
|
|
80
|
+
" rf dub ./clip.mp4 --no-subtitles",
|
|
81
|
+
"",
|
|
82
|
+
" # 批量 (PowerShell): 对一个目录里所有 mp4 串行处理",
|
|
83
|
+
" Get-ChildItem *.mp4 | ForEach-Object { rf dub $_.FullName }",
|
|
84
|
+
"",
|
|
85
|
+
" # 先看效果再决定 (preview, 不渲染 MP4)",
|
|
86
|
+
" rf dub ./clip.mp4 --preview-only",
|
|
87
|
+
"",
|
|
88
|
+
" # 配上固定片尾「关注卡」(先用 rf clip 生成一次, 之后反复拼)",
|
|
89
|
+
" rf clip --component kinetic-text --spec '{\"text\":\"关注 机械制造师\"}' \\",
|
|
90
|
+
" --narration \"关注机械制造师官方账号,每天一个离谱发明\" -o outro.mp4",
|
|
91
|
+
" rf dub ./clip.mp4 --append outro.mp4",
|
|
92
|
+
].join("\n"))
|
|
93
|
+
.action(async (videoArg, opts) => {
|
|
94
|
+
const abs = path.resolve(videoArg);
|
|
95
|
+
if (!fsSync.existsSync(abs))
|
|
96
|
+
throw new Error(`video not found: ${abs}`);
|
|
97
|
+
const ext = path.extname(abs).toLowerCase();
|
|
98
|
+
const mime = VIDEO_EXT_TO_MIME[ext];
|
|
99
|
+
if (!mime) {
|
|
100
|
+
throw new Error(`unsupported video extension "${ext}" (supported: ${Object.keys(VIDEO_EXT_TO_MIME).join(" / ")})`);
|
|
101
|
+
}
|
|
102
|
+
const gameVolume = opts.gameVolume ?? 0.6;
|
|
103
|
+
if (!Number.isFinite(gameVolume) || gameVolume < 0 || gameVolume > 1) {
|
|
104
|
+
throw new Error(`--game-volume must be between 0 and 1 (got ${opts.gameVolume})`);
|
|
105
|
+
}
|
|
106
|
+
const subtitleStyle = (opts.subtitleStyle || "plate").toLowerCase();
|
|
107
|
+
if (!["plate", "stroke", "cinema"].includes(subtitleStyle)) {
|
|
108
|
+
throw new Error(`--subtitle-style must be plate | stroke | cinema (got ${opts.subtitleStyle})`);
|
|
109
|
+
}
|
|
110
|
+
const buf = await fs.readFile(abs);
|
|
111
|
+
if (buf.byteLength > MAX_BYTES) {
|
|
112
|
+
throw new Error(`video is ${fmtSize(buf.byteLength)} — exceeds the ${fmtSize(MAX_BYTES)} upload cap. ` +
|
|
113
|
+
`Compress / trim it first (e.g. ffmpeg -crf 28).`);
|
|
114
|
+
}
|
|
115
|
+
info(`Uploading ${path.basename(abs)} (${fmtSize(buf.byteLength)})...`);
|
|
116
|
+
const body = {
|
|
117
|
+
video: `data:${mime};base64,${buf.toString("base64")}`,
|
|
118
|
+
voice: opts.voice || "熊小二",
|
|
119
|
+
tts_model: opts.ttsModel || "vox/index-tts-2",
|
|
120
|
+
game_audio_volume: gameVolume,
|
|
121
|
+
subtitles: opts.subtitles !== false,
|
|
122
|
+
subtitle_style: subtitleStyle,
|
|
123
|
+
cover: opts.cover !== false,
|
|
124
|
+
};
|
|
125
|
+
if (opts.coverTemplate)
|
|
126
|
+
body.cover_template = opts.coverTemplate;
|
|
127
|
+
if (opts.coverTitle)
|
|
128
|
+
body.cover_title = opts.coverTitle;
|
|
129
|
+
if (opts.coverSubtitle)
|
|
130
|
+
body.cover_subtitle = opts.coverSubtitle;
|
|
131
|
+
if (opts.coverBadge)
|
|
132
|
+
body.cover_badge = opts.coverBadge;
|
|
133
|
+
if (opts.theme)
|
|
134
|
+
body.theme = opts.theme;
|
|
135
|
+
const appendFiles = opts.append ?? [];
|
|
136
|
+
if (appendFiles.length) {
|
|
137
|
+
const appendUris = [];
|
|
138
|
+
for (const f of appendFiles) {
|
|
139
|
+
const aAbs = path.resolve(f);
|
|
140
|
+
if (!fsSync.existsSync(aAbs))
|
|
141
|
+
throw new Error(`--append file not found: ${aAbs}`);
|
|
142
|
+
const aExt = path.extname(aAbs).toLowerCase();
|
|
143
|
+
const aMime = VIDEO_EXT_TO_MIME[aExt];
|
|
144
|
+
if (!aMime)
|
|
145
|
+
throw new Error(`--append: unsupported extension "${aExt}" (${path.basename(aAbs)})`);
|
|
146
|
+
const aBuf = await fs.readFile(aAbs);
|
|
147
|
+
if (aBuf.byteLength > MAX_BYTES) {
|
|
148
|
+
throw new Error(`--append clip ${path.basename(aAbs)} is ${fmtSize(aBuf.byteLength)} — exceeds ${fmtSize(MAX_BYTES)}.`);
|
|
149
|
+
}
|
|
150
|
+
info(` + append ${path.basename(aAbs)} (${fmtSize(aBuf.byteLength)})`);
|
|
151
|
+
appendUris.push(`data:${aMime};base64,${aBuf.toString("base64")}`);
|
|
152
|
+
}
|
|
153
|
+
body.append = appendUris;
|
|
154
|
+
}
|
|
155
|
+
if (opts.visionModel)
|
|
156
|
+
body.vision_model = opts.visionModel;
|
|
157
|
+
if (opts.scriptModel)
|
|
158
|
+
body.script_model = opts.scriptModel;
|
|
159
|
+
if (opts.previewOnly)
|
|
160
|
+
body.preview_only = true;
|
|
161
|
+
const submitted = await post("/api/v1/dub", body);
|
|
162
|
+
info(`Submitted task: ${submitted.task_id} — 反推 + 写脚本 + TTS + 渲染, 请稍候...`);
|
|
163
|
+
const t = await waitForTask(submitted.task_id, { pollMs: opts.pollMs, timeoutMs: opts.timeoutMs });
|
|
164
|
+
if (t.status !== "completed") {
|
|
165
|
+
throw new Error(t.error || `Task ended with status ${t.status}`);
|
|
166
|
+
}
|
|
167
|
+
const result = t.result;
|
|
168
|
+
if (result?.script?.length) {
|
|
169
|
+
info(`配音脚本 (${result.voice || opts.voice}):`);
|
|
170
|
+
for (const line of result.script)
|
|
171
|
+
info(` ${line}`);
|
|
172
|
+
}
|
|
173
|
+
if (result?.render_pending) {
|
|
174
|
+
info(`MP4 render skipped (--preview-only). Run \`rf render ${t.id}\` to finalize.`);
|
|
175
|
+
}
|
|
176
|
+
else if (result?.video_url) {
|
|
177
|
+
const stdoutIsPipe = !process.stdout.isTTY;
|
|
178
|
+
const skipDownload = stdoutIsPipe && !opts.output;
|
|
179
|
+
let savedPath;
|
|
180
|
+
if (opts.output) {
|
|
181
|
+
savedPath = path.resolve(opts.output);
|
|
182
|
+
}
|
|
183
|
+
else if (!skipDownload) {
|
|
184
|
+
const stem = path.parse(abs).name;
|
|
185
|
+
savedPath = path.resolve(`${stem}-dub.mp4`);
|
|
186
|
+
}
|
|
187
|
+
if (savedPath) {
|
|
188
|
+
await downloadTo(result.video_url, savedPath);
|
|
189
|
+
success(`Saved → ${savedPath}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (result?.generation_ms != null) {
|
|
193
|
+
const playback = result.duration != null ? `, video ${result.duration.toFixed(1)}s` : "";
|
|
194
|
+
info(`generated in ${humanDuration(result.generation_ms)}${playback}`);
|
|
195
|
+
}
|
|
196
|
+
if (result?.price_usd != null)
|
|
197
|
+
info(`charge: ${formatUsd(result.price_usd)}`);
|
|
198
|
+
if (isJson())
|
|
199
|
+
print({ task_id: t.id, status: t.status, ...result });
|
|
200
|
+
});
|
|
201
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -34,6 +34,8 @@ 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 { registerDub } from "./commands/dub.js";
|
|
38
|
+
import { registerClip } from "./commands/clip.js";
|
|
37
39
|
import { registerVlog } from "./commands/vlog.js";
|
|
38
40
|
import { error as logError } from "./utils/output.js";
|
|
39
41
|
import { ApiCallError } from "./client.js";
|
|
@@ -109,6 +111,7 @@ program.addHelpText("after", [
|
|
|
109
111
|
" rf fetch '<抖音分享链接>' -t 下载抖音 MP4 + ASR 转写",
|
|
110
112
|
" rf cover -i scene.png -t '标题' -o cover.png 单图渲染封面",
|
|
111
113
|
" rf assets describe ./photo.jpg 图片 / 视频 → 一句话描述",
|
|
114
|
+
" rf dub ./clip.mp4 一个视频 → 自动解说配音+字幕成片",
|
|
112
115
|
" rf history list 看之前生成的视频",
|
|
113
116
|
" rf tasks list --status running 看进行中的任务",
|
|
114
117
|
"",
|
|
@@ -146,6 +149,8 @@ registerExtract(program);
|
|
|
146
149
|
registerMedia(program);
|
|
147
150
|
registerScript(program);
|
|
148
151
|
registerCompose(program);
|
|
152
|
+
registerDub(program);
|
|
153
|
+
registerClip(program);
|
|
149
154
|
registerVlog(program);
|
|
150
155
|
async function main() {
|
|
151
156
|
if (process.argv.length <= 2) {
|
package/package.json
CHANGED
|
@@ -1,44 +1,44 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "reelforge",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "AI 视频生成 CLI。一句话主题或自己的脚本 → 自动出抖音/TikTok/视频号竖屏 MP4。安装即用:`reelforge` 或短别名 `rf`。",
|
|
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": ">=22"
|
|
18
|
-
},
|
|
19
|
-
"scripts": {
|
|
20
|
-
"build": "tsc -p tsconfig.json && node scripts/copy-docs.mjs",
|
|
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
|
-
"douyin",
|
|
39
|
-
"tiktok",
|
|
40
|
-
"shorts",
|
|
41
|
-
"vertical-video",
|
|
42
|
-
"cli"
|
|
43
|
-
]
|
|
44
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "reelforge",
|
|
3
|
+
"version": "1.24.0",
|
|
4
|
+
"description": "AI 视频生成 CLI。一句话主题或自己的脚本 → 自动出抖音/TikTok/视频号竖屏 MP4。安装即用:`reelforge` 或短别名 `rf`。",
|
|
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": ">=22"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc -p tsconfig.json && node scripts/copy-docs.mjs",
|
|
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
|
+
"douyin",
|
|
39
|
+
"tiktok",
|
|
40
|
+
"shorts",
|
|
41
|
+
"vertical-video",
|
|
42
|
+
"cli"
|
|
43
|
+
]
|
|
44
|
+
}
|