reelforge 0.5.1 → 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 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 <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
+ | `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`:
@@ -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,7 @@
1
1
  import { get, post } from "../client.js";
2
2
  import { downloadTo } from "../utils/download.js";
3
- import { print, table, success } from "../utils/output.js";
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 <key>")
32
- .description("Render a preview frame from a template (e.g. 1080x1920/static_default.html)")
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
- .action(async (key, opts) => {
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
- template: key,
52
+ ...payload,
41
53
  values: { title: opts.title, text: opts.text, image: opts.image || "", index: 1 },
42
54
  });
43
55
  if (opts.output) {
@@ -67,14 +67,45 @@ export function registerTts(program) {
67
67
  });
68
68
  tts
69
69
  .command("voices")
70
- .description("List supported Edge-TTS voices (id, label, locale, gender)")
70
+ .description("List TTS voices: Edge (built-in 25) or RelayX (live, per-model)")
71
71
  .helpOption("-h, --help", "show help")
72
- .option("--locale <prefix>", "filter by locale prefix, e.g. zh, en-US")
72
+ .option("--provider <p>", "edge (default) | relayx", "edge")
73
+ .option("--model <id>", "RelayX TTS model id (required when --provider=relayx), e.g. vox/index-tts-2")
74
+ .option("--locale <prefix>", "Edge only: filter by locale prefix, e.g. zh, en-US")
75
+ .option("--refresh", "RelayX only: bypass the 5-minute server-side cache")
76
+ .addHelpText("after", [
77
+ "",
78
+ "Examples:",
79
+ " reelforge tts voices # Edge 25 built-in",
80
+ " reelforge tts voices --locale zh # Edge filtered",
81
+ " reelforge tts voices --provider relayx --model vox/index-tts-2 # 149 vox voices",
82
+ " reelforge tts voices --provider relayx --model minimax/speech-2.6-hd",
83
+ ].join("\n"))
73
84
  .action(async (opts) => {
74
- const r = await get("/api/v1/tts/voices");
75
- let voices = r.voices;
76
- if (opts.locale)
77
- voices = voices.filter((v) => v.locale.startsWith(opts.locale));
78
- table(voices);
85
+ const provider = opts.provider || "edge";
86
+ if (provider === "edge") {
87
+ const r = await get("/api/v1/tts/voices?provider=edge");
88
+ let voices = r.voices;
89
+ if (opts.locale)
90
+ voices = voices.filter((v) => v.locale.startsWith(opts.locale));
91
+ table(voices);
92
+ return;
93
+ }
94
+ if (provider === "relayx") {
95
+ if (!opts.model)
96
+ throw new Error("--model is required when --provider=relayx (e.g. vox/index-tts-2)");
97
+ const qs = new URLSearchParams({ provider: "relayx", model: opts.model });
98
+ if (opts.refresh)
99
+ qs.set("refresh", "1");
100
+ const r = await get(`/api/v1/tts/voices?${qs.toString()}`);
101
+ table(r.voices.map((v) => ({
102
+ id: v.id,
103
+ label: v.label,
104
+ featured: v.featured ? "★" : "",
105
+ demo_url: v.demo_url ?? "",
106
+ })));
107
+ return;
108
+ }
109
+ throw new Error(`Unknown --provider: ${provider} (expected "edge" or "relayx")`);
79
110
  });
80
111
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reelforge",
3
- "version": "0.5.1",
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",