@gitruck/cli 0.2.7 → 0.2.9
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/AGENT.md +8 -0
- package/README.md +60 -4
- package/dist/index.js +1899 -3
- package/package.json +64 -62
- package/skills/gtrk-mg/SKILL.md +3 -3
- package/skills/gtrk-splitter/SKILL.md +2 -2
- package/skills/gtrk-splitter/references/field-schema.md +1 -1
- package/skills/gtrk-tools/SKILL.md +53 -0
package/dist/index.js
CHANGED
|
@@ -4197,7 +4197,7 @@ var {
|
|
|
4197
4197
|
|
|
4198
4198
|
// src/index.ts
|
|
4199
4199
|
import { readFileSync as readFileSync5 } from "node:fs";
|
|
4200
|
-
import { join as
|
|
4200
|
+
import { join as join26 } from "node:path";
|
|
4201
4201
|
|
|
4202
4202
|
// src/lib/paths.ts
|
|
4203
4203
|
import { dirname, join } from "node:path";
|
|
@@ -4221,6 +4221,9 @@ var GITRUCK_HOME = join(homedir(), ".gitruck");
|
|
|
4221
4221
|
function gitruckHome() {
|
|
4222
4222
|
return GITRUCK_HOME;
|
|
4223
4223
|
}
|
|
4224
|
+
function homeFile(name) {
|
|
4225
|
+
return join(GITRUCK_HOME, name);
|
|
4226
|
+
}
|
|
4224
4227
|
function ffmpegDir() {
|
|
4225
4228
|
return join(GITRUCK_HOME, "ffmpeg");
|
|
4226
4229
|
}
|
|
@@ -4280,7 +4283,8 @@ var SKILL_NAMES = [
|
|
|
4280
4283
|
"gtrk-matrix",
|
|
4281
4284
|
"gtrk-mg",
|
|
4282
4285
|
"gtrk-ai-drama",
|
|
4283
|
-
"gtrk-style-maker"
|
|
4286
|
+
"gtrk-style-maker",
|
|
4287
|
+
"gtrk-tools"
|
|
4284
4288
|
];
|
|
4285
4289
|
function installSkill(opts = {}) {
|
|
4286
4290
|
const destRoot = opts.dir ?? join2(homedir2(), ".claude", "skills");
|
|
@@ -8171,12 +8175,1903 @@ function done(opts, result) {
|
|
|
8171
8175
|
return result;
|
|
8172
8176
|
}
|
|
8173
8177
|
|
|
8178
|
+
// src/lib/tool-descriptors.ts
|
|
8179
|
+
import { extname as extname4 } from "node:path";
|
|
8180
|
+
var IMAGE_EXTS = [".jpg", ".jpeg", ".png", ".webp", ".bmp", ".gif", ".tif", ".tiff", ".heic", ".heif", ".avif"];
|
|
8181
|
+
var VIDEO_EXTS = [".mp4", ".mov", ".mkv", ".webm", ".avi", ".m4v", ".flv", ".wmv", ".mpg", ".mpeg", ".ts", ".m2ts"];
|
|
8182
|
+
var AUDIO_EXTS = [".mp3", ".wav", ".m4a", ".aac", ".flac", ".ogg", ".opus", ".wma"];
|
|
8183
|
+
function defaultExtsFor(kind) {
|
|
8184
|
+
if (kind === "image")
|
|
8185
|
+
return IMAGE_EXTS;
|
|
8186
|
+
if (kind === "video")
|
|
8187
|
+
return VIDEO_EXTS;
|
|
8188
|
+
if (kind === "audio")
|
|
8189
|
+
return AUDIO_EXTS;
|
|
8190
|
+
return;
|
|
8191
|
+
}
|
|
8192
|
+
function pickUrl(out, keys) {
|
|
8193
|
+
for (const k of keys) {
|
|
8194
|
+
const v = out[k];
|
|
8195
|
+
if (typeof v === "string" && v.trim())
|
|
8196
|
+
return v;
|
|
8197
|
+
}
|
|
8198
|
+
return;
|
|
8199
|
+
}
|
|
8200
|
+
function extFromUrl(url, fallback) {
|
|
8201
|
+
let path = url;
|
|
8202
|
+
try {
|
|
8203
|
+
path = new URL(url).pathname;
|
|
8204
|
+
} catch {
|
|
8205
|
+
path = url.split("?")[0] ?? url;
|
|
8206
|
+
}
|
|
8207
|
+
const e = extname4(path);
|
|
8208
|
+
return e || fallback;
|
|
8209
|
+
}
|
|
8210
|
+
function deriveMoveGeometry(dims) {
|
|
8211
|
+
if (!dims || !(dims.width > 0) || !(dims.height > 0))
|
|
8212
|
+
return { fellBack: true };
|
|
8213
|
+
return dims.height > dims.width ? { width: 1080, height: 1920, fellBack: false } : { width: 1920, height: 1080, fellBack: false };
|
|
8214
|
+
}
|
|
8215
|
+
function probeImageDims(inputAbs, ffmpegPath) {
|
|
8216
|
+
if (!resolveFfmpeg(ffmpegPath))
|
|
8217
|
+
return;
|
|
8218
|
+
try {
|
|
8219
|
+
const g2 = probeGeometry(inputAbs, ffmpegPath);
|
|
8220
|
+
if (g2.width > 0 && g2.height > 0)
|
|
8221
|
+
return { width: g2.width, height: g2.height };
|
|
8222
|
+
} catch {}
|
|
8223
|
+
return;
|
|
8224
|
+
}
|
|
8225
|
+
var imageMove = {
|
|
8226
|
+
name: "image_move",
|
|
8227
|
+
title: "图转运镜",
|
|
8228
|
+
description: "把一张静态图生成带运镜的短视频。",
|
|
8229
|
+
kind: "cloud",
|
|
8230
|
+
input: { kind: "image" },
|
|
8231
|
+
billingHint: "2 积分/个",
|
|
8232
|
+
outputHint: "运镜视频",
|
|
8233
|
+
enabled: true,
|
|
8234
|
+
taskType: "image_move",
|
|
8235
|
+
buildPayload(fileId, ctx) {
|
|
8236
|
+
const p = { file_id: fileId };
|
|
8237
|
+
const explicit = ctx.extraParams.width != null || ctx.extraParams.height != null;
|
|
8238
|
+
if (!explicit && ctx.inputAbs) {
|
|
8239
|
+
const dims = probeImageDims(ctx.inputAbs, ctx.ffmpegPath);
|
|
8240
|
+
const geo = deriveMoveGeometry(dims);
|
|
8241
|
+
if (geo.width && geo.height) {
|
|
8242
|
+
p.width = geo.width;
|
|
8243
|
+
p.height = geo.height;
|
|
8244
|
+
}
|
|
8245
|
+
if (geo.fellBack) {
|
|
8246
|
+
ctx.warn("未探测到图片朝向(缺 ffprobe),将按云端默认横屏 1920×1080 出片;" + "可用 --param width=… --param height=… 显式指定几何。");
|
|
8247
|
+
}
|
|
8248
|
+
}
|
|
8249
|
+
return p;
|
|
8250
|
+
},
|
|
8251
|
+
mapOutputs(out, ctx) {
|
|
8252
|
+
const url = pickUrl(out, ["download_url", "video_download_url", "url"]);
|
|
8253
|
+
if (!url)
|
|
8254
|
+
return [];
|
|
8255
|
+
return [{ url, filename: `${ctx.baseName}-image_move${extFromUrl(url, ".mp4")}` }];
|
|
8256
|
+
}
|
|
8257
|
+
};
|
|
8258
|
+
var imageMatting = {
|
|
8259
|
+
name: "image_matting",
|
|
8260
|
+
title: "图片抠像",
|
|
8261
|
+
description: "把图片主体从背景抠出,产透明背景 png(可经 --param 请求额外背景底板输出)。",
|
|
8262
|
+
kind: "cloud",
|
|
8263
|
+
input: { kind: "image" },
|
|
8264
|
+
billingHint: "免费",
|
|
8265
|
+
outputHint: "透明 png",
|
|
8266
|
+
enabled: true,
|
|
8267
|
+
taskType: "image_matting",
|
|
8268
|
+
buildPayload(fileId) {
|
|
8269
|
+
return { file_id: fileId, output_format: "png" };
|
|
8270
|
+
},
|
|
8271
|
+
mapOutputs(out, ctx) {
|
|
8272
|
+
const files = [];
|
|
8273
|
+
const main = pickUrl(out, ["download_url", "image_download_url", "url"]);
|
|
8274
|
+
if (main)
|
|
8275
|
+
files.push({ url: main, filename: `${ctx.baseName}-matte${extFromUrl(main, ".png")}` });
|
|
8276
|
+
const bg = pickUrl(out, ["background_download_url", "bg_download_url"]);
|
|
8277
|
+
if (bg)
|
|
8278
|
+
files.push({ url: bg, filename: `${ctx.baseName}-bg${extFromUrl(bg, ".png")}` });
|
|
8279
|
+
return files;
|
|
8280
|
+
}
|
|
8281
|
+
};
|
|
8282
|
+
var videoMatting = {
|
|
8283
|
+
name: "video_matting",
|
|
8284
|
+
title: "视频抠像",
|
|
8285
|
+
description: "把视频主体从背景抠出,产透明背景 webm(像素级、原片直传不压代理;单片 ≤10 分钟)。",
|
|
8286
|
+
kind: "cloud",
|
|
8287
|
+
input: { kind: "video", maxDurationSec: 600 },
|
|
8288
|
+
billingHint: "免费",
|
|
8289
|
+
outputHint: "透明 webm",
|
|
8290
|
+
enabled: true,
|
|
8291
|
+
taskType: "video_matting",
|
|
8292
|
+
pollTimeoutMs: 60 * 60 * 1000,
|
|
8293
|
+
buildPayload(fileId) {
|
|
8294
|
+
return { file_id: fileId, target_mode: "auto", output_format: "webm" };
|
|
8295
|
+
},
|
|
8296
|
+
mapOutputs(out, ctx) {
|
|
8297
|
+
const files = [];
|
|
8298
|
+
const main = pickUrl(out, ["download_url", "video_download_url", "url"]);
|
|
8299
|
+
if (main)
|
|
8300
|
+
files.push({ url: main, filename: `${ctx.baseName}-matte${extFromUrl(main, ".webm")}` });
|
|
8301
|
+
const mask2 = pickUrl(out, ["mask_download_url"]);
|
|
8302
|
+
if (mask2)
|
|
8303
|
+
files.push({ url: mask2, filename: `${ctx.baseName}-mask${extFromUrl(mask2, ".webm")}` });
|
|
8304
|
+
return files;
|
|
8305
|
+
}
|
|
8306
|
+
};
|
|
8307
|
+
var mad = {
|
|
8308
|
+
name: "mad",
|
|
8309
|
+
title: "一键剪 MAD",
|
|
8310
|
+
description: "素材文件夹(3~10 条视频)+ 可选 BGM → 自动选技法 → 单一 .jsx,AE 2020+ 跑一遍出 15~30s 卡点成片工程。仅支持 AE。",
|
|
8311
|
+
kind: "local",
|
|
8312
|
+
input: { kind: "directory" },
|
|
8313
|
+
billingHint: "免费(--bgm 卡点时计费一次云端节拍分析 audio_music_analyze)",
|
|
8314
|
+
outputHint: "AE 母合成工程 .jsx",
|
|
8315
|
+
enabled: true,
|
|
8316
|
+
options: [
|
|
8317
|
+
{ flag: "--bgm <音频文件>", desc: "可选 BGM,卡点到 downbeat(需 API Key,计费一次;无 Key 则 BGM 仍入轨、固定节奏)" },
|
|
8318
|
+
{ flag: "--duration <秒>", desc: "成片目标时长(默认 20,文案口径 15~30)" },
|
|
8319
|
+
{ flag: "--seed <n>", desc: "选窗随机种子(同素材同种子同数据版本 → 可复现同序列)" },
|
|
8320
|
+
{ flag: "--refresh", desc: "强制忽略本地缓存、重拉当前 manifest 版本数据" }
|
|
8321
|
+
]
|
|
8322
|
+
};
|
|
8323
|
+
var RESERVED_NAMES = new Set(["list"]);
|
|
8324
|
+
var TOOL_REGISTRY = [imageMove, imageMatting, videoMatting, mad];
|
|
8325
|
+
function findTool(name, registry = TOOL_REGISTRY) {
|
|
8326
|
+
return registry.find((d) => d.name === name);
|
|
8327
|
+
}
|
|
8328
|
+
function validateRegistry(registry = TOOL_REGISTRY) {
|
|
8329
|
+
const seen = new Set;
|
|
8330
|
+
for (const d of registry) {
|
|
8331
|
+
if (!d.name)
|
|
8332
|
+
throw new Error("descriptor 缺 name");
|
|
8333
|
+
if (RESERVED_NAMES.has(d.name))
|
|
8334
|
+
throw new Error(`descriptor 名与保留字冲突:「${d.name}」`);
|
|
8335
|
+
if (seen.has(d.name))
|
|
8336
|
+
throw new Error(`descriptor 名重复:「${d.name}」`);
|
|
8337
|
+
seen.add(d.name);
|
|
8338
|
+
if (!d.enabled && !d.disabledReason)
|
|
8339
|
+
throw new Error(`未启用工具缺 disabledReason:「${d.name}」`);
|
|
8340
|
+
if (d.kind === "cloud" && !d.taskType)
|
|
8341
|
+
throw new Error(`cloud 型工具缺 taskType:「${d.name}」`);
|
|
8342
|
+
}
|
|
8343
|
+
}
|
|
8344
|
+
|
|
8345
|
+
// src/lib/tool-runner.ts
|
|
8346
|
+
import { resolve as resolve9, join as join21, dirname as dirname8, basename as basename11, extname as extname5 } from "node:path";
|
|
8347
|
+
import { mkdir as mkdir8, writeFile as writeFile8, stat as stat5 } from "node:fs/promises";
|
|
8348
|
+
import { createWriteStream, existsSync as existsSync17 } from "node:fs";
|
|
8349
|
+
import { Readable as Readable2 } from "node:stream";
|
|
8350
|
+
import { pipeline } from "node:stream/promises";
|
|
8351
|
+
var DEFAULT_POLL_TIMEOUT_MS = 30 * 60 * 1000;
|
|
8352
|
+
var DEFAULT_POLL_INTERVAL_MS = 5000;
|
|
8353
|
+
function coerceValue2(v) {
|
|
8354
|
+
if (v === "true")
|
|
8355
|
+
return true;
|
|
8356
|
+
if (v === "false")
|
|
8357
|
+
return false;
|
|
8358
|
+
if (v.trim() !== "" && !Number.isNaN(Number(v)))
|
|
8359
|
+
return Number(v);
|
|
8360
|
+
return v;
|
|
8361
|
+
}
|
|
8362
|
+
function parseExtraParams2(pairs, jsonStr) {
|
|
8363
|
+
const out = {};
|
|
8364
|
+
for (const pair of pairs) {
|
|
8365
|
+
const i = pair.indexOf("=");
|
|
8366
|
+
if (i < 0)
|
|
8367
|
+
throw new Error(`--param 需要 key=value 格式:「${pair}」`);
|
|
8368
|
+
out[pair.slice(0, i).trim()] = coerceValue2(pair.slice(i + 1));
|
|
8369
|
+
}
|
|
8370
|
+
if (jsonStr) {
|
|
8371
|
+
let parsed;
|
|
8372
|
+
try {
|
|
8373
|
+
parsed = JSON.parse(jsonStr);
|
|
8374
|
+
} catch {
|
|
8375
|
+
throw new Error(`--params-json 不是合法 JSON:${jsonStr}`);
|
|
8376
|
+
}
|
|
8377
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
8378
|
+
throw new Error("--params-json 必须是一个 JSON 对象");
|
|
8379
|
+
}
|
|
8380
|
+
Object.assign(out, parsed);
|
|
8381
|
+
}
|
|
8382
|
+
return out;
|
|
8383
|
+
}
|
|
8384
|
+
function mergeParams(payload, extra) {
|
|
8385
|
+
for (const [k, v] of Object.entries(extra)) {
|
|
8386
|
+
const cur = payload[k];
|
|
8387
|
+
const bothObj = !!cur && !!v && typeof cur === "object" && typeof v === "object" && !Array.isArray(cur) && !Array.isArray(v);
|
|
8388
|
+
payload[k] = bothObj ? { ...cur, ...v } : v;
|
|
8389
|
+
}
|
|
8390
|
+
}
|
|
8391
|
+
function validateToolInput(descriptor, inputAbs) {
|
|
8392
|
+
const spec = descriptor.input;
|
|
8393
|
+
if (spec.kind === "none")
|
|
8394
|
+
return;
|
|
8395
|
+
if (!inputAbs)
|
|
8396
|
+
throw new Error(`${descriptor.name} 需要输入${spec.kind === "directory" ? "目录" : "文件"}`);
|
|
8397
|
+
if (!existsSync17(inputAbs))
|
|
8398
|
+
throw new Error(`输入不存在:${inputAbs}`);
|
|
8399
|
+
if (spec.kind === "directory")
|
|
8400
|
+
return;
|
|
8401
|
+
const exts = spec.exts ?? defaultExtsFor(spec.kind);
|
|
8402
|
+
if (exts && exts.length) {
|
|
8403
|
+
const e = extname5(inputAbs).toLowerCase();
|
|
8404
|
+
if (!exts.includes(e)) {
|
|
8405
|
+
throw new Error(`${descriptor.name} 需要 ${spec.kind} 输入,但拿到「${e || "无扩展名"}」(支持:${exts.join(" ")})`);
|
|
8406
|
+
}
|
|
8407
|
+
}
|
|
8408
|
+
}
|
|
8409
|
+
function guardDuration(descriptor, inputAbs, probe, ffmpegPath) {
|
|
8410
|
+
const max = descriptor.input.maxDurationSec;
|
|
8411
|
+
if (max == null || !inputAbs)
|
|
8412
|
+
return;
|
|
8413
|
+
const sec = probe(inputAbs, ffmpegPath);
|
|
8414
|
+
if (sec > max) {
|
|
8415
|
+
throw new Error(`视频超过 ${Math.round(max / 60)} 分钟上限,请先裁剪`);
|
|
8416
|
+
}
|
|
8417
|
+
}
|
|
8418
|
+
async function downloadStream(url, dest) {
|
|
8419
|
+
const res = await fetch(url);
|
|
8420
|
+
if (!res.ok || !res.body)
|
|
8421
|
+
throw new Error(`下载失败 HTTP ${res.status}:${url}`);
|
|
8422
|
+
await pipeline(Readable2.fromWeb(res.body), createWriteStream(dest));
|
|
8423
|
+
}
|
|
8424
|
+
async function pollToolTask(cfg, taskType, taskId, opts = {}) {
|
|
8425
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_POLL_TIMEOUT_MS;
|
|
8426
|
+
const intervalMs = opts.intervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
8427
|
+
const getResult = opts.getResult ?? getTaskResult;
|
|
8428
|
+
const sleep2 = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
8429
|
+
const now = opts.now ?? Date.now;
|
|
8430
|
+
const start = now();
|
|
8431
|
+
for (;; ) {
|
|
8432
|
+
if (now() - start > timeoutMs) {
|
|
8433
|
+
throw new Error(`任务超时(超过 ${Math.round(timeoutMs / 60000)} 分钟)。可稍后凭 task_id(${taskId})在云端查询或重试。`);
|
|
8434
|
+
}
|
|
8435
|
+
await sleep2(intervalMs);
|
|
8436
|
+
let got;
|
|
8437
|
+
try {
|
|
8438
|
+
got = await getResult(cfg, taskType, taskId);
|
|
8439
|
+
} catch (e) {
|
|
8440
|
+
if (isCloudErrorCode(e) != null)
|
|
8441
|
+
throw e;
|
|
8442
|
+
continue;
|
|
8443
|
+
}
|
|
8444
|
+
if (got.status === "completed")
|
|
8445
|
+
return got.output;
|
|
8446
|
+
if (got.status === "failed" || got.status === "cancelled") {
|
|
8447
|
+
const out = got.output;
|
|
8448
|
+
throw new Error(out?.error ?? (got.status === "failed" ? "任务失败" : "任务已取消"));
|
|
8449
|
+
}
|
|
8450
|
+
opts.onTick?.(got.status || "处理中", got.progress);
|
|
8451
|
+
}
|
|
8452
|
+
}
|
|
8453
|
+
function isCloudErrorCode(e) {
|
|
8454
|
+
const c3 = e && typeof e === "object" ? e.code : undefined;
|
|
8455
|
+
return typeof c3 === "number" ? c3 : undefined;
|
|
8456
|
+
}
|
|
8457
|
+
function timestamp3() {
|
|
8458
|
+
const d = new Date;
|
|
8459
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
8460
|
+
return `${p(d.getFullYear() % 100)}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
|
|
8461
|
+
}
|
|
8462
|
+
function resolveOutDir(descriptor, inputAbs, out) {
|
|
8463
|
+
if (out)
|
|
8464
|
+
return resolve9(out);
|
|
8465
|
+
if (inputAbs) {
|
|
8466
|
+
const base = basename11(inputAbs, extname5(inputAbs));
|
|
8467
|
+
return join21(dirname8(inputAbs), `${base}-${descriptor.name}`);
|
|
8468
|
+
}
|
|
8469
|
+
return join21(process.cwd(), `${descriptor.name}-${timestamp3()}`);
|
|
8470
|
+
}
|
|
8471
|
+
async function safeFingerprint(inputAbs) {
|
|
8472
|
+
try {
|
|
8473
|
+
const s = await stat5(inputAbs);
|
|
8474
|
+
return `${s.size}:${Math.round(s.mtimeMs)}`;
|
|
8475
|
+
} catch {
|
|
8476
|
+
return;
|
|
8477
|
+
}
|
|
8478
|
+
}
|
|
8479
|
+
function emitBilling(hint) {
|
|
8480
|
+
process.stderr.write(`\x1B[33m⚠️ 计费提示:${hint}\x1B[0m
|
|
8481
|
+
`);
|
|
8482
|
+
}
|
|
8483
|
+
async function runCloudTool(descriptor, inputArg, opts, deps) {
|
|
8484
|
+
const inputAbs = inputArg ? resolve9(inputArg) : undefined;
|
|
8485
|
+
const baseName = inputAbs ? basename11(inputAbs, extname5(inputAbs)) : descriptor.name;
|
|
8486
|
+
validateToolInput(descriptor, inputAbs);
|
|
8487
|
+
const probe = deps.probeDurationSec ?? probeDuration;
|
|
8488
|
+
guardDuration(descriptor, inputAbs, probe, opts.ffmpegPath);
|
|
8489
|
+
const extraParams = parseExtraParams2(opts.param ?? [], opts.paramsJson);
|
|
8490
|
+
const ctx = {
|
|
8491
|
+
inputAbs,
|
|
8492
|
+
baseName,
|
|
8493
|
+
ffmpegPath: opts.ffmpegPath,
|
|
8494
|
+
opts,
|
|
8495
|
+
extraParams,
|
|
8496
|
+
warn: (m) => process.stderr.write(`\x1B[2m ${m}\x1B[0m
|
|
8497
|
+
`)
|
|
8498
|
+
};
|
|
8499
|
+
const outDir = resolveOutDir(descriptor, inputAbs, opts.out);
|
|
8500
|
+
let uploadPath = inputAbs;
|
|
8501
|
+
if (descriptor.preprocess)
|
|
8502
|
+
uploadPath = await descriptor.preprocess(ctx);
|
|
8503
|
+
if (!uploadPath)
|
|
8504
|
+
throw new Error(`${descriptor.name} 缺上传物(input=none 的 cloud 型工具需 preprocess 产上传物)`);
|
|
8505
|
+
emitBilling(descriptor.billingHint);
|
|
8506
|
+
let up = await deps.uploadCached(deps.cfg, uploadPath, { force: opts.reupload });
|
|
8507
|
+
const buildPayload = (fid) => {
|
|
8508
|
+
const p = descriptor.buildPayload ? descriptor.buildPayload(fid, ctx) : { file_id: fid };
|
|
8509
|
+
mergeParams(p, extraParams);
|
|
8510
|
+
return p;
|
|
8511
|
+
};
|
|
8512
|
+
const taskType = descriptor.taskType;
|
|
8513
|
+
let taskId;
|
|
8514
|
+
try {
|
|
8515
|
+
taskId = await deps.submitTask(deps.cfg, taskType, buildPayload(up.fileId));
|
|
8516
|
+
} catch (e) {
|
|
8517
|
+
if (up.cached && isCloudErrorCode(e) === 6004) {
|
|
8518
|
+
await deps.invalidateUpload(uploadPath);
|
|
8519
|
+
up = await deps.uploadCached(deps.cfg, uploadPath, { force: true });
|
|
8520
|
+
taskId = await deps.submitTask(deps.cfg, taskType, buildPayload(up.fileId));
|
|
8521
|
+
} else
|
|
8522
|
+
throw e;
|
|
8523
|
+
}
|
|
8524
|
+
await mkdir8(outDir, { recursive: true });
|
|
8525
|
+
const fingerprint2 = inputAbs ? await safeFingerprint(inputAbs) : undefined;
|
|
8526
|
+
await writeFile8(join21(outDir, "task.json"), JSON.stringify({ tool: descriptor.name, taskType, taskId, fileId: up.fileId, source: inputAbs, fingerprint: fingerprint2, createdAt: new Date().toISOString() }, null, 2));
|
|
8527
|
+
const output = await pollToolTask(deps.cfg, taskType, taskId, {
|
|
8528
|
+
timeoutMs: descriptor.pollTimeoutMs,
|
|
8529
|
+
intervalMs: deps.pollIntervalMs,
|
|
8530
|
+
getResult: deps.getTaskResult,
|
|
8531
|
+
sleep: deps.sleep,
|
|
8532
|
+
now: deps.now
|
|
8533
|
+
});
|
|
8534
|
+
const items = descriptor.mapOutputs ? descriptor.mapOutputs(output, ctx) : [];
|
|
8535
|
+
const files = [];
|
|
8536
|
+
const errors = {};
|
|
8537
|
+
if (items.length === 0)
|
|
8538
|
+
errors["output"] = "任务完成但未解析到产物下载链接(output_result 形态异常)";
|
|
8539
|
+
for (const it of items) {
|
|
8540
|
+
const dest = join21(outDir, it.filename);
|
|
8541
|
+
try {
|
|
8542
|
+
await deps.downloadStream(it.url, dest);
|
|
8543
|
+
files.push(dest);
|
|
8544
|
+
} catch (e) {
|
|
8545
|
+
errors[it.filename] = e instanceof Error ? e.message : String(e);
|
|
8546
|
+
}
|
|
8547
|
+
}
|
|
8548
|
+
const ok = files.length > 0 && Object.keys(errors).length === 0;
|
|
8549
|
+
const result = {
|
|
8550
|
+
ok,
|
|
8551
|
+
tool: descriptor.name,
|
|
8552
|
+
taskType,
|
|
8553
|
+
taskId,
|
|
8554
|
+
fileId: up.fileId,
|
|
8555
|
+
outDir,
|
|
8556
|
+
files,
|
|
8557
|
+
...Object.keys(errors).length ? { errors } : {}
|
|
8558
|
+
};
|
|
8559
|
+
await writeFile8(join21(outDir, "result.json"), JSON.stringify({ ...result, finishedAt: new Date().toISOString() }, null, 2));
|
|
8560
|
+
return result;
|
|
8561
|
+
}
|
|
8562
|
+
|
|
8563
|
+
// src/lib/mad/mad.ts
|
|
8564
|
+
import { mkdir as mkdir11, writeFile as writeFile10 } from "node:fs/promises";
|
|
8565
|
+
import { existsSync as existsSync20, statSync as statSync3 } from "node:fs";
|
|
8566
|
+
import { resolve as resolve10, join as join25 } from "node:path";
|
|
8567
|
+
|
|
8568
|
+
// src/lib/convert/types.ts
|
|
8569
|
+
function num(v, d = 0) {
|
|
8570
|
+
return typeof v === "number" && isFinite(v) ? v : d;
|
|
8571
|
+
}
|
|
8572
|
+
function parseCubicBezier(e) {
|
|
8573
|
+
if (!e || typeof e !== "string")
|
|
8574
|
+
return null;
|
|
8575
|
+
const m = e.match(/cubic-bezier\(\s*([-\d.]+)\s*,\s*([-\d.]+)\s*,\s*([-\d.]+)\s*,\s*([-\d.]+)\s*\)/);
|
|
8576
|
+
if (!m)
|
|
8577
|
+
return null;
|
|
8578
|
+
const v = m.slice(1, 5).map(Number);
|
|
8579
|
+
return v.every((x) => isFinite(x)) ? v : null;
|
|
8580
|
+
}
|
|
8581
|
+
function fontPx(font, d = 90) {
|
|
8582
|
+
if (!font)
|
|
8583
|
+
return d;
|
|
8584
|
+
const m = font.match(/([\d.]+)px/);
|
|
8585
|
+
return m ? Math.max(1, parseFloat(m[1])) : d;
|
|
8586
|
+
}
|
|
8587
|
+
function parseCssColor(c3) {
|
|
8588
|
+
if (!c3 || typeof c3 !== "string")
|
|
8589
|
+
return null;
|
|
8590
|
+
const s = c3.trim();
|
|
8591
|
+
let m = s.match(/^#([0-9a-fA-F]{3})$/);
|
|
8592
|
+
if (m) {
|
|
8593
|
+
const h = m[1];
|
|
8594
|
+
return [parseInt(h[0] + h[0], 16), parseInt(h[1] + h[1], 16), parseInt(h[2] + h[2], 16), 1];
|
|
8595
|
+
}
|
|
8596
|
+
m = s.match(/^#([0-9a-fA-F]{6})([0-9a-fA-F]{2})?$/);
|
|
8597
|
+
if (m) {
|
|
8598
|
+
const h = m[1];
|
|
8599
|
+
const a = m[2] ? parseInt(m[2], 16) / 255 : 1;
|
|
8600
|
+
return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16), a];
|
|
8601
|
+
}
|
|
8602
|
+
m = s.match(/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+)\s*)?\)$/);
|
|
8603
|
+
if (m) {
|
|
8604
|
+
return [
|
|
8605
|
+
Math.min(255, parseFloat(m[1])),
|
|
8606
|
+
Math.min(255, parseFloat(m[2])),
|
|
8607
|
+
Math.min(255, parseFloat(m[3])),
|
|
8608
|
+
m[4] !== undefined ? Math.min(1, parseFloat(m[4])) : 1
|
|
8609
|
+
];
|
|
8610
|
+
}
|
|
8611
|
+
return null;
|
|
8612
|
+
}
|
|
8613
|
+
function mergeChannelTracks(ta, tb, defA, defB) {
|
|
8614
|
+
const sample = (track, t, dflt) => {
|
|
8615
|
+
if (!track || track.length === 0)
|
|
8616
|
+
return dflt;
|
|
8617
|
+
if (t <= track[0].t)
|
|
8618
|
+
return num(track[0].v, dflt);
|
|
8619
|
+
for (let i = 1;i < track.length; i++) {
|
|
8620
|
+
if (t <= track[i].t) {
|
|
8621
|
+
const p = track[i - 1];
|
|
8622
|
+
const q = track[i];
|
|
8623
|
+
const span = q.t - p.t;
|
|
8624
|
+
if (span <= 0)
|
|
8625
|
+
return num(q.v, dflt);
|
|
8626
|
+
const k = (t - p.t) / span;
|
|
8627
|
+
return num(p.v, dflt) + (num(q.v, dflt) - num(p.v, dflt)) * k;
|
|
8628
|
+
}
|
|
8629
|
+
}
|
|
8630
|
+
return num(track[track.length - 1].v, dflt);
|
|
8631
|
+
};
|
|
8632
|
+
const times = new Set;
|
|
8633
|
+
for (const k of ta ?? [])
|
|
8634
|
+
times.add(k.t);
|
|
8635
|
+
for (const k of tb ?? [])
|
|
8636
|
+
times.add(k.t);
|
|
8637
|
+
const sorted = [...times].sort((x, y) => x - y);
|
|
8638
|
+
return sorted.map((t) => {
|
|
8639
|
+
const ea = (ta ?? []).find((k) => k.t === t)?.e;
|
|
8640
|
+
const eb = (tb ?? []).find((k) => k.t === t)?.e;
|
|
8641
|
+
return { t, a: sample(ta, t, defA), b: sample(tb, t, defB), e: ea ?? eb ?? null };
|
|
8642
|
+
});
|
|
8643
|
+
}
|
|
8644
|
+
|
|
8645
|
+
// src/lib/convert/bake_ops.ts
|
|
8646
|
+
var TIME_DOMAIN_OPS = new Set(["shake", "pulsate", "oscillate", "flicker", "zoom"]);
|
|
8647
|
+
function sampleTrack(track, t, def) {
|
|
8648
|
+
if (!track || track.length === 0)
|
|
8649
|
+
return def;
|
|
8650
|
+
if (t <= track[0].t)
|
|
8651
|
+
return num(track[0].v, def);
|
|
8652
|
+
for (let i = 1;i < track.length; i++) {
|
|
8653
|
+
const a = track[i - 1];
|
|
8654
|
+
const b = track[i];
|
|
8655
|
+
if (t <= b.t) {
|
|
8656
|
+
const span = Math.max(b.t - a.t, 0.000001);
|
|
8657
|
+
const k = (t - a.t) / span;
|
|
8658
|
+
return num(a.v, def) + (num(b.v, def) - num(a.v, def)) * k;
|
|
8659
|
+
}
|
|
8660
|
+
}
|
|
8661
|
+
return num(track[track.length - 1].v, def);
|
|
8662
|
+
}
|
|
8663
|
+
function phasePair(i) {
|
|
8664
|
+
return [Math.sin(i * 2.399), Math.cos(i * 3.11)];
|
|
8665
|
+
}
|
|
8666
|
+
var round = (v, p) => {
|
|
8667
|
+
const k = 10 ** p;
|
|
8668
|
+
return Math.round(v * k) / k;
|
|
8669
|
+
};
|
|
8670
|
+
var clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
|
|
8671
|
+
var dn = (n) => Math.max(2, Math.floor(n));
|
|
8672
|
+
function basePos(anim, pos, t) {
|
|
8673
|
+
return [sampleTrack(anim.x, t, pos[0]), sampleTrack(anim.y, t, pos[1])];
|
|
8674
|
+
}
|
|
8675
|
+
function baseScale(anim, t) {
|
|
8676
|
+
if (anim.scale?.length) {
|
|
8677
|
+
const s = sampleTrack(anim.scale, t, 1);
|
|
8678
|
+
return [s, s];
|
|
8679
|
+
}
|
|
8680
|
+
if (anim.sx?.length || anim.sy?.length) {
|
|
8681
|
+
return [sampleTrack(anim.sx, t, 1), sampleTrack(anim.sy, t, 1)];
|
|
8682
|
+
}
|
|
8683
|
+
return [1, 1];
|
|
8684
|
+
}
|
|
8685
|
+
function baseRot(anim, t) {
|
|
8686
|
+
return sampleTrack(anim.rot, t, 0);
|
|
8687
|
+
}
|
|
8688
|
+
function opWindow(fx, ly) {
|
|
8689
|
+
const t0 = num(fx.t0, num(ly.in, 0));
|
|
8690
|
+
const t1 = num(fx.t1, num(ly.out, t0 + 1));
|
|
8691
|
+
return [t0, t1];
|
|
8692
|
+
}
|
|
8693
|
+
function bakeLayerOps(ly) {
|
|
8694
|
+
const anim = ly.anim ?? {};
|
|
8695
|
+
const pos = [num(ly.pos?.[0], 0), num(ly.pos?.[1], 0)];
|
|
8696
|
+
const tracks = [];
|
|
8697
|
+
const warnings = [];
|
|
8698
|
+
for (const fx of ly.fx ?? []) {
|
|
8699
|
+
const op = fx.op;
|
|
8700
|
+
if (!TIME_DOMAIN_OPS.has(op))
|
|
8701
|
+
continue;
|
|
8702
|
+
if (op === "zoom") {
|
|
8703
|
+
const tr = fx.tracks?.scale;
|
|
8704
|
+
if (!tr || tr.length < 1) {
|
|
8705
|
+
warnings.push(`bake: zoom 无 scale 参数轨,跳过(layer ${ly.id})`);
|
|
8706
|
+
continue;
|
|
8707
|
+
}
|
|
8708
|
+
const times = [];
|
|
8709
|
+
const values = [];
|
|
8710
|
+
for (const k of tr) {
|
|
8711
|
+
const t = num(k.t, 0);
|
|
8712
|
+
const v = num(k.v, 1);
|
|
8713
|
+
const [bx, by] = baseScale(anim, t);
|
|
8714
|
+
times.push(round(t, 3));
|
|
8715
|
+
values.push([round(bx * v * 100, 3), round(by * v * 100, 3)]);
|
|
8716
|
+
}
|
|
8717
|
+
const w = [times[0], times[times.length - 1]];
|
|
8718
|
+
tracks.push({ prop: "scale", times, values, window: w, ease: "power2.out" });
|
|
8719
|
+
continue;
|
|
8720
|
+
}
|
|
8721
|
+
const [t0, t1] = opWindow(fx, ly);
|
|
8722
|
+
if (op === "flicker") {
|
|
8723
|
+
if (t1 - t0 < 0.05)
|
|
8724
|
+
continue;
|
|
8725
|
+
let freq = num(fx.freq, 8);
|
|
8726
|
+
freq = clamp(freq >= 0.5 ? freq : freq * 30, 0.5, 20);
|
|
8727
|
+
const mag = clamp(num(fx.mag, 0.6), 0.05, 1);
|
|
8728
|
+
const hi = 1 + mag * 0.8;
|
|
8729
|
+
const lo = Math.max(0.3, 1 - mag * 0.6);
|
|
8730
|
+
const n = Math.min(dn(Math.max(2, Math.floor((t1 - t0) * freq * 2))), 160);
|
|
8731
|
+
const times = [round(t0, 3)];
|
|
8732
|
+
const values = [[0]];
|
|
8733
|
+
for (let j = 1;j <= n; j++) {
|
|
8734
|
+
const tt = t0 + (t1 - t0) * j / n;
|
|
8735
|
+
const v = j % 2 ? hi : lo;
|
|
8736
|
+
const b = clamp((v - 1) * 100, -100, 100);
|
|
8737
|
+
times.push(round(tt, 3));
|
|
8738
|
+
values.push([round(b, 3)]);
|
|
8739
|
+
}
|
|
8740
|
+
tracks.push({ prop: "brightness", times, values, window: [t0, t1] });
|
|
8741
|
+
continue;
|
|
8742
|
+
}
|
|
8743
|
+
if (t1 - t0 < 0.05)
|
|
8744
|
+
continue;
|
|
8745
|
+
if (op === "shake") {
|
|
8746
|
+
const amp0 = num(fx.mag ?? fx.amp, 18);
|
|
8747
|
+
const freq = clamp(num(fx.freq, 20), 1, 40);
|
|
8748
|
+
const n = Math.min(dn(Math.max(2, Math.floor((t1 - t0) * freq))), 160);
|
|
8749
|
+
const tr = fx.tracks?.mag;
|
|
8750
|
+
const seqX = [];
|
|
8751
|
+
const seqY = [];
|
|
8752
|
+
const ttArr = [];
|
|
8753
|
+
for (let i = 0;i <= n; i++) {
|
|
8754
|
+
const tt = t0 + (t1 - t0) * i / n;
|
|
8755
|
+
let a = sampleTrack(tr, tt, amp0);
|
|
8756
|
+
if (fx.decay)
|
|
8757
|
+
a *= 1 - i / n;
|
|
8758
|
+
const [px, py] = phasePair(i);
|
|
8759
|
+
seqX.push(round(a * px, 1));
|
|
8760
|
+
seqY.push(round(a * py, 1));
|
|
8761
|
+
ttArr.push(round(tt, 3));
|
|
8762
|
+
}
|
|
8763
|
+
const off0x = seqX[0];
|
|
8764
|
+
const off0y = seqY[0];
|
|
8765
|
+
const times = [];
|
|
8766
|
+
const values = [];
|
|
8767
|
+
for (let i = 0;i <= n; i++) {
|
|
8768
|
+
const [bx, by] = basePos(anim, pos, ttArr[i]);
|
|
8769
|
+
times.push(ttArr[i]);
|
|
8770
|
+
values.push([round(bx + (seqX[i] - off0x), 3), round(by + (seqY[i] - off0y), 3)]);
|
|
8771
|
+
}
|
|
8772
|
+
tracks.push({ prop: "position", times, values, window: [t0, t1] });
|
|
8773
|
+
continue;
|
|
8774
|
+
}
|
|
8775
|
+
if (op === "oscillate") {
|
|
8776
|
+
if (fx.rot === true) {
|
|
8777
|
+
const a1 = num(fx.a1, -3);
|
|
8778
|
+
const a2 = num(fx.a2, 3);
|
|
8779
|
+
const freq = clamp(num(fx.freq, 2), 0.2, 20);
|
|
8780
|
+
const n = Math.min(dn(Math.max(4, Math.floor((t1 - t0) * freq * 4))), 120);
|
|
8781
|
+
const times = [round(t0, 3)];
|
|
8782
|
+
const values = [[round(baseRot(anim, t0), 3)]];
|
|
8783
|
+
let prev = 0;
|
|
8784
|
+
let acc = 0;
|
|
8785
|
+
for (let i = 1;i <= n; i++) {
|
|
8786
|
+
const tt = t0 + (t1 - t0) * i / n;
|
|
8787
|
+
const ph = Math.sin(2 * Math.PI * freq * (tt - t0));
|
|
8788
|
+
const cur = (a1 + a2) / 2 + (a2 - a1) / 2 * ph;
|
|
8789
|
+
acc += round(cur - prev, 2);
|
|
8790
|
+
prev = cur;
|
|
8791
|
+
times.push(round(tt, 3));
|
|
8792
|
+
values.push([round(baseRot(anim, tt) + acc, 3)]);
|
|
8793
|
+
}
|
|
8794
|
+
tracks.push({ prop: "rotation", times, values, window: [t0, t1] });
|
|
8795
|
+
} else {
|
|
8796
|
+
const mag0 = num(fx.mag, 40);
|
|
8797
|
+
const ang = num(fx.angle, 90) * Math.PI / 180;
|
|
8798
|
+
const freq = clamp(num(fx.freq, 4), 0.2, 30);
|
|
8799
|
+
const n = Math.min(dn(Math.max(4, Math.floor((t1 - t0) * freq * 4))), 160);
|
|
8800
|
+
const tr = fx.tracks?.mag;
|
|
8801
|
+
const times = [round(t0, 3)];
|
|
8802
|
+
const [b0x, b0y] = basePos(anim, pos, t0);
|
|
8803
|
+
const values = [[round(b0x, 3), round(b0y, 3)]];
|
|
8804
|
+
let prevX = 0;
|
|
8805
|
+
let prevY = 0;
|
|
8806
|
+
let accX = 0;
|
|
8807
|
+
let accY = 0;
|
|
8808
|
+
for (let i = 1;i <= n; i++) {
|
|
8809
|
+
const tt = t0 + (t1 - t0) * i / n;
|
|
8810
|
+
const a = sampleTrack(tr, tt, mag0);
|
|
8811
|
+
const ph = Math.sin(2 * Math.PI * freq * (tt - t0));
|
|
8812
|
+
const curX = a * ph * Math.cos(ang);
|
|
8813
|
+
const curY = a * ph * Math.sin(ang);
|
|
8814
|
+
accX += round(curX - prevX, 2);
|
|
8815
|
+
accY += round(curY - prevY, 2);
|
|
8816
|
+
prevX = curX;
|
|
8817
|
+
prevY = curY;
|
|
8818
|
+
const [bx, by] = basePos(anim, pos, tt);
|
|
8819
|
+
times.push(round(tt, 3));
|
|
8820
|
+
values.push([round(bx + accX, 3), round(by + accY, 3)]);
|
|
8821
|
+
}
|
|
8822
|
+
tracks.push({ prop: "position", times, values, window: [t0, t1] });
|
|
8823
|
+
}
|
|
8824
|
+
continue;
|
|
8825
|
+
}
|
|
8826
|
+
if (op === "pulsate") {
|
|
8827
|
+
let lo;
|
|
8828
|
+
let hi;
|
|
8829
|
+
if (fx.amp != null) {
|
|
8830
|
+
lo = 1 - num(fx.amp, 0);
|
|
8831
|
+
hi = 1 + num(fx.amp, 0);
|
|
8832
|
+
} else {
|
|
8833
|
+
lo = num(fx.min, 1);
|
|
8834
|
+
hi = num(fx.max, 1.06);
|
|
8835
|
+
}
|
|
8836
|
+
const freq = clamp(num(fx.freq, 2), 0.2, 20);
|
|
8837
|
+
const n = Math.min(dn(Math.max(4, Math.floor((t1 - t0) * freq * 4))), 160);
|
|
8838
|
+
const tr = fx.tracks?.max;
|
|
8839
|
+
const times = [round(t0, 3)];
|
|
8840
|
+
const [bs0x, bs0y] = baseScale(anim, t0);
|
|
8841
|
+
const values = [[round(bs0x * 100, 3), round(bs0y * 100, 3)]];
|
|
8842
|
+
for (let i = 1;i <= n; i++) {
|
|
8843
|
+
const tt = t0 + (t1 - t0) * i / n;
|
|
8844
|
+
const h = sampleTrack(tr, tt, hi);
|
|
8845
|
+
const ph = (Math.sin(2 * Math.PI * freq * (tt - t0)) + 1) / 2;
|
|
8846
|
+
const s = round(lo + (Math.max(h, lo) - lo) * ph, 3);
|
|
8847
|
+
const [bx, by] = baseScale(anim, tt);
|
|
8848
|
+
times.push(round(tt, 3));
|
|
8849
|
+
values.push([round(bx * s * 100, 3), round(by * s * 100, 3)]);
|
|
8850
|
+
}
|
|
8851
|
+
tracks.push({ prop: "scale", times, values, window: [t0, t1] });
|
|
8852
|
+
continue;
|
|
8853
|
+
}
|
|
8854
|
+
}
|
|
8855
|
+
return { tracks, warnings };
|
|
8856
|
+
}
|
|
8857
|
+
|
|
8858
|
+
// src/lib/convert/ir_to_jsx.ts
|
|
8859
|
+
var HEADER = `// 由技法图鉴重建生成,仅供学习研究;素材为占位,请替换
|
|
8860
|
+
// Generated by gitruck-creation IR->JSX converter. For study only; placeholder footage, replace before use.
|
|
8861
|
+
// 用法: AE 菜单 文件>脚本>运行脚本文件 选择本文件(AE 2020+ 建议)`;
|
|
8862
|
+
var FX_MATCHNAME = {
|
|
8863
|
+
glow: "ADBE Glo2",
|
|
8864
|
+
blur: "ADBE Gaussian Blur 2",
|
|
8865
|
+
dirBlur: "ADBE Motion Blur",
|
|
8866
|
+
turbulence: "ADBE Turbulent Displace",
|
|
8867
|
+
tile: "ADBE Tile",
|
|
8868
|
+
dropShadow: "ADBE Drop Shadow",
|
|
8869
|
+
invert: "ADBE Invert",
|
|
8870
|
+
wipe: "ADBE Linear Wipe",
|
|
8871
|
+
bulge: "ADBE Bulge",
|
|
8872
|
+
noise: "ADBE Noise",
|
|
8873
|
+
fill: "ADBE Fill",
|
|
8874
|
+
vignette: "ADBE Vignette",
|
|
8875
|
+
colorAdjust: "ADBE Brightness & Contrast 2"
|
|
8876
|
+
};
|
|
8877
|
+
function esc(s) {
|
|
8878
|
+
return String(s).replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\r/g, "\\r").replace(/\n/g, "\\n").replace(/\t/g, "\\t").replace(new RegExp(String.fromCharCode(8232), "g"), "\\u2028").replace(new RegExp(String.fromCharCode(8233), "g"), "\\u2029");
|
|
8879
|
+
}
|
|
8880
|
+
function r35(v) {
|
|
8881
|
+
return Math.round(v * 1000) / 1000;
|
|
8882
|
+
}
|
|
8883
|
+
function colorArr(css, fallback) {
|
|
8884
|
+
const c3 = parseCssColor(css);
|
|
8885
|
+
if (!c3)
|
|
8886
|
+
return fallback;
|
|
8887
|
+
return [r35(c3[0] / 255), r35(c3[1] / 255), r35(c3[2] / 255)];
|
|
8888
|
+
}
|
|
8889
|
+
function easeInfluence(e) {
|
|
8890
|
+
const cb = parseCubicBezier(e);
|
|
8891
|
+
if (!cb)
|
|
8892
|
+
return null;
|
|
8893
|
+
const clamp2 = (v) => Math.min(100, Math.max(0.1, v));
|
|
8894
|
+
return { out: clamp2(cb[0] * 100 || 33), inn: clamp2((1 - cb[2]) * 100 || 33) };
|
|
8895
|
+
}
|
|
8896
|
+
function emitKeys(ctx, propExpr, keys) {
|
|
8897
|
+
if (keys.length === 0)
|
|
8898
|
+
return;
|
|
8899
|
+
const L = ctx.lines;
|
|
8900
|
+
L.push(` try {`);
|
|
8901
|
+
L.push(` var p = ${propExpr};`);
|
|
8902
|
+
for (const k of keys) {
|
|
8903
|
+
L.push(` p.setValueAtTime(${r35(k.t)}, ${k.value});`);
|
|
8904
|
+
}
|
|
8905
|
+
keys.forEach((k, i) => {
|
|
8906
|
+
const inf = easeInfluence(k.e);
|
|
8907
|
+
if (!inf)
|
|
8908
|
+
return;
|
|
8909
|
+
L.push(` try {`);
|
|
8910
|
+
L.push(` var dim = 1; try { dim = p.value.length || 1; } catch (e) { dim = 1; }`);
|
|
8911
|
+
L.push(` var eo = [], ei = [];`);
|
|
8912
|
+
L.push(` for (var d = 0; d < Math.min(dim, 3); d++) { eo.push(new KeyframeEase(0, ${r35(inf.out)})); ei.push(new KeyframeEase(0, ${r35(inf.inn)})); }`);
|
|
8913
|
+
L.push(` p.setTemporalEaseAtKey(${i + 1}, ei, eo);`);
|
|
8914
|
+
L.push(` } catch (e) {}`);
|
|
8915
|
+
});
|
|
8916
|
+
L.push(` } catch (e) {}`);
|
|
8917
|
+
}
|
|
8918
|
+
function fxComment(fx) {
|
|
8919
|
+
const params = Object.keys(fx).filter((k) => !["op", "src", "tracks"].includes(k)).map((k) => `${k}=${JSON.stringify(fx[k])}`).join(" ");
|
|
8920
|
+
const tracks = fx.tracks ? ` tracks:[${Object.keys(fx.tracks).join(",")}]` : "";
|
|
8921
|
+
return `${fx.op}${fx.src ? ` (原特效: ${fx.src})` : ""}${params ? " " + params : ""}${tracks}`;
|
|
8922
|
+
}
|
|
8923
|
+
function emitEffects(ctx, layerVar, fxList) {
|
|
8924
|
+
if (!fxList || fxList.length === 0)
|
|
8925
|
+
return;
|
|
8926
|
+
const L = ctx.lines;
|
|
8927
|
+
L.push(` // —— 特效(原工程特效清单,已知 matchName 自动 applyEffect,其余 TODO 手动补)——`);
|
|
8928
|
+
for (const fx of fxList) {
|
|
8929
|
+
const mn = FX_MATCHNAME[fx.op];
|
|
8930
|
+
if (ctx.bakeOps && TIME_DOMAIN_OPS.has(fx.op))
|
|
8931
|
+
continue;
|
|
8932
|
+
if (mn && !TIME_DOMAIN_OPS.has(fx.op)) {
|
|
8933
|
+
L.push(` // fx: ${esc(fxComment(fx))}`);
|
|
8934
|
+
L.push(` try { ${layerVar}.property("ADBE Effect Parade").addProperty("${mn}"); } catch (e) {}`);
|
|
8935
|
+
} else if (TIME_DOMAIN_OPS.has(fx.op)) {
|
|
8936
|
+
L.push(` // TODO fx(时序算子,请用表达式或关键帧手动复现): ${esc(fxComment(fx))}`);
|
|
8937
|
+
ctx.warnings.push(`jsx: 时序算子 ${fx.op} 未自动铺帧(已写 TODO 注释)`);
|
|
8938
|
+
} else {
|
|
8939
|
+
L.push(` // TODO fx(无对应 AE 原生 matchName): ${esc(fxComment(fx))}`);
|
|
8940
|
+
ctx.warnings.push(`jsx: 未映射特效 ${fx.op}(已写 TODO 注释)`);
|
|
8941
|
+
}
|
|
8942
|
+
}
|
|
8943
|
+
}
|
|
8944
|
+
function samplePos(anim, pos, t) {
|
|
8945
|
+
return [sampleTrack(anim.x, t, pos[0]), sampleTrack(anim.y, t, pos[1])];
|
|
8946
|
+
}
|
|
8947
|
+
function sampleScaleMul(anim, t) {
|
|
8948
|
+
if (anim.scale?.length) {
|
|
8949
|
+
const s = sampleTrack(anim.scale, t, 1);
|
|
8950
|
+
return [s, s];
|
|
8951
|
+
}
|
|
8952
|
+
if (anim.sx?.length || anim.sy?.length)
|
|
8953
|
+
return [sampleTrack(anim.sx, t, 1), sampleTrack(anim.sy, t, 1)];
|
|
8954
|
+
return [1, 1];
|
|
8955
|
+
}
|
|
8956
|
+
function mergeBaked(baseKeys, bakedKeys, windows, anchorAt, layerIn, layerOut) {
|
|
8957
|
+
const inWindow = (t) => {
|
|
8958
|
+
for (const w of windows)
|
|
8959
|
+
if (t > w[0] + 0.000001 && t < w[1] - 0.000001)
|
|
8960
|
+
return true;
|
|
8961
|
+
return false;
|
|
8962
|
+
};
|
|
8963
|
+
const out = [];
|
|
8964
|
+
for (const k of baseKeys)
|
|
8965
|
+
if (!inWindow(k.t))
|
|
8966
|
+
out.push(k);
|
|
8967
|
+
const EPS = 0.033;
|
|
8968
|
+
for (const w of windows) {
|
|
8969
|
+
if (w[1] < layerOut - EPS) {
|
|
8970
|
+
const at = Math.min(w[1] + EPS, layerOut);
|
|
8971
|
+
out.push({ t: at, value: anchorAt(at), e: null });
|
|
8972
|
+
}
|
|
8973
|
+
if (w[0] > layerIn + EPS) {
|
|
8974
|
+
const at = Math.max(w[0] - EPS, layerIn);
|
|
8975
|
+
out.push({ t: at, value: anchorAt(at), e: null });
|
|
8976
|
+
}
|
|
8977
|
+
}
|
|
8978
|
+
out.push(...bakedKeys);
|
|
8979
|
+
out.sort((a, b) => a.t - b.t);
|
|
8980
|
+
const ded = [];
|
|
8981
|
+
for (const k of out) {
|
|
8982
|
+
if (ded.length && Math.abs(k.t - ded[ded.length - 1].t) < 0.000001)
|
|
8983
|
+
ded[ded.length - 1] = k;
|
|
8984
|
+
else
|
|
8985
|
+
ded.push(k);
|
|
8986
|
+
}
|
|
8987
|
+
return ded;
|
|
8988
|
+
}
|
|
8989
|
+
function positionBaseKeys(anim, pos) {
|
|
8990
|
+
if (!(anim.x?.length || anim.y?.length))
|
|
8991
|
+
return [];
|
|
8992
|
+
const merged = mergeChannelTracks(anim.x, anim.y, pos[0], pos[1]);
|
|
8993
|
+
return merged.map((k) => ({ t: k.t, value: `[${r35(k.a)}, ${r35(k.b)}]`, e: k.e }));
|
|
8994
|
+
}
|
|
8995
|
+
function scaleBaseKeys(anim, coverExpr) {
|
|
8996
|
+
const sc = (v) => coverExpr ? `${r35(v)}*${coverExpr}` : `${r35(v)}`;
|
|
8997
|
+
if (anim.scale?.length) {
|
|
8998
|
+
return anim.scale.map((k) => ({ t: k.t, value: `[${sc(num(k.v, 1) * 100)}, ${sc(num(k.v, 1) * 100)}]`, e: k.e }));
|
|
8999
|
+
}
|
|
9000
|
+
if (anim.sx?.length || anim.sy?.length) {
|
|
9001
|
+
const merged = mergeChannelTracks(anim.sx, anim.sy, 1, 1);
|
|
9002
|
+
return merged.map((k) => ({ t: k.t, value: `[${sc(k.a * 100)}, ${sc(k.b * 100)}]`, e: k.e }));
|
|
9003
|
+
}
|
|
9004
|
+
return [];
|
|
9005
|
+
}
|
|
9006
|
+
function emitBakedOps(ctx, ly, layerVar, tf, coverExpr) {
|
|
9007
|
+
const { tracks, warnings } = bakeLayerOps(ly);
|
|
9008
|
+
for (const w of warnings)
|
|
9009
|
+
ctx.warnings.push(w);
|
|
9010
|
+
if (tracks.length === 0)
|
|
9011
|
+
return;
|
|
9012
|
+
const anim = ly.anim ?? {};
|
|
9013
|
+
const pos = [num(ly.pos?.[0], 0), num(ly.pos?.[1], 0)];
|
|
9014
|
+
const inn = num(ly.in, 0);
|
|
9015
|
+
const out = Math.max(inn + 0.01, num(ly.out, inn + 1));
|
|
9016
|
+
const sc = (v) => coverExpr ? `${r35(v)}*${coverExpr}` : `${r35(v)}`;
|
|
9017
|
+
const brights = tracks.filter((t) => t.prop === "brightness");
|
|
9018
|
+
if (brights.length) {
|
|
9019
|
+
ctx.lines.push(` // fx: flicker → 亮度脉冲(ADBE Brightness & Contrast 2,避开 opacity 通道)`);
|
|
9020
|
+
ctx.lines.push(` var flkFx = null; try { flkFx = ${layerVar}.property("ADBE Effect Parade").addProperty("ADBE Brightness & Contrast 2"); } catch (e) { flkFx = null; }`);
|
|
9021
|
+
for (const bt of brights) {
|
|
9022
|
+
const keys = bt.times.map((t, i) => ({ t, value: `${r35(bt.values[i][0])}`, e: null }));
|
|
9023
|
+
emitKeys(ctx, `flkFx.property(1)`, keys);
|
|
9024
|
+
}
|
|
9025
|
+
}
|
|
9026
|
+
const posBaked = tracks.filter((t) => t.prop === "position");
|
|
9027
|
+
if (posBaked.length) {
|
|
9028
|
+
const baseKeys = positionBaseKeys(anim, pos);
|
|
9029
|
+
const bakedKeys = [];
|
|
9030
|
+
const windows = [];
|
|
9031
|
+
for (const bt of posBaked) {
|
|
9032
|
+
windows.push(bt.window);
|
|
9033
|
+
for (let i = 0;i < bt.times.length; i++) {
|
|
9034
|
+
bakedKeys.push({ t: bt.times[i], value: `[${r35(bt.values[i][0])}, ${r35(bt.values[i][1])}]`, e: null });
|
|
9035
|
+
}
|
|
9036
|
+
}
|
|
9037
|
+
const merged = mergeBaked(baseKeys, bakedKeys, windows, (t) => {
|
|
9038
|
+
const [x, y] = samplePos(anim, pos, t);
|
|
9039
|
+
return `[${r35(x)}, ${r35(y)}]`;
|
|
9040
|
+
}, inn, out);
|
|
9041
|
+
emitKeys(ctx, `${tf}.property("ADBE Position")`, merged);
|
|
9042
|
+
}
|
|
9043
|
+
const scaleBaked = tracks.filter((t) => t.prop === "scale");
|
|
9044
|
+
if (scaleBaked.length) {
|
|
9045
|
+
const baseKeys = scaleBaseKeys(anim, coverExpr);
|
|
9046
|
+
const bakedKeys = [];
|
|
9047
|
+
const windows = [];
|
|
9048
|
+
for (const bt of scaleBaked) {
|
|
9049
|
+
windows.push(bt.window);
|
|
9050
|
+
for (let i = 0;i < bt.times.length; i++) {
|
|
9051
|
+
bakedKeys.push({ t: bt.times[i], value: `[${sc(bt.values[i][0])}, ${sc(bt.values[i][1])}]`, e: null });
|
|
9052
|
+
}
|
|
9053
|
+
}
|
|
9054
|
+
const merged = mergeBaked(baseKeys, bakedKeys, windows, (t) => {
|
|
9055
|
+
const [mx, my] = sampleScaleMul(anim, t);
|
|
9056
|
+
return `[${sc(mx * 100)}, ${sc(my * 100)}]`;
|
|
9057
|
+
}, inn, out);
|
|
9058
|
+
emitKeys(ctx, `${tf}.property("ADBE Scale")`, merged);
|
|
9059
|
+
}
|
|
9060
|
+
const rotBaked = tracks.filter((t) => t.prop === "rotation");
|
|
9061
|
+
if (rotBaked.length) {
|
|
9062
|
+
const baseKeys = (anim.rot ?? []).map((k) => ({ t: k.t, value: `${r35(num(k.v, 0))}`, e: k.e }));
|
|
9063
|
+
const bakedKeys = [];
|
|
9064
|
+
const windows = [];
|
|
9065
|
+
for (const bt of rotBaked) {
|
|
9066
|
+
windows.push(bt.window);
|
|
9067
|
+
for (let i = 0;i < bt.times.length; i++)
|
|
9068
|
+
bakedKeys.push({ t: bt.times[i], value: `${r35(bt.values[i][0])}`, e: null });
|
|
9069
|
+
}
|
|
9070
|
+
const merged = mergeBaked(baseKeys, bakedKeys, windows, (t) => `${r35(sampleTrack(anim.rot, t, 0))}`, inn, out);
|
|
9071
|
+
emitKeys(ctx, `${tf}.property("ADBE Rotate Z")`, merged);
|
|
9072
|
+
}
|
|
9073
|
+
}
|
|
9074
|
+
function emitLayer(ctx, ir, ly, parentNullVar) {
|
|
9075
|
+
const L = ctx.lines;
|
|
9076
|
+
const cv = ctx.compVar;
|
|
9077
|
+
const c3 = ir.canvas;
|
|
9078
|
+
const id = ++ctx.layerSeq;
|
|
9079
|
+
const v = `ly${id}`;
|
|
9080
|
+
const inn = num(ly.in, 0);
|
|
9081
|
+
const out = Math.max(inn + 0.01, num(ly.out, c3.duration));
|
|
9082
|
+
const px = num(ly.pos?.[0], c3.w / 2);
|
|
9083
|
+
const py = num(ly.pos?.[1], c3.h / 2);
|
|
9084
|
+
const name = esc(`${ly.id || "L" + id} [${ly.type}]`);
|
|
9085
|
+
L.push(``);
|
|
9086
|
+
L.push(` // ---- 层 ${name} in=${r35(inn)} out=${r35(out)} ----`);
|
|
9087
|
+
if (ly.type === "group") {
|
|
9088
|
+
L.push(` var ${v} = ${cv}.layers.addNull(${r35(c3.duration)});`);
|
|
9089
|
+
L.push(` ${v}.name = "${name}";`);
|
|
9090
|
+
L.push(` ${v}.inPoint = ${r35(inn)}; ${v}.outPoint = ${r35(out)};`);
|
|
9091
|
+
L.push(` ${v}.property("ADBE Transform Group").property("ADBE Anchor Point").setValue([0,0]);`);
|
|
9092
|
+
L.push(` ${v}.property("ADBE Transform Group").property("ADBE Position").setValue([0,0]);`);
|
|
9093
|
+
if (parentNullVar)
|
|
9094
|
+
L.push(` try { ${v}.parent = ${parentNullVar}; } catch (e) {}`);
|
|
9095
|
+
emitGroupAnim(ctx, v, ly);
|
|
9096
|
+
emitEffects(ctx, v, ly.fx);
|
|
9097
|
+
for (const child of ly.children ?? [])
|
|
9098
|
+
emitLayer(ctx, ir, child, v);
|
|
9099
|
+
return;
|
|
9100
|
+
}
|
|
9101
|
+
let coverExpr = null;
|
|
9102
|
+
const footageHit = ly.type === "image" || ly.type === "video" ? ctx.footage[ly.id] : undefined;
|
|
9103
|
+
const fgVar = footageHit ? ctx.footageVars.get(footageHit.path) : undefined;
|
|
9104
|
+
if (footageHit && fgVar) {
|
|
9105
|
+
const slotW = Math.max(2, Math.round(num(ly.w, c3.w)));
|
|
9106
|
+
const slotH = Math.max(2, Math.round(num(ly.h, c3.h)));
|
|
9107
|
+
const srcOffset = num(footageHit.srcOffset, 0);
|
|
9108
|
+
const cvv = `cv${id}`;
|
|
9109
|
+
coverExpr = cvv;
|
|
9110
|
+
L.push(` // 素材注入(${esc(String(ly.type))}): footage srcOffset=${r35(srcOffset)}`);
|
|
9111
|
+
L.push(` var ${v} = null, ${cvv} = 1;`);
|
|
9112
|
+
L.push(` if (${fgVar} != null) {`);
|
|
9113
|
+
L.push(` try {`);
|
|
9114
|
+
L.push(` ${v} = ${cv}.layers.add(${fgVar});`);
|
|
9115
|
+
L.push(` var _fw = ${fgVar}.width || ${slotW}, _fh = ${fgVar}.height || ${slotH};`);
|
|
9116
|
+
L.push(` ${cvv} = Math.max(${slotW} / _fw, ${slotH} / _fh);`);
|
|
9117
|
+
L.push(` ${v}.startTime = ${r35(inn)} - ${r35(srcOffset)};`);
|
|
9118
|
+
L.push(` } catch (e) { ${v} = null; }`);
|
|
9119
|
+
L.push(` }`);
|
|
9120
|
+
L.push(` if (${v} == null) {`);
|
|
9121
|
+
L.push(` ${v} = ${cv}.layers.addSolid([0.35,0.35,0.4], "${name} [占位]", ${slotW}, ${slotH}, 1, ${r35(c3.duration)});`);
|
|
9122
|
+
L.push(` ${cvv} = 1;`);
|
|
9123
|
+
L.push(` }`);
|
|
9124
|
+
} else if (ly.type === "text") {
|
|
9125
|
+
const size = fontPx(ly.font);
|
|
9126
|
+
const col = colorArr(ly.color, [1, 1, 1]);
|
|
9127
|
+
L.push(` var ${v} = ${cv}.layers.addText("${esc(ly.text ?? "")}");`);
|
|
9128
|
+
L.push(` try {`);
|
|
9129
|
+
L.push(` var td = ${v}.property("ADBE Text Properties").property("ADBE Text Document");`);
|
|
9130
|
+
L.push(` var tdv = td.value;`);
|
|
9131
|
+
L.push(` tdv.fontSize = ${Math.round(size)};`);
|
|
9132
|
+
L.push(` tdv.fillColor = [${col.join(",")}];`);
|
|
9133
|
+
L.push(` tdv.justification = ParagraphJustification.CENTER_JUSTIFY;`);
|
|
9134
|
+
L.push(` td.setValue(tdv);`);
|
|
9135
|
+
L.push(` } catch (e) {}`);
|
|
9136
|
+
} else if (ly.type === "shape") {
|
|
9137
|
+
const col = colorArr(ly.fill, [0.2, 0.33, 0.67]);
|
|
9138
|
+
const w = Math.max(2, Math.round(num(ly.w, 400)));
|
|
9139
|
+
const h = Math.max(2, Math.round(num(ly.h, 400)));
|
|
9140
|
+
if (ly.shape === "ellipse")
|
|
9141
|
+
L.push(` // TODO: 原层为椭圆形状,固态占位,可手动换 shape layer`);
|
|
9142
|
+
L.push(` var ${v} = ${cv}.layers.addSolid([${col.join(",")}], "${name}", ${w}, ${h}, 1, ${r35(c3.duration)});`);
|
|
9143
|
+
} else {
|
|
9144
|
+
const w = Math.max(2, Math.round(num(ly.w, c3.w)));
|
|
9145
|
+
const h = Math.max(2, Math.round(num(ly.h, c3.h)));
|
|
9146
|
+
L.push(` // 占位素材(${esc(String(ly.type))}${ly.asset ? ` asset=${esc(String(ly.asset))}` : ""}): 请替换为真实素材`);
|
|
9147
|
+
L.push(` var ${v} = ${cv}.layers.addSolid([0.35,0.35,0.4], "${name} [占位]", ${w}, ${h}, 1, ${r35(c3.duration)});`);
|
|
9148
|
+
}
|
|
9149
|
+
L.push(` ${v}.name = "${name}";`);
|
|
9150
|
+
L.push(` ${v}.inPoint = ${r35(inn)}; ${v}.outPoint = ${r35(out)};`);
|
|
9151
|
+
if (parentNullVar)
|
|
9152
|
+
L.push(` try { ${v}.parent = ${parentNullVar}; } catch (e) {}`);
|
|
9153
|
+
if (ly.blend && ly.blend !== "normal") {
|
|
9154
|
+
L.push(` // 混合模式: ${esc(ly.blend)}`);
|
|
9155
|
+
const bm = BLEND_JSX[ly.blend];
|
|
9156
|
+
if (bm)
|
|
9157
|
+
L.push(` try { ${v}.blendingMode = BlendingMode.${bm}; } catch (e) {}`);
|
|
9158
|
+
}
|
|
9159
|
+
const tf = `${v}.property("ADBE Transform Group")`;
|
|
9160
|
+
L.push(` ${tf}.property("ADBE Position").setValue([${r35(px)}, ${r35(py)}]);`);
|
|
9161
|
+
const anim = ly.anim ?? {};
|
|
9162
|
+
const bakedProps = new Set(ctx.bakeOps ? bakeLayerOps(ly).tracks.map((t) => t.prop) : []);
|
|
9163
|
+
if ((anim.x?.length || anim.y?.length) && !bakedProps.has("position")) {
|
|
9164
|
+
emitKeys(ctx, `${tf}.property("ADBE Position")`, positionBaseKeys(anim, [px, py]));
|
|
9165
|
+
}
|
|
9166
|
+
if (!bakedProps.has("scale")) {
|
|
9167
|
+
const scKeys = scaleBaseKeys(anim, coverExpr);
|
|
9168
|
+
if (scKeys.length)
|
|
9169
|
+
emitKeys(ctx, `${tf}.property("ADBE Scale")`, scKeys);
|
|
9170
|
+
else if (coverExpr) {
|
|
9171
|
+
L.push(` ${tf}.property("ADBE Scale").setValue([100*${coverExpr}, 100*${coverExpr}]);`);
|
|
9172
|
+
}
|
|
9173
|
+
}
|
|
9174
|
+
if (anim.rot?.length && !bakedProps.has("rotation")) {
|
|
9175
|
+
emitKeys(ctx, `${tf}.property("ADBE Rotate Z")`, anim.rot.map((k) => ({ t: k.t, value: `${r35(num(k.v, 0))}`, e: k.e })));
|
|
9176
|
+
}
|
|
9177
|
+
if (anim.opacity?.length) {
|
|
9178
|
+
emitKeys(ctx, `${tf}.property("ADBE Opacity")`, anim.opacity.map((k) => ({ t: k.t, value: `${r35(Math.min(1, Math.max(0, num(k.v, 1))) * 100)}`, e: k.e })));
|
|
9179
|
+
}
|
|
9180
|
+
if (anim.ls?.length) {
|
|
9181
|
+
L.push(` // TODO: letterspacing 轨未自动映射(AE 需 Animator>Tracking),共 ${anim.ls.length} 帧`);
|
|
9182
|
+
}
|
|
9183
|
+
emitEffects(ctx, v, ly.fx);
|
|
9184
|
+
if (ctx.bakeOps)
|
|
9185
|
+
emitBakedOps(ctx, ly, v, tf, coverExpr);
|
|
9186
|
+
if (ly.raw_fx?.length) {
|
|
9187
|
+
L.push(` // TODO 原工程还有未解析特效: ${esc(ly.raw_fx.join(", "))}`);
|
|
9188
|
+
}
|
|
9189
|
+
}
|
|
9190
|
+
function emitGroupAnim(ctx, v, ly) {
|
|
9191
|
+
const anim = ly.anim ?? {};
|
|
9192
|
+
const px = num(ly.pos?.[0], 0);
|
|
9193
|
+
const py = num(ly.pos?.[1], 0);
|
|
9194
|
+
if (anim.x?.length || anim.y?.length) {
|
|
9195
|
+
const merged = mergeChannelTracks(anim.x, anim.y, px, py);
|
|
9196
|
+
emitKeys(ctx, `${v}.property("ADBE Transform Group").property("ADBE Position")`, merged.map((k) => ({ t: k.t, value: `[${r35(k.a - px)}, ${r35(k.b - py)}]`, e: k.e })));
|
|
9197
|
+
}
|
|
9198
|
+
for (const ch of ["scale", "rot", "opacity"]) {
|
|
9199
|
+
if (anim[ch]?.length) {
|
|
9200
|
+
ctx.lines.push(` // TODO: 组自身 ${ch} 动画未映射到 null(子层坐标为画布绝对值,直接缩放会偏移)`);
|
|
9201
|
+
ctx.warnings.push(`jsx: 组 ${ly.id} 的 ${ch} 动画需手动复现`);
|
|
9202
|
+
}
|
|
9203
|
+
}
|
|
9204
|
+
}
|
|
9205
|
+
var BLEND_JSX = {
|
|
9206
|
+
screen: "SCREEN",
|
|
9207
|
+
add: "ADD",
|
|
9208
|
+
multiply: "MULTIPLY",
|
|
9209
|
+
overlay: "OVERLAY",
|
|
9210
|
+
lighten: "LIGHTEN",
|
|
9211
|
+
darken: "DARKEN",
|
|
9212
|
+
"soft-light": "SOFT_LIGHT",
|
|
9213
|
+
"hard-light": "HARD_LIGHT",
|
|
9214
|
+
difference: "DIFFERENCE",
|
|
9215
|
+
exclude: "EXCLUSION",
|
|
9216
|
+
hue: "HUE",
|
|
9217
|
+
color: "COLOR"
|
|
9218
|
+
};
|
|
9219
|
+
function fwdSlash(p) {
|
|
9220
|
+
return p.replace(/\\/g, "/");
|
|
9221
|
+
}
|
|
9222
|
+
function emitFootageImports(ctx, paths) {
|
|
9223
|
+
const L = ctx.lines;
|
|
9224
|
+
const uniq2 = [...new Set(paths)];
|
|
9225
|
+
if (uniq2.length === 0)
|
|
9226
|
+
return;
|
|
9227
|
+
L.push(` // —— 素材导入(每素材仅导入一次、变量缓存复用)——`);
|
|
9228
|
+
let i = 0;
|
|
9229
|
+
for (const p of uniq2) {
|
|
9230
|
+
const vn = `fg${++i}`;
|
|
9231
|
+
ctx.footageVars.set(p, vn);
|
|
9232
|
+
L.push(` var ${vn} = null;`);
|
|
9233
|
+
L.push(` try { ${vn} = app.project.importFile(new ImportOptions(new File("${esc(fwdSlash(p))}"))); } catch (e) { ${vn} = null; }`);
|
|
9234
|
+
}
|
|
9235
|
+
}
|
|
9236
|
+
function newCtx(opts) {
|
|
9237
|
+
return {
|
|
9238
|
+
lines: [],
|
|
9239
|
+
warnings: [],
|
|
9240
|
+
layerSeq: 0,
|
|
9241
|
+
compVar: opts?.compVar ?? "comp",
|
|
9242
|
+
footage: opts?.footage ?? {},
|
|
9243
|
+
bakeOps: !!opts?.bakeOps,
|
|
9244
|
+
footageVars: new Map
|
|
9245
|
+
};
|
|
9246
|
+
}
|
|
9247
|
+
function madJsx(opts) {
|
|
9248
|
+
const { master, windows } = opts;
|
|
9249
|
+
const mw = Math.max(4, Math.round(master.w));
|
|
9250
|
+
const mh = Math.max(4, Math.round(master.h));
|
|
9251
|
+
const mfps = Math.min(99, Math.max(1, master.fps || 30));
|
|
9252
|
+
const masterName = esc(master.name ?? "MAD Master");
|
|
9253
|
+
const allPaths = [];
|
|
9254
|
+
for (const win of windows) {
|
|
9255
|
+
for (const f of Object.values(win.footage ?? {}))
|
|
9256
|
+
allPaths.push(f.path);
|
|
9257
|
+
}
|
|
9258
|
+
const ctx = newCtx({ bakeOps: true });
|
|
9259
|
+
const L = ctx.lines;
|
|
9260
|
+
L.push(opts.header ?? HEADER);
|
|
9261
|
+
L.push(``);
|
|
9262
|
+
L.push(`app.beginUndoGroup("MAD Rebuild ${masterName}");`);
|
|
9263
|
+
L.push(`(function () {`);
|
|
9264
|
+
emitFootageImports(ctx, allPaths);
|
|
9265
|
+
const globalFootageVars = ctx.footageVars;
|
|
9266
|
+
let totalDur = 0.1;
|
|
9267
|
+
for (const win of windows) {
|
|
9268
|
+
const len = win.outLen ?? win.t1 - win.t0;
|
|
9269
|
+
totalDur = Math.max(totalDur, win.dropAt + Math.max(0.01, len));
|
|
9270
|
+
}
|
|
9271
|
+
L.push(``);
|
|
9272
|
+
L.push(` var master = app.project.items.addComp("${masterName}", ${mw}, ${mh}, 1, ${r35(totalDur)}, ${r35(mfps)});`);
|
|
9273
|
+
L.push(` try { master.bgColor = [0.04,0.04,0.07]; } catch (e) {}`);
|
|
9274
|
+
const subVars = [];
|
|
9275
|
+
windows.forEach((win, wi) => {
|
|
9276
|
+
const c3 = win.ir.canvas;
|
|
9277
|
+
const sw = Math.max(4, Math.round(num(c3.w, 1920)));
|
|
9278
|
+
const sh = Math.max(4, Math.round(num(c3.h, 1080)));
|
|
9279
|
+
const sfps = Math.min(99, Math.max(1, num(c3.fps, 30)));
|
|
9280
|
+
const sdur = Math.max(0.1, num(c3.duration, 3));
|
|
9281
|
+
const subName = esc(`${win.uid}-${win.seq}`);
|
|
9282
|
+
const subVar = `sub${wi}`;
|
|
9283
|
+
L.push(``);
|
|
9284
|
+
L.push(` // ==== 子合成 #${wi} 技法 ${subName}(源窗 t0=${r35(win.t0)} t1=${r35(win.t1)})====`);
|
|
9285
|
+
L.push(` var ${subVar} = app.project.items.addComp("${subName}", ${sw}, ${sh}, 1, ${r35(sdur)}, ${r35(sfps)});`);
|
|
9286
|
+
const winFootage = { ...win.footage ?? {} };
|
|
9287
|
+
const subCtx = {
|
|
9288
|
+
lines: L,
|
|
9289
|
+
warnings: ctx.warnings,
|
|
9290
|
+
layerSeq: 0,
|
|
9291
|
+
compVar: subVar,
|
|
9292
|
+
footage: winFootage,
|
|
9293
|
+
bakeOps: true,
|
|
9294
|
+
footageVars: globalFootageVars
|
|
9295
|
+
};
|
|
9296
|
+
for (const ly of win.ir.layers ?? [])
|
|
9297
|
+
emitLayer(subCtx, win.ir, ly, null);
|
|
9298
|
+
subVars.push({ subVar, win });
|
|
9299
|
+
});
|
|
9300
|
+
L.push(``);
|
|
9301
|
+
L.push(` // ==== 母合成裁窗串接(startTime=落点−t0,inPoint=落点,outPoint=落点+窗长)====`);
|
|
9302
|
+
subVars.forEach(({ subVar, win }, wi) => {
|
|
9303
|
+
const len = Math.max(0.01, win.outLen ?? win.t1 - win.t0);
|
|
9304
|
+
const lv = `mL${wi}`;
|
|
9305
|
+
L.push(` var ${lv} = master.layers.add(${subVar});`);
|
|
9306
|
+
L.push(` ${lv}.startTime = ${r35(win.dropAt - win.t0)};`);
|
|
9307
|
+
L.push(` ${lv}.inPoint = ${r35(win.dropAt)};`);
|
|
9308
|
+
L.push(` ${lv}.outPoint = ${r35(win.dropAt + len)};`);
|
|
9309
|
+
L.push(` try {`);
|
|
9310
|
+
L.push(` var _cov = Math.max(${mw} / ${subVar}.width, ${mh} / ${subVar}.height) * 100;`);
|
|
9311
|
+
L.push(` ${lv}.property("ADBE Transform Group").property("ADBE Scale").setValue([_cov, _cov]);`);
|
|
9312
|
+
L.push(` ${lv}.property("ADBE Transform Group").property("ADBE Position").setValue([${mw / 2}, ${mh / 2}]);`);
|
|
9313
|
+
L.push(` } catch (e) {}`);
|
|
9314
|
+
});
|
|
9315
|
+
if (opts.bgm) {
|
|
9316
|
+
L.push(``);
|
|
9317
|
+
L.push(` // ==== BGM 入轨(importFile 包 try/catch,AE 侧文件缺失/损坏脚本继续跑完仅缺音轨)====`);
|
|
9318
|
+
L.push(` try {`);
|
|
9319
|
+
L.push(` var bgm = app.project.importFile(new ImportOptions(new File("${esc(fwdSlash(opts.bgm.path))}")));`);
|
|
9320
|
+
L.push(` var bgmL = master.layers.add(bgm);`);
|
|
9321
|
+
L.push(` bgmL.startTime = 0;`);
|
|
9322
|
+
L.push(` } catch (e) {}`);
|
|
9323
|
+
const markers = opts.bgm.markers ?? [];
|
|
9324
|
+
if (markers.length) {
|
|
9325
|
+
L.push(` // beat marker(downbeat 带标签区分)`);
|
|
9326
|
+
L.push(` try {`);
|
|
9327
|
+
L.push(` var mk = master.property("ADBE Marker");`);
|
|
9328
|
+
for (const m of markers) {
|
|
9329
|
+
const label = m.downbeat ? "downbeat" : "beat";
|
|
9330
|
+
L.push(` mk.setValueAtTime(${r35(m.t)}, new MarkerValue("${label}"));`);
|
|
9331
|
+
}
|
|
9332
|
+
L.push(` } catch (e) {}`);
|
|
9333
|
+
}
|
|
9334
|
+
}
|
|
9335
|
+
L.push(``);
|
|
9336
|
+
L.push(` master.openInViewer();`);
|
|
9337
|
+
L.push(`})();`);
|
|
9338
|
+
L.push(`app.endUndoGroup();`);
|
|
9339
|
+
return { jsx: L.join(`
|
|
9340
|
+
`) + `
|
|
9341
|
+
`, warnings: ctx.warnings };
|
|
9342
|
+
}
|
|
9343
|
+
|
|
9344
|
+
// src/lib/mad/scan.ts
|
|
9345
|
+
import { readdirSync, statSync as statSync2 } from "node:fs";
|
|
9346
|
+
import { extname as extname6, join as join22 } from "node:path";
|
|
9347
|
+
var VIDEO_EXTS2 = new Set(defaultExtsFor("video") ?? []);
|
|
9348
|
+
function scanFolder(dirAbs, opts = {}) {
|
|
9349
|
+
const probe = opts.probe ?? probeGeometry;
|
|
9350
|
+
const warn = opts.warn ?? (() => {});
|
|
9351
|
+
let entries;
|
|
9352
|
+
try {
|
|
9353
|
+
entries = readdirSync(dirAbs);
|
|
9354
|
+
} catch {
|
|
9355
|
+
throw new Error(`素材文件夹不存在或无法读取:${dirAbs}`);
|
|
9356
|
+
}
|
|
9357
|
+
const files = entries.filter((n) => VIDEO_EXTS2.has(extname6(n).toLowerCase())).map((n) => join22(dirAbs, n)).filter((p) => {
|
|
9358
|
+
try {
|
|
9359
|
+
return statSync2(p).isFile();
|
|
9360
|
+
} catch {
|
|
9361
|
+
return false;
|
|
9362
|
+
}
|
|
9363
|
+
}).sort();
|
|
9364
|
+
const videos = [];
|
|
9365
|
+
const skipped = [];
|
|
9366
|
+
for (const p of files) {
|
|
9367
|
+
try {
|
|
9368
|
+
const g2 = probe(p, opts.ffmpegPath);
|
|
9369
|
+
if (!(g2.width > 0) || !(g2.height > 0)) {
|
|
9370
|
+
skipped.push(p);
|
|
9371
|
+
warn(`跳过无法探测几何的文件:${p}`);
|
|
9372
|
+
continue;
|
|
9373
|
+
}
|
|
9374
|
+
videos.push({ path: p, width: g2.width, height: g2.height, duration: g2.duration || 0 });
|
|
9375
|
+
} catch {
|
|
9376
|
+
skipped.push(p);
|
|
9377
|
+
warn(`跳过 ffprobe 失败的文件:${p}`);
|
|
9378
|
+
}
|
|
9379
|
+
}
|
|
9380
|
+
if (videos.length === 0) {
|
|
9381
|
+
throw new Error(`未在「${dirAbs}」发现可用素材视频。支持的扩展名:${[...VIDEO_EXTS2].join(" ")}。请确认文件夹内含 3~10 条视频。`);
|
|
9382
|
+
}
|
|
9383
|
+
const portrait = videos.filter((v) => v.height > v.width).length;
|
|
9384
|
+
const orientation = portrait > videos.length / 2 ? "portrait" : "landscape";
|
|
9385
|
+
return { videos, orientation, skipped };
|
|
9386
|
+
}
|
|
9387
|
+
function masterCanvas(orientation) {
|
|
9388
|
+
return orientation === "portrait" ? { w: 1080, h: 1920, fps: 30 } : { w: 1920, h: 1080, fps: 30 };
|
|
9389
|
+
}
|
|
9390
|
+
|
|
9391
|
+
// src/lib/mad/selector.ts
|
|
9392
|
+
function mulberry322(seed) {
|
|
9393
|
+
let a = seed >>> 0;
|
|
9394
|
+
return () => {
|
|
9395
|
+
a |= 0;
|
|
9396
|
+
a = a + 1831565813 | 0;
|
|
9397
|
+
let t = Math.imul(a ^ a >>> 15, 1 | a);
|
|
9398
|
+
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
|
9399
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
9400
|
+
};
|
|
9401
|
+
}
|
|
9402
|
+
function tierWeight(format) {
|
|
9403
|
+
if (format === "am" || format === "nv")
|
|
9404
|
+
return 1;
|
|
9405
|
+
return 0.55;
|
|
9406
|
+
}
|
|
9407
|
+
function entryOrientation(e) {
|
|
9408
|
+
return e.h > e.w ? "portrait" : "landscape";
|
|
9409
|
+
}
|
|
9410
|
+
function baseWeight(e, orientation) {
|
|
9411
|
+
const nSeen = Math.sqrt(Math.max(1, e.n_seen));
|
|
9412
|
+
const tier = tierWeight(e.format);
|
|
9413
|
+
const orient = entryOrientation(e) === orientation ? 1 : 0.5;
|
|
9414
|
+
const fxPenalty = Math.max(0.1, 1 - Math.min(1, Math.max(0, e.unmapped_fx_ratio)));
|
|
9415
|
+
return nSeen * tier * orient * fxPenalty;
|
|
9416
|
+
}
|
|
9417
|
+
function weightedPick(weights, rnd) {
|
|
9418
|
+
const total = weights.reduce((s, w) => s + Math.max(0, w), 0);
|
|
9419
|
+
if (total <= 0)
|
|
9420
|
+
return weights.length ? Math.floor(rnd() * weights.length) : -1;
|
|
9421
|
+
let r = rnd() * total;
|
|
9422
|
+
for (let i = 0;i < weights.length; i++) {
|
|
9423
|
+
r -= Math.max(0, weights[i]);
|
|
9424
|
+
if (r <= 0)
|
|
9425
|
+
return i;
|
|
9426
|
+
}
|
|
9427
|
+
return weights.length - 1;
|
|
9428
|
+
}
|
|
9429
|
+
function budgetWindowCount(durationSec, targetAvgSec = 2.2) {
|
|
9430
|
+
const n = Math.round(durationSec / Math.max(0.5, targetAvgSec));
|
|
9431
|
+
return Math.min(12, Math.max(6, n));
|
|
9432
|
+
}
|
|
9433
|
+
function selectWindows(opts) {
|
|
9434
|
+
const { pool, videos, durationSec, orientation, seed } = opts;
|
|
9435
|
+
const rnd = mulberry322(seed || 1);
|
|
9436
|
+
const want = budgetWindowCount(durationSec, opts.targetAvgSec);
|
|
9437
|
+
if (pool.length === 0 || videos.length === 0)
|
|
9438
|
+
return [];
|
|
9439
|
+
const cats = [...new Set(pool.map((e) => e.cat))];
|
|
9440
|
+
const byCat = new Map;
|
|
9441
|
+
for (const c3 of cats)
|
|
9442
|
+
byCat.set(c3, pool.filter((e) => e.cat === c3));
|
|
9443
|
+
const chosen = [];
|
|
9444
|
+
const usedPatterns = new Set;
|
|
9445
|
+
const usedUids = new Set;
|
|
9446
|
+
let catCursor = 0;
|
|
9447
|
+
let videoCursor = 0;
|
|
9448
|
+
const srcCursor = new Map;
|
|
9449
|
+
const tryPickFrom = (candidates, relaxDedup) => {
|
|
9450
|
+
const pool2 = candidates.filter((e) => !usedUids.has(e.uid) && (relaxDedup || !usedPatterns.has(e.pid)));
|
|
9451
|
+
if (pool2.length === 0)
|
|
9452
|
+
return null;
|
|
9453
|
+
const weights = pool2.map((e) => baseWeight(e, orientation));
|
|
9454
|
+
const idx = weightedPick(weights, rnd);
|
|
9455
|
+
return idx >= 0 ? pool2[idx] : null;
|
|
9456
|
+
};
|
|
9457
|
+
let guard = 0;
|
|
9458
|
+
while (chosen.length < want && guard < want * (cats.length + 4) + 50) {
|
|
9459
|
+
guard++;
|
|
9460
|
+
let entry = null;
|
|
9461
|
+
for (let k = 0;k < cats.length && !entry; k++) {
|
|
9462
|
+
const cat = cats[(catCursor + k) % cats.length];
|
|
9463
|
+
entry = tryPickFrom(byCat.get(cat) ?? [], false);
|
|
9464
|
+
if (entry)
|
|
9465
|
+
catCursor = (catCursor + k + 1) % cats.length;
|
|
9466
|
+
}
|
|
9467
|
+
if (!entry)
|
|
9468
|
+
entry = tryPickFrom(pool, true);
|
|
9469
|
+
if (!entry)
|
|
9470
|
+
break;
|
|
9471
|
+
usedUids.add(entry.uid);
|
|
9472
|
+
usedPatterns.add(entry.pid);
|
|
9473
|
+
const video = videos[videoCursor % videos.length];
|
|
9474
|
+
videoCursor++;
|
|
9475
|
+
const winLen = Math.max(0.4, entry.t1 - entry.t0);
|
|
9476
|
+
const prev = srcCursor.get(video.path) ?? 0;
|
|
9477
|
+
const room = Math.max(0.1, video.duration - winLen);
|
|
9478
|
+
const srcOffsetBase = room > 0 ? prev % room : 0;
|
|
9479
|
+
srcCursor.set(video.path, prev + winLen);
|
|
9480
|
+
chosen.push({ entry, video, srcOffsetBase });
|
|
9481
|
+
}
|
|
9482
|
+
return chosen;
|
|
9483
|
+
}
|
|
9484
|
+
|
|
9485
|
+
// src/lib/mad/beat.ts
|
|
9486
|
+
var clamp2 = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
|
|
9487
|
+
var r36 = (v) => Math.round(v * 1000) / 1000;
|
|
9488
|
+
var MIN_WIN = 0.4;
|
|
9489
|
+
var MAX_WIN = 6;
|
|
9490
|
+
function fixedRhythm(natLens) {
|
|
9491
|
+
const placements = [];
|
|
9492
|
+
let t = 0;
|
|
9493
|
+
for (const nl of natLens) {
|
|
9494
|
+
const outLen = clamp2(nl, MIN_WIN, MAX_WIN);
|
|
9495
|
+
placements.push({ dropAt: r36(t), outLen: r36(outLen) });
|
|
9496
|
+
t += outLen;
|
|
9497
|
+
}
|
|
9498
|
+
return { placements, markers: [] };
|
|
9499
|
+
}
|
|
9500
|
+
function beatQuantized(natLens, analysis) {
|
|
9501
|
+
const dbs = (analysis.downbeats ?? []).filter((t2) => Number.isFinite(t2) && t2 >= 0).sort((a, b) => a - b);
|
|
9502
|
+
const bts = (analysis.beats ?? []).filter((t2) => Number.isFinite(t2) && t2 >= 0).sort((a, b) => a - b);
|
|
9503
|
+
const dbSpanOk = dbs.length >= 2 && dbs[1] - dbs[0] <= MAX_WIN;
|
|
9504
|
+
const snap = dbSpanOk ? dbs : bts;
|
|
9505
|
+
const snapSet = new Set(dbs.map((t2) => r36(t2)));
|
|
9506
|
+
if (snap.length < 2) {
|
|
9507
|
+
return { plan: fixedRhythm(natLens), level: 2 };
|
|
9508
|
+
}
|
|
9509
|
+
const placements = [];
|
|
9510
|
+
let t = 0;
|
|
9511
|
+
let si = 0;
|
|
9512
|
+
for (let i = 0;i < natLens.length; i++) {
|
|
9513
|
+
const dropAt = t;
|
|
9514
|
+
while (si < snap.length && snap[si] <= dropAt + MIN_WIN)
|
|
9515
|
+
si++;
|
|
9516
|
+
if (si >= snap.length) {
|
|
9517
|
+
const outLen = clamp2(natLens[i], MIN_WIN, MAX_WIN);
|
|
9518
|
+
placements.push({ dropAt: r36(dropAt), outLen: r36(outLen) });
|
|
9519
|
+
t = dropAt + outLen;
|
|
9520
|
+
continue;
|
|
9521
|
+
}
|
|
9522
|
+
const nextSnap = snap[si];
|
|
9523
|
+
const slotLen = clamp2(nextSnap - dropAt, MIN_WIN, MAX_WIN);
|
|
9524
|
+
placements.push({ dropAt: r36(dropAt), outLen: r36(slotLen) });
|
|
9525
|
+
t = dropAt + slotLen;
|
|
9526
|
+
si++;
|
|
9527
|
+
}
|
|
9528
|
+
const totalDur = placements.length ? placements[placements.length - 1].dropAt + placements[placements.length - 1].outLen : 0;
|
|
9529
|
+
const allBeats = [...new Set([...bts, ...dbs].map((x) => r36(x)))].sort((a, b) => a - b);
|
|
9530
|
+
const markers = allBeats.filter((tt) => tt >= 0 && tt <= totalDur + 0.000001).map((tt) => ({ t: tt, downbeat: snapSet.has(tt) }));
|
|
9531
|
+
return { plan: { placements, markers }, level: 1 };
|
|
9532
|
+
}
|
|
9533
|
+
|
|
9534
|
+
// src/lib/mad/data.ts
|
|
9535
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
9536
|
+
import { mkdir as mkdir9, readFile as readFile7, rename, rm, writeFile as writeFile9, readdir } from "node:fs/promises";
|
|
9537
|
+
import { existsSync as existsSync18 } from "node:fs";
|
|
9538
|
+
import { join as join23 } from "node:path";
|
|
9539
|
+
function madCacheDir() {
|
|
9540
|
+
return homeFile("mad-cache");
|
|
9541
|
+
}
|
|
9542
|
+
function madContentBase() {
|
|
9543
|
+
return (process.env.GITRUCK_MAD_BASE ?? "https://api.ai-mcn.tv:10000").replace(/\/+$/, "");
|
|
9544
|
+
}
|
|
9545
|
+
function manifestUrl() {
|
|
9546
|
+
return `${madContentBase()}/task/mad/manifest`;
|
|
9547
|
+
}
|
|
9548
|
+
var REQUIRED_KEYS = ["mad_pool"];
|
|
9549
|
+
function sha256Hex(buf) {
|
|
9550
|
+
return createHash2("sha256").update(buf).digest("hex");
|
|
9551
|
+
}
|
|
9552
|
+
async function fetchWithTimeout(fetchFn, url, timeoutMs) {
|
|
9553
|
+
const ctrl = new AbortController;
|
|
9554
|
+
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
9555
|
+
try {
|
|
9556
|
+
return await fetchFn(url, { signal: ctrl.signal });
|
|
9557
|
+
} finally {
|
|
9558
|
+
clearTimeout(timer);
|
|
9559
|
+
}
|
|
9560
|
+
}
|
|
9561
|
+
async function atomicWrite(dest, data) {
|
|
9562
|
+
const tmp = `${dest}.tmp-${process.pid}-${Date.now()}`;
|
|
9563
|
+
await writeFile9(tmp, data);
|
|
9564
|
+
await rename(tmp, dest);
|
|
9565
|
+
}
|
|
9566
|
+
async function verifyFile(path, sha256) {
|
|
9567
|
+
if (!existsSync18(path))
|
|
9568
|
+
return false;
|
|
9569
|
+
try {
|
|
9570
|
+
const buf = await readFile7(path);
|
|
9571
|
+
return sha256Hex(buf) === sha256;
|
|
9572
|
+
} catch {
|
|
9573
|
+
return false;
|
|
9574
|
+
}
|
|
9575
|
+
}
|
|
9576
|
+
function validateManifest(obj) {
|
|
9577
|
+
if (!obj || typeof obj !== "object")
|
|
9578
|
+
throw new Error("manifest 结构非法");
|
|
9579
|
+
const m = obj;
|
|
9580
|
+
if (typeof m.version !== "number")
|
|
9581
|
+
throw new Error("manifest 缺 version");
|
|
9582
|
+
if (!m.datasets || typeof m.datasets !== "object")
|
|
9583
|
+
throw new Error("manifest 缺 datasets");
|
|
9584
|
+
if (typeof m.assets_base !== "string" || !/^https:\/\//i.test(m.assets_base)) {
|
|
9585
|
+
throw new Error("manifest 缺 assets_base(须 HTTPS)");
|
|
9586
|
+
}
|
|
9587
|
+
return m;
|
|
9588
|
+
}
|
|
9589
|
+
async function cleanupOldVersions(cacheRoot, keepVersion, warn) {
|
|
9590
|
+
try {
|
|
9591
|
+
const entries = await readdir(cacheRoot);
|
|
9592
|
+
for (const e of entries) {
|
|
9593
|
+
const m = /^v(\d+)$/.exec(e);
|
|
9594
|
+
if (m && Number(m[1]) !== keepVersion) {
|
|
9595
|
+
await rm(join23(cacheRoot, e), { recursive: true, force: true }).catch(() => {});
|
|
9596
|
+
}
|
|
9597
|
+
}
|
|
9598
|
+
} catch {}
|
|
9599
|
+
}
|
|
9600
|
+
async function ensureMadData(opts, deps) {
|
|
9601
|
+
const { cacheRoot, warn } = deps;
|
|
9602
|
+
const timeout = deps.manifestTimeoutMs ?? 8000;
|
|
9603
|
+
const mfUrl = deps.manifestUrl ?? manifestUrl();
|
|
9604
|
+
const snapshotPath = join23(cacheRoot, "manifest.json");
|
|
9605
|
+
let manifest = null;
|
|
9606
|
+
let online = false;
|
|
9607
|
+
try {
|
|
9608
|
+
if (!/^https:\/\//i.test(mfUrl))
|
|
9609
|
+
throw new Error("manifest 必须 HTTPS");
|
|
9610
|
+
const res = await fetchWithTimeout(deps.fetchFn, mfUrl, timeout);
|
|
9611
|
+
if (!res.ok)
|
|
9612
|
+
throw new Error(`manifest HTTP ${res.status}`);
|
|
9613
|
+
manifest = validateManifest(await res.json());
|
|
9614
|
+
online = true;
|
|
9615
|
+
} catch (e) {
|
|
9616
|
+
warn(`manifest 拉取失败(${e instanceof Error ? e.message : String(e)}),回退本地缓存`);
|
|
9617
|
+
}
|
|
9618
|
+
if (!manifest) {
|
|
9619
|
+
if (!existsSync18(snapshotPath)) {
|
|
9620
|
+
throw new Error("首次使用需联网下载技法数据。请连网后重跑 `gtrk tool mad`(数据会缓存到本地,之后可离线出片)。");
|
|
9621
|
+
}
|
|
9622
|
+
try {
|
|
9623
|
+
manifest = validateManifest(JSON.parse(await readFile7(snapshotPath, "utf8")));
|
|
9624
|
+
} catch {
|
|
9625
|
+
throw new Error("本地 manifest 缓存损坏且当前离线。请连网重跑,或加 --refresh 强制重新下载。");
|
|
9626
|
+
}
|
|
9627
|
+
}
|
|
9628
|
+
for (const k of REQUIRED_KEYS) {
|
|
9629
|
+
if (!manifest.datasets[k]) {
|
|
9630
|
+
throw new Error("技法数据尚未就绪,请稍后再试。");
|
|
9631
|
+
}
|
|
9632
|
+
}
|
|
9633
|
+
const version = manifest.version;
|
|
9634
|
+
const verDir = join23(cacheRoot, `v${version}`);
|
|
9635
|
+
const poolPath = join23(verDir, "mad_pool.json");
|
|
9636
|
+
const poolMeta = manifest.datasets.mad_pool;
|
|
9637
|
+
const cacheValid = await verifyFile(poolPath, poolMeta.sha256);
|
|
9638
|
+
const needDownload = !!opts.refresh || !cacheValid;
|
|
9639
|
+
if (needDownload) {
|
|
9640
|
+
if (!online) {
|
|
9641
|
+
if (existsSync18(poolPath)) {
|
|
9642
|
+
throw new Error("本地技法数据缓存损坏且当前离线。请连网重跑,或加 --refresh 强制重新下载。");
|
|
9643
|
+
}
|
|
9644
|
+
throw new Error("首次使用需联网下载技法数据。请连网后重跑 `gtrk tool mad`(数据会缓存到本地,之后可离线出片)。");
|
|
9645
|
+
}
|
|
9646
|
+
await mkdir9(verDir, { recursive: true });
|
|
9647
|
+
warn(`下载技法池数据 mad_pool(版本 v${version},约 ${Math.round(poolMeta.size / 1024)} KB)…`);
|
|
9648
|
+
const res = await deps.fetchFn(poolMeta.url);
|
|
9649
|
+
if (!res.ok)
|
|
9650
|
+
throw new Error(`mad_pool 下载失败 HTTP ${res.status}`);
|
|
9651
|
+
const buf = new Uint8Array(await res.arrayBuffer());
|
|
9652
|
+
if (sha256Hex(buf) !== poolMeta.sha256) {
|
|
9653
|
+
throw new Error("mad_pool 下载校验不通过(sha256 不符),请重试或 --refresh。");
|
|
9654
|
+
}
|
|
9655
|
+
await atomicWrite(poolPath, buf);
|
|
9656
|
+
warn(`技法池数据就绪(v${version})`);
|
|
9657
|
+
}
|
|
9658
|
+
if (online) {
|
|
9659
|
+
await mkdir9(cacheRoot, { recursive: true });
|
|
9660
|
+
await atomicWrite(snapshotPath, new Uint8Array(Buffer.from(JSON.stringify(manifest), "utf8")));
|
|
9661
|
+
await cleanupOldVersions(cacheRoot, version, warn);
|
|
9662
|
+
}
|
|
9663
|
+
let pool;
|
|
9664
|
+
try {
|
|
9665
|
+
pool = JSON.parse(await readFile7(poolPath, "utf8"));
|
|
9666
|
+
if (!Array.isArray(pool))
|
|
9667
|
+
throw new Error("mad_pool 非数组");
|
|
9668
|
+
} catch (e) {
|
|
9669
|
+
throw new Error(`技法池数据装载失败:${e instanceof Error ? e.message : String(e)}(可加 --refresh 重拉)`);
|
|
9670
|
+
}
|
|
9671
|
+
return { version, assetsBase: manifest.assets_base, pool, verDir, online };
|
|
9672
|
+
}
|
|
9673
|
+
|
|
9674
|
+
// src/lib/mad/pool.ts
|
|
9675
|
+
import { gunzipSync } from "node:zlib";
|
|
9676
|
+
import { mkdir as mkdir10, readFile as readFile8 } from "node:fs/promises";
|
|
9677
|
+
import { existsSync as existsSync19 } from "node:fs";
|
|
9678
|
+
import { join as join24 } from "node:path";
|
|
9679
|
+
function shardPath(verDir, shard) {
|
|
9680
|
+
return join24(verDir, "ir", `${shard}.json.gz`);
|
|
9681
|
+
}
|
|
9682
|
+
function decodeShard(gz) {
|
|
9683
|
+
const json = gunzipSync(gz).toString("utf8");
|
|
9684
|
+
const obj = JSON.parse(json);
|
|
9685
|
+
if (!obj || typeof obj !== "object")
|
|
9686
|
+
throw new Error("IR 分片结构非法");
|
|
9687
|
+
return obj;
|
|
9688
|
+
}
|
|
9689
|
+
function makeIrLoader(assetsBase, verDir, online, deps) {
|
|
9690
|
+
const memo = new Map;
|
|
9691
|
+
const base = assetsBase.replace(/\/+$/, "");
|
|
9692
|
+
async function loadShard(shard) {
|
|
9693
|
+
const cached = memo.get(shard);
|
|
9694
|
+
if (cached)
|
|
9695
|
+
return cached;
|
|
9696
|
+
const path = shardPath(verDir, shard);
|
|
9697
|
+
if (existsSync19(path)) {
|
|
9698
|
+
try {
|
|
9699
|
+
const s2 = decodeShard(await readFile8(path));
|
|
9700
|
+
memo.set(shard, s2);
|
|
9701
|
+
return s2;
|
|
9702
|
+
} catch {
|
|
9703
|
+
deps.warn(`IR 分片缓存损坏(${shard}),尝试重拉`);
|
|
9704
|
+
}
|
|
9705
|
+
}
|
|
9706
|
+
if (!online) {
|
|
9707
|
+
throw new Error(`离线且 IR 分片「${shard}」无缓存。请连网重跑,或加 --refresh 预热数据。`);
|
|
9708
|
+
}
|
|
9709
|
+
const url = `${base}/ir/${shard}.json.gz`;
|
|
9710
|
+
const res = await deps.fetchFn(url);
|
|
9711
|
+
if (!res.ok)
|
|
9712
|
+
throw new Error(`IR 分片下载失败 HTTP ${res.status}:${url}`);
|
|
9713
|
+
const buf = new Uint8Array(await res.arrayBuffer());
|
|
9714
|
+
const s = decodeShard(buf);
|
|
9715
|
+
await mkdir10(join24(verDir, "ir"), { recursive: true });
|
|
9716
|
+
await atomicWrite(path, buf);
|
|
9717
|
+
memo.set(shard, s);
|
|
9718
|
+
return s;
|
|
9719
|
+
}
|
|
9720
|
+
return {
|
|
9721
|
+
async getIr(entry) {
|
|
9722
|
+
const shard = await loadShard(entry.shard);
|
|
9723
|
+
const ir = shard[entry.ir];
|
|
9724
|
+
if (!ir)
|
|
9725
|
+
throw new Error(`IR 分片「${entry.shard}」中缺工程 ir=${entry.ir}`);
|
|
9726
|
+
return ir;
|
|
9727
|
+
},
|
|
9728
|
+
shardCached(shard) {
|
|
9729
|
+
return memo.has(shard) || existsSync19(shardPath(verDir, shard));
|
|
9730
|
+
}
|
|
9731
|
+
};
|
|
9732
|
+
}
|
|
9733
|
+
|
|
9734
|
+
// src/lib/mad/cloud-beat.ts
|
|
9735
|
+
var ANALYZE_TASK = "audio_music_analyze";
|
|
9736
|
+
function isCode(e, code) {
|
|
9737
|
+
return !!e && typeof e === "object" && e.code === code;
|
|
9738
|
+
}
|
|
9739
|
+
function extractAnalysis(output) {
|
|
9740
|
+
const o = output ?? {};
|
|
9741
|
+
const arr = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "number" && Number.isFinite(x)) : [];
|
|
9742
|
+
return {
|
|
9743
|
+
bpm: typeof o.bpm === "number" ? o.bpm : undefined,
|
|
9744
|
+
beats: arr(o.beats),
|
|
9745
|
+
downbeats: arr(o.downbeats)
|
|
9746
|
+
};
|
|
9747
|
+
}
|
|
9748
|
+
async function analyzeBgm(cfg, bgmAbs, deps) {
|
|
9749
|
+
let up = await deps.uploadCached(cfg, bgmAbs, {});
|
|
9750
|
+
const payload = (fid) => ({ file_id: fid });
|
|
9751
|
+
let taskId;
|
|
9752
|
+
try {
|
|
9753
|
+
taskId = await deps.submitTask(cfg, ANALYZE_TASK, payload(up.fileId));
|
|
9754
|
+
} catch (e) {
|
|
9755
|
+
if (up.cached && isCode(e, 6004)) {
|
|
9756
|
+
await deps.invalidateUpload(bgmAbs);
|
|
9757
|
+
up = await deps.uploadCached(cfg, bgmAbs, { force: true });
|
|
9758
|
+
taskId = await deps.submitTask(cfg, ANALYZE_TASK, payload(up.fileId));
|
|
9759
|
+
} else
|
|
9760
|
+
throw e;
|
|
9761
|
+
}
|
|
9762
|
+
const output = await deps.pollToolTask(cfg, ANALYZE_TASK, taskId, {});
|
|
9763
|
+
return extractAnalysis(output);
|
|
9764
|
+
}
|
|
9765
|
+
|
|
9766
|
+
// src/lib/mad/mad.ts
|
|
9767
|
+
function madHeader(version, generatedAt) {
|
|
9768
|
+
return [
|
|
9769
|
+
"/*═══════════════════════════════════════════════",
|
|
9770
|
+
" MAD 一键工程 — 由 gtrk 自动生成",
|
|
9771
|
+
" ───────────────────────────────────────────────",
|
|
9772
|
+
" 用法:After Effects 文件 › 脚本 › 运行脚本文件… 选择本文件",
|
|
9773
|
+
" 运行后自动生成母合成:素材已按技法段填入各素材位,",
|
|
9774
|
+
" 彩色占位块为素材位,可在合成里替换或微调。",
|
|
9775
|
+
"",
|
|
9776
|
+
" 生成工具:gtrk tool mad · 同合云 gitruck",
|
|
9777
|
+
" 了解一键成片流程:https://cloud.ai-mcn.tv/cli",
|
|
9778
|
+
` 版本 ${version} · 生成于 ${generatedAt}`,
|
|
9779
|
+
"═══════════════════════════════════════════════*/"
|
|
9780
|
+
].join(`
|
|
9781
|
+
`);
|
|
9782
|
+
}
|
|
9783
|
+
function completionMessage(jsxPath, level) {
|
|
9784
|
+
const beat = level === 1 ? "已按 BGM downbeat 卡点" : level === 2 ? "BGM 已入轨、按固定节奏切窗" : "按固定节奏切窗";
|
|
9785
|
+
return [
|
|
9786
|
+
`工程文件已生成:${jsxPath}(${beat})`,
|
|
9787
|
+
"用法:装 After Effects 2020+ 后,文件 › 脚本 › 运行脚本文件… 选它,30 秒重建整条时间线。"
|
|
9788
|
+
].join(`
|
|
9789
|
+
`);
|
|
9790
|
+
}
|
|
9791
|
+
function collectSlotIds(ir) {
|
|
9792
|
+
const ids = [];
|
|
9793
|
+
const walk = (layers) => {
|
|
9794
|
+
for (const l of layers) {
|
|
9795
|
+
const ly = l;
|
|
9796
|
+
if (ly.type === "image" || ly.type === "video")
|
|
9797
|
+
ids.push(String(ly.id ?? ""));
|
|
9798
|
+
if (ly.type === "group" && Array.isArray(ly.children))
|
|
9799
|
+
walk(ly.children);
|
|
9800
|
+
}
|
|
9801
|
+
};
|
|
9802
|
+
walk(ir.layers ?? []);
|
|
9803
|
+
return ids.filter((x) => x);
|
|
9804
|
+
}
|
|
9805
|
+
var SLOT_STAGGER = 0.3;
|
|
9806
|
+
function timestamp4(now) {
|
|
9807
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
9808
|
+
return `${p(now.getFullYear() % 100)}${p(now.getMonth() + 1)}${p(now.getDate())}-${p(now.getHours())}${p(now.getMinutes())}${p(now.getSeconds())}`;
|
|
9809
|
+
}
|
|
9810
|
+
async function runMad(inputArg, opts, deps = {}) {
|
|
9811
|
+
const warn = deps.warn ?? ((m) => process.stderr.write(`\x1B[2m ${m}\x1B[0m
|
|
9812
|
+
`));
|
|
9813
|
+
const emitBilling2 = deps.emitBilling ?? ((h) => process.stderr.write(`\x1B[33m⚠️ 计费提示:${h}\x1B[0m
|
|
9814
|
+
`));
|
|
9815
|
+
const now = (deps.now ?? (() => new Date))();
|
|
9816
|
+
const fetchFn = deps.fetchFn ?? fetch;
|
|
9817
|
+
const probeDur = deps.probeDurationFn ?? probeDuration;
|
|
9818
|
+
if (!inputArg)
|
|
9819
|
+
throw new Error("用法:gtrk tool mad <素材文件夹> [--bgm 歌.mp3]");
|
|
9820
|
+
const dirAbs = resolve10(inputArg);
|
|
9821
|
+
if (!existsSync20(dirAbs) || !statSync3(dirAbs).isDirectory()) {
|
|
9822
|
+
throw new Error(`素材文件夹不存在或不是目录:${dirAbs}`);
|
|
9823
|
+
}
|
|
9824
|
+
const { videos, orientation, skipped } = scanFolder(dirAbs, {
|
|
9825
|
+
ffmpegPath: opts.ffmpegPath,
|
|
9826
|
+
probe: deps.probeGeometry,
|
|
9827
|
+
warn
|
|
9828
|
+
});
|
|
9829
|
+
for (const s of skipped)
|
|
9830
|
+
;
|
|
9831
|
+
const master = masterCanvas(orientation);
|
|
9832
|
+
const dataDeps = { fetchFn, cacheRoot: deps.cacheRoot ?? madCacheDir(), warn };
|
|
9833
|
+
const data = await ensureMadData({ refresh: opts.refresh }, dataDeps);
|
|
9834
|
+
const irLoader = makeIrLoader(data.assetsBase, data.verDir, data.online, { fetchFn, warn });
|
|
9835
|
+
const durationSec = opts.duration && opts.duration > 0 ? opts.duration : 20;
|
|
9836
|
+
const seed = opts.seed && Number.isFinite(opts.seed) ? opts.seed : Math.floor(Math.random() * 2 ** 31);
|
|
9837
|
+
warn(`选窗种子 seed=${seed}(复现加 --seed ${seed})`);
|
|
9838
|
+
const selectablePool = data.online ? data.pool : data.pool.filter((e) => irLoader.shardCached(e.shard));
|
|
9839
|
+
if (!data.online && selectablePool.length === 0) {
|
|
9840
|
+
throw new Error("当前离线且无已缓存的技法数据分片。请连网重跑首拉,或加 --refresh 预热。");
|
|
9841
|
+
}
|
|
9842
|
+
const chosen = selectWindows({ pool: selectablePool, videos, durationSec, orientation, seed });
|
|
9843
|
+
if (chosen.length === 0) {
|
|
9844
|
+
throw new Error("技法池为空或无可用条目(数据版本可能异常,可加 --refresh 重拉)。");
|
|
9845
|
+
}
|
|
9846
|
+
const irs = [];
|
|
9847
|
+
for (const c3 of chosen)
|
|
9848
|
+
irs.push(await irLoader.getIr(c3.entry));
|
|
9849
|
+
let level = 3;
|
|
9850
|
+
let analysis = null;
|
|
9851
|
+
let bgmForTrack;
|
|
9852
|
+
const bgmAbs = opts.bgm ? resolve10(opts.bgm) : undefined;
|
|
9853
|
+
if (bgmAbs) {
|
|
9854
|
+
let dur = -1;
|
|
9855
|
+
try {
|
|
9856
|
+
dur = probeDur(bgmAbs, opts.ffmpegPath);
|
|
9857
|
+
} catch {
|
|
9858
|
+
dur = -1;
|
|
9859
|
+
}
|
|
9860
|
+
if (!(dur > 0)) {
|
|
9861
|
+
warn(`BGM 无法读取(ffprobe 校验失败),按无 BGM 固定节奏出片:${bgmAbs}`);
|
|
9862
|
+
level = 3;
|
|
9863
|
+
} else {
|
|
9864
|
+
let cfg = null;
|
|
9865
|
+
try {
|
|
9866
|
+
cfg = (deps.loadConfig ?? loadConfig)();
|
|
9867
|
+
} catch {
|
|
9868
|
+
cfg = null;
|
|
9869
|
+
}
|
|
9870
|
+
if (!cfg) {
|
|
9871
|
+
warn("未配置 API Key,无法解锁 BGM 卡点。跑 `gtrk init` 配置后可卡点;本次 BGM 已入轨、按固定节奏出片。");
|
|
9872
|
+
level = 2;
|
|
9873
|
+
bgmForTrack = bgmAbs;
|
|
9874
|
+
} else {
|
|
9875
|
+
emitBilling2("BGM 卡点将调用一次云端节拍分析(audio_music_analyze),按现行价计费。");
|
|
9876
|
+
const beatCloud = deps.beatCloud ?? { uploadCached, invalidateUpload, submitTask, pollToolTask };
|
|
9877
|
+
try {
|
|
9878
|
+
analysis = await analyzeBgm(cfg, bgmAbs, beatCloud);
|
|
9879
|
+
level = 1;
|
|
9880
|
+
bgmForTrack = bgmAbs;
|
|
9881
|
+
} catch (e) {
|
|
9882
|
+
warn(`BGM 云端分析失败(${e instanceof Error ? e.message : String(e)}),BGM 已入轨、切点回退固定节奏`);
|
|
9883
|
+
level = 2;
|
|
9884
|
+
bgmForTrack = bgmAbs;
|
|
9885
|
+
}
|
|
9886
|
+
}
|
|
9887
|
+
}
|
|
9888
|
+
}
|
|
9889
|
+
const natLens = chosen.map((c3) => Math.max(MIN_WIN, c3.entry.t1 - c3.entry.t0));
|
|
9890
|
+
let placements;
|
|
9891
|
+
let markers = [];
|
|
9892
|
+
if (level === 1 && analysis) {
|
|
9893
|
+
const q = beatQuantized(natLens, analysis);
|
|
9894
|
+
placements = q.plan.placements;
|
|
9895
|
+
markers = q.plan.markers;
|
|
9896
|
+
if (q.level === 2) {
|
|
9897
|
+
warn("BGM 无有效节拍(beats/downbeats 均空),切点回退固定节奏。");
|
|
9898
|
+
level = 2;
|
|
9899
|
+
markers = [];
|
|
9900
|
+
}
|
|
9901
|
+
} else {
|
|
9902
|
+
placements = fixedRhythm(natLens).placements;
|
|
9903
|
+
}
|
|
9904
|
+
const windows = chosen.map((c3, i) => {
|
|
9905
|
+
const ir = irs[i];
|
|
9906
|
+
const slotIds = collectSlotIds(ir);
|
|
9907
|
+
const footage = {};
|
|
9908
|
+
const vidDur = c3.video.duration || 0;
|
|
9909
|
+
const winLen = Math.max(MIN_WIN, c3.entry.t1 - c3.entry.t0);
|
|
9910
|
+
slotIds.forEach((lid, idx) => {
|
|
9911
|
+
let off = c3.srcOffsetBase + idx * SLOT_STAGGER;
|
|
9912
|
+
const room = Math.max(0, vidDur - winLen);
|
|
9913
|
+
if (room > 0)
|
|
9914
|
+
off = off % room;
|
|
9915
|
+
else
|
|
9916
|
+
off = 0;
|
|
9917
|
+
footage[lid] = { path: c3.video.path, srcOffset: Math.round(off * 1000) / 1000 };
|
|
9918
|
+
});
|
|
9919
|
+
return {
|
|
9920
|
+
ir,
|
|
9921
|
+
uid: c3.entry.uid,
|
|
9922
|
+
seq: i,
|
|
9923
|
+
t0: c3.entry.t0,
|
|
9924
|
+
t1: c3.entry.t1,
|
|
9925
|
+
dropAt: placements[i].dropAt,
|
|
9926
|
+
outLen: placements[i].outLen,
|
|
9927
|
+
footage
|
|
9928
|
+
};
|
|
9929
|
+
});
|
|
9930
|
+
const version = deps.cliVersion ?? "";
|
|
9931
|
+
const header = madHeader(version, now.toISOString());
|
|
9932
|
+
const bgm = level <= 2 && bgmForTrack ? { path: bgmForTrack, markers } : undefined;
|
|
9933
|
+
const { jsx } = madJsx({ master, windows, header, bgm });
|
|
9934
|
+
const outDir = opts.out ? resolve10(opts.out) : join25(process.cwd(), `mad-${timestamp4(now)}`);
|
|
9935
|
+
await mkdir11(outDir, { recursive: true });
|
|
9936
|
+
const jsxPath = join25(outDir, "mad.jsx");
|
|
9937
|
+
await writeFile10(jsxPath, jsx);
|
|
9938
|
+
const result = {
|
|
9939
|
+
ok: true,
|
|
9940
|
+
tool: "mad",
|
|
9941
|
+
outDir,
|
|
9942
|
+
files: [jsxPath],
|
|
9943
|
+
seed,
|
|
9944
|
+
dataVersion: data.version,
|
|
9945
|
+
degradeLevel: level,
|
|
9946
|
+
techniques: chosen.map((c3) => ({ uid: c3.entry.uid, pid: c3.entry.pid, cat: c3.entry.cat, t0: c3.entry.t0, t1: c3.entry.t1 }))
|
|
9947
|
+
};
|
|
9948
|
+
await writeFile10(join25(outDir, "result.json"), JSON.stringify({ ...result, finishedAt: now.toISOString() }, null, 2));
|
|
9949
|
+
warn(completionMessage(jsxPath, level));
|
|
9950
|
+
return result;
|
|
9951
|
+
}
|
|
9952
|
+
|
|
9953
|
+
// src/commands/tool.ts
|
|
9954
|
+
var collectParam2 = (v, acc) => {
|
|
9955
|
+
acc.push(v);
|
|
9956
|
+
return acc;
|
|
9957
|
+
};
|
|
9958
|
+
function configureToolCommand(cmd, registry = TOOL_REGISTRY) {
|
|
9959
|
+
cmd.description("单点工具族:`gtrk tool <name> [input]` 跑单个能力;`gtrk tool list` 查全部(含输入/产物/计费/状态)").option("-o, --out <dir>", "产物目录(缺省 = <输入名>-<tool>/;input=none 落 cwd 下 <tool>-<时间戳>/)").option("--param <k=v>", "透传任意云端参数(标量、可重复;如 --param width=1080)", collectParam2, []).option("--params-json <json>", "透传任意云端参数(JSON 对象)").option("--ffmpeg-path <dir>", "指定 ffmpeg/ffprobe 所在目录(缺省 ~/.gitruck/ffmpeg → 系统 PATH)").option("--reupload", "强制重新上传,忽略本地上传缓存").option("--json", "机读模式:人读日志转 stderr,stdout 只输出结果 JSON(给 agent/脚本解析)");
|
|
9960
|
+
const seen = new Set;
|
|
9961
|
+
for (const d of registry) {
|
|
9962
|
+
for (const o of d.options ?? []) {
|
|
9963
|
+
if (seen.has(o.flag))
|
|
9964
|
+
continue;
|
|
9965
|
+
seen.add(o.flag);
|
|
9966
|
+
cmd.option(o.flag, o.desc);
|
|
9967
|
+
}
|
|
9968
|
+
}
|
|
9969
|
+
cmd.action(async (words, opts) => {
|
|
9970
|
+
await runToolCommand(words ?? [], opts, registry);
|
|
9971
|
+
});
|
|
9972
|
+
return cmd;
|
|
9973
|
+
}
|
|
9974
|
+
function registerTool(program2, registry = TOOL_REGISTRY) {
|
|
9975
|
+
validateRegistry(registry);
|
|
9976
|
+
const cmd = program2.command("tool [words...]");
|
|
9977
|
+
configureToolCommand(cmd, registry);
|
|
9978
|
+
}
|
|
9979
|
+
async function runToolCommand(words, opts, registry = TOOL_REGISTRY, deps) {
|
|
9980
|
+
if (opts.json)
|
|
9981
|
+
routeLogsToStderr();
|
|
9982
|
+
const name = words[0];
|
|
9983
|
+
if (!name) {
|
|
9984
|
+
throw new Error("用法:`gtrk tool <name> [input]` 跑工具;`gtrk tool list` 查全部工具");
|
|
9985
|
+
}
|
|
9986
|
+
if (name === "list") {
|
|
9987
|
+
runList(opts, registry);
|
|
9988
|
+
return;
|
|
9989
|
+
}
|
|
9990
|
+
const descriptor = findTool(name, registry);
|
|
9991
|
+
if (!descriptor) {
|
|
9992
|
+
const names = registry.map((d) => d.name).join(", ");
|
|
9993
|
+
throw new Error(`未知工具「${name}」。可用工具:${names || "(空)"}(用 gtrk tool list 查看详情)`);
|
|
9994
|
+
}
|
|
9995
|
+
return runTool(descriptor, words[1], opts, deps);
|
|
9996
|
+
}
|
|
9997
|
+
function runList(opts, registry = TOOL_REGISTRY) {
|
|
9998
|
+
const rows = registry.map((d) => ({
|
|
9999
|
+
name: d.name,
|
|
10000
|
+
title: d.title,
|
|
10001
|
+
input: d.input.kind,
|
|
10002
|
+
output: d.outputHint,
|
|
10003
|
+
billingHint: d.billingHint,
|
|
10004
|
+
enabled: d.enabled,
|
|
10005
|
+
...d.disabledReason ? { disabledReason: d.disabledReason } : {}
|
|
10006
|
+
}));
|
|
10007
|
+
if (opts.json) {
|
|
10008
|
+
console.log(JSON.stringify(rows));
|
|
10009
|
+
return;
|
|
10010
|
+
}
|
|
10011
|
+
log.step("▶ gtrk 工具族(gtrk tool <name> [input]):");
|
|
10012
|
+
for (const r of rows) {
|
|
10013
|
+
const status = r.enabled ? "已上线" : `未开放(${r.disabledReason ?? "无原因"})`;
|
|
10014
|
+
log.info(`${r.name} — ${r.title}|输入 ${r.input}|产物 ${r.output}|${r.billingHint}|${status}`);
|
|
10015
|
+
}
|
|
10016
|
+
log.info("agent 一律带 --json;缺 API Key 先跑 `gtrk init`;跑前把计费提示转述给用户。");
|
|
10017
|
+
}
|
|
10018
|
+
async function runTool(descriptor, inputArg, opts, depsOverride) {
|
|
10019
|
+
if (!descriptor.enabled) {
|
|
10020
|
+
throw new Error(`能力未开放:${descriptor.disabledReason ?? "(未提供原因)"}(用 gtrk tool list 查看全部工具)`);
|
|
10021
|
+
}
|
|
10022
|
+
if (descriptor.kind === "local") {
|
|
10023
|
+
if (descriptor.name === "mad")
|
|
10024
|
+
return runMadInTool(inputArg, opts);
|
|
10025
|
+
throw new Error(`local 型工具「${descriptor.name}」由后续 change 实现,暂不可用`);
|
|
10026
|
+
}
|
|
10027
|
+
const cfg = loadConfig();
|
|
10028
|
+
const deps = {
|
|
10029
|
+
cfg,
|
|
10030
|
+
uploadCached,
|
|
10031
|
+
invalidateUpload,
|
|
10032
|
+
submitTask,
|
|
10033
|
+
getTaskResult,
|
|
10034
|
+
downloadStream,
|
|
10035
|
+
probeDurationSec: (p, ff) => probeDuration(p, ff),
|
|
10036
|
+
...depsOverride
|
|
10037
|
+
};
|
|
10038
|
+
log.step(`▶ ${descriptor.title}(${descriptor.name})…`);
|
|
10039
|
+
const result = await runCloudTool(descriptor, inputArg, opts, deps);
|
|
10040
|
+
if (opts.json)
|
|
10041
|
+
console.log(JSON.stringify(result));
|
|
10042
|
+
if (result.ok)
|
|
10043
|
+
log.ok(`完成。产物目录:${result.outDir}`);
|
|
10044
|
+
else {
|
|
10045
|
+
log.err(`部分产物未落地(任务已完成、积分可能已扣)。task.json 已保留,可凭 task_id 恢复:${result.taskId}`);
|
|
10046
|
+
process.exitCode = 1;
|
|
10047
|
+
}
|
|
10048
|
+
return result;
|
|
10049
|
+
}
|
|
10050
|
+
async function runMadInTool(inputArg, opts) {
|
|
10051
|
+
log.step("▶ 一键剪 MAD(mad)…");
|
|
10052
|
+
const madOpts = {
|
|
10053
|
+
bgm: typeof opts.bgm === "string" ? opts.bgm : undefined,
|
|
10054
|
+
duration: opts.duration != null ? Number(opts.duration) : undefined,
|
|
10055
|
+
seed: opts.seed != null ? Number(opts.seed) : undefined,
|
|
10056
|
+
refresh: !!opts.refresh,
|
|
10057
|
+
out: opts.out,
|
|
10058
|
+
ffmpegPath: opts.ffmpegPath,
|
|
10059
|
+
json: !!opts.json
|
|
10060
|
+
};
|
|
10061
|
+
const r = await runMad(inputArg, madOpts, { cliVersion: currentVersion() });
|
|
10062
|
+
if (opts.json)
|
|
10063
|
+
console.log(JSON.stringify(r));
|
|
10064
|
+
if (r.ok)
|
|
10065
|
+
log.ok(`完成。产物目录:${r.outDir}`);
|
|
10066
|
+
return { ok: r.ok, tool: r.tool, outDir: r.outDir, files: r.files };
|
|
10067
|
+
}
|
|
10068
|
+
|
|
8174
10069
|
// src/index.ts
|
|
8175
10070
|
try {
|
|
8176
10071
|
process.loadEnvFile?.();
|
|
8177
10072
|
} catch {}
|
|
8178
10073
|
migrateLegacyHome();
|
|
8179
|
-
var { version } = JSON.parse(readFileSync5(
|
|
10074
|
+
var { version } = JSON.parse(readFileSync5(join26(packageRoot(), "package.json"), "utf8"));
|
|
8180
10075
|
var program2 = new Command;
|
|
8181
10076
|
program2.name("gtrk").description("同合云成片流水线 CLI —— agent 驱动云端任务、产物拉回本地、三方工程文件(客户端/剪映/PR)互通").version(version);
|
|
8182
10077
|
registerInstall(program2);
|
|
@@ -8190,6 +10085,7 @@ registerRender(program2);
|
|
|
8190
10085
|
registerSplit(program2);
|
|
8191
10086
|
registerMatrix(program2);
|
|
8192
10087
|
registerMg(program2);
|
|
10088
|
+
registerTool(program2);
|
|
8193
10089
|
program2.parseAsync(process.argv).catch((e) => {
|
|
8194
10090
|
console.error(`
|
|
8195
10091
|
❌ ${e instanceof Error ? e.message : String(e)}`);
|