reelforge 0.9.0 → 0.10.1
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 +29 -0
- package/dist/commands/create.js +60 -0
- package/dist/commands/pipelines.js +24 -0
- package/dist/commands/templates.js +161 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -152,6 +152,35 @@ rf create "..." --layout letterbox --layout-matte-color "#1a1a1a" # 柔和黑
|
|
|
152
152
|
| `history list / get <id> / delete <id>` | Browse / delete completed runs |
|
|
153
153
|
| `health` | Server health + capability check |
|
|
154
154
|
|
|
155
|
+
### Heavy brand customization (custom overlay templates)
|
|
156
|
+
|
|
157
|
+
`--motion` / `--layout` / `--subtitle-*` / `--brand-*` cover the common cases. For full visual identity ownership (custom path bar, accent decorations, light theme, footer block, etc.) pass a custom overlay HTML via `--frame-template <local.html | preset_key>`:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
rf templates show 1080x1920/default.html -o ./my-brand.html
|
|
161
|
+
# ...edit my-brand.html (change colors, add structure, etc.)...
|
|
162
|
+
rf create "我的视频" --frame-template ./my-brand.html
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**Contract for custom HTML**:
|
|
166
|
+
|
|
167
|
+
- Canvas is **1080×1920** (pipeline-fixed; don't declare a different size in `<meta>`).
|
|
168
|
+
- Background must be **transparent** — the AI image is composited by ffmpeg OUTSIDE the HTML layer.
|
|
169
|
+
- **`{{image}}` no longer exists.** Using `<img src="{{image}}">` is a hard error at submit time. Image is composited by ffmpeg.
|
|
170
|
+
- The pipeline injects these placeholders for you:
|
|
171
|
+
|
|
172
|
+
| Placeholder | What it is |
|
|
173
|
+
|---|---|
|
|
174
|
+
| `{{title}}` `{{text}}` | per-frame content |
|
|
175
|
+
| `{{index}}` `{{total}}` | "scene N of M" — `{{total}}` is the LLM-decided scene count, don't hardcode |
|
|
176
|
+
| `{{layout}}` | `"full"` / `"blur-bg"` / `"letterbox"` — react via `body[data-layout="..."]` CSS to put title/subtitle in the matte zones when layout ≠ full |
|
|
177
|
+
| `{{subtitle_style}}` `{{subtitle_color}}` `{{subtitle_background}}` | subtitle preset + overrides |
|
|
178
|
+
| `{{brand_position}}` `{{brand_handle}}` `{{brand_slogan}}` `{{brand_logo}}` `{{brand_color}}` | brand-chrome inputs (use or ignore as you like) |
|
|
179
|
+
|
|
180
|
+
Inline HTML is hard-capped at 2 MB. The audio + motion + character-ref + scene-plan stages all keep working identically — only the overlay layer is yours.
|
|
181
|
+
|
|
182
|
+
**Publishing to short-video platforms (Douyin / TikTok / WeChat Channels)** — the default template renders to the full 1080×1920 canvas, but those apps overlay UI on top/bottom/right and cover-crop ~96-180px on each side on taller phones. ReelForge does NOT bake platform-specific padding into the default template. For reference safe-zone numbers + a copy-pasteable CSS diff: `rf templates safezone [douyin|tiktok|wechat]`.
|
|
183
|
+
|
|
155
184
|
## Examples
|
|
156
185
|
|
|
157
186
|
```bash
|
package/dist/commands/create.js
CHANGED
|
@@ -45,6 +45,22 @@ async function resolveTextOrFile(input) {
|
|
|
45
45
|
* Returns undefined when input is missing/blank so the caller can branch on
|
|
46
46
|
* "user actually provided this knob".
|
|
47
47
|
*/
|
|
48
|
+
/**
|
|
49
|
+
* Tell apart a preset template key (e.g. "1080x1920/default.html") from a
|
|
50
|
+
* local file path. Heuristic: anything that starts with ./ ~ /, contains a
|
|
51
|
+
* backslash, or whose .html extension corresponds to an existing file on
|
|
52
|
+
* disk is treated as a local path. Otherwise it's a key for the server's
|
|
53
|
+
* preset registry. Mirrors the 0.7.x detection so muscle memory carries over.
|
|
54
|
+
*/
|
|
55
|
+
function looksLikeLocalHtmlPath(value) {
|
|
56
|
+
if (/^[.~]|^\//.test(value))
|
|
57
|
+
return true;
|
|
58
|
+
if (value.includes("\\"))
|
|
59
|
+
return true;
|
|
60
|
+
if (value.endsWith(".html") && fsSync.existsSync(value))
|
|
61
|
+
return true;
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
48
64
|
async function resolveRefImage(input, flagName) {
|
|
49
65
|
if (input === undefined)
|
|
50
66
|
return undefined;
|
|
@@ -167,6 +183,22 @@ function optsToBody(opts) {
|
|
|
167
183
|
out.layout = opts.layout;
|
|
168
184
|
if (opts.layoutMatteColor !== undefined)
|
|
169
185
|
out.layout_matte_color = opts.layoutMatteColor;
|
|
186
|
+
if (opts.frameTemplate !== undefined) {
|
|
187
|
+
// Local .html path? Read inline and send as frame_template_html. Otherwise
|
|
188
|
+
// treat as a preset key. Same heuristic as 0.7.x had for --frame-template.
|
|
189
|
+
if (looksLikeLocalHtmlPath(opts.frameTemplate)) {
|
|
190
|
+
const abs = path.resolve(opts.frameTemplate);
|
|
191
|
+
if (!fsSync.existsSync(abs)) {
|
|
192
|
+
throw new Error(`--frame-template: local file not found: ${abs}`);
|
|
193
|
+
}
|
|
194
|
+
out.frame_template_html = fsSync.readFileSync(abs, "utf-8");
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
out.frame_template = opts.frameTemplate;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (opts.frameTemplateHtml !== undefined)
|
|
201
|
+
out.frame_template_html = opts.frameTemplateHtml;
|
|
170
202
|
if (opts.subtitleStyle !== undefined)
|
|
171
203
|
out.subtitle_style = opts.subtitleStyle;
|
|
172
204
|
if (opts.subtitleColor !== undefined)
|
|
@@ -322,6 +354,7 @@ export function registerCreate(program) {
|
|
|
322
354
|
.option("--motion <preset>", "per-scene image animation intensity. See 'Motion presets' below. Default: lite.")
|
|
323
355
|
.option("--layout <preset>", "image layout within canvas: full (default) | blur-bg | letterbox. See 'Layout presets' below.")
|
|
324
356
|
.option("--layout-matte-color <css>", "letterbox matte color (CSS string, e.g. 'black', '#1a1a1a', '#2d3748'). Ignored unless --layout letterbox. Default: black.")
|
|
357
|
+
.option("--frame-template <keyOrPath>", "custom overlay template — preset key (e.g. 1080x1920/default.html) OR path to a local .html (auto-sent inline). See 'Custom templates' below.")
|
|
325
358
|
.option("--subtitle-style <preset>", "subtitle visual style. See 'Subtitle styles' below. Default: plate.")
|
|
326
359
|
.option("--subtitle-color <css>", "override subtitle text color, e.g. '#ffeb3b'. Omit for preset default.")
|
|
327
360
|
.option("--subtitle-background <css>", "override plate-preset background, e.g. 'rgba(20,30,80,0.75)'. Other presets ignore.")
|
|
@@ -398,6 +431,28 @@ export function registerCreate(program) {
|
|
|
398
431
|
" All fields optional; missing handle/slogan/logo are individually hidden.",
|
|
399
432
|
" Per-request flags merge over config defaults field by field.",
|
|
400
433
|
"",
|
|
434
|
+
"Custom templates (--frame-template) — heavy brand customization:",
|
|
435
|
+
" Override the entire overlay HTML when --brand-* and --subtitle-color aren't",
|
|
436
|
+
" enough (custom top bar / footer / decoration / theme color etc.).",
|
|
437
|
+
" --frame-template ./my-overlay.html # local .html, auto sent inline",
|
|
438
|
+
" --frame-template 1080x1920/default.html # preset key (the built-in default)",
|
|
439
|
+
"",
|
|
440
|
+
" Contract for custom HTML:",
|
|
441
|
+
" · Canvas is 1080×1920 (pipeline-fixed; don't declare a different size meta).",
|
|
442
|
+
" · Background must be transparent — the AI image is composited by ffmpeg",
|
|
443
|
+
" OUTSIDE the HTML layer. {{image}} placeholder no longer exists; using",
|
|
444
|
+
" <img src=\"{{image}}\"> in your template is a hard error at submit time.",
|
|
445
|
+
" · Inject points the pipeline writes for you:",
|
|
446
|
+
" {{title}} {{text}} per-frame content",
|
|
447
|
+
" {{index}} {{total}} \"scene N of M\"",
|
|
448
|
+
" {{layout}} \"full\" | \"blur-bg\" | \"letterbox\" — react via",
|
|
449
|
+
" body[data-layout=\"...\"] CSS to move title /",
|
|
450
|
+
" subtitle to the matte zones when layout != full.",
|
|
451
|
+
" {{subtitle_style}} / {{subtitle_color}} / {{subtitle_background}}",
|
|
452
|
+
" {{brand_*}} handle / slogan / logo / position / color",
|
|
453
|
+
" · Bootstrap from the built-in: rf templates show 1080x1920/default.html -o my-overlay.html",
|
|
454
|
+
" · Inline HTML hard-capped at 2 MB.",
|
|
455
|
+
"",
|
|
401
456
|
"Image style presets (--style <preset>) — quick shortcut for --prompt-prefix:",
|
|
402
457
|
formatStylePresetsList(),
|
|
403
458
|
" · Pass --prompt-prefix to override (raw string always wins).",
|
|
@@ -437,6 +492,11 @@ export function registerCreate(program) {
|
|
|
437
492
|
' rf create "纪录片片段" --layout letterbox --motion max # 电影感',
|
|
438
493
|
' rf create "..." --layout letterbox --layout-matte-color "#1a1a1a" # 柔和黑',
|
|
439
494
|
"",
|
|
495
|
+
" # Custom overlay HTML (heavy brand customization)",
|
|
496
|
+
" rf templates show 1080x1920/default.html -o ./brand.html # copy as starting point",
|
|
497
|
+
" # ...edit brand.html: change colors, add top path bar / footer, etc.",
|
|
498
|
+
' rf create "..." --frame-template ./brand.html # apply your custom overlay',
|
|
499
|
+
"",
|
|
440
500
|
" # Recipe + replay last",
|
|
441
501
|
" rf create --recipe ./space.recipe.json",
|
|
442
502
|
" rf create --redo # replay last successful create",
|
|
@@ -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";
|
|
@@ -48,6 +50,7 @@ export function registerPipelines(program) {
|
|
|
48
50
|
.option("--motion <preset>", "per-scene image animation: off | lite (default) | max")
|
|
49
51
|
.option("--layout <preset>", "image layout: full (default) | blur-bg | letterbox. See below.")
|
|
50
52
|
.option("--layout-matte-color <css>", "letterbox matte color (CSS). Ignored unless --layout letterbox. Default: black.")
|
|
53
|
+
.option("--frame-template <keyOrPath>", "custom overlay: preset key OR local .html (auto sent inline). Canvas 1080×1920, transparent bg, no {{image}}.")
|
|
51
54
|
.option("--subtitle-style <preset>", "subtitle visual style: plate (default) | stroke | cinema")
|
|
52
55
|
.option("--image-model <id>", "RelayX image model (rx-image-z | rx-image-flux | rx-image-qwen | rx-image-qwen-edit)")
|
|
53
56
|
.option("--prompt-prefix <text>", "style prefix prepended to every image prompt")
|
|
@@ -109,6 +112,25 @@ export function registerPipelines(program) {
|
|
|
109
112
|
topic = await fs.readFile(topic.slice(1), "utf-8");
|
|
110
113
|
if (script?.startsWith("@"))
|
|
111
114
|
script = await fs.readFile(script.slice(1), "utf-8");
|
|
115
|
+
// --frame-template can be a preset key OR a local .html — same heuristic
|
|
116
|
+
// as 0.7.x. Local path is read and sent as frame_template_html inline.
|
|
117
|
+
let frame_template;
|
|
118
|
+
let frame_template_html;
|
|
119
|
+
if (opts.frameTemplate) {
|
|
120
|
+
const v = opts.frameTemplate;
|
|
121
|
+
const isLocal = /^[.~]|^\//.test(v) || v.includes("\\") ||
|
|
122
|
+
(v.endsWith(".html") && fsSync.existsSync(v));
|
|
123
|
+
if (isLocal) {
|
|
124
|
+
const abs = path.resolve(v);
|
|
125
|
+
if (!fsSync.existsSync(abs)) {
|
|
126
|
+
throw new Error(`--frame-template: local file not found: ${abs}`);
|
|
127
|
+
}
|
|
128
|
+
frame_template_html = await fs.readFile(abs, "utf-8");
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
frame_template = v;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
112
134
|
await submitAndMaybeWait("/api/v1/pipelines/standard", {
|
|
113
135
|
topic,
|
|
114
136
|
script,
|
|
@@ -117,6 +139,8 @@ export function registerPipelines(program) {
|
|
|
117
139
|
motion: opts.motion,
|
|
118
140
|
layout: opts.layout,
|
|
119
141
|
layout_matte_color: opts.layoutMatteColor,
|
|
142
|
+
frame_template,
|
|
143
|
+
frame_template_html,
|
|
120
144
|
subtitle_style: opts.subtitleStyle,
|
|
121
145
|
image_model: opts.imageModel,
|
|
122
146
|
prompt_prefix: opts.promptPrefix,
|
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* reelforge templates <list|preview|show>
|
|
2
|
+
* reelforge templates <list|preview|show|safezone>
|
|
3
3
|
*/
|
|
4
4
|
import fs from "node:fs/promises";
|
|
5
|
+
import kleur from "kleur";
|
|
5
6
|
import { get, post } from "../client.js";
|
|
6
7
|
import { downloadTo } from "../utils/download.js";
|
|
7
|
-
import { print, success, table } from "../utils/output.js";
|
|
8
|
+
import { isJson, print, success, table } from "../utils/output.js";
|
|
8
9
|
import { buildTemplatePayload } from "./frames.js";
|
|
9
10
|
export function registerTemplates(program) {
|
|
10
11
|
const tpl = program
|
|
11
12
|
.command("templates")
|
|
12
|
-
.description("Browse
|
|
13
|
-
.helpOption("-h, --help", "show help")
|
|
13
|
+
.description("Browse templates, preview, view source, and look up platform safe zones")
|
|
14
|
+
.helpOption("-h, --help", "show help")
|
|
15
|
+
.addHelpText("after", [
|
|
16
|
+
"",
|
|
17
|
+
"Examples:",
|
|
18
|
+
" rf templates list --size 1080x1920",
|
|
19
|
+
" rf templates show 1080x1920/default.html -o my.html",
|
|
20
|
+
" rf templates safezone # all platforms overview",
|
|
21
|
+
" rf templates safezone douyin # CSS diff for 抖音",
|
|
22
|
+
" rf templates preview ./my.html -o p.png",
|
|
23
|
+
].join("\n"));
|
|
14
24
|
tpl
|
|
15
25
|
.command("list")
|
|
16
26
|
.description("List all HTML templates (static / image / video / asset, grouped by size)")
|
|
@@ -70,14 +80,15 @@ export function registerTemplates(program) {
|
|
|
70
80
|
.addHelpText("after", [
|
|
71
81
|
"",
|
|
72
82
|
"Examples:",
|
|
73
|
-
" #
|
|
74
|
-
" rf templates show 1080x1920/
|
|
83
|
+
" # print the standard pipeline's default overlay HTML",
|
|
84
|
+
" rf templates show 1080x1920/default.html",
|
|
75
85
|
"",
|
|
76
|
-
" # copy a
|
|
77
|
-
" rf templates show 1080x1920/
|
|
78
|
-
" # ...edit my-brand.html...",
|
|
79
|
-
" rf
|
|
86
|
+
" # copy as a starting point for a custom brand template",
|
|
87
|
+
" rf templates show 1080x1920/default.html -o my-brand.html",
|
|
88
|
+
" # ...edit my-brand.html (change colors, add path bar / footer, etc.)...",
|
|
89
|
+
" rf create '...' --frame-template ./my-brand.html",
|
|
80
90
|
"",
|
|
91
|
+
" Contract for custom HTML: see `rf create --help` → 'Custom templates'.",
|
|
81
92
|
" Get the list of keys via `rf templates list`.",
|
|
82
93
|
].join("\n"))
|
|
83
94
|
.action(async (key, opts) => {
|
|
@@ -85,9 +96,149 @@ export function registerTemplates(program) {
|
|
|
85
96
|
if (opts.output) {
|
|
86
97
|
await fs.writeFile(opts.output, r.html, "utf-8");
|
|
87
98
|
success(`Saved → ${opts.output} (${r.size}, ${r.type}, ${r.params.length} custom params)`);
|
|
99
|
+
if (/default\.html$/.test(key) && !isJson()) {
|
|
100
|
+
process.stderr.write(kleur.dim(" ↳ for short-video platform safe zones (抖音/TikTok/视频号), see ") +
|
|
101
|
+
kleur.cyan("rf templates safezone") +
|
|
102
|
+
"\n");
|
|
103
|
+
}
|
|
88
104
|
}
|
|
89
105
|
else {
|
|
90
106
|
process.stdout.write(r.html);
|
|
91
107
|
}
|
|
92
108
|
});
|
|
109
|
+
tpl
|
|
110
|
+
.command("safezone [platform]")
|
|
111
|
+
.description("Reference safe-zone padding for short-video platforms (抖音 / TikTok / 视频号)")
|
|
112
|
+
.helpOption("-h, --help", "show help")
|
|
113
|
+
.addHelpText("after", [
|
|
114
|
+
"",
|
|
115
|
+
"Why this exists:",
|
|
116
|
+
" Default 1080×1920 templates render to the full canvas. When a short-video",
|
|
117
|
+
" app plays a 9:16 video on a taller phone (19.5:9 iPhone, 20:9/21:9 Android)",
|
|
118
|
+
" it cover-crops ~96-180px each side, AND overlays its own UI on top, bottom,",
|
|
119
|
+
" and the right action-button column. Important content near those edges gets",
|
|
120
|
+
" cut or covered.",
|
|
121
|
+
"",
|
|
122
|
+
" ReelForge does NOT bake platform-specific padding into the default template",
|
|
123
|
+
" (UI changes; device crop varies). Use this command to look up reference",
|
|
124
|
+
" numbers, then dial them into your own copy of default.html.",
|
|
125
|
+
"",
|
|
126
|
+
"Workflow:",
|
|
127
|
+
" rf templates show 1080x1920/default.html -o my-douyin.html",
|
|
128
|
+
" rf templates safezone douyin # copy the CSS diff",
|
|
129
|
+
" # paste the diff into my-douyin.html",
|
|
130
|
+
" rf create '...' --frame-template ./my-douyin.html",
|
|
131
|
+
"",
|
|
132
|
+
"Examples:",
|
|
133
|
+
" rf templates safezone # overview table for all platforms",
|
|
134
|
+
" rf templates safezone douyin # detailed CSS diff for 抖音",
|
|
135
|
+
" rf templates safezone tiktok",
|
|
136
|
+
" rf templates safezone wechat # 视频号",
|
|
137
|
+
" rf templates safezone --json # machine-readable",
|
|
138
|
+
].join("\n"))
|
|
139
|
+
.action((platform) => {
|
|
140
|
+
if (isJson()) {
|
|
141
|
+
if (platform) {
|
|
142
|
+
const zone = PLATFORM_SAFEZONES[platform.toLowerCase()];
|
|
143
|
+
if (!zone) {
|
|
144
|
+
print({ ok: false, error: `unknown platform: ${platform}`, known: Object.keys(PLATFORM_SAFEZONES) });
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
print({ ok: true, platform: platform.toLowerCase(), ...zone });
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
print({ ok: true, platforms: PLATFORM_SAFEZONES });
|
|
151
|
+
}
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (platform) {
|
|
155
|
+
const key = platform.toLowerCase();
|
|
156
|
+
const zone = PLATFORM_SAFEZONES[key];
|
|
157
|
+
if (!zone) {
|
|
158
|
+
process.stderr.write(kleur.red(`✗ unknown platform: ${platform}\n`) +
|
|
159
|
+
` known: ${Object.keys(PLATFORM_SAFEZONES).join(", ")}\n`);
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
printPlatformDetail(key, zone);
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
printPlatformOverview();
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
const PLATFORM_SAFEZONES = {
|
|
170
|
+
douyin: {
|
|
171
|
+
label: "Douyin (抖音)",
|
|
172
|
+
top: 250,
|
|
173
|
+
bottom: 400,
|
|
174
|
+
left: 150,
|
|
175
|
+
right: 250,
|
|
176
|
+
centerMax: 820,
|
|
177
|
+
reason: "Cover-crop on tall phones cuts ~96-180px per side; status bar ~250px; bottom caption+progress+buttons ~400px; right action-button column ~150px from the visible edge.",
|
|
178
|
+
},
|
|
179
|
+
tiktok: {
|
|
180
|
+
label: "TikTok",
|
|
181
|
+
top: 220,
|
|
182
|
+
bottom: 380,
|
|
183
|
+
left: 150,
|
|
184
|
+
right: 240,
|
|
185
|
+
centerMax: 820,
|
|
186
|
+
reason: "Same cover-crop behavior as Douyin; slightly smaller top tab area and bottom caption.",
|
|
187
|
+
},
|
|
188
|
+
wechat: {
|
|
189
|
+
label: "WeChat Channels (视频号)",
|
|
190
|
+
top: 200,
|
|
191
|
+
bottom: 350,
|
|
192
|
+
left: 120,
|
|
193
|
+
right: 120,
|
|
194
|
+
centerMax: 880,
|
|
195
|
+
reason: "Cover-crop similar; right-side action buttons are less obtrusive than 抖音/TikTok, so right padding can be smaller.",
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
function printPlatformOverview() {
|
|
199
|
+
process.stdout.write(kleur.bold("Reference safe-zone padding for 1080×1920 vertical video.\n") +
|
|
200
|
+
kleur.dim("Numbers cover ~90% of mainstream phones — verify on your own target devices.\n\n"));
|
|
201
|
+
table(Object.entries(PLATFORM_SAFEZONES).map(([key, z]) => ({
|
|
202
|
+
platform: key,
|
|
203
|
+
name: z.label,
|
|
204
|
+
top: z.top,
|
|
205
|
+
bottom: z.bottom,
|
|
206
|
+
left: z.left,
|
|
207
|
+
right: z.right,
|
|
208
|
+
"center-max": z.centerMax,
|
|
209
|
+
})));
|
|
210
|
+
process.stdout.write("\n" +
|
|
211
|
+
kleur.dim("CSS diff for a specific platform: ") +
|
|
212
|
+
kleur.cyan("rf templates safezone <platform>") +
|
|
213
|
+
"\n" +
|
|
214
|
+
kleur.dim("Start from default.html: ") +
|
|
215
|
+
kleur.cyan("rf templates show 1080x1920/default.html -o my.html") +
|
|
216
|
+
"\n");
|
|
217
|
+
}
|
|
218
|
+
function printPlatformDetail(key, z) {
|
|
219
|
+
process.stdout.write(kleur.bold(`${z.label} — safe-zone padding for 1080×1920 vertical video.\n\n`) +
|
|
220
|
+
kleur.dim("Why: ") +
|
|
221
|
+
z.reason +
|
|
222
|
+
"\n\n" +
|
|
223
|
+
kleur.bold("Apply this CSS to your custom template (copy of default.html):\n\n") +
|
|
224
|
+
kleur.cyan(buildCssDiff(z)) +
|
|
225
|
+
"\n" +
|
|
226
|
+
kleur.dim("Note: layout=blur-bg / letterbox push title/subtitle into matte zones\n" +
|
|
227
|
+
`(y=0..420 / 1500..1920) that overlap ${z.label}'s UI heavily. For ${key}\n` +
|
|
228
|
+
"investment, layout=full + the padding above is the safest combo.\n"));
|
|
229
|
+
}
|
|
230
|
+
function buildCssDiff(z) {
|
|
231
|
+
return [
|
|
232
|
+
` .title {`,
|
|
233
|
+
` top: ${z.top}px;`,
|
|
234
|
+
` max-width: ${z.centerMax}px;`,
|
|
235
|
+
` }`,
|
|
236
|
+
` .subtitle {`,
|
|
237
|
+
` bottom: ${z.bottom}px;`,
|
|
238
|
+
` max-width: ${z.centerMax}px;`,
|
|
239
|
+
` }`,
|
|
240
|
+
` body[data-brand-position$="left"] .brand-corner { left: ${z.left}px; }`,
|
|
241
|
+
` body[data-brand-position$="right"] .brand-corner { right: ${z.right}px; }`,
|
|
242
|
+
"",
|
|
243
|
+
].join("\n");
|
|
93
244
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reelforge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.1",
|
|
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",
|