制作与迁移过程中实际遇到的问题,按主题整理。每条先写现象,再写原因与做法。新遇到的坑追加到对应的主题下。
- 一个元素只做一次入场。
wipe、slam、slide、appear、show都会把元素在入场之前设为不可见。同一个元素登记两次入场,状态会打架。需要「消失后再出现」时建两个元素,或用appear+vanish+ 再set(el, t, { autoAlpha: 1 })。 - 画面依赖播放历史。 现象:重新渲染后,个别时段的画面与原先不同;或并行渲染的片段接缝处有一帧跳变。原因是「顺着播到这里」与「直接跳到这里」的状态不同:同一个属性被两个补间在重叠的时间里改;带
overwrite的补间互相覆盖;元素缺少入场前的初始状态;F()里累加了变量;只在元素可见时才更新它的几何。scan.js的NONDET专查这一条。首部作品里有四处这样的时刻,至今记在它的已知问题里。 - 被补间的元素不要靠 CSS
transform定位或居中(例如translate(-50%)):补间会覆盖transform。用left / top定位;需要居中就包一层 flex 容器。 box()量不到没挂到页面上的元素,也量不到display: none的元素。 先建好、挂好,再量。引擎构建场景时所有场景都是显示状态,可以放心量。T(句号, 词)取首次出现的位置。 同一句里一个词出现两次、想取第二个时,换一个只出现一次的相邻词。词在合成文本里找:读法表把词换掉之后,原来的写法就找不到了,用timeline.js <视频> <句号>查可用的词。- 章节卡与第一句旁白之间只隔零点几秒。 场景里第一个元素的入场不要早于
c0 + 0.1,否则会叠在章节卡上。 - 多个 SVG 叠加层里的
<marker>不能重名。 先出现的那个所在的场景隐藏时,后面的箭头会丢头。overlay()已经给标记加了序号,画箭头一律用它。 - SVG 图形做缩放、旋转的补间要用
svgOrigin: "x y"(SVG 自身的坐标)。用transformOrigin写像素值会按外接框解释,图形会飞走。 - 给零尺寸的容器做
wipe会把里面的东西全裁掉。wipe用clip-path: inset(…),裁剪区按元素自己的盒子算;一个只用来打组、宽高为 0 的容器,盒子是空的,子元素即使溢出可见也全部被裁掉,而且不报错。对子元素分别登记入场,或给容器定宽高;同理,叠加层 SVG 的盒子是 1920×1080,画在它之外的内容一做wipe就消失,要把 SVG 自己放到内容所在的位置。 - 同一个元素的同一个类名只登记一次
classAt。 两次登记各管各的开关,谁后变谁说了算,顺着播与直接跳到的结果就不同(NONDET)。两个触发时刻取其一,写成一次登记。 - 部件的状态要在同一帧里生效。 一个帧函数改了另一个帧函数用的颜色或参数、却等「下一帧再重画」,换色的那一帧就是旧画面,也会被报成
NONDET。改参数的函数里立即重画。 fromTo的起始状态在构建时就生效。 引擎的fromTo带immediateRender:起点里的autoAlpha: 1会让元素在入场之前就可见,只起一个可见状态的fromTo同样让元素一开始就出现。只补间位置时不带autoAlpha,显隐另用appear(el, t)管;需要「到某一刻才出现」的元素,先appear(el, t),之后的动作用immediateRender: false的补间。- 改了
script.js的文本之后必须重新运行tts.js,否则时间线用的是旧时长。改了任何入场时刻之后必须重新运行audio.js,因为音效事件是从场景里登记的。 - 全片的第 0 帧,镜头不在起点。
cam.track的每一段都是带immediateRender的fromTo,构建时会套用自己的起点(补间库把这次写入延迟到下一拍),构建完镜头停在最后一站。别的时刻一跳过去时间线就会重算,唯独 t=0 不会:开机时时间线本来就在 0。于是第一个场景的第一帧机位是错的,而且时对时错。做法:在makeCamera之前登记一个帧函数,t 落在轨迹第一段开始之前时每帧都把cam.st放回起点(见 WSL 一片的s0_open.js)。scan.js的确定性检查是倒着每隔三帧抽一帧,t=0 不一定被抽到,这个问题可能潜伏好几轮才报出来。各版本补间库对这次写入的时机不同,表现也不同,以制作机上的检查为准。 - 给场景根设
z-index会盖住常驻层。#scenes自己不是层叠上下文,场景根一旦带正的z-index,就排到了#hud上面,章节卡与底栏整个不见,字幕若是白字还会一起「消失」。后一个场景要压住前一个,靠 DOM 的先后顺序就够了。 - 读数、计数这类动画,落点要在说这个数的那一句之内。 把起点挂在那个词上、再往后走两秒,数字就落到了这句话之后;逐句的总览图(每句说完那一刻)里显示的是走到一半的数。起点往前提,让它在读到那个数时正好走完。
- 按词定时的动作,两种语言的落点不一样。 同一个词在中文里在句中、在英文里可能在句末;挂在它后面的一串动作会落到这一句之后,甚至落到镜头离开之后。给起点加一个上限(不晚于这一句结束前若干秒),两种语言的逐句总览图都要看。
- 用
draw()描的线,开始之前就露出一个点。 线帽是圆头时,虚线偏移到零长度仍会画出一个圆点;改成平头也还会留一丝抗锯齿的痕迹。做法是开始描之前把它整个藏起来,而且要用display、每帧都设:draw()的补间带autoAlpha,会改visibility,两边抢同一个属性、再加上「状态没变就不写」的缓存,画面就依赖播放历史了。 - 时间线上不能有负的起点。 没有章节卡的场景,
c0只比场景起点晚 0.1 秒;开场里写c0 - 0.6就成了负时刻。补间库遇到负的起点会把整条时间线上的补间一起后移:全片扫描报「顺着播与直接跳到这里不一样」,由时间线驱动的动作也整体晚了那么多。第一个场景里的时刻一律从s.start算起。 - 场景的 SVG 画布要盖住整张大图。 全片扫描只把落在画面内、面积不小于 16 的 SVG 元素算进画面指纹;画布若只有一点大(靠
overflow: visible画出去),镜头移开后里面形状的运动就不算变化,会报 STILL、EMPTY。用viewBox对齐 world 坐标,把画布的位置与大小设成整张图的范围。 - 只有边框的方框不算内容。 没有底色也没有文字的
div不进画面指纹;场景开头若只有这样的框,会报空屏。开头放一段文字或一个 SVG 形状。 check.camera按场景文件里是否出现makeCamera字样判断。 用部件函数包装建镜头会被判没有镜头;场景里自己const cam = makeCamera(world)。- 场景的 SVG 画布在所有文字与卡片之下。 画布先挂上去,之后建的
div都压在它上面:画在卡片上的虚线框、标记被卡片的底色盖住,写在 SVG 方块上的标签被扫描报成对比度 1:1。要压在卡片上的图形放进另一块z-index更高的画布(OpenClaw/OpenCode v2 一片stage()返回的gt),要压字的色块改成div。 - 带标题的面板用
wipe入场,标题被裁掉。 终端风格的面板把标题压在上边框上、伸出盒子之外;wipe的clip-path: inset(…)按盒子裁,标题只剩下半截。裁剪区的终点向外放出标题的高度(同片的wipeP,终点是inset(-40px -8px -8px -8px))。 wipe刷完之后,大号衬线字的下伸部被切掉。 《垃圾回收的艺术》英文章卡「Reference counting」的 g 底下少一截,策划者看片时指出。字形的下伸部(g、p、y)伸出行高,wipe停在inset(0%)就按盒子切掉;这只在正式渲染里出现,look.js的截图与总览图上看不出来,查成片要按原尺寸抽帧。引擎的wipe现在停在inset(-30% -2% -30% -2%)(2026-10-20 起的新片自动带上;旧片是拷进视频目录的快照,要自己改src/js/util.js)。- 镜头的两段补间在时间上重叠。 按句子时刻排的镜头关键帧,前一段还没走完、后一段就开始了,两段同时改镜头,扫描报
NONDET。轨迹函数里把每段的起点限制在上一段结束之后。 - 镜头往下一站移动的那一秒落在空处。 上一站已经出画、下一站的第一个元素还要等下一句才入场,低清预演与成片检查报空画面。下一站的标题或面板在镜头起步时就入场(比下一句早 0.6 秒左右)。
- 特写并排面板中的一块,另一块被画面边缘切成半截。 镜头推近,相邻面板必然露出一截,字被切成半行,读起来像取景失误。给不在讲的那块盖一层与底色相同的半透明遮罩,透明度在帧函数里按 t 算出;露出的部分就成了背景。
- 场景根是透明的,换场时刷出来的是旧场景。 用
after()编排「新场景跟在一道竖线后面刷出」时,新场景左侧还没有元素的地方是透明的,看到的仍是底下的旧场景;旧场景要等换场结束才消失,于是画面上没有刷出的动作,只有一道线扫过,随后一跳。给.scene设本片的底色,刷出的那一侧就是新场景(风格派一集的style.css)。新场景左侧最先被刷到的地方,第一个元素在场景开始后 0.1–0.3 秒内入场,否则刷出的是一块空底色。 - 换场时常驻元素的字色要跟着刷线走。 角标按「某段时间压在什么底色上」换字色(
hudGround);前一场登记到「本场结束 + 换场时长」的红底,换场的刷线经过角标只要 0.1 秒左右,此后角标已经压在新场景的纸色上,纸色字就看不见了。底色登记到刷线经过角标的那一刻为止。 - 用镜头往复平移凑「任意 3 秒有变化」会分散注意力。 InnoDB 第一版给每段停留加了 ±34px 的左右往复,策划者直接指出「镜头一直晃来晃去」(
feedback.mdP6)。静止检查要靠内容的节拍(跟着旁白的高亮、标注、图形变化)通过;镜头只做站间移动,停留时最多极慢地单向横移。换成往复缩放也一样:InnoDB 新版第三次重渲给每章加了「每 10 秒推近 4% 再拉回」,章节卡也慢推,策划者看片指出「镜头一直在缩放」(P78)。规则见 scenes.md 第 3 节,scan.js的ZOOM会报。 - 两种静止检查的口径不同。
scan.js每 0.5 秒取一帧,元素坐标按 2 像素取整后比较:每秒移动 2 像素以下的变化可能测不出来。preview.js在 1/4 分辨率上用 freezedetect(−48 dB)比整帧,Y/U/V 三个平面一起平均:纸面上只有一行标题和一行正在打字的命令时,镜头移动二十多像素也算「不动」。到站时就要有成块的内容,稀疏画面靠镜头救不回来。 - 绝对定位、
white-space: normal的文字放在 world 右侧很远处会塌成一个词一行。 不给宽度时,它的可用宽度按 world(1920px)减去 left 计算,x 超过 1920 就是负数,文字按最小宽度折行,长串作者名竖着排到字幕带里。文献卡的作者行、引文都要显式给width。 也可以把 world 本身设宽:InnoDB 第二版把.world设为 16000px,一次解决了站点在右侧的所有说明文字。 - 类名里多出的空格会被判为「画面依赖播放历史」。 部件用
'panel ' + cls拼类名,cls为空时留下尾随空格;之后classAt一切换,classList把整串规范成单个空格。顺着播到的元素切换过、直接跳到的没有,scan.js的签名就不同,报NONDET。部件里统一先把类名去掉多余空格(InnoDB 第二版parts.js的CL)。同理,keyed(el)每次切到新的关键帧都会把className整体设回它记下的值,抹掉classAt加上的类,而classAt有「状态没变就不写」的缓存、不会再加回来;同一个元素既要换内容又要换类名时,类名也写进keyed的关键帧(.at(t, { cls: 'row dk' }))。 - 用极短的
fromTo切显隐,倒着跳时元素不会再藏起来。 「到某一刻出现、过一段再消失」的元素,若用 0.01 秒、immediateRender: false的fromTo管autoAlpha,再用out()收走,从后往前跳回出现之前时,它停在out()的起点状态(可见)。这类区间显隐改用F()由 t 直接算出opacity与visibility,位置另用补间。 - 长句里只有一个小元素变化,低清预演仍判静止。 单元格换值、小标签弹出、细边框变色,在 1/4 分辨率的整帧平均差里几乎看不见;只有标题的开头句、只说结论的句子最容易连成 4–9 秒的静止。按句中的关键词让成块的东西出现:面板、一行 SQL、一排占位框、一条较粗的线。不要用镜头的慢推去补(停留时不缩放,见 scenes.md 第 3 节)。
- 正方形转 90° 看不出变化。 Web 登录一集用「转 90° 拼不上」表示拒绝,用在方形的网站框上,转完和原样一模一样,
look.js量到的外接框也不变,看截图才发现。方形、圆形这类对称的形状,拒绝或倒下改用转 25° 加下沉、变暗;只有钥匙、长条这类不对称的形状才用 90°。 - 浅色元素要很大才算画面变了。
preview.js的freezedetect把 Y、U、V 三个平面的全部样本一起平均,跟一段静止开始时的那一帧比,平均差到约 1 个灰阶(−48 dB,即 3.98‰)才算变了,所以算数的是「面积 × 与底色的亮度差」。InnoDB 第二集在低清预演上逐帧量过:浅色面板(#FAFBFA压在#EEF1F0上,只差约 10 个灰阶)要占到画面六分之一左右才刚过门限,第 05 章 560×600 的 MyISAM 面板入场升 5.3‰,第 06 章的磁盘面板升 5.0‰;深色块占画面 2% 就够,第 02 章一行(730×56)由标志蓝变回浅色升 8.3‰,另一行随后压暗,合计 18‰。小的白色元素不算:改之前的第 05 章,两个 120×160 的白底文件图标和两行小字标签都在一段 3.5 秒的静止里先后出现,没能打断它。修法是让讲到的东西成块出现,并且用深色:讲到「数据」「索引」时,数据文件下排出三条深青的记录、索引文件下长出一棵小树;第 03 章「锁的粒度」一句,读到术语时弹出一块标志蓝的释义标签。
- 等宽字里的空格。 flex 容器会丢掉纯空白的文本节点,行内代码的空格要和文字写在同一个
span里;整行代码的容器设white-space: pre。 - HTML 实体。 代码与终端内容里的
<、>、&要写成实体,或用esc()。>>、提示符里的>都在此列。JS 字符串里的反斜杠要写两个。 - 等宽字体的连字。 JetBrains Mono 会把
->画成箭头;base.css已全局关闭连字。 - 放在画布右侧的文字被挤成一字一行。 绝对定位的块
left超过容器宽度时,可用宽度为负。world里的文字默认不换行;要换行就显式写<br>,或给块定宽并设white-space: normal。 - 遮罩揭示时字头露出来。 遮罩留了上下内边距防止裁掉笔画,位移只给 110% 不够,给到 140% 左右。
- 整行反白比上下行多出一截缩进。 用负外边距抵掉内边距。
- 会转动、会长出标签的图越界。 按「转到最不利位置时」留边;成片前用
margin.js把每句的画面都量一遍。 - 大数字与紧挨着的标签被报为重叠。 文字项量到的是行框,上下各有一截空白。检查时已按字形大致占的范围计算;仍被报出时,多半是真的压住了。
- 字号比直觉大。 先看缩小一半的总览图,缩小后仍读得清的才算够大。
- 拉远看全图时,正常字号的字会低于最小字号。 镜头缩到 0.5 倍,28 像素的标签只剩 14 像素,
SMALL报错,实际也看不清。拉远之前把自身带字的元素收起,只留为全图准备的大字标签。 - 一排并列的标签别按固定间距排。 英文比中文宽得多,固定间距在英文版里会互相压住。放进一个
display: flex的容器,让它们按内容自然排开。 - 英文里的弯撇号
’在先列 CJK 字体的类里是全角字形。 屏幕上的英文文字用直撇号;字幕条与等宽类不受影响。 - 两段内容共用同一块位置时,前一段的退场要锚在后一段的入场上。 引文的退场按「下一句的后半句」定时,接替它的金线与文字按「下一句的前半句」定时,中间就有约一秒两段文字叠在一起。这种相压落在两句之间:逐句末的总览图看不到,全片扫描也可能不报。退场时刻直接写成「接替者入场之前」,并用
sheets.js <视频> every 4通看。 - 镜头在大图上移动,正在讲的内容不能被画面边缘截断。 当前这句讲的那一行、或一个大字标题被截成半个词,就是缺陷;推近或平移让相邻区域露出半截,也常被看成取景失误(MyISAM 新版第一轮,P70–P72)。取景时把这一句要看的内容的最右端、最下端与画面边缘、字幕条的距离算出来,连同镜头漂移的幅度一起留够。
- 文字伸出所在面板的边框,扫描不报。 全片扫描只查文字与文字、文字与画面边缘;一行说明从面板里伸出去、压在面板的边框上,不在检查之内。InnoDB 第二集第 07 章「← 加锁读:读最新的值,并且不让别人改」一行的末字压在面板右边框上(10:43–10:58),是看逐句总览图时发现的,删去「并且」两字才收进面板。放在面板里的说明,按「面板右边框减去起点」算好可用宽度;改了字数要再看一遍总览图。
- 字幕会和图解的线条相压。 大图上到处是线,镜头怎么摆都难保字幕下面没有东西。给字幕加一块与当前底色相同的衬底(深底深衬、浅底浅衬),平时看不出,线条经过时被它挡住。
- 成片悄悄少了一段。 无头 Chrome 的截图通常是不带透明通道的 PNG,但个别帧里某个合成层的边缘会留下一条 1 像素宽的透明缝(带
will-change: transform的遮罩文字在切换反白的那一帧),这一帧就成了带透明通道的。图像流的像素格式一变,ffmpeg 重建滤镜图,用setpts=N/…给帧编号时计数归零,其后的帧被当成迟到帧整段丢掉;日志里没有任何报错,封装时的-shortest又把声音一起截短,成片看上去正常,却少了 28 秒。两次渲染丢的帧数一模一样,才看出不是偶发。现在的render.js在入管道前把这类帧改写成不带透明通道的,时间戳由输入时间戳平移得到,并逐段、整片数帧;finish.js另外核对音画长度。排查用pngprobe.js。 - 多个无头浏览器必须逐个串行启动。 同时加载几十 MB 的字体会互相拖到导航超时。
openPage已如此。 - 不要用 CSS
blur()滤镜。 单帧出图从约 120 毫秒变成 365 毫秒。 <canvas>在场景显隐切换之后会丢瓦片。 预览时看不出来,渲染时才出现。画完之后用freeze()换成图片。- 编码进程不限线程时每个占约 1.6 GB。 九路就是 14 GB,内存紧张时会报
x264 malloc … failed。render.js缺省每个编码进程 4 线程,降到约 0.7 GB,速度不变(瓶颈在截图)。 - 低清预演的每一帧都被判为不合法。 报「截图重试后仍不是合法的 480x270 PNG」。新版 Chrome(实测 154)用调试协议直接截图时不再理会小于 1 的设备像素比,出来仍是 1920×1080。
render.js现在在缩小出图时显式给出裁剪区与比例(clip.scale);look.js走的是另一条截图路径,不受影响。 - 封面页的 canvas 截出来是空的(解析阶段画必空,字体就绪后画也偶发)。 封面里的图形在页面脚本一开始就建 canvas、立即作画,制作机的无头浏览器截出来那块是纸色;
getImageData读回的像素全为透明(0,0,0,0),fillRect之后也是。同一段画法挪到document.fonts.ready之后的回调里执行就正常了;原因没有查明。正片的 canvas 本来就是在引擎等字体载入之后的构建阶段画的,没有遇到。挪到字体就绪之后也只是多数时候正常:同一集改版后连出封面,英文 16:9 仍两次整张空白(连黑框都没有,画完换成<img>也一样)。封面里的数据图形改用 SVG 矩形绘制,连出三轮都正常(风格派一集的cover.html)。 - 4K 封面在个别机器上底部缺瓦片。 封面以正式出图的那台机器为准,出图后看一眼底部。
- 慢速缩放的大字,压缩后边缘有约 1 像素的抖动。 把已交付的画面与无损截图逐帧相减时,大号高反差的文字边缘会显出一圈细线。这是编码器对极慢的亚像素运动的处理,不是内容不同;比较两次渲染是否一致时,先把画面缩小再比,或用接近无损的码率重新渲染后再比。
- 带
will-change: transform的层,直接跳到与顺着播到的像素可能略有不同。 元素的位置与内容完全一致,差别只在浏览器对合成层的栅格化(在运动中的文字上最明显)。成片是顺着播放渲染的,各段内部一致。 - 整屏只有一种颜色的画面会被成片检查判为空画面。
finish.js的空画面检查以每帧最常见的灰度为底色,数与它不同的像素;一块同色幕布盖满屏幕、或镜头在大图上移动时扫过空白,都会连续几帧低于门限,check.js --final不通过。幕布上放大字标题;长距离换位用整屏的小节幕盖住,在盖住的那一刻切镜头。 - 空画面检查对采样相位敏感。 它每 0.1 秒取一帧,「持续 0.3 秒以上」是按取到的帧数算的;恰好在门限上的片段,时间轴平移零点几秒(例如改了一句旁白)就可能从不报变成报。两段内容交接处别留空档:前一段的元素留到后一段进来之后再收。
check.js --final看的是src/下的全部文件。 成片是否过期按src/里每个文件的内容哈希判断,不管页面是否加载它。早已不用的设计稿、示范场景要在最后一次渲染之前清掉;渲染之后再删,成片即被判为过期。封面cover.html也在src/里,同样要在最后一次渲染之前定稿。- 章名只有一个字,换章那半秒成片是空的。 InnoDB 第二集用整场淡入(
sceneFade):章节标题在本场内容淡入前约 0.4 秒开始收向左上角。「08 锁」只有一个字,标题缩小时占画面不到 0.6%,本场的面板与记录又要等本场淡入之后才各自淡入,check.js --final报 661.7–662.0 秒空画面;章名长的各章没有报。做法:本场的第一批元素(面板、记录、两个会话标签)在本场淡入之前就把自己的入场做完,随本场一起出现;深色的字和色块在本场不透明度只有一成时就算内容。只看低清预演的静止检查查不出这一项,要跑下一条的空画面检查。 - 空画面检查可以先在低清预演上跑。
blank_check.py接受一个视频文件作参数(环境变量LVS_VIDEO、LVS_LANG、LVS_FFMPEG);对out/<语言>/preview_video.mp4跑一遍只要十几秒,不必等正式渲染之后才发现。 - 稀疏的线条图上,镜头的慢移在低清预演里等于没动。 0.25 倍、15 帧/秒下,线条每帧的位移不到一个像素时,逐帧差低于静止检测的门限。不要靠加大镜头漂移去补:OpenClaw/OpenCode v2 一片一度让漂移每秒走二十几个像素、上限放到 240 像素,横向撑满画面的站点整排文字被推出画面,全片扫描报了 188 处「被切掉」。停留时的横移按 scenes.md 第 3 节(每秒不超过 2 像素);稀疏画面要让成块的内容变化。
- 推近之后,画面下方的说明落到了字幕栏底下。 字幕栏是实心带时,压在底下的字直接看不见。MyISAM 新版第 03 章推近到字节表,「其余 15 个字节不变」一行落在字幕栏下(02:33),策划者截图指出。可读的纵向范围是顶栏与字幕栏之间,算取景时扣掉两条带;
scan.js的CAPZONE就是在报这件事。 - 章节卡上的光标不会自己闪。 不能用 CSS 动画,静态的
<span class="cur">就是一个不动的色块;要在帧函数里按 t 切换它的不透明度(typeText自带的光标已经这样做)。MyISAM 新版第一轮的章节卡光标不闪,策划者看片指出。 - 用脚本批量改源码时,插进去的行内注释吞掉了同一行后面的代码。 把
// 说明插到一行中间,这一行剩下的语句全成了注释:有一次是一处定位不再生效,有一次是整个声明断掉、页面报错。说明写在行尾或单独一行,改完对每个文件跑node --check。 - 全片渲染不能续渲,长片的默认并发会把内存用尽。
render.js开头就删掉上一次的分段,中途被终止只能从头再来。9–10 分钟的片子在 40 GB 的机器上按旧的缺省并发渲染,被系统以内存不足终止过(2026-10-20)。缺省并发现在不超过「可用内存 ÷ 3 GB」;同机还有别的重任务时,显式加--workers 4。 - 两个开浏览器的工具不要同时跑。 一边跑
preview.js、一边跑自写的逐帧测量脚本,新页面的导航被ERR_ABORTED打断过。同一部片子的浏览器类工具一次只跑一个;一部片子整片渲染时,另一部片子起一个短渲染,同样被ERR_ABORTED打断过(2026-10-20)。 - 低清预演的静止检测对节拍网格的量化很敏感。 改一点停顿,别处一段 2.8 秒的静止就可能变成 3.2 秒,来回修了四轮。先用报告里的时刻找到对应的句号与场景(
timeline.js),再看那一段整体有没有足够的变化,不要只把边界挪一点点。 - 低清预演没报的静止段,成片检查照样会报。 《垃圾回收的艺术》两种语言的低清预演最长只有 2.9 秒静止,整片渲染后
finish.js在原尺寸上(−52 dB、裁掉字幕条)报了四段 3.0–4.0 秒:都是一块源码或指令石板出现之后,到下一个高亮之间只有背景的小方块在闪。低清预演里 2.5 秒以上的段落,正式渲染前先用render.js --from --to --out按正式参数渲那几秒,用finish.js里同一条 freezedetect 命令量一遍;补的动作要成块(整行的底色、一个方块),只变字色的面积太小。 - 开了
--gpu 1,渲染偶尔停在收尾。 帧已全部写完,个别无头浏览器的close()一直不返回,进度行停在「frames N/N」反复打印(Windows、2026-10-20:几十秒的短片段上反复出现,一次 4 路里有 3 路挂住;一次整片渲染没有遇到)。lib.js现在等 15 秒仍未关上就结束该浏览器的进程树,日志里会有一行「浏览器 15 秒内没有关上」;帧数照常逐段核对。
- 连写的英文专名被读错或逐字母拼读。 拆开写(
lua installer、My ISAM)。读法先试再定:tts_probe.js。 - 同音词听不出区别。 「计数」与「技术」同音,「密钥和计数」听上去是另一个意思;定稿改成「计数器」。写完稿用
asr.js回听时,留意识别结果里出现的同音近义词。 - 多音字读错。 换成同音的单音字(数据「行」→「航」,「月相」→「月象」),只改合成文本,字幕与画面不变。
- 两种语言的成片响度不一致。 不同音色的峰值与均值之比差别很大,按峰值归一会相差数 dB。混音按响度对齐旁白。
- 合成铺底的低音过重。 用数值自检看三段能量占比,见 sound.md 第 5 节。
- 封装报告的真峰值是编码前的。
finish.js在编码前限幅、测的也是 WAV:InnoDB 第二集报 −1.4 dBTP,AAC 256k 编码之后成片实测 −0.9 dBTP,高出约 0.5 dB,超过 −1 dB 的目标;check.js --final读的是编码前的数,照样判为通过。交付记录以成片实测为准:ffmpeg -i <成片> -map 0:a -af ebur128=peak=true -f null -。 loudnorm单遍处理会抽动。 成片用「固定增益 + 限幅」。- 无配乐版重建后与原先不逐样本相同。 音效里的噪声成分由同一个随机数发生器按顺序生成,配乐不同则音效的噪声实现不同。现在的无配乐版直接取主混音里的那条音效分轨,与主混音完全一致。
- 数据的「行」被读成 xíng。 Edge 的中文语音把「整行」「一行」「取行」「行上」「行级锁」读成 xíng(回听听成「整形」「异形」「平行」)。在 SAY 表里把表示数据行的「行」换成同音的「航」:
[/(?<![执进串银])行/g, '航'],「执行」「进行」「串行」「银行」不换;写在tts字段里的句子要手动改。 - 整段回听报的读错,逐句复听常常就没了。 Whisper 整段转写时靠上下文猜词:InnoDB 第二集里「回滚」被听成「毁滚」、「空隙」被听成「空气」,把成片用的旁白轨按句切出来单独转写(不带前后文),这些就都对了;单独转写仍错的(「Oracle」两次都听成「Boracow」),才是值得人去听的地方。
- MiniMax 合成文本里的逗号会变成很长的停顿。 Web 登录一集想让几句偏快的英文慢一点,在读法里各加了一个逗号:o3b 从 1.8 秒变 3.4 秒(正好),j4 却从 3.9 秒变 5.8 秒、j7b 从 2.9 秒变 5.8 秒,全片英文一下超出 200 秒。加逗号要逐句试,不要一次改好几句;同一句重新合成,时长本身也会差零点几到一秒(j4 改回原读法后成了 5.1 秒),重新合成过的句子要重排场景定点。
- 英文的 you know 会被读成口头禅。 「It proves you know a secret」合成出来是「It proves, you know, a secret」,单独转写这一句才听出来(整段回听只报了漏词)。改成「It proves that you know a secret」。英文稿里出现 you know、I mean、like 这类兼作插入语的词组时,单句转写核对一遍。
- 重新配音之后要重跑
asr.js。out/<语言>/asr.txt不会随tts.js更新。InnoDB 第二集按 R8 重新配音后,回听记录仍是 R7 那一版的,直到收尾时重跑才有 R8 的结果。 - 改读法或停顿会挪动整条时间线。 任何一句的时长变了,后面所有句子的起点都跟着变,已经开始的渲染作废。顺序是:
tts.js→asr.js回听 →stats.js看「过快、过长、停顿过短」→ 全部改完,再渲染。
- 字体规则误报文案里的词。
check.style.fonts打开后,源码里出现Microsoft、Arial这类词就会被当成系统字体;片尾名单里写到「Microsoft Edge 在线语音」即中招。确属文案时在行尾注释lint-ok: <原因>。 - 远程会话会断,长任务跟着死。 经 SSH 起的渲染、实验,会话一断子进程就被带走。长任务脱离会话启动(Windows 上用 WMI 的
Win32_Process.Create,或计划任务),输出写进文件,结束时落一个标记文件,再轮询;中止时按进程树结束,只清理这一次留下的临时档案目录。 - 别用整目录同步覆盖
timing.<语言>.js。 两台机器分工时,这两个文件由制作机上的tts.js生成,本机副本只是占位;整目录覆盖会让时间线退回按字数估算。 - 在线语音服务会突然拒绝连接。 2026-10-18 用 edge-tts 7.2.8 合成时握手被拒(403),既不能试读法,也不能重新配音。已有配音缓存、旁白不改的片子不受影响:混音、渲染、封装都只用缓存。要改读法时先查服务能否连上,再动脚本。原因是客户端过旧,不是被封:2026-10-21 换成 edge-tts 7.3.1 即恢复,
kit/requirements.txt的下限随之提到 7.3.1。再遇到 403,先pip index versions edge-tts看有没有新版本。 - 制作机离线时可以在本机看图,但检查以制作机为准。 本机的工具链与依赖库版本可能不同(例如补间库),同一份代码在两边的首帧状态都可能不一样。
- 经 stdin 传给 Windows PowerShell 的脚本里,中文字面量会乱码。
ssh ... 'powershell -NoProfile -Command -' < script.ps1按控制台代码页读脚本,Contains('两阶段锁')永远匹配不到,却不报错。要搜的中文先在本机 base64 编码,脚本里用[Text.Encoding]::UTF8.GetString([Convert]::FromBase64String(...))还原。 - 擦除结束后留下的
clip-path: inset(0%)曾让大字被报「被裁:T」。 衬线大字的字形框比行高略高,按元素盒子裁剪就误判为上边被裁,自写的可读时长脚本因此量不到这些字。inspect.js已把全为 0 的 inset 当作不裁剪(2026-10-20)。
- 交付与发布是两件事,记录各有各的。 2FA 于 2026-10-06 发布到哔哩哔哩与 YouTube,片单已经是「已成片」,但根 README 的「已成片」表没有它,片中 README 与
FACTS.md还写着「成片还在制作机上,没有上传」。发布之后按 process.md 第 11 节回填,漏了这一步,仓库就与已经发生的事不一致。 - 渲染之后又改了源码,会话却停在预演。 InnoDB 第二集第一次正式渲染(2026-10-10 09:36)之后又改了 12 个源文件,跑过一次低清预演(仍有两段静止)就中断了:
out/里留着一版看上去完整的成片,README 的「成片」一节空着,片单仍是「制作中」。一周后接手,比对out/<语言>/video.json记下的源文件哈希才确认它早已过期。接手时先做这一步比对,再决定从哪一步重做;中断前在 README 写明停在哪一步、还差什么。 - 改完之后结果反而变差,先确认改动真的生效了。 InnoDB 第二集第 02 章想让 notes 表在读到「两张表」时就出现,改的时候把说明写成了行尾注释:
const tNt = …; showPanel(PU, tA); // 说明…… showPanel(PN, tNt); fin(…); …,同一行后面的五个入场调用被一并注释掉,notes 表与两张表的表头、记录变成本场一淡入就在。预演里那段静止从 2.9 秒变成 3.7 秒;当时没有回头看画面,就在预演视频上量差值,得出「浅色面板测不出来」的错误结论,写进了源码注释与本页(上文「浅色元素」一条是后来重测的数)。直到成片投稿之后、提交前复查改动才发现。改了出现时刻却没有预期的效果时,先用look.js看那一刻、在git diff里逐行读改动,再找原因;说明写在单独一行,不跟在代码后面。 - 画面风格先取证讲解对象,再动手。 InnoDB 第一版的纸色底、墨色字、单一强调色,与同期另一个系列的 150 秒片子是同一套路子,被要求完全重做(
feedback.mdP7)。开工时先取讲解对象自己的视觉材料(标志、官网样式表、官方文档插图),测出配色与字体并写进本片的设计说明;再与近期各片并排比较,确认不是换色复用。 - 术语逐个过一遍再交审。 第一版的稿子出现了 SDI、脏读、幻读等词却没有解释(
feedback.mdP9)。交审前列出稿中每个术语第一次出现的句号和解释它的那一句,没有解释句的要么补、要么删。 - 片单是共享文件,
new.js会整份重写它。new.js在开始时读入videos/catalog.json,结束时把整份文件写回。同一仓库里同时有别的制作者开工(另一个会话在登记自己的新集、或改了状态)时,你这次new.js结束的瞬间会把对方的改动一起覆盖掉。做法:改片单前先git status看有没有别人的未提交改动;批量新建时一条一条跑,每条之后重新核对片单里自己的条目与别人的条目都还在;提交时用路径限定的git commit -F <提交信息文件> -- <pathspec>,只带自己改的条目;推送前重新 fetch 比对,别只看git branch -vv的快照。 - Windows 上整篇 CRLF 的系列说明,会让片单检查一直判为不一致。
catalog.js重写分集表时用的是 LF;syncSeries比较的是整份文件,于是文件里若通篇是 CRLF,就永远比不相等,catalog.js --check报「与 videos/catalog.json 不一致:videos/series/<系列>/README.md」,而表格内容其实一字不差。仓库规定全仓 LF(.gitattributes的* text=auto eol=lf),把这类文件规范化成 LF 即可。实测 2026-10-20:videos/agent-harnesses/README.md是全仓唯一一篇通篇 CRLF 的系列说明。 - 一部片子从双语改成只做一种语言,三处要一起改。
new.js按片单里的languages生成src/js/timing.<语言>.js,并把每个语言写成src/index.html里的<script src="js/timing.<语言>.js">。只把project.json的languages收窄成一种语言,删掉另一个语言的timing.*.js,index.html里那行<script>还在,浏览器每次都报一次 404(look.js的「页面日志」里看得到)。改语言范围时三处一起改:project.json的languages、videos/catalog.json里本片的languages、以及src/index.html的时间线<script>行。catalog.js --check会核对片单与project.json的语言是否一致,少改一处就被它挡下。实测 2026-10-20:Firebird 一集只做中文。 - 逐句对应表要在字幕断句之后再写。 一部片子在确认后把 61 句超宽句拆成 142 句,断句前写好的
expect.zh.json有十几处要求某个词在「前一句」说完时就已上屏。句号一变,对应表按新句号重写。 - 内容哈希包含章名。 校对确认的内容哈希覆盖语言、标题、场景号、章节卡的编号与标题、句号与字幕、是否无旁白;不含读法、
tts、停顿与lead、gap。确认之后改章名会让确认失效;章名需要调整时,同样交校对确认(见 文案校对 第 3 节)。 - 任务书与策划者的现场指示冲突时,以策划者为准。 一部片子的任务书要求沿用系列的瑞士风格,策划者在会话里明确要求不要与那个系列同质化。规则见 角色与权限 第 2 节:照策划者的话做,原话记进 README 的「简报」。
- 推算出来的数字是错的。 一张文件布局图按「总大小减去各部分大小」推出,与实测不符。画之前先实测。
- 安装器顺带装上了依赖。 用系统的包管理器装工具时,会连带装上编译器与头文件,「只缺某一样时会怎样」的实验因此做不出来。改用源码目录里的安装脚本。
- 包装脚本吞掉了工具自己的报错。 经包管理器的包装脚本运行时,得到的是「找不到模块」而不是工具自身的报错。改为直接运行源码目录里的入口。
- 回显里带出了本机信息。 用户目录、主机名会随回显进入取证材料。入库前替换成通用写法并注明;能在干净容器里做的实验就在容器里做。
- 实验回显里的家目录,可以在运行时就是通用路径。 在一个新的用户命名空间里(
unshare -Urm --fork)把实验目录绑定挂载成/home/user,再运行被测程序:它看到的家目录、配置与数据目录都是这个路径,回显不必事后替换,也不需要容器。进程列表仍要过滤掉与实验无关的进程。被测程序启动时要联网安装依赖的,按它自己锁文件里的版本预先放进缓存目录。 - 按脚本应答的模型,不能拿来说明真实模型的行为。 为了看清 harness 自己写了什么,实验里的「模型」是一段固定规则;它在恢复后重做了一次命令,是规则让它这么做。旁白若写「于是它又执行了一遍」,就把规则的结果说成了模型的推断。写成「按脚本,它又执行了一遍」,画面上标明「按脚本应答」。
- 被计时的命令里混进了准备步骤。 把「清空目标目录」和「解压」写在同一条被计时的命令里,量到的就不是解压。准备步骤放到计时之外,并另量一条空命令作基线(进程启动的开销),两个数都列出来。
- 容器里的小工具没有想当然的功能。 Alpine 的 busybox:
date不认%N(算出来的耗时全是 0),httpd不在最小根文件系统里。计时改在宿主一侧用秒表量;要一个能应答的端口用nc -lk -p 80 -e cat <文件>。 - 本地化的命令行回显,另一种语言的版本要有来路。 有的程序按用户的界面语言输出,没法按进程切换。不改测试机的系统语言,也不凭印象翻译:从它自己的字符串表里取两种语言的模板,用一种语言的模板把实测回显反解成各项的值,再套进另一种语言的模板,对不上就报错(见 WSL 一片的
tools/gen_data.py)。 - 事后才发现的因果词。 「所以」「于是」「因此」各是一条事实陈述,要有出处支持这层因果;只有两件事各自的出处是不够的。脚本定稿后把这几个词单独搜一遍。
- 取回的 README 标题里带 emoji。 回显原样保存,生成画面数据时剥掉,否则
check.style.no_emoji在data.js上报错。 - 锚点摘的行数要够。 按正则定位、只摘一两行的引文,被引的词可能在下一行;场景里的断言会报「引文不符」。加宽行数,或为那个词单独立一条锚点。