reelforge 1.18.2
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 +295 -0
- package/bin/reelforge.js +8 -0
- package/dist/client.js +120 -0
- package/dist/commands/assets-workflow-text.js +22 -0
- package/dist/commands/assets.js +231 -0
- package/dist/commands/audio.js +73 -0
- package/dist/commands/auth.js +170 -0
- package/dist/commands/bgm.js +45 -0
- package/dist/commands/compose.js +293 -0
- package/dist/commands/compositions.js +143 -0
- package/dist/commands/config.js +62 -0
- package/dist/commands/content.js +66 -0
- package/dist/commands/cover.js +397 -0
- package/dist/commands/create.js +629 -0
- package/dist/commands/extract.js +102 -0
- package/dist/commands/fetch.js +129 -0
- package/dist/commands/files.js +56 -0
- package/dist/commands/health.js +12 -0
- package/dist/commands/history.js +44 -0
- package/dist/commands/images.js +88 -0
- package/dist/commands/llm.js +67 -0
- package/dist/commands/media.js +128 -0
- package/dist/commands/models.js +36 -0
- package/dist/commands/pipelines.js +142 -0
- package/dist/commands/platform.js +218 -0
- package/dist/commands/regen.js +134 -0
- package/dist/commands/render.js +82 -0
- package/dist/commands/script.js +128 -0
- package/dist/commands/styles.js +113 -0
- package/dist/commands/subtitles.js +246 -0
- package/dist/commands/tasks.js +59 -0
- package/dist/commands/tts.js +134 -0
- package/dist/index.js +173 -0
- package/dist/utils/config-file.js +37 -0
- package/dist/utils/download.js +13 -0
- package/dist/utils/file-upload.js +59 -0
- package/dist/utils/output.js +91 -0
- package/dist/utils/task-waiter.js +40 -0
- package/package.json +44 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { post } from "../client.js";
|
|
2
|
+
import { downloadTo } from "../utils/download.js";
|
|
3
|
+
import { print, success, info } from "../utils/output.js";
|
|
4
|
+
import { fmtSize, resolveFileInput, resolveFileInputs } from "../utils/file-upload.js";
|
|
5
|
+
const WHEN_TO_USE = [
|
|
6
|
+
"",
|
|
7
|
+
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
|
|
8
|
+
"何时用 / 不要用 rf compositions (AGENT 必读)",
|
|
9
|
+
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
|
|
10
|
+
"✓ 用户已经有完整素材 (视频 / 图片 / 音频),仅需 拼接 / 加 BGM / 叠 logo",
|
|
11
|
+
"✓ 用户明确说『不要 AI 配文案』『只是简单合成』",
|
|
12
|
+
"",
|
|
13
|
+
"✗ 用户说『帮我做个视频』+ 给的是主题/想法 → 走 `rf create -t \"<topic>\"`",
|
|
14
|
+
"✗ 用户给了文案脚本 → 走 `rf create --script @file.txt`",
|
|
15
|
+
"✗ 用户想 AI 在自己的素材上加配音 + 字幕 → 当前需要分两步:",
|
|
16
|
+
" 先 `rf create --script ...` 出 AI 部分,再用 compositions 拼到自己素材里",
|
|
17
|
+
"✗ Agent 全控每帧 → 走 `rf compose <spec.json>` (EXPERIMENTAL)",
|
|
18
|
+
"",
|
|
19
|
+
"文件输入 (1.14.0+):",
|
|
20
|
+
" 全部命令的文件参数都支持三种形式,CLI 自动判定:",
|
|
21
|
+
" 本地路径 → CLI 读 + base64 上传 (最常用)",
|
|
22
|
+
" https:// → 透传给服务端,ffmpeg 直接拉",
|
|
23
|
+
" data: URI → 透传,服务端解码",
|
|
24
|
+
" 注: 老接口里 `rf files upload` 拿到的服务端路径 (data/uploads/<name>) ",
|
|
25
|
+
" 不再支持 — 这组命令现在自带上传,无需先 upload。",
|
|
26
|
+
].join("\n");
|
|
27
|
+
export function registerCompositions(program) {
|
|
28
|
+
const comp = program
|
|
29
|
+
.command("compositions")
|
|
30
|
+
.description("视频合成 (底层 ffmpeg): 拼接 / 加 BGM / 图+音 → 视频 / 透明叠加")
|
|
31
|
+
.helpOption("-h, --help", "show help")
|
|
32
|
+
.addHelpText("after", WHEN_TO_USE);
|
|
33
|
+
comp
|
|
34
|
+
.command("concat <videos...>")
|
|
35
|
+
.description("Concatenate multiple MP4 files into one (optionally mix a BGM track)")
|
|
36
|
+
.helpOption("-h, --help", "show help")
|
|
37
|
+
.option("-o, --output <file>", "output path (REQUIRED)")
|
|
38
|
+
.option("--method <method>", "demuxer | filter", "demuxer")
|
|
39
|
+
.option("--bgm <path>", "optional BGM file to mix in (local / URL / data: URI)")
|
|
40
|
+
.option("--bgm-volume <n>", "BGM volume (0..1)", parseFloat, 0.2)
|
|
41
|
+
.option("--bgm-mode <mode>", "loop | once", "loop")
|
|
42
|
+
.addHelpText("after", [
|
|
43
|
+
"",
|
|
44
|
+
"Example:",
|
|
45
|
+
" rf compositions concat clip1.mp4 clip2.mp4 clip3.mp4 -o final.mp4 \\",
|
|
46
|
+
" --bgm bgm/default.mp3 --bgm-volume 0.15",
|
|
47
|
+
"",
|
|
48
|
+
"Files are auto-uploaded — pass local paths directly. Don't pre-upload",
|
|
49
|
+
"via `rf files upload` and pass server-side paths; that's the old broken",
|
|
50
|
+
"pattern.",
|
|
51
|
+
].join("\n"))
|
|
52
|
+
.action(async (videos, opts) => {
|
|
53
|
+
if (!opts.output)
|
|
54
|
+
throw new Error("--output is required");
|
|
55
|
+
const { resolved, totalBytes } = await resolveFileInputs(videos, {
|
|
56
|
+
flagName: "<videos...>",
|
|
57
|
+
});
|
|
58
|
+
const bgm = opts.bgm
|
|
59
|
+
? await resolveFileInput(opts.bgm, { flagName: "--bgm" })
|
|
60
|
+
: null;
|
|
61
|
+
if (totalBytes)
|
|
62
|
+
info(`Uploading ${resolved.length} videos (${fmtSize(totalBytes + (bgm?.bytes ?? 0))})...`);
|
|
63
|
+
const r = await post("/api/v1/compositions/concat", {
|
|
64
|
+
videos: resolved.map((r) => r.url),
|
|
65
|
+
method: opts.method,
|
|
66
|
+
bgm_path: bgm?.url,
|
|
67
|
+
bgm_volume: opts.bgmVolume,
|
|
68
|
+
bgm_mode: opts.bgmMode,
|
|
69
|
+
});
|
|
70
|
+
await downloadTo(r.url, opts.output);
|
|
71
|
+
success(`Saved → ${opts.output}`);
|
|
72
|
+
print(r);
|
|
73
|
+
});
|
|
74
|
+
comp
|
|
75
|
+
.command("bgm")
|
|
76
|
+
.description("Add background music to an existing video")
|
|
77
|
+
.helpOption("-h, --help", "show help")
|
|
78
|
+
.requiredOption("-i, --input <file>", "input video (local / URL / data: URI)")
|
|
79
|
+
.requiredOption("--bgm <file>", "BGM audio file (local / URL / data: URI)")
|
|
80
|
+
.option("-o, --output <file>", "output path (REQUIRED)")
|
|
81
|
+
.option("--volume <n>", "BGM volume (0..1)", parseFloat, 0.2)
|
|
82
|
+
.option("--mode <mode>", "loop | once", "loop")
|
|
83
|
+
.action(async (opts) => {
|
|
84
|
+
if (!opts.output)
|
|
85
|
+
throw new Error("--output is required");
|
|
86
|
+
const v = await resolveFileInput(opts.input, { flagName: "-i / --input" });
|
|
87
|
+
const b = await resolveFileInput(opts.bgm, { flagName: "--bgm" });
|
|
88
|
+
info(`Uploading video (${fmtSize(v.bytes ?? 0)}) + bgm (${fmtSize(b.bytes ?? 0)})...`);
|
|
89
|
+
const r = await post("/api/v1/compositions/bgm", {
|
|
90
|
+
video: v.url,
|
|
91
|
+
bgm: b.url,
|
|
92
|
+
volume: opts.volume,
|
|
93
|
+
mode: opts.mode,
|
|
94
|
+
});
|
|
95
|
+
await downloadTo(r.url, opts.output);
|
|
96
|
+
success(`Saved → ${opts.output}`);
|
|
97
|
+
print(r);
|
|
98
|
+
});
|
|
99
|
+
comp
|
|
100
|
+
.command("image-to-video")
|
|
101
|
+
.description("Build a video from a single image + an audio track")
|
|
102
|
+
.helpOption("-h, --help", "show help")
|
|
103
|
+
.requiredOption("-i, --image <file>", "input image (local / URL / data: URI)")
|
|
104
|
+
.requiredOption("-a, --audio <file>", "input audio (local / URL / data: URI)")
|
|
105
|
+
.option("-o, --output <file>", "output path (REQUIRED)")
|
|
106
|
+
.option("--fps <n>", "frames per second", parseInt, 30)
|
|
107
|
+
.action(async (opts) => {
|
|
108
|
+
if (!opts.output)
|
|
109
|
+
throw new Error("--output is required");
|
|
110
|
+
const img = await resolveFileInput(opts.image, { flagName: "-i / --image" });
|
|
111
|
+
const aud = await resolveFileInput(opts.audio, { flagName: "-a / --audio" });
|
|
112
|
+
info(`Uploading image (${fmtSize(img.bytes ?? 0)}) + audio (${fmtSize(aud.bytes ?? 0)})...`);
|
|
113
|
+
const r = await post("/api/v1/compositions/image-to-video", {
|
|
114
|
+
image: img.url,
|
|
115
|
+
audio: aud.url,
|
|
116
|
+
fps: opts.fps,
|
|
117
|
+
});
|
|
118
|
+
await downloadTo(r.url, opts.output);
|
|
119
|
+
success(`Saved → ${opts.output}`);
|
|
120
|
+
print(r);
|
|
121
|
+
});
|
|
122
|
+
comp
|
|
123
|
+
.command("overlay")
|
|
124
|
+
.description("Overlay a transparent PNG on top of a video")
|
|
125
|
+
.helpOption("-h, --help", "show help")
|
|
126
|
+
.requiredOption("-v, --video <file>", "input video (local / URL / data: URI)")
|
|
127
|
+
.requiredOption("--overlay <file>", "overlay PNG, transparent (local / URL / data: URI)")
|
|
128
|
+
.option("-o, --output <file>", "output path (REQUIRED)")
|
|
129
|
+
.action(async (opts) => {
|
|
130
|
+
if (!opts.output)
|
|
131
|
+
throw new Error("--output is required");
|
|
132
|
+
const v = await resolveFileInput(opts.video, { flagName: "-v / --video" });
|
|
133
|
+
const o = await resolveFileInput(opts.overlay, { flagName: "--overlay" });
|
|
134
|
+
info(`Uploading video (${fmtSize(v.bytes ?? 0)}) + overlay (${fmtSize(o.bytes ?? 0)})...`);
|
|
135
|
+
const r = await post("/api/v1/compositions/overlay", {
|
|
136
|
+
video: v.url,
|
|
137
|
+
overlay_image: o.url,
|
|
138
|
+
});
|
|
139
|
+
await downloadTo(r.url, opts.output);
|
|
140
|
+
success(`Saved → ${opts.output}`);
|
|
141
|
+
print(r);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import { get, patch } from "../client.js";
|
|
3
|
+
import { print } from "../utils/output.js";
|
|
4
|
+
export function registerConfig(program) {
|
|
5
|
+
const cfg = program
|
|
6
|
+
.command("config")
|
|
7
|
+
.description("读取或更新服务器配置 (LLM / 模型网关 keys)")
|
|
8
|
+
.helpOption("-h, --help", "show help");
|
|
9
|
+
cfg
|
|
10
|
+
.command("get")
|
|
11
|
+
.description("Print the current config (API keys masked)")
|
|
12
|
+
.helpOption("-h, --help", "show help")
|
|
13
|
+
.action(async () => {
|
|
14
|
+
const r = await get("/api/v1/config");
|
|
15
|
+
print(r);
|
|
16
|
+
});
|
|
17
|
+
cfg
|
|
18
|
+
.command("set <key> <value>")
|
|
19
|
+
.description("Update a single config value using dotted path, e.g. `llm.api_key sk-xxx`")
|
|
20
|
+
.helpOption("-h, --help", "show help")
|
|
21
|
+
.addHelpText("after", [
|
|
22
|
+
"",
|
|
23
|
+
"Examples:",
|
|
24
|
+
" reelforge config set llm.api_key sk-xxxxxx",
|
|
25
|
+
" reelforge config set llm.model <model-id>",
|
|
26
|
+
].join("\n"))
|
|
27
|
+
.action(async (key, value) => {
|
|
28
|
+
const parts = key.split(".");
|
|
29
|
+
const patchBody = {};
|
|
30
|
+
let cur = patchBody;
|
|
31
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
32
|
+
const next = {};
|
|
33
|
+
cur[parts[i]] = next;
|
|
34
|
+
cur = next;
|
|
35
|
+
}
|
|
36
|
+
cur[parts[parts.length - 1]] = coerce(value);
|
|
37
|
+
const r = await patch("/api/v1/config", patchBody);
|
|
38
|
+
print(r);
|
|
39
|
+
});
|
|
40
|
+
cfg
|
|
41
|
+
.command("patch <file>")
|
|
42
|
+
.description("Apply a JSON-merge patch file to the config")
|
|
43
|
+
.helpOption("-h, --help", "show help")
|
|
44
|
+
.action(async (file) => {
|
|
45
|
+
const body = JSON.parse(await fs.readFile(file, "utf-8"));
|
|
46
|
+
const r = await patch("/api/v1/config", body);
|
|
47
|
+
print(r);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
function coerce(s) {
|
|
51
|
+
if (s === "true")
|
|
52
|
+
return true;
|
|
53
|
+
if (s === "false")
|
|
54
|
+
return false;
|
|
55
|
+
if (s === "null")
|
|
56
|
+
return null;
|
|
57
|
+
if (/^-?\d+$/.test(s))
|
|
58
|
+
return parseInt(s, 10);
|
|
59
|
+
if (/^-?\d+\.\d+$/.test(s))
|
|
60
|
+
return parseFloat(s);
|
|
61
|
+
return s;
|
|
62
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import { post } from "../client.js";
|
|
3
|
+
import { print } from "../utils/output.js";
|
|
4
|
+
export function registerContent(program) {
|
|
5
|
+
const content = program
|
|
6
|
+
.command("content")
|
|
7
|
+
.description("内容原子能力: scene-plan (脚本 + 时长 → 分镜 + 每镜图像 prompt)")
|
|
8
|
+
.helpOption("-h, --help", "show help");
|
|
9
|
+
content
|
|
10
|
+
.command("scene-plan")
|
|
11
|
+
.description("Generate a master script + per-scene image prompts (replaces narration/image-prompts/title)")
|
|
12
|
+
.helpOption("-h, --help", "show help")
|
|
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")
|
|
18
|
+
.addHelpText("after", [
|
|
19
|
+
"",
|
|
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",
|
|
28
|
+
].join("\n"))
|
|
29
|
+
.action(async (opts) => {
|
|
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,
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
}
|