reelforge 0.5.2 → 0.5.4

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 CHANGED
@@ -94,8 +94,9 @@ 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 <key> [-o out.png]` | Render a template preview |
98
- | `frames render -t <key> --title ... --text ...` | Render a single composed frame to PNG |
97
+ | `templates preview <keyOrPath> [-o out.png]` | Render a preview from a preset key **or your own local .html file** |
98
+ | `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 |
99
+ | `frames render -t <keyOrPath> --title ... --text ...` | Render a single composed frame to PNG. `-t` accepts a preset key **or a local .html path** |
99
100
  | `compositions concat <v1> <v2> -o out.mp4` | FFmpeg concat (+ optional BGM) |
100
101
  | `compositions bgm -i video.mp4 --bgm bgm.mp3 -o out.mp4` | Add background music |
101
102
  | `compositions image-to-video -i img.png -a aud.mp3 -o out.mp4` | Build video from image + audio |
@@ -153,8 +154,38 @@ rf config set llm.api_key rx-xxxxx # RelayX key (or your own provider k
153
154
  rf config set llm.base_url https://relayx.timor419.com/v1
154
155
  rf config set llm.model anthropic/claude-4-7-sonnet
155
156
  rf llm chat -p 'one-sentence summary of antifragile'
157
+
158
+ # 6. Use your own HTML template (no PR/release needed)
159
+ # Any of -t / --frame-template that points to a local .html file is read and
160
+ # sent inline. Declare size inside the file via
161
+ # <meta name="template:width" content="1080">
162
+ # <meta name="template:height" content="1920">
163
+ # or pass --size 1080x1920 on the CLI.
164
+ rf templates show 1080x1920/image_default.html -o my-brand.html # copy a preset
165
+ # ...edit my-brand.html to suit your style...
166
+ rf templates preview ./my-brand.html --title "Hello" -o preview.png
167
+ rf frames render -t ./my-brand.html --values '{"author":"Alice"}' -o frame.png
168
+ rf pipelines standard -t "宠物" --frame-template ./my-brand.html -o final.mp4
156
169
  ```
157
170
 
171
+ ### Custom HTML templates
172
+
173
+ Easiest way to start: grab a preset as a reference.
174
+
175
+ ```bash
176
+ rf templates list # see all keys
177
+ rf templates show 1080x1920/static_default.html # print to stdout
178
+ rf templates show 1080x1920/image_default.html -o my-brand.html # save and edit
179
+ ```
180
+
181
+ `{{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).
182
+
183
+ Limits and safety:
184
+
185
+ - Max 2 MB per inline HTML.
186
+ - 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.
187
+ - 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.
188
+
158
189
  ## Tip — getting unstuck
159
190
 
160
191
  Every level has `--help`:
@@ -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 <key>", "template key, e.g. 1080x1920/image_default.html")
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 r = await post("/api/v1/frames/render", { template: opts.template, values });
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 <key>", "template, e.g. 1080x1920/static_default.html", "1080x1920/static_default.html")
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
- frame_template: opts.frameTemplate,
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,11 @@
1
+ /**
2
+ * reelforge templates <list|preview|show>
3
+ */
4
+ import fs from "node:fs/promises";
1
5
  import { get, post } from "../client.js";
2
6
  import { downloadTo } from "../utils/download.js";
3
- import { print, table, success } from "../utils/output.js";
7
+ import { print, success, table } from "../utils/output.js";
8
+ import { buildTemplatePayload } from "./frames.js";
4
9
  export function registerTemplates(program) {
5
10
  const tpl = program
6
11
  .command("templates")
@@ -28,16 +33,27 @@ export function registerTemplates(program) {
28
33
  })));
29
34
  });
30
35
  tpl
31
- .command("preview <key>")
32
- .description("Render a preview frame from a template (e.g. 1080x1920/static_default.html)")
36
+ .command("preview <keyOrPath>")
37
+ .description("Render a preview frame from a preset key (e.g. 1080x1920/static_default.html) or a local .html file path")
33
38
  .helpOption("-h, --help", "show help")
39
+ .option("--size <wxh>", "size for inline HTML when the file lacks <meta template:width|height>")
34
40
  .option("--title <text>", "title placeholder", "示例标题")
35
41
  .option("--text <text>", "text placeholder", "示例字幕,用于预览模板效果。")
36
42
  .option("--image <pathOrUrl>", "image placeholder")
37
43
  .option("-o, --output <file>", "save preview PNG to this path")
38
- .action(async (key, opts) => {
44
+ .addHelpText("after", [
45
+ "",
46
+ "Examples:",
47
+ " rf templates preview 1080x1920/static_default.html -o p.png",
48
+ " rf templates preview ./my-brand.html --size 1080x1920 -o p.png",
49
+ "",
50
+ " Local .html: declare size via <meta name=\"template:width|height\"> or pass --size.",
51
+ " HTML ≤ 2 MB; sandbox blocks file://, intranet IPs, cloud-metadata.",
52
+ ].join("\n"))
53
+ .action(async (keyOrPath, opts) => {
54
+ const payload = buildTemplatePayload(keyOrPath, opts.size);
39
55
  const r = await post("/api/v1/templates/preview", {
40
- template: key,
56
+ ...payload,
41
57
  values: { title: opts.title, text: opts.text, image: opts.image || "", index: 1 },
42
58
  });
43
59
  if (opts.output) {
@@ -46,4 +62,32 @@ export function registerTemplates(program) {
46
62
  }
47
63
  print(r);
48
64
  });
65
+ tpl
66
+ .command("show <key>")
67
+ .description("Print or save the source HTML of a preset template — use as a starting point for your own")
68
+ .helpOption("-h, --help", "show help")
69
+ .option("-o, --output <file>", "save HTML to this path (otherwise print to stdout)")
70
+ .addHelpText("after", [
71
+ "",
72
+ "Examples:",
73
+ " # see what the default static template looks like",
74
+ " rf templates show 1080x1920/static_default.html",
75
+ "",
76
+ " # copy a preset and tweak it as your own brand template",
77
+ " rf templates show 1080x1920/image_default.html -o my-brand.html",
78
+ " # ...edit my-brand.html...",
79
+ " rf frames render -t ./my-brand.html --title 'X' -o frame.png",
80
+ "",
81
+ " Get the list of keys via `rf templates list`.",
82
+ ].join("\n"))
83
+ .action(async (key, opts) => {
84
+ const r = await get(`/api/v1/templates/source?key=${encodeURIComponent(key)}`);
85
+ if (opts.output) {
86
+ await fs.writeFile(opts.output, r.html, "utf-8");
87
+ success(`Saved → ${opts.output} (${r.size}, ${r.type}, ${r.params.length} custom params)`);
88
+ }
89
+ else {
90
+ process.stdout.write(r.html);
91
+ }
92
+ });
49
93
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reelforge",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
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",