@gitruck/cli 0.2.13 → 0.2.15

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,13 +5435,15 @@ function buildLanding(doc, view, opts) {
5001
5435
  dispatch.mg.push({
5002
5436
  beat: beat.id,
5003
5437
  composition_id: compositionId,
5004
- duration: typeof h.duration_hint === "number" ? h.duration_hint : null,
5438
+ duration: r33(track_ed - track_st),
5439
+ duration_hint: typeof h.duration_hint === "number" ? h.duration_hint : null,
5005
5440
  ...h.category !== undefined ? { category: h.category } : {},
5006
5441
  theme: h.theme,
5007
5442
  bg: h.bg,
5008
5443
  slug_hint: h.slug_hint,
5009
5444
  track_st,
5010
- track_ed
5445
+ track_ed,
5446
+ span: beat.span
5011
5447
  });
5012
5448
  } else if (lane === "FILM_BROLL") {
5013
5449
  dispatch.film_broll.push({
@@ -5017,10 +5453,11 @@ function buildLanding(doc, view, opts) {
5017
5453
  per_shot_sec: h.per_shot_sec,
5018
5454
  exclude: h.exclude,
5019
5455
  track_st,
5020
- track_ed
5456
+ track_ed,
5457
+ span: beat.span
5021
5458
  });
5022
5459
  } else if (lane === "AI_DRAMA") {
5023
- 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 });
5024
5461
  } else if (lane !== "A_ROLL") {
5025
5462
  unhandledLanes.add(lane);
5026
5463
  }
@@ -5043,33 +5480,33 @@ function buildLanding(doc, view, opts) {
5043
5480
  skipped.push({ beat: auxTag, reason: "overlay aux 使用 {trigger} 点挂载,一期不支持(二期补合成窗口)" });
5044
5481
  continue;
5045
5482
  }
5046
- const auxFromIdx = idIndex.get(auxFromId);
5047
- const auxToIdx = idIndex.get(auxToId);
5048
- const auxSpanIds = opts.utteranceIds.slice(auxFromIdx, auxToIdx + 1);
5049
- const auxInstances = auxSpanIds.flatMap((id) => byId.get(id) ?? []);
5050
- if (auxInstances.length === 0) {
5483
+ const auxSpan = { from: auxFromId, to: auxToId };
5484
+ const auxEnv = envelopeForSpan(spanIndex, auxSpan);
5485
+ if (auxEnv.kind !== "ok") {
5051
5486
  skipped.push({ beat: auxTag, reason: "overlay aux 源区间 utterance 全被剪,未落轨" });
5052
5487
  continue;
5053
5488
  }
5054
- const auxTrackSt = Math.min(...auxInstances.map((x) => x.track_st));
5055
- const auxTrackEd = Math.max(...auxInstances.map((x) => x.track_ed));
5489
+ const auxTrackSt = auxEnv.track_st;
5490
+ const auxTrackEd = auxEnv.track_ed;
5056
5491
  const auxCompositionId = `${opts.projectSlug}-${beat.id}-aux${auxN}`;
5057
5492
  const ah = aux.handoff;
5058
5493
  dispatch.mg.push({
5059
5494
  beat: beat.id,
5060
5495
  composition_id: auxCompositionId,
5061
- duration: ah && typeof ah.duration_hint === "number" ? ah.duration_hint : null,
5496
+ duration: r33(auxTrackEd - auxTrackSt),
5497
+ duration_hint: ah && typeof ah.duration_hint === "number" ? ah.duration_hint : null,
5062
5498
  category: "overlay",
5063
5499
  theme: ah?.theme,
5064
5500
  bg: ah?.bg,
5065
5501
  slug_hint: ah?.slug_hint,
5066
5502
  track_st: auxTrackSt,
5067
- track_ed: auxTrackEd
5503
+ track_ed: auxTrackEd,
5504
+ span: auxSpan
5068
5505
  });
5069
5506
  const auxMetaBeat = {
5070
5507
  id: auxTag,
5071
5508
  lane: "MG",
5072
- span: { from: auxFromId, to: auxToId },
5509
+ span: auxSpan,
5073
5510
  track_st: auxTrackSt,
5074
5511
  track_ed: auxTrackEd,
5075
5512
  category: "overlay"
@@ -5078,7 +5515,7 @@ function buildLanding(doc, view, opts) {
5078
5515
  const sfrom = opts.sourceIndex.utterances.get(auxFromId);
5079
5516
  const sto = opts.sourceIndex.utterances.get(auxToId);
5080
5517
  if (sfrom && sto && sto.ed > sfrom.st) {
5081
- auxMetaBeat.source_ranges = [{ st: r3(sfrom.st), ed: r3(sto.ed) }];
5518
+ auxMetaBeat.source_ranges = [{ st: r33(sfrom.st), ed: r33(sto.ed) }];
5082
5519
  }
5083
5520
  }
5084
5521
  split.beats.push(auxMetaBeat);
@@ -5130,7 +5567,9 @@ function renderSplitMarkdown(doc, landing, meta) {
5130
5567
  L.push("");
5131
5568
  L.push("## MG Queue");
5132
5569
  for (const r of landing.dispatch.mg) {
5133
- L.push(`- \`${r.beat}\` composition_id=\`${r.composition_id}\`${r.duration != null ? ` · ${r.duration}s` : ""}`);
5570
+ const slot = r.duration != null ? ` · 坑位 ${r.duration}s` : "";
5571
+ const hint = r.duration_hint != null ? ` · hint ${r.duration_hint}s` : "";
5572
+ L.push(`- \`${r.beat}\` composition_id=\`${r.composition_id}\`${slot}${hint}`);
5134
5573
  }
5135
5574
  L.push("");
5136
5575
  L.push("## AI_DRAMA Queue");
@@ -5157,7 +5596,7 @@ var DEFAULT_COLUMN_CONFIG = {
5157
5596
  fallback: { unknown_narrative: "reject" }
5158
5597
  };
5159
5598
  function columnsDir() {
5160
- return join5(gitruckHome(), "columns");
5599
+ return join6(gitruckHome(), "columns");
5161
5600
  }
5162
5601
  var uniq = (xs) => [...new Set(xs)];
5163
5602
  function strArr(v) {
@@ -5214,8 +5653,8 @@ function foldColumnConfigs(layers) {
5214
5653
  return out;
5215
5654
  }
5216
5655
  function readLocalColumn(columnId, dir, warnings) {
5217
- const p = join5(dir, `${columnId}.json`);
5218
- if (!existsSync5(p)) {
5656
+ const p = join6(dir, `${columnId}.json`);
5657
+ if (!existsSync6(p)) {
5219
5658
  warnings.push(`栏目配置不存在:${p},回落内置默认`);
5220
5659
  return;
5221
5660
  }
@@ -5349,8 +5788,8 @@ function effectiveVocab(config) {
5349
5788
 
5350
5789
  // src/lib/ffmpeg.ts
5351
5790
  import { spawn as spawn2, spawnSync as spawnSync3 } from "node:child_process";
5352
- import { existsSync as existsSync6 } from "node:fs";
5353
- import { join as join6 } from "node:path";
5791
+ import { existsSync as existsSync7 } from "node:fs";
5792
+ import { join as join7 } from "node:path";
5354
5793
  var isWin = process.platform === "win32";
5355
5794
  var bin = (base) => isWin ? `${base}.exe` : base;
5356
5795
  var FFMPEG_INSTALL_HINT = `未找到 ffmpeg/ffprobe。请把二者放到 ${ffmpegDir()}(agent 可代办:先查本地确实缺失才拉,` + `面向国内用户优先国内加速站点——GitHub 代理 pass-through 拉 BtbN/gyan.dev 官方静态构建,或同合云自建镜像,` + `并做 sha256 校验),或用 --ffmpeg-path <目录> 指定已装位置。`;
@@ -5372,9 +5811,9 @@ function resolveFfmpeg(ffmpegPath) {
5372
5811
  dirs.push([ffmpegDir(), "~/.gitruck/ffmpeg"]);
5373
5812
  let found = null;
5374
5813
  for (const [dir, label] of dirs) {
5375
- const ff = join6(dir, bin("ffmpeg"));
5376
- const fp = join6(dir, bin("ffprobe"));
5377
- 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)) {
5378
5817
  found = { ffmpeg: ff, ffprobe: fp, source: label };
5379
5818
  break;
5380
5819
  }
@@ -5456,11 +5895,11 @@ function probeCapabilities(res) {
5456
5895
 
5457
5896
  // src/lib/version.ts
5458
5897
  import { readFileSync as readFileSync3 } from "node:fs";
5459
- import { join as join7 } from "node:path";
5898
+ import { join as join8 } from "node:path";
5460
5899
  var REGISTRY = "https://registry.npmjs.org/@gitruck%2Fcli";
5461
5900
  function currentVersion() {
5462
5901
  try {
5463
- const { version } = JSON.parse(readFileSync3(join7(packageRoot(), "package.json"), "utf8"));
5902
+ const { version } = JSON.parse(readFileSync3(join8(packageRoot(), "package.json"), "utf8"));
5464
5903
  return version;
5465
5904
  } catch {
5466
5905
  return "0.0.0";
@@ -5542,7 +5981,7 @@ async function runDoctor() {
5542
5981
  }
5543
5982
  rows.push({ name: "云端连通 + 鉴权", status: apiStatus, detail: apiDetail });
5544
5983
  const draftDir = resolveJianyingDraftDir(undefined);
5545
- const draftOk = !!draftDir && existsSync7(draftDir);
5984
+ const draftOk = !!draftDir && existsSync8(draftDir);
5546
5985
  rows.push({
5547
5986
  name: "剪映草稿目录",
5548
5987
  status: draftOk ? "ok" : "warn",
@@ -5550,15 +5989,15 @@ async function runDoctor() {
5550
5989
  });
5551
5990
  rows.push({
5552
5991
  name: "配置文件",
5553
- status: existsSync7(configPath()) ? "ok" : "warn",
5554
- detail: existsSync7(configPath()) ? configPath() : `未生成 —— 跑 gtrk init(${configPath()})`
5992
+ status: existsSync8(configPath()) ? "ok" : "warn",
5993
+ detail: existsSync8(configPath()) ? configPath() : `未生成 —— 跑 gtrk init(${configPath()})`
5555
5994
  });
5556
5995
  const col = uc.defaultColumn;
5557
- const colFile = col ? join8(columnsDir(), `${col}.json`) : undefined;
5996
+ const colFile = col ? join9(columnsDir(), `${col}.json`) : undefined;
5558
5997
  rows.push({
5559
5998
  name: "当前栏目",
5560
5999
  status: "ok",
5561
- detail: col ? `${col}${colFile && existsSync7(colFile) ? `(${colFile})` : `(⚠ 配置文件缺失:${colFile},将回落内置默认)`}` : "内置默认 —— 想建自己栏目的风格体系,跑 /gtrk-style-maker(不建也能直接用默认)"
6000
+ detail: col ? `${col}${colFile && existsSync8(colFile) ? `(${colFile})` : `(⚠ 配置文件缺失:${colFile},将回落内置默认)`}` : "内置默认 —— 想建自己栏目的风格体系,跑 /gtrk-style-maker(不建也能直接用默认)"
5562
6001
  });
5563
6002
  const ff = resolveFfmpeg();
5564
6003
  if (ff) {
@@ -5605,7 +6044,7 @@ gtrk 体检:
5605
6044
  }
5606
6045
 
5607
6046
  // src/commands/init.ts
5608
- var GUIDE_IMAGE = join9(packageRoot(), "assets", "jianying-draft-path.png");
6047
+ var GUIDE_IMAGE = join10(packageRoot(), "assets", "jianying-draft-path.png");
5609
6048
  function registerInit(program2) {
5610
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);
5611
6050
  }
@@ -5644,7 +6083,7 @@ async function runInit(opts) {
5644
6083
  defaultValue: existing.apiBase ?? DEFAULT_API_BASE
5645
6084
  })).trim();
5646
6085
  let jianyingDraftDir;
5647
- if (existing.jianyingDraftDir && existsSync8(existing.jianyingDraftDir)) {
6086
+ if (existing.jianyingDraftDir && existsSync9(existing.jianyingDraftDir)) {
5648
6087
  if (await promptConfirm(`剪映草稿目录现为 ${existing.jianyingDraftDir},保留吗?`, true)) {
5649
6088
  jianyingDraftDir = existing.jianyingDraftDir;
5650
6089
  }
@@ -5660,7 +6099,7 @@ async function runInit(opts) {
5660
6099
  openFile(GUIDE_IMAGE);
5661
6100
  const manual = (await promptText("剪映草稿根目录(…\\com.lveditor.draft),留空跳过:")).trim();
5662
6101
  if (manual) {
5663
- if (existsSync8(manual))
6102
+ if (existsSync9(manual))
5664
6103
  jianyingDraftDir = resolve3(manual);
5665
6104
  else
5666
6105
  log.warn(`目录不存在,已跳过:${manual}`);
@@ -5734,9 +6173,9 @@ function registerInstall(program2) {
5734
6173
  }
5735
6174
 
5736
6175
  // src/commands/oralcut.ts
5737
- import { resolve as resolve4, join as join14, dirname as dirname3, basename as basename5, extname as extname2 } from "node:path";
5738
- import { mkdir as mkdir4, writeFile as writeFile5, readFile as readFile3 } from "node:fs/promises";
5739
- 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";
5740
6179
 
5741
6180
  // src/lib/config.ts
5742
6181
  function loadConfig() {
@@ -5750,7 +6189,7 @@ function loadConfig() {
5750
6189
  }
5751
6190
 
5752
6191
  // src/lib/cloud.ts
5753
- import { basename } from "node:path";
6192
+ import { basename as basename2 } from "node:path";
5754
6193
  import { writeFile, stat } from "node:fs/promises";
5755
6194
  import { createReadStream } from "node:fs";
5756
6195
  import { Readable } from "node:stream";
@@ -5788,7 +6227,7 @@ async function uploadFile(cfg, path, runtime = {}) {
5788
6227
  if (!bunFile)
5789
6228
  throw new Error("Bun 上传运行时缺少 Bun.file");
5790
6229
  const form = new FormData;
5791
- form.append("file", bunFile(path), basename(path));
6230
+ form.append("file", bunFile(path), basename2(path));
5792
6231
  res = await fetchFn(`${cfg.base}/base/file/upload`, {
5793
6232
  method: "POST",
5794
6233
  headers: { Authorization: cfg.apiKey },
@@ -5798,7 +6237,7 @@ async function uploadFile(cfg, path, runtime = {}) {
5798
6237
  const size = (await stat(path)).size;
5799
6238
  const boundary = `----gtrkFormBoundary${randomBytes(16).toString("hex")}`;
5800
6239
  const head = Buffer.from(`--${boundary}\r
5801
- ` + `Content-Disposition: form-data; name="file"; filename="${basename(path)}"\r
6240
+ ` + `Content-Disposition: form-data; name="file"; filename="${basename2(path)}"\r
5802
6241
  ` + `Content-Type: application/octet-stream\r
5803
6242
  \r
5804
6243
  `, "utf8");
@@ -5889,14 +6328,14 @@ async function download(url, dest) {
5889
6328
  }
5890
6329
 
5891
6330
  // src/lib/upload-cache.ts
5892
- import { join as join10 } from "node:path";
5893
- import { stat as stat3, mkdir, readFile, writeFile as writeFile2 } from "node:fs/promises";
5894
- 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";
5895
6334
 
5896
6335
  // src/lib/chunk-upload.ts
5897
6336
  var import_hash_wasm = __toESM(require_index_umd(), 1);
5898
6337
  import { open, stat as stat2 } from "node:fs/promises";
5899
- import { basename as basename2 } from "node:path";
6338
+ import { basename as basename3 } from "node:path";
5900
6339
  var CHUNK_THRESHOLD = 256 * 1024 * 1024;
5901
6340
  var CONCURRENCY = 3;
5902
6341
  var PART_RETRIES = 3;
@@ -5958,7 +6397,7 @@ async function sleep(ms) {
5958
6397
  }
5959
6398
  async function uploadChunked(cfg, path, opts) {
5960
6399
  const size = (await stat2(path)).size;
5961
- const name = basename2(path);
6400
+ const name = basename3(path);
5962
6401
  for (let rebuilds = 0;; rebuilds++) {
5963
6402
  try {
5964
6403
  return await attemptOnce(cfg, path, name, size, opts);
@@ -6123,8 +6562,8 @@ async function putPart(cfg, uploadId, idx, view) {
6123
6562
 
6124
6563
  // src/lib/upload-cache.ts
6125
6564
  var CACHE_DIR = gitruckHome();
6126
- var CACHE_FILE = join10(CACHE_DIR, "upload-cache.json");
6127
- 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");
6128
6567
  function fingerprintFromStat(s) {
6129
6568
  return `${s.size}:${Math.round(s.mtimeMs)}`;
6130
6569
  }
@@ -6132,10 +6571,10 @@ async function fingerprint(path) {
6132
6571
  return fingerprintFromStat(await stat3(path));
6133
6572
  }
6134
6573
  async function load() {
6135
- if (!existsSync9(CACHE_FILE))
6574
+ if (!existsSync10(CACHE_FILE))
6136
6575
  return {};
6137
6576
  try {
6138
- return JSON.parse(await readFile(CACHE_FILE, "utf8"));
6577
+ return JSON.parse(await readFile2(CACHE_FILE, "utf8"));
6139
6578
  } catch {
6140
6579
  return {};
6141
6580
  }
@@ -6159,10 +6598,10 @@ async function invalidateUpload(path) {
6159
6598
  }
6160
6599
  }
6161
6600
  async function loadSessions() {
6162
- if (!existsSync9(SESSION_FILE))
6601
+ if (!existsSync10(SESSION_FILE))
6163
6602
  return {};
6164
6603
  try {
6165
- return JSON.parse(await readFile(SESSION_FILE, "utf8"));
6604
+ return JSON.parse(await readFile2(SESSION_FILE, "utf8"));
6166
6605
  } catch {
6167
6606
  return {};
6168
6607
  }
@@ -6257,8 +6696,8 @@ async function uploadAndSubmitTask(cfg, path, taskType, buildPayload, options =
6257
6696
 
6258
6697
  // src/lib/media.ts
6259
6698
  import { mkdir as mkdir2, stat as stat4 } from "node:fs/promises";
6260
- import { existsSync as existsSync10 } from "node:fs";
6261
- 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";
6262
6701
  function parseFps(rate) {
6263
6702
  if (typeof rate !== "string")
6264
6703
  return 0;
@@ -6306,14 +6745,14 @@ function probeDuration(path, ffmpegPath) {
6306
6745
  }
6307
6746
  async function artifactPath(inputAbs, ext) {
6308
6747
  const s = await stat4(inputAbs);
6309
- const base = basename3(inputAbs, extname(inputAbs));
6748
+ const base = basename4(inputAbs, extname(inputAbs));
6310
6749
  await mkdir2(audioCacheDir(), { recursive: true });
6311
- return join11(audioCacheDir(), `${base}.${s.size}_${Math.round(s.mtimeMs)}.${ext}`);
6750
+ return join12(audioCacheDir(), `${base}.${s.size}_${Math.round(s.mtimeMs)}.${ext}`);
6312
6751
  }
6313
6752
  async function extractAudio(inputAbs, ffmpegPath) {
6314
6753
  const { ffmpeg } = requireFfmpeg(ffmpegPath);
6315
6754
  const out = await artifactPath(inputAbs, "mp3");
6316
- if (existsSync10(out))
6755
+ if (existsSync11(out))
6317
6756
  return out;
6318
6757
  await runFfmpeg(ffmpeg, [
6319
6758
  "-y",
@@ -6337,7 +6776,7 @@ async function extractAudio(inputAbs, ffmpegPath) {
6337
6776
  async function compress720p(inputAbs, ffmpegPath) {
6338
6777
  const { ffmpeg } = requireFfmpeg(ffmpegPath);
6339
6778
  const out = await artifactPath(inputAbs, "720p.mp4");
6340
- if (existsSync10(out))
6779
+ if (existsSync11(out))
6341
6780
  return out;
6342
6781
  await runFfmpeg(ffmpeg, [
6343
6782
  "-y",
@@ -6369,14 +6808,14 @@ function assertDurationConsistent(originalDuration, artifactAbs, ffmpegPath, tol
6369
6808
  }
6370
6809
 
6371
6810
  // src/lib/materialize.ts
6372
- import { join as join13, basename as basename4 } from "node:path";
6811
+ import { join as join14, basename as basename5 } from "node:path";
6373
6812
  import { mkdir as mkdir3, cp, writeFile as writeFile4 } from "node:fs/promises";
6374
6813
 
6375
6814
  // src/lib/render.ts
6376
- import { writeFile as writeFile3, unlink, readFile as readFile2 } from "node:fs/promises";
6377
- 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";
6378
6817
  import { tmpdir } from "node:os";
6379
- import { join as join12 } from "node:path";
6818
+ import { join as join13 } from "node:path";
6380
6819
  var AUDIO_SAMPLE_RATE = 48000;
6381
6820
  var AUDIO_LAYOUT = "stereo";
6382
6821
  var DEFAULT_CRF = 18;
@@ -6534,7 +6973,7 @@ function materialPathsFromGtrk(gtrk) {
6534
6973
  continue;
6535
6974
  if (!m.path)
6536
6975
  throw new Error(`gtrk 素材 ${m.id} 缺 path(source_path),无法本地渲染`);
6537
- if (!existsSync11(m.path))
6976
+ if (!existsSync12(m.path))
6538
6977
  throw new Error(`gtrk 素材文件不存在:${m.path}`);
6539
6978
  map[String(m.id)] = m.path;
6540
6979
  }
@@ -6548,7 +6987,7 @@ async function renderGtrk(gtrk, outputPath, opts = {}) {
6548
6987
  const { ffmpeg } = requireFfmpeg(opts.ffmpegPath);
6549
6988
  const materialPaths = materialPathsFromGtrk(gtrk);
6550
6989
  const { inputs, graph, total } = buildFilterGraph(gtrk, materialPaths, { crf });
6551
- const filterFile = join12(tmpdir(), `gtrk-filter-${process.pid}-${inputs.length}.txt`);
6990
+ const filterFile = join13(tmpdir(), `gtrk-filter-${process.pid}-${inputs.length}.txt`);
6552
6991
  await writeFile3(filterFile, graph, "utf8");
6553
6992
  try {
6554
6993
  const args = ["-y"];
@@ -6562,7 +7001,7 @@ async function renderGtrk(gtrk, outputPath, opts = {}) {
6562
7001
  }
6563
7002
  }
6564
7003
  async function readGtrkFile(gtrkPath) {
6565
- return JSON.parse(await readFile2(gtrkPath, "utf8"));
7004
+ return JSON.parse(await readFile3(gtrkPath, "utf8"));
6566
7005
  }
6567
7006
 
6568
7007
  // src/lib/materialize.ts
@@ -6586,7 +7025,7 @@ function gtrkSourceName(gtrk) {
6586
7025
  const p = gtrk.materials?.[0]?.path;
6587
7026
  if (!p)
6588
7027
  return;
6589
- const b = basename4(p);
7028
+ const b = basename5(p);
6590
7029
  const dot = b.lastIndexOf(".");
6591
7030
  return dot > 0 ? b.slice(0, dot) : b;
6592
7031
  }
@@ -6598,7 +7037,7 @@ async function materializeResult(opts) {
6598
7037
  throw new Error("任务无工程文件产物(检查 project_formats / 任务是否产出)");
6599
7038
  const errors = { ...output.errors ?? {} };
6600
7039
  await mkdir3(outDir, { recursive: true });
6601
- const resultPath = join13(outDir, "result.json");
7040
+ const resultPath = join14(outDir, "result.json");
6602
7041
  const writeResult = async (extra) => {
6603
7042
  const r = {
6604
7043
  ok: Object.keys(errors).length === 0,
@@ -6620,9 +7059,9 @@ async function materializeResult(opts) {
6620
7059
  const byFormat = {};
6621
7060
  for (const f of files) {
6622
7061
  const base = baseFormat(f.format);
6623
- const fmtDir = join13(outDir, base);
7062
+ const fmtDir = join14(outDir, base);
6624
7063
  await mkdir3(fmtDir, { recursive: true });
6625
- const dest = join13(fmtDir, f.filename);
7064
+ const dest = join14(fmtDir, f.filename);
6626
7065
  try {
6627
7066
  await dl(f.download_url, dest);
6628
7067
  (byFormat[base] ??= []).push(dest);
@@ -6639,9 +7078,9 @@ async function materializeResult(opts) {
6639
7078
  let jianyingDraftPath = null;
6640
7079
  if (byFormat.jianying && opts.draftDir) {
6641
7080
  try {
6642
- jianyingDraftPath = join13(opts.draftDir, basename4(outDir));
7081
+ jianyingDraftPath = join14(opts.draftDir, basename5(outDir));
6643
7082
  await mkdir3(jianyingDraftPath, { recursive: true });
6644
- await cp(join13(outDir, "jianying"), jianyingDraftPath, { recursive: true });
7083
+ await cp(join14(outDir, "jianying"), jianyingDraftPath, { recursive: true });
6645
7084
  log.info(`剪映草稿已落到:${jianyingDraftPath}`);
6646
7085
  } catch (e) {
6647
7086
  jianyingDraftPath = null;
@@ -6658,7 +7097,7 @@ async function materializeResult(opts) {
6658
7097
  log.step("本地渲染成片(ffmpeg)…");
6659
7098
  const project = await readGtrkFile(gtrkPath);
6660
7099
  const name = opts.projName ?? gtrkSourceName(project) ?? taskId;
6661
- const outMp4 = join13(outDir, `${name}.mp4`);
7100
+ const outMp4 = join14(outDir, `${name}.mp4`);
6662
7101
  const r = await renderGtrk(project, outMp4, {
6663
7102
  crf: opts.crf != null ? Number(opts.crf) : undefined,
6664
7103
  codec: opts.codec,
@@ -6680,7 +7119,7 @@ async function materializeResult(opts) {
6680
7119
  log.step("三方打开(产物已就位,按需自取):");
6681
7120
  for (const base of Object.keys(byFormat)) {
6682
7121
  const meta = FORMAT_META[base];
6683
- const target = base === "jianying" ? jianyingDraftPath ?? join13(outDir, "jianying") : byFormat[base][0];
7122
+ const target = base === "jianying" ? jianyingDraftPath ?? join14(outDir, "jianying") : byFormat[base][0];
6684
7123
  console.log(` • ${meta?.label ?? base}:${meta?.openHint(target) ?? target}`);
6685
7124
  }
6686
7125
  if (rendered)
@@ -6748,23 +7187,23 @@ async function runOralCut(input, opts) {
6748
7187
  routeLogsToStderr();
6749
7188
  const cfg = loadConfig();
6750
7189
  const inputAbs = resolve4(input);
6751
- if (!existsSync12(inputAbs))
7190
+ if (!existsSync13(inputAbs))
6752
7191
  throw new Error(`毛片不存在:${inputAbs}`);
6753
- const projName = basename5(inputAbs, extname2(inputAbs));
7192
+ const projName = basename6(inputAbs, extname2(inputAbs));
6754
7193
  const formats = opts.formats.split(",").map((s) => s.trim()).filter(Boolean);
6755
7194
  if (opts.render && !formats.includes("gtrk"))
6756
7195
  formats.push("gtrk");
6757
7196
  const wantJianying = formats.some((f) => f === "jianying" || f === "capcut");
6758
- 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()}`));
6759
7198
  let scriptPath = opts.script ? resolve4(opts.script) : undefined;
6760
7199
  if (!scriptPath) {
6761
- const sibling = join14(dirname3(inputAbs), `${projName}.txt`);
6762
- if (existsSync12(sibling)) {
7200
+ const sibling = join15(dirname3(inputAbs), `${projName}.txt`);
7201
+ if (existsSync13(sibling)) {
6763
7202
  scriptPath = sibling;
6764
7203
  log.info(`自动识别到同名文稿:${sibling}(按有稿剪辑;不想用就改名或显式 --script)`);
6765
7204
  }
6766
7205
  }
6767
- const script = scriptPath ? await readFile3(scriptPath, "utf8") : undefined;
7206
+ const script = scriptPath ? await readFile4(scriptPath, "utf8") : undefined;
6768
7207
  let draftDir;
6769
7208
  if (wantJianying) {
6770
7209
  draftDir = resolveJianyingDraftDir(opts.jianyingDraftDir);
@@ -6773,14 +7212,14 @@ async function runOralCut(input, opts) {
6773
7212
  else
6774
7213
  log.warn("没找到剪映草稿目录 → 将只产 draft_content.json、缺 meta。可加 --jianying-draft-dir <你的草稿目录> 重跑。");
6775
7214
  }
6776
- 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 ? " · 本地渲染" : ""})`);
6777
7216
  const extraParams = parseExtraParams(opts.param, opts.paramsJson);
6778
7217
  log.step("① 本地预处理(探几何 + 抽音频/720p)…");
6779
7218
  const geo = probeGeometry(inputAbs, opts.ffmpegPath);
6780
7219
  log.info(`原片几何 ${geo.width}x${geo.height} @ ${geo.fps.toFixed(2)}fps · ${geo.duration.toFixed(1)}s`);
6781
7220
  const artifact = opts.visualAssist ? await compress720p(inputAbs, opts.ffmpegPath) : await extractAudio(inputAbs, opts.ffmpegPath);
6782
7221
  assertDurationConsistent(geo.duration, artifact, opts.ffmpegPath);
6783
- log.info(opts.visualAssist ? `已压 720p 代理(上传物):${basename5(artifact)}` : `已抽 16k 单声道 mp3(上传物):${basename5(artifact)}`);
7222
+ log.info(opts.visualAssist ? `已压 720p 代理(上传物):${basename6(artifact)}` : `已抽 16k 单声道 mp3(上传物):${basename6(artifact)}`);
6784
7223
  log.step("② 上传抽出物到云端…");
6785
7224
  const buildPayload = (fid) => {
6786
7225
  const p = {
@@ -6820,7 +7259,7 @@ async function runOralCut(input, opts) {
6820
7259
  const up = { fileId: submitted.fileId, cached: submitted.cached };
6821
7260
  log.info(`task_id = ${taskId}`);
6822
7261
  await mkdir4(outDir, { recursive: true });
6823
- 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));
6824
7263
  log.step("④ 云端处理中(每 5s 轮询)…");
6825
7264
  const result = await pollTask(cfg, TASK_TYPE, taskId, (status, progress) => {
6826
7265
  log.tick(`${status}${progress != null ? ` ${Math.round(progress)}%` : ""}`);
@@ -6844,7 +7283,7 @@ async function runOralCut(input, opts) {
6844
7283
  }
6845
7284
 
6846
7285
  // src/commands/oralcut-result.ts
6847
- import { resolve as resolve5, join as join15 } from "node:path";
7286
+ import { resolve as resolve5, join as join16 } from "node:path";
6848
7287
  var TASK_TYPE2 = "cli/video_oral_cut_for_cli";
6849
7288
  function timestamp2() {
6850
7289
  const d = new Date;
@@ -6878,7 +7317,7 @@ async function runOralCutResult(taskId, opts) {
6878
7317
  const pct = got.progress != null ? ` ${Math.round(got.progress)}%` : "";
6879
7318
  throw new Error(`任务尚未完成(当前 ${got.status || "未知"}${pct}),暂无法取回结果;请稍后再试。`);
6880
7319
  }
6881
- 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()}`));
6882
7321
  const draftDir = resolveJianyingDraftDir(opts.jianyingDraftDir);
6883
7322
  await materializeResult({
6884
7323
  outDir,
@@ -6938,17 +7377,17 @@ function registerUpgrade(program2) {
6938
7377
  }
6939
7378
 
6940
7379
  // src/commands/render.ts
6941
- import { resolve as resolve6, dirname as dirname4, join as join16, basename as basename6, extname as extname3 } from "node:path";
6942
- 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";
6943
7382
  function registerRender(program2) {
6944
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) => {
6945
7384
  if (opts.json)
6946
7385
  routeLogsToStderr();
6947
7386
  const gtrkAbs = resolve6(gtrk);
6948
- if (!existsSync13(gtrkAbs))
7387
+ if (!existsSync14(gtrkAbs))
6949
7388
  throw new Error(`gtrk 工程不存在:${gtrkAbs}`);
6950
- const outMp4 = resolve6(opts.out ?? join16(dirname4(gtrkAbs), `${basename6(gtrkAbs, extname3(gtrkAbs))}.mp4`));
6951
- 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)}`);
6952
7391
  const project = await readGtrkFile(gtrkAbs);
6953
7392
  const result = await renderGtrk(project, outMp4, {
6954
7393
  crf: opts.crf != null ? Number(opts.crf) : undefined,
@@ -6971,132 +7410,14 @@ function registerRender(program2) {
6971
7410
  }
6972
7411
 
6973
7412
  // src/commands/split.ts
6974
- import { resolve as resolve7, join as join18, dirname as dirname6, basename as basename8 } from "node:path";
6975
- import { existsSync as existsSync14 } from "node:fs";
6976
- 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";
6977
7416
  import { createHash } from "node:crypto";
6978
7417
 
6979
- // src/lib/projection.ts
6980
- function r32(n) {
6981
- return Math.round(n * 1000) / 1000;
6982
- }
6983
- function normClip(c3) {
6984
- const clip_st = c3.clip_st ?? 0;
6985
- const track_st = c3.track_st ?? 0;
6986
- const dur = c3.duration ?? (c3.clip_ed != null ? c3.clip_ed - clip_st : 0);
6987
- const clip_ed = c3.clip_ed ?? clip_st + dur;
6988
- return { clip_st, clip_ed, track_st };
6989
- }
6990
- function pickMainVideoTrack(gtrk) {
6991
- const tracks = gtrk.video_track ?? [];
6992
- if (!tracks.length)
6993
- return;
6994
- let best = tracks[0];
6995
- let bestIdx = best.track_index ?? 0;
6996
- for (const t of tracks) {
6997
- const idx = t.track_index ?? 0;
6998
- if (idx < bestIdx) {
6999
- best = t;
7000
- bestIdx = idx;
7001
- }
7002
- }
7003
- return best;
7004
- }
7005
- function projectTranscript(transcript, gtrk, opts = {}) {
7006
- const materialId = String(transcript.material_id);
7007
- const mainTrack = pickMainVideoTrack(gtrk);
7008
- const clips = (mainTrack?.track_timeline ?? []).filter((c3) => c3.material != null && String(c3.material) === materialId).map(normClip);
7009
- const entries = [];
7010
- transcript.utterances.forEach((utt, sourceIndex) => {
7011
- const totalWords = utt.words?.length ?? 0;
7012
- const instances = [];
7013
- for (const clip of clips) {
7014
- const surviving = [];
7015
- for (const word of utt.words ?? []) {
7016
- const s = Math.max(word.st, clip.clip_st);
7017
- const e = Math.min(word.ed, clip.clip_ed);
7018
- if (e > s) {
7019
- surviving.push({
7020
- w: word.w,
7021
- track_st: r32(clip.track_st + (s - clip.clip_st)),
7022
- track_ed: r32(clip.track_st + (e - clip.clip_st))
7023
- });
7024
- }
7025
- }
7026
- if (surviving.length) {
7027
- instances.push({
7028
- track_st: Math.min(...surviving.map((x) => x.track_st)),
7029
- track_ed: Math.max(...surviving.map((x) => x.track_ed)),
7030
- kept_words: surviving.length,
7031
- words: surviving
7032
- });
7033
- }
7034
- }
7035
- if (!instances.length) {
7036
- entries.push({
7037
- id: utt.id,
7038
- text: utt.text,
7039
- dropped: true,
7040
- sourceIndex,
7041
- instIndex: 0,
7042
- track_st: null,
7043
- track_ed: null,
7044
- kept_words: 0,
7045
- total_words: totalWords,
7046
- words: [],
7047
- sortKey: 0
7048
- });
7049
- } else {
7050
- instances.sort((a, b) => a.track_st - b.track_st);
7051
- instances.forEach((inst, instIndex) => {
7052
- entries.push({
7053
- id: utt.id,
7054
- text: utt.text,
7055
- dropped: false,
7056
- sourceIndex,
7057
- instIndex,
7058
- track_st: inst.track_st,
7059
- track_ed: inst.track_ed,
7060
- kept_words: inst.kept_words,
7061
- total_words: totalWords,
7062
- words: inst.words,
7063
- sortKey: inst.track_st
7064
- });
7065
- });
7066
- }
7067
- });
7068
- let maxEd = 0;
7069
- for (const e of entries) {
7070
- if (e.dropped)
7071
- e.sortKey = maxEd;
7072
- else
7073
- maxEd = Math.max(maxEd, e.track_ed ?? maxEd);
7074
- }
7075
- entries.sort((a, b) => a.sortKey - b.sortKey || a.sourceIndex - b.sourceIndex || a.instIndex - b.instIndex);
7076
- const utterances = entries.map((e) => {
7077
- const u = {
7078
- id: e.id,
7079
- text: e.text,
7080
- track_st: e.track_st,
7081
- track_ed: e.track_ed,
7082
- dropped: e.dropped,
7083
- kept_words: e.kept_words,
7084
- total_words: e.total_words
7085
- };
7086
- if (opts.words)
7087
- u.words = e.words;
7088
- return u;
7089
- });
7090
- return {
7091
- transcript_hash: transcript.text_hash,
7092
- projected_at: opts.projectedAt ?? new Date().toISOString(),
7093
- utterances
7094
- };
7095
- }
7096
-
7097
7418
  // src/lib/gtrk-writeback.ts
7098
7419
  import { readFileSync as readFileSync4, statSync, writeFileSync as writeFileSync2, renameSync, unlinkSync } from "node:fs";
7099
- 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";
7100
7421
  import { randomBytes as randomBytes2 } from "node:crypto";
7101
7422
  function readGtrk(path) {
7102
7423
  const raw = readFileSync4(path, "utf8");
@@ -7123,7 +7444,7 @@ function writeStructMetaSplit(path, gtrk, splitObj, expectedMtimeMs) {
7123
7444
  }
7124
7445
  const nextStructMeta = { ...gtrk.struct_meta ?? {}, split: splitObj };
7125
7446
  const next = { ...gtrk, struct_meta: nextStructMeta };
7126
- 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`);
7127
7448
  try {
7128
7449
  writeFileSync2(tmp, JSON.stringify(next, null, 2));
7129
7450
  renameSync(tmp, path);
@@ -7139,7 +7460,7 @@ function writeGtrkAtomic(path, next, expectedMtimeMs) {
7139
7460
  if (cur !== expectedMtimeMs) {
7140
7461
  throw new Error("工程文件在 matrix 运行期间被外部修改(保存冲突),已拒绝写入;请关闭客户端未保存的工程或重跑(plan 与已下载代理均保留)");
7141
7462
  }
7142
- 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`);
7143
7464
  try {
7144
7465
  writeFileSync2(tmp, JSON.stringify(next, null, 2));
7145
7466
  renameSync(tmp, path);
@@ -7159,7 +7480,7 @@ function registerSplit(program2) {
7159
7480
  });
7160
7481
  }
7161
7482
  function firstExisting(cands) {
7162
- return cands.find((p) => existsSync14(p));
7483
+ return cands.find((p) => existsSync15(p));
7163
7484
  }
7164
7485
  function resolvePaths(opts) {
7165
7486
  const project = opts.project ? resolve7(opts.project) : undefined;
@@ -7167,30 +7488,30 @@ function resolvePaths(opts) {
7167
7488
  if (opts.gtrk) {
7168
7489
  gtrkPath = resolve7(opts.gtrk);
7169
7490
  } else if (project) {
7170
- 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");
7171
7492
  } else {
7172
7493
  throw new Error("需 --project <目录> 或显式 --gtrk <path>");
7173
7494
  }
7174
- if (!existsSync14(gtrkPath))
7495
+ if (!existsSync15(gtrkPath))
7175
7496
  throw new Error(`找不到工程文件:${gtrkPath}`);
7176
7497
  let transcriptPath;
7177
7498
  if (opts.transcript)
7178
7499
  transcriptPath = resolve7(opts.transcript);
7179
7500
  else if (project)
7180
7501
  transcriptPath = firstExisting([
7181
- join18(project, "transcript", "transcript.json"),
7182
- join18(project, "json", "transcript.json"),
7183
- join18(project, "transcript.json")
7502
+ join19(project, "transcript", "transcript.json"),
7503
+ join19(project, "json", "transcript.json"),
7504
+ join19(project, "transcript.json")
7184
7505
  ]);
7185
7506
  const baseDir = project ?? dirname6(gtrkPath);
7186
7507
  return { baseDir, gtrkPath, transcriptPath };
7187
7508
  }
7188
- function slugify(name) {
7509
+ function slugify2(name) {
7189
7510
  const s = name.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "-").replace(/^-+|-+$/g, "");
7190
7511
  return s || "project";
7191
7512
  }
7192
7513
  async function loadTranscript(path) {
7193
- const t = JSON.parse(await readFile4(path, "utf8"));
7514
+ const t = JSON.parse(await readFile5(path, "utf8"));
7194
7515
  if (!t || !Array.isArray(t.utterances) || typeof t.material_id !== "string" || typeof t.text_hash !== "string") {
7195
7516
  throw new Error(`transcript.json 结构异常(缺 utterances/material_id/text_hash):${path}`);
7196
7517
  }
@@ -7205,15 +7526,15 @@ async function runSplit(splitdoc, opts) {
7205
7526
  return splitdoc ? runLand(resolve7(splitdoc), baseDir, gtrkPath, transcriptPath, opts) : runView(baseDir, gtrkPath, transcriptPath, opts);
7206
7527
  }
7207
7528
  async function runView(baseDir, gtrkPath, transcriptPath, opts) {
7208
- if (!transcriptPath || !existsSync14(transcriptPath))
7529
+ if (!transcriptPath || !existsSync15(transcriptPath))
7209
7530
  throw new Error(TRANSCRIPT_MISSING);
7210
7531
  log.step("▶ 导出投影视图(transcript × 当刻 .gtrk)…");
7211
7532
  const transcript = await loadTranscript(transcriptPath);
7212
7533
  const { gtrk } = readGtrk(gtrkPath);
7213
7534
  const view = projectTranscript(transcript, gtrk, { words: opts.words });
7214
- const splitDir = join18(baseDir, "split");
7535
+ const splitDir = join19(baseDir, "split");
7215
7536
  await mkdir5(splitDir, { recursive: true });
7216
- const viewPath = join18(splitDir, "view.json");
7537
+ const viewPath = join19(splitDir, "view.json");
7217
7538
  await writeFile6(viewPath, JSON.stringify(view, null, 2));
7218
7539
  const dropped = view.utterances.filter((u) => u.dropped).length;
7219
7540
  log.ok(`投影视图已生成:${viewPath}(${view.utterances.length} 条,其中 ${dropped} 条被剪)`);
@@ -7231,12 +7552,12 @@ async function runView(baseDir, gtrkPath, transcriptPath, opts) {
7231
7552
  return result;
7232
7553
  }
7233
7554
  async function runLand(splitdocPath, baseDir, gtrkPath, transcriptPath, opts) {
7234
- if (!existsSync14(splitdocPath))
7555
+ if (!existsSync15(splitdocPath))
7235
7556
  throw new Error(`找不到拆分稿:${splitdocPath}`);
7236
- if (!transcriptPath || !existsSync14(transcriptPath))
7557
+ if (!transcriptPath || !existsSync15(transcriptPath))
7237
7558
  throw new Error(TRANSCRIPT_MISSING);
7238
7559
  log.step("▶ 校验拆分稿并落地…");
7239
- const doc = JSON.parse(await readFile4(splitdocPath, "utf8"));
7560
+ const doc = JSON.parse(await readFile5(splitdocPath, "utf8"));
7240
7561
  const transcript = await loadTranscript(transcriptPath);
7241
7562
  const { gtrk, mtimeMs } = readGtrk(gtrkPath);
7242
7563
  assertGtrkV1(gtrk);
@@ -7259,7 +7580,7 @@ async function runLand(splitdocPath, baseDir, gtrkPath, transcriptPath, opts) {
7259
7580
  }
7260
7581
  const projectedAt = new Date().toISOString();
7261
7582
  const view = projectTranscript(transcript, gtrk, { projectedAt });
7262
- const projectSlug = slugify(basename8(baseDir));
7583
+ const projectSlug = slugify2(basename9(baseDir));
7263
7584
  const landing = buildLanding(doc, view, {
7264
7585
  utteranceIds: ctx.utteranceIds,
7265
7586
  projectSlug,
@@ -7270,13 +7591,13 @@ async function runLand(splitdocPath, baseDir, gtrkPath, transcriptPath, opts) {
7270
7591
  }
7271
7592
  });
7272
7593
  writeStructMetaSplit(gtrkPath, gtrk, landing.split, mtimeMs);
7273
- const splitDir = join18(baseDir, "split");
7594
+ const splitDir = join19(baseDir, "split");
7274
7595
  await mkdir5(splitDir, { recursive: true });
7275
- const dispatchPath = join18(splitDir, "dispatch.json");
7596
+ const dispatchPath = join19(splitDir, "dispatch.json");
7276
7597
  await writeFile6(dispatchPath, JSON.stringify(landing.dispatch, null, 2));
7277
7598
  let mdPath = null;
7278
7599
  if (opts.md) {
7279
- mdPath = join18(splitDir, "visual-split.md");
7600
+ mdPath = join19(splitDir, "visual-split.md");
7280
7601
  await writeFile6(mdPath, renderSplitMarkdown(doc, landing, { projectSlug, projectedAt }));
7281
7602
  }
7282
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})`);
@@ -7308,18 +7629,165 @@ async function runLand(splitdocPath, baseDir, gtrkPath, transcriptPath, opts) {
7308
7629
  }
7309
7630
 
7310
7631
  // src/commands/matrix.ts
7311
- import { resolve as resolve8, join as join19, dirname as dirname7, basename as basename9 } from "node:path";
7312
- import { existsSync as existsSync15 } from "node:fs";
7313
- import { readFile as readFile5, writeFile as writeFile7, mkdir as mkdir6 } 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";
7635
+
7636
+ // src/lib/solid-png.ts
7637
+ import { deflateSync } from "node:zlib";
7638
+ var BLACK_BED_HEX = "000000";
7639
+ var SOLID_MATERIAL_PREFIX = "ex-solid-";
7640
+ var BUILTIN_ASSET_DIR = "assets/builtin";
7641
+ var MAX_BED_DIM = 8192;
7642
+ var PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
7643
+ var CRC_TABLE = (() => {
7644
+ const table = new Uint32Array(256);
7645
+ for (let n = 0;n < 256; n++) {
7646
+ let c3 = n;
7647
+ for (let k = 0;k < 8; k++)
7648
+ c3 = c3 & 1 ? 3988292384 ^ c3 >>> 1 : c3 >>> 1;
7649
+ table[n] = c3 >>> 0;
7650
+ }
7651
+ return table;
7652
+ })();
7653
+ function crc32(buf) {
7654
+ let c3 = 4294967295;
7655
+ for (let i = 0;i < buf.length; i++)
7656
+ c3 = (CRC_TABLE[(c3 ^ buf[i]) & 255] ^ c3 >>> 8) >>> 0;
7657
+ return (c3 ^ 4294967295) >>> 0;
7658
+ }
7659
+ function chunk(type, data) {
7660
+ const len = Buffer.alloc(4);
7661
+ len.writeUInt32BE(data.length, 0);
7662
+ const body = Buffer.concat([Buffer.from(type, "ascii"), data]);
7663
+ const crc = Buffer.alloc(4);
7664
+ crc.writeUInt32BE(crc32(body), 0);
7665
+ return Buffer.concat([len, body, crc]);
7666
+ }
7667
+ function normalizeHex(hex) {
7668
+ const h = hex.replace(/^#/, "").toLowerCase();
7669
+ return /^[0-9a-f]{6}$/.test(h) ? h : null;
7670
+ }
7671
+ function solidMaterialId({
7672
+ hex,
7673
+ width,
7674
+ height
7675
+ }) {
7676
+ const h = normalizeHex(hex);
7677
+ if (!h)
7678
+ throw new Error(`非法纯色 hex:${hex}`);
7679
+ return `${SOLID_MATERIAL_PREFIX}${h}-${width}x${height}`;
7680
+ }
7681
+ function solidRelPath({
7682
+ hex,
7683
+ width,
7684
+ height
7685
+ }) {
7686
+ const h = normalizeHex(hex);
7687
+ if (!h)
7688
+ throw new Error(`非法纯色 hex:${hex}`);
7689
+ return `${BUILTIN_ASSET_DIR}/solid-${h}-${width}x${height}.png`;
7690
+ }
7691
+ function isLayoutableCanvas(canvas) {
7692
+ if (!Array.isArray(canvas) || canvas.length !== 2)
7693
+ return false;
7694
+ return canvas.every((v) => typeof v === "number" && Number.isInteger(v) && v > 0 && v <= MAX_BED_DIM);
7695
+ }
7696
+ function encodeSolidPng({
7697
+ hex,
7698
+ width,
7699
+ height
7700
+ }) {
7701
+ const h = normalizeHex(hex);
7702
+ if (!h)
7703
+ throw new Error(`非法纯色 hex:${hex}`);
7704
+ if (!isLayoutableCanvas([width, height])) {
7705
+ throw new Error(`纯色画布尺寸非法或超上限(${width}x${height},上限 ${MAX_BED_DIM})`);
7706
+ }
7707
+ const r = Number.parseInt(h.slice(0, 2), 16);
7708
+ const g2 = Number.parseInt(h.slice(2, 4), 16);
7709
+ const b = Number.parseInt(h.slice(4, 6), 16);
7710
+ const ihdr = Buffer.alloc(13);
7711
+ ihdr.writeUInt32BE(width, 0);
7712
+ ihdr.writeUInt32BE(height, 4);
7713
+ ihdr[8] = 8;
7714
+ ihdr[9] = 2;
7715
+ ihdr[10] = 0;
7716
+ ihdr[11] = 0;
7717
+ ihdr[12] = 0;
7718
+ const stride = 1 + width * 3;
7719
+ const row = Buffer.alloc(stride);
7720
+ row[0] = 0;
7721
+ for (let x = 0;x < width; x++) {
7722
+ const off = 1 + x * 3;
7723
+ row[off] = r;
7724
+ row[off + 1] = g2;
7725
+ row[off + 2] = b;
7726
+ }
7727
+ const raw = Buffer.alloc(stride * height);
7728
+ for (let y = 0;y < height; y++)
7729
+ row.copy(raw, y * stride);
7730
+ return Buffer.concat([
7731
+ PNG_SIGNATURE,
7732
+ chunk("IHDR", ihdr),
7733
+ chunk("IDAT", deflateSync(raw, { level: 9 })),
7734
+ chunk("IEND", Buffer.alloc(0))
7735
+ ]);
7736
+ }
7314
7737
 
7315
7738
  // src/lib/matrix-lay.ts
7316
7739
  var BROLL_PREVIEW_DIR = "assets/broll-preview";
7317
7740
  var BROLL_MATERIAL_PREFIX = "broll-";
7741
+ var BROLL_RAW_MATERIAL_PREFIX = "broll-raw-";
7318
7742
  var BROLL_META_CANDIDATE_CAP = 12;
7319
7743
  var SHOT_TARGET_DEFAULT = 3;
7320
7744
  var MIN_SHOT_SEC = 1.2;
7321
7745
  var SCORE_FLOOR_DEFAULT = 0.2;
7322
7746
  var MAX_SLOTS_PER_BEAT = 32;
7747
+ var BLACK_BED_MERGE_EPS = 0.001;
7748
+ function mergeBlackBedSegments(envelopes) {
7749
+ const valid = envelopes.filter((e) => Number.isFinite(e.track_st) && Number.isFinite(e.track_ed) && e.track_ed > e.track_st).sort((a, b) => a.track_st - b.track_st);
7750
+ const out = [];
7751
+ for (const e of valid) {
7752
+ const last = out[out.length - 1];
7753
+ if (last && e.track_st - last.track_ed <= BLACK_BED_MERGE_EPS) {
7754
+ if (e.track_ed > last.track_ed)
7755
+ last.track_ed = e.track_ed;
7756
+ continue;
7757
+ }
7758
+ out.push({ track_st: e.track_st, track_ed: e.track_ed });
7759
+ }
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)) };
7790
+ }
7323
7791
  function mergedCandidates(beat) {
7324
7792
  const all = [];
7325
7793
  for (const q of beat.queries)
@@ -7347,7 +7815,7 @@ function previewDims(width, height) {
7347
7815
  const h = Math.max(2, Math.round(height * 640 / width / 2) * 2);
7348
7816
  return [640, h];
7349
7817
  }
7350
- var r33 = (n) => Math.round(n * 1000) / 1000;
7818
+ var r34 = (n) => Math.round(n * 1000) / 1000;
7351
7819
  function buildQueryPools(beat, scoreFloor) {
7352
7820
  const out = [];
7353
7821
  for (const q of beat.queries) {
@@ -7450,10 +7918,10 @@ function fillBeatTrack(opts) {
7450
7918
  clip_id: pick.cand.clip_id,
7451
7919
  query: pick.query,
7452
7920
  score: pick.seg.score,
7453
- clip_st: r33(clipSt),
7454
- clip_ed: r33(clipSt + d),
7455
- track_st: r33(cursor),
7456
- 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)
7457
7925
  });
7458
7926
  consumed.add(pick.key);
7459
7927
  prevClip = pick.cand.clip_id;
@@ -7468,47 +7936,241 @@ function fillBeatTrack(opts) {
7468
7936
  const hi = dur ?? lastPick.seg.end;
7469
7937
  const ext = Math.min(tail, Math.max(0, hi - last.clip_ed));
7470
7938
  if (ext > 0.000001) {
7471
- last.clip_ed = r33(last.clip_ed + ext);
7472
- 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);
7473
7941
  }
7474
7942
  }
7475
7943
  }
7476
- return slots;
7944
+ return slots;
7945
+ }
7946
+ function planBeatFills(plan, lay, scoreFloor) {
7947
+ const fills = new Map;
7948
+ const clipIds = new Set;
7949
+ const consumed = new Set;
7950
+ for (const beat of plan.beats) {
7951
+ const perTrack = [];
7952
+ for (let k = 0;k < Math.max(0, lay); k++) {
7953
+ const slots = fillBeatTrack({ beat, trackOrder: k, consumed, scoreFloor });
7954
+ perTrack.push(slots);
7955
+ for (const s of slots)
7956
+ clipIds.add(s.clip_id);
7957
+ }
7958
+ fills.set(beat.beat, perTrack);
7959
+ }
7960
+ return { fills, clipIds };
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
+ };
7477
8056
  }
7478
- function planBeatFills(plan, lay, scoreFloor) {
7479
- const fills = new Map;
7480
- const clipIds = new Set;
7481
- const consumed = new Set;
7482
- for (const beat of plan.beats) {
7483
- const perTrack = [];
7484
- for (let k = 0;k < Math.max(0, lay); k++) {
7485
- const slots = fillBeatTrack({ beat, trackOrder: k, consumed, scoreFloor });
7486
- perTrack.push(slots);
7487
- for (const s of slots)
7488
- clipIds.add(s.clip_id);
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);
7489
8080
  }
7490
- fills.set(beat.beat, perTrack);
7491
8081
  }
7492
- return { fills, clipIds };
8082
+ return tracks.map((t, i) => classifyTrack(t, expected, { layTracks, registered, matched: claimed.get(i) ?? null }));
7493
8083
  }
7494
8084
  function layBrollTracks(opts) {
7495
8085
  const { gtrk, plan, lay, fills, downloads } = opts;
8086
+ const blackBedOn = opts.blackBed !== false;
8087
+ const forceRelay = opts.forceRelay === true;
8088
+ const warnings = [];
7496
8089
  const videoTracks = [...gtrk.video_track ?? []];
7497
8090
  const materials = [...gtrk.materials ?? []];
7498
8091
  const structMeta = { ...gtrk.struct_meta ?? {} };
7499
8092
  const prevBroll = structMeta.broll;
7500
8093
  const prevIndices = new Set(Array.isArray(prevBroll?.lay_tracks) ? prevBroll.lay_tracks.filter((x) => typeof x === "number") : []);
7501
- const removedTracks = videoTracks.filter((t) => typeof t.track_index === "number" && prevIndices.has(t.track_index));
7502
- 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
+ }
7503
8139
  const removedMaterialIds = new Set;
7504
8140
  for (const t of removedTracks) {
7505
8141
  for (const c3 of t.track_timeline ?? []) {
7506
8142
  const m = c3.material;
7507
- if (typeof m === "string" && m.startsWith(BROLL_MATERIAL_PREFIX))
8143
+ if (typeof m === "string" && (m.startsWith(BROLL_MATERIAL_PREFIX) || m.startsWith(SOLID_MATERIAL_PREFIX))) {
7508
8144
  removedMaterialIds.add(m);
8145
+ }
8146
+ }
8147
+ }
8148
+ const stillReferenced = new Set;
8149
+ for (const group of [keptTracks, gtrk.beat_track ?? [], gtrk.audio_track ?? []]) {
8150
+ for (const t of group) {
8151
+ for (const c3 of t.track_timeline ?? []) {
8152
+ if (typeof c3.material === "string")
8153
+ stillReferenced.add(c3.material);
8154
+ }
7509
8155
  }
7510
8156
  }
8157
+ for (const id of stillReferenced)
8158
+ removedMaterialIds.delete(id);
7511
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
+ }
8166
+ for (const t of keptTracks) {
8167
+ const clips = t.track_timeline ?? [];
8168
+ if (!clips.length)
8169
+ continue;
8170
+ if (clips.every((c3) => typeof c3.material === "string" && c3.material.startsWith(SOLID_MATERIAL_PREFIX))) {
8171
+ warnings.push(`检测到疑似手工纯色底轨(track_index=${t.track_index}):新候选轨会落到它之下被遮住。建议删除该轨后重跑,或用 --no-black-bed。`);
8172
+ }
8173
+ }
7512
8174
  const canvas = Array.isArray(gtrk.video_size) ? gtrk.video_size : [1920, 1080];
7513
8175
  const baseIndex = keptTracks.reduce((mx, t) => Math.max(mx, typeof t.track_index === "number" ? t.track_index : 0), -1) + 1;
7514
8176
  const candById = new Map;
@@ -7554,7 +8216,7 @@ function layBrollTracks(opts) {
7554
8216
  clip_ed: s.clip_ed,
7555
8217
  track_st: s.track_st,
7556
8218
  track_ed: s.track_ed,
7557
- duration: r33(s.track_ed - s.track_st)
8219
+ duration: r34(s.track_ed - s.track_st)
7558
8220
  });
7559
8221
  laidClips++;
7560
8222
  });
@@ -7591,26 +8253,287 @@ function layBrollTracks(opts) {
7591
8253
  muted: false,
7592
8254
  track_timeline: clips.sort((a, b) => a.track_st - b.track_st)
7593
8255
  }));
8256
+ let blackTrack = null;
8257
+ let blackTrackObj = null;
8258
+ if (blackBedOn && createdTracks.length > 0) {
8259
+ if (!isLayoutableCanvas([canvas[0], canvas[1]])) {
8260
+ warnings.push(`画布尺寸非法(${canvas[0]}x${canvas[1]}),跳过铺纯黑底轨(候选轨照常铺)。`);
8261
+ } else {
8262
+ const laidBeatEnvelopes = metaBeats.filter((b) => b.laid.length > 0).map((b) => ({ track_st: b.track_st, track_ed: b.track_ed }));
8263
+ const segments = mergeBlackBedSegments(laidBeatEnvelopes);
8264
+ if (segments.length > 0) {
8265
+ const width = canvas[0];
8266
+ const height = canvas[1];
8267
+ const solidId = solidMaterialId({ hex: BLACK_BED_HEX, width, height });
8268
+ newMaterialsById.set(solidId, {
8269
+ id: solidId,
8270
+ path: solidRelPath({ hex: BLACK_BED_HEX, width, height }),
8271
+ video_size: [width, height]
8272
+ });
8273
+ blackTrack = baseIndex + lay;
8274
+ blackTrackObj = {
8275
+ track_index: blackTrack,
8276
+ track_size: [width, height],
8277
+ muted: false,
8278
+ track_timeline: segments.map((s, i) => ({
8279
+ clip_id: `blackbed-${i}`,
8280
+ material: solidId,
8281
+ clip_st: 0,
8282
+ clip_ed: r34(s.track_ed - s.track_st),
8283
+ track_st: s.track_st,
8284
+ track_ed: s.track_ed,
8285
+ duration: r34(s.track_ed - s.track_st)
8286
+ }))
8287
+ };
8288
+ }
8289
+ }
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
+ }
8326
+ const laidTrackIndices = createdTracks.map((t) => t.track_index);
7594
8327
  const broll = {
7595
8328
  contract_version: "v1",
7596
8329
  generated_at: opts.generatedAt,
7597
8330
  plan_path: opts.planPath,
7598
- lay_tracks: createdTracks.map((t) => t.track_index),
8331
+ lay_tracks: blackTrack === null ? laidTrackIndices : [...laidTrackIndices, blackTrack],
8332
+ black_track: blackTrack,
7599
8333
  confirmed: false,
7600
8334
  beats: metaBeats
7601
8335
  };
8336
+ const mergedMaterials = new Map;
8337
+ for (const m of keptMaterials) {
8338
+ if (typeof m.id === "string")
8339
+ mergedMaterials.set(m.id, m);
8340
+ else
8341
+ mergedMaterials.set(`__anon_${mergedMaterials.size}`, m);
8342
+ }
8343
+ for (const [id, m] of newMaterialsById)
8344
+ mergedMaterials.set(id, m);
7602
8345
  const next = {
7603
8346
  ...gtrk,
7604
- materials: [...keptMaterials, ...newMaterialsById.values()],
7605
- video_track: [...keptTracks, ...createdTracks],
8347
+ materials: [...mergedMaterials.values()],
8348
+ video_track: [...keptTracks, ...createdTracks, ...blackTrackObj ? [blackTrackObj] : []],
7606
8349
  struct_meta: { ...structMeta, broll }
7607
8350
  };
7608
8351
  return {
7609
8352
  next,
7610
- summary: { laidTracks: broll.lay_tracks, laidClips, beatsWithCandidates },
7611
- broll
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
+ },
8364
+ broll,
8365
+ warnings
8366
+ };
8367
+ }
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 } } : {}
7612
8489
  };
7613
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
+ }
7614
8537
 
7615
8538
  // src/lib/matrix.ts
7616
8539
  var URL_TTL_NOTE = "结果 url 带签名默认 24h 过期;过期后重跑 gtrk matrix 即重签(plan 幂等重生成)。";
@@ -7799,7 +8722,7 @@ async function searchOnce(cfg, tier, body) {
7799
8722
 
7800
8723
  // src/commands/matrix.ts
7801
8724
  function registerMatrix(program2) {
7802
- 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("--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) => {
7803
8726
  await runMatrix(parseAdhocQuery(words), opts);
7804
8727
  });
7805
8728
  }
@@ -7847,18 +8770,40 @@ async function runPlanMode(cfg, tier, broll, overrides, columnId, opts) {
7847
8770
  let dispatchPath;
7848
8771
  let baseDir;
7849
8772
  if (opts.dispatch) {
7850
- dispatchPath = resolve8(opts.dispatch);
8773
+ dispatchPath = resolve9(opts.dispatch);
7851
8774
  baseDir = dirname7(dirname7(dispatchPath));
7852
8775
  } else if (opts.project) {
7853
- baseDir = resolve8(opts.project);
7854
- dispatchPath = join19(baseDir, "split", "dispatch.json");
8776
+ baseDir = resolve9(opts.project);
8777
+ dispatchPath = join20(baseDir, "split", "dispatch.json");
7855
8778
  } else {
7856
8779
  throw new Error('需 --project <目录> 或显式 --dispatch <path>(ad-hoc 检索用:gtrk matrix search "<query>")');
7857
8780
  }
7858
- if (!existsSync15(dispatchPath))
8781
+ if (!existsSync17(dispatchPath))
7859
8782
  throw new Error(`找不到派单清单:${dispatchPath}(先跑 gtrk split <拆分稿> 落地派单)`);
7860
- const dispatch = JSON.parse(await readFile5(dispatchPath, "utf8"));
7861
- 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
+ });
7862
8807
  log.step(`▶ B-roll 检索:${queue.length} 个 beat(${tier} 口)…`);
7863
8808
  const beats = [];
7864
8809
  let okCount = 0;
@@ -7885,12 +8830,12 @@ async function runPlanMode(cfg, tier, broll, overrides, columnId, opts) {
7885
8830
  beats.push(buildPlanBeat(entry, outcomes));
7886
8831
  }
7887
8832
  const totalQueries = okCount + errCount;
7888
- if (queue.length === 0)
8833
+ if (rawQueue.length === 0)
7889
8834
  log.warn("无 B-roll 派单(film_broll 队列为空)——照常写出空 plan");
7890
8835
  if (totalQueries > 0 && okCount === 0) {
7891
8836
  throw new Error(`全部 ${totalQueries} 个 query 检索失败,未写入 plan(逐条原因见上方日志)`);
7892
8837
  }
7893
- const projectSlug = slugify2(basename9(baseDir));
8838
+ const projectSlug = slugify3(basename10(baseDir));
7894
8839
  const plan = buildPlan({
7895
8840
  generatedAt: new Date().toISOString(),
7896
8841
  memberType: tier,
@@ -7898,26 +8843,33 @@ async function runPlanMode(cfg, tier, broll, overrides, columnId, opts) {
7898
8843
  columnId,
7899
8844
  beats
7900
8845
  });
7901
- const splitDir = join19(baseDir, "split");
8846
+ const splitDir = join20(baseDir, "split");
7902
8847
  await mkdir6(splitDir, { recursive: true });
7903
- const planPath = join19(splitDir, "broll-plan.json");
8848
+ const planPath = join20(splitDir, "broll-plan.json");
7904
8849
  await writeFile7(planPath, JSON.stringify(plan, null, 2));
7905
8850
  log.ok(`候选清单已生成:${planPath}(${beats.length} beat · ${okCount}/${totalQueries} query 成功 · ${resultCount} 条候选)`);
7906
8851
  log.info("清单只含引用不含素材:cover_url 可直接预览;url 带签名默认 24h 过期,过期重跑本命令即重签。");
7907
8852
  const layN = parseLay(opts.lay);
7908
- let laySummary;
8853
+ let laid;
7909
8854
  if (layN > 0) {
7910
- laySummary = await layIntoProject(baseDir, plan, layN, parseScoreFloor(opts.scoreFloor));
8855
+ laid = await layIntoProject(baseDir, plan, layN, parseScoreFloor(opts.scoreFloor), opts.blackBed ?? true, opts.forceRelay === true, reproj);
7911
8856
  }
8857
+ const laySummary = laid?.lay;
8858
+ const refused = laySummary?.refused === true ? laySummary.keptEditedTracks : undefined;
7912
8859
  const result = {
7913
- ok: true,
8860
+ ok: refused === undefined,
7914
8861
  mode: "plan",
7915
8862
  memberType: tier,
7916
8863
  ...columnId ? { columnId } : {},
7917
8864
  planPath,
8865
+ ...refused ? { refused, reason: "tracks_edited", planReusable: true } : {},
7918
8866
  ...laySummary ? { lay: laySummary } : {},
8867
+ ...laid?.integrity ? { integrity: laid.integrity } : {},
8868
+ reprojection: reproj.summary,
7919
8869
  counts: { beats: beats.length, queries: totalQueries, results: resultCount, errors: errCount }
7920
8870
  };
8871
+ if (!result.ok)
8872
+ process.exitCode = 1;
7921
8873
  if (opts.json)
7922
8874
  console.log(JSON.stringify(result));
7923
8875
  return result;
@@ -7941,13 +8893,13 @@ function parseScoreFloor(raw) {
7941
8893
  return SCORE_FLOOR_DEFAULT;
7942
8894
  }
7943
8895
  function locateGtrk(baseDir) {
7944
- const cands = [join19(baseDir, "gtrk", "project.gtrk"), join19(baseDir, "project.gtrk")];
7945
- 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));
7946
8898
  }
7947
- async function layIntoProject(baseDir, plan, layN, scoreFloor) {
8899
+ async function layIntoProject(baseDir, plan, layN, scoreFloor, blackBed, forceRelay, reproj) {
7948
8900
  const gtrkPath = locateGtrk(baseDir);
7949
8901
  if (!gtrkPath) {
7950
- log.warn(`未找到工程文件(${join19(baseDir, "gtrk", "project.gtrk")}),跳过铺轨——plan 已产出,可后续在有工程的目录重跑`);
8902
+ log.warn(`未找到工程文件(${join20(baseDir, "gtrk", "project.gtrk")}),跳过铺轨——plan 已产出,可后续在有工程的目录重跑`);
7951
8903
  return;
7952
8904
  }
7953
8905
  const { gtrk, mtimeMs } = readGtrk(gtrkPath);
@@ -7956,7 +8908,7 @@ async function layIntoProject(baseDir, plan, layN, scoreFloor) {
7956
8908
  const slotCount = [...fills.values()].flat().reduce((n, s) => n + s.length, 0);
7957
8909
  log.step(`▶ 候选铺轨(${layN} 轨 · 平铺 ${slotCount} 槽位 · ${clipIds.size} 个 clip,preview 代理)…`);
7958
8910
  const gtrkDir = dirname7(gtrkPath);
7959
- const previewDir = join19(gtrkDir, ...BROLL_PREVIEW_DIR.split("/"));
8911
+ const previewDir = join20(gtrkDir, ...BROLL_PREVIEW_DIR.split("/"));
7960
8912
  await mkdir6(previewDir, { recursive: true });
7961
8913
  const prevSource = new Map;
7962
8914
  const prevBroll = gtrk.struct_meta?.broll;
@@ -7979,8 +8931,8 @@ async function layIntoProject(baseDir, plan, layN, scoreFloor) {
7979
8931
  if (!cand)
7980
8932
  continue;
7981
8933
  const rel = `${BROLL_PREVIEW_DIR}/${clipId}.mp4`;
7982
- const abs = join19(gtrkDir, ...rel.split("/"));
7983
- if (existsSync15(abs)) {
8934
+ const abs = join20(gtrkDir, ...rel.split("/"));
8935
+ if (existsSync17(abs)) {
7984
8936
  const prev = prevSource.get(clipId);
7985
8937
  if (prev !== "raw") {
7986
8938
  downloads.set(clipId, { rel, source: prev ?? "preview" });
@@ -8006,22 +8958,94 @@ async function layIntoProject(baseDir, plan, layN, scoreFloor) {
8006
8958
  dlStats.failed++;
8007
8959
  }
8008
8960
  }
8009
- const { next, summary } = layBrollTracks({
8961
+ let { next, summary, warnings } = layBrollTracks({
8010
8962
  gtrk,
8011
8963
  plan,
8012
8964
  lay: layN,
8013
8965
  fills,
8014
8966
  downloads,
8015
8967
  generatedAt: new Date().toISOString(),
8016
- planPath: "split/broll-plan.json"
8968
+ planPath: "split/broll-plan.json",
8969
+ blackBed,
8970
+ forceRelay
8017
8971
  });
8018
- writeGtrkAtomic(gtrkPath, next, mtimeMs);
8019
- 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}` : ""})`);
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
+ }
8993
+ if (summary.blackTrack !== null) {
8994
+ const canvas = gtrk.video_size;
8995
+ const spec = { hex: BLACK_BED_HEX, width: canvas[0], height: canvas[1] };
8996
+ const rel = solidRelPath(spec);
8997
+ const abs = join20(gtrkDir, ...rel.split("/"));
8998
+ try {
8999
+ if (!existsSync17(abs)) {
9000
+ await mkdir6(dirname7(abs), { recursive: true });
9001
+ const tmp = `${abs}.tmp-${process.pid}`;
9002
+ await writeFile7(tmp, encodeSolidPng(spec));
9003
+ await rename(tmp, abs);
9004
+ }
9005
+ } catch (e) {
9006
+ log.warn(`纯黑底 PNG 落盘失败(${rel}):${e.message} —— 本次不铺黑底垫轨,候选轨照常。`);
9007
+ ({ next, summary, warnings } = layBrollTracks({
9008
+ gtrk,
9009
+ plan,
9010
+ lay: layN,
9011
+ fills,
9012
+ downloads,
9013
+ generatedAt: new Date().toISOString(),
9014
+ planPath: "split/broll-plan.json",
9015
+ blackBed: false,
9016
+ forceRelay
9017
+ }));
9018
+ }
9019
+ }
9020
+ const written = withTimecodeSource(next, "broll", reproj);
9021
+ writeGtrkAtomic(gtrkPath, written, mtimeMs);
9022
+ const integrity = safeCheckMaterialIntegrity({ gtrk: written, gtrkDir, log });
9023
+ const bedNote = summary.blackTrack !== null ? ` · 纯黑底垫轨 track_index ${summary.blackTrack}` : blackBed ? " · 未铺纯黑底垫轨" : " · 纯黑底垫轨已关闭(--no-black-bed)";
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}`);
8020
9027
  log.info("opencut 打开工程即见候选轨:轨道头小眼睛可开关对比;确认下载原片属挑选 UI(E-P1)。");
9028
+ for (const w of warnings)
9029
+ log.warn(w);
8021
9030
  if (dlStats.raw > 0) {
8022
9031
  log.warn("部分候选无 preview 代理已回落原片(体积较大)——服务端 backfill 后重跑本命令可换回代理。");
8023
9032
  }
8024
- return { laidTracks: summary.laidTracks, laidClips: summary.laidClips, downloads: dlStats };
9033
+ if (integrity)
9034
+ reportMaterialIntegrity(integrity, log);
9035
+ return {
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 } : {}
9048
+ };
8025
9049
  }
8026
9050
  async function downloadProxy(cand, absPath, opts = {}) {
8027
9051
  const tryFetch = async (url) => {
@@ -8068,7 +9092,7 @@ async function runAdhoc(query, cfg, tier, broll, overrides, columnId, opts) {
8068
9092
  counts: { beats: 0, queries: 1, results: results.length, errors: 0 }
8069
9093
  };
8070
9094
  if (opts.out) {
8071
- const outPath = resolve8(opts.out);
9095
+ const outPath = resolve9(opts.out);
8072
9096
  await writeFile7(outPath, JSON.stringify({ query, recalled: data.recalled, results }, null, 2));
8073
9097
  log.ok(`结果已落盘:${outPath}`);
8074
9098
  result.outPath = outPath;
@@ -8082,15 +9106,15 @@ async function runAdhoc(query, cfg, tier, broll, overrides, columnId, opts) {
8082
9106
  console.log(JSON.stringify(result));
8083
9107
  return result;
8084
9108
  }
8085
- function slugify2(name) {
9109
+ function slugify3(name) {
8086
9110
  const s = name.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "-").replace(/^-+|-+$/g, "");
8087
9111
  return s || "project";
8088
9112
  }
8089
9113
 
8090
9114
  // src/commands/mg.ts
8091
- import { resolve as resolve9, join as join20, dirname as dirname8, basename as basename10 } from "node:path";
8092
- import { existsSync as existsSync16 } from "node:fs";
8093
- 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";
8094
9118
 
8095
9119
  // src/lib/mg-lint.ts
8096
9120
  var CDN_OK = [/lib\.baomitu\.com/i, /cdnjs\.cloudflare\.com/i, /unpkg\.com/i];
@@ -8103,16 +9127,68 @@ function attr(tag, name) {
8103
9127
  const m = tag.match(new RegExp(`\\b${name}\\s*=\\s*["']([^"']*)["']`, "i"));
8104
9128
  return m ? m[1] : undefined;
8105
9129
  }
8106
- 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) {
8107
9136
  if (!rootTagStr)
8108
- return { opaque: false, declared: false };
8109
- 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) {
8110
9173
  const bg = style.match(/background(?:-color)?\s*:\s*([^;"']+)/i);
8111
9174
  if (!bg)
8112
- return { opaque: false, declared: false };
9175
+ return { declared: false, opaque: false };
8113
9176
  const val = bg[1].trim().toLowerCase();
8114
9177
  const transparent = val === "transparent" || val === "none" || /rgba\([^)]*,\s*0\s*\)/.test(val);
8115
- 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
+ };
8116
9192
  }
8117
9193
  var CATEGORY_EXPECTED_OPAQUE2 = {
8118
9194
  overlay: false,
@@ -8124,6 +9200,179 @@ var CATEGORY_EXPECTED_OPAQUE2 = {
8124
9200
  "explain-subtitle": false,
8125
9201
  "op-ed-title": true
8126
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
+ }
9214
+ function estimateTimelineSec(html) {
9215
+ const re = /(?:([A-Za-z_$][\w$]*)\s*)?\.\s*(?:to|from|fromTo|set|add)\s*\(([\s\S]*?)\)\s*;/g;
9216
+ const calls = [];
9217
+ let m;
9218
+ while (m = re.exec(html)) {
9219
+ if (m[1] === "gsap")
9220
+ continue;
9221
+ const args = m[2] ?? "";
9222
+ const lastBrace = args.lastIndexOf("}");
9223
+ let pos = null;
9224
+ if (lastBrace >= 0) {
9225
+ const tail = args.slice(lastBrace + 1).replace(/^\s*,\s*/, "").trim();
9226
+ if (tail)
9227
+ pos = tail.replace(/^["']|["']$/g, "");
9228
+ }
9229
+ calls.push({ body: args, pos });
9230
+ }
9231
+ let chain = 0;
9232
+ let maxEnd = 0;
9233
+ let parsed = 0;
9234
+ let skipped = 0;
9235
+ let hasInfiniteRepeat = false;
9236
+ for (const c3 of calls) {
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
+ function bodyWritesDom(html, body, hops, seen) {
9322
+ if (DOM_WRITE.test(body))
9323
+ return true;
9324
+ if (hops <= 0)
9325
+ return false;
9326
+ for (const m of body.matchAll(/\b([A-Za-z_$][\w$]*)\s*\(/g)) {
9327
+ const name = m[1];
9328
+ if (NOT_A_CALL.has(name) || seen.has(name))
9329
+ continue;
9330
+ seen.add(name);
9331
+ const b = namedFnBody(html, name);
9332
+ if (b && bodyWritesDom(html, b, hops - 1, seen))
9333
+ return true;
9334
+ }
9335
+ return false;
9336
+ }
9337
+ function detectSeekSignals(html) {
9338
+ const hooks = new Set;
9339
+ const hookRe = new RegExp(`\\b(${TL_HOOKS.join("|")})\\s*:\\s*`, "g");
9340
+ let hm;
9341
+ while (hm = hookRe.exec(html)) {
9342
+ const hook = hm[1];
9343
+ const at = hm.index + hm[0].length;
9344
+ const rest = html.slice(at);
9345
+ let body = null;
9346
+ let mm;
9347
+ if (mm = /^function\s*[A-Za-z_$]?[\w$]*\s*\([^)]*\)\s*\{/.exec(rest)) {
9348
+ body = braceBlock(html, at + mm[0].length - 1);
9349
+ } else if (mm = /^(?:\([^)]*\)|[A-Za-z_$][\w$]*)\s*=>\s*/.exec(rest)) {
9350
+ const after = at + mm[0].length;
9351
+ body = html[after] === "{" ? braceBlock(html, after) : rest.slice(mm[0].length).split(`
9352
+ `)[0];
9353
+ } else if (mm = /^([A-Za-z_$][\w$]*)/.exec(rest)) {
9354
+ body = namedFnBody(html, mm[1]);
9355
+ }
9356
+ if (body && bodyWritesDom(html, body, 2, new Set))
9357
+ hooks.add(hook);
9358
+ }
9359
+ const overrideForms = [];
9360
+ if (/\.\s*seek\s*=(?!=)|\[\s*["']seek["']\s*\]\s*=(?!=)/.test(html))
9361
+ overrideForms.push("重新赋值 `.seek`");
9362
+ for (const rm of html.matchAll(/window\s*\.\s*__timelines\s*\[[^\]]*\]\s*=\s*([^;\n]*)/g)) {
9363
+ const rhs = rm[1].split("//")[0].trim();
9364
+ if (rhs && !/^[A-Za-z_$][\w$]*$/.test(rhs)) {
9365
+ overrideForms.push("`__timelines[…]` 被赋成包装对象而非时间线本体");
9366
+ break;
9367
+ }
9368
+ }
9369
+ return {
9370
+ callbackDom: hooks.size > 0,
9371
+ hooks: TL_HOOKS.filter((h) => hooks.has(h)),
9372
+ engineApiOverride: overrideForms.length > 0,
9373
+ overrideForms
9374
+ };
9375
+ }
8127
9376
  function lintParticle(html, opts = {}) {
8128
9377
  const v = [];
8129
9378
  const push = (law, fatal, msg) => v.push({ law, fatal, msg });
@@ -8133,6 +9382,8 @@ function lintParticle(html, opts = {}) {
8133
9382
  const cid = root ? attr(root, "data-composition-id") : undefined;
8134
9383
  if (!root || !cid)
8135
9384
  push("1-composition-id", true, "根元素缺 data-composition-id");
9385
+ if (opts.compositionId && cid && opts.compositionId !== cid)
9386
+ push("1-cid-expect", true, `HTML 内 data-composition-id「${cid}」与期望 id「${opts.compositionId}」不符(复制改名漏改内部 id?)——` + `按期望 id 落轨会写出 clip_id/material 指向「${opts.compositionId}」,而本文件注册的是 __timelines["${cid}"],渲染必错`);
8136
9387
  if (root) {
8137
9388
  if (attr(root, "data-width") !== "1920")
8138
9389
  push("1-width", true, `根 data-width 应为 "1920"(实为 ${attr(root, "data-width") ?? "缺"})`);
@@ -8180,19 +9431,41 @@ function lintParticle(html, opts = {}) {
8180
9431
  if (re.test(html))
8181
9432
  push("4-rel-asset", true, `含相对外链 ${tag}(违反自包含,渲染机读不到)`);
8182
9433
  }
8183
- const { opaque, declared } = deriveOpaque(root);
9434
+ const { opaque, declared, solidOnRoot, solidOnChild } = deriveOpaque(root, firstChildTag(html, root));
8184
9435
  if (root && !declared)
8185
- push("4-bg-explicit", false, "根未显式声明 background(透明与否应明确;缺省按透明叠加 opaque=false 处理)");
9436
+ push("4-bg-explicit", false, "根与根下首个全幅子层**都**没有显式声明 background(契约铁律4③:透明与否 MUST 显式)——" + "满屏颗粒应在根下第一个全幅子层(position:absolute;inset:0)声明实心底;" + "透明叠加颗粒应在根显式写 background:transparent。缺省按透明叠加 opaque=false 处理");
9437
+ if (solidOnRoot && !solidOnChild)
9438
+ 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');
8186
9439
  if (/var\(\s*--/.test(html))
8187
9440
  push("6-css-var", true, "含 CSS var(--...)(Hyperframes 不解析→整片全黑,须字面值)");
8188
- const effectiveCid = opts.compositionId ?? cid;
8189
- if (opts.dispatchIds && effectiveCid && !opts.dispatchIds.includes(effectiveCid))
8190
- push("x-dispatch", false, `composition_id "${effectiveCid}" 不在 dispatch.mg 派单中`);
9441
+ const sig = detectSeekSignals(html);
9442
+ if (sig.engineApiOverride)
9443
+ push("x-engine-api-override", false, `颗粒在运行时覆写了渲染引擎所调用的 API(${sig.overrideForms.join("、")})——它会推翻引擎**显式**传入的 ` + `seek(t, true)(颗粒无权静默否决引擎意图),且只在引擎恰好走 seek 时有效,引擎改走 time() / progress() 即完全失效。` + `按契约「回调与 seek 语义」,回调可达性应由**引擎侧**承担,颗粒侧垫片属过渡态——` + `引擎侧结论已于 2026-07-26 经真机核实(引擎 producer 0.6.101 定帧时回调可达),既有垫片可择期清理(删后须重渲复验),新颗粒不应再加`);
9444
+ if (sig.callbackDom && !sig.engineApiOverride)
9445
+ push("x-callback-driven", false, `画面靠时间线回调驱动(${sig.hooks.join(" / ")} 体内写 DOM),且本颗粒无任何 seek 兜底。` + `该写法**合规**——其可达性由契约「回调与 seek 语义」的**引擎侧 MUST 条款**保证` + `(2026-07-26 已真机核实:引擎 producer 0.6.101 定帧时回调可达;结论**绑定该引擎版本**),` + `作者不必也不应为此加装 tl.seek 垫片。本项只是**哨兵**:引擎若失守或换实现,补间属性照常插值、回调不跑 → ` + `画面会静默定格在初始态(不是黑屏,本地播放器与客户端预览都看不出)`);
9446
+ const timers = [
9447
+ /\brequestAnimationFrame\s*\(/.test(html) ? "requestAnimationFrame(" : null,
9448
+ /\bsetInterval\s*\(/.test(html) ? "setInterval(" : null
9449
+ ].filter(Boolean);
9450
+ if (timers.length)
9451
+ push("x-raf-interval", false, `含 ${timers.join(" 与 ")}——这类自有时钟**不被 seek 驱动**,逐帧渲染时等于冻结(契约:所有视觉变化必须挂在 tl 上)。` + `静态正则分不清「驱动画面」与其它用途(如一次性布局测量),故只提醒、不拦`);
9452
+ if (opts.dispatchIds && cid && !opts.dispatchIds.includes(cid))
9453
+ push("x-dispatch", false, `composition_id "${cid}" 不在 dispatch.mg 派单中`);
8191
9454
  if (opts.category && opts.category in CATEGORY_EXPECTED_OPAQUE2) {
8192
9455
  const expect = CATEGORY_EXPECTED_OPAQUE2[opts.category];
8193
9456
  if (expect !== opaque)
8194
9457
  push("x-category-opaque", false, `category「${opts.category}」期望${expect ? "不透明满屏" : "透明叠加"},但颗粒 HTML 反推为${opaque ? "不透明满屏" : "透明叠加"}(以 HTML 为准落 clip.opaque=${opaque})`);
8195
9458
  }
9459
+ if (typeof opts.slotDuration === "number" && opts.slotDuration > 0) {
9460
+ const slot = opts.slotDuration;
9461
+ const { est, parsed, skipped, hasInfiniteRepeat } = estimateTimelineSec(html);
9462
+ if (hasInfiniteRepeat)
9463
+ push("7-infinite-repeat", false, `含 repeat:-1 无限循环 → tl 时长为 Infinity,铁律⑦(总长 ≥ 坑位)无法静态验证;请改用按坑位算死的有限 repeat(次数 = ceil(剩余时长 / 单圈时长)),坑位包络 ${slot}s`);
9464
+ if (parsed === 0)
9465
+ push("7-no-estimate", false, `无法静态估长(无一条可解析的时长调用${skipped ? `,${skipped} 条因表达式 position / 非字面量 duration 跳过` : ""}),铁律⑦未校验——须真渲染引擎 seek 验收颗粒是否占满坑位 ${slot}s 并终态驻留`);
9466
+ else if (Number.isFinite(est) && est < slot)
9467
+ push("7-fill-slot", false, `时间线静态估长 ~${est}s,短于槽位包络 ${slot}s(铁律⑦:颗粒应占满坑位并终态驻留)——静态估算是**下界**(${skipped} 条调用因无法静态解析被跳过),仅供参考,最终以真渲染引擎逐帧为准`);
9468
+ }
8196
9469
  return { ok: !v.some((x) => x.fatal), violations: v, opaque, compositionId: cid };
8197
9470
  }
8198
9471
 
@@ -8200,10 +9473,66 @@ function lintParticle(html, opts = {}) {
8200
9473
  var MG_MATERIAL_PREFIX = "mg-";
8201
9474
  var LEGACY_MATERIAL_PREFIX = "rrv-";
8202
9475
  var isOwnMaterialId = (id) => id.startsWith(MG_MATERIAL_PREFIX) || id.startsWith(LEGACY_MATERIAL_PREFIX);
8203
- var r34 = (n) => Math.round(n * 1000) / 1000;
9476
+ var OWN_ASSET_DIRS = ["assets/mg/", "assets/rrv/"];
9477
+ var normalizeRel2 = (p) => p.replace(/\\/g, "/").replace(/^\.\//, "");
9478
+ function ownAssetCompositionId(p) {
9479
+ if (typeof p !== "string")
9480
+ return;
9481
+ const rel = normalizeRel2(p);
9482
+ for (const d of OWN_ASSET_DIRS) {
9483
+ if (!rel.startsWith(d))
9484
+ continue;
9485
+ const name = rel.slice(d.length);
9486
+ if (name.includes("/"))
9487
+ return;
9488
+ return name.replace(/\.html?$/i, "");
9489
+ }
9490
+ return;
9491
+ }
9492
+ var r36 = (n) => Math.round(n * 1000) / 1000;
9493
+ var slotEnvelope = (it) => r36(it.track_ed - it.track_st);
8204
9494
  function layTracksOf(prev) {
8205
9495
  return Array.isArray(prev?.lay_tracks) ? prev.lay_tracks.filter((x) => typeof x === "number") : [];
8206
9496
  }
9497
+ function beatsOf(prev) {
9498
+ return Array.isArray(prev?.beats) ? prev.beats.filter((b) => !!b && typeof b.composition_id === "string") : [];
9499
+ }
9500
+ function clipCompositionId(c3) {
9501
+ const hm = c3.html_material;
9502
+ if (typeof hm === "string") {
9503
+ if (hm.startsWith(MG_MATERIAL_PREFIX))
9504
+ return hm.slice(MG_MATERIAL_PREFIX.length);
9505
+ if (hm.startsWith(LEGACY_MATERIAL_PREFIX))
9506
+ return hm.slice(LEGACY_MATERIAL_PREFIX.length);
9507
+ }
9508
+ if (typeof c3.material === "string")
9509
+ return c3.material;
9510
+ return typeof c3.clip_id === "string" ? c3.clip_id : undefined;
9511
+ }
9512
+ var stOf = (c3) => typeof c3.track_st === "number" ? c3.track_st : 0;
9513
+ function collectClipRefs(c3, into) {
9514
+ for (const k of ["material", "html_material"]) {
9515
+ const v = c3[k];
9516
+ if (typeof v === "string")
9517
+ into.add(v);
9518
+ }
9519
+ }
9520
+ function collectSurvivingRefs(doc, extraClips) {
9521
+ const refs = new Set;
9522
+ for (const bucket of Object.values(doc)) {
9523
+ if (!Array.isArray(bucket))
9524
+ continue;
9525
+ for (const t of bucket) {
9526
+ if (!t || !Array.isArray(t.track_timeline))
9527
+ continue;
9528
+ for (const c3 of t.track_timeline)
9529
+ collectClipRefs(c3, refs);
9530
+ }
9531
+ }
9532
+ for (const c3 of extraClips)
9533
+ collectClipRefs(c3, refs);
9534
+ return refs;
9535
+ }
8207
9536
  function layMgTracks(opts) {
8208
9537
  const { gtrk, items, generatedAt } = opts;
8209
9538
  const beatTracks = [...gtrk.beat_track ?? []];
@@ -8214,15 +9543,37 @@ function layMgTracks(opts) {
8214
9543
  const prevIndices = new Set([...layTracksOf(prevMg), ...layTracksOf(prevRrv)]);
8215
9544
  const removedTracks = beatTracks.filter((t) => typeof t.track_index === "number" && prevIndices.has(t.track_index));
8216
9545
  const keptTracks = beatTracks.filter((t) => !(typeof t.track_index === "number" && prevIndices.has(t.track_index)));
8217
- const removedMaterialIds = new Set;
9546
+ const ownCompositionIds = new Set([
9547
+ ...beatsOf(prevMg).map((b) => b.composition_id),
9548
+ ...beatsOf(prevRrv).map((b) => b.composition_id),
9549
+ ...items.map((it) => it.composition_id)
9550
+ ]);
8218
9551
  for (const t of removedTracks) {
8219
9552
  for (const c3 of t.track_timeline ?? []) {
8220
- const hm = c3.html_material;
8221
- if (typeof hm === "string" && isOwnMaterialId(hm))
8222
- removedMaterialIds.add(hm);
9553
+ const cid = clipCompositionId(c3);
9554
+ if (cid !== undefined)
9555
+ ownCompositionIds.add(cid);
9556
+ }
9557
+ }
9558
+ const isOwnMaterial = (m) => {
9559
+ if (typeof m.id === "string" && isOwnMaterialId(m.id))
9560
+ return true;
9561
+ const cid = ownAssetCompositionId(m.path);
9562
+ return cid !== undefined && ownCompositionIds.has(cid);
9563
+ };
9564
+ const itemIds = new Set(items.map((it) => it.composition_id));
9565
+ const keepIds = new Set((opts.keep ?? []).filter((id) => !itemIds.has(id)));
9566
+ const carriedClips = [];
9567
+ const carriedIds = new Set;
9568
+ for (const t of removedTracks) {
9569
+ for (const c3 of t.track_timeline ?? []) {
9570
+ const cid = clipCompositionId(c3);
9571
+ if (cid === undefined || !keepIds.has(cid) || carriedIds.has(cid))
9572
+ continue;
9573
+ carriedIds.add(cid);
9574
+ carriedClips.push({ ...c3 });
8223
9575
  }
8224
9576
  }
8225
- const keptMaterials = materials.filter((m) => !(typeof m.id === "string" && removedMaterialIds.has(m.id)));
8226
9577
  const newMaterials = [];
8227
9578
  const clips = [];
8228
9579
  const metaBeats = [];
@@ -8233,7 +9584,8 @@ function layMgTracks(opts) {
8233
9584
  ].map((t) => typeof t.track_index === "number" ? t.track_index : 0);
8234
9585
  const newIndex = Math.max(9, ...allIndices) + 1;
8235
9586
  for (const it of items) {
8236
- if (!(it.duration > 0)) {
9587
+ const envelope = slotEnvelope(it);
9588
+ if (!(envelope > 0)) {
8237
9589
  metaBeats.push({ ...toMetaBeat(it), laid: null });
8238
9590
  continue;
8239
9591
  }
@@ -8245,21 +9597,36 @@ function layMgTracks(opts) {
8245
9597
  html_material: materialId,
8246
9598
  opaque: it.opaque,
8247
9599
  track_st: it.track_st,
8248
- duration: it.duration
9600
+ duration: envelope
8249
9601
  });
8250
9602
  metaBeats.push({ ...toMetaBeat(it), laid: { track_index: newIndex } });
8251
9603
  }
8252
- const createdTracks = clips.length > 0 ? [
8253
- {
8254
- track_index: newIndex,
8255
- track_timeline: clips.sort((a, b) => a.track_st - b.track_st)
8256
- }
8257
- ] : [];
9604
+ const mergedClips = [...carriedClips, ...clips].sort((a, b) => stOf(a) - stOf(b));
9605
+ const createdTracks = mergedClips.length > 0 ? [{ track_index: newIndex, track_timeline: mergedClips }] : [];
9606
+ const survivingRefs = collectSurvivingRefs({ ...gtrk, beat_track: keptTracks }, carriedClips);
9607
+ const newMaterialIds = new Set(newMaterials.map((m) => String(m.id)));
9608
+ const keptMaterials = materials.filter((m) => {
9609
+ if (typeof m.id !== "string")
9610
+ return true;
9611
+ if (newMaterialIds.has(m.id))
9612
+ return false;
9613
+ if (!isOwnMaterial(m))
9614
+ return true;
9615
+ return survivingRefs.has(m.id);
9616
+ });
9617
+ const carriedBeats = [];
9618
+ const seenCarriedBeat = new Set;
9619
+ for (const b of [...beatsOf(prevMg), ...beatsOf(prevRrv)]) {
9620
+ if (!keepIds.has(b.composition_id) || seenCarriedBeat.has(b.composition_id))
9621
+ continue;
9622
+ seenCarriedBeat.add(b.composition_id);
9623
+ carriedBeats.push({ ...b, laid: carriedIds.has(b.composition_id) ? { track_index: newIndex } : null });
9624
+ }
8258
9625
  const mg = {
8259
9626
  contract_version: "v1",
8260
9627
  generated_at: generatedAt,
8261
9628
  lay_tracks: createdTracks.map((t) => t.track_index),
8262
- beats: metaBeats
9629
+ beats: [...carriedBeats, ...metaBeats]
8263
9630
  };
8264
9631
  const nextStructMeta = { ...structMeta, mg };
8265
9632
  delete nextStructMeta.rrv;
@@ -8271,7 +9638,11 @@ function layMgTracks(opts) {
8271
9638
  };
8272
9639
  return {
8273
9640
  next,
8274
- summary: { laidTrack: createdTracks[0]?.track_index ?? null, laidParticles: clips.length },
9641
+ summary: {
9642
+ laidTrack: createdTracks[0]?.track_index ?? null,
9643
+ laidParticles: clips.length,
9644
+ keptParticles: carriedClips.length
9645
+ },
8275
9646
  mg
8276
9647
  };
8277
9648
  }
@@ -8280,8 +9651,8 @@ function toMetaBeat(it) {
8280
9651
  beat: it.beat,
8281
9652
  composition_id: it.composition_id,
8282
9653
  track_st: it.track_st,
8283
- track_ed: r34(it.track_ed),
8284
- duration: it.duration,
9654
+ track_ed: r36(it.track_ed),
9655
+ duration: slotEnvelope(it),
8285
9656
  html_path: it.html_rel,
8286
9657
  ...it.category ? { category: it.category } : {}
8287
9658
  };
@@ -8291,7 +9662,7 @@ function toMetaBeat(it) {
8291
9662
  var MG_ASSET_DIR = "assets/mg";
8292
9663
  var MG_SRC_DIRS = ["mg", "rrv"];
8293
9664
  function registerMg(program2) {
8294
- 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) => {
9665
+ 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) => {
8295
9666
  if (process.argv[2] === "rrv")
8296
9667
  log.warn("`gtrk rrv` 已更名为 `gtrk mg`(去品牌化),别名仍可用但建议改用 `gtrk mg`。");
8297
9668
  await runMg(words ?? [], opts);
@@ -8311,53 +9682,115 @@ async function runMg(words, opts) {
8311
9682
  }
8312
9683
  function resolveDispatch(opts) {
8313
9684
  if (opts.dispatch) {
8314
- const dispatchPath = resolve9(opts.dispatch);
9685
+ const dispatchPath = resolve10(opts.dispatch);
8315
9686
  return { dispatchPath, baseDir: dirname8(dirname8(dispatchPath)) };
8316
9687
  }
8317
9688
  if (opts.project) {
8318
- const baseDir = resolve9(opts.project);
8319
- return { dispatchPath: join20(baseDir, "split", "dispatch.json"), baseDir };
9689
+ const baseDir = resolve10(opts.project);
9690
+ return { dispatchPath: join21(baseDir, "split", "dispatch.json"), baseDir };
8320
9691
  }
8321
9692
  throw new Error("需 --project <目录> 或显式 --dispatch <path>");
8322
9693
  }
8323
9694
  function locateGtrk2(baseDir) {
8324
- return [join20(baseDir, "gtrk", "project.gtrk"), join20(baseDir, "project.gtrk")].find((p) => existsSync16(p));
9695
+ return [join21(baseDir, "gtrk", "project.gtrk"), join21(baseDir, "project.gtrk")].find((p) => existsSync18(p));
8325
9696
  }
8326
9697
  function locateSrcHtml(baseDir, compositionId) {
8327
9698
  for (const d of MG_SRC_DIRS) {
8328
- const p = join20(baseDir, d, `${compositionId}.html`);
8329
- if (existsSync16(p))
9699
+ const p = join21(baseDir, d, `${compositionId}.html`);
9700
+ if (existsSync18(p))
8330
9701
  return p;
8331
9702
  }
8332
9703
  return;
8333
9704
  }
8334
9705
  async function readMgQueue(dispatchPath) {
8335
- if (!existsSync16(dispatchPath))
9706
+ if (!existsSync18(dispatchPath))
8336
9707
  throw new Error(`找不到派单清单:${dispatchPath}(先跑 gtrk split 落地派单)`);
8337
- const dispatch = JSON.parse(await readFile6(dispatchPath, "utf8"));
9708
+ const dispatch = JSON.parse(await readFile7(dispatchPath, "utf8"));
8338
9709
  const queue = dispatch.mg ?? dispatch.rrv_mg;
8339
9710
  return Array.isArray(queue) ? queue : [];
8340
9711
  }
9712
+ function laidCompositionIds(gtrk) {
9713
+ if (!gtrk)
9714
+ return [];
9715
+ const structMeta = gtrk.struct_meta;
9716
+ const ids = [];
9717
+ const seen = new Set;
9718
+ for (const meta of [structMeta?.mg, structMeta?.rrv]) {
9719
+ for (const b of meta?.beats ?? []) {
9720
+ if (!b?.laid || typeof b.composition_id !== "string" || seen.has(b.composition_id))
9721
+ continue;
9722
+ seen.add(b.composition_id);
9723
+ ids.push(b.composition_id);
9724
+ }
9725
+ }
9726
+ return ids;
9727
+ }
8341
9728
  async function runLay(opts) {
8342
9729
  const { dispatchPath, baseDir } = resolveDispatch(opts);
8343
- let queue = await readMgQueue(dispatchPath);
8344
- if (opts.only)
8345
- queue = queue.filter((q) => q.beat === opts.only);
9730
+ const allQueue = await readMgQueue(dispatchPath);
9731
+ const queue = opts.only ? allQueue.filter((q) => q.beat === opts.only) : allQueue;
8346
9732
  log.step(`▶ MG 颗粒铺轨:${queue.length} 个 beat…`);
9733
+ if (opts.only && queue.length === 0) {
9734
+ const beats = [...new Set(allQueue.map((q) => q.beat))];
9735
+ log.warn(`--only ${opts.only} 未命中任何 beat——该选择器收的是「beat id」(如 B12),不是 composition_id(如 <slug>-B12)。`);
9736
+ log.warn(`dispatch 现有 beat:${beats.slice(0, 12).join("、") || "(空)"}${beats.length > 12 ? ` …共 ${beats.length} 个` : ""}`);
9737
+ }
9738
+ const gtrkPath = locateGtrk2(baseDir);
9739
+ let project;
9740
+ let gtrkUnreadable = false;
9741
+ if (gtrkPath) {
9742
+ try {
9743
+ project = readGtrk(gtrkPath);
9744
+ } catch (e) {
9745
+ if (!opts.lintOnly)
9746
+ throw e;
9747
+ gtrkUnreadable = true;
9748
+ }
9749
+ }
9750
+ if (!opts.lintOnly && project)
9751
+ assertGtrkV1(project.gtrk);
9752
+ const laidBefore = laidCompositionIds(opts.lintOnly ? undefined : project?.gtrk);
9753
+ const reproj = await reprojectDispatchWindows({
9754
+ baseDir,
9755
+ gtrk: project?.gtrk,
9756
+ gtrkUnreadable,
9757
+ entries: queue.map((q) => ({
9758
+ key: q.composition_id,
9759
+ beat: q.beat,
9760
+ compositionId: q.composition_id,
9761
+ span: q.span,
9762
+ track_st: q.track_st,
9763
+ track_ed: q.track_ed
9764
+ }))
9765
+ });
9766
+ reportReprojection(reproj);
8347
9767
  const dispatchIds = queue.map((q) => q.composition_id);
8348
9768
  const items = [];
8349
9769
  const srcByComp = new Map;
8350
9770
  const skipped = [];
9771
+ const windowOf = new Map(reproj.entries.map((o) => [o.key, o]));
8351
9772
  for (const q of queue) {
9773
+ const outcome = windowOf.get(q.composition_id);
9774
+ if (outcome?.dropped) {
9775
+ skipped.push({ beat: q.beat, reason: "重投影后零存活(该段已被剪出成片)" });
9776
+ continue;
9777
+ }
9778
+ const win = outcome ?? { track_st: q.track_st, track_ed: q.track_ed };
8352
9779
  const srcPath = locateSrcHtml(baseDir, q.composition_id);
8353
9780
  if (!srcPath) {
8354
9781
  skipped.push({ beat: q.beat, reason: "缺颗粒 HTML(未产出)" });
8355
- log.warn(`${q.beat}:缺 ${join20(baseDir, MG_SRC_DIRS[0], `${q.composition_id}.html`)},跳过`);
9782
+ log.warn(`${q.beat}:缺 ${join21(baseDir, MG_SRC_DIRS[0], `${q.composition_id}.html`)},跳过`);
8356
9783
  continue;
8357
9784
  }
8358
- const html = await readFile6(srcPath, "utf8");
9785
+ const html = await readFile7(srcPath, "utf8");
8359
9786
  const category = typeof q.category === "string" ? q.category : undefined;
8360
- const lint = lintParticle(html, { compositionId: q.composition_id, dispatchIds, category });
9787
+ const slotDuration = Math.round((win.track_ed - win.track_st) * 1000) / 1000;
9788
+ const lint = lintParticle(html, {
9789
+ compositionId: q.composition_id,
9790
+ dispatchIds,
9791
+ category,
9792
+ ...slotDuration > 0 ? { slotDuration } : {}
9793
+ });
8361
9794
  for (const vv of lint.violations)
8362
9795
  (vv.fatal ? log.warn : log.info)(`${q.beat} lint ${vv.fatal ? "✗" : "·"} ${vv.law}: ${vv.msg}`);
8363
9796
  if (!lint.ok) {
@@ -8365,64 +9798,170 @@ async function runLay(opts) {
8365
9798
  log.warn(`${q.beat}:lint 未过,跳过`);
8366
9799
  continue;
8367
9800
  }
8368
- if (typeof q.duration !== "number" || !(q.duration > 0)) {
8369
- skipped.push({ beat: q.beat, reason: "duration 非正数" });
9801
+ if (!(slotDuration > 0)) {
9802
+ skipped.push({ beat: q.beat, reason: "槽位包络非正数" });
8370
9803
  continue;
8371
9804
  }
8372
9805
  srcByComp.set(q.composition_id, srcPath);
8373
9806
  items.push({
8374
9807
  beat: q.beat,
8375
9808
  composition_id: q.composition_id,
8376
- track_st: q.track_st,
8377
- track_ed: q.track_ed,
8378
- duration: q.duration,
9809
+ track_st: win.track_st,
9810
+ track_ed: win.track_ed,
8379
9811
  opaque: lint.opaque,
8380
9812
  html_rel: `${MG_ASSET_DIR}/${q.composition_id}.html`,
8381
9813
  ...category ? { category } : {}
8382
9814
  });
8383
9815
  }
8384
9816
  if (opts.lintOnly) {
8385
- log.ok(`lint-only:${items.length}/${queue.length} 通过,${skipped.length} 跳过(不铺轨)`);
8386
- return done(opts, { ok: skipped.length === 0, mode: "lay", lintOnly: true, passed: items.length, skipped });
9817
+ const lintOk = skipped.length === 0;
9818
+ (lintOk ? log.ok : log.warn)(`lint-only:${items.length}/${queue.length} 通过,${skipped.length} 跳过(不铺轨)`);
9819
+ return done(opts, {
9820
+ ok: lintOk,
9821
+ mode: "lay",
9822
+ lintOnly: true,
9823
+ ...lintOk ? {} : { reason: "skipped" },
9824
+ passed: items.length,
9825
+ skipped,
9826
+ reprojection: reproj.summary
9827
+ });
8387
9828
  }
8388
- const gtrkPath = locateGtrk2(baseDir);
8389
- if (!gtrkPath) {
8390
- log.warn(`未找到工程文件(${join20(baseDir, "gtrk", "project.gtrk")}),跳过铺轨——lint 已完成`);
8391
- return done(opts, { ok: true, mode: "lay", laid: 0, skipped, note: "工程缺失,未铺轨" });
9829
+ if (!gtrkPath || !project) {
9830
+ log.warn(`未找到工程文件(${join21(baseDir, "gtrk", "project.gtrk")}),跳过铺轨——lint 已完成`);
9831
+ return done(opts, {
9832
+ ok: skipped.length === 0,
9833
+ mode: "lay",
9834
+ reason: "no_project",
9835
+ laid: 0,
9836
+ laidTrack: null,
9837
+ track_total: 0,
9838
+ removed: 0,
9839
+ kept: 0,
9840
+ kept_ids: [],
9841
+ skipped,
9842
+ note: "工程缺失,未铺轨",
9843
+ reprojection: reproj.summary
9844
+ });
8392
9845
  }
8393
- const { gtrk, mtimeMs } = readGtrk(gtrkPath);
8394
- assertGtrkV1(gtrk);
9846
+ const { gtrk, mtimeMs } = project;
8395
9847
  const gtrkDir = dirname8(gtrkPath);
8396
- await mkdir7(join20(gtrkDir, ...MG_ASSET_DIR.split("/")), { recursive: true });
9848
+ const covered = new Set(items.map((it) => it.composition_id));
9849
+ const orphans = laidBefore.filter((id) => !covered.has(id));
9850
+ const inDispatch = new Set(allQueue.map((q) => q.composition_id));
9851
+ const keep = opts.replaceAll ? [] : opts.only ? orphans : orphans.filter((id) => inDispatch.has(id));
9852
+ const keepSet = new Set(keep);
9853
+ const wouldRemove = laidBefore.filter((id) => !covered.has(id) && !keepSet.has(id));
9854
+ const refuse = laidBefore.length > 0 && items.length === 0 ? "empty_queue" : undefined;
9855
+ if (refuse && !opts.replaceAll) {
9856
+ log.err(`拒绝写回:本次一条颗粒都没定位到,而轨上已铺 ${laidBefore.length} 颗——「一条都没定位到」是派单/选择器出问题的信号,不是清空指令。`);
9857
+ if (opts.only && queue.length === 0) {
9858
+ log.warn(`成因:--only ${opts.only} 未命中任何 beat(选择器提示见上)。`);
9859
+ } else if (allQueue.length === 0) {
9860
+ log.warn("dispatch.mg(读旧 rrv_mg)为空或缺失——先跑 gtrk split 落地派单,再铺轨。");
9861
+ } else {
9862
+ log.warn(`本次 ${queue.length} 个条目全部被跳过(${[...new Set(skipped.map((s) => s.reason))].join("、")})——先按上面的 lint 报因补产 / 修颗粒,再重铺。`);
9863
+ }
9864
+ if (wouldRemove.length) {
9865
+ const preview = wouldRemove.slice(0, 8);
9866
+ log.warn(`将被铲掉:${preview.join("、")}${wouldRemove.length > preview.length ? ` …等共 ${wouldRemove.length} 颗` : ""}`);
9867
+ }
9868
+ log.warn("出路二选一:① 按上面的报因修好派单 / 选择器 / 颗粒后重铺;② 确知要清空整轨 → 加 --replace-all 显式授权(会删掉轨上全部已铺颗粒)");
9869
+ log.warn(`工程未被改动(.gtrk 逐字节不变,轨上仍 ${laidBefore.length} 颗)。`);
9870
+ return done(opts, {
9871
+ ok: false,
9872
+ mode: "lay",
9873
+ reason: refuse,
9874
+ refused: true,
9875
+ laid: 0,
9876
+ laidTrack: null,
9877
+ track_total: laidBefore.length,
9878
+ removed: 0,
9879
+ kept: 0,
9880
+ kept_ids: [],
9881
+ blocked: wouldRemove,
9882
+ skipped,
9883
+ reprojection: reproj.summary
9884
+ });
9885
+ }
9886
+ if (opts.replaceAll && orphans.length > 0) {
9887
+ log.warn(`--replace-all:已显式授权重置整轨,轨上其余 ${orphans.length} 颗已铺颗粒将被剥离(不走增量保留)。`);
9888
+ }
9889
+ await mkdir7(join21(gtrkDir, ...MG_ASSET_DIR.split("/")), { recursive: true });
8397
9890
  for (const it of items) {
8398
- await copyFile(srcByComp.get(it.composition_id), join20(gtrkDir, ...it.html_rel.split("/")));
9891
+ await copyFile(srcByComp.get(it.composition_id), join21(gtrkDir, ...it.html_rel.split("/")));
9892
+ }
9893
+ const { next, summary, mg } = layMgTracks({ gtrk, items, generatedAt: new Date().toISOString(), keep });
9894
+ const written = withTimecodeSource(next, "mg", reproj);
9895
+ writeGtrkAtomic(gtrkPath, written, mtimeMs);
9896
+ const integrity = safeCheckMaterialIntegrity({ gtrk: written, gtrkDir, log });
9897
+ const trackTotal = mg.beats.filter((b) => b.laid).length;
9898
+ const keptIds = mg.beats.filter((b) => b.laid && keepSet.has(b.composition_id)).map((b) => b.composition_id);
9899
+ const removed = laidBefore.length - keptIds.length;
9900
+ const ok = skipped.length === 0;
9901
+ const tail = `本次 ${summary.laidParticles} 颗 / 轨上共 ${trackTotal} 颗${keptIds.length ? `(保留 ${keptIds.length} 颗)` : ""}` + ` → beat_track ${summary.laidTrack ?? "-"}` + `${removed ? `(剥旧 ${removed} 颗)` : ""}${skipped.length ? `(${skipped.length} beat 跳过)` : ""}`;
9902
+ if (summary.laidTrack === null)
9903
+ log.warn(`未铺成任何颗粒:${tail}`);
9904
+ else if (ok)
9905
+ log.ok(`铺轨完成:${tail}`);
9906
+ else
9907
+ log.warn(`铺轨完成(有跳过):${tail}`);
9908
+ if (keptIds.length) {
9909
+ const preview = keptIds.slice(0, 8);
9910
+ log.warn(`其中 ${keptIds.length} 颗是上轮遗留、本次未重铺:${preview.join("、")}${keptIds.length > preview.length ? ` …等共 ${keptIds.length} 颗` : ""}` + `——轨上内容 = 本次 ${summary.laidParticles} 颗 + 上轮 ${keptIds.length} 颗,与本次派单不完全对应(要全部刷新就去掉 --only 全量重铺)。`);
8399
9911
  }
8400
- const { next, summary } = layMgTracks({ gtrk, items, generatedAt: new Date().toISOString() });
8401
- writeGtrkAtomic(gtrkPath, next, mtimeMs);
8402
- log.ok(`铺轨完成:${summary.laidParticles} 颗粒 → beat_track ${summary.laidTrack ?? "-"}` + `${skipped.length ? `(${skipped.length} beat 跳过)` : ""}`);
8403
9912
  log.info("opencut 打开工程即见 MG overlay 轨(预览需 add-particle-project-folder-preview 上线);出片时客户端云渲。");
9913
+ if (integrity)
9914
+ reportMaterialIntegrity(integrity, log);
8404
9915
  return done(opts, {
8405
- ok: true,
9916
+ ok,
8406
9917
  mode: "lay",
9918
+ ...ok ? {} : { reason: "skipped" },
8407
9919
  laid: summary.laidParticles,
8408
9920
  laidTrack: summary.laidTrack,
8409
- skipped
9921
+ track_total: trackTotal,
9922
+ removed,
9923
+ kept: keptIds.length,
9924
+ kept_ids: keptIds,
9925
+ skipped,
9926
+ ...integrity ? { integrity } : {},
9927
+ reprojection: reproj.summary
8410
9928
  });
8411
9929
  }
9930
+ var CID_SHAPE = /-B\d+(?:-aux\d+)?$/;
8412
9931
  async function runLint(args, opts) {
8413
9932
  const file = args[0];
8414
9933
  if (!file)
8415
9934
  throw new Error("用法:gtrk mg lint <particle.html> [--dispatch <path>]");
8416
- const html = await readFile6(resolve9(file), "utf8");
9935
+ const html = await readFile7(resolve10(file), "utf8");
9936
+ const nameId = basename11(file).replace(/\.html?$/i, "");
8417
9937
  let dispatchIds;
8418
- if (opts.dispatch && existsSync16(resolve9(opts.dispatch))) {
8419
- dispatchIds = (await readMgQueue(resolve9(opts.dispatch))).map((q) => q.composition_id);
8420
- }
8421
- const lint = lintParticle(html, { dispatchIds });
9938
+ let slotDuration;
9939
+ let compositionId;
9940
+ if (opts.dispatch && existsSync18(resolve10(opts.dispatch))) {
9941
+ const queue = await readMgQueue(resolve10(opts.dispatch));
9942
+ dispatchIds = queue.map((q) => q.composition_id);
9943
+ const byName = queue.find((q) => q.composition_id === nameId);
9944
+ const innerCid = parseCompositionId(html);
9945
+ const hit = byName ?? (innerCid ? queue.find((q) => q.composition_id === innerCid) : undefined);
9946
+ if (hit) {
9947
+ const d = Math.round((hit.track_ed - hit.track_st) * 1000) / 1000;
9948
+ if (d > 0)
9949
+ slotDuration = d;
9950
+ }
9951
+ if (byName)
9952
+ compositionId = byName.composition_id;
9953
+ }
9954
+ if (compositionId === undefined && CID_SHAPE.test(nameId))
9955
+ compositionId = nameId;
9956
+ const lint = lintParticle(html, {
9957
+ ...dispatchIds ? { dispatchIds } : {},
9958
+ ...compositionId ? { compositionId } : {},
9959
+ ...slotDuration ? { slotDuration } : {}
9960
+ });
8422
9961
  for (const vv of lint.violations)
8423
9962
  (vv.fatal ? log.err : log.warn)(`${vv.fatal ? "✗" : "·"} ${vv.law}: ${vv.msg}`);
8424
9963
  if (lint.ok)
8425
- log.ok(`lint 通过(${basename10(file)};opaque=${lint.opaque})`);
9964
+ log.ok(`lint 通过(${basename11(file)};opaque=${lint.opaque})`);
8426
9965
  else
8427
9966
  log.err(`lint 未过(${lint.violations.filter((v) => v.fatal).length} 项致命)`);
8428
9967
  const result = { mode: "lint", ...lint, ok: lint.ok };
@@ -8456,6 +9995,8 @@ async function runStatus(opts) {
8456
9995
  return done(opts, { ok: true, mode: "status", total: queue.length, authored, laid, rows });
8457
9996
  }
8458
9997
  function done(opts, result) {
9998
+ if (!result.ok)
9999
+ process.exitCode = 1;
8459
10000
  if (opts.json)
8460
10001
  console.log(JSON.stringify(result));
8461
10002
  return result;
@@ -9444,9 +10985,9 @@ function validateRegistry(registry = TOOL_REGISTRY) {
9444
10985
  }
9445
10986
 
9446
10987
  // src/lib/tool-runner.ts
9447
- import { resolve as resolve10, join as join21, dirname as dirname9, basename as basename11, extname as extname5 } from "node:path";
10988
+ import { resolve as resolve11, join as join22, dirname as dirname9, basename as basename12, extname as extname5 } from "node:path";
9448
10989
  import { mkdir as mkdir8, writeFile as writeFile8, stat as stat5 } from "node:fs/promises";
9449
- import { createWriteStream, existsSync as existsSync17 } from "node:fs";
10990
+ import { createWriteStream, existsSync as existsSync19 } from "node:fs";
9450
10991
  import { Readable as Readable2 } from "node:stream";
9451
10992
  import { pipeline } from "node:stream/promises";
9452
10993
 
@@ -9576,7 +11117,7 @@ function validateToolInput(descriptor, inputAbs) {
9576
11117
  return;
9577
11118
  if (!inputAbs)
9578
11119
  throw new Error(`${descriptor.name} 需要输入${spec.kind === "directory" ? "目录" : "文件"}`);
9579
- if (!existsSync17(inputAbs))
11120
+ if (!existsSync19(inputAbs))
9580
11121
  throw new Error(`输入不存在:${inputAbs}`);
9581
11122
  if (spec.kind === "directory")
9582
11123
  return;
@@ -9643,12 +11184,12 @@ function timestamp3() {
9643
11184
  }
9644
11185
  function resolveOutDir(descriptor, inputAbs, out) {
9645
11186
  if (out)
9646
- return resolve10(out);
11187
+ return resolve11(out);
9647
11188
  if (inputAbs) {
9648
- const base = basename11(inputAbs, extname5(inputAbs));
9649
- return join21(dirname9(inputAbs), `${base}-${descriptor.name}`);
11189
+ const base = basename12(inputAbs, extname5(inputAbs));
11190
+ return join22(dirname9(inputAbs), `${base}-${descriptor.name}`);
9650
11191
  }
9651
- return join21(process.cwd(), `${descriptor.name}-${timestamp3()}`);
11192
+ return join22(process.cwd(), `${descriptor.name}-${timestamp3()}`);
9652
11193
  }
9653
11194
  async function safeFingerprint(inputAbs) {
9654
11195
  try {
@@ -9663,8 +11204,8 @@ function emitBilling(hint) {
9663
11204
  `);
9664
11205
  }
9665
11206
  async function runCloudTool(descriptor, inputArg, opts, deps) {
9666
- const inputAbs = inputArg ? resolve10(inputArg) : undefined;
9667
- const baseName = inputAbs ? basename11(inputAbs, extname5(inputAbs)) : descriptor.name;
11207
+ const inputAbs = inputArg ? resolve11(inputArg) : undefined;
11208
+ const baseName = inputAbs ? basename12(inputAbs, extname5(inputAbs)) : descriptor.name;
9668
11209
  validateToolInput(descriptor, inputAbs);
9669
11210
  const probe = deps.probeDurationSec ?? probeDuration;
9670
11211
  guardDuration(descriptor, inputAbs, probe, opts.ffmpegPath);
@@ -9701,13 +11242,13 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
9701
11242
  uploadCached: deps.uploadCached,
9702
11243
  invalidateUpload: deps.invalidateUpload,
9703
11244
  submitTask: deps.submitTask,
9704
- sleep: deps.sleep ?? ((ms) => new Promise((resolve11) => setTimeout(resolve11, ms)))
11245
+ sleep: deps.sleep ?? ((ms) => new Promise((resolve12) => setTimeout(resolve12, ms)))
9705
11246
  });
9706
11247
  const { taskId } = submitted;
9707
11248
  const up = { fileId: submitted.fileId, cached: submitted.cached };
9708
11249
  await mkdir8(outDir, { recursive: true });
9709
11250
  const fingerprint2 = inputAbs ? await safeFingerprint(inputAbs) : undefined;
9710
- 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));
11251
+ 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));
9711
11252
  const output = await pollToolTask(deps.cfg, taskType, taskId, {
9712
11253
  timeoutMs: descriptor.pollTimeoutMs,
9713
11254
  intervalMs: deps.pollIntervalMs,
@@ -9720,7 +11261,7 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
9720
11261
  const errors = {};
9721
11262
  const items = descriptor.mapOutputs ? descriptor.mapOutputs(outputResult, ctx) : [];
9722
11263
  for (const it of items) {
9723
- const dest = join21(outDir, it.filename);
11264
+ const dest = join22(outDir, it.filename);
9724
11265
  try {
9725
11266
  await deps.downloadStream(it.url, dest);
9726
11267
  files.push(dest);
@@ -9731,7 +11272,7 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
9731
11272
  let resultFile;
9732
11273
  const structured = descriptor.mapResult ? descriptor.mapResult(outputResult, ctx) : undefined;
9733
11274
  if (structured != null) {
9734
- resultFile = join21(outDir, "result-output.json");
11275
+ resultFile = join22(outDir, "result-output.json");
9735
11276
  await writeFile8(resultFile, JSON.stringify(structured, null, 2));
9736
11277
  }
9737
11278
  if (items.length === 0 && resultFile == null) {
@@ -9749,14 +11290,14 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
9749
11290
  ...resultFile ? { resultFile } : {},
9750
11291
  ...Object.keys(errors).length ? { errors } : {}
9751
11292
  };
9752
- await writeFile8(join21(outDir, "result.json"), JSON.stringify({ ...result, finishedAt: new Date().toISOString() }, null, 2));
11293
+ await writeFile8(join22(outDir, "result.json"), JSON.stringify({ ...result, finishedAt: new Date().toISOString() }, null, 2));
9753
11294
  return result;
9754
11295
  }
9755
11296
 
9756
11297
  // src/lib/mad/mad.ts
9757
11298
  import { mkdir as mkdir11, writeFile as writeFile10 } from "node:fs/promises";
9758
- import { existsSync as existsSync20, statSync as statSync3 } from "node:fs";
9759
- import { resolve as resolve11, join as join25 } from "node:path";
11299
+ import { existsSync as existsSync22, statSync as statSync3 } from "node:fs";
11300
+ import { resolve as resolve12, join as join26 } from "node:path";
9760
11301
 
9761
11302
  // src/lib/convert/types.ts
9762
11303
  function num(v, d = 0) {
@@ -10070,14 +11611,14 @@ var FX_MATCHNAME = {
10070
11611
  function esc(s) {
10071
11612
  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");
10072
11613
  }
10073
- function r35(v) {
11614
+ function r37(v) {
10074
11615
  return Math.round(v * 1000) / 1000;
10075
11616
  }
10076
11617
  function colorArr(css, fallback) {
10077
11618
  const c3 = parseCssColor(css);
10078
11619
  if (!c3)
10079
11620
  return fallback;
10080
- return [r35(c3[0] / 255), r35(c3[1] / 255), r35(c3[2] / 255)];
11621
+ return [r37(c3[0] / 255), r37(c3[1] / 255), r37(c3[2] / 255)];
10081
11622
  }
10082
11623
  function easeInfluence(e) {
10083
11624
  const cb = parseCubicBezier(e);
@@ -10093,7 +11634,7 @@ function emitKeys(ctx, propExpr, keys) {
10093
11634
  L.push(` try {`);
10094
11635
  L.push(` var p = ${propExpr};`);
10095
11636
  for (const k of keys) {
10096
- L.push(` p.setValueAtTime(${r35(k.t)}, ${k.value});`);
11637
+ L.push(` p.setValueAtTime(${r37(k.t)}, ${k.value});`);
10097
11638
  }
10098
11639
  keys.forEach((k, i) => {
10099
11640
  const inf = easeInfluence(k.e);
@@ -10102,7 +11643,7 @@ function emitKeys(ctx, propExpr, keys) {
10102
11643
  L.push(` try {`);
10103
11644
  L.push(` var dim = 1; try { dim = p.value.length || 1; } catch (e) { dim = 1; }`);
10104
11645
  L.push(` var eo = [], ei = [];`);
10105
- 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)})); }`);
11646
+ 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)})); }`);
10106
11647
  L.push(` p.setTemporalEaseAtKey(${i + 1}, ei, eo);`);
10107
11648
  L.push(` } catch (e) {}`);
10108
11649
  });
@@ -10183,10 +11724,10 @@ function positionBaseKeys(anim, pos) {
10183
11724
  if (!(anim.x?.length || anim.y?.length))
10184
11725
  return [];
10185
11726
  const merged = mergeChannelTracks(anim.x, anim.y, pos[0], pos[1]);
10186
- return merged.map((k) => ({ t: k.t, value: `[${r35(k.a)}, ${r35(k.b)}]`, e: k.e }));
11727
+ return merged.map((k) => ({ t: k.t, value: `[${r37(k.a)}, ${r37(k.b)}]`, e: k.e }));
10187
11728
  }
10188
11729
  function scaleBaseKeys(anim, coverExpr) {
10189
- const sc = (v) => coverExpr ? `${r35(v)}*${coverExpr}` : `${r35(v)}`;
11730
+ const sc = (v) => coverExpr ? `${r37(v)}*${coverExpr}` : `${r37(v)}`;
10190
11731
  if (anim.scale?.length) {
10191
11732
  return anim.scale.map((k) => ({ t: k.t, value: `[${sc(num(k.v, 1) * 100)}, ${sc(num(k.v, 1) * 100)}]`, e: k.e }));
10192
11733
  }
@@ -10206,13 +11747,13 @@ function emitBakedOps(ctx, ly, layerVar, tf, coverExpr) {
10206
11747
  const pos = [num(ly.pos?.[0], 0), num(ly.pos?.[1], 0)];
10207
11748
  const inn = num(ly.in, 0);
10208
11749
  const out = Math.max(inn + 0.01, num(ly.out, inn + 1));
10209
- const sc = (v) => coverExpr ? `${r35(v)}*${coverExpr}` : `${r35(v)}`;
11750
+ const sc = (v) => coverExpr ? `${r37(v)}*${coverExpr}` : `${r37(v)}`;
10210
11751
  const brights = tracks.filter((t) => t.prop === "brightness");
10211
11752
  if (brights.length) {
10212
11753
  ctx.lines.push(` // fx: flicker → 亮度脉冲(ADBE Brightness & Contrast 2,避开 opacity 通道)`);
10213
11754
  ctx.lines.push(` var flkFx = null; try { flkFx = ${layerVar}.property("ADBE Effect Parade").addProperty("ADBE Brightness & Contrast 2"); } catch (e) { flkFx = null; }`);
10214
11755
  for (const bt of brights) {
10215
- const keys = bt.times.map((t, i) => ({ t, value: `${r35(bt.values[i][0])}`, e: null }));
11756
+ const keys = bt.times.map((t, i) => ({ t, value: `${r37(bt.values[i][0])}`, e: null }));
10216
11757
  emitKeys(ctx, `flkFx.property(1)`, keys);
10217
11758
  }
10218
11759
  }
@@ -10224,12 +11765,12 @@ function emitBakedOps(ctx, ly, layerVar, tf, coverExpr) {
10224
11765
  for (const bt of posBaked) {
10225
11766
  windows.push(bt.window);
10226
11767
  for (let i = 0;i < bt.times.length; i++) {
10227
- bakedKeys.push({ t: bt.times[i], value: `[${r35(bt.values[i][0])}, ${r35(bt.values[i][1])}]`, e: null });
11768
+ bakedKeys.push({ t: bt.times[i], value: `[${r37(bt.values[i][0])}, ${r37(bt.values[i][1])}]`, e: null });
10228
11769
  }
10229
11770
  }
10230
11771
  const merged = mergeBaked(baseKeys, bakedKeys, windows, (t) => {
10231
11772
  const [x, y] = samplePos(anim, pos, t);
10232
- return `[${r35(x)}, ${r35(y)}]`;
11773
+ return `[${r37(x)}, ${r37(y)}]`;
10233
11774
  }, inn, out);
10234
11775
  emitKeys(ctx, `${tf}.property("ADBE Position")`, merged);
10235
11776
  }
@@ -10252,15 +11793,15 @@ function emitBakedOps(ctx, ly, layerVar, tf, coverExpr) {
10252
11793
  }
10253
11794
  const rotBaked = tracks.filter((t) => t.prop === "rotation");
10254
11795
  if (rotBaked.length) {
10255
- const baseKeys = (anim.rot ?? []).map((k) => ({ t: k.t, value: `${r35(num(k.v, 0))}`, e: k.e }));
11796
+ const baseKeys = (anim.rot ?? []).map((k) => ({ t: k.t, value: `${r37(num(k.v, 0))}`, e: k.e }));
10256
11797
  const bakedKeys = [];
10257
11798
  const windows = [];
10258
11799
  for (const bt of rotBaked) {
10259
11800
  windows.push(bt.window);
10260
11801
  for (let i = 0;i < bt.times.length; i++)
10261
- bakedKeys.push({ t: bt.times[i], value: `${r35(bt.values[i][0])}`, e: null });
11802
+ bakedKeys.push({ t: bt.times[i], value: `${r37(bt.values[i][0])}`, e: null });
10262
11803
  }
10263
- const merged = mergeBaked(baseKeys, bakedKeys, windows, (t) => `${r35(sampleTrack(anim.rot, t, 0))}`, inn, out);
11804
+ const merged = mergeBaked(baseKeys, bakedKeys, windows, (t) => `${r37(sampleTrack(anim.rot, t, 0))}`, inn, out);
10264
11805
  emitKeys(ctx, `${tf}.property("ADBE Rotate Z")`, merged);
10265
11806
  }
10266
11807
  }
@@ -10276,11 +11817,11 @@ function emitLayer(ctx, ir, ly, parentNullVar) {
10276
11817
  const py = num(ly.pos?.[1], c3.h / 2);
10277
11818
  const name = esc(`${ly.id || "L" + id} [${ly.type}]`);
10278
11819
  L.push(``);
10279
- L.push(` // ---- 层 ${name} in=${r35(inn)} out=${r35(out)} ----`);
11820
+ L.push(` // ---- 层 ${name} in=${r37(inn)} out=${r37(out)} ----`);
10280
11821
  if (ly.type === "group") {
10281
- L.push(` var ${v} = ${cv}.layers.addNull(${r35(c3.duration)});`);
11822
+ L.push(` var ${v} = ${cv}.layers.addNull(${r37(c3.duration)});`);
10282
11823
  L.push(` ${v}.name = "${name}";`);
10283
- L.push(` ${v}.inPoint = ${r35(inn)}; ${v}.outPoint = ${r35(out)};`);
11824
+ L.push(` ${v}.inPoint = ${r37(inn)}; ${v}.outPoint = ${r37(out)};`);
10284
11825
  L.push(` ${v}.property("ADBE Transform Group").property("ADBE Anchor Point").setValue([0,0]);`);
10285
11826
  L.push(` ${v}.property("ADBE Transform Group").property("ADBE Position").setValue([0,0]);`);
10286
11827
  if (parentNullVar)
@@ -10300,18 +11841,18 @@ function emitLayer(ctx, ir, ly, parentNullVar) {
10300
11841
  const srcOffset = num(footageHit.srcOffset, 0);
10301
11842
  const cvv = `cv${id}`;
10302
11843
  coverExpr = cvv;
10303
- L.push(` // 素材注入(${esc(String(ly.type))}): footage srcOffset=${r35(srcOffset)}`);
11844
+ L.push(` // 素材注入(${esc(String(ly.type))}): footage srcOffset=${r37(srcOffset)}`);
10304
11845
  L.push(` var ${v} = null, ${cvv} = 1;`);
10305
11846
  L.push(` if (${fgVar} != null) {`);
10306
11847
  L.push(` try {`);
10307
11848
  L.push(` ${v} = ${cv}.layers.add(${fgVar});`);
10308
11849
  L.push(` var _fw = ${fgVar}.width || ${slotW}, _fh = ${fgVar}.height || ${slotH};`);
10309
11850
  L.push(` ${cvv} = Math.max(${slotW} / _fw, ${slotH} / _fh);`);
10310
- L.push(` ${v}.startTime = ${r35(inn)} - ${r35(srcOffset)};`);
11851
+ L.push(` ${v}.startTime = ${r37(inn)} - ${r37(srcOffset)};`);
10311
11852
  L.push(` } catch (e) { ${v} = null; }`);
10312
11853
  L.push(` }`);
10313
11854
  L.push(` if (${v} == null) {`);
10314
- L.push(` ${v} = ${cv}.layers.addSolid([0.35,0.35,0.4], "${name} [占位]", ${slotW}, ${slotH}, 1, ${r35(c3.duration)});`);
11855
+ L.push(` ${v} = ${cv}.layers.addSolid([0.35,0.35,0.4], "${name} [占位]", ${slotW}, ${slotH}, 1, ${r37(c3.duration)});`);
10315
11856
  L.push(` ${cvv} = 1;`);
10316
11857
  L.push(` }`);
10317
11858
  } else if (ly.type === "text") {
@@ -10332,15 +11873,15 @@ function emitLayer(ctx, ir, ly, parentNullVar) {
10332
11873
  const h = Math.max(2, Math.round(num(ly.h, 400)));
10333
11874
  if (ly.shape === "ellipse")
10334
11875
  L.push(` // TODO: 原层为椭圆形状,固态占位,可手动换 shape layer`);
10335
- L.push(` var ${v} = ${cv}.layers.addSolid([${col.join(",")}], "${name}", ${w}, ${h}, 1, ${r35(c3.duration)});`);
11876
+ L.push(` var ${v} = ${cv}.layers.addSolid([${col.join(",")}], "${name}", ${w}, ${h}, 1, ${r37(c3.duration)});`);
10336
11877
  } else {
10337
11878
  const w = Math.max(2, Math.round(num(ly.w, c3.w)));
10338
11879
  const h = Math.max(2, Math.round(num(ly.h, c3.h)));
10339
11880
  L.push(` // 占位素材(${esc(String(ly.type))}${ly.asset ? ` asset=${esc(String(ly.asset))}` : ""}): 请替换为真实素材`);
10340
- L.push(` var ${v} = ${cv}.layers.addSolid([0.35,0.35,0.4], "${name} [占位]", ${w}, ${h}, 1, ${r35(c3.duration)});`);
11881
+ L.push(` var ${v} = ${cv}.layers.addSolid([0.35,0.35,0.4], "${name} [占位]", ${w}, ${h}, 1, ${r37(c3.duration)});`);
10341
11882
  }
10342
11883
  L.push(` ${v}.name = "${name}";`);
10343
- L.push(` ${v}.inPoint = ${r35(inn)}; ${v}.outPoint = ${r35(out)};`);
11884
+ L.push(` ${v}.inPoint = ${r37(inn)}; ${v}.outPoint = ${r37(out)};`);
10344
11885
  if (parentNullVar)
10345
11886
  L.push(` try { ${v}.parent = ${parentNullVar}; } catch (e) {}`);
10346
11887
  if (ly.blend && ly.blend !== "normal") {
@@ -10350,7 +11891,7 @@ function emitLayer(ctx, ir, ly, parentNullVar) {
10350
11891
  L.push(` try { ${v}.blendingMode = BlendingMode.${bm}; } catch (e) {}`);
10351
11892
  }
10352
11893
  const tf = `${v}.property("ADBE Transform Group")`;
10353
- L.push(` ${tf}.property("ADBE Position").setValue([${r35(px)}, ${r35(py)}]);`);
11894
+ L.push(` ${tf}.property("ADBE Position").setValue([${r37(px)}, ${r37(py)}]);`);
10354
11895
  const anim = ly.anim ?? {};
10355
11896
  const bakedProps = new Set(ctx.bakeOps ? bakeLayerOps(ly).tracks.map((t) => t.prop) : []);
10356
11897
  if ((anim.x?.length || anim.y?.length) && !bakedProps.has("position")) {
@@ -10365,10 +11906,10 @@ function emitLayer(ctx, ir, ly, parentNullVar) {
10365
11906
  }
10366
11907
  }
10367
11908
  if (anim.rot?.length && !bakedProps.has("rotation")) {
10368
- emitKeys(ctx, `${tf}.property("ADBE Rotate Z")`, anim.rot.map((k) => ({ t: k.t, value: `${r35(num(k.v, 0))}`, e: k.e })));
11909
+ emitKeys(ctx, `${tf}.property("ADBE Rotate Z")`, anim.rot.map((k) => ({ t: k.t, value: `${r37(num(k.v, 0))}`, e: k.e })));
10369
11910
  }
10370
11911
  if (anim.opacity?.length) {
10371
- 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 })));
11912
+ 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 })));
10372
11913
  }
10373
11914
  if (anim.ls?.length) {
10374
11915
  L.push(` // TODO: letterspacing 轨未自动映射(AE 需 Animator>Tracking),共 ${anim.ls.length} 帧`);
@@ -10386,7 +11927,7 @@ function emitGroupAnim(ctx, v, ly) {
10386
11927
  const py = num(ly.pos?.[1], 0);
10387
11928
  if (anim.x?.length || anim.y?.length) {
10388
11929
  const merged = mergeChannelTracks(anim.x, anim.y, px, py);
10389
- 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 })));
11930
+ 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 })));
10390
11931
  }
10391
11932
  for (const ch of ["scale", "rot", "opacity"]) {
10392
11933
  if (anim[ch]?.length) {
@@ -10462,7 +12003,7 @@ function madJsx(opts) {
10462
12003
  totalDur = Math.max(totalDur, win.dropAt + Math.max(0.01, len));
10463
12004
  }
10464
12005
  L.push(``);
10465
- L.push(` var master = app.project.items.addComp("${masterName}", ${mw}, ${mh}, 1, ${r35(totalDur)}, ${r35(mfps)});`);
12006
+ L.push(` var master = app.project.items.addComp("${masterName}", ${mw}, ${mh}, 1, ${r37(totalDur)}, ${r37(mfps)});`);
10466
12007
  L.push(` try { master.bgColor = [0.04,0.04,0.07]; } catch (e) {}`);
10467
12008
  const subVars = [];
10468
12009
  windows.forEach((win, wi) => {
@@ -10474,8 +12015,8 @@ function madJsx(opts) {
10474
12015
  const subName = esc(`${win.uid}-${win.seq}`);
10475
12016
  const subVar = `sub${wi}`;
10476
12017
  L.push(``);
10477
- L.push(` // ==== 子合成 #${wi} 技法 ${subName}(源窗 t0=${r35(win.t0)} t1=${r35(win.t1)})====`);
10478
- L.push(` var ${subVar} = app.project.items.addComp("${subName}", ${sw}, ${sh}, 1, ${r35(sdur)}, ${r35(sfps)});`);
12018
+ L.push(` // ==== 子合成 #${wi} 技法 ${subName}(源窗 t0=${r37(win.t0)} t1=${r37(win.t1)})====`);
12019
+ L.push(` var ${subVar} = app.project.items.addComp("${subName}", ${sw}, ${sh}, 1, ${r37(sdur)}, ${r37(sfps)});`);
10479
12020
  const winFootage = { ...win.footage ?? {} };
10480
12021
  const subCtx = {
10481
12022
  lines: L,
@@ -10496,9 +12037,9 @@ function madJsx(opts) {
10496
12037
  const len = Math.max(0.01, win.outLen ?? win.t1 - win.t0);
10497
12038
  const lv = `mL${wi}`;
10498
12039
  L.push(` var ${lv} = master.layers.add(${subVar});`);
10499
- L.push(` ${lv}.startTime = ${r35(win.dropAt - win.t0)};`);
10500
- L.push(` ${lv}.inPoint = ${r35(win.dropAt)};`);
10501
- L.push(` ${lv}.outPoint = ${r35(win.dropAt + len)};`);
12040
+ L.push(` ${lv}.startTime = ${r37(win.dropAt - win.t0)};`);
12041
+ L.push(` ${lv}.inPoint = ${r37(win.dropAt)};`);
12042
+ L.push(` ${lv}.outPoint = ${r37(win.dropAt + len)};`);
10502
12043
  L.push(` try {`);
10503
12044
  L.push(` var _cov = Math.max(${mw} / ${subVar}.width, ${mh} / ${subVar}.height) * 100;`);
10504
12045
  L.push(` ${lv}.property("ADBE Transform Group").property("ADBE Scale").setValue([_cov, _cov]);`);
@@ -10520,7 +12061,7 @@ function madJsx(opts) {
10520
12061
  L.push(` var mk = master.property("ADBE Marker");`);
10521
12062
  for (const m of markers) {
10522
12063
  const label = m.downbeat ? "downbeat" : "beat";
10523
- L.push(` mk.setValueAtTime(${r35(m.t)}, new MarkerValue("${label}"));`);
12064
+ L.push(` mk.setValueAtTime(${r37(m.t)}, new MarkerValue("${label}"));`);
10524
12065
  }
10525
12066
  L.push(` } catch (e) {}`);
10526
12067
  }
@@ -10536,7 +12077,7 @@ function madJsx(opts) {
10536
12077
 
10537
12078
  // src/lib/mad/scan.ts
10538
12079
  import { readdirSync, statSync as statSync2 } from "node:fs";
10539
- import { extname as extname6, join as join22 } from "node:path";
12080
+ import { extname as extname6, join as join23 } from "node:path";
10540
12081
  var VIDEO_EXTS2 = new Set(defaultExtsFor("video") ?? []);
10541
12082
  function scanFolder(dirAbs, opts = {}) {
10542
12083
  const probe = opts.probe ?? probeGeometry;
@@ -10547,7 +12088,7 @@ function scanFolder(dirAbs, opts = {}) {
10547
12088
  } catch {
10548
12089
  throw new Error(`素材文件夹不存在或无法读取:${dirAbs}`);
10549
12090
  }
10550
- const files = entries.filter((n) => VIDEO_EXTS2.has(extname6(n).toLowerCase())).map((n) => join22(dirAbs, n)).filter((p) => {
12091
+ const files = entries.filter((n) => VIDEO_EXTS2.has(extname6(n).toLowerCase())).map((n) => join23(dirAbs, n)).filter((p) => {
10551
12092
  try {
10552
12093
  return statSync2(p).isFile();
10553
12094
  } catch {
@@ -10677,7 +12218,7 @@ function selectWindows(opts) {
10677
12218
 
10678
12219
  // src/lib/mad/beat.ts
10679
12220
  var clamp2 = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
10680
- var r36 = (v) => Math.round(v * 1000) / 1000;
12221
+ var r38 = (v) => Math.round(v * 1000) / 1000;
10681
12222
  var MIN_WIN = 0.4;
10682
12223
  var MAX_WIN = 6;
10683
12224
  function fixedRhythm(natLens) {
@@ -10685,7 +12226,7 @@ function fixedRhythm(natLens) {
10685
12226
  let t = 0;
10686
12227
  for (const nl of natLens) {
10687
12228
  const outLen = clamp2(nl, MIN_WIN, MAX_WIN);
10688
- placements.push({ dropAt: r36(t), outLen: r36(outLen) });
12229
+ placements.push({ dropAt: r38(t), outLen: r38(outLen) });
10689
12230
  t += outLen;
10690
12231
  }
10691
12232
  return { placements, markers: [] };
@@ -10695,7 +12236,7 @@ function beatQuantized(natLens, analysis) {
10695
12236
  const bts = (analysis.beats ?? []).filter((t2) => Number.isFinite(t2) && t2 >= 0).sort((a, b) => a - b);
10696
12237
  const dbSpanOk = dbs.length >= 2 && dbs[1] - dbs[0] <= MAX_WIN;
10697
12238
  const snap = dbSpanOk ? dbs : bts;
10698
- const snapSet = new Set(dbs.map((t2) => r36(t2)));
12239
+ const snapSet = new Set(dbs.map((t2) => r38(t2)));
10699
12240
  if (snap.length < 2) {
10700
12241
  return { plan: fixedRhythm(natLens), level: 2 };
10701
12242
  }
@@ -10708,27 +12249,27 @@ function beatQuantized(natLens, analysis) {
10708
12249
  si++;
10709
12250
  if (si >= snap.length) {
10710
12251
  const outLen = clamp2(natLens[i], MIN_WIN, MAX_WIN);
10711
- placements.push({ dropAt: r36(dropAt), outLen: r36(outLen) });
12252
+ placements.push({ dropAt: r38(dropAt), outLen: r38(outLen) });
10712
12253
  t = dropAt + outLen;
10713
12254
  continue;
10714
12255
  }
10715
12256
  const nextSnap = snap[si];
10716
12257
  const slotLen = clamp2(nextSnap - dropAt, MIN_WIN, MAX_WIN);
10717
- placements.push({ dropAt: r36(dropAt), outLen: r36(slotLen) });
12258
+ placements.push({ dropAt: r38(dropAt), outLen: r38(slotLen) });
10718
12259
  t = dropAt + slotLen;
10719
12260
  si++;
10720
12261
  }
10721
12262
  const totalDur = placements.length ? placements[placements.length - 1].dropAt + placements[placements.length - 1].outLen : 0;
10722
- const allBeats = [...new Set([...bts, ...dbs].map((x) => r36(x)))].sort((a, b) => a - b);
12263
+ const allBeats = [...new Set([...bts, ...dbs].map((x) => r38(x)))].sort((a, b) => a - b);
10723
12264
  const markers = allBeats.filter((tt) => tt >= 0 && tt <= totalDur + 0.000001).map((tt) => ({ t: tt, downbeat: snapSet.has(tt) }));
10724
12265
  return { plan: { placements, markers }, level: 1 };
10725
12266
  }
10726
12267
 
10727
12268
  // src/lib/mad/data.ts
10728
12269
  import { createHash as createHash2 } from "node:crypto";
10729
- import { mkdir as mkdir9, readFile as readFile7, rename, rm, writeFile as writeFile9, readdir } from "node:fs/promises";
10730
- import { existsSync as existsSync18 } from "node:fs";
10731
- import { join as join23 } from "node:path";
12270
+ import { mkdir as mkdir9, readFile as readFile8, rename as rename2, rm, writeFile as writeFile9, readdir } from "node:fs/promises";
12271
+ import { existsSync as existsSync20 } from "node:fs";
12272
+ import { join as join24 } from "node:path";
10732
12273
  function madCacheDir() {
10733
12274
  return homeFile("mad-cache");
10734
12275
  }
@@ -10754,13 +12295,13 @@ async function fetchWithTimeout(fetchFn, url, timeoutMs) {
10754
12295
  async function atomicWrite(dest, data) {
10755
12296
  const tmp = `${dest}.tmp-${process.pid}-${Date.now()}`;
10756
12297
  await writeFile9(tmp, data);
10757
- await rename(tmp, dest);
12298
+ await rename2(tmp, dest);
10758
12299
  }
10759
12300
  async function verifyFile(path, sha256) {
10760
- if (!existsSync18(path))
12301
+ if (!existsSync20(path))
10761
12302
  return false;
10762
12303
  try {
10763
- const buf = await readFile7(path);
12304
+ const buf = await readFile8(path);
10764
12305
  return sha256Hex(buf) === sha256;
10765
12306
  } catch {
10766
12307
  return false;
@@ -10785,7 +12326,7 @@ async function cleanupOldVersions(cacheRoot, keepVersion, warn) {
10785
12326
  for (const e of entries) {
10786
12327
  const m = /^v(\d+)$/.exec(e);
10787
12328
  if (m && Number(m[1]) !== keepVersion) {
10788
- await rm(join23(cacheRoot, e), { recursive: true, force: true }).catch(() => {});
12329
+ await rm(join24(cacheRoot, e), { recursive: true, force: true }).catch(() => {});
10789
12330
  }
10790
12331
  }
10791
12332
  } catch {}
@@ -10794,7 +12335,7 @@ async function ensureMadData(opts, deps) {
10794
12335
  const { cacheRoot, warn } = deps;
10795
12336
  const timeout = deps.manifestTimeoutMs ?? 8000;
10796
12337
  const mfUrl = deps.manifestUrl ?? manifestUrl();
10797
- const snapshotPath = join23(cacheRoot, "manifest.json");
12338
+ const snapshotPath = join24(cacheRoot, "manifest.json");
10798
12339
  let manifest = null;
10799
12340
  let online = false;
10800
12341
  try {
@@ -10809,11 +12350,11 @@ async function ensureMadData(opts, deps) {
10809
12350
  warn(`manifest 拉取失败(${e instanceof Error ? e.message : String(e)}),回退本地缓存`);
10810
12351
  }
10811
12352
  if (!manifest) {
10812
- if (!existsSync18(snapshotPath)) {
12353
+ if (!existsSync20(snapshotPath)) {
10813
12354
  throw new Error("首次使用需联网下载技法数据。请连网后重跑 `gtrk tool mad`(数据会缓存到本地,之后可离线出片)。");
10814
12355
  }
10815
12356
  try {
10816
- manifest = validateManifest(JSON.parse(await readFile7(snapshotPath, "utf8")));
12357
+ manifest = validateManifest(JSON.parse(await readFile8(snapshotPath, "utf8")));
10817
12358
  } catch {
10818
12359
  throw new Error("本地 manifest 缓存损坏且当前离线。请连网重跑,或加 --refresh 强制重新下载。");
10819
12360
  }
@@ -10824,14 +12365,14 @@ async function ensureMadData(opts, deps) {
10824
12365
  }
10825
12366
  }
10826
12367
  const version = manifest.version;
10827
- const verDir = join23(cacheRoot, `v${version}`);
10828
- const poolPath = join23(verDir, "mad_pool.json");
12368
+ const verDir = join24(cacheRoot, `v${version}`);
12369
+ const poolPath = join24(verDir, "mad_pool.json");
10829
12370
  const poolMeta = manifest.datasets.mad_pool;
10830
12371
  const cacheValid = await verifyFile(poolPath, poolMeta.sha256);
10831
12372
  const needDownload = !!opts.refresh || !cacheValid;
10832
12373
  if (needDownload) {
10833
12374
  if (!online) {
10834
- if (existsSync18(poolPath)) {
12375
+ if (existsSync20(poolPath)) {
10835
12376
  throw new Error("本地技法数据缓存损坏且当前离线。请连网重跑,或加 --refresh 强制重新下载。");
10836
12377
  }
10837
12378
  throw new Error("首次使用需联网下载技法数据。请连网后重跑 `gtrk tool mad`(数据会缓存到本地,之后可离线出片)。");
@@ -10855,7 +12396,7 @@ async function ensureMadData(opts, deps) {
10855
12396
  }
10856
12397
  let pool;
10857
12398
  try {
10858
- pool = JSON.parse(await readFile7(poolPath, "utf8"));
12399
+ pool = JSON.parse(await readFile8(poolPath, "utf8"));
10859
12400
  if (!Array.isArray(pool))
10860
12401
  throw new Error("mad_pool 非数组");
10861
12402
  } catch (e) {
@@ -10866,11 +12407,11 @@ async function ensureMadData(opts, deps) {
10866
12407
 
10867
12408
  // src/lib/mad/pool.ts
10868
12409
  import { gunzipSync } from "node:zlib";
10869
- import { mkdir as mkdir10, readFile as readFile8 } from "node:fs/promises";
10870
- import { existsSync as existsSync19 } from "node:fs";
10871
- import { join as join24 } from "node:path";
12410
+ import { mkdir as mkdir10, readFile as readFile9 } from "node:fs/promises";
12411
+ import { existsSync as existsSync21 } from "node:fs";
12412
+ import { join as join25 } from "node:path";
10872
12413
  function shardPath(verDir, shard) {
10873
- return join24(verDir, "ir", `${shard}.json.gz`);
12414
+ return join25(verDir, "ir", `${shard}.json.gz`);
10874
12415
  }
10875
12416
  function decodeShard(gz) {
10876
12417
  const json = gunzipSync(gz).toString("utf8");
@@ -10887,9 +12428,9 @@ function makeIrLoader(assetsBase, verDir, online, deps) {
10887
12428
  if (cached)
10888
12429
  return cached;
10889
12430
  const path = shardPath(verDir, shard);
10890
- if (existsSync19(path)) {
12431
+ if (existsSync21(path)) {
10891
12432
  try {
10892
- const s2 = decodeShard(await readFile8(path));
12433
+ const s2 = decodeShard(await readFile9(path));
10893
12434
  memo.set(shard, s2);
10894
12435
  return s2;
10895
12436
  } catch {
@@ -10905,7 +12446,7 @@ function makeIrLoader(assetsBase, verDir, online, deps) {
10905
12446
  throw new Error(`IR 分片下载失败 HTTP ${res.status}:${url}`);
10906
12447
  const buf = new Uint8Array(await res.arrayBuffer());
10907
12448
  const s = decodeShard(buf);
10908
- await mkdir10(join24(verDir, "ir"), { recursive: true });
12449
+ await mkdir10(join25(verDir, "ir"), { recursive: true });
10909
12450
  await atomicWrite(path, buf);
10910
12451
  memo.set(shard, s);
10911
12452
  return s;
@@ -10919,7 +12460,7 @@ function makeIrLoader(assetsBase, verDir, online, deps) {
10919
12460
  return ir;
10920
12461
  },
10921
12462
  shardCached(shard) {
10922
- return memo.has(shard) || existsSync19(shardPath(verDir, shard));
12463
+ return memo.has(shard) || existsSync21(shardPath(verDir, shard));
10923
12464
  }
10924
12465
  };
10925
12466
  }
@@ -10941,7 +12482,7 @@ async function analyzeBgm(cfg, bgmAbs, deps) {
10941
12482
  uploadCached: deps.uploadCached,
10942
12483
  invalidateUpload: deps.invalidateUpload,
10943
12484
  submitTask: deps.submitTask,
10944
- sleep: deps.sleep ?? ((ms) => new Promise((resolve11) => setTimeout(resolve11, ms)))
12485
+ sleep: deps.sleep ?? ((ms) => new Promise((resolve12) => setTimeout(resolve12, ms)))
10945
12486
  });
10946
12487
  const { taskId } = submitted;
10947
12488
  const output = await deps.pollToolTask(cfg, ANALYZE_TASK, taskId, {});
@@ -11002,8 +12543,8 @@ async function runMad(inputArg, opts, deps = {}) {
11002
12543
  const probeDur = deps.probeDurationFn ?? probeDuration;
11003
12544
  if (!inputArg)
11004
12545
  throw new Error("用法:gtrk tool mad <素材文件夹> [--bgm 歌.mp3]");
11005
- const dirAbs = resolve11(inputArg);
11006
- if (!existsSync20(dirAbs) || !statSync3(dirAbs).isDirectory()) {
12546
+ const dirAbs = resolve12(inputArg);
12547
+ if (!existsSync22(dirAbs) || !statSync3(dirAbs).isDirectory()) {
11007
12548
  throw new Error(`素材文件夹不存在或不是目录:${dirAbs}`);
11008
12549
  }
11009
12550
  const { videos, orientation, skipped } = scanFolder(dirAbs, {
@@ -11034,7 +12575,7 @@ async function runMad(inputArg, opts, deps = {}) {
11034
12575
  let level = 3;
11035
12576
  let analysis = null;
11036
12577
  let bgmForTrack;
11037
- const bgmAbs = opts.bgm ? resolve11(opts.bgm) : undefined;
12578
+ const bgmAbs = opts.bgm ? resolve12(opts.bgm) : undefined;
11038
12579
  if (bgmAbs) {
11039
12580
  let dur = -1;
11040
12581
  try {
@@ -11117,9 +12658,9 @@ async function runMad(inputArg, opts, deps = {}) {
11117
12658
  const header = madHeader(version, now.toISOString());
11118
12659
  const bgm = level <= 2 && bgmForTrack ? { path: bgmForTrack, markers } : undefined;
11119
12660
  const { jsx } = madJsx({ master, windows, header, bgm });
11120
- const outDir = opts.out ? resolve11(opts.out) : join25(process.cwd(), `mad-${timestamp4(now)}`);
12661
+ const outDir = opts.out ? resolve12(opts.out) : join26(process.cwd(), `mad-${timestamp4(now)}`);
11121
12662
  await mkdir11(outDir, { recursive: true });
11122
- const jsxPath = join25(outDir, "mad.jsx");
12663
+ const jsxPath = join26(outDir, "mad.jsx");
11123
12664
  await writeFile10(jsxPath, jsx);
11124
12665
  const result = {
11125
12666
  ok: true,
@@ -11131,7 +12672,7 @@ async function runMad(inputArg, opts, deps = {}) {
11131
12672
  degradeLevel: level,
11132
12673
  techniques: chosen.map((c3) => ({ uid: c3.entry.uid, pid: c3.entry.pid, cat: c3.entry.cat, t0: c3.entry.t0, t1: c3.entry.t1 }))
11133
12674
  };
11134
- await writeFile10(join25(outDir, "result.json"), JSON.stringify({ ...result, finishedAt: now.toISOString() }, null, 2));
12675
+ await writeFile10(join26(outDir, "result.json"), JSON.stringify({ ...result, finishedAt: now.toISOString() }, null, 2));
11135
12676
  warn(completionMessage(jsxPath, level));
11136
12677
  return result;
11137
12678
  }
@@ -11263,9 +12804,9 @@ async function runMadInTool(inputArg, opts) {
11263
12804
  }
11264
12805
 
11265
12806
  // src/commands/transcript.ts
11266
- import { existsSync as existsSync21 } from "node:fs";
11267
- import { mkdir as mkdir12, rename as rename2, rm as rm2, stat as stat6, writeFile as writeFile11 } from "node:fs/promises";
11268
- import { basename as basename12, dirname as dirname10, extname as extname7, join as join26, resolve as resolve12 } from "node:path";
12807
+ import { existsSync as existsSync23 } from "node:fs";
12808
+ import { mkdir as mkdir12, rename as rename3, rm as rm2, stat as stat6, writeFile as writeFile11 } from "node:fs/promises";
12809
+ import { basename as basename13, dirname as dirname10, extname as extname7, join as join27, resolve as resolve13 } from "node:path";
11269
12810
 
11270
12811
  // src/lib/transcript.ts
11271
12812
  var AGENT_SUMMARY_PENDING = "<!-- gtrk:agent-summary-pending -->";
@@ -11386,7 +12927,7 @@ function buildDeps(overrides = {}) {
11386
12927
  upload: overrides.upload ?? uploadCached,
11387
12928
  invalidate: overrides.invalidate ?? invalidateUpload,
11388
12929
  submit: overrides.submit ?? submitTask,
11389
- sleep: overrides.sleep ?? ((ms) => new Promise((resolve13) => setTimeout(resolve13, ms))),
12930
+ sleep: overrides.sleep ?? ((ms) => new Promise((resolve14) => setTimeout(resolve14, ms))),
11390
12931
  poll: overrides.poll ?? (async (cfg, taskType, taskId, onTick) => await pollToolTask(cfg, taskType, taskId, { onTick })),
11391
12932
  writeMarkdown: overrides.writeMarkdown ?? writeMarkdownAtomic,
11392
12933
  now: overrides.now ?? (() => new Date)
@@ -11401,8 +12942,8 @@ async function validateTranscriptInput(input) {
11401
12942
  if (looksLikeRemote(input)) {
11402
12943
  throw new Error("视频转文字稿仅支持本地视频文件,不支持 URL、平台视频地址或远端下载");
11403
12944
  }
11404
- const inputAbs = resolve12(input);
11405
- if (!existsSync21(inputAbs))
12945
+ const inputAbs = resolve13(input);
12946
+ if (!existsSync23(inputAbs))
11406
12947
  throw new Error(`本地视频不存在:${inputAbs}`);
11407
12948
  const info = await stat6(inputAbs);
11408
12949
  if (!info.isFile())
@@ -11414,8 +12955,8 @@ async function validateTranscriptInput(input) {
11414
12955
  return inputAbs;
11415
12956
  }
11416
12957
  function resolveTranscriptOutput(inputAbs, out) {
11417
- const base = basename12(inputAbs, extname7(inputAbs));
11418
- const output = out ? resolve12(out) : join26(dirname10(inputAbs), `${base}-transcript.md`);
12958
+ const base = basename13(inputAbs, extname7(inputAbs));
12959
+ const output = out ? resolve13(out) : join27(dirname10(inputAbs), `${base}-transcript.md`);
11419
12960
  if (extname7(output).toLowerCase() !== ".md")
11420
12961
  throw new Error("--out 必须指向一个 .md 文件");
11421
12962
  return output;
@@ -11425,7 +12966,7 @@ async function writeMarkdownAtomic(path, markdown) {
11425
12966
  const temp = `${path}.${process.pid}.${Math.random().toString(16).slice(2)}.tmp`;
11426
12967
  try {
11427
12968
  await writeFile11(temp, markdown, "utf8");
11428
- await rename2(temp, path);
12969
+ await rename3(temp, path);
11429
12970
  } finally {
11430
12971
  await rm2(temp, { force: true });
11431
12972
  }
@@ -11437,8 +12978,8 @@ async function runTranscript(input, opts = {}, depsOverride) {
11437
12978
  const output = resolveTranscriptOutput(inputAbs, opts.out);
11438
12979
  const deps = buildDeps(depsOverride);
11439
12980
  const language = opts.lang?.trim() || "zh-CN";
11440
- const sourceName = basename12(inputAbs);
11441
- const title = basename12(inputAbs, extname7(inputAbs));
12981
+ const sourceName = basename13(inputAbs);
12982
+ const title = basename13(inputAbs, extname7(inputAbs));
11442
12983
  log.step(`▶ 视频转文字稿:${sourceName}`);
11443
12984
  log.step("① 本地探测视频…");
11444
12985
  const geometry = deps.probe(inputAbs, opts.ffmpegPath);
@@ -11450,7 +12991,7 @@ async function runTranscript(input, opts = {}, depsOverride) {
11450
12991
  log.step("② 本地抽取 16k 单声道音频(原视频不上传)…");
11451
12992
  const audio = await deps.extract(inputAbs, opts.ffmpegPath);
11452
12993
  deps.assertDuration(geometry.duration, audio, opts.ffmpegPath);
11453
- log.info(`上传物:${basename12(audio)}(仅音频衍生物)`);
12994
+ log.info(`上传物:${basename13(audio)}(仅音频衍生物)`);
11454
12995
  log.step("③ 上传音频并提交 ASR…");
11455
12996
  const payload = (fileId) => ({ file_id: fileId, language, word_level: true });
11456
12997
  const submitted = await uploadAndSubmitTask(deps.cfg, audio, TASK_TYPE3, payload, {
@@ -11498,9 +13039,9 @@ function registerTranscript(program2) {
11498
13039
  }
11499
13040
 
11500
13041
  // src/commands/music-visualizer.ts
11501
- import { resolve as resolve13, join as join27, dirname as dirname11, basename as basename13, extname as extname8 } from "node:path";
13042
+ import { resolve as resolve14, join as join28, dirname as dirname11, basename as basename14, extname as extname8 } from "node:path";
11502
13043
  import { mkdir as mkdir13, writeFile as writeFile12 } from "node:fs/promises";
11503
- import { existsSync as existsSync22 } from "node:fs";
13044
+ import { existsSync as existsSync24 } from "node:fs";
11504
13045
  var TASK_TYPE4 = "music_visualizer";
11505
13046
  var PRICE_KEY2 = "music_visualizer";
11506
13047
  var HEX_RE = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
@@ -11548,7 +13089,7 @@ function timestamp5() {
11548
13089
  return `${p(d.getFullYear() % 100)}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
11549
13090
  }
11550
13091
  function assertExt(pathAbs, exts, label) {
11551
- if (!existsSync22(pathAbs))
13092
+ if (!existsSync24(pathAbs))
11552
13093
  throw new Error(`${label}不存在:${pathAbs}`);
11553
13094
  const e = extname8(pathAbs).toLowerCase();
11554
13095
  if (exts.length && !exts.includes(e)) {
@@ -11604,22 +13145,22 @@ async function runMusicVisualizer(audio, opts) {
11604
13145
  if (opts.json)
11605
13146
  routeLogsToStderr();
11606
13147
  const cfg = loadConfig();
11607
- const audioAbs = resolve13(audio);
13148
+ const audioAbs = resolve14(audio);
11608
13149
  assertExt(audioAbs, AUDIO_EXTS2, "音频");
11609
13150
  const template = opts.template == null ? "" : String(opts.template).trim();
11610
13151
  if (!template)
11611
13152
  throw new Error("--template 必填:请指定可视化模板 id(取值见云端 API 文档 / 服务端模板列表)");
11612
13153
  const styleFields = buildStyleFields(opts);
11613
- const bgAbs = opts.background ? resolve13(opts.background) : undefined;
13154
+ const bgAbs = opts.background ? resolve14(opts.background) : undefined;
11614
13155
  if (bgAbs)
11615
13156
  assertExt(bgAbs, [...IMAGE_EXTS2, ...VIDEO_EXTS3], "背景素材");
11616
- const coverAbs = opts.cover ? resolve13(opts.cover) : undefined;
13157
+ const coverAbs = opts.cover ? resolve14(opts.cover) : undefined;
11617
13158
  if (coverAbs)
11618
13159
  assertExt(coverAbs, IMAGE_EXTS2, "封面图");
11619
13160
  const extraParams = parseExtraParams3(opts.param, opts.paramsJson);
11620
- const projName = basename13(audioAbs, extname8(audioAbs));
11621
- const outDir = resolve13(opts.out ?? join27(dirname11(audioAbs), `${projName}-visualizer-${timestamp5()}`));
11622
- log.step(`▶ 音乐可视化:${basename13(audioAbs)}(模板 ${template}${bgAbs ? " · 背景" : ""}${coverAbs ? " · 封面" : ""})`);
13161
+ const projName = basename14(audioAbs, extname8(audioAbs));
13162
+ const outDir = resolve14(opts.out ?? join28(dirname11(audioAbs), `${projName}-visualizer-${timestamp5()}`));
13163
+ log.step(`▶ 音乐可视化:${basename14(audioAbs)}(模板 ${template}${bgAbs ? " · 背景" : ""}${coverAbs ? " · 封面" : ""})`);
11623
13164
  let billingHint;
11624
13165
  try {
11625
13166
  billingHint = (await resolveToolPricing(PRICE_KEY2)).billingHint;
@@ -11665,7 +13206,7 @@ async function runMusicVisualizer(audio, opts) {
11665
13206
  const { taskId } = submitted;
11666
13207
  log.info(`task_id = ${taskId}`);
11667
13208
  await mkdir13(outDir, { recursive: true });
11668
- await writeFile12(join27(outDir, "task.json"), JSON.stringify({ taskId, taskType: TASK_TYPE4, fileId: submitted.fileId, source: audioAbs, template, createdAt: new Date().toISOString() }, null, 2));
13209
+ await writeFile12(join28(outDir, "task.json"), JSON.stringify({ taskId, taskType: TASK_TYPE4, fileId: submitted.fileId, source: audioAbs, template, createdAt: new Date().toISOString() }, null, 2));
11669
13210
  log.step("③ 云端处理中(每 5s 轮询)…");
11670
13211
  const result = await pollTask(cfg, TASK_TYPE4, taskId, (status, progress) => {
11671
13212
  log.tick(`${status}${progress != null ? ` ${Math.round(progress)}%` : ""}`);
@@ -11676,7 +13217,7 @@ async function runMusicVisualizer(audio, opts) {
11676
13217
  const files = [];
11677
13218
  const errors = {};
11678
13219
  if (url) {
11679
- const dest = join27(outDir, `${projName}-visualizer${extFromUrl(url, ".mp4")}`);
13220
+ const dest = join28(outDir, `${projName}-visualizer${extFromUrl(url, ".mp4")}`);
11680
13221
  try {
11681
13222
  await downloadStream(url, dest);
11682
13223
  files.push(dest);
@@ -11698,7 +13239,7 @@ async function runMusicVisualizer(audio, opts) {
11698
13239
  ...Object.keys(errors).length ? { errors } : {},
11699
13240
  finishedAt: new Date().toISOString()
11700
13241
  };
11701
- await writeFile12(join27(outDir, "result.json"), JSON.stringify(resultJson, null, 2));
13242
+ await writeFile12(join28(outDir, "result.json"), JSON.stringify(resultJson, null, 2));
11702
13243
  if (opts.json) {
11703
13244
  process.stdout.write(`${JSON.stringify(resultJson)}
11704
13245
  `);
@@ -11716,7 +13257,7 @@ try {
11716
13257
  process.loadEnvFile?.();
11717
13258
  } catch {}
11718
13259
  migrateLegacyHome();
11719
- var { version } = JSON.parse(readFileSync5(join28(packageRoot(), "package.json"), "utf8"));
13260
+ var { version } = JSON.parse(readFileSync5(join29(packageRoot(), "package.json"), "utf8"));
11720
13261
  var program2 = new Command;
11721
13262
  program2.name("gtrk").description("同合云成片流水线 CLI —— agent 驱动云端任务、产物拉回本地、三方工程文件(客户端/剪映/PR)互通").version(version);
11722
13263
  registerInstall(program2);