reelforge 1.24.0 → 1.24.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 -295
- package/bin/reelforge.js +8 -8
- package/dist/commands/assets.js +15 -4
- package/dist/commands/audio.js +34 -16
- package/dist/commands/bgm.js +19 -5
- package/dist/commands/compose.js +48 -43
- package/dist/commands/compositions.js +23 -18
- package/dist/commands/cover.js +36 -12
- package/dist/commands/create.js +45 -15
- package/dist/commands/dub.js +34 -15
- package/dist/commands/extract.js +21 -8
- package/dist/commands/files.js +19 -5
- package/dist/commands/images.js +24 -5
- package/dist/commands/media.js +11 -3
- package/dist/utils/file-upload.js +43 -0
- package/dist/utils/oss-upload.js +44 -0
- package/package.json +44 -44
package/dist/commands/files.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { del, get, uploadMultipart } from "../client.js";
|
|
3
|
+
import { del, get, post, uploadMultipart } from "../client.js";
|
|
4
|
+
import { uploadToOss } from "../utils/oss-upload.js";
|
|
4
5
|
import { downloadTo } from "../utils/download.js";
|
|
5
|
-
import { print, table, success, bytes } from "../utils/output.js";
|
|
6
|
+
import { print, table, success, bytes, warn } from "../utils/output.js";
|
|
6
7
|
export function registerFiles(program) {
|
|
7
8
|
const files = program
|
|
8
9
|
.command("files")
|
|
@@ -28,10 +29,23 @@ export function registerFiles(program) {
|
|
|
28
29
|
.helpOption("-h, --help", "show help")
|
|
29
30
|
.action(async (file) => {
|
|
30
31
|
const abs = path.resolve(file);
|
|
31
|
-
const buf = fs.readFileSync(abs);
|
|
32
32
|
const name = path.basename(abs);
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
let key = null;
|
|
34
|
+
try {
|
|
35
|
+
key = await uploadToOss(abs);
|
|
36
|
+
}
|
|
37
|
+
catch (e) {
|
|
38
|
+
warn(`OSS 直传不可用,回退 multipart 上传 (${e instanceof Error ? e.message : e})`);
|
|
39
|
+
}
|
|
40
|
+
let r;
|
|
41
|
+
if (key) {
|
|
42
|
+
r = await post("/api/v1/files", { key, filename: name });
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
const buf = fs.readFileSync(abs);
|
|
46
|
+
const fileObj = new File([new Uint8Array(buf)], name);
|
|
47
|
+
r = await uploadMultipart("/api/v1/files", { file: fileObj });
|
|
48
|
+
}
|
|
35
49
|
success(`Uploaded → ${r.relative_path}`);
|
|
36
50
|
print(r);
|
|
37
51
|
});
|
package/dist/commands/images.js
CHANGED
|
@@ -2,14 +2,16 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import fsSync from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { post } from "../client.js";
|
|
5
|
+
import { uploadToOss } from "../utils/oss-upload.js";
|
|
5
6
|
import { downloadTo } from "../utils/download.js";
|
|
6
|
-
import { print, success } from "../utils/output.js";
|
|
7
|
+
import { print, success, warn } from "../utils/output.js";
|
|
8
|
+
const MAX_REF_BYTES = 15 * 1024 * 1024;
|
|
7
9
|
async function resolveImageRef(input) {
|
|
8
10
|
const t = input.trim();
|
|
9
11
|
if (!t)
|
|
10
12
|
throw new Error("--image: empty value");
|
|
11
13
|
if (/^https?:\/\//i.test(t) || t.startsWith("data:"))
|
|
12
|
-
return t;
|
|
14
|
+
return { url: t };
|
|
13
15
|
const abs = path.resolve(t);
|
|
14
16
|
if (!fsSync.existsSync(abs)) {
|
|
15
17
|
throw new Error(`--image: local file not found: ${abs}`);
|
|
@@ -22,8 +24,19 @@ async function resolveImageRef(input) {
|
|
|
22
24
|
if (!mime) {
|
|
23
25
|
throw new Error(`--image: unsupported extension ${ext} (use png/jpg/jpeg/webp)`);
|
|
24
26
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
try {
|
|
28
|
+
const key = await uploadToOss(abs);
|
|
29
|
+
return { key };
|
|
30
|
+
}
|
|
31
|
+
catch (e) {
|
|
32
|
+
const sz = (await fs.stat(abs)).size;
|
|
33
|
+
if (sz > MAX_REF_BYTES) {
|
|
34
|
+
throw new Error(`--image ${path.basename(abs)} 直传 OSS 失败 (${e instanceof Error ? e.message : e}),且文件超过内联回退上限 ${MAX_REF_BYTES} 字节。`);
|
|
35
|
+
}
|
|
36
|
+
warn(`--image ${path.basename(abs)}: OSS 直传不可用,回退内联上传 (${e instanceof Error ? e.message : e})`);
|
|
37
|
+
const buf = await fs.readFile(abs);
|
|
38
|
+
return { url: `data:${mime};base64,${buf.toString("base64")}` };
|
|
39
|
+
}
|
|
27
40
|
}
|
|
28
41
|
export function registerImages(program) {
|
|
29
42
|
const images = program
|
|
@@ -69,7 +82,13 @@ export function registerImages(program) {
|
|
|
69
82
|
if (opts.image.length > 3) {
|
|
70
83
|
throw new Error(`--image accepts at most 3 refs (got ${opts.image.length})`);
|
|
71
84
|
}
|
|
72
|
-
|
|
85
|
+
const resolved = await Promise.all(opts.image.map(resolveImageRef));
|
|
86
|
+
const urls = resolved.filter((r) => r.url).map((r) => r.url);
|
|
87
|
+
const keys = resolved.filter((r) => r.key).map((r) => r.key);
|
|
88
|
+
if (urls.length)
|
|
89
|
+
body.image_urls = urls;
|
|
90
|
+
if (keys.length)
|
|
91
|
+
body.image_keys = keys;
|
|
73
92
|
}
|
|
74
93
|
const r = await post("/api/v1/images/generate", body);
|
|
75
94
|
if (opts.output && r.images?.[0]) {
|
package/dist/commands/media.js
CHANGED
|
@@ -2,7 +2,8 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import fsSync from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { post } from "../client.js";
|
|
5
|
-
import {
|
|
5
|
+
import { uploadToOss } from "../utils/oss-upload.js";
|
|
6
|
+
import { info, isJson, print, warn } from "../utils/output.js";
|
|
6
7
|
const MEDIA_EXT_TO_MIME = {
|
|
7
8
|
".mp4": "video/mp4",
|
|
8
9
|
".mov": "video/quicktime",
|
|
@@ -112,8 +113,15 @@ export function registerMedia(program) {
|
|
|
112
113
|
throw new Error(`unsupported extension "${ext}". Supported: ${Object.keys(MEDIA_EXT_TO_MIME).join(" / ")}, ` +
|
|
113
114
|
`or pass a URL.`);
|
|
114
115
|
}
|
|
115
|
-
|
|
116
|
-
|
|
116
|
+
try {
|
|
117
|
+
const key = await uploadToOss(abs);
|
|
118
|
+
body = { key };
|
|
119
|
+
}
|
|
120
|
+
catch (e) {
|
|
121
|
+
warn(`${path.basename(abs)}: OSS 直传不可用,回退内联上传 (${e instanceof Error ? e.message : e})`);
|
|
122
|
+
const buf = await fs.readFile(abs);
|
|
123
|
+
body = { data_uri: `data:${mime};base64,${buf.toString("base64")}` };
|
|
124
|
+
}
|
|
117
125
|
label = path.basename(abs);
|
|
118
126
|
}
|
|
119
127
|
const r = await post("/api/v1/media/probe", body);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import fsSync from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { uploadToOss } from "./oss-upload.js";
|
|
4
5
|
export const EXT_TO_MIME = {
|
|
5
6
|
".png": "image/png",
|
|
6
7
|
".jpg": "image/jpeg",
|
|
@@ -57,3 +58,45 @@ export function fmtSize(bytes) {
|
|
|
57
58
|
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
58
59
|
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
59
60
|
}
|
|
61
|
+
export async function resolveUploadInput(input, opts) {
|
|
62
|
+
const t = input.trim();
|
|
63
|
+
if (!t)
|
|
64
|
+
throw new Error(`${opts.flagName}: empty value`);
|
|
65
|
+
if (/^https?:\/\//i.test(t))
|
|
66
|
+
return { url: t, kind: "http-url" };
|
|
67
|
+
if (t.startsWith("data:"))
|
|
68
|
+
return { url: t, kind: "data-uri" };
|
|
69
|
+
const abs = path.resolve(t);
|
|
70
|
+
if (!fsSync.existsSync(abs)) {
|
|
71
|
+
throw new Error(`${opts.flagName}: local file not found: ${abs}\n` +
|
|
72
|
+
` (Acceptable forms: local path / https:// URL / data: URI. ` +
|
|
73
|
+
`Server-side paths like 'data/uploads/...' do NOT work — CLI commands ` +
|
|
74
|
+
`auto-upload local files, so just pass the local path directly.)`);
|
|
75
|
+
}
|
|
76
|
+
const ext = path.extname(abs).toLowerCase();
|
|
77
|
+
const map = opts.extToMime ?? EXT_TO_MIME;
|
|
78
|
+
const mime = map[ext];
|
|
79
|
+
if (!mime) {
|
|
80
|
+
throw new Error(`${opts.flagName}: unsupported file extension "${ext}" (path: ${abs}). ` +
|
|
81
|
+
`Supported: ${Object.keys(map).join(" / ")}`);
|
|
82
|
+
}
|
|
83
|
+
const bytes = (await fs.stat(abs)).size;
|
|
84
|
+
try {
|
|
85
|
+
const key = await uploadToOss(abs);
|
|
86
|
+
return { key, bytes, kind: "oss-key" };
|
|
87
|
+
}
|
|
88
|
+
catch (e) {
|
|
89
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
90
|
+
opts.onFallback?.(opts.flagName, reason);
|
|
91
|
+
const buf = await fs.readFile(abs);
|
|
92
|
+
return { url: `data:${mime};base64,${buf.toString("base64")}`, bytes, kind: "local-file-inline" };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
export async function resolveUploadInputs(inputs, opts) {
|
|
96
|
+
const resolved = [];
|
|
97
|
+
for (const input of inputs) {
|
|
98
|
+
resolved.push(await resolveUploadInput(input, opts));
|
|
99
|
+
}
|
|
100
|
+
const totalBytes = resolved.reduce((n, r) => n + (r.bytes ?? 0), 0);
|
|
101
|
+
return { resolved, totalBytes };
|
|
102
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import fsSync from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { post } from "../client.js";
|
|
5
|
+
const EXT_TO_MIME = {
|
|
6
|
+
".mp4": "video/mp4",
|
|
7
|
+
".mov": "video/quicktime",
|
|
8
|
+
".webm": "video/webm",
|
|
9
|
+
".mkv": "video/x-matroska",
|
|
10
|
+
".m4v": "video/mp4",
|
|
11
|
+
".jpg": "image/jpeg",
|
|
12
|
+
".jpeg": "image/jpeg",
|
|
13
|
+
".png": "image/png",
|
|
14
|
+
".webp": "image/webp",
|
|
15
|
+
".mp3": "audio/mpeg",
|
|
16
|
+
".wav": "audio/wav",
|
|
17
|
+
".m4a": "audio/mp4",
|
|
18
|
+
".ogg": "audio/ogg",
|
|
19
|
+
".flac": "audio/flac",
|
|
20
|
+
".pdf": "application/pdf",
|
|
21
|
+
};
|
|
22
|
+
export function mimeForFile(filePath) {
|
|
23
|
+
return EXT_TO_MIME[path.extname(filePath).toLowerCase()] || "application/octet-stream";
|
|
24
|
+
}
|
|
25
|
+
export async function uploadToOss(filePath) {
|
|
26
|
+
if (!fsSync.existsSync(filePath))
|
|
27
|
+
throw new Error(`file not found: ${filePath}`);
|
|
28
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
29
|
+
const contentType = mimeForFile(filePath);
|
|
30
|
+
const signed = await post("/api/v1/uploads/sign", { ext, content_type: contentType });
|
|
31
|
+
if (!signed?.put_url || !signed?.key)
|
|
32
|
+
throw new Error("uploads/sign returned no put_url/key");
|
|
33
|
+
const bytes = await fs.readFile(filePath);
|
|
34
|
+
const res = await fetch(signed.put_url, {
|
|
35
|
+
method: "PUT",
|
|
36
|
+
headers: { "Content-Type": contentType },
|
|
37
|
+
body: bytes,
|
|
38
|
+
});
|
|
39
|
+
if (!res.ok) {
|
|
40
|
+
const detail = await res.text().catch(() => "");
|
|
41
|
+
throw new Error(`OSS PUT failed: ${res.status} ${res.statusText} ${detail.slice(0, 200)}`);
|
|
42
|
+
}
|
|
43
|
+
return signed.key;
|
|
44
|
+
}
|
package/package.json
CHANGED
|
@@ -1,44 +1,44 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "reelforge",
|
|
3
|
-
"version": "1.24.
|
|
4
|
-
"description": "AI 视频生成 CLI。一句话主题或自己的脚本 → 自动出抖音/TikTok/视频号竖屏 MP4。安装即用:`reelforge` 或短别名 `rf`。",
|
|
5
|
-
"license": "Apache-2.0",
|
|
6
|
-
"type": "module",
|
|
7
|
-
"bin": {
|
|
8
|
-
"reelforge": "./bin/reelforge.js",
|
|
9
|
-
"rf": "./bin/reelforge.js"
|
|
10
|
-
},
|
|
11
|
-
"files": [
|
|
12
|
-
"bin",
|
|
13
|
-
"dist",
|
|
14
|
-
"README.md"
|
|
15
|
-
],
|
|
16
|
-
"engines": {
|
|
17
|
-
"node": ">=22"
|
|
18
|
-
},
|
|
19
|
-
"scripts": {
|
|
20
|
-
"build": "tsc -p tsconfig.json && node scripts/copy-docs.mjs",
|
|
21
|
-
"dev": "tsc -p tsconfig.json --watch",
|
|
22
|
-
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
23
|
-
"clean": "rimraf dist",
|
|
24
|
-
"prepublishOnly": "npm run clean && npm run build"
|
|
25
|
-
},
|
|
26
|
-
"dependencies": {
|
|
27
|
-
"commander": "^12.1.0",
|
|
28
|
-
"kleur": "^4.1.5"
|
|
29
|
-
},
|
|
30
|
-
"devDependencies": {
|
|
31
|
-
"@types/node": "^20.14.0",
|
|
32
|
-
"rimraf": "^6.0.1",
|
|
33
|
-
"typescript": "^5.5.0"
|
|
34
|
-
},
|
|
35
|
-
"keywords": [
|
|
36
|
-
"reelforge",
|
|
37
|
-
"ai-video",
|
|
38
|
-
"douyin",
|
|
39
|
-
"tiktok",
|
|
40
|
-
"shorts",
|
|
41
|
-
"vertical-video",
|
|
42
|
-
"cli"
|
|
43
|
-
]
|
|
44
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "reelforge",
|
|
3
|
+
"version": "1.24.2",
|
|
4
|
+
"description": "AI 视频生成 CLI。一句话主题或自己的脚本 → 自动出抖音/TikTok/视频号竖屏 MP4。安装即用:`reelforge` 或短别名 `rf`。",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"reelforge": "./bin/reelforge.js",
|
|
9
|
+
"rf": "./bin/reelforge.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"bin",
|
|
13
|
+
"dist",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=22"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc -p tsconfig.json && node scripts/copy-docs.mjs",
|
|
21
|
+
"dev": "tsc -p tsconfig.json --watch",
|
|
22
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
23
|
+
"clean": "rimraf dist",
|
|
24
|
+
"prepublishOnly": "npm run clean && npm run build"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"commander": "^12.1.0",
|
|
28
|
+
"kleur": "^4.1.5"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^20.14.0",
|
|
32
|
+
"rimraf": "^6.0.1",
|
|
33
|
+
"typescript": "^5.5.0"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"reelforge",
|
|
37
|
+
"ai-video",
|
|
38
|
+
"douyin",
|
|
39
|
+
"tiktok",
|
|
40
|
+
"shorts",
|
|
41
|
+
"vertical-video",
|
|
42
|
+
"cli"
|
|
43
|
+
]
|
|
44
|
+
}
|