@gitruck/cli 0.2.12 → 0.2.14
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/README.md +3 -0
- package/dist/index.js +302 -28
- package/package.json +2 -2
- package/skills/gtrk-matrix/SKILL.md +7 -3
- package/skills/gtrk-mg/SKILL.md +1 -0
- package/skills/gtrk-splitter/SKILL.md +2 -0
package/README.md
CHANGED
|
@@ -339,10 +339,13 @@ gtrk transcript "D:/素材/采访视频.mp4" --lang zh-CN --out "D:/文字稿/
|
|
|
339
339
|
| `--top-k <n>` | 每 query 候选数上限(覆盖派单 shots;服务端上限 50) | 派单值 |
|
|
340
340
|
| `--material-class <c>` | 素材类型 `real_shot` \| `concept`(仅矩阵成员口;覆盖栏目策略) | 栏目策略 |
|
|
341
341
|
| `--score-floor <f>` | 填充置信度地板:segment score 低于此值不采纳、留空露主轨 | `0.2` |
|
|
342
|
+
| `--no-black-bed` | 不铺纯黑底垫轨(默认铺一条) | 默认铺 |
|
|
342
343
|
| `--out <file>` | ad-hoc 模式结果落文件 | stdout |
|
|
343
344
|
| `--json` | 机读:stdout 只输出结果 JSON | 关 |
|
|
344
345
|
|
|
345
346
|
> preview 代理 url 约 24h 过期,重跑即重签。
|
|
347
|
+
>
|
|
348
|
+
> **纯黑底垫轨**:默认在全部候选轨之下、口播主轨之上垫一条纯黑底轨(`struct_meta.broll.black_track` 记其 `track_index`),按已落成的 beat 包络整条铺满,使 B-roll 期间(含候选轨留空处)不漏出底下的口播画面。字节落 `assets/builtin/solid-000000-<W>x<H>.png`,与客户端内置纯色素材同 id 命名空间、幂等复用。删候选轨时别误删它;换片请拖到候选轨颗粒上、**别拖到黑底条上**——含拖拽保护的客户端会直接拒绝并提示,尚未升级到该版本的客户端会被误拖打出黑底破洞(该处漏口播)。不想要加 `--no-black-bed` 重跑即剥净。
|
|
346
349
|
|
|
347
350
|
### `gtrk mg` — MG 动态图颗粒(铺轨 / lint / status)
|
|
348
351
|
|
package/dist/index.js
CHANGED
|
@@ -5001,7 +5001,8 @@ function buildLanding(doc, view, opts) {
|
|
|
5001
5001
|
dispatch.mg.push({
|
|
5002
5002
|
beat: beat.id,
|
|
5003
5003
|
composition_id: compositionId,
|
|
5004
|
-
duration:
|
|
5004
|
+
duration: r3(track_ed - track_st),
|
|
5005
|
+
duration_hint: typeof h.duration_hint === "number" ? h.duration_hint : null,
|
|
5005
5006
|
...h.category !== undefined ? { category: h.category } : {},
|
|
5006
5007
|
theme: h.theme,
|
|
5007
5008
|
bg: h.bg,
|
|
@@ -5058,7 +5059,8 @@ function buildLanding(doc, view, opts) {
|
|
|
5058
5059
|
dispatch.mg.push({
|
|
5059
5060
|
beat: beat.id,
|
|
5060
5061
|
composition_id: auxCompositionId,
|
|
5061
|
-
duration:
|
|
5062
|
+
duration: r3(auxTrackEd - auxTrackSt),
|
|
5063
|
+
duration_hint: ah && typeof ah.duration_hint === "number" ? ah.duration_hint : null,
|
|
5062
5064
|
category: "overlay",
|
|
5063
5065
|
theme: ah?.theme,
|
|
5064
5066
|
bg: ah?.bg,
|
|
@@ -5130,7 +5132,9 @@ function renderSplitMarkdown(doc, landing, meta) {
|
|
|
5130
5132
|
L.push("");
|
|
5131
5133
|
L.push("## MG Queue");
|
|
5132
5134
|
for (const r of landing.dispatch.mg) {
|
|
5133
|
-
|
|
5135
|
+
const slot = r.duration != null ? ` · 坑位 ${r.duration}s` : "";
|
|
5136
|
+
const hint = r.duration_hint != null ? ` · hint ${r.duration_hint}s` : "";
|
|
5137
|
+
L.push(`- \`${r.beat}\` composition_id=\`${r.composition_id}\`${slot}${hint}`);
|
|
5134
5138
|
}
|
|
5135
5139
|
L.push("");
|
|
5136
5140
|
L.push("## AI_DRAMA Queue");
|
|
@@ -7310,7 +7314,109 @@ async function runLand(splitdocPath, baseDir, gtrkPath, transcriptPath, opts) {
|
|
|
7310
7314
|
// src/commands/matrix.ts
|
|
7311
7315
|
import { resolve as resolve8, join as join19, dirname as dirname7, basename as basename9 } from "node:path";
|
|
7312
7316
|
import { existsSync as existsSync15 } from "node:fs";
|
|
7313
|
-
import { readFile as readFile5, writeFile as writeFile7, mkdir as mkdir6 } from "node:fs/promises";
|
|
7317
|
+
import { readFile as readFile5, writeFile as writeFile7, mkdir as mkdir6, rename } from "node:fs/promises";
|
|
7318
|
+
|
|
7319
|
+
// src/lib/solid-png.ts
|
|
7320
|
+
import { deflateSync } from "node:zlib";
|
|
7321
|
+
var BLACK_BED_HEX = "000000";
|
|
7322
|
+
var SOLID_MATERIAL_PREFIX = "ex-solid-";
|
|
7323
|
+
var BUILTIN_ASSET_DIR = "assets/builtin";
|
|
7324
|
+
var MAX_BED_DIM = 8192;
|
|
7325
|
+
var PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
7326
|
+
var CRC_TABLE = (() => {
|
|
7327
|
+
const table = new Uint32Array(256);
|
|
7328
|
+
for (let n = 0;n < 256; n++) {
|
|
7329
|
+
let c3 = n;
|
|
7330
|
+
for (let k = 0;k < 8; k++)
|
|
7331
|
+
c3 = c3 & 1 ? 3988292384 ^ c3 >>> 1 : c3 >>> 1;
|
|
7332
|
+
table[n] = c3 >>> 0;
|
|
7333
|
+
}
|
|
7334
|
+
return table;
|
|
7335
|
+
})();
|
|
7336
|
+
function crc32(buf) {
|
|
7337
|
+
let c3 = 4294967295;
|
|
7338
|
+
for (let i = 0;i < buf.length; i++)
|
|
7339
|
+
c3 = (CRC_TABLE[(c3 ^ buf[i]) & 255] ^ c3 >>> 8) >>> 0;
|
|
7340
|
+
return (c3 ^ 4294967295) >>> 0;
|
|
7341
|
+
}
|
|
7342
|
+
function chunk(type, data) {
|
|
7343
|
+
const len = Buffer.alloc(4);
|
|
7344
|
+
len.writeUInt32BE(data.length, 0);
|
|
7345
|
+
const body = Buffer.concat([Buffer.from(type, "ascii"), data]);
|
|
7346
|
+
const crc = Buffer.alloc(4);
|
|
7347
|
+
crc.writeUInt32BE(crc32(body), 0);
|
|
7348
|
+
return Buffer.concat([len, body, crc]);
|
|
7349
|
+
}
|
|
7350
|
+
function normalizeHex(hex) {
|
|
7351
|
+
const h = hex.replace(/^#/, "").toLowerCase();
|
|
7352
|
+
return /^[0-9a-f]{6}$/.test(h) ? h : null;
|
|
7353
|
+
}
|
|
7354
|
+
function solidMaterialId({
|
|
7355
|
+
hex,
|
|
7356
|
+
width,
|
|
7357
|
+
height
|
|
7358
|
+
}) {
|
|
7359
|
+
const h = normalizeHex(hex);
|
|
7360
|
+
if (!h)
|
|
7361
|
+
throw new Error(`非法纯色 hex:${hex}`);
|
|
7362
|
+
return `${SOLID_MATERIAL_PREFIX}${h}-${width}x${height}`;
|
|
7363
|
+
}
|
|
7364
|
+
function solidRelPath({
|
|
7365
|
+
hex,
|
|
7366
|
+
width,
|
|
7367
|
+
height
|
|
7368
|
+
}) {
|
|
7369
|
+
const h = normalizeHex(hex);
|
|
7370
|
+
if (!h)
|
|
7371
|
+
throw new Error(`非法纯色 hex:${hex}`);
|
|
7372
|
+
return `${BUILTIN_ASSET_DIR}/solid-${h}-${width}x${height}.png`;
|
|
7373
|
+
}
|
|
7374
|
+
function isLayoutableCanvas(canvas) {
|
|
7375
|
+
if (!Array.isArray(canvas) || canvas.length !== 2)
|
|
7376
|
+
return false;
|
|
7377
|
+
return canvas.every((v) => typeof v === "number" && Number.isInteger(v) && v > 0 && v <= MAX_BED_DIM);
|
|
7378
|
+
}
|
|
7379
|
+
function encodeSolidPng({
|
|
7380
|
+
hex,
|
|
7381
|
+
width,
|
|
7382
|
+
height
|
|
7383
|
+
}) {
|
|
7384
|
+
const h = normalizeHex(hex);
|
|
7385
|
+
if (!h)
|
|
7386
|
+
throw new Error(`非法纯色 hex:${hex}`);
|
|
7387
|
+
if (!isLayoutableCanvas([width, height])) {
|
|
7388
|
+
throw new Error(`纯色画布尺寸非法或超上限(${width}x${height},上限 ${MAX_BED_DIM})`);
|
|
7389
|
+
}
|
|
7390
|
+
const r = Number.parseInt(h.slice(0, 2), 16);
|
|
7391
|
+
const g2 = Number.parseInt(h.slice(2, 4), 16);
|
|
7392
|
+
const b = Number.parseInt(h.slice(4, 6), 16);
|
|
7393
|
+
const ihdr = Buffer.alloc(13);
|
|
7394
|
+
ihdr.writeUInt32BE(width, 0);
|
|
7395
|
+
ihdr.writeUInt32BE(height, 4);
|
|
7396
|
+
ihdr[8] = 8;
|
|
7397
|
+
ihdr[9] = 2;
|
|
7398
|
+
ihdr[10] = 0;
|
|
7399
|
+
ihdr[11] = 0;
|
|
7400
|
+
ihdr[12] = 0;
|
|
7401
|
+
const stride = 1 + width * 3;
|
|
7402
|
+
const row = Buffer.alloc(stride);
|
|
7403
|
+
row[0] = 0;
|
|
7404
|
+
for (let x = 0;x < width; x++) {
|
|
7405
|
+
const off = 1 + x * 3;
|
|
7406
|
+
row[off] = r;
|
|
7407
|
+
row[off + 1] = g2;
|
|
7408
|
+
row[off + 2] = b;
|
|
7409
|
+
}
|
|
7410
|
+
const raw = Buffer.alloc(stride * height);
|
|
7411
|
+
for (let y = 0;y < height; y++)
|
|
7412
|
+
row.copy(raw, y * stride);
|
|
7413
|
+
return Buffer.concat([
|
|
7414
|
+
PNG_SIGNATURE,
|
|
7415
|
+
chunk("IHDR", ihdr),
|
|
7416
|
+
chunk("IDAT", deflateSync(raw, { level: 9 })),
|
|
7417
|
+
chunk("IEND", Buffer.alloc(0))
|
|
7418
|
+
]);
|
|
7419
|
+
}
|
|
7314
7420
|
|
|
7315
7421
|
// src/lib/matrix-lay.ts
|
|
7316
7422
|
var BROLL_PREVIEW_DIR = "assets/broll-preview";
|
|
@@ -7320,6 +7426,21 @@ var SHOT_TARGET_DEFAULT = 3;
|
|
|
7320
7426
|
var MIN_SHOT_SEC = 1.2;
|
|
7321
7427
|
var SCORE_FLOOR_DEFAULT = 0.2;
|
|
7322
7428
|
var MAX_SLOTS_PER_BEAT = 32;
|
|
7429
|
+
var BLACK_BED_MERGE_EPS = 0.001;
|
|
7430
|
+
function mergeBlackBedSegments(envelopes) {
|
|
7431
|
+
const valid = envelopes.filter((e) => Number.isFinite(e.track_st) && Number.isFinite(e.track_ed) && e.track_ed > e.track_st).sort((a, b) => a.track_st - b.track_st);
|
|
7432
|
+
const out = [];
|
|
7433
|
+
for (const e of valid) {
|
|
7434
|
+
const last = out[out.length - 1];
|
|
7435
|
+
if (last && e.track_st - last.track_ed <= BLACK_BED_MERGE_EPS) {
|
|
7436
|
+
if (e.track_ed > last.track_ed)
|
|
7437
|
+
last.track_ed = e.track_ed;
|
|
7438
|
+
continue;
|
|
7439
|
+
}
|
|
7440
|
+
out.push({ track_st: e.track_st, track_ed: e.track_ed });
|
|
7441
|
+
}
|
|
7442
|
+
return out.map((s) => ({ track_st: r33(s.track_st), track_ed: r33(s.track_ed) }));
|
|
7443
|
+
}
|
|
7323
7444
|
function mergedCandidates(beat) {
|
|
7324
7445
|
const all = [];
|
|
7325
7446
|
for (const q of beat.queries)
|
|
@@ -7493,6 +7614,8 @@ function planBeatFills(plan, lay, scoreFloor) {
|
|
|
7493
7614
|
}
|
|
7494
7615
|
function layBrollTracks(opts) {
|
|
7495
7616
|
const { gtrk, plan, lay, fills, downloads } = opts;
|
|
7617
|
+
const blackBedOn = opts.blackBed !== false;
|
|
7618
|
+
const warnings = [];
|
|
7496
7619
|
const videoTracks = [...gtrk.video_track ?? []];
|
|
7497
7620
|
const materials = [...gtrk.materials ?? []];
|
|
7498
7621
|
const structMeta = { ...gtrk.struct_meta ?? {} };
|
|
@@ -7504,11 +7627,31 @@ function layBrollTracks(opts) {
|
|
|
7504
7627
|
for (const t of removedTracks) {
|
|
7505
7628
|
for (const c3 of t.track_timeline ?? []) {
|
|
7506
7629
|
const m = c3.material;
|
|
7507
|
-
if (typeof m === "string" && m.startsWith(BROLL_MATERIAL_PREFIX))
|
|
7630
|
+
if (typeof m === "string" && (m.startsWith(BROLL_MATERIAL_PREFIX) || m.startsWith(SOLID_MATERIAL_PREFIX))) {
|
|
7508
7631
|
removedMaterialIds.add(m);
|
|
7632
|
+
}
|
|
7509
7633
|
}
|
|
7510
7634
|
}
|
|
7635
|
+
const stillReferenced = new Set;
|
|
7636
|
+
for (const group of [keptTracks, gtrk.beat_track ?? [], gtrk.audio_track ?? []]) {
|
|
7637
|
+
for (const t of group) {
|
|
7638
|
+
for (const c3 of t.track_timeline ?? []) {
|
|
7639
|
+
if (typeof c3.material === "string")
|
|
7640
|
+
stillReferenced.add(c3.material);
|
|
7641
|
+
}
|
|
7642
|
+
}
|
|
7643
|
+
}
|
|
7644
|
+
for (const id of stillReferenced)
|
|
7645
|
+
removedMaterialIds.delete(id);
|
|
7511
7646
|
const keptMaterials = materials.filter((m) => !(typeof m.id === "string" && removedMaterialIds.has(m.id)));
|
|
7647
|
+
for (const t of keptTracks) {
|
|
7648
|
+
const clips = t.track_timeline ?? [];
|
|
7649
|
+
if (!clips.length)
|
|
7650
|
+
continue;
|
|
7651
|
+
if (clips.every((c3) => typeof c3.material === "string" && c3.material.startsWith(SOLID_MATERIAL_PREFIX))) {
|
|
7652
|
+
warnings.push(`检测到疑似手工纯色底轨(track_index=${t.track_index}):新候选轨会落到它之下被遮住。建议删除该轨后重跑,或用 --no-black-bed。`);
|
|
7653
|
+
}
|
|
7654
|
+
}
|
|
7512
7655
|
const canvas = Array.isArray(gtrk.video_size) ? gtrk.video_size : [1920, 1080];
|
|
7513
7656
|
const baseIndex = keptTracks.reduce((mx, t) => Math.max(mx, typeof t.track_index === "number" ? t.track_index : 0), -1) + 1;
|
|
7514
7657
|
const candById = new Map;
|
|
@@ -7591,24 +7734,71 @@ function layBrollTracks(opts) {
|
|
|
7591
7734
|
muted: false,
|
|
7592
7735
|
track_timeline: clips.sort((a, b) => a.track_st - b.track_st)
|
|
7593
7736
|
}));
|
|
7737
|
+
let blackTrack = null;
|
|
7738
|
+
let blackTrackObj = null;
|
|
7739
|
+
if (blackBedOn && createdTracks.length > 0) {
|
|
7740
|
+
if (!isLayoutableCanvas([canvas[0], canvas[1]])) {
|
|
7741
|
+
warnings.push(`画布尺寸非法(${canvas[0]}x${canvas[1]}),跳过铺纯黑底轨(候选轨照常铺)。`);
|
|
7742
|
+
} else {
|
|
7743
|
+
const laidBeatEnvelopes = metaBeats.filter((b) => b.laid.length > 0).map((b) => ({ track_st: b.track_st, track_ed: b.track_ed }));
|
|
7744
|
+
const segments = mergeBlackBedSegments(laidBeatEnvelopes);
|
|
7745
|
+
if (segments.length > 0) {
|
|
7746
|
+
const width = canvas[0];
|
|
7747
|
+
const height = canvas[1];
|
|
7748
|
+
const solidId = solidMaterialId({ hex: BLACK_BED_HEX, width, height });
|
|
7749
|
+
newMaterialsById.set(solidId, {
|
|
7750
|
+
id: solidId,
|
|
7751
|
+
path: solidRelPath({ hex: BLACK_BED_HEX, width, height }),
|
|
7752
|
+
video_size: [width, height]
|
|
7753
|
+
});
|
|
7754
|
+
blackTrack = baseIndex + lay;
|
|
7755
|
+
blackTrackObj = {
|
|
7756
|
+
track_index: blackTrack,
|
|
7757
|
+
track_size: [width, height],
|
|
7758
|
+
muted: false,
|
|
7759
|
+
track_timeline: segments.map((s, i) => ({
|
|
7760
|
+
clip_id: `blackbed-${i}`,
|
|
7761
|
+
material: solidId,
|
|
7762
|
+
clip_st: 0,
|
|
7763
|
+
clip_ed: r33(s.track_ed - s.track_st),
|
|
7764
|
+
track_st: s.track_st,
|
|
7765
|
+
track_ed: s.track_ed,
|
|
7766
|
+
duration: r33(s.track_ed - s.track_st)
|
|
7767
|
+
}))
|
|
7768
|
+
};
|
|
7769
|
+
}
|
|
7770
|
+
}
|
|
7771
|
+
}
|
|
7772
|
+
const laidTrackIndices = createdTracks.map((t) => t.track_index);
|
|
7594
7773
|
const broll = {
|
|
7595
7774
|
contract_version: "v1",
|
|
7596
7775
|
generated_at: opts.generatedAt,
|
|
7597
7776
|
plan_path: opts.planPath,
|
|
7598
|
-
lay_tracks:
|
|
7777
|
+
lay_tracks: blackTrack === null ? laidTrackIndices : [...laidTrackIndices, blackTrack],
|
|
7778
|
+
black_track: blackTrack,
|
|
7599
7779
|
confirmed: false,
|
|
7600
7780
|
beats: metaBeats
|
|
7601
7781
|
};
|
|
7782
|
+
const mergedMaterials = new Map;
|
|
7783
|
+
for (const m of keptMaterials) {
|
|
7784
|
+
if (typeof m.id === "string")
|
|
7785
|
+
mergedMaterials.set(m.id, m);
|
|
7786
|
+
else
|
|
7787
|
+
mergedMaterials.set(`__anon_${mergedMaterials.size}`, m);
|
|
7788
|
+
}
|
|
7789
|
+
for (const [id, m] of newMaterialsById)
|
|
7790
|
+
mergedMaterials.set(id, m);
|
|
7602
7791
|
const next = {
|
|
7603
7792
|
...gtrk,
|
|
7604
|
-
materials: [...
|
|
7605
|
-
video_track: [...keptTracks, ...createdTracks],
|
|
7793
|
+
materials: [...mergedMaterials.values()],
|
|
7794
|
+
video_track: [...keptTracks, ...createdTracks, ...blackTrackObj ? [blackTrackObj] : []],
|
|
7606
7795
|
struct_meta: { ...structMeta, broll }
|
|
7607
7796
|
};
|
|
7608
7797
|
return {
|
|
7609
7798
|
next,
|
|
7610
|
-
summary: { laidTracks:
|
|
7611
|
-
broll
|
|
7799
|
+
summary: { laidTracks: laidTrackIndices, laidClips, beatsWithCandidates, blackTrack },
|
|
7800
|
+
broll,
|
|
7801
|
+
warnings
|
|
7612
7802
|
};
|
|
7613
7803
|
}
|
|
7614
7804
|
|
|
@@ -7799,7 +7989,7 @@ async function searchOnce(cfg, tier, body) {
|
|
|
7799
7989
|
|
|
7800
7990
|
// src/commands/matrix.ts
|
|
7801
7991
|
function registerMatrix(program2) {
|
|
7802
|
-
program2.command("matrix [words...]").description('B-roll 检索:无 positional=消费 split/dispatch.json 的 film_broll 队列产候选清单;`matrix search "<query>"`=单条 ad-hoc 检索').option("--project <dir>", "oralcut 产物目录(定位 split/dispatch.json 与产物落点)").option("--dispatch <path>", "显式指定 dispatch.json(非标准布局兜底)").option("--column <id>", "栏目配置 id(缺省取 config defaultColumn,再缺省内置默认栏目)").option("--top-k <n>", "每 query 候选数上限(覆盖派单 shots 翻译;服务端上限 50)").option("--material-class <c>", "素材类型 real_shot|concept(仅矩阵成员口;覆盖栏目 material_class_policy)").option("--lay <n>", "候选铺轨数:下载 preview 代理并在工程里平铺 N 条 B-roll 候选轨(默认 1;0=只出 plan 不铺轨)", "1").option("--score-floor <f>", "填充置信度地板:segment score 低于此值不采纳,槽位留空露主轨(默认 0.2)").option("--out <file>", "ad-hoc 模式:结果落文件(缺省输出 stdout)").option("--json", "机读模式:人读日志转 stderr,stdout 只输出结果 JSON").action(async (words, opts) => {
|
|
7992
|
+
program2.command("matrix [words...]").description('B-roll 检索:无 positional=消费 split/dispatch.json 的 film_broll 队列产候选清单;`matrix search "<query>"`=单条 ad-hoc 检索').option("--project <dir>", "oralcut 产物目录(定位 split/dispatch.json 与产物落点)").option("--dispatch <path>", "显式指定 dispatch.json(非标准布局兜底)").option("--column <id>", "栏目配置 id(缺省取 config defaultColumn,再缺省内置默认栏目)").option("--top-k <n>", "每 query 候选数上限(覆盖派单 shots 翻译;服务端上限 50)").option("--material-class <c>", "素材类型 real_shot|concept(仅矩阵成员口;覆盖栏目 material_class_policy)").option("--lay <n>", "候选铺轨数:下载 preview 代理并在工程里平铺 N 条 B-roll 候选轨(默认 1;0=只出 plan 不铺轨)", "1").option("--score-floor <f>", "填充置信度地板:segment score 低于此值不采纳,槽位留空露主轨(默认 0.2)").option("--no-black-bed", "不铺纯黑底垫轨(默认铺一条,垫在候选轨之下、口播主轨之上,用于 B-roll 期间遮住口播画面)").option("--out <file>", "ad-hoc 模式:结果落文件(缺省输出 stdout)").option("--json", "机读模式:人读日志转 stderr,stdout 只输出结果 JSON").action(async (words, opts) => {
|
|
7803
7993
|
await runMatrix(parseAdhocQuery(words), opts);
|
|
7804
7994
|
});
|
|
7805
7995
|
}
|
|
@@ -7907,7 +8097,7 @@ async function runPlanMode(cfg, tier, broll, overrides, columnId, opts) {
|
|
|
7907
8097
|
const layN = parseLay(opts.lay);
|
|
7908
8098
|
let laySummary;
|
|
7909
8099
|
if (layN > 0) {
|
|
7910
|
-
laySummary = await layIntoProject(baseDir, plan, layN, parseScoreFloor(opts.scoreFloor));
|
|
8100
|
+
laySummary = await layIntoProject(baseDir, plan, layN, parseScoreFloor(opts.scoreFloor), opts.blackBed ?? true);
|
|
7911
8101
|
}
|
|
7912
8102
|
const result = {
|
|
7913
8103
|
ok: true,
|
|
@@ -7944,7 +8134,7 @@ function locateGtrk(baseDir) {
|
|
|
7944
8134
|
const cands = [join19(baseDir, "gtrk", "project.gtrk"), join19(baseDir, "project.gtrk")];
|
|
7945
8135
|
return cands.find((p) => existsSync15(p));
|
|
7946
8136
|
}
|
|
7947
|
-
async function layIntoProject(baseDir, plan, layN, scoreFloor) {
|
|
8137
|
+
async function layIntoProject(baseDir, plan, layN, scoreFloor, blackBed) {
|
|
7948
8138
|
const gtrkPath = locateGtrk(baseDir);
|
|
7949
8139
|
if (!gtrkPath) {
|
|
7950
8140
|
log.warn(`未找到工程文件(${join19(baseDir, "gtrk", "project.gtrk")}),跳过铺轨——plan 已产出,可后续在有工程的目录重跑`);
|
|
@@ -8006,22 +8196,57 @@ async function layIntoProject(baseDir, plan, layN, scoreFloor) {
|
|
|
8006
8196
|
dlStats.failed++;
|
|
8007
8197
|
}
|
|
8008
8198
|
}
|
|
8009
|
-
|
|
8199
|
+
let { next, summary, warnings } = layBrollTracks({
|
|
8010
8200
|
gtrk,
|
|
8011
8201
|
plan,
|
|
8012
8202
|
lay: layN,
|
|
8013
8203
|
fills,
|
|
8014
8204
|
downloads,
|
|
8015
8205
|
generatedAt: new Date().toISOString(),
|
|
8016
|
-
planPath: "split/broll-plan.json"
|
|
8206
|
+
planPath: "split/broll-plan.json",
|
|
8207
|
+
blackBed
|
|
8017
8208
|
});
|
|
8209
|
+
if (summary.blackTrack !== null) {
|
|
8210
|
+
const canvas = gtrk.video_size;
|
|
8211
|
+
const spec = { hex: BLACK_BED_HEX, width: canvas[0], height: canvas[1] };
|
|
8212
|
+
const rel = solidRelPath(spec);
|
|
8213
|
+
const abs = join19(gtrkDir, ...rel.split("/"));
|
|
8214
|
+
try {
|
|
8215
|
+
if (!existsSync15(abs)) {
|
|
8216
|
+
await mkdir6(dirname7(abs), { recursive: true });
|
|
8217
|
+
const tmp = `${abs}.tmp-${process.pid}`;
|
|
8218
|
+
await writeFile7(tmp, encodeSolidPng(spec));
|
|
8219
|
+
await rename(tmp, abs);
|
|
8220
|
+
}
|
|
8221
|
+
} catch (e) {
|
|
8222
|
+
log.warn(`纯黑底 PNG 落盘失败(${rel}):${e.message} —— 本次不铺黑底垫轨,候选轨照常。`);
|
|
8223
|
+
({ next, summary, warnings } = layBrollTracks({
|
|
8224
|
+
gtrk,
|
|
8225
|
+
plan,
|
|
8226
|
+
lay: layN,
|
|
8227
|
+
fills,
|
|
8228
|
+
downloads,
|
|
8229
|
+
generatedAt: new Date().toISOString(),
|
|
8230
|
+
planPath: "split/broll-plan.json",
|
|
8231
|
+
blackBed: false
|
|
8232
|
+
}));
|
|
8233
|
+
}
|
|
8234
|
+
}
|
|
8018
8235
|
writeGtrkAtomic(gtrkPath, next, mtimeMs);
|
|
8019
|
-
|
|
8236
|
+
const bedNote = summary.blackTrack !== null ? ` · 纯黑底垫轨 track_index ${summary.blackTrack}` : blackBed ? " · 未铺纯黑底垫轨" : " · 纯黑底垫轨已关闭(--no-black-bed)";
|
|
8237
|
+
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}` : ""})${bedNote}`);
|
|
8020
8238
|
log.info("opencut 打开工程即见候选轨:轨道头小眼睛可开关对比;确认下载原片属挑选 UI(E-P1)。");
|
|
8239
|
+
for (const w of warnings)
|
|
8240
|
+
log.warn(w);
|
|
8021
8241
|
if (dlStats.raw > 0) {
|
|
8022
8242
|
log.warn("部分候选无 preview 代理已回落原片(体积较大)——服务端 backfill 后重跑本命令可换回代理。");
|
|
8023
8243
|
}
|
|
8024
|
-
return {
|
|
8244
|
+
return {
|
|
8245
|
+
laidTracks: summary.laidTracks,
|
|
8246
|
+
laidClips: summary.laidClips,
|
|
8247
|
+
blackTrack: summary.blackTrack,
|
|
8248
|
+
downloads: dlStats
|
|
8249
|
+
};
|
|
8025
8250
|
}
|
|
8026
8251
|
async function downloadProxy(cand, absPath, opts = {}) {
|
|
8027
8252
|
const tryFetch = async (url) => {
|
|
@@ -8124,6 +8349,42 @@ var CATEGORY_EXPECTED_OPAQUE2 = {
|
|
|
8124
8349
|
"explain-subtitle": false,
|
|
8125
8350
|
"op-ed-title": true
|
|
8126
8351
|
};
|
|
8352
|
+
function estimateTimelineSec(html) {
|
|
8353
|
+
const re = /\.\s*(?:to|from|fromTo|set|add)\s*\(([\s\S]*?)\)\s*;/g;
|
|
8354
|
+
const calls = [];
|
|
8355
|
+
let m;
|
|
8356
|
+
while (m = re.exec(html)) {
|
|
8357
|
+
const args = m[1] ?? "";
|
|
8358
|
+
const lastBrace = args.lastIndexOf("}");
|
|
8359
|
+
let pos = null;
|
|
8360
|
+
if (lastBrace >= 0) {
|
|
8361
|
+
const tail = args.slice(lastBrace + 1).replace(/^\s*,\s*/, "").trim();
|
|
8362
|
+
if (tail)
|
|
8363
|
+
pos = tail.replace(/^["']|["']$/g, "");
|
|
8364
|
+
}
|
|
8365
|
+
calls.push({ body: args, pos });
|
|
8366
|
+
}
|
|
8367
|
+
if (!calls.length)
|
|
8368
|
+
return null;
|
|
8369
|
+
let chain = 0;
|
|
8370
|
+
let maxEnd = 0;
|
|
8371
|
+
for (const c3 of calls) {
|
|
8372
|
+
if (/\brepeat\s*:/.test(c3.body) || /\byoyo\s*:\s*true\b/.test(c3.body))
|
|
8373
|
+
return null;
|
|
8374
|
+
if (c3.pos !== null && !/^-?\d+(?:\.\d+)?$/.test(c3.pos))
|
|
8375
|
+
return null;
|
|
8376
|
+
const d = /\bduration\s*:\s*(-?\d+(?:\.\d+)?)/.exec(c3.body);
|
|
8377
|
+
const dur = d ? Number(d[1]) : 0;
|
|
8378
|
+
if (!Number.isFinite(dur) || dur < 0)
|
|
8379
|
+
return null;
|
|
8380
|
+
if (c3.pos !== null)
|
|
8381
|
+
maxEnd = Math.max(maxEnd, Number(c3.pos) + dur);
|
|
8382
|
+
else
|
|
8383
|
+
chain += dur;
|
|
8384
|
+
}
|
|
8385
|
+
const est = Math.max(chain, maxEnd);
|
|
8386
|
+
return est > 0 ? Math.round(est * 1000) / 1000 : null;
|
|
8387
|
+
}
|
|
8127
8388
|
function lintParticle(html, opts = {}) {
|
|
8128
8389
|
const v = [];
|
|
8129
8390
|
const push = (law, fatal, msg) => v.push({ law, fatal, msg });
|
|
@@ -8193,6 +8454,12 @@ function lintParticle(html, opts = {}) {
|
|
|
8193
8454
|
if (expect !== opaque)
|
|
8194
8455
|
push("x-category-opaque", false, `category「${opts.category}」期望${expect ? "不透明满屏" : "透明叠加"},但颗粒 HTML 反推为${opaque ? "不透明满屏" : "透明叠加"}(以 HTML 为准落 clip.opaque=${opaque})`);
|
|
8195
8456
|
}
|
|
8457
|
+
if (typeof opts.slotDuration === "number" && opts.slotDuration > 0) {
|
|
8458
|
+
const est = estimateTimelineSec(html);
|
|
8459
|
+
if (est !== null && est < opts.slotDuration * 0.8) {
|
|
8460
|
+
push("7-fill-slot", false, `时间线静态估长 ~${est}s,短于槽位包络 ${opts.slotDuration}s(铁律⑦:颗粒应占满坑位并终态驻留)——静态估算仅供参考,含 repeat/yoyo/相对定位时不作数`);
|
|
8461
|
+
}
|
|
8462
|
+
}
|
|
8196
8463
|
return { ok: !v.some((x) => x.fatal), violations: v, opaque, compositionId: cid };
|
|
8197
8464
|
}
|
|
8198
8465
|
|
|
@@ -8201,6 +8468,7 @@ var MG_MATERIAL_PREFIX = "mg-";
|
|
|
8201
8468
|
var LEGACY_MATERIAL_PREFIX = "rrv-";
|
|
8202
8469
|
var isOwnMaterialId = (id) => id.startsWith(MG_MATERIAL_PREFIX) || id.startsWith(LEGACY_MATERIAL_PREFIX);
|
|
8203
8470
|
var r34 = (n) => Math.round(n * 1000) / 1000;
|
|
8471
|
+
var slotEnvelope = (it) => r34(it.track_ed - it.track_st);
|
|
8204
8472
|
function layTracksOf(prev) {
|
|
8205
8473
|
return Array.isArray(prev?.lay_tracks) ? prev.lay_tracks.filter((x) => typeof x === "number") : [];
|
|
8206
8474
|
}
|
|
@@ -8233,7 +8501,8 @@ function layMgTracks(opts) {
|
|
|
8233
8501
|
].map((t) => typeof t.track_index === "number" ? t.track_index : 0);
|
|
8234
8502
|
const newIndex = Math.max(9, ...allIndices) + 1;
|
|
8235
8503
|
for (const it of items) {
|
|
8236
|
-
|
|
8504
|
+
const envelope = slotEnvelope(it);
|
|
8505
|
+
if (!(envelope > 0)) {
|
|
8237
8506
|
metaBeats.push({ ...toMetaBeat(it), laid: null });
|
|
8238
8507
|
continue;
|
|
8239
8508
|
}
|
|
@@ -8245,7 +8514,7 @@ function layMgTracks(opts) {
|
|
|
8245
8514
|
html_material: materialId,
|
|
8246
8515
|
opaque: it.opaque,
|
|
8247
8516
|
track_st: it.track_st,
|
|
8248
|
-
duration:
|
|
8517
|
+
duration: envelope
|
|
8249
8518
|
});
|
|
8250
8519
|
metaBeats.push({ ...toMetaBeat(it), laid: { track_index: newIndex } });
|
|
8251
8520
|
}
|
|
@@ -8281,7 +8550,7 @@ function toMetaBeat(it) {
|
|
|
8281
8550
|
composition_id: it.composition_id,
|
|
8282
8551
|
track_st: it.track_st,
|
|
8283
8552
|
track_ed: r34(it.track_ed),
|
|
8284
|
-
duration: it
|
|
8553
|
+
duration: slotEnvelope(it),
|
|
8285
8554
|
html_path: it.html_rel,
|
|
8286
8555
|
...it.category ? { category: it.category } : {}
|
|
8287
8556
|
};
|
|
@@ -8357,7 +8626,13 @@ async function runLay(opts) {
|
|
|
8357
8626
|
}
|
|
8358
8627
|
const html = await readFile6(srcPath, "utf8");
|
|
8359
8628
|
const category = typeof q.category === "string" ? q.category : undefined;
|
|
8360
|
-
const
|
|
8629
|
+
const slotDuration = Math.round((q.track_ed - q.track_st) * 1000) / 1000;
|
|
8630
|
+
const lint = lintParticle(html, {
|
|
8631
|
+
compositionId: q.composition_id,
|
|
8632
|
+
dispatchIds,
|
|
8633
|
+
category,
|
|
8634
|
+
...slotDuration > 0 ? { slotDuration } : {}
|
|
8635
|
+
});
|
|
8361
8636
|
for (const vv of lint.violations)
|
|
8362
8637
|
(vv.fatal ? log.warn : log.info)(`${q.beat} lint ${vv.fatal ? "✗" : "·"} ${vv.law}: ${vv.msg}`);
|
|
8363
8638
|
if (!lint.ok) {
|
|
@@ -8365,8 +8640,8 @@ async function runLay(opts) {
|
|
|
8365
8640
|
log.warn(`${q.beat}:lint 未过,跳过`);
|
|
8366
8641
|
continue;
|
|
8367
8642
|
}
|
|
8368
|
-
if (
|
|
8369
|
-
skipped.push({ beat: q.beat, reason: "
|
|
8643
|
+
if (!(slotDuration > 0)) {
|
|
8644
|
+
skipped.push({ beat: q.beat, reason: "槽位包络非正数" });
|
|
8370
8645
|
continue;
|
|
8371
8646
|
}
|
|
8372
8647
|
srcByComp.set(q.composition_id, srcPath);
|
|
@@ -8375,7 +8650,6 @@ async function runLay(opts) {
|
|
|
8375
8650
|
composition_id: q.composition_id,
|
|
8376
8651
|
track_st: q.track_st,
|
|
8377
8652
|
track_ed: q.track_ed,
|
|
8378
|
-
duration: q.duration,
|
|
8379
8653
|
opaque: lint.opaque,
|
|
8380
8654
|
html_rel: `${MG_ASSET_DIR}/${q.composition_id}.html`,
|
|
8381
8655
|
...category ? { category } : {}
|
|
@@ -10726,7 +11000,7 @@ function beatQuantized(natLens, analysis) {
|
|
|
10726
11000
|
|
|
10727
11001
|
// src/lib/mad/data.ts
|
|
10728
11002
|
import { createHash as createHash2 } from "node:crypto";
|
|
10729
|
-
import { mkdir as mkdir9, readFile as readFile7, rename, rm, writeFile as writeFile9, readdir } from "node:fs/promises";
|
|
11003
|
+
import { mkdir as mkdir9, readFile as readFile7, rename as rename2, rm, writeFile as writeFile9, readdir } from "node:fs/promises";
|
|
10730
11004
|
import { existsSync as existsSync18 } from "node:fs";
|
|
10731
11005
|
import { join as join23 } from "node:path";
|
|
10732
11006
|
function madCacheDir() {
|
|
@@ -10754,7 +11028,7 @@ async function fetchWithTimeout(fetchFn, url, timeoutMs) {
|
|
|
10754
11028
|
async function atomicWrite(dest, data) {
|
|
10755
11029
|
const tmp = `${dest}.tmp-${process.pid}-${Date.now()}`;
|
|
10756
11030
|
await writeFile9(tmp, data);
|
|
10757
|
-
await
|
|
11031
|
+
await rename2(tmp, dest);
|
|
10758
11032
|
}
|
|
10759
11033
|
async function verifyFile(path, sha256) {
|
|
10760
11034
|
if (!existsSync18(path))
|
|
@@ -11264,7 +11538,7 @@ async function runMadInTool(inputArg, opts) {
|
|
|
11264
11538
|
|
|
11265
11539
|
// src/commands/transcript.ts
|
|
11266
11540
|
import { existsSync as existsSync21 } from "node:fs";
|
|
11267
|
-
import { mkdir as mkdir12, rename as
|
|
11541
|
+
import { mkdir as mkdir12, rename as rename3, rm as rm2, stat as stat6, writeFile as writeFile11 } from "node:fs/promises";
|
|
11268
11542
|
import { basename as basename12, dirname as dirname10, extname as extname7, join as join26, resolve as resolve12 } from "node:path";
|
|
11269
11543
|
|
|
11270
11544
|
// src/lib/transcript.ts
|
|
@@ -11425,7 +11699,7 @@ async function writeMarkdownAtomic(path, markdown) {
|
|
|
11425
11699
|
const temp = `${path}.${process.pid}.${Math.random().toString(16).slice(2)}.tmp`;
|
|
11426
11700
|
try {
|
|
11427
11701
|
await writeFile11(temp, markdown, "utf8");
|
|
11428
|
-
await
|
|
11702
|
+
await rename3(temp, path);
|
|
11429
11703
|
} finally {
|
|
11430
11704
|
await rm2(temp, { force: true });
|
|
11431
11705
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gitruck/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.14",
|
|
4
4
|
"description": "同合云成片流水线 CLI —— agent 驱动云端任务、产物拉回本地、三方工程文件(客户端/剪映/PR)互通。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Gitruck (同合云)",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"build": "bun build ./src/index.ts --target=node --outfile dist/index.js",
|
|
49
49
|
"typecheck": "tsc --noEmit",
|
|
50
50
|
"prepublishOnly": "bun run build",
|
|
51
|
-
"test": "esbuild src/lib/chunk-upload.ts src/lib/upload-cache.ts src/lib/upload-submit.ts src/lib/render.ts src/lib/cloud.ts src/lib/materialize.ts src/lib/projection.ts src/lib/splitdoc.ts src/lib/gtrk-writeback.ts src/lib/column-config.ts src/lib/matrix.ts src/lib/matrix-lay.ts src/lib/mg-lint.ts src/lib/mg-lay.ts src/lib/tool-descriptors.ts src/lib/tool-runner.ts src/lib/tool-pricing.ts src/lib/transcript.ts --bundle --platform=node --format=esm --outdir=.test-build --out-extension:.js=.mjs && esbuild src/commands/split.ts --bundle --platform=node --format=esm --outdir=.test-build --out-extension:.js=.mjs && esbuild src/commands/matrix.ts --bundle --platform=node --format=esm --outfile=.test-build/matrix-cmd.mjs && esbuild src/commands/mg.ts --bundle --platform=node --format=esm --outfile=.test-build/mg-cmd.mjs && esbuild src/commands/tool.ts --bundle --platform=node --format=esm --outfile=.test-build/tool-cmd.mjs && esbuild src/commands/transcript.ts --bundle --platform=node --format=esm --outfile=.test-build/transcript-cmd.mjs && esbuild src/commands/music-visualizer.ts --bundle --platform=node --format=esm --external:commander --outfile=.test-build/music-visualizer-cmd.mjs && esbuild src/commands/skills.ts --bundle --platform=node --format=esm --outfile=.test-build/skills-cmd.mjs && esbuild src/lib/convert/ir_to_jsx.ts src/lib/convert/ir_to_am.ts src/lib/convert/ir_to_nv.ts src/lib/convert/am_parse.ts src/lib/convert/nv_parse.ts src/lib/convert/parse.ts src/lib/convert/download.ts src/lib/convert/bake_ops.ts --bundle --platform=node --format=esm --outdir=.test-build/convert --out-extension:.js=.mjs && esbuild src/lib/mad/selector.ts src/lib/mad/beat.ts src/lib/mad/scan.ts src/lib/mad/data.ts src/lib/mad/pool.ts src/lib/mad/cloud-beat.ts src/lib/mad/mad.ts --bundle --platform=node --format=esm --outdir=.test-build/mad --out-extension:.js=.mjs && node --test \"test/*.test.mjs\""
|
|
51
|
+
"test": "esbuild src/lib/chunk-upload.ts src/lib/upload-cache.ts src/lib/upload-submit.ts src/lib/render.ts src/lib/cloud.ts src/lib/materialize.ts src/lib/projection.ts src/lib/splitdoc.ts src/lib/gtrk-writeback.ts src/lib/column-config.ts src/lib/matrix.ts src/lib/matrix-lay.ts src/lib/solid-png.ts src/lib/mg-lint.ts src/lib/mg-lay.ts src/lib/tool-descriptors.ts src/lib/tool-runner.ts src/lib/tool-pricing.ts src/lib/transcript.ts --bundle --platform=node --format=esm --outdir=.test-build --out-extension:.js=.mjs && esbuild src/commands/split.ts --bundle --platform=node --format=esm --outdir=.test-build --out-extension:.js=.mjs && esbuild src/commands/matrix.ts --bundle --platform=node --format=esm --outfile=.test-build/matrix-cmd.mjs && esbuild src/commands/mg.ts --bundle --platform=node --format=esm --outfile=.test-build/mg-cmd.mjs && esbuild src/commands/tool.ts --bundle --platform=node --format=esm --outfile=.test-build/tool-cmd.mjs && esbuild src/commands/transcript.ts --bundle --platform=node --format=esm --outfile=.test-build/transcript-cmd.mjs && esbuild src/commands/music-visualizer.ts --bundle --platform=node --format=esm --external:commander --outfile=.test-build/music-visualizer-cmd.mjs && esbuild src/commands/skills.ts --bundle --platform=node --format=esm --outfile=.test-build/skills-cmd.mjs && esbuild src/lib/convert/ir_to_jsx.ts src/lib/convert/ir_to_am.ts src/lib/convert/ir_to_nv.ts src/lib/convert/am_parse.ts src/lib/convert/nv_parse.ts src/lib/convert/parse.ts src/lib/convert/download.ts src/lib/convert/bake_ops.ts --bundle --platform=node --format=esm --outdir=.test-build/convert --out-extension:.js=.mjs && esbuild src/lib/mad/selector.ts src/lib/mad/beat.ts src/lib/mad/scan.ts src/lib/mad/data.ts src/lib/mad/pool.ts src/lib/mad/cloud-beat.ts src/lib/mad/mad.ts --bundle --platform=node --format=esm --outdir=.test-build/mad --out-extension:.js=.mjs && node --test \"test/*.test.mjs\""
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@xmldom/xmldom": "^0.9.6",
|
|
@@ -46,10 +46,11 @@ description: B-roll 检索铺轨器——成片管线里第一个铺的车道(
|
|
|
46
46
|
| 每段多给几个候选 | `--top-k <n>` | 整数 · 派单 `shots` 值(服务端上限 50) | 覆盖派单里每 query 的候选数上限 |
|
|
47
47
|
| 指定素材类型 | `--material-class <c>` | `real_shot` \| `concept` · 栏目策略 | 仅 internal 矩阵成员口;external 固定 real_shot、传 concept 报错 |
|
|
48
48
|
| 填充太满/太差调门槛 | `--score-floor <f>` | 浮点 0–1 · `0.2` | segment score 低于此值不采纳、槽位留空露主轨 |
|
|
49
|
+
| 不要那条纯黑底轨 | `--no-black-bed` | 开关 · 默认铺 | 默认在候选轨之下、口播主轨之上垫一条纯黑底轨,按 beat 包络整条铺满,B-roll 期间遮住底下的口播画面(含候选轨留空处);不想要就传这个 |
|
|
49
50
|
| ad-hoc 结果落文件 | `--out <file>` | 路径 · 缺省 stdout | 仅 `search` 模式 |
|
|
50
51
|
| 机读(你必带) | `--json` | 开关 · 关 | 人读日志转 stderr,stdout 只出一行结果 JSON |
|
|
51
52
|
|
|
52
|
-
> **因势象形举例**:「B-roll 多铺几条候选让我挑」→ `--lay 3`;「这段填得太杂、卡严点」→ `--score-floor 0.35`;「每段多给点候选」→ `--top-k 12`;「先只出清单别铺轨」→ `--lay 0`;「单独给『深夜地铁失神』搜一组」→ `gtrk matrix search "exhausted commuter staring blankly on a late night subway" --project <目录> --json`。
|
|
53
|
+
> **因势象形举例**:「B-roll 多铺几条候选让我挑」→ `--lay 3`;「这段填得太杂、卡严点」→ `--score-floor 0.35`;「每段多给点候选」→ `--top-k 12`;「先只出清单别铺轨」→ `--lay 0`;「别给我垫那条黑底」→ `--no-black-bed`;「单独给『深夜地铁失神』搜一组」→ `gtrk matrix search "exhausted commuter staring blankly on a late night subway" --project <目录> --json`。
|
|
53
54
|
|
|
54
55
|
## 执行(每次都带 `--json`)
|
|
55
56
|
|
|
@@ -58,7 +59,7 @@ gtrk matrix --project "<split 产物目录>" [--lay N] [--score-floor F] [--top-
|
|
|
58
59
|
```
|
|
59
60
|
|
|
60
61
|
- `--json`:人读日志走 stderr,**成功时 stdout 只有一行结果 JSON**:
|
|
61
|
-
`{ ok, mode:"plan", memberType:"internal"|"external", columnId?, planPath, lay:{ laidTracks:[…], laidClips, downloads:{preview,raw,reused,failed} }, counts:{ beats, queries, results, errors } }`
|
|
62
|
+
`{ ok, mode:"plan", memberType:"internal"|"external", columnId?, planPath, lay:{ laidTracks:[…], laidClips, blackTrack, downloads:{preview,raw,reused,failed} }, counts:{ beats, queries, results, errors } }`
|
|
62
63
|
(`--lay 0` 时无 `lay` 字段;ad-hoc `search` 模式则是 `{ ok, mode:"search", results:[…], counts, outPath? }`)
|
|
63
64
|
- 产物:候选清单 `<目录>/split/broll-plan.json`(只含引用不含素材,`cover_url` 可预览、签名 url 约 24h 过期,过期重跑即重签),铺轨则把候选轨写回工程 `gtrk/project.gtrk`。
|
|
64
65
|
- **命令失败**(缺 `--dispatch`/派单、鉴权失败、全部 query 检索失败、参数越界等)→ **进程非 0 退出、报错打到 stderr、stdout 无 JSON**。先看退出码,非 0 就把 stderr 的报错如实回给用户,别当成功。
|
|
@@ -70,7 +71,8 @@ gtrk matrix --project "<split 产物目录>" [--lay N] [--score-floor F] [--top-
|
|
|
70
71
|
|
|
71
72
|
- `counts`:`beats` 个 beat、`queries` 条检索(`errors` 条失败)、`results` 条候选——一句话概括这次铺了多大盘子。
|
|
72
73
|
- `memberType`:`internal` = 矩阵成员口(栏目偏好/concept 生效);`external` = 通用口(固定 real_shot + 有版权素材)——档位影响命中类型,若用户期望 concept 却是 external,明说「当前身份只能出实拍有版权素材」。
|
|
73
|
-
- `lay.laidTracks` / `lay.laidClips`:铺了几条候选轨、共几个颗粒(`--lay 0` 时无此字段,只出了 plan
|
|
74
|
+
- `lay.laidTracks` / `lay.laidClips`:铺了几条候选轨、共几个颗粒(`--lay 0` 时无此字段,只出了 plan)。**`laidTracks` 只含候选轨、不含黑底垫轨。**
|
|
75
|
+
- `lay.blackTrack`:纯黑底垫轨的 `track_index`(未铺时 `null`,如传了 `--no-black-bed`、画布尺寸非法、或零候选轨落成)。
|
|
74
76
|
- `lay.downloads`:`preview` 代理数 / `raw` **原片回落**数(无 preview 代理的候选回落下原片、体积大,服务端 backfill 后重跑本命令可换回代理)/ `reused` 复用 / `failed` 掉的槽位——`raw`/`failed` 非零时提一句。
|
|
75
77
|
- **单 query 失败是局部化的**(`counts.errors > 0` 但 `ok:true`):个别检索失败不拖垮整盘,如实说哪几段没检到、其余照铺。只有**全部 query 都失败**才会整体非 0 退出。
|
|
76
78
|
- 工程缺失/非 v1 → 命令会**告警跳过铺轨但仍产 plan**(stderr 有提示)——这时 `lay` 字段缺失,告诉用户 plan 已出、可在有工程的目录重跑铺轨。
|
|
@@ -82,6 +84,7 @@ gtrk matrix --project "<split 产物目录>" [--lay N] [--score-floor F] [--top-
|
|
|
82
84
|
- **在同合云桌面客户端(opencut / OpenCut Gitruck Edition)里打开这个工程**,B-roll 候选轨已经铺好。
|
|
83
85
|
- **用轨道头的「小眼睛」开关逐条切换对比**:看哪条候选最贴这段口播的情绪/画面,留下满意的、关掉不要的。
|
|
84
86
|
- 候选默认是 preview 代理(轻量预览);**下载原片属挑选后的动作**(客户端挑选 UI),确认要哪条再拉原片。
|
|
87
|
+
- **候选轨下方还垫了一条纯黑底轨**(`lay.blackTrack`),按 beat 包络整条铺满,作用是 B-roll 期间遮住底下的口播画面(含候选轨上的留空处)——删多余候选轨时**别误删它**;换片请拖到**候选轨的颗粒**上、**别拖到黑底条上**(含拖拽保护的客户端会直接拒绝并提示;尚未升级到该版本的客户端会被误拖打出黑底破洞、该处漏口播)。不想要黑底就加 `--no-black-bed` 重跑,会自动剥净。
|
|
85
88
|
- 觉得填充有问题(太杂/太空/漏段)先别急着往下——**回来告诉我**,我按下面「常见情况」调参重铺。
|
|
86
89
|
|
|
87
90
|
**用户明确说「B-roll 就这样、可以了」之后**,才交棒 ④。别自作主张替他拍板往下冲。
|
|
@@ -91,6 +94,7 @@ gtrk matrix --project "<split 产物目录>" [--lay N] [--score-floor F] [--top-
|
|
|
91
94
|
| 情况 | 怎么做 |
|
|
92
95
|
|---|---|
|
|
93
96
|
| 想在多个候选里挑 | `--lay N` 多铺几条候选轨,opencut 小眼睛切换对比(N 越大越占轨、挑完可删多余轨) |
|
|
97
|
+
| 不要那条黑底垫轨 | `--no-black-bed` 重跑,黑轨与黑底素材一并剥净、候选轨层级不受影响 |
|
|
94
98
|
| 填充太差 / 命中太杂 | 调 `--score-floor`(调高更严、露主轨;调低更满、可能杂)重跑 |
|
|
95
99
|
| 每段候选太少不够挑 | 调 `--top-k`(每 query 给更多候选)重跑 |
|
|
96
100
|
| 某段有空槽 / 漏检 / 想补个特定意象 | `gtrk matrix search "<英文长句场景描述>" --project <目录> --json` 单条 ad-hoc 补检,把中意的候选记下、在 opencut 里手动铺进那段 |
|
package/skills/gtrk-mg/SKILL.md
CHANGED
|
@@ -49,6 +49,7 @@ gtrk mg status --project "<split产物目录>" --json
|
|
|
49
49
|
### 2. 逐槽位产颗粒(驱动栏目 MG 生产 skill)
|
|
50
50
|
|
|
51
51
|
对每个「缺 HTML」的槽位:把它的 `composition_id` + `handoff`(`theme` / `duration_hint` / `category`:`overlay` 透明叠加·不挡主体 / `fullscreen` 不透明满屏)+ beat 语义上下文,**交给第「业务分离」节解析出的栏目 MG 生产 skill 产一颗 html-particle**。产完把颗粒**存到 `<产物目录>/mg/<composition_id>.html`**(`gtrk mg` 就从这里读)。
|
|
52
|
+
- **产前先抽帧看底轨(`overlay` 槽位必做,别只凭派单盲产)**:颗粒最终要叠在真实画面上,构图必须因势象形。对该槽位挂点用本地 ffmpeg 抽 1-2 帧(`ffmpeg -ss <track_st秒> -i <底轨视频> -frames:v 1 <out.jpg>`,成本≈0)看清三件事——①真人主体/人脸的位置(颗粒 bbox 避开,呼应栏目「不盖说话人」铁律);②该处是否已有烧入包装或 AI 画面(有 → 别铺这颗,报用户裁决——二次过毛片高发);③画面明暗基调(深底/浅底影响衬底与配色取舍)。把这些**连同抽帧图一起附进产片订单**交给栏目生产 skill;`fullscreen` 槽位可免(满屏不透明无避让问题)。
|
|
52
53
|
- **产片订单必须附带坑位硬约束(gsap-emit v1 铁律⑦,2026-07-24 主理人硬性规定)**:颗粒 GSAP 时间线总长 ≥ **坑位时长**(该槽位 `track_ed − track_st` 包络,**不是** `duration_hint`)+ 0.3s 余量;`duration_hint` 只是动画主叙事节奏参考。主叙事播完后颗粒必须**定格保持或有限循环驻留到坑位末尾**——坑位内任意一帧核心内容都可见;**禁全局渐隐/整体退场/清空画面**(渐隐与剪辑层转场冲突,淡出由剪辑层决定);定格不动是合法终态。违反的观感 = 动画一过完颗粒突兀消失。
|
|
53
54
|
- **`-aux<n>` 叠层颗粒同样处理**:`gtrk split` 若在某 beat 的 `aux_layers` 派了 `overlay`,会派生 `<beat>-aux<n>` 进 `dispatch.mg`(多为 B-roll 底轨之上叠透明概念图解)——照样产、照样存到对应 `composition_id.html`。
|
|
54
55
|
- **category 决定叠法**:`overlay` 颗粒背景透明、盖在 B-roll 上不挡主体;`fullscreen` 不透明满屏。最终透明度由颗粒 HTML 根 `background` 反推的 `opaque` 定,生产 skill 要让二者自洽(lint 会查)。
|
|
@@ -41,6 +41,8 @@ gtrk split --project "<oralcut产物目录>" --json
|
|
|
41
41
|
|
|
42
42
|
按下面「拆分方法论」把连续的 utterance 分成若干 beat。一个 beat 覆盖**连续的 id 区间**(可跨多句),但**不能跨前后两处拼接**。beat 之间允许留空隙(未覆盖段默认 A_ROLL 底轨直出),**不许重叠**。
|
|
43
43
|
|
|
44
|
+
**抽帧确认底轨画面(凡毛片可能已含视觉包装时必做,勿只凭文稿派单)**:view.json 只给你文字,看不见画面——但毛片未必是素的。**二次过 / 补 ov 场景**(毛片本身是已烧入 MG、AI 再现或其他包装的成片再加工)尤其危险:只凭时码派 overlay 会撞上已有包装叠罗汉。姿势:对候选挂点(尤其打算派 `overlay` 的句区间)用本地 ffmpeg 按 `track_st` 抽 1 帧看一眼(`ffmpeg -ss <秒> -i <毛片> -frames:v 1 <out.jpg>`,成本≈0),确认该处画面性质——**裸口播 → 正常派;已有包装 → 避开该区间或另选挂点;拿不准 → 列给用户裁决**。首次过全素毛片可免;一旦毛片来历含「粗剪成片 / 待 ov 补充 / 二次过」字样,此步为硬门。抽帧顺带看清真人主体位置与画面明暗,写进 beat 的语义上下文,供下游产颗粒时构图避让。
|
|
45
|
+
|
|
44
46
|
### 3. 写拆分稿 JSON
|
|
45
47
|
|
|
46
48
|
按 `references/field-schema.md` 的契约写。骨架:
|