reelforge 0.5.3 → 0.5.5

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
@@ -95,6 +95,7 @@ Run `rf <command> --help` for full details on any of these.
95
95
  |---|---|
96
96
  | `templates list [--size 1080x1920] [--type image]` | List HTML frame templates |
97
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 |
98
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 |
@@ -160,6 +161,8 @@ rf llm chat -p 'one-sentence summary of antifragile'
160
161
  # <meta name="template:width" content="1080">
161
162
  # <meta name="template:height" content="1920">
162
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...
163
166
  rf templates preview ./my-brand.html --title "Hello" -o preview.png
164
167
  rf frames render -t ./my-brand.html --values '{"author":"Alice"}' -o frame.png
165
168
  rf pipelines standard -t "宠物" --frame-template ./my-brand.html -o final.mp4
@@ -167,14 +170,42 @@ rf pipelines standard -t "宠物" --frame-template ./my-brand.html -o final.mp4
167
170
 
168
171
  ### Custom HTML templates
169
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
+
170
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).
171
182
 
183
+ #### Template type — does the pipeline generate an AI image per scene?
184
+
185
+ When you ship an inline template through `rf create` / `rf pipelines standard`, ReelForge needs to know whether each scene should kick off RelayX image generation. Resolution priority (high → low):
186
+
187
+ 1. Explicit flag — `--frame-template-type image|static|asset` (or `frame_template_type` in the API body).
188
+ 2. Inside the HTML — `<meta name="template:type" content="image">` (or `static` / `asset`).
189
+ 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.
190
+
191
+ The placeholder `{{image}}` no longer doubles as a type signal — declare type explicitly.
192
+
172
193
  Limits and safety:
173
194
 
174
195
  - Max 2 MB per inline HTML.
175
196
  - 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
197
  - 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
198
 
199
+ #### API field reference
200
+
201
+ | endpoint | inline HTML field | size field | type field |
202
+ |---|---|---|---|
203
+ | `POST /api/v1/frames/render` | `template_html` | `size` | — (n/a, no image generation) |
204
+ | `POST /api/v1/templates/preview` | `template_html` | `size` | — |
205
+ | `POST /api/v1/pipelines/standard` | `frame_template_inline` | `frame_template_size` | `frame_template_type` |
206
+
207
+ 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.
208
+
178
209
  ## Tip — getting unstuck
179
210
 
180
211
  Every level has `--help`:
@@ -1,4 +1,5 @@
1
1
  import fs from "node:fs/promises";
2
+ import fsSync from "node:fs";
2
3
  import path from "node:path";
3
4
  import os from "node:os";
4
5
  import { post } from "../client.js";
@@ -13,14 +14,31 @@ function estimateUnits(body) {
13
14
  const mode = body.mode || "generate";
14
15
  const titleExplicit = !!body.title;
15
16
  const N = body.n_scenes ?? 5;
16
- // Template type from filename prefix
17
- const tplKey = body.frame_template || "1080x1920/static_default.html";
18
- const tplBase = (tplKey.split("/").pop() || "").toLowerCase();
19
- const tplType = tplBase.startsWith("static_")
20
- ? "static"
21
- : tplBase.startsWith("asset_")
22
- ? "asset"
23
- : "image";
17
+ // Template type resolution mirrors the server (src/lib/billing.ts):
18
+ // inline HTML explicit body.frame_template_type
19
+ // → <meta name="template:type" content="..."> in the HTML
20
+ // → default "image"
21
+ // preset key → parsed from the filename prefix (static_/asset_/image_)
22
+ let tplType;
23
+ if (body.frame_template_inline) {
24
+ if (body.frame_template_type) {
25
+ tplType = body.frame_template_type;
26
+ }
27
+ else {
28
+ const m = body.frame_template_inline.match(/<meta[^>]+name=["']template:type["'][^>]+content=["']([a-z]+)["']/i);
29
+ const v = m?.[1].toLowerCase();
30
+ tplType = v === "static" || v === "asset" || v === "image" ? v : "image";
31
+ }
32
+ }
33
+ else {
34
+ const tplKey = body.frame_template || "1080x1920/static_default.html";
35
+ const tplBase = (tplKey.split("/").pop() || "").toLowerCase();
36
+ tplType = tplBase.startsWith("static_")
37
+ ? "static"
38
+ : tplBase.startsWith("asset_")
39
+ ? "asset"
40
+ : "image";
41
+ }
24
42
  const mediaPerFrame = tplType === "image" ? IMAGE_UNITS : 0;
25
43
  const ttsMode = body.tts_inference_mode || "edge";
26
44
  const ttsPerFrame = ttsMode === "relayx" ? TTS_RELAYX_UNITS : 0;
@@ -30,6 +48,22 @@ function estimateUnits(body) {
30
48
  return narrations + title + imagePrompts + N * (ttsPerFrame + mediaPerFrame);
31
49
  }
32
50
  // ── Helpers ─────────────────────────────────────────────────────
51
+ /**
52
+ * Distinguish a local HTML file path from a preset template key.
53
+ * Preset keys look like `"<size>/<file>.html"` (one slash, no dots/slashes
54
+ * outside that pattern). Anything starting with `./`, `../`, `/`, `~`, or
55
+ * containing a backslash, or that ends with `.html` and exists on disk, is
56
+ * treated as a local path.
57
+ */
58
+ function looksLikeLocalHtmlPath(value) {
59
+ if (/^[.~]|^\//.test(value))
60
+ return true;
61
+ if (value.includes("\\"))
62
+ return true;
63
+ if (value.endsWith(".html") && fsSync.existsSync(value))
64
+ return true;
65
+ return false;
66
+ }
33
67
  async function resolveText(input) {
34
68
  if (input.startsWith("@")) {
35
69
  const file = input.slice(1);
@@ -138,8 +172,23 @@ function optsToBody(opts) {
138
172
  out.tts_speed = opts.ttsSpeed;
139
173
  if (opts.imageModel !== undefined)
140
174
  out.image_model = opts.imageModel;
141
- if (opts.frameTemplate !== undefined)
142
- out.frame_template = opts.frameTemplate;
175
+ if (opts.frameTemplate !== undefined) {
176
+ // Local .html path → read and send as inline; preset key → send as-is.
177
+ if (looksLikeLocalHtmlPath(opts.frameTemplate)) {
178
+ const abs = path.resolve(opts.frameTemplate);
179
+ if (!fsSync.existsSync(abs)) {
180
+ throw new Error(`--frame-template: local file not found: ${abs}`);
181
+ }
182
+ out.frame_template_inline = fsSync.readFileSync(abs, "utf-8");
183
+ }
184
+ else {
185
+ out.frame_template = opts.frameTemplate;
186
+ }
187
+ }
188
+ if (opts.frameTemplateSize !== undefined)
189
+ out.frame_template_size = opts.frameTemplateSize;
190
+ if (opts.frameTemplateType !== undefined)
191
+ out.frame_template_type = opts.frameTemplateType;
143
192
  if (opts.promptPrefix !== undefined)
144
193
  out.prompt_prefix = opts.promptPrefix;
145
194
  if (opts.bgm !== undefined)
@@ -295,7 +344,9 @@ export function registerCreate(program) {
295
344
  .option("--min-image-prompt-words <N>", "image prompt min words", (v) => parseInt(v, 10))
296
345
  .option("--max-image-prompt-words <N>", "image prompt max words", (v) => parseInt(v, 10))
297
346
  // --- Visual ---
298
- .option("--frame-template <key>", "HTML frame template, e.g. 1080x1920/image_default.html")
347
+ .option("--frame-template <keyOrPath>", "HTML frame template: preset key (e.g. 1080x1920/image_default.html) OR path to a local .html (auto-sent inline)")
348
+ .option("--frame-template-size <wxh>", "size for inline HTML when the file lacks <meta template:width|height>, e.g. 1080x1920")
349
+ .option("--frame-template-type <type>", "inline template type: image (default) | static | asset. Controls whether AI image generation runs per frame. Can also be set via <meta name=\"template:type\" content=\"...\"> in the HTML.")
299
350
  .option("--image-model <id>", "RelayX image model (rx-image-z | rx-image-flux | rx-image-qwen)")
300
351
  .option("--prompt-prefix <text>", "raw style prefix prepended to every image prompt (overrides --style)")
301
352
  .option("--style <preset>", "image style preset — shortcut for --prompt-prefix; see 'Style presets' below for the full list")
@@ -383,6 +434,12 @@ export function registerCreate(program) {
383
434
  " # Free-form style — write your own prefix from scratch",
384
435
  ' rf create "..." --prompt-prefix "Studio Ghibli, pastel, dreamy"',
385
436
  "",
437
+ " # Custom HTML template (auto-detected when --frame-template is a local path)",
438
+ " rf create '...' --frame-template ./my-brand.html",
439
+ " # ↳ default type=image (best-practice; AI image generated per scene).",
440
+ " # ↳ pure-text template? declare `--frame-template-type static`",
441
+ " # OR add `<meta name=\"template:type\" content=\"static\">` inside the HTML.",
442
+ "",
386
443
  " # Full recipe in one file",
387
444
  " rf create --recipe ./space.recipe.json",
388
445
  "",
@@ -470,6 +527,12 @@ export function registerCreate(program) {
470
527
  ...body,
471
528
  text: body.text,
472
529
  };
530
+ // When the user supplied inline HTML, the DEFAULTS' `frame_template`
531
+ // key is irrelevant — drop it so the server-side request body stays
532
+ // clean and the dry-run output isn't misleading.
533
+ if (finalBody.frame_template_inline && finalBody.frame_template) {
534
+ delete finalBody.frame_template;
535
+ }
473
536
  // 3. Estimate cost
474
537
  const estimate = estimateUnits(finalBody);
475
538
  // 4. Dry-run: print & exit
@@ -50,6 +50,7 @@ export function registerPipelines(program) {
50
50
  .option("--split-mode <mode>", "paragraph | line | sentence (mode=fixed)", "paragraph")
51
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
52
  .option("--frame-template-size <wxh>", "size for inline HTML when the file lacks <meta template:width|height>")
53
+ .option("--frame-template-type <type>", "inline type: image (default) | static | asset. Or set <meta name=\"template:type\"> in the HTML.")
53
54
  .option("--image-model <id>", "RelayX image model (rx-image-z | rx-image-flux | rx-image-qwen) — only when template requires AI images")
54
55
  .option("--prompt-prefix <text>", "style prefix prepended to image prompts")
55
56
  .option("--tts-voice <id>", "Edge TTS voice", "zh-CN-YunjianNeural")
@@ -78,6 +79,7 @@ export function registerPipelines(program) {
78
79
  n_scenes: opts.nScenes,
79
80
  split_mode: opts.splitMode,
80
81
  ...tpl,
82
+ frame_template_type: opts.frameTemplateType,
81
83
  image_model: opts.imageModel,
82
84
  prompt_prefix: opts.promptPrefix,
83
85
  tts_voice: opts.ttsVoice,
@@ -1,3 +1,7 @@
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
7
  import { print, success, table } from "../utils/output.js";
@@ -58,4 +62,32 @@ export function registerTemplates(program) {
58
62
  }
59
63
  print(r);
60
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
+ });
61
93
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reelforge",
3
- "version": "0.5.3",
3
+ "version": "0.5.5",
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",