@gitruck/cli 0.2.3 → 0.2.6
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 +241 -208
- package/README.md +37 -7
- package/contracts/README.md +14 -0
- package/contracts/gsap-emit-v1.md +62 -0
- package/contracts/handoff-contracts.json +10 -0
- package/dist/index.js +1774 -74
- package/package.json +62 -61
- package/skills/gtrk-splitter/SKILL.md +150 -0
- package/skills/gtrk-splitter/references/example-visual-split.json +638 -0
- package/skills/gtrk-splitter/references/example-visual-split.md +439 -0
- package/skills/gtrk-splitter/references/field-schema.md +100 -0
- package/skills/gtrk-style-maker/SKILL.md +88 -0
- package/skills/gtrk-style-maker/references/ammo.md +16 -0
- package/skills/gtrk-style-maker/references/contracts-ref.md +9 -0
- package/skills/gtrk-style-maker/references/craft-output-spec.md +46 -0
- package/skills/gtrk-style-maker/references/seeds/README.md +4 -0
- package/skills/gtrk-style-maker/references/seeds/seed-ecom-home-goods.md +37 -0
- package/skills/gtrk-style-maker/references/seeds/seed-psych-humanities.md +35 -0
package/dist/index.js
CHANGED
|
@@ -4196,8 +4196,8 @@ var {
|
|
|
4196
4196
|
} = import__.default;
|
|
4197
4197
|
|
|
4198
4198
|
// src/index.ts
|
|
4199
|
-
import { readFileSync as
|
|
4200
|
-
import { join as
|
|
4199
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
4200
|
+
import { join as join20 } from "node:path";
|
|
4201
4201
|
|
|
4202
4202
|
// src/lib/paths.ts
|
|
4203
4203
|
import { dirname, join } from "node:path";
|
|
@@ -4245,7 +4245,7 @@ function migrateLegacyHome() {
|
|
|
4245
4245
|
// src/commands/skills.ts
|
|
4246
4246
|
import { homedir as homedir2 } from "node:os";
|
|
4247
4247
|
import { join as join2 } from "node:path";
|
|
4248
|
-
import { existsSync as existsSync2, mkdirSync as mkdirSync2,
|
|
4248
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, cpSync as cpSync2 } from "node:fs";
|
|
4249
4249
|
|
|
4250
4250
|
// src/lib/log.ts
|
|
4251
4251
|
var c = {
|
|
@@ -4274,35 +4274,40 @@ var log = {
|
|
|
4274
4274
|
};
|
|
4275
4275
|
|
|
4276
4276
|
// src/commands/skills.ts
|
|
4277
|
-
var
|
|
4278
|
-
var SRC = join2(packageRoot(), "skills", SKILL_NAME, "SKILL.md");
|
|
4277
|
+
var SKILL_NAMES = ["gtrk-oralcut", "gtrk-splitter", "gtrk-style-maker"];
|
|
4279
4278
|
function installSkill(opts = {}) {
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4279
|
+
const destRoot = opts.dir ?? join2(homedir2(), ".claude", "skills");
|
|
4280
|
+
let allOk = true;
|
|
4281
|
+
for (const name of SKILL_NAMES) {
|
|
4282
|
+
const src = join2(packageRoot(), "skills", name);
|
|
4283
|
+
if (!existsSync2(join2(src, "SKILL.md"))) {
|
|
4284
|
+
log.warn(`找不到打包的 skill 源:${join2(src, "SKILL.md")}(跳过 ${name},不影响命令行)`);
|
|
4285
|
+
allOk = false;
|
|
4286
|
+
continue;
|
|
4287
|
+
}
|
|
4288
|
+
const dest = join2(destRoot, name);
|
|
4289
|
+
try {
|
|
4290
|
+
mkdirSync2(dest, { recursive: true });
|
|
4291
|
+
cpSync2(src, dest, { recursive: true });
|
|
4292
|
+
log.ok(`已安装 /${name} → ${join2(dest, "SKILL.md")}`);
|
|
4293
|
+
} catch (e) {
|
|
4294
|
+
allOk = false;
|
|
4295
|
+
log.warn(`skill 安装失败(${name},不影响命令行使用):${e instanceof Error ? e.message : String(e)}`);
|
|
4296
|
+
}
|
|
4294
4297
|
}
|
|
4298
|
+
log.info("在 Claude Code 里打 /gtrk-oralcut、/gtrk-splitter 或 /gtrk-style-maker,也可直接说「帮我剪个口播 / 拆个分镜 / 造我栏目的风格 skill」触发(可能需重载会话)。");
|
|
4299
|
+
return allOk;
|
|
4295
4300
|
}
|
|
4296
4301
|
function registerSkills(program2) {
|
|
4297
4302
|
const skills = program2.command("skills").description("管理 agent skill(安装到 Claude Code)");
|
|
4298
|
-
skills.command("install").description(
|
|
4303
|
+
skills.command("install").description("把 /gtrk-oralcut 与 /gtrk-splitter 安装到 ~/.claude/skills(对标飞书 skills add)").option("--dir <dir>", "自定义 skills 目录(缺省 ~/.claude/skills)").action((opts) => {
|
|
4299
4304
|
installSkill({ dir: opts.dir });
|
|
4300
4305
|
});
|
|
4301
4306
|
}
|
|
4302
4307
|
|
|
4303
4308
|
// src/commands/init.ts
|
|
4304
|
-
import { join as
|
|
4305
|
-
import { existsSync as
|
|
4309
|
+
import { join as join9, resolve as resolve2 } from "node:path";
|
|
4310
|
+
import { existsSync as existsSync8 } from "node:fs";
|
|
4306
4311
|
|
|
4307
4312
|
// src/lib/user-config.ts
|
|
4308
4313
|
import { join as join3 } from "node:path";
|
|
@@ -4461,12 +4466,566 @@ async function promptConfirm(message, defaultYes = true) {
|
|
|
4461
4466
|
}
|
|
4462
4467
|
|
|
4463
4468
|
// src/commands/doctor.ts
|
|
4464
|
-
import { existsSync as
|
|
4469
|
+
import { existsSync as existsSync7 } from "node:fs";
|
|
4470
|
+
import { join as join8 } from "node:path";
|
|
4471
|
+
|
|
4472
|
+
// src/lib/column-config.ts
|
|
4473
|
+
import { join as join5 } from "node:path";
|
|
4474
|
+
import { existsSync as existsSync5, readFileSync as readFileSync2 } from "node:fs";
|
|
4475
|
+
|
|
4476
|
+
// src/lib/splitdoc.ts
|
|
4477
|
+
var BASE_TRACKS = ["真人出镜", "口播继续", "旁白主导"];
|
|
4478
|
+
var LANES = ["A_ROLL", "RRV_MG", "AI_DRAMA", "FILM_BROLL"];
|
|
4479
|
+
var NARRATIVES = [
|
|
4480
|
+
"mirror-hook",
|
|
4481
|
+
"demolition",
|
|
4482
|
+
"container-translation",
|
|
4483
|
+
"abyssal-fall",
|
|
4484
|
+
"holding",
|
|
4485
|
+
"reversal-elevation",
|
|
4486
|
+
"callback-closure",
|
|
4487
|
+
"typography-emphasis"
|
|
4488
|
+
];
|
|
4489
|
+
var CONTAINER_STAGES = [
|
|
4490
|
+
"none",
|
|
4491
|
+
"seed",
|
|
4492
|
+
"expand",
|
|
4493
|
+
"translate",
|
|
4494
|
+
"rupture",
|
|
4495
|
+
"flip",
|
|
4496
|
+
"callback"
|
|
4497
|
+
];
|
|
4498
|
+
var IRREPLACEABILITY = ["必须真人出镜", "优先 MG", "可被 B-roll 替代", "可降级处理"];
|
|
4499
|
+
var AUX_TYPES = [
|
|
4500
|
+
"quote-card",
|
|
4501
|
+
"term-callout",
|
|
4502
|
+
"network-diagram",
|
|
4503
|
+
"archive-caption",
|
|
4504
|
+
"pause-card",
|
|
4505
|
+
"data-annotation",
|
|
4506
|
+
"timeline-tag"
|
|
4507
|
+
];
|
|
4508
|
+
function isNonEmptyStr(v) {
|
|
4509
|
+
return typeof v === "string" && v.trim().length > 0;
|
|
4510
|
+
}
|
|
4511
|
+
function enumOk(v, list) {
|
|
4512
|
+
return typeof v === "string" && list.includes(v);
|
|
4513
|
+
}
|
|
4514
|
+
function validateSplitDoc(doc, ctx) {
|
|
4515
|
+
const errors = [];
|
|
4516
|
+
const warnings = [];
|
|
4517
|
+
if (typeof doc !== "object" || doc === null || Array.isArray(doc)) {
|
|
4518
|
+
return { errors: ["拆分稿必须是一个 JSON 对象"], warnings };
|
|
4519
|
+
}
|
|
4520
|
+
const d = doc;
|
|
4521
|
+
const vocab = ctx.vocab ?? {
|
|
4522
|
+
narrative: NARRATIVES,
|
|
4523
|
+
container_stage: CONTAINER_STAGES,
|
|
4524
|
+
base_track: BASE_TRACKS
|
|
4525
|
+
};
|
|
4526
|
+
const freeVocab = vocab.unknown_narrative === "allow";
|
|
4527
|
+
if (d.contract_version !== "v1") {
|
|
4528
|
+
errors.push(`contract_version 必须为 "v1"(实际:${JSON.stringify(d.contract_version)})`);
|
|
4529
|
+
}
|
|
4530
|
+
if (!isNonEmptyStr(d.transcript_hash)) {
|
|
4531
|
+
errors.push("缺 transcript_hash(应从投影视图透传)");
|
|
4532
|
+
} else if (d.transcript_hash !== ctx.transcriptHash) {
|
|
4533
|
+
errors.push(`transcript_hash 不匹配:拆分稿 ${d.transcript_hash} ≠ 当前 transcript ${ctx.transcriptHash}——转写已变更,请重新导出视图并重拆`);
|
|
4534
|
+
}
|
|
4535
|
+
if (!Array.isArray(d.beats) || d.beats.length === 0) {
|
|
4536
|
+
errors.push("beats 必须是非空数组");
|
|
4537
|
+
return { errors, warnings };
|
|
4538
|
+
}
|
|
4539
|
+
const idIndex = new Map;
|
|
4540
|
+
ctx.utteranceIds.forEach((id, i) => idIndex.set(id, i));
|
|
4541
|
+
const ranges = [];
|
|
4542
|
+
const seenBeatIds = new Set;
|
|
4543
|
+
d.beats.forEach((raw, i) => {
|
|
4544
|
+
const tag = (() => {
|
|
4545
|
+
const bid = raw?.id;
|
|
4546
|
+
return isNonEmptyStr(bid) ? bid : `beats[${i}]`;
|
|
4547
|
+
})();
|
|
4548
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
4549
|
+
errors.push(`${tag}:beat 必须是对象`);
|
|
4550
|
+
return;
|
|
4551
|
+
}
|
|
4552
|
+
const b = raw;
|
|
4553
|
+
if (!isNonEmptyStr(b.id))
|
|
4554
|
+
errors.push(`${tag}:缺 id`);
|
|
4555
|
+
else if (!/^B\d{2,}$/.test(b.id))
|
|
4556
|
+
errors.push(`${b.id}:id 须为 "B"+两位起序号(如 B01)`);
|
|
4557
|
+
else if (seenBeatIds.has(b.id))
|
|
4558
|
+
errors.push(`${b.id}:beat id 重复`);
|
|
4559
|
+
else
|
|
4560
|
+
seenBeatIds.add(b.id);
|
|
4561
|
+
if (freeVocab) {
|
|
4562
|
+
if (!isNonEmptyStr(b.base_track))
|
|
4563
|
+
errors.push(`${tag}:缺 base_track`);
|
|
4564
|
+
if (!isNonEmptyStr(b.narrative))
|
|
4565
|
+
errors.push(`${tag}:缺 narrative`);
|
|
4566
|
+
if (!isNonEmptyStr(b.container_stage))
|
|
4567
|
+
errors.push(`${tag}:缺 container_stage`);
|
|
4568
|
+
} else {
|
|
4569
|
+
if (!enumOk(b.base_track, vocab.base_track))
|
|
4570
|
+
errors.push(`${tag}:base_track 非法(栏目词表:${vocab.base_track.join(" | ")})`);
|
|
4571
|
+
if (!enumOk(b.narrative, vocab.narrative))
|
|
4572
|
+
errors.push(`${tag}:narrative 非法(不在栏目词表内)`);
|
|
4573
|
+
if (!enumOk(b.container_stage, vocab.container_stage))
|
|
4574
|
+
errors.push(`${tag}:container_stage 非法(不在栏目词表内)`);
|
|
4575
|
+
}
|
|
4576
|
+
if (!enumOk(b.lane, LANES))
|
|
4577
|
+
errors.push(`${tag}:lane 非法(四选一:${LANES.join(" | ")})`);
|
|
4578
|
+
if (!enumOk(b.irreplaceability, IRREPLACEABILITY))
|
|
4579
|
+
errors.push(`${tag}:irreplaceability 非法(四枚举之一)`);
|
|
4580
|
+
if (!isNonEmptyStr(b.rhythm))
|
|
4581
|
+
errors.push(`${tag}:缺 rhythm(人读节奏标签)`);
|
|
4582
|
+
if (!isNonEmptyStr(b.visual_task))
|
|
4583
|
+
errors.push(`${tag}:缺 visual_task(一句话视觉任务)`);
|
|
4584
|
+
const span = b.span;
|
|
4585
|
+
let fromIdx = -1;
|
|
4586
|
+
let toIdx = -1;
|
|
4587
|
+
if (!span || !isNonEmptyStr(span.from) || !isNonEmptyStr(span.to)) {
|
|
4588
|
+
errors.push(`${tag}:缺 span.from / span.to(utterance id 区间)`);
|
|
4589
|
+
} else {
|
|
4590
|
+
if (!idIndex.has(span.from))
|
|
4591
|
+
errors.push(`${tag}:span.from 引用了不存在的 utterance id ${span.from}`);
|
|
4592
|
+
else
|
|
4593
|
+
fromIdx = idIndex.get(span.from);
|
|
4594
|
+
if (!idIndex.has(span.to))
|
|
4595
|
+
errors.push(`${tag}:span.to 引用了不存在的 utterance id ${span.to}`);
|
|
4596
|
+
else
|
|
4597
|
+
toIdx = idIndex.get(span.to);
|
|
4598
|
+
if (fromIdx >= 0 && toIdx >= 0) {
|
|
4599
|
+
if (fromIdx > toIdx)
|
|
4600
|
+
errors.push(`${tag}:区间倒序(span.from ${span.from} 晚于 span.to ${span.to})`);
|
|
4601
|
+
else if (isNonEmptyStr(b.id))
|
|
4602
|
+
ranges.push({ id: b.id, from: fromIdx, to: toIdx });
|
|
4603
|
+
}
|
|
4604
|
+
}
|
|
4605
|
+
validateHandoff(tag, b, errors, warnings);
|
|
4606
|
+
if (b.aux_layers != null) {
|
|
4607
|
+
if (!Array.isArray(b.aux_layers))
|
|
4608
|
+
errors.push(`${tag}:aux_layers 必须是数组`);
|
|
4609
|
+
else
|
|
4610
|
+
b.aux_layers.forEach((a, ai) => validateAux(`${tag}.aux[${ai}]`, a, idIndex, errors));
|
|
4611
|
+
}
|
|
4612
|
+
});
|
|
4613
|
+
const sorted = [...ranges].sort((a, b) => a.from - b.from || a.to - b.to);
|
|
4614
|
+
for (let i = 1;i < sorted.length; i++) {
|
|
4615
|
+
const prev = sorted[i - 1];
|
|
4616
|
+
const cur = sorted[i];
|
|
4617
|
+
if (cur.from <= prev.to) {
|
|
4618
|
+
errors.push(`${prev.id} 与 ${cur.id}:utterance 区间重叠(beats 之间不允许交集)`);
|
|
4619
|
+
}
|
|
4620
|
+
}
|
|
4621
|
+
return { errors, warnings };
|
|
4622
|
+
}
|
|
4623
|
+
function validateHandoff(tag, b, errors, warnings) {
|
|
4624
|
+
const lane = b.lane;
|
|
4625
|
+
const handoff = b.handoff;
|
|
4626
|
+
if (lane === "A_ROLL") {
|
|
4627
|
+
if (handoff != null)
|
|
4628
|
+
warnings.push(`${tag}:A_ROLL 不应带 handoff,已忽略`);
|
|
4629
|
+
return;
|
|
4630
|
+
}
|
|
4631
|
+
if (lane === "RRV_MG") {
|
|
4632
|
+
if (!handoff || typeof handoff.duration_hint !== "number") {
|
|
4633
|
+
errors.push(`${tag}:RRV_MG 的 handoff.duration_hint 必填(秒,数值)`);
|
|
4634
|
+
}
|
|
4635
|
+
return;
|
|
4636
|
+
}
|
|
4637
|
+
if (lane === "FILM_BROLL") {
|
|
4638
|
+
const q = handoff?.queries;
|
|
4639
|
+
if (!Array.isArray(q) || q.length === 0 || !q.every((x) => isNonEmptyStr(x))) {
|
|
4640
|
+
errors.push(`${tag}:FILM_BROLL 缺检索 query(handoff.queries 必须为非空字符串数组)`);
|
|
4641
|
+
}
|
|
4642
|
+
return;
|
|
4643
|
+
}
|
|
4644
|
+
}
|
|
4645
|
+
function validateAux(tag, raw, idIndex, errors) {
|
|
4646
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
4647
|
+
errors.push(`${tag}:辅助层必须是对象`);
|
|
4648
|
+
return;
|
|
4649
|
+
}
|
|
4650
|
+
const a = raw;
|
|
4651
|
+
if (!enumOk(a.type, AUX_TYPES))
|
|
4652
|
+
errors.push(`${tag}:type 非法(七类之一)`);
|
|
4653
|
+
if (!isNonEmptyStr(a.role))
|
|
4654
|
+
errors.push(`${tag}:缺 role(职责)`);
|
|
4655
|
+
const m = a.mount;
|
|
4656
|
+
if (m === "same_beat")
|
|
4657
|
+
return;
|
|
4658
|
+
if (typeof m === "object" && m !== null) {
|
|
4659
|
+
const mo = m;
|
|
4660
|
+
if (isNonEmptyStr(mo.trigger)) {
|
|
4661
|
+
if (!idIndex.has(mo.trigger))
|
|
4662
|
+
errors.push(`${tag}:mount.trigger 引用了不存在的 utterance id ${mo.trigger}`);
|
|
4663
|
+
return;
|
|
4664
|
+
}
|
|
4665
|
+
if (isNonEmptyStr(mo.from) && isNonEmptyStr(mo.to)) {
|
|
4666
|
+
if (!idIndex.has(mo.from))
|
|
4667
|
+
errors.push(`${tag}:mount.from 引用了不存在的 utterance id ${mo.from}`);
|
|
4668
|
+
if (!idIndex.has(mo.to))
|
|
4669
|
+
errors.push(`${tag}:mount.to 引用了不存在的 utterance id ${mo.to}`);
|
|
4670
|
+
if (idIndex.has(mo.from) && idIndex.has(mo.to) && idIndex.get(mo.from) > idIndex.get(mo.to)) {
|
|
4671
|
+
errors.push(`${tag}:mount 区间倒序`);
|
|
4672
|
+
}
|
|
4673
|
+
return;
|
|
4674
|
+
}
|
|
4675
|
+
}
|
|
4676
|
+
errors.push(`${tag}:mount 非法(应为 "same_beat" | {from,to} | {trigger})`);
|
|
4677
|
+
}
|
|
4678
|
+
function r3(n) {
|
|
4679
|
+
return Math.round(n * 1000) / 1000;
|
|
4680
|
+
}
|
|
4681
|
+
function buildLanding(doc, view, opts) {
|
|
4682
|
+
const byId = new Map;
|
|
4683
|
+
for (const id of opts.utteranceIds)
|
|
4684
|
+
byId.set(id, []);
|
|
4685
|
+
for (const u of view.utterances) {
|
|
4686
|
+
if (u.dropped || u.track_st == null || u.track_ed == null)
|
|
4687
|
+
continue;
|
|
4688
|
+
if (!byId.has(u.id))
|
|
4689
|
+
byId.set(u.id, []);
|
|
4690
|
+
byId.get(u.id).push({ track_st: u.track_st, track_ed: u.track_ed });
|
|
4691
|
+
}
|
|
4692
|
+
const idIndex = new Map;
|
|
4693
|
+
opts.utteranceIds.forEach((id, i) => idIndex.set(id, i));
|
|
4694
|
+
const split = {
|
|
4695
|
+
contract_version: doc.contract_version,
|
|
4696
|
+
transcript_hash: doc.transcript_hash,
|
|
4697
|
+
projected_at: opts.projectedAt,
|
|
4698
|
+
...opts.sourceIndex ? { material_id: opts.sourceIndex.materialId } : {},
|
|
4699
|
+
beats: []
|
|
4700
|
+
};
|
|
4701
|
+
const dispatch = { rrv_mg: [], film_broll: [], ai_drama: [] };
|
|
4702
|
+
const skipped = [];
|
|
4703
|
+
const shrunk = [];
|
|
4704
|
+
const unhandledLanes = new Set;
|
|
4705
|
+
for (const beat of doc.beats) {
|
|
4706
|
+
const fromIdx = idIndex.get(beat.span.from);
|
|
4707
|
+
const toIdx = idIndex.get(beat.span.to);
|
|
4708
|
+
const spanIds = opts.utteranceIds.slice(fromIdx, toIdx + 1);
|
|
4709
|
+
const instances = spanIds.flatMap((id) => byId.get(id) ?? []);
|
|
4710
|
+
const droppedCount = spanIds.filter((id) => (byId.get(id)?.length ?? 0) === 0).length;
|
|
4711
|
+
if (instances.length === 0) {
|
|
4712
|
+
skipped.push({ beat: beat.id, reason: "span 内全部 utterance 被剪,未落轨" });
|
|
4713
|
+
continue;
|
|
4714
|
+
}
|
|
4715
|
+
const track_st = Math.min(...instances.map((x) => x.track_st));
|
|
4716
|
+
const track_ed = Math.max(...instances.map((x) => x.track_ed));
|
|
4717
|
+
const isShrunk = droppedCount > 0;
|
|
4718
|
+
const metaBeat = { id: beat.id, lane: beat.lane, span: beat.span, track_st, track_ed };
|
|
4719
|
+
if (opts.sourceIndex) {
|
|
4720
|
+
const from = opts.sourceIndex.utterances.get(beat.span.from);
|
|
4721
|
+
const to = opts.sourceIndex.utterances.get(beat.span.to);
|
|
4722
|
+
if (from && to && to.ed > from.st) {
|
|
4723
|
+
metaBeat.source_ranges = [{ st: r3(from.st), ed: r3(to.ed) }];
|
|
4724
|
+
}
|
|
4725
|
+
}
|
|
4726
|
+
if (isShrunk)
|
|
4727
|
+
metaBeat.shrunk = true;
|
|
4728
|
+
if (beat.narrative)
|
|
4729
|
+
metaBeat.narrative = beat.narrative;
|
|
4730
|
+
if (beat.container_stage)
|
|
4731
|
+
metaBeat.container_stage = beat.container_stage;
|
|
4732
|
+
if (beat.visual_task)
|
|
4733
|
+
metaBeat.visual_task = beat.visual_task;
|
|
4734
|
+
if (beat.lane !== "A_ROLL" && beat.handoff)
|
|
4735
|
+
metaBeat.handoff = beat.handoff;
|
|
4736
|
+
split.beats.push(metaBeat);
|
|
4737
|
+
if (isShrunk) {
|
|
4738
|
+
shrunk.push({ beat: beat.id, kept: spanIds.length - droppedCount, dropped: droppedCount, track_st, track_ed });
|
|
4739
|
+
}
|
|
4740
|
+
const h = beat.handoff ?? {};
|
|
4741
|
+
const compositionId = `${opts.projectSlug}-${beat.id}`;
|
|
4742
|
+
if (beat.lane === "RRV_MG") {
|
|
4743
|
+
dispatch.rrv_mg.push({
|
|
4744
|
+
beat: beat.id,
|
|
4745
|
+
composition_id: compositionId,
|
|
4746
|
+
duration: typeof h.duration_hint === "number" ? h.duration_hint : null,
|
|
4747
|
+
theme: h.theme,
|
|
4748
|
+
bg: h.bg,
|
|
4749
|
+
slug_hint: h.slug_hint,
|
|
4750
|
+
track_st,
|
|
4751
|
+
track_ed
|
|
4752
|
+
});
|
|
4753
|
+
} else if (beat.lane === "FILM_BROLL") {
|
|
4754
|
+
dispatch.film_broll.push({
|
|
4755
|
+
beat: beat.id,
|
|
4756
|
+
queries: Array.isArray(h.queries) ? h.queries : [],
|
|
4757
|
+
shots: h.shots,
|
|
4758
|
+
per_shot_sec: h.per_shot_sec,
|
|
4759
|
+
exclude: h.exclude,
|
|
4760
|
+
track_st,
|
|
4761
|
+
track_ed
|
|
4762
|
+
});
|
|
4763
|
+
} else if (beat.lane === "AI_DRAMA") {
|
|
4764
|
+
dispatch.ai_drama.push({ beat: beat.id, ...h, track_st, track_ed });
|
|
4765
|
+
} else if (beat.lane !== "A_ROLL") {
|
|
4766
|
+
unhandledLanes.add(beat.lane);
|
|
4767
|
+
}
|
|
4768
|
+
}
|
|
4769
|
+
return { split, dispatch, skipped, shrunk, unhandledLanes: [...unhandledLanes] };
|
|
4770
|
+
}
|
|
4771
|
+
function renderSplitMarkdown(doc, landing, meta) {
|
|
4772
|
+
const L = [];
|
|
4773
|
+
const metaById = new Map(landing.split.beats.map((b) => [b.id, b]));
|
|
4774
|
+
const skippedIds = new Set(landing.skipped.map((s) => s.beat));
|
|
4775
|
+
L.push(`# 视觉拆分稿(${meta.projectSlug})`);
|
|
4776
|
+
L.push("");
|
|
4777
|
+
L.push(`- contract_version:\`${doc.contract_version}\``);
|
|
4778
|
+
L.push(`- transcript_hash:\`${doc.transcript_hash}\``);
|
|
4779
|
+
L.push(`- projected_at:\`${meta.projectedAt}\``);
|
|
4780
|
+
L.push(`- beats:${doc.beats.length}(落轨 ${landing.split.beats.length} · 跳过 ${landing.skipped.length} · 收缩 ${landing.shrunk.length})`);
|
|
4781
|
+
L.push("");
|
|
4782
|
+
L.push("# Beat Timeline");
|
|
4783
|
+
L.push("");
|
|
4784
|
+
for (const beat of doc.beats) {
|
|
4785
|
+
const mb = metaById.get(beat.id);
|
|
4786
|
+
L.push(`## ${beat.id}${skippedIds.has(beat.id) ? "(整段被剪 · 跳过)" : mb?.shrunk ? "(部分被剪 · 已收缩)" : ""}`);
|
|
4787
|
+
L.push(`- 文稿范围:\`${beat.span.from} … ${beat.span.to}\``);
|
|
4788
|
+
L.push(`- 底轨:\`${beat.base_track}\``);
|
|
4789
|
+
L.push(`- 主层:\`${beat.lane}\``);
|
|
4790
|
+
L.push(`- 叙事功能:\`${beat.narrative}\``);
|
|
4791
|
+
L.push(`- 容器阶段:\`${beat.container_stage}\``);
|
|
4792
|
+
if (beat.rhythm)
|
|
4793
|
+
L.push(`- 节奏标签:\`${beat.rhythm}\``);
|
|
4794
|
+
L.push(`- 视觉任务:${beat.visual_task}`);
|
|
4795
|
+
L.push(`- 不可替代性:\`${beat.irreplaceability}\``);
|
|
4796
|
+
if (mb)
|
|
4797
|
+
L.push(`- 轨道时码:\`${mb.track_st}s … ${mb.track_ed}s\``);
|
|
4798
|
+
if (beat.callback_of)
|
|
4799
|
+
L.push(`- 回扣对象:\`${beat.callback_of}\``);
|
|
4800
|
+
for (const a of beat.aux_layers ?? []) {
|
|
4801
|
+
const mount = a.mount === "same_beat" ? "同 beat" : ("trigger" in a.mount) ? `触发 ${a.mount.trigger}` : `${a.mount.from} … ${a.mount.to}`;
|
|
4802
|
+
L.push(` - 辅助层 \`${a.type}\`(${mount}):${a.role}`);
|
|
4803
|
+
}
|
|
4804
|
+
L.push("");
|
|
4805
|
+
}
|
|
4806
|
+
L.push("# Production Queues");
|
|
4807
|
+
L.push("");
|
|
4808
|
+
L.push("## A_ROLL Queue");
|
|
4809
|
+
for (const b of doc.beats.filter((x) => x.lane === "A_ROLL" && !skippedIds.has(x.id))) {
|
|
4810
|
+
L.push(`- \`${b.id}\` ${b.visual_task}`);
|
|
4811
|
+
}
|
|
4812
|
+
L.push("");
|
|
4813
|
+
L.push("## RRV_MG Queue");
|
|
4814
|
+
for (const r of landing.dispatch.rrv_mg) {
|
|
4815
|
+
L.push(`- \`${r.beat}\` composition_id=\`${r.composition_id}\`${r.duration != null ? ` · ${r.duration}s` : ""}`);
|
|
4816
|
+
}
|
|
4817
|
+
L.push("");
|
|
4818
|
+
L.push("## AI_DRAMA Queue");
|
|
4819
|
+
for (const a of landing.dispatch.ai_drama)
|
|
4820
|
+
L.push(`- \`${a.beat}\` ${a.track_st}s…${a.track_ed}s`);
|
|
4821
|
+
L.push("");
|
|
4822
|
+
L.push("## FILM_BROLL Queue");
|
|
4823
|
+
for (const f of landing.dispatch.film_broll)
|
|
4824
|
+
L.push(`- \`${f.beat}\` queries=[${f.queries.join(" / ")}]`);
|
|
4825
|
+
L.push("");
|
|
4826
|
+
return L.join(`
|
|
4827
|
+
`);
|
|
4828
|
+
}
|
|
4829
|
+
|
|
4830
|
+
// src/lib/column-config.ts
|
|
4831
|
+
var DEFAULT_COLUMN_CONFIG = {
|
|
4832
|
+
meta: { id: "real-roam-guide", name: "实在界漫游指南(内置默认)" },
|
|
4833
|
+
vocab: {
|
|
4834
|
+
narrative: [...NARRATIVES],
|
|
4835
|
+
container_stage: [...CONTAINER_STAGES],
|
|
4836
|
+
base_track: [...BASE_TRACKS]
|
|
4837
|
+
},
|
|
4838
|
+
lanes: { enabled: [...LANES] },
|
|
4839
|
+
fallback: { unknown_narrative: "reject" }
|
|
4840
|
+
};
|
|
4841
|
+
function columnsDir() {
|
|
4842
|
+
return join5(gitruckHome(), "columns");
|
|
4843
|
+
}
|
|
4844
|
+
var uniq = (xs) => [...new Set(xs)];
|
|
4845
|
+
function strArr(v) {
|
|
4846
|
+
if (!Array.isArray(v))
|
|
4847
|
+
return;
|
|
4848
|
+
return v.filter((x) => typeof x === "string");
|
|
4849
|
+
}
|
|
4850
|
+
function foldColumnConfigs(layers) {
|
|
4851
|
+
const out = {};
|
|
4852
|
+
for (const l of layers) {
|
|
4853
|
+
if (!l || typeof l !== "object" || Array.isArray(l))
|
|
4854
|
+
continue;
|
|
4855
|
+
if (l.meta && typeof l.meta === "object")
|
|
4856
|
+
out.meta = { ...out.meta, ...l.meta };
|
|
4857
|
+
if (l.vocab && typeof l.vocab === "object") {
|
|
4858
|
+
out.vocab ??= {};
|
|
4859
|
+
for (const k of ["narrative", "container_stage", "base_track"]) {
|
|
4860
|
+
const add = strArr(l.vocab[k]);
|
|
4861
|
+
if (add)
|
|
4862
|
+
out.vocab[k] = uniq([...out.vocab[k] ?? [], ...add]);
|
|
4863
|
+
}
|
|
4864
|
+
}
|
|
4865
|
+
if (l.lanes && typeof l.lanes === "object") {
|
|
4866
|
+
out.lanes ??= {};
|
|
4867
|
+
const en = strArr(l.lanes.enabled);
|
|
4868
|
+
if (en)
|
|
4869
|
+
out.lanes.enabled = uniq([...out.lanes.enabled ?? [], ...en]);
|
|
4870
|
+
if (l.lanes.appearance && typeof l.lanes.appearance === "object") {
|
|
4871
|
+
out.lanes.appearance = l.lanes.appearance;
|
|
4872
|
+
}
|
|
4873
|
+
}
|
|
4874
|
+
if (l.broll && typeof l.broll === "object") {
|
|
4875
|
+
out.broll ??= {};
|
|
4876
|
+
const tags = strArr(l.broll.column_tag_ids);
|
|
4877
|
+
if (tags)
|
|
4878
|
+
out.broll.column_tag_ids = uniq([...out.broll.column_tag_ids ?? [], ...tags]);
|
|
4879
|
+
const fa = strArr(l.broll.facet_allowed);
|
|
4880
|
+
if (fa) {
|
|
4881
|
+
out.broll.facet_allowed = out.broll.facet_allowed ? out.broll.facet_allowed.filter((x) => fa.includes(x)) : [...fa];
|
|
4882
|
+
}
|
|
4883
|
+
if (typeof l.broll.material_class_policy === "string")
|
|
4884
|
+
out.broll.material_class_policy = l.broll.material_class_policy;
|
|
4885
|
+
if (l.broll.facet_defaults && typeof l.broll.facet_defaults === "object")
|
|
4886
|
+
out.broll.facet_defaults = l.broll.facet_defaults;
|
|
4887
|
+
}
|
|
4888
|
+
if (l.style !== undefined)
|
|
4889
|
+
out.style = l.style;
|
|
4890
|
+
if (l.fallback && typeof l.fallback === "object") {
|
|
4891
|
+
const un = l.fallback.unknown_narrative;
|
|
4892
|
+
if (un === "allow" || un === "reject")
|
|
4893
|
+
out.fallback = { unknown_narrative: un };
|
|
4894
|
+
}
|
|
4895
|
+
}
|
|
4896
|
+
return out;
|
|
4897
|
+
}
|
|
4898
|
+
function readLocalColumn(columnId, dir, warnings) {
|
|
4899
|
+
const p = join5(dir, `${columnId}.json`);
|
|
4900
|
+
if (!existsSync5(p)) {
|
|
4901
|
+
warnings.push(`栏目配置不存在:${p},回落内置默认`);
|
|
4902
|
+
return;
|
|
4903
|
+
}
|
|
4904
|
+
try {
|
|
4905
|
+
const parsed = JSON.parse(readFileSync2(p, "utf8"));
|
|
4906
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
4907
|
+
warnings.push(`栏目配置格式异常(非 JSON 对象):${p},回落内置默认`);
|
|
4908
|
+
return;
|
|
4909
|
+
}
|
|
4910
|
+
return parsed;
|
|
4911
|
+
} catch {
|
|
4912
|
+
warnings.push(`栏目配置损坏(JSON 解析失败):${p},回落内置默认`);
|
|
4913
|
+
return;
|
|
4914
|
+
}
|
|
4915
|
+
}
|
|
4916
|
+
function resolveColumnConfig(opts = {}) {
|
|
4917
|
+
const warnings = [];
|
|
4918
|
+
const layers = [DEFAULT_COLUMN_CONFIG];
|
|
4919
|
+
if (opts.columnId) {
|
|
4920
|
+
const l2 = readLocalColumn(opts.columnId, opts.columnsDir ?? columnsDir(), warnings);
|
|
4921
|
+
if (l2)
|
|
4922
|
+
layers.push(l2);
|
|
4923
|
+
}
|
|
4924
|
+
const config = foldColumnConfigs(layers);
|
|
4925
|
+
config.style = normalizeColumnStyle(config.style, warnings);
|
|
4926
|
+
for (const n of producesNotices(config.style))
|
|
4927
|
+
warnings.push(n);
|
|
4928
|
+
return { config, warnings };
|
|
4929
|
+
}
|
|
4930
|
+
var HANDOFF_REGISTRY = LANES;
|
|
4931
|
+
function normalizeEntries(list, kind, warnings) {
|
|
4932
|
+
if (list === undefined)
|
|
4933
|
+
return;
|
|
4934
|
+
if (!Array.isArray(list)) {
|
|
4935
|
+
warnings.push(`style.${kind} 非数组,已忽略`);
|
|
4936
|
+
return;
|
|
4937
|
+
}
|
|
4938
|
+
const out = [];
|
|
4939
|
+
list.forEach((e, i) => {
|
|
4940
|
+
if (typeof e !== "object" || e === null || Array.isArray(e)) {
|
|
4941
|
+
warnings.push(`style.${kind}[${i}] 非对象,已跳过`);
|
|
4942
|
+
return;
|
|
4943
|
+
}
|
|
4944
|
+
const o = e;
|
|
4945
|
+
if (typeof o.id !== "string" || typeof o.ref !== "string" || !o.id || !o.ref) {
|
|
4946
|
+
warnings.push(`style.${kind}[${i}] 缺 id/ref,已跳过`);
|
|
4947
|
+
return;
|
|
4948
|
+
}
|
|
4949
|
+
out.push(e);
|
|
4950
|
+
});
|
|
4951
|
+
return out;
|
|
4952
|
+
}
|
|
4953
|
+
function normalizeColumnStyle(raw, warnings) {
|
|
4954
|
+
if (raw === undefined)
|
|
4955
|
+
return;
|
|
4956
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
4957
|
+
warnings.push("style 块非对象,已忽略");
|
|
4958
|
+
return;
|
|
4959
|
+
}
|
|
4960
|
+
const r = raw;
|
|
4961
|
+
const style = {};
|
|
4962
|
+
const skills = normalizeEntries(r.skills, "skills", warnings);
|
|
4963
|
+
if (skills)
|
|
4964
|
+
style.skills = skills;
|
|
4965
|
+
const shared = normalizeEntries(r.shared, "shared", warnings);
|
|
4966
|
+
if (shared)
|
|
4967
|
+
style.shared = shared;
|
|
4968
|
+
if (typeof r.bundle_ref === "string")
|
|
4969
|
+
style.bundle_ref = r.bundle_ref;
|
|
4970
|
+
return style;
|
|
4971
|
+
}
|
|
4972
|
+
function editDistance(a, b) {
|
|
4973
|
+
const m = a.length;
|
|
4974
|
+
const n = b.length;
|
|
4975
|
+
if (m === 0)
|
|
4976
|
+
return n;
|
|
4977
|
+
if (n === 0)
|
|
4978
|
+
return m;
|
|
4979
|
+
let prev = Array.from({ length: n + 1 }, (_, j) => j);
|
|
4980
|
+
for (let i = 1;i <= m; i++) {
|
|
4981
|
+
const cur = [i];
|
|
4982
|
+
for (let j = 1;j <= n; j++) {
|
|
4983
|
+
cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
4984
|
+
}
|
|
4985
|
+
prev = cur;
|
|
4986
|
+
}
|
|
4987
|
+
return prev[n];
|
|
4988
|
+
}
|
|
4989
|
+
function producesValues(e) {
|
|
4990
|
+
if (typeof e.produces === "string")
|
|
4991
|
+
return [e.produces];
|
|
4992
|
+
if (Array.isArray(e.produces))
|
|
4993
|
+
return e.produces.filter((x) => typeof x === "string");
|
|
4994
|
+
return [];
|
|
4995
|
+
}
|
|
4996
|
+
function producesNotices(style) {
|
|
4997
|
+
const notices = [];
|
|
4998
|
+
if (!style?.skills)
|
|
4999
|
+
return notices;
|
|
5000
|
+
const seen = new Set;
|
|
5001
|
+
for (const e of style.skills) {
|
|
5002
|
+
if (e.routing === "none")
|
|
5003
|
+
continue;
|
|
5004
|
+
for (const v of producesValues(e)) {
|
|
5005
|
+
if (HANDOFF_REGISTRY.includes(v) || seen.has(v))
|
|
5006
|
+
continue;
|
|
5007
|
+
const near = HANDOFF_REGISTRY.find((r) => r.toLowerCase() === v.toLowerCase() || editDistance(r.toUpperCase(), v.toUpperCase()) <= 2);
|
|
5008
|
+
if (near) {
|
|
5009
|
+
seen.add(v);
|
|
5010
|
+
notices.push(`style.skills[${e.id}].produces="${v}" 疑似拼写(接近注册类型 "${near}");如确为管线外产物可设 routing:"none"`);
|
|
5011
|
+
}
|
|
5012
|
+
}
|
|
5013
|
+
}
|
|
5014
|
+
return notices;
|
|
5015
|
+
}
|
|
5016
|
+
function effectiveVocab(config) {
|
|
5017
|
+
return {
|
|
5018
|
+
narrative: config.vocab?.narrative ?? [...NARRATIVES],
|
|
5019
|
+
container_stage: config.vocab?.container_stage ?? [...CONTAINER_STAGES],
|
|
5020
|
+
base_track: config.vocab?.base_track ?? [...BASE_TRACKS],
|
|
5021
|
+
unknown_narrative: config.fallback?.unknown_narrative
|
|
5022
|
+
};
|
|
5023
|
+
}
|
|
4465
5024
|
|
|
4466
5025
|
// src/lib/ffmpeg.ts
|
|
4467
5026
|
import { spawn as spawn2, spawnSync as spawnSync2 } from "node:child_process";
|
|
4468
|
-
import { existsSync as
|
|
4469
|
-
import { join as
|
|
5027
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
5028
|
+
import { join as join6 } from "node:path";
|
|
4470
5029
|
var isWin = process.platform === "win32";
|
|
4471
5030
|
var bin = (base) => isWin ? `${base}.exe` : base;
|
|
4472
5031
|
var FFMPEG_INSTALL_HINT = `未找到 ffmpeg/ffprobe。请把二者放到 ${ffmpegDir()}(agent 可代办:先查本地确实缺失才拉,` + `面向国内用户优先国内加速站点——GitHub 代理 pass-through 拉 BtbN/gyan.dev 官方静态构建,或同合云自建镜像,` + `并做 sha256 校验),或用 --ffmpeg-path <目录> 指定已装位置。`;
|
|
@@ -4488,9 +5047,9 @@ function resolveFfmpeg(ffmpegPath) {
|
|
|
4488
5047
|
dirs.push([ffmpegDir(), "~/.gitruck/ffmpeg"]);
|
|
4489
5048
|
let found = null;
|
|
4490
5049
|
for (const [dir, label] of dirs) {
|
|
4491
|
-
const ff =
|
|
4492
|
-
const fp =
|
|
4493
|
-
if (
|
|
5050
|
+
const ff = join6(dir, bin("ffmpeg"));
|
|
5051
|
+
const fp = join6(dir, bin("ffprobe"));
|
|
5052
|
+
if (existsSync6(ff) && existsSync6(fp)) {
|
|
4494
5053
|
found = { ffmpeg: ff, ffprobe: fp, source: label };
|
|
4495
5054
|
break;
|
|
4496
5055
|
}
|
|
@@ -4571,12 +5130,12 @@ function probeCapabilities(res) {
|
|
|
4571
5130
|
}
|
|
4572
5131
|
|
|
4573
5132
|
// src/lib/version.ts
|
|
4574
|
-
import { readFileSync as
|
|
4575
|
-
import { join as
|
|
5133
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
5134
|
+
import { join as join7 } from "node:path";
|
|
4576
5135
|
var REGISTRY = "https://registry.npmjs.org/@gitruck%2Fcli";
|
|
4577
5136
|
function currentVersion() {
|
|
4578
5137
|
try {
|
|
4579
|
-
const { version } = JSON.parse(
|
|
5138
|
+
const { version } = JSON.parse(readFileSync3(join7(packageRoot(), "package.json"), "utf8"));
|
|
4580
5139
|
return version;
|
|
4581
5140
|
} catch {
|
|
4582
5141
|
return "0.0.0";
|
|
@@ -4658,7 +5217,7 @@ async function runDoctor() {
|
|
|
4658
5217
|
}
|
|
4659
5218
|
rows.push({ name: "云端连通 + 鉴权", status: apiStatus, detail: apiDetail });
|
|
4660
5219
|
const draftDir = resolveJianyingDraftDir(undefined);
|
|
4661
|
-
const draftOk = !!draftDir &&
|
|
5220
|
+
const draftOk = !!draftDir && existsSync7(draftDir);
|
|
4662
5221
|
rows.push({
|
|
4663
5222
|
name: "剪映草稿目录",
|
|
4664
5223
|
status: draftOk ? "ok" : "warn",
|
|
@@ -4666,8 +5225,15 @@ async function runDoctor() {
|
|
|
4666
5225
|
});
|
|
4667
5226
|
rows.push({
|
|
4668
5227
|
name: "配置文件",
|
|
4669
|
-
status:
|
|
4670
|
-
detail:
|
|
5228
|
+
status: existsSync7(configPath()) ? "ok" : "warn",
|
|
5229
|
+
detail: existsSync7(configPath()) ? configPath() : `未生成 —— 跑 gtrk init(${configPath()})`
|
|
5230
|
+
});
|
|
5231
|
+
const col = uc.defaultColumn;
|
|
5232
|
+
const colFile = col ? join8(columnsDir(), `${col}.json`) : undefined;
|
|
5233
|
+
rows.push({
|
|
5234
|
+
name: "当前栏目",
|
|
5235
|
+
status: "ok",
|
|
5236
|
+
detail: col ? `${col}${colFile && existsSync7(colFile) ? `(${colFile})` : `(⚠ 配置文件缺失:${colFile},将回落内置默认)`}` : "内置默认 —— 想建自己栏目的风格体系,跑 /gtrk-style-maker(不建也能直接用默认)"
|
|
4671
5237
|
});
|
|
4672
5238
|
const ff = resolveFfmpeg();
|
|
4673
5239
|
if (ff) {
|
|
@@ -4714,7 +5280,7 @@ gtrk 体检:
|
|
|
4714
5280
|
}
|
|
4715
5281
|
|
|
4716
5282
|
// src/commands/init.ts
|
|
4717
|
-
var GUIDE_IMAGE =
|
|
5283
|
+
var GUIDE_IMAGE = join9(packageRoot(), "assets", "jianying-draft-path.png");
|
|
4718
5284
|
function registerInit(program2) {
|
|
4719
5285
|
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);
|
|
4720
5286
|
}
|
|
@@ -4753,7 +5319,7 @@ async function runInit(opts) {
|
|
|
4753
5319
|
defaultValue: existing.apiBase ?? DEFAULT_API_BASE
|
|
4754
5320
|
})).trim();
|
|
4755
5321
|
let jianyingDraftDir;
|
|
4756
|
-
if (existing.jianyingDraftDir &&
|
|
5322
|
+
if (existing.jianyingDraftDir && existsSync8(existing.jianyingDraftDir)) {
|
|
4757
5323
|
if (await promptConfirm(`剪映草稿目录现为 ${existing.jianyingDraftDir},保留吗?`, true)) {
|
|
4758
5324
|
jianyingDraftDir = existing.jianyingDraftDir;
|
|
4759
5325
|
}
|
|
@@ -4769,7 +5335,7 @@ async function runInit(opts) {
|
|
|
4769
5335
|
openFile(GUIDE_IMAGE);
|
|
4770
5336
|
const manual = (await promptText("剪映草稿根目录(…\\com.lveditor.draft),留空跳过:")).trim();
|
|
4771
5337
|
if (manual) {
|
|
4772
|
-
if (
|
|
5338
|
+
if (existsSync8(manual))
|
|
4773
5339
|
jianyingDraftDir = resolve2(manual);
|
|
4774
5340
|
else
|
|
4775
5341
|
log.warn(`目录不存在,已跳过:${manual}`);
|
|
@@ -4794,6 +5360,7 @@ async function afterConfigDoctor() {
|
|
|
4794
5360
|
log.step("装好了!两种用法任选:");
|
|
4795
5361
|
log.info('① 命令行直接剪:gtrk oralcut "<毛片.mp4>" --script "<文字稿.txt>"(无稿就别加 --script)');
|
|
4796
5362
|
log.info("② 重启你常用的 AI agent(Claude / Codex / Trae / WorkBuddy 等),用 /gtrk-oralcut <你的口播剪辑需求>,一句话交给它,体验更智能的剪辑~");
|
|
5363
|
+
log.info("想有自己栏目的风格体系?在 agent 里跑 /gtrk-style-maker 建一次「你的厨房」(skill 家族 + 栏目配置);不建就直接用默认,照常开剪。");
|
|
4797
5364
|
} else {
|
|
4798
5365
|
log.warn("上面体检有项没通,按提示处理好再开剪(多半是 API Key 或剪映目录)。");
|
|
4799
5366
|
}
|
|
@@ -4835,9 +5402,9 @@ function registerInstall(program2) {
|
|
|
4835
5402
|
}
|
|
4836
5403
|
|
|
4837
5404
|
// src/commands/oralcut.ts
|
|
4838
|
-
import { resolve as resolve3, join as
|
|
5405
|
+
import { resolve as resolve3, join as join14, dirname as dirname2, basename as basename5, extname as extname2 } from "node:path";
|
|
4839
5406
|
import { mkdir as mkdir4, writeFile as writeFile5, readFile as readFile3 } from "node:fs/promises";
|
|
4840
|
-
import { existsSync as
|
|
5407
|
+
import { existsSync as existsSync12 } from "node:fs";
|
|
4841
5408
|
|
|
4842
5409
|
// src/lib/config.ts
|
|
4843
5410
|
function loadConfig() {
|
|
@@ -4966,9 +5533,9 @@ async function download(url, dest) {
|
|
|
4966
5533
|
}
|
|
4967
5534
|
|
|
4968
5535
|
// src/lib/upload-cache.ts
|
|
4969
|
-
import { join as
|
|
5536
|
+
import { join as join10 } from "node:path";
|
|
4970
5537
|
import { stat as stat3, mkdir, readFile, writeFile as writeFile2 } from "node:fs/promises";
|
|
4971
|
-
import { existsSync as
|
|
5538
|
+
import { existsSync as existsSync9 } from "node:fs";
|
|
4972
5539
|
|
|
4973
5540
|
// src/lib/chunk-upload.ts
|
|
4974
5541
|
var import_hash_wasm = __toESM(require_index_umd(), 1);
|
|
@@ -5200,14 +5767,14 @@ async function putPart(cfg, uploadId, idx, view) {
|
|
|
5200
5767
|
|
|
5201
5768
|
// src/lib/upload-cache.ts
|
|
5202
5769
|
var CACHE_DIR = gitruckHome();
|
|
5203
|
-
var CACHE_FILE =
|
|
5204
|
-
var SESSION_FILE =
|
|
5770
|
+
var CACHE_FILE = join10(CACHE_DIR, "upload-cache.json");
|
|
5771
|
+
var SESSION_FILE = join10(CACHE_DIR, "upload-sessions.json");
|
|
5205
5772
|
async function fingerprint(path) {
|
|
5206
5773
|
const s = await stat3(path);
|
|
5207
5774
|
return `${s.size}:${Math.round(s.mtimeMs)}`;
|
|
5208
5775
|
}
|
|
5209
5776
|
async function load() {
|
|
5210
|
-
if (!
|
|
5777
|
+
if (!existsSync9(CACHE_FILE))
|
|
5211
5778
|
return {};
|
|
5212
5779
|
try {
|
|
5213
5780
|
return JSON.parse(await readFile(CACHE_FILE, "utf8"));
|
|
@@ -5228,7 +5795,7 @@ async function invalidateUpload(path) {
|
|
|
5228
5795
|
}
|
|
5229
5796
|
}
|
|
5230
5797
|
async function loadSessions() {
|
|
5231
|
-
if (!
|
|
5798
|
+
if (!existsSync9(SESSION_FILE))
|
|
5232
5799
|
return {};
|
|
5233
5800
|
try {
|
|
5234
5801
|
return JSON.parse(await readFile(SESSION_FILE, "utf8"));
|
|
@@ -5283,8 +5850,8 @@ async function uploadCached(cfg, path, opts) {
|
|
|
5283
5850
|
|
|
5284
5851
|
// src/lib/media.ts
|
|
5285
5852
|
import { mkdir as mkdir2, stat as stat4 } from "node:fs/promises";
|
|
5286
|
-
import { existsSync as
|
|
5287
|
-
import { basename as basename3, extname, join as
|
|
5853
|
+
import { existsSync as existsSync10 } from "node:fs";
|
|
5854
|
+
import { basename as basename3, extname, join as join11 } from "node:path";
|
|
5288
5855
|
function parseFps(rate) {
|
|
5289
5856
|
if (typeof rate !== "string")
|
|
5290
5857
|
return 0;
|
|
@@ -5334,12 +5901,12 @@ async function artifactPath(inputAbs, ext) {
|
|
|
5334
5901
|
const s = await stat4(inputAbs);
|
|
5335
5902
|
const base = basename3(inputAbs, extname(inputAbs));
|
|
5336
5903
|
await mkdir2(audioCacheDir(), { recursive: true });
|
|
5337
|
-
return
|
|
5904
|
+
return join11(audioCacheDir(), `${base}.${s.size}_${Math.round(s.mtimeMs)}.${ext}`);
|
|
5338
5905
|
}
|
|
5339
5906
|
async function extractAudio(inputAbs, ffmpegPath) {
|
|
5340
5907
|
const { ffmpeg } = requireFfmpeg(ffmpegPath);
|
|
5341
5908
|
const out = await artifactPath(inputAbs, "mp3");
|
|
5342
|
-
if (
|
|
5909
|
+
if (existsSync10(out))
|
|
5343
5910
|
return out;
|
|
5344
5911
|
await runFfmpeg(ffmpeg, [
|
|
5345
5912
|
"-y",
|
|
@@ -5363,7 +5930,7 @@ async function extractAudio(inputAbs, ffmpegPath) {
|
|
|
5363
5930
|
async function compress720p(inputAbs, ffmpegPath) {
|
|
5364
5931
|
const { ffmpeg } = requireFfmpeg(ffmpegPath);
|
|
5365
5932
|
const out = await artifactPath(inputAbs, "720p.mp4");
|
|
5366
|
-
if (
|
|
5933
|
+
if (existsSync10(out))
|
|
5367
5934
|
return out;
|
|
5368
5935
|
await runFfmpeg(ffmpeg, [
|
|
5369
5936
|
"-y",
|
|
@@ -5395,14 +5962,14 @@ function assertDurationConsistent(originalDuration, artifactAbs, ffmpegPath, tol
|
|
|
5395
5962
|
}
|
|
5396
5963
|
|
|
5397
5964
|
// src/lib/materialize.ts
|
|
5398
|
-
import { join as
|
|
5965
|
+
import { join as join13, basename as basename4 } from "node:path";
|
|
5399
5966
|
import { mkdir as mkdir3, cp, writeFile as writeFile4 } from "node:fs/promises";
|
|
5400
5967
|
|
|
5401
5968
|
// src/lib/render.ts
|
|
5402
5969
|
import { writeFile as writeFile3, unlink, readFile as readFile2 } from "node:fs/promises";
|
|
5403
|
-
import { existsSync as
|
|
5970
|
+
import { existsSync as existsSync11 } from "node:fs";
|
|
5404
5971
|
import { tmpdir } from "node:os";
|
|
5405
|
-
import { join as
|
|
5972
|
+
import { join as join12 } from "node:path";
|
|
5406
5973
|
var AUDIO_SAMPLE_RATE = 48000;
|
|
5407
5974
|
var AUDIO_LAYOUT = "stereo";
|
|
5408
5975
|
var DEFAULT_CRF = 18;
|
|
@@ -5542,11 +6109,25 @@ function buildFilterGraph(gtrk, materialPaths, params = {}) {
|
|
|
5542
6109
|
return { inputs, graph: chains.join(";"), total };
|
|
5543
6110
|
}
|
|
5544
6111
|
function materialPathsFromGtrk(gtrk) {
|
|
6112
|
+
const used = new Set;
|
|
6113
|
+
const sortedV = sortedTracks(gtrk.video_track || []);
|
|
6114
|
+
const consumers = [sortedV[0], ...gtrk.audio_track || []];
|
|
6115
|
+
for (const t of consumers) {
|
|
6116
|
+
if (!t)
|
|
6117
|
+
continue;
|
|
6118
|
+
for (const c3 of t.track_timeline || []) {
|
|
6119
|
+
const m = c3.material;
|
|
6120
|
+
if (m != null)
|
|
6121
|
+
used.add(String(m));
|
|
6122
|
+
}
|
|
6123
|
+
}
|
|
5545
6124
|
const map = {};
|
|
5546
6125
|
for (const m of gtrk.materials || []) {
|
|
6126
|
+
if (!used.has(String(m.id)))
|
|
6127
|
+
continue;
|
|
5547
6128
|
if (!m.path)
|
|
5548
6129
|
throw new Error(`gtrk 素材 ${m.id} 缺 path(source_path),无法本地渲染`);
|
|
5549
|
-
if (!
|
|
6130
|
+
if (!existsSync11(m.path))
|
|
5550
6131
|
throw new Error(`gtrk 素材文件不存在:${m.path}`);
|
|
5551
6132
|
map[String(m.id)] = m.path;
|
|
5552
6133
|
}
|
|
@@ -5560,7 +6141,7 @@ async function renderGtrk(gtrk, outputPath, opts = {}) {
|
|
|
5560
6141
|
const { ffmpeg } = requireFfmpeg(opts.ffmpegPath);
|
|
5561
6142
|
const materialPaths = materialPathsFromGtrk(gtrk);
|
|
5562
6143
|
const { inputs, graph, total } = buildFilterGraph(gtrk, materialPaths, { crf });
|
|
5563
|
-
const filterFile =
|
|
6144
|
+
const filterFile = join12(tmpdir(), `gtrk-filter-${process.pid}-${inputs.length}.txt`);
|
|
5564
6145
|
await writeFile3(filterFile, graph, "utf8");
|
|
5565
6146
|
try {
|
|
5566
6147
|
const args = ["-y"];
|
|
@@ -5610,7 +6191,7 @@ async function materializeResult(opts) {
|
|
|
5610
6191
|
throw new Error("任务无工程文件产物(检查 project_formats / 任务是否产出)");
|
|
5611
6192
|
const errors = { ...output.errors ?? {} };
|
|
5612
6193
|
await mkdir3(outDir, { recursive: true });
|
|
5613
|
-
const resultPath =
|
|
6194
|
+
const resultPath = join13(outDir, "result.json");
|
|
5614
6195
|
const writeResult = async (extra) => {
|
|
5615
6196
|
const r = {
|
|
5616
6197
|
ok: Object.keys(errors).length === 0,
|
|
@@ -5632,9 +6213,9 @@ async function materializeResult(opts) {
|
|
|
5632
6213
|
const byFormat = {};
|
|
5633
6214
|
for (const f of files) {
|
|
5634
6215
|
const base = baseFormat(f.format);
|
|
5635
|
-
const fmtDir =
|
|
6216
|
+
const fmtDir = join13(outDir, base);
|
|
5636
6217
|
await mkdir3(fmtDir, { recursive: true });
|
|
5637
|
-
const dest =
|
|
6218
|
+
const dest = join13(fmtDir, f.filename);
|
|
5638
6219
|
try {
|
|
5639
6220
|
await dl(f.download_url, dest);
|
|
5640
6221
|
(byFormat[base] ??= []).push(dest);
|
|
@@ -5651,9 +6232,9 @@ async function materializeResult(opts) {
|
|
|
5651
6232
|
let jianyingDraftPath = null;
|
|
5652
6233
|
if (byFormat.jianying && opts.draftDir) {
|
|
5653
6234
|
try {
|
|
5654
|
-
jianyingDraftPath =
|
|
6235
|
+
jianyingDraftPath = join13(opts.draftDir, basename4(outDir));
|
|
5655
6236
|
await mkdir3(jianyingDraftPath, { recursive: true });
|
|
5656
|
-
await cp(
|
|
6237
|
+
await cp(join13(outDir, "jianying"), jianyingDraftPath, { recursive: true });
|
|
5657
6238
|
log.info(`剪映草稿已落到:${jianyingDraftPath}`);
|
|
5658
6239
|
} catch (e) {
|
|
5659
6240
|
jianyingDraftPath = null;
|
|
@@ -5670,7 +6251,7 @@ async function materializeResult(opts) {
|
|
|
5670
6251
|
log.step("本地渲染成片(ffmpeg)…");
|
|
5671
6252
|
const project = await readGtrkFile(gtrkPath);
|
|
5672
6253
|
const name = opts.projName ?? gtrkSourceName(project) ?? taskId;
|
|
5673
|
-
const outMp4 =
|
|
6254
|
+
const outMp4 = join13(outDir, `${name}.mp4`);
|
|
5674
6255
|
const r = await renderGtrk(project, outMp4, {
|
|
5675
6256
|
crf: opts.crf != null ? Number(opts.crf) : undefined,
|
|
5676
6257
|
codec: opts.codec,
|
|
@@ -5692,7 +6273,7 @@ async function materializeResult(opts) {
|
|
|
5692
6273
|
log.step("三方打开(产物已就位,按需自取):");
|
|
5693
6274
|
for (const base of Object.keys(byFormat)) {
|
|
5694
6275
|
const meta = FORMAT_META[base];
|
|
5695
|
-
const target = base === "jianying" ? jianyingDraftPath ??
|
|
6276
|
+
const target = base === "jianying" ? jianyingDraftPath ?? join13(outDir, "jianying") : byFormat[base][0];
|
|
5696
6277
|
console.log(` • ${meta?.label ?? base}:${meta?.openHint(target) ?? target}`);
|
|
5697
6278
|
}
|
|
5698
6279
|
if (rendered)
|
|
@@ -5760,18 +6341,18 @@ async function runOralCut(input, opts) {
|
|
|
5760
6341
|
routeLogsToStderr();
|
|
5761
6342
|
const cfg = loadConfig();
|
|
5762
6343
|
const inputAbs = resolve3(input);
|
|
5763
|
-
if (!
|
|
6344
|
+
if (!existsSync12(inputAbs))
|
|
5764
6345
|
throw new Error(`毛片不存在:${inputAbs}`);
|
|
5765
6346
|
const projName = basename5(inputAbs, extname2(inputAbs));
|
|
5766
6347
|
const formats = opts.formats.split(",").map((s) => s.trim()).filter(Boolean);
|
|
5767
6348
|
if (opts.render && !formats.includes("gtrk"))
|
|
5768
6349
|
formats.push("gtrk");
|
|
5769
6350
|
const wantJianying = formats.some((f) => f === "jianying" || f === "capcut");
|
|
5770
|
-
const outDir = resolve3(opts.out ??
|
|
6351
|
+
const outDir = resolve3(opts.out ?? join14(dirname2(inputAbs), `${projName}-video-project-${timestamp()}`));
|
|
5771
6352
|
let scriptPath = opts.script ? resolve3(opts.script) : undefined;
|
|
5772
6353
|
if (!scriptPath) {
|
|
5773
|
-
const sibling =
|
|
5774
|
-
if (
|
|
6354
|
+
const sibling = join14(dirname2(inputAbs), `${projName}.txt`);
|
|
6355
|
+
if (existsSync12(sibling)) {
|
|
5775
6356
|
scriptPath = sibling;
|
|
5776
6357
|
log.info(`自动识别到同名文稿:${sibling}(按有稿剪辑;不想用就改名或显式 --script)`);
|
|
5777
6358
|
}
|
|
@@ -5837,7 +6418,7 @@ async function runOralCut(input, opts) {
|
|
|
5837
6418
|
}
|
|
5838
6419
|
log.info(`task_id = ${taskId}`);
|
|
5839
6420
|
await mkdir4(outDir, { recursive: true });
|
|
5840
|
-
await writeFile5(
|
|
6421
|
+
await writeFile5(join14(outDir, "task.json"), JSON.stringify({ taskId, taskType: TASK_TYPE, fileId: up.fileId, source: inputAbs, formats, createdAt: new Date().toISOString() }, null, 2));
|
|
5841
6422
|
log.step("④ 云端处理中(每 5s 轮询)…");
|
|
5842
6423
|
const result = await pollTask(cfg, TASK_TYPE, taskId, (status, progress) => {
|
|
5843
6424
|
log.tick(`${status}${progress != null ? ` ${Math.round(progress)}%` : ""}`);
|
|
@@ -5861,7 +6442,7 @@ async function runOralCut(input, opts) {
|
|
|
5861
6442
|
}
|
|
5862
6443
|
|
|
5863
6444
|
// src/commands/oralcut-result.ts
|
|
5864
|
-
import { resolve as resolve4, join as
|
|
6445
|
+
import { resolve as resolve4, join as join15 } from "node:path";
|
|
5865
6446
|
var TASK_TYPE2 = "cli/video_oral_cut_for_cli";
|
|
5866
6447
|
function timestamp2() {
|
|
5867
6448
|
const d = new Date;
|
|
@@ -5895,7 +6476,7 @@ async function runOralCutResult(taskId, opts) {
|
|
|
5895
6476
|
const pct = got.progress != null ? ` ${Math.round(got.progress)}%` : "";
|
|
5896
6477
|
throw new Error(`任务尚未完成(当前 ${got.status || "未知"}${pct}),暂无法取回结果;请稍后再试。`);
|
|
5897
6478
|
}
|
|
5898
|
-
const outDir = resolve4(opts.out ??
|
|
6479
|
+
const outDir = resolve4(opts.out ?? join15(process.cwd(), `${taskId}-video-project-${timestamp2()}`));
|
|
5899
6480
|
const draftDir = resolveJianyingDraftDir(opts.jianyingDraftDir);
|
|
5900
6481
|
await materializeResult({
|
|
5901
6482
|
outDir,
|
|
@@ -5955,16 +6536,16 @@ function registerUpgrade(program2) {
|
|
|
5955
6536
|
}
|
|
5956
6537
|
|
|
5957
6538
|
// src/commands/render.ts
|
|
5958
|
-
import { resolve as resolve5, dirname as dirname3, join as
|
|
5959
|
-
import { existsSync as
|
|
6539
|
+
import { resolve as resolve5, dirname as dirname3, join as join16, basename as basename6, extname as extname3 } from "node:path";
|
|
6540
|
+
import { existsSync as existsSync13 } from "node:fs";
|
|
5960
6541
|
function registerRender(program2) {
|
|
5961
6542
|
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) => {
|
|
5962
6543
|
if (opts.json)
|
|
5963
6544
|
routeLogsToStderr();
|
|
5964
6545
|
const gtrkAbs = resolve5(gtrk);
|
|
5965
|
-
if (!
|
|
6546
|
+
if (!existsSync13(gtrkAbs))
|
|
5966
6547
|
throw new Error(`gtrk 工程不存在:${gtrkAbs}`);
|
|
5967
|
-
const outMp4 = resolve5(opts.out ??
|
|
6548
|
+
const outMp4 = resolve5(opts.out ?? join16(dirname3(gtrkAbs), `${basename6(gtrkAbs, extname3(gtrkAbs))}.mp4`));
|
|
5968
6549
|
log.step(`▶ 本地渲染:${basename6(gtrkAbs)} → ${basename6(outMp4)}`);
|
|
5969
6550
|
const project = await readGtrkFile(gtrkAbs);
|
|
5970
6551
|
const result = await renderGtrk(project, outMp4, {
|
|
@@ -5987,12 +6568,1129 @@ function registerRender(program2) {
|
|
|
5987
6568
|
});
|
|
5988
6569
|
}
|
|
5989
6570
|
|
|
6571
|
+
// src/commands/split.ts
|
|
6572
|
+
import { resolve as resolve6, join as join18, dirname as dirname5, basename as basename8 } from "node:path";
|
|
6573
|
+
import { existsSync as existsSync14 } from "node:fs";
|
|
6574
|
+
import { readFile as readFile4, writeFile as writeFile6, mkdir as mkdir5 } from "node:fs/promises";
|
|
6575
|
+
import { createHash } from "node:crypto";
|
|
6576
|
+
|
|
6577
|
+
// src/lib/projection.ts
|
|
6578
|
+
function r32(n) {
|
|
6579
|
+
return Math.round(n * 1000) / 1000;
|
|
6580
|
+
}
|
|
6581
|
+
function normClip(c3) {
|
|
6582
|
+
const clip_st = c3.clip_st ?? 0;
|
|
6583
|
+
const track_st = c3.track_st ?? 0;
|
|
6584
|
+
const dur = c3.duration ?? (c3.clip_ed != null ? c3.clip_ed - clip_st : 0);
|
|
6585
|
+
const clip_ed = c3.clip_ed ?? clip_st + dur;
|
|
6586
|
+
return { clip_st, clip_ed, track_st };
|
|
6587
|
+
}
|
|
6588
|
+
function pickMainVideoTrack(gtrk) {
|
|
6589
|
+
const tracks = gtrk.video_track ?? [];
|
|
6590
|
+
if (!tracks.length)
|
|
6591
|
+
return;
|
|
6592
|
+
let best = tracks[0];
|
|
6593
|
+
let bestIdx = best.track_index ?? 0;
|
|
6594
|
+
for (const t of tracks) {
|
|
6595
|
+
const idx = t.track_index ?? 0;
|
|
6596
|
+
if (idx < bestIdx) {
|
|
6597
|
+
best = t;
|
|
6598
|
+
bestIdx = idx;
|
|
6599
|
+
}
|
|
6600
|
+
}
|
|
6601
|
+
return best;
|
|
6602
|
+
}
|
|
6603
|
+
function projectTranscript(transcript, gtrk, opts = {}) {
|
|
6604
|
+
const materialId = String(transcript.material_id);
|
|
6605
|
+
const mainTrack = pickMainVideoTrack(gtrk);
|
|
6606
|
+
const clips = (mainTrack?.track_timeline ?? []).filter((c3) => c3.material != null && String(c3.material) === materialId).map(normClip);
|
|
6607
|
+
const entries = [];
|
|
6608
|
+
transcript.utterances.forEach((utt, sourceIndex) => {
|
|
6609
|
+
const totalWords = utt.words?.length ?? 0;
|
|
6610
|
+
const instances = [];
|
|
6611
|
+
for (const clip of clips) {
|
|
6612
|
+
const surviving = [];
|
|
6613
|
+
for (const word of utt.words ?? []) {
|
|
6614
|
+
const s = Math.max(word.st, clip.clip_st);
|
|
6615
|
+
const e = Math.min(word.ed, clip.clip_ed);
|
|
6616
|
+
if (e > s) {
|
|
6617
|
+
surviving.push({
|
|
6618
|
+
w: word.w,
|
|
6619
|
+
track_st: r32(clip.track_st + (s - clip.clip_st)),
|
|
6620
|
+
track_ed: r32(clip.track_st + (e - clip.clip_st))
|
|
6621
|
+
});
|
|
6622
|
+
}
|
|
6623
|
+
}
|
|
6624
|
+
if (surviving.length) {
|
|
6625
|
+
instances.push({
|
|
6626
|
+
track_st: Math.min(...surviving.map((x) => x.track_st)),
|
|
6627
|
+
track_ed: Math.max(...surviving.map((x) => x.track_ed)),
|
|
6628
|
+
kept_words: surviving.length,
|
|
6629
|
+
words: surviving
|
|
6630
|
+
});
|
|
6631
|
+
}
|
|
6632
|
+
}
|
|
6633
|
+
if (!instances.length) {
|
|
6634
|
+
entries.push({
|
|
6635
|
+
id: utt.id,
|
|
6636
|
+
text: utt.text,
|
|
6637
|
+
dropped: true,
|
|
6638
|
+
sourceIndex,
|
|
6639
|
+
instIndex: 0,
|
|
6640
|
+
track_st: null,
|
|
6641
|
+
track_ed: null,
|
|
6642
|
+
kept_words: 0,
|
|
6643
|
+
total_words: totalWords,
|
|
6644
|
+
words: [],
|
|
6645
|
+
sortKey: 0
|
|
6646
|
+
});
|
|
6647
|
+
} else {
|
|
6648
|
+
instances.sort((a, b) => a.track_st - b.track_st);
|
|
6649
|
+
instances.forEach((inst, instIndex) => {
|
|
6650
|
+
entries.push({
|
|
6651
|
+
id: utt.id,
|
|
6652
|
+
text: utt.text,
|
|
6653
|
+
dropped: false,
|
|
6654
|
+
sourceIndex,
|
|
6655
|
+
instIndex,
|
|
6656
|
+
track_st: inst.track_st,
|
|
6657
|
+
track_ed: inst.track_ed,
|
|
6658
|
+
kept_words: inst.kept_words,
|
|
6659
|
+
total_words: totalWords,
|
|
6660
|
+
words: inst.words,
|
|
6661
|
+
sortKey: inst.track_st
|
|
6662
|
+
});
|
|
6663
|
+
});
|
|
6664
|
+
}
|
|
6665
|
+
});
|
|
6666
|
+
let maxEd = 0;
|
|
6667
|
+
for (const e of entries) {
|
|
6668
|
+
if (e.dropped)
|
|
6669
|
+
e.sortKey = maxEd;
|
|
6670
|
+
else
|
|
6671
|
+
maxEd = Math.max(maxEd, e.track_ed ?? maxEd);
|
|
6672
|
+
}
|
|
6673
|
+
entries.sort((a, b) => a.sortKey - b.sortKey || a.sourceIndex - b.sourceIndex || a.instIndex - b.instIndex);
|
|
6674
|
+
const utterances = entries.map((e) => {
|
|
6675
|
+
const u = {
|
|
6676
|
+
id: e.id,
|
|
6677
|
+
text: e.text,
|
|
6678
|
+
track_st: e.track_st,
|
|
6679
|
+
track_ed: e.track_ed,
|
|
6680
|
+
dropped: e.dropped,
|
|
6681
|
+
kept_words: e.kept_words,
|
|
6682
|
+
total_words: e.total_words
|
|
6683
|
+
};
|
|
6684
|
+
if (opts.words)
|
|
6685
|
+
u.words = e.words;
|
|
6686
|
+
return u;
|
|
6687
|
+
});
|
|
6688
|
+
return {
|
|
6689
|
+
transcript_hash: transcript.text_hash,
|
|
6690
|
+
projected_at: opts.projectedAt ?? new Date().toISOString(),
|
|
6691
|
+
utterances
|
|
6692
|
+
};
|
|
6693
|
+
}
|
|
6694
|
+
|
|
6695
|
+
// src/lib/gtrk-writeback.ts
|
|
6696
|
+
import { readFileSync as readFileSync4, statSync, writeFileSync as writeFileSync2, renameSync, unlinkSync } from "node:fs";
|
|
6697
|
+
import { dirname as dirname4, join as join17, basename as basename7 } from "node:path";
|
|
6698
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
6699
|
+
function readGtrk(path) {
|
|
6700
|
+
const raw = readFileSync4(path, "utf8");
|
|
6701
|
+
let gtrk;
|
|
6702
|
+
try {
|
|
6703
|
+
gtrk = JSON.parse(raw);
|
|
6704
|
+
} catch (e) {
|
|
6705
|
+
throw new Error(`工程文件不是合法 JSON:${path}(${e instanceof Error ? e.message : String(e)})`);
|
|
6706
|
+
}
|
|
6707
|
+
if (typeof gtrk !== "object" || gtrk === null || Array.isArray(gtrk)) {
|
|
6708
|
+
throw new Error(`工程文件结构异常(顶层非对象):${path}`);
|
|
6709
|
+
}
|
|
6710
|
+
return { gtrk, mtimeMs: statSync(path).mtimeMs };
|
|
6711
|
+
}
|
|
6712
|
+
function assertGtrkV1(gtrk) {
|
|
6713
|
+
if (gtrk.version !== "v1") {
|
|
6714
|
+
throw new Error(`工程文件不是 v1(version=${JSON.stringify(gtrk.version)}):请用新链路重产 v1 工程后再拆分`);
|
|
6715
|
+
}
|
|
6716
|
+
}
|
|
6717
|
+
function writeStructMetaSplit(path, gtrk, splitObj, expectedMtimeMs) {
|
|
6718
|
+
const cur = statSync(path).mtimeMs;
|
|
6719
|
+
if (cur !== expectedMtimeMs) {
|
|
6720
|
+
throw new Error("工程文件在 split 运行期间被外部修改(保存冲突),已拒绝写入;请重新导出视图后重试(客户端侧需先保存、发起后等重载)");
|
|
6721
|
+
}
|
|
6722
|
+
const nextStructMeta = { ...gtrk.struct_meta ?? {}, split: splitObj };
|
|
6723
|
+
const next = { ...gtrk, struct_meta: nextStructMeta };
|
|
6724
|
+
const tmp = join17(dirname4(path), `.${basename7(path)}.${randomBytes2(6).toString("hex")}.tmp`);
|
|
6725
|
+
try {
|
|
6726
|
+
writeFileSync2(tmp, JSON.stringify(next, null, 2));
|
|
6727
|
+
renameSync(tmp, path);
|
|
6728
|
+
} catch (e) {
|
|
6729
|
+
try {
|
|
6730
|
+
unlinkSync(tmp);
|
|
6731
|
+
} catch {}
|
|
6732
|
+
throw e;
|
|
6733
|
+
}
|
|
6734
|
+
}
|
|
6735
|
+
function writeGtrkAtomic(path, next, expectedMtimeMs) {
|
|
6736
|
+
const cur = statSync(path).mtimeMs;
|
|
6737
|
+
if (cur !== expectedMtimeMs) {
|
|
6738
|
+
throw new Error("工程文件在 matrix 运行期间被外部修改(保存冲突),已拒绝写入;请关闭客户端未保存的工程或重跑(plan 与已下载代理均保留)");
|
|
6739
|
+
}
|
|
6740
|
+
const tmp = join17(dirname4(path), `.${basename7(path)}.${randomBytes2(6).toString("hex")}.tmp`);
|
|
6741
|
+
try {
|
|
6742
|
+
writeFileSync2(tmp, JSON.stringify(next, null, 2));
|
|
6743
|
+
renameSync(tmp, path);
|
|
6744
|
+
} catch (e) {
|
|
6745
|
+
try {
|
|
6746
|
+
unlinkSync(tmp);
|
|
6747
|
+
} catch {}
|
|
6748
|
+
throw e;
|
|
6749
|
+
}
|
|
6750
|
+
}
|
|
6751
|
+
|
|
6752
|
+
// src/commands/split.ts
|
|
6753
|
+
var TRANSCRIPT_MISSING = "工程目录内找不到 transcript.json(可能是旧任务产物):请用新版本重跑 gtrk oralcut(恒出 transcript)," + "或(规划中)用 transcribe 生成后再拆分;本命令不做降级猜测。";
|
|
6754
|
+
function registerSplit(program2) {
|
|
6755
|
+
program2.command("split [splitdoc]").description("视觉拆分派单器:无 positional=导出投影视图;带拆分稿=校验落地(写回 struct_meta.split + dispatch)").option("--project <dir>", "oralcut 产物目录(自动定位 gtrk/project.gtrk 与 transcript/transcript.json)").option("--gtrk <path>", "显式指定 .gtrk 工程文件(非标准布局兜底)").option("--transcript <path>", "显式指定 transcript.json(非标准布局兜底)").option("--column <id>", "栏目配置 id(~/.gitruck/columns/<id>.json;缺省取 config defaultColumn,再缺省内置默认栏目)").option("--md", "落地时额外渲染人读稿 split/visual-split.md").option("--words", "视图模式附字级明细(缺省只出句级)").option("--json", "机读模式:人读日志转 stderr,stdout 只输出结果 JSON").action(async (splitdoc, opts) => {
|
|
6756
|
+
await runSplit(splitdoc, opts);
|
|
6757
|
+
});
|
|
6758
|
+
}
|
|
6759
|
+
function firstExisting(cands) {
|
|
6760
|
+
return cands.find((p) => existsSync14(p));
|
|
6761
|
+
}
|
|
6762
|
+
function resolvePaths(opts) {
|
|
6763
|
+
const project = opts.project ? resolve6(opts.project) : undefined;
|
|
6764
|
+
let gtrkPath;
|
|
6765
|
+
if (opts.gtrk) {
|
|
6766
|
+
gtrkPath = resolve6(opts.gtrk);
|
|
6767
|
+
} else if (project) {
|
|
6768
|
+
gtrkPath = firstExisting([join18(project, "gtrk", "project.gtrk"), join18(project, "project.gtrk")]) ?? join18(project, "gtrk", "project.gtrk");
|
|
6769
|
+
} else {
|
|
6770
|
+
throw new Error("需 --project <目录> 或显式 --gtrk <path>");
|
|
6771
|
+
}
|
|
6772
|
+
if (!existsSync14(gtrkPath))
|
|
6773
|
+
throw new Error(`找不到工程文件:${gtrkPath}`);
|
|
6774
|
+
let transcriptPath;
|
|
6775
|
+
if (opts.transcript)
|
|
6776
|
+
transcriptPath = resolve6(opts.transcript);
|
|
6777
|
+
else if (project)
|
|
6778
|
+
transcriptPath = firstExisting([
|
|
6779
|
+
join18(project, "transcript", "transcript.json"),
|
|
6780
|
+
join18(project, "json", "transcript.json"),
|
|
6781
|
+
join18(project, "transcript.json")
|
|
6782
|
+
]);
|
|
6783
|
+
const baseDir = project ?? dirname5(gtrkPath);
|
|
6784
|
+
return { baseDir, gtrkPath, transcriptPath };
|
|
6785
|
+
}
|
|
6786
|
+
function slugify(name) {
|
|
6787
|
+
const s = name.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "-").replace(/^-+|-+$/g, "");
|
|
6788
|
+
return s || "project";
|
|
6789
|
+
}
|
|
6790
|
+
async function loadTranscript(path) {
|
|
6791
|
+
const t = JSON.parse(await readFile4(path, "utf8"));
|
|
6792
|
+
if (!t || !Array.isArray(t.utterances) || typeof t.material_id !== "string" || typeof t.text_hash !== "string") {
|
|
6793
|
+
throw new Error(`transcript.json 结构异常(缺 utterances/material_id/text_hash):${path}`);
|
|
6794
|
+
}
|
|
6795
|
+
t.text_hash = createHash("sha256").update(t.utterances.map((u) => u.text ?? "").join(`
|
|
6796
|
+
`), "utf8").digest("hex");
|
|
6797
|
+
return t;
|
|
6798
|
+
}
|
|
6799
|
+
async function runSplit(splitdoc, opts) {
|
|
6800
|
+
if (opts.json)
|
|
6801
|
+
routeLogsToStderr();
|
|
6802
|
+
const { baseDir, gtrkPath, transcriptPath } = resolvePaths(opts);
|
|
6803
|
+
return splitdoc ? runLand(resolve6(splitdoc), baseDir, gtrkPath, transcriptPath, opts) : runView(baseDir, gtrkPath, transcriptPath, opts);
|
|
6804
|
+
}
|
|
6805
|
+
async function runView(baseDir, gtrkPath, transcriptPath, opts) {
|
|
6806
|
+
if (!transcriptPath || !existsSync14(transcriptPath))
|
|
6807
|
+
throw new Error(TRANSCRIPT_MISSING);
|
|
6808
|
+
log.step("▶ 导出投影视图(transcript × 当刻 .gtrk)…");
|
|
6809
|
+
const transcript = await loadTranscript(transcriptPath);
|
|
6810
|
+
const { gtrk } = readGtrk(gtrkPath);
|
|
6811
|
+
const view = projectTranscript(transcript, gtrk, { words: opts.words });
|
|
6812
|
+
const splitDir = join18(baseDir, "split");
|
|
6813
|
+
await mkdir5(splitDir, { recursive: true });
|
|
6814
|
+
const viewPath = join18(splitDir, "view.json");
|
|
6815
|
+
await writeFile6(viewPath, JSON.stringify(view, null, 2));
|
|
6816
|
+
const dropped = view.utterances.filter((u) => u.dropped).length;
|
|
6817
|
+
log.ok(`投影视图已生成:${viewPath}(${view.utterances.length} 条,其中 ${dropped} 条被剪)`);
|
|
6818
|
+
const result = {
|
|
6819
|
+
ok: true,
|
|
6820
|
+
mode: "view",
|
|
6821
|
+
viewPath,
|
|
6822
|
+
transcript_hash: view.transcript_hash,
|
|
6823
|
+
projected_at: view.projected_at,
|
|
6824
|
+
counts: { entries: view.utterances.length, dropped },
|
|
6825
|
+
view
|
|
6826
|
+
};
|
|
6827
|
+
if (opts.json)
|
|
6828
|
+
console.log(JSON.stringify(result));
|
|
6829
|
+
return result;
|
|
6830
|
+
}
|
|
6831
|
+
async function runLand(splitdocPath, baseDir, gtrkPath, transcriptPath, opts) {
|
|
6832
|
+
if (!existsSync14(splitdocPath))
|
|
6833
|
+
throw new Error(`找不到拆分稿:${splitdocPath}`);
|
|
6834
|
+
if (!transcriptPath || !existsSync14(transcriptPath))
|
|
6835
|
+
throw new Error(TRANSCRIPT_MISSING);
|
|
6836
|
+
log.step("▶ 校验拆分稿并落地…");
|
|
6837
|
+
const doc = JSON.parse(await readFile4(splitdocPath, "utf8"));
|
|
6838
|
+
const transcript = await loadTranscript(transcriptPath);
|
|
6839
|
+
const { gtrk, mtimeMs } = readGtrk(gtrkPath);
|
|
6840
|
+
assertGtrkV1(gtrk);
|
|
6841
|
+
const columnId = opts.column ?? readUserConfig().defaultColumn;
|
|
6842
|
+
const resolved = resolveColumnConfig({ columnId });
|
|
6843
|
+
for (const w of resolved.warnings)
|
|
6844
|
+
log.warn(w);
|
|
6845
|
+
const ctx = {
|
|
6846
|
+
utteranceIds: transcript.utterances.map((u) => u.id),
|
|
6847
|
+
transcriptHash: transcript.text_hash,
|
|
6848
|
+
vocab: effectiveVocab(resolved.config)
|
|
6849
|
+
};
|
|
6850
|
+
const { errors, warnings } = validateSplitDoc(doc, ctx);
|
|
6851
|
+
for (const w of warnings)
|
|
6852
|
+
log.warn(w);
|
|
6853
|
+
if (errors.length) {
|
|
6854
|
+
throw new Error(`拆分稿校验失败(${errors.length} 条,未写入任何产物):
|
|
6855
|
+
` + errors.map((e) => ` - ${e}`).join(`
|
|
6856
|
+
`));
|
|
6857
|
+
}
|
|
6858
|
+
const projectedAt = new Date().toISOString();
|
|
6859
|
+
const view = projectTranscript(transcript, gtrk, { projectedAt });
|
|
6860
|
+
const projectSlug = slugify(basename8(baseDir));
|
|
6861
|
+
const landing = buildLanding(doc, view, {
|
|
6862
|
+
utteranceIds: ctx.utteranceIds,
|
|
6863
|
+
projectSlug,
|
|
6864
|
+
projectedAt,
|
|
6865
|
+
sourceIndex: {
|
|
6866
|
+
materialId: String(transcript.material_id),
|
|
6867
|
+
utterances: new Map(transcript.utterances.map((u) => [u.id, { st: u.st, ed: u.ed }]))
|
|
6868
|
+
}
|
|
6869
|
+
});
|
|
6870
|
+
writeStructMetaSplit(gtrkPath, gtrk, landing.split, mtimeMs);
|
|
6871
|
+
const splitDir = join18(baseDir, "split");
|
|
6872
|
+
await mkdir5(splitDir, { recursive: true });
|
|
6873
|
+
const dispatchPath = join18(splitDir, "dispatch.json");
|
|
6874
|
+
await writeFile6(dispatchPath, JSON.stringify(landing.dispatch, null, 2));
|
|
6875
|
+
let mdPath = null;
|
|
6876
|
+
if (opts.md) {
|
|
6877
|
+
mdPath = join18(splitDir, "visual-split.md");
|
|
6878
|
+
await writeFile6(mdPath, renderSplitMarkdown(doc, landing, { projectSlug, projectedAt }));
|
|
6879
|
+
}
|
|
6880
|
+
log.ok(`落地完成:${landing.split.beats.length}/${doc.beats.length} beat 落轨` + `(RRV_MG ${landing.dispatch.rrv_mg.length} · FILM_BROLL ${landing.dispatch.film_broll.length} · AI_DRAMA ${landing.dispatch.ai_drama.length})`);
|
|
6881
|
+
for (const s of landing.skipped)
|
|
6882
|
+
log.warn(`跳过 ${s.beat}:${s.reason}`);
|
|
6883
|
+
for (const s of landing.shrunk)
|
|
6884
|
+
log.warn(`收缩 ${s.beat}:${s.dropped} 句被剪,按存活 ${s.kept} 句包络 → ${s.track_st}s…${s.track_ed}s(建议人工复核)`);
|
|
6885
|
+
if (landing.unhandledLanes.length > 0) {
|
|
6886
|
+
log.warn(`未派单 lane:${landing.unhandledLanes.join("、")}——通过校验却无 dispatch 分支,落地静默丢队列(新增 lane 时请同步 buildLanding 分派)`);
|
|
6887
|
+
}
|
|
6888
|
+
const result = {
|
|
6889
|
+
ok: true,
|
|
6890
|
+
mode: "land",
|
|
6891
|
+
gtrk: gtrkPath,
|
|
6892
|
+
dispatchPath,
|
|
6893
|
+
mdPath,
|
|
6894
|
+
transcript_hash: doc.transcript_hash,
|
|
6895
|
+
projected_at: projectedAt,
|
|
6896
|
+
beats: { total: doc.beats.length, landed: landing.split.beats.length, skipped: landing.skipped, shrunk: landing.shrunk },
|
|
6897
|
+
queues: {
|
|
6898
|
+
rrv_mg: landing.dispatch.rrv_mg.length,
|
|
6899
|
+
film_broll: landing.dispatch.film_broll.length,
|
|
6900
|
+
ai_drama: landing.dispatch.ai_drama.length
|
|
6901
|
+
}
|
|
6902
|
+
};
|
|
6903
|
+
if (opts.json)
|
|
6904
|
+
console.log(JSON.stringify(result));
|
|
6905
|
+
return result;
|
|
6906
|
+
}
|
|
6907
|
+
|
|
6908
|
+
// src/commands/matrix.ts
|
|
6909
|
+
import { resolve as resolve7, join as join19, dirname as dirname6, basename as basename9 } from "node:path";
|
|
6910
|
+
import { existsSync as existsSync15 } from "node:fs";
|
|
6911
|
+
import { readFile as readFile5, writeFile as writeFile7, mkdir as mkdir6 } from "node:fs/promises";
|
|
6912
|
+
|
|
6913
|
+
// src/lib/matrix-lay.ts
|
|
6914
|
+
var BROLL_PREVIEW_DIR = "assets/broll-preview";
|
|
6915
|
+
var BROLL_MATERIAL_PREFIX = "broll-";
|
|
6916
|
+
var BROLL_META_CANDIDATE_CAP = 12;
|
|
6917
|
+
var SHOT_TARGET_DEFAULT = 3;
|
|
6918
|
+
var MIN_SHOT_SEC = 1.2;
|
|
6919
|
+
var SCORE_FLOOR_DEFAULT = 0.2;
|
|
6920
|
+
var MAX_SLOTS_PER_BEAT = 32;
|
|
6921
|
+
function mergedCandidates(beat) {
|
|
6922
|
+
const all = [];
|
|
6923
|
+
for (const q of beat.queries)
|
|
6924
|
+
for (const r of q.results ?? [])
|
|
6925
|
+
all.push(r);
|
|
6926
|
+
return all.sort((a, b) => b.score - a.score);
|
|
6927
|
+
}
|
|
6928
|
+
function previewUrlFor(result) {
|
|
6929
|
+
const direct = result.preview_url;
|
|
6930
|
+
if (typeof direct === "string" && direct)
|
|
6931
|
+
return direct;
|
|
6932
|
+
const cover = result.cover_url;
|
|
6933
|
+
if (typeof cover === "string") {
|
|
6934
|
+
const derived = cover.replace(/\/keyframe\/([^/]+)\/cover\.jpg.*$/, "/preview/$1.mp4");
|
|
6935
|
+
if (derived !== cover)
|
|
6936
|
+
return derived;
|
|
6937
|
+
}
|
|
6938
|
+
return null;
|
|
6939
|
+
}
|
|
6940
|
+
function previewDims(width, height) {
|
|
6941
|
+
if (!width || !height || width <= 0 || height <= 0)
|
|
6942
|
+
return;
|
|
6943
|
+
if (width <= 640)
|
|
6944
|
+
return [width, height];
|
|
6945
|
+
const h = Math.max(2, Math.round(height * 640 / width / 2) * 2);
|
|
6946
|
+
return [640, h];
|
|
6947
|
+
}
|
|
6948
|
+
var r33 = (n) => Math.round(n * 1000) / 1000;
|
|
6949
|
+
function buildQueryPools(beat, scoreFloor) {
|
|
6950
|
+
const out = [];
|
|
6951
|
+
for (const q of beat.queries) {
|
|
6952
|
+
const pool = [];
|
|
6953
|
+
for (const cand of q.results ?? []) {
|
|
6954
|
+
if (cand.excluded_hint)
|
|
6955
|
+
continue;
|
|
6956
|
+
const segs = cand.segments?.length ? cand.segments : [{ start: 0, end: cand.duration ?? SHOT_TARGET_DEFAULT, best: (cand.duration ?? SHOT_TARGET_DEFAULT) / 2, score: cand.score }];
|
|
6957
|
+
for (const seg of segs) {
|
|
6958
|
+
if (seg.score < scoreFloor)
|
|
6959
|
+
continue;
|
|
6960
|
+
pool.push({ cand, seg, query: q.query, key: `${cand.clip_id}@${seg.start}` });
|
|
6961
|
+
}
|
|
6962
|
+
}
|
|
6963
|
+
pool.sort((a, b) => b.seg.score - a.seg.score);
|
|
6964
|
+
if (pool.length)
|
|
6965
|
+
out.push({ query: q.query, pool });
|
|
6966
|
+
}
|
|
6967
|
+
return out;
|
|
6968
|
+
}
|
|
6969
|
+
function pairAvail(p) {
|
|
6970
|
+
const dur = typeof p.cand.duration === "number" && p.cand.duration > 0 ? p.cand.duration : undefined;
|
|
6971
|
+
return dur ?? Math.max(0, p.seg.end - p.seg.start);
|
|
6972
|
+
}
|
|
6973
|
+
function hashStr(s) {
|
|
6974
|
+
let h = 2166136261;
|
|
6975
|
+
for (let i = 0;i < s.length; i++) {
|
|
6976
|
+
h ^= s.charCodeAt(i);
|
|
6977
|
+
h = Math.imul(h, 16777619);
|
|
6978
|
+
}
|
|
6979
|
+
return h >>> 0;
|
|
6980
|
+
}
|
|
6981
|
+
function mulberry32(seed) {
|
|
6982
|
+
let a = seed >>> 0;
|
|
6983
|
+
return () => {
|
|
6984
|
+
a = a + 1831565813 >>> 0;
|
|
6985
|
+
let t = a;
|
|
6986
|
+
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
6987
|
+
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
6988
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
6989
|
+
};
|
|
6990
|
+
}
|
|
6991
|
+
function shotRange(beat, span) {
|
|
6992
|
+
const shots = typeof beat.requested_shots === "number" && beat.requested_shots > 0 ? beat.requested_shots : undefined;
|
|
6993
|
+
const anchor = typeof beat.per_shot_sec === "number" && beat.per_shot_sec > 0 ? beat.per_shot_sec : shots ? Math.min(Math.max(span / shots, 1.5), 6) : undefined;
|
|
6994
|
+
if (anchor === undefined)
|
|
6995
|
+
return [2, 4];
|
|
6996
|
+
const lo = Math.max(1.5, anchor * 0.8);
|
|
6997
|
+
const hi = Math.max(lo + 0.5, Math.min(8, anchor * 1.6));
|
|
6998
|
+
return [lo, hi];
|
|
6999
|
+
}
|
|
7000
|
+
function fillBeatTrack(opts) {
|
|
7001
|
+
const { beat, trackOrder, consumed, scoreFloor } = opts;
|
|
7002
|
+
const span = beat.track_ed - beat.track_st;
|
|
7003
|
+
if (!(span > 0))
|
|
7004
|
+
return [];
|
|
7005
|
+
const [shotMin, shotMax] = shotRange(beat, span);
|
|
7006
|
+
const rand = mulberry32(hashStr(`${beat.beat}#${trackOrder}`));
|
|
7007
|
+
const pools = buildQueryPools(beat, scoreFloor);
|
|
7008
|
+
if (!pools.length)
|
|
7009
|
+
return [];
|
|
7010
|
+
const slots = [];
|
|
7011
|
+
let cursor = beat.track_st;
|
|
7012
|
+
let prevClip = null;
|
|
7013
|
+
let lastPick = null;
|
|
7014
|
+
let gapRun = 0;
|
|
7015
|
+
for (let slotIdx = 0;slotIdx < MAX_SLOTS_PER_BEAT; slotIdx++) {
|
|
7016
|
+
const remaining = beat.track_ed - cursor;
|
|
7017
|
+
if (remaining < MIN_SHOT_SEC)
|
|
7018
|
+
break;
|
|
7019
|
+
let dTarget;
|
|
7020
|
+
if (remaining <= shotMax)
|
|
7021
|
+
dTarget = remaining;
|
|
7022
|
+
else if (remaining < shotMax + shotMin)
|
|
7023
|
+
dTarget = remaining / 2;
|
|
7024
|
+
else
|
|
7025
|
+
dTarget = shotMin + rand() * (shotMax - shotMin);
|
|
7026
|
+
const minLen = Math.min(MIN_SHOT_SEC, remaining);
|
|
7027
|
+
let pick = null;
|
|
7028
|
+
for (let t = 0;t < pools.length && !pick; t++) {
|
|
7029
|
+
const { pool } = pools[(slotIdx + t) % pools.length];
|
|
7030
|
+
pick = pool.find((p) => !consumed.has(p.key) && p.cand.clip_id !== prevClip && pairAvail(p) >= minLen) ?? null;
|
|
7031
|
+
}
|
|
7032
|
+
if (!pick) {
|
|
7033
|
+
gapRun++;
|
|
7034
|
+
if (gapRun >= 2)
|
|
7035
|
+
break;
|
|
7036
|
+
cursor += Math.min(dTarget, remaining);
|
|
7037
|
+
prevClip = null;
|
|
7038
|
+
continue;
|
|
7039
|
+
}
|
|
7040
|
+
gapRun = 0;
|
|
7041
|
+
const d = Math.min(dTarget, pairAvail(pick), remaining);
|
|
7042
|
+
const dur = typeof pick.cand.duration === "number" && pick.cand.duration > 0 ? pick.cand.duration : undefined;
|
|
7043
|
+
const lo = dur !== undefined ? 0 : pick.seg.start;
|
|
7044
|
+
const hi = dur ?? pick.seg.end;
|
|
7045
|
+
const maxSt = Math.max(lo, hi - d);
|
|
7046
|
+
const clipSt = Math.min(Math.max(pick.seg.best - d / 2, lo), maxSt);
|
|
7047
|
+
slots.push({
|
|
7048
|
+
clip_id: pick.cand.clip_id,
|
|
7049
|
+
query: pick.query,
|
|
7050
|
+
score: pick.seg.score,
|
|
7051
|
+
clip_st: r33(clipSt),
|
|
7052
|
+
clip_ed: r33(clipSt + d),
|
|
7053
|
+
track_st: r33(cursor),
|
|
7054
|
+
track_ed: r33(cursor + d)
|
|
7055
|
+
});
|
|
7056
|
+
consumed.add(pick.key);
|
|
7057
|
+
prevClip = pick.cand.clip_id;
|
|
7058
|
+
lastPick = pick;
|
|
7059
|
+
cursor += d;
|
|
7060
|
+
}
|
|
7061
|
+
const last = slots[slots.length - 1];
|
|
7062
|
+
if (last && lastPick) {
|
|
7063
|
+
const tail = beat.track_ed - last.track_ed;
|
|
7064
|
+
if (tail > 0.000001 && tail < MIN_SHOT_SEC) {
|
|
7065
|
+
const dur = typeof lastPick.cand.duration === "number" && lastPick.cand.duration > 0 ? lastPick.cand.duration : undefined;
|
|
7066
|
+
const hi = dur ?? lastPick.seg.end;
|
|
7067
|
+
const ext = Math.min(tail, Math.max(0, hi - last.clip_ed));
|
|
7068
|
+
if (ext > 0.000001) {
|
|
7069
|
+
last.clip_ed = r33(last.clip_ed + ext);
|
|
7070
|
+
last.track_ed = r33(last.track_ed + ext);
|
|
7071
|
+
}
|
|
7072
|
+
}
|
|
7073
|
+
}
|
|
7074
|
+
return slots;
|
|
7075
|
+
}
|
|
7076
|
+
function planBeatFills(plan, lay, scoreFloor) {
|
|
7077
|
+
const fills = new Map;
|
|
7078
|
+
const clipIds = new Set;
|
|
7079
|
+
const consumed = new Set;
|
|
7080
|
+
for (const beat of plan.beats) {
|
|
7081
|
+
const perTrack = [];
|
|
7082
|
+
for (let k = 0;k < Math.max(0, lay); k++) {
|
|
7083
|
+
const slots = fillBeatTrack({ beat, trackOrder: k, consumed, scoreFloor });
|
|
7084
|
+
perTrack.push(slots);
|
|
7085
|
+
for (const s of slots)
|
|
7086
|
+
clipIds.add(s.clip_id);
|
|
7087
|
+
}
|
|
7088
|
+
fills.set(beat.beat, perTrack);
|
|
7089
|
+
}
|
|
7090
|
+
return { fills, clipIds };
|
|
7091
|
+
}
|
|
7092
|
+
function layBrollTracks(opts) {
|
|
7093
|
+
const { gtrk, plan, lay, fills, downloads } = opts;
|
|
7094
|
+
const videoTracks = [...gtrk.video_track ?? []];
|
|
7095
|
+
const materials = [...gtrk.materials ?? []];
|
|
7096
|
+
const structMeta = { ...gtrk.struct_meta ?? {} };
|
|
7097
|
+
const prevBroll = structMeta.broll;
|
|
7098
|
+
const prevIndices = new Set(Array.isArray(prevBroll?.lay_tracks) ? prevBroll.lay_tracks.filter((x) => typeof x === "number") : []);
|
|
7099
|
+
const removedTracks = videoTracks.filter((t) => typeof t.track_index === "number" && prevIndices.has(t.track_index));
|
|
7100
|
+
const keptTracks = videoTracks.filter((t) => !(typeof t.track_index === "number" && prevIndices.has(t.track_index)));
|
|
7101
|
+
const removedMaterialIds = new Set;
|
|
7102
|
+
for (const t of removedTracks) {
|
|
7103
|
+
for (const c3 of t.track_timeline ?? []) {
|
|
7104
|
+
const m = c3.material;
|
|
7105
|
+
if (typeof m === "string" && m.startsWith(BROLL_MATERIAL_PREFIX))
|
|
7106
|
+
removedMaterialIds.add(m);
|
|
7107
|
+
}
|
|
7108
|
+
}
|
|
7109
|
+
const keptMaterials = materials.filter((m) => !(typeof m.id === "string" && removedMaterialIds.has(m.id)));
|
|
7110
|
+
const canvas = Array.isArray(gtrk.video_size) ? gtrk.video_size : [1920, 1080];
|
|
7111
|
+
const baseIndex = keptTracks.reduce((mx, t) => Math.max(mx, typeof t.track_index === "number" ? t.track_index : 0), -1) + 1;
|
|
7112
|
+
const candById = new Map;
|
|
7113
|
+
for (const beat of plan.beats)
|
|
7114
|
+
for (const c3 of mergedCandidates(beat))
|
|
7115
|
+
if (!candById.has(c3.clip_id))
|
|
7116
|
+
candById.set(c3.clip_id, c3);
|
|
7117
|
+
const metaBeats = [];
|
|
7118
|
+
const newMaterialsById = new Map;
|
|
7119
|
+
const trackClips = new Map;
|
|
7120
|
+
let laidClips = 0;
|
|
7121
|
+
let beatsWithCandidates = 0;
|
|
7122
|
+
for (const beat of plan.beats) {
|
|
7123
|
+
const merged = mergedCandidates(beat);
|
|
7124
|
+
if (merged.length > 0)
|
|
7125
|
+
beatsWithCandidates++;
|
|
7126
|
+
const perTrack = fills.get(beat.beat) ?? [];
|
|
7127
|
+
const laid = [];
|
|
7128
|
+
for (let k = 0;k < perTrack.length; k++) {
|
|
7129
|
+
const slots = perTrack[k].filter((s) => downloads.has(s.clip_id));
|
|
7130
|
+
if (!slots.length)
|
|
7131
|
+
continue;
|
|
7132
|
+
const trackIndex = baseIndex + k;
|
|
7133
|
+
const bucket = trackClips.get(trackIndex) ?? [];
|
|
7134
|
+
slots.forEach((s, i) => {
|
|
7135
|
+
const materialId = `${BROLL_MATERIAL_PREFIX}${s.clip_id}`;
|
|
7136
|
+
if (!newMaterialsById.has(materialId)) {
|
|
7137
|
+
const cand = candById.get(s.clip_id);
|
|
7138
|
+
const dims = previewDims(cand?.width, cand?.height);
|
|
7139
|
+
const mat = { id: materialId, path: downloads.get(s.clip_id).rel };
|
|
7140
|
+
if (typeof cand?.duration === "number")
|
|
7141
|
+
mat.duration = cand.duration;
|
|
7142
|
+
if (dims)
|
|
7143
|
+
mat.video_size = dims;
|
|
7144
|
+
if (typeof cand?.fps === "number")
|
|
7145
|
+
mat.video_rate = cand.fps;
|
|
7146
|
+
newMaterialsById.set(materialId, mat);
|
|
7147
|
+
}
|
|
7148
|
+
bucket.push({
|
|
7149
|
+
clip_id: `${beat.beat}-broll-${k}-${i}`,
|
|
7150
|
+
material: materialId,
|
|
7151
|
+
clip_st: s.clip_st,
|
|
7152
|
+
clip_ed: s.clip_ed,
|
|
7153
|
+
track_st: s.track_st,
|
|
7154
|
+
track_ed: s.track_ed,
|
|
7155
|
+
duration: r33(s.track_ed - s.track_st)
|
|
7156
|
+
});
|
|
7157
|
+
laidClips++;
|
|
7158
|
+
});
|
|
7159
|
+
trackClips.set(trackIndex, bucket);
|
|
7160
|
+
laid.push({ order: k, clip_id: slots[0].clip_id, track_index: trackIndex, slots });
|
|
7161
|
+
}
|
|
7162
|
+
const metaBeat = {
|
|
7163
|
+
beat: beat.beat,
|
|
7164
|
+
track_st: beat.track_st,
|
|
7165
|
+
track_ed: beat.track_ed,
|
|
7166
|
+
candidates: merged.slice(0, BROLL_META_CANDIDATE_CAP).map((c3) => {
|
|
7167
|
+
const dl = downloads.get(c3.clip_id);
|
|
7168
|
+
const seg = c3.segments?.[0];
|
|
7169
|
+
return {
|
|
7170
|
+
clip_id: c3.clip_id,
|
|
7171
|
+
score: c3.score,
|
|
7172
|
+
cover_url: c3.cover_url,
|
|
7173
|
+
preview_path: dl?.rel ?? null,
|
|
7174
|
+
source: dl?.source ?? null,
|
|
7175
|
+
raw_url: c3.url,
|
|
7176
|
+
seg: seg ? { start: seg.start, end: seg.end, best: seg.best } : null
|
|
7177
|
+
};
|
|
7178
|
+
}),
|
|
7179
|
+
laid,
|
|
7180
|
+
pinned: null
|
|
7181
|
+
};
|
|
7182
|
+
if (typeof beat.per_shot_sec === "number")
|
|
7183
|
+
metaBeat.per_shot_sec = beat.per_shot_sec;
|
|
7184
|
+
metaBeats.push(metaBeat);
|
|
7185
|
+
}
|
|
7186
|
+
const createdTracks = [...trackClips.entries()].filter(([, clips]) => clips.length > 0).sort((a, b) => a[0] - b[0]).map(([track_index, clips]) => ({
|
|
7187
|
+
track_index,
|
|
7188
|
+
track_size: [canvas[0], canvas[1]],
|
|
7189
|
+
muted: false,
|
|
7190
|
+
track_timeline: clips.sort((a, b) => a.track_st - b.track_st)
|
|
7191
|
+
}));
|
|
7192
|
+
const broll = {
|
|
7193
|
+
contract_version: "v1",
|
|
7194
|
+
generated_at: opts.generatedAt,
|
|
7195
|
+
plan_path: opts.planPath,
|
|
7196
|
+
lay_tracks: createdTracks.map((t) => t.track_index),
|
|
7197
|
+
confirmed: false,
|
|
7198
|
+
beats: metaBeats
|
|
7199
|
+
};
|
|
7200
|
+
const next = {
|
|
7201
|
+
...gtrk,
|
|
7202
|
+
materials: [...keptMaterials, ...newMaterialsById.values()],
|
|
7203
|
+
video_track: [...keptTracks, ...createdTracks],
|
|
7204
|
+
struct_meta: { ...structMeta, broll }
|
|
7205
|
+
};
|
|
7206
|
+
return {
|
|
7207
|
+
next,
|
|
7208
|
+
summary: { laidTracks: broll.lay_tracks, laidClips, beatsWithCandidates },
|
|
7209
|
+
broll
|
|
7210
|
+
};
|
|
7211
|
+
}
|
|
7212
|
+
|
|
7213
|
+
// src/lib/matrix.ts
|
|
7214
|
+
var URL_TTL_NOTE = "结果 url 带签名默认 24h 过期;过期后重跑 gtrk matrix 即重签(plan 幂等重生成)。";
|
|
7215
|
+
var ENDPOINTS = {
|
|
7216
|
+
internal: "/task/custom/search",
|
|
7217
|
+
external: "/task/video_clip_search"
|
|
7218
|
+
};
|
|
7219
|
+
function decideRoute(memberType) {
|
|
7220
|
+
const tier = memberType === "internal" ? "internal" : "external";
|
|
7221
|
+
return { tier, endpoint: ENDPOINTS[tier] };
|
|
7222
|
+
}
|
|
7223
|
+
var WIDE_RECALL_FACTOR = 3;
|
|
7224
|
+
var TOP_K_MIN = 10;
|
|
7225
|
+
var TOP_K_MAX = 50;
|
|
7226
|
+
var TOP_K_DEFAULT = 10;
|
|
7227
|
+
function asPositiveInt(v) {
|
|
7228
|
+
return typeof v === "number" && Number.isInteger(v) && v > 0 ? v : undefined;
|
|
7229
|
+
}
|
|
7230
|
+
function asPositiveNum(v) {
|
|
7231
|
+
return typeof v === "number" && Number.isFinite(v) && v > 0 ? v : undefined;
|
|
7232
|
+
}
|
|
7233
|
+
function shotsToTopK(shots, override) {
|
|
7234
|
+
if (override && override > 0)
|
|
7235
|
+
return Math.min(Math.max(Math.floor(override), 1), TOP_K_MAX);
|
|
7236
|
+
const n = asPositiveInt(shots);
|
|
7237
|
+
if (!n)
|
|
7238
|
+
return TOP_K_DEFAULT;
|
|
7239
|
+
return Math.min(Math.max(n * WIDE_RECALL_FACTOR, TOP_K_MIN), TOP_K_MAX);
|
|
7240
|
+
}
|
|
7241
|
+
function trimFacets(defaults, allowed) {
|
|
7242
|
+
if (!defaults || typeof defaults !== "object")
|
|
7243
|
+
return;
|
|
7244
|
+
const entries = Object.entries(defaults).filter(([k]) => !allowed || allowed.includes(k));
|
|
7245
|
+
return entries.length ? Object.fromEntries(entries) : undefined;
|
|
7246
|
+
}
|
|
7247
|
+
var MATERIAL_CLASSES = ["real_shot", "concept"];
|
|
7248
|
+
function buildSearchBody(tier, query, dispatch, broll, overrides = {}) {
|
|
7249
|
+
const body = { query, top_k: shotsToTopK(dispatch?.shots, overrides.topK) };
|
|
7250
|
+
const perShot = asPositiveNum(dispatch?.per_shot_sec);
|
|
7251
|
+
if (perShot)
|
|
7252
|
+
body.filters = { min_duration: perShot };
|
|
7253
|
+
if (tier === "internal") {
|
|
7254
|
+
if (broll?.column_tag_ids?.length)
|
|
7255
|
+
body.column_tag_ids = [...broll.column_tag_ids];
|
|
7256
|
+
const mc = overrides.materialClass ?? broll?.material_class_policy;
|
|
7257
|
+
if (mc && MATERIAL_CLASSES.includes(mc))
|
|
7258
|
+
body.material_class = mc;
|
|
7259
|
+
const facets = trimFacets(broll?.facet_defaults, broll?.facet_allowed);
|
|
7260
|
+
if (facets)
|
|
7261
|
+
body.facets = facets;
|
|
7262
|
+
}
|
|
7263
|
+
return body;
|
|
7264
|
+
}
|
|
7265
|
+
function strArr2(v) {
|
|
7266
|
+
if (!Array.isArray(v))
|
|
7267
|
+
return;
|
|
7268
|
+
const out = v.filter((x) => typeof x === "string");
|
|
7269
|
+
return out.length ? out : undefined;
|
|
7270
|
+
}
|
|
7271
|
+
function markExcluded(results, exclude) {
|
|
7272
|
+
if (!exclude?.length)
|
|
7273
|
+
return;
|
|
7274
|
+
for (const r of results) {
|
|
7275
|
+
if (typeof r.note === "string" && r.note && exclude.some((w) => r.note.includes(w))) {
|
|
7276
|
+
r.excluded_hint = true;
|
|
7277
|
+
}
|
|
7278
|
+
}
|
|
7279
|
+
}
|
|
7280
|
+
function dedupeBeatQueries(queries) {
|
|
7281
|
+
const bestByClip = new Map;
|
|
7282
|
+
for (let qi = 0;qi < queries.length; qi++) {
|
|
7283
|
+
for (const r of queries[qi].results ?? []) {
|
|
7284
|
+
const prev = bestByClip.get(r.clip_id);
|
|
7285
|
+
if (!prev || r.score > prev.r.score)
|
|
7286
|
+
bestByClip.set(r.clip_id, { qi, r });
|
|
7287
|
+
}
|
|
7288
|
+
}
|
|
7289
|
+
for (let qi = 0;qi < queries.length; qi++) {
|
|
7290
|
+
const q = queries[qi];
|
|
7291
|
+
if (!q.results)
|
|
7292
|
+
continue;
|
|
7293
|
+
q.results = q.results.filter((r) => {
|
|
7294
|
+
const best = bestByClip.get(r.clip_id);
|
|
7295
|
+
if (best.qi === qi && best.r === r)
|
|
7296
|
+
return true;
|
|
7297
|
+
const list = best.r.also_matched_queries ??= [];
|
|
7298
|
+
if (!list.includes(q.query))
|
|
7299
|
+
list.push(q.query);
|
|
7300
|
+
return false;
|
|
7301
|
+
});
|
|
7302
|
+
}
|
|
7303
|
+
}
|
|
7304
|
+
function buildPlanBeat(entry, outcomes) {
|
|
7305
|
+
const beat = {
|
|
7306
|
+
beat: entry.beat,
|
|
7307
|
+
track_st: entry.track_st,
|
|
7308
|
+
track_ed: entry.track_ed,
|
|
7309
|
+
queries: []
|
|
7310
|
+
};
|
|
7311
|
+
const shots = asPositiveInt(entry.shots);
|
|
7312
|
+
if (shots)
|
|
7313
|
+
beat.requested_shots = shots;
|
|
7314
|
+
const perShot = asPositiveNum(entry.per_shot_sec);
|
|
7315
|
+
if (perShot)
|
|
7316
|
+
beat.per_shot_sec = perShot;
|
|
7317
|
+
const exclude = strArr2(entry.exclude);
|
|
7318
|
+
if (exclude)
|
|
7319
|
+
beat.exclude = exclude;
|
|
7320
|
+
for (const o of outcomes) {
|
|
7321
|
+
if (o.error) {
|
|
7322
|
+
beat.queries.push({ query: o.query, error: o.error });
|
|
7323
|
+
continue;
|
|
7324
|
+
}
|
|
7325
|
+
const results = (o.data?.results ?? []).map((r) => ({ ...r }));
|
|
7326
|
+
markExcluded(results, exclude);
|
|
7327
|
+
const pq = { query: o.query, results };
|
|
7328
|
+
if (typeof o.data?.recalled === "number")
|
|
7329
|
+
pq.recalled = o.data.recalled;
|
|
7330
|
+
beat.queries.push(pq);
|
|
7331
|
+
}
|
|
7332
|
+
dedupeBeatQueries(beat.queries);
|
|
7333
|
+
return beat;
|
|
7334
|
+
}
|
|
7335
|
+
function buildPlan(opts) {
|
|
7336
|
+
const plan = {
|
|
7337
|
+
plan_version: "v1",
|
|
7338
|
+
generated_at: opts.generatedAt,
|
|
7339
|
+
member_type: opts.memberType,
|
|
7340
|
+
url_ttl_note: URL_TTL_NOTE,
|
|
7341
|
+
beats: opts.beats
|
|
7342
|
+
};
|
|
7343
|
+
if (opts.projectSlug)
|
|
7344
|
+
plan.project_slug = opts.projectSlug;
|
|
7345
|
+
if (opts.columnId)
|
|
7346
|
+
plan.column_id = opts.columnId;
|
|
7347
|
+
return plan;
|
|
7348
|
+
}
|
|
7349
|
+
function classifyApiError(code, msg) {
|
|
7350
|
+
switch (code) {
|
|
7351
|
+
case 400:
|
|
7352
|
+
return `请求体非法(网关校验):${msg || "请求参数或请求体格式错误"}`;
|
|
7353
|
+
case 6502:
|
|
7354
|
+
return "鉴权失败——检查 API Key(gtrk init 重配)";
|
|
7355
|
+
case 403:
|
|
7356
|
+
return "非矩阵成员或身份可能已变更——矩阵成员口(custom/search)仅对 internal 档位开放";
|
|
7357
|
+
case 6401:
|
|
7358
|
+
return "检索上游故障,或检索参数/栏目配置非法(如 facets 值拼写)——稍后重试仍失败请检查栏目配置的 broll 块";
|
|
7359
|
+
case 6402:
|
|
7360
|
+
return "检索上游超时——稍后重试";
|
|
7361
|
+
default:
|
|
7362
|
+
return `云端错误 (code=${code ?? "?"}):${msg || "未知错误"}`;
|
|
7363
|
+
}
|
|
7364
|
+
}
|
|
7365
|
+
var SEARCH_TIMEOUT_MS = 25000;
|
|
7366
|
+
async function probeMemberType(cfg) {
|
|
7367
|
+
const res = await fetch(`${cfg.base}/user/get_user_info`, {
|
|
7368
|
+
method: "POST",
|
|
7369
|
+
headers: { accept: "application/json", Authorization: cfg.apiKey },
|
|
7370
|
+
body: "",
|
|
7371
|
+
signal: AbortSignal.timeout(SEARCH_TIMEOUT_MS)
|
|
7372
|
+
});
|
|
7373
|
+
const r = await parseJson(res);
|
|
7374
|
+
if (r.code !== 200)
|
|
7375
|
+
throw new CloudError(r.code, classifyApiError(r.code, r.msg));
|
|
7376
|
+
return decideRoute(r.data?.matrix_member_type).tier;
|
|
7377
|
+
}
|
|
7378
|
+
function parseClipIdSafe(text, status) {
|
|
7379
|
+
try {
|
|
7380
|
+
return JSON.parse(text.replace(/"clip_id"\s*:\s*(\d+)/g, '"clip_id":"$1"'));
|
|
7381
|
+
} catch {
|
|
7382
|
+
throw new Error(`服务响应解析失败 (HTTP ${status})`);
|
|
7383
|
+
}
|
|
7384
|
+
}
|
|
7385
|
+
async function searchOnce(cfg, tier, body) {
|
|
7386
|
+
const res = await fetch(`${cfg.base}${ENDPOINTS[tier]}`, {
|
|
7387
|
+
method: "POST",
|
|
7388
|
+
headers: { accept: "application/json", "Content-Type": "application/json", Authorization: cfg.apiKey },
|
|
7389
|
+
body: JSON.stringify(body),
|
|
7390
|
+
signal: AbortSignal.timeout(SEARCH_TIMEOUT_MS)
|
|
7391
|
+
});
|
|
7392
|
+
const r = parseClipIdSafe(await res.text(), res.status);
|
|
7393
|
+
if (r.code !== 200)
|
|
7394
|
+
throw new CloudError(r.code, classifyApiError(r.code, r.msg));
|
|
7395
|
+
return r.data ?? {};
|
|
7396
|
+
}
|
|
7397
|
+
|
|
7398
|
+
// src/commands/matrix.ts
|
|
7399
|
+
function registerMatrix(program2) {
|
|
7400
|
+
program2.command("matrix [words...]").description('B-roll 检索:无 positional=消费 split/dispatch.json 的 film_broll 队列产候选清单;`matrix search "<query>"`=单条 ad-hoc 检索').option("--project <dir>", "oralcut 产物目录(定位 split/dispatch.json 与产物落点)").option("--dispatch <path>", "显式指定 dispatch.json(非标准布局兜底)").option("--column <id>", "栏目配置 id(缺省取 config defaultColumn,再缺省内置默认栏目)").option("--top-k <n>", "每 query 候选数上限(覆盖派单 shots 翻译;服务端上限 50)").option("--material-class <c>", "素材类型 real_shot|concept(仅矩阵成员口;覆盖栏目 material_class_policy)").option("--lay <n>", "候选铺轨数:下载 preview 代理并在工程里平铺 N 条 B-roll 候选轨(默认 1;0=只出 plan 不铺轨)", "1").option("--score-floor <f>", "填充置信度地板:segment score 低于此值不采纳,槽位留空露主轨(默认 0.2)").option("--out <file>", "ad-hoc 模式:结果落文件(缺省输出 stdout)").option("--json", "机读模式:人读日志转 stderr,stdout 只输出结果 JSON").action(async (words, opts) => {
|
|
7401
|
+
await runMatrix(parseAdhocQuery(words), opts);
|
|
7402
|
+
});
|
|
7403
|
+
}
|
|
7404
|
+
function parseAdhocQuery(words) {
|
|
7405
|
+
if (!words || words.length === 0)
|
|
7406
|
+
return;
|
|
7407
|
+
if (words[0] !== "search") {
|
|
7408
|
+
throw new Error(`未知子命令「${words[0]}」——ad-hoc 检索用法:gtrk matrix search "<query>";派单消费用法:gtrk matrix --project <dir>`);
|
|
7409
|
+
}
|
|
7410
|
+
const query = words.slice(1).join(" ").trim();
|
|
7411
|
+
if (!query)
|
|
7412
|
+
throw new Error('检索词不能为空:gtrk matrix search "<query>"');
|
|
7413
|
+
return query;
|
|
7414
|
+
}
|
|
7415
|
+
async function runMatrix(searchQuery, opts) {
|
|
7416
|
+
if (opts.json)
|
|
7417
|
+
routeLogsToStderr();
|
|
7418
|
+
const cfg = loadConfig();
|
|
7419
|
+
log.step("▶ 身份探针(matrix_member_type)…");
|
|
7420
|
+
const tier = await probeMemberType(cfg);
|
|
7421
|
+
log.info(`档位:${tier}${tier === "internal" ? "(矩阵成员口 /task/custom/search)" : "(通用口 /task/video_clip_search)"}`);
|
|
7422
|
+
const columnId = opts.column ?? readUserConfig().defaultColumn;
|
|
7423
|
+
const resolved = resolveColumnConfig({ columnId });
|
|
7424
|
+
for (const w of resolved.warnings)
|
|
7425
|
+
log.warn(w);
|
|
7426
|
+
const broll = resolved.config.broll;
|
|
7427
|
+
const effectiveColumnId = columnId ?? resolved.config.meta?.id;
|
|
7428
|
+
if (tier === "external") {
|
|
7429
|
+
if (opts.materialClass === "concept") {
|
|
7430
|
+
throw new Error("external 档位服务端固定 real_shot+有版权素材,concept 不可用(--material-class concept 无法满足)");
|
|
7431
|
+
}
|
|
7432
|
+
if (opts.materialClass) {
|
|
7433
|
+
log.warn("external 档位服务端固定 real_shot+有版权素材,--material-class 参数不适用(已忽略)");
|
|
7434
|
+
}
|
|
7435
|
+
if (broll && (broll.column_tag_ids?.length || broll.material_class_policy || broll.facet_defaults)) {
|
|
7436
|
+
log.warn("当前身份为 external,栏目检索偏好(column_tag_ids/material_class/facets)不适用");
|
|
7437
|
+
}
|
|
7438
|
+
}
|
|
7439
|
+
const topK = opts.topK ? Number(opts.topK) : undefined;
|
|
7440
|
+
const overrides = { topK, materialClass: opts.materialClass };
|
|
7441
|
+
const brollForTier = tier === "internal" ? broll : undefined;
|
|
7442
|
+
return searchQuery !== undefined ? runAdhoc(searchQuery, cfg, tier, brollForTier, overrides, effectiveColumnId, opts) : runPlanMode(cfg, tier, brollForTier, overrides, effectiveColumnId, opts);
|
|
7443
|
+
}
|
|
7444
|
+
async function runPlanMode(cfg, tier, broll, overrides, columnId, opts) {
|
|
7445
|
+
let dispatchPath;
|
|
7446
|
+
let baseDir;
|
|
7447
|
+
if (opts.dispatch) {
|
|
7448
|
+
dispatchPath = resolve7(opts.dispatch);
|
|
7449
|
+
baseDir = dirname6(dirname6(dispatchPath));
|
|
7450
|
+
} else if (opts.project) {
|
|
7451
|
+
baseDir = resolve7(opts.project);
|
|
7452
|
+
dispatchPath = join19(baseDir, "split", "dispatch.json");
|
|
7453
|
+
} else {
|
|
7454
|
+
throw new Error('需 --project <目录> 或显式 --dispatch <path>(ad-hoc 检索用:gtrk matrix search "<query>")');
|
|
7455
|
+
}
|
|
7456
|
+
if (!existsSync15(dispatchPath))
|
|
7457
|
+
throw new Error(`找不到派单清单:${dispatchPath}(先跑 gtrk split <拆分稿> 落地派单)`);
|
|
7458
|
+
const dispatch = JSON.parse(await readFile5(dispatchPath, "utf8"));
|
|
7459
|
+
const queue = Array.isArray(dispatch.film_broll) ? dispatch.film_broll : [];
|
|
7460
|
+
log.step(`▶ B-roll 检索:${queue.length} 个 beat(${tier} 口)…`);
|
|
7461
|
+
const beats = [];
|
|
7462
|
+
let okCount = 0;
|
|
7463
|
+
let errCount = 0;
|
|
7464
|
+
let resultCount = 0;
|
|
7465
|
+
for (const entry of queue) {
|
|
7466
|
+
const outcomes = [];
|
|
7467
|
+
for (const q of entry.queries) {
|
|
7468
|
+
try {
|
|
7469
|
+
const body = buildSearchBody(tier, q, entry, broll, overrides);
|
|
7470
|
+
const data = await searchOnce(cfg, tier, body);
|
|
7471
|
+
outcomes.push({ query: q, data });
|
|
7472
|
+
okCount++;
|
|
7473
|
+
resultCount += data.results?.length ?? 0;
|
|
7474
|
+
log.info(`${entry.beat}「${q}」→ ${data.results?.length ?? 0} 条候选(召回 ${data.recalled ?? "?"})`);
|
|
7475
|
+
} catch (e) {
|
|
7476
|
+
const code = e.code;
|
|
7477
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
7478
|
+
outcomes.push({ query: q, error: { ...code != null ? { code } : {}, msg } });
|
|
7479
|
+
errCount++;
|
|
7480
|
+
log.warn(`${entry.beat}「${q}」失败:${msg}`);
|
|
7481
|
+
}
|
|
7482
|
+
}
|
|
7483
|
+
beats.push(buildPlanBeat(entry, outcomes));
|
|
7484
|
+
}
|
|
7485
|
+
const totalQueries = okCount + errCount;
|
|
7486
|
+
if (queue.length === 0)
|
|
7487
|
+
log.warn("无 B-roll 派单(film_broll 队列为空)——照常写出空 plan");
|
|
7488
|
+
if (totalQueries > 0 && okCount === 0) {
|
|
7489
|
+
throw new Error(`全部 ${totalQueries} 个 query 检索失败,未写入 plan(逐条原因见上方日志)`);
|
|
7490
|
+
}
|
|
7491
|
+
const projectSlug = slugify2(basename9(baseDir));
|
|
7492
|
+
const plan = buildPlan({
|
|
7493
|
+
generatedAt: new Date().toISOString(),
|
|
7494
|
+
memberType: tier,
|
|
7495
|
+
projectSlug,
|
|
7496
|
+
columnId,
|
|
7497
|
+
beats
|
|
7498
|
+
});
|
|
7499
|
+
const splitDir = join19(baseDir, "split");
|
|
7500
|
+
await mkdir6(splitDir, { recursive: true });
|
|
7501
|
+
const planPath = join19(splitDir, "broll-plan.json");
|
|
7502
|
+
await writeFile7(planPath, JSON.stringify(plan, null, 2));
|
|
7503
|
+
log.ok(`候选清单已生成:${planPath}(${beats.length} beat · ${okCount}/${totalQueries} query 成功 · ${resultCount} 条候选)`);
|
|
7504
|
+
log.info("清单只含引用不含素材:cover_url 可直接预览;url 带签名默认 24h 过期,过期重跑本命令即重签。");
|
|
7505
|
+
const layN = parseLay(opts.lay);
|
|
7506
|
+
let laySummary;
|
|
7507
|
+
if (layN > 0) {
|
|
7508
|
+
laySummary = await layIntoProject(baseDir, plan, layN, parseScoreFloor(opts.scoreFloor));
|
|
7509
|
+
}
|
|
7510
|
+
const result = {
|
|
7511
|
+
ok: true,
|
|
7512
|
+
mode: "plan",
|
|
7513
|
+
memberType: tier,
|
|
7514
|
+
...columnId ? { columnId } : {},
|
|
7515
|
+
planPath,
|
|
7516
|
+
...laySummary ? { lay: laySummary } : {},
|
|
7517
|
+
counts: { beats: beats.length, queries: totalQueries, results: resultCount, errors: errCount }
|
|
7518
|
+
};
|
|
7519
|
+
if (opts.json)
|
|
7520
|
+
console.log(JSON.stringify(result));
|
|
7521
|
+
return result;
|
|
7522
|
+
}
|
|
7523
|
+
function parseLay(raw) {
|
|
7524
|
+
if (raw === undefined)
|
|
7525
|
+
return 1;
|
|
7526
|
+
const n = Number(raw);
|
|
7527
|
+
if (Number.isInteger(n) && n >= 0)
|
|
7528
|
+
return n;
|
|
7529
|
+
log.warn(`--lay 取值非法(${raw}),按默认 1 处理`);
|
|
7530
|
+
return 1;
|
|
7531
|
+
}
|
|
7532
|
+
function parseScoreFloor(raw) {
|
|
7533
|
+
if (raw === undefined)
|
|
7534
|
+
return SCORE_FLOOR_DEFAULT;
|
|
7535
|
+
const n = Number(raw);
|
|
7536
|
+
if (Number.isFinite(n) && n >= 0 && n <= 1)
|
|
7537
|
+
return n;
|
|
7538
|
+
log.warn(`--score-floor 取值非法(${raw}),按默认 ${SCORE_FLOOR_DEFAULT} 处理`);
|
|
7539
|
+
return SCORE_FLOOR_DEFAULT;
|
|
7540
|
+
}
|
|
7541
|
+
function locateGtrk(baseDir) {
|
|
7542
|
+
const cands = [join19(baseDir, "gtrk", "project.gtrk"), join19(baseDir, "project.gtrk")];
|
|
7543
|
+
return cands.find((p) => existsSync15(p));
|
|
7544
|
+
}
|
|
7545
|
+
async function layIntoProject(baseDir, plan, layN, scoreFloor) {
|
|
7546
|
+
const gtrkPath = locateGtrk(baseDir);
|
|
7547
|
+
if (!gtrkPath) {
|
|
7548
|
+
log.warn(`未找到工程文件(${join19(baseDir, "gtrk", "project.gtrk")}),跳过铺轨——plan 已产出,可后续在有工程的目录重跑`);
|
|
7549
|
+
return;
|
|
7550
|
+
}
|
|
7551
|
+
const { gtrk, mtimeMs } = readGtrk(gtrkPath);
|
|
7552
|
+
assertGtrkV1(gtrk);
|
|
7553
|
+
const { fills, clipIds } = planBeatFills(plan, layN, scoreFloor);
|
|
7554
|
+
const slotCount = [...fills.values()].flat().reduce((n, s) => n + s.length, 0);
|
|
7555
|
+
log.step(`▶ 候选铺轨(${layN} 轨 · 平铺 ${slotCount} 槽位 · ${clipIds.size} 个 clip,preview 代理)…`);
|
|
7556
|
+
const gtrkDir = dirname6(gtrkPath);
|
|
7557
|
+
const previewDir = join19(gtrkDir, ...BROLL_PREVIEW_DIR.split("/"));
|
|
7558
|
+
await mkdir6(previewDir, { recursive: true });
|
|
7559
|
+
const prevSource = new Map;
|
|
7560
|
+
const prevBroll = gtrk.struct_meta?.broll;
|
|
7561
|
+
for (const b of prevBroll?.beats ?? []) {
|
|
7562
|
+
for (const c3 of b.candidates ?? []) {
|
|
7563
|
+
if (typeof c3.clip_id === "string" && c3.preview_path && (c3.source === "preview" || c3.source === "raw")) {
|
|
7564
|
+
prevSource.set(c3.clip_id, c3.source);
|
|
7565
|
+
}
|
|
7566
|
+
}
|
|
7567
|
+
}
|
|
7568
|
+
const candById = new Map;
|
|
7569
|
+
for (const beat of plan.beats)
|
|
7570
|
+
for (const c3 of mergedCandidates(beat))
|
|
7571
|
+
if (!candById.has(c3.clip_id))
|
|
7572
|
+
candById.set(c3.clip_id, c3);
|
|
7573
|
+
const downloads = new Map;
|
|
7574
|
+
const dlStats = { preview: 0, raw: 0, reused: 0, failed: 0 };
|
|
7575
|
+
for (const clipId of clipIds) {
|
|
7576
|
+
const cand = candById.get(clipId);
|
|
7577
|
+
if (!cand)
|
|
7578
|
+
continue;
|
|
7579
|
+
const rel = `${BROLL_PREVIEW_DIR}/${clipId}.mp4`;
|
|
7580
|
+
const abs = join19(gtrkDir, ...rel.split("/"));
|
|
7581
|
+
if (existsSync15(abs)) {
|
|
7582
|
+
const prev = prevSource.get(clipId);
|
|
7583
|
+
if (prev !== "raw") {
|
|
7584
|
+
downloads.set(clipId, { rel, source: prev ?? "preview" });
|
|
7585
|
+
dlStats.reused++;
|
|
7586
|
+
continue;
|
|
7587
|
+
}
|
|
7588
|
+
const retried = await downloadProxy(cand, abs, { previewOnly: true });
|
|
7589
|
+
if (retried === "preview") {
|
|
7590
|
+
downloads.set(clipId, { rel, source: "preview" });
|
|
7591
|
+
dlStats.preview++;
|
|
7592
|
+
log.info(`clip ${clipId} 代理已补产,已从原片回落态换回 preview`);
|
|
7593
|
+
} else {
|
|
7594
|
+
downloads.set(clipId, { rel, source: "raw" });
|
|
7595
|
+
dlStats.reused++;
|
|
7596
|
+
}
|
|
7597
|
+
continue;
|
|
7598
|
+
}
|
|
7599
|
+
const got = await downloadProxy(cand, abs);
|
|
7600
|
+
if (got) {
|
|
7601
|
+
downloads.set(clipId, { rel, source: got });
|
|
7602
|
+
dlStats[got]++;
|
|
7603
|
+
} else {
|
|
7604
|
+
dlStats.failed++;
|
|
7605
|
+
}
|
|
7606
|
+
}
|
|
7607
|
+
const { next, summary } = layBrollTracks({
|
|
7608
|
+
gtrk,
|
|
7609
|
+
plan,
|
|
7610
|
+
lay: layN,
|
|
7611
|
+
fills,
|
|
7612
|
+
downloads,
|
|
7613
|
+
generatedAt: new Date().toISOString(),
|
|
7614
|
+
planPath: "split/broll-plan.json"
|
|
7615
|
+
});
|
|
7616
|
+
writeGtrkAtomic(gtrkPath, next, mtimeMs);
|
|
7617
|
+
log.ok(`铺轨完成:${summary.laidTracks.length} 条候选轨(track_index ${summary.laidTracks.join("/") || "-"})· 平铺 ${summary.laidClips} 个颗粒 / ${clipIds.size} 个 clip` + `(代理 ${dlStats.preview} · 原片回落 ${dlStats.raw} · 复用 ${dlStats.reused}${dlStats.failed ? ` · 失败 ${dlStats.failed}` : ""})`);
|
|
7618
|
+
log.info("opencut 打开工程即见候选轨:轨道头小眼睛可开关对比;确认下载原片属挑选 UI(E-P1)。");
|
|
7619
|
+
if (dlStats.raw > 0) {
|
|
7620
|
+
log.warn("部分候选无 preview 代理已回落原片(体积较大)——服务端 backfill 后重跑本命令可换回代理。");
|
|
7621
|
+
}
|
|
7622
|
+
return { laidTracks: summary.laidTracks, laidClips: summary.laidClips, downloads: dlStats };
|
|
7623
|
+
}
|
|
7624
|
+
async function downloadProxy(cand, absPath, opts = {}) {
|
|
7625
|
+
const tryFetch = async (url) => {
|
|
7626
|
+
try {
|
|
7627
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(180000) });
|
|
7628
|
+
if (!res.ok)
|
|
7629
|
+
return null;
|
|
7630
|
+
return Buffer.from(await res.arrayBuffer());
|
|
7631
|
+
} catch {
|
|
7632
|
+
return null;
|
|
7633
|
+
}
|
|
7634
|
+
};
|
|
7635
|
+
const previewUrl = previewUrlFor(cand);
|
|
7636
|
+
if (previewUrl) {
|
|
7637
|
+
const bytes = await tryFetch(previewUrl);
|
|
7638
|
+
if (bytes) {
|
|
7639
|
+
await writeFile7(absPath, bytes);
|
|
7640
|
+
return "preview";
|
|
7641
|
+
}
|
|
7642
|
+
}
|
|
7643
|
+
if (opts.previewOnly)
|
|
7644
|
+
return null;
|
|
7645
|
+
const raw = await tryFetch(cand.url);
|
|
7646
|
+
if (raw) {
|
|
7647
|
+
await writeFile7(absPath, raw);
|
|
7648
|
+
log.warn(`clip ${cand.clip_id} 无 preview 代理,已回落原片(${(raw.length / 1048576).toFixed(1)}MB)`);
|
|
7649
|
+
return "raw";
|
|
7650
|
+
}
|
|
7651
|
+
log.warn(`clip ${cand.clip_id} 代理与原片均下载失败,该候选槽位跳过`);
|
|
7652
|
+
return null;
|
|
7653
|
+
}
|
|
7654
|
+
async function runAdhoc(query, cfg, tier, broll, overrides, columnId, opts) {
|
|
7655
|
+
log.step(`▶ ad-hoc 检索「${query}」(${tier} 口)…`);
|
|
7656
|
+
const body = buildSearchBody(tier, query, undefined, broll, overrides);
|
|
7657
|
+
const data = await searchOnce(cfg, tier, body);
|
|
7658
|
+
const results = data.results ?? [];
|
|
7659
|
+
log.ok(`${results.length} 条候选(召回 ${data.recalled ?? "?"})`);
|
|
7660
|
+
const result = {
|
|
7661
|
+
ok: true,
|
|
7662
|
+
mode: "search",
|
|
7663
|
+
memberType: tier,
|
|
7664
|
+
...columnId ? { columnId } : {},
|
|
7665
|
+
results,
|
|
7666
|
+
counts: { beats: 0, queries: 1, results: results.length, errors: 0 }
|
|
7667
|
+
};
|
|
7668
|
+
if (opts.out) {
|
|
7669
|
+
const outPath = resolve7(opts.out);
|
|
7670
|
+
await writeFile7(outPath, JSON.stringify({ query, recalled: data.recalled, results }, null, 2));
|
|
7671
|
+
log.ok(`结果已落盘:${outPath}`);
|
|
7672
|
+
result.outPath = outPath;
|
|
7673
|
+
} else if (!opts.json) {
|
|
7674
|
+
for (const r of results.slice(0, 10)) {
|
|
7675
|
+
const seg = r.segments?.[0];
|
|
7676
|
+
log.info(`clip ${r.clip_id} · score ${r.score}${seg ? ` · 最佳段 ${seg.start}s–${seg.end}s(锚点 ${seg.best}s)` : ""}${r.note ? ` · ${String(r.note).slice(0, 40)}` : ""}`);
|
|
7677
|
+
}
|
|
7678
|
+
}
|
|
7679
|
+
if (opts.json)
|
|
7680
|
+
console.log(JSON.stringify(result));
|
|
7681
|
+
return result;
|
|
7682
|
+
}
|
|
7683
|
+
function slugify2(name) {
|
|
7684
|
+
const s = name.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "-").replace(/^-+|-+$/g, "");
|
|
7685
|
+
return s || "project";
|
|
7686
|
+
}
|
|
7687
|
+
|
|
5990
7688
|
// src/index.ts
|
|
5991
7689
|
try {
|
|
5992
7690
|
process.loadEnvFile?.();
|
|
5993
7691
|
} catch {}
|
|
5994
7692
|
migrateLegacyHome();
|
|
5995
|
-
var { version } = JSON.parse(
|
|
7693
|
+
var { version } = JSON.parse(readFileSync5(join20(packageRoot(), "package.json"), "utf8"));
|
|
5996
7694
|
var program2 = new Command;
|
|
5997
7695
|
program2.name("gtrk").description("同合云成片流水线 CLI —— agent 驱动云端任务、产物拉回本地、三方工程文件(客户端/剪映/PR)互通").version(version);
|
|
5998
7696
|
registerInstall(program2);
|
|
@@ -6003,6 +7701,8 @@ registerDoctor(program2);
|
|
|
6003
7701
|
registerSkills(program2);
|
|
6004
7702
|
registerUpgrade(program2);
|
|
6005
7703
|
registerRender(program2);
|
|
7704
|
+
registerSplit(program2);
|
|
7705
|
+
registerMatrix(program2);
|
|
6006
7706
|
program2.parseAsync(process.argv).catch((e) => {
|
|
6007
7707
|
console.error(`
|
|
6008
7708
|
❌ ${e instanceof Error ? e.message : String(e)}`);
|