psychopomp

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Psychopomp explainer reels

Psychopomp 讲解短片

An explainer reel is several independently authored Scene Plans joined on one clock: behavior stories on the GPU Stage (orbs, cards, beams, packets, camera, bloom), pinned Callouts, Rolling Numbers, changes as animated Stepped Diffs, captions in a terminal voice, and optional spoken narration. When narrated, every visual beat waits for a phrase in the transcript, so re-voicing re-times the film automatically.
The working examples are in
scenes/pr-walkthrough
:
src/flagship.rs
(#50825 as a Stage film that zooms into its code),
src/stop_stage.rs
(#50042 combining Stage, Callouts, Rolling Number, and a Zoom into an annotated code diff), and
src/lib.rs
(the five-PR reel with sequence diagrams). Copy their shape; do not start from a blank crate. Component payloads, channels, and commands live in
SCENE_PLANS.md
("Make A Narrated Explainer Reel") and terms in
CONTEXT.md
; read both before authoring. Engineering rules are in
AGENTS.md
.
一个讲解短片由多个独立编写的 Scene Plans 在同一时钟上衔接而成:在 GPU Stage 上呈现行为演示(球体、卡片、光束、数据包、摄像机、辉光),固定 Callouts 和 Rolling Numbers,以动画 Stepped Diffs 呈现变更,加入终端风格的字幕,并可选用口头旁白。加入旁白后,每个视觉节拍都会等待转录文本中的一个短语,因此更换配音后,影片会自动重新计时。
可运行的示例位于
scenes/pr-walkthrough
:
src/flagship.rs
(#50825,一部缩放到其代码的 Stage 影片)、
src/stop_stage.rs
(#50042,将 Stage、Callouts、Rolling Number 和 Zoom 组合成带注释的代码 diff),以及
src/lib.rs
(包含五个 PR 和时序图的短片)。复制它们的结构;不要从空白 crate 开始。component payload、channel 和 command 位于
SCENE_PLANS.md
("Make A Narrated Explainer Reel"),术语位于
CONTEXT.md
;创作前务必阅读这两份文档。工程规则位于
AGENTS.md
。

Steps

Steps

  1. Establish the facts. For each change, read the actual diff and PR description (
    gh pr diff
    ,
    gh pr view
    ) and write one sentence each for the broken behavior, the fixed behavior, and the change. Done when every claim you will narrate is traceable to code, a test, or a reproduced run; mark anything you could not verify and keep it out of the script.
  2. Write the script as
    scenes/<name>/narration/script.json
    , one clip per part: intro, then per change
    <slug>-before
    ,
    <slug>-after
    ,
    <slug>-code
    , then outro. Follow STORY.md when writing it. Done when each clip is under about 30 seconds spoken and every planned visual beat has a distinct anchor phrase in it.
  3. Voice it (optional). Draft first with no credentials (
    --draft
    uses macOS
    say
    ), or synthesize final narration with ElevenLabs (
    engine: "elevenlabs"
    , default model
    eleven_v4
    ) or Fish Audio (
    engine: "fish"
    ):
    json
    {
      "engine": "elevenlabs",
      "model": "eleven_v4",
      "voice": "<elevenlabs-voice-id>",
      "settings": { "stability": 0.5, "similarity": 0.7 },
      "clips": [
        { "id": "change-before", "text": "[warm, conversational voice] ..." }
      ]
    }
    sh
    # Draft (no API key required)
    bun scripts/narrate.ts scenes/<name>/narration/script.json --draft
    
    # ElevenLabs (requires ELEVENLABS_API_KEY and script.voice)
    ELEVENLABS_API_KEY=... bun scripts/narrate.ts scenes/<name>/narration/script.json
    
    # Fish Audio (requires FISH_AUDIO_API_KEY)
    FISH_AUDIO_API_KEY=... bun scripts/narrate.ts scenes/<name>/narration/script.json
    scripts/narrate.ts
    normalizes loudness, transcribes word timings with Whisper, and writes
    <id>.mp3
    ,
    <id>.words.json
    , and
    narration.json
    . When switching voices or engines, rebuild the Scene Plan against the new word timings.
    Or declare the narration in the Scene Program with
    psychopomp-media
    (SCENE_PLANS.md, "Declare Narration And Sound"):
    media.say(id, &voice, text)
    ,
    media.dialogue
    ,
    media.sfx
    , and
    audio.derive(Effect::pitch(..))
    , then
    media.finish()?
    . Each line is generated once and recorded in
    media.lock.json
    ; later runs call nothing unless a declaration changed, and ElevenLabs words come from its character alignment with the script's spelling. Check the cost first with
    PSYCHOPOMP_MEDIA=plan cargo run -p <crate>
    (zero API calls), time with
    PSYCHOPOMP_MEDIA=draft
    , and clean up with
    PSYCHOPOMP_MEDIA=prune
    . Move a narrate.ts scene with
    cargo run -p psychopomp-media -- adopt scenes/<name>/narration
    (no regeneration). Done when a plain run reports every resource
    =
    .
  4. Author the Scene Program in
    scenes/<name>
    (the workspace picks it up; add it to
    verify.json
    with a few key times). Use
    psychopomp::score
    (
    SCENE_PLANS.md
    , "Author With The Score DSL") so
    PlanBuilder
    is the only
    mut
    binding and every actor (
    Stage
    ,
    Camera
    ,
    Caption
    ,
    Callout
    ,
    RollingNumber
    ,
    Checklist
    ,
    Meter
    ,
    Bars
    ,
    Subtitles
    ,
    Confetti
    ,
    Terminal
    ,
    Chat
    ,
    ChangedFiles
    ,
    LowerThird
    ,
    Lens
    ) is an immutable value whose methods return composable
    Beat
    s (
    .then
    ,
    .also
    ,
    .with
    ,
    .on_end
    ,
    .after
    ,
    .early
    ,
    stagger
    ,
    each
    ,
    at!(scene, time => ...)
    ). Schedule clips with
    Narration::reading
    , build elements with
    StageElement::card|orb|beam|packet|label|ring
    and relative
    psychopomp::layout::Placement
    envelopes on
    StagePost::RESTRAINED
    , frame with
    Caption::header|chip|footer
    , and play sounds with
    sfx::*.beat(id, gain_db)
    (or
    audio.beat(gain_db)
    for
    psychopomp-media
    ). Load the
    explainer-motion
    skill and apply it to every beat: cards
    settle_in
    , beams
    connect
    , messages
    send
    , replies wait for
    .reply()
    , impacts
    hit
    , the hero
    orb_in
    , the fix's
    rewind
    ; use
    Callout
    to pin annotations to Stage elements or Editor code ranges and
    RollingNumber
    for live counters/timers; drive camera moves through
    stage.camera()
    (
    establish
    ,
    frame
    ,
    follow
    ,
    release
    ,
    orbit
    ,
    whip
    ,
    dolly_zoom
    ). Reach for the visualization and text overlays instead of hand-building them from Stage labels and rings:
    Checklist
    for checks that run and resolve,
    Meter
    (
    MeterPlan::countdown
    ) for a timeout ring,
    Bars
    for before/after numbers,
    Confetti
    for a success beat, and
    Subtitles
    (
    SubtitlesPlan::from_spoken
    ) to burn in word-timed captions. Changes are
    Diff
    s of
    keep
    ,
    add(step)
    , and
    remove(step)
    lines, each step keyed to a phrase. Done when
    cargo run -p <crate>
    writes the reel and
    cargo run --release -- plan validate <reel>
    reports valid. A missing phrase panics with its clip: change the anchor to words the transcript actually contains.
  5. Review before rendering. Get segment spans from
    plan inspect <reel>
    , then contact-sheet each segment at its before, switch, after, and code beats:
    sh
    bun scripts/sheet.ts <reel> t1,t2,... --theme neutral --shutter --out output/sheet.jpg
    When tuning the look, set
    PSYCHOPOMP_SHADER_DIR=crates/psychopomp-render/src/render
    and edit
    stage.wgsl
    /
    stage_post.wgsl
    : every frame and sheet picks up shader edits without a Rust rebuild. Then render one behavior segment with audio (
    plan render <reel> out.mp4 --cue <scene-id>
    ) or a 2–3 second window (
    --range a..b
    ) and inspect frames extracted during motion. Done when every segment has been looked at and no text overlaps, clips, or reads against the wrong chip.
  6. Render and verify the whole reel once, after the sheets and short windows pass. Run it in the foreground with a long timeout and never end a turn while it is still running:
    sh
    cargo run --release -- plan render <reel> output/<name>.mp4 --theme neutral
    Done when
    ffprobe
    shows 1920x1080, 60 fps, AAC audio, and a duration matching
    plan inspect
    , and loudness is near -16 LUFS with peaks under -1 dBFS.
  7. Deliver the MP4 path. Commit the scene, narration assets, and any engine changes in Psychopomp with conventional messages.
  1. 确定事实。 针对每项变更,阅读实际的 diff 和 PR description(
    gh pr diff
    、
    gh pr view
    ),分别用一句话说明故障行为、修复后的行为以及变更内容。完成标准是:你将要旁白陈述的每一项说法都可追溯到代码、测试或复现运行;对于任何无法验证的内容,应予以标记并排除在脚本之外。
  2. 编写脚本,保存为
    scenes/<name>/narration/script.json
    ,每个部分对应一个 clip:先使用
    intro
    ,然后针对每项变更依次使用
    <slug>-before
    、
    <slug>-after
    、
    <slug>-code
    ,最后使用 outro。编写时遵循 STORY.md。完成标准是:每个 clip 的朗读时长约不超过 30 秒,并且每个计划中的视觉节拍都有一段独特的 anchor phrase。
  3. 配音(可选)。 先在没有凭据的情况下生成草稿(
    --draft
    使用 macOS
    say
    ),或使用 ElevenLabs(
    engine: "elevenlabs"
    ,默认模型
    eleven_v4
    )或 Fish Audio(
    engine: "fish"
    )合成最终旁白:
    json
    {
      "engine": "elevenlabs",
      "model": "eleven_v4",
      "voice": "<elevenlabs-voice-id>",
      "settings": { "stability": 0.5, "similarity": 0.7 },
      "clips": [
        { "id": "change-before", "text": "[warm, conversational voice] ..." }
      ]
    }
    sh
    # Draft (no API key required)
    bun scripts/narrate.ts scenes/<name>/narration/script.json --draft
    
    # ElevenLabs (requires ELEVENLABS_API_KEY and script.voice)
    ELEVENLABS_API_KEY=... bun scripts/narrate.ts scenes/<name>/narration/script.json
    
    # Fish Audio (requires FISH_AUDIO_API_KEY)
    FISH_AUDIO_API_KEY=... bun scripts/narrate.ts scenes/<name>/narration/script.json
    scripts/narrate.ts
    会标准化响度,使用 Whisper 转录单词时序,并写入
    <id>.mp3
    、
    <id>.words.json
    和
    narration.json
    。更换声音或引擎时,需要根据新的单词时序重建 Scene Plan。
    或者在 Scene Program 中声明旁白,使用
    psychopomp-media
    (SCENE_PLANS.md,"Declare Narration And Sound"):
    media.say(id, &voice, text)
    、
    media.dialogue
    、
    media.sfx
    和
    audio.derive(Effect::pitch(..))
    ,然后调用
    media.finish()?
    。每条内容只生成一次并记录在
    media.lock.json
    中;除非声明发生变化,后续运行不会执行任何调用,ElevenLabs 的单词时序来自其 character alignment,并与脚本中的拼写对应。先使用
    PSYCHOPOMP_MEDIA=plan cargo run -p <crate>
    检查成本(零 API 调用),使用
    PSYCHOPOMP_MEDIA=draft
    计时,再使用
    PSYCHOPOMP_MEDIA=prune
    清理。使用
    cargo run -p psychopomp-media -- adopt scenes/<name>/narration
    迁移 narrate.ts scene(不重新生成)。完成标准:普通运行会报告每个资源均为
    =
    。
  4. 在
    scenes/<name>
    中编写 Scene Program
    (workspace 会自动识别;将其加入
    verify.json
    ,并附上几个关键时刻)。使用
    psychopomp::score
    (SCENE_PLANS.md,"Author With The Score DSL"),使
    PlanBuilder
    成为唯一带
    mut
    的绑定,并且每个 actor(
    Stage
    、
    Camera
    、
    Caption
    、
    Callout
    、
    RollingNumber
    、
    Checklist
    、
    Meter
    、
    Bars
    、
    Subtitles
    、
    Confetti
    、
    Terminal
    、
    Chat
    、
    ChangedFiles
    、
    LowerThird
    、
    Lens
    )都是不可变值,其方法返回可组合的 Beat(
    .then
    、
    .also
    、
    .with
    、
    .on_end
    、
    .after
    、
    .early
    、
    stagger
    、
    each
    、
    at!(scene, time => ...)
    )。使用
    Narration::reading
    安排 clip,使用
    StageElement::card|orb|beam|packet|label|ring
    构建元素,在
    StagePost::RESTRAINED
    上应用相对
    psychopomp::layout::Placement
    envelope,使用
    Caption::header|chip|footer
    设置画面框架,并通过
    sfx::*.beat(id, gain_db)
    播放声音(或对
    psychopomp-media
    使用
    audio.beat(gain_db)
    )。
    加载
    explainer-motion
    skill,并将其应用于每个 beat:卡片使用
    settle_in
    ,光束使用
    connect
    ,消息使用
    send
    ,回复等待
    .reply()
    ,冲击使用
    hit
    ,主角使用
    orb_in
    ,修复使用
    rewind
    ;使用
    Callout
    将标注固定到 Stage element 或 Editor code range,使用
    RollingNumber
    显示实时计数器或计时器;通过
    stage.camera()
    驱动摄像机移动(
    establish
    、
    frame
    、
    follow
    、
    release
    、
    orbit
    、
    whip
    、
    dolly_zoom
    )。应优先使用可视化和文字叠加层,而不是通过 Stage label 和 ring 手工构建它们:使用
    Checklist
    呈现执行并完成的检查项,使用
    Meter
    (
    MeterPlan::countdown
    )呈现超时环,使用
    Bars
    呈现前后数字,使用
    Confetti
    呈现成功节拍,使用
    Subtitles
    (
    SubtitlesPlan::from_spoken
    )烧录按词计时的字幕。变更由
    Diff
    表示,其中包含
    keep
    、
    add(step)
    和
    remove(step)
    行,每个 step 都以短语为键。完成标准:
    cargo run -p <crate>
    写出 reel,且
    cargo run --release -- plan validate <reel>
    报告有效。如果缺少短语,程序会连同其 clip 触发 panic:请把 anchor 改为转录文本中确实包含的词。
  5. 渲染前审查。 从
    plan inspect <reel>
    获取 segment span,然后在每个 segment 的 before、switch、after 和 code beat 处,为各 segment 生成 contact sheet:
    sh
    bun scripts/sheet.ts <reel> t1,t2,... --theme neutral --shutter --out output/sheet.jpg
    调整外观时,设置
    PSYCHOPOMP_SHADER_DIR=crates/psychopomp-render/src/render
    并编辑
    stage.wgsl
    /
    stage_post.wgsl
    :每一帧和 sheet 都会应用 shader 修改,无需重新构建 Rust。
    然后渲染一个带音频的 behavior segment(
    plan render <reel> out.mp4 --cue <scene-id>
    ),或渲染一个 2–3 秒窗口(
    --range a..b
    ),并检查在运动过程中提取的帧。完成标准:每个 segment 都已检查过,没有文字重叠或被裁切,也不会落在错误的 chip 上。
  6. 完整渲染并验证一次整个 reel,且仅在 contact sheet 和短窗口全部通过后进行。使用较长的 timeout 在前台运行,且仍在运行期间绝不能结束当前 turn:
    sh
    cargo run --release -- plan render <reel> output/<name>.mp4 --theme neutral
    完成标准:
    ffprobe
    显示 1920x1080、60 fps、AAC 音频,时长与
    plan inspect
    一致;响度接近 -16 LUFS,峰值低于 -1 dBFS。
  7. 交付 MP4 路径。 使用符合惯例的 commit message,将 scene、旁白素材以及对 Psychopomp 所做的引擎变更提交到版本库。

Improving Psychopomp

改进 Psychopomp

When a reel needs something the engine cannot do, add it to Psychopomp rather than working around it in one scene: plan types and validation in
crates/psychopomp
, strict-channel preflight in
plan_runtime
, pixels in
render
, GPU-free tests plus an
#[ignore]
GPU test, and the docs
AGENTS.md
asks you to keep current. Run
cargo run --release -- verify baseline
before the change and
verify compare --expect <scenes you changed>
after it to prove every other showroom and film is untouched.
Build from small reusable pieces. Interpolation, easing, curves, shape ports, and connectors belong in
psychopomp::math
(organized like pmndrs
math
: core
lerp
/
remap
/
smoothstep
,
easing
,
curve
,
shapes
,
random
; glam vectors). Extend it instead of inlining math in a renderer or scene, and split renderers into one small helper per element, as
render/stage.rs
does. Keep
cargo fmt --check
,
cargo clippy --workspace --all-targets --all-features -- -D warnings
, and
cargo test --workspace
green.
如果某个 reel 需要引擎尚不支持的功能,应将其添加到 Psychopomp,而不是在单个 scene 中绕过:在
crates/psychopomp
中添加 plan 类型和验证,在
plan_runtime
中添加 strict-channel preflight,在
render
中添加 pixels,添加无需 GPU 的测试和一个带
#[ignore]
的 GPU 测试,并按照
AGENTS.md
的要求保持文档为最新状态。变更前运行
cargo run --release -- verify baseline
,变更后运行
verify compare --expect <scenes you changed>
,以证明其他每个 showroom 和 film 均未受影响。
应使用小型可复用组件进行构建。插值、缓动、曲线、形状端口和连接器应归入
psychopomp::math
(其组织方式类似 pmndrs
math
:核心
lerp
/
remap
/
smoothstep
、
easing
、
curve
、
shapes
、
random
;glam vectors)。应扩展该模块,而不是在 renderer 或 scene 中内联数学逻辑;并将 renderer 拆分为每个 element 一个小型 helper,正如
render/stage.rs
所做。保持
cargo fmt --check
、
cargo clippy --workspace --all-targets --all-features -- -D warnings
和
cargo test --workspace
全部通过。