@gitruck/cli 0.2.14 → 0.2.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENT.md +38 -37
- package/README.md +24 -5
- package/contracts/gsap-emit-v1.md +133 -5
- package/dist/index.js +1772 -505
- package/package.json +65 -64
- package/skills/gtrk-matrix/SKILL.md +33 -8
- package/skills/gtrk-mg/SKILL.md +59 -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,150 @@ 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 (
|
|
8373
|
-
|
|
8374
|
-
|
|
8375
|
-
|
|
8376
|
-
const
|
|
8377
|
-
|
|
8378
|
-
|
|
8379
|
-
|
|
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++;
|
|
8380
9261
|
if (c3.pos !== null)
|
|
8381
|
-
maxEnd = Math.max(maxEnd, Number(c3.pos) +
|
|
9262
|
+
maxEnd = Math.max(maxEnd, Number(c3.pos) + span);
|
|
8382
9263
|
else
|
|
8383
|
-
chain +=
|
|
9264
|
+
chain += span;
|
|
9265
|
+
}
|
|
9266
|
+
const raw = Math.max(chain, maxEnd);
|
|
9267
|
+
const est = Number.isFinite(raw) ? Math.round(raw * 1000) / 1000 : raw;
|
|
9268
|
+
return { est, parsed, skipped, hasInfiniteRepeat };
|
|
9269
|
+
}
|
|
9270
|
+
var TL_HOOKS = ["onUpdate", "onStart", "onComplete", "onRepeat"];
|
|
9271
|
+
var DOM_WRITE = /\.setAttribute(?:NS)?\s*\(|\.textContent\s*=(?!=)|\.inner(?:HTML|Text)\s*=(?!=)|\.style\.[A-Za-z_$][\w$]*\s*=(?!=)|\.style\.setProperty\s*\(|\.classList\s*\.\s*(?:add|remove|toggle|replace)\s*\(/;
|
|
9272
|
+
function skipString(src, i) {
|
|
9273
|
+
const q = src[i];
|
|
9274
|
+
for (let j = i + 1;j < src.length; j++) {
|
|
9275
|
+
if (src[j] === "\\") {
|
|
9276
|
+
j++;
|
|
9277
|
+
continue;
|
|
9278
|
+
}
|
|
9279
|
+
if (src[j] === q)
|
|
9280
|
+
return j;
|
|
9281
|
+
}
|
|
9282
|
+
return src.length;
|
|
9283
|
+
}
|
|
9284
|
+
function braceBlock(src, open2) {
|
|
9285
|
+
let depth = 0;
|
|
9286
|
+
for (let i = open2;i < src.length; i++) {
|
|
9287
|
+
const c3 = src[i];
|
|
9288
|
+
if (c3 === '"' || c3 === "'" || c3 === "`") {
|
|
9289
|
+
i = skipString(src, i);
|
|
9290
|
+
continue;
|
|
9291
|
+
}
|
|
9292
|
+
if (c3 === "/" && src[i + 1] === "/") {
|
|
9293
|
+
const nl = src.indexOf(`
|
|
9294
|
+
`, i);
|
|
9295
|
+
i = nl < 0 ? src.length : nl;
|
|
9296
|
+
continue;
|
|
9297
|
+
}
|
|
9298
|
+
if (c3 === "/" && src[i + 1] === "*") {
|
|
9299
|
+
const e = src.indexOf("*/", i + 2);
|
|
9300
|
+
i = e < 0 ? src.length : e + 1;
|
|
9301
|
+
continue;
|
|
9302
|
+
}
|
|
9303
|
+
if (c3 === "{")
|
|
9304
|
+
depth++;
|
|
9305
|
+
else if (c3 === "}" && --depth === 0)
|
|
9306
|
+
return src.slice(open2 + 1, i);
|
|
9307
|
+
}
|
|
9308
|
+
return src.slice(open2 + 1);
|
|
9309
|
+
}
|
|
9310
|
+
function namedFnBody(html, name) {
|
|
9311
|
+
const n = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
9312
|
+
const decl = new RegExp(`\\bfunction\\s+${n}\\s*\\([^)]*\\)\\s*\\{`).exec(html);
|
|
9313
|
+
if (decl)
|
|
9314
|
+
return braceBlock(html, decl.index + decl[0].length - 1);
|
|
9315
|
+
const assign = new RegExp(`\\b(?:var|let|const)\\s+${n}\\s*=\\s*(?:function\\s*[\\w$]*\\s*\\([^)]*\\)|\\([^)]*\\)\\s*=>|[A-Za-z_$][\\w$]*\\s*=>)\\s*\\{`).exec(html);
|
|
9316
|
+
if (assign)
|
|
9317
|
+
return braceBlock(html, assign.index + assign[0].length - 1);
|
|
9318
|
+
return null;
|
|
9319
|
+
}
|
|
9320
|
+
var NOT_A_CALL = new Set(["if", "for", "while", "switch", "catch", "return", "function", "typeof", "new", "delete", "void", "in", "of", "do", "else"]);
|
|
9321
|
+
function bodyWritesDom(html, body, hops, seen) {
|
|
9322
|
+
if (DOM_WRITE.test(body))
|
|
9323
|
+
return true;
|
|
9324
|
+
if (hops <= 0)
|
|
9325
|
+
return false;
|
|
9326
|
+
for (const m of body.matchAll(/\b([A-Za-z_$][\w$]*)\s*\(/g)) {
|
|
9327
|
+
const name = m[1];
|
|
9328
|
+
if (NOT_A_CALL.has(name) || seen.has(name))
|
|
9329
|
+
continue;
|
|
9330
|
+
seen.add(name);
|
|
9331
|
+
const b = namedFnBody(html, name);
|
|
9332
|
+
if (b && bodyWritesDom(html, b, hops - 1, seen))
|
|
9333
|
+
return true;
|
|
8384
9334
|
}
|
|
8385
|
-
|
|
8386
|
-
|
|
9335
|
+
return false;
|
|
9336
|
+
}
|
|
9337
|
+
function detectSeekSignals(html) {
|
|
9338
|
+
const hooks = new Set;
|
|
9339
|
+
const hookRe = new RegExp(`\\b(${TL_HOOKS.join("|")})\\s*:\\s*`, "g");
|
|
9340
|
+
let hm;
|
|
9341
|
+
while (hm = hookRe.exec(html)) {
|
|
9342
|
+
const hook = hm[1];
|
|
9343
|
+
const at = hm.index + hm[0].length;
|
|
9344
|
+
const rest = html.slice(at);
|
|
9345
|
+
let body = null;
|
|
9346
|
+
let mm;
|
|
9347
|
+
if (mm = /^function\s*[A-Za-z_$]?[\w$]*\s*\([^)]*\)\s*\{/.exec(rest)) {
|
|
9348
|
+
body = braceBlock(html, at + mm[0].length - 1);
|
|
9349
|
+
} else if (mm = /^(?:\([^)]*\)|[A-Za-z_$][\w$]*)\s*=>\s*/.exec(rest)) {
|
|
9350
|
+
const after = at + mm[0].length;
|
|
9351
|
+
body = html[after] === "{" ? braceBlock(html, after) : rest.slice(mm[0].length).split(`
|
|
9352
|
+
`)[0];
|
|
9353
|
+
} else if (mm = /^([A-Za-z_$][\w$]*)/.exec(rest)) {
|
|
9354
|
+
body = namedFnBody(html, mm[1]);
|
|
9355
|
+
}
|
|
9356
|
+
if (body && bodyWritesDom(html, body, 2, new Set))
|
|
9357
|
+
hooks.add(hook);
|
|
9358
|
+
}
|
|
9359
|
+
const overrideForms = [];
|
|
9360
|
+
if (/\.\s*seek\s*=(?!=)|\[\s*["']seek["']\s*\]\s*=(?!=)/.test(html))
|
|
9361
|
+
overrideForms.push("重新赋值 `.seek`");
|
|
9362
|
+
for (const rm of html.matchAll(/window\s*\.\s*__timelines\s*\[[^\]]*\]\s*=\s*([^;\n]*)/g)) {
|
|
9363
|
+
const rhs = rm[1].split("//")[0].trim();
|
|
9364
|
+
if (rhs && !/^[A-Za-z_$][\w$]*$/.test(rhs)) {
|
|
9365
|
+
overrideForms.push("`__timelines[…]` 被赋成包装对象而非时间线本体");
|
|
9366
|
+
break;
|
|
9367
|
+
}
|
|
9368
|
+
}
|
|
9369
|
+
return {
|
|
9370
|
+
callbackDom: hooks.size > 0,
|
|
9371
|
+
hooks: TL_HOOKS.filter((h) => hooks.has(h)),
|
|
9372
|
+
engineApiOverride: overrideForms.length > 0,
|
|
9373
|
+
overrideForms
|
|
9374
|
+
};
|
|
8387
9375
|
}
|
|
8388
9376
|
function lintParticle(html, opts = {}) {
|
|
8389
9377
|
const v = [];
|
|
@@ -8394,6 +9382,8 @@ function lintParticle(html, opts = {}) {
|
|
|
8394
9382
|
const cid = root ? attr(root, "data-composition-id") : undefined;
|
|
8395
9383
|
if (!root || !cid)
|
|
8396
9384
|
push("1-composition-id", true, "根元素缺 data-composition-id");
|
|
9385
|
+
if (opts.compositionId && cid && opts.compositionId !== cid)
|
|
9386
|
+
push("1-cid-expect", true, `HTML 内 data-composition-id「${cid}」与期望 id「${opts.compositionId}」不符(复制改名漏改内部 id?)——` + `按期望 id 落轨会写出 clip_id/material 指向「${opts.compositionId}」,而本文件注册的是 __timelines["${cid}"],渲染必错`);
|
|
8397
9387
|
if (root) {
|
|
8398
9388
|
if (attr(root, "data-width") !== "1920")
|
|
8399
9389
|
push("1-width", true, `根 data-width 应为 "1920"(实为 ${attr(root, "data-width") ?? "缺"})`);
|
|
@@ -8441,24 +9431,40 @@ function lintParticle(html, opts = {}) {
|
|
|
8441
9431
|
if (re.test(html))
|
|
8442
9432
|
push("4-rel-asset", true, `含相对外链 ${tag}(违反自包含,渲染机读不到)`);
|
|
8443
9433
|
}
|
|
8444
|
-
const { opaque, declared } = deriveOpaque(root);
|
|
9434
|
+
const { opaque, declared, solidOnRoot, solidOnChild } = deriveOpaque(root, firstChildTag(html, root));
|
|
8445
9435
|
if (root && !declared)
|
|
8446
|
-
push("4-bg-explicit", false, "
|
|
9436
|
+
push("4-bg-explicit", false, "根与根下首个全幅子层**都**没有显式声明 background(契约铁律4③:透明与否 MUST 显式)——" + "满屏颗粒应在根下第一个全幅子层(position:absolute;inset:0)声明实心底;" + "透明叠加颗粒应在根显式写 background:transparent。缺省按透明叠加 opaque=false 处理");
|
|
9437
|
+
if (solidOnRoot && !solidOnChild)
|
|
9438
|
+
push("4-bg-on-root", false, "实心底写在**根 style** 上(契约铁律4①②:MUST 下沉为根下第一个全幅子层,根 MUST 保持零视觉)——" + "**根元素的绘制属性会在子合成挂载时被丢弃**,这层底在成片里一个像素都不落地:" + "前景照常渲出、底没了、底轨透出来,观感是「浮空面板」;而本地播放器与客户端预览都看不出(2026-07-26 r69 真渲实测,见契约铁律4证据锚)。" + "改法是机械的:把这条 background 声明整体搬到根下新增的第一个全幅子层" + '(<div style="position:absolute;inset:0;background:<原底色>;z-index:0;"></div>),根上改写 background:transparent');
|
|
8447
9439
|
if (/var\(\s*--/.test(html))
|
|
8448
9440
|
push("6-css-var", true, "含 CSS var(--...)(Hyperframes 不解析→整片全黑,须字面值)");
|
|
8449
|
-
const
|
|
8450
|
-
if (
|
|
8451
|
-
push("x-
|
|
9441
|
+
const sig = detectSeekSignals(html);
|
|
9442
|
+
if (sig.engineApiOverride)
|
|
9443
|
+
push("x-engine-api-override", false, `颗粒在运行时覆写了渲染引擎所调用的 API(${sig.overrideForms.join("、")})——它会推翻引擎**显式**传入的 ` + `seek(t, true)(颗粒无权静默否决引擎意图),且只在引擎恰好走 seek 时有效,引擎改走 time() / progress() 即完全失效。` + `按契约「回调与 seek 语义」,回调可达性应由**引擎侧**承担,颗粒侧垫片属过渡态——` + `引擎侧结论已于 2026-07-26 经真机核实(引擎 producer 0.6.101 定帧时回调可达),既有垫片可择期清理(删后须重渲复验),新颗粒不应再加`);
|
|
9444
|
+
if (sig.callbackDom && !sig.engineApiOverride)
|
|
9445
|
+
push("x-callback-driven", false, `画面靠时间线回调驱动(${sig.hooks.join(" / ")} 体内写 DOM),且本颗粒无任何 seek 兜底。` + `该写法**合规**——其可达性由契约「回调与 seek 语义」的**引擎侧 MUST 条款**保证` + `(2026-07-26 已真机核实:引擎 producer 0.6.101 定帧时回调可达;结论**绑定该引擎版本**),` + `作者不必也不应为此加装 tl.seek 垫片。本项只是**哨兵**:引擎若失守或换实现,补间属性照常插值、回调不跑 → ` + `画面会静默定格在初始态(不是黑屏,本地播放器与客户端预览都看不出)`);
|
|
9446
|
+
const timers = [
|
|
9447
|
+
/\brequestAnimationFrame\s*\(/.test(html) ? "requestAnimationFrame(" : null,
|
|
9448
|
+
/\bsetInterval\s*\(/.test(html) ? "setInterval(" : null
|
|
9449
|
+
].filter(Boolean);
|
|
9450
|
+
if (timers.length)
|
|
9451
|
+
push("x-raf-interval", false, `含 ${timers.join(" 与 ")}——这类自有时钟**不被 seek 驱动**,逐帧渲染时等于冻结(契约:所有视觉变化必须挂在 tl 上)。` + `静态正则分不清「驱动画面」与其它用途(如一次性布局测量),故只提醒、不拦`);
|
|
9452
|
+
if (opts.dispatchIds && cid && !opts.dispatchIds.includes(cid))
|
|
9453
|
+
push("x-dispatch", false, `composition_id "${cid}" 不在 dispatch.mg 派单中`);
|
|
8452
9454
|
if (opts.category && opts.category in CATEGORY_EXPECTED_OPAQUE2) {
|
|
8453
9455
|
const expect = CATEGORY_EXPECTED_OPAQUE2[opts.category];
|
|
8454
9456
|
if (expect !== opaque)
|
|
8455
9457
|
push("x-category-opaque", false, `category「${opts.category}」期望${expect ? "不透明满屏" : "透明叠加"},但颗粒 HTML 反推为${opaque ? "不透明满屏" : "透明叠加"}(以 HTML 为准落 clip.opaque=${opaque})`);
|
|
8456
9458
|
}
|
|
8457
9459
|
if (typeof opts.slotDuration === "number" && opts.slotDuration > 0) {
|
|
8458
|
-
const
|
|
8459
|
-
|
|
8460
|
-
|
|
8461
|
-
|
|
9460
|
+
const slot = opts.slotDuration;
|
|
9461
|
+
const { est, parsed, skipped, hasInfiniteRepeat } = estimateTimelineSec(html);
|
|
9462
|
+
if (hasInfiniteRepeat)
|
|
9463
|
+
push("7-infinite-repeat", false, `含 repeat:-1 无限循环 → tl 时长为 Infinity,铁律⑦(总长 ≥ 坑位)无法静态验证;请改用按坑位算死的有限 repeat(次数 = ceil(剩余时长 / 单圈时长)),坑位包络 ${slot}s`);
|
|
9464
|
+
if (parsed === 0)
|
|
9465
|
+
push("7-no-estimate", false, `无法静态估长(无一条可解析的时长调用${skipped ? `,${skipped} 条因表达式 position / 非字面量 duration 跳过` : ""}),铁律⑦未校验——须真渲染引擎 seek 验收颗粒是否占满坑位 ${slot}s 并终态驻留`);
|
|
9466
|
+
else if (Number.isFinite(est) && est < slot)
|
|
9467
|
+
push("7-fill-slot", false, `时间线静态估长 ~${est}s,短于槽位包络 ${slot}s(铁律⑦:颗粒应占满坑位并终态驻留)——静态估算是**下界**(${skipped} 条调用因无法静态解析被跳过),仅供参考,最终以真渲染引擎逐帧为准`);
|
|
8462
9468
|
}
|
|
8463
9469
|
return { ok: !v.some((x) => x.fatal), violations: v, opaque, compositionId: cid };
|
|
8464
9470
|
}
|
|
@@ -8467,11 +9473,66 @@ function lintParticle(html, opts = {}) {
|
|
|
8467
9473
|
var MG_MATERIAL_PREFIX = "mg-";
|
|
8468
9474
|
var LEGACY_MATERIAL_PREFIX = "rrv-";
|
|
8469
9475
|
var isOwnMaterialId = (id) => id.startsWith(MG_MATERIAL_PREFIX) || id.startsWith(LEGACY_MATERIAL_PREFIX);
|
|
8470
|
-
var
|
|
8471
|
-
var
|
|
9476
|
+
var OWN_ASSET_DIRS = ["assets/mg/", "assets/rrv/"];
|
|
9477
|
+
var normalizeRel2 = (p) => p.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
9478
|
+
function ownAssetCompositionId(p) {
|
|
9479
|
+
if (typeof p !== "string")
|
|
9480
|
+
return;
|
|
9481
|
+
const rel = normalizeRel2(p);
|
|
9482
|
+
for (const d of OWN_ASSET_DIRS) {
|
|
9483
|
+
if (!rel.startsWith(d))
|
|
9484
|
+
continue;
|
|
9485
|
+
const name = rel.slice(d.length);
|
|
9486
|
+
if (name.includes("/"))
|
|
9487
|
+
return;
|
|
9488
|
+
return name.replace(/\.html?$/i, "");
|
|
9489
|
+
}
|
|
9490
|
+
return;
|
|
9491
|
+
}
|
|
9492
|
+
var r36 = (n) => Math.round(n * 1000) / 1000;
|
|
9493
|
+
var slotEnvelope = (it) => r36(it.track_ed - it.track_st);
|
|
8472
9494
|
function layTracksOf(prev) {
|
|
8473
9495
|
return Array.isArray(prev?.lay_tracks) ? prev.lay_tracks.filter((x) => typeof x === "number") : [];
|
|
8474
9496
|
}
|
|
9497
|
+
function beatsOf(prev) {
|
|
9498
|
+
return Array.isArray(prev?.beats) ? prev.beats.filter((b) => !!b && typeof b.composition_id === "string") : [];
|
|
9499
|
+
}
|
|
9500
|
+
function clipCompositionId(c3) {
|
|
9501
|
+
const hm = c3.html_material;
|
|
9502
|
+
if (typeof hm === "string") {
|
|
9503
|
+
if (hm.startsWith(MG_MATERIAL_PREFIX))
|
|
9504
|
+
return hm.slice(MG_MATERIAL_PREFIX.length);
|
|
9505
|
+
if (hm.startsWith(LEGACY_MATERIAL_PREFIX))
|
|
9506
|
+
return hm.slice(LEGACY_MATERIAL_PREFIX.length);
|
|
9507
|
+
}
|
|
9508
|
+
if (typeof c3.material === "string")
|
|
9509
|
+
return c3.material;
|
|
9510
|
+
return typeof c3.clip_id === "string" ? c3.clip_id : undefined;
|
|
9511
|
+
}
|
|
9512
|
+
var stOf = (c3) => typeof c3.track_st === "number" ? c3.track_st : 0;
|
|
9513
|
+
function collectClipRefs(c3, into) {
|
|
9514
|
+
for (const k of ["material", "html_material"]) {
|
|
9515
|
+
const v = c3[k];
|
|
9516
|
+
if (typeof v === "string")
|
|
9517
|
+
into.add(v);
|
|
9518
|
+
}
|
|
9519
|
+
}
|
|
9520
|
+
function collectSurvivingRefs(doc, extraClips) {
|
|
9521
|
+
const refs = new Set;
|
|
9522
|
+
for (const bucket of Object.values(doc)) {
|
|
9523
|
+
if (!Array.isArray(bucket))
|
|
9524
|
+
continue;
|
|
9525
|
+
for (const t of bucket) {
|
|
9526
|
+
if (!t || !Array.isArray(t.track_timeline))
|
|
9527
|
+
continue;
|
|
9528
|
+
for (const c3 of t.track_timeline)
|
|
9529
|
+
collectClipRefs(c3, refs);
|
|
9530
|
+
}
|
|
9531
|
+
}
|
|
9532
|
+
for (const c3 of extraClips)
|
|
9533
|
+
collectClipRefs(c3, refs);
|
|
9534
|
+
return refs;
|
|
9535
|
+
}
|
|
8475
9536
|
function layMgTracks(opts) {
|
|
8476
9537
|
const { gtrk, items, generatedAt } = opts;
|
|
8477
9538
|
const beatTracks = [...gtrk.beat_track ?? []];
|
|
@@ -8482,15 +9543,37 @@ function layMgTracks(opts) {
|
|
|
8482
9543
|
const prevIndices = new Set([...layTracksOf(prevMg), ...layTracksOf(prevRrv)]);
|
|
8483
9544
|
const removedTracks = beatTracks.filter((t) => typeof t.track_index === "number" && prevIndices.has(t.track_index));
|
|
8484
9545
|
const keptTracks = beatTracks.filter((t) => !(typeof t.track_index === "number" && prevIndices.has(t.track_index)));
|
|
8485
|
-
const
|
|
9546
|
+
const ownCompositionIds = new Set([
|
|
9547
|
+
...beatsOf(prevMg).map((b) => b.composition_id),
|
|
9548
|
+
...beatsOf(prevRrv).map((b) => b.composition_id),
|
|
9549
|
+
...items.map((it) => it.composition_id)
|
|
9550
|
+
]);
|
|
8486
9551
|
for (const t of removedTracks) {
|
|
8487
9552
|
for (const c3 of t.track_timeline ?? []) {
|
|
8488
|
-
const
|
|
8489
|
-
if (
|
|
8490
|
-
|
|
9553
|
+
const cid = clipCompositionId(c3);
|
|
9554
|
+
if (cid !== undefined)
|
|
9555
|
+
ownCompositionIds.add(cid);
|
|
9556
|
+
}
|
|
9557
|
+
}
|
|
9558
|
+
const isOwnMaterial = (m) => {
|
|
9559
|
+
if (typeof m.id === "string" && isOwnMaterialId(m.id))
|
|
9560
|
+
return true;
|
|
9561
|
+
const cid = ownAssetCompositionId(m.path);
|
|
9562
|
+
return cid !== undefined && ownCompositionIds.has(cid);
|
|
9563
|
+
};
|
|
9564
|
+
const itemIds = new Set(items.map((it) => it.composition_id));
|
|
9565
|
+
const keepIds = new Set((opts.keep ?? []).filter((id) => !itemIds.has(id)));
|
|
9566
|
+
const carriedClips = [];
|
|
9567
|
+
const carriedIds = new Set;
|
|
9568
|
+
for (const t of removedTracks) {
|
|
9569
|
+
for (const c3 of t.track_timeline ?? []) {
|
|
9570
|
+
const cid = clipCompositionId(c3);
|
|
9571
|
+
if (cid === undefined || !keepIds.has(cid) || carriedIds.has(cid))
|
|
9572
|
+
continue;
|
|
9573
|
+
carriedIds.add(cid);
|
|
9574
|
+
carriedClips.push({ ...c3 });
|
|
8491
9575
|
}
|
|
8492
9576
|
}
|
|
8493
|
-
const keptMaterials = materials.filter((m) => !(typeof m.id === "string" && removedMaterialIds.has(m.id)));
|
|
8494
9577
|
const newMaterials = [];
|
|
8495
9578
|
const clips = [];
|
|
8496
9579
|
const metaBeats = [];
|
|
@@ -8518,17 +9601,32 @@ function layMgTracks(opts) {
|
|
|
8518
9601
|
});
|
|
8519
9602
|
metaBeats.push({ ...toMetaBeat(it), laid: { track_index: newIndex } });
|
|
8520
9603
|
}
|
|
8521
|
-
const
|
|
8522
|
-
|
|
8523
|
-
|
|
8524
|
-
|
|
8525
|
-
|
|
8526
|
-
|
|
9604
|
+
const mergedClips = [...carriedClips, ...clips].sort((a, b) => stOf(a) - stOf(b));
|
|
9605
|
+
const createdTracks = mergedClips.length > 0 ? [{ track_index: newIndex, track_timeline: mergedClips }] : [];
|
|
9606
|
+
const survivingRefs = collectSurvivingRefs({ ...gtrk, beat_track: keptTracks }, carriedClips);
|
|
9607
|
+
const newMaterialIds = new Set(newMaterials.map((m) => String(m.id)));
|
|
9608
|
+
const keptMaterials = materials.filter((m) => {
|
|
9609
|
+
if (typeof m.id !== "string")
|
|
9610
|
+
return true;
|
|
9611
|
+
if (newMaterialIds.has(m.id))
|
|
9612
|
+
return false;
|
|
9613
|
+
if (!isOwnMaterial(m))
|
|
9614
|
+
return true;
|
|
9615
|
+
return survivingRefs.has(m.id);
|
|
9616
|
+
});
|
|
9617
|
+
const carriedBeats = [];
|
|
9618
|
+
const seenCarriedBeat = new Set;
|
|
9619
|
+
for (const b of [...beatsOf(prevMg), ...beatsOf(prevRrv)]) {
|
|
9620
|
+
if (!keepIds.has(b.composition_id) || seenCarriedBeat.has(b.composition_id))
|
|
9621
|
+
continue;
|
|
9622
|
+
seenCarriedBeat.add(b.composition_id);
|
|
9623
|
+
carriedBeats.push({ ...b, laid: carriedIds.has(b.composition_id) ? { track_index: newIndex } : null });
|
|
9624
|
+
}
|
|
8527
9625
|
const mg = {
|
|
8528
9626
|
contract_version: "v1",
|
|
8529
9627
|
generated_at: generatedAt,
|
|
8530
9628
|
lay_tracks: createdTracks.map((t) => t.track_index),
|
|
8531
|
-
beats: metaBeats
|
|
9629
|
+
beats: [...carriedBeats, ...metaBeats]
|
|
8532
9630
|
};
|
|
8533
9631
|
const nextStructMeta = { ...structMeta, mg };
|
|
8534
9632
|
delete nextStructMeta.rrv;
|
|
@@ -8540,7 +9638,11 @@ function layMgTracks(opts) {
|
|
|
8540
9638
|
};
|
|
8541
9639
|
return {
|
|
8542
9640
|
next,
|
|
8543
|
-
summary: {
|
|
9641
|
+
summary: {
|
|
9642
|
+
laidTrack: createdTracks[0]?.track_index ?? null,
|
|
9643
|
+
laidParticles: clips.length,
|
|
9644
|
+
keptParticles: carriedClips.length
|
|
9645
|
+
},
|
|
8544
9646
|
mg
|
|
8545
9647
|
};
|
|
8546
9648
|
}
|
|
@@ -8549,7 +9651,7 @@ function toMetaBeat(it) {
|
|
|
8549
9651
|
beat: it.beat,
|
|
8550
9652
|
composition_id: it.composition_id,
|
|
8551
9653
|
track_st: it.track_st,
|
|
8552
|
-
track_ed:
|
|
9654
|
+
track_ed: r36(it.track_ed),
|
|
8553
9655
|
duration: slotEnvelope(it),
|
|
8554
9656
|
html_path: it.html_rel,
|
|
8555
9657
|
...it.category ? { category: it.category } : {}
|
|
@@ -8560,7 +9662,7 @@ function toMetaBeat(it) {
|
|
|
8560
9662
|
var MG_ASSET_DIR = "assets/mg";
|
|
8561
9663
|
var MG_SRC_DIRS = ["mg", "rrv"];
|
|
8562
9664
|
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) => {
|
|
9665
|
+
program2.command("mg [words...]").alias("rrv").description("MG 颗粒铺轨:无 positional=消费 dispatch.mg 铺 html-particle;`mg lint <file>`=单文件 lint;`mg status`=看板").option("--project <dir>", "oralcut 产物目录(定位 split/dispatch.json 与工程)").option("--dispatch <path>", "显式指定 dispatch.json(非标准布局兜底)").option("--only <beat>", "只跑单 beat(收 beat id 如 B12,非 composition_id):增量重铺该 beat,轨上其余已铺颗粒原样保留").option("--lint-only", "只 lint 校验,不铺轨不写回").option("--replace-all", "显式授权重置整轨:不走增量保留、整轨剥掉重铺——会删掉轨上其余已铺颗粒(不在本次派单/--only 里的那些)").option("--json", "机读模式:人读日志转 stderr,stdout 只输出结果 JSON").action(async (words, opts) => {
|
|
8564
9666
|
if (process.argv[2] === "rrv")
|
|
8565
9667
|
log.warn("`gtrk rrv` 已更名为 `gtrk mg`(去品牌化),别名仍可用但建议改用 `gtrk mg`。");
|
|
8566
9668
|
await runMg(words ?? [], opts);
|
|
@@ -8580,53 +9682,109 @@ async function runMg(words, opts) {
|
|
|
8580
9682
|
}
|
|
8581
9683
|
function resolveDispatch(opts) {
|
|
8582
9684
|
if (opts.dispatch) {
|
|
8583
|
-
const dispatchPath =
|
|
9685
|
+
const dispatchPath = resolve10(opts.dispatch);
|
|
8584
9686
|
return { dispatchPath, baseDir: dirname8(dirname8(dispatchPath)) };
|
|
8585
9687
|
}
|
|
8586
9688
|
if (opts.project) {
|
|
8587
|
-
const baseDir =
|
|
8588
|
-
return { dispatchPath:
|
|
9689
|
+
const baseDir = resolve10(opts.project);
|
|
9690
|
+
return { dispatchPath: join21(baseDir, "split", "dispatch.json"), baseDir };
|
|
8589
9691
|
}
|
|
8590
9692
|
throw new Error("需 --project <目录> 或显式 --dispatch <path>");
|
|
8591
9693
|
}
|
|
8592
9694
|
function locateGtrk2(baseDir) {
|
|
8593
|
-
return [
|
|
9695
|
+
return [join21(baseDir, "gtrk", "project.gtrk"), join21(baseDir, "project.gtrk")].find((p) => existsSync18(p));
|
|
8594
9696
|
}
|
|
8595
9697
|
function locateSrcHtml(baseDir, compositionId) {
|
|
8596
9698
|
for (const d of MG_SRC_DIRS) {
|
|
8597
|
-
const p =
|
|
8598
|
-
if (
|
|
9699
|
+
const p = join21(baseDir, d, `${compositionId}.html`);
|
|
9700
|
+
if (existsSync18(p))
|
|
8599
9701
|
return p;
|
|
8600
9702
|
}
|
|
8601
9703
|
return;
|
|
8602
9704
|
}
|
|
8603
9705
|
async function readMgQueue(dispatchPath) {
|
|
8604
|
-
if (!
|
|
9706
|
+
if (!existsSync18(dispatchPath))
|
|
8605
9707
|
throw new Error(`找不到派单清单:${dispatchPath}(先跑 gtrk split 落地派单)`);
|
|
8606
|
-
const dispatch = JSON.parse(await
|
|
9708
|
+
const dispatch = JSON.parse(await readFile7(dispatchPath, "utf8"));
|
|
8607
9709
|
const queue = dispatch.mg ?? dispatch.rrv_mg;
|
|
8608
9710
|
return Array.isArray(queue) ? queue : [];
|
|
8609
9711
|
}
|
|
9712
|
+
function laidCompositionIds(gtrk) {
|
|
9713
|
+
if (!gtrk)
|
|
9714
|
+
return [];
|
|
9715
|
+
const structMeta = gtrk.struct_meta;
|
|
9716
|
+
const ids = [];
|
|
9717
|
+
const seen = new Set;
|
|
9718
|
+
for (const meta of [structMeta?.mg, structMeta?.rrv]) {
|
|
9719
|
+
for (const b of meta?.beats ?? []) {
|
|
9720
|
+
if (!b?.laid || typeof b.composition_id !== "string" || seen.has(b.composition_id))
|
|
9721
|
+
continue;
|
|
9722
|
+
seen.add(b.composition_id);
|
|
9723
|
+
ids.push(b.composition_id);
|
|
9724
|
+
}
|
|
9725
|
+
}
|
|
9726
|
+
return ids;
|
|
9727
|
+
}
|
|
8610
9728
|
async function runLay(opts) {
|
|
8611
9729
|
const { dispatchPath, baseDir } = resolveDispatch(opts);
|
|
8612
|
-
|
|
8613
|
-
|
|
8614
|
-
queue = queue.filter((q) => q.beat === opts.only);
|
|
9730
|
+
const allQueue = await readMgQueue(dispatchPath);
|
|
9731
|
+
const queue = opts.only ? allQueue.filter((q) => q.beat === opts.only) : allQueue;
|
|
8615
9732
|
log.step(`▶ MG 颗粒铺轨:${queue.length} 个 beat…`);
|
|
9733
|
+
if (opts.only && queue.length === 0) {
|
|
9734
|
+
const beats = [...new Set(allQueue.map((q) => q.beat))];
|
|
9735
|
+
log.warn(`--only ${opts.only} 未命中任何 beat——该选择器收的是「beat id」(如 B12),不是 composition_id(如 <slug>-B12)。`);
|
|
9736
|
+
log.warn(`dispatch 现有 beat:${beats.slice(0, 12).join("、") || "(空)"}${beats.length > 12 ? ` …共 ${beats.length} 个` : ""}`);
|
|
9737
|
+
}
|
|
9738
|
+
const gtrkPath = locateGtrk2(baseDir);
|
|
9739
|
+
let project;
|
|
9740
|
+
let gtrkUnreadable = false;
|
|
9741
|
+
if (gtrkPath) {
|
|
9742
|
+
try {
|
|
9743
|
+
project = readGtrk(gtrkPath);
|
|
9744
|
+
} catch (e) {
|
|
9745
|
+
if (!opts.lintOnly)
|
|
9746
|
+
throw e;
|
|
9747
|
+
gtrkUnreadable = true;
|
|
9748
|
+
}
|
|
9749
|
+
}
|
|
9750
|
+
if (!opts.lintOnly && project)
|
|
9751
|
+
assertGtrkV1(project.gtrk);
|
|
9752
|
+
const laidBefore = laidCompositionIds(opts.lintOnly ? undefined : project?.gtrk);
|
|
9753
|
+
const reproj = await reprojectDispatchWindows({
|
|
9754
|
+
baseDir,
|
|
9755
|
+
gtrk: project?.gtrk,
|
|
9756
|
+
gtrkUnreadable,
|
|
9757
|
+
entries: queue.map((q) => ({
|
|
9758
|
+
key: q.composition_id,
|
|
9759
|
+
beat: q.beat,
|
|
9760
|
+
compositionId: q.composition_id,
|
|
9761
|
+
span: q.span,
|
|
9762
|
+
track_st: q.track_st,
|
|
9763
|
+
track_ed: q.track_ed
|
|
9764
|
+
}))
|
|
9765
|
+
});
|
|
9766
|
+
reportReprojection(reproj);
|
|
8616
9767
|
const dispatchIds = queue.map((q) => q.composition_id);
|
|
8617
9768
|
const items = [];
|
|
8618
9769
|
const srcByComp = new Map;
|
|
8619
9770
|
const skipped = [];
|
|
9771
|
+
const windowOf = new Map(reproj.entries.map((o) => [o.key, o]));
|
|
8620
9772
|
for (const q of queue) {
|
|
9773
|
+
const outcome = windowOf.get(q.composition_id);
|
|
9774
|
+
if (outcome?.dropped) {
|
|
9775
|
+
skipped.push({ beat: q.beat, reason: "重投影后零存活(该段已被剪出成片)" });
|
|
9776
|
+
continue;
|
|
9777
|
+
}
|
|
9778
|
+
const win = outcome ?? { track_st: q.track_st, track_ed: q.track_ed };
|
|
8621
9779
|
const srcPath = locateSrcHtml(baseDir, q.composition_id);
|
|
8622
9780
|
if (!srcPath) {
|
|
8623
9781
|
skipped.push({ beat: q.beat, reason: "缺颗粒 HTML(未产出)" });
|
|
8624
|
-
log.warn(`${q.beat}:缺 ${
|
|
9782
|
+
log.warn(`${q.beat}:缺 ${join21(baseDir, MG_SRC_DIRS[0], `${q.composition_id}.html`)},跳过`);
|
|
8625
9783
|
continue;
|
|
8626
9784
|
}
|
|
8627
|
-
const html = await
|
|
9785
|
+
const html = await readFile7(srcPath, "utf8");
|
|
8628
9786
|
const category = typeof q.category === "string" ? q.category : undefined;
|
|
8629
|
-
const slotDuration = Math.round((
|
|
9787
|
+
const slotDuration = Math.round((win.track_ed - win.track_st) * 1000) / 1000;
|
|
8630
9788
|
const lint = lintParticle(html, {
|
|
8631
9789
|
compositionId: q.composition_id,
|
|
8632
9790
|
dispatchIds,
|
|
@@ -8648,55 +9806,162 @@ async function runLay(opts) {
|
|
|
8648
9806
|
items.push({
|
|
8649
9807
|
beat: q.beat,
|
|
8650
9808
|
composition_id: q.composition_id,
|
|
8651
|
-
track_st:
|
|
8652
|
-
track_ed:
|
|
9809
|
+
track_st: win.track_st,
|
|
9810
|
+
track_ed: win.track_ed,
|
|
8653
9811
|
opaque: lint.opaque,
|
|
8654
9812
|
html_rel: `${MG_ASSET_DIR}/${q.composition_id}.html`,
|
|
8655
9813
|
...category ? { category } : {}
|
|
8656
9814
|
});
|
|
8657
9815
|
}
|
|
8658
9816
|
if (opts.lintOnly) {
|
|
8659
|
-
|
|
8660
|
-
|
|
9817
|
+
const lintOk = skipped.length === 0;
|
|
9818
|
+
(lintOk ? log.ok : log.warn)(`lint-only:${items.length}/${queue.length} 通过,${skipped.length} 跳过(不铺轨)`);
|
|
9819
|
+
return done(opts, {
|
|
9820
|
+
ok: lintOk,
|
|
9821
|
+
mode: "lay",
|
|
9822
|
+
lintOnly: true,
|
|
9823
|
+
...lintOk ? {} : { reason: "skipped" },
|
|
9824
|
+
passed: items.length,
|
|
9825
|
+
skipped,
|
|
9826
|
+
reprojection: reproj.summary
|
|
9827
|
+
});
|
|
8661
9828
|
}
|
|
8662
|
-
|
|
8663
|
-
|
|
8664
|
-
|
|
8665
|
-
|
|
9829
|
+
if (!gtrkPath || !project) {
|
|
9830
|
+
log.warn(`未找到工程文件(${join21(baseDir, "gtrk", "project.gtrk")}),跳过铺轨——lint 已完成`);
|
|
9831
|
+
return done(opts, {
|
|
9832
|
+
ok: skipped.length === 0,
|
|
9833
|
+
mode: "lay",
|
|
9834
|
+
reason: "no_project",
|
|
9835
|
+
laid: 0,
|
|
9836
|
+
laidTrack: null,
|
|
9837
|
+
track_total: 0,
|
|
9838
|
+
removed: 0,
|
|
9839
|
+
kept: 0,
|
|
9840
|
+
kept_ids: [],
|
|
9841
|
+
skipped,
|
|
9842
|
+
note: "工程缺失,未铺轨",
|
|
9843
|
+
reprojection: reproj.summary
|
|
9844
|
+
});
|
|
8666
9845
|
}
|
|
8667
|
-
const { gtrk, mtimeMs } =
|
|
8668
|
-
assertGtrkV1(gtrk);
|
|
9846
|
+
const { gtrk, mtimeMs } = project;
|
|
8669
9847
|
const gtrkDir = dirname8(gtrkPath);
|
|
8670
|
-
|
|
9848
|
+
const covered = new Set(items.map((it) => it.composition_id));
|
|
9849
|
+
const orphans = laidBefore.filter((id) => !covered.has(id));
|
|
9850
|
+
const inDispatch = new Set(allQueue.map((q) => q.composition_id));
|
|
9851
|
+
const keep = opts.replaceAll ? [] : opts.only ? orphans : orphans.filter((id) => inDispatch.has(id));
|
|
9852
|
+
const keepSet = new Set(keep);
|
|
9853
|
+
const wouldRemove = laidBefore.filter((id) => !covered.has(id) && !keepSet.has(id));
|
|
9854
|
+
const refuse = laidBefore.length > 0 && items.length === 0 ? "empty_queue" : undefined;
|
|
9855
|
+
if (refuse && !opts.replaceAll) {
|
|
9856
|
+
log.err(`拒绝写回:本次一条颗粒都没定位到,而轨上已铺 ${laidBefore.length} 颗——「一条都没定位到」是派单/选择器出问题的信号,不是清空指令。`);
|
|
9857
|
+
if (opts.only && queue.length === 0) {
|
|
9858
|
+
log.warn(`成因:--only ${opts.only} 未命中任何 beat(选择器提示见上)。`);
|
|
9859
|
+
} else if (allQueue.length === 0) {
|
|
9860
|
+
log.warn("dispatch.mg(读旧 rrv_mg)为空或缺失——先跑 gtrk split 落地派单,再铺轨。");
|
|
9861
|
+
} else {
|
|
9862
|
+
log.warn(`本次 ${queue.length} 个条目全部被跳过(${[...new Set(skipped.map((s) => s.reason))].join("、")})——先按上面的 lint 报因补产 / 修颗粒,再重铺。`);
|
|
9863
|
+
}
|
|
9864
|
+
if (wouldRemove.length) {
|
|
9865
|
+
const preview = wouldRemove.slice(0, 8);
|
|
9866
|
+
log.warn(`将被铲掉:${preview.join("、")}${wouldRemove.length > preview.length ? ` …等共 ${wouldRemove.length} 颗` : ""}`);
|
|
9867
|
+
}
|
|
9868
|
+
log.warn("出路二选一:① 按上面的报因修好派单 / 选择器 / 颗粒后重铺;② 确知要清空整轨 → 加 --replace-all 显式授权(会删掉轨上全部已铺颗粒)");
|
|
9869
|
+
log.warn(`工程未被改动(.gtrk 逐字节不变,轨上仍 ${laidBefore.length} 颗)。`);
|
|
9870
|
+
return done(opts, {
|
|
9871
|
+
ok: false,
|
|
9872
|
+
mode: "lay",
|
|
9873
|
+
reason: refuse,
|
|
9874
|
+
refused: true,
|
|
9875
|
+
laid: 0,
|
|
9876
|
+
laidTrack: null,
|
|
9877
|
+
track_total: laidBefore.length,
|
|
9878
|
+
removed: 0,
|
|
9879
|
+
kept: 0,
|
|
9880
|
+
kept_ids: [],
|
|
9881
|
+
blocked: wouldRemove,
|
|
9882
|
+
skipped,
|
|
9883
|
+
reprojection: reproj.summary
|
|
9884
|
+
});
|
|
9885
|
+
}
|
|
9886
|
+
if (opts.replaceAll && orphans.length > 0) {
|
|
9887
|
+
log.warn(`--replace-all:已显式授权重置整轨,轨上其余 ${orphans.length} 颗已铺颗粒将被剥离(不走增量保留)。`);
|
|
9888
|
+
}
|
|
9889
|
+
await mkdir7(join21(gtrkDir, ...MG_ASSET_DIR.split("/")), { recursive: true });
|
|
8671
9890
|
for (const it of items) {
|
|
8672
|
-
await copyFile(srcByComp.get(it.composition_id),
|
|
9891
|
+
await copyFile(srcByComp.get(it.composition_id), join21(gtrkDir, ...it.html_rel.split("/")));
|
|
9892
|
+
}
|
|
9893
|
+
const { next, summary, mg } = layMgTracks({ gtrk, items, generatedAt: new Date().toISOString(), keep });
|
|
9894
|
+
const written = withTimecodeSource(next, "mg", reproj);
|
|
9895
|
+
writeGtrkAtomic(gtrkPath, written, mtimeMs);
|
|
9896
|
+
const integrity = safeCheckMaterialIntegrity({ gtrk: written, gtrkDir, log });
|
|
9897
|
+
const trackTotal = mg.beats.filter((b) => b.laid).length;
|
|
9898
|
+
const keptIds = mg.beats.filter((b) => b.laid && keepSet.has(b.composition_id)).map((b) => b.composition_id);
|
|
9899
|
+
const removed = laidBefore.length - keptIds.length;
|
|
9900
|
+
const ok = skipped.length === 0;
|
|
9901
|
+
const tail = `本次 ${summary.laidParticles} 颗 / 轨上共 ${trackTotal} 颗${keptIds.length ? `(保留 ${keptIds.length} 颗)` : ""}` + ` → beat_track ${summary.laidTrack ?? "-"}` + `${removed ? `(剥旧 ${removed} 颗)` : ""}${skipped.length ? `(${skipped.length} beat 跳过)` : ""}`;
|
|
9902
|
+
if (summary.laidTrack === null)
|
|
9903
|
+
log.warn(`未铺成任何颗粒:${tail}`);
|
|
9904
|
+
else if (ok)
|
|
9905
|
+
log.ok(`铺轨完成:${tail}`);
|
|
9906
|
+
else
|
|
9907
|
+
log.warn(`铺轨完成(有跳过):${tail}`);
|
|
9908
|
+
if (keptIds.length) {
|
|
9909
|
+
const preview = keptIds.slice(0, 8);
|
|
9910
|
+
log.warn(`其中 ${keptIds.length} 颗是上轮遗留、本次未重铺:${preview.join("、")}${keptIds.length > preview.length ? ` …等共 ${keptIds.length} 颗` : ""}` + `——轨上内容 = 本次 ${summary.laidParticles} 颗 + 上轮 ${keptIds.length} 颗,与本次派单不完全对应(要全部刷新就去掉 --only 全量重铺)。`);
|
|
8673
9911
|
}
|
|
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
9912
|
log.info("opencut 打开工程即见 MG overlay 轨(预览需 add-particle-project-folder-preview 上线);出片时客户端云渲。");
|
|
9913
|
+
if (integrity)
|
|
9914
|
+
reportMaterialIntegrity(integrity, log);
|
|
8678
9915
|
return done(opts, {
|
|
8679
|
-
ok
|
|
9916
|
+
ok,
|
|
8680
9917
|
mode: "lay",
|
|
9918
|
+
...ok ? {} : { reason: "skipped" },
|
|
8681
9919
|
laid: summary.laidParticles,
|
|
8682
9920
|
laidTrack: summary.laidTrack,
|
|
8683
|
-
|
|
9921
|
+
track_total: trackTotal,
|
|
9922
|
+
removed,
|
|
9923
|
+
kept: keptIds.length,
|
|
9924
|
+
kept_ids: keptIds,
|
|
9925
|
+
skipped,
|
|
9926
|
+
...integrity ? { integrity } : {},
|
|
9927
|
+
reprojection: reproj.summary
|
|
8684
9928
|
});
|
|
8685
9929
|
}
|
|
9930
|
+
var CID_SHAPE = /-B\d+(?:-aux\d+)?$/;
|
|
8686
9931
|
async function runLint(args, opts) {
|
|
8687
9932
|
const file = args[0];
|
|
8688
9933
|
if (!file)
|
|
8689
9934
|
throw new Error("用法:gtrk mg lint <particle.html> [--dispatch <path>]");
|
|
8690
|
-
const html = await
|
|
9935
|
+
const html = await readFile7(resolve10(file), "utf8");
|
|
9936
|
+
const nameId = basename11(file).replace(/\.html?$/i, "");
|
|
8691
9937
|
let dispatchIds;
|
|
8692
|
-
|
|
8693
|
-
|
|
8694
|
-
|
|
8695
|
-
|
|
9938
|
+
let slotDuration;
|
|
9939
|
+
let compositionId;
|
|
9940
|
+
if (opts.dispatch && existsSync18(resolve10(opts.dispatch))) {
|
|
9941
|
+
const queue = await readMgQueue(resolve10(opts.dispatch));
|
|
9942
|
+
dispatchIds = queue.map((q) => q.composition_id);
|
|
9943
|
+
const byName = queue.find((q) => q.composition_id === nameId);
|
|
9944
|
+
const innerCid = parseCompositionId(html);
|
|
9945
|
+
const hit = byName ?? (innerCid ? queue.find((q) => q.composition_id === innerCid) : undefined);
|
|
9946
|
+
if (hit) {
|
|
9947
|
+
const d = Math.round((hit.track_ed - hit.track_st) * 1000) / 1000;
|
|
9948
|
+
if (d > 0)
|
|
9949
|
+
slotDuration = d;
|
|
9950
|
+
}
|
|
9951
|
+
if (byName)
|
|
9952
|
+
compositionId = byName.composition_id;
|
|
9953
|
+
}
|
|
9954
|
+
if (compositionId === undefined && CID_SHAPE.test(nameId))
|
|
9955
|
+
compositionId = nameId;
|
|
9956
|
+
const lint = lintParticle(html, {
|
|
9957
|
+
...dispatchIds ? { dispatchIds } : {},
|
|
9958
|
+
...compositionId ? { compositionId } : {},
|
|
9959
|
+
...slotDuration ? { slotDuration } : {}
|
|
9960
|
+
});
|
|
8696
9961
|
for (const vv of lint.violations)
|
|
8697
9962
|
(vv.fatal ? log.err : log.warn)(`${vv.fatal ? "✗" : "·"} ${vv.law}: ${vv.msg}`);
|
|
8698
9963
|
if (lint.ok)
|
|
8699
|
-
log.ok(`lint 通过(${
|
|
9964
|
+
log.ok(`lint 通过(${basename11(file)};opaque=${lint.opaque})`);
|
|
8700
9965
|
else
|
|
8701
9966
|
log.err(`lint 未过(${lint.violations.filter((v) => v.fatal).length} 项致命)`);
|
|
8702
9967
|
const result = { mode: "lint", ...lint, ok: lint.ok };
|
|
@@ -8730,6 +9995,8 @@ async function runStatus(opts) {
|
|
|
8730
9995
|
return done(opts, { ok: true, mode: "status", total: queue.length, authored, laid, rows });
|
|
8731
9996
|
}
|
|
8732
9997
|
function done(opts, result) {
|
|
9998
|
+
if (!result.ok)
|
|
9999
|
+
process.exitCode = 1;
|
|
8733
10000
|
if (opts.json)
|
|
8734
10001
|
console.log(JSON.stringify(result));
|
|
8735
10002
|
return result;
|
|
@@ -9718,9 +10985,9 @@ function validateRegistry(registry = TOOL_REGISTRY) {
|
|
|
9718
10985
|
}
|
|
9719
10986
|
|
|
9720
10987
|
// src/lib/tool-runner.ts
|
|
9721
|
-
import { resolve as
|
|
10988
|
+
import { resolve as resolve11, join as join22, dirname as dirname9, basename as basename12, extname as extname5 } from "node:path";
|
|
9722
10989
|
import { mkdir as mkdir8, writeFile as writeFile8, stat as stat5 } from "node:fs/promises";
|
|
9723
|
-
import { createWriteStream, existsSync as
|
|
10990
|
+
import { createWriteStream, existsSync as existsSync19 } from "node:fs";
|
|
9724
10991
|
import { Readable as Readable2 } from "node:stream";
|
|
9725
10992
|
import { pipeline } from "node:stream/promises";
|
|
9726
10993
|
|
|
@@ -9850,7 +11117,7 @@ function validateToolInput(descriptor, inputAbs) {
|
|
|
9850
11117
|
return;
|
|
9851
11118
|
if (!inputAbs)
|
|
9852
11119
|
throw new Error(`${descriptor.name} 需要输入${spec.kind === "directory" ? "目录" : "文件"}`);
|
|
9853
|
-
if (!
|
|
11120
|
+
if (!existsSync19(inputAbs))
|
|
9854
11121
|
throw new Error(`输入不存在:${inputAbs}`);
|
|
9855
11122
|
if (spec.kind === "directory")
|
|
9856
11123
|
return;
|
|
@@ -9917,12 +11184,12 @@ function timestamp3() {
|
|
|
9917
11184
|
}
|
|
9918
11185
|
function resolveOutDir(descriptor, inputAbs, out) {
|
|
9919
11186
|
if (out)
|
|
9920
|
-
return
|
|
11187
|
+
return resolve11(out);
|
|
9921
11188
|
if (inputAbs) {
|
|
9922
|
-
const base =
|
|
9923
|
-
return
|
|
11189
|
+
const base = basename12(inputAbs, extname5(inputAbs));
|
|
11190
|
+
return join22(dirname9(inputAbs), `${base}-${descriptor.name}`);
|
|
9924
11191
|
}
|
|
9925
|
-
return
|
|
11192
|
+
return join22(process.cwd(), `${descriptor.name}-${timestamp3()}`);
|
|
9926
11193
|
}
|
|
9927
11194
|
async function safeFingerprint(inputAbs) {
|
|
9928
11195
|
try {
|
|
@@ -9937,8 +11204,8 @@ function emitBilling(hint) {
|
|
|
9937
11204
|
`);
|
|
9938
11205
|
}
|
|
9939
11206
|
async function runCloudTool(descriptor, inputArg, opts, deps) {
|
|
9940
|
-
const inputAbs = inputArg ?
|
|
9941
|
-
const baseName = inputAbs ?
|
|
11207
|
+
const inputAbs = inputArg ? resolve11(inputArg) : undefined;
|
|
11208
|
+
const baseName = inputAbs ? basename12(inputAbs, extname5(inputAbs)) : descriptor.name;
|
|
9942
11209
|
validateToolInput(descriptor, inputAbs);
|
|
9943
11210
|
const probe = deps.probeDurationSec ?? probeDuration;
|
|
9944
11211
|
guardDuration(descriptor, inputAbs, probe, opts.ffmpegPath);
|
|
@@ -9975,13 +11242,13 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
|
|
|
9975
11242
|
uploadCached: deps.uploadCached,
|
|
9976
11243
|
invalidateUpload: deps.invalidateUpload,
|
|
9977
11244
|
submitTask: deps.submitTask,
|
|
9978
|
-
sleep: deps.sleep ?? ((ms) => new Promise((
|
|
11245
|
+
sleep: deps.sleep ?? ((ms) => new Promise((resolve12) => setTimeout(resolve12, ms)))
|
|
9979
11246
|
});
|
|
9980
11247
|
const { taskId } = submitted;
|
|
9981
11248
|
const up = { fileId: submitted.fileId, cached: submitted.cached };
|
|
9982
11249
|
await mkdir8(outDir, { recursive: true });
|
|
9983
11250
|
const fingerprint2 = inputAbs ? await safeFingerprint(inputAbs) : undefined;
|
|
9984
|
-
await writeFile8(
|
|
11251
|
+
await writeFile8(join22(outDir, "task.json"), JSON.stringify({ tool: descriptor.name, taskType, taskId, fileId: up.fileId, source: inputAbs, fingerprint: fingerprint2, createdAt: new Date().toISOString() }, null, 2));
|
|
9985
11252
|
const output = await pollToolTask(deps.cfg, taskType, taskId, {
|
|
9986
11253
|
timeoutMs: descriptor.pollTimeoutMs,
|
|
9987
11254
|
intervalMs: deps.pollIntervalMs,
|
|
@@ -9994,7 +11261,7 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
|
|
|
9994
11261
|
const errors = {};
|
|
9995
11262
|
const items = descriptor.mapOutputs ? descriptor.mapOutputs(outputResult, ctx) : [];
|
|
9996
11263
|
for (const it of items) {
|
|
9997
|
-
const dest =
|
|
11264
|
+
const dest = join22(outDir, it.filename);
|
|
9998
11265
|
try {
|
|
9999
11266
|
await deps.downloadStream(it.url, dest);
|
|
10000
11267
|
files.push(dest);
|
|
@@ -10005,7 +11272,7 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
|
|
|
10005
11272
|
let resultFile;
|
|
10006
11273
|
const structured = descriptor.mapResult ? descriptor.mapResult(outputResult, ctx) : undefined;
|
|
10007
11274
|
if (structured != null) {
|
|
10008
|
-
resultFile =
|
|
11275
|
+
resultFile = join22(outDir, "result-output.json");
|
|
10009
11276
|
await writeFile8(resultFile, JSON.stringify(structured, null, 2));
|
|
10010
11277
|
}
|
|
10011
11278
|
if (items.length === 0 && resultFile == null) {
|
|
@@ -10023,14 +11290,14 @@ async function runCloudTool(descriptor, inputArg, opts, deps) {
|
|
|
10023
11290
|
...resultFile ? { resultFile } : {},
|
|
10024
11291
|
...Object.keys(errors).length ? { errors } : {}
|
|
10025
11292
|
};
|
|
10026
|
-
await writeFile8(
|
|
11293
|
+
await writeFile8(join22(outDir, "result.json"), JSON.stringify({ ...result, finishedAt: new Date().toISOString() }, null, 2));
|
|
10027
11294
|
return result;
|
|
10028
11295
|
}
|
|
10029
11296
|
|
|
10030
11297
|
// src/lib/mad/mad.ts
|
|
10031
11298
|
import { mkdir as mkdir11, writeFile as writeFile10 } from "node:fs/promises";
|
|
10032
|
-
import { existsSync as
|
|
10033
|
-
import { resolve as
|
|
11299
|
+
import { existsSync as existsSync22, statSync as statSync3 } from "node:fs";
|
|
11300
|
+
import { resolve as resolve12, join as join26 } from "node:path";
|
|
10034
11301
|
|
|
10035
11302
|
// src/lib/convert/types.ts
|
|
10036
11303
|
function num(v, d = 0) {
|
|
@@ -10344,14 +11611,14 @@ var FX_MATCHNAME = {
|
|
|
10344
11611
|
function esc(s) {
|
|
10345
11612
|
return String(s).replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\r/g, "\\r").replace(/\n/g, "\\n").replace(/\t/g, "\\t").replace(new RegExp(String.fromCharCode(8232), "g"), "\\u2028").replace(new RegExp(String.fromCharCode(8233), "g"), "\\u2029");
|
|
10346
11613
|
}
|
|
10347
|
-
function
|
|
11614
|
+
function r37(v) {
|
|
10348
11615
|
return Math.round(v * 1000) / 1000;
|
|
10349
11616
|
}
|
|
10350
11617
|
function colorArr(css, fallback) {
|
|
10351
11618
|
const c3 = parseCssColor(css);
|
|
10352
11619
|
if (!c3)
|
|
10353
11620
|
return fallback;
|
|
10354
|
-
return [
|
|
11621
|
+
return [r37(c3[0] / 255), r37(c3[1] / 255), r37(c3[2] / 255)];
|
|
10355
11622
|
}
|
|
10356
11623
|
function easeInfluence(e) {
|
|
10357
11624
|
const cb = parseCubicBezier(e);
|
|
@@ -10367,7 +11634,7 @@ function emitKeys(ctx, propExpr, keys) {
|
|
|
10367
11634
|
L.push(` try {`);
|
|
10368
11635
|
L.push(` var p = ${propExpr};`);
|
|
10369
11636
|
for (const k of keys) {
|
|
10370
|
-
L.push(` p.setValueAtTime(${
|
|
11637
|
+
L.push(` p.setValueAtTime(${r37(k.t)}, ${k.value});`);
|
|
10371
11638
|
}
|
|
10372
11639
|
keys.forEach((k, i) => {
|
|
10373
11640
|
const inf = easeInfluence(k.e);
|
|
@@ -10376,7 +11643,7 @@ function emitKeys(ctx, propExpr, keys) {
|
|
|
10376
11643
|
L.push(` try {`);
|
|
10377
11644
|
L.push(` var dim = 1; try { dim = p.value.length || 1; } catch (e) { dim = 1; }`);
|
|
10378
11645
|
L.push(` var eo = [], ei = [];`);
|
|
10379
|
-
L.push(` for (var d = 0; d < Math.min(dim, 3); d++) { eo.push(new KeyframeEase(0, ${
|
|
11646
|
+
L.push(` for (var d = 0; d < Math.min(dim, 3); d++) { eo.push(new KeyframeEase(0, ${r37(inf.out)})); ei.push(new KeyframeEase(0, ${r37(inf.inn)})); }`);
|
|
10380
11647
|
L.push(` p.setTemporalEaseAtKey(${i + 1}, ei, eo);`);
|
|
10381
11648
|
L.push(` } catch (e) {}`);
|
|
10382
11649
|
});
|
|
@@ -10457,10 +11724,10 @@ function positionBaseKeys(anim, pos) {
|
|
|
10457
11724
|
if (!(anim.x?.length || anim.y?.length))
|
|
10458
11725
|
return [];
|
|
10459
11726
|
const merged = mergeChannelTracks(anim.x, anim.y, pos[0], pos[1]);
|
|
10460
|
-
return merged.map((k) => ({ t: k.t, value: `[${
|
|
11727
|
+
return merged.map((k) => ({ t: k.t, value: `[${r37(k.a)}, ${r37(k.b)}]`, e: k.e }));
|
|
10461
11728
|
}
|
|
10462
11729
|
function scaleBaseKeys(anim, coverExpr) {
|
|
10463
|
-
const sc = (v) => coverExpr ? `${
|
|
11730
|
+
const sc = (v) => coverExpr ? `${r37(v)}*${coverExpr}` : `${r37(v)}`;
|
|
10464
11731
|
if (anim.scale?.length) {
|
|
10465
11732
|
return anim.scale.map((k) => ({ t: k.t, value: `[${sc(num(k.v, 1) * 100)}, ${sc(num(k.v, 1) * 100)}]`, e: k.e }));
|
|
10466
11733
|
}
|
|
@@ -10480,13 +11747,13 @@ function emitBakedOps(ctx, ly, layerVar, tf, coverExpr) {
|
|
|
10480
11747
|
const pos = [num(ly.pos?.[0], 0), num(ly.pos?.[1], 0)];
|
|
10481
11748
|
const inn = num(ly.in, 0);
|
|
10482
11749
|
const out = Math.max(inn + 0.01, num(ly.out, inn + 1));
|
|
10483
|
-
const sc = (v) => coverExpr ? `${
|
|
11750
|
+
const sc = (v) => coverExpr ? `${r37(v)}*${coverExpr}` : `${r37(v)}`;
|
|
10484
11751
|
const brights = tracks.filter((t) => t.prop === "brightness");
|
|
10485
11752
|
if (brights.length) {
|
|
10486
11753
|
ctx.lines.push(` // fx: flicker → 亮度脉冲(ADBE Brightness & Contrast 2,避开 opacity 通道)`);
|
|
10487
11754
|
ctx.lines.push(` var flkFx = null; try { flkFx = ${layerVar}.property("ADBE Effect Parade").addProperty("ADBE Brightness & Contrast 2"); } catch (e) { flkFx = null; }`);
|
|
10488
11755
|
for (const bt of brights) {
|
|
10489
|
-
const keys = bt.times.map((t, i) => ({ t, value: `${
|
|
11756
|
+
const keys = bt.times.map((t, i) => ({ t, value: `${r37(bt.values[i][0])}`, e: null }));
|
|
10490
11757
|
emitKeys(ctx, `flkFx.property(1)`, keys);
|
|
10491
11758
|
}
|
|
10492
11759
|
}
|
|
@@ -10498,12 +11765,12 @@ function emitBakedOps(ctx, ly, layerVar, tf, coverExpr) {
|
|
|
10498
11765
|
for (const bt of posBaked) {
|
|
10499
11766
|
windows.push(bt.window);
|
|
10500
11767
|
for (let i = 0;i < bt.times.length; i++) {
|
|
10501
|
-
bakedKeys.push({ t: bt.times[i], value: `[${
|
|
11768
|
+
bakedKeys.push({ t: bt.times[i], value: `[${r37(bt.values[i][0])}, ${r37(bt.values[i][1])}]`, e: null });
|
|
10502
11769
|
}
|
|
10503
11770
|
}
|
|
10504
11771
|
const merged = mergeBaked(baseKeys, bakedKeys, windows, (t) => {
|
|
10505
11772
|
const [x, y] = samplePos(anim, pos, t);
|
|
10506
|
-
return `[${
|
|
11773
|
+
return `[${r37(x)}, ${r37(y)}]`;
|
|
10507
11774
|
}, inn, out);
|
|
10508
11775
|
emitKeys(ctx, `${tf}.property("ADBE Position")`, merged);
|
|
10509
11776
|
}
|
|
@@ -10526,15 +11793,15 @@ function emitBakedOps(ctx, ly, layerVar, tf, coverExpr) {
|
|
|
10526
11793
|
}
|
|
10527
11794
|
const rotBaked = tracks.filter((t) => t.prop === "rotation");
|
|
10528
11795
|
if (rotBaked.length) {
|
|
10529
|
-
const baseKeys = (anim.rot ?? []).map((k) => ({ t: k.t, value: `${
|
|
11796
|
+
const baseKeys = (anim.rot ?? []).map((k) => ({ t: k.t, value: `${r37(num(k.v, 0))}`, e: k.e }));
|
|
10530
11797
|
const bakedKeys = [];
|
|
10531
11798
|
const windows = [];
|
|
10532
11799
|
for (const bt of rotBaked) {
|
|
10533
11800
|
windows.push(bt.window);
|
|
10534
11801
|
for (let i = 0;i < bt.times.length; i++)
|
|
10535
|
-
bakedKeys.push({ t: bt.times[i], value: `${
|
|
11802
|
+
bakedKeys.push({ t: bt.times[i], value: `${r37(bt.values[i][0])}`, e: null });
|
|
10536
11803
|
}
|
|
10537
|
-
const merged = mergeBaked(baseKeys, bakedKeys, windows, (t) => `${
|
|
11804
|
+
const merged = mergeBaked(baseKeys, bakedKeys, windows, (t) => `${r37(sampleTrack(anim.rot, t, 0))}`, inn, out);
|
|
10538
11805
|
emitKeys(ctx, `${tf}.property("ADBE Rotate Z")`, merged);
|
|
10539
11806
|
}
|
|
10540
11807
|
}
|
|
@@ -10550,11 +11817,11 @@ function emitLayer(ctx, ir, ly, parentNullVar) {
|
|
|
10550
11817
|
const py = num(ly.pos?.[1], c3.h / 2);
|
|
10551
11818
|
const name = esc(`${ly.id || "L" + id} [${ly.type}]`);
|
|
10552
11819
|
L.push(``);
|
|
10553
|
-
L.push(` // ---- 层 ${name} in=${
|
|
11820
|
+
L.push(` // ---- 层 ${name} in=${r37(inn)} out=${r37(out)} ----`);
|
|
10554
11821
|
if (ly.type === "group") {
|
|
10555
|
-
L.push(` var ${v} = ${cv}.layers.addNull(${
|
|
11822
|
+
L.push(` var ${v} = ${cv}.layers.addNull(${r37(c3.duration)});`);
|
|
10556
11823
|
L.push(` ${v}.name = "${name}";`);
|
|
10557
|
-
L.push(` ${v}.inPoint = ${
|
|
11824
|
+
L.push(` ${v}.inPoint = ${r37(inn)}; ${v}.outPoint = ${r37(out)};`);
|
|
10558
11825
|
L.push(` ${v}.property("ADBE Transform Group").property("ADBE Anchor Point").setValue([0,0]);`);
|
|
10559
11826
|
L.push(` ${v}.property("ADBE Transform Group").property("ADBE Position").setValue([0,0]);`);
|
|
10560
11827
|
if (parentNullVar)
|
|
@@ -10574,18 +11841,18 @@ function emitLayer(ctx, ir, ly, parentNullVar) {
|
|
|
10574
11841
|
const srcOffset = num(footageHit.srcOffset, 0);
|
|
10575
11842
|
const cvv = `cv${id}`;
|
|
10576
11843
|
coverExpr = cvv;
|
|
10577
|
-
L.push(` // 素材注入(${esc(String(ly.type))}): footage srcOffset=${
|
|
11844
|
+
L.push(` // 素材注入(${esc(String(ly.type))}): footage srcOffset=${r37(srcOffset)}`);
|
|
10578
11845
|
L.push(` var ${v} = null, ${cvv} = 1;`);
|
|
10579
11846
|
L.push(` if (${fgVar} != null) {`);
|
|
10580
11847
|
L.push(` try {`);
|
|
10581
11848
|
L.push(` ${v} = ${cv}.layers.add(${fgVar});`);
|
|
10582
11849
|
L.push(` var _fw = ${fgVar}.width || ${slotW}, _fh = ${fgVar}.height || ${slotH};`);
|
|
10583
11850
|
L.push(` ${cvv} = Math.max(${slotW} / _fw, ${slotH} / _fh);`);
|
|
10584
|
-
L.push(` ${v}.startTime = ${
|
|
11851
|
+
L.push(` ${v}.startTime = ${r37(inn)} - ${r37(srcOffset)};`);
|
|
10585
11852
|
L.push(` } catch (e) { ${v} = null; }`);
|
|
10586
11853
|
L.push(` }`);
|
|
10587
11854
|
L.push(` if (${v} == null) {`);
|
|
10588
|
-
L.push(` ${v} = ${cv}.layers.addSolid([0.35,0.35,0.4], "${name} [占位]", ${slotW}, ${slotH}, 1, ${
|
|
11855
|
+
L.push(` ${v} = ${cv}.layers.addSolid([0.35,0.35,0.4], "${name} [占位]", ${slotW}, ${slotH}, 1, ${r37(c3.duration)});`);
|
|
10589
11856
|
L.push(` ${cvv} = 1;`);
|
|
10590
11857
|
L.push(` }`);
|
|
10591
11858
|
} else if (ly.type === "text") {
|
|
@@ -10606,15 +11873,15 @@ function emitLayer(ctx, ir, ly, parentNullVar) {
|
|
|
10606
11873
|
const h = Math.max(2, Math.round(num(ly.h, 400)));
|
|
10607
11874
|
if (ly.shape === "ellipse")
|
|
10608
11875
|
L.push(` // TODO: 原层为椭圆形状,固态占位,可手动换 shape layer`);
|
|
10609
|
-
L.push(` var ${v} = ${cv}.layers.addSolid([${col.join(",")}], "${name}", ${w}, ${h}, 1, ${
|
|
11876
|
+
L.push(` var ${v} = ${cv}.layers.addSolid([${col.join(",")}], "${name}", ${w}, ${h}, 1, ${r37(c3.duration)});`);
|
|
10610
11877
|
} else {
|
|
10611
11878
|
const w = Math.max(2, Math.round(num(ly.w, c3.w)));
|
|
10612
11879
|
const h = Math.max(2, Math.round(num(ly.h, c3.h)));
|
|
10613
11880
|
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, ${
|
|
11881
|
+
L.push(` var ${v} = ${cv}.layers.addSolid([0.35,0.35,0.4], "${name} [占位]", ${w}, ${h}, 1, ${r37(c3.duration)});`);
|
|
10615
11882
|
}
|
|
10616
11883
|
L.push(` ${v}.name = "${name}";`);
|
|
10617
|
-
L.push(` ${v}.inPoint = ${
|
|
11884
|
+
L.push(` ${v}.inPoint = ${r37(inn)}; ${v}.outPoint = ${r37(out)};`);
|
|
10618
11885
|
if (parentNullVar)
|
|
10619
11886
|
L.push(` try { ${v}.parent = ${parentNullVar}; } catch (e) {}`);
|
|
10620
11887
|
if (ly.blend && ly.blend !== "normal") {
|
|
@@ -10624,7 +11891,7 @@ function emitLayer(ctx, ir, ly, parentNullVar) {
|
|
|
10624
11891
|
L.push(` try { ${v}.blendingMode = BlendingMode.${bm}; } catch (e) {}`);
|
|
10625
11892
|
}
|
|
10626
11893
|
const tf = `${v}.property("ADBE Transform Group")`;
|
|
10627
|
-
L.push(` ${tf}.property("ADBE Position").setValue([${
|
|
11894
|
+
L.push(` ${tf}.property("ADBE Position").setValue([${r37(px)}, ${r37(py)}]);`);
|
|
10628
11895
|
const anim = ly.anim ?? {};
|
|
10629
11896
|
const bakedProps = new Set(ctx.bakeOps ? bakeLayerOps(ly).tracks.map((t) => t.prop) : []);
|
|
10630
11897
|
if ((anim.x?.length || anim.y?.length) && !bakedProps.has("position")) {
|
|
@@ -10639,10 +11906,10 @@ function emitLayer(ctx, ir, ly, parentNullVar) {
|
|
|
10639
11906
|
}
|
|
10640
11907
|
}
|
|
10641
11908
|
if (anim.rot?.length && !bakedProps.has("rotation")) {
|
|
10642
|
-
emitKeys(ctx, `${tf}.property("ADBE Rotate Z")`, anim.rot.map((k) => ({ t: k.t, value: `${
|
|
11909
|
+
emitKeys(ctx, `${tf}.property("ADBE Rotate Z")`, anim.rot.map((k) => ({ t: k.t, value: `${r37(num(k.v, 0))}`, e: k.e })));
|
|
10643
11910
|
}
|
|
10644
11911
|
if (anim.opacity?.length) {
|
|
10645
|
-
emitKeys(ctx, `${tf}.property("ADBE Opacity")`, anim.opacity.map((k) => ({ t: k.t, value: `${
|
|
11912
|
+
emitKeys(ctx, `${tf}.property("ADBE Opacity")`, anim.opacity.map((k) => ({ t: k.t, value: `${r37(Math.min(1, Math.max(0, num(k.v, 1))) * 100)}`, e: k.e })));
|
|
10646
11913
|
}
|
|
10647
11914
|
if (anim.ls?.length) {
|
|
10648
11915
|
L.push(` // TODO: letterspacing 轨未自动映射(AE 需 Animator>Tracking),共 ${anim.ls.length} 帧`);
|
|
@@ -10660,7 +11927,7 @@ function emitGroupAnim(ctx, v, ly) {
|
|
|
10660
11927
|
const py = num(ly.pos?.[1], 0);
|
|
10661
11928
|
if (anim.x?.length || anim.y?.length) {
|
|
10662
11929
|
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: `[${
|
|
11930
|
+
emitKeys(ctx, `${v}.property("ADBE Transform Group").property("ADBE Position")`, merged.map((k) => ({ t: k.t, value: `[${r37(k.a - px)}, ${r37(k.b - py)}]`, e: k.e })));
|
|
10664
11931
|
}
|
|
10665
11932
|
for (const ch of ["scale", "rot", "opacity"]) {
|
|
10666
11933
|
if (anim[ch]?.length) {
|
|
@@ -10736,7 +12003,7 @@ function madJsx(opts) {
|
|
|
10736
12003
|
totalDur = Math.max(totalDur, win.dropAt + Math.max(0.01, len));
|
|
10737
12004
|
}
|
|
10738
12005
|
L.push(``);
|
|
10739
|
-
L.push(` var master = app.project.items.addComp("${masterName}", ${mw}, ${mh}, 1, ${
|
|
12006
|
+
L.push(` var master = app.project.items.addComp("${masterName}", ${mw}, ${mh}, 1, ${r37(totalDur)}, ${r37(mfps)});`);
|
|
10740
12007
|
L.push(` try { master.bgColor = [0.04,0.04,0.07]; } catch (e) {}`);
|
|
10741
12008
|
const subVars = [];
|
|
10742
12009
|
windows.forEach((win, wi) => {
|
|
@@ -10748,8 +12015,8 @@ function madJsx(opts) {
|
|
|
10748
12015
|
const subName = esc(`${win.uid}-${win.seq}`);
|
|
10749
12016
|
const subVar = `sub${wi}`;
|
|
10750
12017
|
L.push(``);
|
|
10751
|
-
L.push(` // ==== 子合成 #${wi} 技法 ${subName}(源窗 t0=${
|
|
10752
|
-
L.push(` var ${subVar} = app.project.items.addComp("${subName}", ${sw}, ${sh}, 1, ${
|
|
12018
|
+
L.push(` // ==== 子合成 #${wi} 技法 ${subName}(源窗 t0=${r37(win.t0)} t1=${r37(win.t1)})====`);
|
|
12019
|
+
L.push(` var ${subVar} = app.project.items.addComp("${subName}", ${sw}, ${sh}, 1, ${r37(sdur)}, ${r37(sfps)});`);
|
|
10753
12020
|
const winFootage = { ...win.footage ?? {} };
|
|
10754
12021
|
const subCtx = {
|
|
10755
12022
|
lines: L,
|
|
@@ -10770,9 +12037,9 @@ function madJsx(opts) {
|
|
|
10770
12037
|
const len = Math.max(0.01, win.outLen ?? win.t1 - win.t0);
|
|
10771
12038
|
const lv = `mL${wi}`;
|
|
10772
12039
|
L.push(` var ${lv} = master.layers.add(${subVar});`);
|
|
10773
|
-
L.push(` ${lv}.startTime = ${
|
|
10774
|
-
L.push(` ${lv}.inPoint = ${
|
|
10775
|
-
L.push(` ${lv}.outPoint = ${
|
|
12040
|
+
L.push(` ${lv}.startTime = ${r37(win.dropAt - win.t0)};`);
|
|
12041
|
+
L.push(` ${lv}.inPoint = ${r37(win.dropAt)};`);
|
|
12042
|
+
L.push(` ${lv}.outPoint = ${r37(win.dropAt + len)};`);
|
|
10776
12043
|
L.push(` try {`);
|
|
10777
12044
|
L.push(` var _cov = Math.max(${mw} / ${subVar}.width, ${mh} / ${subVar}.height) * 100;`);
|
|
10778
12045
|
L.push(` ${lv}.property("ADBE Transform Group").property("ADBE Scale").setValue([_cov, _cov]);`);
|
|
@@ -10794,7 +12061,7 @@ function madJsx(opts) {
|
|
|
10794
12061
|
L.push(` var mk = master.property("ADBE Marker");`);
|
|
10795
12062
|
for (const m of markers) {
|
|
10796
12063
|
const label = m.downbeat ? "downbeat" : "beat";
|
|
10797
|
-
L.push(` mk.setValueAtTime(${
|
|
12064
|
+
L.push(` mk.setValueAtTime(${r37(m.t)}, new MarkerValue("${label}"));`);
|
|
10798
12065
|
}
|
|
10799
12066
|
L.push(` } catch (e) {}`);
|
|
10800
12067
|
}
|
|
@@ -10810,7 +12077,7 @@ function madJsx(opts) {
|
|
|
10810
12077
|
|
|
10811
12078
|
// src/lib/mad/scan.ts
|
|
10812
12079
|
import { readdirSync, statSync as statSync2 } from "node:fs";
|
|
10813
|
-
import { extname as extname6, join as
|
|
12080
|
+
import { extname as extname6, join as join23 } from "node:path";
|
|
10814
12081
|
var VIDEO_EXTS2 = new Set(defaultExtsFor("video") ?? []);
|
|
10815
12082
|
function scanFolder(dirAbs, opts = {}) {
|
|
10816
12083
|
const probe = opts.probe ?? probeGeometry;
|
|
@@ -10821,7 +12088,7 @@ function scanFolder(dirAbs, opts = {}) {
|
|
|
10821
12088
|
} catch {
|
|
10822
12089
|
throw new Error(`素材文件夹不存在或无法读取:${dirAbs}`);
|
|
10823
12090
|
}
|
|
10824
|
-
const files = entries.filter((n) => VIDEO_EXTS2.has(extname6(n).toLowerCase())).map((n) =>
|
|
12091
|
+
const files = entries.filter((n) => VIDEO_EXTS2.has(extname6(n).toLowerCase())).map((n) => join23(dirAbs, n)).filter((p) => {
|
|
10825
12092
|
try {
|
|
10826
12093
|
return statSync2(p).isFile();
|
|
10827
12094
|
} catch {
|
|
@@ -10951,7 +12218,7 @@ function selectWindows(opts) {
|
|
|
10951
12218
|
|
|
10952
12219
|
// src/lib/mad/beat.ts
|
|
10953
12220
|
var clamp2 = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
|
|
10954
|
-
var
|
|
12221
|
+
var r38 = (v) => Math.round(v * 1000) / 1000;
|
|
10955
12222
|
var MIN_WIN = 0.4;
|
|
10956
12223
|
var MAX_WIN = 6;
|
|
10957
12224
|
function fixedRhythm(natLens) {
|
|
@@ -10959,7 +12226,7 @@ function fixedRhythm(natLens) {
|
|
|
10959
12226
|
let t = 0;
|
|
10960
12227
|
for (const nl of natLens) {
|
|
10961
12228
|
const outLen = clamp2(nl, MIN_WIN, MAX_WIN);
|
|
10962
|
-
placements.push({ dropAt:
|
|
12229
|
+
placements.push({ dropAt: r38(t), outLen: r38(outLen) });
|
|
10963
12230
|
t += outLen;
|
|
10964
12231
|
}
|
|
10965
12232
|
return { placements, markers: [] };
|
|
@@ -10969,7 +12236,7 @@ function beatQuantized(natLens, analysis) {
|
|
|
10969
12236
|
const bts = (analysis.beats ?? []).filter((t2) => Number.isFinite(t2) && t2 >= 0).sort((a, b) => a - b);
|
|
10970
12237
|
const dbSpanOk = dbs.length >= 2 && dbs[1] - dbs[0] <= MAX_WIN;
|
|
10971
12238
|
const snap = dbSpanOk ? dbs : bts;
|
|
10972
|
-
const snapSet = new Set(dbs.map((t2) =>
|
|
12239
|
+
const snapSet = new Set(dbs.map((t2) => r38(t2)));
|
|
10973
12240
|
if (snap.length < 2) {
|
|
10974
12241
|
return { plan: fixedRhythm(natLens), level: 2 };
|
|
10975
12242
|
}
|
|
@@ -10982,27 +12249,27 @@ function beatQuantized(natLens, analysis) {
|
|
|
10982
12249
|
si++;
|
|
10983
12250
|
if (si >= snap.length) {
|
|
10984
12251
|
const outLen = clamp2(natLens[i], MIN_WIN, MAX_WIN);
|
|
10985
|
-
placements.push({ dropAt:
|
|
12252
|
+
placements.push({ dropAt: r38(dropAt), outLen: r38(outLen) });
|
|
10986
12253
|
t = dropAt + outLen;
|
|
10987
12254
|
continue;
|
|
10988
12255
|
}
|
|
10989
12256
|
const nextSnap = snap[si];
|
|
10990
12257
|
const slotLen = clamp2(nextSnap - dropAt, MIN_WIN, MAX_WIN);
|
|
10991
|
-
placements.push({ dropAt:
|
|
12258
|
+
placements.push({ dropAt: r38(dropAt), outLen: r38(slotLen) });
|
|
10992
12259
|
t = dropAt + slotLen;
|
|
10993
12260
|
si++;
|
|
10994
12261
|
}
|
|
10995
12262
|
const totalDur = placements.length ? placements[placements.length - 1].dropAt + placements[placements.length - 1].outLen : 0;
|
|
10996
|
-
const allBeats = [...new Set([...bts, ...dbs].map((x) =>
|
|
12263
|
+
const allBeats = [...new Set([...bts, ...dbs].map((x) => r38(x)))].sort((a, b) => a - b);
|
|
10997
12264
|
const markers = allBeats.filter((tt) => tt >= 0 && tt <= totalDur + 0.000001).map((tt) => ({ t: tt, downbeat: snapSet.has(tt) }));
|
|
10998
12265
|
return { plan: { placements, markers }, level: 1 };
|
|
10999
12266
|
}
|
|
11000
12267
|
|
|
11001
12268
|
// src/lib/mad/data.ts
|
|
11002
12269
|
import { createHash as createHash2 } from "node:crypto";
|
|
11003
|
-
import { mkdir as mkdir9, readFile as
|
|
11004
|
-
import { existsSync as
|
|
11005
|
-
import { join as
|
|
12270
|
+
import { mkdir as mkdir9, readFile as readFile8, rename as rename2, rm, writeFile as writeFile9, readdir } from "node:fs/promises";
|
|
12271
|
+
import { existsSync as existsSync20 } from "node:fs";
|
|
12272
|
+
import { join as join24 } from "node:path";
|
|
11006
12273
|
function madCacheDir() {
|
|
11007
12274
|
return homeFile("mad-cache");
|
|
11008
12275
|
}
|
|
@@ -11031,10 +12298,10 @@ async function atomicWrite(dest, data) {
|
|
|
11031
12298
|
await rename2(tmp, dest);
|
|
11032
12299
|
}
|
|
11033
12300
|
async function verifyFile(path, sha256) {
|
|
11034
|
-
if (!
|
|
12301
|
+
if (!existsSync20(path))
|
|
11035
12302
|
return false;
|
|
11036
12303
|
try {
|
|
11037
|
-
const buf = await
|
|
12304
|
+
const buf = await readFile8(path);
|
|
11038
12305
|
return sha256Hex(buf) === sha256;
|
|
11039
12306
|
} catch {
|
|
11040
12307
|
return false;
|
|
@@ -11059,7 +12326,7 @@ async function cleanupOldVersions(cacheRoot, keepVersion, warn) {
|
|
|
11059
12326
|
for (const e of entries) {
|
|
11060
12327
|
const m = /^v(\d+)$/.exec(e);
|
|
11061
12328
|
if (m && Number(m[1]) !== keepVersion) {
|
|
11062
|
-
await rm(
|
|
12329
|
+
await rm(join24(cacheRoot, e), { recursive: true, force: true }).catch(() => {});
|
|
11063
12330
|
}
|
|
11064
12331
|
}
|
|
11065
12332
|
} catch {}
|
|
@@ -11068,7 +12335,7 @@ async function ensureMadData(opts, deps) {
|
|
|
11068
12335
|
const { cacheRoot, warn } = deps;
|
|
11069
12336
|
const timeout = deps.manifestTimeoutMs ?? 8000;
|
|
11070
12337
|
const mfUrl = deps.manifestUrl ?? manifestUrl();
|
|
11071
|
-
const snapshotPath =
|
|
12338
|
+
const snapshotPath = join24(cacheRoot, "manifest.json");
|
|
11072
12339
|
let manifest = null;
|
|
11073
12340
|
let online = false;
|
|
11074
12341
|
try {
|
|
@@ -11083,11 +12350,11 @@ async function ensureMadData(opts, deps) {
|
|
|
11083
12350
|
warn(`manifest 拉取失败(${e instanceof Error ? e.message : String(e)}),回退本地缓存`);
|
|
11084
12351
|
}
|
|
11085
12352
|
if (!manifest) {
|
|
11086
|
-
if (!
|
|
12353
|
+
if (!existsSync20(snapshotPath)) {
|
|
11087
12354
|
throw new Error("首次使用需联网下载技法数据。请连网后重跑 `gtrk tool mad`(数据会缓存到本地,之后可离线出片)。");
|
|
11088
12355
|
}
|
|
11089
12356
|
try {
|
|
11090
|
-
manifest = validateManifest(JSON.parse(await
|
|
12357
|
+
manifest = validateManifest(JSON.parse(await readFile8(snapshotPath, "utf8")));
|
|
11091
12358
|
} catch {
|
|
11092
12359
|
throw new Error("本地 manifest 缓存损坏且当前离线。请连网重跑,或加 --refresh 强制重新下载。");
|
|
11093
12360
|
}
|
|
@@ -11098,14 +12365,14 @@ async function ensureMadData(opts, deps) {
|
|
|
11098
12365
|
}
|
|
11099
12366
|
}
|
|
11100
12367
|
const version = manifest.version;
|
|
11101
|
-
const verDir =
|
|
11102
|
-
const poolPath =
|
|
12368
|
+
const verDir = join24(cacheRoot, `v${version}`);
|
|
12369
|
+
const poolPath = join24(verDir, "mad_pool.json");
|
|
11103
12370
|
const poolMeta = manifest.datasets.mad_pool;
|
|
11104
12371
|
const cacheValid = await verifyFile(poolPath, poolMeta.sha256);
|
|
11105
12372
|
const needDownload = !!opts.refresh || !cacheValid;
|
|
11106
12373
|
if (needDownload) {
|
|
11107
12374
|
if (!online) {
|
|
11108
|
-
if (
|
|
12375
|
+
if (existsSync20(poolPath)) {
|
|
11109
12376
|
throw new Error("本地技法数据缓存损坏且当前离线。请连网重跑,或加 --refresh 强制重新下载。");
|
|
11110
12377
|
}
|
|
11111
12378
|
throw new Error("首次使用需联网下载技法数据。请连网后重跑 `gtrk tool mad`(数据会缓存到本地,之后可离线出片)。");
|
|
@@ -11129,7 +12396,7 @@ async function ensureMadData(opts, deps) {
|
|
|
11129
12396
|
}
|
|
11130
12397
|
let pool;
|
|
11131
12398
|
try {
|
|
11132
|
-
pool = JSON.parse(await
|
|
12399
|
+
pool = JSON.parse(await readFile8(poolPath, "utf8"));
|
|
11133
12400
|
if (!Array.isArray(pool))
|
|
11134
12401
|
throw new Error("mad_pool 非数组");
|
|
11135
12402
|
} catch (e) {
|
|
@@ -11140,11 +12407,11 @@ async function ensureMadData(opts, deps) {
|
|
|
11140
12407
|
|
|
11141
12408
|
// src/lib/mad/pool.ts
|
|
11142
12409
|
import { gunzipSync } from "node:zlib";
|
|
11143
|
-
import { mkdir as mkdir10, readFile as
|
|
11144
|
-
import { existsSync as
|
|
11145
|
-
import { join as
|
|
12410
|
+
import { mkdir as mkdir10, readFile as readFile9 } from "node:fs/promises";
|
|
12411
|
+
import { existsSync as existsSync21 } from "node:fs";
|
|
12412
|
+
import { join as join25 } from "node:path";
|
|
11146
12413
|
function shardPath(verDir, shard) {
|
|
11147
|
-
return
|
|
12414
|
+
return join25(verDir, "ir", `${shard}.json.gz`);
|
|
11148
12415
|
}
|
|
11149
12416
|
function decodeShard(gz) {
|
|
11150
12417
|
const json = gunzipSync(gz).toString("utf8");
|
|
@@ -11161,9 +12428,9 @@ function makeIrLoader(assetsBase, verDir, online, deps) {
|
|
|
11161
12428
|
if (cached)
|
|
11162
12429
|
return cached;
|
|
11163
12430
|
const path = shardPath(verDir, shard);
|
|
11164
|
-
if (
|
|
12431
|
+
if (existsSync21(path)) {
|
|
11165
12432
|
try {
|
|
11166
|
-
const s2 = decodeShard(await
|
|
12433
|
+
const s2 = decodeShard(await readFile9(path));
|
|
11167
12434
|
memo.set(shard, s2);
|
|
11168
12435
|
return s2;
|
|
11169
12436
|
} catch {
|
|
@@ -11179,7 +12446,7 @@ function makeIrLoader(assetsBase, verDir, online, deps) {
|
|
|
11179
12446
|
throw new Error(`IR 分片下载失败 HTTP ${res.status}:${url}`);
|
|
11180
12447
|
const buf = new Uint8Array(await res.arrayBuffer());
|
|
11181
12448
|
const s = decodeShard(buf);
|
|
11182
|
-
await mkdir10(
|
|
12449
|
+
await mkdir10(join25(verDir, "ir"), { recursive: true });
|
|
11183
12450
|
await atomicWrite(path, buf);
|
|
11184
12451
|
memo.set(shard, s);
|
|
11185
12452
|
return s;
|
|
@@ -11193,7 +12460,7 @@ function makeIrLoader(assetsBase, verDir, online, deps) {
|
|
|
11193
12460
|
return ir;
|
|
11194
12461
|
},
|
|
11195
12462
|
shardCached(shard) {
|
|
11196
|
-
return memo.has(shard) ||
|
|
12463
|
+
return memo.has(shard) || existsSync21(shardPath(verDir, shard));
|
|
11197
12464
|
}
|
|
11198
12465
|
};
|
|
11199
12466
|
}
|
|
@@ -11215,7 +12482,7 @@ async function analyzeBgm(cfg, bgmAbs, deps) {
|
|
|
11215
12482
|
uploadCached: deps.uploadCached,
|
|
11216
12483
|
invalidateUpload: deps.invalidateUpload,
|
|
11217
12484
|
submitTask: deps.submitTask,
|
|
11218
|
-
sleep: deps.sleep ?? ((ms) => new Promise((
|
|
12485
|
+
sleep: deps.sleep ?? ((ms) => new Promise((resolve12) => setTimeout(resolve12, ms)))
|
|
11219
12486
|
});
|
|
11220
12487
|
const { taskId } = submitted;
|
|
11221
12488
|
const output = await deps.pollToolTask(cfg, ANALYZE_TASK, taskId, {});
|
|
@@ -11276,8 +12543,8 @@ async function runMad(inputArg, opts, deps = {}) {
|
|
|
11276
12543
|
const probeDur = deps.probeDurationFn ?? probeDuration;
|
|
11277
12544
|
if (!inputArg)
|
|
11278
12545
|
throw new Error("用法:gtrk tool mad <素材文件夹> [--bgm 歌.mp3]");
|
|
11279
|
-
const dirAbs =
|
|
11280
|
-
if (!
|
|
12546
|
+
const dirAbs = resolve12(inputArg);
|
|
12547
|
+
if (!existsSync22(dirAbs) || !statSync3(dirAbs).isDirectory()) {
|
|
11281
12548
|
throw new Error(`素材文件夹不存在或不是目录:${dirAbs}`);
|
|
11282
12549
|
}
|
|
11283
12550
|
const { videos, orientation, skipped } = scanFolder(dirAbs, {
|
|
@@ -11308,7 +12575,7 @@ async function runMad(inputArg, opts, deps = {}) {
|
|
|
11308
12575
|
let level = 3;
|
|
11309
12576
|
let analysis = null;
|
|
11310
12577
|
let bgmForTrack;
|
|
11311
|
-
const bgmAbs = opts.bgm ?
|
|
12578
|
+
const bgmAbs = opts.bgm ? resolve12(opts.bgm) : undefined;
|
|
11312
12579
|
if (bgmAbs) {
|
|
11313
12580
|
let dur = -1;
|
|
11314
12581
|
try {
|
|
@@ -11391,9 +12658,9 @@ async function runMad(inputArg, opts, deps = {}) {
|
|
|
11391
12658
|
const header = madHeader(version, now.toISOString());
|
|
11392
12659
|
const bgm = level <= 2 && bgmForTrack ? { path: bgmForTrack, markers } : undefined;
|
|
11393
12660
|
const { jsx } = madJsx({ master, windows, header, bgm });
|
|
11394
|
-
const outDir = opts.out ?
|
|
12661
|
+
const outDir = opts.out ? resolve12(opts.out) : join26(process.cwd(), `mad-${timestamp4(now)}`);
|
|
11395
12662
|
await mkdir11(outDir, { recursive: true });
|
|
11396
|
-
const jsxPath =
|
|
12663
|
+
const jsxPath = join26(outDir, "mad.jsx");
|
|
11397
12664
|
await writeFile10(jsxPath, jsx);
|
|
11398
12665
|
const result = {
|
|
11399
12666
|
ok: true,
|
|
@@ -11405,7 +12672,7 @@ async function runMad(inputArg, opts, deps = {}) {
|
|
|
11405
12672
|
degradeLevel: level,
|
|
11406
12673
|
techniques: chosen.map((c3) => ({ uid: c3.entry.uid, pid: c3.entry.pid, cat: c3.entry.cat, t0: c3.entry.t0, t1: c3.entry.t1 }))
|
|
11407
12674
|
};
|
|
11408
|
-
await writeFile10(
|
|
12675
|
+
await writeFile10(join26(outDir, "result.json"), JSON.stringify({ ...result, finishedAt: now.toISOString() }, null, 2));
|
|
11409
12676
|
warn(completionMessage(jsxPath, level));
|
|
11410
12677
|
return result;
|
|
11411
12678
|
}
|
|
@@ -11537,9 +12804,9 @@ async function runMadInTool(inputArg, opts) {
|
|
|
11537
12804
|
}
|
|
11538
12805
|
|
|
11539
12806
|
// src/commands/transcript.ts
|
|
11540
|
-
import { existsSync as
|
|
12807
|
+
import { existsSync as existsSync23 } from "node:fs";
|
|
11541
12808
|
import { mkdir as mkdir12, rename as rename3, rm as rm2, stat as stat6, writeFile as writeFile11 } from "node:fs/promises";
|
|
11542
|
-
import { basename as
|
|
12809
|
+
import { basename as basename13, dirname as dirname10, extname as extname7, join as join27, resolve as resolve13 } from "node:path";
|
|
11543
12810
|
|
|
11544
12811
|
// src/lib/transcript.ts
|
|
11545
12812
|
var AGENT_SUMMARY_PENDING = "<!-- gtrk:agent-summary-pending -->";
|
|
@@ -11660,7 +12927,7 @@ function buildDeps(overrides = {}) {
|
|
|
11660
12927
|
upload: overrides.upload ?? uploadCached,
|
|
11661
12928
|
invalidate: overrides.invalidate ?? invalidateUpload,
|
|
11662
12929
|
submit: overrides.submit ?? submitTask,
|
|
11663
|
-
sleep: overrides.sleep ?? ((ms) => new Promise((
|
|
12930
|
+
sleep: overrides.sleep ?? ((ms) => new Promise((resolve14) => setTimeout(resolve14, ms))),
|
|
11664
12931
|
poll: overrides.poll ?? (async (cfg, taskType, taskId, onTick) => await pollToolTask(cfg, taskType, taskId, { onTick })),
|
|
11665
12932
|
writeMarkdown: overrides.writeMarkdown ?? writeMarkdownAtomic,
|
|
11666
12933
|
now: overrides.now ?? (() => new Date)
|
|
@@ -11675,8 +12942,8 @@ async function validateTranscriptInput(input) {
|
|
|
11675
12942
|
if (looksLikeRemote(input)) {
|
|
11676
12943
|
throw new Error("视频转文字稿仅支持本地视频文件,不支持 URL、平台视频地址或远端下载");
|
|
11677
12944
|
}
|
|
11678
|
-
const inputAbs =
|
|
11679
|
-
if (!
|
|
12945
|
+
const inputAbs = resolve13(input);
|
|
12946
|
+
if (!existsSync23(inputAbs))
|
|
11680
12947
|
throw new Error(`本地视频不存在:${inputAbs}`);
|
|
11681
12948
|
const info = await stat6(inputAbs);
|
|
11682
12949
|
if (!info.isFile())
|
|
@@ -11688,8 +12955,8 @@ async function validateTranscriptInput(input) {
|
|
|
11688
12955
|
return inputAbs;
|
|
11689
12956
|
}
|
|
11690
12957
|
function resolveTranscriptOutput(inputAbs, out) {
|
|
11691
|
-
const base =
|
|
11692
|
-
const output = out ?
|
|
12958
|
+
const base = basename13(inputAbs, extname7(inputAbs));
|
|
12959
|
+
const output = out ? resolve13(out) : join27(dirname10(inputAbs), `${base}-transcript.md`);
|
|
11693
12960
|
if (extname7(output).toLowerCase() !== ".md")
|
|
11694
12961
|
throw new Error("--out 必须指向一个 .md 文件");
|
|
11695
12962
|
return output;
|
|
@@ -11711,8 +12978,8 @@ async function runTranscript(input, opts = {}, depsOverride) {
|
|
|
11711
12978
|
const output = resolveTranscriptOutput(inputAbs, opts.out);
|
|
11712
12979
|
const deps = buildDeps(depsOverride);
|
|
11713
12980
|
const language = opts.lang?.trim() || "zh-CN";
|
|
11714
|
-
const sourceName =
|
|
11715
|
-
const title =
|
|
12981
|
+
const sourceName = basename13(inputAbs);
|
|
12982
|
+
const title = basename13(inputAbs, extname7(inputAbs));
|
|
11716
12983
|
log.step(`▶ 视频转文字稿:${sourceName}`);
|
|
11717
12984
|
log.step("① 本地探测视频…");
|
|
11718
12985
|
const geometry = deps.probe(inputAbs, opts.ffmpegPath);
|
|
@@ -11724,7 +12991,7 @@ async function runTranscript(input, opts = {}, depsOverride) {
|
|
|
11724
12991
|
log.step("② 本地抽取 16k 单声道音频(原视频不上传)…");
|
|
11725
12992
|
const audio = await deps.extract(inputAbs, opts.ffmpegPath);
|
|
11726
12993
|
deps.assertDuration(geometry.duration, audio, opts.ffmpegPath);
|
|
11727
|
-
log.info(`上传物:${
|
|
12994
|
+
log.info(`上传物:${basename13(audio)}(仅音频衍生物)`);
|
|
11728
12995
|
log.step("③ 上传音频并提交 ASR…");
|
|
11729
12996
|
const payload = (fileId) => ({ file_id: fileId, language, word_level: true });
|
|
11730
12997
|
const submitted = await uploadAndSubmitTask(deps.cfg, audio, TASK_TYPE3, payload, {
|
|
@@ -11772,9 +13039,9 @@ function registerTranscript(program2) {
|
|
|
11772
13039
|
}
|
|
11773
13040
|
|
|
11774
13041
|
// src/commands/music-visualizer.ts
|
|
11775
|
-
import { resolve as
|
|
13042
|
+
import { resolve as resolve14, join as join28, dirname as dirname11, basename as basename14, extname as extname8 } from "node:path";
|
|
11776
13043
|
import { mkdir as mkdir13, writeFile as writeFile12 } from "node:fs/promises";
|
|
11777
|
-
import { existsSync as
|
|
13044
|
+
import { existsSync as existsSync24 } from "node:fs";
|
|
11778
13045
|
var TASK_TYPE4 = "music_visualizer";
|
|
11779
13046
|
var PRICE_KEY2 = "music_visualizer";
|
|
11780
13047
|
var HEX_RE = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
|
|
@@ -11822,7 +13089,7 @@ function timestamp5() {
|
|
|
11822
13089
|
return `${p(d.getFullYear() % 100)}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
|
|
11823
13090
|
}
|
|
11824
13091
|
function assertExt(pathAbs, exts, label) {
|
|
11825
|
-
if (!
|
|
13092
|
+
if (!existsSync24(pathAbs))
|
|
11826
13093
|
throw new Error(`${label}不存在:${pathAbs}`);
|
|
11827
13094
|
const e = extname8(pathAbs).toLowerCase();
|
|
11828
13095
|
if (exts.length && !exts.includes(e)) {
|
|
@@ -11878,22 +13145,22 @@ async function runMusicVisualizer(audio, opts) {
|
|
|
11878
13145
|
if (opts.json)
|
|
11879
13146
|
routeLogsToStderr();
|
|
11880
13147
|
const cfg = loadConfig();
|
|
11881
|
-
const audioAbs =
|
|
13148
|
+
const audioAbs = resolve14(audio);
|
|
11882
13149
|
assertExt(audioAbs, AUDIO_EXTS2, "音频");
|
|
11883
13150
|
const template = opts.template == null ? "" : String(opts.template).trim();
|
|
11884
13151
|
if (!template)
|
|
11885
13152
|
throw new Error("--template 必填:请指定可视化模板 id(取值见云端 API 文档 / 服务端模板列表)");
|
|
11886
13153
|
const styleFields = buildStyleFields(opts);
|
|
11887
|
-
const bgAbs = opts.background ?
|
|
13154
|
+
const bgAbs = opts.background ? resolve14(opts.background) : undefined;
|
|
11888
13155
|
if (bgAbs)
|
|
11889
13156
|
assertExt(bgAbs, [...IMAGE_EXTS2, ...VIDEO_EXTS3], "背景素材");
|
|
11890
|
-
const coverAbs = opts.cover ?
|
|
13157
|
+
const coverAbs = opts.cover ? resolve14(opts.cover) : undefined;
|
|
11891
13158
|
if (coverAbs)
|
|
11892
13159
|
assertExt(coverAbs, IMAGE_EXTS2, "封面图");
|
|
11893
13160
|
const extraParams = parseExtraParams3(opts.param, opts.paramsJson);
|
|
11894
|
-
const projName =
|
|
11895
|
-
const outDir =
|
|
11896
|
-
log.step(`▶ 音乐可视化:${
|
|
13161
|
+
const projName = basename14(audioAbs, extname8(audioAbs));
|
|
13162
|
+
const outDir = resolve14(opts.out ?? join28(dirname11(audioAbs), `${projName}-visualizer-${timestamp5()}`));
|
|
13163
|
+
log.step(`▶ 音乐可视化:${basename14(audioAbs)}(模板 ${template}${bgAbs ? " · 背景" : ""}${coverAbs ? " · 封面" : ""})`);
|
|
11897
13164
|
let billingHint;
|
|
11898
13165
|
try {
|
|
11899
13166
|
billingHint = (await resolveToolPricing(PRICE_KEY2)).billingHint;
|
|
@@ -11939,7 +13206,7 @@ async function runMusicVisualizer(audio, opts) {
|
|
|
11939
13206
|
const { taskId } = submitted;
|
|
11940
13207
|
log.info(`task_id = ${taskId}`);
|
|
11941
13208
|
await mkdir13(outDir, { recursive: true });
|
|
11942
|
-
await writeFile12(
|
|
13209
|
+
await writeFile12(join28(outDir, "task.json"), JSON.stringify({ taskId, taskType: TASK_TYPE4, fileId: submitted.fileId, source: audioAbs, template, createdAt: new Date().toISOString() }, null, 2));
|
|
11943
13210
|
log.step("③ 云端处理中(每 5s 轮询)…");
|
|
11944
13211
|
const result = await pollTask(cfg, TASK_TYPE4, taskId, (status, progress) => {
|
|
11945
13212
|
log.tick(`${status}${progress != null ? ` ${Math.round(progress)}%` : ""}`);
|
|
@@ -11950,7 +13217,7 @@ async function runMusicVisualizer(audio, opts) {
|
|
|
11950
13217
|
const files = [];
|
|
11951
13218
|
const errors = {};
|
|
11952
13219
|
if (url) {
|
|
11953
|
-
const dest =
|
|
13220
|
+
const dest = join28(outDir, `${projName}-visualizer${extFromUrl(url, ".mp4")}`);
|
|
11954
13221
|
try {
|
|
11955
13222
|
await downloadStream(url, dest);
|
|
11956
13223
|
files.push(dest);
|
|
@@ -11972,7 +13239,7 @@ async function runMusicVisualizer(audio, opts) {
|
|
|
11972
13239
|
...Object.keys(errors).length ? { errors } : {},
|
|
11973
13240
|
finishedAt: new Date().toISOString()
|
|
11974
13241
|
};
|
|
11975
|
-
await writeFile12(
|
|
13242
|
+
await writeFile12(join28(outDir, "result.json"), JSON.stringify(resultJson, null, 2));
|
|
11976
13243
|
if (opts.json) {
|
|
11977
13244
|
process.stdout.write(`${JSON.stringify(resultJson)}
|
|
11978
13245
|
`);
|
|
@@ -11990,7 +13257,7 @@ try {
|
|
|
11990
13257
|
process.loadEnvFile?.();
|
|
11991
13258
|
} catch {}
|
|
11992
13259
|
migrateLegacyHome();
|
|
11993
|
-
var { version } = JSON.parse(readFileSync5(
|
|
13260
|
+
var { version } = JSON.parse(readFileSync5(join29(packageRoot(), "package.json"), "utf8"));
|
|
11994
13261
|
var program2 = new Command;
|
|
11995
13262
|
program2.name("gtrk").description("同合云成片流水线 CLI —— agent 驱动云端任务、产物拉回本地、三方工程文件(客户端/剪映/PR)互通").version(version);
|
|
11996
13263
|
registerInstall(program2);
|