@gitruck/cli 0.2.14 → 0.2.16

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 readFileSync5 } from "node:fs";
4200
- import { join as join28 } from "node:path";
4200
+ import { join as join29 } from "node:path";
4201
4201
 
4202
4202
  // src/lib/paths.ts
4203
4203
  import { dirname, join } from "node:path";
@@ -4526,8 +4526,8 @@ function registerSkills(program2) {
4526
4526
  }
4527
4527
 
4528
4528
  // src/commands/init.ts
4529
- import { join as join9, resolve as resolve3 } from "node:path";
4530
- import { existsSync as existsSync8 } from "node:fs";
4529
+ import { join as join10, resolve as resolve3 } from "node:path";
4530
+ import { existsSync as existsSync9 } from "node:fs";
4531
4531
 
4532
4532
  // src/lib/user-config.ts
4533
4533
  import { join as join3 } from "node:path";
@@ -4686,12 +4686,463 @@ async function promptConfirm(message, defaultYes = true) {
4686
4686
  }
4687
4687
 
4688
4688
  // src/commands/doctor.ts
4689
- import { existsSync as existsSync7 } from "node:fs";
4690
- import { join as join8 } from "node:path";
4689
+ import { existsSync as existsSync8 } from "node:fs";
4690
+ import { join as join9 } from "node:path";
4691
4691
 
4692
4692
  // src/lib/column-config.ts
4693
- import { join as join5 } from "node:path";
4694
- import { existsSync as existsSync5, readFileSync as readFileSync2 } from "node:fs";
4693
+ import { join as join6 } from "node:path";
4694
+ import { existsSync as existsSync6, readFileSync as readFileSync2 } from "node:fs";
4695
+
4696
+ // src/lib/reproject.ts
4697
+ import { existsSync as existsSync5 } from "node:fs";
4698
+ import { readFile } from "node:fs/promises";
4699
+ import { basename, join as join5 } from "node:path";
4700
+
4701
+ // src/lib/projection.ts
4702
+ function r3(n) {
4703
+ return Math.round(n * 1000) / 1000;
4704
+ }
4705
+ function normClip(c3) {
4706
+ const clip_st = c3.clip_st ?? 0;
4707
+ const track_st = c3.track_st ?? 0;
4708
+ const dur = c3.duration ?? (c3.clip_ed != null ? c3.clip_ed - clip_st : 0);
4709
+ const clip_ed = c3.clip_ed ?? clip_st + dur;
4710
+ return { clip_st, clip_ed, track_st };
4711
+ }
4712
+ function pickMainVideoTrack(gtrk) {
4713
+ const tracks = gtrk.video_track ?? [];
4714
+ if (!tracks.length)
4715
+ return;
4716
+ let best = tracks[0];
4717
+ let bestIdx = best.track_index ?? 0;
4718
+ for (const t of tracks) {
4719
+ const idx = t.track_index ?? 0;
4720
+ if (idx < bestIdx) {
4721
+ best = t;
4722
+ bestIdx = idx;
4723
+ }
4724
+ }
4725
+ return best;
4726
+ }
4727
+ function countMainTrackMaterialClips(gtrk, materialId) {
4728
+ const id = String(materialId);
4729
+ const mainTrack = pickMainVideoTrack(gtrk);
4730
+ return (mainTrack?.track_timeline ?? []).filter((c3) => c3.material != null && String(c3.material) === id).length;
4731
+ }
4732
+ function projectTranscript(transcript, gtrk, opts = {}) {
4733
+ const materialId = String(transcript.material_id);
4734
+ const mainTrack = pickMainVideoTrack(gtrk);
4735
+ const clips = (mainTrack?.track_timeline ?? []).filter((c3) => c3.material != null && String(c3.material) === materialId).map(normClip);
4736
+ const entries = [];
4737
+ transcript.utterances.forEach((utt, sourceIndex) => {
4738
+ const totalWords = utt.words?.length ?? 0;
4739
+ const instances = [];
4740
+ for (const clip of clips) {
4741
+ const surviving = [];
4742
+ for (const word of utt.words ?? []) {
4743
+ const s = Math.max(word.st, clip.clip_st);
4744
+ const e = Math.min(word.ed, clip.clip_ed);
4745
+ if (e > s) {
4746
+ surviving.push({
4747
+ w: word.w,
4748
+ track_st: r3(clip.track_st + (s - clip.clip_st)),
4749
+ track_ed: r3(clip.track_st + (e - clip.clip_st))
4750
+ });
4751
+ }
4752
+ }
4753
+ if (surviving.length) {
4754
+ instances.push({
4755
+ track_st: Math.min(...surviving.map((x) => x.track_st)),
4756
+ track_ed: Math.max(...surviving.map((x) => x.track_ed)),
4757
+ kept_words: surviving.length,
4758
+ words: surviving
4759
+ });
4760
+ }
4761
+ }
4762
+ if (!instances.length) {
4763
+ entries.push({
4764
+ id: utt.id,
4765
+ text: utt.text,
4766
+ dropped: true,
4767
+ sourceIndex,
4768
+ instIndex: 0,
4769
+ track_st: null,
4770
+ track_ed: null,
4771
+ kept_words: 0,
4772
+ total_words: totalWords,
4773
+ words: [],
4774
+ sortKey: 0
4775
+ });
4776
+ } else {
4777
+ instances.sort((a, b) => a.track_st - b.track_st);
4778
+ instances.forEach((inst, instIndex) => {
4779
+ entries.push({
4780
+ id: utt.id,
4781
+ text: utt.text,
4782
+ dropped: false,
4783
+ sourceIndex,
4784
+ instIndex,
4785
+ track_st: inst.track_st,
4786
+ track_ed: inst.track_ed,
4787
+ kept_words: inst.kept_words,
4788
+ total_words: totalWords,
4789
+ words: inst.words,
4790
+ sortKey: inst.track_st
4791
+ });
4792
+ });
4793
+ }
4794
+ });
4795
+ let maxEd = 0;
4796
+ for (const e of entries) {
4797
+ if (e.dropped)
4798
+ e.sortKey = maxEd;
4799
+ else
4800
+ maxEd = Math.max(maxEd, e.track_ed ?? maxEd);
4801
+ }
4802
+ entries.sort((a, b) => a.sortKey - b.sortKey || a.sourceIndex - b.sourceIndex || a.instIndex - b.instIndex);
4803
+ const utterances = entries.map((e) => {
4804
+ const u = {
4805
+ id: e.id,
4806
+ text: e.text,
4807
+ track_st: e.track_st,
4808
+ track_ed: e.track_ed,
4809
+ dropped: e.dropped,
4810
+ kept_words: e.kept_words,
4811
+ total_words: e.total_words
4812
+ };
4813
+ if (opts.words)
4814
+ u.words = e.words;
4815
+ return u;
4816
+ });
4817
+ return {
4818
+ transcript_hash: transcript.text_hash,
4819
+ projected_at: opts.projectedAt ?? new Date().toISOString(),
4820
+ utterances
4821
+ };
4822
+ }
4823
+
4824
+ // src/lib/reproject.ts
4825
+ function buildSpanIndex(view, utteranceIds) {
4826
+ const byId = new Map;
4827
+ for (const id of utteranceIds)
4828
+ byId.set(id, []);
4829
+ for (const u of view.utterances) {
4830
+ if (u.dropped || u.track_st == null || u.track_ed == null)
4831
+ continue;
4832
+ if (!byId.has(u.id))
4833
+ byId.set(u.id, []);
4834
+ byId.get(u.id).push({ track_st: u.track_st, track_ed: u.track_ed });
4835
+ }
4836
+ const idIndex = new Map;
4837
+ utteranceIds.forEach((id, i) => idIndex.set(id, i));
4838
+ return { utteranceIds, byId, idIndex };
4839
+ }
4840
+ function envelopeForSpan(index, span) {
4841
+ const fromIdx = index.idIndex.get(span.from);
4842
+ const toIdx = index.idIndex.get(span.to);
4843
+ if (fromIdx === undefined || toIdx === undefined)
4844
+ return { kind: "unresolved" };
4845
+ const spanIds = index.utteranceIds.slice(fromIdx, toIdx + 1);
4846
+ const instances = spanIds.flatMap((id) => index.byId.get(id) ?? []);
4847
+ if (instances.length === 0)
4848
+ return { kind: "dropped" };
4849
+ const droppedCount = spanIds.filter((id) => (index.byId.get(id)?.length ?? 0) === 0).length;
4850
+ return {
4851
+ kind: "ok",
4852
+ track_st: Math.min(...instances.map((x) => x.track_st)),
4853
+ track_ed: Math.max(...instances.map((x) => x.track_ed)),
4854
+ shrunk: droppedCount > 0,
4855
+ kept: spanIds.length - droppedCount,
4856
+ dropped: droppedCount
4857
+ };
4858
+ }
4859
+ function transcriptCandidates(baseDir) {
4860
+ return [
4861
+ join5(baseDir, "transcript", "transcript.json"),
4862
+ join5(baseDir, "json", "transcript.json"),
4863
+ join5(baseDir, "transcript.json")
4864
+ ];
4865
+ }
4866
+ var DEGRADE_TEXT = {
4867
+ no_project: "定位不到工程文件(.gtrk)",
4868
+ gtrk_unreadable: "工程文件读不出来(JSON 不可解析)",
4869
+ project_not_v1: "工程不是 v1(重投影不介入,版本门行为不变)",
4870
+ transcript_missing: "找不到 transcript.json",
4871
+ transcript_unreadable: "transcript.json 读不出来或结构异常",
4872
+ no_material_clip: "当刻主轨查不到命中口播素材(material_id)的 clip"
4873
+ };
4874
+ var ENTRY_DEGRADE_TEXT = {
4875
+ no_span: "派单条目无 span,且 struct_meta.split 里也定位不到(老档 + 工程账本对不上)",
4876
+ span_unresolved: "span 端点在当刻 transcript 里不存在(transcript 被换过)"
4877
+ };
4878
+ function r32(n) {
4879
+ return Math.round(n * 1000) / 1000;
4880
+ }
4881
+ function slugify(name) {
4882
+ const s = name.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "-").replace(/^-+|-+$/g, "");
4883
+ return s || "project";
4884
+ }
4885
+ function splitSpanIndex(gtrk) {
4886
+ const out = new Map;
4887
+ const split = gtrk?.struct_meta?.split;
4888
+ for (const b of split?.beats ?? []) {
4889
+ const span = b?.span;
4890
+ if (typeof b?.id !== "string" || typeof span?.from !== "string" || typeof span?.to !== "string")
4891
+ continue;
4892
+ out.set(b.id, { from: span.from, to: span.to });
4893
+ }
4894
+ return out;
4895
+ }
4896
+ function beatIdForComposition(compositionId, projectSlug, knownIds) {
4897
+ const prefix = `${projectSlug}-`;
4898
+ const ids = [...knownIds];
4899
+ if (compositionId.startsWith(prefix)) {
4900
+ const stripped = compositionId.slice(prefix.length);
4901
+ if (ids.includes(stripped))
4902
+ return stripped;
4903
+ }
4904
+ let best;
4905
+ for (const id of ids) {
4906
+ if (!compositionId.endsWith(`-${id}`))
4907
+ continue;
4908
+ if (best === undefined || id.length > best.length)
4909
+ best = id;
4910
+ }
4911
+ return best;
4912
+ }
4913
+ function loadTranscriptShape(raw) {
4914
+ let t;
4915
+ try {
4916
+ t = JSON.parse(raw);
4917
+ } catch {
4918
+ return;
4919
+ }
4920
+ const c3 = t;
4921
+ if (!c3 || !Array.isArray(c3.utterances) || typeof c3.material_id !== "string")
4922
+ return;
4923
+ return c3;
4924
+ }
4925
+ function degradeAll(entries, reason) {
4926
+ const outcomes = entries.map((e) => ({
4927
+ key: e.key,
4928
+ beat: e.beat,
4929
+ track_st: e.track_st,
4930
+ track_ed: e.track_ed,
4931
+ source: "dispatch_snapshot",
4932
+ drifted: false,
4933
+ offset: 0,
4934
+ shrunk: false,
4935
+ dropped: false
4936
+ }));
4937
+ return {
4938
+ summary: {
4939
+ mode: "dispatch_snapshot",
4940
+ degraded: true,
4941
+ reason,
4942
+ reason_text: DEGRADE_TEXT[reason],
4943
+ projected_at: null,
4944
+ total: entries.length,
4945
+ reprojected: 0,
4946
+ drifted: 0,
4947
+ max_offset: 0,
4948
+ shrunk: [],
4949
+ dropped: [],
4950
+ degraded_entries: []
4951
+ },
4952
+ entries: outcomes,
4953
+ windows: new Map(outcomes.map((o) => [o.key, { track_st: o.track_st, track_ed: o.track_ed }]))
4954
+ };
4955
+ }
4956
+ async function reprojectDispatchWindows(req) {
4957
+ const { baseDir, entries } = req;
4958
+ if (req.gtrkUnreadable)
4959
+ return degradeAll(entries, "gtrk_unreadable");
4960
+ if (!req.gtrk)
4961
+ return degradeAll(entries, "no_project");
4962
+ if (req.gtrk.version !== "v1")
4963
+ return degradeAll(entries, "project_not_v1");
4964
+ const transcriptPath = transcriptCandidates(baseDir).find((p) => existsSync5(p));
4965
+ if (!transcriptPath)
4966
+ return degradeAll(entries, "transcript_missing");
4967
+ let transcript;
4968
+ try {
4969
+ transcript = loadTranscriptShape(await readFile(transcriptPath, "utf8"));
4970
+ } catch {
4971
+ transcript = undefined;
4972
+ }
4973
+ if (!transcript)
4974
+ return degradeAll(entries, "transcript_unreadable");
4975
+ const gtrkProject = req.gtrk;
4976
+ if (countMainTrackMaterialClips(gtrkProject, transcript.material_id) === 0) {
4977
+ return degradeAll(entries, "no_material_clip");
4978
+ }
4979
+ const projectedAt = req.now ?? new Date().toISOString();
4980
+ const view = projectTranscript(transcript, gtrkProject, { projectedAt });
4981
+ const index = buildSpanIndex(view, transcript.utterances.map((u) => u.id));
4982
+ const fallback = splitSpanIndex(req.gtrk);
4983
+ const projectSlug = slugify(basename(baseDir));
4984
+ const outcomes = [];
4985
+ const shrunk = [];
4986
+ const dropped = [];
4987
+ const degradedEntries = [];
4988
+ let drifted = 0;
4989
+ let maxOffset = 0;
4990
+ let reprojected = 0;
4991
+ for (const e of entries) {
4992
+ let span = e.span;
4993
+ if (!span) {
4994
+ const bid = e.compositionId ? beatIdForComposition(e.compositionId, projectSlug, fallback.keys()) : e.beat;
4995
+ span = bid ? fallback.get(bid) : undefined;
4996
+ }
4997
+ if (!span) {
4998
+ outcomes.push({
4999
+ key: e.key,
5000
+ beat: e.beat,
5001
+ track_st: e.track_st,
5002
+ track_ed: e.track_ed,
5003
+ source: "dispatch_snapshot",
5004
+ drifted: false,
5005
+ offset: 0,
5006
+ shrunk: false,
5007
+ dropped: false,
5008
+ degradeReason: "no_span"
5009
+ });
5010
+ degradedEntries.push({ beat: e.beat, reason: "no_span", text: ENTRY_DEGRADE_TEXT.no_span });
5011
+ continue;
5012
+ }
5013
+ const env = envelopeForSpan(index, span);
5014
+ if (env.kind === "unresolved") {
5015
+ outcomes.push({
5016
+ key: e.key,
5017
+ beat: e.beat,
5018
+ track_st: e.track_st,
5019
+ track_ed: e.track_ed,
5020
+ source: "dispatch_snapshot",
5021
+ drifted: false,
5022
+ offset: 0,
5023
+ shrunk: false,
5024
+ dropped: false,
5025
+ degradeReason: "span_unresolved"
5026
+ });
5027
+ degradedEntries.push({ beat: e.beat, reason: "span_unresolved", text: ENTRY_DEGRADE_TEXT.span_unresolved });
5028
+ continue;
5029
+ }
5030
+ if (env.kind === "dropped") {
5031
+ reprojected += 1;
5032
+ dropped.push(e.beat);
5033
+ outcomes.push({
5034
+ key: e.key,
5035
+ beat: e.beat,
5036
+ track_st: e.track_st,
5037
+ track_ed: e.track_ed,
5038
+ source: "reprojected",
5039
+ drifted: false,
5040
+ offset: 0,
5041
+ shrunk: false,
5042
+ dropped: true
5043
+ });
5044
+ continue;
5045
+ }
5046
+ reprojected += 1;
5047
+ const offset = r32(Math.max(Math.abs(env.track_st - e.track_st), Math.abs(env.track_ed - e.track_ed)));
5048
+ const isDrifted = offset > 0;
5049
+ if (isDrifted) {
5050
+ drifted += 1;
5051
+ maxOffset = Math.max(maxOffset, offset);
5052
+ }
5053
+ if (env.shrunk)
5054
+ shrunk.push(e.beat);
5055
+ outcomes.push({
5056
+ key: e.key,
5057
+ beat: e.beat,
5058
+ track_st: env.track_st,
5059
+ track_ed: env.track_ed,
5060
+ source: "reprojected",
5061
+ drifted: isDrifted,
5062
+ offset,
5063
+ shrunk: env.shrunk,
5064
+ dropped: false
5065
+ });
5066
+ }
5067
+ return {
5068
+ summary: {
5069
+ mode: "reprojected",
5070
+ degraded: degradedEntries.length > 0,
5071
+ projected_at: projectedAt,
5072
+ total: entries.length,
5073
+ reprojected,
5074
+ drifted,
5075
+ max_offset: r32(maxOffset),
5076
+ shrunk,
5077
+ dropped,
5078
+ degraded_entries: degradedEntries
5079
+ },
5080
+ entries: outcomes,
5081
+ windows: new Map(outcomes.map((o) => [o.key, { track_st: o.track_st, track_ed: o.track_ed }]))
5082
+ };
5083
+ }
5084
+ function reportReprojection(reproj) {
5085
+ const s = reproj.summary;
5086
+ if (s.mode === "dispatch_snapshot") {
5087
+ log.warn(reprojectSummaryLine(s));
5088
+ log.warn(`正在用**可能已过期**的派单快照时码继续。${reprojectRecoveryHint(s.reason)}`);
5089
+ return;
5090
+ }
5091
+ log.step(`▶ ${reprojectSummaryLine(s)}`);
5092
+ for (const d of s.degraded_entries)
5093
+ log.warn(`${d.beat}:无法重投影(${d.text}),本条回退派单快照时码`);
5094
+ for (const b of s.shrunk)
5095
+ log.warn(`${b}:span 内有句被剪,已按存活包络收缩(建议人工复核)`);
5096
+ for (const b of s.dropped)
5097
+ log.warn(`${b}:重投影后零存活(整段已被剪出成片),本条跳过——不按快照时码铺回去`);
5098
+ }
5099
+ function withTimecodeSource(next, key, reproj) {
5100
+ const structMeta = next.struct_meta ?? {};
5101
+ const ledger = structMeta[key];
5102
+ if (!ledger)
5103
+ return next;
5104
+ return {
5105
+ ...next,
5106
+ struct_meta: {
5107
+ ...structMeta,
5108
+ [key]: {
5109
+ ...ledger,
5110
+ timecode_source: reproj.summary.mode === "reprojected" ? "reprojected" : "dispatch_snapshot",
5111
+ ...reproj.summary.projected_at ? { reprojected_at: reproj.summary.projected_at } : {},
5112
+ ...reproj.summary.reason ? { timecode_degrade_reason: reproj.summary.reason } : {}
5113
+ }
5114
+ }
5115
+ };
5116
+ }
5117
+ function reprojectSummaryLine(s) {
5118
+ if (s.mode === "dispatch_snapshot") {
5119
+ return `重投影降级:按派单快照时码铺 ${s.total} 个条目(原因:${s.reason_text ?? s.reason})——快照可能已过期`;
5120
+ }
5121
+ const bits = [`重投影 ${s.reprojected} 个 beat`, `漂移 ${s.drifted} 条`];
5122
+ if (s.drifted > 0)
5123
+ bits.push(`最大偏移 ${s.max_offset}s`);
5124
+ if (s.shrunk.length)
5125
+ bits.push(`收缩 ${s.shrunk.length} 条`);
5126
+ if (s.dropped.length)
5127
+ bits.push(`整段被剪跳过 ${s.dropped.length} 条`);
5128
+ if (s.degraded_entries.length)
5129
+ bits.push(`逐条降级 ${s.degraded_entries.length} 条`);
5130
+ return bits.join(" · ");
5131
+ }
5132
+ function reprojectRecoveryHint(reason) {
5133
+ switch (reason) {
5134
+ case "transcript_missing":
5135
+ case "transcript_unreadable":
5136
+ return "复原:把 transcript.json 补回产物目录(transcript/ 或 json/ 下),或用新版 gtrk oralcut 重出产物后重跑本命令。";
5137
+ case "no_project":
5138
+ case "gtrk_unreadable":
5139
+ return "复原:把工程放回 <产物目录>/gtrk/project.gtrk(或用 --project 指到工程所在的产物目录)后重跑本命令。";
5140
+ case "project_not_v1":
5141
+ return "复原:用新链路重产 v1 工程后重跑本命令。";
5142
+ case "no_material_clip":
5143
+ return "复原:确认口播主轨还在、且 relink 没换掉素材 id;拿另一个工程的 dispatch 来跑也会命中本情形。";
5144
+ }
5145
+ }
4695
5146
 
4696
5147
  // src/lib/splitdoc.ts
4697
5148
  var BASE_TRACKS = ["真人出镜", "口播继续", "旁白主导"];
@@ -4930,22 +5381,11 @@ var CATEGORY_EXPECTED_OPAQUE = {
4930
5381
  function isKnownCategory(v) {
4931
5382
  return typeof v === "string" && Object.prototype.hasOwnProperty.call(CATEGORY_EXPECTED_OPAQUE, v);
4932
5383
  }
4933
- function r3(n) {
5384
+ function r33(n) {
4934
5385
  return Math.round(n * 1000) / 1000;
4935
5386
  }
4936
5387
  function buildLanding(doc, view, opts) {
4937
- const byId = new Map;
4938
- for (const id of opts.utteranceIds)
4939
- byId.set(id, []);
4940
- for (const u of view.utterances) {
4941
- if (u.dropped || u.track_st == null || u.track_ed == null)
4942
- continue;
4943
- if (!byId.has(u.id))
4944
- byId.set(u.id, []);
4945
- byId.get(u.id).push({ track_st: u.track_st, track_ed: u.track_ed });
4946
- }
4947
- const idIndex = new Map;
4948
- opts.utteranceIds.forEach((id, i) => idIndex.set(id, i));
5388
+ const spanIndex = buildSpanIndex(view, opts.utteranceIds);
4949
5389
  const split = {
4950
5390
  contract_version: doc.contract_version,
4951
5391
  transcript_hash: doc.transcript_hash,
@@ -4958,25 +5398,19 @@ function buildLanding(doc, view, opts) {
4958
5398
  const shrunk = [];
4959
5399
  const unhandledLanes = new Set;
4960
5400
  for (const beat of doc.beats) {
4961
- const fromIdx = idIndex.get(beat.span.from);
4962
- const toIdx = idIndex.get(beat.span.to);
4963
- const spanIds = opts.utteranceIds.slice(fromIdx, toIdx + 1);
4964
- const instances = spanIds.flatMap((id) => byId.get(id) ?? []);
4965
- const droppedCount = spanIds.filter((id) => (byId.get(id)?.length ?? 0) === 0).length;
4966
- if (instances.length === 0) {
5401
+ const env = envelopeForSpan(spanIndex, beat.span);
5402
+ if (env.kind !== "ok") {
4967
5403
  skipped.push({ beat: beat.id, reason: "span 内全部 utterance 被剪,未落轨" });
4968
5404
  continue;
4969
5405
  }
4970
- const track_st = Math.min(...instances.map((x) => x.track_st));
4971
- const track_ed = Math.max(...instances.map((x) => x.track_ed));
4972
- const isShrunk = droppedCount > 0;
5406
+ const { track_st, track_ed, shrunk: isShrunk } = env;
4973
5407
  const lane = normalizeLane(beat.lane) ?? beat.lane;
4974
5408
  const metaBeat = { id: beat.id, lane, span: beat.span, track_st, track_ed };
4975
5409
  if (opts.sourceIndex) {
4976
5410
  const from = opts.sourceIndex.utterances.get(beat.span.from);
4977
5411
  const to = opts.sourceIndex.utterances.get(beat.span.to);
4978
5412
  if (from && to && to.ed > from.st) {
4979
- metaBeat.source_ranges = [{ st: r3(from.st), ed: r3(to.ed) }];
5413
+ metaBeat.source_ranges = [{ st: r33(from.st), ed: r33(to.ed) }];
4980
5414
  }
4981
5415
  }
4982
5416
  if (isShrunk)
@@ -4993,7 +5427,7 @@ function buildLanding(doc, view, opts) {
4993
5427
  metaBeat.handoff = beat.handoff;
4994
5428
  split.beats.push(metaBeat);
4995
5429
  if (isShrunk) {
4996
- shrunk.push({ beat: beat.id, kept: spanIds.length - droppedCount, dropped: droppedCount, track_st, track_ed });
5430
+ shrunk.push({ beat: beat.id, kept: env.kept, dropped: env.dropped, track_st, track_ed });
4997
5431
  }
4998
5432
  const h = beat.handoff ?? {};
4999
5433
  const compositionId = `${opts.projectSlug}-${beat.id}`;
@@ -5001,14 +5435,15 @@ function buildLanding(doc, view, opts) {
5001
5435
  dispatch.mg.push({
5002
5436
  beat: beat.id,
5003
5437
  composition_id: compositionId,
5004
- duration: r3(track_ed - track_st),
5438
+ duration: r33(track_ed - track_st),
5005
5439
  duration_hint: typeof h.duration_hint === "number" ? h.duration_hint : null,
5006
5440
  ...h.category !== undefined ? { category: h.category } : {},
5007
5441
  theme: h.theme,
5008
5442
  bg: h.bg,
5009
5443
  slug_hint: h.slug_hint,
5010
5444
  track_st,
5011
- track_ed
5445
+ track_ed,
5446
+ span: beat.span
5012
5447
  });
5013
5448
  } else if (lane === "FILM_BROLL") {
5014
5449
  dispatch.film_broll.push({
@@ -5018,10 +5453,11 @@ function buildLanding(doc, view, opts) {
5018
5453
  per_shot_sec: h.per_shot_sec,
5019
5454
  exclude: h.exclude,
5020
5455
  track_st,
5021
- track_ed
5456
+ track_ed,
5457
+ span: beat.span
5022
5458
  });
5023
5459
  } else if (lane === "AI_DRAMA") {
5024
- dispatch.ai_drama.push({ beat: beat.id, ...h, track_st, track_ed });
5460
+ dispatch.ai_drama.push({ beat: beat.id, ...h, track_st, track_ed, span: beat.span });
5025
5461
  } else if (lane !== "A_ROLL") {
5026
5462
  unhandledLanes.add(lane);
5027
5463
  }
@@ -5044,34 +5480,33 @@ function buildLanding(doc, view, opts) {
5044
5480
  skipped.push({ beat: auxTag, reason: "overlay aux 使用 {trigger} 点挂载,一期不支持(二期补合成窗口)" });
5045
5481
  continue;
5046
5482
  }
5047
- const auxFromIdx = idIndex.get(auxFromId);
5048
- const auxToIdx = idIndex.get(auxToId);
5049
- const auxSpanIds = opts.utteranceIds.slice(auxFromIdx, auxToIdx + 1);
5050
- const auxInstances = auxSpanIds.flatMap((id) => byId.get(id) ?? []);
5051
- if (auxInstances.length === 0) {
5483
+ const auxSpan = { from: auxFromId, to: auxToId };
5484
+ const auxEnv = envelopeForSpan(spanIndex, auxSpan);
5485
+ if (auxEnv.kind !== "ok") {
5052
5486
  skipped.push({ beat: auxTag, reason: "overlay aux 源区间 utterance 全被剪,未落轨" });
5053
5487
  continue;
5054
5488
  }
5055
- const auxTrackSt = Math.min(...auxInstances.map((x) => x.track_st));
5056
- const auxTrackEd = Math.max(...auxInstances.map((x) => x.track_ed));
5489
+ const auxTrackSt = auxEnv.track_st;
5490
+ const auxTrackEd = auxEnv.track_ed;
5057
5491
  const auxCompositionId = `${opts.projectSlug}-${beat.id}-aux${auxN}`;
5058
5492
  const ah = aux.handoff;
5059
5493
  dispatch.mg.push({
5060
5494
  beat: beat.id,
5061
5495
  composition_id: auxCompositionId,
5062
- duration: r3(auxTrackEd - auxTrackSt),
5496
+ duration: r33(auxTrackEd - auxTrackSt),
5063
5497
  duration_hint: ah && typeof ah.duration_hint === "number" ? ah.duration_hint : null,
5064
5498
  category: "overlay",
5065
5499
  theme: ah?.theme,
5066
5500
  bg: ah?.bg,
5067
5501
  slug_hint: ah?.slug_hint,
5068
5502
  track_st: auxTrackSt,
5069
- track_ed: auxTrackEd
5503
+ track_ed: auxTrackEd,
5504
+ span: auxSpan
5070
5505
  });
5071
5506
  const auxMetaBeat = {
5072
5507
  id: auxTag,
5073
5508
  lane: "MG",
5074
- span: { from: auxFromId, to: auxToId },
5509
+ span: auxSpan,
5075
5510
  track_st: auxTrackSt,
5076
5511
  track_ed: auxTrackEd,
5077
5512
  category: "overlay"
@@ -5080,7 +5515,7 @@ function buildLanding(doc, view, opts) {
5080
5515
  const sfrom = opts.sourceIndex.utterances.get(auxFromId);
5081
5516
  const sto = opts.sourceIndex.utterances.get(auxToId);
5082
5517
  if (sfrom && sto && sto.ed > sfrom.st) {
5083
- auxMetaBeat.source_ranges = [{ st: r3(sfrom.st), ed: r3(sto.ed) }];
5518
+ auxMetaBeat.source_ranges = [{ st: r33(sfrom.st), ed: r33(sto.ed) }];
5084
5519
  }
5085
5520
  }
5086
5521
  split.beats.push(auxMetaBeat);
@@ -5161,7 +5596,7 @@ var DEFAULT_COLUMN_CONFIG = {
5161
5596
  fallback: { unknown_narrative: "reject" }
5162
5597
  };
5163
5598
  function columnsDir() {
5164
- return join5(gitruckHome(), "columns");
5599
+ return join6(gitruckHome(), "columns");
5165
5600
  }
5166
5601
  var uniq = (xs) => [...new Set(xs)];
5167
5602
  function strArr(v) {
@@ -5218,8 +5653,8 @@ function foldColumnConfigs(layers) {
5218
5653
  return out;
5219
5654
  }
5220
5655
  function readLocalColumn(columnId, dir, warnings) {
5221
- const p = join5(dir, `${columnId}.json`);
5222
- if (!existsSync5(p)) {
5656
+ const p = join6(dir, `${columnId}.json`);
5657
+ if (!existsSync6(p)) {
5223
5658
  warnings.push(`栏目配置不存在:${p},回落内置默认`);
5224
5659
  return;
5225
5660
  }
@@ -5353,8 +5788,8 @@ function effectiveVocab(config) {
5353
5788
 
5354
5789
  // src/lib/ffmpeg.ts
5355
5790
  import { spawn as spawn2, spawnSync as spawnSync3 } from "node:child_process";
5356
- import { existsSync as existsSync6 } from "node:fs";
5357
- import { join as join6 } from "node:path";
5791
+ import { existsSync as existsSync7 } from "node:fs";
5792
+ import { join as join7 } from "node:path";
5358
5793
  var isWin = process.platform === "win32";
5359
5794
  var bin = (base) => isWin ? `${base}.exe` : base;
5360
5795
  var FFMPEG_INSTALL_HINT = `未找到 ffmpeg/ffprobe。请把二者放到 ${ffmpegDir()}(agent 可代办:先查本地确实缺失才拉,` + `面向国内用户优先国内加速站点——GitHub 代理 pass-through 拉 BtbN/gyan.dev 官方静态构建,或同合云自建镜像,` + `并做 sha256 校验),或用 --ffmpeg-path <目录> 指定已装位置。`;
@@ -5376,9 +5811,9 @@ function resolveFfmpeg(ffmpegPath) {
5376
5811
  dirs.push([ffmpegDir(), "~/.gitruck/ffmpeg"]);
5377
5812
  let found = null;
5378
5813
  for (const [dir, label] of dirs) {
5379
- const ff = join6(dir, bin("ffmpeg"));
5380
- const fp = join6(dir, bin("ffprobe"));
5381
- if (existsSync6(ff) && existsSync6(fp)) {
5814
+ const ff = join7(dir, bin("ffmpeg"));
5815
+ const fp = join7(dir, bin("ffprobe"));
5816
+ if (existsSync7(ff) && existsSync7(fp)) {
5382
5817
  found = { ffmpeg: ff, ffprobe: fp, source: label };
5383
5818
  break;
5384
5819
  }
@@ -5460,11 +5895,11 @@ function probeCapabilities(res) {
5460
5895
 
5461
5896
  // src/lib/version.ts
5462
5897
  import { readFileSync as readFileSync3 } from "node:fs";
5463
- import { join as join7 } from "node:path";
5898
+ import { join as join8 } from "node:path";
5464
5899
  var REGISTRY = "https://registry.npmjs.org/@gitruck%2Fcli";
5465
5900
  function currentVersion() {
5466
5901
  try {
5467
- const { version } = JSON.parse(readFileSync3(join7(packageRoot(), "package.json"), "utf8"));
5902
+ const { version } = JSON.parse(readFileSync3(join8(packageRoot(), "package.json"), "utf8"));
5468
5903
  return version;
5469
5904
  } catch {
5470
5905
  return "0.0.0";
@@ -5546,7 +5981,7 @@ async function runDoctor() {
5546
5981
  }
5547
5982
  rows.push({ name: "云端连通 + 鉴权", status: apiStatus, detail: apiDetail });
5548
5983
  const draftDir = resolveJianyingDraftDir(undefined);
5549
- const draftOk = !!draftDir && existsSync7(draftDir);
5984
+ const draftOk = !!draftDir && existsSync8(draftDir);
5550
5985
  rows.push({
5551
5986
  name: "剪映草稿目录",
5552
5987
  status: draftOk ? "ok" : "warn",
@@ -5554,15 +5989,15 @@ async function runDoctor() {
5554
5989
  });
5555
5990
  rows.push({
5556
5991
  name: "配置文件",
5557
- status: existsSync7(configPath()) ? "ok" : "warn",
5558
- detail: existsSync7(configPath()) ? configPath() : `未生成 —— 跑 gtrk init(${configPath()})`
5992
+ status: existsSync8(configPath()) ? "ok" : "warn",
5993
+ detail: existsSync8(configPath()) ? configPath() : `未生成 —— 跑 gtrk init(${configPath()})`
5559
5994
  });
5560
5995
  const col = uc.defaultColumn;
5561
- const colFile = col ? join8(columnsDir(), `${col}.json`) : undefined;
5996
+ const colFile = col ? join9(columnsDir(), `${col}.json`) : undefined;
5562
5997
  rows.push({
5563
5998
  name: "当前栏目",
5564
5999
  status: "ok",
5565
- detail: col ? `${col}${colFile && existsSync7(colFile) ? `(${colFile})` : `(⚠ 配置文件缺失:${colFile},将回落内置默认)`}` : "内置默认 —— 想建自己栏目的风格体系,跑 /gtrk-style-maker(不建也能直接用默认)"
6000
+ detail: col ? `${col}${colFile && existsSync8(colFile) ? `(${colFile})` : `(⚠ 配置文件缺失:${colFile},将回落内置默认)`}` : "内置默认 —— 想建自己栏目的风格体系,跑 /gtrk-style-maker(不建也能直接用默认)"
5566
6001
  });
5567
6002
  const ff = resolveFfmpeg();
5568
6003
  if (ff) {
@@ -5609,7 +6044,7 @@ gtrk 体检:
5609
6044
  }
5610
6045
 
5611
6046
  // src/commands/init.ts
5612
- var GUIDE_IMAGE = join9(packageRoot(), "assets", "jianying-draft-path.png");
6047
+ var GUIDE_IMAGE = join10(packageRoot(), "assets", "jianying-draft-path.png");
5613
6048
  function registerInit(program2) {
5614
6049
  program2.command("init").description("一次性配置:API Key + 剪映草稿目录(之后所有命令免重复配置)").option("--api-key <key>", "非交互:直接指定 API Key").option("--api-base <url>", "非交互:指定 API 根地址(缺省用默认生产地址)").option("--jianying-draft-dir <dir>", "非交互:剪映草稿目录(传 auto 则自动探测)").option("--reconfigure", "重走配置向导(默认:已配过则跳过、保留现有配置)").option("-y, --yes", "非交互:用传入值 + 自动探测,不弹任何提示").action(runInit);
5615
6050
  }
@@ -5648,7 +6083,7 @@ async function runInit(opts) {
5648
6083
  defaultValue: existing.apiBase ?? DEFAULT_API_BASE
5649
6084
  })).trim();
5650
6085
  let jianyingDraftDir;
5651
- if (existing.jianyingDraftDir && existsSync8(existing.jianyingDraftDir)) {
6086
+ if (existing.jianyingDraftDir && existsSync9(existing.jianyingDraftDir)) {
5652
6087
  if (await promptConfirm(`剪映草稿目录现为 ${existing.jianyingDraftDir},保留吗?`, true)) {
5653
6088
  jianyingDraftDir = existing.jianyingDraftDir;
5654
6089
  }
@@ -5664,7 +6099,7 @@ async function runInit(opts) {
5664
6099
  openFile(GUIDE_IMAGE);
5665
6100
  const manual = (await promptText("剪映草稿根目录(…\\com.lveditor.draft),留空跳过:")).trim();
5666
6101
  if (manual) {
5667
- if (existsSync8(manual))
6102
+ if (existsSync9(manual))
5668
6103
  jianyingDraftDir = resolve3(manual);
5669
6104
  else
5670
6105
  log.warn(`目录不存在,已跳过:${manual}`);
@@ -5738,9 +6173,9 @@ function registerInstall(program2) {
5738
6173
  }
5739
6174
 
5740
6175
  // src/commands/oralcut.ts
5741
- import { resolve as resolve4, join as join14, dirname as dirname3, basename as basename5, extname as extname2 } from "node:path";
5742
- import { mkdir as mkdir4, writeFile as writeFile5, readFile as readFile3 } from "node:fs/promises";
5743
- import { existsSync as existsSync12 } from "node:fs";
6176
+ import { resolve as resolve4, join as join15, dirname as dirname3, basename as basename6, extname as extname2 } from "node:path";
6177
+ import { mkdir as mkdir4, writeFile as writeFile5, readFile as readFile4 } from "node:fs/promises";
6178
+ import { existsSync as existsSync13 } from "node:fs";
5744
6179
 
5745
6180
  // src/lib/config.ts
5746
6181
  function loadConfig() {
@@ -5754,7 +6189,7 @@ function loadConfig() {
5754
6189
  }
5755
6190
 
5756
6191
  // src/lib/cloud.ts
5757
- import { basename } from "node:path";
6192
+ import { basename as basename2 } from "node:path";
5758
6193
  import { writeFile, stat } from "node:fs/promises";
5759
6194
  import { createReadStream } from "node:fs";
5760
6195
  import { Readable } from "node:stream";
@@ -5792,7 +6227,7 @@ async function uploadFile(cfg, path, runtime = {}) {
5792
6227
  if (!bunFile)
5793
6228
  throw new Error("Bun 上传运行时缺少 Bun.file");
5794
6229
  const form = new FormData;
5795
- form.append("file", bunFile(path), basename(path));
6230
+ form.append("file", bunFile(path), basename2(path));
5796
6231
  res = await fetchFn(`${cfg.base}/base/file/upload`, {
5797
6232
  method: "POST",
5798
6233
  headers: { Authorization: cfg.apiKey },
@@ -5802,7 +6237,7 @@ async function uploadFile(cfg, path, runtime = {}) {
5802
6237
  const size = (await stat(path)).size;
5803
6238
  const boundary = `----gtrkFormBoundary${randomBytes(16).toString("hex")}`;
5804
6239
  const head = Buffer.from(`--${boundary}\r
5805
- ` + `Content-Disposition: form-data; name="file"; filename="${basename(path)}"\r
6240
+ ` + `Content-Disposition: form-data; name="file"; filename="${basename2(path)}"\r
5806
6241
  ` + `Content-Type: application/octet-stream\r
5807
6242
  \r
5808
6243
  `, "utf8");
@@ -5893,14 +6328,14 @@ async function download(url, dest) {
5893
6328
  }
5894
6329
 
5895
6330
  // src/lib/upload-cache.ts
5896
- import { join as join10 } from "node:path";
5897
- import { stat as stat3, mkdir, readFile, writeFile as writeFile2 } from "node:fs/promises";
5898
- import { existsSync as existsSync9 } from "node:fs";
6331
+ import { join as join11 } from "node:path";
6332
+ import { stat as stat3, mkdir, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
6333
+ import { existsSync as existsSync10 } from "node:fs";
5899
6334
 
5900
6335
  // src/lib/chunk-upload.ts
5901
6336
  var import_hash_wasm = __toESM(require_index_umd(), 1);
5902
6337
  import { open, stat as stat2 } from "node:fs/promises";
5903
- import { basename as basename2 } from "node:path";
6338
+ import { basename as basename3 } from "node:path";
5904
6339
  var CHUNK_THRESHOLD = 256 * 1024 * 1024;
5905
6340
  var CONCURRENCY = 3;
5906
6341
  var PART_RETRIES = 3;
@@ -5962,7 +6397,7 @@ async function sleep(ms) {
5962
6397
  }
5963
6398
  async function uploadChunked(cfg, path, opts) {
5964
6399
  const size = (await stat2(path)).size;
5965
- const name = basename2(path);
6400
+ const name = basename3(path);
5966
6401
  for (let rebuilds = 0;; rebuilds++) {
5967
6402
  try {
5968
6403
  return await attemptOnce(cfg, path, name, size, opts);
@@ -6127,8 +6562,8 @@ async function putPart(cfg, uploadId, idx, view) {
6127
6562
 
6128
6563
  // src/lib/upload-cache.ts
6129
6564
  var CACHE_DIR = gitruckHome();
6130
- var CACHE_FILE = join10(CACHE_DIR, "upload-cache.json");
6131
- var SESSION_FILE = join10(CACHE_DIR, "upload-sessions.json");
6565
+ var CACHE_FILE = join11(CACHE_DIR, "upload-cache.json");
6566
+ var SESSION_FILE = join11(CACHE_DIR, "upload-sessions.json");
6132
6567
  function fingerprintFromStat(s) {
6133
6568
  return `${s.size}:${Math.round(s.mtimeMs)}`;
6134
6569
  }
@@ -6136,10 +6571,10 @@ async function fingerprint(path) {
6136
6571
  return fingerprintFromStat(await stat3(path));
6137
6572
  }
6138
6573
  async function load() {
6139
- if (!existsSync9(CACHE_FILE))
6574
+ if (!existsSync10(CACHE_FILE))
6140
6575
  return {};
6141
6576
  try {
6142
- return JSON.parse(await readFile(CACHE_FILE, "utf8"));
6577
+ return JSON.parse(await readFile2(CACHE_FILE, "utf8"));
6143
6578
  } catch {
6144
6579
  return {};
6145
6580
  }
@@ -6163,10 +6598,10 @@ async function invalidateUpload(path) {
6163
6598
  }
6164
6599
  }
6165
6600
  async function loadSessions() {
6166
- if (!existsSync9(SESSION_FILE))
6601
+ if (!existsSync10(SESSION_FILE))
6167
6602
  return {};
6168
6603
  try {
6169
- return JSON.parse(await readFile(SESSION_FILE, "utf8"));
6604
+ return JSON.parse(await readFile2(SESSION_FILE, "utf8"));
6170
6605
  } catch {
6171
6606
  return {};
6172
6607
  }
@@ -6261,8 +6696,8 @@ async function uploadAndSubmitTask(cfg, path, taskType, buildPayload, options =
6261
6696
 
6262
6697
  // src/lib/media.ts
6263
6698
  import { mkdir as mkdir2, stat as stat4 } from "node:fs/promises";
6264
- import { existsSync as existsSync10 } from "node:fs";
6265
- import { basename as basename3, extname, join as join11 } from "node:path";
6699
+ import { existsSync as existsSync11 } from "node:fs";
6700
+ import { basename as basename4, extname, join as join12 } from "node:path";
6266
6701
  function parseFps(rate) {
6267
6702
  if (typeof rate !== "string")
6268
6703
  return 0;
@@ -6310,14 +6745,14 @@ function probeDuration(path, ffmpegPath) {
6310
6745
  }
6311
6746
  async function artifactPath(inputAbs, ext) {
6312
6747
  const s = await stat4(inputAbs);
6313
- const base = basename3(inputAbs, extname(inputAbs));
6748
+ const base = basename4(inputAbs, extname(inputAbs));
6314
6749
  await mkdir2(audioCacheDir(), { recursive: true });
6315
- return join11(audioCacheDir(), `${base}.${s.size}_${Math.round(s.mtimeMs)}.${ext}`);
6750
+ return join12(audioCacheDir(), `${base}.${s.size}_${Math.round(s.mtimeMs)}.${ext}`);
6316
6751
  }
6317
6752
  async function extractAudio(inputAbs, ffmpegPath) {
6318
6753
  const { ffmpeg } = requireFfmpeg(ffmpegPath);
6319
6754
  const out = await artifactPath(inputAbs, "mp3");
6320
- if (existsSync10(out))
6755
+ if (existsSync11(out))
6321
6756
  return out;
6322
6757
  await runFfmpeg(ffmpeg, [
6323
6758
  "-y",
@@ -6341,7 +6776,7 @@ async function extractAudio(inputAbs, ffmpegPath) {
6341
6776
  async function compress720p(inputAbs, ffmpegPath) {
6342
6777
  const { ffmpeg } = requireFfmpeg(ffmpegPath);
6343
6778
  const out = await artifactPath(inputAbs, "720p.mp4");
6344
- if (existsSync10(out))
6779
+ if (existsSync11(out))
6345
6780
  return out;
6346
6781
  await runFfmpeg(ffmpeg, [
6347
6782
  "-y",
@@ -6373,14 +6808,14 @@ function assertDurationConsistent(originalDuration, artifactAbs, ffmpegPath, tol
6373
6808
  }
6374
6809
 
6375
6810
  // src/lib/materialize.ts
6376
- import { join as join13, basename as basename4 } from "node:path";
6811
+ import { join as join14, basename as basename5 } from "node:path";
6377
6812
  import { mkdir as mkdir3, cp, writeFile as writeFile4 } from "node:fs/promises";
6378
6813
 
6379
6814
  // src/lib/render.ts
6380
- import { writeFile as writeFile3, unlink, readFile as readFile2 } from "node:fs/promises";
6381
- import { existsSync as existsSync11 } from "node:fs";
6815
+ import { writeFile as writeFile3, unlink, readFile as readFile3 } from "node:fs/promises";
6816
+ import { existsSync as existsSync12 } from "node:fs";
6382
6817
  import { tmpdir } from "node:os";
6383
- import { join as join12 } from "node:path";
6818
+ import { join as join13 } from "node:path";
6384
6819
  var AUDIO_SAMPLE_RATE = 48000;
6385
6820
  var AUDIO_LAYOUT = "stereo";
6386
6821
  var DEFAULT_CRF = 18;
@@ -6538,7 +6973,7 @@ function materialPathsFromGtrk(gtrk) {
6538
6973
  continue;
6539
6974
  if (!m.path)
6540
6975
  throw new Error(`gtrk 素材 ${m.id} 缺 path(source_path),无法本地渲染`);
6541
- if (!existsSync11(m.path))
6976
+ if (!existsSync12(m.path))
6542
6977
  throw new Error(`gtrk 素材文件不存在:${m.path}`);
6543
6978
  map[String(m.id)] = m.path;
6544
6979
  }
@@ -6552,7 +6987,7 @@ async function renderGtrk(gtrk, outputPath, opts = {}) {
6552
6987
  const { ffmpeg } = requireFfmpeg(opts.ffmpegPath);
6553
6988
  const materialPaths = materialPathsFromGtrk(gtrk);
6554
6989
  const { inputs, graph, total } = buildFilterGraph(gtrk, materialPaths, { crf });
6555
- const filterFile = join12(tmpdir(), `gtrk-filter-${process.pid}-${inputs.length}.txt`);
6990
+ const filterFile = join13(tmpdir(), `gtrk-filter-${process.pid}-${inputs.length}.txt`);
6556
6991
  await writeFile3(filterFile, graph, "utf8");
6557
6992
  try {
6558
6993
  const args = ["-y"];
@@ -6566,7 +7001,7 @@ async function renderGtrk(gtrk, outputPath, opts = {}) {
6566
7001
  }
6567
7002
  }
6568
7003
  async function readGtrkFile(gtrkPath) {
6569
- return JSON.parse(await readFile2(gtrkPath, "utf8"));
7004
+ return JSON.parse(await readFile3(gtrkPath, "utf8"));
6570
7005
  }
6571
7006
 
6572
7007
  // src/lib/materialize.ts
@@ -6590,7 +7025,7 @@ function gtrkSourceName(gtrk) {
6590
7025
  const p = gtrk.materials?.[0]?.path;
6591
7026
  if (!p)
6592
7027
  return;
6593
- const b = basename4(p);
7028
+ const b = basename5(p);
6594
7029
  const dot = b.lastIndexOf(".");
6595
7030
  return dot > 0 ? b.slice(0, dot) : b;
6596
7031
  }
@@ -6602,7 +7037,7 @@ async function materializeResult(opts) {
6602
7037
  throw new Error("任务无工程文件产物(检查 project_formats / 任务是否产出)");
6603
7038
  const errors = { ...output.errors ?? {} };
6604
7039
  await mkdir3(outDir, { recursive: true });
6605
- const resultPath = join13(outDir, "result.json");
7040
+ const resultPath = join14(outDir, "result.json");
6606
7041
  const writeResult = async (extra) => {
6607
7042
  const r = {
6608
7043
  ok: Object.keys(errors).length === 0,
@@ -6624,9 +7059,9 @@ async function materializeResult(opts) {
6624
7059
  const byFormat = {};
6625
7060
  for (const f of files) {
6626
7061
  const base = baseFormat(f.format);
6627
- const fmtDir = join13(outDir, base);
7062
+ const fmtDir = join14(outDir, base);
6628
7063
  await mkdir3(fmtDir, { recursive: true });
6629
- const dest = join13(fmtDir, f.filename);
7064
+ const dest = join14(fmtDir, f.filename);
6630
7065
  try {
6631
7066
  await dl(f.download_url, dest);
6632
7067
  (byFormat[base] ??= []).push(dest);
@@ -6643,9 +7078,9 @@ async function materializeResult(opts) {
6643
7078
  let jianyingDraftPath = null;
6644
7079
  if (byFormat.jianying && opts.draftDir) {
6645
7080
  try {
6646
- jianyingDraftPath = join13(opts.draftDir, basename4(outDir));
7081
+ jianyingDraftPath = join14(opts.draftDir, basename5(outDir));
6647
7082
  await mkdir3(jianyingDraftPath, { recursive: true });
6648
- await cp(join13(outDir, "jianying"), jianyingDraftPath, { recursive: true });
7083
+ await cp(join14(outDir, "jianying"), jianyingDraftPath, { recursive: true });
6649
7084
  log.info(`剪映草稿已落到:${jianyingDraftPath}`);
6650
7085
  } catch (e) {
6651
7086
  jianyingDraftPath = null;
@@ -6662,7 +7097,7 @@ async function materializeResult(opts) {
6662
7097
  log.step("本地渲染成片(ffmpeg)…");
6663
7098
  const project = await readGtrkFile(gtrkPath);
6664
7099
  const name = opts.projName ?? gtrkSourceName(project) ?? taskId;
6665
- const outMp4 = join13(outDir, `${name}.mp4`);
7100
+ const outMp4 = join14(outDir, `${name}.mp4`);
6666
7101
  const r = await renderGtrk(project, outMp4, {
6667
7102
  crf: opts.crf != null ? Number(opts.crf) : undefined,
6668
7103
  codec: opts.codec,
@@ -6684,7 +7119,7 @@ async function materializeResult(opts) {
6684
7119
  log.step("三方打开(产物已就位,按需自取):");
6685
7120
  for (const base of Object.keys(byFormat)) {
6686
7121
  const meta = FORMAT_META[base];
6687
- const target = base === "jianying" ? jianyingDraftPath ?? join13(outDir, "jianying") : byFormat[base][0];
7122
+ const target = base === "jianying" ? jianyingDraftPath ?? join14(outDir, "jianying") : byFormat[base][0];
6688
7123
  console.log(` • ${meta?.label ?? base}:${meta?.openHint(target) ?? target}`);
6689
7124
  }
6690
7125
  if (rendered)
@@ -6752,23 +7187,23 @@ async function runOralCut(input, opts) {
6752
7187
  routeLogsToStderr();
6753
7188
  const cfg = loadConfig();
6754
7189
  const inputAbs = resolve4(input);
6755
- if (!existsSync12(inputAbs))
7190
+ if (!existsSync13(inputAbs))
6756
7191
  throw new Error(`毛片不存在:${inputAbs}`);
6757
- const projName = basename5(inputAbs, extname2(inputAbs));
7192
+ const projName = basename6(inputAbs, extname2(inputAbs));
6758
7193
  const formats = opts.formats.split(",").map((s) => s.trim()).filter(Boolean);
6759
7194
  if (opts.render && !formats.includes("gtrk"))
6760
7195
  formats.push("gtrk");
6761
7196
  const wantJianying = formats.some((f) => f === "jianying" || f === "capcut");
6762
- const outDir = resolve4(opts.out ?? join14(dirname3(inputAbs), `${projName}-video-project-${timestamp()}`));
7197
+ const outDir = resolve4(opts.out ?? join15(dirname3(inputAbs), `${projName}-video-project-${timestamp()}`));
6763
7198
  let scriptPath = opts.script ? resolve4(opts.script) : undefined;
6764
7199
  if (!scriptPath) {
6765
- const sibling = join14(dirname3(inputAbs), `${projName}.txt`);
6766
- if (existsSync12(sibling)) {
7200
+ const sibling = join15(dirname3(inputAbs), `${projName}.txt`);
7201
+ if (existsSync13(sibling)) {
6767
7202
  scriptPath = sibling;
6768
7203
  log.info(`自动识别到同名文稿:${sibling}(按有稿剪辑;不想用就改名或显式 --script)`);
6769
7204
  }
6770
7205
  }
6771
- const script = scriptPath ? await readFile3(scriptPath, "utf8") : undefined;
7206
+ const script = scriptPath ? await readFile4(scriptPath, "utf8") : undefined;
6772
7207
  let draftDir;
6773
7208
  if (wantJianying) {
6774
7209
  draftDir = resolveJianyingDraftDir(opts.jianyingDraftDir);
@@ -6777,14 +7212,14 @@ async function runOralCut(input, opts) {
6777
7212
  else
6778
7213
  log.warn("没找到剪映草稿目录 → 将只产 draft_content.json、缺 meta。可加 --jianying-draft-dir <你的草稿目录> 重跑。");
6779
7214
  }
6780
- log.step(`▶ 智能口播剪辑:${basename5(inputAbs)}(预设 ${opts.preset}${opts.visualAssist ? " · 视觉兜底(720p)" : ""},格式 ${formats.join("/")}${opts.render ? " · 本地渲染" : ""})`);
7215
+ log.step(`▶ 智能口播剪辑:${basename6(inputAbs)}(预设 ${opts.preset}${opts.visualAssist ? " · 视觉兜底(720p)" : ""},格式 ${formats.join("/")}${opts.render ? " · 本地渲染" : ""})`);
6781
7216
  const extraParams = parseExtraParams(opts.param, opts.paramsJson);
6782
7217
  log.step("① 本地预处理(探几何 + 抽音频/720p)…");
6783
7218
  const geo = probeGeometry(inputAbs, opts.ffmpegPath);
6784
7219
  log.info(`原片几何 ${geo.width}x${geo.height} @ ${geo.fps.toFixed(2)}fps · ${geo.duration.toFixed(1)}s`);
6785
7220
  const artifact = opts.visualAssist ? await compress720p(inputAbs, opts.ffmpegPath) : await extractAudio(inputAbs, opts.ffmpegPath);
6786
7221
  assertDurationConsistent(geo.duration, artifact, opts.ffmpegPath);
6787
- log.info(opts.visualAssist ? `已压 720p 代理(上传物):${basename5(artifact)}` : `已抽 16k 单声道 mp3(上传物):${basename5(artifact)}`);
7222
+ log.info(opts.visualAssist ? `已压 720p 代理(上传物):${basename6(artifact)}` : `已抽 16k 单声道 mp3(上传物):${basename6(artifact)}`);
6788
7223
  log.step("② 上传抽出物到云端…");
6789
7224
  const buildPayload = (fid) => {
6790
7225
  const p = {
@@ -6824,7 +7259,7 @@ async function runOralCut(input, opts) {
6824
7259
  const up = { fileId: submitted.fileId, cached: submitted.cached };
6825
7260
  log.info(`task_id = ${taskId}`);
6826
7261
  await mkdir4(outDir, { recursive: true });
6827
- await writeFile5(join14(outDir, "task.json"), JSON.stringify({ taskId, taskType: TASK_TYPE, fileId: up.fileId, source: inputAbs, formats, createdAt: new Date().toISOString() }, null, 2));
7262
+ await writeFile5(join15(outDir, "task.json"), JSON.stringify({ taskId, taskType: TASK_TYPE, fileId: up.fileId, source: inputAbs, formats, createdAt: new Date().toISOString() }, null, 2));
6828
7263
  log.step("④ 云端处理中(每 5s 轮询)…");
6829
7264
  const result = await pollTask(cfg, TASK_TYPE, taskId, (status, progress) => {
6830
7265
  log.tick(`${status}${progress != null ? ` ${Math.round(progress)}%` : ""}`);
@@ -6848,7 +7283,7 @@ async function runOralCut(input, opts) {
6848
7283
  }
6849
7284
 
6850
7285
  // src/commands/oralcut-result.ts
6851
- import { resolve as resolve5, join as join15 } from "node:path";
7286
+ import { resolve as resolve5, join as join16 } from "node:path";
6852
7287
  var TASK_TYPE2 = "cli/video_oral_cut_for_cli";
6853
7288
  function timestamp2() {
6854
7289
  const d = new Date;
@@ -6882,7 +7317,7 @@ async function runOralCutResult(taskId, opts) {
6882
7317
  const pct = got.progress != null ? ` ${Math.round(got.progress)}%` : "";
6883
7318
  throw new Error(`任务尚未完成(当前 ${got.status || "未知"}${pct}),暂无法取回结果;请稍后再试。`);
6884
7319
  }
6885
- const outDir = resolve5(opts.out ?? join15(process.cwd(), `${taskId}-video-project-${timestamp2()}`));
7320
+ const outDir = resolve5(opts.out ?? join16(process.cwd(), `${taskId}-video-project-${timestamp2()}`));
6886
7321
  const draftDir = resolveJianyingDraftDir(opts.jianyingDraftDir);
6887
7322
  await materializeResult({
6888
7323
  outDir,
@@ -6942,17 +7377,17 @@ function registerUpgrade(program2) {
6942
7377
  }
6943
7378
 
6944
7379
  // src/commands/render.ts
6945
- import { resolve as resolve6, dirname as dirname4, join as join16, basename as basename6, extname as extname3 } from "node:path";
6946
- import { existsSync as existsSync13 } from "node:fs";
7380
+ import { resolve as resolve6, dirname as dirname4, join as join17, basename as basename7, extname as extname3 } from "node:path";
7381
+ import { existsSync as existsSync14 } from "node:fs";
6947
7382
  function registerRender(program2) {
6948
7383
  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) => {
6949
7384
  if (opts.json)
6950
7385
  routeLogsToStderr();
6951
7386
  const gtrkAbs = resolve6(gtrk);
6952
- if (!existsSync13(gtrkAbs))
7387
+ if (!existsSync14(gtrkAbs))
6953
7388
  throw new Error(`gtrk 工程不存在:${gtrkAbs}`);
6954
- const outMp4 = resolve6(opts.out ?? join16(dirname4(gtrkAbs), `${basename6(gtrkAbs, extname3(gtrkAbs))}.mp4`));
6955
- log.step(`▶ 本地渲染:${basename6(gtrkAbs)} → ${basename6(outMp4)}`);
7389
+ const outMp4 = resolve6(opts.out ?? join17(dirname4(gtrkAbs), `${basename7(gtrkAbs, extname3(gtrkAbs))}.mp4`));
7390
+ log.step(`▶ 本地渲染:${basename7(gtrkAbs)} → ${basename7(outMp4)}`);
6956
7391
  const project = await readGtrkFile(gtrkAbs);
6957
7392
  const result = await renderGtrk(project, outMp4, {
6958
7393
  crf: opts.crf != null ? Number(opts.crf) : undefined,
@@ -6975,132 +7410,14 @@ function registerRender(program2) {
6975
7410
  }
6976
7411
 
6977
7412
  // src/commands/split.ts
6978
- import { resolve as resolve7, join as join18, dirname as dirname6, basename as basename8 } from "node:path";
6979
- import { existsSync as existsSync14 } from "node:fs";
6980
- import { readFile as readFile4, writeFile as writeFile6, mkdir as mkdir5 } from "node:fs/promises";
7413
+ import { resolve as resolve7, join as join19, dirname as dirname6, basename as basename9 } from "node:path";
7414
+ import { existsSync as existsSync15 } from "node:fs";
7415
+ import { readFile as readFile5, writeFile as writeFile6, mkdir as mkdir5 } from "node:fs/promises";
6981
7416
  import { createHash } from "node:crypto";
6982
7417
 
6983
- // src/lib/projection.ts
6984
- function r32(n) {
6985
- return Math.round(n * 1000) / 1000;
6986
- }
6987
- function normClip(c3) {
6988
- const clip_st = c3.clip_st ?? 0;
6989
- const track_st = c3.track_st ?? 0;
6990
- const dur = c3.duration ?? (c3.clip_ed != null ? c3.clip_ed - clip_st : 0);
6991
- const clip_ed = c3.clip_ed ?? clip_st + dur;
6992
- return { clip_st, clip_ed, track_st };
6993
- }
6994
- function pickMainVideoTrack(gtrk) {
6995
- const tracks = gtrk.video_track ?? [];
6996
- if (!tracks.length)
6997
- return;
6998
- let best = tracks[0];
6999
- let bestIdx = best.track_index ?? 0;
7000
- for (const t of tracks) {
7001
- const idx = t.track_index ?? 0;
7002
- if (idx < bestIdx) {
7003
- best = t;
7004
- bestIdx = idx;
7005
- }
7006
- }
7007
- return best;
7008
- }
7009
- function projectTranscript(transcript, gtrk, opts = {}) {
7010
- const materialId = String(transcript.material_id);
7011
- const mainTrack = pickMainVideoTrack(gtrk);
7012
- const clips = (mainTrack?.track_timeline ?? []).filter((c3) => c3.material != null && String(c3.material) === materialId).map(normClip);
7013
- const entries = [];
7014
- transcript.utterances.forEach((utt, sourceIndex) => {
7015
- const totalWords = utt.words?.length ?? 0;
7016
- const instances = [];
7017
- for (const clip of clips) {
7018
- const surviving = [];
7019
- for (const word of utt.words ?? []) {
7020
- const s = Math.max(word.st, clip.clip_st);
7021
- const e = Math.min(word.ed, clip.clip_ed);
7022
- if (e > s) {
7023
- surviving.push({
7024
- w: word.w,
7025
- track_st: r32(clip.track_st + (s - clip.clip_st)),
7026
- track_ed: r32(clip.track_st + (e - clip.clip_st))
7027
- });
7028
- }
7029
- }
7030
- if (surviving.length) {
7031
- instances.push({
7032
- track_st: Math.min(...surviving.map((x) => x.track_st)),
7033
- track_ed: Math.max(...surviving.map((x) => x.track_ed)),
7034
- kept_words: surviving.length,
7035
- words: surviving
7036
- });
7037
- }
7038
- }
7039
- if (!instances.length) {
7040
- entries.push({
7041
- id: utt.id,
7042
- text: utt.text,
7043
- dropped: true,
7044
- sourceIndex,
7045
- instIndex: 0,
7046
- track_st: null,
7047
- track_ed: null,
7048
- kept_words: 0,
7049
- total_words: totalWords,
7050
- words: [],
7051
- sortKey: 0
7052
- });
7053
- } else {
7054
- instances.sort((a, b) => a.track_st - b.track_st);
7055
- instances.forEach((inst, instIndex) => {
7056
- entries.push({
7057
- id: utt.id,
7058
- text: utt.text,
7059
- dropped: false,
7060
- sourceIndex,
7061
- instIndex,
7062
- track_st: inst.track_st,
7063
- track_ed: inst.track_ed,
7064
- kept_words: inst.kept_words,
7065
- total_words: totalWords,
7066
- words: inst.words,
7067
- sortKey: inst.track_st
7068
- });
7069
- });
7070
- }
7071
- });
7072
- let maxEd = 0;
7073
- for (const e of entries) {
7074
- if (e.dropped)
7075
- e.sortKey = maxEd;
7076
- else
7077
- maxEd = Math.max(maxEd, e.track_ed ?? maxEd);
7078
- }
7079
- entries.sort((a, b) => a.sortKey - b.sortKey || a.sourceIndex - b.sourceIndex || a.instIndex - b.instIndex);
7080
- const utterances = entries.map((e) => {
7081
- const u = {
7082
- id: e.id,
7083
- text: e.text,
7084
- track_st: e.track_st,
7085
- track_ed: e.track_ed,
7086
- dropped: e.dropped,
7087
- kept_words: e.kept_words,
7088
- total_words: e.total_words
7089
- };
7090
- if (opts.words)
7091
- u.words = e.words;
7092
- return u;
7093
- });
7094
- return {
7095
- transcript_hash: transcript.text_hash,
7096
- projected_at: opts.projectedAt ?? new Date().toISOString(),
7097
- utterances
7098
- };
7099
- }
7100
-
7101
7418
  // src/lib/gtrk-writeback.ts
7102
7419
  import { readFileSync as readFileSync4, statSync, writeFileSync as writeFileSync2, renameSync, unlinkSync } from "node:fs";
7103
- import { dirname as dirname5, join as join17, basename as basename7 } from "node:path";
7420
+ import { dirname as dirname5, join as join18, basename as basename8 } from "node:path";
7104
7421
  import { randomBytes as randomBytes2 } from "node:crypto";
7105
7422
  function readGtrk(path) {
7106
7423
  const raw = readFileSync4(path, "utf8");
@@ -7127,7 +7444,7 @@ function writeStructMetaSplit(path, gtrk, splitObj, expectedMtimeMs) {
7127
7444
  }
7128
7445
  const nextStructMeta = { ...gtrk.struct_meta ?? {}, split: splitObj };
7129
7446
  const next = { ...gtrk, struct_meta: nextStructMeta };
7130
- const tmp = join17(dirname5(path), `.${basename7(path)}.${randomBytes2(6).toString("hex")}.tmp`);
7447
+ const tmp = join18(dirname5(path), `.${basename8(path)}.${randomBytes2(6).toString("hex")}.tmp`);
7131
7448
  try {
7132
7449
  writeFileSync2(tmp, JSON.stringify(next, null, 2));
7133
7450
  renameSync(tmp, path);
@@ -7143,7 +7460,7 @@ function writeGtrkAtomic(path, next, expectedMtimeMs) {
7143
7460
  if (cur !== expectedMtimeMs) {
7144
7461
  throw new Error("工程文件在 matrix 运行期间被外部修改(保存冲突),已拒绝写入;请关闭客户端未保存的工程或重跑(plan 与已下载代理均保留)");
7145
7462
  }
7146
- const tmp = join17(dirname5(path), `.${basename7(path)}.${randomBytes2(6).toString("hex")}.tmp`);
7463
+ const tmp = join18(dirname5(path), `.${basename8(path)}.${randomBytes2(6).toString("hex")}.tmp`);
7147
7464
  try {
7148
7465
  writeFileSync2(tmp, JSON.stringify(next, null, 2));
7149
7466
  renameSync(tmp, path);
@@ -7163,7 +7480,7 @@ function registerSplit(program2) {
7163
7480
  });
7164
7481
  }
7165
7482
  function firstExisting(cands) {
7166
- return cands.find((p) => existsSync14(p));
7483
+ return cands.find((p) => existsSync15(p));
7167
7484
  }
7168
7485
  function resolvePaths(opts) {
7169
7486
  const project = opts.project ? resolve7(opts.project) : undefined;
@@ -7171,30 +7488,30 @@ function resolvePaths(opts) {
7171
7488
  if (opts.gtrk) {
7172
7489
  gtrkPath = resolve7(opts.gtrk);
7173
7490
  } else if (project) {
7174
- gtrkPath = firstExisting([join18(project, "gtrk", "project.gtrk"), join18(project, "project.gtrk")]) ?? join18(project, "gtrk", "project.gtrk");
7491
+ gtrkPath = firstExisting([join19(project, "gtrk", "project.gtrk"), join19(project, "project.gtrk")]) ?? join19(project, "gtrk", "project.gtrk");
7175
7492
  } else {
7176
7493
  throw new Error("需 --project <目录> 或显式 --gtrk <path>");
7177
7494
  }
7178
- if (!existsSync14(gtrkPath))
7495
+ if (!existsSync15(gtrkPath))
7179
7496
  throw new Error(`找不到工程文件:${gtrkPath}`);
7180
7497
  let transcriptPath;
7181
7498
  if (opts.transcript)
7182
7499
  transcriptPath = resolve7(opts.transcript);
7183
7500
  else if (project)
7184
7501
  transcriptPath = firstExisting([
7185
- join18(project, "transcript", "transcript.json"),
7186
- join18(project, "json", "transcript.json"),
7187
- join18(project, "transcript.json")
7502
+ join19(project, "transcript", "transcript.json"),
7503
+ join19(project, "json", "transcript.json"),
7504
+ join19(project, "transcript.json")
7188
7505
  ]);
7189
7506
  const baseDir = project ?? dirname6(gtrkPath);
7190
7507
  return { baseDir, gtrkPath, transcriptPath };
7191
7508
  }
7192
- function slugify(name) {
7509
+ function slugify2(name) {
7193
7510
  const s = name.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "-").replace(/^-+|-+$/g, "");
7194
7511
  return s || "project";
7195
7512
  }
7196
7513
  async function loadTranscript(path) {
7197
- const t = JSON.parse(await readFile4(path, "utf8"));
7514
+ const t = JSON.parse(await readFile5(path, "utf8"));
7198
7515
  if (!t || !Array.isArray(t.utterances) || typeof t.material_id !== "string" || typeof t.text_hash !== "string") {
7199
7516
  throw new Error(`transcript.json 结构异常(缺 utterances/material_id/text_hash):${path}`);
7200
7517
  }
@@ -7209,15 +7526,15 @@ async function runSplit(splitdoc, opts) {
7209
7526
  return splitdoc ? runLand(resolve7(splitdoc), baseDir, gtrkPath, transcriptPath, opts) : runView(baseDir, gtrkPath, transcriptPath, opts);
7210
7527
  }
7211
7528
  async function runView(baseDir, gtrkPath, transcriptPath, opts) {
7212
- if (!transcriptPath || !existsSync14(transcriptPath))
7529
+ if (!transcriptPath || !existsSync15(transcriptPath))
7213
7530
  throw new Error(TRANSCRIPT_MISSING);
7214
7531
  log.step("▶ 导出投影视图(transcript × 当刻 .gtrk)…");
7215
7532
  const transcript = await loadTranscript(transcriptPath);
7216
7533
  const { gtrk } = readGtrk(gtrkPath);
7217
7534
  const view = projectTranscript(transcript, gtrk, { words: opts.words });
7218
- const splitDir = join18(baseDir, "split");
7535
+ const splitDir = join19(baseDir, "split");
7219
7536
  await mkdir5(splitDir, { recursive: true });
7220
- const viewPath = join18(splitDir, "view.json");
7537
+ const viewPath = join19(splitDir, "view.json");
7221
7538
  await writeFile6(viewPath, JSON.stringify(view, null, 2));
7222
7539
  const dropped = view.utterances.filter((u) => u.dropped).length;
7223
7540
  log.ok(`投影视图已生成:${viewPath}(${view.utterances.length} 条,其中 ${dropped} 条被剪)`);
@@ -7235,12 +7552,12 @@ async function runView(baseDir, gtrkPath, transcriptPath, opts) {
7235
7552
  return result;
7236
7553
  }
7237
7554
  async function runLand(splitdocPath, baseDir, gtrkPath, transcriptPath, opts) {
7238
- if (!existsSync14(splitdocPath))
7555
+ if (!existsSync15(splitdocPath))
7239
7556
  throw new Error(`找不到拆分稿:${splitdocPath}`);
7240
- if (!transcriptPath || !existsSync14(transcriptPath))
7557
+ if (!transcriptPath || !existsSync15(transcriptPath))
7241
7558
  throw new Error(TRANSCRIPT_MISSING);
7242
7559
  log.step("▶ 校验拆分稿并落地…");
7243
- const doc = JSON.parse(await readFile4(splitdocPath, "utf8"));
7560
+ const doc = JSON.parse(await readFile5(splitdocPath, "utf8"));
7244
7561
  const transcript = await loadTranscript(transcriptPath);
7245
7562
  const { gtrk, mtimeMs } = readGtrk(gtrkPath);
7246
7563
  assertGtrkV1(gtrk);
@@ -7263,7 +7580,7 @@ async function runLand(splitdocPath, baseDir, gtrkPath, transcriptPath, opts) {
7263
7580
  }
7264
7581
  const projectedAt = new Date().toISOString();
7265
7582
  const view = projectTranscript(transcript, gtrk, { projectedAt });
7266
- const projectSlug = slugify(basename8(baseDir));
7583
+ const projectSlug = slugify2(basename9(baseDir));
7267
7584
  const landing = buildLanding(doc, view, {
7268
7585
  utteranceIds: ctx.utteranceIds,
7269
7586
  projectSlug,
@@ -7274,13 +7591,13 @@ async function runLand(splitdocPath, baseDir, gtrkPath, transcriptPath, opts) {
7274
7591
  }
7275
7592
  });
7276
7593
  writeStructMetaSplit(gtrkPath, gtrk, landing.split, mtimeMs);
7277
- const splitDir = join18(baseDir, "split");
7594
+ const splitDir = join19(baseDir, "split");
7278
7595
  await mkdir5(splitDir, { recursive: true });
7279
- const dispatchPath = join18(splitDir, "dispatch.json");
7596
+ const dispatchPath = join19(splitDir, "dispatch.json");
7280
7597
  await writeFile6(dispatchPath, JSON.stringify(landing.dispatch, null, 2));
7281
7598
  let mdPath = null;
7282
7599
  if (opts.md) {
7283
- mdPath = join18(splitDir, "visual-split.md");
7600
+ mdPath = join19(splitDir, "visual-split.md");
7284
7601
  await writeFile6(mdPath, renderSplitMarkdown(doc, landing, { projectSlug, projectedAt }));
7285
7602
  }
7286
7603
  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})`);
@@ -7312,9 +7629,9 @@ async function runLand(splitdocPath, baseDir, gtrkPath, transcriptPath, opts) {
7312
7629
  }
7313
7630
 
7314
7631
  // src/commands/matrix.ts
7315
- import { resolve as resolve8, join as join19, dirname as dirname7, basename as basename9 } from "node:path";
7316
- import { existsSync as existsSync15 } from "node:fs";
7317
- import { readFile as readFile5, writeFile as writeFile7, mkdir as mkdir6, rename } from "node:fs/promises";
7632
+ import { resolve as resolve9, join as join20, dirname as dirname7, basename as basename10 } from "node:path";
7633
+ import { existsSync as existsSync17 } from "node:fs";
7634
+ import { readFile as readFile6, writeFile as writeFile7, mkdir as mkdir6, rename } from "node:fs/promises";
7318
7635
 
7319
7636
  // src/lib/solid-png.ts
7320
7637
  import { deflateSync } from "node:zlib";
@@ -7421,6 +7738,7 @@ function encodeSolidPng({
7421
7738
  // src/lib/matrix-lay.ts
7422
7739
  var BROLL_PREVIEW_DIR = "assets/broll-preview";
7423
7740
  var BROLL_MATERIAL_PREFIX = "broll-";
7741
+ var BROLL_RAW_MATERIAL_PREFIX = "broll-raw-";
7424
7742
  var BROLL_META_CANDIDATE_CAP = 12;
7425
7743
  var SHOT_TARGET_DEFAULT = 3;
7426
7744
  var MIN_SHOT_SEC = 1.2;
@@ -7439,7 +7757,36 @@ function mergeBlackBedSegments(envelopes) {
7439
7757
  }
7440
7758
  out.push({ track_st: e.track_st, track_ed: e.track_ed });
7441
7759
  }
7442
- return out.map((s) => ({ track_st: r33(s.track_st), track_ed: r33(s.track_ed) }));
7760
+ return out.map((s) => ({ track_st: r34(s.track_st), track_ed: r34(s.track_ed) }));
7761
+ }
7762
+ var HOLE_WARN_SEC = 3;
7763
+ var HOLE_WARN_RATIO = 0.15;
7764
+ function computeBlackBedHoles(opts) {
7765
+ const holes = [];
7766
+ for (const b of opts.beats) {
7767
+ if (!(b.track_ed - b.track_st > BLACK_BED_MERGE_EPS))
7768
+ continue;
7769
+ const covered = mergeBlackBedSegments(b.slots.map((s) => ({
7770
+ track_st: Math.max(b.track_st, s.track_st),
7771
+ track_ed: Math.min(b.track_ed, s.track_ed)
7772
+ })));
7773
+ const push = (st, ed) => {
7774
+ const track_st = r34(st);
7775
+ const track_ed = r34(ed);
7776
+ if (track_ed - track_st > BLACK_BED_MERGE_EPS) {
7777
+ holes.push({ beat: b.beat, track_st, track_ed, sec: r34(track_ed - track_st) });
7778
+ }
7779
+ };
7780
+ let cursor = b.track_st;
7781
+ for (const c3 of covered) {
7782
+ push(cursor, c3.track_st);
7783
+ if (c3.track_ed > cursor)
7784
+ cursor = c3.track_ed;
7785
+ }
7786
+ push(cursor, b.track_ed);
7787
+ }
7788
+ holes.sort((a, b) => a.track_st - b.track_st || a.track_ed - b.track_ed);
7789
+ return { holes, totalSec: r34(holes.reduce((n, h) => n + h.sec, 0)) };
7443
7790
  }
7444
7791
  function mergedCandidates(beat) {
7445
7792
  const all = [];
@@ -7468,7 +7815,7 @@ function previewDims(width, height) {
7468
7815
  const h = Math.max(2, Math.round(height * 640 / width / 2) * 2);
7469
7816
  return [640, h];
7470
7817
  }
7471
- var r33 = (n) => Math.round(n * 1000) / 1000;
7818
+ var r34 = (n) => Math.round(n * 1000) / 1000;
7472
7819
  function buildQueryPools(beat, scoreFloor) {
7473
7820
  const out = [];
7474
7821
  for (const q of beat.queries) {
@@ -7571,10 +7918,10 @@ function fillBeatTrack(opts) {
7571
7918
  clip_id: pick.cand.clip_id,
7572
7919
  query: pick.query,
7573
7920
  score: pick.seg.score,
7574
- clip_st: r33(clipSt),
7575
- clip_ed: r33(clipSt + d),
7576
- track_st: r33(cursor),
7577
- track_ed: r33(cursor + d)
7921
+ clip_st: r34(clipSt),
7922
+ clip_ed: r34(clipSt + d),
7923
+ track_st: r34(cursor),
7924
+ track_ed: r34(cursor + d)
7578
7925
  });
7579
7926
  consumed.add(pick.key);
7580
7927
  prevClip = pick.cand.clip_id;
@@ -7589,8 +7936,8 @@ function fillBeatTrack(opts) {
7589
7936
  const hi = dur ?? lastPick.seg.end;
7590
7937
  const ext = Math.min(tail, Math.max(0, hi - last.clip_ed));
7591
7938
  if (ext > 0.000001) {
7592
- last.clip_ed = r33(last.clip_ed + ext);
7593
- last.track_ed = r33(last.track_ed + ext);
7939
+ last.clip_ed = r34(last.clip_ed + ext);
7940
+ last.track_ed = r34(last.track_ed + ext);
7594
7941
  }
7595
7942
  }
7596
7943
  }
@@ -7612,17 +7959,183 @@ function planBeatFills(plan, lay, scoreFloor) {
7612
7959
  }
7613
7960
  return { fills, clipIds };
7614
7961
  }
7962
+ var expectedLabel = (e) => `${e.kind === "black" ? "黑底轨" : "候选轨"}#${e.trackIndex}=${e.clipCount} clip`;
7963
+ function expectedSelfProducedTracks(prevBroll) {
7964
+ if (!prevBroll || typeof prevBroll !== "object")
7965
+ return [];
7966
+ const meta = prevBroll;
7967
+ const beats = Array.isArray(meta.beats) ? meta.beats : [];
7968
+ const out = [];
7969
+ const byTrack = new Map;
7970
+ for (const b of beats) {
7971
+ for (const l of Array.isArray(b?.laid) ? b.laid : []) {
7972
+ const idx = l.track_index;
7973
+ const slots = l.slots;
7974
+ if (typeof idx !== "number" || !Array.isArray(slots) || slots.length === 0)
7975
+ continue;
7976
+ byTrack.set(idx, (byTrack.get(idx) ?? 0) + slots.length);
7977
+ }
7978
+ }
7979
+ for (const [trackIndex, clipCount] of [...byTrack.entries()].sort((a, b) => a[0] - b[0])) {
7980
+ out.push({ kind: "candidate", trackIndex, clipCount });
7981
+ }
7982
+ if (typeof meta.black_track === "number") {
7983
+ const envelopes = beats.filter((b) => Array.isArray(b?.laid) && b.laid.length > 0).map((b) => ({ track_st: Number(b.track_st), track_ed: Number(b.track_ed) }));
7984
+ const segCount = mergeBlackBedSegments(envelopes).length;
7985
+ if (segCount > 0)
7986
+ out.push({ kind: "black", trackIndex: meta.black_track, clipCount: segCount });
7987
+ }
7988
+ return out;
7989
+ }
7990
+ function pickExpectation(expected, clipCount, trackIndex) {
7991
+ let best = null;
7992
+ let bestDist = Number.POSITIVE_INFINITY;
7993
+ for (const e of expected) {
7994
+ if (e.clipCount !== clipCount)
7995
+ continue;
7996
+ const dist = trackIndex === null ? 0 : Math.abs(e.trackIndex - trackIndex);
7997
+ if (dist < bestDist || dist === bestDist && best !== null && e.trackIndex < best.trackIndex) {
7998
+ best = e;
7999
+ bestDist = dist;
8000
+ }
8001
+ }
8002
+ return best;
8003
+ }
8004
+ function classifyTrack(track, expected, opts = {}) {
8005
+ const clips = Array.isArray(track.track_timeline) ? track.track_timeline : [];
8006
+ const trackIndex = typeof track.track_index === "number" ? track.track_index : null;
8007
+ const materials = clips.map((c3) => typeof c3?.material === "string" ? c3.material : "");
8008
+ const rawClips = materials.filter((m) => m.startsWith(BROLL_RAW_MATERIAL_PREFIX)).length;
8009
+ const foreignClips = materials.filter((m) => !(m.startsWith(BROLL_MATERIAL_PREFIX) || m.startsWith(SOLID_MATERIAL_PREFIX))).length;
8010
+ const base = {
8011
+ trackIndex,
8012
+ clipCount: clips.length,
8013
+ rawClips,
8014
+ foreignClips,
8015
+ samples: [...new Set(materials.filter(Boolean))].slice(0, 2),
8016
+ matched: null
8017
+ };
8018
+ const registered = opts.registered ?? expected.length > 0;
8019
+ if (clips.length === 0)
8020
+ return { ...base, cls: "user", reason: "空轨(track_timeline 为空)" };
8021
+ if (!registered) {
8022
+ return { ...base, cls: "user", reason: "盘上无 struct_meta.broll 登记(或上轮一条都没铺成):宁留勿删" };
8023
+ }
8024
+ if (rawClips > 0) {
8025
+ return {
8026
+ ...base,
8027
+ cls: "self-produced-edited",
8028
+ reason: `${rawClips}/${clips.length} 个 clip 的 material 已是 broll-raw-*(你在客户端确认过原片)`
8029
+ };
8030
+ }
8031
+ if (foreignClips > 0) {
8032
+ return {
8033
+ ...base,
8034
+ cls: "user",
8035
+ reason: `${foreignClips}/${clips.length} 个 clip 的 material 非自产前缀(用户轨 / 混合轨)`
8036
+ };
8037
+ }
8038
+ const matched = opts.matched !== undefined ? opts.matched : pickExpectation(expected, clips.length, trackIndex);
8039
+ if (matched) {
8040
+ return { ...base, matched, cls: "self-produced", reason: `与登记吻合(${expectedLabel(matched)})` };
8041
+ }
8042
+ const inLay = new Set(opts.layTracks ?? []).has(trackIndex ?? Number.NaN);
8043
+ const counts = expected.length ? expected.map(expectedLabel).join("、") : "(登记里一条自产轨都没有)";
8044
+ if (inLay) {
8045
+ return {
8046
+ ...base,
8047
+ cls: "self-produced-edited",
8048
+ reason: `clip 数 ${clips.length} 与登记的自产轨条数都对不上(登记:${counts}),而该 track_index 在 lay_tracks 在册`
8049
+ };
8050
+ }
8051
+ return {
8052
+ ...base,
8053
+ cls: "user",
8054
+ reason: `clip 数 ${clips.length} 对不上任何登记指纹(登记:${counts})且 track_index 不在 lay_tracks 在册:按用户轨保留`
8055
+ };
8056
+ }
8057
+ function classifyVideoTracks(tracks, expected, layTracks = []) {
8058
+ const registered = expected.length > 0;
8059
+ const eligible = tracks.map((t, i) => ({ i, idx: typeof t.track_index === "number" ? t.track_index : null, v: classifyTrack(t, [], { registered: true, matched: null }) })).filter((e) => e.v.clipCount > 0 && e.v.rawClips === 0 && e.v.foreignClips === 0);
8060
+ const claimed = new Map;
8061
+ const taken = new Set;
8062
+ for (const exp of expected) {
8063
+ let best = -1;
8064
+ let bestDist = Number.POSITIVE_INFINITY;
8065
+ let bestIdx = Number.POSITIVE_INFINITY;
8066
+ for (const e of eligible) {
8067
+ if (taken.has(e.i) || e.v.clipCount !== exp.clipCount)
8068
+ continue;
8069
+ const idx = e.idx ?? Number.MAX_SAFE_INTEGER;
8070
+ const dist = Math.abs(idx - exp.trackIndex);
8071
+ if (dist < bestDist || dist === bestDist && idx < bestIdx) {
8072
+ best = e.i;
8073
+ bestDist = dist;
8074
+ bestIdx = idx;
8075
+ }
8076
+ }
8077
+ if (best >= 0) {
8078
+ taken.add(best);
8079
+ claimed.set(best, exp);
8080
+ }
8081
+ }
8082
+ return tracks.map((t, i) => classifyTrack(t, expected, { layTracks, registered, matched: claimed.get(i) ?? null }));
8083
+ }
7615
8084
  function layBrollTracks(opts) {
7616
8085
  const { gtrk, plan, lay, fills, downloads } = opts;
7617
8086
  const blackBedOn = opts.blackBed !== false;
8087
+ const forceRelay = opts.forceRelay === true;
7618
8088
  const warnings = [];
7619
8089
  const videoTracks = [...gtrk.video_track ?? []];
7620
8090
  const materials = [...gtrk.materials ?? []];
7621
8091
  const structMeta = { ...gtrk.struct_meta ?? {} };
7622
8092
  const prevBroll = structMeta.broll;
7623
8093
  const prevIndices = new Set(Array.isArray(prevBroll?.lay_tracks) ? prevBroll.lay_tracks.filter((x) => typeof x === "number") : []);
7624
- const removedTracks = videoTracks.filter((t) => typeof t.track_index === "number" && prevIndices.has(t.track_index));
7625
- const keptTracks = videoTracks.filter((t) => !(typeof t.track_index === "number" && prevIndices.has(t.track_index)));
8094
+ const expected = expectedSelfProducedTracks(prevBroll);
8095
+ const verdicts = classifyVideoTracks(videoTracks, expected, prevIndices);
8096
+ const editedTracks = videoTracks.filter((_, i) => verdicts[i].cls === "self-produced-edited");
8097
+ const keptEditedTracks = editedTracks.map((t) => typeof t.track_index === "number" ? t.track_index : -1).filter((n) => n >= 0);
8098
+ const strippable = (i) => verdicts[i].cls === "self-produced" || forceRelay && verdicts[i].cls === "self-produced-edited";
8099
+ const removedTracks = videoTracks.filter((_, i) => strippable(i));
8100
+ const keptTracks = videoTracks.filter((_, i) => !strippable(i));
8101
+ for (let i = 0;i < videoTracks.length; i++) {
8102
+ const v = verdicts[i];
8103
+ if (v.cls !== "self-produced-edited")
8104
+ continue;
8105
+ const samples = v.samples.length ? `,material 样例 ${v.samples.join(" / ")}` : "";
8106
+ warnings.push(`候选轨 track_index=${v.trackIndex} 判定为「已被你编辑」:${v.reason}(该轨 ${v.clipCount} clip${samples})。` + (forceRelay ? "已按 --force-relay 强制剥离重铺。" : "本次不剥它、也不铺新轨——在客户端处置该轨后重跑,或加 --force-relay 强剥重铺。"));
8107
+ }
8108
+ if (editedTracks.length > 0 && !forceRelay) {
8109
+ let beatsWithCands = 0;
8110
+ for (const beat of plan.beats)
8111
+ if (mergedCandidates(beat).length > 0)
8112
+ beatsWithCands++;
8113
+ warnings.push(`本次未铺任何轨、工程文件零改动(.gtrk 未写回、struct_meta.broll 未刷新);` + `broll-plan.json 与已落盘的 preview 代理照常产出/复用。`);
8114
+ return {
8115
+ next: gtrk,
8116
+ summary: {
8117
+ laidTracks: [],
8118
+ laidClips: 0,
8119
+ beatsWithCandidates: beatsWithCands,
8120
+ blackTrack: null,
8121
+ removedTracks: [],
8122
+ keptEditedTracks,
8123
+ refused: true,
8124
+ blackBedHoleSec: 0,
8125
+ blackBedHoles: []
8126
+ },
8127
+ broll: prevBroll ?? {
8128
+ contract_version: "v1",
8129
+ generated_at: opts.generatedAt,
8130
+ plan_path: opts.planPath,
8131
+ lay_tracks: [],
8132
+ black_track: null,
8133
+ confirmed: false,
8134
+ beats: []
8135
+ },
8136
+ warnings
8137
+ };
8138
+ }
7626
8139
  const removedMaterialIds = new Set;
7627
8140
  for (const t of removedTracks) {
7628
8141
  for (const c3 of t.track_timeline ?? []) {
@@ -7644,6 +8157,12 @@ function layBrollTracks(opts) {
7644
8157
  for (const id of stillReferenced)
7645
8158
  removedMaterialIds.delete(id);
7646
8159
  const keptMaterials = materials.filter((m) => !(typeof m.id === "string" && removedMaterialIds.has(m.id)));
8160
+ if (forceRelay) {
8161
+ const rawGone = [...removedMaterialIds].filter((id) => id.startsWith(BROLL_RAW_MATERIAL_PREFIX)).length;
8162
+ if (rawGone > 0) {
8163
+ warnings.push(`--force-relay:本次强制剥离将删除 ${rawGone} 条 broll-raw-* 素材登记,` + `gtrk/assets/broll/ 下对应的已下载原片文件会就地成孤儿(CLI 不删字节,但工程里再无引用)。`);
8164
+ }
8165
+ }
7647
8166
  for (const t of keptTracks) {
7648
8167
  const clips = t.track_timeline ?? [];
7649
8168
  if (!clips.length)
@@ -7697,7 +8216,7 @@ function layBrollTracks(opts) {
7697
8216
  clip_ed: s.clip_ed,
7698
8217
  track_st: s.track_st,
7699
8218
  track_ed: s.track_ed,
7700
- duration: r33(s.track_ed - s.track_st)
8219
+ duration: r34(s.track_ed - s.track_st)
7701
8220
  });
7702
8221
  laidClips++;
7703
8222
  });
@@ -7760,15 +8279,50 @@ function layBrollTracks(opts) {
7760
8279
  clip_id: `blackbed-${i}`,
7761
8280
  material: solidId,
7762
8281
  clip_st: 0,
7763
- clip_ed: r33(s.track_ed - s.track_st),
8282
+ clip_ed: r34(s.track_ed - s.track_st),
7764
8283
  track_st: s.track_st,
7765
8284
  track_ed: s.track_ed,
7766
- duration: r33(s.track_ed - s.track_st)
8285
+ duration: r34(s.track_ed - s.track_st)
7767
8286
  }))
7768
8287
  };
7769
8288
  }
7770
8289
  }
7771
8290
  }
8291
+ let blackBedHoles = [];
8292
+ let blackBedHoleSec = 0;
8293
+ if (blackTrack !== null) {
8294
+ const holeBeats = metaBeats.filter((b) => b.laid.length > 0).map((b) => ({
8295
+ beat: b.beat,
8296
+ track_st: b.track_st,
8297
+ track_ed: b.track_ed,
8298
+ slots: b.laid.flatMap((l) => l.slots)
8299
+ }));
8300
+ ({ holes: blackBedHoles, totalSec: blackBedHoleSec } = computeBlackBedHoles({ beats: holeBeats }));
8301
+ const spanOf = new Map(holeBeats.map((b) => [b.beat, b.track_ed - b.track_st]));
8302
+ const perBeat = new Map;
8303
+ for (const h of blackBedHoles) {
8304
+ const cur = perBeat.get(h.beat);
8305
+ if (!cur)
8306
+ perBeat.set(h.beat, { sec: h.sec, longest: h });
8307
+ else {
8308
+ cur.sec = r34(cur.sec + h.sec);
8309
+ if (h.sec > cur.longest.sec)
8310
+ cur.longest = h;
8311
+ }
8312
+ }
8313
+ const offenders = [...perBeat.entries()].filter(([beat, v]) => {
8314
+ const span = spanOf.get(beat) ?? 0;
8315
+ return v.longest.sec >= HOLE_WARN_SEC || span > 0 && v.sec / span >= HOLE_WARN_RATIO;
8316
+ });
8317
+ if (offenders.length > 0) {
8318
+ const detail = offenders.map(([beat, v]) => {
8319
+ const span = spanOf.get(beat) ?? 0;
8320
+ const pct = span > 0 ? Math.round(v.sec / span * 100) : 0;
8321
+ return `${beat} 纯黑 ${v.sec}s / 占 ${pct}%(最长一段 ${v.longest.sec}s @ ${v.longest.track_st}–${v.longest.track_ed})`;
8322
+ }).join(";");
8323
+ warnings.push(`黑底空洞:${offenders.length} 个 beat 的黑底之上没有任何 B-roll,这几段现在是纯黑压住口播——${detail};` + `全片累计纯黑 ${blackBedHoleSec}s。铺轨已照常完成(黑底按 beat 包络整条铺,槽位没填满处就是纯黑,属粗剪期预期内);` + `想调整可:调低 --score-floor 放宽取材、或改用 --no-black-bed 让这些地方露出主轨口播、或到客户端手动往这几段补片。`);
8324
+ }
8325
+ }
7772
8326
  const laidTrackIndices = createdTracks.map((t) => t.track_index);
7773
8327
  const broll = {
7774
8328
  contract_version: "v1",
@@ -7796,12 +8350,191 @@ function layBrollTracks(opts) {
7796
8350
  };
7797
8351
  return {
7798
8352
  next,
7799
- summary: { laidTracks: laidTrackIndices, laidClips, beatsWithCandidates, blackTrack },
8353
+ summary: {
8354
+ laidTracks: laidTrackIndices,
8355
+ laidClips,
8356
+ beatsWithCandidates,
8357
+ blackTrack,
8358
+ removedTracks: removedTracks.map((t) => typeof t.track_index === "number" ? t.track_index : -1).filter((n) => n >= 0).sort((a, b) => a - b),
8359
+ keptEditedTracks: forceRelay ? [] : keptEditedTracks,
8360
+ refused: false,
8361
+ blackBedHoleSec,
8362
+ blackBedHoles
8363
+ },
7800
8364
  broll,
7801
8365
  warnings
7802
8366
  };
7803
8367
  }
7804
8368
 
8369
+ // src/lib/material-integrity.ts
8370
+ import { existsSync as existsSync16 } from "node:fs";
8371
+ import { resolve as resolve8 } from "node:path";
8372
+ var INTEGRITY_LIST_CAP = 10;
8373
+ var r35 = (n) => Math.round(n * 1000) / 1000;
8374
+ function classifyMaterialPath(p) {
8375
+ if (typeof p !== "string" || p.trim() === "")
8376
+ return { kind: "none", path: typeof p === "string" ? p : "" };
8377
+ const path = p.trim();
8378
+ if (/^https?:\/\//i.test(path))
8379
+ return { kind: "remote", path };
8380
+ if (/^[a-zA-Z]:[\\/]/.test(path) || /^[\\/]{2}/.test(path) || /^[\\/]/.test(path)) {
8381
+ return { kind: "absolute", path };
8382
+ }
8383
+ return { kind: "relative", path };
8384
+ }
8385
+ var normalizeRel = (p) => p.replace(/\\/g, "/").replace(/^\.\//, "");
8386
+ function clipTrackEd(clip) {
8387
+ if (typeof clip.track_ed === "number")
8388
+ return clip.track_ed;
8389
+ if (typeof clip.track_st === "number" && typeof clip.duration === "number") {
8390
+ return r35(clip.track_st + clip.duration);
8391
+ }
8392
+ return null;
8393
+ }
8394
+ var TRACK_GROUPS = ["video_track", "audio_track", "beat_track"];
8395
+ var REF_KEYS = ["material", "html_material"];
8396
+ function collectMaterialRefs(gtrk) {
8397
+ const out = new Map;
8398
+ for (const group of TRACK_GROUPS) {
8399
+ const tracks = gtrk[group];
8400
+ if (!Array.isArray(tracks))
8401
+ continue;
8402
+ for (const t of tracks) {
8403
+ if (!t || typeof t !== "object")
8404
+ continue;
8405
+ const trackIndex = typeof t.track_index === "number" ? t.track_index : null;
8406
+ const clips = Array.isArray(t.track_timeline) ? t.track_timeline : [];
8407
+ for (const c3 of clips) {
8408
+ if (!c3 || typeof c3 !== "object")
8409
+ continue;
8410
+ for (const key of REF_KEYS) {
8411
+ const id = c3[key];
8412
+ if (typeof id !== "string" || id === "")
8413
+ continue;
8414
+ const list = out.get(id) ?? [];
8415
+ list.push({
8416
+ track: group,
8417
+ track_index: trackIndex,
8418
+ clip_id: typeof c3.clip_id === "string" ? c3.clip_id : null,
8419
+ track_st: typeof c3.track_st === "number" ? c3.track_st : null,
8420
+ track_ed: clipTrackEd(c3),
8421
+ key
8422
+ });
8423
+ out.set(id, list);
8424
+ }
8425
+ }
8426
+ }
8427
+ }
8428
+ return out;
8429
+ }
8430
+ function checkMaterialIntegrity(opts) {
8431
+ const exists = opts.exists ?? ((p) => existsSync16(p));
8432
+ const materials = Array.isArray(opts.gtrk.materials) ? opts.gtrk.materials : [];
8433
+ const refs = collectMaterialRefs(opts.gtrk);
8434
+ const counts = { relative: 0, absolute: 0, remote: 0, noPath: 0 };
8435
+ const dangling = [];
8436
+ const external = [];
8437
+ const noPathIds = [];
8438
+ let degradedCount = 0;
8439
+ let degradedReason = "";
8440
+ for (const m of materials) {
8441
+ const id = typeof m?.id === "string" ? m.id : "";
8442
+ const { kind, path } = classifyMaterialPath(m?.path);
8443
+ if (kind === "none") {
8444
+ counts.noPath++;
8445
+ noPathIds.push(id || "(无 id)");
8446
+ continue;
8447
+ }
8448
+ if (kind === "remote") {
8449
+ counts.remote++;
8450
+ continue;
8451
+ }
8452
+ counts[kind]++;
8453
+ const resolved = kind === "relative" ? resolve8(opts.gtrkDir, normalizeRel(path)) : path;
8454
+ let present;
8455
+ try {
8456
+ present = exists(resolved);
8457
+ } catch (e) {
8458
+ degradedCount++;
8459
+ if (!degradedReason)
8460
+ degradedReason = e instanceof Error ? e.message : String(e);
8461
+ continue;
8462
+ }
8463
+ if (present)
8464
+ continue;
8465
+ const list = refs.get(id) ?? [];
8466
+ const entry = {
8467
+ id,
8468
+ path,
8469
+ kind,
8470
+ resolved,
8471
+ referenced: list.length > 0,
8472
+ refCount: list.length,
8473
+ refs: list
8474
+ };
8475
+ (kind === "relative" ? dangling : external).push(entry);
8476
+ }
8477
+ const bySeverity = (a, b) => a.referenced === b.referenced ? a.id.localeCompare(b.id) : a.referenced ? -1 : 1;
8478
+ dangling.sort(bySeverity);
8479
+ external.sort(bySeverity);
8480
+ return {
8481
+ checked: materials.length,
8482
+ counts,
8483
+ dangling,
8484
+ danglingReferenced: dangling.filter((d) => d.referenced).length,
8485
+ danglingOrphan: dangling.filter((d) => !d.referenced).length,
8486
+ external,
8487
+ noPathIds,
8488
+ ...degradedCount > 0 ? { degraded: { count: degradedCount, reason: degradedReason } } : {}
8489
+ };
8490
+ }
8491
+ function safeCheckMaterialIntegrity(opts) {
8492
+ try {
8493
+ return checkMaterialIntegrity({ gtrk: opts.gtrk, gtrkDir: opts.gtrkDir, exists: opts.exists });
8494
+ } catch (e) {
8495
+ opts.log.warn(`素材落盘自检未能完成(${e instanceof Error ? e.message : String(e)})——写回结果与命令判定不受影响。`);
8496
+ return;
8497
+ }
8498
+ }
8499
+ function integritySummaryLine(r) {
8500
+ if (r.dangling.length === 0) {
8501
+ return `素材落盘自检:${r.checked} 条素材全部就位`;
8502
+ }
8503
+ return `素材落盘自检:${r.checked} 条素材里有 ${r.dangling.length} 条的文件不在盘上` + `(被时间线引用 ${r.danglingReferenced} 条 · 孤儿 ${r.danglingOrphan} 条)——只报不动,工程与文件零改动。`;
8504
+ }
8505
+ function missingLine(m) {
8506
+ const tag = m.referenced ? "被引用" : "孤儿";
8507
+ if (!m.referenced)
8508
+ return `[${tag}] ${m.id} → ${m.path}`;
8509
+ const head = m.refs[0];
8510
+ const span = head.track_st !== null ? ` · ${r35(head.track_st)}→${head.track_ed === null ? "?" : r35(head.track_ed)}s` : "";
8511
+ const more = m.refCount > 1 ? ` 等 ${m.refCount} 处` : "";
8512
+ return `[${tag}] ${m.id} → ${m.path}` + `(${head.track} track_index=${head.track_index ?? "?"} · clip ${head.clip_id ?? "?"}${span}${more})`;
8513
+ }
8514
+ function reportMaterialIntegrity(r, log2) {
8515
+ if (r.degraded) {
8516
+ log2.warn(`素材落盘自检降级:${r.degraded.count} 条素材查不了存在性(${r.degraded.reason})——` + `这几条既不算就位也不算悬空,其余条目照常已查。`);
8517
+ }
8518
+ if (r.dangling.length === 0) {
8519
+ log2.info(integritySummaryLine(r));
8520
+ } else {
8521
+ log2.warn(integritySummaryLine(r));
8522
+ for (const m of r.dangling.slice(0, INTEGRITY_LIST_CAP))
8523
+ log2.warn(` ${missingLine(m)}`);
8524
+ if (r.dangling.length > INTEGRITY_LIST_CAP) {
8525
+ log2.warn(` …等共 ${r.dangling.length} 条(全量见 --json 的 integrity.dangling)`);
8526
+ }
8527
+ log2.info("被引用的悬空 = 时间线上那一段没有素材可放(客户端可能 relink 回落到别的素材);孤儿 = 只在 materials 里挂着、不影响画面。");
8528
+ log2.info("悬空多半是历史遗留(如客户端「确认原片」下载中断);CLI 只报不删,要修就在客户端重新确认原片、或删掉那条 clip。");
8529
+ }
8530
+ if (r.external.length > 0) {
8531
+ log2.warn(`另有 ${r.external.length} 条**绝对路径**素材当前找不到文件(外接盘/网络盘没挂载也会这样,不计入上面的悬空):` + r.external.slice(0, INTEGRITY_LIST_CAP).map((m) => `${m.id} → ${m.path}`).join(";") + (r.external.length > INTEGRITY_LIST_CAP ? ` …等共 ${r.external.length} 条` : ""));
8532
+ }
8533
+ if (r.noPathIds.length > 0) {
8534
+ log2.warn(`另有 ${r.noPathIds.length} 条素材没有 path(结构问题,非落盘问题):` + r.noPathIds.slice(0, INTEGRITY_LIST_CAP).join("、") + (r.noPathIds.length > INTEGRITY_LIST_CAP ? ` …等共 ${r.noPathIds.length} 条` : ""));
8535
+ }
8536
+ }
8537
+
7805
8538
  // src/lib/matrix.ts
7806
8539
  var URL_TTL_NOTE = "结果 url 带签名默认 24h 过期;过期后重跑 gtrk matrix 即重签(plan 幂等重生成)。";
7807
8540
  var ENDPOINTS = {
@@ -7989,7 +8722,7 @@ async function searchOnce(cfg, tier, body) {
7989
8722
 
7990
8723
  // src/commands/matrix.ts
7991
8724
  function registerMatrix(program2) {
7992
- program2.command("matrix [words...]").description('B-roll 检索:无 positional=消费 split/dispatch.json 的 film_broll 队列产候选清单;`matrix search "<query>"`=单条 ad-hoc 检索').option("--project <dir>", "oralcut 产物目录(定位 split/dispatch.json 与产物落点)").option("--dispatch <path>", "显式指定 dispatch.json(非标准布局兜底)").option("--column <id>", "栏目配置 id(缺省取 config defaultColumn,再缺省内置默认栏目)").option("--top-k <n>", "每 query 候选数上限(覆盖派单 shots 翻译;服务端上限 50)").option("--material-class <c>", "素材类型 real_shot|concept(仅矩阵成员口;覆盖栏目 material_class_policy)").option("--lay <n>", "候选铺轨数:下载 preview 代理并在工程里平铺 N 条 B-roll 候选轨(默认 1;0=只出 plan 不铺轨)", "1").option("--score-floor <f>", "填充置信度地板:segment score 低于此值不采纳,槽位留空露主轨(默认 0.2)").option("--no-black-bed", "不铺纯黑底垫轨(默认铺一条,垫在候选轨之下、口播主轨之上,用于 B-roll 期间遮住口播画面)").option("--out <file>", "ad-hoc 模式:结果落文件(缺省输出 stdout)").option("--json", "机读模式:人读日志转 stderr,stdout 只输出结果 JSON").action(async (words, opts) => {
8725
+ program2.command("matrix [words...]").description('B-roll 检索:无 positional=消费 split/dispatch.json 的 film_broll 队列产候选清单;`matrix search "<query>"`=单条 ad-hoc 检索').option("--project <dir>", "oralcut 产物目录(定位 split/dispatch.json 与产物落点)").option("--dispatch <path>", "显式指定 dispatch.json(非标准布局兜底)").option("--column <id>", "栏目配置 id(缺省取 config defaultColumn,再缺省内置默认栏目)").option("--top-k <n>", "每 query 候选数上限(覆盖派单 shots 翻译;服务端上限 50)").option("--material-class <c>", "素材类型 real_shot|concept(仅矩阵成员口;覆盖栏目 material_class_policy)").option("--lay <n>", "候选铺轨数:下载 preview 代理并在工程里平铺 N 条 B-roll 候选轨(默认 1;0=只出 plan 不铺轨)", "1").option("--score-floor <f>", "填充置信度地板:segment score 低于此值不采纳,槽位留空——黑底垫轨默认开,留空处露的是黑底(要露主轨口播画面得配 --no-black-bed)。" + "调高会收缩取材池、可能整段无槽位铺成纯黑,调完先看铺轨输出的空洞告警(默认 0.2)").option("--no-black-bed", "不铺纯黑底垫轨(默认铺一条,垫在候选轨之下、口播主轨之上,用于 B-roll 期间遮住口播画面)").option("--force-relay", "候选轨已被你在客户端编辑过(改过 clip / 确认过原片)时仍强制剥离重铺:缺省会拒铺并保留那条轨,本开关是逃生门——" + "会删除已确认原片的 broll-raw-* 素材登记,盘上已下载的原片文件就地成孤儿,且那条轨上的编辑不可恢复").option("--out <file>", "ad-hoc 模式:结果落文件(缺省输出 stdout)").option("--json", "机读模式:人读日志转 stderr,stdout 只输出结果 JSON").action(async (words, opts) => {
7993
8726
  await runMatrix(parseAdhocQuery(words), opts);
7994
8727
  });
7995
8728
  }
@@ -8037,18 +8770,40 @@ async function runPlanMode(cfg, tier, broll, overrides, columnId, opts) {
8037
8770
  let dispatchPath;
8038
8771
  let baseDir;
8039
8772
  if (opts.dispatch) {
8040
- dispatchPath = resolve8(opts.dispatch);
8773
+ dispatchPath = resolve9(opts.dispatch);
8041
8774
  baseDir = dirname7(dirname7(dispatchPath));
8042
8775
  } else if (opts.project) {
8043
- baseDir = resolve8(opts.project);
8044
- dispatchPath = join19(baseDir, "split", "dispatch.json");
8776
+ baseDir = resolve9(opts.project);
8777
+ dispatchPath = join20(baseDir, "split", "dispatch.json");
8045
8778
  } else {
8046
8779
  throw new Error('需 --project <目录> 或显式 --dispatch <path>(ad-hoc 检索用:gtrk matrix search "<query>")');
8047
8780
  }
8048
- if (!existsSync15(dispatchPath))
8781
+ if (!existsSync17(dispatchPath))
8049
8782
  throw new Error(`找不到派单清单:${dispatchPath}(先跑 gtrk split <拆分稿> 落地派单)`);
8050
- const dispatch = JSON.parse(await readFile5(dispatchPath, "utf8"));
8051
- const queue = Array.isArray(dispatch.film_broll) ? dispatch.film_broll : [];
8783
+ const dispatch = JSON.parse(await readFile6(dispatchPath, "utf8"));
8784
+ const rawQueue = Array.isArray(dispatch.film_broll) ? dispatch.film_broll : [];
8785
+ const earlyGtrkPath = locateGtrk(baseDir);
8786
+ let earlyGtrk;
8787
+ let earlyUnreadable = false;
8788
+ if (earlyGtrkPath) {
8789
+ try {
8790
+ earlyGtrk = readGtrk(earlyGtrkPath).gtrk;
8791
+ } catch {
8792
+ earlyUnreadable = true;
8793
+ }
8794
+ }
8795
+ const reproj = await reprojectDispatchWindows({
8796
+ baseDir,
8797
+ gtrk: earlyGtrk,
8798
+ gtrkUnreadable: earlyUnreadable,
8799
+ entries: rawQueue.map((e) => ({ key: e.beat, beat: e.beat, span: e.span, track_st: e.track_st, track_ed: e.track_ed }))
8800
+ });
8801
+ reportReprojection(reproj);
8802
+ const droppedBeats = new Set(reproj.summary.dropped);
8803
+ const queue = rawQueue.filter((e) => !droppedBeats.has(e.beat)).map((e) => {
8804
+ const win = reproj.windows.get(e.beat);
8805
+ return win ? { ...e, track_st: win.track_st, track_ed: win.track_ed } : e;
8806
+ });
8052
8807
  log.step(`▶ B-roll 检索:${queue.length} 个 beat(${tier} 口)…`);
8053
8808
  const beats = [];
8054
8809
  let okCount = 0;
@@ -8075,12 +8830,12 @@ async function runPlanMode(cfg, tier, broll, overrides, columnId, opts) {
8075
8830
  beats.push(buildPlanBeat(entry, outcomes));
8076
8831
  }
8077
8832
  const totalQueries = okCount + errCount;
8078
- if (queue.length === 0)
8833
+ if (rawQueue.length === 0)
8079
8834
  log.warn("无 B-roll 派单(film_broll 队列为空)——照常写出空 plan");
8080
8835
  if (totalQueries > 0 && okCount === 0) {
8081
8836
  throw new Error(`全部 ${totalQueries} 个 query 检索失败,未写入 plan(逐条原因见上方日志)`);
8082
8837
  }
8083
- const projectSlug = slugify2(basename9(baseDir));
8838
+ const projectSlug = slugify3(basename10(baseDir));
8084
8839
  const plan = buildPlan({
8085
8840
  generatedAt: new Date().toISOString(),
8086
8841
  memberType: tier,
@@ -8088,26 +8843,33 @@ async function runPlanMode(cfg, tier, broll, overrides, columnId, opts) {
8088
8843
  columnId,
8089
8844
  beats
8090
8845
  });
8091
- const splitDir = join19(baseDir, "split");
8846
+ const splitDir = join20(baseDir, "split");
8092
8847
  await mkdir6(splitDir, { recursive: true });
8093
- const planPath = join19(splitDir, "broll-plan.json");
8848
+ const planPath = join20(splitDir, "broll-plan.json");
8094
8849
  await writeFile7(planPath, JSON.stringify(plan, null, 2));
8095
8850
  log.ok(`候选清单已生成:${planPath}(${beats.length} beat · ${okCount}/${totalQueries} query 成功 · ${resultCount} 条候选)`);
8096
8851
  log.info("清单只含引用不含素材:cover_url 可直接预览;url 带签名默认 24h 过期,过期重跑本命令即重签。");
8097
8852
  const layN = parseLay(opts.lay);
8098
- let laySummary;
8853
+ let laid;
8099
8854
  if (layN > 0) {
8100
- laySummary = await layIntoProject(baseDir, plan, layN, parseScoreFloor(opts.scoreFloor), opts.blackBed ?? true);
8855
+ laid = await layIntoProject(baseDir, plan, layN, parseScoreFloor(opts.scoreFloor), opts.blackBed ?? true, opts.forceRelay === true, reproj);
8101
8856
  }
8857
+ const laySummary = laid?.lay;
8858
+ const refused = laySummary?.refused === true ? laySummary.keptEditedTracks : undefined;
8102
8859
  const result = {
8103
- ok: true,
8860
+ ok: refused === undefined,
8104
8861
  mode: "plan",
8105
8862
  memberType: tier,
8106
8863
  ...columnId ? { columnId } : {},
8107
8864
  planPath,
8865
+ ...refused ? { refused, reason: "tracks_edited", planReusable: true } : {},
8108
8866
  ...laySummary ? { lay: laySummary } : {},
8867
+ ...laid?.integrity ? { integrity: laid.integrity } : {},
8868
+ reprojection: reproj.summary,
8109
8869
  counts: { beats: beats.length, queries: totalQueries, results: resultCount, errors: errCount }
8110
8870
  };
8871
+ if (!result.ok)
8872
+ process.exitCode = 1;
8111
8873
  if (opts.json)
8112
8874
  console.log(JSON.stringify(result));
8113
8875
  return result;
@@ -8131,13 +8893,13 @@ function parseScoreFloor(raw) {
8131
8893
  return SCORE_FLOOR_DEFAULT;
8132
8894
  }
8133
8895
  function locateGtrk(baseDir) {
8134
- const cands = [join19(baseDir, "gtrk", "project.gtrk"), join19(baseDir, "project.gtrk")];
8135
- return cands.find((p) => existsSync15(p));
8896
+ const cands = [join20(baseDir, "gtrk", "project.gtrk"), join20(baseDir, "project.gtrk")];
8897
+ return cands.find((p) => existsSync17(p));
8136
8898
  }
8137
- async function layIntoProject(baseDir, plan, layN, scoreFloor, blackBed) {
8899
+ async function layIntoProject(baseDir, plan, layN, scoreFloor, blackBed, forceRelay, reproj) {
8138
8900
  const gtrkPath = locateGtrk(baseDir);
8139
8901
  if (!gtrkPath) {
8140
- log.warn(`未找到工程文件(${join19(baseDir, "gtrk", "project.gtrk")}),跳过铺轨——plan 已产出,可后续在有工程的目录重跑`);
8902
+ log.warn(`未找到工程文件(${join20(baseDir, "gtrk", "project.gtrk")}),跳过铺轨——plan 已产出,可后续在有工程的目录重跑`);
8141
8903
  return;
8142
8904
  }
8143
8905
  const { gtrk, mtimeMs } = readGtrk(gtrkPath);
@@ -8146,7 +8908,7 @@ async function layIntoProject(baseDir, plan, layN, scoreFloor, blackBed) {
8146
8908
  const slotCount = [...fills.values()].flat().reduce((n, s) => n + s.length, 0);
8147
8909
  log.step(`▶ 候选铺轨(${layN} 轨 · 平铺 ${slotCount} 槽位 · ${clipIds.size} 个 clip,preview 代理)…`);
8148
8910
  const gtrkDir = dirname7(gtrkPath);
8149
- const previewDir = join19(gtrkDir, ...BROLL_PREVIEW_DIR.split("/"));
8911
+ const previewDir = join20(gtrkDir, ...BROLL_PREVIEW_DIR.split("/"));
8150
8912
  await mkdir6(previewDir, { recursive: true });
8151
8913
  const prevSource = new Map;
8152
8914
  const prevBroll = gtrk.struct_meta?.broll;
@@ -8169,8 +8931,8 @@ async function layIntoProject(baseDir, plan, layN, scoreFloor, blackBed) {
8169
8931
  if (!cand)
8170
8932
  continue;
8171
8933
  const rel = `${BROLL_PREVIEW_DIR}/${clipId}.mp4`;
8172
- const abs = join19(gtrkDir, ...rel.split("/"));
8173
- if (existsSync15(abs)) {
8934
+ const abs = join20(gtrkDir, ...rel.split("/"));
8935
+ if (existsSync17(abs)) {
8174
8936
  const prev = prevSource.get(clipId);
8175
8937
  if (prev !== "raw") {
8176
8938
  downloads.set(clipId, { rel, source: prev ?? "preview" });
@@ -8204,15 +8966,37 @@ async function layIntoProject(baseDir, plan, layN, scoreFloor, blackBed) {
8204
8966
  downloads,
8205
8967
  generatedAt: new Date().toISOString(),
8206
8968
  planPath: "split/broll-plan.json",
8207
- blackBed
8969
+ blackBed,
8970
+ forceRelay
8208
8971
  });
8972
+ if (summary.refused) {
8973
+ const list = summary.keptEditedTracks;
8974
+ log.err(`拒绝铺轨:${list.length} 条候选轨已被你在客户端编辑过(track_index ${list.join("/") || "-"})——` + "本次不剥它们、也不铺新轨,工程文件零改动。");
8975
+ for (const w of warnings)
8976
+ log.warn(w);
8977
+ log.warn("下一步二选一:① 在客户端处置那条轨(删掉 / 移走 / 改用别的轨)后重跑本命令;" + "② 确知要丢弃那条轨上的编辑 → 加 --force-relay 强制剥离重铺" + "(会删掉已确认原片的 broll-raw-* 素材登记,盘上原片文件成孤儿,不可恢复)。");
8978
+ log.warn("已产出的 broll-plan.json 与已落盘的 preview 代理照常可用——拒的只是「改工程」这一步。");
8979
+ return {
8980
+ lay: {
8981
+ refused: true,
8982
+ keptEditedTracks: list,
8983
+ laidTracks: [],
8984
+ laidClips: 0,
8985
+ removedTracks: [],
8986
+ blackTrack: null,
8987
+ blackBedHoleSec: 0,
8988
+ blackBedHoles: [],
8989
+ downloads: dlStats
8990
+ }
8991
+ };
8992
+ }
8209
8993
  if (summary.blackTrack !== null) {
8210
8994
  const canvas = gtrk.video_size;
8211
8995
  const spec = { hex: BLACK_BED_HEX, width: canvas[0], height: canvas[1] };
8212
8996
  const rel = solidRelPath(spec);
8213
- const abs = join19(gtrkDir, ...rel.split("/"));
8997
+ const abs = join20(gtrkDir, ...rel.split("/"));
8214
8998
  try {
8215
- if (!existsSync15(abs)) {
8999
+ if (!existsSync17(abs)) {
8216
9000
  await mkdir6(dirname7(abs), { recursive: true });
8217
9001
  const tmp = `${abs}.tmp-${process.pid}`;
8218
9002
  await writeFile7(tmp, encodeSolidPng(spec));
@@ -8228,24 +9012,39 @@ async function layIntoProject(baseDir, plan, layN, scoreFloor, blackBed) {
8228
9012
  downloads,
8229
9013
  generatedAt: new Date().toISOString(),
8230
9014
  planPath: "split/broll-plan.json",
8231
- blackBed: false
9015
+ blackBed: false,
9016
+ forceRelay
8232
9017
  }));
8233
9018
  }
8234
9019
  }
8235
- writeGtrkAtomic(gtrkPath, next, mtimeMs);
9020
+ const written = withTimecodeSource(next, "broll", reproj);
9021
+ writeGtrkAtomic(gtrkPath, written, mtimeMs);
9022
+ const integrity = safeCheckMaterialIntegrity({ gtrk: written, gtrkDir, log });
8236
9023
  const bedNote = summary.blackTrack !== null ? ` · 纯黑底垫轨 track_index ${summary.blackTrack}` : blackBed ? " · 未铺纯黑底垫轨" : " · 纯黑底垫轨已关闭(--no-black-bed)";
8237
- log.ok(`铺轨完成:${summary.laidTracks.length} 条候选轨(track_index ${summary.laidTracks.join("/") || "-"})· 平铺 ${summary.laidClips} 个颗粒 / ${clipIds.size} 个 clip` + `(代理 ${dlStats.preview} · 原片回落 ${dlStats.raw} · 复用 ${dlStats.reused}${dlStats.failed ? ` · 失败 ${dlStats.failed}` : ""})${bedNote}`);
9024
+ const stripNote = `剥离 ${summary.removedTracks.length} 条旧自产轨` + (summary.removedTracks.length ? `(track_index ${summary.removedTracks.join("/")})` : "") + (forceRelay ? "(含 --force-relay 强剥的已编辑轨)" : "") + " · ";
9025
+ const keptNote = summary.keptEditedTracks.length ? ` · 保留 ${summary.keptEditedTracks.length} 条已被你编辑的轨(track_index ${summary.keptEditedTracks.join("/")},本次未剥,因由见下方告警)` : "";
9026
+ log.ok(`铺轨完成:${stripNote}${summary.laidTracks.length} 条候选轨(track_index ${summary.laidTracks.join("/") || "-"})· 平铺 ${summary.laidClips} 个颗粒 / ${clipIds.size} 个 clip` + `(代理 ${dlStats.preview} · 原片回落 ${dlStats.raw} · 复用 ${dlStats.reused}${dlStats.failed ? ` · 失败 ${dlStats.failed}` : ""})${bedNote}${keptNote}`);
8238
9027
  log.info("opencut 打开工程即见候选轨:轨道头小眼睛可开关对比;确认下载原片属挑选 UI(E-P1)。");
8239
9028
  for (const w of warnings)
8240
9029
  log.warn(w);
8241
9030
  if (dlStats.raw > 0) {
8242
9031
  log.warn("部分候选无 preview 代理已回落原片(体积较大)——服务端 backfill 后重跑本命令可换回代理。");
8243
9032
  }
9033
+ if (integrity)
9034
+ reportMaterialIntegrity(integrity, log);
8244
9035
  return {
8245
- laidTracks: summary.laidTracks,
8246
- laidClips: summary.laidClips,
8247
- blackTrack: summary.blackTrack,
8248
- downloads: dlStats
9036
+ lay: {
9037
+ refused: false,
9038
+ laidTracks: summary.laidTracks,
9039
+ laidClips: summary.laidClips,
9040
+ removedTracks: summary.removedTracks,
9041
+ keptEditedTracks: summary.keptEditedTracks,
9042
+ blackTrack: summary.blackTrack,
9043
+ blackBedHoleSec: summary.blackBedHoleSec,
9044
+ blackBedHoles: summary.blackBedHoles,
9045
+ downloads: dlStats
9046
+ },
9047
+ ...integrity ? { integrity } : {}
8249
9048
  };
8250
9049
  }
8251
9050
  async function downloadProxy(cand, absPath, opts = {}) {
@@ -8293,7 +9092,7 @@ async function runAdhoc(query, cfg, tier, broll, overrides, columnId, opts) {
8293
9092
  counts: { beats: 0, queries: 1, results: results.length, errors: 0 }
8294
9093
  };
8295
9094
  if (opts.out) {
8296
- const outPath = resolve8(opts.out);
9095
+ const outPath = resolve9(opts.out);
8297
9096
  await writeFile7(outPath, JSON.stringify({ query, recalled: data.recalled, results }, null, 2));
8298
9097
  log.ok(`结果已落盘:${outPath}`);
8299
9098
  result.outPath = outPath;
@@ -8307,15 +9106,15 @@ async function runAdhoc(query, cfg, tier, broll, overrides, columnId, opts) {
8307
9106
  console.log(JSON.stringify(result));
8308
9107
  return result;
8309
9108
  }
8310
- function slugify2(name) {
9109
+ function slugify3(name) {
8311
9110
  const s = name.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "-").replace(/^-+|-+$/g, "");
8312
9111
  return s || "project";
8313
9112
  }
8314
9113
 
8315
9114
  // src/commands/mg.ts
8316
- import { resolve as resolve9, join as join20, dirname as dirname8, basename as basename10 } from "node:path";
8317
- import { existsSync as existsSync16 } from "node:fs";
8318
- import { readFile as readFile6, mkdir as mkdir7, copyFile } from "node:fs/promises";
9115
+ import { resolve as resolve10, join as join21, dirname as dirname8, basename as basename11 } from "node:path";
9116
+ import { existsSync as existsSync18 } from "node:fs";
9117
+ import { readFile as readFile7, mkdir as mkdir7, copyFile } from "node:fs/promises";
8319
9118
 
8320
9119
  // src/lib/mg-lint.ts
8321
9120
  var CDN_OK = [/lib\.baomitu\.com/i, /cdnjs\.cloudflare\.com/i, /unpkg\.com/i];
@@ -8328,16 +9127,68 @@ function attr(tag, name) {
8328
9127
  const m = tag.match(new RegExp(`\\b${name}\\s*=\\s*["']([^"']*)["']`, "i"));
8329
9128
  return m ? m[1] : undefined;
8330
9129
  }
8331
- function deriveOpaque(rootTagStr) {
9130
+ function parseCompositionId(html) {
9131
+ const root = rootTag(html);
9132
+ return root ? attr(root, "data-composition-id") : undefined;
9133
+ }
9134
+ var NON_RENDERING = ["style", "script", "meta", "link", "title"];
9135
+ function firstChildTag(html, rootTagStr) {
8332
9136
  if (!rootTagStr)
8333
- return { opaque: false, declared: false };
8334
- const style = attr(rootTagStr, "style") ?? "";
9137
+ return null;
9138
+ const at = html.indexOf(rootTagStr);
9139
+ if (at < 0)
9140
+ return null;
9141
+ let i = at + rootTagStr.length;
9142
+ for (;; ) {
9143
+ while (i < html.length && /\s/.test(html[i]))
9144
+ i++;
9145
+ if (html.startsWith("<!--", i)) {
9146
+ const end = html.indexOf("-->", i + 4);
9147
+ if (end < 0)
9148
+ return null;
9149
+ i = end + 3;
9150
+ continue;
9151
+ }
9152
+ const m = /^<([a-zA-Z][\w-]*)\b[^>]*>/.exec(html.slice(i));
9153
+ if (!m)
9154
+ return null;
9155
+ const name = m[1].toLowerCase();
9156
+ if (!NON_RENDERING.includes(name))
9157
+ return m[0];
9158
+ const close = new RegExp(`</${name}\\s*>`, "i").exec(html.slice(i));
9159
+ i += close ? close.index + close[0].length : m[0].length;
9160
+ }
9161
+ }
9162
+ function isFullBleed(tagStr) {
9163
+ const style = (attr(tagStr, "style") ?? "").toLowerCase();
9164
+ if (!/position\s*:\s*(?:absolute|fixed)/.test(style))
9165
+ return false;
9166
+ if (/\binset\s*:\s*0(?:px|%)?\b/.test(style))
9167
+ return true;
9168
+ const has = (p) => new RegExp(`\\b${p}\\s*:\\s*0(?:px|%)?\\s*(?:;|$)`).test(style);
9169
+ const full = (p) => new RegExp(`\\b${p}\\s*:\\s*(?:100%|100vw|100vh|1920px|1080px)\\s*(?:;|$)`).test(style);
9170
+ return has("top") && has("left") && full("width") && full("height");
9171
+ }
9172
+ function bgOf(style) {
8335
9173
  const bg = style.match(/background(?:-color)?\s*:\s*([^;"']+)/i);
8336
9174
  if (!bg)
8337
- return { opaque: false, declared: false };
9175
+ return { declared: false, opaque: false };
8338
9176
  const val = bg[1].trim().toLowerCase();
8339
9177
  const transparent = val === "transparent" || val === "none" || /rgba\([^)]*,\s*0\s*\)/.test(val);
8340
- return { opaque: !transparent, declared: true };
9178
+ return { declared: true, opaque: !transparent };
9179
+ }
9180
+ function deriveOpaque(rootTagStr, childTagStr) {
9181
+ if (!rootTagStr)
9182
+ return { opaque: false, declared: false, solidOnRoot: false, solidOnChild: false };
9183
+ const root = bgOf(attr(rootTagStr, "style") ?? "");
9184
+ const childFull = childTagStr !== null && isFullBleed(childTagStr);
9185
+ const child = childFull ? bgOf(attr(childTagStr, "style") ?? "") : { declared: false, opaque: false };
9186
+ return {
9187
+ opaque: root.opaque || child.opaque,
9188
+ declared: root.declared || child.declared,
9189
+ solidOnRoot: root.opaque,
9190
+ solidOnChild: child.opaque
9191
+ };
8341
9192
  }
8342
9193
  var CATEGORY_EXPECTED_OPAQUE2 = {
8343
9194
  overlay: false,
@@ -8349,12 +9200,25 @@ var CATEGORY_EXPECTED_OPAQUE2 = {
8349
9200
  "explain-subtitle": false,
8350
9201
  "op-ed-title": true
8351
9202
  };
9203
+ var NUM_LIT = /^-?\d+(?:\.\d+)?$/;
9204
+ function rawProp(body, key) {
9205
+ const m = new RegExp(`\\b${key}\\s*:\\s*([^,}\\n]+)`).exec(body);
9206
+ return m ? m[1].trim() : undefined;
9207
+ }
9208
+ function numProp(body, key) {
9209
+ const raw = rawProp(body, key);
9210
+ if (raw === undefined)
9211
+ return null;
9212
+ return NUM_LIT.test(raw) ? Number(raw) : Number.NaN;
9213
+ }
8352
9214
  function estimateTimelineSec(html) {
8353
- const re = /\.\s*(?:to|from|fromTo|set|add)\s*\(([\s\S]*?)\)\s*;/g;
9215
+ const re = /(?:([A-Za-z_$][\w$]*)\s*)?\.\s*(?:to|from|fromTo|set|add)\s*\(([\s\S]*?)\)\s*;/g;
8354
9216
  const calls = [];
8355
9217
  let m;
8356
9218
  while (m = re.exec(html)) {
8357
- const args = m[1] ?? "";
9219
+ if (m[1] === "gsap")
9220
+ continue;
9221
+ const args = m[2] ?? "";
8358
9222
  const lastBrace = args.lastIndexOf("}");
8359
9223
  let pos = null;
8360
9224
  if (lastBrace >= 0) {
@@ -8364,26 +9228,869 @@ function estimateTimelineSec(html) {
8364
9228
  }
8365
9229
  calls.push({ body: args, pos });
8366
9230
  }
8367
- if (!calls.length)
8368
- return null;
8369
9231
  let chain = 0;
8370
9232
  let maxEnd = 0;
9233
+ let parsed = 0;
9234
+ let skipped = 0;
9235
+ let hasInfiniteRepeat = false;
8371
9236
  for (const c3 of calls) {
8372
- if (/\brepeat\s*:/.test(c3.body) || /\byoyo\s*:\s*true\b/.test(c3.body))
9237
+ if (c3.pos !== null && !NUM_LIT.test(c3.pos)) {
9238
+ skipped++;
9239
+ continue;
9240
+ }
9241
+ const durProp = numProp(c3.body, "duration");
9242
+ if (durProp !== null && (!Number.isFinite(durProp) || durProp < 0)) {
9243
+ skipped++;
9244
+ continue;
9245
+ }
9246
+ const dur = durProp ?? 0;
9247
+ const repProp = numProp(c3.body, "repeat");
9248
+ let span;
9249
+ if (repProp === -1) {
9250
+ hasInfiniteRepeat = true;
9251
+ span = Number.POSITIVE_INFINITY;
9252
+ } else {
9253
+ const rep = repProp !== null && Number.isFinite(repProp) && repProp > 0 ? repProp : 0;
9254
+ const rdProp = numProp(c3.body, "repeatDelay");
9255
+ const rd = rdProp !== null && Number.isFinite(rdProp) && rdProp > 0 ? rdProp : 0;
9256
+ span = dur * (rep + 1) + rd * rep;
9257
+ }
9258
+ const contrib = c3.pos !== null ? Number(c3.pos) + span : span;
9259
+ if (contrib > 0)
9260
+ parsed++;
9261
+ if (c3.pos !== null)
9262
+ maxEnd = Math.max(maxEnd, Number(c3.pos) + span);
9263
+ else
9264
+ chain += span;
9265
+ }
9266
+ const raw = Math.max(chain, maxEnd);
9267
+ const est = Number.isFinite(raw) ? Math.round(raw * 1000) / 1000 : raw;
9268
+ return { est, parsed, skipped, hasInfiniteRepeat };
9269
+ }
9270
+ var TL_HOOKS = ["onUpdate", "onStart", "onComplete", "onRepeat"];
9271
+ var DOM_WRITE = /\.setAttribute(?:NS)?\s*\(|\.textContent\s*=(?!=)|\.inner(?:HTML|Text)\s*=(?!=)|\.style\.[A-Za-z_$][\w$]*\s*=(?!=)|\.style\.setProperty\s*\(|\.classList\s*\.\s*(?:add|remove|toggle|replace)\s*\(/;
9272
+ function skipString(src, i) {
9273
+ const q = src[i];
9274
+ for (let j = i + 1;j < src.length; j++) {
9275
+ if (src[j] === "\\") {
9276
+ j++;
9277
+ continue;
9278
+ }
9279
+ if (src[j] === q)
9280
+ return j;
9281
+ }
9282
+ return src.length;
9283
+ }
9284
+ function braceBlock(src, open2) {
9285
+ let depth = 0;
9286
+ for (let i = open2;i < src.length; i++) {
9287
+ const c3 = src[i];
9288
+ if (c3 === '"' || c3 === "'" || c3 === "`") {
9289
+ i = skipString(src, i);
9290
+ continue;
9291
+ }
9292
+ if (c3 === "/" && src[i + 1] === "/") {
9293
+ const nl = src.indexOf(`
9294
+ `, i);
9295
+ i = nl < 0 ? src.length : nl;
9296
+ continue;
9297
+ }
9298
+ if (c3 === "/" && src[i + 1] === "*") {
9299
+ const e = src.indexOf("*/", i + 2);
9300
+ i = e < 0 ? src.length : e + 1;
9301
+ continue;
9302
+ }
9303
+ if (c3 === "{")
9304
+ depth++;
9305
+ else if (c3 === "}" && --depth === 0)
9306
+ return src.slice(open2 + 1, i);
9307
+ }
9308
+ return src.slice(open2 + 1);
9309
+ }
9310
+ function namedFnBody(html, name) {
9311
+ const n = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
9312
+ const decl = new RegExp(`\\bfunction\\s+${n}\\s*\\([^)]*\\)\\s*\\{`).exec(html);
9313
+ if (decl)
9314
+ return braceBlock(html, decl.index + decl[0].length - 1);
9315
+ const assign = new RegExp(`\\b(?:var|let|const)\\s+${n}\\s*=\\s*(?:function\\s*[\\w$]*\\s*\\([^)]*\\)|\\([^)]*\\)\\s*=>|[A-Za-z_$][\\w$]*\\s*=>)\\s*\\{`).exec(html);
9316
+ if (assign)
9317
+ return braceBlock(html, assign.index + assign[0].length - 1);
9318
+ return null;
9319
+ }
9320
+ var NOT_A_CALL = new Set(["if", "for", "while", "switch", "catch", "return", "function", "typeof", "new", "delete", "void", "in", "of", "do", "else"]);
9321
+ var MERGEABLE_PRIMITIVES = new Set(["line", "rect", "path", "polyline", "polygon"]);
9322
+ var PRIMITIVE_MERGE_MIN = 8;
9323
+ function maskHtmlComments(src) {
9324
+ const out = src.split("");
9325
+ for (let at = src.indexOf("<!--");at >= 0; at = src.indexOf("<!--", at)) {
9326
+ const close = src.indexOf("-->", at + 4);
9327
+ const end = close < 0 ? src.length : close + 3;
9328
+ for (let i = at;i < end; i++)
9329
+ if (out[i] !== "\r" && out[i] !== `
9330
+ `)
9331
+ out[i] = " ";
9332
+ at = end;
9333
+ }
9334
+ return out.join("");
9335
+ }
9336
+ function scriptBodiesOnly(src) {
9337
+ const openRe = /<script\b[^>]*>/gi;
9338
+ const first = openRe.exec(src);
9339
+ if (!first)
9340
+ return src;
9341
+ const out = src.split("").map((c3) => c3 === "\r" || c3 === `
9342
+ ` ? c3 : " ");
9343
+ let open2 = first;
9344
+ while (open2) {
9345
+ const bodyStart = open2.index + open2[0].length;
9346
+ const closeRe = /<\/script\s*>/gi;
9347
+ closeRe.lastIndex = bodyStart;
9348
+ const close = closeRe.exec(src);
9349
+ const bodyEnd = close?.index ?? src.length;
9350
+ for (let i = bodyStart;i < bodyEnd; i++)
9351
+ out[i] = src[i];
9352
+ openRe.lastIndex = close ? close.index + close[0].length : src.length;
9353
+ open2 = openRe.exec(src);
9354
+ }
9355
+ return out.join("");
9356
+ }
9357
+ function maskJsComments(src) {
9358
+ const out = src.split("");
9359
+ const blank = (from, to) => {
9360
+ for (let i = from;i < to; i++)
9361
+ if (out[i] !== "\r" && out[i] !== `
9362
+ `)
9363
+ out[i] = " ";
9364
+ };
9365
+ const canStartRegexAfter = new Set(["(", "[", "{", "=", ":", ",", ";", "!", "?", "&", "|", "+", "-", "*", "%", "^", "~", "<", ">"]);
9366
+ const keywordBeforeRegex = new Set(["return", "case", "throw", "else", "do", "yield", "await"]);
9367
+ const regexStartsAt = (at) => {
9368
+ let prev = at - 1;
9369
+ while (prev >= 0 && /\s/.test(out[prev]))
9370
+ prev--;
9371
+ if (prev < 0 || canStartRegexAfter.has(out[prev]))
9372
+ return true;
9373
+ if (!/[\w$]/.test(out[prev]))
9374
+ return false;
9375
+ let begin = prev;
9376
+ while (begin > 0 && /[\w$]/.test(out[begin - 1]))
9377
+ begin--;
9378
+ return keywordBeforeRegex.has(out.slice(begin, prev + 1).join(""));
9379
+ };
9380
+ for (let i = 0;i < src.length; i++) {
9381
+ const c3 = src[i];
9382
+ if (c3 === '"' || c3 === "'" || c3 === "`") {
9383
+ i = skipString(src, i);
9384
+ continue;
9385
+ }
9386
+ if (src.startsWith("<!--", i)) {
9387
+ const end2 = src.indexOf("-->", i + 4);
9388
+ const to = end2 < 0 ? src.length : end2 + 3;
9389
+ blank(i, to);
9390
+ i = to - 1;
9391
+ continue;
9392
+ }
9393
+ if (c3 === "/" && src[i + 1] === "/") {
9394
+ const end2 = src.indexOf(`
9395
+ `, i + 2);
9396
+ const to = end2 < 0 ? src.length : end2;
9397
+ blank(i, to);
9398
+ i = to - 1;
9399
+ continue;
9400
+ }
9401
+ if (c3 === "/" && src[i + 1] === "*") {
9402
+ const end2 = src.indexOf("*/", i + 2);
9403
+ const to = end2 < 0 ? src.length : end2 + 2;
9404
+ blank(i, to);
9405
+ i = to - 1;
9406
+ continue;
9407
+ }
9408
+ if (c3 !== "/" || src[i + 1] === "=" || !regexStartsAt(i))
9409
+ continue;
9410
+ let inClass = false;
9411
+ let end = -1;
9412
+ for (let j = i + 1;j < src.length; j++) {
9413
+ if (src[j] === "\\") {
9414
+ j++;
9415
+ continue;
9416
+ }
9417
+ if (src[j] === "\r" || src[j] === `
9418
+ `)
9419
+ break;
9420
+ if (src[j] === "[")
9421
+ inClass = true;
9422
+ else if (src[j] === "]")
9423
+ inClass = false;
9424
+ else if (src[j] === "/" && !inClass) {
9425
+ end = j + 1;
9426
+ while (end < src.length && /[A-Za-z]/.test(src[end]))
9427
+ end++;
9428
+ break;
9429
+ }
9430
+ }
9431
+ if (end < 0)
9432
+ continue;
9433
+ blank(i, end);
9434
+ i = end - 1;
9435
+ }
9436
+ return out.join("");
9437
+ }
9438
+ function maskJsStrings(src) {
9439
+ const out = src.split("");
9440
+ for (let i = 0;i < src.length; i++) {
9441
+ const c3 = src[i];
9442
+ if (c3 !== '"' && c3 !== "'" && c3 !== "`")
9443
+ continue;
9444
+ const end = skipString(src, i);
9445
+ for (let j = i;j <= end && j < out.length; j++)
9446
+ if (out[j] !== "\r" && out[j] !== `
9447
+ `)
9448
+ out[j] = " ";
9449
+ i = end;
9450
+ }
9451
+ return out.join("");
9452
+ }
9453
+ function closeParen(src, open2) {
9454
+ let depth = 0;
9455
+ for (let i = open2;i < src.length; i++) {
9456
+ const c3 = src[i];
9457
+ if (c3 === '"' || c3 === "'" || c3 === "`") {
9458
+ i = skipString(src, i);
9459
+ continue;
9460
+ }
9461
+ if (c3 === "/" && src[i + 1] === "/") {
9462
+ const nl = src.indexOf(`
9463
+ `, i);
9464
+ i = nl < 0 ? src.length : nl;
9465
+ continue;
9466
+ }
9467
+ if (c3 === "/" && src[i + 1] === "*") {
9468
+ const e = src.indexOf("*/", i + 2);
9469
+ i = e < 0 ? src.length : e + 1;
9470
+ continue;
9471
+ }
9472
+ if (c3 === "(")
9473
+ depth++;
9474
+ else if (c3 === ")" && --depth === 0)
9475
+ return i;
9476
+ }
9477
+ return -1;
9478
+ }
9479
+ function skipSpace(src, at) {
9480
+ while (at < src.length && /\s/.test(src[at]))
9481
+ at++;
9482
+ return at;
9483
+ }
9484
+ function braceStatementEnd(src, open2) {
9485
+ const body = braceBlock(src, open2);
9486
+ const close = open2 + 1 + body.length;
9487
+ return src[close] === "}" ? close + 1 : src.length;
9488
+ }
9489
+ function statementEnd(src, start) {
9490
+ const at = skipSpace(src, start);
9491
+ if (src[at] === "{")
9492
+ return braceStatementEnd(src, at);
9493
+ const keyword = /^([A-Za-z_$][\w$]*)\b/.exec(src.slice(at))?.[1] ?? "";
9494
+ if (keyword === "if" || keyword === "for" || keyword === "while" || keyword === "with" || keyword === "switch") {
9495
+ const open2 = src.indexOf("(", at + keyword.length);
9496
+ const close = open2 < 0 ? -1 : closeParen(src, open2);
9497
+ if (close < 0)
9498
+ return src.length;
9499
+ if (keyword === "switch") {
9500
+ const block = skipSpace(src, close + 1);
9501
+ return src[block] === "{" ? braceStatementEnd(src, block) : statementEnd(src, block);
9502
+ }
9503
+ const bodyEnd = statementEnd(src, close + 1);
9504
+ if (keyword !== "if")
9505
+ return bodyEnd;
9506
+ const next = skipSpace(src, bodyEnd);
9507
+ return /^else\b/.test(src.slice(next)) ? statementEnd(src, next + 4) : bodyEnd;
9508
+ }
9509
+ if (keyword === "do") {
9510
+ const bodyEnd = statementEnd(src, at + 2);
9511
+ const trailer = skipSpace(src, bodyEnd);
9512
+ if (!/^while\b/.test(src.slice(trailer)))
9513
+ return bodyEnd;
9514
+ const open2 = src.indexOf("(", trailer + 5);
9515
+ const close = open2 < 0 ? -1 : closeParen(src, open2);
9516
+ if (close < 0)
9517
+ return src.length;
9518
+ const semi = skipSpace(src, close + 1);
9519
+ return src[semi] === ";" ? semi + 1 : close + 1;
9520
+ }
9521
+ if (keyword === "try") {
9522
+ let end = statementEnd(src, at + 3);
9523
+ for (;; ) {
9524
+ const next = skipSpace(src, end);
9525
+ if (/^catch\b/.test(src.slice(next))) {
9526
+ let body = skipSpace(src, next + 5);
9527
+ if (src[body] === "(") {
9528
+ const close = closeParen(src, body);
9529
+ if (close < 0)
9530
+ return src.length;
9531
+ body = skipSpace(src, close + 1);
9532
+ }
9533
+ end = statementEnd(src, body);
9534
+ continue;
9535
+ }
9536
+ if (/^finally\b/.test(src.slice(next))) {
9537
+ end = statementEnd(src, next + 7);
9538
+ continue;
9539
+ }
9540
+ return end;
9541
+ }
9542
+ }
9543
+ let parens = 0;
9544
+ let brackets = 0;
9545
+ let braces = 0;
9546
+ for (let i = at;i < src.length; i++) {
9547
+ const c3 = src[i];
9548
+ if (c3 === '"' || c3 === "'" || c3 === "`") {
9549
+ i = skipString(src, i);
9550
+ continue;
9551
+ }
9552
+ if (c3 === "(")
9553
+ parens++;
9554
+ else if (c3 === ")")
9555
+ parens = Math.max(0, parens - 1);
9556
+ else if (c3 === "[")
9557
+ brackets++;
9558
+ else if (c3 === "]")
9559
+ brackets = Math.max(0, brackets - 1);
9560
+ else if (c3 === "{")
9561
+ braces++;
9562
+ else if (c3 === "}") {
9563
+ if (braces === 0 && parens === 0 && brackets === 0)
9564
+ return i;
9565
+ braces = Math.max(0, braces - 1);
9566
+ } else if (c3 === ";" && parens === 0 && brackets === 0 && braces === 0)
9567
+ return i + 1;
9568
+ else if ((c3 === "\r" || c3 === `
9569
+ `) && parens === 0 && brackets === 0 && braces === 0) {
9570
+ let prev = i - 1;
9571
+ while (prev >= at && /\s/.test(src[prev]))
9572
+ prev--;
9573
+ let next = skipSpace(src, i + 1);
9574
+ if (prev >= at && /[\w$)\]'"`}]/.test(src[prev]) && next < src.length && !/[.(\[`?+\-*/%&|^<>=,:]/.test(src[next]))
9575
+ return i;
9576
+ }
9577
+ }
9578
+ return src.length;
9579
+ }
9580
+ function numericForCount(header) {
9581
+ const parts = header.split(";");
9582
+ if (parts.length !== 3)
9583
+ return null;
9584
+ const init = /^(?:(?:var|let|const)\s+)?([A-Za-z_$][\w$]*)\s*=\s*(-?\d+(?:\.\d+)?)\s*$/.exec(parts[0].trim());
9585
+ if (!init)
9586
+ return null;
9587
+ const name = init[1];
9588
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
9589
+ const cond = new RegExp(`^${escaped}\\s*(<=|<|>=|>)\\s*(-?\\d+(?:\\.\\d+)?)\\s*$`).exec(parts[1].trim());
9590
+ if (!cond)
9591
+ return null;
9592
+ const updateRaw = parts[2].trim();
9593
+ let step = null;
9594
+ if (new RegExp(`^(?:${escaped}\\s*\\+\\+|\\+\\+\\s*${escaped})$`).test(updateRaw))
9595
+ step = 1;
9596
+ else if (new RegExp(`^(?:${escaped}\\s*--|--\\s*${escaped})$`).test(updateRaw))
9597
+ step = -1;
9598
+ else {
9599
+ const by = new RegExp(`^${escaped}\\s*([+-])=\\s*(-?\\d+(?:\\.\\d+)?)$`).exec(updateRaw);
9600
+ if (by)
9601
+ step = (by[1] === "+" ? 1 : -1) * Number(by[2]);
9602
+ }
9603
+ if (step === null || !Number.isFinite(step) || step === 0)
9604
+ return null;
9605
+ const from = Number(init[2]);
9606
+ const to = Number(cond[2]);
9607
+ const op = cond[1];
9608
+ if (!Number.isFinite(from) || !Number.isFinite(to))
9609
+ return null;
9610
+ const compare = (value2) => op === "<" ? value2 < to : op === "<=" ? value2 <= to : op === ">" ? value2 > to : value2 >= to;
9611
+ if (!compare(from))
9612
+ return 0;
9613
+ if (step > 0 && op.startsWith(">") || step < 0 && op.startsWith("<"))
9614
+ return null;
9615
+ if (Number.isSafeInteger(from) && Number.isSafeInteger(to) && Number.isSafeInteger(step)) {
9616
+ const distance = step > 0 ? to - from : from - to;
9617
+ if (!Number.isSafeInteger(distance) || distance < 0)
8373
9618
  return null;
8374
- if (c3.pos !== null && !/^-?\d+(?:\.\d+)?$/.test(c3.pos))
9619
+ const stride = Math.abs(step);
9620
+ const count2 = op === "<" || op === ">" ? Math.ceil(distance / stride) : Math.floor(distance / stride) + 1;
9621
+ return Number.isSafeInteger(count2) ? count2 : null;
9622
+ }
9623
+ let value = from;
9624
+ let count = 0;
9625
+ while (compare(value)) {
9626
+ if (++count > 1e6) {
9627
+ const distance = step > 0 ? to - from : from - to;
9628
+ const stride = Math.abs(step);
9629
+ const estimated = op === "<" || op === ">" ? Math.ceil(distance / stride) : Math.floor(distance / stride) + 1;
9630
+ return Number.isSafeInteger(estimated) && estimated >= 0 ? estimated : null;
9631
+ }
9632
+ const next = value + step;
9633
+ if (Object.is(next, value) || !Number.isFinite(next))
8375
9634
  return null;
8376
- const d = /\bduration\s*:\s*(-?\d+(?:\.\d+)?)/.exec(c3.body);
8377
- const dur = d ? Number(d[1]) : 0;
8378
- if (!Number.isFinite(dur) || dur < 0)
9635
+ value = next;
9636
+ }
9637
+ return count;
9638
+ }
9639
+ function loopSites(html) {
9640
+ const out = [];
9641
+ const doWhileTrailers = new Set;
9642
+ const doRe = /\bdo\b/g;
9643
+ let dm;
9644
+ while (dm = doRe.exec(html)) {
9645
+ const bodyEnd = statementEnd(html, dm.index + dm[0].length);
9646
+ const trailer = skipSpace(html, bodyEnd);
9647
+ if (/^while\b/.test(html.slice(trailer)))
9648
+ doWhileTrailers.add(trailer);
9649
+ }
9650
+ const re = /\b(for|while)\s*\(/g;
9651
+ let m;
9652
+ while (m = re.exec(html)) {
9653
+ if (m[1] === "while" && doWhileTrailers.has(m.index))
9654
+ continue;
9655
+ const open2 = m.index + m[0].lastIndexOf("(");
9656
+ const close = closeParen(html, open2);
9657
+ if (close < 0)
9658
+ continue;
9659
+ let at = close + 1;
9660
+ while (at < html.length && /\s/.test(html[at]))
9661
+ at++;
9662
+ let bodyStart = at;
9663
+ let bodyEnd;
9664
+ if (html[at] === "{") {
9665
+ const body = braceBlock(html, at);
9666
+ bodyStart = at + 1;
9667
+ bodyEnd = bodyStart + body.length;
9668
+ } else
9669
+ bodyEnd = statementEnd(html, at);
9670
+ const kind = m[1];
9671
+ const header = html.slice(open2 + 1, close);
9672
+ out.push({
9673
+ kind,
9674
+ bodyStart,
9675
+ bodyEnd,
9676
+ count: kind === "for" ? numericForCount(header) : null,
9677
+ header
9678
+ });
9679
+ }
9680
+ return out;
9681
+ }
9682
+ function namedFunctionSites(code) {
9683
+ const out = [];
9684
+ const invocationAfter = (declStart, bodyEnd, expression) => {
9685
+ let before = declStart - 1;
9686
+ while (before >= 0 && /\s/.test(code[before]))
9687
+ before--;
9688
+ const expressionContext = expression || before >= 0 && /[(!=,:+\-~?]/.test(code[before]);
9689
+ let after = bodyEnd + 1;
9690
+ while (after < code.length && /\s/.test(code[after]))
9691
+ after++;
9692
+ if (code[after] === "(")
9693
+ return expressionContext;
9694
+ if (code[before] !== "(")
9695
+ return false;
9696
+ let groupingCount = 0;
9697
+ let beforeOpen = before;
9698
+ while (beforeOpen >= 0 && code[beforeOpen] === "(") {
9699
+ groupingCount++;
9700
+ beforeOpen--;
9701
+ while (beforeOpen >= 0 && /\s/.test(code[beforeOpen]))
9702
+ beforeOpen--;
9703
+ }
9704
+ if (beforeOpen >= 0 && /[\w$)\]]/.test(code[beforeOpen]))
9705
+ return false;
9706
+ for (let i = 0;i < groupingCount; i++) {
9707
+ if (code[after] !== ")")
9708
+ return false;
9709
+ after = skipSpace(code, after + 1);
9710
+ }
9711
+ return expressionContext && code[after] === "(";
9712
+ };
9713
+ const patterns = [
9714
+ {
9715
+ re: /\b(?:var|let|const)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:(?:async\s+)?function\s*\*?\s*[\w$]*\s*\([^)]*\)|(?:async\s*)?(?:\([^)]*\)|[A-Za-z_$][\w$]*)\s*=>)\s*\{/g,
9716
+ nameGroup: 1,
9717
+ expression: true
9718
+ },
9719
+ {
9720
+ re: /\b(?:async\s+)?function\s*\*?\s+([A-Za-z_$][\w$]*)\s*\([^)]*\)\s*\{/g,
9721
+ nameGroup: 1,
9722
+ expression: false
9723
+ },
9724
+ {
9725
+ re: /\b(?:async\s+)?function\s*\*?\s*\([^)]*\)\s*\{/g,
9726
+ nameGroup: null,
9727
+ expression: true
9728
+ },
9729
+ {
9730
+ re: /(?:async\s*)?(?:\([^)]*\)|[A-Za-z_$][\w$]*)\s*=>\s*\{/g,
9731
+ nameGroup: null,
9732
+ expression: true
9733
+ }
9734
+ ];
9735
+ for (const { re, nameGroup, expression } of patterns) {
9736
+ let m;
9737
+ while (m = re.exec(code)) {
9738
+ const open2 = m.index + m[0].lastIndexOf("{");
9739
+ const paired = braceBlock(code, open2);
9740
+ const bodyStart = open2 + 1;
9741
+ const bodyEnd = bodyStart + paired.length;
9742
+ if (out.some((fn) => fn.bodyStart === bodyStart))
9743
+ continue;
9744
+ const name = nameGroup === null ? null : m[nameGroup];
9745
+ const signature = m[0].slice(0, m[0].lastIndexOf("{"));
9746
+ const parenGroups = [...signature.matchAll(/\(([^()]*)\)/g)];
9747
+ const singleArrow = /=\s*([A-Za-z_$][\w$]*)\s*=>\s*$/.exec(signature);
9748
+ const rawParams = parenGroups.at(-1)?.[1] ?? singleArrow?.[1] ?? "";
9749
+ const params = new Set(rawParams.split(",").map((part) => /^([A-Za-z_$][\w$]*)/.exec(part.trim())?.[1]).filter((name2) => Boolean(name2)));
9750
+ out.push({
9751
+ name,
9752
+ declStart: m.index,
9753
+ bodyStart,
9754
+ bodyEnd,
9755
+ body: paired,
9756
+ params,
9757
+ immediatelyInvoked: invocationAfter(m.index, bodyEnd, expression)
9758
+ });
9759
+ }
9760
+ }
9761
+ const functionRe = /\b(?:async\s+)?function\s*\*?\s*([A-Za-z_$][\w$]*)?\s*\(/g;
9762
+ let fm;
9763
+ while (fm = functionRe.exec(code)) {
9764
+ const open2 = fm.index + fm[0].lastIndexOf("(");
9765
+ const close = closeParen(code, open2);
9766
+ if (close < 0)
9767
+ continue;
9768
+ let brace = close + 1;
9769
+ while (brace < code.length && /\s/.test(code[brace]))
9770
+ brace++;
9771
+ if (code[brace] !== "{")
9772
+ continue;
9773
+ const paired = braceBlock(code, brace);
9774
+ const bodyStart = brace + 1;
9775
+ const bodyEnd = bodyStart + paired.length;
9776
+ if (out.some((fn) => fn.bodyStart === bodyStart))
9777
+ continue;
9778
+ const prefix = code.slice(Math.max(0, fm.index - 160), fm.index);
9779
+ const assigned = /\b(?:var|let|const)\s+([A-Za-z_$][\w$]*)\s*=\s*$/.exec(prefix);
9780
+ const name = assigned?.[1] ?? fm[1] ?? null;
9781
+ const params = new Set(code.slice(open2 + 1, close).split(",").map((part) => /^([A-Za-z_$][\w$]*)/.exec(part.trim())?.[1]).filter((param) => Boolean(param)));
9782
+ out.push({
9783
+ name,
9784
+ declStart: fm.index,
9785
+ bodyStart,
9786
+ bodyEnd,
9787
+ body: paired,
9788
+ params,
9789
+ immediatelyInvoked: invocationAfter(fm.index, bodyEnd, assigned !== null || fm[1] === undefined)
9790
+ });
9791
+ }
9792
+ const arrowRe = /\b(?:var|let|const)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s*)?\(/g;
9793
+ let am;
9794
+ while (am = arrowRe.exec(code)) {
9795
+ const open2 = am.index + am[0].lastIndexOf("(");
9796
+ const close = closeParen(code, open2);
9797
+ if (close < 0)
9798
+ continue;
9799
+ let arrow = skipSpace(code, close + 1);
9800
+ if (!code.startsWith("=>", arrow))
9801
+ continue;
9802
+ const brace = skipSpace(code, arrow + 2);
9803
+ if (code[brace] !== "{")
9804
+ continue;
9805
+ const paired = braceBlock(code, brace);
9806
+ const bodyStart = brace + 1;
9807
+ const bodyEnd = bodyStart + paired.length;
9808
+ if (out.some((fn) => fn.bodyStart === bodyStart))
9809
+ continue;
9810
+ const params = new Set(code.slice(open2 + 1, close).split(",").map((part) => /^([A-Za-z_$][\w$]*)/.exec(part.trim())?.[1]).filter((param) => Boolean(param)));
9811
+ out.push({
9812
+ name: am[1],
9813
+ declStart: am.index,
9814
+ bodyStart,
9815
+ bodyEnd,
9816
+ body: paired,
9817
+ params,
9818
+ immediatelyInvoked: invocationAfter(am.index, bodyEnd, true)
9819
+ });
9820
+ }
9821
+ return out.sort((a, b) => a.declStart - b.declStart);
9822
+ }
9823
+ var CREATE_PRIMITIVE = /\b(?:(?:var|let|const)\s+)?([A-Za-z_$][\w$]*)\s*=(?!=)\s*document\s*\.\s*createElementNS\s*\(\s*[^,]+,\s*["']([A-Za-z][\w-]*)["']\s*\)/gi;
9824
+ function escapedIdent(name) {
9825
+ return name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
9826
+ }
9827
+ function appendedParent(body, elVar, baseAt, sameExecutionScope) {
9828
+ const el = escapedIdent(elVar);
9829
+ const appendRe = new RegExp(`\\b([A-Za-z_$][\\w$]*)\\s*\\.\\s*appendChild\\s*\\(\\s*${el}\\s*\\)`, "g");
9830
+ let append;
9831
+ while ((append = appendRe.exec(body)) && !sameExecutionScope(baseAt + append.index))
9832
+ ;
9833
+ if (!append)
9834
+ return null;
9835
+ const reassignRe = new RegExp(`\\b(?:(?:var|let|const)\\s+)?${el}\\s*=(?!=)`, "g");
9836
+ let reassign;
9837
+ while (reassign = reassignRe.exec(body)) {
9838
+ if (reassign.index >= append.index)
9839
+ break;
9840
+ if (body[reassign.index - 1] === ".")
9841
+ continue;
9842
+ if (sameExecutionScope(baseAt + reassign.index))
8379
9843
  return null;
8380
- if (c3.pos !== null)
8381
- maxEnd = Math.max(maxEnd, Number(c3.pos) + dur);
8382
- else
8383
- chain += dur;
8384
9844
  }
8385
- const est = Math.max(chain, maxEnd);
8386
- return est > 0 ? Math.round(est * 1000) / 1000 : null;
9845
+ return append[1];
9846
+ }
9847
+ function perElementDriven(html, body, elVar) {
9848
+ const el = escapedIdent(elVar);
9849
+ if (new RegExp(`\\bgsap\\s*\\.\\s*set\\s*\\(\\s*${el}\\b`).test(body))
9850
+ return true;
9851
+ if (new RegExp(`\\b[A-Za-z_$][\\w$]*\\s*\\.\\s*(?:to|from|fromTo)\\s*\\(\\s*${el}\\b`).test(body))
9852
+ return true;
9853
+ const pushes = new Set;
9854
+ for (const pm of body.matchAll(new RegExp(`\\b([A-Za-z_$][\\w$]*)(?:\\s*\\[[^\\]]+\\])?\\s*\\.\\s*push\\s*\\(\\s*${el}\\s*\\)`, "g")))
9855
+ pushes.add(pm[1]);
9856
+ for (const arr of pushes) {
9857
+ const a = escapedIdent(arr);
9858
+ if (new RegExp(`\\b[A-Za-z_$][\\w$]*\\s*\\.\\s*(?:to|from|fromTo)\\s*\\(\\s*${a}(?:\\s*\\[[^\\]]+\\])?\\s*,`).test(html))
9859
+ return true;
9860
+ }
9861
+ return false;
9862
+ }
9863
+ function detectPrimitiveLoops(html) {
9864
+ const source = maskJsComments(scriptBodiesOnly(maskHtmlComments(html)));
9865
+ const code = maskJsStrings(source);
9866
+ const loops = loopSites(code);
9867
+ const functions = namedFunctionSites(code);
9868
+ const containingFunction = (at) => functions.filter((fn) => at >= fn.bodyStart && at < fn.bodyEnd).sort((a, b) => a.bodyEnd - a.bodyStart - (b.bodyEnd - b.bodyStart))[0] ?? null;
9869
+ const functionOwner = (fn) => containingFunction(fn.declStart);
9870
+ const hasOwnVariableBinding = (scope, name) => {
9871
+ if (scope.params.has(name))
9872
+ return true;
9873
+ const re = new RegExp(`\\b(?:var|let|const|class)\\s+${escapedIdent(name)}\\b`, "g");
9874
+ re.lastIndex = scope.bodyStart;
9875
+ let binding;
9876
+ while ((binding = re.exec(code)) && binding.index < scope.bodyEnd)
9877
+ if (containingFunction(binding.index) === scope)
9878
+ return true;
9879
+ return false;
9880
+ };
9881
+ const resolveFunction = (name, at) => {
9882
+ let scope = containingFunction(at);
9883
+ while (scope) {
9884
+ const local = functions.filter((fn) => fn.name === name && functionOwner(fn) === scope).sort((a, b) => b.declStart - a.declStart)[0];
9885
+ if (local)
9886
+ return local;
9887
+ if (scope.name === name)
9888
+ return scope;
9889
+ if (hasOwnVariableBinding(scope, name))
9890
+ return null;
9891
+ scope = functionOwner(scope);
9892
+ }
9893
+ return functions.filter((fn) => fn.name === name && functionOwner(fn) === null).sort((a, b) => b.declStart - a.declStart)[0] ?? null;
9894
+ };
9895
+ const parentBindingScope = (fn, parent) => {
9896
+ let scope = fn;
9897
+ while (scope) {
9898
+ if (hasOwnVariableBinding(scope, parent))
9899
+ return scope;
9900
+ scope = functionOwner(scope);
9901
+ }
9902
+ return null;
9903
+ };
9904
+ const freshParentDeclaration = (parent, before, scope) => {
9905
+ const ident = escapedIdent(parent);
9906
+ const re = new RegExp(`\\b(?:var|let|const)\\s+${ident}\\s*=\\s*(?:document\\s*\\.\\s*createElement(?:NS)?\\s*\\(|new\\b)`, "g");
9907
+ re.lastIndex = scope?.bodyStart ?? 0;
9908
+ let found = null;
9909
+ let m;
9910
+ while ((m = re.exec(code)) && m.index < before && (!scope || m.index < scope.bodyEnd))
9911
+ if (containingFunction(m.index) === scope)
9912
+ found = m.index;
9913
+ return found;
9914
+ };
9915
+ const loopsForOneParent = (owned, freshAt) => freshAt === null ? owned : owned.filter((loop) => freshAt < loop.bodyStart || freshAt >= loop.bodyEnd);
9916
+ const executionLoopsAt = (at) => {
9917
+ const fn = containingFunction(at);
9918
+ return loops.filter((loop) => {
9919
+ if (at < loop.bodyStart || at >= loop.bodyEnd)
9920
+ return false;
9921
+ if (fn)
9922
+ return loop.bodyStart >= fn.bodyStart && loop.bodyEnd <= fn.bodyEnd;
9923
+ return containingFunction(loop.bodyStart) === null;
9924
+ });
9925
+ };
9926
+ const loopProduct = (owned) => {
9927
+ if (owned.some((loop) => loop.count === null))
9928
+ return null;
9929
+ let product = 1;
9930
+ for (const loop of owned) {
9931
+ product *= loop.count;
9932
+ if (!Number.isSafeInteger(product))
9933
+ return null;
9934
+ }
9935
+ return product;
9936
+ };
9937
+ const innermostLoopBody = (at, owned = executionLoopsAt(at)) => {
9938
+ const loop = owned.sort((a, b) => a.bodyEnd - a.bodyStart - (b.bodyEnd - b.bodyStart))[0];
9939
+ return loop ? code.slice(loop.bodyStart, loop.bodyEnd) : "";
9940
+ };
9941
+ const grouped = new Map;
9942
+ const add = (tag, parent, count, driven, site, scope) => {
9943
+ const key = `${scope}\x00${tag}\x00${parent}`;
9944
+ const batch = grouped.get(key) ?? { tag, parent, count: 0, unknown: false, perElementDriven: false, sites: new Set };
9945
+ if (count === null)
9946
+ batch.unknown = true;
9947
+ else {
9948
+ batch.count += count;
9949
+ if (!Number.isSafeInteger(batch.count))
9950
+ batch.unknown = true;
9951
+ }
9952
+ batch.perElementDriven ||= driven;
9953
+ batch.sites.add(site);
9954
+ grouped.set(key, batch);
9955
+ };
9956
+ const creates = [];
9957
+ CREATE_PRIMITIVE.lastIndex = 0;
9958
+ let cm;
9959
+ while (cm = CREATE_PRIMITIVE.exec(source)) {
9960
+ if (!code[cm.index] || /\s/.test(code[cm.index]))
9961
+ continue;
9962
+ const tag = cm[2].toLowerCase();
9963
+ if (!MERGEABLE_PRIMITIVES.has(tag))
9964
+ continue;
9965
+ const fn = containingFunction(cm.index);
9966
+ const owned = executionLoopsAt(cm.index);
9967
+ const nearest = owned.slice().sort((a, b) => a.bodyEnd - a.bodyStart - (b.bodyEnd - b.bodyStart))[0];
9968
+ const bodyStart = nearest?.bodyStart ?? fn?.bodyStart ?? cm.index;
9969
+ const body = nearest ? code.slice(nearest.bodyStart, nearest.bodyEnd) : fn?.body ?? "";
9970
+ const tailStart = Math.max(0, cm.index - bodyStart) + cm[0].length;
9971
+ const parent = appendedParent(body.slice(tailStart), cm[1], bodyStart + tailStart, (at) => containingFunction(at) === fn);
9972
+ if (!parent)
9973
+ continue;
9974
+ creates.push({ at: cm.index, tag, elVar: cm[1], parent, fn, loops: owned });
9975
+ }
9976
+ const callsByFunction = new Map;
9977
+ const pushCall = (fn, at) => {
9978
+ const list = callsByFunction.get(fn.declStart) ?? [];
9979
+ list.push({ at, loops: executionLoopsAt(at) });
9980
+ callsByFunction.set(fn.declStart, list);
9981
+ };
9982
+ const functionNames = new Set(functions.map((fn) => fn.name).filter((name) => name !== null));
9983
+ for (const call of code.matchAll(/\b([A-Za-z_$][\w$]*)\s*\(/g)) {
9984
+ const name = call[1];
9985
+ const at = call.index;
9986
+ let before = at - 1;
9987
+ while (before >= 0 && /\s/.test(code[before]))
9988
+ before--;
9989
+ if (NOT_A_CALL.has(name) || !functionNames.has(name) || code[before] === ".")
9990
+ continue;
9991
+ if (functions.some((fn) => fn.name === name && at >= fn.declStart && at < fn.bodyStart))
9992
+ continue;
9993
+ const target = resolveFunction(name, at);
9994
+ if (target)
9995
+ pushCall(target, at);
9996
+ }
9997
+ for (const fn of functions)
9998
+ if (fn.immediatelyInvoked)
9999
+ pushCall(fn, fn.bodyEnd);
10000
+ for (const create of creates) {
10001
+ if (!create.fn) {
10002
+ if (create.loops.length === 0)
10003
+ continue;
10004
+ const nearest = create.loops.slice().sort((a, b) => a.bodyEnd - a.bodyStart - (b.bodyEnd - b.bodyStart))[0];
10005
+ const body = innermostLoopBody(create.at, create.loops);
10006
+ const freshAt = freshParentDeclaration(create.parent, create.at, null);
10007
+ add(create.tag, create.parent, loopProduct(loopsForOneParent(create.loops, freshAt)), perElementDriven(code, body, create.elVar), `${nearest.kind} (${nearest.header.trim()})`, freshAt === null ? "shared-parent" : `fresh-parent:${freshAt}`);
10008
+ continue;
10009
+ }
10010
+ const fnCalls = callsByFunction.get(create.fn.declStart) ?? [];
10011
+ if (fnCalls.length === 0 && create.loops.length > 0) {
10012
+ const nearest = create.loops.slice().sort((a, b) => a.bodyEnd - a.bodyStart - (b.bodyEnd - b.bodyStart))[0];
10013
+ const boundParent = parentBindingScope(create.fn, create.parent);
10014
+ const freshAt = freshParentDeclaration(create.parent, create.at, boundParent);
10015
+ add(create.tag, create.parent, loopProduct(loopsForOneParent(create.loops, freshAt)), perElementDriven(code, innermostLoopBody(create.at, create.loops), create.elVar), `${nearest.kind} (${nearest.header.trim()})`, freshAt !== null ? `fresh-parent:${freshAt}` : boundParent ? `function:${boundParent.declStart}` : "shared-parent");
10016
+ }
10017
+ for (const call of fnCalls) {
10018
+ if (create.loops.length === 0 && call.loops.length === 0)
10019
+ continue;
10020
+ const inside = loopProduct(create.loops);
10021
+ const callerBody = innermostLoopBody(call.at, call.loops);
10022
+ const boundParent = parentBindingScope(create.fn, create.parent);
10023
+ const freshAt = freshParentDeclaration(create.parent, create.at, boundParent);
10024
+ const oneParentInside = loopProduct(loopsForOneParent(create.loops, freshAt));
10025
+ const oneParentOutside = loopProduct(loopsForOneParent(call.loops, freshAt));
10026
+ const freshOwnedByFactory = freshAt !== null && boundParent === create.fn;
10027
+ const count = freshOwnedByFactory ? oneParentInside : oneParentInside === null || oneParentOutside === null ? null : oneParentInside * oneParentOutside;
10028
+ add(create.tag, create.parent, count, perElementDriven(code, `${create.fn.body}
10029
+ ${callerBody}`, create.elVar), create.fn.name ? `factory ${create.fn.name}` : `IIFE @${create.fn.declStart}`, freshAt !== null ? `fresh-parent:${freshAt}` : boundParent ? `function:${boundParent.declStart}` : "shared-parent");
10030
+ }
10031
+ }
10032
+ return [...grouped.values()].map((batch) => ({
10033
+ tag: batch.tag,
10034
+ parent: batch.parent,
10035
+ count: batch.unknown ? null : batch.count,
10036
+ perElementDriven: batch.perElementDriven,
10037
+ site: [...batch.sites].join(" + ")
10038
+ }));
10039
+ }
10040
+ function bodyWritesDom(html, body, hops, seen) {
10041
+ if (DOM_WRITE.test(body))
10042
+ return true;
10043
+ if (hops <= 0)
10044
+ return false;
10045
+ for (const m of body.matchAll(/\b([A-Za-z_$][\w$]*)\s*\(/g)) {
10046
+ const name = m[1];
10047
+ if (NOT_A_CALL.has(name) || seen.has(name))
10048
+ continue;
10049
+ seen.add(name);
10050
+ const b = namedFnBody(html, name);
10051
+ if (b && bodyWritesDom(html, b, hops - 1, seen))
10052
+ return true;
10053
+ }
10054
+ return false;
10055
+ }
10056
+ function detectSeekSignals(html) {
10057
+ const hooks = new Set;
10058
+ const hookRe = new RegExp(`\\b(${TL_HOOKS.join("|")})\\s*:\\s*`, "g");
10059
+ let hm;
10060
+ while (hm = hookRe.exec(html)) {
10061
+ const hook = hm[1];
10062
+ const at = hm.index + hm[0].length;
10063
+ const rest = html.slice(at);
10064
+ let body = null;
10065
+ let mm;
10066
+ if (mm = /^function\s*[A-Za-z_$]?[\w$]*\s*\([^)]*\)\s*\{/.exec(rest)) {
10067
+ body = braceBlock(html, at + mm[0].length - 1);
10068
+ } else if (mm = /^(?:\([^)]*\)|[A-Za-z_$][\w$]*)\s*=>\s*/.exec(rest)) {
10069
+ const after = at + mm[0].length;
10070
+ body = html[after] === "{" ? braceBlock(html, after) : rest.slice(mm[0].length).split(`
10071
+ `)[0];
10072
+ } else if (mm = /^([A-Za-z_$][\w$]*)/.exec(rest)) {
10073
+ body = namedFnBody(html, mm[1]);
10074
+ }
10075
+ if (body && bodyWritesDom(html, body, 2, new Set))
10076
+ hooks.add(hook);
10077
+ }
10078
+ const overrideForms = [];
10079
+ if (/\.\s*seek\s*=(?!=)|\[\s*["']seek["']\s*\]\s*=(?!=)/.test(html))
10080
+ overrideForms.push("重新赋值 `.seek`");
10081
+ for (const rm of html.matchAll(/window\s*\.\s*__timelines\s*\[[^\]]*\]\s*=\s*([^;\n]*)/g)) {
10082
+ const rhs = rm[1].split("//")[0].trim();
10083
+ if (rhs && !/^[A-Za-z_$][\w$]*$/.test(rhs)) {
10084
+ overrideForms.push("`__timelines[…]` 被赋成包装对象而非时间线本体");
10085
+ break;
10086
+ }
10087
+ }
10088
+ return {
10089
+ callbackDom: hooks.size > 0,
10090
+ hooks: TL_HOOKS.filter((h) => hooks.has(h)),
10091
+ engineApiOverride: overrideForms.length > 0,
10092
+ overrideForms
10093
+ };
8387
10094
  }
8388
10095
  function lintParticle(html, opts = {}) {
8389
10096
  const v = [];
@@ -8394,6 +10101,8 @@ function lintParticle(html, opts = {}) {
8394
10101
  const cid = root ? attr(root, "data-composition-id") : undefined;
8395
10102
  if (!root || !cid)
8396
10103
  push("1-composition-id", true, "根元素缺 data-composition-id");
10104
+ if (opts.compositionId && cid && opts.compositionId !== cid)
10105
+ push("1-cid-expect", true, `HTML 内 data-composition-id「${cid}」与期望 id「${opts.compositionId}」不符(复制改名漏改内部 id?)——` + `按期望 id 落轨会写出 clip_id/material 指向「${opts.compositionId}」,而本文件注册的是 __timelines["${cid}"],渲染必错`);
8397
10106
  if (root) {
8398
10107
  if (attr(root, "data-width") !== "1920")
8399
10108
  push("1-width", true, `根 data-width 应为 "1920"(实为 ${attr(root, "data-width") ?? "缺"})`);
@@ -8441,24 +10150,46 @@ function lintParticle(html, opts = {}) {
8441
10150
  if (re.test(html))
8442
10151
  push("4-rel-asset", true, `含相对外链 ${tag}(违反自包含,渲染机读不到)`);
8443
10152
  }
8444
- const { opaque, declared } = deriveOpaque(root);
10153
+ const { opaque, declared, solidOnRoot, solidOnChild } = deriveOpaque(root, firstChildTag(html, root));
8445
10154
  if (root && !declared)
8446
- push("4-bg-explicit", false, "根未显式声明 background(透明与否应明确;缺省按透明叠加 opaque=false 处理)");
10155
+ push("4-bg-explicit", false, "根与根下首个全幅子层**都**没有显式声明 background(契约铁律4③:透明与否 MUST 显式)——" + "满屏颗粒应在根下第一个全幅子层(position:absolute;inset:0)声明实心底;" + "透明叠加颗粒应在根显式写 background:transparent。缺省按透明叠加 opaque=false 处理");
10156
+ if (solidOnRoot && !solidOnChild)
10157
+ push("4-bg-on-root", false, "实心底写在**根 style** 上(契约铁律4①②:MUST 下沉为根下第一个全幅子层,根 MUST 保持零视觉)——" + "**根元素的绘制属性会在子合成挂载时被丢弃**,这层底在成片里一个像素都不落地:" + "前景照常渲出、底没了、底轨透出来,观感是「浮空面板」;而本地播放器与客户端预览都看不出(2026-07-26 r69 真渲实测,见契约铁律4证据锚)。" + "改法是机械的:把这条 background 声明整体搬到根下新增的第一个全幅子层" + '(<div style="position:absolute;inset:0;background:<原底色>;z-index:0;"></div>),根上改写 background:transparent');
8447
10158
  if (/var\(\s*--/.test(html))
8448
10159
  push("6-css-var", true, "含 CSS var(--...)(Hyperframes 不解析→整片全黑,须字面值)");
8449
- const effectiveCid = opts.compositionId ?? cid;
8450
- if (opts.dispatchIds && effectiveCid && !opts.dispatchIds.includes(effectiveCid))
8451
- push("x-dispatch", false, `composition_id "${effectiveCid}" 不在 dispatch.mg 派单中`);
10160
+ const sig = detectSeekSignals(html);
10161
+ if (sig.engineApiOverride)
10162
+ push("x-engine-api-override", false, `颗粒在运行时覆写了渲染引擎所调用的 API(${sig.overrideForms.join("、")})——它会推翻引擎**显式**传入的 ` + `seek(t, true)(颗粒无权静默否决引擎意图),且只在引擎恰好走 seek 时有效,引擎改走 time() / progress() 即完全失效。` + `按契约「回调与 seek 语义」,回调可达性应由**引擎侧**承担,颗粒侧垫片属过渡态——` + `引擎侧结论已于 2026-07-26 经真机核实(引擎 producer 0.6.101 定帧时回调可达),既有垫片可择期清理(删后须重渲复验),新颗粒不应再加`);
10163
+ if (sig.callbackDom && !sig.engineApiOverride)
10164
+ push("x-callback-driven", false, `画面靠时间线回调驱动(${sig.hooks.join(" / ")} 体内写 DOM),且本颗粒无任何 seek 兜底。` + `该写法**合规**——其可达性由契约「回调与 seek 语义」的**引擎侧 MUST 条款**保证` + `(2026-07-26 已真机核实:引擎 producer 0.6.101 定帧时回调可达;结论**绑定该引擎版本**),` + `作者不必也不应为此加装 tl.seek 垫片。本项只是**哨兵**:引擎若失守或换实现,补间属性照常插值、回调不跑 → ` + `画面会静默定格在初始态(不是黑屏,本地播放器与客户端预览都看不出)`);
10165
+ const timers = [
10166
+ /\brequestAnimationFrame\s*\(/.test(html) ? "requestAnimationFrame(" : null,
10167
+ /\bsetInterval\s*\(/.test(html) ? "setInterval(" : null
10168
+ ].filter(Boolean);
10169
+ if (timers.length)
10170
+ push("x-raf-interval", false, `含 ${timers.join(" 与 ")}——这类自有时钟**不被 seek 驱动**,逐帧渲染时等于冻结(契约:所有视觉变化必须挂在 tl 上)。` + `静态正则分不清「驱动画面」与其它用途(如一次性布局测量),故只提醒、不拦`);
10171
+ if (opts.dispatchIds && cid && !opts.dispatchIds.includes(cid))
10172
+ push("x-dispatch", false, `composition_id "${cid}" 不在 dispatch.mg 派单中`);
8452
10173
  if (opts.category && opts.category in CATEGORY_EXPECTED_OPAQUE2) {
8453
10174
  const expect = CATEGORY_EXPECTED_OPAQUE2[opts.category];
8454
10175
  if (expect !== opaque)
8455
10176
  push("x-category-opaque", false, `category「${opts.category}」期望${expect ? "不透明满屏" : "透明叠加"},但颗粒 HTML 反推为${opaque ? "不透明满屏" : "透明叠加"}(以 HTML 为准落 clip.opaque=${opaque})`);
8456
10177
  }
8457
10178
  if (typeof opts.slotDuration === "number" && opts.slotDuration > 0) {
8458
- const est = estimateTimelineSec(html);
8459
- if (est !== null && est < opts.slotDuration * 0.8) {
8460
- push("7-fill-slot", false, `时间线静态估长 ~${est}s,短于槽位包络 ${opts.slotDuration}s(铁律⑦:颗粒应占满坑位并终态驻留)——静态估算仅供参考,含 repeat/yoyo/相对定位时不作数`);
8461
- }
10179
+ const slot = opts.slotDuration;
10180
+ const { est, parsed, skipped, hasInfiniteRepeat } = estimateTimelineSec(html);
10181
+ if (hasInfiniteRepeat)
10182
+ push("7-infinite-repeat", false, `含 repeat:-1 无限循环 → tl 时长为 Infinity,铁律⑦(总长 ≥ 坑位)无法静态验证;请改用按坑位算死的有限 repeat(次数 = ceil(剩余时长 / 单圈时长)),坑位包络 ${slot}s`);
10183
+ if (parsed === 0)
10184
+ push("7-no-estimate", false, `无法静态估长(无一条可解析的时长调用${skipped ? `,${skipped} 条因表达式 position / 非字面量 duration 跳过` : ""}),铁律⑦未校验——须真渲染引擎 seek 验收颗粒是否占满坑位 ${slot}s 并终态驻留`);
10185
+ else if (Number.isFinite(est) && est < slot)
10186
+ push("7-fill-slot", false, `时间线静态估长 ~${est}s,短于槽位包络 ${slot}s(铁律⑦:颗粒应占满坑位并终态驻留)——静态估算是**下界**(${skipped} 条调用因无法静态解析被跳过),仅供参考,最终以真渲染引擎逐帧为准`);
10187
+ }
10188
+ for (const batch of detectPrimitiveLoops(html)) {
10189
+ if (batch.perElementDriven || batch.count !== null && batch.count < PRIMITIVE_MERGE_MIN)
10190
+ continue;
10191
+ const amount = batch.count === null ? "条数未知(循环边界非数字字面量)" : `静态估算 ${batch.count} 个`;
10192
+ push("8-primitive-merge", false, `${batch.site} 向同一父节点「${batch.parent}」循环生成 ${amount} <${batch.tag}>。` + "这里有一批可无损合并的重复图元:请按相同 stroke/fill 分档合并成单个 <path> 的多子路径;" + "合并后画面逐像素不变,属零成本改法。本项只提示写法形态,最终画面仍以真渲染出片为准");
8462
10193
  }
8463
10194
  return { ok: !v.some((x) => x.fatal), violations: v, opaque, compositionId: cid };
8464
10195
  }
@@ -8467,11 +10198,66 @@ function lintParticle(html, opts = {}) {
8467
10198
  var MG_MATERIAL_PREFIX = "mg-";
8468
10199
  var LEGACY_MATERIAL_PREFIX = "rrv-";
8469
10200
  var isOwnMaterialId = (id) => id.startsWith(MG_MATERIAL_PREFIX) || id.startsWith(LEGACY_MATERIAL_PREFIX);
8470
- var r34 = (n) => Math.round(n * 1000) / 1000;
8471
- var slotEnvelope = (it) => r34(it.track_ed - it.track_st);
10201
+ var OWN_ASSET_DIRS = ["assets/mg/", "assets/rrv/"];
10202
+ var normalizeRel2 = (p) => p.replace(/\\/g, "/").replace(/^\.\//, "");
10203
+ function ownAssetCompositionId(p) {
10204
+ if (typeof p !== "string")
10205
+ return;
10206
+ const rel = normalizeRel2(p);
10207
+ for (const d of OWN_ASSET_DIRS) {
10208
+ if (!rel.startsWith(d))
10209
+ continue;
10210
+ const name = rel.slice(d.length);
10211
+ if (name.includes("/"))
10212
+ return;
10213
+ return name.replace(/\.html?$/i, "");
10214
+ }
10215
+ return;
10216
+ }
10217
+ var r36 = (n) => Math.round(n * 1000) / 1000;
10218
+ var slotEnvelope = (it) => r36(it.track_ed - it.track_st);
8472
10219
  function layTracksOf(prev) {
8473
10220
  return Array.isArray(prev?.lay_tracks) ? prev.lay_tracks.filter((x) => typeof x === "number") : [];
8474
10221
  }
10222
+ function beatsOf(prev) {
10223
+ return Array.isArray(prev?.beats) ? prev.beats.filter((b) => !!b && typeof b.composition_id === "string") : [];
10224
+ }
10225
+ function clipCompositionId(c3) {
10226
+ const hm = c3.html_material;
10227
+ if (typeof hm === "string") {
10228
+ if (hm.startsWith(MG_MATERIAL_PREFIX))
10229
+ return hm.slice(MG_MATERIAL_PREFIX.length);
10230
+ if (hm.startsWith(LEGACY_MATERIAL_PREFIX))
10231
+ return hm.slice(LEGACY_MATERIAL_PREFIX.length);
10232
+ }
10233
+ if (typeof c3.material === "string")
10234
+ return c3.material;
10235
+ return typeof c3.clip_id === "string" ? c3.clip_id : undefined;
10236
+ }
10237
+ var stOf = (c3) => typeof c3.track_st === "number" ? c3.track_st : 0;
10238
+ function collectClipRefs(c3, into) {
10239
+ for (const k of ["material", "html_material"]) {
10240
+ const v = c3[k];
10241
+ if (typeof v === "string")
10242
+ into.add(v);
10243
+ }
10244
+ }
10245
+ function collectSurvivingRefs(doc, extraClips) {
10246
+ const refs = new Set;
10247
+ for (const bucket of Object.values(doc)) {
10248
+ if (!Array.isArray(bucket))
10249
+ continue;
10250
+ for (const t of bucket) {
10251
+ if (!t || !Array.isArray(t.track_timeline))
10252
+ continue;
10253
+ for (const c3 of t.track_timeline)
10254
+ collectClipRefs(c3, refs);
10255
+ }
10256
+ }
10257
+ for (const c3 of extraClips)
10258
+ collectClipRefs(c3, refs);
10259
+ return refs;
10260
+ }
8475
10261
  function layMgTracks(opts) {
8476
10262
  const { gtrk, items, generatedAt } = opts;
8477
10263
  const beatTracks = [...gtrk.beat_track ?? []];
@@ -8482,15 +10268,37 @@ function layMgTracks(opts) {
8482
10268
  const prevIndices = new Set([...layTracksOf(prevMg), ...layTracksOf(prevRrv)]);
8483
10269
  const removedTracks = beatTracks.filter((t) => typeof t.track_index === "number" && prevIndices.has(t.track_index));
8484
10270
  const keptTracks = beatTracks.filter((t) => !(typeof t.track_index === "number" && prevIndices.has(t.track_index)));
8485
- const removedMaterialIds = new Set;
10271
+ const ownCompositionIds = new Set([
10272
+ ...beatsOf(prevMg).map((b) => b.composition_id),
10273
+ ...beatsOf(prevRrv).map((b) => b.composition_id),
10274
+ ...items.map((it) => it.composition_id)
10275
+ ]);
8486
10276
  for (const t of removedTracks) {
8487
10277
  for (const c3 of t.track_timeline ?? []) {
8488
- const hm = c3.html_material;
8489
- if (typeof hm === "string" && isOwnMaterialId(hm))
8490
- removedMaterialIds.add(hm);
10278
+ const cid = clipCompositionId(c3);
10279
+ if (cid !== undefined)
10280
+ ownCompositionIds.add(cid);
10281
+ }
10282
+ }
10283
+ const isOwnMaterial = (m) => {
10284
+ if (typeof m.id === "string" && isOwnMaterialId(m.id))
10285
+ return true;
10286
+ const cid = ownAssetCompositionId(m.path);
10287
+ return cid !== undefined && ownCompositionIds.has(cid);
10288
+ };
10289
+ const itemIds = new Set(items.map((it) => it.composition_id));
10290
+ const keepIds = new Set((opts.keep ?? []).filter((id) => !itemIds.has(id)));
10291
+ const carriedClips = [];
10292
+ const carriedIds = new Set;
10293
+ for (const t of removedTracks) {
10294
+ for (const c3 of t.track_timeline ?? []) {
10295
+ const cid = clipCompositionId(c3);
10296
+ if (cid === undefined || !keepIds.has(cid) || carriedIds.has(cid))
10297
+ continue;
10298
+ carriedIds.add(cid);
10299
+ carriedClips.push({ ...c3 });
8491
10300
  }
8492
10301
  }
8493
- const keptMaterials = materials.filter((m) => !(typeof m.id === "string" && removedMaterialIds.has(m.id)));
8494
10302
  const newMaterials = [];
8495
10303
  const clips = [];
8496
10304
  const metaBeats = [];
@@ -8518,17 +10326,32 @@ function layMgTracks(opts) {
8518
10326
  });
8519
10327
  metaBeats.push({ ...toMetaBeat(it), laid: { track_index: newIndex } });
8520
10328
  }
8521
- const createdTracks = clips.length > 0 ? [
8522
- {
8523
- track_index: newIndex,
8524
- track_timeline: clips.sort((a, b) => a.track_st - b.track_st)
8525
- }
8526
- ] : [];
10329
+ const mergedClips = [...carriedClips, ...clips].sort((a, b) => stOf(a) - stOf(b));
10330
+ const createdTracks = mergedClips.length > 0 ? [{ track_index: newIndex, track_timeline: mergedClips }] : [];
10331
+ const survivingRefs = collectSurvivingRefs({ ...gtrk, beat_track: keptTracks }, carriedClips);
10332
+ const newMaterialIds = new Set(newMaterials.map((m) => String(m.id)));
10333
+ const keptMaterials = materials.filter((m) => {
10334
+ if (typeof m.id !== "string")
10335
+ return true;
10336
+ if (newMaterialIds.has(m.id))
10337
+ return false;
10338
+ if (!isOwnMaterial(m))
10339
+ return true;
10340
+ return survivingRefs.has(m.id);
10341
+ });
10342
+ const carriedBeats = [];
10343
+ const seenCarriedBeat = new Set;
10344
+ for (const b of [...beatsOf(prevMg), ...beatsOf(prevRrv)]) {
10345
+ if (!keepIds.has(b.composition_id) || seenCarriedBeat.has(b.composition_id))
10346
+ continue;
10347
+ seenCarriedBeat.add(b.composition_id);
10348
+ carriedBeats.push({ ...b, laid: carriedIds.has(b.composition_id) ? { track_index: newIndex } : null });
10349
+ }
8527
10350
  const mg = {
8528
10351
  contract_version: "v1",
8529
10352
  generated_at: generatedAt,
8530
10353
  lay_tracks: createdTracks.map((t) => t.track_index),
8531
- beats: metaBeats
10354
+ beats: [...carriedBeats, ...metaBeats]
8532
10355
  };
8533
10356
  const nextStructMeta = { ...structMeta, mg };
8534
10357
  delete nextStructMeta.rrv;
@@ -8540,7 +10363,11 @@ function layMgTracks(opts) {
8540
10363
  };
8541
10364
  return {
8542
10365
  next,
8543
- summary: { laidTrack: createdTracks[0]?.track_index ?? null, laidParticles: clips.length },
10366
+ summary: {
10367
+ laidTrack: createdTracks[0]?.track_index ?? null,
10368
+ laidParticles: clips.length,
10369
+ keptParticles: carriedClips.length
10370
+ },
8544
10371
  mg
8545
10372
  };
8546
10373
  }
@@ -8549,7 +10376,7 @@ function toMetaBeat(it) {
8549
10376
  beat: it.beat,
8550
10377
  composition_id: it.composition_id,
8551
10378
  track_st: it.track_st,
8552
- track_ed: r34(it.track_ed),
10379
+ track_ed: r36(it.track_ed),
8553
10380
  duration: slotEnvelope(it),
8554
10381
  html_path: it.html_rel,
8555
10382
  ...it.category ? { category: it.category } : {}
@@ -8560,7 +10387,7 @@ function toMetaBeat(it) {
8560
10387
  var MG_ASSET_DIR = "assets/mg";
8561
10388
  var MG_SRC_DIRS = ["mg", "rrv"];
8562
10389
  function registerMg(program2) {
8563
- program2.command("mg [words...]").alias("rrv").description("MG 颗粒铺轨:无 positional=消费 dispatch.mg 铺 html-particle;`mg lint <file>`=单文件 lint;`mg status`=看板").option("--project <dir>", "oralcut 产物目录(定位 split/dispatch.json 与工程)").option("--dispatch <path>", "显式指定 dispatch.json(非标准布局兜底)").option("--only <beat>", "只跑单 beat").option("--lint-only", "只 lint 校验,不铺轨不写回").option("--json", "机读模式:人读日志转 stderr,stdout 只输出结果 JSON").action(async (words, opts) => {
10390
+ program2.command("mg [words...]").alias("rrv").description("MG 颗粒铺轨:无 positional=消费 dispatch.mg 铺 html-particle;`mg lint <file>`=单文件 lint;`mg status`=看板").option("--project <dir>", "oralcut 产物目录(定位 split/dispatch.json 与工程)").option("--dispatch <path>", "显式指定 dispatch.json(非标准布局兜底)").option("--only <beat>", "只跑单 beat(收 beat id 如 B12,非 composition_id):增量重铺该 beat,轨上其余已铺颗粒原样保留").option("--lint-only", "只 lint 校验,不铺轨不写回").option("--replace-all", "显式授权重置整轨:不走增量保留、整轨剥掉重铺——会删掉轨上其余已铺颗粒(不在本次派单/--only 里的那些)").option("--json", "机读模式:人读日志转 stderr,stdout 只输出结果 JSON").action(async (words, opts) => {
8564
10391
  if (process.argv[2] === "rrv")
8565
10392
  log.warn("`gtrk rrv` 已更名为 `gtrk mg`(去品牌化),别名仍可用但建议改用 `gtrk mg`。");
8566
10393
  await runMg(words ?? [], opts);
@@ -8580,53 +10407,109 @@ async function runMg(words, opts) {
8580
10407
  }
8581
10408
  function resolveDispatch(opts) {
8582
10409
  if (opts.dispatch) {
8583
- const dispatchPath = resolve9(opts.dispatch);
10410
+ const dispatchPath = resolve10(opts.dispatch);
8584
10411
  return { dispatchPath, baseDir: dirname8(dirname8(dispatchPath)) };
8585
10412
  }
8586
10413
  if (opts.project) {
8587
- const baseDir = resolve9(opts.project);
8588
- return { dispatchPath: join20(baseDir, "split", "dispatch.json"), baseDir };
10414
+ const baseDir = resolve10(opts.project);
10415
+ return { dispatchPath: join21(baseDir, "split", "dispatch.json"), baseDir };
8589
10416
  }
8590
10417
  throw new Error("需 --project <目录> 或显式 --dispatch <path>");
8591
10418
  }
8592
10419
  function locateGtrk2(baseDir) {
8593
- return [join20(baseDir, "gtrk", "project.gtrk"), join20(baseDir, "project.gtrk")].find((p) => existsSync16(p));
10420
+ return [join21(baseDir, "gtrk", "project.gtrk"), join21(baseDir, "project.gtrk")].find((p) => existsSync18(p));
8594
10421
  }
8595
10422
  function locateSrcHtml(baseDir, compositionId) {
8596
10423
  for (const d of MG_SRC_DIRS) {
8597
- const p = join20(baseDir, d, `${compositionId}.html`);
8598
- if (existsSync16(p))
10424
+ const p = join21(baseDir, d, `${compositionId}.html`);
10425
+ if (existsSync18(p))
8599
10426
  return p;
8600
10427
  }
8601
10428
  return;
8602
10429
  }
8603
10430
  async function readMgQueue(dispatchPath) {
8604
- if (!existsSync16(dispatchPath))
10431
+ if (!existsSync18(dispatchPath))
8605
10432
  throw new Error(`找不到派单清单:${dispatchPath}(先跑 gtrk split 落地派单)`);
8606
- const dispatch = JSON.parse(await readFile6(dispatchPath, "utf8"));
10433
+ const dispatch = JSON.parse(await readFile7(dispatchPath, "utf8"));
8607
10434
  const queue = dispatch.mg ?? dispatch.rrv_mg;
8608
10435
  return Array.isArray(queue) ? queue : [];
8609
10436
  }
10437
+ function laidCompositionIds(gtrk) {
10438
+ if (!gtrk)
10439
+ return [];
10440
+ const structMeta = gtrk.struct_meta;
10441
+ const ids = [];
10442
+ const seen = new Set;
10443
+ for (const meta of [structMeta?.mg, structMeta?.rrv]) {
10444
+ for (const b of meta?.beats ?? []) {
10445
+ if (!b?.laid || typeof b.composition_id !== "string" || seen.has(b.composition_id))
10446
+ continue;
10447
+ seen.add(b.composition_id);
10448
+ ids.push(b.composition_id);
10449
+ }
10450
+ }
10451
+ return ids;
10452
+ }
8610
10453
  async function runLay(opts) {
8611
10454
  const { dispatchPath, baseDir } = resolveDispatch(opts);
8612
- let queue = await readMgQueue(dispatchPath);
8613
- if (opts.only)
8614
- queue = queue.filter((q) => q.beat === opts.only);
10455
+ const allQueue = await readMgQueue(dispatchPath);
10456
+ const queue = opts.only ? allQueue.filter((q) => q.beat === opts.only) : allQueue;
8615
10457
  log.step(`▶ MG 颗粒铺轨:${queue.length} 个 beat…`);
10458
+ if (opts.only && queue.length === 0) {
10459
+ const beats = [...new Set(allQueue.map((q) => q.beat))];
10460
+ log.warn(`--only ${opts.only} 未命中任何 beat——该选择器收的是「beat id」(如 B12),不是 composition_id(如 <slug>-B12)。`);
10461
+ log.warn(`dispatch 现有 beat:${beats.slice(0, 12).join("、") || "(空)"}${beats.length > 12 ? ` …共 ${beats.length} 个` : ""}`);
10462
+ }
10463
+ const gtrkPath = locateGtrk2(baseDir);
10464
+ let project;
10465
+ let gtrkUnreadable = false;
10466
+ if (gtrkPath) {
10467
+ try {
10468
+ project = readGtrk(gtrkPath);
10469
+ } catch (e) {
10470
+ if (!opts.lintOnly)
10471
+ throw e;
10472
+ gtrkUnreadable = true;
10473
+ }
10474
+ }
10475
+ if (!opts.lintOnly && project)
10476
+ assertGtrkV1(project.gtrk);
10477
+ const laidBefore = laidCompositionIds(opts.lintOnly ? undefined : project?.gtrk);
10478
+ const reproj = await reprojectDispatchWindows({
10479
+ baseDir,
10480
+ gtrk: project?.gtrk,
10481
+ gtrkUnreadable,
10482
+ entries: queue.map((q) => ({
10483
+ key: q.composition_id,
10484
+ beat: q.beat,
10485
+ compositionId: q.composition_id,
10486
+ span: q.span,
10487
+ track_st: q.track_st,
10488
+ track_ed: q.track_ed
10489
+ }))
10490
+ });
10491
+ reportReprojection(reproj);
8616
10492
  const dispatchIds = queue.map((q) => q.composition_id);
8617
10493
  const items = [];
8618
10494
  const srcByComp = new Map;
8619
10495
  const skipped = [];
10496
+ const windowOf = new Map(reproj.entries.map((o) => [o.key, o]));
8620
10497
  for (const q of queue) {
10498
+ const outcome = windowOf.get(q.composition_id);
10499
+ if (outcome?.dropped) {
10500
+ skipped.push({ beat: q.beat, reason: "重投影后零存活(该段已被剪出成片)" });
10501
+ continue;
10502
+ }
10503
+ const win = outcome ?? { track_st: q.track_st, track_ed: q.track_ed };
8621
10504
  const srcPath = locateSrcHtml(baseDir, q.composition_id);
8622
10505
  if (!srcPath) {
8623
10506
  skipped.push({ beat: q.beat, reason: "缺颗粒 HTML(未产出)" });
8624
- log.warn(`${q.beat}:缺 ${join20(baseDir, MG_SRC_DIRS[0], `${q.composition_id}.html`)},跳过`);
10507
+ log.warn(`${q.beat}:缺 ${join21(baseDir, MG_SRC_DIRS[0], `${q.composition_id}.html`)},跳过`);
8625
10508
  continue;
8626
10509
  }
8627
- const html = await readFile6(srcPath, "utf8");
10510
+ const html = await readFile7(srcPath, "utf8");
8628
10511
  const category = typeof q.category === "string" ? q.category : undefined;
8629
- const slotDuration = Math.round((q.track_ed - q.track_st) * 1000) / 1000;
10512
+ const slotDuration = Math.round((win.track_ed - win.track_st) * 1000) / 1000;
8630
10513
  const lint = lintParticle(html, {
8631
10514
  compositionId: q.composition_id,
8632
10515
  dispatchIds,
@@ -8648,55 +10531,162 @@ async function runLay(opts) {
8648
10531
  items.push({
8649
10532
  beat: q.beat,
8650
10533
  composition_id: q.composition_id,
8651
- track_st: q.track_st,
8652
- track_ed: q.track_ed,
10534
+ track_st: win.track_st,
10535
+ track_ed: win.track_ed,
8653
10536
  opaque: lint.opaque,
8654
10537
  html_rel: `${MG_ASSET_DIR}/${q.composition_id}.html`,
8655
10538
  ...category ? { category } : {}
8656
10539
  });
8657
10540
  }
8658
10541
  if (opts.lintOnly) {
8659
- log.ok(`lint-only:${items.length}/${queue.length} 通过,${skipped.length} 跳过(不铺轨)`);
8660
- return done(opts, { ok: skipped.length === 0, mode: "lay", lintOnly: true, passed: items.length, skipped });
10542
+ const lintOk = skipped.length === 0;
10543
+ (lintOk ? log.ok : log.warn)(`lint-only:${items.length}/${queue.length} 通过,${skipped.length} 跳过(不铺轨)`);
10544
+ return done(opts, {
10545
+ ok: lintOk,
10546
+ mode: "lay",
10547
+ lintOnly: true,
10548
+ ...lintOk ? {} : { reason: "skipped" },
10549
+ passed: items.length,
10550
+ skipped,
10551
+ reprojection: reproj.summary
10552
+ });
8661
10553
  }
8662
- const gtrkPath = locateGtrk2(baseDir);
8663
- if (!gtrkPath) {
8664
- log.warn(`未找到工程文件(${join20(baseDir, "gtrk", "project.gtrk")}),跳过铺轨——lint 已完成`);
8665
- return done(opts, { ok: true, mode: "lay", laid: 0, skipped, note: "工程缺失,未铺轨" });
10554
+ if (!gtrkPath || !project) {
10555
+ log.warn(`未找到工程文件(${join21(baseDir, "gtrk", "project.gtrk")}),跳过铺轨——lint 已完成`);
10556
+ return done(opts, {
10557
+ ok: skipped.length === 0,
10558
+ mode: "lay",
10559
+ reason: "no_project",
10560
+ laid: 0,
10561
+ laidTrack: null,
10562
+ track_total: 0,
10563
+ removed: 0,
10564
+ kept: 0,
10565
+ kept_ids: [],
10566
+ skipped,
10567
+ note: "工程缺失,未铺轨",
10568
+ reprojection: reproj.summary
10569
+ });
8666
10570
  }
8667
- const { gtrk, mtimeMs } = readGtrk(gtrkPath);
8668
- assertGtrkV1(gtrk);
10571
+ const { gtrk, mtimeMs } = project;
8669
10572
  const gtrkDir = dirname8(gtrkPath);
8670
- await mkdir7(join20(gtrkDir, ...MG_ASSET_DIR.split("/")), { recursive: true });
10573
+ const covered = new Set(items.map((it) => it.composition_id));
10574
+ const orphans = laidBefore.filter((id) => !covered.has(id));
10575
+ const inDispatch = new Set(allQueue.map((q) => q.composition_id));
10576
+ const keep = opts.replaceAll ? [] : opts.only ? orphans : orphans.filter((id) => inDispatch.has(id));
10577
+ const keepSet = new Set(keep);
10578
+ const wouldRemove = laidBefore.filter((id) => !covered.has(id) && !keepSet.has(id));
10579
+ const refuse = laidBefore.length > 0 && items.length === 0 ? "empty_queue" : undefined;
10580
+ if (refuse && !opts.replaceAll) {
10581
+ log.err(`拒绝写回:本次一条颗粒都没定位到,而轨上已铺 ${laidBefore.length} 颗——「一条都没定位到」是派单/选择器出问题的信号,不是清空指令。`);
10582
+ if (opts.only && queue.length === 0) {
10583
+ log.warn(`成因:--only ${opts.only} 未命中任何 beat(选择器提示见上)。`);
10584
+ } else if (allQueue.length === 0) {
10585
+ log.warn("dispatch.mg(读旧 rrv_mg)为空或缺失——先跑 gtrk split 落地派单,再铺轨。");
10586
+ } else {
10587
+ log.warn(`本次 ${queue.length} 个条目全部被跳过(${[...new Set(skipped.map((s) => s.reason))].join("、")})——先按上面的 lint 报因补产 / 修颗粒,再重铺。`);
10588
+ }
10589
+ if (wouldRemove.length) {
10590
+ const preview = wouldRemove.slice(0, 8);
10591
+ log.warn(`将被铲掉:${preview.join("、")}${wouldRemove.length > preview.length ? ` …等共 ${wouldRemove.length} 颗` : ""}`);
10592
+ }
10593
+ log.warn("出路二选一:① 按上面的报因修好派单 / 选择器 / 颗粒后重铺;② 确知要清空整轨 → 加 --replace-all 显式授权(会删掉轨上全部已铺颗粒)");
10594
+ log.warn(`工程未被改动(.gtrk 逐字节不变,轨上仍 ${laidBefore.length} 颗)。`);
10595
+ return done(opts, {
10596
+ ok: false,
10597
+ mode: "lay",
10598
+ reason: refuse,
10599
+ refused: true,
10600
+ laid: 0,
10601
+ laidTrack: null,
10602
+ track_total: laidBefore.length,
10603
+ removed: 0,
10604
+ kept: 0,
10605
+ kept_ids: [],
10606
+ blocked: wouldRemove,
10607
+ skipped,
10608
+ reprojection: reproj.summary
10609
+ });
10610
+ }
10611
+ if (opts.replaceAll && orphans.length > 0) {
10612
+ log.warn(`--replace-all:已显式授权重置整轨,轨上其余 ${orphans.length} 颗已铺颗粒将被剥离(不走增量保留)。`);
10613
+ }
10614
+ await mkdir7(join21(gtrkDir, ...MG_ASSET_DIR.split("/")), { recursive: true });
8671
10615
  for (const it of items) {
8672
- await copyFile(srcByComp.get(it.composition_id), join20(gtrkDir, ...it.html_rel.split("/")));
10616
+ await copyFile(srcByComp.get(it.composition_id), join21(gtrkDir, ...it.html_rel.split("/")));
10617
+ }
10618
+ const { next, summary, mg } = layMgTracks({ gtrk, items, generatedAt: new Date().toISOString(), keep });
10619
+ const written = withTimecodeSource(next, "mg", reproj);
10620
+ writeGtrkAtomic(gtrkPath, written, mtimeMs);
10621
+ const integrity = safeCheckMaterialIntegrity({ gtrk: written, gtrkDir, log });
10622
+ const trackTotal = mg.beats.filter((b) => b.laid).length;
10623
+ const keptIds = mg.beats.filter((b) => b.laid && keepSet.has(b.composition_id)).map((b) => b.composition_id);
10624
+ const removed = laidBefore.length - keptIds.length;
10625
+ const ok = skipped.length === 0;
10626
+ const tail = `本次 ${summary.laidParticles} 颗 / 轨上共 ${trackTotal} 颗${keptIds.length ? `(保留 ${keptIds.length} 颗)` : ""}` + ` → beat_track ${summary.laidTrack ?? "-"}` + `${removed ? `(剥旧 ${removed} 颗)` : ""}${skipped.length ? `(${skipped.length} beat 跳过)` : ""}`;
10627
+ if (summary.laidTrack === null)
10628
+ log.warn(`未铺成任何颗粒:${tail}`);
10629
+ else if (ok)
10630
+ log.ok(`铺轨完成:${tail}`);
10631
+ else
10632
+ log.warn(`铺轨完成(有跳过):${tail}`);
10633
+ if (keptIds.length) {
10634
+ const preview = keptIds.slice(0, 8);
10635
+ log.warn(`其中 ${keptIds.length} 颗是上轮遗留、本次未重铺:${preview.join("、")}${keptIds.length > preview.length ? ` …等共 ${keptIds.length} 颗` : ""}` + `——轨上内容 = 本次 ${summary.laidParticles} 颗 + 上轮 ${keptIds.length} 颗,与本次派单不完全对应(要全部刷新就去掉 --only 全量重铺)。`);
8673
10636
  }
8674
- const { next, summary } = layMgTracks({ gtrk, items, generatedAt: new Date().toISOString() });
8675
- writeGtrkAtomic(gtrkPath, next, mtimeMs);
8676
- log.ok(`铺轨完成:${summary.laidParticles} 颗粒 → beat_track ${summary.laidTrack ?? "-"}` + `${skipped.length ? `(${skipped.length} beat 跳过)` : ""}`);
8677
10637
  log.info("opencut 打开工程即见 MG overlay 轨(预览需 add-particle-project-folder-preview 上线);出片时客户端云渲。");
10638
+ if (integrity)
10639
+ reportMaterialIntegrity(integrity, log);
8678
10640
  return done(opts, {
8679
- ok: true,
10641
+ ok,
8680
10642
  mode: "lay",
10643
+ ...ok ? {} : { reason: "skipped" },
8681
10644
  laid: summary.laidParticles,
8682
10645
  laidTrack: summary.laidTrack,
8683
- skipped
10646
+ track_total: trackTotal,
10647
+ removed,
10648
+ kept: keptIds.length,
10649
+ kept_ids: keptIds,
10650
+ skipped,
10651
+ ...integrity ? { integrity } : {},
10652
+ reprojection: reproj.summary
8684
10653
  });
8685
10654
  }
10655
+ var CID_SHAPE = /-B\d+(?:-aux\d+)?$/;
8686
10656
  async function runLint(args, opts) {
8687
10657
  const file = args[0];
8688
10658
  if (!file)
8689
10659
  throw new Error("用法:gtrk mg lint <particle.html> [--dispatch <path>]");
8690
- const html = await readFile6(resolve9(file), "utf8");
10660
+ const html = await readFile7(resolve10(file), "utf8");
10661
+ const nameId = basename11(file).replace(/\.html?$/i, "");
8691
10662
  let dispatchIds;
8692
- if (opts.dispatch && existsSync16(resolve9(opts.dispatch))) {
8693
- dispatchIds = (await readMgQueue(resolve9(opts.dispatch))).map((q) => q.composition_id);
8694
- }
8695
- const lint = lintParticle(html, { dispatchIds });
10663
+ let slotDuration;
10664
+ let compositionId;
10665
+ if (opts.dispatch && existsSync18(resolve10(opts.dispatch))) {
10666
+ const queue = await readMgQueue(resolve10(opts.dispatch));
10667
+ dispatchIds = queue.map((q) => q.composition_id);
10668
+ const byName = queue.find((q) => q.composition_id === nameId);
10669
+ const innerCid = parseCompositionId(html);
10670
+ const hit = byName ?? (innerCid ? queue.find((q) => q.composition_id === innerCid) : undefined);
10671
+ if (hit) {
10672
+ const d = Math.round((hit.track_ed - hit.track_st) * 1000) / 1000;
10673
+ if (d > 0)
10674
+ slotDuration = d;
10675
+ }
10676
+ if (byName)
10677
+ compositionId = byName.composition_id;
10678
+ }
10679
+ if (compositionId === undefined && CID_SHAPE.test(nameId))
10680
+ compositionId = nameId;
10681
+ const lint = lintParticle(html, {
10682
+ ...dispatchIds ? { dispatchIds } : {},
10683
+ ...compositionId ? { compositionId } : {},
10684
+ ...slotDuration ? { slotDuration } : {}
10685
+ });
8696
10686
  for (const vv of lint.violations)
8697
10687
  (vv.fatal ? log.err : log.warn)(`${vv.fatal ? "✗" : "·"} ${vv.law}: ${vv.msg}`);
8698
10688
  if (lint.ok)
8699
- log.ok(`lint 通过(${basename10(file)};opaque=${lint.opaque})`);
10689
+ log.ok(`lint 通过(${basename11(file)};opaque=${lint.opaque})`);
8700
10690
  else
8701
10691
  log.err(`lint 未过(${lint.violations.filter((v) => v.fatal).length} 项致命)`);
8702
10692
  const result = { mode: "lint", ...lint, ok: lint.ok };
@@ -8730,6 +10720,8 @@ async function runStatus(opts) {
8730
10720
  return done(opts, { ok: true, mode: "status", total: queue.length, authored, laid, rows });
8731
10721
  }
8732
10722
  function done(opts, result) {
10723
+ if (!result.ok)
10724
+ process.exitCode = 1;
8733
10725
  if (opts.json)
8734
10726
  console.log(JSON.stringify(result));
8735
10727
  return result;
@@ -9718,9 +11710,9 @@ function validateRegistry(registry = TOOL_REGISTRY) {
9718
11710
  }
9719
11711
 
9720
11712
  // src/lib/tool-runner.ts
9721
- import { resolve as resolve10, join as join21, dirname as dirname9, basename as basename11, extname as extname5 } from "node:path";
11713
+ import { resolve as resolve11, join as join22, dirname as dirname9, basename as basename12, extname as extname5 } from "node:path";
9722
11714
  import { mkdir as mkdir8, writeFile as writeFile8, stat as stat5 } from "node:fs/promises";
9723
- import { createWriteStream, existsSync as existsSync17 } from "node:fs";
11715
+ import { createWriteStream, existsSync as existsSync19 } from "node:fs";
9724
11716
  import { Readable as Readable2 } from "node:stream";
9725
11717
  import { pipeline } from "node:stream/promises";
9726
11718
 
@@ -9850,7 +11842,7 @@ function validateToolInput(descriptor, inputAbs) {
9850
11842
  return;
9851
11843
  if (!inputAbs)
9852
11844
  throw new Error(`${descriptor.name} 需要输入${spec.kind === "directory" ? "目录" : "文件"}`);
9853
- if (!existsSync17(inputAbs))
11845
+ if (!existsSync19(inputAbs))
9854
11846
  throw new Error(`输入不存在:${inputAbs}`);
9855
11847
  if (spec.kind === "directory")
9856
11848
  return;
@@ -9917,12 +11909,12 @@ function timestamp3() {
9917
11909
  }
9918
11910
  function resolveOutDir(descriptor, inputAbs, out) {
9919
11911
  if (out)
9920
- return resolve10(out);
11912
+ return resolve11(out);
9921
11913
  if (inputAbs) {
9922
- const base = basename11(inputAbs, extname5(inputAbs));
9923
- return join21(dirname9(inputAbs), `${base}-${descriptor.name}`);
11914
+ const base = basename12(inputAbs, extname5(inputAbs));
11915
+ return join22(dirname9(inputAbs), `${base}-${descriptor.name}`);
9924
11916
  }
9925
- return join21(process.cwd(), `${descriptor.name}-${timestamp3()}`);
11917
+ return join22(process.cwd(), `${descriptor.name}-${timestamp3()}`);
9926
11918
  }
9927
11919
  async function safeFingerprint(inputAbs) {
9928
11920
  try {
@@ -9937,8 +11929,8 @@ function emitBilling(hint) {
9937
11929
  `);
9938
11930
  }
9939
11931
  async function runCloudTool(descriptor, inputArg, opts, deps) {
9940
- const inputAbs = inputArg ? resolve10(inputArg) : undefined;
9941
- const baseName = inputAbs ? basename11(inputAbs, extname5(inputAbs)) : descriptor.name;
11932
+ const inputAbs = inputArg ? resolve11(inputArg) : undefined;
11933
+ const baseName = inputAbs ? basename12(inputAbs, extname5(inputAbs)) : descriptor.name;
9942
11934
  validateToolInput(descriptor, inputAbs);
9943
11935
  const probe = deps.probeDurationSec ?? probeDuration;
9944
11936
  guardDuration(descriptor, inputAbs, probe, opts.ffmpegPath);
@@ -9975,13 +11967,13 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
9975
11967
  uploadCached: deps.uploadCached,
9976
11968
  invalidateUpload: deps.invalidateUpload,
9977
11969
  submitTask: deps.submitTask,
9978
- sleep: deps.sleep ?? ((ms) => new Promise((resolve11) => setTimeout(resolve11, ms)))
11970
+ sleep: deps.sleep ?? ((ms) => new Promise((resolve12) => setTimeout(resolve12, ms)))
9979
11971
  });
9980
11972
  const { taskId } = submitted;
9981
11973
  const up = { fileId: submitted.fileId, cached: submitted.cached };
9982
11974
  await mkdir8(outDir, { recursive: true });
9983
11975
  const fingerprint2 = inputAbs ? await safeFingerprint(inputAbs) : undefined;
9984
- await writeFile8(join21(outDir, "task.json"), JSON.stringify({ tool: descriptor.name, taskType, taskId, fileId: up.fileId, source: inputAbs, fingerprint: fingerprint2, createdAt: new Date().toISOString() }, null, 2));
11976
+ await writeFile8(join22(outDir, "task.json"), JSON.stringify({ tool: descriptor.name, taskType, taskId, fileId: up.fileId, source: inputAbs, fingerprint: fingerprint2, createdAt: new Date().toISOString() }, null, 2));
9985
11977
  const output = await pollToolTask(deps.cfg, taskType, taskId, {
9986
11978
  timeoutMs: descriptor.pollTimeoutMs,
9987
11979
  intervalMs: deps.pollIntervalMs,
@@ -9994,7 +11986,7 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
9994
11986
  const errors = {};
9995
11987
  const items = descriptor.mapOutputs ? descriptor.mapOutputs(outputResult, ctx) : [];
9996
11988
  for (const it of items) {
9997
- const dest = join21(outDir, it.filename);
11989
+ const dest = join22(outDir, it.filename);
9998
11990
  try {
9999
11991
  await deps.downloadStream(it.url, dest);
10000
11992
  files.push(dest);
@@ -10005,7 +11997,7 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
10005
11997
  let resultFile;
10006
11998
  const structured = descriptor.mapResult ? descriptor.mapResult(outputResult, ctx) : undefined;
10007
11999
  if (structured != null) {
10008
- resultFile = join21(outDir, "result-output.json");
12000
+ resultFile = join22(outDir, "result-output.json");
10009
12001
  await writeFile8(resultFile, JSON.stringify(structured, null, 2));
10010
12002
  }
10011
12003
  if (items.length === 0 && resultFile == null) {
@@ -10023,14 +12015,14 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
10023
12015
  ...resultFile ? { resultFile } : {},
10024
12016
  ...Object.keys(errors).length ? { errors } : {}
10025
12017
  };
10026
- await writeFile8(join21(outDir, "result.json"), JSON.stringify({ ...result, finishedAt: new Date().toISOString() }, null, 2));
12018
+ await writeFile8(join22(outDir, "result.json"), JSON.stringify({ ...result, finishedAt: new Date().toISOString() }, null, 2));
10027
12019
  return result;
10028
12020
  }
10029
12021
 
10030
12022
  // src/lib/mad/mad.ts
10031
12023
  import { mkdir as mkdir11, writeFile as writeFile10 } from "node:fs/promises";
10032
- import { existsSync as existsSync20, statSync as statSync3 } from "node:fs";
10033
- import { resolve as resolve11, join as join25 } from "node:path";
12024
+ import { existsSync as existsSync22, statSync as statSync3 } from "node:fs";
12025
+ import { resolve as resolve12, join as join26 } from "node:path";
10034
12026
 
10035
12027
  // src/lib/convert/types.ts
10036
12028
  function num(v, d = 0) {
@@ -10344,14 +12336,14 @@ var FX_MATCHNAME = {
10344
12336
  function esc(s) {
10345
12337
  return String(s).replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\r/g, "\\r").replace(/\n/g, "\\n").replace(/\t/g, "\\t").replace(new RegExp(String.fromCharCode(8232), "g"), "\\u2028").replace(new RegExp(String.fromCharCode(8233), "g"), "\\u2029");
10346
12338
  }
10347
- function r35(v) {
12339
+ function r37(v) {
10348
12340
  return Math.round(v * 1000) / 1000;
10349
12341
  }
10350
12342
  function colorArr(css, fallback) {
10351
12343
  const c3 = parseCssColor(css);
10352
12344
  if (!c3)
10353
12345
  return fallback;
10354
- return [r35(c3[0] / 255), r35(c3[1] / 255), r35(c3[2] / 255)];
12346
+ return [r37(c3[0] / 255), r37(c3[1] / 255), r37(c3[2] / 255)];
10355
12347
  }
10356
12348
  function easeInfluence(e) {
10357
12349
  const cb = parseCubicBezier(e);
@@ -10367,7 +12359,7 @@ function emitKeys(ctx, propExpr, keys) {
10367
12359
  L.push(` try {`);
10368
12360
  L.push(` var p = ${propExpr};`);
10369
12361
  for (const k of keys) {
10370
- L.push(` p.setValueAtTime(${r35(k.t)}, ${k.value});`);
12362
+ L.push(` p.setValueAtTime(${r37(k.t)}, ${k.value});`);
10371
12363
  }
10372
12364
  keys.forEach((k, i) => {
10373
12365
  const inf = easeInfluence(k.e);
@@ -10376,7 +12368,7 @@ function emitKeys(ctx, propExpr, keys) {
10376
12368
  L.push(` try {`);
10377
12369
  L.push(` var dim = 1; try { dim = p.value.length || 1; } catch (e) { dim = 1; }`);
10378
12370
  L.push(` var eo = [], ei = [];`);
10379
- L.push(` for (var d = 0; d < Math.min(dim, 3); d++) { eo.push(new KeyframeEase(0, ${r35(inf.out)})); ei.push(new KeyframeEase(0, ${r35(inf.inn)})); }`);
12371
+ L.push(` for (var d = 0; d < Math.min(dim, 3); d++) { eo.push(new KeyframeEase(0, ${r37(inf.out)})); ei.push(new KeyframeEase(0, ${r37(inf.inn)})); }`);
10380
12372
  L.push(` p.setTemporalEaseAtKey(${i + 1}, ei, eo);`);
10381
12373
  L.push(` } catch (e) {}`);
10382
12374
  });
@@ -10457,10 +12449,10 @@ function positionBaseKeys(anim, pos) {
10457
12449
  if (!(anim.x?.length || anim.y?.length))
10458
12450
  return [];
10459
12451
  const merged = mergeChannelTracks(anim.x, anim.y, pos[0], pos[1]);
10460
- return merged.map((k) => ({ t: k.t, value: `[${r35(k.a)}, ${r35(k.b)}]`, e: k.e }));
12452
+ return merged.map((k) => ({ t: k.t, value: `[${r37(k.a)}, ${r37(k.b)}]`, e: k.e }));
10461
12453
  }
10462
12454
  function scaleBaseKeys(anim, coverExpr) {
10463
- const sc = (v) => coverExpr ? `${r35(v)}*${coverExpr}` : `${r35(v)}`;
12455
+ const sc = (v) => coverExpr ? `${r37(v)}*${coverExpr}` : `${r37(v)}`;
10464
12456
  if (anim.scale?.length) {
10465
12457
  return anim.scale.map((k) => ({ t: k.t, value: `[${sc(num(k.v, 1) * 100)}, ${sc(num(k.v, 1) * 100)}]`, e: k.e }));
10466
12458
  }
@@ -10480,13 +12472,13 @@ function emitBakedOps(ctx, ly, layerVar, tf, coverExpr) {
10480
12472
  const pos = [num(ly.pos?.[0], 0), num(ly.pos?.[1], 0)];
10481
12473
  const inn = num(ly.in, 0);
10482
12474
  const out = Math.max(inn + 0.01, num(ly.out, inn + 1));
10483
- const sc = (v) => coverExpr ? `${r35(v)}*${coverExpr}` : `${r35(v)}`;
12475
+ const sc = (v) => coverExpr ? `${r37(v)}*${coverExpr}` : `${r37(v)}`;
10484
12476
  const brights = tracks.filter((t) => t.prop === "brightness");
10485
12477
  if (brights.length) {
10486
12478
  ctx.lines.push(` // fx: flicker → 亮度脉冲(ADBE Brightness & Contrast 2,避开 opacity 通道)`);
10487
12479
  ctx.lines.push(` var flkFx = null; try { flkFx = ${layerVar}.property("ADBE Effect Parade").addProperty("ADBE Brightness & Contrast 2"); } catch (e) { flkFx = null; }`);
10488
12480
  for (const bt of brights) {
10489
- const keys = bt.times.map((t, i) => ({ t, value: `${r35(bt.values[i][0])}`, e: null }));
12481
+ const keys = bt.times.map((t, i) => ({ t, value: `${r37(bt.values[i][0])}`, e: null }));
10490
12482
  emitKeys(ctx, `flkFx.property(1)`, keys);
10491
12483
  }
10492
12484
  }
@@ -10498,12 +12490,12 @@ function emitBakedOps(ctx, ly, layerVar, tf, coverExpr) {
10498
12490
  for (const bt of posBaked) {
10499
12491
  windows.push(bt.window);
10500
12492
  for (let i = 0;i < bt.times.length; i++) {
10501
- bakedKeys.push({ t: bt.times[i], value: `[${r35(bt.values[i][0])}, ${r35(bt.values[i][1])}]`, e: null });
12493
+ bakedKeys.push({ t: bt.times[i], value: `[${r37(bt.values[i][0])}, ${r37(bt.values[i][1])}]`, e: null });
10502
12494
  }
10503
12495
  }
10504
12496
  const merged = mergeBaked(baseKeys, bakedKeys, windows, (t) => {
10505
12497
  const [x, y] = samplePos(anim, pos, t);
10506
- return `[${r35(x)}, ${r35(y)}]`;
12498
+ return `[${r37(x)}, ${r37(y)}]`;
10507
12499
  }, inn, out);
10508
12500
  emitKeys(ctx, `${tf}.property("ADBE Position")`, merged);
10509
12501
  }
@@ -10526,15 +12518,15 @@ function emitBakedOps(ctx, ly, layerVar, tf, coverExpr) {
10526
12518
  }
10527
12519
  const rotBaked = tracks.filter((t) => t.prop === "rotation");
10528
12520
  if (rotBaked.length) {
10529
- const baseKeys = (anim.rot ?? []).map((k) => ({ t: k.t, value: `${r35(num(k.v, 0))}`, e: k.e }));
12521
+ const baseKeys = (anim.rot ?? []).map((k) => ({ t: k.t, value: `${r37(num(k.v, 0))}`, e: k.e }));
10530
12522
  const bakedKeys = [];
10531
12523
  const windows = [];
10532
12524
  for (const bt of rotBaked) {
10533
12525
  windows.push(bt.window);
10534
12526
  for (let i = 0;i < bt.times.length; i++)
10535
- bakedKeys.push({ t: bt.times[i], value: `${r35(bt.values[i][0])}`, e: null });
12527
+ bakedKeys.push({ t: bt.times[i], value: `${r37(bt.values[i][0])}`, e: null });
10536
12528
  }
10537
- const merged = mergeBaked(baseKeys, bakedKeys, windows, (t) => `${r35(sampleTrack(anim.rot, t, 0))}`, inn, out);
12529
+ const merged = mergeBaked(baseKeys, bakedKeys, windows, (t) => `${r37(sampleTrack(anim.rot, t, 0))}`, inn, out);
10538
12530
  emitKeys(ctx, `${tf}.property("ADBE Rotate Z")`, merged);
10539
12531
  }
10540
12532
  }
@@ -10550,11 +12542,11 @@ function emitLayer(ctx, ir, ly, parentNullVar) {
10550
12542
  const py = num(ly.pos?.[1], c3.h / 2);
10551
12543
  const name = esc(`${ly.id || "L" + id} [${ly.type}]`);
10552
12544
  L.push(``);
10553
- L.push(` // ---- 层 ${name} in=${r35(inn)} out=${r35(out)} ----`);
12545
+ L.push(` // ---- 层 ${name} in=${r37(inn)} out=${r37(out)} ----`);
10554
12546
  if (ly.type === "group") {
10555
- L.push(` var ${v} = ${cv}.layers.addNull(${r35(c3.duration)});`);
12547
+ L.push(` var ${v} = ${cv}.layers.addNull(${r37(c3.duration)});`);
10556
12548
  L.push(` ${v}.name = "${name}";`);
10557
- L.push(` ${v}.inPoint = ${r35(inn)}; ${v}.outPoint = ${r35(out)};`);
12549
+ L.push(` ${v}.inPoint = ${r37(inn)}; ${v}.outPoint = ${r37(out)};`);
10558
12550
  L.push(` ${v}.property("ADBE Transform Group").property("ADBE Anchor Point").setValue([0,0]);`);
10559
12551
  L.push(` ${v}.property("ADBE Transform Group").property("ADBE Position").setValue([0,0]);`);
10560
12552
  if (parentNullVar)
@@ -10574,18 +12566,18 @@ function emitLayer(ctx, ir, ly, parentNullVar) {
10574
12566
  const srcOffset = num(footageHit.srcOffset, 0);
10575
12567
  const cvv = `cv${id}`;
10576
12568
  coverExpr = cvv;
10577
- L.push(` // 素材注入(${esc(String(ly.type))}): footage srcOffset=${r35(srcOffset)}`);
12569
+ L.push(` // 素材注入(${esc(String(ly.type))}): footage srcOffset=${r37(srcOffset)}`);
10578
12570
  L.push(` var ${v} = null, ${cvv} = 1;`);
10579
12571
  L.push(` if (${fgVar} != null) {`);
10580
12572
  L.push(` try {`);
10581
12573
  L.push(` ${v} = ${cv}.layers.add(${fgVar});`);
10582
12574
  L.push(` var _fw = ${fgVar}.width || ${slotW}, _fh = ${fgVar}.height || ${slotH};`);
10583
12575
  L.push(` ${cvv} = Math.max(${slotW} / _fw, ${slotH} / _fh);`);
10584
- L.push(` ${v}.startTime = ${r35(inn)} - ${r35(srcOffset)};`);
12576
+ L.push(` ${v}.startTime = ${r37(inn)} - ${r37(srcOffset)};`);
10585
12577
  L.push(` } catch (e) { ${v} = null; }`);
10586
12578
  L.push(` }`);
10587
12579
  L.push(` if (${v} == null) {`);
10588
- L.push(` ${v} = ${cv}.layers.addSolid([0.35,0.35,0.4], "${name} [占位]", ${slotW}, ${slotH}, 1, ${r35(c3.duration)});`);
12580
+ L.push(` ${v} = ${cv}.layers.addSolid([0.35,0.35,0.4], "${name} [占位]", ${slotW}, ${slotH}, 1, ${r37(c3.duration)});`);
10589
12581
  L.push(` ${cvv} = 1;`);
10590
12582
  L.push(` }`);
10591
12583
  } else if (ly.type === "text") {
@@ -10606,15 +12598,15 @@ function emitLayer(ctx, ir, ly, parentNullVar) {
10606
12598
  const h = Math.max(2, Math.round(num(ly.h, 400)));
10607
12599
  if (ly.shape === "ellipse")
10608
12600
  L.push(` // TODO: 原层为椭圆形状,固态占位,可手动换 shape layer`);
10609
- L.push(` var ${v} = ${cv}.layers.addSolid([${col.join(",")}], "${name}", ${w}, ${h}, 1, ${r35(c3.duration)});`);
12601
+ L.push(` var ${v} = ${cv}.layers.addSolid([${col.join(",")}], "${name}", ${w}, ${h}, 1, ${r37(c3.duration)});`);
10610
12602
  } else {
10611
12603
  const w = Math.max(2, Math.round(num(ly.w, c3.w)));
10612
12604
  const h = Math.max(2, Math.round(num(ly.h, c3.h)));
10613
12605
  L.push(` // 占位素材(${esc(String(ly.type))}${ly.asset ? ` asset=${esc(String(ly.asset))}` : ""}): 请替换为真实素材`);
10614
- L.push(` var ${v} = ${cv}.layers.addSolid([0.35,0.35,0.4], "${name} [占位]", ${w}, ${h}, 1, ${r35(c3.duration)});`);
12606
+ L.push(` var ${v} = ${cv}.layers.addSolid([0.35,0.35,0.4], "${name} [占位]", ${w}, ${h}, 1, ${r37(c3.duration)});`);
10615
12607
  }
10616
12608
  L.push(` ${v}.name = "${name}";`);
10617
- L.push(` ${v}.inPoint = ${r35(inn)}; ${v}.outPoint = ${r35(out)};`);
12609
+ L.push(` ${v}.inPoint = ${r37(inn)}; ${v}.outPoint = ${r37(out)};`);
10618
12610
  if (parentNullVar)
10619
12611
  L.push(` try { ${v}.parent = ${parentNullVar}; } catch (e) {}`);
10620
12612
  if (ly.blend && ly.blend !== "normal") {
@@ -10624,7 +12616,7 @@ function emitLayer(ctx, ir, ly, parentNullVar) {
10624
12616
  L.push(` try { ${v}.blendingMode = BlendingMode.${bm}; } catch (e) {}`);
10625
12617
  }
10626
12618
  const tf = `${v}.property("ADBE Transform Group")`;
10627
- L.push(` ${tf}.property("ADBE Position").setValue([${r35(px)}, ${r35(py)}]);`);
12619
+ L.push(` ${tf}.property("ADBE Position").setValue([${r37(px)}, ${r37(py)}]);`);
10628
12620
  const anim = ly.anim ?? {};
10629
12621
  const bakedProps = new Set(ctx.bakeOps ? bakeLayerOps(ly).tracks.map((t) => t.prop) : []);
10630
12622
  if ((anim.x?.length || anim.y?.length) && !bakedProps.has("position")) {
@@ -10639,10 +12631,10 @@ function emitLayer(ctx, ir, ly, parentNullVar) {
10639
12631
  }
10640
12632
  }
10641
12633
  if (anim.rot?.length && !bakedProps.has("rotation")) {
10642
- emitKeys(ctx, `${tf}.property("ADBE Rotate Z")`, anim.rot.map((k) => ({ t: k.t, value: `${r35(num(k.v, 0))}`, e: k.e })));
12634
+ emitKeys(ctx, `${tf}.property("ADBE Rotate Z")`, anim.rot.map((k) => ({ t: k.t, value: `${r37(num(k.v, 0))}`, e: k.e })));
10643
12635
  }
10644
12636
  if (anim.opacity?.length) {
10645
- emitKeys(ctx, `${tf}.property("ADBE Opacity")`, anim.opacity.map((k) => ({ t: k.t, value: `${r35(Math.min(1, Math.max(0, num(k.v, 1))) * 100)}`, e: k.e })));
12637
+ 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 })));
10646
12638
  }
10647
12639
  if (anim.ls?.length) {
10648
12640
  L.push(` // TODO: letterspacing 轨未自动映射(AE 需 Animator>Tracking),共 ${anim.ls.length} 帧`);
@@ -10660,7 +12652,7 @@ function emitGroupAnim(ctx, v, ly) {
10660
12652
  const py = num(ly.pos?.[1], 0);
10661
12653
  if (anim.x?.length || anim.y?.length) {
10662
12654
  const merged = mergeChannelTracks(anim.x, anim.y, px, py);
10663
- emitKeys(ctx, `${v}.property("ADBE Transform Group").property("ADBE Position")`, merged.map((k) => ({ t: k.t, value: `[${r35(k.a - px)}, ${r35(k.b - py)}]`, e: k.e })));
12655
+ 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 })));
10664
12656
  }
10665
12657
  for (const ch of ["scale", "rot", "opacity"]) {
10666
12658
  if (anim[ch]?.length) {
@@ -10736,7 +12728,7 @@ function madJsx(opts) {
10736
12728
  totalDur = Math.max(totalDur, win.dropAt + Math.max(0.01, len));
10737
12729
  }
10738
12730
  L.push(``);
10739
- L.push(` var master = app.project.items.addComp("${masterName}", ${mw}, ${mh}, 1, ${r35(totalDur)}, ${r35(mfps)});`);
12731
+ L.push(` var master = app.project.items.addComp("${masterName}", ${mw}, ${mh}, 1, ${r37(totalDur)}, ${r37(mfps)});`);
10740
12732
  L.push(` try { master.bgColor = [0.04,0.04,0.07]; } catch (e) {}`);
10741
12733
  const subVars = [];
10742
12734
  windows.forEach((win, wi) => {
@@ -10748,8 +12740,8 @@ function madJsx(opts) {
10748
12740
  const subName = esc(`${win.uid}-${win.seq}`);
10749
12741
  const subVar = `sub${wi}`;
10750
12742
  L.push(``);
10751
- L.push(` // ==== 子合成 #${wi} 技法 ${subName}(源窗 t0=${r35(win.t0)} t1=${r35(win.t1)})====`);
10752
- L.push(` var ${subVar} = app.project.items.addComp("${subName}", ${sw}, ${sh}, 1, ${r35(sdur)}, ${r35(sfps)});`);
12743
+ L.push(` // ==== 子合成 #${wi} 技法 ${subName}(源窗 t0=${r37(win.t0)} t1=${r37(win.t1)})====`);
12744
+ L.push(` var ${subVar} = app.project.items.addComp("${subName}", ${sw}, ${sh}, 1, ${r37(sdur)}, ${r37(sfps)});`);
10753
12745
  const winFootage = { ...win.footage ?? {} };
10754
12746
  const subCtx = {
10755
12747
  lines: L,
@@ -10770,9 +12762,9 @@ function madJsx(opts) {
10770
12762
  const len = Math.max(0.01, win.outLen ?? win.t1 - win.t0);
10771
12763
  const lv = `mL${wi}`;
10772
12764
  L.push(` var ${lv} = master.layers.add(${subVar});`);
10773
- L.push(` ${lv}.startTime = ${r35(win.dropAt - win.t0)};`);
10774
- L.push(` ${lv}.inPoint = ${r35(win.dropAt)};`);
10775
- L.push(` ${lv}.outPoint = ${r35(win.dropAt + len)};`);
12765
+ L.push(` ${lv}.startTime = ${r37(win.dropAt - win.t0)};`);
12766
+ L.push(` ${lv}.inPoint = ${r37(win.dropAt)};`);
12767
+ L.push(` ${lv}.outPoint = ${r37(win.dropAt + len)};`);
10776
12768
  L.push(` try {`);
10777
12769
  L.push(` var _cov = Math.max(${mw} / ${subVar}.width, ${mh} / ${subVar}.height) * 100;`);
10778
12770
  L.push(` ${lv}.property("ADBE Transform Group").property("ADBE Scale").setValue([_cov, _cov]);`);
@@ -10794,7 +12786,7 @@ function madJsx(opts) {
10794
12786
  L.push(` var mk = master.property("ADBE Marker");`);
10795
12787
  for (const m of markers) {
10796
12788
  const label = m.downbeat ? "downbeat" : "beat";
10797
- L.push(` mk.setValueAtTime(${r35(m.t)}, new MarkerValue("${label}"));`);
12789
+ L.push(` mk.setValueAtTime(${r37(m.t)}, new MarkerValue("${label}"));`);
10798
12790
  }
10799
12791
  L.push(` } catch (e) {}`);
10800
12792
  }
@@ -10810,7 +12802,7 @@ function madJsx(opts) {
10810
12802
 
10811
12803
  // src/lib/mad/scan.ts
10812
12804
  import { readdirSync, statSync as statSync2 } from "node:fs";
10813
- import { extname as extname6, join as join22 } from "node:path";
12805
+ import { extname as extname6, join as join23 } from "node:path";
10814
12806
  var VIDEO_EXTS2 = new Set(defaultExtsFor("video") ?? []);
10815
12807
  function scanFolder(dirAbs, opts = {}) {
10816
12808
  const probe = opts.probe ?? probeGeometry;
@@ -10821,7 +12813,7 @@ function scanFolder(dirAbs, opts = {}) {
10821
12813
  } catch {
10822
12814
  throw new Error(`素材文件夹不存在或无法读取:${dirAbs}`);
10823
12815
  }
10824
- const files = entries.filter((n) => VIDEO_EXTS2.has(extname6(n).toLowerCase())).map((n) => join22(dirAbs, n)).filter((p) => {
12816
+ const files = entries.filter((n) => VIDEO_EXTS2.has(extname6(n).toLowerCase())).map((n) => join23(dirAbs, n)).filter((p) => {
10825
12817
  try {
10826
12818
  return statSync2(p).isFile();
10827
12819
  } catch {
@@ -10951,7 +12943,7 @@ function selectWindows(opts) {
10951
12943
 
10952
12944
  // src/lib/mad/beat.ts
10953
12945
  var clamp2 = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
10954
- var r36 = (v) => Math.round(v * 1000) / 1000;
12946
+ var r38 = (v) => Math.round(v * 1000) / 1000;
10955
12947
  var MIN_WIN = 0.4;
10956
12948
  var MAX_WIN = 6;
10957
12949
  function fixedRhythm(natLens) {
@@ -10959,7 +12951,7 @@ function fixedRhythm(natLens) {
10959
12951
  let t = 0;
10960
12952
  for (const nl of natLens) {
10961
12953
  const outLen = clamp2(nl, MIN_WIN, MAX_WIN);
10962
- placements.push({ dropAt: r36(t), outLen: r36(outLen) });
12954
+ placements.push({ dropAt: r38(t), outLen: r38(outLen) });
10963
12955
  t += outLen;
10964
12956
  }
10965
12957
  return { placements, markers: [] };
@@ -10969,7 +12961,7 @@ function beatQuantized(natLens, analysis) {
10969
12961
  const bts = (analysis.beats ?? []).filter((t2) => Number.isFinite(t2) && t2 >= 0).sort((a, b) => a - b);
10970
12962
  const dbSpanOk = dbs.length >= 2 && dbs[1] - dbs[0] <= MAX_WIN;
10971
12963
  const snap = dbSpanOk ? dbs : bts;
10972
- const snapSet = new Set(dbs.map((t2) => r36(t2)));
12964
+ const snapSet = new Set(dbs.map((t2) => r38(t2)));
10973
12965
  if (snap.length < 2) {
10974
12966
  return { plan: fixedRhythm(natLens), level: 2 };
10975
12967
  }
@@ -10982,27 +12974,27 @@ function beatQuantized(natLens, analysis) {
10982
12974
  si++;
10983
12975
  if (si >= snap.length) {
10984
12976
  const outLen = clamp2(natLens[i], MIN_WIN, MAX_WIN);
10985
- placements.push({ dropAt: r36(dropAt), outLen: r36(outLen) });
12977
+ placements.push({ dropAt: r38(dropAt), outLen: r38(outLen) });
10986
12978
  t = dropAt + outLen;
10987
12979
  continue;
10988
12980
  }
10989
12981
  const nextSnap = snap[si];
10990
12982
  const slotLen = clamp2(nextSnap - dropAt, MIN_WIN, MAX_WIN);
10991
- placements.push({ dropAt: r36(dropAt), outLen: r36(slotLen) });
12983
+ placements.push({ dropAt: r38(dropAt), outLen: r38(slotLen) });
10992
12984
  t = dropAt + slotLen;
10993
12985
  si++;
10994
12986
  }
10995
12987
  const totalDur = placements.length ? placements[placements.length - 1].dropAt + placements[placements.length - 1].outLen : 0;
10996
- const allBeats = [...new Set([...bts, ...dbs].map((x) => r36(x)))].sort((a, b) => a - b);
12988
+ const allBeats = [...new Set([...bts, ...dbs].map((x) => r38(x)))].sort((a, b) => a - b);
10997
12989
  const markers = allBeats.filter((tt) => tt >= 0 && tt <= totalDur + 0.000001).map((tt) => ({ t: tt, downbeat: snapSet.has(tt) }));
10998
12990
  return { plan: { placements, markers }, level: 1 };
10999
12991
  }
11000
12992
 
11001
12993
  // src/lib/mad/data.ts
11002
12994
  import { createHash as createHash2 } from "node:crypto";
11003
- import { mkdir as mkdir9, readFile as readFile7, rename as rename2, rm, writeFile as writeFile9, readdir } from "node:fs/promises";
11004
- import { existsSync as existsSync18 } from "node:fs";
11005
- import { join as join23 } from "node:path";
12995
+ import { mkdir as mkdir9, readFile as readFile8, rename as rename2, rm, writeFile as writeFile9, readdir } from "node:fs/promises";
12996
+ import { existsSync as existsSync20 } from "node:fs";
12997
+ import { join as join24 } from "node:path";
11006
12998
  function madCacheDir() {
11007
12999
  return homeFile("mad-cache");
11008
13000
  }
@@ -11031,10 +13023,10 @@ async function atomicWrite(dest, data) {
11031
13023
  await rename2(tmp, dest);
11032
13024
  }
11033
13025
  async function verifyFile(path, sha256) {
11034
- if (!existsSync18(path))
13026
+ if (!existsSync20(path))
11035
13027
  return false;
11036
13028
  try {
11037
- const buf = await readFile7(path);
13029
+ const buf = await readFile8(path);
11038
13030
  return sha256Hex(buf) === sha256;
11039
13031
  } catch {
11040
13032
  return false;
@@ -11059,7 +13051,7 @@ async function cleanupOldVersions(cacheRoot, keepVersion, warn) {
11059
13051
  for (const e of entries) {
11060
13052
  const m = /^v(\d+)$/.exec(e);
11061
13053
  if (m && Number(m[1]) !== keepVersion) {
11062
- await rm(join23(cacheRoot, e), { recursive: true, force: true }).catch(() => {});
13054
+ await rm(join24(cacheRoot, e), { recursive: true, force: true }).catch(() => {});
11063
13055
  }
11064
13056
  }
11065
13057
  } catch {}
@@ -11068,7 +13060,7 @@ async function ensureMadData(opts, deps) {
11068
13060
  const { cacheRoot, warn } = deps;
11069
13061
  const timeout = deps.manifestTimeoutMs ?? 8000;
11070
13062
  const mfUrl = deps.manifestUrl ?? manifestUrl();
11071
- const snapshotPath = join23(cacheRoot, "manifest.json");
13063
+ const snapshotPath = join24(cacheRoot, "manifest.json");
11072
13064
  let manifest = null;
11073
13065
  let online = false;
11074
13066
  try {
@@ -11083,11 +13075,11 @@ async function ensureMadData(opts, deps) {
11083
13075
  warn(`manifest 拉取失败(${e instanceof Error ? e.message : String(e)}),回退本地缓存`);
11084
13076
  }
11085
13077
  if (!manifest) {
11086
- if (!existsSync18(snapshotPath)) {
13078
+ if (!existsSync20(snapshotPath)) {
11087
13079
  throw new Error("首次使用需联网下载技法数据。请连网后重跑 `gtrk tool mad`(数据会缓存到本地,之后可离线出片)。");
11088
13080
  }
11089
13081
  try {
11090
- manifest = validateManifest(JSON.parse(await readFile7(snapshotPath, "utf8")));
13082
+ manifest = validateManifest(JSON.parse(await readFile8(snapshotPath, "utf8")));
11091
13083
  } catch {
11092
13084
  throw new Error("本地 manifest 缓存损坏且当前离线。请连网重跑,或加 --refresh 强制重新下载。");
11093
13085
  }
@@ -11098,14 +13090,14 @@ async function ensureMadData(opts, deps) {
11098
13090
  }
11099
13091
  }
11100
13092
  const version = manifest.version;
11101
- const verDir = join23(cacheRoot, `v${version}`);
11102
- const poolPath = join23(verDir, "mad_pool.json");
13093
+ const verDir = join24(cacheRoot, `v${version}`);
13094
+ const poolPath = join24(verDir, "mad_pool.json");
11103
13095
  const poolMeta = manifest.datasets.mad_pool;
11104
13096
  const cacheValid = await verifyFile(poolPath, poolMeta.sha256);
11105
13097
  const needDownload = !!opts.refresh || !cacheValid;
11106
13098
  if (needDownload) {
11107
13099
  if (!online) {
11108
- if (existsSync18(poolPath)) {
13100
+ if (existsSync20(poolPath)) {
11109
13101
  throw new Error("本地技法数据缓存损坏且当前离线。请连网重跑,或加 --refresh 强制重新下载。");
11110
13102
  }
11111
13103
  throw new Error("首次使用需联网下载技法数据。请连网后重跑 `gtrk tool mad`(数据会缓存到本地,之后可离线出片)。");
@@ -11129,7 +13121,7 @@ async function ensureMadData(opts, deps) {
11129
13121
  }
11130
13122
  let pool;
11131
13123
  try {
11132
- pool = JSON.parse(await readFile7(poolPath, "utf8"));
13124
+ pool = JSON.parse(await readFile8(poolPath, "utf8"));
11133
13125
  if (!Array.isArray(pool))
11134
13126
  throw new Error("mad_pool 非数组");
11135
13127
  } catch (e) {
@@ -11140,11 +13132,11 @@ async function ensureMadData(opts, deps) {
11140
13132
 
11141
13133
  // src/lib/mad/pool.ts
11142
13134
  import { gunzipSync } from "node:zlib";
11143
- import { mkdir as mkdir10, readFile as readFile8 } from "node:fs/promises";
11144
- import { existsSync as existsSync19 } from "node:fs";
11145
- import { join as join24 } from "node:path";
13135
+ import { mkdir as mkdir10, readFile as readFile9 } from "node:fs/promises";
13136
+ import { existsSync as existsSync21 } from "node:fs";
13137
+ import { join as join25 } from "node:path";
11146
13138
  function shardPath(verDir, shard) {
11147
- return join24(verDir, "ir", `${shard}.json.gz`);
13139
+ return join25(verDir, "ir", `${shard}.json.gz`);
11148
13140
  }
11149
13141
  function decodeShard(gz) {
11150
13142
  const json = gunzipSync(gz).toString("utf8");
@@ -11161,9 +13153,9 @@ function makeIrLoader(assetsBase, verDir, online, deps) {
11161
13153
  if (cached)
11162
13154
  return cached;
11163
13155
  const path = shardPath(verDir, shard);
11164
- if (existsSync19(path)) {
13156
+ if (existsSync21(path)) {
11165
13157
  try {
11166
- const s2 = decodeShard(await readFile8(path));
13158
+ const s2 = decodeShard(await readFile9(path));
11167
13159
  memo.set(shard, s2);
11168
13160
  return s2;
11169
13161
  } catch {
@@ -11179,7 +13171,7 @@ function makeIrLoader(assetsBase, verDir, online, deps) {
11179
13171
  throw new Error(`IR 分片下载失败 HTTP ${res.status}:${url}`);
11180
13172
  const buf = new Uint8Array(await res.arrayBuffer());
11181
13173
  const s = decodeShard(buf);
11182
- await mkdir10(join24(verDir, "ir"), { recursive: true });
13174
+ await mkdir10(join25(verDir, "ir"), { recursive: true });
11183
13175
  await atomicWrite(path, buf);
11184
13176
  memo.set(shard, s);
11185
13177
  return s;
@@ -11193,7 +13185,7 @@ function makeIrLoader(assetsBase, verDir, online, deps) {
11193
13185
  return ir;
11194
13186
  },
11195
13187
  shardCached(shard) {
11196
- return memo.has(shard) || existsSync19(shardPath(verDir, shard));
13188
+ return memo.has(shard) || existsSync21(shardPath(verDir, shard));
11197
13189
  }
11198
13190
  };
11199
13191
  }
@@ -11215,7 +13207,7 @@ async function analyzeBgm(cfg, bgmAbs, deps) {
11215
13207
  uploadCached: deps.uploadCached,
11216
13208
  invalidateUpload: deps.invalidateUpload,
11217
13209
  submitTask: deps.submitTask,
11218
- sleep: deps.sleep ?? ((ms) => new Promise((resolve11) => setTimeout(resolve11, ms)))
13210
+ sleep: deps.sleep ?? ((ms) => new Promise((resolve12) => setTimeout(resolve12, ms)))
11219
13211
  });
11220
13212
  const { taskId } = submitted;
11221
13213
  const output = await deps.pollToolTask(cfg, ANALYZE_TASK, taskId, {});
@@ -11276,8 +13268,8 @@ async function runMad(inputArg, opts, deps = {}) {
11276
13268
  const probeDur = deps.probeDurationFn ?? probeDuration;
11277
13269
  if (!inputArg)
11278
13270
  throw new Error("用法:gtrk tool mad <素材文件夹> [--bgm 歌.mp3]");
11279
- const dirAbs = resolve11(inputArg);
11280
- if (!existsSync20(dirAbs) || !statSync3(dirAbs).isDirectory()) {
13271
+ const dirAbs = resolve12(inputArg);
13272
+ if (!existsSync22(dirAbs) || !statSync3(dirAbs).isDirectory()) {
11281
13273
  throw new Error(`素材文件夹不存在或不是目录:${dirAbs}`);
11282
13274
  }
11283
13275
  const { videos, orientation, skipped } = scanFolder(dirAbs, {
@@ -11308,7 +13300,7 @@ async function runMad(inputArg, opts, deps = {}) {
11308
13300
  let level = 3;
11309
13301
  let analysis = null;
11310
13302
  let bgmForTrack;
11311
- const bgmAbs = opts.bgm ? resolve11(opts.bgm) : undefined;
13303
+ const bgmAbs = opts.bgm ? resolve12(opts.bgm) : undefined;
11312
13304
  if (bgmAbs) {
11313
13305
  let dur = -1;
11314
13306
  try {
@@ -11391,9 +13383,9 @@ async function runMad(inputArg, opts, deps = {}) {
11391
13383
  const header = madHeader(version, now.toISOString());
11392
13384
  const bgm = level <= 2 && bgmForTrack ? { path: bgmForTrack, markers } : undefined;
11393
13385
  const { jsx } = madJsx({ master, windows, header, bgm });
11394
- const outDir = opts.out ? resolve11(opts.out) : join25(process.cwd(), `mad-${timestamp4(now)}`);
13386
+ const outDir = opts.out ? resolve12(opts.out) : join26(process.cwd(), `mad-${timestamp4(now)}`);
11395
13387
  await mkdir11(outDir, { recursive: true });
11396
- const jsxPath = join25(outDir, "mad.jsx");
13388
+ const jsxPath = join26(outDir, "mad.jsx");
11397
13389
  await writeFile10(jsxPath, jsx);
11398
13390
  const result = {
11399
13391
  ok: true,
@@ -11405,7 +13397,7 @@ async function runMad(inputArg, opts, deps = {}) {
11405
13397
  degradeLevel: level,
11406
13398
  techniques: chosen.map((c3) => ({ uid: c3.entry.uid, pid: c3.entry.pid, cat: c3.entry.cat, t0: c3.entry.t0, t1: c3.entry.t1 }))
11407
13399
  };
11408
- await writeFile10(join25(outDir, "result.json"), JSON.stringify({ ...result, finishedAt: now.toISOString() }, null, 2));
13400
+ await writeFile10(join26(outDir, "result.json"), JSON.stringify({ ...result, finishedAt: now.toISOString() }, null, 2));
11409
13401
  warn(completionMessage(jsxPath, level));
11410
13402
  return result;
11411
13403
  }
@@ -11537,9 +13529,9 @@ async function runMadInTool(inputArg, opts) {
11537
13529
  }
11538
13530
 
11539
13531
  // src/commands/transcript.ts
11540
- import { existsSync as existsSync21 } from "node:fs";
13532
+ import { existsSync as existsSync23 } from "node:fs";
11541
13533
  import { mkdir as mkdir12, rename as rename3, rm as rm2, stat as stat6, writeFile as writeFile11 } from "node:fs/promises";
11542
- import { basename as basename12, dirname as dirname10, extname as extname7, join as join26, resolve as resolve12 } from "node:path";
13534
+ import { basename as basename13, dirname as dirname10, extname as extname7, join as join27, resolve as resolve13 } from "node:path";
11543
13535
 
11544
13536
  // src/lib/transcript.ts
11545
13537
  var AGENT_SUMMARY_PENDING = "<!-- gtrk:agent-summary-pending -->";
@@ -11660,7 +13652,7 @@ function buildDeps(overrides = {}) {
11660
13652
  upload: overrides.upload ?? uploadCached,
11661
13653
  invalidate: overrides.invalidate ?? invalidateUpload,
11662
13654
  submit: overrides.submit ?? submitTask,
11663
- sleep: overrides.sleep ?? ((ms) => new Promise((resolve13) => setTimeout(resolve13, ms))),
13655
+ sleep: overrides.sleep ?? ((ms) => new Promise((resolve14) => setTimeout(resolve14, ms))),
11664
13656
  poll: overrides.poll ?? (async (cfg, taskType, taskId, onTick) => await pollToolTask(cfg, taskType, taskId, { onTick })),
11665
13657
  writeMarkdown: overrides.writeMarkdown ?? writeMarkdownAtomic,
11666
13658
  now: overrides.now ?? (() => new Date)
@@ -11675,8 +13667,8 @@ async function validateTranscriptInput(input) {
11675
13667
  if (looksLikeRemote(input)) {
11676
13668
  throw new Error("视频转文字稿仅支持本地视频文件,不支持 URL、平台视频地址或远端下载");
11677
13669
  }
11678
- const inputAbs = resolve12(input);
11679
- if (!existsSync21(inputAbs))
13670
+ const inputAbs = resolve13(input);
13671
+ if (!existsSync23(inputAbs))
11680
13672
  throw new Error(`本地视频不存在:${inputAbs}`);
11681
13673
  const info = await stat6(inputAbs);
11682
13674
  if (!info.isFile())
@@ -11688,8 +13680,8 @@ async function validateTranscriptInput(input) {
11688
13680
  return inputAbs;
11689
13681
  }
11690
13682
  function resolveTranscriptOutput(inputAbs, out) {
11691
- const base = basename12(inputAbs, extname7(inputAbs));
11692
- const output = out ? resolve12(out) : join26(dirname10(inputAbs), `${base}-transcript.md`);
13683
+ const base = basename13(inputAbs, extname7(inputAbs));
13684
+ const output = out ? resolve13(out) : join27(dirname10(inputAbs), `${base}-transcript.md`);
11693
13685
  if (extname7(output).toLowerCase() !== ".md")
11694
13686
  throw new Error("--out 必须指向一个 .md 文件");
11695
13687
  return output;
@@ -11711,8 +13703,8 @@ async function runTranscript(input, opts = {}, depsOverride) {
11711
13703
  const output = resolveTranscriptOutput(inputAbs, opts.out);
11712
13704
  const deps = buildDeps(depsOverride);
11713
13705
  const language = opts.lang?.trim() || "zh-CN";
11714
- const sourceName = basename12(inputAbs);
11715
- const title = basename12(inputAbs, extname7(inputAbs));
13706
+ const sourceName = basename13(inputAbs);
13707
+ const title = basename13(inputAbs, extname7(inputAbs));
11716
13708
  log.step(`▶ 视频转文字稿:${sourceName}`);
11717
13709
  log.step("① 本地探测视频…");
11718
13710
  const geometry = deps.probe(inputAbs, opts.ffmpegPath);
@@ -11724,7 +13716,7 @@ async function runTranscript(input, opts = {}, depsOverride) {
11724
13716
  log.step("② 本地抽取 16k 单声道音频(原视频不上传)…");
11725
13717
  const audio = await deps.extract(inputAbs, opts.ffmpegPath);
11726
13718
  deps.assertDuration(geometry.duration, audio, opts.ffmpegPath);
11727
- log.info(`上传物:${basename12(audio)}(仅音频衍生物)`);
13719
+ log.info(`上传物:${basename13(audio)}(仅音频衍生物)`);
11728
13720
  log.step("③ 上传音频并提交 ASR…");
11729
13721
  const payload = (fileId) => ({ file_id: fileId, language, word_level: true });
11730
13722
  const submitted = await uploadAndSubmitTask(deps.cfg, audio, TASK_TYPE3, payload, {
@@ -11772,9 +13764,9 @@ function registerTranscript(program2) {
11772
13764
  }
11773
13765
 
11774
13766
  // src/commands/music-visualizer.ts
11775
- import { resolve as resolve13, join as join27, dirname as dirname11, basename as basename13, extname as extname8 } from "node:path";
13767
+ import { resolve as resolve14, join as join28, dirname as dirname11, basename as basename14, extname as extname8 } from "node:path";
11776
13768
  import { mkdir as mkdir13, writeFile as writeFile12 } from "node:fs/promises";
11777
- import { existsSync as existsSync22 } from "node:fs";
13769
+ import { existsSync as existsSync24 } from "node:fs";
11778
13770
  var TASK_TYPE4 = "music_visualizer";
11779
13771
  var PRICE_KEY2 = "music_visualizer";
11780
13772
  var HEX_RE = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
@@ -11822,7 +13814,7 @@ function timestamp5() {
11822
13814
  return `${p(d.getFullYear() % 100)}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
11823
13815
  }
11824
13816
  function assertExt(pathAbs, exts, label) {
11825
- if (!existsSync22(pathAbs))
13817
+ if (!existsSync24(pathAbs))
11826
13818
  throw new Error(`${label}不存在:${pathAbs}`);
11827
13819
  const e = extname8(pathAbs).toLowerCase();
11828
13820
  if (exts.length && !exts.includes(e)) {
@@ -11878,22 +13870,22 @@ async function runMusicVisualizer(audio, opts) {
11878
13870
  if (opts.json)
11879
13871
  routeLogsToStderr();
11880
13872
  const cfg = loadConfig();
11881
- const audioAbs = resolve13(audio);
13873
+ const audioAbs = resolve14(audio);
11882
13874
  assertExt(audioAbs, AUDIO_EXTS2, "音频");
11883
13875
  const template = opts.template == null ? "" : String(opts.template).trim();
11884
13876
  if (!template)
11885
13877
  throw new Error("--template 必填:请指定可视化模板 id(取值见云端 API 文档 / 服务端模板列表)");
11886
13878
  const styleFields = buildStyleFields(opts);
11887
- const bgAbs = opts.background ? resolve13(opts.background) : undefined;
13879
+ const bgAbs = opts.background ? resolve14(opts.background) : undefined;
11888
13880
  if (bgAbs)
11889
13881
  assertExt(bgAbs, [...IMAGE_EXTS2, ...VIDEO_EXTS3], "背景素材");
11890
- const coverAbs = opts.cover ? resolve13(opts.cover) : undefined;
13882
+ const coverAbs = opts.cover ? resolve14(opts.cover) : undefined;
11891
13883
  if (coverAbs)
11892
13884
  assertExt(coverAbs, IMAGE_EXTS2, "封面图");
11893
13885
  const extraParams = parseExtraParams3(opts.param, opts.paramsJson);
11894
- const projName = basename13(audioAbs, extname8(audioAbs));
11895
- const outDir = resolve13(opts.out ?? join27(dirname11(audioAbs), `${projName}-visualizer-${timestamp5()}`));
11896
- log.step(`▶ 音乐可视化:${basename13(audioAbs)}(模板 ${template}${bgAbs ? " · 背景" : ""}${coverAbs ? " · 封面" : ""})`);
13886
+ const projName = basename14(audioAbs, extname8(audioAbs));
13887
+ const outDir = resolve14(opts.out ?? join28(dirname11(audioAbs), `${projName}-visualizer-${timestamp5()}`));
13888
+ log.step(`▶ 音乐可视化:${basename14(audioAbs)}(模板 ${template}${bgAbs ? " · 背景" : ""}${coverAbs ? " · 封面" : ""})`);
11897
13889
  let billingHint;
11898
13890
  try {
11899
13891
  billingHint = (await resolveToolPricing(PRICE_KEY2)).billingHint;
@@ -11939,7 +13931,7 @@ async function runMusicVisualizer(audio, opts) {
11939
13931
  const { taskId } = submitted;
11940
13932
  log.info(`task_id = ${taskId}`);
11941
13933
  await mkdir13(outDir, { recursive: true });
11942
- await writeFile12(join27(outDir, "task.json"), JSON.stringify({ taskId, taskType: TASK_TYPE4, fileId: submitted.fileId, source: audioAbs, template, createdAt: new Date().toISOString() }, null, 2));
13934
+ await writeFile12(join28(outDir, "task.json"), JSON.stringify({ taskId, taskType: TASK_TYPE4, fileId: submitted.fileId, source: audioAbs, template, createdAt: new Date().toISOString() }, null, 2));
11943
13935
  log.step("③ 云端处理中(每 5s 轮询)…");
11944
13936
  const result = await pollTask(cfg, TASK_TYPE4, taskId, (status, progress) => {
11945
13937
  log.tick(`${status}${progress != null ? ` ${Math.round(progress)}%` : ""}`);
@@ -11950,7 +13942,7 @@ async function runMusicVisualizer(audio, opts) {
11950
13942
  const files = [];
11951
13943
  const errors = {};
11952
13944
  if (url) {
11953
- const dest = join27(outDir, `${projName}-visualizer${extFromUrl(url, ".mp4")}`);
13945
+ const dest = join28(outDir, `${projName}-visualizer${extFromUrl(url, ".mp4")}`);
11954
13946
  try {
11955
13947
  await downloadStream(url, dest);
11956
13948
  files.push(dest);
@@ -11972,7 +13964,7 @@ async function runMusicVisualizer(audio, opts) {
11972
13964
  ...Object.keys(errors).length ? { errors } : {},
11973
13965
  finishedAt: new Date().toISOString()
11974
13966
  };
11975
- await writeFile12(join27(outDir, "result.json"), JSON.stringify(resultJson, null, 2));
13967
+ await writeFile12(join28(outDir, "result.json"), JSON.stringify(resultJson, null, 2));
11976
13968
  if (opts.json) {
11977
13969
  process.stdout.write(`${JSON.stringify(resultJson)}
11978
13970
  `);
@@ -11990,7 +13982,7 @@ try {
11990
13982
  process.loadEnvFile?.();
11991
13983
  } catch {}
11992
13984
  migrateLegacyHome();
11993
- var { version } = JSON.parse(readFileSync5(join28(packageRoot(), "package.json"), "utf8"));
13985
+ var { version } = JSON.parse(readFileSync5(join29(packageRoot(), "package.json"), "utf8"));
11994
13986
  var program2 = new Command;
11995
13987
  program2.name("gtrk").description("同合云成片流水线 CLI —— agent 驱动云端任务、产物拉回本地、三方工程文件(客户端/剪映/PR)互通").version(version);
11996
13988
  registerInstall(program2);