reelforge 0.5.5 → 0.7.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.
@@ -4,109 +4,63 @@ import { print } from "../utils/output.js";
4
4
  export function registerContent(program) {
5
5
  const content = program
6
6
  .command("content")
7
- .description("LLM-based content generators (script, image prompts, titles, asset scripts)")
7
+ .description("Content atomics scene planning (master script + image prompts in one call)")
8
8
  .helpOption("-h, --help", "show help");
9
9
  content
10
- .command("narration")
11
- .description("Generate N narration sentences from a topic")
10
+ .command("scene-plan")
11
+ .description("Generate a master script + per-scene image prompts (replaces narration/image-prompts/title)")
12
12
  .helpOption("-h, --help", "show help")
13
- .requiredOption("-t, --topic <text>", "the video topic")
14
- .option("-n, --n-scenes <n>", "number of scenes", parseInt, 5)
15
- .option("--min-words <n>", "minimum words per narration", parseInt, 5)
16
- .option("--max-words <n>", "maximum words per narration", parseInt, 20)
17
- .addHelpText("after", "\nExample:\n reelforge content narration -t 'why we explore space' -n 5")
18
- .action(async (opts) => {
19
- const r = await post("/api/v1/content/narration", {
20
- topic: opts.topic,
21
- n_scenes: opts.nScenes,
22
- min_words: opts.minWords,
23
- max_words: opts.maxWords,
24
- });
25
- print(r);
26
- });
27
- content
28
- .command("split")
29
- .description("Split a fixed script into narrations (no LLM cost)")
30
- .helpOption("-h, --help", "show help")
31
- .requiredOption("-s, --script <text>", "raw script text (use @file for a file)")
32
- .option("-m, --mode <mode>", "paragraph | line | sentence", "paragraph")
33
- .addHelpText("after", "\nExample:\n reelforge content split -s @script.txt -m sentence")
34
- .action(async (opts) => {
35
- let script = opts.script;
36
- if (script.startsWith("@"))
37
- script = await fs.readFile(script.slice(1), "utf-8");
38
- const r = await post("/api/v1/content/narration/split", { script, mode: opts.mode });
39
- print(r);
40
- });
41
- content
42
- .command("image-prompts")
43
- .description("Generate English image-generation prompts from narrations")
44
- .helpOption("-h, --help", "show help")
45
- .requiredOption("-i, --narrations <file>", "file with one narration per line (or @file)")
46
- .option("--prefix <text>", "style prefix prepended to each prompt")
47
- .option("--min-words <n>", "minimum words per prompt", parseInt, 30)
48
- .option("--max-words <n>", "maximum words per prompt", parseInt, 60)
49
- .addHelpText("after", "\nExample:\n reelforge content image-prompts -i narrations.txt --prefix 'cinematic'")
50
- .action(async (opts) => {
51
- let src = opts.narrations;
52
- if (src.startsWith("@"))
53
- src = src.slice(1);
54
- const text = await fs.readFile(src, "utf-8");
55
- const narrations = text.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
56
- const r = await post("/api/v1/content/image-prompts", {
57
- narrations,
58
- prompt_prefix: opts.prefix,
59
- min_words: opts.minWords,
60
- max_words: opts.maxWords,
61
- });
62
- print(r);
63
- });
64
- content
65
- .command("title")
66
- .description("Generate a short video title from content")
67
- .helpOption("-h, --help", "show help")
68
- .requiredOption("-c, --content <text>", "content to title (use @file)")
69
- .option("--max-length <n>", "maximum characters", parseInt, 15)
70
- .action(async (opts) => {
71
- let body = opts.content;
72
- if (body.startsWith("@"))
73
- body = await fs.readFile(body.slice(1), "utf-8");
74
- const r = await post("/api/v1/content/title", {
75
- content: body,
76
- max_length: opts.maxLength,
77
- });
78
- print(r);
79
- });
80
- content
81
- .command("asset-script")
82
- .description("Generate a scene script that assigns user-uploaded assets to scenes")
83
- .helpOption("-h, --help", "show help")
84
- .requiredOption("--intent <text>", "video intent / purpose")
85
- .option("--title <text>", "optional video title")
86
- .option("--duration <s>", "target duration in seconds", parseInt, 30)
87
- .requiredOption("--assets <file>", "file with one asset per line, format: `path | description`")
13
+ .option("-t, --topic <text>", "video topic; AI writes the script (generate mode). Use @file for disk input.")
14
+ .option("--script <text>", "your own master script text (fixed mode). Use @file for disk input.")
15
+ .option("-d, --duration <sec>", "target video duration in seconds (generate mode; default 45)", (v) => parseInt(v, 10))
16
+ .option("-p, --pace <pace>", "visual rhythm hint: slow | normal | fast (default normal)")
17
+ .option("-m, --model <id>", "override LLM model")
88
18
  .addHelpText("after", [
89
19
  "",
90
- "Example assets.txt:",
91
- " data/uploads/cat.jpg | A fluffy cat",
92
- " data/uploads/dog.jpg | A happy dog wagging tail",
20
+ "Two modes (exactly one required):",
21
+ " generate -t / --topic <text> LLM writes both script and image prompts",
22
+ " fixed --script @file or text LLM only segments + writes image prompts; text unchanged verbatim",
23
+ "",
24
+ "Examples:",
25
+ " rf content scene-plan -t '深夜便利店' -d 60 -p slow",
26
+ " rf content scene-plan --script @./my-script.txt -p fast",
27
+ " rf content scene-plan -t '雨天的玻璃窗' --json | jq .scenes",
93
28
  ].join("\n"))
94
29
  .action(async (opts) => {
95
- const raw = await fs.readFile(opts.assets, "utf-8");
96
- const assets = raw
97
- .split(/\r?\n/)
98
- .map((s) => s.trim())
99
- .filter(Boolean)
100
- .map((line) => {
101
- const [p, d] = line.split("|").map((s) => s.trim());
102
- return { path: p, description: d || "" };
103
- });
104
- const r = await post("/api/v1/content/asset-script", {
105
- intent: opts.intent,
106
- title: opts.title,
107
- duration: opts.duration,
108
- assets,
30
+ const hasTopic = typeof opts.topic === "string" && opts.topic.length > 0;
31
+ const hasScript = typeof opts.script === "string" && opts.script.length > 0;
32
+ if (!hasTopic && !hasScript) {
33
+ throw new Error("either --topic / -t or --script is required");
34
+ }
35
+ if (hasTopic && hasScript) {
36
+ throw new Error("--topic and --script are mutually exclusive");
37
+ }
38
+ if (opts.pace && !["slow", "normal", "fast"].includes(opts.pace)) {
39
+ throw new Error(`--pace must be one of slow|normal|fast (got: ${opts.pace})`);
40
+ }
41
+ let topic = opts.topic;
42
+ let script = opts.script;
43
+ if (topic?.startsWith("@"))
44
+ topic = (await fs.readFile(topic.slice(1), "utf-8")).trim();
45
+ if (script?.startsWith("@"))
46
+ script = (await fs.readFile(script.slice(1), "utf-8")).trim();
47
+ const body = {};
48
+ if (topic)
49
+ body.topic = topic;
50
+ if (script)
51
+ body.script = script;
52
+ if (opts.duration !== undefined)
53
+ body.duration = opts.duration;
54
+ if (opts.pace)
55
+ body.pace = opts.pace;
56
+ if (opts.model)
57
+ body.model = opts.model;
58
+ const r = await post("/api/v1/content/scene-plan", body);
59
+ print({
60
+ mode: r.mode,
61
+ title: r.title,
62
+ n_scenes: r.scenes.length,
63
+ scenes: r.scenes,
109
64
  });
110
- print(r);
111
65
  });
112
66
  }