reelforge 0.5.2 → 0.5.3
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 +22 -2
- package/dist/commands/frames.js +52 -3
- package/dist/commands/pipelines.js +21 -2
- package/dist/commands/templates.js +17 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -94,8 +94,8 @@ Run `rf <command> --help` for full details on any of these.
|
|
|
94
94
|
| command | what it does |
|
|
95
95
|
|---|---|
|
|
96
96
|
| `templates list [--size 1080x1920] [--type image]` | List HTML frame templates |
|
|
97
|
-
| `templates preview <
|
|
98
|
-
| `frames render -t <
|
|
97
|
+
| `templates preview <keyOrPath> [-o out.png]` | Render a preview from a preset key **or your own local .html file** |
|
|
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
99
|
| `compositions concat <v1> <v2> -o out.mp4` | FFmpeg concat (+ optional BGM) |
|
|
100
100
|
| `compositions bgm -i video.mp4 --bgm bgm.mp3 -o out.mp4` | Add background music |
|
|
101
101
|
| `compositions image-to-video -i img.png -a aud.mp3 -o out.mp4` | Build video from image + audio |
|
|
@@ -153,8 +153,28 @@ rf config set llm.api_key rx-xxxxx # RelayX key (or your own provider k
|
|
|
153
153
|
rf config set llm.base_url https://relayx.timor419.com/v1
|
|
154
154
|
rf config set llm.model anthropic/claude-4-7-sonnet
|
|
155
155
|
rf llm chat -p 'one-sentence summary of antifragile'
|
|
156
|
+
|
|
157
|
+
# 6. Use your own HTML template (no PR/release needed)
|
|
158
|
+
# Any of -t / --frame-template that points to a local .html file is read and
|
|
159
|
+
# sent inline. Declare size inside the file via
|
|
160
|
+
# <meta name="template:width" content="1080">
|
|
161
|
+
# <meta name="template:height" content="1920">
|
|
162
|
+
# or pass --size 1080x1920 on the CLI.
|
|
163
|
+
rf templates preview ./my-brand.html --title "Hello" -o preview.png
|
|
164
|
+
rf frames render -t ./my-brand.html --values '{"author":"Alice"}' -o frame.png
|
|
165
|
+
rf pipelines standard -t "宠物" --frame-template ./my-brand.html -o final.mp4
|
|
156
166
|
```
|
|
157
167
|
|
|
168
|
+
### Custom HTML templates
|
|
169
|
+
|
|
170
|
+
`{{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).
|
|
171
|
+
|
|
172
|
+
Limits and safety:
|
|
173
|
+
|
|
174
|
+
- Max 2 MB per inline HTML.
|
|
175
|
+
- 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.
|
|
176
|
+
- 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.
|
|
177
|
+
|
|
158
178
|
## Tip — getting unstuck
|
|
159
179
|
|
|
160
180
|
Every level has `--help`:
|
package/dist/commands/frames.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* reelforge frames render
|
|
3
|
+
*/
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
1
6
|
import { post } from "../client.js";
|
|
2
7
|
import { downloadTo } from "../utils/download.js";
|
|
3
8
|
import { print, success } from "../utils/output.js";
|
|
@@ -8,15 +13,30 @@ export function registerFrames(program) {
|
|
|
8
13
|
.helpOption("-h, --help", "show help");
|
|
9
14
|
frames
|
|
10
15
|
.command("render")
|
|
11
|
-
.description("Render one frame with custom values")
|
|
16
|
+
.description("Render one frame with custom values (preset key or your own local .html)")
|
|
12
17
|
.helpOption("-h, --help", "show help")
|
|
13
|
-
.requiredOption("-t, --template <
|
|
18
|
+
.requiredOption("-t, --template <keyOrPath>", "template key (e.g. 1080x1920/image_default.html) OR path to a local .html file")
|
|
19
|
+
.option("--size <wxh>", "size for inline HTML when the file lacks <meta template:width|height>, e.g. 1080x1920")
|
|
14
20
|
.option("--title <text>", "title placeholder", "")
|
|
15
21
|
.option("--text <text>", "text placeholder", "")
|
|
16
22
|
.option("--image <pathOrUrl>", "image placeholder")
|
|
17
23
|
.option("--index <n>", "frame index (1-based)", parseInt, 1)
|
|
18
24
|
.option("--values <json>", "JSON string with extra placeholder values, e.g. '{\"author\":\"Alice\"}'")
|
|
19
25
|
.option("-o, --output <file>", "save the rendered PNG to this path")
|
|
26
|
+
.addHelpText("after", [
|
|
27
|
+
"",
|
|
28
|
+
"Examples:",
|
|
29
|
+
" # preset",
|
|
30
|
+
" rf frames render -t 1080x1920/image_default.html --title T --text X -o out.png",
|
|
31
|
+
"",
|
|
32
|
+
" # custom local HTML (declare size via <meta template:width|height> or --size)",
|
|
33
|
+
" rf frames render -t ./my-brand.html --size 1080x1920 \\",
|
|
34
|
+
" --title 'Hello' --text 'world' --values '{\"author\":\"Alice\"}' -o out.png",
|
|
35
|
+
"",
|
|
36
|
+
" Built-in placeholders: {{title}} {{text}} {{image}} {{index}}",
|
|
37
|
+
" Custom params: {{name:type=default}} — type ∈ text|number|color|bool",
|
|
38
|
+
" Limits: HTML ≤ 2 MB; sandbox blocks file://, intranet IPs, cloud-metadata.",
|
|
39
|
+
].join("\n"))
|
|
20
40
|
.action(async (opts) => {
|
|
21
41
|
const extra = opts.values ? JSON.parse(opts.values) : {};
|
|
22
42
|
const values = {
|
|
@@ -26,7 +46,8 @@ export function registerFrames(program) {
|
|
|
26
46
|
index: opts.index,
|
|
27
47
|
...extra,
|
|
28
48
|
};
|
|
29
|
-
const
|
|
49
|
+
const payload = buildTemplatePayload(opts.template, opts.size);
|
|
50
|
+
const r = await post("/api/v1/frames/render", { ...payload, values });
|
|
30
51
|
if (opts.output) {
|
|
31
52
|
await downloadTo(r.url, opts.output);
|
|
32
53
|
success(`Saved → ${opts.output} (${r.width}x${r.height})`);
|
|
@@ -34,3 +55,31 @@ export function registerFrames(program) {
|
|
|
34
55
|
print(r);
|
|
35
56
|
});
|
|
36
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Resolve `-t` into the right request fields:
|
|
60
|
+
* - local file path → read HTML, send as `template_html`
|
|
61
|
+
* - preset key (size/file.html) → send as `template`
|
|
62
|
+
*/
|
|
63
|
+
export function buildTemplatePayload(value, size) {
|
|
64
|
+
if (looksLikeLocalPath(value)) {
|
|
65
|
+
const abs = path.resolve(value);
|
|
66
|
+
if (!fs.existsSync(abs)) {
|
|
67
|
+
throw new Error(`Local template not found: ${abs}`);
|
|
68
|
+
}
|
|
69
|
+
const html = fs.readFileSync(abs, "utf-8");
|
|
70
|
+
return size ? { template_html: html, size } : { template_html: html };
|
|
71
|
+
}
|
|
72
|
+
return { template: value };
|
|
73
|
+
}
|
|
74
|
+
function looksLikeLocalPath(value) {
|
|
75
|
+
// Preset keys are always exactly "<size>/<file.html>" (one slash, no traversal).
|
|
76
|
+
// Anything that starts with ./, ../, /, ~ or contains a backslash is a path.
|
|
77
|
+
// Also: if it ends with .html AND the file exists on disk, treat as path.
|
|
78
|
+
if (/^[.~]|^\//.test(value))
|
|
79
|
+
return true;
|
|
80
|
+
if (value.includes("\\"))
|
|
81
|
+
return true;
|
|
82
|
+
if (value.endsWith(".html") && fs.existsSync(value))
|
|
83
|
+
return true;
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
|
+
import fsSync from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
2
4
|
import { post } from "../client.js";
|
|
3
5
|
import { waitForTask } from "../utils/task-waiter.js";
|
|
4
6
|
import { downloadTo } from "../utils/download.js";
|
|
@@ -46,7 +48,8 @@ export function registerPipelines(program) {
|
|
|
46
48
|
.option("--title <text>", "explicit video title (skip LLM title gen)")
|
|
47
49
|
.option("-n, --n-scenes <n>", "number of scenes (mode=generate)", parseInt, 5)
|
|
48
50
|
.option("--split-mode <mode>", "paragraph | line | sentence (mode=fixed)", "paragraph")
|
|
49
|
-
.option("--frame-template <
|
|
51
|
+
.option("--frame-template <keyOrPath>", "preset key (e.g. 1080x1920/static_default.html) OR path to a local .html file", "1080x1920/static_default.html")
|
|
52
|
+
.option("--frame-template-size <wxh>", "size for inline HTML when the file lacks <meta template:width|height>")
|
|
50
53
|
.option("--image-model <id>", "RelayX image model (rx-image-z | rx-image-flux | rx-image-qwen) — only when template requires AI images")
|
|
51
54
|
.option("--prompt-prefix <text>", "style prefix prepended to image prompts")
|
|
52
55
|
.option("--tts-voice <id>", "Edge TTS voice", "zh-CN-YunjianNeural")
|
|
@@ -59,17 +62,22 @@ export function registerPipelines(program) {
|
|
|
59
62
|
" reelforge pipelines standard -t 'why we explore space' -n 5 -o space.mp4",
|
|
60
63
|
" reelforge pipelines standard -t @script.txt --mode fixed --split-mode paragraph --title 'My Show' -o out.mp4",
|
|
61
64
|
" reelforge pipelines standard -t '宠物' --frame-template 1080x1920/image_default.html --image-model rx-image-flux --prompt-prefix 'cinematic'",
|
|
65
|
+
"",
|
|
66
|
+
" Custom HTML template (sent inline; no upload needed):",
|
|
67
|
+
" reelforge pipelines standard -t '宠物' --frame-template ./my-brand.html -o final.mp4",
|
|
68
|
+
" (declare size via <meta name=\"template:width|height\"> or pass --frame-template-size 1080x1920)",
|
|
62
69
|
].join("\n"))).action(async (opts) => {
|
|
63
70
|
let text = opts.text;
|
|
64
71
|
if (text.startsWith("@"))
|
|
65
72
|
text = await fs.readFile(text.slice(1), "utf-8");
|
|
73
|
+
const tpl = resolveTemplateArg(opts.frameTemplate, opts.frameTemplateSize);
|
|
66
74
|
await submitAndMaybeWait("/api/v1/pipelines/standard", {
|
|
67
75
|
text,
|
|
68
76
|
mode: opts.mode,
|
|
69
77
|
title: opts.title,
|
|
70
78
|
n_scenes: opts.nScenes,
|
|
71
79
|
split_mode: opts.splitMode,
|
|
72
|
-
|
|
80
|
+
...tpl,
|
|
73
81
|
image_model: opts.imageModel,
|
|
74
82
|
prompt_prefix: opts.promptPrefix,
|
|
75
83
|
tts_voice: opts.ttsVoice,
|
|
@@ -79,3 +87,14 @@ export function registerPipelines(program) {
|
|
|
79
87
|
}, { wait: opts.wait, output: opts.output, pollMs: opts.pollMs, timeoutMs: opts.timeoutMs });
|
|
80
88
|
});
|
|
81
89
|
}
|
|
90
|
+
function resolveTemplateArg(value, size) {
|
|
91
|
+
if (/^[.~]|^\//.test(value) || value.includes("\\") || (value.endsWith(".html") && fsSync.existsSync(value))) {
|
|
92
|
+
const abs = path.resolve(value);
|
|
93
|
+
if (!fsSync.existsSync(abs)) {
|
|
94
|
+
throw new Error(`Local template not found: ${abs}`);
|
|
95
|
+
}
|
|
96
|
+
const html = fsSync.readFileSync(abs, "utf-8");
|
|
97
|
+
return size ? { frame_template_inline: html, frame_template_size: size } : { frame_template_inline: html };
|
|
98
|
+
}
|
|
99
|
+
return { frame_template: value };
|
|
100
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { get, post } from "../client.js";
|
|
2
2
|
import { downloadTo } from "../utils/download.js";
|
|
3
|
-
import { print,
|
|
3
|
+
import { print, success, table } from "../utils/output.js";
|
|
4
|
+
import { buildTemplatePayload } from "./frames.js";
|
|
4
5
|
export function registerTemplates(program) {
|
|
5
6
|
const tpl = program
|
|
6
7
|
.command("templates")
|
|
@@ -28,16 +29,27 @@ export function registerTemplates(program) {
|
|
|
28
29
|
})));
|
|
29
30
|
});
|
|
30
31
|
tpl
|
|
31
|
-
.command("preview <
|
|
32
|
-
.description("Render a preview frame from a
|
|
32
|
+
.command("preview <keyOrPath>")
|
|
33
|
+
.description("Render a preview frame from a preset key (e.g. 1080x1920/static_default.html) or a local .html file path")
|
|
33
34
|
.helpOption("-h, --help", "show help")
|
|
35
|
+
.option("--size <wxh>", "size for inline HTML when the file lacks <meta template:width|height>")
|
|
34
36
|
.option("--title <text>", "title placeholder", "示例标题")
|
|
35
37
|
.option("--text <text>", "text placeholder", "示例字幕,用于预览模板效果。")
|
|
36
38
|
.option("--image <pathOrUrl>", "image placeholder")
|
|
37
39
|
.option("-o, --output <file>", "save preview PNG to this path")
|
|
38
|
-
.
|
|
40
|
+
.addHelpText("after", [
|
|
41
|
+
"",
|
|
42
|
+
"Examples:",
|
|
43
|
+
" rf templates preview 1080x1920/static_default.html -o p.png",
|
|
44
|
+
" rf templates preview ./my-brand.html --size 1080x1920 -o p.png",
|
|
45
|
+
"",
|
|
46
|
+
" Local .html: declare size via <meta name=\"template:width|height\"> or pass --size.",
|
|
47
|
+
" HTML ≤ 2 MB; sandbox blocks file://, intranet IPs, cloud-metadata.",
|
|
48
|
+
].join("\n"))
|
|
49
|
+
.action(async (keyOrPath, opts) => {
|
|
50
|
+
const payload = buildTemplatePayload(keyOrPath, opts.size);
|
|
39
51
|
const r = await post("/api/v1/templates/preview", {
|
|
40
|
-
|
|
52
|
+
...payload,
|
|
41
53
|
values: { title: opts.title, text: opts.text, image: opts.image || "", index: 1 },
|
|
42
54
|
});
|
|
43
55
|
if (opts.output) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reelforge",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
4
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
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|