reelforge 0.7.1 → 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.
@@ -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
- let tplType;
19
- if (body.frame_template_inline) {
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
- const imageUnits = tplType === "image" ? estimatedScenes * IMAGE_UNITS : 0;
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("@")) {
@@ -190,24 +161,10 @@ function optsToBody(opts) {
190
161
  out.tts_speed = opts.ttsSpeed;
191
162
  if (opts.videoFps !== undefined)
192
163
  out.video_fps = opts.videoFps;
193
- if (opts.frameTemplate !== undefined) {
194
- if (looksLikeLocalHtmlPath(opts.frameTemplate)) {
195
- const abs = path.resolve(opts.frameTemplate);
196
- if (!fsSync.existsSync(abs)) {
197
- throw new Error(`--frame-template: local file not found: ${abs}`);
198
- }
199
- out.frame_template_inline = fsSync.readFileSync(abs, "utf-8");
200
- }
201
- else {
202
- out.frame_template = opts.frameTemplate;
203
- }
204
- }
205
- if (opts.frameTemplateSize !== undefined)
206
- out.frame_template_size = opts.frameTemplateSize;
207
- if (opts.frameTemplateType !== undefined)
208
- out.frame_template_type = opts.frameTemplateType;
209
- if (opts.templateParams !== undefined)
210
- 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;
211
168
  if (opts.subtitleMinChars !== undefined)
212
169
  out.subtitle_min_chars = opts.subtitleMinChars;
213
170
  if (opts.subtitleHardMax !== undefined)
@@ -334,13 +291,12 @@ export function registerCreate(program) {
334
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))
335
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.")
336
293
  // --- Visual ---
337
- .option("--frame-template <keyOrPath>", "HTML frame template: preset key (e.g. 1080x1920/image_default.html) OR path to a local .html (auto-sent inline)")
338
- .option("--frame-template-size <wxh>", "size for inline HTML when the file lacks <meta template:width|height>, e.g. 1080x1920")
339
- .option("--frame-template-type <type>", "inline template type: image (default) | static | asset. Controls whether AI image generation runs per scene.")
340
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.")
341
295
  .option("--prompt-prefix <text>", "raw style prefix prepended to every image prompt (overrides --style)")
342
296
  .option("--style <preset>", "image style preset — shortcut for --prompt-prefix; see 'Style presets' below")
343
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.")
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.")
344
300
  // --- Audio (TTS) ---
345
301
  .option("--voice-id <id>", "RelayX TTS voice id (default 专业解说); see `rf tts voices`")
346
302
  .option("--tts-speed <n>", "speech speed 0.5..2 (default 1.0)", parseFloat)
@@ -353,14 +309,6 @@ export function registerCreate(program) {
353
309
  .option("--subtitle-hard-max <N>", "subtitle line absolute max chars (default 24)", (v) => parseInt(v, 10))
354
310
  // --- Output / extra ---
355
311
  .option("--video-fps <n>", "output video fps (default 30)", (v) => parseInt(v, 10))
356
- .option("--template-params <json>", "extra template placeholders as JSON string", (v) => {
357
- try {
358
- return JSON.parse(v);
359
- }
360
- catch {
361
- throw new Error(`--template-params: invalid JSON: ${v}`);
362
- }
363
- })
364
312
  // --- Runtime / workflow ---
365
313
  .option("--recipe <file>", "load defaults from a JSON recipe file (CLI flags still override)")
366
314
  .option("--redo", "replay last successful create from ~/.reelforge/last-create.json")
@@ -382,9 +330,21 @@ export function registerCreate(program) {
382
330
  " fast split long semantic chunks into multiple shots for variety",
383
331
  "",
384
332
  "Defaults:",
385
- " duration=45s · pace=normal · frame-template=1080x1920/image_default.html · tts-speed=1.0",
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.",
341
+ "",
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)",
386
346
  "",
387
- "Style presets (--style <preset>) — quick shortcut for --prompt-prefix:",
347
+ "Image style presets (--style <preset>) — quick shortcut for --prompt-prefix:",
388
348
  formatStylePresetsList(),
389
349
  " · Pass --prompt-prefix to override (raw string always wins).",
390
350
  " · Omit both to use the server's configured default style (if any).",
@@ -406,16 +366,18 @@ export function registerCreate(program) {
406
366
  " rf create --script @./script.txt",
407
367
  ' rf create --script "整段文案文本..."',
408
368
  "",
409
- " # Custom HTML template (auto-detected when --frame-template is a local path)",
410
- " rf create '...' --frame-template ./my-brand.html",
411
- "",
412
- " # Pick a built-in style preset",
369
+ " # Pick a built-in image style preset",
413
370
  ' rf create "..." --style cinematic',
414
371
  "",
415
372
  " # Cross-scene character consistency (auto-switches image model)",
416
373
  ' rf create "主角小女孩的一天" --character-ref ./hero.png',
417
374
  ' rf create "..." --character-ref https://example.com/hero.png',
418
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 模式(旧行为)',
380
+ "",
419
381
  " # Recipe + replay last",
420
382
  " rf create --recipe ./space.recipe.json",
421
383
  " rf create --redo # replay last successful create",
@@ -444,6 +406,12 @@ export function registerCreate(program) {
444
406
  if (opts.pace && !["slow", "normal", "fast"].includes(opts.pace)) {
445
407
  throw new Error(`--pace must be one of slow|normal|fast (got: ${opts.pace})`);
446
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
+ }
447
415
  // 1. Layer defaults: --redo → --recipe → CLI opts → positional topic
448
416
  let body = {};
449
417
  if (opts.redo) {
@@ -493,11 +461,8 @@ export function registerCreate(program) {
493
461
  if (hasTopic && hasScript) {
494
462
  throw new Error("--topic and --script are mutually exclusive (pick one mode).");
495
463
  }
496
- // 3. Final body — drop empty / null fields
464
+ // 3. Final body
497
465
  const finalBody = { ...body };
498
- if (finalBody.frame_template_inline && finalBody.frame_template) {
499
- delete finalBody.frame_template;
500
- }
501
466
  // 4. Estimate cost
502
467
  const estimate = estimateUnits(finalBody);
503
468
  if (opts.dryRun) {
@@ -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("--frame-template <keyOrPath>", "preset key (e.g. 1080x1920/image_default.html) OR path to a local .html file")
51
- .option("--frame-template-size <wxh>", "size for inline HTML when the file lacks <meta template:width|height>")
52
- .option("--frame-template-type <type>", "inline type: image (default) | static | asset")
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 '宠物' --frame-template ./my-brand.html -o final.mp4",
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
- ...tpl,
99
- frame_template_type: opts.frameTemplateType,
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.7.1",
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",