@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/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 join30 } from "node:path";
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 mkdir4, writeFile as writeFile5, readFile as readFile4 } from "node:fs/promises";
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 mkdir(CACHE_DIR, { recursive: true });
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 mkdir(CACHE_DIR, { recursive: true });
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 mkdir2, stat as stat4 } from "node:fs/promises";
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 mkdir2(audioCacheDir(), { recursive: true });
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 mkdir3, cp, writeFile as writeFile4 } from "node:fs/promises";
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 mkdir3(outDir, { recursive: true });
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 mkdir3(fmtDir, { recursive: true });
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
- jianyingDraftPath = join14(opts.draftDir, basename5(outDir));
7083
- await mkdir3(jianyingDraftPath, { recursive: true });
7084
- await cp(join14(outDir, "jianying"), jianyingDraftPath, { recursive: true });
7085
- log.info(`剪映草稿已落到:${jianyingDraftPath}`);
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 mkdir4(outDir, { recursive: true });
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 join17, dirname as dirname5, basename as basename8, extname as extname5 } from "node:path";
7288
- import { mkdir as mkdir6, writeFile as writeFile7, cp as cp2 } from "node:fs/promises";
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 join16, dirname as dirname4, basename as basename7, extname as extname4 } from "node:path";
7293
- import { mkdir as mkdir5, writeFile as writeFile6, stat as stat5 } from "node:fs/promises";
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 { extname as extname3, resolve as resolvePath } from "node:path";
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 join16(dirname4(inputAbs), `${base}-${descriptor.name}`);
9352
+ return join17(dirname5(inputAbs), `${base}-${descriptor.name}`);
8886
9353
  }
8887
- return join16(process.cwd(), `${descriptor.name}-${timestamp2()}`);
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 baseName = inputAbs ? basename7(inputAbs, extname4(inputAbs)) : descriptor.name;
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 mkdir5(outDir, { recursive: true });
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 writeFile6(join16(outDir, "task.json"), JSON.stringify({
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 = join16(outDir, it.filename);
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 = join16(outDir, "result-output.json");
9010
- await writeFile6(resultFile, JSON.stringify(structured, null, 2));
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 writeFile6(join16(outDir, "result.json"), JSON.stringify({ ...result, finishedAt: new Date().toISOString() }, null, 2));
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 ?? join17(dirname5(inputAbs), `${projName}-long2short`));
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 ? join17(draftRoot, outName) : undefined;
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 mkdir6(outDir, { recursive: true });
9128
- await writeFile7(join17(outDir, "task.json"), JSON.stringify({ taskId, taskType: TASK_TYPE2, fileId, source: inputAbs, formats, createdAt: new Date().toISOString() }, null, 2));
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 = join17(dirname5(inputAbs), dest);
9655
+ dest = join18(dirname6(inputAbs), dest);
9152
9656
  try {
9153
- await mkdir6(dirname5(dest), { recursive: true });
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 = join17(outDir, `clip${i}`);
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
- for (const [i, cr] of clipResults.entries()) {
9191
- const src = join17(cr.dir, "jianying");
9192
- if (!existsSync16(src))
9193
- continue;
9194
- const dest = `${draftTarget}_clip${i}`;
9195
- try {
9196
- await mkdir6(dest, { recursive: true });
9197
- await cp2(src, dest, { recursive: true });
9198
- } catch (e) {
9199
- errors[`clip${i}:jianying:draft`] = e instanceof Error ? e.message : String(e);
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
- await writeFile7(join17(outDir, "report.json"), JSON.stringify(output.report ?? {}, null, 2));
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: join17(outDir, "report.json"),
9739
+ reportFile: join18(outDir, "report.json"),
9216
9740
  ...Object.keys(errors).length ? { errors } : {}
9217
9741
  };
9218
- await writeFile7(join17(outDir, "result.json"), JSON.stringify({ ...rootResult, finishedAt: new Date().toISOString() }, null, 2));
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 join18 } from "node:path";
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 ?? join18(process.cwd(), `${taskId}-video-project-${timestamp3()}`));
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 dirname6, join as join19, basename as basename9, extname as extname6 } from "node:path";
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 ?? join19(dirname6(gtrkAbs), `${basename9(gtrkAbs, extname6(gtrkAbs))}.mp4`));
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(dirname6(outMp4));
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 join21, dirname as dirname8, basename as basename11 } from "node:path";
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 readFile5, writeFile as writeFile8, mkdir as mkdir7 } from "node:fs/promises";
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 dirname7, join as join20, basename as basename10 } from "node:path";
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 = join20(dirname7(path), `.${basename10(path)}.${randomBytes2(6).toString("hex")}.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 = join20(dirname7(path), `.${basename10(path)}.${randomBytes2(6).toString("hex")}.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([join21(project, "gtrk", "project.gtrk"), join21(project, "project.gtrk")]) ?? join21(project, "gtrk", "project.gtrk");
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
- join21(project, "transcript", "transcript.json"),
9451
- join21(project, "json", "transcript.json"),
9452
- join21(project, "transcript.json")
9974
+ join22(project, "transcript", "transcript.json"),
9975
+ join22(project, "json", "transcript.json"),
9976
+ join22(project, "transcript.json")
9453
9977
  ]);
9454
- const baseDir = project ?? dirname8(gtrkPath);
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 readFile5(path, "utf8"));
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 = join21(baseDir, "split");
9484
- await mkdir7(splitDir, { recursive: true });
9485
- const viewPath = join21(splitDir, "view.json");
9486
- await writeFile8(viewPath, JSON.stringify(view, null, 2));
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 readFile5(splitdocPath, "utf8"));
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 = join21(baseDir, "split");
9543
- await mkdir7(splitDir, { recursive: true });
9544
- const dispatchPath = join21(splitDir, "dispatch.json");
9545
- await writeFile8(dispatchPath, JSON.stringify(landing.dispatch, null, 2));
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 = join21(splitDir, "visual-split.md");
9549
- await writeFile8(mdPath, renderSplitMarkdown(doc, landing, { projectSlug, projectedAt }));
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 join22, dirname as dirname9, basename as basename12 } from "node:path";
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 readFile6, writeFile as writeFile9, mkdir as mkdir8, rename } from "node:fs/promises";
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 list = out.get(id) ?? [];
10363
- list.push({
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, list);
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 list = refs.get(id) ?? [];
10937
+ const list2 = refs.get(id) ?? [];
10414
10938
  const entry = {
10415
10939
  id,
10416
10940
  path,
10417
10941
  kind,
10418
10942
  resolved,
10419
- referenced: list.length > 0,
10420
- refCount: list.length,
10421
- refs: list
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 list = best.r.also_matched_queries ??= [];
10571
- if (!list.includes(q.query))
10572
- list.push(q.query);
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 = dirname9(dirname9(dispatchPath));
11246
+ baseDir = dirname10(dirname10(dispatchPath));
10723
11247
  } else if (opts.project) {
10724
11248
  baseDir = resolve11(opts.project);
10725
- dispatchPath = join22(baseDir, "split", "dispatch.json");
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 readFile6(dispatchPath, "utf8"));
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 = join22(baseDir, "split");
10795
- await mkdir8(splitDir, { recursive: true });
10796
- const planPath = join22(splitDir, "broll-plan.json");
10797
- await writeFile9(planPath, JSON.stringify(plan, null, 2));
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 = [join22(baseDir, "gtrk", "project.gtrk"), join22(baseDir, "project.gtrk")];
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(`未找到工程文件(${join22(baseDir, "gtrk", "project.gtrk")}),跳过铺轨——plan 已产出,可后续在有工程的目录重跑`);
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 = dirname9(gtrkPath);
10859
- const previewDir = join22(gtrkDir, ...BROLL_PREVIEW_DIR.split("/"));
10860
- await mkdir8(previewDir, { recursive: true });
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 = join22(gtrkDir, ...rel.split("/"));
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 list = summary.keptEditedTracks;
10922
- log.err(`拒绝铺轨:${list.length} 条候选轨已被你在客户端编辑过(track_index ${list.join("/") || "-"})——` + "本次不剥它们、也不铺新轨,工程文件零改动。");
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: list,
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 = join22(gtrkDir, ...rel.split("/"));
11469
+ const abs = join23(gtrkDir, ...rel.split("/"));
10946
11470
  try {
10947
11471
  if (!existsSync20(abs)) {
10948
- await mkdir8(dirname9(abs), { recursive: true });
11472
+ await mkdir9(dirname10(abs), { recursive: true });
10949
11473
  const tmp = `${abs}.tmp-${process.pid}`;
10950
- await writeFile9(tmp, encodeSolidPng(spec));
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 writeFile9(absPath, bytes);
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 writeFile9(absPath, raw);
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 writeFile9(outPath, JSON.stringify({ query, recalled: data.recalled, results }, null, 2));
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 join23, dirname as dirname10, basename as basename13 } from "node:path";
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 readFile7, mkdir as mkdir9, copyFile } from "node:fs/promises";
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 list = callsByFunction.get(fn.declStart) ?? [];
11930
- list.push({ at, loops: executionLoopsAt(at) });
11931
- callsByFunction.set(fn.declStart, list);
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 styleBlockOwnDecls(html) {
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
- else if (c3 === "}") {
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
- for (const own of styleBlockOwnDecls(src)) {
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 && isFullBleedStyle(own))
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, inline.length ? `内联 style:filter:${inline[0]}` : `SVG 属性:filter="${byAttr}"`);
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
- return { animated, staticFullBleed, indeterminate };
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: dirname10(dirname10(dispatchPath)) };
13250
+ return { dispatchPath, baseDir: dirname11(dirname11(dispatchPath)) };
12582
13251
  }
12583
13252
  if (opts.project) {
12584
13253
  const baseDir = resolve12(opts.project);
12585
- return { dispatchPath: join23(baseDir, "split", "dispatch.json"), baseDir };
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 [join23(baseDir, "gtrk", "project.gtrk"), join23(baseDir, "project.gtrk")].find((p) => existsSync21(p));
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 = join23(baseDir, d, `${compositionId}.html`);
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 readFile7(dispatchPath, "utf8"));
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}:缺 ${join23(baseDir, MG_SRC_DIRS[0], `${q.composition_id}.html`)},跳过`);
13346
+ log.warn(`${q.beat}:缺 ${join24(baseDir, MG_SRC_DIRS[0], `${q.composition_id}.html`)},跳过`);
12678
13347
  continue;
12679
13348
  }
12680
- const html = await readFile7(srcPath, "utf8");
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(`未找到工程文件(${join23(baseDir, "gtrk", "project.gtrk")}),跳过铺轨——lint 已完成`);
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 = dirname10(gtrkPath);
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 mkdir9(join23(gtrkDir, ...MG_ASSET_DIR.split("/")), { recursive: true });
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), join23(gtrkDir, ...it.html_rel.split("/")));
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 readFile7(resolve12(file), "utf8");
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 mkdir12, writeFile as writeFile11 } from "node:fs/promises";
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 join27 } from "node:path";
13572
+ import { resolve as resolve13, join as join28 } from "node:path";
12904
13573
 
12905
13574
  // src/lib/convert/types.ts
12906
- function num(v, d = 0) {
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 num(track[0].v, dflt);
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 num(q.v, dflt);
13631
+ return num2(q.v, dflt);
12963
13632
  const k = (t - p.t) / span;
12964
- return num(p.v, dflt) + (num(q.v, dflt) - num(p.v, dflt)) * k;
13633
+ return num2(p.v, dflt) + (num2(q.v, dflt) - num2(p.v, dflt)) * k;
12965
13634
  }
12966
13635
  }
12967
- return num(track[track.length - 1].v, dflt);
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 num(track[0].v, def);
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 num(a.v, def) + (num(b.v, def) - num(a.v, def)) * k;
13664
+ return num2(a.v, def) + (num2(b.v, def) - num2(a.v, def)) * k;
12996
13665
  }
12997
13666
  }
12998
- return num(track[track.length - 1].v, def);
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 = num(fx.t0, num(ly.in, 0));
13027
- const t1 = num(fx.t1, num(ly.out, t0 + 1));
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 = [num(ly.pos?.[0], 0), num(ly.pos?.[1], 0)];
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 = num(k.t, 0);
13049
- const v = num(k.v, 1);
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 = num(fx.freq, 8);
13731
+ let freq = num2(fx.freq, 8);
13063
13732
  freq = clamp(freq >= 0.5 ? freq : freq * 30, 0.5, 20);
13064
- const mag = clamp(num(fx.mag, 0.6), 0.05, 1);
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 = num(fx.mag ?? fx.amp, 18);
13084
- const freq = clamp(num(fx.freq, 20), 1, 40);
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 = num(fx.a1, -3);
13115
- const a2 = num(fx.a2, 3);
13116
- const freq = clamp(num(fx.freq, 2), 0.2, 20);
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 = num(fx.mag, 40);
13134
- const ang = num(fx.angle, 90) * Math.PI / 180;
13135
- const freq = clamp(num(fx.freq, 4), 0.2, 30);
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 - num(fx.amp, 0);
13168
- hi = 1 + num(fx.amp, 0);
13836
+ lo = 1 - num2(fx.amp, 0);
13837
+ hi = 1 + num2(fx.amp, 0);
13169
13838
  } else {
13170
- lo = num(fx.min, 1);
13171
- hi = num(fx.max, 1.06);
13839
+ lo = num2(fx.min, 1);
13840
+ hi = num2(fx.max, 1.06);
13172
13841
  }
13173
- const freq = clamp(num(fx.freq, 2), 0.2, 20);
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(num(k.v, 1) * 100)}, ${sc(num(k.v, 1) * 100)}]`, e: k.e }));
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 = [num(ly.pos?.[0], 0), num(ly.pos?.[1], 0)];
13351
- const inn = num(ly.in, 0);
13352
- const out = Math.max(inn + 0.01, num(ly.out, inn + 1));
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(num(k.v, 0))}`, e: k.e }));
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 = num(ly.in, 0);
13418
- const out = Math.max(inn + 0.01, num(ly.out, c3.duration));
13419
- const px = num(ly.pos?.[0], c3.w / 2);
13420
- const py = num(ly.pos?.[1], c3.h / 2);
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(num(ly.w, c3.w)));
13443
- const slotH = Math.max(2, Math.round(num(ly.h, c3.h)));
13444
- const srcOffset = num(footageHit.srcOffset, 0);
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(num(ly.w, 400)));
13476
- const h = Math.max(2, Math.round(num(ly.h, 400)));
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(num(ly.w, c3.w)));
13482
- const h = Math.max(2, Math.round(num(ly.h, c3.h)));
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(num(k.v, 0))}`, e: k.e })));
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, num(k.v, 1))) * 100)}`, e: k.e })));
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 = num(ly.pos?.[0], 0);
13530
- const py = num(ly.pos?.[1], 0);
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(num(c3.w, 1920)));
13615
- const sh = Math.max(4, Math.round(num(c3.h, 1080)));
13616
- const sfps = Math.min(99, Math.max(1, num(c3.fps, 30)));
13617
- const sdur = Math.max(0.1, num(c3.duration, 3));
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 join24 } from "node:path";
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) => join24(dirAbs, n)).filter((p) => {
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 mkdir10, readFile as readFile8, rename as rename2, rm, writeFile as writeFile10, readdir } from "node:fs/promises";
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 join25 } from "node:path";
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 writeFile10(tmp, data);
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 readFile8(path);
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 readdir(cacheRoot);
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(join25(cacheRoot, e), { recursive: true, force: true }).catch(() => {});
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 = join25(cacheRoot, "manifest.json");
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 readFile8(snapshotPath, "utf8")));
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 = join25(cacheRoot, `v${version}`);
13972
- const poolPath = join25(verDir, "mad_pool.json");
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 mkdir10(verDir, { recursive: true });
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 mkdir10(cacheRoot, { recursive: true });
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 readFile8(poolPath, "utf8"));
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 mkdir11, readFile as readFile9 } from "node:fs/promises";
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 join26 } from "node:path";
14684
+ import { join as join27 } from "node:path";
14016
14685
  function shardPath(verDir, shard) {
14017
- return join26(verDir, "ir", `${shard}.json.gz`);
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 readFile9(path));
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 mkdir11(join26(verDir, "ir"), { recursive: true });
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) : join27(process.cwd(), `mad-${timestamp4(now)}`);
14265
- await mkdir12(outDir, { recursive: true });
14266
- const jsxPath = join27(outDir, "mad.jsx");
14267
- await writeFile11(jsxPath, jsx);
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 writeFile11(join27(outDir, "result.json"), JSON.stringify({ ...result, finishedAt: now.toISOString() }, null, 2));
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 mkdir13, rename as rename3, rm as rm2, stat as stat6, writeFile as writeFile12 } from "node:fs/promises";
14413
- import { basename as basename14, dirname as dirname11, extname as extname8, join as join28, resolve as resolve14 } from "node:path";
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) : join28(dirname11(inputAbs), `${base}-transcript.md`);
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 mkdir13(dirname11(path), { recursive: true });
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 writeFile12(temp, markdown, "utf8");
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 join29, dirname as dirname12, basename as basename15, extname as extname9 } from "node:path";
14647
- import { mkdir as mkdir14, writeFile as writeFile13 } from "node:fs/promises";
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 ?? join29(dirname12(audioAbs), `${projName}-visualizer-${timestamp5()}`));
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 mkdir14(outDir, { recursive: true });
14813
- await writeFile13(join29(outDir, "task.json"), JSON.stringify({ taskId, taskType: TASK_TYPE5, fileId: submitted.fileId, source: audioAbs, template, createdAt: new Date().toISOString() }, null, 2));
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 = join29(outDir, `${projName}-visualizer${extFromUrl(url, ".mp4")}`);
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 writeFile13(join29(outDir, "result.json"), JSON.stringify(resultJson, null, 2));
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(join30(packageRoot(), "package.json"), "utf8"));
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);