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,7 +1,8 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { uploadMultipart, post } from "../client.js";
4
- import { print } from "../utils/output.js";
4
+ import { uploadToOss } from "../utils/oss-upload.js";
5
+ import { print, warn } from "../utils/output.js";
5
6
  export function registerAudio(program) {
6
7
  const audio = program
7
8
  .command("audio")
@@ -32,21 +33,38 @@ export function registerAudio(program) {
32
33
  }
33
34
  let r;
34
35
  if (opts.file) {
35
- const buf = await fs.readFile(opts.file);
36
- const filename = path.basename(opts.file);
37
- const ext = path.extname(filename).toLowerCase();
38
- const mime = ext === ".wav" ? "audio/wav" :
39
- ext === ".m4a" ? "audio/mp4" :
40
- ext === ".flac" ? "audio/flac" :
41
- ext === ".ogg" ? "audio/ogg" :
42
- "audio/mpeg";
43
- const fileBlob = new File([new Uint8Array(buf)], filename, { type: mime });
44
- const fields = { file: fileBlob };
45
- if (opts.language)
46
- fields.language = opts.language;
47
- if (opts.model)
48
- fields.model = opts.model;
49
- r = await uploadMultipart("/api/v1/audio/transcribe", fields);
36
+ let key = null;
37
+ try {
38
+ key = await uploadToOss(opts.file);
39
+ }
40
+ catch (e) {
41
+ warn(`OSS 直传不可用,回退 multipart 上传 (${e instanceof Error ? e.message : e})`);
42
+ }
43
+ if (key) {
44
+ const body = { key };
45
+ if (opts.language)
46
+ body.language = opts.language;
47
+ if (opts.model)
48
+ body.model = opts.model;
49
+ r = await post("/api/v1/audio/transcribe", body);
50
+ }
51
+ else {
52
+ const buf = await fs.readFile(opts.file);
53
+ const filename = path.basename(opts.file);
54
+ const ext = path.extname(filename).toLowerCase();
55
+ const mime = ext === ".wav" ? "audio/wav" :
56
+ ext === ".m4a" ? "audio/mp4" :
57
+ ext === ".flac" ? "audio/flac" :
58
+ ext === ".ogg" ? "audio/ogg" :
59
+ "audio/mpeg";
60
+ const fileBlob = new File([new Uint8Array(buf)], filename, { type: mime });
61
+ const fields = { file: fileBlob };
62
+ if (opts.language)
63
+ fields.language = opts.language;
64
+ if (opts.model)
65
+ fields.model = opts.model;
66
+ r = await uploadMultipart("/api/v1/audio/transcribe", fields);
67
+ }
50
68
  }
51
69
  else {
52
70
  const body = { audio_url: opts.url };
@@ -1,7 +1,8 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { del, get, uploadMultipart } from "../client.js";
4
- import { print, table, success, bytes } from "../utils/output.js";
3
+ import { del, get, post, uploadMultipart } from "../client.js";
4
+ import { uploadToOss } from "../utils/oss-upload.js";
5
+ import { print, table, success, bytes, warn } from "../utils/output.js";
5
6
  export function registerBgm(program) {
6
7
  const bgm = program
7
8
  .command("bgm")
@@ -26,10 +27,23 @@ export function registerBgm(program) {
26
27
  .helpOption("-h, --help", "show help")
27
28
  .action(async (file) => {
28
29
  const abs = path.resolve(file);
29
- const buf = fs.readFileSync(abs);
30
30
  const name = path.basename(abs);
31
- const fileObj = new File([new Uint8Array(buf)], name, { type: "audio/mpeg" });
32
- const r = await uploadMultipart("/api/v1/bgm", { file: fileObj });
31
+ let r;
32
+ let key = null;
33
+ try {
34
+ key = await uploadToOss(abs);
35
+ }
36
+ catch (e) {
37
+ warn(`OSS 直传不可用,回退 multipart 上传 (${e instanceof Error ? e.message : e})`);
38
+ }
39
+ if (key) {
40
+ r = await post("/api/v1/bgm", { key, filename: name });
41
+ }
42
+ else {
43
+ const buf = fs.readFileSync(abs);
44
+ const fileObj = new File([new Uint8Array(buf)], name, { type: "audio/mpeg" });
45
+ r = await uploadMultipart("/api/v1/bgm", { file: fileObj });
46
+ }
33
47
  success(`Uploaded ${name}`);
34
48
  print(r);
35
49
  });
@@ -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
- let totalBytes = 0;
183
+ const fileKeys = {};
192
184
  for (const p of specPaths) {
193
- const r = await readAsDataUri(p, baseDir);
194
- files[p] = r.dataUri;
195
- totalBytes += r.bytes;
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 (${fmtSize(totalBytes)} total)...`);
198
- const body = { spec, files };
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, resolveFileInput, resolveFileInputs } from "../utils/file-upload.js";
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 resolveFileInputs(videos, {
56
+ const { resolved, totalBytes } = await resolveUploadInputs(videos, {
56
57
  flagName: "<videos...>",
58
+ onFallback: onOssFallback,
57
59
  });
58
60
  const bgm = opts.bgm
59
- ? await resolveFileInput(opts.bgm, { flagName: "--bgm" })
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: resolved.map((r) => r.url),
68
+ videos: videoUris,
69
+ video_keys: videoKeys,
65
70
  method: opts.method,
66
- bgm_path: bgm?.url,
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 resolveFileInput(opts.input, { flagName: "-i / --input" });
87
- const b = await resolveFileInput(opts.bgm, { flagName: "--bgm" });
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 resolveFileInput(opts.image, { flagName: "-i / --image" });
111
- const aud = await resolveFileInput(opts.audio, { flagName: "-a / --audio" });
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 resolveFileInput(opts.video, { flagName: "-v / --video" });
133
- const o = await resolveFileInput(opts.overlay, { flagName: "--overlay" });
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}`);
@@ -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 buf = await fs.readFile(abs);
41
- const url = `data:${mime};base64,${buf.toString("base64")}`;
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 body = {
259
- cover_data_uri: `data:${coverMime};base64,${coverBuf.toString("base64")}`,
260
- video_data_uri: `data:video/mp4;base64,${videoBuf.toString("base64")}`,
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(coverBuf.byteLength)}) → video (${fmtSize(videoBuf.byteLength)})...`);
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" };
@@ -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
- async function resolveRefImage(input, flagName) {
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
- const buf = await fs.readFile(abs);
62
- return `data:${mime};base64,${buf.toString("base64")}`;
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 resolveRefImage(body.character_ref, "--character-ref");
516
- if (resolvedChar !== undefined)
517
- body.character_ref = resolvedChar;
518
- else
519
- delete body.character_ref;
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 resolveRefImage(body.brand.logo_url, "--brand-logo");
522
- if (resolvedLogo !== undefined)
523
- body.brand.logo_url = resolvedLogo;
524
- else
525
- delete body.brand.logo_url;
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
- await saveLastCreate(finalBody).catch((e) => {
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) {
@@ -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
- const aBuf = await fs.readFile(aAbs);
147
- if (aBuf.byteLength > MAX_BYTES) {
148
- throw new Error(`--append clip ${path.basename(aAbs)} is ${fmtSize(aBuf.byteLength)} — exceeds ${fmtSize(MAX_BYTES)}.`);
149
- }
150
- info(` + append ${path.basename(aAbs)} (${fmtSize(aBuf.byteLength)})`);
151
- appendUris.push(`data:${aMime};base64,${aBuf.toString("base64")}`);
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
- body.append = appendUris;
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;
@@ -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 { info, isJson, print, success } from "../utils/output.js";
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
- const buf = await fs.readFile(abs);
36
- return `data:${mime};base64,${buf.toString("base64")}`;
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", { source: resolved });
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 });