@gitruck/cli 0.2.3 → 0.2.4
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 +34 -1
- package/README.md +3 -2
- package/dist/index.js +648 -21
- package/package.json +2 -2
- package/skills/gtrk-splitter/SKILL.md +148 -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 +93 -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 readFileSync4 } from "node:fs";
|
|
4200
|
+
import { join as join17 } 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,28 +4274,33 @@ 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"];
|
|
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,也可直接说「帮我剪个口播 / 拆个分镜」触发(可能需重载会话)。");
|
|
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
|
}
|
|
@@ -5987,12 +5992,633 @@ function registerRender(program2) {
|
|
|
5987
5992
|
});
|
|
5988
5993
|
}
|
|
5989
5994
|
|
|
5995
|
+
// src/commands/split.ts
|
|
5996
|
+
import { resolve as resolve6, join as join16, dirname as dirname5, basename as basename8 } from "node:path";
|
|
5997
|
+
import { existsSync as existsSync13 } from "node:fs";
|
|
5998
|
+
import { readFile as readFile4, writeFile as writeFile6, mkdir as mkdir5 } from "node:fs/promises";
|
|
5999
|
+
import { createHash } from "node:crypto";
|
|
6000
|
+
|
|
6001
|
+
// src/lib/projection.ts
|
|
6002
|
+
function r3(n) {
|
|
6003
|
+
return Math.round(n * 1000) / 1000;
|
|
6004
|
+
}
|
|
6005
|
+
function normClip(c3) {
|
|
6006
|
+
const clip_st = c3.clip_st ?? 0;
|
|
6007
|
+
const track_st = c3.track_st ?? 0;
|
|
6008
|
+
const dur = c3.duration ?? (c3.clip_ed != null ? c3.clip_ed - clip_st : 0);
|
|
6009
|
+
const clip_ed = c3.clip_ed ?? clip_st + dur;
|
|
6010
|
+
return { clip_st, clip_ed, track_st };
|
|
6011
|
+
}
|
|
6012
|
+
function pickMainVideoTrack(gtrk) {
|
|
6013
|
+
const tracks = gtrk.video_track ?? [];
|
|
6014
|
+
if (!tracks.length)
|
|
6015
|
+
return;
|
|
6016
|
+
let best = tracks[0];
|
|
6017
|
+
let bestIdx = best.track_index ?? 0;
|
|
6018
|
+
for (const t of tracks) {
|
|
6019
|
+
const idx = t.track_index ?? 0;
|
|
6020
|
+
if (idx < bestIdx) {
|
|
6021
|
+
best = t;
|
|
6022
|
+
bestIdx = idx;
|
|
6023
|
+
}
|
|
6024
|
+
}
|
|
6025
|
+
return best;
|
|
6026
|
+
}
|
|
6027
|
+
function projectTranscript(transcript, gtrk, opts = {}) {
|
|
6028
|
+
const materialId = String(transcript.material_id);
|
|
6029
|
+
const mainTrack = pickMainVideoTrack(gtrk);
|
|
6030
|
+
const clips = (mainTrack?.track_timeline ?? []).filter((c3) => c3.material != null && String(c3.material) === materialId).map(normClip);
|
|
6031
|
+
const entries = [];
|
|
6032
|
+
transcript.utterances.forEach((utt, sourceIndex) => {
|
|
6033
|
+
const totalWords = utt.words?.length ?? 0;
|
|
6034
|
+
const instances = [];
|
|
6035
|
+
for (const clip of clips) {
|
|
6036
|
+
const surviving = [];
|
|
6037
|
+
for (const word of utt.words ?? []) {
|
|
6038
|
+
const s = Math.max(word.st, clip.clip_st);
|
|
6039
|
+
const e = Math.min(word.ed, clip.clip_ed);
|
|
6040
|
+
if (e > s) {
|
|
6041
|
+
surviving.push({
|
|
6042
|
+
w: word.w,
|
|
6043
|
+
track_st: r3(clip.track_st + (s - clip.clip_st)),
|
|
6044
|
+
track_ed: r3(clip.track_st + (e - clip.clip_st))
|
|
6045
|
+
});
|
|
6046
|
+
}
|
|
6047
|
+
}
|
|
6048
|
+
if (surviving.length) {
|
|
6049
|
+
instances.push({
|
|
6050
|
+
track_st: Math.min(...surviving.map((x) => x.track_st)),
|
|
6051
|
+
track_ed: Math.max(...surviving.map((x) => x.track_ed)),
|
|
6052
|
+
kept_words: surviving.length,
|
|
6053
|
+
words: surviving
|
|
6054
|
+
});
|
|
6055
|
+
}
|
|
6056
|
+
}
|
|
6057
|
+
if (!instances.length) {
|
|
6058
|
+
entries.push({
|
|
6059
|
+
id: utt.id,
|
|
6060
|
+
text: utt.text,
|
|
6061
|
+
dropped: true,
|
|
6062
|
+
sourceIndex,
|
|
6063
|
+
instIndex: 0,
|
|
6064
|
+
track_st: null,
|
|
6065
|
+
track_ed: null,
|
|
6066
|
+
kept_words: 0,
|
|
6067
|
+
total_words: totalWords,
|
|
6068
|
+
words: [],
|
|
6069
|
+
sortKey: 0
|
|
6070
|
+
});
|
|
6071
|
+
} else {
|
|
6072
|
+
instances.sort((a, b) => a.track_st - b.track_st);
|
|
6073
|
+
instances.forEach((inst, instIndex) => {
|
|
6074
|
+
entries.push({
|
|
6075
|
+
id: utt.id,
|
|
6076
|
+
text: utt.text,
|
|
6077
|
+
dropped: false,
|
|
6078
|
+
sourceIndex,
|
|
6079
|
+
instIndex,
|
|
6080
|
+
track_st: inst.track_st,
|
|
6081
|
+
track_ed: inst.track_ed,
|
|
6082
|
+
kept_words: inst.kept_words,
|
|
6083
|
+
total_words: totalWords,
|
|
6084
|
+
words: inst.words,
|
|
6085
|
+
sortKey: inst.track_st
|
|
6086
|
+
});
|
|
6087
|
+
});
|
|
6088
|
+
}
|
|
6089
|
+
});
|
|
6090
|
+
let maxEd = 0;
|
|
6091
|
+
for (const e of entries) {
|
|
6092
|
+
if (e.dropped)
|
|
6093
|
+
e.sortKey = maxEd;
|
|
6094
|
+
else
|
|
6095
|
+
maxEd = Math.max(maxEd, e.track_ed ?? maxEd);
|
|
6096
|
+
}
|
|
6097
|
+
entries.sort((a, b) => a.sortKey - b.sortKey || a.sourceIndex - b.sourceIndex || a.instIndex - b.instIndex);
|
|
6098
|
+
const utterances = entries.map((e) => {
|
|
6099
|
+
const u = {
|
|
6100
|
+
id: e.id,
|
|
6101
|
+
text: e.text,
|
|
6102
|
+
track_st: e.track_st,
|
|
6103
|
+
track_ed: e.track_ed,
|
|
6104
|
+
dropped: e.dropped,
|
|
6105
|
+
kept_words: e.kept_words,
|
|
6106
|
+
total_words: e.total_words
|
|
6107
|
+
};
|
|
6108
|
+
if (opts.words)
|
|
6109
|
+
u.words = e.words;
|
|
6110
|
+
return u;
|
|
6111
|
+
});
|
|
6112
|
+
return {
|
|
6113
|
+
transcript_hash: transcript.text_hash,
|
|
6114
|
+
projected_at: opts.projectedAt ?? new Date().toISOString(),
|
|
6115
|
+
utterances
|
|
6116
|
+
};
|
|
6117
|
+
}
|
|
6118
|
+
|
|
6119
|
+
// src/lib/splitdoc.ts
|
|
6120
|
+
var BASE_TRACKS = ["真人出镜", "口播继续", "旁白主导"];
|
|
6121
|
+
var LANES = ["A_ROLL", "RRV_MG", "AI_DRAMA", "FILM_BROLL"];
|
|
6122
|
+
var NARRATIVES = [
|
|
6123
|
+
"mirror-hook",
|
|
6124
|
+
"demolition",
|
|
6125
|
+
"container-translation",
|
|
6126
|
+
"abyssal-fall",
|
|
6127
|
+
"holding",
|
|
6128
|
+
"reversal-elevation",
|
|
6129
|
+
"callback-closure",
|
|
6130
|
+
"typography-emphasis"
|
|
6131
|
+
];
|
|
6132
|
+
var CONTAINER_STAGES = [
|
|
6133
|
+
"none",
|
|
6134
|
+
"seed",
|
|
6135
|
+
"expand",
|
|
6136
|
+
"translate",
|
|
6137
|
+
"rupture",
|
|
6138
|
+
"flip",
|
|
6139
|
+
"callback"
|
|
6140
|
+
];
|
|
6141
|
+
var IRREPLACEABILITY = ["必须真人出镜", "优先 MG", "可被 B-roll 替代", "可降级处理"];
|
|
6142
|
+
var AUX_TYPES = [
|
|
6143
|
+
"quote-card",
|
|
6144
|
+
"term-callout",
|
|
6145
|
+
"network-diagram",
|
|
6146
|
+
"archive-caption",
|
|
6147
|
+
"pause-card",
|
|
6148
|
+
"data-annotation",
|
|
6149
|
+
"timeline-tag"
|
|
6150
|
+
];
|
|
6151
|
+
function isNonEmptyStr(v) {
|
|
6152
|
+
return typeof v === "string" && v.trim().length > 0;
|
|
6153
|
+
}
|
|
6154
|
+
function enumOk(v, list) {
|
|
6155
|
+
return typeof v === "string" && list.includes(v);
|
|
6156
|
+
}
|
|
6157
|
+
function validateSplitDoc(doc, ctx) {
|
|
6158
|
+
const errors = [];
|
|
6159
|
+
const warnings = [];
|
|
6160
|
+
if (typeof doc !== "object" || doc === null || Array.isArray(doc)) {
|
|
6161
|
+
return { errors: ["拆分稿必须是一个 JSON 对象"], warnings };
|
|
6162
|
+
}
|
|
6163
|
+
const d = doc;
|
|
6164
|
+
if (d.contract_version !== "v1") {
|
|
6165
|
+
errors.push(`contract_version 必须为 "v1"(实际:${JSON.stringify(d.contract_version)})`);
|
|
6166
|
+
}
|
|
6167
|
+
if (!isNonEmptyStr(d.transcript_hash)) {
|
|
6168
|
+
errors.push("缺 transcript_hash(应从投影视图透传)");
|
|
6169
|
+
} else if (d.transcript_hash !== ctx.transcriptHash) {
|
|
6170
|
+
errors.push(`transcript_hash 不匹配:拆分稿 ${d.transcript_hash} ≠ 当前 transcript ${ctx.transcriptHash}——转写已变更,请重新导出视图并重拆`);
|
|
6171
|
+
}
|
|
6172
|
+
if (!Array.isArray(d.beats) || d.beats.length === 0) {
|
|
6173
|
+
errors.push("beats 必须是非空数组");
|
|
6174
|
+
return { errors, warnings };
|
|
6175
|
+
}
|
|
6176
|
+
const idIndex = new Map;
|
|
6177
|
+
ctx.utteranceIds.forEach((id, i) => idIndex.set(id, i));
|
|
6178
|
+
const ranges = [];
|
|
6179
|
+
const seenBeatIds = new Set;
|
|
6180
|
+
d.beats.forEach((raw, i) => {
|
|
6181
|
+
const tag = (() => {
|
|
6182
|
+
const bid = raw?.id;
|
|
6183
|
+
return isNonEmptyStr(bid) ? bid : `beats[${i}]`;
|
|
6184
|
+
})();
|
|
6185
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
6186
|
+
errors.push(`${tag}:beat 必须是对象`);
|
|
6187
|
+
return;
|
|
6188
|
+
}
|
|
6189
|
+
const b = raw;
|
|
6190
|
+
if (!isNonEmptyStr(b.id))
|
|
6191
|
+
errors.push(`${tag}:缺 id`);
|
|
6192
|
+
else if (!/^B\d{2,}$/.test(b.id))
|
|
6193
|
+
errors.push(`${b.id}:id 须为 "B"+两位起序号(如 B01)`);
|
|
6194
|
+
else if (seenBeatIds.has(b.id))
|
|
6195
|
+
errors.push(`${b.id}:beat id 重复`);
|
|
6196
|
+
else
|
|
6197
|
+
seenBeatIds.add(b.id);
|
|
6198
|
+
if (!enumOk(b.base_track, BASE_TRACKS))
|
|
6199
|
+
errors.push(`${tag}:base_track 非法(三选一:${BASE_TRACKS.join(" | ")})`);
|
|
6200
|
+
if (!enumOk(b.lane, LANES))
|
|
6201
|
+
errors.push(`${tag}:lane 非法(四选一:${LANES.join(" | ")})`);
|
|
6202
|
+
if (!enumOk(b.narrative, NARRATIVES))
|
|
6203
|
+
errors.push(`${tag}:narrative 非法(八枚举之一)`);
|
|
6204
|
+
if (!enumOk(b.container_stage, CONTAINER_STAGES))
|
|
6205
|
+
errors.push(`${tag}:container_stage 非法(七枚举之一)`);
|
|
6206
|
+
if (!enumOk(b.irreplaceability, IRREPLACEABILITY))
|
|
6207
|
+
errors.push(`${tag}:irreplaceability 非法(四枚举之一)`);
|
|
6208
|
+
if (!isNonEmptyStr(b.rhythm))
|
|
6209
|
+
errors.push(`${tag}:缺 rhythm(人读节奏标签)`);
|
|
6210
|
+
if (!isNonEmptyStr(b.visual_task))
|
|
6211
|
+
errors.push(`${tag}:缺 visual_task(一句话视觉任务)`);
|
|
6212
|
+
const span = b.span;
|
|
6213
|
+
let fromIdx = -1;
|
|
6214
|
+
let toIdx = -1;
|
|
6215
|
+
if (!span || !isNonEmptyStr(span.from) || !isNonEmptyStr(span.to)) {
|
|
6216
|
+
errors.push(`${tag}:缺 span.from / span.to(utterance id 区间)`);
|
|
6217
|
+
} else {
|
|
6218
|
+
if (!idIndex.has(span.from))
|
|
6219
|
+
errors.push(`${tag}:span.from 引用了不存在的 utterance id ${span.from}`);
|
|
6220
|
+
else
|
|
6221
|
+
fromIdx = idIndex.get(span.from);
|
|
6222
|
+
if (!idIndex.has(span.to))
|
|
6223
|
+
errors.push(`${tag}:span.to 引用了不存在的 utterance id ${span.to}`);
|
|
6224
|
+
else
|
|
6225
|
+
toIdx = idIndex.get(span.to);
|
|
6226
|
+
if (fromIdx >= 0 && toIdx >= 0) {
|
|
6227
|
+
if (fromIdx > toIdx)
|
|
6228
|
+
errors.push(`${tag}:区间倒序(span.from ${span.from} 晚于 span.to ${span.to})`);
|
|
6229
|
+
else if (isNonEmptyStr(b.id))
|
|
6230
|
+
ranges.push({ id: b.id, from: fromIdx, to: toIdx });
|
|
6231
|
+
}
|
|
6232
|
+
}
|
|
6233
|
+
validateHandoff(tag, b, errors, warnings);
|
|
6234
|
+
if (b.aux_layers != null) {
|
|
6235
|
+
if (!Array.isArray(b.aux_layers))
|
|
6236
|
+
errors.push(`${tag}:aux_layers 必须是数组`);
|
|
6237
|
+
else
|
|
6238
|
+
b.aux_layers.forEach((a, ai) => validateAux(`${tag}.aux[${ai}]`, a, idIndex, errors));
|
|
6239
|
+
}
|
|
6240
|
+
});
|
|
6241
|
+
const sorted = [...ranges].sort((a, b) => a.from - b.from || a.to - b.to);
|
|
6242
|
+
for (let i = 1;i < sorted.length; i++) {
|
|
6243
|
+
const prev = sorted[i - 1];
|
|
6244
|
+
const cur = sorted[i];
|
|
6245
|
+
if (cur.from <= prev.to) {
|
|
6246
|
+
errors.push(`${prev.id} 与 ${cur.id}:utterance 区间重叠(beats 之间不允许交集)`);
|
|
6247
|
+
}
|
|
6248
|
+
}
|
|
6249
|
+
return { errors, warnings };
|
|
6250
|
+
}
|
|
6251
|
+
function validateHandoff(tag, b, errors, warnings) {
|
|
6252
|
+
const lane = b.lane;
|
|
6253
|
+
const handoff = b.handoff;
|
|
6254
|
+
if (lane === "A_ROLL") {
|
|
6255
|
+
if (handoff != null)
|
|
6256
|
+
warnings.push(`${tag}:A_ROLL 不应带 handoff,已忽略`);
|
|
6257
|
+
return;
|
|
6258
|
+
}
|
|
6259
|
+
if (lane === "RRV_MG") {
|
|
6260
|
+
if (!handoff || typeof handoff.duration_hint !== "number") {
|
|
6261
|
+
errors.push(`${tag}:RRV_MG 的 handoff.duration_hint 必填(秒,数值)`);
|
|
6262
|
+
}
|
|
6263
|
+
return;
|
|
6264
|
+
}
|
|
6265
|
+
if (lane === "FILM_BROLL") {
|
|
6266
|
+
const q = handoff?.queries;
|
|
6267
|
+
if (!Array.isArray(q) || q.length === 0 || !q.every((x) => isNonEmptyStr(x))) {
|
|
6268
|
+
errors.push(`${tag}:FILM_BROLL 缺检索 query(handoff.queries 必须为非空字符串数组)`);
|
|
6269
|
+
}
|
|
6270
|
+
return;
|
|
6271
|
+
}
|
|
6272
|
+
}
|
|
6273
|
+
function validateAux(tag, raw, idIndex, errors) {
|
|
6274
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
6275
|
+
errors.push(`${tag}:辅助层必须是对象`);
|
|
6276
|
+
return;
|
|
6277
|
+
}
|
|
6278
|
+
const a = raw;
|
|
6279
|
+
if (!enumOk(a.type, AUX_TYPES))
|
|
6280
|
+
errors.push(`${tag}:type 非法(七类之一)`);
|
|
6281
|
+
if (!isNonEmptyStr(a.role))
|
|
6282
|
+
errors.push(`${tag}:缺 role(职责)`);
|
|
6283
|
+
const m = a.mount;
|
|
6284
|
+
if (m === "same_beat")
|
|
6285
|
+
return;
|
|
6286
|
+
if (typeof m === "object" && m !== null) {
|
|
6287
|
+
const mo = m;
|
|
6288
|
+
if (isNonEmptyStr(mo.trigger)) {
|
|
6289
|
+
if (!idIndex.has(mo.trigger))
|
|
6290
|
+
errors.push(`${tag}:mount.trigger 引用了不存在的 utterance id ${mo.trigger}`);
|
|
6291
|
+
return;
|
|
6292
|
+
}
|
|
6293
|
+
if (isNonEmptyStr(mo.from) && isNonEmptyStr(mo.to)) {
|
|
6294
|
+
if (!idIndex.has(mo.from))
|
|
6295
|
+
errors.push(`${tag}:mount.from 引用了不存在的 utterance id ${mo.from}`);
|
|
6296
|
+
if (!idIndex.has(mo.to))
|
|
6297
|
+
errors.push(`${tag}:mount.to 引用了不存在的 utterance id ${mo.to}`);
|
|
6298
|
+
if (idIndex.has(mo.from) && idIndex.has(mo.to) && idIndex.get(mo.from) > idIndex.get(mo.to)) {
|
|
6299
|
+
errors.push(`${tag}:mount 区间倒序`);
|
|
6300
|
+
}
|
|
6301
|
+
return;
|
|
6302
|
+
}
|
|
6303
|
+
}
|
|
6304
|
+
errors.push(`${tag}:mount 非法(应为 "same_beat" | {from,to} | {trigger})`);
|
|
6305
|
+
}
|
|
6306
|
+
function buildLanding(doc, view, opts) {
|
|
6307
|
+
const byId = new Map;
|
|
6308
|
+
for (const id of opts.utteranceIds)
|
|
6309
|
+
byId.set(id, []);
|
|
6310
|
+
for (const u of view.utterances) {
|
|
6311
|
+
if (u.dropped || u.track_st == null || u.track_ed == null)
|
|
6312
|
+
continue;
|
|
6313
|
+
if (!byId.has(u.id))
|
|
6314
|
+
byId.set(u.id, []);
|
|
6315
|
+
byId.get(u.id).push({ track_st: u.track_st, track_ed: u.track_ed });
|
|
6316
|
+
}
|
|
6317
|
+
const idIndex = new Map;
|
|
6318
|
+
opts.utteranceIds.forEach((id, i) => idIndex.set(id, i));
|
|
6319
|
+
const split = {
|
|
6320
|
+
contract_version: doc.contract_version,
|
|
6321
|
+
transcript_hash: doc.transcript_hash,
|
|
6322
|
+
projected_at: opts.projectedAt,
|
|
6323
|
+
beats: []
|
|
6324
|
+
};
|
|
6325
|
+
const dispatch = { rrv_mg: [], film_broll: [], ai_drama: [] };
|
|
6326
|
+
const skipped = [];
|
|
6327
|
+
const shrunk = [];
|
|
6328
|
+
for (const beat of doc.beats) {
|
|
6329
|
+
const fromIdx = idIndex.get(beat.span.from);
|
|
6330
|
+
const toIdx = idIndex.get(beat.span.to);
|
|
6331
|
+
const spanIds = opts.utteranceIds.slice(fromIdx, toIdx + 1);
|
|
6332
|
+
const instances = spanIds.flatMap((id) => byId.get(id) ?? []);
|
|
6333
|
+
const droppedCount = spanIds.filter((id) => (byId.get(id)?.length ?? 0) === 0).length;
|
|
6334
|
+
if (instances.length === 0) {
|
|
6335
|
+
skipped.push({ beat: beat.id, reason: "span 内全部 utterance 被剪,未落轨" });
|
|
6336
|
+
continue;
|
|
6337
|
+
}
|
|
6338
|
+
const track_st = Math.min(...instances.map((x) => x.track_st));
|
|
6339
|
+
const track_ed = Math.max(...instances.map((x) => x.track_ed));
|
|
6340
|
+
const isShrunk = droppedCount > 0;
|
|
6341
|
+
const metaBeat = { id: beat.id, lane: beat.lane, span: beat.span, track_st, track_ed };
|
|
6342
|
+
if (isShrunk)
|
|
6343
|
+
metaBeat.shrunk = true;
|
|
6344
|
+
if (beat.lane !== "A_ROLL" && beat.handoff)
|
|
6345
|
+
metaBeat.handoff = beat.handoff;
|
|
6346
|
+
split.beats.push(metaBeat);
|
|
6347
|
+
if (isShrunk) {
|
|
6348
|
+
shrunk.push({ beat: beat.id, kept: spanIds.length - droppedCount, dropped: droppedCount, track_st, track_ed });
|
|
6349
|
+
}
|
|
6350
|
+
const h = beat.handoff ?? {};
|
|
6351
|
+
const compositionId = `${opts.projectSlug}-${beat.id}`;
|
|
6352
|
+
if (beat.lane === "RRV_MG") {
|
|
6353
|
+
dispatch.rrv_mg.push({
|
|
6354
|
+
beat: beat.id,
|
|
6355
|
+
composition_id: compositionId,
|
|
6356
|
+
duration: typeof h.duration_hint === "number" ? h.duration_hint : null,
|
|
6357
|
+
theme: h.theme,
|
|
6358
|
+
bg: h.bg,
|
|
6359
|
+
slug_hint: h.slug_hint,
|
|
6360
|
+
track_st,
|
|
6361
|
+
track_ed
|
|
6362
|
+
});
|
|
6363
|
+
} else if (beat.lane === "FILM_BROLL") {
|
|
6364
|
+
dispatch.film_broll.push({
|
|
6365
|
+
beat: beat.id,
|
|
6366
|
+
queries: Array.isArray(h.queries) ? h.queries : [],
|
|
6367
|
+
shots: h.shots,
|
|
6368
|
+
per_shot_sec: h.per_shot_sec,
|
|
6369
|
+
exclude: h.exclude,
|
|
6370
|
+
track_st,
|
|
6371
|
+
track_ed
|
|
6372
|
+
});
|
|
6373
|
+
} else if (beat.lane === "AI_DRAMA") {
|
|
6374
|
+
dispatch.ai_drama.push({ beat: beat.id, ...h, track_st, track_ed });
|
|
6375
|
+
}
|
|
6376
|
+
}
|
|
6377
|
+
return { split, dispatch, skipped, shrunk };
|
|
6378
|
+
}
|
|
6379
|
+
function renderSplitMarkdown(doc, landing, meta) {
|
|
6380
|
+
const L = [];
|
|
6381
|
+
const metaById = new Map(landing.split.beats.map((b) => [b.id, b]));
|
|
6382
|
+
const skippedIds = new Set(landing.skipped.map((s) => s.beat));
|
|
6383
|
+
L.push(`# 视觉拆分稿(${meta.projectSlug})`);
|
|
6384
|
+
L.push("");
|
|
6385
|
+
L.push(`- contract_version:\`${doc.contract_version}\``);
|
|
6386
|
+
L.push(`- transcript_hash:\`${doc.transcript_hash}\``);
|
|
6387
|
+
L.push(`- projected_at:\`${meta.projectedAt}\``);
|
|
6388
|
+
L.push(`- beats:${doc.beats.length}(落轨 ${landing.split.beats.length} · 跳过 ${landing.skipped.length} · 收缩 ${landing.shrunk.length})`);
|
|
6389
|
+
L.push("");
|
|
6390
|
+
L.push("# Beat Timeline");
|
|
6391
|
+
L.push("");
|
|
6392
|
+
for (const beat of doc.beats) {
|
|
6393
|
+
const mb = metaById.get(beat.id);
|
|
6394
|
+
L.push(`## ${beat.id}${skippedIds.has(beat.id) ? "(整段被剪 · 跳过)" : mb?.shrunk ? "(部分被剪 · 已收缩)" : ""}`);
|
|
6395
|
+
L.push(`- 文稿范围:\`${beat.span.from} … ${beat.span.to}\``);
|
|
6396
|
+
L.push(`- 底轨:\`${beat.base_track}\``);
|
|
6397
|
+
L.push(`- 主层:\`${beat.lane}\``);
|
|
6398
|
+
L.push(`- 叙事功能:\`${beat.narrative}\``);
|
|
6399
|
+
L.push(`- 容器阶段:\`${beat.container_stage}\``);
|
|
6400
|
+
if (beat.rhythm)
|
|
6401
|
+
L.push(`- 节奏标签:\`${beat.rhythm}\``);
|
|
6402
|
+
L.push(`- 视觉任务:${beat.visual_task}`);
|
|
6403
|
+
L.push(`- 不可替代性:\`${beat.irreplaceability}\``);
|
|
6404
|
+
if (mb)
|
|
6405
|
+
L.push(`- 轨道时码:\`${mb.track_st}s … ${mb.track_ed}s\``);
|
|
6406
|
+
if (beat.callback_of)
|
|
6407
|
+
L.push(`- 回扣对象:\`${beat.callback_of}\``);
|
|
6408
|
+
for (const a of beat.aux_layers ?? []) {
|
|
6409
|
+
const mount = a.mount === "same_beat" ? "同 beat" : ("trigger" in a.mount) ? `触发 ${a.mount.trigger}` : `${a.mount.from} … ${a.mount.to}`;
|
|
6410
|
+
L.push(` - 辅助层 \`${a.type}\`(${mount}):${a.role}`);
|
|
6411
|
+
}
|
|
6412
|
+
L.push("");
|
|
6413
|
+
}
|
|
6414
|
+
L.push("# Production Queues");
|
|
6415
|
+
L.push("");
|
|
6416
|
+
L.push("## A_ROLL Queue");
|
|
6417
|
+
for (const b of doc.beats.filter((x) => x.lane === "A_ROLL" && !skippedIds.has(x.id))) {
|
|
6418
|
+
L.push(`- \`${b.id}\` ${b.visual_task}`);
|
|
6419
|
+
}
|
|
6420
|
+
L.push("");
|
|
6421
|
+
L.push("## RRV_MG Queue");
|
|
6422
|
+
for (const r of landing.dispatch.rrv_mg) {
|
|
6423
|
+
L.push(`- \`${r.beat}\` composition_id=\`${r.composition_id}\`${r.duration != null ? ` · ${r.duration}s` : ""}`);
|
|
6424
|
+
}
|
|
6425
|
+
L.push("");
|
|
6426
|
+
L.push("## AI_DRAMA Queue");
|
|
6427
|
+
for (const a of landing.dispatch.ai_drama)
|
|
6428
|
+
L.push(`- \`${a.beat}\` ${a.track_st}s…${a.track_ed}s`);
|
|
6429
|
+
L.push("");
|
|
6430
|
+
L.push("## FILM_BROLL Queue");
|
|
6431
|
+
for (const f of landing.dispatch.film_broll)
|
|
6432
|
+
L.push(`- \`${f.beat}\` queries=[${f.queries.join(" / ")}]`);
|
|
6433
|
+
L.push("");
|
|
6434
|
+
return L.join(`
|
|
6435
|
+
`);
|
|
6436
|
+
}
|
|
6437
|
+
|
|
6438
|
+
// src/lib/gtrk-writeback.ts
|
|
6439
|
+
import { readFileSync as readFileSync3, statSync, writeFileSync as writeFileSync2, renameSync, unlinkSync } from "node:fs";
|
|
6440
|
+
import { dirname as dirname4, join as join15, basename as basename7 } from "node:path";
|
|
6441
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
6442
|
+
function readGtrk(path) {
|
|
6443
|
+
const raw = readFileSync3(path, "utf8");
|
|
6444
|
+
let gtrk;
|
|
6445
|
+
try {
|
|
6446
|
+
gtrk = JSON.parse(raw);
|
|
6447
|
+
} catch (e) {
|
|
6448
|
+
throw new Error(`工程文件不是合法 JSON:${path}(${e instanceof Error ? e.message : String(e)})`);
|
|
6449
|
+
}
|
|
6450
|
+
if (typeof gtrk !== "object" || gtrk === null || Array.isArray(gtrk)) {
|
|
6451
|
+
throw new Error(`工程文件结构异常(顶层非对象):${path}`);
|
|
6452
|
+
}
|
|
6453
|
+
return { gtrk, mtimeMs: statSync(path).mtimeMs };
|
|
6454
|
+
}
|
|
6455
|
+
function assertGtrkV1(gtrk) {
|
|
6456
|
+
if (gtrk.version !== "v1") {
|
|
6457
|
+
throw new Error(`工程文件不是 v1(version=${JSON.stringify(gtrk.version)}):请用新链路重产 v1 工程后再拆分`);
|
|
6458
|
+
}
|
|
6459
|
+
}
|
|
6460
|
+
function writeStructMetaSplit(path, gtrk, splitObj, expectedMtimeMs) {
|
|
6461
|
+
const cur = statSync(path).mtimeMs;
|
|
6462
|
+
if (cur !== expectedMtimeMs) {
|
|
6463
|
+
throw new Error("工程文件在 split 运行期间被外部修改(保存冲突),已拒绝写入;请重新导出视图后重试(客户端侧需先保存、发起后等重载)");
|
|
6464
|
+
}
|
|
6465
|
+
const nextStructMeta = { ...gtrk.struct_meta ?? {}, split: splitObj };
|
|
6466
|
+
const next = { ...gtrk, struct_meta: nextStructMeta };
|
|
6467
|
+
const tmp = join15(dirname4(path), `.${basename7(path)}.${randomBytes2(6).toString("hex")}.tmp`);
|
|
6468
|
+
try {
|
|
6469
|
+
writeFileSync2(tmp, JSON.stringify(next, null, 2));
|
|
6470
|
+
renameSync(tmp, path);
|
|
6471
|
+
} catch (e) {
|
|
6472
|
+
try {
|
|
6473
|
+
unlinkSync(tmp);
|
|
6474
|
+
} catch {}
|
|
6475
|
+
throw e;
|
|
6476
|
+
}
|
|
6477
|
+
}
|
|
6478
|
+
|
|
6479
|
+
// src/commands/split.ts
|
|
6480
|
+
var TRANSCRIPT_MISSING = "工程目录内找不到 transcript.json(可能是旧任务产物):请用新版本重跑 gtrk oralcut(恒出 transcript)," + "或(规划中)用 transcribe 生成后再拆分;本命令不做降级猜测。";
|
|
6481
|
+
function registerSplit(program2) {
|
|
6482
|
+
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("--md", "落地时额外渲染人读稿 split/visual-split.md").option("--words", "视图模式附字级明细(缺省只出句级)").option("--json", "机读模式:人读日志转 stderr,stdout 只输出结果 JSON").action(async (splitdoc, opts) => {
|
|
6483
|
+
await runSplit(splitdoc, opts);
|
|
6484
|
+
});
|
|
6485
|
+
}
|
|
6486
|
+
function firstExisting(cands) {
|
|
6487
|
+
return cands.find((p) => existsSync13(p));
|
|
6488
|
+
}
|
|
6489
|
+
function resolvePaths(opts) {
|
|
6490
|
+
const project = opts.project ? resolve6(opts.project) : undefined;
|
|
6491
|
+
let gtrkPath;
|
|
6492
|
+
if (opts.gtrk) {
|
|
6493
|
+
gtrkPath = resolve6(opts.gtrk);
|
|
6494
|
+
} else if (project) {
|
|
6495
|
+
gtrkPath = firstExisting([join16(project, "gtrk", "project.gtrk"), join16(project, "project.gtrk")]) ?? join16(project, "gtrk", "project.gtrk");
|
|
6496
|
+
} else {
|
|
6497
|
+
throw new Error("需 --project <目录> 或显式 --gtrk <path>");
|
|
6498
|
+
}
|
|
6499
|
+
if (!existsSync13(gtrkPath))
|
|
6500
|
+
throw new Error(`找不到工程文件:${gtrkPath}`);
|
|
6501
|
+
let transcriptPath;
|
|
6502
|
+
if (opts.transcript)
|
|
6503
|
+
transcriptPath = resolve6(opts.transcript);
|
|
6504
|
+
else if (project)
|
|
6505
|
+
transcriptPath = firstExisting([
|
|
6506
|
+
join16(project, "transcript", "transcript.json"),
|
|
6507
|
+
join16(project, "json", "transcript.json"),
|
|
6508
|
+
join16(project, "transcript.json")
|
|
6509
|
+
]);
|
|
6510
|
+
const baseDir = project ?? dirname5(gtrkPath);
|
|
6511
|
+
return { baseDir, gtrkPath, transcriptPath };
|
|
6512
|
+
}
|
|
6513
|
+
function slugify(name) {
|
|
6514
|
+
const s = name.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "-").replace(/^-+|-+$/g, "");
|
|
6515
|
+
return s || "project";
|
|
6516
|
+
}
|
|
6517
|
+
async function loadTranscript(path) {
|
|
6518
|
+
const t = JSON.parse(await readFile4(path, "utf8"));
|
|
6519
|
+
if (!t || !Array.isArray(t.utterances) || typeof t.material_id !== "string" || typeof t.text_hash !== "string") {
|
|
6520
|
+
throw new Error(`transcript.json 结构异常(缺 utterances/material_id/text_hash):${path}`);
|
|
6521
|
+
}
|
|
6522
|
+
t.text_hash = createHash("sha256").update(t.utterances.map((u) => u.text ?? "").join(`
|
|
6523
|
+
`), "utf8").digest("hex");
|
|
6524
|
+
return t;
|
|
6525
|
+
}
|
|
6526
|
+
async function runSplit(splitdoc, opts) {
|
|
6527
|
+
if (opts.json)
|
|
6528
|
+
routeLogsToStderr();
|
|
6529
|
+
const { baseDir, gtrkPath, transcriptPath } = resolvePaths(opts);
|
|
6530
|
+
return splitdoc ? runLand(resolve6(splitdoc), baseDir, gtrkPath, transcriptPath, opts) : runView(baseDir, gtrkPath, transcriptPath, opts);
|
|
6531
|
+
}
|
|
6532
|
+
async function runView(baseDir, gtrkPath, transcriptPath, opts) {
|
|
6533
|
+
if (!transcriptPath || !existsSync13(transcriptPath))
|
|
6534
|
+
throw new Error(TRANSCRIPT_MISSING);
|
|
6535
|
+
log.step("▶ 导出投影视图(transcript × 当刻 .gtrk)…");
|
|
6536
|
+
const transcript = await loadTranscript(transcriptPath);
|
|
6537
|
+
const { gtrk } = readGtrk(gtrkPath);
|
|
6538
|
+
const view = projectTranscript(transcript, gtrk, { words: opts.words });
|
|
6539
|
+
const splitDir = join16(baseDir, "split");
|
|
6540
|
+
await mkdir5(splitDir, { recursive: true });
|
|
6541
|
+
const viewPath = join16(splitDir, "view.json");
|
|
6542
|
+
await writeFile6(viewPath, JSON.stringify(view, null, 2));
|
|
6543
|
+
const dropped = view.utterances.filter((u) => u.dropped).length;
|
|
6544
|
+
log.ok(`投影视图已生成:${viewPath}(${view.utterances.length} 条,其中 ${dropped} 条被剪)`);
|
|
6545
|
+
const result = {
|
|
6546
|
+
ok: true,
|
|
6547
|
+
mode: "view",
|
|
6548
|
+
viewPath,
|
|
6549
|
+
transcript_hash: view.transcript_hash,
|
|
6550
|
+
projected_at: view.projected_at,
|
|
6551
|
+
counts: { entries: view.utterances.length, dropped },
|
|
6552
|
+
view
|
|
6553
|
+
};
|
|
6554
|
+
if (opts.json)
|
|
6555
|
+
console.log(JSON.stringify(result));
|
|
6556
|
+
return result;
|
|
6557
|
+
}
|
|
6558
|
+
async function runLand(splitdocPath, baseDir, gtrkPath, transcriptPath, opts) {
|
|
6559
|
+
if (!existsSync13(splitdocPath))
|
|
6560
|
+
throw new Error(`找不到拆分稿:${splitdocPath}`);
|
|
6561
|
+
if (!transcriptPath || !existsSync13(transcriptPath))
|
|
6562
|
+
throw new Error(TRANSCRIPT_MISSING);
|
|
6563
|
+
log.step("▶ 校验拆分稿并落地…");
|
|
6564
|
+
const doc = JSON.parse(await readFile4(splitdocPath, "utf8"));
|
|
6565
|
+
const transcript = await loadTranscript(transcriptPath);
|
|
6566
|
+
const { gtrk, mtimeMs } = readGtrk(gtrkPath);
|
|
6567
|
+
assertGtrkV1(gtrk);
|
|
6568
|
+
const ctx = { utteranceIds: transcript.utterances.map((u) => u.id), transcriptHash: transcript.text_hash };
|
|
6569
|
+
const { errors, warnings } = validateSplitDoc(doc, ctx);
|
|
6570
|
+
for (const w of warnings)
|
|
6571
|
+
log.warn(w);
|
|
6572
|
+
if (errors.length) {
|
|
6573
|
+
throw new Error(`拆分稿校验失败(${errors.length} 条,未写入任何产物):
|
|
6574
|
+
` + errors.map((e) => ` - ${e}`).join(`
|
|
6575
|
+
`));
|
|
6576
|
+
}
|
|
6577
|
+
const projectedAt = new Date().toISOString();
|
|
6578
|
+
const view = projectTranscript(transcript, gtrk, { projectedAt });
|
|
6579
|
+
const projectSlug = slugify(basename8(baseDir));
|
|
6580
|
+
const landing = buildLanding(doc, view, { utteranceIds: ctx.utteranceIds, projectSlug, projectedAt });
|
|
6581
|
+
writeStructMetaSplit(gtrkPath, gtrk, landing.split, mtimeMs);
|
|
6582
|
+
const splitDir = join16(baseDir, "split");
|
|
6583
|
+
await mkdir5(splitDir, { recursive: true });
|
|
6584
|
+
const dispatchPath = join16(splitDir, "dispatch.json");
|
|
6585
|
+
await writeFile6(dispatchPath, JSON.stringify(landing.dispatch, null, 2));
|
|
6586
|
+
let mdPath = null;
|
|
6587
|
+
if (opts.md) {
|
|
6588
|
+
mdPath = join16(splitDir, "visual-split.md");
|
|
6589
|
+
await writeFile6(mdPath, renderSplitMarkdown(doc, landing, { projectSlug, projectedAt }));
|
|
6590
|
+
}
|
|
6591
|
+
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})`);
|
|
6592
|
+
for (const s of landing.skipped)
|
|
6593
|
+
log.warn(`跳过 ${s.beat}:${s.reason}`);
|
|
6594
|
+
for (const s of landing.shrunk)
|
|
6595
|
+
log.warn(`收缩 ${s.beat}:${s.dropped} 句被剪,按存活 ${s.kept} 句包络 → ${s.track_st}s…${s.track_ed}s(建议人工复核)`);
|
|
6596
|
+
const result = {
|
|
6597
|
+
ok: true,
|
|
6598
|
+
mode: "land",
|
|
6599
|
+
gtrk: gtrkPath,
|
|
6600
|
+
dispatchPath,
|
|
6601
|
+
mdPath,
|
|
6602
|
+
transcript_hash: doc.transcript_hash,
|
|
6603
|
+
projected_at: projectedAt,
|
|
6604
|
+
beats: { total: doc.beats.length, landed: landing.split.beats.length, skipped: landing.skipped, shrunk: landing.shrunk },
|
|
6605
|
+
queues: {
|
|
6606
|
+
rrv_mg: landing.dispatch.rrv_mg.length,
|
|
6607
|
+
film_broll: landing.dispatch.film_broll.length,
|
|
6608
|
+
ai_drama: landing.dispatch.ai_drama.length
|
|
6609
|
+
}
|
|
6610
|
+
};
|
|
6611
|
+
if (opts.json)
|
|
6612
|
+
console.log(JSON.stringify(result));
|
|
6613
|
+
return result;
|
|
6614
|
+
}
|
|
6615
|
+
|
|
5990
6616
|
// src/index.ts
|
|
5991
6617
|
try {
|
|
5992
6618
|
process.loadEnvFile?.();
|
|
5993
6619
|
} catch {}
|
|
5994
6620
|
migrateLegacyHome();
|
|
5995
|
-
var { version } = JSON.parse(
|
|
6621
|
+
var { version } = JSON.parse(readFileSync4(join17(packageRoot(), "package.json"), "utf8"));
|
|
5996
6622
|
var program2 = new Command;
|
|
5997
6623
|
program2.name("gtrk").description("同合云成片流水线 CLI —— agent 驱动云端任务、产物拉回本地、三方工程文件(客户端/剪映/PR)互通").version(version);
|
|
5998
6624
|
registerInstall(program2);
|
|
@@ -6003,6 +6629,7 @@ registerDoctor(program2);
|
|
|
6003
6629
|
registerSkills(program2);
|
|
6004
6630
|
registerUpgrade(program2);
|
|
6005
6631
|
registerRender(program2);
|
|
6632
|
+
registerSplit(program2);
|
|
6006
6633
|
program2.parseAsync(process.argv).catch((e) => {
|
|
6007
6634
|
console.error(`
|
|
6008
6635
|
❌ ${e instanceof Error ? e.message : String(e)}`);
|