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