reelforge 0.7.0 → 0.8.0
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/dist/commands/create.js +41 -85
- package/dist/commands/images.js +50 -4
- package/dist/commands/pipelines.js +19 -24
- package/package.json +1 -1
package/dist/commands/create.js
CHANGED
|
@@ -15,27 +15,8 @@ const IMAGE_UNITS = 3;
|
|
|
15
15
|
const CHARS_PER_SEC_ZH = 5;
|
|
16
16
|
const TARGET_SEC_PER_SCENE = 8;
|
|
17
17
|
function estimateUnits(body) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if (body.frame_template_type) {
|
|
21
|
-
tplType = body.frame_template_type;
|
|
22
|
-
}
|
|
23
|
-
else {
|
|
24
|
-
const m = body.frame_template_inline.match(/<meta[^>]+name=["']template:type["'][^>]+content=["']([a-z]+)["']/i);
|
|
25
|
-
const v = m?.[1].toLowerCase();
|
|
26
|
-
tplType = v === "static" || v === "asset" || v === "image" ? v : "image";
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
else {
|
|
30
|
-
const tplKey = body.frame_template || "1080x1920/image_default.html";
|
|
31
|
-
const tplBase = (tplKey.split("/").pop() || "").toLowerCase();
|
|
32
|
-
tplType = tplBase.startsWith("static_")
|
|
33
|
-
? "static"
|
|
34
|
-
: tplBase.startsWith("asset_")
|
|
35
|
-
? "asset"
|
|
36
|
-
: "image";
|
|
37
|
-
}
|
|
38
|
-
// Estimated scene count: from script length (fixed) or from duration (generate).
|
|
18
|
+
// The standard pipeline now always generates one AI image per scene (the
|
|
19
|
+
// old static_/asset_ template-type carveouts are gone).
|
|
39
20
|
let estimatedScenes;
|
|
40
21
|
if (body.script) {
|
|
41
22
|
const estSec = body.script.length / CHARS_PER_SEC_ZH;
|
|
@@ -45,19 +26,9 @@ function estimateUnits(body) {
|
|
|
45
26
|
const dur = body.duration ?? 45;
|
|
46
27
|
estimatedScenes = Math.max(2, Math.round(dur / TARGET_SEC_PER_SCENE));
|
|
47
28
|
}
|
|
48
|
-
|
|
49
|
-
return PLAN_UNITS + TTS_UNITS + ASR_UNITS + imageUnits;
|
|
29
|
+
return PLAN_UNITS + TTS_UNITS + ASR_UNITS + estimatedScenes * IMAGE_UNITS;
|
|
50
30
|
}
|
|
51
31
|
// ── Helpers ─────────────────────────────────────────────────────
|
|
52
|
-
function looksLikeLocalHtmlPath(value) {
|
|
53
|
-
if (/^[.~]|^\//.test(value))
|
|
54
|
-
return true;
|
|
55
|
-
if (value.includes("\\"))
|
|
56
|
-
return true;
|
|
57
|
-
if (value.endsWith(".html") && fsSync.existsSync(value))
|
|
58
|
-
return true;
|
|
59
|
-
return false;
|
|
60
|
-
}
|
|
61
32
|
/** `@file` prefix → load file contents; raw text → return as-is. */
|
|
62
33
|
async function resolveTextOrFile(input) {
|
|
63
34
|
if (input.startsWith("@")) {
|
|
@@ -184,32 +155,16 @@ function optsToBody(opts) {
|
|
|
184
155
|
out.prompt_prefix = opts.promptPrefix;
|
|
185
156
|
if (opts.characterRef !== undefined)
|
|
186
157
|
out.character_ref = opts.characterRef;
|
|
187
|
-
if (opts.styleRef !== undefined)
|
|
188
|
-
out.style_ref = opts.styleRef;
|
|
189
158
|
if (opts.voiceId !== undefined)
|
|
190
159
|
out.voice_id = opts.voiceId;
|
|
191
160
|
if (opts.ttsSpeed !== undefined)
|
|
192
161
|
out.tts_speed = opts.ttsSpeed;
|
|
193
162
|
if (opts.videoFps !== undefined)
|
|
194
163
|
out.video_fps = opts.videoFps;
|
|
195
|
-
if (opts.
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
throw new Error(`--frame-template: local file not found: ${abs}`);
|
|
200
|
-
}
|
|
201
|
-
out.frame_template_inline = fsSync.readFileSync(abs, "utf-8");
|
|
202
|
-
}
|
|
203
|
-
else {
|
|
204
|
-
out.frame_template = opts.frameTemplate;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
if (opts.frameTemplateSize !== undefined)
|
|
208
|
-
out.frame_template_size = opts.frameTemplateSize;
|
|
209
|
-
if (opts.frameTemplateType !== undefined)
|
|
210
|
-
out.frame_template_type = opts.frameTemplateType;
|
|
211
|
-
if (opts.templateParams !== undefined)
|
|
212
|
-
out.template_params = opts.templateParams;
|
|
164
|
+
if (opts.motion !== undefined)
|
|
165
|
+
out.motion = opts.motion;
|
|
166
|
+
if (opts.subtitleStyle !== undefined)
|
|
167
|
+
out.subtitle_style = opts.subtitleStyle;
|
|
213
168
|
if (opts.subtitleMinChars !== undefined)
|
|
214
169
|
out.subtitle_min_chars = opts.subtitleMinChars;
|
|
215
170
|
if (opts.subtitleHardMax !== undefined)
|
|
@@ -336,14 +291,12 @@ export function registerCreate(program) {
|
|
|
336
291
|
.option("-d, --duration <sec>", "target video duration in seconds (generate mode only; default 45). LLM aims for ~duration × 5 chars of narration.", (v) => parseInt(v, 10))
|
|
337
292
|
.option("-p, --pace <pace>", "visual rhythm hint passed to the LLM: slow | normal | fast (default normal). LLM still decides the actual scene count from semantic structure.")
|
|
338
293
|
// --- Visual ---
|
|
339
|
-
.option("--
|
|
340
|
-
.option("--frame-template-size <wxh>", "size for inline HTML when the file lacks <meta template:width|height>, e.g. 1080x1920")
|
|
341
|
-
.option("--frame-template-type <type>", "inline template type: image (default) | static | asset. Controls whether AI image generation runs per scene.")
|
|
342
|
-
.option("--image-model <id>", "RelayX image model (rx-image-z | rx-image-flux | rx-image-qwen | rx-image-qwen-edit). Auto-switches to rx-image-qwen-edit when --character-ref or --style-ref is set.")
|
|
294
|
+
.option("--image-model <id>", "RelayX image model (rx-image-z | rx-image-flux | rx-image-qwen | rx-image-qwen-edit). Auto-switches to rx-image-qwen-edit when --character-ref is set.")
|
|
343
295
|
.option("--prompt-prefix <text>", "raw style prefix prepended to every image prompt (overrides --style)")
|
|
344
296
|
.option("--style <preset>", "image style preset — shortcut for --prompt-prefix; see 'Style presets' below")
|
|
345
297
|
.option("--character-ref <urlOrPath>", "reference image of the main character — locks identity across scenes. URL, data: URI, or local png/jpg/webp path (auto-encoded). Auto-enables rx-image-qwen-edit.")
|
|
346
|
-
.option("--
|
|
298
|
+
.option("--motion <preset>", "per-scene image animation intensity. See 'Motion presets' below. Default: lite.")
|
|
299
|
+
.option("--subtitle-style <preset>", "subtitle visual style. See 'Subtitle styles' below. Default: plate.")
|
|
347
300
|
// --- Audio (TTS) ---
|
|
348
301
|
.option("--voice-id <id>", "RelayX TTS voice id (default 专业解说); see `rf tts voices`")
|
|
349
302
|
.option("--tts-speed <n>", "speech speed 0.5..2 (default 1.0)", parseFloat)
|
|
@@ -356,14 +309,6 @@ export function registerCreate(program) {
|
|
|
356
309
|
.option("--subtitle-hard-max <N>", "subtitle line absolute max chars (default 24)", (v) => parseInt(v, 10))
|
|
357
310
|
// --- Output / extra ---
|
|
358
311
|
.option("--video-fps <n>", "output video fps (default 30)", (v) => parseInt(v, 10))
|
|
359
|
-
.option("--template-params <json>", "extra template placeholders as JSON string", (v) => {
|
|
360
|
-
try {
|
|
361
|
-
return JSON.parse(v);
|
|
362
|
-
}
|
|
363
|
-
catch {
|
|
364
|
-
throw new Error(`--template-params: invalid JSON: ${v}`);
|
|
365
|
-
}
|
|
366
|
-
})
|
|
367
312
|
// --- Runtime / workflow ---
|
|
368
313
|
.option("--recipe <file>", "load defaults from a JSON recipe file (CLI flags still override)")
|
|
369
314
|
.option("--redo", "replay last successful create from ~/.reelforge/last-create.json")
|
|
@@ -385,9 +330,21 @@ export function registerCreate(program) {
|
|
|
385
330
|
" fast split long semantic chunks into multiple shots for variety",
|
|
386
331
|
"",
|
|
387
332
|
"Defaults:",
|
|
388
|
-
" duration=45s · pace=normal ·
|
|
333
|
+
" duration=45s · pace=normal · motion=lite · subtitle-style=plate · tts-speed=1.0",
|
|
334
|
+
"",
|
|
335
|
+
"Motion presets (--motion <preset>) — per-scene image animation intensity:",
|
|
336
|
+
" off no motion, hard cuts between scenes — PPT / slideshow mode",
|
|
337
|
+
" lite 6% zoom + 0.3s crossfade, 4 sub-anims (default; safe + tasteful)",
|
|
338
|
+
" max 20% zoom/pan + 0.5s crossfade, 10 sub-anims (cinematic, more dramatic)",
|
|
339
|
+
" · sub-animations are Fisher-Yates shuffled per task_id, so every video",
|
|
340
|
+
" cycles a different order — no two videos feel identical.",
|
|
389
341
|
"",
|
|
390
|
-
"
|
|
342
|
+
"Subtitle styles (--subtitle-style <preset>):",
|
|
343
|
+
" plate semi-transparent black plate + white text (CapCut default; safest readability)",
|
|
344
|
+
" stroke bold white text with black stroke + shadow, no plate (抖音网红风)",
|
|
345
|
+
" cinema bottom black gradient backdrop + lighter text (film / documentary look)",
|
|
346
|
+
"",
|
|
347
|
+
"Image style presets (--style <preset>) — quick shortcut for --prompt-prefix:",
|
|
391
348
|
formatStylePresetsList(),
|
|
392
349
|
" · Pass --prompt-prefix to override (raw string always wins).",
|
|
393
350
|
" · Omit both to use the server's configured default style (if any).",
|
|
@@ -409,16 +366,17 @@ export function registerCreate(program) {
|
|
|
409
366
|
" rf create --script @./script.txt",
|
|
410
367
|
' rf create --script "整段文案文本..."',
|
|
411
368
|
"",
|
|
412
|
-
" #
|
|
413
|
-
" rf create '...' --frame-template ./my-brand.html",
|
|
414
|
-
"",
|
|
415
|
-
" # Pick a built-in style preset",
|
|
369
|
+
" # Pick a built-in image style preset",
|
|
416
370
|
' rf create "..." --style cinematic',
|
|
417
371
|
"",
|
|
418
372
|
" # Cross-scene character consistency (auto-switches image model)",
|
|
419
373
|
' rf create "主角小女孩的一天" --character-ref ./hero.png',
|
|
420
|
-
|
|
421
|
-
|
|
374
|
+
' rf create "..." --character-ref https://example.com/hero.png',
|
|
375
|
+
"",
|
|
376
|
+
" # Motion + subtitle style combos",
|
|
377
|
+
' rf create "..." --motion max --subtitle-style stroke # 抖音网红风',
|
|
378
|
+
' rf create "..." --motion lite --subtitle-style cinema # 文艺纪录片',
|
|
379
|
+
' rf create "..." --motion off # PPT 模式(旧行为)',
|
|
422
380
|
"",
|
|
423
381
|
" # Recipe + replay last",
|
|
424
382
|
" rf create --recipe ./space.recipe.json",
|
|
@@ -448,6 +406,12 @@ export function registerCreate(program) {
|
|
|
448
406
|
if (opts.pace && !["slow", "normal", "fast"].includes(opts.pace)) {
|
|
449
407
|
throw new Error(`--pace must be one of slow|normal|fast (got: ${opts.pace})`);
|
|
450
408
|
}
|
|
409
|
+
if (opts.motion && !["off", "lite", "max"].includes(opts.motion)) {
|
|
410
|
+
throw new Error(`--motion must be one of off|lite|max (got: ${opts.motion})`);
|
|
411
|
+
}
|
|
412
|
+
if (opts.subtitleStyle && !["plate", "stroke", "cinema"].includes(opts.subtitleStyle)) {
|
|
413
|
+
throw new Error(`--subtitle-style must be one of plate|stroke|cinema (got: ${opts.subtitleStyle})`);
|
|
414
|
+
}
|
|
451
415
|
// 1. Layer defaults: --redo → --recipe → CLI opts → positional topic
|
|
452
416
|
let body = {};
|
|
453
417
|
if (opts.redo) {
|
|
@@ -480,19 +444,14 @@ export function registerCreate(program) {
|
|
|
480
444
|
if (typeof body.script === "string") {
|
|
481
445
|
body.script = await resolveTextOrFile(body.script);
|
|
482
446
|
}
|
|
483
|
-
// Resolve
|
|
484
|
-
// https:// and data: in image_urls). Done after layering so a
|
|
485
|
-
// can carry
|
|
447
|
+
// Resolve character ref: local file path → data: URI (RelayX accepts
|
|
448
|
+
// both https:// and data: in image_urls). Done after layering so a
|
|
449
|
+
// recipe can carry the ref by path too.
|
|
486
450
|
const resolvedChar = await resolveRefImage(body.character_ref, "--character-ref");
|
|
487
|
-
const resolvedStyle = await resolveRefImage(body.style_ref, "--style-ref");
|
|
488
451
|
if (resolvedChar !== undefined)
|
|
489
452
|
body.character_ref = resolvedChar;
|
|
490
453
|
else
|
|
491
454
|
delete body.character_ref;
|
|
492
|
-
if (resolvedStyle !== undefined)
|
|
493
|
-
body.style_ref = resolvedStyle;
|
|
494
|
-
else
|
|
495
|
-
delete body.style_ref;
|
|
496
455
|
// Validate content mode
|
|
497
456
|
const hasTopic = typeof body.topic === "string" && body.topic.trim().length > 0;
|
|
498
457
|
const hasScript = typeof body.script === "string" && body.script.trim().length > 0;
|
|
@@ -502,11 +461,8 @@ export function registerCreate(program) {
|
|
|
502
461
|
if (hasTopic && hasScript) {
|
|
503
462
|
throw new Error("--topic and --script are mutually exclusive (pick one mode).");
|
|
504
463
|
}
|
|
505
|
-
// 3. Final body
|
|
464
|
+
// 3. Final body
|
|
506
465
|
const finalBody = { ...body };
|
|
507
|
-
if (finalBody.frame_template_inline && finalBody.frame_template) {
|
|
508
|
-
delete finalBody.frame_template;
|
|
509
|
-
}
|
|
510
466
|
// 4. Estimate cost
|
|
511
467
|
const estimate = estimateUnits(finalBody);
|
|
512
468
|
if (opts.dryRun) {
|
package/dist/commands/images.js
CHANGED
|
@@ -1,6 +1,35 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import fsSync from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
1
4
|
import { post } from "../client.js";
|
|
2
5
|
import { downloadTo } from "../utils/download.js";
|
|
3
6
|
import { print, success } from "../utils/output.js";
|
|
7
|
+
/**
|
|
8
|
+
* Reference image resolver. Accepts an https:// URL, a data: URI, or a local
|
|
9
|
+
* file path. Local files are read + base64-encoded into a data: URI so the
|
|
10
|
+
* server can forward them inside the JSON body — no file-hosting step needed.
|
|
11
|
+
*/
|
|
12
|
+
async function resolveImageRef(input) {
|
|
13
|
+
const t = input.trim();
|
|
14
|
+
if (!t)
|
|
15
|
+
throw new Error("--image: empty value");
|
|
16
|
+
if (/^https?:\/\//i.test(t) || t.startsWith("data:"))
|
|
17
|
+
return t;
|
|
18
|
+
const abs = path.resolve(t);
|
|
19
|
+
if (!fsSync.existsSync(abs)) {
|
|
20
|
+
throw new Error(`--image: local file not found: ${abs}`);
|
|
21
|
+
}
|
|
22
|
+
const ext = path.extname(abs).toLowerCase();
|
|
23
|
+
const mime = ext === ".jpg" || ext === ".jpeg" ? "image/jpeg" :
|
|
24
|
+
ext === ".webp" ? "image/webp" :
|
|
25
|
+
ext === ".png" ? "image/png" :
|
|
26
|
+
null;
|
|
27
|
+
if (!mime) {
|
|
28
|
+
throw new Error(`--image: unsupported extension ${ext} (use png/jpg/jpeg/webp)`);
|
|
29
|
+
}
|
|
30
|
+
const buf = await fs.readFile(abs);
|
|
31
|
+
return `data:${mime};base64,${buf.toString("base64")}`;
|
|
32
|
+
}
|
|
4
33
|
export function registerImages(program) {
|
|
5
34
|
const images = program
|
|
6
35
|
.command("images")
|
|
@@ -8,19 +37,30 @@ export function registerImages(program) {
|
|
|
8
37
|
.helpOption("-h, --help", "show help");
|
|
9
38
|
images
|
|
10
39
|
.command("generate")
|
|
11
|
-
.description("Generate an image via RelayX")
|
|
40
|
+
.description("Generate an image via RelayX (text-to-image or ref-image, depending on model + --image)")
|
|
12
41
|
.helpOption("-h, --help", "show help")
|
|
13
|
-
.requiredOption("-p, --prompt <text>", "text prompt")
|
|
14
|
-
.option("-m, --model <id>", "RelayX image model id (rx-image-z | rx-image-flux | rx-image-qwen)")
|
|
42
|
+
.requiredOption("-p, --prompt <text>", "text prompt. With --image, reference each ref via 「图 N」/「Image N」.")
|
|
43
|
+
.option("-m, --model <id>", "RelayX image model id (rx-image-z | rx-image-flux | rx-image-qwen | rx-image-qwen-edit)")
|
|
15
44
|
.option("--width <n>", "image width", parseInt)
|
|
16
45
|
.option("--height <n>", "image height", parseInt)
|
|
46
|
+
.option("-i, --image <urlOrPath>", "reference image, repeatable up to 3 times. URL / data: URI / local png/jpg/webp path (auto-base64'd). Index N-1 maps to 「图 N」in the prompt. Only honored by edit-capable SKUs (rx-image-qwen-edit).", (val, prev) => [...prev, val], [])
|
|
17
47
|
.option("-o, --output <file>", "download first image to this local path")
|
|
18
48
|
.option("--all-output <dir>", "download ALL generated images into this directory")
|
|
19
49
|
.addHelpText("after", [
|
|
20
50
|
"",
|
|
21
51
|
"Examples:",
|
|
52
|
+
" # plain text-to-image",
|
|
22
53
|
" reelforge images generate -p 'a cat' -m rx-image-flux --width 1024 --height 1024 -o cat.png",
|
|
23
|
-
"
|
|
54
|
+
"",
|
|
55
|
+
" # ref-image edit (Qwen-Image-Edit), 1 ref local file",
|
|
56
|
+
" reelforge images generate -m rx-image-qwen-edit \\",
|
|
57
|
+
" -p '保持图1人物,把背景改成雪山黄昏' \\",
|
|
58
|
+
" -i ./hero.png -o out.png",
|
|
59
|
+
"",
|
|
60
|
+
" # ref-image edit, 2 refs (URL + local), 「图1」+「图2」",
|
|
61
|
+
" reelforge images generate -m rx-image-qwen-edit \\",
|
|
62
|
+
" -p '让图1的人物穿上图2的衣服' \\",
|
|
63
|
+
" -i https://example.com/person.jpg -i ./outfit.png -o composite.png",
|
|
24
64
|
].join("\n"))
|
|
25
65
|
.action(async (opts) => {
|
|
26
66
|
const body = { prompt: opts.prompt };
|
|
@@ -30,6 +70,12 @@ export function registerImages(program) {
|
|
|
30
70
|
body.width = opts.width;
|
|
31
71
|
if (opts.height !== undefined)
|
|
32
72
|
body.height = opts.height;
|
|
73
|
+
if (opts.image && Array.isArray(opts.image) && opts.image.length > 0) {
|
|
74
|
+
if (opts.image.length > 3) {
|
|
75
|
+
throw new Error(`--image accepts at most 3 refs (got ${opts.image.length})`);
|
|
76
|
+
}
|
|
77
|
+
body.image_urls = await Promise.all(opts.image.map(resolveImageRef));
|
|
78
|
+
}
|
|
33
79
|
const r = await post("/api/v1/images/generate", body);
|
|
34
80
|
if (opts.output && r.images?.[0]) {
|
|
35
81
|
await downloadTo(r.images[0], opts.output);
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
|
-
import fsSync from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
2
|
import { post } from "../client.js";
|
|
5
3
|
import { waitForTask } from "../utils/task-waiter.js";
|
|
6
4
|
import { downloadTo } from "../utils/download.js";
|
|
@@ -47,11 +45,11 @@ export function registerPipelines(program) {
|
|
|
47
45
|
.option("--script <text>", "your own master script text (mode=fixed). Use @file to read from disk.")
|
|
48
46
|
.option("-d, --duration <sec>", "target video duration in seconds (generate mode; default 45)", (v) => parseInt(v, 10))
|
|
49
47
|
.option("-p, --pace <pace>", "visual rhythm hint: slow | normal | fast (default normal)")
|
|
50
|
-
.option("--
|
|
51
|
-
.option("--
|
|
52
|
-
.option("--
|
|
53
|
-
.option("--image-model <id>", "RelayX image model (rx-image-z | rx-image-flux | rx-image-qwen)")
|
|
48
|
+
.option("--motion <preset>", "per-scene image animation: off | lite (default) | max")
|
|
49
|
+
.option("--subtitle-style <preset>", "subtitle visual style: plate (default) | stroke | cinema")
|
|
50
|
+
.option("--image-model <id>", "RelayX image model (rx-image-z | rx-image-flux | rx-image-qwen | rx-image-qwen-edit)")
|
|
54
51
|
.option("--prompt-prefix <text>", "style prefix prepended to every image prompt")
|
|
52
|
+
.option("--character-ref <urlOrPath>", "main character ref for cross-scene identity lock")
|
|
55
53
|
.option("--voice-id <id>", "RelayX TTS voice id (default 专业解说); see `rf tts voices`")
|
|
56
54
|
.option("--tts-speed <n>", "speech speed (0.5..2; default 1.0)", parseFloat)
|
|
57
55
|
.option("--video-fps <n>", "output video fps (default 30)", (v) => parseInt(v, 10))
|
|
@@ -64,11 +62,15 @@ export function registerPipelines(program) {
|
|
|
64
62
|
" fixed You supply the script. --script <text-or-@file>",
|
|
65
63
|
"",
|
|
66
64
|
"Pace (LLM visual rhythm hint): slow | normal | fast",
|
|
65
|
+
"Motion (per-scene animation): off | lite | max",
|
|
66
|
+
"Subtitle style: plate | stroke | cinema",
|
|
67
67
|
"",
|
|
68
68
|
"Examples:",
|
|
69
69
|
" rf pipelines standard -t 'why we explore space' -d 60 -o space.mp4",
|
|
70
|
-
" rf pipelines standard --script @script.txt -p slow -o out.mp4",
|
|
71
|
-
" rf pipelines standard -t '宠物' --
|
|
70
|
+
" rf pipelines standard --script @script.txt -p slow --motion max -o out.mp4",
|
|
71
|
+
" rf pipelines standard -t '宠物' --motion lite --subtitle-style cinema -o final.mp4",
|
|
72
|
+
"",
|
|
73
|
+
"Tip: `rf create` is a more ergonomic wrapper around the same endpoint.",
|
|
72
74
|
].join("\n"))).action(async (opts) => {
|
|
73
75
|
const hasTopic = typeof opts.topic === "string" && opts.topic.length > 0;
|
|
74
76
|
const hasScript = typeof opts.script === "string" && opts.script.length > 0;
|
|
@@ -81,24 +83,28 @@ export function registerPipelines(program) {
|
|
|
81
83
|
if (opts.pace && !["slow", "normal", "fast"].includes(opts.pace)) {
|
|
82
84
|
throw new Error(`--pace must be one of slow|normal|fast (got: ${opts.pace})`);
|
|
83
85
|
}
|
|
86
|
+
if (opts.motion && !["off", "lite", "max"].includes(opts.motion)) {
|
|
87
|
+
throw new Error(`--motion must be one of off|lite|max (got: ${opts.motion})`);
|
|
88
|
+
}
|
|
89
|
+
if (opts.subtitleStyle && !["plate", "stroke", "cinema"].includes(opts.subtitleStyle)) {
|
|
90
|
+
throw new Error(`--subtitle-style must be one of plate|stroke|cinema (got: ${opts.subtitleStyle})`);
|
|
91
|
+
}
|
|
84
92
|
let topic = opts.topic;
|
|
85
93
|
let script = opts.script;
|
|
86
94
|
if (topic?.startsWith("@"))
|
|
87
95
|
topic = await fs.readFile(topic.slice(1), "utf-8");
|
|
88
96
|
if (script?.startsWith("@"))
|
|
89
97
|
script = await fs.readFile(script.slice(1), "utf-8");
|
|
90
|
-
const tpl = opts.frameTemplate
|
|
91
|
-
? resolveTemplateArg(opts.frameTemplate, opts.frameTemplateSize)
|
|
92
|
-
: {};
|
|
93
98
|
await submitAndMaybeWait("/api/v1/pipelines/standard", {
|
|
94
99
|
topic,
|
|
95
100
|
script,
|
|
96
101
|
duration: opts.duration,
|
|
97
102
|
pace: opts.pace,
|
|
98
|
-
|
|
99
|
-
|
|
103
|
+
motion: opts.motion,
|
|
104
|
+
subtitle_style: opts.subtitleStyle,
|
|
100
105
|
image_model: opts.imageModel,
|
|
101
106
|
prompt_prefix: opts.promptPrefix,
|
|
107
|
+
character_ref: opts.characterRef,
|
|
102
108
|
voice_id: opts.voiceId,
|
|
103
109
|
tts_speed: opts.ttsSpeed,
|
|
104
110
|
video_fps: opts.videoFps,
|
|
@@ -107,14 +113,3 @@ export function registerPipelines(program) {
|
|
|
107
113
|
}, { wait: opts.wait, output: opts.output, pollMs: opts.pollMs, timeoutMs: opts.timeoutMs });
|
|
108
114
|
});
|
|
109
115
|
}
|
|
110
|
-
function resolveTemplateArg(value, size) {
|
|
111
|
-
if (/^[.~]|^\//.test(value) || value.includes("\\") || (value.endsWith(".html") && fsSync.existsSync(value))) {
|
|
112
|
-
const abs = path.resolve(value);
|
|
113
|
-
if (!fsSync.existsSync(abs)) {
|
|
114
|
-
throw new Error(`Local template not found: ${abs}`);
|
|
115
|
-
}
|
|
116
|
-
const html = fsSync.readFileSync(abs, "utf-8");
|
|
117
|
-
return size ? { frame_template_inline: html, frame_template_size: size } : { frame_template_inline: html };
|
|
118
|
-
}
|
|
119
|
-
return { frame_template: value };
|
|
120
|
-
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reelforge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
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",
|