@gitruck/cli 1.3.6 → 1.3.8
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 +23 -0
- package/CHANGELOG.md +32 -0
- package/README.en.md +1 -0
- package/README.md +1 -0
- package/assets/composite-kit/kit.css +2 -2
- package/assets/composite-kit/kit.js +2 -0
- package/assets/composite-kit/stage.html +14 -2
- package/assets/readme/wordmark.svg +6 -6
- package/contracts/README.md +16 -16
- package/contracts/gsap-emit-v1.md +163 -163
- package/contracts/handoff-contracts.json +10 -10
- package/dist/index.js +352 -313
- package/docs/development.en.md +47 -47
- package/docs/development.md +47 -47
- package/docs/reference.en.md +71 -13
- package/docs/reference.md +74 -15
- package/package.json +2 -2
- package/skills/gtrk-ai-drama/SKILL.md +10 -10
- package/skills/gtrk-ai-drama/references/sparse-key-scenes.md +38 -38
- package/skills/gtrk-ai-drama/scripts/validate-storyboard.mjs +119 -119
- package/skills/gtrk-composite/SKILL.md +28 -5
- package/skills/gtrk-cover/SKILL.md +3 -3
- package/skills/gtrk-cover/references/layout-recipes.md +337 -337
- package/skills/gtrk-cover/references/prompt-templates.md +119 -119
- package/skills/gtrk-food-recap/SKILL.md +2 -2
- package/skills/gtrk-live-slicing/SKILL.md +10 -4
- package/skills/gtrk-long2short/SKILL.md +3 -0
- package/skills/gtrk-matrix/SKILL.md +5 -0
- package/skills/gtrk-mg/SKILL.md +398 -391
- package/skills/gtrk-music-visualizer/SKILL.md +3 -3
- package/skills/gtrk-narration/SKILL.md +6 -2
- package/skills/gtrk-ref-reading/SKILL.md +145 -0
- package/skills/gtrk-ref-reading/references/close-reading-checklist.md +47 -0
- package/skills/gtrk-ref-reading/references/landing-templates.md +95 -0
- package/skills/gtrk-splitter/SKILL.md +298 -298
- package/skills/gtrk-splitter/references/example-visual-split.json +645 -645
- package/skills/gtrk-splitter/references/example-visual-split.md +439 -439
- package/skills/gtrk-splitter/references/field-schema.md +171 -171
- package/skills/gtrk-style-maker/SKILL.md +7 -0
- package/skills/gtrk-style-maker/references/ammo.md +16 -16
- package/skills/gtrk-style-maker/references/contracts-ref.md +9 -9
- package/skills/gtrk-style-maker/references/seeds/README.md +4 -4
- package/skills/gtrk-style-maker/references/seeds/seed-ecom-home-goods.md +37 -37
- package/skills/gtrk-style-maker/references/seeds/seed-psych-humanities.md +35 -35
- package/skills/gtrk-talking-head/SKILL.md +11 -9
- package/skills/gtrk-tools/SKILL.md +1 -1
- package/skills/gtrk-transcript/SKILL.md +4 -4
- package/skills/gtrk-transcript/agents/openai.yaml +4 -4
- package/skills/gtrk-travel-recap/SKILL.md +8 -0
- package/skills/gtrk-vlog-docu/SKILL.md +8 -0
- package/skills/gtrk-voiceover/SKILL.md +7 -2
|
@@ -1,119 +1,119 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { readFile } from "node:fs/promises";
|
|
4
|
-
import { basename } from "node:path";
|
|
5
|
-
|
|
6
|
-
const files = process.argv.slice(2);
|
|
7
|
-
if (files.length === 0) {
|
|
8
|
-
console.error("用法:bun scripts/validate-storyboard.mjs <分镜稿.md> [...]");
|
|
9
|
-
process.exit(2);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function firstMatchIndex(text, re) {
|
|
13
|
-
const m = re.exec(text);
|
|
14
|
-
return m ? m.index : -1;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function validate(md, file) {
|
|
18
|
-
const errors = [];
|
|
19
|
-
const beat = md.match(/[((]\s*beat\s+([A-Za-z0-9_-]+)\s*[))]/i)?.[1] ?? "";
|
|
20
|
-
if (!beat) errors.push("H1 标题缺少「(beat BXX)」");
|
|
21
|
-
|
|
22
|
-
const meta = md.match(/^[-*]\s*区间总时长\s*[::]\s*track_st\s+(-?\d+(?:\.\d+)?)\s*→\s*track_ed\s+(-?\d+(?:\.\d+)?)\s*≈\s*(\d+(?:\.\d+)?)\s*秒\s*$/m);
|
|
23
|
-
if (!meta) errors.push("缺少单行元信息「区间总时长:track_st … → track_ed … ≈ … 秒」");
|
|
24
|
-
|
|
25
|
-
const cnStart = firstMatchIndex(md, /^##\s*一、中文稿.*$/m);
|
|
26
|
-
const enStart = firstMatchIndex(md, /^##\s*二、English Storyboard.*$/mi);
|
|
27
|
-
if (cnStart < 0) errors.push("缺少「## 一、中文稿…」区块");
|
|
28
|
-
if (enStart < 0) errors.push("缺少「## 二、English Storyboard…」区块");
|
|
29
|
-
if (cnStart >= 0 && enStart >= 0 && cnStart >= enStart) errors.push("中文稿必须位于 English Storyboard 之前");
|
|
30
|
-
|
|
31
|
-
const cn = cnStart >= 0 ? md.slice(cnStart, enStart >= 0 ? enStart : md.length) : "";
|
|
32
|
-
const en = enStart >= 0 ? md.slice(enStart) : "";
|
|
33
|
-
for (const n of ["①", "②", "③", "④", "⑤"]) {
|
|
34
|
-
if (!new RegExp(`^#{1,6}\\s*${n}`, "m").test(cn)) errors.push(`中文稿缺少 ${n} 区块`);
|
|
35
|
-
}
|
|
36
|
-
for (const n of ["①", "②", "③", "④"]) {
|
|
37
|
-
if (!new RegExp(`^#{1,6}\\s*${n}`, "m").test(en)) errors.push(`English Storyboard 缺少 ${n} 区块`);
|
|
38
|
-
}
|
|
39
|
-
if (!/^>\s*禁忌\s*[::]/m.test(cn)) errors.push("① 视觉基调缺少独立的「> 禁忌:…」行");
|
|
40
|
-
if (!/^>\s*Avoid\s*[::]/mi.test(en)) errors.push("English ① Style Lock 缺少独立的「> Avoid: …」行");
|
|
41
|
-
|
|
42
|
-
const cnCharBlock = cn.match(/^#{1,6}\s*③[^\n]*\n([\s\S]*?)(?=^#{1,6}\s*④)/m)?.[1] ?? "";
|
|
43
|
-
const enCharBlock = en.match(/^#{1,6}\s*③[^\n]*\n([\s\S]*?)(?=^#{1,6}\s*④)/m)?.[1] ?? "";
|
|
44
|
-
const cnChars = [...cnCharBlock.matchAll(/^####\s+(.+?)\s*$/gm)].map((m) => m[1].trim());
|
|
45
|
-
const enChars = [...enCharBlock.matchAll(/^####\s+(.+?)\s*$/gm)].map((m) => m[1].trim());
|
|
46
|
-
if (cnChars.length === 0) errors.push("③ 角色描述中没有「#### 角色名(备注)」标题");
|
|
47
|
-
if (enChars.length === 0) errors.push("English ③ Characters 中没有角色四级标题");
|
|
48
|
-
|
|
49
|
-
const cnShotRe = /^####\s*分镜\s*(\d+)\s*[·•..]\s*(.*?)\s*[||]\s*建议\s*≈?\s*(\d+(?:\.\d+)?)\s*s\b.*[||]\s*角色\s*[::]\s*([^||\n]+)/gim;
|
|
50
|
-
const enShotRe = /^####\s*Shot\s*(\d+)\s*[·•..]\s*(.*?)\s*[||]\s*≈?\s*(\d+(?:\.\d+)?)\s*s\b.*[||]\s*cast\s*[::]\s*([^||\n]+)/gim;
|
|
51
|
-
const cnShots = [...cn.matchAll(cnShotRe)].map((m) => Number(m[1]));
|
|
52
|
-
const enShots = [...en.matchAll(enShotRe)].map((m) => Number(m[1]));
|
|
53
|
-
if (cnShots.length === 0) errors.push("④ 中没有可解析的「#### 分镜 01 · … |建议 ≈Xs … |角色:…」标题");
|
|
54
|
-
if (enShots.length === 0) errors.push("English ④ 中没有可解析的「#### Shot 01 · … |≈Xs … |cast: …」标题");
|
|
55
|
-
if (cnShots.length > 0 && enShots.length > 0 && JSON.stringify(cnShots) !== JSON.stringify(enShots)) {
|
|
56
|
-
errors.push(`中英文镜头序号不一致:CN=${cnShots.join(",")},EN=${enShots.join(",")}`);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const cnShotBlock = cn.match(/^#{1,6}\s*④[^\n]*\n([\s\S]*?)(?=^#{1,6}\s*⑤)/m)?.[1] ?? "";
|
|
60
|
-
const enShotBlock = en.match(/^#{1,6}\s*④[^\n]*\n([\s\S]*)/m)?.[1] ?? "";
|
|
61
|
-
const cnHeads = [...cnShotBlock.matchAll(/^####\s+(.+?)\s*$/gm)];
|
|
62
|
-
const enHeads = [...enShotBlock.matchAll(/^####\s+(.+?)\s*$/gm)];
|
|
63
|
-
const extraCnHeads = cnHeads.filter((m) => !/^分镜\s*\d+/i.test(m[1]));
|
|
64
|
-
const extraEnHeads = enHeads.filter((m) => !/^Shot\s*\d+/i.test(m[1]));
|
|
65
|
-
if (extraCnHeads.length > 0) errors.push(`④ 内存在非分镜四级标题:${extraCnHeads.map((m) => m[1]).join("、")};请把蒙太奇名放进「段」字段`);
|
|
66
|
-
if (extraEnHeads.length > 0) errors.push(`English ④ 内存在非 Shot 四级标题:${extraEnHeads.map((m) => m[1]).join("、")};请把 montage 名放进 segment 字段`);
|
|
67
|
-
|
|
68
|
-
function checkPrefixes(block, heads, label) {
|
|
69
|
-
for (let i = 0; i < heads.length; i += 1) {
|
|
70
|
-
const start = (heads[i].index ?? 0) + heads[i][0].length;
|
|
71
|
-
const end = i + 1 < heads.length ? heads[i + 1].index : block.length;
|
|
72
|
-
const firstBodyLine = block.slice(start, end).split(/\r?\n/).map((line) => line.trim()).find(Boolean) ?? "";
|
|
73
|
-
if (!firstBodyLine.startsWith("〔") || !firstBodyLine.includes("〕")) {
|
|
74
|
-
errors.push(`${label} ${heads[i][1].match(/\d+/)?.[0] ?? "?"} 的首个正文不是「〔视觉基调〕」前缀`);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
checkPrefixes(cnShotBlock, cnHeads.filter((m) => /^分镜\s*\d+/i.test(m[1])), "中文分镜");
|
|
79
|
-
checkPrefixes(enShotBlock, enHeads.filter((m) => /^Shot\s*\d+/i.test(m[1])), "English Shot");
|
|
80
|
-
|
|
81
|
-
if (/^\s*[-*]\s*(男性|女性|男主|女主|父亲|母亲|角色)\s*[::]/m.test(cnCharBlock)) {
|
|
82
|
-
errors.push("③ 角色仍使用项目符号;请改成四级标题并把描述另起段落");
|
|
83
|
-
}
|
|
84
|
-
if (/^\s*\*\*(镜头|分镜|Shot)\s*\d+/mi.test(md)) {
|
|
85
|
-
errors.push("镜头仍使用粗体行;请改成「#### 分镜…」/「#### Shot…」四级标题");
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (meta) {
|
|
89
|
-
const st = Number(meta[1]);
|
|
90
|
-
const ed = Number(meta[2]);
|
|
91
|
-
const total = Number(meta[3]);
|
|
92
|
-
if (!(ed > st)) errors.push("track_ed 必须大于 track_st");
|
|
93
|
-
if (Math.abs((ed - st) - total) > 0.11) errors.push(`区间总时长与 track_ed-track_st 不一致:${total} vs ${(ed - st).toFixed(3)}`);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return {
|
|
97
|
-
ok: errors.length === 0,
|
|
98
|
-
file: basename(file),
|
|
99
|
-
beatId: beat || null,
|
|
100
|
-
characters: cnChars.length,
|
|
101
|
-
shots: cnShots.length,
|
|
102
|
-
errors,
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
let failed = false;
|
|
107
|
-
for (const file of files) {
|
|
108
|
-
try {
|
|
109
|
-
const md = await readFile(file, "utf8");
|
|
110
|
-
const result = validate(md, file);
|
|
111
|
-
console.log(JSON.stringify(result));
|
|
112
|
-
failed ||= !result.ok;
|
|
113
|
-
} catch (error) {
|
|
114
|
-
failed = true;
|
|
115
|
-
console.log(JSON.stringify({ ok: false, file: basename(file), errors: [String(error?.message ?? error)] }));
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
process.exit(failed ? 1 : 0);
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { basename } from "node:path";
|
|
5
|
+
|
|
6
|
+
const files = process.argv.slice(2);
|
|
7
|
+
if (files.length === 0) {
|
|
8
|
+
console.error("用法:bun scripts/validate-storyboard.mjs <分镜稿.md> [...]");
|
|
9
|
+
process.exit(2);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function firstMatchIndex(text, re) {
|
|
13
|
+
const m = re.exec(text);
|
|
14
|
+
return m ? m.index : -1;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function validate(md, file) {
|
|
18
|
+
const errors = [];
|
|
19
|
+
const beat = md.match(/[((]\s*beat\s+([A-Za-z0-9_-]+)\s*[))]/i)?.[1] ?? "";
|
|
20
|
+
if (!beat) errors.push("H1 标题缺少「(beat BXX)」");
|
|
21
|
+
|
|
22
|
+
const meta = md.match(/^[-*]\s*区间总时长\s*[::]\s*track_st\s+(-?\d+(?:\.\d+)?)\s*→\s*track_ed\s+(-?\d+(?:\.\d+)?)\s*≈\s*(\d+(?:\.\d+)?)\s*秒\s*$/m);
|
|
23
|
+
if (!meta) errors.push("缺少单行元信息「区间总时长:track_st … → track_ed … ≈ … 秒」");
|
|
24
|
+
|
|
25
|
+
const cnStart = firstMatchIndex(md, /^##\s*一、中文稿.*$/m);
|
|
26
|
+
const enStart = firstMatchIndex(md, /^##\s*二、English Storyboard.*$/mi);
|
|
27
|
+
if (cnStart < 0) errors.push("缺少「## 一、中文稿…」区块");
|
|
28
|
+
if (enStart < 0) errors.push("缺少「## 二、English Storyboard…」区块");
|
|
29
|
+
if (cnStart >= 0 && enStart >= 0 && cnStart >= enStart) errors.push("中文稿必须位于 English Storyboard 之前");
|
|
30
|
+
|
|
31
|
+
const cn = cnStart >= 0 ? md.slice(cnStart, enStart >= 0 ? enStart : md.length) : "";
|
|
32
|
+
const en = enStart >= 0 ? md.slice(enStart) : "";
|
|
33
|
+
for (const n of ["①", "②", "③", "④", "⑤"]) {
|
|
34
|
+
if (!new RegExp(`^#{1,6}\\s*${n}`, "m").test(cn)) errors.push(`中文稿缺少 ${n} 区块`);
|
|
35
|
+
}
|
|
36
|
+
for (const n of ["①", "②", "③", "④"]) {
|
|
37
|
+
if (!new RegExp(`^#{1,6}\\s*${n}`, "m").test(en)) errors.push(`English Storyboard 缺少 ${n} 区块`);
|
|
38
|
+
}
|
|
39
|
+
if (!/^>\s*禁忌\s*[::]/m.test(cn)) errors.push("① 视觉基调缺少独立的「> 禁忌:…」行");
|
|
40
|
+
if (!/^>\s*Avoid\s*[::]/mi.test(en)) errors.push("English ① Style Lock 缺少独立的「> Avoid: …」行");
|
|
41
|
+
|
|
42
|
+
const cnCharBlock = cn.match(/^#{1,6}\s*③[^\n]*\n([\s\S]*?)(?=^#{1,6}\s*④)/m)?.[1] ?? "";
|
|
43
|
+
const enCharBlock = en.match(/^#{1,6}\s*③[^\n]*\n([\s\S]*?)(?=^#{1,6}\s*④)/m)?.[1] ?? "";
|
|
44
|
+
const cnChars = [...cnCharBlock.matchAll(/^####\s+(.+?)\s*$/gm)].map((m) => m[1].trim());
|
|
45
|
+
const enChars = [...enCharBlock.matchAll(/^####\s+(.+?)\s*$/gm)].map((m) => m[1].trim());
|
|
46
|
+
if (cnChars.length === 0) errors.push("③ 角色描述中没有「#### 角色名(备注)」标题");
|
|
47
|
+
if (enChars.length === 0) errors.push("English ③ Characters 中没有角色四级标题");
|
|
48
|
+
|
|
49
|
+
const cnShotRe = /^####\s*分镜\s*(\d+)\s*[·•..]\s*(.*?)\s*[||]\s*建议\s*≈?\s*(\d+(?:\.\d+)?)\s*s\b.*[||]\s*角色\s*[::]\s*([^||\n]+)/gim;
|
|
50
|
+
const enShotRe = /^####\s*Shot\s*(\d+)\s*[·•..]\s*(.*?)\s*[||]\s*≈?\s*(\d+(?:\.\d+)?)\s*s\b.*[||]\s*cast\s*[::]\s*([^||\n]+)/gim;
|
|
51
|
+
const cnShots = [...cn.matchAll(cnShotRe)].map((m) => Number(m[1]));
|
|
52
|
+
const enShots = [...en.matchAll(enShotRe)].map((m) => Number(m[1]));
|
|
53
|
+
if (cnShots.length === 0) errors.push("④ 中没有可解析的「#### 分镜 01 · … |建议 ≈Xs … |角色:…」标题");
|
|
54
|
+
if (enShots.length === 0) errors.push("English ④ 中没有可解析的「#### Shot 01 · … |≈Xs … |cast: …」标题");
|
|
55
|
+
if (cnShots.length > 0 && enShots.length > 0 && JSON.stringify(cnShots) !== JSON.stringify(enShots)) {
|
|
56
|
+
errors.push(`中英文镜头序号不一致:CN=${cnShots.join(",")},EN=${enShots.join(",")}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const cnShotBlock = cn.match(/^#{1,6}\s*④[^\n]*\n([\s\S]*?)(?=^#{1,6}\s*⑤)/m)?.[1] ?? "";
|
|
60
|
+
const enShotBlock = en.match(/^#{1,6}\s*④[^\n]*\n([\s\S]*)/m)?.[1] ?? "";
|
|
61
|
+
const cnHeads = [...cnShotBlock.matchAll(/^####\s+(.+?)\s*$/gm)];
|
|
62
|
+
const enHeads = [...enShotBlock.matchAll(/^####\s+(.+?)\s*$/gm)];
|
|
63
|
+
const extraCnHeads = cnHeads.filter((m) => !/^分镜\s*\d+/i.test(m[1]));
|
|
64
|
+
const extraEnHeads = enHeads.filter((m) => !/^Shot\s*\d+/i.test(m[1]));
|
|
65
|
+
if (extraCnHeads.length > 0) errors.push(`④ 内存在非分镜四级标题:${extraCnHeads.map((m) => m[1]).join("、")};请把蒙太奇名放进「段」字段`);
|
|
66
|
+
if (extraEnHeads.length > 0) errors.push(`English ④ 内存在非 Shot 四级标题:${extraEnHeads.map((m) => m[1]).join("、")};请把 montage 名放进 segment 字段`);
|
|
67
|
+
|
|
68
|
+
function checkPrefixes(block, heads, label) {
|
|
69
|
+
for (let i = 0; i < heads.length; i += 1) {
|
|
70
|
+
const start = (heads[i].index ?? 0) + heads[i][0].length;
|
|
71
|
+
const end = i + 1 < heads.length ? heads[i + 1].index : block.length;
|
|
72
|
+
const firstBodyLine = block.slice(start, end).split(/\r?\n/).map((line) => line.trim()).find(Boolean) ?? "";
|
|
73
|
+
if (!firstBodyLine.startsWith("〔") || !firstBodyLine.includes("〕")) {
|
|
74
|
+
errors.push(`${label} ${heads[i][1].match(/\d+/)?.[0] ?? "?"} 的首个正文不是「〔视觉基调〕」前缀`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
checkPrefixes(cnShotBlock, cnHeads.filter((m) => /^分镜\s*\d+/i.test(m[1])), "中文分镜");
|
|
79
|
+
checkPrefixes(enShotBlock, enHeads.filter((m) => /^Shot\s*\d+/i.test(m[1])), "English Shot");
|
|
80
|
+
|
|
81
|
+
if (/^\s*[-*]\s*(男性|女性|男主|女主|父亲|母亲|角色)\s*[::]/m.test(cnCharBlock)) {
|
|
82
|
+
errors.push("③ 角色仍使用项目符号;请改成四级标题并把描述另起段落");
|
|
83
|
+
}
|
|
84
|
+
if (/^\s*\*\*(镜头|分镜|Shot)\s*\d+/mi.test(md)) {
|
|
85
|
+
errors.push("镜头仍使用粗体行;请改成「#### 分镜…」/「#### Shot…」四级标题");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (meta) {
|
|
89
|
+
const st = Number(meta[1]);
|
|
90
|
+
const ed = Number(meta[2]);
|
|
91
|
+
const total = Number(meta[3]);
|
|
92
|
+
if (!(ed > st)) errors.push("track_ed 必须大于 track_st");
|
|
93
|
+
if (Math.abs((ed - st) - total) > 0.11) errors.push(`区间总时长与 track_ed-track_st 不一致:${total} vs ${(ed - st).toFixed(3)}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
ok: errors.length === 0,
|
|
98
|
+
file: basename(file),
|
|
99
|
+
beatId: beat || null,
|
|
100
|
+
characters: cnChars.length,
|
|
101
|
+
shots: cnShots.length,
|
|
102
|
+
errors,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let failed = false;
|
|
107
|
+
for (const file of files) {
|
|
108
|
+
try {
|
|
109
|
+
const md = await readFile(file, "utf8");
|
|
110
|
+
const result = validate(md, file);
|
|
111
|
+
console.log(JSON.stringify(result));
|
|
112
|
+
failed ||= !result.ok;
|
|
113
|
+
} catch (error) {
|
|
114
|
+
failed = true;
|
|
115
|
+
console.log(JSON.stringify({ ok: false, file: basename(file), errors: [String(error?.message ?? error)] }));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
process.exit(failed ? 1 : 0);
|
|
@@ -66,7 +66,7 @@ gtrk composite scaffold --project <工程> # 为已采纳窗口落场
|
|
|
66
66
|
看联系表(每格左上是源时间码)、切点列表、黑边实测。入点与在场时长 MUST 落在一个镜头内(规则 5),`data-crop` 按黑边实测取(规则 3)。工程里还没登记的素材写进 `<工程>/composite/materials.json`(`[{ "id", "path", "duration" }]`),铺轨时一并登记。
|
|
67
67
|
3. **写场景**(格式见「场景写法速查」)。
|
|
68
68
|
4. **构建**:`gtrk composite build --project <工程> [--only cp-w03]`——按 infra 规则解析(帧格窗口、轨身份、锚点、字幕输入、`inputs_hash`)并校验,产 `<工程>/gtrk/assets/composite/<id>.html`。回执里每颗应为 `fresh`。
|
|
69
|
-
5. **机检**:`gtrk composite lint --project <工程> --stills 26,86.6`——静态校验 +
|
|
69
|
+
5. **机检**:`gtrk composite lint --project <工程> --stills 26,86.6`——静态校验 + 逐帧排版(下层画面的前景描边压上层画面、前景线条穿过画面、画面越出舞台、容器里的元素写了偏移却没定位),顺带出静帧。**不过就改场景重跑,过了才往下走。**
|
|
70
70
|
- 退出码 3 = 本机找不到 Chrome / Edge,**排版没机检**:照实告诉用户,用静帧(或客户端预览)人眼逐颗查穿模,MUST NOT 当成通过。
|
|
71
71
|
|
|
72
72
|
### 检查点②:看静帧
|
|
@@ -93,12 +93,14 @@ gtrk composite bake --project <工程> # 全部合成;--only cp-w03
|
|
|
93
93
|
- **某颗失败**(退出码 1):其余照常。回执给出原因(常见:素材丢失、窗口为零、未解析——重跑 `build`);修好再烤那一颗。不修的话出片时该段按粗剪出画,MUST 照实告诉用户。
|
|
94
94
|
- **出片时**:成功的那段以烘焙视频出画;**整段落在合成窗口内的 MG / OV 不再预渲、不计费**(被合成接管了)。
|
|
95
95
|
- 帧纯度(`lint` 的 `impure-frame`)不过的颗粒,烘焙段首帧会与连续播放不同——**lint 过了再烤**。
|
|
96
|
+
- **产物尺寸 = 舞台 = 工程画布**:竖屏 1080×1920 与横屏 1920×1080 像素数相同、耗时相同;4K 画布像素是 1080p 的 4 倍,烘焙明显更慢,开烤前照实告诉用户。
|
|
96
97
|
|
|
97
98
|
## 编排规则(每条都是测试工程走查时真犯过的)
|
|
98
99
|
|
|
99
100
|
1. **画面数由内容定,不照搬粗剪的轨结构。** 逐句看台词:一句话说到什么就换成什么;没有具体可看的,让讲者本人占画框或用纯排版;**只有内容本身是两个以上的东西才同时放多画**——关系(「你再指点妈妈」→ 女儿 / 妈妈并排)、对照(「她会化浓妆」VS「妈妈没化过妆」)、并列(「打耳钉 / 抽烟 / 喝酒」三张卡逐词亮起)。
|
|
100
101
|
- ❌ 反例:粗剪在这段有两条 B-roll 候选轨,就放一个大框 + 一个小框,两条轨各占一个——画面数由轨数决定,跟内容无关。
|
|
101
102
|
- 画面多用 `own` 槽位按内容另挑入点(粗剪轨上的片段只是候选之一);用 `ref-lane` 的前提是那条轨在这段的时序恰好就是你要的。
|
|
103
|
+
- **没有讲者的工程**(解说、配音、旅拍等主画面不是真人口播):只用 B-roll 画面与排版;没有具体可看的段交给纯排版或不做,MUST NOT 造讲者画框、MUST NOT 拿空白圆窗或占位图充当讲者。
|
|
102
104
|
2. **任一时刻画面互不重叠。** 客户端先画全部画面、再画前景层,**下层画面的前景描边一定压在上层画面上**。同一位置交替换画面时,**等前一张的框到位、两框重合后再换**(`seq` 已按此写);版式切换(一画 → 并排)时,先让旧画面缩到位,新画面再进来。讲者不在同一时刻出现两次(大框里是讲者时不放讲者圆窗)。
|
|
103
105
|
- ❌ 反例:讲者圆窗与小画面叠在大画框的边线上,边线横穿它们;分叉曲线从一张画中画中间穿过去。
|
|
104
106
|
3. **裁掉画面里的黑边与烧录字幕。** 电影源常带信箱黑边,综艺源常有烧录字幕与人名条;按 `shots` 的实测写 `data-crop="上,右,下,左"`(比例)。
|
|
@@ -108,15 +110,21 @@ gtrk composite bake --project <工程> # 全部合成;--only cp-w03
|
|
|
108
110
|
5. **选片不跨切点。** `own` 的取用段 `[入点, 入点 + 在场时长)` MUST 落在一个镜头内;镜头不够长就接受最后一帧定格或换一个镜头,MUST NOT 拉伸或变速。
|
|
109
111
|
6. **字幕:接管就自己排。** 场景声明 `takeover: ["subtitles","overlay"]` 后,窗口内原字幕与 MG 叠层不再显示,字幕由场景用 `caption()` 或对话气泡排出;屏上排版已经写着同一句话时可以在那段隐藏字幕。
|
|
110
112
|
7. **先求准,再求满。** AI 判定模式的密度上限是对成片吵闹度的约束;整片铺满时也要让「没有具体可看的段」安静(讲者 + 少量排版),别为了铺满每段都堆画面。
|
|
113
|
+
8. **舞台随工程画布,竖屏 / 方形按自己的版式写。** `scaffold` 按工程画布写舞台(规格里的 `stage`),场景坐标一律是这块舞台的像素;横屏工程的版式不能拿到竖屏工程里用(`build` 会报「场景舞台 A,工程画布 B」)。竖屏与方形舞台:
|
|
114
|
+
- 多画并置优先**上下分区**(关系、对照都上下摆),MUST NOT 把横屏的左右并排硬塞进窄舞台;
|
|
115
|
+
- 字幕与关键信息避开上下边缘与右侧按钮区(平台界面会盖住),字幕放画面中下部;
|
|
116
|
+
- 人脸构图规则(规则 4)照旧;机检规则对任何舞台同样适用。
|
|
117
|
+
- ❌ 反例:竖屏里左右各放一张 540 宽的画面,人脸被挤成一条缝,右边那张还被点赞按钮压住。
|
|
111
118
|
|
|
112
119
|
## 场景写法速查
|
|
113
120
|
|
|
114
|
-
|
|
121
|
+
场景文件四段(下例舞台 1920×1080;竖屏工程 `stage` 是 `[1080, 1920]`,坐标按它写):
|
|
115
122
|
|
|
116
123
|
```html
|
|
117
124
|
<script type="application/x-cp-spec">
|
|
118
125
|
{
|
|
119
126
|
"id": "cp-w03",
|
|
127
|
+
"stage": [1920, 1080], // 舞台 = 工程画布(scaffold 写好,别手改)
|
|
120
128
|
"window": [77.483, 90.85], // 时间线秒;build 会吸附到帧格
|
|
121
129
|
"takeover": ["subtitles", "overlay"],
|
|
122
130
|
"slots": {
|
|
@@ -147,10 +155,11 @@ CP.render = (t) => {
|
|
|
147
155
|
```
|
|
148
156
|
|
|
149
157
|
- **槽位绑定**:`ref-lane {track}`(某条轨在该时刻的画面,讲者常用);`ref {clip_id}`(某个片段,窗口外定格首末帧);`own {material, src_in_ms, dur_t | dur_f, starts_at: 锚点名 | starts_at_t: 时间线秒}`(自选素材的一段;之前首帧、之后末帧定格);`text`(字幕文本输入,无几何)。
|
|
150
|
-
- **锚点**:`[句 id, 关键词, 第几处(0 起)]`,取关键词首字在时间线上的时刻;场景里 `A("名字")`
|
|
158
|
+
- **锚点**:`[句 id, 关键词, 第几处(0 起)]`,取关键词首字在时间线上的时刻;场景里 `A("名字")` 得局部秒。关键词所在的字被剪掉时取其后第一个保留的字。匹配时关键词与句子都忽略空格、标点与大小写,英文按整词(`["u0001", "cold brew"]` 直接写;`"tea"` 不会命中 `teas`)。
|
|
151
159
|
- **槽位元素**:`data-slot` 一槽一个;`data-z` 叠放次序;`data-fit` cover / contain;`data-shape` rect / roundrect / circle;`data-crop` 上右下左比例;`data-filter` 只收白名单滤镜。
|
|
152
160
|
- **装饰跟随**:`data-for="<槽位>"` 的元素与槽位同框(`data-pad` 外扩);`.shadow` 在下(背景层),`.frame` / `.ring` 加 `data-layer="front"` 在上——**只给最上层的画面加前景描边**(规则 2)。
|
|
153
161
|
- **分层**:缺省是背景层(画在视频之下);`data-layer="front"` 的子树在视频之上(文字、标签、描边、线条)。
|
|
162
|
+
- **定位**:工具箱只给**舞台的直接子元素**绝对定位(`#stage > *`)。放进容器里的子元素(如 `<div id="grid">` 下的一排徽章)不会自动定位,`left` / `top` 不生效、按文档流挤成一列(lint 报 `unpositioned-offset`)——容器内要摆位置的子元素自己写 `position:absolute`,或加工具箱的 `.abs` 类;坐标相对容器(容器自己要写宽高)。
|
|
154
163
|
- **工具箱 `CP`**:`prog(t, 起, 时长, ease)` 进度;`span` 进出;`at(id, [x,y,w,h], 透明度)` 定框;`mix(矩形A, 矩形B, p)` 版式插值;`seq(t, 到点, 下一张到点)` 同一位置逐句换画面的透明度;`place(el, x, y, o)`;`reveal / look / wipe / draw / typeOn`;`caption(el, "subs", t, 高亮词[], 隐藏区间[])`;`cues("subs")` 取字幕条目(局部帧);`fps()`。
|
|
155
164
|
- **常用样式**:`.bed`(底色)/ `.bed.dark`;`.kicker` `.h1` `.h2` `.h3` `.body` `.tag` `.num`;`.ptag` / `.ptag.warm`(画面标签);`.cap` / `.cap.dark`(字幕);`.bubble`(对话气泡);`.stamp`(印章);`.card` `.pill` `.rule`。配色与字体都是变量:`--bg --bg2 --ink --muted --line --accent --accent2 --accent3 --alert --font --font-serif`。
|
|
156
165
|
- **纯函数**:场景脚本 MUST NOT 用 `Date`、`Math.random`、`requestAnimationFrame`、`setInterval`、`setTimeout`(lint 会拦)——同一帧号永远画出同一画面,客户端是逐帧 seek 的。
|
|
@@ -173,11 +182,13 @@ CP.render = (t) => {
|
|
|
173
182
|
| 现象 | 处理 |
|
|
174
183
|
|---|---|
|
|
175
184
|
| build 报「素材 X 未登记」 | 写进 `composite/materials.json`(`id` + `path`),或换工程里已登记的素材 |
|
|
176
|
-
| build 报「锚点落在窗口外 / 找不到关键词」 | 关键词照句子原文抄(看 `context.md
|
|
185
|
+
| build 报「锚点落在窗口外 / 找不到关键词」 | 关键词照句子原文抄(看 `context.md`;空格、标点、大小写不影响,英文按整词),或换一个落在窗口内的词 |
|
|
177
186
|
| build 报「轨 N 在窗口内没有片段」 | 那条轨这段是空的:换轨,或改用 `own` |
|
|
178
187
|
| lint 报 `border-over-slot` | 两张画面重叠且下层有前景描边:错开时序(等前一张到位再换)或去掉下层的前景描边 |
|
|
179
188
|
| lint 报 `stroke-through-slot` | 前景线条穿过画面:挪画面或改线条走向 |
|
|
180
|
-
| lint 报 `slot-out-of-stage` |
|
|
189
|
+
| lint 报 `slot-out-of-stage` | 画面出了舞台:收回到舞台(= 工程画布,竖屏是 1080×1920)内(满屏底图除外) |
|
|
190
|
+
| lint 报 `unpositioned-offset`(容器里的徽章 / 卡片挤成一列、`left` / `top` 不生效) | 它们不是舞台的直接子元素:自己写 `position:absolute` 或加 `.abs`(见场景速查「定位」);本来就想按文档流 / flex 排的,删掉偏移 |
|
|
191
|
+
| build 报「场景舞台 A,工程画布 B」 | 工程画布改过(或拿了别的画幅的场景):按新画布重写场景,`gtrk composite scaffold --force` 重起骨架 |
|
|
181
192
|
| lint / lay 报「需重渲」 | 上游(粗剪 / 素材 / 字幕)改过:重跑 `build`(再 `lint`、`lay`) |
|
|
182
193
|
| lint 退出码 3 | 本机没 Chrome / Edge:排版未机检,用静帧人眼查,照实告诉用户 |
|
|
183
194
|
| lint 报 `impure-frame` | 同一帧号随渲染顺序画出不同画面:多半读了带变换元素的渲染后几何(如对正在缩放的元素用 `getBoundingClientRect` 量宽度)——改读 `offsetWidth` / `offsetLeft` 等不含变换的布局值 |
|
|
@@ -186,9 +197,21 @@ CP.render = (t) => {
|
|
|
186
197
|
| lay 报「没铺:时间线上已有同 id 的片段」 | 用户在客户端改过那颗:要重铺先请他在客户端删掉它 |
|
|
187
198
|
| 客户端 CP 行没出现提案块 | 请用户重新载入工程(外部写入要重载才读进来) |
|
|
188
199
|
|
|
200
|
+
## 被图纸快速成片接力调用
|
|
201
|
+
|
|
202
|
+
成片图纸(口播 / 解说 / 美食 / 配音 / 旅拍 / Vlog;直播切片与长剪短在用户答要时)的快速成片跑到「精剪包装」交棒本 skill 时,按《成片型图纸公约》§三⁵ 一杆到底,**不停**:
|
|
203
|
+
|
|
204
|
+
- **检查点①由你代审**:`plan --mode auto`(缺省密度 25%)之后,逐窗口读 `composite/plan.md`,判断这段是否真值得做,用 `review --approve / --reject` 写回;信号只是起点,不值得的不做。
|
|
205
|
+
- **逐窗口照常写设计说明、选片、场景、构建、机检**;机检不过改场景重跑,**至多三轮**,仍不过就 `review --reject` 该窗口,不阻塞其余窗口与交付。
|
|
206
|
+
- **检查点②并入图纸交付**:铺轨、烘焙之后,各窗口静帧随图纸交付时的那次检查一次给出。
|
|
207
|
+
- **回执**:交付时「精剪包装」一节列三项——做了的(时间、对应句、为什么做)、建议了没做的(超上限 / 不值得 / 机检三轮不过,各带原因)、烘焙结果(失败段按粗剪出画并点名)。
|
|
208
|
+
|
|
209
|
+
用户直接要合成(「看看哪里适合做合成」)或走逐步推进时,照下面「双模式」停。
|
|
210
|
+
|
|
189
211
|
## 双模式
|
|
190
212
|
|
|
191
213
|
| 模式 | 停点 |
|
|
192
214
|
|---|---|
|
|
193
215
|
| 快速成片 | 检查点①(审窗口)必停;全部窗口做完、lint 过后,检查点②一次给静帧,点头后一次铺完 |
|
|
194
216
|
| 逐步推进 | 检查点①;每个窗口的设计说明、静帧各停一次 |
|
|
217
|
+
| 图纸快速成片接力 | 不停:检查点①你代审、检查点②并入图纸交付(见上节) |
|
|
@@ -19,11 +19,11 @@ description: 封面工作台——把一期视频的封面从「设计诊断 +
|
|
|
19
19
|
2. **两阶段不能合并。** 用户还没抽图就端出工作台 = 盲排(不看背景图排字,等于退回指令稿时代);工作台的价值就在「因势象形地就着背景图排字」。阶段一交付后**停下等图**。
|
|
20
20
|
3. **审美决策由本 skill 做,工作台只暴露微调面。** 工作台暴露给用户的只有:图层位置 / 字号 / 文案 / 显隐、显式声明的装饰件开关、T1 全局样式滑杆(描边 / 发光 / 既定字色选项)、**T1 字体与字重档位(字体列表读用户本机已装字体,默认字仍由本 skill 定)**。色板体系、字体选型、布局拓扑、装饰件组合是审美决策——由你按注入的风格资产做出、交付时说明理由;用户想改审美层,**经对话增量改 HTML**(不推倒重来),别让他在工作台里找不存在的控件。
|
|
21
21
|
|
|
22
|
-
> **产物落点纪律(MUST · 全文见随包 `AGENT.md` 同名一节)**:
|
|
23
|
-
> 交付物 MUST NOT 先写 agent 自有目录再拷进工程;中转本身即违规,体积不是豁免理由。
|
|
22
|
+
> **产物落点纪律(MUST · 全文见随包 `AGENT.md` 同名一节)**:
|
|
23
|
+
> 交付物 MUST NOT 先写 agent 自有目录再拷进工程;中转本身即违规,体积不是豁免理由。
|
|
24
24
|
> Prompt 文本与工作台 HTML 只落**用户显式指定目录**,或有工程上下文时落 `<project>/cover/`;
|
|
25
25
|
> **MUST NOT** 把用户媒体(背景图 / 导出的封面 PNG / 任何大文件)复制到 agent 自有工作目录(如用户文档目录下 agent 产品自建的目录、agent 家目录缓存、会话工作区)——压缩背景图时**从原路径读取**,压缩产物直接以 base64 写进工作台 HTML,不留中间副本;
|
|
26
|
-
> 压缩脚本等临时文件一律放系统 temp 且**用完即删**。交付物直接写入工程目录或用户显式指定的路径;引用媒体用原路径。
|
|
26
|
+
> 压缩脚本等临时文件一律放系统 temp 且**用完即删**。交付物直接写入工程目录或用户显式指定的路径;引用媒体用原路径。
|
|
27
27
|
|
|
28
28
|
## 业务分离:栏目封面风格从哪来(关键)
|
|
29
29
|
|