@gitruck/cli 1.3.5 → 1.3.7

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.
Files changed (43) hide show
  1. package/AGENT.md +564 -564
  2. package/CHANGELOG.md +14 -0
  3. package/README.en.md +50 -50
  4. package/README.md +50 -50
  5. package/assets/composite-kit/kit.css +57 -57
  6. package/assets/composite-kit/kit.js +246 -246
  7. package/assets/composite-kit/stage.html +19 -2
  8. package/assets/readme/wordmark.svg +6 -6
  9. package/contracts/README.md +16 -16
  10. package/contracts/gsap-emit-v1.md +163 -163
  11. package/contracts/handoff-contracts.json +10 -10
  12. package/dist/index.js +266 -262
  13. package/docs/development.en.md +47 -47
  14. package/docs/development.md +47 -47
  15. package/docs/reference.en.md +32 -2
  16. package/docs/reference.md +32 -2
  17. package/package.json +79 -79
  18. package/skills/gtrk-ai-drama/SKILL.md +10 -10
  19. package/skills/gtrk-ai-drama/references/sparse-key-scenes.md +38 -38
  20. package/skills/gtrk-ai-drama/scripts/validate-storyboard.mjs +119 -119
  21. package/skills/gtrk-composite/SKILL.md +194 -194
  22. package/skills/gtrk-cover/SKILL.md +3 -3
  23. package/skills/gtrk-cover/references/layout-recipes.md +337 -337
  24. package/skills/gtrk-cover/references/prompt-templates.md +119 -119
  25. package/skills/gtrk-food-recap/SKILL.md +9 -9
  26. package/skills/gtrk-mg/SKILL.md +391 -391
  27. package/skills/gtrk-music-visualizer/SKILL.md +3 -3
  28. package/skills/gtrk-narration/SKILL.md +23 -23
  29. package/skills/gtrk-splitter/SKILL.md +298 -298
  30. package/skills/gtrk-splitter/references/example-visual-split.json +645 -645
  31. package/skills/gtrk-splitter/references/example-visual-split.md +439 -439
  32. package/skills/gtrk-splitter/references/field-schema.md +171 -171
  33. package/skills/gtrk-style-maker/SKILL.md +1 -1
  34. package/skills/gtrk-style-maker/references/ammo.md +16 -16
  35. package/skills/gtrk-style-maker/references/contracts-ref.md +9 -9
  36. package/skills/gtrk-style-maker/references/seeds/README.md +4 -4
  37. package/skills/gtrk-style-maker/references/seeds/seed-ecom-home-goods.md +37 -37
  38. package/skills/gtrk-style-maker/references/seeds/seed-psych-humanities.md +35 -35
  39. package/skills/gtrk-talking-head/SKILL.md +205 -205
  40. package/skills/gtrk-transcript/SKILL.md +4 -4
  41. package/skills/gtrk-transcript/agents/openai.yaml +4 -4
  42. package/skills/gtrk-travel-recap/SKILL.md +51 -51
  43. package/skills/gtrk-voiceover/SKILL.md +2 -2
@@ -1,246 +1,246 @@
1
- /* 合成颗粒工具箱 composite-kit@1(html@1 引擎)——`gtrk composite build` 构建时内联进每颗颗粒,颗粒自包含、不外链。
2
- * 随 @gitruck/cli 分发(assets/composite-kit/);改行为即升版本号(颗粒 <meta name="generator"> 与 CP.VERSION 记着它)。
3
- *
4
- * 场景脚本只写一件事:`CP.render = (t) => { ... }`(t = 局部秒)。其余由这里负责:
5
- * - 宿主协议:hello → meta;init → 帧率 / 锚点 / 图层 / 字幕;apply(frame) → 画这一帧并回报槽位几何;lint → 排版自检;
6
- * - 分层:舞台缺省属 back,`data-layer="front"` 的子树属 front(visibility 继承);
7
- * - 槽位几何:`[data-slot]` 的包围盒 + data-z / data-fit / data-shape / data-radius / data-filter / data-crop(上右下左);
8
- * - 装饰跟随:`data-for="<槽位>"` 的元素与槽位同框(data-pad 外扩),用于边框、阴影、角标;
9
- * - 字幕:接管字幕后由颗粒自己排(`CP.caption`),文字来自 init.texts;
10
- * - 逐句换画面与版式切换:`at` / `mix` / `seq` / `place`;
11
- * - 排版自检 `lint`:下层画面的 front 描边压上层画面、front 线条穿过画面、画面越出舞台。
12
- * 纯函数:同一帧号永远画出同一画面;不用 Date / Math.random / requestAnimationFrame / setInterval。
13
- */
14
- (() => {
15
- "use strict";
16
- const META = JSON.parse(document.querySelector("script[data-gtrk-composite]").textContent);
17
- const W = META.stage.width, H = META.stage.height;
18
- const stage = document.getElementById("stage");
19
- let FPS = (META.resolved && META.resolved.video_rate) || 60;
20
- let ANCHORS = (META.resolved && META.resolved.anchors_f) || {};
21
- let LAYER = "all";
22
- let TEXTS = {};
23
-
24
- const clamp = (x, a = 0, b = 1) => Math.min(b, Math.max(a, x));
25
- const lerp = (a, b, p) => a + (b - a) * p;
26
- const ease = {
27
- out: (x) => 1 - Math.pow(1 - x, 3),
28
- inOut: (x) => (x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2),
29
- back: (x) => { const c = 1.4; return 1 + (c + 1) * Math.pow(x - 1, 3) + c * Math.pow(x - 1, 2); },
30
- lin: (x) => x,
31
- };
32
- const prog = (t, t0, dur, e = ease.out) => e(clamp((t - t0) / Math.max(dur, 1e-6)));
33
- /** 窗口内出现:t0 前 0,淡入 fi 秒到 1,t1 起淡出 fo 秒到 0。 */
34
- const span = (t, t0, t1, fi = 0.5, fo = 0.4) => Math.min(prog(t, t0, fi), 1 - prog(t, t1, fo, ease.inOut));
35
- const $ = (id) => document.getElementById(id);
36
- const A = (key) => {
37
- if (!(key in ANCHORS)) throw new Error("锚点「" + key + "」未解析");
38
- return ANCHORS[key] / FPS;
39
- };
40
-
41
- function box(el, x, y, w, h) {
42
- if (typeof el === "string") el = $(el);
43
- el.style.left = x + "px"; el.style.top = y + "px"; el.style.width = w + "px"; el.style.height = h + "px";
44
- return el;
45
- }
46
- /** 透明度 + 位移 / 缩放(不旋转——槽位与其装饰不得旋转)。 */
47
- function look(el, o = 1, dx = 0, dy = 0, s = 1) {
48
- if (typeof el === "string") el = $(el);
49
- el.style.opacity = String(clamp(o));
50
- el.style.transform = dx || dy || s !== 1 ? "translate(" + dx + "px," + dy + "px) scale(" + s + ")" : "";
51
- return el;
52
- }
53
- /** 槽位与跟随它的装饰一起定框;o 同时作用于两者。 */
54
- function slot(id, x, y, w, h, o = 1) {
55
- const el = document.querySelector('[data-slot="' + id + '"]');
56
- box(el, x, y, w, h); el.style.opacity = String(clamp(o));
57
- for (const d of document.querySelectorAll('[data-for="' + id + '"]')) {
58
- const p = Number(d.dataset.pad || 0);
59
- box(d, x - p, y - p, w + 2 * p, h + 2 * p); d.style.opacity = String(clamp(o));
60
- }
61
- }
62
- /** 槽位按矩形 [x, y, w, h] 定框(slot 的数组写法)。 */
63
- const at = (id, r, o = 1) => slot(id, r[0], r[1], r[2], r[3], o);
64
- /** 两个矩形之间插值(版式切换:单画 → 并排)。 */
65
- const mix = (a, b, p) => a.map((v, i) => lerp(v, b[i], p));
66
- /**
67
- * 逐句换画面的透明度:在 a 前 fade 秒淡入、a 时刻已全显;到 b(下一张已全显)之后直接关掉。
68
- * 同一位置叠放的几张画面按 data-z 递增排,下一张盖住上一张再关上一张——任一时刻最多两张在解码。
69
- */
70
- const seq = (t, a, b = null, fade = 0.3) => prog(t, a - fade, fade) * (b == null || t < b + 0.02 ? 1 : 0);
71
- /** 把元素放到 (x, y) 并给透明度(标签、角标跟随画面用)。 */
72
- function place(el, x, y, o = 1) {
73
- if (typeof el === "string") el = $(el);
74
- el.style.left = x + "px"; el.style.top = y + "px"; el.style.opacity = String(clamp(o));
75
- return el;
76
- }
77
- /** 逐字显现(p ∈ [0,1]):未显现的字留占位但透明,排版不跳。 */
78
- function typeOn(el, text, p) {
79
- if (typeof el === "string") el = $(el);
80
- const n = Math.round(clamp(p) * text.length);
81
- const key = text + "|" + n;
82
- if (el.dataset.typed === key) return;
83
- el.dataset.typed = key;
84
- el.innerHTML = "";
85
- const a = document.createElement("span"); a.textContent = text.slice(0, n);
86
- const b = document.createElement("span"); b.textContent = text.slice(n); b.style.opacity = "0";
87
- el.append(a, b);
88
- }
89
- /** 字幕:取文本输入 slot 在第 f 帧的那一条;highlight 里的词加强调色。 */
90
- function cueAt(slotId, t) {
91
- const f = Math.floor(t * FPS + 1e-6);
92
- const list = TEXTS[slotId] || [];
93
- for (const c of list) if (f >= c.f0 && f < c.f1) return c;
94
- return null;
95
- }
96
- /** 显现:淡入 + 上浮(p ∈ [0,1])。 */
97
- const reveal = (el, p, dy = 26) => look(el, p, 0, dy * (1 - p));
98
- /** 从左往右擦出。 */
99
- function wipe(el, p) {
100
- if (typeof el === "string") el = $(el);
101
- el.style.clipPath = "inset(-20% " + (100 - clamp(p) * 100).toFixed(2) + "% -20% 0)";
102
- el.style.opacity = p > 0 ? "1" : "0";
103
- }
104
- /** SVG 路径描边动画。 */
105
- function draw(el, p) {
106
- if (typeof el === "string") el = $(el);
107
- const L = el.getTotalLength();
108
- el.style.strokeDasharray = String(L);
109
- el.style.strokeDashoffset = String(L * (1 - clamp(p)));
110
- }
111
- /** 字幕;hide = [[t0, t1], …] 时段内不显示(屏上排版已经写着同一句话时用)。 */
112
- function caption(el, slotId, t, highlight = [], hide = []) {
113
- if (typeof el === "string") el = $(el);
114
- if (hide.some(([a, b]) => t >= a && t < b)) { el.style.opacity = "0"; return null; }
115
- const c = cueAt(slotId, t);
116
- const text = c ? c.text : "";
117
- const key = text + "|" + highlight.join(",");
118
- if (el.dataset.cue !== key) {
119
- el.dataset.cue = key;
120
- el.innerHTML = "";
121
- let rest = text;
122
- while (rest) {
123
- let hit = null, at = -1;
124
- for (const w of highlight) {
125
- const i = rest.indexOf(w);
126
- if (i >= 0 && (at < 0 || i < at)) { at = i; hit = w; }
127
- }
128
- if (!hit) { el.append(document.createTextNode(rest)); break; }
129
- if (at > 0) el.append(document.createTextNode(rest.slice(0, at)));
130
- const b = document.createElement("b"); b.textContent = hit; el.append(b);
131
- rest = rest.slice(at + hit.length);
132
- }
133
- }
134
- const age = c ? t - c.f0 / FPS : 0;
135
- el.style.opacity = c ? String(prog(age, 0, 0.12)) : "0";
136
- return c;
137
- }
138
-
139
- function applyLayer() {
140
- stage.style.visibility = LAYER === "front" ? "hidden" : "visible";
141
- for (const el of stage.querySelectorAll("[data-layer]")) {
142
- el.style.visibility = LAYER === "all" || el.dataset.layer === LAYER ? "visible" : "hidden";
143
- }
144
- }
145
-
146
- function measure() {
147
- const s = stage.getBoundingClientRect(), k = s.width / W;
148
- const out = [];
149
- for (const el of stage.querySelectorAll("[data-slot]")) {
150
- const r = el.getBoundingClientRect();
151
- const opacity = parseFloat(getComputedStyle(el).opacity);
152
- const crop = (el.dataset.crop || "0,0,0,0").split(",").map(Number);
153
- out.push({
154
- id: el.dataset.slot, z: Number(el.dataset.z || 0),
155
- x: (r.left - s.left) / k, y: (r.top - s.top) / k, w: r.width / k, h: r.height / k,
156
- opacity, visible: opacity > 0.001 && r.width > 0.5 && r.height > 0.5,
157
- shape: el.dataset.shape || "rect", radius: Number(el.dataset.radius || 0),
158
- fit: el.dataset.fit || "cover", filter: el.dataset.filter || "",
159
- crop: crop.length === 4 && crop.every((c) => c >= 0 && c < 1) ? crop : [0, 0, 0, 0],
160
- });
161
- }
162
- return out.sort((a, b) => a.z - b.z);
163
- }
164
-
165
- /**
166
- * 排版自检(只给出片台 / 构建工具用,宿主协议不依赖它)。查三类:
167
- * 1. 两张可见画面重叠,而下面那张有 front 层描边 —— 宿主先画全部画面、再画 front 镜像,
168
- * 下面那张的边框会压在上面那张画面上(同框交替淡入时两框重合,不算);
169
- * 2. front 层的线条(path / line / polyline,按已画出的长度取点)穿过某张可见画面;
170
- * 3. 画面越出舞台(铺满舞台的底图除外)。
171
- */
172
- function lint() {
173
- const s = stage.getBoundingClientRect(), k = s.width / W;
174
- const op = (el) => { let o = 1; for (let e = el; e && e !== stage; e = e.parentElement) o *= parseFloat(getComputedStyle(e).opacity); return o; };
175
- // 铺满舞台的画面是底图,压在它上面的任何东西都是有意叠加,不算穿模
176
- const slots = measure().filter((g) => g.visible && !(g.x <= 1 && g.y <= 1 && g.x + g.w >= W - 1 && g.y + g.h >= H - 1));
177
- const same = (a, b) => ["x", "y", "w", "h"].every((q) => Math.abs(a[q] - b[q]) < 2);
178
- const meet = (a, b) => Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x) > 2 && Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y) > 2;
179
- const deco = (id) => [...stage.querySelectorAll('[data-for="' + id + '"][data-layer="front"]')].some((d) => op(d) > 0.01);
180
- const issues = [];
181
- for (const g of measure().filter((g) => g.visible)) {
182
- if (g.x < -1 || g.y < -1 || g.x + g.w > W + 1 || g.y + g.h > H + 1) {
183
- const full = g.x <= 1 && g.y <= 1 && g.x + g.w >= W - 1 && g.y + g.h >= H - 1;
184
- if (!full) issues.push({ kind: "slot-out-of-stage", slot: g.id });
185
- }
186
- }
187
- for (let i = 0; i < slots.length; i++) {
188
- for (let j = i + 1; j < slots.length; j++) {
189
- const a = slots[i], b = slots[j];
190
- if (!meet(a, b) || same(a, b)) continue;
191
- if (deco(a.id)) issues.push({ kind: "border-over-slot", lower: a.id, upper: b.id });
192
- else if (a.z === b.z && deco(b.id)) issues.push({ kind: "same-z-overlap", a: a.id, b: b.id });
193
- }
194
- }
195
- const inside = (g, x, y) => {
196
- if (g.shape === "circle") { const dx = (x - g.x - g.w / 2) / (g.w / 2), dy = (y - g.y - g.h / 2) / (g.h / 2); return dx * dx + dy * dy < 0.96; }
197
- return x > g.x + 3 && x < g.x + g.w - 3 && y > g.y + 3 && y < g.y + g.h - 3;
198
- };
199
- for (const p of stage.querySelectorAll('[data-layer="front"] path, [data-layer="front"] line, [data-layer="front"] polyline')) {
200
- if (op(p) < 0.01 || !p.getTotalLength) continue;
201
- const L = p.getTotalLength();
202
- const cs = getComputedStyle(p);
203
- const drawn = cs.strokeDasharray && cs.strokeDasharray !== "none" ? L - (parseFloat(cs.strokeDashoffset) || 0) : L;
204
- const m = p.getScreenCTM();
205
- const hit = new Set();
206
- for (let d = 0; d <= drawn; d += 6) {
207
- const q = new DOMPoint(p.getPointAtLength(d).x, p.getPointAtLength(d).y).matrixTransform(m);
208
- const x = (q.x - s.left) / k, y = (q.y - s.top) / k;
209
- for (const g of slots) if (!hit.has(g.id) && inside(g, x, y)) hit.add(g.id);
210
- }
211
- for (const id of hit) issues.push({ kind: "stroke-through-slot", stroke: p.id || p.tagName, slot: id });
212
- }
213
- return issues;
214
- }
215
-
216
- function fitStage() { stage.style.transform = "scale(" + innerWidth / W + ")"; }
217
- addEventListener("resize", fitStage); fitStage();
218
-
219
- const CP = (window.CP = {
220
- VERSION: "composite-kit@1",
221
- META, W, H, $, A, has: (k) => k in ANCHORS, clamp, lerp, ease, prog, span, box, look, slot, at, mix, seq, place, typeOn, cueAt, caption,
222
- reveal, wipe, draw, cues: (slotId) => TEXTS[slotId] || [], fps: () => FPS,
223
- render: () => {},
224
- /** init 之后调用一次(字幕等输入已就位):按输入建一次性的 DOM。必须幂等。 */
225
- setup: () => {},
226
- });
227
-
228
- addEventListener("message", (ev) => {
229
- const m = ev.data;
230
- if (!m || m.ch !== "gtrk-composite") return;
231
- const reply = (x) => parent.postMessage(Object.assign({ ch: "gtrk-composite" }, x), "*");
232
- try {
233
- if (m.type === "hello") reply({ type: "meta", meta: META });
234
- else if (m.type === "init") {
235
- FPS = m.fps || FPS; ANCHORS = m.anchors || ANCHORS; LAYER = m.layer || "all"; TEXTS = m.texts || {};
236
- CP.setup(); applyLayer(); reply({ type: "ready", layer: LAYER });
237
- } else if (m.type === "apply") {
238
- CP.render(m.frame / FPS);
239
- reply({ type: "frame", seq: m.seq, frame: m.frame, slots: measure(), layer: LAYER });
240
- } else if (m.type === "lint") {
241
- // 回包类型不能也叫 lint:颗粒作为顶层页面跑(gtrk composite lint)时回包会回到自己的监听里,同名就会无限往返
242
- reply({ type: "lint-result", seq: m.seq, issues: lint() });
243
- }
244
- } catch (e) { reply({ type: "error", seq: m.seq, message: String((e && e.message) || e) }); }
245
- });
246
- })();
1
+ /* 合成颗粒工具箱 composite-kit@1(html@1 引擎)——`gtrk composite build` 构建时内联进每颗颗粒,颗粒自包含、不外链。
2
+ * 随 @gitruck/cli 分发(assets/composite-kit/);改行为即升版本号(颗粒 <meta name="generator"> 与 CP.VERSION 记着它)。
3
+ *
4
+ * 场景脚本只写一件事:`CP.render = (t) => { ... }`(t = 局部秒)。其余由这里负责:
5
+ * - 宿主协议:hello → meta;init → 帧率 / 锚点 / 图层 / 字幕;apply(frame) → 画这一帧并回报槽位几何;lint → 排版自检;
6
+ * - 分层:舞台缺省属 back,`data-layer="front"` 的子树属 front(visibility 继承);
7
+ * - 槽位几何:`[data-slot]` 的包围盒 + data-z / data-fit / data-shape / data-radius / data-filter / data-crop(上右下左);
8
+ * - 装饰跟随:`data-for="<槽位>"` 的元素与槽位同框(data-pad 外扩),用于边框、阴影、角标;
9
+ * - 字幕:接管字幕后由颗粒自己排(`CP.caption`),文字来自 init.texts;
10
+ * - 逐句换画面与版式切换:`at` / `mix` / `seq` / `place`;
11
+ * - 排版自检 `lint`:下层画面的 front 描边压上层画面、front 线条穿过画面、画面越出舞台。
12
+ * 纯函数:同一帧号永远画出同一画面;不用 Date / Math.random / requestAnimationFrame / setInterval。
13
+ */
14
+ (() => {
15
+ "use strict";
16
+ const META = JSON.parse(document.querySelector("script[data-gtrk-composite]").textContent);
17
+ const W = META.stage.width, H = META.stage.height;
18
+ const stage = document.getElementById("stage");
19
+ let FPS = (META.resolved && META.resolved.video_rate) || 60;
20
+ let ANCHORS = (META.resolved && META.resolved.anchors_f) || {};
21
+ let LAYER = "all";
22
+ let TEXTS = {};
23
+
24
+ const clamp = (x, a = 0, b = 1) => Math.min(b, Math.max(a, x));
25
+ const lerp = (a, b, p) => a + (b - a) * p;
26
+ const ease = {
27
+ out: (x) => 1 - Math.pow(1 - x, 3),
28
+ inOut: (x) => (x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2),
29
+ back: (x) => { const c = 1.4; return 1 + (c + 1) * Math.pow(x - 1, 3) + c * Math.pow(x - 1, 2); },
30
+ lin: (x) => x,
31
+ };
32
+ const prog = (t, t0, dur, e = ease.out) => e(clamp((t - t0) / Math.max(dur, 1e-6)));
33
+ /** 窗口内出现:t0 前 0,淡入 fi 秒到 1,t1 起淡出 fo 秒到 0。 */
34
+ const span = (t, t0, t1, fi = 0.5, fo = 0.4) => Math.min(prog(t, t0, fi), 1 - prog(t, t1, fo, ease.inOut));
35
+ const $ = (id) => document.getElementById(id);
36
+ const A = (key) => {
37
+ if (!(key in ANCHORS)) throw new Error("锚点「" + key + "」未解析");
38
+ return ANCHORS[key] / FPS;
39
+ };
40
+
41
+ function box(el, x, y, w, h) {
42
+ if (typeof el === "string") el = $(el);
43
+ el.style.left = x + "px"; el.style.top = y + "px"; el.style.width = w + "px"; el.style.height = h + "px";
44
+ return el;
45
+ }
46
+ /** 透明度 + 位移 / 缩放(不旋转——槽位与其装饰不得旋转)。 */
47
+ function look(el, o = 1, dx = 0, dy = 0, s = 1) {
48
+ if (typeof el === "string") el = $(el);
49
+ el.style.opacity = String(clamp(o));
50
+ el.style.transform = dx || dy || s !== 1 ? "translate(" + dx + "px," + dy + "px) scale(" + s + ")" : "";
51
+ return el;
52
+ }
53
+ /** 槽位与跟随它的装饰一起定框;o 同时作用于两者。 */
54
+ function slot(id, x, y, w, h, o = 1) {
55
+ const el = document.querySelector('[data-slot="' + id + '"]');
56
+ box(el, x, y, w, h); el.style.opacity = String(clamp(o));
57
+ for (const d of document.querySelectorAll('[data-for="' + id + '"]')) {
58
+ const p = Number(d.dataset.pad || 0);
59
+ box(d, x - p, y - p, w + 2 * p, h + 2 * p); d.style.opacity = String(clamp(o));
60
+ }
61
+ }
62
+ /** 槽位按矩形 [x, y, w, h] 定框(slot 的数组写法)。 */
63
+ const at = (id, r, o = 1) => slot(id, r[0], r[1], r[2], r[3], o);
64
+ /** 两个矩形之间插值(版式切换:单画 → 并排)。 */
65
+ const mix = (a, b, p) => a.map((v, i) => lerp(v, b[i], p));
66
+ /**
67
+ * 逐句换画面的透明度:在 a 前 fade 秒淡入、a 时刻已全显;到 b(下一张已全显)之后直接关掉。
68
+ * 同一位置叠放的几张画面按 data-z 递增排,下一张盖住上一张再关上一张——任一时刻最多两张在解码。
69
+ */
70
+ const seq = (t, a, b = null, fade = 0.3) => prog(t, a - fade, fade) * (b == null || t < b + 0.02 ? 1 : 0);
71
+ /** 把元素放到 (x, y) 并给透明度(标签、角标跟随画面用)。 */
72
+ function place(el, x, y, o = 1) {
73
+ if (typeof el === "string") el = $(el);
74
+ el.style.left = x + "px"; el.style.top = y + "px"; el.style.opacity = String(clamp(o));
75
+ return el;
76
+ }
77
+ /** 逐字显现(p ∈ [0,1]):未显现的字留占位但透明,排版不跳。 */
78
+ function typeOn(el, text, p) {
79
+ if (typeof el === "string") el = $(el);
80
+ const n = Math.round(clamp(p) * text.length);
81
+ const key = text + "|" + n;
82
+ if (el.dataset.typed === key) return;
83
+ el.dataset.typed = key;
84
+ el.innerHTML = "";
85
+ const a = document.createElement("span"); a.textContent = text.slice(0, n);
86
+ const b = document.createElement("span"); b.textContent = text.slice(n); b.style.opacity = "0";
87
+ el.append(a, b);
88
+ }
89
+ /** 字幕:取文本输入 slot 在第 f 帧的那一条;highlight 里的词加强调色。 */
90
+ function cueAt(slotId, t) {
91
+ const f = Math.floor(t * FPS + 1e-6);
92
+ const list = TEXTS[slotId] || [];
93
+ for (const c of list) if (f >= c.f0 && f < c.f1) return c;
94
+ return null;
95
+ }
96
+ /** 显现:淡入 + 上浮(p ∈ [0,1])。 */
97
+ const reveal = (el, p, dy = 26) => look(el, p, 0, dy * (1 - p));
98
+ /** 从左往右擦出。 */
99
+ function wipe(el, p) {
100
+ if (typeof el === "string") el = $(el);
101
+ el.style.clipPath = "inset(-20% " + (100 - clamp(p) * 100).toFixed(2) + "% -20% 0)";
102
+ el.style.opacity = p > 0 ? "1" : "0";
103
+ }
104
+ /** SVG 路径描边动画。 */
105
+ function draw(el, p) {
106
+ if (typeof el === "string") el = $(el);
107
+ const L = el.getTotalLength();
108
+ el.style.strokeDasharray = String(L);
109
+ el.style.strokeDashoffset = String(L * (1 - clamp(p)));
110
+ }
111
+ /** 字幕;hide = [[t0, t1], …] 时段内不显示(屏上排版已经写着同一句话时用)。 */
112
+ function caption(el, slotId, t, highlight = [], hide = []) {
113
+ if (typeof el === "string") el = $(el);
114
+ if (hide.some(([a, b]) => t >= a && t < b)) { el.style.opacity = "0"; return null; }
115
+ const c = cueAt(slotId, t);
116
+ const text = c ? c.text : "";
117
+ const key = text + "|" + highlight.join(",");
118
+ if (el.dataset.cue !== key) {
119
+ el.dataset.cue = key;
120
+ el.innerHTML = "";
121
+ let rest = text;
122
+ while (rest) {
123
+ let hit = null, at = -1;
124
+ for (const w of highlight) {
125
+ const i = rest.indexOf(w);
126
+ if (i >= 0 && (at < 0 || i < at)) { at = i; hit = w; }
127
+ }
128
+ if (!hit) { el.append(document.createTextNode(rest)); break; }
129
+ if (at > 0) el.append(document.createTextNode(rest.slice(0, at)));
130
+ const b = document.createElement("b"); b.textContent = hit; el.append(b);
131
+ rest = rest.slice(at + hit.length);
132
+ }
133
+ }
134
+ const age = c ? t - c.f0 / FPS : 0;
135
+ el.style.opacity = c ? String(prog(age, 0, 0.12)) : "0";
136
+ return c;
137
+ }
138
+
139
+ function applyLayer() {
140
+ stage.style.visibility = LAYER === "front" ? "hidden" : "visible";
141
+ for (const el of stage.querySelectorAll("[data-layer]")) {
142
+ el.style.visibility = LAYER === "all" || el.dataset.layer === LAYER ? "visible" : "hidden";
143
+ }
144
+ }
145
+
146
+ function measure() {
147
+ const s = stage.getBoundingClientRect(), k = s.width / W;
148
+ const out = [];
149
+ for (const el of stage.querySelectorAll("[data-slot]")) {
150
+ const r = el.getBoundingClientRect();
151
+ const opacity = parseFloat(getComputedStyle(el).opacity);
152
+ const crop = (el.dataset.crop || "0,0,0,0").split(",").map(Number);
153
+ out.push({
154
+ id: el.dataset.slot, z: Number(el.dataset.z || 0),
155
+ x: (r.left - s.left) / k, y: (r.top - s.top) / k, w: r.width / k, h: r.height / k,
156
+ opacity, visible: opacity > 0.001 && r.width > 0.5 && r.height > 0.5,
157
+ shape: el.dataset.shape || "rect", radius: Number(el.dataset.radius || 0),
158
+ fit: el.dataset.fit || "cover", filter: el.dataset.filter || "",
159
+ crop: crop.length === 4 && crop.every((c) => c >= 0 && c < 1) ? crop : [0, 0, 0, 0],
160
+ });
161
+ }
162
+ return out.sort((a, b) => a.z - b.z);
163
+ }
164
+
165
+ /**
166
+ * 排版自检(只给出片台 / 构建工具用,宿主协议不依赖它)。查三类:
167
+ * 1. 两张可见画面重叠,而下面那张有 front 层描边 —— 宿主先画全部画面、再画 front 镜像,
168
+ * 下面那张的边框会压在上面那张画面上(同框交替淡入时两框重合,不算);
169
+ * 2. front 层的线条(path / line / polyline,按已画出的长度取点)穿过某张可见画面;
170
+ * 3. 画面越出舞台(铺满舞台的底图除外)。
171
+ */
172
+ function lint() {
173
+ const s = stage.getBoundingClientRect(), k = s.width / W;
174
+ const op = (el) => { let o = 1; for (let e = el; e && e !== stage; e = e.parentElement) o *= parseFloat(getComputedStyle(e).opacity); return o; };
175
+ // 铺满舞台的画面是底图,压在它上面的任何东西都是有意叠加,不算穿模
176
+ const slots = measure().filter((g) => g.visible && !(g.x <= 1 && g.y <= 1 && g.x + g.w >= W - 1 && g.y + g.h >= H - 1));
177
+ const same = (a, b) => ["x", "y", "w", "h"].every((q) => Math.abs(a[q] - b[q]) < 2);
178
+ const meet = (a, b) => Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x) > 2 && Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y) > 2;
179
+ const deco = (id) => [...stage.querySelectorAll('[data-for="' + id + '"][data-layer="front"]')].some((d) => op(d) > 0.01);
180
+ const issues = [];
181
+ for (const g of measure().filter((g) => g.visible)) {
182
+ if (g.x < -1 || g.y < -1 || g.x + g.w > W + 1 || g.y + g.h > H + 1) {
183
+ const full = g.x <= 1 && g.y <= 1 && g.x + g.w >= W - 1 && g.y + g.h >= H - 1;
184
+ if (!full) issues.push({ kind: "slot-out-of-stage", slot: g.id });
185
+ }
186
+ }
187
+ for (let i = 0; i < slots.length; i++) {
188
+ for (let j = i + 1; j < slots.length; j++) {
189
+ const a = slots[i], b = slots[j];
190
+ if (!meet(a, b) || same(a, b)) continue;
191
+ if (deco(a.id)) issues.push({ kind: "border-over-slot", lower: a.id, upper: b.id });
192
+ else if (a.z === b.z && deco(b.id)) issues.push({ kind: "same-z-overlap", a: a.id, b: b.id });
193
+ }
194
+ }
195
+ const inside = (g, x, y) => {
196
+ if (g.shape === "circle") { const dx = (x - g.x - g.w / 2) / (g.w / 2), dy = (y - g.y - g.h / 2) / (g.h / 2); return dx * dx + dy * dy < 0.96; }
197
+ return x > g.x + 3 && x < g.x + g.w - 3 && y > g.y + 3 && y < g.y + g.h - 3;
198
+ };
199
+ for (const p of stage.querySelectorAll('[data-layer="front"] path, [data-layer="front"] line, [data-layer="front"] polyline')) {
200
+ if (op(p) < 0.01 || !p.getTotalLength) continue;
201
+ const L = p.getTotalLength();
202
+ const cs = getComputedStyle(p);
203
+ const drawn = cs.strokeDasharray && cs.strokeDasharray !== "none" ? L - (parseFloat(cs.strokeDashoffset) || 0) : L;
204
+ const m = p.getScreenCTM();
205
+ const hit = new Set();
206
+ for (let d = 0; d <= drawn; d += 6) {
207
+ const q = new DOMPoint(p.getPointAtLength(d).x, p.getPointAtLength(d).y).matrixTransform(m);
208
+ const x = (q.x - s.left) / k, y = (q.y - s.top) / k;
209
+ for (const g of slots) if (!hit.has(g.id) && inside(g, x, y)) hit.add(g.id);
210
+ }
211
+ for (const id of hit) issues.push({ kind: "stroke-through-slot", stroke: p.id || p.tagName, slot: id });
212
+ }
213
+ return issues;
214
+ }
215
+
216
+ function fitStage() { stage.style.transform = "scale(" + innerWidth / W + ")"; }
217
+ addEventListener("resize", fitStage); fitStage();
218
+
219
+ const CP = (window.CP = {
220
+ VERSION: "composite-kit@1",
221
+ META, W, H, $, A, has: (k) => k in ANCHORS, clamp, lerp, ease, prog, span, box, look, slot, at, mix, seq, place, typeOn, cueAt, caption,
222
+ reveal, wipe, draw, cues: (slotId) => TEXTS[slotId] || [], fps: () => FPS,
223
+ render: () => {},
224
+ /** init 之后调用一次(字幕等输入已就位):按输入建一次性的 DOM。必须幂等。 */
225
+ setup: () => {},
226
+ });
227
+
228
+ addEventListener("message", (ev) => {
229
+ const m = ev.data;
230
+ if (!m || m.ch !== "gtrk-composite") return;
231
+ const reply = (x) => parent.postMessage(Object.assign({ ch: "gtrk-composite" }, x), "*");
232
+ try {
233
+ if (m.type === "hello") reply({ type: "meta", meta: META });
234
+ else if (m.type === "init") {
235
+ FPS = m.fps || FPS; ANCHORS = m.anchors || ANCHORS; LAYER = m.layer || "all"; TEXTS = m.texts || {};
236
+ CP.setup(); applyLayer(); reply({ type: "ready", layer: LAYER });
237
+ } else if (m.type === "apply") {
238
+ CP.render(m.frame / FPS);
239
+ reply({ type: "frame", seq: m.seq, frame: m.frame, slots: measure(), layer: LAYER });
240
+ } else if (m.type === "lint") {
241
+ // 回包类型不能也叫 lint:颗粒作为顶层页面跑(gtrk composite lint)时回包会回到自己的监听里,同名就会无限往返
242
+ reply({ type: "lint-result", seq: m.seq, issues: lint() });
243
+ }
244
+ } catch (e) { reply({ type: "error", seq: m.seq, message: String((e && e.message) || e) }); }
245
+ });
246
+ })();
@@ -143,7 +143,8 @@ function sourceFor(b, T, f) {
143
143
  const hit = byClip.get(b.clip_id); if (!hit) return null;
144
144
  const c = hit.clip;
145
145
  const tt = Math.min(Math.max(T, c.track_st), lastFrameT(c));
146
- return { material: c.material, sec: clipSrc(c, tt) };
146
+ // slip_ms:源时钟上整体前后挪取帧(add-composite-slot-user-overrides);超过素材末尾由 locate 的 eof 段取末帧
147
+ return { material: c.material, sec: Math.max(0, clipSrc(c, tt) + (b.slip_ms || 0) / 1000) };
147
148
  }
148
149
  if (b.kind === "ref-lane") {
149
150
  const hit = byClip.get(b.lane_clip_id); if (!hit) return null;
@@ -160,6 +161,21 @@ function sourceFor(b, T, f) {
160
161
  }
161
162
  return null;
162
163
  }
164
+ /**
165
+ * 用户覆盖套到颗粒回报的几何上(add-composite-slot-user-overrides,与客户端 `applySlotOverrides` 逐值相同):
166
+ * box = 以槽位中心缩放 scale_pm/1000 倍、中心位移舞台宽高的 dx_pm / dy_pm 千分比,圆角同乘缩放;
167
+ * crop = 替换颗粒的 data-crop([上, 右, 下, 左] 比例)。
168
+ */
169
+ function applySlotOverrides(g, b) {
170
+ let out = g;
171
+ if (b.box) {
172
+ const s = b.box.scale_pm / 1000, w = g.w * s, h = g.h * s;
173
+ const cx = g.x + g.w / 2 + (b.box.dx_pm / 1000) * 1920, cy = g.y + g.h / 2 + (b.box.dy_pm / 1000) * 1080;
174
+ out = { ...out, x: cx - w / 2, y: cy - h / 2, w, h, radius: (g.radius || 0) * s };
175
+ }
176
+ if (b.crop) out = { ...out, crop: [b.crop.t_pm / 1000, b.crop.r_pm / 1000, b.crop.b_pm / 1000, b.crop.l_pm / 1000] };
177
+ return out;
178
+ }
163
179
  /** fit / framing / crop 合成的绘制矩形(与客户端 `slotDrawRect` 逐值相同;源或槽位尺寸为 0 时返回 null,不画)。 */
164
180
  function drawRect(g, sw0, sh0, fit, framing) {
165
181
  const [top, right, bottom, left] = g.crop;
@@ -245,7 +261,8 @@ window.renderAt = async (T) => {
245
261
  });
246
262
  const ready = (await Promise.all(jobs)).filter(Boolean);
247
263
  ctx.clearRect(0, 0, 1920, 1080);
248
- for (const { g, b, v, k, tag, img } of ready) {
264
+ for (const { g: g0, b, v, k, tag, img } of ready) {
265
+ const g = applySlotOverrides(g0, b);
249
266
  // 中间文件底部的帧号条不进画面:源高度按去掉条带算
250
267
  const r = img ? drawRect(g, img.naturalWidth, img.naturalHeight, b.fit || g.fit, b.framing) : drawRect(g, v.videoWidth, v.videoHeight - (tag || 0), b.fit || g.fit, b.framing);
251
268
  if (!r) continue;
@@ -1,6 +1,6 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" width="560" height="170" viewBox="0 0 560 170" role="img" aria-labelledby="title">
2
- <title id="title">gtrk CLI</title>
3
- <text x="84" y="128" fill="#f43b48" font-family="Segoe UI, Helvetica, Arial, sans-serif" font-size="150" font-weight="750" letter-spacing="-9">gtrk</text>
4
- <path d="M403 63v61" stroke="#f43b48" stroke-opacity=".3" stroke-width="2"/>
5
- <text x="425" y="106" fill="#888888" font-family="Consolas, monospace" font-size="28" letter-spacing="3">CLI</text>
6
- </svg>
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="560" height="170" viewBox="0 0 560 170" role="img" aria-labelledby="title">
2
+ <title id="title">gtrk CLI</title>
3
+ <text x="84" y="128" fill="#f43b48" font-family="Segoe UI, Helvetica, Arial, sans-serif" font-size="150" font-weight="750" letter-spacing="-9">gtrk</text>
4
+ <path d="M403 63v61" stroke="#f43b48" stroke-opacity=".3" stroke-width="2"/>
5
+ <text x="425" y="106" fill="#888888" font-family="Consolas, monospace" font-size="28" letter-spacing="3">CLI</text>
6
+ </svg>
@@ -1,16 +1,16 @@
1
- # 框架契约库(handoff contracts)
2
-
3
- 同合云管线的 handoff 契约**正本**,随 `@gitruck/cli` npm 包分发。创作 skill 以「契约名 + 版本」引用(如 `gsap-emit v1`);`handoff-contracts.json` 是「handoff 类型 → 契约」映射表,消费方查表、不硬编码。
4
-
5
- ## 收录双轴边界(handoff-contract-library spec,扩项须走 change 过审)
6
-
7
- - **轴一(管线性,审美零预设)**:契约只约束**机器可判定的管线消费属性**(封装/注册/确定性/依赖可达/产物格式)。**不得**规定颜色、字体、构图、红点数量等任何视觉取值或栏目词表词条;凡涉视觉取值处一律参数化/中性占位。「全片最多一个红点」这类审美铁律属栏目 skill 侧,**不入库**。
8
- - **轴二(脱敏)**:只收**自有管线**的合规契约——渲染引擎合规、`.gtrk` 契约、派单格式(封闭列举)。外部供应商/外部平台适配内容(AI 视频平台差异、文生图平台参数等)**永不收录**,那属于用户自己的 skill。
9
-
10
- ## 现有契约
11
-
12
- | 契约 | 版本 | 适用 handoff | 文档 |
13
- |---|---|---|---|
14
- | gsap-emit(HTML 动画颗粒逐帧 seek 合规) | v1 | MG(去品牌化前 RRV_MG,消费方查表前归一) | [gsap-emit-v1.md](gsap-emit-v1.md) |
15
-
16
- 维护者证据附录:[`gsap-emit-v1-evidence.md`](gsap-emit-v1-evidence.md)。该文件不属于 agent 执行时的必读契约。
1
+ # 框架契约库(handoff contracts)
2
+
3
+ 同合云管线的 handoff 契约**正本**,随 `@gitruck/cli` npm 包分发。创作 skill 以「契约名 + 版本」引用(如 `gsap-emit v1`);`handoff-contracts.json` 是「handoff 类型 → 契约」映射表,消费方查表、不硬编码。
4
+
5
+ ## 收录双轴边界(handoff-contract-library spec,扩项须走 change 过审)
6
+
7
+ - **轴一(管线性,审美零预设)**:契约只约束**机器可判定的管线消费属性**(封装/注册/确定性/依赖可达/产物格式)。**不得**规定颜色、字体、构图、红点数量等任何视觉取值或栏目词表词条;凡涉视觉取值处一律参数化/中性占位。「全片最多一个红点」这类审美铁律属栏目 skill 侧,**不入库**。
8
+ - **轴二(脱敏)**:只收**自有管线**的合规契约——渲染引擎合规、`.gtrk` 契约、派单格式(封闭列举)。外部供应商/外部平台适配内容(AI 视频平台差异、文生图平台参数等)**永不收录**,那属于用户自己的 skill。
9
+
10
+ ## 现有契约
11
+
12
+ | 契约 | 版本 | 适用 handoff | 文档 |
13
+ |---|---|---|---|
14
+ | gsap-emit(HTML 动画颗粒逐帧 seek 合规) | v1 | MG(去品牌化前 RRV_MG,消费方查表前归一) | [gsap-emit-v1.md](gsap-emit-v1.md) |
15
+
16
+ 维护者证据附录:[`gsap-emit-v1-evidence.md`](gsap-emit-v1-evidence.md)。该文件不属于 agent 执行时的必读契约。