reelforge 0.7.0 → 0.7.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.
@@ -184,8 +184,6 @@ function optsToBody(opts) {
184
184
  out.prompt_prefix = opts.promptPrefix;
185
185
  if (opts.characterRef !== undefined)
186
186
  out.character_ref = opts.characterRef;
187
- if (opts.styleRef !== undefined)
188
- out.style_ref = opts.styleRef;
189
187
  if (opts.voiceId !== undefined)
190
188
  out.voice_id = opts.voiceId;
191
189
  if (opts.ttsSpeed !== undefined)
@@ -339,11 +337,10 @@ export function registerCreate(program) {
339
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)")
340
338
  .option("--frame-template-size <wxh>", "size for inline HTML when the file lacks <meta template:width|height>, e.g. 1080x1920")
341
339
  .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.")
340
+ .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
341
  .option("--prompt-prefix <text>", "raw style prefix prepended to every image prompt (overrides --style)")
344
342
  .option("--style <preset>", "image style preset — shortcut for --prompt-prefix; see 'Style presets' below")
345
343
  .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("--style-ref <urlOrPath>", "reference image of the overall visual style — locks palette/composition/mood across scenes. Same input formats as --character-ref.")
347
344
  // --- Audio (TTS) ---
348
345
  .option("--voice-id <id>", "RelayX TTS voice id (default 专业解说); see `rf tts voices`")
349
346
  .option("--tts-speed <n>", "speech speed 0.5..2 (default 1.0)", parseFloat)
@@ -417,8 +414,7 @@ export function registerCreate(program) {
417
414
  "",
418
415
  " # Cross-scene character consistency (auto-switches image model)",
419
416
  ' rf create "主角小女孩的一天" --character-ref ./hero.png',
420
- " rf create '...' --character-ref ./hero.png --style-ref ./mood.jpg",
421
- ' rf create "..." --style-ref https://example.com/style.png',
417
+ ' rf create "..." --character-ref https://example.com/hero.png',
422
418
  "",
423
419
  " # Recipe + replay last",
424
420
  " rf create --recipe ./space.recipe.json",
@@ -480,19 +476,14 @@ export function registerCreate(program) {
480
476
  if (typeof body.script === "string") {
481
477
  body.script = await resolveTextOrFile(body.script);
482
478
  }
483
- // Resolve refs: local file paths → data: URIs (RelayX accepts both
484
- // https:// and data: in image_urls). Done after layering so a recipe
485
- // can carry refs by path too.
479
+ // Resolve character ref: local file path → data: URI (RelayX accepts
480
+ // both https:// and data: in image_urls). Done after layering so a
481
+ // recipe can carry the ref by path too.
486
482
  const resolvedChar = await resolveRefImage(body.character_ref, "--character-ref");
487
- const resolvedStyle = await resolveRefImage(body.style_ref, "--style-ref");
488
483
  if (resolvedChar !== undefined)
489
484
  body.character_ref = resolvedChar;
490
485
  else
491
486
  delete body.character_ref;
492
- if (resolvedStyle !== undefined)
493
- body.style_ref = resolvedStyle;
494
- else
495
- delete body.style_ref;
496
487
  // Validate content mode
497
488
  const hasTopic = typeof body.topic === "string" && body.topic.trim().length > 0;
498
489
  const hasScript = typeof body.script === "string" && body.script.trim().length > 0;
@@ -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
- " reelforge images generate -p 'cyberpunk city, neon, 9:16' --width 1080 --height 1920 -o city.png",
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reelforge",
3
- "version": "0.7.0",
3
+ "version": "0.7.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",