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.
@@ -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
- const fileObj = new File([new Uint8Array(buf)], name);
34
- const r = await uploadMultipart("/api/v1/files", { file: fileObj });
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
  });
@@ -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
- const buf = await fs.readFile(abs);
26
- return `data:${mime};base64,${buf.toString("base64")}`;
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
- body.image_urls = await Promise.all(opts.image.map(resolveImageRef));
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]) {
@@ -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 { info, isJson, print } from "../utils/output.js";
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
- const buf = await fs.readFile(abs);
116
- body = { data_uri: `data:${mime};base64,${buf.toString("base64")}` };
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.0",
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
+ }