video-storytelling

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Video storytelling

视频叙事

How to build the visual layer of a long-form video so a viewer is still oriented at minute nine. Applies to any talking-head video with a script: a news take, a single-topic deep dive, a comparison, a tutorial, a listicle.
The failure this replaces is scene-thinking. A new graphic every few seconds makes the viewer restart comprehension every few seconds, and no individual graphic is at fault, which is why the feedback comes back as the vague "confusing" or "too much going on" rather than as a specific defect.
For a neutral practice beat sheet, read
docs/STORYTELLING-WORKBOOK.md
at the repository root. The reusable design system is in this skill's
reference/design-system.md
; the worked open-loop geometry is in
reference/patterns/wall-of-slots/
. Private source videos are not required.
This is not
hyperframes-video-beats
. That skill places overlay cards on top of a talking head. This one designs the thing the video is about.
如何构建长篇视频的视觉层,让观众到了第九分钟依然能够理解当前内容。适用于任何配有脚本的 talking-head 视频:新闻解读、单一主题深度分析、对比、教程或清单类视频。
本 skill 要取代的是“场景式思维”。每隔几秒就换一张新图,会迫使观众每隔几秒便重新开始理解;而单张图本身并无明显问题,因此反馈通常不会指出某个具体缺陷,只会笼统地说“令人困惑”或“同时发生太多事情”。
若需要一份中性的练习 beat sheet,请阅读仓库根目录的
docs/STORYTELLING-WORKBOOK.md
。可复用的设计系统位于本 skill 的
reference/design-system.md
;open-loop 几何结构的实战示例位于
reference/patterns/wall-of-slots/
。无需私有源视频。
本 skill 不是
hyperframes-video-beats
。后者是在 talking-head 画面上叠加卡片;本 skill 设计的是视频所讲述的对象本身。

1 · The four principles

1 · 四项原则

One persistent world

单一持久世界

The video lives in a single space, and it never resets. Everything the viewer has already seen still exists where they left it, and new material is added to it rather than replacing it. An element, once drawn, is never redrawn, moved, or relabelled.
Everything else in this skill depends on that. The camera has nowhere to go if the space keeps changing, a spotlight means nothing if there is nothing else on screen, and an open loop cannot stay open in a world that resets.
The world also has to be worth staying in, so decide the spatial grammar before building anything and hold it for the whole runtime. In the reference build: left to right is toward the outcome, up is toward a human and is the only warm thing on screen, down is into a system of record. A viewer who learns that in the first two minutes reads the rest of the video for free.
Break the grammar exactly once, deliberately, for meaning. The reference build's only right-to-left move in ten minutes is a resolved ticket reversing back into the queue on the words "reopen rate". It lands without a word because direction had meant something for the previous three minutes.
视频存在于一个单一空间中,并且永不重置。观众已经看到的一切仍留在原处,新内容会被加入其中,而不是替换原有内容。一个元素一旦绘制,就绝不重绘、移动或重新标注。
本 skill 中其他一切都依赖这一点。如果空间不断变化,摄影机就无处可去;如果屏幕上没有其他内容,spotlight 也就失去意义;如果世界会重置,open loop 也不可能持续未闭合。
这个世界也必须值得观众停留,因此应先决定空间语法,再开始构建,并在整个时长中保持一致。在参考实现中:从左到右表示朝结果推进,向上表示朝向人,并且是屏幕上唯一的暖色,向下表示进入记录系统。观众只要在前两分钟学会这套规则,之后就能毫不费力地读懂视频。
为了让意义生效,只刻意打破这套语法一次。参考实现中,整整十分钟唯一一次从右向左的动作,是一个已解决的工单在说到“reopen rate”时反转回队列。由于此前三分钟方向一直具有明确含义,这一动作无需解释便能准确落地。

The camera is the edit

摄影机就是剪辑

Cutting to a new graphic costs the viewer their bearings every time. Moving through a space that stays put costs them nothing. A push-in then reads as "look closer at the part of the thing you already understand", and the pull-back is where the work pays off, because everything built so far is still there.
Work in three altitudes and move between adjacent ones:
AltitudeShowsUsed for
Worldthe whole spaceorienting, payoffs, "here is where we are"
Regionthe subgraph under discussionmost of the runtime
Detailone elementa number, a state change, a decision
Jumping detail to world is disorienting unless it is a deliberate payoff beat, in which case it is the best move available.
Rules that cost a rebuild each to learn:
  • Name framings as exact world windows, not as scale factors. Window boundaries must fall between labels. Specifying a framing as "scale 0.6" slices words in half at the frame edge.
  • Reuse the same window for the same purpose. If the orient shot is a particular window, always use that exact window. Near-identical framings read as drift.
  • Vary duration with distance travelled, not one global preset.
  • Camera and full-frame wipes sequence, never overlap. Concurrent, they guillotine content. Camera first, then wipe.
  • A cover layer goes opaque before the camera moves underneath it. Reversed, part of the move is exposed and reads as a jump cut.
  • No label within 60px of the frame edge. An element may bleed and read as "the world continues"; a word cut mid-glyph reads as a bug. Solve it by placement, not by masking, since a feather cannot catch a blade travelling ~1800 px/s.
  • Two sequential moves on the same subject read as a glitch. A reframe that finishes and THEN a wipe that starts is a zoom, a dead stop, and a second motion — the client calls it "a glitch and a pause". Merge them into ONE gesture: shared symmetric ease (power2.inOut), landing on the same frame, so the panel reads as pushing the footage aside. Related: front-loaded eases (power3/4.out) on a visible footage move front-load ~90% of the travel and crawl to a stop; reserve them for element entrances, where the crawl reads as settling. (Cost a client-flagged revision on the worked example intro.)
切到一张新图,每次都会让观众失去方位感。在保持不动的空间中移动,则不会造成这种成本。推进镜头因此会被理解为“靠近看看你已经理解的那个部分”,而拉远则是整个工作成果兑现的时刻,因为此前构建的一切仍然都在。
在三个 Altitude 层级之间工作,并且只在相邻层级之间移动:
Altitude展示内容用途
World整个空间定位、兑现、“我们在这里”
Region当前讨论的子图大部分视频时长
Detail单个元素数字、状态变化或决策
从 Detail 直接跳到 World 会令人迷失方向,除非它是一个刻意设计的兑现节拍;在这种情况下,它也是最有效的镜头运动。
以下规则每一条都需要付出一次重建的代价才能学会:
  • 将 framing 命名为精确的 world window,而不是缩放系数。 窗口边界必须落在标签之间。将 framing 写成“scale 0.6”会让文字在画面边缘被切成两半。
  • 同一个用途始终复用同一个 window。 如果定位镜头使用某个特定 window,之后就必须始终使用完全相同的 window。几乎相同的 framing 会让人感觉画面在漂移。
  • 根据移动距离调整时长,而不是使用一个全局预设值。
  • 摄影机运动与全屏 wipe 必须前后编排,绝不能重叠。 两者同时发生会像铡刀一样切断内容。应先移动摄影机,再执行 wipe。
  • 摄影机在 cover layer 下方移动前,cover layer 必须先变为不透明。 顺序相反会露出部分移动过程,看起来像 jump cut。
  • 任何标签都不得距离画面边缘 60px 以内。 元素可以延伸到画面外,以表达“世界仍在继续”;但文字在字形中间被截断会看起来像 bug。应通过布局解决,而不是使用遮罩,因为羽化效果追不上以约 1800 px/s 移动的刀锋。
  • 同一主体上连续进行两次运动会看起来像 glitch。 先完成一次 reframe、然后再开始 wipe,会产生 zoom、dead stop 和第二次运动,客户会将其称为“glitch 和停顿”。应将二者合并为一个动作:使用共享的对称 easing(power2.inOut),落在同一帧上,使 panel 看起来是在推开 footage。相关地:对可见 footage 的移动使用 front-loaded easing(power3/4.out)会让约 90% 的位移集中在开始,随后缓慢爬行至停止;这种 easing 只保留给元素入场,因为此时爬行可被理解为逐渐落定。(在工作示例的 intro 中,此问题曾导致一次客户标记的返工。)

Attention is a spotlight

注意力就是 spotlight

Exactly one element at full brightness at any moment. Everything else already drawn stays quiet but legible. Not optional. This is the rule that lets a world accrete to a dozen elements without ever feeling busy, and it is what makes the pull-back readable rather than overwhelming.
Hierarchy comes from the RATIO between active and quiet, never from making everything dark. The first prototype of the reference build had a brightest pixel of 101/255 and was unreadable at 960x540; the fix was brighter ink, not more dim.
StateTarget luma (Y, 16-235)Opacity in the reference
Active stroke + label200-2351.0
Context, drawn and still relevant120-1500.56
Spent, said its piece85-1050.33
Any label, any statenever below 90
Implementation is one function, called at every narration beat with the ids being talked about (
build_s01.py
,
lit()
):
python
def lit(t, active, warm=(), dur=0.5, floor=FLOOR):
    active = list(active) + [g + "-l" for g in active if g + "-l" in order]
    for gid in order:
        if gid not in opac:      # never drawn: nothing to light
            continue
        tgt = 1.0 if gid in active else (WARM if gid in warm else floor)
        if gid not in active:
            tgt = min(tgt, MUTED.get(gid, 1.0))
        ...
Three details in there are load-bearing:
  • A label always inherits its element's state (the
    -l
    suffix fold-in). A lit element with an unreadable label under it is the exact defect being fixed.
  • A prop that is finished retires to opacity 0, via a
    MUTED
    cap, not to 0.3. Muting to 0.3 leaves unexplained confetti on screen for the rest of the section. Generator rule: emit the
    mute()
    before any later-starting
    lit()
    , or the mute is overwritten when its tween completes.
  • Opacity events must be emitted in timeline order or the tracker sees stale state and computes the wrong target. Keep a
    seq()
    guard that raises on out-of-order emission.
  • One opacity authority per element per window. Two simultaneously-active tweens driving the same element's opacity to different values render smoothly in a standalone section but strobe at ~10Hz in the assembled master, where the winning tween alternates per frame. The reference build shipped this defect at every dock. When a sweep like
    lit()
    overlaps an explicit brighten or mute, the later-starting tween is the sole driver and the sweep skips those ids (
    lit(..., skip=(...))
    ). Gate it: scan assembled renders for one-frame out-and-back luminance pops in every transition window (
    qa/v4/flicker/qa-flicker.mjs
    in the reference build).
任何时刻,恰好一个元素保持全亮度。已绘制的其他元素保持安静,但仍然清晰可辨。 这不是可选项。正是这条规则让世界可以逐步累积到十几个元素却始终不显拥挤,也让拉远镜头清晰可读,而不是令人不知所措。
层级来自 active 与 quiet 之间的比例,绝不能通过把一切都调暗来制造。参考实现的第一个原型中,最亮像素为 101/255,在 960x540 下无法阅读;修复方法是提高墨色亮度,而不是增加暗化程度。
状态目标 luma(Y,16-235)参考实现中的 opacity
Active stroke + label200-2351.0
Context,已绘制且仍然相关120-1500.56
Spent,已经完成其表达85-1050.33
任意标签、任意状态绝不低于 90
实现只需一个函数,在每个 narration beat 调用,并传入正在讨论的 ids(
build_s01.py
中的
lit()
):
python
def lit(t, active, warm=(), dur=0.5, floor=FLOOR):
    active = list(active) + [g + "-l" for g in active if g + "-l" in order]
    for gid in order:
        if gid not in opac:      # never drawn: nothing to light
            continue
        tgt = 1.0 if gid in active else (WARM if gid in warm else floor)
        if gid not in active:
            tgt = min(tgt, MUTED.get(gid, 1.0))
        ...
其中有三个关键细节承载着核心功能:
  • 标签始终继承其元素的状态,也就是
    -l
    后缀的 fold-in。亮起的元素下方却有一块无法阅读的标签,正是当前要修复的缺陷。
  • 已经完成的 prop 应退场至 opacity 0,通过
    MUTED
    上限实现,而不是停在 0.3。淡化到 0.3 会让无法解释的彩纸碎片在后续整个章节中留在屏幕上。Generator 规则:必须先 emit 后续
    lit()
    之前开始的
    mute()
    ,否则 tween 完成时 mute 会被覆盖。
  • Opacity 事件必须按 timeline 顺序 emit,否则 tracker 会看到过期状态并计算出错误的目标值。保留一个
    seq()
    guard,在乱序 emit 时抛出异常。
  • 每个 window 内,每个元素只能有一个 opacity 权威来源。 两个同时 active 的 tween 驱动同一元素产生不同 opacity 时,在独立 section 中看起来仍然平滑,但在合成 master 中会以约 10Hz 频闪,因为获胜的 tween 会逐帧切换。参考实现曾在每个 dock 都带上了这一缺陷。当
    lit()
    这样的 sweep 与显式 brighten 或 mute 重叠时,后开始的 tween 应成为唯一驱动源,而 sweep 跳过这些 ids(
    lit(..., skip=(...))
    )。应设置 gate:在合成 render 中扫描每个 transition window 是否存在单帧的亮度回跳 pop(参考实现中的
    qa/v4/flicker/qa-flicker.mjs
    )。

Open loops are spatial

Open loop 是空间化的

Never say "we'll come back to this." Leave something visibly incomplete in the world and return to it. A promise the viewer can see is a promise they can feel outstanding.
The mechanic is identical whatever the shape:
  1. Plant it in the hook, in the space, on the line that makes the promise.
  2. Re-show it at every boundary. Any section handoff is a boundary. The re-show costs about two seconds and is the retention mechanic; skipping it is how a payoff ends up paying off nothing.
  3. Advance it visibly. The viewer must see the incompleteness shrink.
  4. Close on the image you opened on.
Pick the shape from the script:
Script shapeThe worldWhat is incomplete
N parallel itemsN sockets on a spineempty slots (see
reference/patterns/wall-of-slots/
)
One process, deepthe full pipeline, laid outa broken or missing link
Levels or tiersa stack that builds upwardthe top of the stack is a ghost
Comparisontwo columns on one shared axisone column empty, or an unfilled scorecard
News or announcementa timeline, or a landscape mapthe right side, the future, is blank
Tutorial or build-alongthe finished architecture, ghostedevery node not yet built
Handing an item to its slot is a live camera move, ~1.4s, never a cut to a pre-made picture of the filled state. The viewer watches the thing they just learned become the tile. If the video has a numeral or counter, roll it inside that same move, as an odometer, with the same start, duration and ease for both digits or they merge into one band mid-roll.
The counter needs a name, not just a number. A corner numeral tells the viewer where they are in the sequence but not what the current item is; nine minutes in, "03" carries nothing. Pair the numeral with a 1-2 word label naming the current item (SUPPORT, VOICE AI), in the same plate chemistry, moving as one rigid fixture with the numeral, swapping inside the same odometer roll. Client review of the reference build asked for exactly this: the visuals were good and the viewer still lost track of which use case was on screen.
永远不要说“我们稍后回来讲这个”。应在世界空间中留下一个明显未完成的内容,然后再回来。观众能看见的承诺,才会让人感到它仍未解决。
无论采用何种形态,其机制都相同:
  1. 在 hook 中种下它,把它放在空间中,并放在作出承诺的那句话上。
  2. 在每个边界重新展示它。 任何 section handoff 都是边界。重新展示大约需要两秒,却正是 retention 机制;跳过它会让最终兑现变得毫无意义。
  3. 让它取得可见进展。 观众必须看到未完成的部分逐渐缩小。
  4. 在开场时打开的那张画面上收尾。
根据脚本选择形态:
脚本形态世界未完成的部分
N 个并行项目主干上的 N 个插槽空插槽(见
reference/patterns/wall-of-slots/
)
一个深度流程展开的完整 pipeline断裂或缺失的连接
层级或阶段向上构建的 stackstack 顶端的 ghost
对比共用轴线上的两列其中一列为空,或 scorecard 尚未填写
新闻或公告timeline,或横向地图右侧的未来区域为空白
教程或逐步构建以 ghost 状态呈现的完整 architecture尚未构建的每个节点
将一个项目交接到其 slot 应通过实时摄影机移动完成,约 1.4 秒,绝不能切成一张预先制作的已填充状态图片。观众要亲眼看到刚学会的东西变成 tile。如果视频中有数字或计数器,应在同一次移动中以 odometer 的方式滚动,两个 digit 必须使用相同的起点、时长和 easing,否则滚动到中途就会合并成一条光带。
计数器需要一个名称,而不只是数字。 角落里的数字可以告诉观众当前处于序列的哪个位置,却无法说明当前项目是什么;到了第九分钟,“03”已经不再承载任何信息。应将数字与一个 1 至 2 个词的标签配对,用它命名当前项目,例如 SUPPORT、VOICE AI。数字与标签应使用相同的 plate chemistry,作为一个刚性 fixture 一起移动,并在同一次 odometer roll 中完成切换。参考实现的客户评审明确提出了这一点:视觉效果很好,但观众仍然不知道屏幕上展示的是哪个 use case。

2 · Discipline

2 · 纪律

If it doesn't get a word, it doesn't get drawn

没有被讲到的内容,就不要绘制

Every shape is a labelled element, a connector between labelled elements, the travelling subject, or the ambient ground. Nothing else.
This came from a mute-test review that scored comprehension 7/10 and named the cause: the diagram was clean, the decorations were not. A dozen unlabeled shapes drawn in the same weight and palette as the meaningful ones, so a first-time viewer cannot tell signal from texture and spends real attention decoding things that were never meant to be decoded. That is "too much going on" relocated from words into shapes.
Budget: ~20 on-screen words per section. No sentences. Labels are 1-2 words, under their element. The reference section 1 is 21 words in 84 seconds.
Corollary: an anti-pattern visual never overlays the recommendation. Take the world to near-black first, or put the counter-example where the good diagram isn't.
每个形状都必须是带标签的元素、带标签元素之间的连接线、移动中的主体,或环境底图。除此之外别无其他。
这一原则来自一次 mute-test 评审,理解度得分为 7/10,评审指出了原因:diagram 很干净,但装饰并不干净。十几个无标签形状采用了与有效信息相同的线宽和配色,导致首次观看的观众无法区分 signal 与 texture,只能花费真实注意力去解读那些本来就不需要被解读的东西。这只是把“同时发生太多事情”从文字转移到了形状。
预算:每个 section 约 20 个屏幕词语。 不使用句子。标签为 1 至 2 个词,并放在对应元素下方。参考实现的 section 1 在 84 秒内使用了 21 个词。
推论:反例视觉绝不能覆盖建议方案。 应先将世界压暗至接近纯黑,或者将反例放在推荐 diagram 之外。

Give the viewer one thing to follow

只给观众一个可跟随的对象

Carry the story on a single travelling subject whose state visibly changes: a chip with data rows and a score bar, a document, a request. Rows fill, a bar moves, a border changes colour. A viewer must be able to follow an entire section by watching that one object, and it must never teleport; it moves along drawn connectors at a constant speed.
用一个移动主体承载整个故事,并让它的状态发生可见变化:可以是带数据行和评分条的 chip、文档或请求。数据行逐渐填满,评分条发生移动,边框改变颜色。观众必须能够通过观察这一个对象跟随整个 section,而且它绝不能瞬移;它应沿已绘制的连接线以恒定速度移动。

One global token set

使用一套全局 token

Full detail in
reference/design-system.md
.
TokenValues
Stroke1.5 / 2.5 / 4 / 7, and nothing else
Type22 / 30 / 42 / 60 / 84
Radius10 / 22 / 40, always superellipse(2)
Five colours, five fixed meanings, zero decoration. Warm means a human is involved and nothing else is ever warm. Never pure white for type; it halates on dark.
The failure this prevents, from the craft review of the first build: "every decision was made locally and correctly; no decision was made globally." 18 stroke weights and 9 type sizes, most of them 0.2 apart, individually invisible and collectively fatal. The eye gets noise instead of hierarchy. Gate it with a token linter that fails the build.
完整细节见
reference/design-system.md
。
Token数值
Stroke(描边)1.5 / 2.5 / 4 / 7,且不得有其他值
Type(字号)22 / 30 / 42 / 60 / 84
Radius(圆角)10 / 22 / 40,始终使用 superellipse(2)
五种颜色、五种固定含义、零装饰。暖色只表示有人参与,其他任何元素都不得使用暖色。排版文字绝不使用纯白色,因为它在深色背景上会产生光晕。
这一原则防止了第一次构建的 craft review 中出现的问题:“每个决策都在局部正确,却没有任何决策在全局层面正确。”18 种 stroke weight 和 9 种 type size,其中多数只相差 0.2;单独看几乎无差别,合在一起却会造成致命问题。眼睛得到的不是层级,而是噪声。应使用一个会在构建失败时终止流程的 token linter 来把守这一规则。

Motion

Motion

Entrances on
cubic-bezier(0.16,1,0.3,1)
, exits faster than entrances, duration varies with mass so a 290px node and a 40px label do not decelerate identically, and
back.out
exactly once per section, on the beat that deserves it.
No cross-fade, ever. Full-bleed layers enter and exit by clip on an already opaque background. At any instant a pixel is either graphic or footage.
入场使用
cubic-bezier(0.16,1,0.3,1)
,退场比入场更快,时长根据元素的 mass 变化,因此 290px 的节点与 40px 的标签不会以完全相同的方式减速。每个 section 中,
back.out
只能使用一次,并且只用在真正值得强调的节拍上。
绝不使用 cross-fade。 全屏图层应通过 clip 在已经完全不透明的背景上入场和退场。任何一个像素在同一时刻只能是 graphic 或 footage。

3 · Build contract

3 · 构建契约

  • One generator per section.
    scripts/build_<id>.py
    writes only
    compositions/<id>.html
    . Edit the generator, never the generated HTML. Section roots
    index-*.html
    are hand-maintained, so framing changes go there.
  • Shared modules stay shared. Anything that defines shared geometry is imported by every section. If sections are built by parallel agents, forbid them from touching shared modules; divergent geometry silently breaks every handoff.
  • Namespace the SVG defs before assembly. Each section defines its own
    gEdge
    ,
    shadow
    ,
    edgeFade
    . In one assembled DOM
    url(#id)
    resolves to the first match in document order, so once an earlier composition unmounts, every later graphic paints nothing. Cost in the reference build: 35 seconds of empty screen with every gate green. Only looking at the assembled render caught it.
  • Time everything to word starts from a word-level transcript, entering 0.15-0.25s ahead of the anchor word. Never divide a section evenly into beats.
  • Audit gaps. Any stretch over ~1.5s with nothing happening is a dead screen; have the generator print them.
  • Leave air in the cut. Aggressive pause-capping produces near-zero gaps at sentence boundaries that the client hears as "stutters" and jump-cuts. Calibrate against the speaker's own norm: flag sentence-boundary gaps that are ~10x tighter than their median (the reference speaker's was 470ms; the flagged junctions sat at 20-40ms), and repair by inserting 4-8 frames of air, words never touched. Air is a donor slice of the track's real room tone with 10ms fades, never
    anullsrc
    : digital silence against a -50dBFS floor is an audible dropout. Video holds the frame; between sentences a talking head is near-still and 4-8 frames of hold is invisible. Insertion-only changes shift the transcript deterministically, so no re-transcription is needed. An insert's edges must respect the speech envelope: butting donor tone against a word tail in 1ms amputates a decay that naturally takes 60-120ms, and the client hears the cliff as a stutter. Splice with a 40ms equal-power crossfade into the tail's own decay, never a hard butt.
  • Baked times mean scripted shifts. Generators bake hundreds of proto-time literals, and section offsets, seam tables, and root tweens all hold absolute times. Any timeline change after graphics exist (even a 2s air pass) must go through a reviewed AST/parse-level transform that shifts every literal after each edit point, then a diff review, never hand edits. Derive every shift from the canonical edit list itself; the reference pass caught a 2-frame error in its own audit's precomputed offsets by re-deriving.
  • --workers 1
    on render. Parallel workers make the video layer paint black.
  • 每个 section 使用一个 generator。
    scripts/build_<id>.py
    只写入
    compositions/<id>.html
    。应编辑 generator,绝不编辑生成的 HTML。Section 根文件
    index-*.html
    由手工维护,因此 framing 调整应放在这些文件中。
  • 共享模块必须保持共享。 任何定义共享几何结构的内容都由每个 section 导入。如果多个 section 由并行 agents 构建,应禁止它们修改共享模块;几何结构一旦分化,所有 handoff 都会被悄然破坏。
  • 组装前必须为 SVG defs 添加 namespace。 每个 section 都要定义自己的
    gEdge
    、
    shadow
    、
    edgeFade
    。在一个组装后的 DOM 中,
    url(#id)
    会解析到 document order 中第一个匹配项,因此当前面的 composition 卸载后,后续每个 graphic 都会绘制为空白。参考实现中曾因此出现 35 秒空屏,而且所有 gate 都是绿色。只有查看组装后的 render 才能发现它。
  • 所有元素都要以词级 transcript 中的词起点为基准进行计时,并在锚点词之前 0.15 至 0.25 秒入场。绝不能将一个 section 平均划分成若干节拍。
  • 审计空白间隔。 任何超过约 1.5 秒且没有事情发生的区段都是死画面;应由 generator 将其打印出来。
  • 在剪辑中留出 air。 激进的 pause capping 会让句子边界处出现接近零的间隔,客户会将其听成“结巴”和 jump cut。应以说话者自身的正常节奏进行校准:标记那些比中位数紧约 10 倍的句子边界间隔(参考说话者的中位数为 470ms,被标记的连接处为 20 至 40ms),并通过插入 4 至 8 帧 air 进行修复,词本身绝不能改动。Air 是取自音轨真实 room tone 的供体片段,并带有 10ms fade,绝不能使用
    anullsrc
    ;相对于 -50dBFS 的底噪,数字静音会形成可听见的音频断点。Video 会保持画面帧不变;句子之间的 talking-head 几乎静止,插入 4 至 8 帧停顿在视觉上不可见。仅插入的变更会以确定性方式平移 transcript,因此无需重新转录。插入片段的边缘必须尊重 speech envelope:如果以 1ms 的硬切把供体 room tone 接到词尾,就会截断自然需要 60 至 120ms 才能衰减的声音尾音,客户会把这种断崖听成结巴。应使用 40ms equal-power crossfade,将供体片段接入词尾自身的衰减过程,绝不能硬切。
  • Baked 时间意味着脚本化的时间平移。 Generator 会 bake 数百个 proto-time literal,而 section offset、seam table 和 root tween 都保存绝对时间。Graphic 已经存在后,任何 timeline 变更,即使只是一次 2 秒的 air pass,也必须经过经审阅的 AST/parse-level transform,由它平移每个编辑点之后的所有 literal,然后再进行 diff review,绝不能手动修改。所有时间平移都应从 canonical edit list 本身推导;参考实现中的处理流程通过重新推导,发现了自身 audit 预计算 offset 中存在的一个 2 帧误差。
  • Render 时使用
    --workers 1
    。
    并行 workers 会让 video layer 绘制为纯黑。

4 · Gates

4 · Gates

Run all of them, then look anyway.
bash
node scripts/qa-tokens.mjs          # 0 off-system strokes / type sizes / radii
node scripts/qa-no-crossfade.mjs    # 0 full-bleed layers animating opacity
node scripts/qa-legibility.mjs <render>
node scripts/qa-seamjump.mjs <render> plan/cover-seams.json --control <cut>
node scripts/qa-deadframe.mjs <render> --control <cut>
node scripts/qa-presence.mjs <render> --every 1      # speaker on-screen share
node scripts/integrate.mjs --check                   # 0 gaps, 0 stale mounts
npx hyperframes lint
Gates do not replace looking, and gates that have never failed are not gates.
  • Motion smoothness is verified on contiguous frames, never on beat frames. A single frame per beat proves composition; every motion defect — a jump into a move, a crawl to a dead stop before a second move, a freeze, a one-frame pop — lives between beat frames and survives every other gate. Before any delivery, tile a contiguous strip across EVERY transition window (camera move, wipe, reframe, dock, section seam) and read the strips:
    bash
    ffmpeg -i render.mp4 -vf "select='between(n,A,B)',scale=480:270,tile=layout=6x7" \
      -frames:v 1 tile-<window>.png
    Defect signatures to read for: adjacent tiles with a large jump then several near-identical tiles (front-loaded ease / dead stop); motion that halts and a different motion that then begins (two moves that should be one gesture); any single tile unlike both neighbours (pop). The worked example intro shipped a zoom-freeze glitch that passed tokens, cross-fade, legibility, dead-gap and per-beat frame reads; the client caught it in one viewing. This class recurs — treat this strip-read as load-bearing, not optional.
  • Legibility is not automatable. Absolute pixel-share thresholds measure text volume, not legibility; a wordy build scores higher than a sparse one. Read a 960x540 downscale of every major beat by eye.
  • Validate every gate against a control known to trip it. A seam gate that reports zero on a broken build is worse than no gate.
  • Verify the assembled artifact, never section prototypes. Most of the bugs that survived this build's gates existed only in the assembly.
  • Audit audio at the acoustic level, not the transcript level. A transcript derived from the pre-edit source is structurally blind to artifacts the edit created, and a text sweep over it will report "clean" while the client hears stutters. What actually finds them, in yield order from the reference build: a whole-track dead-span scan (runs of digital-zero samples inside pauses whose real floor is ~-70dBFS read as dead holes; fill with room tone), 1ms-resolution envelope forensics at every junction edge (a sample-derivative click check passes an envelope cliff that the ear flags instantly), and a fresh ASR pass on the delivered audio diffed against the expected text. Phoneme-repeat autocorrelation found nothing: what clients call "stutters" are usually truncation cliffs and dead holes, not doubled words.
Presence: aim for ~40% speaker-on-screen across a graphics-led video. Long absences are fine at payoff beats that need full width; audit them rather than capping them.
运行全部 gate,然后仍然要亲自查看。
bash
node scripts/qa-tokens.mjs          # 0 off-system strokes / type sizes / radii
node scripts/qa-no-crossfade.mjs    # 0 full-bleed layers animating opacity
node scripts/qa-legibility.mjs <render>
node scripts/qa-seamjump.mjs <render> plan/cover-seams.json --control <cut>
node scripts/qa-deadframe.mjs <render> --control <cut>
node scripts/qa-presence.mjs <render> --every 1      # speaker on-screen share
node scripts/integrate.mjs --check                   # 0 gaps, 0 stale mounts
npx hyperframes lint
Gate 无法取代亲眼检查,而从未失败过的 gate 也不是 gate。
  • Motion 平滑度必须在连续帧上验证,绝不能只在节拍帧上验证。 每个节拍只取一帧只能证明 composition;所有 motion 缺陷——跳进一次移动、在第二次移动前爬行至 dead stop、冻结、单帧 pop——都存在于节拍帧之间,并会通过其他所有 gate。交付前,应在每个 transition window(camera move、wipe、reframe、dock、section seam)上铺设一条连续帧 strip,并逐一阅读:
    bash
    ffmpeg -i render.mp4 -vf "select='between(n,A,B)',scale=480:270,tile=layout=6x7" \
      -frames:v 1 tile-<window>.png
    需要识别的缺陷特征包括:相邻 tile 先出现大幅跳变,随后数个 tile 几乎完全相同(front-loaded ease / dead stop);motion 中途停止,随后开始另一种不同的 motion(两次本应合并的移动);某个单独 tile 与两侧邻居均不相同(pop)。工作示例的 intro 曾交付一个 zoom-freeze glitch,它通过了 token、cross-fade、legibility、dead-gap 以及逐节拍帧检查,却由客户在一次观看中发现。这类问题会反复出现,因此应将这种 strip-read 视为必需环节,而不是可选项。
  • 可读性无法自动化。 绝对像素占比阈值衡量的是文字数量,而不是可读性;文字过多的构建得分反而会高于稀疏构建。应以肉眼阅读每个主要节拍缩放到 960x540 后的画面。
  • 必须使用一个已知会触发该问题的 control 来验证每个 gate。 一个在损坏构建上仍报告零问题的 seam gate,比没有 gate 更糟糕。
  • 必须验证组装后的 artifact,绝不能只验证 section 原型。 穿过这些 gate 存活下来的大多数 bug,只存在于组装结果中。
  • 必须在声学层面审计音频,而不能只在 transcript 层面审计。 从编辑前源文件生成的 transcript,在结构上无法发现编辑过程造成的 artifact;对 transcript 做文本扫描会报告“clean”,而客户却能听到结巴。参考实现中,真正能够发现它们的方法按产出顺序排列如下:首先对整条音轨进行 dead-span 扫描,查找暂停区间中的 digital-zero sample 串;这些位置的真实底噪约为 -70dBFS,却会被听成 dead hole,应使用 room tone 填充。其次,在每个连接边缘进行 1ms 分辨率的 envelope 取证;sample-derivative click check 可能通过,但耳朵会立即察觉 envelope cliff。最后,对交付音频执行新的 ASR,并与预期文本进行 diff。Phoneme-repeat autocorrelation 没有发现任何问题:客户所说的“结巴”通常是截断断崖和 dead hole,而不是重复词。
Presence:对于 graphics-led 视频,目标是 speaker-on-screen 占比约 40%。在需要完整画面宽度的兑现节拍中,长时间不显示 speaker 也没有问题;应对其进行审计,而不是强行设置上限。

5 · Order of work

5 · 工作顺序

  1. Lock the cut and get a word-level transcript. Everything is timed to it.
  2. Choose the world, the spatial grammar, and the travelling subject. Write them down before building.
  3. Choose the open loop from the script shape and build its shared module first. Its geometry is referenced by the hook, every boundary, and the payoff.
  4. Build one section end to end and render it. Prove the system there before touching the others.
  5. Review that section by eye, muted, for ornament and legibility. Fix the system, not the section.
  6. Build the rest against the proven section. Then namespace, assemble, render, gate, and read the assembled render.
Steps 4 and 5 are where the quality comes from. The reference build's entire visual system is the output of reviewing its first section three times.
  1. 锁定 cut,并获取词级 transcript。所有内容都以此计时。
  2. 选择世界、空间语法和移动主体。在开始构建前将它们写下来。
  3. 根据脚本形态选择 open loop,并首先构建它的共享模块。其几何结构会被 hook、每个边界和最终兑现共同引用。
  4. 完整构建一个 section 并进行 render。先在该 section 中证明整个系统,再处理其他 section。
  5. 将该 section 静音后逐帧审阅,检查装饰和可读性。修复系统,而不是只修复这个 section。
  6. 以已经验证的 section 为基准构建其余部分。随后添加 namespace、组装、render、运行 gate,并阅读组装后的 render。
步骤 4 和 5 决定了最终质量。参考实现的完整视觉系统,是对第一个 section 进行三次审阅后产生的成果。