@gitruck/cli 0.2.13 → 0.2.15

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 CHANGED
@@ -13,9 +13,9 @@
13
13
  ## 0. 一句话流程
14
14
 
15
15
  ```
16
- gtrk init # 一次性配置(API Key + 剪映目录)
17
- gtrk oralcut <毛片.mp4> [--script 文字稿.txt] # 剪一条;剪完自动打开产物目录
18
- gtrk transcript <本地视频.mp4> --json # 转成一个含总结/时码记录/纯文本的 Markdown
16
+ gtrk init # 一次性配置(API Key + 剪映目录)
17
+ gtrk oralcut <毛片.mp4> [--script 文字稿.txt] # 剪一条;剪完自动打开产物目录
18
+ gtrk transcript <本地视频.mp4> --json # 转成一个含总结/时码记录/纯文本的 Markdown
19
19
  ```
20
20
 
21
21
  跑完得到一个产物目录:`<毛片同目录>/<毛片名>-video-project-<YYMMDD-HHMMSS>/`,里面按格式分子目录,
@@ -144,40 +144,41 @@ gtrk split <拆分稿.json> --project <目录> --md --json # ⑤ 校验落地
144
144
 
145
145
  **关键行为(agent 需知):**
146
146
  - **时码恒挂源时基、每次发起现场投影**:用户手调切点后重导视图即跟随;此前被剪、现落回 clip 的句子自动复活,无需重跑转写。「拖入已剪好成片」= 恒等投影,同一套逻辑。
147
+ - **dispatch 的时码是快照,但消费侧会现场重投影**:`dispatch.json` 里的 `track_st/track_ed` 只在投影那一刻成立;`gtrk mg` / `gtrk matrix` **每次消费都用 `transcript × 当刻 .gtrk` 重算窗口**(与 split 落地同一段代码,构造性同源),所以**用户在 split 之后继续微调口播轨无需重跑 split**——只有**拆分稿本身**变了才要重跑。派单条目另带 `span:{from,to}` 自述「派什么」(aux 条目写 aux 自己的 span)。重投影不可行时(transcript 缺失 / 工程定位不到 / 主轨查不到口播素材)**降级用快照 + 显式告警 + `--json` 标 `reprojection.degraded`**,绝不硬崩。
147
148
  - **拆分稿零时码、id 区间引用**:beat 的文稿范围 = `span:{from:"u0007",to:"u0011"}`(utterance id 区间),**绝不抄原句文字、绝不自造时码**(防 LLM 幻觉)。幻觉 id / 区间倒序 / 跨 beat 重叠 / `transcript_hash` 错版 → **硬拒、非 0 退出、零副作用**。
148
149
  - **dropped 处理**:beat 的 span 内 utterance 全被剪 → 跳过该 beat 并入报告;部分被剪 → 按存活句包络收缩、标 `shrunk`。均不使命令失败。
149
- - **transcript 缺失**(旧任务)→ 明确报错引导「用新版本重跑 oralcut(恒出工程所需的结构化 transcript)」,不做降级猜测;`gtrk transcript` 只产人读 Markdown,不能替代 split 所需的结构化词表。
150
+ - **transcript 缺失**(旧任务)→ 明确报错引导「用新版本重跑 oralcut(恒出工程所需的结构化 transcript)」,不做降级猜测;`gtrk transcript` 只产人读 Markdown,不能替代 split 所需的结构化词表。
150
151
  - **只动 `struct_meta.split`**:写回不碰 materials/tracks,配合客户端「保存 → 发起 → 写回 → 重载」闭环(opencut 联动)。
151
152
  - **下游消费**:`dispatch.json` 的 `film_broll` 队列 → `gtrk matrix --project <dir>`(B-roll 双口检索 → `split/broll-plan.json` 候选清单;url 24h 过期,重跑即重签);`mg` 槽位表(去品牌化前 `rrv_mg`)→ `gtrk mg` 铺轨 / real-roam-viz 产颗粒;`ai_drama` 队列 → ai-drama-prompter。
152
153
 
153
- > **skill 分工**:拆 beat / 选 lane / 写 handoff 是脑(`gtrk-splitter` skill)的活;投影 / 校验 / 落地 / 写时码是手(本命令)的活。skill 铁律:只引用视图存在的 utterance id、不抄原文定位、不碰时码。
154
-
155
- ### 2.3 视频转文字稿:`gtrk transcript <本地视频>`
156
-
157
- 这一能力由独立的 `gtrk-transcript` Skill 驱动,不属于 `gtrk-tools` 单点工具族。用户说「视频转文字 / 视频转文字稿 / 提取视频文稿 / 把本地视频整理成妙记式文稿」时,直接运行:
158
-
159
- ```bash
160
- gtrk transcript "D:/素材/采访视频.mp4" --json
161
- ```
162
-
163
- | 参数 | 作用 | 缺省 |
164
- |---|---|---|
165
- | `<本地视频>` | 用户电脑上的视频文件;拒绝 URL、平台地址和远端下载 | 必填 |
166
- | `-o, --out <file>` | 唯一 Markdown 产物路径 | `<视频同目录>/<视频名>-transcript.md` |
167
- | `--lang <code>` | 识别语言 | `zh-CN` |
168
- | `--ffmpeg-path <dir>` | 指定本地 ffmpeg/ffprobe | 自动解析 |
169
- | `--reupload` | 忽略上传缓存,强制重传抽取音频 | 关 |
170
- | `--json` | stdout 只留 `{ok,taskId,fileId,output,summaryPending}` | agent 必带 |
171
-
172
- **边界与产物:**
173
-
174
- - 原视频始终留在本机;CLI 本地抽取 16 kHz 单声道 MP3,时长自检后只上传这份音频衍生物。
175
- - 用户目录最终只新增一个 `.md`,固定按「总结 → 文字记录 → 纯文本」排列;文字记录用 `[00:01:23]` 时码,不虚构说话人标签。
176
- - CLI 不负责总结:它在 `## 总结` 下写入 `<!-- gtrk:agent-summary-pending -->`,并返回 `summaryPending:true`。驱动 Agent 必须阅读全文,生成简洁、忠于原文的语义总结,**原地替换标记及提示语**;不得另建总结文件。
177
- - 写回时只改 `## 总结` 到 `## 文字记录` 之间,保留后两段原样;建议 3–7 条要点,覆盖主题、关键论点/事实和结论,不补原文没有的信息。
178
- - `output` 就是最终 Markdown 的绝对路径;确认文件存在、三段标题齐全且待总结标记已消失后,才能回给用户。ASR 实时价格运行前从官网价格表查询,严禁引用记忆价格。
179
-
180
- ---
154
+ > **skill 分工**:拆 beat / 选 lane / 写 handoff 是脑(`gtrk-splitter` skill)的活;投影 / 校验 / 落地 / 写时码是手(本命令)的活。skill 铁律:只引用视图存在的 utterance id、不抄原文定位、不碰时码。
155
+
156
+ ### 2.3 视频转文字稿:`gtrk transcript <本地视频>`
157
+
158
+ 这一能力由独立的 `gtrk-transcript` Skill 驱动,不属于 `gtrk-tools` 单点工具族。用户说「视频转文字 / 视频转文字稿 / 提取视频文稿 / 把本地视频整理成妙记式文稿」时,直接运行:
159
+
160
+ ```bash
161
+ gtrk transcript "D:/素材/采访视频.mp4" --json
162
+ ```
163
+
164
+ | 参数 | 作用 | 缺省 |
165
+ |---|---|---|
166
+ | `<本地视频>` | 用户电脑上的视频文件;拒绝 URL、平台地址和远端下载 | 必填 |
167
+ | `-o, --out <file>` | 唯一 Markdown 产物路径 | `<视频同目录>/<视频名>-transcript.md` |
168
+ | `--lang <code>` | 识别语言 | `zh-CN` |
169
+ | `--ffmpeg-path <dir>` | 指定本地 ffmpeg/ffprobe | 自动解析 |
170
+ | `--reupload` | 忽略上传缓存,强制重传抽取音频 | 关 |
171
+ | `--json` | stdout 只留 `{ok,taskId,fileId,output,summaryPending}` | agent 必带 |
172
+
173
+ **边界与产物:**
174
+
175
+ - 原视频始终留在本机;CLI 本地抽取 16 kHz 单声道 MP3,时长自检后只上传这份音频衍生物。
176
+ - 用户目录最终只新增一个 `.md`,固定按「总结 → 文字记录 → 纯文本」排列;文字记录用 `[00:01:23]` 时码,不虚构说话人标签。
177
+ - CLI 不负责总结:它在 `## 总结` 下写入 `<!-- gtrk:agent-summary-pending -->`,并返回 `summaryPending:true`。驱动 Agent 必须阅读全文,生成简洁、忠于原文的语义总结,**原地替换标记及提示语**;不得另建总结文件。
178
+ - 写回时只改 `## 总结` 到 `## 文字记录` 之间,保留后两段原样;建议 3–7 条要点,覆盖主题、关键论点/事实和结论,不补原文没有的信息。
179
+ - `output` 就是最终 Markdown 的绝对路径;确认文件存在、三段标题齐全且待总结标记已消失后,才能回给用户。ASR 实时价格运行前从官网价格表查询,严禁引用记忆价格。
180
+
181
+ ---
181
182
 
182
183
  ## 3. 产物结构 + 三端打开
183
184
 
@@ -264,14 +265,14 @@ gtrk oralcut "D:/素材/某条.mp4" --params-json '{"punctuation_breaks":{"。":
264
265
  ## 7. 扩展(给改 CLI 的 agent)
265
266
 
266
267
  新增命令 = 写 `src/commands/<name>.ts` 的 `register<Name>(program)` + 在 `src/index.ts` 注册一行。
267
- 云端调用走 `src/lib/cloud.ts`(`{code,msg,data}` 包装、鉴权 Header `Authorization:<裸key>`;单发取结果 `getTaskResult`、`pollTask` 复用之);上传一律走 `src/lib/upload-cache.ts` 的 `uploadCached`(白嫖指纹缓存;≥256MiB 自动分片断点续传,见 `src/lib/chunk-upload.ts`)。本地预处理(探几何 / 抽音频 / 压 720p)在 `src/lib/media.ts`;本地渲染(gtrk EDL → ffmpeg filter_complex)在 `src/lib/render.ts`;三方产物落地 + `result.json` 两段写在 `src/lib/materialize.ts`(`oralcut` 与 `oralcut-result` 共用)。已上线:`oralcut`(云剪)、`oralcut-result`(按 task_id 取回)、`render`(本地渲染 gtrk)、`split`(视觉拆分派单器:`src/lib/projection.ts` 投影纯函数 + `src/lib/splitdoc.ts` 拆分稿校验/落地 + `src/lib/gtrk-writeback.ts` 原子写回 `struct_meta.split`,随包分发 `skills/gtrk-splitter/`)。`matrix`(B-roll 检索:`src/lib/matrix.ts` 双口路由/派单翻译/plan 构建 + `src/commands/matrix.ts`;`matrix search "<词>"` ad-hoc)。规划中:`struct`(已有 gtrk → 三方工程)。
268
+ 云端调用走 `src/lib/cloud.ts`(`{code,msg,data}` 包装、鉴权 Header `Authorization:<裸key>`;单发取结果 `getTaskResult`、`pollTask` 复用之);上传一律走 `src/lib/upload-cache.ts` 的 `uploadCached`(白嫖指纹缓存;≥256MiB 自动分片断点续传,见 `src/lib/chunk-upload.ts`)。本地预处理(探几何 / 抽音频 / 压 720p)在 `src/lib/media.ts`;本地渲染(gtrk EDL → ffmpeg filter_complex)在 `src/lib/render.ts`;三方产物落地 + `result.json` 两段写在 `src/lib/materialize.ts`(`oralcut` 与 `oralcut-result` 共用)。已上线:`oralcut`(云剪)、`oralcut-result`(按 task_id 取回)、`render`(本地渲染 gtrk)、`split`(视觉拆分派单器:`src/lib/projection.ts` 投影纯函数 + `src/lib/splitdoc.ts` 拆分稿校验/落地 + `src/lib/gtrk-writeback.ts` 原子写回 `struct_meta.split`,随包分发 `skills/gtrk-splitter/`)。`matrix`(B-roll 检索:`src/lib/matrix.ts` 双口路由/派单翻译/plan 构建 + `src/commands/matrix.ts`;`matrix search "<词>"` ad-hoc)。写回 `.gtrk` 的命令(`matrix` 铺轨 / `mg` 铺轨)在写回**之后**跑一次**素材落盘自检**(`src/lib/material-integrity.ts`,纯只读、可注入 `exists` 谓词):遍历 `materials[].path` 确认文件真在盘上,相对路径**恒以 `.gtrk` 所在目录为基准**(历史坑:按工程根解析会全面误报),结果以 `integrity` 字段出 `--json`、以摘要 + 逐条清单出人读日志。**非致命**——查出悬空不改 `ok` / 退出码,也**不删任何素材条目或文件**(预防面在 opencut 客户端侧,本仓只做检测)。规划中:`struct`(已有 gtrk → 三方工程)。
268
269
 
269
270
  ### 工具族:接单点云能力 = 加一个 descriptor(不写编排)
270
271
 
271
- 单发单收的单点能力(图转运镜、图片/视频抠像…)不各开顶层命令,而入 `gtrk tool <name>` 工具族。骨架全归共享 runner `src/lib/tool-runner.ts`(校验 → 可选 preprocess → 按 descriptor `priceKey` 匿名查询官网实时价格并提示 → `uploadCached` → `submitTask`(6004 失效重传收编于此)→ 自循环轮询(复用 `getTaskResult`、墙钟 per-tool 可覆盖,**不改 `pollTask`**)→ `mapOutputs` **流式下载**落地(fetch body pipe 到 `createWriteStream`,大产物不过内存,**不用 `cloud.ts` 全内存 `download`**)→ `task.json`/`result.json` 面包屑)。差异全归一个薄 descriptor `src/lib/tool-descriptors.ts`(`name`/`kind`/`input`(含扩展名白名单、视频类 `maxDurationSec` 硬上限)/`taskType`/`priceKey`/`pricingContext`/`buildPayload`/`mapOutputs`/`options`/`enabled`+`disabledReason`/`pollTimeoutMs`)。价格数字不得写进 descriptor、README 或 skill;以 `https://cloud.ai-mcn.tv/api/get_price_list` 为唯一真相,失败显示暂不可用但不阻断任务。
272
+ 单发单收的单点能力(图转运镜、图片/视频抠像…)不各开顶层命令,而入 `gtrk tool <name>` 工具族。骨架全归共享 runner `src/lib/tool-runner.ts`(校验 → 可选 preprocess → 按 descriptor `priceKey` 匿名查询官网实时价格并提示 → `uploadCached` → `submitTask`(6004 失效重传收编于此)→ 自循环轮询(复用 `getTaskResult`、墙钟 per-tool 可覆盖,**不改 `pollTask`**)→ `mapOutputs` **流式下载**落地(fetch body pipe 到 `createWriteStream`,大产物不过内存,**不用 `cloud.ts` 全内存 `download`**)→ `task.json`/`result.json` 面包屑)。差异全归一个薄 descriptor `src/lib/tool-descriptors.ts`(`name`/`kind`/`input`(含扩展名白名单、视频类 `maxDurationSec` 硬上限)/`taskType`/`priceKey`/`pricingContext`/`buildPayload`/`mapOutputs`/`options`/`enabled`+`disabledReason`/`pollTimeoutMs`)。价格数字不得写进 descriptor、README 或 skill;以 `https://cloud.ai-mcn.tv/api/get_price_list` 为唯一真相,失败显示暂不可用但不阻断任务。
273
+
274
+ **接新工具 = 在 `TOOL_REGISTRY` 追加一个 descriptor 对象**(`tool list` 与分派自动生效),不新写命令编排。`--param`/`--params-json` 在 `buildPayload` 结果上逐字段合并覆盖。`cloud.ts`/`upload-cache.ts`/`chunk-upload.ts`/`media.ts` 只被复用、零改动。`list` 为保留字。工具长出多模式子命令 / SOP 检查点链 / 栏目风格注入时按 `mg`/`matrix` 先例毕业为独立命令。随包分发伞形 skill `skills/gtrk-tools/`(一个 skill 覆盖全族)。
272
275
 
273
- **接新工具 = 在 `TOOL_REGISTRY` 追加一个 descriptor 对象**(`tool list` 与分派自动生效),不新写命令编排。`--param`/`--params-json` `buildPayload` 结果上逐字段合并覆盖。`cloud.ts`/`upload-cache.ts`/`chunk-upload.ts`/`media.ts` 只被复用、零改动。`list` 为保留字。工具长出多模式子命令 / SOP 检查点链 / 栏目风格注入时按 `mg`/`matrix` 先例毕业为独立命令。随包分发伞形 skill `skills/gtrk-tools/`(一个 skill 覆盖全族)。
274
-
275
- `transcript` 已毕业为一级命令,并由 `skills/gtrk-transcript/` 独立承接自然语言触发和总结写回;不得再把这套工作流塞回 `gtrk-tools`。
276
+ `transcript` 已毕业为一级命令,并由 `skills/gtrk-transcript/` 独立承接自然语言触发和总结写回;不得再把这套工作流塞回 `gtrk-tools`。
276
277
 
277
278
  **local 型(`kind:"local"`)首个实例 = `mad`(一键剪 MAD,add-tool-mad)**:无 Key 可跑、可选云端加料。cloud 型走共享 runner 全链,local 型(复杂度上限标尺)在 `tool.ts` 的 local 分支**分派到自己的 handler**(`src/lib/mad/mad.ts::runMad`),不套 runCloudTool。mad 的新逻辑全住 `src/lib/mad/`(数据获取层 `data.ts` = 云端 manifest `/task/mad/manifest` + `~/.gitruck/mad-cache` 版本感知缓存 + sha256 自愈;`selector.ts` 六维规则选窗 + 种子 PRNG;`beat.ts` downbeat 量化 + 三级降级;`pool.ts` IR 分片按需拉取;`scan.ts` 素材扫描;`cloud-beat.ts` audio_music_analyze 接线 + 6004 失效重传)与 `src/lib/convert/`(IR→JSX,含 `madJsx` 母合成拼接 + `bake_ops.ts` 时序算子烘焙)——`tool-descriptors.ts`/`tool-runner.ts`/`cloud.ts` 零改动(红线)。产物仅 `.jsx`/仅支持 AE。
package/README.md CHANGED
@@ -325,6 +325,8 @@ gtrk transcript "D:/素材/采访视频.mp4" --lang zh-CN --out "D:/文字稿/
325
325
  | `--json` | 机读:stdout 只输出结果 JSON | 关 |
326
326
 
327
327
  > 落地产物 `dispatch.json` 三队列 → 下游消费:`mg`(MG 颗粒)→ `gtrk mg` 命令、`film_broll` → `gtrk matrix` 命令、`ai_drama` → `/gtrk-ai-drama` skill(产四段描述稿·中英分块,纯创作、无命令)。配套 skill `/gtrk-splitter` 产拆分稿。
328
+ >
329
+ > **派单条目自带 `span:{from,to}`**(该条目对应的 utterance 区间;`overlay` aux 派生条目写 **aux 自己的** span,可为主 beat span 的子区间)。**`track_st/track_ed` 是投影时刻的快照**——`gtrk mg` / `gtrk matrix` 消费时会**现场重投影**(见下),所以改完口播轨**不必**回来重跑 `gtrk split`,只有拆分稿本身变了才要重跑。
328
330
 
329
331
  ### `gtrk matrix` — B-roll 检索 + 候选铺轨
330
332
 
@@ -338,11 +340,21 @@ gtrk transcript "D:/素材/采访视频.mp4" --lang zh-CN --out "D:/文字稿/
338
340
  | `--lay <n>` | 候选铺轨数:平铺 N 条 B-roll 候选轨(`0` = 只出 plan 不铺轨) | `1` |
339
341
  | `--top-k <n>` | 每 query 候选数上限(覆盖派单 shots;服务端上限 50) | 派单值 |
340
342
  | `--material-class <c>` | 素材类型 `real_shot` \| `concept`(仅矩阵成员口;覆盖栏目策略) | 栏目策略 |
341
- | `--score-floor <f>` | 填充置信度地板:segment score 低于此值不采纳、留空露主轨 | `0.2` |
343
+ | `--score-floor <f>` | 填充置信度地板:segment score 低于此值不采纳、槽位留空——留空处**露黑底垫轨**(默认铺;除非 `--no-black-bed` 才露主轨)。调高会收缩取材池,整段铺不满即纯黑压口播,调完先看空洞告警 | `0.2` |
344
+ | `--no-black-bed` | 不铺纯黑底垫轨(默认铺一条) | 默认铺 |
345
+ | `--force-relay` | 候选轨已被你在客户端编辑过时仍强剥重铺(缺省会拒铺并保留那条轨)——**会删掉已确认原片的 `broll-raw-*` 素材登记、盘上原片成孤儿** | 关 |
342
346
  | `--out <file>` | ad-hoc 模式结果落文件 | stdout |
343
347
  | `--json` | 机读:stdout 只输出结果 JSON | 关 |
344
348
 
345
- > preview 代理 url 24h 过期,重跑即重签。
349
+ > **beat 窗口现场重投影**:派单消费模式在**发起第一次云端检索之前**,用「`transcript` × 当刻 `.gtrk`」重算每个 beat 的 `[track_st, track_ed]`,检索、`broll-plan.json` 与铺轨一律以重算值为准(`--lay 0` 同守;ad-hoc `search` 不受影响)。`dispatch.json` 里的时码只是**投影时刻快照**,仅在重投影不可行时兜底——**所以改完口播轨直接跑本命令即可,不必先重跑 `gtrk split`**。`--json` 恒出 `reprojection:{mode,degraded,reason?,drifted,max_offset,shrunk,dropped}`;重投影后**零存活**的 beat 会被跳过(不为它烧检索配额、也不铺)。重投影不可行(transcript 缺失 / 工程定位不到 / 主轨查不到口播素材)→ **降级用快照 + 告警 + `--json` 标注**,检索与 plan 照产、不硬崩;非 v1 工程的既有行为不变(plan 先落盘、随后版本门非 0 退出)。
350
+ >
351
+ > 候选的 `preview_url`/`cover_url` **不带签名、不会过期**(本地代理落盘后一律复用);带签名约 24h 过期的是**原片 `url`**,由客户端「确认原片」链路重签——**不必为「重签」重跑本命令**。
352
+ >
353
+ > **重跑会剥旧重铺,但不碰你改过的轨**:候选轨的身份按「素材前缀 + 上一轮登记指纹」认,不再认轨号(客户端保存会把 overlay 轨整体重编号)。一旦某条候选轨被判定「你编辑过」(改过 clip,或在客户端确认过原片使 material 变成 `broll-raw-*`),本次**整体不铺**:不剥任何轨、不追加新轨、`.gtrk` 逐字节不变,`broll-plan.json` 照常产出,命令给出「哪条轨 / 什么证据 / 下一步」并以非 0 退出码结束(`--json` 出 `{ok:false, refused:[…]}`)。要强行重铺加 `--force-relay`。
354
+ >
355
+ > **素材落盘自检**:写回工程之后自动查一遍 `materials[].path` 是不是真的都落盘了(**只读、只报不动**)。相对路径恒以 **`.gtrk` 文件所在目录**(`<产物目录>/gtrk/`)为基准解析。`--json` 出 `integrity:{ checked, counts, dangling:[…], danglingReferenced, danglingOrphan, external:[…], noPathIds:[…] }`——`dangling` 是工程自带素材的**悬空引用**(登记在、文件不在)全量清单,每条标出**是否被时间线引用**及引用位置(被引用 = 那一段没素材可放,比孤儿严重得多);绝对路径缺失另计 `external`(外接盘没挂载也会这样,不混进主判);http(s) 素材只计数、**不发网络请求**。**这是告知不是拦阻**:查出悬空不改 `ok`、不改退出码、不删任何素材条目或文件。悬空多半是历史遗留(如客户端「确认原片」下载中断),修法是在客户端重新确认原片或删掉那条 clip。没写回的运行(`--lay 0` / 拒铺 / 工程缺失)**不出 `integrity` 字段**——缺席 = 本次没查,不是「查过且干净」。
356
+ >
357
+ > **纯黑底垫轨**:默认在全部候选轨之下、口播主轨之上垫一条纯黑底轨(`struct_meta.broll.black_track` 记其 `track_index`),按已落成的 beat 包络整条铺满,使 B-roll 期间(含候选轨留空处)不漏出底下的口播画面。**代价是「黑底空洞」**:候选轨没填满的地方就是纯黑压口播,铺轨会把它算出来——`--json` 恒出 `lay.blackBedHoleSec` 与逐段的 `lay.blackBedHoles`,单段 ≥ 3s 或单 beat 占比 ≥ 15% 时另出一条非致命告警(不改退出码、不阻断铺轨),可据此调 `--score-floor`、改用 `--no-black-bed`、或到客户端手动补片。字节落 `assets/builtin/solid-000000-<W>x<H>.png`,与客户端内置纯色素材同 id 命名空间、幂等复用。删候选轨时别误删它;换片请拖到候选轨颗粒上、**别拖到黑底条上**——含拖拽保护的客户端会直接拒绝并提示,尚未升级到该版本的客户端会被误拖打出黑底破洞(该处漏口播)。不想要加 `--no-black-bed` 重跑即剥净。
346
358
 
347
359
  ### `gtrk mg` — MG 动态图颗粒(铺轨 / lint / status)
348
360
 
@@ -352,15 +364,25 @@ gtrk transcript "D:/素材/采访视频.mp4" --lang zh-CN --out "D:/文字稿/
352
364
  |---|---|---|
353
365
  | `--project <dir>` | oralcut / split 产物目录(定位 `split/dispatch.json` 与工程 `.gtrk`) | — |
354
366
  | `--dispatch <path>` | 显式指定 `dispatch.json`(非标准布局兜底) | 由 `--project` 推 |
355
- | `--only <beat>` | 只跑单 beat(主 + 其 `-aux<n>` 叠层颗粒一并选) | 全部 |
367
+ | `--only <beat>` | 只跑单 beat(收 **beat id** 如 `B12`、非 `composition_id`;主 + 其 `-aux<n>` 叠层颗粒一并选)。**真增量合并**:只重铺命中的那几颗,轨上其余已铺颗粒(连同手调)原样保留 | 全部 |
356
368
  | `--lint-only` | 只 lint 校验,不铺轨不写回 | 关 |
369
+ | `--replace-all` | 显式授权**重置整轨**:不走增量保留、整轨剥掉重铺——**会删掉轨上其余已铺颗粒** | 关 |
357
370
  | `--json` | 机读:人读日志转 stderr,stdout 只输出结果 JSON | 关 |
358
371
 
359
372
  - **铺轨**(`gtrk mg --project <dir>`):读 `dispatch.mg` → 逐 beat 从 `<project>/mg/<composition_id>.html` 取源颗粒 → lint → 铺进 `beat_track`,把 `struct_meta.mg` 原子写回 `.gtrk`(幂等登记自产轨 `lay_tracks`,重铺先剥旧自产物再 append、用户手加轨零连带)。「透明叠加 / 满屏底层」由颗粒 HTML 根 `background` 反推的 `opaque` 决定。缺 HTML / lint 失败的 beat 计入 `skipped`、不拦其余。
373
+ - **剥离面 ≠ 「本次铺什么」,也 ≠ 「登记轨全集」**:`--only <beat>` **只剥命中的那几颗**(真增量合并)——轨上其余已铺颗粒的 clip / 素材 / 登记条目**原样保留**,连同用户在 opencut 对它们的手调(保留的是既有 clip 原件,非照登记重建,故透明度 `opaque` 不会丢);这些保留条目**不重新 lint、不重新复制源 HTML**(工程自包含,`<project>/mg/` 下源文件删了也不影响)。全量重铺仍是「剥净再整轨重建」,**唯一例外**是本次派单里有、却因缺 HTML / lint 未过 / 重投影后零存活而**没铺成**的那几颗——它们上一轮的 clip 保留在轨上(不因为新的做坏了就把旧的也毁掉);反之**派单里已不存在**的已铺条目仍照剥(计划变更 ≠ 做坏了)。要连其余已铺颗粒一起剥掉重来:`--replace-all` 显式授权。
374
+ - **素材表不囤积**:素材的剥离键按「**自产身份 × 零引用**」判(自产 = `mg-`/`rrv-` 前缀 **或** 落在 CLI 独占的 `assets/mg/` 下且文件名在自产登记里),**不认客户端可改写的 `html_material` 前缀**——所以在 opencut 里编辑过工程之后重铺,旧素材照样剥得掉,`mg-` 素材数**恒等于轨上颗粒数**,历史遗留的重复 / 孤儿条目一并清掉。**非自产素材零连带**(`broll-*` / `ex-solid-*` / 你自加的,哪怕零引用也不碰);仍被存活 clip 引用的自产素材也不剥(不会剥出失联 clip);盘上 `assets/mg/` 的 html 副本从不删。
375
+ - **「一条都没定位到」不是清空指令**:`--only` 打空、`dispatch.mg` 为空/缺失、或本次条目全被 skip,**而轨上已有已铺颗粒**时,同样拒绝写回(那是派单或选择器出问题的信号)。确要清空加 `--replace-all`。首次铺轨(轨上本就没有已铺条目)不受此限,照常走完报 `laid=0`。
376
+ - **槽位窗口现场重投影**:铺轨与 lint 之前先用「`transcript` × 当刻 `.gtrk`」重算每条队列条目的 `[track_st, track_ed]`,之后 lint 的坑位包络(铁律⑦)与落轨 clip 时长一律以重算值为准(`--only` 同守;aux 派生颗粒按**自己的** span 重投影,不与主 beat 窗口混同)。`dispatch.mg` 里的时码只是**投影时刻快照**,仅在重投影不可行时兜底——**改完口播轨直接铺即可,不必先重跑 `gtrk split`**。`--json` 恒出 `reprojection:{mode,degraded,reason?,drifted,max_offset,shrunk,dropped}`(`--lint-only` 也有)。重投影后**零存活**的条目 skip 并计入 `skipped`(不复制 HTML、不按快照铺回去);重投影不可行(transcript 缺失 / 工程定位不到 / 主轨查不到口播素材)→ 降级用快照 + 告警 + `--json` 标注,退出码不变;工程**非 v1** 的既有行为不变(铺轨路径版本门非 0 退出、`--lint-only` 照旧出报告)。铺轨成功会把本次时码来源(`timecode_source` / `reprojected_at`)纯追加登记进 `struct_meta.mg`。
360
377
  - **lint**(`gtrk mg lint <颗粒.html> [--dispatch <path>]`):纯本地静态校验颗粒 HTML 的六铁律机器可判定子集(`<template>` 包裹、`data-composition-id` + 1920×1080、`gsap.timeline({ paused: true })`、`window.__timelines` 注册、无 `Math.random` / `Date.now`、自包含无相对外链、根 `background` 与 `opaque` 自洽…);给 `--dispatch` 时校验 `composition_id` 命中派单。任一致命项非 0 退出并逐条报因。
378
+ - **期望 id 一致性**(`1-cid-expect`,**致命**):HTML 内 `data-composition-id` 必须等于期望 id(铺轨=该条派单的 `composition_id`;`mg lint`=文件名,仅当它命中派单或形如 `…-B<数字>[-aux<n>]` 时比对,`./tmp.html` 这类改过名的副本不比对)。防的是「复制 `<id>.html` 改名时漏改内部 id」——落轨会写出以文件名命名的 clip/material,而文件注册的是另一个 `__timelines` 键、还与同名颗粒抢同一个样式作用域。
379
+ - **铁律⑦ tl 总长估长**(`7-fill-slot` / `7-no-estimate` / `7-infinite-repeat`,**恒非致命、不拦铺轨**):已知坑位包络时(铺轨逐颗;`mg lint --dispatch` 命中派单条目)对 GSAP 时间线做**静态下界估算**——逐调用降级,能解析的计入(`duration×(repeat+1) + repeatDelay×repeat`,`yoyo` 不加时长),表达式 position / 非字面量 duration 那条**跳过不计**(忽略若干调用仍是合法下界)。估长 < 包络 → 告警;一条都算不出 → 显式提示「无法静态估长,铁律⑦未校验,须真引擎 seek 验收」(**不静默**,「算不出」与「算过且通过」在输出上可区分);含 `repeat:-1` → 告警「无限循环令总长 Infinity、铁律⑦不可静态验证,请改按坑位算死的有限 repeat」。真判据永远是渲染引擎逐帧,本项只做提醒层。
380
+ - **回调与 seek 语义**(`x-callback-driven` / `x-engine-api-override` / `x-raf-interval`,**恒非致命、不拦铺轨**):对齐契约同名一节(2026-07-26 增补)。GSAP `seek(t)` 默认抑制回调 → 补间属性照常插值、但 `onUpdate` 里的 DOM 写入不执行,翻车形态是**画面定在初始态而非黑屏**。契约把保证压在**引擎侧**(定帧 MUST 用 `seek(t,false)` / `time(t)` / `progress(p)`),故颗粒**用回调驱动画面是合规写法**;lint 这三项只是**哨兵**:`x-callback-driven` = 回调写 DOM 且无任何 seek 兜底(有兜底则沉默,避免重复提醒);`x-engine-api-override` = 颗粒运行时覆写 `tl.seek` 或把 `__timelines[…]` 换成包装对象(会推翻引擎显式传的 `seek(t,true)`,且引擎改走 `time()`/`progress()` 即失效,属过渡态);`x-raf-interval` = 含 `requestAnimationFrame(` / `setInterval(`(自有时钟不被 seek,等于冻结)。三项 MUST NOT 致命——「用回调驱动画面」不是违规。
361
381
  - **status**(`gtrk mg status --project <dir>`):汇总 MG 流水线——`dispatch.mg` beat 总数 / 已产源 HTML 数 / 已铺进 `.gtrk` 数,并逐 beat 标注(缺 HTML / 已产未铺 / 已铺)。
362
382
 
363
- `--json` 输出:`{ ok, mode:"lay"|"lint"|"status", … }`(各模式带对应字段,如铺轨的 `laid` / `skipped`、status 的逐 beat 状态)。
383
+ `--json` 输出:`{ ok, mode:"lay"|"lint"|"status", … }`(各模式带对应字段,如铺轨的 `laid` / `skipped`、status 的逐 beat 状态)。铺轨模式另带 **`track_total`**(轨上现存已铺数)、**`kept`** / **`kept_ids`**(其中**上轮遗留**、本次没重铺的颗数与 `composition_id` 清单)与 **`removed`**(本次被剥的旧自产颗粒数)——`laid`(本次)、`track_total`(轨上共)与 `kept`(上轮遗留)**要一起读**,恒有 `track_total = laid + kept`;只看 `laid` 会让「铺 1 颗、剥 20 颗」跟「补铺 1 颗」长得一模一样,只看前两个又会漏掉「轨上那几颗不是这一轮的版本」这条代价(`kept_ids` 与 `skipped` 会重叠:这轮没铺成、上一轮那条还在轨上)。非全绿时另带机读 **`reason`**:`skipped`(部分没铺上)/ `empty_queue`(**拒写回**,工程未被改动,另带 `refused:true` 与 `blocked[]`)/ `no_project`(工程缺失未铺轨)。真写回过的运行另带 **`integrity`**(素材落盘自检,与 `gtrk matrix` 同名同形,口径见上节)。
384
+
385
+ > **退出码**:铺轨与 `--lint-only` 的 `ok:false` **一律连带非 0 退出**(含「有 beat 被 skip」这种循环中途的正常态)。agent 别把非 0 读成「命令崩了」——按 `reason` / `skipped` 判断即可。
364
386
 
365
387
  > **aux 叠层颗粒**:`gtrk split` 若在某 beat 的 `aux_layers` 派了 `overlay` 颗粒,会派生 `<beat>-aux<n>` 合成条目进 `dispatch.mg`——`gtrk mg` 一并铺,实现「同段既有底轨主视觉、又叠透明概念图解」。
366
388
  > **双读兼容**:`dispatch.mg`(读旧 `rrv_mg`)、源目录 `mg/`(读旧 `rrv/`)、素材前缀 `mg-`(读旧 `rrv-`)——去品牌化前的既有工程零迁移。
@@ -1,28 +1,146 @@
1
1
  # GSAP-emit 契约 v1 · HTML 动画颗粒的逐帧 seek 渲染合规
2
2
 
3
- > **契约版本**:gsap-emit v1(2026-07-10;2026-07-24 增补铁律 7「占满坑位 + 终态驻留」,主理人硬性规定;同日铁律 6 增补字体注册表命中规则,对齐 gitruck-infra change `align-render-font-contract`)。产 HTML 动画颗粒、经同合云渲染管线(html_animate_render)逐帧 seek 合成的 skill/工具,其产物 MUST 满足本契约。
3
+ > **契约版本**:gsap-emit v1(2026-07-10;2026-07-24 增补铁律 7「占满坑位 + 终态驻留」,主理人硬性规定;同日铁律 6 增补字体注册表命中规则,对齐 gitruck-infra change `align-render-font-contract`;**2026-07-26 增补「回调与 seek 语义」一节**——补此前留白,首次对**渲染引擎侧**提出 MUST 条款,对齐 change `define-seek-suppress-events-contract`;**同日该节核实状态转「已核实(行为层)」**——真渲染引擎(`@hyperframes/producer@0.6.101`)三帧实测回调可达、结论绑定该引擎版本;
4
+ > **2026-07-26 铁律 4 按真机实测改写**——实心底 MUST 下沉为根下第一个全幅子层、根元素 MUST 保持零视觉、overlay 颗粒 MUST 在根显式写 `background:transparent`,
5
+ > 附证据锚并消歧铁律编号,对齐 change `align-particle-solid-backdrop-contract`)。产 HTML 动画颗粒、经同合云渲染管线(html_animate_render)逐帧 seek 合成的 skill/工具,其产物 MUST 满足本契约。
4
6
  > **边界**:本契约只约束**机器可判定的管线消费属性**(封装/注册/确定性/自包含/依赖可达/禁 var()/字体名命中注册表)。画面长什么样——颜色、字体取值、构图、节奏——**一律由调用方按其栏目自身规则决定**,本契约不点名任何具体字体/颜色;文中示例取值均为中性占位。
5
7
 
6
8
  ## 原理(为什么不能用 CSS animation)
7
9
 
8
10
  渲染引擎逐帧渲染时,靠调用每个子合成在 `window.__timelines` 注册的 GSAP 时间线的 `.seek(t)` 把画面定格到第 t 秒。GSAP `paused` 时间线 = 可被外部 seek 的虚拟时钟 → 逐帧正确;纯 CSS `animation-delay` 动画不在 `window.__timelines` 里,引擎 seek 不到 → 画面冻结(实测)。
9
11
 
12
+ ## 回调与 seek 语义(2026-07-26 增补 · 补此前留白)
13
+
14
+ > **一句话**:颗粒**可以**用时间线回调(`onUpdate` 等)驱动画面;**保证这些回调在定帧时被触发是渲染引擎侧的义务**,不是颗粒作者的义务。
15
+ > 本节是该主题的**唯一口径来源**——契约引用(`skills/gtrk-style-maker/references/contracts-ref.md`)与消费侧检查(`gtrk mg lint`)的文案都指向这里,不得各自表述。
16
+
17
+ ### 一、GSAP 3.13.0 实测事实(写死在此,免后来者重测)
18
+
19
+ 复现方式:拉铁律⑤指定 CDN 的 `gsap.min.js` 到本地,Node `vm` 里造 `paused` timeline + proxy 对象 + 计数回调,**每种定位方式各新建一条 timeline 单独跑**(回调计数互不串)。
20
+
21
+ **样本一**——10s、`ease:"none"` 的单条 tween(`{v:0}` → `{v:100}`),分别 seek 到 t=5(中途)与 t=10(末端):
22
+
23
+ | 定位方式 | `onUpdate` / `onStart` | `onComplete` | 补间目标属性 |
24
+ |---|---|---|---|
25
+ | `tl.seek(t)`(默认,`suppressEvents` 缺省为 `true`) | **不触发** | **不触发**——连 `seek(10)`(到末端)也不触发 | 已更新(t=5 → `v=50.00`;t=10 → `v=100.00`) |
26
+ | `tl.seek(t, true)` | 不触发 | 不触发 | 已更新 |
27
+ | `tl.seek(t, false)` | 触发 | `seek(10,false)` 触发;`seek(5,false)` 不触发 | 已更新 |
28
+ | `tl.time(t)` / `tl.progress(p)` / `tl.totalTime(t)` | 触发 | 同上(按是否到末端) | 已更新 |
29
+
30
+ ⚠️ **`onComplete` 那两行必须分清**:默认 seek 下它**到末端也不触发**,这才是「被抑制」的证据;而 `false` 模式下 `seek(5,false)` 的 `onComplete` 不触发,只是「补间尚未走完」的正常语义。混为一谈,后来者会拿后者误判成「垫片无效」。
31
+
32
+ **样本二(含真实 DOM 写入 · 引擎侧条款的直接依据)**——同一 t=1.0 定帧点,补间在 t=1.0 处的目标属性期望值 75.0,`onUpdate` 体内做一次真实 DOM 写入:
33
+
34
+ | 定位方式(同一 t=1.0 定帧点) | `onUpdate` 触发次数 | 补间目标属性 | 回调内的 DOM 写入 |
35
+ |---|---|---|---|
36
+ | `tl.seek(1.0)`(默认) | **0 次** | 已更新为 **75.0** | **未执行** |
37
+ | `tl.seek(1.0, false)` | 1 次 | 75.0 | 正常 |
38
+ | `tl.time(1.0)` | 1 次 | 75.0 | 正常 |
39
+ | `tl.progress(0.5)` | 1 次 | 75.0 | 正常 |
40
+
41
+ 要害是**「值更新了、回调没跑」**:补间目标对象的属性照常插值,但写在 `onUpdate` 里的 DOM 写入一次都不执行。所以这种翻车**不是黑屏,是画面定在初始态**——本地播放器与客户端预览都完全正常,比黑屏更难发现。
42
+
43
+ 样本二同时给出条款一的**可行性依据**:三种「不抑制」写法(`seek(t,false)` / `time(t)` / `progress(p)`)行为**完全一致**,故对引擎侧的要求**可实现且不挑实现**——引擎爱用哪种用哪种,只要不是裸 `seek(t)`。
44
+
45
+ ### 二、条款
46
+
47
+ 1. **引擎侧(MUST)**:渲染引擎定帧时 **MUST 保证 GSAP 时间线回调可达**——MUST 用 `seek(t, false)` 或 `time(t)` / `progress(p)` / `totalTime(t)`,**MUST NOT 用默认的 `seek(t)`**。违反后果:补间目标属性照常更新、回调内的 DOM 写入不执行 → 画面**定在初始态而非黑屏**,本地播放器与客户端预览均看不出异常。
48
+ 2. **颗粒侧(合规声明)**:颗粒**可以**用时间线回调(`onUpdate` / `onStart` / `onComplete` / `onRepeat`)写 DOM / 属性来驱动画面——这是**合规**写法(相机推进、数值读数这类效果用纯属性补间难以表达)。其可达性由条款 1 承担;契约 **MUST NOT** 反过来要求颗粒作者自行保证。
49
+ 3. **颗粒侧(不应)**:颗粒**不应**在运行时覆写引擎所调用的 API——重新赋值 `tl.seek`、或把 `window.__timelines[…]` 换成包装对象。两条实测理由:① 覆写会推翻引擎**显式**传入的 `seek(t, true)`(实测:打上垫片后 `seek(5)` 与 `seek(6, true)` 两次调用回调都触发),颗粒无权静默否决引擎的意图;② 垫片只作用于 `seek`,引擎改走 `time()` / `progress()` 时**完全失效**,而作者不会知道。
50
+ 已存在的垫片属**过渡态**:**新颗粒不应再加**。**2026-07-26 更新**——条款 1 的前提已经真引擎核实(见下「三」),垫片赖以存在的「真空期」已结束,故已存在的垫片**可择期清理**(清理是**可做**、不是 MUST 做;删后须重跑 lint 并**重渲复验**)。垫片留着也无害:实测引擎既然本就不抑制回调,垫片对画面结果无影响,只会被 lint 记一条非致命的 `x-engine-api-override`。
51
+ 4. **作者侧应知(这不是甩锅)**:作者 MUST 知晓——回调可达性的保证方是**引擎侧**,且该保证当前的核实状态见下「三」(2026-07-26 起为**已核实(行为层)· 绑定引擎 0.6.101**)。`gtrk mg lint` 会对「回调驱动画面且无任何兜底」的颗粒给一条**非致命**提醒,那是**哨兵**(引擎换实现或失守时有人喊一声),**不是**「请作者自行保证 seek 下回调可达」——**核实之后哨兵照留**,因为结论绑死引擎版本,换版即须重测。
52
+
53
+ ### 三、核实状态:**已核实(行为层)· 2026-07-26 · 绑定引擎 `@hyperframes/producer@0.6.101`**
54
+
55
+ **把某条引擎行为写成 MUST,MUST NOT 被当作它已被核实**——二者在本节分别成文。本小节记的是**核实到了什么、以及没核实到什么**。
56
+
57
+ **结论:真渲染引擎在定帧时 `onUpdate` 回调可达,条款 1 的前提成立。** 结论**绑定上述引擎版本**,换版即失效、须重测。
58
+
59
+ - **核实方式(真引擎,非本地无头模拟)**:造一颗最小颗粒 `seekcb-probe`,**不打任何 seek 垫片**,画面正中的大号数字**只**由一条 tween 的 `onUpdate` 写入(proxy `{v:0}` → `480`,`ease:"none"`,16s),初值静态写死 `CB 000`;同屏并排一组**纯属性补间**对照物(方块横移 + 进度条拉长,均无回调),刻度尺与回调数字**同单位 0..480**,故单帧之内即可比对「应该是多少」与「实际是多少」。该颗粒先过 `gtrk mg lint`(`lint.ok=true`,`opaque=true`,只出 `x-callback-driven` 哨兵、无 `x-engine-api-override`),排除「因不合规才没渲」。走生产同款装配(`CompositionRender` 整轨路径 / 同底轨 `bed.mp4` / 1920×1080 / fps=60 / 15.97s / 958 帧分片 13 chunk)。
60
+ - **实测结果(三帧,肉眼判读整帧 PNG)**:
61
+
62
+ | 抽帧时刻 | 回调驱动的数字(onUpdate 唯一驱动) | 纯属性补间对照物 | 期望值 |
63
+ |---|---|---|---|
64
+ | t=2s | **CB 060** | 进度条/方块 @ 60 | 060 |
65
+ | t=8s | **CB 240** | 进度条/方块 @ 240 | 240 |
66
+ | t=14s | **CB 420** | 进度条/方块 @ 420 | 420 |
67
+
68
+ 三帧数字**各不相同**、**无一帧停在初值 `CB 000`**,且**逐帧与同单位对照物精确重合**——即回调不仅被触发,还是在**当前帧对应的时间点**上触发的(不是滞后一帧的陈值)。对照物同时移动,排除「整颗没渲/时间线没被定位」这一混淆解释。
69
+ - **证据留档**:帧 `D:/file/ops/_seekcb/SEEKCB-t2.png` / `-t8.png` / `-t14.png`;成片 r69 `/opt/gitruck/tmp/backdrop-ab-2026-07-26/SEEKCB.mp4`;颗粒 `D:/file/ops/_seekcb/P-seekcb.html`;渲染脚本 `_seekcb_render.py`。引擎版本取自渲染日志自报的 `"producerVersion":"0.6.101"`。
70
+ - **⚠️ 本次核实的边界(MUST NOT 越界引用)**:核实的是**可观测行为**(回调可达),**不是调用形态**。引擎仍是无源码第三方包,本轮**没有**读到定帧调用点,故**无法区分**它走的是 `seek(t,false)` / `time(t)` / `progress(p)`,还是压根不经 GSAP 定位 API 的自有推进方式。条款 1 里那串 API 白名单仍是**应然的实现约束**,只有「回调 MUST 可达」这个**结果**被真机背书。调用形态的核实仍由 gitruck-infra 侧联动 change `link-guarantee-seek-callback-reachability` 承接(读调用点 + 把「纯回调驱动无垫片颗粒渲两帧、像素 MUST 不同」挂进引擎版本变更门禁)。
71
+ - **旁证(本轮之前的间接证据,保留备查)**:gitruck-infra 自家 9 个 music_visualizer 模板 **9/9** 把整块画面挂在单条 tween 的 `onUpdate` 上、且 **0/9** 带任何垫片,走的正是同一渲染内核,而 `gtrk music-visualizer` 是**已上线**能力——若引擎默认抑制回调,这 9 个模板早该渲成静止首帧。本仓 exemplar b06/b07/b08 同理。本轮直接核实**与旁证同向**。
72
+ - **假设失效的后果(哨兵为何 MUST 留)**:结论绑定 `0.6.101`;引擎升版换调用方式 → 全部回调驱动型颗粒(MG 与 music_visualizer 模板)**一起静默冻结**,lint 全绿、退出码 0、无任何告警。故 `gtrk mg lint` 的 `x-callback-driven` 哨兵(条款 4)**MUST NOT 因本次核实而撤**——它防的正是「换版后无人喊一声」。
73
+
74
+ ### 四、连带风险:回调可达 = 逐帧反复触发
75
+
76
+ 条款 1 的另一面:逐帧 scrub 时 `onComplete` / `onStart` / `onRepeat` 会**反复触发**(每帧一次)。故颗粒 **MUST NOT** 把「只跑一次」的逻辑写进回调——累加计数、`push` 进数组、一次性 DOM 插入、抽样。那会把「静默冻结」换成「静默错乱」。
77
+
78
+ **2026-07-26 起这条从「假设的另一面」变成「已确证的现实风险」**:上「三」已实测该引擎**确实**在逐帧定位时触发回调,故本条不是防患于未然,而是**当下就在生效**的约束。
79
+
80
+ **推荐写法**:回调做成**幂等**的——每次都从补间状态**重算**整个画面,不依赖上一次的结果(真机与 exemplar 的 `render()` / `apply()` / `paint()` 均如此)。
81
+
82
+ ### 五、优先级
83
+
84
+ **本节口径优先于任何栏目指南的招式建议。** 栏目指南若与本节冲突,以本节为准。(当前二者同向:栏目指南推荐的 `onUpdate` 驱动写法在本节下**合规**,作坊侧**无需修订**——本节不向作坊侧派发任何待办。此条照写不误,它防的是将来的分歧。)
85
+
10
86
  ## 七条铁律(违反任一条 → 整片渲染失败 / 颗粒冻结 / 全黑 / 坑位内突兀消失)
11
87
 
12
88
  1. **`<template>` 包裹根元素**:`<template><div data-composition-id="<id>" data-width="1920" data-height="1080">…</div></template>`。编译器取的是 `<template>` 内容;裸 `<div>` 会被判 "empty or could not be parsed" 整片失败。(1920×1080 为**当前引擎版本约束**,分辨率参数化预留——升版时以本契约新版为准。)
13
89
  2. **GSAP `paused` 时间线 + 注册**:`var tl = gsap.timeline({paused:true}); … window.__timelines = window.__timelines || {}; window.__timelines["<id>"] = tl;`。`<id>` 必须等于根的 `data-composition-id`。
14
90
  3. **确定性**:禁用 `Math.random` / `Date.now` / 无参 `new Date()`(过不了引擎 StaticGuard)。要"随机感"用固定种子/解析式/递归生成(见「确定性配方」)。
15
- 4. **自包含 + 底色显式声明**:颗粒不依赖外部文件(除脚本 CDN)。**根底色透明与否必须显式声明**——全屏颗粒给根设明确 `background`(色值由调用方按栏目规则指定);叠加在底轨上的透明颗粒根**不设** background。不显式想清楚这一层,叠加合成必出错。
91
+ 4. **自包含 + 实心底下沉子层 + 透明度显式声明**(**2026-07-26 按真机实测改写**;原文「全屏颗粒给根设明确 `background`;透明颗粒根不设 background」**已作废**,见下方证据锚):颗粒不依赖外部文件(除脚本 CDN)。底色的**取值**由调用方按其栏目规则决定,本契约只管**摆在哪个元素上**。
92
+ - **① 实心底 MUST 下沉为根下第一个全幅子层**(`position:absolute;inset:0` 或等价的 `top/left/width/height` 铺满),**MUST NOT 写在根元素的 `style` 上**。理由是实测事实:**根元素的绘制属性在子合成挂载时被丢弃**,写在根上的实心底**一个像素都不落地**——成片里表现为「浮空面板」(前景照常渲出、底没了、底轨透出来),本地播放器与客户端预览都看不出。
93
+ - **② 根元素 MUST 保持零视觉**:根 `style` MUST NOT 出现任何会绘制像素的属性(实心 `background` / `border` / `box-shadow` / `outline` …)。写了不报错,但**不生效**,只会误导后来者以为底已经有了。
94
+ - **③ 透明与否 MUST 显式声明**:`background` 声明 MUST 至少出现一处——**满屏(不透明)颗粒**写在 ① 的全幅子层上;**透明叠加(overlay)颗粒** MUST 在**根**显式写 `background:transparent`,MUST NOT 靠「不写」表达透明。根上的 `transparent` 虽然同样不落成像素(它本就不绘制),但它是**给人和机器读的意图声明**——缺了它,作者与 `gtrk mg lint` 都无从区分「想透明」与「忘了想」。
95
+ - **④ 消费侧同源**:`gtrk mg lint` 的 `opaque` 推导面 = 「根 `style` ∪ 根下首个全幅子层 `style`」,与本条 ①③ 同源;两处皆无 `background` 声明才报 `4-bg-explicit`,满屏颗粒把实心底写在根上另报非致命 `4-bg-on-root`。
16
96
  5. **脚本用渲染机可达的 CDN(编译期内联)**:`<script src="https://lib.baomitu.com/gsap/3.13.0/gsap.min.js"></script>` 或由渲染管线 vendor 本地。⚠️ jsdelivr 在渲染服务器不稳(实测 compile 期 `fetch failed` → GSAP 未加载 → 整片全黑)。编译器**只内联 http(s) CDN、不内联相对本地路径**(写 `src="gsap.min.js"` 运行时 404)。
17
97
  6. **颜色/字体用字面值,禁 CSS `var()` 自定义变量;字体名 MUST 命中服务端注册字体表**:编译器/挂载不可靠地解析 var()(字体映射把 `var(--font-body)` 当字面字体名;颜色 var() 不应用 → 整片全黑,实测)。直接写字面值(如 `#RRGGBB` / `'某字体名'`);SVG 属性里同样禁 var()。栏目级换色/换主题 = **生成期**替换字面值(查调用方自己的词表/token 注入),不是运行时变量。**字体名规则(2026-07-24 增补)**:font-family 的每个具名家族 MUST 逐字符命中渲染服务端注册字体表(gitruck-infra 仓 `utils/assets/text/classic_template/font_manifest.json`,中英别名等价),并 SHOULD 以 `sans-serif`/`serif` 通用族收尾兜底;表外名字渲染不失败但**字形不保证**(服务端 fail-open 系统回退,2026-07-24 真机实锤:错名导致宋体被渲成回退黑体)。具体选哪款仍由调用方栏目规则决定,本契约不点名。
18
- 7. **占满坑位 + 终态驻留(2026-07-24 主理人硬性规定)**:颗粒时间线总长 MUST ≥ 它在成片中的**坑位时长**(落轨 clip 的实际时长,通常 = 派单槽位包络 `track_ed − track_st`,**不是** `duration_hint`);动画主叙事播完后,颗粒 MUST 以「**定格保持**」或「**有限次循环**」驻留到坑位末尾——坑位内任意时刻(含最后一帧)核心内容必须可见。**禁止**「整体渐隐到空 / 全局退场 / 清空画面」类收尾:渐隐会与剪辑层转场冲突,淡出与否由剪辑/装配层决定,不在颗粒内做。局部元素可按叙事退场(黯淡/让位),但画面在坑位内不得归零;**定格不动是完全合法的终态**(不必为凑动作密度在尾段硬加动画)。违反表现 = 观感上「动画一过完整个颗粒突兀消失」(2026-07-24 回声定位真机实测)。
98
+ 7. **占满坑位 + 终态驻留(2026-07-24 主理人硬性规定;2026-07-26 增补无限循环定性)**:颗粒时间线总长 MUST ≥ 它在成片中的**坑位时长**(落轨 clip 的实际时长,通常 = 派单槽位包络 `track_ed − track_st`,**不是** `duration_hint`);动画主叙事播完后,颗粒 MUST 以「**定格保持**」或「**有限次循环**」驻留到坑位末尾。**禁用无限循环 `repeat:-1`(2026-07-26 增补,补此前留白)**:无限循环让时间线总长为 `Infinity`,本条「总长 ≥ 坑位」就**变得不可验证**(机器与人都无从判断它是否真按坑位算过),且掩盖「作者根本没算坑位」这件事。循环次数 MUST **按坑位算死**——`repeat = ceil((坑位时长 − 循环起点) / 单圈时长) − 1`,宁可多算一两圈(末尾被 clip 裁掉无害),也不许写 `-1` 蒙混——坑位内任意时刻(含最后一帧)核心内容必须可见。**禁止**「整体渐隐到空 / 全局退场 / 清空画面」类收尾:渐隐会与剪辑层转场冲突,淡出与否由剪辑/装配层决定,不在颗粒内做。局部元素可按叙事退场(黯淡/让位),但画面在坑位内不得归零;**定格不动是完全合法的终态**(不必为凑动作密度在尾段硬加动画)。违反表现 = 观感上「动画一过完整个颗粒突兀消失」(2026-07-24 回声定位真机实测)。
99
+
100
+ > **⚠️ 铁律编号的唯一定义在本文件。** 本契约的**铁律 7 = 「占满坑位 + 终态驻留」**,这是框架侧对该条号的**唯一**定义。
101
+ > 创作侧(栏目 skill / 作坊)的模板与 references **MUST NOT 另立同号铁律**——引用请写「gsap-emit v1 铁律 7」并指回本文件,
102
+ > 自己的栏目约束请用**栏目注记**或另起独立编号空间。
103
+ > **已知撞号(2026-07-26 记录,收口中)**:某栏目 references 曾把「根元素零视觉」编为其「铁律 7」、同一条在其模板里又编为「铁律③」——
104
+ > 该约束现已**升为本契约的铁律 4**(见上),栏目侧的两个同名条号 SHALL 改为引用铁律 4,不再另立。
105
+
106
+ ### 铁律 4「实心底下沉子层」的真机证据锚(2026-07-26)
107
+
108
+ 本条断言的是**渲染引擎行为**(「根元素的绘制属性在挂载时被丢弃」),故按契约库要求附可追溯证据锚:
109
+
110
+ - **结论**:根 `style` 的 `background` **不落成像素**;同一颗粒把同一底色改写到根下首个全幅子层则**满屏落地并盖住底轨**。
111
+ - **实验日期 / 机器**:2026-07-26 · r69(真渲,非本地无头模拟)。
112
+ - **引擎版本口径**:hyperframes CLI `0.6.101` · `@hyperframes/producer` `0.6.101` · chrome-headless-shell `linux-131.0.6778.85`。
113
+ - **设计**:同颗粒母本造两个变体,**唯一变量 = 实心底摆在根 `style` 还是根下首个全幅子层**,其余逐字节相同;
114
+ 底色临时取高饱和标志色以便与「根合成兜底黑」区分;底轨用一段有明显画面、非黑的真人镜头;抽同一时间点 `t=8.0s` 的帧,
115
+ 取四角 `crop=40:40` 于 `(20,20) (1860,20) (20,1020) (1860,1020)` 后 `scale=1:1` 读像素。
116
+ - **观测**(整轨拆分渲染路径,两格同素材同引擎同批)。
117
+ ⚠️ 下表的六位数字是**抽帧读出的 RGB 像素读数**(实验观测),**不是本契约规定的任何底色**——
118
+ 轴一边界照旧:契约不点名任何色值,底色取值永远由调用方栏目决定:
119
+
120
+ | 变体 | 四角像素 | 判读 |
121
+ |---|---|---|
122
+ | 实心底写**根 `style`** | `dfdad4 / fafcfb / 4e4f52 / f9fbfb` | ≈ 底轨基线 → **底丢失、底轨透出** |
123
+ | 实心底写**根下首个全幅子层** | `01ff81 / 00fe80 / 01fd80 / 01fd80` | ≈ 标志色 → **底存活、满屏盖住底轨** |
124
+ | 底轨本身(阴性对照) | `e0dbd4 / fbfdfc / 4f5052 / fbfdfd` | 四角始终高亮非黑,对照有效 |
125
+
126
+ - **阳性对照(关键,排除平凡解)**:「写根」那格的整帧里颗粒**前景元素完整渲出**(标题文字、网格、连线、色块都在)——
127
+ 所以它**不是「颗粒没渲上」,而是「颗粒渲上了、根 `background` 被丢掉」**。缺这一步,两种情况在像素上同解。
128
+ - **复现方式**:造上述两变体 → 同一批走整轨拆分渲染 → `ffmpeg -ss 8.0 -i <out>.mp4 -frames:v 1 -vf "crop=40:40:<x>:<y>,scale=1:1" -f rawvideo -pix_fmt rgb24 - | xxd -p` 读四角。
129
+ - **历史**:本条现象最早由创作侧于 **2026-07-15** 记载(一颗满屏颗粒的根 `background` 丢帧、成片只剩浮空面板),
130
+ 当时未进框架契约,框架正本反而长期要求「写在根上」——2026-07-26 的实验复现并坐实了该记载,本条据此改写。
131
+ - **相关跨仓 change**:`gitruck-cli` / `align-particle-solid-backdrop-contract`(本条的来源);
132
+ `gitruck-infra` / `add-html-animate-opaque-fullscreen-cover`(**另一个独立问题**:渲染侧要不要按 `opaque` 位替颗粒兜一层黑底)。
133
+ ⚠️ 两者**互不依赖**:上表的观测正是在后者**未 apply** 时取得的——即**本条的修法(下沉子层)当场生效,不需要渲染侧任何配合**。
19
134
 
20
135
  ## 颗粒骨架(中性模板)
21
136
 
22
137
  ```html
23
138
  <template id="p">
139
+ <!-- 铁律4:根 MUST 零视觉。这里的 background 恒为 transparent(显式声明,不是省略),实心底一律下沉到下面的 .bgfill 子层 -->
24
140
  <div data-composition-id="<id>" data-width="1920" data-height="1080"
25
- style="position:absolute;inset:0;/* 底色显式声明:全屏颗粒填你栏目的底色,透明叠加则删除 background */background:<你的底色>;overflow:hidden;font-family:'<你的字体·须命中服务端 font_manifest.json>',sans-serif;">
141
+ style="position:absolute;inset:0;background:transparent;overflow:hidden;font-family:'<你的字体·须命中服务端 font_manifest.json>',sans-serif;">
142
+ <!-- 铁律4①:实心底 MUST 是根下第一个全幅子层。满屏颗粒填你栏目的底色;透明叠加(overlay)颗粒把整行删掉 -->
143
+ <div class="bgfill" style="position:absolute;inset:0;background:<你的底色>;z-index:0;"></div>
26
144
  <style> [data-composition-id="<id>"] .xxx{ … } </style> <!-- 样式用属性选择器作用域,防跨颗粒污染 -->
27
145
  <svg viewBox="0 0 1920 1080" preserveAspectRatio="xMidYMid meet" style="position:absolute;inset:0;width:100%;height:100%;">…</svg>
28
146
  <script src="https://lib.baomitu.com/gsap/3.13.0/gsap.min.js"></script>
@@ -38,6 +156,11 @@
38
156
  </template>
39
157
  ```
40
158
 
159
+ > **骨架里 `.bgfill` 那一行的三个要点**:① 它 MUST 是**根下第一个**子元素(后续前景层自然压在它之上,不必给前景排 z-index);
160
+ > ② 它 MUST **全幅**(`position:absolute;inset:0`,或等价的 `top/left/width/height` 铺满)——不铺满就不是「实心底」;
161
+ > ③ 类名 `bgfill` 只是**惯例**,其底色写在**行内 style** 上(不靠 `<style>` 里的类规则),故不受「样式作用域」那条专属坑影响;
162
+ > 换名字不违约,但 `gtrk mg lint` 认的是「根下首个全幅子层的 `background`」这个**结构**,不认类名。
163
+
41
164
  缓动可用 CustomEase 精确还原你栏目自己的 cubic-bezier(缺插件时给近似回退)——bezier 数值属于栏目审美,本契约不规定。
42
165
 
43
166
  ## 确定性配方(替代 random)
@@ -53,6 +176,11 @@
53
176
  2. 走渲染管线(html_animate_render)渲染;
54
177
  3. 抽不同时间点的帧**比对应当不同**(相同=冻结=铁律没守住)。客观自检:根有 `<template>`、`window.__timelines["<id>"]` 已注册且 id 匹配、无 random/Date、tl 总长 ≥ 颗粒时长。
55
178
  > ⚠️ 不要用「本地等价 seek 脚本 / Node 无头模拟」替代真引擎渲染——它不经真编译+挂载,测不出 var()-不解析、CDN-内联失败、StaticGuard 这类只在真引擎暴露的问题(实测教训:本地等价测试报 OK,真引擎全黑)。
179
+ >
180
+ > ⚠️ **客户端预览对「底色 / 透明度」类问题结构性失明,MUST NOT 用作这类验收的判据**(2026-07-26 增补)。
181
+ > 客户端预览会按 clip 登记的 `opaque` 位**给颗粒根盒强行打底**(`!important`,恒胜过颗粒自身的设计期底色)——
182
+ > 于是无论根 `background` 有没有被丢弃、有没有实心底子层,**预览看到的都是「按登记值应该长的样子」**。
183
+ > 铁律 4 的证据锚正是靠**真渲染出片抽帧**取得的,不是靠预览。预览截图只作对照留档,不入判据。
56
184
 
57
185
  ## 专属坑
58
186
 
@@ -60,4 +188,4 @@
60
188
  - **`<template>` 内的 `<script>` 默认不执行**——引擎把 template 内容克隆进文档后才执行;本地直接开浏览器不会跑,必须经引擎/player。
61
189
  - **transform-origin(SVG)**:缩放 `<g>` 用 GSAP `svgOrigin:"x y"`(SVG 用户坐标),别用 CSS transform-origin。
62
190
  - **时间线总长 ≥ 坑位时长**:颗粒 tl 总时长 ≥ 落轨 clip 时长(坑位包络),否则 seek 越界(已升格为铁律 7,含终态驻留要求)。
63
- - **别用 `requestAnimationFrame`/`setInterval` 驱动画面**——不被 seek,等于冻结。所有视觉变化必须挂在 tl 上。
191
+ - **别用 `requestAnimationFrame`/`setInterval` 驱动画面**——不被 seek,等于冻结。所有视觉变化必须挂在 tl 上。(与「回调与 seek 语义」一节同源:任何**不经 tl** 的自有时钟都不被定帧驱动;而挂在 tl 上的回调是否被触发,则由该节的引擎侧条款保证。`gtrk mg lint` 对本条给**非致命**项 `x-raf-interval`——静态正则分不清「驱动画面」与其它用途,故只提醒不拦。)