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