reelforge 0.5.4 → 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 +20 -0
- package/dist/commands/create.js +74 -11
- package/dist/commands/pipelines.js +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -180,12 +180,32 @@ rf templates show 1080x1920/image_default.html -o my-brand.html # save and ed
|
|
|
180
180
|
|
|
181
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
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
|
+
|
|
183
193
|
Limits and safety:
|
|
184
194
|
|
|
185
195
|
- Max 2 MB per inline HTML.
|
|
186
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.
|
|
187
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.
|
|
188
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
|
+
|
|
189
209
|
## Tip — getting unstuck
|
|
190
210
|
|
|
191
211
|
Every level has `--help`:
|
package/dist/commands/create.js
CHANGED
|
@@ -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
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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 <
|
|
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,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reelforge",
|
|
3
|
-
"version": "0.5.
|
|
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",
|