@gitruck/cli 0.2.20 → 0.2.23
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 +1 -1
- package/README.md +526 -524
- package/contracts/gsap-emit-v1.md +42 -3
- package/dist/index.js +904 -235
- package/package.json +65 -65
- package/skills/gtrk-long2short/SKILL.md +8 -1
- package/skills/gtrk-tools/SKILL.md +22 -1
package/dist/index.js
CHANGED
|
@@ -4197,7 +4197,7 @@ var {
|
|
|
4197
4197
|
|
|
4198
4198
|
// src/index.ts
|
|
4199
4199
|
import { readFileSync as readFileSync6 } from "node:fs";
|
|
4200
|
-
import { join as
|
|
4200
|
+
import { join as join31 } from "node:path";
|
|
4201
4201
|
|
|
4202
4202
|
// src/lib/paths.ts
|
|
4203
4203
|
import { dirname, join } from "node:path";
|
|
@@ -4557,6 +4557,7 @@ function writeUserConfig(patch) {
|
|
|
4557
4557
|
// src/lib/jianying.ts
|
|
4558
4558
|
import { join as join4, resolve as resolve2 } from "node:path";
|
|
4559
4559
|
import { existsSync as existsSync4 } from "node:fs";
|
|
4560
|
+
import { cp, mkdir, readdir } from "node:fs/promises";
|
|
4560
4561
|
function probeJianyingDraftDir() {
|
|
4561
4562
|
const local = process.env.LOCALAPPDATA;
|
|
4562
4563
|
if (!local)
|
|
@@ -4575,6 +4576,31 @@ function resolveJianyingDraftDir(opt) {
|
|
|
4575
4576
|
return saved;
|
|
4576
4577
|
return probeJianyingDraftDir();
|
|
4577
4578
|
}
|
|
4579
|
+
var JIANYING_DRAFT_FILES = ["draft_content.json", "draft_meta_info.json"];
|
|
4580
|
+
async function copyJianyingDraft(srcDir, destDir) {
|
|
4581
|
+
const entries = (await readdir(srcDir, { withFileTypes: true })).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
4582
|
+
const chosen = new Map;
|
|
4583
|
+
for (const fixed of JIANYING_DRAFT_FILES) {
|
|
4584
|
+
const cands = entries.filter((e) => e.isFile() && e.name.toLowerCase().endsWith(fixed));
|
|
4585
|
+
const pick = cands.find((e) => e.name.toLowerCase() === fixed) ?? cands[0];
|
|
4586
|
+
if (pick)
|
|
4587
|
+
chosen.set(pick.name, fixed);
|
|
4588
|
+
}
|
|
4589
|
+
await mkdir(destDir, { recursive: true });
|
|
4590
|
+
const landed = {};
|
|
4591
|
+
const passthrough = [];
|
|
4592
|
+
for (const e of entries) {
|
|
4593
|
+
const fixed = chosen.get(e.name);
|
|
4594
|
+
const dest = join4(destDir, fixed ?? e.name);
|
|
4595
|
+
await cp(join4(srcDir, e.name), dest, { recursive: true });
|
|
4596
|
+
if (fixed)
|
|
4597
|
+
landed[fixed] = dest;
|
|
4598
|
+
else
|
|
4599
|
+
passthrough.push(e.name);
|
|
4600
|
+
}
|
|
4601
|
+
const missing = JIANYING_DRAFT_FILES.filter((f) => !landed[f]);
|
|
4602
|
+
return { landed, missing, complete: missing.length === 0, passthrough };
|
|
4603
|
+
}
|
|
4578
4604
|
|
|
4579
4605
|
// src/lib/open.ts
|
|
4580
4606
|
import { spawn } from "node:child_process";
|
|
@@ -6175,7 +6201,7 @@ function registerInstall(program2) {
|
|
|
6175
6201
|
|
|
6176
6202
|
// src/commands/oralcut.ts
|
|
6177
6203
|
import { resolve as resolve4, join as join15, dirname as dirname3, basename as basename6, extname as extname2 } from "node:path";
|
|
6178
|
-
import { mkdir as
|
|
6204
|
+
import { mkdir as mkdir5, writeFile as writeFile5, readFile as readFile4 } from "node:fs/promises";
|
|
6179
6205
|
import { existsSync as existsSync13 } from "node:fs";
|
|
6180
6206
|
|
|
6181
6207
|
// src/lib/config.ts
|
|
@@ -6330,7 +6356,7 @@ async function download(url, dest) {
|
|
|
6330
6356
|
|
|
6331
6357
|
// src/lib/upload-cache.ts
|
|
6332
6358
|
import { join as join11 } from "node:path";
|
|
6333
|
-
import { stat as stat3, mkdir, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
|
|
6359
|
+
import { stat as stat3, mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
|
|
6334
6360
|
import { existsSync as existsSync10 } from "node:fs";
|
|
6335
6361
|
|
|
6336
6362
|
// src/lib/chunk-upload.ts
|
|
@@ -6581,7 +6607,7 @@ async function load() {
|
|
|
6581
6607
|
}
|
|
6582
6608
|
}
|
|
6583
6609
|
async function save(cache) {
|
|
6584
|
-
await
|
|
6610
|
+
await mkdir2(CACHE_DIR, { recursive: true });
|
|
6585
6611
|
await writeFile2(CACHE_FILE, JSON.stringify(cache, null, 2));
|
|
6586
6612
|
}
|
|
6587
6613
|
var defaultUploadCacheDeps = {
|
|
@@ -6608,7 +6634,7 @@ async function loadSessions() {
|
|
|
6608
6634
|
}
|
|
6609
6635
|
}
|
|
6610
6636
|
async function saveSessions(sessions) {
|
|
6611
|
-
await
|
|
6637
|
+
await mkdir2(CACHE_DIR, { recursive: true });
|
|
6612
6638
|
await writeFile2(SESSION_FILE, JSON.stringify(sessions, null, 2));
|
|
6613
6639
|
}
|
|
6614
6640
|
var fileSessionStore = {
|
|
@@ -6696,7 +6722,7 @@ async function uploadAndSubmitTask(cfg, path, taskType, buildPayload, options =
|
|
|
6696
6722
|
}
|
|
6697
6723
|
|
|
6698
6724
|
// src/lib/media.ts
|
|
6699
|
-
import { mkdir as
|
|
6725
|
+
import { mkdir as mkdir3, stat as stat4 } from "node:fs/promises";
|
|
6700
6726
|
import { existsSync as existsSync11 } from "node:fs";
|
|
6701
6727
|
import { basename as basename4, extname, join as join12 } from "node:path";
|
|
6702
6728
|
function parseFps(rate) {
|
|
@@ -6747,7 +6773,7 @@ function probeDuration(path, ffmpegPath) {
|
|
|
6747
6773
|
async function artifactPath(inputAbs, ext) {
|
|
6748
6774
|
const s = await stat4(inputAbs);
|
|
6749
6775
|
const base = basename4(inputAbs, extname(inputAbs));
|
|
6750
|
-
await
|
|
6776
|
+
await mkdir3(audioCacheDir(), { recursive: true });
|
|
6751
6777
|
return join12(audioCacheDir(), `${base}.${s.size}_${Math.round(s.mtimeMs)}.${ext}`);
|
|
6752
6778
|
}
|
|
6753
6779
|
async function extractAudio(inputAbs, ffmpegPath) {
|
|
@@ -6807,10 +6833,72 @@ function assertDurationConsistent(originalDuration, artifactAbs, ffmpegPath, tol
|
|
|
6807
6833
|
throw new Error(`抽出物时长(${got.toFixed(2)}s)与原片(${originalDuration.toFixed(2)}s)不一致(容差 ${tolSec}s),` + `疑似抽取异常,已中止上传`);
|
|
6808
6834
|
}
|
|
6809
6835
|
}
|
|
6836
|
+
function assFontNames(assText) {
|
|
6837
|
+
const names = new Set;
|
|
6838
|
+
for (const line2 of assText.split(/\r?\n/)) {
|
|
6839
|
+
if (!line2.startsWith("Style:"))
|
|
6840
|
+
continue;
|
|
6841
|
+
const font = line2.slice("Style:".length).split(",")[1]?.trim();
|
|
6842
|
+
if (font)
|
|
6843
|
+
names.add(font);
|
|
6844
|
+
}
|
|
6845
|
+
return [...names];
|
|
6846
|
+
}
|
|
6847
|
+
async function probeFontAvailable(font, ffmpegPath) {
|
|
6848
|
+
const { ffmpeg } = requireFfmpeg(ffmpegPath);
|
|
6849
|
+
try {
|
|
6850
|
+
await runFfmpeg(ffmpeg, [
|
|
6851
|
+
"-v",
|
|
6852
|
+
"error",
|
|
6853
|
+
"-f",
|
|
6854
|
+
"lavfi",
|
|
6855
|
+
"-i",
|
|
6856
|
+
"color=c=black:s=64x64:d=0.04",
|
|
6857
|
+
"-vf",
|
|
6858
|
+
`drawtext=font='${font}':text=A`,
|
|
6859
|
+
"-frames:v",
|
|
6860
|
+
"1",
|
|
6861
|
+
"-f",
|
|
6862
|
+
"null",
|
|
6863
|
+
"-"
|
|
6864
|
+
]);
|
|
6865
|
+
return true;
|
|
6866
|
+
} catch (e) {
|
|
6867
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
6868
|
+
if (/valid font|font.*not found|Fontconfig error/i.test(msg))
|
|
6869
|
+
return false;
|
|
6870
|
+
return null;
|
|
6871
|
+
}
|
|
6872
|
+
}
|
|
6873
|
+
async function burnSubtitle(videoAbs, assAbs, outAbs, opts = {}) {
|
|
6874
|
+
const { ffmpeg } = requireFfmpeg(opts.ffmpegPath);
|
|
6875
|
+
const filterPath = assAbs.replace(/\\/g, "/").replace(/:/g, "\\:");
|
|
6876
|
+
await runFfmpeg(ffmpeg, [
|
|
6877
|
+
"-y",
|
|
6878
|
+
"-v",
|
|
6879
|
+
"error",
|
|
6880
|
+
"-i",
|
|
6881
|
+
videoAbs,
|
|
6882
|
+
"-vf",
|
|
6883
|
+
`ass='${filterPath}'`,
|
|
6884
|
+
"-c:v",
|
|
6885
|
+
opts.codec ?? "libx264",
|
|
6886
|
+
"-crf",
|
|
6887
|
+
String(opts.crf ?? 18),
|
|
6888
|
+
"-preset",
|
|
6889
|
+
"medium",
|
|
6890
|
+
"-c:a",
|
|
6891
|
+
"copy",
|
|
6892
|
+
"-movflags",
|
|
6893
|
+
"+faststart",
|
|
6894
|
+
outAbs
|
|
6895
|
+
]);
|
|
6896
|
+
return outAbs;
|
|
6897
|
+
}
|
|
6810
6898
|
|
|
6811
6899
|
// src/lib/materialize.ts
|
|
6812
6900
|
import { join as join14, basename as basename5 } from "node:path";
|
|
6813
|
-
import { mkdir as
|
|
6901
|
+
import { mkdir as mkdir4, writeFile as writeFile4 } from "node:fs/promises";
|
|
6814
6902
|
|
|
6815
6903
|
// src/lib/render.ts
|
|
6816
6904
|
import { writeFile as writeFile3, unlink, readFile as readFile3 } from "node:fs/promises";
|
|
@@ -7037,7 +7125,7 @@ async function materializeResult(opts) {
|
|
|
7037
7125
|
if (!files.length)
|
|
7038
7126
|
throw new Error("任务无工程文件产物(检查 project_formats / 任务是否产出)");
|
|
7039
7127
|
const errors = { ...output.errors ?? {} };
|
|
7040
|
-
await
|
|
7128
|
+
await mkdir4(outDir, { recursive: true });
|
|
7041
7129
|
const resultPath = join14(outDir, "result.json");
|
|
7042
7130
|
const writeResult = async (extra) => {
|
|
7043
7131
|
const r = {
|
|
@@ -7061,7 +7149,7 @@ async function materializeResult(opts) {
|
|
|
7061
7149
|
for (const f of files) {
|
|
7062
7150
|
const base = baseFormat(f.format);
|
|
7063
7151
|
const fmtDir = join14(outDir, base);
|
|
7064
|
-
await
|
|
7152
|
+
await mkdir4(fmtDir, { recursive: true });
|
|
7065
7153
|
const dest = join14(fmtDir, f.filename);
|
|
7066
7154
|
try {
|
|
7067
7155
|
await dl(f.download_url, dest);
|
|
@@ -7078,13 +7166,17 @@ async function materializeResult(opts) {
|
|
|
7078
7166
|
}
|
|
7079
7167
|
let jianyingDraftPath = null;
|
|
7080
7168
|
if (byFormat.jianying && opts.draftDir) {
|
|
7169
|
+
const dest = join14(opts.draftDir, basename5(outDir));
|
|
7081
7170
|
try {
|
|
7082
|
-
|
|
7083
|
-
|
|
7084
|
-
|
|
7085
|
-
|
|
7171
|
+
const landing = await copyJianyingDraft(join14(outDir, "jianying"), dest);
|
|
7172
|
+
if (landing.complete) {
|
|
7173
|
+
jianyingDraftPath = dest;
|
|
7174
|
+
log.info(`剪映草稿已落到:${dest}`);
|
|
7175
|
+
} else {
|
|
7176
|
+
errors["jianying:draft"] = `草稿两件套不全(缺 ${landing.missing.join("、")}),剪映列表里不会显示:${dest}`;
|
|
7177
|
+
log.warn(errors["jianying:draft"]);
|
|
7178
|
+
}
|
|
7086
7179
|
} catch (e) {
|
|
7087
|
-
jianyingDraftPath = null;
|
|
7088
7180
|
errors["jianying:draft"] = e instanceof Error ? e.message : String(e);
|
|
7089
7181
|
log.warn(`剪映草稿落盘失败:${errors["jianying:draft"]}`);
|
|
7090
7182
|
}
|
|
@@ -7259,7 +7351,7 @@ async function runOralCut(input, opts) {
|
|
|
7259
7351
|
const { taskId } = submitted;
|
|
7260
7352
|
const up = { fileId: submitted.fileId, cached: submitted.cached };
|
|
7261
7353
|
log.info(`task_id = ${taskId}`);
|
|
7262
|
-
await
|
|
7354
|
+
await mkdir5(outDir, { recursive: true });
|
|
7263
7355
|
await writeFile5(join15(outDir, "task.json"), JSON.stringify({ taskId, taskType: TASK_TYPE, fileId: up.fileId, source: inputAbs, formats, createdAt: new Date().toISOString() }, null, 2));
|
|
7264
7356
|
log.step("④ 云端处理中(每 5s 轮询)…");
|
|
7265
7357
|
const result = await pollTask(cfg, TASK_TYPE, taskId, (status, progress) => {
|
|
@@ -7284,13 +7376,241 @@ async function runOralCut(input, opts) {
|
|
|
7284
7376
|
}
|
|
7285
7377
|
|
|
7286
7378
|
// src/commands/long2short.ts
|
|
7287
|
-
import { resolve as resolve6, join as
|
|
7288
|
-
import { mkdir as
|
|
7379
|
+
import { resolve as resolve6, join as join18, dirname as dirname6, basename as basename8, extname as extname5 } from "node:path";
|
|
7380
|
+
import { mkdir as mkdir7, writeFile as writeFile8 } from "node:fs/promises";
|
|
7289
7381
|
import { existsSync as existsSync16 } from "node:fs";
|
|
7290
7382
|
|
|
7383
|
+
// src/lib/clip-brief.ts
|
|
7384
|
+
var str = (v) => {
|
|
7385
|
+
const s = typeof v === "string" ? v.trim() : "";
|
|
7386
|
+
return s ? s : undefined;
|
|
7387
|
+
};
|
|
7388
|
+
var num = (v) => typeof v === "number" && Number.isFinite(v) ? v : undefined;
|
|
7389
|
+
var list = (v) => Array.isArray(v) ? v.map((x) => str(x)).filter((x) => !!x) : [];
|
|
7390
|
+
function fmtTime(ms) {
|
|
7391
|
+
const total = Math.max(0, Math.round((num(ms) ?? 0) / 1000));
|
|
7392
|
+
const s = total % 60;
|
|
7393
|
+
const m = Math.floor(total / 60) % 60;
|
|
7394
|
+
const h = Math.floor(total / 3600);
|
|
7395
|
+
const mm = h ? String(m).padStart(2, "0") : String(m);
|
|
7396
|
+
return `${h ? `${h}:` : ""}${mm}:${String(s).padStart(2, "0")}`;
|
|
7397
|
+
}
|
|
7398
|
+
var baseName = (p) => p.split(/[\\/]/).pop() || p;
|
|
7399
|
+
var cell = (s) => s.replace(/\|/g, "\\|").replace(/\s*\n\s*/g, " ");
|
|
7400
|
+
function jumpcutText(note) {
|
|
7401
|
+
if (note === "jumpcut_off")
|
|
7402
|
+
return "未启用跳剪(`--no-jump-cut`),片段按原样保留。";
|
|
7403
|
+
if (note === "too_short")
|
|
7404
|
+
return "片段过短,跳剪跳过(保留全部内容)。";
|
|
7405
|
+
if (note === "degraded")
|
|
7406
|
+
return "跳剪降级:本条未压缩,保留全部内容。";
|
|
7407
|
+
return note;
|
|
7408
|
+
}
|
|
7409
|
+
function clipDurationMs(clip) {
|
|
7410
|
+
return num(clip.total_duration_ms) ?? num(clip.duration_ms);
|
|
7411
|
+
}
|
|
7412
|
+
function clipSegmentCount(clip) {
|
|
7413
|
+
return num(clip.segment_count) ?? (Array.isArray(clip.segments) ? clip.segments.length : undefined);
|
|
7414
|
+
}
|
|
7415
|
+
function sourceSpan(clip) {
|
|
7416
|
+
const segs = Array.isArray(clip.segments) ? clip.segments : [];
|
|
7417
|
+
if (segs.length) {
|
|
7418
|
+
const b2 = num(segs[0]?.begin_time);
|
|
7419
|
+
const e2 = num(segs[segs.length - 1]?.end_time);
|
|
7420
|
+
if (b2 != null && e2 != null)
|
|
7421
|
+
return [b2, e2];
|
|
7422
|
+
}
|
|
7423
|
+
const b = num(clip.begin_time);
|
|
7424
|
+
const e = num(clip.end_time);
|
|
7425
|
+
return b != null && e != null ? [b, e] : undefined;
|
|
7426
|
+
}
|
|
7427
|
+
function renderClipBrief(clip, index, files = {}) {
|
|
7428
|
+
const title = str(clip.title);
|
|
7429
|
+
const out = [`# clip${index}${title ? `「${title}」` : ""}`, ""];
|
|
7430
|
+
const bits = [];
|
|
7431
|
+
const dur = clipDurationMs(clip);
|
|
7432
|
+
if (dur != null)
|
|
7433
|
+
bits.push(`时长 ${fmtTime(dur)}`);
|
|
7434
|
+
const segCount = clipSegmentCount(clip);
|
|
7435
|
+
if (segCount != null)
|
|
7436
|
+
bits.push(`${segCount} 个保留片段`);
|
|
7437
|
+
const span = sourceSpan(clip);
|
|
7438
|
+
if (span)
|
|
7439
|
+
bits.push(`源片 ${fmtTime(span[0])}–${fmtTime(span[1])}`);
|
|
7440
|
+
if (bits.length)
|
|
7441
|
+
out.push(`- ${bits.join(" · ")}`, "");
|
|
7442
|
+
const score = num(clip.score);
|
|
7443
|
+
const reason = str(clip.score_reason);
|
|
7444
|
+
if (score != null || reason) {
|
|
7445
|
+
out.push("## 入选理由", "");
|
|
7446
|
+
out.push([score != null ? `**评分 ${score}**` : null, reason].filter(Boolean).join(" — "), "");
|
|
7447
|
+
}
|
|
7448
|
+
const summary = str(clip.summary);
|
|
7449
|
+
if (summary)
|
|
7450
|
+
out.push("## 简介", "", summary, "");
|
|
7451
|
+
const note = str(clip.jumpcut_note);
|
|
7452
|
+
if (note)
|
|
7453
|
+
out.push("## 跳剪", "", jumpcutText(note), "");
|
|
7454
|
+
const cats = [
|
|
7455
|
+
["主题", list(clip.themes)],
|
|
7456
|
+
["标签", list(clip.tags)],
|
|
7457
|
+
["类型", list(clip.genres)],
|
|
7458
|
+
["调性", list(clip.moods)]
|
|
7459
|
+
];
|
|
7460
|
+
const shown = cats.filter(([, v]) => v.length);
|
|
7461
|
+
if (shown.length) {
|
|
7462
|
+
out.push("## 分类与调性", "");
|
|
7463
|
+
for (const [k, v] of shown)
|
|
7464
|
+
out.push(`- ${k}:${v.join("、")}`);
|
|
7465
|
+
out.push("");
|
|
7466
|
+
}
|
|
7467
|
+
const hl = Array.isArray(clip.highlight_words) ? clip.highlight_words : [];
|
|
7468
|
+
const hlRows = hl.map((h) => ({ text: str(h?.text), at: num(h?.begin_time) })).filter((h) => h.text);
|
|
7469
|
+
if (hlRows.length) {
|
|
7470
|
+
out.push("## 高光词(源片时码)", "");
|
|
7471
|
+
for (const h of hlRows)
|
|
7472
|
+
out.push(`- ${h.at != null ? `${fmtTime(h.at)} ` : ""}「${h.text}」`);
|
|
7473
|
+
out.push("");
|
|
7474
|
+
}
|
|
7475
|
+
const formats = Object.keys(files).filter((f) => files[f]?.length);
|
|
7476
|
+
if (formats.length) {
|
|
7477
|
+
out.push("## 本条工程文件", "");
|
|
7478
|
+
for (const f of formats)
|
|
7479
|
+
out.push(`- ${f}:${files[f].map(baseName).join("、")}`);
|
|
7480
|
+
out.push("");
|
|
7481
|
+
}
|
|
7482
|
+
return `${out.join(`
|
|
7483
|
+
`).replace(/\n{3,}/g, `
|
|
7484
|
+
|
|
7485
|
+
`).trimEnd()}
|
|
7486
|
+
`;
|
|
7487
|
+
}
|
|
7488
|
+
function renderClipsOverview(clips, ctx) {
|
|
7489
|
+
const name = ctx.source.split(/[\\/]/).pop() || ctx.source;
|
|
7490
|
+
const out = [`# ${name} · 长剪短总览`, ""];
|
|
7491
|
+
out.push(`- 源片:\`${ctx.source}\``);
|
|
7492
|
+
out.push(`- 切片:${clips.length} 条`);
|
|
7493
|
+
out.push(`- 跳剪:${ctx.jumpCut ? "开" : "关"}`);
|
|
7494
|
+
if (ctx.splitMaterials?.total)
|
|
7495
|
+
out.push(`- 分屏素材:${ctx.splitMaterials.landed}/${ctx.splitMaterials.total} 条已落地`);
|
|
7496
|
+
if (ctx.taskId)
|
|
7497
|
+
out.push(`- task_id:\`${ctx.taskId}\``);
|
|
7498
|
+
out.push("");
|
|
7499
|
+
out.push("| # | 标题 | 时长 | 评分 | 简介 |", "| --- | --- | --- | --- | --- |");
|
|
7500
|
+
for (const [i, clip] of clips.entries()) {
|
|
7501
|
+
const dur = clipDurationMs(clip);
|
|
7502
|
+
const score = num(clip.score);
|
|
7503
|
+
out.push(`| clip${i} | ${cell(str(clip.title) ?? "—")} | ${dur != null ? fmtTime(dur) : "—"} | ${score ?? "—"} | ${cell(str(clip.summary) ?? "—")} |`);
|
|
7504
|
+
}
|
|
7505
|
+
out.push("", "> 逐条入选理由、跳剪说明、高光词见各 `clip{i}/clip.md`。");
|
|
7506
|
+
return `${out.join(`
|
|
7507
|
+
`)}
|
|
7508
|
+
`;
|
|
7509
|
+
}
|
|
7510
|
+
var POLISH_LABEL = {
|
|
7511
|
+
split_screen: "智能分屏",
|
|
7512
|
+
camera: "克制运镜",
|
|
7513
|
+
speed: "整体调速",
|
|
7514
|
+
seam: "接缝过渡",
|
|
7515
|
+
subtitle: "智能字幕",
|
|
7516
|
+
subtitle_remap: "字幕时轴重映射",
|
|
7517
|
+
purify_source: "去除原字幕",
|
|
7518
|
+
unknown: "未具名润色项"
|
|
7519
|
+
};
|
|
7520
|
+
function renderProReport(clips, report, ctx) {
|
|
7521
|
+
const name = ctx.source.split(/[\/]/).pop() || ctx.source;
|
|
7522
|
+
const out = [`# ${name} · 长剪短精剪报告`, ""];
|
|
7523
|
+
out.push(`- 源片:\`${ctx.source}\``);
|
|
7524
|
+
out.push(`- 成片:${clips.length} 条`);
|
|
7525
|
+
const jc = report?.jump_cut;
|
|
7526
|
+
if (typeof jc === "boolean")
|
|
7527
|
+
out.push(`- 跳剪:${jc ? "开" : "关"}`);
|
|
7528
|
+
if (ctx.taskId)
|
|
7529
|
+
out.push(`- task_id:\`${ctx.taskId}\``);
|
|
7530
|
+
out.push("");
|
|
7531
|
+
out.push("| # | 标题 | 时长 | 评分 | 简介 |", "| --- | --- | --- | --- | --- |");
|
|
7532
|
+
for (const [i, clip] of clips.entries()) {
|
|
7533
|
+
const dur = clipDurationMs(clip);
|
|
7534
|
+
const score = num(clip.score);
|
|
7535
|
+
out.push(`| clip${i} | ${cell(str(clip.title) ?? "—")} | ${dur != null ? fmtTime(dur) : "—"} | ${score ?? "—"} | ${cell(str(clip.summary) ?? "—")} |`);
|
|
7536
|
+
}
|
|
7537
|
+
out.push("");
|
|
7538
|
+
const degraded = report?.degraded_items;
|
|
7539
|
+
const degRows = [];
|
|
7540
|
+
if (degraded && typeof degraded === "object" && !Array.isArray(degraded)) {
|
|
7541
|
+
for (const [k, v] of Object.entries(degraded)) {
|
|
7542
|
+
const names = list(v).map((x) => POLISH_LABEL[x] ?? x);
|
|
7543
|
+
if (names.length)
|
|
7544
|
+
degRows.push(`- **${k}**:${names.join("、")}`);
|
|
7545
|
+
}
|
|
7546
|
+
}
|
|
7547
|
+
if (degRows.length) {
|
|
7548
|
+
out.push("## ⚠️ 润色降级", "");
|
|
7549
|
+
out.push("以下片子照常出片,但列出的润色项**没有做成**——效果与预期不同,别当成完整成片直接发布。", "");
|
|
7550
|
+
out.push(...degRows, "");
|
|
7551
|
+
}
|
|
7552
|
+
for (const [i, clip] of clips.entries()) {
|
|
7553
|
+
const title = str(clip.title);
|
|
7554
|
+
out.push("---", "", `## clip${i}${title ? `「${title}」` : ""}`, "");
|
|
7555
|
+
const bits = [];
|
|
7556
|
+
const dur = clipDurationMs(clip);
|
|
7557
|
+
if (dur != null)
|
|
7558
|
+
bits.push(`时长 ${fmtTime(dur)}`);
|
|
7559
|
+
const segCount = clipSegmentCount(clip);
|
|
7560
|
+
if (segCount != null)
|
|
7561
|
+
bits.push(`${segCount} 个保留片段`);
|
|
7562
|
+
const span = sourceSpan(clip);
|
|
7563
|
+
if (span)
|
|
7564
|
+
bits.push(`源片 ${fmtTime(span[0])}–${fmtTime(span[1])}`);
|
|
7565
|
+
const f = ctx.clipFiles?.[i];
|
|
7566
|
+
if (f)
|
|
7567
|
+
bits.push(`成片 ${baseName(f)}`);
|
|
7568
|
+
if (bits.length)
|
|
7569
|
+
out.push(`- ${bits.join(" · ")}`, "");
|
|
7570
|
+
const score = num(clip.score);
|
|
7571
|
+
const reason = str(clip.score_reason);
|
|
7572
|
+
if (score != null || reason) {
|
|
7573
|
+
out.push("### 入选理由", "");
|
|
7574
|
+
out.push([score != null ? `**评分 ${score}**` : null, reason].filter(Boolean).join(" — "), "");
|
|
7575
|
+
}
|
|
7576
|
+
const summary = str(clip.summary);
|
|
7577
|
+
if (summary)
|
|
7578
|
+
out.push("### 简介", "", summary, "");
|
|
7579
|
+
const note = str(clip.jumpcut_note);
|
|
7580
|
+
if (note)
|
|
7581
|
+
out.push("### 跳剪", "", jumpcutText(note), "");
|
|
7582
|
+
const cats = [
|
|
7583
|
+
["主题", list(clip.themes)],
|
|
7584
|
+
["标签", list(clip.tags)],
|
|
7585
|
+
["类型", list(clip.genres)],
|
|
7586
|
+
["调性", list(clip.moods)]
|
|
7587
|
+
];
|
|
7588
|
+
const shown = cats.filter(([, v]) => v.length);
|
|
7589
|
+
if (shown.length) {
|
|
7590
|
+
out.push("### 分类与调性", "");
|
|
7591
|
+
for (const [k, v] of shown)
|
|
7592
|
+
out.push(`- ${k}:${v.join("、")}`);
|
|
7593
|
+
out.push("");
|
|
7594
|
+
}
|
|
7595
|
+
const hl = Array.isArray(clip.highlight_words) ? clip.highlight_words : [];
|
|
7596
|
+
const hlRows = hl.map((h) => ({ text: str(h?.text), at: num(h?.begin_time) })).filter((h) => h.text);
|
|
7597
|
+
if (hlRows.length) {
|
|
7598
|
+
out.push("### 高光词(源片时码)", "");
|
|
7599
|
+
for (const h of hlRows)
|
|
7600
|
+
out.push(`- ${h.at != null ? `${fmtTime(h.at)} ` : ""}「${h.text}」`);
|
|
7601
|
+
out.push("");
|
|
7602
|
+
}
|
|
7603
|
+
}
|
|
7604
|
+
return `${out.join(`
|
|
7605
|
+
`).replace(/\n{3,}/g, `
|
|
7606
|
+
|
|
7607
|
+
`).trimEnd()}
|
|
7608
|
+
`;
|
|
7609
|
+
}
|
|
7610
|
+
|
|
7291
7611
|
// src/lib/tool-runner.ts
|
|
7292
|
-
import { resolve as resolve5, join as
|
|
7293
|
-
import { mkdir as
|
|
7612
|
+
import { resolve as resolve5, join as join17, dirname as dirname5, basename as basename7, extname as extname4 } from "node:path";
|
|
7613
|
+
import { mkdir as mkdir6, writeFile as writeFile7, stat as stat5 } from "node:fs/promises";
|
|
7294
7614
|
import { createWriteStream, existsSync as existsSync15 } from "node:fs";
|
|
7295
7615
|
import { Readable as Readable2 } from "node:stream";
|
|
7296
7616
|
import { pipeline } from "node:stream/promises";
|
|
@@ -7376,11 +7696,13 @@ async function resolveToolPricing(priceKey, pricingContext, fetchFn = fetch) {
|
|
|
7376
7696
|
|
|
7377
7697
|
// src/lib/tool-descriptors.ts
|
|
7378
7698
|
import { readFileSync as readFileSync4 } from "node:fs";
|
|
7379
|
-
import {
|
|
7699
|
+
import { readFile as readFile5, writeFile as writeFile6 } from "node:fs/promises";
|
|
7700
|
+
import { dirname as dirname4, extname as extname3, join as join16, resolve as resolvePath } from "node:path";
|
|
7380
7701
|
var MULTI_INPUT_KINDS = new Set(["images", "videos"]);
|
|
7381
7702
|
var IMAGE_EXTS = [".jpg", ".jpeg", ".png", ".webp", ".bmp", ".gif", ".tif", ".tiff", ".heic", ".heif", ".avif"];
|
|
7382
7703
|
var VIDEO_EXTS = [".mp4", ".mov", ".mkv", ".webm", ".avi", ".m4v", ".flv", ".wmv", ".mpg", ".mpeg", ".ts", ".m2ts"];
|
|
7383
7704
|
var AUDIO_EXTS = [".mp3", ".wav", ".m4a", ".aac", ".flac", ".ogg", ".opus", ".wma"];
|
|
7705
|
+
var isAudioFile = (p) => AUDIO_EXTS.includes(extname3(p).toLowerCase());
|
|
7384
7706
|
var PUBLIC_VIDEO_EXTS = [
|
|
7385
7707
|
".mp4",
|
|
7386
7708
|
".avi",
|
|
@@ -8324,18 +8646,37 @@ var videoAiSubtitle = {
|
|
|
8324
8646
|
{ flag: "--subtitle-type <style>", desc: `字幕样式:${AI_SUBTITLE_TYPES.join("/")}(未传则用服务端默认)` },
|
|
8325
8647
|
{ flag: "--subtitle-color <color>", desc: `字幕颜色:${AI_SUBTITLE_COLORS.join("/")}(未传则用服务端默认)` }
|
|
8326
8648
|
],
|
|
8649
|
+
async preprocess(ctx) {
|
|
8650
|
+
const inputAbs = ctx.inputAbs;
|
|
8651
|
+
if (ctx.opts.needPure === true) {
|
|
8652
|
+
ctx.warn("--need-pure 需要画面(去原字幕是画面操作),本次整片上传;只要字幕文件时去掉该 flag 即可只传音频");
|
|
8653
|
+
return inputAbs;
|
|
8654
|
+
}
|
|
8655
|
+
if (isAudioFile(inputAbs))
|
|
8656
|
+
return inputAbs;
|
|
8657
|
+
const audio = await extractAudio(inputAbs, ctx.ffmpegPath);
|
|
8658
|
+
return audio;
|
|
8659
|
+
},
|
|
8327
8660
|
buildPayload(fileId, ctx) {
|
|
8328
8661
|
const language = ctx.opts.language == null ? "" : String(ctx.opts.language).trim();
|
|
8329
8662
|
if (!language)
|
|
8330
8663
|
throw new Error("--language 必填:请指定源语种代码(具体取值见云端 API 文档 / 服务端支持列表)");
|
|
8331
8664
|
const payload = { file_id: fileId, language };
|
|
8665
|
+
const inputAbs = ctx.inputAbs;
|
|
8666
|
+
if (inputAbs && !isAudioFile(inputAbs)) {
|
|
8667
|
+
try {
|
|
8668
|
+
const geo = probeGeometry(inputAbs, ctx.ffmpegPath);
|
|
8669
|
+
if (geo.width > 0 && geo.height > 0)
|
|
8670
|
+
payload.video_size = [geo.width, geo.height];
|
|
8671
|
+
} catch {
|
|
8672
|
+
ctx.warn("探原片几何失败,本次不回传 video_size(服务端将按 1920x1080 兜底出字幕)");
|
|
8673
|
+
}
|
|
8674
|
+
}
|
|
8332
8675
|
if (ctx.opts.translateLanguage != null) {
|
|
8333
8676
|
const t = String(ctx.opts.translateLanguage).trim();
|
|
8334
8677
|
if (t)
|
|
8335
8678
|
payload.translate_language = t;
|
|
8336
8679
|
}
|
|
8337
|
-
if (ctx.opts.needRender === true)
|
|
8338
|
-
payload.need_render = true;
|
|
8339
8680
|
if (ctx.opts.needPure === true)
|
|
8340
8681
|
payload.need_pure = true;
|
|
8341
8682
|
if (ctx.opts.subtitleType != null) {
|
|
@@ -8365,10 +8706,135 @@ var videoAiSubtitle = {
|
|
|
8365
8706
|
files.push({ url: pure, filename: `${ctx.baseName}-pure${extFromUrl(pure, ".mp4")}` });
|
|
8366
8707
|
return files;
|
|
8367
8708
|
},
|
|
8709
|
+
async postprocess(ctx, landed) {
|
|
8710
|
+
if (ctx.opts.needRender !== true)
|
|
8711
|
+
return;
|
|
8712
|
+
const inputAbs = ctx.inputAbs;
|
|
8713
|
+
if (isAudioFile(inputAbs)) {
|
|
8714
|
+
throw new Error("--need-render 需要视频输入:本次输入是音频文件,无画面可烧");
|
|
8715
|
+
}
|
|
8716
|
+
const ass = landed.find((p) => p.toLowerCase().endsWith(".ass"));
|
|
8717
|
+
if (!ass)
|
|
8718
|
+
throw new Error("未拉回 .ass 字幕文件,无法本地烧录");
|
|
8719
|
+
const fonts = assFontNames(await readFile5(ass, "utf8"));
|
|
8720
|
+
const missing = [];
|
|
8721
|
+
for (const f of fonts) {
|
|
8722
|
+
if (await probeFontAvailable(f, ctx.ffmpegPath) === false)
|
|
8723
|
+
missing.push(f);
|
|
8724
|
+
}
|
|
8725
|
+
if (missing.length) {
|
|
8726
|
+
throw new Error(`本机缺字幕模板所需字体:${missing.join("、")}。` + `装上后重跑即可(字幕文件已落地,无需重新提交任务)。CLI 不自带字体、也不用替代字体顶——` + `替代字体烧出来的成片观感与模板设计不符且难以察觉。`);
|
|
8727
|
+
}
|
|
8728
|
+
const out = join16(dirname4(ass), `${ctx.baseName}-subtitled.mp4`);
|
|
8729
|
+
await burnSubtitle(inputAbs, ass, out, { ffmpegPath: ctx.ffmpegPath });
|
|
8730
|
+
return [out];
|
|
8731
|
+
},
|
|
8368
8732
|
mapResult(out) {
|
|
8369
8733
|
return { summary: typeof out.summary === "string" ? out.summary : "", asr: out.asr ?? null };
|
|
8370
8734
|
}
|
|
8371
8735
|
};
|
|
8736
|
+
var PRO_DURATION_PREFS = ["auto", "short", "medium", "long"];
|
|
8737
|
+
var videoLong2ShortPro = {
|
|
8738
|
+
name: "video_long2short_pro",
|
|
8739
|
+
title: "长剪短·精剪(直接出片)",
|
|
8740
|
+
description: "长内容按语义抽多条高光短片并一键出成品:选段+跳剪+分屏内核,叠加模糊底画布适配/克制运镜/调速保音高/智能字幕。只出成片,不产工程文件——要可编辑工程请用 gtrk long2short(粗剪)。",
|
|
8741
|
+
kind: "cloud",
|
|
8742
|
+
input: { kind: "video", exts: PUBLIC_VIDEO_EXTS },
|
|
8743
|
+
priceKey: "video_long2short_pro",
|
|
8744
|
+
outputHint: "逐条高光成片 mp4 + 切片报告 clips.md",
|
|
8745
|
+
enabled: true,
|
|
8746
|
+
taskType: "video_long2short_pro",
|
|
8747
|
+
pollTimeoutMs: 4 * 60 * 60 * 1000,
|
|
8748
|
+
options: [
|
|
8749
|
+
{ flag: "--language <code>", desc: "源语种代码(精剪必填;取值以服务端支持列表为准)" },
|
|
8750
|
+
{ flag: "--output-language <code>", desc: "选段/文案的输出语种(未传则同源语种)" },
|
|
8751
|
+
{ flag: "--main-topic <text>", desc: "主题引导(影响选段偏好)" },
|
|
8752
|
+
{ flag: "--output-size <s>", desc: "成片画幅 9:16|16:9|1:1 或自定义 WxH(未传则服务端默认)" },
|
|
8753
|
+
{ flag: "--no-jump-cut", desc: "关闭跳剪(默认开:片内去水词/冗余,只删不重排)" },
|
|
8754
|
+
{ flag: "--duration-pref <p>", desc: `成片时长偏好 ${PRO_DURATION_PREFS.join("/")}(成片条数由内容语义决定、不可指定)` },
|
|
8755
|
+
{ flag: "--max-clip-sec <n>", desc: "单条成片时长安全上限(秒;未传则服务端默认)" },
|
|
8756
|
+
{ flag: "--split-screen", desc: "开启智能分屏(多人同框段合成分屏画面)" },
|
|
8757
|
+
{ flag: "--split-orientation <o>", desc: "分屏方向 auto|lr|tb(未传则服务端默认)" },
|
|
8758
|
+
{ flag: "--speed-factor <n>", desc: "整体调速倍率(保音高;未传则服务端默认)" },
|
|
8759
|
+
{ flag: "--no-camera-move", desc: "关闭克制运镜(默认开:亚像素推/拉)" },
|
|
8760
|
+
{ flag: "--no-subtitle", desc: "关闭智能字幕(默认开:成片内烧录字幕)" },
|
|
8761
|
+
{ flag: "--subtitle-translate-language <code>", desc: "字幕译文语种(未传则单语字幕)" }
|
|
8762
|
+
],
|
|
8763
|
+
buildPayload(fileId, ctx) {
|
|
8764
|
+
const o = ctx.opts;
|
|
8765
|
+
const language = o.language == null ? "" : String(o.language).trim();
|
|
8766
|
+
if (!language)
|
|
8767
|
+
throw new Error("--language 必填:请指定源语种代码(具体取值见云端 API 文档 / 服务端支持列表)");
|
|
8768
|
+
const p = { file_id: fileId, language };
|
|
8769
|
+
if (o.outputLanguage != null && String(o.outputLanguage).trim())
|
|
8770
|
+
p.output_language = String(o.outputLanguage).trim();
|
|
8771
|
+
if (o.mainTopic != null && String(o.mainTopic).trim())
|
|
8772
|
+
p.main_topic = String(o.mainTopic).trim();
|
|
8773
|
+
if (o.outputSize != null && String(o.outputSize).trim())
|
|
8774
|
+
p.output_size = String(o.outputSize).trim();
|
|
8775
|
+
if (o.jumpCut === false)
|
|
8776
|
+
p.jump_cut = false;
|
|
8777
|
+
const duration = {};
|
|
8778
|
+
if (o.durationPref != null && String(o.durationPref).trim())
|
|
8779
|
+
duration.pref = String(o.durationPref).trim();
|
|
8780
|
+
const mcs = parseCountFlag(o.maxClipSec, "--max-clip-sec");
|
|
8781
|
+
if (mcs != null)
|
|
8782
|
+
duration.max_clip_sec = mcs;
|
|
8783
|
+
if (Object.keys(duration).length)
|
|
8784
|
+
p.duration = duration;
|
|
8785
|
+
if (o.splitScreen === true) {
|
|
8786
|
+
const ss = { enable: true };
|
|
8787
|
+
if (o.splitOrientation != null && String(o.splitOrientation).trim())
|
|
8788
|
+
ss.orientation = String(o.splitOrientation).trim();
|
|
8789
|
+
p.split_screen = ss;
|
|
8790
|
+
}
|
|
8791
|
+
if (o.speedFactor != null) {
|
|
8792
|
+
const n = Number(o.speedFactor);
|
|
8793
|
+
if (!Number.isFinite(n))
|
|
8794
|
+
throw new Error(`--speed-factor 需要有限数值,拿到「${o.speedFactor}」`);
|
|
8795
|
+
p.speed = { factor: n };
|
|
8796
|
+
}
|
|
8797
|
+
if (o.cameraMove === false)
|
|
8798
|
+
p.camera_move = { enable: false };
|
|
8799
|
+
const sub = {};
|
|
8800
|
+
if (o.subtitle === false)
|
|
8801
|
+
sub.enable = false;
|
|
8802
|
+
if (o.subtitleTranslateLanguage != null && String(o.subtitleTranslateLanguage).trim()) {
|
|
8803
|
+
sub.translate_language = String(o.subtitleTranslateLanguage).trim();
|
|
8804
|
+
}
|
|
8805
|
+
if (Object.keys(sub).length)
|
|
8806
|
+
p.subtitle = sub;
|
|
8807
|
+
return p;
|
|
8808
|
+
},
|
|
8809
|
+
mapOutputs(out, _ctx) {
|
|
8810
|
+
const clips = Array.isArray(out.clips) ? out.clips : [];
|
|
8811
|
+
const files = [];
|
|
8812
|
+
for (const [i, clip] of clips.entries()) {
|
|
8813
|
+
const f = clip?.file;
|
|
8814
|
+
const url = typeof f?.download_url === "string" ? f.download_url : undefined;
|
|
8815
|
+
if (!url)
|
|
8816
|
+
continue;
|
|
8817
|
+
files.push({ url, filename: `clip${i}${extFromUrl(url, ".mp4")}` });
|
|
8818
|
+
}
|
|
8819
|
+
return files;
|
|
8820
|
+
},
|
|
8821
|
+
mapResult(out) {
|
|
8822
|
+
return { clips: out.clips ?? [], report: out.report ?? {} };
|
|
8823
|
+
},
|
|
8824
|
+
async postprocess(ctx, landed, out) {
|
|
8825
|
+
const clips = Array.isArray(out.clips) ? out.clips : [];
|
|
8826
|
+
if (!clips.length)
|
|
8827
|
+
return;
|
|
8828
|
+
const mp4s = landed.filter((p) => p.toLowerCase().endsWith(".mp4"));
|
|
8829
|
+
const byIndex = clips.map((_c, i) => mp4s.find((p) => new RegExp(`clip${i}.[a-z0-9]+$`, "i").test(p)));
|
|
8830
|
+
const dest = join16(dirname4(landed[0] ?? "."), "clips.md");
|
|
8831
|
+
await writeFile6(dest, renderProReport(clips, out.report ?? {}, {
|
|
8832
|
+
source: ctx.inputAbs ?? ctx.baseName,
|
|
8833
|
+
clipFiles: byIndex
|
|
8834
|
+
}), "utf8");
|
|
8835
|
+
return [dest];
|
|
8836
|
+
}
|
|
8837
|
+
};
|
|
8372
8838
|
function parseCountFlag(v, flag) {
|
|
8373
8839
|
if (v == null)
|
|
8374
8840
|
return;
|
|
@@ -8660,6 +9126,7 @@ var TOOL_REGISTRY = [
|
|
|
8660
9126
|
imageClassicTemplate,
|
|
8661
9127
|
imageVerticalStitch,
|
|
8662
9128
|
videoSplitScreen,
|
|
9129
|
+
videoLong2ShortPro,
|
|
8663
9130
|
audioTtsClone,
|
|
8664
9131
|
mad
|
|
8665
9132
|
];
|
|
@@ -8882,9 +9349,9 @@ function resolveOutDir(descriptor, inputAbs, out) {
|
|
|
8882
9349
|
return resolve5(out);
|
|
8883
9350
|
if (inputAbs) {
|
|
8884
9351
|
const base = basename7(inputAbs, extname4(inputAbs));
|
|
8885
|
-
return
|
|
9352
|
+
return join17(dirname5(inputAbs), `${base}-${descriptor.name}`);
|
|
8886
9353
|
}
|
|
8887
|
-
return
|
|
9354
|
+
return join17(process.cwd(), `${descriptor.name}-${timestamp2()}`);
|
|
8888
9355
|
}
|
|
8889
9356
|
async function safeFingerprint(inputAbs) {
|
|
8890
9357
|
try {
|
|
@@ -8902,7 +9369,7 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
|
|
|
8902
9369
|
const isMulti = MULTI_INPUT_KINDS.has(descriptor.input.kind);
|
|
8903
9370
|
const inputList = isMulti ? (Array.isArray(inputArg) ? inputArg : inputArg != null ? [inputArg] : []).map((p) => resolve5(p)) : undefined;
|
|
8904
9371
|
const inputAbs = isMulti ? inputList[0] : typeof inputArg === "string" ? resolve5(inputArg) : undefined;
|
|
8905
|
-
const
|
|
9372
|
+
const baseName2 = inputAbs ? basename7(inputAbs, extname4(inputAbs)) : descriptor.name;
|
|
8906
9373
|
if (isMulti) {
|
|
8907
9374
|
validateToolInputs(descriptor, inputList);
|
|
8908
9375
|
} else {
|
|
@@ -8914,7 +9381,7 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
|
|
|
8914
9381
|
const ctx = {
|
|
8915
9382
|
inputAbs,
|
|
8916
9383
|
...inputList ? { inputAbsList: inputList } : {},
|
|
8917
|
-
baseName,
|
|
9384
|
+
baseName: baseName2,
|
|
8918
9385
|
ffmpegPath: opts.ffmpegPath,
|
|
8919
9386
|
opts,
|
|
8920
9387
|
extraParams,
|
|
@@ -8971,9 +9438,9 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
|
|
|
8971
9438
|
taskId = submitted.taskId;
|
|
8972
9439
|
fileId = submitted.fileId;
|
|
8973
9440
|
}
|
|
8974
|
-
await
|
|
9441
|
+
await mkdir6(outDir, { recursive: true });
|
|
8975
9442
|
const fingerprint2 = inputList ? await Promise.all(inputList.map((p) => safeFingerprint(p))) : inputAbs ? await safeFingerprint(inputAbs) : undefined;
|
|
8976
|
-
await
|
|
9443
|
+
await writeFile7(join17(outDir, "task.json"), JSON.stringify({
|
|
8977
9444
|
tool: descriptor.name,
|
|
8978
9445
|
taskType,
|
|
8979
9446
|
taskId,
|
|
@@ -8995,7 +9462,7 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
|
|
|
8995
9462
|
const errors = {};
|
|
8996
9463
|
const items = descriptor.mapOutputs ? descriptor.mapOutputs(outputResult, ctx) : [];
|
|
8997
9464
|
for (const it of items) {
|
|
8998
|
-
const dest =
|
|
9465
|
+
const dest = join17(outDir, it.filename);
|
|
8999
9466
|
try {
|
|
9000
9467
|
await deps.downloadStream(it.url, dest);
|
|
9001
9468
|
files.push(dest);
|
|
@@ -9003,11 +9470,20 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
|
|
|
9003
9470
|
errors[it.filename] = e instanceof Error ? e.message : String(e);
|
|
9004
9471
|
}
|
|
9005
9472
|
}
|
|
9473
|
+
if (descriptor.postprocess) {
|
|
9474
|
+
try {
|
|
9475
|
+
const extra = await descriptor.postprocess(ctx, [...files], outputResult);
|
|
9476
|
+
if (Array.isArray(extra))
|
|
9477
|
+
files.push(...extra);
|
|
9478
|
+
} catch (e) {
|
|
9479
|
+
errors[`${descriptor.name}:postprocess`] = e instanceof Error ? e.message : String(e);
|
|
9480
|
+
}
|
|
9481
|
+
}
|
|
9006
9482
|
let resultFile;
|
|
9007
9483
|
const structured = descriptor.mapResult ? descriptor.mapResult(outputResult, ctx) : undefined;
|
|
9008
9484
|
if (structured != null) {
|
|
9009
|
-
resultFile =
|
|
9010
|
-
await
|
|
9485
|
+
resultFile = join17(outDir, "result-output.json");
|
|
9486
|
+
await writeFile7(resultFile, JSON.stringify(structured, null, 2));
|
|
9011
9487
|
}
|
|
9012
9488
|
if (items.length === 0 && resultFile == null) {
|
|
9013
9489
|
errors["output"] = "任务完成但未解析到任何产物(下载链接与结构化结果均为空,output_result 形态异常)";
|
|
@@ -9025,7 +9501,7 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
|
|
|
9025
9501
|
...resultFile ? { resultFile } : {},
|
|
9026
9502
|
...Object.keys(errors).length ? { errors } : {}
|
|
9027
9503
|
};
|
|
9028
|
-
await
|
|
9504
|
+
await writeFile7(join17(outDir, "result.json"), JSON.stringify({ ...result, finishedAt: new Date().toISOString() }, null, 2));
|
|
9029
9505
|
return result;
|
|
9030
9506
|
}
|
|
9031
9507
|
|
|
@@ -9077,6 +9553,34 @@ function buildLong2ShortPayload(fileId, opts, geo, inputAbs, formats, draftTarge
|
|
|
9077
9553
|
mergeParams(p, parseExtraParams2(opts.param ?? [], opts.paramsJson));
|
|
9078
9554
|
return p;
|
|
9079
9555
|
}
|
|
9556
|
+
async function copyClipDraftsToRoot(clipDirs, draftTarget, errors) {
|
|
9557
|
+
const paths = [];
|
|
9558
|
+
let complete = 0;
|
|
9559
|
+
let attempted = 0;
|
|
9560
|
+
for (const [i, dir] of clipDirs.entries()) {
|
|
9561
|
+
const src = join18(dir, "jianying");
|
|
9562
|
+
if (!existsSync16(src)) {
|
|
9563
|
+
paths.push(null);
|
|
9564
|
+
continue;
|
|
9565
|
+
}
|
|
9566
|
+
attempted++;
|
|
9567
|
+
const dest = `${draftTarget}_clip${i}`;
|
|
9568
|
+
try {
|
|
9569
|
+
const landing = await copyJianyingDraft(src, dest);
|
|
9570
|
+
if (landing.complete) {
|
|
9571
|
+
complete++;
|
|
9572
|
+
paths.push(dest);
|
|
9573
|
+
} else {
|
|
9574
|
+
paths.push(null);
|
|
9575
|
+
errors[`clip${i}:jianying:draft`] = `草稿两件套不全(缺 ${landing.missing.join("、")}),剪映列表里不会显示:${dest}`;
|
|
9576
|
+
}
|
|
9577
|
+
} catch (e) {
|
|
9578
|
+
paths.push(null);
|
|
9579
|
+
errors[`clip${i}:jianying:draft`] = e instanceof Error ? e.message : String(e);
|
|
9580
|
+
}
|
|
9581
|
+
}
|
|
9582
|
+
return { paths, complete, attempted };
|
|
9583
|
+
}
|
|
9080
9584
|
function registerLong2Short(program2) {
|
|
9081
9585
|
program2.command("long2short <input>").description("长剪短闭环:本地抽音频/720p 代理 → 只传抽出物 → 云端选段跳剪(可选分屏)→ 逐 clip 拉回 gtrk/剪映/PR 三方工程").option("--language <code>", "源语种(必填,如 zh-CN;取值以服务端支持列表为准)").option("--split-screen", "开启智能分屏:本地改传 720p 代理,云端在代理上检测多人同框并烤 720p 分屏素材(素材落毛片旁 split_screen/)").option("--split-orientation <o>", "分屏方向 auto|lr|tb(缺省服务端 auto=按内容随机)").option("--main-topic <text>", "主题引导(影响选段偏好)").option("--duration-pref <p>", "成片时长偏好(缺省服务端 auto;成片条数由内容语义决定、不可指定)").option("--max-clip-sec <n>", "单条成片时长安全上限(秒;缺省服务端默认)").option("--no-jump-cut", "关闭跳剪(默认开:片内去水词/冗余,只删不重排)").option("--output-size <s>", "输出画布 9:16|16:9|1:1 或自定义 WxH(缺省服务端 9:16)").option("-f, --formats <list>", "三方格式(逗号分隔,云端逐 clip 直产)", "gtrk,jianying,xml").option("--jianying-draft-dir <dir>", "剪映草稿根目录;传路径或 auto(默认读 gtrk init 配置 / 自动探测)").option("-o, --out <dir>", "产物根目录(缺省 = <毛片同目录>/<毛片名>-long2short)").option("--ffmpeg-path <dir>", "指定 ffmpeg/ffprobe 所在目录(缺省 ~/.gitruck/ffmpeg → 系统 PATH)").option("--param <k=v>", "透传任意云端参数(标量、可重复)", collectParam2, []).option("--params-json <json>", `透传任意云端参数(JSON 对象、支持嵌套;如 '{"split_screen":{"prefer_mode":"..."}}')`).option("--reupload", "强制重新上传,忽略本地上传缓存").option("--no-open", "完成后不自动打开产物根目录(默认会自动打开一次)").option("--json", "机读模式:人读日志转 stderr,stdout 只输出根级结果 JSON").action(async (input, opts) => {
|
|
9082
9586
|
await runLong2Short(input, opts);
|
|
@@ -9094,7 +9598,7 @@ async function runLong2Short(input, opts) {
|
|
|
9094
9598
|
if (!formats.includes("gtrk"))
|
|
9095
9599
|
formats.push("gtrk");
|
|
9096
9600
|
const wantJianying = formats.some((f) => f === "jianying" || f === "capcut");
|
|
9097
|
-
const outDir = resolve6(opts.out ??
|
|
9601
|
+
const outDir = resolve6(opts.out ?? join18(dirname6(inputAbs), `${projName}-long2short`));
|
|
9098
9602
|
const outName = basename8(outDir);
|
|
9099
9603
|
let draftRoot;
|
|
9100
9604
|
if (wantJianying) {
|
|
@@ -9104,7 +9608,7 @@ async function runLong2Short(input, opts) {
|
|
|
9104
9608
|
else
|
|
9105
9609
|
log.warn("没找到剪映草稿目录 → 将只产 draft_content.json、缺 meta。可加 --jianying-draft-dir <你的草稿目录> 重跑。");
|
|
9106
9610
|
}
|
|
9107
|
-
const draftTarget = draftRoot ?
|
|
9611
|
+
const draftTarget = draftRoot ? join18(draftRoot, outName) : undefined;
|
|
9108
9612
|
log.step(`▶ 长剪短:${basename8(inputAbs)}(${opts.splitScreen ? "720p 代理 · 智能分屏" : "纯选段 · 音频上传"},格式 ${formats.join("/")})`);
|
|
9109
9613
|
log.step("① 本地预处理(探几何 + 抽音频/720p 代理)…");
|
|
9110
9614
|
const geo = probeGeometry(inputAbs, opts.ffmpegPath);
|
|
@@ -9124,8 +9628,8 @@ async function runLong2Short(input, opts) {
|
|
|
9124
9628
|
});
|
|
9125
9629
|
const { taskId, fileId } = { taskId: submitted.taskId, fileId: submitted.fileId };
|
|
9126
9630
|
log.info(`task_id = ${taskId}`);
|
|
9127
|
-
await
|
|
9128
|
-
await
|
|
9631
|
+
await mkdir7(outDir, { recursive: true });
|
|
9632
|
+
await writeFile8(join18(outDir, "task.json"), JSON.stringify({ taskId, taskType: TASK_TYPE2, fileId, source: inputAbs, formats, createdAt: new Date().toISOString() }, null, 2));
|
|
9129
9633
|
log.step("④ 云端处理中(选段/跳剪" + (opts.splitScreen ? "/分屏" : "") + ",每 5s 轮询)…");
|
|
9130
9634
|
const output = await pollToolTask(cfg, TASK_TYPE2, taskId, {
|
|
9131
9635
|
timeoutMs: POLL_TIMEOUT_MS,
|
|
@@ -9148,9 +9652,9 @@ async function runLong2Short(input, opts) {
|
|
|
9148
9652
|
continue;
|
|
9149
9653
|
}
|
|
9150
9654
|
if (!/^(?:[A-Za-z]:[\\/]|[\\/])/.test(dest))
|
|
9151
|
-
dest =
|
|
9655
|
+
dest = join18(dirname6(inputAbs), dest);
|
|
9152
9656
|
try {
|
|
9153
|
-
await
|
|
9657
|
+
await mkdir7(dirname6(dest), { recursive: true });
|
|
9154
9658
|
await download(url, dest);
|
|
9155
9659
|
splitLanded++;
|
|
9156
9660
|
} catch (e) {
|
|
@@ -9164,7 +9668,7 @@ async function runLong2Short(input, opts) {
|
|
|
9164
9668
|
log.step(`⑥ 逐 clip 拉回三方工程(共 ${clips.length} 条)…`);
|
|
9165
9669
|
const clipResults = [];
|
|
9166
9670
|
for (const [i, clip] of clips.entries()) {
|
|
9167
|
-
const clipDir =
|
|
9671
|
+
const clipDir = join18(outDir, `clip${i}`);
|
|
9168
9672
|
const { files: clipFiles, ...clipMeta } = clip;
|
|
9169
9673
|
try {
|
|
9170
9674
|
const r = await materializeResult({
|
|
@@ -9186,22 +9690,42 @@ async function runLong2Short(input, opts) {
|
|
|
9186
9690
|
log.warn(`clip${i} 落地失败(不连坐其余):${errors[`clip${i}`]}`);
|
|
9187
9691
|
}
|
|
9188
9692
|
}
|
|
9189
|
-
if (draftRoot) {
|
|
9190
|
-
|
|
9191
|
-
|
|
9192
|
-
|
|
9193
|
-
|
|
9194
|
-
|
|
9195
|
-
|
|
9196
|
-
|
|
9197
|
-
|
|
9198
|
-
}
|
|
9199
|
-
|
|
9200
|
-
}
|
|
9693
|
+
if (draftRoot && draftTarget) {
|
|
9694
|
+
const landing = await copyClipDraftsToRoot(clipResults.map((c3) => c3.dir), draftTarget, errors);
|
|
9695
|
+
landing.paths.forEach((p, i) => {
|
|
9696
|
+
clipResults[i].jianyingDraftPath = p;
|
|
9697
|
+
});
|
|
9698
|
+
const { complete, attempted } = landing;
|
|
9699
|
+
if (!attempted)
|
|
9700
|
+
log.warn("没有任何 clip 产出剪映草稿,草稿根未落地");
|
|
9701
|
+
else if (complete === attempted)
|
|
9702
|
+
log.info(`剪映草稿:<草稿根>/${outName}_clip{i} —— ${complete}/${attempted} 条两件套齐全(剪映里直接可见)`);
|
|
9703
|
+
else if (complete) {
|
|
9704
|
+
log.info(`剪映草稿:<草稿根>/${outName}_clip{i} —— ${complete}/${attempted} 条两件套齐全(剪映里可见)`);
|
|
9705
|
+
log.warn(`另有 ${attempted - complete} 条草稿两件套不全,剪映里不会显示(详见 result.json errors)`);
|
|
9706
|
+
} else
|
|
9707
|
+
log.warn(`${attempted} 条草稿两件套均不全,剪映里不会显示(详见 result.json errors)`);
|
|
9708
|
+
}
|
|
9709
|
+
for (const [i, cr] of clipResults.entries()) {
|
|
9710
|
+
try {
|
|
9711
|
+
await mkdir7(cr.dir, { recursive: true });
|
|
9712
|
+
await writeFile8(join18(cr.dir, "clip.md"), renderClipBrief(clips[i], i, cr.files));
|
|
9713
|
+
} catch (e) {
|
|
9714
|
+
errors[`clip${i}:brief`] = e instanceof Error ? e.message : String(e);
|
|
9201
9715
|
}
|
|
9202
|
-
log.info(`剪映草稿:<草稿根>/${outName}_clip{i}(剪映里直接可见)`);
|
|
9203
9716
|
}
|
|
9204
|
-
|
|
9717
|
+
try {
|
|
9718
|
+
await writeFile8(join18(outDir, "clips.md"), renderClipsOverview(clips, {
|
|
9719
|
+
source: inputAbs,
|
|
9720
|
+
jumpCut: opts.jumpCut !== false,
|
|
9721
|
+
splitMaterials: { landed: splitLanded, total: manifest.length },
|
|
9722
|
+
taskId
|
|
9723
|
+
}));
|
|
9724
|
+
log.info(`总览已生成:clips.md(逐条见 clip{i}/clip.md)`);
|
|
9725
|
+
} catch (e) {
|
|
9726
|
+
errors["clips.md"] = e instanceof Error ? e.message : String(e);
|
|
9727
|
+
}
|
|
9728
|
+
await writeFile8(join18(outDir, "report.json"), JSON.stringify(output.report ?? {}, null, 2));
|
|
9205
9729
|
const ok = Object.keys(errors).length === 0 && clipResults.some((c3) => c3.ok);
|
|
9206
9730
|
const rootResult = {
|
|
9207
9731
|
ok,
|
|
@@ -9212,10 +9736,10 @@ async function runLong2Short(input, opts) {
|
|
|
9212
9736
|
outDir,
|
|
9213
9737
|
clips: clipResults,
|
|
9214
9738
|
splitMaterials: { landed: splitLanded, total: manifest.length },
|
|
9215
|
-
reportFile:
|
|
9739
|
+
reportFile: join18(outDir, "report.json"),
|
|
9216
9740
|
...Object.keys(errors).length ? { errors } : {}
|
|
9217
9741
|
};
|
|
9218
|
-
await
|
|
9742
|
+
await writeFile8(join18(outDir, "result.json"), JSON.stringify({ ...rootResult, finishedAt: new Date().toISOString() }, null, 2));
|
|
9219
9743
|
if (opts.json)
|
|
9220
9744
|
console.log(JSON.stringify(rootResult));
|
|
9221
9745
|
if (opts.open) {
|
|
@@ -9231,7 +9755,7 @@ async function runLong2Short(input, opts) {
|
|
|
9231
9755
|
}
|
|
9232
9756
|
|
|
9233
9757
|
// src/commands/oralcut-result.ts
|
|
9234
|
-
import { resolve as resolve7, join as
|
|
9758
|
+
import { resolve as resolve7, join as join19 } from "node:path";
|
|
9235
9759
|
var TASK_TYPE3 = "cli/video_oral_cut_for_cli";
|
|
9236
9760
|
function timestamp3() {
|
|
9237
9761
|
const d = new Date;
|
|
@@ -9265,7 +9789,7 @@ async function runOralCutResult(taskId, opts) {
|
|
|
9265
9789
|
const pct = got.progress != null ? ` ${Math.round(got.progress)}%` : "";
|
|
9266
9790
|
throw new Error(`任务尚未完成(当前 ${got.status || "未知"}${pct}),暂无法取回结果;请稍后再试。`);
|
|
9267
9791
|
}
|
|
9268
|
-
const outDir = resolve7(opts.out ??
|
|
9792
|
+
const outDir = resolve7(opts.out ?? join19(process.cwd(), `${taskId}-video-project-${timestamp3()}`));
|
|
9269
9793
|
const draftDir = resolveJianyingDraftDir(opts.jianyingDraftDir);
|
|
9270
9794
|
await materializeResult({
|
|
9271
9795
|
outDir,
|
|
@@ -9325,7 +9849,7 @@ function registerUpgrade(program2) {
|
|
|
9325
9849
|
}
|
|
9326
9850
|
|
|
9327
9851
|
// src/commands/render.ts
|
|
9328
|
-
import { resolve as resolve8, dirname as
|
|
9852
|
+
import { resolve as resolve8, dirname as dirname7, join as join20, basename as basename9, extname as extname6 } from "node:path";
|
|
9329
9853
|
import { existsSync as existsSync17 } from "node:fs";
|
|
9330
9854
|
function registerRender(program2) {
|
|
9331
9855
|
program2.command("render <gtrk>").description("本地渲染:gtrk 工程按 EDL 用本地 ffmpeg 渲染成片 mp4(素材取原片本地路径)").option("-o, --out <file>", "输出 mp4 路径(缺省 = <gtrk 同目录>/<gtrk 名>.mp4)").option("--crf <n>", "视频质量 CRF 14-28(越小越清晰/文件越大,默认 18)").option("--codec <c>", "视频编码(默认 h264)").option("--ffmpeg-path <dir>", "指定 ffmpeg/ffprobe 所在目录(缺省 ~/.gitruck/ffmpeg → 系统)").option("--no-open", "完成后不自动打开产物目录").option("--json", "机读模式:人读日志转 stderr,stdout 只输出结果 JSON").action(async (gtrk, opts) => {
|
|
@@ -9334,7 +9858,7 @@ function registerRender(program2) {
|
|
|
9334
9858
|
const gtrkAbs = resolve8(gtrk);
|
|
9335
9859
|
if (!existsSync17(gtrkAbs))
|
|
9336
9860
|
throw new Error(`gtrk 工程不存在:${gtrkAbs}`);
|
|
9337
|
-
const outMp4 = resolve8(opts.out ??
|
|
9861
|
+
const outMp4 = resolve8(opts.out ?? join20(dirname7(gtrkAbs), `${basename9(gtrkAbs, extname6(gtrkAbs))}.mp4`));
|
|
9338
9862
|
log.step(`▶ 本地渲染:${basename9(gtrkAbs)} → ${basename9(outMp4)}`);
|
|
9339
9863
|
const project = await readGtrkFile(gtrkAbs);
|
|
9340
9864
|
const result = await renderGtrk(project, outMp4, {
|
|
@@ -9350,7 +9874,7 @@ function registerRender(program2) {
|
|
|
9350
9874
|
log.tickEnd();
|
|
9351
9875
|
log.ok(`渲染完成:${outMp4}(${result.duration.toFixed(1)}s)`);
|
|
9352
9876
|
if (opts.open)
|
|
9353
|
-
openFolder(
|
|
9877
|
+
openFolder(dirname7(outMp4));
|
|
9354
9878
|
if (opts.json) {
|
|
9355
9879
|
console.log(JSON.stringify({ ok: true, output: outMp4, duration: result.duration }));
|
|
9356
9880
|
}
|
|
@@ -9358,14 +9882,14 @@ function registerRender(program2) {
|
|
|
9358
9882
|
}
|
|
9359
9883
|
|
|
9360
9884
|
// src/commands/split.ts
|
|
9361
|
-
import { resolve as resolve9, join as
|
|
9885
|
+
import { resolve as resolve9, join as join22, dirname as dirname9, basename as basename11 } from "node:path";
|
|
9362
9886
|
import { existsSync as existsSync18 } from "node:fs";
|
|
9363
|
-
import { readFile as
|
|
9887
|
+
import { readFile as readFile6, writeFile as writeFile9, mkdir as mkdir8 } from "node:fs/promises";
|
|
9364
9888
|
import { createHash } from "node:crypto";
|
|
9365
9889
|
|
|
9366
9890
|
// src/lib/gtrk-writeback.ts
|
|
9367
9891
|
import { readFileSync as readFileSync5, statSync, writeFileSync as writeFileSync2, renameSync, unlinkSync } from "node:fs";
|
|
9368
|
-
import { dirname as
|
|
9892
|
+
import { dirname as dirname8, join as join21, basename as basename10 } from "node:path";
|
|
9369
9893
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
9370
9894
|
function readGtrk(path) {
|
|
9371
9895
|
const raw = readFileSync5(path, "utf8");
|
|
@@ -9392,7 +9916,7 @@ function writeStructMetaSplit(path, gtrk, splitObj, expectedMtimeMs) {
|
|
|
9392
9916
|
}
|
|
9393
9917
|
const nextStructMeta = { ...gtrk.struct_meta ?? {}, split: splitObj };
|
|
9394
9918
|
const next = { ...gtrk, struct_meta: nextStructMeta };
|
|
9395
|
-
const tmp =
|
|
9919
|
+
const tmp = join21(dirname8(path), `.${basename10(path)}.${randomBytes2(6).toString("hex")}.tmp`);
|
|
9396
9920
|
try {
|
|
9397
9921
|
writeFileSync2(tmp, JSON.stringify(next, null, 2));
|
|
9398
9922
|
renameSync(tmp, path);
|
|
@@ -9408,7 +9932,7 @@ function writeGtrkAtomic(path, next, expectedMtimeMs) {
|
|
|
9408
9932
|
if (cur !== expectedMtimeMs) {
|
|
9409
9933
|
throw new Error("工程文件在 matrix 运行期间被外部修改(保存冲突),已拒绝写入;请关闭客户端未保存的工程或重跑(plan 与已下载代理均保留)");
|
|
9410
9934
|
}
|
|
9411
|
-
const tmp =
|
|
9935
|
+
const tmp = join21(dirname8(path), `.${basename10(path)}.${randomBytes2(6).toString("hex")}.tmp`);
|
|
9412
9936
|
try {
|
|
9413
9937
|
writeFileSync2(tmp, JSON.stringify(next, null, 2));
|
|
9414
9938
|
renameSync(tmp, path);
|
|
@@ -9436,7 +9960,7 @@ function resolvePaths(opts) {
|
|
|
9436
9960
|
if (opts.gtrk) {
|
|
9437
9961
|
gtrkPath = resolve9(opts.gtrk);
|
|
9438
9962
|
} else if (project) {
|
|
9439
|
-
gtrkPath = firstExisting([
|
|
9963
|
+
gtrkPath = firstExisting([join22(project, "gtrk", "project.gtrk"), join22(project, "project.gtrk")]) ?? join22(project, "gtrk", "project.gtrk");
|
|
9440
9964
|
} else {
|
|
9441
9965
|
throw new Error("需 --project <目录> 或显式 --gtrk <path>");
|
|
9442
9966
|
}
|
|
@@ -9447,11 +9971,11 @@ function resolvePaths(opts) {
|
|
|
9447
9971
|
transcriptPath = resolve9(opts.transcript);
|
|
9448
9972
|
else if (project)
|
|
9449
9973
|
transcriptPath = firstExisting([
|
|
9450
|
-
|
|
9451
|
-
|
|
9452
|
-
|
|
9974
|
+
join22(project, "transcript", "transcript.json"),
|
|
9975
|
+
join22(project, "json", "transcript.json"),
|
|
9976
|
+
join22(project, "transcript.json")
|
|
9453
9977
|
]);
|
|
9454
|
-
const baseDir = project ??
|
|
9978
|
+
const baseDir = project ?? dirname9(gtrkPath);
|
|
9455
9979
|
return { baseDir, gtrkPath, transcriptPath };
|
|
9456
9980
|
}
|
|
9457
9981
|
function slugify2(name) {
|
|
@@ -9459,7 +9983,7 @@ function slugify2(name) {
|
|
|
9459
9983
|
return s || "project";
|
|
9460
9984
|
}
|
|
9461
9985
|
async function loadTranscript(path) {
|
|
9462
|
-
const t = JSON.parse(await
|
|
9986
|
+
const t = JSON.parse(await readFile6(path, "utf8"));
|
|
9463
9987
|
if (!t || !Array.isArray(t.utterances) || typeof t.material_id !== "string" || typeof t.text_hash !== "string") {
|
|
9464
9988
|
throw new Error(`transcript.json 结构异常(缺 utterances/material_id/text_hash):${path}`);
|
|
9465
9989
|
}
|
|
@@ -9480,10 +10004,10 @@ async function runView(baseDir, gtrkPath, transcriptPath, opts) {
|
|
|
9480
10004
|
const transcript = await loadTranscript(transcriptPath);
|
|
9481
10005
|
const { gtrk } = readGtrk(gtrkPath);
|
|
9482
10006
|
const view = projectTranscript(transcript, gtrk, { words: opts.words });
|
|
9483
|
-
const splitDir =
|
|
9484
|
-
await
|
|
9485
|
-
const viewPath =
|
|
9486
|
-
await
|
|
10007
|
+
const splitDir = join22(baseDir, "split");
|
|
10008
|
+
await mkdir8(splitDir, { recursive: true });
|
|
10009
|
+
const viewPath = join22(splitDir, "view.json");
|
|
10010
|
+
await writeFile9(viewPath, JSON.stringify(view, null, 2));
|
|
9487
10011
|
const dropped = view.utterances.filter((u) => u.dropped).length;
|
|
9488
10012
|
log.ok(`投影视图已生成:${viewPath}(${view.utterances.length} 条,其中 ${dropped} 条被剪)`);
|
|
9489
10013
|
const result = {
|
|
@@ -9505,7 +10029,7 @@ async function runLand(splitdocPath, baseDir, gtrkPath, transcriptPath, opts) {
|
|
|
9505
10029
|
if (!transcriptPath || !existsSync18(transcriptPath))
|
|
9506
10030
|
throw new Error(TRANSCRIPT_MISSING);
|
|
9507
10031
|
log.step("▶ 校验拆分稿并落地…");
|
|
9508
|
-
const doc = JSON.parse(await
|
|
10032
|
+
const doc = JSON.parse(await readFile6(splitdocPath, "utf8"));
|
|
9509
10033
|
const transcript = await loadTranscript(transcriptPath);
|
|
9510
10034
|
const { gtrk, mtimeMs } = readGtrk(gtrkPath);
|
|
9511
10035
|
assertGtrkV1(gtrk);
|
|
@@ -9539,14 +10063,14 @@ async function runLand(splitdocPath, baseDir, gtrkPath, transcriptPath, opts) {
|
|
|
9539
10063
|
}
|
|
9540
10064
|
});
|
|
9541
10065
|
writeStructMetaSplit(gtrkPath, gtrk, landing.split, mtimeMs);
|
|
9542
|
-
const splitDir =
|
|
9543
|
-
await
|
|
9544
|
-
const dispatchPath =
|
|
9545
|
-
await
|
|
10066
|
+
const splitDir = join22(baseDir, "split");
|
|
10067
|
+
await mkdir8(splitDir, { recursive: true });
|
|
10068
|
+
const dispatchPath = join22(splitDir, "dispatch.json");
|
|
10069
|
+
await writeFile9(dispatchPath, JSON.stringify(landing.dispatch, null, 2));
|
|
9546
10070
|
let mdPath = null;
|
|
9547
10071
|
if (opts.md) {
|
|
9548
|
-
mdPath =
|
|
9549
|
-
await
|
|
10072
|
+
mdPath = join22(splitDir, "visual-split.md");
|
|
10073
|
+
await writeFile9(mdPath, renderSplitMarkdown(doc, landing, { projectSlug, projectedAt }));
|
|
9550
10074
|
}
|
|
9551
10075
|
log.ok(`落地完成:${landing.split.beats.length}/${doc.beats.length} beat 落轨` + `(MG ${landing.dispatch.mg.length} · FILM_BROLL ${landing.dispatch.film_broll.length} · AI_DRAMA ${landing.dispatch.ai_drama.length})`);
|
|
9552
10076
|
for (const s of landing.skipped)
|
|
@@ -9577,9 +10101,9 @@ async function runLand(splitdocPath, baseDir, gtrkPath, transcriptPath, opts) {
|
|
|
9577
10101
|
}
|
|
9578
10102
|
|
|
9579
10103
|
// src/commands/matrix.ts
|
|
9580
|
-
import { resolve as resolve11, join as
|
|
10104
|
+
import { resolve as resolve11, join as join23, dirname as dirname10, basename as basename12 } from "node:path";
|
|
9581
10105
|
import { existsSync as existsSync20 } from "node:fs";
|
|
9582
|
-
import { readFile as
|
|
10106
|
+
import { readFile as readFile7, writeFile as writeFile10, mkdir as mkdir9, rename } from "node:fs/promises";
|
|
9583
10107
|
|
|
9584
10108
|
// src/lib/solid-png.ts
|
|
9585
10109
|
import { deflateSync } from "node:zlib";
|
|
@@ -10359,8 +10883,8 @@ function collectMaterialRefs(gtrk) {
|
|
|
10359
10883
|
const id = c3[key];
|
|
10360
10884
|
if (typeof id !== "string" || id === "")
|
|
10361
10885
|
continue;
|
|
10362
|
-
const
|
|
10363
|
-
|
|
10886
|
+
const list2 = out.get(id) ?? [];
|
|
10887
|
+
list2.push({
|
|
10364
10888
|
track: group,
|
|
10365
10889
|
track_index: trackIndex,
|
|
10366
10890
|
clip_id: typeof c3.clip_id === "string" ? c3.clip_id : null,
|
|
@@ -10368,7 +10892,7 @@ function collectMaterialRefs(gtrk) {
|
|
|
10368
10892
|
track_ed: clipTrackEd(c3),
|
|
10369
10893
|
key
|
|
10370
10894
|
});
|
|
10371
|
-
out.set(id,
|
|
10895
|
+
out.set(id, list2);
|
|
10372
10896
|
}
|
|
10373
10897
|
}
|
|
10374
10898
|
}
|
|
@@ -10410,15 +10934,15 @@ function checkMaterialIntegrity(opts) {
|
|
|
10410
10934
|
}
|
|
10411
10935
|
if (present)
|
|
10412
10936
|
continue;
|
|
10413
|
-
const
|
|
10937
|
+
const list2 = refs.get(id) ?? [];
|
|
10414
10938
|
const entry = {
|
|
10415
10939
|
id,
|
|
10416
10940
|
path,
|
|
10417
10941
|
kind,
|
|
10418
10942
|
resolved,
|
|
10419
|
-
referenced:
|
|
10420
|
-
refCount:
|
|
10421
|
-
refs:
|
|
10943
|
+
referenced: list2.length > 0,
|
|
10944
|
+
refCount: list2.length,
|
|
10945
|
+
refs: list2
|
|
10422
10946
|
};
|
|
10423
10947
|
(kind === "relative" ? dangling : external).push(entry);
|
|
10424
10948
|
}
|
|
@@ -10567,9 +11091,9 @@ function dedupeBeatQueries(queries) {
|
|
|
10567
11091
|
const best = bestByClip.get(r.clip_id);
|
|
10568
11092
|
if (best.qi === qi && best.r === r)
|
|
10569
11093
|
return true;
|
|
10570
|
-
const
|
|
10571
|
-
if (!
|
|
10572
|
-
|
|
11094
|
+
const list2 = best.r.also_matched_queries ??= [];
|
|
11095
|
+
if (!list2.includes(q.query))
|
|
11096
|
+
list2.push(q.query);
|
|
10573
11097
|
return false;
|
|
10574
11098
|
});
|
|
10575
11099
|
}
|
|
@@ -10719,16 +11243,16 @@ async function runPlanMode(cfg, tier, broll, overrides, columnId, opts) {
|
|
|
10719
11243
|
let baseDir;
|
|
10720
11244
|
if (opts.dispatch) {
|
|
10721
11245
|
dispatchPath = resolve11(opts.dispatch);
|
|
10722
|
-
baseDir =
|
|
11246
|
+
baseDir = dirname10(dirname10(dispatchPath));
|
|
10723
11247
|
} else if (opts.project) {
|
|
10724
11248
|
baseDir = resolve11(opts.project);
|
|
10725
|
-
dispatchPath =
|
|
11249
|
+
dispatchPath = join23(baseDir, "split", "dispatch.json");
|
|
10726
11250
|
} else {
|
|
10727
11251
|
throw new Error('需 --project <目录> 或显式 --dispatch <path>(ad-hoc 检索用:gtrk matrix search "<query>")');
|
|
10728
11252
|
}
|
|
10729
11253
|
if (!existsSync20(dispatchPath))
|
|
10730
11254
|
throw new Error(`找不到派单清单:${dispatchPath}(先跑 gtrk split <拆分稿> 落地派单)`);
|
|
10731
|
-
const dispatch = JSON.parse(await
|
|
11255
|
+
const dispatch = JSON.parse(await readFile7(dispatchPath, "utf8"));
|
|
10732
11256
|
const rawQueue = Array.isArray(dispatch.film_broll) ? dispatch.film_broll : [];
|
|
10733
11257
|
const earlyGtrkPath = locateGtrk(baseDir);
|
|
10734
11258
|
let earlyGtrk;
|
|
@@ -10791,10 +11315,10 @@ async function runPlanMode(cfg, tier, broll, overrides, columnId, opts) {
|
|
|
10791
11315
|
columnId,
|
|
10792
11316
|
beats
|
|
10793
11317
|
});
|
|
10794
|
-
const splitDir =
|
|
10795
|
-
await
|
|
10796
|
-
const planPath =
|
|
10797
|
-
await
|
|
11318
|
+
const splitDir = join23(baseDir, "split");
|
|
11319
|
+
await mkdir9(splitDir, { recursive: true });
|
|
11320
|
+
const planPath = join23(splitDir, "broll-plan.json");
|
|
11321
|
+
await writeFile10(planPath, JSON.stringify(plan, null, 2));
|
|
10798
11322
|
log.ok(`候选清单已生成:${planPath}(${beats.length} beat · ${okCount}/${totalQueries} query 成功 · ${resultCount} 条候选)`);
|
|
10799
11323
|
log.info("清单只含引用不含素材:cover_url 可直接预览;url 带签名默认 24h 过期,过期重跑本命令即重签。");
|
|
10800
11324
|
const layN = parseLay(opts.lay);
|
|
@@ -10841,13 +11365,13 @@ function parseScoreFloor(raw) {
|
|
|
10841
11365
|
return SCORE_FLOOR_DEFAULT;
|
|
10842
11366
|
}
|
|
10843
11367
|
function locateGtrk(baseDir) {
|
|
10844
|
-
const cands = [
|
|
11368
|
+
const cands = [join23(baseDir, "gtrk", "project.gtrk"), join23(baseDir, "project.gtrk")];
|
|
10845
11369
|
return cands.find((p) => existsSync20(p));
|
|
10846
11370
|
}
|
|
10847
11371
|
async function layIntoProject(baseDir, plan, layN, scoreFloor, blackBed, forceRelay, reproj) {
|
|
10848
11372
|
const gtrkPath = locateGtrk(baseDir);
|
|
10849
11373
|
if (!gtrkPath) {
|
|
10850
|
-
log.warn(`未找到工程文件(${
|
|
11374
|
+
log.warn(`未找到工程文件(${join23(baseDir, "gtrk", "project.gtrk")}),跳过铺轨——plan 已产出,可后续在有工程的目录重跑`);
|
|
10851
11375
|
return;
|
|
10852
11376
|
}
|
|
10853
11377
|
const { gtrk, mtimeMs } = readGtrk(gtrkPath);
|
|
@@ -10855,9 +11379,9 @@ async function layIntoProject(baseDir, plan, layN, scoreFloor, blackBed, forceRe
|
|
|
10855
11379
|
const { fills, clipIds } = planBeatFills(plan, layN, scoreFloor);
|
|
10856
11380
|
const slotCount = [...fills.values()].flat().reduce((n, s) => n + s.length, 0);
|
|
10857
11381
|
log.step(`▶ 候选铺轨(${layN} 轨 · 平铺 ${slotCount} 槽位 · ${clipIds.size} 个 clip,preview 代理)…`);
|
|
10858
|
-
const gtrkDir =
|
|
10859
|
-
const previewDir =
|
|
10860
|
-
await
|
|
11382
|
+
const gtrkDir = dirname10(gtrkPath);
|
|
11383
|
+
const previewDir = join23(gtrkDir, ...BROLL_PREVIEW_DIR.split("/"));
|
|
11384
|
+
await mkdir9(previewDir, { recursive: true });
|
|
10861
11385
|
const prevSource = new Map;
|
|
10862
11386
|
const prevBroll = gtrk.struct_meta?.broll;
|
|
10863
11387
|
for (const b of prevBroll?.beats ?? []) {
|
|
@@ -10879,7 +11403,7 @@ async function layIntoProject(baseDir, plan, layN, scoreFloor, blackBed, forceRe
|
|
|
10879
11403
|
if (!cand)
|
|
10880
11404
|
continue;
|
|
10881
11405
|
const rel = `${BROLL_PREVIEW_DIR}/${clipId}.mp4`;
|
|
10882
|
-
const abs =
|
|
11406
|
+
const abs = join23(gtrkDir, ...rel.split("/"));
|
|
10883
11407
|
if (existsSync20(abs)) {
|
|
10884
11408
|
const prev = prevSource.get(clipId);
|
|
10885
11409
|
if (prev !== "raw") {
|
|
@@ -10918,8 +11442,8 @@ async function layIntoProject(baseDir, plan, layN, scoreFloor, blackBed, forceRe
|
|
|
10918
11442
|
forceRelay
|
|
10919
11443
|
});
|
|
10920
11444
|
if (summary.refused) {
|
|
10921
|
-
const
|
|
10922
|
-
log.err(`拒绝铺轨:${
|
|
11445
|
+
const list2 = summary.keptEditedTracks;
|
|
11446
|
+
log.err(`拒绝铺轨:${list2.length} 条候选轨已被你在客户端编辑过(track_index ${list2.join("/") || "-"})——` + "本次不剥它们、也不铺新轨,工程文件零改动。");
|
|
10923
11447
|
for (const w of warnings)
|
|
10924
11448
|
log.warn(w);
|
|
10925
11449
|
log.warn("下一步二选一:① 在客户端处置那条轨(删掉 / 移走 / 改用别的轨)后重跑本命令;" + "② 确知要丢弃那条轨上的编辑 → 加 --force-relay 强制剥离重铺" + "(会删掉已确认原片的 broll-raw-* 素材登记,盘上原片文件成孤儿,不可恢复)。");
|
|
@@ -10927,7 +11451,7 @@ async function layIntoProject(baseDir, plan, layN, scoreFloor, blackBed, forceRe
|
|
|
10927
11451
|
return {
|
|
10928
11452
|
lay: {
|
|
10929
11453
|
refused: true,
|
|
10930
|
-
keptEditedTracks:
|
|
11454
|
+
keptEditedTracks: list2,
|
|
10931
11455
|
laidTracks: [],
|
|
10932
11456
|
laidClips: 0,
|
|
10933
11457
|
removedTracks: [],
|
|
@@ -10942,12 +11466,12 @@ async function layIntoProject(baseDir, plan, layN, scoreFloor, blackBed, forceRe
|
|
|
10942
11466
|
const canvas = gtrk.video_size;
|
|
10943
11467
|
const spec = { hex: BLACK_BED_HEX, width: canvas[0], height: canvas[1] };
|
|
10944
11468
|
const rel = solidRelPath(spec);
|
|
10945
|
-
const abs =
|
|
11469
|
+
const abs = join23(gtrkDir, ...rel.split("/"));
|
|
10946
11470
|
try {
|
|
10947
11471
|
if (!existsSync20(abs)) {
|
|
10948
|
-
await
|
|
11472
|
+
await mkdir9(dirname10(abs), { recursive: true });
|
|
10949
11473
|
const tmp = `${abs}.tmp-${process.pid}`;
|
|
10950
|
-
await
|
|
11474
|
+
await writeFile10(tmp, encodeSolidPng(spec));
|
|
10951
11475
|
await rename(tmp, abs);
|
|
10952
11476
|
}
|
|
10953
11477
|
} catch (e) {
|
|
@@ -11010,7 +11534,7 @@ async function downloadProxy(cand, absPath, opts = {}) {
|
|
|
11010
11534
|
if (previewUrl) {
|
|
11011
11535
|
const bytes = await tryFetch(previewUrl);
|
|
11012
11536
|
if (bytes) {
|
|
11013
|
-
await
|
|
11537
|
+
await writeFile10(absPath, bytes);
|
|
11014
11538
|
return "preview";
|
|
11015
11539
|
}
|
|
11016
11540
|
}
|
|
@@ -11018,7 +11542,7 @@ async function downloadProxy(cand, absPath, opts = {}) {
|
|
|
11018
11542
|
return null;
|
|
11019
11543
|
const raw = await tryFetch(cand.url);
|
|
11020
11544
|
if (raw) {
|
|
11021
|
-
await
|
|
11545
|
+
await writeFile10(absPath, raw);
|
|
11022
11546
|
log.warn(`clip ${cand.clip_id} 无 preview 代理,已回落原片(${(raw.length / 1048576).toFixed(1)}MB)`);
|
|
11023
11547
|
return "raw";
|
|
11024
11548
|
}
|
|
@@ -11041,7 +11565,7 @@ async function runAdhoc(query, cfg, tier, broll, overrides, columnId, opts) {
|
|
|
11041
11565
|
};
|
|
11042
11566
|
if (opts.out) {
|
|
11043
11567
|
const outPath = resolve11(opts.out);
|
|
11044
|
-
await
|
|
11568
|
+
await writeFile10(outPath, JSON.stringify({ query, recalled: data.recalled, results }, null, 2));
|
|
11045
11569
|
log.ok(`结果已落盘:${outPath}`);
|
|
11046
11570
|
result.outPath = outPath;
|
|
11047
11571
|
} else if (!opts.json) {
|
|
@@ -11060,9 +11584,9 @@ function slugify3(name) {
|
|
|
11060
11584
|
}
|
|
11061
11585
|
|
|
11062
11586
|
// src/commands/mg.ts
|
|
11063
|
-
import { resolve as resolve12, join as
|
|
11587
|
+
import { resolve as resolve12, join as join24, dirname as dirname11, basename as basename13 } from "node:path";
|
|
11064
11588
|
import { existsSync as existsSync21 } from "node:fs";
|
|
11065
|
-
import { readFile as
|
|
11589
|
+
import { readFile as readFile8, mkdir as mkdir10, copyFile } from "node:fs/promises";
|
|
11066
11590
|
|
|
11067
11591
|
// src/lib/mg-lint.ts
|
|
11068
11592
|
var CDN_OK = [/lib\.baomitu\.com/i, /cdnjs\.cloudflare\.com/i, /unpkg\.com/i];
|
|
@@ -11926,9 +12450,9 @@ function detectPrimitiveLoops(html) {
|
|
|
11926
12450
|
}
|
|
11927
12451
|
const callsByFunction = new Map;
|
|
11928
12452
|
const pushCall = (fn, at) => {
|
|
11929
|
-
const
|
|
11930
|
-
|
|
11931
|
-
callsByFunction.set(fn.declStart,
|
|
12453
|
+
const list2 = callsByFunction.get(fn.declStart) ?? [];
|
|
12454
|
+
list2.push({ at, loops: executionLoopsAt(at) });
|
|
12455
|
+
callsByFunction.set(fn.declStart, list2);
|
|
11932
12456
|
};
|
|
11933
12457
|
const functionNames = new Set(functions.map((fn) => fn.name).filter((name) => name !== null));
|
|
11934
12458
|
for (const call of code.matchAll(/\b([A-Za-z_$][\w$]*)\s*\(/g)) {
|
|
@@ -12129,21 +12653,27 @@ function filterDecls(styleDecls) {
|
|
|
12129
12653
|
out.push(m[1].trim());
|
|
12130
12654
|
return out;
|
|
12131
12655
|
}
|
|
12132
|
-
function
|
|
12656
|
+
function styleBlockRules(html) {
|
|
12133
12657
|
const out = [];
|
|
12134
12658
|
for (const sm of html.matchAll(/<style\b[^>]*>([\s\S]*?)<\/style\s*>/gi)) {
|
|
12135
12659
|
const css = sm[1].replace(/\/\*[\s\S]*?\*\//g, " ");
|
|
12136
12660
|
const stack = [];
|
|
12661
|
+
const selStack = [];
|
|
12662
|
+
let segStart = 0;
|
|
12137
12663
|
for (let i = 0;i < css.length; i++) {
|
|
12138
12664
|
const c3 = css[i];
|
|
12139
12665
|
if (c3 === '"' || c3 === "'") {
|
|
12140
12666
|
i = skipString(css, i);
|
|
12141
12667
|
continue;
|
|
12142
12668
|
}
|
|
12143
|
-
if (c3 === "{")
|
|
12669
|
+
if (c3 === "{") {
|
|
12144
12670
|
stack.push(i);
|
|
12145
|
-
|
|
12671
|
+
selStack.push(css.slice(segStart, i));
|
|
12672
|
+
segStart = i + 1;
|
|
12673
|
+
} else if (c3 === "}") {
|
|
12146
12674
|
const open2 = stack.pop();
|
|
12675
|
+
const sel = selStack.pop() ?? "";
|
|
12676
|
+
segStart = i + 1;
|
|
12147
12677
|
if (open2 === undefined)
|
|
12148
12678
|
continue;
|
|
12149
12679
|
let own = css.slice(open2 + 1, i);
|
|
@@ -12151,8 +12681,9 @@ function styleBlockOwnDecls(html) {
|
|
|
12151
12681
|
prev = own;
|
|
12152
12682
|
own = own.replace(/\{[^{}]*\}/g, " ");
|
|
12153
12683
|
}
|
|
12154
|
-
out.push(own);
|
|
12155
|
-
}
|
|
12684
|
+
out.push({ selector: sel.trim(), own });
|
|
12685
|
+
} else if (c3 === ";")
|
|
12686
|
+
segStart = i + 1;
|
|
12156
12687
|
}
|
|
12157
12688
|
}
|
|
12158
12689
|
return out;
|
|
@@ -12212,21 +12743,138 @@ function tweenVarObjects(js) {
|
|
|
12212
12743
|
}
|
|
12213
12744
|
return out;
|
|
12214
12745
|
}
|
|
12746
|
+
var GSAP_TRANSFORM_KEY = /^(?:transform|x|y|z|xPercent|yPercent|scale|scaleX|scaleY|scaleZ|rotate|rotation|rotationX|rotationY|rotationZ|skew|skewX|skewY|translateX|translateY|translateZ)$/;
|
|
12747
|
+
var TWEENING_METHOD = /^(?:to|from|fromTo)$/;
|
|
12748
|
+
function selectorSubjectKeys(selector) {
|
|
12749
|
+
const keys = [];
|
|
12750
|
+
for (const alt of selector.split(",")) {
|
|
12751
|
+
const subject = alt.trim().split(/[\s>+~]+/).filter(Boolean).pop();
|
|
12752
|
+
if (!subject)
|
|
12753
|
+
continue;
|
|
12754
|
+
const core = subject.split(/[:\[]/)[0];
|
|
12755
|
+
for (const m of core.matchAll(/([.#])([A-Za-z_][\w-]*)/g))
|
|
12756
|
+
keys.push(`${m[1] === "#" ? "id" : "class"}:${m[2]}`);
|
|
12757
|
+
}
|
|
12758
|
+
return keys;
|
|
12759
|
+
}
|
|
12760
|
+
function tagIdentityKeys(tag) {
|
|
12761
|
+
const keys = [];
|
|
12762
|
+
const id = attr(tag, "id");
|
|
12763
|
+
if (id)
|
|
12764
|
+
keys.push(`id:${id.trim()}`);
|
|
12765
|
+
for (const cls of (attr(tag, "class") ?? "").trim().split(/\s+/))
|
|
12766
|
+
if (cls)
|
|
12767
|
+
keys.push(`class:${cls}`);
|
|
12768
|
+
return keys;
|
|
12769
|
+
}
|
|
12770
|
+
function tweenCalls(js) {
|
|
12771
|
+
const out = [];
|
|
12772
|
+
const re = /\.\s*(to|from|fromTo|set)\s*\(/g;
|
|
12773
|
+
let m;
|
|
12774
|
+
while (m = re.exec(js)) {
|
|
12775
|
+
const open2 = m.index + m[0].length - 1;
|
|
12776
|
+
const args = parenArg(js, open2);
|
|
12777
|
+
re.lastIndex = open2 + 1;
|
|
12778
|
+
if (args === null)
|
|
12779
|
+
continue;
|
|
12780
|
+
const target = readObjValue(args, 0);
|
|
12781
|
+
const bodies = [];
|
|
12782
|
+
for (let i = 0;i < args.length; i++) {
|
|
12783
|
+
const c3 = args[i];
|
|
12784
|
+
if (c3 === '"' || c3 === "'" || c3 === "`") {
|
|
12785
|
+
i = skipString(args, i);
|
|
12786
|
+
continue;
|
|
12787
|
+
}
|
|
12788
|
+
if (c3 !== "{")
|
|
12789
|
+
continue;
|
|
12790
|
+
const body = braceBlock(args, i);
|
|
12791
|
+
bodies.push(body);
|
|
12792
|
+
i += body.length + 1;
|
|
12793
|
+
}
|
|
12794
|
+
out.push({ method: m[1], target, bodies });
|
|
12795
|
+
}
|
|
12796
|
+
return out;
|
|
12797
|
+
}
|
|
12798
|
+
function selectorBindings(js) {
|
|
12799
|
+
const out = new Map;
|
|
12800
|
+
const re = /([A-Za-z_$][\w$]*)\s*=\s*(?:[A-Za-z_$][\w$]*\s*\.\s*)?([A-Za-z_$][\w$]*)\s*\(\s*(["'])(.*?)\3\s*\)/g;
|
|
12801
|
+
for (const m of js.matchAll(re)) {
|
|
12802
|
+
const [, name, fn, , raw] = m;
|
|
12803
|
+
const keys = fn === "getElementById" ? [`id:${raw.trim()}`] : /^\s*[.#]/.test(raw) ? selectorSubjectKeys(raw) : [];
|
|
12804
|
+
if (!keys.length)
|
|
12805
|
+
continue;
|
|
12806
|
+
const prev = out.get(name) ?? [];
|
|
12807
|
+
out.set(name, [...new Set([...prev, ...keys])]);
|
|
12808
|
+
}
|
|
12809
|
+
return out;
|
|
12810
|
+
}
|
|
12811
|
+
function tweenTargetKeys(target, bindings) {
|
|
12812
|
+
const t = target.trim();
|
|
12813
|
+
const lit = stringLiteral(t);
|
|
12814
|
+
if (lit !== null)
|
|
12815
|
+
return /^\s*[.#]/.test(lit) ? selectorSubjectKeys(lit) : [];
|
|
12816
|
+
if (/^[A-Za-z_$][\w$]*$/.test(t))
|
|
12817
|
+
return bindings.get(t) ?? [];
|
|
12818
|
+
return [];
|
|
12819
|
+
}
|
|
12820
|
+
function topLevelObjKeys(body) {
|
|
12821
|
+
const keys = [];
|
|
12822
|
+
let depth = 0;
|
|
12823
|
+
let expectKey = true;
|
|
12824
|
+
for (let i = 0;i < body.length; i++) {
|
|
12825
|
+
const c3 = body[i];
|
|
12826
|
+
if (c3 === '"' || c3 === "'" || c3 === "`") {
|
|
12827
|
+
if (depth === 0 && expectKey) {
|
|
12828
|
+
const end = skipString(body, i);
|
|
12829
|
+
const name = body.slice(i + 1, end);
|
|
12830
|
+
const rest = body.slice(end + 1);
|
|
12831
|
+
if (/^\s*:/.test(rest)) {
|
|
12832
|
+
keys.push(name);
|
|
12833
|
+
expectKey = false;
|
|
12834
|
+
}
|
|
12835
|
+
}
|
|
12836
|
+
i = skipString(body, i);
|
|
12837
|
+
continue;
|
|
12838
|
+
}
|
|
12839
|
+
if (c3 === "(" || c3 === "[" || c3 === "{")
|
|
12840
|
+
depth++;
|
|
12841
|
+
else if (c3 === ")" || c3 === "]" || c3 === "}")
|
|
12842
|
+
depth = Math.max(0, depth - 1);
|
|
12843
|
+
else if (c3 === "," && depth === 0)
|
|
12844
|
+
expectKey = true;
|
|
12845
|
+
else if (depth === 0 && expectKey && /[A-Za-z_$]/.test(c3)) {
|
|
12846
|
+
const m = /^([A-Za-z_$][\w$]*)\s*:/.exec(body.slice(i));
|
|
12847
|
+
if (m) {
|
|
12848
|
+
keys.push(m[1]);
|
|
12849
|
+
expectKey = false;
|
|
12850
|
+
i += m[1].length;
|
|
12851
|
+
}
|
|
12852
|
+
}
|
|
12853
|
+
}
|
|
12854
|
+
return keys;
|
|
12855
|
+
}
|
|
12215
12856
|
function detectFilterCost(html) {
|
|
12216
12857
|
const src = maskHtmlComments(html);
|
|
12217
12858
|
const svgConvIds = svgConvolutionFilterIds(src);
|
|
12218
12859
|
const animated = [];
|
|
12219
12860
|
const staticFullBleed = [];
|
|
12861
|
+
const staticTransformed = [];
|
|
12220
12862
|
const indeterminate = [];
|
|
12221
12863
|
const add = (bucket, site) => {
|
|
12222
12864
|
const s = site.replace(/\s+/g, " ").trim().slice(0, 120);
|
|
12223
12865
|
if (!bucket.includes(s))
|
|
12224
12866
|
bucket.push(s);
|
|
12225
12867
|
};
|
|
12226
|
-
|
|
12868
|
+
const staticFilterSites = new Map;
|
|
12869
|
+
for (const { selector, own } of styleBlockRules(src)) {
|
|
12227
12870
|
const hit = filterDecls(own).filter((val) => filterValueConvolves(val, svgConvIds));
|
|
12228
|
-
if (hit.length
|
|
12871
|
+
if (!hit.length)
|
|
12872
|
+
continue;
|
|
12873
|
+
if (isFullBleedStyle(own))
|
|
12229
12874
|
add(staticFullBleed, `<style> 规则:filter:${hit[0]}`);
|
|
12875
|
+
for (const key of selectorSubjectKeys(selector))
|
|
12876
|
+
if (!staticFilterSites.has(key))
|
|
12877
|
+
staticFilterSites.set(key, `<style> 规则 ${selector.slice(0, 60)}:filter:${hit[0]}`);
|
|
12230
12878
|
}
|
|
12231
12879
|
for (const m of src.matchAll(/<[a-zA-Z][^>]*>/g)) {
|
|
12232
12880
|
const tag = m[0];
|
|
@@ -12236,9 +12884,13 @@ function detectFilterCost(html) {
|
|
|
12236
12884
|
const byAttr = refAttr !== undefined && filterValueConvolves(refAttr, svgConvIds) ? refAttr : null;
|
|
12237
12885
|
if (!inline.length && byAttr === null)
|
|
12238
12886
|
continue;
|
|
12887
|
+
const site = inline.length ? `内联 style:filter:${inline[0]}` : `SVG 属性:filter="${byAttr}"`;
|
|
12888
|
+
for (const key of tagIdentityKeys(tag))
|
|
12889
|
+
if (!staticFilterSites.has(key))
|
|
12890
|
+
staticFilterSites.set(key, site);
|
|
12239
12891
|
if (!isFullBleedStyle(style))
|
|
12240
12892
|
continue;
|
|
12241
|
-
add(staticFullBleed,
|
|
12893
|
+
add(staticFullBleed, site);
|
|
12242
12894
|
}
|
|
12243
12895
|
const js = maskJsComments(scriptBodiesOnly(src));
|
|
12244
12896
|
for (const body of tweenVarObjects(js)) {
|
|
@@ -12253,7 +12905,22 @@ function detectFilterCost(html) {
|
|
|
12253
12905
|
if (svgConvIds.size && /(?:^|[{,])\s*["']?stdDeviation["']?\s*:/.test(body))
|
|
12254
12906
|
add(animated, "补间驱动 SVG 滤镜基元的 stdDeviation");
|
|
12255
12907
|
}
|
|
12256
|
-
|
|
12908
|
+
if (staticFilterSites.size) {
|
|
12909
|
+
const bindings = selectorBindings(js);
|
|
12910
|
+
for (const { method, target, bodies } of tweenCalls(js)) {
|
|
12911
|
+
if (!TWEENING_METHOD.test(method))
|
|
12912
|
+
continue;
|
|
12913
|
+
const driven = bodies.some((b) => topLevelObjKeys(b).some((k) => GSAP_TRANSFORM_KEY.test(k)));
|
|
12914
|
+
if (!driven)
|
|
12915
|
+
continue;
|
|
12916
|
+
for (const key of tweenTargetKeys(target, bindings)) {
|
|
12917
|
+
const site = staticFilterSites.get(key);
|
|
12918
|
+
if (site)
|
|
12919
|
+
add(staticTransformed, `${site} —— 而 \`.${method}(${target.trim().slice(0, 30)}, …)\` 驱动了它的 transform`);
|
|
12920
|
+
}
|
|
12921
|
+
}
|
|
12922
|
+
}
|
|
12923
|
+
return { animated, staticFullBleed, staticTransformed, indeterminate };
|
|
12257
12924
|
}
|
|
12258
12925
|
function lintParticle(html, opts = {}) {
|
|
12259
12926
|
const v = [];
|
|
@@ -12359,6 +13026,8 @@ function lintParticle(html, opts = {}) {
|
|
|
12359
13026
|
push("c-filter-animated", false, `时间线补间直接驱动了真卷积滤镜(${cost.animated.slice(0, 3).join(";")})——` + "blur / drop-shadow / feGaussianBlur 是**真卷积**,每帧成本 ∝「被覆盖面积 × 半径 × 帧数」;" + "一旦被补间驱动,卷积结果**每帧失效**、缓存彻底失灵,这是本组三项里成本机制最重的一项。" + "改法:用 `opacity` / `transform`(`scale` / 位移)表达同一叙事动作;" + "确需滤镜就做成**静态两态切换**(滤镜值不随时间连续变),并把被滤元素的几何覆盖面积收到真正需要的那块矩形上。" + "本项是**成本提示**,不影响 ok / 退出码 / 铺轨;**未报 ≠ 这颗便宜**——真判据是真渲染出片计时");
|
|
12360
13027
|
if (cost.staticFullBleed.length)
|
|
12361
13028
|
push("c-filter-static-fullbleed", false, `整幅静态真卷积滤镜(${cost.staticFullBleed.slice(0, 3).join(";")})——该声明自身即全幅` + "(`position:absolute|fixed` + `inset:0` 或等价铺满),于是每帧都要对整幅做一次卷积。" + "**首选改法:缩小被滤镜覆盖的几何面积**(把滤镜收到它真正需要的那块矩形上——面积一项同时压掉卷积与整幅逐帧合成两笔成本);" + "次选把静态滤镜结果**预烘成图**,且 MUST 用 **RGBA PNG**(透明叠加颗粒的 alpha 是成片合成的必需通道," + "烘成 JPEG 或任何无 alpha 格式会在成片里塌成不透明色块),但预烘只消得掉卷积、消不掉整幅逐帧合成,收益有上限、必有残差。" + "去不去滤镜属审美取值,判断权在作者:本项只给信息与改法,**恒非致命**、不拦铺轨;未报 ≠ 便宜,真判据是真渲染出片计时");
|
|
13029
|
+
if (cost.staticTransformed.length)
|
|
13030
|
+
push("c-filter-static-transformed", false, `静态真卷积滤镜的元素,其 transform 被补间驱动(${cost.staticTransformed.slice(0, 3).join(";")})——` + "滤镜值本身不变,但被滤子树**每帧换一个几何**,卷积结果每帧失效、必须重算," + "与 `c-filter-animated` 是**同一类缓存失效**,只是驱动源从滤镜值换成了 transform。" + "r69 真渲实测:同颗粒对照下这一档 **+42.7%**,比「补间驱动 blur」的 +36.2% **还贵**——它是本组里最容易被忽略的一档。" + "改法(按优先级):① 把滤镜移到**不参与位移/缩放**的那一层(滤镜层与运动层拆开,运动交给外层容器);" + "② 缩小被滤元素的几何覆盖面积;③ 把静态滤镜结果**预烘成 RGBA PNG** 再让它随 transform 走(预烘图平移是零卷积)。" + "⚠️ 射程只收 `transform`:`opacity` 被补间驱动**不在本项射程**(opacity 侧尚无对照实测,不凭推测扩)。" + "本项恒非致命、不拦铺轨;未报 ≠ 便宜,真判据是真渲染出片计时");
|
|
12362
13031
|
if (cost.indeterminate.length)
|
|
12363
13032
|
push("c-filter-indeterminate", false, `该处 filter 值无法静态判定是否含真卷积(${cost.indeterminate.slice(0, 3).join(";")})——` + "补间值不是字符串字面量(变量 / 模板串 / 函数返回),本 lint 不做表达式求值与常量折叠," + "故对该处**未作判定**:既不是「判过且通过」,也不是命中。请人工确认它是否会驱动 blur / drop-shadow;" + "若是,按 `c-filter-animated` 的改法处理。本项恒非致命、不拦铺轨");
|
|
12364
13033
|
return { ok: !v.some((x) => x.fatal), violations: v, opaque, compositionId: cid };
|
|
@@ -12578,20 +13247,20 @@ async function runMg(words, opts) {
|
|
|
12578
13247
|
function resolveDispatch(opts) {
|
|
12579
13248
|
if (opts.dispatch) {
|
|
12580
13249
|
const dispatchPath = resolve12(opts.dispatch);
|
|
12581
|
-
return { dispatchPath, baseDir:
|
|
13250
|
+
return { dispatchPath, baseDir: dirname11(dirname11(dispatchPath)) };
|
|
12582
13251
|
}
|
|
12583
13252
|
if (opts.project) {
|
|
12584
13253
|
const baseDir = resolve12(opts.project);
|
|
12585
|
-
return { dispatchPath:
|
|
13254
|
+
return { dispatchPath: join24(baseDir, "split", "dispatch.json"), baseDir };
|
|
12586
13255
|
}
|
|
12587
13256
|
throw new Error("需 --project <目录> 或显式 --dispatch <path>");
|
|
12588
13257
|
}
|
|
12589
13258
|
function locateGtrk2(baseDir) {
|
|
12590
|
-
return [
|
|
13259
|
+
return [join24(baseDir, "gtrk", "project.gtrk"), join24(baseDir, "project.gtrk")].find((p) => existsSync21(p));
|
|
12591
13260
|
}
|
|
12592
13261
|
function locateSrcHtml(baseDir, compositionId) {
|
|
12593
13262
|
for (const d of MG_SRC_DIRS) {
|
|
12594
|
-
const p =
|
|
13263
|
+
const p = join24(baseDir, d, `${compositionId}.html`);
|
|
12595
13264
|
if (existsSync21(p))
|
|
12596
13265
|
return p;
|
|
12597
13266
|
}
|
|
@@ -12600,7 +13269,7 @@ function locateSrcHtml(baseDir, compositionId) {
|
|
|
12600
13269
|
async function readMgQueue(dispatchPath) {
|
|
12601
13270
|
if (!existsSync21(dispatchPath))
|
|
12602
13271
|
throw new Error(`找不到派单清单:${dispatchPath}(先跑 gtrk split 落地派单)`);
|
|
12603
|
-
const dispatch = JSON.parse(await
|
|
13272
|
+
const dispatch = JSON.parse(await readFile8(dispatchPath, "utf8"));
|
|
12604
13273
|
const queue = dispatch.mg ?? dispatch.rrv_mg;
|
|
12605
13274
|
return Array.isArray(queue) ? queue : [];
|
|
12606
13275
|
}
|
|
@@ -12674,10 +13343,10 @@ async function runLay(opts) {
|
|
|
12674
13343
|
const srcPath = locateSrcHtml(baseDir, q.composition_id);
|
|
12675
13344
|
if (!srcPath) {
|
|
12676
13345
|
skipped.push({ beat: q.beat, reason: "缺颗粒 HTML(未产出)" });
|
|
12677
|
-
log.warn(`${q.beat}:缺 ${
|
|
13346
|
+
log.warn(`${q.beat}:缺 ${join24(baseDir, MG_SRC_DIRS[0], `${q.composition_id}.html`)},跳过`);
|
|
12678
13347
|
continue;
|
|
12679
13348
|
}
|
|
12680
|
-
const html = await
|
|
13349
|
+
const html = await readFile8(srcPath, "utf8");
|
|
12681
13350
|
const category = typeof q.category === "string" ? q.category : undefined;
|
|
12682
13351
|
const slotDuration = Math.round((win.track_ed - win.track_st) * 1000) / 1000;
|
|
12683
13352
|
const lint = lintParticle(html, {
|
|
@@ -12722,7 +13391,7 @@ async function runLay(opts) {
|
|
|
12722
13391
|
});
|
|
12723
13392
|
}
|
|
12724
13393
|
if (!gtrkPath || !project) {
|
|
12725
|
-
log.warn(`未找到工程文件(${
|
|
13394
|
+
log.warn(`未找到工程文件(${join24(baseDir, "gtrk", "project.gtrk")}),跳过铺轨——lint 已完成`);
|
|
12726
13395
|
return done(opts, {
|
|
12727
13396
|
ok: skipped.length === 0,
|
|
12728
13397
|
mode: "lay",
|
|
@@ -12739,7 +13408,7 @@ async function runLay(opts) {
|
|
|
12739
13408
|
});
|
|
12740
13409
|
}
|
|
12741
13410
|
const { gtrk, mtimeMs } = project;
|
|
12742
|
-
const gtrkDir =
|
|
13411
|
+
const gtrkDir = dirname11(gtrkPath);
|
|
12743
13412
|
const covered = new Set(items.map((it) => it.composition_id));
|
|
12744
13413
|
const orphans = laidBefore.filter((id) => !covered.has(id));
|
|
12745
13414
|
const inDispatch = new Set(allQueue.map((q) => q.composition_id));
|
|
@@ -12781,9 +13450,9 @@ async function runLay(opts) {
|
|
|
12781
13450
|
if (opts.replaceAll && orphans.length > 0) {
|
|
12782
13451
|
log.warn(`--replace-all:已显式授权重置整轨,轨上其余 ${orphans.length} 颗已铺颗粒将被剥离(不走增量保留)。`);
|
|
12783
13452
|
}
|
|
12784
|
-
await
|
|
13453
|
+
await mkdir10(join24(gtrkDir, ...MG_ASSET_DIR.split("/")), { recursive: true });
|
|
12785
13454
|
for (const it of items) {
|
|
12786
|
-
await copyFile(srcByComp.get(it.composition_id),
|
|
13455
|
+
await copyFile(srcByComp.get(it.composition_id), join24(gtrkDir, ...it.html_rel.split("/")));
|
|
12787
13456
|
}
|
|
12788
13457
|
const { next, summary, mg } = layMgTracks({ gtrk, items, generatedAt: new Date().toISOString(), keep });
|
|
12789
13458
|
const written = withTimecodeSource(next, "mg", reproj);
|
|
@@ -12827,7 +13496,7 @@ async function runLint(args, opts) {
|
|
|
12827
13496
|
const file = args[0];
|
|
12828
13497
|
if (!file)
|
|
12829
13498
|
throw new Error("用法:gtrk mg lint <particle.html> [--dispatch <path>]");
|
|
12830
|
-
const html = await
|
|
13499
|
+
const html = await readFile8(resolve12(file), "utf8");
|
|
12831
13500
|
const nameId = basename13(file).replace(/\.html?$/i, "");
|
|
12832
13501
|
let dispatchIds;
|
|
12833
13502
|
let slotDuration;
|
|
@@ -12898,12 +13567,12 @@ function done(opts, result) {
|
|
|
12898
13567
|
}
|
|
12899
13568
|
|
|
12900
13569
|
// src/lib/mad/mad.ts
|
|
12901
|
-
import { mkdir as
|
|
13570
|
+
import { mkdir as mkdir13, writeFile as writeFile12 } from "node:fs/promises";
|
|
12902
13571
|
import { existsSync as existsSync24, statSync as statSync3 } from "node:fs";
|
|
12903
|
-
import { resolve as resolve13, join as
|
|
13572
|
+
import { resolve as resolve13, join as join28 } from "node:path";
|
|
12904
13573
|
|
|
12905
13574
|
// src/lib/convert/types.ts
|
|
12906
|
-
function
|
|
13575
|
+
function num2(v, d = 0) {
|
|
12907
13576
|
return typeof v === "number" && isFinite(v) ? v : d;
|
|
12908
13577
|
}
|
|
12909
13578
|
function parseCubicBezier(e) {
|
|
@@ -12952,19 +13621,19 @@ function mergeChannelTracks(ta, tb, defA, defB) {
|
|
|
12952
13621
|
if (!track || track.length === 0)
|
|
12953
13622
|
return dflt;
|
|
12954
13623
|
if (t <= track[0].t)
|
|
12955
|
-
return
|
|
13624
|
+
return num2(track[0].v, dflt);
|
|
12956
13625
|
for (let i = 1;i < track.length; i++) {
|
|
12957
13626
|
if (t <= track[i].t) {
|
|
12958
13627
|
const p = track[i - 1];
|
|
12959
13628
|
const q = track[i];
|
|
12960
13629
|
const span = q.t - p.t;
|
|
12961
13630
|
if (span <= 0)
|
|
12962
|
-
return
|
|
13631
|
+
return num2(q.v, dflt);
|
|
12963
13632
|
const k = (t - p.t) / span;
|
|
12964
|
-
return
|
|
13633
|
+
return num2(p.v, dflt) + (num2(q.v, dflt) - num2(p.v, dflt)) * k;
|
|
12965
13634
|
}
|
|
12966
13635
|
}
|
|
12967
|
-
return
|
|
13636
|
+
return num2(track[track.length - 1].v, dflt);
|
|
12968
13637
|
};
|
|
12969
13638
|
const times = new Set;
|
|
12970
13639
|
for (const k of ta ?? [])
|
|
@@ -12985,17 +13654,17 @@ function sampleTrack(track, t, def) {
|
|
|
12985
13654
|
if (!track || track.length === 0)
|
|
12986
13655
|
return def;
|
|
12987
13656
|
if (t <= track[0].t)
|
|
12988
|
-
return
|
|
13657
|
+
return num2(track[0].v, def);
|
|
12989
13658
|
for (let i = 1;i < track.length; i++) {
|
|
12990
13659
|
const a = track[i - 1];
|
|
12991
13660
|
const b = track[i];
|
|
12992
13661
|
if (t <= b.t) {
|
|
12993
13662
|
const span = Math.max(b.t - a.t, 0.000001);
|
|
12994
13663
|
const k = (t - a.t) / span;
|
|
12995
|
-
return
|
|
13664
|
+
return num2(a.v, def) + (num2(b.v, def) - num2(a.v, def)) * k;
|
|
12996
13665
|
}
|
|
12997
13666
|
}
|
|
12998
|
-
return
|
|
13667
|
+
return num2(track[track.length - 1].v, def);
|
|
12999
13668
|
}
|
|
13000
13669
|
function phasePair(i) {
|
|
13001
13670
|
return [Math.sin(i * 2.399), Math.cos(i * 3.11)];
|
|
@@ -13023,13 +13692,13 @@ function baseRot(anim, t) {
|
|
|
13023
13692
|
return sampleTrack(anim.rot, t, 0);
|
|
13024
13693
|
}
|
|
13025
13694
|
function opWindow(fx, ly) {
|
|
13026
|
-
const t0 =
|
|
13027
|
-
const t1 =
|
|
13695
|
+
const t0 = num2(fx.t0, num2(ly.in, 0));
|
|
13696
|
+
const t1 = num2(fx.t1, num2(ly.out, t0 + 1));
|
|
13028
13697
|
return [t0, t1];
|
|
13029
13698
|
}
|
|
13030
13699
|
function bakeLayerOps(ly) {
|
|
13031
13700
|
const anim = ly.anim ?? {};
|
|
13032
|
-
const pos = [
|
|
13701
|
+
const pos = [num2(ly.pos?.[0], 0), num2(ly.pos?.[1], 0)];
|
|
13033
13702
|
const tracks = [];
|
|
13034
13703
|
const warnings = [];
|
|
13035
13704
|
for (const fx of ly.fx ?? []) {
|
|
@@ -13045,8 +13714,8 @@ function bakeLayerOps(ly) {
|
|
|
13045
13714
|
const times = [];
|
|
13046
13715
|
const values = [];
|
|
13047
13716
|
for (const k of tr) {
|
|
13048
|
-
const t =
|
|
13049
|
-
const v =
|
|
13717
|
+
const t = num2(k.t, 0);
|
|
13718
|
+
const v = num2(k.v, 1);
|
|
13050
13719
|
const [bx, by] = baseScale(anim, t);
|
|
13051
13720
|
times.push(round(t, 3));
|
|
13052
13721
|
values.push([round(bx * v * 100, 3), round(by * v * 100, 3)]);
|
|
@@ -13059,9 +13728,9 @@ function bakeLayerOps(ly) {
|
|
|
13059
13728
|
if (op === "flicker") {
|
|
13060
13729
|
if (t1 - t0 < 0.05)
|
|
13061
13730
|
continue;
|
|
13062
|
-
let freq =
|
|
13731
|
+
let freq = num2(fx.freq, 8);
|
|
13063
13732
|
freq = clamp(freq >= 0.5 ? freq : freq * 30, 0.5, 20);
|
|
13064
|
-
const mag = clamp(
|
|
13733
|
+
const mag = clamp(num2(fx.mag, 0.6), 0.05, 1);
|
|
13065
13734
|
const hi = 1 + mag * 0.8;
|
|
13066
13735
|
const lo = Math.max(0.3, 1 - mag * 0.6);
|
|
13067
13736
|
const n = Math.min(dn(Math.max(2, Math.floor((t1 - t0) * freq * 2))), 160);
|
|
@@ -13080,8 +13749,8 @@ function bakeLayerOps(ly) {
|
|
|
13080
13749
|
if (t1 - t0 < 0.05)
|
|
13081
13750
|
continue;
|
|
13082
13751
|
if (op === "shake") {
|
|
13083
|
-
const amp0 =
|
|
13084
|
-
const freq = clamp(
|
|
13752
|
+
const amp0 = num2(fx.mag ?? fx.amp, 18);
|
|
13753
|
+
const freq = clamp(num2(fx.freq, 20), 1, 40);
|
|
13085
13754
|
const n = Math.min(dn(Math.max(2, Math.floor((t1 - t0) * freq))), 160);
|
|
13086
13755
|
const tr = fx.tracks?.mag;
|
|
13087
13756
|
const seqX = [];
|
|
@@ -13111,9 +13780,9 @@ function bakeLayerOps(ly) {
|
|
|
13111
13780
|
}
|
|
13112
13781
|
if (op === "oscillate") {
|
|
13113
13782
|
if (fx.rot === true) {
|
|
13114
|
-
const a1 =
|
|
13115
|
-
const a2 =
|
|
13116
|
-
const freq = clamp(
|
|
13783
|
+
const a1 = num2(fx.a1, -3);
|
|
13784
|
+
const a2 = num2(fx.a2, 3);
|
|
13785
|
+
const freq = clamp(num2(fx.freq, 2), 0.2, 20);
|
|
13117
13786
|
const n = Math.min(dn(Math.max(4, Math.floor((t1 - t0) * freq * 4))), 120);
|
|
13118
13787
|
const times = [round(t0, 3)];
|
|
13119
13788
|
const values = [[round(baseRot(anim, t0), 3)]];
|
|
@@ -13130,9 +13799,9 @@ function bakeLayerOps(ly) {
|
|
|
13130
13799
|
}
|
|
13131
13800
|
tracks.push({ prop: "rotation", times, values, window: [t0, t1] });
|
|
13132
13801
|
} else {
|
|
13133
|
-
const mag0 =
|
|
13134
|
-
const ang =
|
|
13135
|
-
const freq = clamp(
|
|
13802
|
+
const mag0 = num2(fx.mag, 40);
|
|
13803
|
+
const ang = num2(fx.angle, 90) * Math.PI / 180;
|
|
13804
|
+
const freq = clamp(num2(fx.freq, 4), 0.2, 30);
|
|
13136
13805
|
const n = Math.min(dn(Math.max(4, Math.floor((t1 - t0) * freq * 4))), 160);
|
|
13137
13806
|
const tr = fx.tracks?.mag;
|
|
13138
13807
|
const times = [round(t0, 3)];
|
|
@@ -13164,13 +13833,13 @@ function bakeLayerOps(ly) {
|
|
|
13164
13833
|
let lo;
|
|
13165
13834
|
let hi;
|
|
13166
13835
|
if (fx.amp != null) {
|
|
13167
|
-
lo = 1 -
|
|
13168
|
-
hi = 1 +
|
|
13836
|
+
lo = 1 - num2(fx.amp, 0);
|
|
13837
|
+
hi = 1 + num2(fx.amp, 0);
|
|
13169
13838
|
} else {
|
|
13170
|
-
lo =
|
|
13171
|
-
hi =
|
|
13839
|
+
lo = num2(fx.min, 1);
|
|
13840
|
+
hi = num2(fx.max, 1.06);
|
|
13172
13841
|
}
|
|
13173
|
-
const freq = clamp(
|
|
13842
|
+
const freq = clamp(num2(fx.freq, 2), 0.2, 20);
|
|
13174
13843
|
const n = Math.min(dn(Math.max(4, Math.floor((t1 - t0) * freq * 4))), 160);
|
|
13175
13844
|
const tr = fx.tracks?.max;
|
|
13176
13845
|
const times = [round(t0, 3)];
|
|
@@ -13332,7 +14001,7 @@ function positionBaseKeys(anim, pos) {
|
|
|
13332
14001
|
function scaleBaseKeys(anim, coverExpr) {
|
|
13333
14002
|
const sc = (v) => coverExpr ? `${r37(v)}*${coverExpr}` : `${r37(v)}`;
|
|
13334
14003
|
if (anim.scale?.length) {
|
|
13335
|
-
return anim.scale.map((k) => ({ t: k.t, value: `[${sc(
|
|
14004
|
+
return anim.scale.map((k) => ({ t: k.t, value: `[${sc(num2(k.v, 1) * 100)}, ${sc(num2(k.v, 1) * 100)}]`, e: k.e }));
|
|
13336
14005
|
}
|
|
13337
14006
|
if (anim.sx?.length || anim.sy?.length) {
|
|
13338
14007
|
const merged = mergeChannelTracks(anim.sx, anim.sy, 1, 1);
|
|
@@ -13347,9 +14016,9 @@ function emitBakedOps(ctx, ly, layerVar, tf, coverExpr) {
|
|
|
13347
14016
|
if (tracks.length === 0)
|
|
13348
14017
|
return;
|
|
13349
14018
|
const anim = ly.anim ?? {};
|
|
13350
|
-
const pos = [
|
|
13351
|
-
const inn =
|
|
13352
|
-
const out = Math.max(inn + 0.01,
|
|
14019
|
+
const pos = [num2(ly.pos?.[0], 0), num2(ly.pos?.[1], 0)];
|
|
14020
|
+
const inn = num2(ly.in, 0);
|
|
14021
|
+
const out = Math.max(inn + 0.01, num2(ly.out, inn + 1));
|
|
13353
14022
|
const sc = (v) => coverExpr ? `${r37(v)}*${coverExpr}` : `${r37(v)}`;
|
|
13354
14023
|
const brights = tracks.filter((t) => t.prop === "brightness");
|
|
13355
14024
|
if (brights.length) {
|
|
@@ -13396,7 +14065,7 @@ function emitBakedOps(ctx, ly, layerVar, tf, coverExpr) {
|
|
|
13396
14065
|
}
|
|
13397
14066
|
const rotBaked = tracks.filter((t) => t.prop === "rotation");
|
|
13398
14067
|
if (rotBaked.length) {
|
|
13399
|
-
const baseKeys = (anim.rot ?? []).map((k) => ({ t: k.t, value: `${r37(
|
|
14068
|
+
const baseKeys = (anim.rot ?? []).map((k) => ({ t: k.t, value: `${r37(num2(k.v, 0))}`, e: k.e }));
|
|
13400
14069
|
const bakedKeys = [];
|
|
13401
14070
|
const windows = [];
|
|
13402
14071
|
for (const bt of rotBaked) {
|
|
@@ -13414,10 +14083,10 @@ function emitLayer(ctx, ir, ly, parentNullVar) {
|
|
|
13414
14083
|
const c3 = ir.canvas;
|
|
13415
14084
|
const id = ++ctx.layerSeq;
|
|
13416
14085
|
const v = `ly${id}`;
|
|
13417
|
-
const inn =
|
|
13418
|
-
const out = Math.max(inn + 0.01,
|
|
13419
|
-
const px =
|
|
13420
|
-
const py =
|
|
14086
|
+
const inn = num2(ly.in, 0);
|
|
14087
|
+
const out = Math.max(inn + 0.01, num2(ly.out, c3.duration));
|
|
14088
|
+
const px = num2(ly.pos?.[0], c3.w / 2);
|
|
14089
|
+
const py = num2(ly.pos?.[1], c3.h / 2);
|
|
13421
14090
|
const name = esc(`${ly.id || "L" + id} [${ly.type}]`);
|
|
13422
14091
|
L.push(``);
|
|
13423
14092
|
L.push(` // ---- 层 ${name} in=${r37(inn)} out=${r37(out)} ----`);
|
|
@@ -13439,9 +14108,9 @@ function emitLayer(ctx, ir, ly, parentNullVar) {
|
|
|
13439
14108
|
const footageHit = ly.type === "image" || ly.type === "video" ? ctx.footage[ly.id] : undefined;
|
|
13440
14109
|
const fgVar = footageHit ? ctx.footageVars.get(footageHit.path) : undefined;
|
|
13441
14110
|
if (footageHit && fgVar) {
|
|
13442
|
-
const slotW = Math.max(2, Math.round(
|
|
13443
|
-
const slotH = Math.max(2, Math.round(
|
|
13444
|
-
const srcOffset =
|
|
14111
|
+
const slotW = Math.max(2, Math.round(num2(ly.w, c3.w)));
|
|
14112
|
+
const slotH = Math.max(2, Math.round(num2(ly.h, c3.h)));
|
|
14113
|
+
const srcOffset = num2(footageHit.srcOffset, 0);
|
|
13445
14114
|
const cvv = `cv${id}`;
|
|
13446
14115
|
coverExpr = cvv;
|
|
13447
14116
|
L.push(` // 素材注入(${esc(String(ly.type))}): footage srcOffset=${r37(srcOffset)}`);
|
|
@@ -13472,14 +14141,14 @@ function emitLayer(ctx, ir, ly, parentNullVar) {
|
|
|
13472
14141
|
L.push(` } catch (e) {}`);
|
|
13473
14142
|
} else if (ly.type === "shape") {
|
|
13474
14143
|
const col = colorArr(ly.fill, [0.2, 0.33, 0.67]);
|
|
13475
|
-
const w = Math.max(2, Math.round(
|
|
13476
|
-
const h = Math.max(2, Math.round(
|
|
14144
|
+
const w = Math.max(2, Math.round(num2(ly.w, 400)));
|
|
14145
|
+
const h = Math.max(2, Math.round(num2(ly.h, 400)));
|
|
13477
14146
|
if (ly.shape === "ellipse")
|
|
13478
14147
|
L.push(` // TODO: 原层为椭圆形状,固态占位,可手动换 shape layer`);
|
|
13479
14148
|
L.push(` var ${v} = ${cv}.layers.addSolid([${col.join(",")}], "${name}", ${w}, ${h}, 1, ${r37(c3.duration)});`);
|
|
13480
14149
|
} else {
|
|
13481
|
-
const w = Math.max(2, Math.round(
|
|
13482
|
-
const h = Math.max(2, Math.round(
|
|
14150
|
+
const w = Math.max(2, Math.round(num2(ly.w, c3.w)));
|
|
14151
|
+
const h = Math.max(2, Math.round(num2(ly.h, c3.h)));
|
|
13483
14152
|
L.push(` // 占位素材(${esc(String(ly.type))}${ly.asset ? ` asset=${esc(String(ly.asset))}` : ""}): 请替换为真实素材`);
|
|
13484
14153
|
L.push(` var ${v} = ${cv}.layers.addSolid([0.35,0.35,0.4], "${name} [占位]", ${w}, ${h}, 1, ${r37(c3.duration)});`);
|
|
13485
14154
|
}
|
|
@@ -13509,10 +14178,10 @@ function emitLayer(ctx, ir, ly, parentNullVar) {
|
|
|
13509
14178
|
}
|
|
13510
14179
|
}
|
|
13511
14180
|
if (anim.rot?.length && !bakedProps.has("rotation")) {
|
|
13512
|
-
emitKeys(ctx, `${tf}.property("ADBE Rotate Z")`, anim.rot.map((k) => ({ t: k.t, value: `${r37(
|
|
14181
|
+
emitKeys(ctx, `${tf}.property("ADBE Rotate Z")`, anim.rot.map((k) => ({ t: k.t, value: `${r37(num2(k.v, 0))}`, e: k.e })));
|
|
13513
14182
|
}
|
|
13514
14183
|
if (anim.opacity?.length) {
|
|
13515
|
-
emitKeys(ctx, `${tf}.property("ADBE Opacity")`, anim.opacity.map((k) => ({ t: k.t, value: `${r37(Math.min(1, Math.max(0,
|
|
14184
|
+
emitKeys(ctx, `${tf}.property("ADBE Opacity")`, anim.opacity.map((k) => ({ t: k.t, value: `${r37(Math.min(1, Math.max(0, num2(k.v, 1))) * 100)}`, e: k.e })));
|
|
13516
14185
|
}
|
|
13517
14186
|
if (anim.ls?.length) {
|
|
13518
14187
|
L.push(` // TODO: letterspacing 轨未自动映射(AE 需 Animator>Tracking),共 ${anim.ls.length} 帧`);
|
|
@@ -13526,8 +14195,8 @@ function emitLayer(ctx, ir, ly, parentNullVar) {
|
|
|
13526
14195
|
}
|
|
13527
14196
|
function emitGroupAnim(ctx, v, ly) {
|
|
13528
14197
|
const anim = ly.anim ?? {};
|
|
13529
|
-
const px =
|
|
13530
|
-
const py =
|
|
14198
|
+
const px = num2(ly.pos?.[0], 0);
|
|
14199
|
+
const py = num2(ly.pos?.[1], 0);
|
|
13531
14200
|
if (anim.x?.length || anim.y?.length) {
|
|
13532
14201
|
const merged = mergeChannelTracks(anim.x, anim.y, px, py);
|
|
13533
14202
|
emitKeys(ctx, `${v}.property("ADBE Transform Group").property("ADBE Position")`, merged.map((k) => ({ t: k.t, value: `[${r37(k.a - px)}, ${r37(k.b - py)}]`, e: k.e })));
|
|
@@ -13611,10 +14280,10 @@ function madJsx(opts) {
|
|
|
13611
14280
|
const subVars = [];
|
|
13612
14281
|
windows.forEach((win, wi) => {
|
|
13613
14282
|
const c3 = win.ir.canvas;
|
|
13614
|
-
const sw = Math.max(4, Math.round(
|
|
13615
|
-
const sh = Math.max(4, Math.round(
|
|
13616
|
-
const sfps = Math.min(99, Math.max(1,
|
|
13617
|
-
const sdur = Math.max(0.1,
|
|
14283
|
+
const sw = Math.max(4, Math.round(num2(c3.w, 1920)));
|
|
14284
|
+
const sh = Math.max(4, Math.round(num2(c3.h, 1080)));
|
|
14285
|
+
const sfps = Math.min(99, Math.max(1, num2(c3.fps, 30)));
|
|
14286
|
+
const sdur = Math.max(0.1, num2(c3.duration, 3));
|
|
13618
14287
|
const subName = esc(`${win.uid}-${win.seq}`);
|
|
13619
14288
|
const subVar = `sub${wi}`;
|
|
13620
14289
|
L.push(``);
|
|
@@ -13680,7 +14349,7 @@ function madJsx(opts) {
|
|
|
13680
14349
|
|
|
13681
14350
|
// src/lib/mad/scan.ts
|
|
13682
14351
|
import { readdirSync, statSync as statSync2 } from "node:fs";
|
|
13683
|
-
import { extname as extname7, join as
|
|
14352
|
+
import { extname as extname7, join as join25 } from "node:path";
|
|
13684
14353
|
var VIDEO_EXTS2 = new Set(defaultExtsFor("video") ?? []);
|
|
13685
14354
|
function scanFolder(dirAbs, opts = {}) {
|
|
13686
14355
|
const probe = opts.probe ?? probeGeometry;
|
|
@@ -13691,7 +14360,7 @@ function scanFolder(dirAbs, opts = {}) {
|
|
|
13691
14360
|
} catch {
|
|
13692
14361
|
throw new Error(`素材文件夹不存在或无法读取:${dirAbs}`);
|
|
13693
14362
|
}
|
|
13694
|
-
const files = entries.filter((n) => VIDEO_EXTS2.has(extname7(n).toLowerCase())).map((n) =>
|
|
14363
|
+
const files = entries.filter((n) => VIDEO_EXTS2.has(extname7(n).toLowerCase())).map((n) => join25(dirAbs, n)).filter((p) => {
|
|
13695
14364
|
try {
|
|
13696
14365
|
return statSync2(p).isFile();
|
|
13697
14366
|
} catch {
|
|
@@ -13870,9 +14539,9 @@ function beatQuantized(natLens, analysis) {
|
|
|
13870
14539
|
|
|
13871
14540
|
// src/lib/mad/data.ts
|
|
13872
14541
|
import { createHash as createHash2 } from "node:crypto";
|
|
13873
|
-
import { mkdir as
|
|
14542
|
+
import { mkdir as mkdir11, readFile as readFile9, rename as rename2, rm, writeFile as writeFile11, readdir as readdir2 } from "node:fs/promises";
|
|
13874
14543
|
import { existsSync as existsSync22 } from "node:fs";
|
|
13875
|
-
import { join as
|
|
14544
|
+
import { join as join26 } from "node:path";
|
|
13876
14545
|
function madCacheDir() {
|
|
13877
14546
|
return homeFile("mad-cache");
|
|
13878
14547
|
}
|
|
@@ -13897,14 +14566,14 @@ async function fetchWithTimeout(fetchFn, url, timeoutMs) {
|
|
|
13897
14566
|
}
|
|
13898
14567
|
async function atomicWrite(dest, data) {
|
|
13899
14568
|
const tmp = `${dest}.tmp-${process.pid}-${Date.now()}`;
|
|
13900
|
-
await
|
|
14569
|
+
await writeFile11(tmp, data);
|
|
13901
14570
|
await rename2(tmp, dest);
|
|
13902
14571
|
}
|
|
13903
14572
|
async function verifyFile(path, sha256) {
|
|
13904
14573
|
if (!existsSync22(path))
|
|
13905
14574
|
return false;
|
|
13906
14575
|
try {
|
|
13907
|
-
const buf = await
|
|
14576
|
+
const buf = await readFile9(path);
|
|
13908
14577
|
return sha256Hex(buf) === sha256;
|
|
13909
14578
|
} catch {
|
|
13910
14579
|
return false;
|
|
@@ -13925,11 +14594,11 @@ function validateManifest(obj) {
|
|
|
13925
14594
|
}
|
|
13926
14595
|
async function cleanupOldVersions(cacheRoot, keepVersion, warn) {
|
|
13927
14596
|
try {
|
|
13928
|
-
const entries = await
|
|
14597
|
+
const entries = await readdir2(cacheRoot);
|
|
13929
14598
|
for (const e of entries) {
|
|
13930
14599
|
const m = /^v(\d+)$/.exec(e);
|
|
13931
14600
|
if (m && Number(m[1]) !== keepVersion) {
|
|
13932
|
-
await rm(
|
|
14601
|
+
await rm(join26(cacheRoot, e), { recursive: true, force: true }).catch(() => {});
|
|
13933
14602
|
}
|
|
13934
14603
|
}
|
|
13935
14604
|
} catch {}
|
|
@@ -13938,7 +14607,7 @@ async function ensureMadData(opts, deps) {
|
|
|
13938
14607
|
const { cacheRoot, warn } = deps;
|
|
13939
14608
|
const timeout = deps.manifestTimeoutMs ?? 8000;
|
|
13940
14609
|
const mfUrl = deps.manifestUrl ?? manifestUrl();
|
|
13941
|
-
const snapshotPath =
|
|
14610
|
+
const snapshotPath = join26(cacheRoot, "manifest.json");
|
|
13942
14611
|
let manifest = null;
|
|
13943
14612
|
let online = false;
|
|
13944
14613
|
try {
|
|
@@ -13957,7 +14626,7 @@ async function ensureMadData(opts, deps) {
|
|
|
13957
14626
|
throw new Error("首次使用需联网下载技法数据。请连网后重跑 `gtrk tool mad`(数据会缓存到本地,之后可离线出片)。");
|
|
13958
14627
|
}
|
|
13959
14628
|
try {
|
|
13960
|
-
manifest = validateManifest(JSON.parse(await
|
|
14629
|
+
manifest = validateManifest(JSON.parse(await readFile9(snapshotPath, "utf8")));
|
|
13961
14630
|
} catch {
|
|
13962
14631
|
throw new Error("本地 manifest 缓存损坏且当前离线。请连网重跑,或加 --refresh 强制重新下载。");
|
|
13963
14632
|
}
|
|
@@ -13968,8 +14637,8 @@ async function ensureMadData(opts, deps) {
|
|
|
13968
14637
|
}
|
|
13969
14638
|
}
|
|
13970
14639
|
const version = manifest.version;
|
|
13971
|
-
const verDir =
|
|
13972
|
-
const poolPath =
|
|
14640
|
+
const verDir = join26(cacheRoot, `v${version}`);
|
|
14641
|
+
const poolPath = join26(verDir, "mad_pool.json");
|
|
13973
14642
|
const poolMeta = manifest.datasets.mad_pool;
|
|
13974
14643
|
const cacheValid = await verifyFile(poolPath, poolMeta.sha256);
|
|
13975
14644
|
const needDownload = !!opts.refresh || !cacheValid;
|
|
@@ -13980,7 +14649,7 @@ async function ensureMadData(opts, deps) {
|
|
|
13980
14649
|
}
|
|
13981
14650
|
throw new Error("首次使用需联网下载技法数据。请连网后重跑 `gtrk tool mad`(数据会缓存到本地,之后可离线出片)。");
|
|
13982
14651
|
}
|
|
13983
|
-
await
|
|
14652
|
+
await mkdir11(verDir, { recursive: true });
|
|
13984
14653
|
warn(`下载技法池数据 mad_pool(版本 v${version},约 ${Math.round(poolMeta.size / 1024)} KB)…`);
|
|
13985
14654
|
const res = await deps.fetchFn(poolMeta.url);
|
|
13986
14655
|
if (!res.ok)
|
|
@@ -13993,13 +14662,13 @@ async function ensureMadData(opts, deps) {
|
|
|
13993
14662
|
warn(`技法池数据就绪(v${version})`);
|
|
13994
14663
|
}
|
|
13995
14664
|
if (online) {
|
|
13996
|
-
await
|
|
14665
|
+
await mkdir11(cacheRoot, { recursive: true });
|
|
13997
14666
|
await atomicWrite(snapshotPath, new Uint8Array(Buffer.from(JSON.stringify(manifest), "utf8")));
|
|
13998
14667
|
await cleanupOldVersions(cacheRoot, version, warn);
|
|
13999
14668
|
}
|
|
14000
14669
|
let pool;
|
|
14001
14670
|
try {
|
|
14002
|
-
pool = JSON.parse(await
|
|
14671
|
+
pool = JSON.parse(await readFile9(poolPath, "utf8"));
|
|
14003
14672
|
if (!Array.isArray(pool))
|
|
14004
14673
|
throw new Error("mad_pool 非数组");
|
|
14005
14674
|
} catch (e) {
|
|
@@ -14010,11 +14679,11 @@ async function ensureMadData(opts, deps) {
|
|
|
14010
14679
|
|
|
14011
14680
|
// src/lib/mad/pool.ts
|
|
14012
14681
|
import { gunzipSync } from "node:zlib";
|
|
14013
|
-
import { mkdir as
|
|
14682
|
+
import { mkdir as mkdir12, readFile as readFile10 } from "node:fs/promises";
|
|
14014
14683
|
import { existsSync as existsSync23 } from "node:fs";
|
|
14015
|
-
import { join as
|
|
14684
|
+
import { join as join27 } from "node:path";
|
|
14016
14685
|
function shardPath(verDir, shard) {
|
|
14017
|
-
return
|
|
14686
|
+
return join27(verDir, "ir", `${shard}.json.gz`);
|
|
14018
14687
|
}
|
|
14019
14688
|
function decodeShard(gz) {
|
|
14020
14689
|
const json = gunzipSync(gz).toString("utf8");
|
|
@@ -14033,7 +14702,7 @@ function makeIrLoader(assetsBase, verDir, online, deps) {
|
|
|
14033
14702
|
const path = shardPath(verDir, shard);
|
|
14034
14703
|
if (existsSync23(path)) {
|
|
14035
14704
|
try {
|
|
14036
|
-
const s2 = decodeShard(await
|
|
14705
|
+
const s2 = decodeShard(await readFile10(path));
|
|
14037
14706
|
memo.set(shard, s2);
|
|
14038
14707
|
return s2;
|
|
14039
14708
|
} catch {
|
|
@@ -14049,7 +14718,7 @@ function makeIrLoader(assetsBase, verDir, online, deps) {
|
|
|
14049
14718
|
throw new Error(`IR 分片下载失败 HTTP ${res.status}:${url}`);
|
|
14050
14719
|
const buf = new Uint8Array(await res.arrayBuffer());
|
|
14051
14720
|
const s = decodeShard(buf);
|
|
14052
|
-
await
|
|
14721
|
+
await mkdir12(join27(verDir, "ir"), { recursive: true });
|
|
14053
14722
|
await atomicWrite(path, buf);
|
|
14054
14723
|
memo.set(shard, s);
|
|
14055
14724
|
return s;
|
|
@@ -14261,10 +14930,10 @@ async function runMad(inputArg, opts, deps = {}) {
|
|
|
14261
14930
|
const header = madHeader(version, now.toISOString());
|
|
14262
14931
|
const bgm = level <= 2 && bgmForTrack ? { path: bgmForTrack, markers } : undefined;
|
|
14263
14932
|
const { jsx } = madJsx({ master, windows, header, bgm });
|
|
14264
|
-
const outDir = opts.out ? resolve13(opts.out) :
|
|
14265
|
-
await
|
|
14266
|
-
const jsxPath =
|
|
14267
|
-
await
|
|
14933
|
+
const outDir = opts.out ? resolve13(opts.out) : join28(process.cwd(), `mad-${timestamp4(now)}`);
|
|
14934
|
+
await mkdir13(outDir, { recursive: true });
|
|
14935
|
+
const jsxPath = join28(outDir, "mad.jsx");
|
|
14936
|
+
await writeFile12(jsxPath, jsx);
|
|
14268
14937
|
const result = {
|
|
14269
14938
|
ok: true,
|
|
14270
14939
|
tool: "mad",
|
|
@@ -14275,7 +14944,7 @@ async function runMad(inputArg, opts, deps = {}) {
|
|
|
14275
14944
|
degradeLevel: level,
|
|
14276
14945
|
techniques: chosen.map((c3) => ({ uid: c3.entry.uid, pid: c3.entry.pid, cat: c3.entry.cat, t0: c3.entry.t0, t1: c3.entry.t1 }))
|
|
14277
14946
|
};
|
|
14278
|
-
await
|
|
14947
|
+
await writeFile12(join28(outDir, "result.json"), JSON.stringify({ ...result, finishedAt: now.toISOString() }, null, 2));
|
|
14279
14948
|
warn(completionMessage(jsxPath, level));
|
|
14280
14949
|
return result;
|
|
14281
14950
|
}
|
|
@@ -14409,8 +15078,8 @@ async function runMadInTool(inputArg, opts) {
|
|
|
14409
15078
|
|
|
14410
15079
|
// src/commands/transcript.ts
|
|
14411
15080
|
import { existsSync as existsSync25 } from "node:fs";
|
|
14412
|
-
import { mkdir as
|
|
14413
|
-
import { basename as basename14, dirname as
|
|
15081
|
+
import { mkdir as mkdir14, rename as rename3, rm as rm2, stat as stat6, writeFile as writeFile13 } from "node:fs/promises";
|
|
15082
|
+
import { basename as basename14, dirname as dirname12, extname as extname8, join as join29, resolve as resolve14 } from "node:path";
|
|
14414
15083
|
|
|
14415
15084
|
// src/lib/transcript.ts
|
|
14416
15085
|
var AGENT_SUMMARY_PENDING = "<!-- gtrk:agent-summary-pending -->";
|
|
@@ -14560,16 +15229,16 @@ async function validateTranscriptInput(input) {
|
|
|
14560
15229
|
}
|
|
14561
15230
|
function resolveTranscriptOutput(inputAbs, out) {
|
|
14562
15231
|
const base = basename14(inputAbs, extname8(inputAbs));
|
|
14563
|
-
const output = out ? resolve14(out) :
|
|
15232
|
+
const output = out ? resolve14(out) : join29(dirname12(inputAbs), `${base}-transcript.md`);
|
|
14564
15233
|
if (extname8(output).toLowerCase() !== ".md")
|
|
14565
15234
|
throw new Error("--out 必须指向一个 .md 文件");
|
|
14566
15235
|
return output;
|
|
14567
15236
|
}
|
|
14568
15237
|
async function writeMarkdownAtomic(path, markdown) {
|
|
14569
|
-
await
|
|
15238
|
+
await mkdir14(dirname12(path), { recursive: true });
|
|
14570
15239
|
const temp = `${path}.${process.pid}.${Math.random().toString(16).slice(2)}.tmp`;
|
|
14571
15240
|
try {
|
|
14572
|
-
await
|
|
15241
|
+
await writeFile13(temp, markdown, "utf8");
|
|
14573
15242
|
await rename3(temp, path);
|
|
14574
15243
|
} finally {
|
|
14575
15244
|
await rm2(temp, { force: true });
|
|
@@ -14643,8 +15312,8 @@ function registerTranscript(program2) {
|
|
|
14643
15312
|
}
|
|
14644
15313
|
|
|
14645
15314
|
// src/commands/music-visualizer.ts
|
|
14646
|
-
import { resolve as resolve15, join as
|
|
14647
|
-
import { mkdir as
|
|
15315
|
+
import { resolve as resolve15, join as join30, dirname as dirname13, basename as basename15, extname as extname9 } from "node:path";
|
|
15316
|
+
import { mkdir as mkdir15, writeFile as writeFile14 } from "node:fs/promises";
|
|
14648
15317
|
import { existsSync as existsSync26 } from "node:fs";
|
|
14649
15318
|
var TASK_TYPE5 = "music_visualizer";
|
|
14650
15319
|
var PRICE_KEY2 = "music_visualizer";
|
|
@@ -14763,7 +15432,7 @@ async function runMusicVisualizer(audio, opts) {
|
|
|
14763
15432
|
assertExt(coverAbs, IMAGE_EXTS2, "封面图");
|
|
14764
15433
|
const extraParams = parseExtraParams3(opts.param, opts.paramsJson);
|
|
14765
15434
|
const projName = basename15(audioAbs, extname9(audioAbs));
|
|
14766
|
-
const outDir = resolve15(opts.out ??
|
|
15435
|
+
const outDir = resolve15(opts.out ?? join30(dirname13(audioAbs), `${projName}-visualizer-${timestamp5()}`));
|
|
14767
15436
|
log.step(`▶ 音乐可视化:${basename15(audioAbs)}(模板 ${template}${bgAbs ? " · 背景" : ""}${coverAbs ? " · 封面" : ""})`);
|
|
14768
15437
|
let billingHint;
|
|
14769
15438
|
try {
|
|
@@ -14809,8 +15478,8 @@ async function runMusicVisualizer(audio, opts) {
|
|
|
14809
15478
|
});
|
|
14810
15479
|
const { taskId } = submitted;
|
|
14811
15480
|
log.info(`task_id = ${taskId}`);
|
|
14812
|
-
await
|
|
14813
|
-
await
|
|
15481
|
+
await mkdir15(outDir, { recursive: true });
|
|
15482
|
+
await writeFile14(join30(outDir, "task.json"), JSON.stringify({ taskId, taskType: TASK_TYPE5, fileId: submitted.fileId, source: audioAbs, template, createdAt: new Date().toISOString() }, null, 2));
|
|
14814
15483
|
log.step("③ 云端处理中(每 5s 轮询)…");
|
|
14815
15484
|
const result = await pollTask(cfg, TASK_TYPE5, taskId, (status, progress) => {
|
|
14816
15485
|
log.tick(`${status}${progress != null ? ` ${Math.round(progress)}%` : ""}`);
|
|
@@ -14821,7 +15490,7 @@ async function runMusicVisualizer(audio, opts) {
|
|
|
14821
15490
|
const files = [];
|
|
14822
15491
|
const errors = {};
|
|
14823
15492
|
if (url) {
|
|
14824
|
-
const dest =
|
|
15493
|
+
const dest = join30(outDir, `${projName}-visualizer${extFromUrl(url, ".mp4")}`);
|
|
14825
15494
|
try {
|
|
14826
15495
|
await downloadStream(url, dest);
|
|
14827
15496
|
files.push(dest);
|
|
@@ -14843,7 +15512,7 @@ async function runMusicVisualizer(audio, opts) {
|
|
|
14843
15512
|
...Object.keys(errors).length ? { errors } : {},
|
|
14844
15513
|
finishedAt: new Date().toISOString()
|
|
14845
15514
|
};
|
|
14846
|
-
await
|
|
15515
|
+
await writeFile14(join30(outDir, "result.json"), JSON.stringify(resultJson, null, 2));
|
|
14847
15516
|
if (opts.json) {
|
|
14848
15517
|
process.stdout.write(`${JSON.stringify(resultJson)}
|
|
14849
15518
|
`);
|
|
@@ -14861,7 +15530,7 @@ try {
|
|
|
14861
15530
|
process.loadEnvFile?.();
|
|
14862
15531
|
} catch {}
|
|
14863
15532
|
migrateLegacyHome();
|
|
14864
|
-
var { version } = JSON.parse(readFileSync6(
|
|
15533
|
+
var { version } = JSON.parse(readFileSync6(join31(packageRoot(), "package.json"), "utf8"));
|
|
14865
15534
|
var program2 = new Command;
|
|
14866
15535
|
program2.name("gtrk").description("同合云成片流水线 CLI —— agent 驱动云端任务、产物拉回本地、三方工程文件(客户端/剪映/PR)互通").version(version);
|
|
14867
15536
|
registerInstall(program2);
|