Skip to content

Commit 90dfad5

Browse files
authored
round-25 self-review: spoken-form silence alignment (matchBoundaries to tools), digit-bounded year regex, small cleanups (1.9.12)
1 parent c2c44ca commit 90dfad5

9 files changed

Lines changed: 72 additions & 46 deletions

File tree

‎plugins/Wzdhehe/html2video-for-mcode/CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
- **Year-reading gate (plan-timings, zh/yue):** a clause whose effective spoken text (`say ?? text`) contains a four-digit year followed by 年 gets a warning that names the digit-by-digit broadcast form and hands over the ready-to-paste `say` value (`加 say:"二零二六年…"`). The gate is deliberately narrow — whether a number reads digit-by-digit or as an integer is intent (`2026年` digit-by-digit, `2026点` as an integer), so only the unambiguous year shape is gated and the rest of the number discipline lives in the docs (SKILL.md schema section + tts-and-timing.md, with all three reported examples).
99
- **Tests +1 → 274 in fifteen files:** the year gate (no-`say` named with the paste-ready form / with-`say` silent / `en` not over-warned / `say` passed through to timings verbatim and never invented for `say`-less clauses), and the smoke pipeline now runs `build-video --asr` on a `say`-bearing clause and asserts the checklist expects the spoken form while the srt keeps the display form (`say` must not leak into subtitles). Each guard red-proofed: disabling the year gate, dropping the `say` propagation, or reverting the checklist to `c.text` reddens exactly its own case.
1010
- **Round-24 self-review corrections (same release):** the two-axis review of this increment found four real items in the fix itself, all closed here — ① **clause-start allocation weighted by the display form**: the audio speaks `say` ("82.3%" shows 5 chars, "百分之八十二点三" speaks 8), so time weights, per-clause `chars` and the pacing check now use `say ?? text` (subtitle-wrap geometry still uses `text` — the pill is on screen); the year-gate test now pins the weighted start against both candidate formulas. ② the year gate's regex only matched `19xx/20xx` while the comments claimed "four-digit year" — now any `\d{4}年` (1897 warns too). ③ the zh-guard used the layout metric `emPer===1` as a language proxy — replaced by an explicit `yearGate` flag on `zh`/`yue` (unknown-language fallback stays ungated). ④ two `evals.json` cases still taught the old "write the spoken form into the narration text" move (the EN number-swap line and the ASR-mismatch fix) — both now teach the `say` mechanism. Each correction red-proofed (weight reverted → weighted-start assert red; regex reverted → 1897 red; gate off → warn red).
11+
- **Round-25 self-review corrections (same release, tests +1 → 275 in fifteen files):** five smaller items — ① **`check-timing`'s silence-to-clause alignment still used the display form**: `matchBoundaries` moved to `tools.mjs` as the single source and now counts pause punctuation on `say ?? text` (with `say`/`text` punctuation disagreeing, exact mode misaligned and `--calibrate` would write back wrong starts); unit-tested with a fixture where the two forms produce different methods/values. ② the year regex matched four digits **inside** longer digit runs ("12026年" sliced to "2026年" and suggested a digit-dropping `say`) — now digit-bounded on both sides, with a non-warn case pinned. ③ a self-referencing comment cited a line number that the same release had moved. ④ `LANG_CFG` now states `yearGate` explicitly on every branch and shares one `zh`/`yue` literal. ⑤ the `evals.json` ASR-mismatch case now says the `say` rewrite must actually change the pronunciation (九成二-style), not restate what TTS already says (百分之九十二 wouldn't fix the mishearing). Both behavioural corrections red-proofed (spoken-marks reverted → alignment test red; boundary reverted → the 12026 case red).
1112

1213
## 1.9.11 — 2026-09-28
1314

‎plugins/Wzdhehe/html2video-for-mcode/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ node --test "plugins/Wzdhehe/html2video-for-mcode/skills/html2video-for-mcode/te
231231
node --test "tests/*.test.mjs"
232232
```
233233

234-
274 tests in fifteen files: `safe-paths` (malicious slide ids / paths, canary intactness, symlink
234+
275 tests in fifteen files: `safe-paths` (malicious slide ids / paths, canary intactness, symlink
235235
escapes including a **dangling** link that must be caught rather than skipped), `no-clobber` (refusing to overwrite), `endpoint-allowlist` (key never leaves the official
236236
hosts — plus a local server that proves the gate sits before the request, and provider selection:
237237
`mmx speech transcribe` default vs `--provider api` fallback, with an mmx-shim integration pair

‎plugins/Wzdhehe/html2video-for-mcode/README.zh-CN.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ node --test "plugins/Wzdhehe/html2video-for-mcode/skills/html2video-for-mcode/te
145145
node --test "tests/*.test.mjs"
146146
```
147147

148-
十五个文件共 274 例:`safe-paths`(恶意 slide id / 路径、canary 完好性、符号链接逃逸 —— 含**悬空**链接必须被拦下,而不是当"不存在"跳过)、`no-clobber`(覆盖拒绝)、`endpoint-allowlist`(Key 不离开官方域,并用本地服务器证明闸门在请求之前;外加提供方选择:`mmx speech transcribe` 缺省与 `--provider api` 后备,mmx shim 集成对断言参数映射与临时目录中转/清理)、`fetch-policy`(SSRF、`file://`、重定向与文件名规则、**IPv4-mapped IPv6 十六进制形式**)、`connect-binding`(DNS 失败即拒(fail-closed),SSRF 否决挂到**建连那次** lookup —— 出网走 `policyGet`、浏览器流量走本地否决代理;rebinding 用例以"目标收到 **0 个请求**"为判据)、`preview-page`(快照注入实测延迟、`base` 顺序、自包含无外链、越界拒绝、无计时器、**竖版画布、iframe 强制相对解析**)、`tokens-fx`(模板里每个入场动画的关键帧必须声明 `opacity`、`no-fx` 必须重置基础态、`--upgrade-css` 幂等)、`chart-kit`(模板里真带着图表工具箱:keyframes / 注册属性 /「关动效 = 终态」不变量)、`table-kit`(表格原语的可读性硬指标:正文号、行高 ≥72px、涨跌走令牌、矩阵高亮列覆盖表头)、`css-kit`(**整段受管区**:陈旧 / 缺失 / 重复 / 定界符被改坏 / 区外还留着旧副本, 五种都要检出并原地修好 —— 区外副本会在层叠上压过受管区, 这正是本轮重写的起因; 项目端覆写按条保留、老格式裸文本原地包裹而不重复、`--check-css` 退出码、CRLF 不误报、病态互踩一次到位、超大输入拒绝 —— 外加复查在新机制自己身上揪出的两个假绿灯: 受管区是最新的、但区外还留着一份**主体副本**时以前报 `ok`(现在按主体形状识别并报 `legacy-outside`); 项目覆写的 `ok` 提示会点名它**在受管区之前还是之后**(之前则层叠上输, 之后才赢))与 `render-smoke`(init → 对时 → 静态闸门 → 截图 → 成片全链,**still 复截后静态回退必须点名警告**,外加**升级前后画面真的变了的像素级证明**), 以及 capture 的布局几何自检(溢出/顶部被裁点名数字)与骨架 safe center 契约。`review-round2`(输出收监 canary:项目内符号链接不得写到项目外;本地服务器证明被拒目标收到 0 个请求;单引号属性;ASR 请求失败必须计入失败;项目内 ffmpeg 发现)与 `cover-transition`(封面 + `attached_pic` 流 + 首帧非黑 + 硬切/溶解两种模式下切页点均无黑帧且总时长不变)与 `review-round3`(七个功能缺陷:`--calibrate` 不许写回半份实测、`--topic <值> <项目目录>` 不许把值当目录、`--json` 位置无关、`--min WxH`、`--get` 全失败退非零、`capture --mode/--dsf` 与 `grab-frames --at` 在入口拒绝非法值、字幕带闸门只对定位元素生效、`--dry-run` 打完计划必须退 0、裸字符串 `transition` 在闸门与渲染器里同义;以及 check-slides / capture / build-video 的陈旧 CSS 闸门, 外加图表结构闸门: 百分比柱体裸放 flex column 会被点名(flex-shrink 压扁, 应包进 .chart-plot-cell))与 `subtitles-invalidate`(删掉 clauses 或 `--no-subs` 重建后, 成片里不得再有旧字幕 —— 像素级断言; 外加字幕关键帧形状不变量、静帧满亮/跨句残留判据、以及封面/静帧基底不得烙字幕(2026-09-22 字幕回归);全套用例需要装好 ffmpeg、ffprobe 与 Chromium(`PATH` 上,或本仓/项目 `node_modules` 里 —— `npm i ffmpeg-static ffprobe-static playwright`):缺了它们,需要这些工具的用例会**带着明确原因跳过**(缺工具、文件系统不允许建 junction),不会假装通过;渲染脚本本身则会明确报 `找不到 ffmpeg` 退出,不会假装跑完半条流水线。scoped workflow `.github/workflows/html2video-for-mcode-smoke.yml`(**位于 MiniMax-Code-Plugins monorepo**)会在插件 PR 与 main 上装齐依赖并把全部用例真跑一遍。
148+
十五个文件共 275 例:`safe-paths`(恶意 slide id / 路径、canary 完好性、符号链接逃逸 —— 含**悬空**链接必须被拦下,而不是当"不存在"跳过)、`no-clobber`(覆盖拒绝)、`endpoint-allowlist`(Key 不离开官方域,并用本地服务器证明闸门在请求之前;外加提供方选择:`mmx speech transcribe` 缺省与 `--provider api` 后备,mmx shim 集成对断言参数映射与临时目录中转/清理)、`fetch-policy`(SSRF、`file://`、重定向与文件名规则、**IPv4-mapped IPv6 十六进制形式**)、`connect-binding`(DNS 失败即拒(fail-closed),SSRF 否决挂到**建连那次** lookup —— 出网走 `policyGet`、浏览器流量走本地否决代理;rebinding 用例以"目标收到 **0 个请求**"为判据)、`preview-page`(快照注入实测延迟、`base` 顺序、自包含无外链、越界拒绝、无计时器、**竖版画布、iframe 强制相对解析**)、`tokens-fx`(模板里每个入场动画的关键帧必须声明 `opacity`、`no-fx` 必须重置基础态、`--upgrade-css` 幂等)、`chart-kit`(模板里真带着图表工具箱:keyframes / 注册属性 /「关动效 = 终态」不变量)、`table-kit`(表格原语的可读性硬指标:正文号、行高 ≥72px、涨跌走令牌、矩阵高亮列覆盖表头)、`css-kit`(**整段受管区**:陈旧 / 缺失 / 重复 / 定界符被改坏 / 区外还留着旧副本, 五种都要检出并原地修好 —— 区外副本会在层叠上压过受管区, 这正是本轮重写的起因; 项目端覆写按条保留、老格式裸文本原地包裹而不重复、`--check-css` 退出码、CRLF 不误报、病态互踩一次到位、超大输入拒绝 —— 外加复查在新机制自己身上揪出的两个假绿灯: 受管区是最新的、但区外还留着一份**主体副本**时以前报 `ok`(现在按主体形状识别并报 `legacy-outside`); 项目覆写的 `ok` 提示会点名它**在受管区之前还是之后**(之前则层叠上输, 之后才赢))与 `render-smoke`(init → 对时 → 静态闸门 → 截图 → 成片全链,**still 复截后静态回退必须点名警告**,外加**升级前后画面真的变了的像素级证明**), 以及 capture 的布局几何自检(溢出/顶部被裁点名数字)与骨架 safe center 契约。`review-round2`(输出收监 canary:项目内符号链接不得写到项目外;本地服务器证明被拒目标收到 0 个请求;单引号属性;ASR 请求失败必须计入失败;项目内 ffmpeg 发现)与 `cover-transition`(封面 + `attached_pic` 流 + 首帧非黑 + 硬切/溶解两种模式下切页点均无黑帧且总时长不变)与 `review-round3`(七个功能缺陷:`--calibrate` 不许写回半份实测、`--topic <值> <项目目录>` 不许把值当目录、`--json` 位置无关、`--min WxH`、`--get` 全失败退非零、`capture --mode/--dsf` 与 `grab-frames --at` 在入口拒绝非法值、字幕带闸门只对定位元素生效、`--dry-run` 打完计划必须退 0、裸字符串 `transition` 在闸门与渲染器里同义;以及 check-slides / capture / build-video 的陈旧 CSS 闸门, 外加图表结构闸门: 百分比柱体裸放 flex column 会被点名(flex-shrink 压扁, 应包进 .chart-plot-cell))与 `subtitles-invalidate`(删掉 clauses 或 `--no-subs` 重建后, 成片里不得再有旧字幕 —— 像素级断言; 外加字幕关键帧形状不变量、静帧满亮/跨句残留判据、以及封面/静帧基底不得烙字幕(2026-09-22 字幕回归);全套用例需要装好 ffmpeg、ffprobe 与 Chromium(`PATH` 上,或本仓/项目 `node_modules` 里 —— `npm i ffmpeg-static ffprobe-static playwright`):缺了它们,需要这些工具的用例会**带着明确原因跳过**(缺工具、文件系统不允许建 junction),不会假装通过;渲染脚本本身则会明确报 `找不到 ffmpeg` 退出,不会假装跑完半条流水线。scoped workflow `.github/workflows/html2video-for-mcode-smoke.yml`(**位于 MiniMax-Code-Plugins monorepo**)会在插件 PR 与 main 上装齐依赖并把全部用例真跑一遍。
149149

150150
## 排错
151151

‎plugins/Wzdhehe/html2video-for-mcode/skills/html2video-for-mcode/evals/evals.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"id": 4,
3434
"name": "asr-mismatch",
3535
"prompt": "ASR 校验发现第 5 张转写里 '92%' 听成了 '九二', 数字对不上, 怎么处理?",
36-
"expected_output": "text(字幕)保持 '92%' 不动, 给该句加 say:'…百分之九十二…'(显示/发音分离: 发音形态只进 say, 不上屏)→ 只重做该段 TTS → 重跑 plan-timings → 删 build/frames/05 与 out/slide-05.mp4 → 该张重 capture + build-video, 不整片重做。若确认是同音字级微小差异可接受, 数字/产品名不一致必须返工。",
36+
"expected_output": "text(字幕)保持 '92%' 不动, 给该句加 say 换一个更清晰的读法(如 say:'…增长九成二…' —— 显示/发音分离: 发音形态只进 say, 不上屏; 换词要真的改变发音, 念作'百分之九十二'的同义改写解决不了听错)→ 只重做该段 TTS → 重跑 plan-timings → 删 build/frames/05 与 out/slide-05.mp4 → 该张重 capture + build-video, 不整片重做。若确认是同音字级微小差异可接受, 数字/产品名不一致必须返工。",
3737
"files": []
3838
},
3939
{

‎plugins/Wzdhehe/html2video-for-mcode/skills/html2video-for-mcode/scripts/check-timing.mjs‎

Lines changed: 3 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import fs from 'node:fs';
77
import path from 'node:path';
88
import { spawnSync } from 'node:child_process';
9+
import { matchBoundaries } from './tools.mjs';
910
import { positionalDir, requireTool, safeId, safeOut, safeRel, validateScriptPaths, validateTimingsIds } from './tools.mjs';
1011

1112
const argv = process.argv.slice(2);
@@ -38,40 +39,8 @@ function detectGaps(file, tts) {
3839
}
3940

4041
// 句间边界匹配: 静音段里混着句内逗号停顿, 不能假设段数=句数-1。
41-
// 精确模式: 句读标点总数-1 == 静音段数 → 按标点累积下标对齐(最可信)。
42-
// 最近邻模式: 否则在估算时刻 ±1s 窗内取最近的静音段末端, 时序单调; 找不到就报未测(宁缺勿错)。
43-
const PAUSE_PUNCT = /[,,、;;::。!?!?…]/g;
44-
function matchBoundaries(gaps, clauses) {
45-
const nBound = clauses.length - 1;
46-
if (nBound <= 0) return { method: 'none', meas: [] };
47-
const marks = clauses.map(c => (c.text.match(PAUSE_PUNCT) || []).length);
48-
const totalMarks = marks.reduce((a, b) => a + b, 0);
49-
const meas = new Array(nBound).fill(null); // 精确模式自己一份
50-
if (gaps.length === totalMarks - 1 || gaps.length === totalMarks) {
51-
let cum = 0, gi = 0;
52-
for (let k = 0; k < nBound; k++) {
53-
cum += marks[k]; // 第 k 边界 = 前 k 句累积标点数对应的那个停顿
54-
const idx = Math.min(cum - 1, gaps.length - 1);
55-
if (idx >= gi) { meas[k] = gaps[idx].end; gi = idx + 1; }
56-
}
57-
if (meas.every(v => v != null)) return { method: 'exact', meas };
58-
}
59-
// 最近邻: **重新分配**, 绝不从上面那份里继承已填值 —— 继承的话写回的就是"精确 + 最近邻"的
60-
// 混合值, 而 method 只报 nearest, 属于谎报方法(2026-09-18 复查 D1: 精确模式失败即走到此处)。
61-
const near = new Array(nBound).fill(null);
62-
const est = clauses.slice(1).map(c => c.start);
63-
let lastUsed = -1;
64-
for (let k = 0; k < nBound; k++) {
65-
let best = -1, bestD = Infinity;
66-
for (let g = lastUsed + 1; g < gaps.length; g++) {
67-
const d = Math.abs(gaps[g].end - est[k]);
68-
if (d < bestD && d <= 1.0) { bestD = d; best = g; }
69-
}
70-
if (best > -1) { near[k] = gaps[best].end; lastUsed = best; }
71-
}
72-
// 只有**每条边界都有实测**才叫 nearest(可写回); 有缺口的报 sparse, 只展示不写回
73-
return { method: near.every(v => v != null) ? 'nearest' : 'sparse', meas: near };
74-
}
42+
// 实现已挪 tools.matchBoundaries(单一来源, 发音形态口径 say ?? text —— 第 25 轮: 按 text
43+
// 对齐时 say/text 标点不一致会精确模式错位, --calibrate 写回错的 start)。
7544

7645
const report = [];
7746
let calibratable = 0;

‎plugins/Wzdhehe/html2video-for-mcode/skills/html2video-for-mcode/scripts/plan-timings.mjs‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,13 @@ import { positionalDir, probeDuration, requireTool, safeId, safeOut, safeRel, su
1313
// tools.subLineCap, 字幕胶囊几何的唯一来源也在 tools.SUB_GEOMETRY); pace 区间用于语速异常预警。
1414
// yearGate = 年份逐位读的闸门只对中/粤语面开(第 24 轮: 用 emPer===1 充当语族判别是排版
1515
// 属性代理语义, 未来 emPer:1 的语种会误触); 未知语种 fallback 不开闸。
16+
const ZH_BASE = { unit: '字', pacing: 4.8, paceMin: 3, paceMax: 6.5, emPer: 1, word: null, yearGate: true };
1617
const LANG_CFG = {
17-
zh: { unit: '字', pacing: 4.8, paceMin: 3, paceMax: 6.5, emPer: 1, word: null, yearGate: true },
18-
yue: { unit: '字', pacing: 4.8, paceMin: 3, paceMax: 6.5, emPer: 1, word: null, yearGate: true },
19-
en: { unit: '字符', pacing: 14, paceMin: 9, paceMax: 18, emPer: 0.44, word: 'words' },
18+
zh: ZH_BASE,
19+
yue: ZH_BASE,
20+
en: { unit: '字符', pacing: 14, paceMin: 9, paceMax: 18, emPer: 0.44, word: 'words', yearGate: false },
2021
};
21-
const langCfg = code => LANG_CFG[code] ?? { unit: '字符', pacing: 14, paceMin: 8, paceMax: 20, emPer: 0.44, word: null };
22+
const langCfg = code => LANG_CFG[code] ?? { unit: '字符', pacing: 14, paceMin: 8, paceMax: 20, emPer: 0.44, word: null, yearGate: false };
2223

2324
const argv = process.argv.slice(2);
2425
const dir = positionalDir(argv);
@@ -58,7 +59,7 @@ for (const s of script.slides) {
5859

5960
// 时间权重按**发音形态**(say ?? text)算 —— 音频念的是 say(第 24 轮: "82.3%"显示 5 字、
6061
// 发音"百分之八十二点三"8 字, 按显示形态分摊会把长 say 句的开口系统性估早, 字幕窗与
61-
// ASR 切分跟着偏)。字幕行宽(:93)仍按显示形态 text —— 胶囊几何是屏幕上的事。
62+
// ASR 切分跟着偏)。字幕行宽(下方 wrap 检查)仍按显示形态 text —— 胶囊几何是屏幕上的事。
6263
const spokenLen = c => charCount(c.say ?? c.text);
6364
const total = s.clauses.reduce((n, c) => n + spokenLen(c), 0);
6465
if (total === 0) warns.push(`${s.id}: clauses 为空, 将整张静止`);
@@ -106,7 +107,7 @@ for (const s of script.slides) {
106107
// 整读/逐位读是意图问题, 2026点 就该整读 —— 其余数字写法走文档纪律)。
107108
if (CFG.yearGate) {
108109
const spoken = c.say ?? c.text;
109-
const ym = /\d{4}\s*年/.exec(spoken);
110+
const ym = /(?<!\d)\d{4}\s*年(?!\d)/.exec(spoken); // 数字边界: 12026年 这类长串截出的"2026年"不是年份
110111
if (ym) {
111112
const digitsRead = ym[0].replace(/\D/g, '').split('').map(d => '零一二三四五六七八九'[+d]).join('');
112113
warns.push(`${s.id} 第 ${clauses.indexOf(c) + 1} 句 "${ym[0]}" 会被 TTS 按整数读(2026年→"两千零二十六年") — 年份按播报惯例逐位读: text 原样上字幕, 加 say:"${digitsRead}年…"`);

0 commit comments

Comments
 (0)