reelforge 1.24.0 → 1.24.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.
- package/README.md +295 -295
- package/bin/reelforge.js +8 -8
- package/dist/commands/assets.js +15 -4
- 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/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 +43 -0
- package/package.json +44 -44
package/dist/commands/compose.js
CHANGED
|
@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import fsSync from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { post, getServer } from "../client.js";
|
|
5
|
+
import { uploadToOss } from "../utils/oss-upload.js";
|
|
5
6
|
import { waitForTask } from "../utils/task-waiter.js";
|
|
6
7
|
import { downloadTo } from "../utils/download.js";
|
|
7
8
|
import { formatUsd, humanDuration, info, print, success, warn } from "../utils/output.js";
|
|
@@ -18,15 +19,6 @@ const EXT_TO_MIME = {
|
|
|
18
19
|
".mov": "video/quicktime",
|
|
19
20
|
".webm": "video/webm",
|
|
20
21
|
};
|
|
21
|
-
function fmtSize(bytes) {
|
|
22
|
-
if (!bytes)
|
|
23
|
-
return "0";
|
|
24
|
-
if (bytes < 1024)
|
|
25
|
-
return `${bytes} B`;
|
|
26
|
-
if (bytes < 1024 * 1024)
|
|
27
|
-
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
28
|
-
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
29
|
-
}
|
|
30
22
|
function listSpecPaths(spec) {
|
|
31
23
|
const paths = new Set();
|
|
32
24
|
if (spec.audio.narration)
|
|
@@ -59,35 +51,35 @@ async function readAsDataUri(specPath, baseDir) {
|
|
|
59
51
|
const buf = await fs.readFile(abs);
|
|
60
52
|
return { dataUri: `data:${mime};base64,${buf.toString("base64")}`, bytes: buf.byteLength };
|
|
61
53
|
}
|
|
62
|
-
const EXAMPLE_SPEC_HINT = `{
|
|
63
|
-
"experimental": "compose.v2",
|
|
64
|
-
"audio": { "narration": "./narration.mp3" },
|
|
65
|
-
"scenes": [
|
|
66
|
-
{ "start_sec": 0, "end_sec": 5, "image": "./s1.png", "motion": "zoom-in" },
|
|
67
|
-
{ "start_sec": 5, "end_sec": 11, "video": "./clip.mp4", "muted": true }
|
|
68
|
-
],
|
|
69
|
-
"subtitles": [
|
|
70
|
-
{ "start_sec": 0, "end_sec": 5, "text": "为什么咖啡因让人精神" },
|
|
71
|
-
{ "start_sec": 5, "end_sec": 11, "text": "原来它跟睡眠物质是同一把钥匙" }
|
|
72
|
-
],
|
|
73
|
-
"title": "咖啡因的真相"
|
|
54
|
+
const EXAMPLE_SPEC_HINT = `{
|
|
55
|
+
"experimental": "compose.v2",
|
|
56
|
+
"audio": { "narration": "./narration.mp3" },
|
|
57
|
+
"scenes": [
|
|
58
|
+
{ "start_sec": 0, "end_sec": 5, "image": "./s1.png", "motion": "zoom-in" },
|
|
59
|
+
{ "start_sec": 5, "end_sec": 11, "video": "./clip.mp4", "muted": true }
|
|
60
|
+
],
|
|
61
|
+
"subtitles": [
|
|
62
|
+
{ "start_sec": 0, "end_sec": 5, "text": "为什么咖啡因让人精神" },
|
|
63
|
+
{ "start_sec": 5, "end_sec": 11, "text": "原来它跟睡眠物质是同一把钥匙" }
|
|
64
|
+
],
|
|
65
|
+
"title": "咖啡因的真相"
|
|
74
66
|
}`;
|
|
75
|
-
const EXAMPLE_SPEC_V3_HINT = `{
|
|
76
|
-
"experimental": "compose.v3",
|
|
77
|
-
"audio": { "tts_voice": "熊小二" },
|
|
78
|
-
"scenes": [
|
|
79
|
-
{
|
|
80
|
-
"video": "./s1.mp4", "muted": true,
|
|
81
|
-
"narration": ["今天热得不行", "我跑厕所睡觉"],
|
|
82
|
-
"intro_title": "热到躲厕所"
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
"image": "./splash.jpg",
|
|
86
|
-
"narration": ["主人你找拖鞋去吧"]
|
|
87
|
-
}
|
|
88
|
-
],
|
|
89
|
-
"subtitle_style": "stroke",
|
|
90
|
-
"title": "贼喵被抓现行"
|
|
67
|
+
const EXAMPLE_SPEC_V3_HINT = `{
|
|
68
|
+
"experimental": "compose.v3",
|
|
69
|
+
"audio": { "tts_voice": "熊小二" },
|
|
70
|
+
"scenes": [
|
|
71
|
+
{
|
|
72
|
+
"video": "./s1.mp4", "muted": true,
|
|
73
|
+
"narration": ["今天热得不行", "我跑厕所睡觉"],
|
|
74
|
+
"intro_title": "热到躲厕所"
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"image": "./splash.jpg",
|
|
78
|
+
"narration": ["主人你找拖鞋去吧"]
|
|
79
|
+
}
|
|
80
|
+
],
|
|
81
|
+
"subtitle_style": "stroke",
|
|
82
|
+
"title": "贼喵被抓现行"
|
|
91
83
|
}`;
|
|
92
84
|
export function registerCompose(program) {
|
|
93
85
|
program
|
|
@@ -188,14 +180,27 @@ async function renderComposeOnce(specPath, opts) {
|
|
|
188
180
|
info(`Reading spec from ${specPath}...`);
|
|
189
181
|
const specPaths = listSpecPaths(spec);
|
|
190
182
|
const files = {};
|
|
191
|
-
|
|
183
|
+
const fileKeys = {};
|
|
192
184
|
for (const p of specPaths) {
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
185
|
+
const abs = path.isAbsolute(p) ? p : path.resolve(baseDir, p);
|
|
186
|
+
if (!fsSync.existsSync(abs)) {
|
|
187
|
+
throw new Error(`compose: spec references "${p}" but file not found at ${abs}`);
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
fileKeys[p] = await uploadToOss(abs);
|
|
191
|
+
}
|
|
192
|
+
catch (e) {
|
|
193
|
+
warn(`${p}: OSS 直传不可用,回退内联上传 (${e instanceof Error ? e.message : e})`);
|
|
194
|
+
const r = await readAsDataUri(p, baseDir);
|
|
195
|
+
files[p] = r.dataUri;
|
|
196
|
+
}
|
|
196
197
|
}
|
|
197
|
-
info(`Bundling ${specPaths.length} files (${
|
|
198
|
-
const body = { spec
|
|
198
|
+
info(`Bundling ${specPaths.length} files (${Object.keys(fileKeys).length} 直传 OSS, ${Object.keys(files).length} 内联)...`);
|
|
199
|
+
const body = { spec };
|
|
200
|
+
if (Object.keys(fileKeys).length)
|
|
201
|
+
body.file_keys = fileKeys;
|
|
202
|
+
if (Object.keys(files).length)
|
|
203
|
+
body.files = files;
|
|
199
204
|
if (opts.previewOnly)
|
|
200
205
|
body.preview_only = true;
|
|
201
206
|
const submitted = await post("/api/v1/compose", body);
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { post } from "../client.js";
|
|
2
2
|
import { downloadTo } from "../utils/download.js";
|
|
3
|
-
import { print, success, info } from "../utils/output.js";
|
|
4
|
-
import { fmtSize,
|
|
3
|
+
import { print, success, info, warn } from "../utils/output.js";
|
|
4
|
+
import { fmtSize, resolveUploadInput, resolveUploadInputs } from "../utils/file-upload.js";
|
|
5
|
+
const onOssFallback = (flagName, reason) => warn(`${flagName}: OSS 直传不可用,回退内联上传 (${reason})`);
|
|
5
6
|
const WHEN_TO_USE = [
|
|
6
7
|
"",
|
|
7
8
|
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
|
|
@@ -52,18 +53,22 @@ export function registerCompositions(program) {
|
|
|
52
53
|
.action(async (videos, opts) => {
|
|
53
54
|
if (!opts.output)
|
|
54
55
|
throw new Error("--output is required");
|
|
55
|
-
const { resolved, totalBytes } = await
|
|
56
|
+
const { resolved, totalBytes } = await resolveUploadInputs(videos, {
|
|
56
57
|
flagName: "<videos...>",
|
|
58
|
+
onFallback: onOssFallback,
|
|
57
59
|
});
|
|
58
60
|
const bgm = opts.bgm
|
|
59
|
-
? await
|
|
61
|
+
? await resolveUploadInput(opts.bgm, { flagName: "--bgm", onFallback: onOssFallback })
|
|
60
62
|
: null;
|
|
61
63
|
if (totalBytes)
|
|
62
64
|
info(`Uploading ${resolved.length} videos (${fmtSize(totalBytes + (bgm?.bytes ?? 0))})...`);
|
|
65
|
+
const videoKeys = resolved.filter((r) => r.key).map((r) => r.key);
|
|
66
|
+
const videoUris = resolved.filter((r) => r.url).map((r) => r.url);
|
|
63
67
|
const r = await post("/api/v1/compositions/concat", {
|
|
64
|
-
videos:
|
|
68
|
+
videos: videoUris,
|
|
69
|
+
video_keys: videoKeys,
|
|
65
70
|
method: opts.method,
|
|
66
|
-
|
|
71
|
+
...(bgm?.key ? { bgm_key: bgm.key } : bgm?.url ? { bgm_path: bgm.url } : {}),
|
|
67
72
|
bgm_volume: opts.bgmVolume,
|
|
68
73
|
bgm_mode: opts.bgmMode,
|
|
69
74
|
});
|
|
@@ -83,12 +88,12 @@ export function registerCompositions(program) {
|
|
|
83
88
|
.action(async (opts) => {
|
|
84
89
|
if (!opts.output)
|
|
85
90
|
throw new Error("--output is required");
|
|
86
|
-
const v = await
|
|
87
|
-
const b = await
|
|
91
|
+
const v = await resolveUploadInput(opts.input, { flagName: "-i / --input", onFallback: onOssFallback });
|
|
92
|
+
const b = await resolveUploadInput(opts.bgm, { flagName: "--bgm", onFallback: onOssFallback });
|
|
88
93
|
info(`Uploading video (${fmtSize(v.bytes ?? 0)}) + bgm (${fmtSize(b.bytes ?? 0)})...`);
|
|
89
94
|
const r = await post("/api/v1/compositions/bgm", {
|
|
90
|
-
video: v.url,
|
|
91
|
-
bgm: b.url,
|
|
95
|
+
...(v.key ? { video_key: v.key } : { video: v.url }),
|
|
96
|
+
...(b.key ? { bgm_key: b.key } : { bgm: b.url }),
|
|
92
97
|
volume: opts.volume,
|
|
93
98
|
mode: opts.mode,
|
|
94
99
|
});
|
|
@@ -107,12 +112,12 @@ export function registerCompositions(program) {
|
|
|
107
112
|
.action(async (opts) => {
|
|
108
113
|
if (!opts.output)
|
|
109
114
|
throw new Error("--output is required");
|
|
110
|
-
const img = await
|
|
111
|
-
const aud = await
|
|
115
|
+
const img = await resolveUploadInput(opts.image, { flagName: "-i / --image", onFallback: onOssFallback });
|
|
116
|
+
const aud = await resolveUploadInput(opts.audio, { flagName: "-a / --audio", onFallback: onOssFallback });
|
|
112
117
|
info(`Uploading image (${fmtSize(img.bytes ?? 0)}) + audio (${fmtSize(aud.bytes ?? 0)})...`);
|
|
113
118
|
const r = await post("/api/v1/compositions/image-to-video", {
|
|
114
|
-
image: img.url,
|
|
115
|
-
audio: aud.url,
|
|
119
|
+
...(img.key ? { image_key: img.key } : { image: img.url }),
|
|
120
|
+
...(aud.key ? { audio_key: aud.key } : { audio: aud.url }),
|
|
116
121
|
fps: opts.fps,
|
|
117
122
|
});
|
|
118
123
|
await downloadTo(r.url, opts.output);
|
|
@@ -129,12 +134,12 @@ export function registerCompositions(program) {
|
|
|
129
134
|
.action(async (opts) => {
|
|
130
135
|
if (!opts.output)
|
|
131
136
|
throw new Error("--output is required");
|
|
132
|
-
const v = await
|
|
133
|
-
const o = await
|
|
137
|
+
const v = await resolveUploadInput(opts.video, { flagName: "-v / --video", onFallback: onOssFallback });
|
|
138
|
+
const o = await resolveUploadInput(opts.overlay, { flagName: "--overlay", onFallback: onOssFallback });
|
|
134
139
|
info(`Uploading video (${fmtSize(v.bytes ?? 0)}) + overlay (${fmtSize(o.bytes ?? 0)})...`);
|
|
135
140
|
const r = await post("/api/v1/compositions/overlay", {
|
|
136
|
-
video: v.url,
|
|
137
|
-
overlay_image: o.url,
|
|
141
|
+
...(v.key ? { video_key: v.key } : { video: v.url }),
|
|
142
|
+
...(o.key ? { overlay_key: o.key } : { overlay_image: o.url }),
|
|
138
143
|
});
|
|
139
144
|
await downloadTo(r.url, opts.output);
|
|
140
145
|
success(`Saved → ${opts.output}`);
|
package/dist/commands/cover.js
CHANGED
|
@@ -2,7 +2,9 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import fsSync from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { getServer, getApiKey } from "../client.js";
|
|
5
|
+
import { uploadToOss } from "../utils/oss-upload.js";
|
|
5
6
|
import { info, success, warn } from "../utils/output.js";
|
|
7
|
+
const MAX_BYTES = 50 * 1024 * 1024;
|
|
6
8
|
const TEMPLATES = [
|
|
7
9
|
{ id: "paper-band", label: "牛皮纸条 横卡", supports: "title, subtitle, badge", useFor: "财经 / 干货 / 商业分析" },
|
|
8
10
|
{ id: "carbon-copy", label: "复写纸 暗调", supports: "title, subtitle", useFor: "情感 / 哲思 / 治愈(默认)" },
|
|
@@ -17,6 +19,21 @@ const TEMPLATES = [
|
|
|
17
19
|
const TEMPLATE_IDS = new Set(TEMPLATES.map((t) => t.id));
|
|
18
20
|
const DEFAULT_TEMPLATE = "carbon-copy";
|
|
19
21
|
const SMALL_IMAGE_WARN_BYTES = 100 * 1024;
|
|
22
|
+
async function uploadOrInline(fileAbs, fileMime, label) {
|
|
23
|
+
const bytes = (await fs.stat(fileAbs)).size;
|
|
24
|
+
try {
|
|
25
|
+
const key = await uploadToOss(fileAbs);
|
|
26
|
+
return { key, bytes };
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
if (bytes > MAX_BYTES) {
|
|
30
|
+
throw new Error(`${label} 直传 OSS 失败 (${e instanceof Error ? e.message : e}),且文件 ${fmtSize(bytes)} 超过内联回退上限 ${fmtSize(MAX_BYTES)}。`);
|
|
31
|
+
}
|
|
32
|
+
warn(`${label}: OSS 直传不可用,回退内联上传 (${e instanceof Error ? e.message : e})`);
|
|
33
|
+
const buf = await fs.readFile(fileAbs);
|
|
34
|
+
return { uri: `data:${fileMime};base64,${buf.toString("base64")}`, bytes };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
20
37
|
async function resolveImage(input) {
|
|
21
38
|
const t = input.trim();
|
|
22
39
|
if (!t)
|
|
@@ -37,9 +54,8 @@ async function resolveImage(input) {
|
|
|
37
54
|
if (!mime) {
|
|
38
55
|
throw new Error(`--image: unsupported extension ${ext} (use png/jpg/jpeg/webp)`);
|
|
39
56
|
}
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
return { url, bytes: buf.byteLength };
|
|
57
|
+
const up = await uploadOrInline(abs, mime, "--image");
|
|
58
|
+
return { key: up.key, url: up.uri, bytes: up.bytes };
|
|
43
59
|
}
|
|
44
60
|
function fmtSize(b) {
|
|
45
61
|
if (b < 1024)
|
|
@@ -177,7 +193,7 @@ export function registerCover(program) {
|
|
|
177
193
|
if (missing.length > 0) {
|
|
178
194
|
throw new Error(`required: ${missing.join(", ")}`);
|
|
179
195
|
}
|
|
180
|
-
const { url: imageUrl, bytes } = await resolveImage(opts.image);
|
|
196
|
+
const { key: imageKey, url: imageUrl, bytes } = await resolveImage(opts.image);
|
|
181
197
|
if (typeof bytes === "number" && bytes < SMALL_IMAGE_WARN_BYTES) {
|
|
182
198
|
warn(`--image is ${fmtSize(bytes)} — that's quite small, the cover may look blurry. ` +
|
|
183
199
|
`For best results use a source image of at least 1080×1920.`);
|
|
@@ -188,10 +204,13 @@ export function registerCover(program) {
|
|
|
188
204
|
}
|
|
189
205
|
const params = buildParamsPayload(opts.param, opts.params);
|
|
190
206
|
const body = {
|
|
191
|
-
image_url: imageUrl,
|
|
192
207
|
template,
|
|
193
208
|
title: opts.title,
|
|
194
209
|
};
|
|
210
|
+
if (imageKey)
|
|
211
|
+
body.image_key = imageKey;
|
|
212
|
+
else
|
|
213
|
+
body.image_url = imageUrl;
|
|
195
214
|
if (opts.subtitle)
|
|
196
215
|
body.subtitle = opts.subtitle;
|
|
197
216
|
if (opts.badge)
|
|
@@ -251,17 +270,22 @@ export function registerCover(program) {
|
|
|
251
270
|
if (!fsSync.existsSync(coverAbs)) {
|
|
252
271
|
throw new Error(`cover not found: ${coverAbs}`);
|
|
253
272
|
}
|
|
254
|
-
const coverBuf = await fs.readFile(coverAbs);
|
|
255
|
-
const videoBuf = await fs.readFile(videoAbs);
|
|
256
273
|
const coverExt = path.extname(coverAbs).toLowerCase();
|
|
257
274
|
const coverMime = coverExt === ".jpg" || coverExt === ".jpeg" ? "image/jpeg" : "image/png";
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
275
|
+
const coverUp = await uploadOrInline(coverAbs, coverMime, "cover");
|
|
276
|
+
const videoUp = await uploadOrInline(videoAbs, "video/mp4", "video");
|
|
277
|
+
const body = {};
|
|
278
|
+
if (coverUp.key)
|
|
279
|
+
body.cover_key = coverUp.key;
|
|
280
|
+
else
|
|
281
|
+
body.cover_data_uri = coverUp.uri;
|
|
282
|
+
if (videoUp.key)
|
|
283
|
+
body.video_key = videoUp.key;
|
|
284
|
+
else
|
|
285
|
+
body.video_data_uri = videoUp.uri;
|
|
262
286
|
if (typeof opts.fps === "number")
|
|
263
287
|
body.fps = opts.fps;
|
|
264
|
-
info(`Prepending cover (${fmtSize(
|
|
288
|
+
info(`Prepending cover (${fmtSize(coverUp.bytes)}) → video (${fmtSize(videoUp.bytes)})...`);
|
|
265
289
|
const server = getServer().replace(/\/+$/, "");
|
|
266
290
|
const apiKey = getApiKey();
|
|
267
291
|
const headers = { "content-type": "application/json" };
|
package/dist/commands/create.js
CHANGED
|
@@ -3,6 +3,7 @@ import fsSync from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import { post, getServer } from "../client.js";
|
|
6
|
+
import { uploadToOss } from "../utils/oss-upload.js";
|
|
6
7
|
import { waitForTask } from "../utils/task-waiter.js";
|
|
7
8
|
import { downloadTo } from "../utils/download.js";
|
|
8
9
|
import { formatUsd, humanDuration, info, print, success, warn } from "../utils/output.js";
|
|
@@ -38,14 +39,15 @@ async function resolveTextOrFile(input) {
|
|
|
38
39
|
}
|
|
39
40
|
return input;
|
|
40
41
|
}
|
|
41
|
-
|
|
42
|
+
const REF_IMAGE_MAX_BYTES = 15 * 1024 * 1024;
|
|
43
|
+
async function resolveRefImageUpload(input, flagName) {
|
|
42
44
|
if (input === undefined)
|
|
43
45
|
return undefined;
|
|
44
46
|
const t = input.trim();
|
|
45
47
|
if (!t)
|
|
46
48
|
return undefined;
|
|
47
49
|
if (/^https?:\/\//i.test(t) || t.startsWith("data:"))
|
|
48
|
-
return t;
|
|
50
|
+
return { passthrough: t };
|
|
49
51
|
const abs = path.resolve(t);
|
|
50
52
|
if (!fsSync.existsSync(abs)) {
|
|
51
53
|
throw new Error(`${flagName}: local file not found: ${abs}`);
|
|
@@ -58,8 +60,19 @@ async function resolveRefImage(input, flagName) {
|
|
|
58
60
|
if (!mime) {
|
|
59
61
|
throw new Error(`${flagName}: unsupported extension ${ext} (use png/jpg/jpeg/webp)`);
|
|
60
62
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
+
try {
|
|
64
|
+
const key = await uploadToOss(abs);
|
|
65
|
+
return { key };
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
const sz = (await fs.stat(abs)).size;
|
|
69
|
+
if (sz > REF_IMAGE_MAX_BYTES) {
|
|
70
|
+
throw new Error(`${flagName} 直传 OSS 失败 (${e instanceof Error ? e.message : e}),且文件超过内联回退上限 ${REF_IMAGE_MAX_BYTES} 字节。`);
|
|
71
|
+
}
|
|
72
|
+
warn(`${flagName}: OSS 直传不可用,回退内联上传 (${e instanceof Error ? e.message : e})`);
|
|
73
|
+
const buf = await fs.readFile(abs);
|
|
74
|
+
return { passthrough: `data:${mime};base64,${buf.toString("base64")}` };
|
|
75
|
+
}
|
|
63
76
|
}
|
|
64
77
|
async function loadRecipe(recipePath) {
|
|
65
78
|
const raw = await fs.readFile(recipePath, "utf-8");
|
|
@@ -512,17 +525,30 @@ export function registerCreate(program) {
|
|
|
512
525
|
" # (compose your script using /tmp/ref.txt as raw material in your agent)\n" +
|
|
513
526
|
" rf create --script @final.txt -d 60 # render it");
|
|
514
527
|
}
|
|
515
|
-
const resolvedChar = await
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
528
|
+
const resolvedChar = await resolveRefImageUpload(body.character_ref, "--character-ref");
|
|
529
|
+
delete body.character_ref;
|
|
530
|
+
delete body.character_ref_key;
|
|
531
|
+
if (resolvedChar?.key)
|
|
532
|
+
body.character_ref_key = resolvedChar.key;
|
|
533
|
+
else if (resolvedChar?.passthrough)
|
|
534
|
+
body.character_ref = resolvedChar.passthrough;
|
|
520
535
|
if (body.brand?.logo_url) {
|
|
521
|
-
const resolvedLogo = await
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
536
|
+
const resolvedLogo = await resolveRefImageUpload(body.brand.logo_url, "--brand-logo");
|
|
537
|
+
delete body.brand.logo_url;
|
|
538
|
+
delete body.brand_logo_key;
|
|
539
|
+
if (resolvedLogo?.key)
|
|
540
|
+
body.brand_logo_key = resolvedLogo.key;
|
|
541
|
+
else if (resolvedLogo?.passthrough)
|
|
542
|
+
body.brand.logo_url = resolvedLogo.passthrough;
|
|
543
|
+
}
|
|
544
|
+
if (body.cover?.base) {
|
|
545
|
+
const resolvedBase = await resolveRefImageUpload(body.cover.base, "--cover-base");
|
|
546
|
+
delete body.cover.base;
|
|
547
|
+
delete body.cover_base_key;
|
|
548
|
+
if (resolvedBase?.key)
|
|
549
|
+
body.cover_base_key = resolvedBase.key;
|
|
550
|
+
else if (resolvedBase?.passthrough)
|
|
551
|
+
body.cover.base = resolvedBase.passthrough;
|
|
526
552
|
}
|
|
527
553
|
const hasTopic = typeof body.topic === "string" && body.topic.trim().length > 0;
|
|
528
554
|
const hasScript = typeof body.script === "string" && body.script.trim().length > 0;
|
|
@@ -545,7 +571,11 @@ export function registerCreate(program) {
|
|
|
545
571
|
}
|
|
546
572
|
info(`Submitting create task (≈ ${formatUsd(estimate)})...`);
|
|
547
573
|
const submitted = await post("/api/v1/pipelines/standard", finalBody);
|
|
548
|
-
|
|
574
|
+
const toSave = { ...finalBody };
|
|
575
|
+
delete toSave.character_ref_key;
|
|
576
|
+
delete toSave.brand_logo_key;
|
|
577
|
+
delete toSave.cover_base_key;
|
|
578
|
+
await saveLastCreate(toSave).catch((e) => {
|
|
549
579
|
warn(`Could not save last-create.json: ${e.message}`);
|
|
550
580
|
});
|
|
551
581
|
if (opts.wait === false) {
|
package/dist/commands/dub.js
CHANGED
|
@@ -2,9 +2,10 @@ 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 { waitForTask } from "../utils/task-waiter.js";
|
|
6
7
|
import { downloadTo } from "../utils/download.js";
|
|
7
|
-
import { formatUsd, humanDuration, info, isJson, print, success } from "../utils/output.js";
|
|
8
|
+
import { formatUsd, humanDuration, info, isJson, print, success, warn } from "../utils/output.js";
|
|
8
9
|
const VIDEO_EXT_TO_MIME = {
|
|
9
10
|
".mp4": "video/mp4",
|
|
10
11
|
".mov": "video/quicktime",
|
|
@@ -107,14 +108,7 @@ export function registerDub(program) {
|
|
|
107
108
|
if (!["plate", "stroke", "cinema"].includes(subtitleStyle)) {
|
|
108
109
|
throw new Error(`--subtitle-style must be plate | stroke | cinema (got ${opts.subtitleStyle})`);
|
|
109
110
|
}
|
|
110
|
-
const buf = await fs.readFile(abs);
|
|
111
|
-
if (buf.byteLength > MAX_BYTES) {
|
|
112
|
-
throw new Error(`video is ${fmtSize(buf.byteLength)} — exceeds the ${fmtSize(MAX_BYTES)} upload cap. ` +
|
|
113
|
-
`Compress / trim it first (e.g. ffmpeg -crf 28).`);
|
|
114
|
-
}
|
|
115
|
-
info(`Uploading ${path.basename(abs)} (${fmtSize(buf.byteLength)})...`);
|
|
116
111
|
const body = {
|
|
117
|
-
video: `data:${mime};base64,${buf.toString("base64")}`,
|
|
118
112
|
voice: opts.voice || "熊小二",
|
|
119
113
|
tts_model: opts.ttsModel || "vox/index-tts-2",
|
|
120
114
|
game_audio_volume: gameVolume,
|
|
@@ -132,8 +126,30 @@ export function registerDub(program) {
|
|
|
132
126
|
body.cover_badge = opts.coverBadge;
|
|
133
127
|
if (opts.theme)
|
|
134
128
|
body.theme = opts.theme;
|
|
129
|
+
const uploadOrInline = async (fileAbs, fileMime, label) => {
|
|
130
|
+
try {
|
|
131
|
+
const key = await uploadToOss(fileAbs);
|
|
132
|
+
return { key };
|
|
133
|
+
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
const sz = (await fs.stat(fileAbs)).size;
|
|
136
|
+
if (sz > MAX_BYTES) {
|
|
137
|
+
throw new Error(`${label} 直传 OSS 失败 (${e instanceof Error ? e.message : e}),且文件 ${fmtSize(sz)} 超过内联回退上限 ${fmtSize(MAX_BYTES)}。`);
|
|
138
|
+
}
|
|
139
|
+
warn(`${label}: OSS 直传不可用,回退内联上传 (${e instanceof Error ? e.message : e})`);
|
|
140
|
+
const buf = await fs.readFile(fileAbs);
|
|
141
|
+
return { uri: `data:${fileMime};base64,${buf.toString("base64")}` };
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
info(`Uploading ${path.basename(abs)} (${fmtSize((await fs.stat(abs)).size)}) → 直传对象存储...`);
|
|
145
|
+
const mainUp = await uploadOrInline(abs, mime, "video");
|
|
146
|
+
if (mainUp.key)
|
|
147
|
+
body.video_key = mainUp.key;
|
|
148
|
+
else
|
|
149
|
+
body.video = mainUp.uri;
|
|
135
150
|
const appendFiles = opts.append ?? [];
|
|
136
151
|
if (appendFiles.length) {
|
|
152
|
+
const appendKeys = [];
|
|
137
153
|
const appendUris = [];
|
|
138
154
|
for (const f of appendFiles) {
|
|
139
155
|
const aAbs = path.resolve(f);
|
|
@@ -143,14 +159,17 @@ export function registerDub(program) {
|
|
|
143
159
|
const aMime = VIDEO_EXT_TO_MIME[aExt];
|
|
144
160
|
if (!aMime)
|
|
145
161
|
throw new Error(`--append: unsupported extension "${aExt}" (${path.basename(aAbs)})`);
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
162
|
+
info(` + append ${path.basename(aAbs)} (${fmtSize((await fs.stat(aAbs)).size)})`);
|
|
163
|
+
const up = await uploadOrInline(aAbs, aMime, `--append ${path.basename(aAbs)}`);
|
|
164
|
+
if (up.key)
|
|
165
|
+
appendKeys.push(up.key);
|
|
166
|
+
else if (up.uri)
|
|
167
|
+
appendUris.push(up.uri);
|
|
152
168
|
}
|
|
153
|
-
|
|
169
|
+
if (appendKeys.length)
|
|
170
|
+
body.append_keys = appendKeys;
|
|
171
|
+
if (appendUris.length)
|
|
172
|
+
body.append = appendUris;
|
|
154
173
|
}
|
|
155
174
|
if (opts.visionModel)
|
|
156
175
|
body.vision_model = opts.visionModel;
|
package/dist/commands/extract.js
CHANGED
|
@@ -2,7 +2,9 @@ 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, success, warn } from "../utils/output.js";
|
|
7
|
+
const MAX_BYTES = 25 * 1024 * 1024;
|
|
6
8
|
const FILE_EXT_TO_MIME = {
|
|
7
9
|
".pdf": "application/pdf",
|
|
8
10
|
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
@@ -13,13 +15,13 @@ async function resolveSource(input) {
|
|
|
13
15
|
if (!t)
|
|
14
16
|
throw new Error("--reference / source is empty");
|
|
15
17
|
if (t.startsWith("data:"))
|
|
16
|
-
return t;
|
|
18
|
+
return { source: t };
|
|
17
19
|
if (/^https?:\/\//i.test(t))
|
|
18
|
-
return t;
|
|
20
|
+
return { source: t };
|
|
19
21
|
if (/v\.douyin\.com\//i.test(t) || /复制此链接/.test(t) || /打开抖音/.test(t))
|
|
20
|
-
return t;
|
|
22
|
+
return { source: t };
|
|
21
23
|
if (/^[a-z0-9][a-z0-9-._]*\/[a-z0-9][a-z0-9-._]*$/i.test(t) && !t.split("/")[0].includes(".")) {
|
|
22
|
-
return t;
|
|
24
|
+
return { source: t };
|
|
23
25
|
}
|
|
24
26
|
const abs = path.resolve(t);
|
|
25
27
|
if (!fsSync.existsSync(abs)) {
|
|
@@ -32,8 +34,19 @@ async function resolveSource(input) {
|
|
|
32
34
|
throw new Error(`unsupported file extension "${ext}". ` +
|
|
33
35
|
`Supported: ${Object.keys(FILE_EXT_TO_MIME).join(" / ")}, or pass a URL / github owner/repo / 抖音 link.`);
|
|
34
36
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
try {
|
|
38
|
+
const key = await uploadToOss(abs);
|
|
39
|
+
return { source_key: key };
|
|
40
|
+
}
|
|
41
|
+
catch (e) {
|
|
42
|
+
const sz = (await fs.stat(abs)).size;
|
|
43
|
+
if (sz > MAX_BYTES) {
|
|
44
|
+
throw new Error(`${path.basename(abs)} 直传 OSS 失败 (${e instanceof Error ? e.message : e}),且文件超过内联回退上限 ${MAX_BYTES} 字节。`);
|
|
45
|
+
}
|
|
46
|
+
warn(`source: OSS 直传不可用,回退内联上传 (${e instanceof Error ? e.message : e})`);
|
|
47
|
+
const buf = await fs.readFile(abs);
|
|
48
|
+
return { source: `data:${mime};base64,${buf.toString("base64")}` };
|
|
49
|
+
}
|
|
37
50
|
}
|
|
38
51
|
export function registerExtract(program) {
|
|
39
52
|
program
|
|
@@ -81,7 +94,7 @@ export function registerExtract(program) {
|
|
|
81
94
|
].join("\n"))
|
|
82
95
|
.action(async (source, opts) => {
|
|
83
96
|
const resolved = await resolveSource(source);
|
|
84
|
-
const r = await post("/api/v1/reference/extract", {
|
|
97
|
+
const r = await post("/api/v1/reference/extract", { ...resolved });
|
|
85
98
|
if (opts.output) {
|
|
86
99
|
const outAbs = path.resolve(opts.output);
|
|
87
100
|
await fs.mkdir(path.dirname(outAbs), { recursive: true });
|
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);
|